@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.
- package/dist/browser.mjs +1 -1
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/generated/precompiled-validators.js +978 -840
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/json-schemas/definitions.json +5 -0
- package/dist/esm/src/core/dwn-constant.js +2 -2
- package/dist/esm/src/core/dwn-error.js +0 -1
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +1 -24
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +1 -5
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/core/validation-state-reader.js +2 -3
- package/dist/esm/src/core/validation-state-reader.js.map +1 -1
- package/dist/esm/src/handlers/records-collection.js +200 -0
- package/dist/esm/src/handlers/records-collection.js.map +1 -0
- package/dist/esm/src/handlers/records-count.js +26 -195
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +19 -373
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +21 -338
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +8 -26
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js +4 -9
- package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
- package/dist/esm/src/interfaces/records-count.js +1 -1
- package/dist/esm/src/interfaces/records-count.js.map +1 -1
- package/dist/esm/src/interfaces/records-query.js +1 -1
- package/dist/esm/src/interfaces/records-query.js.map +1 -1
- package/dist/esm/src/interfaces/records-subscribe.js +3 -1
- package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
- package/dist/esm/src/store/index-level.js +210 -21
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +192 -21
- package/dist/esm/src/store/message-store-level.js.map +1 -1
- package/dist/esm/src/store/storage-controller.js +1 -3
- package/dist/esm/src/store/storage-controller.js.map +1 -1
- package/dist/esm/src/types/protocols-types.js +0 -10
- package/dist/esm/src/types/protocols-types.js.map +1 -1
- package/dist/esm/src/utils/filter.js +35 -1
- package/dist/esm/src/utils/filter.js.map +1 -1
- package/dist/esm/src/utils/grant-key-coverage.js +4 -6
- package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
- package/dist/esm/src/utils/messages.js +4 -14
- package/dist/esm/src/utils/messages.js.map +1 -1
- package/dist/esm/src/utils/permission-scope.js +3 -9
- package/dist/esm/src/utils/permission-scope.js.map +1 -1
- package/dist/esm/src/utils/record-limit-occupancy.js +73 -202
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +19 -52
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
- package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
- package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
- package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
- package/dist/esm/tests/dwn.spec.js +2 -2
- package/dist/esm/tests/dwn.spec.js.map +1 -1
- package/dist/esm/tests/features/records-delivery.spec.js +2 -5
- package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
- package/dist/esm/tests/features/records-nested-query-scope.spec.js +149 -70
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +380 -95
- package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
- package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-count.spec.js +52 -1
- package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +57 -3
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/store/index-level.spec.js +208 -1
- package/dist/esm/tests/store/index-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
- package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
- package/dist/esm/tests/store/message-store-level.spec.js +76 -1
- package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store.spec.js +81 -0
- package/dist/esm/tests/store/message-store.spec.js.map +1 -1
- package/dist/esm/tests/utils/filters.spec.js +25 -4
- package/dist/esm/tests/utils/filters.spec.js.map +1 -1
- package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
- package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
- package/dist/esm/tests/utils/records.spec.js +12 -0
- package/dist/esm/tests/utils/records.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/dwn-constant.d.ts +2 -2
- package/dist/types/src/core/dwn-error.d.ts +0 -1
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
- package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
- package/dist/types/src/handlers/records-collection.d.ts +32 -0
- package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
- package/dist/types/src/handlers/records-count.d.ts +0 -34
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts +0 -61
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-write.d.ts +3 -0
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/store/index-level.d.ts +61 -0
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +24 -6
- package/dist/types/src/store/message-store-level.d.ts.map +1 -1
- package/dist/types/src/store/storage-controller.d.ts.map +1 -1
- package/dist/types/src/types/message-store.d.ts +28 -2
- package/dist/types/src/types/message-store.d.ts.map +1 -1
- package/dist/types/src/types/protocols-types.d.ts +7 -24
- package/dist/types/src/types/protocols-types.d.ts.map +1 -1
- package/dist/types/src/types/query-types.d.ts +8 -1
- package/dist/types/src/types/query-types.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +3 -2
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/utils/filter.d.ts +14 -1
- package/dist/types/src/utils/filter.d.ts.map +1 -1
- package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
- package/dist/types/src/utils/messages.d.ts.map +1 -1
- package/dist/types/src/utils/permission-scope.d.ts +0 -2
- package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
- package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
- package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +6 -24
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
- package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
- package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
- package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/dwn-constant.ts +2 -2
- package/src/core/dwn-error.ts +0 -2
- package/src/core/protocol-authorization-validation.ts +1 -33
- package/src/core/protocol-authorization.ts +0 -7
- package/src/core/validation-state-reader.ts +2 -3
- package/src/handlers/records-collection.ts +269 -0
- package/src/handlers/records-count.ts +38 -232
- package/src/handlers/records-query.ts +22 -444
- package/src/handlers/records-subscribe.ts +26 -397
- package/src/handlers/records-write.ts +17 -32
- package/src/index.ts +3 -3
- package/src/interfaces/protocols-configure.ts +4 -13
- package/src/interfaces/records-count.ts +1 -1
- package/src/interfaces/records-query.ts +1 -1
- package/src/interfaces/records-subscribe.ts +5 -2
- package/src/store/index-level.ts +323 -28
- package/src/store/message-store-level.ts +251 -25
- package/src/store/storage-controller.ts +1 -3
- package/src/types/message-store.ts +30 -2
- package/src/types/protocols-types.ts +7 -26
- package/src/types/query-types.ts +10 -2
- package/src/types/records-types.ts +3 -2
- package/src/utils/filter.ts +40 -3
- package/src/utils/grant-key-coverage.ts +4 -7
- package/src/utils/messages.ts +4 -14
- package/src/utils/permission-scope.ts +4 -11
- package/src/utils/record-limit-occupancy.ts +108 -293
- 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 {
|
|
13
|
-
import {
|
|
14
|
-
|
|
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 (
|
|
28
|
-
return messageReplyFromError(
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
|
148
|
-
validationStateReader : this.deps.validationStateReader,
|
|
66
|
+
messageStore: this.deps.messageStore,
|
|
149
67
|
tenant,
|
|
150
68
|
filters,
|
|
151
|
-
|
|
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,
|
|
102
|
+
return this.countProjectedRecords(tenant, filters, recordLimit);
|
|
184
103
|
}
|
|
185
104
|
|
|
186
|
-
const totalCount = await this.countProjectedRecords(tenant,
|
|
187
|
-
const controlCount = await this.countProjectedRecords(tenant,
|
|
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
|
|
194
|
-
validationStateReader : this.deps.validationStateReader,
|
|
112
|
+
messageStore : this.deps.messageStore,
|
|
195
113
|
tenant,
|
|
196
|
-
filters
|
|
197
|
-
|
|
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
|
}
|