@vex-chat/libvex 6.1.9 → 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 +111 -10
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +103 -1
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +60 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +22 -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 +255 -12
- package/src/codecs.ts +30 -0
- package/src/index.ts +2 -1
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/
|
|
97
|
-
"@vex-chat/
|
|
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,
|
|
@@ -258,6 +259,10 @@ import {
|
|
|
258
259
|
InviteCodec,
|
|
259
260
|
KeyBundleCodec,
|
|
260
261
|
OtkCountCodec,
|
|
262
|
+
PasskeyArrayCodec,
|
|
263
|
+
PasskeyAuthFinishResponseCodec,
|
|
264
|
+
PasskeyCodec,
|
|
265
|
+
PasskeyOptionsCodec,
|
|
261
266
|
PendingDeviceRequestArrayCodec,
|
|
262
267
|
PendingDeviceRequestCodec,
|
|
263
268
|
PermissionArrayCodec,
|
|
@@ -316,6 +321,14 @@ export interface Channels {
|
|
|
316
321
|
*/
|
|
317
322
|
export type { Device } from "@vex-chat/types";
|
|
318
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
|
+
|
|
319
332
|
/**
|
|
320
333
|
* ClientOptions are the options you can pass into the client.
|
|
321
334
|
*/
|
|
@@ -358,6 +371,8 @@ export interface Devices {
|
|
|
358
371
|
delete: (deviceID: string) => Promise<void>;
|
|
359
372
|
/** Fetches one pending registration request by ID for the current user. */
|
|
360
373
|
getRequest: (requestID: string) => Promise<null | PendingDeviceRequest>;
|
|
374
|
+
/** Lists every device belonging to the current account. */
|
|
375
|
+
list: () => Promise<Device[]>;
|
|
361
376
|
/** Lists pending/processed registration requests for the current user. */
|
|
362
377
|
listRequests: () => Promise<PendingDeviceRequest[]>;
|
|
363
378
|
/**
|
|
@@ -438,6 +453,16 @@ export interface FileProgress {
|
|
|
438
453
|
*/
|
|
439
454
|
export type FileRes = FileResponse;
|
|
440
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
|
+
|
|
441
466
|
/**
|
|
442
467
|
* Channel is a chat channel on a server.
|
|
443
468
|
*
|
|
@@ -458,16 +483,6 @@ export type { Channel } from "@vex-chat/types";
|
|
|
458
483
|
*/
|
|
459
484
|
export type { Server } from "@vex-chat/types";
|
|
460
485
|
|
|
461
|
-
/**
|
|
462
|
-
* @ignore
|
|
463
|
-
*/
|
|
464
|
-
export interface Files {
|
|
465
|
-
/** Uploads and encrypts a file. */
|
|
466
|
-
create: (file: Uint8Array) => Promise<[FileSQL, string]>;
|
|
467
|
-
/** Downloads and decrypts a file using a file ID and key. */
|
|
468
|
-
retrieve: (fileID: string, key: string) => Promise<FileResponse | null>;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
486
|
/**
|
|
472
487
|
* @ignore
|
|
473
488
|
*/
|
|
@@ -533,6 +548,65 @@ export interface Message {
|
|
|
533
548
|
timestamp: string;
|
|
534
549
|
}
|
|
535
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
|
+
|
|
536
610
|
export type PendingDeviceApprovalStatus =
|
|
537
611
|
| "approved"
|
|
538
612
|
| "expired"
|
|
@@ -896,6 +970,7 @@ export class Client {
|
|
|
896
970
|
approveRequest: this.approveDeviceRequest.bind(this),
|
|
897
971
|
delete: this.deleteDevice.bind(this),
|
|
898
972
|
getRequest: this.getDeviceRegistrationRequest.bind(this),
|
|
973
|
+
list: this.listDevices.bind(this),
|
|
899
974
|
listRequests: this.listDeviceRegistrationRequests.bind(this),
|
|
900
975
|
pollPendingRegistration: this.pollPendingDeviceRegistration.bind(this),
|
|
901
976
|
register: this.registerDevice.bind(this),
|
|
@@ -969,6 +1044,7 @@ export class Client {
|
|
|
969
1044
|
*/
|
|
970
1045
|
user: this.getUser.bind(this),
|
|
971
1046
|
};
|
|
1047
|
+
|
|
972
1048
|
/**
|
|
973
1049
|
* Message operations (direct and group).
|
|
974
1050
|
*
|
|
@@ -1009,7 +1085,6 @@ export class Client {
|
|
|
1009
1085
|
*/
|
|
1010
1086
|
send: this.sendMessage.bind(this),
|
|
1011
1087
|
};
|
|
1012
|
-
|
|
1013
1088
|
/**
|
|
1014
1089
|
* Server moderation helper methods.
|
|
1015
1090
|
*/
|
|
@@ -1018,6 +1093,31 @@ export class Client {
|
|
|
1018
1093
|
kick: this.kickUser.bind(this),
|
|
1019
1094
|
};
|
|
1020
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
|
+
|
|
1021
1121
|
/**
|
|
1022
1122
|
* Permission-management methods for the current user.
|
|
1023
1123
|
*/
|
|
@@ -1981,6 +2081,33 @@ export class Client {
|
|
|
1981
2081
|
return decodeAxios(DeviceCodec, response.data);
|
|
1982
2082
|
}
|
|
1983
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
|
+
|
|
1984
2111
|
private censorPreKey(preKey: PreKeysSQL): PreKeysWS {
|
|
1985
2112
|
if (!preKey.index) {
|
|
1986
2113
|
throw new Error("Key index is required.");
|
|
@@ -2337,6 +2464,13 @@ export class Client {
|
|
|
2337
2464
|
await this.database.deleteHistory(channelOrUserID);
|
|
2338
2465
|
}
|
|
2339
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
|
+
|
|
2340
2474
|
private async deletePermission(permissionID: string): Promise<void> {
|
|
2341
2475
|
await this.http.delete(this.getHost() + "/permission/" + permissionID);
|
|
2342
2476
|
}
|
|
@@ -2374,7 +2508,6 @@ export class Client {
|
|
|
2374
2508
|
);
|
|
2375
2509
|
return decodeAxios(PermissionArrayCodec, res.data);
|
|
2376
2510
|
}
|
|
2377
|
-
|
|
2378
2511
|
private async fetchUser(
|
|
2379
2512
|
userIdentifier: string,
|
|
2380
2513
|
): Promise<[null | User, AxiosError | null]> {
|
|
@@ -2469,6 +2602,50 @@ export class Client {
|
|
|
2469
2602
|
}
|
|
2470
2603
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
2471
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
|
+
|
|
2472
2649
|
private async forward(message: Message) {
|
|
2473
2650
|
if (this.isManualCloseInFlight()) {
|
|
2474
2651
|
return;
|
|
@@ -2959,6 +3136,8 @@ export class Client {
|
|
|
2959
3136
|
}
|
|
2960
3137
|
}
|
|
2961
3138
|
|
|
3139
|
+
// ── Passkeys ────────────────────────────────────────────────────────
|
|
3140
|
+
|
|
2962
3141
|
/**
|
|
2963
3142
|
* Fresh read of the `manuallyClosing` flag for async loops — direct property checks
|
|
2964
3143
|
* after `await` are flagged as always-false by control-flow analysis even though
|
|
@@ -3001,6 +3180,28 @@ export class Client {
|
|
|
3001
3180
|
return decodeAxios(PendingDeviceRequestArrayCodec, response.data);
|
|
3002
3181
|
}
|
|
3003
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
|
+
|
|
3004
3205
|
private async markSessionVerified(sessionID: string) {
|
|
3005
3206
|
return this.database.markSessionVerified(sessionID);
|
|
3006
3207
|
}
|
|
@@ -3047,6 +3248,48 @@ export class Client {
|
|
|
3047
3248
|
void this.database.saveMessage(message);
|
|
3048
3249
|
};
|
|
3049
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
|
+
|
|
3050
3293
|
private ping() {
|
|
3051
3294
|
if (!this.isAlive) {
|
|
3052
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,
|
|
@@ -156,6 +157,35 @@ export const WhoamiCodec = createCodec(
|
|
|
156
157
|
|
|
157
158
|
export const OtkCountCodec = createCodec(z.object({ count: z.number() }));
|
|
158
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
|
+
|
|
159
189
|
// ── Helper: decode axios response buffer ────────────────────────────────────
|
|
160
190
|
|
|
161
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";
|