The platform API module is a dependency-free route-equivalent contract for the
normal product API and generated Token Host UI. It does not start an HTTP
server; a Worker, Node server, or generated adapter can wrap the same
createPlatformApi({ registry }) handler.
Canonical routes follow spec.md section 6.7.10:
POST /storage/upload-requests
POST /storage/uploads/:objectId/bytes
GET /storage/uploads/:objectId/status
GET /storage/uploads/status?objectId=:objectId
GET /storage/objects/:objectId
GET /storage/usage/:accountIdThe implementation also accepts issue/UI aliases:
POST /storage/upload
POST /storage/tokenhost/upload
GET /storage/tokenhost/upload/status
GET /storage/uploads/:objectId
GET /usagePOST /storage/tokenhost/upload and
GET /storage/tokenhost/upload/status are the generated Token Host upload
adapter endpoints. They speak Token Host Builder's byte-upload contract and
then bridge into the section 6.7 create/submit/read model. The canonical object
status route remains GET /storage/uploads/:objectId/status; the
GET /storage/uploads/status?objectId=:objectId alias is only an object-status
read alias.
Requests use normal platform authentication outside this repo. The current
handler requires x-platform-user-id as the authenticated platform subject and
accepts optional x-platform-wallet-address when the product has a user wallet.
The API maps that subject to an opaque bytes32 accountId; raw user ids are not
written into contract request parameters or returned as onchain identifiers.
createPlatformApi delegates contract work to a registry adapter with:
createUploadRequest({ account, auth, request }),submitUploadBytes({ objectId, account, auth, bytes }),readUploadStatus({ objectId, account, auth }),readObject({ objectId, account, auth }),readUsage({ account, auth }).
The adapter is responsible for using the section 6.7 registry, relayer, coordinator, and read-model semantics. User-facing current reads must come from contract views when available, not from coordinator-private state or Token Host-owned generated CRUD state. Reconstructed event state remains a fixture, history, and fallback path.
POST /storage/upload-requests and its POST /storage/upload alias require
an idempotency key in either the JSON body or the idempotency-key header. Non
bytes32 keys are hashed into the registry-compatible bytes32 key. Duplicate keys
for the same accountId return 409 duplicate_idempotency_key with the
existing object id instead of creating another object.
The byte endpoint is coordinator-facing in v1. Retrying bytes against a terminal upload returns a terminal-state error; clients should create a new upload request with a new idempotency key when they need a fresh attempt.
Production HTTP wrappers must add authentication, body limits, rate limits,
timeouts, and durable idempotency around this route-equivalent module. The
baseline values and validation command are documented in
docs/production-hardening-runbook.md.
Generated UI/client code should bind to:
POST /storage/tokenhost/uploadfor direct file-byte uploads from generated Token Host upload UI. Successful responses include{ ok: true, upload: { url, size, provider, runnerMode, metadata } };GET /storage/tokenhost/upload/statusfor upload adapter metadata;request.objectId,request.accountId,request.status, andrequest.requestExpiresAtfrom create responses;links.uploadBytes,links.status,links.object, andlinks.usagefor follow-up calls;upload,usage,copyReceipts,receiptPayer, and optionalprojectionfrom status/object responses.
The dev tests also bind these read endpoints to the runDevUploadSpine
contract/read-model result, preserving the mocked boundary: no real FOC bytes,
Synapse SDK call, or Calibration transaction is claimed until the hosted
coordinator and Phase 0 evidence exist.
The user API above is separate from the operator/admin surface in
src/api/platform-admin-api.mjs. Admin routes are read-only, require an
explicit admin authorization hook, and do not use user object-ownership checks:
GET /admin/storage/dashboard
GET /admin/storage/objects
GET /admin/storage/objects/:objectId
GET /admin/storage/usage
GET /admin/storage/datasets
GET /admin/storage/coordinators
GET /admin/storage/reconciliationAdmin responses are built by src/admin/reconciliation.mjs from registry
contract views or reconstructed event state. Current direct-onchain dashboard
reads enumerate rows from FocPlatformRegistry list/count views and then fetch
detail state from point reads or bounded readBatch calls:
- objects:
listStorageObjectIds/listAccountObjectIdsplusgetStorageObject; - accounts:
listAccountIdsplusgetAccountUsage; - datasets/providers:
listDatasetKeysplusgetDatasetRecord; - coordinators:
listCoordinatorAddressespluscoordinatorPolicies; - relayers:
listRelayerAddressesplusisRelayer; - receipts:
getCopyReceiptsandreceiptPayer.
Event reconstruction may supplement audit/history screens and local fixtures,
but it must not replace direct contract reads for current admin rows. Optional
FOC evidence can be supplied by a wrapper; without it, stored-object
reconciliation reports
foc_evidence_not_checked rather than claiming live FOC verification. See
docs/admin-reconciliation.md.