@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
@@ -1,5 +1,6 @@
1
1
  import freeForAll from '../vectors/protocol-definitions/free-for-all.json' with { type: 'json' };
2
2
  import { Jws } from '../../src/utils/jws.js';
3
+ import { KEY_DELIVERY_PROTOCOL_URI } from '../../src/core/constants.js';
3
4
  import { Message } from '../../src/core/message.js';
4
5
  import { MessagesSync } from '../../src/interfaces/messages-sync.js';
5
6
  import { MessagesSyncHandler } from '../../src/handlers/messages-sync.js';
@@ -8,8 +9,8 @@ import { TestDataGenerator } from '../utils/test-data-generator.js';
8
9
  import { TestEventLog } from '../test-event-stream.js';
9
10
  import { TestStores } from '../test-stores.js';
10
11
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test';
12
+ import { DataStream, Dwn, DwnErrorCode, DwnInterfaceName, DwnMethodName, Encoder, PermissionGrant, PermissionsProtocol, RECORDS_PROJECTION_ROOT_VERSION, RecordsProjection, Time } from '../../src/index.js';
11
13
  import { DidKey, UniversalResolver } from '@enbox/dids';
12
- import { Dwn, DwnErrorCode, DwnInterfaceName, DwnMethodName } from '../../src/index.js';
13
14
  export function testMessagesSyncHandler() {
14
15
  describe('MessagesSyncHandler.handle()', () => {
15
16
  let didResolver;
@@ -231,6 +232,126 @@ export function testMessagesSyncHandler() {
231
232
  expect(reply.entries).toContain(protocolCid);
232
233
  expect(reply.entries).toContain(recordCid);
233
234
  });
235
+ it('returns projected record leaves without protocol configs or out-of-path records', async () => {
236
+ const alice = await TestDataGenerator.generateDidKeyPersona();
237
+ const protocolDefinition = { ...freeForAll, published: true };
238
+ const protocol = protocolDefinition.protocol;
239
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
240
+ author: alice,
241
+ protocolDefinition,
242
+ });
243
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
244
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
245
+ author: alice,
246
+ protocol,
247
+ protocolPath: 'post',
248
+ schema: protocolDefinition.types.post.schema,
249
+ });
250
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
251
+ const { message: attachmentMessage, dataStream: attachmentDataStream } = await TestDataGenerator.generateRecordsWrite({
252
+ author: alice,
253
+ protocol,
254
+ protocolPath: 'post/attachment',
255
+ parentContextId: postMessage.contextId,
256
+ });
257
+ expect((await dwn.processMessage(alice.did, attachmentMessage, { dataStream: attachmentDataStream })).status.code).toBe(202);
258
+ const { message } = await MessagesSync.create({
259
+ signer: Jws.createSigner(alice),
260
+ action: 'leaves',
261
+ prefix: '',
262
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
263
+ projectionScopes: [{ protocol, protocolPath: 'post' }],
264
+ });
265
+ const reply = await dwn.processMessage(alice.did, message);
266
+ expect(reply.status.code).toBe(200);
267
+ expect(reply.entries).toEqual([await Message.getCid(postMessage)]);
268
+ expect(reply.entries).not.toContain(await Message.getCid(protocolMessage));
269
+ expect(reply.entries).not.toContain(await Message.getCid(attachmentMessage));
270
+ });
271
+ it('excludes infrastructure protocols from records-primary projection leaves', async () => {
272
+ const alice = await TestDataGenerator.generateDidKeyPersona();
273
+ const bob = await TestDataGenerator.generateDidKeyPersona();
274
+ const { message: permissionGrantMessage, dataStream: permissionGrantDataStream } = await TestDataGenerator.generateGrantCreate({
275
+ author: alice,
276
+ grantedTo: bob,
277
+ scope: {
278
+ interface: DwnInterfaceName.Messages,
279
+ method: DwnMethodName.Read,
280
+ protocol: 'http://projected-sync-app-protocol',
281
+ },
282
+ });
283
+ expect((await dwn.processMessage(alice.did, permissionGrantMessage, { dataStream: permissionGrantDataStream })).status.code).toBe(202);
284
+ const keyDeliverySchema = 'https://identity.foundation/schemas/key-delivery/context-key';
285
+ const keyDeliveryProtocolDefinition = {
286
+ protocol: KEY_DELIVERY_PROTOCOL_URI,
287
+ published: false,
288
+ types: {
289
+ contextKey: {
290
+ schema: keyDeliverySchema,
291
+ dataFormats: ['application/json'],
292
+ },
293
+ },
294
+ structure: {
295
+ contextKey: {},
296
+ },
297
+ };
298
+ const { message: keyDeliveryConfigureMessage } = await TestDataGenerator.generateProtocolsConfigure({
299
+ author: alice,
300
+ protocolDefinition: keyDeliveryProtocolDefinition,
301
+ });
302
+ expect((await dwn.processMessage(alice.did, keyDeliveryConfigureMessage)).status.code).toBe(202);
303
+ const { message: keyDeliveryMessage, dataStream: keyDeliveryDataStream } = await TestDataGenerator.generateRecordsWrite({
304
+ author: alice,
305
+ protocol: KEY_DELIVERY_PROTOCOL_URI,
306
+ protocolPath: 'contextKey',
307
+ schema: keyDeliverySchema,
308
+ });
309
+ expect((await dwn.processMessage(alice.did, keyDeliveryMessage, { dataStream: keyDeliveryDataStream })).status.code).toBe(202);
310
+ for (const protocol of [PermissionsProtocol.uri, KEY_DELIVERY_PROTOCOL_URI]) {
311
+ const { message } = await MessagesSync.create({
312
+ signer: Jws.createSigner(alice),
313
+ action: 'leaves',
314
+ prefix: '',
315
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
316
+ projectionScopes: [{ protocol }],
317
+ });
318
+ const reply = await dwn.processMessage(alice.did, message);
319
+ expect(reply.status.code).toBe(200);
320
+ expect(reply.entries).toEqual([]);
321
+ }
322
+ });
323
+ it('returns projected roots from the Records projection algorithm', async () => {
324
+ const alice = await TestDataGenerator.generateDidKeyPersona();
325
+ const protocolDefinition = { ...freeForAll, published: true };
326
+ const protocol = protocolDefinition.protocol;
327
+ const projectionScopes = [{ protocol, protocolPath: 'post' }];
328
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
329
+ author: alice,
330
+ protocolDefinition,
331
+ });
332
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
333
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
334
+ author: alice,
335
+ protocol,
336
+ protocolPath: 'post',
337
+ schema: protocolDefinition.types.post.schema,
338
+ });
339
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
340
+ const expectedRoot = await RecordsProjection.getRootHex({
341
+ tenant: alice.did,
342
+ messageStore: messageStore,
343
+ scopes: projectionScopes,
344
+ });
345
+ const { message } = await MessagesSync.create({
346
+ signer: Jws.createSigner(alice),
347
+ action: 'root',
348
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
349
+ projectionScopes,
350
+ });
351
+ const reply = await dwn.processMessage(alice.did, message);
352
+ expect(reply.status.code).toBe(200);
353
+ expect(reply.root).toBe(expectedRoot);
354
+ });
234
355
  });
