إنتقل إلى المحتوى الرئيسي

17 · API reference

Purpose: The complete public surface of every @rasd/* package — TypeScript signatures, parameters, return types, events, error codes and stability — expanding the frozen names in 00 §11 without renaming anything. Audience: Engineers implementing the packages (this is the contract their index.ts must export) and host-app developers wiring Rasd Forms into a web or React Native app.

TL;DR

  • Names in 00 §11 are frozen; this document adds parameters, options, return shapes and stability tags. Shapes already fixed by a sibling document (03, 05, 06, 09, 10, 15) are reproduced or linked, never reinvented.
  • Every package is ESM-only, sideEffects: false (CSS/theme files excepted), named exports only, react-native export condition first, TypeScript 6 strict + isolatedDeclarations (research/08).
  • One error class everywhere: RasdError { code, message, details, cause, retryable, userMessage }; adapters never leak driver errors (§16).
  • One event convention everywhere: on(event, handler) / subscribe(listener) return an unsubscribe function; hooks wrap them (§17).
  • Stability: stable = semver, breaking only in a major; beta = may change in a minor with a changeset note; experimental = may change or disappear in any release.
  • createFormEngine is the only stateful object in @rasd/core; everything else in core is a pure function of its arguments.
  • @rasd/react and @rasd/native export the same names with the same types; only JSX and per-part styling props differ (§3.7).

1. Conventions used in this reference

ConventionRule
Import pathsRoot import per package; heavy or platform-specific code lives under documented sub-paths (@rasd/react/elements/matrix, @rasd/pwa/sw, @rasd/media/camera, @rasd/storage-sqlite/node). The exports map is closed — nothing else resolves.
FactoriescreateX(options) returns an object with dispose(); all I/O accepts an optional trailing { signal?: AbortSignal } (omitted from tables) and rejects with RasdError('RASD_ABORTED') on abort.
Time / idsISO-8601 UTC strings; UUID v7; clock?: () => number injectable for tests.
FieldPathString path: consent, hh_members[2].m_age0-based in engine APIs, 1-based inside REL (${hh_members[3].m_age}). Core returns LocalizedStrings unresolved unless the API says "resolved".
Errors columnRasdError.code values the API throws or rejects with; validation results are returned, not thrown. Types referenced by a stable export are stable.

Package dependency direction (arrow = "depends on"):

flowchart LR
core["@rasd/core"]
react["@rasd/react"] --> core
native["@rasd/native"] --> core
builder["@rasd/builder"] --> react
element["@rasd/element"] --> react
storage["@rasd/storage"] --> core
dexie["@rasd/storage-dexie"] --> storage
sqlite["@rasd/storage-sqlite"] --> storage
sync["@rasd/sync"] --> storage
pwa["@rasd/pwa"] --> sync
media["@rasd/media"] --> core
themes["@rasd/themes"] --> core
xlsform["@rasd/xlsform"] --> core
cli["@rasd/cli"] --> xlsform
cli --> core
testing["@rasd/testing"] --> core
license["@rasd/license"]
server["@rasd/server"]

2. @rasd/core (Apache-2.0)

2.1 Types — RFD v1 (stable)

The zod schemas in @rasd/core/schema are the executable source; these types are inferred from them (04 §1.2). Per-type props are specified property-by-property in 04 §10.

export type LocalizedString = string | { [bcp47Locale: string]: string };
export type Expr = string; // REL v1 source
export type Ext = { [vendorKey: string]: unknown };
export type Severity = 'error' | 'warning' | 'info';
export interface Geo { lat: number; lng: number; alt?: number; accuracy?: number; capturedAt: string }
export interface AttachmentRef { attachmentId: string; name?: string; mime?: string; bytes?: number; sha256?: string; capturedAt?: string; geo?: Geo }
export interface Media { image?: LocalizedString; audio?: LocalizedString; video?: LocalizedString }

export interface FormDefinition {
$schema?: string; rasd: string /* "1.0" */; id: string; version: string;
meta: { title: LocalizedString; description?: LocalizedString; tags?: string[]; author?: string; createdAt?: string; updatedAt?: string; changelog?: LocalizedString; ext?: Ext };
settings: FormSettings; choiceLists?: Record<string, ChoiceList>; datasets?: Dataset[];
pages: Page[]; logic?: { calculated?: CalculatedValue[]; triggers?: Trigger[] };
requires?: { rasd?: string; features?: string[] }; ext?: Ext;
}
export interface FormSettings {
defaultLocale: string; locales: string[]; navigation?: 'paged' | 'scroll'; showProgress?: boolean; allowDrafts?: boolean; autosaveMs?: number;
instanceName?: Expr; submissionIdPrefix?: string;
audit?: { enabled: boolean; trackChanges?: boolean; location?: { enabled: boolean; priority?: 'high' | 'balanced' | 'low'; minSeconds?: number; minMeters?: number } };
encryption?: { mode: 'none' | 'field' | 'submission'; publicKeyId?: string };
theme?: { themeId?: string; overrides?: Partial<RasdTheme> };
numbering?: 'latn' | 'native'; calendar?: 'gregorian' | 'islamic-umalqura';
localeMeta?: Record<string, { dir: 'rtl' | 'ltr'; numbering?: 'latn' | 'native'; calendar?: 'gregorian' | 'islamic-umalqura' }>;
ext?: Ext;
}
export interface Page { id: string; title?: LocalizedString; description?: LocalizedString; relevant?: Expr; elements: Element[]; ext?: Ext }
export interface Choice { value: string; label: LocalizedString; media?: Media; attrs?: Record<string, string | number | boolean>; ext?: Ext }
export interface ChoiceList { choices?: Choice[]; source?: { type: 'dataset'; dataset: string }; valueKey?: string; labelKey?: string; filterKeys?: string[]; ext?: Ext }
export interface Dataset { name: string; source: 'server' | 'inline' | 'url'; keyField: string; inline?: Record<string, unknown>[]; url?: string; columns?: { name: string; type?: string; label?: LocalizedString }[]; minVersion?: string; ext?: Ext }
export interface CalculatedValue { name: string; calculate: Expr; includeInData?: boolean; ext?: Ext }
export interface Trigger { id: string; when: Expr; on?: 'change' | 'pageLeave' | 'finalize'; once?: boolean; actions: TriggerAction[]; ext?: Ext }
export type TriggerAction =
| { type: 'setValue'; target: string; value?: unknown; expr?: Expr } | { type: 'clearValue'; target: string }
| { type: 'complete'; message?: LocalizedString } | { type: 'skipTo'; page: string }
| { type: 'showMessage'; message: LocalizedString; severity?: Severity; blocking?: boolean }
| { type: 'custom'; id: string; payload?: unknown };
export type Validator = { message?: LocalizedString; severity?: Severity } & (
| { type: 'regex'; pattern: string } | { type: 'range'; min?: number | string; max?: number | string }
| { type: 'length'; min?: number; max?: number } | { type: 'expr'; expr: Expr } | { type: 'custom'; id: string });

export interface ElementBase<T extends string, P> {
type: T; name: string; label?: LocalizedString; hint?: LocalizedString; guidance?: LocalizedString; media?: Media;
required?: boolean | Expr; requiredMessage?: LocalizedString; relevant?: Expr; readonly?: boolean | Expr;
default?: { value: unknown } | { expr: Expr }; calculate?: Expr; constraint?: Expr; constraintMessage?: LocalizedString;
validators?: Validator[]; appearance?: { variant?: string; columns?: number; size?: 'sm' | 'md' | 'lg'; ext?: Ext };
bind?: { sensitive?: boolean; saveIncomplete?: boolean; trackChanges?: boolean; index?: boolean; ext?: Ext };
props?: P; ext?: Ext;
}
type SelectProps = { list?: string; choices?: Choice[]; choiceFilter?: Expr; search?: boolean; randomize?: boolean };
export type Element =
| ElementBase<'text', { multiline?: boolean; format?: 'email' | 'phone' | 'url' | 'none'; maxLength?: number; mask?: string; placeholder?: LocalizedString }>
| ElementBase<'number', { kind?: 'integer' | 'decimal'; min?: number; max?: number; step?: number; unit?: LocalizedString; thousandsSeparator?: boolean }>
| ElementBase<'date' | 'time' | 'datetime', { min?: string; max?: string; calendar?: 'gregorian' | 'hijri' }>
| ElementBase<'select_one', SelectProps & { other?: { enabled: boolean; label?: LocalizedString; value?: string } }>
| ElementBase<'select_multiple', SelectProps & { minSelected?: number; maxSelected?: number; exclusive?: string[] }>
| ElementBase<'rank', { list?: string; choices?: Choice[] }>
| ElementBase<'rating', { max?: number; icon?: 'star' | 'number' | 'smiley'; labels?: { min?: LocalizedString; max?: LocalizedString } }>
| ElementBase<'range', { min: number; max: number; step?: number; showValue?: boolean }>
| ElementBase<'checkbox', object>
| ElementBase<'consent', { text: LocalizedString; textVersion: string; method?: 'tap' | 'signature' | 'verbal'; allowWithdraw?: boolean; onWithdraw?: 'clearSensitive' | 'keep' }>
| ElementBase<'matrix', { rows: { value: string; label: LocalizedString; relevant?: Expr }[]; columns: { type: 'select_one'; list?: string; choices?: Choice[] } | { type: 'number'; min?: number; max?: number; kind?: 'integer' | 'decimal' } | { type: 'text'; maxLength?: number }; requiredRows?: 'all' | 'any' | 'none' }>
| ElementBase<'geopoint', { accuracyThreshold?: number; warningThreshold?: number; autoCapture?: boolean; allowManual?: boolean; map?: boolean }>
| ElementBase<'geotrace' | 'geoshape', { mode?: 'manual' | 'auto'; intervalSeconds?: number; minPoints?: number; accuracyThreshold?: number }>
| ElementBase<'image', { source?: 'camera' | 'gallery' | 'both'; maxPixels?: number; quality?: number; annotate?: boolean; geotag?: boolean; multiple?: boolean; maxCount?: number; maxBytes?: number }>
| ElementBase<'audio' | 'video' | 'file', { maxDurationSeconds?: number; accept?: string[]; maxBytes?: number; multiple?: boolean }>
| ElementBase<'barcode', { formats?: string[]; allowManual?: boolean }>
| ElementBase<'signature', { penColor?: string }>
| ElementBase<'note', { style?: 'info' | 'warning' | 'success'; collapsible?: boolean }>
| ElementBase<'hidden', object> | ElementBase<'calculate', object>
| (ElementBase<'group', { nestData?: boolean }> & { elements: Element[] })
| (ElementBase<'repeat', { min?: number; max?: number; count?: Expr; addLabel?: LocalizedString; removeLabel?: LocalizedString; itemLabel?: Expr; keyField?: string; confirmDelete?: boolean; allowReorder?: boolean }> & { elements: Element[] })
| ElementBase<`x:${string}`, Record<string, unknown>>;
export type ElementType = Element['type'];

