@gryt/core 0.16.2 → 0.17.0
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/mls/deliveryService.fake.js +16 -2
- package/dist/mls/dmDriver.js +68 -4
- package/dist/mls/interfaces.d.ts +15 -1
- package/dist/pairing/approver.js +19 -12
- package/dist/pairing/channel.d.ts +1 -1
- package/dist/pairing/channel.js +4 -2
- package/dist/pairing/historySender.js +1 -1
- package/dist/pairing/historySnapshot.js +5 -1
- package/dist/pairing/newDevice.js +9 -3
- package/dist/reports/report.d.ts +4 -1
- package/dist/reports/report.js +1 -5
- package/package.json +10 -6
|
@@ -264,6 +264,15 @@ export class FakeDeliveryService {
|
|
|
264
264
|
this.welcomes = this.welcomes.filter((w) => !(w.serverUserId === me && w.deviceId === deviceId && welcomeIds.includes(w.welcomeId)));
|
|
265
265
|
return { ok: true, deleted: before - this.welcomes.length };
|
|
266
266
|
},
|
|
267
|
+
requestAdd: async ({ conversationId, deviceId }) => {
|
|
268
|
+
await tick();
|
|
269
|
+
if (!member(conversationId))
|
|
270
|
+
return fail("not_found", "No such conversation.");
|
|
271
|
+
if (!this.isDevice(me, deviceId))
|
|
272
|
+
return fail("unknown_device", "Publish KeyPackages from this device first.");
|
|
273
|
+
this.push(this.conversations.get(conversationId), (d) => d.handleAddRequest({ conversationId, serverUserId: me, deviceId }));
|
|
274
|
+
return { ok: true };
|
|
275
|
+
},
|
|
267
276
|
};
|
|
268
277
|
// socket.io-parser closes the connection on a packet with more than ten binary parts.
|
|
269
278
|
const guarded = {};
|
|
@@ -296,8 +305,13 @@ export class FakeDeliveryService {
|
|
|
296
305
|
return guarded;
|
|
297
306
|
}
|
|
298
307
|
checkParts(socket, event, value) {
|
|
299
|
-
const count = (v) =>
|
|
300
|
-
|
|
308
|
+
const count = (v) => {
|
|
309
|
+
if (v instanceof Uint8Array)
|
|
310
|
+
return 1;
|
|
311
|
+
if (v && typeof v === "object")
|
|
312
|
+
return Object.values(v).reduce((n, x) => n + count(x), 0);
|
|
313
|
+
return 0;
|
|
314
|
+
};
|
|
301
315
|
const parts = count(value);
|
|
302
316
|
this.stats.mostBinaryParts = Math.max(this.stats.mostBinaryParts, parts);
|
|
303
317
|
if (parts > 10) {
|
package/dist/mls/dmDriver.js
CHANGED
|
@@ -9,6 +9,10 @@ const PUBLISH_BATCH = MAX_BINARY_PARTS - 1;
|
|
|
9
9
|
/** The server's cap on files in one message. */
|
|
10
10
|
const MAX_ATTACHMENT_IDS = 10;
|
|
11
11
|
const WELCOME_WAIT_MS = 10_000;
|
|
12
|
+
/** How often a device out of a group asks to be added, unless the person asks. */
|
|
13
|
+
const ASK_AGAIN_MS = 60_000;
|
|
14
|
+
/** A message can beat the Welcome that comes with it, so a missing group is only news after this. */
|
|
15
|
+
const NOT_ADDED_GRACE_MS = 3_000;
|
|
12
16
|
/** The same cap as a message that isn't MLS: after this a send fails and the text goes back. */
|
|
13
17
|
const SEND_GIVE_UP_MS = 5 * 60_000;
|
|
14
18
|
/** A send with no answer goes again after this, or sooner if the connection comes back. */
|
|
@@ -85,6 +89,7 @@ export function createMlsDmDriver(options) {
|
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
const raw = options.transport;
|
|
92
|
+
const rawRequestAdd = raw.requestAdd?.bind(raw);
|
|
88
93
|
const transport = {
|
|
89
94
|
publishKeyPackages: stopOnRemoval((req) => raw.publishKeyPackages(req)),
|
|
90
95
|
claimKeyPackages: stopOnRemoval((req) => raw.claimKeyPackages(req)),
|
|
@@ -96,6 +101,7 @@ export function createMlsDmDriver(options) {
|
|
|
96
101
|
fetchLog: stopOnRemoval((req) => raw.fetchLog(req)),
|
|
97
102
|
sync: stopOnRemoval((req) => raw.sync(req)),
|
|
98
103
|
ackWelcomes: stopOnRemoval((req) => raw.ackWelcomes(req)),
|
|
104
|
+
...(rawRequestAdd ? { requestAdd: stopOnRemoval(rawRequestAdd) } : {}),
|
|
99
105
|
};
|
|
100
106
|
const scope = asIdentityScope(options.scope);
|
|
101
107
|
const trusts = new Map();
|
|
@@ -116,6 +122,11 @@ export function createMlsDmDriver(options) {
|
|
|
116
122
|
const reconciled = new Set();
|
|
117
123
|
const seen = new Set();
|
|
118
124
|
const welcomeWaiters = new Map();
|
|
125
|
+
/** Conversations with a Welcome for this device that it couldn't join yet, most likely an unpinned member. */
|
|
126
|
+
const unjoinable = new Set();
|
|
127
|
+
const asked = new Map();
|
|
128
|
+
/** Groups this device was taken out of here. It doesn't ask its way back into those. */
|
|
129
|
+
const left = new Set();
|
|
119
130
|
const flights = new Set();
|
|
120
131
|
let waitingForServer = false;
|
|
121
132
|
let stopped = false;
|
|
@@ -269,6 +280,7 @@ export function createMlsDmDriver(options) {
|
|
|
269
280
|
const r = await processMlsMessage(open.state, e.data);
|
|
270
281
|
if (r.kind === "handshake" && r.removed) {
|
|
271
282
|
await store.deleteGroup(open.rec.conversationId);
|
|
283
|
+
left.add(open.rec.conversationId);
|
|
272
284
|
events.onGroupLost?.({ conversationId: open.rec.conversationId, reason: "removed" });
|
|
273
285
|
return false;
|
|
274
286
|
}
|
|
@@ -514,12 +526,38 @@ export function createMlsDmDriver(options) {
|
|
|
514
526
|
welcomeWaiters.set(conversationId, [...(welcomeWaiters.get(conversationId) ?? []), done]);
|
|
515
527
|
});
|
|
516
528
|
}
|
|
529
|
+
/* Only a member can add a device, so one that's out asks the members' apps to. An old
|
|
530
|
+
server doesn't answer, which is why nothing waits on it. */
|
|
531
|
+
async function askToBeAdded(conversationId, force = false) {
|
|
532
|
+
events.onGroupLost?.({ conversationId, reason: "not_added" });
|
|
533
|
+
if (!transport.requestAdd || (!force && Date.now() - (asked.get(conversationId) ?? 0) < ASK_AGAIN_MS))
|
|
534
|
+
return;
|
|
535
|
+
asked.set(conversationId, Date.now());
|
|
536
|
+
const r = await transport.requestAdd({ conversationId, deviceId: (await ownDevice()).deviceId });
|
|
537
|
+
if (!r.ok)
|
|
538
|
+
throw refused("Asking to be added", r);
|
|
539
|
+
}
|
|
540
|
+
function notAddedLater(conversationId) {
|
|
541
|
+
setTimeout(() => {
|
|
542
|
+
if (stopped || left.has(conversationId) || unjoinable.has(conversationId))
|
|
543
|
+
return;
|
|
544
|
+
void store
|
|
545
|
+
.loadGroup(conversationId)
|
|
546
|
+
.then((rec) => (rec ? undefined : askToBeAdded(conversationId)))
|
|
547
|
+
.catch(() => undefined);
|
|
548
|
+
}, NOT_ADDED_GRACE_MS);
|
|
549
|
+
}
|
|
517
550
|
async function ensureOpen(conversationId) {
|
|
518
551
|
if ((await run(conversationId, () => openGroup(conversationId))) === "open")
|
|
519
552
|
return;
|
|
520
553
|
await takeWaitingWelcomes();
|
|
521
|
-
if (
|
|
522
|
-
|
|
554
|
+
if (await store.loadGroup(conversationId))
|
|
555
|
+
return;
|
|
556
|
+
if (unjoinable.has(conversationId)) {
|
|
557
|
+
throw new MlsDriverError("peer_unverified", "This device was added, and a member's key isn't pinned here to join with.");
|
|
558
|
+
}
|
|
559
|
+
void askToBeAdded(conversationId).catch(() => undefined);
|
|
560
|
+
await waitForWelcome(conversationId);
|
|
523
561
|
}
|
|
524
562
|
/** "joined", "not_ours" (ack it and forget it), or "retry" (keep it for next time). */
|
|
525
563
|
async function joinFrom(w) {
|
|
@@ -569,12 +607,16 @@ export function createMlsDmDriver(options) {
|
|
|
569
607
|
return "retry";
|
|
570
608
|
const conversationId = w.conversationId;
|
|
571
609
|
const { result, usedLastResort } = await run(conversationId, () => joinFrom(w));
|
|
572
|
-
if (result === "retry")
|
|
610
|
+
if (result === "retry") {
|
|
611
|
+
unjoinable.add(conversationId);
|
|
573
612
|
return "retry";
|
|
613
|
+
}
|
|
574
614
|
await transport.ackWelcomes({ deviceId: d.deviceId, welcomeIds: [w.welcomeId] });
|
|
575
615
|
if (result !== "joined")
|
|
576
616
|
return "done";
|
|
577
617
|
reconciled.delete(conversationId);
|
|
618
|
+
unjoinable.delete(conversationId);
|
|
619
|
+
left.delete(conversationId);
|
|
578
620
|
events.onJoined?.({ conversationId, groupId: w.groupId });
|
|
579
621
|
for (const done of welcomeWaiters.get(conversationId) ?? [])
|
|
580
622
|
done();
|
|
@@ -702,7 +744,13 @@ export function createMlsDmDriver(options) {
|
|
|
702
744
|
async function handleMessage(entry) {
|
|
703
745
|
await run(entry.conversationId, async () => {
|
|
704
746
|
const open = await load(entry.conversationId);
|
|
705
|
-
if (!open
|
|
747
|
+
if (!open) {
|
|
748
|
+
// A message for a group this device isn't in: without this it would show nothing.
|
|
749
|
+
if (entry.kind === "application" && entry.senderDeviceId !== (await ownDevice()).deviceId)
|
|
750
|
+
notAddedLater(entry.conversationId);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (entry.seq <= open.rec.cursor)
|
|
706
754
|
return;
|
|
707
755
|
if (entry.seq === open.rec.cursor + 1 && entry.groupId === open.rec.groupId)
|
|
708
756
|
await applyEntry(open, entry);
|
|
@@ -730,6 +778,20 @@ export function createMlsDmDriver(options) {
|
|
|
730
778
|
});
|
|
731
779
|
}
|
|
732
780
|
}
|
|
781
|
+
/** Adds whatever is missing, as a send would. Who asked only says which group to look at. */
|
|
782
|
+
async function handleAddRequest(push) {
|
|
783
|
+
if (push.deviceId === (await ownDevice()).deviceId)
|
|
784
|
+
return;
|
|
785
|
+
await run(push.conversationId, async () => {
|
|
786
|
+
const open = await load(push.conversationId);
|
|
787
|
+
if (!open || !(await catchUp(open)))
|
|
788
|
+
return;
|
|
789
|
+
const peer = (await peopleIn(push.conversationId, open.state)).find((p) => !!p && p !== self);
|
|
790
|
+
if (!peer || (push.serverUserId !== self && push.serverUserId !== peer))
|
|
791
|
+
return;
|
|
792
|
+
await reconcile(open, peer, true);
|
|
793
|
+
});
|
|
794
|
+
}
|
|
733
795
|
/** Names from certificates under your own person key: this device's, your leaves in each group,
|
|
734
796
|
then the last-resort KeyPackage the server lists with each of your devices. */
|
|
735
797
|
async function ownDeviceNames(d, listed) {
|
|
@@ -921,6 +983,8 @@ export function createMlsDmDriver(options) {
|
|
|
921
983
|
handleMessage,
|
|
922
984
|
handleWelcome,
|
|
923
985
|
handleDevicesChanged,
|
|
986
|
+
handleAddRequest,
|
|
987
|
+
requestAdd: (conversationId) => askToBeAdded(conversationId, true),
|
|
924
988
|
ownDevices,
|
|
925
989
|
async removeOwnDevice(deviceId) {
|
|
926
990
|
const r = await transport.removeDevice({ deviceId });
|
package/dist/mls/interfaces.d.ts
CHANGED
|
@@ -140,6 +140,12 @@ export interface MlsTransport {
|
|
|
140
140
|
}): Promise<MlsReply<{
|
|
141
141
|
deleted: number;
|
|
142
142
|
}>>;
|
|
143
|
+
/** This device isn't in that conversation's group: the server asks the members' apps to add it.
|
|
144
|
+
Optional, since a server from before GRYT-1858 doesn't take it. */
|
|
145
|
+
requestAdd?(req: {
|
|
146
|
+
conversationId: string;
|
|
147
|
+
deviceId: string;
|
|
148
|
+
}): Promise<MlsReply<object>>;
|
|
143
149
|
}
|
|
144
150
|
/** `server:info.mls`. Absent means the server can't do MLS. */
|
|
145
151
|
export interface MlsServerCapability {
|
|
@@ -237,7 +243,7 @@ export interface MlsDmEvents {
|
|
|
237
243
|
/** This device is out of the group: removed, or its state couldn't keep up. */
|
|
238
244
|
onGroupLost?(info: {
|
|
239
245
|
conversationId: string;
|
|
240
|
-
reason: "removed" | "out_of_sync" | "gap";
|
|
246
|
+
reason: "removed" | "out_of_sync" | "gap" | "not_added";
|
|
241
247
|
}): void;
|
|
242
248
|
onJoined?(info: {
|
|
243
249
|
conversationId: string;
|
|
@@ -335,6 +341,14 @@ export interface MlsDmDriver {
|
|
|
335
341
|
handleDevicesChanged(push: {
|
|
336
342
|
serverUserId: string;
|
|
337
343
|
}): Promise<void>;
|
|
344
|
+
/** `mls:add:requested`: a device of somebody in the conversation asked to be added. */
|
|
345
|
+
handleAddRequest(push: {
|
|
346
|
+
conversationId: string;
|
|
347
|
+
serverUserId: string;
|
|
348
|
+
deviceId: string;
|
|
349
|
+
}): Promise<void>;
|
|
350
|
+
/** Asks the other members' apps to add this device, as the driver does when it finds it's out. */
|
|
351
|
+
requestAdd(conversationId: string): Promise<void>;
|
|
338
352
|
/** Oldest first, as the server lists them. */
|
|
339
353
|
ownDevices(): Promise<MlsOwnDevice[]>;
|
|
340
354
|
/** Your own devices only. Every group you share drops it on the next pass. */
|
package/dist/pairing/approver.js
CHANGED
|
@@ -54,6 +54,12 @@ export function createApproverPairing(options) {
|
|
|
54
54
|
await closeQuietly();
|
|
55
55
|
};
|
|
56
56
|
const fail = (e) => (abort.signal.aborted ? undefined : end(reasonFor(e)));
|
|
57
|
+
// Only reached past "confirming", which claim() enters with all three set.
|
|
58
|
+
const confirmed = () => {
|
|
59
|
+
if (!current?.session || !device)
|
|
60
|
+
throw new Error("Nothing confirmed yet.");
|
|
61
|
+
return { ...current, session: current.session, device };
|
|
62
|
+
};
|
|
57
63
|
async function claim(input) {
|
|
58
64
|
let by;
|
|
59
65
|
let scanned;
|
|
@@ -122,7 +128,7 @@ export function createApproverPairing(options) {
|
|
|
122
128
|
const res = await fetch(`${issuer.replace(/\/+$/, "")}/gryt-pairing/approve`, {
|
|
123
129
|
method: "POST",
|
|
124
130
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
125
|
-
body: JSON.stringify({ user_code: userCode, binding:
|
|
131
|
+
body: JSON.stringify({ user_code: userCode, binding: confirmed().session.keycloakNonce }),
|
|
126
132
|
}).catch(() => {
|
|
127
133
|
throw new PairingEnded("approve:network");
|
|
128
134
|
});
|
|
@@ -143,7 +149,7 @@ export function createApproverPairing(options) {
|
|
|
143
149
|
for (let tries = 0;; tries++) {
|
|
144
150
|
try {
|
|
145
151
|
added[host] = await driver.addOwnDevice(deviceId, {
|
|
146
|
-
onProgress: ({ done, total }) => set({ phase: "adding", device: device, host, done, total }),
|
|
152
|
+
onProgress: ({ done, total }) => set({ phase: "adding", device: confirmed().device, host, done, total }),
|
|
147
153
|
});
|
|
148
154
|
break;
|
|
149
155
|
}
|
|
@@ -168,7 +174,7 @@ export function createApproverPairing(options) {
|
|
|
168
174
|
return snap;
|
|
169
175
|
}
|
|
170
176
|
function startHistory(archive) {
|
|
171
|
-
const { id, token, session } =
|
|
177
|
+
const { id, token, session } = confirmed();
|
|
172
178
|
return createHistorySender({
|
|
173
179
|
relay, id, token, archive, clock, signal: abort.signal,
|
|
174
180
|
post: (message) => relay.post(id, token, { type: "sealed", body: sealJson(session, message) }),
|
|
@@ -176,19 +182,20 @@ export function createApproverPairing(options) {
|
|
|
176
182
|
});
|
|
177
183
|
}
|
|
178
184
|
async function approve(envelope, accessToken) {
|
|
179
|
-
const { id, token, box, session } =
|
|
180
|
-
set({ phase: envelope.account ? "signing_in" : "waiting_ready", device
|
|
185
|
+
const { id, token, box, session, device } = confirmed();
|
|
186
|
+
set({ phase: envelope.account ? "signing_in" : "waiting_ready", device });
|
|
181
187
|
let sender = null;
|
|
182
188
|
let snap = new Map();
|
|
189
|
+
let sealed = envelope;
|
|
183
190
|
if (options.history) {
|
|
184
191
|
// Recording starts before the positions are read, so nothing lands between the two unseen.
|
|
185
192
|
tail.start();
|
|
186
193
|
snap = await positions(envelope);
|
|
187
194
|
sender = startHistory(options.history);
|
|
188
195
|
const chunks = await sender.forEnvelope(Math.min(clock.now() + ENVELOPE_BUDGET_MS, deadline - 10_000));
|
|
189
|
-
|
|
196
|
+
sealed = { ...envelope, history: { key: sender.key, manifest: { v: 1, chunks } } };
|
|
190
197
|
}
|
|
191
|
-
await relay.post(id, token, { type: "sealed", body: base64Url(session.seal(encodePairingEnvelope(
|
|
198
|
+
await relay.post(id, token, { type: "sealed", body: base64Url(session.seal(encodePairingEnvelope(sealed))) });
|
|
192
199
|
if (envelope.account && accessToken) {
|
|
193
200
|
const message = await box.next(abort.signal, clock.now() + approvalMs);
|
|
194
201
|
if (!message)
|
|
@@ -196,27 +203,27 @@ export function createApproverPairing(options) {
|
|
|
196
203
|
const userCode = String(openJson(session, message, "user_code").userCode ?? "");
|
|
197
204
|
if (!(await approveExtension(envelope.account.issuer, userCode, accessToken))) {
|
|
198
205
|
const url = `${envelope.account.issuer.replace(/\/+$/, "")}/device?user_code=${encodeURIComponent(userCode)}`;
|
|
199
|
-
set({ phase: "browser", device
|
|
206
|
+
set({ phase: "browser", device, url });
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
if (state.phase === "signing_in")
|
|
203
|
-
set({ phase: "waiting_ready", device
|
|
210
|
+
set({ phase: "waiting_ready", device });
|
|
204
211
|
const ready = openJson(session, await box.next(abort.signal), "ready");
|
|
205
212
|
const list = Array.isArray(ready.devices) ? ready.devices : [];
|
|
206
213
|
const named = list.filter((d) => typeof d?.host === "string" && typeof d?.deviceId === "string");
|
|
207
214
|
sender?.startPaging();
|
|
208
215
|
const added = await addEverywhere(named);
|
|
209
216
|
if (!sender) {
|
|
210
|
-
set({ phase: "done", device
|
|
217
|
+
set({ phase: "done", device, added });
|
|
211
218
|
session.close();
|
|
212
219
|
return void (await closeQuietly());
|
|
213
220
|
}
|
|
214
|
-
set({ phase: "sending", device
|
|
221
|
+
set({ phase: "sending", device, added });
|
|
215
222
|
await sender.send(tail.take(snap, added), false);
|
|
216
223
|
await clock.sleep(options.lateWindowMs ?? HISTORY_LATE_WINDOW_MS, abort.signal);
|
|
217
224
|
await sender.send(tail.take(snap, added), true);
|
|
218
225
|
// N closes the session once it has every chunk; closing here would delete them.
|
|
219
|
-
set({ phase: "done", device
|
|
226
|
+
set({ phase: "done", device, added });
|
|
220
227
|
session.close();
|
|
221
228
|
}
|
|
222
229
|
return {
|
|
@@ -8,7 +8,7 @@ export declare class PairingEnded extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
export declare const systemClock: PairingClock;
|
|
10
10
|
export declare function reasonFor(error: unknown): PairingEndReason;
|
|
11
|
-
export declare function sealJson(session: PairingSession, value:
|
|
11
|
+
export declare function sealJson(session: PairingSession, value: object): string;
|
|
12
12
|
/** The next sealed message as JSON with a `t`, or a throw that ends the pairing as tampered. */
|
|
13
13
|
export declare function openJson(session: PairingSession, message: RelayMessage | null, t?: string): Record<string, unknown>;
|
|
14
14
|
/** The other side's messages in order, long-polling the relay. `next` gives null once `deadline` passes. */
|
package/dist/pairing/channel.js
CHANGED
|
@@ -66,8 +66,10 @@ export function mailbox(relay, id, token, clock) {
|
|
|
66
66
|
return null;
|
|
67
67
|
try {
|
|
68
68
|
for (const m of await relay.poll(id, token, after, Math.min(25, Math.ceil(left / 1000)), signal)) {
|
|
69
|
-
if (m.seq > after)
|
|
70
|
-
|
|
69
|
+
if (m.seq > after) {
|
|
70
|
+
queue.push(m);
|
|
71
|
+
after = m.seq;
|
|
72
|
+
}
|
|
71
73
|
}
|
|
72
74
|
failures = 0;
|
|
73
75
|
}
|
|
@@ -130,7 +130,7 @@ export function createHistorySender(options) {
|
|
|
130
130
|
const listed = unlisted.splice(0, HISTORY_ENVELOPE_ENTRIES);
|
|
131
131
|
progress.listed += listed.length;
|
|
132
132
|
report();
|
|
133
|
-
return listed.map(({ n
|
|
133
|
+
return listed.map(({ n, ...entry }) => entry);
|
|
134
134
|
},
|
|
135
135
|
/** After N's "ready": list what's waiting, and keep listing as pages fill. */
|
|
136
136
|
startPaging() {
|
|
@@ -2,7 +2,11 @@ const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
2
2
|
export const dayOf = (sentAt) => Math.floor(sentAt / DAY_MS);
|
|
3
3
|
/** Negative when `a` is newer: by time, then by message id, the same order the archive pages in. */
|
|
4
4
|
export function newerFirst(a, b) {
|
|
5
|
-
|
|
5
|
+
if (a.sentAt !== b.sentAt)
|
|
6
|
+
return b.sentAt - a.sentAt;
|
|
7
|
+
if (a.messageId === b.messageId)
|
|
8
|
+
return 0;
|
|
9
|
+
return a.messageId < b.messageId ? 1 : -1;
|
|
6
10
|
}
|
|
7
11
|
/** A's archive one UTC day at a time, newest day first across every conversation and server. */
|
|
8
12
|
export async function* archiveByDay(archive, pageSize = 500) {
|
|
@@ -90,8 +90,14 @@ export function createNewDevicePairing(options) {
|
|
|
90
90
|
throw new PairingEnded(e instanceof Error && /newer version/.test(e.message) ? "newer_version" : "tampered");
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
// attempt() sets both before signIn, run's history step or linked can run.
|
|
94
|
+
const paired = () => {
|
|
95
|
+
if (!current?.session)
|
|
96
|
+
throw new Error("No pairing session yet.");
|
|
97
|
+
return { ...current, session: current.session };
|
|
98
|
+
};
|
|
93
99
|
async function signIn(account, from) {
|
|
94
|
-
const { id, token, box, session } =
|
|
100
|
+
const { id, token, box, session } = paired();
|
|
95
101
|
set({ phase: "signing_in", username: account.username, from });
|
|
96
102
|
const verifier = base64Url(randomBytes(32));
|
|
97
103
|
const auth = await oidc.deviceAuthorization({
|
|
@@ -151,7 +157,7 @@ export function createNewDevicePairing(options) {
|
|
|
151
157
|
const tokens = envelope.account ? await signIn(envelope.account, envelope.from) : null;
|
|
152
158
|
await storage.commit(envelope, tokens);
|
|
153
159
|
if (envelope.history && options.history) {
|
|
154
|
-
const { id, token } =
|
|
160
|
+
const { id, token } = paired();
|
|
155
161
|
receiver = createHistoryReceiver({
|
|
156
162
|
relay, id, token, key: envelope.history.key, sink: options.history, clock, signal: abort.signal, onProgress: setHistory,
|
|
157
163
|
});
|
|
@@ -166,7 +172,7 @@ export function createNewDevicePairing(options) {
|
|
|
166
172
|
}
|
|
167
173
|
/** Reads A's history messages until every chunk is in, or A closing is the finish. */
|
|
168
174
|
async function linked(from) {
|
|
169
|
-
const { box, session } =
|
|
175
|
+
const { box, session } = paired();
|
|
170
176
|
const stop = new AbortController();
|
|
171
177
|
const onAbort = () => stop.abort();
|
|
172
178
|
abort.signal.addEventListener("abort", onAbort, { once: true });
|
package/dist/reports/report.d.ts
CHANGED
|
@@ -45,7 +45,10 @@ export interface Report {
|
|
|
45
45
|
};
|
|
46
46
|
/** The tail of the app's own log, where it keeps one. */
|
|
47
47
|
logs?: string[];
|
|
48
|
-
extra?:
|
|
48
|
+
extra?: {
|
|
49
|
+
embeddedServer?: boolean;
|
|
50
|
+
embeddedServerVersion?: string;
|
|
51
|
+
};
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* What an app knows about itself when somebody opens the form. Passed in rather than read
|
package/dist/reports/report.js
CHANGED
|
@@ -120,11 +120,7 @@ export function describeAttached(report) {
|
|
|
120
120
|
? `v${report.app.version}${report.app.build ? ` (${report.app.build})` : ""}`
|
|
121
121
|
: undefined);
|
|
122
122
|
add("Channel", report.app?.channel);
|
|
123
|
-
add("System", device?.platform
|
|
124
|
-
? `${platformLabel(device.platform)} ${device.osVersion}`
|
|
125
|
-
: device?.platform
|
|
126
|
-
? platformLabel(device.platform)
|
|
127
|
-
: undefined);
|
|
123
|
+
add("System", device?.platform ? [platformLabel(device.platform), device.osVersion].filter(Boolean).join(" ") : undefined);
|
|
128
124
|
add("Simulator", device?.isEmulator ? "yes" : undefined);
|
|
129
125
|
add("Electron", report.runtime?.electronVersion);
|
|
130
126
|
add("Chrome", report.runtime?.chromeVersion);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gryt/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Gryt's shared application logic: one implementation of the things the desktop and mobile apps both do.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -24,16 +24,20 @@
|
|
|
24
24
|
"dist"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/add-esm-extensions.
|
|
28
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
-
"
|
|
27
|
+
"build": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node scripts/add-esm-extensions.ts",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p scripts",
|
|
29
|
+
"lint": "eslint --flag unstable_native_nodejs_ts_config .",
|
|
30
|
+
"test:comment-length": "node scripts/check-comment-length.ts",
|
|
30
31
|
"test": "node --test \"src/**/*.test.ts\"",
|
|
31
|
-
"check-surface": "node scripts/check-public-surface.
|
|
32
|
+
"check-surface": "node scripts/check-public-surface.ts",
|
|
32
33
|
"prepublishOnly": "npm run build && npm run test && npm run check-surface"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^9.39.5",
|
|
35
37
|
"@types/node": "^20.19.33",
|
|
36
|
-
"
|
|
38
|
+
"eslint": "^9.39.5",
|
|
39
|
+
"typescript": "^5.9.3",
|
|
40
|
+
"typescript-eslint": "^8.71.2"
|
|
37
41
|
},
|
|
38
42
|
"publishConfig": {
|
|
39
43
|
"access": "public"
|