1tube 0.1.3 → 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/eslint-plugin.js +45 -0
- package/package.json +1 -1
- package/src/health.ts +23 -2
- package/src/server.ts +30 -4
package/.env.example
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
|
@@ -19,6 +19,19 @@ import { createRateLimiter } from "./gateway/rate-limit.ts";
|
|
|
19
19
|
import { createHealthHandler, createMetricsHandler } from "./health.ts";
|
|
20
20
|
import { VERSION } from "./version.ts";
|
|
21
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
|
+
});
|
|
34
|
+
|
|
22
35
|
// ---------------------------------------------------------------------------
|
|
23
36
|
// CLI args & env
|
|
24
37
|
// ---------------------------------------------------------------------------
|
|
@@ -72,6 +85,7 @@ function parseArgs(): { port: number; functionsPath: string; defaultTimeoutMs: n
|
|
|
72
85
|
// ---------------------------------------------------------------------------
|
|
73
86
|
|
|
74
87
|
const { port, functionsPath, defaultTimeoutMs } = parseArgs();
|
|
88
|
+
const internalKey = Deno.env.get("INTERNAL_KEY")?.trim() || undefined;
|
|
75
89
|
|
|
76
90
|
applyLocalDefaults();
|
|
77
91
|
|
|
@@ -181,7 +195,16 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
181
195
|
const originalUrl = new URL(c.req.raw.url);
|
|
182
196
|
const rewrittenPath = originalUrl.pathname.replace(/^\/functions\/v1/, "") || "/";
|
|
183
197
|
const rewrittenUrl = new URL(rewrittenPath + originalUrl.search, originalUrl.origin);
|
|
184
|
-
|
|
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
|
+
});
|
|
185
208
|
|
|
186
209
|
let handlerPromise: Promise<Response>;
|
|
187
210
|
|
|
@@ -199,7 +222,10 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
199
222
|
try {
|
|
200
223
|
if (timeoutMs > 0) {
|
|
201
224
|
const timeout = new Promise<never>((_, reject) =>
|
|
202
|
-
setTimeout(() =>
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
abort.abort();
|
|
227
|
+
reject(new Error(`Function "${name}" timed out after ${timeoutMs}ms`));
|
|
228
|
+
}, timeoutMs),
|
|
203
229
|
);
|
|
204
230
|
return await Promise.race([handlerPromise, timeout]);
|
|
205
231
|
}
|
|
@@ -215,8 +241,8 @@ app.all("/functions/v1/:name{.+}", async (c) => {
|
|
|
215
241
|
});
|
|
216
242
|
|
|
217
243
|
// Health & observability
|
|
218
|
-
app.get("/health", createHealthHandler(registry));
|
|
219
|
-
app.get("/metrics", createMetricsHandler());
|
|
244
|
+
app.get("/health", createHealthHandler(registry, internalKey));
|
|
245
|
+
app.get("/metrics", createMetricsHandler(internalKey));
|
|
220
246
|
|
|
221
247
|
// Root info
|
|
222
248
|
app.get("/", (c) =>
|