馃 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:
72 err instanceof Error ? err.message : "Failed to register passkey",
73 };
74 }
75}
76
77/**
78 * Authenticate with a passkey
79 */
80export async function authenticateWithPasskey(
81 email?: string,
82): Promise<{ success: boolean; error?: string }> {
83 try {
84 // Get authentication options from server
85 const optionsResponse = await fetch("/api/passkeys/authenticate/options", {
86 method: "POST",
87 headers: { "Content-Type": "application/json" },
88 body: JSON.stringify({ email }),
89 });
90
91 if (!optionsResponse.ok) {
92 const error = await optionsResponse.json();
93 return {
94 success: false,
95 error: error.error || "Failed to get authentication options",
96 };
97 }
98
99 const options: PublicKeyCredentialRequestOptionsJSON =
100 await optionsResponse.json();
101
102 // Start browser passkey authentication
103 let credential: Awaited<ReturnType<typeof startAuthentication>>;
104 try {
105 credential = await startAuthentication({ optionsJSON: options });
106 } catch (err) {
107 // User cancelled or no passkey available
108 return {
109 success: false,
110 error:
111 err instanceof Error
112 ? err.message
113 : "Passkey authentication was cancelled",
114 };
115 }
116
117 // Verify with server
118 const verifyResponse = await fetch("/api/passkeys/authenticate/verify", {
119 method: "POST",
120 headers: { "Content-Type": "application/json" },
121 body: JSON.stringify({
122 response: credential,
123 challenge: options.challenge,
124 }),
125 });
126
127 if (!verifyResponse.ok) {
128 const error = await verifyResponse.json();
129 return {
130 success: false,
131 error: error.error || "Failed to verify passkey",
132 };
133 }
134
135 return { success: true };
136 } catch (err) {
137 return {
138 success: false,
139 error:
140 err instanceof Error ? err.message : "Failed to authenticate with passkey",
141 };
142 }
143}
144
145/**
146 * Check if passkeys are supported in this browser
147 */
148export function isPasskeySupported(): boolean {
149 return (
150 window.PublicKeyCredential !== undefined &&
151 typeof window.PublicKeyCredential === "function"
152 );
153}