Prism Labs

@crvouga/mockingbird-service-prism

Stateful emulator of the Prism Labs body-scan API: subjects, scans, presigned capture upload, processing stages to READY, and deterministic body-composition, measurement, health-report and asset results.

Parity: Body scans and measurementsHealth & labs11 of 11 operationsIn-process fetchNode servermockingbird-prism
$
npm install -D @crvouga/mockingbird-service-prism

Playground

Requests go to the real emulator running in this tab. State persists across requests, and the journal shows what it received. A green dot marks operations whose sample succeeds as-is; others need ids from earlier responses.

Loads the real @crvouga/mockingbird-service-prism into this tab on first send
https://sandbox-api.hosted.prismlabs.tech

Send a request to see the emulator's response.

Operations

11 of 11 operations in the vendored contract are emulated. Wire shapes from apps/backend/src/modules/body-scan/adapters/outbound/prism-scan.adapter.ts and packages/body-scan-capture-page/src/main.ts (the PUT upload).

MethodPathOperationStatus
POST/usersUpsertUserCreate or update the scan subject identified by `token`.
POST/scansCreateScanStart a scan for a subject.
GET/scans/{scanId}GetScanA scan's status and weight.
POST/scans/{scanId}/upload-urlCreateUploadUrlA presigned PUT URL for the capture video (15 minutes).
GET/scans/{scanId}/scan-assetsGetScanAssetsPer-stage processing states.
GET/scans/{scanId}/bodyfatGetBodyfatBody composition (READY scans).
GET/scans/{scanId}/measurementsGetMeasurementsCircumferences and indices (READY scans).
GET/scans/{scanId}/health-reportGetHealthReportHealth report incl. metabolic age (READY scans).
GET/scans/{scanId}/asset-urlsGetAssetUrlsSigned URLs for the scan's 3D assets (READY scans).
PUT/uploads/{scanId}UploadCaptureThe presigned upload target (what the capture page PUTs the video to). Moves the scan to PROCESSING.
GET/assets/{scanId}/{file}GetAssetA signed 3D asset (tiny placeholder bytes).

Documentation

The package README, the same file that ships in the npm tarball.Edit on GitHub

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

Shell
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.

Shell
npx mockingbird-prism serve --port 8825 --auto-advance 2000
TypeScript
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.

↑↓ to navigate↵ to open