1tube 0.1.51 → 0.1.53

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.51",
3
+ "version": "0.1.53",
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",
@@ -53,7 +53,7 @@ function getSecret(): string {
53
53
  verifyCache.clear();
54
54
  if (!current && !_warnedNoSecret) {
55
55
  console.warn(
56
- "[1tube] JWT_SECRET not set — authenticated endpoints will reject all requests",
56
+ "[1tube] JWT_SECRET not set — authenticated endpoints return 503, not 401",
57
57
  );
58
58
  _warnedNoSecret = true;
59
59
  }
@@ -165,6 +165,64 @@ async function verifyToken(token: string): Promise<JWTPayload | null> {
165
165
  }
166
166
  }
167
167
 
168
+ export type RequestAuthProbe =
169
+ | { status: "ok"; auth: AuthContext }
170
+ | { status: "missing" }
171
+ | { status: "invalid" }
172
+ | { status: "unavailable" };
173
+
174
+ function serviceRoleAuth(token: string): AuthContext {
175
+ const nowSec = Math.floor(Date.now() / 1000);
176
+ return {
177
+ userId: "",
178
+ email: "",
179
+ payload: {
180
+ sub: "",
181
+ email: "",
182
+ role: "service_role",
183
+ iss: "supabase",
184
+ aud: "service_role",
185
+ iat: nowSec,
186
+ // Synthetic exp matching JWTPayload shape; the token has no real
187
+ // expiry of its own — rotation is governed by env reload.
188
+ exp: nowSec + 60 * 60,
189
+ },
190
+ rawToken: token,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Probe the Authorization header without collapsing every miss into `null`.
196
+ *
197
+ * A missing `JWT_SECRET` is `unavailable` — the gateway cannot tell a live
198
+ * session from a bad one, so the dispatcher must return 503 rather than 401.
199
+ * Treating that as Unauthorized signs SPAs out during deploys.
200
+ */
201
+ export async function probeRequest(req: Request): Promise<RequestAuthProbe> {
202
+ const token = parseBearer(req.headers.get("Authorization"));
203
+ if (!token) return { status: "missing" };
204
+
205
+ const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
206
+ if (serviceKey && token === serviceKey) {
207
+ return { status: "ok", auth: serviceRoleAuth(token) };
208
+ }
209
+
210
+ if (!getSecret()) return { status: "unavailable" };
211
+
212
+ const payload = await verifyToken(token);
213
+ if (!payload) return { status: "invalid" };
214
+
215
+ return {
216
+ status: "ok",
217
+ auth: {
218
+ userId: payload.sub,
219
+ email: payload.email,
220
+ payload,
221
+ rawToken: token,
222
+ },
223
+ };
224
+ }
225
+
168
226
  /**
169
227
  * Validate the Authorization header and return an AuthContext, or null if invalid.
170
228
  *
@@ -187,39 +245,8 @@ async function verifyToken(token: string): Promise<JWTPayload | null> {
187
245
  export async function validateRequest(
188
246
  req: Request,
189
247
  ): Promise<AuthContext | null> {
190
- const token = parseBearer(req.headers.get("Authorization"));
191
- if (!token) return null;
192
-
193
- const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
194
- if (serviceKey && token === serviceKey) {
195
- const nowSec = Math.floor(Date.now() / 1000);
196
- return {
197
- userId: "",
198
- email: "",
199
- payload: {
200
- sub: "",
201
- email: "",
202
- role: "service_role",
203
- iss: "supabase",
204
- aud: "service_role",
205
- iat: nowSec,
206
- // Synthetic exp matching JWTPayload shape; the token has no real
207
- // expiry of its own — rotation is governed by env reload.
208
- exp: nowSec + 60 * 60,
209
- },
210
- rawToken: token,
211
- };
212
- }
213
-
214
- const payload = await verifyToken(token);
215
- if (!payload) return null;
216
-
217
- return {
218
- userId: payload.sub,
219
- email: payload.email,
220
- payload,
221
- rawToken: token,
222
- };
248
+ const probed = await probeRequest(req);
249
+ return probed.status === "ok" ? probed.auth : null;
223
250
  }
224
251
 
225
252
  // ---------------------------------------------------------------------------
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * `Deno.serve` does not enforce a per-request body-read timeout. A client that
5
5
  * dribbles bytes at a few per second can occupy a request slot for the full
6
- * `FUNCTION_TIMEOUT_MS` (default 150 s), starving real traffic. We don't want
6
+ * `FUNCTION_TIMEOUT_MS` (default 800 s), starving real traffic. We don't want
7
7
  * to *cap total body read time* — large but fast uploads are fine — we want
8
8
  * to detect *stalled progress*: too long a gap between consecutive chunks.
9
9
  *
@@ -36,7 +36,8 @@ const DEFAULT_ALLOWED_METHODS = "POST, GET, OPTIONS, PUT, PATCH, DELETE";
36
36
  // its HMR respawn is still in flight; `retry-after` lets clients
37
37
  // honour the gateway's suggested backoff. These are always exposed —
38
38
  // operator config only ADDS to them.
39
- const INTERNAL_EXPOSED_HEADERS = "x-1tube-warming, x-1tube-stale, retry-after";
39
+ const INTERNAL_EXPOSED_HEADERS =
40
+ "x-1tube-warming, x-1tube-stale, x-1tube-auth-unavailable, retry-after";
40
41
 
41
42
  interface CorsConfig {
42
43
  /** "*" means allow any. Empty array means CORS disabled. */
package/src/server.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  import { type Context, Hono } from "npm:hono@4";
20
20
  import { bodyLimit } from "npm:hono@4/body-limit";
21
21
  import { currentRequestStorage, FunctionRegistry } from "./registry.ts";
22
- import { validateRequest } from "./gateway/auth.ts";
22
+ import { probeRequest } from "./gateway/auth.ts";
23
23
  import { watchdogBody } from "./gateway/body-watchdog.ts";
24
24
  import { corsMiddleware } from "./gateway/cors.ts";
25
25
  import {
@@ -426,8 +426,11 @@ function parseArgs(): CliOpts {
426
426
  // Normalized at the end via setRoutePrefix(); kept raw here so an
427
427
  // explicit CLI flag can still override the env var.
428
428
  let routePrefix = Deno.env.get("1TUBE_ROUTE_PREFIX") || "/functions/v1";
429
+ // Matches Vercel Fluid's maxDuration cap. Per-function `serve({ timeoutMs })`
430
+ // / `1tube.json` still override. `FUNCTION_TIMEOUT_MS` / `--timeout` win last.
431
+ const DEFAULT_FUNCTION_TIMEOUT_MS = 800_000;
429
432
  let defaultTimeoutMs = parseInt(
430
- Deno.env.get("FUNCTION_TIMEOUT_MS") || "150000",
433
+ Deno.env.get("FUNCTION_TIMEOUT_MS") || String(DEFAULT_FUNCTION_TIMEOUT_MS),
431
434
  10,
432
435
  );
433
436
  const bodyLimitMb = parseFloat(Deno.env.get("1TUBE_BODY_LIMIT_MB") || "30");
@@ -1842,13 +1845,15 @@ app.use(fnRouteWildcard, async (c, next) => {
1842
1845
  // unauthenticated requests here. Public functions need to pass through and
1843
1846
  // rate-limit by IP; protected functions are gated below in the dispatcher.
1844
1847
  try {
1845
- const auth = await validateRequest(c.req.raw);
1846
- if (auth) {
1847
- c.set("userId", auth.userId);
1848
- (c as any).set("__authContext", auth);
1848
+ const probed = await probeRequest(c.req.raw);
1849
+ (c as any).set("__authProbe", probed.status);
1850
+ if (probed.status === "ok") {
1851
+ c.set("userId", probed.auth.userId);
1852
+ (c as any).set("__authContext", probed.auth);
1849
1853
  }
1850
1854
  } catch {
1851
- // Ignore validation errors at probe time — dispatcher will return 401.
1855
+ // Ignore validation errors at probe time — dispatcher will 401/503.
1856
+ (c as any).set("__authProbe", "invalid");
1852
1857
  }
1853
1858
  await next();
1854
1859
  });
@@ -2024,6 +2029,15 @@ app.all(routeDispatchPattern(), async (c) => {
2024
2029
  duplex: "half",
2025
2030
  });
2026
2031
  const auth = (c as any).get("__authContext") ?? null;
2032
+ const knownPublic = registry.get(name)?.isPublic ??
2033
+ registry.workerHandle(name)?.isPublic;
2034
+ if (knownPublic === false && !auth && (c as any).get("__authProbe") === "unavailable") {
2035
+ return c.json(
2036
+ { error: "Auth temporarily unavailable", reason: "jwt_secret_unavailable" },
2037
+ 503,
2038
+ { "X-1tube-Auth-Unavailable": "1" },
2039
+ );
2040
+ }
2027
2041
 
2028
2042
  // Per-function timeout from the manifest, falling back to the
2029
2043
  // gateway-wide default. Same precedence the Deno path uses.
@@ -2248,6 +2262,13 @@ app.all(routeDispatchPattern(), async (c) => {
2248
2262
 
2249
2263
  const auth = (c as any).get("__authContext") ?? null;
2250
2264
  if (!handle.isPublic && !auth) {
2265
+ if ((c as any).get("__authProbe") === "unavailable") {
2266
+ return c.json(
2267
+ { error: "Auth temporarily unavailable", reason: "jwt_secret_unavailable" },
2268
+ 503,
2269
+ { "X-1tube-Auth-Unavailable": "1" },
2270
+ );
2271
+ }
2251
2272
  return c.json({ error: "Unauthorized" }, 401);
2252
2273
  }
2253
2274