@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.
Files changed (142) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/src/core/dwn-error.js +8 -1
  4. package/dist/esm/src/core/dwn-error.js.map +1 -1
  5. package/dist/esm/src/core/encryption-control.js +38 -2
  6. package/dist/esm/src/core/encryption-control.js.map +1 -1
  7. package/dist/esm/src/core/message-reply.js +14 -1
  8. package/dist/esm/src/core/message-reply.js.map +1 -1
  9. package/dist/esm/src/core/protocol-authorization-action.js +130 -107
  10. package/dist/esm/src/core/protocol-authorization-action.js.map +1 -1
  11. package/dist/esm/src/core/protocol-authorization.js +10 -0
  12. package/dist/esm/src/core/protocol-authorization.js.map +1 -1
  13. package/dist/esm/src/event-stream/broadcast-channel-wake-publisher.js +63 -0
  14. package/dist/esm/src/event-stream/broadcast-channel-wake-publisher.js.map +1 -0
  15. package/dist/esm/src/handlers/protocols-configure.js +2 -0
  16. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  17. package/dist/esm/src/handlers/records-subscribe.js +73 -46
  18. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  19. package/dist/esm/src/handlers/records-write.js +132 -73
  20. package/dist/esm/src/handlers/records-write.js.map +1 -1
  21. package/dist/esm/src/index.js +1 -0
  22. package/dist/esm/src/index.js.map +1 -1
  23. package/dist/esm/src/interfaces/protocols-configure.js +3 -5
  24. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  25. package/dist/esm/src/interfaces/records-write-query.js +2 -3
  26. package/dist/esm/src/interfaces/records-write-query.js.map +1 -1
  27. package/dist/esm/src/interfaces/records-write.js +50 -35
  28. package/dist/esm/src/interfaces/records-write.js.map +1 -1
  29. package/dist/esm/src/jose/jws/general/verifier.js +1 -1
  30. package/dist/esm/src/store/blockstore-mock.js +3 -0
  31. package/dist/esm/src/store/blockstore-mock.js.map +1 -1
  32. package/dist/esm/src/store/index-level-compound.js +34 -18
  33. package/dist/esm/src/store/index-level-compound.js.map +1 -1
  34. package/dist/esm/src/store/index-level.js +5 -5
  35. package/dist/esm/src/store/index-level.js.map +1 -1
  36. package/dist/esm/src/store/message-store-level.js +2 -2
  37. package/dist/esm/src/store/message-store-level.js.map +1 -1
  38. package/dist/esm/src/store/storage-controller.js +26 -17
  39. package/dist/esm/src/store/storage-controller.js.map +1 -1
  40. package/dist/esm/src/types/message-types.js.map +1 -1
  41. package/dist/esm/src/utils/filter.js +4 -8
  42. package/dist/esm/src/utils/filter.js.map +1 -1
  43. package/dist/esm/src/utils/messages.js +46 -28
  44. package/dist/esm/src/utils/messages.js.map +1 -1
  45. package/dist/esm/src/utils/protocols.js +2 -2
  46. package/dist/esm/src/utils/protocols.js.map +1 -1
  47. package/dist/esm/src/utils/records.js +49 -33
  48. package/dist/esm/src/utils/records.js.map +1 -1
  49. package/dist/esm/src/utils/secp256r1.js +1 -1
  50. package/dist/esm/src/utils/secp256r1.js.map +1 -1
  51. package/dist/esm/src/utils/url.js +1 -1
  52. package/dist/esm/src/utils/url.js.map +1 -1
  53. package/dist/esm/tests/broadcast-channel-wake-publisher.spec.js +74 -0
  54. package/dist/esm/tests/broadcast-channel-wake-publisher.spec.js.map +1 -0
  55. package/dist/esm/tests/core/message-reply.spec.js +27 -0
  56. package/dist/esm/tests/core/message-reply.spec.js.map +1 -1
  57. package/dist/esm/tests/core/process-message-parity.spec.js +3 -0
  58. package/dist/esm/tests/core/process-message-parity.spec.js.map +1 -1
  59. package/dist/esm/tests/durable-event-log.spec.js +37 -0
  60. package/dist/esm/tests/durable-event-log.spec.js.map +1 -1
  61. package/dist/esm/tests/features/records-squash.spec.js +5 -0
  62. package/dist/esm/tests/features/records-squash.spec.js.map +1 -1
  63. package/dist/esm/tests/handlers/messages-query.spec.js +14 -28
  64. package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
  65. package/dist/esm/tests/handlers/protocols-configure.spec.js +441 -1
  66. package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
  67. package/dist/esm/tests/handlers/records-write.spec.js +1 -0
  68. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  69. package/dist/esm/tests/test-suite.js +8 -0
  70. package/dist/esm/tests/test-suite.js.map +1 -1
  71. package/dist/types/src/core/dwn-error.d.ts +14 -1
  72. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  73. package/dist/types/src/core/encryption-control.d.ts +16 -0
  74. package/dist/types/src/core/encryption-control.d.ts.map +1 -1
  75. package/dist/types/src/core/message-reply.d.ts +5 -0
  76. package/dist/types/src/core/message-reply.d.ts.map +1 -1
  77. package/dist/types/src/core/protocol-authorization-action.d.ts +6 -1
  78. package/dist/types/src/core/protocol-authorization-action.d.ts.map +1 -1
  79. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  80. package/dist/types/src/event-stream/broadcast-channel-wake-publisher.d.ts +35 -0
  81. package/dist/types/src/event-stream/broadcast-channel-wake-publisher.d.ts.map +1 -0
  82. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  83. package/dist/types/src/handlers/records-subscribe.d.ts +18 -0
  84. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  85. package/dist/types/src/handlers/records-write.d.ts +28 -0
  86. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  87. package/dist/types/src/index.d.ts +2 -0
  88. package/dist/types/src/index.d.ts.map +1 -1
  89. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  90. package/dist/types/src/interfaces/records-write-query.d.ts.map +1 -1
  91. package/dist/types/src/interfaces/records-write.d.ts +12 -0
  92. package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
  93. package/dist/types/src/store/blockstore-mock.d.ts.map +1 -1
  94. package/dist/types/src/store/index-level-compound.d.ts +4 -1
  95. package/dist/types/src/store/index-level-compound.d.ts.map +1 -1
  96. package/dist/types/src/store/index-level.d.ts.map +1 -1
  97. package/dist/types/src/store/storage-controller.d.ts +6 -0
  98. package/dist/types/src/store/storage-controller.d.ts.map +1 -1
  99. package/dist/types/src/types/jose-types.d.ts +3 -2
  100. package/dist/types/src/types/jose-types.d.ts.map +1 -1
  101. package/dist/types/src/types/message-types.d.ts +13 -0
  102. package/dist/types/src/types/message-types.d.ts.map +1 -1
  103. package/dist/types/src/utils/filter.d.ts.map +1 -1
  104. package/dist/types/src/utils/messages.d.ts +14 -0
  105. package/dist/types/src/utils/messages.d.ts.map +1 -1
  106. package/dist/types/src/utils/records.d.ts +10 -0
  107. package/dist/types/src/utils/records.d.ts.map +1 -1
  108. package/dist/types/tests/broadcast-channel-wake-publisher.spec.d.ts +2 -0
  109. package/dist/types/tests/broadcast-channel-wake-publisher.spec.d.ts.map +1 -0
  110. package/dist/types/tests/features/records-squash.spec.d.ts.map +1 -1
  111. package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
  112. package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
  113. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  114. package/dist/types/tests/test-suite.d.ts.map +1 -1
  115. package/package.json +4 -4
  116. package/src/core/dwn-error.ts +15 -1
  117. package/src/core/encryption-control.ts +66 -2
  118. package/src/core/message-reply.ts +18 -2
  119. package/src/core/protocol-authorization-action.ts +167 -117
  120. package/src/core/protocol-authorization.ts +13 -0
  121. package/src/event-stream/broadcast-channel-wake-publisher.ts +71 -0
  122. package/src/handlers/protocols-configure.ts +2 -0
  123. package/src/handlers/records-subscribe.ts +98 -47
  124. package/src/handlers/records-write.ts +177 -91
  125. package/src/index.ts +2 -0
  126. package/src/interfaces/protocols-configure.ts +6 -8
  127. package/src/interfaces/records-write-query.ts +2 -3
  128. package/src/interfaces/records-write.ts +62 -44
  129. package/src/jose/jws/general/verifier.ts +1 -1
  130. package/src/store/blockstore-mock.ts +3 -0
  131. package/src/store/index-level-compound.ts +55 -24
  132. package/src/store/index-level.ts +6 -4
  133. package/src/store/message-store-level.ts +2 -2
  134. package/src/store/storage-controller.ts +27 -16
  135. package/src/types/jose-types.ts +3 -2
  136. package/src/types/message-types.ts +16 -0
  137. package/src/utils/filter.ts +4 -8
  138. package/src/utils/messages.ts +53 -30
  139. package/src/utils/protocols.ts +2 -2
  140. package/src/utils/records.ts +76 -51
  141. package/src/utils/secp256r1.ts +1 -1
  142. 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: RecordsCount | RecordsDelete | RecordsQuery | RecordsRead | RecordsSubscribe | RecordsWrite,
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
- const recordsDelete = incomingMessage as RecordsDelete;
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
- const incomingRecordsWrite = incomingMessage as RecordsWrite;
145
+ case DwnMethodName.Write:
146
+ return getWriteActionsSeekingARuleMatch(tenant, incomingMessage, validationStateReader);
147
+ }
172
148
 
