@enbox/dwn-sdk-js 0.4.12 → 0.4.13
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/src/core/dwn-error.js +8 -1
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/encryption-control.js +38 -2
- package/dist/esm/src/core/encryption-control.js.map +1 -1
- package/dist/esm/src/core/message-reply.js +14 -1
- package/dist/esm/src/core/message-reply.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-action.js +130 -107
- package/dist/esm/src/core/protocol-authorization-action.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +10 -0
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/event-stream/broadcast-channel-wake-publisher.js +63 -0
- package/dist/esm/src/event-stream/broadcast-channel-wake-publisher.js.map +1 -0
- package/dist/esm/src/handlers/protocols-configure.js +2 -0
- package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +73 -46
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +132 -73
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +1 -0
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js +3 -5
- package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
- package/dist/esm/src/interfaces/records-write-query.js +2 -3
- package/dist/esm/src/interfaces/records-write-query.js.map +1 -1
- package/dist/esm/src/interfaces/records-write.js +50 -35
- package/dist/esm/src/interfaces/records-write.js.map +1 -1
- package/dist/esm/src/jose/jws/general/verifier.js +1 -1
- package/dist/esm/src/store/blockstore-mock.js +3 -0
- package/dist/esm/src/store/blockstore-mock.js.map +1 -1
- package/dist/esm/src/store/index-level-compound.js +34 -18
- package/dist/esm/src/store/index-level-compound.js.map +1 -1
- package/dist/esm/src/store/index-level.js +5 -5
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +2 -2
- package/dist/esm/src/store/message-store-level.js.map +1 -1
- package/dist/esm/src/store/storage-controller.js +26 -17
- package/dist/esm/src/store/storage-controller.js.map +1 -1
- package/dist/esm/src/types/message-types.js.map +1 -1
- package/dist/esm/src/utils/filter.js +4 -8
- package/dist/esm/src/utils/filter.js.map +1 -1
- package/dist/esm/src/utils/messages.js +46 -28
- package/dist/esm/src/utils/messages.js.map +1 -1
- package/dist/esm/src/utils/protocols.js +2 -2
- package/dist/esm/src/utils/protocols.js.map +1 -1
- package/dist/esm/src/utils/records.js +49 -33
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/src/utils/secp256r1.js +1 -1
- package/dist/esm/src/utils/secp256r1.js.map +1 -1
- package/dist/esm/src/utils/url.js +1 -1
- package/dist/esm/src/utils/url.js.map +1 -1
- package/dist/esm/tests/broadcast-channel-wake-publisher.spec.js +74 -0
- package/dist/esm/tests/broadcast-channel-wake-publisher.spec.js.map +1 -0
- package/dist/esm/tests/core/message-reply.spec.js +27 -0
- package/dist/esm/tests/core/message-reply.spec.js.map +1 -1
- package/dist/esm/tests/core/process-message-parity.spec.js +3 -0
- package/dist/esm/tests/core/process-message-parity.spec.js.map +1 -1
- package/dist/esm/tests/durable-event-log.spec.js +37 -0
- package/dist/esm/tests/durable-event-log.spec.js.map +1 -1
- package/dist/esm/tests/features/records-squash.spec.js +5 -0
- package/dist/esm/tests/features/records-squash.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-query.spec.js +14 -28
- package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
- package/dist/esm/tests/handlers/protocols-configure.spec.js +441 -1
- package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +1 -0
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/test-suite.js +8 -0
- package/dist/esm/tests/test-suite.js.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +14 -1
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/encryption-control.d.ts +16 -0
- package/dist/types/src/core/encryption-control.d.ts.map +1 -1
- package/dist/types/src/core/message-reply.d.ts +5 -0
- package/dist/types/src/core/message-reply.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-action.d.ts +6 -1
- package/dist/types/src/core/protocol-authorization-action.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/event-stream/broadcast-channel-wake-publisher.d.ts +35 -0
- package/dist/types/src/event-stream/broadcast-channel-wake-publisher.d.ts.map +1 -0
- package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +18 -0
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-write.d.ts +28 -0
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -0
- 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-write-query.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-write.d.ts +12 -0
- package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
- package/dist/types/src/store/blockstore-mock.d.ts.map +1 -1
- package/dist/types/src/store/index-level-compound.d.ts +4 -1
- package/dist/types/src/store/index-level-compound.d.ts.map +1 -1
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/storage-controller.d.ts +6 -0
- package/dist/types/src/store/storage-controller.d.ts.map +1 -1
- package/dist/types/src/types/jose-types.d.ts +3 -2
- package/dist/types/src/types/jose-types.d.ts.map +1 -1
- package/dist/types/src/types/message-types.d.ts +13 -0
- package/dist/types/src/types/message-types.d.ts.map +1 -1
- package/dist/types/src/utils/filter.d.ts.map +1 -1
- package/dist/types/src/utils/messages.d.ts +14 -0
- package/dist/types/src/utils/messages.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +10 -0
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/broadcast-channel-wake-publisher.spec.d.ts +2 -0
- package/dist/types/tests/broadcast-channel-wake-publisher.spec.d.ts.map +1 -0
- package/dist/types/tests/features/records-squash.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/test-suite.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/dwn-error.ts +15 -1
- package/src/core/encryption-control.ts +66 -2
- package/src/core/message-reply.ts +18 -2
- package/src/core/protocol-authorization-action.ts +167 -117
- package/src/core/protocol-authorization.ts +13 -0
- package/src/event-stream/broadcast-channel-wake-publisher.ts +71 -0
- package/src/handlers/protocols-configure.ts +2 -0
- package/src/handlers/records-subscribe.ts +98 -47
- package/src/handlers/records-write.ts +177 -91
- package/src/index.ts +2 -0
- package/src/interfaces/protocols-configure.ts +6 -8
- package/src/interfaces/records-write-query.ts +2 -3
- package/src/interfaces/records-write.ts +62 -44
- package/src/jose/jws/general/verifier.ts +1 -1
- package/src/store/blockstore-mock.ts +3 -0
- package/src/store/index-level-compound.ts +55 -24
- package/src/store/index-level.ts +6 -4
- package/src/store/message-store-level.ts +2 -2
- package/src/store/storage-controller.ts +27 -16
- package/src/types/jose-types.ts +3 -2
- package/src/types/message-types.ts +16 -0
- package/src/utils/filter.ts +4 -8
- package/src/utils/messages.ts +53 -30
- package/src/utils/protocols.ts +2 -2
- package/src/utils/records.ts +76 -51
- package/src/utils/secp256r1.ts +1 -1
- package/src/utils/url.ts +1 -1
|
@@ -13,6 +13,11 @@ import { DwnError, DwnErrorCode } from './dwn-error.js';
|
|
|
13
13
|
import { getRoleContextPrefix, getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
|
|
14
14
|
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* A DWN message subject to protocol action-rule authorization checks.
|
|
18
|
+
*/
|
|
19
|
+
type ProtocolAuthorizableMessage = RecordsCount | RecordsDelete | RecordsQuery | RecordsRead | RecordsSubscribe | RecordsWrite;
|
|
20
|
+
|
|
16
21
|
/**
|
|
17
22
|
* Check if the incoming message is invoking a role. If so, validate the invoked role.
|
|
18
23
|
* For cross-protocol role invocation, the role record may live in a different protocol
|
|
@@ -20,7 +25,7 @@ import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
|
|
|
20
25
|
*/
|
|
21
26
|
export async function verifyInvokedRole(
|
|
22
27
|
tenant: string,
|
|
23
|
-
incomingMessage:
|
|
28
|
+
incomingMessage: ProtocolAuthorizableMessage,
|
|
24
29
|
protocolUri: string,
|
|
25
30
|
contextId: string | undefined,
|
|
26
31
|
protocolDefinition: ProtocolDefinition,
|
|
@@ -122,38 +127,8 @@ export async function getActionsSeekingARuleMatch(
|
|
|
122
127
|
): Promise<ProtocolAction[]> {
|
|
123
128
|
|
|
124
129
|
switch (incomingMessage.message.descriptor.method) {
|
|
125
|
-
case DwnMethodName.Delete:
|
|
126
|
-
|
|
127
|
-
const recordId = recordsDelete.message.descriptor.recordId;
|
|
128
|
-
const initialWrite = await validationStateReader.fetchInitialRecordsWrite(tenant, recordId);
|
|
129
|
-
|
|
130
|
-
// if there is no initial write, then no action rule can authorize the incoming message, because we won't know who the original author is
|
|
131
|
-
// NOTE: purely defensive programming: currently not reachable
|
|
132
|
-
// because RecordsDelete handler already have an existence check prior to this method being called.
|
|
133
|
-
if (initialWrite === undefined) {
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const actionsThatWouldAuthorizeDelete: ProtocolAction[] = [];
|
|
138
|
-
const prune = recordsDelete.message.descriptor.prune;
|
|
139
|
-
if (prune) {
|
|
140
|
-
actionsThatWouldAuthorizeDelete.push(ProtocolAction.CoPrune);
|
|
141
|
-
|
|
142
|
-
// A prune by the original record author can also be authorized by a 'prune' rule.
|
|
143
|
-
if (incomingMessage.author === initialWrite.author) {
|
|
144
|
-
actionsThatWouldAuthorizeDelete.push(ProtocolAction.Prune);
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
actionsThatWouldAuthorizeDelete.push(ProtocolAction.CoDelete);
|
|
148
|
-
|
|
149
|
-
// A delete by the original record author can also be authorized by a 'delete' rule.
|
|
150
|
-
if (incomingMessage.author === initialWrite.author) {
|
|
151
|
-
actionsThatWouldAuthorizeDelete.push(ProtocolAction.Delete);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return actionsThatWouldAuthorizeDelete;
|
|
156
|
-
}
|
|
130
|
+
case DwnMethodName.Delete:
|
|
131
|
+
return getDeleteActionsSeekingARuleMatch(tenant, incomingMessage, validationStateReader);
|
|
157
132
|
|
|
158
133
|
case DwnMethodName.Count:
|
|
159
134
|
return [ProtocolAction.Read];
|
|
@@ -167,41 +142,91 @@ export async function getActionsSeekingARuleMatch(
|
|
|
167
142
|
case DwnMethodName.Subscribe:
|
|
168
143
|
return [ProtocolAction.Read];
|
|
169
144
|
|
|
170
|
-
case DwnMethodName.Write:
|
|
171
|
-
|
|
145
|
+
case DwnMethodName.Write:
|
|
146
|
+
return getWriteActionsSeekingARuleMatch(tenant, incomingMessage, validationStateReader);
|
|
147
|
+
}
|
|
172
148
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
149
|
+
// purely defensive programming: should not be reachable
|
|
150
|
+
// setting to empty array will prevent any message from being authorized
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handles the `RecordsDelete` case of `getActionsSeekingARuleMatch()`.
|
|
156
|
+
*/
|
|
157
|
+
async function getDeleteActionsSeekingARuleMatch(
|
|
158
|
+
tenant: string,
|
|
159
|
+
incomingMessage: RecordsCount | RecordsDelete | RecordsQuery | RecordsRead | RecordsSubscribe | RecordsWrite,
|
|
160
|
+
validationStateReader: ValidationStateReader,
|
|
161
|
+
): Promise<ProtocolAction[]> {
|
|
162
|
+
const recordsDelete = incomingMessage as RecordsDelete;
|
|
163
|
+
const recordId = recordsDelete.message.descriptor.recordId;
|
|
164
|
+
const initialWrite = await validationStateReader.fetchInitialRecordsWrite(tenant, recordId);
|
|
165
|
+
|
|
166
|
+
// if there is no initial write, then no action rule can authorize the incoming message, because we won't know who the original author is
|
|
167
|
+
// NOTE: purely defensive programming: currently not reachable
|
|
168
|
+
// because RecordsDelete handler already have an existence check prior to this method being called.
|
|
169
|
+
if (initialWrite === undefined) {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
182
172
|
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
const actionsThatWouldAuthorizeDelete: ProtocolAction[] = [];
|
|
174
|
+
const prune = recordsDelete.message.descriptor.prune;
|
|
175
|
+
if (prune) {
|
|
176
|
+
actionsThatWouldAuthorizeDelete.push(ProtocolAction.CoPrune);
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
178
|
+
// A prune by the original record author can also be authorized by a 'prune' rule.
|
|
179
|
+
if (incomingMessage.author === initialWrite.author) {
|
|
180
|
+
actionsThatWouldAuthorizeDelete.push(ProtocolAction.Prune);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
actionsThatWouldAuthorizeDelete.push(ProtocolAction.CoDelete);
|
|
190
184
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} else {
|
|
195
|
-
// An update by someone who is not the record author can only be authorized by a 'co-update' rule.
|
|
196
|
-
return [ProtocolAction.CoUpdate];
|
|
197
|
-
}
|
|
185
|
+
// A delete by the original record author can also be authorized by a 'delete' rule.
|
|
186
|
+
if (incomingMessage.author === initialWrite.author) {
|
|
187
|
+
actionsThatWouldAuthorizeDelete.push(ProtocolAction.Delete);
|
|
198
188
|
}
|
|
199
189
|
}
|
|
200
|
-
}
|
|
201
190
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
return actionsThatWouldAuthorizeDelete;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Handles the `RecordsWrite` case of `getActionsSeekingARuleMatch()`.
|
|
196
|
+
*/
|
|
197
|
+
async function getWriteActionsSeekingARuleMatch(
|
|
198
|
+
tenant: string,
|
|
199
|
+
incomingMessage: RecordsCount | RecordsDelete | RecordsQuery | RecordsRead | RecordsSubscribe | RecordsWrite,
|
|
200
|
+
validationStateReader: ValidationStateReader,
|
|
201
|
+
): Promise<ProtocolAction[]> {
|
|
202
|
+
const incomingRecordsWrite = incomingMessage as RecordsWrite;
|
|
203
|
+
|
|
204
|
+
if (await incomingRecordsWrite.isInitialWrite()) {
|
|
205
|
+
// A squash write seeks the `squash` action first, with fallback to `create`.
|
|
206
|
+
// This means any DID authorized to `create` can also squash when no explicit `squash` rule exists.
|
|
207
|
+
if (incomingRecordsWrite.message.descriptor.squash === true) {
|
|
208
|
+
return [ProtocolAction.Squash, ProtocolAction.Create];
|
|
209
|
+
}
|
|
210
|
+
return [ProtocolAction.Create];
|
|
211
|
+
} else {
|
|
212
|
+
// else incoming RecordsWrite not an initial write
|
|
213
|
+
|
|
214
|
+
const recordId = (incomingMessage as RecordsWrite).message.recordId;
|
|
215
|
+
const initialWrite = await validationStateReader.fetchInitialRecordsWrite(tenant, recordId);
|
|
216
|
+
|
|
217
|
+
// if there is no initial write to update from, then no action rule can authorize the incoming message
|
|
218
|
+
if (initialWrite === undefined) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (incomingMessage.author === initialWrite.author) {
|
|
223
|
+
// 'update' or 'co-update' action authorizes the incoming message
|
|
224
|
+
return [ProtocolAction.CoUpdate, ProtocolAction.Update];
|
|
225
|
+
} else {
|
|
226
|
+
// An update by someone who is not the record author can only be authorized by a 'co-update' rule.
|
|
227
|
+
return [ProtocolAction.CoUpdate];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
205
230
|
}
|
|
206
231
|
|
|
207
232
|
/**
|
|
@@ -235,76 +260,101 @@ export async function authorizeAgainstAllowedActions(
|
|
|
235
260
|
|
|
236
261
|
// Iterate through the action rules to find a rule that authorizes the incoming message.
|
|
237
262
|
for (const actionRule of actionRules) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
(allowedAction: string): boolean => actionsSeekingARuleMatch.includes(allowedAction as ProtocolAction)
|
|
263
|
+
const actionRuleAuthorizes = await actionRuleAuthorizesMessage(
|
|
264
|
+
actionRule, actionsSeekingARuleMatch, author, invokedRole, incomingMessage, recordChain, protocolDefinition
|
|
241
265
|
);
|
|
242
|
-
if (
|
|
243
|
-
|
|
266
|
+
if (actionRuleAuthorizes) {
|
|
267
|
+
return;
|
|
244
268
|
}
|
|
269
|
+
}
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
// No action rules were satisfied, message is not authorized
|
|
272
|
+
throw new DwnError(
|
|
273
|
+
DwnErrorCode.ProtocolAuthorizationActionNotAllowed,
|
|
274
|
+
`Inbound message action Records${incomingMessageMethod} by author ${incomingMessage.author} not allowed.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
248
277
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Evaluates whether a single action rule authorizes the incoming message, mirroring the
|
|
280
|
+
* original per-rule evaluation performed inline in the `authorizeAgainstAllowedActions()` loop.
|
|
281
|
+
* Returns `true` when the rule authorizes the message (the caller should stop and authorize);
|
|
282
|
+
* `false` when the rule does not match and the caller should move on to the next rule.
|
|
283
|
+
*/
|
|
284
|
+
async function actionRuleAuthorizesMessage(
|
|
285
|
+
actionRule: ProtocolActionRule,
|
|
286
|
+
actionsSeekingARuleMatch: ProtocolAction[],
|
|
287
|
+
author: string | undefined,
|
|
288
|
+
invokedRole: string | undefined,
|
|
289
|
+
incomingMessage: RecordsCount | RecordsDelete | RecordsQuery | RecordsRead | RecordsSubscribe | RecordsWrite,
|
|
290
|
+
recordChain: RecordsWriteMessage[],
|
|
291
|
+
protocolDefinition?: ProtocolDefinition,
|
|
292
|
+
): Promise<boolean> {
|
|
293
|
+
// If the action rule does not have an allowed action that matches an action that can authorize the message, skip to evaluate next action rule.
|
|
294
|
+
const ruleHasAMatchingAllowedAction = actionRule.can.some(
|
|
295
|
+
(allowedAction: string): boolean => actionsSeekingARuleMatch.includes(allowedAction as ProtocolAction)
|
|
296
|
+
);
|
|
297
|
+
if (!ruleHasAMatchingAllowedAction) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
253
300
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (author === undefined) {
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
301
|
+
// Code reaches here means this action rule has an allowed action that matches the action of the message.
|
|
302
|
+
// The remaining code checks the actor/author of the incoming message.
|
|
259
303
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
304
|
+
// If the action rule allows `anyone`, then no further checks are needed.
|
|
305
|
+
if (actionRule.who === ProtocolActor.Anyone) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Since not `anyone` is allowed in this action rule, we will need to check the author of the incoming message,
|
|
310
|
+
// if the author of incoming message is not defined, this action rule cannot authorize the incoming message.
|
|
311
|
+
if (author === undefined) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// go through role validation path if a role is invoked by the incoming message
|
|
316
|
+
if (invokedRole !== undefined) {
|
|
317
|
+
// When a protocol role is being invoked, we require that there is a matching `role` rule.
|
|
318
|
+
if (actionRule.role === invokedRole) {
|
|
319
|
+
// role is successfully invoked
|
|
320
|
+
return true;
|
|
321
|
+
} else {
|
|
322
|
+
return false;
|
|
269
323
|
}
|
|
324
|
+
}
|
|
270
325
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// If `of` is not set, handle it as a special case
|
|
274
|
-
// NOTE: `of` is always set if `who` is set to `author` (we do this check in `validateRuleSetRecursively()`)
|
|
275
|
-
if (actionRule.who === ProtocolActor.Recipient && actionRule.of === undefined) {
|
|
276
|
-
// If the action rule specifies a recipient without `of` and the incoming message is authenticated:
|
|
277
|
-
|
|
278
|
-
// Author must be recipient of the record being accessed
|
|
279
|
-
let recordsWriteMessage: RecordsWriteMessage;
|
|
280
|
-
if (incomingMessage.message.descriptor.method === DwnMethodName.Write) {
|
|
281
|
-
recordsWriteMessage = incomingMessage.message as RecordsWriteMessage;
|
|
282
|
-
} else {
|
|
283
|
-
// else the incoming message must be a `RecordsDelete` because only `co-update`, `co-delete`, `co-prune` are allowed recipient actions,
|
|
284
|
-
// (we do this check in `validateRuleSetRecursively()`)
|
|
285
|
-
// and we have already checked that the incoming message is not a `RecordsWrite` above which covers `co-update` path.
|
|
286
|
-
recordsWriteMessage = recordChain[recordChain.length - 1];
|
|
287
|
-
}
|
|
326
|
+
// else we go through the actor (`who`) validation
|
|
288
327
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
// If `of` is not set, handle it as a special case
|
|
329
|
+
// NOTE: `of` is always set if `who` is set to `author` (we do this check in `validateRuleSetRecursively()`)
|
|
330
|
+
if (actionRule.who === ProtocolActor.Recipient && actionRule.of === undefined) {
|
|
331
|
+
// If the action rule specifies a recipient without `of` and the incoming message is authenticated:
|
|
332
|
+
|
|
333
|
+
// Author must be recipient of the record being accessed
|
|
334
|
+
let recordsWriteMessage: RecordsWriteMessage;
|
|
335
|
+
if (incomingMessage.message.descriptor.method === DwnMethodName.Write) {
|
|
336
|
+
recordsWriteMessage = incomingMessage.message as RecordsWriteMessage;
|
|
337
|
+
} else {
|
|
338
|
+
// else the incoming message must be a `RecordsDelete` because only `co-update`, `co-delete`, `co-prune` are allowed recipient actions,
|
|
339
|
+
// (we do this check in `validateRuleSetRecursively()`)
|
|
340
|
+
// and we have already checked that the incoming message is not a `RecordsWrite` above which covers `co-update` path.
|
|
341
|
+
recordsWriteMessage = recordChain.at(-1)!;
|
|
294
342
|
}
|
|
295
343
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return;
|
|
344
|
+
if (recordsWriteMessage.descriptor.recipient === author) {
|
|
345
|
+
return true;
|
|
346
|
+
} else {
|
|
347
|
+
return false;
|
|
300
348
|
}
|
|
301
349
|
}
|
|
302
350
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
351
|
+
// validate the actor is allowed by the current action rule
|
|
352
|
+
const ancestorRuleSuccess: boolean = await checkActor(author, actionRule, recordChain, protocolDefinition);
|
|
353
|
+
if (ancestorRuleSuccess) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false;
|
|
308
358
|
}
|
|
309
359
|
|
|
310
360
|
/**
|
|
@@ -108,6 +108,19 @@ export class ProtocolAuthorization {
|
|
|
108
108
|
): Promise<void> {
|
|
109
109
|
await ProtocolAuthorization.verifyStoredInitialWrite(incomingMessage);
|
|
110
110
|
|
|
111
|
+
// Encryption control records live at reserved virtual paths and are admitted through
|
|
112
|
+
// `EncryptionControl` validation (see `validateReferentialIntegrity()`), never against the
|
|
113
|
+
// app definition's structure. Revalidate their config-owned admission invariants against
|
|
114
|
+
// the role rule set that governed the record timestamp, then apply the same conservative
|
|
115
|
+
// static-create authorization replay used for ordinary records.
|
|
116
|
+
if (isEncryptionControlPath(incomingMessage.message.descriptor.protocolPath)) {
|
|
117
|
+
const governingRuleSet = await EncryptionControl.validateStoredControlRecord(
|
|
118
|
+
tenant, incomingMessage, validationStateReader
|
|
119
|
+
);
|
|
120
|
+
ProtocolAuthorization.verifyStoredInitialWriteCreateAction(tenant, incomingMessage, governingRuleSet);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
const protocolDefinitionTimestamp = incomingMessage.message.descriptor.messageTimestamp;
|
|
112
125
|
const protocolDefinition = await validationStateReader.fetchProtocolDefinition(
|
|
113
126
|
tenant,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { WakeListener } from './event-emitter-wake-publisher.js';
|
|
2
|
+
import type { Wake, WakePublisher, WakeSubscriber } from '../types/subscriptions.js';
|
|
3
|
+
|
|
4
|
+
import { EventEmitterWakePublisher } from './event-emitter-wake-publisher.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cross-context {@link WakePublisher} + {@link WakeSubscriber}: fans each wake
|
|
8
|
+
* out to in-process listeners AND mirrors it over a named `BroadcastChannel`,
|
|
9
|
+
* so sibling execution contexts sharing the same underlying store (browser
|
|
10
|
+
* tabs, workers, a SharedWorker over one IndexedDB) observe each other's
|
|
11
|
+
* commits immediately instead of waiting for their event log's idle re-drain.
|
|
12
|
+
*
|
|
13
|
+
* Wakes received from the channel are delivered to in-process listeners only
|
|
14
|
+
* and never re-posted, so wakes cannot loop between contexts. Environments
|
|
15
|
+
* without `BroadcastChannel` degrade to in-process-only delivery, and
|
|
16
|
+
* publication stays best-effort by contract: channel failures must not affect
|
|
17
|
+
* the write path that already committed the row.
|
|
18
|
+
*
|
|
19
|
+
* In Node/Bun, `BroadcastChannel` spans only threads within one process —
|
|
20
|
+
* multi-process server deployments still converge through the event log's
|
|
21
|
+
* idle re-drain rather than through this bridge.
|
|
22
|
+
*/
|
|
23
|
+
export class BroadcastChannelWakePublisher implements WakePublisher, WakeSubscriber {
|
|
24
|
+
private readonly _local = new EventEmitterWakePublisher();
|
|
25
|
+
private readonly _channel?: BroadcastChannel;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param channelName - Channel identity. Contexts sharing one physical
|
|
29
|
+
* store must use the same name; derive it from the store location so
|
|
30
|
+
* distinct stores on the same origin do not cross-wake.
|
|
31
|
+
*/
|
|
32
|
+
public constructor(channelName: string) {
|
|
33
|
+
if (typeof BroadcastChannel === 'undefined') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._channel = new BroadcastChannel(channelName);
|
|
38
|
+
// Node's channel keeps the event loop alive unless unreferenced; the
|
|
39
|
+
// browser implementation has no `unref` and needs none.
|
|
40
|
+
(this._channel as { unref?: () => void }).unref?.();
|
|
41
|
+
this._channel.onmessage = (event: MessageEvent): void => {
|
|
42
|
+
const wake = event.data as Wake | undefined;
|
|
43
|
+
if (typeof wake?.tenant !== 'string' || typeof wake?.seq !== 'string') {
|
|
44
|
+
return; // not a wake — ignore foreign traffic on the channel
|
|
45
|
+
}
|
|
46
|
+
this._local.publish(wake);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public publish(wake: Wake): void {
|
|
51
|
+
this._local.publish(wake);
|
|
52
|
+
try {
|
|
53
|
+
this._channel?.postMessage(wake);
|
|
54
|
+
} catch {
|
|
55
|
+
// Best-effort by contract — e.g. a closed channel.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public subscribe(listener: WakeListener): () => void {
|
|
60
|
+
return this._local.subscribe(listener);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public clear(): void {
|
|
64
|
+
this._local.clear();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Releases the underlying channel. In-process delivery keeps working. */
|
|
68
|
+
public close(): void {
|
|
69
|
+
this._channel?.close();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -20,6 +20,8 @@ import { getRuleSetAtPath, getTypeName, parseCrossProtocolRef } from '../utils/p
|
|
|
20
20
|
type StoredInitialWriteConfigValidity = 'valid' | 'invalid' | 'unknown';
|
|
21
21
|
|
|
22
22
|
const STORED_INITIAL_WRITE_CONFIG_INVALID_CODES = new Set<string>([
|
|
23
|
+
DwnErrorCode.EncryptionControlValidateAudienceRolePathInvalid,
|
|
24
|
+
DwnErrorCode.EncryptionControlValidateAudienceSealKeyIdMismatch,
|
|
23
25
|
DwnErrorCode.ProtocolAuthorizationEncryptionRequired,
|
|
24
26
|
DwnErrorCode.ProtocolAuthorizationIncorrectDataFormat,
|
|
25
27
|
DwnErrorCode.ProtocolAuthorizationInitialWriteRevalidationNotInitial,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MessageSort } from '../types/message-types.js';
|
|
2
|
-
import type { EventSubscription, ProgressGapInfo, SubscriptionEvent, SubscriptionListener, SubscriptionMessage } from '../types/subscriptions.js';
|
|
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';
|
|
@@ -51,34 +51,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
51
51
|
return messageReplyFromError(e, 400);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
let eventFilters: Filter[] = [];
|
|
55
|
-
let queryFilters: Filter[] = [];
|
|
56
54
|
const requester = Message.getRequester(recordsSubscribe.message);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
eventFilters = [RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe)];
|
|
61
|
-
queryFilters = [RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe)];
|
|
62
|
-
// delete the undefined authorization property else the code will encounter the following IPLD issue when attempting to generate CID:
|
|
63
|
-
// Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded
|
|
64
|
-
delete message.authorization;
|
|
65
|
-
} else {
|
|
66
|
-
// authentication and authorization
|
|
67
|
-
try {
|
|
68
|
-
await authenticate(message.authorization!, this.deps.didResolver);
|
|
69
|
-
await RecordsSubscribeHandler.authorizeRecordsSubscribe(tenant, recordsSubscribe, this.deps);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
return messageReplyFromError(error, 401);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (recordsSubscribe.author === tenant) {
|
|
75
|
-
eventFilters = RecordsSubscribeHandler.buildOwnerEventFilters(recordsSubscribe);
|
|
76
|
-
queryFilters = RecordsSubscribeHandler.buildOwnerQueryFilters(recordsSubscribe);
|
|
77
|
-
} else {
|
|
78
|
-
eventFilters = RecordsSubscribeHandler.buildNonOwnerEventFilters(recordsSubscribe);
|
|
79
|
-
queryFilters = RecordsSubscribeHandler.buildNonOwnerQueryFilters(recordsSubscribe);
|
|
80
|
-
}
|
|
55
|
+
const filterResolution = await this.resolveSubscriptionFilters(tenant, message, recordsSubscribe);
|
|
56
|
+
if ('errorReply' in filterResolution) {
|
|
57
|
+
return filterResolution.errorReply;
|
|
81
58
|
}
|
|
59
|
+
const { eventFilters, queryFilters } = filterResolution;
|
|
82
60
|
|
|
83
61
|
const messageCid = await Message.getCid(message);
|
|
84
62
|
const { cursor: eventLogCursor } = recordsSubscribe.message.descriptor;
|
|
@@ -95,34 +73,107 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
95
73
|
// All catch-up, buffering, dedup, and EOSE delivery are handled by the
|
|
96
74
|
// EventLog implementation. The handler just passes the cursor and filters.
|
|
97
75
|
// The subscriptionHandler receives SubscriptionMessage (event + EOSE) directly.
|
|
76
|
+
return this.handleCursorSubscription(tenant, messageCid, eventFilters, eventLogCursor, projectedSubscriptionHandler);
|
|
77
|
+
}
|
|
98
78
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
// ---- No cursor: existing behavior (initial snapshot from MessageStore) ----
|
|
80
|
+
return this.handleSnapshotSubscription(
|
|
81
|
+
tenant, messageCid, recordsSubscribe, requester, eventFilters, queryFilters, projectedSubscriptionHandler
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolves the event/query filters for the subscription, performing authentication and
|
|
87
|
+
* authorization when the request is not an anonymous published-records-only subscribe.
|
|
88
|
+
* Returns the resolved filters, or an `errorReply` if authentication/authorization failed.
|
|
89
|
+
*/
|
|
90
|
+
private async resolveSubscriptionFilters(
|
|
91
|
+
tenant: string,
|
|
92
|
+
message: RecordsSubscribeMessage,
|
|
93
|
+
recordsSubscribe: RecordsSubscribe,
|
|
94
|
+
): Promise<{ eventFilters: Filter[]; queryFilters: Filter[] } | { errorReply: RecordsSubscribeReply }> {
|
|
95
|
+
// if this is an anonymous subscribe and the filter supports published records, subscribe to only published records
|
|
96
|
+
if (Records.filterIncludesPublishedRecords(recordsSubscribe.message.descriptor.filter) && recordsSubscribe.author === undefined) {
|
|
97
|
+
const eventFilters = [RecordsSubscribeHandler.buildPublishedEventFilter(recordsSubscribe)];
|
|
98
|
+
const queryFilters = [RecordsSubscribeHandler.buildPublishedQueryFilter(recordsSubscribe)];
|
|
99
|
+
// delete the undefined authorization property else the code will encounter the following IPLD issue when attempting to generate CID:
|
|
100
|
+
// Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded
|
|
101
|
+
delete message.authorization;
|
|
102
|
+
return { eventFilters, queryFilters };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// authentication and authorization
|
|
106
|
+
try {
|
|
107
|
+
await authenticate(message.authorization!, this.deps.didResolver);
|
|
108
|
+
await RecordsSubscribeHandler.authorizeRecordsSubscribe(tenant, recordsSubscribe, this.deps);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { errorReply: messageReplyFromError(error, 401) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (recordsSubscribe.author === tenant) {
|
|
114
|
+
return {
|
|
115
|
+
eventFilters : RecordsSubscribeHandler.buildOwnerEventFilters(recordsSubscribe),
|
|
116
|
+
queryFilters : RecordsSubscribeHandler.buildOwnerQueryFilters(recordsSubscribe),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
eventFilters : RecordsSubscribeHandler.buildNonOwnerEventFilters(recordsSubscribe),
|
|
122
|
+
queryFilters : RecordsSubscribeHandler.buildNonOwnerQueryFilters(recordsSubscribe),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handles cursor-mode subscription: catch-up from EventLog + EOSE + live. All catch-up,
|
|
128
|
+
* buffering, dedup, and EOSE delivery are handled by the EventLog implementation. The handler
|
|
129
|
+
* just passes the cursor and filters. The subscriptionHandler receives SubscriptionMessage
|
|
130
|
+
* (event + EOSE) directly.
|
|
131
|
+
*/
|
|
132
|
+
private async handleCursorSubscription(
|
|
133
|
+
tenant: string,
|
|
134
|
+
messageCid: string,
|
|
135
|
+
eventFilters: Filter[],
|
|
136
|
+
eventLogCursor: ProgressToken,
|
|
137
|
+
projectedSubscriptionHandler: ProjectedRecordsSubscriptionHandler,
|
|
138
|
+
): Promise<RecordsSubscribeReply> {
|
|
139
|
+
try {
|
|
140
|
+
const subscription = await this.deps.eventLog!.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
|
|
141
|
+
cursor : eventLogCursor,
|
|
142
|
+
filters : eventFilters,
|
|
143
|
+
});
|
|
144
|
+
await projectedSubscriptionHandler.setSubscription(subscription);
|
|
105
145
|
|
|
146
|
+
return {
|
|
147
|
+
status: { code: 200, detail: 'OK' },
|
|
148
|
+
subscription,
|
|
149
|
+
};
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
|
|
152
|
+
const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
|
|
106
153
|
return {
|
|
107
|
-
status: { code:
|
|
108
|
-
|
|
154
|
+
status : { code: 410, detail: 'Progress token gap' },
|
|
155
|
+
error : gapInfo === undefined ? undefined : { code: 'ProgressGap' as const, ...gapInfo },
|
|
109
156
|
};
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
|
|
112
|
-
const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
|
|
113
|
-
return {
|
|
114
|
-
status : { code: 410, detail: 'Progress token gap' },
|
|
115
|
-
error : gapInfo === undefined ? undefined : { code: 'ProgressGap' as const, ...gapInfo },
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
return messageReplyFromError(error, 500);
|
|
119
157
|
}
|
|
158
|
+
return messageReplyFromError(error, 500);
|
|
120
159
|
}
|
|
160
|
+
}
|
|
121
161
|
|
|
122
|
-
|
|
123
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Handles the no-cursor path: registers the event listener first (so no events are missed
|
|
164
|
+
* between query and subscribe), then queries for the initial snapshot of matching records.
|
|
165
|
+
*/
|
|
166
|
+
private async handleSnapshotSubscription(
|
|
167
|
+
tenant: string,
|
|
168
|
+
messageCid: string,
|
|
169
|
+
recordsSubscribe: RecordsSubscribe,
|
|
170
|
+
requester: string | undefined,
|
|
171
|
+
eventFilters: Filter[],
|
|
172
|
+
queryFilters: Filter[],
|
|
173
|
+
projectedSubscriptionHandler: ProjectedRecordsSubscriptionHandler,
|
|
174
|
+
): Promise<RecordsSubscribeReply> {
|
|
124
175
|
// Step 1: Register event listener FIRST to ensure no events are missed between query and subscribe
|
|
125
|
-
const subscription = await this.deps.eventLog
|
|
176
|
+
const subscription = await this.deps.eventLog!.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
|
|
126
177
|
filters: eventFilters,
|
|
127
178
|
});
|
|
128
179
|
await projectedSubscriptionHandler.setSubscription(subscription);
|