rasd types <form.json> (§13) generates a per-form Data interface from these types ({ consent: 'yes' | 'no'; hh_members: { m_name: string; m_age: number }[]; photo: AttachmentRef }).

2.2 Types — Submission (stable)

export type SubmissionStatus = 'draft' | 'finalized' | 'queued' | 'sending' | 'synced' | 'rejected' | 'conflict';
export type AttachmentStatus = 'pending' | 'uploading' | 'uploaded' | 'failed';
export interface Submission {
id: string; formId: string; formVersion: string; definitionHash: string; status: SubmissionStatus;
data: Record<string, unknown>;
meta: { startedAt: string; finalizedAt?: string; deviceId: string; userId?: string; username?: string; locale: string; appVersion?: string; platform: 'web' | 'android' | 'ios' | (string & {}); instanceName?: string; geo?: Geo | null; ext?: Ext };
attachments: Attachment[]; audit: AuditEvent[];
clientRev: number; serverRev: number | null; syncedAt: string | null; createdAt: string; updatedAt: string; checksum: string /* "sha256:…" */; ext?: Ext;
}
export interface Attachment { id: string; field: string; mime: string; bytes: number; sha256: string; localUri?: string; remoteId: string | null; status: AttachmentStatus }
export interface AuditEvent { t: string; event: 'value' | 'calculate' | 'repeat_add' | 'repeat_remove' | 'reorder' | 'page' | 'locale' | 'constraint warning' | 'finalize' | 'migrate' | (string & {}); field?: string; old?: unknown; new?: unknown; source?: string; ext?: Ext }

2.3 createFormEngine(def, opts?)FormEngine (stable)

export function createFormEngine(def: FormDefinition, opts?: EngineOptions): FormEngine;
export interface EngineOptions {
submissionId?: string; // default uuidv7()
initialData?: Record<string, unknown>; // applied before default.expr; unknown keys preserved
meta?: Partial<Submission['meta']> & { custom?: Record<string, unknown> }; // ${meta.*}, ${meta.custom.*}
locale?: string; // default settings.defaultLocale
coercion?: 'rel' | 'xpath'; // default 'rel' (05 §5)
host?: unknown; // ctx.host for host functions
functions?: Record<string, RelFunction>; // per-engine additions to the default registry
validators?: Record<string, CustomValidator>; // targets of { type:'custom', id }
datasets?: Record<string, DatasetIndex>; // preloaded indexes; add later with invalidateDataset()
clock?: () => number; // now(), today(), meta.now
validateDefinition?: boolean; // default true in dev, false in production (zod not loaded)
budget?: { steps?: number /* 100_000 */; timeMs?: number /* 10 */ };
onDiagnostic?: (d: Diagnostic) => void;
}

Throws: RASD_SCHEMA_INVALID (details.issues), RASD_EXPR_PARSE, RASD_EXPR_UNKNOWN_REF, RASD_EXPR_UNKNOWN_FUNCTION, RASD_EXPR_CYCLE, RASD_UNSUPPORTED_SPEC (newer rasd major or unmet requires). The compiled program is cached process-wide per definitionHash (LRU 32). The engine never throws after construction; runtime expression failures become diagnostics (03 §10).

export interface FormEngine {
readonly definition: FormDefinition; readonly submissionId: string; readonly diagnostics: Diagnostic[];
getState(): EngineState;
setValue(path: FieldPath, value: unknown, opts?: { source?: 'user' | 'calc' | 'migrate' | `trigger:${string}` }): void;
addRepeat(path: FieldPath, at?: number, initial?: Record<string, unknown>): void;
removeRepeat(path: FieldPath, index: number): void;
moveRepeat(path: FieldPath, from: number, to: number): void;
setLocale(locale: string): void;
validate(scope?: { page?: string; path?: FieldPath }): ValidationResult;
finalize(): Result<Submission, ValidationResult>;
toSubmission(): Submission;
recompute(opts?: { volatile?: boolean }): void;
flushSync(): void;
invalidateDataset(name: string, index?: DatasetIndex): void;
subscribe(listener: (patch: EnginePatch) => void): () => void;
subscribeField(path: FieldPath, listener: (f: FieldState) => void): () => void;
select<T>(selector: (s: EngineState) => T, equals?: (a: T, b: T) => boolean): { get(): T; subscribe(cb: () => void): () => void };
on<E extends keyof EngineEvents>(event: E, handler: (e: EngineEvents[E]) => void): () => void;
dispose(): void;
}
export type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
export interface EngineState { rev: number; data: Readonly<Record<string, unknown>>; fields: ReadonlyMap<FieldPath, FieldState>; pages: { id: string; relevant: boolean; errorCount: number }[]; locale: string; status: SubmissionStatus; dirty: boolean }
export interface EnginePatch { rev: number; changedPaths: FieldPath[]; structural: boolean }
export interface FieldIssue { path: FieldPath; code: string; severity: Severity; message: string /* resolved */; source: 'required' | 'constraint' | 'validator' | 'custom' | 'valueSchema' | 'repeat'; validatorIndex?: number }
export interface ValidationResult { ok: boolean; errors: FieldIssue[]; warnings: FieldIssue[]; infos: FieldIssue[]; firstInvalidPath?: FieldPath }
export interface EngineEvents { patch: EnginePatch; diagnostic: Diagnostic; trigger: { id: string; actions: TriggerAction[] }; complete: { trigger: string; message?: LocalizedString }; message: { trigger: string; message: LocalizedString; severity: Severity; blocking: boolean }; customAction: { trigger: string; id: string; payload?: unknown }; locale: { old: string; new: string }; finalized: Submission }

FieldState (path, name, type, value, relevant, required, readonly, calculated, label, hint, guidance, choices, errors, warnings, infos, touched, dirty, validating, repeat, a11y, ext) is normative in 03 §7.

MethodSemantics
setValue(path, v, opts?)Coalesced per microtask; dependents recomputed in topological order; audited when bind.trackChanges. Unknown path or write to a calculate field ⇒ ignored + diagnostic (reason: 'READONLY_TARGET').
addRepeat / removeRepeat / moveRepeatRespect min/max/count; initial runs default for the new instance; instance ids are stable so field subscriptions survive moves. Out of range ⇒ no-op + diagnostic.
validate(scope?)Required/constraint/validators/custom/valueSchema for relevant fields in scope; async custom validators set field.validating and resolve into a later patch.
finalize()recompute({ volatile: true }) → full validate → on: 'finalize' triggers → strip irrelevant values → status finalized, meta.finalizedAt, checksum. { ok: false, error } when any error-severity issue exists; never throws. toSubmission() is the unvalidated draft with irrelevant values retained.
recompute({ volatile }) / flushSync()Re-evaluate volatile nodes (now(), random(), impure host functions) / force the pending flush (tests, before finalize).
invalidateDataset(name, index?)Replace the index; recompute every node depending on dataset:<name>.
select() (beta) / dispose()Selector store for useSyncExternalStore (equals defaults to Object.is) / unsubscribe everything; later calls throw RASD_ENGINE_DISPOSED.
const engine = createFormEngine(def, { meta: { userId: 'u-17', custom: { office: 'AMM' } }, locale: 'ar' });
const off = engine.subscribeField('hh_size', (f) => console.log(f.value, f.errors));
engine.setValue('hh_size', 5);
engine.addRepeat('hh_members'); engine.setValue('hh_members[0].m_name', 'ليلى');
const r = engine.finalize();
if (r.ok) await storage.submissions.put(r.value); else focus(r.error.firstInvalidPath);
off(); engine.dispose();
stateDiagram-v2
[*] --> compiled: createFormEngine (validate · parse · DAG)
compiled --> draft: initialData + default.expr + once()
draft --> draft: setValue / addRepeat / … → microtask flush → patch
draft --> validating: validate() / finalize()
validating --> draft: errors (Result.ok = false)
validating --> finalized: finalize() ok → event finalized
draft --> disposed: dispose()
finalized --> disposed: dispose()

2.4 Expressions (stable unless tagged)

export function parseExpression(src: string, opts?: { cache?: boolean }): Ast; // throws RASD_EXPR_PARSE { details: { source, offset, expected } }
export function evaluate(ast: Ast, ctx: EvalContext): unknown; // never throws; ctx.onDiagnostic receives RASD_EXPR_RUNTIME / RASD_EXPR_BUDGET
export function printExpression(ast: Ast): string; // canonical REL
export function dependenciesOf(ast: Ast): { refs: RefPattern[]; meta: string[]; datasets: string[]; volatile: boolean; usesPosition: boolean; usesOwner: boolean; functions: string[] };
export function registerFunction(name: string, fn: RelFunction, meta: { pure: boolean; minArgs?: number; maxArgs?: number; returns?: 'string' | 'number' | 'boolean' | 'any' }): void; // default registry; throws RASD_EXPR_FUNCTION_EXISTS for core names
export function createDatasetIndex(rows: Record<string, unknown>[], opts: { keyField: string; indexColumns?: string[] }): DatasetIndex;
export function createEvalContext(partial: Partial<EvalContext> & { data: Record<string, unknown> }): EvalContext; // defaults for tests/servers
export function isVisualisable(ast: Ast): boolean; // beta — builder helper (05 §18)
export type RelFunction = (...args: [...unknown[], FunctionContext]) => unknown; // synchronous; ctx is the last argument
export interface FunctionContext { locale: string; meta: Readonly<Record<string, unknown>>; host: unknown }
export interface Diagnostic { code: 'RASD_EXPR_RUNTIME' | 'RASD_EXPR_BUDGET' | 'RASD_EXPR_TRIGGER_LOOP'; path: string; property: string; reason?: string; message: string }

EvalContext (data, scope, owner, meta, locale, datasets, choiceLists, functions, coercion, isRelevant, budget, onDiagnostic) is normative in 05 §4.1.

ParameterNotes
registerFunction.name^[a-z][A-Za-z0-9_]*$, vendor-prefixed (wfp_…); call before creating engines; register on @rasd/server too when used in instanceName.
meta.puretrue ⇒ memoised per argument tuple; false ⇒ the expression is volatile (re-evaluated on every recompute).
createDatasetIndexSynchronous; ≤ 100 k rows; keys compared as string(); one lazy Map per queried column.
registerFunction('wfp_ration', (hh, ctx) => Number(hh ?? 0) * 12.5, { pure: true, minArgs: 1, maxArgs: 1, returns: 'number' });
evaluate(parseExpression("if(${hh_size} > 5, wfp_ration(${hh_size}), 0)"), createEvalContext({ data: { hh_size: 7 } })); // 87.5

