@hypit/hypit 0.1.13 → 0.1.14
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 +36 -2
- package/bin/hypit.mjs +0 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -0
- package/packages/cli/src/commands/environment.ts +12 -7
- package/packages/cli/src/main.ts +2 -1
- package/packages/cli/src/oauth.ts +50 -7
- package/packages/credential-store-file/README.md +60 -0
- package/packages/credential-store-file/package.json +21 -0
- package/packages/credential-store-file/src/activation.ts +29 -0
- package/packages/credential-store-file/src/index.ts +1 -0
- package/packages/credential-store-file/src/store.ts +89 -0
- package/packages/fonts-open/src/surface.ts +8 -2
- package/packages/hyperframes/README.md +16 -2
- package/packages/hyperframes/src/browser-program.ts +6 -1
- package/packages/hyperframes/src/document.ts +31 -21
- package/packages/hyperframes/src/project.ts +11 -20
- package/packages/media-execution/README.md +7 -0
- package/packages/media-execution/src/execute.ts +5 -5
- package/packages/media-execution/src/index.ts +1 -1
- package/packages/media-execution/src/process-env.ts +20 -0
- package/packages/media-execution/src/surface.ts +83 -76
- package/packages/package-loader-node/README.md +10 -0
- package/packages/package-loader-node/src/index.ts +1 -0
- package/packages/package-loader-node/src/loader.ts +25 -5
- package/packages/package-loader-node/src/location.ts +11 -2
- package/packages/provider-hyperframes-local/README.md +81 -8
- package/packages/provider-hyperframes-local/package.json +13 -3
- package/packages/provider-hyperframes-local/src/activation.ts +20 -6
- package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
- package/packages/provider-hyperframes-local/src/browser.ts +118 -0
- package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
- package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
- package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
- package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
- package/packages/provider-hyperframes-local/src/capture.ts +12 -5
- package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
- package/packages/provider-hyperframes-local/src/options.ts +2 -2
- package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
- package/packages/provider-hyperframes-local/src/process.ts +22 -0
- package/packages/provider-hyperframes-local/src/program.ts +25 -33
- package/packages/provider-hyperframes-local/src/provider.ts +4 -7
- package/packages/provider-hyperframes-local/src/render.ts +9 -4
- package/packages/runtime-host-node/README.md +5 -0
- package/packages/runtime-host-node/src/index.ts +7 -2
- package/packages/runtime-host-node/src/packages.ts +13 -7
- package/packages/runtime-local/README.md +10 -0
- package/packages/runtime-local/src/config.ts +1 -1
- package/packages/runtime-local/src/credentials.ts +10 -5
- package/packages/studio/src/preview/runtime-shim.ts +17 -3
- package/packages/video-cli/README.md +5 -1
- package/packages/video-cli/package.json +1 -0
- package/packages/video-cli/src/distribution.ts +3 -10
- package/packages/video-cli/src/version.ts +1 -1
- package/packages/yt-dlp/README.md +3 -2
- package/packages/yt-dlp/package.json +4 -0
- package/packages/yt-dlp/src/download.ts +10 -14
|
@@ -12,9 +12,10 @@ export type HyperframesArtifactReader = (
|
|
|
12
12
|
artifact: BlobRef,
|
|
13
13
|
signal?: AbortSignal,
|
|
14
14
|
) => Promise<Uint8Array | AsyncIterable<Uint8Array>>;
|
|
15
|
+
/** Inspect the completed staged file; its lifetime belongs to the staging caller. */
|
|
15
16
|
export type HyperframesSurfaceValidator = (
|
|
16
17
|
surface: CompositableSurfaceRef,
|
|
17
|
-
|
|
18
|
+
path: string,
|
|
18
19
|
signal?: AbortSignal,
|
|
19
20
|
) => Promise<void>;
|
|
20
21
|
|
|
@@ -39,9 +40,7 @@ function extension(mediaType: string): string {
|
|
|
39
40
|
/**
|
|
40
41
|
* Lay one document out as a directory HyperFrames can render.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
* Provider serves it to the capture engine, and the AWS Provider tars it into a site. If
|
|
44
|
-
* the two laid it out differently, one Need would name two different projects.
|
|
43
|
+
* A local or hosted Provider can serve the same staged document to its capture engine.
|
|
45
44
|
* Resource names are local to this staged project and carry no content claim.
|
|
46
45
|
*/
|
|
47
46
|
export async function stageHyperframesProject(options: {
|
|
@@ -63,25 +62,25 @@ export async function stageHyperframesProject(options: {
|
|
|
63
62
|
await mkdir(artifactDirectory, { recursive: true });
|
|
64
63
|
const paths = new Map<string, string>();
|
|
65
64
|
const surfaces = new Map(document.surfaces.map((surface) => [surface.artifact.resource, surface]));
|
|
66
|
-
await Promise.allSettled(document.artifacts.map(async (artifact) => {
|
|
65
|
+
await Promise.allSettled(document.artifacts.map(async (artifact, index) => {
|
|
67
66
|
try {
|
|
68
67
|
signal.throwIfAborted();
|
|
69
|
-
|
|
68
|
+
// Resource identities are portable protocol values, not filesystem names.
|
|
69
|
+
const name = `asset-${index}${extension(artifact.mediaType)}`;
|
|
70
70
|
const opened = await read(artifact, signal);
|
|
71
71
|
signal.throwIfAborted();
|
|
72
72
|
const chunks = opened instanceof Uint8Array
|
|
73
73
|
? (async function* () { yield opened; })()
|
|
74
74
|
: opened;
|
|
75
75
|
const surface = surfaces.get(artifact.resource);
|
|
76
|
-
const retained: Uint8Array[] = [];
|
|
77
76
|
let size = 0;
|
|
78
|
-
const
|
|
77
|
+
const path = join(artifactDirectory, name);
|
|
78
|
+
const target = await open(path, "w");
|
|
79
79
|
try {
|
|
80
80
|
for await (const chunk of chunks) {
|
|
81
81
|
signal.throwIfAborted();
|
|
82
|
-
await target.
|
|
82
|
+
await target.writeFile(chunk);
|
|
83
83
|
size += chunk.byteLength;
|
|
84
|
-
if (surface !== undefined) retained.push(Uint8Array.from(chunk));
|
|
85
84
|
}
|
|
86
85
|
} finally {
|
|
87
86
|
await target.close();
|
|
@@ -89,17 +88,9 @@ export async function stageHyperframesProject(options: {
|
|
|
89
88
|
if (size !== artifact.size) {
|
|
90
89
|
throw new Error(`HyperFrames Artifact ${artifact.resource} size differs`);
|
|
91
90
|
}
|
|
92
|
-
const bytes = surface === undefined ? undefined : (() => {
|
|
93
|
-
const value = new Uint8Array(size);
|
|
94
|
-
let offset = 0;
|
|
95
|
-
for (const chunk of retained) {
|
|
96
|
-
value.set(chunk, offset);
|
|
97
|
-
offset += chunk.byteLength;
|
|
98
|
-
}
|
|
99
|
-
return value;
|
|
100
|
-
})();
|
|
101
91
|
if (surface !== undefined) {
|
|
102
|
-
|
|
92
|
+
// The validator borrows the completed staged file for this call only.
|
|
93
|
+
await options.validateSurface!(structuredClone(surface), path, signal);
|
|
103
94
|
}
|
|
104
95
|
paths.set(artifact.resource, `./artifacts/${name}`);
|
|
105
96
|
} catch (error) {
|
|
@@ -26,6 +26,13 @@ deployments can reuse the same functions to preserve stream selection, timing, c
|
|
|
26
26
|
This is not an author package, Provider, queue or Core extension. It performs no endpoint selection,
|
|
27
27
|
credential lookup or SVML parsing.
|
|
28
28
|
|
|
29
|
+
`verifyCompositableSurfaceFile({ surface, path, ... })` inspects a file already staged by an execution
|
|
30
|
+
owner. That owner must keep it unchanged until the call settles, including cancellation; inspection
|
|
31
|
+
neither copies nor removes it. `verifyCompositableSurfaceBytes({ surface, bytes, ... })` remains a
|
|
32
|
+
convenience wrapper for callers that have bytes, creates its own temporary file, and removes that
|
|
33
|
+
file only after probing has settled. Both use the same decoded Surface checks and return no extra
|
|
34
|
+
result facts. Filesystem paths belong to this Node execution boundary, not to Surface or Blob refs.
|
|
35
|
+
|
|
29
36
|
Audio gain envelopes and audible subranges are evaluated per sample after tempo/looping and Clip
|
|
30
37
|
fades, before the requested range is cropped. Original source and envelope progress therefore survive
|
|
31
38
|
local silence and range rendering. This is generic AudioProgramPlan execution, independent of author
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "@hypit/protocol";
|
|
34
34
|
import type { BlobRef, CanonicalValue, StoredValue } from "@hypit/protocol";
|
|
35
35
|
|
|
36
|
+
import { mediaProcessEnv } from "./process-env.js";
|
|
36
37
|
import { parseMediaInspection } from "./probe.js";
|
|
37
38
|
import {
|
|
38
39
|
compositeAnimatedWebpFrame,
|
|
@@ -98,10 +99,7 @@ async function runProcess(args: {
|
|
|
98
99
|
shell: false,
|
|
99
100
|
windowsHide: true,
|
|
100
101
|
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
-
env: {
|
|
102
|
-
PATH: process.env.PATH ?? "",
|
|
103
|
-
...(args.sharedLibraryPath === undefined ? {} : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
|
|
104
|
-
},
|
|
102
|
+
env: mediaProcessEnv(args.sharedLibraryPath === undefined ? undefined : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
|
|
105
103
|
});
|
|
106
104
|
const stdout: Buffer[] = [];
|
|
107
105
|
let stdoutBytes = 0;
|
|
@@ -678,8 +676,10 @@ export async function executeNormalizeMedia(
|
|
|
678
676
|
"scale=trunc(iw*max(sar\\,1)/2)*2:trunc(ih*max(1/sar\\,1)/2)*2",
|
|
679
677
|
"setsar=1",
|
|
680
678
|
].join(",");
|
|
679
|
+
// FFmpeg enables autorotation by default. Its positive boolean option changed
|
|
680
|
+
// syntax across releases; no override is needed to materialize source rotation.
|
|
681
681
|
const visualInput = animation === undefined
|
|
682
|
-
? [...encoding.inputArgs, "-
|
|
682
|
+
? [...encoding.inputArgs, "-i", input, "-map", `0:${plan.video.index}`]
|
|
683
683
|
: ["-f", "concat", "-safe", "0", "-i", await animatedWebpConcat(env, animation, work), "-map", "0:v:0"];
|
|
684
684
|
// The execution format must retain the source's alpha while materializing
|
|
685
685
|
// the program clock. Both encodings publish the same SynchronizedMedia type.
|
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
executeTransformMedia,
|
|
11
11
|
} from "./execute.js";
|
|
12
12
|
export { parseMediaInspection } from "./probe.js";
|
|
13
|
-
export { verifyCompositableSurfaceBytes } from "./surface.js";
|
|
13
|
+
export { verifyCompositableSurfaceBytes, verifyCompositableSurfaceFile } from "./surface.js";
|
|
14
14
|
export { probeMediaToolchain } from "./toolchain.js";
|
|
15
15
|
export type { MediaToolchainState } from "./toolchain.js";
|
|
16
16
|
export type * from "./execute.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as const;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ffmpeg and ffprobe are started as PATH executables. The Host environment is
|
|
5
|
+
* not forwarded, so credential values stay out of those processes. Windows still
|
|
6
|
+
* needs the process-creation variables CreateProcess uses to resolve `.exe`.
|
|
7
|
+
*/
|
|
8
|
+
export function mediaProcessEnv(
|
|
9
|
+
extra?: Readonly<Record<string, string>>,
|
|
10
|
+
): NodeJS.ProcessEnv {
|
|
11
|
+
const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "" };
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
for (const name of windowsProcessCreation) {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
if (value !== undefined && value.length > 0) env[name] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (extra !== undefined) Object.assign(env, extra);
|
|
19
|
+
return env;
|
|
20
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { assertCompositableSurfaceRef } from "@hypit/media";
|
|
7
|
+
import { mediaProcessEnv } from "./process-env.js";
|
|
7
8
|
import type { CompositableSurfaceRef } from "@hypit/media";
|
|
8
9
|
|
|
9
10
|
type JsonObject = Record<string, unknown>;
|
|
@@ -57,7 +58,7 @@ async function runProcess(args: {
|
|
|
57
58
|
shell: false,
|
|
58
59
|
windowsHide: true,
|
|
59
60
|
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
-
env:
|
|
61
|
+
env: mediaProcessEnv(),
|
|
61
62
|
});
|
|
62
63
|
const stdout: Buffer[] = [];
|
|
63
64
|
let bytes = 0;
|
|
@@ -192,95 +193,101 @@ async function pixelFormatAlpha(
|
|
|
192
193
|
return result;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
if (mediaType === "image/png") return ".png";
|
|
197
|
-
if (mediaType === "image/webp") return ".webp";
|
|
198
|
-
if (mediaType === "video/webm") return ".webm";
|
|
199
|
-
if (mediaType === "video/mp4") return ".mp4";
|
|
200
|
-
throw new Error(`Surface media type ${mediaType} is unsupported`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Decode and verify one typed Surface before its bytes enter a renderer.
|
|
205
|
-
*
|
|
206
|
-
* This is an admission gate, not a second media Product. Success returns no
|
|
207
|
-
* side-channel facts; anything a later graph step needs must be an explicit output.
|
|
208
|
-
*/
|
|
209
|
-
export async function verifyCompositableSurfaceBytes(options: {
|
|
196
|
+
type SurfaceInspectionOptions = {
|
|
210
197
|
readonly surface: CompositableSurfaceRef;
|
|
211
|
-
readonly bytes: Uint8Array;
|
|
212
198
|
readonly ffprobePath?: string;
|
|
213
199
|
readonly processTimeoutMs?: number;
|
|
214
200
|
readonly maxProbeOutputBytes?: number;
|
|
215
201
|
readonly signal?: AbortSignal;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Inspect bytes supplied by callers that do not already own a staged file. */
|
|
205
|
+
export async function verifyCompositableSurfaceBytes(options: SurfaceInspectionOptions & {
|
|
206
|
+
readonly bytes: Uint8Array;
|
|
207
|
+
}): Promise<void> {
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
assertCompositableSurfaceRef(options.surface);
|
|
210
|
+
assert(options.bytes.byteLength === options.surface.artifact.size,
|
|
211
|
+
`Surface ${options.surface.artifact.resource} byte size differs`);
|
|
212
|
+
const directory = await mkdtemp(join(tmpdir(), "hypit-surface-verify-"));
|
|
213
|
+
try {
|
|
214
|
+
// FFprobe detects the encoded media itself; both entry points inspect the same formats.
|
|
215
|
+
const path = join(directory, "surface");
|
|
216
|
+
await writeFile(path, options.bytes, { signal: options.signal });
|
|
217
|
+
await verifyCompositableSurfaceFile({ ...options, path });
|
|
218
|
+
} finally {
|
|
219
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Inspect an existing execution file without copying it. The caller owns the file
|
|
225
|
+
* and keeps it unchanged until this call settles. Success returns no side-channel facts.
|
|
226
|
+
*/
|
|
227
|
+
export async function verifyCompositableSurfaceFile(options: SurfaceInspectionOptions & {
|
|
228
|
+
readonly path: string;
|
|
216
229
|
}): Promise<void> {
|
|
217
230
|
const controller = new AbortController();
|
|
218
231
|
const signal = options.signal === undefined ? controller.signal : AbortSignal.any([controller.signal, options.signal]);
|
|
219
232
|
signal.throwIfAborted();
|
|
220
233
|
assertCompositableSurfaceRef(options.surface);
|
|
221
|
-
assert(options.
|
|
234
|
+
assert((await stat(options.path)).size === options.surface.artifact.size,
|
|
222
235
|
`Surface ${options.surface.artifact.resource} byte size differs`);
|
|
236
|
+
signal.throwIfAborted();
|
|
223
237
|
const ffprobePath = options.ffprobePath ?? "ffprobe";
|
|
224
238
|
const timeoutMs = options.processTimeoutMs ?? 120_000;
|
|
225
239
|
const maxOutputBytes = options.maxProbeOutputBytes ?? 8 * 1024 * 1024;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
assert(rotation(stream) === 0, "Surface must not depend on display rotation metadata");
|
|
257
|
-
assert(stream.field_order === undefined || stream.field_order === "progressive" || stream.field_order === "unknown",
|
|
258
|
-
"Surface must be progressive");
|
|
259
|
-
assertSrgb(stream);
|
|
240
|
+
const pending = [
|
|
241
|
+
runProcess({
|
|
242
|
+
executable: ffprobePath,
|
|
243
|
+
argv: ["-v", "error", "-print_format", "json", "-show_streams", "-count_frames", options.path],
|
|
244
|
+
timeoutMs,
|
|
245
|
+
maxOutputBytes, signal,
|
|
246
|
+
}),
|
|
247
|
+
pixelFormatAlpha(ffprobePath, timeoutMs, maxOutputBytes, signal),
|
|
248
|
+
] as const;
|
|
249
|
+
await Promise.allSettled(pending.map(async (job) => {
|
|
250
|
+
try { return await job; } catch (error) { controller.abort(error); throw error; }
|
|
251
|
+
}));
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
const [probe, formats] = await Promise.all(pending);
|
|
254
|
+
const root = json(probe, "ffprobe Surface query");
|
|
255
|
+
assert(Array.isArray(root.streams) && root.streams.length === 1,
|
|
256
|
+
"Surface must contain exactly one visual stream and no audio or auxiliary streams");
|
|
257
|
+
const stream = root.streams[0] as ProbeStream;
|
|
258
|
+
assert(stream.codec_type === "video", "Surface stream is not visual");
|
|
259
|
+
assert(stream.disposition?.attached_pic !== 1, "Surface cannot be an attached-picture stream");
|
|
260
|
+
const width = positiveInteger(stream.width, "Surface width");
|
|
261
|
+
const height = positiveInteger(stream.height, "Surface height");
|
|
262
|
+
assert(width === options.surface.width && height === options.surface.height,
|
|
263
|
+
"Surface decoded dimensions differ from its declaration");
|
|
264
|
+
assert(stream.sample_aspect_ratio === undefined || stream.sample_aspect_ratio === "1:1",
|
|
265
|
+
"Surface must use square pixels");
|
|
266
|
+
assert(rotation(stream) === 0, "Surface must not depend on display rotation metadata");
|
|
267
|
+
assert(stream.field_order === undefined || stream.field_order === "progressive" || stream.field_order === "unknown",
|
|
268
|
+
"Surface must be progressive");
|
|
269
|
+
assertSrgb(stream);
|
|
260
270
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
assert(typeof stream.pix_fmt === "string" && stream.pix_fmt.length > 0,
|
|
272
|
+
"Surface pixel format is absent");
|
|
273
|
+
const pixelAlpha = formats.get(stream.pix_fmt);
|
|
274
|
+
assert(pixelAlpha !== undefined, `Surface pixel format ${stream.pix_fmt} is unknown to ffprobe`);
|
|
275
|
+
const taggedAlpha = stream.tags?.alpha_mode === "1" || stream.tags?.alpha_mode === "straight";
|
|
276
|
+
const hasAlpha = pixelAlpha || taggedAlpha;
|
|
277
|
+
if (options.surface.alphaMode === "straight") {
|
|
278
|
+
assert(hasAlpha, "Surface declares straight alpha but its bytes carry no alpha channel");
|
|
279
|
+
} else {
|
|
280
|
+
assert(!hasAlpha, "Surface declares opaque pixels but its encoded format carries alpha");
|
|
281
|
+
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
} finally {
|
|
284
|
-
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
283
|
+
const frameCount = positiveInteger(stream.nb_read_frames ?? stream.nb_frames, "Surface frame count");
|
|
284
|
+
if (options.surface.timing.kind === "still") {
|
|
285
|
+
assert(frameCount === 1, "Still Surface must decode to exactly one frame");
|
|
286
|
+
} else {
|
|
287
|
+
assert(frameCount === options.surface.timing.frameCount,
|
|
288
|
+
"Surface decoded frame count differs from its declaration");
|
|
289
|
+
const rate = rational(stream.avg_frame_rate ?? stream.r_frame_rate, "Surface frame rate");
|
|
290
|
+
assert(sameRational(rate, options.surface.timing.frameRate),
|
|
291
|
+
"Surface decoded frame rate differs from its declaration");
|
|
285
292
|
}
|
|
286
293
|
}
|
|
@@ -51,3 +51,13 @@ This is trusted execution, not a sandbox. Builtins, native libraries and process
|
|
|
51
51
|
(including `process.cwd()` and `process.env`) remain shared. Existing module bindings do not change
|
|
52
52
|
under an active Build; a file or dependency first
|
|
53
53
|
read later uses ordinary filesystem semantics. Scoped loading does not make a project tree immutable.
|
|
54
|
+
|
|
55
|
+
## Dependency installation options
|
|
56
|
+
|
|
57
|
+
A Distribution package may declare `hypit.dependencyInstallEnv` in its package.json, keyed by its
|
|
58
|
+
ordinary direct external dependency names. Each value is an environment-variable map passed to that
|
|
59
|
+
dependency's explicit npm installation. For example, an owner can disable an SDK's automatic asset
|
|
60
|
+
download and prepare that asset through its ManagedProgram instead. Options do not propagate to
|
|
61
|
+
unrelated installations or the caller's process. Conflicting values from selected packages fail;
|
|
62
|
+
no package wins by discovery order. They are command inputs, not a second dependency inventory,
|
|
63
|
+
readiness record, or new version system. Project packages remain owned by their package manager.
|
|
@@ -16,7 +16,7 @@ import { externalPackageRoots, locateNodePackage } from "./location.js";
|
|
|
16
16
|
type PackageJson = {
|
|
17
17
|
readonly name: string;
|
|
18
18
|
readonly version?: string;
|
|
19
|
-
readonly hypit?: { readonly activation?: string };
|
|
19
|
+
readonly hypit?: { readonly activation?: string; readonly dependencyInstallEnv?: Readonly<Record<string, Readonly<Record<string, string>>>> };
|
|
20
20
|
readonly dependencies: Readonly<Record<string, string>>;
|
|
21
21
|
};
|
|
22
22
|
|
|
@@ -43,7 +43,19 @@ function parsePackageJson(value: unknown, subject: string): PackageJson {
|
|
|
43
43
|
return {
|
|
44
44
|
name: text(item.name, `${subject}.name`),
|
|
45
45
|
...(item.version === undefined ? {} : { version: text(item.version, `${subject}.version`) }),
|
|
46
|
-
...(hypit
|
|
46
|
+
...(hypit === undefined ? {} : { hypit: {
|
|
47
|
+
...(hypit.activation === undefined ? {} : { activation: text(hypit.activation, `${subject}.hypit.activation`) }),
|
|
48
|
+
...(hypit.dependencyInstallEnv === undefined ? {} : { dependencyInstallEnv: Object.fromEntries(
|
|
49
|
+
Object.entries(object(hypit.dependencyInstallEnv, `${subject}.hypit.dependencyInstallEnv`)).map(([name, raw]) => {
|
|
50
|
+
assert(Object.hasOwn(dependencies, name) && !name.startsWith("@hypit/"), `${subject}: installation environment must name a direct external dependency: ${name}`);
|
|
51
|
+
const env = object(raw, `${subject}.hypit.dependencyInstallEnv.${name}`);
|
|
52
|
+
for (const [key, value] of Object.entries(env)) {
|
|
53
|
+
assert(/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) && typeof value === "string", `${subject}: invalid installation environment for ${name}`);
|
|
54
|
+
}
|
|
55
|
+
return [name, env as Record<string, string>];
|
|
56
|
+
}),
|
|
57
|
+
) }),
|
|
58
|
+
} }),
|
|
47
59
|
dependencies: Object.fromEntries(Object.entries(dependencies).map(([name, version]) => [
|
|
48
60
|
name,
|
|
49
61
|
text(version, `${subject}.dependencies.${name}`),
|
|
@@ -183,11 +195,11 @@ export class NodePackageSelectionMissingError extends Error {
|
|
|
183
195
|
export async function distributionExternalPackageRequirements(
|
|
184
196
|
specifiers: readonly string[],
|
|
185
197
|
distributionRoot: string,
|
|
186
|
-
): Promise<readonly { readonly name: string; readonly version: string; readonly specifier: string }[]> {
|
|
198
|
+
): Promise<readonly { readonly name: string; readonly version: string; readonly specifier: string; readonly env?: Readonly<Record<string, string>> }[]> {
|
|
187
199
|
const root = resolve(distributionRoot);
|
|
188
200
|
const queue = [...new Set(specifiers.filter((name) => name.startsWith("@hypit/")))].sort();
|
|
189
201
|
const visited = new Set<string>();
|
|
190
|
-
const external = new Map<string, { readonly name: string; readonly version: string; readonly specifier: string }>();
|
|
202
|
+
const external = new Map<string, { readonly name: string; readonly version: string; readonly specifier: string; readonly env?: Readonly<Record<string, string>> }>();
|
|
191
203
|
while (queue.length > 0) {
|
|
192
204
|
const name = queue.shift()!;
|
|
193
205
|
if (visited.has(name)) continue;
|
|
@@ -199,7 +211,15 @@ export async function distributionExternalPackageRequirements(
|
|
|
199
211
|
continue;
|
|
200
212
|
}
|
|
201
213
|
const specifier = `${dependency}@${version}`;
|
|
202
|
-
|
|
214
|
+
const requested = physical.json.hypit?.dependencyInstallEnv?.[dependency];
|
|
215
|
+
const previous = external.get(specifier);
|
|
216
|
+
const env = { ...previous?.env };
|
|
217
|
+
for (const [key, value] of Object.entries(requested ?? {})) {
|
|
218
|
+
assert(env[key] === undefined || env[key] === value, `Conflicting installation environment ${key} for ${specifier}`);
|
|
219
|
+
env[key] = value;
|
|
220
|
+
}
|
|
221
|
+
external.set(specifier, { name: dependency, version, specifier,
|
|
222
|
+
...(Object.keys(env).length === 0 ? {} : { env }) });
|
|
203
223
|
}
|
|
204
224
|
}
|
|
205
225
|
return [...external.values()].sort((left, right) => left.specifier.localeCompare(right.specifier));
|
|
@@ -2,6 +2,14 @@ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "n
|
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
/** Only absence is repairable by installing a package; malformed manifests and I/O errors are not. */
|
|
6
|
+
export class NodePackageNotFoundError extends Error {
|
|
7
|
+
constructor(name: string) {
|
|
8
|
+
super(`cannot locate installed package ${name}`);
|
|
9
|
+
this.name = "NodePackageNotFoundError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export type LocatedNodePackage = {
|
|
6
14
|
readonly root: string;
|
|
7
15
|
readonly manifest: {
|
|
@@ -88,7 +96,8 @@ export function declaredExternalPackageRoot(root: string, from: string | URL, na
|
|
|
88
96
|
// An upstream asset a package ships as optional is still selected by one exact version here.
|
|
89
97
|
// `hypit packages install` places it under the machine npm root named by this selection, so
|
|
90
98
|
// reading only the required map makes the documented repair unusable for every optional asset.
|
|
91
|
-
|
|
99
|
+
// npm lets optionalDependencies override dependencies with the same name.
|
|
100
|
+
const version = value.optionalDependencies?.[name] ?? value.dependencies?.[name];
|
|
92
101
|
return version === undefined ? undefined : externalPackageInstallRoot(root, name, version);
|
|
93
102
|
}
|
|
94
103
|
const parent = dirname(cursor);
|
|
@@ -223,7 +232,7 @@ export function locateNodePackage(nameValue: string, options: LocateNodePackageO
|
|
|
223
232
|
if (found !== undefined) return found;
|
|
224
233
|
}
|
|
225
234
|
}
|
|
226
|
-
throw new
|
|
235
|
+
throw new NodePackageNotFoundError(name);
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
function packageFile(
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Trusted local Provider for the `@hypit/render-hyperframes#render-visual` capability. It stages the
|
|
4
4
|
Resource dependencies declared by a `HyperframesDocument`, renders a silent MP4 with the
|
|
5
|
-
HyperFrames engine, probes the bytes, and returns a verified `RenderedVisual`. Before
|
|
5
|
+
HyperFrames engine, probes the bytes, and returns a verified `RenderedVisual`. Before capturing a typed
|
|
6
6
|
Surface it decodes the exact bytes and checks declared dimensions, still/frame timing, SDR/sRGB and
|
|
7
7
|
opaque/straight-alpha facts. These checks validate the typed rendering input; they do not create
|
|
8
8
|
content identity or hidden output metadata.
|
|
@@ -64,12 +64,67 @@ GPU, or `auto` to let the engine decide. Capture uses screenshots and independen
|
|
|
64
64
|
the CLI's automatic worker and drawElement policies do not override the count. The opaque screenshot
|
|
65
65
|
adapter uses the engine's public session, video injector and page seek protocol plus Chrome CDP.
|
|
66
66
|
It waits for seek completion, dynamic images/fonts and the page compositor before capturing PNGs.
|
|
67
|
+
Image readiness includes CSS class and pseudo-element images, CSS masks and SVG images. A failed
|
|
68
|
+
image decode reports its URL instead of producing a successful frame with missing media. Failed
|
|
69
|
+
declared fonts also fail capture instead of silently leaving fallback glyphs in the output.
|
|
67
70
|
The pinned engine couples its PNG session setup to transparent export, so this adapter initializes
|
|
68
71
|
an opaque session and chooses PNG separately at capture. It does not patch engine methods or files.
|
|
69
72
|
|
|
70
|
-
The Runtime Adapter
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
The Runtime Adapter declares one managed browser program. Prepare it explicitly:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
hypit programs up --runtime ./hypit.runtime.json --endpoint hyperframes.local
|
|
77
|
+
hypit doctor --runtime ./hypit.runtime.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Use the instance name from the Profile. `runtime up` also prepares it and starts the Runtime Worker.
|
|
81
|
+
The Provider's `package.json` declares its recommended Chrome Headless Shell version in
|
|
82
|
+
`hypit.renderBrowser.version`, alongside the engine dependency used to test that release. The
|
|
83
|
+
installer consumes that declaration; it does not have a browser version constant or fetch a
|
|
84
|
+
"latest" channel. `config.browserVersion` explicitly selects another exact four-part version.
|
|
85
|
+
Changing the recommendation belongs to a Provider release and requires real rendering tests; a
|
|
86
|
+
Puppeteer recommendation alone is not a HyperFrames compatibility guarantee.
|
|
87
|
+
|
|
88
|
+
The default cache is `~/.cache/hyperframes/chrome`; `config.browserCacheDirectory` selects another
|
|
89
|
+
location. Only the selected version is used, regardless of other cached or system browsers.
|
|
90
|
+
Projects sharing this cache use the existing ManagedProgram preparation lock/logs under
|
|
91
|
+
`.hypit-render-program`. Readiness comes from the executable and its reported version, not a receipt.
|
|
92
|
+
Explicit preparation reuses a healthy installation or repairs only the selected managed version.
|
|
93
|
+
A failed download reports failure without selecting another version or browser.
|
|
94
|
+
|
|
95
|
+
`config.browserDownloadBaseUrl` selects a Chrome for Testing archive mirror for explicit preparation.
|
|
96
|
+
It is an absolute HTTP(S) base URL, without credentials, a query or a fragment. The browser library
|
|
97
|
+
appends the selected version, platform and archive name; the mirror must serve that same layout.
|
|
98
|
+
For example, a base of `https://mirror.example/chrome-for-testing` serves archives beneath
|
|
99
|
+
`<base>/<version>/<platform>/chrome-headless-shell-<platform>.zip`. Choose a source maintained by
|
|
100
|
+
the user or organization; this Provider does not keep a mirror list or choose one by region.
|
|
101
|
+
Omitting the setting uses the browser library's official Chrome for Testing source. A configured
|
|
102
|
+
source replaces it: failed transfers or invalid archives fail preparation without trying the official
|
|
103
|
+
source or another mirror. Normal HTTP redirects supplied by the selected server are handled by the
|
|
104
|
+
download library. Preparation displays the complete archive URL, version and destination before
|
|
105
|
+
download. An npm registry setting does not redirect this binary download.
|
|
106
|
+
|
|
107
|
+
The download source only determines where missing installation bytes come from. It does not change
|
|
108
|
+
the executable selection, and changing it does not invalidate a healthy cached version. No source
|
|
109
|
+
receipt is stored. To install into an empty location, explicitly choose `browserCacheDirectory`.
|
|
110
|
+
|
|
111
|
+
`config.chromePath` selects a user-managed Chrome/Chromium executable. It cannot be combined with
|
|
112
|
+
`browserVersion` or `browserDownloadBaseUrl`; invalid combinations fail instead of assigning precedence. Relative paths resolve
|
|
113
|
+
from the Runtime Profile root. This mode never downloads or repairs a browser. Its version remains
|
|
114
|
+
under the user's control, including system-browser auto-updates. `HYPERFRAMES_BROWSER_PATH` and
|
|
115
|
+
`PRODUCER_HEADLESS_SHELL_PATH` do not select browsers in this Provider; configure `chromePath`.
|
|
116
|
+
On platforms without a supported managed download, explicitly select an installed browser.
|
|
117
|
+
|
|
118
|
+
`doctor` displays the selected path and its source, and only inspects it. Build preflight, rendering
|
|
119
|
+
and previews never install a browser. `programs up` / `runtime up` display the selected managed
|
|
120
|
+
version, installation location and download URL before running preparation. The probe runs `--version` and checks
|
|
121
|
+
FFmpeg/FFprobe; it does not promise GPU or page compatibility. Capture receives that same selected
|
|
122
|
+
path as the engine's `chromePath`, including its GPU probe. Active Workers keep their loaded package
|
|
123
|
+
recommendation; restart them explicitly after changing Profile or package dependencies.
|
|
124
|
+
|
|
125
|
+
The Provider's `hypit.dependencyInstallEnv` disables Puppeteer's browser download while preparing its
|
|
126
|
+
engine/producer npm dependencies. The repository `.puppeteerrc.cjs` does the same for checkout installs.
|
|
127
|
+
No browser postinstall allowlist is required. The former `hyperframesCliPath` option is removed.
|
|
73
128
|
|
|
74
129
|
The same executor is exported for callers with an already compiled document:
|
|
75
130
|
|
|
@@ -101,6 +156,17 @@ flushes that message and disconnects IPC so it can exit normally. The owner awai
|
|
|
101
156
|
diagnostics before returning. Normal completion does not enumerate or forcibly terminate processes.
|
|
102
157
|
On failure, cleanup may be incomplete: the child reports the error and keeps IPC open while the owner
|
|
103
158
|
discovers and terminates the remaining process tree, before it can become orphaned.
|
|
159
|
+
The capture child also installs synchronous exit cleanup before loading the engine. Early
|
|
160
|
+
`process.exit()`, uncaught exceptions, and catchable `SIGINT`/`SIGTERM` exits stop descendants
|
|
161
|
+
while their owner still exists, even if browser initialization never returned a session.
|
|
162
|
+
Successful resource closure removes this exit handler. No historical browser PID list is retained.
|
|
163
|
+
|
|
164
|
+
Uncatchable termination (`SIGKILL`, native crashes, or OS termination on Windows) cannot run that
|
|
165
|
+
cleanup. The owner reports the termination and cannot confirm descendant cleanup; it never searches
|
|
166
|
+
by an already-exited root PID. If an orphan keeps the output pipes open, the owner closes its pipe
|
|
167
|
+
ends after five seconds so the failed invocation can settle. Guaranteed cleanup after an OS hard
|
|
168
|
+
kill requires containment supplied by that deployment (for example a process job or container);
|
|
169
|
+
this local Node implementation does not provide that guarantee.
|
|
104
170
|
|
|
105
171
|
At cancellation the child receives a stop request and has up to five seconds to clean up. A child
|
|
106
172
|
that remains after cancellation or its completion message is forcibly terminated along with its discovered
|
|
@@ -118,7 +184,10 @@ more of the shared render budget on a slow initialization or frame.
|
|
|
118
184
|
An explicit stage-timeout error names the worker and stage/frame, aborts sibling workers and awaits
|
|
119
185
|
cleanup. A completed worker closes its Chrome immediately.
|
|
120
186
|
|
|
121
|
-
One call stages the HTML and every declared asset once. Typed Surface
|
|
187
|
+
One call stages the HTML and every declared asset once. Typed Surface inspection reads the completed
|
|
188
|
+
staged file directly, without retaining its chunks, assembling another whole-file buffer, or writing
|
|
189
|
+
a second temporary copy. The caller keeps that file until inspection and capture have settled.
|
|
190
|
+
Typed Surface validation includes a complete
|
|
122
191
|
decoded-frame count, even for a short render interval. The renderer then finds source-frame windows
|
|
123
192
|
needed by that interval, merges overlapping windows, and extracts them one source/window at a time.
|
|
124
193
|
Decoded PNGs are shared by all workers in this call. Each worker initializes its own page, then takes
|
|
@@ -134,9 +203,13 @@ do not share staged files or decoded PNGs. Exact compiler sampling markers retai
|
|
|
134
203
|
fractional playback rates. There is no SVML rewrite, intermediate cut MP4 or repeat normalization.
|
|
135
204
|
Already compiled video documents need the current compiler's frame markers.
|
|
136
205
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
206
|
+
`ffmpegPath` selects both source decoding and final H.264 encoding; `ffprobePath` selects source,
|
|
207
|
+
output and typed Surface inspection. Bare commands resolve through PATH. The capture child passes
|
|
208
|
+
these selected executables to the engine's public binary overrides; inherited
|
|
209
|
+
`HYPERFRAMES_FFMPEG_PATH` / `HYPERFRAMES_FFPROBE_PATH` cannot select a different decoding toolchain.
|
|
210
|
+
`nodePath` selects the managed
|
|
211
|
+
browser installer's Node executable. Capture uses the current Node process's executable.
|
|
212
|
+
The requested frame range travels in the Model's Need; browser paths stay in the Provider.
|
|
140
213
|
|
|
141
214
|
```ts
|
|
142
215
|
import { createLocalHyperframesProvider } from "@hypit/provider-hyperframes-local";
|
|
@@ -8,7 +8,18 @@
|
|
|
8
8
|
".": "./src/index.ts"
|
|
9
9
|
},
|
|
10
10
|
"hypit": {
|
|
11
|
-
"activation": "./src/activation.ts"
|
|
11
|
+
"activation": "./src/activation.ts",
|
|
12
|
+
"renderBrowser": {
|
|
13
|
+
"version": "152.0.7928.2"
|
|
14
|
+
},
|
|
15
|
+
"dependencyInstallEnv": {
|
|
16
|
+
"@hyperframes/engine": {
|
|
17
|
+
"PUPPETEER_SKIP_DOWNLOAD": "true"
|
|
18
|
+
},
|
|
19
|
+
"@hyperframes/producer": {
|
|
20
|
+
"PUPPETEER_SKIP_DOWNLOAD": "true"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
12
23
|
},
|
|
13
24
|
"dependencies": {
|
|
14
25
|
"@hyperframes/engine": "0.7.101",
|
|
@@ -17,13 +28,12 @@
|
|
|
17
28
|
"@hypit/hyperframes": "workspace:*",
|
|
18
29
|
"@hypit/media": "workspace:*",
|
|
19
30
|
"@hypit/media-execution": "workspace:*",
|
|
20
|
-
"@hypit/package-loader-node": "workspace:*",
|
|
21
31
|
"@hypit/protocol": "workspace:*",
|
|
22
32
|
"@hypit/render-hyperframes": "workspace:*",
|
|
23
33
|
"@hypit/runtime": "workspace:*",
|
|
24
34
|
"@hypit/runtime-host-node": "workspace:*",
|
|
25
35
|
"@hypit/runtime-kit": "workspace:*",
|
|
26
|
-
"
|
|
36
|
+
"@puppeteer/browsers": "3.2.2",
|
|
27
37
|
"tsx": "4.21.0"
|
|
28
38
|
},
|
|
29
39
|
"devDependencies": {
|