File size: 30,563 Bytes
4e11c88
 
a0319ec
4e11c88
 
 
 
 
 
 
46016e1
 
 
27329ef
 
 
 
 
 
4e11c88
 
 
 
 
4df94cd
 
 
 
 
 
 
 
 
 
 
 
 
 
439a2ff
 
 
 
 
 
 
 
 
 
4e11c88
 
46016e1
 
 
 
 
 
 
 
 
 
 
4e11c88
 
 
 
 
27329ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4e11c88
 
 
27329ef
 
 
 
 
 
 
4e11c88
 
 
 
46016e1
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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.