import { Interceptor, handleUnaryCall, ServerUnaryCall, sendUnaryData, status, ServiceError } from '@grpc/grpc-js'; import { S as S2SAuthContext, a as ServerUnaryCallWithAuth, b as UserActorEnvelope } from '../auth-context-CQT2iDIf.js'; /** * gRPC server interceptor: verifies the `Bearer` token in the incoming * `authorization` metadata header and attaches an `S2SAuthContext` to * the call. * * Lifted 1:1 from provisioning's * `server/grpc/interceptors/auth/server/auth-server-interceptor.ts`. */ type ServerAuthOptions = { expectedAudience: string; expectedIssuer: string; }; /** * Wiring: * * ```ts * const server = new grpc.Server({ * interceptors: [ * createServerAuthInterceptor({ * expectedIssuer: "nucleus-s2s-auth", * expectedAudience: "nucleus-internal", * }), * ], * }); * * server.addService( * MyServiceDefinition, * wrapUnaryServiceHandlers(new MyServiceImpl(), withLoggingUnary), * ); * ``` */ declare function createServerAuthInterceptor(opts: ServerAuthOptions): Interceptor; declare function requireScope(auth: S2SAuthContext | undefined, scope: string): void; /** * Per-handler auth wrapper for unary RPCs. Verifies the token, attaches * the auth context to the call, and enforces a per-method required-scope * table. * * Lifted from provisioning's * `server/grpc/interceptors/auth/server/withAuthUnary.ts` with two * narrow changes from the source: * * 1. The required-scope table moves from a hardcoded constant to the * `requiredScopesByMethod` parameter. Provisioning's table lives in * its own bootstrap. * 2. The local-dev short-circuit (`if (env.LOCAL) return handler(...)`) * moves to the `skipAuth` parameter — pass `() => process.env.LOCAL === "true"` * to preserve provisioning's behavior. * * Behavior is otherwise identical: same `Bearer` parsing, same call shape, * same error responses, same scope check. */ type WithAuthUnaryOptions = { expectedAudience: string; expectedIssuer: string; /** * Maps RPC method names to a required scope. If a method appears in * this map, the wrapped handler will reject calls whose token lacks * the listed scope with PERMISSION_DENIED. */ requiredScopesByMethod?: Record; /** * If provided and returns true, auth is skipped entirely and the * handler runs as if unauthenticated. Provisioning passes * `() => env.LOCAL` to keep local dev frictionless. */ skipAuth?: () => boolean; /** * Default-DENY for unlisted methods (request e95b3100 / D429). * * OMITTED (the common case) → inherits `configureGrpcAuth`'s * `denyUnlistedMethods`, which DEFAULTS TO `true` as of 0.15.0. Before * 0.15.0 this option defaulted to `false` INDEPENDENTLY of the global * config, so a service that set `denyUnlistedMethods: true` globally still * ran every `withAuthUnary` handler fail-open — the global switch simply did * not reach this code path. Two switches, one of which silently won, is * worse than one; there is now one. * * `false` explicitly → skip the gate for unlisted methods (fail-open, * deliberate). `true` explicitly → reject unlisted methods with * PERMISSION_DENIED naming the method. */ denyUnlistedMethods?: boolean; }; declare function withAuthUnary(opts: WithAuthUnaryOptions): (methodName: string, handler: handleUnaryCall) => (call: ServerUnaryCall, callback: sendUnaryData) => Promise; type UnaryValueHandler = (call: ServerUnaryCallWithAuth) => Promise; type WithAuthUnaryValueOptions = { /** * If provided and returns `true`, auth is skipped entirely. Use ONLY * for explicit health-check / reflection methods. Parity with the * legacy `withAuthUnary({ skipAuth })` escape hatch. */ skipAuth?: () => boolean; }; /** * Wraps a handler with S2S auth verification + per-method scope * enforcement. The returned function is the call-site handler that * the 13-stack composer plugs into slot 3. * * @param methodName - the fully-qualified gRPC method * (`./`); used as the key into * `requiredScopesByMethod`. * @param handler - the value-returning business-logic handler. */ declare function withAuthUnaryValue(methodName: string, handler: UnaryValueHandler, opts?: WithAuthUnaryValueOptions): UnaryValueHandler; type MfaRequiredOptions = { /** * Set of fully-qualified gRPC method names that require fresh MFA. * Match is exact-string against the `methodName` the 13-stack * composer passes through. */ methods: ReadonlySet | readonly string[]; /** * Max age (seconds) of the user JWT's `iat` for the MFA assertion * to count as "fresh". Recommended: 300s (5 min) for high-risk * operations, 3600s (1h) for medium-risk. */ freshnessSeconds: number; /** * Allowed clock-skew tolerance (seconds) for a future-dated `iat`. * An MFA assertion whose `iat` is more than this many seconds AHEAD of * `now` is rejected as `mfa_future_iat` (fail-closed against forged / * future-dated proofs and badly-skewed issuers). Must be a non-negative * integer. Defaults to `DEFAULT_CLOCK_SKEW_SECONDS` (60s). */ clockSkewSeconds?: number; /** * Clock injection (epoch seconds). Defaults to * `Math.floor(Date.now()/1000)`. Tests inject a fixed clock. */ nowFn?: () => number; }; /** Default future-`iat` clock-skew tolerance (seconds). */ declare const DEFAULT_CLOCK_SKEW_SECONDS = 60; /** * Build the MFA-required wrapper. Apply to an inner handler at slot 10 * of the 13-stack composer: * * ```ts * const mfaGate = mfaRequiredInterceptor({ * methods: new Set([ * "billing.InvoiceService/RefundInvoice", * "billing.PaymentMethodService/RemovePaymentMethod", * ]), * freshnessSeconds: 300, * }); * * const wrapped = mfaGate("billing.InvoiceService/RefundInvoice", inner); * ``` */ declare function mfaRequiredInterceptor(opts: MfaRequiredOptions): (methodName: string, handler: UnaryValueHandler) => UnaryValueHandler; /** * `@requirePermission(scope)` — class-method decorator that registers a * `methodName → requiredScope` mapping into a process-local registry. * `configureGrpcAuth(...)` merges this registry into the * `requiredScopesByMethod` config so `withAuthUnaryValue` can enforce * per-method scopes without each call site re-passing the table. * * Per `06-library-integration § 4.3`: * * ```ts * class InvoiceService { * @requirePermission("billing.invoice.create") * CreateInvoice = withAuthUnaryValue("billing.InvoiceService/CreateInvoice", ...); * } * ``` * * Implementation uses ES2022 modern decorators (TypeScript 5.0+ / * stage-3 proposal). The decorator runs at class-evaluation time and * reads `context.name` for the method name (or the property name for * field decorators) — that becomes the registry key. The decorator * does NOT modify the descriptor; it's a side-effect registration. * * If the consumer's method name is NOT the fully-qualified gRPC * method (`./`), use the imperative form: * * ```ts * registerRequiredScope("billing.InvoiceService/CreateInvoice", "billing.invoice.create"); * ``` * * Conflicting registrations (same key, different scope) throw at load * time — that's a programmer bug. Idempotent re-registration (same * key + same scope) is a no-op. */ /** * Imperative form. Use when the JS method/property name does NOT match * the gRPC fully-qualified method name (which is the common case for * services exposing gRPC). */ declare function registerRequiredScope(methodName: string, scope: string): void; /** * Modern decorator (`@requirePermission(scope)`). Works as a method or * class-field decorator. The registry key is `String(context.name)` — * which is the JS property name. If you need the fully-qualified gRPC * method name (`Service/Method`), prefer `registerRequiredScope` * directly (the gRPC method-name format contains `/` which is not a * valid JS identifier). */ declare function requirePermission(scope: string): (_target: unknown, context: ClassMethodDecoratorContext | ClassFieldDecoratorContext | { name: string | symbol; kind?: string; }) => void; /** Read-only snapshot of the registry. */ declare function getRequiredScopesRegistry(): Record; /** Test-only — wipe the registry between cases. */ declare function resetRequiredScopesRegistryForTesting(): void; /** * Resolver the consumer registers at bootstrap. Returns the user's * EFFECTIVE permission keys in the given tenant (the consumer resolves * `user -> roles -> permissions` against its own RBAC replicas). Mirrors * `@nodii/approval`'s `MembershipRolesLookup` * (`ts/approval/src/types.ts:230`). */ type UserPermissionResolver = (userId: string, tenantId: string) => Promise; /** Options for {@link checkUserPermission}. */ type CheckUserPermissionOptions = { /** * TTL (seconds) for a resolved permission set in the in-memory cache. * Defaults to 30. Because `permissions_digest` is part of the cache * key, a permission change (new digest) is automatically a cache miss * regardless of TTL. */ cacheTtlSec?: number; /** * INLINE resolver (D308 / request f4d47a07). Same type as the global * `configureGrpcAuth({ userPermissionResolver })`: * `(userId, tenantId) => Promise` returning the * user's effective permissions. When supplied, this WINS over the * global resolver and `checkUserPermission` runs WITHOUT any * `configureGrpcAuth` call — keeping it a pure-functional API over an * already-verified envelope (no JWT-verify config required). */ resolver?: UserPermissionResolver; }; /** Result of a {@link checkUserPermission} call. */ type CheckUserPermissionResult = { /** `true` iff `permissionKey` is in the resolved permission set. */ granted: boolean; /** The full effective permission set resolved for the user/tenant. */ resolvedPermissions: string[]; /** When the resolved set was produced (cache-entry creation time). */ asOf: Date; }; /** * Thrown when `checkUserPermission` is called but no * `userPermissionResolver` was supplied by EITHER path: inline via * `opts.resolver`, or globally via * `configureGrpcAuth({ userPermissionResolver })`. R1: there is no * default resolver — the consumer is the injection point for its own * RBAC store. */ declare class CheckUserPermissionNotConfigured extends Error { constructor(); } /** * Thrown when the actor envelope is malformed (missing `user_id` or * `permissions_digest`). */ declare class CheckUserPermissionValidationError extends Error { constructor(message: string); } /** * Thrown when the digest of the freshly-resolved permission set does NOT * match the digest the user JWT carried — the JWT is stale and the user * must re-authenticate / re-issue. Error `name` is the canonical D308 * token `PERMISSION_DIGEST_MISMATCH`. */ declare class PermissionDigestMismatch extends Error { /** Digest carried by the (stale) actor envelope / JWT. */ readonly actorDigest: string; /** Digest recomputed from the freshly-resolved permission set. */ readonly resolvedDigest: string; constructor(actorDigest: string, resolvedDigest: string); } /** * Compute the D308 canonical permissions digest: sha256 hex of the * COMPACT `JSON.stringify` of the lexicographically-sorted permission * list, truncated to the first 32 hex chars. * * Pure + deterministic. Byte-parity anchor: * `computePermissionsDigest(["audit:chain:read","audit:chain:acknowledge"])` * === `"71cb5ce51e6479ef13a1b91cb16a5dbe"`. */ declare function computePermissionsDigest(permissions: readonly string[]): string; /** * Test-only reset hook. Wipes the process-global permission cache between * cases. NOT a production code path. */ declare function resetCheckUserPermissionCacheForTesting(): void; /** * Resolve whether the user described by `actor` holds `permissionKey` in * `tenantId`. * * Flow: * 1. Validate the actor has a non-empty `user_id` + `permissions_digest`. * 2. Cache lookup by `user_id:tenantId:permissions_digest`. On a * non-expired hit, reuse the cached `{ permissions, asOf }` WITHOUT * calling the resolver again. * 3. On a miss, resolve the `userPermissionResolver` — INLINE * `opts.resolver` WINS, else fall back to the global resolver via * the NON-throwing `getGrpcAuthConfigOrNull()` peek (throwing * `CheckUserPermissionNotConfigured` only if NEITHER is present), * resolve the permissions, recompute the digest, and reject with * `PermissionDigestMismatch` if it differs from the actor's digest * (stale JWT). Store the result with `expiresAt = now + ttl`. * 4. `granted = permissions.includes(permissionKey)`. * * @param actor the decoded `UserActorEnvelope` v2. * @param permissionKey the permission to check for (e.g. * `"audit:chain:read"`). * @param tenantId the tenant the check is scoped to. * @param opts optional cache TTL override + inline resolver. */ declare function checkUserPermission(actor: UserActorEnvelope, permissionKey: string, tenantId: string, opts?: CheckUserPermissionOptions): Promise; /** * Helper that produces a `ServiceError`-shaped object suitable for * passing to `sendUnaryData` callbacks. Lifted from provisioning's * `server/grpc/utils/grpcErrors.ts`. */ declare function grpcError(code: status, message: string, details?: string): ServiceError; export { CheckUserPermissionNotConfigured, type CheckUserPermissionOptions, type CheckUserPermissionResult, CheckUserPermissionValidationError, DEFAULT_CLOCK_SKEW_SECONDS, type MfaRequiredOptions, PermissionDigestMismatch, type ServerAuthOptions, type UnaryValueHandler, type UserPermissionResolver, type WithAuthUnaryOptions, type WithAuthUnaryValueOptions, checkUserPermission, computePermissionsDigest, createServerAuthInterceptor, getRequiredScopesRegistry, grpcError, mfaRequiredInterceptor, registerRequiredScope, requirePermission, requireScope, resetCheckUserPermissionCacheForTesting, resetRequiredScopesRegistryForTesting, withAuthUnary, withAuthUnaryValue };