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/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);