better-ship 0.44.0 → 0.46.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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/infrastructure/posthog/server.ts"],"mappings":";;;;;;iBAiBiB;WACN;WACA;WACA;WACA,YAAY,SAAS;WACrB,UAAU,KACjB;aAGS,UAAU,KAAK,YAAY;;;;qBAK3B,mCAAmC;;WAMnC,SAAS;mBACD;;WALV,SAAS;UAEX;;SASA,OAAO,QAAQ,yBAAyB;;EAqB/C,SAAS,oBAAoB,aAAa;;EAO1C,MAAM,oBAAoB;;EAK1B,MACE,cACA,YAAY,mCACZ,QAAQ;;EAMV,eACE,cACA,YAAY,mCACZ,QAAQ;UAKF"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../../src/infrastructure/posthog/server.ts"],"mappings":";;;;;;iBAkBiB;WACN;WACA;WACA;WACA,YAAY,SAAS;WACrB,UAAU,KACjB;aAGS,UAAU,KAAK,YAAY;;;;qBAK3B,mCAAmC;;WAMnC,SAAS;mBACD;;WALV,SAAS;UAEX;;SASA,OAAO,QAAQ,yBAAyB;;EAqB/C,SAAS,oBAAoB,aAAa;;EAO1C,MAAM,oBAAoB;;EAK1B,MACE,cACA,YAAY,mCACZ,QAAQ;;EAMV,eACE,cACA,YAAY,mCACZ,QAAQ;UAKF"}
@@ -1,5 +1,6 @@
1
1
  import { nt as MissingConfigurationError } from "../core-CGj1ukXV.js";
2
2
  import { t as PostHogMetricsService } from "../metrics-CWv37yf9.js";
3
+ import { t as currentPostHogSession } from "../request-session-Qe_Q_u9G.js";
3
4
  import { v5, validate } from "uuid";
4
5
  import { PostHog } from "posthog-node";
5
6
 
@@ -60,7 +61,11 @@ var PostHogAnalyticsService = class PostHogAnalyticsService {
60
61
  this.capture(name, properties, target, "anonymous");
61
62
  }
