# @passkey-x/sdk

The official JavaScript and TypeScript SDK for the Passkey-X REST APIs.

- **One client for every API:** the Customer API (`pxu_` personal keys or a session), the Organization API (`pxk_` keys) and the Agent API (`pxa1.` tokens).
- **Production-ready by default:**
  - Retries with exponential backoff and jitter for 429 and 5xx answers and network errors, following `Retry-After`.
  - A timeout on every attempt.
  - An automatic `Idempotency-Key` on every write, so a retried POST never runs twice.
  - `If-Match` revisions for optimistic concurrency.
- **Pagination** with `for await`: every list has `.iterate()`, which follows `next_cursor` for you.
- **Typed errors:** `PasskeyXError` has the HTTP status, the API's error `code`, the `requestId` to quote to support, and `retryAfter`.
- **Webhook verification** (`verifyWebhook`): HMAC-SHA256 over `"<timestamp>.<raw body>"` with a constant-time comparison. It rejects replays and accepts two secrets while you rotate.
- **Observability:** `onRequest` and `onResponse` hooks, and `px.rateLimit` from the last answer.
- **Zero dependencies and complete TypeScript types.**
  - Runs in Node.js 18+, Deno, Bun, Cloudflare Workers, Vercel/Netlify edge functions and modern browsers.
  - Works anywhere with `fetch` and Web Crypto.
- **Zero knowledge:**
  - Vault items travel as ciphertext envelopes; the SDK never handles plaintext secrets.
  - The API rejects bodies that contain fields such as `password`.

## Install

```bash
# From the Passkey-X repository (or the SDK kit download)
npm install ./packages/sdk
```

Without a bundler (Deno, browsers and edge runtimes), import it straight from Passkey-X:

```js
import { PasskeyX } from "https://passkey-x.com/sdk/v1/index.mjs";
```

## Quick start

```js
import { PasskeyX } from "@passkey-x/sdk";

const px = new PasskeyX({ apiKey: process.env.PASSKEY_X_API_KEY }); // pxu_… from Passkey-X → Developers → API keys

const me = await px.me();
console.log(`Key ${me.key?.name}; scopes: ${me.key?.scopes?.join(", ")}; calls come from ${me.your_ip}`);

for (const vault of (await px.workspaces.list()).workspaces ?? []) console.log(vault.id);
```

### Organization API (`pxk_` key)

```js
const org = new PasskeyX({ apiKey: process.env.PASSKEY_X_ORG_KEY });

// Every audit event, page by page, without handling cursors yourself
for await (const event of org.org.auditEvents.iterate({ limit: 500 })) {
  console.log(event.sequence, event.occurred_at, event.action);
}

// Signature requests still waiting, and cancelling one
const waiting = await org.org.signatureRequests.list({ status: "pending" });
await org.org.documents.cancel(waiting.data[0].id, { reason: "Replaced by a new version" });

// Offboarding: suspend a member
await org.org.members.suspend("identity-uuid");
```

### Webhooks

```js
import express from "express";
import { verifyWebhook } from "@passkey-x/sdk/webhooks";

const app = express();
app.post("/passkey-x/webhook", express.raw({ type: "*/*" }), async (req, res) => {
  try {
    await verifyWebhook({ body: req.body, headers: req.headers, secrets: [process.env.PX_WEBHOOK_SECRET] });
  } catch {
    return res.sendStatus(401);
  }
  const { events } = JSON.parse(req.body.toString("utf8"));
  for (const event of events) {
    // Delivery is at-least-once and in order: skip sequences you already processed.
  }
  res.sendStatus(204);
});
```

Create, test and replay webhooks from code with `px.org.webhooks.create / test / replay / deliveries`.

### Errors and retries

```js
import { PasskeyXError } from "@passkey-x/sdk";

try {
  await px.vaultItems.update(itemId, revision, envelope);
} catch (error) {
  if (error instanceof PasskeyXError) {
    // e.g. 409 conflict (someone changed it), 403 insufficient_scope, 403 ip_not_allowed, 429 rate_limited
    console.error(error.status, error.code, error.message, "request id:", error.requestId);
  }
}
```

### Options

| Option | Default | Meaning |
|---|---|---|
| `apiKey` / `accessToken` | (required) | `pxu_…`, `pxk_…`, `pxa1.…` or a session access token |
| `baseUrl` | `https://passkey-x.com/api` | Your own `/api` for a self-hosted server |
| `timeoutMs` | `30000` | Timeout for each attempt (`0` turns it off) |
| `maxRetries` | `2` | Retries for 408, 425, 429, 5xx and network errors |
| `publishableKey` | none | Sessions only: the public `apikey` |
| `fetch` | `globalThis.fetch` | A custom fetch, e.g. for proxies or tests |
| `onRequest`, `onResponse` | none | Logging and metrics hooks; they never receive the token |

Any route that isn't wrapped yet works through `px.request(method, path, init)`, for example `px.request("GET", "/org/v1/storage")`.

## Security notes

- **Keep keys server-side.**
  - Never put a `pxu_` or `pxk_` key in front-end code.
  - In the browser, use the signed-in user's session token.
- **Give each key the fewest scopes it needs**, an expiry and an IP allowlist (Developers → API keys).
- **Rotate webhook secrets** with `webhooks.rotateSecret`. While you switch, pass both the old and new secrets to `verifyWebhook`.
- **Signing pages can't be embedded:** they refuse to load inside an iframe, which protects signers from clickjacking. Open the private signing link in a new tab or window.

## Reference

- API guide: https://passkey-x.com/api-docs
- OpenAPI: https://passkey-x.com/openapi.yaml
- Postman: Passkey-X → Developers → Postman

## Controller

```js
const fleet = await px.org.controller.get();
await px.org.controller.createJob({ kind: "lock_screen", target_kind: "agents", computer_ids: [fleet.data.computers[0].id], reason: "Left unlocked" });
```

Restart, install, wipe and other signed actions are sent from the admin console (Controller).
