npm.io
3.8.0 • Published 2d ago

@crvouga/mockingbird-service-prism

Licence
MIT
Version
3.8.0
Deps
0
Vulns
0
Weekly
0
Stars
3

@crvouga/mockingbird-service-prism

Local emulators. Real API contracts. Part of Mockingbird.

Stateful emulator of the Prism Labs body-scan API for test suites: subject upsert, scan creation, the presigned capture upload the capture page PUTs its video to, per-stage processing states, and the READY-scan results our backend persists (body composition, measurements, health report with metabolic age, asset URLs). Scans move through the real lifecycle (CREATED → PROCESSING → READY / FAILED) on command or on the emulator clock, and results are computed deterministically from the subject's height, weight, sex and age.

  • Operation coverage: SUPPORT.md
  • The contract (openapi.yaml) is hand-authored from the consumer's zod schemas (prism-scan.adapter.ts) and the capture page's upload (body-scan-capture-page).

Install

npm install -D @crvouga/mockingbird-service-prism

ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with npx mockingbird-prism serve, createServer from ./server (Node), or createRuntime with any Fetch server.

Usage

Point PRISM_API_URL at the emulator (http is allowed) and set PRISM_API_KEY to anything (or pin it with --api-key). Without both, our adapter reports unavailable and never calls out.

npx mockingbird-prism serve --port 8825 --auto-advance 2000
import { createServer } from "@crvouga/mockingbird-service-prism/server"

// Test setup: a local Prism.
const server = await createServer()
const baseUrl = server.url // the app's PRISM_API_URL

// Application code: the requests it sends to Prism.
const prism = async (path: string, body?: unknown) => {
  const response = await fetch(`${baseUrl}${path}`, {
    method: body === undefined ? "GET" : "POST",
    headers: { authorization: "Bearer k", accept: "application/json;v=1", "content-type": "application/json" },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  })
  return (await response.json()) as Record<string, any>
}
await prism("/users", {
  token: "member-1", sex: "female", region: "north_america", birthDate: "1990-04-02",
  weight: { value: 64, unit: "kg" }, height: { value: 1.7, unit: "m" },
  researchConsent: false, termsOfService: { accepted: true, version: "1" },
})
const scan = await prism("/scans", { userToken: "member-1", deviceConfigName: "IPHONE_SCANNER", bodyfatMethod: "coco2" })
const upload = await prism(`/scans/${scan.id}/upload-url`, {})
await fetch(upload.url, { method: "PUT", body: "capture video bytes" }) // the capture page's upload

// Test control: finish processing without waiting.
await fetch(`${baseUrl}/__admin/scans/${scan.id}/advance`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ to: "READY" }),
})

// Application code: the READY scan's results.
const bodyfat = await prism(`/scans/${scan.id}/bodyfat`)
// → {bodyfatMethod: "coco2", bodyfatPercentage: 29.6, leanMass: 45.07, fatMass: 18.93, …}

await server.close()
Routes

All with Authorization: Bearer <key> and Accept: application/json;v=1, except the presigned upload and asset URLs.

Route Behaviour
POST /users Upsert by token (sex, region, birthDate, weight {value, unit kg|lb}, height {value, unit m|in}, researchConsent, termsOfService) → 201 on create, 200 on update, same id.
POST /scans {userToken, deviceConfigName: IPHONE_SCANNER|ANDROID_SCANNER, bodyfatMethod, assetConfigId?} → 201 scan {id, status: "CREATED", weight, height, …}; unknown user 404.
GET /scans/{id}?unit-system=metric|imperial Status and the subject's weight/height in that unit system.
POST /scans/{id}/upload-url {url, expirationTime}: a presigned PUT to the emulator itself (15 minutes on the emulator clock; /__admin/ns/<name> kept in the URL). 409 once the capture is uploaded.
PUT /uploads/{id}?expires&signature The capture upload. Moves the scan to PROCESSING (captureData succeeded, body started); an empty body fails it. Expired or tampered URLs are S3-style 403 XML.
GET /scans/{id}/scan-assets {captureData, body, fittedBody, measurement} each started / succeeded / failed / null, with …UpdatedAt.
GET /scans/{id}/bodyfat READY only: {bodyfatMethod, bodyfatPercentage, leanMass, fatMass, skeletalMuscleMass}.
GET /scans/{id}/measurements?unit-system= READY only: {waistFit, hipsFit, chestFit, waistToHipRatio, bodyRoundnessIndex, bmiPredicted} (cm or in).
GET /scans/{id}/health-report READY only: {metabolicAgeReport: {metabolicAgeYears, chronologicalAgeYears, ageDeltaYears, percentile}, bodyShapeReport}.
GET /scans/{id}/asset-urls READY only: signed URLs for previewImage, model, canonicalBody, texture, material, stripes, served by GET /assets/{id}/{file}.

Results before READY are 404 (our adapter: not_found). Errors are {message, errors?}.

Admin (beyond the standard contract)
Route Effect
POST /__admin/scans/:id/advance One processing stage on ({"to": "READY"} for all of them). The scan must be uploaded.
POST /__admin/scans/:id/fail Fail the started stage; the scan becomes FAILED.
GET /__admin/scans The namespace's scans.
GET/PUT /__admin/settings {apiKeys?, uploadUrlTtlMs?, autoAdvance?: {afterMs, failAt?} | null}; auto-advance walks uploaded scans one stage per afterMs of emulator time.
POST /__admin/tick Apply due auto-advance steps now (the served emulator ticks every 100 ms).

Fault presets (POST /__admin/faults {"preset": "<name>", "count"?: n}): unauthorized, server_error, scan_not_found, schema_drift (an unknown scan status), stage_states_slow (6 s, past our 5 s timeout), metabolic_age_missing, metabolic_age_implausible, upload_forbidden, connection_drop.

Namespaces

x-mockingbird-namespace, a /__admin/ns/<name> prefix on PRISM_API_URL, or by API key: PUT /__admin/credentials {"credentials": {"<PRISM_API_KEY>": "<namespace>"}}. Presigned upload and asset URLs carry the namespace in their path, since the capture page's PUT has no other carrier.

Deliberately not modelled
  • Webhooks: our backend polls Prism, it has no receiver.
  • Real body reconstruction: results are formulas over the subject's inputs, and the 3D assets are placeholder bytes.
  • Scan deletion, user listing, and every endpoint our adapter does not call.

API

Export Kind Description
PrismAPI class The in-process emulator: fetch(request), reset(), advance(scanId, fail?), tick(), scans(). Options: sqlite, now, namespace, publicNamespace, settings.
createRuntime function The emulator with the full service contract (health, admin, namespaces, credentials, presets). Options: settings, tickMs, clock, seed, adminKey, onLog, sqlite.
PRISM_PRESETS object Every named fault preset.
PRISM_NAMESPACE string The service name, "prism".
STAGES array The processing stages, in order.
scanResults function The deterministic results a READY scan answers.
prismError function Build a Prism error response {message, errors?}.
document, operationIds, supportedOperationIds values The OpenAPI contract and its operation ids.
createServer, serveTarget, DEFAULT_PORT (./server) Node Serve over node:http (auto-advance ticks every 100 ms); the serve CLI target (--api-key, --auto-advance); port 8825.

Part of mockingbird.

Keywords