@enbox/dwn-sdk-js 0.3.7 → 0.3.8

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 (230) hide show
  1. package/dist/browser.mjs +8 -8
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +2591 -1435
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/core/constants.js +20 -0
  6. package/dist/esm/src/core/constants.js.map +1 -1
  7. package/dist/esm/src/core/dwn-error.js +24 -1
  8. package/dist/esm/src/core/dwn-error.js.map +1 -1
  9. package/dist/esm/src/core/grant-authorization.js +4 -4
  10. package/dist/esm/src/core/grant-authorization.js.map +1 -1
  11. package/dist/esm/src/core/message.js +89 -4
  12. package/dist/esm/src/core/message.js.map +1 -1
  13. package/dist/esm/src/core/messages-grant-authorization.js +147 -55
  14. package/dist/esm/src/core/messages-grant-authorization.js.map +1 -1
  15. package/dist/esm/src/core/protocol-authorization.js +76 -0
  16. package/dist/esm/src/core/protocol-authorization.js.map +1 -1
  17. package/dist/esm/src/core/records-grant-authorization.js +40 -15
  18. package/dist/esm/src/core/records-grant-authorization.js.map +1 -1
  19. package/dist/esm/src/handlers/messages-read.js +5 -5
  20. package/dist/esm/src/handlers/messages-read.js.map +1 -1
  21. package/dist/esm/src/handlers/messages-subscribe.js +109 -7
  22. package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
  23. package/dist/esm/src/handlers/messages-sync.js +341 -96
  24. package/dist/esm/src/handlers/messages-sync.js.map +1 -1
  25. package/dist/esm/src/handlers/protocols-configure.js +81 -2
  26. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  27. package/dist/esm/src/handlers/records-count.js +30 -0
  28. package/dist/esm/src/handlers/records-count.js.map +1 -1
  29. package/dist/esm/src/handlers/records-delete.js +3 -2
  30. package/dist/esm/src/handlers/records-delete.js.map +1 -1
  31. package/dist/esm/src/handlers/records-query.js +30 -0
  32. package/dist/esm/src/handlers/records-query.js.map +1 -1
  33. package/dist/esm/src/handlers/records-read.js +3 -2
  34. package/dist/esm/src/handlers/records-read.js.map +1 -1
  35. package/dist/esm/src/handlers/records-subscribe.js +31 -0
  36. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  37. package/dist/esm/src/handlers/records-write.js +3 -2
  38. package/dist/esm/src/handlers/records-write.js.map +1 -1
  39. package/dist/esm/src/index.js +2 -0
  40. package/dist/esm/src/index.js.map +1 -1
  41. package/dist/esm/src/interfaces/messages-read.js +6 -3
  42. package/dist/esm/src/interfaces/messages-read.js.map +1 -1
  43. package/dist/esm/src/interfaces/messages-subscribe.js +6 -3
  44. package/dist/esm/src/interfaces/messages-subscribe.js.map +1 -1
  45. package/dist/esm/src/interfaces/messages-sync.js +17 -3
  46. package/dist/esm/src/interfaces/messages-sync.js.map +1 -1
  47. package/dist/esm/src/interfaces/protocols-configure.js +5 -2
  48. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  49. package/dist/esm/src/interfaces/protocols-query.js +8 -4
  50. package/dist/esm/src/interfaces/protocols-query.js.map +1 -1
  51. package/dist/esm/src/interfaces/records-count.js +5 -0
  52. package/dist/esm/src/interfaces/records-count.js.map +1 -1
  53. package/dist/esm/src/interfaces/records-delete.js +6 -2
  54. package/dist/esm/src/interfaces/records-delete.js.map +1 -1
  55. package/dist/esm/src/interfaces/records-query.js +5 -0
  56. package/dist/esm/src/interfaces/records-query.js.map +1 -1
  57. package/dist/esm/src/interfaces/records-read.js +6 -3
  58. package/dist/esm/src/interfaces/records-read.js.map +1 -1
  59. package/dist/esm/src/interfaces/records-subscribe.js +5 -0
  60. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  61. package/dist/esm/src/interfaces/records-write.js +6 -3
  62. package/dist/esm/src/interfaces/records-write.js.map +1 -1
  63. package/dist/esm/src/protocols/permissions.js +28 -7
  64. package/dist/esm/src/protocols/permissions.js.map +1 -1
  65. package/dist/esm/src/sync/records-projection.js +228 -0
  66. package/dist/esm/src/sync/records-projection.js.map +1 -0
  67. package/dist/esm/src/types/message-types.js.map +1 -1
  68. package/dist/esm/src/types/permission-types.js.map +1 -1
  69. package/dist/esm/src/utils/permission-scope.js +37 -0
  70. package/dist/esm/src/utils/permission-scope.js.map +1 -0
  71. package/dist/esm/tests/core/grant-authorization.spec.js +26 -3
  72. package/dist/esm/tests/core/grant-authorization.spec.js.map +1 -1
  73. package/dist/esm/tests/core/records-grant-authorization.spec.js +117 -0
  74. package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -0
  75. package/dist/esm/tests/features/permissions.spec.js +126 -0
  76. package/dist/esm/tests/features/permissions.spec.js.map +1 -1
  77. package/dist/esm/tests/handlers/messages-read.spec.js +345 -12
  78. package/dist/esm/tests/handlers/messages-read.spec.js.map +1 -1
  79. package/dist/esm/tests/handlers/messages-subscribe.spec.js +326 -9
  80. package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
  81. package/dist/esm/tests/handlers/messages-sync.spec.js +1053 -7
  82. package/dist/esm/tests/handlers/messages-sync.spec.js.map +1 -1
  83. package/dist/esm/tests/handlers/protocols-configure.spec.js +361 -0
  84. package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
  85. package/dist/esm/tests/handlers/records-count.spec.js +75 -2
  86. package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
  87. package/dist/esm/tests/handlers/records-query.spec.js +73 -0
  88. package/dist/esm/tests/handlers/records-query.spec.js.map +1 -1
  89. package/dist/esm/tests/handlers/records-subscribe.spec.js +75 -1
  90. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  91. package/dist/esm/tests/interfaces/messages-get.spec.js +107 -5
  92. package/dist/esm/tests/interfaces/messages-get.spec.js.map +1 -1
  93. package/dist/esm/tests/interfaces/protocols-configure.spec.js +13 -0
  94. package/dist/esm/tests/interfaces/protocols-configure.spec.js.map +1 -1
  95. package/dist/esm/tests/interfaces/records-delete.spec.js +12 -0
  96. package/dist/esm/tests/interfaces/records-delete.spec.js.map +1 -1
  97. package/dist/esm/tests/interfaces/records-query.spec.js +10 -0
  98. package/dist/esm/tests/interfaces/records-query.spec.js.map +1 -1
  99. package/dist/esm/tests/interfaces/records-subscribe.spec.js +10 -0
  100. package/dist/esm/tests/interfaces/records-subscribe.spec.js.map +1 -1
  101. package/dist/esm/tests/interfaces/records-write.spec.js +33 -0
  102. package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
  103. package/dist/esm/tests/sync/records-projection.spec.js +245 -0
  104. package/dist/esm/tests/sync/records-projection.spec.js.map +1 -0
  105. package/dist/esm/tests/test-suite.js +2 -0
  106. package/dist/esm/tests/test-suite.js.map +1 -1
  107. package/dist/esm/tests/utils/permission-scope.spec.js +66 -0
  108. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -0
  109. package/dist/esm/tests/utils/test-data-generator.js +5 -2
  110. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  111. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  112. package/dist/types/src/core/constants.d.ts +13 -0
  113. package/dist/types/src/core/constants.d.ts.map +1 -1
  114. package/dist/types/src/core/dwn-error.d.ts +24 -1
  115. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  116. package/dist/types/src/core/grant-authorization.d.ts +1 -2
  117. package/dist/types/src/core/grant-authorization.d.ts.map +1 -1
  118. package/dist/types/src/core/message.d.ts +41 -1
  119. package/dist/types/src/core/message.d.ts.map +1 -1
  120. package/dist/types/src/core/messages-grant-authorization.d.ts +36 -4
  121. package/dist/types/src/core/messages-grant-authorization.d.ts.map +1 -1
  122. package/dist/types/src/core/protocol-authorization.d.ts +12 -0
  123. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  124. package/dist/types/src/core/records-grant-authorization.d.ts +6 -0
  125. package/dist/types/src/core/records-grant-authorization.d.ts.map +1 -1
  126. package/dist/types/src/handlers/messages-read.d.ts.map +1 -1
  127. package/dist/types/src/handlers/messages-subscribe.d.ts +2 -1
  128. package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
  129. package/dist/types/src/handlers/messages-sync.d.ts +31 -0
  130. package/dist/types/src/handlers/messages-sync.d.ts.map +1 -1
  131. package/dist/types/src/handlers/protocols-configure.d.ts +3 -0
  132. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  133. package/dist/types/src/handlers/records-count.d.ts +4 -0
  134. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  135. package/dist/types/src/handlers/records-delete.d.ts.map +1 -1
  136. package/dist/types/src/handlers/records-query.d.ts +4 -0
  137. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  138. package/dist/types/src/handlers/records-read.d.ts.map +1 -1
  139. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  140. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  141. package/dist/types/src/index.d.ts +6 -2
  142. package/dist/types/src/index.d.ts.map +1 -1
  143. package/dist/types/src/interfaces/messages-read.d.ts +1 -1
  144. package/dist/types/src/interfaces/messages-read.d.ts.map +1 -1
  145. package/dist/types/src/interfaces/messages-subscribe.d.ts +1 -1
  146. package/dist/types/src/interfaces/messages-subscribe.d.ts.map +1 -1
  147. package/dist/types/src/interfaces/messages-sync.d.ts +4 -1
  148. package/dist/types/src/interfaces/messages-sync.d.ts.map +1 -1
  149. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  150. package/dist/types/src/interfaces/protocols-query.d.ts.map +1 -1
  151. package/dist/types/src/interfaces/records-count.d.ts +1 -0
  152. package/dist/types/src/interfaces/records-count.d.ts.map +1 -1
  153. package/dist/types/src/interfaces/records-delete.d.ts.map +1 -1
  154. package/dist/types/src/interfaces/records-query.d.ts +1 -0
  155. package/dist/types/src/interfaces/records-query.d.ts.map +1 -1
  156. package/dist/types/src/interfaces/records-read.d.ts.map +1 -1
  157. package/dist/types/src/interfaces/records-subscribe.d.ts +1 -0
  158. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  159. package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
  160. package/dist/types/src/protocols/permissions.d.ts +2 -0
  161. package/dist/types/src/protocols/permissions.d.ts.map +1 -1
  162. package/dist/types/src/sync/records-projection.d.ts +98 -0
  163. package/dist/types/src/sync/records-projection.d.ts.map +1 -0
  164. package/dist/types/src/types/message-types.d.ts +1 -0
  165. package/dist/types/src/types/message-types.d.ts.map +1 -1
  166. package/dist/types/src/types/messages-types.d.ts +21 -3
  167. package/dist/types/src/types/messages-types.d.ts.map +1 -1
  168. package/dist/types/src/types/permission-types.d.ts +4 -0
  169. package/dist/types/src/types/permission-types.d.ts.map +1 -1
  170. package/dist/types/src/types/records-types.d.ts +4 -0
  171. package/dist/types/src/types/records-types.d.ts.map +1 -1
  172. package/dist/types/src/types/subscriptions.d.ts +18 -3
  173. package/dist/types/src/types/subscriptions.d.ts.map +1 -1
  174. package/dist/types/src/utils/permission-scope.d.ts +29 -0
  175. package/dist/types/src/utils/permission-scope.d.ts.map +1 -0
  176. package/dist/types/tests/core/records-grant-authorization.spec.d.ts +2 -0
  177. package/dist/types/tests/core/records-grant-authorization.spec.d.ts.map +1 -0
  178. package/dist/types/tests/features/permissions.spec.d.ts.map +1 -1
  179. package/dist/types/tests/handlers/messages-read.spec.d.ts.map +1 -1
  180. package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
  181. package/dist/types/tests/handlers/messages-sync.spec.d.ts.map +1 -1
  182. package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
  183. package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
  184. package/dist/types/tests/handlers/records-query.spec.d.ts.map +1 -1
  185. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  186. package/dist/types/tests/sync/records-projection.spec.d.ts +2 -0
  187. package/dist/types/tests/sync/records-projection.spec.d.ts.map +1 -0
  188. package/dist/types/tests/test-suite.d.ts.map +1 -1
  189. package/dist/types/tests/utils/permission-scope.spec.d.ts +2 -0
  190. package/dist/types/tests/utils/permission-scope.spec.d.ts.map +1 -0
  191. package/dist/types/tests/utils/test-data-generator.d.ts +5 -2
  192. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  193. package/package.json +1 -1
  194. package/src/core/constants.ts +24 -0
  195. package/src/core/dwn-error.ts +24 -1
  196. package/src/core/grant-authorization.ts +7 -5
  197. package/src/core/message.ts +153 -6
  198. package/src/core/messages-grant-authorization.ts +282 -70
  199. package/src/core/protocol-authorization.ts +130 -0
  200. package/src/core/records-grant-authorization.ts +64 -21
  201. package/src/handlers/messages-read.ts +7 -5
  202. package/src/handlers/messages-subscribe.ts +149 -9
  203. package/src/handlers/messages-sync.ts +593 -102
  204. package/src/handlers/protocols-configure.ts +103 -2
  205. package/src/handlers/records-count.ts +33 -0
  206. package/src/handlers/records-delete.ts +3 -2
  207. package/src/handlers/records-query.ts +33 -0
  208. package/src/handlers/records-read.ts +3 -2
  209. package/src/handlers/records-subscribe.ts +34 -0
  210. package/src/handlers/records-write.ts +3 -2
  211. package/src/index.ts +7 -3
  212. package/src/interfaces/messages-read.ts +8 -5
  213. package/src/interfaces/messages-subscribe.ts +12 -9
  214. package/src/interfaces/messages-sync.ts +33 -12
  215. package/src/interfaces/protocols-configure.ts +8 -4
  216. package/src/interfaces/protocols-query.ts +13 -9
  217. package/src/interfaces/records-count.ts +7 -0
  218. package/src/interfaces/records-delete.ts +9 -5
  219. package/src/interfaces/records-query.ts +7 -0
  220. package/src/interfaces/records-read.ts +6 -3
  221. package/src/interfaces/records-subscribe.ts +7 -0
  222. package/src/interfaces/records-write.ts +25 -17
  223. package/src/protocols/permissions.ts +47 -9
  224. package/src/sync/records-projection.ts +328 -0
  225. package/src/types/message-types.ts +1 -0
  226. package/src/types/messages-types.ts +23 -3
  227. package/src/types/permission-types.ts +5 -1
  228. package/src/types/records-types.ts +5 -1
  229. package/src/types/subscriptions.ts +19 -3
  230. package/src/utils/permission-scope.ts +55 -0
