tempest-react-sdk 0.39.1 → 0.40.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"passkey.cjs","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.js","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"passkey.js","names":[],"sources":["../../src/auth/passkey.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the WebAuthn client half: the JSON\n * shapes both ceremonies exchange with a backend, base64url ↔ ArrayBuffer, the\n * capability probes and the error classifier. The two ceremonies are near-mirrors\n * that must not drift — register and authenticate encode the same credential fields\n * in the same order — and the file's docstring is also the specification of the four\n * backend routes it expects.\n */\n/**\n * Classified reason a passkey ceremony did not produce a credential.\n *\n * The kinds group the raw `DOMException.name` values the way a UI has to branch\n * on them, which is *not* how the spec groups them:\n *\n * - `\"cancelled\"` covers `NotAllowedError`, which the browser raises both when\n * the user dismissed the sheet and when the ceremony timed out. They are\n * indistinguishable **by design** — telling a site \"the user has no credential\n * for you\" would leak account existence — so a UI must treat them as one thing.\n * - `\"already-registered\"` (`InvalidStateError`) is not really a failure: this\n * device already holds a credential for this user. The correct reaction is\n * \"you are already set up on this device\", never a red error.\n * - `\"rp-mismatch\"` (`SecurityError`) is the single most common integration bug:\n * `rp.id` must equal the page's domain or a registrable parent of it.\n */\nexport type PasskeyErrorKind =\n | \"unsupported\"\n | \"insecure\"\n | \"cancelled\"\n | \"already-registered\"\n | \"not-supported\"\n | \"rp-mismatch\"\n | \"invalid-options\"\n | \"aborted\"\n | \"unknown\";\n\n/** Which ceremony was running, so the message can name it. */\nexport type PasskeyCeremony = \"register\" | \"authenticate\";\n\n/**\n * A passkey failure carrying a stable {@link PasskeyErrorKind} plus an English\n * message safe to show a user.\n *\n * A class rather than the plain `{ kind, message }` object the media classifier\n * returns, because these surface by rejecting a promise: an `Error` subclass keeps\n * stack traces, `instanceof` checks and logging intact, and `kind` is what code\n * branches on.\n */\nexport class PasskeyError extends Error {\n /** Stable, branchable classification. */\n readonly kind: PasskeyErrorKind;\n\n /**\n * Build a classified passkey error.\n *\n * @param kind - The classification a UI branches on.\n * @param message - English, user-safe explanation.\n * @param cause - The original thrown value, when there was one.\n */\n constructor(kind: PasskeyErrorKind, message: string, cause?: unknown) {\n super(message);\n this.name = \"PasskeyError\";\n this.kind = kind;\n this.cause = cause;\n }\n}\n\n/**\n * Minimal subset of `navigator.credentials` the passkey client touches.\n *\n * Declared here — the `<X>Like` pattern the SDK's adapters use — for two reasons:\n * jsdom has no `navigator.credentials` at all, so tests must inject a double; and\n * `mediation: \"conditional\"` is newer than some TypeScript DOM libs, which would\n * otherwise reject the call that makes autofill work.\n */\nexport interface CredentialsContainerLike {\n /** Runs the registration ceremony. */\n create(options: {\n publicKey: PublicKeyCredentialCreationOptions;\n signal?: AbortSignal;\n }): Promise<Credential | null>;\n /** Runs the authentication ceremony. */\n get(options: {\n publicKey: PublicKeyCredentialRequestOptions;\n signal?: AbortSignal;\n mediation?: string;\n }): Promise<Credential | null>;\n}\n\n/** How the browser should surface the authentication ceremony. */\nexport type PasskeyMediation = \"optional\" | \"conditional\" | \"required\" | \"silent\";\n\n/**\n * Server-issued registration options, in the base64url JSON shape every WebAuthn\n * backend speaks (`PublicKeyCredentialCreationOptionsJSON` in the spec).\n *\n * `challenge`, `user.id` and every `excludeCredentials[].id` are **base64url**\n * strings here and `ArrayBuffer`s in the DOM API. Converting them is the plumbing\n * this client owns.\n */\nexport interface PasskeyCreationOptionsJSON {\n /** Base64url server challenge. Single-use; the server must remember it. */\n challenge: string;\n /** Relying party. `id` defaults to the client's `rpId`, then to the origin. */\n rp: { name: string; id?: string };\n /** The account. `id` is base64url of an opaque, stable user handle. */\n user: { id: string; name: string; displayName: string };\n /** Allowed COSE algorithms. Defaults to {@link DEFAULT_PUB_KEY_CRED_PARAMS}. */\n pubKeyCredParams?: { type: \"public-key\"; alg: number }[];\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Credentials this user already has, so the authenticator refuses a duplicate. */\n excludeCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Resident-key / user-verification / attachment requirements. */\n authenticatorSelection?: AuthenticatorSelectionCriteria;\n /** Attestation conveyance. Leave unset (`\"none\"`) unless you verify it. */\n attestation?: AttestationConveyancePreference;\n /** Client extension inputs (`credProps`, `largeBlob`, …). */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** Server-issued authentication options, base64url JSON. */\nexport interface PasskeyRequestOptionsJSON {\n /** Base64url server challenge. */\n challenge: string;\n /** Relying party id. Defaults to the client's `rpId`, then to the origin. */\n rpId?: string;\n /** Ceremony timeout in ms. Defaults to the client's `timeoutMs`. */\n timeout?: number;\n /** Restrict to these credentials. **Omit it** for usernameless / autofill flows. */\n allowCredentials?: { id: string; type: \"public-key\"; transports?: string[] }[];\n /** Whether the authenticator must verify the user (biometric / PIN). */\n userVerification?: UserVerificationRequirement;\n /** Client extension inputs. */\n extensions?: AuthenticationExtensionsClientInputs;\n}\n\n/** What the client sends to the backend to finish registration. */\nexport interface PasskeyRegistrationJSON {\n /** Base64url credential id. */\n id: string;\n /** Same bytes as `id`; both are sent because servers differ on which they read. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` (this device) or `\"cross-platform\"` (a phone or key). */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data — the server re-checks challenge, origin and type. */\n clientDataJSON: string;\n /** Base64url attestation object, holding the new public key. */\n attestationObject: string;\n /** Transports the authenticator advertises, when the browser exposes them. */\n transports?: string[];\n /** COSE algorithm of the new key, when the browser exposes it. */\n publicKeyAlgorithm?: number;\n /** Base64url SPKI public key, when the browser exposes it. */\n publicKey?: string;\n /** Base64url authenticator data, when the browser exposes it. */\n authenticatorData?: string;\n };\n /** Extension outputs — `credProps.rk` tells you whether it is discoverable. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** What the client sends to the backend to finish authentication. */\nexport interface PasskeyAuthenticationJSON {\n /** Base64url credential id — the server looks up the stored public key by it. */\n id: string;\n /** Same bytes as `id`. */\n rawId: string;\n type: \"public-key\";\n /** `\"platform\"` or `\"cross-platform\"`. */\n authenticatorAttachment: string | null;\n response: {\n /** Base64url client data. */\n clientDataJSON: string;\n /** Base64url authenticator data, carrying the signature counter. */\n authenticatorData: string;\n /** Base64url signature over `authenticatorData || sha256(clientDataJSON)`. */\n signature: string;\n /** Base64url user handle — present on discoverable credentials, else null. */\n userHandle: string | null;\n };\n /** Extension outputs. */\n clientExtensionResults: AuthenticationExtensionsClientOutputs;\n}\n\n/** Per-call knobs for {@link PasskeyClient.register}. */\nexport interface PasskeyRegisterInit {\n /** Cancel the ceremony (closes the browser sheet). */\n signal?: AbortSignal;\n}\n\n/** Per-call knobs for {@link PasskeyClient.authenticate}. */\nexport interface PasskeyAuthenticateInit extends PasskeyRegisterInit {\n /**\n * `\"conditional\"` is the autofill flow: no modal, the browser offers passkeys\n * inside a field marked `autocomplete=\"webauthn\"`. Requires a `signal`, and\n * only one conditional request may be live per page.\n */\n mediation?: PasskeyMediation;\n}\n\n/** Framework-free WebAuthn client. Build one with {@link createPasskeyClient}. */\nexport interface PasskeyClient {\n /** Run the registration ceremony and return the JSON your backend verifies. */\n register(\n options: PasskeyCreationOptionsJSON,\n init?: PasskeyRegisterInit,\n ): Promise<PasskeyRegistrationJSON>;\n /** Run the authentication ceremony and return the JSON your backend verifies. */\n authenticate(\n options: PasskeyRequestOptionsJSON,\n init?: PasskeyAuthenticateInit,\n ): Promise<PasskeyAuthenticationJSON>;\n /**\n * Whether this client can run a ceremony at all — the WebAuthn API exists, or a\n * `credentials` container was injected.\n */\n isSupported(): boolean;\n /** Whether this device has a built-in authenticator (Face ID, Hello, …). */\n isPlatformAuthenticatorAvailable(): Promise<boolean>;\n /** Whether autofill-driven (`\"conditional\"`) requests are available. */\n isConditionalMediationAvailable(): Promise<boolean>;\n}\n\n/** Options for {@link createPasskeyClient}. */\nexport interface CreatePasskeyClientOptions {\n /**\n * Default relying-party id, applied when the server options omit one. Must be\n * the page's domain or a registrable parent of it (`app.acme.com` may use\n * `acme.com`, never the other way round).\n */\n rpId?: string;\n /** Default ceremony timeout in ms. Default `60_000`. */\n timeoutMs?: number;\n /** `navigator.credentials` replacement, for tests. */\n credentials?: CredentialsContainerLike;\n}\n\n/**\n * COSE algorithms offered when the server sends no `pubKeyCredParams`, in\n * preference order.\n *\n * `-8` is Ed25519, which modern authenticators prefer and which produces the\n * smallest signatures. `-7` is ES256, the one algorithm every WebAuthn\n * authenticator supports. `-257` is RS256, needed for TPM-backed Windows Hello.\n * Offering all three is what avoids a `NotSupportedError` on some device you do\n * not own; a server that cannot verify one of them should send its own list.\n */\nexport const DEFAULT_PUB_KEY_CRED_PARAMS: { type: \"public-key\"; alg: number }[] = [\n { type: \"public-key\", alg: -8 },\n { type: \"public-key\", alg: -7 },\n { type: \"public-key\", alg: -257 },\n];\n\n/** Static members of `PublicKeyCredential` that are not in every DOM lib yet. */\ninterface PublicKeyCredentialStatics {\n isUserVerifyingPlatformAuthenticatorAvailable?: () => Promise<boolean>;\n isConditionalMediationAvailable?: () => Promise<boolean>;\n}\n\n/** The browser-supplied extras on an attestation response, all optional. */\ninterface AttestationExtras {\n getTransports?: () => string[];\n getPublicKey?: () => ArrayBuffer | null;\n getPublicKeyAlgorithm?: () => number;\n getAuthenticatorData?: () => ArrayBuffer;\n}\n\nfunction publicKeyCredentialStatics(): PublicKeyCredentialStatics | undefined {\n return (globalThis as { PublicKeyCredential?: PublicKeyCredentialStatics }).PublicKeyCredential;\n}\n\n/**\n * Decode a base64url string into bytes.\n *\n * WebAuthn transports every binary field as base64url (`-`/`_`, no padding)\n * because that is what survives JSON, while the DOM API insists on\n * `ArrayBuffer`. Getting this pair wrong — usually by feeding plain base64 to\n * `atob` and losing the last byte — is the classic broken-WebAuthn bug, which is\n * why the SDK owns it instead of leaving it to each app.\n *\n * @param value - Base64url text, with or without `=` padding.\n * @returns The decoded bytes.\n */\nexport function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer> {\n const base64 = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), \"=\");\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\n\n/**\n * Encode bytes as an unpadded base64url string.\n *\n * @param value - Bytes to encode, as a view or a raw buffer.\n * @returns Base64url text, safe to put in JSON and in a URL.\n */\nexport function bytesToBase64Url(value: ArrayBuffer | Uint8Array): string {\n const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n/**\n * Whether this browser exposes WebAuthn at all.\n *\n * Note that \"supported\" is not \"usable\": WebAuthn also requires a secure context,\n * and a device may have no authenticator. Use\n * {@link isPlatformAuthenticatorAvailable} before offering a passkey button.\n */\nexport function isPasskeySupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n navigator.credentials !== undefined &&\n typeof navigator.credentials.create === \"function\" &&\n publicKeyCredentialStatics() !== undefined\n );\n}\n\n/**\n * Whether the device has a **built-in** authenticator — Face ID, Touch ID,\n * Windows Hello, an Android screen lock.\n *\n * This is the check that decides whether \"Entrar com passkey\" may be shown at\n * all. `isPasskeySupported()` is true on a desktop with no biometrics and no\n * security key, and offering a passkey there sends the user into a sheet that can\n * only be cancelled. A `false` here does not forbid passkeys — a phone can still\n * be used over hybrid/QR — it means the flow needs a second step, so present it\n * as \"usar meu celular\", not as one tap.\n *\n * @returns `false` when the API is missing, so a caller never has to null-check.\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isUserVerifyingPlatformAuthenticatorAvailable) return false;\n try {\n return await statics.isUserVerifyingPlatformAuthenticatorAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Whether autofill-driven passkeys (`mediation: \"conditional\"`) are available.\n *\n * @returns `false` when the API is missing or throws.\n */\nexport async function isConditionalMediationAvailable(): Promise<boolean> {\n const statics = publicKeyCredentialStatics();\n if (!statics?.isConditionalMediationAvailable) return false;\n try {\n return await statics.isConditionalMediationAvailable();\n } catch {\n return false;\n }\n}\n\n/**\n * Map a thrown WebAuthn failure onto a {@link PasskeyError}.\n *\n * The secure-context check runs first for the same reason it does in the media\n * classifier: over plain HTTP the whole API is absent or refuses, and reporting\n * \"not supported\" sends a developer hunting for a polyfill for something an\n * `https://` URL fixes.\n *\n * @param error - Whatever `navigator.credentials` rejected with.\n * @param ceremony - Which ceremony was running, for the message.\n * @returns The classified error, ready to throw or to show.\n */\nexport function classifyPasskeyError(error: unknown, ceremony: PasskeyCeremony): PasskeyError {\n if (error instanceof PasskeyError) return error;\n\n if (typeof window !== \"undefined\" && !window.isSecureContext) {\n return new PasskeyError(\"insecure\", \"Passkeys require a secure (HTTPS) connection.\", error);\n }\n\n if (error instanceof DOMException) {\n switch (error.name) {\n case \"NotAllowedError\":\n return new PasskeyError(\n \"cancelled\",\n \"The passkey prompt was dismissed or timed out. The browser does not say which — it must not reveal whether a credential exists.\",\n error,\n );\n case \"InvalidStateError\":\n return new PasskeyError(\n \"already-registered\",\n \"This device already has a passkey for this account. Sign in with it instead of creating another.\",\n error,\n );\n case \"NotSupportedError\":\n return new PasskeyError(\n \"not-supported\",\n \"No authenticator here supports the requested algorithms. Check the server's pubKeyCredParams.\",\n error,\n );\n case \"SecurityError\":\n return new PasskeyError(\n \"rp-mismatch\",\n \"The relying-party id does not match this origin. rp.id must be the page's domain or a registrable parent of it.\",\n error,\n );\n case \"AbortError\":\n return new PasskeyError(\"aborted\", \"The passkey request was aborted.\", error);\n }\n }\n\n if (error instanceof TypeError) {\n return new PasskeyError(\n \"invalid-options\",\n `The ${ceremony} options the server sent are malformed. Check that challenge, user.id and credential ids are base64url.`,\n error,\n );\n }\n\n return new PasskeyError(\n \"unknown\",\n error instanceof Error\n ? error.message\n : `Unexpected error during the passkey ${ceremony} ceremony.`,\n error,\n );\n}\n\nfunction toDescriptors(\n list: { id: string; type: \"public-key\"; transports?: string[] }[] | undefined,\n): PublicKeyCredentialDescriptor[] | undefined {\n if (!list) return undefined;\n return list.map((item) => ({\n id: base64UrlToBytes(item.id) as unknown as BufferSource,\n type: item.type,\n transports: item.transports as AuthenticatorTransport[] | undefined,\n }));\n}\n\n/**\n * Build a WebAuthn client: the base64url ↔ `ArrayBuffer` plumbing, the two\n * ceremonies, and one classified error type.\n *\n * ## What your backend must do\n *\n * This is the **client half only**, and a WebAuthn client that documents only its\n * own half is unusable. Four routes are yours to implement:\n *\n * 1. `POST /webauthn/register/begin` → a {@link PasskeyCreationOptionsJSON}. Mint a\n * random `challenge` (≥16 bytes), store it against the session, and list the\n * user's existing credentials in `excludeCredentials`.\n * 2. `POST /webauthn/register/finish` ← a {@link PasskeyRegistrationJSON}. Verify\n * the challenge, `origin` and `type` inside `clientDataJSON`, parse the\n * attestation object, then store the credential id, public key and signature\n * counter.\n * 3. `POST /webauthn/signin/begin` → a {@link PasskeyRequestOptionsJSON}. New\n * challenge. Omit `allowCredentials` for a usernameless or autofill flow.\n * 4. `POST /webauthn/signin/finish` ← a {@link PasskeyAuthenticationJSON}. Look the\n * credential up by `id`, verify the signature over\n * `authenticatorData || sha256(clientDataJSON)`, and reject a signature counter\n * that did not grow (a clone). Only then issue your session token.\n *\n * @param options - Defaults applied when the server options omit them.\n * @returns A client usable from anywhere — React, a plain form, a worker.\n *\n * @example\n * const passkeys = createPasskeyClient({ rpId: \"acme.com\" });\n *\n * const options = await api.post(\"/webauthn/register/begin\");\n * const credential = await passkeys.register(options);\n * await api.post(\"/webauthn/register/finish\", { body: credential });\n */\nexport function createPasskeyClient(options: CreatePasskeyClientOptions = {}): PasskeyClient {\n const { rpId, timeoutMs = 60_000, credentials } = options;\n\n function container(): CredentialsContainerLike {\n if (credentials) return credentials;\n if (!isPasskeySupported()) {\n throw new PasskeyError(\n \"unsupported\",\n typeof window !== \"undefined\" && !window.isSecureContext\n ? \"Passkeys require a secure (HTTPS) connection.\"\n : \"This browser does not support passkeys (WebAuthn).\",\n );\n }\n return navigator.credentials as unknown as CredentialsContainerLike;\n }\n\n async function register(\n json: PasskeyCreationOptionsJSON,\n init: PasskeyRegisterInit = {},\n ): Promise<PasskeyRegistrationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialCreationOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rp: { name: json.rp.name, id: json.rp.id ?? rpId },\n user: {\n id: base64UrlToBytes(json.user.id) as unknown as BufferSource,\n name: json.user.name,\n displayName: json.user.displayName,\n },\n pubKeyCredParams: json.pubKeyCredParams ?? DEFAULT_PUB_KEY_CRED_PARAMS,\n timeout: json.timeout ?? timeoutMs,\n excludeCredentials: toDescriptors(json.excludeCredentials),\n authenticatorSelection: json.authenticatorSelection,\n attestation: json.attestation,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.create({ publicKey, signal: init.signal });\n } catch (error) {\n throw classifyPasskeyError(error, \"register\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAttestationResponse & AttestationExtras;\n const publicKeyBytes = response.getPublicKey?.();\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n attestationObject: bytesToBase64Url(response.attestationObject),\n transports: response.getTransports?.(),\n publicKeyAlgorithm: response.getPublicKeyAlgorithm?.(),\n publicKey: publicKeyBytes ? bytesToBase64Url(publicKeyBytes) : undefined,\n authenticatorData: response.getAuthenticatorData\n ? bytesToBase64Url(response.getAuthenticatorData())\n : undefined,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n async function authenticate(\n json: PasskeyRequestOptionsJSON,\n init: PasskeyAuthenticateInit = {},\n ): Promise<PasskeyAuthenticationJSON> {\n const api = container();\n const publicKey: PublicKeyCredentialRequestOptions = {\n challenge: base64UrlToBytes(json.challenge) as unknown as BufferSource,\n rpId: json.rpId ?? rpId,\n timeout: json.timeout ?? timeoutMs,\n allowCredentials: toDescriptors(json.allowCredentials),\n userVerification: json.userVerification,\n extensions: json.extensions,\n };\n\n let credential: Credential | null;\n try {\n credential = await api.get({\n publicKey,\n signal: init.signal,\n mediation: init.mediation,\n });\n } catch (error) {\n throw classifyPasskeyError(error, \"authenticate\");\n }\n if (!credential) {\n throw new PasskeyError(\"unknown\", \"The authenticator returned no credential.\");\n }\n\n const typed = credential as PublicKeyCredential;\n const response = typed.response as AuthenticatorAssertionResponse;\n\n return {\n id: typed.id,\n rawId: bytesToBase64Url(typed.rawId),\n type: \"public-key\",\n authenticatorAttachment: typed.authenticatorAttachment ?? null,\n response: {\n clientDataJSON: bytesToBase64Url(response.clientDataJSON),\n authenticatorData: bytesToBase64Url(response.authenticatorData),\n signature: bytesToBase64Url(response.signature),\n userHandle: response.userHandle ? bytesToBase64Url(response.userHandle) : null,\n },\n clientExtensionResults: typed.getClientExtensionResults(),\n };\n }\n\n return {\n register,\n authenticate,\n isSupported: () => credentials !== undefined || isPasskeySupported(),\n isPlatformAuthenticatorAvailable,\n isConditionalMediationAvailable,\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- const e=require("../components/Input/Input.cjs"),t=require("../utils/format.cjs"),n=require("./br-validators.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(t,n=`numeric`){return(0,r.forwardRef)(function({value:r,onChange:a,...o},s){return(0,i.jsx)(e.Input,{...o,ref:s,value:t(r??``),inputMode:n,onChange:e=>a(t(e.target.value))})})}var o=a(t.formatCPF),s=a(n.formatCNPJ),c=a(t.formatPhone,`tel`),l=a(n.formatCEP);function u(e,t,n){return new Intl.NumberFormat(t,{style:`currency`,currency:n}).format(e/100)}function d(e){let t=e.replace(/\D/g,``);return t?Number.parseInt(t,10):0}var f=(0,r.forwardRef)(function({value:t,onChange:n,currency:r=`BRL`,locale:a=`pt-BR`,...o},s){return(0,i.jsx)(e.Input,{...o,ref:s,type:`text`,inputMode:`numeric`,value:u(t||0,a,r),onChange:e=>n(d(e.target.value))})});exports.CEPInput=l,exports.CNPJInput=s,exports.CPFInput=o,exports.MoneyInput=f,exports.PhoneInput=c;
1
+ const e=require("../components/Input/Input.cjs"),t=require("../utils/format.cjs"),n=require("./br-validators.cjs");let r=require("react"),i=require("react/jsx-runtime");function a(t,n=`numeric`){return(0,r.forwardRef)(function({value:r,onChange:a,...o},s){return(0,i.jsx)(e.Input,{...o,ref:s,value:t(r??``),inputMode:n,onChange:e=>a?.(t(e.target.value))})})}var o=a(t.formatCPF),s=a(n.formatCNPJ),c=a(t.formatPhone,`tel`),l=a(n.formatCEP);function u(e,t,n){return new Intl.NumberFormat(t,{style:`currency`,currency:n}).format(e/100)}function d(e){let t=e.replace(/\D/g,``);return t?Number.parseInt(t,10):0}var f=(0,r.forwardRef)(function({value:t,onChange:n,currency:r=`BRL`,locale:a=`pt-BR`,...o},s){return(0,i.jsx)(e.Input,{...o,ref:s,type:`text`,inputMode:`numeric`,value:u(t||0,a,r),onChange:e=>n(d(e.target.value))})});exports.CEPInput=l,exports.CNPJInput=s,exports.CPFInput=o,exports.MoneyInput=f,exports.PhoneInput=c;
2
2
  //# sourceMappingURL=masked-inputs.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"masked-inputs.cjs","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\n\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value: string;\n onChange: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(cents / 100);\n}\n\nfunction parseCents(text: string): number {\n const digits = text.replace(/\\D/g, \"\");\n if (!digits) return 0;\n return Number.parseInt(digits, 10);\n}\n\n/**\n * Currency-masked input. Stores the value as an integer number of cents to\n * avoid floating-point error. Suitable for `react-hook-form` once you adapt\n * the field to expose cents.\n */\nexport const MoneyInput = forwardRef<HTMLInputElement, MoneyInputProps>(function MoneyInput(\n { value, onChange, currency = \"BRL\", locale = \"pt-BR\", ...props },\n ref,\n) {\n return (\n <Input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"numeric\"\n value={formatCents(value || 0, locale, currency)}\n onChange={(event) => onChange(parseCents(event.target.value))}\n />\n );\n});\n"],"mappings":"yKAWA,SAAS,EACL,EACA,EAAgE,UAClE,CACE,OAAA,EAAO,EAAA,WAAA,CAA+C,SAClD,CAAE,QAAO,WAAU,GAAG,GACtB,EACF,CACE,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,GAAI,EACC,MACL,MAAO,EAAK,GAAS,EAAE,EACZ,YACX,SAAW,GAAU,EAAS,EAAK,EAAM,OAAO,KAAK,CAAC,CACzD,CAAA,CAET,CAAC,CACL,CAEA,IAAa,EAAW,EAAY,EAAA,SAAS,EAChC,EAAY,EAAY,EAAA,UAAU,EAClC,EAAa,EAAY,EAAA,YAAa,KAAK,EAC3C,EAAW,EAAY,EAAA,SAAS,EAY7C,SAAS,EAAY,EAAe,EAAgB,EAA0B,CAC1E,OAAO,IAAI,KAAK,aAAa,EAAQ,CAAE,MAAO,WAAY,UAAS,CAAC,CAAC,CAAC,OAAO,EAAQ,GAAG,CAC5F,CAEA,SAAS,EAAW,EAAsB,CACtC,IAAM,EAAS,EAAK,QAAQ,MAAO,EAAE,EAErC,OADK,EACE,OAAO,SAAS,EAAQ,EAAE,EADb,CAExB,CAOA,IAAa,GAAA,EAAa,EAAA,WAAA,CAA8C,SACpE,CAAE,QAAO,WAAU,WAAW,MAAO,SAAS,QAAS,GAAG,GAC1D,EACF,CACE,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,GAAI,EACC,MACL,KAAK,OACL,UAAU,UACV,MAAO,EAAY,GAAS,EAAG,EAAQ,CAAQ,EAC/C,SAAW,GAAU,EAAS,EAAW,EAAM,OAAO,KAAK,CAAC,CAC/D,CAAA,CAET,CAAC"}
