Skip to content

Provision an agent identity

super-line has no bot type. An AI agent or service is an ordinary API-key user, provisioned server-side with authKit from @super-line/plugin-auth/server. Because profiles and credentials are separate imperative surfaces, an API-key-only identity has no empty credential row — you create the user, mint a key, and the agent connects as itself.

Provision an agent

Create the user, then mint an API key for it. The agent connects with params: { apiKey } and acts as itself — its writes carry its own userId, so row policies key on the agent exactly as they do on a person.

ts
const scoutUser = await authKit.users.create({ displayName: 'scout', roles: ['user'], metadata: { kind: 'agent' } })
const { key } = await authKit.apiKeys.create(scoutUser.id, { role: 'user', label: 'scout-agent' })

From examples/auth — the agent connects and writes as itself, no credential row ever created:

ts
const scoutUser = await authKit.users.create({ displayName: 'scout', roles: ['user'], metadata: { kind: 'agent' } })
console.log('  scout metadata →', scoutUser.metadata) // { kind:'agent', provisionedVia:'agent-kit' } — the transform landed
const { key: scoutKey } = await authKit.apiKeys.create(scoutUser.id, { role: 'user', label: 'scout-agent' })

// The minted key authenticates: scout connects with `params: { apiKey }` and acts as itself.
const scout = createSuperLineClient(app, { transport: transport(), role: 'user', params: { apiKey: scoutKey } })
console.log('  scout whoami →', await scout.whoami()) // { userId, displayName:'scout', roles:['user'] }
await scout.collection('notes').insert({ id: 'n3', ownerId: scoutUser.id, text: 'recon complete', createdAt: Date.now() })
console.log('  scout wrote a note as itself')
scout.close()

The raw key is returned once

apiKeys.create returns the raw slp_… key a single time. Store it where the agent will read it (a vault, a secret env var) at provisioning; you cannot recover it later. An API key is long-lived with one fixed role.

Agents that also call tools or an LLM inline are covered in Build with AI agents.

Manage identities from server code

The kit exposes imperative surfaces for back-office code and provisioning — including AI-agent users. They need the running server (the co-writer binds at plugin setup):

ts
// authKit.users — the directory, back-office edits, and provisioning
authKit.users.get(id)                                         // → AuthUser | undefined
authKit.users.find({ filter?, limit?, offset?, includeDeactivated? }) // → AuthUser[] (active-only by default)
authKit.users.create({ displayName, roles?, metadata? })
authKit.users.update(id, { displayName?, metadata? })
authKit.users.setRoles(id, roles)                             // validated against contract roles (connect-time)
authKit.users.deactivate(id)   // soft-delete: stamp deletedAt, flush sessions/keys/resets, kick connections
authKit.users.reactivate(id)   // lift the deactivation
// authKit.credentials — attach email/password authentication only when needed
authKit.credentials.create(userId, { email, password? })     // omit password → invite flow
authKit.credentials.setPassword(userId, newPassword)         // rotation; revokes access + reset tokens

// authKit.apiKeys — provision agents & services
authKit.apiKeys.create(userId, { role, label, expiresInMs? }) // → { …info, key } — raw slp_… returned ONCE
authKit.apiKeys.listFor(userId)                              // → ApiKeyInfo[]
authKit.apiKeys.revoke(id)
  • Users soft-delete rather than vanish — deactivate stamps deletedAt, reactivate lifts it, and old rows keep rendering author names.
  • users.find excludes deactivated users unless you pass includeDeactivated.
  • authKit.tokens.{mintSigned,mintSealed,verify} mint and check bearer assertions — also server-side, also needing the running server.

Revoke & lock out

await authKit.revoke(userId) flushes the user's access tokens, ends their sessions, and disconnects their live connections cluster-wide (an admin ban or "sign out of all devices"). It does not touch API keys or outstanding JWT assertions:

  • API keys survive revoke — retire them per-key with authKit.apiKeys.revoke(id).
  • Bearer assertions are in no table, so nothing revokes them; keep their ttlMs short.

For a compromised account, authKit.users.deactivate(userId) is the emergency stop that even closes the signed-assertion door — connect reads the user row, so a deactivated user resolves to guest on the very next connect. From examples/auth:

ts
// 3 · The cost of statelessness, stated plainly. `revoke` flushes access tokens, ends sessions and
//     disconnects Bob everywhere — but an outstanding JWT is in no table, so there is nothing to
//     revoke. It keeps working until it expires. Short TTLs are the mitigation, not revocation.
console.log('\n— Bob is revoked —')
await authKit.revoke(bobId)
const afterRevoke = createSuperLineClient(app, { transport: transport(), role: 'user', params: { jwt } })
console.log('  whoami over jwt →', await afterRevoke.whoami()) // still Bob — the signature is still valid
afterRevoke.close()

// 4 · …and the emergency stop. `resolveBase` reads the user row to check for deactivation — the one
//     deliberate dent in statelessness — so deactivating Bob closes even the signed door.
console.log('\n— Bob is deactivated —')
await authKit.users.deactivate(bobId)
const afterDeactivate = createSuperLineClient(app, { transport: transport(), role: 'user', params: { jwt } })
console.log('  whoami over jwt →', await afterDeactivate.whoami()) // null — accepted as a guest
afterDeactivate.close()

Next: Reset a password · back to Choose an auth strategy.

Released under the MIT License.