Oloka-Video-Dev / docs /architecture /API_CONTRACT_V1.md
phucsd's picture
Deploy GitHub commit 7c8ae5c8c2f82880aab7364acaf21b26c8db01fc
a0319ec verified
|
Raw History Blame Contribute Delete
30.6 kB
# API Contract V1
Status: capability map and transport contract. Slice 3C, Slice 3D, and the schema-v6 Phase 3E core are implemented, merged, and production-cutover complete; the Phase 3E.3 transactional-outbox runtime correction is not deployed until its later cutover verification. Phase 3F has not started.
## Protocol conventions
- Product API paths are `/api/v1/**`. Foundation probes remain `/api/health`, `/api/ready`, and `/api/version`.
- JSON contracts are strict Zod schemas exported once from `packages/contracts`; unknown keys fail with the canonical error envelope from `ERROR_MODEL.md`.
- UUID resource IDs are opaque. Unauthorized private resources are indistinguishable from missing resources (`404 RESOURCE_NOT_FOUND`).
- All unsafe cookie-authenticated requests apply `AUTH_SESSION_CSRF.md`.
- First-admin bootstrap is part of the verified Google callback transaction and
has no separate HTTP route. Admin role/status mutation enforces the
last-active-admin guard; emergency recovery is operator maintenance only.
- Important mutations require exactly one `Idempotency-Key` containing 1-128
ASCII characters from `A-Z a-z 0-9 . _ : -`. Missing, empty, multiple,
Unicode, whitespace, control, slash, backslash, quote, or oversized values
are `400 VALIDATION_ERROR`. The server hashes the key and canonical semantic
request; same key/same request replays the stored result and same key/different
request is `409 IDEMPOTENCY_CONFLICT`.
- Mutable resources expose an integer `version` and ETag. Updates require `If-Match` or `expectedVersion`; stale writes return `409 VERSION_CONFLICT` with current safe version, never silently overwrite.
- Lists use an opaque base64url cursor containing versioned canonical sort position, filter fingerprint, and ID tie-breaker. Default/max page sizes are 25/100. Sorting is deterministic; cursor/filter mismatch is `400 INVALID_CURSOR`.
- Times are ISO 8601 UTC strings at the HTTP boundary and epoch milliseconds in SQLite.
- IDs, not storage keys/paths/provider secrets, cross the API boundary.
### Slice 3C Project projection
Owner Project responses expose only `id`, `name`, nullable `description`,
`favorite`, `status`, ISO-8601 UTC `createdAt`/`updatedAt`, and positive
`version`. They never expose owner columns, retention/purge internals, storage
keys, or filesystem paths. Owner routes return `404 RESOURCE_NOT_FOUND` for a
different owner, including when the actor is an admin.
Slice 3C lists only `active` Projects at API-011 and only `soft_deleted`
Projects at API-062. Both use the authenticated opaque cursor contract and the
owner list order `favorite DESC, updated_at DESC, id DESC`. Slice 3C does not
implement purge scheduling/worker side effects despite the later-state schema
foundation.
### Slice 3D Asset projection
Owner Asset responses expose only the resource/project IDs, normalized original
filename, kind, declared/verified MIME, immutable byte size/checksum, bounded
technical metadata, ingestion/lifecycle status, safe failure code, timestamps,
and optimistic version. They never expose owner columns, storage/staging keys,
filesystem paths, reconciliation state, or capability hashes. Upload responses
expose the UploadSession ID, Asset ID, canonical received offset, declared
length, lifecycle status, expiry, and recommended chunk size only.
## Common errors
The stable code, status, retryability, message key, action, severity, and allowed
surface come only from `ERROR_MODEL.md`. Common mappings are
`400 VALIDATION_ERROR`/`INVALID_CURSOR`, `401 AUTHENTICATION_REQUIRED`,
`403 AUTHORIZATION_DENIED` or an account-status code,
`404 RESOURCE_NOT_FOUND`, `409 RESOURCE_STATE_CONFLICT`/`VERSION_CONFLICT`/
`IDEMPOTENCY_CONFLICT`, `413 PAYLOAD_TOO_LARGE`,
`415 UNSUPPORTED_MEDIA_TYPE`, `416 RANGE_NOT_SATISFIABLE`, typed 422 domain
codes, `429 QUOTA_EXCEEDED`/`RATE_LIMITED`/`PROVIDER_RATE_LIMITED`, typed
502/503 provider or storage codes, and `500 INTERNAL_ERROR`. Every response
contains safe `code`, localized `messageKey`, `suggestedAction`, `retryable`,
`requestId`, and optional allowlisted field details; never a stack or provider body.
## Capability map
Legend: `U` authenticated user/owner, `A` administrator, `P` public/operator probe, `O` OIDC provider callback. `Idem` means `Idempotency-Key` required. `Ver` means optimistic version required. “Audit” names the success/denial class; security denials may be sampled/rate-limited.
| ID | Method and path | Actor; request -> response | Errors | Idempotency/concurrency | Audit; transaction/side effect |
| ------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------ | ------------------------------------------------------------ |
| API-001 | `GET /api/health` | P; none -> liveness/build | 500 | none/read-only | metric only; no tx |
| API-002 | `GET /api/ready` | P/operator; none -> DB/storage/migration readiness without secrets | 503 | none/read-only | operational log; no write tx |
| API-003 | `GET /api/version` | P; none -> build SHA/schema compatibility | 500 | cache-safe/read-only | metric only; no tx |
| API-004 | `GET /api/v1/auth/google/start` | browser; safe same-origin return path -> Google redirect | 400,429,503 | one-use OAuth transaction | login-start; short security-state write |
| API-005 | `GET /api/v1/auth/google/callback` | O/browser; code/state -> session cookie + validated return path; every failure -> fixed `303 /auth/error` without query disclosure | fixed safe redirect | state/nonce/PKCE one-use | login success/failure; provider denial consumes state; TX-01 |
| API-006 | `GET /api/v1/auth/session` | U/pending member; none -> safe user status/session expiry | 401 | read-only | none; session last-seen coalesced |
| API-007 | `GET /api/v1/auth/csrf` | U/pending member; none -> synchronizer token | 401 | no-store; per session | none; may rotate token in a guarded session write |
| API-008 | `POST /api/v1/auth/logout` | U/pending member; CSRF -> 204 | 401,403 | repeat succeeds | logout; TX-03 |
| API-009 | `GET /api/v1/auth/sessions` | U; cursor -> own safe session list | 401,400 | cursor/read-only | none |
| API-010 | `POST /api/v1/auth/sessions/revoke-all` | U; CSRF/confirm -> revoked count + current-session policy result | 401,403 | Idem | session revoke-all; TX-03 |
| API-011 | `GET /api/v1/projects` | U; cursor/status -> project summaries | 400,401 | cursor/read-only | none |
| API-012 | `POST /api/v1/projects` | U; name/description -> project | 400,409 | Idem | project.create; TX-04 |
| API-013 | `GET /api/v1/projects/:projectId` | U owner; ID -> project snapshot | 401,404 | ETag/read-only | none |
| API-014 | `PATCH /api/v1/projects/:projectId` | U owner; rename/favorite + expected version -> updated project | 400,401,404,409 | Idem + Ver | project.update; TX-05 |
| API-015 | `DELETE /api/v1/projects/:projectId` | U owner; expected version/confirm -> soft-deleted project | 401,404,409 | Idem + Ver | project.delete_requested; TX-06, later queues purge |
| API-016 | `POST /api/v1/projects/:projectId/restore` | U owner; expected version -> active project when policy permits | 401,404,409 | Idem + Ver | project.restore; TX-06 |
| API-017 | `POST /api/v1/projects/:projectId/uploads` | U owner; file metadata/size/checksum -> Asset + upload session | 400,404,409,413,415 | Idem; project Ver guard | upload.create; TX-07 |
| API-018 | `HEAD /api/v1/uploads/:uploadId` | U owner; ID -> offset/length/status headers | 401,404,409 | read-only authoritative offset | none |
| API-019 | `PATCH /api/v1/uploads/:uploadId` | U owner; offset + binary chunk -> new offset | 400,404,409,413,415 | offset/chunk checksum retry; Ver | upload.append metric; file then TX-08 |
| API-020 | `POST /api/v1/uploads/:uploadId/complete` | U owner; optional checksum -> asset/job | 404,409,415,422 | Idem + upload Ver | upload.complete; TX-09 |
| API-021 | `DELETE /api/v1/uploads/:uploadId` | U owner; ID -> 204 | 404 | repeat succeeds | upload.abort; TX-10 |
| API-022 | `GET /api/v1/projects/:projectId/assets` | U owner; cursor/media type/upload time/ingestion/lifecycle/original-filename search -> assets | 400,404 | cursor/read-only | none |
| API-023 | `GET /api/v1/assets/:assetId` | U owner; ID -> metadata/status | 401,404 | ETag/read-only | none |
| API-024 | `POST /api/v1/assets/:assetId/retry-ingestion` | U owner; failed active Asset/version -> same Asset + new ingestion attempt | 404,409 | Idem + Ver | asset.ingestion_retry; TX-10-style admission |
| API-025 | `DELETE /api/v1/assets/:assetId` | U owner; expected version -> soft-deleted lifecycle | 404,409 | Idem + Ver | asset.delete_requested; later TX-20 schedule |
| API-026 | `GET /api/v1/assets/:assetId/content` | U owner; Range/conditional headers -> bytes | 404,416,503 | immutable ETag/read-only | access metric; authorized range stream |
| API-027 | `POST /api/v1/assets/:assetId/delivery-capabilities` | U owner; operation -> short opaque capability | 404,409 | Idem; resource state guard | capability.issue; TX-23 |
| API-028 | `GET /api/v1/projects/:projectId/compositions` | U owner; cursor/status -> version summaries | 400,404 | cursor/read-only | none |
| API-029 | `POST /api/v1/projects/:projectId/compositions` | U owner; CompositionDocumentV1/parent/version -> immutable version | 400,404,409,422 | Idem + project Ver | composition.create; TX-11 |
| API-030 | `GET /api/v1/compositions/:compositionVersionId` | U owner; ID -> canonical document/validation | 404 | immutable ETag | none |
| API-031 | `POST /api/v1/compositions/:compositionVersionId/derive` | U owner; structured patch/edit intent -> new version | 400,404,409,422 | Idem + expected parent/project Ver | composition.derive; TX-11 |
| API-032 | `POST /api/v1/compositions/:compositionVersionId/validate` | U owner; ID -> durable validation result | 404,409,422 | Idem; immutable input | composition.validate; bounded job or deterministic service |
| API-033 | `POST /api/v1/projects/:projectId/generation-jobs` | U owner; brief/options/asset IDs -> Job | 400,404,409,422,503 | Idem + project Ver | generation.request; TX-12 |
| API-034 | `POST /api/v1/compositions/:compositionVersionId/regenerate` | U owner; structured change brief/options -> Job | 400,404,409,422,503 | Idem; immutable source + project Ver | generation.regenerate; TX-12 |
| API-035 | `POST /api/v1/compositions/:compositionVersionId/preview` | U owner; ID -> preview artifact/status | 404,409,422 | Idem; immutable version | preview.request/materialize; TX-12/TX-17 |
| API-036 | `GET /api/v1/previews/:previewArtifactId` | U owner; ID -> artifact metadata/player bootstrap | 404,409,503 | immutable ETag/read-only | preview.access; authorized bootstrap |
| API-037 | `GET /api/v1/previews/:previewArtifactId/content` | U owner or scoped capability; Range -> immutable artifact bytes | 404,416,503 | ETag/read-only | safe access metric; range stream |
| API-038 | `POST /api/v1/compositions/:compositionVersionId/render-jobs` | U owner; output preset -> Job | 400,404,409,422,503 | Idem; immutable composition | render.request; TX-12 |
| API-039 | `GET /api/v1/jobs` | U; cursor/type/status/project -> summaries | 400 | cursor/read-only | none |
| API-040 | `GET /api/v1/jobs/:jobId` | U owner; ID -> durable snapshot | 404 | ETag/read-only | none |
| API-041 | `GET /api/v1/jobs/:jobId/steps` | U owner; cursor -> safe step summaries | 400,404 | cursor/read-only | none |
| API-042 | `GET /api/v1/jobs/:jobId/event-history` | U owner; cursor -> durable events | 400,404 | cursor/read-only | none |
| API-043 | `GET /api/v1/jobs/:jobId/events` | U owner; Last-Event-ID -> SSE | 400,404,409 | durable replay; no mutation | connection metric; no tx beyond reads |
| API-044 | `POST /api/v1/jobs/:jobId/cancel` | U owner; cancellable job/version -> snapshot | 404,409 | Idem + Ver | job.cancel_requested; TX-16 |
| API-045 | `GET /api/v1/projects/:projectId/outputs` | U owner; cursor/state -> output summaries | 400,404 | cursor/read-only | none |
| API-046 | `GET /api/v1/outputs/:outputId` | U owner; ID -> immutable evidence + lifecycle | 404 | ETag/read-only | none |
| API-047 | `GET /api/v1/outputs/:outputId/content` | U owner/capability; Range -> video bytes | 404,416,503 | immutable ETag | output.download/stream metric; no write tx |
| API-048 | `POST /api/v1/outputs/:outputId/delivery-capabilities` | U owner; download/stream -> opaque capability | 404,409 | Idem | capability.issue; TX-23 |
| API-049 | `DELETE /api/v1/outputs/:outputId` | U owner; expected state version -> purge-scheduled | 404,409 | Idem + Ver | output.delete_requested; TX-20 |
| API-050 | `GET /api/v1/settings/profile` | U; none -> safe profile/preferences | 401 | ETag/read-only | none |
| API-051 | `PATCH /api/v1/settings/profile` | U; display/preferences/version -> profile | 400,409 | Idem + Ver | user.profile_update; short user tx |
| API-052 | `GET /api/v1/settings/provider-status` | U; none -> enabled capability health (no reference names) | 401 | short cache/read-only | none |
| API-053 | `GET /api/v1/admin/users` | A; cursor/status/search -> safe users | 400,403 | cursor/read-only | admin.read_users |
| API-054 | `PATCH /api/v1/admin/users/:userId` | A; role/status/version -> user; reject last-active-admin lockout | 400,403,404,409 | Idem + Ver | admin.user_update; TX-02 |
| API-055 | `GET /api/v1/admin/jobs` | A; cursor/type/status/lease -> redacted operational summaries | 400,403 | cursor/read-only | admin.read_jobs |
| API-056 | `POST /api/v1/admin/jobs/:jobId/reconcile` | A; reason/version -> redacted reconciliation intent | 403,404,409 | Idem + Ver | admin.job_reconcile; TX-13/outbox |
| API-057 | `GET /api/v1/admin/provider-credentials` | A; cursor -> reference names/status/health | 403 | cursor/read-only | admin.read_provider_refs |
| API-058 | `PUT /api/v1/admin/provider-credentials/:referenceId` | A; provider/purpose/env-name/status/version -> reference | 400,403,409 | Idem + Ver | admin.provider_ref_update; never writes HF secret |
| API-059 | `GET /api/v1/admin/audit-events` | A; cursor/actor/action/resource/time -> redacted events | 400,403 | cursor/read-only | admin.read_audit |
| API-060 | `GET /api/v1/admin/operations` | A; none -> queue/lease/outbox/storage/backup health summary | 403,503 | read-only snapshot | admin.read_operations |
| API-062 | `GET /api/v1/trash/projects` | U; cursor -> own soft-deleted/purge-scheduled Projects | 400,401 | cursor/read-only | none |
| API-063 | `GET /api/v1/projects/:projectId/compositions/current` | U owner; project ID -> selected immutable version or null | 404 | project ETag/read-only | none |
| API-064 | `POST /api/v1/outputs/:outputId/pin` | U owner; state version -> pinned state | 404,409 | Idem + Ver | output.pin; guarded output-state tx |
| API-065 | `POST /api/v1/outputs/:outputId/unpin` | U owner; state version -> unpinned state | 404,409 | Idem + Ver | output.unpin; guarded output-state tx |
| API-066 | `GET /api/v1/admin/users/pending` | A; cursor -> pending members | 400,403 | cursor/read-only | admin.read_pending_users |
| API-067 | `POST /api/v1/admin/provider-credentials/:referenceId/health-check` | A; reference/version -> safe health result/job | 403,404,409,503 | Idem + Ver | admin.provider_health_check; bounded outbox/job intent |
| API-068 | `GET /api/v1/admin/quota-policies` | A; cursor/scope -> versioned policies | 400,403 | cursor/read-only | admin.read_quota_policies |
| API-069 | `POST /api/v1/admin/quota-policies` | A; scope/policy/effective interval -> new policy version | 400,403,409 | Idem + expected prior Ver | admin.quota_policy_create; quota/audit tx |
| API-070 | `POST /api/v1/jobs/:jobId/retry` | U owner; retryable terminal job -> new Job | 404,409,503 | Idem; immutable original request | job.retry; TX-12, never reopens terminal row |
| API-071 | `GET /api/v1/admin/jobs/:jobId` | A; ID -> redacted state/step/code/timestamps/lease diagnostics | 403,404 | ETag/read-only | admin.read_job_diagnostics; no owner SSE/content |
| API-072 | `GET /api/v1/outputs/:outputId/download` | U owner or download-scoped capability; conditional request -> attachment bytes | 404,416,503 | immutable ETag/read-only | output.download metric; authorized Range stream |
## Cursor shape
The opaque cursor encodes versioned canonical sort position, filter hash, and
UUID tie-breaker as base64url JSON plus HMAC-SHA256 under an HKDF-derived app
key. The implemented admin-user cursor is `{v:1, sort:{createdAt,id},
filterHash}` and binds normalized status/search. It never contains email,
display name, secret values, storage keys, provider operation IDs, or token
material. Invalid encoding/MAC/version/key/filter is `400 INVALID_CURSOR`;
default/max page sizes are 25/100.
## Contract ownership and evolution
Request, response, event, cursor, error, and composition schemas live in `packages/contracts` and are imported by both Fastify and React. API v1 additions are backward compatible; breaking shape/meaning requires a new API or schema version. OpenAPI may be generated from the canonical Zod contracts in Phase 3, but generated output is not a competing source of truth.
The capability map contains 72 sequential capabilities. Render cancellation and
retry use only the generic Job commands; no parallel render-specific command
routes exist. Owner-facing Project, CompositionVersion, Job history/status and
SSE routes are owner-only. Administrators inspect Jobs only through the redacted
admin list/detail/command surfaces, which exclude prompts, private bytes,
storage paths/keys, raw provider payloads, and owner SSE.