@spfn/core 0.3.0-beta.7 → 0.3.0-beta.9
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/dist/{boss-Caj5dGMm.d.ts → boss-BiDCbbAQ.d.ts} +13 -4
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +28 -7
- package/dist/db/index.js +103 -102
- package/dist/db/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +15 -2
- package/dist/event/sse/client.js +38 -2
- package/dist/event/sse/client.js.map +1 -1
- package/dist/job/index.d.ts +2 -2
- package/dist/nextjs/index.d.ts +20 -1
- package/dist/nextjs/index.js +1 -1
- package/dist/server/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/db/README.md +1 -1
- package/src/db/schema/README.md +14 -4
- package/src/event/README.md +6 -1
- package/src/job/README.md +4 -0
|
@@ -95,10 +95,23 @@ interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'> {
|
|
|
95
95
|
*/
|
|
96
96
|
rpcBaseUrl?: string;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Headers of the one-time token request.
|
|
100
|
+
*
|
|
101
|
+
* The token route is a cookie-authenticated POST through the RPC proxy, so the
|
|
102
|
+
* @spfn/auth CSRF rule applies to it exactly as to any other mutation: the
|
|
103
|
+
* readable `spfn_csrf` cookie family is mirrored into `x-spfn-csrf`, the way
|
|
104
|
+
* the api client does. Without the header, `SPFN_AUTH_CSRF=enforce` refuses
|
|
105
|
+
* the request and the stream never opens.
|
|
106
|
+
*
|
|
107
|
+
* @internal Exported for tests
|
|
108
|
+
*/
|
|
109
|
+
declare function tokenRequestHeaders(cookies?: Iterable<[string, string]>): Record<string, string>;
|
|
98
110
|
/**
|
|
99
111
|
* Create SSE client with built-in token authentication
|
|
100
112
|
*
|
|
101
|
-
* Acquires one-time SSE tokens via RPC proxy automatically
|
|
113
|
+
* Acquires one-time SSE tokens via RPC proxy automatically, sending the
|
|
114
|
+
* `x-spfn-csrf` header whenever a `spfn_csrf` cookie is present.
|
|
102
115
|
* Requires eventRouteMap to be merged into RPC proxy config.
|
|
103
116
|
*
|
|
104
117
|
* @example
|
|
@@ -118,4 +131,4 @@ interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'> {
|
|
|
118
131
|
*/
|
|
119
132
|
declare function createAuthSSEClient<TRouter extends EventRouterDef<any>>(config?: AuthSSEClientConfig): SSEClient<TRouter>;
|
|
120
133
|
|
|
121
|
-
export { type AuthSSEClientConfig, type SSEClient, createAuthSSEClient, createSSEClient, subscribeToEvents };
|
|
134
|
+
export { type AuthSSEClientConfig, type SSEClient, createAuthSSEClient, createSSEClient, subscribeToEvents, tokenRequestHeaders };
|
package/dist/event/sse/client.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
// src/nextjs/client/csrf.ts
|
|
2
|
+
var CSRF_HEADER = "x-spfn-csrf";
|
|
3
|
+
var CSRF_COOKIE_PATTERN = /^spfn_csrf(_\d+)?$/;
|
|
4
|
+
var HEADER_SAFE_VALUE = /^[!-~]+$/;
|
|
5
|
+
var MAX_CANDIDATES = 32;
|
|
6
|
+
function selectCandidates(values) {
|
|
7
|
+
const unique = Array.from(new Set(values));
|
|
8
|
+
if (unique.length <= MAX_CANDIDATES) {
|
|
9
|
+
return unique;
|
|
10
|
+
}
|
|
11
|
+
const half = MAX_CANDIDATES / 2;
|
|
12
|
+
return [...unique.slice(0, half), ...unique.slice(-half)];
|
|
13
|
+
}
|
|
14
|
+
function csrfHeaderValue(cookies) {
|
|
15
|
+
const values = Array.from(cookies).filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name) && HEADER_SAFE_VALUE.test(value) && !value.includes(",")).map(([, value]) => value);
|
|
16
|
+
const candidates = selectCandidates(values);
|
|
17
|
+
return candidates.length > 0 ? candidates.join(",") : void 0;
|
|
18
|
+
}
|
|
19
|
+
function documentCookieEntries() {
|
|
20
|
+
if (typeof document === "undefined" || !document.cookie) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const entries = [];
|
|
24
|
+
for (const pair of document.cookie.split(";")) {
|
|
25
|
+
const separator = pair.indexOf("=");
|
|
26
|
+
if (separator > 0) {
|
|
27
|
+
entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
|
|
1
33
|
// src/event/sse/client.ts
|
|
2
34
|
var SSE_DEFAULTS = {
|
|
3
35
|
host: typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SPFN_API_URL || "http://localhost:8790" : "http://localhost:8790",
|
|
@@ -173,6 +205,10 @@ function subscribeToEvents(events, handlers, options) {
|
|
|
173
205
|
handlers
|
|
174
206
|
});
|
|
175
207
|
}
|
|
208
|
+
function tokenRequestHeaders(cookies = documentCookieEntries()) {
|
|
209
|
+
const csrf = csrfHeaderValue(cookies);
|
|
210
|
+
return csrf === void 0 ? { "Content-Type": "application/json" } : { "Content-Type": "application/json", [CSRF_HEADER]: csrf };
|
|
211
|
+
}
|
|
176
212
|
function createAuthSSEClient(config = {}) {
|
|
177
213
|
const { rpcBaseUrl = "/api/rpc", ...sseConfig } = config;
|
|
178
214
|
return createSSEClient({
|
|
@@ -181,7 +217,7 @@ function createAuthSSEClient(config = {}) {
|
|
|
181
217
|
const res = await fetch(`${rpcBaseUrl}/eventsToken`, {
|
|
182
218
|
method: "POST",
|
|
183
219
|
credentials: "include",
|
|
184
|
-
headers:
|
|
220
|
+
headers: tokenRequestHeaders(),
|
|
185
221
|
body: JSON.stringify({})
|
|
186
222
|
});
|
|
187
223
|
if (!res.ok) {
|
|
@@ -193,6 +229,6 @@ function createAuthSSEClient(config = {}) {
|
|
|
193
229
|
});
|
|
194
230
|
}
|
|
195
231
|
|
|
196
|
-
export { createAuthSSEClient, createSSEClient, subscribeToEvents };
|
|
232
|
+
export { createAuthSSEClient, createSSEClient, subscribeToEvents, tokenRequestHeaders };
|
|
197
233
|
//# sourceMappingURL=client.js.map
|
|
198
234
|
//# sourceMappingURL=client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/event/sse/client.ts"],"names":[],"mappings":";AAqEA,IAAM,YAAA,GAAe;AAAA,EACjB,MAAM,OAAO,OAAA,KAAY,cAClB,OAAA,CAAQ,GAAA,CAAI,4BAA4B,uBAAA,GACzC,uBAAA;AAAA,EACN,QAAA,EAAU;AACd,CAAA;AAsDO,SAAS,eAAA,CACZ,MAAA,GAA0B,EAAC,EAE/B;AACI,EAAA,MAAM;AAAA,IACF,GAAA;AAAA,IACA,OAAO,YAAA,CAAa,IAAA;AAAA,IACpB,WAAW,YAAA,CAAa,QAAA;AAAA,IACxB,SAAA,GAAY,IAAA;AAAA,IACZ,cAAA,GAAiB,GAAA;AAAA,IACjB,oBAAA,GAAuB,CAAA;AAAA,IACvB,eAAA,GAAkB,KAAA;AAAA,IAClB;AAAA,GACJ,GAAI,MAAA;AAGJ,EAAA,MAAM,OAAA,GAAU,GAAA,IAAO,CAAA,EAAG,IAAI,GAAG,QAAQ,CAAA,CAAA;AAEzC,EAAA,IAAI,KAAA,GAA4B,QAAA;AAChC,EAAA,IAAI,MAAA,GAA+B,IAAA;AAInC,EAAA,SAAS,UAAU,IAAA,EACnB;AACI,IAAA,IAAI,KAAK,MAAA,EACT;AACI,MAAA;AAAA,IACJ;AAEA,IAAA,IAAA,CAAK,MAAA,GAAS,IAAA;AAEd,IAAA,IAAI,KAAK,cAAA,EACT;AACI,MAAA,YAAA,CAAa,KAAK,cAAc,CAAA;AAChC,MAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AAAA,IAC1B;AAEA,IAAA,IAAI,KAAK,WAAA,EACT;AACI,MAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,MAAA,IAAA,CAAK,WAAA,GAAc,IAAA;AAAA,IACvB;AAEA,IAAA,IAAI,WAAW,IAAA,EACf;AACI,MAAA,MAAA,GAAS,IAAA;AACT,MAAA,KAAA,GAAQ,QAAA;AAAA,IACZ;AAEA,IAAA,IAAA,CAAK,OAAA,IAAU;AAAA,EACnB;AAEA,EAAA,SAAS,UAAU,OAAA,EACnB;AACI,IAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAU,QAAQ,OAAA,EAAS,WAAA,EAAa,SAAQ,GAAI,OAAA;AAGpE,IAAA,IAAI,MAAA,EACJ;AACI,MAAA,SAAA,CAAU,MAAM,CAAA;AAAA,IACpB;AAEA,IAAA,MAAM,IAAA,GAAsB;AAAA,MACxB,WAAA,EAAa,IAAA;AAAA,MACb,cAAA,EAAgB,IAAA;AAAA,MAChB,iBAAA,EAAmB,CAAA;AAAA,MACnB,MAAA,EAAQ,KAAA;AAAA,MACR;AAAA,KACJ;AACA,IAAA,MAAA,GAAS,IAAA;AAET,IAAA,MAAM,UAAA,GAAa,MAAA;AAEnB,IAAA,SAAS,OAAA,GACT;AACI,MAAA,KAAA,GAAQ,YAAA;AAER,MAAA,MAAM,OAAO,YACb;AACI,QAAA,IAAI,UAAA,GAAa,EAAA;AAEjB,QAAA,IAAI,YAAA,EACJ;AACI,UAAA,MAAM,KAAA,GAAQ,MAAM,YAAA,EAAa;AAIjC,UAAA,IAAI,KAAK,MAAA,EACT;AACI,YAAA;AAAA,UACJ;AAEA,UAAA,UAAA,GAAa,CAAA,OAAA,EAAU,kBAAA,CAAmB,KAAK,CAAC,CAAA,CAAA;AAAA,QACpD;AAEA,QAAA,IAAI,KAAK,MAAA,EACT;AACI,UAAA;AAAA,QACJ;AAEA,QAAA,MAAM,SAAA,GAAY,GAAG,OAAO,CAAA,QAAA,EAAW,WAAW,IAAA,CAAK,GAAG,CAAC,CAAA,EAAG,UAAU,CAAA,CAAA;AAExE,QAAA,IAAA,CAAK,WAAA,GAAc,IAAI,WAAA,CAAY,SAAA,EAAW;AAAA,UAC1C;AAAA,SACH,CAAA;AAED,QAAA,kBAAA,CAAmB,IAAA,CAAK,WAAA,EAAa,UAAA,EAAY,QAAA,EAAU,QAAQ,OAAO,CAAA;AAC1E,QAAA,cAAA,CAAe,WAAW,CAAA;AAAA,MAC9B,CAAA;AAEA,MAAA,IAAA,EAAK,CAAE,MAAM,MACb;AAEI,QAAA,IAAI,KAAK,MAAA,EACT;AACI,UAAA;AAAA,QACJ;AAEA,QAAA,KAAA,GAAQ,OAAA;AACR,QAAA,gBAAA,CAAiB,WAAW,CAAA;AAAA,MAChC,CAAC,CAAA;AAAA,IACL;AAEA,IAAA,SAAS,kBAAA,CACL,EAAA,EACA,KAAA,EACA,UAAA,EACA,UACA,SAAA,EAEJ;AACI,MAAA,EAAA,CAAG,SAAS,MACZ;AACI,QAAA,KAAA,GAAQ,MAAA;AACR,QAAA,IAAA,CAAK,iBAAA,GAAoB,CAAA;AACzB,QAAA,QAAA,IAAW;AAAA,MACf,CAAA;AAEA,MAAA,EAAA,CAAG,OAAA,GAAU,CAAC,KAAA,KACd;AACI,QAAA,KAAA,GAAQ,OAAA;AACR,QAAA,SAAA,GAAY,KAAK,CAAA;AAAA,MACrB,CAAA;AAGA,MAAA,EAAA,CAAG,gBAAA,CAAiB,WAAA,EAAa,CAAC,CAAA,KAClC;AACI,QAAA,IACA;AACI,UAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,IAAI,CAAA;AAC9B,UAAA,OAAA,CAAQ,KAAA,CAAM,oBAAoB,IAAI,CAAA;AAAA,QAC1C,CAAA,CAAA,MAEA;AAAA,QAEA;AAAA,MACJ,CAAC,CAAA;AAGD,MAAA,EAAA,CAAG,gBAAA,CAAiB,QAAQ,MAC5B;AAAA,MAEA,CAAC,CAAA;AAGD,MAAA,KAAA,MAAW,aAAa,KAAA,EACxB;AACI,QAAA,MAAM,OAAA,GAAW,WAAwE,SAAS,CAAA;AAElG,QAAA,IAAI,CAAC,OAAA,EACL;AACI,UAAA;AAAA,QACJ;AAEA,QAAA,EAAA,CAAG,gBAAA,CAAiB,SAAA,EAAW,CAAC,CAAA,KAChC;AACI,UAAA,IACA;AACI,YAAA,MAAM,OAAA,GAAsB,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,IAAI,CAAA;AAC7C,YAAA,OAAA,CAAQ,QAAQ,IAAI,CAAA;AAAA,UACxB,SACO,GAAA,EACP;AACI,YAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,6BAAA,EAAgC,SAAS,CAAA,EAAA,CAAA,EAAM,GAAG,CAAA;AAAA,UACpE;AAAA,QACJ,CAAC,CAAA;AAAA,MACL;AAAA,IACJ;AAEA,IAAA,SAAS,eAAe,aAAA,EACxB;AACI,MAAA,IAAI,CAAC,KAAK,WAAA,EACV;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,MAAM,YAAY,IAAA,CAAK,WAAA;AACvB,MAAA,MAAM,kBAAkB,SAAA,CAAU,OAAA;AAElC,MAAA,SAAA,CAAU,OAAA,GAAU,CAAC,KAAA,KACrB;AACI,QAAA,IAAI,eAAA,EACJ;AACI,UAAC,gBAAwC,KAAK,CAAA;AAAA,QAClD;AAIA,QAAA,IAAI,aAAa,YAAA,EACjB;AACI,UAAA,SAAA,CAAU,KAAA,EAAM;AAChB,UAAA,gBAAA,CAAiB,aAAa,CAAA;AAAA,QAClC,CAAA,MAAA,IACS,SAAA,IAAa,SAAA,CAAU,UAAA,KAAe,YAAY,MAAA,EAC3D;AACI,UAAA,gBAAA,CAAiB,aAAa,CAAA;AAAA,QAClC;AAAA,MACJ,CAAA;AAAA,IACJ;AAEA,IAAA,SAAS,iBAAiB,aAAA,EAC1B;AACI,MAAA,IAAI,IAAA,CAAK,MAAA,IAAU,CAAC,SAAA,EACpB;AACI,QAAA;AAAA,MACJ;AAEA,MAAA,IAAI,oBAAA,GAAuB,CAAA,IAAK,IAAA,CAAK,iBAAA,IAAqB,oBAAA,EAC1D;AACI,QAAA,SAAA,CAAU,IAAI,CAAA;AAEd,QAAA;AAAA,MACJ;AAEA,MAAA,IAAA,CAAK,iBAAA,EAAA;AACL,MAAA,aAAA,GAAgB,KAAK,iBAAiB,CAAA;AAEtC,MAAA,IAAA,CAAK,cAAA,GAAiB,WAAW,MACjC;AACI,QAAA,OAAA,EAAQ;AAAA,MACZ,GAAG,cAAc,CAAA;AAAA,IACrB;AAGA,IAAA,OAAA,EAAQ;AAGR,IAAA,OAAO,MACP;AACI,MAAA,SAAA,CAAU,IAAI,CAAA;AAAA,IAClB,CAAA;AAAA,EACJ;AAEA,EAAA,SAAS,QAAA,GACT;AACI,IAAA,OAAO,KAAA;AAAA,EACX;AAEA,EAAA,SAAS,KAAA,GACT;AACI,IAAA,IAAI,MAAA,EACJ;AACI,MAAA,SAAA,CAAU,MAAM,CAAA;AAAA,IACpB,CAAA,MAEA;AACI,MAAA,KAAA,GAAQ,QAAA;AAAA,IACZ;AAAA,EACJ;AAEA,EAAA,OAAO;AAAA,IACH,SAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACJ;AACJ;AA2BO,SAAS,iBAAA,CACZ,MAAA,EACA,QAAA,EACA,OAAA,EAEJ;AACI,EAAA,MAAM,MAAA,GAAS,gBAAyB,OAAO,CAAA;AAE/C,EAAA,OAAO,OAAO,SAAA,CAAU;AAAA,IACpB,MAAA;AAAA,IACA;AAAA,GACH,CAAA;AACL;AAyCO,SAAS,mBAAA,CACZ,MAAA,GAA8B,EAAC,EAEnC;AACI,EAAA,MAAM,EAAE,UAAA,GAAa,UAAA,EAAY,GAAG,WAAU,GAAI,MAAA;AAElD,EAAA,OAAO,eAAA,CAAyB;AAAA,IAC5B,GAAG,SAAA;AAAA,IACH,cAAc,YACd;AACI,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,UAAU,CAAA,YAAA,CAAA,EAAgB;AAAA,QACjD,MAAA,EAAQ,MAAA;AAAA,QACR,WAAA,EAAa,SAAA;AAAA,QACb,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAAA,QAC9C,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,EAAE;AAAA,OAC1B,CAAA;AAED,MAAA,IAAI,CAAC,IAAI,EAAA,EACT;AACI,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,GAAA,CAAI,MAAM,CAAA,CAAE,CAAA;AAAA,MAChE;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAE5B,MAAA,OAAO,IAAA,CAAK,KAAA;AAAA,IAChB;AAAA,GACH,CAAA;AACL","file":"client.js","sourcesContent":["/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose } = options;\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/nextjs/client/csrf.ts","../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * CSRF token transport for the RPC client\n *\n * The Next.js proxy sets a readable (non-HttpOnly) cookie carrying an HMAC of\n * the session's key id; this module mirrors it into a request header. The proxy\n * never compares cookie against header — it recomputes the value from the\n * session — so the cookie is transport only and reading it here is not a trust\n * decision. See the CSRF section of the @spfn/auth README.\n */\n\n/**\n * Header the readable CSRF cookie is mirrored into.\n *\n * Must match CSRF_HEADER in @spfn/auth. It is duplicated rather than imported\n * because @spfn/core cannot depend on @spfn/auth.\n */\nexport const CSRF_HEADER = 'x-spfn-csrf';\n\n/**\n * Cookie names carrying a CSRF token.\n *\n * @spfn/auth suffixes its cookie names with the server port so several local dev\n * instances on the same host do not overwrite each other's session, and the\n * browser cannot know that port — so match the family, not one name. Every\n * candidate is sent and the proxy accepts the request if any of them recomputes\n * to the expected value; a candidate an attacker tossed in never will.\n */\nconst CSRF_COOKIE_PATTERN = /^spfn_csrf(_\\d+)?$/;\n\n/**\n * Values that can be put in a header at all: printable ASCII, no space.\n *\n * A cookie jar is attacker-writable in the very case this feature exists for, and\n * `new Headers()` rejects a value outside Latin-1 — so a sibling subdomain that\n * tosses `spfn_csrf=한` would make every RPC call throw before it was sent, not\n * merely fail the check. A genuine token is 64 hex characters; anything that\n * cannot be a header value cannot be one.\n */\nconst HEADER_SAFE_VALUE = /^[!-~]+$/;\n\n/**\n * Upper bound on candidates sent, so a cookie-flooded jar stays a bounded header.\n *\n * Deliberately generous. The server recomputes the expected value once and then\n * only compares fixed-length strings, so extra candidates cost it almost nothing,\n * while every candidate dropped here is a chance to lock the user out of every\n * mutation — the far worse failure. Must not exceed the matching bound in\n * @spfn/auth's `matchesCsrfToken`, or values sent would never be looked at.\n */\nconst MAX_CANDIDATES = 32;\n\n/**\n * Choose which candidates to send when the jar holds more than the cap.\n *\n * `document.cookie` exposes neither Domain nor Path, so a tossed cookie is\n * indistinguishable from the genuine one here — the proxy is what tells them\n * apart, by recomputing. What this can do is refuse to drop the genuine value on\n * the floor. Browsers order the jar by path length descending, then by age\n * ascending, so a flood pushes the genuine cookie towards the end (attacker\n * cookies on a longer path) or towards the start (attacker cookies written later\n * on the same path). Keeping both ends survives either, where a plain prefix\n * survives only one.\n *\n * A jar flooded far past the cap is a denial of service that no client-side\n * choice fully prevents; the structural fix is a `__Host-` prefixed cookie name,\n * which a sibling subdomain cannot write at all.\n */\nfunction selectCandidates(values: string[]): string[]\n{\n // Identical duplicates carry no extra information, so collapsing them buys\n // back slots that a flood of one repeated value would otherwise burn.\n const unique = Array.from(new Set(values));\n\n if (unique.length <= MAX_CANDIDATES)\n {\n return unique;\n }\n\n const half = MAX_CANDIDATES / 2;\n\n return [...unique.slice(0, half), ...unique.slice(-half)];\n}\n\n/**\n * Collect the CSRF token candidates from cookie entries.\n *\n * Takes entries rather than a map because a name can legitimately appear twice:\n * a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the\n * browser holding two cookies of that name, and folding them into a map would\n * throw away whichever the tossed one displaced — locking the user out of every\n * mutation. Sending both keeps the real one in play, and the tossed one is inert\n * because the proxy recomputes what it expects.\n *\n * @returns Header value (comma-separated), or undefined when no cookie exists\n */\nexport function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined\n{\n // A comma separates candidates on the wire, so a value containing one would\n // arrive as several. That cannot be a genuine token — they are hex — but it\n // would let a single tossed cookie spend the receiver's whole candidate\n // budget and bury the real value behind it. Drop those, and anything that\n // could not be a header value at all.\n const values = Array.from(cookies)\n .filter(([name, value]) => CSRF_COOKIE_PATTERN.test(name)\n && HEADER_SAFE_VALUE.test(value)\n && !value.includes(','))\n .map(([, value]) => value);\n\n const candidates = selectCandidates(values);\n\n return candidates.length > 0 ? candidates.join(',') : undefined;\n}\n\n/**\n * Read the browser cookie jar as entries, duplicates preserved.\n *\n * Empty outside a browser (SSR reads the jar it forwards instead).\n */\nexport function documentCookieEntries(): Array<[string, string]>\n{\n if (typeof document === 'undefined' || !document.cookie)\n {\n return [];\n }\n\n const entries: Array<[string, string]> = [];\n\n for (const pair of document.cookie.split(';'))\n {\n const separator = pair.indexOf('=');\n\n if (separator > 0)\n {\n entries.push([pair.slice(0, separator).trim(), pair.slice(separator + 1).trim()]);\n }\n }\n\n return entries;\n}\n","/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * // With token authentication (recommended: use createAuthSSEClient)\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * const client = createAuthSSEClient<EventRouter>();\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport { CSRF_HEADER, csrfHeaderValue, documentCookieEntries } from '../../nextjs/client/csrf';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Per-subscription connection resources.\n *\n * Grouped so an in-flight async connect can be cancelled atomically and\n * `close()` can tear down whichever subscription is currently active —\n * the shared module-level EventSource used to leak across the token await.\n */\ninterface SSEConnection\n{\n eventSource: EventSource | null;\n reconnectTimer: ReturnType<typeof setTimeout> | null;\n reconnectAttempts: number;\n closed: boolean;\n onClose?: () => void;\n}\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n acquireToken,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let state: SSEConnectionState = 'closed';\n let active: SSEConnection | null = null;\n\n // Idempotent teardown: closes the connection, fires onClose exactly once,\n // and clears the active slot if this was the live connection.\n function closeConn(conn: SSEConnection)\n {\n if (conn.closed)\n {\n return;\n }\n\n conn.closed = true;\n\n if (conn.reconnectTimer)\n {\n clearTimeout(conn.reconnectTimer);\n conn.reconnectTimer = null;\n }\n\n if (conn.eventSource)\n {\n conn.eventSource.close();\n conn.eventSource = null;\n }\n\n if (active === conn)\n {\n active = null;\n state = 'closed';\n }\n\n conn.onClose?.();\n }\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect, onClose } = options;\n\n // A new subscription supersedes any previous one on this client.\n if (active)\n {\n closeConn(active);\n }\n\n const conn: SSEConnection = {\n eventSource: null,\n reconnectTimer: null,\n reconnectAttempts: 0,\n closed: false,\n onClose,\n };\n active = conn;\n\n const eventNames = events as string[];\n\n function connect()\n {\n state = 'connecting';\n\n const init = async () =>\n {\n let tokenParam = '';\n\n if (acquireToken)\n {\n const token = await acquireToken();\n\n // Cancelled during the token await (e.g. StrictMode cleanup) —\n // don't open a connection that has no teardown waiting for it.\n if (conn.closed)\n {\n return;\n }\n\n tokenParam = `&token=${encodeURIComponent(token)}`;\n }\n\n if (conn.closed)\n {\n return;\n }\n\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}${tokenParam}`;\n\n conn.eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n setupEventHandlers(conn.eventSource, eventNames, handlers, onOpen, onError);\n setupReconnect(onReconnect);\n };\n\n init().catch(() =>\n {\n // Don't resurrect a torn-down subscription via reconnect.\n if (conn.closed)\n {\n return;\n }\n\n state = 'error';\n attemptReconnect(onReconnect);\n });\n }\n\n function setupEventHandlers(\n es: EventSource,\n names: string[],\n handlerMap: SSESubscribeOptions<TRouter>['handlers'],\n onOpenCb?: () => void,\n onErrorCb?: (error: Event) => void,\n )\n {\n es.onopen = () =>\n {\n state = 'open';\n conn.reconnectAttempts = 0;\n onOpenCb?.();\n };\n\n es.onerror = (error) =>\n {\n state = 'error';\n onErrorCb?.(error);\n };\n\n // Handle connected event (server sends this on connection)\n es.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n es.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of names)\n {\n const handler = (handlerMap as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n es.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n function setupReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (!conn.eventSource)\n {\n return;\n }\n\n const currentEs = conn.eventSource;\n const originalOnError = currentEs.onerror;\n\n currentEs.onerror = (error) =>\n {\n if (originalOnError)\n {\n (originalOnError as (ev: Event) => void)(error);\n }\n\n // Token-auth 사용 시 브라우저 auto-retry는 소비된 토큰으로 재시도하므로\n // 즉시 close하고 우리 reconnect로 새 토큰 발급받아 재연결\n if (reconnect && acquireToken)\n {\n currentEs.close();\n attemptReconnect(onReconnectCb);\n }\n else if (reconnect && currentEs.readyState === EventSource.CLOSED)\n {\n attemptReconnect(onReconnectCb);\n }\n };\n }\n\n function attemptReconnect(onReconnectCb?: (attempt: number) => void)\n {\n if (conn.closed || !reconnect)\n {\n return;\n }\n\n if (maxReconnectAttempts > 0 && conn.reconnectAttempts >= maxReconnectAttempts)\n {\n closeConn(conn);\n\n return;\n }\n\n conn.reconnectAttempts++;\n onReconnectCb?.(conn.reconnectAttempts);\n\n conn.reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n closeConn(conn);\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (active)\n {\n closeConn(active);\n }\n else\n {\n state = 'closed';\n }\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig,\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n\n// ============================================================================\n// Auth SSE Client\n// ============================================================================\n\n/**\n * SSE client configuration for authenticated connections\n *\n * Same as SSEClientConfig but without acquireToken (auto-configured).\n */\nexport interface AuthSSEClientConfig extends Omit<SSEClientConfig, 'acquireToken'>\n{\n /**\n * RPC proxy base URL for token acquisition\n * @default '/api/rpc'\n */\n rpcBaseUrl?: string;\n}\n\n/**\n * Headers of the one-time token request.\n *\n * The token route is a cookie-authenticated POST through the RPC proxy, so the\n * @spfn/auth CSRF rule applies to it exactly as to any other mutation: the\n * readable `spfn_csrf` cookie family is mirrored into `x-spfn-csrf`, the way\n * the api client does. Without the header, `SPFN_AUTH_CSRF=enforce` refuses\n * the request and the stream never opens.\n *\n * @internal Exported for tests\n */\nexport function tokenRequestHeaders(cookies: Iterable<[string, string]> = documentCookieEntries()): Record<string, string>\n{\n const csrf = csrfHeaderValue(cookies);\n\n return csrf === undefined\n ? { 'Content-Type': 'application/json' }\n : { 'Content-Type': 'application/json', [CSRF_HEADER]: csrf };\n}\n\n/**\n * Create SSE client with built-in token authentication\n *\n * Acquires one-time SSE tokens via RPC proxy automatically, sending the\n * `x-spfn-csrf` header whenever a `spfn_csrf` cookie is present.\n * Requires eventRouteMap to be merged into RPC proxy config.\n *\n * @example\n * ```typescript\n * import { createAuthSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * const client = createAuthSSEClient<EventRouter>();\n *\n * client.subscribe({\n * events: ['userCreated'],\n * handlers: {\n * userCreated: (payload) => console.log(payload),\n * },\n * });\n * ```\n */\nexport function createAuthSSEClient<TRouter extends EventRouterDef<any>>(\n config: AuthSSEClientConfig = {},\n): SSEClient<TRouter>\n{\n const { rpcBaseUrl = '/api/rpc', ...sseConfig } = config;\n\n return createSSEClient<TRouter>({\n ...sseConfig,\n acquireToken: async () =>\n {\n const res = await fetch(`${rpcBaseUrl}/eventsToken`, {\n method: 'POST',\n credentials: 'include',\n headers: tokenRequestHeaders(),\n body: JSON.stringify({}),\n });\n\n if (!res.ok)\n {\n throw new Error(`Failed to acquire SSE token: ${res.status}`);\n }\n\n const data = await res.json();\n\n return data.token;\n },\n });\n}\n"]}
|
package/dist/job/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry, e as JobQueuePolicy } from '../boss-
|
|
2
|
-
export { B as BossConfig, f as BossOptions, I as InferJobInput, g as InferJobOutput, h as JobSendOptions, i as getBoss, j as initBoss, k as isBossRunning, s as shouldClearOnStart, l as stopBoss } from '../boss-
|
|
1
|
+
import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry, e as JobQueuePolicy } from '../boss-BiDCbbAQ.js';
|
|
2
|
+
export { B as BossConfig, f as BossOptions, I as InferJobInput, g as InferJobOutput, h as JobSendOptions, i as getBoss, j as initBoss, k as isBossRunning, s as shouldClearOnStart, l as stopBoss } from '../boss-BiDCbbAQ.js';
|
|
3
3
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
4
4
|
import { Static } from '@sinclair/typebox';
|
|
5
5
|
import { EventDef, InferEventPayload } from '@spfn/core/event';
|
package/dist/nextjs/index.d.ts
CHANGED
|
@@ -122,6 +122,25 @@ declare function createApi<TRouter extends Router<any>>(config?: ApiConfig): Cli
|
|
|
122
122
|
* because @spfn/core cannot depend on @spfn/auth.
|
|
123
123
|
*/
|
|
124
124
|
declare const CSRF_HEADER = "x-spfn-csrf";
|
|
125
|
+
/**
|
|
126
|
+
* Collect the CSRF token candidates from cookie entries.
|
|
127
|
+
*
|
|
128
|
+
* Takes entries rather than a map because a name can legitimately appear twice:
|
|
129
|
+
* a sibling subdomain that tosses `spfn_csrf` for the parent domain leaves the
|
|
130
|
+
* browser holding two cookies of that name, and folding them into a map would
|
|
131
|
+
* throw away whichever the tossed one displaced — locking the user out of every
|
|
132
|
+
* mutation. Sending both keeps the real one in play, and the tossed one is inert
|
|
133
|
+
* because the proxy recomputes what it expects.
|
|
134
|
+
*
|
|
135
|
+
* @returns Header value (comma-separated), or undefined when no cookie exists
|
|
136
|
+
*/
|
|
137
|
+
declare function csrfHeaderValue(cookies: Iterable<[string, string]>): string | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Read the browser cookie jar as entries, duplicates preserved.
|
|
140
|
+
*
|
|
141
|
+
* Empty outside a browser (SSR reads the jar it forwards instead).
|
|
142
|
+
*/
|
|
143
|
+
declare function documentCookieEntries(): Array<[string, string]>;
|
|
125
144
|
|
|
126
145
|
/**
|
|
127
146
|
* Typed client error
|
|
@@ -134,4 +153,4 @@ declare class ApiError extends Error {
|
|
|
134
153
|
constructor(message: string, status: number, url: string, response?: unknown | undefined, errorType?: "http" | "network" | "timeout" | undefined);
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
export { ApiConfig, ApiError, CSRF_HEADER, type Client, InferRouteInput, InferRouteOutput, RequestInterceptor, ResponseInterceptor, type RouteClient, createApi };
|
|
156
|
+
export { ApiConfig, ApiError, CSRF_HEADER, type Client, InferRouteInput, InferRouteOutput, RequestInterceptor, ResponseInterceptor, type RouteClient, createApi, csrfHeaderValue, documentCookieEntries };
|
package/dist/nextjs/index.js
CHANGED
|
@@ -496,6 +496,6 @@ function createApi(config = {}) {
|
|
|
496
496
|
return buildProxy();
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
export { ApiError, CSRF_HEADER, createApi };
|
|
499
|
+
export { ApiError, CSRF_HEADER, createApi, csrfHeaderValue, documentCookieEntries };
|
|
500
500
|
//# sourceMappingURL=index.js.map
|
|
501
501
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { serve } from '@hono/node-server';
|
|
|
5
5
|
import { NamedMiddleware, Router } from '@spfn/core/route';
|
|
6
6
|
import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
|
|
7
7
|
import { SafeFetchPolicy } from '@spfn/core/security';
|
|
8
|
-
import { c as JobRouter, f as BossOptions } from '../boss-
|
|
8
|
+
import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
|
|
9
9
|
import { b as EventRouterDef, E as EventDef } from '../token-manager-BT5EnUAR.js';
|
|
10
10
|
import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-ZQODsBft.js';
|
|
11
11
|
import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-2AbaW4Ie.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.9",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/db/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Everything re-exported from `@spfn/core/db` (`src/db/index.ts`):
|
|
|
55
55
|
`setDatabaseProvider`, `closeDatabase`,
|
|
56
56
|
`getDatabaseInfo`, `forceReconnectDatabase`, `createDatabaseConnection`, `checkConnection`,
|
|
57
57
|
`reportDatabaseError`, `isConnectionLevelError`, `resetConnectionErrorCounter`,
|
|
58
|
-
`getDrizzleConfig`, `detectDialect`, `generateDrizzleConfigFile`
|
|
58
|
+
`getDrizzleConfig`, `detectDialect`, `generateDrizzleConfigFile`, `renderDrizzleConfig` (0.3.0-beta.9: `getDrizzleConfig` returns `schemaSource` and `schemaRegistry`; the `autoDetectSchemas` option is gone — the CLI derives PostgreSQL schemas from the loaded modules, and a hand-written config passes `schemaFilter` itself)
|
|
59
59
|
Types: `DatabaseClients`, `DatabaseInitOptions`, `DatabaseOptions`, `DatabaseProvider`,
|
|
60
60
|
`DatabaseTransaction`, `DefaultDatabase`, `DrizzleDatabase`, `PoolConfig`, `RetryConfig`,
|
|
61
61
|
`DrizzleConfigOptions`
|
package/src/db/schema/README.md
CHANGED
|
@@ -256,11 +256,21 @@ column helpers above work identically inside `schema.table`.
|
|
|
256
256
|
column literally named `emailVerified`.
|
|
257
257
|
- **`enumText` is plain text + CHECK, not a PG enum type.** Adding values requires no
|
|
258
258
|
migration; renaming/removing the column still does.
|
|
259
|
-
- **Exported tables must be reachable by drizzle-kit to land in migrations.**
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
- **Exported tables must be reachable by drizzle-kit to land in migrations.** Entity
|
|
260
|
+
files under `src/server/entities/` are found by the folder scan; tables kept elsewhere
|
|
261
|
+
must be re-exported from the registry (`src/server/entities/config.ts → export * from
|
|
262
|
+
'../(workspace)/entities/users'`), which `spfn db push`, `db generate` and `db studio`
|
|
263
|
+
read when the folder holds no entity file. A table nothing reaches generates no migration.
|
|
262
264
|
- **When using `createSchema`, all tables in that package must use `schema.table(...)`.**
|
|
263
265
|
Mixing `pgTable(...)` puts that table in the default `public` schema.
|
|
266
|
+
- **Do not re-export an installed function package's tables from the project registry.**
|
|
267
|
+
A package that ships migrations (`@spfn/auth` → `spfn_auth`) owns its schema: it creates
|
|
268
|
+
and alters those tables itself. `spfn db push` leaves them out of the project's diff, and
|
|
269
|
+
`spfn db generate` refuses outright — the schema files it reads become a project migration,
|
|
270
|
+
and a migration that recreates a package's table cannot run on a fresh database and
|
|
271
|
+
collides with the package's own. A relation needs no re-export: `import { users } from
|
|
272
|
+
'@spfn/auth/entities'` and reference it from your own table. Only re-export what the
|
|
273
|
+
project itself defines.
|
|
264
274
|
|
|
265
275
|
---
|
|
266
276
|
|
|
@@ -307,7 +317,7 @@ export type NewPost = typeof posts.$inferInsert;
|
|
|
307
317
|
```
|
|
308
318
|
|
|
309
319
|
```typescript
|
|
310
|
-
// src/server/entities/
|
|
320
|
+
// src/server/entities/config.ts — the registry; must re-export so drizzle-kit sees every table
|
|
311
321
|
export * from './users';
|
|
312
322
|
export * from './posts';
|
|
313
323
|
export * from './relations';
|
package/src/event/README.md
CHANGED
|
@@ -414,7 +414,12 @@ client.subscribe({
|
|
|
414
414
|
|
|
415
415
|
`AuthSSEClientConfig` = `Omit<SSEClientConfig, 'acquireToken'>` plus `rpcBaseUrl` (default
|
|
416
416
|
`/api/rpc`). It requires `eventRouteMap` merged into your RPC proxy so `eventsToken`
|
|
417
|
-
resolves to `POST /events/token
|
|
417
|
+
resolves to `POST /events/token`. That token request is a cookie-authenticated POST
|
|
418
|
+
through the proxy, so `@spfn/auth`'s CSRF rule applies to it: the client mirrors the
|
|
419
|
+
readable `spfn_csrf` cookie family into the `x-spfn-csrf` header, the way the api
|
|
420
|
+
client does, so the stream opens under `SPFN_AUTH_CSRF=enforce` too. A custom
|
|
421
|
+
`acquireToken` passed to `createSSEClient` has to send that header itself
|
|
422
|
+
(`csrfHeaderValue` and `documentCookieEntries` from `@spfn/core/nextjs` build it).
|
|
418
423
|
|
|
419
424
|
```typescript
|
|
420
425
|
// app/api/rpc/[routeName]/route.ts
|
package/src/job/README.md
CHANGED
|
@@ -140,6 +140,10 @@ const typedJob = job('typed')
|
|
|
140
140
|
const noInput = job('simple').handler(async () => { await db.cleanup(); });
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
A `Type.Union([...])` input is supported: `send`, `run`, `sendBatch` and the handler are
|
|
144
|
+
typed on the whole union, so each member is accepted and a non-member is rejected — no cast
|
|
145
|
+
and no per-member overload.
|
|
146
|
+
|
|
143
147
|
### Cron — scheduled
|
|
144
148
|
|
|
145
149
|
```typescript
|