Files
Shade/packages/shade-sdk/src/config.ts
Sterister c95824f95f feat(sdk): M-Magic 1-4 — high-level SDK with magic drop-in
Phase A complete: createShade() one-liner with auto-establish, auto-publish,
and auto-replenish.

M-Magic 1-4 rolled into @shade/sdk:
- createShade() factory with config validation and storage resolution
  (memory | sqlite:... | { type: 'postgres', url: ... } | explicit instance)
- Shade class wraps crypto + storage + session manager + transport
- Auto-publish: initialize() automatically registers with the prekey server
- Auto-establish: send() transparently fetches bundles and creates sessions
  on first message to a new peer
- Per-address mutex serializes concurrent sends to prevent ratchet corruption
- BackgroundTasks class for periodic replenishment + opt-in identity rotation
- rotate() rebuilds the transport with the new signing key so subsequent
  signed operations work after rotation
- onMessage() handler API for incoming plaintext

API:
  const shade = await createShade({ prekeyServer, storage });
  await shade.send('bob', 'hello');
  await shade.receive('alice', envelope);
  shade.onMessage((from, msg) => ...);
  await shade.rotate();
  await shade.shutdown();

13 new SDK tests covering: happy path, auto-publish, two-process
conversation, onMessage handlers, concurrent sends, unknown peer,
fingerprint verification, shutdown, manual replenish, auto-replenish
off, rotate, and config validation.

233 tests passing, 0 failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:27:59 +02:00

122 lines
3.6 KiB
TypeScript

import type { StorageProvider } from '@shade/core';
import { ConfigurationError } from '@shade/core';
/**
* Shade SDK configuration.
*
* All fields have sane defaults except `prekeyServer` which is required
* for any networked usage.
*/
export interface ShadeConfig {
/** Required: base URL of the prekey server */
prekeyServer: string;
/**
* Storage backend. Accepts:
* - "memory" — in-memory only (lost on restart, good for tests)
* - "sqlite:/path/to/file.db" — SQLite backend
* - { type: 'postgres', url: 'postgres://...' } — PostgreSQL backend
* - An explicit StorageProvider instance
*
* Default: "memory"
*/
storage?: string | StorageProvider | { type: 'postgres'; url: string };
/**
* Your address on the prekey server (e.g. "alice@example.com" or "device:abc123").
* If omitted, a random UUID is generated and persisted.
*/
address?: string;
/**
* Auto-replenish configuration. When the one-time prekey stock drops
* below `min`, the SDK will generate enough to reach `target` and
* upload them to the prekey server.
*
* Default: { min: 5, target: 20, intervalMs: 60_000 }
* Pass `false` to disable.
*/
autoReplenish?: { min: number; target: number; intervalMs: number } | false;
/**
* Auto identity rotation. Default OFF.
* Pass an interval string like '7d' or '30d' to enable periodic rotation.
*/
autoRotate?: false | '1d' | '7d' | '30d' | '90d';
/**
* Optional observer configuration. If provided, starts a Shade Observer
* endpoint for the dashboard.
*/
observer?: {
/** Bearer token for the observer. Must be at least 16 chars. */
token: string;
/** Port to listen on. If not set, observer is created but not served. */
port?: number;
/** Path prefix to mount under (default: "/shade-observer") */
basePath?: string;
};
}
export interface ResolvedConfig {
prekeyServer: string;
storage: string | StorageProvider | { type: 'postgres'; url: string };
address?: string;
autoReplenish: { min: number; target: number; intervalMs: number } | false;
autoRotate: false | '1d' | '7d' | '30d' | '90d';
observer?: {
token: string;
port?: number;
basePath: string;
};
}
/** Parse and validate a ShadeConfig, resolving defaults and env var overrides */
export function resolveConfig(input: ShadeConfig): ResolvedConfig {
if (!input.prekeyServer) {
throw new ConfigurationError('prekeyServer is required');
}
const autoReplenish = input.autoReplenish === false
? false
: {
min: input.autoReplenish?.min ?? 5,
target: input.autoReplenish?.target ?? 20,
intervalMs: input.autoReplenish?.intervalMs ?? 60_000,
};
const resolved: ResolvedConfig = {
prekeyServer: input.prekeyServer.replace(/\/$/, ''),
storage: input.storage ?? 'memory',
address: input.address,
autoReplenish,
autoRotate: input.autoRotate ?? false,
};
if (input.observer) {
if (!input.observer.token || input.observer.token.length < 16) {
throw new ConfigurationError('observer.token must be at least 16 characters');
}
resolved.observer = {
token: input.observer.token,
port: input.observer.port,
basePath: input.observer.basePath ?? '/shade-observer',
};
}
return resolved;
}
/** Parse a rotation interval string ('7d', '30d', etc.) into milliseconds */
export function parseRotationInterval(
interval: '1d' | '7d' | '30d' | '90d',
): number {
const map: Record<string, number> = {
'1d': 24 * 60 * 60 * 1000,
'7d': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
'90d': 90 * 24 * 60 * 60 * 1000,
};
return map[interval]!;
}