@@ -16,15 +16,14 @@ import * as PasslockBrowser from '@passlock/browser';
1616
1717import {
1818 DeletePasskeySuccess ,
19- DeletePasskeyWarning ,
2019 DeleteUserPasskeysSuccess ,
2120 Error ,
2221 PasskeyStatusSuccess ,
2322 AuthorizedPasskeyAuthentication ,
2423 AuthorizedPasskeyRegistration ,
2524 UpdatePasskeysSuccess
2625} from '$lib/shared/schemas' ;
27- import { parse , variant } from 'valibot' ;
26+ import { parse } from 'valibot' ;
2827import { resolve } from '$app/paths' ;
2928import { fetchData } from './network' ;
3029
@@ -209,38 +208,36 @@ export type UpdatePasskeysInput = {
209208 * - the user's device or password manager, which shows the username and
210209 * display name during sign-in
211210 *
212- * The server updates the first two; the browser then uses the credential list
213- * returned by the server to request the local update.
211+ * The server updates the first two and returns a short-lived token. The browser
212+ * exchanges that token to request the local update.
214213 */
215- export const updateUserPasskeys = async ( input : UpdatePasskeysInput ) => {
214+ export const updateUserPasskeys = async (
215+ input : UpdatePasskeysInput ,
216+ config : PasslockClientConfig
217+ ) => {
216218 const ERROR_TAG = '@error/UpdatePasskeyError' ;
217219 const { username, givenName, familyName } = input ;
218220 const displayName = `${ givenName } ${ familyName } ` . trim ( ) ;
219221
220- // `PATCH /passkeys` updates the Passlock vault and the local SQLite record.
222+ // `PATCH /passkeys` updates the Passlock vault and the local SQLite record,
223+ // then returns a prepared token for browser-side signal instructions.
221224 const serverResult = await fetchData ( {
222225 url : resolve ( '/passkeys' ) ,
223226 method : 'PATCH' ,
224227 body : { username, displayName } ,
225- on2xx : ( jsonResponse ) => {
226- const { credentials } = parse ( UpdatePasskeysSuccess , jsonResponse ) ;
227- return credentials . length === 0
228- ? ( { _tag : ERROR_TAG , message : 'No passkeys found' } as const )
229- : ( { _tag : 'Credentials' , credentials } as const ) ;
230- } ,
228+ on2xx : ( jsonResponse ) => parse ( UpdatePasskeysSuccess , jsonResponse ) ,
231229 orElse : ( jsonResponse ) => {
232230 const { message } = parse ( Error , jsonResponse ) ;
233231 return { _tag : ERROR_TAG , message } as const ;
234232 }
235233 } ) ;
236234
237- // Only the credential payload contains enough information for the browser to
238- // request a local device update.
239- if ( serverResult . _tag !== 'Credentials' ) return serverResult ;
235+ if ( serverResult . _tag === ERROR_TAG ) return serverResult ;
240236
241- // This step is purely local to the browser/device, so no tenancy config is
242- // required.
243- const clientResult = await PasslockBrowser . updatePasskeyUsernames ( serverResult . credentials ) ;
237+ const clientResult = await PasslockBrowser . updatePasskeys (
238+ { updatePasskeysToken : serverResult . updatePasskeysToken } ,
239+ config
240+ ) ;
244241
245242 if ( clientResult . success ) return { _tag : 'UpdatePasskeySuccess' } as const ;
246243
@@ -258,20 +255,17 @@ export type DeletePasskeyInput = {
258255 * best-effort browser-side part. The account should stop trusting the passkey
259256 * even if the browser cannot remove it from the local password manager.
260257 */
261- export const deletePasskey = async ( input : DeletePasskeyInput ) => {
258+ export const deletePasskey = async ( input : DeletePasskeyInput , config : PasslockClientConfig ) => {
262259 const ERROR_TAG = '@error/DeletePasskeyError' ;
263260 const PAUSED_TAG = '@warning/PasskeyDeletePaused' ;
264261
265- // The endpoint can return either a successful deletion payload or a warning
266- // that the server-side record was already gone.
267- const EndpointResponse = variant ( '_tag' , [ DeletePasskeySuccess , DeletePasskeyWarning ] ) ;
268-
269- // `DELETE /passkeys/[id]` removes the server-side association first.
262+ // `DELETE /passkeys/[id]` removes the server-side association first and
263+ // returns a prepared token for browser-side cleanup.
270264 const serverResult = await fetchData ( {
271265 url : resolve ( `/passkeys/${ encodeURIComponent ( input . passkeyId ) } ` ) ,
272266 method : 'DELETE' ,
273267 body : { } ,
274- on2xx : ( jsonResponse ) => parse ( EndpointResponse , jsonResponse ) ,
268+ on2xx : ( jsonResponse ) => parse ( DeletePasskeySuccess , jsonResponse ) ,
275269 orElse : ( jsonResponse ) => {
276270 const { message } = parse ( Error , jsonResponse ) ;
277271 return { _tag : ERROR_TAG , message } as const ;
@@ -281,14 +275,23 @@ export const deletePasskey = async (input: DeletePasskeyInput) => {
281275 // If the server still trusts the credential, we must stop here.
282276 if ( serverResult . _tag === '@error/DeletePasskeyError' ) return serverResult ;
283277
284- // The local device is already in the desired state from the server's point
285- // of view, so the caller can treat this as a warning rather than a failure.
286- if ( serverResult . _tag === '@warning/PasskeyNotFound' ) return serverResult ;
287-
288278 // Local device deletion is best-effort and browser-dependent.
289- const clientResult = await PasslockBrowser . deletePasskey ( serverResult . deleted ) ;
279+ const clientResult = await PasslockBrowser . deletePasskeys (
280+ { deletePasskeysToken : serverResult . deletePasskeysToken } ,
281+ config
282+ ) ;
283+
284+ if ( clientResult . success && clientResult . warnings . length === 0 ) {
285+ return { _tag : 'DeleteSuccess' } as const ;
286+ }
290287
291- if ( clientResult . success ) return { _tag : 'DeleteSuccess' } as const ;
288+ if ( clientResult . success ) {
289+ const message =
290+ 'The passkey was removed from your account, but browser cleanup returned a warning. ' +
291+ 'Check your device password manager if the passkey still appears.' ;
292+
293+ return { _tag : PAUSED_TAG , message } as const ;
294+ }
292295
293296 if ( clientResult . code === 'PASSKEY_DELETION_UNSUPPORTED' ) {
294297 const message =
@@ -306,12 +309,11 @@ export const deletePasskey = async (input: DeletePasskeyInput) => {
306309/**
307310 * Delete every passkey associated with the current account.
308311 *
309- * `DELETE /passkeys` removes the trusted server-side records and returns the
310- * deleted credentials for the current user. The browser then tries to remove
311- * those credentials from the device. Browser limitations are reported as
312- * warnings so account deletion can continue.
312+ * `DELETE /passkeys` removes the trusted server-side records and returns a
313+ * short-lived token. The browser exchanges that token to request local cleanup.
314+ * Browser limitations are reported as warnings so account deletion can continue.
313315 */
314- export const deleteAccountPasskeys = async ( ) => {
316+ export const deleteAccountPasskeys = async ( config : PasslockClientConfig ) => {
315317 const ERROR_TAG = '@error/DeletePasskeyError' ;
316318 const PAUSED_TAG = '@warning/PasskeyDeletePaused' ;
317319
@@ -327,13 +329,28 @@ export const deleteAccountPasskeys = async () => {
327329 } ) ;
328330
329331 // If this fails, the account would still trust one or more passkeys.
330- if ( serverResult . _tag !== 'DeleteUserPasskeysSuccess' ) {
332+ if ( serverResult . _tag === ERROR_TAG ) {
331333 return serverResult ;
332334 }
333335
334- const clientResult = await PasslockBrowser . deleteUserPasskeys ( serverResult . deleted ) ;
336+ const clientResult = await PasslockBrowser . deletePasskeys (
337+ { deletePasskeysToken : serverResult . deletePasskeysToken } ,
338+ config
339+ ) ;
340+
341+ if ( clientResult . success && clientResult . warnings . length === 0 ) {
342+ return { _tag : 'DeleteSuccess' } as const ;
343+ }
335344
336- if ( clientResult . success ) return { _tag : 'DeleteSuccess' } as const ;
345+ if ( clientResult . success ) {
346+ return {
347+ _tag : PAUSED_TAG ,
348+ message :
349+ 'Passkeys were removed from your account, but browser cleanup returned warnings. ' +
350+ 'Remove any remaining passkeys manually from your device password manager after ' +
351+ 'the account is deleted.'
352+ } as const ;
353+ }
337354
338355 if ( clientResult . code === 'PASSKEY_DELETION_UNSUPPORTED' ) {
339356 return {
0 commit comments