2.5 Definition tooling (stable)

export function validateFormDefinition(def: unknown, opts?: { previous?: FormDefinition; functions?: string[]; translations?: 'warn' | 'error'; features?: string[]; limits?: Partial<ValidationLimits>; retiredNames?: Record<string, string> }): { ok: boolean; errors: Issue[]; warnings: Issue[] };
export interface Issue { code: string; path: string /* JSON pointer */; message: string; hint?: string; ref?: string }
export function definitionHash(def: FormDefinition): string; // "sha256:" + hex(SHA-256(JCS(def minus meta.updatedAt)))
export function canonicalJson(value: unknown): string; // RFC 8785
export function diffDefinitions(from: FormDefinition, to: FormDefinition): { changes: SemanticChange[]; plan: MigrationPlan; loss: 'none' | 'possible' | 'certain' };
export interface SemanticChange { kind: string; compat: 'C' | 'T' | 'B'; target?: string; from?: unknown; to?: unknown; message: LocalizedString; suggestedStep?: MigrationStep }
export interface MigrationPlan { rasd: string; formId: string; from: string; to: string; fromHash: string; toHash: string; mode: 'auto' | 'assisted' | 'manual'; loss: 'none' | 'possible' | 'certain'; steps: MigrationStep[]; review?: { requiresUser: boolean; message?: LocalizedString } }
export type MigrationStep =
| { op: 'test'; path: string; type: string } | { op: 'rename'; from: string; to: string }
| { op: 'transform'; target: string; expr: Expr; onError?: 'null' | 'keep' | 'fail' }
| { op: 'default'; target: string; value: unknown; when?: Expr }
| { op: 'remapChoices'; target: string; map: Record<string, string>; unmapped?: 'keep' | 'drop' | 'fail' }
| { op: 'drop'; target: string; keep?: 'orphan' | 'discard' }
| { op: 'wrapRepeat'; target: string; into: string; as: string } | { op: 'unwrapRepeat'; target: string; pick: 'first'; as: string }
| { op: 'recalculate'; targets: string[] };
export function migrateSubmission(sub: Submission, fromDef: FormDefinition, toDef: FormDefinition, plan?: MigrationPlan, opts?: { force?: boolean; clock?: () => number }): Submission;
export function planMigration(sub: Submission, fromDef: FormDefinition, toDef: FormDefinition, plan?: MigrationPlan): { ok: boolean; loss: 'none' | 'possible' | 'certain'; warnings: Issue[]; orphaned: Record<string, unknown>; changedPaths: string[] }; // dry run
FunctionBehaviourErrors
validateFormDefinitionStructural pass (zod, ~60 kB gz, lazy — dev/CLI/builder only) then semantic pass (names, references, a full REL parse of every expression site — a lexical scan alone missed 1 + and 1 2 3 — cycles, publish rules when previous is given). Codes: E_* / W_* catalogue in 04 §15; every one of them has a fixture in examples/invalid/.never throws
diffDefinitionsClassifies per research/12 §3; auto-seeds plan.steps for T changes; loss derives from B changes lacking a step.never throws
migrateSubmissionRuns plan.steps in order, atomically (a fail leaves sub untouched); plan omitted ⇒ derived from diffDefinitions; appends audit event migrate, sets formVersion/definitionHash to toDef. Refuses unless sub.status === 'draft' or force.RASD_MIGRATION_REFUSED (details.reason: 'NOT_DRAFT' | 'BREAKING_WITHOUT_STEP' | 'TEST_FAILED' | 'HASH_MISMATCH')

2.6 i18n helpers (stable; 13 §1)

export function negotiateLocale(requested: string[], available: string[], defaultLocale: string): string;
export function resolveLocalized(ls: LocalizedString | undefined, cfg: LocaleConfig): { text: string; locale: string; fallback: boolean };
export function formatMessage(text: string, vars: Record<string, unknown>, cfg: LocaleConfig): string; // Mini-Message; throws RASD_I18N_SYNTAX in dev, returns source in prod
export function pluralCategory(locale: string, n: number): 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
export function normalizeDigits(input: string): string;
export function createLocaleConfig(locale: string, settings: FormSettings): LocaleConfig;
export function translationReport(def: FormDefinition): { perLocale: Record<string, { total: number; translated: number; missing: number; stale: number; mt: number; percent: number }>; issues: Issue[] };
export interface LocaleConfig { locale: string; fallbacks: string[]; dir: 'ltr' | 'rtl'; numbering: 'latn' | 'native'; numberingSystem: string; calendar: 'gregorian' | 'islamic-umalqura'; hourCycle?: 'h12' | 'h23'; timeZone?: string }

2.7 RasdError and misc (stable)

export class RasdError extends Error {
constructor(code: RasdErrorCode, message: string, opts?: { details?: Record<string, unknown>; cause?: unknown; retryable?: boolean; userMessage?: LocalizedString });
readonly code: RasdErrorCode; readonly details?: Record<string, unknown>; readonly cause?: unknown; readonly retryable: boolean; readonly userMessage?: LocalizedString;
toJSON(): { code: string; message: string; details?: unknown; retryable: boolean }; // never includes answers or tokens
}
export function isRasdError(e: unknown, code?: RasdErrorCode): e is RasdError;
export const RASD_ERROR_CODES: { RASD_SCHEMA_INVALID: { retryable: false }; RASD_EXPR_PARSE: { retryable: false }; /* … one literal-keyed entry per code in §16 */ };
export type RasdErrorCode = keyof typeof RASD_ERROR_CODES; // §16
export function uuidv7(): string; export function sha256(input: Uint8Array | string): Promise<string>;
export const RASD_SPEC_VERSION: '1.0'; export function supportsSpec(rasd: string): boolean;
// '@rasd/core/schema' (lazy): formSchema, themeSchema (JSON Schema 2020-12 documents), zod schemas

3. @rasd/react and @rasd/native (FSL-1.1)

Identical export list and types; "native" notes mark the only differences (07 §2).

3.1 <RasdProvider> (stable)

export function RasdProvider(props: RasdProviderProps): JSX.Element;
export interface RasdProviderProps {
storage?: StorageAdapter | (() => Promise<StorageAdapter>); // default MemoryStorage + dev warning; factory form opens in an effect (SSR-safe)
license?: LicenseHandle; // createLicense()
theme?: RasdTheme | string; // theme object or built-in id; default 'rasd-light'
locale?: string; // chrome locale; default navigator.language / expo-localization
messages?: Record<string, Record<string, string>>; // chrome catalog overrides, deep-merged (region > language > built-in)
loadLocale?: (lc: string) => Promise<Record<string, string>>; onMissingKey?: (key: string, locale: string) => void; pluralRules?: Record<string, (n: number) => string>;
registry?: Registry; // createRegistry()
sync?: SyncEngine; // createSyncEngine()
media?: MediaAdapters; // @rasd/media web | native
mode?: { colorScheme?: 'light' | 'dark'; contrast?: 'normal' | 'high'; density?: 'comfortable' | 'compact' | 'spacious'; reducedMotion?: boolean };
portalContainer?: HTMLElement; cssNonce?: string; unstyled?: boolean; // web only
as?: keyof JSX.IntrinsicElements; className?: string; style?: React.CSSProperties; // web; native: style?: ViewStyle
security?: SecurityPolicy; // native ([16](16-security-and-data-protection.md))
logger?: Logger; onError?: (e: RasdError) => void; onAudit?: (e: AuditEvent & { submissionId: string }) => void;
onPolicyViolation?: (e: RasdError) => void; onStorageBlocked?: (info: { reason: string; unsyncedCount: number }) => void;
children: React.ReactNode;
}

Renders <div class="rasd-root" data-theme data-color-scheme data-contrast data-density dir> (web) or a direction-aware root View (native). Nested providers inherit unset props. Nothing is thrown; onError receives adapter failures (RASD_STORAGE_*, RASD_LICENSE_*).

3.2 <FormRenderer> (stable)

export function FormRenderer(props: FormRendererProps): JSX.Element;
export interface FormRendererProps {
definition: FormDefinition | { id: string; version?: string }; // reference form → storage.forms.get()
submissionId?: string; initialData?: Record<string, unknown>; meta?: Record<string, unknown>;
locale?: string; readOnly?: boolean; mode?: 'fill' | 'review' | 'summary';
validateOn?: 'change' | 'blur' | 'page' | 'finalize'; // default 'change'
page?: number; onPageChange?: (index: number, reason: 'next' | 'prev' | 'jump' | 'error' | 'trigger') => void;
persist?: boolean; engine?: FormEngine; // never both engine and submissionId
autosave?: boolean | { debounceMs: number; maxWaitMs?: number }; // min 250 ms; default settings.autosaveMs (2000)
renderers?: Partial<Record<ElementType, ElementComponent>>; components?: Partial<Components>;
classNames?: PartClassNames; styles?: PartStyles; // web: both; native: styles only
reviewBeforeFinalize?: boolean; fallback?: React.ReactNode;
slots?: { header?: SlotRenderer; footer?: SlotRenderer; nav?: SlotRenderer; watermark?: SlotRenderer };
keyboardVerticalOffset?: number; // native only
onChange?: (e: { type: 'value' | 'repeat_add' | 'repeat_remove' | 'repeat_move'; path: FieldPath; value: unknown; previous: unknown; submissionId: string; readonly data: Record<string, unknown> }) => void;
onSave?: (s: Submission) => void;
onFinalize?: (s: Submission) => void | boolean | Promise<void | boolean>; // return false or throw RasdError to veto
onInvalid?: (e: { errors: FieldIssue[]; page: number }) => void;
onComplete?: (e: { trigger: string; message?: LocalizedString }) => void;
onTriggerAction?: (id: string, payload: unknown, ctx: { submissionId: string; engine: FormEngine }) => void;
onError?: (e: RasdError) => void;
}

Event semantics: onChange fires after each engine flush (data is a lazy getter, materialised once); onSave after each autosave commit including the flush at pagehide / AppState background; onFinalize after validation passes and the submission is written — veto by false/throw keeps status draft, and the renderer enqueues to storage.outbox regardless of the sync prop; onInvalid when Next/Finalize is blocked (focus already on the first invalid field); onError on boundary catch, storage failure or RASD_LICENSE_EXPIRED (hard), bubbling to the provider's onError when unset — codes RASD_SCHEMA_INVALID / RASD_EXPR_* (non-recoverable card), RASD_STORAGE_QUOTA (banner + retry), RASD_STORAGE_*, RASD_ELEMENT_RENDER, RASD_MEDIA_*.

