@lucid-dreams/agent-auth
v0.2.24
Published
Agent authentication runtime primitives
Readme
Agent Runtime Module Scaffold
Context & Goals
- Build the
@lucid-dreams/agent-authruntime around the challenge → exchange → refresh loop while hiding protocol details from consumers. - Lean on the generated HTTP layer exported from
packages/sdk/src/index.tsso every network call stays in sync with the server OpenAPI contract. - Deliver a structure that downstream teams can implement module-by-module without having to re-derive responsibilities.
Runtime Package Layout
src/
runtime/
agent-runtime.ts
agent-auth-client.ts
agent-api-client.ts
token-manager.ts
storage/
memory-adapter.ts
file-adapter.ts
types.ts
wallet/
base-connector.ts
coinbase-connector.ts
privy-connector.ts
local-key-connector.ts
transport/
http-transport.ts
undici-transport.ts
fetch-transport.ts
events.ts
errors.ts
logger.ts
config-loader.ts- The runtime owns orchestration and abstractions; all HTTP contracts flow through the generated SDK under
@lucid-dreams/sdk. - CLI and agent-kit consumers import only the public surface inside
src/runtime.
Generated SDK Integration
packages/sdk/src/index.tsre-exportssdk.gen(request builders) andtypes.gen(request/response types), so the runtime always imports from@lucid-dreams/sdkroot.- The runtime currently relies on the following request builders:
- Auth:
postV1AuthAgentsByAgentRefChallenge,postV1AuthAgentsByAgentRefExchange,postV1AuthAgentsByAgentRefRefresh. - Agents:
getV1Agents,getV1AgentsByAgentRef. - Wallet:
postV1AgentsByAgentRefWalletSignMessage,postV1AgentsByAgentRefWalletSignTypedData,postV1AgentsByAgentRefWalletSignTransaction,postV1AgentsByAgentRefWalletSendTransaction.
- Auth:
- See
src/runtime/__tests__/sdk-contract.test.tsfor a regression test that asserts these helpers remain available and keep their expected request shapes. - To regenerate the SDK after OpenAPI changes:
pnpm --filter @lucid-dreams/sdk run openapi-tspnpm --filter @lucid-dreams/sdk run build- Re-run
bun test(at minimumpackages/agent-auth) to ensure the runtime still compiles.
Module Specifications
AgentRuntime (src/runtime/agent-runtime.ts)
- Purpose: High-level façade that bootstraps config, authenticates on demand, and keeps tokens fresh.
- Public Surface:
static fromConfig(options?: FromConfigOptions): Promise<AgentRuntime>static load(options?: LoadOptions): Promise<AgentRuntimeLoadResult>authenticate(): Promise<AuthenticatedSession>ensureAccessToken(): Promise<string>on(event: AgentAuthEvent, handler: EventHandler): () => voidshutdown(): Promise<void>(optional lifecycle hook to stop background refresh).
- Collaborators:
ConfigLoaderfor config hydration.AgentAuthClientfor challenge/exchange/refresh.AgentApiClientfor authenticated API calls.TokenManagerfor caching and refresh scheduling.Logger+events.tsfor observability.
- Generated SDK Touchpoints: Delegates to
AgentAuthClient, which is the only module talking to@lucid-dreams/sdk/auth. - Implementation Notes:
- Start background refresh on first successful
authenticate. - Emit
Authenticated,TokenRefreshed,RefreshFailed,CredentialRevoked,ReauthenticationRequiredevents. - Provide escape hatches to override storage, transport, wallet connector, and logger via constructor options.
- Start background refresh on first successful
- Status: Implemented
AgentRuntime.fromConfigwith configurable transport/auth/token wiring, background refresh, and event hooks, plusAgentRuntime.loadconvenience wrapper that composes the config loader (src/runtime/agent-runtime.ts,src/runtime/__tests__/agent-runtime.test.ts).
AgentAuthClient (src/runtime/agent-auth-client.ts)
- Purpose: Typed wrapper over
/challenge,/exchange,/refreshendpoints. - Public Surface:
requestChallenge(payload: ChallengeRequest): Promise<ChallengeResponse>exchange(payload: ExchangeRequest): Promise<ExchangeResponse>refresh(payload: RefreshRequest): Promise<RefreshResponse>
- Collaborators:
- Accepts an
HttpTransportinstance for network execution. - Uses
WalletConnectorto sign challenge payloads (if runtime delegates signing here). - Returns typed data using interfaces from
@lucid-dreams/sdk/types.
- Accepts an
- Generated SDK Touchpoints:
- Calls
auth.challenge,auth.exchange,auth.refreshexported fromsdk.gen. - Re-exports relevant TypeScript types (
ChallengePayload, etc.) so consumers do not import the generated files directly.
- Calls
- Implementation Notes:
- Inject
baseUrl,clientId,agentId, and telemetry metadata through constructor options. - Translate HTTP errors into discriminated union errors defined in
errors.ts.
- Inject
AgentApiClient (src/runtime/agent-api-client.ts)
- Purpose: Authenticated convenience layer over protected agent APIs.
- Public Surface (held behind typed namespaces):
listAgents(params?): Promise<AgentsListResponse>sendTransaction(payload): Promise<SendTransactionResponse>- Additional generated calls as coverage grows.
- Collaborators:
- Consumes the generated SDK groups (
agents.*,transactions.*, etc.). - Accepts a callback
getAccessToken(): Promise<string>to attach bearer tokens lazily. - Uses shared
HttpTransportfor retries/telemetry.
- Consumes the generated SDK groups (
- Generated SDK Touchpoints:
- Import relevant groups from
@lucid-dreams/sdkrather than reimplementing fetch. - Provide thin wrappers that enrich errors/events before re-throwing.
- Import relevant groups from
- Implementation Notes:
- Surfacing coverage: optionally track which generated endpoints remain unwrapped.
- Expose only stable APIs; experimental ones live under
experimental/namespace.
- Status: Agent API client scaffolded with
listAgents,getAgent,signMessage, andsendTransaction, leveraging the shared transport + access-token callback with unit coverage (src/runtime/agent-api-client.ts,src/runtime/__tests__/agent-api-client.test.ts).
TokenManager (src/runtime/token-manager.ts)
- Purpose: Persist access + refresh tokens, schedule refresh, dedupe concurrent refresh requests.
- Public Surface:
getCachedAccessToken(): CachedToken | nullsetTokens(tokens: TokenBundle): voidscheduleRefresh(trigger: () => Promise<TokenBundle>): CancelHandlewithRefreshLock<T>(fn: () => Promise<T>): Promise<T>(single-flight helper).
- Collaborators:
StorageAdapterfor persistence.Logger+eventsfor refresh telemetry.
- Implementation Notes:
- Default refresh window: 30s prior to
exp. - Detect nonce/jti changes to guard against replayed refresh handles.
- Provide serialization format for disk persistence and document it.
- Default refresh window: 30s prior to
Storage Adapters (src/runtime/storage/*)
- Purpose: Abstract persistence for refresh handles and metadata.
- Interfaces:
StorageAdapter#get(key: string): Promise<StoredValue | null>StorageAdapter#set(key: string, value: StoredValue): Promise<void>StorageAdapter#clear(key: string): Promise<void>
- Default Implementations:
MemoryStorageAdapter— in-process map.FileStorageAdapter— JSON file with advisory locking to survive restarts.
- Extensibility: Document how to supply custom adapters (Redis, AWS Secrets Manager) via
AgentRuntime.fromConfigoptions.
Wallet Connectors (src/runtime/wallet/*)
- Purpose: Provide signing capabilities for different execution environments.
- Interface (
base-connector.ts):- Extends the runtime
ChallengeSignerinterface with metadata helpers (getWalletMetadata(),getAddress(),supportsCaip2()). - Offers shared utilities for challenge normalization, stable JSON stringification, payload encoding detection, and signature extraction.
- Extends the runtime
- Concrete Connectors:
ServerOrchestratorWalletConnector— delegates signing to the hosted server orchestrator via the agent wallet endpoints.LocalEoaWalletConnector— wraps local EOA signers (Node or browser) and supports message + typed data flows.CoinbaseManagedWalletConnector(stub) — placeholder for Coinbase managed wallet orchestration.PrivyEip191Connector(stub) — placeholder for Privy managed wallet signing.
- Generated SDK Touchpoints: Connectors interact with
@lucid-dreams/sdkwallet endpoints when delegating signature requests. - Implementation Notes: Guardrails for CAIP-2 chain matching, payload normalization, and metadata propagation are covered by dedicated unit tests.
HttpTransport (src/runtime/transport/*)
- Purpose: Normalise HTTP execution across runtimes.
- Interface:
request<T>(options: HttpRequestOptions): Promise<HttpResponse<T>>
- Default Implementations:
undici-transport.tsfor Node/Bun.fetch-transport.tsfor browser/edge environments.
- Status: Fetch-based transport implemented with timeout handling, JSON helpers, and a fetch-compatible façade (
asFetch()); Undici factory reuses the fetch transport while allowing optional dispatcher injection. - Responsibilities:
- Handle retries, backoff, timeout, and instrumentation hooks.
- Provide hook for injecting headers (user agent, telemetry metadata).
- Generated SDK Touchpoints: The generated SDK accepts a
fetchimplementation; we should pass our wrapped transport to ensure consistent behaviour.
Logger & Events (src/runtime/logger.ts, src/runtime/events.ts)
- Logger: Accept Pino-compatible logger; default to lightweight structured logger with redacted secrets.
- Events:
- Define
AgentAuthEventenum and payload contracts. - Provide subscription helpers used by
AgentRuntime. - Emit events from
AgentAuthClient,TokenManager, andAgentApiClientas relevant.
- Define
Config Loader (src/runtime/config-loader.ts)
- Purpose: Merge configuration sources for the runtime.
- Responsibilities:
- Load
.lucid-agent.jsongenerated by CLI. - Overlay environment variables (
LUCID_AGENT_*). - Accept runtime overrides (custom transport, storage adapter, wallet connector, logger).
- Validate schema using zod/types exported from
@lucid-dreams/sdkwhere available.
- Load
- Status: Loader implemented with file/env/override precedence, sensible defaults, unit coverage, and now exposed through
AgentRuntime.loadfor a one-call bootstrap that returns the resolved config and source metadata (src/runtime/config-loader.ts,src/runtime/agent-runtime.ts,src/runtime/__tests__/config-loader.test.ts,src/runtime/__tests__/agent-runtime.test.ts).
Errors (src/runtime/errors.ts)
- Purpose: Centralised discriminated union for runtime failures.
- Shape:
AgentRuntimeErrorunion (CredentialRevoked,RefreshFailed,ChallengeFailed,TransportError,ConfigurationError,CoverageMismatch).- Include helper type guards (
isCredentialRevoked(error)etc.). - Map generated SDK error responses into these unions.
- Status: Client-facing error taxonomy extended for auth + agent API flows (
AgentAuthClientError,AgentApiClientError,TokenManagerError) with contextual metadata surfaced to callers.
Cross-Cutting Concerns
- Telemetry: Optional OpenTelemetry spans named
agent.auth.challenge,agent.auth.exchange,agent.auth.refresh; attach HTTP status, retry count, latency. - Debug Mode: Toggle via
LUCID_AGENT_DEBUG=1to emit verbose logs without leaking tokens. - Edge Compatibility: Document required polyfills (
crypto.subtle,TextEncoder) and how transports select runtime-specific implementations. - Testing:
- Contract tests: mock generated SDK responses to verify token lifecycle scenarios.
- Storage adapter tests: concurrency + persistence.
- Integration smoke: run against local stack using the generated SDK to guarantee parity.
- CLI Alignment: CLI should boot the runtime via
AgentRuntime.fromConfig()and rely onAgentApiClientfor admin operations.
Usage Examples
Quick start with createWallet
import { createWallet } from "@lucid-dreams/agent-auth";
const wallet = await createWallet({
// Omitting `config` lets createWallet read standard LUCID_* environment variables
events: {
authenticated: (event) => {
console.log("agent authenticated", event.accessToken);
},
},
});
await wallet.ensureAccessToken();
const signed = await wallet.signMessage({
message: "gm",
});
console.log("signature", signed.signed);
await wallet.shutdown();Integrate with viem
import {
createWallet,
createViemAccount,
} from "@lucid-dreams/agent-auth";
import {
createPublicClient,
createWalletClient,
http,
parseEther,
} from "viem";
import { base } from "viem/chains";
const lucidWallet = await createWallet();
// Ensure the wallet has an address (authenticate once if using the server orchestrator).
const account = await createViemAccount(lucidWallet, { chainId: base.id });
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: base,
transport: http(),
});
const hash = await walletClient.sendTransaction({
account,
to: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
value: parseEther("0.01"),
});
console.log("submitted transaction", hash);Want a runnable script? Check
example/viem.tsfor a full workflow that wires environment-driven configuration into a viem wallet client, signs messages, typed data, and demonstrates optional transaction signing.
Bootstrap with explicit config
import {
AgentRuntime,
MemoryStorageAdapter,
} from "@lucid-dreams/agent-auth";
const runtime = await AgentRuntime.fromConfig({
config: {
baseUrl: "https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
credentialId: process.env.LUCID_AGENT_CREDENTIAL!,
refreshToken: process.env.LUCID_AGENT_REFRESH_TOKEN,
scopes: ["agents.read"],
},
wallet: {
signer: myWalletConnector, // e.g. new ServerOrchestratorWalletConnector({...})
},
storage: new MemoryStorageAdapter(), // swap in a durable adapter in production
});
runtime.on("tokenRefreshed", (event) => {
console.log("tokens rotated", event.expiresAt.toISOString());
});
const agents = await runtime.api.listAgents();
console.log("agent count", agents.items.length);
await runtime.shutdown();Bootstrap from .lucid-agent.json
import { AgentRuntime } from "@lucid-dreams/agent-auth";
const { runtime, config, sourcePath } = await AgentRuntime.load({
wallet: { signer: myWalletConnector },
loader: {
// optional overrides; defaults align with CLI output
cwd: process.cwd(),
},
});
console.log("loaded agent config from", sourcePath ?? "environment only");
console.log("authenticated agent ref", config.agentRef);
const session = await runtime.ensureAccessToken();
console.log("active scopes", session.scopes);Use the server wallet connector
import {
AgentRuntime,
MemoryStorageAdapter,
ServerOrchestratorWalletConnector,
} from "@lucid-dreams/agent-auth";
const serverWallet = new ServerOrchestratorWalletConnector({
baseUrl:
process.env.LUCID_SERVER_URL ??
process.env.LUCID_BASE_URL ??
"https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
});
if (process.env.LUCID_SERVER_ACCESS_TOKEN) {
serverWallet.setAccessToken(process.env.LUCID_SERVER_ACCESS_TOKEN);
}
const runtime = await AgentRuntime.fromConfig({
config: {
baseUrl:
process.env.LUCID_BASE_URL ?? "https://lucid.daydream.systems",
agentRef: process.env.LUCID_AGENT_REF!,
credentialId: process.env.LUCID_AGENT_CREDENTIAL!,
refreshToken: process.env.LUCID_AGENT_REFRESH_TOKEN,
},
wallet: { signer: serverWallet },
storage: new MemoryStorageAdapter(),
});
// Keep the orchestrator token in sync with runtime refresh events
runtime.on("authenticated", (event) => {
serverWallet.setAccessToken(event.accessToken);
});
runtime.on("tokenRefreshed", (event) => {
serverWallet.setAccessToken(event.accessToken);
});Sign a transaction
const agentRef = process.env.LUCID_AGENT_REF ?? config.agentRef; // reuse the agent ref from your loaded config
const signed = await runtime.api.signTransaction(
{
caip2: "eip155:8453",
params: {
transaction: {
to: "0x0000000000000000000000000000000000000000",
value: "0x0",
data: "0x",
},
},
authorization_context: {
reason: "lucid.example.sign-transaction",
},
},
{ agentRef }
);
console.log("signed transaction", signed.signed);Customize transport and storage
import {
AgentRuntime,
createFetchTransport,
MemoryStorageAdapter,
} from "@lucid-dreams/agent-auth";
const resolvedConfig = {
baseUrl: "https://lucid.daydream.systems",
agentRef: "agent-123",
credentialId: "cred-123",
};
const runtime = await AgentRuntime.fromConfig({
config: resolvedConfig,
wallet: { signer: myWalletConnector },
transport: createFetchTransport({
baseUrl: resolvedConfig.baseUrl,
fetch: myInstrumentedFetch,
defaultHeaders: {
"x-lucid-sdk-version": "agent-auth/0.1.0",
},
}),
storage: new MemoryStorageAdapter(),
});Implementation Checklist
- [x] Implemented
AgentAuthClientvia generated SDK helpers with unit coverage (src/runtime/agent-auth-client.ts,src/runtime/__tests__/agent-auth-client.test.ts). - [x] Added
AgentApiClientcoverage for initial agent + wallet calls with unit tests (src/runtime/agent-api-client.ts,src/runtime/__tests__/agent-api-client.test.ts). - [ ] Finalise generated SDK namespace mappings (
auth.*,agents.*, etc.) and document them here. - [ ] Create runtime module files with exported interfaces matching the above specs. (TokenManager + storage adapters + HTTP transport + AgentApiClient + AgentRuntime + ConfigLoader scaffolded with tests.)
- [x] Implemented config loader with env/file/override precedence and tests (
src/runtime/config-loader.ts,src/runtime/__tests__/config-loader.test.ts). - [x] Implement
AgentRuntimebootstrap that wiresConfigLoader,TokenManager,AgentAuthClient, andAgentApiClienttogether. - [x] Added
AgentRuntime.loadconvenience entrypoint that composes the config loader and surfaces resolved metadata (src/runtime/agent-runtime.ts,src/runtime/__tests__/agent-runtime.test.ts). - [ ] Provide default storage, transport, wallet connector, and logger implementations with the documented extension points.
- [x] Add DX examples demonstrating how to call the runtime (
const runtime = await AgentRuntime.fromConfig();). - [ ] Wire CI to regenerate SDK and run doc/example compilation to catch drift.
