@enbox/dwn-sdk-js 0.4.4 → 0.4.6

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 (201) hide show
  1. package/dist/browser.mjs +15 -15
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +5406 -2391
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/json-schemas/definitions.json +18 -0
  6. package/dist/esm/src/core/constants.js +3 -3
  7. package/dist/esm/src/core/constants.js.map +1 -1
  8. package/dist/esm/src/core/dwn-error.js +29 -1
  9. package/dist/esm/src/core/dwn-error.js.map +1 -1
  10. package/dist/esm/src/core/messages-grant-authorization.js +20 -0
  11. package/dist/esm/src/core/messages-grant-authorization.js.map +1 -1
  12. package/dist/esm/src/core/protocol-authorization-validation.js +80 -3
  13. package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
  14. package/dist/esm/src/core/recording-validation-state-reader.js +5 -0
  15. package/dist/esm/src/core/recording-validation-state-reader.js.map +1 -1
  16. package/dist/esm/src/core/replication-apply.js +163 -7
  17. package/dist/esm/src/core/replication-apply.js.map +1 -1
  18. package/dist/esm/src/core/validation-state-reader.js +20 -0
  19. package/dist/esm/src/core/validation-state-reader.js.map +1 -1
  20. package/dist/esm/src/dwn.js +19 -2
  21. package/dist/esm/src/dwn.js.map +1 -1
  22. package/dist/esm/src/handlers/messages-query.js +3 -5
  23. package/dist/esm/src/handlers/messages-query.js.map +1 -1
  24. package/dist/esm/src/handlers/protocols-configure.js +10 -4
  25. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  26. package/dist/esm/src/handlers/records-write.js +1 -1
  27. package/dist/esm/src/handlers/records-write.js.map +1 -1
  28. package/dist/esm/src/index.js +2 -1
  29. package/dist/esm/src/index.js.map +1 -1
  30. package/dist/esm/src/interfaces/protocols-configure.js +23 -10
  31. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  32. package/dist/esm/src/interfaces/records-write-signing.js +2 -8
  33. package/dist/esm/src/interfaces/records-write-signing.js.map +1 -1
  34. package/dist/esm/src/interfaces/records-write.js +43 -65
  35. package/dist/esm/src/interfaces/records-write.js.map +1 -1
  36. package/dist/esm/src/protocols/encryption.js +411 -0
  37. package/dist/esm/src/protocols/encryption.js.map +1 -0
  38. package/dist/esm/src/protocols/permission-grant.js +0 -5
  39. package/dist/esm/src/protocols/permission-grant.js.map +1 -1
  40. package/dist/esm/src/types/message-types.js.map +1 -1
  41. package/dist/esm/src/types/permission-types.js.map +1 -1
  42. package/dist/esm/src/types/protocols-types.js.map +1 -1
  43. package/dist/esm/src/utils/encryption.js +156 -188
  44. package/dist/esm/src/utils/encryption.js.map +1 -1
  45. package/dist/esm/src/utils/hd-key.js +0 -1
  46. package/dist/esm/src/utils/hd-key.js.map +1 -1
  47. package/dist/esm/src/utils/permission-scope.js +9 -6
  48. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  49. package/dist/esm/src/utils/protocols.js +12 -9
  50. package/dist/esm/src/utils/protocols.js.map +1 -1
  51. package/dist/esm/src/utils/records.js +54 -56
  52. package/dist/esm/src/utils/records.js.map +1 -1
  53. package/dist/esm/src/utils/replication.js +27 -4
  54. package/dist/esm/src/utils/replication.js.map +1 -1
  55. package/dist/esm/tests/core/replication-apply.spec.js +141 -1
  56. package/dist/esm/tests/core/replication-apply.spec.js.map +1 -1
  57. package/dist/esm/tests/core/replication-replay-property.spec.js +1 -0
  58. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  59. package/dist/esm/tests/core/validation-read-closure.spec.js +111 -0
  60. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  61. package/dist/esm/tests/core/validation-state-reader.spec.js +89 -0
  62. package/dist/esm/tests/core/validation-state-reader.spec.js.map +1 -1
  63. package/dist/esm/tests/features/protocol-composition.spec.js +30 -31
  64. package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
  65. package/dist/esm/tests/fuzz/encryption.fuzz.spec.js +29 -121
  66. package/dist/esm/tests/fuzz/encryption.fuzz.spec.js.map +1 -1
  67. package/dist/esm/tests/handlers/messages-query.spec.js +71 -3
  68. package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
  69. package/dist/esm/tests/handlers/messages-read.spec.js +2 -2
  70. package/dist/esm/tests/handlers/messages-read.spec.js.map +1 -1
  71. package/dist/esm/tests/handlers/records-read.spec.js +9 -135
  72. package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
  73. package/dist/esm/tests/handlers/records-write.spec.js +279 -19
  74. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  75. package/dist/esm/tests/interfaces/protocols-configure.spec.js +17 -22
  76. package/dist/esm/tests/interfaces/protocols-configure.spec.js.map +1 -1
  77. package/dist/esm/tests/interfaces/records-write.spec.js +170 -150
  78. package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
  79. package/dist/esm/tests/protocols/encryption.spec.js +914 -0
  80. package/dist/esm/tests/protocols/encryption.spec.js.map +1 -0
  81. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js +100 -100
  82. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js.map +1 -1
  83. package/dist/esm/tests/store/message-store-level.spec.js +20 -2
  84. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  85. package/dist/esm/tests/utils/encryption-callbacks.spec.js +88 -125
  86. package/dist/esm/tests/utils/encryption-callbacks.spec.js.map +1 -1
  87. package/dist/esm/tests/utils/encryption.spec.js +121 -271
  88. package/dist/esm/tests/utils/encryption.spec.js.map +1 -1
  89. package/dist/esm/tests/utils/permission-scope.spec.js +5 -1
  90. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  91. package/dist/esm/tests/utils/records.spec.js +15 -5
  92. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  93. package/dist/esm/tests/utils/test-data-generator.js +7 -44
  94. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  95. package/dist/esm/tests/validation/json-schemas/encryption/audience-key.spec.js +40 -0
  96. package/dist/esm/tests/validation/json-schemas/encryption/audience-key.spec.js.map +1 -0
  97. package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js +72 -0
  98. package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js.map +1 -0
  99. package/dist/types/generated/precompiled-validators.d.ts +140 -92
  100. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  101. package/dist/types/src/core/constants.d.ts +2 -2
  102. package/dist/types/src/core/constants.d.ts.map +1 -1
  103. package/dist/types/src/core/core-protocol.d.ts +1 -1
  104. package/dist/types/src/core/core-protocol.d.ts.map +1 -1
  105. package/dist/types/src/core/dwn-error.d.ts +29 -1
  106. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  107. package/dist/types/src/core/messages-grant-authorization.d.ts +2 -0
  108. package/dist/types/src/core/messages-grant-authorization.d.ts.map +1 -1
  109. package/dist/types/src/core/protocol-authorization-validation.d.ts +2 -2
  110. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  111. package/dist/types/src/core/recording-validation-state-reader.d.ts +9 -0
  112. package/dist/types/src/core/recording-validation-state-reader.d.ts.map +1 -1
  113. package/dist/types/src/core/replication-apply.d.ts +4 -3
  114. package/dist/types/src/core/replication-apply.d.ts.map +1 -1
  115. package/dist/types/src/core/validation-state-reader.d.ts +9 -0
  116. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  117. package/dist/types/src/dwn.d.ts +1 -0
  118. package/dist/types/src/dwn.d.ts.map +1 -1
  119. package/dist/types/src/handlers/messages-query.d.ts.map +1 -1
  120. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  121. package/dist/types/src/index.d.ts +6 -5
  122. package/dist/types/src/index.d.ts.map +1 -1
  123. package/dist/types/src/interfaces/protocols-configure.d.ts +1 -1
  124. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  125. package/dist/types/src/interfaces/records-write-signing.d.ts +4 -8
  126. package/dist/types/src/interfaces/records-write-signing.d.ts.map +1 -1
  127. package/dist/types/src/interfaces/records-write.d.ts +10 -26
  128. package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
  129. package/dist/types/src/protocols/encryption.d.ts +43 -0
  130. package/dist/types/src/protocols/encryption.d.ts.map +1 -0
  131. package/dist/types/src/protocols/permission-grant.d.ts +0 -7
  132. package/dist/types/src/protocols/permission-grant.d.ts.map +1 -1
  133. package/dist/types/src/types/encryption-types.d.ts +5 -38
  134. package/dist/types/src/types/encryption-types.d.ts.map +1 -1
  135. package/dist/types/src/types/message-types.d.ts +0 -13
  136. package/dist/types/src/types/message-types.d.ts.map +1 -1
  137. package/dist/types/src/types/permission-types.d.ts +0 -9
  138. package/dist/types/src/types/permission-types.d.ts.map +1 -1
  139. package/dist/types/src/types/protocols-types.d.ts +9 -14
  140. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  141. package/dist/types/src/types/records-types.d.ts +3 -3
  142. package/dist/types/src/types/records-types.d.ts.map +1 -1
  143. package/dist/types/src/types/validation-state-reader.d.ts +12 -0
  144. package/dist/types/src/types/validation-state-reader.d.ts.map +1 -1
  145. package/dist/types/src/utils/encryption.d.ts +61 -145
  146. package/dist/types/src/utils/encryption.d.ts.map +1 -1
  147. package/dist/types/src/utils/hd-key.d.ts +1 -1
  148. package/dist/types/src/utils/hd-key.d.ts.map +1 -1
  149. package/dist/types/src/utils/permission-scope.d.ts +4 -3
  150. package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
  151. package/dist/types/src/utils/protocols.d.ts +3 -5
  152. package/dist/types/src/utils/protocols.d.ts.map +1 -1
  153. package/dist/types/src/utils/records.d.ts +5 -13
  154. package/dist/types/src/utils/records.d.ts.map +1 -1
  155. package/dist/types/src/utils/replication.d.ts +3 -0
  156. package/dist/types/src/utils/replication.d.ts.map +1 -1
  157. package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
  158. package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
  159. package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
  160. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  161. package/dist/types/tests/protocols/encryption.spec.d.ts +2 -0
  162. package/dist/types/tests/protocols/encryption.spec.d.ts.map +1 -0
  163. package/dist/types/tests/scenarios/end-to-end-tests.spec.d.ts.map +1 -1
  164. package/dist/types/tests/utils/test-data-generator.d.ts +0 -5
  165. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  166. package/dist/types/tests/validation/json-schemas/encryption/audience-key.spec.d.ts +2 -0
  167. package/dist/types/tests/validation/json-schemas/encryption/audience-key.spec.d.ts.map +1 -0
  168. package/dist/types/tests/validation/json-schemas/encryption/grant-key.spec.d.ts +2 -0
  169. package/dist/types/tests/validation/json-schemas/encryption/grant-key.spec.d.ts.map +1 -0
  170. package/package.json +3 -3
  171. package/src/core/constants.ts +3 -3
  172. package/src/core/core-protocol.ts +1 -1
  173. package/src/core/dwn-error.ts +29 -1
  174. package/src/core/messages-grant-authorization.ts +27 -0
  175. package/src/core/protocol-authorization-validation.ts +145 -6
  176. package/src/core/recording-validation-state-reader.ts +13 -0
  177. package/src/core/replication-apply.ts +222 -8
  178. package/src/core/validation-state-reader.ts +30 -0
  179. package/src/dwn.ts +24 -2
  180. package/src/handlers/messages-query.ts +6 -5
  181. package/src/handlers/protocols-configure.ts +19 -3
  182. package/src/handlers/records-write.ts +1 -1
  183. package/src/index.ts +15 -5
  184. package/src/interfaces/protocols-configure.ts +39 -12
  185. package/src/interfaces/records-write-signing.ts +5 -12
  186. package/src/interfaces/records-write.ts +75 -92
  187. package/src/protocols/encryption.ts +649 -0
  188. package/src/protocols/permission-grant.ts +0 -6
  189. package/src/types/encryption-types.ts +9 -38
  190. package/src/types/message-types.ts +0 -13
  191. package/src/types/permission-types.ts +0 -10
  192. package/src/types/protocols-types.ts +9 -16
  193. package/src/types/records-types.ts +3 -3
  194. package/src/types/validation-state-reader.ts +13 -0
  195. package/src/utils/encryption.ts +258 -280
  196. package/src/utils/hd-key.ts +1 -1
  197. package/src/utils/permission-scope.ts +10 -6
  198. package/src/utils/protocols.ts +16 -15
  199. package/src/utils/records.ts +86 -71
  200. package/src/utils/replication.ts +30 -4
  201. package/dist/esm/tests/vectors/protocol-definitions/chat.json +0 -70