1
+ {"version":3,"file":"masked-inputs.cjs","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\n\n/**\n * Props of every masked input.\n *\n * `value` and `onChange` are optional because the primary documented usage is\n * inside `<FormField>`, which supplies both through `cloneElement` — declaring\n * them required made `<FormField name=\"cpf\"><CPFInput /></FormField>` fail to\n * type-check while working perfectly at runtime. Standalone usage still passes\n * both; an uncontrolled input without `onChange` simply reports nothing.\n */\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value?: string;\n onChange?: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange?.(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(cents / 100);\n}\n\nfunction parseCents(text: string): number {\n const digits = text.replace(/\\D/g, \"\");\n if (!digits) return 0;\n return Number.parseInt(digits, 10);\n}\n\n/**\n * Currency-masked input. Stores the value as an integer number of cents to\n * avoid floating-point error. Suitable for `react-hook-form` once you adapt\n * the field to expose cents.\n */\nexport const MoneyInput = forwardRef<HTMLInputElement, MoneyInputProps>(function MoneyInput(\n { value, onChange, currency = \"BRL\", locale = \"pt-BR\", ...props },\n ref,\n) {\n return (\n <Input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"numeric\"\n value={formatCents(value || 0, locale, currency)}\n onChange={(event) => onChange(parseCents(event.target.value))}\n />\n );\n});\n"],"mappings":"yKAoBA,SAAS,EACL,EACA,EAAgE,UAClE,CACE,OAAA,EAAO,EAAA,WAAA,CAA+C,SAClD,CAAE,QAAO,WAAU,GAAG,GACtB,EACF,CACE,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,GAAI,EACC,MACL,MAAO,EAAK,GAAS,EAAE,EACZ,YACX,SAAW,GAAU,IAAW,EAAK,EAAM,OAAO,KAAK,CAAC,CAC3D,CAAA,CAET,CAAC,CACL,CAEA,IAAa,EAAW,EAAY,EAAA,SAAS,EAChC,EAAY,EAAY,EAAA,UAAU,EAClC,EAAa,EAAY,EAAA,YAAa,KAAK,EAC3C,EAAW,EAAY,EAAA,SAAS,EAY7C,SAAS,EAAY,EAAe,EAAgB,EAA0B,CAC1E,OAAO,IAAI,KAAK,aAAa,EAAQ,CAAE,MAAO,WAAY,UAAS,CAAC,CAAC,CAAC,OAAO,EAAQ,GAAG,CAC5F,CAEA,SAAS,EAAW,EAAsB,CACtC,IAAM,EAAS,EAAK,QAAQ,MAAO,EAAE,EAErC,OADK,EACE,OAAO,SAAS,EAAQ,EAAE,EADb,CAExB,CAOA,IAAa,GAAA,EAAa,EAAA,WAAA,CAA8C,SACpE,CAAE,QAAO,WAAU,WAAW,MAAO,SAAS,QAAS,GAAG,GAC1D,EACF,CACE,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,MAAD,CACI,GAAI,EACC,MACL,KAAK,OACL,UAAU,UACV,MAAO,EAAY,GAAS,EAAG,EAAQ,CAAQ,EAC/C,SAAW,GAAU,EAAS,EAAW,EAAM,OAAO,KAAK,CAAC,CAC/D,CAAA,CAET,CAAC"}
@@ -11,7 +11,7 @@ function s(t, n = "numeric") {
11
11
  ref: s,
12
12
  value: t(r ?? ""),
13
13
  inputMode: n,
14
- onChange: (e) => i(t(e.target.value))
14
+ onChange: (e) => i?.(t(e.target.value))
15
15
  });
