1tube 0.1.2 → 0.1.4
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 +1 -0
- package/README.md +2 -0
- package/eslint-plugin.js +45 -0
- package/package.json +1 -1
- package/src/health.ts +23 -2
- package/src/server.ts +38 -4
- package/src/version.ts +13 -0
package/.env.example
CHANGED
package/README.md
CHANGED
package/eslint-plugin.js
CHANGED
|
@@ -57,6 +57,51 @@ const plugin = {
|
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
|
|
60
|
+
"require-access-policy": {
|
|
61
|
+
meta: {
|
|
62
|
+
type: "problem",
|
|
63
|
+
docs: {
|
|
64
|
+
description:
|
|
65
|
+
"Require every serve() call to declare an access policy via the `require` option",
|
|
66
|
+
},
|
|
67
|
+
messages: {
|
|
68
|
+
missingRequire:
|
|
69
|
+
'serve() must declare an access policy. Add { require: "public" }, { require: ["authenticated"] }, or { require: ["isAdminOrHigher"] } etc.',
|
|
70
|
+
},
|
|
71
|
+
schema: [],
|
|
72
|
+
},
|
|
73
|
+
create(context) {
|
|
74
|
+
return {
|
|
75
|
+
CallExpression(node) {
|
|
76
|
+
if (
|
|
77
|
+
node.callee.type !== "Identifier" ||
|
|
78
|
+
node.callee.name !== "serve"
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const opts = node.arguments[1];
|
|
84
|
+
|
|
85
|
+
if (!opts || opts.type !== "ObjectExpression") {
|
|
86
|
+
context.report({ node, messageId: "missingRequire" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hasRequire = opts.properties.some(
|
|
91
|
+
(p) =>
|
|
92
|
+
p.type === "Property" &&
|
|
93
|
+
p.key.type === "Identifier" &&
|
|
94
|
+
p.key.name === "require",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (!hasRequire) {
|
|
98
|
+
context.report({ node, messageId: "missingRequire" });
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
|
|
60
105
|
"no-direct-edge-url": {
|
|
61
106
|
meta: {
|
|
62
107
|
type: "problem",
|
package/package.json
CHANGED
package/src/health.ts
CHANGED
|
@@ -6,20 +6,41 @@ import type { Context } from "npm:hono@4";
|
|
|
6
6
|
import type { FunctionRegistry } from "./registry.ts";
|
|
7
7
|
import { getCollectedMetrics, getPrometheusMetrics } from "./gateway/logging.ts";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
function hasValidKey(c: Context, secretKey?: string): boolean {
|
|
10
|
+
return !!secretKey && c.req.query("key") === secretKey;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createHealthHandler(
|
|
14
|
+
registry: FunctionRegistry,
|
|
15
|
+
secretKey?: string,
|
|
16
|
+
) {
|
|
10
17
|
return (c: Context) => {
|
|
18
|
+
if (!hasValidKey(c, secretKey)) {
|
|
19
|
+
return c.json({ status: "ok" });
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
const metrics = getCollectedMetrics();
|
|
23
|
+
const mem = Deno.memoryUsage();
|
|
12
24
|
return c.json({
|
|
13
25
|
status: "ok",
|
|
14
26
|
function_count: registry.size,
|
|
15
27
|
functionList: registry.list(),
|
|
28
|
+
memory: {
|
|
29
|
+
rss_mb: Math.round(mem.rss / 1024 / 1024),
|
|
30
|
+
heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
31
|
+
heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024),
|
|
32
|
+
},
|
|
16
33
|
...metrics,
|
|
17
34
|
});
|
|
18
35
|
};
|
|
19
36
|
}
|
|
20
37
|
|
|
21
|
-
export function createMetricsHandler() {
|
|
38
|
+
export function createMetricsHandler(secretKey?: string) {
|
|
22
39
|
return (c: Context) => {
|
|
40
|
+
if (!hasValidKey(c, secretKey)) {
|
|
41
|
+
return c.json({ error: "Forbidden" }, 403);
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
return new Response(getPrometheusMetrics(), {
|
|
24
45
|
headers: { "Content-Type": "text/plain; version=0.0.4; charset=utf-8" },
|
|
25
46
|
});
|
package/src/server.ts
CHANGED
|
@@ -17,6 +17,20 @@ import { corsMiddleware } from "./gateway/cors.ts";
|
|
|
17
17
|
import { loggingMiddleware } from "./gateway/logging.ts";
|
|
18
18
|
import { createRateLimiter } from "./gateway/rate-limit.ts";
|
|
19
19
|
import { createHealthHandler, createMetricsHandler } from "./health.ts";
|
|
20
|
+
import { VERSION } from "./version.ts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Process-level error handlers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
globalThis.addEventListener("unhandledrejection", (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
console.error("[1tube] Unhandled promise rejection:", e.reason);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
globalThis.addEventListener("error", (e) => {
|
|
32
|
+
console.error("[1tube] Uncaught error:", e.error ?? e.message);
|
|
33
|
+
});
|
|
20
34
|
|
|
21
35
|
// ---------------------------------------------------------------------------
|
|
22
36
|
// CLI args & env
|
|
@@ -71,9 +85,17 @@ function parseArgs(): { port: number; functionsPath: string; defaultTimeoutMs: n
|
|
|
71
85
|
// ---------------------------------------------------------------------------
|
|
72
86
|
|
|
73
87
|
const { port, functionsPath, defaultTimeoutMs } = parseArgs();
|
|
88
|
+
const internalKey = Deno.env.get("INTERNAL_KEY")?.trim() || undefined;
|
|
74
89
|
|
|
75
90
|
applyLocalDefaults();
|
|
76
91
|
|
|
92
|
+
const denoVersion = `${Deno.version.deno} (V8 ${Deno.version.v8}, TS ${Deno.version.typescript})`;
|
|
93
|
+
const bannerText = ` 1tube v${VERSION} · Deno ${denoVersion} `;
|
|
94
|
+
const bannerLine = "─".repeat(bannerText.length);
|
|
95
|
+
console.log(`\x1b[36m┌${bannerLine}┐\x1b[0m`);
|
|
96
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1m1tube\x1b[0m v${VERSION} \x1b[2m·\x1b[0m Deno ${denoVersion} \x1b[36m│\x1b[0m`);
|
|
97
|
+
console.log(`\x1b[36m└${bannerLine}┘\x1b[0m`);
|
|
98
|
+
|
|
77
99
|
const registry = new FunctionRegistry();
|
|
78
100
|
(globalThis as any).__edgeFunctionRegistry = registry;
|
|
79
101
|
|
|
@@ -173,7 +195,16 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
173
195
|
const originalUrl = new URL(c.req.raw.url);
|
|
174
196
|
const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
|
|
175
197
|
const rewrittenUrl = new URL(rewrittenPath + originalUrl.search, originalUrl.origin);
|
|
176
|
-
|
|
198
|
+
|
|
199
|
+
const abort = new AbortController();
|
|
200
|
+
const rewrittenReq = new Request(rewrittenUrl.toString(), {
|
|
201
|
+
method: c.req.raw.method,
|
|
202
|
+
headers: c.req.raw.headers,
|
|
203
|
+
body: c.req.raw.body,
|
|
204
|
+
signal: abort.signal,
|
|
205
|
+
// @ts-ignore -- Deno supports duplex on Request
|
|
206
|
+
duplex: "half",
|
|
207
|
+
});
|
|
177
208
|
|
|
178
209
|
let handlerPromise: Promise<Response>;
|
|
179
210
|
|
|
@@ -191,7 +222,10 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
191
222
|
try {
|
|
192
223
|
if (timeoutMs > 0) {
|
|
193
224
|
const timeout = new Promise<never>((_, reject) =>
|
|
194
|
-
setTimeout(() =>
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
abort.abort();
|
|
227
|
+
reject(new Error(`Function "${name}" timed out after ${timeoutMs}ms`));
|
|
228
|
+
}, timeoutMs),
|
|
195
229
|
);
|
|
196
230
|
return await Promise.race([handlerPromise, timeout]);
|
|
197
231
|
}
|
|
@@ -207,8 +241,8 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
207
241
|
});
|
|
208
242
|
|
|
209
243
|
// Health & observability
|
|
210
|
-
app.get("/health", createHealthHandler(registry));
|
|
211
|
-
app.get("/metrics", createMetricsHandler());
|
|
244
|
+
app.get("/health", createHealthHandler(registry, internalKey));
|
|
245
|
+
app.get("/metrics", createMetricsHandler(internalKey));
|
|
212
246
|
|
|
213
247
|
// Root info
|
|
214
248
|
app.get("/", (c) =>
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { join, fromFileUrl } from "jsr:@std/path@^1";
|
|
2
|
+
|
|
3
|
+
function readVersion(): string {
|
|
4
|
+
try {
|
|
5
|
+
const dir = fromFileUrl(new URL(".", import.meta.url));
|
|
6
|
+
const raw = Deno.readTextFileSync(join(dir, "..", "package.json"));
|
|
7
|
+
return JSON.parse(raw).version ?? "unknown";
|
|
8
|
+
} catch {
|
|
9
|
+
return "unknown";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VERSION = readVersion();
|