@peers-app/peers-sdk 0.25.14 → 0.25.15

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 (74) hide show
  1. package/dist/context/user-context-identity.test.d.ts +1 -0
  2. package/dist/context/user-context-identity.test.js +220 -0
  3. package/dist/context/user-context.d.ts +5 -1
  4. package/dist/context/user-context.js +63 -8
  5. package/dist/contracts/index.d.ts +1 -1
  6. package/dist/contracts/system/identity-contract.d.ts +156 -0
  7. package/dist/contracts/system/identity-contract.js +172 -0
  8. package/dist/contracts/system/index.d.ts +1 -1
  9. package/dist/data/assistants.d.ts +4 -4
  10. package/dist/data/devices.d.ts +70 -4
  11. package/dist/data/devices.js +3 -0
  12. package/dist/data/group-members.d.ts +2 -2
  13. package/dist/data/groups.d.ts +2 -2
  14. package/dist/data/invites.d.ts +93 -25
  15. package/dist/data/persistent-vars.d.ts +8 -0
  16. package/dist/data/persistent-vars.js +16 -0
  17. package/dist/data/tool-access-approvals.d.ts +2 -2
  18. package/dist/data/user-permissions.d.ts +10 -2
  19. package/dist/data/user-permissions.js +35 -4
  20. package/dist/data/user-permissions.test.js +26 -0
  21. package/dist/data/users.d.ts +48 -4
  22. package/dist/data/users.js +61 -5
  23. package/dist/data/workflows.d.ts +1 -1
  24. package/dist/device/connection.d.ts +7 -1
  25. package/dist/device/connection.js +38 -6
  26. package/dist/device/connection.test.js +33 -4
  27. package/dist/device/device.d.ts +2 -2
  28. package/dist/device/device.js +4 -2
  29. package/dist/device/get-trust-level-fn.d.ts +1 -0
  30. package/dist/device/get-trust-level-fn.js +28 -4
  31. package/dist/device/get-trust-level-fn.test.js +95 -0
  32. package/dist/device-pairing/types.d.ts +26 -26
  33. package/dist/host-credentials.d.ts +78 -0
  34. package/dist/host-credentials.js +128 -0
  35. package/dist/host-credentials.test.d.ts +1 -0
  36. package/dist/host-credentials.test.js +88 -0
  37. package/dist/identity/account-email.d.ts +59 -0
  38. package/dist/identity/account-email.js +87 -0
  39. package/dist/identity/account-email.test.d.ts +1 -0
  40. package/dist/identity/account-email.test.js +59 -0
  41. package/dist/identity/anchor-document.d.ts +205 -0
  42. package/dist/identity/anchor-document.js +119 -0
  43. package/dist/identity/anchor-document.test.d.ts +1 -0
  44. package/dist/identity/anchor-document.test.js +16 -0
  45. package/dist/identity/anchor-resolution.d.ts +61 -0
  46. package/dist/identity/anchor-resolution.js +117 -0
  47. package/dist/identity/anchor-resolution.test.d.ts +1 -0
  48. package/dist/identity/anchor-resolution.test.js +130 -0
  49. package/dist/identity/key-change.d.ts +142 -0
  50. package/dist/identity/key-change.js +472 -0
  51. package/dist/identity/key-change.test.d.ts +1 -0
  52. package/dist/identity/key-change.test.js +811 -0
  53. package/dist/identity/key-contest.d.ts +91 -0
  54. package/dist/identity/key-contest.js +55 -0
  55. package/dist/identity/key-resolutions.d.ts +55 -0
  56. package/dist/identity/key-resolutions.js +42 -0
  57. package/dist/identity/passkey.d.ts +22 -0
  58. package/dist/identity/passkey.js +33 -0
  59. package/dist/identity/user-row-change.d.ts +45 -0
  60. package/dist/identity/user-row-change.js +81 -0
  61. package/dist/index.d.ts +9 -0
  62. package/dist/index.js +9 -0
  63. package/dist/invites/invite-messages.d.ts +512 -116
  64. package/dist/invites/invite-token.d.ts +114 -24
  65. package/dist/invites/invite-token.js +6 -0
  66. package/dist/keys.d.ts +7 -0
  67. package/dist/keys.js +20 -0
  68. package/dist/system-ids.d.ts +16 -0
  69. package/dist/system-ids.js +16 -0
  70. package/dist/types/workflow.d.ts +2 -2
  71. package/dist/utils.d.ts +10 -0
  72. package/dist/utils.js +43 -0
  73. package/dist/utils.test.js +23 -0
  74. package/package.json +1 -1
