@agilesyndrome/cf-genai-base 0.1.0 → 0.1.2
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/README.md +7 -0
- package/package.json +2 -2
- package/src/index.js +51 -11
package/README.md
CHANGED
|
@@ -19,3 +19,10 @@ export default createWorker({
|
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
21
|
```
|
|
22
|
+
|
|
23
|
+
## Shared platform helpers
|
|
24
|
+
|
|
25
|
+
`createWorker` can own `/health` and `/api/health`, run a boot validator before
|
|
26
|
+
requests, and optionally deliver server-side PostHog events. Use
|
|
27
|
+
`assertBoot(env, { bindings: ["DB"], required: ["AUTH_SESSION_SECRET"] })` in a
|
|
28
|
+
site initializer to fail closed when its Cloudflare configuration is incomplete.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agilesyndrome/cf-genai-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.js"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/agilesyndrome/cf-genai-base.git"
|
|
21
|
+
"url": "git+https://github.com/agilesyndrome/cf-genai-base.git"
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://github.com/agilesyndrome/cf-genai-base#readme"
|
|
24
24
|
}
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Site code owns
|
|
2
|
+
* Lean, opinionated Worker composition for Cloudflare sites.
|
|
3
|
+
* Site code owns domain routes and data; this owns lifecycle and edge concerns.
|
|
4
4
|
*/
|
|
5
|
-
export function createWorker({ fetch, scheduled, auth, security = true }) {
|
|
5
|
+
export function createWorker({ fetch, scheduled, auth, health, boot, metrics, security = true }) {
|
|
6
|
+
if (typeof fetch !== "function") throw new TypeError("createWorker requires a fetch handler");
|
|
6
7
|
return {
|
|
7
8
|
async fetch(request, env, ctx) {
|
|
8
9
|
try {
|
|
10
|
+
if (boot) await boot(env, { request, ctx });
|
|
11
|
+
const url = new URL(request.url);
|
|
9
12
|
const authResponse = auth ? await auth(request, env, ctx) : null;
|
|
10
|
-
|
|
13
|
+
let response = authResponse;
|
|
14
|
+
if (!response) {
|
|
15
|
+
if (url.pathname === "/health" || url.pathname === "/api/health") {
|
|
16
|
+
const details = health ? await health(env, { request, ctx }) : {};
|
|
17
|
+
response = healthResponse(env, details);
|
|
18
|
+
} else {
|
|
19
|
+
response = await fetch(request, env, ctx);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (metrics) metrics.request(request, response, env, ctx);
|
|
11
23
|
return security ? secureResponse(response) : response;
|
|
12
24
|
} catch (error) {
|
|
13
25
|
console.error("[worker] request failed", error);
|
|
14
|
-
return secureResponse(Response.json({ error: "Internal server error" }, { status: 500 }));
|
|
26
|
+
return secureResponse(Response.json({ error: "Internal server error" }, { status: 500, headers: { "Cache-Control": "no-store" } }));
|
|
15
27
|
}
|
|
16
28
|
},
|
|
17
29
|
...(scheduled ? { scheduled } : {}),
|
|
18
30
|
};
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
export function validateBoot(env, { bindings = [], required = [] } = {}) {
|
|
34
|
+
const missingBindings = bindings.filter((name) => !env?.[name]);
|
|
35
|
+
const missingValues = required.filter((name) => !env?.[name] || String(env[name]).startsWith("replace-with-"));
|
|
36
|
+
return { ok: missingBindings.length === 0 && missingValues.length === 0, missingBindings, missingValues };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertBoot(env, spec = {}) {
|
|
40
|
+
const result = validateBoot(env, spec);
|
|
41
|
+
if (!result.ok) throw new Error(`Worker boot validation failed: ${[...result.missingBindings, ...result.missingValues].join(", ")}`);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
export function secureResponse(response) {
|
|
22
46
|
const headers = new Headers(response.headers);
|
|
23
47
|
headers.set("X-Content-Type-Options", "nosniff");
|
|
@@ -34,10 +58,26 @@ export function methodNotAllowed(allow = "GET") {
|
|
|
34
58
|
}
|
|
35
59
|
|
|
36
60
|
export function healthResponse(env, details = {}) {
|
|
37
|
-
return Response.json({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
return Response.json({ ok: true, version: String(env.BUILD_SHA || "unknown").slice(0, 7), build_number: env.BUILD_NUMBER ? String(env.BUILD_NUMBER) : null, ...details }, { headers: { "Cache-Control": "no-store" } });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createMetrics({ tokenEnv = "POSTHOG_TOKEN", host = "https://us.i.posthog.com" } = {}) {
|
|
65
|
+
return {
|
|
66
|
+
request(request, response, env, ctx) {
|
|
67
|
+
if (!env?.[tokenEnv] || !ctx?.waitUntil || new URL(request.url).pathname === "/health") return;
|
|
68
|
+
const event = response.status >= 500 ? "server_error" : "request";
|
|
69
|
+
ctx.waitUntil(track(env, event, { path: new URL(request.url).pathname, method: request.method, status: response.status }, { tokenEnv, host }));
|
|
70
|
+
},
|
|
71
|
+
track: (env, event, properties, ctx) => ctx?.waitUntil?.(track(env, event, properties, { tokenEnv, host })),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function track(env, event, properties, { tokenEnv, host }) {
|
|
76
|
+
try {
|
|
77
|
+
const token = String(env?.[tokenEnv] || "");
|
|
78
|
+
if (!token) return;
|
|
79
|
+
await fetch(`${host.replace(/\/+$/, "")}/capture/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: token, event, properties: { ...properties, distinct_id: properties?.distinct_id || "anonymous" } }) });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("[metrics] delivery failed", error);
|
|
82
|
+
}
|
|
43
83
|
}
|