release: WRNexusJS 0.5.0

This commit is contained in:
2026-07-29 12:51:10 +05:30
parent 76c768099d
commit 6afe32f63f
456 changed files with 40879 additions and 8850 deletions
+100
View File
@@ -0,0 +1,100 @@
import type {
PasskeyAuthenticationOptions,
PasskeyProvider,
PasskeyRegistrationOptions,
} from "../types.ts";
export type { PasskeyProvider } from "../types.ts";
export type PasskeyChallengeKind = "registration" | "authentication";
export interface PasskeyChallengeRecord {
challenge: string;
kind: PasskeyChallengeKind;
userId?: string;
rpId: string;
origin: string;
expiresAt: number;
}
/**
* Durable challenge storage contract. Production deployments with more than one
* process should provide a shared implementation (for example Redis or SQL).
*/
export interface PasskeyChallengeStore {
set(key: string, value: PasskeyChallengeRecord): Promise<void>;
consume(key: string): Promise<PasskeyChallengeRecord | undefined>;
}
export class MemoryPasskeyChallengeStore implements PasskeyChallengeStore {
private readonly values = new Map<string, PasskeyChallengeRecord>();
private readonly now: () => number;
constructor(now: () => number = () => Date.now()) {
this.now = now;
}
private pruneExpired(): void {
const timestamp = this.now();
for (const [key, value] of this.values) {
if (value.expiresAt <= timestamp) this.values.delete(key);
}
}
async set(key: string, value: PasskeyChallengeRecord): Promise<void> {
this.pruneExpired();
this.values.set(key, { ...value });
}
async consume(key: string): Promise<PasskeyChallengeRecord | undefined> {
this.pruneExpired();
const value = this.values.get(key);
this.values.delete(key);
return value ? { ...value } : undefined;
}
}
export function assertPasskeyProvider(provider: PasskeyProvider | undefined): PasskeyProvider {
if (!provider) throw new Error("WRN-AUTH-PASSKEY-PROVIDER: configure a PasskeyProvider");
return provider;
}
export function publicKeyCreationOptions(
options: PasskeyRegistrationOptions,
): PublicKeyCredentialCreationOptions {
return {
...options,
challenge: decode(options.challenge),
user: { ...options.user, id: decode(options.user.id) },
excludeCredentials: options.excludeCredentials?.map((item) => ({
...item,
id: decode(item.id),
transports: item.transports as AuthenticatorTransport[] | undefined,
})),
} as unknown as PublicKeyCredentialCreationOptions;
}
export function publicKeyRequestOptions(
options: PasskeyAuthenticationOptions,
): PublicKeyCredentialRequestOptions {
return {
...options,
challenge: decode(options.challenge),
allowCredentials: options.allowCredentials?.map((item) => ({
...item,
id: decode(item.id),
transports: item.transports as AuthenticatorTransport[] | undefined,
})),
} as unknown as PublicKeyCredentialRequestOptions;
}
function decode(value: string): Uint8Array {
if (!value || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) {
throw new TypeError("Passkey data must be canonical Base64URL without padding");
}
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
const binary = atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4));
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
return bytes;
}