@noy-db/hub 0.1.0-pre.3

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -0
  3. package/dist/aggregate/index.cjs +476 -0
  4. package/dist/aggregate/index.cjs.map +1 -0
  5. package/dist/aggregate/index.d.cts +38 -0
  6. package/dist/aggregate/index.d.ts +38 -0
  7. package/dist/aggregate/index.js +53 -0
  8. package/dist/aggregate/index.js.map +1 -0
  9. package/dist/blobs/index.cjs +1480 -0
  10. package/dist/blobs/index.cjs.map +1 -0
  11. package/dist/blobs/index.d.cts +45 -0
  12. package/dist/blobs/index.d.ts +45 -0
  13. package/dist/blobs/index.js +48 -0
  14. package/dist/blobs/index.js.map +1 -0
  15. package/dist/bundle/index.cjs +436 -0
  16. package/dist/bundle/index.cjs.map +1 -0
  17. package/dist/bundle/index.d.cts +7 -0
  18. package/dist/bundle/index.d.ts +7 -0
  19. package/dist/bundle/index.js +40 -0
  20. package/dist/bundle/index.js.map +1 -0
  21. package/dist/chunk-2QR2PQTT.js +217 -0
  22. package/dist/chunk-2QR2PQTT.js.map +1 -0
  23. package/dist/chunk-4OWFYIDQ.js +79 -0
  24. package/dist/chunk-4OWFYIDQ.js.map +1 -0
  25. package/dist/chunk-5AATM2M2.js +90 -0
  26. package/dist/chunk-5AATM2M2.js.map +1 -0
  27. package/dist/chunk-ACLDOTNQ.js +543 -0
  28. package/dist/chunk-ACLDOTNQ.js.map +1 -0
  29. package/dist/chunk-BTDCBVJW.js +160 -0
  30. package/dist/chunk-BTDCBVJW.js.map +1 -0
  31. package/dist/chunk-CIMZBAZB.js +72 -0
  32. package/dist/chunk-CIMZBAZB.js.map +1 -0
  33. package/dist/chunk-E445ICYI.js +365 -0
  34. package/dist/chunk-E445ICYI.js.map +1 -0
  35. package/dist/chunk-EXQRC2L4.js +722 -0
  36. package/dist/chunk-EXQRC2L4.js.map +1 -0
  37. package/dist/chunk-FZU343FL.js +32 -0
  38. package/dist/chunk-FZU343FL.js.map +1 -0
  39. package/dist/chunk-GJILMRPO.js +354 -0
  40. package/dist/chunk-GJILMRPO.js.map +1 -0
  41. package/dist/chunk-GOUT6DND.js +1285 -0
  42. package/dist/chunk-GOUT6DND.js.map +1 -0
  43. package/dist/chunk-J66GRPNH.js +111 -0
  44. package/dist/chunk-J66GRPNH.js.map +1 -0
  45. package/dist/chunk-M2F2JAWB.js +464 -0
  46. package/dist/chunk-M2F2JAWB.js.map +1 -0
  47. package/dist/chunk-M5INGEFC.js +84 -0
  48. package/dist/chunk-M5INGEFC.js.map +1 -0
  49. package/dist/chunk-M62XNWRA.js +72 -0
  50. package/dist/chunk-M62XNWRA.js.map +1 -0
  51. package/dist/chunk-MR4424N3.js +275 -0
  52. package/dist/chunk-MR4424N3.js.map +1 -0
  53. package/dist/chunk-NPC4LFV5.js +132 -0
  54. package/dist/chunk-NPC4LFV5.js.map +1 -0
  55. package/dist/chunk-NXFEYLVG.js +311 -0
  56. package/dist/chunk-NXFEYLVG.js.map +1 -0
  57. package/dist/chunk-R36SIKES.js +79 -0
  58. package/dist/chunk-R36SIKES.js.map +1 -0
  59. package/dist/chunk-TDR6T5CJ.js +381 -0
  60. package/dist/chunk-TDR6T5CJ.js.map +1 -0
  61. package/dist/chunk-UF3BUNQZ.js +1 -0
  62. package/dist/chunk-UF3BUNQZ.js.map +1 -0
  63. package/dist/chunk-UQFSPSWG.js +1109 -0
  64. package/dist/chunk-UQFSPSWG.js.map +1 -0
  65. package/dist/chunk-USKYUS74.js +793 -0
  66. package/dist/chunk-USKYUS74.js.map +1 -0
  67. package/dist/chunk-XCL3WP6J.js +121 -0
  68. package/dist/chunk-XCL3WP6J.js.map +1 -0
  69. package/dist/chunk-XHFOENR2.js +680 -0
  70. package/dist/chunk-XHFOENR2.js.map +1 -0
  71. package/dist/chunk-ZFKD4QMV.js +430 -0
  72. package/dist/chunk-ZFKD4QMV.js.map +1 -0
  73. package/dist/chunk-ZLMV3TUA.js +490 -0
  74. package/dist/chunk-ZLMV3TUA.js.map +1 -0
  75. package/dist/chunk-ZRG4V3F5.js +17 -0
  76. package/dist/chunk-ZRG4V3F5.js.map +1 -0
  77. package/dist/consent/index.cjs +204 -0
  78. package/dist/consent/index.cjs.map +1 -0
  79. package/dist/consent/index.d.cts +24 -0
  80. package/dist/consent/index.d.ts +24 -0
  81. package/dist/consent/index.js +23 -0
  82. package/dist/consent/index.js.map +1 -0
  83. package/dist/crdt/index.cjs +152 -0
  84. package/dist/crdt/index.cjs.map +1 -0
  85. package/dist/crdt/index.d.cts +30 -0
  86. package/dist/crdt/index.d.ts +30 -0
  87. package/dist/crdt/index.js +24 -0
  88. package/dist/crdt/index.js.map +1 -0
  89. package/dist/crypto-IVKU7YTT.js +44 -0
  90. package/dist/crypto-IVKU7YTT.js.map +1 -0
  91. package/dist/delegation-XDJCBTI2.js +16 -0
  92. package/dist/delegation-XDJCBTI2.js.map +1 -0
  93. package/dist/dev-unlock-CeXic1xC.d.cts +263 -0
  94. package/dist/dev-unlock-KrKkcqD3.d.ts +263 -0
  95. package/dist/hash-9KO1BGxh.d.cts +63 -0
  96. package/dist/hash-ChfJjRjQ.d.ts +63 -0
  97. package/dist/history/index.cjs +1215 -0
  98. package/dist/history/index.cjs.map +1 -0
  99. package/dist/history/index.d.cts +62 -0
  100. package/dist/history/index.d.ts +62 -0
  101. package/dist/history/index.js +79 -0
  102. package/dist/history/index.js.map +1 -0
  103. package/dist/i18n/index.cjs +746 -0
  104. package/dist/i18n/index.cjs.map +1 -0
  105. package/dist/i18n/index.d.cts +38 -0
  106. package/dist/i18n/index.d.ts +38 -0
  107. package/dist/i18n/index.js +55 -0
  108. package/dist/i18n/index.js.map +1 -0
  109. package/dist/index-BRHBCmLt.d.ts +1940 -0
  110. package/dist/index-C8kQtmOk.d.ts +380 -0
  111. package/dist/index-DN-J-5wT.d.cts +1940 -0
  112. package/dist/index-DhjMjz7L.d.cts +380 -0
  113. package/dist/index.cjs +14756 -0
  114. package/dist/index.cjs.map +1 -0
  115. package/dist/index.d.cts +269 -0
  116. package/dist/index.d.ts +269 -0
  117. package/dist/index.js +6085 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/indexing/index.cjs +736 -0
  120. package/dist/indexing/index.cjs.map +1 -0
  121. package/dist/indexing/index.d.cts +36 -0
  122. package/dist/indexing/index.d.ts +36 -0
  123. package/dist/indexing/index.js +77 -0
  124. package/dist/indexing/index.js.map +1 -0
  125. package/dist/lazy-builder-BwEoBQZ9.d.ts +304 -0
  126. package/dist/lazy-builder-CZVLKh0Z.d.cts +304 -0
  127. package/dist/ledger-2NX4L7PN.js +33 -0
  128. package/dist/ledger-2NX4L7PN.js.map +1 -0
  129. package/dist/mime-magic-CBBSOkjm.d.cts +50 -0
  130. package/dist/mime-magic-CBBSOkjm.d.ts +50 -0
  131. package/dist/periods/index.cjs +1035 -0
  132. package/dist/periods/index.cjs.map +1 -0
  133. package/dist/periods/index.d.cts +21 -0
  134. package/dist/periods/index.d.ts +21 -0
  135. package/dist/periods/index.js +25 -0
  136. package/dist/periods/index.js.map +1 -0
  137. package/dist/predicate-SBHmi6D0.d.cts +161 -0
  138. package/dist/predicate-SBHmi6D0.d.ts +161 -0
  139. package/dist/query/index.cjs +1957 -0
  140. package/dist/query/index.cjs.map +1 -0
  141. package/dist/query/index.d.cts +3 -0
  142. package/dist/query/index.d.ts +3 -0
  143. package/dist/query/index.js +62 -0
  144. package/dist/query/index.js.map +1 -0
  145. package/dist/session/index.cjs +487 -0
  146. package/dist/session/index.cjs.map +1 -0
  147. package/dist/session/index.d.cts +45 -0
  148. package/dist/session/index.d.ts +45 -0
  149. package/dist/session/index.js +44 -0
  150. package/dist/session/index.js.map +1 -0
  151. package/dist/shadow/index.cjs +133 -0
  152. package/dist/shadow/index.cjs.map +1 -0
  153. package/dist/shadow/index.d.cts +16 -0
  154. package/dist/shadow/index.d.ts +16 -0
  155. package/dist/shadow/index.js +20 -0
  156. package/dist/shadow/index.js.map +1 -0
  157. package/dist/store/index.cjs +1069 -0
  158. package/dist/store/index.cjs.map +1 -0
  159. package/dist/store/index.d.cts +491 -0
  160. package/dist/store/index.d.ts +491 -0
  161. package/dist/store/index.js +34 -0
  162. package/dist/store/index.js.map +1 -0
  163. package/dist/strategy-BSxFXGzb.d.cts +110 -0
  164. package/dist/strategy-BSxFXGzb.d.ts +110 -0
  165. package/dist/strategy-D-SrOLCl.d.cts +548 -0
  166. package/dist/strategy-D-SrOLCl.d.ts +548 -0
  167. package/dist/sync/index.cjs +1062 -0
  168. package/dist/sync/index.cjs.map +1 -0
  169. package/dist/sync/index.d.cts +42 -0
  170. package/dist/sync/index.d.ts +42 -0
  171. package/dist/sync/index.js +28 -0
  172. package/dist/sync/index.js.map +1 -0
  173. package/dist/team/index.cjs +1233 -0
  174. package/dist/team/index.cjs.map +1 -0
  175. package/dist/team/index.d.cts +117 -0
  176. package/dist/team/index.d.ts +117 -0
  177. package/dist/team/index.js +39 -0
  178. package/dist/team/index.js.map +1 -0
  179. package/dist/tx/index.cjs +212 -0
  180. package/dist/tx/index.cjs.map +1 -0
  181. package/dist/tx/index.d.cts +20 -0
  182. package/dist/tx/index.d.ts +20 -0
  183. package/dist/tx/index.js +20 -0
  184. package/dist/tx/index.js.map +1 -0
  185. package/dist/types-BZpCZB8N.d.ts +7526 -0
  186. package/dist/types-Bfs0qr5F.d.cts +7526 -0
  187. package/dist/ulid-COREQ2RQ.js +9 -0
  188. package/dist/ulid-COREQ2RQ.js.map +1 -0
  189. package/dist/util/index.cjs +230 -0
  190. package/dist/util/index.cjs.map +1 -0
  191. package/dist/util/index.d.cts +77 -0
  192. package/dist/util/index.d.ts +77 -0
  193. package/dist/util/index.js +190 -0
  194. package/dist/util/index.js.map +1 -0
  195. package/package.json +244 -0
