@peers-app/peers-sdk 0.25.13 → 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.
- package/dist/context/user-context-identity.test.d.ts +1 -0
- package/dist/context/user-context-identity.test.js +220 -0
- package/dist/context/user-context.d.ts +5 -1
- package/dist/context/user-context.js +63 -8
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/system/identity-contract.d.ts +156 -0
- package/dist/contracts/system/identity-contract.js +172 -0
- package/dist/contracts/system/index.d.ts +1 -1
- package/dist/data/assistants.d.ts +4 -4
- package/dist/data/devices.d.ts +70 -4
- package/dist/data/devices.js +3 -0
- package/dist/data/group-members.d.ts +2 -2
- package/dist/data/groups.d.ts +2 -2
- package/dist/data/invites.d.ts +93 -25
- package/dist/data/persistent-vars.d.ts +8 -0
- package/dist/data/persistent-vars.js +16 -0
- package/dist/data/tool-access-approvals.d.ts +2 -2
- package/dist/data/user-permissions.d.ts +10 -2
- package/dist/data/user-permissions.js +35 -4
- package/dist/data/user-permissions.test.js +26 -0
- package/dist/data/users.d.ts +48 -4
- package/dist/data/users.js +61 -5
- package/dist/data/workflows.d.ts +1 -1
- package/dist/device/connection.d.ts +7 -1
- package/dist/device/connection.js +38 -6
- package/dist/device/connection.test.js +33 -4
- package/dist/device/device.d.ts +2 -2
- package/dist/device/device.js +4 -2
- package/dist/device/get-trust-level-fn.d.ts +1 -0
- package/dist/device/get-trust-level-fn.js +28 -4
- package/dist/device/get-trust-level-fn.test.js +95 -0
- package/dist/device-pairing/types.d.ts +26 -26
- package/dist/host-credentials.d.ts +78 -0
- package/dist/host-credentials.js +128 -0
- package/dist/host-credentials.test.d.ts +1 -0
- package/dist/host-credentials.test.js +88 -0
- package/dist/identity/account-email.d.ts +59 -0
- package/dist/identity/account-email.js +87 -0
- package/dist/identity/account-email.test.d.ts +1 -0
- package/dist/identity/account-email.test.js +59 -0
- package/dist/identity/anchor-document.d.ts +205 -0
- package/dist/identity/anchor-document.js +119 -0
- package/dist/identity/anchor-document.test.d.ts +1 -0
- package/dist/identity/anchor-document.test.js +16 -0
- package/dist/identity/anchor-resolution.d.ts +61 -0
- package/dist/identity/anchor-resolution.js +117 -0
- package/dist/identity/anchor-resolution.test.d.ts +1 -0
- package/dist/identity/anchor-resolution.test.js +130 -0
- package/dist/identity/key-change.d.ts +142 -0
- package/dist/identity/key-change.js +472 -0
- package/dist/identity/key-change.test.d.ts +1 -0
- package/dist/identity/key-change.test.js +811 -0
- package/dist/identity/key-contest.d.ts +91 -0
- package/dist/identity/key-contest.js +55 -0
- package/dist/identity/key-resolutions.d.ts +55 -0
- package/dist/identity/key-resolutions.js +42 -0
- package/dist/identity/passkey.d.ts +22 -0
- package/dist/identity/passkey.js +33 -0
- package/dist/identity/user-row-change.d.ts +45 -0
- package/dist/identity/user-row-change.js +81 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/invites/invite-messages.d.ts +512 -116
- package/dist/invites/invite-token.d.ts +114 -24
- package/dist/invites/invite-token.js +6 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +20 -0
- package/dist/package-installer/package-author-signing.test.js +11 -0
- package/dist/package-installer/package-publisher.d.ts +5 -1
- package/dist/package-installer/package-publisher.js +1 -1
- package/dist/package-installer/package-publisher.test.js +11 -0
- package/dist/package-installer/package-remote-checker.js +3 -0
- package/dist/system-ids.d.ts +16 -0
- package/dist/system-ids.js +16 -0
- package/dist/types/workflow.d.ts +2 -2
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +43 -0
- package/dist/utils.test.js +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../identity/user-row-change";
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const SQLiteDB = require("better-sqlite3");
|
|
4
|
+
const sql_data_source_1 = require("../data/orm/sql.data-source");
|
|
5
|
+
const user_permissions_1 = require("../data/user-permissions");
|
|
6
|
+
const users_1 = require("../data/users");
|
|
7
|
+
const anchor_document_1 = require("../identity/anchor-document");
|
|
8
|
+
const anchor_resolution_1 = require("../identity/anchor-resolution");
|
|
9
|
+
const key_change_1 = require("../identity/key-change");
|
|
10
|
+
const keys_1 = require("../keys");
|
|
11
|
+
const utils_1 = require("../utils");
|
|
12
|
+
const user_context_1 = require("./user-context");
|
|
13
|
+
const user_context_singleton_1 = require("./user-context-singleton");
|
|
14
|
+
// Installs the UsersTable guard.
|
|
15
|
+
require("../identity/user-row-change");
|
|
16
|
+
class DBHarness {
|
|
17
|
+
_db = null;
|
|
18
|
+
get db() {
|
|
19
|
+
if (!this._db) {
|
|
20
|
+
this._db = new SQLiteDB(":memory:");
|
|
21
|
+
this._db.pragma("journal_mode = WAL");
|
|
22
|
+
}
|
|
23
|
+
return this._db;
|
|
24
|
+
}
|
|
25
|
+
async get(sql, params = []) {
|
|
26
|
+
return this.db.prepare(sql).get(params);
|
|
27
|
+
}
|
|
28
|
+
async all(sql, params = []) {
|
|
29
|
+
return this.db.prepare(sql).all(params);
|
|
30
|
+
}
|
|
31
|
+
async exec(sql, params = []) {
|
|
32
|
+
const result = this.db.prepare(sql).run(params);
|
|
33
|
+
return { changes: result.changes };
|
|
34
|
+
}
|
|
35
|
+
async close() {
|
|
36
|
+
this._db?.close();
|
|
37
|
+
this._db = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
describe("UserContext.syncUserAndGroupObjects and the profile row's key", () => {
|
|
41
|
+
const anchor = {
|
|
42
|
+
type: "https",
|
|
43
|
+
url: "https://example.com/.well-known/peers-identity.json",
|
|
44
|
+
};
|
|
45
|
+
// The process-wide user context can be installed once, so one context serves every case;
|
|
46
|
+
// each case starts by writing the profile row it needs (the owner's own row is unrestricted).
|
|
47
|
+
const db = new DBHarness();
|
|
48
|
+
const userId = (0, utils_1.newid)();
|
|
49
|
+
let userContext;
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
const dataSourceFactory = (metaData, schema) => new sql_data_source_1.SQLDataSource(db, metaData, schema);
|
|
52
|
+
userContext = new user_context_1.UserContext(userId, dataSourceFactory, true);
|
|
53
|
+
await userContext.loadingPromise;
|
|
54
|
+
userContext.deviceId((0, utils_1.newid)());
|
|
55
|
+
(0, user_context_singleton_1.setUserContext)(userContext);
|
|
56
|
+
});
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
(0, anchor_resolution_1.setAnchorFetcher)(undefined);
|
|
59
|
+
});
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
userContext.dispose();
|
|
62
|
+
await db.close();
|
|
63
|
+
});
|
|
64
|
+
function document(publicKey, authorizedBy) {
|
|
65
|
+
return {
|
|
66
|
+
outcome: "document",
|
|
67
|
+
document: {
|
|
68
|
+
version: 1,
|
|
69
|
+
userId,
|
|
70
|
+
keys: [{ publicKey, issuedAt: "2026-10-03T18:00:00.000Z", authorizedBy }],
|
|
71
|
+
anchors: [anchor],
|
|
72
|
+
updatedAt: "2026-10-03T18:00:00.000Z",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
it("signs a fresh profile with the device key", async () => {
|
|
77
|
+
const keys = (0, keys_1.newKeys)();
|
|
78
|
+
await userContext.syncUserAndGroupObjects(userId, keys);
|
|
79
|
+
const me = await (0, users_1.Users)(userContext.userDataContext).get(userId);
|
|
80
|
+
expect(me?.publicKey).toBe(keys.publicKey);
|
|
81
|
+
expect(() => (0, user_permissions_1.verifyUserSignature)(me)).not.toThrow();
|
|
82
|
+
});
|
|
83
|
+
it("re-signs a profile the anchors say this device's key succeeded", async () => {
|
|
84
|
+
// A rotation stored the new secret and then stopped before re-signing the row, or an
|
|
85
|
+
// operator replaced the secret in credentials.json after publishing the key. The anchors
|
|
86
|
+
// list the device key with the old key's co-signature, so the row follows the device.
|
|
87
|
+
const k1 = (0, keys_1.newKeys)();
|
|
88
|
+
const k2 = (0, keys_1.newKeys)();
|
|
89
|
+
await (0, users_1.Users)(userContext.userDataContext).save((0, user_permissions_1.signUserObject)({
|
|
90
|
+
userId,
|
|
91
|
+
name: "Alice",
|
|
92
|
+
publicKey: k1.publicKey,
|
|
93
|
+
publicBoxKey: k1.publicBoxKey,
|
|
94
|
+
anchors: [anchor],
|
|
95
|
+
}, k1.secretKey));
|
|
96
|
+
const succession = (0, anchor_document_1.signSuccession)({
|
|
97
|
+
purpose: "peers-key-succession",
|
|
98
|
+
userId,
|
|
99
|
+
publicKey: k2.publicKey,
|
|
100
|
+
issuedAt: "2026-10-03T18:00:00.000Z",
|
|
101
|
+
}, k1.secretKey);
|
|
102
|
+
(0, anchor_resolution_1.setAnchorFetcher)(async () => document(k2.publicKey, { publicKey: succession.publicKey, signature: succession.signature }));
|
|
103
|
+
await userContext.syncUserAndGroupObjects(userId, k2);
|
|
104
|
+
const me = await (0, users_1.Users)(userContext.userDataContext).get(userId);
|
|
105
|
+
expect(me?.publicKey).toBe(k2.publicKey);
|
|
106
|
+
expect(me?.publicBoxKey).toBe(k2.publicBoxKey);
|
|
107
|
+
expect(me?.name).toBe("Alice");
|
|
108
|
+
expect(me?.anchors).toEqual([anchor]);
|
|
109
|
+
expect(() => (0, user_permissions_1.verifyUserSignature)(me)).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
it("does not put the device key back over a row the anchors still confirm", async () => {
|
|
112
|
+
// The row holds K2 and the anchors list only K2: this device's K1 was rotated away. The
|
|
113
|
+
// row must not be re-signed with K1, or the device would publish a retired key.
|
|
114
|
+
const k1 = (0, keys_1.newKeys)();
|
|
115
|
+
const k2 = (0, keys_1.newKeys)();
|
|
116
|
+
const row = (0, user_permissions_1.signUserObject)({
|
|
117
|
+
userId,
|
|
118
|
+
name: "Alice",
|
|
119
|
+
publicKey: k2.publicKey,
|
|
120
|
+
publicBoxKey: k2.publicBoxKey,
|
|
121
|
+
anchors: [anchor],
|
|
122
|
+
}, k2.secretKey);
|
|
123
|
+
await (0, users_1.Users)(userContext.userDataContext).save(row);
|
|
124
|
+
(0, anchor_resolution_1.setAnchorFetcher)(async () => document(k2.publicKey));
|
|
125
|
+
await userContext.syncUserAndGroupObjects(userId, k1);
|
|
126
|
+
const me = await (0, users_1.Users)(userContext.userDataContext).get(userId);
|
|
127
|
+
expect(me?.publicKey).toBe(k2.publicKey);
|
|
128
|
+
expect(me?.signature).toBe(row.signature);
|
|
129
|
+
});
|
|
130
|
+
it("leaves the row alone while the anchors have not answered", async () => {
|
|
131
|
+
const k1 = (0, keys_1.newKeys)();
|
|
132
|
+
const k2 = (0, keys_1.newKeys)();
|
|
133
|
+
const row = (0, user_permissions_1.signUserObject)({
|
|
134
|
+
userId,
|
|
135
|
+
name: "Alice",
|
|
136
|
+
publicKey: k1.publicKey,
|
|
137
|
+
publicBoxKey: k1.publicBoxKey,
|
|
138
|
+
anchors: [anchor],
|
|
139
|
+
}, k1.secretKey);
|
|
140
|
+
await (0, users_1.Users)(userContext.userDataContext).save(row);
|
|
141
|
+
(0, anchor_resolution_1.setAnchorFetcher)(async () => ({ outcome: "abstain" }));
|
|
142
|
+
await userContext.syncUserAndGroupObjects(userId, k2);
|
|
143
|
+
const me = await (0, users_1.Users)(userContext.userDataContext).get(userId);
|
|
144
|
+
expect(me?.publicKey).toBe(k1.publicKey);
|
|
145
|
+
expect(me?.signature).toBe(row.signature);
|
|
146
|
+
});
|
|
147
|
+
describe("the rotated-away notice", () => {
|
|
148
|
+
const noticeFor = (oldKey, newKey) => ({
|
|
149
|
+
userId,
|
|
150
|
+
oldPublicKey: oldKey,
|
|
151
|
+
newPublicKey: newKey,
|
|
152
|
+
detectedAt: "2026-10-03T18:00:00.000Z",
|
|
153
|
+
});
|
|
154
|
+
afterEach(async () => {
|
|
155
|
+
await key_change_1.identityRotatedAwayVar.loadingPromise;
|
|
156
|
+
(0, key_change_1.identityRotatedAwayVar)(null);
|
|
157
|
+
});
|
|
158
|
+
it("is cleared once this device holds the key the anchors confirmed", async () => {
|
|
159
|
+
// The device was told its K1 was rotated away, then paired again onto the same
|
|
160
|
+
// database and now holds K2. The banner must not outlive the problem.
|
|
161
|
+
const k1 = (0, keys_1.newKeys)();
|
|
162
|
+
const k2 = (0, keys_1.newKeys)();
|
|
163
|
+
await (0, users_1.Users)(userContext.userDataContext).save((0, user_permissions_1.signUserObject)({
|
|
164
|
+
userId,
|
|
165
|
+
name: "Alice",
|
|
166
|
+
publicKey: k2.publicKey,
|
|
167
|
+
publicBoxKey: k2.publicBoxKey,
|
|
168
|
+
anchors: [anchor],
|
|
169
|
+
}, k2.secretKey));
|
|
170
|
+
await key_change_1.identityRotatedAwayVar.loadingPromise;
|
|
171
|
+
(0, key_change_1.identityRotatedAwayVar)(noticeFor(k1.publicKey, k2.publicKey));
|
|
172
|
+
await userContext.syncUserAndGroupObjects(userId, k2);
|
|
173
|
+
expect((0, key_change_1.identityRotatedAwayVar)()).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
it("is cleared when the anchors confirm the key this device now holds", async () => {
|
|
176
|
+
// Paired again after a further rotation: the notice names K2, the device holds K3, and
|
|
177
|
+
// the row on disk is still K1 until the anchors answer.
|
|
178
|
+
const k1 = (0, keys_1.newKeys)();
|
|
179
|
+
const k2 = (0, keys_1.newKeys)();
|
|
180
|
+
const k3 = (0, keys_1.newKeys)();
|
|
181
|
+
await (0, users_1.Users)(userContext.userDataContext).save((0, user_permissions_1.signUserObject)({
|
|
182
|
+
userId,
|
|
183
|
+
name: "Alice",
|
|
184
|
+
publicKey: k1.publicKey,
|
|
185
|
+
publicBoxKey: k1.publicBoxKey,
|
|
186
|
+
anchors: [anchor],
|
|
187
|
+
}, k1.secretKey));
|
|
188
|
+
const succession = (0, anchor_document_1.signSuccession)({
|
|
189
|
+
purpose: "peers-key-succession",
|
|
190
|
+
userId,
|
|
191
|
+
publicKey: k3.publicKey,
|
|
192
|
+
issuedAt: "2026-10-03T18:00:00.000Z",
|
|
193
|
+
}, k1.secretKey);
|
|
194
|
+
(0, anchor_resolution_1.setAnchorFetcher)(async () => document(k3.publicKey, {
|
|
195
|
+
publicKey: succession.publicKey,
|
|
196
|
+
signature: succession.signature,
|
|
197
|
+
}));
|
|
198
|
+
await key_change_1.identityRotatedAwayVar.loadingPromise;
|
|
199
|
+
(0, key_change_1.identityRotatedAwayVar)(noticeFor(k1.publicKey, k2.publicKey));
|
|
200
|
+
await userContext.syncUserAndGroupObjects(userId, k3);
|
|
201
|
+
expect((0, key_change_1.identityRotatedAwayVar)()).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
it("stays on a device that still holds the retired key", async () => {
|
|
204
|
+
const k1 = (0, keys_1.newKeys)();
|
|
205
|
+
const k2 = (0, keys_1.newKeys)();
|
|
206
|
+
await (0, users_1.Users)(userContext.userDataContext).save((0, user_permissions_1.signUserObject)({
|
|
207
|
+
userId,
|
|
208
|
+
name: "Alice",
|
|
209
|
+
publicKey: k1.publicKey,
|
|
210
|
+
publicBoxKey: k1.publicBoxKey,
|
|
211
|
+
anchors: [anchor],
|
|
212
|
+
}, k1.secretKey));
|
|
213
|
+
await key_change_1.identityRotatedAwayVar.loadingPromise;
|
|
214
|
+
const notice = noticeFor(k1.publicKey, k2.publicKey);
|
|
215
|
+
(0, key_change_1.identityRotatedAwayVar)(notice);
|
|
216
|
+
await userContext.syncUserAndGroupObjects(userId, k1);
|
|
217
|
+
expect((0, key_change_1.identityRotatedAwayVar)()).toEqual(notice);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -62,9 +62,13 @@ export declare class UserContext {
|
|
|
62
62
|
getMe(): Promise<{
|
|
63
63
|
name: string;
|
|
64
64
|
publicKey: string;
|
|
65
|
-
publicBoxKey: string;
|
|
66
65
|
userId: string;
|
|
66
|
+
publicBoxKey: string;
|
|
67
67
|
signature?: string | undefined;
|
|
68
|
+
anchors?: {
|
|
69
|
+
type: "peers-services" | "https";
|
|
70
|
+
url: string;
|
|
71
|
+
}[] | undefined;
|
|
68
72
|
} | undefined>;
|
|
69
73
|
/**
|
|
70
74
|
* Sign and persist this user's identity, then copy it into every loaded
|
|
@@ -4,6 +4,7 @@ exports.UserContext = void 0;
|
|
|
4
4
|
const lodash_1 = require("lodash");
|
|
5
5
|
const data_context_1 = require("../context/data-context");
|
|
6
6
|
const data_1 = require("../data");
|
|
7
|
+
const user_permissions_1 = require("../data/user-permissions");
|
|
7
8
|
const events_1 = require("../events");
|
|
8
9
|
const keys_1 = require("../keys");
|
|
9
10
|
const observable_1 = require("../observable");
|
|
@@ -234,13 +235,41 @@ class UserContext {
|
|
|
234
235
|
};
|
|
235
236
|
}
|
|
236
237
|
me.name = me.name.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
let rowFollowsDevice = true;
|
|
239
|
+
let anchorsConfirmed = false;
|
|
240
|
+
if (me.publicKey && me.publicKey !== keys.publicKey) {
|
|
241
|
+
// The row on record holds another key. The anchors on record decide: when they list the
|
|
242
|
+
// key this device holds (a rotation that stored the new secret but stopped before
|
|
243
|
+
// re-signing the row, or credentials an operator replaced after publishing the key) the
|
|
244
|
+
// row follows the device. Otherwise this device's key is the one that was rotated away,
|
|
245
|
+
// and it must not be put back over the row.
|
|
246
|
+
const { evaluatePresentedKey } = await Promise.resolve().then(() => require("../identity/key-change"));
|
|
247
|
+
const decision = await evaluatePresentedKey({
|
|
248
|
+
dataContext: this.userDataContext,
|
|
249
|
+
userId,
|
|
250
|
+
presentedKey: keys.publicKey,
|
|
251
|
+
presentedBoxKey: keys.publicBoxKey,
|
|
252
|
+
adopt: false,
|
|
253
|
+
});
|
|
254
|
+
rowFollowsDevice = decision.accepted;
|
|
255
|
+
anchorsConfirmed = decision.accepted;
|
|
256
|
+
if (!rowFollowsDevice) {
|
|
257
|
+
console.warn(`The profile row for ${userId} holds a key this device does not (${decision.state}); leaving it alone`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (rowFollowsDevice) {
|
|
261
|
+
// A "pair this device again" notice from before is stale once this device holds the key
|
|
262
|
+
// it named, or a key the anchors have just confirmed (paired again after a later rotation).
|
|
263
|
+
const { clearRotatedAwayNotice } = await Promise.resolve().then(() => require("../identity/key-change"));
|
|
264
|
+
await clearRotatedAwayNotice({ userId, deviceKey: keys.publicKey, anchorsConfirmed });
|
|
265
|
+
const { signature: _previous, ...profile } = me;
|
|
266
|
+
me = { ...profile, publicKey: keys.publicKey, publicBoxKey: keys.publicBoxKey };
|
|
267
|
+
const meSigned = (0, user_permissions_1.signUserObject)(me, keys.secretKey);
|
|
268
|
+
// sync my user to personal db
|
|
269
|
+
if (!(0, lodash_1.isEqual)(me, meSigned)) {
|
|
270
|
+
(0, user_permissions_1.verifyUserSignature)(meSigned);
|
|
271
|
+
await (0, data_1.Users)(this.userDataContext).save(meSigned, { weakInsert: true });
|
|
272
|
+
}
|
|
244
273
|
}
|
|
245
274
|
await this.syncMyUserToAllGroups();
|
|
246
275
|
// sync group objects to my personal db
|
|
@@ -313,9 +342,35 @@ class UserContext {
|
|
|
313
342
|
return;
|
|
314
343
|
}
|
|
315
344
|
const groupMe = await (0, data_1.Users)(dataContext).get(currentUser.userId);
|
|
316
|
-
if (
|
|
345
|
+
if ((0, lodash_1.isEqual)(groupMe, currentUser)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (groupMe && groupMe.publicKey !== currentUser.publicKey) {
|
|
349
|
+
// The group holds another key for us. A confirmed successor of our key means this
|
|
350
|
+
// device was rotated away. A confirmed successor of the group's key means our row
|
|
351
|
+
// is ahead and may be copied. Anything else leaves the group row alone.
|
|
352
|
+
const { detectIdentityRotatedAway, evaluatePresentedKey } = await Promise.resolve().then(() => require("../identity/key-change"));
|
|
353
|
+
const rotatedAway = await detectIdentityRotatedAway({ userId: currentUser.userId, publicKey: currentUser.publicKey }, groupMe);
|
|
354
|
+
if (rotatedAway)
|
|
355
|
+
return;
|
|
356
|
+
const ahead = await evaluatePresentedKey({
|
|
357
|
+
dataContext: this.userDataContext,
|
|
358
|
+
userId: currentUser.userId,
|
|
359
|
+
presentedKey: currentUser.publicKey,
|
|
360
|
+
presentedBoxKey: currentUser.publicBoxKey,
|
|
361
|
+
record: groupMe,
|
|
362
|
+
});
|
|
363
|
+
if (!ahead.accepted) {
|
|
364
|
+
console.warn(`Group ${contextId} holds my profile under another key; leaving it alone until anchors confirm`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
317
369
|
await (0, data_1.Users)(dataContext).save(currentUser);
|
|
318
370
|
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
console.error(`Error syncing my user row to group ${contextId}:`, err);
|
|
373
|
+
}
|
|
319
374
|
});
|
|
320
375
|
this.groupUserSyncs.set(contextId, currentSync);
|
|
321
376
|
try {
|
|
@@ -13,7 +13,7 @@ export { PersistentContractRegistry } from "./persistent-registry";
|
|
|
13
13
|
export { ContractRegistry, type IContractProviderRegistration } from "./registry";
|
|
14
14
|
export { createRemoteContractEndpoint, getRemoteContractTransportFactory, type IRemoteContractHost, type IRemoteContractTarget, type RemoteContractTransportFactory, resolveRemoteTargetEndpoint, setRemoteContractTransportFactory, } from "./remote-contract-transport";
|
|
15
15
|
export { DEVICE_OPERATIONS_CONTRACT_VERSION, DEVICE_OPERATIONS_MAX_QUERY_LENGTH, DEVICE_OPERATIONS_MAX_RESULT_BYTES, DEVICE_OPERATIONS_MAX_ROWS, DEVICE_OPERATIONS_MAX_TOOLS, type DeviceOperationCapability, deviceOperationsProviderPackageId, deviceOperationsToolIds, getDeviceOperationsContractDefinition, type IDeviceDatabaseQueryInput, type IDeviceDatabaseQueryResult, type IDeviceOperationStatus, type IDeviceToolListInput, type IDeviceToolLookupInput, type IDeviceToolLookupResult, type IDeviceToolRunInput, } from "./system/device-operations-contract";
|
|
16
|
-
export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentitySessionInfo, } from "./system/identity-contract";
|
|
16
|
+
export { getIdentityContractDefinition, IDENTITY_CONTRACT_VERSION, type IIdentityConsumer, type IIdentityKeyInfo, type IIdentityRegistryKey, type IIdentityRotateKeyInput, type IIdentityRotateKeyResult, type IIdentitySessionInfo, } from "./system/identity-contract";
|
|
17
17
|
export { getInvitesContractDefinition, type IAcceptInviteResult, type ICreatedInvite, type IInvitePreview, type IInvitesConsumer, INVITES_CONTRACT_VERSION, } from "./system/invites-contract";
|
|
18
18
|
export { getLocalFoldersContractDefinition, type ILocalFolderAssembleInput, type ILocalFolderAssembleResult, type ILocalFolderChange, type ILocalFolderChangedEvent, type ILocalFolderEntry, type ILocalFolderFileBlocks, type ILocalFolderFileBlocksInput, type ILocalFolderGrantInfo, type ILocalFolderIndexInput, type ILocalFolderIndexResult, type ILocalFolderJobFinishedEvent, type ILocalFolderJobHandle, type ILocalFolderJobInfo, type ILocalFolderJobProgressEvent, type ILocalFolderListInput, type ILocalFolderManifestBlock, type ILocalFolderMoveInput, type ILocalFolderPathInput, type ILocalFolderPurgeTrashInput, type ILocalFolderReadRangeInput, type ILocalFolderReadRangeResult, type ILocalFolderRequestGrantInput, type ILocalFolderStatInput, LOCAL_FOLDERS_ASSEMBLE_STALL_MS, LOCAL_FOLDERS_CHUNK_AVG, LOCAL_FOLDERS_CHUNK_MAX, LOCAL_FOLDERS_CHUNK_MIN, LOCAL_FOLDERS_CONTRACT_VERSION, LOCAL_FOLDERS_GETBLOCKS_MAX_BLOCKS, LOCAL_FOLDERS_GETBLOCKS_MAX_BYTES, LOCAL_FOLDERS_HAVE_BATCH, LOCAL_FOLDERS_JOB_TTL_MS, LOCAL_FOLDERS_READ_RANGE_MAX, LOCAL_FOLDERS_RESERVED_DIR, type LocalFolderChangeKind, type LocalFolderEntryKind, type LocalFolderJobKind, type LocalFolderMode, localFoldersProviderPackageId, } from "./system/local-folders-contract";
|
|
19
19
|
export { getLogsContractDefinition, type ILogsConsumer, LOGS_CONTRACT_VERSION, resolveLogsContract, } from "./system/logs-contract";
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DevicePairingCeremonyAction, DevicePairingStartDestinationInput, DevicePairingStartSourceInput, DevicePairingState } from "../../device-pairing";
|
|
2
|
+
import type { IPreviousPublicKey, KeyRotationReason } from "../../host-credentials";
|
|
3
|
+
import type { IAnchorDocument, IAnchorRef } from "../../identity/anchor-document";
|
|
2
4
|
import type { Observable } from "../../observable";
|
|
3
5
|
import type { IContractConsumerBase } from "../contract-proxy";
|
|
4
6
|
import type { IContractDefinition } from "../types";
|
|
@@ -11,6 +13,82 @@ export interface IIdentitySessionInfo {
|
|
|
11
13
|
/** True when this user has completed peers-services registration on this device. */
|
|
12
14
|
registered: boolean;
|
|
13
15
|
}
|
|
16
|
+
/** One key as the key registry reports it, for {@link IIdentityKeyInfo}. */
|
|
17
|
+
export interface IIdentityRegistryKey {
|
|
18
|
+
publicKey: string;
|
|
19
|
+
status: "active" | "retired" | "revoked";
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
/** The signed-in user's current and previous signing keys. */
|
|
23
|
+
export interface IIdentityKeyInfo {
|
|
24
|
+
userId: string;
|
|
25
|
+
/** Current Ed25519 public key. */
|
|
26
|
+
publicKey: string;
|
|
27
|
+
/** Current X25519 box public key. */
|
|
28
|
+
publicBoxKey: string;
|
|
29
|
+
/** Keys this host retired by rotating, newest last. */
|
|
30
|
+
previousPublicKeys: IPreviousPublicKey[];
|
|
31
|
+
/** Whether this host can rotate (false when the identity comes from the environment). */
|
|
32
|
+
canRotate: boolean;
|
|
33
|
+
/** Anchors on record, and what each one currently lists. */
|
|
34
|
+
anchors?: {
|
|
35
|
+
onRecord: IAnchorRef[];
|
|
36
|
+
views: {
|
|
37
|
+
type: IAnchorRef["type"];
|
|
38
|
+
url: string;
|
|
39
|
+
/** Public keys the anchor currently lists. Absent when it vetoed or did not answer. */
|
|
40
|
+
publicKeys?: string[];
|
|
41
|
+
error?: string;
|
|
42
|
+
}[];
|
|
43
|
+
};
|
|
44
|
+
/** What the key registry knows, when one is configured and reachable. */
|
|
45
|
+
registry?: {
|
|
46
|
+
url: string;
|
|
47
|
+
/** `undefined` when the registry has never seen this user. */
|
|
48
|
+
keys?: IIdentityRegistryKey[];
|
|
49
|
+
/** Set when the registry could not be reached. */
|
|
50
|
+
error?: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Input for the `rotateKey` tool. */
|
|
54
|
+
export interface IIdentityRotateKeyInput {
|
|
55
|
+
/** `rotation` (default) retires the old key; `compromised` or `lost` revokes it. */
|
|
56
|
+
reason?: KeyRotationReason;
|
|
57
|
+
/**
|
|
58
|
+
* `anchors` (default) writes through an installed writer for each on-record anchor that has
|
|
59
|
+
* one (peers-services accepts that write during an anchor session opened with the recovery
|
|
60
|
+
* email) and polls the rest. `manual` writes nowhere and polls every on-record anchor.
|
|
61
|
+
* Either way the key changes only once every anchor on record lists it.
|
|
62
|
+
*/
|
|
63
|
+
publish?: "anchors" | "manual";
|
|
64
|
+
/**
|
|
65
|
+
* When an anchor on record must be updated by hand (`manual`, or `anchors` with an https
|
|
66
|
+
* anchor on record), `false` returns the document and does not change the key, and `true`
|
|
67
|
+
* waits for every anchor to list the staged key and then rotates.
|
|
68
|
+
*/
|
|
69
|
+
confirm?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** One anchor's answer while publishing a new key. */
|
|
72
|
+
export interface IAnchorPublishOutcome {
|
|
73
|
+
type: IAnchorRef["type"];
|
|
74
|
+
url: string;
|
|
75
|
+
result: "accepted" | "refused";
|
|
76
|
+
}
|
|
77
|
+
/** Output of the `rotateKey` tool. A committed rotation restarts the host shortly after. */
|
|
78
|
+
export interface IIdentityRotateKeyResult {
|
|
79
|
+
oldPublicKey: string;
|
|
80
|
+
newPublicKey: string;
|
|
81
|
+
reason: KeyRotationReason;
|
|
82
|
+
/** True once the local key has changed. A staged rotation is false. */
|
|
83
|
+
committed: boolean;
|
|
84
|
+
restartRequired: boolean;
|
|
85
|
+
rewrappedSecrets: number;
|
|
86
|
+
/** Per-anchor results. Every entry is `accepted` when committed; empty when only staged. */
|
|
87
|
+
anchors: IAnchorPublishOutcome[];
|
|
88
|
+
/** Document to place at the anchors that must be updated by hand, when staged. */
|
|
89
|
+
manualDocument?: IAnchorDocument;
|
|
90
|
+
warnings: string[];
|
|
91
|
+
}
|
|
14
92
|
/**
|
|
15
93
|
* Local-only host contract for device pairing and the signed-in session.
|
|
16
94
|
*
|
|
@@ -36,6 +114,84 @@ export interface IIdentityConsumer extends IContractConsumerBase {
|
|
|
36
114
|
registerWithPeersServices(): Promise<{
|
|
37
115
|
token: string;
|
|
38
116
|
}>;
|
|
117
|
+
getKeyInfo(): Promise<IIdentityKeyInfo>;
|
|
118
|
+
rotateKey(input?: IIdentityRotateKeyInput): Promise<IIdentityRotateKeyResult>;
|
|
119
|
+
getRecoveryEmail(): Promise<{
|
|
120
|
+
email?: string;
|
|
121
|
+
}>;
|
|
122
|
+
bindEmailStart(input: {
|
|
123
|
+
email: string;
|
|
124
|
+
}): Promise<{
|
|
125
|
+
status: string;
|
|
126
|
+
}>;
|
|
127
|
+
bindEmailVerify(input: {
|
|
128
|
+
email: string;
|
|
129
|
+
code: string;
|
|
130
|
+
}): Promise<{
|
|
131
|
+
email: string;
|
|
132
|
+
}>;
|
|
133
|
+
unbindEmail(): Promise<{
|
|
134
|
+
status: string;
|
|
135
|
+
}>;
|
|
136
|
+
startAnchorSession(input?: {
|
|
137
|
+
userId?: string;
|
|
138
|
+
}): Promise<{
|
|
139
|
+
status: string;
|
|
140
|
+
}>;
|
|
141
|
+
confirmAnchorSession(input: {
|
|
142
|
+
code: string;
|
|
143
|
+
userId?: string;
|
|
144
|
+
}): Promise<{
|
|
145
|
+
expiresIn: number;
|
|
146
|
+
}>;
|
|
147
|
+
contestRecovery(input: {
|
|
148
|
+
pendingKey: string;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
status: string;
|
|
151
|
+
}>;
|
|
152
|
+
acceptRecovery(input: {
|
|
153
|
+
userId: string;
|
|
154
|
+
pendingKey: string;
|
|
155
|
+
}): Promise<{
|
|
156
|
+
status: string;
|
|
157
|
+
}>;
|
|
158
|
+
recoverKey(input: {
|
|
159
|
+
userId: string;
|
|
160
|
+
publish?: "anchors" | "manual";
|
|
161
|
+
confirm?: boolean;
|
|
162
|
+
}): Promise<{
|
|
163
|
+
publicKey: string;
|
|
164
|
+
committed: boolean;
|
|
165
|
+
manualDocument?: IAnchorDocument;
|
|
166
|
+
warnings: string[];
|
|
167
|
+
}>;
|
|
168
|
+
setAnchors(input: {
|
|
169
|
+
anchors: IAnchorRef[];
|
|
170
|
+
}): Promise<{
|
|
171
|
+
anchors: IAnchorRef[];
|
|
172
|
+
}>;
|
|
173
|
+
passkeyRegisterStart(input: {
|
|
174
|
+
origin: string;
|
|
175
|
+
}): Promise<{
|
|
176
|
+
options: Record<string, unknown>;
|
|
177
|
+
}>;
|
|
178
|
+
passkeyRegisterFinish(input: {
|
|
179
|
+
response: Record<string, unknown>;
|
|
180
|
+
}): Promise<{
|
|
181
|
+
status: string;
|
|
182
|
+
}>;
|
|
183
|
+
passkeyAuthStart(input: {
|
|
184
|
+
origin: string;
|
|
185
|
+
userId?: string;
|
|
186
|
+
}): Promise<{
|
|
187
|
+
options: Record<string, unknown>;
|
|
188
|
+
}>;
|
|
189
|
+
passkeyAuthFinish(input: {
|
|
190
|
+
response: Record<string, unknown>;
|
|
191
|
+
userId?: string;
|
|
192
|
+
}): Promise<{
|
|
193
|
+
expiresIn: number;
|
|
194
|
+
}>;
|
|
39
195
|
};
|
|
40
196
|
observables: {
|
|
41
197
|
state: Observable<DevicePairingState>;
|