1tube 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/deno.json CHANGED
@@ -16,6 +16,7 @@
16
16
  "@std/assert": "jsr:@std/assert@^1",
17
17
  "hono": "npm:hono@4",
18
18
  "esbuild": "npm:esbuild@^0.28",
19
+ "es-module-lexer": "npm:es-module-lexer@^2",
19
20
  "@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.2",
20
21
  "@zip-js/zip-js": "jsr:@zip-js/zip-js@^2.8"
21
22
  },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "private": false,
5
5
  "description": "Self-hosted Supabase Edge Functions gateway for Deno, plus an ESLint plugin for consumers",
6
6
  "license": "MIT",
@@ -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 {
@@ -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("HMAC", cryptoKey, message as BufferSource);
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 (typeof o.functionCount !== "number" || typeof o.totalBundleBytes !== "number") {
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: {
@@ -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 namePart = e.name ? ` ${truncateMiddle(e.name, 40)}` : "";
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
+ }