@enbox/dwn-sdk-js 0.4.16 → 0.4.18

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 (175) 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 +978 -840
  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/records-collection.js +200 -0
  16. package/dist/esm/src/handlers/records-collection.js.map +1 -0
  17. package/dist/esm/src/handlers/records-count.js +26 -195
  18. package/dist/esm/src/handlers/records-count.js.map +1 -1
  19. package/dist/esm/src/handlers/records-query.js +19 -373
  20. package/dist/esm/src/handlers/records-query.js.map +1 -1
  21. package/dist/esm/src/handlers/records-subscribe.js +21 -338
  22. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  23. package/dist/esm/src/handlers/records-write.js +8 -26
  24. package/dist/esm/src/handlers/records-write.js.map +1 -1
  25. package/dist/esm/src/index.js +1 -1
  26. package/dist/esm/src/index.js.map +1 -1
  27. package/dist/esm/src/interfaces/protocols-configure.js +4 -9
  28. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  29. package/dist/esm/src/interfaces/records-count.js +1 -1
  30. package/dist/esm/src/interfaces/records-count.js.map +1 -1
  31. package/dist/esm/src/interfaces/records-query.js +1 -1
  32. package/dist/esm/src/interfaces/records-query.js.map +1 -1
  33. package/dist/esm/src/interfaces/records-subscribe.js +3 -1
  34. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  35. package/dist/esm/src/store/index-level.js +210 -21
  36. package/dist/esm/src/store/index-level.js.map +1 -1
  37. package/dist/esm/src/store/message-store-level.js +192 -21
  38. package/dist/esm/src/store/message-store-level.js.map +1 -1
  39. package/dist/esm/src/store/storage-controller.js +1 -3
  40. package/dist/esm/src/store/storage-controller.js.map +1 -1
  41. package/dist/esm/src/types/protocols-types.js +0 -10
  42. package/dist/esm/src/types/protocols-types.js.map +1 -1
  43. package/dist/esm/src/utils/filter.js +35 -1
  44. package/dist/esm/src/utils/filter.js.map +1 -1
  45. package/dist/esm/src/utils/grant-key-coverage.js +4 -6
  46. package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
  47. package/dist/esm/src/utils/messages.js +4 -14
  48. package/dist/esm/src/utils/messages.js.map +1 -1
  49. package/dist/esm/src/utils/permission-scope.js +3 -9
  50. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  51. package/dist/esm/src/utils/record-limit-occupancy.js +73 -202
  52. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  53. package/dist/esm/src/utils/records.js +19 -52
  54. package/dist/esm/src/utils/records.js.map +1 -1
  55. package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
  56. package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
  57. package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
  58. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  59. package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
  60. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  61. package/dist/esm/tests/dwn.spec.js +2 -2
  62. package/dist/esm/tests/dwn.spec.js.map +1 -1
  63. package/dist/esm/tests/features/records-delivery.spec.js +2 -5
  64. package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
  65. package/dist/esm/tests/features/records-nested-query-scope.spec.js +149 -70
  66. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  67. package/dist/esm/tests/features/records-record-limit.spec.js +380 -95
  68. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  69. package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
  70. package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
  71. package/dist/esm/tests/handlers/records-count.spec.js +52 -1
  72. package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
  73. package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
  74. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  75. package/dist/esm/tests/handlers/records-write.spec.js +57 -3
  76. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  77. package/dist/esm/tests/store/index-level.spec.js +208 -1
  78. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  79. package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
  80. package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
  81. package/dist/esm/tests/store/message-store-level.spec.js +76 -1
  82. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  83. package/dist/esm/tests/store/message-store.spec.js +81 -0
  84. package/dist/esm/tests/store/message-store.spec.js.map +1 -1
  85. package/dist/esm/tests/utils/filters.spec.js +25 -4
  86. package/dist/esm/tests/utils/filters.spec.js.map +1 -1
  87. package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
  88. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  89. package/dist/esm/tests/utils/records.spec.js +12 -0
  90. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  91. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
  92. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
  93. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  94. package/dist/types/src/core/dwn-constant.d.ts +2 -2
  95. package/dist/types/src/core/dwn-error.d.ts +0 -1
  96. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  97. package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
  98. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  99. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  100. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  101. package/dist/types/src/handlers/records-collection.d.ts +32 -0
  102. package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
  103. package/dist/types/src/handlers/records-count.d.ts +0 -34
  104. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  105. package/dist/types/src/handlers/records-query.d.ts +0 -61
  106. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  107. package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
  108. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  109. package/dist/types/src/handlers/records-write.d.ts +3 -0
  110. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  111. package/dist/types/src/index.d.ts +3 -3
  112. package/dist/types/src/index.d.ts.map +1 -1
  113. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  114. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  115. package/dist/types/src/store/index-level.d.ts +61 -0
  116. package/dist/types/src/store/index-level.d.ts.map +1 -1
  117. package/dist/types/src/store/message-store-level.d.ts +24 -6
  118. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  119. package/dist/types/src/store/storage-controller.d.ts.map +1 -1
  120. package/dist/types/src/types/message-store.d.ts +28 -2
  121. package/dist/types/src/types/message-store.d.ts.map +1 -1
  122. package/dist/types/src/types/protocols-types.d.ts +7 -24
  123. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  124. package/dist/types/src/types/query-types.d.ts +8 -1
  125. package/dist/types/src/types/query-types.d.ts.map +1 -1
  126. package/dist/types/src/types/records-types.d.ts +3 -2
  127. package/dist/types/src/types/records-types.d.ts.map +1 -1
  128. package/dist/types/src/utils/filter.d.ts +14 -1
  129. package/dist/types/src/utils/filter.d.ts.map +1 -1
  130. package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
  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 +6 -24
  137. package/dist/types/src/utils/records.d.ts.map +1 -1
  138. package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
  139. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  140. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  141. package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
  142. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  143. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  144. package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
  145. package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
  146. package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
  147. package/package.json +4 -4
  148. package/src/core/dwn-constant.ts +2 -2
  149. package/src/core/dwn-error.ts +0 -2
  150. package/src/core/protocol-authorization-validation.ts +1 -33
  151. package/src/core/protocol-authorization.ts +0 -7
  152. package/src/core/validation-state-reader.ts +2 -3
  153. package/src/handlers/records-collection.ts +269 -0
  154. package/src/handlers/records-count.ts +38 -232
  155. package/src/handlers/records-query.ts +22 -444
  156. package/src/handlers/records-subscribe.ts +26 -397
  157. package/src/handlers/records-write.ts +17 -32
  158. package/src/index.ts +3 -3
  159. package/src/interfaces/protocols-configure.ts +4 -13
  160. package/src/interfaces/records-count.ts +1 -1
  161. package/src/interfaces/records-query.ts +1 -1
  162. package/src/interfaces/records-subscribe.ts +5 -2
  163. package/src/store/index-level.ts +323 -28
  164. package/src/store/message-store-level.ts +251 -25
  165. package/src/store/storage-controller.ts +1 -3
  166. package/src/types/message-store.ts +30 -2
  167. package/src/types/protocols-types.ts +7 -26
  168. package/src/types/query-types.ts +10 -2
  169. package/src/types/records-types.ts +3 -2
  170. package/src/utils/filter.ts +40 -3
  171. package/src/utils/grant-key-coverage.ts +4 -7
  172. package/src/utils/messages.ts +4 -14
  173. package/src/utils/permission-scope.ts +4 -11
  174. package/src/utils/record-limit-occupancy.ts +108 -293
  175. package/src/utils/records.ts +22 -57
@@ -0,0 +1,269 @@
1
+ import type { HandlerDependencies } from '../types/method-handler.js';
2
+ import type { RecordsCount } from '../interfaces/records-count.js';
3
+ import type { RecordsQuery } from '../interfaces/records-query.js';
4
+ import type { RecordsSubscribe } from '../interfaces/records-subscribe.js';
5
+ import type { Filter, PaginationCursor } from '../types/query-types.js';
6
+ import type { RecordsFilter, RecordsQueryReplyEntry } from '../types/records-types.js';
7
+
8
+ import { authenticate } from '../core/auth.js';
9
+ import { DateSort } from '../types/records-types.js';
10
+ import { EncryptionControl } from '../core/encryption-control.js';
11
+ import { Message } from '../core/message.js';
12
+ import { ProtocolAuthorization } from '../core/protocol-authorization.js';
13
+ import { Records } from '../utils/records.js';
14
+ import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
15
+ import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
16
+ import { queryRecordsWithRecordLimitOccupancy, resolveRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
17
+
18
+ type RecordsCollectionRequest = RecordsCount | RecordsQuery | RecordsSubscribe;
19
+
20
+ export type RecordsCollectionVisibility = 'nonOwner' | 'owner' | 'published';
21
+
22
+ /**
23
+ * Resolves the visible population for a Records collection request, authenticating and
24
+ * authorizing every request that is not an anonymous published-records read.
25
+ */
26
+ export async function resolveRecordsCollectionVisibility(
27
+ tenant: string,
28
+ request: RecordsCollectionRequest,
29
+ deps: HandlerDependencies,
30
+ ): Promise<RecordsCollectionVisibility> {
31
+ const recordsFilter = request.message.descriptor.filter;
32
+ if (Records.filterIncludesPublishedRecords(recordsFilter) && request.author === undefined) {
33
+ return 'published';
34
+ }
35
+
36
+ await authenticate(request.message.authorization!, deps.didResolver);
37
+
38
+ if (Message.isSignedByAuthorDelegate(request.message) && !EncryptionControl.filterTargetsOnlyControlRecords(recordsFilter)) {
39
+ await request.authorizeDelegate(deps.validationStateReader);
40
+ } else if (EncryptionControl.filterTargetsOnlyControlRecords(recordsFilter)) {
41
+ await EncryptionControl.authorizeControlReadRequest({
42
+ tenant,
43
+ incomingMessage : request.message,
44
+ requester : Message.getRequester(request.message),
45
+ validationStateReader : deps.validationStateReader,
46
+ });
47
+ }
48
+
49
+ const permissionGrantId = Message.getPermissionGrantId(request.signaturePayload!);
50
+ if (permissionGrantId !== undefined) {
51
+ const permissionGrant = await deps.validationStateReader.fetchGrant(tenant, permissionGrantId);
52
+ await RecordsGrantAuthorization.authorizeQueryOrSubscribe({
53
+ incomingMessage : request.message,
54
+ expectedGrantor : tenant,
55
+ expectedGrantee : request.author!,
56
+ permissionGrant,
57
+ validationStateReader : deps.validationStateReader,
58
+ });
59
+ } else if (invokesProtocolRole(request)) {
60
+ // A protocol filter alone does not require protocol authorization because unauthorized
61
+ // records are removed by the visibility filters. An invoked protocol role does.
62
+ await ProtocolAuthorization.authorizeQueryOrSubscribe(tenant, request, deps.validationStateReader);
63
+ }
64
+
65
+ return request.author === tenant ? 'owner' : 'nonOwner';
66
+ }
67
+
68
+ /** Builds latest-write filters shared by Records Query, Count, and Subscribe snapshots. */
69
+ export function buildRecordsSnapshotFilters(
70
+ request: RecordsCollectionRequest,
71
+ visibility: RecordsCollectionVisibility,
72
+ ): Filter[] {
73
+ const descriptor = request.message.descriptor;
74
+ const dateSort = 'dateSort' in descriptor ? descriptor.dateSort : undefined;
75
+ return buildRecordsVisibilityFilters({
76
+ request,
77
+ visibility,
78
+ filter : descriptor.filter,
79
+ purpose : 'snapshot',
80
+ dateSort,
81
+ });
82
+ }
83
+
84
+ /** Builds Write-and-Delete filters for Records Subscribe event delivery. */
85
+ export function buildRecordsEventFilters(
86
+ request: RecordsSubscribe,
87
+ visibility: RecordsCollectionVisibility,
88
+ ): Filter[] {
89
+ return buildRecordsVisibilityFilters({
90
+ request,
91
+ visibility,
92
+ filter : request.message.descriptor.filter,
93
+ purpose : 'event',
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Queries one visible Records page after applying record-limit occupancy, current-audience
99
+ * projection, and encryption-control authorization in their required order.
100
+ */
101
+ export async function queryVisibleRecordsPage(input: {
102
+ deps: HandlerDependencies;
103
+ request: RecordsQuery | RecordsSubscribe;
104
+ tenant: string;
105
+ visibility: RecordsCollectionVisibility;
106
+ }): Promise<{ messages: RecordsQueryReplyEntry[]; cursor?: PaginationCursor }> {
107
+ const { deps, request, tenant, visibility } = input;
108
+ const { dateSort, filter, messageTimestamp, pagination } = request.message.descriptor;
109
+ const filters = buildRecordsSnapshotFilters(request, visibility);
110
+ const recordLimit = await resolveRecordLimitOccupancy({
111
+ validationStateReader : deps.validationStateReader,
112
+ tenant,
113
+ recordsFilter : filter,
114
+ messageTimestamp,
115
+ });
116
+ const controlFilters = Records.buildControlRecordsFilters(filters);
117
+ const currentAudienceRecordIdCache = new Map<string, string | undefined>();
118
+ const requester = Message.getRequester(request.message);
119
+ const messageSort = Records.convertDateSort(dateSort ?? DateSort.CreatedAscending);
120
+
121
+ const queryProjectedPage = async (page = pagination): Promise<{
122
+ messages: RecordsQueryReplyEntry[];
123
+ cursor?: PaginationCursor;
124
+ }> => {
125
+ const result = await queryRecordsWithRecordLimitOccupancy({
126
+ messageStore : deps.messageStore,
127
+ tenant,
128
+ filters,
129
+ recordLimit,
130
+ messageSort,
131
+ pagination : page,
132
+ });
133
+ return EncryptionControl.projectCurrentAudienceRecordPage({
134
+ messageStore: deps.messageStore,
135
+ tenant,
136
+ filters,
137
+ currentAudienceRecordIdCache,
138
+ result,
139
+ });
140
+ };
141
+
142
+ const filterVisibleControlRecords = (messages: RecordsQueryReplyEntry[]): Promise<RecordsQueryReplyEntry[]> =>
143
+ EncryptionControl.filterVisibleControlRecords({
144
+ tenant,
145
+ incomingMessage : request.message,
146
+ requester,
147
+ recordsWriteMessages : messages,
148
+ validationStateReader : deps.validationStateReader,
149
+ });
150
+
151
+ if (controlFilters.length === 0) {
152
+ return queryProjectedPage();
153
+ }
154
+
155
+ if (pagination?.limit === undefined || pagination.limit <= 0) {
156
+ const result = await queryProjectedPage();
157
+ return {
158
+ messages : await filterVisibleControlRecords(result.messages),
159
+ cursor : result.cursor,
160
+ };
161
+ }
162
+
163
+ const visibleMessages: RecordsQueryReplyEntry[] = [];
164
+ let cursor = pagination.cursor;
165
+ let nextCursor: PaginationCursor | undefined;
166
+ // Keeps visible-page pagination stable until #1100 moves control visibility into indexed store filters.
167
+ do {
168
+ const remainingLimit = pagination.limit - visibleMessages.length;
169
+ const result = await queryProjectedPage({ ...pagination, cursor, limit: remainingLimit });
170
+ visibleMessages.push(...await filterVisibleControlRecords(result.messages));
171
+ nextCursor = result.cursor;
172
+ cursor = result.cursor;
173
+ } while (visibleMessages.length < pagination.limit && cursor !== undefined);
174
+
175
+ return { messages: visibleMessages, cursor: nextCursor };
176
+ }
177
+
178
+ function buildRecordsVisibilityFilters(input: {
179
+ dateSort?: DateSort;
180
+ filter: RecordsFilter;
181
+ purpose: 'event' | 'snapshot';
182
+ request: RecordsCollectionRequest;
183
+ visibility: RecordsCollectionVisibility;
184
+ }): Filter[] {
185
+ const { dateSort, filter, purpose, request, visibility } = input;
186
+ if (visibility === 'owner') {
187
+ return [buildRecordsFilter({ dateSort, filter, purpose })];
188
+ }
189
+
190
+ if (visibility === 'published') {
191
+ return [buildRecordsFilter({ dateSort, filter, purpose, published: true })];
192
+ }
193
+
194
+ const filters: Filter[] = [];
195
+ if (Records.filterIncludesPublishedRecords(filter)) {
196
+ filters.push(buildRecordsFilter({ dateSort, filter, purpose, published: true }));
197
+ }
198
+
199
+ if (!Records.filterIncludesUnpublishedRecords(filter)) {
200
+ return filters;
201
+ }
202
+
203
+ if (EncryptionControl.isExactAudienceFilter(filter)) {
204
+ filters.push(buildRecordsFilter({ dateSort, filter, purpose, published: false }));
205
+ }
206
+
207
+ if (shouldBuildUnpublishedAuthorFilter(filter, request.author!)) {
208
+ filters.push(buildRecordsFilter({ author: request.author, dateSort, filter, purpose, published: false }));
209
+ }
210
+
211
+ if (invokesProtocolRole(request) || Message.getPermissionGrantId(request.signaturePayload!) !== undefined) {
212
+ filters.push(buildRecordsFilter({ dateSort, filter, purpose, published: false }));
213
+ }
214
+
215
+ if (shouldBuildUnpublishedRecipientFilter(filter, request.author!)) {
216
+ filters.push(buildRecordsFilter({ dateSort, filter, purpose, published: false, recipient: request.author }));
217
+ }
218
+
219
+ return filters;
220
+ }
221
+
222
+ function buildRecordsFilter(input: {
223
+ author?: string;
224
+ dateSort?: DateSort;
225
+ filter: RecordsFilter;
226
+ purpose: 'event' | 'snapshot';
227
+ published?: boolean;
228
+ recipient?: string;
229
+ }): Filter {
230
+ const { author, dateSort, filter, purpose, published, recipient } = input;
231
+ const result: Filter = {
232
+ ...Records.convertFilter(filter, purpose === 'snapshot' ? dateSort : undefined),
233
+ interface : DwnInterfaceName.Records,
234
+ method : purpose === 'snapshot' ? DwnMethodName.Write : [DwnMethodName.Write, DwnMethodName.Delete],
235
+ };
236
+
237
+ if (purpose === 'snapshot') {
238
+ result.isLatestBaseState = true;
239
+ }
240
+ if (author !== undefined) {
241
+ result.author = author;
242
+ }
243
+ if (published !== undefined) {
244
+ result.published = published;
245
+ }
246
+ if (recipient !== undefined) {
247
+ result.recipient = recipient;
248
+ }
249
+
250
+ return result;
251
+ }
252
+
253
+ function invokesProtocolRole(request: RecordsCollectionRequest): boolean {
254
+ return request.signaturePayload?.protocolRole !== undefined;
255
+ }
256
+
257
+ function shouldBuildUnpublishedRecipientFilter(filter: RecordsFilter, recipient: string): boolean {
258
+ const { recipient: recipientFilter } = filter;
259
+ return Array.isArray(recipientFilter)
260
+ ? recipientFilter.length === 0 || recipientFilter.includes(recipient)
261
+ : recipientFilter === undefined || recipientFilter === recipient;
262
+ }
263
+
264
+ function shouldBuildUnpublishedAuthorFilter(filter: RecordsFilter, author: string): boolean {
265
+ const { author: authorFilter } = filter;
266
+ return Array.isArray(authorFilter)
267
+ ? authorFilter.length === 0 || authorFilter.includes(author)
268
+ : authorFilter === undefined || authorFilter === author;
269
+ }
@@ -1,17 +1,20 @@
1
1
  import type { Filter } from '../types/query-types.js';
2
+ import type { RecordLimitOccupancy } from '../types/message-store.js';
3
+ import type { RecordsCollectionVisibility } from './records-collection.js';
2
4
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
3
5
  import type { RecordsCountMessage, RecordsCountReply } from '../types/records-types.js';
4
6
 
5
- import { authenticate } from '../core/auth.js';
6
7
  import { EncryptionControl } from '../core/encryption-control.js';
7
8
  import { Message } from '../core/message.js';
8
9
  import { messageReplyFromError } from '../core/message-reply.js';
9
- import { ProtocolAuthorization } from '../core/protocol-authorization.js';
10
10
  import { Records } from '../utils/records.js';
11
11
  import { RecordsCount } from '../interfaces/records-count.js';
12
- import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
13
- import { countRecordsWithRecordLimitOccupancy, queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
14
- import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
12
+ import { buildRecordsSnapshotFilters, resolveRecordsCollectionVisibility } from './records-collection.js';
13
+ import {
14
+ countRecordsWithRecordLimitOccupancy,
15
+ queryRecordsWithRecordLimitOccupancy,
16
+ resolveRecordLimitOccupancy,
17
+ } from '../utils/record-limit-occupancy.js';
15
18
 
16
19
  export class RecordsCountHandler implements MethodHandler {
17
20
 
@@ -24,34 +27,29 @@ export class RecordsCountHandler implements MethodHandler {
24
27
  let recordsCount: RecordsCount;
25
28
  try {
26
29
  recordsCount = await RecordsCount.parse(message);
27
- } catch (e) {
28
- return messageReplyFromError(e, 400);
30
+ } catch (error) {
31
+ return messageReplyFromError(error, 400);
29
32
  }
30
33
 
31
- let count: number;
32
34
  const requester = Message.getRequester(recordsCount.message);
35
+ let visibility: RecordsCollectionVisibility;
36
+ try {
37
+ visibility = await resolveRecordsCollectionVisibility(tenant, recordsCount, this.deps);
38
+ } catch (error) {
39
+ return messageReplyFromError(error, 401);
40
+ }
33
41
 
34
- // if this is an anonymous count and the filter supports published records, count only published records
35
- if (Records.filterIncludesPublishedRecords(recordsCount.message.descriptor.filter) && recordsCount.author === undefined) {
36
- count = await this.countPublishedRecords(tenant, recordsCount, requester);
37
- } else {
38
- // authentication and authorization
39
- try {
40
- await authenticate(message.authorization!, this.deps.didResolver);
41
-
42
- await RecordsCountHandler.authorizeRecordsCount(tenant, recordsCount, this.deps);
43
- } catch (e) {
44
- return messageReplyFromError(e, 401);
45
- }
42
+ const filters = buildRecordsSnapshotFilters(recordsCount, visibility);
43
+ const recordLimit = await resolveRecordLimitOccupancy({
44
+ validationStateReader : this.deps.validationStateReader,
45
+ tenant,
46
+ recordsFilter : recordsCount.message.descriptor.filter,
47
+ messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
48
+ });
46
49
 
47
- if (recordsCount.author === tenant && requester === tenant) {
48
- count = await this.countRecordsAsOwner(tenant, recordsCount);
49
- } else if (recordsCount.author === tenant) {
50
- count = await this.countRecordsAsOwnerDelegate(tenant, recordsCount, requester);
51
- } else {
52
- count = await this.countRecordsAsNonOwner(tenant, recordsCount, requester);
53
- }
54
- }
50
+ const count = recordsCount.author === tenant && requester === tenant
51
+ ? await this.countProjectedRecords(tenant, filters, recordLimit)
52
+ : await this.countProjectedRecordsForRequester(tenant, recordsCount, requester, filters, recordLimit);
55
53
 
56
54
  return {
57
55
  status: { code: 200, detail: 'OK' },
@@ -59,96 +57,16 @@ export class RecordsCountHandler implements MethodHandler {
59
57
  };
60
58
  }
61
59
 
62
- /**
63
- * Counts records as the owner of the DWN with no additional filtering.
64
- */
65
- private async countRecordsAsOwner(tenant: string, recordsCount: RecordsCount): Promise<number> {
66
- const { filter } = recordsCount.message.descriptor;
67
- const countFilter = {
68
- ...Records.convertFilter(filter),
69
- interface : DwnInterfaceName.Records,
70
- method : DwnMethodName.Write,
71
- isLatestBaseState : true
72
- };
73
-
74
- return this.countProjectedRecords(tenant, recordsCount, [countFilter]);
75
- }
76
-
77
- private async countRecordsAsOwnerDelegate(
78
- tenant: string,
79
- recordsCount: RecordsCount,
80
- requester: string | undefined,
81
- ): Promise<number> {
82
- const { filter } = recordsCount.message.descriptor;
83
- const countFilter = {
84
- ...Records.convertFilter(filter),
85
- interface : DwnInterfaceName.Records,
86
- method : DwnMethodName.Write,
87
- isLatestBaseState : true
88
- };
89
-
90
- return this.countProjectedRecordsForRequester(tenant, recordsCount, requester, [countFilter]);
91
- }
92
-
93
- /**
94
- * Counts records as a non-owner, applying the same filter logic as RecordsQuery.
95
- */
96
- private async countRecordsAsNonOwner(
60
+ private async countProjectedRecords(
97
61
  tenant: string,
98
- recordsCount: RecordsCount,
99
- requester: string | undefined,
100
- ): Promise<number> {
101
- const { filter } = recordsCount.message.descriptor;
102
- const filters: Filter[] = [];
103
-
104
- if (Records.filterIncludesPublishedRecords(filter)) {
105
- filters.push(RecordsCountHandler.buildPublishedRecordsFilter(recordsCount));
106
- }
107
-
108
- if (Records.filterIncludesUnpublishedRecords(filter)) {
109
- if (EncryptionControl.isExactAudienceFilter(filter)) {
110
- filters.push(Records.buildUnpublishedControlRecordsFilter(filter));
111
- }
112
-
113
- if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsCount.author!)) {
114
- filters.push(RecordsCountHandler.buildUnpublishedRecordsByCountAuthorFilter(recordsCount));
115
- }
116
-
117
- if (Records.shouldProtocolAuthorize(recordsCount.signaturePayload!)) {
118
- filters.push(RecordsCountHandler.buildUnpublishedProtocolAuthorizedRecordsFilter(recordsCount));
119
- }
120
-
121
- if (Message.getPermissionGrantId(recordsCount.signaturePayload!) !== undefined) {
122
- filters.push(RecordsCountHandler.buildUnpublishedPermissionGrantAuthorizedRecordsFilter(recordsCount));
123
- }
124
-
125
- if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsCount.author!)) {
126
- filters.push(RecordsCountHandler.buildUnpublishedRecordsForCountAuthorFilter(recordsCount));
127
- }
128
- }
129
-
130
- return this.countProjectedRecordsForRequester(tenant, recordsCount, requester, filters);
131
- }
132
-
133
- /**
134
- * Counts only published records.
135
- */
136
- private async countPublishedRecords(
137
- tenant: string,
138
- recordsCount: RecordsCount,
139
- requester: string | undefined,
62
+ filters: Filter[],
63
+ recordLimit: RecordLimitOccupancy | undefined,
140
64
  ): Promise<number> {
141
- const filter = RecordsCountHandler.buildPublishedRecordsFilter(recordsCount);
142
- return this.countProjectedRecordsForRequester(tenant, recordsCount, requester, [filter]);
143
- }
144
-
145
- private async countProjectedRecords(tenant: string, recordsCount: RecordsCount, filters: Filter[]): Promise<number> {
146
65
  const totalCount = await countRecordsWithRecordLimitOccupancy({
147
- messageStore : this.deps.messageStore,
148
- validationStateReader : this.deps.validationStateReader,
66
+ messageStore: this.deps.messageStore,
149
67
  tenant,
150
68
  filters,
151
- messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
69
+ recordLimit,
152
70
  });
153
71
 
154
72
  const audienceFilters = EncryptionControl.buildAudienceRecordFilters(filters);
@@ -177,24 +95,24 @@ export class RecordsCountHandler implements MethodHandler {
177
95
  recordsCount: RecordsCount,
178
96
  requester: string | undefined,
179
97
  filters: Filter[],
98
+ recordLimit: RecordLimitOccupancy | undefined,
180
99
  ): Promise<number> {
181
100
  const controlFilters = Records.buildControlRecordsFilters(filters);
182
101
  if (controlFilters.length === 0) {
183
- return this.countProjectedRecords(tenant, recordsCount, filters);
102
+ return this.countProjectedRecords(tenant, filters, recordLimit);
184
103
  }
185
104
 
186
- const totalCount = await this.countProjectedRecords(tenant, recordsCount, filters);
187
- const controlCount = await this.countProjectedRecords(tenant, recordsCount, controlFilters);
105
+ const totalCount = await this.countProjectedRecords(tenant, filters, recordLimit);
106
+ const controlCount = await this.countProjectedRecords(tenant, controlFilters, recordLimit);
188
107
  if (controlCount === 0) {
189
108
  return totalCount;
190
109
  }
191
110
 
192
111
  const { messages } = await queryRecordsWithRecordLimitOccupancy({
193
- messageStore : this.deps.messageStore,
194
- validationStateReader : this.deps.validationStateReader,
112
+ messageStore : this.deps.messageStore,
195
113
  tenant,
196
- filters : controlFilters,
197
- messageTimestamp : recordsCount.message.descriptor.messageTimestamp,
114
+ filters : controlFilters,
115
+ recordLimit,
198
116
  });
199
117
  const projectedMessages = await EncryptionControl.projectCurrentAudienceRecords({
200
118
  messageStore : this.deps.messageStore,
@@ -211,116 +129,4 @@ export class RecordsCountHandler implements MethodHandler {
211
129
  });
212
130
  return totalCount - controlCount + visibleMessages.length;
213
131
  }
214
-
215
- private static buildPublishedRecordsFilter(recordsCount: RecordsCount): Filter {
216
- const { filter } = recordsCount.message.descriptor;
217
- return {
218
- ...Records.convertFilter(filter),
219
- interface : DwnInterfaceName.Records,
220
- method : DwnMethodName.Write,
221
- published : true,
222
- isLatestBaseState : true
223
- };
224
- }
225
-
226
- /**
227
- * Creates a filter for unpublished records that are intended for the count author (where `recipient` is the author).
228
- */
229
- private static buildUnpublishedRecordsForCountAuthorFilter(recordsCount: RecordsCount): Filter {
230
- const { filter } = recordsCount.message.descriptor;
231
- return {
232
- ...Records.convertFilter(filter),
233
- interface : DwnInterfaceName.Records,
234
- method : DwnMethodName.Write,
235
- recipient : recordsCount.author!,
236
- isLatestBaseState : true,
237
- published : false
238
- };
239
- }
240
-
241
- /**
242
- * Creates a filter for unpublished records that are within the specified protocol.
243
- */
244
- private static buildUnpublishedProtocolAuthorizedRecordsFilter(recordsCount: RecordsCount): Filter {
245
- const { filter } = recordsCount.message.descriptor;
246
- return {
247
- ...Records.convertFilter(filter),
248
- interface : DwnInterfaceName.Records,
249
- method : DwnMethodName.Write,
250
- isLatestBaseState : true,
251
- published : false
252
- };
253
- }
254
-
255
- /**
256
- * Creates a filter for unpublished records authorized by a permission grant.
257
- */
258
- private static buildUnpublishedPermissionGrantAuthorizedRecordsFilter(recordsCount: RecordsCount): Filter {
259
- const { filter } = recordsCount.message.descriptor;
260
- return {
261
- ...Records.convertFilter(filter),
262
- interface : DwnInterfaceName.Records,
263
- method : DwnMethodName.Write,
264
- isLatestBaseState : true,
265
- published : false
266
- };
267
- }
268
-
269
- /**
270
- * Creates a filter for only unpublished records where the author is the same as the count author.
271
- */
272
- private static buildUnpublishedRecordsByCountAuthorFilter(recordsCount: RecordsCount): Filter {
273
- const { filter } = recordsCount.message.descriptor;
274
- return {
275
- ...Records.convertFilter(filter),
276
- author : recordsCount.author!,
277
- interface : DwnInterfaceName.Records,
278
- method : DwnMethodName.Write,
279
- isLatestBaseState : true,
280
- published : false
281
- };
282
- }
283
-
284
- /**
285
- * @param messageStore Used to check if the grant has been revoked.
286
- */
287
- private static async authorizeRecordsCount(
288
- tenant: string,
289
- recordsCount: RecordsCount,
290
- deps: HandlerDependencies,
291
- ): Promise<void> {
292
-
293
- if (Message.isSignedByAuthorDelegate(recordsCount.message) &&
294
- !EncryptionControl.filterTargetsOnlyControlRecords(recordsCount.message.descriptor.filter)) {
295
- await recordsCount.authorizeDelegate(deps.validationStateReader);
296
- } else if (EncryptionControl.filterTargetsOnlyControlRecords(recordsCount.message.descriptor.filter)) {
297
- await EncryptionControl.authorizeControlReadRequest({
298
- tenant,
299
- incomingMessage : recordsCount.message,
300
- requester : Message.getRequester(recordsCount.message),
301
- validationStateReader : deps.validationStateReader,
302
- });
303
- }
304
-
305
- const permissionGrantId = Message.getPermissionGrantId(recordsCount.signaturePayload!);
306
- if (permissionGrantId !== undefined) {
307
- const permissionGrant = await deps.validationStateReader.fetchGrant(tenant, permissionGrantId);
308
- await RecordsGrantAuthorization.authorizeQueryOrSubscribe({
309
- incomingMessage : recordsCount.message,
310
- expectedGrantor : tenant,
311
- expectedGrantee : recordsCount.author!,
312
- permissionGrant,
313
- validationStateReader : deps.validationStateReader,
314
- });
315
- return;
316
- }
317
-
318
- // NOTE: not all RecordsCount messages require protocol authorization even if the filter includes protocol-related fields,
319
- // this is because we dynamically filter out records that the caller is not authorized to see.
320
- // Currently only run protocol authorization if message deliberately invokes a protocol role.
321
- if (Records.shouldProtocolAuthorize(recordsCount.signaturePayload!)) {
322
- await ProtocolAuthorization.authorizeQueryOrSubscribe(tenant, recordsCount, deps.validationStateReader);
323
- }
324
- }
325
-
326
132
  }