@vex-chat/libvex 6.1.8 → 6.2.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/Client.d.ts +126 -10
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +112 -1
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +64 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +27 -1
- package/dist/codecs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +272 -12
- package/src/codecs.ts +35 -0
- package/src/index.ts +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
6
|
export { Client, DeviceApprovalRequiredError } from "./Client.js";
|
|
7
|
-
export type { Channel, Channels, ClientEvents, ClientOptions, Device, DeviceRegistrationResult, Devices, Emojis, FileProgress, FileRes, Files, Invites, Keys, Me, Message, Messages, Moderation, PendingDeviceApprovalStatus, PendingDeviceRegistration, PendingDeviceRequest, Permission, Permissions, Server, Servers, Session, Sessions, User, Users, VexFile, } from "./Client.js";
|
|
7
|
+
export type { Channel, Channels, ClientEvents, ClientOptions, Device, DeviceRegistrationResult, Devices, Emojis, FileProgress, FileRes, Files, Invites, Keys, Me, Message, Messages, Moderation, Passkeys, PendingDeviceApprovalStatus, PendingDeviceRegistration, PendingDeviceRequest, Permission, Permissions, Server, Servers, Session, Sessions, User, Users, VexFile, } from "./Client.js";
|
|
8
8
|
export { createCodec, msgpack } from "./codec.js";
|
|
9
9
|
export type { Storage } from "./Storage.js";
|
|
10
10
|
export type { KeyPair, KeyStore, PreKeysCrypto, SessionCrypto, StoredCredentials, UnsavedPreKey, } from "./types/index.js";
|
|
11
|
-
export type { Invite } from "@vex-chat/types";
|
|
11
|
+
export type { Invite, Passkey } from "@vex-chat/types";
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACR,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,MAAM,EACN,wBAAwB,EACxB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,OAAO,EACP,KAAK,EACL,OAAO,EACP,IAAI,EACJ,EAAE,EACF,OAAO,EACP,QAAQ,EACR,UAAU,EACV,2BAA2B,EAC3B,yBAAyB,EACzB,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,MAAM,EACN,OAAO,EACP,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,OAAO,GACV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EACR,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,aAAa,GAChB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAClE,YAAY,EACR,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,MAAM,EACN,wBAAwB,EACxB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,OAAO,EACP,KAAK,EACL,OAAO,EACP,IAAI,EACJ,EAAE,EACF,OAAO,EACP,QAAQ,EACR,UAAU,EACV,QAAQ,EACR,2BAA2B,EAC3B,yBAAyB,EACzB,oBAAoB,EACpB,UAAU,EACV,WAAW,EACX,MAAM,EACN,OAAO,EACP,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,OAAO,GACV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAClD,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,YAAY,EACR,OAAO,EACP,QAAQ,EACR,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,aAAa,GAChB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAiClE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/libvex",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "Library for communicating with xchat server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"msgpackr": "^1.11.9",
|
|
94
94
|
"uuid": "^14.0.0",
|
|
95
95
|
"zod": "^4.3.6",
|
|
96
|
-
"@vex-chat/crypto": "^
|
|
97
|
-
"@vex-chat/types": "^3.
|
|
96
|
+
"@vex-chat/crypto": "^5.0.0",
|
|
97
|
+
"@vex-chat/types": "^3.2.0"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"better-sqlite3": ">=11.0.0"
|
package/src/Client.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
KeyBundle,
|
|
27
27
|
MailWS,
|
|
28
28
|
NotifyMsg,
|
|
29
|
+
Passkey,
|
|
29
30
|
Permission,
|
|
30
31
|
PreKeysSQL,
|
|
31
32
|
PreKeysWS,
|
|
@@ -112,10 +113,18 @@ export class DeviceApprovalRequiredError extends Error {
|
|
|
112
113
|
public readonly challenge: string;
|
|
113
114
|
public readonly expiresAt: string;
|
|
114
115
|
public readonly requestID: string;
|
|
116
|
+
/**
|
|
117
|
+
* Existing user's ID, when the server provides it. Lets the new
|
|
118
|
+
* (unauthenticated) device fetch the public avatar and show an
|
|
119
|
+
* "is this you?" confirmation before continuing the approval
|
|
120
|
+
* dance. Optional because older servers don't return it.
|
|
121
|
+
*/
|
|
122
|
+
public readonly userID: null | string;
|
|
115
123
|
constructor(args: {
|
|
116
124
|
challenge: string;
|
|
117
125
|
expiresAt: string;
|
|
118
126
|
requestID: string;
|
|
127
|
+
userID?: null | string;
|
|
119
128
|
}) {
|
|
120
129
|
super(
|
|
121
130
|
"Device registration requires approval from an existing device. requestID=" +
|
|
@@ -125,6 +134,7 @@ export class DeviceApprovalRequiredError extends Error {
|
|
|
125
134
|
this.challenge = args.challenge;
|
|
126
135
|
this.expiresAt = args.expiresAt;
|
|
127
136
|
this.requestID = args.requestID;
|
|
137
|
+
this.userID = args.userID ?? null;
|
|
128
138
|
}
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -249,6 +259,10 @@ import {
|
|
|
249
259
|
InviteCodec,
|
|
250
260
|
KeyBundleCodec,
|
|
251
261
|
OtkCountCodec,
|
|
262
|
+
PasskeyArrayCodec,
|
|
263
|
+
PasskeyAuthFinishResponseCodec,
|
|
264
|
+
PasskeyCodec,
|
|
265
|
+
PasskeyOptionsCodec,
|
|
252
266
|
PendingDeviceRequestArrayCodec,
|
|
253
267
|
PendingDeviceRequestCodec,
|
|
254
268
|
PermissionArrayCodec,
|
|
@@ -307,6 +321,14 @@ export interface Channels {
|
|
|
307
321
|
*/
|
|
308
322
|
export type { Device } from "@vex-chat/types";
|
|
309
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Public passkey record returned by `client.passkeys.list()` and
|
|
326
|
+
* `client.passkeys.finishRegistration()`. Server-private fields
|
|
327
|
+
* (credential ID, public key, COSE algorithm, signature counter) are
|
|
328
|
+
* never exposed.
|
|
329
|
+
*/
|
|
330
|
+
export type { Passkey } from "@vex-chat/types";
|
|
331
|
+
|
|
310
332
|
/**
|
|
311
333
|
* ClientOptions are the options you can pass into the client.
|
|
312
334
|
*/
|
|
@@ -349,6 +371,8 @@ export interface Devices {
|
|
|
349
371
|
delete: (deviceID: string) => Promise<void>;
|
|
350
372
|
/** Fetches one pending registration request by ID for the current user. */
|
|
351
373
|
getRequest: (requestID: string) => Promise<null | PendingDeviceRequest>;
|
|
374
|
+
/** Lists every device belonging to the current account. */
|
|
375
|
+
list: () => Promise<Device[]>;
|
|
352
376
|
/** Lists pending/processed registration requests for the current user. */
|
|
353
377
|
listRequests: () => Promise<PendingDeviceRequest[]>;
|
|
354
378
|
/**
|
|
@@ -429,6 +453,16 @@ export interface FileProgress {
|
|
|
429
453
|
*/
|
|
430
454
|
export type FileRes = FileResponse;
|
|
431
455
|
|
|
456
|
+
/**
|
|
457
|
+
* @ignore
|
|
458
|
+
*/
|
|
459
|
+
export interface Files {
|
|
460
|
+
/** Uploads and encrypts a file. */
|
|
461
|
+
create: (file: Uint8Array) => Promise<[FileSQL, string]>;
|
|
462
|
+
/** Downloads and decrypts a file using a file ID and key. */
|
|
463
|
+
retrieve: (fileID: string, key: string) => Promise<FileResponse | null>;
|
|
464
|
+
}
|
|
465
|
+
|
|
432
466
|
/**
|
|
433
467
|
* Channel is a chat channel on a server.
|
|
434
468
|
*
|
|
@@ -449,16 +483,6 @@ export type { Channel } from "@vex-chat/types";
|
|
|
449
483
|
*/
|
|
450
484
|
export type { Server } from "@vex-chat/types";
|
|
451
485
|
|
|
452
|
-
/**
|
|
453
|
-
* @ignore
|
|
454
|
-
*/
|
|
455
|
-
export interface Files {
|
|
456
|
-
/** Uploads and encrypts a file. */
|
|
457
|
-
create: (file: Uint8Array) => Promise<[FileSQL, string]>;
|
|
458
|
-
/** Downloads and decrypts a file using a file ID and key. */
|
|
459
|
-
retrieve: (fileID: string, key: string) => Promise<FileResponse | null>;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
486
|
/**
|
|
463
487
|
* @ignore
|
|
464
488
|
*/
|
|
@@ -524,6 +548,65 @@ export interface Message {
|
|
|
524
548
|
timestamp: string;
|
|
525
549
|
}
|
|
526
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Begin/finish handshakes for a passkey (WebAuthn) ceremony plus the
|
|
553
|
+
* passkey-only admin/recovery surface. The host application (a
|
|
554
|
+
* browser, Tauri webview, etc.) is responsible for invoking
|
|
555
|
+
* `navigator.credentials.create()` / `.get()` itself (e.g. via
|
|
556
|
+
* `@simplewebauthn/browser`) using the `options` returned from
|
|
557
|
+
* `begin*`, and then handing the resulting `RegistrationResponseJSON`
|
|
558
|
+
* / `AuthenticationResponseJSON` to `finish*`.
|
|
559
|
+
*
|
|
560
|
+
* @public
|
|
561
|
+
*/
|
|
562
|
+
export interface Passkeys {
|
|
563
|
+
/** Approves a pending device-enrollment request using the passkey session. */
|
|
564
|
+
approveDeviceRequest: (requestID: string) => Promise<Device>;
|
|
565
|
+
/** Begin a public passkey authentication ceremony for `username`. */
|
|
566
|
+
beginAuthentication: (username: string) => Promise<{
|
|
567
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebAuthn options shape varies per simplewebauthn version
|
|
568
|
+
options: any;
|
|
569
|
+
requestID: string;
|
|
570
|
+
}>;
|
|
571
|
+
/** Begin adding a new passkey to the currently authenticated account. */
|
|
572
|
+
beginRegistration: (name: string) => Promise<{
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebAuthn options shape varies per simplewebauthn version
|
|
574
|
+
options: any;
|
|
575
|
+
requestID: string;
|
|
576
|
+
}>;
|
|
577
|
+
/** Remove a passkey from the account. */
|
|
578
|
+
delete: (passkeyID: string) => Promise<void>;
|
|
579
|
+
/** Delete one of the account's devices using the passkey session. */
|
|
580
|
+
deleteDevice: (deviceID: string) => Promise<void>;
|
|
581
|
+
/**
|
|
582
|
+
* Finish the public passkey authentication ceremony with the
|
|
583
|
+
* assertion produced by the host. On success the client is
|
|
584
|
+
* placed in passkey-only mode: the bearer is the passkey JWT,
|
|
585
|
+
* device-only flows (mail, etc.) will not work, and the
|
|
586
|
+
* `client.passkeys.*` admin methods become available.
|
|
587
|
+
*/
|
|
588
|
+
finishAuthentication: (args: {
|
|
589
|
+
requestID: string;
|
|
590
|
+
response: Record<string, unknown>;
|
|
591
|
+
}) => Promise<{
|
|
592
|
+
passkeyID: string;
|
|
593
|
+
token: string;
|
|
594
|
+
user: User;
|
|
595
|
+
}>;
|
|
596
|
+
/** Finish adding a passkey to the currently authenticated account. */
|
|
597
|
+
finishRegistration: (args: {
|
|
598
|
+
name: string;
|
|
599
|
+
requestID: string;
|
|
600
|
+
response: Record<string, unknown>;
|
|
601
|
+
}) => Promise<Passkey>;
|
|
602
|
+
/** List the account's passkeys (public shape only — no key material). */
|
|
603
|
+
list: () => Promise<Passkey[]>;
|
|
604
|
+
/** List all of the account's devices using the passkey session. */
|
|
605
|
+
listDevices: () => Promise<Device[]>;
|
|
606
|
+
/** Reject a pending device-enrollment request using the passkey session. */
|
|
607
|
+
rejectDeviceRequest: (requestID: string) => Promise<void>;
|
|
608
|
+
}
|
|
609
|
+
|
|
527
610
|
export type PendingDeviceApprovalStatus =
|
|
528
611
|
| "approved"
|
|
529
612
|
| "expired"
|
|
@@ -535,6 +618,13 @@ export interface PendingDeviceRegistration {
|
|
|
535
618
|
expiresAt: string;
|
|
536
619
|
requestID: string;
|
|
537
620
|
status: "pending_approval";
|
|
621
|
+
/**
|
|
622
|
+
* Existing user's ID. Optional for backward compat with older
|
|
623
|
+
* servers that don't include it; when present, the new device can
|
|
624
|
+
* fetch the public avatar from `/avatar/:userID` (no auth required)
|
|
625
|
+
* to power an "is this you?" confirmation.
|
|
626
|
+
*/
|
|
627
|
+
userID?: string | undefined;
|
|
538
628
|
}
|
|
539
629
|
|
|
540
630
|
export interface PendingDeviceRequest {
|
|
@@ -880,6 +970,7 @@ export class Client {
|
|
|
880
970
|
approveRequest: this.approveDeviceRequest.bind(this),
|
|
881
971
|
delete: this.deleteDevice.bind(this),
|
|
882
972
|
getRequest: this.getDeviceRegistrationRequest.bind(this),
|
|
973
|
+
list: this.listDevices.bind(this),
|
|
883
974
|
listRequests: this.listDeviceRegistrationRequests.bind(this),
|
|
884
975
|
pollPendingRegistration: this.pollPendingDeviceRegistration.bind(this),
|
|
885
976
|
register: this.registerDevice.bind(this),
|
|
@@ -953,6 +1044,7 @@ export class Client {
|
|
|
953
1044
|
*/
|
|
954
1045
|
user: this.getUser.bind(this),
|
|
955
1046
|
};
|
|
1047
|
+
|
|
956
1048
|
/**
|
|
957
1049
|
* Message operations (direct and group).
|
|
958
1050
|
*
|
|
@@ -993,7 +1085,6 @@ export class Client {
|
|
|
993
1085
|
*/
|
|
994
1086
|
send: this.sendMessage.bind(this),
|
|
995
1087
|
};
|
|
996
|
-
|
|
997
1088
|
/**
|
|
998
1089
|
* Server moderation helper methods.
|
|
999
1090
|
*/
|
|
@@ -1002,6 +1093,31 @@ export class Client {
|
|
|
1002
1093
|
kick: this.kickUser.bind(this),
|
|
1003
1094
|
};
|
|
1004
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Passkey ("recovery credential") methods.
|
|
1098
|
+
*
|
|
1099
|
+
* Passkeys are an account-bound second-class credential that can
|
|
1100
|
+
* authenticate the owning user, list devices, delete devices, and
|
|
1101
|
+
* approve/reject pending device-enrollment requests — i.e.
|
|
1102
|
+
* provisioning + recovery. They cannot send/decrypt mail.
|
|
1103
|
+
*
|
|
1104
|
+
* The host app drives the WebAuthn ceremony (e.g. via
|
|
1105
|
+
* `@simplewebauthn/browser`) and hands the JSON response to
|
|
1106
|
+
* `finish*`.
|
|
1107
|
+
*/
|
|
1108
|
+
public passkeys: Passkeys = {
|
|
1109
|
+
approveDeviceRequest: this.passkeyApproveDeviceRequest.bind(this),
|
|
1110
|
+
beginAuthentication: this.beginPasskeyAuthentication.bind(this),
|
|
1111
|
+
beginRegistration: this.beginPasskeyRegistration.bind(this),
|
|
1112
|
+
delete: this.deletePasskey.bind(this),
|
|
1113
|
+
deleteDevice: this.passkeyDeleteDevice.bind(this),
|
|
1114
|
+
finishAuthentication: this.finishPasskeyAuthentication.bind(this),
|
|
1115
|
+
finishRegistration: this.finishPasskeyRegistration.bind(this),
|
|
1116
|
+
list: this.listPasskeys.bind(this),
|
|
1117
|
+
listDevices: this.passkeyListDevices.bind(this),
|
|
1118
|
+
rejectDeviceRequest: this.passkeyRejectDeviceRequest.bind(this),
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1005
1121
|
/**
|
|
1006
1122
|
* Permission-management methods for the current user.
|
|
1007
1123
|
*/
|
|
@@ -1848,6 +1964,7 @@ export class Client {
|
|
|
1848
1964
|
challenge: pendingApproval.challenge,
|
|
1849
1965
|
expiresAt: pendingApproval.expiresAt,
|
|
1850
1966
|
requestID: pendingApproval.requestID,
|
|
1967
|
+
userID: pendingApproval.userID ?? null,
|
|
1851
1968
|
}),
|
|
1852
1969
|
];
|
|
1853
1970
|
}
|
|
@@ -1964,6 +2081,33 @@ export class Client {
|
|
|
1964
2081
|
return decodeAxios(DeviceCodec, response.data);
|
|
1965
2082
|
}
|
|
1966
2083
|
|
|
2084
|
+
private async beginPasskeyAuthentication(username: string): Promise<{
|
|
2085
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebAuthn options shape varies per simplewebauthn version
|
|
2086
|
+
options: any;
|
|
2087
|
+
requestID: string;
|
|
2088
|
+
}> {
|
|
2089
|
+
const response = await this.http.post(
|
|
2090
|
+
this.getHost() + "/auth/passkey/begin",
|
|
2091
|
+
msgpack.encode({ username }),
|
|
2092
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
2093
|
+
);
|
|
2094
|
+
return decodeAxios(PasskeyOptionsCodec, response.data);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
private async beginPasskeyRegistration(name: string): Promise<{
|
|
2098
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebAuthn options shape varies per simplewebauthn version
|
|
2099
|
+
options: any;
|
|
2100
|
+
requestID: string;
|
|
2101
|
+
}> {
|
|
2102
|
+
const userID = this.getUser().userID;
|
|
2103
|
+
const response = await this.http.post(
|
|
2104
|
+
this.getHost() + "/user/" + userID + "/passkeys/register/begin",
|
|
2105
|
+
msgpack.encode({ name }),
|
|
2106
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
2107
|
+
);
|
|
2108
|
+
return decodeAxios(PasskeyOptionsCodec, response.data);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1967
2111
|
private censorPreKey(preKey: PreKeysSQL): PreKeysWS {
|
|
1968
2112
|
if (!preKey.index) {
|
|
1969
2113
|
throw new Error("Key index is required.");
|
|
@@ -2320,6 +2464,13 @@ export class Client {
|
|
|
2320
2464
|
await this.database.deleteHistory(channelOrUserID);
|
|
2321
2465
|
}
|
|
2322
2466
|
|
|
2467
|
+
private async deletePasskey(passkeyID: string): Promise<void> {
|
|
2468
|
+
const userID = this.getUser().userID;
|
|
2469
|
+
await this.http.delete(
|
|
2470
|
+
this.getHost() + "/user/" + userID + "/passkeys/" + passkeyID,
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2323
2474
|
private async deletePermission(permissionID: string): Promise<void> {
|
|
2324
2475
|
await this.http.delete(this.getHost() + "/permission/" + permissionID);
|
|
2325
2476
|
}
|
|
@@ -2357,7 +2508,6 @@ export class Client {
|
|
|
2357
2508
|
);
|
|
2358
2509
|
return decodeAxios(PermissionArrayCodec, res.data);
|
|
2359
2510
|
}
|
|
2360
|
-
|
|
2361
2511
|
private async fetchUser(
|
|
2362
2512
|
userIdentifier: string,
|
|
2363
2513
|
): Promise<[null | User, AxiosError | null]> {
|
|
@@ -2452,6 +2602,50 @@ export class Client {
|
|
|
2452
2602
|
}
|
|
2453
2603
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
2454
2604
|
}
|
|
2605
|
+
|
|
2606
|
+
/**
|
|
2607
|
+
* Finish a passkey login and adopt the resulting JWT as the
|
|
2608
|
+
* client's bearer token. After this call, `client.passkeys.*`
|
|
2609
|
+
* admin methods are usable; messaging routes will continue to
|
|
2610
|
+
* require a real device token.
|
|
2611
|
+
*/
|
|
2612
|
+
private async finishPasskeyAuthentication(args: {
|
|
2613
|
+
requestID: string;
|
|
2614
|
+
response: Record<string, unknown>;
|
|
2615
|
+
}): Promise<{
|
|
2616
|
+
passkeyID: string;
|
|
2617
|
+
token: string;
|
|
2618
|
+
user: User;
|
|
2619
|
+
}> {
|
|
2620
|
+
const response = await this.http.post(
|
|
2621
|
+
this.getHost() + "/auth/passkey/finish",
|
|
2622
|
+
msgpack.encode(args),
|
|
2623
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
2624
|
+
);
|
|
2625
|
+
const decoded = decodeAxios(
|
|
2626
|
+
PasskeyAuthFinishResponseCodec,
|
|
2627
|
+
response.data,
|
|
2628
|
+
);
|
|
2629
|
+
this.setUser(decoded.user);
|
|
2630
|
+
this.token = decoded.token;
|
|
2631
|
+
this.http.defaults.headers.common.Authorization = `Bearer ${decoded.token}`;
|
|
2632
|
+
return decoded;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
private async finishPasskeyRegistration(args: {
|
|
2636
|
+
name: string;
|
|
2637
|
+
requestID: string;
|
|
2638
|
+
response: Record<string, unknown>;
|
|
2639
|
+
}): Promise<Passkey> {
|
|
2640
|
+
const userID = this.getUser().userID;
|
|
2641
|
+
const response = await this.http.post(
|
|
2642
|
+
this.getHost() + "/user/" + userID + "/passkeys/register/finish",
|
|
2643
|
+
msgpack.encode(args),
|
|
2644
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
2645
|
+
);
|
|
2646
|
+
return decodeAxios(PasskeyCodec, response.data);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2455
2649
|
private async forward(message: Message) {
|
|
2456
2650
|
if (this.isManualCloseInFlight()) {
|
|
2457
2651
|
return;
|
|
@@ -2942,6 +3136,8 @@ export class Client {
|
|
|
2942
3136
|
}
|
|
2943
3137
|
}
|
|
2944
3138
|
|
|
3139
|
+
// ── Passkeys ────────────────────────────────────────────────────────
|
|
3140
|
+
|
|
2945
3141
|
/**
|
|
2946
3142
|
* Fresh read of the `manuallyClosing` flag for async loops — direct property checks
|
|
2947
3143
|
* after `await` are flagged as always-false by control-flow analysis even though
|
|
@@ -2984,6 +3180,28 @@ export class Client {
|
|
|
2984
3180
|
return decodeAxios(PendingDeviceRequestArrayCodec, response.data);
|
|
2985
3181
|
}
|
|
2986
3182
|
|
|
3183
|
+
/**
|
|
3184
|
+
* Lists every device the current account owns.
|
|
3185
|
+
*
|
|
3186
|
+
* Uses the device-authenticated `/user/:id/devices` route. For
|
|
3187
|
+
* the passkey-recovery equivalent see `client.passkeys.listDevices`.
|
|
3188
|
+
*/
|
|
3189
|
+
private async listDevices(): Promise<Device[]> {
|
|
3190
|
+
const userID = this.getUser().userID;
|
|
3191
|
+
const res = await this.http.get(
|
|
3192
|
+
this.getHost() + "/user/" + userID + "/devices",
|
|
3193
|
+
);
|
|
3194
|
+
return decodeAxios(DeviceArrayCodec, res.data);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
private async listPasskeys(): Promise<Passkey[]> {
|
|
3198
|
+
const userID = this.getUser().userID;
|
|
3199
|
+
const response = await this.http.get(
|
|
3200
|
+
this.getHost() + "/user/" + userID + "/passkeys",
|
|
3201
|
+
);
|
|
3202
|
+
return decodeAxios(PasskeyArrayCodec, response.data);
|
|
3203
|
+
}
|
|
3204
|
+
|
|
2987
3205
|
private async markSessionVerified(sessionID: string) {
|
|
2988
3206
|
return this.database.markSessionVerified(sessionID);
|
|
2989
3207
|
}
|
|
@@ -3030,6 +3248,48 @@ export class Client {
|
|
|
3030
3248
|
void this.database.saveMessage(message);
|
|
3031
3249
|
};
|
|
3032
3250
|
|
|
3251
|
+
private async passkeyApproveDeviceRequest(
|
|
3252
|
+
requestID: string,
|
|
3253
|
+
): Promise<Device> {
|
|
3254
|
+
const userID = this.getUser().userID;
|
|
3255
|
+
const response = await this.http.post(
|
|
3256
|
+
this.getHost() +
|
|
3257
|
+
"/user/" +
|
|
3258
|
+
userID +
|
|
3259
|
+
"/passkey/devices/requests/" +
|
|
3260
|
+
requestID +
|
|
3261
|
+
"/approve",
|
|
3262
|
+
);
|
|
3263
|
+
return decodeAxios(DeviceCodec, response.data);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
private async passkeyDeleteDevice(deviceID: string): Promise<void> {
|
|
3267
|
+
const userID = this.getUser().userID;
|
|
3268
|
+
await this.http.delete(
|
|
3269
|
+
this.getHost() + "/user/" + userID + "/passkey/devices/" + deviceID,
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
private async passkeyListDevices(): Promise<Device[]> {
|
|
3274
|
+
const userID = this.getUser().userID;
|
|
3275
|
+
const response = await this.http.get(
|
|
3276
|
+
this.getHost() + "/user/" + userID + "/passkey/devices",
|
|
3277
|
+
);
|
|
3278
|
+
return decodeAxios(DeviceArrayCodec, response.data);
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
private async passkeyRejectDeviceRequest(requestID: string): Promise<void> {
|
|
3282
|
+
const userID = this.getUser().userID;
|
|
3283
|
+
await this.http.post(
|
|
3284
|
+
this.getHost() +
|
|
3285
|
+
"/user/" +
|
|
3286
|
+
userID +
|
|
3287
|
+
"/passkey/devices/requests/" +
|
|
3288
|
+
requestID +
|
|
3289
|
+
"/reject",
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3033
3293
|
private ping() {
|
|
3034
3294
|
if (!this.isAlive) {
|
|
3035
3295
|
// Previous ping went unanswered — the WebSocket is half-open
|
package/src/codecs.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
FileSQLSchema,
|
|
22
22
|
InviteSchema,
|
|
23
23
|
KeyBundleSchema,
|
|
24
|
+
PasskeySchema,
|
|
24
25
|
PermissionSchema,
|
|
25
26
|
ServerSchema,
|
|
26
27
|
UserSchema,
|
|
@@ -80,6 +81,11 @@ export const RegisterPendingApprovalCodec = createCodec(
|
|
|
80
81
|
expiresAt: z.string(),
|
|
81
82
|
requestID: z.string(),
|
|
82
83
|
status: z.literal("pending_approval"),
|
|
84
|
+
// Optional for backward compat with older servers that don't
|
|
85
|
+
// surface the existing user's ID in this response. When present,
|
|
86
|
+
// clients can use it to fetch the unauthenticated avatar and
|
|
87
|
+
// show an "is this you?" confirmation before proceeding.
|
|
88
|
+
userID: z.string().optional(),
|
|
83
89
|
}),
|
|
84
90
|
);
|
|
85
91
|
|
|
@@ -151,6 +157,35 @@ export const WhoamiCodec = createCodec(
|
|
|
151
157
|
|
|
152
158
|
export const OtkCountCodec = createCodec(z.object({ count: z.number() }));
|
|
153
159
|
|
|
160
|
+
// ── Passkey response codecs ────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export const PasskeyCodec = createCodec(PasskeySchema);
|
|
163
|
+
export const PasskeyArrayCodec = createCodec(z.array(PasskeySchema));
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The shape of `/user/:id/passkeys/register/begin` and
|
|
167
|
+
* `/auth/passkey/begin` responses. `options` is the WebAuthn JSON
|
|
168
|
+
* the host hands straight to `navigator.credentials.create()` /
|
|
169
|
+
* `.get()` (via `@simplewebauthn/browser`); we don't validate its
|
|
170
|
+
* inner shape because both ends of the wire (`@simplewebauthn/server`
|
|
171
|
+
* on spire, `@simplewebauthn/browser` on the host) already do.
|
|
172
|
+
*/
|
|
173
|
+
export const PasskeyOptionsCodec = createCodec(
|
|
174
|
+
z.object({
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- WebAuthn options shape varies by simplewebauthn version
|
|
176
|
+
options: z.unknown() as z.ZodType<any>,
|
|
177
|
+
requestID: z.string(),
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
export const PasskeyAuthFinishResponseCodec = createCodec(
|
|
182
|
+
z.object({
|
|
183
|
+
passkeyID: z.string(),
|
|
184
|
+
token: z.string(),
|
|
185
|
+
user: UserSchema,
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
154
189
|
// ── Helper: decode axios response buffer ────────────────────────────────────
|
|
155
190
|
|
|
156
191
|
/**
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type {
|
|
|
23
23
|
Message,
|
|
24
24
|
Messages,
|
|
25
25
|
Moderation,
|
|
26
|
+
Passkeys,
|
|
26
27
|
PendingDeviceApprovalStatus,
|
|
27
28
|
PendingDeviceRegistration,
|
|
28
29
|
PendingDeviceRequest,
|
|
@@ -47,4 +48,4 @@ export type {
|
|
|
47
48
|
UnsavedPreKey,
|
|
48
49
|
} from "./types/index.js";
|
|
49
50
|
// Re-export app-facing types
|
|
50
|
-
export type { Invite } from "@vex-chat/types";
|
|
51
|
+
export type { Invite, Passkey } from "@vex-chat/types";
|