@meekojs/meeko 0.0.3 → 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 (188) hide show
  1. package/README.md +35 -16
  2. package/dist/autoimports-ts.d.ts +26 -0
  3. package/dist/autoimports-ts.js +183 -0
  4. package/dist/autoimports-ts.js.map +1 -0
  5. package/dist/autoimports.d.ts +21 -0
  6. package/dist/autoimports.js +301 -0
  7. package/dist/autoimports.js.map +1 -0
  8. package/dist/check.js +74 -33
  9. package/dist/check.js.map +1 -0
  10. package/dist/cli.js +67 -5
  11. package/dist/cli.js.map +1 -0
  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/import-graph.d.ts +8 -0
  16. package/dist/compiler/import-graph.js +33 -0
  17. package/dist/compiler/import-graph.js.map +1 -0
  18. package/dist/compiler/mapping.js +1 -0
  19. package/dist/compiler/mapping.js.map +1 -0
  20. package/dist/compiler/project-root.d.ts +2 -0
  21. package/dist/compiler/project-root.js +18 -0
  22. package/dist/compiler/project-root.js.map +1 -0
  23. package/dist/compiler/props.d.ts +12 -0
  24. package/dist/compiler/props.js +32 -0
  25. package/dist/compiler/props.js.map +1 -0
  26. package/dist/compiler/sfc.js +1 -0
  27. package/dist/compiler/sfc.js.map +1 -0
  28. package/dist/compiler/sourcemap.d.ts +3 -0
  29. package/dist/compiler/sourcemap.js +75 -0
  30. package/dist/compiler/sourcemap.js.map +1 -0
  31. package/dist/compiler/template.js +1 -0
  32. package/dist/compiler/template.js.map +1 -0
  33. package/dist/compiler.js +66 -17
  34. package/dist/compiler.js.map +1 -0
  35. package/dist/component-errors.d.ts +18 -0
  36. package/dist/component-errors.js +65 -0
  37. package/dist/component-errors.js.map +1 -0
  38. package/dist/devtools-ui/index.js +1 -0
  39. package/dist/devtools-ui/index.js.map +1 -0
  40. package/dist/devtools.js +3 -4
  41. package/dist/devtools.js.map +1 -0
  42. package/dist/dom.d.ts +18 -5
  43. package/dist/dom.js +295 -90
  44. package/dist/dom.js.map +1 -0
  45. package/dist/error-observer.d.ts +6 -0
  46. package/dist/error-observer.js +41 -0
  47. package/dist/error-observer.js.map +1 -0
  48. package/dist/framework/check-app.d.ts +5 -0
  49. package/dist/framework/check-app.js +214 -0
  50. package/dist/framework/check-app.js.map +1 -0
  51. package/dist/framework/check-project.d.ts +4 -0
  52. package/dist/framework/check-project.js +42 -0
  53. package/dist/framework/check-project.js.map +1 -0
  54. package/dist/framework/client.d.ts +48 -0
  55. package/dist/framework/client.js +249 -0
  56. package/dist/framework/client.js.map +1 -0
  57. package/dist/framework/clients.d.ts +82 -0
  58. package/dist/framework/clients.js +190 -0
  59. package/dist/framework/clients.js.map +1 -0
  60. package/dist/framework/config-type.d.ts +10 -0
  61. package/dist/framework/config-type.js +4 -0
  62. package/dist/framework/config-type.js.map +1 -0
  63. package/dist/framework/config.d.ts +15 -0
  64. package/dist/framework/config.js +138 -0
  65. package/dist/framework/config.js.map +1 -0
  66. package/dist/framework/contracts-generate.d.ts +8 -0
  67. package/dist/framework/contracts-generate.js +290 -0
  68. package/dist/framework/contracts-generate.js.map +1 -0
  69. package/dist/framework/generate.d.ts +7 -0
  70. package/dist/framework/generate.js +67 -0
  71. package/dist/framework/generate.js.map +1 -0
  72. package/dist/framework/init.d.ts +2 -0
  73. package/dist/framework/init.js +183 -0
  74. package/dist/framework/init.js.map +1 -0
  75. package/dist/framework/middleware.d.ts +14 -0
  76. package/dist/framework/middleware.js +16 -0
  77. package/dist/framework/middleware.js.map +1 -0
  78. package/dist/framework/modules.d.ts +22 -0
  79. package/dist/framework/modules.js +29 -0
  80. package/dist/framework/modules.js.map +1 -0
  81. package/dist/framework/page-contracts-check.d.ts +8 -0
  82. package/dist/framework/page-contracts-check.js +110 -0
  83. package/dist/framework/page-contracts-check.js.map +1 -0
  84. package/dist/framework/page-contracts-generate.d.ts +2 -0
  85. package/dist/framework/page-contracts-generate.js +94 -0
  86. package/dist/framework/page-contracts-generate.js.map +1 -0
  87. package/dist/framework/page.d.ts +64 -0
  88. package/dist/framework/page.js +44 -0
  89. package/dist/framework/page.js.map +1 -0
  90. package/dist/framework/prerender.d.ts +14 -0
  91. package/dist/framework/prerender.js +110 -0
  92. package/dist/framework/prerender.js.map +1 -0
  93. package/dist/framework/project-templates.d.ts +3 -0
  94. package/dist/framework/project-templates.js +119 -0
  95. package/dist/framework/project-templates.js.map +1 -0
  96. package/dist/framework/project.d.ts +9 -0
  97. package/dist/framework/project.js +272 -0
  98. package/dist/framework/project.js.map +1 -0
  99. package/dist/framework/router.d.ts +41 -0
  100. package/dist/framework/router.js +259 -0
  101. package/dist/framework/router.js.map +1 -0
  102. package/dist/framework/server.d.ts +65 -0
  103. package/dist/framework/server.js +239 -0
  104. package/dist/framework/server.js.map +1 -0
  105. package/dist/framework/socket-contracts-generate.d.ts +5 -0
  106. package/dist/framework/socket-contracts-generate.js +162 -0
  107. package/dist/framework/socket-contracts-generate.js.map +1 -0
  108. package/dist/framework/socket.d.ts +72 -0
  109. package/dist/framework/socket.js +252 -0
  110. package/dist/framework/socket.js.map +1 -0
  111. package/dist/framework/standard-schema-types.d.ts +7 -0
  112. package/dist/framework/standard-schema-types.js +69 -0
  113. package/dist/framework/standard-schema-types.js.map +1 -0
  114. package/dist/framework/state.d.ts +22 -0
  115. package/dist/framework/state.js +136 -0
  116. package/dist/framework/state.js.map +1 -0
  117. package/dist/framework/styles.d.ts +2 -0
  118. package/dist/framework/styles.js +6 -0
  119. package/dist/framework/styles.js.map +1 -0
  120. package/dist/framework/type-declarations.d.ts +5 -0
  121. package/dist/framework/type-declarations.js +84 -0
  122. package/dist/framework/type-declarations.js.map +1 -0
  123. package/dist/framework-server/auto-clients.d.ts +3 -0
  124. package/dist/framework-server/auto-clients.js +27 -0
  125. package/dist/framework-server/auto-clients.js.map +1 -0
  126. package/dist/framework-server/context.d.ts +21 -0
  127. package/dist/framework-server/context.js +22 -0
  128. package/dist/framework-server/context.js.map +1 -0
  129. package/dist/framework-server/contracts.d.ts +73 -0
  130. package/dist/framework-server/contracts.js +184 -0
  131. package/dist/framework-server/contracts.js.map +1 -0
  132. package/dist/framework-server/discovery.d.ts +43 -0
  133. package/dist/framework-server/discovery.js +257 -0
  134. package/dist/framework-server/discovery.js.map +1 -0
  135. package/dist/framework-server/local.d.ts +21 -0
  136. package/dist/framework-server/local.js +100 -0
  137. package/dist/framework-server/local.js.map +1 -0
  138. package/dist/framework-server/middleware.d.ts +4 -0
  139. package/dist/framework-server/middleware.js +56 -0
  140. package/dist/framework-server/middleware.js.map +1 -0
  141. package/dist/framework-server/node-upgrade.d.ts +23 -0
  142. package/dist/framework-server/node-upgrade.js +96 -0
  143. package/dist/framework-server/node-upgrade.js.map +1 -0
  144. package/dist/framework-server/node-websocket.d.ts +16 -0
  145. package/dist/framework-server/node-websocket.js +127 -0
  146. package/dist/framework-server/node-websocket.js.map +1 -0
  147. package/dist/framework-server/openapi.d.ts +17 -0
  148. package/dist/framework-server/openapi.js +118 -0
  149. package/dist/framework-server/openapi.js.map +1 -0
  150. package/dist/framework-server/session-stores.d.ts +73 -0
  151. package/dist/framework-server/session-stores.js +122 -0
  152. package/dist/framework-server/session-stores.js.map +1 -0
  153. package/dist/framework-server/sessions.d.ts +70 -0
  154. package/dist/framework-server/sessions.js +167 -0
  155. package/dist/framework-server/sessions.js.map +1 -0
  156. package/dist/framework-server/socket.d.ts +83 -0
  157. package/dist/framework-server/socket.js +196 -0
  158. package/dist/framework-server/socket.js.map +1 -0
  159. package/dist/framework-server.d.ts +23 -0
  160. package/dist/framework-server.js +13 -0
  161. package/dist/framework-server.js.map +1 -0
  162. package/dist/framework.d.ts +31 -0
  163. package/dist/framework.js +14 -0
  164. package/dist/framework.js.map +1 -0
  165. package/dist/hmr.js +75 -24
  166. package/dist/hmr.js.map +1 -0
  167. package/dist/index.d.ts +5 -3
  168. package/dist/index.js +3 -1
  169. package/dist/index.js.map +1 -0
  170. package/dist/init.js +31 -3
  171. package/dist/init.js.map +1 -0
  172. package/dist/internal.d.ts +2 -2
  173. package/dist/internal.js +3 -2
  174. package/dist/internal.js.map +1 -0
  175. package/dist/node.d.ts +16 -0
  176. package/dist/node.js +122 -0
  177. package/dist/node.js.map +1 -0
  178. package/dist/reactivity.d.ts +22 -1
  179. package/dist/reactivity.js +132 -18
  180. package/dist/reactivity.js.map +1 -0
  181. package/dist/server.js +1 -0
  182. package/dist/server.js.map +1 -0
  183. package/dist/types.d.ts +2 -0
  184. package/dist/types.js +26 -9
  185. package/dist/types.js.map +1 -0
  186. package/dist/vite.js +112 -7
  187. package/dist/vite.js.map +1 -0
  188. package/package.json +15 -4