235
356
  describe('authorization', () => {
236
357
  it('returns 401 if tenant is not the author', async () => {
@@ -278,7 +399,7 @@ export function testMessagesSyncHandler() {
278
399
  const { message: syncMsg } = await MessagesSync.create({
279
400
  signer: Jws.createSigner(bob),
280
401
  action: 'root',
281
- permissionGrantId: grantMessage.recordId,
402
+ permissionGrantIds: [grantMessage.recordId],
282
403
  });
283
404
  const reply = await dwn.processMessage(alice.did, syncMsg);
284
405
  expect(reply.status.code).toBe(200);
@@ -309,7 +430,7 @@ export function testMessagesSyncHandler() {
309
430
  const { message: syncMsg } = await MessagesSync.create({
310
431
  signer: Jws.createSigner(bob),
311
432
  action: 'root',
312
- permissionGrantId: grantMessage.recordId,
433
+ permissionGrantIds: [grantMessage.recordId],
313
434
  });
314
435
  const reply2 = await dwn.processMessage(alice.did, syncMsg);
315
436
  expect(reply2.status.code).toBe(200);
@@ -352,7 +473,7 @@ export function testMessagesSyncHandler() {
352
473
  action: 'leaves',
353
474
  prefix: '',
354
475
  protocol: protocolDefinition.protocol,
355
- permissionGrantId: grantMessage.recordId,
476
+ permissionGrantIds: [grantMessage.recordId],
356
477
  });
357
478
  const reply2 = await dwn.processMessage(alice.did, syncMsg);
358
479
  expect(reply2.status.code).toBe(200);
@@ -394,7 +515,7 @@ export function testMessagesSyncHandler() {
394
515
  action: 'leaves',
395
516
  prefix: '',
396
517
  protocol: protocolDefinition.protocol,
397
- permissionGrantId: grantMessage.recordId,
518
+ permissionGrantIds: [grantMessage.recordId],
398
519
  });
399
520
  const reply = await dwn.processMessage(alice.did, syncMsg);
400
521
  expect(reply.status.code).toBe(200);
@@ -406,6 +527,131 @@ export function testMessagesSyncHandler() {
406
527
  expect(reply.entries).toContain(protocolCid);
407
528
  expect(reply.entries).toContain(recordCid);
408
529
  });
530
+ it('allows protocol sync when one grant in a plural grant set covers the protocol', async () => {
531
+ const alice = await TestDataGenerator.generateDidKeyPersona();
532
+ const bob = await TestDataGenerator.generateDidKeyPersona();
533
+ const protocol1 = { ...freeForAll, published: true, protocol: 'http://plural-grant-sync-1' };
534
+ const protocol2 = { ...freeForAll, published: true, protocol: 'http://plural-grant-sync-2' };
535
+ for (const protocolDefinition of [protocol1, protocol2]) {
536
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
537
+ author: alice,
538
+ protocolDefinition,
539
+ });
540
+ await dwn.processMessage(alice.did, protocolMessage);
541
+ }
542
+ const { message: recordMessage, dataStream } = await TestDataGenerator.generateRecordsWrite({
543
+ author: alice,
544
+ protocol: protocol2.protocol,
545
+ protocolPath: 'post',
546
+ schema: protocol2.types.post.schema,
547
+ });
548
+ await dwn.processMessage(alice.did, recordMessage, { dataStream });
549
+ const grantIds = [];
550
+ for (const protocolDefinition of [protocol1, protocol2]) {
551
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
552
+ author: alice,
553
+ grantedTo: bob,
554
+ scope: {
555
+ interface: DwnInterfaceName.Messages,
556
+ method: DwnMethodName.Read,
557
+ protocol: protocolDefinition.protocol,
558
+ },
559
+ });
560
+ const grantReply = await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream });
561
+ expect(grantReply.status.code).toBe(202);
562
+ grantIds.push(grantMessage.recordId);
563
+ }
564
+ const { message: syncMsg } = await MessagesSync.create({
565
+ signer: Jws.createSigner(bob),
566
+ action: 'leaves',
567
+ prefix: '',
568
+ protocol: protocol2.protocol,
569
+ permissionGrantIds: grantIds.reverse(),
570
+ });
571
+ const reply = await dwn.processMessage(alice.did, syncMsg);
572
+ expect(reply.status.code).toBe(200);
573
+ expect(reply.entries).toContain(await Message.getCid(recordMessage));
574
+ expect(syncMsg.descriptor.permissionGrantIds).toEqual([...grantIds].sort());
575
+ });
576
+ it('rejects sync when any grant in a plural grant set is expired', async () => {
577
+ const alice = await TestDataGenerator.generateDidKeyPersona();
578
+ const bob = await TestDataGenerator.generateDidKeyPersona();
579
+ const protocol = 'http://plural-grant-sync-expired';
580
+ const now = Time.getCurrentTimestamp();
581
+ const { message: activeGrantMessage, dataStream: activeGrantDataStream } = await TestDataGenerator.generateGrantCreate({
582
+ author: alice,
583
+ grantedTo: bob,
584
+ dateExpires: Time.createOffsetTimestamp({ seconds: 60 * 60 }, now),
585
+ scope: {
586
+ interface: DwnInterfaceName.Messages,
587
+ method: DwnMethodName.Read,
588
+ protocol,
589
+ },
590
+ });
591
+ expect((await dwn.processMessage(alice.did, activeGrantMessage, { dataStream: activeGrantDataStream })).status.code).toBe(202);
592
+ const { message: expiredGrantMessage, dataStream: expiredGrantDataStream } = await TestDataGenerator.generateGrantCreate({
593
+ author: alice,
594
+ grantedTo: bob,
595
+ dateGranted: Time.createOffsetTimestamp({ seconds: -120 }, now),
596
+ dateExpires: Time.createOffsetTimestamp({ seconds: -60 }, now),
597
+ scope: {
598
+ interface: DwnInterfaceName.Messages,
599
+ method: DwnMethodName.Read,
600
+ protocol,
601
+ },
602
+ });
603
+ expect((await dwn.processMessage(alice.did, expiredGrantMessage, { dataStream: expiredGrantDataStream })).status.code).toBe(202);
604
+ const { message: syncMsg } = await MessagesSync.create({
605
+ signer: Jws.createSigner(bob),
606
+ action: 'root',
607
+ protocol,
608
+ permissionGrantIds: [activeGrantMessage.recordId, expiredGrantMessage.recordId],
609
+ });
610
+ const reply = await dwn.processMessage(alice.did, syncMsg);
611
+ expect(reply.status.code).toBe(401);
612
+ expect(reply.status.detail).toContain(DwnErrorCode.GrantAuthorizationGrantExpired);
613
+ });
614
+ it('rejects sync when any grant in a plural grant set is revoked', async () => {
615
+ const alice = await TestDataGenerator.generateDidKeyPersona();
616
+ const bob = await TestDataGenerator.generateDidKeyPersona();
617
+ const protocol = 'http://plural-grant-sync-revoked';
618
+ const { message: activeGrantMessage, dataStream: activeGrantDataStream } = await TestDataGenerator.generateGrantCreate({
619
+ author: alice,
620
+ grantedTo: bob,
621
+ scope: {
622
+ interface: DwnInterfaceName.Messages,
623
+ method: DwnMethodName.Read,
624
+ protocol,
625
+ },
626
+ });
627
+ expect((await dwn.processMessage(alice.did, activeGrantMessage, { dataStream: activeGrantDataStream })).status.code).toBe(202);
628
+ const revokedGrant = await TestDataGenerator.generateGrantCreate({
629
+ author: alice,
630
+ grantedTo: bob,
631
+ scope: {
632
+ interface: DwnInterfaceName.Messages,
633
+ method: DwnMethodName.Read,
634
+ protocol,
635
+ },
636
+ });
637
+ expect((await dwn.processMessage(alice.did, revokedGrant.message, { dataStream: revokedGrant.dataStream })).status.code).toBe(202);
638
+ const revocation = await PermissionsProtocol.createRevocation({
639
+ signer: Jws.createSigner(alice),
640
+ grant: PermissionGrant.parse(revokedGrant.dataEncodedMessage),
641
+ });
642
+ const revocationReply = await dwn.processMessage(alice.did, revocation.recordsWrite.message, { dataStream: DataStream.fromBytes(revocation.permissionRevocationBytes) });
643
+ expect(revocationReply.status.code).toBe(202);
644
+ await Time.minimalSleep();
645
+ const { message: syncMsg } = await MessagesSync.create({
646
+ signer: Jws.createSigner(bob),
647
+ action: 'root',
648
+ protocol,
649
+ permissionGrantIds: [activeGrantMessage.recordId, revokedGrant.message.recordId],
650
+ });
651
+ const reply = await dwn.processMessage(alice.did, syncMsg);
652
+ expect(reply.status.code).toBe(401);
653
+ expect(reply.status.detail).toContain(DwnErrorCode.GrantAuthorizationGrantRevoked);
654
+ });
409
655
  it('rejects sync with mismatching interface grant scope', async () => {
410
656
  const alice = await TestDataGenerator.generateDidKeyPersona();
411
657
  const bob = await TestDataGenerator.generateDidKeyPersona();
@@ -424,7 +670,7 @@ export function testMessagesSyncHandler() {
424
670
  const { message: syncMsg } = await MessagesSync.create({
425
671
  signer: Jws.createSigner(bob),
426
672
  action: 'root',
427
- permissionGrantId: grantMessage.recordId,
673
+ permissionGrantIds: [grantMessage.recordId],
428
674
  });
429
675
  const reply = await dwn.processMessage(alice.did, syncMsg);
430
676
  expect(reply.status.code).toBe(401);
@@ -450,12 +696,695 @@ export function testMessagesSyncHandler() {
450
696
  signer: Jws.createSigner(bob),
451
697
  action: 'root',
452
698
  protocol: 'http://protocol2',
453
- permissionGrantId: grantMessage.recordId,
699
+ permissionGrantIds: [grantMessage.recordId],
454
700
  });
455
701
  const reply = await dwn.processMessage(alice.did, syncMsg);
456
702
  expect(reply.status.code).toBe(401);
457
703
  expect(reply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol);
458
704
  });
