@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
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * BeginFrame regression guard for `@sparticuz/chromium`.
4
+ *
5
+ * The load-bearing assumption of `@hyperframes/aws-lambda` is that the
6
+ * Chromium build shipped by `@sparticuz/chromium` honours CDP
7
+ * `HeadlessExperimental.beginFrame` with `screenshot: true`. This script
8
+ * boots that Chromium build (decompressing into `/tmp` per the library's
9
+ * runtime contract), navigates to a tiny static page, issues one
10
+ * `beginFrame` with a screenshot request, and asserts the response
11
+ * carries a PNG buffer.
12
+ *
13
+ * The script is the contract test, not a one-shot verification — every
14
+ * release should run it inside the Docker container at
15
+ * `scripts/probe-beginframe.dockerfile` to catch any future
16
+ * `@sparticuz/chromium` rebuild that drops `HeadlessExperimental` support.
17
+ *
18
+ * Exits 0 on pass, 1 on fail. Run via:
19
+ *
20
+ * bun run --cwd packages/aws-lambda probe:beginframe # host
21
+ * bun run --cwd packages/aws-lambda probe:beginframe:docker # Lambda-like
22
+ */
23
+
24
+ import { mkdtempSync, promises as fs } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import { join } from "node:path";
27
+
28
+ interface ProbeResult {
29
+ passed: boolean;
30
+ durationMs: number;
31
+ chromiumPath: string;
32
+ screenshotBytes: number;
33
+ hasDamage: boolean;
34
+ detail: string;
35
+ }
36
+
37
+ const PROBE_HTML = `<!doctype html>
38
+ <html><head><meta charset="utf-8"><title>hf-beginframe-probe</title>
39
+ <style>html,body{margin:0;background:#173;color:#fff;font:48px/1 sans-serif;display:flex;align-items:center;justify-content:center;height:100vh}</style>
40
+ </head><body><div id="x">hf-beginframe-probe</div></body></html>`;
41
+
42
+ async function main(): Promise<void> {
43
+ const start = Date.now();
44
+ const result = await probe();
45
+ result.durationMs = Date.now() - start;
46
+ console.log(JSON.stringify(result, null, 2));
47
+ if (!result.passed) {
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ async function probe(): Promise<ProbeResult> {
53
+ let chromiumPath = "";
54
+ try {
55
+ const { default: chromium } = await import("@sparticuz/chromium");
56
+ chromiumPath = await chromium.executablePath();
57
+ const args = chromium.args;
58
+
59
+ const puppeteer = await import("puppeteer-core");
60
+
61
+ // Write probe HTML to /tmp + serve via file:// — no HTTP server in the
62
+ // probe so we don't add a dependency surface that could mask a
63
+ // Chrome-side issue. `mkdtempSync` (vs `tmpdir() + Date.now()`) gives
64
+ // an unguessable directory name so two concurrent probes on the same
65
+ // host don't collide and CodeQL's insecure-tempfile rule clears.
66
+ const tmpHtmlDir = mkdtempSync(join(tmpdir(), "hf-beginframe-"));
67
+ const htmlPath = join(tmpHtmlDir, "probe.html");
68
+ await fs.writeFile(htmlPath, PROBE_HTML, "utf-8");
69
+
70
+ // BeginFrame requires the full compositor-driving flag set. These match
71
+ // the args the engine's `browserManager` passes when `captureMode !==
72
+ // "screenshot"`. Without the surface-synchronization + threaded-disable
73
+ // flags, Chrome's compositor returns `hasDamage: false` and skips the
74
+ // screenshot — the same observation pinned in the hyperframes memory
75
+ // ("Chrome's beginFrame with `screenshot` param always reports
76
+ // hasDamage=true").
77
+ const beginFrameFlags = [
78
+ "--deterministic-mode",
79
+ "--enable-begin-frame-control",
80
+ "--disable-new-content-rendering-timeout",
81
+ "--run-all-compositor-stages-before-draw",
82
+ "--disable-threaded-animation",
83
+ "--disable-threaded-scrolling",
84
+ "--disable-checker-imaging",
85
+ "--disable-image-animation-resync",
86
+ "--enable-surface-synchronization",
87
+ // Software GL — Lambda has no GPU; matches the in-process renderer's
88
+ // software-locked path.
89
+ "--use-gl=angle",
90
+ "--use-angle=swiftshader",
91
+ "--enable-unsafe-swiftshader",
92
+ ];
93
+
94
+ const browser = await puppeteer.launch({
95
+ executablePath: chromiumPath,
96
+ headless: "shell",
97
+ args: [...args, ...beginFrameFlags],
98
+ defaultViewport: { width: 800, height: 600 },
99
+ });
100
+ try {
101
+ const page = await browser.newPage();
102
+ await page.goto(`file://${htmlPath}`, { waitUntil: "domcontentloaded", timeout: 30_000 });
103
+ const session = await page.createCDPSession();
104
+ await session.send("HeadlessExperimental.enable");
105
+ // Warm-up beginFrame with noDisplayUpdates: true — drives the
106
+ // compositor without producing a screenshot, matching how the engine
107
+ // primes a capture loop.
108
+ await session.send("HeadlessExperimental.beginFrame", {
109
+ frameTimeTicks: 0,
110
+ interval: 33,
111
+ noDisplayUpdates: true,
112
+ });
113
+ const response = await session.send("HeadlessExperimental.beginFrame", {
114
+ frameTimeTicks: 1000,
115
+ interval: 33,
116
+ screenshot: { format: "png" },
117
+ });
118
+ await fs.rm(tmpHtmlDir, { recursive: true, force: true }).catch(() => {});
119
+ const screenshot = response.screenshotData ?? "";
120
+ const bytes = screenshot ? Buffer.from(screenshot, "base64") : Buffer.alloc(0);
121
+ const isPng =
122
+ bytes.length >= 8 &&
123
+ bytes[0] === 0x89 &&
124
+ bytes[1] === 0x50 &&
125
+ bytes[2] === 0x4e &&
126
+ bytes[3] === 0x47;
127
+ return {
128
+ passed: isPng && bytes.length > 0,
129
+ durationMs: 0,
130
+ chromiumPath,
131
+ screenshotBytes: bytes.length,
132
+ hasDamage: response.hasDamage,
133
+ detail: isPng
134
+ ? "OK — BeginFrame returned a PNG buffer."
135
+ : `FAIL — BeginFrame returned ${bytes.length} bytes, PNG signature ${
136
+ bytes.length >= 4 ? bytes.subarray(0, 4).toString("hex") : "<empty>"
137
+ }`,
138
+ };
139
+ } finally {
140
+ await browser.close().catch(() => {});
141
+ }
142
+ } catch (err) {
143
+ return {
144
+ passed: false,
145
+ durationMs: 0,
146
+ chromiumPath,
147
+ screenshotBytes: 0,
148
+ hasDamage: false,
149
+ detail: `FAIL — ${err instanceof Error ? err.message : String(err)}`,
150
+ };
151
+ }
152
+ }
153
+
154
+ void main().catch((err) => {
155
+ console.error("[probe-beginframe] unexpected:", err);
156
+ process.exit(2);
157
+ });
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CI gate on the Lambda ZIP size.
4
+ *
5
+ * Reads `dist/handler.zip.manifest.json` (written by `build-zip.ts`) and
6
+ * exits non-zero if either the unzipped or zipped size exceeds the
7
+ * declared limits. Lambda's hard ceiling for ZIP-deployed functions is
8
+ * 250 MiB unzipped (262144000 bytes — AWS docs label it "250 MB" but use
9
+ * binary mebibytes); the in-house budget is 248 MiB to keep headroom for
10
+ * the Chrome tarball decompression that happens at cold start.
11
+ */
12
+
13
+ import { existsSync, readFileSync, statSync } from "node:fs";
14
+ import { dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { formatBytes } from "./_formatBytes.js";
17
+
18
+ const scriptDir = dirname(fileURLToPath(import.meta.url));
19
+ const packageRoot = resolve(scriptDir, "..");
20
+ const distDir = join(packageRoot, "dist");
21
+ const zipPath = join(distDir, "handler.zip");
22
+ const manifestPath = join(distDir, "handler.zip.manifest.json");
23
+
24
+ interface Manifest {
25
+ unzippedBytes: number;
26
+ zippedBytes: number;
27
+ source: string;
28
+ }
29
+
30
+ const IN_HOUSE_UNZIPPED_LIMIT = 248 * 1024 * 1024;
31
+ const IN_HOUSE_ZIPPED_LIMIT = 150 * 1024 * 1024;
32
+
33
+ function main(): void {
34
+ if (!existsSync(zipPath)) {
35
+ console.error(`[verify-zip-size] ${zipPath} not found. Run 'bun run build:zip' first.`);
36
+ process.exit(1);
37
+ }
38
+ if (!existsSync(manifestPath)) {
39
+ console.error(
40
+ `[verify-zip-size] ${manifestPath} not found. The manifest is written by build-zip.ts; ` +
41
+ `re-run the build.`,
42
+ );
43
+ process.exit(1);
44
+ }
45
+
46
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as Manifest;
47
+ const actualZipped = statSync(zipPath).size;
48
+ if (actualZipped !== manifest.zippedBytes) {
49
+ console.warn(
50
+ `[verify-zip-size] note: zip file size on disk (${actualZipped}) differs from ` +
51
+ `manifest (${manifest.zippedBytes}). Using on-disk value.`,
52
+ );
53
+ }
54
+
55
+ let failed = false;
56
+ if (manifest.unzippedBytes > IN_HOUSE_UNZIPPED_LIMIT) {
57
+ console.error(
58
+ `[verify-zip-size] FAIL unzipped: ${formatBytes(manifest.unzippedBytes)} > ` +
59
+ `${formatBytes(IN_HOUSE_UNZIPPED_LIMIT)} (in-house limit; Lambda hard ceiling is 250 MiB).`,
60
+ );
61
+ failed = true;
62
+ }
63
+ if (actualZipped > IN_HOUSE_ZIPPED_LIMIT) {
64
+ console.error(
65
+ `[verify-zip-size] FAIL zipped: ${formatBytes(actualZipped)} > ` +
66
+ `${formatBytes(IN_HOUSE_ZIPPED_LIMIT)} (in-house limit; Lambda direct-upload ceiling is 50 MiB, ` +
67
+ `S3-deploy ceiling is 250 MiB).`,
68
+ );
69
+ failed = true;
70
+ }
71
+
72
+ if (failed) {
73
+ console.error("[verify-zip-size] FAILED — bundle is too large for Lambda ZIP deploy.");
74
+ process.exit(1);
75
+ }
76
+ console.log(
77
+ `[verify-zip-size] OK source=${manifest.source} unzipped=${formatBytes(
78
+ manifest.unzippedBytes,
79
+ )} zipped=${formatBytes(actualZipped)}`,
80
+ );
81
+ }
82
+
83
+ main();