cojson 0.20.7 → 0.20.9

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 (209) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -0
  3. package/dist/SyncStateManager.d.ts.map +1 -1
  4. package/dist/SyncStateManager.js +0 -2
  5. package/dist/SyncStateManager.js.map +1 -1
  6. package/dist/base64url.d.ts +15 -0
  7. package/dist/base64url.d.ts.map +1 -1
  8. package/dist/base64url.js +101 -5
  9. package/dist/base64url.js.map +1 -1
  10. package/dist/base64url.test.js +76 -1
  11. package/dist/base64url.test.js.map +1 -1
  12. package/dist/coValue.d.ts +2 -1
  13. package/dist/coValue.d.ts.map +1 -1
  14. package/dist/coValue.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +9 -11
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +92 -65
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +38 -7
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +226 -30
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/binaryCoStream.d.ts +63 -0
  24. package/dist/coValues/binaryCoStream.d.ts.map +1 -0
  25. package/dist/coValues/binaryCoStream.js +125 -0
  26. package/dist/coValues/binaryCoStream.js.map +1 -0
  27. package/dist/coValues/coList.d.ts +3 -1
  28. package/dist/coValues/coList.d.ts.map +1 -1
  29. package/dist/coValues/coList.js +15 -6
  30. package/dist/coValues/coList.js.map +1 -1
  31. package/dist/coValues/coMap.d.ts +1 -1
  32. package/dist/coValues/coMap.d.ts.map +1 -1
  33. package/dist/coValues/coMap.js +2 -2
  34. package/dist/coValues/coMap.js.map +1 -1
  35. package/dist/coValues/coStream.d.ts +0 -38
  36. package/dist/coValues/coStream.d.ts.map +1 -1
  37. package/dist/coValues/coStream.js +0 -86
  38. package/dist/coValues/coStream.js.map +1 -1
  39. package/dist/coValues/group.d.ts +44 -6
  40. package/dist/coValues/group.d.ts.map +1 -1
  41. package/dist/coValues/group.js +198 -17
  42. package/dist/coValues/group.js.map +1 -1
  43. package/dist/coreToCoValue.d.ts +2 -1
  44. package/dist/coreToCoValue.d.ts.map +1 -1
  45. package/dist/coreToCoValue.js +2 -1
  46. package/dist/coreToCoValue.js.map +1 -1
  47. package/dist/crypto/NapiCrypto.d.ts +18 -24
  48. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  49. package/dist/crypto/NapiCrypto.js +98 -60
  50. package/dist/crypto/NapiCrypto.js.map +1 -1
  51. package/dist/crypto/RNCrypto.d.ts +16 -3
  52. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  53. package/dist/crypto/RNCrypto.js +117 -54
  54. package/dist/crypto/RNCrypto.js.map +1 -1
  55. package/dist/crypto/WasmCrypto.d.ts +18 -24
  56. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  57. package/dist/crypto/WasmCrypto.js +100 -61
  58. package/dist/crypto/WasmCrypto.js.map +1 -1
  59. package/dist/crypto/crypto.d.ts +55 -19
  60. package/dist/crypto/crypto.d.ts.map +1 -1
  61. package/dist/crypto/crypto.js +14 -3
  62. package/dist/crypto/crypto.js.map +1 -1
  63. package/dist/exports.d.ts +7 -3
  64. package/dist/exports.d.ts.map +1 -1
  65. package/dist/exports.js +4 -2
  66. package/dist/exports.js.map +1 -1
  67. package/dist/localNode.d.ts +3 -1
  68. package/dist/localNode.d.ts.map +1 -1
  69. package/dist/localNode.js +10 -3
  70. package/dist/localNode.js.map +1 -1
  71. package/dist/media.d.ts +1 -1
  72. package/dist/media.d.ts.map +1 -1
  73. package/dist/permissions.d.ts +2 -1
  74. package/dist/permissions.d.ts.map +1 -1
  75. package/dist/permissions.js +19 -3
  76. package/dist/permissions.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +24 -12
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +70 -58
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/sqliteAsync/types.d.ts +1 -1
  82. package/dist/storage/sqliteAsync/types.d.ts.map +1 -1
  83. package/dist/storage/types.d.ts +1 -0
  84. package/dist/storage/types.d.ts.map +1 -1
  85. package/dist/sync.d.ts.map +1 -1
  86. package/dist/sync.js +7 -1
  87. package/dist/sync.js.map +1 -1
  88. package/dist/tests/CojsonMessageChannel.test.js +2 -2
  89. package/dist/tests/SQLiteClientAsync.test.d.ts +2 -0
  90. package/dist/tests/SQLiteClientAsync.test.d.ts.map +1 -0
  91. package/dist/tests/SQLiteClientAsync.test.js +64 -0
  92. package/dist/tests/SQLiteClientAsync.test.js.map +1 -0
  93. package/dist/tests/StorageApiAsync.test.js +2 -8
  94. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  95. package/dist/tests/SyncStateManager.test.js +2 -2
  96. package/dist/tests/WasmCrypto.test.js +1 -15
  97. package/dist/tests/WasmCrypto.test.js.map +1 -1
  98. package/dist/tests/coList.test.js +24 -5
  99. package/dist/tests/coList.test.js.map +1 -1
  100. package/dist/tests/coStream.test.js +4 -3
  101. package/dist/tests/coStream.test.js.map +1 -1
  102. package/dist/tests/coValueCore.initTransaction.test.d.ts +2 -0
  103. package/dist/tests/coValueCore.initTransaction.test.d.ts.map +1 -0
  104. package/dist/tests/coValueCore.initTransaction.test.js +438 -0
  105. package/dist/tests/coValueCore.initTransaction.test.js.map +1 -0
  106. package/dist/tests/coValueCore.test.js +11 -19
  107. package/dist/tests/coValueCore.test.js.map +1 -1
  108. package/dist/tests/crypto.test.js +83 -0
  109. package/dist/tests/crypto.test.js.map +1 -1
  110. package/dist/tests/deleteCoValue.test.js +5 -5
  111. package/dist/tests/deleteCoValue.test.js.map +1 -1
  112. package/dist/tests/group.inheritance.test.js +11 -0
  113. package/dist/tests/group.inheritance.test.js.map +1 -1
  114. package/dist/tests/group.test.js +24 -1
  115. package/dist/tests/group.test.js.map +1 -1
  116. package/dist/tests/groupSealer.test.d.ts +2 -0
  117. package/dist/tests/groupSealer.test.d.ts.map +1 -0
  118. package/dist/tests/groupSealer.test.js +913 -0
  119. package/dist/tests/groupSealer.test.js.map +1 -0
  120. package/dist/tests/setup.js +5 -0
  121. package/dist/tests/setup.js.map +1 -1
  122. package/dist/tests/sync.auth.test.js +10 -10
  123. package/dist/tests/sync.concurrentLoad.test.js +12 -12
  124. package/dist/tests/sync.deleted.test.js +8 -8
  125. package/dist/tests/sync.garbageCollection.test.js +10 -10
  126. package/dist/tests/sync.invite.test.js +12 -12
  127. package/dist/tests/sync.known.test.js +2 -2
  128. package/dist/tests/sync.load.test.js +107 -107
  129. package/dist/tests/sync.mesh.test.js +164 -46
  130. package/dist/tests/sync.mesh.test.js.map +1 -1
  131. package/dist/tests/sync.multipleServers.test.js +43 -43
  132. package/dist/tests/sync.peerReconciliation.test.js +29 -29
  133. package/dist/tests/sync.sharding.test.js +3 -3
  134. package/dist/tests/sync.storage.test.js +104 -104
  135. package/dist/tests/sync.storage.test.js.map +1 -1
  136. package/dist/tests/sync.storageAsync.test.js +56 -56
  137. package/dist/tests/sync.upload.test.js +22 -22
  138. package/dist/tests/testStorage.d.ts +2 -0
  139. package/dist/tests/testStorage.d.ts.map +1 -1
  140. package/dist/tests/testStorage.js +30 -6
  141. package/dist/tests/testStorage.js.map +1 -1
  142. package/dist/typeUtils/isCoValue.js +1 -1
  143. package/dist/typeUtils/isCoValue.js.map +1 -1
  144. package/package.json +4 -4
  145. package/src/SyncStateManager.ts +0 -2
  146. package/src/base64url.test.ts +89 -1
  147. package/src/base64url.ts +134 -6
  148. package/src/coValue.ts +2 -1
  149. package/src/coValueCore/coValueCore.ts +126 -84
  150. package/src/coValueCore/verifiedState.ts +335 -53
  151. package/src/coValues/binaryCoStream.ts +217 -0
  152. package/src/coValues/coList.ts +21 -8
  153. package/src/coValues/coMap.ts +3 -0
  154. package/src/coValues/coStream.ts +0 -170
  155. package/src/coValues/group.ts +270 -21
  156. package/src/coreToCoValue.ts +2 -1
  157. package/src/crypto/NapiCrypto.ts +198 -95
  158. package/src/crypto/RNCrypto.ts +229 -102
  159. package/src/crypto/WasmCrypto.ts +201 -95
  160. package/src/crypto/crypto.ts +118 -45
  161. package/src/exports.ts +11 -5
  162. package/src/localNode.ts +17 -1
  163. package/src/media.ts +1 -1
  164. package/src/permissions.ts +30 -7
  165. package/src/storage/sqliteAsync/client.ts +136 -115
  166. package/src/storage/sqliteAsync/types.ts +3 -1
  167. package/src/storage/types.ts +4 -0
  168. package/src/sync.ts +10 -1
  169. package/src/tests/CojsonMessageChannel.test.ts +2 -2
  170. package/src/tests/SQLiteClientAsync.test.ts +75 -0
  171. package/src/tests/StorageApiAsync.test.ts +4 -9
  172. package/src/tests/SyncStateManager.test.ts +2 -2
  173. package/src/tests/WasmCrypto.test.ts +1 -25
  174. package/src/tests/coList.test.ts +39 -5
  175. package/src/tests/coStream.test.ts +4 -5
  176. package/src/tests/coValueCore.initTransaction.test.ts +836 -0
  177. package/src/tests/coValueCore.test.ts +11 -22
  178. package/src/tests/crypto.test.ts +107 -0
  179. package/src/tests/deleteCoValue.test.ts +5 -5
  180. package/src/tests/group.inheritance.test.ts +16 -0
  181. package/src/tests/group.test.ts +29 -1
  182. package/src/tests/groupSealer.test.ts +1473 -0
  183. package/src/tests/setup.ts +6 -0
  184. package/src/tests/sync.auth.test.ts +10 -10
  185. package/src/tests/sync.concurrentLoad.test.ts +12 -12
  186. package/src/tests/sync.deleted.test.ts +8 -8
  187. package/src/tests/sync.garbageCollection.test.ts +10 -10
  188. package/src/tests/sync.invite.test.ts +12 -12
  189. package/src/tests/sync.known.test.ts +2 -2
  190. package/src/tests/sync.load.test.ts +107 -107
  191. package/src/tests/sync.mesh.test.ts +189 -46
  192. package/src/tests/sync.multipleServers.test.ts +43 -43
  193. package/src/tests/sync.peerReconciliation.test.ts +29 -29
  194. package/src/tests/sync.sharding.test.ts +3 -3
  195. package/src/tests/sync.storage.test.ts +104 -104
  196. package/src/tests/sync.storageAsync.test.ts +56 -56
  197. package/src/tests/sync.upload.test.ts +22 -22
  198. package/src/tests/testStorage.ts +39 -9
  199. package/src/typeUtils/isCoValue.ts +1 -1
  200. package/dist/coValueCore/SessionMap.d.ts +0 -55
  201. package/dist/coValueCore/SessionMap.d.ts.map +0 -1
  202. package/dist/coValueCore/SessionMap.js +0 -206
  203. package/dist/coValueCore/SessionMap.js.map +0 -1
  204. package/dist/tests/coreWasm.test.d.ts +0 -2
  205. package/dist/tests/coreWasm.test.d.ts.map +0 -1
  206. package/dist/tests/coreWasm.test.js +0 -203
  207. package/dist/tests/coreWasm.test.js.map +0 -1
  208. package/src/coValueCore/SessionMap.ts +0 -394
  209. package/src/tests/coreWasm.test.ts +0 -452