@@ -15,7 +15,7 @@ import { Time } from '../utils/time.js';
15
15
  import { validateProtocolTagSchemaDefinition } from '../utils/protocol-tags.js';
16
16
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
17
17
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
18
- import { isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
18
+ import { getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
19
19
  import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js';
20
20
  import { ProtocolAction, ProtocolActor, ProtocolRecordLimitStrategy } from '../types/protocols-types.js';
21
21
 
@@ -150,6 +150,14 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
150
150
  // gather all declared record types
151
151
  const recordTypes = Object.keys(definition.types);
152
152
 
153
+ const hasEncryptedTypes = Object.values(definition.types).some((type): boolean => type.encryptionRequired === true);
154
+ if (hasEncryptedTypes && definition.$keyAgreement === undefined) {
155
+ throw new DwnError(
156
+ DwnErrorCode.ProtocolsConfigureMissingTopLevelKeyAgreement,
157
+ `Protocol '${definition.protocol}' declares encrypted types but has no top-level $keyAgreement.`
158
+ );
159
+ }
160
+
153
161
  // gather all roles (local roles only — cross-protocol roles are validated by alias existence)
154
162
  const roles = ProtocolsConfigure.fetchAllRolePathsRecursively('', definition.structure as ProtocolRuleSet, []);
155
163
 
@@ -161,6 +169,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
161
169
  roles,
162
170
  uses,
163
171
  types : definition.types,
172
+ rootStructure : definition.structure,
164
173
  });