16
16
  });
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"masked-inputs.js","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\n\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value: string;\n onChange: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(cents / 100);\n}\n\nfunction parseCents(text: string): number {\n const digits = text.replace(/\\D/g, \"\");\n if (!digits) return 0;\n return Number.parseInt(digits, 10);\n}\n\n/**\n * Currency-masked input. Stores the value as an integer number of cents to\n * avoid floating-point error. Suitable for `react-hook-form` once you adapt\n * the field to expose cents.\n */\nexport const MoneyInput = forwardRef<HTMLInputElement, MoneyInputProps>(function MoneyInput(\n { value, onChange, currency = \"BRL\", locale = \"pt-BR\", ...props },\n ref,\n) {\n return (\n <Input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"numeric\"\n value={formatCents(value || 0, locale, currency)}\n onChange={(event) => onChange(parseCents(event.target.value))}\n />\n );\n});\n"],"mappings":";;;;;;AAWA,SAAS,EACL,GACA,IAAgE,WAClE;CACE,OAAO,EAA+C,SAClD,EAAE,UAAO,aAAU,GAAG,KACtB,GACF;EACE,OACI,kBAAC,GAAD;GACI,GAAI;GACC;GACL,OAAO,EAAK,KAAS,EAAE;GACZ;GACX,WAAW,MAAU,EAAS,EAAK,EAAM,OAAO,KAAK,CAAC;EACzD,CAAA;CAET,CAAC;AACL;AAEA,IAAa,IAAW,EAAY,CAAS,GAChC,IAAY,EAAY,CAAU,GAClC,IAAa,EAAY,GAAa,KAAK,GAC3C,IAAW,EAAY,CAAS;AAY7C,SAAS,EAAY,GAAe,GAAgB,GAA0B;CAC1E,OAAO,IAAI,KAAK,aAAa,GAAQ;EAAE,OAAO;EAAY;CAAS,CAAC,CAAC,CAAC,OAAO,IAAQ,GAAG;AAC5F;AAEA,SAAS,EAAW,GAAsB;CACtC,IAAM,IAAS,EAAK,QAAQ,OAAO,EAAE;CAErC,OADK,IACE,OAAO,SAAS,GAAQ,EAAE,IADb;AAExB;AAOA,IAAa,IAAa,EAA8C,SACpE,EAAE,UAAO,aAAU,cAAW,OAAO,YAAS,SAAS,GAAG,KAC1D,GACF;CACE,OACI,kBAAC,GAAD;EACI,GAAI;EACC;EACL,MAAK;EACL,WAAU;EACV,OAAO,EAAY,KAAS,GAAG,GAAQ,CAAQ;EAC/C,WAAW,MAAU,EAAS,EAAW,EAAM,OAAO,KAAK,CAAC;CAC/D,CAAA;AAET,CAAC"}
1
+ {"version":3,"file":"masked-inputs.js","names":[],"sources":["../../src/forms/masked-inputs.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { InputHTMLAttributes } from \"react\";\nimport { Input, type InputProps } from \"@/components/Input\";\nimport { formatCEP, formatCNPJ } from \"./br-validators\";\nimport { formatCPF, formatPhone } from \"@/utils/format\";\n\n/**\n * Props of every masked input.\n *\n * `value` and `onChange` are optional because the primary documented usage is\n * inside `<FormField>`, which supplies both through `cloneElement` — declaring\n * them required made `<FormField name=\"cpf\"><CPFInput /></FormField>` fail to\n * type-check while working perfectly at runtime. Standalone usage still passes\n * both; an uncontrolled input without `onChange` simply reports nothing.\n */\ntype MaskedFieldProps = Omit<InputProps, \"value\" | \"onChange\"> & {\n value?: string;\n onChange?: (value: string) => void;\n};\n\nfunction maskedInput(\n mask: (input: string) => string,\n inputMode: InputHTMLAttributes<HTMLInputElement>[\"inputMode\"] = \"numeric\",\n) {\n return forwardRef<HTMLInputElement, MaskedFieldProps>(function MaskedInput(\n { value, onChange, ...props },\n ref,\n ) {\n return (\n <Input\n {...props}\n ref={ref}\n value={mask(value ?? \"\")}\n inputMode={inputMode}\n onChange={(event) => onChange?.(mask(event.target.value))}\n />\n );\n });\n}\n\nexport const CPFInput = maskedInput(formatCPF);\nexport const CNPJInput = maskedInput(formatCNPJ);\nexport const PhoneInput = maskedInput(formatPhone, \"tel\");\nexport const CEPInput = maskedInput(formatCEP);\n\nexport interface MoneyInputProps extends Omit<InputProps, \"value\" | \"onChange\" | \"type\"> {\n /** Cents (integer). Internally treated as 1/100 of the currency unit. */\n value: number;\n onChange: (cents: number) => void;\n /** Currency code for `Intl.NumberFormat`. Default: `\"BRL\"`. */\n currency?: string;\n /** Locale for `Intl.NumberFormat`. Default: `\"pt-BR\"`. */\n locale?: string;\n}\n\nfunction formatCents(cents: number, locale: string, currency: string): string {\n return new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(cents / 100);\n}\n\nfunction parseCents(text: string): number {\n const digits = text.replace(/\\D/g, \"\");\n if (!digits) return 0;\n return Number.parseInt(digits, 10);\n}\n\n/**\n * Currency-masked input. Stores the value as an integer number of cents to\n * avoid floating-point error. Suitable for `react-hook-form` once you adapt\n * the field to expose cents.\n */\nexport const MoneyInput = forwardRef<HTMLInputElement, MoneyInputProps>(function MoneyInput(\n { value, onChange, currency = \"BRL\", locale = \"pt-BR\", ...props },\n ref,\n) {\n return (\n <Input\n {...props}\n ref={ref}\n type=\"text\"\n inputMode=\"numeric\"\n value={formatCents(value || 0, locale, currency)}\n onChange={(event) => onChange(parseCents(event.target.value))}\n />\n );\n});\n"],"mappings":";;;;;;AAoBA,SAAS,EACL,GACA,IAAgE,WAClE;CACE,OAAO,EAA+C,SAClD,EAAE,UAAO,aAAU,GAAG,KACtB,GACF;EACE,OACI,kBAAC,GAAD;GACI,GAAI;GACC;GACL,OAAO,EAAK,KAAS,EAAE;GACZ;GACX,WAAW,MAAU,IAAW,EAAK,EAAM,OAAO,KAAK,CAAC;EAC3D,CAAA;CAET,CAAC;AACL;AAEA,IAAa,IAAW,EAAY,CAAS,GAChC,IAAY,EAAY,CAAU,GAClC,IAAa,EAAY,GAAa,KAAK,GAC3C,IAAW,EAAY,CAAS;AAY7C,SAAS,EAAY,GAAe,GAAgB,GAA0B;CAC1E,OAAO,IAAI,KAAK,aAAa,GAAQ;EAAE,OAAO;EAAY;CAAS,CAAC,CAAC,CAAC,OAAO,IAAQ,GAAG;AAC5F;AAEA,SAAS,EAAW,GAAsB;CACtC,IAAM,IAAS,EAAK,QAAQ,OAAO,EAAE;CAErC,OADK,IACE,OAAO,SAAS,GAAQ,EAAE,IADb;AAExB;AAOA,IAAa,IAAa,EAA8C,SACpE,EAAE,UAAO,aAAU,cAAW,OAAO,YAAS,SAAS,GAAG,KAC1D,GACF;CACE,OACI,kBAAC,GAAD;EACI,GAAI;EACC;EACL,MAAK;EACL,WAAU;EACV,OAAO,EAAY,KAAS,GAAG,GAAQ,CAAQ;EAC/C,WAAW,MAAU,EAAS,EAAW,EAAM,OAAO,KAAK,CAAC;CAC/D,CAAA;AAET,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"zod-resolver.cjs","names":[],"sources":["../../src/forms/zod-resolver.ts"],"sourcesContent":["import type { z } from \"zod\";\n\ninterface ResolverError {\n type: string;\n message: string;\n}\n\ninterface ResolverOutput<T> {\n values: T | object;\n errors: Record<string, ResolverError | object>;\n}\n\ntype Resolver<T> = (\n values: unknown,\n context: unknown,\n options: { criteriaMode?: \"firstError\" | \"all\" },\n) => Promise<ResolverOutput<T>>;\n\n/**\n * Minimal `react-hook-form` resolver built on top of zod. Mirrors the shape\n * produced by `@hookform/resolvers/zod` so it can be passed straight to\n * `useForm({ resolver })`.\n *\n * @example\n * const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });\n */\nexport function zodResolver<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n): Resolver<z.infer<TSchema>> {\n return async (values, _context, options) => {\n const result = schema.safeParse(values);\n if (result.success) {\n return { values: result.data, errors: {} };\n }\n\n const errors: Record<string, ResolverError | object> = {};\n const criteriaMode = options.criteriaMode ?? \"firstError\";\n\n for (const issue of result.error.issues) {\n const path = issue.path.length === 0 ? \"_root\" : issue.path.join(\".\");\n if (criteriaMode === \"firstError\" && errors[path]) continue;\n errors[path] = { type: issue.code, message: issue.message };\n }\n\n return { values: {}, errors };\n };\n}\n"],"mappings":"AA0BA,SAAgB,EACZ,EAC0B,CAC1B,OAAO,MAAO,EAAQ,EAAU,IAAY,CACxC,IAAM,EAAS,EAAO,UAAU,CAAM,EACtC,GAAI,EAAO,QACP,MAAO,CAAE,OAAQ,EAAO,KAAM,OAAQ,CAAC,CAAE,EAG7C,IAAM,EAAiD,CAAC,EAClD,EAAe,EAAQ,cAAgB,aAE7C,IAAK,IAAM,KAAS,EAAO,MAAM,OAAQ,CACrC,IAAM,EAAO,EAAM,KAAK,SAAW,EAAI,QAAU,EAAM,KAAK,KAAK,GAAG,EAChE,IAAiB,cAAgB,EAAO,KAC5C,EAAO,GAAQ,CAAE,KAAM,EAAM,KAAM,QAAS,EAAM,OAAQ,EAC9D,CAEA,MAAO,CAAE,OAAQ,CAAC,EAAG,QAAO,CAChC,CACJ"}
1
+ {"version":3,"file":"zod-resolver.cjs","names":[],"sources":["../../src/forms/zod-resolver.ts"],"sourcesContent":["import type {\n FieldErrors,\n FieldValues,\n Resolver,\n ResolverOptions,\n ResolverResult,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\n\ninterface ResolverError {\n type: string;\n message: string;\n}\n\n/**\n * Minimal `react-hook-form` resolver built on top of zod. Mirrors the shape\n * produced by `@hookform/resolvers/zod` so it can be passed straight to\n * `useForm({ resolver })`.\n *\n * Typed with react-hook-form's own `Resolver`, not a local look-alike: a\n * structurally similar type of our own compiled fine here and was then rejected\n * at the `useForm({ resolver })` call site — the only place a resolver is ever\n * used — which is why `useZodForm` had to cast its way past it.\n *\n * Of the resolver options only `criteriaMode` is read; the rest of\n * `ResolverOptions` is accepted and ignored, because react-hook-form always\n * passes the whole object.\n *\n * @example\n * const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });\n */\nexport function zodResolver<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n): Resolver<z.infer<TSchema> & FieldValues> {\n type Values = z.infer<TSchema> & FieldValues;\n\n return async (\n values: Values,\n _context: unknown,\n options: ResolverOptions<Values>,\n ): Promise<ResolverResult<Values>> => {\n const result = schema.safeParse(values);\n if (result.success) {\n return { values: result.data as Values, errors: {} };\n }\n\n const errors: Record<string, ResolverError | object> = {};\n const criteriaMode = options.criteriaMode ?? \"firstError\";\n\n for (const issue of result.error.issues) {\n const path = issue.path.length === 0 ? \"_root\" : issue.path.join(\".\");\n if (criteriaMode === \"firstError\" && errors[path]) continue;\n errors[path] = { type: issue.code, message: issue.message };\n }\n\n return { values: {}, errors: errors as FieldErrors<Values> };\n };\n}\n"],"mappings":"AA+BA,SAAgB,EACZ,EACwC,CAGxC,OAAO,MACH,EACA,EACA,IACkC,CAClC,IAAM,EAAS,EAAO,UAAU,CAAM,EACtC,GAAI,EAAO,QACP,MAAO,CAAE,OAAQ,EAAO,KAAgB,OAAQ,CAAC,CAAE,EAGvD,IAAM,EAAiD,CAAC,EAClD,EAAe,EAAQ,cAAgB,aAE7C,IAAK,IAAM,KAAS,EAAO,MAAM,OAAQ,CACrC,IAAM,EAAO,EAAM,KAAK,SAAW,EAAI,QAAU,EAAM,KAAK,KAAK,GAAG,EAChE,IAAiB,cAAgB,EAAO,KAC5C,EAAO,GAAQ,CAAE,KAAM,EAAM,KAAM,QAAS,EAAM,OAAQ,EAC9D,CAEA,MAAO,CAAE,OAAQ,CAAC,EAAW,QAA8B,CAC/D,CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"zod-resolver.js","names":[],"sources":["../../src/forms/zod-resolver.ts"],"sourcesContent":["import type { z } from \"zod\";\n\ninterface ResolverError {\n type: string;\n message: string;\n}\n\ninterface ResolverOutput<T> {\n values: T | object;\n errors: Record<string, ResolverError | object>;\n}\n\ntype Resolver<T> = (\n values: unknown,\n context: unknown,\n options: { criteriaMode?: \"firstError\" | \"all\" },\n) => Promise<ResolverOutput<T>>;\n\n/**\n * Minimal `react-hook-form` resolver built on top of zod. Mirrors the shape\n * produced by `@hookform/resolvers/zod` so it can be passed straight to\n * `useForm({ resolver })`.\n *\n * @example\n * const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });\n */\nexport function zodResolver<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n): Resolver<z.infer<TSchema>> {\n return async (values, _context, options) => {\n const result = schema.safeParse(values);\n if (result.success) {\n return { values: result.data, errors: {} };\n }\n\n const errors: Record<string, ResolverError | object> = {};\n const criteriaMode = options.criteriaMode ?? \"firstError\";\n\n for (const issue of result.error.issues) {\n const path = issue.path.length === 0 ? \"_root\" : issue.path.join(\".\");\n if (criteriaMode === \"firstError\" && errors[path]) continue;\n errors[path] = { type: issue.code, message: issue.message };\n }\n\n return { values: {}, errors };\n };\n}\n"],"mappings":";AA0BA,SAAgB,EACZ,GAC0B;CAC1B,OAAO,OAAO,GAAQ,GAAU,MAAY;EACxC,IAAM,IAAS,EAAO,UAAU,CAAM;EACtC,IAAI,EAAO,SACP,OAAO;GAAE,QAAQ,EAAO;GAAM,QAAQ,CAAC;EAAE;EAG7C,IAAM,IAAiD,CAAC,GAClD,IAAe,EAAQ,gBAAgB;EAE7C,KAAK,IAAM,KAAS,EAAO,MAAM,QAAQ;GACrC,IAAM,IAAO,EAAM,KAAK,WAAW,IAAI,UAAU,EAAM,KAAK,KAAK,GAAG;GAChE,MAAiB,gBAAgB,EAAO,OAC5C,EAAO,KAAQ;IAAE,MAAM,EAAM;IAAM,SAAS,EAAM;GAAQ;EAC9D;EAEA,OAAO;GAAE,QAAQ,CAAC;GAAG;EAAO;CAChC;AACJ"}
1
+ {"version":3,"file":"zod-resolver.js","names":[],"sources":["../../src/forms/zod-resolver.ts"],"sourcesContent":["import type {\n FieldErrors,\n FieldValues,\n Resolver,\n ResolverOptions,\n ResolverResult,\n} from \"react-hook-form\";\nimport type { z } from \"zod\";\n\ninterface ResolverError {\n type: string;\n message: string;\n}\n\n/**\n * Minimal `react-hook-form` resolver built on top of zod. Mirrors the shape\n * produced by `@hookform/resolvers/zod` so it can be passed straight to\n * `useForm({ resolver })`.\n *\n * Typed with react-hook-form's own `Resolver`, not a local look-alike: a\n * structurally similar type of our own compiled fine here and was then rejected\n * at the `useForm({ resolver })` call site — the only place a resolver is ever\n * used — which is why `useZodForm` had to cast its way past it.\n *\n * Of the resolver options only `criteriaMode` is read; the rest of\n * `ResolverOptions` is accepted and ignored, because react-hook-form always\n * passes the whole object.\n *\n * @example\n * const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });\n */\nexport function zodResolver<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n): Resolver<z.infer<TSchema> & FieldValues> {\n type Values = z.infer<TSchema> & FieldValues;\n\n return async (\n values: Values,\n _context: unknown,\n options: ResolverOptions<Values>,\n ): Promise<ResolverResult<Values>> => {\n const result = schema.safeParse(values);\n if (result.success) {\n return { values: result.data as Values, errors: {} };\n }\n\n const errors: Record<string, ResolverError | object> = {};\n const criteriaMode = options.criteriaMode ?? \"firstError\";\n\n for (const issue of result.error.issues) {\n const path = issue.path.length === 0 ? \"_root\" : issue.path.join(\".\");\n if (criteriaMode === \"firstError\" && errors[path]) continue;\n errors[path] = { type: issue.code, message: issue.message };\n }\n\n return { values: {}, errors: errors as FieldErrors<Values> };\n };\n}\n"],"mappings":";AA+BA,SAAgB,EACZ,GACwC;CAGxC,OAAO,OACH,GACA,GACA,MACkC;EAClC,IAAM,IAAS,EAAO,UAAU,CAAM;EACtC,IAAI,EAAO,SACP,OAAO;GAAE,QAAQ,EAAO;GAAgB,QAAQ,CAAC;EAAE;EAGvD,IAAM,IAAiD,CAAC,GAClD,IAAe,EAAQ,gBAAgB;EAE7C,KAAK,IAAM,KAAS,EAAO,MAAM,QAAQ;GACrC,IAAM,IAAO,EAAM,KAAK,WAAW,IAAI,UAAU,EAAM,KAAK,KAAK,GAAG;GAChE,MAAiB,gBAAgB,EAAO,OAC5C,EAAO,KAAQ;IAAE,MAAM,EAAM;IAAM,SAAS,EAAM;GAAQ;EAC9D;EAEA,OAAO;GAAE,QAAQ,CAAC;GAAW;EAA8B;CAC/D;AACJ"}
@@ -59,6 +59,7 @@ import { redirect } from 'react-router';
59
59
  import { Ref } from 'react';