3.3 Hooks (stable unless tagged)

export function useRasdForm<T = FormApi>(selector?: (api: FormApi) => T, isEqual?: (a: T, b: T) => boolean): T;
export function useField<V = unknown>(path: FieldPath): FieldApi<V>;
export function useSubmission(id: string): { submission?: Submission; status?: SubmissionStatus; attachments: Attachment[]; patch(p: Partial<Submission>): Promise<void>; remove(): Promise<void>; loading: boolean };
export function useSync(): SyncView;
export function useLicense(): LicenseView; // { state, plan, features, expiresAt, graceUntil, enforcement, reason, clockSuspect, seats, can(), refresh() } — [15 §10.3]
export function useTheme(): ThemeView;
export function useLocale(): LocaleView; // { locale, dir, t, tf, formatNumber, formatDate, formatList, collator, setLocale, config } — [13 §1]
export function useDirection(): 'ltr' | 'rtl';
export function useRasdBusy(): boolean; // dirty || save in flight || finalize in flight || locale loading || media processing

export interface FormApi { engine: FormEngine; definition: FormDefinition; submissionId: string; status: SubmissionStatus; page: number; pages: { id: string; title: string; relevant: boolean; errorCount: number }[]; goTo(pageOrId: number | string): Promise<boolean>; next(): Promise<boolean>; prev(): Promise<void>; canNext: boolean; canFinish: boolean; validate(): ValidationResult; finalize(): Promise<Result<Submission, ValidationResult>>; save(): Promise<void>; dirty: boolean; errorCount: number; locale: string; setLocale(lc: string): void; busy: boolean }
export interface FieldApi<V> { value: V | undefined; setValue(v: V | undefined): void; blur(): void; touched: boolean; dirty: boolean; errors: FieldIssue[]; warnings: FieldIssue[]; infos: FieldIssue[]; relevant: boolean; required: boolean; readonly: boolean; calculated: boolean; element: Element; path: FieldPath; ids: { input: string; label: string; hint: string; error: string }; inputProps: InputA11yProps; state: FieldState }
export interface SyncView { status: SyncStatus; state: 'idle' | 'syncing' | 'paused' | 'offline' | 'error'; pending: { submissions: number; attachments: number }; lastSyncAt?: string; lastError?: RasdError; progress?: SyncProgress; conflicts: Conflict[]; syncNow(reason?: SyncReason): Promise<SyncRunReport>; pause(): void; resume(opts?: { allowMetered?: boolean }): void }
export interface ThemeView { theme: RasdTheme; tokens: ResolvedTokens; resolved: { colorScheme: 'light' | 'dark'; contrast: 'normal' | 'high'; density: 'comfortable' | 'compact' | 'spacious'; reducedMotion: boolean }; setMode(partial: RasdProviderProps['mode']): void; cssVar?(path: string): string /* web only: cssVar('color.primary') → 'var(--rasd-color-primary)' — [12 §4.2](12-theming.md) */; styles?<T>(factory: (t: ResolvedTokens) => T): T /* native only */ }
HookRe-renders whenOutside its context
useRasdForm()structure revision (page, status, dirty, errorCount, repeat count) — not on every valuethrows RASD_HOOK_CONTEXT
useField(path)that path's FieldState identity changesthrows RASD_HOOK_CONTEXT
useSubmission(id)storage change events for idloading: true forever without storage
useSync()sync stateChange / progress / error / conflictinert (state: 'idle', zero pending)
useLicense()license state transitions onlyevaluating on dev hosts, else limited
useTheme() / useLocale() / useDirection()provider prop change or OS signalprovider defaults
useRasdBusy()busy edges onlyfalse

SyncView.state projects SyncStatus.state (10 §4.9): running|acquiring|probing → 'syncing', backoff|authRequired|revoked → 'error', offline → 'offline', paused|wiping → 'paused', else 'idle'.

3.4 Registry — defineElement, createRegistry (stable)

export function defineElement<V = unknown>(d: { type: `x:${string}` | ElementType; component: ElementComponent<V>; valueSchema?: StandardSchema<V>; builder?: BuilderElementMeta; chunk?: () => Promise<{ default: ElementComponent<V> }> }): ElementDefinition<V>;
export function createRegistry(opts?: { elements?: ElementDefinition[]; components?: Partial<Components>; renderers?: Partial<Record<ElementType, ElementComponent>>; testers?: { rank: number; test: (el: Element, ctx: { def: FormDefinition; path: FieldPath }) => boolean; component: ElementComponent }[]; validators?: Record<string, CustomValidator>; functions?: Record<string, RelFunction>; unknownElement?: ElementComponent; extend?: Registry }): Registry;
export type ElementComponent<V = unknown> = React.ComponentType<{ field: FieldApi<V>; element: Element; path: FieldPath; variant?: string; mode: 'fill' | 'review' | 'summary'; readOnly: boolean; locale: string; dir: 'ltr' | 'rtl' }>;
export type CustomValidator = (input: { value: unknown; element: Element; path: FieldPath; data: Record<string, unknown>; locale: string }) => ValidatorOutcome | Promise<ValidatorOutcome>; // 5 s timeout ⇒ warning
export type ValidatorOutcome = { ok: true } | { ok: false; severity?: Severity; message: LocalizedString };
export function FieldWrapper(props: { field: FieldApi; children: React.ReactNode; hideLabel?: boolean }): JSX.Element;
export function preloadElements(types: ElementType[]): Promise<void>; // warms lazy chunks (web)

Resolution order for element type T: FormRenderer.renderers[T] → registry testers (rank desc) → registry renderers[T]defineElement entries → built-in → unknownElement (06 §5) — the RJSF-style registry plus JSON Forms-style tester ranking recommended in research/02 §3–4. defineElement throws RASD_REGISTRY_TYPE for a type that is neither built-in nor ^x:[a-z][a-z0-9-]*$.

3.5 Default components and theming props (stable names, beta internals)

components keys — fields: TextField, NumberField, RangeSlider, Rating, DateField, TimeField, DateTimeField, SelectOne, SearchSelect, SelectMultiple, Checkbox, ConsentBlock, Note, Group, Repeat, RankList, MatrixGrid, GeoPointField, GeoPathField, ImageField, AudioField, VideoField, FileField, SignatureField, BarcodeField, UnknownElement (heavy ones lazy under @rasd/react/elements/{matrix,rank,geo,media,barcode,signature,search-select}); layout: FieldWrapper, Page, PageNav, ProgressBar, ErrorSummary, RepeatItem, Button, Watermark, SummaryRow, LicenseBanner, plus native FormScroll, PageTransition, Sheet, DatePicker, Slider. Utility exports: Watermark, LicenseBanner, ErrorBoundary, RasdOfflineFormList (beta).

Theming props (web): classNames / styles / render per part keyed <Component>.<part> (value or (state) => value); every part carries class="rasd-<Component>__<part>" and data-scope="rasd" data-part. Native: styles per part only. Entries: @rasd/react/styles.css, @rasd/react/locales/<lc>, @rasd/native/locales/<lc>.

3.6 Example — custom element and custom validator

import { defineElement, createRegistry, FieldWrapper } from '@rasd/react'; // or '@rasd/native'
export const registry = createRegistry({
elements: [defineElement<string>({
type: 'x:beneficiary-lookup',
component: ({ field }) => (
<FieldWrapper field={field}>
<input {...field.inputProps} value={field.value ?? ''} onChange={(e) => field.setValue(e.target.value)} />
</FieldWrapper>),
builder: { label: { en: 'Beneficiary lookup', ar: 'بحث عن مستفيد' }, icon: 'id-card', group: 'advanced', inspector: [] },
})],
validators: { hhIdChecksum: ({ value }) => (luhn(String(value)) ? { ok: true } : { ok: false, message: { en: 'Invalid household ID' } }) },
});

3.7 Native-only additions (stable)

@rasd/native/keyboard-controller (KeyboardAwareFormScroll), @rasd/native/pickers/community (CommunityDatePicker), @rasd/native/app.plugin.js (Expo config plugin: backupExclusion, backgroundSync, permissions, usageDescriptions), securityReport(): Promise<SecurityReport>, preventScreenCapture(on: boolean). Peers: react-native >= 0.81 (New Architecture), optional expo-sqlite, @op-engineering/op-sqlite, RNGH 3, Reanimated 4, @shopify/flash-list.

4. @rasd/element<rasd-form> (FSL-1.1, beta)

SurfaceDetail
Attributesdefinition-url, definition-id, submission-id, locale, dir, theme (id or JSON URL), license, api (RSP base URL), namespace, read-only, validate-on, css-nonce
Propertiesdefinition, theme, initialData, registry, storage, sync, license (objects; React 19 maps object props to properties)
Methodsvalidate(): ValidationResult, finalize(): Promise<Result<Submission, ValidationResult>>, getData(), setValue(path, v), getSubmission()
Events (bubbles, composed)rasd:ready, rasd:change { path, value, data }, rasd:save { submission }, rasd:finalize { submission }, rasd:error { error: RasdError }, rasd:sync { pending, lastSyncedAt }
ProgrammaticdefineRasdElement(tag = 'rasd-form'), Rasd.render(target, options); IIFE window.Rasd = { version, ready, render, defineElement, createTheme, registerFunction, createDexieStorage, createSyncEngine, createLicense }
Errorsrasd:error only; lifecycle callbacks never throw. Form-associated (ElementInternals.setFormValue).
<script src="/vendor/rasd/rasd-forms.iife.js" defer></script>
<rasd-form definition-url="/forms/pdm-gfd-2026.form.json" locale="ar" dir="rtl" api="https://forms.example.org"></rasd-form>
<script>Rasd.ready.then(() => document.querySelector('rasd-form').addEventListener('rasd:finalize', e => console.log(e.detail.submission.id)));</script>

5. @rasd/builder (FSL-1.1, beta)

