Identity Recovery (M-of-N Guardian Flow)
Guardian approval, recovery states, the owner safety delay and veto, registering a replacement dashboard passkey, and legacy behavior and rollout boundaries.
Overview
Recovery protects a PasskeyBridge dashboard account with M-of-N guardian approval. The owner nominates guardians and an approval threshold. An owner who can still sign in can start a request; a confirmed guardian can start one for a locked-out owner.
Recovery v2 is implemented in source and remains disabled pending hosted validation. It requires both the server rollout switch and the owner's organization switch. A browser preview flag does not authorize recovery. While those gates are off, new requests follow the legacy flow: reaching the threshold completes the request and attempts to email a one-time sign-in link, without replacing passkeys or closing sessions.
When v2 is enabled for the account, approval starts a safety delay. The owner can veto during the permitted window or cancel an open request. After the delay, an authorized owner or confirmed guardian can request an owner-only link to register a new passkey. Verified registration completes recovery and replaces the owner's other dashboard passkeys and stored authentication sessions. Guardian approval alone never signs the owner in.
This flow does not recover a tenant's hosted relying-party end users. Their credentials belong to the separate hosted RP surface described in Passkeys.
Recovery states and timing
The request's server state is authoritative. The dashboard renders its stored milestones and deadlines, with a timezone label.
| V2 state | Meaning |
|---|---|
pending | Waiting for the captured number of distinct guardian approvals |
threshold_met | The threshold transition has been recorded; it advances into the safety delay |
delay | Approval is sufficient, but registration is blocked until the captured delay ends |
ready | The server permits an owner-only recovery-link request |
rebinding | A recovery registration ceremony has opened under its original deadline |
completed | The replacement passkey and credential/session changes committed together |
vetoed | The owner stopped the request during the veto window |
cancelled | The owner cancelled the open request |
expired | The request deadline passed |
The default request lifetime is 72 hours. The safety delay defaults to 24 hours and accepts 24 to 168 whole hours. The server captures the delay when the threshold is reached, so changing the setting does not move an existing request's deadline. Veto is permitted while pending and during the delay before its end; cancellation is available for an open, unexpired v2 request. The server checks both again under the request lock.
A countdown reaching zero does not grant readiness. Select Refresh status for the server's decision. Reading a stored timestamp locally cannot advance a request. Requests whose v2 gate has been disabled remain visible and read-only.
Legacy requests use pending, completed and expired. They do not gain the v2 delay or credential replacement merely because the interface has been updated.
Authorization and actions
shield-recovery is a direct project-gateway function, invoked with supabase.functions.invoke. It is absent from the public worker allowlist: https://api.passkeybridge.io/v1/shield-recovery answers 404. Tenant API keys do not authorize it. The project's publishable key is the gateway credential; the action also needs its user session or purpose-bound recovery credential.
| Action | Audience and credential | Required input beyond action |
|---|---|---|
add_guardian | Signed-in owner | guardian_email, optional threshold |
list_guardians | Signed-in owner | None |
remove_guardian | Signed-in owner | guardian_id |
list_pending_invitations | Signed-in guardian | None |
confirm_guardian | Guardian signed in with the verified invited address | Optional guardian_id |
confirm_by_token | Invited person, single-use invitation token | token, email |
initiate_recovery | Signed-in owner or confirmed guardian | Optional owner_user_id; omitted means the caller |
list_guarded_owners | Signed-in guardian | None |
list_approvable_recoveries | Signed-in guardian | None |
approve_recovery | Signed-in confirmed guardian | request_id |
resend_recovery_signin | V2: signed-in owner or confirmed guardian; legacy: authorized guardian | request_id |
recovery_status | Signed-in owner | None |
veto_recovery | V2 owner session, or owner veto token | request_id for the session lane, or token |
cancel_recovery | V2 signed-in owner | request_id |
set_recovery_delay | V2 signed-in owner | recovery_delay_hours |
rebind_options | Owner-only v2 recovery token; approved first-party Origin | token |
rebind_verify | Single-use v2 registration challenge; the same approved Origin | challenge_handle, complete browser response |
The five v2-only actions require both rollout gates. A v2-marked request never falls back to legacy completion when its gate is disabled. The two public recovery pages send a POST only after an explicit click; opening their URL does not consume a credential.
There is no plan gate. Adding a guardian needs the owner's tenant membership. Guardian reads, approvals and guardian-started recovery resolve the tenant from the guardian relationship, so a confirmed guardian need not join the owner's organization. The handler and database recheck the owner/guardian relationship rather than trusting a tenant supplied by the browser.
Guardian invitations and identity
Add a guardian. The owner supplies the invited address and an approval threshold. The address is lowercased and trimmed, then keyed with HMAC-SHA-256 under the server-held pepper before it enters guardian configuration. The response exposes guardian metadata without the address digest or invitation-token digest.
The invitation contains a single-use token with a seven-day expiry. Delivery uses the address transiently; a mail failure can leave a pending guardian row, so the existence of that row is not proof of delivery. A duplicate pending invitation for the same address is refused.
Confirm. The invitation opens /recover/confirm. The person submits its token and the invited address; both must match a pending, unexpired invitation. This can find or create the invited auth account and bind its user ID. A signed-in person with that verified address can instead confirm from the Recovery tab. Confirmation clears the invitation digest. Recognition accepts the current and previous pepper during a configured overlap.
The auth account retains its email as account data. Guardian configuration keeps the keyed address digest, and the Recovery tab identifies guardians by short opaque record ID and status. The retired share_index display field is no longer returned or written, including on the legacy path. No secret is split across guardians: approval is a counted vote, not secret-share reconstruction.
Remove. Removal records revoked and a timestamp and retains the row for audit. A revoked guardian cannot vote. Removing a guardian does not lower the threshold already captured by an open request.
Threshold and approval semantics
A guardian's configured threshold is an integer from 2 to 10, default 2. At initiation, the request captures the maximum threshold among confirmed guardians, floored at 2. This applies the strictest configured requirement rather than whichever guardian happens to be read first. The request is refused when no confirmed guardians exist or the threshold exceeds the confirmed pool.
The stored threshold_required does not change when guardians are later added or removed. Each counted approval must come from a distinct, currently confirmed guardian for that owner and tenant. The database locks the request, checks expiry and state, and records the vote atomically. V2 uses a unique guardian-approval record; a duplicate vote is refused with 409.
V2: reaching M approvals records the threshold transition and safety delay. recovery_complete remains false. No sign-in link is issued at this point, and additional approvals cannot bypass the delay. State changes and notices are recorded with their transition; later delivery is performed by the notice worker.
Legacy: the existing atomic approval routine completes at the threshold and claims signin_issued_at before attempting the one-time sign-in email. Its sign_in result is sent, already or failed; a failed send releases the claim for an authorized resend. Existing passkeys and sessions remain valid.
Branch on the returned state and acknowledgement. An approval count alone is never proof that v2 registration or recovery has completed.
Recovery tab
Open Dashboard > Recovery. Guardian configuration and the existing invitation flow remain available. Each section loads independently and shows an error if its request fails.
- Add guardian asks for the invited address and approvals required, from 2 to 10. Guardians appear by short opaque ID, with their lifecycle badge and recorded dates. Removal requires confirmation.
- Guardian invitations lets the invited person confirm while signed in with the verified invited address.
- Accounts you can recover lets a confirmed guardian start recovery for that opaque owner ID. An open request disables another start.
- Recoveries awaiting your approval shows the stored count and threshold. Enabled v2 requests offer approval only while pending. Delay, ready, rebinding and disabled requests display their actual state without offering another vote.
- Recovery timeline shows the latest v2 request's state badge, approvals, stored milestones and delay countdown. Earlier requests stay in history. Refresh asks the server for the latest state.
- Veto recovery and Cancel recovery require a confirmation dialog. Controls appear only while locally eligible; the server makes the final authorization and timing decision.
- Email recovery link requests the owner-only link when the request is ready, or checks whether a rebinding lease can be renewed. Success means queued or already queued, not delivered. The dashboard never receives the bearer.
- Recovery settings, under the advanced section, sets the safety delay to 24–168 whole hours. The saved value affects future threshold captures.
Settings appear only when recovery_status returns enabled v2 metadata for the signed-in owner's resolved tenant. With either gate off that metadata is absent. A missing settings row uses 24 hours; an unreadable or malformed policy fails rather than appearing as a successful empty response.
Owner veto and passkey replacement pages
Owner veto. The initiation notice can contain /recover/veto#token=.... The page explains the action and waits for Veto recovery. It reports success only after the server confirms vetoed. An expired, used or out-of-window link cannot stop the request.
Replacement passkey. After the safety delay, an authorized owner or guardian requests an owner-only notice using resend_recovery_signin. On v2 this queues a rebind link rather than a sign-in link. The notice worker resolves the owner's current verified auth address at send time. The link expires 15 minutes after issuance and opens /recover/rebind#token=....
Select Register a new passkey to redeem the token and open the browser ceremony. Registration requires a discoverable passkey and user verification by the authenticator. Its user handle is random and its display labels contain no account address or personal name. The page forwards the complete server options to the browser SDK and the complete SDK result to the verifier. First-party Origin, RP, challenge, user presence and user verification are checked by the server.
Cancelling the browser prompt keeps the same options only in page memory until their original deadline. The retry control opens those options again without spending the mailed token a second time. Closing or reloading the page loses that in-memory ceremony. Request a new link after the existing lease expires.
A failed or lost verification response may follow a committed completion. After submitting verification, the page discards retry authority and does not replay that challenge. Try signing in with the new passkey; if recovery did not complete, obtain a fresh link after the lease expires. The page reports completed only after the server acknowledges it, and completion creates no session on this page.
Link privacy. The existing blocking startup script captures the bearer into closure memory and removes the fragment or legacy query token before application and analytics startup. Action routes are excluded from the page analytics paths. The bearer is not put in browser storage, page markup or logs. Failed URL redaction exposes no bearer. New notices use fragments; client redaction cannot undo the initial HTTP document request of an older query-token URL. Opening a page, following a GET or scanning a link does not redeem it.
Credential and session revocation
V2 completion is one database transaction. It inserts and reads back the new first-party credential, revokes the owner's other first-party dashboard credentials across tenants, deletes stored authentication sessions and refresh tokens, records completion, and queues completion notices. A failure rolls the transaction back. The new passkey is the one left unrevoked by that completion.
The owner then signs in with the new passkey. Already-issued stateless access tokens keep their normal expiry; deleting stored sessions does not make every previously issued JWT vanish instantly. Hosted-RP end-user credentials are outside this flow. Legacy recovery retains its earlier behavior and changes neither passkeys nor sessions.
Veto, cancellation and expiry prevent subsequent use of that request's recovery authority. A notice already in transit can become obsolete; holding an old link does not authorize a terminal request. Queued notices and provider delivery are separate from the committed state change.
Stored records and privacy
Guardian rows retain the owner and bound guardian IDs, keyed address digest, threshold, invitation-token digest and expiry, status and lifecycle times. The invitation-token digest is unavailable to client reads. The deprecated display-slot column is retired; no cryptographic share material exists.
Requests retain the owner, captured threshold, approval count, compatibility status, v2 state and initiator marker, creation and expiry times, delay end, veto/completion times and new credential reference. A legacy request has no v2 initiator marker. Private veto, rebind and challenge material is excluded from client projections. The server stores bearer digests and purpose-bound encrypted notice material rather than returning recovery bearers in status responses.
V2 adds distinct approval records, owner-and-tenant delay settings and a durable notice outbox. Client roles cannot insert a vote, mark a guardian confirmed or complete a request through the data API. Service-only routines recheck the caller, tenant gates, clocks and state and write the transition audit in the same transaction.
Recipient addresses are resolved for delivery from the auth account, or used transiently for an invitation. They are not written into recovery rows or audit payloads. Auth contact details remain account data, described on the data-handling page.
Reading status from a signed-in client
The dashboard uses the direct functions client. Check both the transport result and any function error before interpreting state:
import { supabase } from "@/integrations/supabase/client";
const { data, error } = await supabase.functions.invoke("shield-recovery", {
body: { action: "recovery_status" },
});
if (error || !data || data.error || !Array.isArray(data.requests)) {
throw new Error("Recovery status could not be loaded");
}
const requests = data.requests;
const recoveryV2Enabled = data.recovery_v2_enabled === true;The response contains the owner's last five requests. Enabled v2 metadata also carries recovery_delay_hours. Neither the client flag nor a countdown authorizes a mutation. Use the dashboard's confirmed owner controls and the original notice pages for veto and registration; do not log tokens, replace signed browser fields or retry a submitted registration handle. The OpenAPI recovery entry documents all seventeen accepted actions.
Errors and next steps
V2 returns closed error codes. A public page maps them to fixed guidance rather than rendering raw database, browser or transport diagnostics.
| HTTP | Code | Meaning and next step |
|---|---|---|
| 404 | feature_disabled | The new flow is unavailable for this account; a browser flag cannot enable it |
| 403 | forbidden or origin_not_permitted | The caller or page origin cannot take the action; use the original notice or sign in to review |
| 404 | not_found | The request or usable link was not found |
| 409 | wrong_state | The request has moved; refresh status |
| 409 | duplicate_vote | This guardian's approval is already recorded |
| 409 | veto_window_closed | The veto deadline has passed; sign in to review the request |
| 410 | expired or rebind_token_expired | The request or link expired; obtain a new eligible request or link |
| 423 | in_delay | The safety delay has not ended |
| 400 | invalid_rebind_token | The token cannot authorize registration |
| 409 | rebind_in_progress | Use the page holding the active ceremony, or request a fresh link after its lease expires |
| 409 / 503 | rebind_delivery_held / rebind_delivery_unavailable | Mail could not be queued; review the current request before trying again |
| 400 | invalid_delay | Choose 24–168 whole hours |
| 429 | Rate-limit refusal | Wait before another attempt |
A verification failure consumes its claimed challenge. Obtain fresh authority after the lease expires; do not replay that submission. An unavailable response is not proof that a mutation failed to commit. Legacy actions retain their earlier error contract.
Confirming the outcome
After adding a guardian, confirm the Pending row and then the Confirmed badge after acceptance. A pending row alone does not prove that an invitation was delivered.
After initiating v2 recovery, check the request timeline. Each distinct approval advances the stored count. At the threshold the request enters delay, without a sign-in link. After the deadline, refresh for the server's state. An owner veto or cancellation must receive its own terminal acknowledgement before the interface reports success.
After requesting an owner-only link, the acknowledgement confirms queueing. Delivery depends on the enabled notice worker and provider. Completion is confirmed by the server's completed response and subsequent sign-in with the replacement passkey, not by an email button or approval count.
The source and local tests do not establish hosted deployment, provider delivery or rollout activation. Both recovery switches and notice delivery remain separate operator-controlled release steps.