nominee-auth0
v3.0.0
Published
Auth0 strategy for Token Vault and push approvals — without it, a resumed action runs the credential held when the pause started.
Maintainers
Readme
Optional. The nominee core has zero dependencies and works without Auth0. Use this if you want Auth0 to manage token storage and push approvals for you.
Installation
npm i nominee nominee-auth0What It Does
flowchart TB
subgraph "nominee-auth0"
direction TB
TV["Token Vault\ngetToken()"]
CIBA["CIBA\nrequestApproval()"]
end
Agent["Agent calls\nnominee.run()"] --> RUN["decision-bound path\n(capability → token)"]
RUN --> TV
TV -->|"token exchange\ngrant (federated)"| Auth0["Auth0 Tenant"]
Auth0 -->|fresh token| TV
TV --> Agent
Agent2["Agent calls\nnominee.approve()"] --> CIBA
CIBA -->|"POST /bc-authorize"| Auth0
Auth0 -->|push notification| Phone["User's phone 📱"]
Phone -->|approve / deny| Auth0
Auth0 -->|poll result| CIBA
CIBA --> Agent2| Feature | What it does | |---|---| | Token Vault | Fetches fresh federated connection tokens (GitHub, Google, Slack…) from Auth0. No token storage in your DB. | | CIBA | Pushes an approval request to the user's device and polls until resolved. Real phone notifications, not a polling UI. |
Quickstart
import { Nominee } from 'nominee'
import { Auth0 } from 'nominee-auth0'
const nominee = new Nominee({
strategy: Auth0({
domain: process.env.AUTH0_DOMAIN!, // e.g. 'my-tenant.us.auth0.com'
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
subjectToken: ({ user }) => sessionStore.getRefreshToken(user),
}),
})
// Decision-bound: credential resolved inside execute after capability consumption
await nominee.run(
{ tool: 'github.issue.close', input: { repo, issue }, user: 'auth0|user_123', connection: 'github' },
({ token }) => closeIssue({ repo, issue, token }),
)For dev and non-production use, standalone nominee.token() still works:
// Fetches a fresh GitHub token from Auth0 Token Vault
const token = await nominee.token({
user: 'auth0|user_123',
connection: 'github',
})Zero-config — auth0()
For the common single-tenant setup, skip the options entirely. The lowercase
auth0() reads everything from the environment:
import { Nominee } from 'nominee'
import { auth0 } from 'nominee-auth0'
const nominee = new Nominee({ strategy: auth0() })It reads AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET,
AUTH0_REFRESH_TOKEN (the subject token), and AUTH0_USER_SUB (CIBA
login_hint, enabling push approvals when present).
When the core Auth0 vars are absent, auth0() transparently falls back to a
built-in mock (a short-TTL token + auto-approve) so an example or test runs
with zero setup. Set the env (e.g. via an example's pnpm setup) and the same
call becomes real Token Vault + CIBA — no code change. A half-set env throws an
actionable error rather than silently mocking.
This is config-code compression, not "zero config": you still provision the
tenant once. Use the explicit Auth0({...}) form above when you need
per-request subjectToken, multi-tenancy, or custom CIBA options.
CIBA — Push Approvals
For a local or single-process integration, the legacy blocking approval API is the shortest way to try CIBA:
const nominee = new Nominee({
strategy: Auth0({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
subjectToken: ({ user }) => sessionStore.getRefreshToken(user),
// Enable CIBA
ciba: {
bindingMessage: (req) => `Approve "${req.action}"?`,
},
}),
})
// Blocks this process until the user approves or the request expires.
await nominee.approve({
user: 'auth0|user_123',
action: 'repo.delete',
detail: 'Delete repository: alice/old-project',
})Production workflows should persist the CIBA request and resume the exact decision-bound action after a restart:
import { Pool } from 'pg'
import { Nominee, ask } from 'nominee'
import { Auth0, PostgresCibaStore } from 'nominee-auth0'
import { PostgresControlStore, postgresDatabase } from 'nominee-postgres'
const database = postgresDatabase(new Pool({ connectionString: process.env.DATABASE_URL }))
const control = new PostgresControlStore(database)
const ciba = new PostgresCibaStore(database)
const nominee = new Nominee({
production: true,
policy: { rules: [ask('repo.delete')], fallback: 'deny' },
actionStore: control,
receipts: { store: control, delivery: 'strict' },
strategy: Auth0({
domain: process.env.AUTH0_DOMAIN!,
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
subjectToken: ({ user }) => sessionStore.getRefreshToken(user),
ciba: { store: ciba },
}),
})
const input = { repo: 'alice/old-project' }
const prepared = await nominee.prepareAction({
tool: 'repo.delete',
user: 'auth0|user_123',
input,
})
if (prepared.status === 'pending_approval') {
await jobs.save({ actionId: prepared.action.id, input })
return
}
// In the resumed job, call resumeAction(actionId). When it returns `ready`,
// execute the returned capability with the original, hash-matched input.Apply both POSTGRES_SCHEMA from nominee-postgres and
POSTGRES_CIBA_SCHEMA from this package through your migration system.
Production mode rejects an in-memory CIBA store.
With Adapters
Drop-in replacement — just swap the strategy:
import { nomineeTool } from 'nominee-ai' // or nominee-eve
import { z } from 'zod'
const starRepo = nomineeTool({
nominee, // Auth0 strategy under the hood
user: 'auth0|user_123',
connection: 'github',
approval: true,
action: 'repo.star',
description: 'Star a GitHub repository',
inputSchema: z.object({ repo: z.string() }),
execute: async ({ repo }, ctx) => {
// ctx.token is a fresh token from Auth0 Token Vault
await fetch(`https://api.github.com/user/starred/${repo}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${ctx.token}` },
})
return `Starred ${repo}`
},
})Auth0 Setup (the honest version)
nominee removes the runtime token pain — one nominee.token() call, always fresh.
It does not remove Auth0/provider setup: that's a one-time job with sharp edges.
Here's what actually works, learned the hard way wiring the live GitHub demo:
- Enable Token Vault on the connection — via Connected Accounts. Token Vault is now
driven by Connected Accounts; set the top-level
connected_accounts.activeon the connection (the olderoptions.federated_connections_access_tokensis deprecated):auth0 api patch "connections/<CONNECTION_ID>" --data '{"connected_accounts":{"active":true}}' - GitHub: use a GitHub App with expiring tokens — not a classic OAuth App. Classic
OAuth Apps never issue refresh tokens, so Token Vault has nothing to vault (you'll get
federated_connection_refresh_token_not_found). Create a GitHub App, turn on "Expire user authorization tokens", set the callback tohttps://<tenant>/login/callback, and point the connection at the App's client id/secret. - Grant the exact permission the action needs — App vs repo scope matters. GitHub App
user tokens carry account permissions from user authorization, but repository
permissions (e.g.
metadata=read) require the App to be installed on the repo. Account-only actions (publish a gist, edit profile) need no installation; repo actions do. A403 "Resource not accessible by integration"means a missing permission — read thex-accepted-github-permissionsresponse header to see exactly which. - Re-vault after changing permissions. Connected Accounts caches the grant. After
changing what the user approved, delete the stale connected account
(
DELETE /me/v1/connected-accounts/{id}via the user's My Account token) and reconnect so the fresh consent is vaulted. - CIBA for push approvals: enable it in the tenant, then set
cibain the strategy.
Once wired, the runtime is just await nominee.token({ user, connection }) — and you can
swap this whole strategy for tokens() or OAuth2() without touching your agent code.
See the Auth0 documentation for the rest of tenant configuration.
Auth0 Strategy Options
Auth0({
domain: string // Auth0 tenant domain
clientId: string // M2M application client ID
clientSecret: string // M2M application client secret
subjectToken: (params: GetTokenParams) => string | Promise<string>
subjectTokenType?: 'refresh_token' | 'access_token'
fetch?: typeof fetch // optional custom fetch (defaults to global)
ciba?: {
loginHint?: (user: string) => string | Promise<string>
bindingMessage?: (req: ApprovalRequest) => string // message shown to user
pollIntervalMs?: number // default: from Auth0 response interval
scope?: string // default: 'openid'
audience?: string
}
})