# @nodii/grpc-auth

Service-to-service gRPC auth for the Nodii microservice stack.

Provides:

- `createServerAuthInterceptor` — verifies an incoming `Bearer` token from the `authorization` metadata header and attaches an `S2SAuthContext` to the call.
- `createClientAuthInterceptor` — acquires a token via an `S2STokenManager` and attaches it to outgoing calls.
- `S2STokenManager` — caches tokens from a SigV4-signed API Gateway endpoint and reuses each one for its lifetime (hub decision D845). Refreshes early without blocking callers. Every mint is bounded by `mintTimeoutMs` (default 1500 ms): a mint that takes longer fails as a transient `S2STokenMintError`, so a hung issuer cannot hang a caller, and a cached token keeps being served. **Build one per process**: a manager built per call never hits its cache.
- `verifyS2SToken` — JWT verifier with JWKS resolution.
- `createSigV4S3JWKSet` — JWKS resolver that pulls a private `.well-known/jwks.json` from S3 using SigV4 (not anonymous HTTPS).
- `withAuthUnary` + `requireScope` — handler-level wrapper for unary RPC scope enforcement.

## Install

```bash
bun add @nodii/grpc-auth
# peer deps
bun add @grpc/grpc-js jose
```

## Server: verify incoming tokens

```ts
import * as grpc from "@grpc/grpc-js";
import {
  createServerAuthInterceptor,
  createSigV4S3JWKSet,
  setJwksResolver,
} from "@nodii/grpc-auth";

// Configure JWKS source once, at boot.
setJwksResolver(
  createSigV4S3JWKSet({
    region: "ap-south-1",
    bucket: "nucleus-cloud-s2s-auth",
    key: ".well-known/jwks.json",
    cacheMaxAgeMs: 60_000,
    cooldownDurationMs: 5_000,
  }),
);

const server = new grpc.Server({
  interceptors: [
    createServerAuthInterceptor({
      expectedIssuer: "nucleus-s2s-auth",
      expectedAudience: "nucleus-internal",
    }),
  ],
});
```

The interceptor extracts `Bearer <token>`, calls `verifyS2SToken`, and attaches the auth context to the call as `(call as any).auth`. Handlers read it via the same path:

```ts
function handler(call: ServerUnaryCall<Req, Res>, cb: sendUnaryData<Res>) {
  const auth = (call as any).auth as S2SAuthContext | undefined;
  // auth.serviceName, auth.scopes, ...
}
```

> **Note.** `(call as any).auth` is the production-proven pattern from provisioning. A typed `getCallContext(call)` helper is planned for v0.3.0 (lifted from inventory). It will live alongside the legacy reader, not replace it.

## Server: per-handler scope enforcement

```ts
import { withAuthUnary } from "@nodii/grpc-auth";

const REQUIRED_SCOPES_BY_METHOD: Record<string, string> = {
  createCustomer: "provisioning:customers:write",
  getCurrentIpAssignmentsForIp: "provisioning:ip_assignments:read",
};

const wrap = withAuthUnary({
  expectedIssuer: "nucleus-s2s-auth",
  expectedAudience: "nucleus-internal",
  requiredScopesByMethod: REQUIRED_SCOPES_BY_METHOD,
  // Optional: skip auth in local dev. Defaults to false.
  skipAuth: () => process.env.LOCAL === "true",
});

server.addService(MyServiceDefinition, {
  createCustomer: wrap("createCustomer", impl.createCustomer),
  // ...
});
```

## Client: acquire and attach a token

```ts
import {
  S2STokenManager,
  createClientAuthInterceptor,
} from "@nodii/grpc-auth";

const tokenManager = new S2STokenManager({
  apiBaseUrl: process.env.S2S_AUTH_API_BASE!,
  region: process.env.AWS_REGION ?? "ap-south-1",
  refreshSkewSeconds: 30,
  // Bound on ONE mint (issue 816). Default 1500; values above 9000 are clamped to 9000.
  mintTimeoutMs: 1500,
});

const client = new MyGrpcServiceClient(target, credentials, {
  interceptors: [
    createClientAuthInterceptor({
      tokenManager,
      audience: "nucleus-internal",
      scopes: ["flows:write"],
      instanceId: process.env.INSTANCE_ID,
    }),
  ],
});
```

## Versioning notes

`v0.1.0` is a 1:1 lift of provisioning's S2S auth code (production-proven, also running in traffic-logs). Two narrow changes vs the source:

1. The JWKS source is configured via `setJwksResolver(...)` instead of a module-level constant. Provisioning's hardcoded `region: "ap-south-1"`, `bucket: "nucleus-cloud-s2s-auth"`, `key: ".well-known/jwks.json"` move into the consumer's bootstrap.
2. `withAuthUnary` accepts `requiredScopesByMethod` and `skipAuth` as parameters instead of importing them from a service-specific env.

These are dependency-injection refactors, not behavior changes. Verifier semantics (algorithms, clock skew, claim shape, scope parsing), token cache logic, and JWKS refresh+cooldown are unchanged.

Planned follow-ups:

- `v0.2.0` — pluggable test JWKS source (`TEST_S2S_JWKS_PATH` pattern from traffic-logs).
- `v0.3.0` — typed `CallContext` / `getCallContext` / `mutateCallContext` API alongside the legacy `(call as any).auth` reads (pattern from inventory).
- `v0.4.0` — pluggable logger (replaces `console.log` calls; quiet by default).

