@enbox/dwn-sdk-js 0.4.15 → 0.4.17

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 (177) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +734 -747
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/json-schemas/definitions.json +5 -0
  6. package/dist/esm/src/core/dwn-constant.js +2 -2
  7. package/dist/esm/src/core/dwn-error.js +0 -1
  8. package/dist/esm/src/core/dwn-error.js.map +1 -1
  9. package/dist/esm/src/core/protocol-authorization-validation.js +1 -24
  10. package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
  11. package/dist/esm/src/core/protocol-authorization.js +1 -5
  12. package/dist/esm/src/core/protocol-authorization.js.map +1 -1
  13. package/dist/esm/src/core/validation-state-reader.js +2 -3
  14. package/dist/esm/src/core/validation-state-reader.js.map +1 -1
  15. package/dist/esm/src/handlers/messages-query.js +2 -29
  16. package/dist/esm/src/handlers/messages-query.js.map +1 -1
  17. package/dist/esm/src/handlers/messages-subscribe.js +52 -1
  18. package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
  19. package/dist/esm/src/handlers/records-count.js +18 -10
  20. package/dist/esm/src/handlers/records-count.js.map +1 -1
  21. package/dist/esm/src/handlers/records-query.js +10 -7
  22. package/dist/esm/src/handlers/records-query.js.map +1 -1
  23. package/dist/esm/src/handlers/records-subscribe.js +10 -7
  24. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  25. package/dist/esm/src/handlers/records-write.js +8 -26
  26. package/dist/esm/src/handlers/records-write.js.map +1 -1
  27. package/dist/esm/src/index.js +1 -1
  28. package/dist/esm/src/index.js.map +1 -1
  29. package/dist/esm/src/interfaces/protocols-configure.js +4 -9
  30. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  31. package/dist/esm/src/store/index-level.js +143 -6
  32. package/dist/esm/src/store/index-level.js.map +1 -1
  33. package/dist/esm/src/store/message-store-level.js +162 -23
  34. package/dist/esm/src/store/message-store-level.js.map +1 -1
  35. package/dist/esm/src/store/storage-controller.js +1 -3
  36. package/dist/esm/src/store/storage-controller.js.map +1 -1
  37. package/dist/esm/src/types/protocols-types.js +0 -10
  38. package/dist/esm/src/types/protocols-types.js.map +1 -1
  39. package/dist/esm/src/utils/filter.js +35 -1
  40. package/dist/esm/src/utils/filter.js.map +1 -1
  41. package/dist/esm/src/utils/grant-key-coverage.js +4 -6
  42. package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
  43. package/dist/esm/src/utils/messages.js +40 -14
  44. package/dist/esm/src/utils/messages.js.map +1 -1
  45. package/dist/esm/src/utils/permission-scope.js +3 -9
  46. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  47. package/dist/esm/src/utils/record-limit-occupancy.js +70 -202
  48. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  49. package/dist/esm/src/utils/records.js +9 -9
  50. package/dist/esm/src/utils/records.js.map +1 -1
  51. package/dist/esm/src/utils/replication.js +14 -0
  52. package/dist/esm/src/utils/replication.js.map +1 -1
  53. package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
  54. package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
  55. package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
  56. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  57. package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
  58. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  59. package/dist/esm/tests/dwn.spec.js +2 -2
  60. package/dist/esm/tests/dwn.spec.js.map +1 -1
  61. package/dist/esm/tests/features/records-delivery.spec.js +2 -5
  62. package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
  63. package/dist/esm/tests/features/records-nested-query-scope.spec.js +138 -73
  64. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  65. package/dist/esm/tests/features/records-record-limit.spec.js +264 -95
  66. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  67. package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
  68. package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
  69. package/dist/esm/tests/handlers/messages-query.spec.js +7 -15
  70. package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
  71. package/dist/esm/tests/handlers/messages-subscribe.spec.js +129 -1
  72. package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
  73. package/dist/esm/tests/handlers/records-write.spec.js +57 -3
  74. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  75. package/dist/esm/tests/store/index-level.spec.js +114 -1
  76. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  77. package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
  78. package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
  79. package/dist/esm/tests/store/message-store-level.spec.js +27 -0
  80. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  81. package/dist/esm/tests/store/message-store.spec.js +81 -0
  82. package/dist/esm/tests/store/message-store.spec.js.map +1 -1
  83. package/dist/esm/tests/utils/filters.spec.js +25 -4
  84. package/dist/esm/tests/utils/filters.spec.js.map +1 -1
  85. package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
  86. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  87. package/dist/esm/tests/utils/records.spec.js +12 -0
  88. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  89. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
  90. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
  91. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  92. package/dist/types/src/core/dwn-constant.d.ts +2 -2
  93. package/dist/types/src/core/dwn-error.d.ts +0 -1
  94. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  95. package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
  96. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  97. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  98. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  99. package/dist/types/src/handlers/messages-query.d.ts +0 -2
  100. package/dist/types/src/handlers/messages-query.d.ts.map +1 -1
  101. package/dist/types/src/handlers/messages-subscribe.d.ts +16 -0
  102. package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
  103. package/dist/types/src/handlers/records-count.d.ts +1 -0
  104. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  105. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  106. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  107. package/dist/types/src/handlers/records-write.d.ts +3 -0
  108. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  109. package/dist/types/src/index.d.ts +3 -3
  110. package/dist/types/src/index.d.ts.map +1 -1
  111. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  112. package/dist/types/src/store/index-level.d.ts +37 -0
  113. package/dist/types/src/store/index-level.d.ts.map +1 -1
  114. package/dist/types/src/store/message-store-level.d.ts +23 -6
  115. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  116. package/dist/types/src/store/storage-controller.d.ts.map +1 -1
  117. package/dist/types/src/types/message-store.d.ts +23 -2
  118. package/dist/types/src/types/message-store.d.ts.map +1 -1
  119. package/dist/types/src/types/messages-types.d.ts +14 -0
  120. package/dist/types/src/types/messages-types.d.ts.map +1 -1
  121. package/dist/types/src/types/protocols-types.d.ts +7 -24
  122. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  123. package/dist/types/src/types/query-types.d.ts +8 -1
  124. package/dist/types/src/types/query-types.d.ts.map +1 -1
  125. package/dist/types/src/types/records-types.d.ts +2 -1
  126. package/dist/types/src/types/records-types.d.ts.map +1 -1
  127. package/dist/types/src/utils/filter.d.ts +14 -1
  128. package/dist/types/src/utils/filter.d.ts.map +1 -1
  129. package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
  130. package/dist/types/src/utils/messages.d.ts +8 -0
  131. package/dist/types/src/utils/messages.d.ts.map +1 -1
  132. package/dist/types/src/utils/permission-scope.d.ts +0 -2
  133. package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
  134. package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
  135. package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
  136. package/dist/types/src/utils/records.d.ts +3 -2
  137. package/dist/types/src/utils/records.d.ts.map +1 -1
  138. package/dist/types/src/utils/replication.d.ts +8 -1
  139. package/dist/types/src/utils/replication.d.ts.map +1 -1
  140. package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
  141. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  142. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  143. package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
  144. package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
  145. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  146. package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
  147. package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
  148. package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
  149. package/package.json +4 -4
  150. package/src/core/dwn-constant.ts +2 -2
  151. package/src/core/dwn-error.ts +0 -2
  152. package/src/core/protocol-authorization-validation.ts +1 -33
  153. package/src/core/protocol-authorization.ts +0 -7
  154. package/src/core/validation-state-reader.ts +2 -3
  155. package/src/handlers/messages-query.ts +3 -41
  156. package/src/handlers/messages-subscribe.ts +67 -3
  157. package/src/handlers/records-count.ts +34 -13
  158. package/src/handlers/records-query.ts +14 -11
  159. package/src/handlers/records-subscribe.ts +18 -11
  160. package/src/handlers/records-write.ts +17 -32
  161. package/src/index.ts +3 -3
  162. package/src/interfaces/protocols-configure.ts +4 -13
  163. package/src/store/index-level.ts +197 -10
  164. package/src/store/message-store-level.ts +220 -27
  165. package/src/store/storage-controller.ts +1 -3
  166. package/src/types/message-store.ts +25 -2
  167. package/src/types/messages-types.ts +14 -0
  168. package/src/types/protocols-types.ts +7 -26
  169. package/src/types/query-types.ts +10 -2
  170. package/src/types/records-types.ts +2 -1
  171. package/src/utils/filter.ts +40 -3
  172. package/src/utils/grant-key-coverage.ts +4 -7
  173. package/src/utils/messages.ts +49 -14
  174. package/src/utils/permission-scope.ts +4 -11
  175. package/src/utils/record-limit-occupancy.ts +105 -293
  176. package/src/utils/records.ts +9 -9
  177. package/src/utils/replication.ts +19 -1
@@ -5,13 +5,13 @@ import type { ValidationStateReader } from '../types/validation-state-reader.js'
5
5
  import type { ProtocolActionRule, ProtocolDefinition, ProtocolRuleSet, ProtocolType, ProtocolTypes } from '../types/protocols-types.js';
6
6
 
7
7
  import { KeyDerivationScheme } from '../utils/hd-key.js';
8
+ import { ProtocolAction } from '../types/protocols-types.js';
8
9
  import { Records } from '../utils/records.js';
9
10
  import { validateProtocolTags } from '../utils/protocol-tags.js';
10
11
  import { DwnError, DwnErrorCode } from './dwn-error.js';
11
12
  import { Encryption, ROLE_AUDIENCE_DERIVATION_SCHEME } from '../utils/encryption.js';
12
13
  import { ENCRYPTION_PROTOCOL_GRANT_KEY_PATH, ENCRYPTION_PROTOCOL_URI } from './constants.js';
13
14
  import { getRoleAudienceContextId, getRuleSetAtPath, getTypeName, parseCrossProtocolRef } from '../utils/protocols.js';
14
- import { ProtocolAction, ProtocolRecordLimitStrategy } from '../types/protocols-types.js';
15
15
 
16
16
  type ResolvedRoleAudience = {
17
17
  contextId: string;
@@ -526,38 +526,6 @@ export async function verifyAsRoleRecordIfNeeded(
526
526
  }
527
527
  }
528
528
 
