@cavuno/board 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,12 +30,15 @@ __export(src_exports, {
30
30
  createBoardClient: () => createBoardClient,
31
31
  isBoardApiError: () => isBoardApiError,
32
32
  isBoardPasswordRequired: () => isBoardPasswordRequired,
33
+ isColdRule: () => isColdRule,
33
34
  isConflict: () => isConflict,
34
35
  isForbidden: () => isForbidden,
35
36
  isNotFound: () => isNotFound,
37
+ isOwnMessage: () => isOwnMessage,
36
38
  isRateLimited: () => isRateLimited,
37
39
  isUnauthorized: () => isUnauthorized,
38
40
  isValidationError: () => isValidationError,
41
+ lastOwnMessageId: () => lastOwnMessageId,
39
42
  paginate: () => paginate
40
43
  });
41
44
  module.exports = __toCommonJS(src_exports);
@@ -176,7 +179,7 @@ var BoardApiError = class extends Error {
176
179
  }
177
180
  };
178
181
  function isBoardApiError(e) {
179
- return e instanceof BoardApiError;
182
+ return e instanceof Error && e.name === "BoardApiError";
180
183
  }
181
184
  function isNotFound(e) {
182
185
  return isBoardApiError(e) && e.status === 404;
@@ -237,14 +240,45 @@ function memoryStorage() {
237
240
  }
238
241
  };
239
242
  }
243
+ function scopeToken(board) {
244
+ return board.replace(/[^a-zA-Z0-9_-]/g, "_");
245
+ }
246
+ function browserStorage(mode, scope) {
247
+ if (!isBrowser()) {
248
+ throw new Error(
249
+ `storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
250
+ );
251
+ }
252
+ let backing;
253
+ try {
254
+ backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
255
+ if (!backing) throw new Error("unavailable");
256
+ } catch {
257
+ throw new Error(
258
+ `storage mode '${mode}' is unavailable in this browsing context (storage access is blocked \u2014 e.g. a sandboxed or third-party iframe). Use 'memory', or 'nostore' + per-call headers.`
259
+ );
260
+ }
261
+ const suffix = scope ? `:${scopeToken(scope)}` : "";
262
+ return {
263
+ getItem: (key) => backing.getItem(`${key}${suffix}`),
264
+ setItem: (key, value) => {
265
+ backing.setItem(`${key}${suffix}`, value);
266
+ },
267
+ removeItem: (key) => {
268
+ backing.removeItem(`${key}${suffix}`);
269
+ }
270
+ };
271
+ }
272
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
240
273
  var NOSTORE = {
274
+ [NOSTORE_BRAND]: true,
241
275
  getItem: () => null,
242
276
  setItem: () => {
243
277
  },
244
278
  removeItem: () => {
245
279
  }
246
280
  };
247
- function resolveStorage(mode) {
281
+ function resolveStorage(mode, scope) {
248
282
  const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
249
283
  if (typeof resolved === "object") return resolved;
250
284
  switch (resolved) {
@@ -252,6 +286,9 @@ function resolveStorage(mode) {
252
286
  return memoryStorage();
253
287
  case "nostore":
254
288
  return NOSTORE;
289
+ case "local":
290
+ case "session":
291
+ return browserStorage(resolved, scope);
255
292
  default:
256
293
  throw new Error(`Unknown storage mode '${String(resolved)}'`);
257
294
  }
@@ -266,7 +303,7 @@ async function clearSession(storage) {
266
303
  }
267
304
 
268
305
  // src/version.ts
269
- var SDK_VERSION = "1.27.0";
306
+ var SDK_VERSION = "1.28.0";
270
307
 
271
308
  // src/client.ts
272
309
  function isRawBody(body) {
@@ -2547,12 +2584,31 @@ function paginate(listFn, query, options) {
2547
2584
  };
2548
2585
  }
2549
2586
 
2587
+ // src/messaging-derive.ts
2588
+ function isOwnMessage(message, counterpartyId) {
2589
+ return message.authorBoardUserId !== counterpartyId;
2590
+ }
2591
+ function isColdRule(messages, counterpartyId) {
2592
+ const theyReplied = messages.some(
2593
+ (m) => m.authorBoardUserId === counterpartyId
2594
+ );
2595
+ const iSent = messages.some((m) => m.authorBoardUserId !== counterpartyId);
2596
+ return iSent && !theyReplied;
2597
+ }
2598
+ function lastOwnMessageId(messages, counterpartyId) {
2599
+ for (let i = messages.length - 1; i >= 0; i--) {
2600
+ const message = messages[i];
2601
+ if (message.authorBoardUserId !== counterpartyId) return message.id;
2602
+ }
2603
+ return null;
2604
+ }
2605
+
2550
2606
  // src/index.ts
2551
2607
  function createBoardClient(options) {
2552
2608
  const client = new BoardClient({
2553
2609
  baseUrl: options.baseUrl,
2554
2610
  board: options.board,
2555
- storage: resolveStorage(options.auth?.storage),
2611
+ storage: resolveStorage(options.auth?.storage, options.board),
2556
2612
  globalHeaders: options.globalHeaders,
2557
2613
  onRequest: options.onRequest,
2558
2614
  onResponse: options.onResponse,
package/dist/index.mjs CHANGED
@@ -134,7 +134,7 @@ var BoardApiError = class extends Error {
134
134
  }
135
135
  };
136
136
  function isBoardApiError(e) {
137
- return e instanceof BoardApiError;
137
+ return e instanceof Error && e.name === "BoardApiError";
138
138
  }
139
139
  function isNotFound(e) {
140
140
  return isBoardApiError(e) && e.status === 404;
@@ -195,14 +195,45 @@ function memoryStorage() {
195
195
  }
196
196
  };
197
197
  }
198
+ function scopeToken(board) {
199
+ return board.replace(/[^a-zA-Z0-9_-]/g, "_");
200
+ }
201
+ function browserStorage(mode, scope) {
202
+ if (!isBrowser()) {
203
+ throw new Error(
204
+ `storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
205
+ );
206
+ }
207
+ let backing;
208
+ try {
209
+ backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
210
+ if (!backing) throw new Error("unavailable");
211
+ } catch {
212
+ throw new Error(
213
+ `storage mode '${mode}' is unavailable in this browsing context (storage access is blocked \u2014 e.g. a sandboxed or third-party iframe). Use 'memory', or 'nostore' + per-call headers.`
214
+ );
215
+ }
216
+ const suffix = scope ? `:${scopeToken(scope)}` : "";
217
+ return {
218
+ getItem: (key) => backing.getItem(`${key}${suffix}`),
219
+ setItem: (key, value) => {
220
+ backing.setItem(`${key}${suffix}`, value);
221
+ },
222
+ removeItem: (key) => {
223
+ backing.removeItem(`${key}${suffix}`);
224
+ }
225
+ };
226
+ }
227
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
198
228
  var NOSTORE = {
229
+ [NOSTORE_BRAND]: true,
199
230
  getItem: () => null,
200
231
  setItem: () => {
201
232
  },
202
233
  removeItem: () => {
203
234
  }
204
235
  };
