API
JavaScript SDK
The @reuseforge/sdk package for Node, Workers, Deno, Bun and browsers: one method per operation, async iterable lists, typed errors, safe installs, webhooks.
@reuseforge/sdk is an ESM client for the whole API with zero runtime dependencies. The main entry uses only global fetch and Web Crypto, so it runs on Node 18+, Cloudflare Workers, Deno, Bun and in browsers. The Node entry adds credentials from the files the rf CLI writes, a safe install(), and the lockfile and manifest readers. It behaves exactly like the Python SDK.
Not on npm yet. Install it from a checkout of the repository: npm install ./sdk/js. Every import below is the package's own entry point, so the code is the same once it is published.
git clone https://github.com/Longfordjames/Claude-1 && cd Claude-1
npm install ./sdk/jsNode
import { ReuseForge, install } from '@reuseforge/sdk/node';
const client = new ReuseForge(); // token: REUSEFORGE_TOKEN, else the file written by `rf login`, else anonymous
for await (const item of client.searchItems({ q: 'invoice pdf', kind: ['skill'], max_price_cents: 0, maxItems: 20 })) {
console.log(item.full_name, item.price_cents);
}
const item = await client.getItem('acme/invoice-extractor');
// Download, verify every hash, write atomically, record in reuseforge.lock
const result = await install(client, 'acme/invoice-extractor', { target: 'claude-code', dest: process.cwd(), version: '^1.2.0' });
console.log(result.files.map((f) => `${f.status} ${f.path}`));Workers, Deno, Bun
Import the main entry. It has no node: imports and no filesystem access, so there is no config file and no install(); pass the token explicitly.
import { ReuseForge } from '@reuseforge/sdk';
export default {
async fetch(request, env) {
const client = new ReuseForge({ token: env.REUSEFORGE_TOKEN });
const plan = await client.planGoal({ goal: 'PDF invoice extraction', budget_cents: 2000 });
return Response.json(plan.recommended);
},
};Browsers
<script type="module">
import { ReuseForge } from 'https://esm.sh/@reuseforge/sdk';
const client = new ReuseForge({ token: null }); // anonymous reads
const page = await client.searchItems({ q: 'rag', limit: 5 });
document.body.append(...page.data.map((i) => Object.assign(document.createElement('p'), { textContent: i.full_name })));
</script>Do not ship a token with write or payment scopes to a browser. For a signed-in experience call the API from your server, or mint a short-lived token restricted to items:read. The client refuses to send a token over plain http to anything but a loopback host. (The CDN URL above works once the package is published; until then bundle it yourself. A page on a site with a strict Content-Security-Policy, like this one, cannot load it from a CDN at all.)
Calling operations
One method per operation: the camelCase of its operationId. Results are the JSON the server sent, with wire keys: price_cents, not priceCents.
await client.searchItems({ q: 'x', kind: ['skill', 'harness'], limit: 10 }); // query parameters in one object
await client.getItem('acme/invoice-extractor'); // path arguments are positional; owner/slug is one argument
await client.getVersion({ full_name: 'acme/invoice-extractor' }, '1.4.0'); // an Item object works too
await client.planGoal({ goal: 'RAG chatbot', budget_cents: 5000 }); // body fields in the same object
await client.payOrder(order.id, { idempotencyKey: 'my-key', signal }); // per-call options sit beside the parametersPer-call options: idempotencyKey, ifMatch, etag, signal, timeout, maxRetries, headers, maxItems. client.withOptions({ maxRetries: 0, timeout: 5000 }) returns a view of the same client with other defaults. client.lastResponse has status, requestId, etag, rateLimit and headers of the most recent response. client.request({ method, path, query, body }) is an escape hatch for endpoints newer than the SDK, with the same auth, retries and error mapping.
Configuration
| Setting | Option | Environment | Files (Node entry only) | Default |
|---|---|---|---|---|
| Token | token (null forces anonymous) | REUSEFORGE_TOKEN | credentials.json (mode 0600) | anonymous |
| API root | baseUrl | REUSEFORGE_API_URL | .reuseforge/config.json, user config.json, credentials | https://reuseforge.com/api/v1 |
Other options: timeout (ms per attempt, default 30000; 60000 for planGoal, discoverExternal and payOrder), maxRetries (default 2), fetch, userAgent, headers, logger. REUSEFORGE_LOG=debug logs requests to stderr. The token is kept in a private field and printed as rf_...abcd by toString(), JSON.stringify(), util.inspect() and in error messages.
Pagination
const first = await client.listOrders({ limit: 20 }); // a Page: first.data, first.has_more, first.next_cursor
const next = await first.nextPage(); // null when there is none
for await (const order of client.listOrders()) {} // every item, across pages, lazily
for await (const page of client.listOrders().pages()) {}
const everything = await client.listOrders({ maxItems: 500 }).toArray();A cursor that repeats stops the iteration. See pagination.
ETags and money
const item = await client.getItem('acme/x');
const again = await client.getItem('acme/x', { etag: client.lastResponse.etag }); // null on 304
await client.updateItem('acme/x', { summary: 'new', ifMatch: client.lastResponse.etag });
await client.updateItem('acme/x', { summary: 'new', ifMatch: 'auto' }); // reads the item, then sends its ETagA 412 precondition_failed is a ConflictError whose currentEtag is the value to retry with. Amounts are integer cents; dollars(1200) is "12.00" and cents("12.50") is 1250. cents rejects floats and sub-cent amounts.
Retries and idempotency
- Network errors,
429and503are always retried.500,502and504are retried forGET,PUT,DELETEand for any call that carries anIdempotency-Key. Other4xxare never retried, including409. - The delay is
min(8 s, 0.5 s * 2^attempt)with full jitter. ARetry-Afterheader wins up to 60 seconds; a longer one raises the error immediately. - Every operation that needs an
Idempotency-Key(createOrder,payOrder,createTokenand so on) gets one generated per call and reused across its retries, so a retry cannot double-spend. PassidempotencyKeyto make a call replay-safe across process restarts. See idempotency. signalaborts the call and its retries.timeoutis per attempt and surfaces asNetworkErrorwithcode: 'timeout'.
Errors
Every failure is a subclass of ReuseForgeError with status, code, title, detail, requestId, errors[] ({pointer, code, message}) and retryable. Import them from @reuseforge/sdk or @reuseforge/sdk/errors.
| Class | When |
|---|---|
AuthenticationError | 401 |
ForbiddenError | 403, with requiredScopes |
SpendLimitError | 403 spend_limit_exceeded, with limitCents and spentCents |
PaymentRequiredError | 402, with orderHint |
NotFoundError | 404, 410 |
ConflictError | 409, 412, 428, with currentEtag, state, retryAfter |
ValidationError | 400, 413, 415, 422 |
RateLimitError | 429, with retryAfter seconds |
ServerError | 5xx, including a proxy's HTML error page (code internal) |
NetworkError | no response, or the per-attempt timeout |
IntegrityError, UnsafePathError | a hash did not match, or a destination path escaped the install root |
InstallConflictError | an install stopped before writing because local edits or config keys would be overwritten |
ManifestError, LockfileError, ConfigError | reuseforge.json, reuseforge.lock or a config file is invalid |
SignatureError | webhook verification failed, with reason |
WaitTimeoutError | waitForOrder or waitForVerification gave up, with last |
try {
await client.payOrder(id);
} catch (e) {
if (e instanceof SpendLimitError) console.error(`limit ${e.limitCents}, spent ${e.spentCents}`);
else if (e instanceof RateLimitError) console.error(`retry in ${e.retryAfter}s`);
else throw e;
}Install (Node)
install(client, item, options) fetches the install descriptor and the bundle, then:
- verifies each file's
sha256and size, the bundlecontent_hashand the descriptor's pinned hash before writing anything (IntegrityError); - refuses destinations that are absolute, contain
.., backslashes, NUL or non-NFC names, escape through a symlink or start with~/(unlessallowHome: true), and never writes through a symlink; - detects local edits (compared with
reuseforge.lock) and conflicting config keys, and stops withInstallConflictErrorunlessforce: true; - writes temporary files first, then renames, caps modes at
0755without setuid or setgid, and enforces 100 files, 512 KiB per file and 2 MiB per version; - merges config patches (JSON deep merge, TOML table append or replace, Markdown blocks) and records everything in
reuseforge.lock.
Options: target (generic, claude-code, cursor, codex, mcp-json), dest, version (latest, exact, or a range: ^, ~, comparators, ||, 1.x, hyphen; yanked versions and prereleases are skipped), includePrerelease, force, allowHome, signal. install() never buys and never runs anything; a paid item you do not own raises PaymentRequiredError.
Helpers: Lockfile.read(dir) and lock.verify(dir) (offline drift check), readManifest(dirOrFile), contentHash(files) and verifyBundle(bundle) (the same bytes as sha256sum), and semver.satisfies and semver.maxSatisfying.
Webhooks and waiting
import { webhooks } from '@reuseforge/sdk/webhooks';
const event = await webhooks.verify(rawBody, request.headers.get('reuseforge-signature'), secret); // throws SignatureErrorPass the raw body. Checks run in this order: missing header, malformed header, timestamp outside the tolerance (300 seconds), no matching v1, invalid payload. Comparison is constant time and tries every v1; pass an array of secrets while rotating. webhooks.verifyRequest(request, secret) does the same from a fetch Request, and webhooks.sign() builds a valid header for tests. See webhooks.
const order = await client.waitForOrder(id, { states: ['funded', 'settled'], timeout: 5 * 60_000, signal });
const verification = await client.waitForVerification(vrfId);Polling starts at 1 second and grows by 1.5 times to 10 seconds. A state that can no longer be reached raises ConflictError (invalid_state); running out of time raises WaitTimeoutError.
TypeScript
Types are generated from api/openapi.json and committed (src/types.d.ts); a test fails if they drift from the contract. ItemKind, Order, Plan, WebhookEvent (a union discriminated by type) and the parameter types of every method are exported. Unknown operations, misspelled fields and non-cent amounts fail to compile.
Development
cd sdk/js
npm test # unit, contract, install-safety and example tests (no network)
npm run gen:check # generated files match api/openapi.json
npm run typecheck # tsc over the public typesThe SDK is generated from, and tested against, one contract version (CONTRACT_VERSION is exported). Unknown response fields are preserved, additive API changes need no SDK upgrade, and client.request() reaches new endpoints before the SDK knows them.
Last updated: