@enbox/dwn-sdk-js 0.4.17 → 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 (77) 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 +349 -198
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/handlers/records-collection.js +200 -0
  6. package/dist/esm/src/handlers/records-collection.js.map +1 -0
  7. package/dist/esm/src/handlers/records-count.js +19 -196
  8. package/dist/esm/src/handlers/records-count.js.map +1 -1
  9. package/dist/esm/src/handlers/records-query.js +19 -376
  10. package/dist/esm/src/handlers/records-query.js.map +1 -1
  11. package/dist/esm/src/handlers/records-subscribe.js +21 -341
  12. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  13. package/dist/esm/src/interfaces/records-count.js +1 -1
  14. package/dist/esm/src/interfaces/records-count.js.map +1 -1
  15. package/dist/esm/src/interfaces/records-query.js +1 -1
  16. package/dist/esm/src/interfaces/records-query.js.map +1 -1
  17. package/dist/esm/src/interfaces/records-subscribe.js +3 -1
  18. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  19. package/dist/esm/src/store/index-level.js +84 -32
  20. package/dist/esm/src/store/index-level.js.map +1 -1
  21. package/dist/esm/src/store/message-store-level.js +45 -11
  22. package/dist/esm/src/store/message-store-level.js.map +1 -1
  23. package/dist/esm/src/utils/record-limit-occupancy.js +5 -2
  24. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  25. package/dist/esm/src/utils/records.js +14 -47
  26. package/dist/esm/src/utils/records.js.map +1 -1
  27. package/dist/esm/tests/features/records-nested-query-scope.spec.js +15 -1
  28. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  29. package/dist/esm/tests/features/records-record-limit.spec.js +116 -0
  30. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  31. package/dist/esm/tests/handlers/records-count.spec.js +52 -1
  32. package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
  33. package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
  34. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  35. package/dist/esm/tests/store/index-level.spec.js +95 -1
  36. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  37. package/dist/esm/tests/store/message-store-level.spec.js +70 -1
  38. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  39. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  40. package/dist/types/src/handlers/records-collection.d.ts +32 -0
  41. package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
  42. package/dist/types/src/handlers/records-count.d.ts +0 -35
  43. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  44. package/dist/types/src/handlers/records-query.d.ts +0 -61
  45. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  46. package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
  47. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  48. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  49. package/dist/types/src/store/index-level.d.ts +27 -3
  50. package/dist/types/src/store/index-level.d.ts.map +1 -1
  51. package/dist/types/src/store/message-store-level.d.ts +1 -0
  52. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  53. package/dist/types/src/types/message-store.d.ts +6 -1
  54. package/dist/types/src/types/message-store.d.ts.map +1 -1
  55. package/dist/types/src/types/records-types.d.ts +1 -1
  56. package/dist/types/src/types/records-types.d.ts.map +1 -1
  57. package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
  58. package/dist/types/src/utils/records.d.ts +6 -25
  59. package/dist/types/src/utils/records.d.ts.map +1 -1
  60. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  61. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  62. package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
  63. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/handlers/records-collection.ts +269 -0
  66. package/src/handlers/records-count.ts +21 -236
  67. package/src/handlers/records-query.ts +22 -447
  68. package/src/handlers/records-subscribe.ts +26 -404
  69. package/src/interfaces/records-count.ts +1 -1
  70. package/src/interfaces/records-query.ts +1 -1
  71. package/src/interfaces/records-subscribe.ts +5 -2
  72. package/src/store/index-level.ts +145 -37
  73. package/src/store/message-store-level.ts +46 -11
  74. package/src/types/message-store.ts +6 -1
  75. package/src/types/records-types.ts +1 -1
  76. package/src/utils/record-limit-occupancy.ts +5 -2
  77. package/src/utils/records.ts +17 -52
@@ -1,27 +1,22 @@
1
- import type { MessageSort } from '../types/message-types.js';
1
+ import type { RecordsCollectionVisibility } from './records-collection.js';
2
2
  import type { EventSubscription, ProgressGapInfo, ProgressToken, SubscriptionEvent, SubscriptionListener, SubscriptionMessage } from '../types/subscriptions.js';
3
3
  import type { Filter, PaginationCursor } from '../types/query-types.js';
4
4
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
5
5
  import type { RecordsQueryReplyEntry, RecordsSubscribeMessage, RecordsSubscribeReply } from '../types/records-types.js';
6
6
 
7
7
  import { attachInitialWrites } from '../utils/initial-write-attachment.js';
8
- import { authenticate } from '../core/auth.js';
9
- import { DateSort } from '../types/records-types.js';
10
8
  import { EncryptionControl } from '../core/encryption-control.js';
9
+ import { isRecordLimitOccupant } from '../utils/record-limit-occupancy.js';
11
10
  import { Message } from '../core/message.js';
12
11
  import { messageReplyFromError } from '../core/message-reply.js';
13
- import { ProtocolAuthorization } from '../core/protocol-authorization.js';
14
12
  import { Records } from '../utils/records.js';
15
- import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
16
13
  import { RecordsSubscribe } from '../interfaces/records-subscribe.js';
17
- import { SortDirection } from '../types/query-types.js';
18
- import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
19
- import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
20
14
  import {
21
- isRecordLimitOccupant,
22
- queryRecordsWithRecordLimitOccupancy,
23
- resolveRecordLimitOccupancy,
24
- } from '../utils/record-limit-occupancy.js';
15
+ buildRecordsEventFilters,
16
+ queryVisibleRecordsPage,
17
+ resolveRecordsCollectionVisibility,
18
+ } from './records-collection.js';
19
+ import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
25
20
 
26
21
  type ProjectedRecordsSubscriptionHandler = {
27
22
  listener: SubscriptionListener;
@@ -51,16 +46,15 @@ export class RecordsSubscribeHandler implements MethodHandler {
51
46
  let recordsSubscribe: RecordsSubscribe;
52
47
  try {
53
48
  recordsSubscribe = await RecordsSubscribe.parse(message);
54
- } catch (e) {
55
- return messageReplyFromError(e, 400);
49
+ } catch (error) {
50
+ return messageReplyFromError(error, 400);
56
51
  }
57
52
 
58
- const requester = Message.getRequester(recordsSubscribe.message);
59
53
  const filterResolution = await this.resolveSubscriptionFilters(tenant, message, recordsSubscribe);
60
54
  if ('errorReply' in filterResolution) {
61
55
  return filterResolution.errorReply;
62
56
  }
63
- const { eventFilters, queryFilters } = filterResolution;
57
+ const { eventFilters, visibility } = filterResolution;
64
58
 
65
59
  const messageCid = await Message.getCid(message);
66
60
  const { cursor: eventLogCursor } = recordsSubscribe.message.descriptor;
@@ -82,12 +76,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
82
76
 
83
77
  // ---- No cursor: existing behavior (initial snapshot from MessageStore) ----
84
78
  return this.handleSnapshotSubscription(
85
- tenant, messageCid, recordsSubscribe, requester, eventFilters, queryFilters, projectedSubscriptionHandler
79
+ tenant, messageCid, recordsSubscribe, eventFilters, visibility, projectedSubscriptionHandler
86
80
  );
87
81
  }
88
82
 
89
83
  /**
90
- * Resolves the event/query filters for the subscription, performing authentication and
84
+ * Resolves the event filters and visibility for the subscription, performing authentication and
91
85
  * authorization when the request is not an anonymous published-records-only subscribe.
92
86
  * Returns the resolved filters, or an `errorReply` if authentication/authorization failed.
93
87
  */
@@ -95,35 +89,21 @@ export class RecordsSubscribeHandler implements MethodHandler {
95
89
  tenant: string,
96
90
  message: RecordsSubscribeMessage,
97
91
  recordsSubscribe: RecordsSubscribe,
98
- ): Promise<{ eventFilters: Filter[]; queryFilters: Filter[] } | { errorReply: RecordsSubscribeReply }> {
99
- // if this is an anonymous subscribe and the filter supports published records, subscribe to only published records
100
- if (Records.filterIncludesPublishedRecords(recordsSubscribe.message.descriptor.filter) && recordsSubscribe.author === undefined) {
101
- const eventFilters = [RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe)];
102
- const queryFilters = [RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe)];
103
- // delete the undefined authorization property else the code will encounter the following IPLD issue when attempting to generate CID:
104
- // Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded
105
- delete message.authorization;
106
- return { eventFilters, queryFilters };
107
- }
108
-
109
- // authentication and authorization
92
+ ): Promise<{ eventFilters: Filter[]; visibility: RecordsCollectionVisibility } | { errorReply: RecordsSubscribeReply }> {
93
+ let visibility: RecordsCollectionVisibility;
110
94
  try {
111
- await authenticate(message.authorization!, this.deps.didResolver);
112
- await RecordsSubscribeHandler.authorizeRecordsSubscribe(tenant, recordsSubscribe, this.deps);
95
+ visibility = await resolveRecordsCollectionVisibility(tenant, recordsSubscribe, this.deps);
113
96
  } catch (error) {
114
97
  return { errorReply: messageReplyFromError(error, 401) };
115
98
  }
116
99
 
117
- if (recordsSubscribe.author === tenant) {
118
- return {
119
- eventFilters : RecordsSubscribeHandler.buildOwnerEventFilters(recordsSubscribe),
120
- queryFilters : RecordsSubscribeHandler.buildOwnerQueryFilters(recordsSubscribe),
121
- };
100
+ if (visibility === 'published') {
101
+ // Remove the undefined property before computing the subscription CID because IPLD cannot encode it.
102
+ delete message.authorization;
122
103
  }
123
-
124
104
  return {
125
- eventFilters : RecordsSubscribeHandler.buildNonOwnerEventFilters(recordsSubscribe),
126
- queryFilters : RecordsSubscribeHandler.buildNonOwnerQueryFilters(recordsSubscribe),
105
+ eventFilters: buildRecordsEventFilters(recordsSubscribe, visibility),
106
+ visibility,
127
107
  };
128
108
  }
129
109
 
@@ -171,9 +151,8 @@ export class RecordsSubscribeHandler implements MethodHandler {
171
151
  tenant: string,
172
152
  messageCid: string,
173
153
  recordsSubscribe: RecordsSubscribe,
174
- requester: string | undefined,
175
154
  eventFilters: Filter[],
176
- queryFilters: Filter[],
155
+ visibility: RecordsCollectionVisibility,
177
156
  projectedSubscriptionHandler: ProjectedRecordsSubscriptionHandler,
178
157
  ): Promise<RecordsSubscribeReply> {
179
158
  // Step 1: Register event listener FIRST to ensure no events are missed between query and subscribe
@@ -186,16 +165,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
186
165
  let entries: RecordsQueryReplyEntry[];
187
166
  let paginationCursor: PaginationCursor | undefined;
188
167
  try {
189
- const { dateSort, pagination } = recordsSubscribe.message.descriptor;
190
- const messageSort = RecordsSubscribeHandler.convertDateSort(dateSort);
191
- const queryResult = await this.queryRecordsWithVisibleControlFiltering(
168
+ const queryResult = await queryVisibleRecordsPage({
169
+ deps : this.deps,
192
170
  tenant,
193
- recordsSubscribe,
194
- requester,
195
- queryFilters,
196
- messageSort,
197
- pagination,
198
- );
171
+ request : recordsSubscribe,
172
+ visibility,
173
+ });
199
174
 
200
175
  // attach the retained initial write to every entry that is not itself an initial write
201
176
  entries = await attachInitialWrites({
@@ -220,29 +195,6 @@ export class RecordsSubscribeHandler implements MethodHandler {
220
195
  };
221
196
  }
222
197
 
223
- /**
224
- * Convert an incoming DateSort to a sort type accepted by MessageStore.
225
- * Defaults to `dateCreated` ascending if no sort is supplied.
226
- */
227
- private static convertDateSort(dateSort?: DateSort): MessageSort {
228
- switch (dateSort) {
229
- case DateSort.CreatedAscending:
230
- return { dateCreated: SortDirection.Ascending };
231
- case DateSort.CreatedDescending:
232
- return { dateCreated: SortDirection.Descending };
233
- case DateSort.PublishedAscending:
234
- return { datePublished: SortDirection.Ascending };
235
- case DateSort.PublishedDescending:
236
- return { datePublished: SortDirection.Descending };
237
- case DateSort.UpdatedAscending:
238
- return { messageTimestamp: SortDirection.Ascending };
239
- case DateSort.UpdatedDescending:
240
- return { messageTimestamp: SortDirection.Descending };
241
- default:
242
- return { dateCreated: SortDirection.Ascending };
243
- }
244
- }
245
-
246
198
  private static createRecordLimitOccupancyGuard(input: {
247
199
  deps: HandlerDependencies;
248
200
  eventFilters: Filter[];
@@ -361,334 +313,4 @@ export class RecordsSubscribeHandler implements MethodHandler {
361
313
  };
362
314
  }
363
315
 
364
- // =============================================
365
- // Event filters (for live subscription)
366
- // These match Write+Delete and do NOT use isLatestBaseState
367
- // =============================================
368
-
369
- /**
370
- * Build event filters for owner: all matching Write+Delete events.
371
- */
372
- private static buildOwnerEventFilters(recordsSubscribe: RecordsSubscribe): Filter[] {
373
- const { filter } = recordsSubscribe.message.descriptor;
374
- return [{
375
- ...Records.convertFilter(filter),
376
- interface : DwnInterfaceName.Records,
377
- method : [DwnMethodName.Write, DwnMethodName.Delete],
378
- }];
379
- }
380
-
381
- /**
382
- * Build event filters for non-owner with visibility rules.
383
- */
384
- private static buildNonOwnerEventFilters(recordsSubscribe: RecordsSubscribe): Filter[] {
385
- const filters: Filter[] = [];
386
- const { filter } = recordsSubscribe.message.descriptor;
387
- if (Records.filterIncludesPublishedRecords(filter)) {
388
- filters.push(RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe));
389
- }
390
-
391
- if (Records.filterIncludesUnpublishedRecords(filter)) {
392
- if (EncryptionControl.isExactAudienceFilter(filter)) {
393
- filters.push({
394
- ...Records.convertFilter(filter),
395
- interface : DwnInterfaceName.Records,
396
- method : [DwnMethodName.Write, DwnMethodName.Delete],
397
- published : false,
398
- });
399
- }
400
-
401
- if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author!)) {
402
- filters.push({
403
- ...Records.convertFilter(filter),
404
- author : recordsSubscribe.author!,
405
- interface : DwnInterfaceName.Records,
406
- method : [DwnMethodName.Write, DwnMethodName.Delete],
407
- published : false,
408
- });
409
- }
410
-
411
- if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) {
412
- filters.push({
413
- ...Records.convertFilter(filter),
414
- interface : DwnInterfaceName.Records,
415
- method : [DwnMethodName.Write, DwnMethodName.Delete],
416
- published : false,
417
- });
418
- }
419
-
420
- if (Message.getPermissionGrantId(recordsSubscribe.signaturePayload!) !== undefined) {
421
- filters.push({
422
- ...Records.convertFilter(filter),
423
- interface : DwnInterfaceName.Records,
424
- method : [DwnMethodName.Write, DwnMethodName.Delete],
425
- published : false,
426
- });
427
- }
428
-
429
- if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author!)) {
430
- filters.push({
431
- ...Records.convertFilter(filter),
432
- interface : DwnInterfaceName.Records,
433
- method : [DwnMethodName.Write, DwnMethodName.Delete],
434
- recipient : recordsSubscribe.author!,
435
- published : false,
436
- });
437
- }
438
- }
439
- return filters;
440
- }
441
-
442
- /**
443
- * Build a published-only event filter (Write+Delete).
444
- */
445
- private static buildPublishedEventFilter(recordsSubscribe: RecordsSubscribe): Filter {
446
- return {
447
- ...Records.convertFilter(recordsSubscribe.message.descriptor.filter),
448
- interface : DwnInterfaceName.Records,
449
- method : [DwnMethodName.Write, DwnMethodName.Delete],
450
- published : true,
451
- };
452
- }
453
-
454
- // =============================================
455
- // Query filters (for initial snapshot)
456
- // These match Write only and use isLatestBaseState: true
457
- // =============================================
458
-
459
- /**
460
- * Build query filters for owner: latest writes matching the filter.
461
- */
462
- private static buildOwnerQueryFilters(recordsSubscribe: RecordsSubscribe): Filter[] {
463
- const { dateSort, filter } = recordsSubscribe.message.descriptor;
464
- return [{
465
- ...Records.convertFilter(filter, dateSort),
466
- interface : DwnInterfaceName.Records,
467
- method : DwnMethodName.Write,
468
- isLatestBaseState : true,
469
- }];
470
- }
471
-
472
- /**
473
- * Build query filters for non-owner with visibility rules.
474
- */
475
- private static buildNonOwnerQueryFilters(recordsSubscribe: RecordsSubscribe): Filter[] {
476
- const filters: Filter[] = [];
477
- const { dateSort, filter } = recordsSubscribe.message.descriptor;
478
- if (Records.filterIncludesPublishedRecords(filter)) {
479
- filters.push(RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe));
480
- }
481
-
482
- if (Records.filterIncludesUnpublishedRecords(filter)) {
483
- if (EncryptionControl.isExactAudienceFilter(filter)) {
484
- filters.push(Records.buildUnpublishedControlRecordsFilter(filter, dateSort));
485
- }
486
-
487
- if (Records.shouldBuildUnpublishedAuthorFilter(filter, recordsSubscribe.author!)) {
488
- filters.push({
489
- ...Records.convertFilter(filter, dateSort),
490
- author : recordsSubscribe.author!,
491
- interface : DwnInterfaceName.Records,
492
- method : DwnMethodName.Write,
493
- isLatestBaseState : true,
494
- published : false,
495
- });
496
- }
497
-
498
- if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) {
499
- filters.push({
500
- ...Records.convertFilter(filter, dateSort),
501
- interface : DwnInterfaceName.Records,
502
- method : DwnMethodName.Write,
503
- isLatestBaseState : true,
504
- published : false,
505
- });
506
- }
507
-
508
- if (Message.getPermissionGrantId(recordsSubscribe.signaturePayload!) !== undefined) {
509
- filters.push({
510
- ...Records.convertFilter(filter, dateSort),
511
- interface : DwnInterfaceName.Records,
512
- method : DwnMethodName.Write,
513
- isLatestBaseState : true,
514
- published : false,
515
- });
516
- }
517
-
518
- if (Records.shouldBuildUnpublishedRecipientFilter(filter, recordsSubscribe.author!)) {
519
- filters.push({
520
- ...Records.convertFilter(filter, dateSort),
521
- interface : DwnInterfaceName.Records,
522
- method : DwnMethodName.Write,
523
- recipient : recordsSubscribe.author!,
524
- isLatestBaseState : true,
525
- published : false,
526
- });
527
- }
528
- }
529
- return filters;
530
- }
531
-
532
- /**
533
- * Build a published-only query filter (latest writes).
534
- */
535
- private static buildPublishedQueryFilter(recordsSubscribe: RecordsSubscribe): Filter {
536
- const { dateSort, filter } = recordsSubscribe.message.descriptor;
537
- return {
538
- ...Records.convertFilter(filter, dateSort),
539
- interface : DwnInterfaceName.Records,
540
- method : DwnMethodName.Write,
541
- published : true,
542
- isLatestBaseState : true,
543
- };
544
- }
545
-
546
- /**
547
- * @param messageStore Used to check if the grant has been revoked.
548
- */
549
- public static async authorizeRecordsSubscribe(
550
- tenant: string,
551
- recordsSubscribe: RecordsSubscribe,
552
- deps: HandlerDependencies,
553
- ): Promise<void> {
554
-
555
- if (Message.isSignedByAuthorDelegate(recordsSubscribe.message) &&
556
- !EncryptionControl.filterTargetsOnlyControlRecords(recordsSubscribe.message.descriptor.filter)) {
557
- await recordsSubscribe.authorizeDelegate(deps.validationStateReader);
558
- } else if (EncryptionControl.filterTargetsOnlyControlRecords(recordsSubscribe.message.descriptor.filter)) {
559
- await EncryptionControl.authorizeControlReadRequest({
560
- tenant,
561
- incomingMessage : recordsSubscribe.message,
562
- requester : Message.getRequester(recordsSubscribe.message),
563
- validationStateReader : deps.validationStateReader,
564
- });
565
- }
566
-
567
- const permissionGrantId = Message.getPermissionGrantId(recordsSubscribe.signaturePayload!);
568
- if (permissionGrantId !== undefined) {
569
- const permissionGrant = await deps.validationStateReader.fetchGrant(tenant, permissionGrantId);
570
- await RecordsGrantAuthorization.authorizeQueryOrSubscribe({
571
- incomingMessage : recordsSubscribe.message,
572
- expectedGrantor : tenant,
573
- expectedGrantee : recordsSubscribe.author!,
574
- permissionGrant,
575
- validationStateReader : deps.validationStateReader,
576
- });
577
- return;
578
- }
579
-
580
- // NOTE: not all RecordsSubscribe messages require protocol authorization even if the filter includes protocol-related fields,
581
- // this is because we dynamically filter out records that the caller is not authorized to see.
582
- // Currently only run protocol authorization if message deliberately invokes a protocol role.
583
- if (Records.shouldProtocolAuthorize(recordsSubscribe.signaturePayload!)) {
584
- await ProtocolAuthorization.authorizeQueryOrSubscribe(tenant, recordsSubscribe, deps.validationStateReader);
585
- }
586
- }
587
-
588
- private async filterControlRecordsForNonOwner(
589
- tenant: string,
590
- recordsSubscribe: RecordsSubscribe,
591
- requester: string | undefined,
592
- recordsWrites: RecordsQueryReplyEntry[],
593
- ): Promise<RecordsQueryReplyEntry[]> {
594
- return EncryptionControl.filterVisibleControlRecords({
595
- tenant,
596
- incomingMessage : recordsSubscribe.message,
597
- requester,
598
- recordsWriteMessages : recordsWrites,
599
- validationStateReader : this.deps.validationStateReader,
600
- });
601
- }
602
-
603
- private async queryRecordsWithVisibleControlFiltering(
604
- tenant: string,
605
- recordsSubscribe: RecordsSubscribe,
606
- requester: string | undefined,
607
- filters: Filter[],
608
- messageSort: MessageSort,
609
- pagination: { cursor?: PaginationCursor; limit?: number } | undefined,
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
- });
617
- const controlFilters = Records.buildControlRecordsFilters(filters);
618
- const currentAudienceRecordIdCache = new Map<string, string | undefined>();
619
- if (controlFilters.length === 0) {
620
- const result = await queryRecordsWithRecordLimitOccupancy({
621
- messageStore: this.deps.messageStore,
622
- tenant,
623
- filters,
624
- recordLimit,
625
- messageSort,
626
- pagination,
627
- });
628
- return EncryptionControl.projectCurrentAudienceRecordPage({
629
- messageStore: this.deps.messageStore,
630
- tenant,
631
- filters,
632
- currentAudienceRecordIdCache,
633
- result,
634
- });
635
- }
636
-
637
- if (pagination?.limit === undefined || pagination.limit <= 0) {
638
- const result = await queryRecordsWithRecordLimitOccupancy({
639
- messageStore: this.deps.messageStore,
640
- tenant,
641
- filters,
642
- recordLimit,
643
- messageSort,
644
- pagination,
645
- });
646
- const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
647
- messageStore: this.deps.messageStore,
648
- tenant,
649
- filters,
650
- currentAudienceRecordIdCache,
651
- result,
652
- });
653
- return {
654
- messages : await this.filterControlRecordsForNonOwner(tenant, recordsSubscribe, requester, projectedResult.messages),
655
- cursor : projectedResult.cursor,
656
- };
657
- }
658
-
659
- const visibleMessages: RecordsQueryReplyEntry[] = [];
660
- let cursor = pagination.cursor;
661
- let nextCursor: PaginationCursor | undefined;
662
- // Keeps visible-page pagination stable until #1100 moves control visibility into indexed store filters.
663
- do {
664
- const remainingLimit = pagination.limit - visibleMessages.length;
665
- const result = await queryRecordsWithRecordLimitOccupancy({
666
- messageStore : this.deps.messageStore,
667
- tenant,
668
- filters,
669
- recordLimit,
670
- messageSort,
671
- pagination : { ...pagination, cursor, limit: remainingLimit },
672
- });
673
- const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
674
- messageStore: this.deps.messageStore,
675
- tenant,
676
- filters,
677
- currentAudienceRecordIdCache,
678
- result,
679
- });
680
- const filteredMessages = await this.filterControlRecordsForNonOwner(
681
- tenant,
682
- recordsSubscribe,
683
- requester,
684
- projectedResult.messages,
685
- );
686
- visibleMessages.push(...filteredMessages);
687
- nextCursor = projectedResult.cursor;
688
- cursor = projectedResult.cursor;
689
- } while (visibleMessages.length < pagination.limit && cursor !== undefined);
690
-
691
- return { messages: visibleMessages, cursor: nextCursor };
692
- }
693
-
694
316
  }
@@ -38,7 +38,7 @@ export class RecordsCount extends AbstractMessage<RecordsCountMessage> {
38
38
  }
39
39
 
40
40
  await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
41
- Records.validateNestedProtocolPathQueryScope(
41
+ Records.validateNestedProtocolPathScope(
42
42
  message.descriptor.filter,
43
43
  DwnErrorCode.RecordsCountNestedProtocolPathContextIdInvalid,
44
44
  'RecordsCount'
@@ -52,7 +52,7 @@ export class RecordsQuery extends AbstractMessage<RecordsQueryMessage> {
52
52
  }
53
53
 
54
54
  await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
55
- Records.validateNestedProtocolPathQueryScope(
55
+ Records.validateNestedProtocolPathScope(
56
56
  message.descriptor.filter,
57
57
  DwnErrorCode.RecordsQueryNestedProtocolPathContextIdInvalid,
58
58
  'RecordsQuery'
@@ -48,10 +48,13 @@ export class RecordsSubscribe extends AbstractMessage<RecordsSubscribeMessage> {
48
48
  }
49
49
 
50
50
  await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
51
- Records.validateNestedProtocolPathQueryScope(
51
+ Records.validateNestedProtocolPathScope(
52
52
  message.descriptor.filter,
53
53
  DwnErrorCode.RecordsSubscribeNestedProtocolPathContextIdInvalid,
54
- 'RecordsSubscribe'
54
+ 'RecordsSubscribe',
55
+ message.descriptor.cursor === undefined
56
+ && message.descriptor.pagination?.limit !== undefined
57
+ && !signaturePayload?.protocolRole?.includes('/'),
55
58
  );
56
59
 
57
60
  if (signaturePayload?.protocolRole !== undefined) {