# Authentication

**One env var and five endpoints separate a scaffolded workspace from a working OAuth sign-in — and the boundaries fail loud with typed errors instead of degrading to a silent anonymous session.** Auth is the part of a backend where "compiles and demos fine" and "actually holds" diverge most easily, so NetScript puts the conventions in the contract rather than in notes an agent has to remember to apply.

NetScript ships authentication as a first-class official plugin: `netscript plugin install @netscript/plugin-auth` adds an `auth-api` oRPC service that boots on port **8094** alongside your workers, sagas, and triggers. The plugin is **pure-backend** — it owns the session lifecycle, OAuth/OIDC redirect flow, and a stable five-endpoint REST/RPC surface, but it does **not** ship UI. You compose exactly **one active backend** (single-active-backend), chosen by an environment variable, and the service exposes the same contract no matter which backend is wired in.

alpha

![A browser sign-in request enters the auth-api service, which resolves the single active backend from an env var; the kv-oauth backend redirects to the OAuth/OIDC provider, handles the callback, mints a normalized session in Deno KV, sets the __Host-ns_session cookie, and maps the session to a NetScript Principal that downstream services trust.](https://rickylabs.github.io/netscript/netscript/assets/diagrams/auth-flow.svg)

*Auth flow: browser → auth-api → resolved backend → provider redirect/callback → session store → __Host-ns_session cookie → Principal.*

The design follows the framework's contracts-first doctrine: a core seam package (`@netscript/plugin-auth-core`) defines the `AuthBackendPort`, three interchangeable adapter packages implement it, and the `@netscript/plugin-auth` plugin composes the selected one into a running service. Of the three adapters, only the KV-OAuth backend implements the full **interactive** sign-in/callback flow today; the WorkOS and better-auth adapters are non-interactive by design.

> Use this when
>
> Reach for the auth plugin when you need
>
> session-based authentication backed by an OAuth or OIDC provider
>
> — a typed
>
> /api/v1/auth/*
>
> surface your services and Fresh UI can call to sign users in, resolve the current session, and sign out. The default
>
> kv-oauth
>
> backend gives you a complete interactive redirect flow with KV-stored sessions and an opaque HMAC-signed session token. To wire it into a workspace step by step, see
>
> Add authentication
>
> ; to understand the pure-backend port model and why only one backend is active, see
>
> The authentication model
>
> .

## Where agent-built auth goes wrong

Auth is a security-sensitive surface, and security-sensitive surfaces have a specific build-efficiency failure shape: the mistake does not stop the build. A missing job queue fails a request; a mis-wired sign-in flow ships. Supabase's published agent-skills guidance names the pattern for its own platform — agents building on it skip RLS policies, hallucinate CLI commands, and create views without `security_invoker = true` — and its remedy is a skill: reference material the agent loads so it applies the conventions at the right moment. NetScript takes the other route for the same problem: move the conventions out of the agent's working memory and into the shipped contract, so the common wrong turns either don't type-check, don't configure, or fail loud at runtime with a typed error.

Concretely, the contract closes off the classic failure modes:

- **The silent anonymous fallback.** An agent wiring auth by hand tends to catch the failure and degrade to an unauthenticated default. `backend.authenticate()` cannot express that: it returns a typed `AuthnResult` — `{ ok: true, principal }` or `{ ok: false, reason }` — never a silent anonymous principal.
- **Calling a capability the backend doesn't have.** Asking a non-interactive backend (WorkOS, better-auth) to run the interactive sign-in returns a typed `AUTH_PROVIDER_ERROR`, and direct session mutations throw `AuthBackendOperationUnsupportedError` — a capability boundary, not a no-op that looks like success.
- **Two half-wired providers.** There is nothing to reconcile: `NETSCRIPT_AUTH_BACKEND` selects exactly one active backend (single-active-backend), and every backend normalizes to the same `Principal`, so downstream authorization code is identical regardless of provider.
- **Hand-rolled session cookies.** The session cookie is `__Host-ns_session`; the `__Host-` prefix makes the browser itself refuse a cookie with a `Domain`, a non-root `Path`, or a plain-HTTP origin — the misconfiguration fails to set rather than silently weakening.
- **Logging raw subjects.** The audit surface (`createAuthTelemetry`) redacts by construction — subject hashes, stripped token-bearing claims — and runs as a no-op until a salt is configured, because an un-salted hash would be worse than nothing.

Each item above is the shipped behavior of the published packages, documented mechanism-by-mechanism in the sections below — start with

[the authentication model](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/) for why the seam is shaped this way.

## What it is

Auth is a **port-and-adapter seam, not a monolith**. The core package `@netscript/plugin-auth-core` is contract-only: it defines the `AuthBackendPort` (and the four sub-ports it composes — provider registry, session store, token crypto, and principal mapper), the normalized `AuthSession` and `Principal` shapes, the `AUTH_SESSION_STATES`, the auth oRPC contract, the config schemas, and the stream-event schemas. It contains **no provider SDK code**. Each backend adapter (`@netscript/auth-kv-oauth`, `@netscript/auth-workos`, `@netscript/auth-better-auth`) implements `AuthBackendPort` against a concrete identity provider, and the `@netscript/plugin-auth` plugin selects exactly one of them at boot via `NETSCRIPT_AUTH_BACKEND` and serves it as the `auth-api` service. Because every backend produces the same `Principal`, downstream services authorize identically regardless of which provider is wired in. The full rationale — why auth is pure-backend, what the single-active boundary buys you, and how `InteractiveFlowPort` gates the redirect flow — is in

[The authentication model](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/) .

## Learn → / Do →

[Do — Add authentication  Task recipe: add the auth plugin, set NETSCRIPT_AUTH_BACKEND, run the auth.prisma migration, and wire kv-oauth with a provider preset.](https://rickylabs.github.io/netscript/netscript/identity-access/how-to/add-authentication/)

[Understand — The authentication model  Why auth is pure-backend: the AuthBackendPort seam, the single-active-backend boundary, InteractiveFlowPort, and the three adapters' capability matrices.](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/)

[Compose — Service authn / authz  Gate a service's own routes with the provider-agnostic /auth middleware seam — verify a credential or trust a header, complementary to this plugin.](https://rickylabs.github.io/netscript/netscript/services-sdk/services/)

## Add it in two shapes

The fastest path is the **provider preset**: add the plugin, set `NETSCRIPT_AUTH_BACKEND` (it already defaults to `kv-oauth`), and hand a preset like `providers.google({...})` to `createKvOAuthBackend`. The **advanced** shape uses the same factory but defines a custom OIDC provider explicitly with `defineOAuthProvider` when your identity provider is not one of the fourteen built-in presets.

```ts
// Add the plugin from the workspace root:
//   netscript plugin install @netscript/plugin-auth
// Default backend is already kv-oauth (NETSCRIPT_AUTH_BACKEND=kv-oauth).

import { createKvOAuthBackend, getRequiredEnv, providers } from '@netscript/auth-kv-oauth';

// One of 14 presets: github, google, gitlab, discord, slack, spotify,
// facebook, twitter, auth0, okta, awsCognito, azureAd, logto, clerk.
export const backend = await createKvOAuthBackend({
  provider: providers.google({
    clientId: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_ID'),
    clientSecret: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_SECRET'),
    redirectUri: getRequiredEnv('NETSCRIPT_AUTH_REDIRECT_URI'),
  }),
});

// backend.name === 'kv-oauth' — the only adapter with backend.interactive.
```

```ts
// Same factory, but define the OIDC provider yourself when your IdP
// is not one of the built-in presets.
import { createKvOAuthBackend, defineOAuthProvider, getRequiredEnv } from '@netscript/auth-kv-oauth';

const provider = defineOAuthProvider({
  id: 'my-idp',
  clientId: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_ID'),
  clientSecret: getRequiredEnv('NETSCRIPT_AUTH_CLIENT_SECRET'),
  authorizationEndpoint: getRequiredEnv('NETSCRIPT_AUTH_AUTHORIZATION_ENDPOINT'),
  tokenEndpoint: getRequiredEnv('NETSCRIPT_AUTH_TOKEN_ENDPOINT'),
  userInfoEndpoint: getRequiredEnv('NETSCRIPT_AUTH_USERINFO_ENDPOINT'),
  redirectUri: getRequiredEnv('NETSCRIPT_AUTH_REDIRECT_URI'),
  scopes: ['openid', 'profile', 'email'],
});

export const backend = await createKvOAuthBackend({ provider });
```

> Alpha package pins
>
> The scaffold pins
>
> jsr:@netscript/plugin-auth-core@0.0.1-beta.11
>
> and siblings at the exact aligned NetScript version. Add auth through
>
> netscript plugin install @netscript/plugin-auth
>
> , which wires the workspace correctly for the alpha instead of hand-editing imports.

## Sign in, resolve the session, sign out

Once the plugin is wired, your front end or a typed client drives the five-endpoint surface. A sign-in begins the redirect, the provider returns to your callback, the service mints a session and sets the cookie, and every subsequent request resolves the current session from that cookie. The snippet below is a copy-ready client flow against the REST surface.

```ts
// app/sign-in.ts — drive the auth-api REST surface from the browser
const AUTH = 'http://localhost:8094/api/v1/auth';

// 1. Begin sign-in. The kv-oauth backend responds with a provider redirect;
//    follow it to authenticate with the IdP.
location.href = `${AUTH}/signin`;

// 2. The IdP redirects back to /api/v1/auth/callback, which mints the
//    session and sets the __Host-ns_session cookie automatically.

// 3. Resolve the current session on any later request (cookie is sent
//    automatically; the response is { authenticated, user, session }).
const me = await fetch(`${AUTH}/me`, { credentials: 'include' })
  .then((r) => r.json());
if (me.authenticated) {
  console.log('signed in as', me.user.subject, '— state', me.session.state);
}

// 4. Sign out — revokes the session and clears the cookie.
await fetch(`${AUTH}/signout`, { method: 'POST', credentials: 'include' });
```

```ts
// server/auth-guard.ts — use the resolved backend directly in app code
import { AuthBackendOperationUnsupportedError } from '@netscript/plugin-auth-core/ports';
import { backend } from './backend.ts';

// Resolve the current session from an incoming Request via the session store.
const session = await backend.sessions.getSession({ request: incoming });
if (session?.state === 'active') {
  // Map the normalized session to a NetScript Principal for authorization.
  const { principal } = backend.principalMapper.mapSessionToPrincipal(session);
  console.log(principal.subject, principal.scopes, principal.roles);
}

// Interactive sign-in is optional capability — guard before you call it.
if (!backend.interactive) {
  throw new AuthBackendOperationUnsupportedError(
    backend.name,
    'interactive.signIn',
    'The active backend authenticates sessions only.',
  );
}
const redirect = await backend.interactive.signIn(incoming);
```

## Key types first — the core auth contract

Every backend normalizes to the same two shapes the rest of NetScript depends on: an `AuthSession` (what the store persists) and a `Principal` (what services authorize against). The `AuthSession` is mapped to a `Principal` by the backend's `principalMapper`, and the `authenticate()` method on every backend returns an `AuthnResult`. These are the contract-only types from `@netscript/plugin-auth-core` — confirmed against the live export surface.

**Principal (@netscript/plugin-auth-core) — the identity services authorize against**

| Name | Type | Description |
| --- | --- | --- |
| `subject` | `string` | Stable subject identifier — a user id, service id, or API-key id. |
| `scopes` | `readonly string[]` | Granted scopes for per-operation RPC/REST permission checks. |
| `roles` | `readonly string[]` | Granted roles for role-based access checks. |
| `scheme` | `'api-key' \| 'bearer' \| 'trusted-header' \| 'custom'` | How the principal was established. Auth-plugin backends use 'custom' with the claim bag below. |
| `claims` | `Readonly>` | Opaque verified claims — organization/tenant id, session id, provider permissions, or normalized WorkOS/better-auth metadata. |

**AuthSession (@netscript/plugin-auth-core) — the normalized session the store persists**

| Name | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable session id; the value an opaque session token resolves to. |
| `userId / subject` | `string` | User id and stable subject the session belongs to. |
| `state` | `AuthSessionState` | Lifecycle state: 'active' \| 'expired' \| 'revoked' (from AUTH_SESSION_STATES). |
| `scopes / roles` | `readonly string[]` | Scopes and roles carried into the mapped Principal. |
| `claims` | `Readonly>` | Verified provider claims preserved on the session. |
| `issuedAt / expiresAt` | `string (ISO)` | Issue and expiry timestamps; refreshedAt / revokedAt are set on those transitions. |
| `accountId / providerId` | `string?` | Optional linked provider account and provider id. |
| `traceparent / tracestate` | `string?` | Optional W3C trace context carried for audit correlation. |

**AuthnResult (@netscript/plugin-auth-core) — what backend.authenticate() returns**

| Name | Type | Description |
| --- | --- | --- |
| `{ ok: true, principal }` | `success` | A resolved Principal; optional responseHeaders and setCookies the service flushes to the client. |
| `{ ok: false, reason }` | `rejection` | A typed rejection reason — fail-loud, never a silent anonymous principal. |

## The service surface

The plugin's service is named `auth-api` and is built with `@netscript/service`'s oRPC builder (`createService(...).withRPC()`), not raw Hono. It mounts a public REST surface at `/api/v1/auth/*` and the equivalent oRPC surface at `/api/rpc/v1/auth/*`, plus standard `/health/live` and `/health/ready` probes.

**auth-api endpoints (REST at /api/v1/auth/*, oRPC at /api/rpc/v1/auth/*)**

| Name | Type | Description |
| --- | --- | --- |
| `signin` | `POST` | Begins the interactive sign-in. Requires backend.interactive; on WorkOS / better-auth it returns AUTH_PROVIDER_ERROR (502) because those backends are non-interactive. |
| `callback` | `POST` | Completes the OAuth/OIDC redirect, mints the session, sets the session cookie. Interactive-only — same non-interactive caveat as signin. |
| `signout` | `POST` | Revokes the current session and clears the cookie. |
| `session` | `GET` | Resolves the current AuthSession from the cookie (active \| expired \| revoked), refreshing on read when policy allows. |
| `me` | `GET` | Returns { authenticated: true, user, session } when a valid active session exists, or { authenticated: false } (HTTP 200) when there is none. |

> Backend selection is one env var
>
> The service picks
>
> one
>
> active backend from
>
> NETSCRIPT_AUTH_BACKEND
>
> (or appsettings
>
> auth.backend
>
> ): valid values are
>
> kv-oauth
>
> |
>
> workos
>
> |
>
> better-auth
>
> , defaulting to
>
> `kv-oauth`
>
> . This is the
>
> single-active-backend
>
> boundary — there is no multi-active routing, cross-backend account linking, or global logout across backends in v1. Switching providers means changing the env var and the backend's credentials, not running two backends at once.

## The three backends

All three implement the same `AuthBackendPort`, so the `auth-api` contract is identical across them. They differ in one decisive capability: whether they expose an interactive sign-in/callback flow. Each ships a single factory that returns an `AuthBackendPort`.

**Backend adapters: factory, capability, and what each one needs**

| Name | Type | Description |
| --- | --- | --- |
| `@netscript/auth-kv-oauth` | `createKvOAuthBackend(options)` | Interactive (default). Full OAuth/OIDC redirect flow, Deno KV session store, real createSession / refreshSession / revokeSession, refresh-on-read (refreshMode / refreshSkewMs), opaque session token, __Host-ns_session cookie, AES-256-GCM token-at-rest. Needs a provider preset or defineOAuthProvider config. |
| `@netscript/auth-workos` | `createWorkosBackend({ workos, cookiePassword })` | Non-interactive. WorkOS AuthKit sealed sessions, stateless verification. Needs a WorkOS SDK client and a cookie password. signin / callback return AUTH_PROVIDER_ERROR; session mutations throw AuthBackendOperationUnsupportedError. |
| `@netscript/auth-better-auth` | `createBetterAuthBackend({ auth, sessionTokenSecret })` | Non-interactive. Validates externally-issued sessions via auth.api.getSession over a Prisma store (createNetscriptBetterAuth wires the better-auth Prisma adapter). Needs a better-auth instance and a token secret. |

> Only kv-oauth is interactive
>
> The
>
> signin
>
> and
>
> callback
>
> endpoints require a backend that implements the optional
>
> InteractiveFlowPort
>
> (
>
> signIn
>
> /
>
> handleCallback
>
> /
>
> getSessionId
>
> /
>
> signOut
>
> ). Today that is
>
> only `kv-oauth`
>
> . On
>
> workos
>
> and
>
> better-auth
>
> those two endpoints return a typed
>
> AUTH_PROVIDER_ERROR
>
> ("does not expose an interactive flow"), and direct session mutations throw
>
> AuthBackendOperationUnsupportedError
>
> — a deliberate, fail-loud capability boundary rather than a silent no-op. Use those adapters when sign-in is handled by WorkOS or an external better-auth deployment and NetScript only verifies the resulting session.

## How auth integrates with services

The auth **plugin** (this page) signs human users in and resolves their sessions. A NetScript **service** gates its own routes with the separate, provider-agnostic `@netscript/service/auth` seam — `.withAuthn()` resolves a `Principal` and `.withAuthz()` makes an authorization decision from it. Both layers speak the same `Principal` type, so they compose: the auth plugin establishes identity, and a service's `.withAuthn({ authenticator })` can trust a backend's authenticator to turn a request into that `Principal`. By default a service protects `/api` and leaves `/health` anonymous. The full builder seam — `createStaticCredentialAuthenticator`, `createTrustedHeaderAuthenticator`, `createScopeAuthorizer`, and the preset `defineService(router, { auth: { authn, authz } })` form — is documented on the services hub.

> Plugin vs. service-auth seam
>
> The
>
> auth plugin
>
> owns interactive sign-in, OAuth callbacks, and session cookies. The
>
> [service-auth seam](https://rickylabs.github.io/netscript/services-sdk/services/)
>
> is for machine-to-machine and gateway-fronted gating — static credentials, trusted upstream headers, scope checks — without an interactive identity provider. They are complementary and can run together: sign users in with the plugin, then gate individual service routes with
>
> .withAuthn()
>
> /
>
> .withAuthz()
>
> . See
>
> [Services & contracts](https://rickylabs.github.io/netscript/netscript/services-sdk/services/) .

## Database & runtime events

The plugin contributes a package-provided `auth.prisma` schema with four better-auth-shaped models — `User` → `auth_users`, `Session` → `auth_sessions`, `Account` → `auth_accounts`, `Verification` → `auth_verifications`. These tables back the better-auth adapter; `kv-oauth` keeps its sessions in Deno KV, and WorkOS is stateless. As with every plugin schema, you bring the tables to life by running the database workflow **after Aspire is up** — see [Database migrations](https://rickylabs.github.io/netscript/data-persistence/how-to/database-migration/).

The plugin also emits five durable `auth.*` runtime events through the durable-streams runtime — the `AUTH_STREAM_EVENT_TYPES`: `auth.signin.started`, `auth.signin.failed`, `auth.token.refreshed`, `auth.session.revoked`, and `auth.oidc.completed`. The session projection is described by the `authStreamSchema` entity stream. For the streams runtime itself see

[Durable streams](https://rickylabs.github.io/netscript/netscript/durable-workflows/streams/) .

> Scope: stream events are best-effort — the audit trail is separate
>
> The
>
> auth.*
>
> events are
>
> best-effort
>
> : they are no-ops unless the durable-streams service is wired (
>
> DURABLE_STREAMS_URL
>
> /
>
> services__streams__http__0
>
> set), so they are
>
> not
>
> a guaranteed audit trail. Only
>
> oidc.completed
>
> ,
>
> token.refreshed
>
> , and
>
> session.revoked
>
> write the session projection;
>
> signin.started
>
> /
>
> signin.failed
>
> are diagnostic-only. The dedicated audit surface is separate:
>
> createAuthTelemetry
>
> records structured, redacted audit spans when a
>
> subjectHashSalt
>
> is configured (see the production notes below and
>
> [Observability](https://rickylabs.github.io/netscript/netscript/explanation/observability/) ) — the raw stream events complement it, they do not replace it. For general tracing and structured logs see [Telemetry & logging](https://rickylabs.github.io/netscript/observability/telemetry/).

## Endpoints & ports

**Authentication runtime surface**

| Name | Type | Description |
| --- | --- | --- |
| `:8094` | `port` | auth-api default port (AUTH_API_DEFAULT_PORT). Family: workers :8091, sagas :8092, triggers :8093, auth :8094. |
| `auth-api` | `service name` | AUTH_API_SERVICE_NAME — the service contribution the auth plugin (AUTH_PLUGIN_ID 'auth') adds. |
| `/api/v1/auth/*` | `REST` | Public REST surface: signin, callback, signout, session, me. |
| `/api/rpc/v1/auth/*` | `oRPC` | The oRPC surface for the same five operations, for typed NetScript clients. |
| `/health/live` | `HTTP` | Liveness probe; /health/ready for readiness. OpenAPI + docs served via .withOpenAPI()/.withDocs(). |
| `NETSCRIPT_AUTH_BACKEND` | `env` | Selects the single active backend: kv-oauth (default) \| workos \| better-auth. |

## Production notes

> Footguns before you ship
>
> - **Only `kv-oauth` signs users in.** If `NETSCRIPT_AUTH_BACKEND` is `workos` or `better-auth`, the `signin` / `callback` endpoints return `AUTH_PROVIDER_ERROR` by design — those adapters only verify externally-issued sessions. Pick the backend that matches who owns the sign-in.
> - **The session cookie is `__Host-ns_session`.** The `__Host-` prefix *requires* `Path=/`, **no Domain**, and **HTTPS** — a misconfigured domain or a plain-HTTP origin makes the cookie refuse to set. Use a real TLS origin in production.
> - **Switching providers is an env-var + credentials change, not a runtime toggle.** There is no multi-active routing, cross-backend account linking, or global logout across backends in v1 (single-active-backend).
> - Auth audit is opt-in, and not a compliance-grade ledger.
>
>   A real, structured, redacted auth audit surface ships via
>
>   createAuthTelemetry
>
>   , but it only records when a
>
>   subjectHashSalt
>
>   is configured (resolved from
>
>   NETSCRIPT_AUTH_AUDIT_SALT
>
>   ) — with no salt it runs as a no-op. The raw
>
>   auth.*
>
>   stream events are themselves best-effort and no-op until the durable-streams service is wired. Treat the audit surface as a strong, queryable trail for operational and security review, not a tamper-evident, write-once compliance log. See
>
>   [Observability](https://rickylabs.github.io/netscript/netscript/explanation/observability/) .
> - **Run the `auth.prisma` migration after Aspire is up.** The better-auth backend's tables (and any DB-backed flow) need your database provisioned first (Postgres is the recommended engine; or `mysql` / `mssql` / `sqlite` via `--db` — better-auth persists through Prisma, so it is not Postgres-specific) — the same Aspire-first ordering every plugin schema follows.

## Reference

The auth runtime is a `@netscript/service`; the auth plugin, core contract package, and backend adapters now have dedicated generated reference pages.

[auth](https://rickylabs.github.io/netscript/netscript/reference/auth/)

[service](https://rickylabs.github.io/netscript/netscript/reference/service/)

[Do — Add authentication  Task recipe: add the auth plugin, set NETSCRIPT_AUTH_BACKEND, run the auth.prisma migration, and wire kv-oauth with a provider preset.](https://rickylabs.github.io/netscript/netscript/identity-access/how-to/add-authentication/)

[Understand — The authentication model  Why auth is pure-backend: the AuthBackendPort seam, the single-active-backend boundary, InteractiveFlowPort, and the three adapters' capability matrices.](https://rickylabs.github.io/netscript/netscript/explanation/auth-model/)

[Look up — @netscript/service reference  The oRPC service builder behind auth-api: createService().withRPC(), the contract surface, the /auth authn-authz seam, and the runtime that serves /api/v1/auth/*.](https://rickylabs.github.io/netscript/netscript/reference/service/)

[Fresh UI & design](https://rickylabs.github.io/netscript/netscript/web-layer/fresh-ui/)
