Skip to main content
Alpha

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

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.
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.

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 .

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.

Principal (@netscript/plugin-auth-core) — the identity services authorize against
NameTypeDescription
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
NameTypeDescription
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
NameTypeDescription
{ 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/*)
NameTypeDescription
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.

Backend adapters: factory, capability, and what each one needs
NameTypeDescription
@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 — Userauth_users, Sessionauth_sessions, Accountauth_accounts, Verificationauth_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

Durable streams .

Endpoints & ports

Authentication runtime surface
NameTypeDescription
: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.

auth

service