165
174
  }
166
175
 
@@ -207,10 +216,10 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
207
216
  private static validateRuleSetRecursively(
208
217
  input: {
209
218
  ruleSet: ProtocolRuleSet, ruleSetProtocolPath: string, recordTypes: string[],
210
- roles: string[], uses?: ProtocolUses, types: ProtocolTypes
219
+ roles: string[], uses?: ProtocolUses, types: ProtocolTypes, rootStructure: { [key: string]: ProtocolRuleSet }
211
220
  }
212
221
  ): void {
213
- const { ruleSet, ruleSetProtocolPath, recordTypes, roles, uses, types } = input;
222
+ const { ruleSet, ruleSetProtocolPath, recordTypes, roles, uses, types, rootStructure } = input;
214
223
 
215
224
  // Validate $ref constraints: $ref is only supported at root level (no `/` in protocol path),
216
225
  // and a $ref node is a pure attachment point with no other directives.
@@ -412,23 +421,40 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
412
421
  }
413
422
  }
414
423
 
415
- // Warn when `encryptionRequired: true` is combined with `{ who: 'anyone', can: ['read'] }`.
416
- // Authorization allows anyone to read the record, but encryption prevents them from
417
- // decrypting the data — almost certainly unintentional. (issue #115)
418
424
  if (ruleSetProtocolPath !== '') {
419
425
  const typeName = ruleSetProtocolPath.split('/').pop()!;
420
426
  const protocolType = types[typeName];
421
427
  if (protocolType?.encryptionRequired === true) {
428
+ if (ruleSet.$keyAgreement === undefined) {
429
+ throw new DwnError(
430
+ DwnErrorCode.ProtocolsConfigureMissingEncryptedPathKeyAgreement,
431
+ `Encrypted protocol path '${ruleSetProtocolPath}' has no $keyAgreement.`
432
+ );
433
+ }
434
+
422
435
  const anyoneCanRead = actionRules.some(
423
436
  (rule: ProtocolActionRule): boolean => rule.who === ProtocolActor.Anyone && rule.can.includes(ProtocolAction.Read)
424
437
  );
425
438
  if (anyoneCanRead) {
426
- console.warn(
427
- `ProtocolsConfigure: type '${typeName}' at path '${ruleSetProtocolPath}' has ` +
428
- `encryptionRequired: true but allows { who: 'anyone', can: ['read'] }. ` +
429
- `Anyone can read the record but no one outside the key holders can decrypt it.`
439
+ throw new DwnError(
440
+ DwnErrorCode.ProtocolsConfigureInvalidEncryptedAnyoneRead,
441
+ `Encrypted protocol path '${ruleSetProtocolPath}' allows { who: 'anyone', can: ['read'] }.`
430
442
  );
431
443
  }
444
+
445
+ for (const actionRule of actionRules) {
446
+ if (actionRule.role === undefined || !actionRule.can.includes(ProtocolAction.Read) || isCrossProtocolRef(actionRule.role)) {
447
+ continue;
448
+ }
449
+
450
+ const roleRuleSet = getRuleSetAtPath(actionRule.role, rootStructure);
451
+ if (roleRuleSet?.$keyAgreement === undefined) {
452
+ throw new DwnError(
453
+ DwnErrorCode.ProtocolsConfigureInvalidEncryptedRoleMissingKeyAgreement,
454
+ `Encrypted protocol path '${ruleSetProtocolPath}' references role '${actionRule.role}' with no $keyAgreement.`
455
+ );
456
+ }
457
+ }
432
458
  }
433
459
  }
434
460
 
@@ -490,13 +516,14 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
490
516
  roles,
491
517
  uses,
492
518
  types,
519
+ rootStructure,
493
520
  });
