@hyperframes/aws-lambda 0.7.71 → 0.7.73
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 +21 -1
- package/dist/cdk/HyperframesRenderStack.d.ts.map +1 -1
- package/dist/cdk/index.js +130 -2
- package/dist/cdk/index.js.map +2 -2
- package/dist/events.d.ts +61 -14
- package/dist/events.d.ts.map +1 -1
- package/dist/handler.d.ts +2 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +448 -37
- package/dist/handler.js.map +4 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +471 -50
- package/dist/index.js.map +4 -4
- package/dist/s3PlanV2Publisher.d.ts +26 -0
- package/dist/s3PlanV2Publisher.d.ts.map +1 -0
- package/dist/s3Transport.d.ts +11 -0
- package/dist/s3Transport.d.ts.map +1 -1
- package/dist/sdk/index.js +17 -6
- package/dist/sdk/index.js.map +3 -3
- package/dist/sdk/renderToLambda.d.ts +6 -1
- package/dist/sdk/renderToLambda.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// src/handler.ts
|
|
2
|
-
import { existsSync as existsSync3, mkdirSync as
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { basename, join } from "node:path";
|
|
2
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
|
|
3
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
4
|
+
import { basename, join as join2 } from "node:path";
|
|
5
5
|
import { S3Client } from "@aws-sdk/client-s3";
|
|
6
6
|
import {
|
|
7
7
|
assemble,
|
|
8
|
+
listPlanV2ArtifactsForTarget,
|
|
9
|
+
materializePlanV2Target,
|
|
8
10
|
plan,
|
|
11
|
+
planV2WithPublisher,
|
|
12
|
+
readPlanV2Manifest,
|
|
9
13
|
renderChunk
|
|
10
14
|
} from "@hyperframes/producer/distributed";
|
|
11
15
|
|
|
@@ -95,9 +99,14 @@ import {
|
|
|
95
99
|
rmSync,
|
|
96
100
|
statSync
|
|
97
101
|
} from "node:fs";
|
|
102
|
+
import { createHash } from "node:crypto";
|
|
98
103
|
import { dirname } from "node:path";
|
|
99
104
|
import { pipeline } from "node:stream/promises";
|
|
100
|
-
import {
|
|
105
|
+
import {
|
|
106
|
+
GetObjectCommand,
|
|
107
|
+
HeadObjectCommand,
|
|
108
|
+
PutObjectCommand
|
|
109
|
+
} from "@aws-sdk/client-s3";
|
|
101
110
|
import * as tar from "tar";
|
|
102
111
|
function parseS3Uri(uri) {
|
|
103
112
|
if (!uri.startsWith("s3://")) {
|
|
@@ -128,6 +137,19 @@ async function downloadS3ObjectToFile(client, uri, destPath) {
|
|
|
128
137
|
mkdirSync(dirname(destPath), { recursive: true });
|
|
129
138
|
await pipeline(body, createWriteStream(destPath));
|
|
130
139
|
}
|
|
140
|
+
async function downloadS3ObjectToFileVerified(client, uri, destPath, expectedSha256) {
|
|
141
|
+
assertSha256(expectedSha256);
|
|
142
|
+
await downloadS3ObjectToFile(client, uri, destPath);
|
|
143
|
+
const actual = await sha256File(destPath);
|
|
144
|
+
if (actual !== expectedSha256) {
|
|
145
|
+
rmSync(destPath, { force: true });
|
|
146
|
+
const error = new Error(
|
|
147
|
+
`[s3Transport] PLAN_ARTIFACT_DIGEST_MISMATCH: ${uri} expected ${expectedSha256}, got ${actual}`
|
|
148
|
+
);
|
|
149
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
131
153
|
async function uploadFileToS3(client, localPath, uri, contentType) {
|
|
132
154
|
if (!existsSync2(localPath)) {
|
|
133
155
|
throw new Error(`[s3Transport] upload source missing: ${localPath}`);
|
|
@@ -144,6 +166,97 @@ async function uploadFileToS3(client, localPath, uri, contentType) {
|
|
|
144
166
|
})
|
|
145
167
|
);
|
|
146
168
|
}
|
|
169
|
+
async function uploadContentAddressedFileToS3(client, localPath, uri, expectedSha256, contentType) {
|
|
170
|
+
assertSha256(expectedSha256);
|
|
171
|
+
if (!existsSync2(localPath)) {
|
|
172
|
+
throw new Error(`[s3Transport] upload source missing: ${localPath}`);
|
|
173
|
+
}
|
|
174
|
+
const actualSha256 = await sha256File(localPath);
|
|
175
|
+
if (actualSha256 !== expectedSha256) {
|
|
176
|
+
const error = new Error(
|
|
177
|
+
`[s3Transport] PLAN_ARTIFACT_DIGEST_MISMATCH: local artifact ${localPath} expected ${expectedSha256}, got ${actualSha256}`
|
|
178
|
+
);
|
|
179
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
const { bucket, key } = parseS3Uri(uri);
|
|
183
|
+
const size = statSync(localPath).size;
|
|
184
|
+
const existing = await inspectContentAddressedObject(client, bucket, key, size, expectedSha256);
|
|
185
|
+
if (existing === "matching") return "reused";
|
|
186
|
+
if (existing === "conflict") throwImmutableObjectConflict(uri);
|
|
187
|
+
const body = createReadStream(localPath);
|
|
188
|
+
try {
|
|
189
|
+
await client.send(
|
|
190
|
+
new PutObjectCommand({
|
|
191
|
+
Bucket: bucket,
|
|
192
|
+
Key: key,
|
|
193
|
+
Body: body,
|
|
194
|
+
ContentType: contentType,
|
|
195
|
+
ContentLength: size,
|
|
196
|
+
Metadata: { sha256: expectedSha256 },
|
|
197
|
+
ChecksumSHA256: Buffer.from(expectedSha256, "hex").toString("base64"),
|
|
198
|
+
// HEAD followed by an unconditional PUT can overwrite a conflicting
|
|
199
|
+
// object published by a concurrent planner. Conditional create makes
|
|
200
|
+
// immutable CAS and fixed-key manifest publication race-safe.
|
|
201
|
+
IfNoneMatch: "*"
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
return "uploaded";
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!isS3PreconditionFailed(error)) throw error;
|
|
207
|
+
const raced = await inspectContentAddressedObject(client, bucket, key, size, expectedSha256);
|
|
208
|
+
if (raced === "matching") return "reused";
|
|
209
|
+
if (raced === "conflict") throwImmutableObjectConflict(uri);
|
|
210
|
+
throw error;
|
|
211
|
+
} finally {
|
|
212
|
+
body.destroy();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function sha256File(path) {
|
|
216
|
+
const hash = createHash("sha256");
|
|
217
|
+
for await (const chunk of createReadStream(path)) {
|
|
218
|
+
hash.update(chunk);
|
|
219
|
+
}
|
|
220
|
+
return hash.digest("hex");
|
|
221
|
+
}
|
|
222
|
+
function assertSha256(value) {
|
|
223
|
+
if (!/^[a-f0-9]{64}$/.test(value)) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`[s3Transport] expected lowercase SHA-256 digest, got ${JSON.stringify(value)}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function inspectContentAddressedObject(client, bucket, key, expectedSize, expectedSha256) {
|
|
230
|
+
try {
|
|
231
|
+
const existing = await client.send(
|
|
232
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key, ChecksumMode: "ENABLED" })
|
|
233
|
+
);
|
|
234
|
+
return existing.ContentLength === expectedSize && existing.Metadata?.sha256 === expectedSha256 ? "matching" : "conflict";
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (isS3NotFound(error)) return "missing";
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function throwImmutableObjectConflict(uri) {
|
|
241
|
+
const error = new Error(
|
|
242
|
+
`[s3Transport] PLAN_ARTIFACT_DIGEST_MISMATCH: immutable object ${uri} already exists with different digest metadata or size`
|
|
243
|
+
);
|
|
244
|
+
error.name = "PLAN_ARTIFACT_DIGEST_MISMATCH";
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
function isS3NotFound(error) {
|
|
248
|
+
if (!isRecord(error)) return false;
|
|
249
|
+
const metadata = isRecord(error.$metadata) ? error.$metadata : void 0;
|
|
250
|
+
return error.name === "NotFound" || error.name === "NoSuchKey" || metadata?.httpStatusCode === 404;
|
|
251
|
+
}
|
|
252
|
+
function isS3PreconditionFailed(error) {
|
|
253
|
+
if (!isRecord(error)) return false;
|
|
254
|
+
const metadata = isRecord(error.$metadata) ? error.$metadata : void 0;
|
|
255
|
+
return error.name === "PreconditionFailed" || metadata?.httpStatusCode === 412;
|
|
256
|
+
}
|
|
257
|
+
function isRecord(value) {
|
|
258
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
259
|
+
}
|
|
147
260
|
async function tarDirectory(sourceDir, destTarball) {
|
|
148
261
|
if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
|
|
149
262
|
throw new Error(`[s3Transport] tar source must be an existing directory: ${sourceDir}`);
|
|
@@ -162,6 +275,112 @@ async function untarDirectory(tarballPath, destDir) {
|
|
|
162
275
|
await tar.extract({ file: tarballPath, cwd: destDir });
|
|
163
276
|
}
|
|
164
277
|
|
|
278
|
+
// src/s3PlanV2Publisher.ts
|
|
279
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
280
|
+
import { mkdirSync as mkdirSync2, mkdtempSync, rmSync as rmSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
281
|
+
import { tmpdir } from "node:os";
|
|
282
|
+
import { join } from "node:path";
|
|
283
|
+
import {
|
|
284
|
+
PlanV2IntegrityError
|
|
285
|
+
} from "@hyperframes/producer/distributed";
|
|
286
|
+
function isRecord2(value) {
|
|
287
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
288
|
+
}
|
|
289
|
+
function assertSha2562(value, label) {
|
|
290
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) {
|
|
291
|
+
throw new PlanV2IntegrityError(`${label} must be a lowercase SHA-256 digest`);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
function manifestDigests(manifestBytes) {
|
|
296
|
+
let value;
|
|
297
|
+
try {
|
|
298
|
+
value = JSON.parse(manifestBytes);
|
|
299
|
+
} catch {
|
|
300
|
+
throw new PlanV2IntegrityError("S3 publisher received invalid manifest JSON");
|
|
301
|
+
}
|
|
302
|
+
if (!isRecord2(value) || !Array.isArray(value.artifacts)) {
|
|
303
|
+
throw new PlanV2IntegrityError("S3 publisher manifest requires an artifacts array");
|
|
304
|
+
}
|
|
305
|
+
return new Set(
|
|
306
|
+
value.artifacts.map((artifact, index) => {
|
|
307
|
+
if (!isRecord2(artifact)) {
|
|
308
|
+
throw new PlanV2IntegrityError(`S3 publisher artifacts[${index}] must be an object`);
|
|
309
|
+
}
|
|
310
|
+
return assertSha2562(artifact.sha256, `S3 publisher artifacts[${index}].sha256`);
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
function trimTrailingSlash(value) {
|
|
315
|
+
let end = value.length;
|
|
316
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
317
|
+
return value.slice(0, end);
|
|
318
|
+
}
|
|
319
|
+
var S3PlanV2ArtifactPublisher = class {
|
|
320
|
+
artifactPrefix;
|
|
321
|
+
manifestUri;
|
|
322
|
+
#s3;
|
|
323
|
+
#temporaryRoot;
|
|
324
|
+
#publishedDigests = /* @__PURE__ */ new Set();
|
|
325
|
+
#state = "open";
|
|
326
|
+
constructor(options) {
|
|
327
|
+
const outputPrefix = `${trimTrailingSlash(options.planOutputS3Prefix)}/v2`;
|
|
328
|
+
parseS3Uri(outputPrefix);
|
|
329
|
+
this.#s3 = options.s3;
|
|
330
|
+
this.artifactPrefix = `${outputPrefix}/artifacts/sha256`;
|
|
331
|
+
this.manifestUri = `${outputPrefix}/manifest.json`;
|
|
332
|
+
this.#temporaryRoot = options.temporaryRoot ?? tmpdir();
|
|
333
|
+
mkdirSync2(this.#temporaryRoot, { recursive: true });
|
|
334
|
+
}
|
|
335
|
+
async putBlob(blob) {
|
|
336
|
+
this.#assertOpen("publish a blob");
|
|
337
|
+
const digest = assertSha2562(blob.sha256, "S3 published blob sha256");
|
|
338
|
+
const sourceSize = statSync2(blob.sourcePath).size;
|
|
339
|
+
if (sourceSize !== blob.sizeBytes) {
|
|
340
|
+
throw new PlanV2IntegrityError(
|
|
341
|
+
`S3 published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const uri = `${this.artifactPrefix}/${digest.slice(0, 2)}/${digest}`;
|
|
345
|
+
await uploadContentAddressedFileToS3(this.#s3, blob.sourcePath, uri, digest);
|
|
346
|
+
this.#publishedDigests.add(digest);
|
|
347
|
+
}
|
|
348
|
+
async commitManifest(manifestBytes) {
|
|
349
|
+
this.#assertOpen("commit a manifest");
|
|
350
|
+
for (const digest of manifestDigests(manifestBytes)) {
|
|
351
|
+
if (!this.#publishedDigests.has(digest)) {
|
|
352
|
+
throw new PlanV2IntegrityError(
|
|
353
|
+
`cannot commit S3 manifest before referenced blob is durable: ${digest}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const manifestDigest = createHash2("sha256").update(manifestBytes, "utf8").digest("hex");
|
|
358
|
+
const stagingDir = mkdtempSync(join(this.#temporaryRoot, "hf-plan-v2-manifest-"));
|
|
359
|
+
const manifestPath = join(stagingDir, "manifest.json");
|
|
360
|
+
try {
|
|
361
|
+
writeFileSync(manifestPath, manifestBytes, "utf8");
|
|
362
|
+
await uploadContentAddressedFileToS3(
|
|
363
|
+
this.#s3,
|
|
364
|
+
manifestPath,
|
|
365
|
+
this.manifestUri,
|
|
366
|
+
manifestDigest,
|
|
367
|
+
"application/json"
|
|
368
|
+
);
|
|
369
|
+
this.#state = "committed";
|
|
370
|
+
} finally {
|
|
371
|
+
rmSync2(stagingDir, { recursive: true, force: true });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async abort() {
|
|
375
|
+
if (this.#state === "open") this.#state = "aborted";
|
|
376
|
+
}
|
|
377
|
+
#assertOpen(operation) {
|
|
378
|
+
if (this.#state !== "open") {
|
|
379
|
+
throw new PlanV2IntegrityError(`cannot ${operation} after publisher is ${this.#state}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
165
384
|
// src/handler.ts
|
|
166
385
|
var cachedS3Client = null;
|
|
167
386
|
function getS3Client() {
|
|
@@ -192,15 +411,24 @@ async function handler(event, deps) {
|
|
|
192
411
|
}
|
|
193
412
|
}
|
|
194
413
|
} catch (err) {
|
|
414
|
+
normalizeTerminalErrorName(err);
|
|
195
415
|
logEvent({
|
|
196
416
|
event: "handler_error",
|
|
197
417
|
action: unwrapped.Action,
|
|
418
|
+
input: summarizeEvent(unwrapped),
|
|
198
419
|
message: err instanceof Error ? err.message : String(err),
|
|
199
420
|
name: err instanceof Error ? err.name : void 0
|
|
200
421
|
});
|
|
201
422
|
throw err;
|
|
202
423
|
}
|
|
203
424
|
}
|
|
425
|
+
function normalizeTerminalErrorName(error) {
|
|
426
|
+
if (!error || typeof error !== "object") return;
|
|
427
|
+
const candidate = error;
|
|
428
|
+
if (candidate.code === "PLAN_PROTOCOL_UNSUPPORTED" || candidate.code === "PLAN_TOO_LARGE" || candidate.code === "PLAN_V2_INTEGRITY_UNRECOVERABLE") {
|
|
429
|
+
candidate.name = candidate.code;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
204
432
|
var MAX_ENVELOPE_DEPTH = 4;
|
|
205
433
|
function unwrapEvent(event) {
|
|
206
434
|
let cursor = event;
|
|
@@ -237,18 +465,21 @@ function summarizeEvent(event) {
|
|
|
237
465
|
return {
|
|
238
466
|
projectS3Uri: event.ProjectS3Uri,
|
|
239
467
|
planOutputS3Prefix: event.PlanOutputS3Prefix,
|
|
468
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
240
469
|
format: event.Config.format,
|
|
241
470
|
fps: event.Config.fps
|
|
242
471
|
};
|
|
243
472
|
case "renderChunk":
|
|
244
473
|
return {
|
|
245
|
-
|
|
474
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
475
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri } : { planS3Uri: event.PlanS3Uri },
|
|
246
476
|
chunkIndex: event.ChunkIndex,
|
|
247
477
|
format: event.Format
|
|
248
478
|
};
|
|
249
479
|
case "assemble":
|
|
250
480
|
return {
|
|
251
|
-
|
|
481
|
+
planProtocol: event.PlanProtocol ?? "v1",
|
|
482
|
+
...event.PlanProtocol === "v2" ? { planV2ManifestS3Uri: event.PlanV2ManifestS3Uri } : { planS3Uri: event.PlanS3Uri },
|
|
252
483
|
chunkCount: event.ChunkS3Uris.length,
|
|
253
484
|
hasAudio: event.AudioS3Uri !== null,
|
|
254
485
|
outputS3Uri: event.OutputS3Uri,
|
|
@@ -261,12 +492,15 @@ function primeRuntimeEnv() {
|
|
|
261
492
|
if (runtimeEnvPrimed) return;
|
|
262
493
|
runtimeEnvPrimed = true;
|
|
263
494
|
const taskRoot = process.env.LAMBDA_TASK_ROOT ?? "/var/task";
|
|
264
|
-
const bin =
|
|
495
|
+
const bin = join2(taskRoot, "bin");
|
|
265
496
|
if (existsSync3(bin)) {
|
|
266
497
|
process.env.PATH = `${bin}:${process.env.PATH ?? ""}`;
|
|
267
498
|
}
|
|
268
499
|
}
|
|
269
500
|
async function handlePlan(event, deps) {
|
|
501
|
+
if (event.PlanProtocol === "v2") {
|
|
502
|
+
return handlePlanV2(event, deps);
|
|
503
|
+
}
|
|
270
504
|
const started = Date.now();
|
|
271
505
|
const s3 = deps?.s3 ?? getS3Client();
|
|
272
506
|
const primitive = deps?.primitives?.plan ?? plan;
|
|
@@ -274,10 +508,10 @@ async function handlePlan(event, deps) {
|
|
|
274
508
|
const chromePath = await resolveChromeExecutablePath();
|
|
275
509
|
process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
|
|
276
510
|
}
|
|
277
|
-
const work =
|
|
278
|
-
const projectArchive =
|
|
279
|
-
const projectDir =
|
|
280
|
-
const planDir =
|
|
511
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-plan-"));
|
|
512
|
+
const projectArchive = join2(work, "project.tar.gz");
|
|
513
|
+
const projectDir = join2(work, "project");
|
|
514
|
+
const planDir = join2(work, "plan");
|
|
281
515
|
try {
|
|
282
516
|
await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
|
|
283
517
|
await untarDirectory(projectArchive, projectDir);
|
|
@@ -285,12 +519,12 @@ async function handlePlan(event, deps) {
|
|
|
285
519
|
...event.Config
|
|
286
520
|
};
|
|
287
521
|
const result = await primitive(projectDir, config, planDir);
|
|
288
|
-
const planTar =
|
|
522
|
+
const planTar = join2(work, "plan.tar.gz");
|
|
289
523
|
await tarDirectory(planDir, planTar);
|
|
290
|
-
const planTarUri = `${
|
|
291
|
-
const audioPath =
|
|
292
|
-
const hasAudio = existsSync3(audioPath) &&
|
|
293
|
-
const audioUri = hasAudio ? `${
|
|
524
|
+
const planTarUri = `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/plan.tar.gz`;
|
|
525
|
+
const audioPath = join2(planDir, "audio.aac");
|
|
526
|
+
const hasAudio = existsSync3(audioPath) && statSync3(audioPath).size > 0;
|
|
527
|
+
const audioUri = hasAudio ? `${trimTrailingSlash2(event.PlanOutputS3Prefix)}/audio.aac` : null;
|
|
294
528
|
await Promise.all([
|
|
295
529
|
uploadFileToS3(s3, planTar, planTarUri, "application/gzip"),
|
|
296
530
|
hasAudio && audioUri ? uploadFileToS3(s3, audioPath, audioUri, "audio/aac") : null
|
|
@@ -315,7 +549,53 @@ async function handlePlan(event, deps) {
|
|
|
315
549
|
cleanupDir(work);
|
|
316
550
|
}
|
|
317
551
|
}
|
|
552
|
+
async function handlePlanV2(event, deps) {
|
|
553
|
+
const started = Date.now();
|
|
554
|
+
const s3 = deps?.s3 ?? getS3Client();
|
|
555
|
+
const primitive = deps?.primitives?.planV2WithPublisher ?? planV2WithPublisher;
|
|
556
|
+
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
557
|
+
process.env.PRODUCER_HEADLESS_SHELL_PATH = await resolveChromeExecutablePath();
|
|
558
|
+
}
|
|
559
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-plan-v2-"));
|
|
560
|
+
const projectArchive = join2(work, "project.tar.gz");
|
|
561
|
+
const projectDir = join2(work, "project");
|
|
562
|
+
try {
|
|
563
|
+
await downloadS3ObjectToFile(s3, event.ProjectS3Uri, projectArchive);
|
|
564
|
+
await untarDirectory(projectArchive, projectDir);
|
|
565
|
+
const publisher = new S3PlanV2ArtifactPublisher({
|
|
566
|
+
s3,
|
|
567
|
+
planOutputS3Prefix: event.PlanOutputS3Prefix,
|
|
568
|
+
temporaryRoot: work
|
|
569
|
+
});
|
|
570
|
+
const manifest = await primitive(projectDir, { ...event.Config }, publisher, {
|
|
571
|
+
stagingParentDir: work
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
Action: "plan",
|
|
575
|
+
PlanProtocol: "v2",
|
|
576
|
+
PlanV2ManifestS3Uri: publisher.manifestUri,
|
|
577
|
+
PlanV2ArtifactS3Prefix: publisher.artifactPrefix,
|
|
578
|
+
PlanHash: manifest.planHash,
|
|
579
|
+
ChunkCount: manifest.chunkCount,
|
|
580
|
+
TotalFrames: manifest.totalFrames,
|
|
581
|
+
Fps: manifest.fps,
|
|
582
|
+
Width: manifest.width,
|
|
583
|
+
Height: manifest.height,
|
|
584
|
+
Format: manifest.format,
|
|
585
|
+
HasAudio: manifest.artifacts.some((artifact) => artifact.path === "audio.aac"),
|
|
586
|
+
AudioS3Uri: null,
|
|
587
|
+
FfmpegVersion: manifest.ffmpegVersion,
|
|
588
|
+
ProducerVersion: manifest.producerVersion,
|
|
589
|
+
DurationMs: Date.now() - started
|
|
590
|
+
};
|
|
591
|
+
} finally {
|
|
592
|
+
cleanupDir(work);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
318
595
|
async function handleRenderChunk(event, deps) {
|
|
596
|
+
if (event.PlanProtocol === "v2") {
|
|
597
|
+
return handleRenderChunkV2(event, deps);
|
|
598
|
+
}
|
|
319
599
|
const started = Date.now();
|
|
320
600
|
const s3 = deps?.s3 ?? getS3Client();
|
|
321
601
|
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
@@ -323,14 +603,52 @@ async function handleRenderChunk(event, deps) {
|
|
|
323
603
|
const chromePath = await resolveChromeExecutablePath();
|
|
324
604
|
process.env.PRODUCER_HEADLESS_SHELL_PATH = chromePath;
|
|
325
605
|
}
|
|
326
|
-
const work =
|
|
327
|
-
const planTar =
|
|
328
|
-
const planDir =
|
|
606
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-"));
|
|
607
|
+
const planTar = join2(work, "plan.tar.gz");
|
|
608
|
+
const planDir = join2(work, "plan");
|
|
329
609
|
try {
|
|
330
610
|
await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
|
|
331
611
|
await untarDirectory(planTar, planDir);
|
|
332
612
|
verifyPlanHash(planDir, event.PlanHash);
|
|
333
|
-
const chunkOutputBase =
|
|
613
|
+
const chunkOutputBase = join2(
|
|
614
|
+
work,
|
|
615
|
+
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
616
|
+
);
|
|
617
|
+
const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
|
|
618
|
+
const chunkUri = await uploadChunkOutput(
|
|
619
|
+
s3,
|
|
620
|
+
result,
|
|
621
|
+
event.ChunkOutputS3Prefix,
|
|
622
|
+
event.ChunkIndex
|
|
623
|
+
);
|
|
624
|
+
return {
|
|
625
|
+
Action: "renderChunk",
|
|
626
|
+
ChunkS3Uri: chunkUri,
|
|
627
|
+
ChunkIndex: event.ChunkIndex,
|
|
628
|
+
Sha256: result.sha256,
|
|
629
|
+
FramesEncoded: result.framesEncoded,
|
|
630
|
+
DurationMs: Date.now() - started
|
|
631
|
+
};
|
|
632
|
+
} finally {
|
|
633
|
+
cleanupDir(work);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async function handleRenderChunkV2(event, deps) {
|
|
637
|
+
const started = Date.now();
|
|
638
|
+
const s3 = deps?.s3 ?? getS3Client();
|
|
639
|
+
const primitive = deps?.primitives?.renderChunk ?? renderChunk;
|
|
640
|
+
if (!deps?.skipChromeResolution && !process.env.PRODUCER_HEADLESS_SHELL_PATH) {
|
|
641
|
+
process.env.PRODUCER_HEADLESS_SHELL_PATH = await resolveChromeExecutablePath();
|
|
642
|
+
}
|
|
643
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-chunk-v2-"));
|
|
644
|
+
try {
|
|
645
|
+
const planDir = await downloadAndMaterializePlanV2(
|
|
646
|
+
s3,
|
|
647
|
+
event,
|
|
648
|
+
{ role: "chunk", chunkIndex: event.ChunkIndex },
|
|
649
|
+
work
|
|
650
|
+
);
|
|
651
|
+
const chunkOutputBase = join2(
|
|
334
652
|
work,
|
|
335
653
|
event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
|
|
336
654
|
);
|
|
@@ -354,7 +672,7 @@ async function handleRenderChunk(event, deps) {
|
|
|
354
672
|
}
|
|
355
673
|
}
|
|
356
674
|
async function uploadChunkOutput(s3, result, prefix, chunkIndex) {
|
|
357
|
-
const trimmed =
|
|
675
|
+
const trimmed = trimTrailingSlash2(prefix);
|
|
358
676
|
if (result.outputKind === "file") {
|
|
359
677
|
const ext = result.outputPath.slice(result.outputPath.lastIndexOf("."));
|
|
360
678
|
const uri2 = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;
|
|
@@ -368,22 +686,25 @@ async function uploadChunkOutput(s3, result, prefix, chunkIndex) {
|
|
|
368
686
|
return uri;
|
|
369
687
|
}
|
|
370
688
|
async function handleAssemble(event, deps) {
|
|
689
|
+
if (event.PlanProtocol === "v2") {
|
|
690
|
+
return handleAssembleV2(event, deps);
|
|
691
|
+
}
|
|
371
692
|
const started = Date.now();
|
|
372
693
|
const s3 = deps?.s3 ?? getS3Client();
|
|
373
694
|
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
374
|
-
const work =
|
|
375
|
-
const planTar =
|
|
376
|
-
const planDir =
|
|
695
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-"));
|
|
696
|
+
const planTar = join2(work, "plan.tar.gz");
|
|
697
|
+
const planDir = join2(work, "plan");
|
|
377
698
|
try {
|
|
378
699
|
await downloadS3ObjectToFile(s3, event.PlanS3Uri, planTar);
|
|
379
700
|
await untarDirectory(planTar, planDir);
|
|
380
701
|
const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
|
|
381
702
|
let audioPath = null;
|
|
382
703
|
if (event.AudioS3Uri) {
|
|
383
|
-
audioPath =
|
|
704
|
+
audioPath = join2(planDir, "audio.aac");
|
|
384
705
|
await downloadS3ObjectToFile(s3, event.AudioS3Uri, audioPath);
|
|
385
706
|
}
|
|
386
|
-
const finalOutput = event.Format === "png-sequence" ?
|
|
707
|
+
const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
|
|
387
708
|
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
388
709
|
cfr: event.Cfr === true
|
|
389
710
|
});
|
|
@@ -405,9 +726,96 @@ async function handleAssemble(event, deps) {
|
|
|
405
726
|
cleanupDir(work);
|
|
406
727
|
}
|
|
407
728
|
}
|
|
729
|
+
async function handleAssembleV2(event, deps) {
|
|
730
|
+
const started = Date.now();
|
|
731
|
+
const s3 = deps?.s3 ?? getS3Client();
|
|
732
|
+
const primitive = deps?.primitives?.assemble ?? assemble;
|
|
733
|
+
const work = mkdtempSync2(join2(deps?.tmpRoot ?? tmpdir2(), "hf-lambda-assemble-v2-"));
|
|
734
|
+
try {
|
|
735
|
+
const planDir = await downloadAndMaterializePlanV2(s3, event, { role: "assembler" }, work);
|
|
736
|
+
const audioPath = existsSync3(join2(planDir, "audio.aac")) ? join2(planDir, "audio.aac") : null;
|
|
737
|
+
const chunkPaths = await downloadChunkObjects(s3, event.ChunkS3Uris, work, event.Format);
|
|
738
|
+
const finalOutput = event.Format === "png-sequence" ? join2(work, "output-frames") : join2(work, `output${formatExtension(event.Format)}`);
|
|
739
|
+
const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
|
|
740
|
+
cfr: event.Cfr === true
|
|
741
|
+
});
|
|
742
|
+
if (event.Format === "png-sequence") {
|
|
743
|
+
const tarball = `${finalOutput}.tar.gz`;
|
|
744
|
+
await tarDirectory(finalOutput, tarball);
|
|
745
|
+
await uploadFileToS3(s3, tarball, event.OutputS3Uri, "application/gzip");
|
|
746
|
+
} else {
|
|
747
|
+
await uploadFileToS3(s3, finalOutput, event.OutputS3Uri);
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
Action: "assemble",
|
|
751
|
+
OutputS3Uri: event.OutputS3Uri,
|
|
752
|
+
FramesEncoded: result.framesEncoded,
|
|
753
|
+
FileSize: result.fileSize,
|
|
754
|
+
DurationMs: Date.now() - started
|
|
755
|
+
};
|
|
756
|
+
} finally {
|
|
757
|
+
cleanupDir(work);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function downloadAndMaterializePlanV2(s3, event, target, work) {
|
|
761
|
+
const transportDir = join2(work, "plan-v2");
|
|
762
|
+
mkdirSync3(transportDir, { recursive: true });
|
|
763
|
+
await downloadS3ObjectToFile(s3, event.PlanV2ManifestS3Uri, join2(transportDir, "plan.json"));
|
|
764
|
+
const manifest = readPlanV2Manifest(transportDir);
|
|
765
|
+
if (manifest.planHash !== event.PlanHash) {
|
|
766
|
+
throwPlanHashMismatch(event.PlanHash, manifest.planHash);
|
|
767
|
+
}
|
|
768
|
+
const artifacts = listPlanV2ArtifactsForTarget(manifest, target);
|
|
769
|
+
const uniqueArtifacts = [
|
|
770
|
+
...new Map(artifacts.map((artifact) => [artifact.sha256, artifact])).values()
|
|
771
|
+
];
|
|
772
|
+
await mapConcurrent(uniqueArtifacts, 16, async (artifact) => {
|
|
773
|
+
await downloadPlanV2Artifact(s3, event.PlanV2ArtifactS3Prefix, transportDir, artifact);
|
|
774
|
+
});
|
|
775
|
+
const planDir = join2(work, "plan");
|
|
776
|
+
materializePlanV2Target(transportDir, target, planDir);
|
|
777
|
+
return planDir;
|
|
778
|
+
}
|
|
779
|
+
async function downloadPlanV2Artifact(s3, artifactPrefix, planV2Dir, artifact) {
|
|
780
|
+
await downloadS3ObjectToFileVerified(
|
|
781
|
+
s3,
|
|
782
|
+
planV2BlobUri(artifactPrefix, artifact.sha256),
|
|
783
|
+
planV2BlobPath(planV2Dir, artifact.sha256),
|
|
784
|
+
artifact.sha256
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
function planV2BlobPath(planV2Dir, digest) {
|
|
788
|
+
return join2(planV2Dir, "artifacts", "sha256", digest.slice(0, 2), digest);
|
|
789
|
+
}
|
|
790
|
+
function planV2BlobUri(prefix, digest) {
|
|
791
|
+
return `${trimTrailingSlash2(prefix)}/${digest.slice(0, 2)}/${digest}`;
|
|
792
|
+
}
|
|
793
|
+
function throwPlanHashMismatch(expected, actual) {
|
|
794
|
+
const error = new Error(
|
|
795
|
+
`PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match v2 manifest planHash=${actual}`
|
|
796
|
+
);
|
|
797
|
+
error.name = "PLAN_HASH_MISMATCH";
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
async function mapConcurrent(values, concurrency, fn) {
|
|
801
|
+
let cursor = 0;
|
|
802
|
+
async function worker() {
|
|
803
|
+
while (cursor < values.length) {
|
|
804
|
+
const index = cursor++;
|
|
805
|
+
await fn(values[index]);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const results = await Promise.allSettled(
|
|
809
|
+
Array.from({ length: Math.min(concurrency, values.length) }, () => worker())
|
|
810
|
+
);
|
|
811
|
+
const failure = results.find(
|
|
812
|
+
(result) => result.status === "rejected"
|
|
813
|
+
);
|
|
814
|
+
if (failure) throw failure.reason;
|
|
815
|
+
}
|
|
408
816
|
async function downloadChunkObjects(s3, uris, workDir, format) {
|
|
409
|
-
const chunksDir =
|
|
410
|
-
|
|
817
|
+
const chunksDir = join2(workDir, "chunks");
|
|
818
|
+
mkdirSync3(chunksDir, { recursive: true });
|
|
411
819
|
const local = new Array(uris.length);
|
|
412
820
|
await Promise.all(
|
|
413
821
|
uris.map(async (uri, i) => {
|
|
@@ -415,10 +823,10 @@ async function downloadChunkObjects(s3, uris, workDir, format) {
|
|
|
415
823
|
throw new Error(`[handler] chunk URI at index ${i} is empty`);
|
|
416
824
|
}
|
|
417
825
|
const { key } = parseS3Uri(uri);
|
|
418
|
-
const localPath =
|
|
826
|
+
const localPath = join2(chunksDir, basename(key));
|
|
419
827
|
await downloadS3ObjectToFile(s3, uri, localPath);
|
|
420
828
|
if (format === "png-sequence") {
|
|
421
|
-
const dirPath =
|
|
829
|
+
const dirPath = join2(chunksDir, `frames-${pad(i)}`);
|
|
422
830
|
await untarDirectory(localPath, dirPath);
|
|
423
831
|
local[i] = dirPath;
|
|
424
832
|
} else {
|
|
@@ -433,11 +841,14 @@ function getEventS3Uris(event) {
|
|
|
433
841
|
case "plan":
|
|
434
842
|
return [event.ProjectS3Uri, event.PlanOutputS3Prefix];
|
|
435
843
|
case "renderChunk":
|
|
436
|
-
return [event.PlanS3Uri, event.ChunkOutputS3Prefix];
|
|
844
|
+
return event.PlanProtocol === "v2" ? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix, event.ChunkOutputS3Prefix] : [event.PlanS3Uri, event.ChunkOutputS3Prefix];
|
|
437
845
|
case "assemble":
|
|
438
|
-
return [
|
|
439
|
-
|
|
440
|
-
|
|
846
|
+
return [
|
|
847
|
+
...event.PlanProtocol === "v2" ? [event.PlanV2ManifestS3Uri, event.PlanV2ArtifactS3Prefix] : [event.PlanS3Uri],
|
|
848
|
+
...event.ChunkS3Uris,
|
|
849
|
+
event.OutputS3Uri,
|
|
850
|
+
event.AudioS3Uri
|
|
851
|
+
].filter((u) => u != null);
|
|
441
852
|
}
|
|
442
853
|
}
|
|
443
854
|
function validateEventS3Uris(event) {
|
|
@@ -457,17 +868,17 @@ function validateEventS3Uris(event) {
|
|
|
457
868
|
function pad(n) {
|
|
458
869
|
return n.toString().padStart(4, "0");
|
|
459
870
|
}
|
|
460
|
-
function
|
|
871
|
+
function trimTrailingSlash2(prefix) {
|
|
461
872
|
return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
462
873
|
}
|
|
463
874
|
function cleanupDir(dir) {
|
|
464
875
|
try {
|
|
465
|
-
|
|
876
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
466
877
|
} catch {
|
|
467
878
|
}
|
|
468
879
|
}
|
|
469
880
|
function verifyPlanHash(planDir, expected) {
|
|
470
|
-
const planJsonPath =
|
|
881
|
+
const planJsonPath = join2(planDir, "plan.json");
|
|
471
882
|
let parsed;
|
|
472
883
|
try {
|
|
473
884
|
parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
|
|
@@ -488,13 +899,13 @@ function verifyPlanHash(planDir, expected) {
|
|
|
488
899
|
}
|
|
489
900
|
|
|
490
901
|
// src/sdk/deploySite.ts
|
|
491
|
-
import { mkdtempSync as
|
|
492
|
-
import { tmpdir as
|
|
493
|
-
import { join as
|
|
494
|
-
import { HeadObjectCommand, S3Client as S3Client2 } from "@aws-sdk/client-s3";
|
|
902
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
|
|
903
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
904
|
+
import { join as join3 } from "node:path";
|
|
905
|
+
import { HeadObjectCommand as HeadObjectCommand2, S3Client as S3Client2 } from "@aws-sdk/client-s3";
|
|
495
906
|
import { hashProjectDir } from "@hyperframes/producer/distributed";
|
|
496
907
|
async function deploySite(opts) {
|
|
497
|
-
if (!
|
|
908
|
+
if (!statSync4(opts.projectDir).isDirectory()) {
|
|
498
909
|
throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
|
|
499
910
|
}
|
|
500
911
|
const siteId = opts.siteId ?? hashProjectDir(opts.projectDir);
|
|
@@ -512,11 +923,11 @@ async function deploySite(opts) {
|
|
|
512
923
|
uploaded: false
|
|
513
924
|
};
|
|
514
925
|
}
|
|
515
|
-
const workdir =
|
|
926
|
+
const workdir = mkdtempSync3(join3(tmpdir3(), "hf-deploy-site-"));
|
|
516
927
|
try {
|
|
517
|
-
const tarball =
|
|
928
|
+
const tarball = join3(workdir, "project.tar.gz");
|
|
518
929
|
await tarDirectory(opts.projectDir, tarball);
|
|
519
|
-
const size =
|
|
930
|
+
const size = statSync4(tarball).size;
|
|
520
931
|
await uploadFileToS3(s3, tarball, projectS3Uri, "application/gzip");
|
|
521
932
|
return {
|
|
522
933
|
siteId,
|
|
@@ -527,12 +938,12 @@ async function deploySite(opts) {
|
|
|
527
938
|
uploaded: true
|
|
528
939
|
};
|
|
529
940
|
} finally {
|
|
530
|
-
|
|
941
|
+
rmSync4(workdir, { recursive: true, force: true });
|
|
531
942
|
}
|
|
532
943
|
}
|
|
533
944
|
async function headObject(s3, bucket, key) {
|
|
534
945
|
try {
|
|
535
|
-
const res = await s3.send(new
|
|
946
|
+
const res = await s3.send(new HeadObjectCommand2({ Bucket: bucket, Key: key }));
|
|
536
947
|
return {
|
|
537
948
|
bytes: typeof res.ContentLength === "number" ? res.ContentLength : 0,
|
|
538
949
|
lastModified: res.LastModified instanceof Date ? res.LastModified.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -614,7 +1025,8 @@ async function renderToLambda(opts) {
|
|
|
614
1025
|
ProjectS3Uri: site.projectS3Uri,
|
|
615
1026
|
PlanOutputS3Prefix: planOutputS3Prefix,
|
|
616
1027
|
OutputS3Uri: outputS3Uri,
|
|
617
|
-
Config: opts.config
|
|
1028
|
+
Config: opts.config,
|
|
1029
|
+
PlanProtocol: opts.planProtocol ?? "v1"
|
|
618
1030
|
};
|
|
619
1031
|
validateStepFunctionsInputSize(input);
|
|
620
1032
|
const sfn = opts.sfn ?? new SFNClient({ region: opts.region });
|
|
@@ -804,7 +1216,7 @@ function summarizeHistory(events, memoryMb) {
|
|
|
804
1216
|
}
|
|
805
1217
|
case "TaskStateExited":
|
|
806
1218
|
case "MapStateExited":
|
|
807
|
-
if (ev.stateExitedEventDetails?.name
|
|
1219
|
+
if (isAssembleState(ev.stateExitedEventDetails?.name)) {
|
|
808
1220
|
assembleComplete = true;
|
|
809
1221
|
const exitPayload = parseJson(ev.stateExitedEventDetails?.output);
|
|
810
1222
|
if (exitPayload && typeof exitPayload === "object") {
|
|
@@ -883,11 +1295,17 @@ function unwrapLambdaPayload(payload) {
|
|
|
883
1295
|
return payload;
|
|
884
1296
|
}
|
|
885
1297
|
function applyPayloadFrameCounts(payload, currentLambdaState, bump) {
|
|
886
|
-
if (currentLambdaState
|
|
1298
|
+
if (!isRenderChunkState(currentLambdaState)) return;
|
|
887
1299
|
if (!payload || typeof payload !== "object") return;
|
|
888
1300
|
const obj = payload;
|
|
889
1301
|
if (typeof obj.FramesEncoded === "number") bump(obj.FramesEncoded);
|
|
890
1302
|
}
|
|
1303
|
+
function isRenderChunkState(name) {
|
|
1304
|
+
return name === "RenderChunk" || name === "RenderChunkV2";
|
|
1305
|
+
}
|
|
1306
|
+
function isAssembleState(name) {
|
|
1307
|
+
return name === "Assemble" || name === "AssembleV2";
|
|
1308
|
+
}
|
|
891
1309
|
function inferBilledMs(payload) {
|
|
892
1310
|
if (!payload || typeof payload !== "object") return 0;
|
|
893
1311
|
const obj = payload;
|
|
@@ -912,9 +1330,11 @@ function isTerminalFailure(status) {
|
|
|
912
1330
|
export {
|
|
913
1331
|
ChromeBinaryUnavailableError,
|
|
914
1332
|
InvalidConfigError2 as InvalidConfigError,
|
|
1333
|
+
S3PlanV2ArtifactPublisher,
|
|
915
1334
|
computeRenderCost,
|
|
916
1335
|
deploySite,
|
|
917
1336
|
downloadS3ObjectToFile,
|
|
1337
|
+
downloadS3ObjectToFileVerified,
|
|
918
1338
|
formatS3Uri,
|
|
919
1339
|
getRenderProgress,
|
|
920
1340
|
handler,
|
|
@@ -926,6 +1346,7 @@ export {
|
|
|
926
1346
|
tarDirectory,
|
|
927
1347
|
untarDirectory,
|
|
928
1348
|
unwrapEvent,
|
|
1349
|
+
uploadContentAddressedFileToS3,
|
|
929
1350
|
uploadFileToS3,
|
|
930
1351
|
validateDistributedRenderConfig
|
|
931
1352
|
};
|