# JavaScript SDK: SparkVault API Reference

> Add passwordless authentication or secure file uploads to your web app.

Canonical: https://sparkvault.com/api/docs/sdk-js/ · OpenAPI: https://sparkvault.com/openapi.yaml

## What are you trying to do?

[

### Replace my login system

Passwordless authentication with passkeys, email codes, social login, and more.

](#use-case-login)[

### Accept secure file uploads

Let users upload files that get encrypted and stored in your vault.

](#use-case-upload)[

### Check API availability

Detect whether SparkVault is reachable before starting network-dependent flows.

](#use-case-health)

## Use Case: Replace My Login System

Add passwordless verification in 2 steps. Users verify via passkey, email code, SMS, SparkLink, or social login. Your backend verifies the signed JWT and maps the person by SVID. For a full login/session replacement, use the [OIDC/BFF flow](/api/docs/products/identity/) described in the Identity API docs.

### Step 1: Add this to your HTML

Copy this snippet and edit the highlighted values:

```html
<script
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
></script>

<script>
  SparkVault.products.identity.attach('.login-btn', {  // ← your login button selector
    onSuccess: async (result) => {
      await fetch('/api/auth/login', {             // ← your auth endpoint
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token: result.token })
      });
      window.location.href = '/dashboard';         // ← your success redirect
    }
  });
</script>
```

### Step 2: Verify the token on your backend

> **Security Critical**
>
> **Always verify tokens server-side.** Never trust the frontend result alone.

> **Identity Product Endpoint**
>
> Verification code must use `https://auth.sparkvault.com` for both the JWKS URL and the expected issuer.

```node.js
import * as jose from 'jose';

const ACCOUNT_ID = 'acc_YOUR_ACCOUNT_ID';  // ← your account ID
const IDENTITY_URL = 'https://auth.sparkvault.com';

app.post('/api/auth/login', async (req, res) => {
  const { token } = req.body;

  const jwksUrl = `${IDENTITY_URL}/${ACCOUNT_ID}/.well-known/jwks.json`;
  const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl));

  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: `${IDENTITY_URL}/${ACCOUNT_ID}`,
    audience: ACCOUNT_ID,
    algorithms: ['EdDSA']
  });

  // Token is valid. SVID is the canonical identity key for your app.
  const svid = payload.svid || payload.sub;
  const user = await findOrCreateUserBySvid(svid, {
    identity: payload.identity,
    identityType: payload.identity_type
  });
  req.session.userId = user.id;
  res.json({ success: true });
});
```

```python
import jwt
from jwt import PyJWKClient

ACCOUNT_ID = 'acc_YOUR_ACCOUNT_ID'  # ← your account ID
IDENTITY_URL = 'https://auth.sparkvault.com'

@app.route('/api/auth/login', methods=['POST'])
def login():
    token = request.json.get('token')

    jwks_url = f"{IDENTITY_URL}/{ACCOUNT_ID}/.well-known/jwks.json"
    jwks_client = PyJWKClient(jwks_url)
    signing_key = jwks_client.get_signing_key_from_jwt(token)

    claims = jwt.decode(token, signing_key.key, algorithms=["EdDSA"],
        issuer=f"{IDENTITY_URL}/{ACCOUNT_ID}", audience=ACCOUNT_ID)

    # Token is valid. SVID is the canonical identity key for your app.
    svid = claims.get('svid') or claims['sub']
    user = find_or_create_user_by_svid(
        svid,
        identity=claims['identity'],
        identity_type=claims['identity_type']
    )
    session['user_id'] = user.id
    return jsonify({'success': True})
```

Configure which auth methods to offer in [Identity Product Settings](https://app.sparkvault.com/products/identity).

> **Managed Sessions**
>
> The JavaScript SDK simple flow returns a signed verification proof. To have SparkVault own refresh-token rotation, revocation, and logout, use OIDC with a BFF: exchange codes server-side, keep refresh tokens in `HttpOnly`/`Secure` cookies, validate access JWTs locally with JWKS for normal requests, introspect only for hard checks, and call `/revoke` on logout.

## Use Case: Accept Secure File Uploads

Let users upload files that get encrypted and stored in your vault.

> **Important: File Access**
>
> Uploaded files are **encrypted at rest**. To access them later, you need a [vault member](https://app.sparkvault.com/vaults) with download access or server-side access via the [Ingots API](/api/docs/ingots/). **Public uploaders cannot retrieve files.**

### Step 1: Enable public upload

[Vaults](https://app.sparkvault.com/vaults) → Edit your vault → Enable **Public Ingot Upload**.

### Step 2: Add this to your HTML

```html
<script
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
></script>

<script>
  SparkVault.vaults.upload.attach('.upload-btn', {  // ← your upload button selector
    vaultId: 'vlt_YOUR_VAULT_ID',                   // ← your vault ID
    onSuccess: (result) => {
      console.log('Uploaded:', result.ingotId, result.filename);
    }
  });
</script>
```

> **No code option**
>
> Share the public upload URL from [Vault Settings](https://app.sparkvault.com/vaults) directly, no code required.

### Native integration (advanced)

Drive the upload yourself instead of using the SDK drop-zone. Pass a `File` from your own button or drop target; the SDK shows only the secure-upload progress dialog, then closes silently so your app renders its own result UI. Ideal for chat attachments and similar flows.

```javascript
// e.g. a chat attachment paperclip
fileInput.addEventListener('change', () => {
  SparkVault.vaults.upload({
    vaultId: 'vlt_YOUR_VAULT_ID',
    file: fileInput.files[0],   // skip the SDK drop-zone
    folder: 'chat/thread-42',   // optional vault sub-path, created if missing
    token: vaultAccessToken,    // optional VAT: authorize without public upload
    hideSuccessScreen: true,    // close silently when the upload finishes
    onSuccess: (result) => {
      // your app renders the "attached" UI
      addAttachmentToChat(result.ingotId, result.filename);
    },
  });
});
```

> **upload() options**
>
> `file` skips the drop-zone, `hideSuccessScreen` closes the dialog silently on completion, `folder` targets a vault sub-path (find-or-created), and `token` (a Vault Access Token) authorizes the upload without public upload being enabled.

## Use Case: Check API Availability

Use the SDK health module for lightweight online checks. It calls SparkVault's root health endpoint and returns a structured result instead of throwing on connectivity failures.

```javascript
const health = await SparkVault.health.check();

if (!health.online) {
  console.log('SparkVault is unreachable:', health.error);
}
```

## Installation

### CDN (Recommended)

```html
<script
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
></script>
```

### npm / yarn

```bash
npm install @sparkvault/sdk-js
```

```javascript
import SparkVault from '@sparkvault/sdk-js';

const sparkvault = SparkVault.init({
  accountId: 'acc_YOUR_ACCOUNT_ID'
});
```

> **Browser-Only SDK**
>
> This SDK is for browser applications (it renders UI). For server-side operations, use the [REST API](/api/docs/).

## React Native Mobile SDK

Use `@sparkvault/sdk-mobile` for React Native and Expo apps. It includes the same config-driven Identity Product experience as the web popup: email/phone UI derived from enabled delivery methods, configured branding, light/dark mode, SVID-scoped passkeys, and OTP email/SMS/voice.

```bash
npm install @sparkvault/sdk-mobile
```

```tsx
import { createSparkVaultMobileClient } from '@sparkvault/sdk-mobile';
import { SparkVaultIdentityDialog } from '@sparkvault/sdk-mobile/identity-dialog';

const client = createSparkVaultMobileClient({
  tokenStorage,
  identityAccountId: 'acc_YOUR_ACCOUNT_ID',
  identityBaseUrl: 'https://auth.sparkvault.com',
  apiBaseUrl: 'https://api.sparkvault.com/v1'
});

<SparkVaultIdentityDialog
  client={client}
  visible={visible}
  passkeyProvider={passkeyProvider}
  onCancel={() => setVisible(false)}
  onSuccess={async (result) => {
    await client.products.identity.verifyIdentityToken(result.token, { jwks: result.jwks });
    const session = await client.account.exchangeIdentityToken(result.token);
    // Continue only after local JWT verification and Core exchange succeed.
  }}
/>;
```

> **Token Verification**
>
> Mobile client-side verification protects the app flow before token exchange. Third-party backends must still verify Identity JWTs server-side with the JWKS endpoint and map product records by SVID.

* * *

## Reference

## Identity API Reference

> **Namespaced under products**
>
> Identity is a first-party product and lives at `sparkvault.products.identity`. All methods below are called as `sparkvault.products.identity.signIn(...)`, etc.

### signIn(options?)

Open the sign-in popup. Returns a promise that resolves with the verification result. This is the single programmatic entry point. `attach()` binds it to element clicks. Sign-in always runs in a first-party popup on the Identity origin (so cross-domain SSO works); there is no inline/in-page mode.

```javascript
const result = await sparkvault.products.identity.signIn({
  email: 'user@example.com',  // Pre-fill email (optional)
  onCancel: () => { ... }     // Called if user closes the popup
});

// result.token - Signed JWT (verify on backend!)
// result.identity - Verified email or phone
// result.identityType - 'email', 'phone', or 'social'
// result.svid - Canonical SparkVault ID
// result.sessionId - Managed Identity session ID
```

#### Options

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | Optional | Pre-fill email address |
| `phone` | string | Optional | Pre-fill phone (E.164 format: "+14155551234") |
| `backdropBlur` | boolean | Optional | Override backdrop blur for this popup (uses global config if omitted) |
| `flow` | "auto" | "popup" | "redirect" | Optional | "auto" (default) and "popup" open the popup; "redirect" navigates the tab to the sign-in page instead (its return leg is handleRedirectResult()) |
| `onSuccess` | function | Optional | Callback on success |
| `onError` | function | Optional | Callback on error |
| `onCancel` | function | Optional | Callback when user closes the popup |

#### Result

| Field | Type | Description |
| --- | --- | --- |
| `token` | string | Signed JWT token (Ed25519). Verify on your backend. |
| `identity` | string | Verified email or phone number |
| `identityType` | "email" | "phone" | "social" | Type of identity verified |
| `svid` | string | Canonical SparkVault ID for the person |
| `sessionId` | string | Managed Identity session ID |
| `refreshToken` | string | Present only for flows that intentionally return managed-session refresh material |

### handleRedirectResult(options?)

Call once on every page load. It redeems a returned one-time, PKCE-bound code (from a `flow: 'redirect'` sign-in, or the silent SSO self-heal), never the token itself, and, when a popup's result was lost (e.g. a mobile browser discarded the backgrounded tab mid-login), it silently re-mints from the SSO session so the sign-in still completes. It no-ops (resolves `null`) otherwise. Wiring it is strongly recommended: it is what makes the popup reliable on mobile. The result arrives here as the same shape `signIn()` delivers; verify `result.token` on your backend.

```javascript
// On EVERY page load: redeems a returned code or self-heals a lost popup result.
sparkvault.products.identity.handleRedirectResult({
  onSuccess: (result) => createSession(result.token),  // same handler as signIn()
}).catch((error) => showError(error));
```

### SparkVault.isRecoveryPending()

Static, side-effect-free, and instance-free: returns `true` when this page load has a sign-in to complete: a returned handoff code/error, or a popup whose result was lost. On a high-traffic page where you defer `init()` for performance, call this first so you only boot the SDK when `handleRedirectResult()` actually has work to do. A visitor who never signed in always gets `false`.

```javascript
// Boot the SDK for the return leg ONLY when a sign-in needs completing.
if (SparkVault.isRecoveryPending()) {
  SparkVault.init({ accountId: 'acc_...' })
    .products.identity.handleRedirectResult({
      onSuccess: (result) => createSession(result.token),
    }).catch(showError);
}
```

### attach(selector, options)

Bind verification to element clicks. Returns a cleanup function.

```javascript
const cleanup = sparkvault.products.identity.attach('.login-btn', {
  onSuccess: (result) => { ... },
  onCancel: () => { ... }
});

// Later: remove click handlers
cleanup();
```

### close()

Programmatically close the sign-in popup.

```javascript
sparkvault.products.identity.close();
```

### signInWithFedcm(options?)

One-click sign-in through the browser's native FedCM account chooser. When the person is already signed into auth.sv (carrying the SSO session), the browser surfaces a "Continue as …" prompt with no popup, redirect, or typing. Requires a registered `clientId` in the SDK config. Resolves with the verified result, or `null` when the browser has no FedCM support (so you can fall back to `signIn()`); throws `UserCancelledError` when the person dismisses the chooser. Feature-detect with the static `IdentityModule.isFedcmAvailable()`.

```javascript
import { IdentityModule } from '@sparkvault/sdk-js';

const result = IdentityModule.isFedcmAvailable()
  ? await sparkvault.products.identity.signInWithFedcm()
      .catch(() => sparkvault.products.identity.signIn())
  : await sparkvault.products.identity.signIn();
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `mediation` | 'optional' | 'required' | 'silent' | Optional | FedCM mediation mode passed to the browser's credential request |
| `nonce` | string | Optional | Replay-binding value carried into the token's `nonce` claim (generated when omitted) |

### portalUrl(options?)

Build a deep link into the auth.sv self-service Identity portal, where a signed-in person manages their SparkVault Identity across every SparkVault-integrated site. Returns an absolute URL on the configured `portalBaseUrl` (default `https://auth.sv`).

```javascript
// Manage this site (block, notifications, etc.)
window.location.href = sparkvault.products.identity.portalUrl({ section: 'sites' });

// Open the security screen
const url = sparkvault.products.identity.portalUrl({ section: 'security' });
```

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `section` | string | Optional | Portal screen to open: `home` | `identity` | `security` | `sessions` | `sites`Default: `home` |
| `site` | string | Optional | Focus a specific connected site on the Sites screen (an `acc_…` account ID). When `section` is `sites` and `site` is omitted, defaults to this SDK's own `accountId`. Ignored for other sections. |

### decodeToken(token)

Decode an identity token's claims **without verifying its signature**. This is an unverified, client-side decode: it parses the JWT payload and sanity-checks structure, `exp`, `iss`, `aud`, and the header `alg` (rejecting `none` and any non-EdDSA algorithm). It does not verify the Ed25519 signature, so its result must never be trusted for an authorization decision. Verify tokens server-side against the Identity JWKS endpoint before trusting any claim.

```javascript
const claims = sparkvault.products.identity.decodeToken(result.token);
console.log(claims.svid, claims.exp);
```

### Authentication Methods

Configure which methods to offer in [Identity Product Settings](https://app.sparkvault.com/products/identity).

> **Social redirects**
>
> Social providers use a full-page OAuth redirect. When the provider returns to your page with `#token=...`, `products.identity.signIn()` and `products.identity.attach()` consume that fragment once, clear it from the URL, and call `onSuccess` with the same result shape as passkey, OTP, and SparkLink.

| Method | Description |
| --- | --- |
| `passkey` | WebAuthn biometric or security key |
| `otp_email` | 6-digit code via email |
| `otp_sms` | 6-digit code via SMS |
| `otp_voice` | 6-digit code via voice call |
| `sparklink` | One-click sign-in link via email |
| `social_google` | Sign in with Google |
| `social_apple` | Sign in with Apple |
| `social_microsoft` | Sign in with Microsoft |
| `social_github` | Sign in with GitHub |
| `social_facebook` | Sign in with Facebook |
| `social_linkedin` | Sign in with LinkedIn |

### Token Claims

```json
{
  "iss": "https://auth.sparkvault.com/acc_xxx",
  "sub": "ing_019e66a4e27875f2822ede0e4f5d8792",
  "aud": "acc_xxx",
  "jti": "0192f3a1-7c4e-7b21-b3d4-9a8e6f5c2d10",
  "identity": "user@example.com",
  "identity_type": "email",
  "method": "otp_email",
  "verified_at": 1703977200,
  "svid": "ing_019e66a4e27875f2822ede0e4f5d8792",
  "session_id": "sess_abc123",
  "iat": 1703977200,
  "exp": 1703977500
}
```

Simple tokens expire after 5 minutes by default (`exp` = `iat` + 300). Optional claims (`nonce`, `amr`, `acr`, and `action_hash`) appear only when the flow binds them.

## Upload API Reference

### upload(options)

Open the upload widget. Returns a promise that resolves when upload completes.

```javascript
const result = await sparkvault.vaults.upload({
  vaultId: 'vlt_abc123',
  target: '#container',  // Render inline instead of modal (optional)
  onProgress: (p) => console.log(p.percentage + '%')
});

// result.ingotId - Uploaded file ID
// result.filename - Original filename
// result.sizeBytes - File size
```

#### Options

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vaultId` | string | Required | Vault ID to upload to (public upload must be enabled unless `token` is provided) |
| `target` | HTMLElement | string | Optional | CSS selector or element for inline rendering |
| `file` | File | Optional | Upload this File directly, skipping the SDK drop-zone. Your app owns file selection and the SDK shows only the secure-upload progress dialog |
| `folder` | string | Optional | Slash-delimited folder path within the vault (e.g. `Accounts/001xx`); missing path segments are find-or-created. Omitted means the vault root. |
| `token` | string | Optional | Vault Access Token (VAT). When provided, the VAT authorizes the upload; no public-upload flag required. When omitted, the public upload flow is used. |
| `backdropBlur` | boolean | Optional | Blur background in dialog mode (default: true) |
| `hideUploadAnother` | boolean | Optional | Hide "Upload Another File" button on success (for single-file flows) |
| `hideSuccessScreen` | boolean | Optional | Skip the success screen entirely: on completion the SDK fires `onSuccess` and closes the dialog, so your app renders its own post-upload UI |
| `onSuccess` | function | Optional | Callback on success |
| `onError` | function | Optional | Callback on error |
| `onCancel` | function | Optional | Callback when user cancels |
| `onProgress` | function | Optional | Callback for progress updates |

#### Result

| Field | Type | Description |
| --- | --- | --- |
| `ingotId` | string | Unique identifier for the uploaded file |
| `vaultId` | string | Vault the file was uploaded to |
| `filename` | string | Original filename |
| `sizeBytes` | number | File size in bytes |
| `uploadTime` | string | Upload timestamp (ISO 8601) |

#### Progress callback

```javascript
{
  bytesUploaded: 52428800,
  bytesTotal: 104857600,
  percentage: 50,
  phase: 'uploading'  // 'uploading' | 'ceremony' | 'complete'
}
```

### upload.attach(selector, options)

Bind upload to element clicks. Returns a cleanup function.

```javascript
const cleanup = sparkvault.vaults.upload.attach('.upload-btn', {
  vaultId: 'vlt_abc123',
  onSuccess: (result) => { ... }
});

cleanup(); // Remove handlers
```

### upload.close()

Programmatically close the upload widget.

### upload.pop(options) / upload.render(options)

Aliases on the callable: `pop()` opens the upload widget as a dialog, equivalent to calling `upload(options)`, and `render()` renders it inline, equivalent to `upload({ ...options, target })`.

## Vaults API Reference

The `vaults` module is the control plane: create vaults, unseal them for data-plane access, and manage their sharing and upload configuration. The browser upload widget (`vaults.upload`, documented above) also lives here.

> **Store the VMK**
>
> `create()` returns the Vault Master Key (`vmk`) exactly once. It cannot be recovered. Store it securely. Unsealing, deleting, and enabling sharing all require it.

```javascript
// Create a vault. Save the VMK!
const vault = await sparkvault.vaults.create({ name: 'My Vault' });

// Unseal to get a short-lived Vault Access Token (VAT).
// The returned object is passed to ingots/folders calls.
const unsealed = await sparkvault.vaults.unseal(vault.id, vault.vmk);

// Inspect, rename, seal
const detail = await sparkvault.vaults.get(vault.id);
await sparkvault.vaults.update(vault.id, { name: 'Renamed' });
await sparkvault.vaults.seal(vault.id);

// List vaults — paginated: returns a page { vaults, nextCursor }.
// Follow nextCursor to fetch subsequent pages.
const page = await sparkvault.vaults.list();
page.vaults.forEach((v) => console.log(v.name));

// Delete requires the vault's exact name as confirmation (sent as the
// X-Confirm-Name header, case-sensitive) — not the VMK.
await sparkvault.vaults.delete(vault.id, vault.name);
```

### Sharing

```javascript
const config = await sparkvault.vaults.getSharingConfig(vault.id);

await sparkvault.vaults.enableSharing(vault.id, vault.vmk, {
  allIngotsPublic: false,
  sessionLengthSeconds: 3600,
});
await sparkvault.vaults.updateSharingConfig(vault.id, { sessionLengthSeconds: 7200 });
await sparkvault.vaults.disableSharing(vault.id);
```

### Upload configuration & access-log retention

```javascript
const upload = await sparkvault.vaults.getUploadConfig(vault.id);

await sparkvault.vaults.enableUploadPortal(vault.id, vault.vmk);
await sparkvault.vaults.disableUploadPortal(vault.id);
await sparkvault.vaults.enableUploadWidget(vault.id, vault.vmk);
await sparkvault.vaults.disableUploadWidget(vault.id);
await sparkvault.vaults.updateUploadConfig(vault.id, {
  maxSizeBytes: 1048576,
  notificationEmail: 'ops@example.com',
});

const retention = await sparkvault.vaults.getAccessLogRetention(vault.id);
await sparkvault.vaults.updateAccessLogRetention(vault.id, 7776000);
```

## Ingots API Reference

The `ingots` module operates on the contents of an _unsealed_ vault. Pass the object returned by `vaults.unseal(...)` as the first argument. It carries the short-lived Vault Access Token.

> **Primitives vs. high-level flows**
>
> `createUpload()` and `createDownloadLink()` are discrete primitives that return a Forge URL. `upload()` and `download()` are the high-level flows: they drive the resumable transfer end-to-end and verify server-side finalization.

```javascript
const unsealed = await sparkvault.vaults.unseal(vault.id, vault.vmk);

// High-level upload: creates the session, streams over TUS, verifies finalized.
const ingot = await sparkvault.ingots.upload(unsealed, {
  file: myFile,
  name: 'document.pdf',
  onProgress: (uploaded, total) => console.log(uploaded / total),
});

// High-level download: fetches a fresh signed link and returns a Blob.
const blob = await sparkvault.ingots.download(unsealed, ingot.id);

// Discrete primitives: drive the transfer yourself.
const session = await sparkvault.ingots.createUpload({
  vault: unsealed, name: 'big.zip', contentType: 'application/zip', sizeBytes: file.size,
});
const link = await sparkvault.ingots.createDownloadLink(unsealed, ingot.id);
```

### Manage & organize

```javascript
await sparkvault.ingots.rename(unsealed, ingot.id, 'renamed.pdf');
await sparkvault.ingots.move(unsealed, ingot.id, 'fld_xyz');   // null = vault root
await sparkvault.ingots.replace(unsealed, ingot.id, { file: newFile });
await sparkvault.ingots.delete(unsealed, ingot.id);

const meta = await sparkvault.ingots.get(unsealed, ingot.id);
const { ingots } = await sparkvault.ingots.list(unsealed);
const found = await sparkvault.ingots.search(unsealed, { q: 'invoice' });
const contents = await sparkvault.ingots.listContents(unsealed, { folderId: 'fld_xyz' });
```

`list()`, `search()`, `listContents()`, and `getAccessLogs()` accept `{ limit, cursor }` pagination and return a `nextCursor`. `list()` and `listContents()` also accept `sortBy` (`name` | `size` | `content_type` | `created` | `access_count`) and `sortOrder` (`asc` | `desc`); `search()` also accepts `fileType`, `minSize`, `maxSize`, and `sortOrder`.

### Sharing & access logs

```javascript
await sparkvault.ingots.updateSharingConfig(unsealed, ingot.id, { shared: true, visibility: 'invite_only' });
const sharing = await sparkvault.ingots.getSharingConfig(unsealed, ingot.id);

// Optional 4th arg: invite lifetime (omit to inherit the link's existing expiry)
const invite = await sparkvault.ingots.addInvite(unsealed, ingot.id, 'user@example.com', { expiresInSeconds: 86400 });
await sparkvault.ingots.revokeInvite(unsealed, ingot.id, invite.id);

const { logs } = await sparkvault.ingots.getAccessLogs(unsealed, ingot.id, { limit: 100 });
```

## Folders API Reference

The `folders` module organizes ingots within an _unsealed_ vault. The flat list returned by `list()` is enough to build the folder tree client-side.

```javascript
const unsealed = await sparkvault.vaults.unseal(vault.id, vault.vmk);

const folders = await sparkvault.folders.list(unsealed);
const folder = await sparkvault.folders.create(unsealed, { name: 'Invoices' });
const detail = await sparkvault.folders.get(unsealed, folder.id);
const trail = await sparkvault.folders.getBreadcrumb(unsealed, folder.id);

await sparkvault.folders.update(unsealed, folder.id, { name: 'Renamed' });
await sparkvault.folders.move(unsealed, folder.id, 'fld_parent');  // null = vault root
await sparkvault.folders.togglePin(unsealed, folder.id, true);
await sparkvault.folders.moveIngot(unsealed, 'ing_abc', folder.id); // null = vault root
await sparkvault.folders.delete(unsealed, folder.id);
```

## Sparks API Reference

Sparks are end-to-end encrypted, read-once ephemeral secrets scoped to your account (no vault or VAT required). Reading a spark with `get()` burns it.

```javascript
const spark = await sparkvault.sparks.create({
  payload: 'top secret',
  ttlMinutes: 60,             // defaults to 1440 (24h)
  contentType: 'text/plain',  // optional MIME type (base64 payload for binary types)
  filename: 'note.txt',       // optional, for file payloads
});

// Kindling groups related sparks into a family
const first = await sparkvault.sparks.create({ payload: 'one', withKindling: true });
const sibling = await sparkvault.sparks.create({ payload: 'two', kindling: first.kindling });

const { sparks } = await sparkvault.sparks.list();       // filters: { kindling, status, limit, cursor }
const revealed = await sparkvault.sparks.get(spark.id);  // burns the spark
await sparkvault.sparks.delete(spark.id);
```

### Public sharing (SparkLink)

```javascript
// Share via a public x.sv link
const share = await sparkvault.sparks.share(spark.id, { visibility: 'public' });
console.log(share.shareUrl);   // https://x.sv/...

const status = await sparkvault.sparks.getShare(spark.id);
await sparkvault.sparks.unshare(spark.id);
```

## Entropy API Reference

Generate cryptographically strong random values from the SparkVault hardware-backed entropy service.

```javascript
// Formatted output: hex | base64 | base64url | alphanumeric | alphanumeric-mixed | password | numeric | uuid | bytes
const { value } = await sparkvault.entropy.generate({ bytes: 32, format: 'hex' });

// Raw bytes as a Uint8Array
const key = await sparkvault.entropy.generateBytes(32);
```

`bytes` must be an integer from 1 to 1024 (a `ValidationError` is thrown otherwise). For `format: 'bytes'` the `value` is a number array.

## Notifications API Reference

The signed-in user's metadata-only notification inbox. The recipient is always derived from the session token, so every call reads or mutates only the caller's own inbox. Rows are locked pointers: the content lives behind each row's SparkLink (`sparklink_code`), never in the row itself. Requires an access token (`accessToken` / `getAccessToken`).

```javascript
// Page the inbox (newest first). Views: all | unseen | unread | archived (default all).
const { notifications, cursor } = await sparkvault.notify.getInbox({ state: 'unread', limit: 50 });
// A page can come back SHORT of limit yet still carry a non-null cursor — always follow the cursor.

// Bounded unseen badge count; `capped` true means render "N+".
const { unread_count, capped } = await sparkvault.notify.getUnreadCount();

// Mark one of your rows seen | read | archived (idempotent). `createdAt` MUST be the row's own
// created_at, round-tripped from the list row — the id alone cannot address the row.
await sparkvault.notify.markState(row.notification_id, { createdAt: row.created_at, state: 'read' });
```

### Browser Web Push

Opt the current browser into push notifications for the pointer (never content). Call from a user gesture. Your app hosts and registers the service worker; the SDK handles the VAPID key, permission, subscription, and server registration, returning a typed status instead of throwing on the expected declines. `subscribeWebPush` returns `not_configured` until the platform VAPID key is provisioned, so gate your UI on `isWebPushSupported()` and that status.

```javascript
if (sparkvault.notify.isWebPushSupported()) {
  const registration = await navigator.serviceWorker.register('/sw.js');
  const { status } = await sparkvault.notify.subscribeWebPush({ serviceWorkerRegistration: registration });
  // status: 'subscribed' | 'already_subscribed' | 'not_supported' | 'not_configured' | 'permission_denied'

  // Later, to turn it off on this device:
  await sparkvault.notify.unsubscribeWebPush({ serviceWorkerRegistration: registration });
}
```

## Health API Reference

### health.check(options?)

Check SparkVault API availability. Returns a structured result and does not throw for offline/network failures.

```javascript
const result = await sparkvault.health.check({
  timeout: 5000
});

// result.online - true when the API returns 2xx
// result.status - 'healthy' | 'unhealthy' | 'unreachable'
// result.httpStatus - HTTP status when a response was received
// result.checkedAt - Unix timestamp
// result.error - network/timeout message when unreachable
```

### health.isOnline(options?)

Convenience method that resolves to a boolean.

```javascript
const online = await sparkvault.health.isOnline({ timeout: 5000 });
```

## Error Handling

```javascript
try {
  const result = await sparkvault.products.identity.signIn();
} catch (error) {
  switch (error.code) {
    case 'user_cancelled':
      // User closed the popup (not an error)
      break;
    case 'validation_error':
      console.error('Invalid input:', error.message);
      break;
    case 'network_error':
      console.error('Network error');
      break;
    case 'timeout_error':
      console.error('Request timed out');
      break;
    default:
      console.error('Error:', error.message);
  }
}
```

| Code | Description |
| --- | --- |
| `user_cancelled` | User closed the popup |
| `validation_error` | Invalid input or configuration (HTTP 400) |
| `authentication_error` | Missing or invalid credentials (HTTP 401) |
| `authorization_error` | Not permitted to access this resource (HTTP 403) |
| `PLAN_REQUIRED` | The account has no active subscription and attempted a billable action (HTTP 402); surface a subscribe CTA to admins |
| `QUOTA_EXCEEDED` | A pooled-capacity or seat limit is exhausted (HTTP 402); `error.resource` identifies the pool: `storage` | `bandwidth` | `seats` | `identity` |
| `network_error` | Network connectivity failure |
| `timeout_error` | Request timed out |
| `popup_blocked` | Browser blocked popup |

The 401/403 and 402 billing gates apply to the authenticated `vaults`, `ingots`, and `sparks` calls on this page. Each code maps to an exported error class (e.g. `PlanRequiredError`, `QuotaExceededError`) for `instanceof` checks.

## Configuration Options

```javascript
const sparkvault = SparkVault.init({
  accountId: 'acc_YOUR_ACCOUNT_ID',  // Required
  backdropBlur: true,                 // Blur background on modals (default: true)
  timeout: 30000,                     // HTTP timeout in ms (default: 30000)
  preloadConfig: true                 // Preload config for instant modals (default: true)
});
```

### Advanced options

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `apiBaseUrl` | string | Optional | Core API origin. A trailing `/v1` is accepted and normalized.Default: `https://api.sparkvault.com` |
| `identityBaseUrl` | string | Optional | Identity Product API base URLDefault: `https://auth.sparkvault.com` |
| `portalBaseUrl` | string | Optional | auth.sv portal origin for `portalUrl()` deep linksDefault: `https://auth.sv` |
| `accessToken` | string | Optional | Static SparkVault Core access token for authenticated API calls |
| `getAccessToken` | function | Optional | Dynamic Core access-token provider (sync or async) for authenticated API calls |
| `apiKey` | string | Optional | API key for server-side SDK use |
| `clientId` | string | Optional | OIDC client ID registered for this account. Required only for FedCM one-click sign-in (`signInWithFedcm()`). |
| `allowedDownloadHostPatterns` | RegExp\[\] | Optional | Host allowlist for backend-issued ingot download URLs. Defaults to the canonical SparkVault Forge / S3 / CloudFront hosts; override only when pointing at a custom deployment. |

## TypeScript

```typescript
import SparkVault, {
  SparkVaultConfig,
  VerifyOptions,
  VerifyResult,
  UploadOptions,
  UploadResult,
  UserCancelledError,
  ValidationError,
  PlanRequiredError,
  QuotaExceededError,
  NetworkError
} from '@sparkvault/sdk-js';

const config: SparkVaultConfig = {
  accountId: 'acc_YOUR_ACCOUNT_ID'
};

const sparkvault = SparkVault.init(config);

const result: VerifyResult = await sparkvault.products.identity.signIn();
```

## Debug Mode

Enable verbose console logging:

```html
<script
  src="https://cdn.sparkvault.com/sdk/v1/sparkvault.js"
  data-account-id="acc_YOUR_ACCOUNT_ID"
  data-debug="true"
></script>
```

## Browser Support

-   Chrome 80+
-   Firefox 75+
-   Safari 13.1+
-   Edge 80+

Passkey authentication requires WebAuthn support. On unsupported browsers, passkey won't appear as an option.
