@cosmicdrift/kumiko-framework 0.285.1 → 0.286.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.285.1",
3
+ "version": "0.286.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -198,8 +198,8 @@
198
198
  "./package.json": "./package.json"
199
199
  },
200
200
  "dependencies": {
201
- "@cosmicdrift/kumiko-http": "0.285.1",
202
- "@cosmicdrift/kumiko-types": "0.285.1",
201
+ "@cosmicdrift/kumiko-http": "0.286.0",
202
+ "@cosmicdrift/kumiko-types": "0.286.0",
203
203
  "bullmq": "^5.76.7",
204
204
  "bun-types": "^1.3.13",
205
205
  "hono": "^4.13.1",
@@ -215,7 +215,7 @@
215
215
  "zod": "^4.4.3"
216
216
  },
217
217
  "devDependencies": {
218
- "@cosmicdrift/kumiko-dispatcher-live": "0.285.1",
218
+ "@cosmicdrift/kumiko-dispatcher-live": "0.286.0",
219
219
  "bun-types": "^1.3.13",
220
220
  "pino-pretty": "^13.1.3"
221
221
  },
package/src/api/index.ts CHANGED
@@ -44,7 +44,10 @@ export { patAllows, qnMatches } from "./pat-scope";
44
44
  export type { RedisSseBroker, RedisSseBrokerOptions } from "./redis-sse-broker";
45
45
  export { createDefaultSseBroker, createRedisSseBroker, isRedisSseBroker } from "./redis-sse-broker";
46
46
  export { type RequestContextData, requestContext } from "./request-context";
47
- export { requestIdMiddleware } from "./request-id-middleware";
47
+ export {
48
+ buildRequestContextDataFromRequest,
49
+ requestIdMiddleware,
50
+ } from "./request-id-middleware";
48
51
  export { createApiRoutes } from "./routes";
49
52
  export type { KumikoServer, ServerOptions } from "./server";
50
53
  export { buildServer } from "./server";
@@ -16,43 +16,47 @@ function sanitizeClientId(value: string | undefined): string | undefined {
16
16
  return value !== undefined && SAFE_ID_RE.test(value) ? value : undefined;
17
17
  }
18
18
 
19
+ // Request.headers.get() returns `string | null` (Fetch API); Hono's
20
+ // c.req.header() normalizes that to `string | undefined`. Match Hono's
21
+ // contract here so both builders return the exact same RequestContextData
22
+ // shape regardless of which one a call-site uses.
23
+ function header(req: Request, name: string): string | undefined {
24
+ return req.headers.get(name) ?? undefined;
25
+ }
26
+
19
27
  /**
20
- * Builds the RequestContextData record for a Hono request — requestId
28
+ * Builds the RequestContextData record for a raw Fetch Request — requestId
21
29
  * (client-supplied + sanitized, or generated), correlationId (mirrors
22
30
  * requestId unless the client set its own), the underlying abort signal,
23
- * and the client IP/User-Agent. Extracted out of `requestIdMiddleware` so
24
- * call-sites that invoke a handler outside that middleware's `next()`
25
- * chain (e.g. server.ts's httpRoute→systemQuery mount) can still populate
26
- * the same AsyncLocalStorage record via `requestContext.run(...)`.
31
+ * and the client IP/User-Agent. Extracted out of `buildRequestContextData`
32
+ * so call-sites that only have a `Request` (no Hono `Context`) — e.g.
33
+ * server-runtime's static-fallback page-head resolver, which runs outside
34
+ * Hono's router entirely — can still populate the same AsyncLocalStorage
35
+ * record via `requestContext.run(...)`.
27
36
  */
