@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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/package.json +2 -2
  3. 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.0",
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
- * Small, opinionated Worker composition layer shared by the Easley Owl sites.
3
- * Site code owns routing and data access; this owns lifecycle and the edges.
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
- const response = authResponse || await fetch(request, env, ctx);
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
- ok: true,
39
- version: String(env.BUILD_SHA || "unknown").slice(0, 7),
40
- build_number: env.BUILD_NUMBER ? String(env.BUILD_NUMBER) : null,
41
- ...details,
42
- }, { headers: { "Cache-Control": "no-store" } });
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
  }