FoxNose TypeScript SDK
Official TypeScript client for FoxNose Management and Flux APIs. Async-only, type-safe, with zero dependencies.
Overview
The FoxNose TypeScript SDK provides a convenient way to interact with FoxNose APIs from your Node.js applications. It includes two main clients:
- ManagementClient — For administrative operations: managing collections, components, resources, roles, and API keys
- FluxClient — For content delivery: fetching published resources, searching content, and accessing localized data
Features
- Type-safe — Full TypeScript interfaces for all API responses
- Async-only — Built on native
fetch(Node 18+) - Automatic retries — Exponential backoff with jitter and Retry-After support
- Four auth strategies — Anonymous, JWT, Simple key, and Secure (ECDSA P-256)
- Zero dependencies — Uses only Node.js built-in modules
- Dual output — ESM and CommonJS builds with full
.d.tsdeclarations
Installation
npm install @foxnose/sdk
# or
pnpm add @foxnose/sdk
Quick Start
Management Client
import { ManagementClient, SimpleKeyAuth } from '@foxnose/sdk';
const client = new ManagementClient({
baseUrl: 'https://api.foxnose.net',
environmentKey: 'your-environment-key',
auth: new SimpleKeyAuth('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY'),
});
// List collections
const collections = await client.listCollections();
console.log(collections.results);
// Create a resource
const resource = await client.createResource('my-collection-key', {
data: { title: 'Hello World' },
});
client.close();
Flux Client
import { FluxClient, SimpleKeyAuth } from '@foxnose/sdk';
const client = new FluxClient({
baseUrl: 'https://your-env.fxns.io',
apiPrefix: 'content',
auth: new SimpleKeyAuth('YOUR_PUBLIC_KEY', 'YOUR_SECRET_KEY'),
});
// List resources
const resources = await client.listResources('articles');
// Vector search
const results = await client.search('articles', {
search_mode: 'vector',
vector_search: {
query: 'how to build AI applications',
top_k: 5,
},
});
client.close();
Authentication
The SDK supports four authentication strategies:
| Strategy | Use case |
|---|---|
SimpleKeyAuth | Development and Flux API access |
SecureKeyAuth | Production — ECDSA P-256 signed requests |
JWTAuth | Server-side apps with user tokens |
AnonymousAuth | Unauthenticated endpoints |
import { SimpleKeyAuth, SecureKeyAuth, JWTAuth, AnonymousAuth } from '@foxnose/sdk';
// Simple key pair
const simple = new SimpleKeyAuth('public-key', 'secret-key');
// ECDSA P-256 signature
const secure = new SecureKeyAuth('public-key', 'base64-der-private-key');
// JWT token
const jwt = JWTAuth.fromStaticToken('your-access-token');
// No authentication
const anon = new AnonymousAuth();
Batch Operations
Efficiently upsert multiple resources with concurrency control:
const items = [
{ external_id: 'article-1', payload: { data: { title: 'First' } } },
{ external_id: 'article-2', payload: { data: { title: 'Second' } } },
];
const result = await client.batchUpsertResources('my-collection-key', items, {
maxConcurrency: 5,
onProgress: (completed, total) => console.log(`${completed}/${total}`),
});
console.log(`OK: ${result.succeeded.length}, Failed: ${result.failed.length}`);
Components on Collections
Collections can embed Components as nested fields with explicit pin semantics (component, component_version, auto_update). The nestedFieldMeta helper builds the meta block with camelCase ergonomics, and syncCollectionComponent advances pinned fields to a target Component version on demand.
import { ManagementClient, JWTAuth, nestedFieldMeta } from '@foxnose/sdk';
const client = new ManagementClient({
baseUrl: 'https://api.foxnose.net',
environmentKey: 'prod',
auth: JWTAuth.fromStaticToken('YOUR_ACCESS_TOKEN'),
});
// Embed a Component as a pinned nested field on a Collection draft.
await client.createCollectionField('articles', 'v2-draft', {
key: 'seo',
name: 'SEO',
type: 'nested',
required: true,
meta: nestedFieldMeta({
component: 'cmp-seo-metadata',
componentVersion: 'ver-abc12345',
autoUpdate: false, // default — pin until explicit sync
}),
});
syncCollectionComponent
Advance pinned nested fields to a newer Component version. An empty body syncs every pinned field to its Component's current version; pass fieldPaths/toVersions to target specific paths.
// Advance every pinned nested field to its Component's current version.
const result = await client.syncCollectionComponent('articles');
console.log(result.synced_paths, result.schema_version);
// Advance specific paths to a chosen Component version.
await client.syncCollectionComponent('articles', {
fieldPaths: ['seo'],
toVersions: { seo: 'ver-def67890' },
});
syncCollectionComponent returns a SyncComponentResponse with synced_paths, skipped (per-path reasons), and schema_version (the UID of the newly published Collection schema version, or null if no field needed advancing). On a compatibility conflict the server returns 409 component_sync_conflict; quota exhaustion returns 422 too_many_versions. Both surface as FoxnoseAPIError.
Error Handling
All API errors are thrown as typed exceptions:
import { FoxnoseAPIError, FoxnoseTransportError } from '@foxnose/sdk';
try {
await client.getResource('my-collection-key', 'nonexistent-key');
} catch (err) {
if (err instanceof FoxnoseAPIError) {
console.error(err.statusCode); // 404
console.error(err.errorCode); // "not_found"
} else if (err instanceof FoxnoseTransportError) {
console.error('Network error:', err.message);
}
}
Billing errors
Billing and quota responses are thrown as typed subclasses of FoxnoseAPIError, so any existing catch (err) { if (err instanceof FoxnoseAPIError) ... } keeps working. Narrow to a subclass to read its typed fields:
| Class | HTTP | Fields |
|---|---|---|
SpendCapExceededError | 402 | capUsd, cycleResetsAt, raiseCapUrl |
PlanExhaustedError | 402 | axis, windowResetsAt, upgradeUrl |
PlanLimitExceededError | 403 | entity, limit, current, upgradeUrl |
RateLimitExceededError | 429 | retryAfter |
import {
FoxnoseAPIError,
SpendCapExceededError,
PlanExhaustedError,
PlanLimitExceededError,
RateLimitExceededError,
} from '@foxnose/sdk';
try {
await client.createResource('my-collection-key', {
data: { title: 'Hello World' },
});
} catch (err) {
if (err instanceof SpendCapExceededError) {
// HTTP 402 — the account spend cap was reached.
console.error(err.capUsd); // Spend cap in USD (or null)
console.error(err.cycleResetsAt); // ISO timestamp when the cycle resets
console.error(err.raiseCapUrl); // Where to raise the cap
} else if (err instanceof PlanExhaustedError) {
// HTTP 402 — a metered plan allowance ran out (Free plans).
console.error(err.axis); // e.g. "retrievals", "writes"
console.error(err.windowResetsAt);
console.error(err.upgradeUrl);
} else if (err instanceof PlanLimitExceededError) {
// HTTP 403 — a structural plan limit was hit.
console.error(err.entity); // e.g. "collections"
console.error(err.current, err.limit);
console.error(err.upgradeUrl); // May be undefined
} else if (err instanceof RateLimitExceededError) {
// HTTP 429 — too many requests.
console.error(err.retryAfter); // Seconds to wait (from Retry-After)
} else if (err instanceof FoxnoseAPIError) {
console.error(err.statusCode, err.errorCode);
}
}
All four subclass FoxnoseAPIError, so a single catch (err) { if (err instanceof FoxnoseAPIError) ... } still catches them when you don't need the typed fields.
Resources
- GitHub Repository — Source code and issues
- npm Package — Package downloads
- Management API Reference — API documentation
- Flux API Reference — Content delivery API docs