1234567891011121314151617181920212223242526272829303132export function installPeachPasskeyProxy(): void {33 const w = window as unknown as Record<string, unknown>;34 if (w.__peachPasskeyProxyInstalled) return;35 w.__peachPasskeyProxyInstalled = true;3637 const credentials = navigator.credentials;38 if (!credentials || typeof credentials.create !== "function" || typeof credentials.get !== "function") {39 return;40 }4142 const originalCreate = credentials.create.bind(credentials);43 const originalGet = credentials.get.bind(credentials);4445 credentials.create = async function patchedCreate(46 options?: CredentialCreationOptions47 ): Promise<Credential | null> {48 if (!options?.publicKey) return originalCreate(options);4950 const publicKey = options.publicKey;51 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));52 const clientDataJSON = encodeJsonBytes({53 type: "webauthn.create",54 challenge,55 origin: window.location.origin,56 crossOrigin: false,57 });5859 const userId = bufferToBase64url(toArrayBuffer(publicKey.user.id));6061 const response = await sendToBackground({62 type: "PASSKEY_CREATE",63 payload: {64 rpId: publicKey.rp.id ?? window.location.hostname,65 rpName: publicKey.rp.name,66 userId,67 userName: publicKey.user.name,68 userDisplayName: publicKey.user.displayName,69 challenge,70 clientDataJSON: Array.from(clientDataJSON),71 origin: window.location.origin,72 pubKeyCredParams: publicKey.pubKeyCredParams,73 authenticatorSelection: publicKey.authenticatorSelection,74 timeout: publicKey.timeout,75 attestation: publicKey.attestation,76 },77 });7879 if (!response?.ok) {80 const error = response?.error;81 if (error === "user_cancelled") {82 83 84 85 throw new DOMException(86 "The operation either timed out or was not allowed.",87 "NotAllowedError"88 );89 }90 if (error === "pro_required") {91 92 93 window.dispatchEvent(94 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } })95 );96 return originalCreate(options);97 }98 99 100 101 102 return originalCreate(options);103 }104105 return buildPublicKeyCredential(response.credential);106 };107108 credentials.get = async function patchedGet(109 options?: CredentialRequestOptions110 ): Promise<Credential | null> {111 if (!options?.publicKey) return originalGet(options);112113 const publicKey = options.publicKey;114 const rpId = publicKey.rpId ?? window.location.hostname;115 const allowCredentials = (publicKey.allowCredentials ?? []).map((c) => ({116 id: bufferToBase64url(toArrayBuffer(c.id)),117 type: c.type,118 }));119120 121 122 const has = await sendToBackground({123 type: "PASSKEY_HAS_CREDENTIALS",124 payload: { rpId, allowCredentials },125 });126 if (!has?.ok || !has.hasCredentials) {127 return originalGet(options);128 }129130 const challenge = bufferToBase64url(toArrayBuffer(publicKey.challenge));131 const clientDataJSON = encodeJsonBytes({132 type: "webauthn.get",133 challenge,134 origin: window.location.origin,135 crossOrigin: false,136 });137138 const response = await sendToBackground({139 type: "PASSKEY_GET",140 payload: {141 rpId,142 challenge,143 clientDataJSON: Array.from(clientDataJSON),144 origin: window.location.origin,145 allowCredentials,146 userVerification: publicKey.userVerification,147 },148 });149150 if (!response?.ok) {151 const error = response?.error;152 if (error === "user_cancelled") {153 throw new DOMException(154 "The operation either timed out or was not allowed.",155 "NotAllowedError"156 );157 }158 if (error === "pro_required") {159 window.dispatchEvent(160 new CustomEvent("peach:passkey-pro-gate", { detail: { feature: "passkeys" } })161 );162 return originalGet(options);163 }164 return originalGet(options);165 }166167 return buildPublicKeyCredential(response.credential);168 };169170 171172 function toArrayBuffer(view: BufferSource): ArrayBuffer {173 if (view instanceof ArrayBuffer) return view;174 const u8 = view as ArrayBufferView;175 const out = new Uint8Array(u8.byteLength);176 out.set(new Uint8Array(u8.buffer, u8.byteOffset, u8.byteLength));177 return out.buffer;178 }179180 function bufferToBase64url(buffer: ArrayBuffer): string {181 const bytes = new Uint8Array(buffer);182 let str = "";183 for (let i = 0; i < bytes.length; i += 1) str += String.fromCharCode(bytes[i]!);184 return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");185 }186187 function base64urlToBuffer(value: string): ArrayBuffer {188 const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(189 Math.ceil(value.length / 4) * 4,190 "="191 );192 const raw = atob(padded);193 const bytes = new Uint8Array(raw.length);194 for (let i = 0; i < raw.length; i += 1) bytes[i] = raw.charCodeAt(i);195 return bytes.buffer;196 }197198 function encodeJsonBytes(value: unknown): Uint8Array {199 return new TextEncoder().encode(JSON.stringify(value));200 }201202 203204205206207208209 function sendToBackground(message: object): Promise<any> {210 return new Promise((resolve) => {211 const id = generateRequestId();212 const handler = (event: MessageEvent) => {213 if (event.source !== window) return;214 const data = event.data;215 if (!data || typeof data !== "object") return;216 if ((data as { type?: string }).type !== "PEACH_PASSKEY_RESPONSE") return;217 if ((data as { id?: string }).id !== id) return;218 cleanup();219 resolve((data as { payload?: unknown }).payload);220 };221 const timer = setTimeout(() => {222 cleanup();223 resolve({ ok: false, error: "timeout" });224 }, 60_000);225 function cleanup() {226 window.removeEventListener("message", handler);227 clearTimeout(timer);228 }229 window.addEventListener("message", handler);230 window.postMessage({ type: "PEACH_PASSKEY_REQUEST", id, payload: message }, "*");231 });232 }233234 function generateRequestId(): string {235 if (typeof crypto.randomUUID === "function") return crypto.randomUUID();236 const bytes = crypto.getRandomValues(new Uint8Array(16));237 return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");238 }239240 241242243244245246247248249250251252253 function buildPublicKeyCredential(cred: any): PublicKeyCredential {254 const credentialId = base64urlToBuffer(cred.id);255 const clientDataJSON = new Uint8Array(cred.clientDataJSON).buffer;256257 if (cred.type === "create") {258 const attestationObject = base64urlToBuffer(cred.attestationObject);259 const authenticatorData = base64urlToBuffer(cred.authenticatorData);260 const publicKey = base64urlToBuffer(cred.publicKey);261 return {262 id: cred.id,263 rawId: credentialId,264 type: "public-key",265 authenticatorAttachment: "platform",266 response: {267 clientDataJSON,268 attestationObject,269 getTransports: () => ["internal"],270 getAuthenticatorData: () => authenticatorData,271 getPublicKey: () => publicKey,272 getPublicKeyAlgorithm: () => cred.algorithm ?? -7,273 } as AuthenticatorAttestationResponse,274 getClientExtensionResults: () => ({}),275 toJSON: () => ({276 id: cred.id,277 rawId: cred.id,278 type: "public-key",279 response: {280 clientDataJSON: bufferToBase64url(clientDataJSON),281 attestationObject: cred.attestationObject,282 authenticatorData: cred.authenticatorData,283 publicKey: cred.publicKey,284 publicKeyAlgorithm: cred.algorithm ?? -7,285 transports: ["internal"],286 },287 authenticatorAttachment: "platform",288 clientExtensionResults: {},289 }),290 } as unknown as PublicKeyCredential;291 }292293 const authenticatorData = base64urlToBuffer(cred.authenticatorData);294 const signature = base64urlToBuffer(cred.signature);295 const userHandle = cred.userHandle ? base64urlToBuffer(cred.userHandle) : null;296 return {297 id: cred.id,298 rawId: credentialId,299 type: "public-key",300 authenticatorAttachment: "platform",301 response: {302 clientDataJSON,303 authenticatorData,304 signature,305 userHandle,306 } as AuthenticatorAssertionResponse,307 getClientExtensionResults: () => ({}),308 toJSON: () => ({309 id: cred.id,310 rawId: cred.id,311 type: "public-key",312 response: {313 clientDataJSON: bufferToBase64url(clientDataJSON),314 authenticatorData: cred.authenticatorData,315 signature: cred.signature,316 userHandle: cred.userHandle ?? null,317 },318 authenticatorAttachment: "platform",319 clientExtensionResults: {},320 }),321 } as unknown as PublicKeyCredential;322 }323}324325installPeachPasskeyProxy();