Authentication
NetScript ships authentication as a first-class official plugin: netscript plugin add @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
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.
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
Learn → / Do →
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.
// Add the plugin from the workspace root:
// netscript plugin add @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.
// 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 });
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.
// 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' });
// 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.
| 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. |
| 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. |
| 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.
| 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. |
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.
| 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. |
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.
Database & runtime events
The plugin contributes a Prisma schema, plugins/auth/database/auth.prisma, 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.
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
Endpoints & ports
| 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
Reference
The auth runtime is a @netscript/service; the auth plugin, core contract package, and backend
adapters now have dedicated generated reference pages.