export function FormBuilder(props: FormBuilderProps): JSX.Element; // must render inside <RasdProvider>; lazy-load it
export interface FormBuilderProps {
definition?: FormDefinition; // undefined → "new form" wizard
onChange?: (def: FormDefinition, meta: { dirty: boolean; command: string; definitionHash: string; baseHash?: string }) => void; // debounced 250 ms
onPublish?: (req: PublishRequest) => Promise<PublishResult>;
plugins?: BuilderPlugin[]; locale?: string; theme?: RasdTheme | string;
features?: { logic?: boolean; translations?: boolean; json?: boolean; versions?: boolean; preview?: boolean; library?: boolean }; // all true by default
readOnly?: boolean; // forced true when license state is 'limited'
versions?: { list(formId: string): Promise<VersionSummary[]>; get(formId: string, version: string): Promise<FormDefinition> };
extSchemas?: Record<string, object>; // JSON Schema per vendor key
onMachineTranslate?: (req: { texts: string[]; from: string; to: string }) => Promise<string[]>;
onCommand?: (e: { id: string; args?: unknown }) => void;
}
export interface PublishRequest { definition: FormDefinition; previous?: { version: string; definitionHash: string }; definitionHash: string; diff: ReturnType<typeof diffDefinitions> | null; changelog: SemanticChange[]; force: boolean }
export type PublishResult = { ok: true; version: string; publishedAt: string } | { ok: false; code: 'RASD_VERSION_EXISTS' | 'RASD_SCHEMA_INVALID' | 'RASD_SYNC_REJECTED' | (string & {}); message: string; details?: unknown };
export interface BuilderPlugin { id: string; elements?: ElementDefinition[]; paletteGroups?: PaletteGroup[]; inspectorPanels?: InspectorPanel[]; commands?: BuilderCommand[]; extSchemas?: Record<string, object> }

BuilderPlugin members, BuilderElementMeta (label, description, icon, group, defaultProps, inspector, container, valueSchema, validate), InspectorField and BuilderApi (getDefinition, dispatch, select, announce, t) are specified in 08 §5 and §11.

CallbackWhenErrors
onChangeafter any command (debounced); immediately before publish and on visibilitychange: hidden
onPublishafter validateFormDefinition passes and no B-class change remains (unless force by an admin)host returns PublishResult; RASD_VERSION_EXISTS becomes an inline error
Builder-side refusalsE_PUBLISH_UNCHANGED, E_VERSION_NOT_MONOTONIC, E_VERSION_REUSED, E_BREAKING_CHANGE from validateFormDefinition(def, { previous })publish button disabled, Versions tab explains

Sub-paths: @rasd/builder/locales/<lc>, @rasd/builder/dnd-adapter (experimental, the swap point for @dnd-kit/react).

6. @rasd/storage, @rasd/storage-dexie, @rasd/storage-sqlite (FSL-1.1)

The StorageAdapter contract is normative in 00 §7 and fully typed in 09 §3; this section lists the factories and helpers around it.

ExportSignatureStabilityErrors
createMemoryStorage(opts?: { quotaBytes?: number; latencyMs?: number; faults?: FaultSpec }) => StorageAdapterstableRASD_STORAGE_QUOTA when simulated
createDexieStorage(opts: { namespace: string; encryptionKey?: CryptoKeyLike; blobs?: 'rows' | 'opfs'; worker?: boolean; persist?: boolean; migrations?: Migration[] }) => StorageAdapterstablefrom open(): RASD_STORAGE_UNAVAILABLE, RASD_STORAGE_KEY_UNAVAILABLE, RASD_STORAGE_MIGRATION, RASD_STORAGE_DOWNGRADE, RASD_STORAGE_CORRUPT
createSqliteStorage(opts: { driver: 'expo' | 'op'; namespace: string; encryptionKey?: CryptoKeyLike; directory?: string; enableFTS?: boolean; fileSystem?: FileSystemAdapter; migrations?: Migration[] }) => StorageAdapterstableas above; RASD_STORAGE_UNAVAILABLE when the driver peer is missing
secureStoreKeyProvider (@rasd/storage-sqlite)(opts?: { key?: string /* default 'rasd.dbkey.<namespace>' */ }) => { unlock(): Promise<string> } — 256-bit key created in expo-secure-store (WHEN_UNLOCKED_THIS_DEVICE_ONLY) on first usebetaRASD_STORAGE_KEY_UNAVAILABLE
deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, opts?: { iterations?: number /* 600_000 */ }) => Promise<CryptoKey> (PBKDF2-HMAC-SHA-256 wrapping key)stable
createCipher, wrapKey, unwrapKeyAES-256-GCM envelope helpers used by adapters and packExportbetaRASD_STORAGE_INTEGRITY on tag failure
runMigrations(ctx: { engine; tx; applied: AppliedRow[] }, steps: Migration[], opts?: { signal?; onProgress? }) => Promise<void>stableRASD_STORAGE_MIGRATION
packExport / unpackExport(chunks: AsyncIterable<ExportChunk>, sink: WritableStream | string, opts?: { encryptTo?; allowPlaintext?: boolean }) => Promise<ExportReport> and the inversestableRASD_STORAGE_INTEGRITY
listNamespaces (@rasd/storage-dexie)() => Promise<string[]>stable
runConformanceSuite (@rasd/storage/conformance)(factory: () => Promise<StorageAdapter>, opts?) => void — 180+ Vitest cases every adapter must passstable
@rasd/storage-sqlite/nodecreateSqliteStorage({ driver: 'better-sqlite3', … }) for Node testsbeta

Factory options are the defaults for open(); open(opts?) merges its argument over them (namespace must come from one of the two). Beyond the repos, every adapter carries state: 'closed' | 'opening' | 'open' | 'blocked' | 'error', import(chunks, opts?): Promise<ImportReport> (the inverse of export()), on('change' | 'blocked' | 'quota' | 'error' | 'migration', h): Unsubscribe and securityReport(), which returns { encryption: 'sqlcipher' | 'aes-gcm' | 'field' | 'none'; keyStore: string; persisted: boolean | null; backupExcluded: boolean | null; ephemeral: boolean; notices: string[] } (00 §7, 09 §3).

const storage = createDexieStorage({ namespace: 'wfp-jo', encryptionKey: await deriveKeyFromPassphrase(pass, salt) });
await storage.open();
const { items } = await storage.submissions.list({ formId: 'pdm-gfd-2026', status: ['draft'], limit: 20 });

7. @rasd/sync (FSL-1.1)

export function createSyncEngine(opts: SyncEngineOptions): SyncEngine; // stable
export interface SyncEngineOptions { storage: StorageAdapter; baseUrl?: string; getAuthToken: (ctx: { forceRefresh: boolean }) => Promise<string>; transport?: SyncTransport; policy?: DeepPartial<SyncPolicy>; deviceInfo?: Partial<DeviceInfo>; fetch?: typeof fetch; license?: LicenseHandle /* receives X-Rasd-License */; clock?: () => number; logger?: Logger }
export interface SyncEngine {
syncNow(reason?: SyncReason): Promise<SyncRunReport>; // single-flight; SyncReason = 'manual' | 'online' | 'foreground' | 'finalize' | 'timer' | 'event' | 'background'
pause(): void; resume(opts?: { allowMetered?: boolean }): void;
status(): SyncStatus; getLog(opts?: { since?: string; level?: 'debug' | 'info' | 'warn' | 'error' }): SyncLogEntry[]; getMetrics(): SyncMetrics;
retry(ids: string[]): Promise<void>; // dead-lettered outbox ops → pending
resolveConflict(id: string, resolution: 'keep-local' | 'keep-server' | { merged: Record<string, unknown> }): Promise<void>; // experimental (records)
on<E extends keyof SyncEvents>(event: E, h: (e: SyncEvents[E]) => void): () => void;
dispose(): Promise<void>;
}
export interface SyncEvents { progress: SyncProgress; error: RasdError; conflict: Conflict; formUpdated: { id: string; version: string; definitionHash: string }; datasetUpdated: { name: string; rows: number }; licenseRefreshed: { exp: string }; stateChange: SyncStatus; remoteWipe: { reason: string; unsyncedCount: number }; policyUpdated: DevicePolicy }
export interface SyncRunReport { reason: SyncReason; startedAt: string; endedAt: string; phases: Partial<Record<'submissions' | 'attachments' | 'forms' | 'datasets' | 'records' | 'purge', { done: number; failed: number; bytes?: number }>>; error?: RasdError }
export function createRspTransport(opts: { baseUrl: string; fetch?: typeof fetch; getAuthToken: SyncEngineOptions['getAuthToken']; clientHeader?: string }): SyncTransport; // stable (default when transport omitted)
export function createOpenRosaTransport(opts: { baseUrl: string; profile: 'central' | 'kobo' | 'ona'; getAuthToken: SyncEngineOptions['getAuthToken']; fetch?: typeof fetch }): SyncTransport; // experimental (phase 3)

SyncPolicy groups and defaults — batch (50 items / 5 MiB), attachments (5 MiB adaptive chunks 256 KiB–8 MiB, parallel 2, onMetered 'wifiOnly' | 'always' | 'ask'), backoff (1 s → 300 s, deadAfterAttempts 5), timers (pullIntervalMs 900_000, probeIntervalMs 60_000, requestTimeoutMs 30_000, chunkTimeoutMs 120_000, leaseMs 120_000), triggers, retention (purgeSyncedAfterDays 7, keepSentMetadataDays 90), background, records, requireHttps, log — are listed with every default in 10 §4.1; server DevicePolicy is merged over them at registration.

syncNow() resolves with report.error for run-level failures and emits error; it rejects only with RASD_SYNC_DISPOSED. Item-level outcomes: RASD_SYNC_REJECTED (submission → rejected), RASD_SYNC_CONFLICT, RASD_ATTACHMENT_FAILED, RASD_SYNC_AUTH (state authRequired), RASD_SYNC_NETWORK (retryable, backoff), RASD_SYNC_REVOKED. SyncTransport, SyncStatus, SyncProgress are in 03 §8 and 10 §4.9.

8. @rasd/pwa (FSL-1.1, beta)

Export (entry)SignatureNotes
useServiceWorkerUpdate (@rasd/pwa)(opts?: { registration?; workbox?; vitePwa?; mode?: 'prompt' | 'auto'; checkIntervalMs?: number /* 3_600_000 */; deferWhileBusy?: boolean }) => { updateAvailable; offlineReady; busy; deferred: boolean; applyUpdate(opts?: { force?: boolean }): Promise<'applied' | 'deferred'>; dismiss(): void }Never reloads on its own; force while busy logs RASD_SW_UPDATE_WHILE_BUSY (11 §5.1)
useInstallPrompt() => { canPrompt: boolean; platform: 'chromium' | 'ios-safari' | 'other'; installed: boolean; prompt(): Promise<'accepted' | 'dismissed' | 'unavailable'> }
useStoragePersistence, requestPersistence() => { persisted: boolean | null; usageBytes: number; quotaBytes: number | null; headroomBytes: number; low: boolean; request(): Promise<boolean> } / () => Promise<boolean>
precacheForms(opts: { definitions: FormDefinition[]; concurrency?: number /* 4 */; maxBytes?: number; onProgress?; signal? }) => Promise<{ cached: number; skipped: number; failed: { url: string; status?: number }[]; bytes: number }>Never throws on per-URL failure
connectBackgroundSync(opts: { sync: SyncEngine; registration: ServiceWorkerRegistration | Promise<ServiceWorkerRegistration> }) => () => voidChromium only; the outbox stays the single queue
isStandalone, InstallHint() => boolean / React.FC<{ locale?: string; onDismiss?(): void }>
registerRasdRoutes, rasdSerwistRoutes, RasdOutboxPlugin (@rasd/pwa/sw)(opts: RasdRouteOptions) => void / (opts) => SerwistRuntimeCaching[] / new RasdOutboxPlugin({ tag?: 'rasd-outbox' })Worker context only
rasdRuntimeCaching, rasdWorkboxConfig (@rasd/pwa/workbox-config)(opts: RasdRouteOptions) => RuntimeCaching[] / Partial<GenerateSWOptions>Data only; safe in Node config files

