mindoodb 0.0.86 → 0.0.88

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 (127) hide show
  1. package/README-server.md +5 -2
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +2 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  5. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +151 -12
  6. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  7. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +340 -24
  8. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  9. package/dist/browser/index.mjs +14 -0
  10. package/dist/core/BaseMindooDB.d.ts +67 -9
  11. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  12. package/dist/core/BaseMindooDB.js +261 -12
  13. package/dist/core/BaseMindooDB.js.map +1 -1
  14. package/dist/core/BaseMindooTenant.d.ts +29 -0
  15. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  16. package/dist/core/BaseMindooTenant.js +111 -6
  17. package/dist/core/BaseMindooTenant.js.map +1 -1
  18. package/dist/core/BaseMindooTenantDirectory.d.ts +93 -0
  19. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  20. package/dist/core/BaseMindooTenantDirectory.js +292 -0
  21. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  22. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  23. package/dist/core/BaseMindooTenantFactory.js +2 -1
  24. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  25. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
  26. package/dist/core/MindooDBServerAdmin.js +2 -1
  27. package/dist/core/MindooDBServerAdmin.js.map +1 -1
  28. package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
  29. package/dist/core/accesscontrol/directoryProjection.js +19 -0
  30. package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
  31. package/dist/core/accesscontrol/evaluate.d.ts +14 -0
  32. package/dist/core/accesscontrol/evaluate.d.ts.map +1 -1
  33. package/dist/core/accesscontrol/evaluate.js +42 -1
  34. package/dist/core/accesscontrol/evaluate.js.map +1 -1
  35. package/dist/core/accesscontrol/types.d.ts +88 -4
  36. package/dist/core/accesscontrol/types.d.ts.map +1 -1
  37. package/dist/core/accesscontrol/types.js +114 -1
  38. package/dist/core/accesscontrol/types.js.map +1 -1
  39. package/dist/core/appendonlystores/network/AuthenticationService.d.ts +6 -0
  40. package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
  41. package/dist/core/appendonlystores/network/AuthenticationService.js +8 -7
  42. package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
  43. package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
  44. package/dist/core/appendonlystores/network/IrohNetworkTransport.js +12 -0
  45. package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
  46. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts +6 -0
  47. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts.map +1 -1
  48. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js +20 -1
  49. package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js.map +1 -1
  50. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts +7 -0
  51. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
  52. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +18 -0
  53. package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
  54. package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
  55. package/dist/core/appendonlystores/syncStores.js +25 -5
  56. package/dist/core/appendonlystores/syncStores.js.map +1 -1
  57. package/dist/core/appendonlystores/types.d.ts +7 -1
  58. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  59. package/dist/core/appendonlystores/types.js +1 -0
  60. package/dist/core/appendonlystores/types.js.map +1 -1
  61. package/dist/core/builtinDbInvariants.d.ts +49 -0
  62. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  63. package/dist/core/builtinDbInvariants.js +86 -1
  64. package/dist/core/builtinDbInvariants.js.map +1 -1
  65. package/dist/core/crypto/AuthChallenge.d.ts +68 -0
  66. package/dist/core/crypto/AuthChallenge.d.ts.map +1 -0
  67. package/dist/core/crypto/AuthChallenge.js +118 -0
  68. package/dist/core/crypto/AuthChallenge.js.map +1 -0
  69. package/dist/core/crypto/DomainStatement.d.ts +5 -3
  70. package/dist/core/crypto/DomainStatement.d.ts.map +1 -1
  71. package/dist/core/crypto/DomainStatement.js +5 -3
  72. package/dist/core/crypto/DomainStatement.js.map +1 -1
  73. package/dist/core/crypto/encryptedFields.d.ts +11 -0
  74. package/dist/core/crypto/encryptedFields.d.ts.map +1 -1
  75. package/dist/core/crypto/encryptedFields.js +18 -0
  76. package/dist/core/crypto/encryptedFields.js.map +1 -1
  77. package/dist/core/index.d.ts +3 -3
  78. package/dist/core/index.d.ts.map +1 -1
  79. package/dist/core/index.js +17 -6
  80. package/dist/core/index.js.map +1 -1
  81. package/dist/core/query/executeQuery.d.ts.map +1 -1
  82. package/dist/core/query/executeQuery.js +5 -1
  83. package/dist/core/query/executeQuery.js.map +1 -1
  84. package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
  85. package/dist/core/query/executeQueryInclude.js +2 -0
  86. package/dist/core/query/executeQueryInclude.js.map +1 -1
  87. package/dist/core/query/queryView.d.ts.map +1 -1
  88. package/dist/core/query/queryView.js +5 -1
  89. package/dist/core/query/queryView.js.map +1 -1
  90. package/dist/core/types.d.ts +143 -8
  91. package/dist/core/types.d.ts.map +1 -1
  92. package/dist/core/types.js.map +1 -1
  93. package/dist/core/userkeys/index.d.ts +1 -0
  94. package/dist/core/userkeys/index.d.ts.map +1 -1
  95. package/dist/core/userkeys/index.js +5 -1
  96. package/dist/core/userkeys/index.js.map +1 -1
  97. package/dist/core/userkeys/sealedValues.d.ts +54 -0
  98. package/dist/core/userkeys/sealedValues.d.ts.map +1 -0
  99. package/dist/core/userkeys/sealedValues.js +67 -0
  100. package/dist/core/userkeys/sealedValues.js.map +1 -0
  101. package/dist/core/utils/dynamicImportInterop.d.ts +10 -0
  102. package/dist/core/utils/dynamicImportInterop.d.ts.map +1 -0
  103. package/dist/core/utils/dynamicImportInterop.js +22 -0
  104. package/dist/core/utils/dynamicImportInterop.js.map +1 -0
  105. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  106. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +7 -0
  107. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  108. package/dist/server/DeviceDiscoveryService.d.ts.map +1 -1
  109. package/dist/server/DeviceDiscoveryService.js +3 -4
  110. package/dist/server/DeviceDiscoveryService.js.map +1 -1
  111. package/dist/server/SystemAdminAuth.d.ts.map +1 -1
  112. package/dist/server/SystemAdminAuth.js +3 -4
  113. package/dist/server/SystemAdminAuth.js.map +1 -1
  114. package/dist/server/TenantManager.d.ts +36 -0
  115. package/dist/server/TenantManager.d.ts.map +1 -1
  116. package/dist/server/TenantManager.js +234 -4
  117. package/dist/server/TenantManager.js.map +1 -1
  118. package/dist/server/http/MindooDBServerCore.d.ts.map +1 -1
  119. package/dist/server/http/MindooDBServerCore.js +7 -0
  120. package/dist/server/http/MindooDBServerCore.js.map +1 -1
  121. package/dist/server/peer/PeerAuthService.d.ts.map +1 -1
  122. package/dist/server/peer/PeerAuthService.js +3 -4
  123. package/dist/server/peer/PeerAuthService.js.map +1 -1
  124. package/dist/server/peer/PeerReplicator.d.ts.map +1 -1
  125. package/dist/server/peer/PeerReplicator.js +2 -1
  126. package/dist/server/peer/PeerReplicator.js.map +1 -1
  127. package/package.json +1 -1