62
63
  capture(name, eventProperties, target, profile) {
63
- const properties = { ...eventProperties };
64
+ const sessionId = currentPostHogSession();
65
+ const properties = sessionId ? {
66
+ $session_id: sessionId,
67
+ ...eventProperties
68
+ } : { ...eventProperties };
64
69
  const payload = {
65
70
  distinctId: target.distinctId,
66
71
  event: name,
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["isUuid","uuidv5"],"sources":["../../src/infrastructure/posthog/server.ts"],"sourcesContent":["import { PostHog, type PostHogOptions } from 'posthog-node'\nimport { validate as isUuid, v5 as uuidv5 } from 'uuid'\n\nimport type { AnalyticsService } from '@/application'\nimport {\n MissingConfigurationError,\n type AnalyticsEventInput,\n type AnalyticsProperties,\n type AnalyticsTarget,\n} from '@/core'\n\nimport { PostHogMetricsService } from './metrics.ts'\n\n// Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.\nconst EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'\n\n/** Deployment values and SDK options; the runtime owns delivery promises. */\nexport interface PostHogAnalyticsConfig {\n readonly apiKey: string\n readonly environment: string\n readonly source: string\n readonly waitUntil: (promise: Promise<unknown>) => void\n readonly options?: Omit<\n PostHogOptions,\n 'waitUntil' | 'flushAt' | 'flushInterval' | 'metrics' | 'disabled'\n > & {\n readonly metrics?: Omit<NonNullable<PostHogOptions['metrics']>, 'serviceName' | 'environment'>\n }\n}\n\n/** Worker analytics ported from Typist; disabled applications select Porte's NULL_ANALYTICS instead. */\nexport class PostHogAnalyticsService implements AnalyticsService {\n /** Metrics share this service's SDK client and delivery lifetime. */\n readonly metrics: PostHogMetricsService\n\n private constructor(\n /** Raw SDK access for integrations such as PostHog AI tracing. */\n readonly posthog: PostHog,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {\n this.metrics = new PostHogMetricsService(posthog, waitUntil)\n }\n\n /** Construct once per Worker invocation; a blank key throws MissingConfigurationError. */\n static create(config: PostHogAnalyticsConfig): PostHogAnalyticsService {\n const apiKey = config.apiKey.trim()\n if (apiKey.length === 0) throw new MissingConfigurationError('POSTHOG_API_KEY')\n const client = new PostHog(apiKey, {\n host: 'https://us.i.posthog.com',\n ...config.options,\n flushAt: 1,\n flushInterval: 0,\n waitUntil: config.waitUntil,\n metrics: {\n ...config.options?.metrics,\n environment: config.environment,\n serviceName: config.source,\n },\n })\n // The SDK applies in-memory super properties synchronously before this promise resolves.\n void client.register({ environment: config.environment, source: config.source })\n return new PostHogAnalyticsService(client, config.waitUntil)\n }\n\n /** Schedule person properties; the SDK's waitUntil option protects queued delivery. */\n identify(distinctId: string, properties?: AnalyticsProperties): void {\n const payload: Parameters<PostHog['identify']>[0] = { distinctId }\n if (properties !== undefined) payload.properties = properties\n this.posthog.identify(payload)\n }\n\n /** Schedule an irreversible person merge; only alias IDs the application proves belong to one person. */\n alias(distinctId: string, previousDistinctId: string): void {\n this.waitUntil(this.posthog.aliasImmediate({ distinctId, alias: previousDistinctId }))\n }\n\n /** Schedule an event; without an event ID the SDK sets a new UUID and the capture time. */\n track(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'person')\n }\n\n /** Schedule an event without creating a person profile for an attempt or resource ID. */\n trackAnonymous(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'anonymous')\n }\n\n private capture(\n name: string,\n eventProperties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n profile: 'person' | 'anonymous',\n ): void {\n const properties = { ...eventProperties }\n const payload: Parameters<PostHog['captureImmediate']>[0] = {\n distinctId: target.distinctId,\n event: name,\n properties,\n }\n if (profile === 'anonymous')\n payload.properties = { ...properties, $process_person_profile: false }\n if (target.eventId !== undefined) {\n // PostHog drops a repeat only when its uuid and timestamp match the first event.\n payload.uuid = isUuid(target.eventId)\n ? target.eventId\n : uuidv5(target.eventId, EVENT_ID_NAMESPACE)\n payload.timestamp = new Date(target.occurredAt)\n }\n this.waitUntil(this.posthog.captureImmediate(payload))\n }\n}\n"],"mappings":";;;;;;AAcA,MAAM,qBAAqB;;AAiB3B,IAAa,0BAAb,MAAa,wBAAoD;CAMpD;CACQ;;CALnB,AAAS;CAET,AAAQ,YAEN,AAAS,SACT,AAAiB,WACjB;EAFS;EACQ;EAEjB,KAAK,UAAU,IAAI,sBAAsB,SAAS,SAAS;CAC7D;;CAGA,OAAO,OAAO,QAAyD;EACrE,MAAM,SAAS,OAAO,OAAO,KAAK;EAClC,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,0BAA0B,iBAAiB;EAC9E,MAAM,SAAS,IAAI,QAAQ,QAAQ;GACjC,MAAM;GACN,GAAG,OAAO;GACV,SAAS;GACT,eAAe;GACf,WAAW,OAAO;GAClB,SAAS;IACP,GAAG,OAAO,SAAS;IACnB,aAAa,OAAO;IACpB,aAAa,OAAO;GACtB;EACF,CAAC;EAED,AAAK,OAAO,SAAS;GAAE,aAAa,OAAO;GAAa,QAAQ,OAAO;EAAO,CAAC;EAC/E,OAAO,IAAI,wBAAwB,QAAQ,OAAO,SAAS;CAC7D;;CAGA,SAAS,YAAoB,YAAwC;EACnE,MAAM,UAA8C,EAAE,WAAW;EACjE,IAAI,eAAe,QAAW,QAAQ,aAAa;EACnD,KAAK,QAAQ,SAAS,OAAO;CAC/B;;CAGA,MAAM,YAAoB,oBAAkC;EAC1D,KAAK,UAAU,KAAK,QAAQ,eAAe;GAAE;GAAY,OAAO;EAAmB,CAAC,CAAC;CACvF;;CAGA,MACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,QAAQ;CACjD;;CAGA,eACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,WAAW;CACpD;CAEA,AAAQ,QACN,MACA,iBACA,QACA,SACM;EACN,MAAM,aAAa,EAAE,GAAG,gBAAgB;EACxC,MAAM,UAAsD;GAC1D,YAAY,OAAO;GACnB,OAAO;GACP;EACF;EACA,IAAI,YAAY,aACd,QAAQ,aAAa;GAAE,GAAG;GAAY,yBAAyB;EAAM;EACvE,IAAI,OAAO,YAAY,QAAW;GAEhC,QAAQ,OAAOA,SAAO,OAAO,OAAO,IAChC,OAAO,UACPC,GAAO,OAAO,SAAS,kBAAkB;GAC7C,QAAQ,YAAY,IAAI,KAAK,OAAO,UAAU;EAChD;EACA,KAAK,UAAU,KAAK,QAAQ,iBAAiB,OAAO,CAAC;CACvD;AACF"}
1
+ {"version":3,"file":"server.js","names":["isUuid","uuidv5"],"sources":["../../src/infrastructure/posthog/server.ts"],"sourcesContent":["import { PostHog, type PostHogOptions } from 'posthog-node'\nimport { validate as isUuid, v5 as uuidv5 } from 'uuid'\n\nimport type { AnalyticsService } from '@/application'\nimport {\n MissingConfigurationError,\n type AnalyticsEventInput,\n type AnalyticsProperties,\n type AnalyticsTarget,\n} from '@/core'\n\nimport { PostHogMetricsService } from './metrics.ts'\nimport { currentPostHogSession } from './request-session.ts'\n\n// Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.\nconst EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'\n\n/** Deployment values and SDK options; the runtime owns delivery promises. */\nexport interface PostHogAnalyticsConfig {\n readonly apiKey: string\n readonly environment: string\n readonly source: string\n readonly waitUntil: (promise: Promise<unknown>) => void\n readonly options?: Omit<\n PostHogOptions,\n 'waitUntil' | 'flushAt' | 'flushInterval' | 'metrics' | 'disabled'\n > & {\n readonly metrics?: Omit<NonNullable<PostHogOptions['metrics']>, 'serviceName' | 'environment'>\n }\n}\n\n/** Worker analytics ported from Typist; disabled applications select Porte's NULL_ANALYTICS instead. */\nexport class PostHogAnalyticsService implements AnalyticsService {\n /** Metrics share this service's SDK client and delivery lifetime. */\n readonly metrics: PostHogMetricsService\n\n private constructor(\n /** Raw SDK access for integrations such as PostHog AI tracing. */\n readonly posthog: PostHog,\n private readonly waitUntil: (promise: Promise<unknown>) => void,\n ) {\n this.metrics = new PostHogMetricsService(posthog, waitUntil)\n }\n\n /** Construct once per Worker invocation; a blank key throws MissingConfigurationError. */\n static create(config: PostHogAnalyticsConfig): PostHogAnalyticsService {\n const apiKey = config.apiKey.trim()\n if (apiKey.length === 0) throw new MissingConfigurationError('POSTHOG_API_KEY')\n const client = new PostHog(apiKey, {\n host: 'https://us.i.posthog.com',\n ...config.options,\n flushAt: 1,\n flushInterval: 0,\n waitUntil: config.waitUntil,\n metrics: {\n ...config.options?.metrics,\n environment: config.environment,\n serviceName: config.source,\n },\n })\n // The SDK applies in-memory super properties synchronously before this promise resolves.\n void client.register({ environment: config.environment, source: config.source })\n return new PostHogAnalyticsService(client, config.waitUntil)\n }\n\n /** Schedule person properties; the SDK's waitUntil option protects queued delivery. */\n identify(distinctId: string, properties?: AnalyticsProperties): void {\n const payload: Parameters<PostHog['identify']>[0] = { distinctId }\n if (properties !== undefined) payload.properties = properties\n this.posthog.identify(payload)\n }\n\n /** Schedule an irreversible person merge; only alias IDs the application proves belong to one person. */\n alias(distinctId: string, previousDistinctId: string): void {\n this.waitUntil(this.posthog.aliasImmediate({ distinctId, alias: previousDistinctId }))\n }\n\n /** Schedule an event; without an event ID the SDK sets a new UUID and the capture time. */\n track(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'person')\n }\n\n /** Schedule an event without creating a person profile for an attempt or resource ID. */\n trackAnonymous(\n name: string,\n properties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n ): void {\n this.capture(name, properties, target, 'anonymous')\n }\n\n private capture(\n name: string,\n eventProperties: AnalyticsEventInput['properties'],\n target: AnalyticsTarget,\n profile: 'person' | 'anonymous',\n ): void {\n const sessionId = currentPostHogSession()\n // An application's own `$session_id` wins over the request's.\n const properties = sessionId\n ? { $session_id: sessionId, ...eventProperties }\n : { ...eventProperties }\n const payload: Parameters<PostHog['captureImmediate']>[0] = {\n distinctId: target.distinctId,\n event: name,\n properties,\n }\n if (profile === 'anonymous')\n payload.properties = { ...properties, $process_person_profile: false }\n if (target.eventId !== undefined) {\n // PostHog drops a repeat only when its uuid and timestamp match the first event.\n payload.uuid = isUuid(target.eventId)\n ? target.eventId\n : uuidv5(target.eventId, EVENT_ID_NAMESPACE)\n payload.timestamp = new Date(target.occurredAt)\n }\n this.waitUntil(this.posthog.captureImmediate(payload))\n }\n}\n"],"mappings":";;;;;;;AAeA,MAAM,qBAAqB;;AAiB3B,IAAa,0BAAb,MAAa,wBAAoD;CAMpD;CACQ;;CALnB,AAAS;CAET,AAAQ,YAEN,AAAS,SACT,AAAiB,WACjB;EAFS;EACQ;EAEjB,KAAK,UAAU,IAAI,sBAAsB,SAAS,SAAS;CAC7D;;CAGA,OAAO,OAAO,QAAyD;EACrE,MAAM,SAAS,OAAO,OAAO,KAAK;EAClC,IAAI,OAAO,WAAW,GAAG,MAAM,IAAI,0BAA0B,iBAAiB;EAC9E,MAAM,SAAS,IAAI,QAAQ,QAAQ;GACjC,MAAM;GACN,GAAG,OAAO;GACV,SAAS;GACT,eAAe;GACf,WAAW,OAAO;GAClB,SAAS;IACP,GAAG,OAAO,SAAS;IACnB,aAAa,OAAO;IACpB,aAAa,OAAO;GACtB;EACF,CAAC;EAED,AAAK,OAAO,SAAS;GAAE,aAAa,OAAO;GAAa,QAAQ,OAAO;EAAO,CAAC;EAC/E,OAAO,IAAI,wBAAwB,QAAQ,OAAO,SAAS;CAC7D;;CAGA,SAAS,YAAoB,YAAwC;EACnE,MAAM,UAA8C,EAAE,WAAW;EACjE,IAAI,eAAe,QAAW,QAAQ,aAAa;EACnD,KAAK,QAAQ,SAAS,OAAO;CAC/B;;CAGA,MAAM,YAAoB,oBAAkC;EAC1D,KAAK,UAAU,KAAK,QAAQ,eAAe;GAAE;GAAY,OAAO;EAAmB,CAAC,CAAC;CACvF;;CAGA,MACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,QAAQ;CACjD;;CAGA,eACE,MACA,YACA,QACM;EACN,KAAK,QAAQ,MAAM,YAAY,QAAQ,WAAW;CACpD;CAEA,AAAQ,QACN,MACA,iBACA,QACA,SACM;EACN,MAAM,YAAY,sBAAsB;EAExC,MAAM,aAAa,YACf;GAAE,aAAa;GAAW,GAAG;EAAgB,IAC7C,EAAE,GAAG,gBAAgB;EACzB,MAAM,UAAsD;GAC1D,YAAY,OAAO;GACnB,OAAO;GACP;EACF;EACA,IAAI,YAAY,aACd,QAAQ,aAAa;GAAE,GAAG;GAAY,yBAAyB;EAAM;EACvE,IAAI,OAAO,YAAY,QAAW;GAEhC,QAAQ,OAAOA,SAAO,OAAO,OAAO,IAChC,OAAO,UACPC,GAAO,OAAO,SAAS,kBAAkB;GAC7C,QAAQ,YAAY,IAAI,KAAK,OAAO,UAAU;EAChD;EACA,KAAK,UAAU,KAAK,QAAQ,iBAAiB,OAAO,CAAC;CACvD;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region src/infrastructure/posthog/tanstack.d.ts
2
+ /**
3
+ * Link Worker events to the browser session that sent the request.
4
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
5
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
6
+ */
7
+ export declare const posthogRequestMiddleware: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
8
+ //#endregion
9
+ //# sourceMappingURL=tanstack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../../src/infrastructure/posthog/tanstack.ts"],"mappings":";;;;;;qBAUa,0DAAwB"}
@@ -0,0 +1,19 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+
3
+ //#region src/infrastructure/posthog/tanstack.ts
4
+ const SESSION_HEADER = "x-posthog-session-id";
5
+ /**
6
+ * Link Worker events to the browser session that sent the request.
7
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
8
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
9
+ */
10
+ const posthogRequestMiddleware = createMiddleware({ type: "request" }).server(async ({ request, next }) => {
11
+ const sessionId = request.headers.get(SESSION_HEADER)?.trim();
12
+ if (!sessionId) return next();
13
+ const { runWithPostHogSession } = await import("../request-session-D1r9ZvcS.js");
14
+ return runWithPostHogSession(sessionId, () => next());
15
+ });
16
+
17
+ //#endregion
18
+ export { posthogRequestMiddleware };
19
+ //# sourceMappingURL=tanstack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tanstack.js","names":[],"sources":["../../src/infrastructure/posthog/tanstack.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\n\n// posthog-js sends this header when the browser sets `tracing_headers` for the application host.\nconst SESSION_HEADER = 'x-posthog-session-id'\n\n/**\n * Link Worker events to the browser session that sent the request.\n * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then\n * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.\n */\nexport const posthogRequestMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ request, next }) => {\n const sessionId = request.headers.get(SESSION_HEADER)?.trim()\n if (!sessionId) return next()\n // The browser evaluates this module, so the Node async scope loads inside `.server()`.\n const { runWithPostHogSession } = await import('./request-session.ts')\n return runWithPostHogSession(sessionId, () => next())\n },\n)\n"],"mappings":";;;AAGA,MAAM,iBAAiB;;;;;;AAOvB,MAAa,2BAA2B,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAC5E,OAAO,EAAE,SAAS,WAAW;CAC3B,MAAM,YAAY,QAAQ,QAAQ,IAAI,cAAc,CAAC,EAAE,KAAK;CAC5D,IAAI,CAAC,WAAW,OAAO,KAAK;CAE5B,MAAM,EAAE,0BAA0B,MAAM,OAAO;CAC/C,OAAO,sBAAsB,iBAAiB,KAAK,CAAC;AACtD,CACF"}
@@ -0,0 +1,3 @@
1
+ import { n as runWithPostHogSession, t as currentPostHogSession } from "./request-session-Qe_Q_u9G.js";
2
+
3
+ export { runWithPostHogSession };
@@ -0,0 +1,16 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ //#region src/infrastructure/posthog/request-session.ts
4
+ const storage = new AsyncLocalStorage();
5
+ /** Run the rest of one request with the browser's PostHog session ID. */
6
+ function runWithPostHogSession(sessionId, work) {
7
+ return storage.run(sessionId, work);
8
+ }
9
+ /** The current request's PostHog session ID, or undefined outside such a request. */
10
+ function currentPostHogSession() {
11
+ return storage.getStore();
12
+ }
13
+
14
+ //#endregion
15
+ export { runWithPostHogSession as n, currentPostHogSession as t };
16
+ //# sourceMappingURL=request-session-Qe_Q_u9G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-session-Qe_Q_u9G.js","names":[],"sources":["../src/infrastructure/posthog/request-session.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\n\n// One async scope per request keeps each browser session ID apart from concurrent requests.\nconst storage = new AsyncLocalStorage<string>()\n\n/** Run the rest of one request with the browser's PostHog session ID. */\nexport function runWithPostHogSession<T>(sessionId: string, work: () => T): T {\n return storage.run(sessionId, work)\n}\n\n/** The current request's PostHog session ID, or undefined outside such a request. */\nexport function currentPostHogSession(): string | undefined {\n return storage.getStore()\n}\n"],"mappings":";;;AAGA,MAAM,UAAU,IAAI,kBAA0B;;AAG9C,SAAgB,sBAAyB,WAAmB,MAAkB;CAC5E,OAAO,QAAQ,IAAI,WAAW,IAAI;AACpC;;AAGA,SAAgB,wBAA4C;CAC1D,OAAO,QAAQ,SAAS;AAC1B"}
@@ -97,6 +97,18 @@ export declare function createErrorMiddleware<Entries>(options: ErrorMiddlewareO
97
97
  toHttpErrorResponse: (cause: unknown) => Response;
98
98
  };
99
99
  //#endregion
100
+ //#region src/infrastructure/tanstack/csrf.middleware.d.ts
101
+ /** A public site origin that differs from the request URL. */
102
+ type CsrfMiddlewareOptions = {
103
+ readonly origin?: string;
104
+ };
105
+ /**
106
+ * Refuse a server function call that does not come from this site.
107
+ * Register the result in `requestMiddleware`. A custom start instance replaces TanStack's default check.
108
+ * A refusal logs `csrf_refused` and returns HTTP 403. This function does not throw.
109
+ */
110
+ export declare function createCsrfMiddleware(options?: CsrfMiddlewareOptions): import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
111
+ //#endregion
100
112
  //#region src/infrastructure/tanstack/function-tracing.middleware.d.ts
101
113
  /**
102
114
  * Run one server function call in a child span named for that function.
@@ -105,5 +117,5 @@ export declare function createErrorMiddleware<Entries>(options: ErrorMiddlewareO
105
117
  */
106
118
  export declare function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing): import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>;
107
119
  //#endregion
108
- export type { AuthContext, AuthCookieSource, AuthMiddlewareOptions, AuthSession, AuthenticatedContext, ErrorMiddlewareOptions, RequiredAuthContext };
120
+ export type { AuthContext, AuthCookieSource, AuthMiddlewareOptions, AuthSession, AuthenticatedContext, CsrfMiddlewareOptions, ErrorMiddlewareOptions, RequiredAuthContext };
109
121
  //# sourceMappingURL=tanstack.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCEQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,qBAAqB,gBAAgB;WACtC,MAAM;WACN,SAAS;WACT,gBAAgB;;;KAIf,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;WAI3D,mBAAmB,SAAS,qBAAqB;;;;;;;wBAc5C,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KChDrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB;;;;;;;;;;;;;;;;;;;;;wBCnB/D,gCAAgC,aAAa,SAAS,mBAAmB,0CAAO"}
1
+ {"version":3,"file":"tanstack.d.ts","names":[],"sources":["../src/infrastructure/tanstack/request-context.ts","../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/csrf.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"mappings":";;;;;;;;;;KAQY,iBAAiB;EAAmB;IAAU,sBAAsB;;IAC5E;;;;KCEQ;WACD;aAAiB;;WACjB;aAAoB;;;;KAInB,oBAAoB,gBAAgB;WACrC,MAAM;WACN,SAAS;;;KAIR,YAAY,gBAAgB,eACpC,oBAAoB;WAET;WACA;;;KAIH,qBAAqB,gBAAgB;WACtC,MAAM;WACN,SAAS;WACT,gBAAgB;;;KAIf,sBAAsB,gBAAgB;WACvC,UAAU,SAAS;aACjB;eACE,aAAa;QAAW,SAAS;YAAc,QAAQ;;;;WAI3D,mBAAmB,SAAS,qBAAqB;;;;;;;wBAc5C,qBAAqB,gBAAgB,aACnD,SAAS,sBAAsB;;;;;;;;KChDrB;WACD,UAAU;aAAmB,aAAa;;;;KAIzC,uBAAuB;WACxB,QAAQ,WAAW;;WAEnB;;WAEA,UAAU,SAAS,mBAAmB;;;;;;wBAOjC,sBAAsB,SAAS,SAAS,uBAAuB;;;;;;;;;;;;;;;;;KCtBnE;WACD;;;;;;;wBAQK,qBAAqB,UAAS,wDAA0B;;;;;;;;wBCNxD,gCAAgC,aAAa,SAAS,mBAAmB,0CAAO"}
package/dist/tanstack.js CHANGED
@@ -4,11 +4,11 @@ import { n as withSpan } from "./tracing-C_7B_odV.js";
4
4
  import { z } from "zod";
5
5
  import { getReasonPhrase } from "http-status-codes";
6
6
  import { isNotFound, isRedirect, redirect } from "@tanstack/react-router";
7
- import { createMiddleware } from "@tanstack/react-start";
7
+ import { createCsrfMiddleware as createCsrfMiddleware$1, createMiddleware } from "@tanstack/react-start";
8
8
  import { getSessionCookie, parseCookies } from "better-auth/cookies";
9
9
 
10
10
  //#region src/infrastructure/tanstack/auth.middleware.ts
11
- const logger$1 = createLogger("auth-middleware");
11
+ const logger$2 = createLogger("auth-middleware");
12
12
  const requestHeaders = async () => {
13
13
  const { getRequestHeaders } = await import("@tanstack/react-start/server");
14
14
  return getRequestHeaders();
@@ -33,7 +33,7 @@ function createAuthMiddleware(options) {
33
33
  const headers = await requestHeaders();
34
34
  resolved = await options.getAuth(context).api.getSession({ headers });
35
35
  } catch (error) {
36
- logger$1.error("get_session_failed", { error });
36
+ logger$2.error("get_session_failed", { error });
37
37
  resolved = null;
38
38
  }
39
39
  if (resolved === null) return next({ context: {
@@ -77,7 +77,7 @@ function createAuthMiddleware(options) {
77
77
 
78
78
  //#endregion
79
79
  //#region src/infrastructure/tanstack/resolve-failure.ts
80
- const logger = createLogger("error-boundary");
80
+ const logger$1 = createLogger("error-boundary");
81
81
  const SerializedIssuesSchema = z.array(z.object({
82
82
  message: z.string(),
83
83
  path: z.array(z.union([z.string(), z.number()]))
@@ -103,14 +103,14 @@ function resolveFailure(table, cause) {
103
103
  const failure = reconstructValidationError(cause) ?? cause;
104
104
  const resolved = table.resolve(failure);
105
105
  if (resolved !== null) {
106
- logger.warn("handled_error", {
106
+ logger$1.warn("handled_error", {
107
107
  tag: resolved.payload._tag,
108
108
  status: resolved.status
109
109
  });
110
110
  return resolved;
111
111
  }
112
112
  const classification = failure instanceof AppError ? failure.classification : "unknown";
113
- logger.error("unexpected_error", {
113
+ logger$1.error("unexpected_error", {
114
114
  error: failure,
115
115
  classification
116
116
  });
@@ -231,6 +231,36 @@ function createErrorMiddleware(options) {
231
231
  };
232
232
  }
233
233
 
234
+ //#endregion
235
+ //#region src/infrastructure/tanstack/csrf.middleware.ts
236
+ const logger = createLogger("csrf");
237
+ /**
238
+ * Refuse a server function call that does not come from this site.
239
+ * Register the result in `requestMiddleware`. A custom start instance replaces TanStack's default check.
240
+ * A refusal logs `csrf_refused` and returns HTTP 403. This function does not throw.
241
+ */
242
+ function createCsrfMiddleware(options = {}) {
243
+ const filter = ({ handlerType }) => handlerType === "serverFn";
244
+ const failureResponse = ({ request }) => {
245
+ logger.warn("csrf_refused", {
246
+ url: request.url,
247
+ origin: request.headers.get("Origin"),
248
+ secFetchSite: request.headers.get("Sec-Fetch-Site"),
249
+ referer: request.headers.get("Referer")
250
+ });
251
+ return new Response("Forbidden", { status: 403 });
252
+ };
253
+ if (options.origin === void 0) return createCsrfMiddleware$1({
254
+ filter,
255
+ failureResponse
256
+ });
257
+ return createCsrfMiddleware$1({
258
+ filter,
259
+ failureResponse,
260
+ origin: options.origin
261
+ });
262
+ }
263
+
234
264
  //#endregion
235
265
  //#region src/infrastructure/tanstack/function-tracing.middleware.ts
236
266
  /**
@@ -246,5 +276,5 @@ function createFunctionTracingMiddleware(getTracing) {
246
276
  }
247
277
 
248
278
  //#endregion
249
- export { createAuthMiddleware, createErrorMiddleware, createFunctionTracingMiddleware };
279
+ export { createAuthMiddleware, createCsrfMiddleware, createErrorMiddleware, createFunctionTracingMiddleware };
250
280
  //# sourceMappingURL=tanstack.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tanstack.js","names":["logger"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport { applyObservabilityUser } from '../../application/request-user.ts'\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** A resolved session and the request context that produced it. */\nexport type AuthenticatedContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n readonly requestContext: RequestContext\n}\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */\n readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n * A resolved session applies `{ id }` on the observability hook, then runs `onAuthenticated`, before the handler.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {\n applyObservabilityUser({ id: resolved.user.id })\n options.onAuthenticated?.({\n user: resolved.user,\n session: resolved.session,\n requestContext,\n })\n }\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n notifyAuthenticated(context, resolved)\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { z } from 'zod'\n\nimport {\n AppError,\n ValidationError,\n createLogger,\n type ErrorTable,\n type ResolvedError,\n} from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\n// A live ZodError keeps `.issues`. TanStack leaves only JSON in `Error.message`.\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (cause instanceof z.ZodError) return ValidationError.fromZod(cause)\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * A thrown ZodError, or issues serialized into an error message, becomes a ValidationError first.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const failure = reconstructValidationError(cause) ?? cause\n const resolved = table.resolve(failure)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = failure instanceof AppError ? failure.classification : 'unknown'\n logger.error('unexpected_error', { error: failure, classification })\n return table.fallback(failure)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import type { ErrorPayloadOf, ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n","import { createMiddleware } from '@tanstack/react-start'\n\nimport { withSpan } from '../cloudflare/tracing.ts'\nimport type { RequestContext } from './request-context.ts'\n\n/**\n * Run one server function call in a child span named for that function.\n * Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.\n * A handler rejection is not a span exception, and this middleware does not log.\n */\nexport function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing) {\n return createMiddleware({ type: 'function' }).server(({ context, serverFnMeta, next }) =>\n withSpan(\n getTracing(context),\n `server_fn ${serverFnMeta.name}`,\n {\n 'code.function.name': serverFnMeta.name,\n 'code.file.path': serverFnMeta.filename,\n },\n () => next(),\n ),\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tanstack.js","names":["logger","logger","createTanStackCsrfMiddleware"],"sources":["../src/infrastructure/tanstack/auth.middleware.ts","../src/infrastructure/tanstack/resolve-failure.ts","../src/infrastructure/tanstack/http-error-response.ts","../src/infrastructure/tanstack/session-cookies.ts","../src/infrastructure/tanstack/to-error-payload.ts","../src/infrastructure/tanstack/error.middleware.ts","../src/infrastructure/tanstack/csrf.middleware.ts","../src/infrastructure/tanstack/function-tracing.middleware.ts"],"sourcesContent":["import { createMiddleware } from '@tanstack/react-start'\nimport { getSessionCookie } from 'better-auth/cookies'\n\nimport { AuthenticationError, StaleSessionError, createLogger } from '@/core'\n\nimport { applyObservabilityUser } from '../../application/request-user.ts'\nimport type { RequestContext } from './request-context.ts'\n\nconst logger = createLogger('auth-middleware')\n\n/** The fields middleware reads; generic callers retain every additional session and user field. */\nexport type AuthSession = {\n readonly user: { readonly id: string }\n readonly session: { readonly id: string }\n}\n\n/** Authenticated context retains the application's complete user and session types. */\nexport type RequiredAuthContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n}\n\n/** Optional authentication supplies both fields together or neither field. */\nexport type AuthContext<Session extends AuthSession> =\n | RequiredAuthContext<Session>\n | {\n readonly user: undefined\n readonly session: undefined\n }\n\n/** A resolved session and the request context that produced it. */\nexport type AuthenticatedContext<Session extends AuthSession> = {\n readonly user: Session['user']\n readonly session: Session['session']\n readonly requestContext: RequestContext\n}\n\n/** Select the current request's Better Auth instance from TanStack's registered context. */\nexport type AuthMiddlewareOptions<Session extends AuthSession> = {\n readonly getAuth: (context: RequestContext) => {\n readonly api: {\n readonly getSession: (options: { headers: Headers }) => Promise<Session | null>\n }\n }\n /** Runs after a session resolves and before the handler. A guest or a failed lookup does not call it. */\n readonly onAuthenticated?: (context: AuthenticatedContext<Session>) => void\n}\n\n// The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily inside `.server()`.\nconst requestHeaders = async () => {\n const { getRequestHeaders } = await import('@tanstack/react-start/server')\n return getRequestHeaders()\n}\n\n/**\n * Preserve Porte's function and request middleware contracts with application-owned services.\n * Optional lookup failures log once and continue as guests. Required lookup failures propagate.\n * A resolved session applies `{ id }` on the observability hook, then runs `onAuthenticated`, before the handler.\n */\nexport function createAuthMiddleware<Session extends AuthSession>(\n options: AuthMiddlewareOptions<Session>,\n) {\n function notifyAuthenticated(requestContext: RequestContext, resolved: Session): void {\n applyObservabilityUser({ id: resolved.user.id })\n options.onAuthenticated?.({\n user: resolved.user,\n session: resolved.session,\n requestContext,\n })\n }\n const extractAuth = createMiddleware({ type: 'function' }).server<AuthContext<Session>>(\n async ({ next, context }) => {\n let resolved: Session | null\n try {\n const headers = await requestHeaders()\n resolved = await options.getAuth(context).api.getSession({ headers })\n } catch (error) {\n logger.error('get_session_failed', { error })\n resolved = null\n }\n if (resolved === null)\n return next<AuthContext<Session>>({ context: { user: undefined, session: undefined } })\n notifyAuthenticated(context, resolved)\n return next<AuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n\n async function resolveRequiredSession(\n context: RequestContext,\n headers: Headers,\n ): Promise<Session> {\n const resolved = await options.getAuth(context).api.getSession({ headers })\n if (resolved === null) {\n if (getSessionCookie(headers)) throw new StaleSessionError()\n throw new AuthenticationError()\n }\n return resolved\n }\n\n const requireAuth = createMiddleware({ type: 'function' }).server(async ({ next, context }) => {\n const resolved = await resolveRequiredSession(context, await requestHeaders())\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n })\n const requireAuthRequest = createMiddleware({ type: 'request' }).server(\n async ({ request, next, context }) => {\n const resolved = await resolveRequiredSession(context, request.headers)\n notifyAuthenticated(context, resolved)\n return next<RequiredAuthContext<Session>>({\n context: { user: resolved.user, session: resolved.session },\n })\n },\n )\n return { extractAuth, requireAuth, requireAuthRequest }\n}\n","import { z } from 'zod'\n\nimport {\n AppError,\n ValidationError,\n createLogger,\n type ErrorTable,\n type ResolvedError,\n} from '@/core'\nimport type { ErrorPayloadOf } from '@/core'\n\nconst logger = createLogger('error-boundary')\n\n// TanStack serializes a validator's issues into `Error.message` as JSON.\nconst SerializedIssuesSchema = z\n .array(z.object({ message: z.string(), path: z.array(z.union([z.string(), z.number()])) }))\n .min(1)\n\n// A live ZodError keeps `.issues`. TanStack leaves only JSON in `Error.message`.\nfunction reconstructValidationError(cause: unknown): ValidationError | null {\n if (cause instanceof z.ZodError) return ValidationError.fromZod(cause)\n if (!(cause instanceof Error)) return null\n let message: unknown\n try {\n message = JSON.parse(cause.message)\n } catch {\n return null\n }\n const parsed = SerializedIssuesSchema.safeParse(message)\n return parsed.success ? ValidationError.fromIssues(parsed.data) : null\n}\n\n/**\n * Resolve one failure through the table or fall back to the generic replacement, and log it once.\n * A thrown ZodError, or issues serialized into an error message, becomes a ValidationError first.\n * Both transport converters call this, so one rule decides the log level: mapped is a warning, unmapped is an error with the cause.\n */\nexport function resolveFailure<Entries>(\n table: ErrorTable<Entries>,\n cause: unknown,\n): ResolvedError<ErrorPayloadOf<ErrorTable<Entries>>> {\n const failure = reconstructValidationError(cause) ?? cause\n const resolved = table.resolve(failure)\n if (resolved !== null) {\n logger.warn('handled_error', {\n tag: resolved.payload._tag,\n status: resolved.status,\n })\n return resolved\n }\n const classification = failure instanceof AppError ? failure.classification : 'unknown'\n logger.error('unexpected_error', { error: failure, classification })\n return table.fallback(failure)\n}\n","import { getReasonPhrase } from 'http-status-codes'\nimport { z } from 'zod'\n\nimport type { ErrorTable, ProblemDetails } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n// A known delay becomes the header; a null delay sends no header.\nconst RetryAfterSchema = z.object({ retryAfter: z.number() })\n\n/**\n * Build the API route converter for one error table.\n * The body is RFC 9457 problem details; public fields become extension members.\n */\nexport function createHttpErrorResponseConverter<Entries>(table: ErrorTable<Entries>) {\n return function toHttpErrorResponse(cause: unknown): Response {\n const { status, payload } = resolveFailure(table, cause)\n const { _tag, message, ...fields } = payload\n const body: ProblemDetails = {\n type: 'about:blank',\n title: getReasonPhrase(status),\n detail: message,\n ...fields,\n }\n const headers = new Headers({ 'Content-Type': 'application/problem+json' })\n const retry = RetryAfterSchema.safeParse(payload)\n if (retry.success) headers.set('Retry-After', String(retry.data.retryAfter))\n return Response.json(body, { status, headers })\n }\n}\n","import type { deleteCookie } from '@tanstack/react-start/server'\nimport type { BetterAuthCookies } from 'better-auth'\nimport { parseCookies } from 'better-auth/cookies'\n\ntype AuthCookie = BetterAuthCookies['sessionToken']\ntype DeleteCookieOptions = NonNullable<Parameters<typeof deleteCookie>[1]>\n\n/**\n * Delete the session, session cache, account cache, and remember-me cookies of one instance.\n * Chunks present on the request go too. Names and attributes come from the instance, so custom prefixes and domain cookies clear.\n */\nexport async function clearStaleSessionCookies(cookies: BetterAuthCookies): Promise<void> {\n // The server entry loads react-dom/server; the browser evaluates this module, so it loads lazily here.\n const server = await import('@tanstack/react-start/server')\n const present = [...parseCookies(server.getRequestHeaders().get('cookie') ?? '').keys()]\n const definitions = [\n cookies.sessionToken,\n cookies.sessionData,\n cookies.accountData,\n cookies.dontRememberToken,\n ]\n for (const cookie of definitions) {\n // Better Auth names a chunk `${name}.${index}`, so a missed chunk would resurrect the value.\n const chunks = present.filter((name) => name.startsWith(`${cookie.name}.`))\n for (const name of new Set([cookie.name, ...chunks]))\n server.deleteCookie(name, deletionOf(cookie.attributes))\n }\n}\n\nfunction deletionOf(attributes: AuthCookie['attributes']): DeleteCookieOptions {\n // Better Auth sets `/` and the secure flag at runtime; the types leave both optional.\n const options: DeleteCookieOptions = {\n path: attributes.path ?? '/',\n secure: attributes.secure ?? false,\n }\n if (attributes.domain !== undefined) options.domain = attributes.domain\n return options\n}\n","import type { ErrorPayloadOf, ErrorTable } from '@/core'\n\nimport { resolveFailure } from './resolve-failure.ts'\n\n/**\n * Build the server-function converter for one error table.\n * The result is a plain object, because only own data survives serialization.\n */\nexport function createErrorPayloadConverter<Entries>(table: ErrorTable<Entries>) {\n return function toErrorPayload(cause: unknown): ErrorPayloadOf<ErrorTable<Entries>> {\n return resolveFailure(table, cause).payload\n }\n}\n","import { isNotFound, isRedirect, redirect } from '@tanstack/react-router'\nimport { createMiddleware } from '@tanstack/react-start'\nimport type { BetterAuthCookies } from 'better-auth'\n\nimport { StaleSessionError, type ErrorTable } from '@/core'\n\nimport { createHttpErrorResponseConverter } from './http-error-response.ts'\nimport type { RequestContext } from './request-context.ts'\nimport { clearStaleSessionCookies } from './session-cookies.ts'\nimport { createErrorPayloadConverter } from './to-error-payload.ts'\n\n/** The part of a Better Auth instance the boundary reads: its computed cookie definitions. */\nexport type AuthCookieSource = {\n readonly $context: Promise<{ readonly authCookies: BetterAuthCookies }>\n}\n\n/** The application's error table, its stale-session redirect, and its Better Auth instance. */\nexport type ErrorMiddlewareOptions<Entries> = {\n readonly errors: ErrorTable<Entries>\n /** The path a server function redirects to for a stale session, with any search string. */\n readonly signIn: string\n /** Select the request's Better Auth instance; its cookie definitions say what to clear. */\n readonly getAuth: (context: RequestContext) => AuthCookieSource\n}\n\n/**\n * Build both transport boundaries from one error table.\n * Register `functionErrorMiddleware` globally. Put `routeErrorMiddleware` first in an API route's `server.middleware`.\n */\nexport function createErrorMiddleware<Entries>(options: ErrorMiddlewareOptions<Entries>) {\n const toErrorPayload = createErrorPayloadConverter(options.errors)\n const toHttpErrorResponse = createHttpErrorResponseConverter(options.errors)\n\n async function clearSession(context: RequestContext): Promise<void> {\n const { authCookies } = await options.getAuth(context).$context\n await clearStaleSessionCookies(authCookies)\n }\n\n async function handleFunctionError(cause: unknown, context: RequestContext): Promise<never> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) {\n await clearSession(context)\n // oxlint-disable-next-line typescript/only-throw-error -- Router redirects by throwing this.\n throw redirect({ href: options.signIn })\n }\n // oxlint-disable-next-line typescript/only-throw-error -- The client rejects with this and reads its tag.\n throw toErrorPayload(cause)\n }\n\n // A route answers 401 instead of a redirect because fetch would follow it into the HTML shell.\n async function handleRouteError(cause: unknown, context: RequestContext): Promise<Response> {\n // oxlint-disable-next-line typescript/only-throw-error -- TanStack uses throwable control-flow objects.\n if (isNotFound(cause) || isRedirect(cause)) throw cause\n if (cause instanceof StaleSessionError) await clearSession(context)\n return toHttpErrorResponse(cause)\n }\n\n const functionErrorMiddleware = createMiddleware({ type: 'function' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleFunctionError(cause, context)\n }\n },\n )\n\n const routeErrorMiddleware = createMiddleware({ type: 'request' }).server(\n async ({ next, context }) => {\n try {\n return await next()\n } catch (cause) {\n return handleRouteError(cause, context)\n }\n },\n )\n\n return { functionErrorMiddleware, routeErrorMiddleware, toErrorPayload, toHttpErrorResponse }\n}\n","import { createCsrfMiddleware as createTanStackCsrfMiddleware } from '@tanstack/react-start'\n\nimport { createLogger } from '@/core'\n\nconst logger = createLogger('csrf')\n\n/** A public site origin that differs from the request URL. */\nexport type CsrfMiddlewareOptions = {\n readonly origin?: string\n}\n\n/**\n * Refuse a server function call that does not come from this site.\n * Register the result in `requestMiddleware`. A custom start instance replaces TanStack's default check.\n * A refusal logs `csrf_refused` and returns HTTP 403. This function does not throw.\n */\nexport function createCsrfMiddleware(options: CsrfMiddlewareOptions = {}) {\n // A page request has no same-origin proof. Start's own default check skips it too.\n const filter = ({ handlerType }: { handlerType: 'serverFn' | 'router' }) =>\n handlerType === 'serverFn'\n // A refusal never reaches the error middleware, so this boundary logs what the check saw.\n const failureResponse = ({ request }: { request: Request }) => {\n logger.warn('csrf_refused', {\n url: request.url,\n origin: request.headers.get('Origin'),\n secFetchSite: request.headers.get('Sec-Fetch-Site'),\n referer: request.headers.get('Referer'),\n })\n return new Response('Forbidden', { status: 403 })\n }\n if (options.origin === undefined) return createTanStackCsrfMiddleware({ filter, failureResponse })\n return createTanStackCsrfMiddleware({ filter, failureResponse, origin: options.origin })\n}\n","import { createMiddleware } from '@tanstack/react-start'\n\nimport { withSpan } from '../cloudflare/tracing.ts'\nimport type { RequestContext } from './request-context.ts'\n\n/**\n * Run one server function call in a child span named for that function.\n * Ported from Suparelay `tracing.middleware.ts`. `getTracing` runs on each request.\n * A handler rejection is not a span exception, and this middleware does not log.\n */\nexport function createFunctionTracingMiddleware(getTracing: (context: RequestContext) => Tracing) {\n return createMiddleware({ type: 'function' }).server(({ context, serverFnMeta, next }) =>\n withSpan(\n getTracing(context),\n `server_fn ${serverFnMeta.name}`,\n {\n 'code.function.name': serverFnMeta.name,\n 'code.file.path': serverFnMeta.filename,\n },\n () => next(),\n ),\n )\n}\n"],"mappings":";;;;;;;;;;AAQA,MAAMA,WAAS,aAAa,iBAAiB;AAyC7C,MAAM,iBAAiB,YAAY;CACjC,MAAM,EAAE,sBAAsB,MAAM,OAAO;CAC3C,OAAO,kBAAkB;AAC3B;;;;;;AAOA,SAAgB,qBACd,SACA;CACA,SAAS,oBAAoB,gBAAgC,UAAyB;EACpF,uBAAuB,EAAE,IAAI,SAAS,KAAK,GAAG,CAAC;EAC/C,QAAQ,kBAAkB;GACxB,MAAM,SAAS;GACf,SAAS,SAAS;GAClB;EACF,CAAC;CACH;CACA,MAAM,cAAc,iBAAiB,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,OACzD,OAAO,EAAE,MAAM,cAAc;EAC3B,IAAI;EACJ,IAAI;GACF,MAAM,UAAU,MAAM,eAAe;GACrC,WAAW,MAAM,QAAQ,QAAQ,OAAO,CAAC,CAAC,IAAI,WAAW,EAAE,QAAQ,CAAC;EACtE,SAAS,OAAO;GACd,SAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC;GAC5C,WAAW;EACb;EACA,IAAI,aAAa,MACf,OAAO,KAA2B,EAAE,SAAS;GAAE,MAAM;GAAW,SAAS;EAAU,EAAE,CAAC;EACxF,oBAAoB,SAAS,QAAQ;EACrC,OAAO,KAA2B,EAChC,SAAS;GAAE,MAAM,SAAS;GAAM,SAAS,SAAS;EAAQ,EAC5D,CAAC;CACH,CACF;CAEA,eAAe,uBACb,SACA,SACkB;EAClB,MAAM,WAAW,MAAM,QAAQ,QAAQ,OAAO,CAAC,CAAC,IAAI,WAAW,EAAE,QAAQ,CAAC;EAC1E,IAAI,aAAa,MAAM;GACrB,IAAI,iBAAiB,OAAO,GAAG,MAAM,IAAI,kBAAkB;GAC3D,MAAM,IAAI,oBAAoB;EAChC;EACA,OAAO;CACT;CAkBA,OAAO;EAAE;EAAa,aAhBF,iBAAiB,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,MAAM,cAAc;GAC7F,MAAM,WAAW,MAAM,uBAAuB,SAAS,MAAM,eAAe,CAAC;GAC7E,oBAAoB,SAAS,QAAQ;GACrC,OAAO,KAAmC,EACxC,SAAS;IAAE,MAAM,SAAS;IAAM,SAAS,SAAS;GAAQ,EAC5D,CAAC;EACH,CAUgC;EAAG,oBATR,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OAC/D,OAAO,EAAE,SAAS,MAAM,cAAc;GACpC,MAAM,WAAW,MAAM,uBAAuB,SAAS,QAAQ,OAAO;GACtE,oBAAoB,SAAS,QAAQ;GACrC,OAAO,KAAmC,EACxC,SAAS;IAAE,MAAM,SAAS;IAAM,SAAS,SAAS;GAAQ,EAC5D,CAAC;EACH,CAEkD;CAAE;AACxD;;;;AC3GA,MAAMC,WAAS,aAAa,gBAAgB;AAG5C,MAAM,yBAAyB,EAC5B,MAAM,EAAE,OAAO;CAAE,SAAS,EAAE,OAAO;CAAG,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAE,CAAC,CAAC,CAAC,CAC1F,IAAI,CAAC;AAGR,SAAS,2BAA2B,OAAwC;CAC1E,IAAI,iBAAiB,EAAE,UAAU,OAAO,gBAAgB,QAAQ,KAAK;CACrE,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI;CACJ,IAAI;EACF,UAAU,KAAK,MAAM,MAAM,OAAO;CACpC,QAAQ;EACN,OAAO;CACT;CACA,MAAM,SAAS,uBAAuB,UAAU,OAAO;CACvD,OAAO,OAAO,UAAU,gBAAgB,WAAW,OAAO,IAAI,IAAI;AACpE;;;;;;AAOA,SAAgB,eACd,OACA,OACoD;CACpD,MAAM,UAAU,2BAA2B,KAAK,KAAK;CACrD,MAAM,WAAW,MAAM,QAAQ,OAAO;CACtC,IAAI,aAAa,MAAM;EACrB,SAAO,KAAK,iBAAiB;GAC3B,KAAK,SAAS,QAAQ;GACtB,QAAQ,SAAS;EACnB,CAAC;EACD,OAAO;CACT;CACA,MAAM,iBAAiB,mBAAmB,WAAW,QAAQ,iBAAiB;CAC9E,SAAO,MAAM,oBAAoB;EAAE,OAAO;EAAS;CAAe,CAAC;CACnE,OAAO,MAAM,SAAS,OAAO;AAC/B;;;;AC7CA,MAAM,mBAAmB,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;;;;;AAM5D,SAAgB,iCAA0C,OAA4B;CACpF,OAAO,SAAS,oBAAoB,OAA0B;EAC5D,MAAM,EAAE,QAAQ,YAAY,eAAe,OAAO,KAAK;EACvD,MAAM,EAAE,MAAM,SAAS,GAAG,WAAW;EACrC,MAAM,OAAuB;GAC3B,MAAM;GACN,OAAO,gBAAgB,MAAM;GAC7B,QAAQ;GACR,GAAG;EACL;EACA,MAAM,UAAU,IAAI,QAAQ,EAAE,gBAAgB,2BAA2B,CAAC;EAC1E,MAAM,QAAQ,iBAAiB,UAAU,OAAO;EAChD,IAAI,MAAM,SAAS,QAAQ,IAAI,eAAe,OAAO,MAAM,KAAK,UAAU,CAAC;EAC3E,OAAO,SAAS,KAAK,MAAM;GAAE;GAAQ;EAAQ,CAAC;CAChD;AACF;;;;;;;;AClBA,eAAsB,yBAAyB,SAA2C;CAExF,MAAM,SAAS,MAAM,OAAO;CAC5B,MAAM,UAAU,CAAC,GAAG,aAAa,OAAO,kBAAkB,CAAC,CAAC,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC;CACvF,MAAM,cAAc;EAClB,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV;CACA,KAAK,MAAM,UAAU,aAAa;EAEhC,MAAM,SAAS,QAAQ,QAAQ,SAAS,KAAK,WAAW,GAAG,OAAO,KAAK,EAAE,CAAC;EAC1E,KAAK,MAAM,wBAAQ,IAAI,IAAI,CAAC,OAAO,MAAM,GAAG,MAAM,CAAC,GACjD,OAAO,aAAa,MAAM,WAAW,OAAO,UAAU,CAAC;CAC3D;AACF;AAEA,SAAS,WAAW,YAA2D;CAE7E,MAAM,UAA+B;EACnC,MAAM,WAAW,QAAQ;EACzB,QAAQ,WAAW,UAAU;CAC/B;CACA,IAAI,WAAW,WAAW,QAAW,QAAQ,SAAS,WAAW;CACjE,OAAO;AACT;;;;;;;;AC7BA,SAAgB,4BAAqC,OAA4B;CAC/E,OAAO,SAAS,eAAe,OAAqD;EAClF,OAAO,eAAe,OAAO,KAAK,CAAC,CAAC;CACtC;AACF;;;;;;;;ACiBA,SAAgB,sBAA+B,SAA0C;CACvF,MAAM,iBAAiB,4BAA4B,QAAQ,MAAM;CACjE,MAAM,sBAAsB,iCAAiC,QAAQ,MAAM;CAE3E,eAAe,aAAa,SAAwC;EAClE,MAAM,EAAE,gBAAgB,MAAM,QAAQ,QAAQ,OAAO,CAAC,CAAC;EACvD,MAAM,yBAAyB,WAAW;CAC5C;CAEA,eAAe,oBAAoB,OAAgB,SAAyC;EAE1F,IAAI,WAAW,KAAK,KAAK,WAAW,KAAK,GAAG,MAAM;EAClD,IAAI,iBAAiB,mBAAmB;GACtC,MAAM,aAAa,OAAO;GAE1B,MAAM,SAAS,EAAE,MAAM,QAAQ,OAAO,CAAC;EACzC;EAEA,MAAM,eAAe,KAAK;CAC5B;CAGA,eAAe,iBAAiB,OAAgB,SAA4C;EAE1F,IAAI,WAAW,KAAK,KAAK,WAAW,KAAK,GAAG,MAAM;EAClD,IAAI,iBAAiB,mBAAmB,MAAM,aAAa,OAAO;EAClE,OAAO,oBAAoB,KAAK;CAClC;CAsBA,OAAO;EAAE,yBApBuB,iBAAiB,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,OACrE,OAAO,EAAE,MAAM,cAAc;GAC3B,IAAI;IACF,OAAO,MAAM,KAAK;GACpB,SAAS,OAAO;IACd,OAAO,oBAAoB,OAAO,OAAO;GAC3C;EACF,CAa6B;EAAG,sBAVL,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,OACjE,OAAO,EAAE,MAAM,cAAc;GAC3B,IAAI;IACF,OAAO,MAAM,KAAK;GACpB,SAAS,OAAO;IACd,OAAO,iBAAiB,OAAO,OAAO;GACxC;EACF,CAGmD;EAAG;EAAgB;CAAoB;AAC9F;;;;AC3EA,MAAM,SAAS,aAAa,MAAM;;;;;;AAYlC,SAAgB,qBAAqB,UAAiC,CAAC,GAAG;CAExE,MAAM,UAAU,EAAE,kBAChB,gBAAgB;CAElB,MAAM,mBAAmB,EAAE,cAAoC;EAC7D,OAAO,KAAK,gBAAgB;GAC1B,KAAK,QAAQ;GACb,QAAQ,QAAQ,QAAQ,IAAI,QAAQ;GACpC,cAAc,QAAQ,QAAQ,IAAI,gBAAgB;GAClD,SAAS,QAAQ,QAAQ,IAAI,SAAS;EACxC,CAAC;EACD,OAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;CAClD;CACA,IAAI,QAAQ,WAAW,QAAW,OAAOC,uBAA6B;EAAE;EAAQ;CAAgB,CAAC;CACjG,OAAOA,uBAA6B;EAAE;EAAQ;EAAiB,QAAQ,QAAQ;CAAO,CAAC;AACzF;;;;;;;;;ACtBA,SAAgB,gCAAgC,YAAkD;CAChG,OAAO,iBAAiB,EAAE,MAAM,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE,SAAS,cAAc,WAC7E,SACE,WAAW,OAAO,GAClB,aAAa,aAAa,QAC1B;EACE,sBAAsB,aAAa;EACnC,kBAAkB,aAAa;CACjC,SACM,KAAK,CACb,CACF;AACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "better-ship",
4
- "version": "0.44.0",
4
+ "version": "0.46.0",
5
5
  "description": "Foundation for applications on Cloudflare Workers: logger, errors, message bus, infrastructure adapters, and UI primitives",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -52,6 +52,7 @@
52
52
  "./posthog/metrics": "./dist/posthog/metrics.js",
53
53
  "./posthog/proxy": "./dist/posthog/proxy.js",
54
54
  "./posthog/server": "./dist/posthog/server.js",
55
+ "./posthog/tanstack": "./dist/posthog/tanstack.js",
55
56
  "./resend": "./dist/resend.js",
56
57
  "./sentry/client": "./dist/sentry/client.js",
57
58
  "./sentry/server": "./dist/sentry/server.js",
@@ -0,0 +1,14 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+
3
+ // One async scope per request keeps each browser session ID apart from concurrent requests.
4
+ const storage = new AsyncLocalStorage<string>()
5
+
6
+ /** Run the rest of one request with the browser's PostHog session ID. */
7
+ export function runWithPostHogSession<T>(sessionId: string, work: () => T): T {
8
+ return storage.run(sessionId, work)
9
+ }
10
+
11
+ /** The current request's PostHog session ID, or undefined outside such a request. */
12
+ export function currentPostHogSession(): string | undefined {
13
+ return storage.getStore()
14
+ }
@@ -10,6 +10,7 @@ import {
10
10
  } from '@/core'
11
11
 
12
12
  import { PostHogMetricsService } from './metrics.ts'
13
+ import { currentPostHogSession } from './request-session.ts'
13
14
 
14
15
  // Fixed forever: changing it changes every derived UUID, so PostHog would keep a repeat.
15
16
  const EVENT_ID_NAMESPACE = '53d7af30-65cb-4176-b2c9-f1ed42367ab1'
@@ -98,7 +99,11 @@ export class PostHogAnalyticsService implements AnalyticsService {
98
99
  target: AnalyticsTarget,
99
100
  profile: 'person' | 'anonymous',
100
101
  ): void {
101
- const properties = { ...eventProperties }
102
+ const sessionId = currentPostHogSession()
103
+ // An application's own `$session_id` wins over the request's.
104
+ const properties = sessionId
105
+ ? { $session_id: sessionId, ...eventProperties }
106
+ : { ...eventProperties }
102
107
  const payload: Parameters<PostHog['captureImmediate']>[0] = {
103
108
  distinctId: target.distinctId,
104
109
  event: name,
@@ -0,0 +1,19 @@
1
+ import { createMiddleware } from '@tanstack/react-start'
2
+
3
+ // posthog-js sends this header when the browser sets `tracing_headers` for the application host.
4
+ const SESSION_HEADER = 'x-posthog-session-id'
5
+
6
+ /**
7
+ * Link Worker events to the browser session that sent the request.
8
+ * Register it in `requestMiddleware`. Each `PostHogAnalyticsService` event in that request then
9
+ * gets `$session_id`. A request without the header, and queue, cron, and Workflow code, add nothing.
10
+ */
11
+ export const posthogRequestMiddleware = createMiddleware({ type: 'request' }).server(
12
+ async ({ request, next }) => {
13
+ const sessionId = request.headers.get(SESSION_HEADER)?.trim()
14
+ if (!sessionId) return next()
15
+ // The browser evaluates this module, so the Node async scope loads inside `.server()`.
16
+ const { runWithPostHogSession } = await import('./request-session.ts')
17
+ return runWithPostHogSession(sessionId, () => next())
18
+ },
19
+ )
@@ -0,0 +1,33 @@
1
+ import { createCsrfMiddleware as createTanStackCsrfMiddleware } from '@tanstack/react-start'
2
+
3
+ import { createLogger } from '@/core'
4
+
5
+ const logger = createLogger('csrf')
6
+
7
+ /** A public site origin that differs from the request URL. */
8
+ export type CsrfMiddlewareOptions = {
9
+ readonly origin?: string
10
+ }
11
+
12
+ /**
13
+ * Refuse a server function call that does not come from this site.
14
+ * Register the result in `requestMiddleware`. A custom start instance replaces TanStack's default check.
15
+ * A refusal logs `csrf_refused` and returns HTTP 403. This function does not throw.
16
+ */
17
+ export function createCsrfMiddleware(options: CsrfMiddlewareOptions = {}) {
18
+ // A page request has no same-origin proof. Start's own default check skips it too.
19
+ const filter = ({ handlerType }: { handlerType: 'serverFn' | 'router' }) =>
20
+ handlerType === 'serverFn'
21
+ // A refusal never reaches the error middleware, so this boundary logs what the check saw.
22
+ const failureResponse = ({ request }: { request: Request }) => {
23
+ logger.warn('csrf_refused', {
24
+ url: request.url,
25
+ origin: request.headers.get('Origin'),
26
+ secFetchSite: request.headers.get('Sec-Fetch-Site'),
27
+ referer: request.headers.get('Referer'),
28
+ })
29
+ return new Response('Forbidden', { status: 403 })
30
+ }
31
+ if (options.origin === undefined) return createTanStackCsrfMiddleware({ filter, failureResponse })
32
+ return createTanStackCsrfMiddleware({ filter, failureResponse, origin: options.origin })
33
+ }
@@ -11,4 +11,5 @@ export {
11
11
  type AuthCookieSource,
12
12
  type ErrorMiddlewareOptions,
13
13
  } from './error.middleware.ts'
14
+ export { createCsrfMiddleware, type CsrfMiddlewareOptions } from './csrf.middleware.ts'
14
15
  export { createFunctionTracingMiddleware } from './function-tracing.middleware.ts'