@@ -12,6 +12,9 @@ import type {
12
12
  KeyID,
13
13
  KeySecret,
14
14
  Sealed,
15
+ SealedForGroup,
16
+ SealerID,
17
+ SealerSecret,
15
18
  } from "../crypto/crypto.js";
16
19
  import {
17
20
  AgentID,
@@ -21,7 +24,7 @@ import {
21
24
  isAgentID,
22
25
  isParentGroupReference,
23
26
  } from "../ids.js";
24
- import { JsonObject } from "../jsonValue.js";
27
+ import { JsonObject, JsonValue } from "../jsonValue.js";
25
28
  import { logger } from "../logger.js";
26
29
  import {
27
30
  AccountRole,
@@ -40,11 +43,52 @@ import {
40
43
  import { RawCoList } from "./coList.js";
41
44
  import { RawCoMap } from "./coMap.js";
42
45
  import { RawCoPlainText } from "./coPlainText.js";
43
- import { RawBinaryCoStream, RawCoStream } from "./coStream.js";
46
+ import { RawBinaryCoStream } from "./binaryCoStream.js";
47
+ import { RawCoStream } from "./coStream.js";
44
48
 
45
49
  export const EVERYONE = "everyone" as const;
46
50
  export type Everyone = "everyone";
47
51
 
52
+ /**
53
+ * Format a composite groupSealer value that includes the readKeyID.
54
+ * New format: "readKeyID@sealerID" - explicitly associates the sealer with
55
+ * the readKey it was derived from. This prevents inconsistency when different
56
+ * admins concurrently rotate keys and migrate the groupSealer.
57
+ *
58
+ * @internal
59
+ */
60
+ export function formatGroupSealerValue(readKeyID: KeyID, sealerID: SealerID) {
61
+ return `${readKeyID}@${sealerID}` as const;
62
+ }
63
+
64
+ /**
65
+ * Extract the SealerID from a groupSealer field value.
66
+ * Handles both new format ("readKeyID@sealerID") and legacy format ("sealer_z...").
67
+ *
68
+ * @internal
69
+ */
70
+ function extractSealerID(groupSealerValue: string): SealerID {
71
+ const idx = groupSealerValue.indexOf("@");
72
+ if (idx > 0) {
73
+ return groupSealerValue.substring(idx + 1) as SealerID;
74
+ }
75
+ return groupSealerValue as SealerID;
76
+ }
77
+
78
+ /**
79
+ * Extract the readKeyID from a groupSealer field value.
80
+ * Returns undefined for legacy format values that don't include the readKeyID.
81
+ *
82
+ * @internal
83
+ */
84
+ function extractReadKeyID(groupSealerValue: string): KeyID | undefined {
85
+ const idx = groupSealerValue.indexOf("@");
86
+ if (idx > 0) {
87
+ return groupSealerValue.substring(0, idx) as KeyID;
88
+ }
89
+ return undefined;
90
+ }
91
+
48
92
  export type ParentGroupReferenceRole =
49
93
  | "revoked"
50
94
  | "extend"
@@ -59,6 +103,9 @@ export type GroupShape = {
59
103
  [key: RawAccountID | AgentID]: Role;
60
104
  [EVERYONE]?: Role;
61
105
  readKey?: KeyID;
106
+ // Group-level asymmetric encryption key (public portion only)
107
+ // Private key is derived from readKey, not stored
108
+ groupSealer?: `${KeyID}@${SealerID}`;
62
109
  [writeKeyFor: `writeKeyFor_${RawAccountID | AgentID}`]: KeyID;
63
110
  [revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`]: Sealed<KeySecret>;
64
111
  [revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
@@ -66,6 +113,9 @@ export type GroupShape = {
66
113
  KeySecret,
67
114
  { encryptedID: KeyID; encryptingID: KeyID }
68
115
  >;
116
+ // Key revelations encrypted to group sealer (from non-members extending child groups)
117
+ // Using _sealedFor_ prefix to distinguish from _for_ patterns used for member/key revelations
118
+ [keyForSealer: `${KeyID}_sealedFor_${SealerID}`]: SealedForGroup<KeySecret>;
69
119
  [parent: ParentGroupReference]: ParentGroupReferenceRole;
70
120
  [child: ChildGroupReference]: "revoked" | "extend";
71
121
  };
@@ -123,6 +173,45 @@ function healMissingKeyForEveryone(group: RawGroup) {
123
173
  }
124
174
  }
125
175
 
176
+ /**
177
+ * Backfill the groupSealer field for groups created before the feature was introduced.
178
+ * Since the groupSealer is derived deterministically from the readKey, parallel migrations
179
+ * from different accounts will always produce the same value.
180
+ *
181
+ * Only admins/managers can set the groupSealer field.
182
+ */
183
+ function healMissingGroupSealer(group: RawGroup) {
184
+ if (group.get("groupSealer")) {
185
+ return;
186
+ }
187
+
188
+ // Check direct membership only (not inherited roles via parent groups)
189
+ // to avoid accessing parentGroupsChanges which may not be initialized during early construction
190
+ const currentAccountOrAgent = group.core.node.getCurrentAccountOrAgentID();
191
+ const directRole = group.get(currentAccountOrAgent);
192
+ if (directRole !== "admin" && directRole !== "manager") {
193
+ return;
194
+ }
195
+
196
+ const readKeyId = group.get("readKey");
197
+ if (!readKeyId) {
198
+ return;
199
+ }
200
+
201
+ const readKeySecret = group.getReadKey(readKeyId);
202
+ if (!readKeySecret) {
203
+ return;
204
+ }
205
+
206
+ const groupSealer =
207
+ group.core.node.crypto.groupSealerFromReadKey(readKeySecret);
208
+ group.set(
209
+ "groupSealer",
210
+ formatGroupSealerValue(readKeyId, groupSealer.publicKey),
211
+ "trusting",
212
+ );
213
+ }
214
+
126
215
  function needsKeyRotation(group: RawGroup) {
127
216
  const myRole = group.myRole();
128
217
 
@@ -262,7 +351,6 @@ export class RawGroup<
262
351
  if (!this.keyRevelations) {
263
352
  this.keyRevelations = new Map();
264
353
  }
265
-
266
354
  // Build caches incrementally
267
355
  for (const changeValue of transaction.changes) {
268
356
  const change = changeValue as {
@@ -324,6 +412,7 @@ export class RawGroup<
324
412
  const runMigrations = () => {
325
413
  // rotateReadKeyIfNeeded(this);
326
414
  healMissingKeyForEveryone(this);
415
+ healMissingGroupSealer(this);
327
416
  };
328
417
 
329
418
  // We need the group and their parents to be completely downloaded to correctly handle the migrations
@@ -337,6 +426,13 @@ export class RawGroup<
337
426
  }
338
427
  }
339
428
 
429
+ /**
430
+ * Optional display name set at group creation. Immutable; stored in plaintext in header meta.
431
+ */
432
+ get name(): string | undefined {
433
+ return (this.headerMeta as { name?: string } | null)?.name;
434
+ }
435
+
340
436
  /**
341
437
  * Returns the current role of a given account.
342
438
  *
@@ -885,6 +981,69 @@ export class RawGroup<
885
981
  }
886
982
  }
887
983
  }
984
+
985
+ // Try to find revelation via parent group sealer (anonymous box)
986
+ const parentContent = expectGroup(parentGroup.getCurrentContent());
987
+ const secret = this.tryDecryptWithGroupSealer(keyID, parentContent);
988
+ if (secret) {
989
+ return secret;
990
+ }
991
+ }
992
+
993
+ return undefined;
994
+ }
995
+
996
+ /**
997
+ * Try to decrypt a key that was revealed via a parent group's sealer.
998
+ * Walks the parent group's groupSealer history backwards (newest first)
999
+ * and tries to unseal the key revelation for each historical sealer value.
1000
+ *
1001
+ * New format groupSealer values embed the readKeyID directly (e.g., "key_z..._sealer_z...")
1002
+ * so we can deterministically find the correct readKey without time-based correlation.
1003
+ * Legacy format values (just "sealer_z...") fall back to time-based readKey lookup.
1004
+ */
1005
+ private tryDecryptWithGroupSealer(
1006
+ keyID: KeyID,
1007
+ parentGroup: RawGroup,
1008
+ ): KeySecret | undefined {
1009
+ const sealerEntries = parentGroup.ops["groupSealer"];
1010
+ if (!sealerEntries) return undefined;
1011
+
1012
+ // Iterate backwards (newest sealer first) to try the most recent one first
1013
+ for (let i = sealerEntries.length - 1; i >= 0; i--) {
1014
+ const sealerEntry = sealerEntries[i]!;
1015
+ if (sealerEntry.change.op !== "set") continue;
1016
+ const groupSealerValue = sealerEntry.change.value as string | undefined;
1017
+ if (!groupSealerValue) continue;
1018
+
1019
+ // Extract the SealerID (handles both new composite and legacy formats)
1020
+ const sealerID = extractSealerID(groupSealerValue);
1021
+
1022
+ const sealedKeyEdit = this.lastEditAt(`${keyID}_sealedFor_${sealerID}`);
1023
+ if (!sealedKeyEdit?.value) continue;
1024
+
1025
+ // Try to get the readKeyID directly from the composite value (new format)
1026
+ const readKeyID = extractReadKeyID(groupSealerValue);
1027
+ if (!readKeyID) continue;
1028
+
1029
+ const readKeySecret = parentGroup.getReadKey(readKeyID);
1030
+ if (!readKeySecret) continue;
1031
+
1032
+ const { secret: sealerSecret } =
1033
+ this.crypto.groupSealerFromReadKey(readKeySecret);
1034
+
1035
+ const secret = this.crypto.unsealForGroup(
1036
+ sealedKeyEdit.value as SealedForGroup<KeySecret>,
1037
+ sealerSecret,
1038
+ {
1039
+ in: this.id,
1040
+ tx: sealedKeyEdit.tx,
1041
+ },
1042
+ );
1043
+
1044
+ if (secret) {
1045
+ return secret;
1046
+ }
888
1047
  }
889
1048
 
890
1049
  return undefined;
@@ -1032,6 +1191,18 @@ export class RawGroup<
1032
1191
 
1033
1192
  this.set("readKey", newReadKey.id, "trusting");
1034
1193
 
1194
+ // Update the group sealer (derived deterministically from the new read key)
1195
+ // Store composite value with readKeyID to prevent race conditions between
1196
+ // concurrent key rotations and groupSealer migrations
1197
+ const newGroupSealer = this.crypto.groupSealerFromReadKey(
1198
+ newReadKey.secret,
1199
+ );
1200
+ this.set(
1201
+ "groupSealer",
1202
+ formatGroupSealerValue(newReadKey.id, newGroupSealer.publicKey),
1203
+ "trusting",
1204
+ );
1205
+
1035
1206
  /**
1036
1207
  * The new read key needs to be revealed to the parent groups
1037
1208
  *
@@ -1098,6 +1269,26 @@ export class RawGroup<
1098
1269
  };
1099
1270
  }
1100
1271
 
1272
+ /**
1273
+ * Get the group sealer secret by deriving it from the associated read key.
1274
+ * Uses the readKeyID embedded in the composite groupSealer value (new format),
1275
+ * or falls back to the current read key (legacy format).
1276
+ * Returns undefined if we don't have access to the read key.
1277
+ */
1278
+ getGroupSealerSecret(): SealerSecret | undefined {
1279
+ const groupSealerValue = this.get("groupSealer");
1280
+ if (!groupSealerValue) return undefined;
1281
+
1282
+ const readKeyID = extractReadKeyID(groupSealerValue as string);
1283
+ const readKeySecret = readKeyID
1284
+ ? this.getReadKey(readKeyID)
1285
+ : this.getCurrentReadKey().secret;
1286
+
1287
+ if (!readKeySecret) return undefined;
1288
+
1289
+ return this.crypto.groupSealerFromReadKey(readKeySecret).secret;
1290
+ }
1291
+
1101
1292
  extend(
1102
1293
  parent: RawGroup,
1103
1294
  role: "reader" | "writer" | "manager" | "admin" | "inherit" = "inherit",
@@ -1136,17 +1327,50 @@ export class RawGroup<
1136
1327
  readKeySecret: KeySecret,
1137
1328
  { revealAllWriteOnlyKeys }: { revealAllWriteOnlyKeys: boolean },
1138
1329
  ) {
1139
- let writeOnlyKeyID: KeyID | undefined;
1330
+ const parentGroupSealerValue = parent.get("groupSealer");
1140
1331
 
1332
+ // If we're not a member of the parent group, we need to use an alternative mechanism
1141
1333
  if (!isAccountRole(parent.myRole())) {
1142
- // Create a writeOnly key in the parent group to be able to reveal the current child key to the parent group
1143
- writeOnlyKeyID = parent.internalCreateWriteOnlyKeyForMember(
1144
- this.core.node.getCurrentAgent().id,
1145
- this.core.node.getCurrentAgent().currentAgentID(),
1146
- );
1334
+ if (parentGroupSealerValue) {
1335
+ // Extract the pure SealerID from the composite value (or legacy format)
1336
+ const parentSealerID = extractSealerID(
1337
+ parentGroupSealerValue as string,
1338
+ );
1339
+
1340
+ // NEW PATH: Use group sealer (anonymous box) instead of writeOnly key
1341
+ this.storeKeyRevelationForGroupSealer(
1342
+ parentSealerID,
1343
+ readKeyId,
1344
+ readKeySecret,
1345
+ );
1346
+
1347
+ // Also reveal all writeOnly keys if requested
1348
+ if (revealAllWriteOnlyKeys) {
1349
+ for (const keyID of this.getWriteOnlyKeys()) {
1350
+ const secret = this.core.getReadKey(keyID);
1351
+ if (!secret) {
1352
+ logger.error("Can't find key " + keyID);
1353
+ continue;
1354
+ }
1355
+ this.storeKeyRevelationForGroupSealer(
1356
+ parentSealerID,
1357
+ keyID,
1358
+ secret,
1359
+ );
1360
+ }
1361
+ }
1362
+ return;
1363
+ } else {
1364
+ // LEGACY FALLBACK: Create a writeOnly key in the parent group
1365
+ parent.internalCreateWriteOnlyKeyForMember(
1366
+ this.core.node.getCurrentAgent().id,
1367
+ this.core.node.getCurrentAgent().currentAgentID(),
1368
+ );
1369
+ }
1147
1370
  }
1148
1371
 
1149
- let { id: parentReadKeyID, secret: parentReadKeySecret } =
1372
+ // Standard path: we have access to the parent's read key
1373
+ const { id: parentReadKeyID, secret: parentReadKeySecret } =
1150
1374
  parent.getCurrentReadKey();
1151
1375
 
1152
1376
  if (!parentReadKeySecret) {
@@ -1162,11 +1386,6 @@ export class RawGroup<
1162
1386
 
1163
1387
  if (revealAllWriteOnlyKeys) {
1164
1388
  for (const keyID of this.getWriteOnlyKeys()) {
1165
- // If there's a new writeOnly key, it's already been revealed
1166
- if (keyID === writeOnlyKeyID) {
1167
- continue;
1168
- }
1169
-
1170
1389
  const secret = this.core.getReadKey(keyID);
1171
1390
 
1172
1391
  if (!secret) {
@@ -1184,6 +1403,29 @@ export class RawGroup<
1184
1403
  }
1185
1404
  }
1186
1405
 
1406
+ /**
1407
+ * Store a key revelation encrypted to a parent group's sealer (anonymous box).
1408
+ * Used when extending a child group to a parent group we don't have access to.
1409
+ */
1410
+ private storeKeyRevelationForGroupSealer(
1411
+ groupSealer: SealerID,
1412
+ childKeyID: KeyID,
1413
+ childKeySecret: KeySecret,
1414
+ ) {
1415
+ this.set(
1416
+ `${childKeyID}_sealedFor_${groupSealer}`,
1417
+ this.crypto.sealForGroup({
1418
+ message: childKeySecret,
1419
+ to: groupSealer,
1420
+ nOnceMaterial: {
1421
+ in: this.id,
1422
+ tx: this.core.nextTransactionID(),
1423
+ },
1424
+ }),
1425
+ "trusting",
1426
+ );
1427
+ }
1428
+
1187
1429
  revokeExtend(parent: RawGroup) {
1188
1430
  if (this.myRole() !== "admin") {
1189
1431
  throw new Error(
@@ -1268,6 +1510,7 @@ export class RawGroup<
1268
1510
  meta?: M["headerMeta"],
1269
1511
  initPrivacy: "trusting" | "private" = "private",
1270
1512
  uniqueness: CoValueUniqueness = this.crypto.createdNowUnique(),
1513
+ initMeta?: JsonObject,
1271
1514
  ): M {
1272
1515
  const map = this.core.node
1273
1516
  .createCoValue({
@@ -1285,10 +1528,10 @@ export class RawGroup<
1285
1528
  .getCurrentContent() as M;
1286
1529
 
1287
1530
  if (init) {
1288
- map.assign(init, initPrivacy);
1531
+ map.assign(init, initPrivacy, initMeta);
1289
1532
  } else if (!uniqueness.createdAt) {
1290
1533
  // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1291
- map.core.makeTransaction([], "trusting");
1534
+ map.core.makeTransaction([], "trusting", initMeta);
1292
1535
  }
1293
1536
 
1294
1537
  return map;
@@ -1305,6 +1548,7 @@ export class RawGroup<
1305
1548
  meta?: L["headerMeta"],
1306
1549
  initPrivacy: "trusting" | "private" = "private",
1307
1550
  uniqueness: CoValueUniqueness = this.crypto.createdNowUnique(),
1551
+ initMeta?: JsonObject,
1308
1552
  ): L {
1309
1553
  const list = this.core.node
1310
1554
  .createCoValue({
@@ -1322,10 +1566,10 @@ export class RawGroup<
1322
1566
  .getCurrentContent() as L;
1323
1567
 
1324
1568
  if (init?.length) {
1325
- list.appendItems(init, undefined, initPrivacy);
1569
+ list.appendItems(init, undefined, initPrivacy, initMeta);
1326
1570
  } else if (!uniqueness.createdAt) {
1327
1571
  // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1328
- list.core.makeTransaction([], "trusting");
1572
+ list.core.makeTransaction([], "trusting", initMeta);
1329
1573
  }
1330
1574
 
1331
1575
  return list;
@@ -1363,8 +1607,11 @@ export class RawGroup<
1363
1607
 
1364
1608
  /** @category 3. Value creation */
1365
1609
  createStream<C extends RawCoStream>(
1610
+ init?: JsonValue[],
1611
+ initPrivacy: "trusting" | "private" = "private",
1366
1612
  meta?: C["headerMeta"],
1367
1613
  uniqueness: CoValueUniqueness = this.crypto.createdNowUnique(),
1614
+ initMeta?: JsonObject,
1368
1615
  ): C {
1369
1616
  const stream = this.core.node
1370
1617
  .createCoValue({
@@ -1381,9 +1628,11 @@ export class RawGroup<
1381
1628
  })
1382
1629
  .getCurrentContent() as C;
1383
1630
 
1384
- if (!uniqueness.createdAt) {
1631
+ if (init?.length) {
1632
+ stream.core.makeTransaction(init, initPrivacy, initMeta);
1633
+ } else if (!uniqueness.createdAt) {
1385
1634
  // If the createdAt is not set, we need to make a trusting transaction to set the createdAt
1386
- stream.core.makeTransaction([], "trusting");
1635
+ stream.core.makeTransaction([], "trusting", initMeta);
1387
1636
  }
1388
1637
 
1389
1638
  return stream;
@@ -4,7 +4,8 @@ import { RawAccount } from "./coValues/account.js";
4
4
  import { RawCoList } from "./coValues/coList.js";
5
5
  import { RawCoMap } from "./coValues/coMap.js";
6
6
  import { RawCoPlainText } from "./coValues/coPlainText.js";
7
- import { RawBinaryCoStream, RawCoStream } from "./coValues/coStream.js";
7
+ import { RawBinaryCoStream } from "./coValues/binaryCoStream.js";
8
+ import { RawCoStream } from "./coValues/coStream.js";
8
9
  import { RawGroup } from "./coValues/group.js";
9
10
 
10
11
  export function coreToCoValue(