@@ -10,6 +10,9 @@ const EntrySignature_1 = require("../../core/crypto/EntrySignature");
10
10
  const idGeneration_1 = require("../../core/utils/idGeneration");
11
11
  const WitnessReceipt_1 = require("../../core/crypto/WitnessReceipt");
12
12
  const logging_1 = require("../../core/logging");
13
+ const types_3 = require("../../core/accesscontrol/types");
14
+ const sealedTypes_1 = require("../../core/userkeys/sealedTypes");
15
+ const types_4 = require("../../core/types");
13
16
  /**
14
17
  * Server-side network handler for ContentAddressedStore operations.
15
18
  *
@@ -22,6 +25,10 @@ const logging_1 = require("../../core/logging");
22
25
  * This is not a ContentAddressedStore implementation itself, but rather
23
26
  * a service that wraps a local ContentAddressedStore and handles network requests.
24
27
  */
28
+ /** Sliding window of the internal-mail rate limit. */
29
+ const INTERNAL_MAIL_WINDOW_MS = 60 * 60 * 1000;
30
+ /** How often the internal-mail counters are pruned at most. */
31
+ const INTERNAL_MAIL_PRUNE_INTERVAL_MS = 60 * 1000;
25
32
  class ServerNetworkContentAddressedStore {
26
33
  /**
27
34
  * Create a new ServerNetworkContentAddressedStore.
@@ -33,6 +40,14 @@ class ServerNetworkContentAddressedStore {
33
40
  * @param logger Optional logger instance
34
41
  */
35
42
  constructor(localStore, directory, authService, cryptoAdapter, logger, accessControl) {
43
+ /** docId → creator of its `doc_create` (never changes once known). */
44
+ this.docCreatorByDocId = new Map();
45
+ /** Internal mail limits (§6.11): bytes per deposited document, accept times per sender. */
46
+ /** Bytes per internal mail doc and when it was last seen (pruned after an hour). */
47
+ this.internalMailBytesByDocId = new Map();
48
+ /** doc_create times per sender within the last hour (pruned, empty senders dropped). */
49
+ this.internalMailTimesBySender = new Map();
50
+ this.internalMailPrunedAt = 0;
36
51
  /** Cached `username_hash` per userdirectory docId, filled as creates are seen. */
37
52
  this.userdirectoryHashByDocId = new Map();
38
53
  /** Cached create signing key per personal userdirectory docId (its owner). */
@@ -51,11 +66,15 @@ class ServerNetworkContentAddressedStore {
51
66
  this.tier1Evaluator = accessControl?.tier1Evaluator;
52
67
  this.wipeGrantDocIdResolver = accessControl?.wipeGrantDocIdResolver;
53
68
  this.dbAccessEvaluator = accessControl?.dbAccessEvaluator;
69
+ this.dbAccessModeEvaluator = accessControl?.dbAccessModeEvaluator;
54
70
  this.revokedKeyResolver = accessControl?.revokedKeyResolver;
55
71
  this.purgedDocResolver = accessControl?.purgedDocResolver;
56
72
  this.builtinWriteContext = accessControl?.builtinWriteContext;
57
73
  this.peerTokenResolver = accessControl?.peerTokenResolver;
58
74
  this.trustedWitnessResolver = accessControl?.trustedWitnessResolver;
75
+ this.readScopeResolver = accessControl?.readScopeResolver;
76
+ this.docCreatorResolver = accessControl?.docCreatorResolver;
77
+ this.purgeRequestValidator = accessControl?.purgeRequestValidator;
59
78
  }
60
79
  /**
61
80
  * Resolve the reader's RSA encryption public key for transport encryption.
@@ -159,6 +178,97 @@ class ServerNetworkContentAddressedStore {
159
178
  }
160
179
  return kept;
161
180
  }
181
+ /**
182
+ * The read scope of the request's principal (§6.12), or `null` when it reads
183
+ * everything. Peers mirror the store completely; the directory stores are
184
+ * never scoped.
185
+ */
186
+ async resolveReadScope(payload) {
187
+ if (payload.peer)
188
+ return null;
189
+ if (!this.readScopeResolver)
190
+ return null;
191
+ const dbId = this.localStore.getId();
192
+ if (dbId === ServerNetworkContentAddressedStore.DIRECTORY_DB_ID || dbId === types_1.USER_DIRECTORY_DB_ID) {
193
+ return null;
194
+ }
195
+ return this.readScopeResolver(this.readPrincipal(payload));
196
+ }
197
+ /** The creator of `docId`'s `doc_create`, cached once known. */
198
+ async docCreator(docId) {
199
+ const cached = this.docCreatorByDocId.get(docId);
200
+ if (cached)
201
+ return cached;
202
+ let found = null;
203
+ if (this.docCreatorResolver) {
204
+ found = await this.docCreatorResolver(docId);
205
+ }
206
+ else {
207
+ const metas = await this.localStore.findNewEntriesForDoc([], docId);
208
+ const create = metas.find((m) => m.entryType === "doc_create");
209
+ if (create) {
210
+ found = { signingKey: create.createdByPublicKey, trustedTime: create.receivedAt ?? create.createdAt };
211
+ }
212
+ }
213
+ if (found)
214
+ this.docCreatorByDocId.set(docId, found);
215
+ return found;
216
+ }
217
+ /**
218
+ * A per-request decision cache for "may the principal see this document".
219
+ * Fail-closed: a document whose `doc_create` is unknown is hidden.
220
+ */
221
+ scopeChecker(scope) {
222
+ const decided = new Map();
223
+ return (docId) => {
224
+ let decision = decided.get(docId);
225
+ if (!decision) {
226
+ decision = (async () => {
227
+ const creator = await this.docCreator(docId);
228
+ return creator ? scope.ownsDocument(creator.signingKey, creator.trustedTime) : false;
229
+ })();
230
+ decided.set(docId, decision);
231
+ }
232
+ return decision;
233
+ };
234
+ }
235
+ /** Drop metadata of documents outside the principal's read scope (§6.12). */
236
+ async filterMetasByReadScope(metas, scope) {
237
+ if (!scope || metas.length === 0)
238
+ return metas;
239
+ const visible = this.scopeChecker(scope);
240
+ const kept = [];
241
+ for (const meta of metas) {
242
+ if (await visible(meta.docId))
243
+ kept.push(meta);
244
+ }
245
+ return kept;
246
+ }
247
+ /** Drop ids of entries outside the principal's read scope (§6.12). */
248
+ async filterIdsByReadScope(ids, scope) {
249
+ if (!scope || ids.length === 0)
250
+ return ids;
251
+ const visible = this.scopeChecker(scope);
252
+ const kept = [];
253
+ for (const id of ids) {
254
+ const meta = await this.localStore.getEntryMetadata(id);
255
+ if (meta && (await visible(meta.docId)))
256
+ kept.push(id);
257
+ }
258
+ return kept;
259
+ }
260
+ /** Both per-user read filters (§13 revoked keys, §6.12 read scope) on metadata. */
261
+ async filterMetasForReader(metas, payload) {
262
+ const revoked = await this.resolveRevokedKeyIds(payload);
263
+ const scope = await this.resolveReadScope(payload);
264
+ return this.filterMetasByReadScope(this.filterMetasByRevokedKeys(metas, revoked), scope);
265
+ }
266
+ /** Both per-user read filters (§13 revoked keys, §6.12 read scope) on ids. */
267
+ async filterIdsForReader(ids, payload) {
268
+ const revoked = await this.resolveRevokedKeyIds(payload);
269
+ const scope = await this.resolveReadScope(payload);
270
+ return this.filterIdsByReadScope(await this.filterIdsByRevokedKeys(ids, revoked), scope);
271
+ }
162
272
  /**
163
273
  * Get the ID of the underlying store.
164
274
  */
@@ -208,8 +318,7 @@ class ServerNetworkContentAddressedStore {
208
318
  this.logger.debug(`Found ${newEntries.length} new entries`);
209
319
  const allowed = await this.wipeAllowedIds(tokenPayload);
210
320
  const scoped = allowed ? newEntries.filter((e) => allowed.has(e.id)) : newEntries;
211
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
212
- return this.filterMetasByRevokedKeys(scoped, revoked);
321
+ return this.filterMetasForReader(scoped, tokenPayload);
213
322
  }
214
323
  /**
215
324
  * Handle a findNewEntriesForDoc request from a client.
@@ -230,8 +339,7 @@ class ServerNetworkContentAddressedStore {
230
339
  this.logger.debug(`Found ${newEntries.length} new entries for doc ${docId}`);
231
340
  const allowed = await this.wipeAllowedIds(tokenPayload);
232
341
  const scoped = allowed ? newEntries.filter((e) => allowed.has(e.id)) : newEntries;
233
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
234
- return this.filterMetasByRevokedKeys(scoped, revoked);
342
+ return this.filterMetasForReader(scoped, tokenPayload);
235
343
  }
236
344
  /**
237
345
  * Handle a findEntries request from a client.
@@ -252,8 +360,7 @@ class ServerNetworkContentAddressedStore {
252
360
  this.logger.debug(`Found ${entries.length} entries`);
253
361
  const allowed = await this.wipeAllowedIds(tokenPayload);
254
362
  const scoped = allowed ? entries.filter((e) => allowed.has(e.id)) : entries;
255
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
256
- return this.filterMetasByRevokedKeys(scoped, revoked);
363
+ return this.filterMetasForReader(scoped, tokenPayload);
257
364
  }
258
365
  /**
259
366
  * Handle a cursor-based scanEntriesSince request from a client.
@@ -286,10 +393,11 @@ class ServerNetworkContentAddressedStore {
286
393
  // Per-user revoked-key blacklist (§13): silently omit revoked entries from
287
394
  // the page. Filtering after paging only shrinks a page; the store's own
288
395
  // cursor still advances so the client keeps making progress.
289
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
396
+ // The read scope (§6.12) filters the same way: a page may come back empty
397
+ // while `nextCursor`/`hasMore` still move on over hidden entries.
290
398
  if (this.localStore.scanEntriesSince) {
291
399
  const result = await this.localStore.scanEntriesSince(cursor, effectiveLimit, filters);
292
- return { ...result, entries: this.filterMetasByRevokedKeys(result.entries, revoked) };
400
+ return { ...result, entries: await this.filterMetasForReader(result.entries, tokenPayload) };
293
401
  }
294
402
  // Fallback: emulate cursor scan using existing metadata query.
295
403
  const all = filters?.docId
@@ -326,7 +434,7 @@ class ServerNetworkContentAddressedStore {
326
434
  const page = sorted.slice(startIndex, startIndex + max);
327
435
  const last = page.length > 0 ? page[page.length - 1] : null;
328
436
  return {
329
- entries: this.filterMetasByRevokedKeys(page, revoked),
437
+ entries: await this.filterMetasForReader(page, tokenPayload),
330
438
  nextCursor: last ? { receiptOrder: last.receiptOrder ?? 0, id: last.id } : cursor,
331
439
  hasMore: startIndex + page.length < sorted.length,
332
440
  };
@@ -348,7 +456,7 @@ class ServerNetworkContentAddressedStore {
348
456
  * Handle sync capability negotiation.
349
457
  */
350
458
  async handleGetCapabilities(token) {
351
- const tokenPayload = await this.validateToken(token);
459
+ const tokenPayload = await this.validateToken(token, true);
352
460
  this.logger.debug(`Token validated for user: ${tokenPayload.sub}`);
353
461
  return {
354
462
  protocolVersion: "sync-v5",
@@ -380,11 +488,17 @@ class ServerNetworkContentAddressedStore {
380
488
  * Cheap: token validation + an in-memory read on the local store.
381
489
  */
382
490
  async handleGetStoreHead(token) {
383
- await this.validateToken(token);
491
+ const tokenPayload = await this.validateToken(token);
384
492
  if (!this.localStore.getStoreHead) {
385
493
  throw new types_2.NetworkError(types_2.NetworkErrorType.SERVER_ERROR, "Local store does not support getStoreHead");
386
494
  }
387
- return this.localStore.getStoreHead();
495
+ const head = await this.localStore.getStoreHead();
496
+ // A scoped reader (§6.12) gets its own epoch lineage: when the scope
497
+ // changes (mode switched, reader rule added or removed) the epoch differs
498
+ // and the client re-scans from the start instead of trusting a cursor that
499
+ // skipped entries it could not see before.
500
+ const scope = await this.resolveReadScope(tokenPayload);
501
+ return scope ? { ...head, epoch: `${head.epoch}:scope:${scope.scopeId}` } : head;
388
502
  }
389
503
  /**
390
504
  * Handle a getCompactionStatus request from a client.
@@ -487,8 +601,8 @@ class ServerNetworkContentAddressedStore {
487
601
  // path. Even if a client already knows a revoked entry's id, the server
488
602
  // never hands back its bytes. A StoreEntry carries its own decryptionKeyId,
489
603
  // so we filter without an extra metadata lookup.
490
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
491
- const servableEntries = this.filterMetasByRevokedKeys(entries, revoked);
604
+ // Read scope (§6.12): fail-closed as well, by the entry's document.
605
+ const servableEntries = await this.filterMetasForReader(entries, tokenPayload);
492
606
  return { servableEntries, encryptionPublicKey, username };
493
607
  }
494
608
  async handleGetEntryMetadata(token, id) {
@@ -508,6 +622,11 @@ class ServerNetworkContentAddressedStore {
508
622
  if (revoked && meta.decryptionKeyId && revoked.has(meta.decryptionKeyId)) {
509
623
  return null;
510
624
  }
625
+ // Read scope (§6.12): nothing about other people's documents.
626
+ const scope = await this.resolveReadScope(tokenPayload);
627
+ if (scope && (await this.filterMetasByReadScope([meta], scope)).length === 0) {
628
+ return null;
629
+ }
511
630
  return meta;
512
631
  }
513
632
  /**
@@ -531,7 +650,7 @@ class ServerNetworkContentAddressedStore {
531
650
  async handlePutEntries(token, entries) {
532
651
  this.logger.debug(`Handling putEntries request for ${entries.length} entries`);
533
652
  // Validate token
534
- const tokenPayload = await this.validateToken(token);
653
+ const tokenPayload = await this.validateToken(token, true);
535
654
  this.logger.debug(`Token validated for user: ${tokenPayload.sub}`);
536
655
  // Server-to-server push: enables witness-receipt preservation below.
537
656
  const peerIngest = tokenPayload.peer !== undefined;
@@ -552,6 +671,10 @@ class ServerNetworkContentAddressedStore {
552
671
  const purgedDocIds = this.purgedDocResolver
553
672
  ? await this.purgedDocResolver()
554
673
  : null;
674
+ // Author-scoped read (§6.12): a scoped principal may only write its own
675
+ // documents (create new ones, change its own), never into other people's.
676
+ const readScope = await this.resolveReadScope(tokenPayload);
677
+ const ownsForWrite = readScope ? this.scopeChecker(readScope) : null;
555
678
  // A single acceptance time for this batch so receipts are consistent and a
556
679
  // batch cannot interleave with the witness's own monotonic clock (§5.3).
557
680
  const receivedAt = Date.now();
@@ -651,6 +774,27 @@ class ServerNetworkContentAddressedStore {
651
774
  this.rejectEntry(rejected, entry.id, `Entry ${entry.id} has an invalid author signature`);
652
775
  continue;
653
776
  }
777
+ // Deposit-only principal (docs/accesscontrol.md §6.10): the deposit check
778
+ // replaces Tier 1 for its entries, because the doc_deposit rule is what
779
+ // authorizes them. Anything that is not a sealed create (or a chunk of
780
+ // one) is refused.
781
+ if (tokenPayload.deposit) {
782
+ const denied = (await this.evaluateDepositEntry(entry, tokenPayload, tokenPayload.deposit.maxRecipients, tokenPayload.deposit.docIdPrefixes)) ?? this.evaluateInternalMailLimits(entry, tokenPayload.deposit.internalMail);
783
+ if (denied) {
784
+ this.rejectEntry(rejected, entry.id, denied.reason, denied.rejectionClass);
785
+ continue;
786
+ }
787
+ }
788
+ // Purge requests in personal databases (§6.11): only by someone who may
789
+ // read the database, only for documents that are already deleted.
790
+ // Independent of the ACL master switch, like the builtin invariants.
791
+ if (this.purgeRequestValidator && (0, builtinDbInvariants_1.isPurgeRequestDocId)(entry.docId)) {
792
+ const denied = await this.purgeRequestValidator(entry, entries);
793
+ if (denied) {
794
+ this.rejectEntry(rejected, entry.id, `Entry ${entry.id}: ${denied}`, "policy");
795
+ continue;
796
+ }
797
+ }
654
798
  // Builtin `userdirectory` write invariant (docs/userkeys.md §6.5). Runs
655
799
  // before Tier 1 so it holds even when the ACL master switch is on.
656
800
  const dbId = this.witnessDbid ?? this.localStore.getId();
@@ -664,13 +808,22 @@ class ServerNetworkContentAddressedStore {
664
808
  // Rule-based Tier 1 enforcement (docs/accesscontrol.md §7). The server can
665
809
  // only decide the identity tier; content-tier (Tier 2) gates are deferred
666
810
  // to clients and treated as allowed here.
667
- if (this.tier1Evaluator) {
811
+ if (this.tier1Evaluator && !tokenPayload.deposit) {
668
812
  const decision = await this.tier1Evaluator(entry, this.witnessDbid ?? this.localStore.getId());
669
813
  if (!decision.allowed) {
670
814
  this.rejectEntry(rejected, entry.id, `Entry ${entry.id} denied by Tier 1 policy: ${decision.reason}`, "policy");
671
815
  continue;
672
816
  }
673
817
  }
818
+ if (readScope && ownsForWrite) {
819
+ const ownsIt = entry.entryType === "doc_create"
820
+ ? await readScope.ownsDocument(entry.createdByPublicKey, receivedAt)
821
+ : await this.ownsDocumentInBatch(entry, entries, readScope, ownsForWrite);
822
+ if (!ownsIt) {
823
+ this.rejectEntry(rejected, entry.id, `Entry ${entry.id} writes into a document outside the principal's read scope`, "policy");
824
+ continue;
825
+ }
826
+ }
674
827
  // Stamp a witness receipt onto the accepted entry (§5.3). Self-authored
675
828
  // entries (the witness pushing its own data) are not self-witnessed.
676
829
  //
@@ -721,10 +874,125 @@ class ServerNetworkContentAddressedStore {
721
874
  (rejected.length > 0 ? ` (${rejected.length} rejected)` : ""));
722
875
  return { receipts: stampedMetadata, rejected };
723
876
  }
877
+ /**
878
+ * What a deposit-only principal may push (docs/accesscontrol.md §6.10):
879
+ *
880
+ * - `doc_create` of a sealed document (`decryptionKeyId` is
881
+ * `$sealed:<docId>`) carrying 1..`maxRecipients` recipient wraps, signed
882
+ * by the depositing device itself, for a document id the store does not
883
+ * hold yet. An existing id is refused as `"exists"`, which a depositor
884
+ * reads as "already delivered".
885
+ * - `attachment_chunk` entries of such a document, signed by the depositor.
886
+ *
887
+ * Changes, deletes, undeletes and snapshots are always refused: a depositor
888
+ * cannot read the document, so it has no business editing it.
889
+ */
890
+ async evaluateDepositEntry(entry, tokenPayload, maxRecipients, docIdPrefixes) {
891
+ const policy = (reason) => ({ reason: `Entry ${entry.id}: ${reason}`, rejectionClass: "policy" });
892
+ // Allowed document ids (§6.10 `docIdPrefixes`); applies to chunks too, which carry the docId.
893
+ if (entry.docId && !(0, types_3.docIdMatchesPrefixes)(entry.docId, docIdPrefixes)) {
894
+ return policy(`deposited document ids must start with ${docIdPrefixes.map((p) => `"${p}"`).join(" or ")}`);
895
+ }
896
+ if (!entry.docId || entry.decryptionKeyId !== (0, sealedTypes_1.sealedKeyId)(entry.docId)) {
897
+ return policy("deposit-only principals may only push documents sealed with their own key ($sealed:<docId>)");
898
+ }
899
+ if (tokenPayload.deviceSigningKey && entry.createdByPublicKey !== tokenPayload.deviceSigningKey) {
900
+ return policy("deposit-only principals may only push entries they signed themselves");
901
+ }
902
+ if (entry.entryType === "attachment_chunk") {
903
+ return this.localStore.getStoreKind() === types_4.StoreKind.attachments
904
+ ? null
905
+ : policy("attachment chunks belong in the attachment store");
906
+ }
907
+ if (entry.entryType !== "doc_create") {
908
+ return policy(`deposit-only principals may only create documents, not push ${entry.entryType}`);
909
+ }
910
+ const wraps = entry.recipients?.wraps?.length ?? 0;
911
+ if (wraps < 1 || wraps > maxRecipients) {
912
+ return policy(`a deposited document needs 1 to ${maxRecipients} recipient wraps (has ${wraps})`);
913
+ }
914
+ const existing = await this.localStore.findNewEntriesForDoc([], entry.docId);
915
+ if (existing.length > 0) {
916
+ return { reason: `Entry ${entry.id}: document ${entry.docId} already exists`, rejectionClass: "exists" };
917
+ }
918
+ return null;
919
+ }
920
+ /**
921
+ * Size and rate limits of internal mail (§6.11): per sender (signing key)
922
+ * and mailbox (this store), a document plus its attachment chunks may not
923
+ * exceed `maxBytesPerMail` (each store counts its own entries), and at most
924
+ * `maxMailsPerHour` documents are accepted per sender in a sliding hour.
925
+ * Counters are in memory: a server restart resets them.
926
+ */
927
+ evaluateInternalMailLimits(entry, limits) {
928
+ if (!limits)
929
+ return null;
930
+ const now = Date.now();
931
+ this.pruneInternalMailCounters(now);
932
+ const policy = (reason) => ({ reason: `Entry ${entry.id}: ${reason}`, rejectionClass: "policy" });
933
+ const bytes = (this.internalMailBytesByDocId.get(entry.docId)?.bytes ?? 0) + entry.encryptedData.byteLength;
934
+ if (bytes > limits.maxBytesPerMail) {
935
+ return policy(`internal mail is limited to ${limits.maxBytesPerMail} bytes per mail`);
936
+ }
937
+ if (entry.entryType === "doc_create") {
938
+ const recent = (this.internalMailTimesBySender.get(entry.createdByPublicKey) ?? []).filter((t) => now - t < INTERNAL_MAIL_WINDOW_MS);
939
+ if (recent.length >= limits.maxMailsPerHour) {
940
+ this.internalMailTimesBySender.set(entry.createdByPublicKey, recent);
941
+ return policy(`internal mail is limited to ${limits.maxMailsPerHour} mails per hour per sender and mailbox`);
942
+ }
943
+ recent.push(now);
944
+ this.internalMailTimesBySender.set(entry.createdByPublicKey, recent);
945
+ }
946
+ this.internalMailBytesByDocId.set(entry.docId, { bytes, at: now });
947
+ return null;
948
+ }
949
+ /**
950
+ * Drops counters outside the one-hour window, at most once a minute: byte
951
+ * counts of mails not touched for an hour (a mail's document and attachment
952
+ * chunks arrive within minutes) and senders without a create in the last hour.
953
+ * Keeps memory bounded by the traffic of the last hour, for the server's lifetime.
954
+ */
955
+ pruneInternalMailCounters(now) {
956
+ if (now - this.internalMailPrunedAt < INTERNAL_MAIL_PRUNE_INTERVAL_MS)
957
+ return;
958
+ this.internalMailPrunedAt = now;
959
+ for (const [docId, counter] of this.internalMailBytesByDocId) {
960
+ if (now - counter.at >= INTERNAL_MAIL_WINDOW_MS)
961
+ this.internalMailBytesByDocId.delete(docId);
962
+ }
963
+ for (const [sender, times] of this.internalMailTimesBySender) {
964
+ const recent = times.filter((t) => now - t < INTERNAL_MAIL_WINDOW_MS);
965
+ if (recent.length === 0)
966
+ this.internalMailTimesBySender.delete(sender);
967
+ else if (recent.length !== times.length)
968
+ this.internalMailTimesBySender.set(sender, recent);
969
+ }
970
+ }
971
+ /** Test hook: sizes of the internal-mail counters. */
972
+ internalMailCounterSizes() {
973
+ return { docs: this.internalMailBytesByDocId.size, senders: this.internalMailTimesBySender.size };
974
+ }
724
975
  /**
725
976
  * Enforce the hard-wired `userdirectory` write invariant. Returns a denial
726
977
  * message, or `null` when the entry is allowed.
727
978
  */
979
+ /**
980
+ * For a non-create entry under a read scope: is its document the principal's?
981
+ * The `doc_create` may be in the same batch (not yet stored).
982
+ */
983
+ async ownsDocumentInBatch(entry, batch, scope, owns) {
984
+ const create = batch.find((e) => e.docId === entry.docId && e.entryType === "doc_create");
985
+ if (create && !(await this.localStore.hasEntries([create.id])).length) {
986
+ return scope.ownsDocument(create.createdByPublicKey, Date.now());
987
+ }
988
+ if (await this.docCreator(entry.docId)) {
989
+ return owns(entry.docId);
990
+ }
991
+ // The document is not on the server yet: attachment chunks are pushed
992
+ // before the document that references them. The entry's own author decides
993
+ // (the principal's own write); reads stay hidden until the doc_create is in.
994
+ return scope.ownsDocument(entry.createdByPublicKey, Date.now());
995
+ }
728
996
  async evaluateUserdirectoryInvariant(entry, trustedTime, batch = []) {
729
997
  const op = (0, builtinDbInvariants_1.entryTypeToBuiltinOp)(entry.entryType);
730
998
  if (!op || !this.builtinWriteContext) {
@@ -882,7 +1150,7 @@ class ServerNetworkContentAddressedStore {
882
1150
  async handleHasEntries(token, ids) {
883
1151
  this.logger.debug(`Handling hasEntries request for ${ids.length} IDs`);
884
1152
  // Validate token
885
- const tokenPayload = await this.validateToken(token);
1153
+ const tokenPayload = await this.validateToken(token, true);
886
1154
  this.logger.debug(`Token validated for user: ${tokenPayload.sub}`);
887
1155
  // Check which IDs exist in local store
888
1156
  const existingIds = await this.localStore.hasEntries(ids);
@@ -909,8 +1177,7 @@ class ServerNetworkContentAddressedStore {
909
1177
  this.logger.debug(`Returning ${allIds.length} entry IDs`);
910
1178
  const allowed = await this.wipeAllowedIds(tokenPayload);
911
1179
  const scoped = allowed ? allIds.filter((id) => allowed.has(id)) : allIds;
912
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
913
- return this.filterIdsByRevokedKeys(scoped, revoked);
1180
+ return this.filterIdsForReader(scoped, tokenPayload);
914
1181
  }
915
1182
  /**
916
1183
  * Handle a resolveDependencies request from a client.
@@ -934,18 +1201,35 @@ class ServerNetworkContentAddressedStore {
934
1201
  // Resolve dependencies in local store
935
1202
  const resolvedIds = await this.localStore.resolveDependencies(startId, options);
936
1203
  this.logger.debug(`Resolved ${resolvedIds.length} dependencies`);
937
- const revoked = await this.resolveRevokedKeyIds(tokenPayload);
938
- return this.filterIdsByRevokedKeys(resolvedIds, revoked);
1204
+ return this.filterIdsForReader(resolvedIds, tokenPayload);
939
1205
  }
940
1206
  async handlePlanDocumentMaterialization(token, docId, options) {
941
1207
  const tokenPayload = await this.validateToken(token);
942
1208
  this.logger.debug(`Token validated for user: ${tokenPayload.sub}`);
1209
+ const scope = await this.resolveReadScope(tokenPayload);
1210
+ if (scope && !(await this.scopeChecker(scope)(docId))) {
1211
+ // Read scope (§6.12): a document outside it looks like an unknown one.
1212
+ return { docId, snapshotEntryId: null, entryIdsToApply: [] };
1213
+ }
943
1214
  return this.localStore.planDocumentMaterialization(docId, options);
944
1215
  }
945
1216
  async handlePlanDocumentMaterializationBatch(token, docIds, options) {
946
1217
  const tokenPayload = await this.validateToken(token);
947
1218
  this.logger.debug(`Token validated for user: ${tokenPayload.sub}`);
948
- return this.localStore.planDocumentMaterializationBatch(docIds, options);
1219
+ const scope = await this.resolveReadScope(tokenPayload);
1220
+ if (!scope) {
1221
+ return this.localStore.planDocumentMaterializationBatch(docIds, options);
1222
+ }
1223
+ const visible = this.scopeChecker(scope);
1224
+ const shown = [];
1225
+ for (const docId of docIds)
1226
+ if (await visible(docId))
1227
+ shown.push(docId);
1228
+ const plan = await this.localStore.planDocumentMaterializationBatch(shown, options);
1229
+ const byId = new Map(plan.plans.map((p) => [p.docId, p]));
1230
+ return {
1231
+ plans: docIds.map((docId) => byId.get(docId) ?? { docId, snapshotEntryId: null, entryIdsToApply: [] }),
1232
+ };
949
1233
  }
950
1234
  async handlePlanAttachmentReadByWalkingMetadata(token, lastChunkId, attachmentSize, options) {
951
1235
  const tokenPayload = await this.validateToken(token);
@@ -953,6 +1237,10 @@ class ServerNetworkContentAddressedStore {
953
1237
  if (!this.localStore.planAttachmentReadByWalkingMetadata) {
954
1238
  throw new Error("Remote store does not support attachment read planning");
955
1239
  }
1240
+ const scope = await this.resolveReadScope(tokenPayload);
1241
+ if (scope && (await this.filterIdsByReadScope([lastChunkId], scope)).length === 0) {
1242
+ throw new types_2.NetworkError(types_2.NetworkErrorType.ACCESS_DENIED, "Attachment is outside the principal's read scope");
1243
+ }
956
1244
  return this.localStore.planAttachmentReadByWalkingMetadata(lastChunkId, attachmentSize, options);
957
1245
  }
958
1246
  /**
@@ -962,7 +1250,13 @@ class ServerNetworkContentAddressedStore {
962
1250
  * @returns The token payload
963
1251
  * @throws NetworkError if token is invalid
964
1252
  */
965
- async validateToken(token) {
1253
+ /**
1254
+ * @param allowDeposit Whether a deposit-only principal (§6.10) may perform
1255
+ * this operation. Only pushing (`putEntries`), the existence check push
1256
+ * relies on (`hasEntries`) and the capability probe pass it; every read is
1257
+ * refused for such a principal.
1258
+ */
1259
+ async validateToken(token, allowDeposit = false) {
966
1260
  const payload = await this.authService.validateToken(token);
967
1261
  if (!payload) {
968
1262
  // Server-to-server replication: a trusted peer authenticates once at the
@@ -992,7 +1286,29 @@ class ServerNetworkContentAddressedStore {
992
1286
  // Applied here so it covers every authenticated sync operation (reads and
993
1287
  // writes) in one place. The directory store is never wired with an
994
1288
  // evaluator, so it always syncs.
995
- if (this.dbAccessEvaluator) {
1289
+ if (this.dbAccessModeEvaluator) {
1290
+ const dbid = this.witnessDbid ?? this.localStore.getId();
1291
+ const access = await this.dbAccessModeEvaluator({ signingKey: payload.deviceSigningKey }, dbid);
1292
+ if (access.mode === "none") {
1293
+ throw new types_2.NetworkError(types_2.NetworkErrorType.ACCESS_DENIED, access.reason
1294
+ ? `Access denied to database "${dbid}": ${access.reason}`
1295
+ : `Database "${dbid}" is not in the tenant's allowed database list`);
1296
+ }
1297
+ if (access.mode === "deposit") {
1298
+ if (!allowDeposit) {
1299
+ throw new types_2.NetworkError(types_2.NetworkErrorType.ACCESS_DENIED, `Database "${dbid}" allows this user to deposit sealed documents only, not to read`);
1300
+ }
1301
+ return {
1302
+ ...payload,
1303
+ deposit: {
1304
+ maxRecipients: access.maxRecipients ?? types_3.DEFAULT_DEPOSIT_MAX_RECIPIENTS,
1305
+ ...(access.docIdPrefixes?.length ? { docIdPrefixes: access.docIdPrefixes } : {}),
1306
+ ...(access.internalMail ? { internalMail: access.internalMail } : {}),
1307
+ },
1308
+ };
1309
+ }
1310
+ }
1311
+ else if (this.dbAccessEvaluator) {
996
1312
  const dbid = this.witnessDbid ?? this.localStore.getId();
997
1313
  const allowed = await this.dbAccessEvaluator({ signingKey: payload.deviceSigningKey }, dbid);
998
1314
  if (!allowed) {