@@ -0,0 +1,122 @@
1
+ import { serializeJson } from "../server.js";
2
+ function decodeRecord(value, id) {
3
+ let record;
4
+ try {
5
+ record = JSON.parse(value);
6
+ }
7
+ catch {
8
+ throw new Error("Corrupt session record");
9
+ }
10
+ if (!record || typeof record !== "object")
11
+ throw new Error("Corrupt session record");
12
+ const candidate = record;
13
+ if (candidate.id !== id || typeof candidate.csrfToken !== "string" || !Object.hasOwn(candidate, "data") ||
14
+ typeof candidate.createdAt !== "number" || typeof candidate.expiresAt !== "number" ||
15
+ !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt) ||
16
+ (candidate.familyId !== undefined && (typeof candidate.familyId !== "string" || candidate.familyId.length === 0))) {
17
+ throw new Error("Corrupt session record");
18
+ }
19
+ return candidate;
20
+ }
21
+ /** Persistent store with Redis-managed expiration and explicit connection ownership. */
22
+ export class RedisSessionStore {
23
+ client;
24
+ options;
25
+ persistent = true;
26
+ constructor(client, options) {
27
+ this.client = client;
28
+ this.options = options;
29
+ if (!options.prefix || !options.prefix.endsWith(":") || /\s/.test(options.prefix))
30
+ throw new TypeError("Session Redis prefix must be nonempty, contain no whitespace, and end with ':'");
31
+ }
32
+ key(id) { return `${this.options.prefix}${id}`; }
33
+ familyKey(id) { return `${this.options.prefix}family:${id}`; }
34
+ async get(id) {
35
+ const value = await this.client.get(this.key(id));
36
+ if (value === null)
37
+ return undefined;
38
+ const record = decodeRecord(value, id);
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
+ }
46
+ await this.delete(id);
47
+ return undefined;
48
+ }
49
+ async set(record) {
50
+ const ttlMs = record.expiresAt - Date.now();
51
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
52
+ throw new RangeError("Session must have a future expiration");
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] });
64
+ }
65
+ async replace(previousId, record) {
66
+ const ttlMs = record.expiresAt - Date.now();
67
+ if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
68
+ throw new RangeError("Session must have a future expiration");
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] });
72
+ if (result !== 0 && result !== 1)
73
+ throw new Error("Unexpected Redis rotation result");
74
+ return result === 1;
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
+ }
79
+ async close() { if (this.options.closeClient)
80
+ await this.client.quit?.(); }
81
+ }
82
+ /** Generic SQL store. The application supplies SQL statements through its driver. */
83
+ export class SqlSessionStore {
84
+ driver;
85
+ options;
86
+ persistent = true;
87
+ constructor(driver, options = {}) {
88
+ this.driver = driver;
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");
92
+ }
93
+ async get(id) {
94
+ const row = await this.driver.read(id);
95
+ if (!row)
96
+ return undefined;
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);
100
+ if (record.expiresAt > Date.now())
101
+ return record;
102
+ await this.delete(id);
103
+ return undefined;
104
+ }
105
+ async set(record) {
106
+ if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
107
+ throw new RangeError("Session must have a future expiration");
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 });
109
+ }
110
+ async delete(id) { await this.driver.remove(id); }
111
+ async replace(previousId, record) {
112
+ if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
113
+ throw new RangeError("Session must have a future expiration");
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());
117
+ }
118
+ async revokeFamily(familyId) { await this.driver.revokeFamily(familyId); }
119
+ async close() { if (this.options.closeDriver)
120
+ await this.driver.close?.(); }
121
+ }
122
+ //# sourceMappingURL=session-stores.js.map
@@ -0,0 +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 (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"]}
@@ -0,0 +1,70 @@
1
+ export interface SessionRecord<T = unknown> {
2
+ id: string;
3
+ /** Stable opaque lineage for atomic revocation after one or more rotations. */
4
+ familyId?: string;
5
+ csrfToken: string;
6
+ data: T;
7
+ createdAt: number;
8
+ expiresAt: number;
9
+ }
10
+ export interface SessionStore<T = unknown> {
11
+ /** Explicitly acknowledge that records survive application process restarts. */
12
+ readonly persistent?: boolean;
13
+ get(id: string): Promise<SessionRecord<T> | undefined>;
14
+ set(record: SessionRecord<T>): Promise<void>;
15
+ delete(id: string): Promise<void>;
16
+ /** Atomically replace an existing id; false means it no longer exists. */
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>;
20
+ close?(): Promise<void>;
21
+ }
22
+ export declare class MemorySessionStore<T = unknown> implements SessionStore<T> {
23
+ readonly developmentOnly = true;
24
+ readonly persistent = false;
25
+ private readonly records;
26
+ private readonly families;
27
+ get(id: string): Promise<SessionRecord<T> | undefined>;
28
+ set(record: SessionRecord<T>): Promise<void>;
29
+ delete(id: string): Promise<void>;
30
+ replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
31
+ revokeFamily(familyId: string): Promise<void>;
32
+ close(): Promise<void>;
33
+ }
34
+ export interface SessionOptions<T> {
35
+ store: SessionStore<T>;
36
+ production?: boolean;
37
+ ttlMs?: number;
38
+ secure?: boolean;
39
+ sameSite?: "Lax" | "Strict" | "None";
40
+ cookieName?: string;
41
+ allowedOrigins?: readonly string[];
42
+ }
43
+ export declare class SessionHandle<T = unknown> {
44
+ private readonly manager;
45
+ current?: SessionRecord<T> | undefined;
46
+ private pendingCookie?;
47
+ constructor(manager: SessionManager<T>, current?: SessionRecord<T> | undefined);
48
+ get cookieHeader(): string | undefined;
49
+ create(data: T): Promise<SessionRecord<T>>;
50
+ rotate(): Promise<SessionRecord<T>>;
51
+ revoke(): Promise<void>;
52
+ }
53
+ export declare class SessionManager<T = unknown> {
54
+ readonly store: SessionStore<T>;
55
+ readonly ttlMs: number;
56
+ readonly cookieName: string;
57
+ readonly allowedOrigins: readonly string[];
58
+ private readonly secure;
59
+ private readonly sameSite;
60
+ constructor(options: SessionOptions<T>);
61
+ serializeCookie(value: string, ttlMs: number): string;
62
+ load(request: Request): Promise<SessionHandle<T>>;
63
+ close(): Promise<void>;
64
+ /** Apply both Origin and token checks to authenticated cookie mutations. */
65
+ assertMutationAllowed(request: Request, session: SessionHandle<T>, mutating?: boolean): void;
66
+ }
67
+ export declare class SessionSecurityError extends Error {
68
+ readonly code: "INVALID_ORIGIN" | "INVALID_CSRF";
69
+ constructor(code: "INVALID_ORIGIN" | "INVALID_CSRF");
70
+ }
@@ -0,0 +1,167 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export class MemorySessionStore {
3
+ developmentOnly = true;
4
+ persistent = false;
5
+ records = new Map();
6
+ families = new Map();
7
+ async get(id) {
8
+ const record = this.records.get(id);
9
+ if (record && record.expiresAt <= Date.now()) {
10
+ await this.delete(id);
11
+ return undefined;
12
+ }
13
+ if (record && this.families.get(record.familyId ?? record.id) !== id)
14
+ return undefined;
15
+ return record;
16
+ }
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
+ }
34
+ async replace(previousId, record) {
35
+ const previous = this.records.get(previousId);
36
+ if (!previous || previous.expiresAt <= Date.now()) {
37
+ await this.delete(previousId);
38
+ return false;
39
+ }
40
+ const familyId = previous.familyId ?? previous.id;
41
+ if (record.familyId !== familyId || this.families.get(familyId) !== previousId)
42
+ return false;
43
+ this.records.set(record.id, record);
44
+ this.families.set(familyId, record.id);
45
+ this.records.delete(previousId);
46
+ return true;
47
+ }
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(); }
55
+ }
56
+ function token() { return randomBytes(32).toString("base64url"); }
57
+ function cookieValue(header, name) {
58
+ for (const cookie of (header ?? "").split(";")) {
59
+ const separator = cookie.indexOf("=");
60
+ if (separator < 0 || cookie.slice(0, separator).trim() !== name)
61
+ continue;
62
+ const value = cookie.slice(separator + 1).trim();
63
+ return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
64
+ }
65
+ return undefined;
66
+ }
67
+ export class SessionHandle {
68
+ manager;
69
+ current;
70
+ pendingCookie;
71
+ constructor(manager, current) {
72
+ this.manager = manager;
73
+ this.current = current;
74
+ }
75
+ get cookieHeader() { return this.pendingCookie; }
76
+ async create(data) {
77
+ const previous = this.current;
78
+ const now = Date.now();
79
+ const record = { id: token(), familyId: previous?.familyId ?? previous?.id ?? token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };
80
+ if (previous) {
81
+ if (!this.manager.store.replace)
82
+ throw new Error("Session storage does not support atomic rotation");
83
+ if (!(await this.manager.store.replace(previous.id, record)))
84
+ throw new Error("Session has expired or been revoked");
85
+ }
86
+ else
87
+ await this.manager.store.set(record);
88
+ this.current = record;
89
+ this.pendingCookie = this.manager.serializeCookie(record.id, this.manager.ttlMs);
90
+ return record;
91
+ }
92
+ async rotate() {
93
+ if (!this.current)
94
+ throw new Error("No active session to rotate");
95
+ return this.create(this.current.data);
96
+ }
97
+ async revoke() {
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
+ }
103
+ this.current = undefined;
104
+ this.pendingCookie = this.manager.serializeCookie("", 0);
105
+ }
106
+ }
107
+ export class SessionManager {
108
+ store;
109
+ ttlMs;
110
+ cookieName;
111
+ allowedOrigins;
112
+ secure;
113
+ sameSite;
114
+ constructor(options) {
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");
117
+ this.store = options.store;
118
+ this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;
119
+ if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0)
120
+ throw new RangeError("Invalid session TTL");
121
+ this.secure = options.secure ?? true;
122
+ this.sameSite = options.sameSite ?? "Lax";
123
+ if (this.sameSite === "None" && !this.secure)
124
+ throw new Error("SameSite=None requires Secure");
125
+ this.cookieName = options.cookieName ?? (this.secure ? "__Host-meeko.sid" : "meeko.sid");
126
+ if (!/^[A-Za-z0-9_.-]+$/.test(this.cookieName))
127
+ throw new TypeError("Invalid cookie name");
128
+ if (this.cookieName.startsWith("__Host-") && !this.secure)
129
+ throw new Error("__Host- cookies require Secure");
130
+ this.allowedOrigins = options.allowedOrigins ?? [];
131
+ }
132
+ serializeCookie(value, ttlMs) {
133
+ const parts = [`${this.cookieName}=${value}`, "Path=/", "HttpOnly", `SameSite=${this.sameSite}`, `Max-Age=${Math.floor(ttlMs / 1000)}`];
134
+ if (this.secure)
135
+ parts.push("Secure");
136
+ return parts.join("; ");
137
+ }
138
+ async load(request) {
139
+ const id = cookieValue(request.headers.get("cookie"), this.cookieName);
140
+ const record = id ? await this.store.get(id) : undefined;
141
+ if (record && record.expiresAt <= Date.now()) {
142
+ await this.store.delete(record.id);
143
+ return new SessionHandle(this);
144
+ }
145
+ return new SessionHandle(this, record);
146
+ }
147
+ async close() { await this.store.close?.(); }
148
+ /** Apply both Origin and token checks to authenticated cookie mutations. */
149
+ assertMutationAllowed(request, session, mutating = !["GET", "HEAD", "OPTIONS"].includes(request.method)) {
150
+ if (!session.current || !mutating)
151
+ return;
152
+ const origin = request.headers.get("origin");
153
+ const expected = new URL(request.url).origin;
154
+ if (!origin || (origin !== expected && !this.allowedOrigins.includes(origin)))
155
+ throw new SessionSecurityError("INVALID_ORIGIN");
156
+ if (request.headers.get("x-meeko-csrf") !== session.current.csrfToken)
157
+ throw new SessionSecurityError("INVALID_CSRF");
158
+ }
159
+ }
160
+ export class SessionSecurityError extends Error {
161
+ code;
162
+ constructor(code) {
163
+ super(code);
164
+ this.code = code;
165
+ }
166
+ }
167
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +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 /** 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"]}
@@ -0,0 +1,83 @@
1
+ import { type RequestContext } from "./context.js";
2
+ import { type Schema } from "./contracts.js";
3
+ import type { SessionManager } from "./sessions.js";
4
+ type Output<S> = S extends Schema<unknown, infer O> ? O : never;
5
+ type SchemaMap = Readonly<Record<string, Schema>>;
6
+ export interface SocketPeer {
7
+ send(message: string): void | Promise<void>;
8
+ close(code?: number, reason?: string): void;
9
+ readonly bufferedAmount?: number;
10
+ }
11
+ export interface SocketChannelDefinition<C extends SchemaMap, S extends SchemaMap, TServices = unknown> {
12
+ clientToServer: C;
13
+ serverToClient: S;
14
+ auth?: "required" | "optional";
15
+ authorize?: (args: {
16
+ context: RequestContext<TServices>;
17
+ roomId?: string;
18
+ }) => boolean | Promise<boolean>;
19
+ authorizeEvent?: (args: {
20
+ event: keyof C & string;
21
+ payload: unknown;
22
+ context: RequestContext<TServices>;
23
+ roomId?: string;
24
+ }) => boolean | Promise<boolean>;
25
+ authorizeSend?: (args: {
26
+ event: keyof S & string;
27
+ payload: unknown;
28
+ context: RequestContext<TServices>;
29
+ roomId?: string;
30
+ }) => boolean | Promise<boolean>;
31
+ handlers?: {
32
+ [K in keyof C]?: (args: {
33
+ payload: Output<C[K]>;
34
+ context: RequestContext<TServices>;
35
+ roomId?: string;
36
+ emit: <E extends keyof S & string>(event: E, payload: Output<S[E]>) => Promise<void>;
37
+ }) => void | Promise<void>;
38
+ };
39
+ }
40
+ export declare function defineSocketChannel<C extends SchemaMap, S extends SchemaMap, TServices = unknown>(definition: SocketChannelDefinition<C, S, TServices>): SocketChannelDefinition<C, S, TServices>;
41
+ export interface SocketPubSub {
42
+ publish(channel: string, room: string, message: {
43
+ event: string;
44
+ payload: unknown;
45
+ }): Promise<void>;
46
+ subscribe(channel: string, room: string, listener: (message: {
47
+ event: string;
48
+ payload: unknown;
49
+ }) => void | Promise<void>): () => void;
50
+ }
51
+ export declare class LocalSocketPubSub implements SocketPubSub {
52
+ private readonly listeners;
53
+ publish(channel: string, room: string, message: {
54
+ event: string;
55
+ payload: unknown;
56
+ }): Promise<void>;
57
+ subscribe(channel: string, room: string, listener: (message: {
58
+ event: string;
59
+ payload: unknown;
60
+ }) => void | Promise<void>): () => void;
61
+ }
62
+ export interface SocketConnection {
63
+ receive(message: string | Uint8Array): Promise<void>;
64
+ emit(event: string, payload: unknown): Promise<void>;
65
+ close(code?: number, reason?: string): void;
66
+ }
67
+ export interface SocketHubOptions<TServices = unknown> {
68
+ channels: Readonly<Record<string, unknown>>;
69
+ sessions: SessionManager;
70
+ services: TServices | ((request: Request) => TServices | Promise<TServices>);
71
+ allowedOrigins?: readonly string[];
72
+ maxMessageBytes?: number;
73
+ maxBufferBytes?: number;
74
+ pubsub?: SocketPubSub;
75
+ }
76
+ export interface SocketHub {
77
+ preflight(name: string, request: Request, roomId?: string): Promise<void>;
78
+ accept(name: string, request: Request, peer: SocketPeer, roomId?: string): Promise<SocketConnection>;
79
+ broadcast(name: string, roomId: string, event: string, payload: unknown): Promise<void>;
80
+ }
81
+ /** The host performs the HTTP upgrade, then passes its connected peer here. */
82
+ export declare function createSocketHub<TServices>(options: SocketHubOptions<TServices>): SocketHub;
83
+ export {};