@enfinitos/sdk-operator-web
EnfinitOS Operator Web SDK — a React component library that tenants embed in their own admin UIs to surface the EnfinitOS operator surfaces (rights registry, proof centre, compliance flows, billing, pacing, pilots) without building those dashboards from scratch.
Who should use this. Any tenant who: - already has an admin UI (Cloudflare-style, AWS-Console-style, or a custom React-based shell) and wants to drop EnfinitOS panels inside their chrome rather than iframe a third-party dashboard; - has their own identity/SSO and wants to issue short-lived JWTs to the SDK without going through the platform's login flow; - prefers a small, themeable, white-label-ready component library over a hosted Workspace experience.
Architecture
┌──────────────────────────────────────────────────┐
│ Tenant Admin UI │
│ (React/Next.js/Remix/Vite — host's choice) │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ <OperatorProvider> │ │
│ │ client = new EnfinitOSOperatorClient(…) │ │
│ │ │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ ThemeProvider (CSS variables, │ │ │
│ │ │ tenant-overridable tokens) │ │ │
│ │ │ │ │ │
│ │ │ <RightsRegistryPanel/> │ │ │
│ │ │ <ProofExplorer/> │ │ │
│ │ │ <ProofVerifier │ │ │
│ │ │ auditorVerify={…}/> │ │ │
│ │ │ <PacingDashboard/> │ │ │
│ │ │ <UsageMeter/> │ │ │
│ │ │ <DsarRequestForm/> │ │ │
│ │ │ <PilotEnrolmentFlow/> │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │ │
└───────────────────────┼──────────────────────────┘
│ HTTPS, JWT auth
│ X-Org-Id, Accept-Version
▼
┌──────────────────────────────────────────────────┐
│ EnfinitOS Platform API (/v1) │
│ │
│ LIVE today (Cloudflare sandbox + Apr-2027 prod) │
│ /v1/rights (+ /issue) /v1/proof-packs (+seal) │
│ /v1/offers (+ /propose) /v1/challenges (+open) │
│ /v1/compliance/dsar (DSAR job pipeline) │
│ │
│ LAUNCH surface (April 2027, gated by default) │
│ /v1/proof (ledger) /v1/compliance/erasure │
│ /v1/consent /v1/billing/{invoices…} │
│ /v1/pacing/health /v1/pilot/enrolment │
└──────────────────────────────────────────────────┘
The SDK has no compile-time dependency on the platform's backend or any other EnfinitOS package. It speaks the public REST contract via Accept-Version negotiation and re-states the contract types in src/types.ts so tenants can install the SDK without dragging in the platform's internal schemas. The public /v1 contract is defined once, in docs/openapi-v1.yaml; a conformance test in this package (src/__tests__/contract-conformance.test.ts) pins every URL the SDK can construct to that spec.
Live sandbox surface vs launch surface
The same client serves two tiers of the /v1 contract:
- Live surface — exists on the public contract today (
sandbox.api.enfinitos.com/v1/*, identicallyapi.enfinitos.com/v1/*at the April 2027 launch). Works with zero configuration. - Launch surface — operator resources that ship with the April 2027 AWS backend and have no live
/v1equivalent. Calling one with the default configuration throws a descriptiveOperatorLaunchSurfaceErrorinstead of firing a request that can only 404. Opt in withlaunchSurface: true(mock backends, staging, tests), or override that resource's path template via thepathsoption (e.g.BACKEND_PATHS_DIRECTfor a local Fastify).
| Surface | Sub-API methods | Live /v1 route |
|---|---|---|
| Live | rights.list / get / issue / suspend / resume / revoke / listBases | GET /v1/rights, GET /v1/rights/{rightId}, POST /v1/rights/issue, POST /v1/rights/{rightId}/suspend|resume|revoke, GET /v1/rights/bases |
| Live | offers.listInbox / propose / accept / reject / counter / withdraw | GET /v1/offers, POST /v1/offers/propose, POST /v1/offers/{offerId}/accept|reject|counter|withdraw |
| Live | challenges.listOpen / open / resolve / withdraw | GET /v1/challenges, POST /v1/challenges/open, POST /v1/challenges/{challengeId}/resolve|withdraw |
| Live | proof.listPacks / getPack / seal | GET /v1/proof-packs, GET /v1/proof-packs/{packId}, POST /v1/proof-packs/seal |
| Live | tenant.get, events.list, metering.summary, settlement.summary | GET /v1/tenant, GET /v1/events, GET /v1/metering, GET /v1/settlement |
| Live | compliance.createDsarJob / listDsarJobs / getDsarJob / runDsarJob / downloadDsarJob / cancelDsarJob (DSAR / tenant-export job pipeline) | POST /v1/compliance/dsar, GET /v1/compliance/dsar, GET /v1/compliance/dsar/{jobId}, POST /v1/compliance/dsar/{jobId}/run, GET /v1/compliance/dsar/{jobId}/download, POST /v1/compliance/dsar/{jobId}/cancel |
| Launch (Apr 2027) | rights.provenance / statusCounts / family | — (live: provenance comes back inline on GET /v1/rights/{rightId} — use rights.getWithProvenance()) |
| Launch (Apr 2027) | offers.get, challenges.get | — |
| Launch (Apr 2027) | proof.list / get / chain (provenance ledger), proof.verify, proof.exportSigned | — (live: verify CLIENT-side with @enfinitos/sdk-auditor over proof.getPack() output) |
| Launch (Apr 2027) | consent.* | — |
| Launch (Apr 2027) | compliance.listErasure / getErasure / fileErasure (per-subject erasure) | — (live ships tenant-level GET /v1/compliance/export + POST /v1/compliance/erase) |
| Launch (Apr 2027) | billing.* (invoices / settlements / meters) | — (live metering/settlement projections are metering.summary / settlement.summary above) |
| Launch (Apr 2027) | pacing.*, pilot.*, crm.*, campaigns.*, assets.*, analytics.* | — |
Two collapses worth knowing about on the live sandbox:
- Resolve →
POST /v1/delivery. There is no standalone runtime resolve/world-model endpoint on/v1: the Cloudflare sandbox collapses the resolve step into the delivery observation itself —POST /v1/deliveryruns the substrate constraint gate against the right and emits the signed receipt in one call (constraint violations come back412). Pacing telemetry rolls up throughGET /v1/metering. - Verification → client-side. There is no server-side verify endpoint. Fetch a pack with
proof.getPack()and verify it offline with@enfinitos/sdk-auditor— that independence is the point of the trust model.
The machine-readable inventory behind this table is exported as OPERATOR_WEB_PATH_MANIFEST (plus LIVE_V1_PATH_KEYS / LAUNCH_SURFACE_PATH_KEYS), and the conformance test cross-checks it against docs/openapi-v1.yaml on every run.
Installation
pnpm add @enfinitos/sdk-operator-web
# or: npm install @enfinitos/sdk-operator-web
Peer dependencies (provided by the host):
react^18.0.0 || ^19.0.0react-dom^18.0.0 || ^19.0.0
The SDK pulls in zero runtime UI libraries — no MUI, no Chakra, no Ant Design, no Tailwind. Components ship with inline styles driven by CSS variables; tenants override those variables to brand the SDK to their own chrome.
5-minute getting started
import {
EnfinitOSOperatorClient,
OperatorProvider,
RightsRegistryPanel,
} from "@enfinitos/sdk-operator-web";
const client = new EnfinitOSOperatorClient({
// Today: the live sandbox. April 2027: https://api.enfinitos.com —
// the contract is identical, only the base URL changes.
apiBaseUrl: "https://sandbox.api.enfinitos.com",
orgId: "org_acme",
authToken: () => fetchYourJwt(), // sync or async
// optional:
timeoutMs: 10_000,
retries: 3,
// launchSurface: true, // opt in to April-2027 resources — see
// // "Live sandbox surface vs launch surface"
});
export function AdminRightsPage() {
return (
<OperatorProvider client={client}>
<RightsRegistryPanel />
</OperatorProvider>
);
}
That's it. The panel paginates, filters by status / substrate, opens a detail card on click, and exposes lifecycle actions (suspend / resume / revoke) inline.
Issue a right, observe deliveries, seal + verify a proof pack
The core live loop against the public contract (docs/openapi-v1.yaml):
import { verifyAll } from "@enfinitos/sdk-auditor";
// 1. Issue a root right — POST /v1/rights/issue. `basisId` travels
// in the BODY (not the path); `scope` is the free-form label and
// effectiveFrom/effectiveUntil bound the validity window.
const right = await client.rights.issue({
basisId: "bas_8f2c1a9b4e7d0c63",
substrate: "DOOH",
scope: "DOOH retail estate, Greater London",
effectiveFrom: new Date().toISOString(),
});
// 2. (Deliveries are observed by your runtime via POST /v1/delivery —
// the constraint gate runs in that call; see the API reference.)
// 3. Seal everything observed since the last seal — POST
// /v1/proof-packs/seal. `sealed: false` means nothing new.
const { pack, sealed } = await client.proof.seal();
// 4. List / fetch signed proof packs — GET /v1/proof-packs[/{packId}].
const packs = await client.proof.listPacks({ limit: 20 });
const fetched = await client.proof.getPack(packs.items[0]!.packId);
// 5. Verify CLIENT-side with the open-source auditor SDK. There is
// deliberately no server-side verify endpoint — a verification you
// have to ask the platform for proves nothing.
const report = await verifyAll({ pack: fetched });
// report.status === "VALID"
Theming
The SDK reads a flat dictionary of CSS variables prefixed with --enfinitos-. Override any subset via the tokens prop on <OperatorProvider> or <ThemeProvider>:
<OperatorProvider
client={client}
tokens={{
"--enfinitos-color-brand": "#0066cc",
"--enfinitos-color-brand-fg": "#ffffff",
"--enfinitos-color-text": "#1a1a1a",
"--enfinitos-color-surface": "#ffffff",
"--enfinitos-color-surface-alt": "#f4f6f8",
"--enfinitos-color-border": "#e2e8f0",
"--enfinitos-font-family": "Inter, sans-serif",
}}
>
…
</OperatorProvider>
CSS-variable reference
| Token | Default | Purpose |
|---|---|---|
--enfinitos-color-brand | #2563eb | Primary brand colour — buttons, links, focus accents. |
--enfinitos-color-brand-fg | #ffffff | Text on top of brand. |
--enfinitos-color-accent | #7c3aed | Secondary highlight (rare). |
--enfinitos-color-success | #16a34a | StatusBadge, success rows. |
--enfinitos-color-warning | #d97706 | Warning state. |
--enfinitos-color-danger | #dc2626 | Errors, destructive actions. |
--enfinitos-color-info | #0284c7 | Info chips. |
--enfinitos-color-neutral | #64748b | Neutral chips. |
--enfinitos-color-bg | #0f172a | Page background (if SDK is the root). |
--enfinitos-color-surface | dark glass | Card surface. |
--enfinitos-color-surface-alt | dark glass | Alt rows / hover. |
--enfinitos-color-border | dim slate | Borders. |
--enfinitos-color-text | #e2e8f0 | Primary text. |
--enfinitos-color-text-muted | #94a3b8 | Secondary text. |
--enfinitos-color-text-inverse | #0f172a | Text on inverse surfaces. |
--enfinitos-font-family | system stack | Body font. |
--enfinitos-font-family-mono | mono stack | IDs / hashes. |
--enfinitos-font-size-{xs,sm,md,lg,xl} | 11–20px | Typography scale. |
--enfinitos-font-weight-{regular,medium,bold} | 400/500/600 | Font weight. |
--enfinitos-space-{0..6} | 0–32px | Spacing scale. |
--enfinitos-radius-{sm,md,lg} | 6/10/14 | Radius scale. |
--enfinitos-shadow-{sm,md,lg} | layered | Drop-shadows. |
--enfinitos-focus-ring | brand glow | Focus outline. |
--enfinitos-motion-{fast,medium} | 120/240ms | Animation timing. |
Tenants who already use design tokens (Tailwind, Radix Colors, a custom system) typically write a one-liner mapping their token names onto --enfinitos-* and inherit their brand automatically.
Component catalogue
Rights Registry
| Component | What it renders |
|---|---|
<RightsRegistryPanel/> | Top-of-page panel: status-counts header, filter strip (status + substrate + search), paginated list (<DataTable/>), per-row click handler. Suspends to <RightDetailCard/> on selection. |
<RightDetailCard/> | Full right detail: status badge, scope label, basis link, full rules dump, provenance chain, action buttons (suspend / resume / revoke). |
<ComposeRuleForm/> | Rule-composition form covering every BehaviourRule kind (max-speed, exclusion zones, age-gate, consent-required, territory restrict, custom). Used to issue rights or counter-offer. |
<ChallengeInbox/> | Open challenges list, per-row resolve / withdraw, rationale textarea. |
Proof Centre
| Component | What it renders |
|---|---|
<ProofExplorer/> | Live (default, surface="live"): signed proof-pack list (packId, label, sealed-at, receipt count, metering/settlement badges) + a pack-detail pane (receipts table with the beforeHash/afterHash chain-continuity hint, metering totals, settlement totals incl. the 8-role split) + a "Seal new pack" action + a client-side verification panel (no server verify; pass verifyPack to wire @enfinitos/sdk-auditor, else shows a copy-paste offline-verify snippet). surface="ledger": the launch-surface provenance-ledger explorer (filter by type + rightId, per-row payload viewer, parent-chain breadcrumbs) — needs a launchSurface: true client. |
<ProofExportButton/> | One-click signed-export trigger; opens the resulting archive URL when ready. |
Verification Centre
| Component | What it renders |
|---|---|
<ProofVerifier/> | Chain-verification panel. On the live sandbox, verification is CLIENT-side — pass an auditorVerify callable (typically @enfinitos/sdk-auditor). The server-side proof.verify() it also consults is launch surface (April 2027) and requires launchSurface: true until then. |
Compliance
| Component | What it renders |
|---|---|
<DsarRequestForm/> | GDPR Art. 15 (Data Subject Access Request) form: subject ref + note → creates a DATA_SUBJECT-scoped DSAR job (compliance.createDsarJob, live /v1/compliance/dsar); confirms with the job id + REQUESTED state. |
<ErasureRequestForm/> | GDPR Art. 17 (erasure) form. Surfaces BLOCKED reason when legal basis conflicts. |
<ConsentRecordsList/> | Paginated subject-consent list, revocation action inline. |
Pacing
| Component | What it renders |
|---|---|
<PacingDashboard/> | Grid of <DeliveryHealthCard/> per scope, auto-refreshes on a tenant-configurable interval. |
<DeliveryHealthCard/> | Single scope: pace index, render success rate, p95 resolve latency, pending impressions. Colour-coded by paceIndex. |
Billing
| Component | What it renders |
|---|---|
<UsageMeter/> | Bar chart for usage meters with used / included / cap. Shows percentage and remaining budget. |
<InvoiceList/> | Paginated invoice table, status badge, due-by, line items expandable. |
<SettlementBreakdown/> | Per-settlement split table: party, percentage, amount. |
Pilot
| Component | What it renders |
|---|---|
<PilotEnrolmentFlow/> | Multi-step enrolment form for first-time tenants: cohort selection, contract upload, primary-contact details. |
<PilotCohortBadge/> | Single pill conveying the current pilot cohort + tier. |
Shared building blocks (composable on their own)
| Component | What it renders |
|---|---|
<Card/> | The SDK's analogue of cp-glass-card — section label, title, subtitle, toolbar slot, body. |
<DataTable/> | Typed, accessible table: column renderers, sort indicators, keyboard navigation, empty state. |
<StatusBadge/> | Status pill with colour + dot. Includes a statusKindFor(status) helper to map domain enums. |
<SignedEvidenceCard/> | Surfaces a signed export: digest, signer, signature preview, download link. |
<AsyncBoundary/> | Loading / error / data renderer for the SDK's async hooks. |
<EmptyState/> | Title + body + CTA for zero-row lists. |
API client reference
Top-level construction:
const client = new EnfinitOSOperatorClient({
apiBaseUrl: "https://sandbox.api.enfinitos.com",
orgId: "org_acme",
authToken: "ey…", // or () => Promise<string>
timeoutMs: 10_000,
retries: 3,
fetchImpl: customFetch, // optional override
userAgentTag: "acme-admin/1.2.3", // optional, appended to X-Operator-Web-SDK
launchSurface: false, // default; true unlocks April-2027 resources
// paths: BACKEND_PATHS_DIRECT, // optional — un-prefixed Fastify dev layout
});
Sub-API surface (entries marked ⏳ are launch surface — gated by default, see the table above):
client.rights → list / get / getWithProvenance / issue / suspend /
resume / revoke / listBases /
⏳statusCounts / ⏳family / ⏳provenance
client.offers → listInbox / propose / accept / reject /
counter / withdraw / ⏳get
client.challenges → listOpen / open / resolve / withdraw / ⏳get
client.proof → listPacks / getPack / seal /
⏳list / ⏳get / ⏳chain / ⏳verify / ⏳exportSigned
client.tenant → get
client.events → list
client.metering → summary
client.settlement → summary
client.consent → ⏳list / ⏳get / ⏳issue / ⏳revoke / ⏳check
client.compliance → createDsarJob / listDsarJobs / listDsarJobsDetailed /
getDsarJob / getDsarJobWithReport / runDsarJob /
downloadDsarJob / cancelDsarJob (DSAR jobs — LIVE)
⏳listErasure / ⏳getErasure / ⏳fileErasure (erasure)
client.billing → ⏳listInvoices / ⏳getInvoice / ⏳listSettlements /
⏳getSettlement / ⏳listMeters
client.pacing → ⏳listHealth / ⏳getHealth
client.pilot → ⏳getEnrolment / ⏳enrol / ⏳listCohorts
rights.get returns the flat Right; rights.getWithProvenance returns { right, provenance } (the live read ships the provenance chain inline). offers.counter returns { original, counter } (the spec's two-sided counter response). tenant / events / metering / settlement are LIVE reads — no launchSurface opt-in needed.
Every method returns the unwrapped data from the platform's envelope; non-2xx responses throw an OperatorApiError carrying the platform's RFC-7807-ish code/message/details triple. A launch-surface method called without opting in rejects with an OperatorLaunchSurfaceError before any request leaves the client — the message names the resource, the live alternative where one exists, and the two opt-in routes (launchSurface: true or a paths override).
Retry policy
- 5xx and 429 responses retry up to
retriestimes (default 3). - 4xx responses (other than 429) are fatal — they throw immediately.
- Backoff is exponential with a small jitter to spread retry storms.
- A network/timeout error is treated as retryable.
Auth
authToken accepts either a static string or a callable returning a string | Promise<string>. The callable form is invoked on every request so a tenant can rotate tokens without reconstructing the client (and it slots cleanly into OAuth-refresh flows).
Cancellation
Every request races an AbortSignal against the configured timeoutMs. The hook layer (useAsync) also wires an abort signal so deps-changes cancel in-flight calls.
Hooks
For tenants that want the SDK's React-flavoured data layer rather than rolling their own:
useRightsList(filter) // → AsyncResult<Page<Right>>
useRight(rightId) // → AsyncResult<{ right, provenance } | null>
useRightFamily(rightId) // → AsyncResult<{ ancestors, descendants } | null>
useRightsStatusCounts() // → AsyncResult<Record<RightStatus, number>>
useProofList(opts) // → AsyncResult<Page<ProofRecord>>
useProofChain(headProofId) // → AsyncResult<ProofChain | null>
useConsentList(subjectRef) // → AsyncResult<Page<ConsentRecord>>
useConsentCheck(input) // → AsyncResult<{ granted, consentId }>
useInvoices(opts) // → AsyncResult<Page<Invoice>>
useSettlements(opts) // → AsyncResult<Page<Settlement>>
useUsageMeters() // → AsyncResult<Page<UsageMeter>>
usePacingHealth(opts) // → AsyncResult<DeliveryHealth[]>
Every hook returns { data, error, loading, refreshing, refetch }, shaped to plug into <AsyncBoundary state={…}> without ceremony.
Tenants who want raw control reach for useOperatorClient() and hit the typed client directly.
Sample integration — minimal viable operator dashboard
A complete, copy-pasteable 30-line dashboard:
import {
EnfinitOSOperatorClient,
OperatorProvider,
RightsRegistryPanel,
ProofExplorer,
PacingDashboard,
UsageMeter,
InvoiceList,
DsarRequestForm,
} from "@enfinitos/sdk-operator-web";
const client = new EnfinitOSOperatorClient({
apiBaseUrl: process.env.NEXT_PUBLIC_ENFINITOS_API!,
orgId: process.env.NEXT_PUBLIC_ENFINITOS_ORG!,
authToken: async () => (await fetch("/api/jwt")).text(),
});
export default function OperatorDashboard() {
return (
<OperatorProvider client={client} tokens={{ "--enfinitos-color-brand": "#0066cc" }}>
<div style={{ display: "grid", gap: 24, padding: 24 }}>
<RightsRegistryPanel />
<PacingDashboard refreshIntervalMs={30_000} />
<UsageMeter />
<InvoiceList />
<ProofExplorer />
<DsarRequestForm />
</div>
</OperatorProvider>
);
}
Verification gate
Verification of the live proof artefact (the SignedProofPack from /v1/proof-packs) is CLIENT-side, on purpose: fetch the pack with client.proof.getPack(packId) and pass it to @enfinitos/sdk-auditor's verifyProofPack / verifyAll. A verification you have to ask the platform for proves nothing — the auditor SDK re-derives signatures, hashes, chain links, metering and settlement from first principles, offline.
Components that render signed evidence accept an optional auditorVerify prop typed against @enfinitos/sdk-auditor. The SDK does NOT pull the auditor as a hard dep — tenants on edge-runtime hosts (Cloudflare Workers, Vercel Edge) opt-in only when they have the crypto primitives available.
import { verifyChain } from "@enfinitos/sdk-auditor";
<ProofVerifier headProofId="p_abc123" auditorVerify={verifyChain} />
(<ProofVerifier/>'s server-side second opinion calls proof.verify() — a launch-surface endpoint; see the surface table.)
What's SDK-side vs platform-side
| Concern | This SDK | Platform |
|---|---|---|
| REST contract types | mirrored verbatim in src/types.ts (the SignedProofPack family mirrors @enfinitos/sdk-auditor field-for-field) | source-of-truth in docs/openapi-v1.yaml |
| Auth — JWT issuance / SSO | NOT part of SDK | reuses the platform's /auth/* plane |
| Theming / branding | tenant-overridable CSS variables | n/a (platform's Workspace is a separate skin) |
| Pacing data | renders via client.pacing.listHealth() (launch surface) | GET /v1/pacing/health at launch; live sandbox rolls delivery telemetry up through GET /v1/metering |
| Audit log persistence | the platform writes; the SDK reads | platform-owned |
| Proof verification | CLIENT-side: proof.getPack() output → @enfinitos/sdk-auditor (also the auditorVerify prop on <ProofVerifier/>) | no server-side verify on the live /v1; the launch backend adds /v1/proof/{id}/verify as a convenience second opinion |
Accessibility
- Every interactive element has an explicit
aria-labelor visible label. - All clickable rows are keyboard-activatable (
Enter/Space) withtabIndex=0androle="button". - Status badges use
role="status"with a sensiblearia-label. - Focus rings honour
--enfinitos-focus-ringso tenants can keep brand contrast. - Async boundaries announce loading + error states via
role="status"androle="alert"so screen readers track state changes.
Versioning
- The SDK ships
SDK_VERSION(semver) andCONTRACT_VERSION(an ISO date matching the platform's accepted contract). Every REST call sends both as headers (X-Operator-Web-SDK,Accept-Version). - The platform pins responses to the negotiated contract version — a newer platform never returns shapes the SDK can't parse.
See also
- the
auditor-tsSDK (@enfinitos/sdk-auditor) — independent signature verifier you can wire into<ProofVerifier auditorVerify={…}/>. - the
robotics-tsSDK — the reference SDK for the ROBOTICS substrate (this Operator SDK speaks the sameBehaviourRuleunion). - the platform's first-party operator Workspace — a dashboard rendered against the same API surface; useful as a worked example.
docs/launch/substrate-readiness-matrix.md— per-substrate readiness status.