1tube 0.1.12 → 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 +3 -0
- package/deno.json +1 -0
- package/deno.lock +5 -0
- package/package.json +1 -1
- package/scripts/npm-cli.js +4 -1
- package/src/backends/workerd/bundler.ts +6 -0
- package/src/cli/envelope.ts +14 -2
- package/src/cli/package.ts +16 -2
- package/src/firmware-content-hash.ts +192 -0
package/README.md
CHANGED
|
@@ -44,6 +44,9 @@ The published npm package exposes a `1tube` binary. It is a tiny Node shim that
|
|
|
44
44
|
launches this repo's Deno CLI, so machines using it need Deno on `PATH`.
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
+
# Show CLI help
|
|
48
|
+
npx 1tube
|
|
49
|
+
|
|
47
50
|
# Build and sign a firmware payload in one command
|
|
48
51
|
npx 1tube package --functions supabase/functions --out fw.1tube --sign-key "$1TUBE_PACKAGE_SIGN_KEY"
|
|
49
52
|
|
package/deno.json
CHANGED
package/deno.lock
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"npm:@google/genai@1.39.0": "1.39.0",
|
|
15
15
|
"npm:@supabase/supabase-js@2": "2.93.3",
|
|
16
16
|
"npm:ai@6": "6.0.67_zod@3.25.76",
|
|
17
|
+
"npm:es-module-lexer@2": "2.0.0",
|
|
17
18
|
"npm:esbuild@0.28": "0.28.0",
|
|
18
19
|
"npm:esbuild@~0.25.5": "0.25.12",
|
|
19
20
|
"npm:hono@4": "4.12.14",
|
|
@@ -514,6 +515,9 @@
|
|
|
514
515
|
"safe-buffer"
|
|
515
516
|
]
|
|
516
517
|
},
|
|
518
|
+
"es-module-lexer@2.0.0": {
|
|
519
|
+
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="
|
|
520
|
+
},
|
|
517
521
|
"esbuild@0.25.12": {
|
|
518
522
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
|
519
523
|
"optionalDependencies": [
|
|
@@ -860,6 +864,7 @@
|
|
|
860
864
|
"jsr:@std/assert@1",
|
|
861
865
|
"jsr:@std/path@1",
|
|
862
866
|
"jsr:@zip-js/zip-js@^2.8.0",
|
|
867
|
+
"npm:es-module-lexer@2",
|
|
863
868
|
"npm:esbuild@0.28",
|
|
864
869
|
"npm:hono@4"
|
|
865
870
|
],
|
package/package.json
CHANGED
package/scripts/npm-cli.js
CHANGED
|
@@ -9,16 +9,19 @@ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
9
9
|
const denoConfig = resolve(packageRoot, "deno.json");
|
|
10
10
|
const cliEntrypoint = resolve(packageRoot, "src", "cli.ts");
|
|
11
11
|
const denoBin = process.env.DENO_BIN || "deno";
|
|
12
|
+
const cliArgs = process.argv.slice(2);
|
|
13
|
+
const forwardedArgs = cliArgs.length === 0 ? ["--help"] : cliArgs;
|
|
12
14
|
|
|
13
15
|
const child = spawn(
|
|
14
16
|
denoBin,
|
|
15
17
|
[
|
|
16
18
|
"run",
|
|
19
|
+
"--quiet",
|
|
17
20
|
"-A",
|
|
18
21
|
"--config",
|
|
19
22
|
denoConfig,
|
|
20
23
|
cliEntrypoint,
|
|
21
|
-
...
|
|
24
|
+
...forwardedArgs,
|
|
22
25
|
],
|
|
23
26
|
{
|
|
24
27
|
stdio: "inherit",
|
|
@@ -290,6 +290,12 @@ export interface SharedBundleResult extends SharedRuntimeModule {
|
|
|
290
290
|
|
|
291
291
|
export async function disposeBundlerResources(): Promise<void> {
|
|
292
292
|
await esbuild.stop();
|
|
293
|
+
// esbuild.stop() tears down the service process synchronously from
|
|
294
|
+
// esbuild's point of view, but Deno's node:child_process shim settles
|
|
295
|
+
// the underlying spawn wait op on the next turn. Let that completion
|
|
296
|
+
// run before sanitizer-enabled tests assert that no child process
|
|
297
|
+
// resources remain.
|
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
293
299
|
}
|
|
294
300
|
|
|
295
301
|
function sharedModuleStubSource(module: WorkerdSharedModuleInput): string {
|
package/src/cli/envelope.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface FirmwareEnvelope {
|
|
|
53
53
|
createdBy: string;
|
|
54
54
|
/** SHA-256 of `dist/manifest.json` bytes, lowercase hex. */
|
|
55
55
|
manifestSha256: string;
|
|
56
|
+
/** Stable runtime-content fingerprint; excludes package/build metadata. */
|
|
57
|
+
contentSha256?: string;
|
|
56
58
|
/** Number of functions in the inner manifest. Diagnostic. */
|
|
57
59
|
functionCount: number;
|
|
58
60
|
/** Sum of every bundle's byte length. Diagnostic. */
|
|
@@ -163,7 +165,11 @@ export async function signEnvelope(
|
|
|
163
165
|
false,
|
|
164
166
|
["sign"],
|
|
165
167
|
);
|
|
166
|
-
const sig = await crypto.subtle.sign(
|
|
168
|
+
const sig = await crypto.subtle.sign(
|
|
169
|
+
"HMAC",
|
|
170
|
+
cryptoKey,
|
|
171
|
+
message as BufferSource,
|
|
172
|
+
);
|
|
167
173
|
return {
|
|
168
174
|
...unsigned,
|
|
169
175
|
signature: {
|
|
@@ -252,7 +258,10 @@ export function parseEnvelope(raw: unknown): FirmwareEnvelope {
|
|
|
252
258
|
throw new Error(`envelope.${k} missing or not a string`);
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
|
-
if (
|
|
261
|
+
if (
|
|
262
|
+
typeof o.functionCount !== "number" ||
|
|
263
|
+
typeof o.totalBundleBytes !== "number"
|
|
264
|
+
) {
|
|
256
265
|
throw new Error("envelope.functionCount / .totalBundleBytes missing");
|
|
257
266
|
}
|
|
258
267
|
return {
|
|
@@ -261,6 +270,9 @@ export function parseEnvelope(raw: unknown): FirmwareEnvelope {
|
|
|
261
270
|
createdAt: o.createdAt as string,
|
|
262
271
|
createdBy: o.createdBy as string,
|
|
263
272
|
manifestSha256: o.manifestSha256 as string,
|
|
273
|
+
...(typeof o.contentSha256 === "string" && o.contentSha256.length > 0
|
|
274
|
+
? { contentSha256: o.contentSha256 }
|
|
275
|
+
: {}),
|
|
264
276
|
functionCount: o.functionCount as number,
|
|
265
277
|
totalBundleBytes: o.totalBundleBytes as number,
|
|
266
278
|
signature: {
|
package/src/cli/package.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
parsePrebuiltManifest,
|
|
54
54
|
type PrebuiltManifest,
|
|
55
55
|
} from "../backends/workerd/prebuilt.ts";
|
|
56
|
+
import { computeFirmwareContentFingerprint } from "../firmware-content-hash.ts";
|
|
56
57
|
import { VERSION } from "../version.ts";
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -326,6 +327,10 @@ export async function packageDist(
|
|
|
326
327
|
const files = await collectDistFiles(distDir, manifest, opts.onProgress);
|
|
327
328
|
const manifestEntry = files.find((f) => f.zipPath === "dist/manifest.json")!;
|
|
328
329
|
const manifestSha256 = await sha256Hex(manifestEntry.bytes);
|
|
330
|
+
const contentFingerprint = await computeFirmwareContentFingerprint(
|
|
331
|
+
distDir,
|
|
332
|
+
manifest,
|
|
333
|
+
);
|
|
329
334
|
|
|
330
335
|
const totalBundleBytes = manifest.functions.reduce(
|
|
331
336
|
(acc, f) => acc + f.bundleBytes,
|
|
@@ -341,6 +346,7 @@ export async function packageDist(
|
|
|
341
346
|
createdAt,
|
|
342
347
|
createdBy: `1tube@${VERSION}`,
|
|
343
348
|
manifestSha256,
|
|
349
|
+
contentSha256: contentFingerprint.sha256,
|
|
344
350
|
functionCount: manifest.functions.length,
|
|
345
351
|
totalBundleBytes,
|
|
346
352
|
};
|
|
@@ -499,7 +505,12 @@ function makeProgressRenderer(): (e: PackageProgress) => void {
|
|
|
499
505
|
: e.phase === "compress"
|
|
500
506
|
? "zipping"
|
|
501
507
|
: "wrote";
|
|
502
|
-
const
|
|
508
|
+
const displayName = e.phase === "finalize"
|
|
509
|
+
? e.name
|
|
510
|
+
: e.name
|
|
511
|
+
? truncateMiddle(e.name, 40)
|
|
512
|
+
: "";
|
|
513
|
+
const namePart = displayName ? ` ${displayName}` : "";
|
|
503
514
|
const line = `[1tube package] ${tag} ${e.current}/${e.total}` +
|
|
504
515
|
(pct ? ` (${pct})` : "") +
|
|
505
516
|
` ${sizeStr}${namePart}`;
|
|
@@ -675,7 +686,10 @@ export async function runPackage(args: string[]): Promise<number> {
|
|
|
675
686
|
console.log(
|
|
676
687
|
`[1tube package] envelope: ${result.envelope.functionCount} function(s), ` +
|
|
677
688
|
`${fmt(result.envelope.totalBundleBytes)} bundle bytes, ` +
|
|
678
|
-
`manifest sha256=${result.envelope.manifestSha256.slice(0, 12)}
|
|
689
|
+
`manifest sha256=${result.envelope.manifestSha256.slice(0, 12)}…, ` +
|
|
690
|
+
`content sha256=${
|
|
691
|
+
result.envelope.contentSha256?.slice(0, 12) ?? "<none>"
|
|
692
|
+
}…`,
|
|
679
693
|
);
|
|
680
694
|
return 0;
|
|
681
695
|
} catch (err) {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { dirname, join, normalize } from "node:path";
|
|
2
|
+
import { init, parse } from "es-module-lexer";
|
|
3
|
+
import { canonicalJson } from "./cli/envelope.ts";
|
|
4
|
+
import type { PrebuiltManifest } from "./backends/workerd/prebuilt.ts";
|
|
5
|
+
|
|
6
|
+
async function sha256Hex(bytes: Uint8Array | string): Promise<string> {
|
|
7
|
+
const data = typeof bytes === "string"
|
|
8
|
+
? new TextEncoder().encode(bytes)
|
|
9
|
+
: bytes;
|
|
10
|
+
const buf = await crypto.subtle.digest("SHA-256", data as BufferSource);
|
|
11
|
+
const view = new Uint8Array(buf);
|
|
12
|
+
let out = "";
|
|
13
|
+
for (let i = 0; i < view.length; i++) {
|
|
14
|
+
out += view[i].toString(16).padStart(2, "0");
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function toPosix(path: string): string {
|
|
20
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function uniqueSorted(values: Iterable<string>): string[] {
|
|
24
|
+
return [...new Set(values)].sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sortPlain(value: unknown): unknown {
|
|
28
|
+
if (Array.isArray(value)) return value.map(sortPlain);
|
|
29
|
+
if (!value || typeof value !== "object") return value;
|
|
30
|
+
const obj = value as Record<string, unknown>;
|
|
31
|
+
const out: Record<string, unknown> = {};
|
|
32
|
+
for (const key of Object.keys(obj).sort()) {
|
|
33
|
+
out[key] = sortPlain(obj[key]);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveModuleSpecifier(
|
|
39
|
+
fromFile: string,
|
|
40
|
+
specifier: string,
|
|
41
|
+
): string | null {
|
|
42
|
+
if (!specifier.startsWith(".")) return null;
|
|
43
|
+
const resolved = normalize(join(dirname(fromFile), specifier));
|
|
44
|
+
return toPosix(resolved);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function normalizeModule(
|
|
48
|
+
moduleFile: string,
|
|
49
|
+
source: string,
|
|
50
|
+
knownModules: ReadonlySet<string>,
|
|
51
|
+
): Promise<{ source: string; dependencies: string[] }> {
|
|
52
|
+
const dependencies: string[] = [];
|
|
53
|
+
const withoutSourceMap = source.replace(
|
|
54
|
+
/\n?\/\/# sourceMappingURL=.*$/m,
|
|
55
|
+
"\n",
|
|
56
|
+
);
|
|
57
|
+
await init;
|
|
58
|
+
const [imports] = parse(withoutSourceMap);
|
|
59
|
+
const patches: Array<{ start: number; end: number; value: string }> = [];
|
|
60
|
+
for (const specifier of imports) {
|
|
61
|
+
const rawSpecifier = specifier.n ??
|
|
62
|
+
withoutSourceMap.slice(specifier.s, specifier.e);
|
|
63
|
+
if (!rawSpecifier.startsWith(".")) continue;
|
|
64
|
+
const target = resolveModuleSpecifier(moduleFile, rawSpecifier);
|
|
65
|
+
if (!target || !knownModules.has(target)) continue;
|
|
66
|
+
|
|
67
|
+
dependencies.push(target);
|
|
68
|
+
patches.push({
|
|
69
|
+
start: specifier.s,
|
|
70
|
+
end: specifier.e,
|
|
71
|
+
value: "__1tube_module_ref__",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let normalized = withoutSourceMap;
|
|
76
|
+
for (const patch of patches.sort((a, b) => b.start - a.start)) {
|
|
77
|
+
normalized = normalized.slice(0, patch.start) + patch.value +
|
|
78
|
+
normalized.slice(patch.end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { source: normalized, dependencies: uniqueSorted(dependencies) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface FirmwareContentFingerprint {
|
|
85
|
+
sha256: string;
|
|
86
|
+
canonical: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function computeFirmwareContentFingerprint(
|
|
90
|
+
distDir: string,
|
|
91
|
+
manifest: PrebuiltManifest,
|
|
92
|
+
): Promise<FirmwareContentFingerprint> {
|
|
93
|
+
const moduleFiles = uniqueSorted([
|
|
94
|
+
...manifest.functions.flatMap((fn) =>
|
|
95
|
+
fn.moduleFiles?.length ? fn.moduleFiles : [fn.bundleFile]
|
|
96
|
+
),
|
|
97
|
+
...manifest.chunks.map((chunk) => chunk.file),
|
|
98
|
+
...manifest.sharedModules.map((shared) => shared.bundleFile),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const rawSources = new Map<string, string>();
|
|
102
|
+
for (const file of moduleFiles) {
|
|
103
|
+
const bytes = await Deno.readFile(join(distDir, file));
|
|
104
|
+
const source = new TextDecoder().decode(bytes);
|
|
105
|
+
rawSources.set(file, source);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const knownModules = new Set(moduleFiles);
|
|
109
|
+
const normalizedModules = new Map(
|
|
110
|
+
await Promise.all(
|
|
111
|
+
moduleFiles.map(async (file) =>
|
|
112
|
+
[
|
|
113
|
+
file,
|
|
114
|
+
await normalizeModule(file, rawSources.get(file) ?? "", knownModules),
|
|
115
|
+
] as const
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
const moduleHashes = new Map<string, string>();
|
|
120
|
+
async function hashModule(
|
|
121
|
+
file: string,
|
|
122
|
+
stack = new Set<string>(),
|
|
123
|
+
): Promise<string> {
|
|
124
|
+
const cached = moduleHashes.get(file);
|
|
125
|
+
if (cached) return cached;
|
|
126
|
+
|
|
127
|
+
const normalized = normalizedModules.get(file);
|
|
128
|
+
if (!normalized) {
|
|
129
|
+
throw new Error(`firmware content fingerprint missing module ${file}`);
|
|
130
|
+
}
|
|
131
|
+
if (stack.has(file)) {
|
|
132
|
+
return await sha256Hex("__1tube_module_cycle__");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
stack.add(file);
|
|
136
|
+
const dependencyHashes = [];
|
|
137
|
+
for (const dependency of normalized.dependencies) {
|
|
138
|
+
dependencyHashes.push(await hashModule(dependency, stack));
|
|
139
|
+
}
|
|
140
|
+
stack.delete(file);
|
|
141
|
+
|
|
142
|
+
const hash = await sha256Hex(canonicalJson({
|
|
143
|
+
source: normalized.source,
|
|
144
|
+
dependencies: uniqueSorted(dependencyHashes),
|
|
145
|
+
}));
|
|
146
|
+
moduleHashes.set(file, hash);
|
|
147
|
+
return hash;
|
|
148
|
+
}
|
|
149
|
+
for (const file of moduleFiles) {
|
|
150
|
+
await hashModule(file);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const functionEntries = manifest.functions
|
|
154
|
+
.map((fn) => {
|
|
155
|
+
const modules = fn.moduleFiles?.length ? fn.moduleFiles : [fn.bundleFile];
|
|
156
|
+
return {
|
|
157
|
+
name: fn.name,
|
|
158
|
+
manifest: sortPlain(fn.manifest),
|
|
159
|
+
modules: uniqueSorted(
|
|
160
|
+
modules.map((file) => moduleHashes.get(file) ?? ""),
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
165
|
+
|
|
166
|
+
const sharedEntries = manifest.sharedModules
|
|
167
|
+
.map((shared) => ({
|
|
168
|
+
id: shared.id,
|
|
169
|
+
exportNames: uniqueSorted(shared.exportNames),
|
|
170
|
+
module: moduleHashes.get(shared.bundleFile) ?? "",
|
|
171
|
+
}))
|
|
172
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
173
|
+
|
|
174
|
+
const chunkEntries = uniqueSorted(
|
|
175
|
+
manifest.chunks.map((chunk) => moduleHashes.get(chunk.file) ?? ""),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const canonical = canonicalJson({
|
|
179
|
+
schema: manifest.schema,
|
|
180
|
+
compatibilityDate: manifest.compatibilityDate,
|
|
181
|
+
compatibilityFlags: uniqueSorted(manifest.compatibilityFlags ?? []),
|
|
182
|
+
envAllowlist: uniqueSorted(manifest.envAllowlist ?? []),
|
|
183
|
+
chunks: chunkEntries,
|
|
184
|
+
sharedModules: sharedEntries,
|
|
185
|
+
functions: functionEntries,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
sha256: await sha256Hex(canonical),
|
|
190
|
+
canonical,
|
|
191
|
+
};
|
|
192
|
+
}
|