@hyperframes/aws-lambda 0.6.20

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.
Files changed (43) hide show
  1. package/README.md +191 -0
  2. package/dist/cdk/HyperframesRenderStack.d.ts +65 -0
  3. package/dist/cdk/HyperframesRenderStack.d.ts.map +1 -0
  4. package/dist/cdk/index.d.ts +10 -0
  5. package/dist/cdk/index.d.ts.map +1 -0
  6. package/dist/cdk/index.js +263 -0
  7. package/dist/cdk/index.js.map +7 -0
  8. package/dist/chromium.d.ts +77 -0
  9. package/dist/chromium.d.ts.map +1 -0
  10. package/dist/events.d.ts +120 -0
  11. package/dist/events.d.ts.map +1 -0
  12. package/dist/formatExtension.d.ts +10 -0
  13. package/dist/formatExtension.d.ts.map +1 -0
  14. package/dist/handler.d.ts +42 -0
  15. package/dist/handler.d.ts.map +1 -0
  16. package/dist/handler.js +432 -0
  17. package/dist/handler.js.map +7 -0
  18. package/dist/index.d.ts +33 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +951 -0
  21. package/dist/index.js.map +7 -0
  22. package/dist/s3Transport.d.ts +55 -0
  23. package/dist/s3Transport.d.ts.map +1 -0
  24. package/dist/sdk/costAccounting.d.ts +51 -0
  25. package/dist/sdk/costAccounting.d.ts.map +1 -0
  26. package/dist/sdk/deploySite.d.ts +55 -0
  27. package/dist/sdk/deploySite.d.ts.map +1 -0
  28. package/dist/sdk/getRenderProgress.d.ts +72 -0
  29. package/dist/sdk/getRenderProgress.d.ts.map +1 -0
  30. package/dist/sdk/index.d.ts +16 -0
  31. package/dist/sdk/index.d.ts.map +1 -0
  32. package/dist/sdk/index.js +578 -0
  33. package/dist/sdk/index.js.map +7 -0
  34. package/dist/sdk/renderToLambda.d.ts +66 -0
  35. package/dist/sdk/renderToLambda.d.ts.map +1 -0
  36. package/dist/sdk/validateConfig.d.ts +35 -0
  37. package/dist/sdk/validateConfig.d.ts.map +1 -0
  38. package/package.json +84 -0
  39. package/scripts/_formatBytes.ts +15 -0
  40. package/scripts/build-zip.ts +480 -0
  41. package/scripts/probe-beginframe.dockerfile +61 -0
  42. package/scripts/probe-beginframe.ts +157 -0
  43. package/scripts/verify-zip-size.ts +83 -0
