cdk-local 0.118.1 → 0.120.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/README.md +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +126 -3
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -2
- package/dist/{local-studio-Downi4pd.js → local-studio--ndMb9dM.js} +876 -56
- package/dist/local-studio--ndMb9dM.js.map +1 -0
- package/dist/{local-studio-A0ZCH8Nu.d.ts → local-studio-tM4meHDb.d.ts} +53 -3
- package/dist/local-studio-tM4meHDb.d.ts.map +1 -0
- package/package.json +4 -1
- package/dist/local-studio-A0ZCH8Nu.d.ts.map +0 -1
- package/dist/local-studio-Downi4pd.js.map +0 -1
|
@@ -34,6 +34,9 @@ import { SignatureV4 } from "@smithy/signature-v4";
|
|
|
34
34
|
import graphlib from "graphlib";
|
|
35
35
|
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
36
36
|
import * as vm from "node:vm";
|
|
37
|
+
import "@aws-sdk/signature-v4a";
|
|
38
|
+
import { CloudFrontClient, paginateListKeyValueStores } from "@aws-sdk/client-cloudfront";
|
|
39
|
+
import { CloudFrontKeyValueStoreClient, GetKeyCommand, ResourceNotFoundException } from "@aws-sdk/client-cloudfront-keyvaluestore";
|
|
37
40
|
import { EventEmitter } from "node:events";
|
|
38
41
|
|
|
39
42
|
//#region src/cli/options.ts
|
|
@@ -23887,7 +23890,7 @@ function writeRawHttpError(socket, statusCode, message) {
|
|
|
23887
23890
|
if (socket.destroyed) return;
|
|
23888
23891
|
const body = `${message}\n`;
|
|
23889
23892
|
const lines = [
|
|
23890
|
-
`HTTP/1.1 ${statusCode} ${STATUS_TEXT[statusCode] ?? "Error"}`,
|
|
23893
|
+
`HTTP/1.1 ${statusCode} ${STATUS_TEXT$1[statusCode] ?? "Error"}`,
|
|
23891
23894
|
"content-type: text/plain; charset=utf-8",
|
|
23892
23895
|
`content-length: ${Buffer.byteLength(body)}`,
|
|
23893
23896
|
"connection: close",
|
|
@@ -23940,7 +23943,7 @@ function writeRawHttpRedirect(socket, action, req, listenerPort, scheme) {
|
|
|
23940
23943
|
function writeRawHttpFixedResponse(socket, action) {
|
|
23941
23944
|
if (socket.destroyed) return;
|
|
23942
23945
|
const body = action.messageBody ?? "";
|
|
23943
|
-
const statusText = STATUS_TEXT[action.statusCode] ?? "";
|
|
23946
|
+
const statusText = STATUS_TEXT$1[action.statusCode] ?? "";
|
|
23944
23947
|
const contentType = sanitizeRawHeaderValue(action.contentType ?? "text/plain; charset=utf-8");
|
|
23945
23948
|
const lines = [
|
|
23946
23949
|
`HTTP/1.1 ${action.statusCode} ${statusText}`,
|
|
@@ -23960,7 +23963,7 @@ function sanitizeRawHeaderValue(value) {
|
|
|
23960
23963
|
return value.replace(/[\r\n]/g, " ");
|
|
23961
23964
|
}
|
|
23962
23965
|
/** Minimal HTTP/1.1 status text map for the codes the raw writers emit. */
|
|
23963
|
-
const STATUS_TEXT = {
|
|
23966
|
+
const STATUS_TEXT$1 = {
|
|
23964
23967
|
301: "Moved Permanently",
|
|
23965
23968
|
302: "Found",
|
|
23966
23969
|
401: "Unauthorized",
|
|
@@ -28095,6 +28098,92 @@ function addAlbSpecificOptions(cmd) {
|
|
|
28095
28098
|
return cmd.addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls", "Terminate TLS locally for cloud-HTTPS listeners. Default: a cloud-HTTPS listener is served over plain HTTP locally (X-Forwarded-Proto: https is preserved so the upstream app still sees the deployed listener protocol). Implied by --tls-cert / --tls-key. Use this when local-dev cookies need Secure / SameSite=None, when the upstream app inspects TLS metadata, or for mTLS / SNI testing — otherwise plain HTTP is friendlier (no self-signed cert warnings in curl / browser).")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Implies --tls. Must be set together with --tls-key. Pass --tls alone (without --tls-cert / --tls-key) to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Implies --tls. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT this command INJECTS only when an inbound request has none (the default-when-missing role) — `cdkl start-alb` is the local ALB front-door RECEIVING outside-in requests, so this token is the fallback the front-door slots in as Authorization: Bearer <jwt> if the caller did not already supply one. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Cookie pass-through (AWSELBAuthSessionCookie-*) also bypasses the guard. Contrast with `cdkl invoke-agentcore --bearer-token`, where the role is reversed — that command is the outbound client and ALWAYS presents this token (the supplier).")).addOption(new Option("--watch", "Hot-reload: re-synth + per-replica reload of every ECS service behind the ALB when the CDK source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). A per-firing classifier picks the per-replica primitive: source-only edits on interpreted-language handlers (Node/Python/Ruby/shell) take a bind-mount FAST PATH (`docker cp` the new source into each replica + `docker restart`; no rebuild, front-door pool entry unchanged since the IP/port are preserved). Dockerfile / dependency manifest / compiled-language source / ambiguous edits fall through to the rebuild rolling primitive — boot a shadow under a bumped generation suffix, wait for its container port to accept a TCP connection, atomically register it in the front-door pool, then drop the old entry and retire the old container. Either path rolls one replica at a time, so a continuous external request stream against the listener port sees zero connection refusals across the reload. The host front-door (TLS, JWKS cache, Lambda-target containers, listener sockets) stays up across the reload. Lambda target groups behind the ALB are a no-op on reload (the warm RIE container keeps its boot-time image). Off by default; existing replica(s) keep serving when synth fails mid-reload.").default(false));
|
|
28096
28099
|
}
|
|
28097
28100
|
|
|
28101
|
+
//#endregion
|
|
28102
|
+
//#region src/local/cloudfront-kvs.ts
|
|
28103
|
+
/** Build a `cf` module backed by the resolved data sources for one function. */
|
|
28104
|
+
function createCloudFrontModule(sources) {
|
|
28105
|
+
return { kvs(kvsId) {
|
|
28106
|
+
return makeHandle(pickSource(sources, kvsId));
|
|
28107
|
+
} };
|
|
28108
|
+
}
|
|
28109
|
+
/**
|
|
28110
|
+
* The `cf` module bound when a function calls `cf.kvs()` but no KeyValueStore
|
|
28111
|
+
* binding was resolved (no `--from-cfn-stack`, no covering `--kvs-file`). The
|
|
28112
|
+
* handle is returned successfully so a top-level `cf.kvs()` call does not throw
|
|
28113
|
+
* at module-eval; the actionable error surfaces when the function actually
|
|
28114
|
+
* reads a key.
|
|
28115
|
+
*/
|
|
28116
|
+
function createUnboundCloudFrontModule(functionLogicalId) {
|
|
28117
|
+
const fail = () => Promise.reject(/* @__PURE__ */ new Error(`CloudFront Function '${functionLogicalId}' reads from a KeyValueStore (cf.kvs()), but no KeyValueStore binding is available locally. Pass --from-cfn-stack to read the deployed store, or --kvs-file <id>=<file.json> to back it with a local JSON map.`));
|
|
28118
|
+
const handle = {
|
|
28119
|
+
get: () => fail(),
|
|
28120
|
+
exists: () => fail(),
|
|
28121
|
+
meta: () => fail(),
|
|
28122
|
+
count: () => fail()
|
|
28123
|
+
};
|
|
28124
|
+
return { kvs: () => handle };
|
|
28125
|
+
}
|
|
28126
|
+
function pickSource(sources, kvsId) {
|
|
28127
|
+
if (sources.length === 0) throw new Error("cf.kvs(): no KeyValueStore is associated with this function.");
|
|
28128
|
+
if (kvsId === void 0) return sources[0];
|
|
28129
|
+
const byId = sources.find((s) => s.kvsId === kvsId);
|
|
28130
|
+
if (byId) return byId;
|
|
28131
|
+
if (sources.length === 1) return sources[0];
|
|
28132
|
+
throw new Error(`cf.kvs('${kvsId}'): no associated KeyValueStore matches this id locally (associated: ${sources.map((s) => s.kvsId ?? s.label).join(", ")}).`);
|
|
28133
|
+
}
|
|
28134
|
+
function makeHandle(source) {
|
|
28135
|
+
return {
|
|
28136
|
+
async get(key, options) {
|
|
28137
|
+
const value = await source.getValue(key);
|
|
28138
|
+
if (value === void 0) throw new Error(`cf.kvs().get('${key}'): key not found in ${source.label}.`);
|
|
28139
|
+
if (options?.format === "json") try {
|
|
28140
|
+
return JSON.parse(value);
|
|
28141
|
+
} catch (err) {
|
|
28142
|
+
throw new Error(`cf.kvs().get('${key}', { format: 'json' }): value is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
28143
|
+
}
|
|
28144
|
+
return value;
|
|
28145
|
+
},
|
|
28146
|
+
async exists(key) {
|
|
28147
|
+
return await source.getValue(key) !== void 0;
|
|
28148
|
+
},
|
|
28149
|
+
meta() {
|
|
28150
|
+
return Promise.reject(/* @__PURE__ */ new Error("cf.kvs().meta() is not reproduced locally by cdkl start-cloudfront (only get / exists are supported)."));
|
|
28151
|
+
},
|
|
28152
|
+
count() {
|
|
28153
|
+
return Promise.reject(/* @__PURE__ */ new Error("cf.kvs().count() is not reproduced locally by cdkl start-cloudfront (only get / exists are supported)."));
|
|
28154
|
+
}
|
|
28155
|
+
};
|
|
28156
|
+
}
|
|
28157
|
+
/**
|
|
28158
|
+
* Build a {@link KvsDataSource} from a local JSON file (the `--kvs-file
|
|
28159
|
+
* <id>=<file.json>` escape hatch). The file is a flat `{ "key": "value" }`
|
|
28160
|
+
* object; non-string values are JSON-stringified (KVS values are always
|
|
28161
|
+
* strings). Read once at construction (boot time); a `--watch` reload rebuilds
|
|
28162
|
+
* it.
|
|
28163
|
+
*/
|
|
28164
|
+
function createLocalFileKvsDataSource(args) {
|
|
28165
|
+
let raw;
|
|
28166
|
+
try {
|
|
28167
|
+
raw = readFileSync(args.filePath, "utf-8");
|
|
28168
|
+
} catch (err) {
|
|
28169
|
+
throw new Error(`--kvs-file '${args.id}=${args.filePath}': could not read the file: ${err instanceof Error ? err.message : String(err)}`);
|
|
28170
|
+
}
|
|
28171
|
+
let parsed;
|
|
28172
|
+
try {
|
|
28173
|
+
parsed = JSON.parse(raw);
|
|
28174
|
+
} catch (err) {
|
|
28175
|
+
throw new Error(`--kvs-file '${args.id}=${args.filePath}': not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
28176
|
+
}
|
|
28177
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--kvs-file '${args.id}=${args.filePath}': expected a JSON object of key -> value entries.`);
|
|
28178
|
+
const map = /* @__PURE__ */ new Map();
|
|
28179
|
+
for (const [key, value] of Object.entries(parsed)) map.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
28180
|
+
return {
|
|
28181
|
+
label: `file:${args.filePath}`,
|
|
28182
|
+
kvsId: args.id,
|
|
28183
|
+
getValue: (key) => Promise.resolve(map.get(key))
|
|
28184
|
+
};
|
|
28185
|
+
}
|
|
28186
|
+
|
|
28098
28187
|
//#endregion
|
|
28099
28188
|
//#region src/local/cloudfront-function-runtime.ts
|
|
28100
28189
|
/**
|
|
@@ -28109,21 +28198,49 @@ function addAlbSpecificOptions(cmd) {
|
|
|
28109
28198
|
const FUNCTION_TIMEOUT_MS = 5e3;
|
|
28110
28199
|
const EVENT_GLOBAL = "__cfEvent";
|
|
28111
28200
|
/**
|
|
28201
|
+
* Regex matching the canonical CloudFront-Functions 2.0 KeyValueStore import
|
|
28202
|
+
* line (`import cf from 'cloudfront';`). The module is default-imported under a
|
|
28203
|
+
* single binding name; this is the only `cloudfront` import form the runtime
|
|
28204
|
+
* supports. Anchored per-line (`m` flag) so only the import statement is
|
|
28205
|
+
* matched, not a substring elsewhere.
|
|
28206
|
+
*/
|
|
28207
|
+
const CLOUDFRONT_IMPORT_RE = /^[ \t]*import\s+(\w+)\s+from\s+['"]cloudfront['"]\s*;?[ \t]*$/m;
|
|
28208
|
+
/**
|
|
28209
|
+
* Strip the `import cf from 'cloudfront'` statement from a 2.0 function's code
|
|
28210
|
+
* and return the binding name, so the code compiles as a plain `vm.Script`
|
|
28211
|
+
* (an `import` statement is invalid outside an ES module) and the `cloudfront`
|
|
28212
|
+
* module can be injected as the binding-named sandbox global instead. Returns
|
|
28213
|
+
* the code unchanged with no binding name when the function does not import
|
|
28214
|
+
* `cloudfront`. The matched line is blanked (not deleted) so error line numbers
|
|
28215
|
+
* still line up with the source.
|
|
28216
|
+
*/
|
|
28217
|
+
function stripCloudFrontImport(code) {
|
|
28218
|
+
const match = CLOUDFRONT_IMPORT_RE.exec(code);
|
|
28219
|
+
if (!match) return { code };
|
|
28220
|
+
return {
|
|
28221
|
+
code: code.replace(CLOUDFRONT_IMPORT_RE, ""),
|
|
28222
|
+
bindingName: match[1]
|
|
28223
|
+
};
|
|
28224
|
+
}
|
|
28225
|
+
/**
|
|
28112
28226
|
* Compile a CloudFront Function's inline code once. Throws a clear error when
|
|
28113
28227
|
* the code has a syntax error or declares no `handler` (surfaced at boot so a
|
|
28114
28228
|
* malformed function never silently no-ops at request time).
|
|
28115
28229
|
*/
|
|
28116
28230
|
function compileCloudFrontFunction(logicalId, code, runtime) {
|
|
28231
|
+
const { code: source, bindingName } = stripCloudFrontImport(code);
|
|
28117
28232
|
let script;
|
|
28118
28233
|
try {
|
|
28119
|
-
script = new vm.Script(`${
|
|
28234
|
+
script = new vm.Script(`${source}\n;handler(${EVENT_GLOBAL})`, { filename: `cloudfront-function-${logicalId}.js` });
|
|
28120
28235
|
} catch (err) {
|
|
28121
28236
|
throw new Error(`CloudFront Function '${logicalId}' failed to compile: ${err instanceof Error ? err.message : String(err)}`);
|
|
28122
28237
|
}
|
|
28123
28238
|
let hasHandler;
|
|
28124
28239
|
try {
|
|
28125
|
-
const
|
|
28126
|
-
|
|
28240
|
+
const probeGlobals = { console };
|
|
28241
|
+
if (bindingName) probeGlobals[bindingName] = createUnboundCloudFrontModule(logicalId);
|
|
28242
|
+
const probeContext = vm.createContext(probeGlobals);
|
|
28243
|
+
hasHandler = new vm.Script(`${source}\n;typeof handler === 'function'`).runInContext(probeContext, { timeout: FUNCTION_TIMEOUT_MS });
|
|
28127
28244
|
} catch (err) {
|
|
28128
28245
|
throw new Error(`CloudFront Function '${logicalId}' failed to compile: ${err instanceof Error ? err.message : String(err)}`);
|
|
28129
28246
|
}
|
|
@@ -28131,7 +28248,8 @@ function compileCloudFrontFunction(logicalId, code, runtime) {
|
|
|
28131
28248
|
return {
|
|
28132
28249
|
logicalId,
|
|
28133
28250
|
runtime,
|
|
28134
|
-
script
|
|
28251
|
+
script,
|
|
28252
|
+
...bindingName !== void 0 && { cloudfrontBindingName: bindingName }
|
|
28135
28253
|
};
|
|
28136
28254
|
}
|
|
28137
28255
|
/**
|
|
@@ -28142,10 +28260,12 @@ function compileCloudFrontFunction(logicalId, code, runtime) {
|
|
|
28142
28260
|
* thrown by the handler is wrapped with the function's logical id.
|
|
28143
28261
|
*/
|
|
28144
28262
|
async function invokeCloudFrontFunction(fn, event) {
|
|
28145
|
-
const
|
|
28263
|
+
const sandbox = {
|
|
28146
28264
|
console,
|
|
28147
28265
|
[EVENT_GLOBAL]: event
|
|
28148
|
-
}
|
|
28266
|
+
};
|
|
28267
|
+
if (fn.cloudfrontBindingName) sandbox[fn.cloudfrontBindingName] = fn.cloudfrontModule ?? createUnboundCloudFrontModule(fn.logicalId);
|
|
28268
|
+
const context = vm.createContext(sandbox);
|
|
28149
28269
|
let result;
|
|
28150
28270
|
try {
|
|
28151
28271
|
result = fn.script.runInContext(context, { timeout: FUNCTION_TIMEOUT_MS });
|
|
@@ -28282,6 +28402,15 @@ function buildViewerResponseEvent(requestEvent, response) {
|
|
|
28282
28402
|
}
|
|
28283
28403
|
};
|
|
28284
28404
|
}
|
|
28405
|
+
/** Serialize a CloudFront request's `querystring` object back to a raw string. */
|
|
28406
|
+
function serializeCfQueryString(qs) {
|
|
28407
|
+
const parts = [];
|
|
28408
|
+
for (const [key, val] of Object.entries(qs)) {
|
|
28409
|
+
const values = val.multiValue && val.multiValue.length > 0 ? val.multiValue : [{ value: val.value }];
|
|
28410
|
+
for (const v of values) parts.push(v.value === "" ? encodeURIComponent(key) : `${encodeURIComponent(key)}=${encodeURIComponent(v.value)}`);
|
|
28411
|
+
}
|
|
28412
|
+
return parts.join("&");
|
|
28413
|
+
}
|
|
28285
28414
|
function parseQueryStringToCf(raw) {
|
|
28286
28415
|
const out = {};
|
|
28287
28416
|
if (!raw) return out;
|
|
@@ -28340,6 +28469,7 @@ const CLOUDFRONT_FUNCTION_TYPE = "AWS::CloudFront::Function";
|
|
|
28340
28469
|
const S3_BUCKET_TYPE = "AWS::S3::Bucket";
|
|
28341
28470
|
const LAMBDA_URL_TYPE = "AWS::Lambda::Url";
|
|
28342
28471
|
const LAMBDA_FUNCTION_TYPE = "AWS::Lambda::Function";
|
|
28472
|
+
const LAMBDA_VERSION_TYPE = "AWS::Lambda::Version";
|
|
28343
28473
|
/**
|
|
28344
28474
|
* Resolve a distribution logical id within a stack into its routing model.
|
|
28345
28475
|
* `originOverrides` maps an origin id to a local directory (the `--origin`
|
|
@@ -28379,11 +28509,55 @@ function compileDistributionFunctions(template) {
|
|
|
28379
28509
|
continue;
|
|
28380
28510
|
}
|
|
28381
28511
|
const config = props["FunctionConfig"];
|
|
28382
|
-
const
|
|
28383
|
-
|
|
28512
|
+
const configObj = config && typeof config === "object" ? config : {};
|
|
28513
|
+
const compiled = compileCloudFrontFunction(logicalId, code, typeof configObj["Runtime"] === "string" ? configObj["Runtime"] : "cloudfront-js-1.0");
|
|
28514
|
+
const kvsAssociations = extractKvsAssociations(configObj);
|
|
28515
|
+
if (kvsAssociations.length > 0) compiled.kvsAssociations = kvsAssociations;
|
|
28516
|
+
out.set(logicalId, compiled);
|
|
28517
|
+
}
|
|
28518
|
+
return out;
|
|
28519
|
+
}
|
|
28520
|
+
/**
|
|
28521
|
+
* Extract a function's `FunctionConfig.KeyValueStoreAssociations[]` into the
|
|
28522
|
+
* compiled-function carrier, pre-resolving each association's KVS resource
|
|
28523
|
+
* logical id from the `KeyValueStoreARN` intrinsic so the command layer can
|
|
28524
|
+
* bind it (deployed store under `--from-cfn-stack`, or a `--kvs-file` map).
|
|
28525
|
+
*/
|
|
28526
|
+
function extractKvsAssociations(functionConfig) {
|
|
28527
|
+
const raw = Array.isArray(functionConfig["KeyValueStoreAssociations"]) ? functionConfig["KeyValueStoreAssociations"] : [];
|
|
28528
|
+
const out = [];
|
|
28529
|
+
for (const entry of raw) {
|
|
28530
|
+
if (!entry || typeof entry !== "object") continue;
|
|
28531
|
+
const arnValue = entry["KeyValueStoreARN"];
|
|
28532
|
+
if (arnValue === void 0) continue;
|
|
28533
|
+
const kvsLogicalId = pickKvsLogicalIdFromArn(arnValue);
|
|
28534
|
+
out.push({
|
|
28535
|
+
arnValue,
|
|
28536
|
+
...kvsLogicalId !== void 0 && { kvsLogicalId }
|
|
28537
|
+
});
|
|
28384
28538
|
}
|
|
28385
28539
|
return out;
|
|
28386
28540
|
}
|
|
28541
|
+
/**
|
|
28542
|
+
* Unwrap a `KeyValueStoreARN` value to the `AWS::CloudFront::KeyValueStore`
|
|
28543
|
+
* logical id when it is an intrinsic reference to a same-template store. CDK /
|
|
28544
|
+
* CloudFormation synthesize it as `Fn::GetAtt[<Kvs>, 'Arn']`, a `Ref`, or
|
|
28545
|
+
* `Fn::Sub: '${<Kvs>.Arn}'`. Returns `undefined` for a literal ARN string (an
|
|
28546
|
+
* imported / out-of-stack store).
|
|
28547
|
+
*/
|
|
28548
|
+
function pickKvsLogicalIdFromArn(value) {
|
|
28549
|
+
if (!value || typeof value !== "object") return void 0;
|
|
28550
|
+
const obj = value;
|
|
28551
|
+
const getAtt = obj["Fn::GetAtt"];
|
|
28552
|
+
if (Array.isArray(getAtt) && getAtt.length === 2 && typeof getAtt[0] === "string") return getAtt[0];
|
|
28553
|
+
if (typeof obj["Ref"] === "string") return obj["Ref"];
|
|
28554
|
+
const sub = obj["Fn::Sub"];
|
|
28555
|
+
const subTemplate = typeof sub === "string" ? sub : Array.isArray(sub) ? sub[0] : void 0;
|
|
28556
|
+
if (typeof subTemplate === "string") {
|
|
28557
|
+
const match = /^\$\{([A-Za-z0-9]+)(?:\.[A-Za-z0-9]+)?\}$/.exec(subTemplate.trim());
|
|
28558
|
+
if (match) return match[1];
|
|
28559
|
+
}
|
|
28560
|
+
}
|
|
28387
28561
|
function resolveBehaviors(dc, functions, distLogicalId, template) {
|
|
28388
28562
|
const behaviors = [];
|
|
28389
28563
|
const def = dc["DefaultCacheBehavior"];
|
|
@@ -28401,10 +28575,9 @@ function resolveBehaviors(dc, functions, distLogicalId, template) {
|
|
|
28401
28575
|
return behaviors;
|
|
28402
28576
|
}
|
|
28403
28577
|
function resolveBehavior(behavior, pathPattern, functions, distLogicalId, template) {
|
|
28404
|
-
const resolved = {
|
|
28405
|
-
|
|
28406
|
-
|
|
28407
|
-
};
|
|
28578
|
+
const resolved = { targetOriginId: typeof behavior["TargetOriginId"] === "string" ? behavior["TargetOriginId"] : "" };
|
|
28579
|
+
const lambdaEdge = resolveLambdaEdgeAssociations(behavior, template, distLogicalId);
|
|
28580
|
+
if (lambdaEdge) resolved.lambdaEdge = lambdaEdge;
|
|
28408
28581
|
if (pathPattern !== void 0) resolved.pathPattern = pathPattern;
|
|
28409
28582
|
const cors = resolveResponseHeadersPolicyCors(template, behavior["ResponseHeadersPolicyId"]);
|
|
28410
28583
|
if (cors) resolved.cors = cors;
|
|
@@ -28436,6 +28609,55 @@ function pickFunctionLogicalIdFromArn(value) {
|
|
|
28436
28609
|
const getAtt = value["Fn::GetAtt"];
|
|
28437
28610
|
if (Array.isArray(getAtt) && getAtt.length === 2 && typeof getAtt[0] === "string") return getAtt[0];
|
|
28438
28611
|
}
|
|
28612
|
+
/**
|
|
28613
|
+
* Resolve a behavior's `LambdaFunctionAssociations[]` into the per-event-type
|
|
28614
|
+
* backing-function map (issue #400). Each association names a
|
|
28615
|
+
* `LambdaFunctionARN` (a versioned ARN) + an `EventType` + `IncludeBody`.
|
|
28616
|
+
*/
|
|
28617
|
+
function resolveLambdaEdgeAssociations(behavior, template, distLogicalId) {
|
|
28618
|
+
const assocs = Array.isArray(behavior["LambdaFunctionAssociations"]) ? behavior["LambdaFunctionAssociations"] : [];
|
|
28619
|
+
const out = {};
|
|
28620
|
+
for (const a of assocs) {
|
|
28621
|
+
if (!a || typeof a !== "object") continue;
|
|
28622
|
+
const assoc = a;
|
|
28623
|
+
const eventType = assoc["EventType"];
|
|
28624
|
+
const functionLogicalId = pickLambdaEdgeFunctionLogicalId(assoc["LambdaFunctionARN"], template);
|
|
28625
|
+
if (!functionLogicalId) {
|
|
28626
|
+
getLogger().warn(`Distribution '${distLogicalId}': a Lambda@Edge association references a LambdaFunctionARN cdk-local could not resolve to a local AWS::Lambda::Function; it will not run.`);
|
|
28627
|
+
continue;
|
|
28628
|
+
}
|
|
28629
|
+
const entry = {
|
|
28630
|
+
functionLogicalId,
|
|
28631
|
+
includeBody: assoc["IncludeBody"] === true
|
|
28632
|
+
};
|
|
28633
|
+
if (eventType === "viewer-request") out.viewerRequest = entry;
|
|
28634
|
+
else if (eventType === "origin-request") out.originRequest = entry;
|
|
28635
|
+
else if (eventType === "origin-response") out.originResponse = entry;
|
|
28636
|
+
else if (eventType === "viewer-response") out.viewerResponse = entry;
|
|
28637
|
+
}
|
|
28638
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
28639
|
+
}
|
|
28640
|
+
/**
|
|
28641
|
+
* Resolve a `LambdaFunctionARN` (a versioned function ARN) to the backing
|
|
28642
|
+
* `AWS::Lambda::Function` logical id. CDK wires the association to a
|
|
28643
|
+
* `lambda.Version` (`{Ref: <Version>}` returns the qualified ARN), whose
|
|
28644
|
+
* `FunctionName` references the function. A direct (unqualified) function ref
|
|
28645
|
+
* is also accepted. Returns `undefined` when it cannot reach an
|
|
28646
|
+
* `AWS::Lambda::Function` (e.g. an imported cross-region EdgeFunction ARN).
|
|
28647
|
+
*/
|
|
28648
|
+
function pickLambdaEdgeFunctionLogicalId(value, template) {
|
|
28649
|
+
const candidate = refLogicalId(value) ?? getAttLogicalId(value);
|
|
28650
|
+
if (!candidate) return void 0;
|
|
28651
|
+
const resources = template.Resources ?? {};
|
|
28652
|
+
const resource = resources[candidate];
|
|
28653
|
+
if (!resource) return void 0;
|
|
28654
|
+
if (resource.Type === LAMBDA_FUNCTION_TYPE) return candidate;
|
|
28655
|
+
if (resource.Type === LAMBDA_VERSION_TYPE) {
|
|
28656
|
+
const fnName = (resource.Properties ?? {})["FunctionName"];
|
|
28657
|
+
const fnId = refLogicalId(fnName) ?? getAttLogicalId(fnName);
|
|
28658
|
+
if (fnId && resources[fnId]?.Type === LAMBDA_FUNCTION_TYPE) return fnId;
|
|
28659
|
+
}
|
|
28660
|
+
}
|
|
28439
28661
|
function resolveOrigins(dc, template, stack, overrides) {
|
|
28440
28662
|
const out = /* @__PURE__ */ new Map();
|
|
28441
28663
|
const origins = Array.isArray(dc["Origins"]) ? dc["Origins"] : [];
|
|
@@ -28635,6 +28857,261 @@ function isCloudFrontDistribution(resource) {
|
|
|
28635
28857
|
return resource.Type === CLOUDFRONT_DISTRIBUTION_TYPE;
|
|
28636
28858
|
}
|
|
28637
28859
|
|
|
28860
|
+
//#endregion
|
|
28861
|
+
//#region src/local/cloudfront-edge-event.ts
|
|
28862
|
+
/** Convert a normalized HTTP header map into the Lambda@Edge multi-map. */
|
|
28863
|
+
function httpHeadersToEdge(headers) {
|
|
28864
|
+
const out = {};
|
|
28865
|
+
for (const [name, values] of Object.entries(headers)) {
|
|
28866
|
+
const lower = name.toLowerCase();
|
|
28867
|
+
out[lower] = values.map((value) => ({
|
|
28868
|
+
key: name,
|
|
28869
|
+
value
|
|
28870
|
+
}));
|
|
28871
|
+
}
|
|
28872
|
+
return out;
|
|
28873
|
+
}
|
|
28874
|
+
/**
|
|
28875
|
+
* Flatten a Lambda@Edge header multi-map into a single-valued `{ name: value }`
|
|
28876
|
+
* map (comma-joining duplicates), EXCEPT `set-cookie`, whose values are returned
|
|
28877
|
+
* separately so multiple cookies survive. Pseudo / read-only headers CloudFront
|
|
28878
|
+
* forbids a function from setting are dropped.
|
|
28879
|
+
*/
|
|
28880
|
+
function edgeHeadersToHttp(headers) {
|
|
28881
|
+
const out = {};
|
|
28882
|
+
const setCookies = [];
|
|
28883
|
+
for (const [name, entries] of Object.entries(headers)) {
|
|
28884
|
+
if (!Array.isArray(entries)) continue;
|
|
28885
|
+
const lower = name.toLowerCase();
|
|
28886
|
+
if (READ_ONLY_RESPONSE_HEADERS.has(lower)) continue;
|
|
28887
|
+
if (lower === "set-cookie") {
|
|
28888
|
+
for (const e of entries) if (e && typeof e.value === "string") setCookies.push(e.value);
|
|
28889
|
+
continue;
|
|
28890
|
+
}
|
|
28891
|
+
const values = entries.filter((e) => e && typeof e.value === "string").map((e) => e.value);
|
|
28892
|
+
if (values.length > 0) out[lower] = values.join(", ");
|
|
28893
|
+
}
|
|
28894
|
+
return {
|
|
28895
|
+
headers: out,
|
|
28896
|
+
setCookies
|
|
28897
|
+
};
|
|
28898
|
+
}
|
|
28899
|
+
/**
|
|
28900
|
+
* Headers a Lambda@Edge function is not allowed to add/modify on a response (a
|
|
28901
|
+
* subset CloudFront blackholes). We drop them rather than fail the response.
|
|
28902
|
+
*/
|
|
28903
|
+
const READ_ONLY_RESPONSE_HEADERS = new Set([
|
|
28904
|
+
"connection",
|
|
28905
|
+
"content-length",
|
|
28906
|
+
"keep-alive",
|
|
28907
|
+
"proxy-authenticate",
|
|
28908
|
+
"proxy-authorization",
|
|
28909
|
+
"te",
|
|
28910
|
+
"trailer",
|
|
28911
|
+
"transfer-encoding",
|
|
28912
|
+
"upgrade"
|
|
28913
|
+
]);
|
|
28914
|
+
/** Build the request-stage event (`viewer-request` / `origin-request`). */
|
|
28915
|
+
function buildEdgeRequestEvent(args) {
|
|
28916
|
+
return { Records: [{ cf: {
|
|
28917
|
+
config: {
|
|
28918
|
+
...args.config,
|
|
28919
|
+
eventType: args.eventType
|
|
28920
|
+
},
|
|
28921
|
+
request: toEdgeRequest(args.request, args.includeBody)
|
|
28922
|
+
} }] };
|
|
28923
|
+
}
|
|
28924
|
+
/** Build the response-stage event (`origin-response` / `viewer-response`). */
|
|
28925
|
+
function buildEdgeResponseEvent(args) {
|
|
28926
|
+
return { Records: [{ cf: {
|
|
28927
|
+
config: {
|
|
28928
|
+
...args.config,
|
|
28929
|
+
eventType: args.eventType
|
|
28930
|
+
},
|
|
28931
|
+
request: toEdgeRequest(args.request, false),
|
|
28932
|
+
response: {
|
|
28933
|
+
status: String(args.response.statusCode),
|
|
28934
|
+
statusDescription: statusText(args.response.statusCode),
|
|
28935
|
+
headers: httpHeadersToEdge(toMultiMap(args.response.headers))
|
|
28936
|
+
}
|
|
28937
|
+
} }] };
|
|
28938
|
+
}
|
|
28939
|
+
function toEdgeRequest(input, includeBody) {
|
|
28940
|
+
const request = {
|
|
28941
|
+
clientIp: input.clientIp,
|
|
28942
|
+
method: input.method,
|
|
28943
|
+
uri: input.uri,
|
|
28944
|
+
querystring: input.querystring,
|
|
28945
|
+
headers: httpHeadersToEdge(input.headers)
|
|
28946
|
+
};
|
|
28947
|
+
if (includeBody) request.body = {
|
|
28948
|
+
action: "read-only",
|
|
28949
|
+
data: (input.body ?? Buffer.alloc(0)).toString("base64"),
|
|
28950
|
+
encoding: "base64",
|
|
28951
|
+
inputTruncated: false
|
|
28952
|
+
};
|
|
28953
|
+
return request;
|
|
28954
|
+
}
|
|
28955
|
+
function interpretEdgeRequestResult(result, fallback) {
|
|
28956
|
+
if (!result || typeof result !== "object") return {
|
|
28957
|
+
kind: "continue",
|
|
28958
|
+
request: fallback
|
|
28959
|
+
};
|
|
28960
|
+
const obj = result;
|
|
28961
|
+
if ("status" in obj) return {
|
|
28962
|
+
kind: "response",
|
|
28963
|
+
response: coerceEdgeResponse(obj)
|
|
28964
|
+
};
|
|
28965
|
+
return {
|
|
28966
|
+
kind: "continue",
|
|
28967
|
+
request: coerceEdgeRequest(obj, fallback)
|
|
28968
|
+
};
|
|
28969
|
+
}
|
|
28970
|
+
/** Interpret a response-stage handler's return value as the (modified) response. */
|
|
28971
|
+
function interpretEdgeResponseResult(result, fallback) {
|
|
28972
|
+
if (!result || typeof result !== "object") return fallback;
|
|
28973
|
+
return coerceEdgeResponse(result, fallback);
|
|
28974
|
+
}
|
|
28975
|
+
/** Collapse an {@link EdgeResponse} into the server's status / headers / body. */
|
|
28976
|
+
function edgeResponseToResult(response, fallbackBody) {
|
|
28977
|
+
const statusCode = Number.parseInt(response.status, 10);
|
|
28978
|
+
const { headers, setCookies } = edgeHeadersToHttp(response.headers ?? {});
|
|
28979
|
+
let body;
|
|
28980
|
+
if (response.body !== void 0) body = response.bodyEncoding === "base64" ? Buffer.from(response.body, "base64") : Buffer.from(response.body);
|
|
28981
|
+
else body = fallbackBody ?? Buffer.alloc(0);
|
|
28982
|
+
return {
|
|
28983
|
+
statusCode: Number.isFinite(statusCode) ? statusCode : 500,
|
|
28984
|
+
headers,
|
|
28985
|
+
setCookies,
|
|
28986
|
+
body
|
|
28987
|
+
};
|
|
28988
|
+
}
|
|
28989
|
+
/**
|
|
28990
|
+
* Server-facing request-stage orchestration: given a handler's raw return value
|
|
28991
|
+
* and the current request input, produce either the (modified) request to
|
|
28992
|
+
* continue with, or the generated response to short-circuit.
|
|
28993
|
+
*/
|
|
28994
|
+
function applyEdgeRequestResult(result, base) {
|
|
28995
|
+
const outcome = interpretEdgeRequestResult(result, toEdgeRequest(base, false));
|
|
28996
|
+
if (outcome.kind === "response") return {
|
|
28997
|
+
kind: "response",
|
|
28998
|
+
response: edgeResponseToResult(outcome.response)
|
|
28999
|
+
};
|
|
29000
|
+
return {
|
|
29001
|
+
kind: "continue",
|
|
29002
|
+
request: applyEdgeRequest(base, outcome.request)
|
|
29003
|
+
};
|
|
29004
|
+
}
|
|
29005
|
+
/**
|
|
29006
|
+
* Server-facing response-stage orchestration: apply a handler's modified
|
|
29007
|
+
* response over the current status / headers, keeping the origin body unless the
|
|
29008
|
+
* function replaced it.
|
|
29009
|
+
*/
|
|
29010
|
+
function applyEdgeResponseResult(result, base, originBody) {
|
|
29011
|
+
return edgeResponseToResult(interpretEdgeResponseResult(result, {
|
|
29012
|
+
status: String(base.statusCode),
|
|
29013
|
+
headers: httpHeadersToEdge(toMultiMap(base.headers))
|
|
29014
|
+
}), originBody);
|
|
29015
|
+
}
|
|
29016
|
+
/** Apply a request-stage function's modified request back onto the server's request input. */
|
|
29017
|
+
function applyEdgeRequest(base, request) {
|
|
29018
|
+
const out = {
|
|
29019
|
+
clientIp: base.clientIp,
|
|
29020
|
+
method: typeof request.method === "string" ? request.method : base.method,
|
|
29021
|
+
uri: typeof request.uri === "string" ? request.uri : base.uri,
|
|
29022
|
+
querystring: typeof request.querystring === "string" ? request.querystring : base.querystring,
|
|
29023
|
+
headers: edgeHeadersToRawMap(request.headers) ?? base.headers
|
|
29024
|
+
};
|
|
29025
|
+
if (request.body && request.body.action === "replace" && typeof request.body.data === "string") out.body = request.body.encoding === "base64" ? Buffer.from(request.body.data, "base64") : Buffer.from(request.body.data);
|
|
29026
|
+
else if (base.body !== void 0) out.body = base.body;
|
|
29027
|
+
return out;
|
|
29028
|
+
}
|
|
29029
|
+
function edgeHeadersToRawMap(headers) {
|
|
29030
|
+
if (!headers || typeof headers !== "object") return void 0;
|
|
29031
|
+
const out = {};
|
|
29032
|
+
for (const [name, entries] of Object.entries(headers)) {
|
|
29033
|
+
if (!Array.isArray(entries)) continue;
|
|
29034
|
+
out[name.toLowerCase()] = entries.filter((e) => e && typeof e.value === "string").map((e) => e.value);
|
|
29035
|
+
}
|
|
29036
|
+
return out;
|
|
29037
|
+
}
|
|
29038
|
+
function coerceEdgeRequest(obj, fallback) {
|
|
29039
|
+
const out = {
|
|
29040
|
+
clientIp: typeof obj["clientIp"] === "string" ? obj["clientIp"] : fallback.clientIp,
|
|
29041
|
+
method: typeof obj["method"] === "string" ? obj["method"] : fallback.method,
|
|
29042
|
+
uri: typeof obj["uri"] === "string" ? obj["uri"] : fallback.uri,
|
|
29043
|
+
querystring: typeof obj["querystring"] === "string" ? obj["querystring"] : fallback.querystring,
|
|
29044
|
+
headers: coerceEdgeHeaders(obj["headers"]) ?? fallback.headers
|
|
29045
|
+
};
|
|
29046
|
+
const body = obj["body"];
|
|
29047
|
+
if (body && typeof body === "object") {
|
|
29048
|
+
const b = body;
|
|
29049
|
+
out.body = {
|
|
29050
|
+
action: b["action"] === "replace" ? "replace" : "read-only",
|
|
29051
|
+
data: typeof b["data"] === "string" ? b["data"] : "",
|
|
29052
|
+
encoding: b["encoding"] === "text" ? "text" : "base64",
|
|
29053
|
+
inputTruncated: b["inputTruncated"] === true
|
|
29054
|
+
};
|
|
29055
|
+
}
|
|
29056
|
+
return out;
|
|
29057
|
+
}
|
|
29058
|
+
function coerceEdgeResponse(obj, fallback) {
|
|
29059
|
+
const out = {
|
|
29060
|
+
status: typeof obj["status"] === "string" ? obj["status"] : typeof obj["status"] === "number" ? String(obj["status"]) : fallback?.status ?? "200",
|
|
29061
|
+
headers: coerceEdgeHeaders(obj["headers"]) ?? fallback?.headers ?? {}
|
|
29062
|
+
};
|
|
29063
|
+
if (typeof obj["statusDescription"] === "string") out.statusDescription = obj["statusDescription"];
|
|
29064
|
+
else if (fallback?.statusDescription !== void 0) out.statusDescription = fallback.statusDescription;
|
|
29065
|
+
if (typeof obj["body"] === "string") {
|
|
29066
|
+
out.body = obj["body"];
|
|
29067
|
+
if (obj["bodyEncoding"] === "base64" || obj["bodyEncoding"] === "text") out.bodyEncoding = obj["bodyEncoding"];
|
|
29068
|
+
}
|
|
29069
|
+
return out;
|
|
29070
|
+
}
|
|
29071
|
+
/** Coerce an arbitrary value into the EdgeHeaders shape, tolerating a bare `{name: 'value'}` map. */
|
|
29072
|
+
function coerceEdgeHeaders(value) {
|
|
29073
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
29074
|
+
const out = {};
|
|
29075
|
+
for (const [name, raw] of Object.entries(value)) {
|
|
29076
|
+
const lower = name.toLowerCase();
|
|
29077
|
+
if (Array.isArray(raw)) out[lower] = raw.filter((e) => !!e && typeof e === "object").map((e) => ({
|
|
29078
|
+
key: typeof e["key"] === "string" ? e["key"] : name,
|
|
29079
|
+
value: typeof e["value"] === "string" ? e["value"] : String(e["value"] ?? "")
|
|
29080
|
+
}));
|
|
29081
|
+
else if (typeof raw === "string") out[lower] = [{
|
|
29082
|
+
key: name,
|
|
29083
|
+
value: raw
|
|
29084
|
+
}];
|
|
29085
|
+
}
|
|
29086
|
+
return out;
|
|
29087
|
+
}
|
|
29088
|
+
function toMultiMap(headers) {
|
|
29089
|
+
const out = {};
|
|
29090
|
+
for (const [name, value] of Object.entries(headers)) out[name] = [value];
|
|
29091
|
+
return out;
|
|
29092
|
+
}
|
|
29093
|
+
function statusText(code) {
|
|
29094
|
+
return STATUS_TEXT[code] ?? "";
|
|
29095
|
+
}
|
|
29096
|
+
const STATUS_TEXT = {
|
|
29097
|
+
200: "OK",
|
|
29098
|
+
201: "Created",
|
|
29099
|
+
204: "No Content",
|
|
29100
|
+
301: "Moved Permanently",
|
|
29101
|
+
302: "Found",
|
|
29102
|
+
303: "See Other",
|
|
29103
|
+
304: "Not Modified",
|
|
29104
|
+
307: "Temporary Redirect",
|
|
29105
|
+
308: "Permanent Redirect",
|
|
29106
|
+
400: "Bad Request",
|
|
29107
|
+
401: "Unauthorized",
|
|
29108
|
+
403: "Forbidden",
|
|
29109
|
+
404: "Not Found",
|
|
29110
|
+
500: "Internal Server Error",
|
|
29111
|
+
502: "Bad Gateway",
|
|
29112
|
+
503: "Service Unavailable"
|
|
29113
|
+
};
|
|
29114
|
+
|
|
28638
29115
|
//#endregion
|
|
28639
29116
|
//#region src/local/cloudfront-lambda-origin.ts
|
|
28640
29117
|
/**
|
|
@@ -28819,9 +29296,10 @@ function contentTypeForKey(key) {
|
|
|
28819
29296
|
async function startCloudFrontServer(options) {
|
|
28820
29297
|
const logger = getLogger().child("cloudfront");
|
|
28821
29298
|
const lambdaInvokers = options.lambdaInvokers ?? /* @__PURE__ */ new Map();
|
|
29299
|
+
const edgeInvokers = options.edgeInvokers ?? /* @__PURE__ */ new Map();
|
|
28822
29300
|
const state = { distribution: options.distribution };
|
|
28823
29301
|
const handler = (req, res) => {
|
|
28824
|
-
handleRequest$1(req, res, state, lambdaInvokers, logger).catch((err) => {
|
|
29302
|
+
handleRequest$1(req, res, state, lambdaInvokers, edgeInvokers, logger).catch((err) => {
|
|
28825
29303
|
logger.warn(`Request handling failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
28826
29304
|
if (!res.headersSent) {
|
|
28827
29305
|
res.statusCode = 500;
|
|
@@ -28851,7 +29329,7 @@ async function startCloudFrontServer(options) {
|
|
|
28851
29329
|
};
|
|
28852
29330
|
}
|
|
28853
29331
|
/** The per-request pipeline: behavior match -> viewer-request -> origin -> viewer-response. */
|
|
28854
|
-
async function handleRequest$1(req, res, state, lambdaInvokers, logger) {
|
|
29332
|
+
async function handleRequest$1(req, res, state, lambdaInvokers, edgeInvokers, logger) {
|
|
28855
29333
|
const distribution = state.distribution;
|
|
28856
29334
|
const rawUrl = req.url ?? "/";
|
|
28857
29335
|
const queryIdx = rawUrl.indexOf("?");
|
|
@@ -28874,37 +29352,72 @@ async function handleRequest$1(req, res, state, lambdaInvokers, logger) {
|
|
|
28874
29352
|
return;
|
|
28875
29353
|
}
|
|
28876
29354
|
}
|
|
29355
|
+
const config = {
|
|
29356
|
+
distributionDomainName: req.headers.host ?? "localhost",
|
|
29357
|
+
distributionId: distribution.logicalId,
|
|
29358
|
+
requestId: `cdkl-${Date.now().toString(36)}-${Math.floor(performance.now()).toString(36)}`
|
|
29359
|
+
};
|
|
29360
|
+
const clientIp = req.socket.remoteAddress ?? "127.0.0.1";
|
|
28877
29361
|
let requestEvent = buildViewerRequestEvent({
|
|
28878
29362
|
method: req.method ?? "GET",
|
|
28879
29363
|
uri,
|
|
28880
29364
|
querystring,
|
|
28881
29365
|
headers: req.headers,
|
|
28882
|
-
ip:
|
|
29366
|
+
ip: clientIp,
|
|
28883
29367
|
distributionId: distribution.logicalId,
|
|
28884
29368
|
domainName: req.headers.host ?? "localhost",
|
|
28885
|
-
requestId:
|
|
29369
|
+
requestId: config.requestId
|
|
28886
29370
|
});
|
|
28887
|
-
let effectiveUri = uri;
|
|
28888
29371
|
if (behavior.viewerRequest) {
|
|
28889
29372
|
const outcome = await runViewerRequest(behavior.viewerRequest, requestEvent);
|
|
28890
29373
|
if (outcome.kind === "response") {
|
|
28891
29374
|
writeCfResponse(res, outcome.response, logger);
|
|
28892
29375
|
return;
|
|
28893
29376
|
}
|
|
28894
|
-
effectiveUri = outcome.request.uri;
|
|
28895
29377
|
requestEvent = {
|
|
28896
29378
|
...requestEvent,
|
|
28897
29379
|
request: outcome.request
|
|
28898
29380
|
};
|
|
28899
29381
|
}
|
|
29382
|
+
let edgeInput = {
|
|
29383
|
+
clientIp,
|
|
29384
|
+
method: requestEvent.request.method,
|
|
29385
|
+
uri: requestEvent.request.uri,
|
|
29386
|
+
querystring: serializeCfQueryString(requestEvent.request.querystring),
|
|
29387
|
+
headers: cfHeadersToRawMap(requestEvent.request.headers)
|
|
29388
|
+
};
|
|
29389
|
+
for (const eventType of ["viewer-request", "origin-request"]) {
|
|
29390
|
+
const assoc = eventType === "viewer-request" ? behavior.lambdaEdge?.viewerRequest : behavior.lambdaEdge?.originRequest;
|
|
29391
|
+
if (!assoc) continue;
|
|
29392
|
+
const invoke = edgeInvokers.get(assoc.functionLogicalId);
|
|
29393
|
+
if (!invoke) {
|
|
29394
|
+
logger.warn(`Lambda@Edge ${eventType} function '${assoc.functionLogicalId}' was not booted (added after start-up); skipping. Restart start-cloudfront.`);
|
|
29395
|
+
continue;
|
|
29396
|
+
}
|
|
29397
|
+
if (assoc.includeBody && edgeInput.body === void 0) edgeInput = {
|
|
29398
|
+
...edgeInput,
|
|
29399
|
+
body: await readRequestBody(req)
|
|
29400
|
+
};
|
|
29401
|
+
const outcome = applyEdgeRequestResult(await invoke(buildEdgeRequestEvent({
|
|
29402
|
+
eventType,
|
|
29403
|
+
config,
|
|
29404
|
+
request: edgeInput,
|
|
29405
|
+
includeBody: assoc.includeBody
|
|
29406
|
+
})), edgeInput);
|
|
29407
|
+
if (outcome.kind === "response") {
|
|
29408
|
+
writeEdgeResponse(res, outcome.response, behavior, req, logger);
|
|
29409
|
+
return;
|
|
29410
|
+
}
|
|
29411
|
+
edgeInput = outcome.request;
|
|
29412
|
+
}
|
|
28900
29413
|
const origin = distribution.origins.get(behavior.targetOriginId);
|
|
28901
|
-
const originResult = await serveFromOrigin(origin,
|
|
28902
|
-
uri:
|
|
28903
|
-
querystring,
|
|
28904
|
-
method:
|
|
28905
|
-
headers:
|
|
28906
|
-
readBody: () => readRequestBody(req),
|
|
28907
|
-
sourceIp:
|
|
29414
|
+
const originResult = await serveFromOrigin(origin, {
|
|
29415
|
+
uri: edgeInput.uri,
|
|
29416
|
+
querystring: edgeInput.querystring,
|
|
29417
|
+
method: edgeInput.method,
|
|
29418
|
+
headers: rawMapToIncomingHeaders(edgeInput.headers),
|
|
29419
|
+
readBody: () => edgeInput.body !== void 0 ? Promise.resolve(edgeInput.body) : readRequestBody(req),
|
|
29420
|
+
sourceIp: clientIp,
|
|
28908
29421
|
distribution,
|
|
28909
29422
|
lambdaInvokers,
|
|
28910
29423
|
logger
|
|
@@ -28912,16 +29425,47 @@ async function handleRequest$1(req, res, state, lambdaInvokers, logger) {
|
|
|
28912
29425
|
if (!originResult) return writePlain(res, 502, originUnavailableMessage(origin, behavior));
|
|
28913
29426
|
let finalStatus = originResult.statusCode;
|
|
28914
29427
|
let finalHeaders = originResult.headers;
|
|
29428
|
+
let finalBody = originResult.body;
|
|
29429
|
+
const setCookies = [...originResult.setCookies ?? []];
|
|
29430
|
+
if (behavior.lambdaEdge?.originResponse) {
|
|
29431
|
+
const r = await runEdgeResponseStage(behavior.lambdaEdge.originResponse, "origin-response", {
|
|
29432
|
+
config,
|
|
29433
|
+
request: edgeInput,
|
|
29434
|
+
statusCode: finalStatus,
|
|
29435
|
+
headers: finalHeaders,
|
|
29436
|
+
body: finalBody
|
|
29437
|
+
}, edgeInvokers, logger);
|
|
29438
|
+
if (r) {
|
|
29439
|
+
finalStatus = r.statusCode;
|
|
29440
|
+
finalHeaders = r.headers;
|
|
29441
|
+
finalBody = r.body;
|
|
29442
|
+
setCookies.push(...r.setCookies);
|
|
29443
|
+
}
|
|
29444
|
+
}
|
|
28915
29445
|
if (behavior.viewerResponse) {
|
|
28916
29446
|
const responseEvent = buildViewerResponseEvent(requestEvent, {
|
|
28917
|
-
statusCode:
|
|
28918
|
-
headers:
|
|
29447
|
+
statusCode: finalStatus,
|
|
29448
|
+
headers: finalHeaders
|
|
28919
29449
|
});
|
|
28920
29450
|
const mutated = await runViewerResponse(behavior.viewerResponse, responseEvent);
|
|
28921
29451
|
finalStatus = mutated.statusCode;
|
|
28922
|
-
finalHeaders = cfHeadersToPlain(mutated.headers,
|
|
29452
|
+
finalHeaders = cfHeadersToPlain(mutated.headers, finalHeaders);
|
|
29453
|
+
}
|
|
29454
|
+
if (behavior.lambdaEdge?.viewerResponse) {
|
|
29455
|
+
const r = await runEdgeResponseStage(behavior.lambdaEdge.viewerResponse, "viewer-response", {
|
|
29456
|
+
config,
|
|
29457
|
+
request: edgeInput,
|
|
29458
|
+
statusCode: finalStatus,
|
|
29459
|
+
headers: finalHeaders,
|
|
29460
|
+
body: finalBody
|
|
29461
|
+
}, edgeInvokers, logger);
|
|
29462
|
+
if (r) {
|
|
29463
|
+
finalStatus = r.statusCode;
|
|
29464
|
+
finalHeaders = r.headers;
|
|
29465
|
+
finalBody = r.body;
|
|
29466
|
+
setCookies.push(...r.setCookies);
|
|
29467
|
+
}
|
|
28923
29468
|
}
|
|
28924
|
-
const setCookies = [...originResult.setCookies ?? []];
|
|
28925
29469
|
for (const name of Object.keys(finalHeaders)) if (name.toLowerCase() === "set-cookie") {
|
|
28926
29470
|
setCookies.push(finalHeaders[name]);
|
|
28927
29471
|
delete finalHeaders[name];
|
|
@@ -28937,7 +29481,54 @@ async function handleRequest$1(req, res, state, lambdaInvokers, logger) {
|
|
|
28937
29481
|
const origin = req.headers.origin;
|
|
28938
29482
|
applyCorsResponseHeadersFromConfig(res, behavior.cors, typeof origin === "string" ? origin : void 0);
|
|
28939
29483
|
}
|
|
28940
|
-
res.end(
|
|
29484
|
+
res.end(finalBody);
|
|
29485
|
+
}
|
|
29486
|
+
/** Run a Lambda@Edge response-stage function, returning the modified response (or undefined when no invoker). */
|
|
29487
|
+
async function runEdgeResponseStage(assoc, eventType, ctx, edgeInvokers, logger) {
|
|
29488
|
+
const invoke = edgeInvokers.get(assoc.functionLogicalId);
|
|
29489
|
+
if (!invoke) {
|
|
29490
|
+
logger.warn(`Lambda@Edge ${eventType} function '${assoc.functionLogicalId}' was not booted (added after start-up); skipping. Restart start-cloudfront.`);
|
|
29491
|
+
return;
|
|
29492
|
+
}
|
|
29493
|
+
return applyEdgeResponseResult(await invoke(buildEdgeResponseEvent({
|
|
29494
|
+
eventType,
|
|
29495
|
+
config: ctx.config,
|
|
29496
|
+
request: ctx.request,
|
|
29497
|
+
response: {
|
|
29498
|
+
statusCode: ctx.statusCode,
|
|
29499
|
+
headers: ctx.headers
|
|
29500
|
+
}
|
|
29501
|
+
})), {
|
|
29502
|
+
statusCode: ctx.statusCode,
|
|
29503
|
+
headers: ctx.headers
|
|
29504
|
+
}, ctx.body);
|
|
29505
|
+
}
|
|
29506
|
+
/** Write a Lambda@Edge generated response (request-stage short-circuit), incl. CORS. */
|
|
29507
|
+
function writeEdgeResponse(res, result, behavior, req, logger) {
|
|
29508
|
+
if (!req.readableEnded) req.resume();
|
|
29509
|
+
res.statusCode = result.statusCode;
|
|
29510
|
+
setHeadersSafely(res, result.headers, logger);
|
|
29511
|
+
if (result.setCookies.length > 0) try {
|
|
29512
|
+
res.setHeader("set-cookie", result.setCookies);
|
|
29513
|
+
} catch {}
|
|
29514
|
+
if (behavior.cors) {
|
|
29515
|
+
const origin = req.headers.origin;
|
|
29516
|
+
applyCorsResponseHeadersFromConfig(res, behavior.cors, typeof origin === "string" ? origin : void 0);
|
|
29517
|
+
}
|
|
29518
|
+
res.end(result.body);
|
|
29519
|
+
}
|
|
29520
|
+
/** Convert a CloudFront-Function header map (`{name: CfValue}`) into the raw `{name: string[]}` form. */
|
|
29521
|
+
function cfHeadersToRawMap(cf) {
|
|
29522
|
+
const out = {};
|
|
29523
|
+
for (const [name, val] of Object.entries(cf)) if (val.multiValue && val.multiValue.length > 0) out[name] = val.multiValue.map((m) => m.value);
|
|
29524
|
+
else out[name] = [val.value];
|
|
29525
|
+
return out;
|
|
29526
|
+
}
|
|
29527
|
+
/** Convert a raw `{name: string[]}` header map into Node's `IncomingHttpHeaders` shape. */
|
|
29528
|
+
function rawMapToIncomingHeaders(headers) {
|
|
29529
|
+
const out = {};
|
|
29530
|
+
for (const [name, values] of Object.entries(headers)) out[name] = values.length === 1 ? values[0] : values;
|
|
29531
|
+
return out;
|
|
28941
29532
|
}
|
|
28942
29533
|
/**
|
|
28943
29534
|
* Convert Node's `IncomingMessage.headers` (`Record<string, string | string[]>`)
|
|
@@ -28974,9 +29565,8 @@ function setHeadersSafely(res, headers, logger) {
|
|
|
28974
29565
|
logger.warn(`Skipping invalid response header '${name}': ${err instanceof Error ? err.message : String(err)}`);
|
|
28975
29566
|
}
|
|
28976
29567
|
}
|
|
28977
|
-
async function serveFromOrigin(origin,
|
|
28978
|
-
const { distribution
|
|
28979
|
-
if (behavior.hasLambdaEdge) logger.warn(`Behavior ${behavior.pathPattern ?? "(default)"} carries a Lambda@Edge association; cdk-local does not run Lambda@Edge — serving the origin only.`);
|
|
29568
|
+
async function serveFromOrigin(origin, args) {
|
|
29569
|
+
const { distribution } = args;
|
|
28980
29570
|
if (!origin) return void 0;
|
|
28981
29571
|
if (origin.kind === "lambda-url") {
|
|
28982
29572
|
const invoke = args.lambdaInvokers.get(origin.functionLogicalId);
|
|
@@ -29073,6 +29663,130 @@ function listen(server, host, port) {
|
|
|
29073
29663
|
});
|
|
29074
29664
|
}
|
|
29075
29665
|
|
|
29666
|
+
//#endregion
|
|
29667
|
+
//#region src/local/cloudfront-kvs-client.ts
|
|
29668
|
+
/**
|
|
29669
|
+
* Build a {@link KvsDataSource} backed by the deployed store's `GetKey` API.
|
|
29670
|
+
* A missing key resolves to `undefined` (the `ResourceNotFoundException` is
|
|
29671
|
+
* caught) so the `cf` shim can surface a clean "key not found" — any other
|
|
29672
|
+
* error (access denied, throttling) propagates so the user sees the real cause.
|
|
29673
|
+
*/
|
|
29674
|
+
function createDeployedKvsDataSource(options) {
|
|
29675
|
+
const client = new CloudFrontKeyValueStoreClient({
|
|
29676
|
+
region: options.region ?? "us-east-1",
|
|
29677
|
+
...options.credentials !== void 0 && { credentials: options.credentials }
|
|
29678
|
+
});
|
|
29679
|
+
return {
|
|
29680
|
+
label: `deployed:${options.kvsArn}`,
|
|
29681
|
+
...options.kvsId !== void 0 && { kvsId: options.kvsId },
|
|
29682
|
+
async getValue(key) {
|
|
29683
|
+
try {
|
|
29684
|
+
return (await client.send(new GetKeyCommand({
|
|
29685
|
+
KvsARN: options.kvsArn,
|
|
29686
|
+
Key: key
|
|
29687
|
+
}))).Value;
|
|
29688
|
+
} catch (err) {
|
|
29689
|
+
if (isKeyNotFound(err)) return void 0;
|
|
29690
|
+
throw new Error(`cf.kvs().get('${key}') against ${options.kvsArn} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29691
|
+
}
|
|
29692
|
+
}
|
|
29693
|
+
};
|
|
29694
|
+
}
|
|
29695
|
+
/**
|
|
29696
|
+
* Resolve a deployed KeyValueStore's ARN (+ Id) from its NAME via the
|
|
29697
|
+
* CloudFront control-plane `ListKeyValueStores` API. Needed because
|
|
29698
|
+
* `--from-cfn-stack` reads the store's physical id from `ListStackResources`,
|
|
29699
|
+
* and for `AWS::CloudFront::KeyValueStore` that physical id is the store NAME
|
|
29700
|
+
* (the `Ref` value) — NOT the ARN. The data-plane `GetKey` needs the ARN (which
|
|
29701
|
+
* embeds the store's UUID Id), so we look the name up against the account's
|
|
29702
|
+
* stores. CloudFront is global, so the client defaults to `us-east-1`. Returns
|
|
29703
|
+
* `undefined` on any miss (no match, access denied) so the caller falls back to
|
|
29704
|
+
* the unbound-KVS warning.
|
|
29705
|
+
*/
|
|
29706
|
+
async function resolveDeployedKvsArnByName(name, options = {}) {
|
|
29707
|
+
const client = new CloudFrontClient({
|
|
29708
|
+
region: options.region ?? "us-east-1",
|
|
29709
|
+
...options.credentials !== void 0 && { credentials: options.credentials }
|
|
29710
|
+
});
|
|
29711
|
+
try {
|
|
29712
|
+
for await (const page of paginateListKeyValueStores({ client }, {})) for (const item of page.KeyValueStoreList?.Items ?? []) if (item.Name === name && item.ARN) return {
|
|
29713
|
+
arn: item.ARN,
|
|
29714
|
+
...item.Id !== void 0 && { id: item.Id }
|
|
29715
|
+
};
|
|
29716
|
+
} catch (err) {
|
|
29717
|
+
getLogger().debug(`ListKeyValueStores lookup for '${name}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29718
|
+
}
|
|
29719
|
+
}
|
|
29720
|
+
/** True when a GetKey error means the key (or store) was not found, not a real failure. */
|
|
29721
|
+
function isKeyNotFound(err) {
|
|
29722
|
+
if (err instanceof ResourceNotFoundException) return true;
|
|
29723
|
+
if (err && typeof err === "object") {
|
|
29724
|
+
if (err.name === "ResourceNotFoundException") return true;
|
|
29725
|
+
if (err.$metadata?.httpStatusCode === 404) return true;
|
|
29726
|
+
}
|
|
29727
|
+
return false;
|
|
29728
|
+
}
|
|
29729
|
+
|
|
29730
|
+
//#endregion
|
|
29731
|
+
//#region src/local/cloudfront-kvs-binding.ts
|
|
29732
|
+
/**
|
|
29733
|
+
* Build + attach the `cf` module to every KVS-reading function in the
|
|
29734
|
+
* distribution. Returns boot warnings for associations that resolved to no
|
|
29735
|
+
* binding (the runtime then injects an unbound module so the read fails with a
|
|
29736
|
+
* clear actionable error). Idempotent per call — overwrites
|
|
29737
|
+
* `cloudfrontModule` so a `--watch` reload rebinds.
|
|
29738
|
+
*/
|
|
29739
|
+
async function resolveKvsModulesForDistribution(distribution, options) {
|
|
29740
|
+
const warnings = [];
|
|
29741
|
+
for (const fn of collectKvsFunctions(distribution)) {
|
|
29742
|
+
const sources = [];
|
|
29743
|
+
for (const assoc of fn.kvsAssociations ?? []) {
|
|
29744
|
+
const source = await buildSourceForAssociation(assoc, options);
|
|
29745
|
+
if (source) sources.push(source);
|
|
29746
|
+
else {
|
|
29747
|
+
const ref = assoc.kvsLogicalId ?? "<store>";
|
|
29748
|
+
warnings.push(`CloudFront Function '${fn.logicalId}' reads KeyValueStore '${ref}', but no binding resolved it — cf.kvs() reads will fail. Pass --from-cfn-stack to read the deployed store, or --kvs-file ${ref}=<file.json> for a local map.`);
|
|
29749
|
+
}
|
|
29750
|
+
}
|
|
29751
|
+
if (sources.length > 0) fn.cloudfrontModule = createCloudFrontModule(sources);
|
|
29752
|
+
else delete fn.cloudfrontModule;
|
|
29753
|
+
}
|
|
29754
|
+
return { warnings };
|
|
29755
|
+
}
|
|
29756
|
+
/** Collect the unique compiled functions in the distribution that read a KVS. */
|
|
29757
|
+
function collectKvsFunctions(distribution) {
|
|
29758
|
+
const byLogicalId = /* @__PURE__ */ new Map();
|
|
29759
|
+
for (const behavior of distribution.behaviors) for (const fn of [behavior.viewerRequest, behavior.viewerResponse]) if (fn && fn.kvsAssociations && fn.kvsAssociations.length > 0) byLogicalId.set(fn.logicalId, fn);
|
|
29760
|
+
return [...byLogicalId.values()];
|
|
29761
|
+
}
|
|
29762
|
+
async function buildSourceForAssociation(assoc, options) {
|
|
29763
|
+
if (assoc.kvsLogicalId !== void 0) {
|
|
29764
|
+
const filePath = options.kvsFiles?.get(assoc.kvsLogicalId);
|
|
29765
|
+
if (filePath !== void 0) return createLocalFileKvsDataSource({
|
|
29766
|
+
id: assoc.kvsLogicalId,
|
|
29767
|
+
filePath
|
|
29768
|
+
});
|
|
29769
|
+
}
|
|
29770
|
+
if (typeof assoc.arnValue === "string" && assoc.arnValue.startsWith("arn:")) return makeDeployedSource(assoc.arnValue, idFromArn(assoc.arnValue), options);
|
|
29771
|
+
if (assoc.kvsLogicalId !== void 0 && options.resolveDeployedKvs) {
|
|
29772
|
+
const ref = await options.resolveDeployedKvs(assoc.kvsLogicalId);
|
|
29773
|
+
if (ref) return makeDeployedSource(ref.arn, ref.id, options);
|
|
29774
|
+
}
|
|
29775
|
+
}
|
|
29776
|
+
function makeDeployedSource(arn, id, options) {
|
|
29777
|
+
return createDeployedKvsDataSource({
|
|
29778
|
+
kvsArn: arn,
|
|
29779
|
+
...id !== void 0 && { kvsId: id },
|
|
29780
|
+
...options.region !== void 0 && { region: options.region },
|
|
29781
|
+
...options.credentials !== void 0 && { credentials: options.credentials }
|
|
29782
|
+
});
|
|
29783
|
+
}
|
|
29784
|
+
/** The KeyValueStore Id is the ARN's last `/`-segment. */
|
|
29785
|
+
function idFromArn(arn) {
|
|
29786
|
+
const slash = arn.lastIndexOf("/");
|
|
29787
|
+
return slash >= 0 && slash < arn.length - 1 ? arn.slice(slash + 1) : void 0;
|
|
29788
|
+
}
|
|
29789
|
+
|
|
29076
29790
|
//#endregion
|
|
29077
29791
|
//#region src/cli/commands/local-start-cloudfront.ts
|
|
29078
29792
|
/** Error thrown by `cdkl start-cloudfront` for target / option problems. */
|
|
@@ -29097,6 +29811,55 @@ function parseOriginOverrides(values) {
|
|
|
29097
29811
|
return out;
|
|
29098
29812
|
}
|
|
29099
29813
|
/**
|
|
29814
|
+
* Parse the repeatable `--kvs-file <kvsLogicalId>=<file.json>` overrides into a
|
|
29815
|
+
* map. Each entry backs a CloudFront Function's `cf.kvs()` reads with a local
|
|
29816
|
+
* JSON map instead of the deployed store — the AWS-free escape hatch for KVS,
|
|
29817
|
+
* symmetric with `--origin <id>=<dir>`. The key is the
|
|
29818
|
+
* `AWS::CloudFront::KeyValueStore` resource logical id (surfaced in the
|
|
29819
|
+
* unbound-KVS boot warning when missing).
|
|
29820
|
+
*/
|
|
29821
|
+
function parseKvsFileOverrides(values) {
|
|
29822
|
+
const out = /* @__PURE__ */ new Map();
|
|
29823
|
+
for (const raw of values ?? []) {
|
|
29824
|
+
const eq = raw.indexOf("=");
|
|
29825
|
+
if (eq <= 0 || eq === raw.length - 1) throw new LocalStartCloudFrontError(`Invalid --kvs-file '${raw}'. Expected <kvsLogicalId>=<file.json> (e.g. MyKvsStore=./kvs.json).`);
|
|
29826
|
+
out.set(raw.slice(0, eq).trim(), raw.slice(eq + 1).trim());
|
|
29827
|
+
}
|
|
29828
|
+
return out;
|
|
29829
|
+
}
|
|
29830
|
+
/**
|
|
29831
|
+
* Resolve + attach the `cf` KeyValueStore module to every KVS-reading
|
|
29832
|
+
* CloudFront Function in the distribution (run after each synth, initial +
|
|
29833
|
+
* `--watch` reload). A `--kvs-file` map wins; otherwise `--from-cfn-stack`
|
|
29834
|
+
* resolves the deployed store's ARN from state and `cf.kvs().get()` reads it
|
|
29835
|
+
* via the `GetKey` API. Unresolved associations log an actionable WARN (the
|
|
29836
|
+
* runtime then fails the read with the same guidance).
|
|
29837
|
+
*/
|
|
29838
|
+
async function attachKvsModules(distribution, stacks, options, profileCredentials, logger) {
|
|
29839
|
+
const kvsFiles = parseKvsFileOverrides(options.kvsFile);
|
|
29840
|
+
const synthRegion = stacks.find((s) => s.stackName === distribution.stackName)?.region;
|
|
29841
|
+
const resolveDeployedKvs = isCfnFlagPresent(options) ? async (kvsLogicalId) => {
|
|
29842
|
+
const provider = createLocalStateProvider(options, distribution.stackName, synthRegion);
|
|
29843
|
+
if (!provider) return void 0;
|
|
29844
|
+
const physicalId = (await provider.load(distribution.stackName, synthRegion))?.resources[kvsLogicalId]?.physicalId;
|
|
29845
|
+
if (!physicalId) return void 0;
|
|
29846
|
+
if (physicalId.startsWith("arn:")) {
|
|
29847
|
+
const id = idFromArn(physicalId);
|
|
29848
|
+
return {
|
|
29849
|
+
arn: physicalId,
|
|
29850
|
+
...id !== void 0 && { id }
|
|
29851
|
+
};
|
|
29852
|
+
}
|
|
29853
|
+
return resolveDeployedKvsArnByName(physicalId, { ...profileCredentials !== void 0 && { credentials: profileCredentials } });
|
|
29854
|
+
} : void 0;
|
|
29855
|
+
const { warnings } = await resolveKvsModulesForDistribution(distribution, {
|
|
29856
|
+
...kvsFiles.size > 0 && { kvsFiles },
|
|
29857
|
+
...resolveDeployedKvs !== void 0 && { resolveDeployedKvs },
|
|
29858
|
+
...profileCredentials !== void 0 && { credentials: profileCredentials }
|
|
29859
|
+
});
|
|
29860
|
+
for (const warning of warnings) logger.warn(warning);
|
|
29861
|
+
}
|
|
29862
|
+
/**
|
|
29100
29863
|
* Resolve a CloudFront target string (`Stack/Path` display path or
|
|
29101
29864
|
* `Stack:LogicalId`) to its stack + `AWS::CloudFront::Distribution` logical id.
|
|
29102
29865
|
* Mirrors the ALB / ECS resolver target grammar.
|
|
@@ -29181,12 +29944,58 @@ async function bootLambdaUrlOrigins(distribution, stacks, opts) {
|
|
|
29181
29944
|
runners
|
|
29182
29945
|
};
|
|
29183
29946
|
}
|
|
29947
|
+
/**
|
|
29948
|
+
* Boot one warm RIE container per unique Lambda@Edge function across the
|
|
29949
|
+
* distribution's behaviors (issue #400), returning the runners + an invoker map
|
|
29950
|
+
* keyed by backing-function logical id for {@link startCloudFrontServer}. Each
|
|
29951
|
+
* function gets the SAME container env as a direct `cdkl invoke` (declared env
|
|
29952
|
+
* vars + `--from-cfn-stack` substitution + `--assume-role` creds) via the shared
|
|
29953
|
+
* {@link resolveLambdaContainerEnv}. A function that cannot be booted is
|
|
29954
|
+
* warn-and-skipped (its edge stage will not run); booted once at start-up, NOT
|
|
29955
|
+
* rebuilt on a `--watch` reload.
|
|
29956
|
+
*/
|
|
29957
|
+
async function bootLambdaEdgeFunctions(distribution, stacks, opts) {
|
|
29958
|
+
const logger = getLogger();
|
|
29959
|
+
const invokers = /* @__PURE__ */ new Map();
|
|
29960
|
+
const runners = [];
|
|
29961
|
+
const functionLogicalIds = /* @__PURE__ */ new Set();
|
|
29962
|
+
for (const behavior of distribution.behaviors) {
|
|
29963
|
+
const edge = behavior.lambdaEdge;
|
|
29964
|
+
if (!edge) continue;
|
|
29965
|
+
for (const assoc of [
|
|
29966
|
+
edge.viewerRequest,
|
|
29967
|
+
edge.originRequest,
|
|
29968
|
+
edge.originResponse,
|
|
29969
|
+
edge.viewerResponse
|
|
29970
|
+
]) if (assoc) functionLogicalIds.add(assoc.functionLogicalId);
|
|
29971
|
+
}
|
|
29972
|
+
for (const functionLogicalId of functionLogicalIds) try {
|
|
29973
|
+
const lambda = resolveLambdaTarget(functionLogicalId, stacks);
|
|
29974
|
+
const containerEnv = await resolveLambdaContainerEnv(lambda, opts.envOptions, opts.profileCredentials);
|
|
29975
|
+
const runner = createFrontDoorLambdaRunner(lambda, {
|
|
29976
|
+
containerHost: opts.containerHost,
|
|
29977
|
+
skipPull: opts.skipPull,
|
|
29978
|
+
...opts.envOptions.region !== void 0 && { region: opts.envOptions.region },
|
|
29979
|
+
containerEnv: containerEnv.env,
|
|
29980
|
+
...containerEnv.sensitiveEnvKeys.length > 0 && { sensitiveEnvKeys: new Set(containerEnv.sensitiveEnvKeys) }
|
|
29981
|
+
});
|
|
29982
|
+
logger.info(`Booting Lambda@Edge container for ${functionLogicalId} (the function runs locally via RIE)...`);
|
|
29983
|
+
await runner.start();
|
|
29984
|
+
runners.push(runner);
|
|
29985
|
+
invokers.set(functionLogicalId, (event) => runner.invoke(event));
|
|
29986
|
+
} catch (err) {
|
|
29987
|
+
logger.warn(`Could not boot Lambda@Edge function '${functionLogicalId}'; its stage will be skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
29988
|
+
}
|
|
29989
|
+
return {
|
|
29990
|
+
invokers,
|
|
29991
|
+
runners
|
|
29992
|
+
};
|
|
29993
|
+
}
|
|
29184
29994
|
/** Emit boot-time WARNs for parts of the distribution cdk-local does not serve. */
|
|
29185
29995
|
function warnUnsupported(distribution) {
|
|
29186
29996
|
const logger = getLogger();
|
|
29187
29997
|
for (const origin of distribution.origins.values()) if (origin.kind === "custom") logger.warn(`Origin '${origin.originId}' is a custom (non-S3, non-Lambda-Function-URL) origin (${origin.domainName}); ${getEmbedConfig().cliName} start-cloudfront serves S3 origins and Lambda Function URL origins only. Requests routed to it return 502.`);
|
|
29188
29998
|
else if (origin.kind === "s3-unresolved") logger.warn(`Origin '${origin.originId}' is an S3 origin with no resolvable local source (no BucketDeployment found, or its source could not be located in the cloud assembly). Point it at a directory with --origin ${origin.originId}=<dir>. Requests routed to it return 502.`);
|
|
29189
|
-
if (distribution.behaviors.some((b) => b.hasLambdaEdge)) logger.warn("One or more cache behaviors carry a Lambda@Edge association; cdk-local does not run Lambda@Edge functions — only CloudFront Functions + the S3 origin are served.");
|
|
29190
29999
|
}
|
|
29191
30000
|
async function localStartCloudFrontCommand(target, options) {
|
|
29192
30001
|
const logger = getLogger();
|
|
@@ -29241,24 +30050,34 @@ async function localStartCloudFrontCommand(target, options) {
|
|
|
29241
30050
|
...options.profile !== void 0 && { profile: options.profile },
|
|
29242
30051
|
...options.stackRegion !== void 0 && { stackRegion: options.stackRegion }
|
|
29243
30052
|
};
|
|
29244
|
-
|
|
30053
|
+
await attachKvsModules(initial.distribution, initial.stacks, options, profileCredentials, logger);
|
|
30054
|
+
const bootOpts = {
|
|
29245
30055
|
containerHost: options.host,
|
|
29246
30056
|
skipPull: options.pull === false,
|
|
29247
30057
|
envOptions,
|
|
29248
30058
|
...profileCredentials !== void 0 && { profileCredentials }
|
|
29249
|
-
}
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
29253
|
-
|
|
29254
|
-
|
|
29255
|
-
|
|
29256
|
-
|
|
29257
|
-
|
|
29258
|
-
|
|
29259
|
-
|
|
29260
|
-
|
|
29261
|
-
|
|
30059
|
+
};
|
|
30060
|
+
const { invokers: lambdaInvokers, runners: lambdaRunners } = await bootLambdaUrlOrigins(initial.distribution, initial.stacks, bootOpts);
|
|
30061
|
+
const { invokers: edgeInvokers, runners: edgeRunners } = await bootLambdaEdgeFunctions(initial.distribution, initial.stacks, bootOpts);
|
|
30062
|
+
let server;
|
|
30063
|
+
try {
|
|
30064
|
+
let tls;
|
|
30065
|
+
if (tlsRequested) tls = await resolveFrontDoorTlsMaterials({
|
|
30066
|
+
certPath: options.tlsCert,
|
|
30067
|
+
keyPath: options.tlsKey
|
|
30068
|
+
});
|
|
30069
|
+
server = await startCloudFrontServer({
|
|
30070
|
+
distribution: initial.distribution,
|
|
30071
|
+
host: options.host,
|
|
30072
|
+
port: basePort,
|
|
30073
|
+
...tls && { tls },
|
|
30074
|
+
...lambdaInvokers.size > 0 && { lambdaInvokers },
|
|
30075
|
+
...edgeInvokers.size > 0 && { edgeInvokers }
|
|
30076
|
+
});
|
|
30077
|
+
} catch (err) {
|
|
30078
|
+
await Promise.all([...lambdaRunners, ...edgeRunners].map((r) => r.stop().catch(() => void 0)));
|
|
30079
|
+
throw err;
|
|
30080
|
+
}
|
|
29262
30081
|
process.stdout.write(`CloudFront distribution serving on ${server.url} (${initial.distribution.logicalId})\n`);
|
|
29263
30082
|
process.stdout.write("^C to stop.\n");
|
|
29264
30083
|
let watcher;
|
|
@@ -29280,6 +30099,7 @@ async function localStartCloudFrontCommand(target, options) {
|
|
|
29280
30099
|
try {
|
|
29281
30100
|
const reloaded = await synthAndResolve();
|
|
29282
30101
|
warnUnsupported(reloaded.distribution);
|
|
30102
|
+
await attachKvsModules(reloaded.distribution, reloaded.stacks, options, profileCredentials, logger);
|
|
29283
30103
|
server.update(reloaded.distribution);
|
|
29284
30104
|
logger.info("Reload complete.");
|
|
29285
30105
|
} catch (err) {
|
|
@@ -29301,8 +30121,8 @@ async function localStartCloudFrontCommand(target, options) {
|
|
|
29301
30121
|
try {
|
|
29302
30122
|
await server.close();
|
|
29303
30123
|
} catch {}
|
|
29304
|
-
await Promise.all(lambdaRunners.map((r) => r.stop().catch((err) => {
|
|
29305
|
-
logger.debug(`Lambda
|
|
30124
|
+
await Promise.all([...lambdaRunners, ...edgeRunners].map((r) => r.stop().catch((err) => {
|
|
30125
|
+
logger.debug(`Lambda runner stop failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
29306
30126
|
})));
|
|
29307
30127
|
process.exit(exitCode);
|
|
29308
30128
|
};
|
|
@@ -29333,7 +30153,7 @@ function createLocalStartCloudFrontCommand(opts = {}) {
|
|
|
29333
30153
|
* `cmd`.
|
|
29334
30154
|
*/
|
|
29335
30155
|
function addStartCloudFrontSpecificOptions(cmd) {
|
|
29336
|
-
return cmd.addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--origin <originId=dir>", "Point a distribution origin at a local directory (repeatable). Use when cdk-local cannot resolve the origin's BucketDeployment source automatically (content uploaded out of band, or a non-CDK bucket).").argParser((value, prev) => [...prev ?? [], value])).addOption(new Option("--tls", "Terminate real TLS (HTTPS). Uses --tls-cert / --tls-key when supplied, else an auto-generated self-signed cert.").default(false)).addOption(new Option("--tls-cert <path>", "PEM server certificate for --tls (implies --tls).")).addOption(new Option("--tls-key <path>", "PEM server private key for --tls (implies --tls).")).addOption(new Option("--no-pull", "Skip 'docker pull' for a Lambda Function URL origin's base image (use the locally cached image).")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Bind a Lambda Function URL origin's backing Lambda to a deployed CloudFormation stack so its env vars resolve to the deployed physical IDs / exports (ListStackResources). Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass a value when the CFn stack name differs. No effect on a pure-S3 distribution.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--assume-role [arn]", "Assume a Lambda Function URL origin's deployed execution role and forward STS-issued temp credentials into its container so the handler runs with the deployed permissions. Three forms: `--assume-role <arn>` (explicit ARN); `--assume-role` (bare, auto-resolves from state — requires --from-cfn-stack); `--no-assume-role` (opt out). Off by default (the dev shell credentials are forwarded).")).addOption(new Option("--watch", "Hot-reload: re-synth + re-resolve the distribution when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). The server keeps the previous version serving when synth fails mid-reload.").default(false));
|
|
30156
|
+
return cmd.addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--origin <originId=dir>", "Point a distribution origin at a local directory (repeatable). Use when cdk-local cannot resolve the origin's BucketDeployment source automatically (content uploaded out of band, or a non-CDK bucket).").argParser((value, prev) => [...prev ?? [], value])).addOption(new Option("--kvs-file <kvsLogicalId=file.json>", "Back a CloudFront Function's KeyValueStore reads (cf.kvs().get()) with a local JSON map (repeatable). The key is the AWS::CloudFront::KeyValueStore resource logical id; the file is a flat { \"key\": \"value\" } object. The AWS-free alternative to --from-cfn-stack, which instead reads the deployed store via the GetKey API.").argParser((value, prev) => [...prev ?? [], value])).addOption(new Option("--tls", "Terminate real TLS (HTTPS). Uses --tls-cert / --tls-key when supplied, else an auto-generated self-signed cert.").default(false)).addOption(new Option("--tls-cert <path>", "PEM server certificate for --tls (implies --tls).")).addOption(new Option("--tls-key <path>", "PEM server private key for --tls (implies --tls).")).addOption(new Option("--no-pull", "Skip 'docker pull' for a Lambda Function URL origin's base image (use the locally cached image).")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Bind a Lambda Function URL origin's backing Lambda to a deployed CloudFormation stack so its env vars resolve to the deployed physical IDs / exports (ListStackResources). Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass a value when the CFn stack name differs. No effect on a pure-S3 distribution.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).addOption(new Option("--assume-role [arn]", "Assume a Lambda Function URL origin's deployed execution role and forward STS-issued temp credentials into its container so the handler runs with the deployed permissions. Three forms: `--assume-role <arn>` (explicit ARN); `--assume-role` (bare, auto-resolves from state — requires --from-cfn-stack); `--no-assume-role` (opt out). Off by default (the dev shell credentials are forwarded).")).addOption(new Option("--watch", "Hot-reload: re-synth + re-resolve the distribution when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). The server keeps the previous version serving when synth fails mid-reload.").default(false));
|
|
29337
30157
|
}
|
|
29338
30158
|
|
|
29339
30159
|
//#endregion
|
|
@@ -34368,5 +35188,5 @@ function addStudioSpecificOptions(cmd) {
|
|
|
34368
35188
|
}
|
|
34369
35189
|
|
|
34370
35190
|
//#endregion
|
|
34371
|
-
export {
|
|
34372
|
-
//# sourceMappingURL=local-studio
|
|
35191
|
+
export { resolveCloudFrontDistribution as $, matchPreflight as $n, AGENTCORE_A2A_PROTOCOL as $r, MCP_CONTAINER_PORT as $t, parseOriginOverrides as A, filterRoutesByApiIdentifiers as An, LocalStateSourceError as Ar, ImageOverrideError as At, applyEdgeRequestResult as B, verifyCognitoJwt as Bn, resolveWatchConfig as Br, buildCloudMapIndex as Bt, addListSpecificOptions as C, createFileWatcher as Cn, resolveRuntimeFileExtension as Cr, buildEcsImageResolutionContext$1 as Ct, addStartCloudFrontSpecificOptions as D, resolveEnvVars$1 as Dn, substituteAgainstStateAsync as Dr, resolveEcsAssumeRoleOption as Dt, LocalStartCloudFrontError as E, materializeLayerFromArn as En, substituteAgainstState as Er, parseRestartPolicy as Et, resolveDeployedKvsArnByName as F, resolveServiceIntegrationParameters as Fn, resolveCfnRegion as Fr, resolveImageOverrides as Ft, httpHeadersToEdge as G, evaluateCachedLambdaPolicy as Gn, discoverWebSocketApis as Gr, getContainerNetworkIp as Gt, buildEdgeRequestEvent as H, verifyJwtViaDiscovery as Hn, countTargets as Hr, DEFAULT_SHADOW_READY_TIMEOUT_MS as Ht, matchBehavior as I, defaultCredentialsLoader as In, resolveCfnStackName as Ir, runImageOverrideBuilds as It, isCloudFrontDistribution as J, attachAuthorizers as Jn, parseSelectionExpressionPath as Jr, createLocalInvokeAgentCoreCommand as Jt, CLOUDFRONT_DISTRIBUTION_TYPE as K, invokeRequestAuthorizer as Kn, discoverWebSocketApisOrThrow as Kr, attachContainerLogStreamer as Kt, startCloudFrontServer as L, buildCognitoJwksUrl as Ln, CfnLocalStateProvider as Lr, describePinnedImageUri as Lt, idFromArn as M, readMtlsMaterialsFromDisk as Mn, isCfnFlagPresent as Mr, enforceImageOverrideOrphans as Mt, resolveKvsModulesForDistribution as N, startApiServer as Nn, rejectExplicitCfnStackWithMultipleStacks as Nr, mergeForService as Nt, createLocalStartCloudFrontCommand as O, availableApiIdentifiers as On, substituteEnvVarsFromState as Or, resolveSharedSidecarCredentials as Ot, createDeployedKvsDataSource as P, resolveSelectionExpression as Pn, resolveCfnFallbackRegion as Pr, parseImageOverrideFlags as Pt, pickTargetFunctionLogicalId as Q, isFunctionUrlOacFronted as Qn, resolveLambdaArnIntrinsic as Qr, a2aInvokeOnce as Qt, serveFromStaticOrigin as R, buildJwksUrlFromIssuer as Rn, collectSsmParameterRefs as Rr, isLocalCdkAssetImage as Rt, StudioEventBus as S, createAuthorizerCache as Sn, resolveRuntimeCodeMountPath as Sr, addImageOverrideOptions as St, formatTargetListing as T, buildStageMap as Tn, EcsTaskResolutionError as Tr, parseMaxTasks as Tt, buildEdgeResponseEvent as U, buildMethodArn as Un, listTargets as Ur, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as Ut, applyEdgeResponseResult as V, verifyJwtAuthorizer as Vn, resolveSingleTarget as Vr, CloudMapRegistry as Vt, edgeHeadersToHttp as W, computeRequestIdentityHash as Wn, availableWebSocketApiIdentifiers as Wr, setShadowReadyTimeoutMs as Wt, pickKvsLogicalIdFromArn as X, buildCorsConfigByApiId as Xn, discoverRoutes as Xr, A2A_CONTAINER_PORT as Xt, pickFunctionUrlLogicalIdFromOrigin as Y, applyCorsResponseHeaders as Yn, webSocketApiMatchesIdentifier as Yr, invokeAgentCoreWs as Yt, pickLambdaEdgeFunctionLogicalId as Z, buildCorsConfigFromCloudFrontChain as Zn, pickRefLogicalId as Zr, A2A_PATH as Zt, filterStudioTargetGroups as _, createLocalInvokeCommand as _n, buildConnectEvent as _r, addRunTaskSpecificOptions as _t, createLocalStudioCommand as a, pickAgentCoreCandidateStack as ai, signAgentCoreInvocation as an, evaluateResponseParameters as ar, createLocalFileKvsDataSource as at, renderStudioHtml as b, createWatchPredicates as bn, architectureToPlatform as br, addCommonEcsServiceOptions as bt, startStudioProxy as c, formatStateRemedy as ci, waitForAgentCorePing as cn, tryParseStatus as cr, albStrategy as ct, createStudioDispatcher as d, LocalInvokeBuildError as di, buildAgentCoreCodeImage as dn, probeHostGatewaySupport as dr, resolveAlbTarget as dt, AGENTCORE_AGUI_PROTOCOL as ei, MCP_PATH as en, matchRoute as er, compileCloudFrontFunction as et, filterStudioCustomResources as f, buildStsClientConfig as fi, computeCodeImageTag as fn, bufferToBody as fr, isApplicationLoadBalancer as ft, annotatePinnedEcsTargets as g, addInvokeSpecificOptions as gn, parseConnectionsPath as gr, serviceStrategy as gt, annotateEcsTaskPinnedTargets as h, classifySourceChange as hn, handleConnectionsRequest as hr, createLocalStartServiceCommand as ht, coerceStopRequest as i, AgentCoreResolutionError as ii, AGENTCORE_SIGV4_SERVICE as in, buildRestV1Event as ir, createCloudFrontModule as it, resolveCloudFrontTarget as j, groupRoutesByServer as jn, createLocalStateProvider as jr, buildImageOverrideTag as jt, parseKvsFileOverrides as k, filterRoutesByApiIdentifier as kn, substituteEnvVarsFromStateAsync as kr, runEcsServiceEmulator as kt, relayServeRequest as l, substituteImagePlaceholders as li, downloadAndExtractS3Bundle as ln, VtlEvaluationError as lr, createLocalStartAlbCommand as lt, annotateAlbPinnedBackingServices as m, toCmdArgv as mn, buildMgmtEndpointEnvUrl as mr, addStartServiceSpecificOptions as mt, coerceRunRequest as n, AGENTCORE_MCP_PROTOCOL as ni, mcpInvokeOnce as nn, applyAuthorizerOverlay as nr, runViewerResponse as nt, resolveServeBaseUrl as o, resolveAgentCoreTarget as oi, AGENTCORE_SESSION_ID_HEADER as on, pickResponseTemplate as or, createUnboundCloudFrontModule as ot, isCustomResourceLambdaTarget as p, resolveProfileCredentials as pi, renderCodeDockerfile as pn, ConnectionRegistry as pr, resolveAlbFrontDoor as pt, extractKvsAssociations as q, invokeTokenAuthorizer as qn, filterWebSocketApisByIdentifiers as qr, addInvokeAgentCoreSpecificOptions as qt, coerceServeRequest as r, AGENTCORE_RUNTIME_TYPE as ri, parseSseForJsonRpc as rn, buildHttpApiV2Event as rr, stripCloudFrontImport as rt, createStudioServeManager as s, derivePseudoParametersFromRegion as si, invokeAgentCore as sn, selectIntegrationResponse as sr, addAlbSpecificOptions as st, addStudioSpecificOptions as t, AGENTCORE_HTTP_PROTOCOL as ti, MCP_PROTOCOL_VERSION as tn, translateLambdaResponse as tr, runViewerRequest as tt, reinvoke as u, tryResolveImageFnJoin as ui, SUPPORTED_CODE_RUNTIMES as un, HOST_GATEWAY_MIN_VERSION as ur, parseLbPortOverrides as ut, startStudioServer as v, addStartApiSpecificOptions as vn, buildDisconnectEvent as vr, createLocalRunTaskCommand as vt, createLocalListCommand as w, attachStageContext as wn, resolveRuntimeImage as wr, ecsClusterOption as wt, createStudioStore as x, resolveApiTargetSubset as xn, buildContainerImage as xr, addEcsAssumeRoleOptions as xt, toStudioTargetGroups as y, createLocalStartApiCommand as yn, buildMessageEvent as yr, MAX_TASKS_SUBNET_RANGE_CAP as yt, serveLambdaUrlOrigin as z, createJwksCache as zn, resolveSsmParameters as zr, listPinnedTargets as zt };
|
|
35192
|
+
//# sourceMappingURL=local-studio--ndMb9dM.js.map
|