@@ -1141,16 +1141,16 @@ export declare const devicePairingTranscriptSchema: z.ZodObject<{
1141
1141
  deviceName: z.ZodOptional<z.ZodString>;
1142
1142
  }, "strip", z.ZodTypeAny, {
1143
1143
  publicKey: string;
1144
- publicBoxKey: string;
1145
1144
  userId: string;
1146
1145
  deviceId: string;
1146
+ publicBoxKey: string;
1147
1147
  userName?: string | undefined;
1148
1148
  deviceName?: string | undefined;
1149
1149
  }, {
1150
1150
  publicKey: string;
1151
- publicBoxKey: string;
1152
1151
  userId: string;
1153
1152
  deviceId: string;
1153
+ publicBoxKey: string;
1154
1154
  userName?: string | undefined;
1155
1155
  deviceName?: string | undefined;
1156
1156
  }>;
@@ -1163,16 +1163,16 @@ export declare const devicePairingTranscriptSchema: z.ZodObject<{
1163
1163
  deviceName: z.ZodOptional<z.ZodString>;
1164
1164
  }, "strip", z.ZodTypeAny, {
1165
1165
  publicKey: string;
1166
- publicBoxKey: string;
1167
1166
  userId: string;
1168
1167
  deviceId: string;
1168
+ publicBoxKey: string;
1169
1169
  userName?: string | undefined;
1170
1170
  deviceName?: string | undefined;
1171
1171
  }, {
1172
1172
  publicKey: string;
1173
- publicBoxKey: string;
1174
1173
  userId: string;
1175
1174
  deviceId: string;
1175
+ publicBoxKey: string;
1176
1176
  userName?: string | undefined;
1177
1177
  deviceName?: string | undefined;
1178
1178
  }>;