60
60
  import { RefAttributes } from 'react';
61
61
  import { RefObject } from 'react';
62
+ import { Resolver } from 'react-hook-form';
62
63
  import { Route } from 'react-router';
63
64
  import { Routes } from 'react-router';
64
65
  import { SelectHTMLAttributes } from 'react';
@@ -1366,7 +1367,7 @@ export declare interface BarcodeScanResult {
1366
1367
  * @param value - Base64url text, with or without `=` padding.
1367
1368
  * @returns The decoded bytes.
1368
1369
  */
1369
- export declare function base64UrlToBytes(value: string): Uint8Array;
1370
+ export declare function base64UrlToBytes(value: string): Uint8Array<ArrayBuffer>;
1370
1371
 
1371
1372
  /**
1372
1373
  * Initial bearing (forward azimuth) from `origin` to `destination`, in degrees
@@ -1862,8 +1863,8 @@ export declare interface CenterProps extends HTMLAttributes<HTMLDivElement> {
1862
1863
  }
1863
1864
 
1864
1865
  export declare const CEPInput: ForwardRefExoticComponent<Omit<InputProps, "value" | "onChange"> & {
1865
- value: string;
1866
- onChange: (value: string) => void;
1866
+ value?: string;
1867
+ onChange?: (value: string) => void;
1867
1868
  } & RefAttributes<HTMLInputElement>>;
1868
1869
 
1869
1870
  /** A `var(--tempest-chart-…)` reference, so the value follows the active theme. */
@@ -2201,8 +2202,8 @@ export declare interface ClickOutsideProps extends HTMLAttributes<HTMLDivElement
2201
2202
  export declare function cn(...values: ClassValue[]): string;
2202
2203
 
2203
2204
  export declare const CNPJInput: ForwardRefExoticComponent<Omit<InputProps, "value" | "onChange"> & {
2204
- value: string;
2205
- onChange: (value: string) => void;
2205
+ value?: string;
2206
+ onChange?: (value: string) => void;
2206
2207
  } & RefAttributes<HTMLInputElement>>;
2207
2208
 
2208
2209
  /**
@@ -2554,8 +2555,8 @@ export declare interface CounterHandlers {
2554
2555
  }
2555
2556
 
2556
2557
  export declare const CPFInput: ForwardRefExoticComponent<Omit<InputProps, "value" | "onChange"> & {
2557
- value: string;
2558
- onChange: (value: string) => void;
2558
+ value?: string;
2559
+ onChange?: (value: string) => void;
2559
2560
  } & RefAttributes<HTMLInputElement>>;
2560
2561
 
2561
2562
  /**
@@ -8140,8 +8141,8 @@ export declare interface PermissionsFromTokenOptions {
8140
8141
  export declare function persistQueryClientOffline(options: OfflineQueryPersistenceOptions): OfflineQueryPersistence;
8141
8142
 
8142
8143
  export declare const PhoneInput: ForwardRefExoticComponent<Omit<InputProps, "value" | "onChange"> & {
8143
- value: string;
8144
- onChange: (value: string) => void;
8144
+ value?: string;
8145
+ onChange?: (value: string) => void;
8145
8146
  } & RefAttributes<HTMLInputElement>>;
8146
8147
 
8147
8148
  /**
@@ -8973,20 +8974,6 @@ export declare type ResolvedTheme = "light" | "dark";
8973
8974
  */
8974
8975
  export declare function resolveLanguage(language: string | undefined): CodeLanguage;
8975
8976
 
8976
- declare type Resolver<T> = (values: unknown, context: unknown, options: {
8977
- criteriaMode?: "firstError" | "all";
8978
- }) => Promise<ResolverOutput<T>>;
8979
-
8980
- declare interface ResolverError {
8981
- type: string;
8982
- message: string;
8983
- }
8984
-
8985
- declare interface ResolverOutput<T> {
8986
- values: T | object;
8987
- errors: Record<string, ResolverError | object>;
8988
- }
8989
-
8990
8977
  /**
8991
8978
  * Responsive value — either a single value applied at all breakpoints, or
8992
8979
  * an object with `mobile` / `tablet` / `desktop` overrides. Apps can mix
@@ -14749,7 +14736,10 @@ export declare interface WizardStep {
14749
14736
  *
14750
14737
  * @param headers - Column headers written as the first row.
14751
14738
  * @param rows - Data rows; each value is a string, a number, or `null` (empty).
14752
- * @returns The `.xlsx` file contents as a `Uint8Array`.
14739
+ * @returns The `.xlsx` file contents, backed by a plain `ArrayBuffer` so the
14740
+ * bytes go straight into `new Blob([...])` — the default `Uint8Array` is
14741
+ * `Uint8Array<ArrayBufferLike>`, which `BlobPart` rejects because it also
14742
+ * admits `SharedArrayBuffer`.
14753
14743
  *
14754
14744
  * @example
14755
14745
  * const bytes = writeXlsx(
@@ -14760,16 +14750,25 @@ export declare interface WizardStep {
14760
14750
  * type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
14761
14751
  * });
14762
14752
  */
14763
- export declare function writeXlsx(headers: readonly string[], rows: readonly (readonly (string | number | null)[])[]): Uint8Array;
14753
+ export declare function writeXlsx(headers: readonly string[], rows: readonly (readonly (string | number | null)[])[]): Uint8Array<ArrayBuffer>;
14764
14754
 
14765
14755
  /**
14766
14756
  * Minimal `react-hook-form` resolver built on top of zod. Mirrors the shape
14767
14757
  * produced by `@hookform/resolvers/zod` so it can be passed straight to
14768
14758
  * `useForm({ resolver })`.
14769
14759
  *
14760
+ * Typed with react-hook-form's own `Resolver`, not a local look-alike: a
14761
+ * structurally similar type of our own compiled fine here and was then rejected
14762
+ * at the `useForm({ resolver })` call site — the only place a resolver is ever
14763
+ * used — which is why `useZodForm` had to cast its way past it.
14764
+ *
14765
+ * Of the resolver options only `criteriaMode` is read; the rest of
14766
+ * `ResolverOptions` is accepted and ignored, because react-hook-form always
14767
+ * passes the whole object.
14768
+ *
14770
14769
  * @example
14771
14770
  * const form = useForm<LoginForm>({ resolver: zodResolver(loginSchema) });
14772
14771
  */
14773
- export declare function zodResolver<TSchema extends z.ZodTypeAny>(schema: TSchema): Resolver<z.infer<TSchema>>;
14772
+ export declare function zodResolver<TSchema extends z.ZodTypeAny>(schema: TSchema): Resolver<z.infer<TSchema> & FieldValues>;
14774
14773
 
14775
14774
  export { }
@@ -1 +1 @@
1
- {"version":3,"file":"xlsx.cjs","names":[],"sources":["../../src/utils/xlsx.ts"],"sourcesContent":["import { zipSync, type Zippable } from \"fflate\";\n\n/** A single spreadsheet cell — a number, a string, or an empty cell. */\ntype Cell = { v: string; t: \"s\" } | { v: number; t: \"n\" } | { v: \"\"; t: \"s\" };\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nfunction colLetter(index0: number): string {\n let n = index0 + 1;\n let result = \"\";\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction toCell(value: string | number | null): Cell {\n if (typeof value === \"number\") return { v: value, t: \"n\" };\n if (value === null || value === \"\") return { v: \"\", t: \"s\" };\n return { v: value, t: \"s\" };\n}\n\nfunction cellXml(cell: Cell, ref: string): string {\n if (cell.t === \"n\") return `<c r=\"${ref}\" t=\"n\"><v>${cell.v}</v></c>`;\n if (cell.v === \"\") return `<c r=\"${ref}\" t=\"inlineStr\"><is><t></t></is></c>`;\n return `<c r=\"${ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">${escapeXml(cell.v)}</t></is></c>`;\n}\n\nfunction rowXml(cells: readonly Cell[], rowIndex1: number): string {\n const inner = cells.map((c, i) => cellXml(c, `${colLetter(i)}${rowIndex1}`)).join(\"\");\n return `<row r=\"${rowIndex1}\">${inner}</row>`;\n}\n\nfunction buildSheetXml(rows: readonly (readonly Cell[])[], columnCount: number): string {\n const dimension = `A1:${colLetter(Math.max(columnCount - 1, 0))}${rows.length}`;\n const body = rows.map((row, i) => rowXml(row, i + 1)).join(\"\");\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">` +\n `<dimension ref=\"${dimension}\"/>` +\n `<sheetData>${body}</sheetData>` +\n `</worksheet>`\n );\n}\n\nconst CONTENT_TYPES_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">` +\n `<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>` +\n `<Default Extension=\"xml\" ContentType=\"application/xml\"/>` +\n `<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>` +\n `<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>` +\n `</Types>`;\n\nconst ROOT_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>` +\n `</Relationships>`;\n\nconst WORKBOOK_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"` +\n ` xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">` +\n `<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>` +\n `</workbook>`;\n\nconst WORKBOOK_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>` +\n `</Relationships>`;\n\n/**\n * Write a minimal single-sheet Office Open XML (`.xlsx`) workbook and return\n * its bytes. No extra dependency beyond `fflate` — the archive is assembled and\n * deflated in-process.\n *\n * The output is UTF-8 throughout, so accents round-trip in\n * Excel/LibreOffice/Google Sheets without the BOM-detection fragility that\n * plagues CSV exports. The XML stays compact: inline strings (no shared-string\n * table), no styles, no merged cells. Numeric cells use the native `\"n\"` type\n * so spreadsheets recognise them as numbers; `null` renders as an empty cell.\n *\n * @param headers - Column headers written as the first row.\n * @param rows - Data rows; each value is a string, a number, or `null` (empty).\n * @returns The `.xlsx` file contents as a `Uint8Array`.\n *\n * @example\n * const bytes = writeXlsx(\n * [\"Name\", \"Score\"],\n * [[\"Ada\", 99], [\"Alan\", null]],\n * );\n * const blob = new Blob([bytes], {\n * type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n * });\n */\nexport function writeXlsx(\n headers: readonly string[],\n rows: readonly (readonly (string | number | null)[])[],\n): Uint8Array {\n const headerRow: Cell[] = headers.map((h) => ({ v: h, t: \"s\" }));\n const dataRows: Cell[][] = rows.map((row) => row.map(toCell));\n const allRows: Cell[][] = [headerRow, ...dataRows];\n const columnCount = Math.max(headers.length, ...dataRows.map((row) => row.length), 0);\n const sheetXml = buildSheetXml(allRows, columnCount);\n\n const encoder = new TextEncoder();\n const archive: Zippable = {\n \"[Content_Types].xml\": encoder.encode(CONTENT_TYPES_XML),\n \"_rels/.rels\": encoder.encode(ROOT_RELS_XML),\n \"xl/workbook.xml\": encoder.encode(WORKBOOK_XML),\n \"xl/_rels/workbook.xml.rels\": encoder.encode(WORKBOOK_RELS_XML),\n \"xl/worksheets/sheet1.xml\": encoder.encode(sheetXml),\n };\n return zipSync(archive);\n}\n"],"mappings":"wBAKA,SAAS,EAAU,EAAuB,CACtC,OAAO,EACF,QAAQ,KAAM,OAAO,CAAC,CACtB,QAAQ,KAAM,MAAM,CAAC,CACrB,QAAQ,KAAM,MAAM,CAAC,CACrB,QAAQ,KAAM,QAAQ,CAAC,CACvB,QAAQ,KAAM,QAAQ,CAC/B,CAEA,SAAS,EAAU,EAAwB,CACvC,IAAI,EAAI,EAAS,EACb,EAAS,GACb,KAAO,EAAI,GAAG,CACV,IAAM,GAAO,EAAI,GAAK,GACtB,EAAS,OAAO,aAAa,GAAK,CAAG,EAAI,EACzC,EAAI,KAAK,OAAO,EAAI,GAAK,EAAE,CAC/B,CACA,OAAO,CACX,CAEA,SAAS,EAAO,EAAqC,CAGjD,OAFI,OAAO,GAAU,SAAiB,CAAE,EAAG,EAAO,EAAG,GAAI,EACrD,IAAU,MAAQ,IAAU,GAAW,CAAE,EAAG,GAAI,EAAG,GAAI,EACpD,CAAE,EAAG,EAAO,EAAG,GAAI,CAC9B,CAEA,SAAS,EAAQ,EAAY,EAAqB,CAG9C,OAFI,EAAK,IAAM,IAAY,SAAS,EAAI,aAAa,EAAK,EAAE,UACxD,EAAK,IAAM,GAAW,SAAS,EAAI,sCAChC,SAAS,EAAI,8CAA8C,EAAU,EAAK,CAAC,EAAE,cACxF,CAEA,SAAS,EAAO,EAAwB,EAA2B,CAE/D,MAAO,WAAW,EAAU,IADd,EAAM,KAAK,EAAG,IAAM,EAAQ,EAAG,GAAG,EAAU,CAAC,IAAI,GAAW,CAAC,CAAC,CAAC,KAAK,EAClD,EAAM,OAC1C,CAEA,SAAS,EAAc,EAAoC,EAA6B,CAGpF,MACI,uJAEmB,MALC,EAAU,KAAK,IAAI,EAAc,EAAG,CAAC,CAAC,IAAI,EAAK,SAKtC,gBAJpB,EAAK,KAAK,EAAK,IAAM,EAAO,EAAK,EAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAKzC,EAAK,yBAG3B,CAEA,IAAM,EACF,gjBAQE,EACF,0SAKE,EACF,8RAME,EACF,2SA6BJ,SAAgB,EACZ,EACA,EACU,CACV,IAAM,EAAoB,EAAQ,IAAK,IAAO,CAAE,EAAG,EAAG,EAAG,GAAI,EAAE,EACzD,EAAqB,EAAK,IAAK,GAAQ,EAAI,IAAI,CAAM,CAAC,EAGtD,EAAW,EAAc,CAFJ,EAAW,GAAG,CAEV,EADX,KAAK,IAAI,EAAQ,OAAQ,GAAG,EAAS,IAAK,GAAQ,EAAI,MAAM,EAAG,CAC3C,CAAW,EAE7C,EAAU,IAAI,YACd,EAAoB,CACtB,sBAAuB,EAAQ,OAAO,CAAiB,EACvD,cAAe,EAAQ,OAAO,CAAa,EAC3C,kBAAmB,EAAQ,OAAO,CAAY,EAC9C,6BAA8B,EAAQ,OAAO,CAAiB,EAC9D,2BAA4B,EAAQ,OAAO,CAAQ,CACvD,EACA,OAAA,EAAO,EAAA,QAAA,CAAQ,CAAO,CAC1B"}
1
+ {"version":3,"file":"xlsx.cjs","names":[],"sources":["../../src/utils/xlsx.ts"],"sourcesContent":["import { zipSync, type Zippable } from \"fflate\";\n\n/** A single spreadsheet cell — a number, a string, or an empty cell. */\ntype Cell = { v: string; t: \"s\" } | { v: number; t: \"n\" } | { v: \"\"; t: \"s\" };\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nfunction colLetter(index0: number): string {\n let n = index0 + 1;\n let result = \"\";\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction toCell(value: string | number | null): Cell {\n if (typeof value === \"number\") return { v: value, t: \"n\" };\n if (value === null || value === \"\") return { v: \"\", t: \"s\" };\n return { v: value, t: \"s\" };\n}\n\nfunction cellXml(cell: Cell, ref: string): string {\n if (cell.t === \"n\") return `<c r=\"${ref}\" t=\"n\"><v>${cell.v}</v></c>`;\n if (cell.v === \"\") return `<c r=\"${ref}\" t=\"inlineStr\"><is><t></t></is></c>`;\n return `<c r=\"${ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">${escapeXml(cell.v)}</t></is></c>`;\n}\n\nfunction rowXml(cells: readonly Cell[], rowIndex1: number): string {\n const inner = cells.map((c, i) => cellXml(c, `${colLetter(i)}${rowIndex1}`)).join(\"\");\n return `<row r=\"${rowIndex1}\">${inner}</row>`;\n}\n\nfunction buildSheetXml(rows: readonly (readonly Cell[])[], columnCount: number): string {\n const dimension = `A1:${colLetter(Math.max(columnCount - 1, 0))}${rows.length}`;\n const body = rows.map((row, i) => rowXml(row, i + 1)).join(\"\");\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">` +\n `<dimension ref=\"${dimension}\"/>` +\n `<sheetData>${body}</sheetData>` +\n `</worksheet>`\n );\n}\n\nconst CONTENT_TYPES_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">` +\n `<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>` +\n `<Default Extension=\"xml\" ContentType=\"application/xml\"/>` +\n `<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>` +\n `<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>` +\n `</Types>`;\n\nconst ROOT_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>` +\n `</Relationships>`;\n\nconst WORKBOOK_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"` +\n ` xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">` +\n `<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>` +\n `</workbook>`;\n\nconst WORKBOOK_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>` +\n `</Relationships>`;\n\n/**\n * Write a minimal single-sheet Office Open XML (`.xlsx`) workbook and return\n * its bytes. No extra dependency beyond `fflate` — the archive is assembled and\n * deflated in-process.\n *\n * The output is UTF-8 throughout, so accents round-trip in\n * Excel/LibreOffice/Google Sheets without the BOM-detection fragility that\n * plagues CSV exports. The XML stays compact: inline strings (no shared-string\n * table), no styles, no merged cells. Numeric cells use the native `\"n\"` type\n * so spreadsheets recognise them as numbers; `null` renders as an empty cell.\n *\n * @param headers - Column headers written as the first row.\n * @param rows - Data rows; each value is a string, a number, or `null` (empty).\n * @returns The `.xlsx` file contents, backed by a plain `ArrayBuffer` so the\n * bytes go straight into `new Blob([...])` — the default `Uint8Array` is\n * `Uint8Array<ArrayBufferLike>`, which `BlobPart` rejects because it also\n * admits `SharedArrayBuffer`.\n *\n * @example\n * const bytes = writeXlsx(\n * [\"Name\", \"Score\"],\n * [[\"Ada\", 99], [\"Alan\", null]],\n * );\n * const blob = new Blob([bytes], {\n * type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n * });\n */\nexport function writeXlsx(\n headers: readonly string[],\n rows: readonly (readonly (string | number | null)[])[],\n): Uint8Array<ArrayBuffer> {\n const headerRow: Cell[] = headers.map((h) => ({ v: h, t: \"s\" }));\n const dataRows: Cell[][] = rows.map((row) => row.map(toCell));\n const allRows: Cell[][] = [headerRow, ...dataRows];\n const columnCount = Math.max(headers.length, ...dataRows.map((row) => row.length), 0);\n const sheetXml = buildSheetXml(allRows, columnCount);\n\n const encoder = new TextEncoder();\n const archive: Zippable = {\n \"[Content_Types].xml\": encoder.encode(CONTENT_TYPES_XML),\n \"_rels/.rels\": encoder.encode(ROOT_RELS_XML),\n \"xl/workbook.xml\": encoder.encode(WORKBOOK_XML),\n \"xl/_rels/workbook.xml.rels\": encoder.encode(WORKBOOK_RELS_XML),\n \"xl/worksheets/sheet1.xml\": encoder.encode(sheetXml),\n };\n return zipSync(archive);\n}\n"],"mappings":"wBAKA,SAAS,EAAU,EAAuB,CACtC,OAAO,EACF,QAAQ,KAAM,OAAO,CAAC,CACtB,QAAQ,KAAM,MAAM,CAAC,CACrB,QAAQ,KAAM,MAAM,CAAC,CACrB,QAAQ,KAAM,QAAQ,CAAC,CACvB,QAAQ,KAAM,QAAQ,CAC/B,CAEA,SAAS,EAAU,EAAwB,CACvC,IAAI,EAAI,EAAS,EACb,EAAS,GACb,KAAO,EAAI,GAAG,CACV,IAAM,GAAO,EAAI,GAAK,GACtB,EAAS,OAAO,aAAa,GAAK,CAAG,EAAI,EACzC,EAAI,KAAK,OAAO,EAAI,GAAK,EAAE,CAC/B,CACA,OAAO,CACX,CAEA,SAAS,EAAO,EAAqC,CAGjD,OAFI,OAAO,GAAU,SAAiB,CAAE,EAAG,EAAO,EAAG,GAAI,EACrD,IAAU,MAAQ,IAAU,GAAW,CAAE,EAAG,GAAI,EAAG,GAAI,EACpD,CAAE,EAAG,EAAO,EAAG,GAAI,CAC9B,CAEA,SAAS,EAAQ,EAAY,EAAqB,CAG9C,OAFI,EAAK,IAAM,IAAY,SAAS,EAAI,aAAa,EAAK,EAAE,UACxD,EAAK,IAAM,GAAW,SAAS,EAAI,sCAChC,SAAS,EAAI,8CAA8C,EAAU,EAAK,CAAC,EAAE,cACxF,CAEA,SAAS,EAAO,EAAwB,EAA2B,CAE/D,MAAO,WAAW,EAAU,IADd,EAAM,KAAK,EAAG,IAAM,EAAQ,EAAG,GAAG,EAAU,CAAC,IAAI,GAAW,CAAC,CAAC,CAAC,KAAK,EAClD,EAAM,OAC1C,CAEA,SAAS,EAAc,EAAoC,EAA6B,CAGpF,MACI,uJAEmB,MALC,EAAU,KAAK,IAAI,EAAc,EAAG,CAAC,CAAC,IAAI,EAAK,SAKtC,gBAJpB,EAAK,KAAK,EAAK,IAAM,EAAO,EAAK,EAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAKzC,EAAK,yBAG3B,CAEA,IAAM,EACF,gjBAQE,EACF,0SAKE,EACF,8RAME,EACF,2SAgCJ,SAAgB,EACZ,EACA,EACuB,CACvB,IAAM,EAAoB,EAAQ,IAAK,IAAO,CAAE,EAAG,EAAG,EAAG,GAAI,EAAE,EACzD,EAAqB,EAAK,IAAK,GAAQ,EAAI,IAAI,CAAM,CAAC,EAGtD,EAAW,EAAc,CAFJ,EAAW,GAAG,CAEV,EADX,KAAK,IAAI,EAAQ,OAAQ,GAAG,EAAS,IAAK,GAAQ,EAAI,MAAM,EAAG,CAC3C,CAAW,EAE7C,EAAU,IAAI,YACd,EAAoB,CACtB,sBAAuB,EAAQ,OAAO,CAAiB,EACvD,cAAe,EAAQ,OAAO,CAAa,EAC3C,kBAAmB,EAAQ,OAAO,CAAY,EAC9C,6BAA8B,EAAQ,OAAO,CAAiB,EAC9D,2BAA4B,EAAQ,OAAO,CAAQ,CACvD,EACA,OAAA,EAAO,EAAA,QAAA,CAAQ,CAAO,CAC1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"xlsx.js","names":[],"sources":["../../src/utils/xlsx.ts"],"sourcesContent":["import { zipSync, type Zippable } from \"fflate\";\n\n/** A single spreadsheet cell — a number, a string, or an empty cell. */\ntype Cell = { v: string; t: \"s\" } | { v: number; t: \"n\" } | { v: \"\"; t: \"s\" };\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nfunction colLetter(index0: number): string {\n let n = index0 + 1;\n let result = \"\";\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction toCell(value: string | number | null): Cell {\n if (typeof value === \"number\") return { v: value, t: \"n\" };\n if (value === null || value === \"\") return { v: \"\", t: \"s\" };\n return { v: value, t: \"s\" };\n}\n\nfunction cellXml(cell: Cell, ref: string): string {\n if (cell.t === \"n\") return `<c r=\"${ref}\" t=\"n\"><v>${cell.v}</v></c>`;\n if (cell.v === \"\") return `<c r=\"${ref}\" t=\"inlineStr\"><is><t></t></is></c>`;\n return `<c r=\"${ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">${escapeXml(cell.v)}</t></is></c>`;\n}\n\nfunction rowXml(cells: readonly Cell[], rowIndex1: number): string {\n const inner = cells.map((c, i) => cellXml(c, `${colLetter(i)}${rowIndex1}`)).join(\"\");\n return `<row r=\"${rowIndex1}\">${inner}</row>`;\n}\n\nfunction buildSheetXml(rows: readonly (readonly Cell[])[], columnCount: number): string {\n const dimension = `A1:${colLetter(Math.max(columnCount - 1, 0))}${rows.length}`;\n const body = rows.map((row, i) => rowXml(row, i + 1)).join(\"\");\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">` +\n `<dimension ref=\"${dimension}\"/>` +\n `<sheetData>${body}</sheetData>` +\n `</worksheet>`\n );\n}\n\nconst CONTENT_TYPES_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">` +\n `<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>` +\n `<Default Extension=\"xml\" ContentType=\"application/xml\"/>` +\n `<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>` +\n `<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>` +\n `</Types>`;\n\nconst ROOT_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>` +\n `</Relationships>`;\n\nconst WORKBOOK_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"` +\n ` xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">` +\n `<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>` +\n `</workbook>`;\n\nconst WORKBOOK_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>` +\n `</Relationships>`;\n\n/**\n * Write a minimal single-sheet Office Open XML (`.xlsx`) workbook and return\n * its bytes. No extra dependency beyond `fflate` — the archive is assembled and\n * deflated in-process.\n *\n * The output is UTF-8 throughout, so accents round-trip in\n * Excel/LibreOffice/Google Sheets without the BOM-detection fragility that\n * plagues CSV exports. The XML stays compact: inline strings (no shared-string\n * table), no styles, no merged cells. Numeric cells use the native `\"n\"` type\n * so spreadsheets recognise them as numbers; `null` renders as an empty cell.\n *\n * @param headers - Column headers written as the first row.\n * @param rows - Data rows; each value is a string, a number, or `null` (empty).\n * @returns The `.xlsx` file contents as a `Uint8Array`.\n *\n * @example\n * const bytes = writeXlsx(\n * [\"Name\", \"Score\"],\n * [[\"Ada\", 99], [\"Alan\", null]],\n * );\n * const blob = new Blob([bytes], {\n * type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n * });\n */\nexport function writeXlsx(\n headers: readonly string[],\n rows: readonly (readonly (string | number | null)[])[],\n): Uint8Array {\n const headerRow: Cell[] = headers.map((h) => ({ v: h, t: \"s\" }));\n const dataRows: Cell[][] = rows.map((row) => row.map(toCell));\n const allRows: Cell[][] = [headerRow, ...dataRows];\n const columnCount = Math.max(headers.length, ...dataRows.map((row) => row.length), 0);\n const sheetXml = buildSheetXml(allRows, columnCount);\n\n const encoder = new TextEncoder();\n const archive: Zippable = {\n \"[Content_Types].xml\": encoder.encode(CONTENT_TYPES_XML),\n \"_rels/.rels\": encoder.encode(ROOT_RELS_XML),\n \"xl/workbook.xml\": encoder.encode(WORKBOOK_XML),\n \"xl/_rels/workbook.xml.rels\": encoder.encode(WORKBOOK_RELS_XML),\n \"xl/worksheets/sheet1.xml\": encoder.encode(sheetXml),\n };\n return zipSync(archive);\n}\n"],"mappings":";;AAKA,SAAS,EAAU,GAAuB;CACtC,OAAO,EACF,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ,CAAC,CACvB,QAAQ,MAAM,QAAQ;AAC/B;AAEA,SAAS,EAAU,GAAwB;CACvC,IAAI,IAAI,IAAS,GACb,IAAS;CACb,OAAO,IAAI,IAAG;EACV,IAAM,KAAO,IAAI,KAAK;EAEtB,AADA,IAAS,OAAO,aAAa,KAAK,CAAG,IAAI,GACzC,IAAI,KAAK,OAAO,IAAI,KAAK,EAAE;CAC/B;CACA,OAAO;AACX;AAEA,SAAS,EAAO,GAAqC;CAGjD,OAFI,OAAO,KAAU,WAAiB;EAAE,GAAG;EAAO,GAAG;CAAI,IACrD,MAAU,QAAQ,MAAU,KAAW;EAAE,GAAG;EAAI,GAAG;CAAI,IACpD;EAAE,GAAG;EAAO,GAAG;CAAI;AAC9B;AAEA,SAAS,EAAQ,GAAY,GAAqB;CAG9C,OAFI,EAAK,MAAM,MAAY,SAAS,EAAI,aAAa,EAAK,EAAE,YACxD,EAAK,MAAM,KAAW,SAAS,EAAI,wCAChC,SAAS,EAAI,8CAA8C,EAAU,EAAK,CAAC,EAAE;AACxF;AAEA,SAAS,EAAO,GAAwB,GAA2B;CAE/D,OAAO,WAAW,EAAU,IADd,EAAM,KAAK,GAAG,MAAM,EAAQ,GAAG,GAAG,EAAU,CAAC,IAAI,GAAW,CAAC,CAAC,CAAC,KAAK,EAClD,EAAM;AAC1C;AAEA,SAAS,EAAc,GAAoC,GAA6B;CAGpF,OACI,uJAEmB,MALC,EAAU,KAAK,IAAI,IAAc,GAAG,CAAC,CAAC,IAAI,EAAK,SAKtC,gBAJpB,EAAK,KAAK,GAAK,MAAM,EAAO,GAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAKzC,EAAK;AAG3B;AAEA,IAAM,IACF,ykBAQE,IACF,yTAKE,IACF,+SAME,IACF;AA6BJ,SAAgB,EACZ,GACA,GACU;CACV,IAAM,IAAoB,EAAQ,KAAK,OAAO;EAAE,GAAG;EAAG,GAAG;CAAI,EAAE,GACzD,IAAqB,EAAK,KAAK,MAAQ,EAAI,IAAI,CAAM,CAAC,GAGtD,IAAW,EAAc,CAFJ,GAAW,GAAG,CAEV,GADX,KAAK,IAAI,EAAQ,QAAQ,GAAG,EAAS,KAAK,MAAQ,EAAI,MAAM,GAAG,CAC3C,CAAW,GAE7C,IAAU,IAAI,YAAY,GAC1B,IAAoB;EACtB,uBAAuB,EAAQ,OAAO,CAAiB;EACvD,eAAe,EAAQ,OAAO,CAAa;EAC3C,mBAAmB,EAAQ,OAAO,CAAY;EAC9C,8BAA8B,EAAQ,OAAO,CAAiB;EAC9D,4BAA4B,EAAQ,OAAO,CAAQ;CACvD;CACA,OAAO,EAAQ,CAAO;AAC1B"}
1
+ {"version":3,"file":"xlsx.js","names":[],"sources":["../../src/utils/xlsx.ts"],"sourcesContent":["import { zipSync, type Zippable } from \"fflate\";\n\n/** A single spreadsheet cell — a number, a string, or an empty cell. */\ntype Cell = { v: string; t: \"s\" } | { v: number; t: \"n\" } | { v: \"\"; t: \"s\" };\n\nfunction escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\nfunction colLetter(index0: number): string {\n let n = index0 + 1;\n let result = \"\";\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction toCell(value: string | number | null): Cell {\n if (typeof value === \"number\") return { v: value, t: \"n\" };\n if (value === null || value === \"\") return { v: \"\", t: \"s\" };\n return { v: value, t: \"s\" };\n}\n\nfunction cellXml(cell: Cell, ref: string): string {\n if (cell.t === \"n\") return `<c r=\"${ref}\" t=\"n\"><v>${cell.v}</v></c>`;\n if (cell.v === \"\") return `<c r=\"${ref}\" t=\"inlineStr\"><is><t></t></is></c>`;\n return `<c r=\"${ref}\" t=\"inlineStr\"><is><t xml:space=\"preserve\">${escapeXml(cell.v)}</t></is></c>`;\n}\n\nfunction rowXml(cells: readonly Cell[], rowIndex1: number): string {\n const inner = cells.map((c, i) => cellXml(c, `${colLetter(i)}${rowIndex1}`)).join(\"\");\n return `<row r=\"${rowIndex1}\">${inner}</row>`;\n}\n\nfunction buildSheetXml(rows: readonly (readonly Cell[])[], columnCount: number): string {\n const dimension = `A1:${colLetter(Math.max(columnCount - 1, 0))}${rows.length}`;\n const body = rows.map((row, i) => rowXml(row, i + 1)).join(\"\");\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<worksheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">` +\n `<dimension ref=\"${dimension}\"/>` +\n `<sheetData>${body}</sheetData>` +\n `</worksheet>`\n );\n}\n\nconst CONTENT_TYPES_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\">` +\n `<Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>` +\n `<Default Extension=\"xml\" ContentType=\"application/xml\"/>` +\n `<Override PartName=\"/xl/workbook.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml\"/>` +\n `<Override PartName=\"/xl/worksheets/sheet1.xml\" ContentType=\"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml\"/>` +\n `</Types>`;\n\nconst ROOT_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument\" Target=\"xl/workbook.xml\"/>` +\n `</Relationships>`;\n\nconst WORKBOOK_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"` +\n ` xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">` +\n `<sheets><sheet name=\"Sheet1\" sheetId=\"1\" r:id=\"rId1\"/></sheets>` +\n `</workbook>`;\n\nconst WORKBOOK_RELS_XML =\n `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>` +\n `<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">` +\n `<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet\" Target=\"worksheets/sheet1.xml\"/>` +\n `</Relationships>`;\n\n/**\n * Write a minimal single-sheet Office Open XML (`.xlsx`) workbook and return\n * its bytes. No extra dependency beyond `fflate` — the archive is assembled and\n * deflated in-process.\n *\n * The output is UTF-8 throughout, so accents round-trip in\n * Excel/LibreOffice/Google Sheets without the BOM-detection fragility that\n * plagues CSV exports. The XML stays compact: inline strings (no shared-string\n * table), no styles, no merged cells. Numeric cells use the native `\"n\"` type\n * so spreadsheets recognise them as numbers; `null` renders as an empty cell.\n *\n * @param headers - Column headers written as the first row.\n * @param rows - Data rows; each value is a string, a number, or `null` (empty).\n * @returns The `.xlsx` file contents, backed by a plain `ArrayBuffer` so the\n * bytes go straight into `new Blob([...])` — the default `Uint8Array` is\n * `Uint8Array<ArrayBufferLike>`, which `BlobPart` rejects because it also\n * admits `SharedArrayBuffer`.\n *\n * @example\n * const bytes = writeXlsx(\n * [\"Name\", \"Score\"],\n * [[\"Ada\", 99], [\"Alan\", null]],\n * );\n * const blob = new Blob([bytes], {\n * type: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n * });\n */\nexport function writeXlsx(\n headers: readonly string[],\n rows: readonly (readonly (string | number | null)[])[],\n): Uint8Array<ArrayBuffer> {\n const headerRow: Cell[] = headers.map((h) => ({ v: h, t: \"s\" }));\n const dataRows: Cell[][] = rows.map((row) => row.map(toCell));\n const allRows: Cell[][] = [headerRow, ...dataRows];\n const columnCount = Math.max(headers.length, ...dataRows.map((row) => row.length), 0);\n const sheetXml = buildSheetXml(allRows, columnCount);\n\n const encoder = new TextEncoder();\n const archive: Zippable = {\n \"[Content_Types].xml\": encoder.encode(CONTENT_TYPES_XML),\n \"_rels/.rels\": encoder.encode(ROOT_RELS_XML),\n \"xl/workbook.xml\": encoder.encode(WORKBOOK_XML),\n \"xl/_rels/workbook.xml.rels\": encoder.encode(WORKBOOK_RELS_XML),\n \"xl/worksheets/sheet1.xml\": encoder.encode(sheetXml),\n };\n return zipSync(archive);\n}\n"],"mappings":";;AAKA,SAAS,EAAU,GAAuB;CACtC,OAAO,EACF,QAAQ,MAAM,OAAO,CAAC,CACtB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,MAAM,CAAC,CACrB,QAAQ,MAAM,QAAQ,CAAC,CACvB,QAAQ,MAAM,QAAQ;AAC/B;AAEA,SAAS,EAAU,GAAwB;CACvC,IAAI,IAAI,IAAS,GACb,IAAS;CACb,OAAO,IAAI,IAAG;EACV,IAAM,KAAO,IAAI,KAAK;EAEtB,AADA,IAAS,OAAO,aAAa,KAAK,CAAG,IAAI,GACzC,IAAI,KAAK,OAAO,IAAI,KAAK,EAAE;CAC/B;CACA,OAAO;AACX;AAEA,SAAS,EAAO,GAAqC;CAGjD,OAFI,OAAO,KAAU,WAAiB;EAAE,GAAG;EAAO,GAAG;CAAI,IACrD,MAAU,QAAQ,MAAU,KAAW;EAAE,GAAG;EAAI,GAAG;CAAI,IACpD;EAAE,GAAG;EAAO,GAAG;CAAI;AAC9B;AAEA,SAAS,EAAQ,GAAY,GAAqB;CAG9C,OAFI,EAAK,MAAM,MAAY,SAAS,EAAI,aAAa,EAAK,EAAE,YACxD,EAAK,MAAM,KAAW,SAAS,EAAI,wCAChC,SAAS,EAAI,8CAA8C,EAAU,EAAK,CAAC,EAAE;AACxF;AAEA,SAAS,EAAO,GAAwB,GAA2B;CAE/D,OAAO,WAAW,EAAU,IADd,EAAM,KAAK,GAAG,MAAM,EAAQ,GAAG,GAAG,EAAU,CAAC,IAAI,GAAW,CAAC,CAAC,CAAC,KAAK,EAClD,EAAM;AAC1C;AAEA,SAAS,EAAc,GAAoC,GAA6B;CAGpF,OACI,uJAEmB,MALC,EAAU,KAAK,IAAI,IAAc,GAAG,CAAC,CAAC,IAAI,EAAK,SAKtC,gBAJpB,EAAK,KAAK,GAAK,MAAM,EAAO,GAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAKzC,EAAK;AAG3B;AAEA,IAAM,IACF,ykBAQE,IACF,yTAKE,IACF,+SAME,IACF;AAgCJ,SAAgB,EACZ,GACA,GACuB;CACvB,IAAM,IAAoB,EAAQ,KAAK,OAAO;EAAE,GAAG;EAAG,GAAG;CAAI,EAAE,GACzD,IAAqB,EAAK,KAAK,MAAQ,EAAI,IAAI,CAAM,CAAC,GAGtD,IAAW,EAAc,CAFJ,GAAW,GAAG,CAEV,GADX,KAAK,IAAI,EAAQ,QAAQ,GAAG,EAAS,KAAK,MAAQ,EAAI,MAAM,GAAG,CAC3C,CAAW,GAE7C,IAAU,IAAI,YAAY,GAC1B,IAAoB;EACtB,uBAAuB,EAAQ,OAAO,CAAiB;EACvD,eAAe,EAAQ,OAAO,CAAa;EAC3C,mBAAmB,EAAQ,OAAO,CAAY;EAC9C,8BAA8B,EAAQ,OAAO,CAAiB;EAC9D,4BAA4B,EAAQ,OAAO,CAAQ;CACvD;CACA,OAAO,EAAQ,CAAO;AAC1B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tempest-react-sdk",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "description": "SDK público da Tempest com componentes, hooks e integrações para projetos React.",
5
5
  "type": "module",
6
6
  "license": "MIT",