705
+ it('rejects full-tenant sync actions with a protocol-scoped grant', async () => {
706
+ const alice = await TestDataGenerator.generateDidKeyPersona();
707
+ const bob = await TestDataGenerator.generateDidKeyPersona();
708
+ const { message: grantMessage, dataStream } = await TestDataGenerator.generateGrantCreate({
709
+ author: alice,
710
+ grantedTo: bob,
711
+ scope: {
712
+ interface: DwnInterfaceName.Messages,
713
+ method: DwnMethodName.Read,
714
+ protocol: 'http://protocol-scoped-sync',
715
+ },
716
+ });
717
+ const grantReply = await dwn.processMessage(alice.did, grantMessage, { dataStream });
718
+ expect(grantReply.status.code).toBe(202);
719
+ const syncActions = [
720
+ { action: 'root' },
721
+ { action: 'subtree', prefix: '' },
722
+ { action: 'leaves', prefix: '' },
723
+ { action: 'diff', hashes: {}, depth: 2 },
724
+ ];
725
+ for (const syncAction of syncActions) {
726
+ const { message: syncMsg } = await MessagesSync.create({
727
+ signer: Jws.createSigner(bob),
728
+ ...syncAction,
729
+ permissionGrantIds: [grantMessage.recordId],
730
+ });
731
+ const reply = await dwn.processMessage(alice.did, syncMsg);
732
+ expect(reply.status.code).toBe(401);
733
+ expect(reply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol);
734
+ }
735
+ });
736
+ it('rejects protocol sync with subtree-scoped grants', async () => {
737
+ const alice = await TestDataGenerator.generateDidKeyPersona();
738
+ const bob = await TestDataGenerator.generateDidKeyPersona();
739
+ const protocol = 'http://subtree-scoped-sync';
740
+ const scopedGrants = [
741
+ {
742
+ interface: DwnInterfaceName.Messages,
743
+ method: DwnMethodName.Read,
744
+ protocol,
745
+ protocolPath: 'post',
746
+ },
747
+ {
748
+ interface: DwnInterfaceName.Messages,
749
+ method: DwnMethodName.Read,
750
+ protocol,
751
+ contextId: 'root',
752
+ },
753
+ ];
754
+ for (const scope of scopedGrants) {
755
+ const { message: grantMessage, dataStream } = await TestDataGenerator.generateGrantCreate({
756
+ author: alice,
757
+ grantedTo: bob,
758
+ scope,
759
+ });
760
+ const grantReply = await dwn.processMessage(alice.did, grantMessage, { dataStream });
761
+ expect(grantReply.status.code).toBe(202);
762
+ const { message: syncMsg } = await MessagesSync.create({
763
+ signer: Jws.createSigner(bob),
764
+ action: 'root',
765
+ protocol,
766
+ permissionGrantIds: [grantMessage.recordId],
767
+ });
768
+ const reply = await dwn.processMessage(alice.did, syncMsg);
769
+ expect(reply.status.code).toBe(401);
770
+ expect(reply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationMismatchedProtocol);
771
+ }
772
+ });
773
+ it('allows projected sync with a matching protocolPath Messages.Read grant', async () => {
774
+ const alice = await TestDataGenerator.generateDidKeyPersona();
775
+ const bob = await TestDataGenerator.generateDidKeyPersona();
776
+ const protocolDefinition = { ...freeForAll, published: true };
777
+ const protocol = protocolDefinition.protocol;
778
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
779
+ author: alice,
780
+ protocolDefinition,
781
+ });
782
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
783
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
784
+ author: alice,
785
+ protocol,
786
+ protocolPath: 'post',
787
+ schema: protocolDefinition.types.post.schema,
788
+ });
789
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
790
+ const { message: attachmentMessage, dataStream: attachmentDataStream } = await TestDataGenerator.generateRecordsWrite({
791
+ author: alice,
792
+ protocol,
793
+ protocolPath: 'post/attachment',
794
+ parentContextId: postMessage.contextId,
795
+ });
796
+ expect((await dwn.processMessage(alice.did, attachmentMessage, { dataStream: attachmentDataStream })).status.code).toBe(202);
797
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
798
+ author: alice,
799
+ grantedTo: bob,
800
+ scope: {
801
+ interface: DwnInterfaceName.Messages,
802
+ method: DwnMethodName.Read,
803
+ protocol,
804
+ protocolPath: 'post',
805
+ },
806
+ });
807
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
808
+ const { message: diffMsg } = await MessagesSync.create({
809
+ signer: Jws.createSigner(bob),
810
+ action: 'diff',
811
+ hashes: {},
812
+ depth: 2,
813
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
814
+ projectionScopes: [{ protocol, protocolPath: 'post' }],
815
+ permissionGrantIds: [grantMessage.recordId],
816
+ });
817
+ const reply = await dwn.processMessage(alice.did, diffMsg);
818
+ expect(reply.status.code).toBe(200);
819
+ const protocolCid = await Message.getCid(protocolMessage);
820
+ const postCid = await Message.getCid(postMessage);
821
+ const remoteCids = reply.onlyRemote.map(entry => entry.messageCid);
822
+ expect(remoteCids).toContain(postCid);
823
+ expect(remoteCids).not.toContain(protocolCid);
824
+ expect(remoteCids).not.toContain(await Message.getCid(attachmentMessage));
825
+ expect(reply.dependencies).toEqual([{
826
+ dependencyClass: 'protocolsConfigure',
827
+ messageCid: protocolCid,
828
+ message: protocolMessage,
829
+ rootMessageCid: postCid,
830
+ }]);
831
+ });
832
+ it('returns the governing protocol config dependency for projected sync', async () => {
833
+ const alice = await TestDataGenerator.generateDidKeyPersona();
834
+ const bob = await TestDataGenerator.generateDidKeyPersona();
835
+ const protocol = 'http://projected-sync-config-history';
836
+ const protocolDefinition = {
837
+ ...freeForAll,
838
+ protocol,
839
+ published: true,
840
+ };
841
+ const { message: firstProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
842
+ author: alice,
843
+ messageTimestamp: '2026-01-01T00:00:00.000000Z',
844
+ protocolDefinition,
845
+ });
846
+ expect((await dwn.processMessage(alice.did, firstProtocolMessage)).status.code).toBe(202);
847
+ const { message: secondProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
848
+ author: alice,
849
+ messageTimestamp: '2026-01-02T00:00:00.000000Z',
850
+ protocolDefinition,
851
+ });
852
+ expect((await dwn.processMessage(alice.did, secondProtocolMessage)).status.code).toBe(202);
853
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
854
+ author: alice,
855
+ dateCreated: '2026-01-03T00:00:00.000000Z',
856
+ messageTimestamp: '2026-01-03T00:00:00.000000Z',
857
+ protocol,
858
+ protocolPath: 'post',
859
+ schema: protocolDefinition.types.post.schema,
860
+ });
861
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
862
+ const { message: futureProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
863
+ author: alice,
864
+ messageTimestamp: '2026-01-04T00:00:00.000000Z',
865
+ protocolDefinition,
866
+ });
867
+ expect((await dwn.processMessage(alice.did, futureProtocolMessage)).status.code).toBe(202);
868
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
869
+ author: alice,
870
+ grantedTo: bob,
871
+ scope: {
872
+ interface: DwnInterfaceName.Messages,
873
+ method: DwnMethodName.Read,
874
+ protocol,
875
+ protocolPath: 'post',
876
+ },
877
+ });
878
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
879
+ const { message: diffMsg } = await MessagesSync.create({
880
+ signer: Jws.createSigner(bob),
881
+ action: 'diff',
882
+ hashes: {},
883
+ depth: 2,
884
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
885
+ projectionScopes: [{ protocol, protocolPath: 'post' }],
886
+ permissionGrantIds: [grantMessage.recordId],
887
+ });
888
+ const reply = await dwn.processMessage(alice.did, diffMsg);
889
+ expect(reply.status.code).toBe(200);
890
+ const postCid = await Message.getCid(postMessage);
891
+ expect(reply.onlyRemote.map(entry => entry.messageCid)).toContain(postCid);
892
+ expect(reply.dependencies.map(entry => entry.messageCid)).not.toContain(await Message.getCid(firstProtocolMessage));
893
+ expect(reply.dependencies.map(entry => entry.messageCid)).not.toContain(await Message.getCid(futureProtocolMessage));
894
+ expect(reply.dependencies).toEqual([
895
+ {
896
+ dependencyClass: 'protocolsConfigure',
897
+ messageCid: await Message.getCid(secondProtocolMessage),
898
+ message: secondProtocolMessage,
899
+ rootMessageCid: postCid,
900
+ },
901
+ ]);
902
+ });
903
+ it('returns initial write and protocol config dependencies for projected delete tombstones', async () => {
904
+ const alice = await TestDataGenerator.generateDidKeyPersona();
905
+ const bob = await TestDataGenerator.generateDidKeyPersona();
906
+ const protocolDefinition = { ...freeForAll, protocol: 'http://projected-sync-delete-hints', published: true };
907
+ const unrelatedDefinition = { ...freeForAll, protocol: 'http://projected-sync-delete-hints-unrelated', published: true };
908
+ const protocol = protocolDefinition.protocol;
909
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
910
+ author: alice,
911
+ protocolDefinition,
912
+ });
913
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
914
+ const { message: unrelatedProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
915
+ author: alice,
916
+ protocolDefinition: unrelatedDefinition,
917
+ });
918
+ expect((await dwn.processMessage(alice.did, unrelatedProtocolMessage)).status.code).toBe(202);
919
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
920
+ author: alice,
921
+ protocol,
922
+ protocolPath: 'post',
923
+ schema: protocolDefinition.types.post.schema,
924
+ });
925
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
926
+ const { message: deleteMessage } = await TestDataGenerator.generateRecordsDelete({
927
+ author: alice,
928
+ recordId: postMessage.recordId,
929
+ });
930
+ expect((await dwn.processMessage(alice.did, deleteMessage)).status.code).toBe(202);
931
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
932
+ author: alice,
933
+ grantedTo: bob,
934
+ scope: {
935
+ interface: DwnInterfaceName.Messages,
936
+ method: DwnMethodName.Read,
937
+ protocol,
938
+ protocolPath: 'post',
939
+ },
940
+ });
941
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
942
+ const { message: diffMsg } = await MessagesSync.create({
943
+ signer: Jws.createSigner(bob),
944
+ action: 'diff',
945
+ hashes: {},
946
+ depth: 2,
947
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
948
+ projectionScopes: [{ protocol, protocolPath: 'post' }],
949
+ permissionGrantIds: [grantMessage.recordId],
950
+ });
951
+ const reply = await dwn.processMessage(alice.did, diffMsg);
952
+ expect(reply.status.code).toBe(200);
953
+ const deleteCid = await Message.getCid(deleteMessage);
954
+ const postCid = await Message.getCid(postMessage);
955
+ const protocolCid = await Message.getCid(protocolMessage);
956
+ expect(reply.onlyRemote.map(entry => entry.messageCid)).toEqual([deleteCid]);
957
+ expect(reply.dependencies.map(entry => entry.messageCid)).not.toContain(await Message.getCid(unrelatedProtocolMessage));
958
+ expect(reply.dependencies).toEqual([
959
+ {
960
+ dependencyClass: 'recordsInitialWrite',
961
+ messageCid: postCid,
962
+ message: postMessage,
963
+ rootMessageCid: deleteCid,
964
+ },
965
+ {
966
+ dependencyClass: 'protocolsConfigure',
967
+ messageCid: protocolCid,
968
+ message: protocolMessage,
969
+ rootMessageCid: deleteCid,
970
+ },
971
+ ]);
972
+ const initialWriteDependency = reply.dependencies.find(entry => entry.dependencyClass === 'recordsInitialWrite');
973
+ expect(initialWriteDependency.encodedData).toBeUndefined();
974
+ expect('encodedData' in initialWriteDependency.message).toBe(false);
975
+ });
976
+ it('returns composed protocol config dependencies for projected sync', async () => {
977
+ const alice = await TestDataGenerator.generateDidKeyPersona();
978
+ const bob = await TestDataGenerator.generateDidKeyPersona();
979
+ const socialDefinition = {
980
+ ...freeForAll,
981
+ protocol: 'http://projected-sync-composed-social',
982
+ published: true,
983
+ };
984
+ const profileDefinition = {
985
+ ...freeForAll,
986
+ protocol: 'http://projected-sync-composed-profile',
987
+ published: true,
988
+ uses: { social: socialDefinition.protocol },
989
+ };
990
+ const unrelatedDefinition = {
991
+ ...freeForAll,
992
+ protocol: 'http://projected-sync-composed-unrelated',
993
+ published: true,
994
+ };
995
+ const { message: socialProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
996
+ author: alice,
997
+ protocolDefinition: socialDefinition,
998
+ });
999
+ expect((await dwn.processMessage(alice.did, socialProtocolMessage)).status.code).toBe(202);
1000
+ const { message: profileProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
1001
+ author: alice,
1002
+ protocolDefinition: profileDefinition,
1003
+ });
1004
+ expect((await dwn.processMessage(alice.did, profileProtocolMessage)).status.code).toBe(202);
1005
+ const { message: unrelatedProtocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
1006
+ author: alice,
1007
+ protocolDefinition: unrelatedDefinition,
1008
+ });
1009
+ expect((await dwn.processMessage(alice.did, unrelatedProtocolMessage)).status.code).toBe(202);
1010
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
1011
+ author: alice,
1012
+ protocol: profileDefinition.protocol,
1013
+ protocolPath: 'post',
1014
+ schema: profileDefinition.types.post.schema,
1015
+ });
1016
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
1017
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1018
+ author: alice,
1019
+ grantedTo: bob,
1020
+ scope: {
1021
+ interface: DwnInterfaceName.Messages,
1022
+ method: DwnMethodName.Read,
1023
+ protocol: profileDefinition.protocol,
1024
+ protocolPath: 'post',
1025
+ },
1026
+ });
1027
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1028
+ const { message: diffMsg } = await MessagesSync.create({
1029
+ signer: Jws.createSigner(bob),
1030
+ action: 'diff',
1031
+ hashes: {},
1032
+ depth: 2,
1033
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1034
+ projectionScopes: [{ protocol: profileDefinition.protocol, protocolPath: 'post' }],
1035
+ permissionGrantIds: [grantMessage.recordId],
1036
+ });
1037
+ const reply = await dwn.processMessage(alice.did, diffMsg);
1038
+ expect(reply.status.code).toBe(200);
1039
+ const postCid = await Message.getCid(postMessage);
1040
+ expect(reply.onlyRemote.map(entry => entry.messageCid)).toContain(postCid);
1041
+ expect(reply.dependencies.map(entry => entry.messageCid)).not.toContain(await Message.getCid(unrelatedProtocolMessage));
1042
+ expect(reply.dependencies).toEqual([
1043
+ {
1044
+ dependencyClass: 'protocolsConfigure',
1045
+ messageCid: await Message.getCid(profileProtocolMessage),
1046
+ message: profileProtocolMessage,
1047
+ rootMessageCid: postCid,
1048
+ },
1049
+ {
1050
+ dependencyClass: 'protocolsConfigure',
1051
+ messageCid: await Message.getCid(socialProtocolMessage),
1052
+ message: socialProtocolMessage,
1053
+ rootMessageCid: postCid,
1054
+ },
1055
+ ]);
1056
+ });
1057
+ it('terminates cyclic composed protocol dependencies for projected sync', async () => {
1058
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1059
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1060
+ const protocolA = 'http://projected-sync-cycle-a';
1061
+ const protocolB = 'http://projected-sync-cycle-b';
1062
+ const protocolBBaseDefinition = {
1063
+ ...freeForAll,
1064
+ protocol: protocolB,
1065
+ published: true,
1066
+ };
1067
+ const protocolADefinition = {
1068
+ ...freeForAll,
1069
+ protocol: protocolA,
1070
+ published: true,
1071
+ uses: { b: protocolB },
1072
+ };
1073
+ const protocolBCycleDefinition = {
1074
+ ...freeForAll,
1075
+ protocol: protocolB,
1076
+ published: true,
1077
+ uses: { a: protocolA },
1078
+ };
1079
+ const { message: protocolBBaseMessage } = await TestDataGenerator.generateProtocolsConfigure({
1080
+ author: alice,
1081
+ messageTimestamp: '2026-01-01T00:00:00.000000Z',
1082
+ protocolDefinition: protocolBBaseDefinition,
1083
+ });
1084
+ expect((await dwn.processMessage(alice.did, protocolBBaseMessage)).status.code).toBe(202);
1085
+ const { message: protocolAMessage } = await TestDataGenerator.generateProtocolsConfigure({
1086
+ author: alice,
1087
+ messageTimestamp: '2026-01-02T00:00:00.000000Z',
1088
+ protocolDefinition: protocolADefinition,
1089
+ });
1090
+ expect((await dwn.processMessage(alice.did, protocolAMessage)).status.code).toBe(202);
1091
+ const { message: protocolBCycleMessage } = await TestDataGenerator.generateProtocolsConfigure({
1092
+ author: alice,
1093
+ messageTimestamp: '2026-01-03T00:00:00.000000Z',
1094
+ protocolDefinition: protocolBCycleDefinition,
1095
+ });
1096
+ expect((await dwn.processMessage(alice.did, protocolBCycleMessage)).status.code).toBe(202);
1097
+ const { message: postMessage, dataStream: postDataStream } = await TestDataGenerator.generateRecordsWrite({
1098
+ author: alice,
1099
+ dateCreated: '2026-01-04T00:00:00.000000Z',
1100
+ messageTimestamp: '2026-01-04T00:00:00.000000Z',
1101
+ protocol: protocolA,
1102
+ protocolPath: 'post',
1103
+ schema: protocolADefinition.types.post.schema,
1104
+ });
1105
+ expect((await dwn.processMessage(alice.did, postMessage, { dataStream: postDataStream })).status.code).toBe(202);
1106
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1107
+ author: alice,
1108
+ grantedTo: bob,
1109
+ scope: {
1110
+ interface: DwnInterfaceName.Messages,
1111
+ method: DwnMethodName.Read,
1112
+ protocol: protocolA,
1113
+ protocolPath: 'post',
1114
+ },
1115
+ });
1116
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1117
+ const { message: diffMsg } = await MessagesSync.create({
1118
+ signer: Jws.createSigner(bob),
1119
+ action: 'diff',
1120
+ hashes: {},
1121
+ depth: 2,
1122
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1123
+ projectionScopes: [{ protocol: protocolA, protocolPath: 'post' }],
1124
+ permissionGrantIds: [grantMessage.recordId],
1125
+ });
1126
+ const reply = await dwn.processMessage(alice.did, diffMsg);
1127
+ expect(reply.status.code).toBe(200);
1128
+ const postCid = await Message.getCid(postMessage);
1129
+ const dependencyCids = reply.dependencies.map(entry => entry.messageCid);
1130
+ expect(reply.onlyRemote.map(entry => entry.messageCid)).toContain(postCid);
1131
+ expect(dependencyCids.sort()).toEqual([
1132
+ await Message.getCid(protocolAMessage),
1133
+ await Message.getCid(protocolBCycleMessage),
1134
+ ].sort());
1135
+ expect(dependencyCids).not.toContain(await Message.getCid(protocolBBaseMessage));
1136
+ expect(reply.dependencies.every(entry => entry.rootMessageCid === postCid)).toBe(true);
1137
+ });
1138
+ it('rejects projected sync when no grant covers a requested projection scope', async () => {
1139
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1140
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1141
+ const protocol = 'http://projected-sync-scope-mismatch';
1142
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1143
+ author: alice,
1144
+ grantedTo: bob,
1145
+ scope: {
1146
+ interface: DwnInterfaceName.Messages,
1147
+ method: DwnMethodName.Read,
1148
+ protocol,
1149
+ protocolPath: 'post',
1150
+ },
1151
+ });
1152
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1153
+ const { message: syncMsg } = await MessagesSync.create({
1154
+ signer: Jws.createSigner(bob),
1155
+ action: 'root',
1156
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1157
+ projectionScopes: [{ protocol }],
1158
+ permissionGrantIds: [grantMessage.recordId],
1159
+ });
1160
+ const reply = await dwn.processMessage(alice.did, syncMsg);
1161
+ expect(reply.status.code).toBe(401);
1162
+ expect(reply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationProjectionScopeMismatch);
1163
+ });
1164
+ it('rejects projected sync when any requested projection scope is uncovered', async () => {
1165
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1166
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1167
+ const coveredProtocol = 'http://projected-sync-covered-scope';
1168
+ const uncoveredProtocol = 'http://projected-sync-uncovered-scope';
1169
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1170
+ author: alice,
1171
+ grantedTo: bob,
1172
+ scope: {
1173
+ interface: DwnInterfaceName.Messages,
1174
+ method: DwnMethodName.Read,
1175
+ protocol: coveredProtocol,
1176
+ protocolPath: 'post',
1177
+ },
1178
+ });
1179
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1180
+ const { message: syncMsg } = await MessagesSync.create({
1181
+ signer: Jws.createSigner(bob),
1182
+ action: 'root',
1183
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1184
+ projectionScopes: [
1185
+ { protocol: coveredProtocol, protocolPath: 'post' },
1186
+ { protocol: uncoveredProtocol, protocolPath: 'post' },
1187
+ ],
1188
+ permissionGrantIds: [grantMessage.recordId],
1189
+ });
1190
+ const reply = await dwn.processMessage(alice.did, syncMsg);
1191
+ expect(reply.status.code).toBe(401);
1192
+ expect(reply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationProjectionScopeMismatch);
1193
+ });
1194
+ it('rejects delegated MessagesSync of infrastructure protocols', async () => {
1195
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1196
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1197
+ const carol = await TestDataGenerator.generateDidKeyPersona();
1198
+ const { message: permissionsGrantMessage, dataStream: permissionsGrantDataStream } = await TestDataGenerator.generateGrantCreate({
1199
+ author: alice,
1200
+ grantedTo: bob,
1201
+ scope: {
1202
+ interface: DwnInterfaceName.Messages,
1203
+ method: DwnMethodName.Read,
1204
+ protocol: PermissionsProtocol.uri,
1205
+ },
1206
+ });
1207
+ expect((await dwn.processMessage(alice.did, permissionsGrantMessage, { dataStream: permissionsGrantDataStream })).status.code).toBe(202);
1208
+ const { message: keyDeliveryGrantMessage, dataStream: keyDeliveryGrantDataStream } = await TestDataGenerator.generateGrantCreate({
1209
+ author: alice,
1210
+ grantedTo: bob,
1211
+ scope: {
1212
+ interface: DwnInterfaceName.Messages,
1213
+ method: DwnMethodName.Read,
1214
+ protocol: KEY_DELIVERY_PROTOCOL_URI,
1215
+ },
1216
+ });
1217
+ expect((await dwn.processMessage(alice.did, keyDeliveryGrantMessage, { dataStream: keyDeliveryGrantDataStream })).status.code).toBe(202);
1218
+ const { message: carolGrantMessage, dataStream: carolGrantDataStream } = await TestDataGenerator.generateGrantCreate({
1219
+ author: alice,
1220
+ grantedTo: carol,
1221
+ scope: {
1222
+ interface: DwnInterfaceName.Messages,
1223
+ method: DwnMethodName.Read,
1224
+ protocol: 'http://private-delegate-protocol',
1225
+ },
1226
+ });
1227
+ expect((await dwn.processMessage(alice.did, carolGrantMessage, { dataStream: carolGrantDataStream })).status.code).toBe(202);
1228
+ const { message: readMessage } = await TestDataGenerator.generateMessagesRead({
1229
+ author: bob,
1230
+ messageCid: await Message.getCid(carolGrantMessage),
1231
+ permissionGrantIds: [permissionsGrantMessage.recordId],
1232
+ });
1233
+ const readReply = await dwn.processMessage(alice.did, readMessage);
1234
+ expect(readReply.status.code).toBe(401);
1235
+ expect(readReply.status.detail).toContain(DwnErrorCode.MessagesReadVerifyScopeFailed);
1236
+ const stateIndexSyncActions = [
1237
+ { action: 'root' },
1238
+ { action: 'subtree', prefix: '' },
1239
+ { action: 'leaves', prefix: '' },
1240
+ { action: 'diff', hashes: {}, depth: 2 },
1241
+ ];
1242
+ const infrastructureProtocolGrants = [
1243
+ { protocol: PermissionsProtocol.uri, grantId: permissionsGrantMessage.recordId },
1244
+ { protocol: KEY_DELIVERY_PROTOCOL_URI, grantId: keyDeliveryGrantMessage.recordId },
1245
+ ];
1246
+ for (const { protocol, grantId } of infrastructureProtocolGrants) {
1247
+ for (const syncAction of stateIndexSyncActions) {
1248
+ const { message: syncMessage } = await MessagesSync.create({
1249
+ signer: Jws.createSigner(bob),
1250
+ ...syncAction,
1251
+ protocol,
1252
+ permissionGrantIds: [grantId],
1253
+ });
1254
+ const syncReply = await dwn.processMessage(alice.did, syncMessage);
1255
+ expect(syncReply.status.code).toBe(401);
1256
+ expect(syncReply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationProtocolSyncInfrastructureProtocol);
1257
+ }
1258
+ }
1259
+ for (const { protocol, grantId } of infrastructureProtocolGrants) {
1260
+ const { message: projectedSyncMessage } = await MessagesSync.create({
1261
+ signer: Jws.createSigner(bob),
1262
+ action: 'diff',
1263
+ hashes: {},
1264
+ depth: 2,
1265
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1266
+ projectionScopes: [{ protocol }],
1267
+ permissionGrantIds: [grantId],
1268
+ });
1269
+ const projectedSyncReply = await dwn.processMessage(alice.did, projectedSyncMessage);
1270
+ expect(projectedSyncReply.status.code).toBe(401);
1271
+ expect(projectedSyncReply.status.detail).toContain(DwnErrorCode.MessagesGrantAuthorizationProjectionInfrastructureProtocol);
1272
+ }
1273
+ });
1274
+ it('allows projected sync with a matching contextId Messages.Read grant', async () => {
1275
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1276
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1277
+ const protocolDefinition = { ...freeForAll, published: true };
1278
+ const protocol = protocolDefinition.protocol;
1279
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
1280
+ author: alice,
1281
+ protocolDefinition,
1282
+ });
1283
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
1284
+ const { message: rootMessage, dataStream: rootDataStream } = await TestDataGenerator.generateRecordsWrite({
1285
+ author: alice,
1286
+ protocol,
1287
+ protocolPath: 'post',
1288
+ schema: protocolDefinition.types.post.schema,
1289
+ });
1290
+ expect((await dwn.processMessage(alice.did, rootMessage, { dataStream: rootDataStream })).status.code).toBe(202);
1291
+ const { message: childMessage, dataStream: childDataStream } = await TestDataGenerator.generateRecordsWrite({
1292
+ author: alice,
1293
+ protocol,
1294
+ protocolPath: 'post/attachment',
1295
+ parentContextId: rootMessage.contextId,
1296
+ });
1297
+ expect((await dwn.processMessage(alice.did, childMessage, { dataStream: childDataStream })).status.code).toBe(202);
1298
+ const { message: siblingMessage, dataStream: siblingDataStream } = await TestDataGenerator.generateRecordsWrite({
1299
+ author: alice,
1300
+ protocol,
1301
+ protocolPath: 'post',
1302
+ schema: protocolDefinition.types.post.schema,
1303
+ });
1304
+ expect((await dwn.processMessage(alice.did, siblingMessage, { dataStream: siblingDataStream })).status.code).toBe(202);
1305
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1306
+ author: alice,
1307
+ grantedTo: bob,
1308
+ scope: {
1309
+ interface: DwnInterfaceName.Messages,
1310
+ method: DwnMethodName.Read,
1311
+ protocol,
1312
+ contextId: rootMessage.contextId,
1313
+ },
1314
+ });
1315
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1316
+ const { message: syncMsg } = await MessagesSync.create({
1317
+ signer: Jws.createSigner(bob),
1318
+ action: 'leaves',
1319
+ prefix: '',
1320
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1321
+ projectionScopes: [{ protocol, contextId: rootMessage.contextId }],
1322
+ permissionGrantIds: [grantMessage.recordId],
1323
+ });
1324
+ const reply = await dwn.processMessage(alice.did, syncMsg);
1325
+ expect(reply.status.code).toBe(200);
1326
+ expect(reply.entries).toContain(await Message.getCid(rootMessage));
1327
+ expect(reply.entries).toContain(await Message.getCid(childMessage));
1328
+ expect(reply.entries).not.toContain(await Message.getCid(protocolMessage));
1329
+ expect(reply.entries).not.toContain(await Message.getCid(siblingMessage));
1330
+ });
1331
+ it('returns only protocol-scoped diff entries for a delegated protocol grant', async () => {
1332
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1333
+ const bob = await TestDataGenerator.generateDidKeyPersona();
1334
+ const protocolA = { ...freeForAll, protocol: 'http://delegated-diff-protocol-a' };
1335
+ const protocolB = { ...freeForAll, protocol: 'http://delegated-diff-protocol-b' };
1336
+ for (const protocolDefinition of [protocolA, protocolB]) {
1337
+ const { message: protocolMessage } = await TestDataGenerator.generateProtocolsConfigure({
1338
+ author: alice,
1339
+ protocolDefinition,
1340
+ });
1341
+ expect((await dwn.processMessage(alice.did, protocolMessage)).status.code).toBe(202);
1342
+ }
1343
+ const { message: recordA, dataStream: dataStreamA } = await TestDataGenerator.generateRecordsWrite({
1344
+ author: alice,
1345
+ protocol: protocolA.protocol,
1346
+ protocolPath: 'post',
1347
+ schema: protocolA.types.post.schema,
1348
+ });
1349
+ expect((await dwn.processMessage(alice.did, recordA, { dataStream: dataStreamA })).status.code).toBe(202);
1350
+ const { message: recordB, dataStream: dataStreamB } = await TestDataGenerator.generateRecordsWrite({
1351
+ author: alice,
1352
+ protocol: protocolB.protocol,
1353
+ protocolPath: 'post',
1354
+ schema: protocolB.types.post.schema,
1355
+ });
1356
+ expect((await dwn.processMessage(alice.did, recordB, { dataStream: dataStreamB })).status.code).toBe(202);
1357
+ const { message: grantMessage, dataStream: grantDataStream } = await TestDataGenerator.generateGrantCreate({
1358
+ author: alice,
1359
+ grantedTo: bob,
1360
+ scope: {
1361
+ interface: DwnInterfaceName.Messages,
1362
+ method: DwnMethodName.Read,
1363
+ protocol: protocolA.protocol,
1364
+ },
1365
+ });
1366
+ expect((await dwn.processMessage(alice.did, grantMessage, { dataStream: grantDataStream })).status.code).toBe(202);
1367
+ const { message: diffMsg } = await MessagesSync.create({
1368
+ signer: Jws.createSigner(bob),
1369
+ action: 'diff',
1370
+ hashes: {},
1371
+ depth: 2,
1372
+ protocol: protocolA.protocol,
1373
+ permissionGrantIds: [grantMessage.recordId],
1374
+ });
1375
+ const reply = await dwn.processMessage(alice.did, diffMsg);
1376
+ expect(reply.status.code).toBe(200);
1377
+ const remoteCids = reply.onlyRemote.map(entry => entry.messageCid);
1378
+ expect(remoteCids).toContain(await Message.getCid(recordA));
1379
+ expect(remoteCids).not.toContain(await Message.getCid(recordB));
1380
+ expect(reply.onlyRemote.every(entry => {
1381
+ if (entry.message?.descriptor.interface !== DwnInterfaceName.Records) {
1382
+ return true;
1383
+ }
1384
+ const recordsMessage = entry.message;
1385
+ return recordsMessage.descriptor.protocol === protocolA.protocol;
1386
+ })).toBe(true);
1387
+ });
459
1388
  });