RasdRouteOptions (apiOrigin, definitionUrls, mediaOrigins, fontOrigins, cacheVersion, limits) is in 11 §2; Workbox 7.4.x is the peer (research/05).

9. @rasd/media (FSL-1.1, beta)

export const web: MediaAdapters; export const native: MediaAdapters; // lazy proxies; resolved by the react-native export condition
export function createMediaAdapters(overrides?: Partial<MediaAdapters> & { policy?: Partial<MediaPolicy> }): MediaAdapters;
export function permissionFor(kind: 'camera' | 'location' | 'microphone' | 'photos'): Promise<PermissionState>;
export function compressImage(input: Blob | string, opts: { maxPixels: number; quality: number; keepExif?: boolean }): Promise<CapturedFile>; // web worker / expo-image-manipulator

Adapter interfaces (GeolocationAdapter, CameraAdapter, BarcodeAdapter, SignatureAdapter, AudioAdapter, VideoAdapter, FilePickerAdapter, MapAdapter, MediaPolicy, CapturedFile, GeoFix, PermissionState) are normative in 14 §2. One factory pair per @rasd/media/<capability> entry: createWebCamera / createExpoCamera, createWebGeolocation / createExpoLocation, createWebBarcode({ wasmUrl }) / createExpoBarcode, createCanvasSignature / createSvgSignature, createWebAudio / createExpoAudio, createWebFilePicker / createExpoDocumentPicker, createMaplibreMap. Errors: RASD_MEDIA_PERMISSION, RASD_MEDIA_UNAVAILABLE, RASD_MEDIA_LIMIT, RASD_MEDIA_TYPE; cancellation resolves null/[].

import { web, createMediaAdapters } from '@rasd/media';
import { createExpoCamera } from '@rasd/media/camera';
const media = createMediaAdapters({ ...web, camera: createExpoCamera(), policy: { image: { maxPixels: 1600 } } });

10. @rasd/license (FSL-1.1, stable)

export function createLicense(opts: { token?: string; tokenEndpoint?: string; siteKey?: string; fetcher?: TokenFetcher; storage: StorageAdapter; appId?: string; clock?: () => number; freeFeatures?: string[] }): LicenseHandle;
export type TokenFetcher = (current: string | null) => Promise<{ token: string; serverTime?: string } | null>;
export type LicenseState = 'evaluating' | 'trial' | 'active' | 'grace' | 'limited' | 'invalid';
export interface LicenseHandle {
readonly state$: { subscribe(listener: (s: LicenseSnapshot) => void): () => void; get(): LicenseSnapshot };
getState(): LicenseState; getSnapshot(): LicenseSnapshot;
refresh(): Promise<void>; // coalesced; one call per 30 s per namespace
setToken(token: string, opts?: { serverTime?: string }): Promise<void>; // used by the sync engine for X-Rasd-License
can(feature: string): boolean;
on(event: 'stateChange' | 'licenseRefreshed' | 'error', h: (e: LicenseSnapshot | RasdError) => void): () => void;
dispose(): void;
}
export interface LicenseSnapshot { state: LicenseState; plan?: 'trial' | 'starter' | 'team' | 'enterprise'; features: string[]; apps: string[]; enforcement: 'soft' | 'hard'; expiresAt?: string; graceUntil?: string; seats?: number; reason?: InvalidReason; clockSuspect: boolean; kid?: string }
export type InvalidReason = 'MALFORMED' | 'WRONG_TYP' | 'UNKNOWN_KID' | 'REVOKED' | 'BAD_SIGNATURE' | 'NOT_YET_VALID' | 'APP_MISMATCH' | 'SCHEMA' | 'ISSUER'; // nine values, ratified in [00 §11]: SCHEMA = claims failed the zod schema, ISSUER = iss !== 'rasd'; a bad or absent alg folds into WRONG_TYP
// Seven SCREAMING_SNAKE values covering the verification steps in [15 §4.1](15-licensing-and-billing.md).
// Claim-shape failures (payload rejected by the zod schema) and a wrong issuer (`iss` ≠ "rasd") collapse into MALFORMED;
// a bad or absent `alg` collapses into WRONG_TYP.
export function verifyToken(token: string, opts?: { keys?: Record<string, string>; now?: number; app?: string }): { ok: true; claims: RltClaims } | { ok: false; reason: InvalidReason }; // pure; used by CLI and server
ParameterNotes
tokenEndpointRecommended production channel; built-in fetcher POSTs { current, sdk, app }.
siteKeyPrototyping only; console warning on any non-dev host.
freeFeaturesFeatures always granted (the Option B switch, 15 §4.4).
storagekv keys license.token, license.lastServerTime, license.lastSeenLocal, license.trial.startedAt.

Errors are events, never thrown in the render path: RASD_LICENSE_INVALID, RASD_LICENSE_EXPIRED (hard enforcement, via renderer onError), RASD_LICENSE_REFRESH_FAILED. Verification uses WebCrypto Ed25519 where available and @noble/ed25519 otherwise (research/06).

11. @rasd/themes (Apache-2.0, stable)

export const rasdLight: RasdTheme; export const rasdDark: RasdTheme; export const rasdHighContrast: RasdTheme; export const rasdField: RasdTheme;
export const themes: Record<'rasd-light' | 'rasd-dark' | 'rasd-high-contrast' | 'rasd-field', RasdTheme>;
export function createTheme(partial: DeepPartial<RasdTheme> & { id: string }, opts?: { extends?: string | RasdTheme }): RasdTheme; // throws RASD_THEME_INVALID
export function resolveTheme(theme: RasdTheme, mode?: { colorScheme?; contrast?; density?; reducedMotion?; direction?: 'ltr' | 'rtl' }): ResolvedTokens;
export function toCss(theme: RasdTheme, opts?: { selector?: string /* '.rasd-root[data-theme="<id>"]' */; layer?: string /* 'rasd' */ }): string;
export function fromDtcg(tokens: object, opts?: { id: string; theme?: string; contrast?: string; resolver?: object }): RasdTheme; // DTCG 2025.10
export function toDtcg(theme: RasdTheme): object;
export function validateTheme(theme: unknown): { ok: boolean; errors: Issue[]; warnings: Issue[] };
export function checkContrast(theme: RasdTheme): { pairs: { fg: string; bg: string; ratio: number; required: number; ok: boolean }[]; ok: boolean }; // WCAG 2.2 AA
export const fonts: { notoSansArabic: FontAsset; notoNaskhArabic: FontAsset }; // OFL; WOFF2 (web) / TTF (native)

RasdTheme is the JSON in 00 §10 (id, name, extends, mode, tokens.{color,typography,spacing,radius,elevation,motion,control}, components, ext), validated by docs/schema/rasd-theme.schema.json. createTheme resolves extends chains to depth 8 and rejects cycles.

12. @rasd/xlsform (Apache-2.0, beta)

export function importXlsform(input: ArrayBuffer | Uint8Array | Blob | XlsformWorkbook, opts?: { locales?: 'header' | 'code'; nestGroups?: boolean; coercion?: 'rel' | 'xpath'; kobo?: boolean; strict?: boolean }): Promise<{ definition: FormDefinition; warnings: ImportIssue[]; report: ImportReport }>;
export function importXform(xml: string, opts?: { coercion?: 'rel' | 'xpath' }): Promise<{ definition: FormDefinition; warnings: ImportIssue[]; xform: XformInfo }>; // XForm XML → RFD (forms pulled from servers)
export function importSurveyJs(json: unknown, opts?: { defaultLocale?: string }): Promise<{ definition: FormDefinition; warnings: ImportIssue[] }>; // experimental
export function exportXlsform(def: FormDefinition, opts?: { target?: 'generic' | 'kobo' | 'central' /* 'generic' */; locales?: 'header' | 'code'; format?: 'xlsx' | 'csv-zip' }): Promise<{ bytes: Uint8Array; warnings: ImportIssue[] }>;
export function xpathToRel(xpath: string, ctx?: { scopePath?: string }): { rel: string; lossless: boolean }; // throws RASD_XLSFORM_UNSUPPORTED
export function relToXpath(rel: string): { xpath: string; lossless: boolean };
export function serializeXformInstance(sub: Submission, def: FormDefinition, xform: XformInfo, opts?: { emitEmpty?: boolean; deprecatedId?: string }): { xml: string; media: MediaPart[] }; // used by createOpenRosaTransport
export function parseXformInstance(xml: string, def: FormDefinition, xform: XformInfo): { data: Record<string, unknown>; attachments: Partial<Submission['attachments']>; warnings: ImportIssue[] };
export type XlsformWorkbook = { survey: Record<string, unknown>[]; choices?: Record<string, unknown>[]; settings?: Record<string, unknown>[]; entities?: Record<string, unknown>[] };
export interface ImportIssue { code: string; severity: Severity; sheet?: string; row?: number; column?: string; path?: string; message: string; original?: string }
// Warning codes: W_XPATH_UNMAPPED, W_FUNCTION_UNKNOWN, W_REL_UNEXPORTABLE, W_EXPORT_DOWNGRADED (catalogue in [20 §1–2](20-interoperability.md)),
// plus W_UNLICENSED — appended to `warnings` when the `xlsform` feature is absent; import/export never throw for licensing ([15 §4.4](15-licensing-and-billing.md)).
// ImportReport ({ summary, issues, expectedMedia, languages, pyxform? }), XformInfo ({ rootName, id, version,
// formhubUuid?, nodeOrder, paths, metaNs? }) and MediaPart are normative in [20 §1–2](20-interoperability.md).
FunctionBehaviourErrors
importXlsformMaps survey/choices/settings/entities, label::Lang (code) columns, Kobo begin_score/rank/kobomatrix; unmappable XPath kept under ext["org.getodk.xpath"] with W_XPATH_UNMAPPED in report.issues; strict turns unmapped into failure.RASD_XLSFORM_IMPORT (details.sheet, row, column)
exportXlsformtarget: 'generic' emits a workbook pyxform 4.5.0 compiles (research/14); 'kobo'/'central' add platform columns; REL without an XPath equivalent becomes W_REL_UNEXPORTABLE + a calculation placeholder.RASD_XLSFORM_EXPORT
const { definition, warnings, report } = await importXlsform(await file.arrayBuffer(), { kobo: true });
if (report.summary.errors === 0) { const { bytes } = await exportXlsform(definition, { target: 'kobo' }); }

