@cavuno/board 1.27.0 → 1.28.1

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" && typeof e.status === "number" && typeof e.code === "string";
180
183
  }
181
184
  function isNotFound(e) {
182
185
  return isBoardApiError(e) && e.status === 404;
@@ -218,6 +221,16 @@ function toSearchParams(query) {
218
221
  return params;
219
222
  }
220
223
 
224
+ // src/scope.ts
225
+ function scopeToken(board) {
226
+ let hash = 5381;
227
+ for (let i = 0; i < board.length; i++) {
228
+ hash = (hash << 5) + hash + board.charCodeAt(i) >>> 0;
229
+ }
230
+ const sanitized = board.replace(/[^a-zA-Z0-9_-]/g, "_");
231
+ return `${sanitized}-${hash.toString(36)}`;
232
+ }
233
+
221
234
  // src/storage.ts
222
235
  var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
223
236
  var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
@@ -237,14 +250,42 @@ function memoryStorage() {
237
250
  }
238
251
  };
239
252
  }
253
+ function browserStorage(mode, scope) {
254
+ if (!isBrowser()) {
255
+ throw new Error(
256
+ `storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
257
+ );
258
+ }
259
+ let backing;
260
+ try {
261
+ backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
262
+ if (!backing) throw new Error("unavailable");
263
+ } catch {
264
+ throw new Error(
265
+ `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.`
266
+ );
267
+ }
268
+ const suffix = scope ? `:${scopeToken(scope)}` : "";
269
+ return {
270
+ getItem: (key) => backing.getItem(`${key}${suffix}`),
271
+ setItem: (key, value) => {
272
+ backing.setItem(`${key}${suffix}`, value);
273
+ },
274
+ removeItem: (key) => {
275
+ backing.removeItem(`${key}${suffix}`);
276
+ }
277
+ };
278
+ }
279
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
240
280
  var NOSTORE = {
281
+ [NOSTORE_BRAND]: true,
241
282
  getItem: () => null,
242
283
  setItem: () => {
243
284
  },
244
285
  removeItem: () => {
245
286
  }
246
287
  };
247
- function resolveStorage(mode) {
288
+ function resolveStorage(mode, scope) {
248
289
  const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
249
290
  if (typeof resolved === "object") return resolved;
250
291
  switch (resolved) {
@@ -252,6 +293,9 @@ function resolveStorage(mode) {
252
293
  return memoryStorage();
253
294
  case "nostore":
254
295
  return NOSTORE;
296
+ case "local":
297
+ case "session":
298
+ return browserStorage(resolved, scope);
255
299
  default:
256
300
  throw new Error(`Unknown storage mode '${String(resolved)}'`);
257
301
  }
@@ -266,7 +310,7 @@ async function clearSession(storage) {
266
310
  }
267
311
 
268
312
  // src/version.ts
269
- var SDK_VERSION = "1.27.0";
313
+ var SDK_VERSION = "1.28.1";
270
314
 
271
315
  // src/client.ts
272
316
  function isRawBody(body) {
@@ -2547,12 +2591,31 @@ function paginate(listFn, query, options) {
2547
2591
  };
2548
2592
  }
2549
2593
 
2594
+ // src/messaging-derive.ts
2595
+ function isOwnMessage(message, counterpartyId) {
2596
+ return message.authorBoardUserId !== counterpartyId;
2597
+ }
2598
+ function isColdRule(messages, counterpartyId) {
2599
+ const theyReplied = messages.some(
2600
+ (m) => m.authorBoardUserId === counterpartyId
2601
+ );
2602
+ const iSent = messages.some((m) => m.authorBoardUserId !== counterpartyId);
2603
+ return iSent && !theyReplied;
2604
+ }
2605
+ function lastOwnMessageId(messages, counterpartyId) {
2606
+ for (let i = messages.length - 1; i >= 0; i--) {
2607
+ const message = messages[i];
2608
+ if (message.authorBoardUserId !== counterpartyId) return message.id;
2609
+ }
2610
+ return null;
2611
+ }
2612
+
2550
2613
  // src/index.ts
2551
2614
  function createBoardClient(options) {
2552
2615
  const client = new BoardClient({
2553
2616
  baseUrl: options.baseUrl,
2554
2617
  board: options.board,
2555
- storage: resolveStorage(options.auth?.storage),
2618
+ storage: resolveStorage(options.auth?.storage, options.board),
2556
2619
  globalHeaders: options.globalHeaders,
2557
2620
  onRequest: options.onRequest,
2558
2621
  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" && typeof e.status === "number" && typeof e.code === "string";
138
138
  }
139
139
  function isNotFound(e) {
140
140
  return isBoardApiError(e) && e.status === 404;
@@ -176,6 +176,16 @@ function toSearchParams(query) {
176
176
  return params;
177
177
  }
178
178
 
179
+ // src/scope.ts
180
+ function scopeToken(board) {
181
+ let hash = 5381;
182
+ for (let i = 0; i < board.length; i++) {
183
+ hash = (hash << 5) + hash + board.charCodeAt(i) >>> 0;
184
+ }
185
+ const sanitized = board.replace(/[^a-zA-Z0-9_-]/g, "_");
186
+ return `${sanitized}-${hash.toString(36)}`;
187
+ }
188
+
179
189
  // src/storage.ts
180
190
  var ACCESS_TOKEN_KEY = "cavuno_board_access_token";
181
191
  var REFRESH_TOKEN_KEY = "cavuno_board_refresh_token";
@@ -195,14 +205,42 @@ function memoryStorage() {
195
205
  }
196
206
  };
197
207
  }
208
+ function browserStorage(mode, scope) {
209
+ if (!isBrowser()) {
210
+ throw new Error(
211
+ `storage mode '${mode}' is browser-only \u2014 use 'nostore' + per-call headers on the server`
212
+ );
213
+ }
214
+ let backing;
215
+ try {
216
+ backing = mode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
217
+ if (!backing) throw new Error("unavailable");
218
+ } catch {
219
+ throw new Error(
220
+ `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.`
221
+ );
222
+ }
223
+ const suffix = scope ? `:${scopeToken(scope)}` : "";
224
+ return {
225
+ getItem: (key) => backing.getItem(`${key}${suffix}`),
226
+ setItem: (key, value) => {
227
+ backing.setItem(`${key}${suffix}`, value);
228
+ },
229
+ removeItem: (key) => {
230
+ backing.removeItem(`${key}${suffix}`);
231
+ }
232
+ };
233
+ }
234
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
198
235
  var NOSTORE = {
236
+ [NOSTORE_BRAND]: true,
199
237
  getItem: () => null,
200
238
  setItem: () => {
201
239
  },
202
240
  removeItem: () => {
203
241
  }
204
242
  };
205
- function resolveStorage(mode) {
243
+ function resolveStorage(mode, scope) {
206
244
  const resolved = mode ?? (isBrowser() ? "memory" : "nostore");
207
245
  if (typeof resolved === "object") return resolved;
208
246
  switch (resolved) {
@@ -210,6 +248,9 @@ function resolveStorage(mode) {
210
248
  return memoryStorage();
211
249
  case "nostore":
212
250
  return NOSTORE;
251
+ case "local":
252
+ case "session":
253
+ return browserStorage(resolved, scope);
213
254
  default:
214
255
  throw new Error(`Unknown storage mode '${String(resolved)}'`);
215
256
  }
@@ -224,7 +265,7 @@ async function clearSession(storage) {
224
265
  }
225
266
 
226
267
  // src/version.ts
227
- var SDK_VERSION = "1.27.0";
268
+ var SDK_VERSION = "1.28.1";
228
269
 
229
270
  // src/client.ts
230
271
  function isRawBody(body) {
@@ -2505,12 +2546,31 @@ function paginate(listFn, query, options) {
2505
2546
  };
2506
2547
  }
2507
2548
 
2549
+ // src/messaging-derive.ts
2550
+ function isOwnMessage(message, counterpartyId) {
2551
+ return message.authorBoardUserId !== counterpartyId;
2552
+ }
2553
+ function isColdRule(messages, counterpartyId) {
2554
+ const theyReplied = messages.some(
2555
+ (m) => m.authorBoardUserId === counterpartyId
2556
+ );
2557
+ const iSent = messages.some((m) => m.authorBoardUserId !== counterpartyId);
2558
+ return iSent && !theyReplied;
2559
+ }
2560
+ function lastOwnMessageId(messages, counterpartyId) {
2561
+ for (let i = messages.length - 1; i >= 0; i--) {
2562
+ const message = messages[i];
2563
+ if (message.authorBoardUserId !== counterpartyId) return message.id;
2564
+ }
2565
+ return null;
2566
+ }
2567
+
2508
2568
  // src/index.ts
2509
2569
  function createBoardClient(options) {
2510
2570
  const client = new BoardClient({
2511
2571
  baseUrl: options.baseUrl,
2512
2572
  board: options.board,
2513
- storage: resolveStorage(options.auth?.storage),
2573
+ storage: resolveStorage(options.auth?.storage, options.board),
2514
2574
  globalHeaders: options.globalHeaders,
2515
2575
  onRequest: options.onRequest,
2516
2576
  onResponse: options.onResponse,
@@ -2572,11 +2632,14 @@ export {
2572
2632
  createBoardClient,
2573
2633
  isBoardApiError,
2574
2634
  isBoardPasswordRequired,
2635
+ isColdRule,
2575
2636
  isConflict,
2576
2637
  isForbidden,
2577
2638
  isNotFound,
2639
+ isOwnMessage,
2578
2640
  isRateLimited,
2579
2641
  isUnauthorized,
2580
2642
  isValidationError,
2643
+ lastOwnMessageId,
2581
2644
  paginate
2582
2645
  };
@@ -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,206 @@
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/scope.ts
41
+ function scopeToken(board) {
42
+ let hash = 5381;
43
+ for (let i = 0; i < board.length; i++) {
44
+ hash = (hash << 5) + hash + board.charCodeAt(i) >>> 0;
45
+ }
46
+ const sanitized = board.replace(/[^a-zA-Z0-9_-]/g, "_");
47
+ return `${sanitized}-${hash.toString(36)}`;
48
+ }
49
+
50
+ // src/server/cookie.ts
51
+ var COOKIE_ATTRIBUTES = "Path=/; HttpOnly; Secure; SameSite=Lax";
52
+ function buildCookie(name, value, maxAgeSeconds) {
53
+ return `${name}=${value}; Max-Age=${maxAgeSeconds}; ${COOKIE_ATTRIBUTES}`;
54
+ }
55
+ function buildClearCookie(name) {
56
+ return `${name}=; Max-Age=0; ${COOKIE_ATTRIBUTES}`;
57
+ }
58
+ function readCookie(header, name) {
59
+ if (!header) return null;
60
+ const pair = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${name}=`));
61
+ if (!pair) return null;
62
+ const raw = pair.slice(name.length + 1);
63
+ if (!raw) return null;
64
+ try {
65
+ return decodeURIComponent(raw);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ // src/server/session.ts
72
+ var SESSION_COOKIE_NAME = "__Host-cavuno_board_session";
73
+ function sessionCookieName(board) {
74
+ return board ? `${SESSION_COOKIE_NAME}_${scopeToken(board)}` : SESSION_COOKIE_NAME;
75
+ }
76
+ var COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
77
+ var EXPIRY_WINDOW_MS = 5 * 60 * 1e3;
78
+ function serializeSessionCookie(session, options) {
79
+ return buildCookie(
80
+ sessionCookieName(options?.board),
81
+ encodeURIComponent(JSON.stringify(session)),
82
+ COOKIE_MAX_AGE_SECONDS
83
+ );
84
+ }
85
+ function clearSessionCookie(options) {
86
+ return buildClearCookie(sessionCookieName(options?.board));
87
+ }
88
+ function parseSessionCookie(cookieHeader, options) {
89
+ const raw = readCookie(cookieHeader, sessionCookieName(options?.board));
90
+ if (!raw) return null;
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(raw);
94
+ } catch {
95
+ return null;
96
+ }
97
+ const session = parsed;
98
+ if (!session || typeof session.accessToken !== "string" || typeof session.refreshToken !== "string" || typeof session.expiresAt !== "number") {
99
+ return null;
100
+ }
101
+ return {
102
+ accessToken: session.accessToken,
103
+ refreshToken: session.refreshToken,
104
+ expiresAt: session.expiresAt
105
+ };
106
+ }
107
+ function isExpiringSoon(session, now, windowMs = EXPIRY_WINDOW_MS) {
108
+ return session.expiresAt - now < windowMs;
109
+ }
110
+
111
+ // src/server/board-access.ts
112
+ var BOARD_ACCESS_COOKIE_NAME = "__Host-cavuno_board_access";
113
+ function grantCookieName(board) {
114
+ return board ? `${BOARD_ACCESS_COOKIE_NAME}_${scopeToken(board)}` : BOARD_ACCESS_COOKIE_NAME;
115
+ }
116
+ var COOKIE_MAX_AGE_SECONDS2 = 24 * 60 * 60;
117
+ function serializeGrantCookie(token, options) {
118
+ return buildCookie(
119
+ grantCookieName(options?.board),
120
+ encodeURIComponent(token),
121
+ COOKIE_MAX_AGE_SECONDS2
122
+ );
123
+ }
124
+ function clearGrantCookie(options) {
125
+ return buildClearCookie(grantCookieName(options?.board));
126
+ }
127
+ function parseGrantCookie(cookieHeader, options) {
128
+ return readCookie(cookieHeader, grantCookieName(options?.board));
129
+ }
130
+
131
+ // src/server/redirect-guard.ts
132
+ function safeRedirectPath(path, defaultPath = "/") {
133
+ if (!path || !path.startsWith("/") || path.startsWith("//")) {
134
+ return defaultPath;
135
+ }
136
+ if (path.includes("://")) return defaultPath;
137
+ try {
138
+ const SENTINEL_ORIGIN = "https://redirect.invalid";
139
+ if (new URL(path, SENTINEL_ORIGIN).origin !== SENTINEL_ORIGIN) {
140
+ return defaultPath;
141
+ }
142
+ } catch {
143
+ return defaultPath;
144
+ }
145
+ return path;
146
+ }
147
+ function currentPathFromReferer(referer) {
148
+ if (!referer) return "/";
149
+ try {
150
+ const url = new URL(referer);
151
+ return safeRedirectPath(url.pathname + url.search);
152
+ } catch {
153
+ return "/";
154
+ }
155
+ }
156
+
157
+ // src/errors.ts
158
+ function isBoardApiError(e) {
159
+ return e instanceof Error && e.name === "BoardApiError" && typeof e.status === "number" && typeof e.code === "string";
160
+ }
161
+ function isUnauthorized(e) {
162
+ return isBoardApiError(e) && e.status === 401;
163
+ }
164
+
165
+ // src/storage.ts
166
+ var NOSTORE_BRAND = /* @__PURE__ */ Symbol.for("@cavuno/board:nostore");
167
+ function isNoStore(storage) {
168
+ return storage[NOSTORE_BRAND] === true;
169
+ }
170
+ var NOSTORE = {
171
+ [NOSTORE_BRAND]: true,
172
+ getItem: () => null,
173
+ setItem: () => {
174
+ },
175
+ removeItem: () => {
176
+ }
177
+ };
178
+
179
+ // src/server/refresher.ts
180
+ function createSessionRefresher(board) {
181
+ if (typeof globalThis.document === "undefined" && !isNoStore(board.client.storage)) {
182
+ throw new Error(
183
+ "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."
184
+ );
185
+ }
186
+ const inflight = /* @__PURE__ */ new Map();
187
+ return async function refresh(session) {
188
+ const key = session.refreshToken;
189
+ const existing = inflight.get(key);
190
+ if (existing) return existing;
191
+ const attempt = board.auth.refresh({ refreshToken: key }).then(
192
+ (rotated) => ({
193
+ accessToken: rotated.accessToken,
194
+ refreshToken: rotated.refreshToken,
195
+ expiresAt: rotated.expiresAt
196
+ })
197
+ ).catch((error) => {
198
+ if (isUnauthorized(error)) return null;
199
+ throw error;
200
+ }).finally(() => {
201
+ inflight.delete(key);
202
+ });
203
+ inflight.set(key, attempt);
204
+ return attempt;
205
+ };
206
+ }