export { O as OnBehalfOf, S as S2SAuthContext, a as ServerUnaryCallWithAuth, U as USER_ROLES, b as UserActorEnvelope, c as UserActorEnvelopeError, d as UserRole, X as X_USER_CONTEXT_HEADER, e as acceptUserActorEnvelope, f as decodeUserActorEnvelope, g as encodeUserActorEnvelope, i as isUserRole } from './auth-context-CQT2iDIf.js'; import { R as ReplayStore } from './jti-replayed-BS2cgKFP.js'; export { J as JtiReplayed, S as S2SAudienceMismatch, a as S2SIssuerMismatch, b as S2SReplayStoreUnavailable, c as S2STokenExpired, d as S2STokenInvalid, e as S2SVerifyError } from './jti-replayed-BS2cgKFP.js'; import { GetKeyFn } from './verify/index.js'; export { DEFAULT_JWKS_S3_KEY, JWKS_ENV_VARS, JwksFromEnvOptions, JwksObject, JwksProviderOptions, RemoteJwksProviderOptions, SigV4S3JwksProviderOptions, SigV4S3RemoteJWKSetOptions, StaticJwksProviderOptions, VerifiedS2SToken, VerifyOptions, buildSigV4S3Client, createJwksProvider, createJwksProviderFromEnv, createSigV4S3JWKSet, setJwksResolver, verifyS2SToken } from './verify/index.js'; import { UserPermissionResolver } from './server/index.js'; export { CheckUserPermissionNotConfigured, CheckUserPermissionOptions, CheckUserPermissionResult, CheckUserPermissionValidationError, DEFAULT_CLOCK_SKEW_SECONDS, MfaRequiredOptions, PermissionDigestMismatch, ServerAuthOptions, UnaryValueHandler, WithAuthUnaryOptions, WithAuthUnaryValueOptions, checkUserPermission, computePermissionsDigest, createServerAuthInterceptor, getRequiredScopesRegistry, grpcError, mfaRequiredInterceptor, registerRequiredScope, requirePermission, requireScope, resetCheckUserPermissionCacheForTesting, resetRequiredScopesRegistryForTesting, withAuthUnary, withAuthUnaryValue } from './server/index.js'; import { S2STokenManager } from './client/index.js'; export { BuildS2SClientOptions, ClientAuthOptions, DEFAULT_WAIT_FOR_READY_MS, GetTokenParams, GrpcClientCtor, GrpcClientLike, IssuedToken, KmsSignerAwsOptions, S2SJwtSigner, S2SJwtSignerOptions, S2STokenManagerConfigError, S2STokenManagerOptions, S2STokenMintError, SignS2SParams, TokenIssuer, buildS2SClient, createClientAuthInterceptor, createS2SJwtSigner, waitForClientReady } from './client/index.js'; import { GatewayHmacSecrets, VerifyGatewayBundleResult } from './gateway-hmac/index.js'; export { CanonicalizeGatewayBundleOptions, GATEWAY_BUNDLE_ORDER, GATEWAY_HMAC_HEADER, GATEWAY_HMAC_ROTATION_GRACE_MS, GatewayBundleIncompleteError, SignGatewayBundleOptions, VerifyGatewayBundleOptions, canonicalizeGatewayBundle, signGatewayBundle, verifyGatewayBundle } from './gateway-hmac/index.js'; import { AsyncLocalStorage } from 'node:async_hooks'; import 'jose'; import '@aws-sdk/client-s3'; import '@grpc/grpc-js'; import '@aws-sdk/credential-provider-node'; import '@aws-sdk/client-kms'; /** * Rotation-aware gateway-HMAC secret provider (D356, amends D192). * * The stateless sign/verify trio in `./index` requires the caller to pass raw * secret bytes. D356 makes that the ONE thing every gateway-plane consumer * (edge + the 7 downstreams) no longer hand-rolls: this provider owns the * secret lifecycle so rotation is uniform + no-redeploy across the platform. * * - FETCH the platform secret `nodii//gateway/hmac_secret` from AWS * Secrets Manager (JSON `{current, previous, rotated_at}`). The producer * (nodii-infra) promotes `previous <- current`, `current <- new` IN the * secret on each rotation, so the fetched value is authoritative — the * provider MIRRORS it (no client-side promotion). * - CACHE current + previous + rotated_at; SIGN with current; VERIFY with * the stateless dual-verify (current, then previous within the 24h grace). * - RE-FETCH on demand via {@link GatewayHmacRotationProvider.onRotationStarted} * — the consumer wires the `gateway.hmac.rotation_started.v1` trigger event * (a trigger only; carries NO key material) to that method with a one-liner. * The event TRANSPORT (Redis Streams / outbox) is consumer-owned, so the * provider does not couple to a specific bus. * - FALLBACK safety net: fetch-on-startup (fail-fast) + a slow background poll * (~10 min) so a consumer that was down during the event still converges. * * R1: the default fetcher hits a REAL AWS Secrets Manager (or a localstack * endpoint in tests). There is NO Noop/in-memory default — a provider that * cannot reach its secret store fails loudly at `start()`. * * Refs: D356 (locked 2026-06-15), D192/D353/D355 (the bundle), requests * bf4874ea (hub) + cf0fff62 (edge). */ /** Default background poll interval (D356 fallback): 10 minutes. */ declare const GATEWAY_HMAC_POLL_INTERVAL_MS: number; /** * The on-the-wire shape of `nodii//gateway/hmac_secret` (D356). Secret * VALUES are strings; `rotated_at` is ISO-8601 (snake_case matches the stored * JSON contract — do NOT camelCase it). */ interface GatewayHmacSecretMaterial { current: string; previous?: string | null; rotated_at?: string | null; } /** * Reads {@link GatewayHmacSecretMaterial} from a backing store. Abstracted so * the provider is unit-testable against a fake without an AWS dependency, while * the shipped default ({@link AwsGatewayHmacSecretFetcher}) hits real Secrets * Manager. */ interface GatewayHmacSecretFetcher { fetch(): Promise; } /** Thrown when the rotation provider is used before {@link GatewayHmacRotationProvider.start}. */ declare class GatewayHmacProviderNotStartedError extends Error { constructor(); } /** Thrown when the fetched secret material is missing the required `current`. */ declare class GatewayHmacSecretMalformedError extends Error { constructor(detail: string); } /** * Storage contract (D356): the gateway HMAC secret material * (`nodii//gateway/hmac_secret` JSON `current`/`previous`) is stored as * **standard base64** of the raw key bytes. That is the SAME encoding the * downstream env verifiers consume — edge's `decodeSecret` * (`Buffer.from(EDGE_GATEWAY_HMAC_SECRET, "base64")`) and auth's gateway-trust * `toLibSecrets` (`Buffer.from(GATEWAY_HMAC_SECRET_CURRENT, "base64")`). So for * ONE stored value the SM-provider path (here) and the env-verifier path derive * IDENTICAL key bytes — the whole point of D356 (uniform secret lifecycle). * * Decode discipline — STRICT-canonical-base64-with-UTF-8-fallback: * 1. If `value` is canonical standard base64 (charset `[A-Za-z0-9+/]` + up to * two `=` pads, length a multiple of 4, and a round-trip re-encode matches * the input byte-for-byte) → return the base64-decoded bytes. * 2. Otherwise (non-base64 ASCII, url-safe `-_`, whitespace, wrong length) → * fall back to UTF-8 bytes. * The round-trip guard mirrors the exact discipline in this package's * `decodeUserActorEnvelope` (charset + `len % 4` guard, `Buffer.from` is lenient * so it must be validated). The UTF-8 fallback keeps a legacy ASCII-in-SM secret * working while the platform migrates to the stored-base64 contract; once the * secret is stored base64 (the contract), both paths agree. */ declare function defaultDecodeSecret(value: string): Uint8Array; interface AwsGatewayHmacSecretFetcherOptions { /** Deployment env → secret id `nodii//gateway/hmac_secret`. */ env: string; region: string; /** Endpoint override for localstack / non-prod AWS-compatible targets. */ endpoint?: string; /** Explicit creds; omit to use the SDK default provider chain. */ credentials?: { accessKeyId: string; secretAccessKey: string; }; /** Full secret id override (defaults to `nodii//gateway/hmac_secret`). */ secretId?: string; } /** * The shipped {@link GatewayHmacSecretFetcher} — reads the gateway HMAC secret * from real AWS Secrets Manager. Mirrors `@nodii/pii`'s AWS client config * (region + optional localstack endpoint + optional explicit creds). */ declare class AwsGatewayHmacSecretFetcher implements GatewayHmacSecretFetcher { private readonly client; private readonly secretId; constructor(opts: AwsGatewayHmacSecretFetcherOptions); fetch(): Promise; } interface GatewayHmacRotationProviderOptions { /** Secret source. Use {@link AwsGatewayHmacSecretFetcher} in production. */ fetcher: GatewayHmacSecretFetcher; /** Background poll interval (missed-event fallback). Default 10 min. */ pollIntervalMs?: number; /** Dual-verify grace window. Default 24h ({@link GATEWAY_HMAC_ROTATION_GRACE_MS}). */ rotationGraceMs?: number; /** SM string secret → bytes. Default {@link defaultDecodeSecret} * (strict-canonical-base64 with UTF-8 fallback; matches the env verifiers). */ decodeSecret?: (value: string) => Uint8Array; /** Sink for non-fatal background poll/rotation fetch failures. */ onError?: (err: unknown, ctx: "poll" | "rotation") => void; } /** * Stateful, rotation-aware gateway-HMAC provider. Lifecycle: * `const p = new GatewayHmacRotationProvider({ fetcher }); await p.start();` * then `p.sign(...)` / `p.verify(...)`, and wire the rotation event: * `onEvent("gateway.hmac.rotation_started.v1", () => p.onRotationStarted())`. * Call `p.stop()` on shutdown to clear the poll timer. */ declare class GatewayHmacRotationProvider { private readonly fetcher; private readonly pollIntervalMs; private readonly rotationGraceMs; private readonly decodeSecret; private readonly onError; private secrets; private timer; constructor(opts: GatewayHmacRotationProviderOptions); /** * Fetch-on-startup (fail-fast — throws if the secret store is unreachable or * the secret is malformed) then arm the background poll. Idempotent: a second * call refreshes + leaves a single timer. */ start(): Promise; /** Stop the background poll. Safe to call repeatedly. */ stop(): void; /** The cached secret set. Throws if not started. */ getSecrets(): GatewayHmacSecrets; /** Sign a bundle with the CURRENT secret (edge always signs current). */ sign(headers: Record, order: string[]): string; /** Dual-verify a bundle against current, then previous within the grace. */ verify(headers: Record, order: string[], signature: string): VerifyGatewayBundleResult; /** * Re-fetch the secret immediately. Wire the consumer's * `gateway.hmac.rotation_started.v1` subscription to this. A failure here is * surfaced via `onError("rotation", ...)` and does NOT throw — the poll + * last-known secrets keep verification working until the next success. */ onRotationStarted(): Promise; private refresh; } /** * Single-use `jti` replay store for the bound gateway-HMAC bundle (D644 § 1). * * D644 chose the STRICTER posture: a per-bundle `jti` nonce that is tracked * server-side with a TTL so a second presentation of the same `jti` is * rejected — replay is defeated even INSIDE the (short) `exp` validity window, * not just after it. That requires an atomic check-and-reserve keyed on `jti`. * * R1 — no Noop/in-memory DEFAULT is wired into a shipped verify path. The * verifier ({@link verifyBoundGatewayBundle}) REQUIRES a store; a service that * turns on bound verification MUST inject a real one. The shipped real * implementation is {@link RedisGatewayBundleReplayStore} (atomic `SET … NX PX` * against the platform Redis every gateway consumer already runs). The * in-memory {@link InMemoryGatewayBundleReplayStore} is a TEST DOUBLE only — * process-local, lost on restart, and never a production default. * * Refs: D644 § 1 (jti single-use nonce), D356 (the secret lifecycle this reuses). */ /** * Atomic reserve-or-reject for a bundle `jti`. * * A single method by design: replay defence needs check-and-set to be ONE * atomic step (a separate "has this jti?" + "record it" races two concurrent * replays through). Implementations MUST make {@link reserve} atomic. */ interface GatewayBundleReplayStore { /** * Reserve `jti` for `ttlMs`. Returns `true` iff `jti` was previously unseen * (it is now reserved and any later `reserve(jti, …)` within the TTL returns * `false`); `false` iff `jti` was already reserved — i.e. a replay. * * `ttlMs` should be the remaining validity of the bundle (time until `exp` * plus the clock-skew allowance): the store only needs to remember a `jti` * for as long as a bundle carrying it could still verify. */ reserve(jti: string, ttlMs: number): Promise; } /** * The minimal Redis client surface {@link RedisGatewayBundleReplayStore} needs * — the `SET key value PX NX` form. Both `ioredis` (`set(k, v, "PX", ms, * "NX")`) and `node-redis` v4 (`set(k, v, { PX: ms, NX: true })`) satisfy one of * the two call shapes below, so the store adapts to either without this package * taking a hard dependency on a specific Redis client. The injected client is * the SAME one the consumer already runs for idempotency / streams. */ interface RedisSetNxPxClient { set(key: string, value: string, ...args: unknown[]): Promise; } interface RedisGatewayBundleReplayStoreOptions { /** An ioredis- or node-redis-v4-compatible client (injected, already connected). */ client: RedisSetNxPxClient; /** * Key namespace. Default `nodii:gw:jti:`. Kept short — one key per live * bundle, each self-expiring at `ttlMs`, so the keyspace is bounded by * (bundle rate × max bundle TTL). At a ~60s exp that is tiny. */ keyPrefix?: string; /** * `node-redis` (v4) uses an options-object `set(k, v, { NX: true, PX: ms })` * and returns `null` when NX fails; `ioredis` uses variadic * `set(k, v, "PX", ms, "NX")` and returns `null` when NX fails. Default * `"ioredis"`. Both return the string `"OK"` on a successful reserve. */ dialect?: "ioredis" | "node-redis"; } /** * The shipped real replay store: an atomic `SET 1 PX NX` * against Redis. `SET … NX` reserves the key iff absent and returns `"OK"`; * a replay finds the key present and gets `null`. `PX` self-expires the key at * the bundle's remaining validity so no sweeper is needed. * * Atomicity is Redis-native (single command), so two concurrent replays of the * same `jti` cannot both win. */ declare class RedisGatewayBundleReplayStore implements GatewayBundleReplayStore { private readonly client; private readonly keyPrefix; private readonly dialect; constructor(opts: RedisGatewayBundleReplayStoreOptions); reserve(jti: string, ttlMs: number): Promise; } /** * TEST DOUBLE ONLY — a process-local {@link GatewayBundleReplayStore}. Reserves * are held in a Map with wall-clock expiry. Never wire this into a shipped * verify path: it is not shared across instances and is lost on restart, so it * cannot actually defeat replay in production (R1). Use * {@link RedisGatewayBundleReplayStore} there; use this in unit tests that * exercise the bound-verify LOGIC without a Redis dependency. */ declare class InMemoryGatewayBundleReplayStore implements GatewayBundleReplayStore { private readonly seen; reserve(jti: string, ttlMs: number): Promise; /** Test helper: drop all reservations. */ clear(): void; } /** * The BOUND gateway-HMAC bundle (D644) — freshness + audience + request * binding layered ON TOP of the D192/D353/D355 principal bundle. * * ## Why this exists * * The principal bundle in `./index` proves WHO the request is (14 signed * principal/operational headers). It carries no freshness, no audience, and no * request binding, so a captured bundle replays fleet-wide until the shared * secret rotates (open drift `14b5f78f`; the auth#298 panel independently found * `GATEWAY_BUNDLE_ORDER` has no timestamp/nonce/expiry and the boundary sets * `iat:0, exp:0`). D644 (locked, STRICTER posture, operator-delegated) fixes * that with a second, bound signature over: * * `jti` (single-use nonce) · `iat`+`nbf`+`exp` (short TTL, skew-bounded) · * `aud` (the specific target service) · `method | path | sha256(body)` * (request binding) · and the principal bundle's own signature (so the * freshness/binding claims are cryptographically tied to THIS principal set — * an attacker cannot splice a fresh bound header onto a different bundle). * * ## Why it is ADDITIVE and inert until an adopter opts in * * `GATEWAY_BUNDLE_ORDER` and the principal `sign/verifyGatewayBundle` are a * byte-for-byte agreement between the edge signer and every verifier; changing * them is a fleet-coordinated re-lock, not a library change (see `./index`). * So this layer adds NOTHING to that canonical. It is a NEW signature over a * NEW claim set, carried in NEW headers ({@link GATEWAY_BOUND_HEADER} + * {@link GATEWAY_BOUND_SIGNATURE_HEADER}). A verifier that does not call * {@link verifyBoundGatewayBundle} is unaffected; the edge does not stamp the * bound headers until it opts in. Shipping this library changes zero runtime * behaviour until the coordinated adopter rollout (D644 § rollout discipline). * * ## Crypto reuse * * The bound signature is computed by the SAME audited primitive as the * principal bundle — {@link signGatewayBundle}/{@link verifyGatewayBundle} — by * canonicalizing the bound claims under a bound-specific order * ({@link BOUND_CLAIM_ORDER}). That inherits, verbatim, the strict * unpadded-base64url handling, constant-time compare, and dual-verify-within- * rotation-grace (D356). No crypto is re-implemented here. * * Refs: D644 (this), D192/D353/D355 (principal bundle), D356 (rotation), * drift `14b5f78f`. */ /** Wire header carrying the bound claims (base64url(JSON)). */ declare const GATEWAY_BOUND_HEADER = "x-nodii-gateway-bound"; /** Wire header carrying the bound signature (unpadded base64url HMAC). */ declare const GATEWAY_BOUND_SIGNATURE_HEADER = "x-nodii-gateway-bound-signature"; /** * Default bound-bundle TTL: 60s (D644 § 2 target, tuned to real gateway→service * latency). Deliberately NOT `exp:0`. */ declare const GATEWAY_BOUND_DEFAULT_TTL_MS = 60000; /** * Maximum accepted bound-bundle TTL (`exp - iat`). A signer that mints a * longer-lived bundle is rejected — the whole point is a tight window, so an * over-long `exp` (a mis-configured or hostile signer) must not widen the * replay surface. Default 5 min. */ declare const GATEWAY_BOUND_MAX_TTL_MS: number; /** * Clock-skew allowance for `nbf`/`exp` (D644 § 3). Applied symmetrically: a * bundle is valid for `[nbf - skew, exp + skew]`. Default 5s. */ declare const GATEWAY_BOUND_DEFAULT_SKEW_MS = 5000; /** * The canonical order of bound claims. Passed as the `order` to the principal * bundle's canonicalizer so the SAME HMAC primitive signs/verifies these. These * are internal canonicalization keys, NOT wire header names (the whole claim set * travels in one {@link GATEWAY_BOUND_HEADER}). Changing this order changes the * bound MAC — a coordinated re-lock like {@link GATEWAY_BUNDLE_ORDER}, but the * bound layer only has one signer and one verifier per request so it is bound to * this library version, not hand-copied across repos. */ declare const BOUND_CLAIM_ORDER: readonly string[]; /** The bound claim set (D644 § 1–5). All times are UNIX milliseconds. */ interface GatewayBoundClaims { /** Single-use nonce (D644 § 1). */ jti: string; /** Issued-at (ms). */ iat: number; /** Not-before (ms). */ nbf: number; /** Expiry (ms). */ exp: number; /** Target service id (D644 § 4). */ aud: string; /** Bound HTTP method, upper-cased (D644 § 5). */ method: string; /** Bound request path (D644 § 5). */ path: string; /** `base64url(sha256(body))` (D644 § 5). */ bodySha256: string; /** * The principal bundle's signature ({@link GATEWAY_HMAC_HEADER} value) — binds * these claims to THAT principal set so a fresh bound header cannot be spliced * onto a different bundle. */ bundleSig: string; } /** `base64url(sha256(bytes))`, unpadded — the body-binding digest (D644 § 5). */ declare function sha256Base64Url(body: Uint8Array): string; /** Encode claims for the wire: base64url(JSON), unpadded. */ declare function encodeBoundClaims(claims: GatewayBoundClaims): string; /** * Strictly decode + validate the wire claim blob. Returns `null` for anything * that is not a well-formed claim object with the exact field types (never * throws on attacker input). */ declare function decodeBoundClaims(blob: string): GatewayBoundClaims | null; interface SignBoundGatewayBundleOptions { /** Target service id → `aud`. */ audience: string; /** HTTP method (case-normalized to upper). */ method: string; /** Request path. */ path: string; /** Raw request body bytes (empty for no-body requests). */ body: Uint8Array; /** The principal bundle signature ({@link GATEWAY_HMAC_HEADER} value). */ bundleSignature: string; /** Raw signing secret bytes (edge signs with `current`). */ secret: Uint8Array; /** Unique nonce for this bundle. MUST be unpredictable (crypto-random). */ jti: string; /** Issued-at (ms). Injectable for tests; defaults to now at call time. */ nowMs: number; /** Validity window (ms). Default {@link GATEWAY_BOUND_DEFAULT_TTL_MS}. */ ttlMs?: number; } interface SignedBoundGatewayBundle { /** Value for {@link GATEWAY_BOUND_HEADER}. */ boundHeader: string; /** Value for {@link GATEWAY_BOUND_SIGNATURE_HEADER}. */ boundSignature: string; /** The claims that were signed (for logging/observability). */ claims: GatewayBoundClaims; } /** * Sign a bound bundle (edge side). Produces the two wire header values the edge * stamps alongside the principal bundle. Does NOT mint the `jti` or read the * clock implicitly — both are passed in so signing is deterministic and * testable; the caller supplies a crypto-random `jti` and `nowMs`. */ declare function signBoundGatewayBundle(opts: SignBoundGatewayBundleOptions): SignedBoundGatewayBundle; type VerifyBoundGatewayBundleReason = "BOUND_MISSING" | "BOUND_MALFORMED" | "BOUND_SIG_MISMATCH" | "BOUND_SIG_BAD_BASE64URL" | "OUT_OF_ROTATION_GRACE" | "WRONG_AUDIENCE" | "NOT_YET_VALID" | "EXPIRED" | "TTL_TOO_LONG" | "BAD_TIME_WINDOW" | "METHOD_MISMATCH" | "PATH_MISMATCH" | "BODY_MISMATCH" | "BUNDLE_SIG_MISMATCH" | "REPLAYED" | "REPLAY_STORE_ERROR"; type VerifyBoundGatewayBundleResult = { ok: true; used: "current" | "previous"; claims: GatewayBoundClaims; } | { ok: false; reason: VerifyBoundGatewayBundleReason; }; interface VerifyBoundGatewayBundleOptions { /** Value received in {@link GATEWAY_BOUND_HEADER}. */ boundHeader: string | undefined; /** Value received in {@link GATEWAY_BOUND_SIGNATURE_HEADER}. */ boundSignature: string | undefined; /** * The principal bundle signature this service ALREADY verified * ({@link GATEWAY_HMAC_HEADER} value). The bound claims must be bound to * exactly this value, or the freshness/binding was minted for a different * principal set. */ principalBundleSignature: string | undefined; /** This service's own id — must equal the bound `aud` (D644 § 4). */ expectedAudience: string; /** Actual request method (case-insensitive compare). */ method: string; /** Actual request path (exact compare). */ path: string; /** Actual request body bytes. */ body: Uint8Array; /** HMAC secrets (current + previous within rotation grace), as principal verify. */ secrets: GatewayHmacSecrets; /** Single-use `jti` store (REQUIRED — no default; R1). */ replayStore: GatewayBundleReplayStore; /** `now` in ms. Injectable for tests; defaults to `Date.now()`. */ nowMs?: number; /** Clock-skew allowance (ms). Default {@link GATEWAY_BOUND_DEFAULT_SKEW_MS}. */ skewMs?: number; /** Max accepted TTL (ms). Default {@link GATEWAY_BOUND_MAX_TTL_MS}. */ maxTtlMs?: number; /** Rotation grace (ms) forwarded to principal verify. Default 24h. */ rotationGraceMs?: number; } /** * Verify a bound bundle (service side). Order of checks is deliberate: cheap, * non-crypto structural + signature checks first; the `jti` reserve LAST, so a * malformed/expired/mis-audienced/replayed-body bundle never consumes a nonce * slot (a reserve is a write). Every failure is returned, never thrown. * * The caller MUST have already verified the PRINCIPAL bundle * (`verifyGatewayBundle`) and pass its signature as `principalBundleSignature`; * this layer binds to it but does not re-verify it. */ declare function verifyBoundGatewayBundle(opts: VerifyBoundGatewayBundleOptions): Promise; /** * Ambient store for the RAW inbound `x-user-context` header value * (base64 `UserActorEnvelope`), used to propagate user identity across * A→B S2S hops (A2 / D454 / drift 0cc7c7af). * * The server auth path (`withAuthUnary` / `withAuthUnaryValue`) wraps the * handler invocation in `userContextStore.run(rawHeader, () => handler(...))` * ONLY when an inbound `x-user-context` header is present. The client auth * interceptor (`createClientAuthInterceptor`) reads * `userContextStore.getStore()` and re-attaches the RAW bytes unchanged onto * the outgoing call — preserving byte-parity + the D454 `on_behalf_of` agent * marker (never re-encoded). * * An `AsyncLocalStorage` (not a module-level mutable) is used because a Node * gRPC server serves concurrent calls on the same event loop: the store scopes * the raw header to the async context of the in-flight handler, so overlapping * requests never leak each other's user context onto outgoing calls. * * SUBPATH-SPLIT SAFETY (request 4f1facd4 class): `@nodii/grpc-auth` ships * SEPARATE tsup subpath bundles (`splitting: false`) — the server path lives in * `/server`, the client interceptor in `/client`. A plain module-level * `new AsyncLocalStorage()` would therefore be DUPLICATED per bundle: the server * would `.run(raw)` on the /server bundle's instance while the client reads a * DIFFERENT /client-bundle instance → `getStore()` always `undefined` → * forwarding SILENTLY dead (exactly the fence-is-dead failure mode A2 closes). * The single instance is therefore held on a `globalThis` slot (same mechanism * as the JWKS resolver / permission cache) so every bundle copy shares it. */ /** * Process-wide ambient store carrying the RAW inbound `x-user-context` * header string for the duration of a handler invocation. `getStore()` is * `undefined` (no active store) outside a handler or for an S2S-only call * with no inbound user context. * * Backed by ONE `globalThis`-slotted `AsyncLocalStorage` so the server * (`/server`) and client (`/client`) subpath bundles share the same instance * within a realm — otherwise the split would silently break forwarding. */ declare const userContextStore: AsyncLocalStorage; /** * Bootstrap-once factory for `@nodii/grpc-auth`. Per * `06-library-integration § 4.3` + `01-communication-doctrine § 13.3`, * every service that exposes or calls gRPC calls this once at process * start; the resulting singleton drives `withAuthUnaryValue`, * `mfaRequiredInterceptor`, `createClientAuthInterceptor`, and the * 13-stack composer in `@nodii/grpc-interceptors`. * * REPLAY ENFORCEMENT IS A DEPRECATED OPT-IN, OFF BY DEFAULT — hub decision * D845 (locked 2026-09-18). Callers cache and reuse one S2S token for its * lifetime, so a receiver that rejects a repeated `jti` cannot be called by * them. `enforceJtiReplay` defaults to `false`, and a `replayStore` reaches the * verifier only when the flag is explicitly `true`. * * R1 (NON-NEGOTIABLE) still holds FOR THE OPT-IN: there is no in-memory * `ReplayStore` default. `enforceJtiReplay: true` without a `replayStore` is a * hard `ConfigError` at boot. * * Idempotency: a second call with the **same** config (deep-equal * shallow on the primitive fields + identity on the injected * resolver/store/manager) is a no-op. A second call with a different * config throws `ConfigError` — services do not "re-bootstrap" * in the middle of a process; that's a sign of a config bug. * * Reset for testing: `resetGrpcAuthConfigForTesting()` is exported but * intentionally NOT re-exported from the package root. Tests import it * via the `./configure` subpath. */ /** * Thrown by `configureGrpcAuth` when the supplied options violate R1 * or when a second call uses a different config than the first. * * Distinguished from `S2SVerifyError` because this is a configuration * error, not a verifier-runtime error — downstream consumers may want * to fail-fast at startup on this class specifically. */ declare class ConfigError extends Error { constructor(message: string); } type ConfigureGrpcAuthOptions = { expectedIssuer: string; expectedAudience: string; jwksResolver: GetKeyFn; /** * Redis-backed (or equivalent) jti-replay store. Used ONLY when * `enforceJtiReplay: true` — and then it is REQUIRED (R1: no in-memory * default). Ignored, with a one-time warning, when the flag is off. */ replayStore?: ReplayStore; /** * DEPRECATED opt-in, default `false` — hub decision D845 (2026-09-18). * Callers cache and reuse one S2S token for its lifetime, so a receiver that * rejects a repeated `jti` cannot be called by them. Enabling this is * incompatible with caching callers (`@nodii/grpc-auth` >= 0.17.0). When * `true`, `replayStore` is required and `configureGrpcAuth` throws without * it. Under this opt-in a token with no `jti` is rejected (`missing_jti`). */ enforceJtiReplay?: boolean; /** * Optional S2S token-issuer manager (client-side). Carried through * the config so consumers can pull it off `getGrpcAuthConfig()` * inside `createClientAuthInterceptor`. */ tokenManager?: S2STokenManager; /** * Per-method required-scope map. Merged with the * `@requirePermission`-populated registry at bootstrap time; * caller-supplied entries WIN on key collision (so consumers can * override a decorator-registered scope from the call site). */ requiredScopesByMethod?: Record; /** * Clock skew tolerance (seconds) passed through to `verifyS2SToken`. * Defaults to 10s. */ maxClockSkewSeconds?: number; /** * Default-DENY for unlisted gRPC methods (request e95b3100, closes the * F1.5 default-ALLOW class — D429). * * When `true` (DEFAULT as of 0.15.0): a method ABSENT from * `requiredScopesByMethod` is REJECTED with `PERMISSION_DENIED` (the message * names the unlisted method). The verifier refuses to serve any RPC that has * not been explicitly bound to a required scope. Listed methods are * scope-checked exactly as before. * * When `false`: a method ABSENT from `requiredScopesByMethod` skips the scope * gate entirely; only token-validity is enforced. * * BREAKING CHANGE IN 0.15.0 — the default flipped from `false` to `true`. * * The old default was the canonical fail-open shape: authorization ran only * when a lookup happened to return something, so forgetting to register a * method did not fail — it silently removed the check. Any holder of ANY valid * S2S token could invoke the unregistered RPC, including a destructive one, * and nothing anywhere reported it. A missing registration is invisible in * exactly the way a missing authorization decision must never be. * * Setting this to `false` re-opens that hole knowingly. It is a legitimate * staged-rollout step (enumerate every served method, then remove the * override) and an illegitimate way to make a failing deploy go away. */ denyUnlistedMethods?: boolean; /** * Globally-configured resolver for handler-side user RBAC * (`checkUserPermission`). Returns the user's EFFECTIVE permission * keys in the given tenant — the consumer resolves * `user -> roles -> permissions` against its own replicas. REQUIRED * for `checkUserPermission` to work; if absent, that call throws * `CheckUserPermissionNotConfigured` (R1: there is no default * resolver). */ userPermissionResolver?: UserPermissionResolver; }; /** Frozen, post-bootstrap config snapshot. */ type GrpcAuthConfig = { expectedIssuer: string; expectedAudience: string; jwksResolver: GetKeyFn; replayStore?: ReplayStore; enforceJtiReplay: boolean; tokenManager?: S2STokenManager; requiredScopesByMethod: Record; maxClockSkewSeconds: number; denyUnlistedMethods: boolean; userPermissionResolver?: UserPermissionResolver; }; declare function configureGrpcAuth(opts: ConfigureGrpcAuthOptions): void; /** * Read the post-bootstrap config. Throws if `configureGrpcAuth` has * not run yet — call sites can either let this propagate (which * crashes the request with a clear message) or catch and surface a * 503-equivalent. Per R1 there's no silent default. */ declare function getGrpcAuthConfig(): GrpcAuthConfig; /** * Non-throwing peek at the post-bootstrap config (D308 / request * f4d47a07). Returns the module singleton, or `null` when * `configureGrpcAuth` has not run yet — it does NOT throw. * * This exists so the pure-functional `checkUserPermission` helper can * fall back to a globally-configured `userPermissionResolver` WITHOUT * forcing the full JWT-verification config (`expectedIssuer` / * `expectedAudience` / `jwksResolver`) to be present. Per-service-auth * consumers that pass the resolver inline never need `configureGrpcAuth` * at all. */ declare function getGrpcAuthConfigOrNull(): GrpcAuthConfig | null; /** * Test-only reset hook. NOT re-exported from the package root; tests * import it via the `@nodii/grpc-auth/configure` subpath (or, in this * monorepo, directly from `./configure`). */ declare function resetGrpcAuthConfigForTesting(): void; export { AwsGatewayHmacSecretFetcher, type AwsGatewayHmacSecretFetcherOptions, BOUND_CLAIM_ORDER, ConfigError, type ConfigureGrpcAuthOptions, GATEWAY_BOUND_DEFAULT_SKEW_MS, GATEWAY_BOUND_DEFAULT_TTL_MS, GATEWAY_BOUND_HEADER, GATEWAY_BOUND_MAX_TTL_MS, GATEWAY_BOUND_SIGNATURE_HEADER, GATEWAY_HMAC_POLL_INTERVAL_MS, type GatewayBoundClaims, type GatewayBundleReplayStore, GatewayHmacProviderNotStartedError, GatewayHmacRotationProvider, type GatewayHmacRotationProviderOptions, type GatewayHmacSecretFetcher, GatewayHmacSecretMalformedError, type GatewayHmacSecretMaterial, GatewayHmacSecrets, GetKeyFn, type GrpcAuthConfig, InMemoryGatewayBundleReplayStore, RedisGatewayBundleReplayStore, type RedisGatewayBundleReplayStoreOptions, type RedisSetNxPxClient, ReplayStore, S2STokenManager, type SignBoundGatewayBundleOptions, type SignedBoundGatewayBundle, UserPermissionResolver, type VerifyBoundGatewayBundleOptions, type VerifyBoundGatewayBundleReason, type VerifyBoundGatewayBundleResult, VerifyGatewayBundleResult, configureGrpcAuth, decodeBoundClaims, defaultDecodeSecret, encodeBoundClaims, getGrpcAuthConfig, getGrpcAuthConfigOrNull, resetGrpcAuthConfigForTesting, sha256Base64Url, signBoundGatewayBundle, userContextStore, verifyBoundGatewayBundle };