@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321

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 (261) hide show
  1. package/AGENTS.md +81 -0
  2. package/README.md +56 -16
  3. package/dist/index.d.ts +2034 -137
  4. package/dist/index.js +15236 -2467
  5. package/dist/live.d.ts +141 -0
  6. package/dist/live.js +733 -0
  7. package/dist/otel/index.d.ts +1 -1
  8. package/dist/otel/index.js +3 -3
  9. package/dist/pure.d.ts +365 -0
  10. package/dist/pure.js +24 -0
  11. package/dist/sqlite-open.js +379 -0
  12. package/dist/sqlite-worker.d.ts +1 -0
  13. package/dist/sqlite-worker.js +439 -0
  14. package/dist/sw.d.ts +207 -0
  15. package/dist/sw.js +393 -0
  16. package/dist/tabs-broker-worker.d.ts +8 -0
  17. package/dist/tabs-broker-worker.js +472 -0
  18. package/dist/types.d.ts +1821 -38
  19. package/dist/worker-edges.js +470 -0
  20. package/dist/worker-impersonation.js +60 -0
  21. package/dist/worker-ref-tables.d.ts +48 -0
  22. package/dist/worker-types.d.ts +112 -0
  23. package/dist/worker-types.js +73 -0
  24. package/package.json +27 -7
  25. package/scripts/check-broker-bundle.mjs +33 -0
  26. package/scripts/check-worker-entries.mjs +172 -0
  27. package/skills/sp00ky-core/SKILL.md +31 -6
  28. package/skills/sp00ky-core/references/config.md +6 -0
  29. package/src/boot/auth-flip.saga.test.ts +135 -0
  30. package/src/boot/auth-flip.saga.ts +77 -0
  31. package/src/boot/boot.saga.test.ts +180 -0
  32. package/src/boot/boot.saga.ts +142 -0
  33. package/src/boot/bucket-switch.saga.test.ts +187 -0
  34. package/src/boot/bucket-switch.saga.ts +133 -0
  35. package/src/boot/preload.saga.test.ts +51 -0
  36. package/src/boot/preload.saga.ts +31 -0
  37. package/src/bucket-blurhash.test.ts +148 -0
  38. package/src/build-globals.d.ts +12 -0
  39. package/src/client/bucket-handle.test.ts +117 -0
  40. package/src/client/bucket-handle.ts +227 -0
  41. package/src/client/router.test.ts +58 -0
  42. package/src/client/router.ts +100 -0
  43. package/src/client/runtime.test.ts +195 -0
  44. package/src/client/runtime.ts +296 -0
  45. package/src/client/services.test.ts +60 -0
  46. package/src/client/services.ts +418 -0
  47. package/src/client/sp00ky-client.test.ts +303 -0
  48. package/src/client/sp00ky-client.ts +586 -0
  49. package/src/events/events.test.ts +2 -1
  50. package/src/events/index.ts +3 -0
  51. package/src/index.ts +69 -1
  52. package/src/kernel/constants.test.ts +19 -0
  53. package/src/kernel/constants.ts +80 -0
  54. package/src/kernel/effects.test.ts +40 -0
  55. package/src/kernel/effects.ts +163 -0
  56. package/src/kernel/events.ts +57 -0
  57. package/src/kernel/interpreter.test.ts +116 -0
  58. package/src/kernel/interpreter.ts +148 -0
  59. package/src/kernel/saga.test.ts +89 -0
  60. package/src/kernel/saga.ts +73 -0
  61. package/src/live/index.test.ts +501 -0
  62. package/src/live/index.ts +1039 -0
  63. package/src/modules/app-release/index.test.ts +125 -0
  64. package/src/modules/app-release/index.ts +209 -0
  65. package/src/modules/auth/auth.local-first.test.ts +169 -0
  66. package/src/modules/auth/events/index.ts +9 -2
  67. package/src/modules/auth/impersonation-banner.ts +443 -0
  68. package/src/modules/auth/impersonation.integration.test.ts +205 -0
  69. package/src/modules/auth/impersonation.test.ts +377 -0
  70. package/src/modules/auth/impersonation.ts +179 -0
  71. package/src/modules/auth/index.ts +466 -19
  72. package/src/modules/crdt/crdt-field.ts +294 -0
  73. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  74. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  75. package/src/modules/crdt/index.ts +463 -0
  76. package/src/modules/crdt/loro-loader.ts +25 -0
  77. package/src/modules/devtools/flags.ts +349 -0
  78. package/src/modules/devtools/index.ts +746 -95
  79. package/src/modules/devtools/mutations.test.ts +92 -0
  80. package/src/modules/devtools/mutations.ts +160 -0
  81. package/src/modules/devtools/notify-throttle.test.ts +166 -0
  82. package/src/modules/devtools/state-shape.test.ts +263 -0
  83. package/src/modules/devtools/storage-info.test.ts +79 -0
  84. package/src/modules/devtools/storage-info.ts +168 -0
  85. package/src/modules/devtools/versions.test.ts +74 -0
  86. package/src/modules/devtools/versions.ts +110 -0
  87. package/src/modules/feature-flag/index.test.ts +298 -0
  88. package/src/modules/feature-flag/index.ts +316 -0
  89. package/src/modules/ref-tables.test.ts +93 -0
  90. package/src/modules/ref-tables.ts +91 -0
  91. package/src/modules/web-push/index.test.ts +824 -0
  92. package/src/modules/web-push/index.ts +1027 -0
  93. package/src/mutation/jobs.test.ts +25 -0
  94. package/src/mutation/jobs.ts +41 -0
  95. package/src/mutation/mutation-id.test.ts +37 -0
  96. package/src/mutation/mutation-id.ts +35 -0
  97. package/src/mutation/pending-writes.integration.test.ts +175 -0
  98. package/src/mutation/push.saga.test.ts +285 -0
  99. package/src/mutation/push.saga.ts +217 -0
  100. package/src/mutation/rows.test.ts +231 -0
  101. package/src/mutation/rows.ts +427 -0
  102. package/src/mutation/tray.saga.test.ts +63 -0
  103. package/src/mutation/tray.saga.ts +48 -0
  104. package/src/mutation/write.saga.test.ts +316 -0
  105. package/src/mutation/write.saga.ts +268 -0
  106. package/src/otel/index.ts +10 -7
  107. package/src/pure.ts +66 -0
  108. package/src/push/types.ts +174 -0
  109. package/src/query/anon-query-isolation.integration.test.ts +337 -0
  110. package/src/query/env.test.ts +23 -0
  111. package/src/query/env.ts +56 -0
  112. package/src/query/fetch.saga.test.ts +199 -0
  113. package/src/query/fetch.saga.ts +120 -0
  114. package/src/query/hash.test.ts +14 -0
  115. package/src/query/hash.ts +18 -0
  116. package/src/query/id-set-plan.test.ts +132 -0
  117. package/src/query/lifecycle.saga.test.ts +274 -0
  118. package/src/query/lifecycle.saga.ts +159 -0
  119. package/src/query/list-ref-live.integration.test.ts +203 -0
  120. package/src/query/materialize.saga.test.ts +115 -0
  121. package/src/query/materialize.saga.ts +84 -0
  122. package/src/query/materialize.test.ts +58 -0
  123. package/src/query/materialize.ts +57 -0
  124. package/src/query/membership.saga.test.ts +350 -0
  125. package/src/query/membership.saga.ts +278 -0
  126. package/src/query/membership.test.ts +118 -0
  127. package/src/query/membership.ts +201 -0
  128. package/src/query/register.saga.test.ts +311 -0
  129. package/src/query/register.saga.ts +229 -0
  130. package/src/query/render-set.test.ts +47 -0
  131. package/src/query/render-set.ts +71 -0
  132. package/src/query/sql.test.ts +112 -0
  133. package/src/query/sql.ts +149 -0
  134. package/src/query/window-query.test.ts +70 -0
  135. package/src/query/window-query.ts +195 -0
  136. package/src/services/blobs/blob-cache.test.ts +359 -0
  137. package/src/services/blobs/blob-cache.ts +603 -0
  138. package/src/services/blobs/blob-manifest.ts +227 -0
  139. package/src/services/blobs/blob-store.test.ts +77 -0
  140. package/src/services/blobs/blob-store.ts +359 -0
  141. package/src/services/blobs/blob.fixture.ts +90 -0
  142. package/src/services/blobs/index.ts +70 -0
  143. package/src/services/database/cache-engine.ts +199 -0
  144. package/src/services/database/connection-supervisor.test.ts +318 -0
  145. package/src/services/database/connection-supervisor.ts +422 -0
  146. package/src/services/database/database.query-timeout.test.ts +140 -0
  147. package/src/services/database/database.responses.test.ts +43 -0
  148. package/src/services/database/database.ts +146 -61
  149. package/src/services/database/engine-factory.ts +33 -0
  150. package/src/services/database/errors.ts +34 -0
  151. package/src/services/database/events/index.ts +2 -1
  152. package/src/services/database/index.ts +7 -0
  153. package/src/services/database/landed-body-clear.integration.test.ts +236 -0
  154. package/src/services/database/local-migrator.integration.test.ts +110 -0
  155. package/src/services/database/local-migrator.ts +59 -16
  156. package/src/services/database/local.test.ts +64 -0
  157. package/src/services/database/local.ts +487 -63
  158. package/src/services/database/plan-render.test.ts +159 -0
  159. package/src/services/database/plan-render.ts +108 -0
  160. package/src/services/database/relation-resolver.test.ts +484 -0
  161. package/src/services/database/relation-resolver.ts +0 -0
  162. package/src/services/database/remote.ts +273 -6
  163. package/src/services/database/sqlite-cache-engine.test.ts +617 -0
  164. package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
  165. package/src/services/database/sqlite-cache-engine.ts +1372 -0
  166. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  167. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  168. package/src/services/database/sqlite-lock-verify.test.ts +33 -0
  169. package/src/services/database/sqlite-lock-verify.ts +45 -0
  170. package/src/services/database/sqlite-open.test.ts +150 -0
  171. package/src/services/database/sqlite-open.ts +164 -0
  172. package/src/services/database/sqlite-plan-sql.test.ts +145 -0
  173. package/src/services/database/sqlite-plan-sql.ts +182 -0
  174. package/src/services/database/sqlite-projection.test.ts +121 -0
  175. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  176. package/src/services/database/sqlite-select.test.ts +261 -0
  177. package/src/services/database/sqlite-select.ts +131 -0
  178. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  179. package/src/services/database/sqlite-transport.ts +224 -0
  180. package/src/services/database/sqlite-worker.ts +437 -0
  181. package/src/services/database/surql-translate.ts +416 -0
  182. package/src/services/database/surreal-cache-engine.ts +161 -0
  183. package/src/services/logger/index.test.ts +132 -0
  184. package/src/services/logger/index.ts +189 -7
  185. package/src/services/persistence/localstorage.ts +2 -2
  186. package/src/services/persistence/resilient.ts +10 -3
  187. package/src/services/persistence/surrealdb.ts +4 -4
  188. package/src/services/stream-processor/index.ts +783 -68
  189. package/src/services/stream-processor/permissions.test.ts +47 -0
  190. package/src/services/stream-processor/permissions.ts +53 -0
  191. package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
  192. package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
  193. package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
  194. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  195. package/src/services/stream-processor/wasm-types.ts +58 -2
  196. package/src/services/tabs/broker-client.ts +283 -0
  197. package/src/services/tabs/broker.test.ts +327 -0
  198. package/src/services/tabs/coordinator.test.ts +386 -0
  199. package/src/services/tabs/coordinator.ts +638 -0
  200. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  201. package/src/services/tabs/leader-locks.ts +75 -0
  202. package/src/services/tabs/protocol.ts +262 -0
  203. package/src/services/tabs/support.ts +36 -0
  204. package/src/services/tabs/tabs-broker-worker.ts +640 -0
  205. package/src/sp00ky.ts +5 -395
  206. package/src/state/client-state.ts +200 -0
  207. package/src/state/lifecycle.test.ts +141 -0
  208. package/src/state/lifecycle.ts +99 -0
  209. package/src/state/reducers.test.ts +304 -0
  210. package/src/state/reducers.ts +526 -0
  211. package/src/state/selectors.test.ts +175 -0
  212. package/src/state/selectors.ts +222 -0
  213. package/src/sw/index.test.ts +582 -0
  214. package/src/sw/index.ts +607 -0
  215. package/src/sw/store.ts +96 -0
  216. package/src/sync/connection.saga.test.ts +97 -0
  217. package/src/sync/connection.saga.ts +89 -0
  218. package/src/sync/live.saga.test.ts +178 -0
  219. package/src/sync/live.saga.ts +107 -0
  220. package/src/sync/policy.health.test.ts +56 -0
  221. package/src/sync/policy.test.ts +663 -0
  222. package/src/sync/policy.ts +515 -0
  223. package/src/sync/poll.saga.test.ts +51 -0
  224. package/src/sync/poll.saga.ts +36 -0
  225. package/src/sync/tabs.saga.test.ts +78 -0
  226. package/src/sync/tabs.saga.ts +96 -0
  227. package/src/testing/adapters.ts +133 -0
  228. package/src/testing/build.ts +63 -0
  229. package/src/testing/fake-services.ts +48 -0
  230. package/src/testing/run-pure.test.ts +92 -0
  231. package/src/testing/run-pure.ts +128 -0
  232. package/src/testing/services.ts +17 -0
  233. package/src/types.ts +624 -10
  234. package/src/utils/blurhash.ts +90 -0
  235. package/src/utils/edges.ts +39 -0
  236. package/src/utils/error-classification.test.ts +54 -0
  237. package/src/utils/error-classification.ts +25 -0
  238. package/src/utils/index.ts +78 -12
  239. package/src/utils/parser.test.ts +67 -102
  240. package/src/utils/parser.ts +64 -2
  241. package/src/utils/semver.test.ts +32 -0
  242. package/src/utils/semver.ts +30 -0
  243. package/src/utils/sha256.ts +15 -0
  244. package/src/utils/surql.ts +30 -18
  245. package/src/utils/utils.test.ts +3 -2
  246. package/tsdown.config.ts +117 -3
  247. package/vitest.config.ts +48 -0
  248. package/src/modules/cache/index.ts +0 -241
  249. package/src/modules/cache/types.ts +0 -19
  250. package/src/modules/data/data.test.ts +0 -58
  251. package/src/modules/data/index.ts +0 -879
  252. package/src/modules/sync/engine.ts +0 -173
  253. package/src/modules/sync/events/index.ts +0 -77
  254. package/src/modules/sync/index.ts +0 -3
  255. package/src/modules/sync/queue/index.ts +0 -2
  256. package/src/modules/sync/queue/queue-down.ts +0 -89
  257. package/src/modules/sync/queue/queue-up.ts +0 -223
  258. package/src/modules/sync/scheduler.ts +0 -84
  259. package/src/modules/sync/sync.ts +0 -421
  260. package/src/modules/sync/utils.test.ts +0 -311
  261. package/src/modules/sync/utils.ts +0 -167
