@thru/passkey 0.2.1

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/src/popup.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type {
2
+ PasskeyPopupAction,
3
+ PasskeyPopupRequestPayload,
4
+ PasskeyPopupRequest,
5
+ PasskeyPopupResponse,
6
+ } from './types';
7
+
8
+ export const PASSKEY_POPUP_PATH = '/passkey/popup';
9
+ export const PASSKEY_POPUP_READY_EVENT = 'thru:passkey-popup-ready';
10
+ export const PASSKEY_POPUP_REQUEST_EVENT = 'thru:passkey-popup-request';
11
+ export const PASSKEY_POPUP_RESPONSE_EVENT = 'thru:passkey-popup-response';
12
+ export const PASSKEY_POPUP_CHANNEL = 'thru:passkey-popup-channel';
13
+
14
+ const PASSKEY_POPUP_TIMEOUT_MS = 60000;
15
+
16
+ export function closePopup(popup: Window | null | undefined): void {
17
+ if (popup && !popup.closed) {
18
+ popup.close();
19
+ }
20
+ }
21
+
22
+ export function openPasskeyPopupWindow(): Window {
23
+ const popupUrl = new URL(PASSKEY_POPUP_PATH, window.location.origin).toString();
24
+ const popup = window.open(
25
+ popupUrl,
26
+ 'thru_passkey_popup',
27
+ 'popup=yes,width=440,height=640'
28
+ );
29
+
30
+ if (!popup) {
31
+ throw new Error('Passkey popup was blocked');
32
+ }
33
+
34
+ return popup;
35
+ }
36
+
37
+ function createPopupRequestId(): string {
38
+ const rand = Math.random().toString(36).slice(2, 10);
39
+ return `passkey_${Date.now()}_${rand}`;
40
+ }
41
+
42
+ export async function requestPasskeyPopup<T>(
43
+ action: PasskeyPopupAction,
44
+ payload: PasskeyPopupRequestPayload,
45
+ preopenedPopup?: Window | null
46
+ ): Promise<T> {
47
+ if (typeof window === 'undefined') {
48
+ throw new Error('Passkey popup is only available in the browser');
49
+ }
50
+
51
+ const requestId = createPopupRequestId();
52
+ const targetOrigin = window.location.origin;
53
+ let popup: Window | null = preopenedPopup ?? null;
54
+ const channel =
55
+ typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel(PASSKEY_POPUP_CHANNEL) : null;
56
+
57
+ return new Promise<T>((resolve, reject) => {
58
+ let timeout: ReturnType<typeof setTimeout> | null = null;
59
+ let closePoll: ReturnType<typeof setInterval> | null = null;
60
+ let requestSent = false;
61
+
62
+ const cleanup = () => {
63
+ if (timeout) {
64
+ clearTimeout(timeout);
65
+ timeout = null;
66
+ }
67
+ if (closePoll) {
68
+ clearInterval(closePoll);
69
+ closePoll = null;
70
+ }
71
+ window.removeEventListener('message', handleMessage);
72
+ if (channel) {
73
+ channel.removeEventListener('message', handleChannelMessage);
74
+ channel.close();
75
+ }
76
+ };
77
+
78
+ const sendRequest = (viaChannel: boolean) => {
79
+ if (requestSent) {
80
+ return;
81
+ }
82
+ requestSent = true;
83
+
84
+ const request: PasskeyPopupRequest = {
85
+ type: PASSKEY_POPUP_REQUEST_EVENT,
86
+ requestId,
87
+ action,
88
+ payload,
89
+ };
90
+
91
+ if (viaChannel) {
92
+ channel?.postMessage(request);
93
+ return;
94
+ }
95
+
96
+ popup?.postMessage(request, targetOrigin);
97
+ };
98
+
99
+ const handleResponse = (data: PasskeyPopupResponse) => {
100
+ if (data.requestId !== requestId) {
101
+ return;
102
+ }
103
+
104
+ cleanup();
105
+ if (popup && !popup.closed) {
106
+ popup.close();
107
+ }
108
+
109
+ if (data.success) {
110
+ resolve((data as Extract<PasskeyPopupResponse, { success: true }>).result as T);
111
+ } else {
112
+ const err = new Error(data.error?.message || 'Passkey popup failed');
113
+ if (data.error?.name) {
114
+ (err as { name?: string }).name = data.error.name;
115
+ }
116
+ reject(err);
117
+ }
118
+ };
119
+
120
+ const handleMessage = (event: MessageEvent) => {
121
+ if (event.origin !== targetOrigin) {
122
+ return;
123
+ }
124
+
125
+ const data = event.data as PasskeyPopupResponse | { type?: string };
126
+ if (!data || typeof data !== 'object') {
127
+ return;
128
+ }
129
+
130
+ if (data.type === PASSKEY_POPUP_READY_EVENT) {
131
+ if (popup && event.source !== popup) {
132
+ return;
133
+ }
134
+ sendRequest(false);
135
+ return;
136
+ }
137
+
138
+ if (data.type === PASSKEY_POPUP_RESPONSE_EVENT && 'requestId' in data) {
139
+ handleResponse(data as PasskeyPopupResponse);
140
+ }
141
+ };
142
+
143
+ window.addEventListener('message', handleMessage);
144
+
145
+ const handleChannelMessage = (event: MessageEvent) => {
146
+ const data = event.data as PasskeyPopupResponse | { type?: string };
147
+ if (!data || typeof data !== 'object') {
148
+ return;
149
+ }
150
+
151
+ if (data.type === PASSKEY_POPUP_READY_EVENT) {
152
+ sendRequest(true);
153
+ return;
154
+ }
155
+
156
+ if (data.type === PASSKEY_POPUP_RESPONSE_EVENT && 'requestId' in data) {
157
+ handleResponse(data as PasskeyPopupResponse);
158
+ }
159
+ };
160
+
161
+ if (channel) {
162
+ channel.addEventListener('message', handleChannelMessage);
163
+ }
164
+
165
+ if (!popup) {
166
+ try {
167
+ popup = openPasskeyPopupWindow();
168
+ } catch (error) {
169
+ cleanup();
170
+ reject(error);
171
+ return;
172
+ }
173
+ }
174
+
175
+ timeout = setTimeout(() => {
176
+ cleanup();
177
+ try {
178
+ popup?.close();
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ reject(new Error('Passkey popup timed out'));
183
+ }, PASSKEY_POPUP_TIMEOUT_MS);
184
+
185
+ closePoll = setInterval(() => {
186
+ if (popup && popup.closed) {
187
+ cleanup();
188
+ reject(new Error('Passkey popup was closed'));
189
+ }
190
+ }, 250);
191
+ });
192
+ }
@@ -0,0 +1,228 @@
1
+ import type { PasskeyRegistrationResult, PasskeyPopupRegistrationResult } from './types';
2
+ import { arrayBufferToBase64Url, bytesToHex } from '@thru/passkey-manager';
3
+ import {
4
+ isWebAuthnSupported,
5
+ getPasskeyPromptMode,
6
+ maybePreopenPopup,
7
+ shouldFallbackToPopup,
8
+ type PasskeyPromptAction,
9
+ } from './capabilities';
10
+ import { requestPasskeyPopup, openPasskeyPopupWindow, closePopup } from './popup';
11
+
12
+ /**
13
+ * Register a new passkey for a profile.
14
+ */
15
+ export async function registerPasskey(
16
+ alias: string,
17
+ userId: string,
18
+ rpId: string
19
+ ): Promise<PasskeyRegistrationResult> {
20
+ if (!isWebAuthnSupported()) {
21
+ throw new Error('WebAuthn is not supported in this browser');
22
+ }
23
+
24
+ return runWithPromptMode(
25
+ 'create',
26
+ () => registerPasskeyInline(alias, userId, rpId),
27
+ (preopenedPopup) => registerPasskeyViaPopup(alias, userId, rpId, preopenedPopup)
28
+ );
29
+ }
30
+
31
+ async function runWithPromptMode<T>(
32
+ action: PasskeyPromptAction,
33
+ inlineFn: () => Promise<T>,
34
+ popupFn: (preopenedPopup?: Window | null) => Promise<T>
35
+ ): Promise<T> {
36
+ const preopenedPopup = maybePreopenPopup(action, openPasskeyPopupWindow);
37
+ const promptMode = await getPasskeyPromptMode(action);
38
+ if (promptMode === 'popup') {
39
+ return popupFn(preopenedPopup);
40
+ }
41
+
42
+ closePopup(preopenedPopup);
43
+
44
+ try {
45
+ return await inlineFn();
46
+ } catch (error) {
47
+ if (shouldFallbackToPopup(error)) {
48
+ return popupFn();
49
+ }
50
+ throw error;
51
+ }
52
+ }
53
+
54
+ async function registerPasskeyInline(
55
+ alias: string,
56
+ userId: string,
57
+ rpId: string
58
+ ): Promise<PasskeyRegistrationResult> {
59
+ const rpName = 'Thru Wallet';
60
+
61
+ const userIdBytes = new TextEncoder().encode(userId);
62
+ const userIdBuffer = userIdBytes.slice(0, 64);
63
+
64
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
65
+
66
+ const createOptions: PublicKeyCredentialCreationOptions = {
67
+ challenge,
68
+ rp: {
69
+ id: rpId,
70
+ name: rpName,
71
+ },
72
+ user: {
73
+ id: userIdBuffer,
74
+ name: alias,
75
+ displayName: alias,
76
+ },
77
+ pubKeyCredParams: [
78
+ { type: 'public-key', alg: -7 },
79
+ ],
80
+ authenticatorSelection: {
81
+ authenticatorAttachment: 'platform',
82
+ userVerification: 'required',
83
+ residentKey: 'required',
84
+ requireResidentKey: true,
85
+ },
86
+ attestation: 'none',
87
+ timeout: 60000,
88
+ };
89
+
90
+ const credential = (await navigator.credentials.create({
91
+ publicKey: createOptions,
92
+ })) as PublicKeyCredential | null;
93
+
94
+ if (!credential) {
95
+ throw new Error('Passkey registration was cancelled');
96
+ }
97
+
98
+ const response = credential.response as AuthenticatorAttestationResponse;
99
+ const { x, y } = extractP256PublicKey(response);
100
+
101
+ return {
102
+ credentialId: arrayBufferToBase64Url(credential.rawId),
103
+ publicKeyX: bytesToHex(x),
104
+ publicKeyY: bytesToHex(y),
105
+ rpId,
106
+ };
107
+ }
108
+
109
+ async function registerPasskeyViaPopup(
110
+ alias: string,
111
+ userId: string,
112
+ rpId: string,
113
+ preopenedPopup?: Window | null
114
+ ): Promise<PasskeyRegistrationResult> {
115
+ const result = await requestPasskeyPopup<PasskeyPopupRegistrationResult>(
116
+ 'create',
117
+ { alias, userId, rpId },
118
+ preopenedPopup
119
+ );
120
+ return result;
121
+ }
122
+
123
+ // Key extraction helpers
124
+
125
+ function extractP256PublicKey(
126
+ response: AuthenticatorAttestationResponse
127
+ ): { x: Uint8Array; y: Uint8Array } {
128
+ if (typeof response.getPublicKey === 'function') {
129
+ const spkiKey = response.getPublicKey();
130
+ if (spkiKey) {
131
+ return extractFromSpki(new Uint8Array(spkiKey));
132
+ }
133
+ }
134
+
135
+ if (typeof response.getAuthenticatorData === 'function') {
136
+ const authData = new Uint8Array(response.getAuthenticatorData());
137
+ return extractFromAuthenticatorData(authData);
138
+ }
139
+
140
+ throw new Error('Unable to extract public key: browser does not support required WebAuthn methods');
141
+ }
142
+
143
+ function extractFromSpki(spki: Uint8Array): { x: Uint8Array; y: Uint8Array } {
144
+ const pointStart = spki.length - 65;
145
+
146
+ if (spki[pointStart] !== 0x04) {
147
+ throw new Error('Invalid SPKI format: expected uncompressed point');
148
+ }
149
+
150
+ const x = spki.slice(pointStart + 1, pointStart + 33);
151
+ const y = spki.slice(pointStart + 33, pointStart + 65);
152
+
153
+ if (x.length !== 32 || y.length !== 32) {
154
+ throw new Error('Invalid SPKI format: incorrect coordinate length');
155
+ }
156
+
157
+ return { x, y };
158
+ }
159
+
160
+ function extractFromAuthenticatorData(authData: Uint8Array): { x: Uint8Array; y: Uint8Array } {
161
+ const rpIdHashLength = 32;
162
+ const flagsLength = 1;
163
+ const counterLength = 4;
164
+ const offset = rpIdHashLength + flagsLength + counterLength;
165
+ const aaguidLength = 16;
166
+ const credIdLenOffset = offset + aaguidLength;
167
+ const credIdLength = (authData[credIdLenOffset] << 8) | authData[credIdLenOffset + 1];
168
+ const coseKeyOffset = credIdLenOffset + 2 + credIdLength;
169
+ const coseKey = authData.slice(coseKeyOffset);
170
+
171
+ return extractFromCoseKey(coseKey);
172
+ }
173
+
174
+ function extractFromCoseKey(coseKey: Uint8Array): { x: Uint8Array; y: Uint8Array } {
175
+ const mapStart = coseKey[0];
176
+ if (mapStart !== 0xa5 && mapStart !== 0xa4) {
177
+ throw new Error('Invalid COSE key format');
178
+ }
179
+
180
+ let offset = 1;
181
+ let x: Uint8Array | null = null;
182
+ let y: Uint8Array | null = null;
183
+
184
+ while (offset < coseKey.length) {
185
+ const key = coseKey[offset++];
186
+ const valueType = coseKey[offset++];
187
+
188
+ if (key === 0x21) {
189
+ const length = valueType & 0x1f;
190
+ x = coseKey.slice(offset, offset + length);
191
+ offset += length;
192
+ continue;
193
+ }
194
+
195
+ if (key === 0x22) {
196
+ const length = valueType & 0x1f;
197
+ y = coseKey.slice(offset, offset + length);
198
+ offset += length;
199
+ continue;
200
+ }
201
+
202
+ if (valueType >= 0x40 && valueType <= 0x5f) {
203
+ const length = valueType & 0x1f;
204
+ offset += length;
205
+ continue;
206
+ }
207
+
208
+ if (valueType === 0x01 || valueType === 0x02 || valueType === 0x03) {
209
+ continue;
210
+ }
211
+
212
+ if (valueType >= 0x18 && valueType <= 0x1b) {
213
+ const size = 1 << (valueType - 0x18);
214
+ offset += size;
215
+ continue;
216
+ }
217
+ }
218
+
219
+ if (!x || !y) {
220
+ throw new Error('Failed to extract P-256 public key from COSE data');
221
+ }
222
+
223
+ if (x.length !== 32 || y.length !== 32) {
224
+ throw new Error('Invalid COSE key: incorrect coordinate length');
225
+ }
226
+
227
+ return { x, y };
228
+ }
package/src/sign.ts ADDED
@@ -0,0 +1,280 @@
1
+ import type {
2
+ PasskeySigningResult,
3
+ PasskeyStoredSigningResult,
4
+ PasskeyDiscoverableSigningResult,
5
+ PasskeyMetadata,
6
+ PasskeyPopupContext,
7
+ PasskeyPopupSigningResult,
8
+ PasskeyPopupStoredSigningResult,
9
+ } from './types';
10
+ import {
11
+ arrayBufferToBase64Url,
12
+ base64UrlToArrayBuffer,
13
+ bytesToBase64Url,
14
+ base64UrlToBytes,
15
+ parseDerSignature,
16
+ normalizeLowS,
17
+ } from '@thru/passkey-manager';
18
+ import {
19
+ isWebAuthnSupported,
20
+ getPasskeyPromptMode,
21
+ isInIframe,
22
+ maybePreopenPopup,
23
+ shouldFallbackToPopup,
24
+ type PasskeyPromptAction,
25
+ } from './capabilities';
26
+ import { requestPasskeyPopup, openPasskeyPopupWindow, closePopup } from './popup';
27
+
28
+ /**
29
+ * Sign a challenge with an existing passkey (by credential ID).
30
+ */
31
+ export async function signWithPasskey(
32
+ credentialId: string,
33
+ challenge: Uint8Array,
34
+ rpId: string
35
+ ): Promise<PasskeySigningResult> {
36
+ if (!isWebAuthnSupported()) {
37
+ throw new Error('WebAuthn is not supported in this browser');
38
+ }
39
+
40
+ return runWithPromptMode(
41
+ 'get',
42
+ () => signWithPasskeyInline(credentialId, challenge, rpId),
43
+ (preopenedPopup) => signWithPasskeyViaPopup(credentialId, challenge, rpId, preopenedPopup)
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Sign with stored passkey (for embedded/popup contexts).
49
+ */
50
+ export async function signWithStoredPasskey(
51
+ challenge: Uint8Array,
52
+ rpId: string,
53
+ preferredPasskey: PasskeyMetadata | null,
54
+ allPasskeys: PasskeyMetadata[],
55
+ context?: PasskeyPopupContext
56
+ ): Promise<PasskeyStoredSigningResult> {
57
+ if (!isWebAuthnSupported()) {
58
+ throw new Error('WebAuthn is not supported in this browser');
59
+ }
60
+
61
+ const preopenedPopup = maybePreopenPopup('get', openPasskeyPopupWindow);
62
+ const promptMode = await getPasskeyPromptMode('get');
63
+ const storedPasskey = preferredPasskey;
64
+ const canUsePopup = isInIframe();
65
+
66
+ if (promptMode === 'popup' || (canUsePopup && !storedPasskey)) {
67
+ return requestStoredPasskeyPopup(challenge, preopenedPopup, context);
68
+ }
69
+
70
+ closePopup(preopenedPopup);
71
+
72
+ try {
73
+ if (storedPasskey) {
74
+ const result = await signWithPasskeyInline(
75
+ storedPasskey.credentialId,
76
+ challenge,
77
+ storedPasskey.rpId
78
+ );
79
+ return {
80
+ ...result,
81
+ passkey: storedPasskey,
82
+ };
83
+ }
84
+
85
+ const discoverable = await signWithDiscoverablePasskey(challenge, rpId);
86
+ const matchingPasskey = allPasskeys.find(p => p.credentialId === discoverable.credentialId) ?? null;
87
+ const now = new Date().toISOString();
88
+ const passkey = matchingPasskey ?? {
89
+ credentialId: discoverable.credentialId,
90
+ publicKeyX: '',
91
+ publicKeyY: '',
92
+ rpId: discoverable.rpId,
93
+ createdAt: now,
94
+ lastUsedAt: now,
95
+ };
96
+
97
+ return {
98
+ signature: discoverable.signature,
99
+ authenticatorData: discoverable.authenticatorData,
100
+ clientDataJSON: discoverable.clientDataJSON,
101
+ signatureR: discoverable.signatureR,
102
+ signatureS: discoverable.signatureS,
103
+ passkey,
104
+ };
105
+ } catch (error) {
106
+ if (canUsePopup && shouldFallbackToPopup(error)) {
107
+ return requestStoredPasskeyPopup(challenge, undefined, context);
108
+ }
109
+
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Sign with a discoverable passkey (no credential ID - browser prompts user to select).
116
+ */
117
+ export async function signWithDiscoverablePasskey(
118
+ challenge: Uint8Array,
119
+ rpId: string
120
+ ): Promise<PasskeyDiscoverableSigningResult> {
121
+ if (!isWebAuthnSupported()) {
122
+ throw new Error('WebAuthn is not supported in this browser');
123
+ }
124
+
125
+ const resolvedRpId = rpId;
126
+ const result = await signWithPasskeyAssertion(challenge, resolvedRpId);
127
+
128
+ return {
129
+ signature: result.signature,
130
+ authenticatorData: result.authenticatorData,
131
+ clientDataJSON: result.clientDataJSON,
132
+ signatureR: result.signatureR,
133
+ signatureS: result.signatureS,
134
+ credentialId: result.credentialId,
135
+ rpId: resolvedRpId,
136
+ };
137
+ }
138
+
139
+ // Internal helpers
140
+
141
+ async function runWithPromptMode<T>(
142
+ action: PasskeyPromptAction,
143
+ inlineFn: () => Promise<T>,
144
+ popupFn: (preopenedPopup?: Window | null) => Promise<T>
145
+ ): Promise<T> {
146
+ const preopenedPopup = maybePreopenPopup(action, openPasskeyPopupWindow);
147
+ const promptMode = await getPasskeyPromptMode(action);
148
+ if (promptMode === 'popup') {
149
+ return popupFn(preopenedPopup);
150
+ }
151
+
152
+ closePopup(preopenedPopup);
153
+
154
+ try {
155
+ return await inlineFn();
156
+ } catch (error) {
157
+ if (shouldFallbackToPopup(error)) {
158
+ return popupFn();
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ async function signWithPasskeyInline(
165
+ credentialId: string,
166
+ challenge: Uint8Array,
167
+ rpId: string
168
+ ): Promise<PasskeySigningResult> {
169
+ const result = await signWithPasskeyAssertion(challenge, rpId, credentialId);
170
+ return {
171
+ signature: result.signature,
172
+ authenticatorData: result.authenticatorData,
173
+ clientDataJSON: result.clientDataJSON,
174
+ signatureR: result.signatureR,
175
+ signatureS: result.signatureS,
176
+ };
177
+ }
178
+
179
+ async function signWithPasskeyAssertion(
180
+ challenge: Uint8Array,
181
+ rpId: string,
182
+ credentialId?: string
183
+ ): Promise<PasskeySigningResult & { credentialId: string }> {
184
+ const challengeBytes = new Uint8Array(challenge);
185
+ const getOptions: PublicKeyCredentialRequestOptions = {
186
+ challenge: challengeBytes,
187
+ rpId,
188
+ userVerification: 'required',
189
+ timeout: 60000,
190
+ };
191
+
192
+ if (credentialId) {
193
+ const credentialIdBuffer = base64UrlToArrayBuffer(credentialId);
194
+ getOptions.allowCredentials = [
195
+ {
196
+ type: 'public-key',
197
+ id: credentialIdBuffer,
198
+ transports: ['internal', 'hybrid', 'usb', 'ble', 'nfc'],
199
+ },
200
+ ];
201
+ }
202
+
203
+ const assertion = (await navigator.credentials.get({
204
+ publicKey: getOptions,
205
+ })) as PublicKeyCredential | null;
206
+
207
+ if (!assertion) {
208
+ throw new Error('Passkey authentication was cancelled');
209
+ }
210
+
211
+ const response = assertion.response as AuthenticatorAssertionResponse;
212
+
213
+ const signature = new Uint8Array(response.signature);
214
+ let { r, s } = parseDerSignature(signature);
215
+ s = normalizeLowS(s);
216
+
217
+ return {
218
+ signature: new Uint8Array([...r, ...s]),
219
+ authenticatorData: new Uint8Array(response.authenticatorData),
220
+ clientDataJSON: new Uint8Array(response.clientDataJSON),
221
+ signatureR: r,
222
+ signatureS: s,
223
+ credentialId: arrayBufferToBase64Url(assertion.rawId),
224
+ };
225
+ }
226
+
227
+ async function signWithPasskeyViaPopup(
228
+ credentialId: string,
229
+ challenge: Uint8Array,
230
+ rpId: string,
231
+ preopenedPopup?: Window | null
232
+ ): Promise<PasskeySigningResult> {
233
+ const result = await requestPasskeyPopup<PasskeyPopupSigningResult>(
234
+ 'get',
235
+ {
236
+ credentialId,
237
+ challengeBase64Url: bytesToBase64Url(challenge),
238
+ rpId,
239
+ },
240
+ preopenedPopup
241
+ );
242
+
243
+ return decodePopupSigningResult(result);
244
+ }
245
+
246
+ async function requestStoredPasskeyPopup(
247
+ challenge: Uint8Array,
248
+ preopenedPopup?: Window | null,
249
+ context?: PasskeyPopupContext
250
+ ): Promise<PasskeyStoredSigningResult> {
251
+ const result = await requestPasskeyPopup<PasskeyPopupStoredSigningResult>(
252
+ 'getStored',
253
+ {
254
+ challengeBase64Url: bytesToBase64Url(challenge),
255
+ context,
256
+ },
257
+ preopenedPopup
258
+ );
259
+ return decodePopupStoredSigningResult(result);
260
+ }
261
+
262
+ function decodePopupSigningResult(result: PasskeyPopupSigningResult): PasskeySigningResult {
263
+ return {
264
+ signature: base64UrlToBytes(result.signatureBase64Url),
265
+ authenticatorData: base64UrlToBytes(result.authenticatorDataBase64Url),
266
+ clientDataJSON: base64UrlToBytes(result.clientDataJSONBase64Url),
267
+ signatureR: base64UrlToBytes(result.signatureRBase64Url),
268
+ signatureS: base64UrlToBytes(result.signatureSBase64Url),
269
+ };
270
+ }
271
+
272
+ function decodePopupStoredSigningResult(
273
+ result: PasskeyPopupStoredSigningResult
274
+ ): PasskeyStoredSigningResult {
275
+ return {
276
+ ...decodePopupSigningResult(result),
277
+ passkey: result.passkey,
278
+ accounts: result.accounts,
279
+ };
280
+ }