@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
@@ -6,6 +6,29 @@ export interface MessageStoreOptions {
6
6
  signal?: AbortSignal;
7
7
  }
8
8
 
9
+ /**
10
+ * Read-time `$recordLimit` occupancy policy for one concrete protocol path.
11
+ *
12
+ * Stores partition matching latest RecordsWrites by `parentId`, rank each
13
+ * partition by `dateCreated` then `recordId` ascending, and apply `max`
14
+ * before the caller's filters, sort, and pagination.
15
+ */
16
+ export type RecordLimitOccupancy = {
17
+ protocol: string;
18
+ protocolPath: string;
19
+ /**
20
+ * Root of the candidate context subtree. An exact-record request supplies
21
+ * its direct-parent context here so sibling candidates still consume slots.
22
+ * Omitted for root protocol paths.
23
+ */
24
+ contextId?: string;
25
+ max: number;
26
+ };
27
+
28
+ export interface MessageStoreQueryOptions extends MessageStoreOptions {
29
+ recordLimit?: RecordLimitOccupancy;
30
+ }
31
+
9
32
  /**
10
33
  * Result of a {@link MessageStore.put} operation.
11
34
  */
@@ -98,7 +121,7 @@ export interface MessageStore {
98
121
  filters: Filter[],
99
122
  messageSort?: MessageSort,
100
123
  pagination?: Pagination,
101
- options?: MessageStoreOptions
124
+ options?: MessageStoreQueryOptions
102
125
  ): Promise<{ messages: GenericMessage[], cursor?: PaginationCursor}>;
103
126
 
104
127
  /**
@@ -109,7 +132,7 @@ export interface MessageStore {
109
132
  tenant: string,
110
133
  filters: Filter[],
111
134
  messageSort?: MessageSort,
112
- options?: MessageStoreOptions
135
+ options?: MessageStoreQueryOptions
113
136
  ): Promise<number>;
114
137
 
115
138
  /**
@@ -87,6 +87,20 @@ export type MessagesSubscribeMessage = {
87
87
 
88
88
  export type MessagesSubscribeReply = GenericMessageReply & {
89
89
  subscription?: MessageSubscription;
90
+ /**
91
+ * Feed fingerprint over the subscription's filter scopes, observed after the
92
+ * subscription became active. Present only when the filters map onto
93
+ * fingerprint domains and the message store exposes the replication feed.
94
+ */
95
+ fingerprint?: string;
96
+ /**
97
+ * High-water progress token of the tenant's replication log, observed after
98
+ * the subscription became active — the position-zero anchor when the log is
99
+ * empty. NOT a delivery cursor: adopting it as a checkpoint is sound only
100
+ * after verifying `fingerprint` against the local feed, since resuming from
101
+ * it skips every earlier log position.
102
+ */
103
+ head?: ProgressToken;
90
104
  /** Present when status.code is 410 — structured gap metadata. */
91
105
  error?: { code: 'ProgressGap' } & ProgressGapInfo;
92
106
  };
@@ -149,33 +149,15 @@ export type ProtocolSizeDefinition = {
149
149
  };
150
150
 
151
151
  /**
152
- * Supported strategies for handling writes that would exceed a `$recordLimit`.
153
- */
154
- export enum ProtocolRecordLimitStrategy {
155
- /** Reject the incoming write with an error. The author must delete old records first. */
156
- Reject = 'reject',
157
- /** Automatically delete the oldest record(s) (by `dateCreated`) to make room for the new one. */
158
- PurgeOldest = 'purgeOldest',
159
- }
160
-
161
- /**
162
- * Limits the number of records at a given protocol path within the same parent context.
152
+ * Limits the visible records at a given protocol path within the same parent context.
163
153
  *
164
- * For root-level records, the count is across the entire protocol for the tenant.
165
- * For nested records, the count is scoped to the parent record's context.
166
- *
167
- * Only initial writes (new records) count toward the limit; updates to existing records do not.
154
+ * Root-level records form one group for the protocol path. Nested records are grouped by
155
+ * direct parent context. Occupants are selected deterministically at read time; valid writes
156
+ * outside the visible population remain stored and may become visible later.
168
157
  */
169
158
  export type ProtocolRecordLimitDefinition = {
170
- /** Maximum number of records allowed at this path within the same parent context. Must be >= 1. */
159
+ /** Maximum number of visible records at this path within the same parent context. Must be >= 1. */
171
160
  max: number;
172
-
173
- /**
174
- * Strategy when the limit is reached.
175
- * Currently only `'reject'` is implemented. Future strategies (e.g. `'purgeOldest'`)
176
- * are defined in the wire format but not yet enforced.
177
- */
178
- strategy: ProtocolRecordLimitStrategy | `${ProtocolRecordLimitStrategy}` | (string & {});
179
161
  };
180
162
 
181
163
  /**
@@ -287,9 +269,8 @@ export type ProtocolRuleSet = {
287
269
  $tags?: ProtocolTagsDefinition;
288
270
 
289
271
  /**
290
- * If $recordLimit is set, the number of records at this protocol path within the same
291
- * parent context is constrained. When the limit is reached, the `strategy` determines
292
- * whether the write is rejected or the oldest record is purged.
272
+ * If `$recordLimit` is set, reads expose at most `max` records at this protocol path
273
+ * within each direct parent context.
293
274
  */
294
275
  $recordLimit?: ProtocolRecordLimitDefinition;
295
276
 
@@ -37,7 +37,15 @@ export type StartsWithFilter = {
37
37
  startsWith: string;
38
38
  };
39
39
 
40
- export type FilterValue = EqualFilter | OneOfFilter | RangeFilter;
40
+ /**
41
+ * Selects one hierarchical path and every proper `/`-delimited descendant.
42
+ * Supported only for the `contextId` and `protocolPath` indexes.
43
+ */
44
+ export type SubtreeFilter = {
45
+ subtree: string;
46
+ };
47
+
48
+ export type FilterValue = EqualFilter | OneOfFilter | RangeFilter | SubtreeFilter;
41
49
 
42
50
  export type Filter = {
43
51
  [property: string]: FilterValue;
@@ -58,4 +66,4 @@ export type RangeCriterion = {
58
66
  export type PaginationCursor = {
59
67
  messageCid: string;
60
68
  value: string | number;
61
- };
69
+ };
@@ -154,7 +154,8 @@ export type RecordsFilter = {
154
154
  published?: boolean;
155
155
 
156
156
  /**
157
- * When given all Records message under the context of the given `contextId` will be returned.
157
+ * Selects the exact context and its proper `/`-delimited descendants.
158
+ * Prefix-sharing sibling contexts are excluded.
158
159
  */
159
160
  contextId?: string;
160
161
  schema?: string;
@@ -1,4 +1,30 @@
1
- import type { EqualFilter, Filter, FilterValue, KeyValues, OneOfFilter, RangeCriterion, RangeFilter, RangeValue } from '../types/query-types.js';
1
+ import type { EqualFilter, Filter, FilterValue, KeyValues, OneOfFilter, RangeCriterion, RangeFilter, RangeValue, SubtreeFilter } from '../types/query-types.js';
2
+
3
+ const SUBTREE_FILTER_PROPERTIES = new Set(['contextId', 'protocolPath']);
4
+
5
+ /** Returns whether the value is a well-formed hierarchical subtree filter. */
6
+ export function isSubtreeFilter(filter: FilterValue): filter is SubtreeFilter {
7
+ return typeof filter === 'object' &&
8
+ filter !== null &&
9
+ !Array.isArray(filter) &&
10
+ Object.keys(filter).length === 1 &&
11
+ typeof (filter as Partial<SubtreeFilter>).subtree === 'string';
12
+ }
13
+
14
+ /**
15
+ * Rejects subtree filters for indexes without hierarchical path semantics.
16
+ *
17
+ * @throws {TypeError} If a subtree filter targets an index other than `contextId` or `protocolPath`.
18
+ */
19
+ export function assertValidSubtreeFilters(filters: Filter[]): void {
20
+ for (const filter of filters) {
21
+ for (const property in filter) {
22
+ if (isSubtreeFilter(filter[property]) && !SUBTREE_FILTER_PROPERTIES.has(property)) {
23
+ throw new TypeError(`SubtreeFilter is not supported for index '${property}'.`);
24
+ }
25
+ }
26
+ }
27
+ }
2
28
 
3
29
  /**
4
30
  * A Utility class to help match indexes against filters.
@@ -84,7 +110,9 @@ export class FilterUtility {
84
110
  if (this.matchOneOf(filterValue, indexValue)) {
85
111
  return true;
86
112
  }
87
- } else if (this.matchRange(filterValue, indexValue as RangeValue)) {
113
+ } else if (isSubtreeFilter(filterValue) && this.matchesSubtree(filterValue.subtree, indexValue)) {
114
+ return true;
115
+ } else if (this.isRangeFilter(filterValue) && this.matchRange(filterValue, indexValue as RangeValue)) {
88
116
  // `filterValue` is a `RangeFilter`
89
117
  // range filters cannot range over booleans
90
118
  return true;
@@ -181,6 +209,15 @@ export class FilterUtility {
181
209
  };
182
210
  }
183
211
 
212
+ /**
213
+ * Returns whether a candidate is the selected hierarchical path or one of
214
+ * its proper `/`-delimited descendants.
215
+ */
216
+ static matchesSubtree(subtree: string, candidate: unknown): candidate is string {
217
+ return typeof candidate === 'string' &&
218
+ (candidate === subtree || candidate.startsWith(`${subtree}/`));
219
+ }
220
+
184
221
  }
185
222
 
186
223
  export class FilterSelector {
@@ -238,4 +275,4 @@ export class FilterSelector {
238
275
  singlePropertyFilter[firstRemainingProperty] = filter[firstRemainingProperty];
239
276
  return singlePropertyFilter;
240
277
  }
241
- }
278
+ }
@@ -1,6 +1,7 @@
1
1
  import type { PermissionScope, RecordsPermissionScope } from '../types/permission-types.js';
2
2
  import type { ProtocolDefinition, ProtocolRuleSet } from '../types/protocols-types.js';
3
3
 
4
+ import { FilterUtility } from './filter.js';
4
5
  import { KeyDerivationScheme } from './hd-key.js';
5
6
  import { ProtocolAction } from '../types/protocols-types.js';
6
7
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
@@ -171,7 +172,7 @@ function readGrantKeyScopeCoversDeliveredScope(input: {
171
172
  return false;
172
173
  }
173
174
 
174
- if (isBoundaryAwareSubtree(grantProtocolPath, deliveredProtocolPath)) {
175
+ if (FilterUtility.matchesSubtree(grantProtocolPath, deliveredProtocolPath)) {
175
176
  return true;
176
177
  }
177
178
 
@@ -206,7 +207,7 @@ function writeGrantKeyScopeCoversDeliveredScope(input: {
206
207
  return true;
207
208
  }
208
209
 
209
- return isBoundaryAwareSubtree(grantProtocolPath, deliveredProtocolPath);
210
+ return FilterUtility.matchesSubtree(grantProtocolPath, deliveredProtocolPath);
210
211
  }
211
212
 
212
213
  function getGrantKeyReadRolePathsForScope(protocolDefinition: ProtocolDefinition, scopeProtocolPath: string): string[] {
@@ -257,7 +258,7 @@ function collectReadRolePaths(protocolDefinition: ProtocolDefinition, ruleSet: P
257
258
 
258
259
  function getGrantKeyRolePathsCoveredByScope(protocolDefinition: ProtocolDefinition, scopeProtocolPath?: string): string[] {
259
260
  return collectRolePaths(protocolDefinition, protocolDefinition.structure as ProtocolRuleSet)
260
- .filter((rolePath) => scopeProtocolPath === undefined || isBoundaryAwareSubtree(scopeProtocolPath, rolePath));
261
+ .filter((rolePath) => scopeProtocolPath === undefined || FilterUtility.matchesSubtree(scopeProtocolPath, rolePath));
261
262
  }
262
263
 
263
264
  function collectRolePaths(protocolDefinition: ProtocolDefinition, ruleSet: ProtocolRuleSet, parentPath: string[] = []): string[] {
@@ -285,10 +286,6 @@ function isGrantKeyLocalRolePath(protocolDefinition: ProtocolDefinition, protoco
285
286
  return ruleSet?.$role === true && ruleSet.$keyAgreement !== undefined;
286
287
  }
287
288
 
288
- function isBoundaryAwareSubtree(scopePath: string, candidatePath: string): boolean {
289
- return `${candidatePath}/`.startsWith(`${scopePath}/`);
290
- }
291
-
292
289
  function getGrantKeyScopeCacheKey(scope: GrantKeyProtocolPathScope): string {
293
290
  return JSON.stringify([scope.protocol, scope.protocolPath]);
294
291
  }
@@ -6,6 +6,7 @@ import type { MessagesFilter } from '../types/messages-types.js';
6
6
  import { FilterUtility } from './filter.js';
7
7
  import { normalizeProtocolUrl } from './url.js';
8
8
  import { Records } from './records.js';
9
+ import { Replication } from './replication.js';
9
10
  import { isEmptyObject, removeUndefinedProperties } from '@enbox/common';
10
11
 
11
12
  type StoredMessageWithEncodedData = GenericMessage & { encodedData?: string };
@@ -122,6 +123,50 @@ export class Messages {
122
123
  return metadataFilter;
123
124
  }
124
125
 
126
+ /**
127
+ * Maps a filter set onto the replication fingerprint domains it covers, or
128
+ * `undefined` when the filters do not correspond to fingerprint domains.
129
+ * An empty filter set covers the global domain; a set of protocol-only
130
+ * filters covers each protocol's domain plus its tagged core-protocol
131
+ * domains. Any other filter shape has no domain mapping.
132
+ */
133
+ public static computeFingerprintScopes(filters: MessagesFilter[]): string[] | undefined {
134
+ if (filters.length === 0) {
135
+ return [Replication.globalDomain];
136
+ }
137
+
138
+ const protocols = new Set<string>();
139
+ for (const filter of filters) {
140
+ const keys = Object.keys(filter);
141
+ if (keys.length !== 1 || typeof filter.protocol !== 'string') {
142
+ return undefined;
143
+ }
144
+
145
+ protocols.add(filter.protocol);
146
+ }
147
+
148
+ // Fail closed for filter sets naming a core protocol directly: the core
149
+ // protocols contribute no tagged domains for themselves, so the domain
150
+ // set would not span everything such filters enumerate — two feeds could
151
+ // fingerprint identically while differing in enumerable messages. No
152
+ // fingerprint beats one whose coverage is known-incomplete.
153
+ for (const protocol of protocols) {
154
+ if (Replication.isCoreProtocolUri(protocol)) {
155
+ return undefined;
156
+ }
157
+ }
158
+
159
+ const scopes: string[] = [];
160
+ for (const protocol of protocols) {
161
+ scopes.push(
162
+ Replication.protocolDomain(protocol),
163
+ ...Replication.taggedCoreProtocolDomains(protocol, protocols),
164
+ );
165
+ }
166
+
167
+ return scopes;
168
+ }
169
+
125
170
  /**
126
171
  * Returns a copy of a RecordsWrite message without inline encodedData, and the
127
172
  * detached encodedData value for wire surfaces that carry data beside the message.
@@ -151,26 +196,16 @@ export class Messages {
151
196
  delete filterCopy.dateUpdated;
152
197
  }
153
198
 
154
- // Convert protocolPathPrefix into a protocolPath range filter.
155
- // The range gte: prefix, lt: prefix + '/\uffff' matches:
156
- // - exact: 'post' (prefix itself)
157
- // - children: 'post/attachment', 'post/comment', etc.
158
- // - NOT siblings: 'poster', 'postfix' (excluded because '/' < any alphanumeric)
199
+ // Convert protocolPathPrefix into a segment-aware protocolPath subtree filter.
159
200
  if (protocolPathPrefix !== undefined) {
160
201
  delete (filterCopy as any).protocolPathPrefix;
161
- filterCopy.protocolPath = {
162
- gte : protocolPathPrefix,
163
- lt : protocolPathPrefix + '/\uffff',
164
- };
202
+ filterCopy.protocolPath = { subtree: protocolPathPrefix };
165
203
  }
166
204
 
167
- // Convert contextIdPrefix into a contextId range filter (same pattern).
205
+ // Convert contextIdPrefix into the same segment-aware subtree filter.
168
206
  if (contextIdPrefix !== undefined) {
169
207
  delete (filterCopy as any).contextIdPrefix;
170
- filterCopy.contextId = {
171
- gte : contextIdPrefix,
172
- lt : contextIdPrefix + '/\uffff',
173
- };
208
+ filterCopy.contextId = { subtree: contextIdPrefix };
174
209
  }
175
210
 
176
211
  return filterCopy;
@@ -1,3 +1,5 @@
1
+ import { FilterUtility } from './filter.js';
2
+
1
3
  /**
2
4
  * Minimal protocol-bound scope shape used by permission grant authorization.
3
5
  *
@@ -38,22 +40,13 @@ export class PermissionScopeMatcher {
38
40
  }
39
41
 
40
42
  if (scope.protocolPath !== undefined) {
41
- return PermissionScopeMatcher.matchesSubtree(scope.protocolPath, target.protocolPath);
43
+ return FilterUtility.matchesSubtree(scope.protocolPath, target.protocolPath);
42
44
  }
43
45
 
44
46
  if (scope.contextId !== undefined) {
45
- return PermissionScopeMatcher.matchesContextId(scope.contextId, target.contextId);
47
+ return FilterUtility.matchesSubtree(scope.contextId, target.contextId);
46
48
  }
47
49
 
48
50
  return true;
49
51
  }
50
-
51
- private static matchesContextId(scopeContextId: string, candidateContextId: unknown): boolean {
52
- return PermissionScopeMatcher.matchesSubtree(scopeContextId, candidateContextId);
53
- }
54
-
55
- private static matchesSubtree(scopeValue: string, candidateValue: unknown): boolean {
56
- return typeof candidateValue === 'string' &&
57
- (candidateValue === scopeValue || candidateValue.startsWith(scopeValue + '/'));
58
- }
59
52
  }