Peer: xlsx (SheetJS) for .xlsx I/O; the CSV path has no dependency.

13. @rasd/cli (Apache-2.0)

Binary rasd. Global flags: --json (machine output on stdout, human text on stderr), --quiet, --no-color, --cwd <dir>. Exit codes: 0 ok, 1 errors, 2 warnings under --strict or license limited, 3 invalid license/token, 64 usage error.

CommandFlagsStabilityBehaviour
rasd validate <form.json…>--previous <file>, --functions a,b, --features a,b, --limit key=n (repeatable, tightens a 04 §16 limit; looser values are ignored), --translations warn|error, --strictstablevalidateFormDefinition; every flag maps onto an option the function already takes; prints code path message; JSON { ok, errors, warnings }
rasd convert xlsform <in.xlsx>-o <out.form.json>, --id, --version, --locale, --kobo, --strictstableimportXlsform; exit 1 on RASD_XLSFORM_IMPORT
rasd convert rfd <in.form.json> --to xlsform-o <out.xlsx>, --target generic|kobo|central, --format xlsx|csv-zipbetaexportXlsform
rasd types <form.json>-o <types.d.ts>, --name <Interface>, --zodstableEmits interface <Name>Data (+ optional zod schema) matching toSubmission().data
rasd theme check <theme.json…>--strict, --min-touch 24stablevalidateTheme + checkContrast; fails below 4.5:1 text / 3:1 UI
rasd i18n check <form.json> / --catalogs--locales ar,fr, --strictstabletranslationReport; --catalogs lints chrome catalogs
rasd i18n export / rasd i18n import--format xliff|csv, --locales, -obetaRound-trips translation cells
rasd diff <from.json> <to.json>--plan-out <plan.json>stablediffDefinitions; exit 1 when loss !== 'none' under --strict
rasd hash <form.json>stablePrints definitionHash
rasd license check--token, --env RASD_LICENSE, --file rasd-license.rlt, --app <origin|bundleId>stableExit 0 ok / 2 limited / 3 invalid
rasd license trial / rasd license refresh--email, --out .rasdrc|- / --org-secret-env RASD_ORG_SECRETbetaSignup / CI refresh
rasd doctor--url <origin>, --namespacebetaStorage estimate, persistence, migration state, license state, outbox age
rasd sign <form.json>--key <ed25519.pem>experimentalJWS-signs a definition for servers publishing signed RFDs

13.1 What 0.1 actually ships

@rasd/cli 0.1 implements the E0.8 slice plus the commands whose backing functions already exist: validate, hash, convert xlsform, convert rfd, theme check, license check, version. Each wraps a library function unchanged, so the CLI cannot develop its own idea of what "valid" means.

types, diff, i18n, doctor, sign, license trial and license refresh need engines that do not exist yet (diffDefinitions, translationReport, a storage probe, a signer, the issuer API). Running one exits 64 and names this section, rather than failing obscurely or — worse — succeeding silently.