173
- if (await incomingRecordsWrite.isInitialWrite()) {
174
- // A squash write seeks the `squash` action first, with fallback to `create`.
175
- // This means any DID authorized to `create` can also squash when no explicit `squash` rule exists.
176
- if (incomingRecordsWrite.message.descriptor.squash === true) {
177
- return [ProtocolAction.Squash, ProtocolAction.Create];
178
- }
179
- return [ProtocolAction.Create];
180
- } else {
181
- // else incoming RecordsWrite not an initial write
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
- const recordId = (incomingMessage as RecordsWrite).message.recordId;
184
- const initialWrite = await validationStateReader.fetchInitialRecordsWrite(tenant, recordId);
173
+ const actionsThatWouldAuthorizeDelete: ProtocolAction[] = [];
174
+ const prune = recordsDelete.message.descriptor.prune;
175
+ if (prune) {
176
+ actionsThatWouldAuthorizeDelete.push(ProtocolAction.CoPrune);
185
177
 
186
- // if there is no initial write to update from, then no action rule can authorize the incoming message
187
- if (initialWrite === undefined) {
188
- return [];
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
- if (incomingMessage.author === initialWrite.author) {
192
- // 'update' or 'co-update' action authorizes the incoming message
193
- return [ProtocolAction.CoUpdate, ProtocolAction.Update];
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
- // purely defensive programming: should not be reachable
203
- // setting to empty array will prevent any message from being authorized
204
- return [];
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
- // 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.
239
- const ruleHasAMatchingAllowedAction = actionRule.can.some(
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 (!ruleHasAMatchingAllowedAction) {
243
- continue;
266
+ if (actionRuleAuthorizes) {
267
+ return;
244
268
  }
269
+ }
245
270
 
246
- // Code reaches here means this action rule has an allowed action that matches the action of the message.
247
- // The remaining code checks the actor/author of the incoming message.
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
- // If the action rule allows `anyone`, then no further checks are needed.
250
- if (actionRule.who === ProtocolActor.Anyone) {
251
- return;
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
- // Since not `anyone` is allowed in this action rule, we will need to check the author of the incoming message,
255
- // if the author of incoming message is not defined, this action rule cannot authorize the incoming message.
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
- // go through role validation path if a role is invoked by the incoming message
261
- if (invokedRole !== undefined) {
262
- // When a protocol role is being invoked, we require that there is a matching `role` rule.
263
- if (actionRule.role === invokedRole) {
264
- // role is successfully invoked
265
- return;
266
- } else {
267
- continue;
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
- // else we go through the actor (`who`) validation
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
- if (recordsWriteMessage.descriptor.recipient === author) {
290
- return;
291
- } else {
292
- continue;
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
- // validate the actor is allowed by the current action rule
297
- const ancestorRuleSuccess: boolean = await checkActor(author, actionRule, recordChain, protocolDefinition);
298
- if (ancestorRuleSuccess) {
299
- return;
344
+ if (recordsWriteMessage.descriptor.recipient === author) {
345
+ return true;
346
+ } else {
347
+ return false;
300
348
  }
301
349
  }
302
350
 
303
- // No action rules were satisfied, message is not authorized
304
- throw new DwnError(
305
- DwnErrorCode.ProtocolAuthorizationActionNotAllowed,
306
- `Inbound message action Records${incomingMessageMethod} by author ${incomingMessage.author} not allowed.`
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
- // if this is an anonymous subscribe and the filter supports published records, subscribe to only published records
59
- if (Records.filterIncludesPublishedRecords(recordsSubscribe.message.descriptor.filter) && recordsSubscribe.author === undefined) {
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
- try {
100
- const subscription = await this.deps.eventLog.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
101
- cursor : eventLogCursor,
102
- filters : eventFilters,
103
- });
104
- await projectedSubscriptionHandler.setSubscription(subscription);
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: 200, detail: 'OK' },
108
- subscription,
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
- // ---- No cursor: existing behavior (initial snapshot from MessageStore) ----
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.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
176
+ const subscription = await this.deps.eventLog!.subscribe(tenant, messageCid, projectedSubscriptionHandler.listener, {
126
177
  filters: eventFilters,
127
178
  });
128
179
  await projectedSubscriptionHandler.setSubscription(subscription);