@meekojs/meeko 0.0.4 → 0.1.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.
Files changed (139) hide show
  1. package/README.md +21 -19
  2. package/dist/autoimports-ts.d.ts +4 -4
  3. package/dist/autoimports-ts.js +86 -30
  4. package/dist/autoimports-ts.js.map +1 -1
  5. package/dist/autoimports.d.ts +2 -2
  6. package/dist/autoimports.js +23 -7
  7. package/dist/autoimports.js.map +1 -1
  8. package/dist/check.js +39 -10
  9. package/dist/check.js.map +1 -1
  10. package/dist/cli.js +37 -4
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compiler/events.d.ts +2 -0
  13. package/dist/compiler/events.js +6 -0
  14. package/dist/compiler/events.js.map +1 -0
  15. package/dist/compiler/props.d.ts +12 -0
  16. package/dist/compiler/props.js +32 -0
  17. package/dist/compiler/props.js.map +1 -0
  18. package/dist/compiler.js +55 -12
  19. package/dist/compiler.js.map +1 -1
  20. package/dist/component-errors.d.ts +18 -0
  21. package/dist/component-errors.js +65 -0
  22. package/dist/component-errors.js.map +1 -0
  23. package/dist/devtools.js +2 -4
  24. package/dist/devtools.js.map +1 -1
  25. package/dist/dom.d.ts +11 -5
  26. package/dist/dom.js +288 -88
  27. package/dist/dom.js.map +1 -1
  28. package/dist/error-observer.d.ts +6 -0
  29. package/dist/error-observer.js +41 -0
  30. package/dist/error-observer.js.map +1 -0
  31. package/dist/framework/check-app.js +160 -5
  32. package/dist/framework/check-app.js.map +1 -1
  33. package/dist/framework/check-project.d.ts +4 -0
  34. package/dist/framework/check-project.js +42 -0
  35. package/dist/framework/check-project.js.map +1 -0
  36. package/dist/framework/client.d.ts +3 -0
  37. package/dist/framework/client.js +136 -33
  38. package/dist/framework/client.js.map +1 -1
  39. package/dist/framework/clients.js +12 -4
  40. package/dist/framework/clients.js.map +1 -1
  41. package/dist/framework/contracts-generate.js +66 -3
  42. package/dist/framework/contracts-generate.js.map +1 -1
  43. package/dist/framework/init.js +21 -200
  44. package/dist/framework/init.js.map +1 -1
  45. package/dist/framework/middleware.d.ts +14 -0
  46. package/dist/framework/middleware.js +16 -0
  47. package/dist/framework/middleware.js.map +1 -0
  48. package/dist/framework/page-contracts-check.d.ts +8 -0
  49. package/dist/framework/page-contracts-check.js +110 -0
  50. package/dist/framework/page-contracts-check.js.map +1 -0
  51. package/dist/framework/page-contracts-generate.d.ts +2 -0
  52. package/dist/framework/page-contracts-generate.js +94 -0
  53. package/dist/framework/page-contracts-generate.js.map +1 -0
  54. package/dist/framework/page.d.ts +7 -3
  55. package/dist/framework/page.js +3 -1
  56. package/dist/framework/page.js.map +1 -1
  57. package/dist/framework/prerender.js +11 -5
  58. package/dist/framework/prerender.js.map +1 -1
  59. package/dist/framework/project-templates.d.ts +3 -0
  60. package/dist/framework/project-templates.js +119 -0
  61. package/dist/framework/project-templates.js.map +1 -0
  62. package/dist/framework/project.d.ts +9 -0
  63. package/dist/framework/project.js +272 -0
  64. package/dist/framework/project.js.map +1 -0
  65. package/dist/framework/router.d.ts +3 -1
  66. package/dist/framework/router.js +94 -8
  67. package/dist/framework/router.js.map +1 -1
  68. package/dist/framework/server.d.ts +17 -3
  69. package/dist/framework/server.js +71 -24
  70. package/dist/framework/server.js.map +1 -1
  71. package/dist/framework/socket-contracts-generate.js +8 -5
  72. package/dist/framework/socket-contracts-generate.js.map +1 -1
  73. package/dist/framework/socket.d.ts +2 -0
  74. package/dist/framework/socket.js +119 -17
  75. package/dist/framework/socket.js.map +1 -1
  76. package/dist/framework/standard-schema-types.d.ts +7 -0
  77. package/dist/framework/standard-schema-types.js +69 -0
  78. package/dist/framework/standard-schema-types.js.map +1 -0
  79. package/dist/framework/state.d.ts +7 -0
  80. package/dist/framework/state.js +78 -14
  81. package/dist/framework/state.js.map +1 -1
  82. package/dist/framework/styles.d.ts +2 -0
  83. package/dist/framework/styles.js +6 -0
  84. package/dist/framework/styles.js.map +1 -0
  85. package/dist/framework/type-declarations.d.ts +5 -0
  86. package/dist/framework/type-declarations.js +84 -0
  87. package/dist/framework/type-declarations.js.map +1 -0
  88. package/dist/framework-server/contracts.d.ts +11 -2
  89. package/dist/framework-server/contracts.js +62 -7
  90. package/dist/framework-server/contracts.js.map +1 -1
  91. package/dist/framework-server/discovery.d.ts +4 -1
  92. package/dist/framework-server/discovery.js +21 -16
  93. package/dist/framework-server/discovery.js.map +1 -1
  94. package/dist/framework-server/local.d.ts +2 -0
  95. package/dist/framework-server/local.js +5 -3
  96. package/dist/framework-server/local.js.map +1 -1
  97. package/dist/framework-server/middleware.d.ts +4 -0
  98. package/dist/framework-server/middleware.js +56 -0
  99. package/dist/framework-server/middleware.js.map +1 -0
  100. package/dist/framework-server/node-upgrade.js +27 -9
  101. package/dist/framework-server/node-upgrade.js.map +1 -1
  102. package/dist/framework-server/node-websocket.js +4 -2
  103. package/dist/framework-server/node-websocket.js.map +1 -1
  104. package/dist/framework-server/session-stores.d.ts +9 -2
  105. package/dist/framework-server/session-stores.js +37 -11
  106. package/dist/framework-server/session-stores.js.map +1 -1
  107. package/dist/framework-server/sessions.d.ts +6 -0
  108. package/dist/framework-server/sessions.js +41 -10
  109. package/dist/framework-server/sessions.js.map +1 -1
  110. package/dist/framework-server/socket.js +62 -27
  111. package/dist/framework-server/socket.js.map +1 -1
  112. package/dist/framework-server.d.ts +2 -0
  113. package/dist/framework-server.js +1 -0
  114. package/dist/framework-server.js.map +1 -1
  115. package/dist/framework.d.ts +3 -1
  116. package/dist/framework.js +1 -0
  117. package/dist/framework.js.map +1 -1
  118. package/dist/hmr.js +74 -24
  119. package/dist/hmr.js.map +1 -1
  120. package/dist/index.d.ts +2 -0
  121. package/dist/index.js +1 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.js +27 -1
  124. package/dist/init.js.map +1 -1
  125. package/dist/internal.d.ts +2 -2
  126. package/dist/internal.js +2 -2
  127. package/dist/internal.js.map +1 -1
  128. package/dist/node.d.ts +5 -1
  129. package/dist/node.js +80 -10
  130. package/dist/node.js.map +1 -1
  131. package/dist/reactivity.d.ts +12 -3
  132. package/dist/reactivity.js +94 -17
  133. package/dist/reactivity.js.map +1 -1
  134. package/dist/types.d.ts +2 -0
  135. package/dist/types.js +3 -1
  136. package/dist/types.js.map +1 -1
  137. package/dist/vite.js +29 -9
  138. package/dist/vite.js.map +1 -1
  139. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"node-upgrade.js","sourceRoot":"","sources":["../../src/framework-server/node-upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport type { SocketHub, SocketPeer } from \"./socket.js\";\nimport { ApiError } from \"./contracts.js\";\n\n/** A WebSocket server library owns framing and the HTTP 101 response. */\nexport interface NodeAcceptedSocket {\n peer: SocketPeer;\n onMessage(listener: (message: string | Uint8Array) => void): void;\n onClose(listener: () => void): void;\n}\n\nexport interface NodeSocketUpgradeOptions {\n hub: SocketHub;\n /** Fixed public origin; never infer security policy from an untrusted Host header. */\n publicOrigin: string;\n pathnamePrefix?: string;\n upgrade(incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> | NodeAcceptedSocket;\n reject?(incoming: IncomingMessage, socket: Duplex, status: number): void;\n onError?(error: unknown): void;\n}\n\n/**\n * Handles the Node `upgrade` event after policy preflight. The host supplies a\n * WebSocket implementation; this adapter does not implement wire framing.\n */\nexport function createNodeSocketUpgradeHandler(options: NodeSocketUpgradeOptions): (incoming: IncomingMessage, socket: Duplex, head: Buffer) => Promise<boolean> {\n const origin = new URL(options.publicOrigin);\n if (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") throw new TypeError(\"Socket publicOrigin must use HTTP or HTTPS\");\n const prefix = options.pathnamePrefix ?? \"/_meeko/socket\";\n if (!prefix.startsWith(\"/\") || prefix.endsWith(\"/\")) throw new TypeError(\"Invalid socket pathname prefix\");\n return async (incoming, socket, head) => {\n let url: URL;\n try { url = new URL(incoming.url ?? \"\", origin); }\n catch { return false; }\n if (!url.pathname.startsWith(`${prefix}/`)) return false;\n const reject = (status: number) => { if (options.reject) options.reject(incoming, socket, status); else socket.destroy(); };\n const name = url.pathname.slice(prefix.length + 1);\n if (url.origin !== origin.origin || !/^[A-Za-z_$][\\w$-]*$/.test(name)) { reject(400); return true; }\n if (incoming.method !== \"GET\" || incoming.headers.upgrade?.toLowerCase() !== \"websocket\") { reject(400); return true; }\n const headers = new Headers();\n for (const [key, value] of Object.entries(incoming.headers)) {\n if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n const request = new Request(url, { headers });\n const roomId = url.searchParams.get(\"room\") ?? undefined;\n try { await options.hub.preflight(name, request, roomId); }\n catch (error) { reject(error instanceof ApiError ? error.status : 500); options.onError?.(error); return true; }\n let accepted: NodeAcceptedSocket;\n try { accepted = await options.upgrade(incoming, socket, head); }\n catch (error) { socket.destroy(); options.onError?.(error); return true; }\n try {\n const connection = await options.hub.accept(name, request, accepted.peer, roomId);\n let delivery = Promise.resolve();\n accepted.onMessage((message) => {\n delivery = delivery.then(() => connection.receive(message)).catch((error: unknown) => {\n options.onError?.(error);\n connection.close(error instanceof ApiError && error.status < 500 ? 1008 : 1011);\n });\n });\n accepted.onClose(() => connection.close());\n } catch (error) {\n accepted.peer.close(1008, \"Unauthorized\");\n options.onError?.(error);\n }\n return true;\n };\n}\n"]}
