@1claw/sdk 0.2.0 → 0.4.0
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 +116 -19
- 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 +86 -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 +4213 -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} +13 -2
- package/dist/resources/agents.d.ts.map +1 -0
- package/dist/{agents.js → resources/agents.js} +18 -0
- 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 +126 -89
- 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:
|
|
@@ -64,21 +66,21 @@ await client.auth.google({ id_token: "..." });
|
|
|
64
66
|
|
|
65
67
|
## API Resources
|
|
66
68
|
|
|
67
|
-
| Resource | Methods
|
|
68
|
-
| ------------------ |
|
|
69
|
-
| `client.vault` | `create`, `get`, `list`, `delete`
|
|
70
|
-
| `client.secrets` | `set`, `get`, `delete`, `list`, `rotate`
|
|
71
|
-
| `client.access` | `grantHuman`, `grantAgent`, `update`, `revoke`, `listGrants`
|
|
72
|
-
| `client.agents` | `create`, `get`, `list`, `update`, `delete`, `rotateKey`, `submitTransaction`, `getTransaction`, `listTransactions` |
|
|
73
|
-
| `client.chains` | `list`, `get`, `adminList`, `create`, `update`, `delete`
|
|
74
|
-
| `client.sharing` | `create`, `access`, `listOutbound`, `listInbound`, `accept`, `decline`, `revoke`
|
|
75
|
-
| `client.approvals` | `request`, `list`, `approve`, `deny`, `check`, `subscribe`
|
|
76
|
-
| `client.billing` | `usage`, `history`
|
|
77
|
-
| `client.audit` | `query`
|
|
78
|
-
| `client.org` | `listMembers`, `updateMemberRole`, `removeMember`
|
|
79
|
-
| `client.auth` | `login`, `agentToken`, `apiKeyToken`, `google`, `changePassword`, `logout`
|
|
80
|
-
| `client.apiKeys` | `create`, `list`, `revoke`
|
|
81
|
-
| `client.x402` | `getPaymentRequirement`, `pay`, `verifyReceipt`, `withPayment`
|
|
69
|
+
| Resource | Methods |
|
|
70
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `client.vault` | `create`, `get`, `list`, `delete` |
|
|
72
|
+
| `client.secrets` | `set`, `get`, `delete`, `list`, `rotate` |
|
|
73
|
+
| `client.access` | `grantHuman`, `grantAgent`, `update`, `revoke`, `listGrants` |
|
|
74
|
+
| `client.agents` | `create`, `getSelf`, `get`, `list`, `update`, `delete`, `rotateKey`, `submitTransaction`, `getTransaction`, `listTransactions`, `simulateTransaction`, `simulateBundle` |
|
|
75
|
+
| `client.chains` | `list`, `get`, `adminList`, `create`, `update`, `delete` |
|
|
76
|
+
| `client.sharing` | `create`, `access`, `listOutbound`, `listInbound`, `accept`, `decline`, `revoke` |
|
|
77
|
+
| `client.approvals` | `request`, `list`, `approve`, `deny`, `check`, `subscribe` |
|
|
78
|
+
| `client.billing` | `usage`, `history` |
|
|
79
|
+
| `client.audit` | `query` |
|
|
80
|
+
| `client.org` | `listMembers`, `updateMemberRole`, `removeMember` |
|
|
81
|
+
| `client.auth` | `login`, `signup`, `agentToken`, `apiKeyToken`, `google`, `changePassword`, `logout`, `getMe`, `updateMe`, `deleteMe` |
|
|
82
|
+
| `client.apiKeys` | `create`, `list`, `revoke` |
|
|
83
|
+
| `client.x402` | `getPaymentRequirement`, `pay`, `verifyReceipt`, `withPayment` |
|
|
82
84
|
|
|
83
85
|
## Response Envelope
|
|
84
86
|
|
|
@@ -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 |
|
|
@@ -150,14 +153,14 @@ Once `crypto_proxy_enabled` is true and the agent has a signing key stored in an
|
|
|
150
153
|
```typescript
|
|
151
154
|
const txRes = await client.agents.submitTransaction(agentId, {
|
|
152
155
|
to: "0x000000000000000000000000000000000000dEaD",
|
|
153
|
-
value: "0.01",
|
|
156
|
+
value: "0.01", // ETH
|
|
154
157
|
chain: "base",
|
|
155
158
|
// Optional: data, signing_key_path, nonce, gas_price, gas_limit
|
|
156
159
|
});
|
|
157
160
|
|
|
158
|
-
console.log(txRes.data?.status);
|
|
159
|
-
console.log(txRes.data?.tx_hash);
|
|
160
|
-
console.log(txRes.data?.signed_tx);
|
|
161
|
+
console.log(txRes.data?.status); // "signed"
|
|
162
|
+
console.log(txRes.data?.tx_hash); // "0x..."
|
|
163
|
+
console.log(txRes.data?.signed_tx); // signed raw transaction hex
|
|
161
164
|
```
|
|
162
165
|
|
|
163
166
|
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.
|
|
@@ -169,6 +172,59 @@ Key properties:
|
|
|
169
172
|
- **Every transaction is audit-logged** with full calldata
|
|
170
173
|
- **Revocable instantly** — set `crypto_proxy_enabled: false` to cut off access
|
|
171
174
|
|
|
175
|
+
## Customer-Managed Encryption Keys (CMEK)
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { cmek } from "@1claw/sdk";
|
|
181
|
+
|
|
182
|
+
// Generate a 256-bit AES key (returns CryptoKey)
|
|
183
|
+
const key = await cmek.generateCmekKey();
|
|
184
|
+
|
|
185
|
+
// Compute fingerprint (SHA-256 hex)
|
|
186
|
+
const fingerprint = await cmek.cmekFingerprint(key);
|
|
187
|
+
|
|
188
|
+
// Enable CMEK on a vault
|
|
189
|
+
await client.vault.enableCmek(vaultId, { fingerprint });
|
|
190
|
+
|
|
191
|
+
// Encrypt a secret value before storing
|
|
192
|
+
const encrypted = await cmek.cmekEncrypt(key, "my-secret-value");
|
|
193
|
+
await client.secrets.set(vaultId, "path/to/secret", encrypted);
|
|
194
|
+
|
|
195
|
+
// Decrypt after retrieving
|
|
196
|
+
const res = await client.secrets.get(vaultId, "path/to/secret");
|
|
197
|
+
const plaintext = await cmek.cmekDecrypt(key, res.data.value);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Server-assisted key rotation
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
await client.vault.rotateCmek(vaultId, oldKey, newKey, {
|
|
204
|
+
new_fingerprint: await cmek.cmekFingerprint(newKey),
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The server re-encrypts all secrets in batches of 100. Poll rotation status:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const job = await client.vault.getRotationJobStatus(vaultId, jobId);
|
|
212
|
+
console.log(job.data?.status, job.data?.processed, "/", job.data?.total_secrets);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Agent Token Auto-Refresh
|
|
216
|
+
|
|
217
|
+
When using agent credentials (`agentId` + `apiKey`), the SDK automatically refreshes tokens 60 seconds before expiry. No manual token management needed:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const client = createClient({
|
|
221
|
+
baseUrl: "https://api.1claw.xyz",
|
|
222
|
+
apiKey: "ocv_...",
|
|
223
|
+
agentId: "agent-uuid",
|
|
224
|
+
});
|
|
225
|
+
// Tokens refresh transparently — just make API calls
|
|
226
|
+
```
|
|
227
|
+
|
|
172
228
|
## x402 Payment Protocol
|
|
173
229
|
|
|
174
230
|
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 +251,47 @@ const client = createClient({
|
|
|
195
251
|
const secret = await client.x402.withPayment("vault-id", "key", signer);
|
|
196
252
|
```
|
|
197
253
|
|
|
254
|
+
## Plugins
|
|
255
|
+
|
|
256
|
+
The SDK supports optional plugin interfaces for extending behavior without modifying the core:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { createClient } from "@1claw/sdk";
|
|
260
|
+
import type { CryptoProvider, AuditSink, PolicyEngine } from "@1claw/sdk";
|
|
261
|
+
|
|
262
|
+
const client = createClient({
|
|
263
|
+
baseUrl: "https://api.1claw.xyz",
|
|
264
|
+
apiKey: "ocv_...",
|
|
265
|
+
plugins: {
|
|
266
|
+
cryptoProvider: myAwsKmsProvider,
|
|
267
|
+
auditSink: mySplunkSink,
|
|
268
|
+
policyEngine: myOpaEngine,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
| Interface | Purpose | Default behavior |
|
|
274
|
+
| ---------------- | ------------------------------------------------------------ | ----------------------------- |
|
|
275
|
+
| `CryptoProvider` | Client-side encryption (encrypt, decrypt, generateKey) | Server-side HSM (no-op) |
|
|
276
|
+
| `AuditSink` | Forward SDK events to external systems (Splunk, Datadog) | No-op (server handles audit) |
|
|
277
|
+
| `PolicyEngine` | Pre-evaluate policies locally before API calls | No-op (server enforces) |
|
|
278
|
+
|
|
279
|
+
Implement any interface in your own package — no PRs to the SDK needed.
|
|
280
|
+
|
|
281
|
+
## OpenAPI Types
|
|
282
|
+
|
|
283
|
+
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:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import type { paths, components, operations, ApiSchemas } from "@1claw/sdk";
|
|
287
|
+
|
|
288
|
+
// Access any schema from the spec
|
|
289
|
+
type Vault = ApiSchemas["VaultResponse"];
|
|
290
|
+
type Agent = ApiSchemas["AgentResponse"];
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Regenerate types after spec changes: `npm run generate`
|
|
294
|
+
|
|
198
295
|
## MCP Integration (AI Agents)
|
|
199
296
|
|
|
200
297
|
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,86 @@
|
|
|
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", async () => {
|
|
34
|
+
const fetcher = mockFetch(200, { access_token: "agent-jwt" });
|
|
35
|
+
globalThis.fetch = fetcher;
|
|
36
|
+
new OneclawClient({
|
|
37
|
+
baseUrl: "https://api.test",
|
|
38
|
+
apiKey: "ocv_abc",
|
|
39
|
+
agentId: "agent-uuid",
|
|
40
|
+
});
|
|
41
|
+
await vi.waitFor(() => expect(fetcher).toHaveBeenCalled());
|
|
42
|
+
const [url, init] = fetcher.mock.calls[0];
|
|
43
|
+
expect(url).toContain("/v1/auth/agent-token");
|
|
44
|
+
const body = JSON.parse(init.body);
|
|
45
|
+
expect(body.agent_id).toBe("agent-uuid");
|
|
46
|
+
expect(body.api_key).toBe("ocv_abc");
|
|
47
|
+
});
|
|
48
|
+
it("auto-authenticates with user apiKey (no agentId)", async () => {
|
|
49
|
+
const fetcher = mockFetch(200, { access_token: "user-jwt" });
|
|
50
|
+
globalThis.fetch = fetcher;
|
|
51
|
+
new OneclawClient({
|
|
52
|
+
baseUrl: "https://api.test",
|
|
53
|
+
apiKey: "1ck_abc",
|
|
54
|
+
});
|
|
55
|
+
await vi.waitFor(() => expect(fetcher).toHaveBeenCalled());
|
|
56
|
+
const [url] = fetcher.mock.calls[0];
|
|
57
|
+
expect(url).toContain("/v1/auth/api-key-token");
|
|
58
|
+
});
|
|
59
|
+
it("skips auto-auth when token is already provided", () => {
|
|
60
|
+
const fetcher = mockFetch(200, {});
|
|
61
|
+
globalThis.fetch = fetcher;
|
|
62
|
+
new OneclawClient({
|
|
63
|
+
baseUrl: "https://api.test",
|
|
64
|
+
token: "existing-jwt",
|
|
65
|
+
apiKey: "ocv_abc",
|
|
66
|
+
});
|
|
67
|
+
expect(fetcher).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it("skips auto-auth when no apiKey is provided", () => {
|
|
70
|
+
const fetcher = mockFetch(200, {});
|
|
71
|
+
globalThis.fetch = fetcher;
|
|
72
|
+
new OneclawClient({
|
|
73
|
+
baseUrl: "https://api.test",
|
|
74
|
+
token: "jwt",
|
|
75
|
+
});
|
|
76
|
+
expect(fetcher).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("createClient", () => {
|
|
80
|
+
it("returns an OneclawClient instance", () => {
|
|
81
|
+
globalThis.fetch = mockFetch(200, {});
|
|
82
|
+
const client = createClient({ baseUrl: "https://api.test", token: "t" });
|
|
83
|
+
expect(client).toBeInstanceOf(OneclawClient);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
//# 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
|