1tube 0.1.0
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/.env.example +26 -0
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/deno.json +19 -0
- package/deno.lock +505 -0
- package/eslint-plugin.js +91 -0
- package/package.json +42 -0
- package/src/discovery.ts +69 -0
- package/src/gateway/auth.ts +88 -0
- package/src/gateway/cors.ts +25 -0
- package/src/gateway/logging.ts +122 -0
- package/src/gateway/rate-limit.ts +85 -0
- package/src/health.ts +27 -0
- package/src/registry.ts +83 -0
- package/src/server.ts +178 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1tube -- Self-hosted Supabase Edge Functions gateway.
|
|
3
|
+
*
|
|
4
|
+
* Discovers and loads edge function modules, exposes them via a single HTTP
|
|
5
|
+
* server with JWT auth, CORS, rate limiting, and structured logging.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* deno run --allow-all src/server.ts
|
|
9
|
+
* deno run --allow-all src/server.ts --functions ../sciobot-next/supabase/functions --port 3100
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Hono } from "npm:hono@4";
|
|
13
|
+
import { FunctionRegistry } from "./registry.ts";
|
|
14
|
+
import { discoverAndLoad } from "./discovery.ts";
|
|
15
|
+
import { validateRequest } from "./gateway/auth.ts";
|
|
16
|
+
import { corsMiddleware } from "./gateway/cors.ts";
|
|
17
|
+
import { loggingMiddleware } from "./gateway/logging.ts";
|
|
18
|
+
import { createRateLimiter } from "./gateway/rate-limit.ts";
|
|
19
|
+
import { createHealthHandler, createMetricsHandler } from "./health.ts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// CLI args & env
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
// Well-known defaults for local Supabase (identical for every `supabase init` project).
|
|
26
|
+
// These are only applied when the env vars are not already set.
|
|
27
|
+
const LOCAL_SUPABASE_DEFAULTS: Record<string, string> = {
|
|
28
|
+
SUPABASE_URL: "http://127.0.0.1:54321",
|
|
29
|
+
SUPABASE_SERVICE_ROLE_KEY:
|
|
30
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU",
|
|
31
|
+
JWT_SECRET: "super-secret-jwt-token-with-at-least-32-characters-long",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function applyLocalDefaults() {
|
|
35
|
+
let applied = 0;
|
|
36
|
+
for (const [key, value] of Object.entries(LOCAL_SUPABASE_DEFAULTS)) {
|
|
37
|
+
if (!Deno.env.get(key)) {
|
|
38
|
+
Deno.env.set(key, value);
|
|
39
|
+
applied++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (applied > 0) {
|
|
43
|
+
console.log(`[1tube] Applied ${applied} local Supabase default(s)`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseArgs(): { port: number; functionsPath: string; defaultTimeoutMs: number } {
|
|
48
|
+
const args = Deno.args;
|
|
49
|
+
let port = parseInt(Deno.env.get("PORT") || "3100", 10);
|
|
50
|
+
let functionsPath = Deno.env.get("FUNCTIONS_PATH") || "./supabase/functions";
|
|
51
|
+
let defaultTimeoutMs = parseInt(Deno.env.get("FUNCTION_TIMEOUT_MS") || "150000", 10);
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < args.length; i++) {
|
|
54
|
+
if (args[i] === "--port" && args[i + 1]) {
|
|
55
|
+
port = parseInt(args[i + 1], 10);
|
|
56
|
+
i++;
|
|
57
|
+
} else if (args[i] === "--functions" && args[i + 1]) {
|
|
58
|
+
functionsPath = args[i + 1];
|
|
59
|
+
i++;
|
|
60
|
+
} else if (args[i] === "--timeout" && args[i + 1]) {
|
|
61
|
+
defaultTimeoutMs = parseInt(args[i + 1], 10);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { port, functionsPath, defaultTimeoutMs };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Bootstrap
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
const { port, functionsPath, defaultTimeoutMs } = parseArgs();
|
|
74
|
+
|
|
75
|
+
applyLocalDefaults();
|
|
76
|
+
|
|
77
|
+
const registry = new FunctionRegistry();
|
|
78
|
+
(globalThis as any).__edgeFunctionRegistry = registry;
|
|
79
|
+
|
|
80
|
+
console.log(`[1tube] Loading functions from: ${functionsPath}`);
|
|
81
|
+
const { loaded, skipped, errors } = await discoverAndLoad(functionsPath, registry);
|
|
82
|
+
|
|
83
|
+
console.log(`[1tube] Loaded ${loaded.length} functions, skipped ${skipped.length} directories`);
|
|
84
|
+
if (loaded.length > 0) {
|
|
85
|
+
console.log(`[1tube] Functions: ${loaded.join(", ")}`);
|
|
86
|
+
}
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
console.error(`[1tube] ${errors.length} function(s) failed to load (check .env for missing vars):`);
|
|
89
|
+
for (const e of errors) {
|
|
90
|
+
console.error(` ${e.name}: ${e.error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// HTTP Server
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** Set on authenticated function calls; read in logging middleware. */
|
|
99
|
+
type GatewayVariables = { userId?: string };
|
|
100
|
+
|
|
101
|
+
const app = new Hono<{ Variables: GatewayVariables }>();
|
|
102
|
+
const rateLimiter = createRateLimiter();
|
|
103
|
+
|
|
104
|
+
// Gateway middleware on all function routes
|
|
105
|
+
app.use("/functions/v1/*", corsMiddleware);
|
|
106
|
+
app.use("/functions/v1/*", loggingMiddleware);
|
|
107
|
+
app.use("/functions/v1/*", rateLimiter);
|
|
108
|
+
|
|
109
|
+
// Function dispatch
|
|
110
|
+
app.all("/functions/v1/:name{.+}", async (c) => {
|
|
111
|
+
const name = c.req.param("name");
|
|
112
|
+
const fn = registry.get(name);
|
|
113
|
+
|
|
114
|
+
if (!fn) {
|
|
115
|
+
return c.json({ error: `Function "${name}" not found` }, 404);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const timeoutMs = fn.timeoutMs ?? defaultTimeoutMs;
|
|
119
|
+
|
|
120
|
+
// Rewrite URL to strip /functions/v1 prefix, matching Supabase Edge Runtime
|
|
121
|
+
// behavior. Functions expect paths like /rooms/:id, not /functions/v1/rooms/:id.
|
|
122
|
+
const originalUrl = new URL(c.req.raw.url);
|
|
123
|
+
const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
|
|
124
|
+
const rewrittenUrl = new URL(rewrittenPath + originalUrl.search, originalUrl.origin);
|
|
125
|
+
const rewrittenReq = new Request(rewrittenUrl.toString(), c.req.raw);
|
|
126
|
+
|
|
127
|
+
let handlerPromise: Promise<Response>;
|
|
128
|
+
|
|
129
|
+
if (!fn.isPublic) {
|
|
130
|
+
const auth = await validateRequest(c.req.raw);
|
|
131
|
+
if (!auth) {
|
|
132
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
133
|
+
}
|
|
134
|
+
c.set("userId", auth.userId);
|
|
135
|
+
handlerPromise = Promise.resolve((fn.handler as any)(rewrittenReq, auth));
|
|
136
|
+
} else {
|
|
137
|
+
handlerPromise = Promise.resolve((fn.handler as any)(rewrittenReq));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (timeoutMs > 0) {
|
|
142
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
143
|
+
setTimeout(() => reject(new Error(`Function "${name}" timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
144
|
+
);
|
|
145
|
+
return await Promise.race([handlerPromise, timeout]);
|
|
146
|
+
}
|
|
147
|
+
return await handlerPromise;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err instanceof Error && err.message.includes("timed out")) {
|
|
150
|
+
console.error(`[1tube] ${err.message}`);
|
|
151
|
+
return c.json({ error: "Function execution timed out" }, 504);
|
|
152
|
+
}
|
|
153
|
+
console.error(`[1tube] Unhandled error in "${name}":`, err);
|
|
154
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Health & observability
|
|
159
|
+
app.get("/health", createHealthHandler(registry));
|
|
160
|
+
app.get("/metrics", createMetricsHandler());
|
|
161
|
+
|
|
162
|
+
// Root info
|
|
163
|
+
app.get("/", (c) =>
|
|
164
|
+
c.json({
|
|
165
|
+
name: "1tube",
|
|
166
|
+
description: "Self-hosted Supabase Edge Functions gateway",
|
|
167
|
+
functions: registry.size,
|
|
168
|
+
endpoints: {
|
|
169
|
+
functions: "/functions/v1/:name",
|
|
170
|
+
health: "/health",
|
|
171
|
+
metrics: "/metrics",
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
console.log(`[1tube] Starting on http://localhost:${port}`);
|
|
177
|
+
|
|
178
|
+
Deno.serve({ port }, app.fetch);
|