460
1389
  });
461
1390
  describe('input validation', () => {
@@ -547,6 +1476,77 @@ export function testMessagesSyncHandler() {
547
1476
  parseStub.restore();
548
1477
  }
549
1478
  });
1479
+ it('returns 400 when projection scopes are provided without a projection root version', async () => {
1480
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1481
+ const { message } = await MessagesSync.create({
1482
+ signer: Jws.createSigner(alice),
1483
+ action: 'root',
1484
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1485
+ projectionScopes: [{ protocol: 'http://projected-sync-schema' }],
1486
+ });
1487
+ delete message.descriptor.projectionRootVersion;
1488
+ const reply = await dwn.processMessage(alice.did, message);
1489
+ expect(reply.status.code).toBe(400);
1490
+ expect(reply.status.detail).toContain('SchemaValidatorFailure');
1491
+ });
1492
+ it('returns 400 when projection root version is provided without projection scopes', async () => {
1493
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1494
+ const { message } = await MessagesSync.create({
1495
+ signer: Jws.createSigner(alice),
1496
+ action: 'root',
1497
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1498
+ projectionScopes: [{ protocol: 'http://projected-sync-schema' }],
1499
+ });
1500
+ delete message.descriptor.projectionScopes;
1501
+ const reply = await dwn.processMessage(alice.did, message);
1502
+ expect(reply.status.code).toBe(400);
1503
+ expect(reply.status.detail).toContain('SchemaValidatorFailure');
1504
+ });
1505
+ it('returns 400 when projection root version is unsupported', async () => {
1506
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1507
+ const { message } = await MessagesSync.create({
1508
+ signer: Jws.createSigner(alice),
1509
+ action: 'root',
1510
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1511
+ projectionScopes: [{ protocol: 'http://projected-sync-schema' }],
1512
+ });
1513
+ const unsupportedProjectionRootVersion = 'records-primary-scope-root-v2';
1514
+ const descriptor = {
1515
+ ...message.descriptor,
1516
+ projectionRootVersion: unsupportedProjectionRootVersion,
1517
+ };
1518
+ const authorization = await Message.createAuthorization({
1519
+ descriptor,
1520
+ signer: Jws.createSigner(alice),
1521
+ });
1522
+ const unsupportedVersionMessage = {
1523
+ ...message,
1524
+ descriptor,
1525
+ authorization,
1526
+ };
1527
+ const validateJsonSchemaStub = sinon.stub(Message, 'validateJsonSchema');
1528
+ try {
1529
+ const reply = await dwn.processMessage(alice.did, unsupportedVersionMessage);
1530
+ expect(reply.status.code).toBe(400);
1531
+ expect(reply.status.detail).toContain(DwnErrorCode.MessagesSyncUnsupportedProjectionRootVersion);
1532
+ }
1533
+ finally {
1534
+ validateJsonSchemaStub.restore();
1535
+ }
1536
+ });
1537
+ it('returns 400 when projected sync also specifies a protocol root', async () => {
1538
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1539
+ const { message } = await MessagesSync.create({
1540
+ signer: Jws.createSigner(alice),
1541
+ action: 'root',
1542
+ projectionRootVersion: RECORDS_PROJECTION_ROOT_VERSION,
1543
+ projectionScopes: [{ protocol: 'http://projected-sync-schema' }],
1544
+ });
1545
+ message.descriptor.protocol = 'http://projected-sync-schema';
1546
+ const reply = await dwn.processMessage(alice.did, message);
1547
+ expect(reply.status.code).toBe(400);
1548
+ expect(reply.status.detail).toContain('SchemaValidatorFailure');
1549
+ });
550
1550
  });
