@peers-app/peers-sdk 0.25.14 → 0.25.16

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
@@ -37,15 +37,33 @@ export declare const inviteSchema: z.ZodObject<{
37
37
  publicKey: z.ZodString;
38
38
  publicBoxKey: z.ZodString;
39
39
  displayName: z.ZodOptional<z.ZodString>;
40
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
41
+ type: z.ZodEnum<["peers-services", "https"]>;
42
+ url: z.ZodString;
43
+ }, "strip", z.ZodTypeAny, {
44
+ type: "peers-services" | "https";
45
+ url: string;
46
+ }, {
47
+ type: "peers-services" | "https";
48
+ url: string;
49
+ }>, "many">>;
40
50
  }, "strict", z.ZodTypeAny, {
41
51
  publicKey: string;
42
- publicBoxKey: string;
43
52
  userId: string;
53
+ publicBoxKey: string;
54
+ anchors?: {
55
+ type: "peers-services" | "https";
56
+ url: string;
57
+ }[] | undefined;
44
58
  displayName?: string | undefined;
45
59
  }, {
46
60
  publicKey: string;
47
- publicBoxKey: string;
48
61
  userId: string;
62
+ publicBoxKey: string;
63
+ anchors?: {
64
+ type: "peers-services" | "https";
65
+ url: string;
66
+ }[] | undefined;
49
67
  displayName?: string | undefined;
50
68
  }>>;
51
69
  /** Device the counterpart used, when they told us; lets the network layer connect early. */
@@ -67,15 +85,33 @@ export declare const inviteSchema: z.ZodObject<{
67
85
  publicKey: z.ZodString;
68
86
  publicBoxKey: z.ZodString;
69
87
  displayName: z.ZodOptional<z.ZodString>;
88
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
89
+ type: z.ZodEnum<["peers-services", "https"]>;
90
+ url: z.ZodString;
91
+ }, "strip", z.ZodTypeAny, {
92
+ type: "peers-services" | "https";
93
+ url: string;
94
+ }, {
95
+ type: "peers-services" | "https";
96
+ url: string;
97
+ }>, "many">>;
70
98
  }, "strict", z.ZodTypeAny, {
71
99
  publicKey: string;
72
- publicBoxKey: string;
73
100
  userId: string;
101
+ publicBoxKey: string;
102
+ anchors?: {
103
+ type: "peers-services" | "https";
104
+ url: string;
105
+ }[] | undefined;
74
106
  displayName?: string | undefined;
75
107
  }, {
76
108
  publicKey: string;
77
- publicBoxKey: string;
78
109
  userId: string;
110
+ publicBoxKey: string;
111
+ anchors?: {
112
+ type: "peers-services" | "https";
113
+ url: string;
114
+ }[] | undefined;
79
115
  displayName?: string | undefined;
80
116
  }>;
81
117
  expiresAt: z.ZodString;
@@ -93,15 +129,15 @@ export declare const inviteSchema: z.ZodObject<{
93
129
  name: string;
94
130
  role: GroupMemberRole;
95
131
  publicKey: string;
96
- groupId: string;
97
132
  publicBoxKey: string;
133
+ groupId: string;
98
134
  mode: "open" | "approve";
99
135
  }, {
100
136
  name: string;
101
137
  role: GroupMemberRole;
102
138
  publicKey: string;
103
- groupId: string;
104
139
  publicBoxKey: string;
140
+ groupId: string;
105
141
  mode: "open" | "approve";
106
142
  }>>;
107
143
  signature: z.ZodString;
