@hyperframes/aws-lambda 0.8.177 → 0.8.179

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,1006 @@
1
+ import {
2
+ downloadS3ObjectToFile,
3
+ downloadS3ObjectToFileVerified,
4
+ downloadSiteFiles,
5
+ ensureContentStoreObject,
6
+ formatExtension,
7
+ formatS3Uri,
8
+ isSiteManifestUri,
9
+ mapConcurrent,
10
+ parseS3Uri,
11
+ presignVerifiedS3GetUrl,
12
+ putContentStoreFile,
13
+ tarDirectory,
14
+ untarDirectory,
15
+ uploadContentAddressedFileToS3,
16
+ uploadFileToS3
17
+ } from "./chunk-ZY5YIH5W.js";
18
+ import {
19
+ CONTENT_STORE_PREFIX
20
+ } from "./chunk-YS3KY3C5.js";
21
+
22
+ // src/handler.ts
23
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
24
+ import { tmpdir as tmpdir2 } from "node:os";
25
+ import { basename, join as join2 } from "node:path";
26
+ import { GetAccountSettingsCommand, LambdaClient } from "@aws-sdk/client-lambda";
27
+ import { S3Client } from "@aws-sdk/client-s3";
28
+ import {
29
+ assemble,
30
+ chunksAreStitchedMp4,
31
+ DEFAULT_MAX_PARALLEL_CHUNKS,
32
+ listPlanV2ArtifactsForTarget,
33
+ materializePlanV2Target,
34
+ plan,
35
+ isPlanAudioArtifactPath,
36
+ PLAN_AUDIO_RELATIVE_PATH,
37
+ resolvePlanAudioPath,
38
+ planV2WithPublisher,
39
+ isPlanV2DeferredSourceArtifact,
40
+ readPlanV2Manifest,
41
+ renderChunk
42
+ } from "@hyperframes/producer/distributed";
43
+
44
+ // src/chromium.ts
45
+ import { existsSync } from "node:fs";
46
+ var ChromeBinaryUnavailableError = class extends Error {
47
+ // Lambda's runtime serializes the error envelope's `errorType` from
48
+ // `err.name`; this class-field override sets it across the structured
49
+ // clone. Read indirectly; fallow can't follow.
50
+ // fallow-ignore-next-line unused-class-member
51
+ name = "ChromeBinaryUnavailableError";
52
+ source;
53
+ resolvedPath;
54
+ constructor(source, resolvedPath, hint) {
55
+ super(`[chromium] Chrome binary unavailable (source=${source}): ${hint}`);
56
+ this.source = source;
57
+ this.resolvedPath = resolvedPath;
58
+ }
59
+ };
60
+ var SPARTICUZ_WEDGE_HINT = "@sparticuz/chromium.executablePath() returned a falsy value or a path that doesn't exist on disk. This typically happens after a chunk hits `Sandbox.Timedout` mid-extraction and leaves /tmp in a wedged state \u2014 subsequent invocations land on the same warm instance and never re-extract. Recycle the function (e.g. `aws lambda update-function-configuration ... --environment ...` with a bumped marker var, or redeploy via `hyperframes lambda deploy --skip-build`) to force fresh execution environments. Tracking: investigate the upstream wedge so this auto-recovers.";
61
+ function resolveChromeSource() {
62
+ const raw = process.env.HYPERFRAMES_LAMBDA_CHROME_SOURCE?.toLowerCase();
63
+ if (raw === "chrome-headless-shell" || raw === "shell") return "chrome-headless-shell";
64
+ return "sparticuz";
65
+ }
66
+ async function resolveChromeExecutablePath() {
67
+ const source = resolveChromeSource();
68
+ if (source === "sparticuz") {
69
+ const mod = await loadSparticuzChromium();
70
+ const path = await mod.executablePath();
71
+ if (!path || typeof path !== "string") {
72
+ throw new ChromeBinaryUnavailableError(source, null, SPARTICUZ_WEDGE_HINT);
73
+ }
74
+ if (!existsSync(path)) {
75
+ throw new ChromeBinaryUnavailableError(source, path, SPARTICUZ_WEDGE_HINT);
76
+ }
77
+ return path;
78
+ }
79
+ const explicit = process.env.HYPERFRAMES_LAMBDA_CHROME_PATH;
80
+ if (!explicit) {
81
+ throw new ChromeBinaryUnavailableError(
82
+ source,
83
+ null,
84
+ "HYPERFRAMES_LAMBDA_CHROME_SOURCE=chrome-headless-shell requires HYPERFRAMES_LAMBDA_CHROME_PATH to be set to the absolute path of the bundled binary."
85
+ );
86
+ }
87
+ if (!existsSync(explicit)) {
88
+ throw new ChromeBinaryUnavailableError(
89
+ source,
90
+ explicit,
91
+ `HYPERFRAMES_LAMBDA_CHROME_PATH=${JSON.stringify(explicit)} does not exist on disk.`
92
+ );
93
+ }
94
+ return explicit;
95
+ }
96
+ async function resolveChromeArgs() {
97
+ if (resolveChromeSource() !== "sparticuz") return [];
98
+ const mod = await loadSparticuzChromium();
99
+ return mod.args;
100
+ }
101
+ var cachedSparticuz = null;
102
+ async function loadSparticuzChromium() {
103
+ if (cachedSparticuz) return cachedSparticuz;
104
+ const mod = await import("@sparticuz/chromium");
105
+ const resolved = "default" in mod ? mod.default : mod;
106
+ cachedSparticuz = resolved;
107
+ return resolved;
108
+ }
109
+
110
+ // src/s3Stitch.ts
111
+ import {
112
+ AbortMultipartUploadCommand,
113
+ CompleteMultipartUploadCommand,
114
+ CreateMultipartUploadCommand,
115
+ GetObjectCommand,
116
+ UploadPartCommand,
117
+ UploadPartCopyCommand
118
+ } from "@aws-sdk/client-s3";
119
+ import { buildStitchedHeader, readMp4Layout } from "@hyperframes/producer/distributed";
120
+ var MIN_PART_BYTES = 5 * 1024 * 1024;
121
+ var segmentSize = (s) => "bytes" in s ? s.bytes.length : s.end - s.start;
122
+ function planParts(segments, min = MIN_PART_BYTES) {
123
+ const parts = [];
124
+ let pending = [];
125
+ let pendingSize = 0;
126
+ const flush = () => {
127
+ if (pending.length) parts.push({ upload: pending });
128
+ pending = [];
129
+ pendingSize = 0;
130
+ };
131
+ for (const segment of segments) {
132
+ const borrow = pendingSize ? min - pendingSize : 0;
133
+ if ("uri" in segment && segmentSize(segment) - borrow >= min) {
134
+ if (borrow) pending.push({ ...segment, end: segment.start + borrow });
135
+ flush();
136
+ parts.push({ copy: { ...segment, start: segment.start + borrow } });
137
+ continue;
138
+ }
139
+ pending.push(segment);
140
+ pendingSize += segmentSize(segment);
141
+ if (pendingSize >= min) flush();
142
+ }
143
+ flush();
144
+ return parts;
145
+ }
146
+ async function getRange(s3, uri, start, end) {
147
+ const { bucket, key } = parseS3Uri(uri);
148
+ const response = await s3.send(
149
+ new GetObjectCommand({ Bucket: bucket, Key: key, Range: `bytes=${start}-${end - 1}` })
150
+ );
151
+ if (!response.Body) throw new Error(`[s3Stitch] empty body for ${uri}`);
152
+ return Buffer.from(await response.Body.transformToByteArray());
153
+ }
154
+ async function sendPart(s3, part, partNumber, target) {
155
+ if ("copy" in part) {
156
+ const { uri, start, end } = part.copy;
157
+ const source = parseS3Uri(uri);
158
+ const result2 = await s3.send(
159
+ new UploadPartCopyCommand({
160
+ ...target,
161
+ PartNumber: partNumber,
162
+ CopySource: encodeURI(`${source.bucket}/${source.key}`),
163
+ CopySourceRange: `bytes=${start}-${end - 1}`
164
+ })
165
+ );
166
+ return result2.CopyPartResult?.ETag;
167
+ }
168
+ const body = await Promise.all(
169
+ part.upload.map((s) => "bytes" in s ? s.bytes : getRange(s3, s.uri, s.start, s.end))
170
+ );
171
+ const result = await s3.send(
172
+ new UploadPartCommand({ ...target, PartNumber: partNumber, Body: Buffer.concat(body) })
173
+ );
174
+ return result.ETag;
175
+ }
176
+ async function sendParts(s3, parts, target) {
177
+ const settled = await Promise.allSettled(
178
+ parts.map(async (part, index) => {
179
+ const etag = await sendPart(s3, part, index + 1, target);
180
+ if (!etag) throw new Error(`[s3Stitch] part ${index + 1} returned no ETag`);
181
+ return etag;
182
+ })
183
+ );
184
+ const failure = settled.find(
185
+ (result) => result.status === "rejected"
186
+ );
187
+ if (failure) throw failure.reason;
188
+ return settled.map((result) => result.value);
189
+ }
190
+ async function uploadParts(s3, parts, destUri, contentType) {
191
+ const { bucket, key } = parseS3Uri(destUri);
192
+ const created = await s3.send(
193
+ new CreateMultipartUploadCommand({ Bucket: bucket, Key: key, ContentType: contentType })
194
+ );
195
+ if (!created.UploadId) throw new Error(`[s3Stitch] no upload id for ${destUri}`);
196
+ const target = { Bucket: bucket, Key: key, UploadId: created.UploadId };
197
+ try {
198
+ const etags = await sendParts(s3, parts, target);
199
+ await s3.send(
200
+ new CompleteMultipartUploadCommand({
201
+ ...target,
202
+ MultipartUpload: { Parts: etags.map((ETag, index) => ({ ETag, PartNumber: index + 1 })) }
203
+ })
204
+ );
205
+ } catch (error) {
206
+ await s3.send(new AbortMultipartUploadCommand(target)).catch((abortError) => {
207
+ console.error(
208
+ `[s3Stitch] abort failed for ${destUri}; parts remain until a lifecycle rule removes them`,
209
+ abortError
210
+ );
211
+ });
212
+ throw error;
213
+ }
214
+ }
215
+ async function stitchMp4InS3(s3, chunkUris, destUri, minPartBytes = MIN_PART_BYTES) {
216
+ const layouts = await Promise.all(
217
+ chunkUris.map((uri) => readMp4Layout((start, end) => getRange(s3, uri, start, end)))
218
+ );
219
+ const segments = [
220
+ { bytes: buildStitchedHeader(layouts) },
221
+ ...layouts.map((layout, i) => ({
222
+ uri: chunkUris[i],
223
+ start: layout.payloadStart,
224
+ end: layout.payloadEnd
225
+ }))
226
+ ];
227
+ const parts = planParts(segments, minPartBytes);
228
+ await uploadParts(s3, parts, destUri, "video/mp4");
229
+ const uploaded = parts.flatMap((part) => "upload" in part ? part.upload : []);
230
+ return {
231
+ fileSize: segments.reduce((total, s) => total + segmentSize(s), 0),
232
+ parts: parts.length,
233
+ downloadedBytes: uploaded.reduce((total, s) => total + ("uri" in s ? segmentSize(s) : 0), 0)
234
+ };
235
+ }
236
+
237
+ // src/s3PlanV2Publisher.ts
238
+ import { createHash } from "node:crypto";
239
+ import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
240
+ import { tmpdir } from "node:os";
241
+ import { join } from "node:path";
242
+ import {
243
+ PlanV2IntegrityError
244
+ } from "@hyperframes/producer/distributed";
245
+ function isRecord(value) {
246
+ return value !== null && typeof value === "object" && !Array.isArray(value);
247
+ }
248
+ function assertSha256(value, label) {
249
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
250
+ throw new PlanV2IntegrityError(`${label} must be a lowercase SHA-256 digest`);
251
+ }
252
+ return value;
253
+ }
254
+ function manifestDigests(manifestBytes) {
255
+ let value;
256
+ try {
257
+ value = JSON.parse(manifestBytes);
258
+ } catch {
259
+ throw new PlanV2IntegrityError("S3 publisher received invalid manifest JSON");
260
+ }
261
+ if (!isRecord(value) || !Array.isArray(value.artifacts)) {
262
+ throw new PlanV2IntegrityError("S3 publisher manifest requires an artifacts array");
263
+ }
264
+ return new Set(
265
+ value.artifacts.map((artifact, index) => {
266
+ if (!isRecord(artifact)) {
267
+ throw new PlanV2IntegrityError(`S3 publisher artifacts[${index}] must be an object`);
268
+ }
269
+ return assertSha256(artifact.sha256, `S3 publisher artifacts[${index}].sha256`);
270
+ })
271
+ );
272
+ }
273
+ function trimTrailingSlash(value) {
274
+ let end = value.length;
275
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
276
+ return value.slice(0, end);
277
+ }
278
+ var S3PlanV2ArtifactPublisher = class {
279
+ artifactPrefix;
280
+ manifestUri;
281
+ #s3;
282
+ #bucket;
283
+ #temporaryRoot;
284
+ #publishedDigests = /* @__PURE__ */ new Set();
285
+ #state = "open";
286
+ constructor(options) {
287
+ const outputPrefix = `${trimTrailingSlash(options.planOutputS3Prefix)}/v2`;
288
+ const { bucket } = parseS3Uri(outputPrefix);
289
+ this.#s3 = options.s3;
290
+ this.#bucket = bucket;
291
+ this.artifactPrefix = formatS3Uri({ bucket, key: CONTENT_STORE_PREFIX });
292
+ this.manifestUri = `${outputPrefix}/manifest.json`;
293
+ this.#temporaryRoot = options.temporaryRoot ?? tmpdir();
294
+ mkdirSync(this.#temporaryRoot, { recursive: true });
295
+ }
296
+ async putBlob(blob) {
297
+ this.#assertOpen("publish a blob");
298
+ const digest = assertSha256(blob.sha256, "S3 published blob sha256");
299
+ if (this.#publishedDigests.has(digest)) return;
300
+ const sourceSize = statSync(blob.sourcePath).size;
301
+ if (sourceSize !== blob.sizeBytes) {
302
+ throw new PlanV2IntegrityError(
303
+ `S3 published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
304
+ );
305
+ }
306
+ await putContentStoreFile(this.#s3, blob.sourcePath, this.#bucket, digest);
307
+ this.#publishedDigests.add(digest);
308
+ }
309
+ async referenceStoredBlob(blob) {
310
+ this.#assertOpen("reference a stored blob");
311
+ const digest = assertSha256(blob.sha256, "S3 referenced blob sha256");
312
+ await ensureContentStoreObject(this.#s3, this.#bucket, digest, blob.sizeBytes);
313
+ this.#publishedDigests.add(digest);
314
+ }
315
+ async commitManifest(manifestBytes) {
316
+ this.#assertOpen("commit a manifest");
317
+ for (const digest of manifestDigests(manifestBytes)) {
318
+ if (!this.#publishedDigests.has(digest)) {
319
+ throw new PlanV2IntegrityError(
320
+ `cannot commit S3 manifest before referenced blob is durable: ${digest}`
321
+ );
322
+ }
323
+ }
324
+ const manifestDigest = createHash("sha256").update(manifestBytes, "utf8").digest("hex");
325
+ const stagingDir = mkdtempSync(join(this.#temporaryRoot, "hf-plan-v2-manifest-"));
326
+ const manifestPath = join(stagingDir, "manifest.json");
327
+ try {
328
+ writeFileSync(manifestPath, manifestBytes, "utf8");
329
+ await uploadContentAddressedFileToS3(
330
+ this.#s3,
331
+ manifestPath,
332
+ this.manifestUri,
333
+ manifestDigest,
334
+ "application/json"
335
+ );
336
+ this.#state = "committed";
337
+ } finally {
338
+ rmSync(stagingDir, { recursive: true, force: true });
339
+ }
340
+ }
341
+ async abort() {
342
+ if (this.#state === "open") this.#state = "aborted";
343
+ }
344
+ #assertOpen(operation) {
345
+ if (this.#state !== "open") {
346
+ throw new PlanV2IntegrityError(`cannot ${operation} after publisher is ${this.#state}`);
347
+ }
348
+ }
349
+ };
350
+
351
+ // src/handler.ts
352
+ var cachedS3Client = null;
353
+ function getS3Client() {
354
+ if (cachedS3Client) return cachedS3Client;
355
+ cachedS3Client = new S3Client({});
356
+ return cachedS3Client;
357
+ }
358
+ async function handler(event, deps) {
359
+ const unwrapped = unwrapEvent(event);
360
+ validatePlanProtocolShape(unwrapped);
361
+ validateEventS3Uris(unwrapped);
362
+ primeRuntimeEnv();
363
+ logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
364
+ try {
365
+ switch (unwrapped.Action) {
366
+ case "plan":
367
+ return await handlePlan(unwrapped, deps);
368
+ case "renderChunk":
369
+ return await handleRenderChunk(unwrapped, deps);
370
+ case "assemble":
371
+ return await handleAssemble(unwrapped, deps);
372
+ default: {
373
+ const _exhaustive = unwrapped;
374
+ throw new Error(
375
+ `[handler] unknown Action: ${JSON.stringify(
376
+ _exhaustive.Action
377
+ )}. Expected one of "plan", "renderChunk", "assemble".`
378
+ );
379
+ }
380
+ }
381
+ } catch (err) {
382
+ normalizeTerminalErrorName(err);
383
+ logEvent({
384
+ event: "handler_error",
385
+ action: unwrapped.Action,
386
+ input: summarizeEvent(unwrapped),
387
+ message: err instanceof Error ? err.message : String(err),
388
+ name: err instanceof Error ? err.name : void 0
389
+ });
390
+ throw err;
391
+ }
392
+ }
393
+ function normalizeTerminalErrorName(error) {
394
+ if (!error || typeof error !== "object") return;
395
+ const candidate = error;
396
+ if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE" || candidate.code === "FONT_FETCH_FAILED" || candidate.code === "FONT_FETCH_UNAVAILABLE" || candidate.code === "NOT_MEDIA_PAYLOAD" || candidate.code === "VIDEO_SOURCE_UNRENDERABLE" || candidate.code === "VIDEO_EXTRACTION_FAILED" || candidate.code === "INVALID_VIDEO_METADATA" || candidate.code === "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED" || candidate.code === "CHUNK_EXCEEDS_WORKER_TIMEOUT") {
397
+ candidate.name = candidate.code;
398
+ }
399
+ }
400
+ var MAX_ENVELOPE_DEPTH = 4;
401
+ function unwrapEvent(event) {
402
+ let cursor = event;
403
+ for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {
404
+ if (cursor && typeof cursor === "object") {
405
+ const obj = cursor;
406
+ if (typeof obj.Action === "string" && isLambdaAction(obj.Action)) {
407
+ return cursor;
408
+ }
409
+ if ("Payload" in obj) {
410
+ cursor = obj.Payload;
411
+ continue;
412
+ }
413
+ if ("Input" in obj) {
414
+ cursor = obj.Input;
415
+ continue;
416
+ }
417
+ }
418
+ break;
419
+ }
420
+ throw new Error(
421
+ `[handler] event has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`
422
+ );
423
+ }
424
+ function isLambdaAction(value) {
425
+ return value === "plan" || value === "renderChunk" || value === "assemble";
426
+ }
427
+ function validatePlanProtocolShape(event) {
428
+ const raw = event;
429
+ const protocol = raw.PlanProtocol;
430
+ if (protocol !== void 0 && protocol !== "v1" && protocol !== "v2") {
431
+ const error = new Error(
432
+ `[handler] unsupported PlanProtocol ${JSON.stringify(protocol)}; expected "v1", "v2", or absent`
433
+ );
434
+ error.name = "PLAN_PROTOCOL_UNSUPPORTED";
435
+ throw error;
436
+ }
437
+ if (event.Action === "plan") return;
438
+ const effectiveProtocol = protocol ?? "v2";
439
+ const hasV1Locator = typeof raw.PlanS3Uri === "string";
440
+ const hasV2Manifest = typeof raw.PlanV2ManifestS3Uri === "string";
441
+ const hasV2Prefix = typeof raw.PlanV2ArtifactS3Prefix === "string";
442
+ const valid = effectiveProtocol === "v2" ? !hasV1Locator && hasV2Manifest && hasV2Prefix : hasV1Locator && !hasV2Manifest && !hasV2Prefix;
443
+ if (!valid) {
444
+ const error = new Error(
445
+ `[handler] ${effectiveProtocol} ${event.Action} event has mixed or missing plan locators`
446
+ );
447
+ error.name = "PLAN_PROTOCOL_UNSUPPORTED";
448
+ throw error;
449
+ }
450
+ if (effectiveProtocol === "v2" && event.Action === "assemble" && event.AudioS3Uri !== null) {
451
+ const error = new Error("[handler] v2 assemble audio must be materialized from the manifest");
452
+ error.name = "PLAN_PROTOCOL_UNSUPPORTED";
453
+ throw error;
454
+ }
455
+ }
456
+ function logEvent(payload) {
457
+ console.log(JSON.stringify(payload));
458
+ }
459
+ function summarizeEvent(event) {
460
+ switch (event.Action) {
461
+ case "plan":
462
+ return {
463
+ projectS3Uri: event.ProjectS3Uri,
464
+ planOutputS3Prefix: event.PlanOutputS3Prefix,
465
+ planProtocol: event.PlanProtocol ?? "v2",
466
+ format: event.Config.format,
467
+ fps: event.Config.fps
468
+ };
469
+ case "renderChunk":
470
+ return {
471
+ planProtocol: event.PlanProtocol ?? "v2",
472
+ ...event.PlanProtocol !== "v1" ? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri } : { planS3Uri: event.PlanS3Uri },
473
+ chunkIndex: event.ChunkIndex,
474
+ format: event.Format
475
+ };
476
+ case "assemble":
477
+ return {
478
+ planProtocol: event.PlanProtocol ?? "v2",
479
+ ...event.PlanProtocol !== "v1" ? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri } : { planS3Uri: event.PlanS3Uri },
480
+ chunkCount: event.ChunkS3Uris.length,
481
+ hasAudio: event.AudioS3Uri !== null,
482
+ outputS3Uri: event.OutputS3Uri,
483
+ format: event.Format
484
+ };
485
+ }
486
+ }
487
+ var runtimeEnvPrimed = false;
488
+ function primeRuntimeEnv() {
489
+ if (runtimeEnvPrimed) return;
490
+ runtimeEnvPrimed = true;
491
+ const taskRoot = process.env.LAMBDA_TASK_ROOT ?? "/var/task";
492
+ const bin = join2(taskRoot, "bin");
493
+ if (existsSync2(bin)) {
494
+ process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
495
+ }
496
+ }
497
+ var ACCOUNT_CONCURRENCY_SHARE = 0.9;
498
+ async function resolveWorkerConcurrency(event, deps) {
499
+ if (event.Config.workerConcurrency !== void 0) return event.Config.workerConcurrency;
500
+ try {
501
+ return await functionConcurrency(deps?.lambda);
502
+ } catch (err) {
503
+ logEvent({
504
+ event: "worker_concurrency",
505
+ level: "WARN",
506
+ source: "fallback",
507
+ concurrency: DEFAULT_MAX_PARALLEL_CHUNKS,
508
+ message: `Could not read the account's Lambda concurrency (${String(err)}); planning for ${DEFAULT_MAX_PARALLEL_CHUNKS} chunks at once. Allow lambda:GetAccountSettings on the function role, or set the concurrency explicitly.`
509
+ });
510
+ return DEFAULT_MAX_PARALLEL_CHUNKS;
511
+ }
512
+ }
513
+ async function functionConcurrency(lambda = new LambdaClient({})) {
514
+ const reserved = Number(process.env.HYPERFRAMES_LAMBDA_RESERVED_CONCURRENCY);
515
+ if (reserved > 0) return reserved;
516
+ const { AccountLimit } = await lambda.send(new GetAccountSettingsCommand({}));
517
+ const unreserved = AccountLimit?.UnreservedConcurrentExecutions;
518
+ if (unreserved === void 0) throw new Error("no UnreservedConcurrentExecutions in the reply");
519
+ const concurrency = Math.max(1, Math.floor(unreserved * ACCOUNT_CONCURRENCY_SHARE));
520
+ logEvent({ event: "worker_concurrency", source: "account", unreserved, concurrency });
521
+ return concurrency;
522
+ }
523
+ async function planConfig(event, deps) {
524
+ const config = {
525
+ ...event.Config,
526
+ workerConcurrency: await resolveWorkerConcurrency(event, deps)
527
+ };
528
+ const remainingMs = deps?.getRemainingTimeInMillis?.();
529
+ if (config.workerTimeoutSeconds === void 0 && remainingMs !== void 0) {
530
+ config.workerTimeoutSeconds = Math.ceil(remainingMs / 1e3);
531
+ }
532
+ return config;
533
+ }
534
+ async function handlePlan(event, deps) {
535
+ if (event.PlanProtocol !== "v1") {
536
+ return handlePlanV2(event, deps);
537
+ }
538
+ const started = Date.now();
539
+ const config = await planConfig(event, deps);
540
+ const s3 = deps?.s3 ?? getS3Client();
541
+ const primitive = deps?.primitives?.plan ?? plan;
542
+ if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
543
+ const chromePath = await resolveChromeExecutablePath();
544
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
545
+ }
546
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-plan-"));
547
+ const planDir = join2(work, "plan");
548
+ try {
549
+ const { projectDir } = await fetchProject(s3, event.ProjectS3Uri, work);
550
+ const result = await primitive(projectDir, config, planDir, {
551
+ chunksCarryAudio: true
552
+ });
553
+ const planTar = join2(work, "plan.tar.gz");
554
+ await tarDirectory(planDir, planTar);
555
+ const planTarUri = `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/plan.tar.gz`;
556
+ const audioPath = join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
557
+ const hasAudio = existsSync2(audioPath) && statSync2(audioPath).size > 0;
558
+ const audioUri = hasAudio ? `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/${PLAN_AUDIO_RELATIVE_PATH}` : null;
559
+ await Promise.all([
560
+ uploadFileToS3(s3, planTar, planTarUri, "application/gzip"),
561
+ hasAudio && audioUri ? uploadFileToS3(s3, audioPath, audioUri, "audio/aac") : null
562
+ ]);
563
+ return {
564
+ Action: "plan",
565
+ PlanS3Uri: planTarUri,
566
+ PlanHash: result.planHash,
567
+ ChunkCount: result.chunkCount,
568
+ MaxChunkConcurrency: Math.min(config.workerConcurrency, result.chunkCount),
569
+ TotalFrames: result.totalFrames,
570
+ Fps: result.fps,
571
+ Width: result.width,
572
+ Height: result.height,
573
+ Format: result.format,
574
+ HasAudio: audioUri !== null,
575
+ AudioS3Uri: audioUri,
576
+ FfmpegVersion: result.ffmpegVersion,
577
+ ProducerVersion: result.producerVersion,
578
+ DurationMs: Date.now() - started
579
+ };
580
+ } finally {
581
+ cleanupDir(work);
582
+ }
583
+ }
584
+ async function fetchProject(s3, projectS3Uri, work, leaveVideosRemote = false) {
585
+ const projectDir = join2(work, "project");
586
+ if (isSiteManifestUri(projectS3Uri)) {
587
+ const remoteFiles = await downloadSiteFiles(s3, projectS3Uri, projectDir, leaveVideosRemote);
588
+ return { projectDir, remoteFiles };
589
+ }
590
+ const projectArchive = join2(work, "project.tar.gz");
591
+ let stageStarted = Date.now();
592
+ await downloadS3ObjectToFile(s3, projectS3Uri, projectArchive);
593
+ logEvent({ event: "plan_stage", stage: "download", ms: Date.now() - stageStarted });
594
+ stageStarted = Date.now();
595
+ await untarDirectory(projectArchive, projectDir);
596
+ logEvent({ event: "plan_stage", stage: "untar", ms: Date.now() - stageStarted });
597
+ return { projectDir, remoteFiles: {} };
598
+ }
599
+ async function handlePlanV2(event, deps) {
600
+ const started = Date.now();
601
+ const config = await planConfig(event, deps);
602
+ const s3 = deps?.s3 ?? getS3Client();
603
+ const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
604
+ if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
605
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = await resolveChromeExecutablePath();
606
+ }
607
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-plan-v2-"));
608
+ try {
609
+ const { projectDir, remoteFiles } = await fetchProject(s3, event.ProjectS3Uri, work, true);
610
+ const publisher = new S3PlanV2ArtifactPublisher({
611
+ s3,
612
+ planOutputS3Prefix: event.PlanOutputS3Prefix,
613
+ temporaryRoot: work
614
+ });
615
+ const manifest = await primitive(projectDir, config, publisher, {
616
+ stagingParentDir: work,
617
+ remoteFiles,
618
+ chunksCarryAudio: true
619
+ });
620
+ return {
621
+ Action: "plan",
622
+ PlanProtocol: "v2",
623
+ PlanV2ManifestS3Uri: publisher.manifestUri,
624
+ PlanV2ArtifactS3Prefix: publisher.artifactPrefix,
625
+ PlanHash: manifest.planHash,
626
+ ChunkCount: manifest.chunkCount,
627
+ MaxChunkConcurrency: Math.min(config.workerConcurrency, manifest.chunkCount),
628
+ TotalFrames: manifest.totalFrames,
629
+ Fps: manifest.fps,
630
+ Width: manifest.width,
631
+ Height: manifest.height,
632
+ Format: manifest.format,
633
+ HasAudio: manifest.artifacts.some((artifact) => isPlanAudioArtifactPath(artifact.path)),
634
+ AudioS3Uri: null,
635
+ FfmpegVersion: manifest.ffmpegVersion,
636
+ ProducerVersion: manifest.producerVersion,
637
+ DurationMs: Date.now() - started
638
+ };
639
+ } finally {
640
+ cleanupDir(work);
641
+ }
642
+ }
643
+ async function handleRenderChunk(event, deps) {
644
+ if (event.PlanProtocol !== "v1") {
645
+ return handleRenderChunkV2(event, deps);
646
+ }
647
+ const started = Date.now();
648
+ const s3 = deps?.s3 ?? getS3Client();
649
+ const primitive = deps?.primitives?.renderChunk ?? renderChunk;
650
+ if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
651
+ const chromePath = await resolveChromeExecutablePath();
652
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
653
+ }
654
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-"));
655
+ const planTar = join2(work, "plan.tar.gz");
656
+ const planDir = join2(work, "plan");
657
+ try {
658
+ await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
659
+ await untarDirectory(planTar, planDir);
660
+ verifyPlanHash(planDir, event.PlanHash);
661
+ const chunkOutputBase = join2(
662
+ work,
663
+ event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
664
+ );
665
+ const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
666
+ const chunkUri = await uploadChunkOutput(
667
+ s3,
668
+ result,
669
+ event.ChunkOutputS3Prefix,
670
+ event.ChunkIndex
671
+ );
672
+ return {
673
+ Action: "renderChunk",
674
+ ChunkS3Uri: chunkUri,
675
+ ChunkIndex: event.ChunkIndex,
676
+ Sha256: result.sha256,
677
+ FramesEncoded: result.framesEncoded,
678
+ CaptureMode: result.captureMode,
679
+ DurationMs: Date.now() - started
680
+ };
681
+ } finally {
682
+ cleanupDir(work);
683
+ }
684
+ }
685
+ async function handleRenderChunkV2(event, deps) {
686
+ const started = Date.now();
687
+ const s3 = deps?.s3 ?? getS3Client();
688
+ const primitive = deps?.primitives?.renderChunk ?? renderChunk;
689
+ if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
690
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = await resolveChromeExecutablePath();
691
+ }
692
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-v2-"));
693
+ try {
694
+ const { planDir } = await downloadAndMaterializePlanV2(
695
+ s3,
696
+ event,
697
+ { role: "chunk", chunkIndex: event.ChunkIndex },
698
+ work
699
+ );
700
+ const chunkOutputBase = join2(
701
+ work,
702
+ event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
703
+ );
704
+ const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
705
+ const chunkUri = await uploadChunkOutput(
706
+ s3,
707
+ result,
708
+ event.ChunkOutputS3Prefix,
709
+ event.ChunkIndex
710
+ );
711
+ return {
712
+ Action: "renderChunk",
713
+ ChunkS3Uri: chunkUri,
714
+ ChunkIndex: event.ChunkIndex,
715
+ Sha256: result.sha256,
716
+ FramesEncoded: result.framesEncoded,
717
+ CaptureMode: result.captureMode,
718
+ DurationMs: Date.now() - started
719
+ };
720
+ } finally {
721
+ cleanupDir(work);
722
+ }
723
+ }
724
+ async function uploadChunkOutput(s3, result, prefix, chunkIndex) {
725
+ const trimmed = trimTrailingSlash2(prefix);
726
+ if (result.outputKind === "file") {
727
+ const ext = result.outputPath.slice(result.outputPath.lastIndexOf("."));
728
+ const uri2 = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;
729
+ await uploadFileToS3(s3, result.outputPath, uri2);
730
+ return uri2;
731
+ }
732
+ const tarball = `${result.outputPath}.tar.gz`;
733
+ await tarDirectory(result.outputPath, tarball);
734
+ const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;
735
+ await uploadFileToS3(s3, tarball, uri, "application/gzip");
736
+ return uri;
737
+ }
738
+ async function handleAssemble(event, deps) {
739
+ if (event.PlanProtocol !== "v1") {
740
+ return handleAssembleV2(event, deps);
741
+ }
742
+ const started = Date.now();
743
+ const s3 = deps?.s3 ?? getS3Client();
744
+ const primitive = deps?.primitives?.assemble ?? assemble;
745
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-"));
746
+ const planTar = join2(work, "plan.tar.gz");
747
+ const planDir = join2(work, "plan");
748
+ try {
749
+ await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
750
+ await untarDirectory(planTar, planDir);
751
+ const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
752
+ let audioPath = null;
753
+ if (event.AudioS3Uri) {
754
+ audioPath = resolvePlanAudioPath(planDir) ?? join2(planDir, PLAN_AUDIO_RELATIVE_PATH);
755
+ await downloadS3ObjectToFile(s3, event.AudioS3Uri, audioPath);
756
+ }
757
+ const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
758
+ const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
759
+ cfr: event.Cfr === true
760
+ });
761
+ if (event.Format === "png-sequence") {
762
+ const tarball = `${finalOutput}.tar.gz`;
763
+ await tarDirectory(finalOutput, tarball);
764
+ await uploadFileToS3(s3, tarball, event.OutputS3Uri, "application/gzip");
765
+ } else {
766
+ await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
767
+ }
768
+ return {
769
+ Action: "assemble",
770
+ OutputS3Uri: event.OutputS3Uri,
771
+ FramesEncoded: result.framesEncoded,
772
+ FileSize: result.fileSize,
773
+ DurationMs: Date.now() - started
774
+ };
775
+ } finally {
776
+ cleanupDir(work);
777
+ }
778
+ }
779
+ async function handleAssembleV2(event, deps) {
780
+ const started = Date.now();
781
+ const s3 = deps?.s3 ?? getS3Client();
782
+ const primitive = deps?.primitives?.assemble ?? assemble;
783
+ const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-v2-"));
784
+ try {
785
+ const { planDir, manifest } = await downloadAndMaterializePlanV2(
786
+ s3,
787
+ event,
788
+ { role: "assembler" },
789
+ work
790
+ );
791
+ if (chunksAreStitchedMp4(planDir, event.Cfr === true)) {
792
+ return await stitchChunksInS3(s3, event, manifest, started);
793
+ }
794
+ const audioPath = resolvePlanAudioPath(planDir);
795
+ const chunksStarted = Date.now();
796
+ const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
797
+ const chunksMs = Date.now() - chunksStarted;
798
+ const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
799
+ const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
800
+ cfr: event.Cfr === true
801
+ });
802
+ const uploadStarted = Date.now();
803
+ if (event.Format === "png-sequence") {
804
+ const tarball = `${finalOutput}.tar.gz`;
805
+ await tarDirectory(finalOutput, tarball);
806
+ await uploadFileToS3(s3, tarball, event.OutputS3Uri, "application/gzip");
807
+ } else {
808
+ await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
809
+ }
810
+ logEvent({
811
+ event: "assemble_stages",
812
+ planMs: chunksStarted - started,
813
+ chunksMs,
814
+ assembleMs: result.durationMs,
815
+ uploadMs: Date.now() - uploadStarted
816
+ });
817
+ return {
818
+ Action: "assemble",
819
+ OutputS3Uri: event.OutputS3Uri,
820
+ FramesEncoded: result.framesEncoded,
821
+ FileSize: result.fileSize,
822
+ DurationMs: Date.now() - started
823
+ };
824
+ } finally {
825
+ cleanupDir(work);
826
+ }
827
+ }
828
+ async function stitchChunksInS3(s3, event, plan2, started) {
829
+ if (event.ChunkS3Uris.length !== plan2.chunkCount) {
830
+ throw new Error(
831
+ `[handler] ${event.ChunkS3Uris.length} chunk URIs for a plan of ${plan2.chunkCount} chunks`
832
+ );
833
+ }
834
+ const stitchStarted = Date.now();
835
+ const stitched = await stitchMp4InS3(s3, event.ChunkS3Uris, event.OutputS3Uri);
836
+ logEvent({
837
+ event: "assemble_stages",
838
+ mode: "s3-stitch",
839
+ planMs: stitchStarted - started,
840
+ stitchMs: Date.now() - stitchStarted,
841
+ parts: stitched.parts,
842
+ downloadedBytes: stitched.downloadedBytes
843
+ });
844
+ return {
845
+ Action: "assemble",
846
+ OutputS3Uri: event.OutputS3Uri,
847
+ FramesEncoded: plan2.totalFrames,
848
+ FileSize: stitched.fileSize,
849
+ DurationMs: Date.now() - started
850
+ };
851
+ }
852
+ async function downloadAndMaterializePlanV2(s3, event, target, work) {
853
+ const transportDir = join2(work, "plan-v2");
854
+ mkdirSync2(transportDir, { recursive: true });
855
+ await downloadS3ObjectToFile(s3, event.PlanV2ManifestS3Uri, join2(transportDir, "plan.json"));
856
+ const manifest = readPlanV2Manifest(transportDir);
857
+ if (manifest.planHash !== event.PlanHash) {
858
+ throwPlanHashMismatch(event.PlanHash, manifest.planHash);
859
+ }
860
+ const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
861
+ const remoteSources = await presignDeferredSources(s3, event.PlanV2ArtifactS3Prefix, artifacts);
862
+ const uniqueArtifacts = [
863
+ ...new Map(
864
+ artifacts.filter((artifact) => !Object.hasOwn(remoteSources, artifact.path)).map((artifact) => [artifact.sha256, artifact])
865
+ ).values()
866
+ ];
867
+ await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
868
+ await downloadPlanV2Artifact(s3, event.PlanV2ArtifactS3Prefix, transportDir, artifact);
869
+ });
870
+ const planDir = join2(work, "plan");
871
+ materializePlanV2Target(transportDir, target, planDir, { remoteSources });
872
+ return { planDir, manifest };
873
+ }
874
+ var DEFERRED_SOURCE_URL_TTL_SECONDS = 3600;
875
+ async function presignDeferredSources(s3, artifactPrefix, artifacts) {
876
+ const entries = await Promise.all(
877
+ artifacts.filter(isPlanV2DeferredSourceArtifact).map(
878
+ async (artifact) => [
879
+ artifact.path,
880
+ await presignVerifiedS3GetUrl(
881
+ s3,
882
+ planV2BlobUri(artifactPrefix, artifact.sha256),
883
+ artifact.sha256,
884
+ artifact.sizeBytes,
885
+ DEFERRED_SOURCE_URL_TTL_SECONDS
886
+ )
887
+ ]
888
+ )
889
+ );
890
+ return Object.fromEntries(entries);
891
+ }
892
+ async function downloadPlanV2Artifact(s3, artifactPrefix, planV2Dir, artifact) {
893
+ await downloadS3ObjectToFileVerified(
894
+ s3,
895
+ planV2BlobUri(artifactPrefix, artifact.sha256),
896
+ planV2BlobPath(planV2Dir, artifact.sha256),
897
+ artifact.sha256
898
+ );
899
+ }
900
+ function planV2BlobPath(planV2Dir, digest) {
901
+ return join2(planV2Dir, "artifacts", "sha256", digest.slice(0, 2), digest);
902
+ }
903
+ function planV2BlobUri(prefix, digest) {
904
+ return `${trimTrailingSlash2(prefix)}/${digest.slice(0, 2)}/${digest}`;
905
+ }
906
+ function throwPlanHashMismatch(expected, actual) {
907
+ const error = new Error(
908
+ `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match v2 manifest planHash=${actual}`
909
+ );
910
+ error.name = "PLAN_HASH_MISMATCH";
911
+ throw error;
912
+ }
913
+ async function downloadChunkObjects(s3, uris, workDir, format) {
914
+ const chunksDir = join2(workDir, "chunks");
915
+ mkdirSync2(chunksDir, { recursive: true });
916
+ const local = new Array(uris.length);
917
+ await Promise.all(
918
+ uris.map(async (uri, i) => {
919
+ if (!uri) {
920
+ throw new Error(`[handler] chunk URI at index ${i} is empty`);
921
+ }
922
+ const { key } = parseS3Uri(uri);
923
+ const localPath = join2(chunksDir, basename(key));
924
+ await downloadS3ObjectToFile(s3, uri, localPath);
925
+ if (format === "png-sequence") {
926
+ const dirPath = join2(chunksDir, `frames-${pad(i)}`);
927
+ await untarDirectory(localPath, dirPath);
928
+ local[i] = dirPath;
929
+ } else {
930
+ local[i] = localPath;
931
+ }
932
+ })
933
+ );
934
+ return local;
935
+ }
936
+ function getEventS3Uris(event) {
937
+ switch (event.Action) {
938
+ case "plan":
939
+ return [event.ProjectS3Uri, event.PlanOutputS3Prefix];
940
+ case "renderChunk":
941
+ return event.PlanProtocol !== "v1" ? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix, event.ChunkOutputS3Prefix] : [event.PlanS3Uri, event.ChunkOutputS3Prefix];
942
+ case "assemble":
943
+ return [
944
+ ...event.PlanProtocol !== "v1" ? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix] : [event.PlanS3Uri],
945
+ ...event.ChunkS3Uris,
946
+ event.OutputS3Uri,
947
+ event.AudioS3Uri
948
+ ].filter((u) => u != null);
949
+ }
950
+ }
951
+ function validateEventS3Uris(event) {
952
+ const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();
953
+ if (!allowedBucket) return;
954
+ for (const uri of getEventS3Uris(event)) {
955
+ const { bucket } = parseS3Uri(uri);
956
+ if (bucket !== allowedBucket) {
957
+ const err = new Error(
958
+ `[handler] S3_URI_NOT_ALLOWED: URI ${JSON.stringify(uri)} targets bucket "${bucket}" but only "${allowedBucket}" is permitted`
959
+ );
960
+ err.name = "S3_URI_NOT_ALLOWED";
961
+ throw err;
962
+ }
963
+ }
964
+ }
965
+ function pad(n) {
966
+ return n.toString().padStart(4, "0");
967
+ }
968
+ function trimTrailingSlash2(prefix) {
969
+ return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
970
+ }
971
+ function cleanupDir(dir) {
972
+ try {
973
+ rmSync2(dir, { recursive: true, force: true });
974
+ } catch {
975
+ }
976
+ }
977
+ function verifyPlanHash(planDir, expected) {
978
+ const planJsonPath = join2(planDir, "plan.json");
979
+ let parsed;
980
+ try {
981
+ parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
982
+ } catch (err) {
983
+ const msg = err instanceof Error ? err.message : String(err);
984
+ const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
985
+ error.name = "PLAN_HASH_MISMATCH";
986
+ throw error;
987
+ }
988
+ const actual = parsed.planHash;
989
+ if (typeof actual !== "string" || actual !== expected) {
990
+ const error = new Error(
991
+ `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
992
+ );
993
+ error.name = "PLAN_HASH_MISMATCH";
994
+ throw error;
995
+ }
996
+ }
997
+
998
+ export {
999
+ ChromeBinaryUnavailableError,
1000
+ resolveChromeSource,
1001
+ resolveChromeExecutablePath,
1002
+ resolveChromeArgs,
1003
+ S3PlanV2ArtifactPublisher,
1004
+ handler,
1005
+ unwrapEvent
1006
+ };