@@ -12,6 +12,20 @@ import { removeUndefinedProperties } from '@enbox/common';
12
12
  import { validateJsonSchema } from '../schema-validator.js';
13
13
  import { DwnError, DwnErrorCode } from './dwn-error.js';
14
14
 
15
+ type PermissionGrantInvocation = {
16
+ permissionGrantId?: string;
17
+ permissionGrantIds?: string[];
18
+ };
19
+
20
+ type ValidateSignatureStructureOptions = {
21
+ /**
22
+ * Permission grant invocations are bound to the author signature payload.
23
+ * Owner signatures only prove owner retention/consent and do not repeat
24
+ * the author's grant invocation fields.
25
+ */
26
+ validatePermissionGrantInvocation?: boolean;
27
+ };
28
+
15
29
  /**
16
30
  * A class containing utility methods for working with DWN messages.
17
31
  */
@@ -100,16 +114,17 @@ export class Message {
100
114
  signer: MessageSigner,
101
115
  delegatedGrant?: DataEncodedRecordsWriteMessage,
102
116
  permissionGrantId?: string,
117
+ permissionGrantIds?: string[],
103
118
  protocolRole?: string
104
119
  }): Promise<AuthorizationModel> {
105
- const { descriptor, signer, delegatedGrant, permissionGrantId, protocolRole } = input;
120
+ const { descriptor, signer, delegatedGrant, permissionGrantId, permissionGrantIds, protocolRole } = input;
106
121
 
107
122
  let delegatedGrantId;
108
123
  if (delegatedGrant !== undefined) {
109
124
  delegatedGrantId = await Message.getCid(delegatedGrant);
110
125
  }
111
126
 
112
- const signature = await Message.createSignature(descriptor, signer, { delegatedGrantId, permissionGrantId, protocolRole });
127
+ const signature = await Message.createSignature(descriptor, signer, { delegatedGrantId, permissionGrantId, permissionGrantIds, protocolRole });
113
128
 
114
129
  const authorization: AuthorizationModel = {
115
130
  signature
@@ -129,11 +144,23 @@ export class Message {
129
144
  public static async createSignature(
130
145
  descriptor: Descriptor,
131
146
  signer: MessageSigner,
132
- additionalPayloadProperties?: { delegatedGrantId?: string, permissionGrantId?: string, protocolRole?: string }
147
+ additionalPayloadProperties?: { delegatedGrantId?: string, permissionGrantId?: string, permissionGrantIds?: string[], protocolRole?: string }
133
148
  ): Promise<GeneralJws> {
134
149
  const descriptorCid = await Cid.computeCid(descriptor);
135
-
136
- const signaturePayload: GenericSignaturePayload = { descriptorCid, ...additionalPayloadProperties };
150
+ const {
151
+ delegatedGrantId,
152
+ permissionGrantId,
153
+ permissionGrantIds,
154
+ protocolRole
155
+ } = additionalPayloadProperties ?? {};
156
+ const permissionGrantInvocation = Message.normalizePermissionGrantInvocation({ permissionGrantId, permissionGrantIds });
157
+
158
+ const signaturePayload: GenericSignaturePayload = {
159
+ descriptorCid,
160
+ delegatedGrantId,
161
+ protocolRole,
162
+ ...permissionGrantInvocation
163
+ };
137
164
  removeUndefinedProperties(signaturePayload);
138
165
 
139
166
  const signaturePayloadBytes = Encoder.objectToBytes(signaturePayload);
@@ -144,6 +171,55 @@ export class Message {
144
171
  return signature;
145
172
  }
146
173
 
174
+ /**
175
+ * Normalizes permission grant invocation before it is written into a descriptor
176
+ * and signed payload.
177
+ *
178
+ * Direct operation messages use `permissionGrantId`; Messages operations use
179
+ * canonicalized `permissionGrantIds` so a request can invoke a grant set.
180
+ */
181
+ public static normalizePermissionGrantInvocation(input: PermissionGrantInvocation): PermissionGrantInvocation {
182
+ if (input.permissionGrantId !== undefined && input.permissionGrantIds !== undefined) {
183
+ throw new DwnError(
184
+ DwnErrorCode.MessagePermissionGrantCreateInvocationAmbiguous,
185
+ 'permission grant invocation must use either `permissionGrantId` or `permissionGrantIds`, not both'
186
+ );
187
+ }
188
+
189
+ if (input.permissionGrantId !== undefined) {
190
+ return { permissionGrantId: input.permissionGrantId };
191
+ }
192
+
193
+ if (input.permissionGrantIds === undefined) {
194
+ return {};
195
+ }
196
+
197
+ const permissionGrantIds = Message.normalizePermissionGrantIds(input);
198
+ return { permissionGrantIds };
199
+ }
200
+
201
+ /**
202
+ * Returns the permission grant ID carried by a direct operation signature payload.
203
+ */
204
+ public static getPermissionGrantId(signaturePayload: GenericSignaturePayload): string | undefined {
205
+ return signaturePayload.permissionGrantId;
206
+ }
207
+
208
+ /**
209
+ * Returns all permission grant IDs invoked by a signature payload.
210
+ *
211
+ * Direct Records/Protocols operations carry a single `permissionGrantId`.
212
+ * Messages operations carry `permissionGrantIds`. Generic cleanup and
213
+ * revocation code uses this helper across both wire shapes.
214
+ */
215
+ public static getPermissionGrantIds(signaturePayload: GenericSignaturePayload): string[] {
216
+ if (signaturePayload.permissionGrantId !== undefined) {
217
+ return [signaturePayload.permissionGrantId];
218
+ }
219
+
220
+ return signaturePayload.permissionGrantIds ?? [];
221
+ }
222
+
147
223
  /**
148
224
  * @returns newest message in the array. `undefined` if given array is empty.
149
225
  */
@@ -227,12 +303,14 @@ export class Message {
227
303
  * 3. The `descriptorCid` property matches the CID of the message descriptor
228
304
  * NOTE: signature is NOT verified.
229
305
  * @param payloadJsonSchemaKey The key to look up the JSON schema referenced in `compile-validators.js` and perform payload schema validation on.
306
+ * @param options Additional structural validation controls.
230
307
  * @returns the parsed JSON payload object if validation succeeds.
231
308
  */
232
309
  public static async validateSignatureStructure(
233
310
  messageSignature: GeneralJws,
234
311
  messageDescriptor: Descriptor,
235
312
  payloadJsonSchemaKey: string = 'GenericSignaturePayload',
313
+ options: ValidateSignatureStructureOptions = {},
236
314
  ): Promise<GenericSignaturePayload> {
237
315
 
238
316
  if (messageSignature.signatures.length !== 1) {
@@ -254,6 +332,75 @@ export class Message {
254
332
  );
255
333
  }
256
334
 
335
+ if (options.validatePermissionGrantInvocation !== false) {
336
+ Message.validatePermissionGrantInvocationMatchesPayload(messageDescriptor, payloadJson);
337
+ }
338
+
257
339
  return payloadJson;
258
340
  }
259
- }
341
+
342
+ private static normalizePermissionGrantIds(input: PermissionGrantInvocation): string[] {
343
+ if (input.permissionGrantIds === undefined) {
344
+ return [];
345
+ }
346
+
347
+ if (input.permissionGrantIds.length === 0) {
348
+ throw new DwnError(
349
+ DwnErrorCode.MessagePermissionGrantIdsEmpty,
350
+ '`permissionGrantIds` must contain at least one grant ID'
351
+ );
352
+ }
353
+
354
+ return [...new Set(input.permissionGrantIds)].sort(lexicographicalCompare);
355
+ }
356
+
357
+ private static validatePermissionGrantInvocationCanonical(input: PermissionGrantInvocation): string[] {
358
+ if (input.permissionGrantId !== undefined && input.permissionGrantIds !== undefined) {
359
+ throw new DwnError(
360
+ DwnErrorCode.MessagePermissionGrantValidateInvocationAmbiguous,
361
+ 'permission grant invocation must use either `permissionGrantId` or `permissionGrantIds`, not both'
362
+ );
363
+ }
364
+
365
+ if (input.permissionGrantId !== undefined) {
366
+ return [input.permissionGrantId];
367
+ }
368
+
369
+ const normalizedPermissionGrantIds = Message.normalizePermissionGrantIds(input);
370
+
371
+ if (input.permissionGrantIds !== undefined && !Message.areStringArraysEqual(input.permissionGrantIds, normalizedPermissionGrantIds)) {
372
+ throw new DwnError(
373
+ DwnErrorCode.MessagePermissionGrantIdsNotCanonical,
374
+ '`permissionGrantIds` must be sorted and deduplicated'
375
+ );
376
+ }
377
+
378
+ return normalizedPermissionGrantIds;
379
+ }
380
+
381
+ private static validatePermissionGrantInvocationMatchesPayload(
382
+ descriptor: Descriptor & PermissionGrantInvocation,
383
+ signaturePayload: GenericSignaturePayload
384
+ ): void {
385
+ if (descriptor.permissionGrantId !== signaturePayload.permissionGrantId) {
386
+ throw new DwnError(
387
+ DwnErrorCode.MessagePermissionGrantDescriptorPayloadMismatch,
388
+ 'permission grant invocation in descriptor does not match signature payload'
389
+ );
390
+ }
391
+
392
+ const descriptorPermissionGrantIds = Message.validatePermissionGrantInvocationCanonical(descriptor);
393
+ const payloadPermissionGrantIds = Message.validatePermissionGrantInvocationCanonical(signaturePayload);
394
+
395
+ if (!Message.areStringArraysEqual(descriptorPermissionGrantIds, payloadPermissionGrantIds)) {
396
+ throw new DwnError(
397
+ DwnErrorCode.MessagePermissionGrantIdsDescriptorPayloadMismatch,
398
+ 'permission grant invocation in descriptor does not match signature payload'
399
+ );
400
+ }
401
+ }
402
+
403
+ private static areStringArraysEqual(a: string[], b: string[]): boolean {
404
+ return a.length === b.length && a.every((value, index) => value === b[index]);
405
+ }
406
+ }
@@ -3,11 +3,14 @@ import type { MessagesPermissionScope } from '../types/permission-types.js';
3
3
  import type { MessageStore } from '../types/message-store.js';
4
4
  import type { PermissionGrant } from '../protocols/permission-grant.js';
5
5
  import type { ProtocolsConfigureMessage } from '../types/protocols-types.js';
6
+ import type { ProtocolScope } from '../utils/permission-scope.js';
6
7
  import type { DataEncodedRecordsWriteMessage, RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';
7
8
  import type { MessagesReadMessage, MessagesSubscribeMessage, MessagesSyncMessage } from '../types/messages-types.js';
8
9
 
9
10
  import { DwnInterfaceName } from '../enums/dwn-interface-method.js';
10
11
  import { GrantAuthorization } from './grant-authorization.js';
12
+ import { isRecordsPrimaryProjectionExcludedProtocol } from './constants.js';
13
+ import { PermissionScopeMatcher } from '../utils/permission-scope.js';
11
14
  import { PermissionsProtocol } from '../protocols/permissions.js';
12
15
  import { Records } from '../utils/records.js';
13
16
  import { RecordsWrite } from '../interfaces/records-write.js';
@@ -15,6 +18,16 @@ import { DwnError, DwnErrorCode } from './dwn-error.js';
15
18
 
16
19
  export class MessagesGrantAuthorization {
17
20
 
21
+ public static async fetchPermissionGrants(
22
+ tenant: string,
23
+ messageStore: MessageStore,
24
+ permissionGrantIds: string[]
25
+ ): Promise<PermissionGrant[]> {
26
+ return Promise.all(
27
+ permissionGrantIds.map(permissionGrantId => PermissionsProtocol.fetchGrant(tenant, messageStore, permissionGrantId))
28
+ );
29
+ }
30
+
18
31
  /**
19
32
  * Authorizes a MessagesReadMessage using the given permission grant.
20
33
  * @param messageStore Used to check if the given grant has been revoked; and to fetch related RecordsWrites if needed.
@@ -24,23 +37,29 @@ export class MessagesGrantAuthorization {
24
37
  messageToRead: GenericMessage,
25
38
  expectedGrantor: string,
26
39
  expectedGrantee: string,
27
- permissionGrant: PermissionGrant,
40
+ permissionGrants: PermissionGrant[],
28
41
  messageStore: MessageStore,
29
42
  }): Promise<void> {
30
43
  const {
31
- messagesReadMessage, messageToRead, expectedGrantor, expectedGrantee, permissionGrant, messageStore
44
+ messagesReadMessage, messageToRead, expectedGrantor, expectedGrantee, permissionGrants, messageStore
32
45
  } = input;
33
46
 
34
- await GrantAuthorization.performBaseValidation({
47
+ await MessagesGrantAuthorization.performBaseValidationForGrantSet({
35
48
  incomingMessage: messagesReadMessage,
36
49
  expectedGrantor,
37
50
  expectedGrantee,
38
- permissionGrant,
51
+ permissionGrants,
39
52
  messageStore
40
53
  });
41
54
 
42
- const scope = permissionGrant.scope as MessagesPermissionScope;
43
- await MessagesGrantAuthorization.verifyScope(expectedGrantor, messageToRead, scope, messageStore);
55
+ for (const permissionGrant of permissionGrants) {
56
+ const scope = permissionGrant.scope as MessagesPermissionScope;
57
+ if (await MessagesGrantAuthorization.isScopeAuthorized(expectedGrantor, messageToRead, scope, messageStore)) {
58
+ return;
59
+ }
60
+ }
61
+
62
+ throw new DwnError(DwnErrorCode.MessagesReadVerifyScopeFailed, 'record message failed scope authorization');
44
63
  }
45
64
 
46
65
  /**
@@ -51,98 +70,291 @@ export class MessagesGrantAuthorization {
51
70
  incomingMessage: MessagesSubscribeMessage | MessagesSyncMessage,
52
71
  expectedGrantor: string,
53
72
  expectedGrantee: string,
54
- permissionGrant: PermissionGrant,
73
+ permissionGrants: PermissionGrant[],
55
74
  messageStore: MessageStore,
56
75
  }): Promise<void> {
57
76
  const {
58
- incomingMessage, expectedGrantor, expectedGrantee, permissionGrant, messageStore
77
+ incomingMessage, expectedGrantor, expectedGrantee, permissionGrants, messageStore
59
78
  } = input;
60
79
 
61
- await GrantAuthorization.performBaseValidation({
80
+ await MessagesGrantAuthorization.performBaseValidationForGrantSet({
62
81
  incomingMessage,
63
82
  expectedGrantor,
64
83
  expectedGrantee,
65
- permissionGrant,
84
+ permissionGrants,
66
85
  messageStore
67
86
  });
68
87
 
69
- // if the grant is scoped to a specific protocol, ensure that the message targets that protocol
70
- if (PermissionsProtocol.hasProtocolScope(permissionGrant.scope)) {
71
- const scopedProtocol = permissionGrant.scope.protocol;
72
-
73
- // MessagesSync uses a direct `protocol` field on the descriptor
74
- if ('action' in incomingMessage.descriptor) {
75
- const syncMessage = incomingMessage as MessagesSyncMessage;
76
- if (syncMessage.descriptor.protocol !== scopedProtocol) {
77
- throw new DwnError(
78
- DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol,
79
- `The protocol ${syncMessage.descriptor.protocol} does not match the scoped protocol ${scopedProtocol}`
80
- );
81
- }
82
- } else {
83
- // MessagesSubscribe uses filters
84
- const filteredMessage = incomingMessage as MessagesSubscribeMessage;
85
- for (const filter of filteredMessage.descriptor.filters) {
86
- if (filter.protocol !== scopedProtocol) {
87
- throw new DwnError(
88
- DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol,
89
- `The protocol ${filter.protocol} does not match the scoped protocol ${scopedProtocol}`
90
- );
91
- }
92
- }
88
+ const scopes = permissionGrants.map(permissionGrant => permissionGrant.scope as MessagesPermissionScope);
89
+
90
+ if ('action' in incomingMessage.descriptor) {
91
+ MessagesGrantAuthorization.authorizeSyncScope(incomingMessage as MessagesSyncMessage, scopes);
92
+ return;
93
+ }
94
+
95
+ MessagesGrantAuthorization.authorizeSubscribeScope(incomingMessage as MessagesSubscribeMessage, scopes);
96
+ }
97
+
98
+ private static authorizeSyncScope(
99
+ syncMessage: MessagesSyncMessage,
100
+ scopes: MessagesPermissionScope[]
101
+ ): void {
102
+ const { projectionScopes, protocol } = syncMessage.descriptor;
103
+
104
+ if (projectionScopes === undefined) {
105
+ MessagesGrantAuthorization.authorizeProtocolSyncScope(scopes, protocol);
106
+ return;
107
+ }
108
+
109
+ MessagesGrantAuthorization.authorizeProjectionScopes(scopes, projectionScopes);
110
+ }
111
+
112
+ private static authorizeProtocolSyncScope(
113
+ scopes: MessagesPermissionScope[],
114
+ protocol: string | undefined
115
+ ): void {
116
+ if (isRecordsPrimaryProjectionExcludedProtocol(protocol)) {
117
+ throw new DwnError(
118
+ DwnErrorCode.MessagesGrantAuthorizationProtocolSyncInfrastructureProtocol,
119
+ `Protocol-scoped MessagesSync cannot authorize infrastructure protocol ${protocol}`
120
+ );
121
+ }
122
+
123
+ if (!MessagesGrantAuthorization.someScopeMatches(scopes, { protocol })) {
124
+ throw new DwnError(
125
+ DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol,
126
+ `No permission grant scope matches protocol ${protocol}`
127
+ );
128
+ }
129
+ }
130
+
131
+ private static authorizeProjectionScopes(
132
+ scopes: MessagesPermissionScope[],
133
+ projectionScopes: ProtocolScope[],
134
+ ): void {
135
+ for (const projectionScope of projectionScopes) {
136
+ if (isRecordsPrimaryProjectionExcludedProtocol(projectionScope.protocol)) {
137
+ throw new DwnError(
138
+ DwnErrorCode.MessagesGrantAuthorizationProjectionInfrastructureProtocol,
139
+ `Projected MessagesSync cannot authorize infrastructure protocol ${projectionScope.protocol}`
140
+ );
141
+ }
142
+
143
+ if (MessagesGrantAuthorization.someScopeMatches(scopes, projectionScope)) {
144
+ continue;
145
+ }
146
+
147
+ throw new DwnError(
148
+ DwnErrorCode.MessagesGrantAuthorizationProjectionScopeMismatch,
149
+ `No permission grant scope matches projection scope ${JSON.stringify(projectionScope)}`
150
+ );
151
+ }
152
+ }
153
+
154
+ private static authorizeSubscribeScope(
155
+ subscribeMessage: MessagesSubscribeMessage,
156
+ scopes: MessagesPermissionScope[]
157
+ ): void {
158
+ const { filters } = subscribeMessage.descriptor;
159
+
160
+ if (filters.length === 0 && !MessagesGrantAuthorization.hasUnscopedGrant(scopes)) {
161
+ throw new DwnError(
162
+ DwnErrorCode.MessagesGrantAuthorizationUnfilteredSubscribeProtocolScope,
163
+ `A protocol-scoped grant cannot authorize an unfiltered subscription`
164
+ );
165
+ }
166
+
167
+ for (const filter of filters) {
168
+ if (MessagesGrantAuthorization.someScopeMatches(scopes, { protocol: filter.protocol })) {
169
+ continue;
93
170
  }
171
+
172
+ throw new DwnError(
173
+ DwnErrorCode.MessagesGrantAuthorizationSubscribeProtocolMismatch,
174
+ `No permission grant scope matches protocol ${filter.protocol}`
175
+ );
94
176
  }
95
177
  }
96
178
 
179
+ private static someScopeMatches(scopes: MessagesPermissionScope[], target: ProtocolScope): boolean {
180
+ return scopes.some(scope => PermissionScopeMatcher.matches(scope, target));
181
+ }
182
+
183
+ private static hasUnscopedGrant(scopes: MessagesPermissionScope[]): boolean {
184
+ return scopes.some(scope => scope.protocol === undefined);
185
+ }
186
+
97
187
  /**
98
- * Verifies the given record against the scope of the given grant.
188
+ * Revalidates an already-open delegated MessagesSubscribe grant set at event
189
+ * delivery time. Subscription messages are signed at open time, but grant
190
+ * expiry and revocation must stop future delivery after the grant set becomes
191
+ * invalid.
99
192
  */
100
- private static async verifyScope(
193
+ public static async authorizeSubscribeDelivery(input: {
194
+ messagesSubscribeMessage: MessagesSubscribeMessage,
195
+ expectedGrantor: string,
196
+ expectedGrantee: string,
197
+ permissionGrants: PermissionGrant[],
198
+ messageStore: MessageStore,
199
+ deliveryTimestamp: string,
200
+ }): Promise<void> {
201
+ const {
202
+ messagesSubscribeMessage,
203
+ expectedGrantor,
204
+ expectedGrantee,
205
+ permissionGrants,
206
+ messageStore,
207
+ deliveryTimestamp,
208
+ } = input;
209
+
210
+ const deliveryMessage: MessagesSubscribeMessage = {
211
+ ...messagesSubscribeMessage,
212
+ descriptor: {
213
+ ...messagesSubscribeMessage.descriptor,
214
+ messageTimestamp: deliveryTimestamp,
215
+ },
216
+ };
217
+
218
+ await MessagesGrantAuthorization.authorizeSubscribeOrSync({
219
+ incomingMessage: deliveryMessage,
220
+ expectedGrantor,
221
+ expectedGrantee,
222
+ permissionGrants,
223
+ messageStore,
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Performs base validation on every invoked grant. The grant set is all-or-nothing:
229
+ * unresolved, revoked, expired, or interface/method-mismatched grants fail the request.
230
+ */
231
+ private static async performBaseValidationForGrantSet(input: {
232
+ incomingMessage: MessagesReadMessage | MessagesSubscribeMessage | MessagesSyncMessage,
233
+ expectedGrantor: string,
234
+ expectedGrantee: string,
235
+ permissionGrants: PermissionGrant[],
236
+ messageStore: MessageStore,
237
+ }): Promise<void> {
238
+ const {
239
+ incomingMessage, expectedGrantor, expectedGrantee, permissionGrants, messageStore
240
+ } = input;
241
+
242
+ for (const permissionGrant of permissionGrants) {
243
+ await GrantAuthorization.performBaseValidation({
244
+ incomingMessage,
245
+ expectedGrantor,
246
+ expectedGrantee,
247
+ permissionGrant,
248
+ messageStore
249
+ });
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Determines whether the given record is inside a grant scope.
255
+ */
256
+ private static async isScopeAuthorized(
101
257
  tenant: string,
102
258
  messageToGet: GenericMessage,
103
259
  incomingScope: MessagesPermissionScope,
104
260
  messageStore: MessageStore,
105
- ): Promise<void> {
261
+ ): Promise<boolean> {
106
262
  if (incomingScope.protocol === undefined) {
107
- // if no protocol is specified in the scope, then the grant is for all records
108
- return;
263
+ return true;
109
264
  }
110
265
 
111
266
  if (messageToGet.descriptor.interface === DwnInterfaceName.Records) {
112
- // if the message is a Records interface message, get the RecordsWrite message associated with the record
113
- const recordsMessage = messageToGet as RecordsWriteMessage | RecordsDeleteMessage;
114
- const recordsWriteMessage = Records.isRecordsWrite(recordsMessage) ? recordsMessage :
115
- await RecordsWrite.fetchNewestRecordsWrite(messageStore, tenant, recordsMessage.descriptor.recordId);
267
+ return MessagesGrantAuthorization.isRecordsMessageScopeAuthorized(
268
+ tenant,
269
+ messageToGet as RecordsWriteMessage | RecordsDeleteMessage,
270
+ incomingScope,
271
+ messageStore
272
+ );
273
+ }
116
274
 
117
- if (recordsWriteMessage.descriptor.protocol === incomingScope.protocol) {
118
- // the record protocol matches the incoming scope protocol
119
- return;
120
- }
275
+ if (messageToGet.descriptor.interface === DwnInterfaceName.Protocols) {
276
+ return MessagesGrantAuthorization.isProtocolsConfigureScopeAuthorized(
277
+ messageToGet as ProtocolsConfigureMessage,
278
+ incomingScope
279
+ );
280
+ }
121
281
 
122
- // we check if the protocol is the internal PermissionsProtocol for further validation
123
- if (recordsWriteMessage.descriptor.protocol === PermissionsProtocol.uri) {
124
- // get the permission scope from the permission message
125
- const permissionScope = await PermissionsProtocol.getScopeFromPermissionRecord(
126
- tenant,
127
- messageStore,
128
- recordsWriteMessage as DataEncodedRecordsWriteMessage
129
- );
282
+ return false;
283
+ }
130
284
 
131
- if (PermissionsProtocol.hasProtocolScope(permissionScope) && permissionScope.protocol === incomingScope.protocol) {
132
- // the permissions record scoped protocol matches the incoming scope protocol
133
- return;
134
- }
135
- }
136
- } else if (messageToGet.descriptor.interface === DwnInterfaceName.Protocols) {
137
- // if the message is a protocol message, it must be a `ProtocolConfigure` message
138
- const protocolsConfigureMessage = messageToGet as ProtocolsConfigureMessage;
139
- const configureProtocol = protocolsConfigureMessage.descriptor.definition.protocol;
140
- if (configureProtocol === incomingScope.protocol) {
141
- // the configured protocol matches the incoming scope protocol
142
- return;
143
- }
285
+ private static async isRecordsMessageScopeAuthorized(
286
+ tenant: string,
287
+ recordsMessage: RecordsWriteMessage | RecordsDeleteMessage,
288
+ incomingScope: MessagesPermissionScope,
289
+ messageStore: MessageStore,
290
+ ): Promise<boolean> {
291
+ const recordsWriteMessage = await MessagesGrantAuthorization.getAssociatedRecordsWrite(
292
+ tenant,
293
+ recordsMessage,
294
+ messageStore
295
+ );
296
+
297
+ if (recordsWriteMessage.descriptor.protocol === PermissionsProtocol.uri) {
298
+ return MessagesGrantAuthorization.isPermissionRecordScopeAuthorized(
299
+ tenant,
300
+ recordsWriteMessage,
301
+ incomingScope,
302
+ messageStore
303
+ );
144
304
  }
145
305
 
146
- throw new DwnError(DwnErrorCode.MessagesReadVerifyScopeFailed, 'record message failed scope authorization');
306
+ return PermissionScopeMatcher.matches(incomingScope, MessagesGrantAuthorization.getRecordsScopeTarget(recordsWriteMessage));
307
+ }
308
+
309
+ private static async isPermissionRecordScopeAuthorized(
310
+ tenant: string,
311
+ recordsWriteMessage: RecordsWriteMessage,
312
+ incomingScope: MessagesPermissionScope,
313
+ messageStore: MessageStore,
314
+ ): Promise<boolean> {
315
+ if (MessagesGrantAuthorization.isSubtreeScope(incomingScope)) {
316
+ return false;
317
+ }
318
+
319
+ const permissionScope = await PermissionsProtocol.getScopeFromPermissionRecord(
320
+ tenant,
321
+ messageStore,
322
+ recordsWriteMessage as DataEncodedRecordsWriteMessage
323
+ );
324
+
325
+ return PermissionsProtocol.hasProtocolScope(permissionScope)
326
+ && PermissionScopeMatcher.matches(incomingScope, permissionScope);
327
+ }
328
+
329
+ private static isProtocolsConfigureScopeAuthorized(
330
+ protocolsConfigureMessage: ProtocolsConfigureMessage,
331
+ incomingScope: MessagesPermissionScope
332
+ ): boolean {
333
+ // A delegate with any Messages.Read grant inside a protocol needs that
334
+ // protocol definition to interpret and validate the records it can read.
335
+ return incomingScope.protocol !== undefined &&
336
+ incomingScope.protocol === protocolsConfigureMessage.descriptor.definition.protocol;
337
+ }
338
+
339
+ private static async getAssociatedRecordsWrite(
340
+ tenant: string,
341
+ recordsMessage: RecordsWriteMessage | RecordsDeleteMessage,
342
+ messageStore: MessageStore,
343
+ ): Promise<RecordsWriteMessage> {
344
+ if (Records.isRecordsWrite(recordsMessage)) {
345
+ return recordsMessage;
346
+ }
347
+
348
+ return RecordsWrite.fetchNewestRecordsWrite(messageStore, tenant, recordsMessage.descriptor.recordId);
349
+ }
350
+
351
+ private static getRecordsScopeTarget(recordsWriteMessage: RecordsWriteMessage): ProtocolScope {
352
+ const { protocol, protocolPath } = recordsWriteMessage.descriptor;
353
+ const { contextId } = recordsWriteMessage;
354
+ return { protocol, protocolPath, contextId };
355
+ }
356
+
357
+ private static isSubtreeScope(scope: MessagesPermissionScope): boolean {
358
+ return scope.protocolPath !== undefined || scope.contextId !== undefined;
147
359
  }
148
- }
360
+ }