On this page

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.

bash
git clone https://github.com/Longfordjames/Claude-1 && cd Claude-1
npm install ./sdk/js

Node

js
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.

js
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

html
<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.

js
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 parameters

Per-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

SettingOptionEnvironmentFiles (Node entry only)Default
Tokentoken (null forces anonymous)REUSEFORGE_TOKENcredentials.json (mode 0600)anonymous
API rootbaseUrlREUSEFORGE_API_URL.reuseforge/config.json, user config.json, credentialshttps://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

js
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

js
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 ETag

A 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, 429 and 503 are always retried. 500, 502 and 504 are retried for GET, PUT, DELETE and for any call that carries an Idempotency-Key. Other 4xx are never retried, including 409.
  • The delay is min(8 s, 0.5 s * 2^attempt) with full jitter. A Retry-After header wins up to 60 seconds; a longer one raises the error immediately.
  • Every operation that needs an Idempotency-Key (createOrder, payOrder, createToken and so on) gets one generated per call and reused across its retries, so a retry cannot double-spend. Pass idempotencyKey to make a call replay-safe across process restarts. See idempotency.
  • signal aborts the call and its retries. timeout is per attempt and surfaces as NetworkError with code: '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.

ClassWhen
AuthenticationError401
ForbiddenError403, with requiredScopes
SpendLimitError403 spend_limit_exceeded, with limitCents and spentCents
PaymentRequiredError402, with orderHint
NotFoundError404, 410
ConflictError409, 412, 428, with currentEtag, state, retryAfter
ValidationError400, 413, 415, 422
RateLimitError429, with retryAfter seconds
ServerError5xx, including a proxy's HTML error page (code internal)
NetworkErrorno response, or the per-attempt timeout
IntegrityError, UnsafePathErrora hash did not match, or a destination path escaped the install root
InstallConflictErroran install stopped before writing because local edits or config keys would be overwritten
ManifestError, LockfileError, ConfigErrorreuseforge.json, reuseforge.lock or a config file is invalid
SignatureErrorwebhook verification failed, with reason
WaitTimeoutErrorwaitForOrder or waitForVerification gave up, with last
js
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:

  1. verifies each file's sha256 and size, the bundle content_hash and the descriptor's pinned hash before writing anything (IntegrityError);
  2. refuses destinations that are absolute, contain .., backslashes, NUL or non-NFC names, escape through a symlink or start with ~/ (unless allowHome: true), and never writes through a symlink;
  3. detects local edits (compared with reuseforge.lock) and conflicting config keys, and stops with InstallConflictError unless force: true;
  4. writes temporary files first, then renames, caps modes at 0755 without setuid or setgid, and enforces 100 files, 512 KiB per file and 2 MiB per version;
  5. 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

js
import { webhooks } from '@reuseforge/sdk/webhooks';

const event = await webhooks.verify(rawBody, request.headers.get('reuseforge-signature'), secret);   // throws SignatureError

Pass 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.

js
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

bash
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 types

The 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: