1tube 0.1.14 → 0.1.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1tube",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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",
@@ -639,6 +639,15 @@ export function resolveEnvAllowlist(
639
639
  return { resolved, missing, mode: "restricted" };
640
640
  }
641
641
 
642
+ function runtimeSecretNames(envSource: EnvSource = Deno.env): string[] {
643
+ const raw = envSource.get("1TUBE_SECRET_NAMES") ?? "";
644
+ const names = raw
645
+ .split(",")
646
+ .map((s) => s.trim())
647
+ .filter((s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s));
648
+ return [...new Set(names)].sort();
649
+ }
650
+
642
651
  /**
643
652
  * Compute the env vars exposed to a single function as workerd
644
653
  * `fromEnvironment` bindings.
@@ -1121,16 +1130,22 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1121
1130
  }
1122
1131
  }
1123
1132
 
1124
- // Resolve the gateway-wide env allowlist. This is the *maximum*
1125
- // surface any single function can ask for; per-function manifests
1126
- // narrow it further via `permissions.env`. Most-restrictive wins
1127
- // — operator declares what's available, the function declares
1128
- // what it actually uses, and we hand workerd the intersection.
1133
+ // Resolve the gateway-wide env policy. Workerd gets live secrets
1134
+ // plus the operator's explicit allowlist. We deliberately do not
1135
+ // infer source usage here: dynamic access is common, and secrets
1136
+ // are runtime configuration.
1137
+ const hasConfiguredEnvPolicy = (opts.envAllowlist?.length ?? 0) > 0 ||
1138
+ ((Deno.env.get("1TUBE_WORKERD_ENV") ?? "").trim().length > 0);
1139
+ const secretNames = runtimeSecretNames();
1129
1140
  const {
1130
- resolved: envBindings,
1141
+ resolved: configuredEnvBindings,
1131
1142
  missing: missingEnv,
1132
- mode: envMode,
1143
+ mode: configuredEnvMode,
1133
1144
  } = resolveEnvAllowlist(opts.envAllowlist);
1145
+ const gatewayEnvBindings = [...new Set([
1146
+ ...secretNames,
1147
+ ...(hasConfiguredEnvPolicy ? configuredEnvBindings : []),
1148
+ ])].sort();
1134
1149
  if (missingEnv.length > 0) {
1135
1150
  console.warn(
1136
1151
  `[1tube] workerd env allowlist references vars not present in the gateway env: ` +
@@ -1138,17 +1153,20 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1138
1153
  `Set them before starting the gateway, or remove them from the allowlist.`,
1139
1154
  );
1140
1155
  }
1141
- if (envBindings.length > 0) {
1142
- if (envMode === "all") {
1156
+ if (hasConfiguredEnvPolicy && configuredEnvBindings.length > 0) {
1157
+ if (configuredEnvMode === "all") {
1143
1158
  console.log(
1144
- `[1tube] forwarding all ${envBindings.length} env var(s) to workerd ` +
1145
- `(default; pass --workerd-env=A,B,C or set 1TUBE_WORKERD_ENV to restrict)`,
1159
+ `[1tube] forwarding all ${configuredEnvBindings.length} env var(s) to workerd ` +
1160
+ `plus ${secretNames.length} runtime secret name(s) (explicit '*')`,
1146
1161
  );
1147
1162
  } else {
1148
1163
  console.log(
1149
- `[1tube] forwarding ${envBindings.length} env var(s) to workerd: ${envBindings.join(", ")}`,
1164
+ `[1tube] forwarding ${gatewayEnvBindings.length} env var(s) to workerd ` +
1165
+ `(${secretNames.length} secret(s), ${configuredEnvBindings.length} explicit): ${gatewayEnvBindings.join(", ")}`,
1150
1166
  );
1151
1167
  }
1168
+ } else if (secretNames.length > 0) {
1169
+ console.log(`[1tube] forwarding ${secretNames.length} runtime secret env var(s) to workerd: ${secretNames.join(", ")}`);
1152
1170
  }
1153
1171
 
1154
1172
  // Load each function's 1tube.json. Missing files yield
@@ -1202,7 +1220,7 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1202
1220
  const m = newManifests.get(r.name)!;
1203
1221
  const bundleEmbedPath = prebuiltBundleFiles?.get(r.name) ??
1204
1222
  r.bundlePath.split(/[\\/]/).pop()!;
1205
- const fnEnvBindings = intersectEnvForFunction(envBindings, m.permissions.env, enforceManifest);
1223
+ const fnEnvBindings = intersectEnvForFunction(gatewayEnvBindings, m.permissions.env, enforceManifest);
1206
1224
  for (const internalName of internalEnvBindings) {
1207
1225
  if (!fnEnvBindings.includes(internalName)) fnEnvBindings.push(internalName);
1208
1226
  }
@@ -1241,6 +1259,21 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1241
1259
 
1242
1260
  const newRoutesByName = new Map(capnp.routes.map((r) => [r.name, r]));
1243
1261
  const newNames = capnp.routes.map((r) => r.name).sort();
1262
+ if (args.gen === 0) {
1263
+ const ports = capnp.routes.map((r) => r.port);
1264
+ const minPort = Math.min(...ports);
1265
+ const maxPort = Math.max(...ports);
1266
+ const totalEnvBindings = capnpInputs.reduce((sum, input) => sum + (input.envBindings?.length ?? 0), 0);
1267
+ const maxEnvBindings = capnpInputs.reduce((max, input) => Math.max(max, input.envBindings?.length ?? 0), 0);
1268
+ console.log(
1269
+ `[1tube] workerd config: path=${capnpPath} services=${capnp.routes.length} ` +
1270
+ `ports=${minPort}${minPort === maxPort ? "" : `-${maxPort}`} ` +
1271
+ `compat=${effectiveCompatDate ?? "default"} ` +
1272
+ `flags=${(opts.compatibilityFlags ?? ["default"]).join(",")} ` +
1273
+ `envMode=${hasConfiguredEnvPolicy ? configuredEnvMode : "secrets"} ` +
1274
+ `envBindingsTotal=${totalEnvBindings} envBindingsMaxPerFn=${maxEnvBindings}`,
1275
+ );
1276
+ }
1244
1277
 
1245
1278
  // Preflight: refuse to spawn workerd against ports that are
1246
1279
  // already in use. Without this, a leftover workerd from a
@@ -1341,11 +1374,16 @@ export function createWorkerdBackend(opts: WorkerdBackendOptions): WorkerdBacken
1341
1374
 
1342
1375
  const maxHeapArgs = buildMaxHeapExtraArgs(opts.maxHeapMB);
1343
1376
  const extraArgs = [...inspectorArgs, ...maxHeapArgs];
1377
+ const verboseWorkerd = (Deno.env.get("1TUBE_WORKERD_VERBOSE") ?? "") === "1";
1378
+ if (verboseWorkerd) {
1379
+ console.log(`[1tube] workerd verbose logging enabled (1TUBE_WORKERD_VERBOSE=1)`);
1380
+ }
1344
1381
 
1345
1382
  const newProcess = createWorkerdProcess({
1346
1383
  binary: workerdBin,
1347
1384
  capnpPath,
1348
1385
  routes: capnp.routes,
1386
+ ...(verboseWorkerd ? { globalArgs: ["-v"] } : {}),
1349
1387
  extraArgs,
1350
1388
  env: sharedRuntime
1351
1389
  ? {
@@ -44,6 +44,8 @@ const DEFAULT_READY_TIMEOUT_MS = 15_000;
44
44
  const DEFAULT_PROBE_INTERVAL_MS = 50;
45
45
  /** Default grace period before escalating from SIGTERM to SIGKILL. */
46
46
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
47
+ /** Bounded child-output tail attached to startup failures. */
48
+ const OUTPUT_TAIL_LINES = 800;
47
49
 
48
50
  export interface WorkerdProcessOptions {
49
51
  /** Absolute or PATH-resolvable path to the `workerd` binary. */
@@ -362,6 +364,46 @@ function bestEffortKill(child: Deno.ChildProcess, signal: Deno.Signal): boolean
362
364
  }
363
365
  }
364
366
 
367
+ function quoteArg(arg: string): string {
368
+ return /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : JSON.stringify(arg);
369
+ }
370
+
371
+ function summarizeRoutes(routes: readonly CapnpRoute[]): string {
372
+ if (routes.length === 0) return "routes=0";
373
+ const ports = routes.map((r) => r.port);
374
+ const min = Math.min(...ports);
375
+ const max = Math.max(...ports);
376
+ const hosts = [...new Set(routes.map((r) => r.address))].join(",");
377
+ return `routes=${routes.length} hosts=${hosts} ports=${min}${min === max ? "" : `-${max}`}`;
378
+ }
379
+
380
+ function formatStartupFailure(opts: {
381
+ command: readonly string[];
382
+ cwd: string;
383
+ capnpPath: string;
384
+ routes: readonly CapnpRoute[];
385
+ exitCode: number | null | undefined;
386
+ outputTail: readonly string[];
387
+ cause: unknown;
388
+ }): string {
389
+ const lines = [
390
+ "workerd exited before sockets became ready",
391
+ ` exitCode=${opts.exitCode ?? "<unknown>"}`,
392
+ ` command=${opts.command.map(quoteArg).join(" ")}`,
393
+ ` cwd=${opts.cwd}`,
394
+ ` capnp=${opts.capnpPath}`,
395
+ ` ${summarizeRoutes(opts.routes)}`,
396
+ ];
397
+ if (opts.cause instanceof Error) {
398
+ lines.push(` readiness=${opts.cause.message}`);
399
+ }
400
+ if (opts.outputTail.length > 0) {
401
+ lines.push(` -- last ${opts.outputTail.length} workerd output line(s) --`);
402
+ for (const line of opts.outputTail) lines.push(` | ${line}`);
403
+ }
404
+ return lines.join("\n");
405
+ }
406
+
365
407
  /** Build the manager. Stateful — caller owns lifecycle. */
366
408
  export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProcess {
367
409
  const sink = opts.logLineSink ?? defaultLogLineSink;
@@ -402,14 +444,23 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
402
444
  // a transitive node-path dependency on this module; the inline
403
445
  // separator-aware split is enough for our purposes.
404
446
  const cwd = opts.cwd ?? opts.capnpPath.replace(/[\\/][^\\/]+$/, "");
447
+ const args = [
448
+ ...(opts.globalArgs ?? []),
449
+ "serve",
450
+ opts.capnpPath,
451
+ ...(opts.extraArgs ?? []),
452
+ ];
453
+ const command = [opts.binary, ...args];
454
+ const outputTail: string[] = [];
455
+ let exitCode: number | null | undefined;
456
+ const captureLine = (line: string) => {
457
+ outputTail.push(line);
458
+ while (outputTail.length > OUTPUT_TAIL_LINES) outputTail.shift();
459
+ sink(line);
460
+ };
405
461
 
406
462
  const cmd = new Deno.Command(opts.binary, {
407
- args: [
408
- ...(opts.globalArgs ?? []),
409
- "serve",
410
- opts.capnpPath,
411
- ...(opts.extraArgs ?? []),
412
- ],
463
+ args,
413
464
  cwd,
414
465
  env: opts.env,
415
466
  stdout: "piped",
@@ -421,8 +472,8 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
421
472
  // Pump stdout + stderr into the sink. Both pumps are awaited at
422
473
  // shutdown so the consumer can be sure no lines are lost.
423
474
  pumpsDone = Promise.all([
424
- pumpLines(child.stdout, sink),
425
- pumpLines(child.stderr, sink),
475
+ pumpLines(child.stdout, captureLine),
476
+ pumpLines(child.stderr, captureLine),
426
477
  ]).then(() => {});
427
478
 
428
479
  // Watch for early exit so a workerd that crashes during boot
@@ -430,6 +481,7 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
430
481
  // the readiness wait instead of hanging until the timeout.
431
482
  const exitAbort = new AbortController();
432
483
  const exitWatch = child.status.then((status) => {
484
+ exitCode = status.code;
433
485
  const expected = stopRequested;
434
486
  if (!expected) {
435
487
  // Crash before ready or during runtime. Abort the readiness
@@ -455,11 +507,17 @@ export function createWorkerdProcess(opts: WorkerdProcessOptions): WorkerdProces
455
507
  // (extremely rare with workerd, but defence in depth).
456
508
  if (child) bestEffortKill(child, "SIGKILL");
457
509
  await exitWatch.catch(() => {}); // swallow — already in failure path
510
+ if (pumpsDone) await pumpsDone.catch(() => {});
458
511
  if (exitAbort.signal.aborted) {
459
- throw new Error(
460
- `workerd exited before sockets became ready` +
461
- (err instanceof Error ? `: ${err.message}` : ""),
462
- );
512
+ throw new Error(formatStartupFailure({
513
+ command,
514
+ cwd,
515
+ capnpPath: opts.capnpPath,
516
+ routes: opts.routes,
517
+ exitCode,
518
+ outputTail,
519
+ cause: err,
520
+ }));
463
521
  }
464
522
  throw err;
465
523
  }