@deployfoundation/foundation-deploy 0.2.51 → 0.2.53

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.
@@ -0,0 +1,336 @@
1
+ import {
2
+ DEFAULT_RELEASE_BUCKET,
3
+ configuredKeyArn
4
+ } from "./chunk-dz91avrb.js";
5
+
6
+ // src/release/manifest.ts
7
+ import { createHash } from "node:crypto";
8
+ import { readFileSync, statSync } from "node:fs";
9
+ import { z } from "zod";
10
+ var RELEASE_VERSION_RE = /^v\d+\.\d+\.\d+$/;
11
+ var Sha256 = z.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase hex sha256");
12
+ var ReleaseArtifactSchema = z.object({
13
+ key: z.string().min(1),
14
+ sha256: Sha256,
15
+ bytes: z.number().int().nonnegative()
16
+ }).strict();
17
+ var SIGNING_ALGORITHMS = [
18
+ "RSASSA_PSS_SHA_256",
19
+ "RSASSA_PSS_SHA_384",
20
+ "RSASSA_PSS_SHA_512",
21
+ "RSASSA_PKCS1_V1_5_SHA_256",
22
+ "RSASSA_PKCS1_V1_5_SHA_384",
23
+ "RSASSA_PKCS1_V1_5_SHA_512",
24
+ "ECDSA_SHA_256",
25
+ "ECDSA_SHA_384",
26
+ "ECDSA_SHA_512"
27
+ ];
28
+ var ReleaseManifestSchema = z.object({
29
+ version: z.string().regex(RELEASE_VERSION_RE, "must be vMAJOR.MINOR.PATCH"),
30
+ gitCommit: z.string().regex(/^[0-9a-f]{40}$/, "must be a full commit sha"),
31
+ createdAt: z.string().datetime(),
32
+ agentImage: z.object({
33
+ repository: z.string().min(1),
34
+ tag: z.string().min(1),
35
+ digest: z.string().regex(/^sha256:[0-9a-f]{64}$/)
36
+ }).strict(),
37
+ lambda: z.record(ReleaseArtifactSchema),
38
+ skills: z.object({ key: z.string().min(1), sha256: Sha256 }).strict(),
39
+ signature: z.object({
40
+ kmsKeyArn: z.string().min(1),
41
+ algorithm: z.enum(SIGNING_ALGORITHMS),
42
+ value: z.string().min(1)
43
+ }).strict().optional()
44
+ }).strict();
45
+ function parseManifest(json) {
46
+ return ReleaseManifestSchema.parse(typeof json === "string" ? JSON.parse(json) : json);
47
+ }
48
+ function releasePrefix(version) {
49
+ return `releases/${version}`;
50
+ }
51
+ function manifestKey(version) {
52
+ return `${releasePrefix(version)}/manifest.json`;
53
+ }
54
+ function lambdaKey(version, id) {
55
+ return `${releasePrefix(version)}/lambda/${id}.zip`;
56
+ }
57
+ function skillsKey(version) {
58
+ return `${releasePrefix(version)}/skills.tar.gz`;
59
+ }
60
+ function sha256Hex(bytes) {
61
+ return createHash("sha256").update(bytes).digest("hex");
62
+ }
63
+ function sha256File(path) {
64
+ return sha256Hex(readFileSync(path));
65
+ }
66
+ function buildManifest(input) {
67
+ const lambda = {};
68
+ for (const [id, path] of Object.entries(input.lambda).sort(([a], [b]) => a < b ? -1 : 1))
69
+ lambda[id] = {
70
+ key: lambdaKey(input.version, id),
71
+ sha256: sha256File(path),
72
+ bytes: statSync(path).size
73
+ };
74
+ return parseManifest({
75
+ version: input.version,
76
+ gitCommit: input.gitCommit,
77
+ createdAt: input.createdAt ?? new Date().toISOString(),
78
+ agentImage: input.agentImage,
79
+ lambda,
80
+ skills: { key: skillsKey(input.version), sha256: sha256File(input.skills) }
81
+ });
82
+ }
83
+ function manifestSigningPayload(manifest) {
84
+ const { signature: _signature, ...unsigned } = manifest;
85
+ return new TextEncoder().encode(canonicalJson(unsigned));
86
+ }
87
+ function canonicalJson(value) {
88
+ if (value === null || typeof value !== "object")
89
+ return JSON.stringify(value) ?? "null";
90
+ if (Array.isArray(value))
91
+ return `[${value.map(canonicalJson).join(",")}]`;
92
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
93
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
94
+ }
95
+ async function verifyManifest(manifest, options) {
96
+ const parsed = parseManifest(manifest);
97
+ if (options.expectVersion !== undefined && parsed.version !== options.expectVersion)
98
+ throw new Error(`release manifest is ${parsed.version}, not the requested ${options.expectVersion}`);
99
+ const signature = parsed.signature;
100
+ if (signature === undefined)
101
+ throw new Error(`release ${parsed.version} carries an unsigned manifest; refusing to deploy`);
102
+ if (signature.kmsKeyArn !== options.kmsKeyArn)
103
+ throw new Error(`release ${parsed.version} is signed by ${signature.kmsKeyArn}, not the expected ${options.kmsKeyArn}`);
104
+ const result = await options.kms.verify({
105
+ KeyId: options.kmsKeyArn,
106
+ Message: manifestSigningPayload(parsed),
107
+ MessageType: "RAW",
108
+ Signature: Uint8Array.from(Buffer.from(signature.value, "base64")),
109
+ SigningAlgorithm: signature.algorithm
110
+ });
111
+ if (result.SignatureValid !== true)
112
+ throw new Error(`release ${parsed.version}: KMS rejected the manifest signature`);
113
+ const readArtifact = options.readArtifact;
114
+ if (readArtifact === undefined)
115
+ return parsed;
116
+ for (const [id, artifact] of Object.entries(parsed.lambda)) {
117
+ const bytes = await readArtifact(artifact.key);
118
+ if (bytes.byteLength !== artifact.bytes)
119
+ throw new Error(`release ${parsed.version}: ${id} is ${bytes.byteLength} bytes, manifest says ${artifact.bytes}`);
120
+ assertDigest(`${parsed.version}: ${id}`, bytes, artifact.sha256);
121
+ }
122
+ assertDigest(`${parsed.version}: skills`, await readArtifact(parsed.skills.key), parsed.skills.sha256);
123
+ return parsed;
124
+ }
125
+ function assertDigest(what, bytes, expected) {
126
+ const actual = sha256Hex(bytes);
127
+ if (actual !== expected)
128
+ throw new Error(`release ${what}: sha256 ${actual}, manifest says ${expected}`);
129
+ }
130
+
131
+ // src/lambda-bundle-context.ts
132
+ import { copyFileSync, cpSync, lstatSync, mkdtempSync, rmSync } from "node:fs";
133
+ import { tmpdir } from "node:os";
134
+ import * as path from "node:path";
135
+ var contextByRoot = new Map;
136
+ var IGNORED_DIRECTORIES = {
137
+ ".git": true,
138
+ "cdk.out": true,
139
+ dist: true,
140
+ node_modules: true
141
+ };
142
+ var WORKSPACE_FILES = ["package.json", "bun.lock", "bunfig.toml", "tsconfig.json"];
143
+ function copySourceTree(source, destination) {
144
+ cpSync(source, destination, {
145
+ recursive: true,
146
+ filter(candidate) {
147
+ const relative2 = path.relative(source, candidate);
148
+ if (relative2 === "")
149
+ return true;
150
+ if (relative2.split(path.sep).some((component) => (component in IGNORED_DIRECTORIES)))
151
+ return false;
152
+ if (lstatSync(candidate).isSymbolicLink())
153
+ throw new Error(`Lambda bundle input contains a symlink: ${candidate}`);
154
+ return true;
155
+ }
156
+ });
157
+ }
158
+ function lambdaBundleContext(workspaceRoot) {
159
+ const existing = contextByRoot.get(workspaceRoot);
160
+ if (existing !== undefined)
161
+ return existing;
162
+ const context = mkdtempSync(path.join(tmpdir(), "foundation-lambda-bundle-"));
163
+ try {
164
+ for (const file of WORKSPACE_FILES)
165
+ copyFileSync(path.join(workspaceRoot, file), path.join(context, file));
166
+ copySourceTree(path.join(workspaceRoot, "packages"), path.join(context, "packages"));
167
+ } catch (error) {
168
+ rmSync(context, { recursive: true, force: true });
169
+ throw error;
170
+ }
171
+ contextByRoot.set(workspaceRoot, context);
172
+ return context;
173
+ }
174
+
175
+ // src/artifacts.ts
176
+ import { spawnSync } from "node:child_process";
177
+ import { readFileSync as readFileSync2 } from "node:fs";
178
+ import { tmpdir as tmpdir2 } from "node:os";
179
+ import { dirname, isAbsolute, join as join2, resolve } from "node:path";
180
+ import { fileURLToPath } from "node:url";
181
+ import * as cdk from "aws-cdk-lib";
182
+ import * as lambda from "aws-cdk-lib/aws-lambda";
183
+ import * as s3 from "aws-cdk-lib/aws-s3";
184
+ var BUN_VERSION = "1.4.0";
185
+ var WORKSPACE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
186
+ var LAMBDA_ENTRY_POINTS = {
187
+ gateway: { entry: "packages/gateway/src/lambda.ts" },
188
+ invoker: { entry: "packages/gateway/src/invoker.ts" },
189
+ "email-proxy": { entry: "packages/capability-email/src/proxy/handler.ts" },
190
+ "google-docs-proxy": { entry: "packages/capability-google-docs/src/proxy/handler.ts" },
191
+ "otter-proxy": { entry: "packages/capability-otter/src/proxy/handler.ts" },
192
+ "knock-proxy": { entry: "packages/capability-knock/src/proxy/handler.ts" },
193
+ "crm-proxy": { entry: "packages/capability-crm/src/proxy/handler.ts" },
194
+ "upwork-proxy": { entry: "packages/capability-upwork/src/proxy/handler.ts" },
195
+ "etrade-proxy": { entry: "packages/capability-etrade/src/proxy/handler.ts" },
196
+ "pages-api": { entry: "packages/capability-pages/src/proxy/handler.ts" },
197
+ "pages-auth": { entry: "packages/capability-pages/src/auth/lambda.ts" },
198
+ "browser-proxy": { entry: "packages/gateway/src/browser-proxy.ts", playwright: true },
199
+ "newsletter-public": { entry: "packages/newsletter/src/lambda-public.ts" },
200
+ "newsletter-campaign": { entry: "packages/newsletter/src/lambda-campaign.ts" },
201
+ "newsletter-ses-events": { entry: "packages/newsletter/src/lambda-ses-events.ts" }
202
+ };
203
+ function skipBundle(env = process.env) {
204
+ return env.FOUNDATION_SKIP_BUNDLE === "1";
205
+ }
206
+ var RELEASE_BUCKET_ENV = "FOUNDATION_RELEASE_BUCKET";
207
+ var RELEASE_DIR_ENV = "FOUNDATION_RELEASE_DIR";
208
+ function releaseCacheDir(version, env = process.env) {
209
+ const configured = env[RELEASE_DIR_ENV];
210
+ if (configured !== undefined && configured !== "")
211
+ return configured;
212
+ return join2(tmpdir2(), "foundation-release", version);
213
+ }
214
+ var manifestCache = new Map;
215
+ function readManifestFile(path2) {
216
+ const absolute = isAbsolute(path2) ? path2 : resolve(process.cwd(), path2);
217
+ const cached = manifestCache.get(absolute);
218
+ if (cached !== undefined)
219
+ return cached;
220
+ let text;
221
+ try {
222
+ text = readFileSync2(absolute, "utf8");
223
+ } catch {
224
+ throw new Error(`no release manifest at ${absolute}`);
225
+ }
226
+ const manifest = parseManifest(text);
227
+ manifestCache.set(absolute, manifest);
228
+ return manifest;
229
+ }
230
+ function releaseSource(scope) {
231
+ const node = scope.node;
232
+ const version = node.tryGetContext("release");
233
+ const manifestPath = node.tryGetContext("releaseManifest");
234
+ if ((version === undefined || version === "") && (manifestPath === undefined || manifestPath === ""))
235
+ return;
236
+ const path2 = manifestPath !== undefined && manifestPath !== "" ? manifestPath : join2(releaseCacheDir(version), "manifest.json");
237
+ const manifest = readManifestFile(path2);
238
+ if (version !== undefined && version !== "" && manifest.version !== version)
239
+ throw new Error(`-c release=${version} but ${path2} describes ${manifest.version}; pass the manifest for the version you are deploying`);
240
+ const bucket = node.tryGetContext("releaseBucket") ?? process.env[RELEASE_BUCKET_ENV] ?? "";
241
+ if (bucket === "")
242
+ throw new Error(`-c releaseBucket=<name> is required when deploying a release (or set ${RELEASE_BUCKET_ENV}); it is the bucket holding ${manifest.lambda.gateway?.key ?? "the release artifacts"}`);
243
+ return { version: manifest.version, bucket, manifest };
244
+ }
245
+ function releaseAccess(scope, env = process.env) {
246
+ const context = scope.node.tryGetContext("releaseBucket");
247
+ const fromContext = typeof context === "string" && context !== "" ? context : undefined;
248
+ const fromEnv = env[RELEASE_BUCKET_ENV];
249
+ const bucket = fromContext ?? (fromEnv !== undefined && fromEnv !== "" ? fromEnv : DEFAULT_RELEASE_BUCKET);
250
+ const signingKeyArn = configuredKeyArn(env);
251
+ return { bucket, ...signingKeyArn === undefined ? {} : { signingKeyArn } };
252
+ }
253
+ function releaseBucket(scope, source) {
254
+ const stack = cdk.Stack.of(scope);
255
+ const existing = stack.node.tryFindChild("FoundationReleaseBucket");
256
+ if (existing !== undefined)
257
+ return existing;
258
+ return s3.Bucket.fromBucketName(stack, "FoundationReleaseBucket", source.bucket);
259
+ }
260
+ function lambdaCode(scope, id) {
261
+ const source = releaseSource(scope);
262
+ if (source !== undefined) {
263
+ const artifact2 = source.manifest.lambda[id];
264
+ if (artifact2 === undefined)
265
+ throw new Error(`release ${source.version} carries no Lambda bundle for "${id}"; it was built by an older Foundation`);
266
+ return lambda.Code.fromBucket(releaseBucket(scope, source), artifact2.key);
267
+ }
268
+ if (skipBundle())
269
+ return lambda.Code.fromInline("export const handler = async () => ({statusCode: 200});");
270
+ const artifact = LAMBDA_ENTRY_POINTS[id];
271
+ const bundleRoot = lambdaBundleContext(WORKSPACE_ROOT);
272
+ return lambda.Code.fromAsset(bundleRoot, {
273
+ exclude: ["**/node_modules", "**/dist", "cdk.out"],
274
+ bundling: {
275
+ image: cdk.DockerImage.fromRegistry(`oven/bun:${BUN_VERSION}`),
276
+ command: [
277
+ "bash",
278
+ "-c",
279
+ `cd /asset-input && bun install --frozen-lockfile && ${bunBuild(artifact, "/asset-output").join(" ")}`
280
+ ],
281
+ local: {
282
+ tryBundle(outputDir) {
283
+ const bun = process.env.BUN_BIN ?? `${process.env.HOME}/.bun/bin/bun`;
284
+ const [, ...args] = bunBuild(artifact, outputDir);
285
+ const result = spawnSync(bun, args, { cwd: WORKSPACE_ROOT, stdio: "inherit" });
286
+ if (result.error !== undefined)
287
+ return false;
288
+ return result.status === 0;
289
+ }
290
+ }
291
+ }
292
+ });
293
+ }
294
+ function bunBuild(artifact, outputDir) {
295
+ return [
296
+ "bun",
297
+ "build",
298
+ artifact.entry,
299
+ "--target=node",
300
+ "--format=esm",
301
+ ...artifact.playwright === true ? [
302
+ "--external",
303
+ "electron",
304
+ "--external",
305
+ "chromium-bidi",
306
+ `--outdir=${outputDir}`,
307
+ "--entry-naming=index.mjs"
308
+ ] : [`--outfile=${join2(outputDir, "index.mjs")}`]
309
+ ];
310
+ }
311
+ function agentImage(scope, repository, tag) {
312
+ const source = releaseSource(scope);
313
+ if (source !== undefined)
314
+ return `${source.manifest.agentImage.repository}@${source.manifest.agentImage.digest}`;
315
+ if (tag === undefined || tag === "")
316
+ throw new Error("agentImage: a tag is required outside release mode");
317
+ return `${repository.repositoryUri}:${tag}`;
318
+ }
319
+ function agentImageTagRequired(scope) {
320
+ return releaseSource(scope) === undefined;
321
+ }
322
+ function releaseImageRepositoryArn(scope) {
323
+ const source = releaseSource(scope);
324
+ if (source === undefined)
325
+ return;
326
+ return ecrRepositoryArn(source.manifest.agentImage.repository);
327
+ }
328
+ function ecrRepositoryArn(repositoryUri) {
329
+ const match = /^(\d{12})\.dkr\.ecr\.([a-z0-9-]+)\.amazonaws\.com\/(.+)$/.exec(repositoryUri);
330
+ if (match === null)
331
+ throw new Error(`not an ECR repository URI: ${repositoryUri} (expected <account>.dkr.ecr.<region>.amazonaws.com/<name>)`);
332
+ const [, account, region, name] = match;
333
+ return `arn:aws:ecr:${region}:${account}:repository/${name}`;
334
+ }
335
+
336
+ export { RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, releaseAccess, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };