1tube 0.1.51 → 0.1.52
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 +1 -1
- package/src/gateway/auth.ts +61 -34
- package/src/gateway/cors.ts +2 -1
- package/src/server.ts +24 -6
package/package.json
CHANGED
package/src/gateway/auth.ts
CHANGED
|
@@ -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
|
|
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
|
|
191
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/gateway/cors.ts
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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 {
|
|
@@ -1842,13 +1842,15 @@ app.use(fnRouteWildcard, async (c, next) => {
|
|
|
1842
1842
|
// unauthenticated requests here. Public functions need to pass through and
|
|
1843
1843
|
// rate-limit by IP; protected functions are gated below in the dispatcher.
|
|
1844
1844
|
try {
|
|
1845
|
-
const
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1845
|
+
const probed = await probeRequest(c.req.raw);
|
|
1846
|
+
(c as any).set("__authProbe", probed.status);
|
|
1847
|
+
if (probed.status === "ok") {
|
|
1848
|
+
c.set("userId", probed.auth.userId);
|
|
1849
|
+
(c as any).set("__authContext", probed.auth);
|
|
1849
1850
|
}
|
|
1850
1851
|
} catch {
|
|
1851
|
-
// Ignore validation errors at probe time — dispatcher will
|
|
1852
|
+
// Ignore validation errors at probe time — dispatcher will 401/503.
|
|
1853
|
+
(c as any).set("__authProbe", "invalid");
|
|
1852
1854
|
}
|
|
1853
1855
|
await next();
|
|
1854
1856
|
});
|
|
@@ -2024,6 +2026,15 @@ app.all(routeDispatchPattern(), async (c) => {
|
|
|
2024
2026
|
duplex: "half",
|
|
2025
2027
|
});
|
|
2026
2028
|
const auth = (c as any).get("__authContext") ?? null;
|
|
2029
|
+
const knownPublic = registry.get(name)?.isPublic ??
|
|
2030
|
+
registry.workerHandle(name)?.isPublic;
|
|
2031
|
+
if (knownPublic === false && !auth && (c as any).get("__authProbe") === "unavailable") {
|
|
2032
|
+
return c.json(
|
|
2033
|
+
{ error: "Auth temporarily unavailable", reason: "jwt_secret_unavailable" },
|
|
2034
|
+
503,
|
|
2035
|
+
{ "X-1tube-Auth-Unavailable": "1" },
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2027
2038
|
|
|
2028
2039
|
// Per-function timeout from the manifest, falling back to the
|
|
2029
2040
|
// gateway-wide default. Same precedence the Deno path uses.
|
|
@@ -2248,6 +2259,13 @@ app.all(routeDispatchPattern(), async (c) => {
|
|
|
2248
2259
|
|
|
2249
2260
|
const auth = (c as any).get("__authContext") ?? null;
|
|
2250
2261
|
if (!handle.isPublic && !auth) {
|
|
2262
|
+
if ((c as any).get("__authProbe") === "unavailable") {
|
|
2263
|
+
return c.json(
|
|
2264
|
+
{ error: "Auth temporarily unavailable", reason: "jwt_secret_unavailable" },
|
|
2265
|
+
503,
|
|
2266
|
+
{ "X-1tube-Auth-Unavailable": "1" },
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2251
2269
|
return c.json({ error: "Unauthorized" }, 401);
|
|
2252
2270
|
}
|
|
2253
2271
|
|