529
- /**
530
- * Verifies that the `$recordLimit` strategy is supported for a new record creation.
531
- *
532
- * This check only applies to initial writes (new records). Updates to existing records do not
533
- * affect record-limit occupancy. The supported `reject` strategy admits every candidate and
534
- * projects the visible occupant set at read time, so validation stays independent of arrival order.
535
- *
536
- * @throws {DwnError} with `ProtocolAuthorizationRecordLimitStrategyNotImplemented` if strategy is not implemented.
537
- */
538
- export async function verifyRecordLimit(
539
- incomingMessage: RecordsWrite,
540
- ruleSet: ProtocolRuleSet,
541
- ): Promise<void> {
542
- if (ruleSet.$recordLimit === undefined) {
543
- return;
544
- }
545
-
546
- // Only initial writes can introduce a new record-limit candidate.
547
- const isInitialWrite = await incomingMessage.isInitialWrite();
548
- if (!isInitialWrite) {
549
- return;
550
- }
551
-
552
- const { strategy } = ruleSet.$recordLimit;
553
- if (strategy !== ProtocolRecordLimitStrategy.Reject) {
554
- throw new DwnError(
555
- DwnErrorCode.ProtocolAuthorizationRecordLimitStrategyNotImplemented,
556
- `record limit strategy '${strategy}' is not yet implemented.`
557
- );
558
- }
559
- }
560
-
561
529
  /**
562
530
  * Verifies that a `RecordsWrite` with `squash: true` is eligible:
563
531
  * 1. The protocol rule set at the record's `protocolPath` must have `$squash: true`.
@@ -19,7 +19,6 @@ import {
19
19
  verifyAsRoleRecordIfNeeded,
20
20
  verifyImmutability,
21
21
  verifyProtocolPathAndContextId,
22
- verifyRecordLimit,
23
22
  verifySizeLimit,
24
23
  verifySquashEligibility,
25
24
  verifyTagsIfNeeded,
@@ -88,9 +87,6 @@ export class ProtocolAuthorization {
88
87
  // Verify squash eligibility — ensure squash writes are at $squash: true paths and are initial writes
89
88
  await verifySquashEligibility(incomingMessage, ruleSet);
90
89
 
91
- // Verify record count limit
92
- await verifyRecordLimit(incomingMessage, ruleSet);
93
-
94
90
  return ruleSet;
95
91
  }
96
92
 
@@ -142,9 +138,6 @@ export class ProtocolAuthorization {
142
138
  verifyTagsIfNeeded(incomingMessage, ruleSet);
143
139
  await verifySquashEligibility(incomingMessage, ruleSet);
144
140
  ProtocolAuthorization.verifyStoredInitialWriteCreateAction(tenant, incomingMessage, ruleSet);
145
-
146
- // `verifyRecordLimit()` is intentionally not replayed here. It only checks strategy
147
- // support for new record candidates; read-time occupancy projection decides visibility.
148
141
  }
149
142
 
150
143
  /**
@@ -8,7 +8,6 @@ import type { DataEncodedRecordsWriteMessage, RecordsWriteMessage } from '../typ
8
8
  import type { ProtocolDefinition, ProtocolsConfigureMessage } from '../types/protocols-types.js';
9
9
 
10
10
  import { ENCRYPTION_CONTROL_AUDIENCE_PATH } from './constants.js';
11
- import { FilterUtility } from '../utils/filter.js';
12
11
  import { PermissionGrant } from '../protocols/permission-grant.js';
13
12
  import { PermissionsProtocol } from '../protocols/permissions.js';
14
13
  import { RecordsWrite } from '../interfaces/records-write.js';
@@ -313,7 +312,7 @@ export class StoreValidationStateReader implements ValidationStateReader {
313
312
  };
314
313
 
315
314
  if (input.contextIdPrefix !== undefined) {
316
- filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(input.contextIdPrefix);
315
+ filter.contextId = { subtree: input.contextIdPrefix };
317
316
  }
318
317
 
319
318
  const { messages } = await this.messageStore.query(
@@ -370,7 +369,7 @@ export class StoreValidationStateReader implements ValidationStateReader {
370
369
  }
371
370
 
372
371
  if (input.contextIdPrefix !== undefined) {
373
- filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(input.contextIdPrefix);
372
+ filter.contextId = { subtree: input.contextIdPrefix };
374
373
  }
375
374
 
376
375
  return filter;
@@ -1,4 +1,4 @@
1
- import type { EventLogEntry, ProgressGapInfo, ReplicationFeedReader } from '../types/subscriptions.js';
1
+ import type { EventLogEntry, ProgressGapInfo } from '../types/subscriptions.js';
2
2
  import type { Filter, KeyValues } from '../types/query-types.js';
3
3
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
4
4
  import type { MessagesFilter, MessagesQueryMessage, MessagesQueryReply, MessagesQueryReplyEntry } from '../types/messages-types.js';
@@ -31,7 +31,7 @@ export class MessagesQueryHandler implements MethodHandler {
31
31
  return messageReplyFromError(e, 401);
32
32
  }
33
33
 
34
- const replicationFeedReader = MessagesQueryHandler.asReplicationFeedReader(this.deps.messageStore);
34
+ const replicationFeedReader = Replication.asFeedReader(this.deps.messageStore);
35
35
  if (replicationFeedReader === undefined) {
36
36
  return {
37
37
  status: {
@@ -56,7 +56,7 @@ export class MessagesQueryHandler implements MethodHandler {
56
56
  drained : result.drained,
57
57
  };
58
58
 
59
- const fingerprintScopes = MessagesQueryHandler.computeFingerprintScopes(message.descriptor.filters);
59
+ const fingerprintScopes = Messages.computeFingerprintScopes(message.descriptor.filters);
60
60
  if (fingerprintScopes !== undefined) {
61
61
  reply.fingerprint = await replicationFeedReader.fingerprint(tenant, fingerprintScopes);
62
62
  }
@@ -87,18 +87,6 @@ export class MessagesQueryHandler implements MethodHandler {
87
87
  });
88
88
  }
89
89
 
90
- private static asReplicationFeedReader(candidate: unknown): ReplicationFeedReader | undefined {
91
- const partial = candidate as Partial<ReplicationFeedReader>;
92
- if (
93
- typeof partial.logRead === 'function' &&
94
- typeof partial.logBounds === 'function' &&
95
- typeof partial.fingerprint === 'function' &&
96
- typeof partial.epoch === 'function'
97
- ) {
98
- return partial as ReplicationFeedReader;
99
- }
100
- }
101
-
102
90
  private static convertFilters(filters: MessagesFilter[], deps: HandlerDependencies): Filter[] | undefined {
103
91
  if (filters.length === 0) {
104
92
  return undefined;
@@ -155,32 +143,6 @@ export class MessagesQueryHandler implements MethodHandler {
155
143
  return indexes.isLatestBaseState === true || indexes.isLatestBaseState === 'true';
156
144
  }
157
145
 
158
- private static computeFingerprintScopes(filters: MessagesFilter[]): string[] | undefined {
159
- if (filters.length === 0) {
160
- return [Replication.globalDomain];
161
- }
162
-
163
- const protocols = new Set<string>();
164
- for (const filter of filters) {
165
- const keys = Object.keys(filter);
166
- if (keys.length !== 1 || typeof filter.protocol !== 'string') {
167
- return undefined;
168
- }
169
-
170
- protocols.add(filter.protocol);
171
- }
172
-
173
- const scopes: string[] = [];
174
- for (const protocol of protocols) {
175
- scopes.push(
176
- Replication.protocolDomain(protocol),
177
- ...Replication.taggedCoreProtocolDomains(protocol, protocols),
178
- );
179
- }
180
-
181
- return scopes;
182
- }
183
-
184
146
  private static getProgressGapInfo(error: DwnError): ProgressGapInfo | undefined {
185
147
  const gapInfo = (error as DwnError & { gapInfo?: ProgressGapInfo }).gapInfo;
186
148
  return gapInfo;
@@ -1,7 +1,7 @@
1
1
  import type { PermissionGrant } from '../protocols/permission-grant.js';
2
- import type { EventSubscription, ProgressGapInfo, SubscriptionEvent, SubscriptionListener, SubscriptionMessage } from '../types/subscriptions.js';
2
+ import type { EventSubscription, ProgressGapInfo, ProgressToken, ReplicationFeedReader, SubscriptionEvent, SubscriptionListener, SubscriptionMessage } from '../types/subscriptions.js';
3
3
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
4
- import type { MessagesSubscribeMessage, MessagesSubscribeReply } from '../types/messages-types.js';
4
+ import type { MessagesFilter, MessagesSubscribeMessage, MessagesSubscribeReply } from '../types/messages-types.js';
5
5
 
6
6
  import { authenticate } from '../core/auth.js';
7
7
  import { Message } from '../core/message.js';
@@ -9,6 +9,7 @@ import { messageReplyFromError } from '../core/message-reply.js';
9
9
  import { Messages } from '../utils/messages.js';
10
10
  import { MessagesGrantAuthorization } from '../core/messages-grant-authorization.js';
11
11
  import { MessagesSubscribe } from '../interfaces/messages-subscribe.js';
12
+ import { Replication } from '../utils/replication.js';
12
13
  import { Time } from '../utils/time.js';
13
14
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
14
15
 
@@ -79,10 +80,20 @@ export class MessagesSubscribeHandler implements MethodHandler {
79
80
  });
80
81
  await guardedHandler.setSubscription(subscription);
81
82
 
82
- return {
83
+ const reply: MessagesSubscribeReply = {
83
84
  status: { code: 200, detail: 'OK' },
84
85
  subscription,
85
86
  };
87
+ try {
88
+ await MessagesSubscribeHandler.attachFeedSnapshot(reply, tenant, filters, this.deps);
89
+ } catch {
90
+ // Best-effort enrichment: the subscription is live and correct
91
+ // without the snapshot — consumers feature-detect the fields. A
92
+ // failure reply here would orphan the just-installed listener, since
93
+ // close handles are registered only from successful replies.
94
+ }
95
+
96
+ return reply;
86
97
  } catch (error) {
87
98
  if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
88
99
  const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
@@ -95,6 +106,59 @@ export class MessagesSubscribeHandler implements MethodHandler {
95
106
  }
96
107
  }
97
108
 
109
+ /**
110
+ * Attaches the replication feed's `head` token and scope `fingerprint` to a
111
+ * successful subscribe reply, when the message store exposes the feed.
112
+ *
113
+ * Ordering is load-bearing: both values are observed AFTER the subscription
114
+ * became active, so every event past `head` is either replayable from it or
115
+ * already flowing on the live stream — a caller that verifies `fingerprint`
116
+ * against its local feed may adopt `head` as a checkpoint without a delivery
117
+ * gap. The head is captured before the fingerprint, mirroring MessagesQuery's
118
+ * cursor-then-fingerprint order: an event landing between the two captures
119
+ * makes the fingerprint read ahead of the head, which a consumer observes as
120
+ * a mismatch (one redundant reconcile), never as a silently skipped event.
121
+ */
122
+ private static async attachFeedSnapshot(
123
+ reply: MessagesSubscribeReply,
124
+ tenant: string,
125
+ filters: MessagesFilter[],
126
+ deps: HandlerDependencies,
127
+ ): Promise<void> {
128
+ const feedReader = Replication.asFeedReader(deps.messageStore);
129
+ if (feedReader === undefined) {
130
+ return;
131
+ }
132
+
133
+ // Build the complete snapshot before attaching anything: a partial
134
+ // snapshot (a head without its fingerprint) would read to consumers as
135
+ // a fingerprint-less server rather than as a failed capture.
136
+ const bounds = await feedReader.logBounds(tenant);
137
+ const head = bounds?.latest ?? await MessagesSubscribeHandler.buildAnchorToken(tenant, feedReader);
138
+
139
+ const fingerprintScopes = Messages.computeFingerprintScopes(filters);
140
+ const fingerprint = fingerprintScopes === undefined
141
+ ? undefined
142
+ : await feedReader.fingerprint(tenant, fingerprintScopes);
143
+
144
+ reply.head = head;
145
+ if (fingerprint !== undefined) {
146
+ reply.fingerprint = fingerprint;
147
+ }
148
+ }
149
+
150
+ /** Builds the position-zero anchor token for a tenant log with no events. */
151
+ private static async buildAnchorToken(
152
+ tenant: string,
153
+ feedReader: ReplicationFeedReader,
154
+ ): Promise<ProgressToken> {
155
+ return {
156
+ streamId : await Replication.deriveStreamId(tenant),
157
+ epoch : await feedReader.epoch(),
158
+ position : '0',
159
+ };
160
+ }
161
+
98
162
  private static async authorizeMessagesSubscribe(
99
163
  tenant: string,
100
164
  messagesSubscribe: MessagesSubscribe,
@@ -1,4 +1,5 @@
1
1
  import type { Filter } from '../types/query-types.js';
2
+ import type { RecordLimitOccupancy } from '../types/message-store.js';
2
3
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
3
4
  import type { RecordsCountMessage, RecordsCountReply } from '../types/records-types.js';
4
5
 
@@ -10,7 +11,11 @@ import { ProtocolAuthorization } from '../core/protocol-authorization.js';
10
11
  import { Records } from '../utils/records.js';
11
12
  import { RecordsCount } from '../interfaces/records-count.js';
12
13
  import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
13
- import { countRecordsWithRecordLimitOccupancy, queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
14
+ import {
15
+ countRecordsWithRecordLimitOccupancy,
16
+ queryRecordsWithRecordLimitOccupancy,
17
+ resolveRecordLimitOccupancy,
18
+ } from '../utils/record-limit-occupancy.js';
14
19
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
15
20
 
16
21
  export class RecordsCountHandler implements MethodHandler {
@@ -71,7 +76,8 @@ export class RecordsCountHandler implements MethodHandler {
71
76
  isLatestBaseState : true
72
77
  };
73
78
 
74
- return this.countProjectedRecords(tenant, recordsCount, [countFilter]);
79
+ const recordLimit = await this.resolveRecordLimit(tenant, recordsCount);
80
+ return this.countProjectedRecords(tenant, [countFilter], recordLimit);
75
81
  }
76
82
 
77
83
  private async countRecordsAsOwnerDelegate(
@@ -142,13 +148,16 @@ export class RecordsCountHandler implements MethodHandler {
142
148
  return this.countProjectedRecordsForRequester(tenant, recordsCount, requester, [filter]);
143
149
  }
144
150
 
145
- private async countProjectedRecords(tenant: string, recordsCount: RecordsCount, filters: Filter[]): Promise<number> {
151
+ private async countProjectedRecords(
152
+ tenant: string,
153
+ filters: Filter[],
154
+ recordLimit: RecordLimitOccupancy | undefined,
155
+ ): Promise<number> {
146
156
  const totalCount = await countRecordsWithRecordLimitOccupancy({
147
- messageStore : this.deps.messageStore,
148
- validationStateReader : this.deps.validationStateReader,
157
+ messageStore: this.deps.messageStore,
149
158
  tenant,
150
159
  filters,
151
- messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
160
+ recordLimit,
152
161
  });
153
162
 
154
163
  const audienceFilters = EncryptionControl.buildAudienceRecordFilters(filters);
@@ -178,23 +187,23 @@ export class RecordsCountHandler implements MethodHandler {
178
187
  requester: string | undefined,
179
188
  filters: Filter[],
180
189
  ): Promise<number> {
190
+ const recordLimit = await this.resolveRecordLimit(tenant, recordsCount);
181
191
  const controlFilters = Records.buildControlRecordsFilters(filters);
182
192
  if (controlFilters.length === 0) {
183
- return this.countProjectedRecords(tenant, recordsCount, filters);
193
+ return this.countProjectedRecords(tenant, filters, recordLimit);
184
194
  }
185
195
 
186
- const totalCount = await this.countProjectedRecords(tenant, recordsCount, filters);
187
- const controlCount = await this.countProjectedRecords(tenant, recordsCount, controlFilters);
196
+ const totalCount = await this.countProjectedRecords(tenant, filters, recordLimit);
197
+ const controlCount = await this.countProjectedRecords(tenant, controlFilters, recordLimit);
188
198
  if (controlCount === 0) {
189
199
  return totalCount;
190
200
  }
191
201
 
192
202
  const { messages } = await queryRecordsWithRecordLimitOccupancy({
193
- messageStore : this.deps.messageStore,
194
- validationStateReader : this.deps.validationStateReader,
203
+ messageStore : this.deps.messageStore,
195
204
  tenant,
196
- filters : controlFilters,
197
- messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
205
+ filters : controlFilters,
206
+ recordLimit,
198
207
  });
199
208
  const projectedMessages = await EncryptionControl.projectCurrentAudienceRecords({
200
209
  messageStore : this.deps.messageStore,
@@ -212,6 +221,18 @@ export class RecordsCountHandler implements MethodHandler {
212
221
  return totalCount - controlCount + visibleMessages.length;
213
222
  }
214
223
 
224
+ private async resolveRecordLimit(
225
+ tenant: string,
226
+ recordsCount: RecordsCount,
227
+ ): Promise<RecordLimitOccupancy | undefined> {
228
+ return resolveRecordLimitOccupancy({
229
+ validationStateReader : this.deps.validationStateReader,
230
+ tenant,
231
+ recordsFilter : recordsCount.message.descriptor.filter,
232
+ messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
233
+ });
234
+ }
235
+
215
236
  private static buildPublishedRecordsFilter(recordsCount: RecordsCount): Filter {
216
237
  const { filter } = recordsCount.message.descriptor;
217
238
  return {
@@ -10,12 +10,12 @@ import { EncryptionControl } from '../core/encryption-control.js';
10
10
  import { Message } from '../core/message.js';
11
11
  import { messageReplyFromError } from '../core/message-reply.js';
12
12
  import { ProtocolAuthorization } from '../core/protocol-authorization.js';
13
- import { queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
14
13
  import { Records } from '../utils/records.js';
15
14
  import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
16
15
  import { RecordsQuery } from '../interfaces/records-query.js';
17
16
  import { SortDirection } from '../types/query-types.js';
18
17
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
18
+ import { queryRecordsWithRecordLimitOccupancy, resolveRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
19
19
 
20
20
  type RecordsQueryProjectionInput = {
21
21
  tenant: string;
@@ -254,17 +254,22 @@ export class RecordsQueryHandler implements MethodHandler {
254
254
  const {
255
255
  tenant, recordsQuery, requester, filters, messageSort, pagination
256
256
  } = input;
257
+ const recordLimit = await resolveRecordLimitOccupancy({
258
+ validationStateReader : this.deps.validationStateReader,
259
+ tenant,
260
+ recordsFilter : recordsQuery.message.descriptor.filter,
261
+ messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
262
+ });
257
263
  const controlFilters = Records.buildControlRecordsFilters(filters);
258
264
  const currentAudienceRecordIdCache = new Map<string, string | undefined>();
259
265
  if (controlFilters.length === 0) {
260
266
  const result = await queryRecordsWithRecordLimitOccupancy({
261
- messageStore : this.deps.messageStore,
262
- validationStateReader : this.deps.validationStateReader,
267
+ messageStore: this.deps.messageStore,
263
268
  tenant,
264
269
  filters,
270
+ recordLimit,
265
271
  messageSort,
266
272
  pagination,
267
- messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
268
273
  });
269
274
  return EncryptionControl.projectCurrentAudienceRecordPage({
270
275
  messageStore : this.deps.messageStore,
@@ -280,13 +285,12 @@ export class RecordsQueryHandler implements MethodHandler {
280
285
 
281
286
  if (pagination?.limit === undefined || pagination.limit <= 0) {
282
287
  const result = await queryRecordsWithRecordLimitOccupancy({
283
- messageStore : this.deps.messageStore,
284
- validationStateReader : this.deps.validationStateReader,
288
+ messageStore: this.deps.messageStore,
285
289
  tenant,
286
290
  filters,
291
+ recordLimit,
287
292
  messageSort,
288
293
  pagination,
289
- messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
290
294
  });
291
295
  const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
292
296
  messageStore : this.deps.messageStore,
@@ -311,13 +315,12 @@ export class RecordsQueryHandler implements MethodHandler {
311
315
  do {
312
316
  const remainingLimit = pagination.limit - visibleMessages.length;
313
317
  const result = await queryRecordsWithRecordLimitOccupancy({
314
- messageStore : this.deps.messageStore,
315
- validationStateReader : this.deps.validationStateReader,
318
+ messageStore : this.deps.messageStore,
316
319
  tenant,
317
320
  filters,
321
+ recordLimit,
318
322
  messageSort,
319
- pagination : { ...pagination, cursor, limit: remainingLimit },
320
- messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
323
+ pagination : { ...pagination, cursor, limit: remainingLimit },
321
324
  });
322
325
  const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
323
326
  messageStore : this.deps.messageStore,
@@ -17,7 +17,11 @@ import { RecordsSubscribe } from '../interfaces/records-subscribe.js';
17
17
  import { SortDirection } from '../types/query-types.js';
18
18
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
19
19
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
20
- import { isRecordLimitOccupant, queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
20
+ import {
21
+ isRecordLimitOccupant,
22
+ queryRecordsWithRecordLimitOccupancy,
23
+ resolveRecordLimitOccupancy,
24
+ } from '../utils/record-limit-occupancy.js';
21
25
 
22
26
  type ProjectedRecordsSubscriptionHandler = {
23
27
  listener: SubscriptionListener;
@@ -604,17 +608,22 @@ export class RecordsSubscribeHandler implements MethodHandler {
604
608
  messageSort: MessageSort,
605
609
  pagination: { cursor?: PaginationCursor; limit?: number } | undefined,
606
610
  ): Promise<{ messages: RecordsQueryReplyEntry[], cursor?: PaginationCursor }> {
611
+ const recordLimit = await resolveRecordLimitOccupancy({
612
+ validationStateReader : this.deps.validationStateReader,
613
+ tenant,
614
+ recordsFilter : recordsSubscribe.message.descriptor.filter,
615
+ messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
616
+ });
607
617
  const controlFilters = Records.buildControlRecordsFilters(filters);
608
618
  const currentAudienceRecordIdCache = new Map<string, string | undefined>();
609
619
  if (controlFilters.length === 0) {
610
620
  const result = await queryRecordsWithRecordLimitOccupancy({
611
- messageStore : this.deps.messageStore,
612
- validationStateReader : this.deps.validationStateReader,
621
+ messageStore: this.deps.messageStore,
613
622
  tenant,
614
623
  filters,
624
+ recordLimit,
615
625
  messageSort,
616
626
  pagination,
617
- messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
618
627
  });
619
628
  return EncryptionControl.projectCurrentAudienceRecordPage({
620
629
  messageStore: this.deps.messageStore,
@@ -627,13 +636,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
627
636
 
628
637
  if (pagination?.limit === undefined || pagination.limit <= 0) {
629
638
  const result = await queryRecordsWithRecordLimitOccupancy({
630
- messageStore : this.deps.messageStore,
631
- validationStateReader : this.deps.validationStateReader,
639
+ messageStore: this.deps.messageStore,
632
640
  tenant,
633
641
  filters,
642
+ recordLimit,
634
643
  messageSort,
635
644
  pagination,
636
- messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
637
645
  });
638
646
  const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
639
647
  messageStore: this.deps.messageStore,
@@ -655,13 +663,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
655
663
  do {
656
664
  const remainingLimit = pagination.limit - visibleMessages.length;
657
665
  const result = await queryRecordsWithRecordLimitOccupancy({
658
- messageStore : this.deps.messageStore,
659
- validationStateReader : this.deps.validationStateReader,
666
+ messageStore : this.deps.messageStore,
660
667
  tenant,
661
668
  filters,
669
+ recordLimit,
662
670
  messageSort,
663
- pagination : { ...pagination, cursor, limit: remainingLimit },
664
- messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
671
+ pagination : { ...pagination, cursor, limit: remainingLimit },
665
672
  });
666
673
  const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
667
674
  messageStore: this.deps.messageStore,
@@ -1,5 +1,6 @@
1
1
  import type { CoreProtocol } from '../core/core-protocol.js';
2
2
  import type { ProgressToken } from '../types/subscriptions.js';
3
+ import type { ProtocolRuleSet } from '../types/protocols-types.js';
3
4
  import type { GenericMessage, GenericMessageReply } from '../types/message-types.js';
4
5
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
5
6
  import type { RecordsQueryReplyEntry, RecordsWriteMessage } from '../types/records-types.js';
@@ -64,8 +65,13 @@ export class RecordsWriteHandler implements MethodHandler {
64
65
  return messageReplyFromError(e, 400);
65
66
  }
66
67
 
68
+ let protocolRuleSet: ProtocolRuleSet;
67
69
  try {
68
- await ProtocolAuthorization.validateReferentialIntegrity(tenant, recordsWrite, this.deps.validationStateReader);
70
+ protocolRuleSet = await ProtocolAuthorization.validateReferentialIntegrity(
71
+ tenant,
72
+ recordsWrite,
73
+ this.deps.validationStateReader,
74
+ );
69
75
  } catch (e) {
70
76
  return messageReplyFromError(e, 400);
71
77
  }
@@ -87,7 +93,7 @@ export class RecordsWriteHandler implements MethodHandler {
87
93
  // messageTimestamp is <= the most recent squash record at the same path and parent context.
88
94
  // The squash record acts as a temporal floor — no record older than the latest squash can exist.
89
95
  try {
90
- await this.enforceSquashBackstop(tenant, message);
96
+ await this.enforceSquashBackstop(tenant, message, protocolRuleSet);
91
97
  } catch (e) {
92
98
  return messageReplyFromError(e, 409);
93
99
  }
@@ -445,38 +451,17 @@ export class RecordsWriteHandler implements MethodHandler {
445
451
  * and there exists a squash record at the same protocol path and parent context whose
446
452
  * `messageTimestamp` is >= the incoming message's `messageTimestamp`, reject with 409.
447
453
  *
454
+ * The rule set is the exact policy already resolved by referential-integrity validation. Reusing
455
+ * it prevents a second store lookup from observing different state or failing open.
456
+ *
448
457
  * This check only applies to protocol-based records at `$squash: true` paths.
449
458
  */
450
- private async enforceSquashBackstop(tenant: string, message: RecordsWriteMessage): Promise<void> {
451
- // Only applies to protocol-based records
452
- if (message.descriptor.protocol === undefined || message.descriptor.protocolPath === undefined) {
453
- return;
454
- }
455
-
456
- // Fetch the protocol definition active at the incoming message timestamp to check if $squash is enabled at this path.
457
- // The reader resolves core protocols (e.g. permissions) from the registry.
458
- let protocolDefinition;
459
- try {
460
- protocolDefinition = await this.deps.validationStateReader.fetchProtocolDefinition(
461
- tenant,
462
- message.descriptor.protocol,
463
- message.descriptor.messageTimestamp,
464
- );
465
- } catch (error) {
466
- // If the protocol definition can't be found, skip the backstop check.
467
- // Authorization will handle the missing protocol error later.
468
- console.warn(`enforceSquashBackstop: failed to fetch protocol definition for '${message.descriptor.protocol}':`, error);
469
- return;
470
- }
471
-
472
- // Walk the structure to find the rule set for this protocol path
473
- const pathSegments = message.descriptor.protocolPath.split('/');
474
- let ruleSet = protocolDefinition.structure[pathSegments[0]];
475
- for (let i = 1; i < pathSegments.length && ruleSet !== undefined; i++) {
476
- ruleSet = ruleSet[pathSegments[i]] as typeof ruleSet;
477
- }
478
-
479
- if (ruleSet?.$squash !== true) {
459
+ private async enforceSquashBackstop(
460
+ tenant: string,
461
+ message: RecordsWriteMessage,
462
+ ruleSet: ProtocolRuleSet,
463
+ ): Promise<void> {
464
+ if (ruleSet.$squash !== true) {
480
465
  return;
481
466
  }
482
467
 
package/src/index.ts CHANGED
@@ -9,9 +9,8 @@ export { StoreValidationStateReader } from './core/validation-state-reader.js';
9
9
  export type { RecordedValidationRead } from './core/recording-validation-state-reader.js';
10
10
  export { RecordingValidationStateReader } from './core/recording-validation-state-reader.js';
11
11
  export type { MessagesFilter, MessagesQueryDescriptor, MessagesQueryMessage, MessagesQueryReply, MessagesQueryReplyEntry, MessagesReadMessage, MessagesReadReply, MessagesReadReplyEntry, MessagesReadDescriptor, MessagesSubscribeDescriptor, MessagesSubscribeMessage, MessagesSubscribeReply, MessagesSubscribeMessageOptions } from './types/messages-types.js';
12
- export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter } from './types/query-types.js';
12
+ export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter, SubtreeFilter } from './types/query-types.js';
13
13
  export type { ProtocolsConfigureDescriptor, ProtocolDefinition, ProtocolTypes, ProtocolRuleSet, ProtocolsQueryFilter, ProtocolsConfigureMessage, ProtocolsQueryMessage, ProtocolsQueryReply, ProtocolActionRule, ProtocolDeliveryStrategy, ProtocolKeyAgreement, ProtocolsQueryDescriptor, ProtocolRecordLimitDefinition, ProtocolSizeDefinition, ProtocolTagsDefinition, ProtocolTagSchema, ProtocolType, ProtocolUses } from './types/protocols-types.js';
14
- export { ProtocolRecordLimitStrategy } from './types/protocols-types.js';
15
14
  export type { DataEncodedRecordsWriteMessage, RecordsCountDescriptor, RecordsCountMessage, RecordsCountReply, RecordsDeleteMessage, RecordsFilter, RecordsQueryMessage, RecordsQueryReply, RecordsQueryReplyEntry, RecordsReadMessage, RecordsReadReply, RecordsSubscribeDescriptor, RecordsSubscribeMessage, RecordsSubscribeReply, RecordsWriteDescriptor, RecordsWriteTags, RecordsWriteTagValue, RecordsWriteMessage, RecordsWriteSignaturePayload, RecordsDeleteDescriptor, RecordsQueryDescriptor, RecordsReadDescriptor, RecordsSubscribeMessageOptions, RecordsWriteMessageOptions, InternalRecordsWriteMessage, RecordEvent, RecordsWriteTagsFilter } from './types/records-types.js';
16
15
  export type { GeneralJws, SignatureEntry } from './types/jws-types.js';
17
16
  export { authenticate } from './core/auth.js';
@@ -53,6 +52,7 @@ export { DwnError, DwnErrorCode } from './core/dwn-error.js';
53
52
  export type { DwnErrorInfo } from './core/dwn-error.js';
54
53
  export { DwnInterfaceName, DwnMethodName } from './enums/dwn-interface-method.js';
55
54
  export { Encoder } from './utils/encoder.js';
55
+ export { assertValidSubtreeFilters, isSubtreeFilter } from './utils/filter.js';
56
56
  export { MessagesSubscribe } from './interfaces/messages-subscribe.js';
57
57
  export type { MessagesSubscribeOptions } from './interfaces/messages-subscribe.js';
58
58
  export { Encryption, ContentEncryptionAlgorithm, KeyAgreementAlgorithm, ROLE_AUDIENCE_DERIVATION_SCHEME, SEAL_DERIVATION_SCHEME } from './utils/encryption.js';
@@ -82,7 +82,7 @@ export type { MessagesReadOptions } from './interfaces/messages-read.js';
82
82
  export { MessagesQuery } from './interfaces/messages-query.js';
83
83
  export type { MessagesQueryOptions } from './interfaces/messages-query.js';
84
84
  export type { UnionMessageReply } from './core/message-reply.js';
85
- export type { MessageStore, MessageStoreLatestStateTransition, MessageStoreOptions, MessageStorePutResult } from './types/message-store.js';
85
+ export type { MessageStore, MessageStoreLatestStateTransition, MessageStoreOptions, MessageStorePutResult, MessageStoreQueryOptions, RecordLimitOccupancy } from './types/message-store.js';
86
86
  export { Replication } from './utils/replication.js';
87
87
  export type { MessageInterface } from './types/message-interface.js';
88
88
  export { PermissionGrant } from './protocols/permission-grant.js';