package/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # @hyperframes/aws-lambda
2
+
3
+ AWS Lambda adapter for HyperFrames distributed rendering. Ships three
4
+ things together:
5
+
6
+ 1. The **Lambda handler** that wraps the OSS `plan` / `renderChunk` /
7
+ `assemble` primitives behind a single dispatch boundary Step Functions
8
+ can drive (`src/handler.ts`).
9
+ 2. A **client-side SDK** — `renderToLambda`, `getRenderProgress`,
10
+ `deploySite`, plus `validateDistributedRenderConfig` and
11
+ `computeRenderCost` (`src/sdk/`).
12
+ 3. An **`aws-cdk-lib` L2 construct** (`HyperframesRenderStack`) that
13
+ provisions the same topology as `examples/aws-lambda/template.yaml`
14
+ inside an adopter's own CDK app (`src/cdk/`).
15
+
16
+ The handler ZIP and the SAM template still drive a maintainer-run real-AWS
17
+ smoke flow; the SDK + CDK are the supported public surface for adopters.
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ ┌──────────────────────────────────────────────────────────────────┐
23
+ │ Step Functions state machine │
24
+ │ Plan → Map(N) RenderChunk → Assemble │
25
+ └──────────────────────────────────────────────────────────────────┘
26
+ │ dispatches by event.Action
27
+
28
+ ┌──────────────────────────────────────────────────────────────────┐
29
+ │ One Lambda function (this package's `dist/handler.zip`) │
30
+ │ handler.mjs │
31
+ │ ├─ Action="plan" → @hyperframes/producer/distributed │
32
+ │ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
33
+ │ └─ Action="assemble" → @hyperframes/producer/distributed │
34
+ │ bin/ffmpeg — ffmpeg-static │
35
+ │ node_modules/@sparticuz/chromium/ — Lambda-optimised Chromium │
36
+ └──────────────────────────────────────────────────────────────────┘
37
+ │ pure functions over local paths
38
+
39
+ ┌──────────────────────────────────────────────────────────────────┐
40
+ │ S3 bucket — plan tarball + per-chunk outputs + final mp4 │
41
+ └──────────────────────────────────────────────────────────────────┘
42
+ ```
43
+
44
+ The handler downloads inputs from S3 into `/tmp`, calls the OSS primitive,
45
+ uploads outputs back to S3, and returns a small JSON result that fits
46
+ inside Step Functions' history budget (under 200 bytes per chunk).
47
+
48
+ ## Chrome runtime
49
+
50
+ The package supports two Chromium sources:
51
+
52
+ | Source | Default | Size | When to pick it |
53
+ | ------------------------------- | ------- | ------------------ | --------------------------------------------------------------------------------------------------------------------- |
54
+ | `@sparticuz/chromium` | yes | ~70 MiB compressed | Lambda. Decompresses into `/tmp` at runtime; the rest of the ecosystem already uses it for headless-Chrome-in-Lambda. |
55
+ | Bundled `chrome-headless-shell` | no | ~140 MiB | Fallback. Used if `@sparticuz/chromium` ever drops `HeadlessExperimental.beginFrame` support. |
56
+
57
+ Pick the source at build time:
58
+
59
+ ```bash
60
+ bun run --cwd packages/aws-lambda build:zip
61
+ bun run --cwd packages/aws-lambda build:zip -- --source=chrome-headless-shell
62
+ ```
63
+
64
+ The handler reads `HYPERFRAMES_LAMBDA_CHROME_SOURCE` at boot. The build
65
+ script sets that env var via Lambda function configuration in
66
+ `examples/aws-lambda/template.yaml`.
67
+
68
+ ## BeginFrame regression guard
69
+
70
+ HyperFrames' renderer drives Chrome via the CDP
71
+ `HeadlessExperimental.beginFrame` command — same path the K8s deploy uses.
72
+ The Lambda adapter assumes that `@sparticuz/chromium`'s
73
+ chrome-headless-shell build honours BeginFrame. To prove it (and re-prove
74
+ it on every release), the package ships a Docker probe:
75
+
76
+ ```bash
77
+ # Build the Lambda-like container and run the probe.
78
+ bun run --cwd packages/aws-lambda probe:beginframe:docker
79
+ ```
80
+
81
+ The probe boots `@sparticuz/chromium` inside
82
+ `public.ecr.aws/lambda/nodejs:22` and asserts CDP `beginFrame` with
83
+ `screenshot: true` returns a PNG buffer. Exit code 0 = green; non-zero =
84
+ fall back to bundling chrome-headless-shell directly via `--source=chrome-headless-shell`.
85
+
86
+ ## Building the ZIP
87
+
88
+ ```bash
89
+ bun install # at the monorepo root
90
+ bun run --cwd packages/aws-lambda build:zip # → packages/aws-lambda/dist/handler.zip
91
+ bun run --cwd packages/aws-lambda verify:zip-size # CI gate
92
+ ```
93
+
94
+ The build script bundles `src/handler.ts` via esbuild, stages
95
+ `@sparticuz/chromium` and `puppeteer-core` under `node_modules/`, copies
96
+ ffmpeg-static into `bin/`, and zips the result. The unzipped layout is
97
+ designed to extract cleanly into Lambda's `/var/task/`.
98
+
99
+ `verify:zip-size` enforces:
100
+
101
+ - Unzipped ≤ 248 MiB (in-house budget; Lambda hard ceiling is 250 MiB unzipped — AWS docs label this "250 MB" but use binary mebibytes)
102
+ - Zipped ≤ 150 MiB (in-house budget; Lambda has no hard zipped cap for S3-deployed functions)
103
+
104
+ CI fails the PR if either is exceeded.
105
+
106
+ ## Running tests
107
+
108
+ ```bash
109
+ bun run --cwd packages/aws-lambda test # unit tests (no Chrome)
110
+ bun run --cwd packages/aws-lambda probe:beginframe # local probe (Linux only)
111
+ ```
112
+
113
+ ## Using the SDK
114
+
115
+ After deploying the stack (via the SAM template, CDK construct below, or
116
+ your own CFN of choice), drive renders from Node:
117
+
118
+ ```ts
119
+ import { deploySite, getRenderProgress, renderToLambda } from "@hyperframes/aws-lambda";
120
+
121
+ // One-time upload per project version.
122
+ const site = await deploySite({
123
+ projectDir: "./my-composition",
124
+ bucketName: "hyperframes-render-bucket",
125
+ });
126
+
127
+ // Start a render. Returns immediately — does NOT poll.
128
+ const handle = await renderToLambda({
129
+ siteHandle: site,
130
+ bucketName: site.bucketName,
131
+ stateMachineArn: "arn:aws:states:us-east-1:123:stateMachine:hyperframes-render",
132
+ config: {
133
+ fps: 30,
134
+ width: 1920,
135
+ height: 1080,
136
+ format: "mp4",
137
+ chunkSize: 240,
138
+ maxParallelChunks: 16,
139
+ runtimeCap: "lambda",
140
+ },
141
+ });
142
+
143
+ // Poll progress + cost on your own cadence.
144
+ const progress = await getRenderProgress({ executionArn: handle.executionArn });
145
+ console.log(progress.overallProgress, progress.costs.displayCost);
146
+ if (progress.status === "SUCCEEDED" && progress.outputFile) {
147
+ console.log("Render landed at", progress.outputFile.s3Uri);
148
+ }
149
+ ```
150
+
151
+ `renderToLambda` validates the config client-side via
152
+ `validateDistributedRenderConfig` and throws a typed `InvalidConfigError`
153
+ before the Step Functions execution starts, so shape errors surface
154
+ synchronously instead of as opaque `ExecutionFailed` results.
155
+
156
+ `getRenderProgress` reports an approximate per-render cost
157
+ (`accruedSoFarUsd` plus a formatted `displayCost`) derived from Lambda
158
+ billed-duration × memory × the us-east-1 on-demand rate plus the Step
159
+ Functions transition price. The math is documented in
160
+ `src/sdk/costAccounting.ts`; numbers are best-effort and exclude S3
161
+ transfer.
162
+
163
+ ## Using the CDK construct
164
+
165
+ ```ts
166
+ import { App, Stack } from "aws-cdk-lib";
167
+ import { HyperframesRenderStack } from "@hyperframes/aws-lambda/cdk";
168
+
169
+ const app = new App();
170
+ const stack = new Stack(app, "MyApp");
171
+ const render = new HyperframesRenderStack(stack, "Render", {
172
+ // optional: reservedConcurrency: 8,
173
+ // optional: lambdaMemoryMb: 10240,
174
+ // optional: chromeSource: "sparticuz",
175
+ });
176
+
177
+ // Re-export so an adopter app can wire dashboards / SNS topics.
178
+ new CfnOutput(stack, "RenderBucketName", { value: render.bucket.bucketName });
179
+ new CfnOutput(stack, "StateMachineArn", { value: render.stateMachine.stateMachineArn });
180
+ ```
181
+
182
+ `aws-cdk-lib` and `constructs` are **optional peer dependencies**: SDK-only
183
+ consumers don't pull them at runtime. The construct itself imports from
184
+ `@hyperframes/aws-lambda/cdk`.
185
+
186
+ ## What's still ahead
187
+
188
+ - `hyperframes lambda` CLI (deploy / sites create / render / progress / destroy) — PR 6.5.
189
+ - IAM bootstrap subcommand (`policies role | user | validate`) — PR 6.9.
190
+ - Lambda-local regression harness (`--mode=lambda-local`) — PR 6.6.
191
+ - Adopter-facing migration guide — PR 6.8.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `HyperframesRenderStack` — aws-cdk-lib L2 construct that emits the same
3
+ * topology as `examples/aws-lambda/template.yaml`.
4
+ *
5
+ * Adopters who embed HyperFrames inside their own CDK app can extend this
6
+ * construct or compose alongside it; the construct exposes its `.bucket`,
7
+ * `.renderFunction`, and `.stateMachine` properties so additional
8
+ * resources (alarms, dashboards, SNS topics) can be wired without
9
+ * re-deriving the ARNs from a stack export.
10
+ *
11
+ * `aws-cdk-lib` and `constructs` are **peerDependencies**. The package
12
+ * still type-checks (and the snapshot test still runs) because they're
13
+ * also `devDependencies`, but adopters who only consume the SDK side of
14
+ * `@hyperframes/aws-lambda` don't pull the CDK tree at runtime.
15
+ *
16
+ * Drift from the SAM template is guarded by the snapshot test
17
+ * (`HyperframesRenderStack.snapshot.test.ts`), which diffs the synthed
18
+ * CloudFormation against the SAM-rendered CloudFormation modulo
19
+ * normalisation.
20
+ */
21
+ import { RemovalPolicy } from "aws-cdk-lib";
22
+ import * as lambda from "aws-cdk-lib/aws-lambda";
23
+ import * as s3 from "aws-cdk-lib/aws-s3";
24
+ import * as sfn from "aws-cdk-lib/aws-stepfunctions";
25
+ import { Construct } from "constructs";
26
+ /** Construction-time props for {@link HyperframesRenderStack}. */
27
+ export interface HyperframesRenderStackProps {
28
+ /** Name prefix applied to function / state-machine / alarm names. Default `"hyperframes"`. */
29
+ projectName?: string;
30
+ /** Lambda memory in MB. Allowed: 2048..10240 in 1024 steps. Default 10240. */
31
+ lambdaMemoryMb?: 2048 | 3072 | 4096 | 5120 | 6144 | 7168 | 8192 | 9216 | 10240;
32
+ /** Per-invocation Lambda timeout. Default 900 (15 min, Lambda hard cap). */
33
+ lambdaTimeoutSec?: number;
34
+ /** Lambda reserved concurrency cap. `undefined` = unreserved (account default). */
35
+ reservedConcurrency?: number;
36
+ /** Which Chrome runtime was bundled into the handler ZIP. Default `"sparticuz"`. */
37
+ chromeSource?: "sparticuz" | "chrome-headless-shell";
38
+ /** Threshold for the runaway-invocations alarm. Default 1000 invocations/hour. */
39
+ chunkInvocationAlarmThreshold?: number;
40
+ /**
41
+ * Absolute path to the handler ZIP produced by
42
+ * `bun run --cwd packages/aws-lambda build:zip`. Defaults to the
43
+ * package-relative path the build script writes to. Adopters who
44
+ * deploy the published handler ZIP set this explicitly.
45
+ */
46
+ handlerZipPath?: string;
47
+ /** S3 bucket retention policy on stack delete. Default RETAIN. */
48
+ bucketRemovalPolicy?: RemovalPolicy;
49
+ }
50
+ export declare class HyperframesRenderStack extends Construct {
51
+ /** S3 bucket for plan tarballs, chunk outputs, and final renders. */
52
+ readonly bucket: s3.Bucket;
53
+ /** The single Lambda function dispatching plan / renderChunk / assemble. */
54
+ readonly renderFunction: lambda.Function;
55
+ /** The Step Functions state machine orchestrating the render. */
56
+ readonly stateMachine: sfn.StateMachine;
57
+ constructor(scope: Construct, id: string, props?: HyperframesRenderStackProps);
58
+ /**
59
+ * Build the state-machine chain. Kept in a single method so the SAM
60
+ * template and this construct can be diffed shape-for-shape during
61
+ * the snapshot test.
62
+ */
63
+ private buildStateMachineDefinition;
64
+ }
65
+ //# sourceMappingURL=HyperframesRenderStack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HyperframesRenderStack.d.ts","sourceRoot":"","sources":["../../src/cdk/HyperframesRenderStack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAY,aAAa,EAAQ,MAAM,aAAa,CAAC;AAE5D,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAC;AAEjD,OAAO,KAAK,EAAE,MAAM,oBAAoB,CAAC;AACzC,OAAO,KAAK,GAAG,MAAM,+BAA+B,CAAC;AAErD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,kEAAkE;AAClE,MAAM,WAAW,2BAA2B;IAC1C,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC;IAC/E,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oFAAoF;IACpF,YAAY,CAAC,EAAE,WAAW,GAAG,uBAAuB,CAAC;IACrD,kFAAkF;IAClF,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,aAAa,CAAC;CACrC;AAOD,qBAAa,sBAAuB,SAAQ,SAAS;IACnD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC;IACzC,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,GAAG,CAAC,YAAY,CAAC;gBAE5B,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,GAAE,2BAAgC;IAmHjF;;;;OAIG;IACH,OAAO,CAAC,2BAA2B;CA2IpC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CDK subpath export — `@hyperframes/aws-lambda/cdk`.
3
+ *
4
+ * Pulled into its own subpath so SDK-only consumers don't import
5
+ * `aws-cdk-lib`. The construct itself depends on `aws-cdk-lib` and
6
+ * `constructs` as peer dependencies; adopters using CDK already have
7
+ * both installed.
8
+ */
9
+ export { HyperframesRenderStack, type HyperframesRenderStackProps, } from "./HyperframesRenderStack.js";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,sBAAsB,EACtB,KAAK,2BAA2B,GACjC,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,263 @@
1
+ // src/cdk/HyperframesRenderStack.ts
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Duration, RemovalPolicy, Size } from "aws-cdk-lib";
5
+ import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
6
+ import * as lambda from "aws-cdk-lib/aws-lambda";
7
+ import * as logs from "aws-cdk-lib/aws-logs";
8
+ import * as s3 from "aws-cdk-lib/aws-s3";
9
+ import * as sfn from "aws-cdk-lib/aws-stepfunctions";
10
+ import * as tasks from "aws-cdk-lib/aws-stepfunctions-tasks";
11
+ import { Construct } from "constructs";
12
+ var DEFAULT_MEMORY_MB = 10240;
13
+ var DEFAULT_TIMEOUT_SEC = 900;
14
+ var DEFAULT_CHROME_SOURCE = "sparticuz";
15
+ var DEFAULT_ALARM_THRESHOLD = 1e3;
16
+ var HyperframesRenderStack = class extends Construct {
17
+ /** S3 bucket for plan tarballs, chunk outputs, and final renders. */
18
+ bucket;
19
+ /** The single Lambda function dispatching plan / renderChunk / assemble. */
20
+ renderFunction;
21
+ /** The Step Functions state machine orchestrating the render. */
22
+ stateMachine;
23
+ constructor(scope, id, props = {}) {
24
+ super(scope, id);
25
+ const projectName = props.projectName ?? "hyperframes";
26
+ const memorySize = props.lambdaMemoryMb ?? DEFAULT_MEMORY_MB;
27
+ const timeoutSec = props.lambdaTimeoutSec ?? DEFAULT_TIMEOUT_SEC;
28
+ const chromeSource = props.chromeSource ?? DEFAULT_CHROME_SOURCE;
29
+ const alarmThreshold = props.chunkInvocationAlarmThreshold ?? DEFAULT_ALARM_THRESHOLD;
30
+ const handlerZipPath = props.handlerZipPath ?? defaultHandlerZipPath();
31
+ this.bucket = new s3.Bucket(this, "RenderBucket", {
32
+ removalPolicy: props.bucketRemovalPolicy ?? RemovalPolicy.RETAIN,
33
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
34
+ // `Suspended` is the cheapest mode that still satisfies KMS / replication
35
+ // prerequisites callers can layer on later. Adopters who treat the final
36
+ // mp4 as user-keepable can switch to `Enabled`.
37
+ versioned: false,
38
+ lifecycleRules: [
39
+ {
40
+ id: "ExpireIntermediates",
41
+ enabled: true,
42
+ prefix: "renders/",
43
+ expiration: Duration.days(7)
44
+ }
45
+ ]
46
+ });
47
+ this.renderFunction = new lambda.Function(this, "RenderFunction", {
48
+ functionName: `${projectName}-render`,
49
+ runtime: lambda.Runtime.NODEJS_22_X,
50
+ handler: "handler.handler",
51
+ code: lambda.Code.fromAsset(handlerZipPath),
52
+ memorySize,
53
+ timeout: Duration.seconds(timeoutSec),
54
+ ephemeralStorageSize: Size.gibibytes(10),
55
+ architecture: lambda.Architecture.X86_64,
56
+ reservedConcurrentExecutions: props.reservedConcurrency,
57
+ tracing: lambda.Tracing.ACTIVE,
58
+ environment: {
59
+ NODE_OPTIONS: "--enable-source-maps",
60
+ TMPDIR: "/tmp",
61
+ HYPERFRAMES_LAMBDA_CHROME_SOURCE: chromeSource
62
+ }
63
+ });
64
+ this.bucket.grantReadWrite(this.renderFunction);
65
+ const stateMachineLogGroup = new logs.LogGroup(this, "RenderStateMachineLogGroup", {
66
+ logGroupName: `/aws/states/${projectName}-render`,
67
+ retention: logs.RetentionDays.ONE_MONTH,
68
+ removalPolicy: RemovalPolicy.DESTROY
69
+ });
70
+ const definition = this.buildStateMachineDefinition();
71
+ this.stateMachine = new sfn.StateMachine(this, "RenderStateMachine", {
72
+ stateMachineName: `${projectName}-render`,
73
+ stateMachineType: sfn.StateMachineType.STANDARD,
74
+ definitionBody: sfn.DefinitionBody.fromChainable(definition),
75
+ tracingEnabled: true,
76
+ timeout: Duration.hours(1),
77
+ logs: {
78
+ destination: stateMachineLogGroup,
79
+ level: sfn.LogLevel.ERROR,
80
+ includeExecutionData: false
81
+ }
82
+ });
83
+ this.renderFunction.grantInvoke(this.stateMachine);
84
+ new cloudwatch.Alarm(this, "RenderChunkInvocationAlarm", {
85
+ alarmName: `${projectName}-runaway-chunk-invocations`,
86
+ alarmDescription: "Fires if RenderChunk Lambda invocations exceed the configured threshold in a 1-hour window.",
87
+ metric: this.renderFunction.metricInvocations({
88
+ period: Duration.hours(1),
89
+ statistic: cloudwatch.Stats.SUM
90
+ }),
91
+ threshold: alarmThreshold,
92
+ evaluationPeriods: 1,
93
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
94
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
95
+ });
96
+ new cloudwatch.Alarm(this, "RenderFunctionErrorsAlarm", {
97
+ alarmName: `${projectName}-render-function-errors`,
98
+ alarmDescription: "Fires if the render Lambda reports any errors in a 5-minute window.",
99
+ metric: this.renderFunction.metricErrors({
100
+ period: Duration.minutes(5),
101
+ statistic: cloudwatch.Stats.SUM
102
+ }),
103
+ threshold: 1,
104
+ evaluationPeriods: 1,
105
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
106
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
107
+ });
108
+ new cloudwatch.Alarm(this, "RenderStateMachineFailedAlarm", {
109
+ alarmName: `${projectName}-render-state-machine-failed`,
110
+ alarmDescription: "Fires when the render state machine reports a failed execution.",
111
+ metric: this.stateMachine.metricFailed({
112
+ period: Duration.minutes(5),
113
+ statistic: cloudwatch.Stats.SUM
114
+ }),
115
+ threshold: 1,
116
+ evaluationPeriods: 1,
117
+ comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
118
+ treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING
119
+ });
120
+ }
121
+ /**
122
+ * Build the state-machine chain. Kept in a single method so the SAM
123
+ * template and this construct can be diffed shape-for-shape during
124
+ * the snapshot test.
125
+ */
126
+ buildStateMachineDefinition() {
127
+ const NON_RETRYABLE_PLAN = [
128
+ "FFMPEG_VERSION_MISMATCH",
129
+ "PLAN_HASH_MISMATCH",
130
+ "BROWSER_GPU_NOT_SOFTWARE",
131
+ "FONT_FETCH_FAILED",
132
+ "PLAN_TOO_LARGE",
133
+ "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED"
134
+ ];
135
+ const NON_RETRYABLE_CHUNK = [
136
+ "FFMPEG_VERSION_MISMATCH",
137
+ "PLAN_HASH_MISMATCH",
138
+ "BROWSER_GPU_NOT_SOFTWARE"
139
+ ];
140
+ const NON_RETRYABLE_ASSEMBLE = [
141
+ "FFMPEG_VERSION_MISMATCH",
142
+ "PLAN_HASH_MISMATCH",
143
+ "FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED"
144
+ ];
145
+ const plan = new tasks.LambdaInvoke(this, "Plan", {
146
+ lambdaFunction: this.renderFunction,
147
+ payload: sfn.TaskInput.fromObject({
148
+ Action: "plan",
149
+ "ProjectS3Uri.$": "$.ProjectS3Uri",
150
+ "PlanOutputS3Prefix.$": "$.PlanOutputS3Prefix",
151
+ "Config.$": "$.Config"
152
+ }),
153
+ resultSelector: {
154
+ "PlanS3Uri.$": "$.Payload.PlanS3Uri",
155
+ "PlanHash.$": "$.Payload.PlanHash",
156
+ "ChunkCount.$": "$.Payload.ChunkCount",
157
+ "Format.$": "$.Payload.Format",
158
+ "HasAudio.$": "$.Payload.HasAudio",
159
+ "AudioS3Uri.$": "$.Payload.AudioS3Uri"
160
+ },
161
+ resultPath: "$.Plan"
162
+ });
163
+ plan.addRetry({
164
+ errors: NON_RETRYABLE_PLAN,
165
+ maxAttempts: 0
166
+ });
167
+ plan.addRetry({
168
+ errors: ["States.ALL"],
169
+ interval: Duration.seconds(2),
170
+ maxAttempts: 4,
171
+ backoffRate: 2,
172
+ maxDelay: Duration.seconds(60)
173
+ });
174
+ const buildChunkList = new sfn.Pass(this, "BuildChunkList", {
175
+ parameters: {
176
+ "ChunkIndexes.$": "States.ArrayRange(0, States.MathAdd($.Plan.ChunkCount, -1), 1)"
177
+ },
178
+ resultPath: "$.Iterator"
179
+ });
180
+ const planProducedZero = new sfn.Fail(this, "PlanProducedZeroChunks", {
181
+ error: "PLAN_TOO_LARGE",
182
+ cause: "Plan returned ChunkCount=0 \u2014 non-retryable producer-side invariant violation."
183
+ });
184
+ const renderChunkTask = new tasks.LambdaInvoke(this, "RenderChunk", {
185
+ lambdaFunction: this.renderFunction,
186
+ payload: sfn.TaskInput.fromObject({
187
+ Action: "renderChunk",
188
+ "ChunkIndex.$": "$.ChunkIndex",
189
+ "PlanS3Uri.$": "$.PlanS3Uri",
190
+ "PlanHash.$": "$.PlanHash",
191
+ "ChunkOutputS3Prefix.$": "$.ChunkOutputS3Prefix",
192
+ "Format.$": "$.Format"
193
+ }),
194
+ resultSelector: {
195
+ "ChunkS3Uri.$": "$.Payload.ChunkS3Uri",
196
+ "ChunkIndex.$": "$.Payload.ChunkIndex",
197
+ "Sha256.$": "$.Payload.Sha256"
198
+ }
199
+ });
200
+ renderChunkTask.addRetry({
201
+ errors: NON_RETRYABLE_CHUNK,
202
+ maxAttempts: 0
203
+ });
204
+ renderChunkTask.addRetry({
205
+ errors: ["States.ALL"],
206
+ interval: Duration.seconds(2),
207
+ maxAttempts: 4,
208
+ backoffRate: 2,
209
+ maxDelay: Duration.seconds(60)
210
+ });
211
+ const renderChunks = new sfn.Map(this, "RenderChunks", {
212
+ itemsPath: "$.Iterator.ChunkIndexes",
213
+ itemSelector: {
214
+ "ChunkIndex.$": "$$.Map.Item.Value",
215
+ "PlanS3Uri.$": "$.Plan.PlanS3Uri",
216
+ "PlanHash.$": "$.Plan.PlanHash",
217
+ "ChunkOutputS3Prefix.$": "$.PlanOutputS3Prefix",
218
+ "Format.$": "$.Plan.Format"
219
+ },
220
+ maxConcurrencyPath: "$.Plan.ChunkCount",
221
+ resultPath: "$.Chunks"
222
+ });
223
+ renderChunks.itemProcessor(renderChunkTask);
224
+ const assemble = new tasks.LambdaInvoke(this, "Assemble", {
225
+ lambdaFunction: this.renderFunction,
226
+ payload: sfn.TaskInput.fromObject({
227
+ Action: "assemble",
228
+ "PlanS3Uri.$": "$.Plan.PlanS3Uri",
229
+ "ChunkS3Uris.$": "$.Chunks[*].ChunkS3Uri",
230
+ "AudioS3Uri.$": "$.Plan.AudioS3Uri",
231
+ "OutputS3Uri.$": "$.OutputS3Uri",
232
+ "Format.$": "$.Plan.Format"
233
+ }),
234
+ resultSelector: {
235
+ "OutputS3Uri.$": "$.Payload.OutputS3Uri",
236
+ "FramesEncoded.$": "$.Payload.FramesEncoded",
237
+ "FileSize.$": "$.Payload.FileSize"
238
+ },
239
+ resultPath: "$.Output"
240
+ });
241
+ assemble.addRetry({
242
+ errors: NON_RETRYABLE_ASSEMBLE,
243
+ maxAttempts: 0
244
+ });
245
+ assemble.addRetry({
246
+ errors: ["States.ALL"],
247
+ interval: Duration.seconds(2),
248
+ maxAttempts: 4,
249
+ backoffRate: 2,
250
+ maxDelay: Duration.seconds(60)
251
+ });
252
+ const assertChunkCount = new sfn.Choice(this, "AssertChunkCount").when(sfn.Condition.numberGreaterThan("$.Plan.ChunkCount", 0), renderChunks.next(assemble)).otherwise(planProducedZero);
253
+ return plan.next(buildChunkList).next(assertChunkCount);
254
+ }
255
+ };
256
+ function defaultHandlerZipPath() {
257
+ const here = dirname(fileURLToPath(import.meta.url));
258
+ return resolve(here, "..", "..", "dist", "handler.zip");
259
+ }
260
+ export {
261
+ HyperframesRenderStack
262
+ };
263
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cdk/HyperframesRenderStack.ts"],
4
+ "sourcesContent": ["/**\n * `HyperframesRenderStack` \u2014 aws-cdk-lib L2 construct that emits the same\n * topology as `examples/aws-lambda/template.yaml`.\n *\n * Adopters who embed HyperFrames inside their own CDK app can extend this\n * construct or compose alongside it; the construct exposes its `.bucket`,\n * `.renderFunction`, and `.stateMachine` properties so additional\n * resources (alarms, dashboards, SNS topics) can be wired without\n * re-deriving the ARNs from a stack export.\n *\n * `aws-cdk-lib` and `constructs` are **peerDependencies**. The package\n * still type-checks (and the snapshot test still runs) because they're\n * also `devDependencies`, but adopters who only consume the SDK side of\n * `@hyperframes/aws-lambda` don't pull the CDK tree at runtime.\n *\n * Drift from the SAM template is guarded by the snapshot test\n * (`HyperframesRenderStack.snapshot.test.ts`), which diffs the synthed\n * CloudFormation against the SAM-rendered CloudFormation modulo\n * normalisation.\n */\n\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Duration, RemovalPolicy, Size } from \"aws-cdk-lib\";\nimport * as cloudwatch from \"aws-cdk-lib/aws-cloudwatch\";\nimport * as lambda from \"aws-cdk-lib/aws-lambda\";\nimport * as logs from \"aws-cdk-lib/aws-logs\";\nimport * as s3 from \"aws-cdk-lib/aws-s3\";\nimport * as sfn from \"aws-cdk-lib/aws-stepfunctions\";\nimport * as tasks from \"aws-cdk-lib/aws-stepfunctions-tasks\";\nimport { Construct } from \"constructs\";\n\n/** Construction-time props for {@link HyperframesRenderStack}. */\nexport interface HyperframesRenderStackProps {\n /** Name prefix applied to function / state-machine / alarm names. Default `\"hyperframes\"`. */\n projectName?: string;\n /** Lambda memory in MB. Allowed: 2048..10240 in 1024 steps. Default 10240. */\n lambdaMemoryMb?: 2048 | 3072 | 4096 | 5120 | 6144 | 7168 | 8192 | 9216 | 10240;\n /** Per-invocation Lambda timeout. Default 900 (15 min, Lambda hard cap). */\n lambdaTimeoutSec?: number;\n /** Lambda reserved concurrency cap. `undefined` = unreserved (account default). */\n reservedConcurrency?: number;\n /** Which Chrome runtime was bundled into the handler ZIP. Default `\"sparticuz\"`. */\n chromeSource?: \"sparticuz\" | \"chrome-headless-shell\";\n /** Threshold for the runaway-invocations alarm. Default 1000 invocations/hour. */\n chunkInvocationAlarmThreshold?: number;\n /**\n * Absolute path to the handler ZIP produced by\n * `bun run --cwd packages/aws-lambda build:zip`. Defaults to the\n * package-relative path the build script writes to. Adopters who\n * deploy the published handler ZIP set this explicitly.\n */\n handlerZipPath?: string;\n /** S3 bucket retention policy on stack delete. Default RETAIN. */\n bucketRemovalPolicy?: RemovalPolicy;\n}\n\nconst DEFAULT_MEMORY_MB = 10240;\nconst DEFAULT_TIMEOUT_SEC = 900;\nconst DEFAULT_CHROME_SOURCE = \"sparticuz\";\nconst DEFAULT_ALARM_THRESHOLD = 1000;\n\nexport class HyperframesRenderStack extends Construct {\n /** S3 bucket for plan tarballs, chunk outputs, and final renders. */\n readonly bucket: s3.Bucket;\n /** The single Lambda function dispatching plan / renderChunk / assemble. */\n readonly renderFunction: lambda.Function;\n /** The Step Functions state machine orchestrating the render. */\n readonly stateMachine: sfn.StateMachine;\n\n constructor(scope: Construct, id: string, props: HyperframesRenderStackProps = {}) {\n super(scope, id);\n\n const projectName = props.projectName ?? \"hyperframes\";\n const memorySize = props.lambdaMemoryMb ?? DEFAULT_MEMORY_MB;\n const timeoutSec = props.lambdaTimeoutSec ?? DEFAULT_TIMEOUT_SEC;\n const chromeSource = props.chromeSource ?? DEFAULT_CHROME_SOURCE;\n const alarmThreshold = props.chunkInvocationAlarmThreshold ?? DEFAULT_ALARM_THRESHOLD;\n const handlerZipPath = props.handlerZipPath ?? defaultHandlerZipPath();\n\n this.bucket = new s3.Bucket(this, \"RenderBucket\", {\n removalPolicy: props.bucketRemovalPolicy ?? RemovalPolicy.RETAIN,\n blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,\n // `Suspended` is the cheapest mode that still satisfies KMS / replication\n // prerequisites callers can layer on later. Adopters who treat the final\n // mp4 as user-keepable can switch to `Enabled`.\n versioned: false,\n lifecycleRules: [\n {\n id: \"ExpireIntermediates\",\n enabled: true,\n prefix: \"renders/\",\n expiration: Duration.days(7),\n },\n ],\n });\n\n this.renderFunction = new lambda.Function(this, \"RenderFunction\", {\n functionName: `${projectName}-render`,\n runtime: lambda.Runtime.NODEJS_22_X,\n handler: \"handler.handler\",\n code: lambda.Code.fromAsset(handlerZipPath),\n memorySize,\n timeout: Duration.seconds(timeoutSec),\n ephemeralStorageSize: Size.gibibytes(10),\n architecture: lambda.Architecture.X86_64,\n reservedConcurrentExecutions: props.reservedConcurrency,\n tracing: lambda.Tracing.ACTIVE,\n environment: {\n NODE_OPTIONS: \"--enable-source-maps\",\n TMPDIR: \"/tmp\",\n HYPERFRAMES_LAMBDA_CHROME_SOURCE: chromeSource,\n },\n });\n\n // Scoped S3 perms only \u2014 explicitly NOT `CloudWatchLogsFullAccess`,\n // which would grant `logs:*` on `*` and overscope adopter accounts.\n // SAM's AWSLambdaBasicExecutionRole equivalent is included by the\n // default `new lambda.Function` execution role.\n this.bucket.grantReadWrite(this.renderFunction);\n\n const stateMachineLogGroup = new logs.LogGroup(this, \"RenderStateMachineLogGroup\", {\n logGroupName: `/aws/states/${projectName}-render`,\n retention: logs.RetentionDays.ONE_MONTH,\n removalPolicy: RemovalPolicy.DESTROY,\n });\n\n const definition = this.buildStateMachineDefinition();\n\n this.stateMachine = new sfn.StateMachine(this, \"RenderStateMachine\", {\n stateMachineName: `${projectName}-render`,\n stateMachineType: sfn.StateMachineType.STANDARD,\n definitionBody: sfn.DefinitionBody.fromChainable(definition),\n tracingEnabled: true,\n timeout: Duration.hours(1),\n logs: {\n destination: stateMachineLogGroup,\n level: sfn.LogLevel.ERROR,\n includeExecutionData: false,\n },\n });\n\n this.renderFunction.grantInvoke(this.stateMachine);\n\n new cloudwatch.Alarm(this, \"RenderChunkInvocationAlarm\", {\n alarmName: `${projectName}-runaway-chunk-invocations`,\n alarmDescription:\n \"Fires if RenderChunk Lambda invocations exceed the configured threshold in a 1-hour window.\",\n metric: this.renderFunction.metricInvocations({\n period: Duration.hours(1),\n statistic: cloudwatch.Stats.SUM,\n }),\n threshold: alarmThreshold,\n evaluationPeriods: 1,\n comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,\n treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,\n });\n\n new cloudwatch.Alarm(this, \"RenderFunctionErrorsAlarm\", {\n alarmName: `${projectName}-render-function-errors`,\n alarmDescription: \"Fires if the render Lambda reports any errors in a 5-minute window.\",\n metric: this.renderFunction.metricErrors({\n period: Duration.minutes(5),\n statistic: cloudwatch.Stats.SUM,\n }),\n threshold: 1,\n evaluationPeriods: 1,\n comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,\n treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,\n });\n\n new cloudwatch.Alarm(this, \"RenderStateMachineFailedAlarm\", {\n alarmName: `${projectName}-render-state-machine-failed`,\n alarmDescription: \"Fires when the render state machine reports a failed execution.\",\n metric: this.stateMachine.metricFailed({\n period: Duration.minutes(5),\n statistic: cloudwatch.Stats.SUM,\n }),\n threshold: 1,\n evaluationPeriods: 1,\n comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,\n treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,\n });\n }\n\n /**\n * Build the state-machine chain. Kept in a single method so the SAM\n * template and this construct can be diffed shape-for-shape during\n * the snapshot test.\n */\n private buildStateMachineDefinition(): sfn.IChainable {\n const NON_RETRYABLE_PLAN = [\n \"FFMPEG_VERSION_MISMATCH\",\n \"PLAN_HASH_MISMATCH\",\n \"BROWSER_GPU_NOT_SOFTWARE\",\n \"FONT_FETCH_FAILED\",\n \"PLAN_TOO_LARGE\",\n \"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED\",\n ];\n const NON_RETRYABLE_CHUNK = [\n \"FFMPEG_VERSION_MISMATCH\",\n \"PLAN_HASH_MISMATCH\",\n \"BROWSER_GPU_NOT_SOFTWARE\",\n ];\n const NON_RETRYABLE_ASSEMBLE = [\n \"FFMPEG_VERSION_MISMATCH\",\n \"PLAN_HASH_MISMATCH\",\n \"FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED\",\n ];\n\n const plan = new tasks.LambdaInvoke(this, \"Plan\", {\n lambdaFunction: this.renderFunction,\n payload: sfn.TaskInput.fromObject({\n Action: \"plan\",\n \"ProjectS3Uri.$\": \"$.ProjectS3Uri\",\n \"PlanOutputS3Prefix.$\": \"$.PlanOutputS3Prefix\",\n \"Config.$\": \"$.Config\",\n }),\n resultSelector: {\n \"PlanS3Uri.$\": \"$.Payload.PlanS3Uri\",\n \"PlanHash.$\": \"$.Payload.PlanHash\",\n \"ChunkCount.$\": \"$.Payload.ChunkCount\",\n \"Format.$\": \"$.Payload.Format\",\n \"HasAudio.$\": \"$.Payload.HasAudio\",\n \"AudioS3Uri.$\": \"$.Payload.AudioS3Uri\",\n },\n resultPath: \"$.Plan\",\n });\n plan.addRetry({\n errors: NON_RETRYABLE_PLAN,\n maxAttempts: 0,\n });\n plan.addRetry({\n errors: [\"States.ALL\"],\n interval: Duration.seconds(2),\n maxAttempts: 4,\n backoffRate: 2,\n maxDelay: Duration.seconds(60),\n });\n\n const buildChunkList = new sfn.Pass(this, \"BuildChunkList\", {\n parameters: {\n \"ChunkIndexes.$\": \"States.ArrayRange(0, States.MathAdd($.Plan.ChunkCount, -1), 1)\",\n },\n resultPath: \"$.Iterator\",\n });\n\n const planProducedZero = new sfn.Fail(this, \"PlanProducedZeroChunks\", {\n error: \"PLAN_TOO_LARGE\",\n cause: \"Plan returned ChunkCount=0 \u2014 non-retryable producer-side invariant violation.\",\n });\n\n const renderChunkTask = new tasks.LambdaInvoke(this, \"RenderChunk\", {\n lambdaFunction: this.renderFunction,\n payload: sfn.TaskInput.fromObject({\n Action: \"renderChunk\",\n \"ChunkIndex.$\": \"$.ChunkIndex\",\n \"PlanS3Uri.$\": \"$.PlanS3Uri\",\n \"PlanHash.$\": \"$.PlanHash\",\n \"ChunkOutputS3Prefix.$\": \"$.ChunkOutputS3Prefix\",\n \"Format.$\": \"$.Format\",\n }),\n resultSelector: {\n \"ChunkS3Uri.$\": \"$.Payload.ChunkS3Uri\",\n \"ChunkIndex.$\": \"$.Payload.ChunkIndex\",\n \"Sha256.$\": \"$.Payload.Sha256\",\n },\n });\n renderChunkTask.addRetry({\n errors: NON_RETRYABLE_CHUNK,\n maxAttempts: 0,\n });\n renderChunkTask.addRetry({\n errors: [\"States.ALL\"],\n interval: Duration.seconds(2),\n maxAttempts: 4,\n backoffRate: 2,\n maxDelay: Duration.seconds(60),\n });\n\n const renderChunks = new sfn.Map(this, \"RenderChunks\", {\n itemsPath: \"$.Iterator.ChunkIndexes\",\n itemSelector: {\n \"ChunkIndex.$\": \"$$.Map.Item.Value\",\n \"PlanS3Uri.$\": \"$.Plan.PlanS3Uri\",\n \"PlanHash.$\": \"$.Plan.PlanHash\",\n \"ChunkOutputS3Prefix.$\": \"$.PlanOutputS3Prefix\",\n \"Format.$\": \"$.Plan.Format\",\n },\n maxConcurrencyPath: \"$.Plan.ChunkCount\",\n resultPath: \"$.Chunks\",\n });\n renderChunks.itemProcessor(renderChunkTask);\n\n const assemble = new tasks.LambdaInvoke(this, \"Assemble\", {\n lambdaFunction: this.renderFunction,\n payload: sfn.TaskInput.fromObject({\n Action: \"assemble\",\n \"PlanS3Uri.$\": \"$.Plan.PlanS3Uri\",\n \"ChunkS3Uris.$\": \"$.Chunks[*].ChunkS3Uri\",\n \"AudioS3Uri.$\": \"$.Plan.AudioS3Uri\",\n \"OutputS3Uri.$\": \"$.OutputS3Uri\",\n \"Format.$\": \"$.Plan.Format\",\n }),\n resultSelector: {\n \"OutputS3Uri.$\": \"$.Payload.OutputS3Uri\",\n \"FramesEncoded.$\": \"$.Payload.FramesEncoded\",\n \"FileSize.$\": \"$.Payload.FileSize\",\n },\n resultPath: \"$.Output\",\n });\n assemble.addRetry({\n errors: NON_RETRYABLE_ASSEMBLE,\n maxAttempts: 0,\n });\n assemble.addRetry({\n errors: [\"States.ALL\"],\n interval: Duration.seconds(2),\n maxAttempts: 4,\n backoffRate: 2,\n maxDelay: Duration.seconds(60),\n });\n\n const assertChunkCount = new sfn.Choice(this, \"AssertChunkCount\")\n .when(sfn.Condition.numberGreaterThan(\"$.Plan.ChunkCount\", 0), renderChunks.next(assemble))\n .otherwise(planProducedZero);\n\n return plan.next(buildChunkList).next(assertChunkCount);\n }\n}\n\n/**\n * Default location of the handler ZIP relative to this source file. Two\n * parents up = `packages/aws-lambda/`; the build script writes the ZIP\n * to `packages/aws-lambda/dist/handler.zip`. The package is published with\n * `main: \"./src/index.ts\"`, so this path resolves correctly both in the\n * source tree (during `bun test` / local CDK synth) and in a consumer's\n * `node_modules/@hyperframes/aws-lambda/` install.\n */\nfunction defaultHandlerZipPath(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n return resolve(here, \"..\", \"..\", \"dist\", \"handler.zip\");\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Lambda-runtime Chrome resolver.
3
+ *
4
+ * `renderChunk()` (the only primitive that needs a browser) launches Chrome
5
+ * via the engine's `BrowserManager`. In Lambda we can't ship the full
6
+ * Puppeteer-managed Chrome download — Puppeteer's Chrome binary is ~330 MB
7
+ * unzipped, well over Lambda's 250 MB ZIP-deploy ceiling.
8
+ *
9
+ * Two valid runtime sources:
10
+ *
11
+ * 1. `@sparticuz/chromium` (primary). Decompresses a Lambda-optimised
12
+ * `chrome-headless-shell` build into `/tmp` at runtime. ~70 MB
13
+ * compressed; the same binary the rest of the ecosystem uses for
14
+ * headless-Chrome-in-Lambda. CDP-level BeginFrame works because the
15
+ * command lives in the protocol, not the binary; the
16
+ * `scripts/probe-beginframe.ts` regression guard pins this.
17
+ *
18
+ * 2. A bundled `chrome-headless-shell` binary (fallback). If
19
+ * `@sparticuz/chromium`'s build ever drops `HeadlessExperimental`
20
+ * support, we fall back to the same `chrome-headless-shell` build
21
+ * the K8s deploy uses. The fallback raises the ZIP from ~70 MB
22
+ * Chrome to ~140 MB Chrome — still well under 250 MB.
23
+ *
24
+ * The runtime path is selected by the `HYPERFRAMES_LAMBDA_CHROME_SOURCE`
25
+ * env var (set by `build-zip.ts`):
26
+ *
27
+ * "sparticuz" → use `@sparticuz/chromium.executablePath()`
28
+ * "chrome-headless-shell" → use `process.env.HYPERFRAMES_LAMBDA_CHROME_PATH`
29
+ *
30
+ * Adapters that bundle this package can override
31
+ * `HYPERFRAMES_LAMBDA_CHROME_PATH` directly when running outside Lambda
32
+ * (e.g. the SAM-local RIE smoke).
33
+ */
34
+ /** Discriminator for the two supported Chrome sources. */
35
+ export type ChromeSource = "sparticuz" | "chrome-headless-shell";
36
+ /**
37
+ * Read which Chrome source the bundled ZIP was built against. Defaults to
38
+ * `"sparticuz"` so a fresh build with no env override picks the primary
39
+ * path.
40
+ */
41
+ export declare function resolveChromeSource(): ChromeSource;
42
+ /**
43
+ * Resolve the absolute path to a Chrome binary suitable for BeginFrame.
44
+ *
45
+ * For `"sparticuz"`: dynamically import `@sparticuz/chromium` and call
46
+ * `chromium.executablePath()`. The module is dynamic so a build-zip that
47
+ * never reaches the import (because the fallback Chrome is bundled) can
48
+ * tree-shake it out.
49
+ *
50
+ * For `"chrome-headless-shell"`: read the path from
51
+ * `HYPERFRAMES_LAMBDA_CHROME_PATH`. Throws if absent or non-existent so a
52
+ * misconfigured deploy fails loudly at boot rather than at first frame.
53
+ */
54
+ export declare function resolveChromeExecutablePath(): Promise<string>;
55
+ /**
56
+ * Resolve the Chromium launch args for the selected source. For
57
+ * `@sparticuz/chromium` we forward `chromium.args` (Lambda-tuned defaults
58
+ * — single-process, no-sandbox, /tmp paths). For the shell fallback the
59
+ * engine's own arg builder owns it; we return an empty array so the
60
+ * engine's defaults apply.
61
+ */
62
+ export declare function resolveChromeArgs(): Promise<string[]>;
63
+ /**
64
+ * Dynamic import wrapper isolated so unit tests can stub the module without
65
+ * jest-style module mocking gymnastics. The narrow type here pins the
66
+ * subset of `@sparticuz/chromium`'s surface this package depends on; if
67
+ * the upstream module ever changes shape the type error here surfaces
68
+ * before runtime.
69
+ */
70
+ interface SparticuzChromiumModule {
71
+ args: string[];
72
+ executablePath(): Promise<string>;
73
+ }
74
+ /** Test-only seam: replace the cached `@sparticuz/chromium` module. */
75
+ export declare function _setSparticuzChromiumForTests(mod: SparticuzChromiumModule | null): void;
76
+ export {};
77
+ //# sourceMappingURL=chromium.d.ts.map