d3ro-voice/apps/mobile-rn/src/lib/pkce-s256.ts
2026-08-29 18:33:45 +09:00

55 lines
2 KiB
TypeScript

import { sha256 } from '@noble/hashes/sha256'
type DigestAlgorithm = string | { readonly name?: unknown }
type DigestInput = ArrayBuffer | ArrayBufferView
interface PkceSubtleCrypto {
digest?: (algorithm: DigestAlgorithm, data: DigestInput) => Promise<ArrayBuffer>
}
export interface PkceCryptoTarget {
getRandomValues?: <T extends ArrayBufferView>(array: T) => T
subtle?: PkceSubtleCrypto
}
function normalizeAlgorithm(algorithm: DigestAlgorithm): string {
const name = typeof algorithm === 'string' ? algorithm : algorithm.name
return typeof name === 'string' ? name.trim().toUpperCase().replaceAll('_', '-') : ''
}
function inputBytes(data: DigestInput): Uint8Array {
if (data instanceof ArrayBuffer) return new Uint8Array(data)
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
}
/**
* Supabase Auth requires WebCrypto SHA-256 to generate an S256 PKCE challenge.
* React Native's native random-values shim supplies CSPRNG bytes but not
* `crypto.subtle`; without this narrow digest implementation auth-js silently
* downgrades to `code_challenge_method=plain`.
*/
export function installPkceS256(target: PkceCryptoTarget | undefined): void {
if (!target || typeof target.getRandomValues !== 'function') {
throw new Error('pkce_native_csprng_unavailable')
}
if (typeof target.subtle?.digest === 'function') return
const digest = async (algorithm: DigestAlgorithm, data: DigestInput): Promise<ArrayBuffer> => {
if (normalizeAlgorithm(algorithm) !== 'SHA-256') {
throw new Error('pkce_digest_algorithm_unsupported')
}
if (!(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
throw new Error('pkce_digest_input_invalid')
}
return new Uint8Array(sha256(inputBytes(data))).buffer
}
Object.defineProperty(target, 'subtle', {
value: Object.freeze({ digest }),
enumerable: true,
configurable: false,
writable: false,
})
}
installPkceS256(globalThis.crypto as unknown as PkceCryptoTarget | undefined)