Two deliberate departures from the table above, both recorded in 00 §14:

  • --version is not global. The table gives --version to convert xlsform (the form's version). One flag cannot mean two things, so the CLI's own version is the rasd version command.
  • license check gains --key <kid>=<base64url>. The embedded key set holds Rasd's own signing keys, so a team running the reference issuer from @rasd/server — which signs with its own kid — could not check its tokens at all. Verification stays entirely offline: the flag supplies a key, it does not fetch one.

A third difference is behavioural rather than surface: a token that fails to verify reports invalid, where deriveState would answer evaluating for a node host. evaluating is the developer-machine leniency that keeps a dev build running without a token; handing that word to a release pipeline would tell it the opposite of the truth.

14. @rasd/testing (Apache-2.0, beta)

export function renderForm(def: FormDefinition, opts?: { platform?: 'web' | 'native'; initialData?; submissionId?; locale?; storage?: StorageAdapter; clock?: FakeClock; network?: FaultyNetwork; registry?: Registry; license?: LicenseHandle; media?: MediaAdapters; sync?: boolean | SyncEngine; validateOn? }): Promise<RenderedForm>;
export interface RenderedForm extends RenderResult { engine: FormEngine; storage: StorageAdapter; clock: FakeClock; sync?: SyncEngine; field(name: string): HTMLElement | ReactTestInstance; fill(name: string, value: unknown): Promise<void>; next(): Promise<void>; prev(): Promise<void>; finalize(): Promise<Result<Submission, ValidationResult>>; expectError(name: string, matcher?: string | RegExp): void; submission(): Promise<Submission>; kill(): Promise<void>; restart(): Promise<RenderedForm> }
export function fakeStorage(opts?: { kind?: 'memory' | 'dexie' | 'sqlite'; quotaBytes?: number; latencyMs?: number }): StorageAdapter & { failNext(method: string, err: RasdError): void; delay(method: string, ms: number): void; disconnect(): void; reconnect(): void };
export function createFakeClock(start?: string | number): FakeClock; // spine §11 name (alias: fakeClock); { now(); advance(ms); set(iso); tick() }
export function faultyNetwork(opts?: { dropResponses?: number; latencyMs?: [number, number]; offlineFor?: number; status?: Record<string, number> }): FaultyNetwork; // fetch shim + navigator.onLine control
export function createFaultyTransport(opts?: { faults?: FaultSpec; forms?: FormDefinition[]; datasets?: Record<string, DatasetRow[]>; verdict?: (s: Submission) => 'accepted' | 'duplicate' | 'rejected' | 'conflict' }): SyncTransport & { received: Submission[] }; // spine §11 name (alias: fakeTransport)
export function fakeMedia(opts?: { geolocation?: { fixes: GeoFix[]; permission?: PermissionState }; camera?: { files: CapturedFile[] }; barcode?: { results: string[] }; audio?: { durationMs: number; bytes: number }; permissions?: Partial<Record<string, PermissionState>> }): MediaAdapters;
export function fakeLicense(state: LicenseState, opts?: { features?: string[]; enforcement?: 'soft' | 'hard' }): LicenseHandle;
export function assertAccessibleContract(Component: ElementComponent | Components[keyof Components], opts?: { platform?: 'web' | 'native'; element?: Element; locale?: string }): void; // fails when a replacement drops the accessible-props contract (ids, aria-labelledby / aria-describedby, aria-invalid, aria-required, dir; native equivalents) — [12 §6](12-theming.md)
export const fixtures: { minimal: FormDefinition; pdmGfd2026: FormDefinition; kitchenSink: FormDefinition; perf500: FormDefinition; repeats200: FormDefinition };

renderForm wires <RasdProvider><FormRenderer/></RasdProvider> with @testing-library/react (web) or RNTL 14 (platform: 'native'); kill()/restart() simulate process death and reopen the same storage. Every fake throws RasdErrors with the same codes as production adapters.

14.1 What 0.1 actually ships

The signatures above are the target surface. @rasd/testing 0.1 implements the E0.6 slice (19 §epics) — the five names frozen in 00 §11, all headless:

Shipped in 0.1Notes against the target above
renderForm(def, opts?)Headless: drives @rasd/core directly, returns no RTL queries and takes no platform, registry, license, media or sync. fill/next/prev/goTo/expectError/kill/restart are present; fill and the navigation helpers are synchronous because there is no render to await. Paging follows the renderer's rule (skip irrelevant pages, refuse to leave a page with blocking errors).
createFakeClock(opts?)Takes { start, autoAdvanceMs } rather than a bare start. Adds iso(), fn, runAll(), pending(), nextDue(), timer functions and install().
createFaultyTransport(opts)Wraps a SyncTransport ({ target, faults, dropResponses, seed, … }) instead of standing one up. It is a decorator, not an in-memory server — the reference server in @rasd/server already is one, and duplicating its verdict logic would create a second source of truth about what the protocol accepts. dropResponse calls through before throwing, which is what makes it test idempotency rather than just retries.
assertAccessibleContract(contract, rendered, opts?)Takes the props object and the rendered node, not a component (see the amendment in 00 §14). Reads DOM attributes or React/RNTL host props, so it works under both runners without depending on either.
fixturesBuilders, not constants: minimal(), consentGated(), repeats(), validation(), calculated(), multiPage(), large({ questions }), fromJson(). large() covers what perf500/repeats200 were for; the agency forms stay in docs/examples/ and load via fromJson().
@rasd/testing/conformancerunConformanceSuite(), re-exported from @rasd/storage behind a separate entry point so the default entry needs no storage peer.

Since added: fakeStorage() (a createMemoryStorage wrapper with failNext / delay / disconnect / reconnect and a call log — a Proxy rather than a hand-listed wrapper, so it keeps covering the contract as it grows), fakeLicense(state, opts?) (every licence state in one line, with a stable snapshot — an unstable one spins useSyncExternalStore until React gives up), and @rasd/testing/react, the RTL variant of renderForm from 06 §18. It lives behind its own entry point because @rasd/react, react, react-dom and @testing-library/react are all needed for it and none of them is needed to test form logic.

Its errors(name) reads the DOM, not the engine, and returns the text the renderer is actually showing. The engine knows a required field is empty the moment the form mounts; the renderer deliberately does not reveal it until the field is touched or the page is validated (06 §7), so a helper reporting the engine's view would pass a test asserting an error the enumerator cannot see.

Also added: rspConformance({ baseUrl, getAuthToken, supports?, runId? }) — the RSP v1 suite of 10 §11, the server-side twin of runConformanceSuite(). It speaks only HTTP against a baseUrl, so an agency that writes its own RSP server in Go runs exactly what the reference server runs, and a green result means their devices will sync. baseUrl may be a function, which is usually what you want: the suite registers its tests at module load, before any beforeAll has bound an ephemeral port. Optional surfaces (publish, datasetImport, attachments, quarantine, records, sse, remoteWipe, definitionSignature) are declared through supports, because a server is conformant without them (10 §10) and a suite that failed on their absence would be untrue. errorBodyProblem(body) and isUtcIso(value) are exported separately for a server's own tests. It covers checks 1–10, 13 and 17; records, SSE, remote wipe, definition signatures and device revocation (11, 12, 14, 15, 16) still need a server-side admin hook the suite does not yet take.

Still not built: the RNTL variant (platform: 'native'), faultyNetwork() (the lower-level fetch shim — createFaultyTransport() covers the transport level), fakeMedia(), propertyEngine() and @rasd/testing/axe.

15. @rasd/server (FSL-1.1, beta)

export function createRasdServer(opts: { db: DbAdapter; blobs: BlobStoreAdapter; auth: AuthAdapter; hooks?: ServerHooks; license?: LicenseServiceOptions | false; settings?: ServerSettings }): { app: Hono; rsp: Hono; license?: Hono; start(port?: number): Promise<{ close(): Promise<void> }>; migrate(): Promise<void>; healthz(): Promise<{ ok: boolean; db: boolean; blobs: boolean }> };
export function createRspApp(opts: { db: DbAdapter; blobs: BlobStoreAdapter; auth: AuthAdapter; hooks?: ServerHooks }): Hono; // RSP routes only, mountable under any prefix
export function createLicenseService(opts: { db: DbAdapter; signingKeys: { kid: string; privateKey: Uint8Array }[]; billing?: BillingAdapter; issuerUrl: string }): Hono; // POST /v1/trials, /v1/tokens/refresh, /v1/tokens/site, GET /.well-known/jwks.json
export function createPostgresDb(opts: { connectionString: string; schema?: string; pool?: { max?: number } }): DbAdapter;
export function createS3BlobStore(opts: { bucket: string; endpoint?: string; region?: string; credentials?: unknown; tus?: { maxSize?: number /* 100 MiB */; expirationMs?: number /* 7 d */ } }): BlobStoreAdapter;
export function jwtAuth(opts: { jwksUrl?: string; secret?: string; issuer: string; audience?: string; claims?: { org: string; user: string; roles?: string } }): AuthAdapter;
export function staticApiKeyAuth(keys: Record<string, { orgId: string; userId: string; roles: string[] }>): AuthAdapter; // dev only
export function centralAppUserAuth(opts: { lookup(token: string): Promise<{ orgId: string; userId: string; roles: string[] } | null> }): AuthAdapter;
export interface ServerHooks { validateSubmission?(s: Submission, def: FormDefinition): Promise<{ verdict: 'accepted' | 'rejected'; reasons?: { code: string; message: string; field?: string }[] } | void>; onEvent?(e: WebhookEvent): Promise<void> }
export function tokenEndpointHandler(opts: { orgSecret: string; licenseUrl?: string; cacheMs?: number /* 24 h */ }): (c: HonoContext) => Promise<Response>; // '@rasd/server/license' proxy recipe
export function xRasdLicenseMiddleware(opts: { issue(orgId: string): Promise<string | null> }): MiddlewareHandler;

AuthAdapter (verify, issueLicense?, onDeviceRegistered?), routes, Postgres DDL, tus handling, quarantine, exports and webhooks are specified in 10 §9. Node ≥ 20 (Node 22 LTS recommended, research/08); Postgres 15+; any S3-compatible store.

const server = createRasdServer({
db: createPostgresDb({ connectionString: process.env.DATABASE_URL! }),
blobs: createS3BlobStore({ bucket: 'rasd-attachments', endpoint: 'https://minio.internal:9000' }),
auth: jwtAuth({ jwksUrl: 'https://id.example.org/.well-known/jwks.json', issuer: 'https://id.example.org' }),
});
await server.migrate();
const { close } = await server.start(8080);

16. Consolidated RasdError code table

CodePackageSurfaces asretryabledetails
RASD_SCHEMA_INVALIDcorethrown by createFormEngine; renderer onErrorno{ issues: Issue[] }
RASD_EXPR_PARSE / RASD_EXPR_UNKNOWN_REF / RASD_EXPR_UNKNOWN_FUNCTION / RASD_EXPR_CYCLEcorethrown at load (parseExpression, createFormEngine)no{ source, offset, expected } / { path, property, name? | cycle[] }
RASD_EXPR_FUNCTION_EXISTS / RASD_ENGINE_DISPOSED / RASD_I18N_SYNTAX / RASD_UNSUPPORTED_SPEC / RASD_MIGRATION_REFUSED / RASD_THEME_INVALIDcore, themesthrownno{ name } / — / { text, offset } / { rasd, supports, requires? } / { reason, step? } / { issues }
RASD_EXPR_RUNTIME / RASD_EXPR_BUDGET / RASD_EXPR_TRIGGER_LOOPcorediagnostic event only{ path, property, reason }
RASD_ABORTEDallrejected when signal abortsyes
RASD_HOOK_CONTEXT / RASD_REGISTRY_TYPE / RASD_ELEMENT_RENDERreact, nativethrown (hooks, registry) / onError (boundary)no{ hook | type | path, componentStack }
RASD_STORAGE_QUOTA / RASD_STORAGE_LOCKEDstoragerejected; quota eventyes{ usageBytes, quotaBytes } / { expectedRev?, actualRev? }
RASD_STORAGE_MIGRATION / _DOWNGRADE / _KEY_UNAVAILABLE / _CORRUPT / _INTEGRITY / _NOT_OPEN / _UNAVAILABLEstoragerejected (mostly from open())no{ migrationId?, storedVersion?, table?, id? }
RASD_SYNC_REJECTEDsyncerror event; submission → rejectedno{ submissionId, reasons: { code, message, field? }[] }
RASD_SYNC_NETWORKsyncerror event; state backoffyes{ httpStatus?, url }
RASD_SYNC_AUTH / RASD_SYNC_REVOKED / RASD_SYNC_CONFLICT / RASD_SYNC_DISPOSED / RASD_ATTACHMENT_FAILEDsync (media)error / conflict events; syncNow rejects only when disposedno{ httpStatus } / { recordId, local, server } / { attachmentId, reason }
RASD_LICENSE_INVALID / RASD_LICENSE_EXPIRED / RASD_LICENSE_REFRESH_FAILEDlicenseevents (RASD_LICENSE_EXPIRED also via renderer onError under hard)refresh: yes{ reason?, apps?, host?, exp?, graceUntil?, status? }
RASD_MEDIA_PERMISSION / RASD_MEDIA_UNAVAILABLE / RASD_MEDIA_LIMIT / RASD_MEDIA_TYPEmediarejected by adapterspermission: when askable{ reason | limit, actual, kind | mime }
RASD_POLICY_VIOLATIONreact, native, storageonPolicyViolationno{ policy, path? }
RASD_XLSFORM_IMPORT / RASD_XLSFORM_EXPORT / RASD_XLSFORM_UNSUPPORTEDxlsformrejected / thrownno{ sheet?, row?, column?, xpath? }
RASD_VERSION_EXISTSbuilder, serverPublishResult.code; server 409no{ id, version }
RASD_SW_UPDATE_WHILE_BUSYpwaconsole warning only

RSP wire error codes (batch_too_large, cursor_expired, checksum_mismatch, device_revoked, …) are lower-case strings in the HTTP body (10 §1.4) and are mapped to the codes above by the sync engine.

17. Event catalogue

SourceSubscribe viaEventPayload
FormEnginesubscribe(l) / on('patch')patchEnginePatch { rev, changedPaths, structural }
FormEnginesubscribeField(path)FieldState
FormEngineon()diagnostic, trigger, complete, message, customAction, locale, finalized§2.3
<FormRenderer>propsonChange, onSave, onFinalize, onInvalid, onPageChange, onComplete, onTriggerAction, onError§3.2
<RasdProvider>propsonError, onAudit, onPolicyViolation, onStorageBlockedRasdError / AuditEvent / info
StorageAdapteron()change, blocked, quota, error, migration{ table, ids, source: 'local' | 'other-tab' | 'sync' }, { reason, otherTabs }, { usageBytes, quotaBytes, level: 'warning' | 'critical' } (09 §3), RasdError, { id, phase, done, total }
SyncEngineon()progress, error, conflict, formUpdated, datasetUpdated, licenseRefreshed, stateChange, remoteWipe, policyUpdated§7
LicenseHandlestate$.subscribe / on()stateChange, licenseRefreshed, errorLicenseSnapshot / RasdError
<FormBuilder>propsonChange, onPublish, onMachineTranslate, onCommand§5
<rasd-form>DOM addEventListenerrasd:ready, rasd:change, rasd:save, rasd:finalize, rasd:error, rasd:syncCustomEvent.detail per §4
Service workerpostMessagerasd:sync-wake{ type: 'rasd:sync-wake' }
RSP server (SSE)GET /v1/eventsformPublished, datasetUpdated, recordChanged, policyUpdatedtrigger-only
RSP server (webhooks)HTTP POSTsubmission.accepted, submission.completed, submission.quarantined, record.updated, record.conflict, form.published, device.registeredentity + { event, orgId, occurredAt }

18. Acceptance criteria

  • Every export in this document exists with the stated name, signature and JSDoc @stability tag; the api-extractor report is committed per package and a PR that changes a stable signature fails CI unless a major changeset is present.
  • publint and @arethetypeswrong/cli pass on every npm pack output; no import outside the documented exports map resolves.
  • RASD_ERROR_CODES in @rasd/core equals the union of §16; each adapter package has a test asserting it never rejects with a non-RasdError; every event in §17 has a contract test.
  • @rasd/react and @rasd/native export lists are diffed in CI and are identical except the documented native-only additions; every hook has an "outside provider" behaviour test.
  • Type-level tests (expect-type) cover Element narrowing by type, FieldApi<V> inference from defineElement<V>, and Result narrowing on finalize(); TypeDoc builds without warnings.

Open questions

  • Should migrateSubmission return a report alongside the Submission (breaking the spine's Submission return type), or is the separate planMigration() dry-run introduced here sufficient?
  • registerFunction is process-global while 05 §8 treats functions as per-engine; confirm the "default registry + per-engine functions" model or move to registry objects only.
  • useSync().state — keep the 5-value projection for hosts, or expose only the 10-value SyncStatus.state?
  • Ship createRasdServer (all-in-one) in addition to createRspApp / createLicenseService, or keep only the two composable factories?
  • Name and home of secureStoreKeyProvider (@rasd/storage-sqlite vs @rasd/native).
  • Fold RASD_TRIGGER_LOOP (04 §8.2) into RASD_EXPR_TRIGGER_LOOP (05 §12)? This document lists only the latter.
  • xpathToRel / relToXpath as public exports of @rasd/xlsform versus internal helpers — decide before the 1.0 API freeze.

00 · Decisions & conventions · 03 · Architecture · 04 · Form schema spec · 05 · Logic & expressions · 06 · Renderer (React) · 07 · Renderer (native) · 08 · Builder · 09 · Offline storage · 10 · Sync protocol · 11 · PWA & embedding · 12 · Theming · 13 · i18n, RTL & accessibility · 14 · Media & field capture · 15 · Licensing & billing · 16 · Security & data protection · 18 · Engineering practices · 20 · Interoperability · 21 · Getting started · Research: 02 · 08 · 12