@@ -0,0 +1,1233 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/team/index.ts
21
+ var team_exports = {};
22
+ __export(team_exports, {
23
+ PresenceHandle: () => PresenceHandle,
24
+ SYNC_CREDENTIALS_COLLECTION: () => SYNC_CREDENTIALS_COLLECTION,
25
+ SyncEngine: () => SyncEngine,
26
+ SyncTransaction: () => SyncTransaction,
27
+ credentialStatus: () => credentialStatus,
28
+ deleteCredential: () => deleteCredential,
29
+ evaluateExportCapability: () => evaluateExportCapability,
30
+ evaluateImportCapability: () => evaluateImportCapability,
31
+ getCredential: () => getCredential,
32
+ hasExportCapability: () => hasExportCapability,
33
+ hasImportCapability: () => hasImportCapability,
34
+ listCredentials: () => listCredentials,
35
+ putCredential: () => putCredential
36
+ });
37
+ module.exports = __toCommonJS(team_exports);
38
+
39
+ // src/types.ts
40
+ var NOYDB_FORMAT_VERSION = 1;
41
+ var NOYDB_KEYRING_VERSION = 1;
42
+ var NOYDB_SYNC_VERSION = 1;
43
+
44
+ // src/errors.ts
45
+ var NoydbError = class extends Error {
46
+ /** Machine-readable error code. Stable across library versions. */
47
+ code;
48
+ constructor(code, message) {
49
+ super(message);
50
+ this.name = "NoydbError";
51
+ this.code = code;
52
+ }
53
+ };
54
+ var DecryptionError = class extends NoydbError {
55
+ constructor(message = "Decryption failed") {
56
+ super("DECRYPTION_FAILED", message);
57
+ this.name = "DecryptionError";
58
+ }
59
+ };
60
+ var TamperedError = class extends NoydbError {
61
+ constructor(message = "Data integrity check failed \u2014 record may have been tampered with") {
62
+ super("TAMPERED", message);
63
+ this.name = "TamperedError";
64
+ }
65
+ };
66
+ var PermissionDeniedError = class extends NoydbError {
67
+ constructor(message = "Permission denied \u2014 insufficient role for this operation") {
68
+ super("PERMISSION_DENIED", message);
69
+ this.name = "PermissionDeniedError";
70
+ }
71
+ };
72
+ var ConflictError = class extends NoydbError {
73
+ /** The actual stored version at the time of conflict. */
74
+ version;
75
+ constructor(version, message = "Version conflict") {
76
+ super("CONFLICT", message);
77
+ this.name = "ConflictError";
78
+ this.version = version;
79
+ }
80
+ };
81
+
82
+ // src/crypto.ts
83
+ var IV_BYTES = 12;
84
+ var KEY_BITS = 256;
85
+ var subtle = globalThis.crypto.subtle;
86
+ async function generateDEK() {
87
+ return subtle.generateKey(
88
+ { name: "AES-GCM", length: KEY_BITS },
89
+ true,
90
+ // extractable — needed for AES-KW wrapping
91
+ ["encrypt", "decrypt"]
92
+ );
93
+ }
94
+ async function wrapKey(dek, kek) {
95
+ const wrapped = await subtle.wrapKey("raw", dek, kek, "AES-KW");
96
+ return bufferToBase64(wrapped);
97
+ }
98
+ async function encrypt(plaintext, dek) {
99
+ const iv = generateIV();
100
+ const encoded = new TextEncoder().encode(plaintext);
101
+ const ciphertext = await subtle.encrypt(
102
+ { name: "AES-GCM", iv },
103
+ dek,
104
+ encoded
105
+ );
106
+ return {
107
+ iv: bufferToBase64(iv),
108
+ data: bufferToBase64(ciphertext)
109
+ };
110
+ }
111
+ async function decrypt(ivBase64, dataBase64, dek) {
112
+ const iv = base64ToBuffer(ivBase64);
113
+ const ciphertext = base64ToBuffer(dataBase64);
114
+ try {
115
+ const plaintext = await subtle.decrypt(
116
+ { name: "AES-GCM", iv },
117
+ dek,
118
+ ciphertext
119
+ );
120
+ return new TextDecoder().decode(plaintext);
121
+ } catch (err) {
122
+ if (err instanceof Error && err.name === "OperationError") {
123
+ throw new TamperedError();
124
+ }
125
+ throw new DecryptionError(
126
+ err instanceof Error ? err.message : "Decryption failed"
127
+ );
128
+ }
129
+ }
130
+ async function derivePresenceKey(dek, collectionName) {
131
+ const rawDek = await subtle.exportKey("raw", dek);
132
+ const hkdfKey = await subtle.importKey(
133
+ "raw",
134
+ rawDek,
135
+ "HKDF",
136
+ false,
137
+ ["deriveBits"]
138
+ );
139
+ const salt = new TextEncoder().encode("noydb-presence");
140
+ const info = new TextEncoder().encode(collectionName);
141
+ const bits = await subtle.deriveBits(
142
+ { name: "HKDF", hash: "SHA-256", salt, info },
143
+ hkdfKey,
144
+ KEY_BITS
145
+ );
146
+ return subtle.importKey(
147
+ "raw",
148
+ bits,
149
+ { name: "AES-GCM", length: KEY_BITS },
150
+ false,
151
+ ["encrypt", "decrypt"]
152
+ );
153
+ }
154
+ function generateIV() {
155
+ return globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
156
+ }
157
+ function bufferToBase64(buffer) {
158
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
159
+ let binary = "";
160
+ for (let i = 0; i < bytes.length; i++) {
161
+ binary += String.fromCharCode(bytes[i]);
162
+ }
163
+ return btoa(binary);
164
+ }
165
+ function base64ToBuffer(base64) {
166
+ const binary = atob(base64);
167
+ const bytes = new Uint8Array(binary.length);
168
+ for (let i = 0; i < binary.length; i++) {
169
+ bytes[i] = binary.charCodeAt(i);
170
+ }
171
+ return bytes;
172
+ }
173
+
174
+ // src/team/keyring.ts
175
+ async function ensureCollectionDEK(adapter, vault, keyring) {
176
+ const inFlight = /* @__PURE__ */ new Map();
177
+ return async (collectionName) => {
178
+ const existing = keyring.deks.get(collectionName);
179
+ if (existing) return existing;
180
+ const pending = inFlight.get(collectionName);
181
+ if (pending) return pending;
182
+ const promise = (async () => {
183
+ const dek = await generateDEK();
184
+ keyring.deks.set(collectionName, dek);
185
+ await persistKeyring(adapter, vault, keyring);
186
+ return dek;
187
+ })();
188
+ inFlight.set(collectionName, promise);
189
+ try {
190
+ return await promise;
191
+ } finally {
192
+ inFlight.delete(collectionName);
193
+ }
194
+ };
195
+ }
196
+ async function persistKeyring(adapter, vault, keyring) {
197
+ const wrappedDeks = {};
198
+ for (const [collName, dek] of keyring.deks) {
199
+ wrappedDeks[collName] = await wrapKey(dek, keyring.kek);
200
+ }
201
+ const keyringFile = {
202
+ _noydb_keyring: NOYDB_KEYRING_VERSION,
203
+ user_id: keyring.userId,
204
+ display_name: keyring.displayName,
205
+ role: keyring.role,
206
+ permissions: keyring.permissions,
207
+ deks: wrappedDeks,
208
+ salt: bufferToBase64(keyring.salt),
209
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
210
+ granted_by: keyring.userId,
211
+ ...keyring.exportCapability !== void 0 && { export_capability: keyring.exportCapability },
212
+ ...keyring.importCapability !== void 0 && { import_capability: keyring.importCapability }
213
+ };
214
+ await writeKeyringFile(adapter, vault, keyring.userId, keyringFile);
215
+ }
216
+ function defaultBundleCapability(role) {
217
+ return role === "owner" || role === "admin";
218
+ }
219
+ function hasExportCapability(keyring, tier, format) {
220
+ const cap = keyring.exportCapability;
221
+ if (tier === "plaintext") {
222
+ const allowed = cap?.plaintext ?? [];
223
+ return allowed.includes("*") || format !== void 0 && allowed.includes(format);
224
+ }
225
+ return cap?.bundle ?? defaultBundleCapability(keyring.role);
226
+ }
227
+ function evaluateExportCapability(capability, role, tier, format) {
228
+ if (tier === "plaintext") {
229
+ const allowed = capability?.plaintext ?? [];
230
+ return allowed.includes("*") || format !== void 0 && allowed.includes(format);
231
+ }
232
+ return capability?.bundle ?? defaultBundleCapability(role);
233
+ }
234
+ function hasImportCapability(keyring, tier, format) {
235
+ const cap = keyring.importCapability;
236
+ if (tier === "plaintext") {
237
+ const allowed = cap?.plaintext ?? [];
238
+ return allowed.includes("*") || format !== void 0 && allowed.includes(format);
239
+ }
240
+ return cap?.bundle === true;
241
+ }
242
+ function evaluateImportCapability(capability, _role, tier, format) {
243
+ if (tier === "plaintext") {
244
+ const allowed = capability?.plaintext ?? [];
245
+ return allowed.includes("*") || format !== void 0 && allowed.includes(format);
246
+ }
247
+ return capability?.bundle === true;
248
+ }
249
+ async function writeKeyringFile(adapter, vault, userId, keyringFile) {
250
+ const envelope = {
251
+ _noydb: 1,
252
+ _v: 1,
253
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
254
+ _iv: "",
255
+ _data: JSON.stringify(keyringFile)
256
+ };
257
+ await adapter.put(vault, "_keyring", userId, envelope);
258
+ }
259
+
260
+ // src/store/sync-policy.ts
261
+ var SyncScheduler = class {
262
+ policy;
263
+ callbacks;
264
+ _state = "idle";
265
+ _lastPushAt = null;
266
+ _lastPullAt = null;
267
+ _lastError = null;
268
+ _lastPushTime = 0;
269
+ // monotonic ms for minIntervalMs enforcement
270
+ // Timers
271
+ debounceTimer = null;
272
+ pushIntervalTimer = null;
273
+ pullIntervalTimer = null;
274
+ // Bound handlers for cleanup
275
+ boundOnVisibilityChange = null;
276
+ boundOnBeforeExit = null;
277
+ boundOnPageHide = null;
278
+ started = false;
279
+ constructor(policy, callbacks) {
280
+ this.policy = policy;
281
+ this.callbacks = callbacks;
282
+ if (this.shouldRegisterUnload()) {
283
+ this.boundOnVisibilityChange = this.handleVisibilityChange.bind(this);
284
+ this.boundOnPageHide = this.handlePageHide.bind(this);
285
+ this.boundOnBeforeExit = this.handleBeforeExit.bind(this);
286
+ }
287
+ }
288
+ /** Current scheduler status snapshot. */
289
+ get status() {
290
+ return {
291
+ state: this._state,
292
+ lastPushAt: this._lastPushAt,
293
+ lastPullAt: this._lastPullAt,
294
+ lastError: this._lastError,
295
+ pendingWrites: this.callbacks.getDirtyCount()
296
+ };
297
+ }
298
+ /** Start the scheduler — registers timers, event listeners. */
299
+ start() {
300
+ if (this.started) return;
301
+ this.started = true;
302
+ if (this.policy.push.mode === "interval" && this.policy.push.intervalMs) {
303
+ this.pushIntervalTimer = setInterval(() => {
304
+ void this.executePush();
305
+ }, this.policy.push.intervalMs);
306
+ }
307
+ if (this.policy.pull.mode === "interval" && this.policy.pull.intervalMs) {
308
+ this.pullIntervalTimer = setInterval(() => {
309
+ void this.executePull();
310
+ }, this.policy.pull.intervalMs);
311
+ }
312
+ if (this.policy.pull.mode === "on-focus" && typeof document !== "undefined") {
313
+ document.addEventListener("visibilitychange", this.handleFocusPull);
314
+ }
315
+ if (this.shouldRegisterUnload()) {
316
+ if (typeof document !== "undefined" && this.boundOnVisibilityChange) {
317
+ document.addEventListener("visibilitychange", this.boundOnVisibilityChange);
318
+ }
319
+ if (typeof globalThis.addEventListener === "function" && this.boundOnPageHide) {
320
+ globalThis.addEventListener("pagehide", this.boundOnPageHide);
321
+ }
322
+ if (typeof process !== "undefined" && this.boundOnBeforeExit) {
323
+ process.on("beforeExit", this.boundOnBeforeExit);
324
+ }
325
+ }
326
+ }
327
+ /** Stop the scheduler — clears timers, removes event listeners. */
328
+ stop() {
329
+ if (!this.started) return;
330
+ this.started = false;
331
+ if (this.debounceTimer) {
332
+ clearTimeout(this.debounceTimer);
333
+ this.debounceTimer = null;
334
+ }
335
+ if (this.pushIntervalTimer) {
336
+ clearInterval(this.pushIntervalTimer);
337
+ this.pushIntervalTimer = null;
338
+ }
339
+ if (this.pullIntervalTimer) {
340
+ clearInterval(this.pullIntervalTimer);
341
+ this.pullIntervalTimer = null;
342
+ }
343
+ if (this.policy.pull.mode === "on-focus" && typeof document !== "undefined") {
344
+ document.removeEventListener("visibilitychange", this.handleFocusPull);
345
+ }
346
+ if (typeof document !== "undefined" && this.boundOnVisibilityChange) {
347
+ document.removeEventListener("visibilitychange", this.boundOnVisibilityChange);
348
+ }
349
+ if (typeof globalThis.removeEventListener === "function" && this.boundOnPageHide) {
350
+ globalThis.removeEventListener("pagehide", this.boundOnPageHide);
351
+ }
352
+ if (typeof process !== "undefined" && this.boundOnBeforeExit) {
353
+ process.removeListener("beforeExit", this.boundOnBeforeExit);
354
+ }
355
+ }
356
+ /**
357
+ * Notify the scheduler that a local write occurred.
358
+ * For `on-change` mode: triggers immediate push (respecting minIntervalMs).
359
+ * For `debounce` mode: resets the debounce timer.
360
+ * For `manual` / `interval`: no-op.
361
+ */
362
+ notifyChange() {
363
+ if (!this.started) return;
364
+ if (this.policy.push.mode === "on-change") {
365
+ void this.executePush();
366
+ } else if (this.policy.push.mode === "debounce") {
367
+ this.resetDebounce();
368
+ }
369
+ }
370
+ /** Force an immediate push, bypassing the scheduler. */
371
+ async forcePush() {
372
+ await this.executePush();
373
+ }
374
+ /** Force an immediate pull, bypassing the scheduler. */
375
+ async forcePull() {
376
+ await this.executePull();
377
+ }
378
+ // ─── Internal ─────────────────────────────────────────────────────
379
+ async executePush() {
380
+ if (this._state === "pushing") return;
381
+ const minInterval = this.policy.push.minIntervalMs ?? 0;
382
+ if (minInterval > 0) {
383
+ const elapsed = Date.now() - this._lastPushTime;
384
+ if (elapsed < minInterval) {
385
+ if (this.policy.push.mode === "debounce") {
386
+ this.scheduleDebounce(minInterval - elapsed);
387
+ }
388
+ return;
389
+ }
390
+ }
391
+ if (this.callbacks.getDirtyCount() === 0) {
392
+ this._state = "idle";
393
+ return;
394
+ }
395
+ this._state = "pushing";
396
+ try {
397
+ await this.callbacks.push();
398
+ this._lastPushAt = (/* @__PURE__ */ new Date()).toISOString();
399
+ this._lastPushTime = Date.now();
400
+ this._lastError = null;
401
+ this._state = this.callbacks.getDirtyCount() > 0 ? "pending" : "idle";
402
+ } catch (err) {
403
+ this._lastError = err instanceof Error ? err : new Error(String(err));
404
+ this._state = "error";
405
+ }
406
+ }
407
+ async executePull() {
408
+ if (this._state === "pulling") return;
409
+ const previousState = this._state;
410
+ this._state = "pulling";
411
+ try {
412
+ await this.callbacks.pull();
413
+ this._lastPullAt = (/* @__PURE__ */ new Date()).toISOString();
414
+ this._lastError = null;
415
+ this._state = previousState === "pending" ? "pending" : "idle";
416
+ } catch (err) {
417
+ this._lastError = err instanceof Error ? err : new Error(String(err));
418
+ this._state = "error";
419
+ }
420
+ }
421
+ resetDebounce() {
422
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
423
+ const ms = this.policy.push.debounceMs ?? 3e4;
424
+ this._state = "pending";
425
+ this.scheduleDebounce(ms);
426
+ }
427
+ scheduleDebounce(ms) {
428
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
429
+ this.debounceTimer = setTimeout(() => {
430
+ this.debounceTimer = null;
431
+ void this.executePush();
432
+ }, ms);
433
+ }
434
+ shouldRegisterUnload() {
435
+ const onUnload = this.policy.push.onUnload;
436
+ if (onUnload !== void 0) return onUnload;
437
+ return this.policy.push.mode !== "manual";
438
+ }
439
+ // ─── Event handlers ───────────────────────────────────────────────
440
+ handleVisibilityChange() {
441
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
442
+ this.fireUnloadPush();
443
+ }
444
+ }
445
+ handlePageHide() {
446
+ this.fireUnloadPush();
447
+ }
448
+ handleBeforeExit() {
449
+ this.fireUnloadPush();
450
+ }
451
+ handleFocusPull = () => {
452
+ if (typeof document !== "undefined" && document.visibilityState === "visible") {
453
+ void this.executePull();
454
+ }
455
+ };
456
+ fireUnloadPush() {
457
+ if (this.callbacks.getDirtyCount() === 0) return;
458
+ void this.callbacks.push().catch(() => {
459
+ });
460
+ }
461
+ };
462
+
463
+ // src/team/sync.ts
464
+ var SyncEngine = class {
465
+ local;
466
+ remote;
467
+ strategy;
468
+ emitter;
469
+ vault;
470
+ role;
471
+ label;
472
+ dirty = [];
473
+ lastPush = null;
474
+ lastPull = null;
475
+ loaded = false;
476
+ autoSyncInterval = null;
477
+ isOnline = true;
478
+ /** Sync scheduler. Manages push/pull timing. */
479
+ scheduler;
480
+ /** Per-collection conflict resolvers registered by Collection instances. */
481
+ conflictResolvers = /* @__PURE__ */ new Map();
482
+ constructor(opts) {
483
+ this.local = opts.local;
484
+ this.remote = opts.remote;
485
+ this.vault = opts.vault;
486
+ this.strategy = opts.strategy;
487
+ this.emitter = opts.emitter;
488
+ this.role = opts.role ?? "sync-peer";
489
+ this.label = opts.label;
490
+ const policy = opts.syncPolicy;
491
+ if (policy && policy.push.mode !== "manual") {
492
+ this.scheduler = new SyncScheduler(policy, {
493
+ push: () => this.push().then(() => {
494
+ }),
495
+ pull: () => this.pull().then(() => {
496
+ }),
497
+ getDirtyCount: () => this.dirty.length
498
+ });
499
+ } else {
500
+ this.scheduler = null;
501
+ }
502
+ }
503
+ /** Start the sync scheduler. Called after vault is fully opened. */
504
+ startScheduler() {
505
+ this.scheduler?.start();
506
+ }
507
+ /** Stop the sync scheduler. Called on close. */
508
+ stopScheduler() {
509
+ this.scheduler?.stop();
510
+ }
511
+ /**
512
+ * Register a per-collection conflict resolver.
513
+ * Called by Collection when `conflictPolicy` is set.
514
+ */
515
+ registerConflictResolver(collection, resolver) {
516
+ this.conflictResolvers.set(collection, resolver);
517
+ }
518
+ /** Record a local change for later push. */
519
+ async trackChange(collection, id, action, version) {
520
+ await this.ensureLoaded();
521
+ const idx = this.dirty.findIndex((d) => d.collection === collection && d.id === id);
522
+ const entry = {
523
+ vault: this.vault,
524
+ collection,
525
+ id,
526
+ action,
527
+ version,
528
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
529
+ };
530
+ if (idx >= 0) {
531
+ this.dirty[idx] = entry;
532
+ } else {
533
+ this.dirty.push(entry);
534
+ }
535
+ await this.persistMeta();
536
+ this.scheduler?.notifyChange();
537
+ }
538
+ /** Push dirty records to remote adapter. Accepts optional `PushOptions` for partial sync. */
539
+ async push(options) {
540
+ await this.ensureLoaded();
541
+ let pushed = 0;
542
+ const conflicts = [];
543
+ const errors = [];
544
+ const completed = [];
545
+ for (let i = 0; i < this.dirty.length; i++) {
546
+ const entry = this.dirty[i];
547
+ if (options?.collections && !options.collections.includes(entry.collection)) {
548
+ continue;
549
+ }
550
+ try {
551
+ if (entry.action === "delete") {
552
+ await this.remote.delete(this.vault, entry.collection, entry.id);
553
+ completed.push(i);
554
+ pushed++;
555
+ } else {
556
+ const envelope = await this.local.get(this.vault, entry.collection, entry.id);
557
+ if (!envelope) {
558
+ completed.push(i);
559
+ continue;
560
+ }
561
+ try {
562
+ await this.remote.put(
563
+ this.vault,
564
+ entry.collection,
565
+ entry.id,
566
+ envelope,
567
+ entry.version - 1
568
+ );
569
+ completed.push(i);
570
+ pushed++;
571
+ } catch (err) {
572
+ if (err instanceof ConflictError) {
573
+ const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id);
574
+ if (remoteEnvelope) {
575
+ const { handled, conflict } = await this.handleConflict(
576
+ entry.collection,
577
+ entry.id,
578
+ envelope,
579
+ remoteEnvelope,
580
+ "push"
581
+ );
582
+ conflicts.push(conflict);
583
+ if (handled === "local") {
584
+ await this.remote.put(this.vault, entry.collection, entry.id, conflict.local);
585
+ completed.push(i);
586
+ pushed++;
587
+ } else if (handled === "remote") {
588
+ await this.local.put(this.vault, entry.collection, entry.id, conflict.remote);
589
+ completed.push(i);
590
+ } else if (handled === "merged" && conflict.local !== envelope) {
591
+ const merged = conflict.local;
592
+ await this.remote.put(this.vault, entry.collection, entry.id, merged);
593
+ await this.local.put(this.vault, entry.collection, entry.id, merged);
594
+ completed.push(i);
595
+ pushed++;
596
+ }
597
+ }
598
+ } else {
599
+ throw err;
600
+ }
601
+ }
602
+ }
603
+ } catch (err) {
604
+ errors.push(err instanceof Error ? err : new Error(String(err)));
605
+ }
606
+ }
607
+ for (const i of completed.sort((a, b) => b - a)) {
608
+ this.dirty.splice(i, 1);
609
+ }
610
+ this.lastPush = (/* @__PURE__ */ new Date()).toISOString();
611
+ await this.persistMeta();
612
+ const result = { pushed, conflicts, errors };
613
+ this.emitter.emit("sync:push", result);
614
+ return result;
615
+ }
616
+ /** Pull remote records to local adapter. Accepts optional `PullOptions` for partial sync. */
617
+ async pull(options) {
618
+ await this.ensureLoaded();
619
+ let pulled = 0;
620
+ const conflicts = [];
621
+ const errors = [];
622
+ try {
623
+ const remoteSnapshot = await this.remote.loadAll(this.vault);
624
+ for (const [collName, records] of Object.entries(remoteSnapshot)) {
625
+ if (options?.collections && !options.collections.includes(collName)) {
626
+ continue;
627
+ }
628
+ for (const [id, remoteEnvelope] of Object.entries(records)) {
629
+ if (options?.modifiedSince && remoteEnvelope._ts <= options.modifiedSince) {
630
+ continue;
631
+ }
632
+ try {
633
+ const localEnvelope = await this.local.get(this.vault, collName, id);
634
+ if (!localEnvelope) {
635
+ await this.local.put(this.vault, collName, id, remoteEnvelope);
636
+ pulled++;
637
+ } else if (remoteEnvelope._v > localEnvelope._v) {
638
+ const isDirty = this.dirty.some((d) => d.collection === collName && d.id === id);
639
+ if (isDirty) {
640
+ const { handled, conflict } = await this.handleConflict(
641
+ collName,
642
+ id,
643
+ localEnvelope,
644
+ remoteEnvelope,
645
+ "pull"
646
+ );
647
+ conflicts.push(conflict);
648
+ if (handled === "remote") {
649
+ await this.local.put(this.vault, collName, id, conflict.remote);
650
+ this.dirty = this.dirty.filter((d) => !(d.collection === collName && d.id === id));
651
+ pulled++;
652
+ } else if (handled === "merged" && conflict.local !== localEnvelope) {
653
+ const merged = conflict.local;
654
+ await this.local.put(this.vault, collName, id, merged);
655
+ this.dirty = this.dirty.filter((d) => !(d.collection === collName && d.id === id));
656
+ pulled++;
657
+ }
658
+ } else {
659
+ await this.local.put(this.vault, collName, id, remoteEnvelope);
660
+ pulled++;
661
+ }
662
+ }
663
+ } catch (err) {
664
+ errors.push(err instanceof Error ? err : new Error(String(err)));
665
+ }
666
+ }
667
+ }
668
+ } catch (err) {
669
+ errors.push(err instanceof Error ? err : new Error(String(err)));
670
+ }
671
+ this.lastPull = (/* @__PURE__ */ new Date()).toISOString();
672
+ await this.persistMeta();
673
+ const result = { pulled, conflicts, errors };
674
+ this.emitter.emit("sync:pull", result);
675
+ return result;
676
+ }
677
+ /** Bidirectional sync: pull then push. */
678
+ async sync(options) {
679
+ const pullResult = await this.pull(options?.pull);
680
+ const pushResult = await this.push(options?.push);
681
+ return { pull: pullResult, push: pushResult };
682
+ }
683
+ /**
684
+ * Push a specific subset of dirty entries (for sync transactions, ).
685
+ * Entries are matched by collection+id from the dirty log; matched entries
686
+ * are removed from the dirty log on success.
687
+ */
688
+ async pushFiltered(predicate) {
689
+ await this.ensureLoaded();
690
+ let pushed = 0;
691
+ const conflicts = [];
692
+ const errors = [];
693
+ const completed = [];
694
+ for (let i = 0; i < this.dirty.length; i++) {
695
+ const entry = this.dirty[i];
696
+ if (!predicate(entry)) continue;
697
+ try {
698
+ if (entry.action === "delete") {
699
+ await this.remote.delete(this.vault, entry.collection, entry.id);
700
+ completed.push(i);
701
+ pushed++;
702
+ } else {
703
+ const envelope = await this.local.get(this.vault, entry.collection, entry.id);
704
+ if (!envelope) {
705
+ completed.push(i);
706
+ continue;
707
+ }
708
+ try {
709
+ await this.remote.put(
710
+ this.vault,
711
+ entry.collection,
712
+ entry.id,
713
+ envelope,
714
+ entry.version - 1
715
+ );
716
+ completed.push(i);
717
+ pushed++;
718
+ } catch (err) {
719
+ if (err instanceof ConflictError) {
720
+ const remoteEnvelope = await this.remote.get(this.vault, entry.collection, entry.id);
721
+ if (remoteEnvelope) {
722
+ const { handled, conflict } = await this.handleConflict(
723
+ entry.collection,
724
+ entry.id,
725
+ envelope,
726
+ remoteEnvelope,
727
+ "push"
728
+ );
729
+ conflicts.push(conflict);
730
+ if (handled === "local") {
731
+ await this.remote.put(this.vault, entry.collection, entry.id, conflict.local);
732
+ completed.push(i);
733
+ pushed++;
734
+ } else if (handled === "remote") {
735
+ await this.local.put(this.vault, entry.collection, entry.id, conflict.remote);
736
+ completed.push(i);
737
+ } else if (handled === "merged" && conflict.local !== envelope) {
738
+ const merged = conflict.local;
739
+ await this.remote.put(this.vault, entry.collection, entry.id, merged);
740
+ await this.local.put(this.vault, entry.collection, entry.id, merged);
741
+ completed.push(i);
742
+ pushed++;
743
+ }
744
+ }
745
+ } else {
746
+ throw err;
747
+ }
748
+ }
749
+ }
750
+ } catch (err) {
751
+ errors.push(err instanceof Error ? err : new Error(String(err)));
752
+ }
753
+ }
754
+ for (const i of completed.sort((a, b) => b - a)) {
755
+ this.dirty.splice(i, 1);
756
+ }
757
+ this.lastPush = (/* @__PURE__ */ new Date()).toISOString();
758
+ await this.persistMeta();
759
+ const result = { pushed, conflicts, errors };
760
+ this.emitter.emit("sync:push", result);
761
+ return result;
762
+ }
763
+ /** Get current sync status. */
764
+ status() {
765
+ return {
766
+ dirty: this.dirty.length,
767
+ lastPush: this.lastPush,
768
+ lastPull: this.lastPull,
769
+ online: this.isOnline
770
+ };
771
+ }
772
+ // ─── Auto-Sync ───────────────────────────────────────────────────
773
+ /** Start auto-sync: listen for online/offline events, optional periodic sync. */
774
+ startAutoSync(intervalMs) {
775
+ if (typeof globalThis.addEventListener === "function") {
776
+ globalThis.addEventListener("online", this.handleOnline);
777
+ globalThis.addEventListener("offline", this.handleOffline);
778
+ }
779
+ if (intervalMs && intervalMs > 0) {
780
+ this.autoSyncInterval = setInterval(() => {
781
+ if (this.isOnline) {
782
+ void this.sync();
783
+ }
784
+ }, intervalMs);
785
+ }
786
+ }
787
+ /** Stop auto-sync and scheduler. */
788
+ stopAutoSync() {
789
+ this.stopScheduler();
790
+ if (typeof globalThis.removeEventListener === "function") {
791
+ globalThis.removeEventListener("online", this.handleOnline);
792
+ globalThis.removeEventListener("offline", this.handleOffline);
793
+ }
794
+ if (this.autoSyncInterval) {
795
+ clearInterval(this.autoSyncInterval);
796
+ this.autoSyncInterval = null;
797
+ }
798
+ }
799
+ handleOnline = () => {
800
+ this.isOnline = true;
801
+ this.emitter.emit("sync:online", void 0);
802
+ void this.sync();
803
+ };
804
+ handleOffline = () => {
805
+ this.isOnline = false;
806
+ this.emitter.emit("sync:offline", void 0);
807
+ };
808
+ /**
809
+ * Resolve a conflict, checking per-collection resolvers first,
810
+ * then falling back to the db-level `ConflictStrategy`.
811
+ *
812
+ * Returns the resolved `Conflict` object (possibly with `resolve` set for
813
+ * manual mode) and a `handled` discriminant:
814
+ * - `'local'` — keep the local envelope; push it to remote.
815
+ * - `'remote'` — keep the remote envelope; update local.
816
+ * - `'merged'` — a custom merge fn produced a new envelope stored as `conflict.local`.
817
+ * - `'deferred'` — manual mode, resolve was not called synchronously.
818
+ */
819
+ async handleConflict(collection, id, local, remote, _phase) {
820
+ const resolver = this.conflictResolvers.get(collection);
821
+ if (resolver) {
822
+ const winner = await resolver(id, local, remote);
823
+ const base = {
824
+ vault: this.vault,
825
+ collection,
826
+ id,
827
+ local,
828
+ remote,
829
+ localVersion: local._v,
830
+ remoteVersion: remote._v
831
+ };
832
+ if (winner === null) return { handled: "deferred", conflict: base };
833
+ if (winner === local) return { handled: "local", conflict: base };
834
+ if (winner === remote) return { handled: "remote", conflict: base };
835
+ return {
836
+ handled: "merged",
837
+ conflict: { ...base, local: winner, localVersion: winner._v }
838
+ };
839
+ }
840
+ const baseConflict = {
841
+ vault: this.vault,
842
+ collection,
843
+ id,
844
+ local,
845
+ remote,
846
+ localVersion: local._v,
847
+ remoteVersion: remote._v
848
+ };
849
+ this.emitter.emit("sync:conflict", baseConflict);
850
+ const side = this.legacyResolve(baseConflict);
851
+ return { handled: side, conflict: baseConflict };
852
+ }
853
+ /** DB-level ConflictStrategy resolution (legacy, kept for backward compat). */
854
+ legacyResolve(conflict) {
855
+ if (typeof this.strategy === "function") {
856
+ return this.strategy(conflict);
857
+ }
858
+ switch (this.strategy) {
859
+ case "local-wins":
860
+ return "local";
861
+ case "remote-wins":
862
+ return "remote";
863
+ case "version":
864
+ default:
865
+ return conflict.localVersion >= conflict.remoteVersion ? "local" : "remote";
866
+ }
867
+ }
868
+ // ─── Persistence ─────────────────────────────────────────────────
869
+ async ensureLoaded() {
870
+ if (this.loaded) return;
871
+ const envelope = await this.local.get(this.vault, "_sync", "meta");
872
+ if (envelope) {
873
+ const meta = JSON.parse(envelope._data);
874
+ this.dirty = [...meta.dirty];
875
+ this.lastPush = meta.last_push;
876
+ this.lastPull = meta.last_pull;
877
+ }
878
+ this.loaded = true;
879
+ }
880
+ async persistMeta() {
881
+ const meta = {
882
+ _noydb_sync: NOYDB_SYNC_VERSION,
883
+ last_push: this.lastPush,
884
+ last_pull: this.lastPull,
885
+ dirty: this.dirty
886
+ };
887
+ const envelope = {
888
+ _noydb: 1,
889
+ _v: 1,
890
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
891
+ _iv: "",
892
+ _data: JSON.stringify(meta)
893
+ };
894
+ await this.local.put(this.vault, "_sync", "meta", envelope);
895
+ }
896
+ };
897
+
898
+ // src/team/sync-transaction.ts
899
+ var SyncTransaction = class {
900
+ comp;
901
+ engine;
902
+ ops = [];
903
+ /** @internal — constructed by `Noydb.transaction()` */
904
+ constructor(comp, engine) {
905
+ this.comp = comp;
906
+ this.engine = engine;
907
+ }
908
+ /** Stage a record write. Does not write to any adapter until `commit()`. */
909
+ put(collection, id, record) {
910
+ this.ops.push({ type: "put", collection, id, record });
911
+ return this;
912
+ }
913
+ /** Stage a record delete. Does not write to any adapter until `commit()`. */
914
+ delete(collection, id) {
915
+ this.ops.push({ type: "delete", collection, id });
916
+ return this;
917
+ }
918
+ /**
919
+ * Commit the transaction.
920
+ *
921
+ * Phase 1 — writes all staged operations to the local adapter via the
922
+ * collection layer (encryption + dirty-log tracking).
923
+ *
924
+ * Phase 2 — pushes only the records that were written in this
925
+ * transaction to the remote adapter. Existing dirty entries from
926
+ * outside this transaction are not affected.
927
+ *
928
+ * If any record conflicts during the push, `status` is `'conflict'`
929
+ * and `conflicts` lists the affected records. No automatic rollback is
930
+ * performed.
931
+ */
932
+ async commit() {
933
+ for (const op of this.ops) {
934
+ if (op.type === "put") {
935
+ await this.comp.collection(op.collection).put(op.id, op.record);
936
+ } else {
937
+ await this.comp.collection(op.collection).delete(op.id);
938
+ }
939
+ }
940
+ const opSet = /* @__PURE__ */ new Set();
941
+ for (const op of this.ops) {
942
+ opSet.add(`${op.collection}::${op.id}`);
943
+ }
944
+ const pushResult = await this.engine.pushFiltered(
945
+ (entry) => opSet.has(`${entry.collection}::${entry.id}`)
946
+ );
947
+ return {
948
+ status: pushResult.conflicts.length > 0 ? "conflict" : "committed",
949
+ pushed: pushResult.pushed,
950
+ conflicts: pushResult.conflicts
951
+ };
952
+ }
953
+ };
954
+
955
+ // src/team/presence.ts
956
+ var PresenceHandle = class {
957
+ adapter;
958
+ syncAdapter;
959
+ vault;
960
+ collectionName;
961
+ userId;
962
+ encrypted;
963
+ getDEK;
964
+ staleMs;
965
+ pollIntervalMs;
966
+ channel;
967
+ storageCollection;
968
+ presenceKey = null;
969
+ subscribers = [];
970
+ unsubscribePubSub = null;
971
+ pollTimer = null;
972
+ stopped = false;
973
+ constructor(opts) {
974
+ this.adapter = opts.adapter;
975
+ this.syncAdapter = opts.syncAdapter;
976
+ this.vault = opts.vault;
977
+ this.collectionName = opts.collectionName;
978
+ this.userId = opts.userId;
979
+ this.encrypted = opts.encrypted;
980
+ this.getDEK = opts.getDEK;
981
+ this.staleMs = opts.staleMs ?? 3e4;
982
+ this.pollIntervalMs = opts.pollIntervalMs ?? 5e3;
983
+ this.channel = `${opts.vault}:${opts.collectionName}:presence`;
984
+ this.storageCollection = `_presence_${opts.collectionName}`;
985
+ }
986
+ /**
987
+ * Announce yourself (or update your cursor/status).
988
+ * Encrypts `payload` with the presence key and publishes it.
989
+ */
990
+ async update(payload) {
991
+ if (this.stopped) return;
992
+ const key = await this.getPresenceKey();
993
+ const now = (/* @__PURE__ */ new Date()).toISOString();
994
+ const plaintext = JSON.stringify({ userId: this.userId, lastSeen: now, payload });
995
+ let encryptedPayload;
996
+ if (this.encrypted && key) {
997
+ const iv = generateIV();
998
+ const ivB64 = bufferToBase64(iv);
999
+ const { data } = await encrypt(plaintext, key);
1000
+ encryptedPayload = JSON.stringify({ iv: ivB64, data });
1001
+ } else {
1002
+ encryptedPayload = plaintext;
1003
+ }
1004
+ const pubAdapter = this.getPubSubAdapter();
1005
+ if (pubAdapter?.presencePublish) {
1006
+ await pubAdapter.presencePublish(this.channel, encryptedPayload);
1007
+ }
1008
+ await this.writeStorageRecord(payload, now);
1009
+ }
1010
+ /**
1011
+ * Subscribe to presence updates. The callback receives a filtered, decrypted
1012
+ * list of all currently-active peers (excluding yourself, excluding stale).
1013
+ *
1014
+ * Returns an unsubscribe function. Also call `stop()` to release all resources.
1015
+ */
1016
+ subscribe(cb) {
1017
+ if (this.stopped) return () => {
1018
+ };
1019
+ this.subscribers.push(cb);
1020
+ if (this.subscribers.length === 1) {
1021
+ this.startListening();
1022
+ }
1023
+ return () => {
1024
+ this.subscribers = this.subscribers.filter((s) => s !== cb);
1025
+ if (this.subscribers.length === 0) this.stopListening();
1026
+ };
1027
+ }
1028
+ /** Stop all listening and clear resources. */
1029
+ stop() {
1030
+ this.stopped = true;
1031
+ this.stopListening();
1032
+ this.subscribers = [];
1033
+ }
1034
+ // ─── Private ────────────────────────────────────────────────────────
1035
+ async getPresenceKey() {
1036
+ if (!this.encrypted) return null;
1037
+ if (!this.presenceKey) {
1038
+ try {
1039
+ const dek = await this.getDEK(this.collectionName);
1040
+ this.presenceKey = await derivePresenceKey(dek, this.collectionName);
1041
+ } catch {
1042
+ }
1043
+ }
1044
+ return this.presenceKey;
1045
+ }
1046
+ getPubSubAdapter() {
1047
+ if (this.syncAdapter?.presencePublish) return this.syncAdapter;
1048
+ if (this.adapter.presencePublish) return this.adapter;
1049
+ return void 0;
1050
+ }
1051
+ startListening() {
1052
+ const pubAdapter = this.getPubSubAdapter();
1053
+ if (pubAdapter?.presenceSubscribe) {
1054
+ this.unsubscribePubSub = pubAdapter.presenceSubscribe(
1055
+ this.channel,
1056
+ (encryptedPayload) => {
1057
+ void this.handlePubSubMessage(encryptedPayload);
1058
+ }
1059
+ );
1060
+ } else {
1061
+ this.pollTimer = setInterval(
1062
+ () => {
1063
+ void this.pollStoragePresence();
1064
+ },
1065
+ this.pollIntervalMs
1066
+ );
1067
+ void this.pollStoragePresence();
1068
+ }
1069
+ }
1070
+ stopListening() {
1071
+ if (this.unsubscribePubSub) {
1072
+ this.unsubscribePubSub();
1073
+ this.unsubscribePubSub = null;
1074
+ }
1075
+ if (this.pollTimer) {
1076
+ clearInterval(this.pollTimer);
1077
+ this.pollTimer = null;
1078
+ }
1079
+ }
1080
+ async handlePubSubMessage(encryptedPayload) {
1081
+ try {
1082
+ const peer = await this.decryptPresencePayload(encryptedPayload);
1083
+ if (!peer || peer.userId === this.userId) return;
1084
+ const cutoff = new Date(Date.now() - this.staleMs).toISOString();
1085
+ if (peer.lastSeen < cutoff) return;
1086
+ await this.pollStoragePresence();
1087
+ } catch {
1088
+ }
1089
+ }
1090
+ async decryptPresencePayload(encryptedPayload) {
1091
+ const key = await this.getPresenceKey();
1092
+ if (!this.encrypted || !key) {
1093
+ return JSON.parse(encryptedPayload);
1094
+ }
1095
+ const { iv: ivB64, data } = JSON.parse(encryptedPayload);
1096
+ const plaintext = await decrypt(ivB64, data, key);
1097
+ return JSON.parse(plaintext);
1098
+ }
1099
+ async writeStorageRecord(payload, now) {
1100
+ const key = await this.getPresenceKey();
1101
+ const plaintext = JSON.stringify(payload);
1102
+ let iv = "";
1103
+ let data;
1104
+ if (this.encrypted && key) {
1105
+ const ivBytes = generateIV();
1106
+ iv = bufferToBase64(ivBytes);
1107
+ const result = await encrypt(plaintext, key);
1108
+ data = result.data;
1109
+ } else {
1110
+ data = plaintext;
1111
+ }
1112
+ const record = { userId: this.userId, lastSeen: now, iv, data };
1113
+ const json = JSON.stringify(record);
1114
+ const storeAdapter = this.syncAdapter ?? this.adapter;
1115
+ const envelope = {
1116
+ _noydb: 1,
1117
+ _v: 1,
1118
+ _ts: now,
1119
+ _iv: "",
1120
+ _data: json
1121
+ };
1122
+ try {
1123
+ await storeAdapter.put(
1124
+ this.vault,
1125
+ this.storageCollection,
1126
+ this.userId,
1127
+ envelope
1128
+ );
1129
+ } catch {
1130
+ }
1131
+ }
1132
+ async pollStoragePresence() {
1133
+ if (this.stopped || this.subscribers.length === 0) return;
1134
+ try {
1135
+ const storeAdapter = this.syncAdapter ?? this.adapter;
1136
+ const ids = await storeAdapter.list(this.vault, this.storageCollection);
1137
+ const cutoff = new Date(Date.now() - this.staleMs).toISOString();
1138
+ const peers = [];
1139
+ for (const id of ids) {
1140
+ if (id === this.userId) continue;
1141
+ const envelope = await storeAdapter.get(this.vault, this.storageCollection, id);
1142
+ if (!envelope) continue;
1143
+ const record = JSON.parse(envelope._data);
1144
+ if (record.lastSeen < cutoff) continue;
1145
+ let peerPayload;
1146
+ if (this.encrypted && this.presenceKey && record.iv) {
1147
+ const plaintext = await decrypt(record.iv, record.data, this.presenceKey);
1148
+ peerPayload = JSON.parse(plaintext);
1149
+ } else {
1150
+ peerPayload = JSON.parse(record.data);
1151
+ }
1152
+ peers.push({ userId: record.userId, payload: peerPayload, lastSeen: record.lastSeen });
1153
+ }
1154
+ for (const cb of this.subscribers) {
1155
+ cb(peers);
1156
+ }
1157
+ } catch {
1158
+ }
1159
+ }
1160
+ };
1161
+
1162
+ // src/team/sync-credentials.ts
1163
+ var SYNC_CREDENTIALS_COLLECTION = "_sync_credentials";
1164
+ function requireAdminAccess(keyring) {
1165
+ if (keyring.role !== "owner" && keyring.role !== "admin") {
1166
+ throw new PermissionDeniedError(
1167
+ `Sync credentials require owner or admin role. Current role: "${keyring.role}"`
1168
+ );
1169
+ }
1170
+ }
1171
+ async function putCredential(adapter, vault, keyring, credential) {
1172
+ requireAdminAccess(keyring);
1173
+ const getDek = await ensureCollectionDEK(adapter, vault, keyring);
1174
+ const dek = await getDek(SYNC_CREDENTIALS_COLLECTION);
1175
+ const { iv, data } = await encrypt(JSON.stringify(credential), dek);
1176
+ const existing = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, credential.adapterId);
1177
+ const version = existing ? existing._v + 1 : 1;
1178
+ const envelope = {
1179
+ _noydb: NOYDB_FORMAT_VERSION,
1180
+ _v: version,
1181
+ _ts: (/* @__PURE__ */ new Date()).toISOString(),
1182
+ _iv: iv,
1183
+ _data: data,
1184
+ _by: keyring.userId
1185
+ };
1186
+ await adapter.put(
1187
+ vault,
1188
+ SYNC_CREDENTIALS_COLLECTION,
1189
+ credential.adapterId,
1190
+ envelope,
1191
+ existing ? existing._v : void 0
1192
+ );
1193
+ }
1194
+ async function getCredential(adapter, vault, keyring, adapterId) {
1195
+ requireAdminAccess(keyring);
1196
+ const getDek = await ensureCollectionDEK(adapter, vault, keyring);
1197
+ const dek = await getDek(SYNC_CREDENTIALS_COLLECTION);
1198
+ const envelope = await adapter.get(vault, SYNC_CREDENTIALS_COLLECTION, adapterId);
1199
+ if (!envelope) return null;
1200
+ const plaintext = await decrypt(envelope._iv, envelope._data, dek);
1201
+ return JSON.parse(plaintext);
1202
+ }
1203
+ async function deleteCredential(adapter, vault, keyring, adapterId) {
1204
+ requireAdminAccess(keyring);
1205
+ await adapter.delete(vault, SYNC_CREDENTIALS_COLLECTION, adapterId);
1206
+ }
1207
+ async function listCredentials(adapter, vault, keyring) {
1208
+ requireAdminAccess(keyring);
1209
+ return adapter.list(vault, SYNC_CREDENTIALS_COLLECTION);
1210
+ }
1211
+ async function credentialStatus(adapter, vault, keyring, adapterId) {
1212
+ const credential = await getCredential(adapter, vault, keyring, adapterId);
1213
+ if (!credential) return { exists: false };
1214
+ const expired = credential.expiresAt ? Date.now() > new Date(credential.expiresAt).getTime() : false;
1215
+ return { exists: true, expired };
1216
+ }
1217
+ // Annotate the CommonJS export names for ESM import in node:
1218
+ 0 && (module.exports = {
1219
+ PresenceHandle,
1220
+ SYNC_CREDENTIALS_COLLECTION,
1221
+ SyncEngine,
1222
+ SyncTransaction,
1223
+ credentialStatus,
1224
+ deleteCredential,
1225
+ evaluateExportCapability,
1226
+ evaluateImportCapability,
1227
+ getCredential,
1228
+ hasExportCapability,
1229
+ hasImportCapability,
1230
+ listCredentials,
1231
+ putCredential
1232
+ });
1233
+ //# sourceMappingURL=index.cjs.map