28
- export function buildRequestContextData(c: Context): RequestContextData {
29
- const requestId =
30
- sanitizeClientId(c.req.header(REQUEST_ID_HEADER)) ?? requestContext.generateId();
31
- const correlationId = sanitizeClientId(c.req.header(CORRELATION_ID_HEADER)) ?? requestId;
37
+ export function buildRequestContextDataFromRequest(req: Request): RequestContextData {
38
+ const requestId = sanitizeClientId(header(req, REQUEST_ID_HEADER)) ?? requestContext.generateId();
39
+ const correlationId = sanitizeClientId(header(req, CORRELATION_ID_HEADER)) ?? requestId;
32
40
 
33
- // Hono exposes the underlying Fetch Request — its `signal` aborts
34
- // when the client disconnects (mobile back-press, tab close). We
35
- // propagate it through requestContext so framework internals can
36
- // honour cancellation at long-running checkpoints. Older Hono /
37
- // adapter combos may not populate `c.req.raw.signal`; conditional
38
- // spread keeps `signal: undefined` out of the stored record so
39
- // downstream `signal?` checks behave as if no signal exists.
40
- const signal = c.req.raw?.signal;
41
+ // The Fetch Request's `signal` aborts when the client disconnects (mobile
42
+ // back-press, tab close). We propagate it through requestContext so
43
+ // framework internals can honour cancellation at long-running checkpoints.
44
+ const signal = req.signal;
41
45
  // Client IP for per-IP rate limiting. Trust `x-forwarded-for` when
42
46
  // present (proxy/CDN) — first hop is the originating client. Adapter-
43
47
  // specific socket-address fallback (bun, node) is not standardized
44
48
  // in Hono; deployments behind a proxy should always set xff. Without
45
49
  // either we leave `ip` undefined and skip ip-bucketed checks rather
46
50
  // than fabricate one.
47
- const xff = c.req.header("x-forwarded-for");
51
+ const xff = header(req, "x-forwarded-for");
48
52
  const ip = xff?.split(",")[0]?.trim();
49
- const userAgent = c.req.header("user-agent");
53
+ const userAgent = header(req, "user-agent");
50
54
  // Runs before auth-middleware, so this reaches public routes too (e.g.
51
55
  // signup-request) — that's the whole point: the active UI locale must
52
56
  // survive to anonymous callers, not just authenticated ones.
53
57
  const locale = resolveHeaderLocale({
54
- headerLocale: c.req.header(LOCALE_HEADER_NAME),
55
- acceptLanguage: c.req.header("accept-language"),
58
+ headerLocale: header(req, LOCALE_HEADER_NAME),
59
+ acceptLanguage: header(req, "accept-language"),
56
60
  });
57
61
 
58
62
  return {
@@ -65,6 +69,23 @@ export function buildRequestContextData(c: Context): RequestContextData {
65
69
  };
66
70
  }
67
71
 
72
+ /**
73
+ * Builds the RequestContextData record for a Hono request. Thin wrapper
74
+ * around `buildRequestContextDataFromRequest(c.req.raw)` — kept as its own
75
+ * export because most call-sites (server.ts's httpRoute→systemQuery mount,
76
+ * `requestIdMiddleware` below) already hold a Hono `Context`.
77
+ */
78
+ export function buildRequestContextData(c: Context): RequestContextData {
79
+ // Older Hono / adapter combos may leave c.req.raw unset even though it's
80
+ // typed as Request — degrade to a bare id pair (no signal/ip/ua/locale)
81
+ // instead of letting req.headers.get() throw on every request.
82
+ if (!c.req.raw) {
83
+ const requestId = requestContext.generateId();
84
+ return { requestId, correlationId: requestId };
85
+ }
86
+ return buildRequestContextDataFromRequest(c.req.raw);
87
+ }
88
+
68
89
  /**
69
90
  * Assigns a requestId + correlationId to every request and wraps execution
70
91
  * in AsyncLocalStorage. Runs BEFORE auth — both ids are available even for
@@ -34,6 +34,13 @@ export type {
34
34
  SignedUrlOptions,
35
35
  WriteStreamOptions,
36
36
  } from "./types";
37
- export { assertSafeStorageKey, buildStorageKey, parseMaxSize, validateFile } from "./types";
37
+ export {
38
+ assertSafeStorageKey,
39
+ buildStorageKey,
40
+ parseMaxSize,
41
+ tenantExportPrefix,
42
+ tenantStoragePrefixes,
43
+ validateFile,
44
+ } from "./types";
38
45
  export type { ZipEntry } from "./zip-stream";
39
46
  export { createZipStream } from "./zip-stream";
@@ -174,3 +174,18 @@ export function buildStorageKey(
174
174
  const ext = /^[A-Za-z0-9]+$/.test(rawExt) ? rawExt.toLowerCase() : "bin";
175
175
  return `${tenantId}/${entityType}/${entityId}/${fieldName}/${uniqueId}.${ext}`;
176
176
  }
177
+
178
+ /** Fixed leading segment so one S3 lifecycle rule (Prefix: "exports/") can expire export bundles for every tenant without matching a buildStorageKey() upload. */
179
+ export function tenantExportPrefix(tenantId: TenantId): string {
180
+ return `exports/${tenantId}/`;
181
+ }
182
+
183
+ /**
184
+ * Every storage-key prefix a tenant's binaries can live under. A tenant-destroy
185
+ * prefix sweep must list ALL of these, not just buildStorageKey()'s
186
+ * `${tenantId}/` — new key layouts (like tenantExportPrefix) that don't put
187
+ * the tenant first need adding here too, or a sweep silently stops covering them.
188
+ */
189
+ export function tenantStoragePrefixes(tenantId: TenantId): readonly string[] {
190
+ return [`${tenantId}/`, tenantExportPrefix(tenantId)];
191
+ }