205
- function resolveStorage(mode) {
236
+ function resolveStorage(mode, scope) {
206
237
  const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
207
238
  if (typeof resolved === "object") return resolved;
208
239
  switch (resolved) {
@@ -210,6 +241,9 @@ function resolveStorage(mode) {
210
241
  return memoryStorage();
211
242
  case "nostore":
212
243
  return NOSTORE;
244
+ case "local":
245
+ case "session":
246
+ return browserStorage(resolved, scope);
213
247
  default:
214
248
  throw new Error(`Unknown storage mode '${String(resolved)}'`);
215
249
  }
@@ -224,7 +258,7 @@ async function clearSession(storage) {
224
258
  }
225
259
 
226
260
  // src/version.ts
227
- var SDK_VERSION = "1.27.0";
261
+ var SDK_VERSION = "1.28.0";
228
262
 
229
263
  // src/client.ts
230
264
  function isRawBody(body) {
@@ -2505,12 +2539,31 @@ function paginate(listFn, query, options) {
2505
2539
  };
2506
2540
  }
2507
2541
 
2542
+ // src/messaging-derive.ts
2543
+ function isOwnMessage(message, counterpartyId) {
2544
+ return message.authorBoardUserId !== counterpartyId;
2545
+ }
2546
+ function isColdRule(messages, counterpartyId) {
2547
+ const theyReplied = messages.some(
2548
+ (m) => m.authorBoardUserId === counterpartyId
2549
+ );
2550
+ const iSent = messages.some((m) => m.authorBoardUserId !== counterpartyId);
2551
+ return iSent && !theyReplied;
2552
+ }
2553
+ function lastOwnMessageId(messages, counterpartyId) {
2554
+ for (let i = messages.length - 1; i >= 0; i--) {
2555
+ const message = messages[i];
2556
+ if (message.authorBoardUserId !== counterpartyId) return message.id;
2557
+ }
2558
+ return null;
2559
+ }
2560
+
2508
2561
  // src/index.ts
2509
2562
  function createBoardClient(options) {
2510
2563
  const client = new BoardClient({
2511
2564
  baseUrl: options.baseUrl,
2512
2565
  board: options.board,
2513
- storage: resolveStorage(options.auth?.storage),
2566
+ storage: resolveStorage(options.auth?.storage, options.board),
2514
2567
  globalHeaders: options.globalHeaders,
2515
2568
  onRequest: options.onRequest,
2516
2569
  onResponse: options.onResponse,
@@ -2572,11 +2625,14 @@ export {
2572
2625
  createBoardClient,
2573
2626
  isBoardApiError,
2574
2627
  isBoardPasswordRequired,
2628
+ isColdRule,
2575
2629
  isConflict,
2576
2630
  isForbidden,
2577
2631
  isNotFound,
2632
+ isOwnMessage,
2578
2633
  isRateLimited,
2579
2634
  isUnauthorized,
2580
2635
  isValidationError,
2636
+ lastOwnMessageId,
2581
2637
  paginate
2582
2638
  };
@@ -0,0 +1,111 @@
1
+ import { BoardSdk } from './index.mjs';
2
+ import './jobs-DK5mPBgq.mjs';
3
+ import './salaries-CrJsaZe6.mjs';
4
+
5
+ /**
6
+ * Session cookie codec — pure (no framework imports, no node imports) so it
7
+ * stays hermetically testable and platform-neutral: helpers speak cookie
8
+ * STRINGS (`Set-Cookie` values / `Cookie` headers), never framework response
9
+ * objects. The session is the SDK bearer pair plus the access-token expiry;
10
+ * it lives in ONE httpOnly cookie owned by the host app (the SDK never sees
11
+ * storage on the server — ADR-0006).
12
+ */
13
+ interface BoardSession {
14
+ accessToken: string;
15
+ refreshToken: string;
16
+ /** Access-token expiry, epoch ms (from board_auth_session). */
17
+ expiresAt: number;
18
+ }
19
+ declare const SESSION_COOKIE_NAME = "__Host-cavuno_board_session";
20
+ /**
21
+ * The session cookie name, optionally board-scoped. One origin can serve
22
+ * multiple boards (the hosted platform scopes its grant cookie per account
23
+ * for this reason) — a multi-board host MUST pass its board identifier so
24
+ * sessions cannot clobber each other; single-board apps omit it.
25
+ */
26
+ declare function sessionCookieName(board?: string): string;
27
+ declare function serializeSessionCookie(session: BoardSession, options?: {
28
+ board?: string;
29
+ }): string;
30
+ declare function clearSessionCookie(options?: {
31
+ board?: string;
32
+ }): string;
33
+ declare function parseSessionCookie(cookieHeader: string | null, options?: {
34
+ board?: string;
35
+ }): BoardSession | null;
36
+ declare function isExpiringSoon(session: BoardSession, now: number, windowMs?: number): boolean;
37
+
38
+ declare const BOARD_ACCESS_COOKIE_NAME = "__Host-cavuno_board_access";
39
+ /**
40
+ * The grant cookie name, optionally board-scoped — hosted scopes its grant
41
+ * cookie per account (`board_access_<accountId>`) because one app instance
42
+ * can gate multiple boards. Multi-board hosts MUST pass their board
43
+ * identifier; single-board apps omit it.
44
+ */
45
+ declare function grantCookieName(board?: string): string;
46
+ declare function serializeGrantCookie(token: string, options?: {
47
+ board?: string;
48
+ }): string;
49
+ declare function clearGrantCookie(options?: {
50
+ board?: string;
51
+ }): string;
52
+ declare function parseGrantCookie(cookieHeader: string | null, options?: {
53
+ board?: string;
54
+ }): string | null;
55
+
56
+ /**
57
+ * Open-redirect guards — pure URL-safety logic (no cookies involved),
58
+ * transcribed from the hosted board's `validate-redirect-path.ts` and
59
+ * golden-tested against it input-for-input.
60
+ */
61
+ /**
62
+ * Guard a `?redirect=` / `?next=` param: a same-origin absolute path only
63
+ * ('/', not '//', no scheme), else `defaultPath` — hosted's
64
+ * `getSafeRedirectPath(path, defaultPath = '/')` shape (the two-arg form is
65
+ * live on the employer sign-up page, which falls back to
66
+ * '/account/connect').
67
+ */
68
+ declare function safeRedirectPath(path: string | undefined | null, defaultPath?: string): string;
69
+ /**
70
+ * The current page path (from the request `Referer`) for the /password
71
+ * redirect-back, guarded by `safeRedirectPath`. Pure — the framework-owned
72
+ * header read happens in the host app's middleware and the value is passed
73
+ * in here, so this stays platform-neutral.
74
+ */
75
+ declare function currentPathFromReferer(referer: string | null): string;
76
+
77
+ /**
78
+ * Single-flight session refresh — dedupes concurrent refreshes for the same
79
+ * session WITHIN one process/isolate (the rotation race, ADR-0057 wart #4,
80
+ * mitigated at the one layer a client library can reach).
81
+ *
82
+ * Refresh tokens are single-use: two concurrent refreshes for the same
83
+ * session burn the pair — the loser 401s and the user is signed out
84
+ * mid-session. This helper keys an in-flight slot per refreshToken so every
85
+ * concurrent caller awaits the SAME rotation; sequential calls after settle
86
+ * start a fresh one.
87
+ *
88
+ * Scope honestly stated: the dedupe is PER PROCESS/ISOLATE (an in-memory
89
+ * map). Two simultaneous requests served by different instances can still
90
+ * race the token; the proactive `isExpiringSoon` window keeps that rare, it
91
+ * does not eliminate it. Pair with `storage: 'nostore'` on the shared
92
+ * server client — `auth.refresh` persists the rotated pair into
93
+ * `client.storage`, and any persistent shared storage would bleed one
94
+ * user's tokens into another's requests.
95
+ *
96
+ * Returns the rotated `BoardSession` (persist it back to the cookie), or
97
+ * `null` on a 401 — the token is burned or revoked: clear the cookie and
98
+ * continue signed out, never retry. Other errors (network, 5xx, 429)
99
+ * rethrow untouched.
100
+ *
101
+ * @example
102
+ * const refreshSession = createSessionRefresher(board);
103
+ * // in the session middleware:
104
+ * if (isExpiringSoon(session, Date.now())) {
105
+ * const next = await refreshSession(session);
106
+ * setCookie(next ? serializeSessionCookie(next) : clearSessionCookie());
107
+ * }
108
+ */
109
+ declare function createSessionRefresher(board: Pick<BoardSdk, 'auth' | 'client'>): (session: BoardSession) => Promise<BoardSession | null>;
110
+
111
+ export { BOARD_ACCESS_COOKIE_NAME, type BoardSession, SESSION_COOKIE_NAME, clearGrantCookie, clearSessionCookie, createSessionRefresher, currentPathFromReferer, grantCookieName, isExpiringSoon, parseGrantCookie, parseSessionCookie, safeRedirectPath, serializeGrantCookie, serializeSessionCookie, sessionCookieName };
@@ -0,0 +1,111 @@
1
+ import { BoardSdk } from './index.js';
2
+ import './jobs-DK5mPBgq.js';
3
+ import './salaries-CXt6Vkrp.js';
4
+
5
+ /**
6
+ * Session cookie codec — pure (no framework imports, no node imports) so it
7
+ * stays hermetically testable and platform-neutral: helpers speak cookie
8
+ * STRINGS (`Set-Cookie` values / `Cookie` headers), never framework response
9
+ * objects. The session is the SDK bearer pair plus the access-token expiry;
10
+ * it lives in ONE httpOnly cookie owned by the host app (the SDK never sees
11
+ * storage on the server — ADR-0006).
12
+ */
13
+ interface BoardSession {
14
+ accessToken: string;
15
+ refreshToken: string;
16
+ /** Access-token expiry, epoch ms (from board_auth_session). */
17
+ expiresAt: number;
18
+ }
19
+ declare const SESSION_COOKIE_NAME = "__Host-cavuno_board_session";
20
+ /**
21
+ * The session cookie name, optionally board-scoped. One origin can serve
22
+ * multiple boards (the hosted platform scopes its grant cookie per account
23
+ * for this reason) — a multi-board host MUST pass its board identifier so
24
+ * sessions cannot clobber each other; single-board apps omit it.
25
+ */
26
+ declare function sessionCookieName(board?: string): string;
27
+ declare function serializeSessionCookie(session: BoardSession, options?: {
28
+ board?: string;
29
+ }): string;
30
+ declare function clearSessionCookie(options?: {
31
+ board?: string;
32
+ }): string;
33
+ declare function parseSessionCookie(cookieHeader: string | null, options?: {
34
+ board?: string;
35
+ }): BoardSession | null;
36
+ declare function isExpiringSoon(session: BoardSession, now: number, windowMs?: number): boolean;
37
+
38
+ declare const BOARD_ACCESS_COOKIE_NAME = "__Host-cavuno_board_access";
39
+ /**
40
+ * The grant cookie name, optionally board-scoped — hosted scopes its grant
41
+ * cookie per account (`board_access_<accountId>`) because one app instance
42
+ * can gate multiple boards. Multi-board hosts MUST pass their board
43
+ * identifier; single-board apps omit it.
44
+ */
45
+ declare function grantCookieName(board?: string): string;
46
+ declare function serializeGrantCookie(token: string, options?: {
47
+ board?: string;
48
+ }): string;
49
+ declare function clearGrantCookie(options?: {
50
+ board?: string;
51
+ }): string;
52
+ declare function parseGrantCookie(cookieHeader: string | null, options?: {
53
+ board?: string;
54
+ }): string | null;
55
+
56
+ /**
57
+ * Open-redirect guards — pure URL-safety logic (no cookies involved),
58
+ * transcribed from the hosted board's `validate-redirect-path.ts` and
59
+ * golden-tested against it input-for-input.
60
+ */
61
+ /**
62
+ * Guard a `?redirect=` / `?next=` param: a same-origin absolute path only
63
+ * ('/', not '//', no scheme), else `defaultPath` — hosted's
64
+ * `getSafeRedirectPath(path, defaultPath = '/')` shape (the two-arg form is
65
+ * live on the employer sign-up page, which falls back to
66
+ * '/account/connect').
67
+ */
68
+ declare function safeRedirectPath(path: string | undefined | null, defaultPath?: string): string;
69
+ /**
70
+ * The current page path (from the request `Referer`) for the /password
71
+ * redirect-back, guarded by `safeRedirectPath`. Pure — the framework-owned
72
+ * header read happens in the host app's middleware and the value is passed
73
+ * in here, so this stays platform-neutral.
74
+ */
75
+ declare function currentPathFromReferer(referer: string | null): string;
76
+
77
+ /**
78
+ * Single-flight session refresh — dedupes concurrent refreshes for the same
79
+ * session WITHIN one process/isolate (the rotation race, ADR-0057 wart #4,
80
+ * mitigated at the one layer a client library can reach).
81
+ *
82
+ * Refresh tokens are single-use: two concurrent refreshes for the same
83
+ * session burn the pair — the loser 401s and the user is signed out
84
+ * mid-session. This helper keys an in-flight slot per refreshToken so every
85
+ * concurrent caller awaits the SAME rotation; sequential calls after settle
86
+ * start a fresh one.
87
+ *
88
+ * Scope honestly stated: the dedupe is PER PROCESS/ISOLATE (an in-memory
89
+ * map). Two simultaneous requests served by different instances can still
90
+ * race the token; the proactive `isExpiringSoon` window keeps that rare, it
91
+ * does not eliminate it. Pair with `storage: 'nostore'` on the shared
92
+ * server client — `auth.refresh` persists the rotated pair into
93
+ * `client.storage`, and any persistent shared storage would bleed one
94
+ * user's tokens into another's requests.
95
+ *
96
+ * Returns the rotated `BoardSession` (persist it back to the cookie), or
97
+ * `null` on a 401 — the token is burned or revoked: clear the cookie and
98
+ * continue signed out, never retry. Other errors (network, 5xx, 429)
99
+ * rethrow untouched.
100
+ *
101
+ * @example
102
+ * const refreshSession = createSessionRefresher(board);
103
+ * // in the session middleware:
104
+ * if (isExpiringSoon(session, Date.now())) {
105
+ * const next = await refreshSession(session);
106
+ * setCookie(next ? serializeSessionCookie(next) : clearSessionCookie());
107
+ * }
108
+ */
109
+ declare function createSessionRefresher(board: Pick<BoardSdk, 'auth' | 'client'>): (session: BoardSession) => Promise<BoardSession | null>;
110
+
111
+ export { BOARD_ACCESS_COOKIE_NAME, type BoardSession, SESSION_COOKIE_NAME, clearGrantCookie, clearSessionCookie, createSessionRefresher, currentPathFromReferer, grantCookieName, isExpiringSoon, parseGrantCookie, parseSessionCookie, safeRedirectPath, serializeGrantCookie, serializeSessionCookie, sessionCookieName };
package/dist/server.js ADDED
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server/index.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ BOARD_ACCESS_COOKIE_NAME: () => BOARD_ACCESS_COOKIE_NAME,
24
+ SESSION_COOKIE_NAME: () => SESSION_COOKIE_NAME,
25
+ clearGrantCookie: () => clearGrantCookie,
26
+ clearSessionCookie: () => clearSessionCookie,
27
+ createSessionRefresher: () => createSessionRefresher,
28
+ currentPathFromReferer: () => currentPathFromReferer,
29
+ grantCookieName: () => grantCookieName,
30
+ isExpiringSoon: () => isExpiringSoon,
31
+ parseGrantCookie: () => parseGrantCookie,
32
+ parseSessionCookie: () => parseSessionCookie,
33
+ safeRedirectPath: () => safeRedirectPath,
34
+ serializeGrantCookie: () => serializeGrantCookie,
35
+ serializeSessionCookie: () => serializeSessionCookie,
36
+ sessionCookieName: () => sessionCookieName
37
+ });
38
+ module.exports = __toCommonJS(server_exports);
39
+
40
+ // src/server/cookie.ts
41
+ var COOKIE_ATTRIBUTES = "Path=/; HttpOnly; Secure; SameSite=Lax";
42
+ function buildCookie(name, value, maxAgeSeconds) {
43
+ return `${name}=${value}; Max-Age=${maxAgeSeconds}; ${COOKIE_ATTRIBUTES}`;
44
+ }
45
+ function buildClearCookie(name) {
46
+ return `${name}=; Max-Age=0; ${COOKIE_ATTRIBUTES}`;
47
+ }
48
+ function readCookie(header, name) {
49
+ if (!header) return null;
50
+ const pair = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
51
+ if (!pair) return null;
52
+ const raw = pair.slice(name.length + 1);
53
+ if (!raw) return null;
54
+ try {
55
+ return decodeURIComponent(raw);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ function cookieScope(board) {
61
+ return board.replace(/[^a-zA-Z0-9_-]/g, "_");
62
+ }
63
+
64
+ // src/server/session.ts
65
+ var SESSION_COOKIE_NAME = "__Host-cavuno_board_session";
66
+ function sessionCookieName(board) {
67
+ return board ? `${SESSION_COOKIE_NAME}_${cookieScope(board)}` : SESSION_COOKIE_NAME;
68
+ }
69
+ var COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
70
+ var EXPIRY_WINDOW_MS = 5 * 60 * 1e3;
71
+ function serializeSessionCookie(session, options) {
72
+ return buildCookie(
73
+ sessionCookieName(options?.board),
74
+ encodeURIComponent(JSON.stringify(session)),
75
+ COOKIE_MAX_AGE_SECONDS
76
+ );
77
+ }
78
+ function clearSessionCookie(options) {
79
+ return buildClearCookie(sessionCookieName(options?.board));
80
+ }
81
+ function parseSessionCookie(cookieHeader, options) {
82
+ const raw = readCookie(cookieHeader, sessionCookieName(options?.board));
83
+ if (!raw) return null;
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(raw);
87
+ } catch {
88
+ return null;
89
+ }
90
+ const session = parsed;
91
+ if (!session || typeof session.accessToken !== "string" || typeof session.refreshToken !== "string" || typeof session.expiresAt !== "number") {
92
+ return null;
93
+ }
94
+ return {
95
+ accessToken: session.accessToken,
96
+ refreshToken: session.refreshToken,
97
+ expiresAt: session.expiresAt
98
+ };
99
+ }
100
+ function isExpiringSoon(session, now, windowMs = EXPIRY_WINDOW_MS) {
101
+ return session.expiresAt - now < windowMs;
102
+ }
103
+
104
+ // src/server/board-access.ts
105
+ var BOARD_ACCESS_COOKIE_NAME = "__Host-cavuno_board_access";
106
+ function grantCookieName(board) {
107
+ return board ? `${BOARD_ACCESS_COOKIE_NAME}_${cookieScope(board)}` : BOARD_ACCESS_COOKIE_NAME;
108
+ }
109
+ var COOKIE_MAX_AGE_SECONDS2 = 24 * 60 * 60;
110
+ function serializeGrantCookie(token, options) {
111
+ return buildCookie(
112
+ grantCookieName(options?.board),
113
+ encodeURIComponent(token),
114
+ COOKIE_MAX_AGE_SECONDS2
115
+ );
116
+ }
117
+ function clearGrantCookie(options) {
118
+ return buildClearCookie(grantCookieName(options?.board));
119
+ }
120
+ function parseGrantCookie(cookieHeader, options) {
121
+ return readCookie(cookieHeader, grantCookieName(options?.board));
122
+ }
123
+
124
+ // src/server/redirect-guard.ts
125
+ function safeRedirectPath(path, defaultPath = "/") {
126
+ if (!path || !path.startsWith("/") || path.startsWith("//")) {
127
+ return defaultPath;
128
+ }
129
+ if (path.includes("://")) return defaultPath;
130
+ try {
131
+ const SENTINEL_ORIGIN = "https://redirect.invalid";
132
+ if (new URL(path, SENTINEL_ORIGIN).origin !== SENTINEL_ORIGIN) {
133
+ return defaultPath;
134
+ }
135
+ } catch {
136
+ return defaultPath;
137
+ }
138
+ return path;
139
+ }
140
+ function currentPathFromReferer(referer) {
141
+ if (!referer) return "/";
142
+ try {
143
+ const url = new URL(referer);
144
+ return safeRedirectPath(url.pathname + url.search);
145
+ } catch {
146
+ return "/";
147
+ }
148
+ }
149
+
150
+ // src/errors.ts
151
+ function isBoardApiError(e) {
152
+ return e instanceof Error && e.name === "BoardApiError";
153
+ }
154
+ function isUnauthorized(e) {
155
+ return isBoardApiError(e) && e.status === 401;
156
+ }
157
+
158
+ // src/storage.ts
159
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
160
+ function isNoStore(storage) {
161
+ return storage[NOSTORE_BRAND] === true;
162
+ }
163
+ var NOSTORE = {
164
+ [NOSTORE_BRAND]: true,
165
+ getItem: () => null,
166
+ setItem: () => {
167
+ },
168
+ removeItem: () => {
169
+ }
170
+ };
171
+
172
+ // src/server/refresher.ts
173
+ function createSessionRefresher(board) {
174
+ if (typeof globalThis.document === "undefined" && !isNoStore(board.client.storage)) {
175
+ throw new Error(
176
+ "createSessionRefresher requires the server client to use storage: 'nostore' \u2014 a shared persistent store would leak rotated tokens across requests. Keep the session in the httpOnly cookie and pass tokens per call."
177
+ );
178
+ }
179
+ const inflight = /* @__PURE__ */ new Map();
180
+ return async function refresh(session) {
181
+ const key = session.refreshToken;
182
+ const existing = inflight.get(key);
183
+ if (existing) return existing;
184
+ const attempt = board.auth.refresh({ refreshToken: key }).then(
185
+ (rotated) => ({
186
+ accessToken: rotated.accessToken,
187
+ refreshToken: rotated.refreshToken,
188
+ expiresAt: rotated.expiresAt
189
+ })
190
+ ).catch((error) => {
191
+ if (isUnauthorized(error)) return null;
192
+ throw error;
193
+ }).finally(() => {
194
+ inflight.delete(key);
195
+ });
196
+ inflight.set(key, attempt);
197
+ return attempt;
198
+ };
199
+ }