release: WRNexusJS 0.4.0

This commit is contained in:
2026-07-27 12:42:18 +05:30
parent 8b728a3e5d
commit 30e5721e84
250 changed files with 10065 additions and 3923 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/oauth",
"version": "0.3.6",
"version": "0.4.0",
"private": true,
"type": "module",
"main": "src/index.ts",
+99
View File
@@ -0,0 +1,99 @@
import { randomToken, type OAuthProvider, type OAuthTokens } from "./index.ts";
export interface OAuthStateRecord {
state: string;
verifier: string;
redirectUri: string;
returnTo?: string;
expiresAt: number;
}
export interface OAuthStateStore {
set(record: OAuthStateRecord): Promise<void>;
consume(state: string): Promise<OAuthStateRecord | null>;
}
export function memoryOAuthStateStore(now: () => number = Date.now): OAuthStateStore {
const records = new Map<string, OAuthStateRecord>();
return {
async set(record) {
records.set(record.state, record);
},
async consume(state) {
const value = records.get(state);
records.delete(state);
if (!value || value.expiresAt <= now()) return null;
return value;
},
};
}
export async function createOAuthState(
store: OAuthStateStore,
input: Omit<OAuthStateRecord, "state" | "expiresAt"> & { ttlMs?: number },
): Promise<OAuthStateRecord> {
const record: OAuthStateRecord = {
state: randomToken(),
verifier: input.verifier,
redirectUri: input.redirectUri,
returnTo: input.returnTo,
expiresAt: Date.now() + (input.ttlMs ?? 10 * 60 * 1000),
};
await store.set(record);
return record;
}
export async function refreshOAuthTokens(
provider: OAuthProvider,
refreshToken: string,
fetchImpl: typeof fetch = fetch,
): Promise<OAuthTokens> {
const response = await fetchImpl(provider.tokenUrl, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: provider.clientId,
client_secret: provider.clientSecret,
}),
});
if (!response.ok) throw new Error(`${provider.name} token refresh failed (${response.status})`);
const tokens = (await response.json()) as OAuthTokens;
if (!tokens.refresh_token) tokens.refresh_token = refreshToken;
return tokens;
}
export interface OidcDiscovery {
issuer: string;
authorization_endpoint: string;
token_endpoint: string;
userinfo_endpoint?: string;
jwks_uri: string;
revocation_endpoint?: string;
}
export async function discoverOidc(
issuer: string,
fetchImpl: typeof fetch = fetch,
): Promise<OidcDiscovery> {
const base = issuer.replace(/\/$/, "");
const response = await fetchImpl(`${base}/.well-known/openid-configuration`);
if (!response.ok) throw new Error(`OIDC discovery failed (${response.status})`);
const value = (await response.json()) as OidcDiscovery;
if (value.issuer !== issuer && value.issuer !== base) throw new Error("OIDC issuer mismatch");
return value;
}
export function validateOAuthReturnTo(
value: string | undefined,
origin: string,
fallback = "/",
): string {
if (!value) return fallback;
try {
const url = new URL(value, origin);
return url.origin === new URL(origin).origin
? `${url.pathname}${url.search}${url.hash}`
: fallback;
} catch {
return fallback;
}
}
+8
View File
@@ -241,3 +241,11 @@ export async function fetchProfile(
if (!res.ok) throw new Error(`${provider.name} userinfo failed (${res.status})`);
return provider.mapProfile((await res.json()) as Record<string, unknown>);
}
export {
memoryOAuthStateStore,
createOAuthState,
refreshOAuthTokens,
discoverOidc,
validateOAuthReturnTo,
} from "./advanced.ts";
export type { OAuthStateRecord, OAuthStateStore, OidcDiscovery } from "./advanced.ts";