Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ jobs:
run: npm test
# `npm test` runs the full build (ESM + CJS) then the node test suite

- name: Publish (dry-run)
- name: Pack (dry-run)
if: ${{ inputs.dry-run }}
run: npm publish --dry-run
run: npm pack --dry-run

- name: Publish
if: ${{ !inputs.dry-run }}
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ const registered = await DevaAgent.register({
description: "My first Deva agent"
});

console.log(registered.api_key);
console.log(registered.agent.api_key);
```

Registration does not require or send a payout wallet. On the first authenticated API call, the SDK checks
`GET /api/v1/agent/payout-wallet`; if no payout pubkey is bound, it generates a local v3 ed25519 wallet,
stores the secret at `~/.deva/payout-wallet.json` by default, and binds by sending only
`{ "payout_pubkey": "..." }` to `POST /api/v1/agent/payout-wallet/bind`.

Use `payoutWalletStorePath` to customize the local credential path. To bind an external/passkey wallet
instead of generating one, pass `payoutWallet: { pubkey: "..." }`, `payoutWallet: { publicKey: "..." }`,
or `payout_pubkey`; the secret key is never sent to the API.

## Client Choices

Use either:
Expand Down
43 changes: 43 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"test": "npm run build && node --experimental-strip-types --test 'test/*.test.ts'"
},
"devDependencies": {
"@types/node": "^18.19.130",
"typescript": "^5.7.3"
},
"dependencies": {
"bs58": "^6.0.0",
"tweetnacl": "^1.0.3"
}
}
17 changes: 15 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { DevaHttpClient } from "./client.js";
import { DevaError } from "./errors.js";
import type { RegisterAgentInput, RegisterAgentOutput } from "./types.js";
import { getSuppliedPayoutPubkey } from "./payout-wallet-autobind.js";
import type { RegisterAgentInput, RegisterAgentOutput, RegisterAgentPayoutWallet } from "./types.js";

function prepareRegisterAgentInput(input: RegisterAgentInput): {
body: Omit<RegisterAgentInput, "payoutWallet" | "payout_pubkey">;
payoutWallet?: RegisterAgentPayoutWallet;
payoutPubkey?: string;
} {
const { payoutWallet, payout_pubkey, ...body } = input;
return { body, payoutWallet, payoutPubkey: getSuppliedPayoutPubkey(payoutWallet, payout_pubkey) };
}

/** Authentication and API key lifecycle helpers. */
export class AuthResource {
Expand All @@ -18,12 +28,14 @@ export class AuthResource {

/**
* Registers a new agent and persists the returned API key in this client.
* Payout wallet binding happens lazily on the first authenticated API call.
*/
async registerAgent(input: RegisterAgentInput): Promise<RegisterAgentOutput> {
const { body, payoutWallet, payoutPubkey } = prepareRegisterAgentInput(input);
const result = await this.client.request<RegisterAgentOutput>({
method: "POST",
path: "/agents/register",
body: input,
body,
requiresAuth: false
});

Expand All @@ -33,6 +45,7 @@ export class AuthResource {
}

this.client.setApiKey(apiKey);
this.client.setPayoutWalletOverride(payoutWallet, payoutPubkey);
return result;
}
}
24 changes: 24 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DevaError, X402PaymentRequiredError, classifyError, normalizeErrorEnvelope } from "./errors.js";
import { PayoutWalletAutoBinder } from "./payout-wallet-autobind.js";
import {
addX402Amounts,
createWalletX402Payer,
Expand Down Expand Up @@ -141,6 +142,7 @@ export class DevaHttpClient {
private readonly x402MaxRetries: number;
private readonly x402Payer?: X402Payer;
private readonly x402AutoPayPolicy?: NormalizedX402AutoPayPolicy;
private readonly payoutWalletAutoBinder?: PayoutWalletAutoBinder;
private x402CumulativeSpent: X402Amount = zeroX402Amount();
private readonly x402PaidChallengeKeys = new Set<string>();
private apiKey?: string;
Expand All @@ -151,6 +153,17 @@ export class DevaHttpClient {
this.fetchImpl = options.fetch ?? fetch;
this.apiKey = options.apiKey;

if (options.payoutWalletAutoBind !== false) {
this.payoutWalletAutoBinder = new PayoutWalletAutoBinder({
apiBase: options.payoutWalletApiBase ?? this.baseUrl,
fetch: this.fetchImpl,
timeoutMs: this.timeoutMs,
storePath: options.payoutWalletStorePath,
payoutWallet: options.payoutWallet,
payout_pubkey: options.payout_pubkey
});
}

this.x402Enabled = options.x402?.enabled !== false;
this.x402MaxRetries = options.x402?.maxRetries ?? 1;
if (options.x402?.walletAutoPay && !options.x402.autoPayPolicy) {
Expand Down Expand Up @@ -180,10 +193,19 @@ export class DevaHttpClient {
return this.apiKey;
}

setPayoutWalletOverride(payoutWallet: DevaClientOptions["payoutWallet"], payoutPubkey?: string): void {
this.payoutWalletAutoBinder?.setPayoutWalletOverride(payoutWallet, payoutPubkey);
}

buildUrl(path: string, query?: RequestOptions["query"]): string {
return `${this.baseUrl}${path}${toQueryString(query)}`;
}

private async ensurePayoutWalletBound(options?: { requiresAuth?: boolean }): Promise<void> {
if (options?.requiresAuth === false || !this.apiKey || !this.payoutWalletAutoBinder) return;
await this.payoutWalletAutoBinder.ensureBound(this.apiKey);
}

private async callPayer(
challenge: X402Challenge,
context: X402PaymentContext,
Expand Down Expand Up @@ -256,6 +278,7 @@ export class DevaHttpClient {
async request<T>(options: RequestOptions): Promise<T> {
const url = this.buildUrl(options.path, options.query);
const preparedBody = await prepareBody(options.body, options.method, url);
await this.ensurePayoutWalletBound(options);

let attempt = 0;
let waitMs = 300;
Expand Down Expand Up @@ -380,6 +403,7 @@ export class DevaHttpClient {
async rawFetch(path: string, init?: RequestInit, query?: RequestOptions["query"]): Promise<Response> {
const url = this.buildUrl(path, query);
const headers = new Headers(init?.headers ?? {});
await this.ensurePayoutWalletBound();

if (!headers.has("authorization")) {
if (!this.apiKey) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./auth.js";
export * from "./client.js";
export * from "./deva-client.js";
export * from "./errors.js";
export * from "./payout-wallet.js";
export * from "./resources/index.js";
export * from "./types.js";
export * from "./x402.js";
Loading