@@ -113,8 +149,12 @@ export declare const inviteSchema: z.ZodObject<{
113
149
  v: 1;
114
150
  inviter: {
115
151
  publicKey: string;
116
- publicBoxKey: string;
117
152
  userId: string;
153
+ publicBoxKey: string;
154
+ anchors?: {
155
+ type: "peers-services" | "https";
156
+ url: string;
157
+ }[] | undefined;
118
158
  displayName?: string | undefined;
119
159
  };
120
160
  epoch: number;
@@ -123,8 +163,8 @@ export declare const inviteSchema: z.ZodObject<{
123
163
  name: string;
124
164
  role: GroupMemberRole;
125
165
  publicKey: string;
126
- groupId: string;
127
166
  publicBoxKey: string;
167
+ groupId: string;
128
168
  mode: "open" | "approve";
129
169
  } | undefined;
130
170
  }, {
@@ -135,8 +175,12 @@ export declare const inviteSchema: z.ZodObject<{
135
175
  v: 1;
136
176
  inviter: {
137
177
  publicKey: string;
138
- publicBoxKey: string;
139
178
  userId: string;
179
+ publicBoxKey: string;
180
+ anchors?: {
181
+ type: "peers-services" | "https";
182
+ url: string;
183
+ }[] | undefined;
140
184
  displayName?: string | undefined;
141
185
  };
142
186
  epoch: number;
@@ -145,8 +189,8 @@ export declare const inviteSchema: z.ZodObject<{
145
189
  name: string;
146
190
  role: GroupMemberRole;
147
191
  publicKey: string;
148
- groupId: string;
149
192
  publicBoxKey: string;
193
+ groupId: string;
150
194
  mode: "open" | "approve";
151
195
  } | undefined;
152
196
  }>>;
@@ -181,10 +225,10 @@ export declare const inviteSchema: z.ZodObject<{
181
225
  }, "strip", z.ZodTypeAny, {
182
226
  type: "group" | "device" | "contact";
183
227
  direction: "in" | "out";
184
- status: "pending" | "expired" | "accepted" | "declined" | "revoked";
228
+ status: "pending" | "expired" | "accepted" | "revoked" | "declined";
185
229
  createdAt: Date;
186
230
  expiresAt: Date;
187
- mode: "direct" | "open" | "approve" | "contact" | "profile";
231
+ mode: "direct" | "open" | "profile" | "approve" | "contact";
188
232
  epoch: number;
189
233
  inviteId: string;
190
234
  role?: GroupMemberRole | undefined;
@@ -203,8 +247,12 @@ export declare const inviteSchema: z.ZodObject<{
203
247
  v: 1;
204
248
  inviter: {
205
249
  publicKey: string;
206
- publicBoxKey: string;
207
250
  userId: string;
251
+ publicBoxKey: string;
252
+ anchors?: {
253
+ type: "peers-services" | "https";
254
+ url: string;
255
+ }[] | undefined;
208
256
  displayName?: string | undefined;
209
257
  };
210
258
  epoch: number;
@@ -213,15 +261,19 @@ export declare const inviteSchema: z.ZodObject<{
213
261
  name: string;
214
262
  role: GroupMemberRole;
215
263
  publicKey: string;
216
- groupId: string;
217
264
  publicBoxKey: string;
265
+ groupId: string;
218
266
  mode: "open" | "approve";
219
267
  } | undefined;
220
268
  } | undefined;
221
269
  counterpart?: {
222
270
  publicKey: string;
223
- publicBoxKey: string;
224
271
  userId: string;
272
+ publicBoxKey: string;
273
+ anchors?: {
274
+ type: "peers-services" | "https";
275
+ url: string;
276
+ }[] | undefined;
225
277
  displayName?: string | undefined;
226
278
  } | undefined;
227
279
  counterpartDeviceId?: string | undefined;
@@ -235,12 +287,12 @@ export declare const inviteSchema: z.ZodObject<{
235
287
  type: "group" | "device" | "contact";
236
288
  direction: "in" | "out";
237
289
  expiresAt: Date;
238
- mode: "direct" | "open" | "approve" | "contact" | "profile";
290
+ mode: "direct" | "open" | "profile" | "approve" | "contact";
239
291
  epoch: number;
240
292
  inviteId: string;
241
293
  role?: GroupMemberRole | undefined;
242
294
  nonce?: string | undefined;
243
- status?: "pending" | "expired" | "accepted" | "declined" | "revoked" | undefined;
295
+ status?: "pending" | "expired" | "accepted" | "revoked" | "declined" | undefined;
244
296
  groupId?: string | undefined;
245
297
  createdAt?: Date | undefined;
246
298
  payload?: {
@@ -256,8 +308,12 @@ export declare const inviteSchema: z.ZodObject<{
256
308
  v: 1;
257
309
  inviter: {
258
310
  publicKey: string;
259
- publicBoxKey: string;
260
311
  userId: string;
312
+ publicBoxKey: string;
313
+ anchors?: {
314
+ type: "peers-services" | "https";
315
+ url: string;
316
+ }[] | undefined;
261
317
  displayName?: string | undefined;
262
318
  };
263
319
  epoch: number;
@@ -266,15 +322,19 @@ export declare const inviteSchema: z.ZodObject<{
266
322
  name: string;
267
323
  role: GroupMemberRole;
268
324
  publicKey: string;
269
- groupId: string;
270
325
  publicBoxKey: string;
326
+ groupId: string;
271
327
  mode: "open" | "approve";
272
328
  } | undefined;
273
329
  } | undefined;
274
330
  counterpart?: {
275
331
  publicKey: string;
276
- publicBoxKey: string;
277
332
  userId: string;
333
+ publicBoxKey: string;
334
+ anchors?: {
335
+ type: "peers-services" | "https";
336
+ url: string;
337
+ }[] | undefined;
278
338
  displayName?: string | undefined;
279
339
  } | undefined;
280
340
  counterpartDeviceId?: string | undefined;
@@ -295,10 +355,10 @@ export type IInvite = z.infer<typeof inviteSchema>;
295
355
  export declare function Invites(dataContext?: DataContext): import("..").Table<{
296
356
  type: "group" | "device" | "contact";
297
357
  direction: "in" | "out";
298
- status: "pending" | "expired" | "accepted" | "declined" | "revoked";
358
+ status: "pending" | "expired" | "accepted" | "revoked" | "declined";
299
359
  createdAt: Date;
300
360
  expiresAt: Date;
301
- mode: "direct" | "open" | "approve" | "contact" | "profile";
361
+ mode: "direct" | "open" | "profile" | "approve" | "contact";
302
362
  epoch: number;
303
363
  inviteId: string;
304
364
  role?: GroupMemberRole | undefined;
@@ -317,8 +377,12 @@ export declare function Invites(dataContext?: DataContext): import("..").Table<{
317
377
  v: 1;
318
378
  inviter: {
319
379
  publicKey: string;
320
- publicBoxKey: string;
321
380
  userId: string;
381
+ publicBoxKey: string;
382
+ anchors?: {
383
+ type: "peers-services" | "https";
384
+ url: string;
385
+ }[] | undefined;
322
386
  displayName?: string | undefined;
323
387
  };
324
388
  epoch: number;
@@ -327,15 +391,19 @@ export declare function Invites(dataContext?: DataContext): import("..").Table<{
327
391
  name: string;
328
392
  role: GroupMemberRole;
329
393
  publicKey: string;
330
- groupId: string;
331
394
  publicBoxKey: string;
395
+ groupId: string;
332
396
  mode: "open" | "approve";
333
397
  } | undefined;
334
398
  } | undefined;
335
399
  counterpart?: {
336
400
  publicKey: string;
337
- publicBoxKey: string;
338
401
  userId: string;
402
+ publicBoxKey: string;
403
+ anchors?: {
404
+ type: "peers-services" | "https";
405
+ url: string;
406
+ }[] | undefined;
339
407
  displayName?: string | undefined;
340
408
  } | undefined;
341
409
  counterpartDeviceId?: string | undefined;
@@ -63,6 +63,14 @@ export declare class PersistentVarsTable extends Table<IPersistentVar> {
63
63
  update(persistentVar: IPersistentVar): Promise<IPersistentVar>;
64
64
  /** Save a variable, assigning its deterministic ID and encrypting new secret plaintext. */
65
65
  save(persistentVar: IPersistentVar, opts?: ISaveOptions): Promise<IPersistentVar>;
66
+ /**
67
+ * Replace a secret variable's stored ciphertext as-is, without passing it through
68
+ * `rpcServerCalls.encryptData` again. Used by key rotation, which re-wraps every personal
69
+ * secret from the old user key to the new one and has already produced the new ciphertext.
70
+ *
71
+ * @throws When the variable is not marked `isSecret`
72
+ */
73
+ replaceEncryptedValue(persistentVar: IPersistentVar, encryptedValue: string): Promise<IPersistentVar>;
66
74
  /**
67
75
  * Replace a legacy random record ID without re-encrypting its already-stored value.
68
76
  *
@@ -109,11 +109,14 @@ function persistentVarIdentityWinner(left, right) {
109
109
  let PersistentVarsTable = (() => {
110
110
  let _classSuper = table_1.Table;
111
111
  let _instanceExtraInitializers = [];
112
+ let _replaceEncryptedValue_decorators;
112
113
  let _migrateLegacyId_decorators;
113
114
  return class PersistentVarsTable extends _classSuper {
114
115
  static {
115
116
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
117
+ _replaceEncryptedValue_decorators = [(0, decorators_1.ProxyClientTableMethodCalls)()];
116
118
  _migrateLegacyId_decorators = [(0, decorators_1.ProxyClientTableMethodCalls)()];
119
+ __esDecorate(this, null, _replaceEncryptedValue_decorators, { kind: "method", name: "replaceEncryptedValue", static: false, private: false, access: { has: obj => "replaceEncryptedValue" in obj, get: obj => obj.replaceEncryptedValue }, metadata: _metadata }, null, _instanceExtraInitializers);
117
120
  __esDecorate(this, null, _migrateLegacyId_decorators, { kind: "method", name: "migrateLegacyId", static: false, private: false, access: { has: obj => "migrateLegacyId" in obj, get: obj => obj.migrateLegacyId }, metadata: _metadata }, null, _instanceExtraInitializers);
118
121
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
119
122
  }
@@ -145,6 +148,19 @@ let PersistentVarsTable = (() => {
145
148
  opts = { saveAsSnapshot: true, ...opts };
146
149
  return super.save(persistentVar, opts);
147
150
  }
151
+ /**
152
+ * Replace a secret variable's stored ciphertext as-is, without passing it through
153
+ * `rpcServerCalls.encryptData` again. Used by key rotation, which re-wraps every personal
154
+ * secret from the old user key to the new one and has already produced the new ciphertext.
155
+ *
156
+ * @throws When the variable is not marked `isSecret`
157
+ */
158
+ async replaceEncryptedValue(persistentVar, encryptedValue) {
159
+ if (!persistentVar.isSecret) {
160
+ throw new Error(`Persistent variable ${persistentVar.name} is not a secret`);
161
+ }
162
+ return super.save({ ...persistentVar, value: { value: encryptedValue }, modifiedAt: Date.now() }, { saveAsSnapshot: true });
163
+ }
148
164
  /**
149
165
  * Replace a legacy random record ID without re-encrypting its already-stored value.
150
166
  *
@@ -46,9 +46,9 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
46
46
  }, "strip", z.ZodTypeAny, {
47
47
  nonce: string;
48
48
  status: ToolAccessApprovalStatus;
49
- scope: ToolAccessApprovalScope;
50
49
  userId: string;
51
50
  toolId: string;
51
+ scope: ToolAccessApprovalScope;
52
52
  dataContextId: string;
53
53
  createdAt: Date;
54
54
  expiresAt: Date;
@@ -102,9 +102,9 @@ export type IToolAccessApproval = z.infer<typeof toolAccessApprovalSchema>;
102
102
  export declare function ToolAccessApprovals(dataContext?: DataContext): import("..").Table<{
103
103
  nonce: string;
104
104
  status: ToolAccessApprovalStatus;
105
- scope: ToolAccessApprovalScope;
106
105
  userId: string;
107
106
  toolId: string;
107
+ scope: ToolAccessApprovalScope;
108
108
  dataContextId: string;
109
109
  createdAt: Date;
110
110
  expiresAt: Date;
@@ -1,15 +1,23 @@
1
1
  import type { IUser } from "./users";
2
2
  /**
3
- * Signs a user object with the provided secret key
3
+ * Signs a user object with the provided secret key. The `anchors` list is kept on the row
4
+ * but is not covered by the signature; see {@link verifyUserSignature}.
4
5
  * @param user The user object to sign
5
6
  * @param secretKey The secret key to sign with
6
7
  * @returns IUser The signed user object
7
8
  */
8
9
  export declare function signUserObject(user: IUser, secretKey: string): IUser;
9
10
  /**
10
- * Verifies that a user update signature is valid, throws on invalid signature
11
+ * Verifies that a user update signature is valid, throws on invalid signature.
12
+ * The `anchors` list is not part of what is verified.
11
13
  * @param user The user record to verify
12
14
  * @param oldUser The existing user record (if any)
13
15
  * @throws Error if signature is invalid or unauthorized
14
16
  */
15
17
  export declare function verifyUserSignature(user: IUser, oldUser?: IUser): void;
18
+ /**
19
+ * Checks that `user` is signed by its own `publicKey`, ignoring any key already on record.
20
+ * Used after the anchors have confirmed a key change: the row still has to be the new key's.
21
+ * @throws Error if the signature is missing, invalid, or by another key
22
+ */
23
+ export declare function verifyUserSignedBySelf(user: IUser): void;
@@ -2,18 +2,34 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.signUserObject = signUserObject;
4
4
  exports.verifyUserSignature = verifyUserSignature;
5
+ exports.verifyUserSignedBySelf = verifyUserSignedBySelf;
5
6
  const keys_1 = require("../keys");
6
7
  /**
7
- * Signs a user object with the provided secret key
8
+ * The part of a `Users` row the owner signs: everything but `anchors`.
9
+ *
10
+ * The anchor list on record is decided by the anchors themselves (the on-record rule), not
11
+ * by whoever wrote the row. A peer that confirms a key change stores the row with the list
12
+ * on record, and a peer on an older release stores the row without the field at all. Both
13
+ * must still verify, so the list is carried beside the signature rather than under it.
14
+ */
15
+ function signedPart(user) {
16
+ const { anchors: _anchors, ...rest } = user;
17
+ return rest;
18
+ }
19
+ /**
20
+ * Signs a user object with the provided secret key. The `anchors` list is kept on the row
21
+ * but is not covered by the signature; see {@link verifyUserSignature}.
8
22
  * @param user The user object to sign
9
23
  * @param secretKey The secret key to sign with
10
24
  * @returns IUser The signed user object
11
25
  */
12
26
  function signUserObject(user, secretKey) {
13
- return (0, keys_1.addSignatureToObject)(user, secretKey);
27
+ const signed = (0, keys_1.addSignatureToObject)(signedPart(user), secretKey);
28
+ return user.anchors ? { ...signed, anchors: user.anchors } : signed;
14
29
  }
15
30
  /**
16
- * Verifies that a user update signature is valid, throws on invalid signature
31
+ * Verifies that a user update signature is valid, throws on invalid signature.
32
+ * The `anchors` list is not part of what is verified.
17
33
  * @param user The user record to verify
18
34
  * @param oldUser The existing user record (if any)
19
35
  * @throws Error if signature is invalid or unauthorized
@@ -26,7 +42,7 @@ function verifyUserSignature(user, oldUser) {
26
42
  }
27
43
  return;
28
44
  }
29
- (0, keys_1.verifyObjectSignature)(user);
45
+ (0, keys_1.verifyObjectSignature)(signedPart(user));
30
46
  const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(user) ?? "";
31
47
  // New user: signature must match their own public key
32
48
  if (!oldUser) {
@@ -44,3 +60,18 @@ function verifyUserSignature(user, oldUser) {
44
60
  throw new Error("Public key changes are not currently supported");
45
61
  }
46
62
  }
63
+ /**
64
+ * Checks that `user` is signed by its own `publicKey`, ignoring any key already on record.
65
+ * Used after the anchors have confirmed a key change: the row still has to be the new key's.
66
+ * @throws Error if the signature is missing, invalid, or by another key
67
+ */
68
+ function verifyUserSignedBySelf(user) {
69
+ if (!user.signature) {
70
+ throw new Error("A confirmed key change still has to be signed by the new key");
71
+ }
72
+ (0, keys_1.verifyObjectSignature)(signedPart(user));
73
+ const signerPublicKey = (0, keys_1.getPublicKeyFromObjectSignature)(user) ?? "";
74
+ if (signerPublicKey !== user.publicKey) {
75
+ throw new Error("User signature must be signed with their own public key");
76
+ }
77
+ }
@@ -201,6 +201,32 @@ describe("User Permissions Logic", () => {
201
201
  expect(() => (0, user_permissions_1.verifyUserSignature)(signedIncoming, existingStub)).not.toThrow();
202
202
  });
203
203
  });
204
+ describe("verifyUserSignature - anchors are not part of the claim", () => {
205
+ // The anchor list on record is decided by the anchors, not by the row's author. A peer
206
+ // stores a confirmed row with the list on record, and a peer on an older release stores
207
+ // the row without the field at all. Both must still verify.
208
+ const anchorA = { type: "https", url: "https://a.example/peers-identity.json" };
209
+ const anchorB = { type: "https", url: "https://b.example/peers-identity.json" };
210
+ it("verifies a signed row after its anchor list is replaced or removed", () => {
211
+ const signed = (0, user_permissions_1.signUserObject)({ ...testUser, anchors: [anchorA] }, userKeys.secretKey);
212
+ expect(() => (0, user_permissions_1.verifyUserSignature)({ ...signed, anchors: [anchorB] })).not.toThrow();
213
+ const { anchors: _dropped, ...withoutAnchors } = signed;
214
+ expect(() => (0, user_permissions_1.verifyUserSignature)(withoutAnchors)).not.toThrow();
215
+ });
216
+ it("verifies a row signed without anchors after a list is added", () => {
217
+ const signed = (0, user_permissions_1.signUserObject)(testUser, userKeys.secretKey);
218
+ expect(() => (0, user_permissions_1.verifyUserSignature)({ ...signed, anchors: [anchorA] })).not.toThrow();
219
+ });
220
+ it("still rejects a change to any other field", () => {
221
+ const signed = (0, user_permissions_1.signUserObject)({ ...testUser, anchors: [anchorA] }, userKeys.secretKey);
222
+ expect(() => (0, user_permissions_1.verifyUserSignature)({ ...signed, name: "Someone else" })).toThrow();
223
+ expect(() => (0, user_permissions_1.verifyUserSignature)({ ...signed, publicBoxKey: otherUserKeys.publicBoxKey })).toThrow();
224
+ });
225
+ it("does not leave the anchors off the signed row", () => {
226
+ const signed = (0, user_permissions_1.signUserObject)({ ...testUser, anchors: [anchorA] }, userKeys.secretKey);
227
+ expect(signed.anchors).toEqual([anchorA]);
228
+ });
229
+ });
204
230
  describe("edge cases and error handling", () => {
205
231
  it("should handle undefined user gracefully", () => {
206
232
  // @ts-expect-error - Testing edge case
@@ -8,28 +8,72 @@ export declare const userSchema: z.ZodObject<{
8
8
  name: z.ZodString;
9
9
  publicKey: z.ZodString;
10
10
  publicBoxKey: z.ZodString;
11
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
12
+ type: z.ZodEnum<["peers-services", "https"]>;
13
+ url: z.ZodString;
14
+ }, "strip", z.ZodTypeAny, {
15
+ type: "peers-services" | "https";
16
+ url: string;
17
+ }, {
18
+ type: "peers-services" | "https";
19
+ url: string;
20
+ }>, "many">>;
11
21
  signature: z.ZodOptional<z.ZodString>;
12
22
  }, "strip", z.ZodTypeAny, {
13
23
  name: string;
14
24
  publicKey: string;
15
- publicBoxKey: string;
16
25
  userId: string;
26
+ publicBoxKey: string;
17
27
  signature?: string | undefined;
28
+ anchors?: {
29
+ type: "peers-services" | "https";
30
+ url: string;
31
+ }[] | undefined;
18
32
  }, {
19
33
  name: string;
20
34
  publicKey: string;
21
- publicBoxKey: string;
22
35
  userId: string;
36
+ publicBoxKey: string;
23
37
  signature?: string | undefined;
38
+ anchors?: {
39
+ type: "peers-services" | "https";
40
+ url: string;
41
+ }[] | undefined;
24
42
  }>;
25
43
  /** A signed user profile shared across personal and group contexts. */
26
44
  export type IUser = z.infer<typeof userSchema>;
45
+ /**
46
+ * Writes a key the resolver already confirmed. Bypasses the contact-row guard,
47
+ * which would otherwise call the resolver again.
48
+ */
49
+ export declare function writeResolvedUserKey(dataContext: DataContext, user: IUser): Promise<IUser>;
50
+ /** Outcome of a {@link UserRowChangeGuard}. See `resolveUserRowChange` in the identity module. */
51
+ export type UserRowChangeVerdict = {
52
+ action: "apply";
53
+ row: IUser;
54
+ } | {
55
+ action: "defer";
56
+ reason: string;
57
+ } | {
58
+ action: "refuse";
59
+ reason: string;
60
+ };
61
+ /**
62
+ * Consulted by {@link UsersTable.save} when another user's row already exists in the table.
63
+ * Decides whether a different key or anchor list may be stored, against the anchors on
64
+ * record. `apply` carries the row to store, whose `anchors` may have been replaced by the
65
+ * list on record.
66
+ */
67
+ export type UserRowChangeGuard = (personalContext: DataContext, incoming: IUser, existing: IUser) => Promise<UserRowChangeVerdict>;
68
+ /** Installs the identity check used by {@link UsersTable.save}. The identity module sets this. */
69
+ export declare function setUserRowChangeGuard(guard: UserRowChangeGuard | undefined): void;
27
70
  /** User table with owner-signature enforcement outside the personal context. */
28
71
  export declare class UsersTable extends Table<IUser> {
29
72
  static isPassthrough: boolean;
30
73
  /**
31
- * Save a user row. Outside the personal context the write must carry a valid owner
32
- * signature (or be an unsigned stub where nothing signed exists yet).
74
+ * Save a user row. A contact row, in the personal table or a group, must carry a
75
+ * valid owner signature (or be an unsigned stub where nothing signed exists yet).
76
+ * The owner's own personal row is unrestricted.
33
77
  *
34
78
  * An unsigned `weakInsert` is a discovery placeholder (connection handshake, invite
35
79
  * identity, join approval). It is written `insertOnly`: seed the row when the table has
@@ -35,10 +35,14 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
35
35
  };
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.UsersTable = exports.userSchema = void 0;
38
+ exports.writeResolvedUserKey = writeResolvedUserKey;
39
+ exports.setUserRowChangeGuard = setUserRowChangeGuard;
38
40
  exports.Users = Users;
39
41
  exports.getMe = getMe;
40
42
  const zod_1 = require("zod");
41
43
  const user_context_singleton_1 = require("../context/user-context-singleton");
44
+ const anchor_document_1 = require("../identity/anchor-document");
45
+ const anchor_resolution_1 = require("../identity/anchor-resolution");
42
46
  const zod_types_1 = require("../types/zod-types");
43
47
  const devices_1 = require("./devices");
44
48
  const decorators_1 = require("./orm/decorators");
@@ -54,6 +58,10 @@ exports.userSchema = zod_1.z.object({
54
58
  publicBoxKey: zod_1.z
55
59
  .string()
56
60
  .describe("The public key to use to encrypt data that only this user can decrypt"),
61
+ anchors: zod_1.z
62
+ .array(anchor_document_1.anchorRefSchema)
63
+ .optional()
64
+ .describe("Anchors on record for this user. A peer that already has a list updates it from those anchors, not from a row that arrives with a new key. Not covered by the row's signature"),
57
65
  signature: zod_1.z
58
66
  .string()
59
67
  .optional()
@@ -66,6 +74,34 @@ const metaData = {
66
74
  fields: (0, types_1.schemaToFields)(exports.userSchema),
67
75
  indexes: [{ fields: ["name"] }, { fields: ["publicKey"] }, { fields: ["publicBoxKey"] }],
68
76
  };
77
+ /**
78
+ * Marks a save performed by the resolver after anchors confirmed a key.
79
+ * A symbol so a proxied client cannot set it: JSON drops symbols, and a
80
+ * remote save of a contact row still has to pass the anchor check.
81
+ */
82
+ const IDENTITY_RESOLUTION = Symbol("identityResolution");
83
+ function isResolutionWrite(opts) {
84
+ return Boolean(opts && opts[IDENTITY_RESOLUTION]);
85
+ }
86
+ /**
87
+ * Writes a key the resolver already confirmed. Bypasses the contact-row guard,
88
+ * which would otherwise call the resolver again.
89
+ */
90
+ async function writeResolvedUserKey(dataContext, user) {
91
+ return Users(dataContext).save(user, {
92
+ saveAsSnapshot: true,
93
+ [IDENTITY_RESOLUTION]: true,
94
+ });
95
+ }
96
+ /** Until the identity module installs its guard, any key or anchor change is refused. */
97
+ const refuseIdentityChanges = async (_personalContext, incoming, existing) => incoming.publicKey !== existing.publicKey || !(0, anchor_resolution_1.sameAnchorList)(existing.anchors, incoming.anchors)
98
+ ? { action: "refuse", reason: "Identity changes need the identity module" }
99
+ : { action: "apply", row: incoming };
100
+ let userRowChangeGuard = refuseIdentityChanges;
101
+ /** Installs the identity check used by {@link UsersTable.save}. The identity module sets this. */
102
+ function setUserRowChangeGuard(guard) {
103
+ userRowChangeGuard = guard ?? refuseIdentityChanges;
104
+ }
69
105
  /** User table with owner-signature enforcement outside the personal context. */
70
106
  let UsersTable = (() => {
71
107
  let _classSuper = table_1.Table;
@@ -80,8 +116,9 @@ let UsersTable = (() => {
80
116
  }
81
117
  static isPassthrough = false;
82
118
  /**
83
- * Save a user row. Outside the personal context the write must carry a valid owner
84
- * signature (or be an unsigned stub where nothing signed exists yet).
119
+ * Save a user row. A contact row, in the personal table or a group, must carry a
120
+ * valid owner signature (or be an unsigned stub where nothing signed exists yet).
121
+ * The owner's own personal row is unrestricted.
85
122
  *
86
123
  * An unsigned `weakInsert` is a discovery placeholder (connection handshake, invite
87
124
  * identity, join approval). It is written `insertOnly`: seed the row when the table has
@@ -101,8 +138,12 @@ let UsersTable = (() => {
101
138
  }
102
139
  const userContext = await (0, user_context_singleton_1.getUserContext)();
103
140
  const userContextUserTable = Users(userContext.userDataContext);
104
- if (this === userContextUserTable) {
105
- // user is allowed to make any changes they want to their own user table
141
+ if (isResolutionWrite(opts)) {
142
+ return super.save(user, opts);
143
+ }
144
+ if (this === userContextUserTable && user.userId === userContext.userId) {
145
+ // The owner can change their own row. A contact's row in this same table
146
+ // is the on-record key and anchor list, so it goes through the checks below.
106
147
  return super.save(user, opts);
107
148
  }
108
149
  // Get the old user record if it exists
@@ -110,8 +151,23 @@ let UsersTable = (() => {
110
151
  if (opts.insertOnly && oldUser) {
111
152
  return oldUser;
112
153
  }
154
+ if (!oldUser) {
155
+ await (0, user_permissions_1.verifyUserSignature)(user, oldUser);
156
+ return super.save(user, opts);
157
+ }
158
+ // A different key or anchor list is judged against the anchors on record. A direct
159
+ // save cannot wait for an answer, so "not yet" is refused the same as "no".
160
+ const verdict = await userRowChangeGuard(userContext.userDataContext, user, oldUser);
161
+ if (verdict.action !== "apply") {
162
+ throw new Error(verdict.reason);
163
+ }
164
+ if (user.publicKey !== oldUser.publicKey) {
165
+ // The anchors confirmed the new key; the row still has to be signed by it.
166
+ (0, user_permissions_1.verifyUserSignedBySelf)(user);
167
+ return super.save(verdict.row, opts);
168
+ }
113
169
  await (0, user_permissions_1.verifyUserSignature)(user, oldUser);
114
- return super.save(user, opts);
170
+ return super.save(verdict.row, opts);
115
171
  }
116
172
  async signAndSave(user, opts) {
117
173
  if (!UsersTable.addSignatureToUser) {