agent-comms 9.0.11 → 9.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +12 -6
  4. package/dist/account-cli.d.ts.map +1 -1
  5. package/dist/account-cli.js +4 -1
  6. package/dist/account-cli.js.map +1 -1
  7. package/dist/bridges/user/web/dist/bundle.js +1 -1
  8. package/dist/bridges/user/web/dist/index.html +1 -7
  9. package/dist/bridges/user/web/dist/mesh-worker.js +4 -4
  10. package/dist/bridges/user/web/dist/sw.js +1 -1
  11. package/dist/bridges/user/web/hosted-dashboard.d.ts +10 -0
  12. package/dist/bridges/user/web/hosted-dashboard.d.ts.map +1 -0
  13. package/dist/bridges/user/web/hosted-dashboard.js +10 -0
  14. package/dist/bridges/user/web/hosted-dashboard.js.map +1 -0
  15. package/dist/bridges/user/web/web-access.d.ts +0 -2
  16. package/dist/bridges/user/web/web-access.d.ts.map +1 -1
  17. package/dist/bridges/user/web/web-access.js +1 -2
  18. package/dist/bridges/user/web/web-access.js.map +1 -1
  19. package/dist/cli-main.d.ts.map +1 -1
  20. package/dist/cli-main.js +9 -12
  21. package/dist/cli-main.js.map +1 -1
  22. package/dist/core/account-bundle.d.ts +1 -1
  23. package/dist/core/account-bundle.d.ts.map +1 -1
  24. package/dist/core/account-bundle.js +26 -19
  25. package/dist/core/account-bundle.js.map +1 -1
  26. package/dist/core/account-join.d.ts.map +1 -1
  27. package/dist/core/account-join.js +5 -2
  28. package/dist/core/account-join.js.map +1 -1
  29. package/dist/core/account-ledger-crypto.d.ts.map +1 -1
  30. package/dist/core/account-ledger-crypto.js +4 -1
  31. package/dist/core/account-ledger-crypto.js.map +1 -1
  32. package/dist/core/account-ledger-store.d.ts +3 -1
  33. package/dist/core/account-ledger-store.d.ts.map +1 -1
  34. package/dist/core/account-ledger-store.js +6 -3
  35. package/dist/core/account-ledger-store.js.map +1 -1
  36. package/dist/core/account-ledger.d.ts +51 -4
  37. package/dist/core/account-ledger.d.ts.map +1 -1
  38. package/dist/core/account-ledger.js +102 -15
  39. package/dist/core/account-ledger.js.map +1 -1
  40. package/dist/core/bridge-mesh.d.ts.map +1 -1
  41. package/dist/core/bridge-mesh.js +3 -0
  42. package/dist/core/bridge-mesh.js.map +1 -1
  43. package/dist/core/bridge.d.ts.map +1 -1
  44. package/dist/core/bridge.js +16 -4
  45. package/dist/core/bridge.js.map +1 -1
  46. package/dist/core/capability-ask.d.ts.map +1 -1
  47. package/dist/core/capability-ask.js +10 -2
  48. package/dist/core/capability-ask.js.map +1 -1
  49. package/dist/core/connection-code-tool.d.ts +1 -1
  50. package/dist/core/connection-code-tool.js +2 -2
  51. package/dist/core/connection-code-tool.js.map +1 -1
  52. package/dist/core/connection-code.d.ts +5 -5
  53. package/dist/core/connection-code.d.ts.map +1 -1
  54. package/dist/core/connection-code.js +22 -6
  55. package/dist/core/connection-code.js.map +1 -1
  56. package/dist/core/device-membership.d.ts +8 -3
  57. package/dist/core/device-membership.d.ts.map +1 -1
  58. package/dist/core/device-membership.js +4 -4
  59. package/dist/core/device-membership.js.map +1 -1
  60. package/dist/core/dm-grant-actions.d.ts.map +1 -1
  61. package/dist/core/dm-grant-actions.js +7 -1
  62. package/dist/core/dm-grant-actions.js.map +1 -1
  63. package/dist/core/error-reporting.d.ts.map +1 -1
  64. package/dist/core/error-reporting.js +5 -0
  65. package/dist/core/error-reporting.js.map +1 -1
  66. package/dist/core/gateway-trust.d.ts +1 -1
  67. package/dist/core/gateway-trust.d.ts.map +1 -1
  68. package/dist/core/gateway-trust.js +4 -4
  69. package/dist/core/gateway-trust.js.map +1 -1
  70. package/dist/core/group-proofs.d.ts.map +1 -1
  71. package/dist/core/group-proofs.js +2 -2
  72. package/dist/core/group-proofs.js.map +1 -1
  73. package/dist/core/identity-store.d.ts +13 -9
  74. package/dist/core/identity-store.d.ts.map +1 -1
  75. package/dist/core/identity-store.js +122 -147
  76. package/dist/core/identity-store.js.map +1 -1
  77. package/dist/core/index.d.ts +1 -0
  78. package/dist/core/index.d.ts.map +1 -1
  79. package/dist/core/index.js +1 -0
  80. package/dist/core/index.js.map +1 -1
  81. package/dist/core/iso-timestamp.d.ts +10 -0
  82. package/dist/core/iso-timestamp.d.ts.map +1 -0
  83. package/dist/core/iso-timestamp.js +19 -0
  84. package/dist/core/iso-timestamp.js.map +1 -0
  85. package/dist/core/issuer-identity-file.d.ts +15 -0
  86. package/dist/core/issuer-identity-file.d.ts.map +1 -1
  87. package/dist/core/issuer-identity-file.js +69 -20
  88. package/dist/core/issuer-identity-file.js.map +1 -1
  89. package/dist/core/machine-identity.d.ts +1 -1
  90. package/dist/core/machine-identity.d.ts.map +1 -1
  91. package/dist/core/machine-identity.js +14 -11
  92. package/dist/core/machine-identity.js.map +1 -1
  93. package/dist/core/mesh-store.d.ts +2 -2
  94. package/dist/core/mesh-store.d.ts.map +1 -1
  95. package/dist/core/mesh-store.js.map +1 -1
  96. package/dist/core/peer-lifecycle.d.ts.map +1 -1
  97. package/dist/core/peer-lifecycle.js +8 -2
  98. package/dist/core/peer-lifecycle.js.map +1 -1
  99. package/dist/core/pid-lock-file.d.ts +27 -4
  100. package/dist/core/pid-lock-file.d.ts.map +1 -1
  101. package/dist/core/pid-lock-file.js +120 -11
  102. package/dist/core/pid-lock-file.js.map +1 -1
  103. package/dist/core/process-start.d.ts +10 -0
  104. package/dist/core/process-start.d.ts.map +1 -0
  105. package/dist/core/process-start.js +43 -0
  106. package/dist/core/process-start.js.map +1 -0
  107. package/dist/core/room-lifecycle.d.ts +3 -2
  108. package/dist/core/room-lifecycle.d.ts.map +1 -1
  109. package/dist/core/room-lifecycle.js +9 -8
  110. package/dist/core/room-lifecycle.js.map +1 -1
  111. package/dist/core/room-protocol.d.ts.map +1 -1
  112. package/dist/core/room-protocol.js +25 -3
  113. package/dist/core/room-protocol.js.map +1 -1
  114. package/dist/core/stale-agent-checker.d.ts.map +1 -1
  115. package/dist/core/stale-agent-checker.js +3 -1
  116. package/dist/core/stale-agent-checker.js.map +1 -1
  117. package/dist/core/store.d.ts +3 -0
  118. package/dist/core/store.d.ts.map +1 -1
  119. package/dist/core/store.js +42 -20
  120. package/dist/core/store.js.map +1 -1
  121. package/dist/core/tool.d.ts +5 -1
  122. package/dist/core/tool.d.ts.map +1 -1
  123. package/dist/core/tool.js +7 -1
  124. package/dist/core/tool.js.map +1 -1
  125. package/dist/core/types.d.ts +1 -1
  126. package/dist/core/types.d.ts.map +1 -1
  127. package/dist/core/types.js +8 -3
  128. package/dist/core/types.js.map +1 -1
  129. package/dist/core/user-identity.d.ts +5 -5
  130. package/dist/core/user-identity.d.ts.map +1 -1
  131. package/dist/core/user-identity.js +59 -45
  132. package/dist/core/user-identity.js.map +1 -1
  133. package/package.json +1 -1
  134. package/src/account-cli.ts +4 -1
  135. package/src/bridges/user/web/dist/bundle.js +1 -1
  136. package/src/bridges/user/web/dist/index.html +1 -7
  137. package/src/bridges/user/web/dist/mesh-worker.js +4 -4
  138. package/src/bridges/user/web/dist/sw.js +1 -1
  139. package/src/bridges/user/web/hosted-dashboard.ts +11 -0
  140. package/src/bridges/user/web/web-access.ts +1 -3
  141. package/src/cli-main.ts +10 -12
  142. package/src/core/account-bundle.ts +36 -25
  143. package/src/core/account-join.ts +5 -2
  144. package/src/core/account-ledger-crypto.ts +4 -1
  145. package/src/core/account-ledger-store.ts +11 -3
  146. package/src/core/account-ledger.ts +142 -20
  147. package/src/core/bridge-mesh.ts +4 -0
  148. package/src/core/bridge.ts +24 -4
  149. package/src/core/capability-ask.ts +16 -1
  150. package/src/core/connection-code-tool.ts +2 -2
  151. package/src/core/connection-code.ts +34 -8
  152. package/src/core/device-membership.ts +12 -8
  153. package/src/core/dm-grant-actions.ts +7 -1
  154. package/src/core/error-reporting.ts +5 -0
  155. package/src/core/gateway-trust.ts +4 -4
  156. package/src/core/group-proofs.ts +5 -2
  157. package/src/core/identity-store.ts +182 -191
  158. package/src/core/index.ts +1 -0
  159. package/src/core/iso-timestamp.ts +21 -0
  160. package/src/core/issuer-identity-file.ts +87 -23
  161. package/src/core/machine-identity.ts +15 -12
  162. package/src/core/mesh-store.ts +2 -1
  163. package/src/core/peer-lifecycle.ts +9 -4
  164. package/src/core/pid-lock-file.ts +139 -11
  165. package/src/core/process-start.ts +45 -0
  166. package/src/core/room-lifecycle.ts +12 -9
  167. package/src/core/room-protocol.ts +23 -3
  168. package/src/core/stale-agent-checker.ts +3 -1
  169. package/src/core/store.ts +48 -23
  170. package/src/core/tool.ts +14 -1
  171. package/src/core/types.ts +9 -3
  172. package/src/core/user-identity.ts +65 -51