494
521
  }
495
522
  }
496
523
 
497
524
  /**
498
525
  * Validates that a `$ref` node is a pure attachment point: it must NOT have
499
- * `$actions`, `$role`, `$size`, `$tags`, or `$encryption`.
526
+ * `$actions`, `$role`, `$size`, `$tags`, or `$keyAgreement`.
500
527
  * Also validates that the `$ref` alias exists in the `uses` map.
501
528
  */
502
529
  private static validateRefNode(ruleSet: ProtocolRuleSet, ruleSetProtocolPath: string, uses: ProtocolUses | undefined): void {
@@ -519,7 +546,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
519
546
  }
520
547
 
521
548
  // validate that `$ref` nodes do not have other directives
522
- const forbiddenDirectives = ['$actions', '$role', '$size', '$tags', '$encryption', '$recordLimit', '$immutable', '$delivery', '$squash'] as const;
549
+ const forbiddenDirectives = ['$actions', '$role', '$size', '$tags', '$keyAgreement', '$encryption', '$recordLimit', '$immutable', '$delivery', '$squash'] as const;
523
550
  for (const directive of forbiddenDirectives) {
524
551
  if (ruleSet[directive] !== undefined) {
525
552
  throw new DwnError(
@@ -1,6 +1,6 @@
1
1
  import type { GeneralJws } from '../types/jws-types.js';
2
2
  import type { MessageSigner } from '../types/signer.js';
3
- import type { EncryptionInput, JweEncryption } from '../utils/encryption.js';
3
+ import type { DwnEncryption, EncryptionInput } from '../utils/encryption.js';
4
4
  import type { RecordsWriteAttestationPayload, RecordsWriteMessage, RecordsWriteSignaturePayload } from '../types/records-types.js';
5
5
 
6
6
  import { Cid } from '../utils/cid.js';
@@ -11,22 +11,15 @@ import { Jws } from '../utils/jws.js';
11
11
  import { removeUndefinedProperties } from '@enbox/common';
12
12
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
13
13
 
14
- /**
15
- * Creates the JWE `encryption` property if encryption input is given. Else `undefined` is returned.
16
- * Uses ECDH-ES+A256KW key agreement with X25519 and AEAD content encryption (A256GCM or XC20P).
17
- * @param encryptionInput The encryption input containing CEK, IV, authentication tag, and recipient key encryption inputs.
18
- */
14
+ /** Creates the RecordsWrite `encryption` property if encryption input is given. */
19
15
  export async function createEncryptionProperty(
20
16
  encryptionInput: EncryptionInput | undefined,
21
- ): Promise<JweEncryption | undefined> {
17
+ ): Promise<DwnEncryption | undefined> {
22
18
  if (encryptionInput === undefined) {
23
19
  return undefined;
24
20
  }
25
21
 
26
- // Build the JWE structure. The authentication tag comes from the AEAD encryption of record data.
27
- const jwe = await Encryption.buildJwe(encryptionInput, encryptionInput.authenticationTag);
28
-
29
- return jwe;
22
+ return Encryption.buildEncryptionProperty(encryptionInput);
30
23
  }
31
24
 
32
25
  /**
@@ -52,7 +45,7 @@ export async function createSignerSignature(input: {
52
45
  contextId: string,
53
46
  descriptorCid: string,
54
47
  attestation: GeneralJws | undefined,
55
- encryption: JweEncryption | undefined,
48
+ encryption: DwnEncryption | undefined,
56
49
  signer: MessageSigner,
57
50
  delegatedGrantId?: string,
58
51
  permissionGrantId?: string,
@@ -3,7 +3,6 @@ import type { KeyValues } from '../types/query-types.js';
3
3
  import type { MessageInterface } from '../types/message-interface.js';
4
4
  import type { MessageSigner } from '../types/signer.js';
5
5
  import type { MessageStore } from '../types/message-store.js';
6
- import type { PublicKeyJwk } from '../types/jose-types.js';
7
6
  import type { ValidationStateReader } from '../types/validation-state-reader.js';
8
7
  import type {
9
8
  DataEncodedRecordsWriteMessage,
@@ -13,10 +12,11 @@ import type {
13
12
  RecordsWriteSignaturePayload,
14
13
  RecordsWriteTags
15
14
  } from '../types/records-types.js';
16
- import type { EncryptionInput, JweEncryption } from '../utils/encryption.js';
15
+ import type { DwnEncryption, EncryptionInput } from '../utils/encryption.js';
17
16
  import type { GenericMessage, GenericSignaturePayload } from '../types/message-types.js';
18
17
 
19
18
  import { Cid } from '../utils/cid.js';
19
+ import { Encryption } from '../utils/encryption.js';
20
20
  import { Jws } from '../utils/jws.js';
21
21
  import { Message } from '../core/message.js';
22
22
  import { PermissionGrant } from '../protocols/permission-grant.js';
@@ -77,6 +77,11 @@ export type RecordsWriteOptions = {
77
77
  delegatedGrant?: DataEncodedRecordsWriteMessage;
78
78
 
79
79
  attestationSigners?: MessageSigner[];
80
+
81
+ /**
82
+ * Pre-built encryption envelope. Use `encryptionInput` when wrapping a CEK from plaintext/ciphertext inputs.
83
+ */
84
+ encryption?: DwnEncryption;
80
85
  encryptionInput?: EncryptionInput;
81
86
  permissionGrantId?: string;
82
87
 
@@ -87,19 +92,14 @@ export type RecordsWriteOptions = {
87
92
  */
88
93
  squash?: true;
89
94
 
90
- /**
91
- * The author's ProtocolPath-derived public key for key delivery.
92
- * When set, this is attached to the authorization model so the DWN owner
93
- * can encrypt context keys back to the author without querying the
94
- * author's DWN.
95
- */
96
- authorKeyDeliveryPublicKey?: {
97
- rootKeyId: string;
98
- publicKeyJwk: PublicKeyJwk;
99
- };
100
95
  };
101
96
 
102
- export type { EncryptionInput, KeyEncryptionInput } from '../utils/encryption.js';
97
+ export type {
98
+ EncryptionInput,
99
+ KeyEncryptionInput,
100
+ ProtocolPathKeyEncryptionInput,
101
+ RoleAudienceKeyEncryptionInput,
102
+ } from '../utils/encryption.js';
103
103
 
104
104
  export type CreateFromOptions = {
105
105
  recordsWriteMessage: RecordsWriteMessage,
@@ -262,6 +262,9 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
262
262
  // - `initialWrite`: the initial RecordsWrite when this message is an update
263
263
  const { encodedData: _, initialWrite: __, ...messageToValidate } = message as RecordsWriteMessage & Record<string, unknown>;
264
264
  Message.validateJsonSchema(messageToValidate);
265
+ if (message.encryption !== undefined) {
266
+ Encryption.validateEncryptionProperty(message.encryption);
267
+ }
265
268
 
266
269
  // asynchronous checks that are required by the constructor to initialize members properly
267
270
 
@@ -298,23 +301,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
298
301
  * If not given, it means this write is for a root protocol record.
299
302
  */
300
303
  public static async create(options: RecordsWriteOptions): Promise<RecordsWrite> {
301
- if (options.protocol === undefined || options.protocolPath === undefined) {
302
- throw new DwnError(DwnErrorCode.RecordsWriteCreateMissingProtocol, '`protocol` and `protocolPath` are required');
303
- }
304
-
305
- if ((options.data === undefined && options.dataCid === undefined) ||
306
- (options.data !== undefined && options.dataCid !== undefined)) {
307
- throw new DwnError(DwnErrorCode.RecordsWriteCreateDataAndDataCidMutuallyExclusive, 'one and only one parameter between `data` and `dataCid` is required');
308
- }
309
-
310
- if ((options.dataCid === undefined && options.dataSize !== undefined) ||
311
- (options.dataCid !== undefined && options.dataSize === undefined)) {
312
- throw new DwnError(DwnErrorCode.RecordsWriteCreateDataCidAndDataSizeMutuallyInclusive, '`dataCid` and `dataSize` must both be defined or undefined at the same time');
313
- }
314
-
315
- if (options.signer === undefined && options.delegatedGrant !== undefined) {
316
- throw new DwnError(DwnErrorCode.RecordsWriteCreateMissingSigner, '`signer` must be given when `delegatedGrant` is given');
317
- }
304
+ RecordsWrite.validateCreateOptions(options);
318
305
 
319
306
  const dataCid = options.dataCid ?? await Cid.computeDagPbCidFromBytes(options.data!);
320
307
  const dataSize = options.dataSize ?? options.data!.length;
@@ -362,7 +349,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
362
349
  const attestation = await createAttestation(descriptorCid, options.attestationSigners);
363
350
 
364
351
  // `encryption` generation
365
- const encryption = await createEncryptionProperty(options.encryptionInput);
352
+ const encryption = options.encryption ?? await createEncryptionProperty(options.encryptionInput);
366
353
 
367
354
  const message: InternalRecordsWriteMessage = {
368
355
  recordId,
@@ -377,17 +364,43 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
377
364
 
378
365
  if (options.signer !== undefined) {
379
366
  await recordsWrite.sign({
380
- signer : options.signer,
381
- delegatedGrant : options.delegatedGrant,
367
+ signer : options.signer,
368
+ delegatedGrant : options.delegatedGrant,
382
369
  ...permissionGrantInvocation,
383
- protocolRole : options.protocolRole,
384
- authorKeyDeliveryPublicKey : options.authorKeyDeliveryPublicKey,
370
+ protocolRole : options.protocolRole,
385
371
  });
386
372
  }
387
373
 
388
374
  return recordsWrite;
389
375
  }
390
376
 
377
+ private static validateCreateOptions(options: RecordsWriteOptions): void {
378
+ if (options.protocol === undefined || options.protocolPath === undefined) {
379
+ throw new DwnError(DwnErrorCode.RecordsWriteCreateMissingProtocol, '`protocol` and `protocolPath` are required');
380
+ }
381
+
382
+ if ((options.data === undefined && options.dataCid === undefined) ||
383
+ (options.data !== undefined && options.dataCid !== undefined)) {
384
+ throw new DwnError(DwnErrorCode.RecordsWriteCreateDataAndDataCidMutuallyExclusive, 'one and only one parameter between `data` and `dataCid` is required');
385
+ }
386
+
387
+ if ((options.dataCid === undefined && options.dataSize !== undefined) ||
388
+ (options.dataCid !== undefined && options.dataSize === undefined)) {
389
+ throw new DwnError(DwnErrorCode.RecordsWriteCreateDataCidAndDataSizeMutuallyInclusive, '`dataCid` and `dataSize` must both be defined or undefined at the same time');
390
+ }
391
+
392
+ if (options.signer === undefined && options.delegatedGrant !== undefined) {
393
+ throw new DwnError(DwnErrorCode.RecordsWriteCreateMissingSigner, '`signer` must be given when `delegatedGrant` is given');
394
+ }
395
+
396
+ if (options.encryption !== undefined && options.encryptionInput !== undefined) {
397
+ throw new DwnError(
398
+ DwnErrorCode.RecordsWriteCreateEncryptionAndEncryptionInputMutuallyExclusive,
399
+ '`encryption` and `encryptionInput` cannot both be defined'
400
+ );
401
+ }
402
+ }
403
+
391
404
  private static getRecordIdFromContextId(contextId: string | undefined): string | undefined {
392
405
  return contextId?.split('/').filter(segment => segment !== '').pop();
393
406
  }
@@ -454,7 +467,9 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
454
467
  delegatedGrant : options.delegatedGrant,
455
468
  // finally still need signers
456
469
  signer : options.signer,
457
- attestationSigners : options.attestationSigners
470
+ attestationSigners : options.attestationSigners,
471
+ encryption : options.encryptionInput === undefined && options.data === undefined ? sourceMessage.encryption : undefined,
472
+ encryptionInput : options.encryptionInput,
458
473
  };
459
474
 
460
475
  const recordsWrite = await RecordsWrite.create(createOptions);
@@ -498,51 +513,16 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
498
513
  }
499
514
 
500
515
  /**
501
- * Encrypts the symmetric encryption key using the public keys given and attach the resulting `encryption` property to the RecordsWrite.
502
- *
503
- * @param options.append - When `true`, appends new `recipients` entries to the existing
504
- * `encryption` property instead of replacing it. Requires `this._message.encryption` to
505
- * already exist (i.e., the record must already be encrypted).
516
+ * Encrypts the symmetric encryption key using the public keys given and attaches the resulting `encryption` property.
506
517
  */
507
518
  public async encryptSymmetricEncryptionKey(
508
519
  encryptionInput: EncryptionInput,
509
- options?: { append?: boolean },
510
520
  ): Promise<void> {
511
- if (options?.append) {
512
- if (!this._message.encryption) {
513
- throw new DwnError(
514
- DwnErrorCode.RecordsWriteMissingEncryption,
515
- 'Cannot append recipients: record does not have an existing `encryption` property.'
516
- );
517
- }
518
-
519
- // Build only the new recipients (reuses createEncryptionProperty for ECDH-ES+A256KW logic)
520
- const newEncryption = await createEncryptionProperty(encryptionInput);
521
- if (newEncryption) {
522
- this._message.encryption.recipients.push(...newEncryption.recipients);
523
- }
524
-
525
- // In append mode, preserve the author's identity and authorization so
526
- // that signAsOwner() can be called afterwards. The author's signature
527
- // payload will have a stale encryptionCid (since we just appended new
528
- // recipients), but the owner's signature vouches for the
529
- // updated state. validateIntegrity() skips the encryptionCid check on
530
- // the author's signature when an ownerSignature is present.
531
- //
532
- // NOTE: An alternative design would deliver the DEK out-of-band via the
533
- // key-delivery protocol (as a field on the contextKey record) instead of
534
- // mutating the record's encryption property. That avoids the stale
535
- // encryptionCid issue entirely but adds complexity to the read path and
536
- // the contextKey schema. We chose the in-record approach because it keeps
537
- // records self-contained and the read/decrypt path unchanged.
538
- } else {
539
- this._message.encryption = await createEncryptionProperty(encryptionInput);
521
+ this._message.encryption = await createEncryptionProperty(encryptionInput);
540
522
 
541
- // Full replacement invalidates the authorization — caller must re-sign.
542
- delete this._message.authorization;
543
- this._signaturePayload = undefined;
544
- this._author = undefined;
545
- }
523
+ delete this._message.authorization;
524
+ this._signaturePayload = undefined;
525
+ this._author = undefined;
546
526
  }
547
527
 
548
528
  /**
@@ -553,9 +533,8 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
553
533
  delegatedGrant?: DataEncodedRecordsWriteMessage,
554
534
  permissionGrantId?: string,
555
535
  protocolRole?: string,
556
- authorKeyDeliveryPublicKey?: { rootKeyId: string; publicKeyJwk: PublicKeyJwk },
557
536
  }): Promise<void> {
558
- const { signer, delegatedGrant, permissionGrantId, protocolRole, authorKeyDeliveryPublicKey } = options;
537
+ const { signer, delegatedGrant, permissionGrantId, protocolRole } = options;
559
538
 
560
539
  // compute delegated grant ID and author if delegated grant is given
561
540
  let delegatedGrantId;
@@ -601,10 +580,6 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
601
580
  this._message.authorization.authorDelegatedGrant = delegatedGrant;
602
581
  }
603
582
 
604
- if (authorKeyDeliveryPublicKey !== undefined) {
605
- this._message.authorization.authorKeyDeliveryPublicKey = authorKeyDeliveryPublicKey;
606
- }
607
-
608
583
  // there is opportunity to optimize here as the payload is constructed within `createAuthorization(...)`
609
584
  this._signaturePayload = Jws.decodePlainObjectPayload(signature);
610
585
  this._author = authorDid;
@@ -719,16 +694,24 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
719
694
  }
720
695
  }
721
696
 
722
- // If `encryption` is given in message, make sure the correct `encryptionCid`
723
- // is in the payload of the message signature UNLESS the message has an
724
- // ownerSignature. When the DWN owner appends recipients to an externally-authored
725
- // record before normal admission, the author's encryptionCid becomes stale. The
726
- // owner's signature vouches for the updated encryption property, so the mismatch
727
- // is expected and safe.
728
- const hasOwnerSignature = this.message.authorization?.ownerSignature !== undefined;
729
- if (signaturePayload.encryptionCid !== undefined && !hasOwnerSignature) {
697
+ // if `encryption` is given in message, make sure the correct `encryptionCid` is in the payload of the message signature
698
+ if (this.message.encryption !== undefined && signaturePayload.encryptionCid === undefined) {
699
+ throw new DwnError(
700
+ DwnErrorCode.RecordsWriteValidateIntegrityEncryptionCidMissing,
701
+ 'encryptionCid must be present in the authorization payload when encryption is present in the message'
702
+ );
703
+ }
704
+
705
+ if (this.message.encryption === undefined && signaturePayload.encryptionCid !== undefined) {
706
+ throw new DwnError(
707
+ DwnErrorCode.RecordsWriteValidateIntegrityEncryptionCidMissing,
708
+ 'encryption must be present in the message when encryptionCid is present in the authorization payload'
709
+ );
710
+ }
711
+
712
+ if (this.message.encryption !== undefined) {
730
713
  const expectedEncryptionCid = await Cid.computeCid(this.message.encryption);
731
- const actualEncryptionCid = signaturePayload.encryptionCid;
714
+ const actualEncryptionCid = signaturePayload.encryptionCid!;
732
715
  if (actualEncryptionCid !== expectedEncryptionCid) {
733
716
  throw new DwnError(
734
717
  DwnErrorCode.RecordsWriteValidateIntegrityEncryptionCidMismatch,
@@ -871,7 +854,7 @@ export class RecordsWrite implements MessageInterface<RecordsWriteMessage> {
871
854
  contextId: string,
872
855
  descriptorCid: string,
873
856
  attestation: GeneralJws | undefined,
874
- encryption: JweEncryption | undefined,
857
+ encryption: DwnEncryption | undefined,
875
858
  signer: MessageSigner,
876
859
  delegatedGrantId?: string,
877
860
  permissionGrantId?: string,