1
+ {"version":3,"file":"node-upgrade.js","sourceRoot":"","sources":["../../src/framework-server/node-upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage } from \"node:http\";\nimport type { Duplex } from \"node:stream\";\nimport type { SocketHub, SocketPeer } from \"./socket.js\";\nimport { normalizeServerError, observeErrorCallback } from \"./contracts.js\";\n\n/** A WebSocket server library owns framing and the HTTP 101 response. */\nexport interface NodeAcceptedSocket {\n peer: SocketPeer;\n onMessage(listener: (message: string | Uint8Array) => void): void;\n onClose(listener: () => void): void;\n}\n\nexport interface NodeSocketUpgradeOptions {\n hub: SocketHub;\n /** Fixed public origin; never infer security policy from an untrusted Host header. */\n publicOrigin: string;\n pathnamePrefix?: string;\n upgrade(incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> | NodeAcceptedSocket;\n reject?(incoming: IncomingMessage, socket: Duplex, status: number): void;\n onError?(error: unknown): void;\n}\n\n/**\n * Handles the Node `upgrade` event after policy preflight. The host supplies a\n * WebSocket implementation; this adapter does not implement wire framing.\n */\nexport function createNodeSocketUpgradeHandler(options: NodeSocketUpgradeOptions): (incoming: IncomingMessage, socket: Duplex, head: Buffer) => Promise<boolean> {\n const origin = new URL(options.publicOrigin);\n if (origin.protocol !== \"http:\" && origin.protocol !== \"https:\") throw new TypeError(\"Socket publicOrigin must use HTTP or HTTPS\");\n const prefix = options.pathnamePrefix ?? \"/_meeko/socket\";\n if (!prefix.startsWith(\"/\") || prefix.endsWith(\"/\")) throw new TypeError(\"Invalid socket pathname prefix\");\n const observeError = (error: unknown): void => observeErrorCallback(error, options.onError);\n return async (incoming, socket, head) => {\n let url: URL;\n try { url = new URL(incoming.url ?? \"\", origin); }\n catch { return false; }\n if (!url.pathname.startsWith(`${prefix}/`)) return false;\n const reject = (status: number) => { if (options.reject) options.reject(incoming, socket, status); else socket.destroy(); };\n const name = url.pathname.slice(prefix.length + 1);\n if (url.origin !== origin.origin || !/^[A-Za-z_$][\\w$-]*$/.test(name)) { reject(400); return true; }\n if (incoming.method !== \"GET\" || incoming.headers.upgrade?.toLowerCase() !== \"websocket\") { reject(400); return true; }\n const headers = new Headers();\n for (const [key, value] of Object.entries(incoming.headers)) {\n if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n const request = new Request(url, { headers });\n const roomId = url.searchParams.get(\"room\") ?? undefined;\n try { await options.hub.preflight(name, request, roomId); }\n catch (error) { reject(normalizeServerError(error).status); observeError(error); return true; }\n let accepted: NodeAcceptedSocket;\n try { accepted = await options.upgrade(incoming, socket, head); }\n catch (error) { socket.destroy(); observeError(error); return true; }\n try {\n const connection = await options.hub.accept(name, request, accepted.peer, roomId);\n let delivery = Promise.resolve();\n accepted.onMessage((message) => {\n delivery = delivery.then(() => connection.receive(message)).catch((error: unknown) => {\n observeError(error);\n try { connection.close(normalizeServerError(error).status < 500 ? 1008 : 1011); }\n catch (closeError) { observeError(closeError); }\n });\n });\n accepted.onClose(() => {\n try { connection.close(); }\n catch (error) { observeError(error); }\n });\n } catch (error) {\n try { accepted.peer.close(1008, \"Unauthorized\"); }\n catch (closeError) { observeError(closeError); }\n observeError(error);\n }\n return true;\n };\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { STATUS_CODES } from "node:http";
2
2
  import { createRequire } from "node:module";
3
3
  import { createNodeSocketUpgradeHandler } from "./node-upgrade.js";
4
+ import { observeErrorCallback } from "./contracts.js";
4
5
  /** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */
5
6
  export function attachNodeWebSocketServer(options) {
6
7
  const maxPayload = options.maxMessageBytes ?? 64 * 1024;
@@ -12,6 +13,7 @@ export function attachNodeWebSocketServer(options) {
12
13
  // Keep the optional server transport out of engine-only and HTTP-only imports.
13
14
  const { WebSocket: WebSocketClass, WebSocketServer } = createRequire(import.meta.url)("ws");
14
15
  const webSockets = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: false });
16
+ const observeError = (error) => observeErrorCallback(error, options.onError);
15
17
  const alive = new WeakMap();
16
18
  let closing = false;
17
19
  let closePromise;
@@ -20,7 +22,7 @@ export function attachNodeWebSocketServer(options) {
20
22
  webSockets.handleUpgrade(incoming, socket, head, (peer) => {
21
23
  alive.set(peer, true);
22
24
  peer.on("pong", () => alive.set(peer, true));
23
- peer.on("error", (error) => options.onError?.(error));
25
+ peer.on("error", observeError);
24
26
  const pending = [];
25
27
  let pendingBytes = 0;
26
28
  let messageListener;
@@ -91,7 +93,7 @@ export function attachNodeWebSocketServer(options) {
91
93
  if (!handled)
92
94
  socket.destroy();
93
95
  }).catch((error) => {
94
- options.onError?.(error);
96
+ observeError(error);
95
97
  socket.destroy();
96
98
  });
97
99
  };
@@ -1 +1 @@
1
- {"version":3,"file":"node-websocket.js","sourceRoot":"","sources":["../../src/framework-server/node-websocket.ts"],"names":[],"mappings":"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","sourcesContent":["import { STATUS_CODES, type IncomingMessage, type Server as HttpServer } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport type { Duplex } from \"node:stream\";\nimport type WebSocket from \"ws\";\nimport { createNodeSocketUpgradeHandler } from \"./node-upgrade.js\";\nimport type { NodeAcceptedSocket, NodeSocketUpgradeOptions } from \"./node-upgrade.js\";\n\nexport interface NodeWebSocketOptions extends Omit<NodeSocketUpgradeOptions, \"upgrade\" | \"reject\"> {\n /** The same HTTP server used by the Fetch handler. */\n server: HttpServer;\n /** Maximum WebSocket frame payload, enforced before decoding. Defaults to 64 KiB. */\n maxMessageBytes?: number;\n /** Ping interval and idle timeout. Defaults to 30 seconds. */\n heartbeatMs?: number;\n}\n\nexport interface NodeWebSocketServer {\n /** Detaches the upgrade listener and closes all peers. The HTTP server remains owned by its caller. */\n close(): Promise<void>;\n}\n\n/** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */\nexport function attachNodeWebSocketServer(options: NodeWebSocketOptions): NodeWebSocketServer {\n const maxPayload = options.maxMessageBytes ?? 64 * 1024;\n const heartbeatMs = options.heartbeatMs ?? 30_000;\n if (!Number.isSafeInteger(maxPayload) || maxPayload <= 0) throw new RangeError(\"Invalid socket message limit\");\n if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs <= 0) throw new RangeError(\"Invalid socket heartbeat interval\");\n // Keep the optional server transport out of engine-only and HTTP-only imports.\n const { WebSocket: WebSocketClass, WebSocketServer } = createRequire(import.meta.url)(\"ws\") as typeof import(\"ws\");\n const webSockets = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: false });\n const alive = new WeakMap<WebSocket, boolean>();\n let closing = false;\n let closePromise: Promise<void> | undefined;\n\n const upgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> =>\n new Promise((resolve, reject) => {\n try {\n webSockets.handleUpgrade(incoming, socket, head, (peer) => {\n alive.set(peer, true);\n peer.on(\"pong\", () => alive.set(peer, true));\n peer.on(\"error\", (error) => options.onError?.(error));\n const pending: Uint8Array[] = [];\n let pendingBytes = 0;\n let messageListener: ((message: string | Uint8Array) => void) | undefined;\n let closeListener: (() => void) | undefined;\n let didClose = false;\n peer.on(\"message\", (data, isBinary) => {\n if (isBinary) { peer.close(1003, \"Text messages required\"); return; }\n const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);\n if (messageListener) { messageListener(bytes); return; }\n pendingBytes += bytes.byteLength;\n if (pendingBytes > maxPayload) { peer.close(1009, \"Message buffer exceeded\"); return; }\n pending.push(bytes);\n });\n peer.once(\"close\", () => { didClose = true; closeListener?.(); });\n resolve({\n peer: {\n send(message) {\n if (peer.readyState !== WebSocketClass.OPEN) throw new Error(\"Socket is disconnected\");\n peer.send(message);\n },\n close(code, reason) { if (peer.readyState === WebSocketClass.OPEN) peer.close(code, reason); },\n get bufferedAmount() { return peer.bufferedAmount; },\n },\n onMessage(listener) {\n messageListener = listener;\n for (const message of pending) listener(message);\n pending.length = 0;\n pendingBytes = 0;\n },\n onClose(listener) { closeListener = listener; if (didClose) listener(); },\n });\n });\n } catch (error) { reject(error); }\n });\n\n const bridge = createNodeSocketUpgradeHandler({\n hub: options.hub,\n publicOrigin: options.publicOrigin,\n ...(options.pathnamePrefix ? { pathnamePrefix: options.pathnamePrefix } : {}),\n upgrade,\n reject(_incoming, socket, status) {\n if (!socket.destroyed) socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? \"Error\"}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n`);\n },\n ...(options.onError ? { onError: options.onError } : {}),\n });\n const onUpgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): void => {\n if (closing) { socket.destroy(); return; }\n void bridge(incoming, socket, head).then((handled) => {\n if (!handled) socket.destroy();\n }).catch((error: unknown) => {\n options.onError?.(error);\n socket.destroy();\n });\n };\n options.server.on(\"upgrade\", onUpgrade);\n const heartbeat = setInterval(() => {\n for (const peer of webSockets.clients) {\n if (alive.get(peer) === false) { peer.terminate(); continue; }\n alive.set(peer, false);\n if (peer.readyState === WebSocketClass.OPEN) peer.ping();\n }\n }, heartbeatMs);\n heartbeat.unref();\n\n return {\n close() {\n if (closePromise) return closePromise;\n closing = true;\n options.server.off(\"upgrade\", onUpgrade);\n clearInterval(heartbeat);\n for (const peer of webSockets.clients) peer.terminate();\n closePromise = new Promise<void>((resolve, reject) => webSockets.close((error) => error ? reject(error) : resolve()));\n return closePromise;\n },\n };\n}\n"]}
1
+ {"version":3,"file":"node-websocket.js","sourceRoot":"","sources":["../../src/framework-server/node-websocket.ts"],"names":[],"mappings":"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","sourcesContent":["import { STATUS_CODES, type IncomingMessage, type Server as HttpServer } from \"node:http\";\nimport { createRequire } from \"node:module\";\nimport type { Duplex } from \"node:stream\";\nimport type WebSocket from \"ws\";\nimport { createNodeSocketUpgradeHandler } from \"./node-upgrade.js\";\nimport { observeErrorCallback } from \"./contracts.js\";\nimport type { NodeAcceptedSocket, NodeSocketUpgradeOptions } from \"./node-upgrade.js\";\n\nexport interface NodeWebSocketOptions extends Omit<NodeSocketUpgradeOptions, \"upgrade\" | \"reject\"> {\n /** The same HTTP server used by the Fetch handler. */\n server: HttpServer;\n /** Maximum WebSocket frame payload, enforced before decoding. Defaults to 64 KiB. */\n maxMessageBytes?: number;\n /** Ping interval and idle timeout. Defaults to 30 seconds. */\n heartbeatMs?: number;\n}\n\nexport interface NodeWebSocketServer {\n /** Detaches the upgrade listener and closes all peers. The HTTP server remains owned by its caller. */\n close(): Promise<void>;\n}\n\n/** Attach Meeko channels to a Node HTTP server using ws for protocol framing. */\nexport function attachNodeWebSocketServer(options: NodeWebSocketOptions): NodeWebSocketServer {\n const maxPayload = options.maxMessageBytes ?? 64 * 1024;\n const heartbeatMs = options.heartbeatMs ?? 30_000;\n if (!Number.isSafeInteger(maxPayload) || maxPayload <= 0) throw new RangeError(\"Invalid socket message limit\");\n if (!Number.isSafeInteger(heartbeatMs) || heartbeatMs <= 0) throw new RangeError(\"Invalid socket heartbeat interval\");\n // Keep the optional server transport out of engine-only and HTTP-only imports.\n const { WebSocket: WebSocketClass, WebSocketServer } = createRequire(import.meta.url)(\"ws\") as typeof import(\"ws\");\n const webSockets = new WebSocketServer({ noServer: true, maxPayload, perMessageDeflate: false });\n const observeError = (error: unknown): void => observeErrorCallback(error, options.onError);\n const alive = new WeakMap<WebSocket, boolean>();\n let closing = false;\n let closePromise: Promise<void> | undefined;\n\n const upgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): Promise<NodeAcceptedSocket> =>\n new Promise((resolve, reject) => {\n try {\n webSockets.handleUpgrade(incoming, socket, head, (peer) => {\n alive.set(peer, true);\n peer.on(\"pong\", () => alive.set(peer, true));\n peer.on(\"error\", observeError);\n const pending: Uint8Array[] = [];\n let pendingBytes = 0;\n let messageListener: ((message: string | Uint8Array) => void) | undefined;\n let closeListener: (() => void) | undefined;\n let didClose = false;\n peer.on(\"message\", (data, isBinary) => {\n if (isBinary) { peer.close(1003, \"Text messages required\"); return; }\n const bytes = Array.isArray(data) ? Buffer.concat(data) : Buffer.isBuffer(data) ? data : Buffer.from(data);\n if (messageListener) { messageListener(bytes); return; }\n pendingBytes += bytes.byteLength;\n if (pendingBytes > maxPayload) { peer.close(1009, \"Message buffer exceeded\"); return; }\n pending.push(bytes);\n });\n peer.once(\"close\", () => { didClose = true; closeListener?.(); });\n resolve({\n peer: {\n send(message) {\n if (peer.readyState !== WebSocketClass.OPEN) throw new Error(\"Socket is disconnected\");\n peer.send(message);\n },\n close(code, reason) { if (peer.readyState === WebSocketClass.OPEN) peer.close(code, reason); },\n get bufferedAmount() { return peer.bufferedAmount; },\n },\n onMessage(listener) {\n messageListener = listener;\n for (const message of pending) listener(message);\n pending.length = 0;\n pendingBytes = 0;\n },\n onClose(listener) { closeListener = listener; if (didClose) listener(); },\n });\n });\n } catch (error) { reject(error); }\n });\n\n const bridge = createNodeSocketUpgradeHandler({\n hub: options.hub,\n publicOrigin: options.publicOrigin,\n ...(options.pathnamePrefix ? { pathnamePrefix: options.pathnamePrefix } : {}),\n upgrade,\n reject(_incoming, socket, status) {\n if (!socket.destroyed) socket.end(`HTTP/1.1 ${status} ${STATUS_CODES[status] ?? \"Error\"}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n`);\n },\n ...(options.onError ? { onError: options.onError } : {}),\n });\n const onUpgrade = (incoming: IncomingMessage, socket: Duplex, head: Buffer): void => {\n if (closing) { socket.destroy(); return; }\n void bridge(incoming, socket, head).then((handled) => {\n if (!handled) socket.destroy();\n }).catch((error: unknown) => {\n observeError(error);\n socket.destroy();\n });\n };\n options.server.on(\"upgrade\", onUpgrade);\n const heartbeat = setInterval(() => {\n for (const peer of webSockets.clients) {\n if (alive.get(peer) === false) { peer.terminate(); continue; }\n alive.set(peer, false);\n if (peer.readyState === WebSocketClass.OPEN) peer.ping();\n }\n }, heartbeatMs);\n heartbeat.unref();\n\n return {\n close() {\n if (closePromise) return closePromise;\n closing = true;\n options.server.off(\"upgrade\", onUpgrade);\n clearInterval(heartbeat);\n for (const peer of webSockets.clients) peer.terminate();\n closePromise = new Promise<void>((resolve, reject) => webSockets.close((error) => error ? reject(error) : resolve()));\n return closePromise;\n },\n };\n}\n"]}
@@ -25,15 +25,19 @@ export declare class RedisSessionStore<T = unknown> implements SessionStore<T> {
25
25
  readonly persistent = true;
26
26
  constructor(client: RedisSessionClient, options: RedisSessionStoreOptions);
27
27
  private key;
28
+ private familyKey;
28
29
  get(id: string): Promise<SessionRecord<T> | undefined>;
29
30
  set(record: SessionRecord<T>): Promise<void>;
30
31
  delete(id: string): Promise<void>;
31
32
  replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
33
+ revokeFamily(familyId: string): Promise<void>;
32
34
  close(): Promise<void>;
33
35
  }
34
36
  /** A driver can implement these operations using any SQL client and dialect. */
35
37
  export interface SqlSessionRow {
36
38
  id: string;
39
+ /** Migrate existing rows by setting family_id = id before deploying this driver. */
40
+ familyId: string;
37
41
  csrfToken: string;
38
42
  dataJson: string;
39
43
  createdAt: number;
@@ -41,11 +45,13 @@ export interface SqlSessionRow {
41
45
  }
42
46
  export interface SqlSessionDriver {
43
47
  read(id: string): Promise<SqlSessionRow | undefined>;
44
- /** Insert or update one row, keyed by its opaque id. */
48
+ /** Insert or update one row, keyed by its opaque id; enforce one active id per family. */
45
49
  write(row: SqlSessionRow): Promise<void>;
46
50
  remove(id: string): Promise<void>;
47
- /** Atomically replace an unexpired row in one transaction; return false if absent or expired. */
51
+ /** Atomically replace an unexpired row of the same family in one transaction. */
48
52
  replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;
53
+ /** Delete the active row for this family in one atomic operation, including stale-handle logout. */
54
+ revokeFamily(familyId: string): Promise<void>;
49
55
  close?(): Promise<void>;
50
56
  }
51
57
  export interface SqlSessionStoreOptions {
@@ -62,5 +68,6 @@ export declare class SqlSessionStore<T = unknown> implements SessionStore<T> {
62
68
  set(record: SessionRecord<T>): Promise<void>;
63
69
  delete(id: string): Promise<void>;
64
70
  replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
71
+ revokeFamily(familyId: string): Promise<void>;
65
72
  close(): Promise<void>;
66
73
  }
@@ -12,7 +12,8 @@ function decodeRecord(value, id) {
12
12
  const candidate = record;
13
13
  if (candidate.id !== id || typeof candidate.csrfToken !== "string" || !Object.hasOwn(candidate, "data") ||
14
14
  typeof candidate.createdAt !== "number" || typeof candidate.expiresAt !== "number" ||
15
- !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt)) {
15
+ !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt) ||
16
+ (candidate.familyId !== undefined && (typeof candidate.familyId !== "string" || candidate.familyId.length === 0))) {
16
17
  throw new Error("Corrupt session record");
17
18
  }
18
19
  return candidate;
@@ -29,13 +30,19 @@ export class RedisSessionStore {
29
30
  throw new TypeError("Session Redis prefix must be nonempty, contain no whitespace, and end with ':'");
30
31
  }
31
32
  key(id) { return `${this.options.prefix}${id}`; }
33
+ familyKey(id) { return `${this.options.prefix}family:${id}`; }
32
34
  async get(id) {
33
35
  const value = await this.client.get(this.key(id));
34
36
  if (value === null)
35
37
  return undefined;
36
38
  const record = decodeRecord(value, id);
37
- if (record.expiresAt > Date.now())
38
- return record;
39
+ if (record.expiresAt > Date.now()) {
40
+ // Older records without a family id are migrated by the first rotation.
41
+ if (!record.familyId || await this.client.get(this.familyKey(record.familyId)) === id)
42
+ return record;
43
+ await this.delete(id);
44
+ return undefined;
45
+ }
39
46
  await this.delete(id);
40
47
  return undefined;
41
48
  }
@@ -43,18 +50,32 @@ export class RedisSessionStore {
43
50
  const ttlMs = record.expiresAt - Date.now();
44
51
  if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
45
52
  throw new RangeError("Session must have a future expiration");
46
- await this.client.set(this.key(record.id), serializeJson(record), { PX: ttlMs });
53
+ const familyId = record.familyId ?? record.id;
54
+ const result = await this.client.eval("-- meeko:session:set\nlocal prior = redis.call('GET', KEYS[1]) if prior then local old = cjson.decode(prior) if (old.familyId or old.id) ~= ARGV[4] then return 0 end end local active = redis.call('GET', KEYS[2]) if active and active ~= ARGV[2] then return 0 end redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[3]) redis.call('SET', KEYS[2], ARGV[2], 'PX', ARGV[3]) return 1", { keys: [this.key(record.id), this.familyKey(familyId)], arguments: [serializeJson({ ...record, familyId }), record.id, String(ttlMs), familyId] });
55
+ if (result !== 1)
56
+ throw new Error("Session family already has an active id");
57
+ }
58
+ async delete(id) {
59
+ const value = await this.client.get(this.key(id));
60
+ if (value === null)
61
+ return;
62
+ const record = decodeRecord(value, id);
63
+ await this.client.eval("-- meeko:session:delete\nlocal raw = redis.call('GET', KEYS[1]) if not raw then return 0 end local active = redis.call('GET', KEYS[2]) redis.call('DEL', KEYS[1]) if active == ARGV[1] then redis.call('DEL', KEYS[2]) end return 1", { keys: [this.key(id), this.familyKey(record.familyId ?? id)], arguments: [id] });
47
64
  }
48
- async delete(id) { await this.client.del(this.key(id)); }
49
65
  async replace(previousId, record) {
50
66
  const ttlMs = record.expiresAt - Date.now();
51
67
  if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
52
68
  throw new RangeError("Session must have a future expiration");
53
- const result = await this.client.eval("if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[2]) redis.call('DEL', KEYS[1]) return 1", { keys: [this.key(previousId), this.key(record.id)], arguments: [serializeJson(record), String(ttlMs)] });
69
+ if (!record.familyId)
70
+ throw new Error("Rotated session requires a family id");
71
+ const result = await this.client.eval("-- meeko:session:replace\nlocal raw = redis.call('GET', KEYS[1]) if not raw then return 0 end local previous = cjson.decode(raw) local family = previous.familyId or previous.id if family ~= ARGV[3] or previous.expiresAt <= tonumber(ARGV[4]) then return 0 end local active = redis.call('GET', KEYS[3]) if active and active ~= ARGV[2] then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[5]) redis.call('SET', KEYS[3], ARGV[6], 'PX', ARGV[5]) redis.call('DEL', KEYS[1]) return 1", { keys: [this.key(previousId), this.key(record.id), this.familyKey(record.familyId)], arguments: [serializeJson(record), previousId, record.familyId, String(Date.now()), String(ttlMs), record.id] });
54
72
  if (result !== 0 && result !== 1)
55
73
  throw new Error("Unexpected Redis rotation result");
56
74
  return result === 1;
57
75
  }
76
+ async revokeFamily(familyId) {
77
+ await this.client.eval("-- meeko:session:revoke-family\nlocal active = redis.call('GET', KEYS[1]) if active then redis.call('DEL', ARGV[1] .. active) redis.call('DEL', KEYS[1]) else redis.call('DEL', ARGV[1] .. ARGV[2]) end return 1", { keys: [this.familyKey(familyId)], arguments: [this.options.prefix, familyId] });
78
+ }
58
79
  async close() { if (this.options.closeClient)
59
80
  await this.client.quit?.(); }
60
81
  }
@@ -66,14 +87,16 @@ export class SqlSessionStore {
66
87
  constructor(driver, options = {}) {
67
88
  this.driver = driver;
68
89
  this.options = options;
90
+ if (typeof driver.replace !== "function" || typeof driver.revokeFamily !== "function")
91
+ throw new TypeError("SQL session driver requires atomic replace and family revocation");
69
92
  }
70
93
  async get(id) {
71
94
  const row = await this.driver.read(id);
72
95
  if (!row)
73
96
  return undefined;
74
- if (row.id !== id || typeof row.dataJson !== "string")
75
- throw new Error("Corrupt session record");
76
- const record = decodeRecord(serializeJson({ id: row.id, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson), createdAt: row.createdAt, expiresAt: row.expiresAt }), id);
97
+ if (row.id !== id || typeof row.dataJson !== "string" || typeof row.familyId !== "string" || !row.familyId)
98
+ throw new Error("Session SQL schema requires a nonempty family_id column");
99
+ const record = decodeRecord(serializeJson({ id: row.id, familyId: row.familyId, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson), createdAt: row.createdAt, expiresAt: row.expiresAt }), id);
77
100
  if (record.expiresAt > Date.now())
78
101
  return record;
79
102
  await this.delete(id);
@@ -82,14 +105,17 @@ export class SqlSessionStore {
82
105
  async set(record) {
83
106
  if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
84
107
  throw new RangeError("Session must have a future expiration");
85
- await this.driver.write({ id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });
108
+ await this.driver.write({ id: record.id, familyId: record.familyId ?? record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });
86
109
  }
87
110
  async delete(id) { await this.driver.remove(id); }
88
111
  async replace(previousId, record) {
89
112
  if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
90
113
  throw new RangeError("Session must have a future expiration");
91
- return this.driver.replace(previousId, { id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());
114
+ if (!record.familyId)
115
+ throw new Error("Rotated session requires a family id");
116
+ return this.driver.replace(previousId, { id: record.id, familyId: record.familyId, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());
92
117
  }
118
+ async revokeFamily(familyId) { await this.driver.revokeFamily(familyId); }
93
119
  async close() { if (this.options.closeDriver)
94
120
  await this.driver.close?.(); }
95
121
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-stores.js","sourceRoot":"","sources":["../../src/framework-server/session-stores.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeJson } from \"../server.js\";\nimport type { SessionRecord, SessionStore } from \"./sessions.js\";\n\n/** Small interface compatible with Redis clients that support SET PX. */\nexport interface RedisSessionClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, options: { PX: number }): Promise<unknown>;\n del(key: string): Promise<unknown>;\n eval(script: string, options: { keys: string[]; arguments: string[] }): Promise<unknown>;\n quit?(): Promise<unknown>;\n}\n\nexport interface RedisSessionStoreOptions {\n /** Use a unique prefix for each application and environment. */\n prefix: string;\n /** Close the Redis client when this store owns its connection. */\n closeClient?: boolean;\n}\n\nfunction decodeRecord<T>(value: string, id: string): SessionRecord<T> {\n let record: unknown;\n try { record = JSON.parse(value) as unknown; }\n catch { throw new Error(\"Corrupt session record\"); }\n if (!record || typeof record !== \"object\") throw new Error(\"Corrupt session record\");\n const candidate = record as Partial<SessionRecord<T>>;\n if (candidate.id !== id || typeof candidate.csrfToken !== \"string\" || !Object.hasOwn(candidate, \"data\") ||\n typeof candidate.createdAt !== \"number\" || typeof candidate.expiresAt !== \"number\" ||\n !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt)) {\n throw new Error(\"Corrupt session record\");\n }\n return candidate as SessionRecord<T>;\n}\n\n/** Persistent store with Redis-managed expiration and explicit connection ownership. */\nexport class RedisSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly client: RedisSessionClient, private readonly options: RedisSessionStoreOptions) {\n if (!options.prefix || !options.prefix.endsWith(\":\") || /\\s/.test(options.prefix)) throw new TypeError(\"Session Redis prefix must be nonempty, contain no whitespace, and end with ':'\");\n }\n private key(id: string): string { return `${this.options.prefix}${id}`; }\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const value = await this.client.get(this.key(id));\n if (value === null) return undefined;\n const record = decodeRecord<T>(value, id);\n if (record.expiresAt > Date.now()) return record;\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n await this.client.set(this.key(record.id), serializeJson(record), { PX: ttlMs });\n }\n async delete(id: string): Promise<void> { await this.client.del(this.key(id)); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n const result = await this.client.eval(\n \"if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[2]) redis.call('DEL', KEYS[1]) return 1\",\n { keys: [this.key(previousId), this.key(record.id)], arguments: [serializeJson(record), String(ttlMs)] },\n );\n if (result !== 0 && result !== 1) throw new Error(\"Unexpected Redis rotation result\");\n return result === 1;\n }\n async close(): Promise<void> { if (this.options.closeClient) await this.client.quit?.(); }\n}\n\n/** A driver can implement these operations using any SQL client and dialect. */\nexport interface SqlSessionRow {\n id: string;\n csrfToken: string;\n dataJson: string;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SqlSessionDriver {\n read(id: string): Promise<SqlSessionRow | undefined>;\n /** Insert or update one row, keyed by its opaque id. */\n write(row: SqlSessionRow): Promise<void>;\n remove(id: string): Promise<void>;\n /** Atomically replace an unexpired row in one transaction; return false if absent or expired. */\n replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;\n close?(): Promise<void>;\n}\n\nexport interface SqlSessionStoreOptions {\n /** Close the SQL driver when this store owns its connection. */\n closeDriver?: boolean;\n}\n\n/** Generic SQL store. The application supplies SQL statements through its driver. */\nexport class SqlSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly driver: SqlSessionDriver, private readonly options: SqlSessionStoreOptions = {}) {}\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const row = await this.driver.read(id);\n if (!row) return undefined;\n if (row.id !== id || typeof row.dataJson !== \"string\") throw new Error(\"Corrupt session record\");\n const record = decodeRecord<T>(serializeJson({ id: row.id, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson) as unknown, createdAt: row.createdAt, expiresAt: row.expiresAt }), id);\n if (record.expiresAt > Date.now()) return record;\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n await this.driver.write({ id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });\n }\n async delete(id: string): Promise<void> { await this.driver.remove(id); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n return this.driver.replace(previousId, { id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());\n }\n async close(): Promise<void> { if (this.options.closeDriver) await this.driver.close?.(); }\n}\n"]}
1
+ {"version":3,"file":"session-stores.js","sourceRoot":"","sources":["../../src/framework-server/session-stores.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeJson } from \"../server.js\";\nimport type { SessionRecord, SessionStore } from \"./sessions.js\";\n\n/** Small interface compatible with Redis clients that support SET PX. */\nexport interface RedisSessionClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, options: { PX: number }): Promise<unknown>;\n del(key: string): Promise<unknown>;\n eval(script: string, options: { keys: string[]; arguments: string[] }): Promise<unknown>;\n quit?(): Promise<unknown>;\n}\n\nexport interface RedisSessionStoreOptions {\n /** Use a unique prefix for each application and environment. */\n prefix: string;\n /** Close the Redis client when this store owns its connection. */\n closeClient?: boolean;\n}\n\nfunction decodeRecord<T>(value: string, id: string): SessionRecord<T> {\n let record: unknown;\n try { record = JSON.parse(value) as unknown; }\n catch { throw new Error(\"Corrupt session record\"); }\n if (!record || typeof record !== \"object\") throw new Error(\"Corrupt session record\");\n const candidate = record as Partial<SessionRecord<T>>;\n if (candidate.id !== id || typeof candidate.csrfToken !== \"string\" || !Object.hasOwn(candidate, \"data\") ||\n typeof candidate.createdAt !== \"number\" || typeof candidate.expiresAt !== \"number\" ||\n !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt) ||\n (candidate.familyId !== undefined && (typeof candidate.familyId !== \"string\" || candidate.familyId.length === 0))) {\n throw new Error(\"Corrupt session record\");\n }\n return candidate as SessionRecord<T>;\n}\n\n/** Persistent store with Redis-managed expiration and explicit connection ownership. */\nexport class RedisSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly client: RedisSessionClient, private readonly options: RedisSessionStoreOptions) {\n if (!options.prefix || !options.prefix.endsWith(\":\") || /\\s/.test(options.prefix)) throw new TypeError(\"Session Redis prefix must be nonempty, contain no whitespace, and end with ':'\");\n }\n private key(id: string): string { return `${this.options.prefix}${id}`; }\n private familyKey(id: string): string { return `${this.options.prefix}family:${id}`; }\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const value = await this.client.get(this.key(id));\n if (value === null) return undefined;\n const record = decodeRecord<T>(value, id);\n if (record.expiresAt > Date.now()) {\n // Older records without a family id are migrated by the first rotation.\n if (!record.familyId || await this.client.get(this.familyKey(record.familyId)) === id) return record;\n await this.delete(id);\n return undefined;\n }\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n const familyId = record.familyId ?? record.id;\n const result = await this.client.eval(\n \"-- meeko:session:set\\nlocal prior = redis.call('GET', KEYS[1]) if prior then local old = cjson.decode(prior) if (old.familyId or old.id) ~= ARGV[4] then return 0 end end local active = redis.call('GET', KEYS[2]) if active and active ~= ARGV[2] then return 0 end redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[3]) redis.call('SET', KEYS[2], ARGV[2], 'PX', ARGV[3]) return 1\",\n { keys: [this.key(record.id), this.familyKey(familyId)], arguments: [serializeJson({ ...record, familyId }), record.id, String(ttlMs), familyId] },\n );\n if (result !== 1) throw new Error(\"Session family already has an active id\");\n }\n async delete(id: string): Promise<void> {\n const value = await this.client.get(this.key(id));\n if (value === null) return;\n const record = decodeRecord<T>(value, id);\n await this.client.eval(\n \"-- meeko:session:delete\\nlocal raw = redis.call('GET', KEYS[1]) if not raw then return 0 end local active = redis.call('GET', KEYS[2]) redis.call('DEL', KEYS[1]) if active == ARGV[1] then redis.call('DEL', KEYS[2]) end return 1\",\n { keys: [this.key(id), this.familyKey(record.familyId ?? id)], arguments: [id] },\n );\n }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n if (!record.familyId) throw new Error(\"Rotated session requires a family id\");\n const result = await this.client.eval(\n \"-- meeko:session:replace\\nlocal raw = redis.call('GET', KEYS[1]) if not raw then return 0 end local previous = cjson.decode(raw) local family = previous.familyId or previous.id if family ~= ARGV[3] or previous.expiresAt <= tonumber(ARGV[4]) then return 0 end local active = redis.call('GET', KEYS[3]) if active and active ~= ARGV[2] then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[5]) redis.call('SET', KEYS[3], ARGV[6], 'PX', ARGV[5]) redis.call('DEL', KEYS[1]) return 1\",\n { keys: [this.key(previousId), this.key(record.id), this.familyKey(record.familyId)], arguments: [serializeJson(record), previousId, record.familyId, String(Date.now()), String(ttlMs), record.id] },\n );\n if (result !== 0 && result !== 1) throw new Error(\"Unexpected Redis rotation result\");\n return result === 1;\n }\n async revokeFamily(familyId: string): Promise<void> {\n await this.client.eval(\n \"-- meeko:session:revoke-family\\nlocal active = redis.call('GET', KEYS[1]) if active then redis.call('DEL', ARGV[1] .. active) redis.call('DEL', KEYS[1]) else redis.call('DEL', ARGV[1] .. ARGV[2]) end return 1\",\n { keys: [this.familyKey(familyId)], arguments: [this.options.prefix, familyId] },\n );\n }\n async close(): Promise<void> { if (this.options.closeClient) await this.client.quit?.(); }\n}\n\n/** A driver can implement these operations using any SQL client and dialect. */\nexport interface SqlSessionRow {\n id: string;\n /** Migrate existing rows by setting family_id = id before deploying this driver. */\n familyId: string;\n csrfToken: string;\n dataJson: string;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SqlSessionDriver {\n read(id: string): Promise<SqlSessionRow | undefined>;\n /** Insert or update one row, keyed by its opaque id; enforce one active id per family. */\n write(row: SqlSessionRow): Promise<void>;\n remove(id: string): Promise<void>;\n /** Atomically replace an unexpired row of the same family in one transaction. */\n replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;\n /** Delete the active row for this family in one atomic operation, including stale-handle logout. */\n revokeFamily(familyId: string): Promise<void>;\n close?(): Promise<void>;\n}\n\nexport interface SqlSessionStoreOptions {\n /** Close the SQL driver when this store owns its connection. */\n closeDriver?: boolean;\n}\n\n/** Generic SQL store. The application supplies SQL statements through its driver. */\nexport class SqlSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly driver: SqlSessionDriver, private readonly options: SqlSessionStoreOptions = {}) {\n if (typeof driver.replace !== \"function\" || typeof driver.revokeFamily !== \"function\") throw new TypeError(\"SQL session driver requires atomic replace and family revocation\");\n }\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const row = await this.driver.read(id);\n if (!row) return undefined;\n if (row.id !== id || typeof row.dataJson !== \"string\" || typeof row.familyId !== \"string\" || !row.familyId) throw new Error(\"Session SQL schema requires a nonempty family_id column\");\n const record = decodeRecord<T>(serializeJson({ id: row.id, familyId: row.familyId, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson) as unknown, createdAt: row.createdAt, expiresAt: row.expiresAt }), id);\n if (record.expiresAt > Date.now()) return record;\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n await this.driver.write({ id: record.id, familyId: record.familyId ?? record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });\n }\n async delete(id: string): Promise<void> { await this.driver.remove(id); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n if (!record.familyId) throw new Error(\"Rotated session requires a family id\");\n return this.driver.replace(previousId, { id: record.id, familyId: record.familyId, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());\n }\n async revokeFamily(familyId: string): Promise<void> { await this.driver.revokeFamily(familyId); }\n async close(): Promise<void> { if (this.options.closeDriver) await this.driver.close?.(); }\n}\n"]}
@@ -1,5 +1,7 @@
1
1
  export interface SessionRecord<T = unknown> {
2
2
  id: string;
3
+ /** Stable opaque lineage for atomic revocation after one or more rotations. */
4
+ familyId?: string;
3
5
  csrfToken: string;
4
6
  data: T;
5
7
  createdAt: number;
@@ -13,16 +15,20 @@ export interface SessionStore<T = unknown> {
13
15
  delete(id: string): Promise<void>;
14
16
  /** Atomically replace an existing id; false means it no longer exists. */
15
17
  replace?(previousId: string, record: SessionRecord<T>): Promise<boolean>;
18
+ /** Atomically revoke the currently active id in this family, even from a stale handle. */
19
+ revokeFamily?(familyId: string): Promise<void>;
16
20
  close?(): Promise<void>;
17
21
  }
18
22
  export declare class MemorySessionStore<T = unknown> implements SessionStore<T> {
19
23
  readonly developmentOnly = true;
20
24
  readonly persistent = false;
21
25
  private readonly records;
26
+ private readonly families;
22
27
  get(id: string): Promise<SessionRecord<T> | undefined>;
23
28
  set(record: SessionRecord<T>): Promise<void>;
24
29
  delete(id: string): Promise<void>;
25
30
  replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
31
+ revokeFamily(familyId: string): Promise<void>;
26
32
  close(): Promise<void>;
27
33
  }
28
34
  export interface SessionOptions<T> {
@@ -3,27 +3,55 @@ export class MemorySessionStore {
3
3
  developmentOnly = true;
4
4
  persistent = false;
5
5
  records = new Map();
6
+ families = new Map();
6
7
  async get(id) {
7
8
  const record = this.records.get(id);
8
9
  if (record && record.expiresAt <= Date.now()) {
9
- this.records.delete(id);
10
+ await this.delete(id);
10
11
  return undefined;
11
12
  }
13
+ if (record && this.families.get(record.familyId ?? record.id) !== id)
14
+ return undefined;
12
15
  return record;
13
16
  }
14
- async set(record) { this.records.set(record.id, record); }
15
- async delete(id) { this.records.delete(id); }
17
+ async set(record) {
18
+ const familyId = record.familyId ?? record.id;
19
+ const existing = this.records.get(record.id);
20
+ if (existing && (existing.familyId ?? existing.id) !== familyId)
21
+ throw new Error("Session id cannot change family");
22
+ const active = this.families.get(familyId);
23
+ if (active && active !== record.id)
24
+ throw new Error("Session family already has an active id");
25
+ this.records.set(record.id, { ...record, familyId });
26
+ this.families.set(familyId, record.id);
27
+ }
28
+ async delete(id) {
29
+ const record = this.records.get(id);
30
+ this.records.delete(id);
31
+ if (record && this.families.get(record.familyId ?? record.id) === id)
32
+ this.families.delete(record.familyId ?? record.id);
33
+ }
16
34
  async replace(previousId, record) {
17
35
  const previous = this.records.get(previousId);
18
36
  if (!previous || previous.expiresAt <= Date.now()) {
19
- this.records.delete(previousId);
37
+ await this.delete(previousId);
20
38
  return false;
21
39
  }
40
+ const familyId = previous.familyId ?? previous.id;
41
+ if (record.familyId !== familyId || this.families.get(familyId) !== previousId)
42
+ return false;
22
43
  this.records.set(record.id, record);
44
+ this.families.set(familyId, record.id);
23
45
  this.records.delete(previousId);
24
46
  return true;
25
47
  }
26
- async close() { this.records.clear(); }
48
+ async revokeFamily(familyId) {
49
+ const active = this.families.get(familyId);
50
+ if (active)
51
+ this.records.delete(active);
52
+ this.families.delete(familyId);
53
+ }
54
+ async close() { this.records.clear(); this.families.clear(); }
27
55
  }
28
56
  function token() { return randomBytes(32).toString("base64url"); }
29
57
  function cookieValue(header, name) {
@@ -48,7 +76,7 @@ export class SessionHandle {
48
76
  async create(data) {
49
77
  const previous = this.current;
50
78
  const now = Date.now();
51
- const record = { id: token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };
79
+ const record = { id: token(), familyId: previous?.familyId ?? previous?.id ?? token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };
52
80
  if (previous) {
53
81
  if (!this.manager.store.replace)
54
82
  throw new Error("Session storage does not support atomic rotation");
@@ -67,8 +95,11 @@ export class SessionHandle {
67
95
  return this.create(this.current.data);
68
96
  }
69
97
  async revoke() {
70
- if (this.current)
71
- await this.manager.store.delete(this.current.id);
98
+ if (this.current) {
99
+ if (!this.manager.store.revokeFamily)
100
+ throw new Error("Session storage does not support atomic family revocation");
101
+ await this.manager.store.revokeFamily(this.current.familyId ?? this.current.id);
102
+ }
72
103
  this.current = undefined;
73
104
  this.pendingCookie = this.manager.serializeCookie("", 0);
74
105
  }
@@ -81,8 +112,8 @@ export class SessionManager {
81
112
  secure;
82
113
  sameSite;
83
114
  constructor(options) {
84
- if (options.production && (options.store.persistent !== true || !options.store.replace))
85
- throw new Error("Persistent session storage with atomic rotation is required in production");
115
+ if (options.production && (options.store.persistent !== true || !options.store.replace || !options.store.revokeFamily))
116
+ throw new Error("Persistent session storage with atomic rotation and family revocation is required in production");
86
117
  this.store = options.store;
87
118
  this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;
88
119
  if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0)
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/framework-server/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\n\nexport interface SessionRecord<T = unknown> {\n id: string;\n csrfToken: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SessionStore<T = unknown> {\n /** Explicitly acknowledge that records survive application process restarts. */\n readonly persistent?: boolean;\n get(id: string): Promise<SessionRecord<T> | undefined>;\n set(record: SessionRecord<T>): Promise<void>;\n delete(id: string): Promise<void>;\n /** Atomically replace an existing id; false means it no longer exists. */\n replace?(previousId: string, record: SessionRecord<T>): Promise<boolean>;\n close?(): Promise<void>;\n}\n\nexport class MemorySessionStore<T = unknown> implements SessionStore<T> {\n readonly developmentOnly = true;\n readonly persistent = false;\n private readonly records = new Map<string, SessionRecord<T>>();\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const record = this.records.get(id);\n if (record && record.expiresAt <= Date.now()) {\n this.records.delete(id);\n return undefined;\n }\n return record;\n }\n async set(record: SessionRecord<T>): Promise<void> { this.records.set(record.id, record); }\n async delete(id: string): Promise<void> { this.records.delete(id); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const previous = this.records.get(previousId);\n if (!previous || previous.expiresAt <= Date.now()) {\n this.records.delete(previousId);\n return false;\n }\n this.records.set(record.id, record);\n this.records.delete(previousId);\n return true;\n }\n async close(): Promise<void> { this.records.clear(); }\n}\n\nexport interface SessionOptions<T> {\n store: SessionStore<T>;\n production?: boolean;\n ttlMs?: number;\n secure?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n cookieName?: string;\n allowedOrigins?: readonly string[];\n}\n\nfunction token(): string { return randomBytes(32).toString(\"base64url\"); }\n\nfunction cookieValue(header: string | null, name: string): string | undefined {\n for (const cookie of (header ?? \"\").split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0 || cookie.slice(0, separator).trim() !== name) continue;\n const value = cookie.slice(separator + 1).trim();\n return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;\n }\n return undefined;\n}\n\nexport class SessionHandle<T = unknown> {\n private pendingCookie?: string;\n constructor(private readonly manager: SessionManager<T>, public current?: SessionRecord<T>) {}\n get cookieHeader(): string | undefined { return this.pendingCookie; }\n async create(data: T): Promise<SessionRecord<T>> {\n const previous = this.current;\n const now = Date.now();\n const record = { id: token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };\n if (previous) {\n if (!this.manager.store.replace) throw new Error(\"Session storage does not support atomic rotation\");\n if (!(await this.manager.store.replace(previous.id, record))) throw new Error(\"Session has expired or been revoked\");\n } else await this.manager.store.set(record);\n this.current = record;\n this.pendingCookie = this.manager.serializeCookie(record.id, this.manager.ttlMs);\n return record;\n }\n async rotate(): Promise<SessionRecord<T>> {\n if (!this.current) throw new Error(\"No active session to rotate\");\n return this.create(this.current.data);\n }\n async revoke(): Promise<void> {\n if (this.current) await this.manager.store.delete(this.current.id);\n this.current = undefined;\n this.pendingCookie = this.manager.serializeCookie(\"\", 0);\n }\n}\n\nexport class SessionManager<T = unknown> {\n readonly store: SessionStore<T>;\n readonly ttlMs: number;\n readonly cookieName: string;\n readonly allowedOrigins: readonly string[];\n private readonly secure: boolean;\n private readonly sameSite: \"Lax\" | \"Strict\" | \"None\";\n constructor(options: SessionOptions<T>) {\n if (options.production && (options.store.persistent !== true || !options.store.replace)) throw new Error(\"Persistent session storage with atomic rotation is required in production\");\n this.store = options.store;\n this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;\n if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) throw new RangeError(\"Invalid session TTL\");\n this.secure = options.secure ?? true;\n this.sameSite = options.sameSite ?? \"Lax\";\n if (this.sameSite === \"None\" && !this.secure) throw new Error(\"SameSite=None requires Secure\");\n this.cookieName = options.cookieName ?? (this.secure ? \"__Host-meeko.sid\" : \"meeko.sid\");\n if (!/^[A-Za-z0-9_.-]+$/.test(this.cookieName)) throw new TypeError(\"Invalid cookie name\");\n if (this.cookieName.startsWith(\"__Host-\") && !this.secure) throw new Error(\"__Host- cookies require Secure\");\n this.allowedOrigins = options.allowedOrigins ?? [];\n }\n serializeCookie(value: string, ttlMs: number): string {\n const parts = [`${this.cookieName}=${value}`, \"Path=/\", \"HttpOnly\", `SameSite=${this.sameSite}`, `Max-Age=${Math.floor(ttlMs / 1000)}`];\n if (this.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n }\n async load(request: Request): Promise<SessionHandle<T>> {\n const id = cookieValue(request.headers.get(\"cookie\"), this.cookieName);\n const record = id ? await this.store.get(id) : undefined;\n if (record && record.expiresAt <= Date.now()) {\n await this.store.delete(record.id);\n return new SessionHandle(this);\n }\n return new SessionHandle(this, record);\n }\n async close(): Promise<void> { await this.store.close?.(); }\n /** Apply both Origin and token checks to authenticated cookie mutations. */\n assertMutationAllowed(request: Request, session: SessionHandle<T>, mutating = ![\"GET\", \"HEAD\", \"OPTIONS\"].includes(request.method)): void {\n if (!session.current || !mutating) return;\n const origin = request.headers.get(\"origin\");\n const expected = new URL(request.url).origin;\n if (!origin || (origin !== expected && !this.allowedOrigins.includes(origin))) throw new SessionSecurityError(\"INVALID_ORIGIN\");\n if (request.headers.get(\"x-meeko-csrf\") !== session.current.csrfToken) throw new SessionSecurityError(\"INVALID_CSRF\");\n }\n}\n\nexport class SessionSecurityError extends Error {\n constructor(public readonly code: \"INVALID_ORIGIN\" | \"INVALID_CSRF\") { super(code); }\n}\n"]}
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/framework-server/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\n\nexport interface SessionRecord<T = unknown> {\n id: string;\n /** Stable opaque lineage for atomic revocation after one or more rotations. */\n familyId?: string;\n csrfToken: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SessionStore<T = unknown> {\n /** Explicitly acknowledge that records survive application process restarts. */\n readonly persistent?: boolean;\n get(id: string): Promise<SessionRecord<T> | undefined>;\n set(record: SessionRecord<T>): Promise<void>;\n delete(id: string): Promise<void>;\n /** Atomically replace an existing id; false means it no longer exists. */\n replace?(previousId: string, record: SessionRecord<T>): Promise<boolean>;\n /** Atomically revoke the currently active id in this family, even from a stale handle. */\n revokeFamily?(familyId: string): Promise<void>;\n close?(): Promise<void>;\n}\n\nexport class MemorySessionStore<T = unknown> implements SessionStore<T> {\n readonly developmentOnly = true;\n readonly persistent = false;\n private readonly records = new Map<string, SessionRecord<T>>();\n private readonly families = new Map<string, string>();\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const record = this.records.get(id);\n if (record && record.expiresAt <= Date.now()) {\n await this.delete(id);\n return undefined;\n }\n if (record && this.families.get(record.familyId ?? record.id) !== id) return undefined;\n return record;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n const familyId = record.familyId ?? record.id;\n const existing = this.records.get(record.id);\n if (existing && (existing.familyId ?? existing.id) !== familyId) throw new Error(\"Session id cannot change family\");\n const active = this.families.get(familyId);\n if (active && active !== record.id) throw new Error(\"Session family already has an active id\");\n this.records.set(record.id, { ...record, familyId });\n this.families.set(familyId, record.id);\n }\n async delete(id: string): Promise<void> {\n const record = this.records.get(id);\n this.records.delete(id);\n if (record && this.families.get(record.familyId ?? record.id) === id) this.families.delete(record.familyId ?? record.id);\n }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const previous = this.records.get(previousId);\n if (!previous || previous.expiresAt <= Date.now()) {\n await this.delete(previousId);\n return false;\n }\n const familyId = previous.familyId ?? previous.id;\n if (record.familyId !== familyId || this.families.get(familyId) !== previousId) return false;\n this.records.set(record.id, record);\n this.families.set(familyId, record.id);\n this.records.delete(previousId);\n return true;\n }\n async revokeFamily(familyId: string): Promise<void> {\n const active = this.families.get(familyId);\n if (active) this.records.delete(active);\n this.families.delete(familyId);\n }\n async close(): Promise<void> { this.records.clear(); this.families.clear(); }\n}\n\nexport interface SessionOptions<T> {\n store: SessionStore<T>;\n production?: boolean;\n ttlMs?: number;\n secure?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n cookieName?: string;\n allowedOrigins?: readonly string[];\n}\n\nfunction token(): string { return randomBytes(32).toString(\"base64url\"); }\n\nfunction cookieValue(header: string | null, name: string): string | undefined {\n for (const cookie of (header ?? \"\").split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0 || cookie.slice(0, separator).trim() !== name) continue;\n const value = cookie.slice(separator + 1).trim();\n return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;\n }\n return undefined;\n}\n\nexport class SessionHandle<T = unknown> {\n private pendingCookie?: string;\n constructor(private readonly manager: SessionManager<T>, public current?: SessionRecord<T>) {}\n get cookieHeader(): string | undefined { return this.pendingCookie; }\n async create(data: T): Promise<SessionRecord<T>> {\n const previous = this.current;\n const now = Date.now();\n const record = { id: token(), familyId: previous?.familyId ?? previous?.id ?? token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };\n if (previous) {\n if (!this.manager.store.replace) throw new Error(\"Session storage does not support atomic rotation\");\n if (!(await this.manager.store.replace(previous.id, record))) throw new Error(\"Session has expired or been revoked\");\n } else await this.manager.store.set(record);\n this.current = record;\n this.pendingCookie = this.manager.serializeCookie(record.id, this.manager.ttlMs);\n return record;\n }\n async rotate(): Promise<SessionRecord<T>> {\n if (!this.current) throw new Error(\"No active session to rotate\");\n return this.create(this.current.data);\n }\n async revoke(): Promise<void> {\n if (this.current) {\n if (!this.manager.store.revokeFamily) throw new Error(\"Session storage does not support atomic family revocation\");\n await this.manager.store.revokeFamily(this.current.familyId ?? this.current.id);\n }\n this.current = undefined;\n this.pendingCookie = this.manager.serializeCookie(\"\", 0);\n }\n}\n\nexport class SessionManager<T = unknown> {\n readonly store: SessionStore<T>;\n readonly ttlMs: number;\n readonly cookieName: string;\n readonly allowedOrigins: readonly string[];\n private readonly secure: boolean;\n private readonly sameSite: \"Lax\" | \"Strict\" | \"None\";\n constructor(options: SessionOptions<T>) {\n if (options.production && (options.store.persistent !== true || !options.store.replace || !options.store.revokeFamily)) throw new Error(\"Persistent session storage with atomic rotation and family revocation is required in production\");\n this.store = options.store;\n this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;\n if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) throw new RangeError(\"Invalid session TTL\");\n this.secure = options.secure ?? true;\n this.sameSite = options.sameSite ?? \"Lax\";\n if (this.sameSite === \"None\" && !this.secure) throw new Error(\"SameSite=None requires Secure\");\n this.cookieName = options.cookieName ?? (this.secure ? \"__Host-meeko.sid\" : \"meeko.sid\");\n if (!/^[A-Za-z0-9_.-]+$/.test(this.cookieName)) throw new TypeError(\"Invalid cookie name\");\n if (this.cookieName.startsWith(\"__Host-\") && !this.secure) throw new Error(\"__Host- cookies require Secure\");\n this.allowedOrigins = options.allowedOrigins ?? [];\n }\n serializeCookie(value: string, ttlMs: number): string {\n const parts = [`${this.cookieName}=${value}`, \"Path=/\", \"HttpOnly\", `SameSite=${this.sameSite}`, `Max-Age=${Math.floor(ttlMs / 1000)}`];\n if (this.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n }\n async load(request: Request): Promise<SessionHandle<T>> {\n const id = cookieValue(request.headers.get(\"cookie\"), this.cookieName);\n const record = id ? await this.store.get(id) : undefined;\n if (record && record.expiresAt <= Date.now()) {\n await this.store.delete(record.id);\n return new SessionHandle(this);\n }\n return new SessionHandle(this, record);\n }\n async close(): Promise<void> { await this.store.close?.(); }\n /** Apply both Origin and token checks to authenticated cookie mutations. */\n assertMutationAllowed(request: Request, session: SessionHandle<T>, mutating = ![\"GET\", \"HEAD\", \"OPTIONS\"].includes(request.method)): void {\n if (!session.current || !mutating) return;\n const origin = request.headers.get(\"origin\");\n const expected = new URL(request.url).origin;\n if (!origin || (origin !== expected && !this.allowedOrigins.includes(origin))) throw new SessionSecurityError(\"INVALID_ORIGIN\");\n if (request.headers.get(\"x-meeko-csrf\") !== session.current.csrfToken) throw new SessionSecurityError(\"INVALID_CSRF\");\n }\n}\n\nexport class SessionSecurityError extends Error {\n constructor(public readonly code: \"INVALID_ORIGIN\" | \"INVALID_CSRF\") { super(code); }\n}\n"]}