package/src/core/store.ts CHANGED
@@ -73,6 +73,16 @@ export function requireDisplayName(raw: string, what: string): string {
73
73
  return name;
74
74
  }
75
75
 
76
+ /** The delivery files already reported by this process, each with the reason given, so a file that stays where it is is reported once and not on every drain. */
77
+ const reportedDeliveryFiles = new Set<string>();
78
+
79
+ function reportDeliveryFileOnce(filePath: string, problem: string): void {
80
+ const key = `${filePath}\n${problem}`;
81
+ if (reportedDeliveryFiles.has(key)) return;
82
+ reportedDeliveryFiles.add(key);
83
+ process.stderr.write(`agent-comms: ${filePath} ${problem}\n`);
84
+ }
85
+
76
86
  /** An agent's registered name is its self-asserted name (display-name.ts), so it is held to the same rules as it is registered or changed: requireDisplayName for "An agent name". */
77
87
  export function requireAgentName(raw: string): string {
78
88
  return requireDisplayName(raw, "An agent name");
@@ -642,35 +652,50 @@ export class FileStore implements CommsStore {
642
652
  await this.writeJsonFile(path.join(dir, `${id}.json`), event);
643
653
  }
644
654
 
655
+ /**
656
+ * Returns the events queued for agentId, oldest message first, and removes each from the queue as it is returned. A file that cannot be read or parsed is left in place and reported on stderr with its name, and every other file is still drained, so one bad file never costs the events around it; a file that cannot be removed is reported the same way and its event is not returned, so it is delivered once, on a later drain, not twice. Each file is reported once per process however often the queue is drained, since a file that stays in place would otherwise be reported on every drain.
657
+ */
645
658
  async drainDelivery(agentId: string): Promise<DeliveryEvent[]> {
646
659
  const dir = this.deliveryDir(agentId);
660
+ let files: string[];
647
661
  try {
648
- const files = await fs.readdir(dir);
649
- const events: DeliveryEvent[] = [];
650
- for (const file of files) {
651
- if (!file.endsWith(".json")) continue;
652
- const event = DeliveryEventSchema.parse(
653
- await this.readJsonFile(path.join(dir, file)),
654
- );
655
- events.push(event);
656
- await fs.unlink(path.join(dir, file));
657
- }
658
- // Same fixed timestamp form as readRoomMessages sorts on.
659
- events.sort((a, b) => {
660
- const ta =
661
- a.type === "room_message" || a.type === "dm"
662
- ? a.message.timestamp
663
- : "";
664
- const tb =
665
- b.type === "room_message" || b.type === "dm"
666
- ? b.message.timestamp
667
- : "";
668
- return compareCodeUnits(ta, tb);
669
- });
670
- return events;
662
+ files = await fs.readdir(dir);
671
663
  } catch {
672
664
  return [];
673
665
  }
666
+ const events: DeliveryEvent[] = [];
667
+ for (const file of files) {
668
+ if (!file.endsWith(".json")) continue;
669
+ const filePath = path.join(dir, file);
670
+ let event: DeliveryEvent;
671
+ try {
672
+ event = DeliveryEventSchema.parse(await this.readJsonFile(filePath));
673
+ } catch {
674
+ reportDeliveryFileOnce(
675
+ filePath,
676
+ "is not a readable delivery event; it was left in place",
677
+ );
678
+ continue;
679
+ }
680
+ try {
681
+ await fs.unlink(filePath);
682
+ } catch {
683
+ reportDeliveryFileOnce(
684
+ filePath,
685
+ "could not be removed; its event was not returned",
686
+ );
687
+ continue;
688
+ }
689
+ events.push(event);
690
+ }
691
+ events.sort((a, b) => {
692
+ const ta =
693
+ a.type === "room_message" || a.type === "dm" ? a.message.timestamp : "";
694
+ const tb =
695
+ b.type === "room_message" || b.type === "dm" ? b.message.timestamp : "";
696
+ return compareCodeUnits(ta, tb);
697
+ });
698
+ return events;
674
699
  }
675
700
 
676
701
  private async deliverToMembers(
package/src/core/tool.ts CHANGED
@@ -36,6 +36,7 @@ import type { CommsStore } from "./comms-store.js";
36
36
  import type { CapabilityToken } from "wire-mesh-core/generated/protocol";
37
37
  import type { DiscoveryManager } from "./discovery.js";
38
38
  import { CommsError } from "./store.js";
39
+ import type { LedgerGaps } from "./account-ledger.js";
39
40
  import { getOwnPackageVersion } from "./package-version.js";
40
41
  import { describeDeliveryFor } from "./send-outcome.js";
41
42
  import {
@@ -178,7 +179,8 @@ export interface MeshOnlyFeatures {
178
179
  removeTrustedGatewayMachine?: (deviceHex: string) => void;
179
180
  listTrustedGatewayMachines?: () => string[];
180
181
  /** Withdraws the dm:send grant issued for bearerId. */
181
- revokeAgentDmAccess?: (bearerId: string) => Promise<void>;
182
+ revokeAgentDmAccess?: (bearerId: string) => Promise<LedgerGaps>;
183
+
182
184
  removeTrustedGatewayPrincipal?: (deviceHex: string) => void;
183
185
  listTrustedGatewayPrincipals?: () => string[];
184
186
  /** Devices trusted only because a trusted principal or machine vouches for them (a verified membership proof), with that issuer. */
@@ -248,6 +250,8 @@ export interface CommsToolOptions {
248
250
  readonly naming?: Naming;
249
251
  /** Returns the newest npm release known to be available, or undefined when none is known (no checker wired up, no successful check yet, or this bridge is already current). Wired at bridge construction time by a VersionDriftChecker (see version-check.ts) -- optional so every call site, and every test that has no interest in drift reporting, is unaffected. */
250
252
  readonly getNewerVersionIfAny?: () => string | undefined;
253
+ /** How many stored account ledger entries cannot be read, for whoami; omitted when the store has no account ledger. */
254
+ readonly getUnreadableLedgerEntries?: () => number;
251
255
  /** Fetches a signer's armored PGP public key by fingerprint, used by gateway_redeem_connection_code when the caller supplies a fingerprint but not the key text itself. Defaults to the real keys.openpgp.org lookup (pgp-keyserver.ts); a test that would otherwise trigger real network I/O injects a fake resolver instead, the same reasoning bridge-mesh.ts's own fetchLatestVersion override already establishes for getNewerVersionIfAny above. */
252
256
  readonly fetchPgpPublicKeyByFingerprintImpl?: (
253
257
  fingerprint: string,
@@ -261,6 +265,7 @@ export class CommsTool {
261
265
  private readonly discovery: DiscoveryManager | undefined;
262
266
  private readonly naming: Naming | undefined;
263
267
  private readonly getNewerVersionIfAny: (() => string | undefined) | undefined;
268
+ private readonly getUnreadableLedgerEntries: (() => number) | undefined;
264
269
  private readonly fetchPgpPublicKeyByFingerprintImpl: (
265
270
  fingerprint: string,
266
271
  ) => Promise<string>;
@@ -273,11 +278,13 @@ export class CommsTool {
273
278
  discovery,
274
279
  naming,
275
280
  getNewerVersionIfAny,
281
+ getUnreadableLedgerEntries,
276
282
  fetchPgpPublicKeyByFingerprintImpl = fetchPgpPublicKeyByFingerprint,
277
283
  } = options ?? {};
278
284
  this.discovery = discovery;
279
285
  this.naming = naming;
280
286
  this.getNewerVersionIfAny = getNewerVersionIfAny;
287
+ this.getUnreadableLedgerEntries = getUnreadableLedgerEntries;
281
288
  this.fetchPgpPublicKeyByFingerprintImpl =
282
289
  fetchPgpPublicKeyByFingerprintImpl;
283
290
  }
@@ -456,6 +463,12 @@ export class CommsTool {
456
463
  const lines = [
457
464
  `Updated: name=${agent.name}, visibility=${agent.visibility}, status=${agent.status}, version=${getOwnPackageVersion()}${formatSelfVersionSuffix(this.store.getCcPeerVersion)}`,
458
465
  ];
466
+ const unreadableLedger = this.getUnreadableLedgerEntries?.() ?? 0;
467
+ if (unreadableLedger > 0) {
468
+ lines.push(
469
+ `Account ledger: ${String(unreadableLedger)} unreadable entries; a grant or revocation recorded only in them is not applied here until they are repaired or replaced from another machine's replica`,
470
+ );
471
+ }
459
472
  const updateAvailable = this.formatUpdateAvailableLine();
460
473
  if (updateAvailable !== undefined) lines.push(updateAvailable);
461
474
  return {
package/src/core/types.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { z } from "zod";
10
+ import { isIsoTimestamp } from "./iso-timestamp.js";
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Schema-attached type guard helper
@@ -312,11 +313,16 @@ export interface NetworkInterface {
312
313
  // ConnectionCode
313
314
  // ---------------------------------------------------------------------------
314
315
 
315
- /** A single-use, short-lived artifact bootstrapping GatewayTrust between two devices that have never established a mesh connection (agent-comms#188): `code`/`expiresAt` answer freshness/liveness and are always checked; `signature` is an optional detached PGP signature (armored) over `${code}:${expiresAt}:${deviceId}`, checked only when present, never required -- a device with no PGP identity still generates and redeems a bare code. */
316
+ /** An ISO 8601 timestamp (iso-timestamp.ts): text that names no instant is refused where it enters, so it can never be compared against the clock as if it were one. */
317
+ const IsoTimestampSchema = z
318
+ .string()
319
+ .refine(isIsoTimestamp, "must be an ISO 8601 timestamp");
320
+
321
+ /** A single-use, short-lived artifact bootstrapping GatewayTrust between two devices that have never established a mesh connection (agent-comms#188): `code`/`expiresAt` answer freshness/liveness and are always checked, and `expiresAt` must be an ISO 8601 timestamp; `signature` is an optional detached PGP signature (armored) over `${code}:${expiresAt}:${deviceId}`. An unsigned code is a bearer hint, not authentication of whoever issued it: redeeming one trusts whatever device-id it names. A redeemer who supplies a public key or fingerprint to check it against requires a signature, and the code is refused without one. */
316
322
  export const ConnectionCodeSchema = defineSchema(
317
323
  z.object({
318
324
  code: z.string(),
319
- expiresAt: z.string(),
325
+ expiresAt: IsoTimestampSchema,
320
326
  deviceId: z.string(),
321
327
  signature: z.string().optional(),
322
328
  }),
@@ -529,7 +535,7 @@ export const CommsActionSchema = defineSchema(
529
535
  z.object({
530
536
  action: z.literal("gateway_redeem_connection_code"),
531
537
  code: z.string(),
532
- expiresAt: z.string(),
538
+ expiresAt: IsoTimestampSchema,
533
539
  device: z.string(),
534
540
  signature: z.string().optional(),
535
541
  publicKey: z.string().optional(),
@@ -7,6 +7,7 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { resolveDataDir } from "./data-directory.js";
10
+ import { withWriteLockAsync } from "./pid-lock-file.js";
10
11
  import type { AccountContents } from "./account-bundle.js";
11
12
  import { certifyKeyPair } from "./identity.js";
12
13
  import type { PeerIdentity } from "./identity.js";
@@ -105,21 +106,28 @@ function requireStoredUserIdentity(file: string): StoredUserIdentity {
105
106
  }
106
107
 
107
108
  /** This machine's writer nonce for the account ledger, generated and persisted on first use. Two processes racing to create it can each end up with a different one; that is harmless, since each writes its own writer log and the ledger is the union of every writer log the account key recognises. What must never happen is two machines sharing one, which is why it is not part of an exported account. */
108
- export function loadOrCreateLedgerWriterNonce(
109
+ export async function loadOrCreateLedgerWriterNonce(
109
110
  options: Readonly<UserIdentityOptions> | undefined,
110
111
  generate: () => Uint8Array,
111
- ): Uint8Array<ArrayBuffer> {
112
+ ): Promise<Uint8Array<ArrayBuffer>> {
112
113
  const file = userIdentityFile(options);
113
- const stored = requireStoredUserIdentity(file);
114
- if (stored.ledgerWriter !== undefined) {
115
- return Uint8Array.from(Buffer.from(stored.ledgerWriter, "base64"));
114
+ const existing = requireStoredUserIdentity(file).ledgerWriter;
115
+ if (existing !== undefined) {
116
+ return Uint8Array.from(Buffer.from(existing, "base64"));
116
117
  }
117
- const nonce = Uint8Array.from(generate());
118
- writeStoredUserIdentity(file, {
119
- ...stored,
120
- ledgerWriter: Buffer.from(nonce).toString("base64"),
118
+ // Read again under the lock: another process may have stored one meanwhile, and two would each keep its own, forking the writer log.
119
+ return withWriteLockAsync(file, () => {
120
+ const stored = requireStoredUserIdentity(file);
121
+ if (stored.ledgerWriter !== undefined) {
122
+ return Uint8Array.from(Buffer.from(stored.ledgerWriter, "base64"));
123
+ }
124
+ const nonce = Uint8Array.from(generate());
125
+ writeStoredUserIdentity(file, {
126
+ ...stored,
127
+ ledgerWriter: Buffer.from(nonce).toString("base64"),
128
+ });
129
+ return nonce;
121
130
  });
122
- return nonce;
123
131
  }
124
132
 
125
133
  /** One grant a build before the replicated ledger recorded in user-identity.json. */
@@ -172,19 +180,21 @@ function withoutMigrated(
172
180
  }
173
181
 
174
182
  /** Removes exactly the given legacy grants from user-identity.json once they are in the ledger. The file is re-read first, so a grant written meanwhile by an older build still running on this machine stays for the next migration rather than being lost. */
175
- export function clearLegacyIssuedGrants(
183
+ export async function clearLegacyIssuedGrants(
176
184
  options: Readonly<UserIdentityOptions> | undefined,
177
185
  migrated: readonly Readonly<LegacyIssuedGrant>[],
178
- ): void {
186
+ ): Promise<void> {
179
187
  const file = userIdentityFile(options);
180
- const { issuedDeviceGrants, issuedDmGrants, ...rest } =
181
- requireStoredUserIdentity(file);
182
- const device = withoutMigrated(issuedDeviceGrants, "device", migrated);
183
- const dm = withoutMigrated(issuedDmGrants, "dm", migrated);
184
- writeStoredUserIdentity(file, {
185
- ...rest,
186
- ...(device === undefined ? {} : { issuedDeviceGrants: device }),
187
- ...(dm === undefined ? {} : { issuedDmGrants: dm }),
188
+ await withWriteLockAsync(file, () => {
189
+ const { issuedDeviceGrants, issuedDmGrants, ...rest } =
190
+ requireStoredUserIdentity(file);
191
+ const device = withoutMigrated(issuedDeviceGrants, "device", migrated);
192
+ const dm = withoutMigrated(issuedDmGrants, "dm", migrated);
193
+ writeStoredUserIdentity(file, {
194
+ ...rest,
195
+ ...(device === undefined ? {} : { issuedDeviceGrants: device }),
196
+ ...(dm === undefined ? {} : { issuedDmGrants: dm }),
197
+ });
188
198
  });
189
199
  }
190
200
 
@@ -196,15 +206,17 @@ export function loadUserDisplayName(
196
206
  }
197
207
 
198
208
  /** Sets (or, given undefined, clears) the name this account asserts for itself, keeping the key material and every other field. Throws if the account has not been created yet. */
199
- export function saveUserDisplayName(
209
+ export async function saveUserDisplayName(
200
210
  options: Readonly<UserIdentityOptions> | undefined,
201
211
  displayName: string | undefined,
202
- ): void {
212
+ ): Promise<void> {
203
213
  const file = userIdentityFile(options);
204
- const next: StoredUserIdentity = { ...requireStoredUserIdentity(file) };
205
- if (displayName === undefined) delete next.displayName;
206
- else next.displayName = displayName;
207
- writeStoredUserIdentity(file, next);
214
+ await withWriteLockAsync(file, () => {
215
+ const next: StoredUserIdentity = { ...requireStoredUserIdentity(file) };
216
+ if (displayName === undefined) delete next.displayName;
217
+ else next.displayName = displayName;
218
+ writeStoredUserIdentity(file, next);
219
+ });
208
220
  }
209
221
 
210
222
  /** What an export or a join carries of the account: its PEM private key and the name it asserts for itself, if any. Throws if the principal has never been created. */
@@ -250,10 +262,10 @@ function setAside(
250
262
  /**
251
263
  * Makes this machine hold the account whose contents are given, the deliberate copy that lets one principal span machines. The key is re-certified here (the device-id, which is the account's identity, depends only on the key). The name the account asserts for itself comes with it; an import carrying none leaves the name of the account already held untouched. This machine's own ledger writer nonce is kept, since it belongs to the machine, not the account. A different account already held here is moved aside to replacedUserIdentityFile first, never overwritten (see setAside for a record already set aside there); importing the account already held changes nothing but that name. Returns the imported identity and, when one was replaced, where the old record now is.
252
264
  */
253
- export function importAccountKey(
265
+ export async function importAccountKey(
254
266
  options: Readonly<UserIdentityOptions> | undefined,
255
267
  contents: Readonly<AccountContents>,
256
- ): { identity: PeerIdentity; replacedFile?: string } {
268
+ ): Promise<{ identity: PeerIdentity; replacedFile?: string }> {
257
269
  const file = userIdentityFile(options);
258
270
  fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
259
271
  const imported = certifyKeyPair(contents.privateKey);
@@ -263,28 +275,30 @@ export function importAccountKey(
263
275
  ? {}
264
276
  : { displayName: contents.displayName }),
265
277
  });
266
- const existing = readStoredUserIdentity(file);
267
- if (existing === undefined) {
268
- writeStoredUserIdentity(file, named(issuerKeyRecord(imported)));
269
- return { identity: imported };
270
- }
271
- const current = issuerPeerIdentity(existing);
272
- if (Buffer.from(current.deviceId).equals(Buffer.from(imported.deviceId))) {
273
- if (contents.displayName !== undefined) {
274
- writeStoredUserIdentity(file, named(existing));
278
+ return withWriteLockAsync(file, () => {
279
+ const existing = readStoredUserIdentity(file);
280
+ if (existing === undefined) {
281
+ writeStoredUserIdentity(file, named(issuerKeyRecord(imported)));
282
+ return { identity: imported };
275
283
  }
276
- return { identity: current };
277
- }
278
- const replacedFile = replacedUserIdentityFile(options, current);
279
- setAside(replacedFile, existing, file);
280
- writeStoredUserIdentity(
281
- file,
282
- named({
283
- ...issuerKeyRecord(imported),
284
- ...(existing.ledgerWriter === undefined
285
- ? {}
286
- : { ledgerWriter: existing.ledgerWriter }),
287
- }),
288
- );
289
- return { identity: imported, replacedFile };
284
+ const current = issuerPeerIdentity(existing);
285
+ if (Buffer.from(current.deviceId).equals(Buffer.from(imported.deviceId))) {
286
+ if (contents.displayName !== undefined) {
287
+ writeStoredUserIdentity(file, named(existing));
288
+ }
289
+ return { identity: current };
290
+ }
291
+ const replacedFile = replacedUserIdentityFile(options, current);
292
+ setAside(replacedFile, existing, file);
293
+ writeStoredUserIdentity(
294
+ file,
295
+ named({
296
+ ...issuerKeyRecord(imported),
297
+ ...(existing.ledgerWriter === undefined
298
+ ? {}
299
+ : { ledgerWriter: existing.ledgerWriter }),
300
+ }),
301
+ );
302
+ return { identity: imported, replacedFile };
303
+ });
290
304
  }