551
1551
  describe('diff action', () => {
552
1552
  it('returns empty diff when client hashes match server hashes', async () => {
@@ -619,6 +1619,26 @@ export function testMessagesSyncHandler() {
619
1619
  expect(reply.onlyLocal).toContain('00');
620
1620
  expect(reply.onlyLocal).toContain('01');
621
1621
  });
1622
+ it('prunes empty server subtrees at the current diff depth', async () => {
1623
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1624
+ const getSubtreeHashSpy = sinon.spy(stateIndex, 'getSubtreeHash');
1625
+ try {
1626
+ const { message: diffMsg } = await MessagesSync.create({
1627
+ signer: Jws.createSigner(alice),
1628
+ action: 'diff',
1629
+ hashes: {},
1630
+ depth: 64,
1631
+ });
1632
+ const reply = await dwn.processMessage(alice.did, diffMsg);
1633
+ expect(reply.status.code).toBe(200);
1634
+ expect(reply.onlyRemote).toEqual([]);
1635
+ expect(reply.onlyLocal).toEqual([]);
1636
+ expect(getSubtreeHashSpy.callCount).toBe(1);
1637
+ }
1638
+ finally {
1639
+ getSubtreeHashSpy.restore();
1640
+ }
1641
+ });
622
1642
  it('inlines small data payloads as encodedData in onlyRemote entries', async () => {
623
1643
  const alice = await TestDataGenerator.generateDidKeyPersona();
624
1644
  await TestDataGenerator.installDefaultTestProtocol(dwn, alice);
@@ -645,6 +1665,32 @@ export function testMessagesSyncHandler() {
645
1665
  expect(recordEntry.encodedData).toBeDefined();
646
1666
  expect(typeof recordEntry.encodedData).toBe('string');
647
1667
  });
1668
+ it('inlines small dataStore-backed payloads using the RecordsWrite recordId', async () => {
1669
+ const alice = await TestDataGenerator.generateDidKeyPersona();
1670
+ await TestDataGenerator.installDefaultTestProtocol(dwn, alice);
1671
+ const dataBytes = new TextEncoder().encode('small data-store payload');
1672
+ const { message: recordMessage, recordsWrite } = await TestDataGenerator.generateRecordsWrite({
1673
+ author: alice,
1674
+ data: dataBytes,
1675
+ dataFormat: 'text/plain',
1676
+ });
1677
+ const indexes = await recordsWrite.constructIndexes(true);
1678
+ const messageCid = await Message.getCid(recordMessage);
1679
+ await dataStore.put(alice.did, recordMessage.recordId, recordMessage.descriptor.dataCid, DataStream.fromBytes(dataBytes));
1680
+ await messageStore.put(alice.did, recordMessage, indexes);
1681
+ await stateIndex.insert(alice.did, messageCid, indexes);
1682
+ const { message: diffMsg } = await MessagesSync.create({
1683
+ signer: Jws.createSigner(alice),
1684
+ action: 'diff',
1685
+ hashes: {},
1686
+ depth: 2,
1687
+ });
1688
+ const reply = await dwn.processMessage(alice.did, diffMsg);
1689
+ expect(reply.status.code).toBe(200);
1690
+ const recordEntry = reply.onlyRemote.find(entry => entry.messageCid === messageCid);
1691
+ expect(recordEntry).toBeDefined();
1692
+ expect(recordEntry.encodedData).toBe(Encoder.bytesToBase64Url(dataBytes));
1693
+ });
648
1694
  it('returns 400 when hashes or depth are missing', async () => {
649
1695
  const alice = await TestDataGenerator.generateDidKeyPersona();
650
1696
  // create a valid diff message then strip required fields