@hyperframes/gcp-cloud-run 0.6.79

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/dist/index.js ADDED
@@ -0,0 +1,855 @@
1
+ // src/server.ts
2
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, extname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { serve } from "@hono/node-server";
7
+ import { Storage } from "@google-cloud/storage";
8
+ import { Hono } from "hono";
9
+ import {
10
+ assemble,
11
+ plan,
12
+ renderChunk
13
+ } from "@hyperframes/producer/distributed";
14
+
15
+ // src/chromium.ts
16
+ import { existsSync } from "node:fs";
17
+ var ChromeBinaryUnavailableError = class extends Error {
18
+ // Read indirectly via the error envelope / Error.prototype.toString.
19
+ // fallow-ignore-next-line unused-class-member
20
+ name = "ChromeBinaryUnavailableError";
21
+ resolvedPath;
22
+ constructor(resolvedPath, hint) {
23
+ super(`[chromium] Chrome binary unavailable: ${hint}`);
24
+ this.resolvedPath = resolvedPath;
25
+ }
26
+ };
27
+ var FALLBACK_CHROME_PATHS = [
28
+ "/opt/chrome/chrome-headless-shell",
29
+ "/usr/bin/chrome-headless-shell",
30
+ "/usr/bin/google-chrome",
31
+ "/usr/bin/google-chrome-stable",
32
+ "/usr/bin/chromium",
33
+ "/usr/bin/chromium-browser"
34
+ ];
35
+ function resolveChromeExecutablePath() {
36
+ const fromEngineOverride = process.env.PRODUCER_HEADLESS_SHELL_PATH?.trim();
37
+ if (fromEngineOverride) {
38
+ if (!existsSync(fromEngineOverride)) {
39
+ throw new ChromeBinaryUnavailableError(
40
+ fromEngineOverride,
41
+ `PRODUCER_HEADLESS_SHELL_PATH=${JSON.stringify(fromEngineOverride)} does not exist on disk.`
42
+ );
43
+ }
44
+ return fromEngineOverride;
45
+ }
46
+ const fromImage = process.env.HYPERFRAMES_CHROME_PATH?.trim();
47
+ if (fromImage) {
48
+ if (!existsSync(fromImage)) {
49
+ throw new ChromeBinaryUnavailableError(
50
+ fromImage,
51
+ `HYPERFRAMES_CHROME_PATH=${JSON.stringify(fromImage)} does not exist on disk.`
52
+ );
53
+ }
54
+ return fromImage;
55
+ }
56
+ for (const candidate of FALLBACK_CHROME_PATHS) {
57
+ if (existsSync(candidate)) return candidate;
58
+ }
59
+ throw new ChromeBinaryUnavailableError(
60
+ null,
61
+ `no Chrome binary found. Set HYPERFRAMES_CHROME_PATH (the Dockerfile does this) or PRODUCER_HEADLESS_SHELL_PATH to the absolute path of a chrome-headless-shell binary. Searched: ${FALLBACK_CHROME_PATHS.join(", ")}.`
62
+ );
63
+ }
64
+
65
+ // src/formatExtension.ts
66
+ var FORMAT_EXTENSIONS = {
67
+ mp4: ".mp4",
68
+ mov: ".mov",
69
+ webm: ".webm",
70
+ "png-sequence": ""
71
+ };
72
+ function formatExtension(format) {
73
+ return FORMAT_EXTENSIONS[format];
74
+ }
75
+
76
+ // src/gcsTransport.ts
77
+ import { createWriteStream, existsSync as existsSync2, mkdirSync, rmSync, statSync } from "node:fs";
78
+ import { dirname } from "node:path";
79
+ import { pipeline } from "node:stream/promises";
80
+ import * as tar from "tar";
81
+ function parseGcsUri(uri) {
82
+ if (!uri.startsWith("gs://")) {
83
+ throw new Error(`[gcsTransport] expected gs:// URI, got: ${JSON.stringify(uri)}`);
84
+ }
85
+ const rest = uri.slice("gs://".length);
86
+ const slash = rest.indexOf("/");
87
+ if (slash === -1) {
88
+ throw new Error(`[gcsTransport] missing key in gs URI: ${JSON.stringify(uri)}`);
89
+ }
90
+ const bucket = rest.slice(0, slash);
91
+ const key = rest.slice(slash + 1);
92
+ if (!bucket || !key) {
93
+ throw new Error(`[gcsTransport] empty bucket or key in gs URI: ${JSON.stringify(uri)}`);
94
+ }
95
+ return { bucket, key };
96
+ }
97
+ function formatGcsUri(loc) {
98
+ return `gs://${loc.bucket}/${loc.key}`;
99
+ }
100
+ async function downloadGcsObjectToFile(storage, uri, destPath) {
101
+ const { bucket, key } = parseGcsUri(uri);
102
+ mkdirSync(dirname(destPath), { recursive: true });
103
+ const file = storage.bucket(bucket).file(key);
104
+ await pipeline(file.createReadStream(), createWriteStream(destPath));
105
+ }
106
+ async function uploadFileToGcs(storage, localPath, uri, contentType) {
107
+ if (!existsSync2(localPath)) {
108
+ throw new Error(`[gcsTransport] upload source missing: ${localPath}`);
109
+ }
110
+ const { bucket, key } = parseGcsUri(uri);
111
+ await storage.bucket(bucket).upload(localPath, {
112
+ destination: key,
113
+ // `resumable: false` (simple upload) is faster for the small-to-medium
114
+ // objects this adapter moves and avoids the extra round-trip a resumable
115
+ // session start costs; GCS recommends resumable only past ~8 MB but our
116
+ // chunks are reliably above that, so let the client pick by default.
117
+ contentType
118
+ });
119
+ }
120
+ async function tarDirectory(sourceDir, destTarball) {
121
+ if (!existsSync2(sourceDir) || !statSync(sourceDir).isDirectory()) {
122
+ throw new Error(`[gcsTransport] tar source must be an existing directory: ${sourceDir}`);
123
+ }
124
+ mkdirSync(dirname(destTarball), { recursive: true });
125
+ await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, ["."]);
126
+ }
127
+ async function untarDirectory(tarballPath, destDir) {
128
+ if (!existsSync2(tarballPath)) {
129
+ throw new Error(`[gcsTransport] tarball missing: ${tarballPath}`);
130
+ }
131
+ if (existsSync2(destDir)) {
132
+ rmSync(destDir, { recursive: true, force: true });
133
+ }
134
+ mkdirSync(destDir, { recursive: true });
135
+ await tar.extract({ file: tarballPath, cwd: destDir });
136
+ }
137
+
138
+ // src/server.ts
139
+ var cachedStorage = null;
140
+ function getStorage() {
141
+ if (cachedStorage) return cachedStorage;
142
+ cachedStorage = new Storage();
143
+ return cachedStorage;
144
+ }
145
+ async function dispatch(event, deps) {
146
+ const unwrapped = unwrapEvent(event);
147
+ validateEventGcsUris(unwrapped);
148
+ logEvent({ event: "handler_start", action: unwrapped.Action, input: summarizeEvent(unwrapped) });
149
+ try {
150
+ switch (unwrapped.Action) {
151
+ case "plan":
152
+ return await handlePlan(unwrapped, deps);
153
+ case "renderChunk":
154
+ return await handleRenderChunk(unwrapped, deps);
155
+ case "assemble":
156
+ return await handleAssemble(unwrapped, deps);
157
+ default: {
158
+ const _exhaustive = unwrapped;
159
+ throw new Error(
160
+ `[handler] unknown Action: ${JSON.stringify(
161
+ _exhaustive.Action
162
+ )}. Expected one of "plan", "renderChunk", "assemble".`
163
+ );
164
+ }
165
+ }
166
+ } catch (err) {
167
+ logEvent({
168
+ event: "handler_error",
169
+ action: unwrapped.Action,
170
+ message: err instanceof Error ? err.message : String(err),
171
+ name: err instanceof Error ? err.name : void 0
172
+ });
173
+ throw err;
174
+ }
175
+ }
176
+ var MAX_ENVELOPE_DEPTH = 4;
177
+ function unwrapEvent(event) {
178
+ let cursor = event;
179
+ for (let i = 0; i < MAX_ENVELOPE_DEPTH; i++) {
180
+ if (cursor && typeof cursor === "object") {
181
+ const obj = cursor;
182
+ if (typeof obj.Action === "string" && isCloudRunAction(obj.Action)) {
183
+ return cursor;
184
+ }
185
+ if ("Payload" in obj) {
186
+ cursor = obj.Payload;
187
+ continue;
188
+ }
189
+ if ("Input" in obj) {
190
+ cursor = obj.Input;
191
+ continue;
192
+ }
193
+ }
194
+ break;
195
+ }
196
+ throw new Error(
197
+ `[handler] body has no recognised Action; unwrapped ${MAX_ENVELOPE_DEPTH} levels of Payload/Input without finding one.`
198
+ );
199
+ }
200
+ function isCloudRunAction(value) {
201
+ return value === "plan" || value === "renderChunk" || value === "assemble";
202
+ }
203
+ function logEvent(payload) {
204
+ console.log(JSON.stringify(payload));
205
+ }
206
+ function summarizeEvent(event) {
207
+ switch (event.Action) {
208
+ case "plan":
209
+ return {
210
+ projectGcsUri: event.ProjectGcsUri,
211
+ planOutputGcsPrefix: event.PlanOutputGcsPrefix,
212
+ format: event.Config.format,
213
+ fps: event.Config.fps
214
+ };
215
+ case "renderChunk":
216
+ return {
217
+ planGcsUri: event.PlanGcsUri,
218
+ chunkIndex: event.ChunkIndex,
219
+ format: event.Format
220
+ };
221
+ case "assemble":
222
+ return {
223
+ planGcsUri: event.PlanGcsUri,
224
+ chunkCount: event.ChunkGcsUris.length,
225
+ hasAudio: event.AudioGcsUri !== null,
226
+ outputGcsUri: event.OutputGcsUri,
227
+ format: event.Format
228
+ };
229
+ }
230
+ }
231
+ function primeChrome(deps) {
232
+ if (deps?.skipChromeResolution) return;
233
+ if (process.env.PRODUCER_HEADLESS_SHELL_PATH) return;
234
+ process.env.PRODUCER_HEADLESS_SHELL_PATH = resolveChromeExecutablePath();
235
+ }
236
+ async function handlePlan(event, deps) {
237
+ const started = Date.now();
238
+ const storage = deps?.storage ?? getStorage();
239
+ const primitive = deps?.primitives?.plan ?? plan;
240
+ primeChrome(deps);
241
+ const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-plan-"));
242
+ const projectArchive = join(work, "project.tar.gz");
243
+ const projectDir = join(work, "project");
244
+ const planDir = join(work, "plan");
245
+ try {
246
+ await downloadGcsObjectToFile(storage, event.ProjectGcsUri, projectArchive);
247
+ await untarDirectory(projectArchive, projectDir);
248
+ const config = {
249
+ ...event.Config
250
+ };
251
+ const result = await primitive(projectDir, config, planDir);
252
+ const planTar = join(work, "plan.tar.gz");
253
+ await tarDirectory(planDir, planTar);
254
+ const planTarUri = `${trimTrailingSlash(event.PlanOutputGcsPrefix)}/plan.tar.gz`;
255
+ const audioPath = join(planDir, "audio.aac");
256
+ const hasAudio = existsSync3(audioPath) && statSync2(audioPath).size > 0;
257
+ await uploadFileToGcs(storage, planTar, planTarUri, "application/gzip");
258
+ return {
259
+ Action: "plan",
260
+ PlanGcsUri: planTarUri,
261
+ PlanHash: result.planHash,
262
+ ChunkCount: result.chunkCount,
263
+ TotalFrames: result.totalFrames,
264
+ Fps: result.fps,
265
+ Width: result.width,
266
+ Height: result.height,
267
+ Format: result.format,
268
+ HasAudio: hasAudio,
269
+ AudioGcsUri: null,
270
+ FfmpegVersion: result.ffmpegVersion,
271
+ ProducerVersion: result.producerVersion,
272
+ DurationMs: Date.now() - started
273
+ };
274
+ } finally {
275
+ cleanupDir(work);
276
+ }
277
+ }
278
+ async function handleRenderChunk(event, deps) {
279
+ const started = Date.now();
280
+ const storage = deps?.storage ?? getStorage();
281
+ const primitive = deps?.primitives?.renderChunk ?? renderChunk;
282
+ primeChrome(deps);
283
+ const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-chunk-"));
284
+ const planTar = join(work, "plan.tar.gz");
285
+ const planDir = join(work, "plan");
286
+ try {
287
+ await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
288
+ await untarDirectory(planTar, planDir);
289
+ verifyPlanHash(planDir, event.PlanHash);
290
+ const chunkOutputBase = join(
291
+ work,
292
+ event.Format === "png-sequence" ? `chunk-${pad(event.ChunkIndex)}` : `chunk-${pad(event.ChunkIndex)}${formatExtension(event.Format)}`
293
+ );
294
+ const result = await primitive(planDir, event.ChunkIndex, chunkOutputBase);
295
+ const chunkUri = await uploadChunkOutput(
296
+ storage,
297
+ result,
298
+ event.ChunkOutputGcsPrefix,
299
+ event.ChunkIndex
300
+ );
301
+ return {
302
+ Action: "renderChunk",
303
+ ChunkGcsUri: chunkUri,
304
+ ChunkIndex: event.ChunkIndex,
305
+ Sha256: result.sha256,
306
+ FramesEncoded: result.framesEncoded,
307
+ DurationMs: Date.now() - started
308
+ };
309
+ } finally {
310
+ cleanupDir(work);
311
+ }
312
+ }
313
+ async function uploadChunkOutput(storage, result, prefix, chunkIndex) {
314
+ const trimmed = trimTrailingSlash(prefix);
315
+ if (result.outputKind === "file") {
316
+ const ext = extname(result.outputPath);
317
+ const uri2 = `${trimmed}/chunks/${pad(chunkIndex)}${ext}`;
318
+ await uploadFileToGcs(storage, result.outputPath, uri2);
319
+ return uri2;
320
+ }
321
+ const tarball = `${result.outputPath}.tar.gz`;
322
+ await tarDirectory(result.outputPath, tarball);
323
+ const uri = `${trimmed}/chunks/${pad(chunkIndex)}.tar.gz`;
324
+ await uploadFileToGcs(storage, tarball, uri, "application/gzip");
325
+ return uri;
326
+ }
327
+ async function handleAssemble(event, deps) {
328
+ const started = Date.now();
329
+ const storage = deps?.storage ?? getStorage();
330
+ const primitive = deps?.primitives?.assemble ?? assemble;
331
+ const work = mkdtempSync(join(deps?.tmpRoot ?? tmpdir(), "hf-cr-assemble-"));
332
+ const planTar = join(work, "plan.tar.gz");
333
+ const planDir = join(work, "plan");
334
+ try {
335
+ await downloadGcsObjectToFile(storage, event.PlanGcsUri, planTar);
336
+ await untarDirectory(planTar, planDir);
337
+ const chunkPaths = await downloadChunkObjects(storage, event.ChunkGcsUris, work, event.Format);
338
+ let audioPath = null;
339
+ const planAudio = join(planDir, "audio.aac");
340
+ if (existsSync3(planAudio) && statSync2(planAudio).size > 0) {
341
+ audioPath = planAudio;
342
+ } else if (event.AudioGcsUri) {
343
+ audioPath = planAudio;
344
+ await downloadGcsObjectToFile(storage, event.AudioGcsUri, audioPath);
345
+ }
346
+ const finalOutput = event.Format === "png-sequence" ? join(work, "output-frames") : join(work, `output${formatExtension(event.Format)}`);
347
+ const result = await primitive(planDir, chunkPaths, audioPath, finalOutput, {
348
+ cfr: event.Cfr === true
349
+ });
350
+ if (event.Format === "png-sequence") {
351
+ const tarball = `${finalOutput}.tar.gz`;
352
+ await tarDirectory(finalOutput, tarball);
353
+ await uploadFileToGcs(storage, tarball, event.OutputGcsUri, "application/gzip");
354
+ } else {
355
+ await uploadFileToGcs(storage, finalOutput, event.OutputGcsUri);
356
+ }
357
+ return {
358
+ Action: "assemble",
359
+ OutputGcsUri: event.OutputGcsUri,
360
+ FramesEncoded: result.framesEncoded,
361
+ FileSize: result.fileSize,
362
+ DurationMs: Date.now() - started
363
+ };
364
+ } finally {
365
+ cleanupDir(work);
366
+ }
367
+ }
368
+ async function downloadChunkObjects(storage, uris, workDir, format) {
369
+ const chunksDir = join(workDir, "chunks");
370
+ mkdirSync2(chunksDir, { recursive: true });
371
+ const local = new Array(uris.length);
372
+ await Promise.all(
373
+ uris.map(async (uri, i) => {
374
+ if (!uri) {
375
+ throw new Error(`[handler] chunk URI at index ${i} is empty`);
376
+ }
377
+ const { key } = parseGcsUri(uri);
378
+ const localPath = join(chunksDir, basename(key));
379
+ await downloadGcsObjectToFile(storage, uri, localPath);
380
+ if (format === "png-sequence") {
381
+ const dirPath = join(chunksDir, `frames-${pad(i)}`);
382
+ await untarDirectory(localPath, dirPath);
383
+ local[i] = dirPath;
384
+ } else {
385
+ local[i] = localPath;
386
+ }
387
+ })
388
+ );
389
+ return local;
390
+ }
391
+ function getEventGcsUris(event) {
392
+ switch (event.Action) {
393
+ case "plan":
394
+ return [event.ProjectGcsUri, event.PlanOutputGcsPrefix];
395
+ case "renderChunk":
396
+ return [event.PlanGcsUri, event.ChunkOutputGcsPrefix];
397
+ case "assemble":
398
+ return [
399
+ event.PlanGcsUri,
400
+ ...event.ChunkGcsUris,
401
+ event.OutputGcsUri,
402
+ event.AudioGcsUri
403
+ ].filter((u) => u != null);
404
+ }
405
+ }
406
+ var warnedAllowlistDisabled = false;
407
+ function validateEventGcsUris(event) {
408
+ const allowedBucket = process.env.HYPERFRAMES_RENDER_BUCKET?.trim();
409
+ if (allowedBucket === "*") return;
410
+ if (!allowedBucket) {
411
+ if (!warnedAllowlistDisabled) {
412
+ warnedAllowlistDisabled = true;
413
+ logEvent({
414
+ event: "bucket_allowlist_disabled",
415
+ level: "WARNING",
416
+ message: 'HYPERFRAMES_RENDER_BUCKET is unset \u2014 the GCS bucket-allowlist guard is DISABLED. Set it to the render bucket name to enforce, or to "*" to opt out intentionally.'
417
+ });
418
+ }
419
+ return;
420
+ }
421
+ for (const uri of getEventGcsUris(event)) {
422
+ const { bucket } = parseGcsUri(uri);
423
+ if (bucket !== allowedBucket) {
424
+ const err = new Error(
425
+ `[handler] GCS_URI_NOT_ALLOWED: URI ${JSON.stringify(uri)} targets bucket "${bucket}" but only "${allowedBucket}" is permitted`
426
+ );
427
+ err.name = "GCS_URI_NOT_ALLOWED";
428
+ throw err;
429
+ }
430
+ }
431
+ }
432
+ function pad(n) {
433
+ return n.toString().padStart(4, "0");
434
+ }
435
+ function trimTrailingSlash(prefix) {
436
+ return prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
437
+ }
438
+ function cleanupDir(dir) {
439
+ try {
440
+ rmSync2(dir, { recursive: true, force: true });
441
+ } catch {
442
+ }
443
+ }
444
+ function verifyPlanHash(planDir, expected) {
445
+ const planJsonPath = join(planDir, "plan.json");
446
+ let parsed;
447
+ try {
448
+ parsed = JSON.parse(readFileSync(planJsonPath, "utf-8"));
449
+ } catch (err) {
450
+ const msg = err instanceof Error ? err.message : String(err);
451
+ const error = new Error(`PLAN_HASH_MISMATCH: failed to read ${planJsonPath}: ${msg}`);
452
+ error.name = "PLAN_HASH_MISMATCH";
453
+ throw error;
454
+ }
455
+ const actual = parsed.planHash;
456
+ if (typeof actual !== "string" || actual !== expected) {
457
+ const error = new Error(
458
+ `PLAN_HASH_MISMATCH: event PlanHash=${expected} did not match plan.json planHash=${String(actual)}`
459
+ );
460
+ error.name = "PLAN_HASH_MISMATCH";
461
+ throw error;
462
+ }
463
+ }
464
+ var NON_RETRYABLE_ERROR_NAMES = /* @__PURE__ */ new Set([
465
+ // Handler-boundary guards.
466
+ "GCS_URI_NOT_ALLOWED",
467
+ "PLAN_HASH_MISMATCH",
468
+ // Producer error class names (`.name`) + their string code aliases — the
469
+ // class sets `.name` to the class name but wraps a `code`; cover both so a
470
+ // raw-code throw is caught too. Mirrors the AWS state machine's
471
+ // non-retryable list.
472
+ "FormatNotSupportedInDistributedError",
473
+ "PlanTooLargeError",
474
+ "RenderChunkValidationError",
475
+ "FFMPEG_VERSION_MISMATCH",
476
+ "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED",
477
+ "PLAN_TOO_LARGE",
478
+ "BROWSER_GPU_NOT_SOFTWARE",
479
+ "FONT_FETCH_FAILED",
480
+ "ChromeBinaryUnavailableError"
481
+ ]);
482
+ function createApp(deps) {
483
+ const app = new Hono();
484
+ app.get("/healthz", (c) => c.json({ status: "ok" }));
485
+ app.post("/", async (c) => {
486
+ let body;
487
+ try {
488
+ body = await c.req.json();
489
+ } catch {
490
+ return c.json({ error: "BAD_REQUEST", message: "request body must be JSON" }, 400);
491
+ }
492
+ try {
493
+ const result = await dispatch(body, deps);
494
+ return c.json(result, 200);
495
+ } catch (err) {
496
+ const name = err instanceof Error ? err.name : void 0;
497
+ const message = err instanceof Error ? err.message : String(err);
498
+ const status = name && NON_RETRYABLE_ERROR_NAMES.has(name) ? 400 : 500;
499
+ return c.json({ error: name ?? "RenderError", message }, status);
500
+ }
501
+ });
502
+ return app;
503
+ }
504
+ function startServer() {
505
+ const port = Number(process.env.PORT ?? 8080);
506
+ const app = createApp();
507
+ serve({ fetch: app.fetch, port }, (info) => {
508
+ logEvent({ event: "server_listening", port: info.port });
509
+ });
510
+ }
511
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
512
+ startServer();
513
+ }
514
+
515
+ // src/sdk/deploySite.ts
516
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
517
+ import { tmpdir as tmpdir2 } from "node:os";
518
+ import { join as join2 } from "node:path";
519
+ import { Storage as Storage2 } from "@google-cloud/storage";
520
+ import { hashProjectDir } from "@hyperframes/producer/distributed";
521
+ async function deploySite(opts) {
522
+ if (!statSync3(opts.projectDir).isDirectory()) {
523
+ throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
524
+ }
525
+ const siteId = opts.siteId ?? hashProjectDir(opts.projectDir);
526
+ const key = `sites/${siteId}/project.tar.gz`;
527
+ const projectGcsUri = formatGcsUri({ bucket: opts.bucketName, key });
528
+ const storage = opts.storage ?? new Storage2();
529
+ const file = storage.bucket(opts.bucketName).file(key);
530
+ const existing = await headObject(file);
531
+ if (existing) {
532
+ return {
533
+ siteId,
534
+ bucketName: opts.bucketName,
535
+ projectGcsUri,
536
+ bytes: existing.bytes,
537
+ uploadedAt: existing.lastModified,
538
+ uploaded: false
539
+ };
540
+ }
541
+ const workdir = mkdtempSync2(join2(tmpdir2(), "hf-deploy-site-"));
542
+ try {
543
+ const tarball = join2(workdir, "project.tar.gz");
544
+ await tarDirectory(opts.projectDir, tarball);
545
+ const size = statSync3(tarball).size;
546
+ await uploadFileToGcs(storage, tarball, projectGcsUri, "application/gzip");
547
+ return {
548
+ siteId,
549
+ bucketName: opts.bucketName,
550
+ projectGcsUri,
551
+ bytes: size,
552
+ uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
553
+ uploaded: true
554
+ };
555
+ } finally {
556
+ rmSync3(workdir, { recursive: true, force: true });
557
+ }
558
+ }
559
+ async function headObject(file) {
560
+ const [exists] = await file.exists();
561
+ if (!exists) return null;
562
+ const [meta] = await file.getMetadata();
563
+ const sizeRaw = meta.size;
564
+ const bytes = typeof sizeRaw === "string" ? Number(sizeRaw) : typeof sizeRaw === "number" ? sizeRaw : 0;
565
+ return {
566
+ bytes: Number.isFinite(bytes) ? bytes : 0,
567
+ lastModified: meta.updated ?? (/* @__PURE__ */ new Date()).toISOString()
568
+ };
569
+ }
570
+
571
+ // src/sdk/renderToCloudRun.ts
572
+ import { randomUUID } from "node:crypto";
573
+
574
+ // src/sdk/validateConfig.ts
575
+ import { InvalidConfigError } from "@hyperframes/producer/distributed";
576
+ import {
577
+ InvalidConfigError as InvalidConfigError2,
578
+ validateDistributedRenderConfig,
579
+ validateVariablesPayload
580
+ } from "@hyperframes/producer/distributed";
581
+ var MAX_WORKFLOWS_INPUT_BYTES = 512 * 1024;
582
+ var LARGE_VARIABLES_DOCS_URL = "https://hyperframes.heygen.com/deploy/templates-on-lambda#working-with-large-variables";
583
+ function validateWorkflowsInputSize(input) {
584
+ let serialized;
585
+ try {
586
+ serialized = JSON.stringify(input);
587
+ } catch (err) {
588
+ throw new InvalidConfigError(
589
+ "config",
590
+ `Cloud Workflows execution argument is not JSON-serializable: ${err instanceof Error ? err.message : String(err)}`
591
+ );
592
+ }
593
+ if (serialized === void 0) {
594
+ throw new InvalidConfigError(
595
+ "config",
596
+ "Cloud Workflows execution argument is not JSON-serializable (JSON.stringify returned undefined). Check that all fields, including config.variables, are plain JSON values."
597
+ );
598
+ }
599
+ const byteLength = Buffer.byteLength(serialized, "utf8");
600
+ if (byteLength > MAX_WORKFLOWS_INPUT_BYTES) {
601
+ throw new InvalidConfigError(
602
+ "config",
603
+ `Cloud Workflows execution argument is ${byteLength} bytes, which exceeds the ${MAX_WORKFLOWS_INPUT_BYTES}-byte (512 KiB) limit. Variables are for typed data (strings, numbers, structured records); media assets (images, audio, video) should be passed as URL references the composition resolves at render time, not inlined as base64. See ${LARGE_VARIABLES_DOCS_URL} for the URL-your-assets convention.`
604
+ );
605
+ }
606
+ }
607
+
608
+ // src/sdk/renderToCloudRun.ts
609
+ async function renderToCloudRun(opts) {
610
+ validateDistributedRenderConfig(opts.config);
611
+ if (!opts.bucketName) throw new Error("[renderToCloudRun] bucketName is required");
612
+ if (!opts.projectId) throw new Error("[renderToCloudRun] projectId is required");
613
+ if (!opts.location) throw new Error("[renderToCloudRun] location is required");
614
+ if (!opts.workflowId) throw new Error("[renderToCloudRun] workflowId is required");
615
+ if (!opts.serviceUrl) throw new Error("[renderToCloudRun] serviceUrl is required");
616
+ if (!opts.siteHandle && !opts.projectDir) {
617
+ throw new Error("[renderToCloudRun] either siteHandle or projectDir must be supplied");
618
+ }
619
+ const renderId = opts.renderId ?? `hf-render-${randomUUID()}`;
620
+ if (!/^[A-Za-z0-9._-]+$/.test(renderId) || renderId.includes("..")) {
621
+ throw new Error(
622
+ `[renderToCloudRun] renderId must match [A-Za-z0-9._-]+ and not contain "..": ${JSON.stringify(renderId)}`
623
+ );
624
+ }
625
+ const ext = formatExtension(opts.config.format);
626
+ const outputKey = opts.outputKey ?? `renders/${renderId}/output${ext}`;
627
+ const planOutputGcsPrefix = formatGcsUri({
628
+ bucket: opts.bucketName,
629
+ key: `renders/${renderId}/`
630
+ });
631
+ const outputGcsUri = formatGcsUri({ bucket: opts.bucketName, key: outputKey });
632
+ const site = opts.siteHandle ?? await deploySite({
633
+ projectDir: opts.projectDir,
634
+ bucketName: opts.bucketName,
635
+ storage: opts.storage
636
+ });
637
+ const argument = {
638
+ RenderId: renderId,
639
+ ProjectGcsUri: site.projectGcsUri,
640
+ PlanOutputGcsPrefix: planOutputGcsPrefix,
641
+ OutputGcsUri: outputGcsUri,
642
+ ServiceUrl: opts.serviceUrl,
643
+ Config: opts.config
644
+ };
645
+ validateWorkflowsInputSize(argument);
646
+ const executions = opts.executions ?? await defaultExecutionsClient();
647
+ const parent = executions.workflowPath(opts.projectId, opts.location, opts.workflowId);
648
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
649
+ const [execution] = await executions.createExecution({
650
+ parent,
651
+ execution: { argument: JSON.stringify(argument) }
652
+ });
653
+ if (!execution.name) {
654
+ throw new Error("[renderToCloudRun] CreateExecution returned no execution name");
655
+ }
656
+ return {
657
+ renderId,
658
+ executionName: execution.name,
659
+ bucketName: opts.bucketName,
660
+ workflowId: opts.workflowId,
661
+ outputGcsUri,
662
+ projectGcsUri: site.projectGcsUri,
663
+ startedAt
664
+ };
665
+ }
666
+ async function defaultExecutionsClient() {
667
+ const mod = await import("@google-cloud/workflows");
668
+ const client = new mod.ExecutionsClient();
669
+ return client;
670
+ }
671
+
672
+ // src/sdk/costAccounting.ts
673
+ var CLOUD_RUN_USD_PER_VCPU_SECOND = 24e-6;
674
+ var CLOUD_RUN_USD_PER_GIB_SECOND = 25e-7;
675
+ var CLOUD_RUN_USD_PER_REQUEST = 4e-7;
676
+ var WORKFLOWS_USD_PER_STEP = 1e-5;
677
+ function computeRenderCost(invocations, workflowSteps) {
678
+ let cloudRunUsd = 0;
679
+ let anyEstimated = false;
680
+ for (const inv of invocations) {
681
+ const seconds = inv.durationMs / 1e3;
682
+ cloudRunUsd += seconds * inv.vcpu * CLOUD_RUN_USD_PER_VCPU_SECOND;
683
+ cloudRunUsd += seconds * inv.memoryGib * CLOUD_RUN_USD_PER_GIB_SECOND;
684
+ cloudRunUsd += CLOUD_RUN_USD_PER_REQUEST;
685
+ if (inv.estimated) anyEstimated = true;
686
+ }
687
+ const workflowsUsd = workflowSteps * WORKFLOWS_USD_PER_STEP;
688
+ const accruedSoFarUsd = roundUsd(cloudRunUsd + workflowsUsd);
689
+ return {
690
+ accruedSoFarUsd,
691
+ displayCost: formatUsd(accruedSoFarUsd),
692
+ breakdown: {
693
+ cloudRunUsd: roundUsd(cloudRunUsd),
694
+ workflowsUsd: roundUsd(workflowsUsd),
695
+ gcsEstimate: "not-included",
696
+ estimated: anyEstimated
697
+ }
698
+ };
699
+ }
700
+ function roundUsd(usd) {
701
+ return Math.round(usd * 1e4) / 1e4;
702
+ }
703
+ function formatUsd(usd) {
704
+ return `$${usd.toFixed(4)}`;
705
+ }
706
+
707
+ // src/sdk/getRenderProgress.ts
708
+ var DEFAULT_VCPU = 4;
709
+ var DEFAULT_MEMORY_GIB = 16;
710
+ async function getRenderProgress(opts) {
711
+ if (!opts.executionName) {
712
+ throw new Error("[getRenderProgress] executionName is required");
713
+ }
714
+ const executions = opts.executions ?? await defaultExecutionsClient2();
715
+ const vcpu = opts.vcpu ?? DEFAULT_VCPU;
716
+ const memoryGib = opts.memoryGib ?? DEFAULT_MEMORY_GIB;
717
+ const [execution] = await executions.getExecution({ name: opts.executionName });
718
+ const status = mapState(execution.state);
719
+ const startedAt = toIso(execution.startTime) ?? (/* @__PURE__ */ new Date(0)).toISOString();
720
+ const endedAt = toIso(execution.endTime);
721
+ const errors = [];
722
+ if (execution.error) {
723
+ errors.push({
724
+ state: execution.error.context ?? "<execution>",
725
+ error: extractErrorName(execution.error.payload) ?? "ExecutionError",
726
+ cause: execution.error.payload ?? ""
727
+ });
728
+ }
729
+ if (status !== "succeeded") {
730
+ return {
731
+ status,
732
+ overallProgress: 0,
733
+ framesRendered: 0,
734
+ totalFrames: null,
735
+ invocationsObserved: 0,
736
+ costs: computeRenderCost([], 0),
737
+ outputFile: null,
738
+ errors,
739
+ fatalErrorEncountered: status === "failed" || status === "cancelled",
740
+ startedAt,
741
+ endedAt
742
+ };
743
+ }
744
+ const acc = parseAccumulated(execution.result);
745
+ const chunks = acc.Chunks?.filter((c) => c != null) ?? [];
746
+ const framesRendered = chunks.reduce((sum, c) => sum + (c.FramesEncoded ?? 0), 0);
747
+ const totalFrames = typeof acc.Plan?.TotalFrames === "number" ? acc.Plan.TotalFrames : null;
748
+ const invocations = [];
749
+ const pushInv = (durationMs) => {
750
+ invocations.push({
751
+ durationMs: typeof durationMs === "number" ? durationMs : 0,
752
+ vcpu,
753
+ memoryGib,
754
+ estimated: typeof durationMs !== "number"
755
+ });
756
+ };
757
+ if (acc.Plan) pushInv(acc.Plan.DurationMs);
758
+ for (const c of chunks) pushInv(c.DurationMs);
759
+ if (acc.Assemble) pushInv(acc.Assemble.DurationMs);
760
+ const workflowSteps = invocations.length + 4;
761
+ const costs = computeRenderCost(invocations, workflowSteps);
762
+ const outputGcsUri = acc.Assemble?.OutputGcsUri;
763
+ const outputFile = outputGcsUri ? {
764
+ gcsUri: outputGcsUri,
765
+ bytes: typeof acc.Assemble?.FileSize === "number" ? acc.Assemble.FileSize : null
766
+ } : null;
767
+ return {
768
+ status,
769
+ overallProgress: 1,
770
+ framesRendered,
771
+ totalFrames,
772
+ invocationsObserved: invocations.length,
773
+ costs,
774
+ outputFile,
775
+ errors,
776
+ fatalErrorEncountered: false,
777
+ startedAt,
778
+ endedAt
779
+ };
780
+ }
781
+ function mapState(state) {
782
+ switch (state) {
783
+ case "ACTIVE":
784
+ case "QUEUED":
785
+ return "running";
786
+ case "SUCCEEDED":
787
+ return "succeeded";
788
+ case "FAILED":
789
+ case "UNAVAILABLE":
790
+ return "failed";
791
+ case "CANCELLED":
792
+ return "cancelled";
793
+ default:
794
+ return "unknown";
795
+ }
796
+ }
797
+ function parseAccumulated(result) {
798
+ if (!result) return {};
799
+ try {
800
+ const parsed = JSON.parse(result);
801
+ if (parsed && typeof parsed === "object") return parsed;
802
+ } catch {
803
+ }
804
+ return {};
805
+ }
806
+ function extractErrorName(payload) {
807
+ if (!payload) return void 0;
808
+ try {
809
+ const outer = JSON.parse(payload);
810
+ if (typeof outer.error === "string") return outer.error;
811
+ if (typeof outer.body === "string") {
812
+ const inner = JSON.parse(outer.body);
813
+ if (typeof inner.error === "string") return inner.error;
814
+ } else if (outer.body && typeof outer.body === "object") {
815
+ const inner = outer.body;
816
+ if (typeof inner.error === "string") return inner.error;
817
+ }
818
+ } catch {
819
+ }
820
+ return void 0;
821
+ }
822
+ function toIso(ts) {
823
+ if (ts == null) return null;
824
+ if (typeof ts === "string") return ts;
825
+ const seconds = ts.seconds == null ? null : Number(ts.seconds);
826
+ if (seconds == null || !Number.isFinite(seconds)) return null;
827
+ const ms = seconds * 1e3 + (ts.nanos ?? 0) / 1e6;
828
+ return new Date(ms).toISOString();
829
+ }
830
+ async function defaultExecutionsClient2() {
831
+ const mod = await import("@google-cloud/workflows");
832
+ const client = new mod.ExecutionsClient();
833
+ return client;
834
+ }
835
+ export {
836
+ ChromeBinaryUnavailableError,
837
+ InvalidConfigError2 as InvalidConfigError,
838
+ computeRenderCost,
839
+ createApp,
840
+ deploySite,
841
+ dispatch,
842
+ downloadGcsObjectToFile,
843
+ formatGcsUri,
844
+ getRenderProgress,
845
+ parseGcsUri,
846
+ renderToCloudRun,
847
+ resolveChromeExecutablePath,
848
+ startServer,
849
+ tarDirectory,
850
+ untarDirectory,
851
+ unwrapEvent,
852
+ uploadFileToGcs,
853
+ validateDistributedRenderConfig
854
+ };
855
+ //# sourceMappingURL=index.js.map