@1claw/sdk 0.2.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -4
- package/dist/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/client.test.js +99 -0
- package/dist/__tests__/client.test.js.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +125 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/http.test.d.ts +2 -0
- package/dist/__tests__/http.test.d.ts.map +1 -0
- package/dist/__tests__/http.test.js +200 -0
- package/dist/__tests__/http.test.js.map +1 -0
- package/dist/__tests__/resources.test.d.ts +2 -0
- package/dist/__tests__/resources.test.d.ts.map +1 -0
- package/dist/__tests__/resources.test.js +538 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/cmek.d.ts +42 -0
- package/dist/cmek.d.ts.map +1 -0
- package/dist/cmek.js +101 -0
- package/dist/cmek.js.map +1 -0
- package/dist/{client.d.ts → core/client.d.ts} +15 -21
- package/dist/core/client.d.ts.map +1 -0
- package/dist/{client.js → core/client.js} +24 -42
- package/dist/core/client.js.map +1 -0
- package/dist/{errors.d.ts → core/errors.d.ts} +8 -1
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/{errors.js → core/errors.js} +22 -2
- package/dist/core/errors.js.map +1 -0
- package/dist/{http.d.ts → core/http.d.ts} +7 -1
- package/dist/core/http.d.ts.map +1 -0
- package/dist/{http.js → core/http.js} +53 -0
- package/dist/core/http.js.map +1 -0
- package/dist/generated/api-types.d.ts +4274 -0
- package/dist/generated/api-types.d.ts.map +1 -0
- package/dist/generated/api-types.js +6 -0
- package/dist/generated/api-types.js.map +1 -0
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp/handler.d.ts +1 -1
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/plugins/audit-sink.d.ts +48 -0
- package/dist/plugins/audit-sink.d.ts.map +1 -0
- package/dist/plugins/audit-sink.js +2 -0
- package/dist/plugins/audit-sink.js.map +1 -0
- package/dist/plugins/crypto-provider.d.ts +38 -0
- package/dist/plugins/crypto-provider.d.ts.map +1 -0
- package/dist/plugins/crypto-provider.js +2 -0
- package/dist/plugins/crypto-provider.js.map +1 -0
- package/dist/plugins/index.d.ts +16 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/policy-engine.d.ts +58 -0
- package/dist/plugins/policy-engine.d.ts.map +1 -0
- package/dist/plugins/policy-engine.js +2 -0
- package/dist/plugins/policy-engine.js.map +1 -0
- package/dist/{access.d.ts → resources/access.d.ts} +2 -2
- package/dist/resources/access.d.ts.map +1 -0
- package/dist/resources/access.js.map +1 -0
- package/dist/{agents.d.ts → resources/agents.d.ts} +9 -4
- package/dist/resources/agents.d.ts.map +1 -0
- package/dist/{agents.js → resources/agents.js} +9 -3
- package/dist/resources/agents.js.map +1 -0
- package/dist/{api-keys.d.ts → resources/api-keys.d.ts} +2 -2
- package/dist/resources/api-keys.d.ts.map +1 -0
- package/dist/resources/api-keys.js.map +1 -0
- package/dist/{approvals.d.ts → resources/approvals.d.ts} +2 -2
- package/dist/resources/approvals.d.ts.map +1 -0
- package/dist/resources/approvals.js.map +1 -0
- package/dist/{audit.d.ts → resources/audit.d.ts} +2 -2
- package/dist/resources/audit.d.ts.map +1 -0
- package/dist/resources/audit.js.map +1 -0
- package/dist/{auth.d.ts → resources/auth.d.ts} +8 -2
- package/dist/resources/auth.d.ts.map +1 -0
- package/dist/{auth.js → resources/auth.js} +16 -0
- package/dist/resources/auth.js.map +1 -0
- package/dist/{billing.d.ts → resources/billing.d.ts} +2 -2
- package/dist/resources/billing.d.ts.map +1 -0
- package/dist/resources/billing.js.map +1 -0
- package/dist/{chains.d.ts → resources/chains.d.ts} +2 -2
- package/dist/resources/chains.d.ts.map +1 -0
- package/dist/resources/chains.js.map +1 -0
- package/dist/{org.d.ts → resources/org.d.ts} +2 -2
- package/dist/resources/org.d.ts.map +1 -0
- package/dist/resources/org.js.map +1 -0
- package/dist/{secrets.d.ts → resources/secrets.d.ts} +2 -2
- package/dist/resources/secrets.d.ts.map +1 -0
- package/dist/resources/secrets.js.map +1 -0
- package/dist/{sharing.d.ts → resources/sharing.d.ts} +2 -2
- package/dist/resources/sharing.d.ts.map +1 -0
- package/dist/resources/sharing.js.map +1 -0
- package/dist/resources/vault.d.ts +29 -0
- package/dist/resources/vault.d.ts.map +1 -0
- package/dist/resources/vault.js +55 -0
- package/dist/resources/vault.js.map +1 -0
- package/dist/{x402.d.ts → resources/x402.d.ts} +2 -2
- package/dist/resources/x402.d.ts.map +1 -0
- package/dist/{x402.js → resources/x402.js} +1 -1
- package/dist/resources/x402.js.map +1 -0
- package/dist/types.d.ts +125 -127
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/package.json +14 -4
- package/dist/access.d.ts.map +0 -1
- package/dist/access.js.map +0 -1
- package/dist/agents.d.ts.map +0 -1
- package/dist/agents.js.map +0 -1
- package/dist/api-keys.d.ts.map +0 -1
- package/dist/api-keys.js.map +0 -1
- package/dist/approvals.d.ts.map +0 -1
- package/dist/approvals.js.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/billing.d.ts.map +0 -1
- package/dist/billing.js.map +0 -1
- package/dist/chains.d.ts.map +0 -1
- package/dist/chains.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js.map +0 -1
- package/dist/org.d.ts.map +0 -1
- package/dist/org.js.map +0 -1
- package/dist/secrets.d.ts.map +0 -1
- package/dist/secrets.js.map +0 -1
- package/dist/sharing.d.ts.map +0 -1
- package/dist/sharing.js.map +0 -1
- package/dist/vault.d.ts +0 -18
- package/dist/vault.d.ts.map +0 -1
- package/dist/vault.js +0 -30
- package/dist/vault.js.map +0 -1
- package/dist/x402.d.ts.map +0 -1
- package/dist/x402.js.map +0 -1
- /package/dist/{access.js → resources/access.js} +0 -0
- /package/dist/{api-keys.js → resources/api-keys.js} +0 -0
- /package/dist/{approvals.js → resources/approvals.js} +0 -0
- /package/dist/{audit.js → resources/audit.js} +0 -0
- /package/dist/{billing.js → resources/billing.js} +0 -0
- /package/dist/{chains.js → resources/chains.js} +0 -0
- /package/dist/{org.js → resources/org.js} +0 -0
- /package/dist/{secrets.js → resources/secrets.js} +0 -0
- /package/dist/{sharing.js → resources/sharing.js} +0 -0
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ const secret = await client.secrets.get("vault-id", "OPENAI_KEY");
|
|
|
32
32
|
console.log(secret.data?.value);
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
**API contract:** This SDK is built from the **OpenAPI 3.1** spec. The canonical spec is published as [@1claw/openapi-spec](https://www.npmjs.com/package/@1claw/openapi-spec) (YAML/JSON). Types are generated from it; the SDK stays in sync with the API. For a full endpoint list, see the [API reference](https://docs.1claw.xyz/docs/reference/api-reference) or the spec.
|
|
36
|
+
|
|
35
37
|
## Authentication
|
|
36
38
|
|
|
37
39
|
The SDK supports three authentication modes:
|
|
@@ -69,14 +71,14 @@ await client.auth.google({ id_token: "..." });
|
|
|
69
71
|
| `client.vault` | `create`, `get`, `list`, `delete` |
|
|
70
72
|
| `client.secrets` | `set`, `get`, `delete`, `list`, `rotate` |
|
|
71
73
|
| `client.access` | `grantHuman`, `grantAgent`, `update`, `revoke`, `listGrants` |
|
|
72
|
-
| `client.agents` | `create`, `get`, `list`, `update`, `delete`, `rotateKey`, `submitTransaction`, `getTransaction`, `listTransactions` |
|
|
74
|
+
| `client.agents` | `create`, `getSelf`, `get`, `list`, `update`, `delete`, `rotateKey`, `submitTransaction`, `getTransaction`, `listTransactions`, `simulateTransaction`, `simulateBundle` |
|
|
73
75
|
| `client.chains` | `list`, `get`, `adminList`, `create`, `update`, `delete` |
|
|
74
76
|
| `client.sharing` | `create`, `access`, `listOutbound`, `listInbound`, `accept`, `decline`, `revoke` |
|
|
75
77
|
| `client.approvals` | `request`, `list`, `approve`, `deny`, `check`, `subscribe` |
|
|
76
78
|
| `client.billing` | `usage`, `history` |
|
|
77
79
|
| `client.audit` | `query` |
|
|
78
80
|
| `client.org` | `listMembers`, `updateMemberRole`, `removeMember` |
|
|
79
|
-
| `client.auth` | `login`, `agentToken`, `apiKeyToken`, `google`, `changePassword`, `logout`
|
|
81
|
+
| `client.auth` | `login`, `signup`, `agentToken`, `apiKeyToken`, `google`, `changePassword`, `logout`, `getMe`, `updateMe`, `deleteMe` |
|
|
80
82
|
| `client.apiKeys` | `create`, `list`, `revoke` |
|
|
81
83
|
| `client.x402` | `getPaymentRequirement`, `pay`, `verifyReceipt`, `withPayment` |
|
|
82
84
|
|
|
@@ -112,6 +114,7 @@ The SDK exports a typed error hierarchy for catch-based flows:
|
|
|
112
114
|
| `OneclawError` | any | Base error class |
|
|
113
115
|
| `AuthError` | 401, 403 | Authentication/authorization failure |
|
|
114
116
|
| `PaymentRequiredError` | 402 | x402 payment required (includes `paymentRequirement`) |
|
|
117
|
+
| `ResourceLimitExceededError` | 403 | Tier limit reached (vaults, agents, secrets) |
|
|
115
118
|
| `ApprovalRequiredError` | 403 | Human approval gate triggered |
|
|
116
119
|
| `NotFoundError` | 404 | Resource not found |
|
|
117
120
|
| `RateLimitError` | 429 | Rate limit exceeded |
|
|
@@ -125,13 +128,30 @@ Agents can be granted the ability to sign and broadcast on-chain transactions th
|
|
|
125
128
|
Toggle `crypto_proxy_enabled` when creating or updating an agent:
|
|
126
129
|
|
|
127
130
|
```typescript
|
|
128
|
-
// Register an agent with crypto proxy access
|
|
131
|
+
// Register an API key agent with crypto proxy access (default auth_method)
|
|
129
132
|
const { data } = await client.agents.create({
|
|
130
133
|
name: "defi-bot",
|
|
131
|
-
auth_method: "api_key",
|
|
134
|
+
auth_method: "api_key", // "api_key" | "mtls" | "oidc_client_credentials"
|
|
132
135
|
scopes: ["vault:read", "tx:sign"],
|
|
133
136
|
crypto_proxy_enabled: true,
|
|
134
137
|
});
|
|
138
|
+
// data.api_key is only returned for auth_method: "api_key"
|
|
139
|
+
// All agents automatically receive an Ed25519 SSH keypair (data.agent.ssh_public_key)
|
|
140
|
+
|
|
141
|
+
// Register an mTLS agent (no API key returned)
|
|
142
|
+
const { data: mtlsAgent } = await client.agents.create({
|
|
143
|
+
name: "mtls-bot",
|
|
144
|
+
auth_method: "mtls",
|
|
145
|
+
client_cert_fingerprint: "sha256-fingerprint-hex",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Register an OIDC agent (no API key returned)
|
|
149
|
+
const { data: oidcAgent } = await client.agents.create({
|
|
150
|
+
name: "oidc-bot",
|
|
151
|
+
auth_method: "oidc_client_credentials",
|
|
152
|
+
oidc_issuer: "https://accounts.google.com",
|
|
153
|
+
oidc_client_id: "your-client-id",
|
|
154
|
+
});
|
|
135
155
|
|
|
136
156
|
// Or enable it later
|
|
137
157
|
await client.agents.update(agentId, {
|
|
@@ -162,13 +182,69 @@ console.log(txRes.data?.signed_tx); // signed raw transaction hex
|
|
|
162
182
|
|
|
163
183
|
The backend fetches the signing key from the vault, signs the EIP-155 transaction, and returns the signed transaction hex. The signing key is decrypted in-memory, used, and immediately zeroized — it never leaves the server.
|
|
164
184
|
|
|
185
|
+
The SDK automatically generates an `Idempotency-Key` header (UUID v4) on each `submitTransaction` call, providing replay protection. Duplicate requests within 24 hours return the cached response instead of re-signing.
|
|
186
|
+
|
|
165
187
|
Key properties:
|
|
166
188
|
|
|
167
189
|
- **Disabled by default** — a human must explicitly enable per-agent
|
|
168
190
|
- **Signing keys never leave the HSM** — same envelope encryption as secrets
|
|
191
|
+
- **Idempotent by default** — each submission includes an auto-generated `Idempotency-Key` header
|
|
169
192
|
- **Every transaction is audit-logged** with full calldata
|
|
170
193
|
- **Revocable instantly** — set `crypto_proxy_enabled: false` to cut off access
|
|
171
194
|
|
|
195
|
+
## Customer-Managed Encryption Keys (CMEK)
|
|
196
|
+
|
|
197
|
+
For enterprises that require cryptographic proof that 1claw cannot access their secrets unilaterally, the SDK provides client-side CMEK utilities. Keys are generated and managed entirely on your side — only the SHA-256 fingerprint is stored on the server.
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { cmek } from "@1claw/sdk";
|
|
201
|
+
|
|
202
|
+
// Generate a 256-bit AES key (returns CryptoKey)
|
|
203
|
+
const key = await cmek.generateCmekKey();
|
|
204
|
+
|
|
205
|
+
// Compute fingerprint (SHA-256 hex)
|
|
206
|
+
const fingerprint = await cmek.cmekFingerprint(key);
|
|
207
|
+
|
|
208
|
+
// Enable CMEK on a vault
|
|
209
|
+
await client.vault.enableCmek(vaultId, { fingerprint });
|
|
210
|
+
|
|
211
|
+
// Encrypt a secret value before storing
|
|
212
|
+
const encrypted = await cmek.cmekEncrypt(key, "my-secret-value");
|
|
213
|
+
await client.secrets.set(vaultId, "path/to/secret", encrypted);
|
|
214
|
+
|
|
215
|
+
// Decrypt after retrieving
|
|
216
|
+
const res = await client.secrets.get(vaultId, "path/to/secret");
|
|
217
|
+
const plaintext = await cmek.cmekDecrypt(key, res.data.value);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Server-assisted key rotation
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
await client.vault.rotateCmek(vaultId, oldKey, newKey, {
|
|
224
|
+
new_fingerprint: await cmek.cmekFingerprint(newKey),
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The server re-encrypts all secrets in batches of 100. Poll rotation status:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
const job = await client.vault.getRotationJobStatus(vaultId, jobId);
|
|
232
|
+
console.log(job.data?.status, job.data?.processed, "/", job.data?.total_secrets);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Agent Token Auto-Refresh
|
|
236
|
+
|
|
237
|
+
When using agent credentials (`agentId` + `apiKey`), the SDK automatically refreshes tokens 60 seconds before expiry. No manual token management needed:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
const client = createClient({
|
|
241
|
+
baseUrl: "https://api.1claw.xyz",
|
|
242
|
+
apiKey: "ocv_...",
|
|
243
|
+
agentId: "agent-uuid",
|
|
244
|
+
});
|
|
245
|
+
// Tokens refresh transparently — just make API calls
|
|
246
|
+
```
|
|
247
|
+
|
|
172
248
|
## x402 Payment Protocol
|
|
173
249
|
|
|
174
250
|
When free-tier limits are exceeded, the API returns `402 Payment Required`. The SDK can automatically handle payments if you provide a signer:
|
|
@@ -195,6 +271,47 @@ const client = createClient({
|
|
|
195
271
|
const secret = await client.x402.withPayment("vault-id", "key", signer);
|
|
196
272
|
```
|
|
197
273
|
|
|
274
|
+
## Plugins
|
|
275
|
+
|
|
276
|
+
The SDK supports optional plugin interfaces for extending behavior without modifying the core:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { createClient } from "@1claw/sdk";
|
|
280
|
+
import type { CryptoProvider, AuditSink, PolicyEngine } from "@1claw/sdk";
|
|
281
|
+
|
|
282
|
+
const client = createClient({
|
|
283
|
+
baseUrl: "https://api.1claw.xyz",
|
|
284
|
+
apiKey: "ocv_...",
|
|
285
|
+
plugins: {
|
|
286
|
+
cryptoProvider: myAwsKmsProvider,
|
|
287
|
+
auditSink: mySplunkSink,
|
|
288
|
+
policyEngine: myOpaEngine,
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| Interface | Purpose | Default behavior |
|
|
294
|
+
| ---------------- | ------------------------------------------------------------ | ----------------------------- |
|
|
295
|
+
| `CryptoProvider` | Client-side encryption (encrypt, decrypt, generateKey) | Server-side HSM (no-op) |
|
|
296
|
+
| `AuditSink` | Forward SDK events to external systems (Splunk, Datadog) | No-op (server handles audit) |
|
|
297
|
+
| `PolicyEngine` | Pre-evaluate policies locally before API calls | No-op (server enforces) |
|
|
298
|
+
|
|
299
|
+
Implement any interface in your own package — no PRs to the SDK needed.
|
|
300
|
+
|
|
301
|
+
## OpenAPI Types
|
|
302
|
+
|
|
303
|
+
The SDK's request types are generated from the **OpenAPI 3.1** spec, published as [@1claw/openapi-spec](https://www.npmjs.com/package/@1claw/openapi-spec). Advanced users can access the raw generated types:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
import type { paths, components, operations, ApiSchemas } from "@1claw/sdk";
|
|
307
|
+
|
|
308
|
+
// Access any schema from the spec
|
|
309
|
+
type Vault = ApiSchemas["VaultResponse"];
|
|
310
|
+
type Agent = ApiSchemas["AgentResponse"];
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Regenerate types after spec changes: `npm run generate`
|
|
314
|
+
|
|
198
315
|
## MCP Integration (AI Agents)
|
|
199
316
|
|
|
200
317
|
The SDK exposes MCP-compatible tool definitions for AI agents:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/client.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { OneclawClient, createClient } from "../core/client";
|
|
3
|
+
const originalFetch = globalThis.fetch;
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
globalThis.fetch = originalFetch;
|
|
6
|
+
});
|
|
7
|
+
function mockFetch(status, body) {
|
|
8
|
+
return vi.fn().mockResolvedValue({
|
|
9
|
+
ok: status >= 200 && status < 300,
|
|
10
|
+
status,
|
|
11
|
+
headers: new Headers(),
|
|
12
|
+
json: () => Promise.resolve(body),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
describe("OneclawClient", () => {
|
|
16
|
+
it("initializes all 14 resource properties", () => {
|
|
17
|
+
globalThis.fetch = mockFetch(200, {});
|
|
18
|
+
const client = new OneclawClient({ baseUrl: "https://api.test", token: "t" });
|
|
19
|
+
expect(client.vault).toBeDefined();
|
|
20
|
+
expect(client.secrets).toBeDefined();
|
|
21
|
+
expect(client.access).toBeDefined();
|
|
22
|
+
expect(client.agents).toBeDefined();
|
|
23
|
+
expect(client.sharing).toBeDefined();
|
|
24
|
+
expect(client.approvals).toBeDefined();
|
|
25
|
+
expect(client.billing).toBeDefined();
|
|
26
|
+
expect(client.audit).toBeDefined();
|
|
27
|
+
expect(client.org).toBeDefined();
|
|
28
|
+
expect(client.auth).toBeDefined();
|
|
29
|
+
expect(client.apiKeys).toBeDefined();
|
|
30
|
+
expect(client.chains).toBeDefined();
|
|
31
|
+
expect(client.x402).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
it("auto-authenticates with agent apiKey + agentId on first request", async () => {
|
|
34
|
+
const fetcher = vi.fn()
|
|
35
|
+
.mockResolvedValueOnce({
|
|
36
|
+
ok: true,
|
|
37
|
+
status: 200,
|
|
38
|
+
headers: new Headers(),
|
|
39
|
+
json: () => Promise.resolve({ access_token: "agent-jwt" }),
|
|
40
|
+
})
|
|
41
|
+
.mockResolvedValueOnce({
|
|
42
|
+
ok: true,
|
|
43
|
+
status: 200,
|
|
44
|
+
headers: new Headers(),
|
|
45
|
+
json: () => Promise.resolve({ vaults: [] }),
|
|
46
|
+
});
|
|
47
|
+
globalThis.fetch = fetcher;
|
|
48
|
+
const client = new OneclawClient({
|
|
49
|
+
baseUrl: "https://api.test",
|
|
50
|
+
apiKey: "ocv_abc",
|
|
51
|
+
agentId: "agent-uuid",
|
|
52
|
+
});
|
|
53
|
+
// Agent auth is lazy — fires on first request, not constructor
|
|
54
|
+
await client.vault.list();
|
|
55
|
+
const [url, init] = fetcher.mock.calls[0];
|
|
56
|
+
expect(url).toContain("/v1/auth/agent-token");
|
|
57
|
+
const body = JSON.parse(init.body);
|
|
58
|
+
expect(body.agent_id).toBe("agent-uuid");
|
|
59
|
+
expect(body.api_key).toBe("ocv_abc");
|
|
60
|
+
});
|
|
61
|
+
it("auto-authenticates with user apiKey (no agentId)", async () => {
|
|
62
|
+
const fetcher = mockFetch(200, { access_token: "user-jwt" });
|
|
63
|
+
globalThis.fetch = fetcher;
|
|
64
|
+
new OneclawClient({
|
|
65
|
+
baseUrl: "https://api.test",
|
|
66
|
+
apiKey: "1ck_abc",
|
|
67
|
+
});
|
|
68
|
+
await vi.waitFor(() => expect(fetcher).toHaveBeenCalled());
|
|
69
|
+
const [url] = fetcher.mock.calls[0];
|
|
70
|
+
expect(url).toContain("/v1/auth/api-key-token");
|
|
71
|
+
});
|
|
72
|
+
it("skips auto-auth when token is already provided", () => {
|
|
73
|
+
const fetcher = mockFetch(200, {});
|
|
74
|
+
globalThis.fetch = fetcher;
|
|
75
|
+
new OneclawClient({
|
|
76
|
+
baseUrl: "https://api.test",
|
|
77
|
+
token: "existing-jwt",
|
|
78
|
+
apiKey: "ocv_abc",
|
|
79
|
+
});
|
|
80
|
+
expect(fetcher).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
it("skips auto-auth when no apiKey is provided", () => {
|
|
83
|
+
const fetcher = mockFetch(200, {});
|
|
84
|
+
globalThis.fetch = fetcher;
|
|
85
|
+
new OneclawClient({
|
|
86
|
+
baseUrl: "https://api.test",
|
|
87
|
+
token: "jwt",
|
|
88
|
+
});
|
|
89
|
+
expect(fetcher).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("createClient", () => {
|
|
93
|
+
it("returns an OneclawClient instance", () => {
|
|
94
|
+
globalThis.fetch = mockFetch(200, {});
|
|
95
|
+
const client = createClient({ baseUrl: "https://api.test", token: "t" });
|
|
96
|
+
expect(client).toBeInstanceOf(OneclawClient);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.test.js","sourceRoot":"","sources":["../../src/__tests__/client.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { OneclawError, AuthError, PaymentRequiredError, ApprovalRequiredError, NotFoundError, RateLimitError, ValidationError, ServerError, errorFromResponse, } from "../core/errors";
|
|
3
|
+
describe("Error classes", () => {
|
|
4
|
+
it("OneclawError stores status, type, and detail", () => {
|
|
5
|
+
const err = new OneclawError("msg", 418, "teapot", "short and stout");
|
|
6
|
+
expect(err.message).toBe("msg");
|
|
7
|
+
expect(err.status).toBe(418);
|
|
8
|
+
expect(err.type).toBe("teapot");
|
|
9
|
+
expect(err.detail).toBe("short and stout");
|
|
10
|
+
expect(err.name).toBe("OneclawError");
|
|
11
|
+
expect(err).toBeInstanceOf(Error);
|
|
12
|
+
});
|
|
13
|
+
it("AuthError defaults to 401", () => {
|
|
14
|
+
const err = new AuthError("bad token");
|
|
15
|
+
expect(err.status).toBe(401);
|
|
16
|
+
expect(err.type).toBe("auth_error");
|
|
17
|
+
});
|
|
18
|
+
it("AuthError accepts 403", () => {
|
|
19
|
+
const err = new AuthError("forbidden", 403);
|
|
20
|
+
expect(err.status).toBe(403);
|
|
21
|
+
});
|
|
22
|
+
it("PaymentRequiredError includes paymentRequirement", () => {
|
|
23
|
+
const req = { x402Version: 1, accepts: [], description: "pay" };
|
|
24
|
+
const err = new PaymentRequiredError("pay up", req);
|
|
25
|
+
expect(err.status).toBe(402);
|
|
26
|
+
expect(err.paymentRequirement).toBe(req);
|
|
27
|
+
});
|
|
28
|
+
it("ApprovalRequiredError includes approvalRequestId", () => {
|
|
29
|
+
const err = new ApprovalRequiredError("req-123");
|
|
30
|
+
expect(err.status).toBe(403);
|
|
31
|
+
expect(err.approvalRequestId).toBe("req-123");
|
|
32
|
+
expect(err.message).toContain("approval");
|
|
33
|
+
});
|
|
34
|
+
it("NotFoundError defaults message", () => {
|
|
35
|
+
const err = new NotFoundError();
|
|
36
|
+
expect(err.status).toBe(404);
|
|
37
|
+
expect(err.message).toBe("Resource not found");
|
|
38
|
+
});
|
|
39
|
+
it("RateLimitError includes retryAfterMs", () => {
|
|
40
|
+
const err = new RateLimitError("slow down", 5000);
|
|
41
|
+
expect(err.status).toBe(429);
|
|
42
|
+
expect(err.retryAfterMs).toBe(5000);
|
|
43
|
+
});
|
|
44
|
+
it("ValidationError includes fields", () => {
|
|
45
|
+
const err = new ValidationError("bad input", { name: "required" });
|
|
46
|
+
expect(err.status).toBe(400);
|
|
47
|
+
expect(err.fields).toEqual({ name: "required" });
|
|
48
|
+
});
|
|
49
|
+
it("ServerError defaults to 500", () => {
|
|
50
|
+
const err = new ServerError();
|
|
51
|
+
expect(err.status).toBe(500);
|
|
52
|
+
expect(err.type).toBe("server_error");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe("errorFromResponse", () => {
|
|
56
|
+
function fakeResponse(status, body, headers) {
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
ok: status >= 200 && status < 300,
|
|
60
|
+
headers: new Headers(headers),
|
|
61
|
+
json: () => Promise.resolve(body),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
it("maps 400 to ValidationError", async () => {
|
|
65
|
+
const err = await errorFromResponse(fakeResponse(400, { detail: "bad field" }));
|
|
66
|
+
expect(err).toBeInstanceOf(ValidationError);
|
|
67
|
+
expect(err.message).toBe("bad field");
|
|
68
|
+
});
|
|
69
|
+
it("maps 401 to AuthError", async () => {
|
|
70
|
+
const err = await errorFromResponse(fakeResponse(401, { detail: "expired" }));
|
|
71
|
+
expect(err).toBeInstanceOf(AuthError);
|
|
72
|
+
expect(err.status).toBe(401);
|
|
73
|
+
});
|
|
74
|
+
it("maps 403 to AuthError", async () => {
|
|
75
|
+
const err = await errorFromResponse(fakeResponse(403, { detail: "forbidden" }));
|
|
76
|
+
expect(err).toBeInstanceOf(AuthError);
|
|
77
|
+
expect(err.status).toBe(403);
|
|
78
|
+
});
|
|
79
|
+
it("maps 402 to PaymentRequiredError", async () => {
|
|
80
|
+
const body = { x402Version: 1, accepts: [], description: "pay" };
|
|
81
|
+
const err = await errorFromResponse(fakeResponse(402, body));
|
|
82
|
+
expect(err).toBeInstanceOf(PaymentRequiredError);
|
|
83
|
+
});
|
|
84
|
+
it("maps 404 to NotFoundError", async () => {
|
|
85
|
+
const err = await errorFromResponse(fakeResponse(404, { detail: "gone" }));
|
|
86
|
+
expect(err).toBeInstanceOf(NotFoundError);
|
|
87
|
+
});
|
|
88
|
+
it("maps 429 to RateLimitError with Retry-After header", async () => {
|
|
89
|
+
const err = await errorFromResponse(fakeResponse(429, { detail: "too many" }, { "Retry-After": "5" }));
|
|
90
|
+
expect(err).toBeInstanceOf(RateLimitError);
|
|
91
|
+
expect(err.retryAfterMs).toBe(5000);
|
|
92
|
+
});
|
|
93
|
+
it("maps 500 to ServerError", async () => {
|
|
94
|
+
const err = await errorFromResponse(fakeResponse(500, { detail: "boom" }));
|
|
95
|
+
expect(err).toBeInstanceOf(ServerError);
|
|
96
|
+
});
|
|
97
|
+
it("maps 502 to ServerError", async () => {
|
|
98
|
+
const err = await errorFromResponse(fakeResponse(502, {}));
|
|
99
|
+
expect(err).toBeInstanceOf(ServerError);
|
|
100
|
+
});
|
|
101
|
+
it("maps unknown status to generic OneclawError", async () => {
|
|
102
|
+
const err = await errorFromResponse(fakeResponse(418, { detail: "teapot" }));
|
|
103
|
+
expect(err).toBeInstanceOf(OneclawError);
|
|
104
|
+
expect(err.type).toBe("unknown");
|
|
105
|
+
});
|
|
106
|
+
it("falls back to 'HTTP {status}' when body has no message", async () => {
|
|
107
|
+
const err = await errorFromResponse(fakeResponse(500, {}));
|
|
108
|
+
expect(err.message).toBe("HTTP 500");
|
|
109
|
+
});
|
|
110
|
+
it("reads message field as fallback", async () => {
|
|
111
|
+
const err = await errorFromResponse(fakeResponse(400, { message: "from message" }));
|
|
112
|
+
expect(err.message).toBe("from message");
|
|
113
|
+
});
|
|
114
|
+
it("handles non-JSON response gracefully", async () => {
|
|
115
|
+
const res = {
|
|
116
|
+
status: 500,
|
|
117
|
+
ok: false,
|
|
118
|
+
headers: new Headers(),
|
|
119
|
+
json: () => Promise.reject(new Error("not json")),
|
|
120
|
+
};
|
|
121
|
+
const err = await errorFromResponse(res);
|
|
122
|
+
expect(err.message).toBe("HTTP 500");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=errors.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.test.js","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/http.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { HttpClient } from "../core/http";
|
|
3
|
+
import { PaymentRequiredError } from "../core/errors";
|
|
4
|
+
function mockFetch(status, body, headers) {
|
|
5
|
+
return vi.fn().mockResolvedValue({
|
|
6
|
+
ok: status >= 200 && status < 300,
|
|
7
|
+
status,
|
|
8
|
+
headers: new Headers(headers),
|
|
9
|
+
json: () => Promise.resolve(body),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
describe("HttpClient", () => {
|
|
13
|
+
const originalFetch = globalThis.fetch;
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
globalThis.fetch = originalFetch;
|
|
16
|
+
});
|
|
17
|
+
it("sends GET with Bearer token", async () => {
|
|
18
|
+
const fetcher = mockFetch(200, { id: "v1" });
|
|
19
|
+
globalThis.fetch = fetcher;
|
|
20
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "tok123" });
|
|
21
|
+
const res = await http.request("GET", "/v1/vaults");
|
|
22
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
23
|
+
const [url, init] = fetcher.mock.calls[0];
|
|
24
|
+
expect(url).toBe("https://api.test/v1/vaults");
|
|
25
|
+
expect(init.method).toBe("GET");
|
|
26
|
+
expect(init.headers["Authorization"]).toBe("Bearer tok123");
|
|
27
|
+
expect(res.data).toEqual({ id: "v1" });
|
|
28
|
+
expect(res.error).toBeNull();
|
|
29
|
+
expect(res.meta?.status).toBe(200);
|
|
30
|
+
});
|
|
31
|
+
it("sends POST with JSON body", async () => {
|
|
32
|
+
const fetcher = mockFetch(201, { id: "new" });
|
|
33
|
+
globalThis.fetch = fetcher;
|
|
34
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "t" });
|
|
35
|
+
await http.request("POST", "/v1/vaults", { body: { name: "test" } });
|
|
36
|
+
const [, init] = fetcher.mock.calls[0];
|
|
37
|
+
expect(init.body).toBe(JSON.stringify({ name: "test" }));
|
|
38
|
+
expect(init.headers["Content-Type"]).toBe("application/json");
|
|
39
|
+
});
|
|
40
|
+
it("omits Authorization when no token", async () => {
|
|
41
|
+
const fetcher = mockFetch(200, {});
|
|
42
|
+
globalThis.fetch = fetcher;
|
|
43
|
+
const http = new HttpClient({ baseUrl: "https://api.test" });
|
|
44
|
+
await http.request("GET", "/v1/health");
|
|
45
|
+
const [, init] = fetcher.mock.calls[0];
|
|
46
|
+
expect(init.headers["Authorization"]).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it("strips trailing slash from baseUrl", async () => {
|
|
49
|
+
const fetcher = mockFetch(200, {});
|
|
50
|
+
globalThis.fetch = fetcher;
|
|
51
|
+
const http = new HttpClient({ baseUrl: "https://api.test/" });
|
|
52
|
+
await http.request("GET", "/v1/health");
|
|
53
|
+
expect(fetcher.mock.calls[0][0]).toBe("https://api.test/v1/health");
|
|
54
|
+
});
|
|
55
|
+
it("appends query parameters and skips undefined values", async () => {
|
|
56
|
+
const fetcher = mockFetch(200, {});
|
|
57
|
+
globalThis.fetch = fetcher;
|
|
58
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "t" });
|
|
59
|
+
await http.request("GET", "/v1/secrets", {
|
|
60
|
+
query: { prefix: "db", limit: 10, missing: undefined },
|
|
61
|
+
});
|
|
62
|
+
const url = new URL(fetcher.mock.calls[0][0]);
|
|
63
|
+
expect(url.searchParams.get("prefix")).toBe("db");
|
|
64
|
+
expect(url.searchParams.get("limit")).toBe("10");
|
|
65
|
+
expect(url.searchParams.has("missing")).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it("returns null data for 204 No Content", async () => {
|
|
68
|
+
globalThis.fetch = mockFetch(204, null);
|
|
69
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "t" });
|
|
70
|
+
const res = await http.request("DELETE", "/v1/vaults/abc");
|
|
71
|
+
expect(res.data).toBeNull();
|
|
72
|
+
expect(res.error).toBeNull();
|
|
73
|
+
expect(res.meta?.status).toBe(204);
|
|
74
|
+
});
|
|
75
|
+
it("returns error envelope on non-ok responses", async () => {
|
|
76
|
+
globalThis.fetch = mockFetch(404, { detail: "Vault not found" });
|
|
77
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "t" });
|
|
78
|
+
const res = await http.request("GET", "/v1/vaults/missing");
|
|
79
|
+
expect(res.data).toBeNull();
|
|
80
|
+
expect(res.error).toBeTruthy();
|
|
81
|
+
expect(res.error?.type).toBe("not_found");
|
|
82
|
+
expect(res.error?.message).toBe("Vault not found");
|
|
83
|
+
expect(res.meta?.status).toBe(404);
|
|
84
|
+
});
|
|
85
|
+
it("requestOrThrow throws on error responses", async () => {
|
|
86
|
+
globalThis.fetch = mockFetch(401, { detail: "Invalid token" });
|
|
87
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "bad" });
|
|
88
|
+
await expect(http.requestOrThrow("GET", "/v1/vaults")).rejects.toThrow("Invalid token");
|
|
89
|
+
});
|
|
90
|
+
it("requestOrThrow returns data on success", async () => {
|
|
91
|
+
globalThis.fetch = mockFetch(200, { vaults: [] });
|
|
92
|
+
const http = new HttpClient({ baseUrl: "https://api.test", token: "t" });
|
|
93
|
+
const data = await http.requestOrThrow("GET", "/v1/vaults");
|
|
94
|
+
expect(data.vaults).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
it("setToken/getToken updates the auth header", async () => {
|
|
97
|
+
const fetcher = mockFetch(200, {});
|
|
98
|
+
globalThis.fetch = fetcher;
|
|
99
|
+
const http = new HttpClient({ baseUrl: "https://api.test" });
|
|
100
|
+
expect(http.getToken()).toBeUndefined();
|
|
101
|
+
http.setToken("new-jwt");
|
|
102
|
+
expect(http.getToken()).toBe("new-jwt");
|
|
103
|
+
await http.request("GET", "/v1/vaults");
|
|
104
|
+
expect(fetcher.mock.calls[0][1].headers["Authorization"]).toBe("Bearer new-jwt");
|
|
105
|
+
});
|
|
106
|
+
describe("x402 auto-payment", () => {
|
|
107
|
+
const paymentRequirement = {
|
|
108
|
+
x402Version: 1,
|
|
109
|
+
accepts: [
|
|
110
|
+
{
|
|
111
|
+
scheme: "exact",
|
|
112
|
+
network: "eip155:8453",
|
|
113
|
+
payTo: "0xabc",
|
|
114
|
+
price: "0.001",
|
|
115
|
+
requiredDeadlineSeconds: 60,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
description: "Pay per query",
|
|
119
|
+
};
|
|
120
|
+
it("retries with X-PAYMENT header when signer is configured", async () => {
|
|
121
|
+
const signer = {
|
|
122
|
+
getAddress: vi.fn().mockResolvedValue("0xsigner"),
|
|
123
|
+
signPayment: vi.fn().mockResolvedValue("sig-bytes"),
|
|
124
|
+
};
|
|
125
|
+
let callCount = 0;
|
|
126
|
+
globalThis.fetch = vi.fn().mockImplementation(() => {
|
|
127
|
+
callCount++;
|
|
128
|
+
if (callCount === 1) {
|
|
129
|
+
return Promise.resolve({
|
|
130
|
+
ok: false,
|
|
131
|
+
status: 402,
|
|
132
|
+
headers: new Headers(),
|
|
133
|
+
json: () => Promise.resolve(paymentRequirement),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return Promise.resolve({
|
|
137
|
+
ok: true,
|
|
138
|
+
status: 200,
|
|
139
|
+
headers: new Headers(),
|
|
140
|
+
json: () => Promise.resolve({ value: "secret" }),
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
const http = new HttpClient({
|
|
144
|
+
baseUrl: "https://api.test",
|
|
145
|
+
token: "t",
|
|
146
|
+
x402Signer: signer,
|
|
147
|
+
maxAutoPayUsd: 1,
|
|
148
|
+
});
|
|
149
|
+
const res = await http.request("GET", "/v1/vaults/v1/secrets/key");
|
|
150
|
+
expect(signer.signPayment).toHaveBeenCalledWith(paymentRequirement.accepts[0]);
|
|
151
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
152
|
+
const retryHeaders = globalThis.fetch.mock.calls[1][1].headers;
|
|
153
|
+
expect(retryHeaders["X-PAYMENT"]).toBeDefined();
|
|
154
|
+
expect(res.data).toEqual({ value: "secret" });
|
|
155
|
+
});
|
|
156
|
+
it("throws PaymentRequiredError when maxAutoPayUsd is 0", async () => {
|
|
157
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
158
|
+
ok: false,
|
|
159
|
+
status: 402,
|
|
160
|
+
headers: new Headers(),
|
|
161
|
+
json: () => Promise.resolve(paymentRequirement),
|
|
162
|
+
});
|
|
163
|
+
const signer = {
|
|
164
|
+
getAddress: vi.fn(),
|
|
165
|
+
signPayment: vi.fn(),
|
|
166
|
+
};
|
|
167
|
+
const http = new HttpClient({
|
|
168
|
+
baseUrl: "https://api.test",
|
|
169
|
+
token: "t",
|
|
170
|
+
x402Signer: signer,
|
|
171
|
+
maxAutoPayUsd: 0,
|
|
172
|
+
});
|
|
173
|
+
await expect(http.request("GET", "/v1/test")).rejects.toThrow(PaymentRequiredError);
|
|
174
|
+
});
|
|
175
|
+
it("throws when payment exceeds maxAutoPayUsd", async () => {
|
|
176
|
+
const expensiveRequirement = {
|
|
177
|
+
...paymentRequirement,
|
|
178
|
+
accepts: [{ ...paymentRequirement.accepts[0], price: "100.0" }],
|
|
179
|
+
};
|
|
180
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
181
|
+
ok: false,
|
|
182
|
+
status: 402,
|
|
183
|
+
headers: new Headers(),
|
|
184
|
+
json: () => Promise.resolve(expensiveRequirement),
|
|
185
|
+
});
|
|
186
|
+
const signer = {
|
|
187
|
+
getAddress: vi.fn(),
|
|
188
|
+
signPayment: vi.fn(),
|
|
189
|
+
};
|
|
190
|
+
const http = new HttpClient({
|
|
191
|
+
baseUrl: "https://api.test",
|
|
192
|
+
token: "t",
|
|
193
|
+
x402Signer: signer,
|
|
194
|
+
maxAutoPayUsd: 1,
|
|
195
|
+
});
|
|
196
|
+
await expect(http.request("GET", "/v1/test")).rejects.toThrow(/exceeds auto-pay limit/);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
//# sourceMappingURL=http.test.js.map
|