/** * UserActorEnvelope v2 — the `x-user-context` metadata envelope carried * across the S2S boundary per D454 + `01-communication-doctrine § 6`. * * grpc-auth is the FOUNDATION lib (both `@nodii/auth-sdk` and * `@nodii/grpc-interceptors` depend on it), so the canonical envelope * type + wire codec live HERE. The edge stamps this exact wire shape and * `@nodii/grpc-interceptors` `enrichAuthContext` reads it. * * WIRE SHAPE = snake_case JSON, base64-encoded, in the `x-user-context` * gRPC metadata header — byte-identical to the edge producer * (`buildUserActorEnvelope`) + the `@nodii/auth-sdk` codec. (Prior to * D454 this type was camelCase — a stale divergence from the actual wire * that left `enrichAuthContext`'s snake_case `tenant_id` read blind; * reconciled here.) * * DIGEST-ONLY (D425 §5.4): the envelope carries `permissions_digest` (a * PROOF, not an authorization grant) — NEVER a literal `permissions[]`. * Sanctioned uses: audit attribution, MFA-freshness, RLS-pool / tenant * selection, and the confused-deputy cross-tenant fence (D454). */ interface UserActorEnvelope { /** Opaque user id (uuid string) — the effective principal. */ user_id: string; /** Tenant id this principal is acting within (`null` = tenant-less). */ tenant_id: string | null; /** Role stamp (D425/D427); one of {@link USER_ROLES}. */ user_role: UserRole; /** * sha256 of the principal's permission set — a PROOF used to detect * stale-grant races (re-fetch when it differs from the local cache), * NEVER an authorization grant (D425 digest-only). */ permissions_digest: string; /** Authentication Method References (RFC 8176); MFA-freshness input. */ amr: string[]; /** Epoch seconds the user JWT was issued — basis for MFA freshness. */ iat: number; /** jti of the user JWT (NOT the S2S jti) — revocation traceability. */ user_jwt_jti: string; /** Opaque request id that originated the chain (first-hop middleware). */ source_request_id: string; /** * D454 agent-actor marker (OPTIONAL — accommodation for the pipeline * agent vectors 1 & 2: an agent acting on behalf of a customer / user, * inheriting the operator's permissions). Absent for direct human / * customer traffic. The effective principal's `tenant_id` / * `permissions_digest` above are UNCHANGED (inherited); this only * records that an agent is the actual driver, so downstream authz + * audit can distinguish "agent acting as X" from "X directly". * PRODUCED by nodii-auth-service; libs decode + relay it unchanged. */ on_behalf_of?: OnBehalfOf; } /** D454 agent-actor "acting on behalf of" marker. */ interface OnBehalfOf { /** The underlying principal the agent inherits from. */ principal_kind: "customer" | "user"; /** The agent's own opaque id (audit attribution). */ agent_id: string; } /** * Canonical `UserRole` values (D425/D427). Parity with `@nodii/auth-sdk` * `USER_ROLES` + Python / Go. `platform_admin` MUST be accepted (the * rewritten s2s admin gate stamps on it). */ type UserRole = "tenant_user" | "tenant_customer" | "platform_admin"; /** All canonical `UserRole` values, for runtime validation. */ declare const USER_ROLES: readonly UserRole[]; /** Narrowing type-guard for a `user_role` string (D425). */ declare function isUserRole(value: string): value is UserRole; /** * The gRPC metadata header carrying the base64-encoded `UserActorEnvelope`. * Exported so producers + consumers reference ONE canonical header name * instead of hand-typing the literal (header-name drift). */ declare const X_USER_CONTEXT_HEADER = "x-user-context"; /** Thrown when an inbound `x-user-context` header is malformed / invalid. */ declare class UserActorEnvelopeError extends Error { constructor(message: string); } /** * ACCEPT a decoded v2 `UserActorEnvelope` object (validation + narrowing). * Throws {@link UserActorEnvelopeError} for an unknown `user_role`. */ declare function acceptUserActorEnvelope(raw: { user_id: string; tenant_id: string | null; user_role: string; permissions_digest: string; amr?: string[]; iat?: number; user_jwt_jti?: string; source_request_id?: string; on_behalf_of?: OnBehalfOf; }): UserActorEnvelope; /** * Canonical PRODUCER-side encoder: `base64(JSON(envelope))` in FIXED field * order for byte-parity with the Python (`encode_user_actor_envelope`) + Go * (`EncodeUserActorEnvelope`) encoders + the edge producer. Standard (not * URL-safe) base64. `on_behalf_of` is emitted only when present (byte-parity * for the common non-agent envelope). */ declare function encodeUserActorEnvelope(env: UserActorEnvelope): string; /** * Canonical CONSUMER-side decoder: the inverse of * {@link encodeUserActorEnvelope}. base64-decodes + `JSON.parse`s the * `x-user-context` header value, then validates via * {@link acceptUserActorEnvelope}. Malformed base64 / JSON / non-object / * unknown-role all throw {@link UserActorEnvelopeError} — so a consumer's * `catch (e) { if (e instanceof UserActorEnvelopeError) ... }` handles * adversarial header input uniformly across TS / Python / Go. */ declare function decodeUserActorEnvelope(headerValue: string): UserActorEnvelope; /** * What `verifyS2SToken` produces and what server-side handlers read * off the call object after the auth interceptor runs. * * Lifted from provisioning's `server/grpc/models/AuthContext.ts`. The * optional `userActor` field carries the UserActorEnvelope v2 that the * upstream `enrichAuthContext` interceptor (in the 13-stack) decodes * from the `x-user-context` metadata header — required for the MFA * freshness check in `mfaRequiredInterceptor`. S2S-only calls (e.g. * worker → service) leave `userActor` undefined. */ type S2SAuthContext = { serviceId: string; serviceName: string; scopes: string[]; instanceId?: string; userActor?: UserActorEnvelope; }; /** * Augmentation type for `ServerUnaryCall` instances that have passed * through the auth interceptor or `withAuthUnary` wrapper. Use it as a * cast target where TypeScript types can't see the runtime mutation: * * ```ts * function handler(call: ServerUnaryCall, cb) { * const auth = (call as ServerUnaryCallWithAuth).auth; * } * ``` */ type ServerUnaryCallWithAuth = { auth?: S2SAuthContext; metadata: { get(key: string): unknown[]; }; } & Record & { request: TReq; response?: TRes; }; export { type OnBehalfOf as O, type S2SAuthContext as S, USER_ROLES as U, X_USER_CONTEXT_HEADER as X, type ServerUnaryCallWithAuth as a, type UserActorEnvelope as b, UserActorEnvelopeError as c, type UserRole as d, acceptUserActorEnvelope as e, decodeUserActorEnvelope as f, encodeUserActorEnvelope as g, isUserRole as i };