> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# Worker auth reference

> JWT verification, Better Auth integration, and HMAC primitives.

```ts theme={null}
import {
  verifyJwt, decodeJwtPayload,
  createDeepSpaceAuth,
  verifyInternalSignature, buildInternalPayload,
  signInternalPayload, computeHmacHex, timingSafeEqualHex,
  DEFAULT_MAX_SKEW_MS,
} from 'deepspace/worker'

import type {
  JwtVerifierConfig, VerifyOutcome, VerifyResult, VerifiedAuth, JwtClaims,
  TokenDebugInfo,
  InternalSignature,
  DeepSpaceAuth, DeepSpaceAuthConfig,
} from 'deepspace/worker'
```

## `verifyJwt(config, token)`

Verifies a JWT against a public key. **Does not throw** - returns a `{ result, error?, debug? }` envelope.

```ts theme={null}
function verifyJwt(
  config: JwtVerifierConfig,
  token: string | null | undefined,
): Promise<VerifyOutcome>

interface JwtVerifierConfig {
  publicKey: string                 // PEM-encoded ES256; env.AUTH_JWT_PUBLIC_KEY
  issuer: string                    // env.AUTH_JWT_ISSUER
  audience?: string | string[]      // single value or list
  authorizedParties?: string[]      // azp patterns; supports "*" wildcards
  clockSkewMs?: number              // default 5000
}

interface VerifyOutcome {
  result: VerifyResult | null       // null on failure
  debug?: TokenDebugInfo            // unverified claims, for logging
  error?: unknown                   // underlying error (not stringified)
}

interface VerifyResult extends VerifiedAuth {}

interface VerifiedAuth {
  userId: string                    // claims.sub, surfaced for convenience
  claims: JwtClaims                 // full verified payload
}

interface JwtClaims {
  sub: string
  iss?: string
  aud?: string | string[]
  azp?: string
  exp?: number
  iat?: number
  name?: string
  email?: string
  image?: string
  [key: string]: unknown
}
```

`VerifyResult` is **not** a flat JWT-claims object - it's a `{ userId, claims }` envelope. Read `result.userId` for the subject, and `result.claims.email` / `result.claims.name` / `result.claims.image` for the optional profile fields:

```ts theme={null}
app.get('/api/me', async (c) => {
  const auth = c.req.header('Authorization') ?? ''
  const token = auth.replace(/^Bearer\s+/i, '')

  const { result } = await verifyJwt({
    publicKey: c.env.AUTH_JWT_PUBLIC_KEY,
    issuer:    c.env.AUTH_JWT_ISSUER,
  }, token)

  if (!result) return c.json({ error: 'unauthorized' }, 401)
  return c.json({
    userId: result.userId,
    email: result.claims.email,
  })
})
```

## `decodeJwtPayload(token)`

Base64url-decode the JWT payload **without verification**. Useful for inspecting `iss` / `aud` / `azp` / `exp` for logging or debugging where verification has already happened upstream.

```ts theme={null}
function decodeJwtPayload(token: string | null | undefined): TokenDebugInfo | undefined

interface TokenDebugInfo {
  iss?: string | null
  aud?: string | string[] | null
  azp?: string | null
  exp?: number | null
  iat?: number | null
}
```

Returns `undefined` if the token is missing or malformed.

<Warning>
  Never use as a substitute for `verifyJwt` on the trust boundary. Decoded but unverified claims can be spoofed by anyone, and `TokenDebugInfo` deliberately omits `sub` to discourage that.
</Warning>

## HMAC primitives - internal signing

For platform → app internal calls and any custom signed-request flow you build yourself. (Cron's `ctx.integrations.call` no longer uses these - it signs with `APP_OWNER_JWT` instead.) All of these are async and use `crypto.subtle` on the Workers runtime (with a Node fallback for testing).

```ts theme={null}
function buildInternalPayload(body: unknown): string

function signInternalPayload(input: {
  secret: string
  payload: string
  timestamp?: string                // defaults to Date.now().toString()
}): Promise<InternalSignature>

function verifyInternalSignature(input: {
  secret: string | undefined
  timestamp: string | null | undefined
  signature: string | null | undefined
  payload: string
  maxSkewMs?: number                // defaults to DEFAULT_MAX_SKEW_MS
}): Promise<boolean>

function computeHmacHex(secret: string, data: string): Promise<string>
function timingSafeEqualHex(a: string, b: string): Promise<boolean>

interface InternalSignature {
  timestamp: string
  signature: string
}

const DEFAULT_MAX_SKEW_MS: number   // 5 * 60_000 - verification window
```

`buildInternalPayload` returns a **plain string** (JSON-stringified body, or the input string passed through). The timestamp lives on the signature object returned by `signInternalPayload`, not on the payload.

These are exported so apps can verify inbound internal calls (e.g., custom webhook endpoints from the platform) using the same HMAC contract the rest of the SDK uses. Most apps never call these directly.

## `createDeepSpaceAuth(config)`

Construct a Better Auth instance pre-wired for DeepSpace conventions (cookie names, JWT issuance, plugin set). The scaffold doesn't build its own auth surface - it proxies to the platform auth-worker - so you only reach for this when standing up a custom auth-worker variant.

```ts theme={null}
function createDeepSpaceAuth(config: DeepSpaceAuthConfig): DeepSpaceAuth

interface DeepSpaceAuthConfig {
  /** D1 database binding */
  database: D1Database
  /** Base URL for the auth worker (e.g. "https://auth.deep.space") */
  baseURL: string
  /** Secret for session signing */
  secret: string
  /** Google OAuth credentials (optional) */
  google?: { clientId: string; clientSecret: string }
  /** GitHub OAuth credentials (optional) */
  github?: { clientId: string; clientSecret: string }
  /** Enable email/password authentication (default: true) */
  emailAndPassword?: boolean
  /** Trusted origins for CORS */
  trustedOrigins?: string[]
}

type DeepSpaceAuth = ReturnType<typeof createDeepSpaceAuth>
```

`DeepSpaceAuth` is the `better-auth` `Auth` instance with the DeepSpace defaults (organization + twoFactor plugins, `*.deep.space` / `*.app.space` / `localhost:*` trusted origins) applied. There are no `privateKey` / `publicKey` / `issuer` fields on the config - JWT signing keys live in the auth-worker's environment, not in this object.

## See also

* [Authentication guide](/guides/authentication)
* [Client auth reference](/sdk-reference/client/auth)
* [Architecture concepts](/concepts/architecture#security-model--websocket-identity)
