@idpartners/client-attestation-sdk 0.0.0-stage → 0.2.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Validator configuration.
3
+ */
4
+ import type { JSONWebKeySet } from "jose";
5
+ /** Default accepted JWS algorithms: EC / RSA / RSA-PSS at 256/384/512. */
6
+ export declare const DEFAULT_ALGORITHMS: readonly string[];
7
+ /** RFC 9068 section 2.1: a JWT access token's `typ` is `at+jwt` (the `application/` prefix is optional). */
8
+ export declare const DEFAULT_TOKEN_TYPES: readonly string[];
9
+ /** How the resource server authenticates to the AS when introspecting. */
10
+ export type IntrospectionAuthMethod = "client_secret_basic" | "client_secret_post";
11
+ /**
12
+ * RFC 7662 introspection endpoint + client credentials (the resource server authenticating to the AS).
13
+ */
14
+ export interface IntrospectionConfig {
15
+ endpoint: string;
16
+ clientId: string;
17
+ clientSecret: string;
18
+ /** Defaults to `"client_secret_basic"`. */
19
+ authMethod?: IntrospectionAuthMethod;
20
+ }
21
+ /**
22
+ * What this resource server accepts: the trusted issuer, its own audience identifier(s), the signing
23
+ * keys (static `jwks` or a `jwksUri` to fetch), required scopes, accepted algorithms, clock leeway,
24
+ * and optional introspection.
25
+ *
26
+ * `issuer` and `audiences` are required: a validator that skips either accepts tokens minted for someone
27
+ * else. `acceptedTokenTypes` is checked against the JWT `typ` header when the token carries one, so an ID
28
+ * token can't stand in for an access token. Pass `null` to turn that check off for an issuer that stamps
29
+ * something other than `at+jwt`.
30
+ */
31
+ export interface ValidatorConfig {
32
+ issuer: string;
33
+ audiences: string | string[];
34
+ jwks?: JSONWebKeySet;
35
+ jwksUri?: string;
36
+ requiredScopes?: string[];
37
+ /** Defaults to {@link DEFAULT_ALGORITHMS}. */
38
+ acceptedAlgorithms?: string[];
39
+ /** Clock skew allowed on `exp` / `nbf` / `iat`, in seconds. Defaults to 60. */
40
+ leewaySeconds?: number;
41
+ introspection?: IntrospectionConfig;
42
+ /** Defaults to {@link DEFAULT_TOKEN_TYPES}; `null` disables the `typ` check. */
43
+ acceptedTokenTypes?: string[] | null;
44
+ }
45
+ /** A {@link ValidatorConfig} with every optional field defaulted. */
46
+ export interface ResolvedConfig {
47
+ issuer: string;
48
+ audiences: string[];
49
+ jwks?: JSONWebKeySet;
50
+ jwksUri?: string;
51
+ requiredScopes: string[];
52
+ acceptedAlgorithms: string[];
53
+ leewaySeconds: number;
54
+ introspection?: IntrospectionConfig;
55
+ /** Normalised with {@link normaliseType}; `null` when the check is off. */
56
+ acceptedTokenTypes: string[] | null;
57
+ }
58
+ /**
59
+ * Compare JOSE `typ` values the way RFC 7515 section 4.1.9 says to: case-insensitively, with an optional
60
+ * `application/` prefix.
61
+ */
62
+ export declare function normaliseType(value: string): string;
63
+ /** Normalize a caller-supplied config, applying defaults (mirrors Python's `ValidatorConfig`). */
64
+ export declare function resolveConfig(config: ValidatorConfig): ResolvedConfig;
@@ -0,0 +1,44 @@
1
+ /** Default accepted JWS algorithms: EC / RSA / RSA-PSS at 256/384/512. */
2
+ export const DEFAULT_ALGORITHMS = [
3
+ "ES256",
4
+ "ES384",
5
+ "ES512",
6
+ "RS256",
7
+ "RS384",
8
+ "RS512",
9
+ "PS256",
10
+ "PS384",
11
+ "PS512",
12
+ ];
13
+ /** RFC 9068 section 2.1: a JWT access token's `typ` is `at+jwt` (the `application/` prefix is optional). */
14
+ export const DEFAULT_TOKEN_TYPES = ["at+jwt"];
15
+ /**
16
+ * Compare JOSE `typ` values the way RFC 7515 section 4.1.9 says to: case-insensitively, with an optional
17
+ * `application/` prefix.
18
+ */
19
+ export function normaliseType(value) {
20
+ const lower = value.toLowerCase();
21
+ return lower.startsWith("application/") ? lower.slice("application/".length) : lower;
22
+ }
23
+ /** Normalize a caller-supplied config, applying defaults (mirrors Python's `ValidatorConfig`). */
24
+ export function resolveConfig(config) {
25
+ if (!config.issuer) {
26
+ throw new Error("issuer is required");
27
+ }
28
+ const audiences = typeof config.audiences === "string" ? [config.audiences] : [...(config.audiences ?? [])];
29
+ if (audiences.length === 0) {
30
+ throw new Error("at least one audience is required");
31
+ }
32
+ const types = config.acceptedTokenTypes === undefined ? DEFAULT_TOKEN_TYPES : config.acceptedTokenTypes;
33
+ return {
34
+ issuer: config.issuer,
35
+ audiences,
36
+ jwks: config.jwks,
37
+ jwksUri: config.jwksUri,
38
+ requiredScopes: [...(config.requiredScopes ?? [])],
39
+ acceptedAlgorithms: [...(config.acceptedAlgorithms ?? DEFAULT_ALGORITHMS)],
40
+ leewaySeconds: config.leewaySeconds ?? 60,
41
+ introspection: config.introspection,
42
+ acceptedTokenTypes: types === null ? null : types.map(normaliseType),
43
+ };
44
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Stable validation outcome codes, shared across the Python / TypeScript / Go ports so their verdicts
3
+ * can be compared directly.
4
+ */
5
+ export declare const VALID = "valid";
6
+ export declare const EXPIRED = "expired";
7
+ export declare const NOT_YET_VALID = "not_yet_valid";
8
+ export declare const INVALID_SIGNATURE = "invalid_signature";
9
+ export declare const INVALID_ISSUER = "invalid_issuer";
10
+ export declare const INVALID_AUDIENCE = "invalid_audience";
11
+ export declare const INSUFFICIENT_SCOPE = "insufficient_scope";
12
+ export declare const INVALID_TOKEN = "invalid_token";
13
+ export declare const UNSUPPORTED_ALGORITHM = "unsupported_algorithm";
14
+ export declare const KEY_NOT_FOUND = "key_not_found";
15
+ export declare const INACTIVE = "inactive";
16
+ export declare const MISSING_EXPIRY = "missing_expiry";
17
+ export declare const ISSUED_IN_FUTURE = "issued_in_future";
18
+ export declare const INVALID_TOKEN_TYPE = "invalid_token_type";
19
+ /** Every stable error code a validation can report. */
20
+ export type ValidationError = typeof EXPIRED | typeof NOT_YET_VALID | typeof INVALID_SIGNATURE | typeof INVALID_ISSUER | typeof INVALID_AUDIENCE | typeof INSUFFICIENT_SCOPE | typeof INVALID_TOKEN | typeof UNSUPPORTED_ALGORITHM | typeof KEY_NOT_FOUND | typeof INACTIVE | typeof MISSING_EXPIRY | typeof ISSUED_IN_FUTURE | typeof INVALID_TOKEN_TYPE;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Stable validation outcome codes, shared across the Python / TypeScript / Go ports so their verdicts
3
+ * can be compared directly.
4
+ */
5
+ export const VALID = "valid";
6
+ export const EXPIRED = "expired";
7
+ export const NOT_YET_VALID = "not_yet_valid";
8
+ export const INVALID_SIGNATURE = "invalid_signature";
9
+ export const INVALID_ISSUER = "invalid_issuer";
10
+ export const INVALID_AUDIENCE = "invalid_audience";
11
+ export const INSUFFICIENT_SCOPE = "insufficient_scope";
12
+ export const INVALID_TOKEN = "invalid_token";
13
+ export const UNSUPPORTED_ALGORITHM = "unsupported_algorithm";
14
+ export const KEY_NOT_FOUND = "key_not_found";
15
+ export const INACTIVE = "inactive";
16
+ export const MISSING_EXPIRY = "missing_expiry";
17
+ export const ISSUED_IN_FUTURE = "issued_in_future";
18
+ export const INVALID_TOKEN_TYPE = "invalid_token_type";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Resource-server access-token validation: JWT signature + scope/audience checks in a fixed,
3
+ * cross-language error-precedence order, plus optional RFC 7662 introspection.
4
+ *
5
+ * This is the validator counterpart to the client-side builder SDK, and mirrors the Python
6
+ * `token_validator` package so every port reports the same verdict for the shared vectors.
7
+ */
8
+ export { AccessTokenValidator } from "./validator.js";
9
+ export type { HttpPost, Claims, ValidatorOptions } from "./validator.js";
10
+ export type { HttpGet } from "./jwks.js";
11
+ export { resolveConfig, DEFAULT_ALGORITHMS, DEFAULT_TOKEN_TYPES } from "./config.js";
12
+ export type { ValidatorConfig, ResolvedConfig, IntrospectionConfig, IntrospectionAuthMethod, } from "./config.js";
13
+ export type { ValidationResult } from "./result.js";
14
+ export type { ValidationError } from "./errors.js";
15
+ export * as errors from "./errors.js";
16
+ export { ProtectedResource, bearerToken } from "./resource.js";
17
+ export type { ProtectedResourceMetadata, AuthDecision } from "./resource.js";
18
+ export declare const VALIDATOR_VERSION = "0.2.0";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resource-server access-token validation: JWT signature + scope/audience checks in a fixed,
3
+ * cross-language error-precedence order, plus optional RFC 7662 introspection.
4
+ *
5
+ * This is the validator counterpart to the client-side builder SDK, and mirrors the Python
6
+ * `token_validator` package so every port reports the same verdict for the shared vectors.
7
+ */
8
+ export { AccessTokenValidator } from "./validator.js";
9
+ export { resolveConfig, DEFAULT_ALGORITHMS, DEFAULT_TOKEN_TYPES } from "./config.js";
10
+ export * as errors from "./errors.js";
11
+ export { ProtectedResource, bearerToken } from "./resource.js";
12
+ export const VALIDATOR_VERSION = "0.2.0";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Resolves issuer signing keys — from a static JWKS or a `jwksUri` (fetched and cached, refreshed on an
3
+ * unknown `kid` and when the cached set grows old).
4
+ *
5
+ * A `jwksUri` provider never fetches more than once per `minRefreshSeconds`, however many unknown `kid`
6
+ * values it is shown, and concurrent misses share one fetch. Without that, a stream of tokens with made-up
7
+ * `kid` values turns every request into a request to the issuer.
8
+ */
9
+ import { type CryptoKey, type JSONWebKeySet } from "jose";
10
+ /** An imported verification key, as returned by jose's `importJWK`. */
11
+ export type VerificationKey = CryptoKey | Uint8Array;
12
+ /** Fetch a URL and return its body text. Injectable so tests can avoid real network I/O. */
13
+ export type HttpGet = (url: string) => Promise<string>;
14
+ export declare const MAX_JWKS_BYTES: number;
15
+ export declare const DEFAULT_MIN_REFRESH_SECONDS = 30;
16
+ export declare const DEFAULT_MAX_AGE_SECONDS = 3600;
17
+ export interface JwksProviderOptions {
18
+ jwks?: JSONWebKeySet;
19
+ jwksUri?: string;
20
+ httpGet?: HttpGet;
21
+ /** Minimum time between fetches. Defaults to {@link DEFAULT_MIN_REFRESH_SECONDS}. */
22
+ minRefreshSeconds?: number;
23
+ /** Refresh a fetched set once it is this old. Defaults to {@link DEFAULT_MAX_AGE_SECONDS}. */
24
+ maxAgeSeconds?: number;
25
+ /** Milliseconds clock, injectable for tests. Defaults to `Date.now`. */
26
+ clock?: () => number;
27
+ }
28
+ export declare class JwksProvider {
29
+ #private;
30
+ /** Why the last fetch failed, or `null` after a successful one. */
31
+ lastError: string | null;
32
+ constructor(opts: JwksProviderOptions);
33
+ /**
34
+ * Resolve the verification key for a header `kid` (and `alg`). Falls back to the single configured key
35
+ * when the header carries no `kid`. Returns `undefined` when no key can be found; never rejects — a
36
+ * failed fetch leaves its reason in {@link lastError}.
37
+ */
38
+ resolve(kid: string | undefined, alg?: string): Promise<VerificationKey | undefined>;
39
+ }
@@ -0,0 +1,135 @@
1
+ var _a;
2
+ /**
3
+ * Resolves issuer signing keys — from a static JWKS or a `jwksUri` (fetched and cached, refreshed on an
4
+ * unknown `kid` and when the cached set grows old).
5
+ *
6
+ * A `jwksUri` provider never fetches more than once per `minRefreshSeconds`, however many unknown `kid`
7
+ * values it is shown, and concurrent misses share one fetch. Without that, a stream of tokens with made-up
8
+ * `kid` values turns every request into a request to the issuer.
9
+ */
10
+ import { importJWK } from "jose";
11
+ import { request } from "../http.js";
12
+ export const MAX_JWKS_BYTES = 512 * 1024;
13
+ export const DEFAULT_MIN_REFRESH_SECONDS = 30;
14
+ export const DEFAULT_MAX_AGE_SECONDS = 3600;
15
+ const FETCH_TIMEOUT_MS = 10_000;
16
+ const defaultGet = async (url) => {
17
+ // the shared transport refuses redirects and caps the body at MAX_JWKS_BYTES while reading it
18
+ const resp = await request(url, { method: "GET", timeoutMs: FETCH_TIMEOUT_MS, maxBytes: MAX_JWKS_BYTES });
19
+ if (!resp.ok) {
20
+ throw new Error(`GET ${url} failed: ${resp.status}`);
21
+ }
22
+ return resp.body;
23
+ };
24
+ /** Best-effort JWS `alg` for a JWK that omits `alg`, from its key type / curve. */
25
+ function algorithmFor(jwk) {
26
+ if (jwk.kty === "EC") {
27
+ return { "P-256": "ES256", "P-384": "ES384", "P-521": "ES512" }[jwk.crv ?? ""] ?? "ES256";
28
+ }
29
+ if (jwk.kty === "RSA") {
30
+ return "RS256";
31
+ }
32
+ if (jwk.kty === "OKP") {
33
+ return "EdDSA";
34
+ }
35
+ return "RS256";
36
+ }
37
+ export class JwksProvider {
38
+ #static;
39
+ #jwksUri;
40
+ #httpGet;
41
+ #minRefreshMs;
42
+ #maxAgeMs;
43
+ #clock;
44
+ /** kid -> key, plus the JWK's declared `alg` (a JWK that declares one is only used for that alg). */
45
+ #keys = new Map();
46
+ #loaded = null;
47
+ #inFlight = null;
48
+ #loadedAt = null;
49
+ #lastAttempt = null;
50
+ /** Why the last fetch failed, or `null` after a successful one. */
51
+ lastError = null;
52
+ constructor(opts) {
53
+ this.#static = opts.jwks != null;
54
+ this.#jwksUri = opts.jwksUri;
55
+ this.#httpGet = opts.httpGet ?? defaultGet;
56
+ this.#minRefreshMs = (opts.minRefreshSeconds ?? DEFAULT_MIN_REFRESH_SECONDS) * 1000;
57
+ this.#maxAgeMs = (opts.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS) * 1000;
58
+ this.#clock = opts.clock ?? Date.now;
59
+ if (opts.jwks != null) {
60
+ this.#loaded = _a.#parse(opts.jwks).then((keys) => {
61
+ this.#keys = keys;
62
+ });
63
+ }
64
+ }
65
+ static async #parse(jwks) {
66
+ const keys = new Map();
67
+ for (const jwk of jwks.keys ?? []) {
68
+ const alg = jwk.alg ?? algorithmFor(jwk);
69
+ try {
70
+ keys.set(jwk.kid, { key: await importJWK(jwk, alg), alg: jwk.alg });
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ }
76
+ return keys;
77
+ }
78
+ /**
79
+ * Resolve the verification key for a header `kid` (and `alg`). Falls back to the single configured key
80
+ * when the header carries no `kid`. Returns `undefined` when no key can be found; never rejects — a
81
+ * failed fetch leaves its reason in {@link lastError}.
82
+ */
83
+ async resolve(kid, alg) {
84
+ if (this.#loaded) {
85
+ await this.#loaded;
86
+ }
87
+ if (!this.#static && this.#jwksUri && (this.#lookup(kid) === undefined || this.#stale())) {
88
+ await this.#refresh();
89
+ }
90
+ const entry = this.#lookup(kid);
91
+ if (entry === undefined || (alg != null && entry.alg != null && entry.alg !== alg)) {
92
+ return undefined;
93
+ }
94
+ return entry.key;
95
+ }
96
+ #lookup(kid) {
97
+ if (kid != null) {
98
+ return this.#keys.get(kid);
99
+ }
100
+ return this.#keys.size === 1 ? this.#keys.values().next().value : undefined;
101
+ }
102
+ #stale() {
103
+ return this.#loadedAt === null || this.#clock() - this.#loadedAt >= this.#maxAgeMs;
104
+ }
105
+ #refresh() {
106
+ if (this.#inFlight) {
107
+ return this.#inFlight;
108
+ }
109
+ const now = this.#clock();
110
+ if (this.#lastAttempt !== null && now - this.#lastAttempt < this.#minRefreshMs) {
111
+ return Promise.resolve();
112
+ }
113
+ this.#lastAttempt = now;
114
+ this.#inFlight = (async () => {
115
+ try {
116
+ const body = await this.#httpGet(this.#jwksUri);
117
+ if (body.length > MAX_JWKS_BYTES) {
118
+ throw new Error(`JWKS is larger than ${MAX_JWKS_BYTES} bytes`);
119
+ }
120
+ this.#keys = await _a.#parse(JSON.parse(body));
121
+ this.#loadedAt = this.#clock();
122
+ this.lastError = null;
123
+ }
124
+ catch (exc) {
125
+ // keep serving the keys already held
126
+ this.lastError = `JWKS fetch failed: ${exc instanceof Error ? exc.message : String(exc)}`;
127
+ }
128
+ finally {
129
+ this.#inFlight = null;
130
+ }
131
+ })();
132
+ return this.#inFlight;
133
+ }
134
+ }
135
+ _a = JwksProvider;
@@ -0,0 +1,42 @@
1
+ import type { ValidationResult } from "./result.js";
2
+ import type { AccessTokenValidator } from "./validator.js";
3
+ /** Extract the token from an `Authorization: Bearer <token>` header value, or null. */
4
+ export declare function bearerToken(authorizationHeader?: string | null): string | null;
5
+ /** RFC 9728 protected-resource metadata document. */
6
+ export interface ProtectedResourceMetadata {
7
+ resource: string;
8
+ authorization_servers: string[];
9
+ bearer_methods_supported: string[];
10
+ scopes_supported?: string[];
11
+ }
12
+ /** The outcome of guarding a request. */
13
+ export interface AuthDecision {
14
+ authorized: boolean;
15
+ result?: ValidationResult;
16
+ status: number;
17
+ wwwAuthenticate?: string;
18
+ error?: string;
19
+ errorDescription?: string | null;
20
+ }
21
+ /**
22
+ * An OAuth-protected resource server. The `validator` must accept `resource` as an audience — that binds
23
+ * incoming tokens to this resource (RFC 8707), which MCP servers MUST enforce. The constructor refuses one
24
+ * that doesn't.
25
+ */
26
+ export declare class ProtectedResource {
27
+ #private;
28
+ readonly resource: string;
29
+ readonly authorizationServers: string[];
30
+ readonly scopesSupported: string[];
31
+ constructor(resource: string, authorizationServers: string[], validator: AccessTokenValidator, scopesSupported?: string[]);
32
+ metadata(): ProtectedResourceMetadata;
33
+ metadataPath(): string;
34
+ metadataUrl(): string;
35
+ challenge(error?: string, errorDescription?: string | null): string;
36
+ authenticate(authorizationHeader?: string | null, requiredScopes?: string[]): Promise<AuthDecision>;
37
+ }
38
+ /**
39
+ * Make a value safe inside a quoted-string header parameter: drop CR, LF and every other control
40
+ * character (0x00-0x1F, 0x7F) so nothing can split or forge a header, then escape backslash and quote.
41
+ */
42
+ export declare function quote(value: string): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OAuth 2.0/2.1 protected-resource conventions on top of {@link AccessTokenValidator}.
3
+ *
4
+ * A {@link ProtectedResource} is the resource-server side of any OAuth-protected HTTP service — an MCP
5
+ * server (OAuth 2.1 resource server per the MCP authorization spec), an A2A agent, or a plain REST API. It
6
+ * adds what the validator itself doesn't: RFC 9728 Protected Resource Metadata
7
+ * (`/.well-known/oauth-protected-resource`), RFC 6750 bearer extraction + `WWW-Authenticate` challenges, and
8
+ * a request guard that binds the token audience to this resource (RFC 8707). Protocol-neutral — not MCP-only.
9
+ */
10
+ import * as errors from "./errors.js";
11
+ const WELL_KNOWN = "/.well-known/oauth-protected-resource";
12
+ /** Extract the token from an `Authorization: Bearer <token>` header value, or null. */
13
+ export function bearerToken(authorizationHeader) {
14
+ if (!authorizationHeader)
15
+ return null;
16
+ const match = /^\s*Bearer\s+(.+?)\s*$/i.exec(authorizationHeader);
17
+ return match ? match[1] : null;
18
+ }
19
+ /**
20
+ * An OAuth-protected resource server. The `validator` must accept `resource` as an audience — that binds
21
+ * incoming tokens to this resource (RFC 8707), which MCP servers MUST enforce. The constructor refuses one
22
+ * that doesn't.
23
+ */
24
+ export class ProtectedResource {
25
+ resource;
26
+ authorizationServers;
27
+ scopesSupported;
28
+ #validator;
29
+ constructor(resource, authorizationServers, validator, scopesSupported = []) {
30
+ if (!validator.config.audiences.includes(resource)) {
31
+ throw new Error(`the validator does not accept this resource (${resource}) as an audience`);
32
+ }
33
+ this.resource = resource;
34
+ this.authorizationServers = authorizationServers;
35
+ this.#validator = validator;
36
+ this.scopesSupported = scopesSupported;
37
+ }
38
+ metadata() {
39
+ const md = {
40
+ resource: this.resource,
41
+ authorization_servers: this.authorizationServers,
42
+ bearer_methods_supported: ["header"],
43
+ };
44
+ if (this.scopesSupported.length > 0)
45
+ md.scopes_supported = this.scopesSupported;
46
+ return md;
47
+ }
48
+ metadataPath() {
49
+ const path = new URL(this.resource).pathname;
50
+ return path && path !== "/" ? WELL_KNOWN + path : WELL_KNOWN;
51
+ }
52
+ metadataUrl() {
53
+ const u = new URL(this.resource);
54
+ return `${u.protocol}//${u.host}${this.metadataPath()}`;
55
+ }
56
+ challenge(error, errorDescription) {
57
+ const params = [];
58
+ if (error) {
59
+ params.push(`error="${quote(error)}"`);
60
+ if (errorDescription)
61
+ params.push(`error_description="${quote(errorDescription)}"`);
62
+ }
63
+ params.push(`resource_metadata="${quote(this.metadataUrl())}"`);
64
+ return "Bearer " + params.join(", ");
65
+ }
66
+ async authenticate(authorizationHeader, requiredScopes) {
67
+ const token = bearerToken(authorizationHeader);
68
+ if (token == null) {
69
+ return { authorized: false, status: 401, wwwAuthenticate: this.challenge(), errorDescription: "authentication required" };
70
+ }
71
+ const result = await this.#validator.validate(token, requiredScopes);
72
+ if (result.valid) {
73
+ return { authorized: true, result, status: 200 };
74
+ }
75
+ const [status, oauthError] = httpError(result.error);
76
+ return {
77
+ authorized: false,
78
+ result,
79
+ status,
80
+ error: oauthError,
81
+ errorDescription: result.errorDescription,
82
+ wwwAuthenticate: this.challenge(oauthError, result.errorDescription),
83
+ };
84
+ }
85
+ }
86
+ function httpError(error) {
87
+ return error === errors.INSUFFICIENT_SCOPE ? [403, "insufficient_scope"] : [401, "invalid_token"];
88
+ }
89
+ /**
90
+ * Make a value safe inside a quoted-string header parameter: drop CR, LF and every other control
91
+ * character (0x00-0x1F, 0x7F) so nothing can split or forge a header, then escape backslash and quote.
92
+ */
93
+ export function quote(value) {
94
+ // eslint-disable-next-line no-control-regex
95
+ return value.replace(/[\x00-\x1f\x7f]/g, "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
96
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The outcome of validating an access token: either a valid token (with its subject, granted scopes,
3
+ * audience and claims) or a failure carrying a stable `error` code (see {@link ./errors}).
4
+ */
5
+ import type { ValidationError } from "./errors.js";
6
+ export interface ValidationResult {
7
+ valid: boolean;
8
+ /** Stable error code when `valid` is false; `null` when valid. */
9
+ error: ValidationError | null;
10
+ errorDescription: string | null;
11
+ subject: string | null;
12
+ scopes: string[];
13
+ audience: string[];
14
+ claims: Record<string, unknown>;
15
+ /** The `exp` claim (integer epoch seconds) when present. */
16
+ expiresAt: number | null;
17
+ }
18
+ export declare function success(claims: Record<string, unknown>, scopes: string[], audience: string[]): ValidationResult;
19
+ export declare function failure(error: ValidationError, description?: string): ValidationResult;
@@ -0,0 +1,26 @@
1
+ export function success(claims, scopes, audience) {
2
+ const sub = claims.sub;
3
+ const exp = claims.exp;
4
+ return {
5
+ valid: true,
6
+ error: null,
7
+ errorDescription: null,
8
+ subject: typeof sub === "string" ? sub : null,
9
+ scopes,
10
+ audience,
11
+ claims,
12
+ expiresAt: exp == null ? null : Number(exp),
13
+ };
14
+ }
15
+ export function failure(error, description) {
16
+ return {
17
+ valid: false,
18
+ error,
19
+ errorDescription: description ?? null,
20
+ subject: null,
21
+ scopes: [],
22
+ audience: [],
23
+ claims: {},
24
+ expiresAt: null,
25
+ };
26
+ }
@@ -0,0 +1,33 @@
1
+ import { type ResolvedConfig, type ValidatorConfig } from "./config.js";
2
+ import { type HttpGet } from "./jwks.js";
3
+ import { type ValidationResult } from "./result.js";
4
+ /** Claims as decoded from a JWT payload (or an introspection response). */
5
+ export type Claims = Record<string, unknown>;
6
+ /**
7
+ * Inject a POST transport (mirrors Python's `http_post`). Resolves to the parsed JSON response body.
8
+ */
9
+ export type HttpPost = (url: string, body: string, headers: Record<string, string>) => Promise<Claims>;
10
+ /** Optional injectable transports, primarily so tests can mock network I/O. */
11
+ export interface ValidatorOptions {
12
+ httpPost?: HttpPost;
13
+ httpGet?: HttpGet;
14
+ }
15
+ /** Validates access tokens for a resource server. */
16
+ export declare class AccessTokenValidator {
17
+ #private;
18
+ readonly config: ResolvedConfig;
19
+ constructor(config: ValidatorConfig, opts?: ValidatorOptions);
20
+ /**
21
+ * Locally validate a JWT access token. Returns the first failure in the fixed contract order, or a
22
+ * success result exposing subject / scopes / audience / claims.
23
+ */
24
+ validate(token: string, requiredScopes?: string[]): Promise<ValidationResult>;
25
+ /**
26
+ * RFC 7662: POST the token to the AS introspection endpoint and return the parsed response.
27
+ */
28
+ introspect(token: string, tokenTypeHint?: string): Promise<Claims>;
29
+ /**
30
+ * Introspect the token and enforce `active` plus scope/audience from the response.
31
+ */
32
+ validateActive(token: string, requiredScopes?: string[]): Promise<ValidationResult>;
33
+ }