@@ -0,0 +1,470 @@
1
+ import { DateTime, Duration, RecordId } from "surrealdb";
2
+ import { RecordId as RecordId$1, baseFieldOfParam, cyrb53 } from "@spooky-sync/query-builder";
3
+
4
+ //#region src/query/hash.ts
5
+ /** Session-salted: two tabs of one user must not share a remote view. */
6
+ const queryHashInput = (input, sessionId) => JSON.stringify({
7
+ surql: input.surql,
8
+ params: input.params,
9
+ sessionId
10
+ });
11
+ /** Session-independent: survives reload and bucket switch. */
12
+ const viewKeyInput = (input) => JSON.stringify({
13
+ surql: input.surql,
14
+ params: input.params
15
+ });
16
+
17
+ //#endregion
18
+ //#region src/utils/sha256.ts
19
+ /**
20
+ * Lowercase hex SHA-256 of a UTF-8 string, through WebCrypto.
21
+ *
22
+ * Pure and dependency free so it can live in `@spooky-sync/core/pure` and run
23
+ * in a service worker, a dedicated worker or Node (`globalThis.crypto` is
24
+ * there on Node 19+). Query keys are `sha256Hex(JSON.stringify(..))`, see
25
+ * `query/hash.ts`.
26
+ */
27
+ async function sha256Hex(input) {
28
+ const bytes = new TextEncoder().encode(input);
29
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
30
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
31
+ }
32
+
33
+ //#endregion
34
+ //#region src/utils/surql.ts
35
+ const surql = {
36
+ seal(query, options) {
37
+ if (typeof query === "string") return `${query};`;
38
+ const txQuery = query;
39
+ const idx = options?.resultIndex ?? txQuery.statementCount - 1;
40
+ return {
41
+ sql: `${txQuery.sql};`,
42
+ extract(results) {
43
+ return results[idx + 1];
44
+ }
45
+ };
46
+ },
47
+ tx(queries) {
48
+ return {
49
+ __brand: "TxQuery",
50
+ sql: `BEGIN TRANSACTION;\n${queries.join(";")};\nCOMMIT TRANSACTION`,
51
+ statementCount: queries.length
52
+ };
53
+ },
54
+ selectById(idVar, returnValues) {
55
+ return `SELECT ${returnValues.join(",")} FROM ONLY $${idVar}`;
56
+ },
57
+ selectByFieldsAnd(table, whereVar, returnValues) {
58
+ return `SELECT ${returnValues.map((rv) => typeof rv === "string" ? rv : `${rv.field} as ${rv.alias}`).join(",")} FROM ${table} WHERE ${whereVar.map((wv) => typeof wv === "string" ? `${wv} = $${wv}` : `${wv.field} = $${wv.variable}`).join(" AND ")}`;
59
+ },
60
+ create(idVar, dataVar) {
61
+ return `CREATE ONLY $${idVar} CONTENT $${dataVar}`;
62
+ },
63
+ createSet(idVar, keyDataVars) {
64
+ return `CREATE ONLY $${idVar} SET ${keyDataVars.map((keyDataVar) => typeof keyDataVar === "string" ? `${keyDataVar} = $${keyDataVar}` : "statement" in keyDataVar ? keyDataVar.statement : `${keyDataVar.key} = $${keyDataVar.variable}`).join(", ")}`;
65
+ },
66
+ upsert(idVar, dataVar) {
67
+ return `UPSERT ONLY $${idVar} REPLACE $${dataVar}`;
68
+ },
69
+ upsertMerge(idVar, dataVar) {
70
+ return `UPSERT ONLY $${idVar} MERGE $${dataVar}`;
71
+ },
72
+ updateMerge(idVar, dataVar) {
73
+ return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
74
+ },
75
+ updateSet(idVar, keyDataVar) {
76
+ return `UPDATE $${idVar} SET ${keyDataVar.map((kdv) => typeof kdv === "string" ? `${kdv} = $${kdv}` : "statement" in kdv ? kdv.statement : `${kdv.key} = $${kdv.variable}`).join(", ")}`;
77
+ },
78
+ delete(idVar) {
79
+ return `DELETE $${idVar}`;
80
+ },
81
+ let(name, query) {
82
+ return `LET $${name} = (${query})`;
83
+ },
84
+ createMutation(t, mutationIdVar, recordIdVar, dataVar, beforeRecordVar) {
85
+ switch (t) {
86
+ case "create": return dataVar ? `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}, data = $${dataVar}` : `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}`;
87
+ case "update": {
88
+ let stmt = `CREATE ONLY $${mutationIdVar} SET mutationType = 'update', recordId = $${recordIdVar}, data = $${dataVar}`;
89
+ if (beforeRecordVar) stmt += `, beforeRecord = $${beforeRecordVar}`;
90
+ return stmt;
91
+ }
92
+ case "delete": return `CREATE ONLY $${mutationIdVar} SET mutationType = 'delete', recordId = $${recordIdVar}`;
93
+ }
94
+ },
95
+ returnObject(entries) {
96
+ return `RETURN {${entries.map(({ key, variable }) => `${key}: $${variable}`).join(",")}}`;
97
+ }
98
+ };
99
+
100
+ //#endregion
101
+ //#region src/modules/ref-tables.ts
102
+ /**
103
+ * Sentinel user id for unauthenticated clients when anonymous live queries are
104
+ * enabled. Mirrors `ssp_protocol::ANON_AUTH_ID`. It carries no `user:` prefix
105
+ * so it can never collide with a real user id (those arrive as `user:<id>`);
106
+ * both sides resolve it to the shared `_00_list_ref_anon` table.
107
+ */
108
+ const ANON_USER_ID = "anon";
109
+ /** The one `_00_list_ref` table every signed-out session shares. */
110
+ const ANON_LIST_REF_TABLE = "_00_list_ref_anon";
111
+ /**
112
+ * Default ref-storage mode for this client build. Mirrors the SSP's
113
+ * default (`RefMode::Dedicated`) so cross-session sync works out of the
114
+ * box.
115
+ */
116
+ const DEFAULT_REF_MODE = "dedicated";
117
+ /**
118
+ * Sanitize a user record id (e.g. `"user:abc"`) into the segment that
119
+ * goes into a dedicated table name (e.g. `"abc"`). Returns `null` if
120
+ * the id is missing the `user:` prefix or contains characters that
121
+ * aren't valid in a SurrealDB table identifier — the server-side
122
+ * `ssp_protocol::sanitize_user_id` uses the same predicate.
123
+ *
124
+ * Accepts both string ids (`"user:abc"`) and SurrealDB `RecordId`
125
+ * objects (which only stringify cleanly via `.toString()`), since
126
+ * `AuthService` passes the record-id object as-is to its subscribers.
127
+ */
128
+ function sanitizeUserId(userId) {
129
+ if (userId === null || userId === void 0) return null;
130
+ const asString = typeof userId === "string" ? userId : typeof userId.toString === "function" ? userId.toString() : null;
131
+ if (!asString) return null;
132
+ const raw = asString.startsWith("user:") ? asString.slice(5) : asString;
133
+ if (raw.length === 0) return null;
134
+ if (!/^[A-Za-z0-9_]+$/.test(raw)) return null;
135
+ return raw;
136
+ }
137
+ /**
138
+ * Resolve the LOCAL storage bucket id for a user. Every user gets their own
139
+ * IndexedDB-backed local store (`indxdb://sp00ky-<bucketId>`) so cached rows,
140
+ * query state, and the mutation outbox never leak across accounts on a shared
141
+ * device. Signed-out sessions share the `anon` bucket.
142
+ *
143
+ * An id that fails sanitization still gets a DETERMINISTIC per-user bucket
144
+ * (cyrb53 hex of the raw id) — falling back to `anon` here would put an
145
+ * authenticated user in the shared bucket and recreate the cross-user leak.
146
+ */
147
+ function bucketIdForUser(userId) {
148
+ if (userId === null || userId === void 0 || userId === ANON_USER_ID) return ANON_USER_ID;
149
+ const uid = sanitizeUserId(userId);
150
+ if (uid) return uid;
151
+ return `u${cyrb53(String(userId)).toString(16)}`;
152
+ }
153
+ /**
154
+ * Returns the `_00_list_ref` table name for `(mode, userId)`. Falls
155
+ * back to the global `_00_list_ref` when sanitization fails or in
156
+ * single mode.
157
+ */
158
+ function listRefTableFor(mode, userId) {
159
+ if (userId === ANON_USER_ID) return ANON_LIST_REF_TABLE;
160
+ if (mode === "single") return "_00_list_ref";
161
+ const uid = sanitizeUserId(userId);
162
+ return uid ? `_00_list_ref_user_${uid}` : "_00_list_ref";
163
+ }
164
+
165
+ //#endregion
166
+ //#region src/query/sql.ts
167
+ /** Every SurrealQL statement the query side sends, as pure builders. */
168
+ const VIEW_TABLE = "_00_view";
169
+ const LEGACY_VIEW_TABLE = "_00_window";
170
+ const viewRecordId = (viewKey) => new RecordId(VIEW_TABLE, viewKey);
171
+ /**
172
+ * The durable `_00_view` row: the confirmed membership plus the subquery
173
+ * children, which the GC must keep and a cold scan may paint as much as the
174
+ * members.
175
+ */
176
+ const viewRow = (ids, children, confirmed, now) => ({
177
+ ids,
178
+ children,
179
+ confirmed,
180
+ updatedAt: now
181
+ });
182
+ /** Every `_00_view` row: the durable view index and the GC read it. */
183
+ const readViewRows = () => `SELECT * FROM ${VIEW_TABLE}`;
184
+ /** Copy every legacy `_00_window` row into `_00_view` once. */
185
+ const readLegacyViewRows = () => `SELECT * FROM ${LEGACY_VIEW_TABLE}`;
186
+ const countViewRows = () => `SELECT count() FROM ${VIEW_TABLE} GROUP ALL`;
187
+ const listRefSelect = (table) => `SELECT out, version FROM $in->${table} WHERE parent IS NONE`;
188
+ const subqueryListRefSelect = (table) => `SELECT out, version FROM $in->${table} WHERE parent IS NOT NONE`;
189
+ const queryRowCountSelect = () => "SELECT VALUE { rowCount: rowCount, state: state } FROM ONLY $in";
190
+ const listRefBatchSelect = (table) => `SELECT in, out, version, parent FROM $ins->${table}`;
191
+ const queryRowCountBatchSelect = () => "SELECT VALUE { id: id, rowCount: rowCount, state: state } FROM $ins";
192
+ /** The single-query read: edges, meta, subquery children in one request. */
193
+ const singleSnapshotSelect = (table) => `${listRefSelect(table)};\n${queryRowCountSelect()};\n${subqueryListRefSelect(table)}`;
194
+ /** The many-query read: all edges + all metas in one request. */
195
+ const batchSnapshotSelect = (table) => `${listRefBatchSelect(table)};\n${queryRowCountBatchSelect()}`;
196
+ /**
197
+ * The session's one LIVE subscription on its `_00_list_ref` table.
198
+ *
199
+ * `inlineBodies` adds `FETCH out`, which resolves the edge's target into the
200
+ * notification so the body arrives with the doorbell.
201
+ *
202
+ * On the anon table it is filtered to this client's own edges. Every
203
+ * signed-out session shares that table, so unfiltered each one received (and
204
+ * the server fetched a body for) every anonymous edge change of every other
205
+ * session, and the client then dropped all but its own. An edge carries the
206
+ * `session::id()` of whichever socket last registered its view, which is a
207
+ * socket of this client but not necessarily the current one: a view stays
208
+ * registered across a reconnect, and a reload keeps its salt and its views,
209
+ * so their older edges still name an earlier socket. `sessionIds` are those
210
+ * earlier sockets. The current one is matched by `session::id()` itself,
211
+ * which SurrealDB evaluates in the subscriber's own session, so the filter
212
+ * holds before the client has learned its new id.
213
+ *
214
+ * The cast is load-bearing: `session::id()` is a uuid, `clientId` a string,
215
+ * and without it nothing matches. Per-user tables need no filter.
216
+ */
217
+ function listRefLive(table, opts) {
218
+ const join = opts.inlineBodies ? " FETCH out" : "";
219
+ if (table !== ANON_LIST_REF_TABLE) return { sql: `LIVE SELECT * FROM ${table}${join}` };
220
+ return {
221
+ sql: `LIVE SELECT * FROM ${table} WHERE clientId = <string>session::id() OR clientId IN $sids${join}`,
222
+ vars: { sids: [...opts.sessionIds] }
223
+ };
224
+ }
225
+ /** Register + read back edges/meta/children in ONE request. */
226
+ const registerSelect = (table) => `fn::query::register($config);\n${singleSnapshotSelect(table)}`;
227
+ const registerVars = (payload) => ({
228
+ config: payload,
229
+ in: payload.id
230
+ });
231
+ /** One heartbeat statement per view id; result index i answers id i. */
232
+ function heartbeatBatch(ids) {
233
+ const vars = {};
234
+ return {
235
+ sql: ids.map((id, i) => {
236
+ vars[`id${i}`] = id;
237
+ return `fn::query::heartbeat($id${i})`;
238
+ }).join(";\n"),
239
+ vars
240
+ };
241
+ }
242
+ /** A heartbeat answer whose statement matched no row: the view was reclaimed. */
243
+ const heartbeatRowGone = (result) => Array.isArray(result) && result.length === 0;
244
+ const QUERY_ID = /^_00_query:(?:[0-9a-f]{64}|⟨[0-9a-f]{64}⟩)$/;
245
+ const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
246
+ /**
247
+ * The pagehide release: one bare statement for `beaconSql`, which has no params
248
+ * channel, so ids are inlined. Only shapes the client minted itself get in, a
249
+ * `_00_query:<sha256>` id and a UUID session id, so nothing else is ever
250
+ * interpolated. `null` when there is nothing the server could act on: with no
251
+ * session ids it could not tell this page's entries from anyone else's.
252
+ */
253
+ function leaveViewsBeacon(ids, sessionIds) {
254
+ const list = ids.map((id) => String(id)).filter((id) => QUERY_ID.test(id));
255
+ const sids = sessionIds.filter((sid) => SESSION_ID.test(sid));
256
+ if (list.length === 0 || sids.length === 0) return null;
257
+ const sidList = sids.map((sid) => `'${sid}'`).join(", ");
258
+ return `FOR $id IN [${list.join(", ")}] { LET $_left = fn::query::leave($id, [${sidList}]); };`;
259
+ }
260
+ const bodySelect = () => "SELECT * FROM $ids";
261
+ /** Local transaction that MERGEs many fetched bodies. */
262
+ function upsertBodiesTx(records) {
263
+ const vars = {};
264
+ const stmts = records.map((r, i) => {
265
+ vars[`id${i}`] = r.id;
266
+ vars[`content${i}`] = r.content;
267
+ return surql.upsertMerge(`id${i}`, `content${i}`);
268
+ });
269
+ return {
270
+ query: surql.seal(surql.tx(stmts)),
271
+ vars
272
+ };
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/utils/parser.ts
277
+ /**
278
+ * Parse a QUERY's params. Unlike a record's fields, a param name is not always a
279
+ * column: an `_or` branch binds under a synthetic `white__or0` (see
280
+ * `orParamName` in @spooky-sync/query-builder), and the surql that references it
281
+ * is already written. So resolve the column through the synthetic name, and keep
282
+ * a param we cannot type rather than dropping it.
283
+ *
284
+ * Dropping was the bug: `parseParams` kept only column-named params, so every
285
+ * `_or` query registered with `$or0`/`$or1` unbound and matched no rows at all,
286
+ * silently (rowCount 0, errorCount 0).
287
+ */
288
+ function parseQueryParams(tableSchema, params) {
289
+ const parsedParams = {};
290
+ for (const [key, value] of Object.entries(params)) {
291
+ if (value === void 0) continue;
292
+ const column = tableSchema[key] ?? tableSchema[baseFieldOfParam(key)];
293
+ parsedParams[key] = column ? parseValue(key, column, value) : value;
294
+ }
295
+ return parsedParams;
296
+ }
297
+ function parseValue(name, column, value) {
298
+ if (column.recordId) {
299
+ if (value instanceof RecordId$1) return value;
300
+ if (typeof value === "string") return parseRecordIdString(value);
301
+ throw new Error(`Invalid value for ${name}: ${value}`);
302
+ }
303
+ if (column.dateTime) {
304
+ if (value instanceof Date) return value;
305
+ if (value instanceof DateTime) return value.toDate();
306
+ if (typeof value === "number" || typeof value === "string") return new Date(value);
307
+ throw new Error(`Invalid value for ${name}: ${value}`);
308
+ }
309
+ return value;
310
+ }
311
+
312
+ //#endregion
313
+ //#region src/utils/index.ts
314
+ const encodeRecordId = (recordId) => {
315
+ return `${recordId.table.name}:${recordId.id}`;
316
+ };
317
+ const parseRecordIdString = (id) => {
318
+ const [table, ...idParts] = id.split(":");
319
+ return new RecordId(table, idParts.join(":"));
320
+ };
321
+ /**
322
+ * Read the millisecond count off a surrealdb `Duration`. Different `surrealdb`
323
+ * releases expose it under `milliseconds` (current) or the older private
324
+ * `_milliseconds` field, so read whichever is set; returns 0 when neither is.
325
+ */
326
+ function durationMillis(duration) {
327
+ const d = duration;
328
+ return Number(d.milliseconds || d._milliseconds || 0);
329
+ }
330
+ /**
331
+ * Parse duration string or Duration object to milliseconds
332
+ */
333
+ function parseDuration(duration) {
334
+ if (duration instanceof Duration) {
335
+ const ms = durationMillis(duration);
336
+ if (ms) return ms;
337
+ const str = duration.toString();
338
+ if (str !== "[object Object]") return parseDuration(str);
339
+ return 6e5;
340
+ }
341
+ if (typeof duration === "bigint") return Number(duration);
342
+ if (typeof duration !== "string") return 6e5;
343
+ const match = duration.match(/^(\d+)([smh])$/);
344
+ if (!match) return 6e5;
345
+ const val = Number.parseInt(match[1], 10);
346
+ switch (match[2]) {
347
+ case "s": return val * 1e3;
348
+ case "h": return val * 36e5;
349
+ default: return val * 6e4;
350
+ }
351
+ }
352
+ /**
353
+ * Reject after `timeoutMs` if `promise` hasn't settled.
354
+ *
355
+ * Exists because a WebSocket RPC has no deadline of its own: the SurrealDB SDK
356
+ * parks each call in a pending map and only rejects it when the socket reports
357
+ * a close. On a half-open socket (peer gone, no `close` event, `readyState`
358
+ * still OPEN) the call never settles at all, which wedges anything serialized
359
+ * behind it.
360
+ *
361
+ * `message` MUST contain "timed out" so `classifySyncError` treats the
362
+ * rejection as `network` — that's what makes the sync queues retry the
363
+ * operation instead of rolling the mutation back as an application error.
364
+ *
365
+ * Non-positive `timeoutMs` returns `promise` unchanged. The underlying promise
366
+ * is NOT cancelled (nothing can cancel an in-flight RPC); its eventual
367
+ * settlement is absorbed, so attach no expectations to it after a timeout.
368
+ */
369
+ function withTimeout(promise, timeoutMs, message) {
370
+ if (!(timeoutMs > 0)) return promise;
371
+ return new Promise((resolve, reject) => {
372
+ const timer = setTimeout(() => reject(typeof message === "function" ? message() : new Error(message)), timeoutMs);
373
+ promise.then((value) => {
374
+ clearTimeout(timer);
375
+ resolve(value);
376
+ }, (err) => {
377
+ clearTimeout(timer);
378
+ reject(err);
379
+ });
380
+ });
381
+ }
382
+
383
+ //#endregion
384
+ //#region src/query/membership.ts
385
+ function metaFromRow(row) {
386
+ if (!row || typeof row !== "object") return {
387
+ present: false,
388
+ rowCount: null,
389
+ state: null
390
+ };
391
+ const state = row.state === "materializing" || row.state === "ready" ? row.state : null;
392
+ return {
393
+ present: true,
394
+ rowCount: typeof row.rowCount === "number" ? row.rowCount : null,
395
+ state
396
+ };
397
+ }
398
+ /** Collapse duplicate `(id, version)` pairs to one per id, keeping the highest version. */
399
+ function dedupeRecordVersions(pairs) {
400
+ if (pairs.length < 2) return pairs;
401
+ const best = /* @__PURE__ */ new Map();
402
+ for (const [id, version] of pairs) {
403
+ const prev = best.get(id);
404
+ if (prev === void 0 || version > prev) best.set(id, version);
405
+ }
406
+ if (best.size === pairs.length) return pairs;
407
+ return Array.from(best.entries());
408
+ }
409
+ /**
410
+ * A statement that did not answer (an ERR, or no result at all). Kept apart
411
+ * from a `null` result, which is the server saying "no such row": reading a
412
+ * failed statement as a missing row turned every timeout under load into a
413
+ * lost view and re-registered it, over and over (whitepawn, 2026-09-16).
414
+ */
415
+ const FAILED = Symbol("statement failed");
416
+ const toPairs = (rows) => dedupeRecordVersions(Array.isArray(rows) ? rows.map((r) => [encodeRecordId(r.out), r.version]) : []);
417
+ /**
418
+ * Fold the single-query statement batch (edges, meta, children) into a
419
+ * snapshot. `null` when any of the three did not answer: half an answer is
420
+ * not one, and a missing meta would read as a lost view.
421
+ */
422
+ function snapshotFromSingle(items, metaRow, children) {
423
+ if (!Array.isArray(items) || metaRow === FAILED || children === FAILED) return null;
424
+ return {
425
+ primary: toPairs(items),
426
+ subquery: toPairs(children),
427
+ meta: metaFromRow(metaRow)
428
+ };
429
+ }
430
+
431
+ //#endregion
432
+ //#region src/utils/edges.ts
433
+ /**
434
+ * Reading `_00_list_ref` edges as a LIVE notification delivers them. Pure, so
435
+ * both the full client and the wasm-free live feed (`@spooky-sync/core/live`)
436
+ * decode the same wire shape the same way.
437
+ */
438
+ /** The hash a `_00_list_ref` edge belongs to: the id part of its `in` (`_00_query:<hash>`). */
439
+ function hashOfEdge(value) {
440
+ const inId = value?.in;
441
+ if (!inId) return null;
442
+ const str = inId instanceof RecordId ? String(inId.id) : String(inId).replace(/^_00_query:/, "");
443
+ return str.length > 0 ? str : null;
444
+ }
445
+ /**
446
+ * The row an edge notification carries, when the subscription joined it on.
447
+ *
448
+ * Without `FETCH out` the edge's `out` is a record id and there is nothing to
449
+ * land, so this returns null and the caller falls back to fetching the body.
450
+ * It also returns null for a row the session may not read (the join yields
451
+ * `out: null`) and for an edge whose target has been deleted.
452
+ */
453
+ function rowOfEdge(value) {
454
+ const edge = value;
455
+ const out = edge?.out;
456
+ if (!out || typeof out !== "object" || Array.isArray(out) || out instanceof RecordId) return null;
457
+ const rid = out.id;
458
+ if (!(rid instanceof RecordId)) return null;
459
+ const id = encodeRecordId(rid);
460
+ const version = typeof edge?.version === "number" ? edge.version : null;
461
+ if (version === null) return null;
462
+ return {
463
+ id,
464
+ version,
465
+ record: out
466
+ };
467
+ }
468
+
469
+ //#endregion
470
+ export { subqueryListRefSelect as A, surql as B, queryRowCountBatchSelect as C, registerSelect as D, readViewRows as E, ANON_USER_ID as F, queryHashInput as H, DEFAULT_REF_MODE as I, bucketIdForUser as L, viewRecordId as M, viewRow as N, registerVars as O, ANON_LIST_REF_TABLE as P, listRefTableFor as R, listRefSelect as S, readLegacyViewRows as T, viewKeyInput as U, sha256Hex as V, heartbeatBatch as _, metaFromRow as a, listRefBatchSelect as b, parseDuration as c, parseQueryParams as d, LEGACY_VIEW_TABLE as f, countViewRows as g, bodySelect as h, dedupeRecordVersions as i, upsertBodiesTx as j, singleSnapshotSelect as k, parseRecordIdString as l, batchSnapshotSelect as m, rowOfEdge as n, snapshotFromSingle as o, VIEW_TABLE as p, FAILED as r, encodeRecordId as s, hashOfEdge as t, withTimeout as u, heartbeatRowGone as v, queryRowCountSelect as w, listRefLive as x, leaveViewsBeacon as y, sanitizeUserId as z };
@@ -0,0 +1,60 @@
1
+ //#region src/modules/auth/impersonation.ts
2
+ /**
3
+ * Admin impersonation, client side.
4
+ *
5
+ * The server does the enforcing (see `impersonation_remote.surql` in the CLI):
6
+ * an admin calls `fn::_00_impersonate::start`, receives a token for the
7
+ * `_00_impersonate` access method, and authenticates with it, so `$auth.id` is
8
+ * the target user. Everything here is state and timers around that:
9
+ *
10
+ * - the admin's own token is kept under {@link ORIGIN_TOKEN_KEY} so Stop (and a
11
+ * reload in between) can return to it;
12
+ * - the impersonation state is derived from the token's claims, never stored
13
+ * separately, so it cannot disagree with the session actually in use;
14
+ * - the token is re-signed before it expires, and the session is polled so a
15
+ * stop or revocation made elsewhere reaches an open WebSocket quickly.
16
+ */
17
+ /** Access method impersonation tokens are issued for. */
18
+ const IMPERSONATION_ACCESS = "_00_impersonate";
19
+ /**
20
+ * Read the claims of a SurrealDB record-access JWT WITHOUT verifying it. The
21
+ * server still enforces the token on every request; this is only so the client
22
+ * can act on what it already holds before a round trip completes.
23
+ *
24
+ * Returns nulls on any malformed input.
25
+ */
26
+ function decodeTokenClaims(token) {
27
+ const none = {
28
+ access: null,
29
+ userId: null,
30
+ exp: null,
31
+ impersonation: null
32
+ };
33
+ try {
34
+ const payload = token.split(".")[1];
35
+ if (!payload) return none;
36
+ let b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
37
+ b64 += "=".repeat((4 - b64.length % 4) % 4);
38
+ const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("binary");
39
+ const claims = JSON.parse(json);
40
+ const ac = claims.AC ?? claims.ac;
41
+ const id = claims.ID ?? claims.id;
42
+ const access = typeof ac === "string" ? ac : null;
43
+ const session = claims.spky_imp;
44
+ const admin = claims.spky_admin;
45
+ return {
46
+ access,
47
+ userId: typeof id === "string" ? id : null,
48
+ exp: typeof claims.exp === "number" ? claims.exp : null,
49
+ impersonation: access === IMPERSONATION_ACCESS && typeof session === "string" && typeof admin === "string" ? {
50
+ session,
51
+ admin
52
+ } : null
53
+ };
54
+ } catch {
55
+ return none;
56
+ }
57
+ }
58
+
59
+ //#endregion
60
+ export { decodeTokenClaims as n, IMPERSONATION_ACCESS as t };
@@ -0,0 +1,48 @@
1
+ //#region src/modules/ref-tables.d.ts
2
+ type RefMode = 'single' | 'dedicated';
3
+ /**
4
+ * Sentinel user id for unauthenticated clients when anonymous live queries are
5
+ * enabled. Mirrors `ssp_protocol::ANON_AUTH_ID`. It carries no `user:` prefix
6
+ * so it can never collide with a real user id (those arrive as `user:<id>`);
7
+ * both sides resolve it to the shared `_00_list_ref_anon` table.
8
+ */
9
+ declare const ANON_USER_ID = "anon";
10
+ /** The one `_00_list_ref` table every signed-out session shares. */
11
+ declare const ANON_LIST_REF_TABLE = "_00_list_ref_anon";
12
+ /**
13
+ * Default ref-storage mode for this client build. Mirrors the SSP's
14
+ * default (`RefMode::Dedicated`) so cross-session sync works out of the
15
+ * box.
16
+ */
17
+ declare const DEFAULT_REF_MODE: RefMode;
18
+ /**
19
+ * Sanitize a user record id (e.g. `"user:abc"`) into the segment that
20
+ * goes into a dedicated table name (e.g. `"abc"`). Returns `null` if
21
+ * the id is missing the `user:` prefix or contains characters that
22
+ * aren't valid in a SurrealDB table identifier — the server-side
23
+ * `ssp_protocol::sanitize_user_id` uses the same predicate.
24
+ *
25
+ * Accepts both string ids (`"user:abc"`) and SurrealDB `RecordId`
26
+ * objects (which only stringify cleanly via `.toString()`), since
27
+ * `AuthService` passes the record-id object as-is to its subscribers.
28
+ */
29
+ declare function sanitizeUserId(userId: unknown): string | null;
30
+ /**
31
+ * Resolve the LOCAL storage bucket id for a user. Every user gets their own
32
+ * IndexedDB-backed local store (`indxdb://sp00ky-<bucketId>`) so cached rows,
33
+ * query state, and the mutation outbox never leak across accounts on a shared
34
+ * device. Signed-out sessions share the `anon` bucket.
35
+ *
36
+ * An id that fails sanitization still gets a DETERMINISTIC per-user bucket
37
+ * (cyrb53 hex of the raw id) — falling back to `anon` here would put an
38
+ * authenticated user in the shared bucket and recreate the cross-user leak.
39
+ */
40
+ declare function bucketIdForUser(userId: unknown): string;
41
+ /**
42
+ * Returns the `_00_list_ref` table name for `(mode, userId)`. Falls
43
+ * back to the global `_00_list_ref` when sanitization fails or in
44
+ * single mode.
45
+ */
46
+ declare function listRefTableFor(mode: RefMode, userId: unknown): string;
47
+ //#endregion
48
+ export { bucketIdForUser as a, RefMode as i, ANON_USER_ID as n, listRefTableFor as o, DEFAULT_REF_MODE as r, sanitizeUserId as s, ANON_LIST_REF_TABLE as t };
@@ -0,0 +1,112 @@
1
+ //#region src/push/types.d.ts
2
+ /**
3
+ * Web Push wire types shared by the page (`db.webPush`), the service worker
4
+ * bridge (`@spooky-sync/core/sw`) and the live feed. Pure: types plus a few
5
+ * dependency-free helpers, safe in any context.
6
+ *
7
+ * `PushPayload` mirrors `packages/push-core/src/config.rs` (`PushPayload`,
8
+ * `PayloadKind`, `Op`, `NotificationTemplate`). Keep the two in step.
9
+ */
10
+ /** Payload format version this build understands. */
11
+ declare const PUSH_PAYLOAD_VERSION = 1;
12
+ type PushPayloadKind = 'rule' | 'message';
13
+ type PushOp = 'create' | 'update' | 'delete';
14
+ type PushUrgency = 'very-low' | 'low' | 'normal' | 'high';
15
+ /** One button on a notification (`NotificationAction` plus a per-action `url`). */
16
+ interface PushNotificationAction {
17
+ action: string;
18
+ title: string;
19
+ icon?: string;
20
+ /** Opened instead of the notification's `url` when this action is clicked. */
21
+ url?: string;
22
+ [key: string]: unknown;
23
+ }
24
+ /**
25
+ * What a content push shows. Every key other than `title` and `url` is passed
26
+ * to `showNotification` as is, unknown ones included (`vibrate`, `timestamp`,
27
+ * whatever a browser adds later).
28
+ */
29
+ interface PushNotification {
30
+ title?: string;
31
+ body?: string;
32
+ icon?: string;
33
+ badge?: string;
34
+ image?: string;
35
+ lang?: string;
36
+ dir?: string;
37
+ /** Defaults to the push's `topic`. */
38
+ tag?: string;
39
+ /** Opened (or focused) on click. */
40
+ url?: string;
41
+ requireInteraction?: boolean;
42
+ renotify?: boolean;
43
+ silent?: boolean;
44
+ actions?: PushNotificationAction[];
45
+ /** Merged into the notification's `data` next to the bridge's own keys. */
46
+ data?: Record<string, unknown>;
47
+ [key: string]: unknown;
48
+ }
49
+ /**
50
+ * The JSON every push carries (then RFC 8291 encrypted). `notification`
51
+ * present: a content push the service worker shows as is. Absent: a
52
+ * content-free nudge, the app renders from its own data.
53
+ */
54
+ interface PushPayload {
55
+ v: number;
56
+ kind: PushPayloadKind;
57
+ /** Rule name (`kind: 'rule'`). */
58
+ rule?: string;
59
+ /** `_00_push_message` id (`kind: 'message'`). */
60
+ message?: string;
61
+ /** The row that fired the rule. */
62
+ table?: string;
63
+ id?: string;
64
+ op?: PushOp;
65
+ topic?: string;
66
+ notification?: PushNotification;
67
+ data?: Record<string, unknown>;
68
+ /** Epoch ms when the engine built it. */
69
+ ts: number;
70
+ }
71
+ /** Page -> service worker: the session to use for live rendering. */
72
+ interface BridgeTokenMessage {
73
+ type: 'sp00ky:token';
74
+ token: string;
75
+ userId: string;
76
+ /** The SurrealDB endpoint (`wss://host/rpc`), not the push endpoint. */
77
+ endpoint: string;
78
+ namespace: string;
79
+ database: string;
80
+ /** VAPID public key (base64url) the page subscribed with, for `pushsubscriptionchange`. */
81
+ publicKey?: string;
82
+ /** This browser's push endpoint, when subscribed. */
83
+ pushEndpoint?: string;
84
+ }
85
+ /** Page -> service worker: forget the session and close every notification. */
86
+ interface BridgeSignOutMessage {
87
+ type: 'sp00ky:signout';
88
+ }
89
+ /** Service worker -> page messages. */
90
+ type WebPushClientMessage = /** A push arrived while this page was visible, so nothing was shown. */
91
+ {
92
+ type: 'sp00ky:push';
93
+ payload: PushPayload;
94
+ }
95
+ /** A notification was clicked and the bridge was told to route in-app. */ | {
96
+ type: 'sp00ky:navigate';
97
+ url: string;
98
+ payload?: PushPayload;
99
+ action?: string;
100
+ }
101
+ /** The browser rotated the push subscription; the page should `sync()`. */ | {
102
+ type: 'sp00ky:subscriptionchange';
103
+ endpoint?: string;
104
+ };
105
+ /** `urlBase64ToUint8Array`: the VAPID key as `applicationServerKey` wants it. */
106
+ declare function base64UrlToBytes(value: string): Uint8Array;
107
+ declare function bytesToBase64Url(bytes: ArrayBuffer | Uint8Array): string;
108
+ declare function sameBytes(a: ArrayBuffer | Uint8Array | null | undefined, b: ArrayBuffer | Uint8Array | null | undefined): boolean;
109
+ /** A payload this build can act on (`v` known, `kind` set). */
110
+ declare function isPushPayload(value: unknown): value is PushPayload;
111
+ //#endregion
112
+ export { PushNotificationAction as a, PushPayloadKind as c, base64UrlToBytes as d, bytesToBase64Url as f, PushNotification as i, PushUrgency as l, sameBytes as m, BridgeTokenMessage as n, PushOp as o, isPushPayload as p, PUSH_PAYLOAD_VERSION as r, PushPayload as s, BridgeSignOutMessage as t, WebPushClientMessage as u };