@@ -1187,17 +1187,17 @@ export declare const devicePairingTranscriptSchema: z.ZodObject<{
1187
1187
  nameplate: number;
1188
1188
  sourceIdentity: {
1189
1189
  publicKey: string;
1190
- publicBoxKey: string;
1191
1190
  userId: string;
1192
1191
  deviceId: string;
1192
+ publicBoxKey: string;
1193
1193
  userName?: string | undefined;
1194
1194
  deviceName?: string | undefined;
1195
1195
  };
1196
1196
  destinationIdentity: {
1197
1197
  publicKey: string;
1198
- publicBoxKey: string;
1199
1198
  userId: string;
1200
1199
  deviceId: string;
1200
+ publicBoxKey: string;
1201
1201
  userName?: string | undefined;
1202
1202
  deviceName?: string | undefined;
1203
1203
  };
@@ -1211,17 +1211,17 @@ export declare const devicePairingTranscriptSchema: z.ZodObject<{
1211
1211
  nameplate: number;
1212
1212
  sourceIdentity: {
1213
1213
  publicKey: string;
1214
- publicBoxKey: string;
1215
1214
  userId: string;
1216
1215
  deviceId: string;
1216
+ publicBoxKey: string;
1217
1217
  userName?: string | undefined;
1218
1218
  deviceName?: string | undefined;
1219
1219
  };
1220
1220
  destinationIdentity: {
1221
1221
  publicKey: string;
1222
- publicBoxKey: string;
1223
1222
  userId: string;
1224
1223
  deviceId: string;
1224
+ publicBoxKey: string;
1225
1225
  userName?: string | undefined;
1226
1226
  deviceName?: string | undefined;
1227
1227
  };
@@ -1261,16 +1261,16 @@ export declare const pairingConfirmationContentsSchema: z.ZodObject<{
1261
1261
  deviceName: z.ZodOptional<z.ZodString>;
1262
1262
  }, "strip", z.ZodTypeAny, {
1263
1263
  publicKey: string;
1264
- publicBoxKey: string;
1265
1264
  userId: string;
1266
1265
  deviceId: string;
1266
+ publicBoxKey: string;
1267
1267
  userName?: string | undefined;
1268
1268
  deviceName?: string | undefined;
1269
1269
  }, {
1270
1270
  publicKey: string;
1271
- publicBoxKey: string;
1272
1271
  userId: string;
1273
1272
  deviceId: string;
1273
+ publicBoxKey: string;
1274
1274
  userName?: string | undefined;
1275
1275
  deviceName?: string | undefined;
1276
1276
  }>;
@@ -1292,9 +1292,9 @@ export declare const pairingConfirmationContentsSchema: z.ZodObject<{
1292
1292
  version: 3;
1293
1293
  device: {
1294
1294
  publicKey: string;
1295
- publicBoxKey: string;
1296
1295
  userId: string;
1297
1296
  deviceId: string;
1297
+ publicBoxKey: string;
1298
1298
  userName?: string | undefined;
1299
1299
  deviceName?: string | undefined;
1300
1300
  };
@@ -1309,9 +1309,9 @@ export declare const pairingConfirmationContentsSchema: z.ZodObject<{
1309
1309
  version: 3;
1310
1310
  device: {
1311
1311
  publicKey: string;
1312
- publicBoxKey: string;
1313
1312
  userId: string;
1314
1313
  deviceId: string;
1314
+ publicBoxKey: string;
1315
1315
  userName?: string | undefined;
1316
1316
  deviceName?: string | undefined;
1317
1317
  };
@@ -1341,16 +1341,16 @@ export declare const pairingConfirmationSchema: z.ZodObject<{
1341
1341
  deviceName: z.ZodOptional<z.ZodString>;
1342
1342
  }, "strip", z.ZodTypeAny, {
1343
1343
  publicKey: string;
1344
- publicBoxKey: string;
1345
1344
  userId: string;
1346
1345
  deviceId: string;
1346
+ publicBoxKey: string;
1347
1347
  userName?: string | undefined;
1348
1348
  deviceName?: string | undefined;
1349
1349
  }, {
1350
1350
  publicKey: string;
1351
- publicBoxKey: string;
1352
1351
  userId: string;
1353
1352
  deviceId: string;
1353
+ publicBoxKey: string;
1354
1354
  userName?: string | undefined;
1355
1355
  deviceName?: string | undefined;
1356
1356
  }>;
@@ -1374,9 +1374,9 @@ export declare const pairingConfirmationSchema: z.ZodObject<{
1374
1374
  version: 3;
1375
1375
  device: {
1376
1376
  publicKey: string;
1377
- publicBoxKey: string;
1378
1377
  userId: string;
1379
1378
  deviceId: string;
1379
+ publicBoxKey: string;
1380
1380
  userName?: string | undefined;
1381
1381
  deviceName?: string | undefined;
1382
1382
  };
@@ -1392,9 +1392,9 @@ export declare const pairingConfirmationSchema: z.ZodObject<{
1392
1392
  version: 3;
1393
1393
  device: {
1394
1394
  publicKey: string;
1395
- publicBoxKey: string;
1396
1395
  userId: string;
1397
1396
  deviceId: string;
1397
+ publicBoxKey: string;
1398
1398
  userName?: string | undefined;
1399
1399
  deviceName?: string | undefined;
1400
1400
  };
@@ -1459,16 +1459,16 @@ export declare const pairingInstallationReceiptContentsSchema: z.ZodObject<{
1459
1459
  deviceName: z.ZodOptional<z.ZodString>;
1460
1460
  }, "strip", z.ZodTypeAny, {
1461
1461
  publicKey: string;
1462
- publicBoxKey: string;
1463
1462
  userId: string;
1464
1463
  deviceId: string;
1464
+ publicBoxKey: string;
1465
1465
  userName?: string | undefined;
1466
1466
  deviceName?: string | undefined;
1467
1467
  }, {
1468
1468
  publicKey: string;
1469
- publicBoxKey: string;
1470
1469
  userId: string;
1471
1470
  deviceId: string;
1471
+ publicBoxKey: string;
1472
1472
  userName?: string | undefined;
1473
1473
  deviceName?: string | undefined;
1474
1474
  }>;
@@ -1478,9 +1478,9 @@ export declare const pairingInstallationReceiptContentsSchema: z.ZodObject<{
1478
1478
  transcriptHash: string;
1479
1479
  initializedDevice: {
1480
1480
  publicKey: string;
1481
- publicBoxKey: string;
1482
1481
  userId: string;
1483
1482
  deviceId: string;
1483
+ publicBoxKey: string;
1484
1484
  userName?: string | undefined;
1485
1485
  deviceName?: string | undefined;
1486
1486
  };
@@ -1490,9 +1490,9 @@ export declare const pairingInstallationReceiptContentsSchema: z.ZodObject<{
1490
1490
  transcriptHash: string;
1491
1491
  initializedDevice: {
1492
1492
  publicKey: string;
1493
- publicBoxKey: string;
1494
1493
  userId: string;
1495
1494
  deviceId: string;
1495
+ publicBoxKey: string;
1496
1496
  userName?: string | undefined;
1497
1497
  deviceName?: string | undefined;
1498
1498
  };
@@ -1514,16 +1514,16 @@ export declare const signedPairingInstallationReceiptSchema: z.ZodObject<{
1514
1514
  deviceName: z.ZodOptional<z.ZodString>;
1515
1515
  }, "strip", z.ZodTypeAny, {
1516
1516
  publicKey: string;
1517
- publicBoxKey: string;
1518
1517
  userId: string;
1519
1518
  deviceId: string;
1519
+ publicBoxKey: string;
1520
1520
  userName?: string | undefined;
1521
1521
  deviceName?: string | undefined;
1522
1522
  }, {
1523
1523
  publicKey: string;
1524
- publicBoxKey: string;
1525
1524
  userId: string;
1526
1525
  deviceId: string;
1526
+ publicBoxKey: string;
1527
1527
  userName?: string | undefined;
1528
1528
  deviceName?: string | undefined;
1529
1529
  }>;
@@ -1533,9 +1533,9 @@ export declare const signedPairingInstallationReceiptSchema: z.ZodObject<{
1533
1533
  transcriptHash: string;
1534
1534
  initializedDevice: {
1535
1535
  publicKey: string;
1536
- publicBoxKey: string;
1537
1536
  userId: string;
1538
1537
  deviceId: string;
1538
+ publicBoxKey: string;
1539
1539
  userName?: string | undefined;
1540
1540
  deviceName?: string | undefined;
1541
1541
  };
@@ -1545,9 +1545,9 @@ export declare const signedPairingInstallationReceiptSchema: z.ZodObject<{
1545
1545
  transcriptHash: string;
1546
1546
  initializedDevice: {
1547
1547
  publicKey: string;
1548
- publicBoxKey: string;
1549
1548
  userId: string;
1550
1549
  deviceId: string;
1550
+ publicBoxKey: string;
1551
1551
  userName?: string | undefined;
1552
1552
  deviceName?: string | undefined;
1553
1553
  };
@@ -1563,9 +1563,9 @@ export declare const signedPairingInstallationReceiptSchema: z.ZodObject<{
1563
1563
  transcriptHash: string;
1564
1564
  initializedDevice: {
1565
1565
  publicKey: string;
1566
- publicBoxKey: string;
1567
1566
  userId: string;
1568
1567
  deviceId: string;
1568
+ publicBoxKey: string;
1569
1569
  userName?: string | undefined;
1570
1570
  deviceName?: string | undefined;
1571
1571
  };
@@ -1579,9 +1579,9 @@ export declare const signedPairingInstallationReceiptSchema: z.ZodObject<{
1579
1579
  transcriptHash: string;
1580
1580
  initializedDevice: {
1581
1581
  publicKey: string;
1582
- publicBoxKey: string;
1583
1582
  userId: string;
1584
1583
  deviceId: string;
1584
+ publicBoxKey: string;
1585
1585
  userName?: string | undefined;
1586
1586
  deviceName?: string | undefined;
1587
1587
  };
@@ -0,0 +1,78 @@
1
+ /** Why a signing key on this host was replaced. */
2
+ export declare const KEY_ROTATION_REASONS: readonly ["rotation", "compromised", "lost"];
3
+ /** One of {@link KEY_ROTATION_REASONS}. */
4
+ export type KeyRotationReason = (typeof KEY_ROTATION_REASONS)[number];
5
+ /**
6
+ * The credential record every host keeps for the signed-in user.
7
+ *
8
+ * Electron stores it in the OS keychain, headless in `credentials.json`, the PWA in its
9
+ * IndexedDB vault. Besides the identity it carries `dbSecret`, the SQLCipher key for this
10
+ * host's database files, which is deliberately *not* derived from `secretKey` any more: that
11
+ * is what lets a user rotate their signing key without re-encrypting every database
12
+ * (`PRAGMA rekey` is unusable under WAL). Existing installs keep working because a missing
13
+ * `dbSecret` means "the legacy derived value", so migration changes no file on disk.
14
+ */
15
+ /** A key this host's user retired, kept for display and for `getKeyInfo`. */
16
+ export interface IPreviousPublicKey {
17
+ publicKey: string;
18
+ /** ISO 8601 */
19
+ replacedAt: string;
20
+ reason?: KeyRotationReason;
21
+ }
22
+ /**
23
+ * The key a rotation retired, kept only until every secret in the personal database has been
24
+ * re-wrapped under the new one. Secrets are rewritten one at a time after the new key is
25
+ * stored; without this slot a crash in between would leave values nothing can open.
26
+ */
27
+ export interface IRewrapSource {
28
+ publicKey: string;
29
+ secretKey: string;
30
+ }
31
+ /** Stored credentials; see the module description. */
32
+ export interface IHostCredentialRecord {
33
+ userId: string;
34
+ /** Current Ed25519 secret key, base64url (64-byte encoding). */
35
+ secretKey: string;
36
+ /** SQLCipher key for this host's databases. Never changes on key rotation. */
37
+ dbSecret: string;
38
+ /** Keys this user retired on this host, newest last. */
39
+ previousPublicKeys?: IPreviousPublicKey[];
40
+ /** Present while a rotation's re-wrap is unfinished; see {@link IRewrapSource}. */
41
+ rewrapFrom?: IRewrapSource;
42
+ }
43
+ /** The SQLCipher key hosts used before `dbSecret` existed: a hash of the user secret. */
44
+ export declare function legacyDbSecret(secretKey: string): string;
45
+ /** A fresh random SQLCipher key for a brand-new identity on this host. */
46
+ export declare function newDbSecret(): string;
47
+ /**
48
+ * Fills in `dbSecret` for a record that predates it (or was copied by pairing, which transfers
49
+ * only `userId` and `secretKey`).
50
+ *
51
+ * @param input Partial record with at least `userId` and `secretKey`
52
+ * @param opts.freshDbSecret When true a missing `dbSecret` is minted at random (new identity on
53
+ * this host, no database file exists yet). Default: the legacy derived value, so an existing
54
+ * database still opens.
55
+ */
56
+ export declare function normalizeCredentialRecord(input: Partial<IHostCredentialRecord> & Pick<IHostCredentialRecord, "userId" | "secretKey">, opts?: {
57
+ freshDbSecret?: boolean;
58
+ }): IHostCredentialRecord;
59
+ /**
60
+ * Parses a stored credential string: the JSON form written by current hosts, or the legacy
61
+ * `userId::secretKey` form. Returns `null` when the value is neither.
62
+ */
63
+ export declare function parseCredentialRecord(value: string | null | undefined): IHostCredentialRecord | null;
64
+ /** Serializes a record for storage (JSON, one line). */
65
+ export declare function serializeCredentialRecord(record: IHostCredentialRecord): string;
66
+ /**
67
+ * The record after a key rotation on this host: new secret, same `dbSecret`, old public key
68
+ * appended to `previousPublicKeys`, and the old secret kept in `rewrapFrom` until
69
+ * {@link completeCredentialRewrap} says every secret is readable under the new key.
70
+ */
71
+ export declare function rotateCredentialRecord(current: IHostCredentialRecord, next: {
72
+ secretKey: string;
73
+ oldPublicKey: string;
74
+ reason?: KeyRotationReason;
75
+ at?: Date;
76
+ }): IHostCredentialRecord;
77
+ /** The record once the re-wrap after a rotation has finished: the retired secret is dropped. */
78
+ export declare function completeCredentialRewrap(record: IHostCredentialRecord): IHostCredentialRecord;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KEY_ROTATION_REASONS = void 0;
4
+ exports.legacyDbSecret = legacyDbSecret;
5
+ exports.newDbSecret = newDbSecret;
6
+ exports.normalizeCredentialRecord = normalizeCredentialRecord;
7
+ exports.parseCredentialRecord = parseCredentialRecord;
8
+ exports.serializeCredentialRecord = serializeCredentialRecord;
9
+ exports.rotateCredentialRecord = rotateCredentialRecord;
10
+ exports.completeCredentialRewrap = completeCredentialRewrap;
11
+ const keys_1 = require("./keys");
12
+ const utils_1 = require("./utils");
13
+ /** Why a signing key on this host was replaced. */
14
+ exports.KEY_ROTATION_REASONS = ["rotation", "compromised", "lost"];
15
+ /** The SQLCipher key hosts used before `dbSecret` existed: a hash of the user secret. */
16
+ function legacyDbSecret(secretKey) {
17
+ return (0, keys_1.hashValue)(secretKey);
18
+ }
19
+ /** A fresh random SQLCipher key for a brand-new identity on this host. */
20
+ function newDbSecret() {
21
+ return (0, keys_1.newToken)(32);
22
+ }
23
+ /**
24
+ * Fills in `dbSecret` for a record that predates it (or was copied by pairing, which transfers
25
+ * only `userId` and `secretKey`).
26
+ *
27
+ * @param input Partial record with at least `userId` and `secretKey`
28
+ * @param opts.freshDbSecret When true a missing `dbSecret` is minted at random (new identity on
29
+ * this host, no database file exists yet). Default: the legacy derived value, so an existing
30
+ * database still opens.
31
+ */
32
+ function normalizeCredentialRecord(input, opts = {}) {
33
+ const dbSecret = input.dbSecret || (opts.freshDbSecret ? newDbSecret() : legacyDbSecret(input.secretKey));
34
+ return {
35
+ userId: input.userId,
36
+ secretKey: input.secretKey,
37
+ dbSecret,
38
+ ...(input.previousPublicKeys?.length ? { previousPublicKeys: input.previousPublicKeys } : {}),
39
+ ...(input.rewrapFrom?.publicKey && input.rewrapFrom.secretKey
40
+ ? {
41
+ rewrapFrom: {
42
+ publicKey: input.rewrapFrom.publicKey,
43
+ secretKey: input.rewrapFrom.secretKey,
44
+ },
45
+ }
46
+ : {}),
47
+ };
48
+ }
49
+ /**
50
+ * Parses a stored credential string: the JSON form written by current hosts, or the legacy
51
+ * `userId::secretKey` form. Returns `null` when the value is neither.
52
+ */
53
+ function parseCredentialRecord(value) {
54
+ if (!value)
55
+ return null;
56
+ const trimmed = value.trim();
57
+ if (trimmed.startsWith("{")) {
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(trimmed);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (!parsed ||
66
+ typeof parsed !== "object" ||
67
+ typeof parsed.userId !== "string" ||
68
+ typeof parsed.secretKey !== "string" ||
69
+ !parsed.userId ||
70
+ !parsed.secretKey) {
71
+ return null;
72
+ }
73
+ return normalizeCredentialRecord({
74
+ userId: parsed.userId,
75
+ secretKey: parsed.secretKey,
76
+ dbSecret: typeof parsed.dbSecret === "string" ? parsed.dbSecret : undefined,
77
+ previousPublicKeys: Array.isArray(parsed.previousPublicKeys)
78
+ ? parsed.previousPublicKeys.filter((p) => p && typeof p.publicKey === "string" && typeof p.replacedAt === "string")
79
+ : undefined,
80
+ rewrapFrom: parsed.rewrapFrom &&
81
+ typeof parsed.rewrapFrom.publicKey === "string" &&
82
+ typeof parsed.rewrapFrom.secretKey === "string"
83
+ ? { publicKey: parsed.rewrapFrom.publicKey, secretKey: parsed.rewrapFrom.secretKey }
84
+ : undefined,
85
+ });
86
+ }
87
+ const separator = trimmed.indexOf("::");
88
+ if (separator <= 0)
89
+ return null;
90
+ const userId = trimmed.slice(0, separator);
91
+ const secretKey = trimmed.slice(separator + 2);
92
+ if (!userId || !secretKey)
93
+ return null;
94
+ return normalizeCredentialRecord({ userId, secretKey });
95
+ }
96
+ /** Serializes a record for storage (JSON, one line). */
97
+ function serializeCredentialRecord(record) {
98
+ return JSON.stringify(normalizeCredentialRecord(record));
99
+ }
100
+ /**
101
+ * The record after a key rotation on this host: new secret, same `dbSecret`, old public key
102
+ * appended to `previousPublicKeys`, and the old secret kept in `rewrapFrom` until
103
+ * {@link completeCredentialRewrap} says every secret is readable under the new key.
104
+ */
105
+ function rotateCredentialRecord(current, next) {
106
+ if (!(0, utils_1.isid)(current.userId)) {
107
+ throw new Error(`Credential record has an invalid userId: ${current.userId}`);
108
+ }
109
+ return normalizeCredentialRecord({
110
+ userId: current.userId,
111
+ secretKey: next.secretKey,
112
+ dbSecret: current.dbSecret,
113
+ previousPublicKeys: [
114
+ ...(current.previousPublicKeys ?? []),
115
+ {
116
+ publicKey: next.oldPublicKey,
117
+ replacedAt: (next.at ?? new Date()).toISOString(),
118
+ ...(next.reason ? { reason: next.reason } : {}),
119
+ },
120
+ ],
121
+ rewrapFrom: { publicKey: next.oldPublicKey, secretKey: current.secretKey },
122
+ });
123
+ }
124
+ /** The record once the re-wrap after a rotation has finished: the retired secret is dropped. */
125
+ function completeCredentialRewrap(record) {
126
+ const { rewrapFrom: _finished, ...rest } = record;
127
+ return normalizeCredentialRecord(rest);
128
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const host_credentials_1 = require("./host-credentials");
4
+ const keys_1 = require("./keys");
5
+ const utils_1 = require("./utils");
6
+ describe("host credential record", () => {
7
+ const userId = (0, utils_1.newid)();
8
+ const k1 = (0, keys_1.newKeys)();
9
+ const k2 = (0, keys_1.newKeys)();
10
+ it("parses the legacy userId::secretKey form and derives the legacy db secret", () => {
11
+ const record = (0, host_credentials_1.parseCredentialRecord)(`${userId}::${k1.secretKey}`);
12
+ expect(record).toEqual({
13
+ userId,
14
+ secretKey: k1.secretKey,
15
+ dbSecret: (0, keys_1.hashValue)(k1.secretKey),
16
+ });
17
+ expect((0, host_credentials_1.legacyDbSecret)(k1.secretKey)).toBe((0, keys_1.hashValue)(k1.secretKey));
18
+ });
19
+ it("parses the JSON form, filling a missing dbSecret with the legacy value", () => {
20
+ expect((0, host_credentials_1.parseCredentialRecord)(JSON.stringify({ userId, secretKey: k1.secretKey }))).toEqual({
21
+ userId,
22
+ secretKey: k1.secretKey,
23
+ dbSecret: (0, keys_1.hashValue)(k1.secretKey),
24
+ });
25
+ const full = {
26
+ userId,
27
+ secretKey: k2.secretKey,
28
+ dbSecret: "db",
29
+ previousPublicKeys: [{ publicKey: k1.publicKey, replacedAt: "2026-10-02T00:00:00.000Z" }],
30
+ };
31
+ expect((0, host_credentials_1.parseCredentialRecord)((0, host_credentials_1.serializeCredentialRecord)(full))).toEqual(full);
32
+ });
33
+ it("returns null for garbage", () => {
34
+ expect((0, host_credentials_1.parseCredentialRecord)(null)).toBeNull();
35
+ expect((0, host_credentials_1.parseCredentialRecord)("")).toBeNull();
36
+ expect((0, host_credentials_1.parseCredentialRecord)("{not json")).toBeNull();
37
+ expect((0, host_credentials_1.parseCredentialRecord)(JSON.stringify({ userId }))).toBeNull();
38
+ expect((0, host_credentials_1.parseCredentialRecord)("::")).toBeNull();
39
+ expect((0, host_credentials_1.parseCredentialRecord)("nosep")).toBeNull();
40
+ });
41
+ it("mints a random dbSecret only when asked", () => {
42
+ const fresh = (0, host_credentials_1.normalizeCredentialRecord)({ userId, secretKey: k1.secretKey }, { freshDbSecret: true });
43
+ expect(fresh.dbSecret).not.toBe((0, keys_1.hashValue)(k1.secretKey));
44
+ expect(fresh.dbSecret.length).toBeGreaterThan(30);
45
+ const kept = (0, host_credentials_1.normalizeCredentialRecord)({ userId, secretKey: k1.secretKey, dbSecret: "keep" }, {
46
+ freshDbSecret: true,
47
+ });
48
+ expect(kept.dbSecret).toBe("keep");
49
+ });
50
+ it("rotates the secret while keeping dbSecret and remembering the old key", () => {
51
+ const current = (0, host_credentials_1.normalizeCredentialRecord)({ userId, secretKey: k1.secretKey });
52
+ const at = new Date("2026-10-03T00:00:00.000Z");
53
+ const rotated = (0, host_credentials_1.rotateCredentialRecord)(current, {
54
+ secretKey: k2.secretKey,
55
+ oldPublicKey: k1.publicKey,
56
+ reason: "compromised",
57
+ at,
58
+ });
59
+ expect(rotated).toEqual({
60
+ userId,
61
+ secretKey: k2.secretKey,
62
+ dbSecret: (0, keys_1.hashValue)(k1.secretKey),
63
+ previousPublicKeys: [
64
+ { publicKey: k1.publicKey, replacedAt: at.toISOString(), reason: "compromised" },
65
+ ],
66
+ rewrapFrom: { publicKey: k1.publicKey, secretKey: k1.secretKey },
67
+ });
68
+ });
69
+ it("keeps the retired secret through storage until the re-wrap is finished", () => {
70
+ // Secrets in the personal database are re-wrapped one by one after the new key is stored.
71
+ // A crash in between must not lose the only key that can still open the rest.
72
+ const rotated = (0, host_credentials_1.rotateCredentialRecord)((0, host_credentials_1.normalizeCredentialRecord)({ userId, secretKey: k1.secretKey }), { secretKey: k2.secretKey, oldPublicKey: k1.publicKey });
73
+ const stored = (0, host_credentials_1.parseCredentialRecord)((0, host_credentials_1.serializeCredentialRecord)(rotated));
74
+ expect(stored?.rewrapFrom).toEqual({ publicKey: k1.publicKey, secretKey: k1.secretKey });
75
+ expect(stored?.secretKey).toBe(k2.secretKey);
76
+ const finished = (0, host_credentials_1.completeCredentialRewrap)(rotated);
77
+ expect(finished.rewrapFrom).toBeUndefined();
78
+ expect(finished.secretKey).toBe(k2.secretKey);
79
+ expect(finished.previousPublicKeys).toEqual(rotated.previousPublicKeys);
80
+ expect((0, host_credentials_1.parseCredentialRecord)((0, host_credentials_1.serializeCredentialRecord)(finished))?.rewrapFrom).toBeUndefined();
81
+ // A half-written slot is not a key to re-wrap from.
82
+ expect((0, host_credentials_1.parseCredentialRecord)(JSON.stringify({
83
+ userId,
84
+ secretKey: k2.secretKey,
85
+ rewrapFrom: { publicKey: k1.publicKey },
86
+ }))?.rewrapFrom).toBeUndefined();
87
+ });
88
+ });
@@ -0,0 +1,59 @@
1
+ import { type ISignedObject } from "../keys";
2
+ /** Purpose string the current key signs to bind a recovery email. */
3
+ export declare const ACCOUNT_EMAIL_BIND_PURPOSE: "peers-account-email-bind";
4
+ /** Statement the current key signs so the registry can bind a recovery email. */
5
+ export interface IAccountEmailBindStatement {
6
+ purpose: typeof ACCOUNT_EMAIL_BIND_PURPOSE;
7
+ userId: string;
8
+ email: string;
9
+ issuedAt: string;
10
+ }
11
+ /**
12
+ * Signs a request to bind `email` to `userId` with the key on record.
13
+ *
14
+ * @param statement The user, the mailbox, and when the request was made
15
+ * @param secretKey Secret half of the key on record
16
+ */
17
+ export declare function signEmailBind(statement: IAccountEmailBindStatement, secretKey: string): ISignedObject<IAccountEmailBindStatement>;
18
+ /**
19
+ * Checks that `signed` is the record key's signature over a bind statement for this user
20
+ * and mailbox. A different signer, user, or email is not a bind.
21
+ */
22
+ export declare function verifyEmailBind(params: {
23
+ signed: ISignedObject<IAccountEmailBindStatement>;
24
+ userId: string;
25
+ email: string;
26
+ recordPublicKey: string;
27
+ }): boolean;
28
+ /** Purpose string the current key signs to read whether a recovery email is bound. */
29
+ export declare const ACCOUNT_EMAIL_STATUS_PURPOSE: "peers-account-email-status";
30
+ /** Statement the current key signs to read the bound recovery email. */
31
+ export interface IAccountEmailStatusStatement {
32
+ purpose: typeof ACCOUNT_EMAIL_STATUS_PURPOSE;
33
+ userId: string;
34
+ issuedAt: string;
35
+ }
36
+ /** Signs a request to read the recovery email bound to `userId`. */
37
+ export declare function signEmailStatus(statement: IAccountEmailStatusStatement, secretKey: string): ISignedObject<IAccountEmailStatusStatement>;
38
+ /** Checks that `signed` is the record key asking for this user's recovery email. */
39
+ export declare function verifyEmailStatus(params: {
40
+ signed: ISignedObject<IAccountEmailStatusStatement>;
41
+ userId: string;
42
+ recordPublicKey: string;
43
+ }): boolean;
44
+ /** Purpose string the current key signs to remove a recovery email. */
45
+ export declare const ACCOUNT_EMAIL_UNBIND_PURPOSE: "peers-account-email-unbind";
46
+ /** Statement the current key signs to remove the recovery email. */
47
+ export interface IAccountEmailUnbindStatement {
48
+ purpose: typeof ACCOUNT_EMAIL_UNBIND_PURPOSE;
49
+ userId: string;
50
+ issuedAt: string;
51
+ }
52
+ /** Signs a request to remove the recovery email bound to `userId`. */
53
+ export declare function signEmailUnbind(statement: IAccountEmailUnbindStatement, secretKey: string): ISignedObject<IAccountEmailUnbindStatement>;
54
+ /** Checks that `signed` is the record key removing this user's recovery email. */
55
+ export declare function verifyEmailUnbind(params: {
56
+ signed: ISignedObject<IAccountEmailUnbindStatement>;
57
+ userId: string;
58
+ recordPublicKey: string;
59
+ }): boolean;