馃 distributed transcription service
thistle.dunkirk.sh
1import {
2 startAuthentication,
3 startRegistration,
4} from "@simplewebauthn/browser";
5import type {
6 PublicKeyCredentialCreationOptionsJSON,
7 PublicKeyCredentialRequestOptionsJSON,
8} from "@simplewebauthn/types";
9
10/**
11 * Register a new passkey for the current user
12 */
13export async function registerPasskey(
14 name?: string,
15): Promise<{ success: boolean; error?: string }> {
16 try {
17 // Get registration options from server
18 const optionsResponse = await fetch("/api/passkeys/register/options", {
19 method: "POST",
20 });
21
22 if (!optionsResponse.ok) {
23 const error = await optionsResponse.json();
24 return {
25 success: false,
26 error: error.error || "Failed to get registration options",
27 };
28 }
29
30 const options: PublicKeyCredentialCreationOptionsJSON =
31 await optionsResponse.json();
32
33 // Start browser passkey creation
34 let credential: Awaited<ReturnType<typeof startRegistration>>;
35 try {
36 credential = await startRegistration({ optionsJSON: options });
37 } catch (err) {
38 // User cancelled or browser doesn't support passkeys
39 return {
40 success: false,
41 error:
42 err instanceof Error
43 ? err.message
44 : "Passkey registration was cancelled",
45 };
46 }
47
48 // Verify with server
49 const verifyResponse = await fetch("/api/passkeys/register/verify", {
50 method: "POST",
51 headers: { "Content-Type": "application/json" },
52 body: JSON.stringify({
53 response: credential,
54 challenge: options.challenge,
55 name,
56 }),
57 });
58
59 if (!verifyResponse.ok) {
60 const error = await verifyResponse.json();
61 return {
62 success: false,
63 error: error.error || "Failed to verify passkey",
64 };
65 }
66
67 return { success: true };
68 } catch (err) {
69 return {
70 success: false,
71 error: err instanceof Error ? err.message : "Failed to register passkey",
72 };
73 }
74}
75
76/**
77 * Authenticate with a passkey
78 */
79export async function authenticateWithPasskey(
80 email?: string,
81): Promise<{ success: boolean; error?: string }> {
82 try {
83 // Get authentication options from server
84 const optionsResponse = await fetch("/api/passkeys/authenticate/options", {
85 method: "POST",
86 headers: { "Content-Type": "application/json" },
87 body: JSON.stringify({ email }),
88 });
89
90 if (!optionsResponse.ok) {
91 const error = await optionsResponse.json();
92 return {
93 success: false,
94 error: error.error || "Failed to get authentication options",
95 };
96 }
97
98 const options: PublicKeyCredentialRequestOptionsJSON =
99 await optionsResponse.json();
100
101 // Start browser passkey authentication
102 let credential: Awaited<ReturnType<typeof startAuthentication>>;
103 try {
104 credential = await startAuthentication({ optionsJSON: options });
105 } catch (err) {
106 // User cancelled or no passkey available
107 return {
108 success: false,
109 error:
110 err instanceof Error
111 ? err.message
112 : "Passkey authentication was cancelled",
113 };
114 }
115
116 // Verify with server
117 const verifyResponse = await fetch("/api/passkeys/authenticate/verify", {
118 method: "POST",
119 headers: { "Content-Type": "application/json" },
120 body: JSON.stringify({
121 response: credential,
122 challenge: options.challenge,
123 }),
124 });
125
126 if (!verifyResponse.ok) {
127 const error = await verifyResponse.json();
128 return {
129 success: false,
130 error: error.error || "Failed to verify passkey",
131 };
132 }
133
134 return { success: true };
135 } catch (err) {
136 return {
137 success: false,
138 error:
139 err instanceof Error
140 ? err.message
141 : "Failed to authenticate with passkey",
142 };
143 }
144}
145
146/**
147 * Check if passkeys are supported in this browser
148 */
149export function isPasskeySupported(): boolean {
150 return (
151 window.PublicKeyCredential !== undefined &&
152 typeof window.PublicKeyCredential === "function"
153 );
154}