@giveitsmaller/contracts 0.8.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (453) hide show
  1. package/asyncapi/events.yaml +806 -173
  2. package/availability/availability.json +355 -98
  3. package/dist/asyncapi/Failure.d.ts +2 -0
  4. package/dist/asyncapi/LongFormJobMessage.d.ts +4 -3
  5. package/dist/asyncapi/LongFormOperationType.d.ts +5 -0
  6. package/dist/asyncapi/LongFormOperationType.js +6 -0
  7. package/dist/asyncapi/LongFormProcessingClass.d.ts +4 -0
  8. package/dist/asyncapi/LongFormProcessingClass.js +5 -0
  9. package/dist/asyncapi/MultiOutputCompletion.d.ts +2 -0
  10. package/dist/asyncapi/{NotificationsOperationsQueue.d.ts → NotificationsJobsQueue.d.ts} +2 -2
  11. package/dist/asyncapi/OperationResultMetadata.d.ts +4 -0
  12. package/dist/asyncapi/OperationResultMetadata.js +1 -0
  13. package/dist/asyncapi/OperationType.d.ts +2 -1
  14. package/dist/asyncapi/OperationType.js +1 -0
  15. package/dist/asyncapi/PageIndexed.d.ts +1 -0
  16. package/dist/asyncapi/PositionIndexed.d.ts +1 -0
  17. package/dist/asyncapi/SingleOutputCompletion.d.ts +2 -0
  18. package/dist/asyncapi/SourceEntry.d.ts +4 -0
  19. package/dist/asyncapi/Unindexed.d.ts +1 -0
  20. package/dist/asyncapi/index.d.ts +4 -2
  21. package/dist/asyncapi/index.js +2 -1
  22. package/dist/openapi/models/AudioWatermarkDecodeRequest.d.ts +2 -2
  23. package/dist/openapi/models/AudioWatermarkDecodeRequest.js +2 -2
  24. package/dist/openapi/models/AudioWatermarkDecodeResponse.d.ts +2 -2
  25. package/dist/openapi/models/AudioWatermarkDecodeResponse.js +2 -2
  26. package/dist/openapi/models/AuthErrorResponse.d.ts +13 -2
  27. package/dist/openapi/models/AuthErrorResponse.js +2 -2
  28. package/dist/openapi/models/AuthErrorType.d.ts +2 -2
  29. package/dist/openapi/models/AuthErrorType.js +2 -2
  30. package/dist/openapi/models/AuthRejectionEnvelope.d.ts +126 -0
  31. package/dist/openapi/models/AuthRejectionEnvelope.js +72 -0
  32. package/dist/openapi/models/AvailabilityValue.d.ts +2 -2
  33. package/dist/openapi/models/AvailabilityValue.js +2 -2
  34. package/dist/openapi/models/BalanceExhaustedResponse.d.ts +13 -2
  35. package/dist/openapi/models/BalanceExhaustedResponse.js +2 -2
  36. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.d.ts +2 -2
  37. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.js +2 -2
  38. package/dist/openapi/models/CallbackEventType.d.ts +2 -2
  39. package/dist/openapi/models/CallbackEventType.js +2 -2
  40. package/dist/openapi/models/ChangePasswordRequest.d.ts +38 -0
  41. package/dist/openapi/models/ChangePasswordRequest.js +47 -0
  42. package/dist/openapi/models/CompositionPlan.d.ts +72 -0
  43. package/dist/openapi/models/CompositionPlan.js +53 -0
  44. package/dist/openapi/models/CompositionPlanJob.d.ts +39 -0
  45. package/dist/openapi/models/CompositionPlanJob.js +48 -0
  46. package/dist/openapi/models/CompositionPlanOperation.d.ts +116 -0
  47. package/dist/openapi/models/CompositionPlanOperation.js +62 -0
  48. package/dist/openapi/models/ConfirmEmailChange200Response.d.ts +46 -0
  49. package/dist/openapi/models/ConfirmEmailChange200Response.js +54 -0
  50. package/dist/openapi/models/ConfirmEmailChange200ResponseData.d.ts +32 -0
  51. package/dist/openapi/models/ConfirmEmailChange200ResponseData.js +43 -0
  52. package/dist/openapi/models/ConfirmEmailChangeRequest.d.ts +32 -0
  53. package/dist/openapi/models/ConfirmEmailChangeRequest.js +43 -0
  54. package/dist/openapi/models/ConnectionSource.d.ts +2 -2
  55. package/dist/openapi/models/ConnectionSource.js +2 -2
  56. package/dist/openapi/models/ContactRequest.d.ts +2 -2
  57. package/dist/openapi/models/ContactRequest.js +2 -2
  58. package/dist/openapi/models/ContactSubject.d.ts +2 -2
  59. package/dist/openapi/models/ContactSubject.js +2 -2
  60. package/dist/openapi/models/ContactValidationErrorResponse.d.ts +2 -2
  61. package/dist/openapi/models/ContactValidationErrorResponse.js +2 -2
  62. package/dist/openapi/models/CreateApiKey201Response.d.ts +46 -0
  63. package/dist/openapi/models/CreateApiKey201Response.js +54 -0
  64. package/dist/openapi/models/CreateApiKey201ResponseData.d.ts +56 -0
  65. package/dist/openapi/models/CreateApiKey201ResponseData.js +59 -0
  66. package/dist/openapi/models/CreateApiKeyRequest.d.ts +32 -0
  67. package/dist/openapi/models/CreateApiKeyRequest.js +43 -0
  68. package/dist/openapi/models/CreateExternalImport403Response.d.ts +2 -2
  69. package/dist/openapi/models/CreateExternalImport403Response.js +2 -2
  70. package/dist/openapi/models/CreateExternalImport422Response.d.ts +7 -3
  71. package/dist/openapi/models/CreateExternalImport422Response.js +18 -22
  72. package/dist/openapi/models/CreateWorkflow422Response.d.ts +11 -3
  73. package/dist/openapi/models/CreateWorkflow422Response.js +28 -36
  74. package/dist/openapi/models/CreditTransaction.d.ts +2 -2
  75. package/dist/openapi/models/CreditTransaction.js +2 -2
  76. package/dist/openapi/models/CreditTransactionSourceBucket.d.ts +2 -2
  77. package/dist/openapi/models/CreditTransactionSourceBucket.js +2 -2
  78. package/dist/openapi/models/CreditsBalanceResponse.d.ts +2 -2
  79. package/dist/openapi/models/CreditsBalanceResponse.js +2 -2
  80. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.d.ts +2 -2
  81. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.js +2 -2
  82. package/dist/openapi/models/CreditsUsageResponse.d.ts +2 -2
  83. package/dist/openapi/models/CreditsUsageResponse.js +2 -2
  84. package/dist/openapi/models/CreditsUsageSuccessEnvelope.d.ts +2 -2
  85. package/dist/openapi/models/CreditsUsageSuccessEnvelope.js +2 -2
  86. package/dist/openapi/models/Delivery.d.ts +2 -2
  87. package/dist/openapi/models/Delivery.js +2 -2
  88. package/dist/openapi/models/DeliveryOutputRef.d.ts +9 -2
  89. package/dist/openapi/models/DeliveryOutputRef.js +2 -2
  90. package/dist/openapi/models/DeliveryPlan.d.ts +2 -2
  91. package/dist/openapi/models/DeliveryPlan.js +2 -2
  92. package/dist/openapi/models/DeliveryPlanOutput.d.ts +17 -2
  93. package/dist/openapi/models/DeliveryPlanOutput.js +4 -2
  94. package/dist/openapi/models/DeliveryPlanReason.d.ts +2 -2
  95. package/dist/openapi/models/DeliveryPlanReason.js +2 -2
  96. package/dist/openapi/models/DeliverySelection.d.ts +6 -4
  97. package/dist/openapi/models/DeliverySelection.js +2 -2
  98. package/dist/openapi/models/EmptySuccessEnvelope.d.ts +58 -0
  99. package/dist/openapi/models/EmptySuccessEnvelope.js +53 -0
  100. package/dist/openapi/models/EndpointProjection.d.ts +12 -3
  101. package/dist/openapi/models/EndpointProjection.js +2 -2
  102. package/dist/openapi/models/ErrorEnvelope.d.ts +13 -2
  103. package/dist/openapi/models/ErrorEnvelope.js +2 -2
  104. package/dist/openapi/models/EstimateQuality.d.ts +2 -2
  105. package/dist/openapi/models/EstimateQuality.js +2 -2
  106. package/dist/openapi/models/EstimateRange.d.ts +2 -2
  107. package/dist/openapi/models/EstimateRange.js +2 -2
  108. package/dist/openapi/models/ExternalDestination.d.ts +2 -2
  109. package/dist/openapi/models/ExternalDestination.js +2 -2
  110. package/dist/openapi/models/ExternalImportCreatedResponse.d.ts +2 -2
  111. package/dist/openapi/models/ExternalImportCreatedResponse.js +2 -2
  112. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.d.ts +2 -2
  113. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.js +2 -2
  114. package/dist/openapi/models/ExternalImportRequest.d.ts +2 -2
  115. package/dist/openapi/models/ExternalImportRequest.js +2 -2
  116. package/dist/openapi/models/ExternalImportToken.d.ts +2 -2
  117. package/dist/openapi/models/ExternalImportToken.js +2 -2
  118. package/dist/openapi/models/ExternalSource.d.ts +2 -2
  119. package/dist/openapi/models/ExternalSource.js +2 -2
  120. package/dist/openapi/models/FeatureNotAvailableResponse.d.ts +19 -9
  121. package/dist/openapi/models/FeatureNotAvailableResponse.js +2 -2
  122. package/dist/openapi/models/FeatureTierRestrictedResponse.d.ts +13 -2
  123. package/dist/openapi/models/FeatureTierRestrictedResponse.js +2 -2
  124. package/dist/openapi/models/FeatureViolation.d.ts +2 -2
  125. package/dist/openapi/models/FeatureViolation.js +2 -2
  126. package/dist/openapi/models/ForgotPasswordRequest.d.ts +32 -0
  127. package/dist/openapi/models/ForgotPasswordRequest.js +43 -0
  128. package/dist/openapi/models/ImageEncodeCapabilities.d.ts +65 -0
  129. package/dist/openapi/models/ImageEncodeCapabilities.js +55 -0
  130. package/dist/openapi/models/JobDefinition.d.ts +27 -8
  131. package/dist/openapi/models/JobDefinition.js +2 -2
  132. package/dist/openapi/models/JobDownload.d.ts +2 -2
  133. package/dist/openapi/models/JobDownload.js +2 -2
  134. package/dist/openapi/models/JobInputV2.d.ts +13 -9
  135. package/dist/openapi/models/JobInputV2.js +5 -5
  136. package/dist/openapi/models/JobMediaClass.d.ts +34 -0
  137. package/dist/openapi/models/JobMediaClass.js +52 -0
  138. package/dist/openapi/models/JobOutputSource.d.ts +2 -2
  139. package/dist/openapi/models/JobOutputSource.js +2 -2
  140. package/dist/openapi/models/JobResponse.d.ts +24 -2
  141. package/dist/openapi/models/JobResponse.js +5 -2
  142. package/dist/openapi/models/JobStatus.d.ts +2 -2
  143. package/dist/openapi/models/JobStatus.js +2 -2
  144. package/dist/openapi/models/JobType.d.ts +2 -2
  145. package/dist/openapi/models/JobType.js +2 -2
  146. package/dist/openapi/models/LivenessResponse.d.ts +2 -2
  147. package/dist/openapi/models/LivenessResponse.js +2 -2
  148. package/dist/openapi/models/LoginUser200Response.d.ts +2 -2
  149. package/dist/openapi/models/LoginUser200Response.js +2 -2
  150. package/dist/openapi/models/LoginUser200ResponseData.d.ts +2 -2
  151. package/dist/openapi/models/LoginUser200ResponseData.js +2 -2
  152. package/dist/openapi/models/LoginUser200ResponseDataUser.d.ts +2 -2
  153. package/dist/openapi/models/LoginUser200ResponseDataUser.js +2 -2
  154. package/dist/openapi/models/LoginUserRequest.d.ts +2 -2
  155. package/dist/openapi/models/LoginUserRequest.js +2 -2
  156. package/dist/openapi/models/MetadataResponse.d.ts +2 -2
  157. package/dist/openapi/models/MetadataResponse.js +2 -2
  158. package/dist/openapi/models/MetadataResponseDimensions.d.ts +2 -2
  159. package/dist/openapi/models/MetadataResponseDimensions.js +2 -2
  160. package/dist/openapi/models/MetadataResponseExif.d.ts +2 -2
  161. package/dist/openapi/models/MetadataResponseExif.js +2 -2
  162. package/dist/openapi/models/MetadataResponseExifGps.d.ts +2 -2
  163. package/dist/openapi/models/MetadataResponseExifGps.js +2 -2
  164. package/dist/openapi/models/MetadataSuccessEnvelope.d.ts +2 -2
  165. package/dist/openapi/models/MetadataSuccessEnvelope.js +2 -2
  166. package/dist/openapi/models/MimeGroupSchema.d.ts +37 -2
  167. package/dist/openapi/models/MimeGroupSchema.js +5 -2
  168. package/dist/openapi/models/MultiInputSource.d.ts +41 -0
  169. package/dist/openapi/models/MultiInputSource.js +52 -0
  170. package/dist/openapi/models/MultipartCompleteRequest.d.ts +2 -2
  171. package/dist/openapi/models/MultipartCompleteRequest.js +2 -2
  172. package/dist/openapi/models/MultipartCompleteRequestPartsInner.d.ts +2 -2
  173. package/dist/openapi/models/MultipartCompleteRequestPartsInner.js +2 -2
  174. package/dist/openapi/models/MultipartCompleteResponse.d.ts +2 -2
  175. package/dist/openapi/models/MultipartCompleteResponse.js +2 -2
  176. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.d.ts +2 -2
  177. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.js +2 -2
  178. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.d.ts +2 -2
  179. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.js +2 -2
  180. package/dist/openapi/models/MultipartInitiateResponse.d.ts +2 -2
  181. package/dist/openapi/models/MultipartInitiateResponse.js +2 -2
  182. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.d.ts +2 -2
  183. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.js +2 -2
  184. package/dist/openapi/models/MultipartKeepaliveResponse.d.ts +2 -2
  185. package/dist/openapi/models/MultipartKeepaliveResponse.js +2 -2
  186. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.d.ts +2 -2
  187. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.js +2 -2
  188. package/dist/openapi/models/MultipartPartListing.d.ts +2 -2
  189. package/dist/openapi/models/MultipartPartListing.js +2 -2
  190. package/dist/openapi/models/MultipartPresignRequest.d.ts +2 -2
  191. package/dist/openapi/models/MultipartPresignRequest.js +2 -2
  192. package/dist/openapi/models/MultipartPresignResponse.d.ts +2 -2
  193. package/dist/openapi/models/MultipartPresignResponse.js +2 -2
  194. package/dist/openapi/models/MultipartPresignSuccessEnvelope.d.ts +2 -2
  195. package/dist/openapi/models/MultipartPresignSuccessEnvelope.js +2 -2
  196. package/dist/openapi/models/MultipartStatusResponse.d.ts +2 -2
  197. package/dist/openapi/models/MultipartStatusResponse.js +2 -2
  198. package/dist/openapi/models/MultipartStatusSuccessEnvelope.d.ts +2 -2
  199. package/dist/openapi/models/MultipartStatusSuccessEnvelope.js +2 -2
  200. package/dist/openapi/models/OperationDefinition.d.ts +27 -2
  201. package/dist/openapi/models/OperationDefinition.js +11 -2
  202. package/dist/openapi/models/OperationDownload.d.ts +30 -2
  203. package/dist/openapi/models/OperationDownload.js +6 -2
  204. package/dist/openapi/models/OperationInputModel.d.ts +3 -3
  205. package/dist/openapi/models/OperationInputModel.js +3 -3
  206. package/dist/openapi/models/OperationResponse.d.ts +18 -2
  207. package/dist/openapi/models/OperationResponse.js +5 -2
  208. package/dist/openapi/models/OperationResult.d.ts +2 -2
  209. package/dist/openapi/models/OperationResult.js +2 -2
  210. package/dist/openapi/models/OperationResultMetadata.d.ts +48 -0
  211. package/dist/openapi/models/OperationResultMetadata.js +41 -0
  212. package/dist/openapi/models/OperationResultMetrics.d.ts +16 -3
  213. package/dist/openapi/models/OperationResultMetrics.js +2 -2
  214. package/dist/openapi/models/OperationSchemaDefinition.d.ts +6 -5
  215. package/dist/openapi/models/OperationSchemaDefinition.js +2 -2
  216. package/dist/openapi/models/OperationStatus.d.ts +2 -2
  217. package/dist/openapi/models/OperationStatus.js +2 -2
  218. package/dist/openapi/models/OperationType.d.ts +19 -14
  219. package/dist/openapi/models/OperationType.js +20 -15
  220. package/dist/openapi/models/OperationsSchemaResponse.d.ts +39 -4
  221. package/dist/openapi/models/OperationsSchemaResponse.js +8 -2
  222. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.d.ts +55 -0
  223. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.js +42 -0
  224. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.d.ts +40 -0
  225. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.js +45 -0
  226. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.d.ts +45 -0
  227. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.js +43 -0
  228. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.d.ts +33 -0
  229. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.js +42 -0
  230. package/dist/openapi/models/OptionSchema.d.ts +2 -2
  231. package/dist/openapi/models/OptionSchema.js +2 -2
  232. package/dist/openapi/models/PerRoleCardinalityEntry.d.ts +2 -2
  233. package/dist/openapi/models/PerRoleCardinalityEntry.js +2 -2
  234. package/dist/openapi/models/PerValueAvailabilityEntry.d.ts +2 -2
  235. package/dist/openapi/models/PerValueAvailabilityEntry.js +2 -2
  236. package/dist/openapi/models/PresignedUrlPart.d.ts +2 -2
  237. package/dist/openapi/models/PresignedUrlPart.js +2 -2
  238. package/dist/openapi/models/ProbePendingResponse.d.ts +20 -8
  239. package/dist/openapi/models/ProbePendingResponse.js +2 -2
  240. package/dist/openapi/models/ProcessingClass.d.ts +2 -2
  241. package/dist/openapi/models/ProcessingClass.js +2 -2
  242. package/dist/openapi/models/ProcessingClassBandViolation.d.ts +2 -2
  243. package/dist/openapi/models/ProcessingClassBandViolation.js +2 -2
  244. package/dist/openapi/models/ProcessingClassConstraints.d.ts +69 -0
  245. package/dist/openapi/models/ProcessingClassConstraints.js +49 -0
  246. package/dist/openapi/models/ProcessingClassEntry.d.ts +93 -0
  247. package/dist/openapi/models/ProcessingClassEntry.js +57 -0
  248. package/dist/openapi/models/ProcessingClassExceedsBandResponse.d.ts +19 -6
  249. package/dist/openapi/models/ProcessingClassExceedsBandResponse.js +2 -2
  250. package/dist/openapi/models/ProcessingClassHint.d.ts +2 -2
  251. package/dist/openapi/models/ProcessingClassHint.js +2 -2
  252. package/dist/openapi/models/ProcessingClassReason.d.ts +2 -2
  253. package/dist/openapi/models/ProcessingClassReason.js +2 -2
  254. package/dist/openapi/models/ProcessingClassRejectReason.d.ts +2 -2
  255. package/dist/openapi/models/ProcessingClassRejectReason.js +2 -2
  256. package/dist/openapi/models/ProcessingPlan.d.ts +2 -2
  257. package/dist/openapi/models/ProcessingPlan.js +2 -2
  258. package/dist/openapi/models/ProcessingPlanJob.d.ts +2 -2
  259. package/dist/openapi/models/ProcessingPlanJob.js +2 -2
  260. package/dist/openapi/models/ReEncodeDecision.d.ts +2 -2
  261. package/dist/openapi/models/ReEncodeDecision.js +2 -2
  262. package/dist/openapi/models/ReadinessResponse.d.ts +2 -2
  263. package/dist/openapi/models/ReadinessResponse.js +2 -2
  264. package/dist/openapi/models/RegisterUser422Response.d.ts +27 -0
  265. package/dist/openapi/models/RegisterUser422Response.js +47 -0
  266. package/dist/openapi/models/RegisterUserRequest.d.ts +38 -0
  267. package/dist/openapi/models/RegisterUserRequest.js +47 -0
  268. package/dist/openapi/models/ResetPasswordRequest.d.ts +38 -0
  269. package/dist/openapi/models/ResetPasswordRequest.js +47 -0
  270. package/dist/openapi/models/ResponseEnvelope.d.ts +2 -2
  271. package/dist/openapi/models/ResponseEnvelope.js +2 -2
  272. package/dist/openapi/models/RetryResponse.d.ts +2 -2
  273. package/dist/openapi/models/RetryResponse.js +2 -2
  274. package/dist/openapi/models/RetrySuccessEnvelope.d.ts +2 -2
  275. package/dist/openapi/models/RetrySuccessEnvelope.js +2 -2
  276. package/dist/openapi/models/SseCompletionBase.d.ts +2 -2
  277. package/dist/openapi/models/SseCompletionBase.js +2 -2
  278. package/dist/openapi/models/SseEventType.d.ts +2 -2
  279. package/dist/openapi/models/SseEventType.js +2 -2
  280. package/dist/openapi/models/SseJobCompletedData.d.ts +2 -2
  281. package/dist/openapi/models/SseJobCompletedData.js +2 -2
  282. package/dist/openapi/models/SseJobFailedData.d.ts +2 -2
  283. package/dist/openapi/models/SseJobFailedData.js +2 -2
  284. package/dist/openapi/models/SseMultiOutputCompletion.d.ts +2 -2
  285. package/dist/openapi/models/SseMultiOutputCompletion.js +2 -2
  286. package/dist/openapi/models/SseMultiOutputCompletionMetrics.d.ts +2 -2
  287. package/dist/openapi/models/SseMultiOutputCompletionMetrics.js +2 -2
  288. package/dist/openapi/models/SseMultiOutputCompletionWithKind.d.ts +2 -2
  289. package/dist/openapi/models/SseMultiOutputCompletionWithKind.js +2 -2
  290. package/dist/openapi/models/SseMultiOutputResultEntry.d.ts +2 -2
  291. package/dist/openapi/models/SseMultiOutputResultEntry.js +2 -2
  292. package/dist/openapi/models/SseOperationCompletedData.d.ts +17 -3
  293. package/dist/openapi/models/SseOperationCompletedData.js +2 -2
  294. package/dist/openapi/models/SseOperationCompletionResult.d.ts +2 -2
  295. package/dist/openapi/models/SseOperationCompletionResult.js +2 -2
  296. package/dist/openapi/models/SseOperationFailedData.d.ts +2 -2
  297. package/dist/openapi/models/SseOperationFailedData.js +2 -2
  298. package/dist/openapi/models/SseOperationProgressData.d.ts +4 -3
  299. package/dist/openapi/models/SseOperationProgressData.js +2 -2
  300. package/dist/openapi/models/SseSingleOutputCompletion.d.ts +2 -2
  301. package/dist/openapi/models/SseSingleOutputCompletion.js +2 -2
  302. package/dist/openapi/models/SseWorkflowTerminalData.d.ts +2 -2
  303. package/dist/openapi/models/SseWorkflowTerminalData.js +2 -2
  304. package/dist/openapi/models/TierRestrictionKind.d.ts +2 -2
  305. package/dist/openapi/models/TierRestrictionKind.js +2 -2
  306. package/dist/openapi/models/TierRestrictionResponse.d.ts +13 -2
  307. package/dist/openapi/models/TierRestrictionResponse.js +2 -2
  308. package/dist/openapi/models/UpdateProfile200Response.d.ts +46 -0
  309. package/dist/openapi/models/UpdateProfile200Response.js +54 -0
  310. package/dist/openapi/models/UpdateProfile200ResponseData.d.ts +71 -0
  311. package/dist/openapi/models/UpdateProfile200ResponseData.js +67 -0
  312. package/dist/openapi/models/UpdateProfile422Response.d.ts +27 -0
  313. package/dist/openapi/models/UpdateProfile422Response.js +47 -0
  314. package/dist/openapi/models/UpdateProfileRequest.d.ts +44 -0
  315. package/dist/openapi/models/UpdateProfileRequest.js +47 -0
  316. package/dist/openapi/models/UploadConstraintsApplied.d.ts +2 -2
  317. package/dist/openapi/models/UploadConstraintsApplied.js +2 -2
  318. package/dist/openapi/models/UploadDurationExceedsTierResponse.d.ts +13 -2
  319. package/dist/openapi/models/UploadDurationExceedsTierResponse.js +2 -2
  320. package/dist/openapi/models/UploadFile403Response.d.ts +2 -2
  321. package/dist/openapi/models/UploadFile403Response.js +2 -2
  322. package/dist/openapi/models/UploadFile422Response.d.ts +2 -2
  323. package/dist/openapi/models/UploadFile422Response.js +2 -2
  324. package/dist/openapi/models/UploadProbeMediaMetadata.d.ts +2 -2
  325. package/dist/openapi/models/UploadProbeMediaMetadata.js +2 -2
  326. package/dist/openapi/models/UploadProbeProcessingClass.d.ts +2 -2
  327. package/dist/openapi/models/UploadProbeProcessingClass.js +2 -2
  328. package/dist/openapi/models/UploadProbeResponse.d.ts +2 -2
  329. package/dist/openapi/models/UploadProbeResponse.js +2 -2
  330. package/dist/openapi/models/UploadProbeStatus.d.ts +2 -2
  331. package/dist/openapi/models/UploadProbeStatus.js +2 -2
  332. package/dist/openapi/models/UploadProbeSuccessEnvelope.d.ts +2 -2
  333. package/dist/openapi/models/UploadProbeSuccessEnvelope.js +2 -2
  334. package/dist/openapi/models/UploadResponse.d.ts +2 -2
  335. package/dist/openapi/models/UploadResponse.js +2 -2
  336. package/dist/openapi/models/UploadSizeExceedsTierResponse.d.ts +13 -2
  337. package/dist/openapi/models/UploadSizeExceedsTierResponse.js +2 -2
  338. package/dist/openapi/models/UploadSource.d.ts +2 -2
  339. package/dist/openapi/models/UploadSource.js +2 -2
  340. package/dist/openapi/models/UploadSuccessEnvelope.d.ts +2 -2
  341. package/dist/openapi/models/UploadSuccessEnvelope.js +2 -2
  342. package/dist/openapi/models/UploadThresholds.d.ts +2 -2
  343. package/dist/openapi/models/UploadThresholds.js +2 -2
  344. package/dist/openapi/models/UserTier.d.ts +2 -2
  345. package/dist/openapi/models/UserTier.js +2 -2
  346. package/dist/openapi/models/ValidationErrorEnvelope.d.ts +32 -5
  347. package/dist/openapi/models/ValidationErrorEnvelope.js +12 -2
  348. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.d.ts +2 -2
  349. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.js +2 -2
  350. package/dist/openapi/models/VerifyEmailRequest.d.ts +32 -0
  351. package/dist/openapi/models/VerifyEmailRequest.js +43 -0
  352. package/dist/openapi/models/WarningType.d.ts +2 -2
  353. package/dist/openapi/models/WarningType.js +2 -2
  354. package/dist/openapi/models/WebhookOperationContext.d.ts +2 -2
  355. package/dist/openapi/models/WebhookOperationContext.js +2 -2
  356. package/dist/openapi/models/WebhookPayload.d.ts +2 -2
  357. package/dist/openapi/models/WebhookPayload.js +2 -2
  358. package/dist/openapi/models/WorkflowCancelBillingEffect.d.ts +2 -2
  359. package/dist/openapi/models/WorkflowCancelBillingEffect.js +2 -2
  360. package/dist/openapi/models/WorkflowCancelResponse.d.ts +2 -2
  361. package/dist/openapi/models/WorkflowCancelResponse.js +2 -2
  362. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.d.ts +2 -2
  363. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.js +2 -2
  364. package/dist/openapi/models/WorkflowCreateRequest.d.ts +64 -4
  365. package/dist/openapi/models/WorkflowCreateRequest.js +10 -6
  366. package/dist/openapi/models/WorkflowCreateResponse.d.ts +24 -2
  367. package/dist/openapi/models/WorkflowCreateResponse.js +5 -2
  368. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.d.ts +2 -2
  369. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.js +2 -2
  370. package/dist/openapi/models/WorkflowDownloadResponse.d.ts +2 -2
  371. package/dist/openapi/models/WorkflowDownloadResponse.js +2 -2
  372. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.d.ts +2 -2
  373. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.js +2 -2
  374. package/dist/openapi/models/WorkflowEdge.d.ts +2 -2
  375. package/dist/openapi/models/WorkflowEdge.js +2 -2
  376. package/dist/openapi/models/WorkflowExpiredResponse.d.ts +13 -2
  377. package/dist/openapi/models/WorkflowExpiredResponse.js +2 -2
  378. package/dist/openapi/models/WorkflowListResponse.d.ts +50 -0
  379. package/dist/openapi/models/WorkflowListResponse.js +50 -0
  380. package/dist/openapi/models/WorkflowListSuccessEnvelope.d.ts +46 -0
  381. package/dist/openapi/models/WorkflowListSuccessEnvelope.js +54 -0
  382. package/dist/openapi/models/WorkflowPauseRequiredAction.d.ts +2 -2
  383. package/dist/openapi/models/WorkflowPauseRequiredAction.js +2 -2
  384. package/dist/openapi/models/WorkflowPausedDetail.d.ts +2 -2
  385. package/dist/openapi/models/WorkflowPausedDetail.js +2 -2
  386. package/dist/openapi/models/WorkflowPausedDetailLinks.d.ts +2 -2
  387. package/dist/openapi/models/WorkflowPausedDetailLinks.js +2 -2
  388. package/dist/openapi/models/WorkflowProcessing.d.ts +2 -2
  389. package/dist/openapi/models/WorkflowProcessing.js +2 -2
  390. package/dist/openapi/models/WorkflowResumeResponse.d.ts +2 -2
  391. package/dist/openapi/models/WorkflowResumeResponse.js +2 -2
  392. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.d.ts +2 -2
  393. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.js +2 -2
  394. package/dist/openapi/models/WorkflowSource.d.ts +7 -4
  395. package/dist/openapi/models/WorkflowSource.js +2 -2
  396. package/dist/openapi/models/WorkflowStatus.d.ts +2 -2
  397. package/dist/openapi/models/WorkflowStatus.js +2 -2
  398. package/dist/openapi/models/WorkflowStatusResponse.d.ts +21 -2
  399. package/dist/openapi/models/WorkflowStatusResponse.js +5 -2
  400. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.d.ts +2 -2
  401. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.js +2 -2
  402. package/dist/openapi/models/WorkflowSummary.d.ts +60 -0
  403. package/dist/openapi/models/WorkflowSummary.js +57 -0
  404. package/dist/openapi/models/WorkflowSummaryJob.d.ts +57 -0
  405. package/dist/openapi/models/WorkflowSummaryJob.js +57 -0
  406. package/dist/openapi/models/WorkflowWarning.d.ts +2 -2
  407. package/dist/openapi/models/WorkflowWarning.js +2 -2
  408. package/dist/openapi/models/WorkflowWarningSeverity.d.ts +2 -2
  409. package/dist/openapi/models/WorkflowWarningSeverity.js +2 -2
  410. package/dist/openapi/models/index.d.ts +35 -1
  411. package/dist/openapi/models/index.js +35 -1
  412. package/dist/openapi/runtime.d.ts +2 -2
  413. package/dist/openapi/runtime.js +2 -2
  414. package/dist/operations/audio_to_video.metadata.js +1 -1
  415. package/dist/operations/compress.d.ts +0 -9
  416. package/dist/operations/compress.js +0 -6
  417. package/dist/operations/compress.metadata.js +4 -12
  418. package/dist/operations/index.d.ts +3 -0
  419. package/dist/operations/index.js +3 -0
  420. package/dist/operations/merge.d.ts +4 -0
  421. package/dist/operations/merge.metadata.js +12 -3
  422. package/dist/operations/passthrough.metadata.d.ts +2 -0
  423. package/dist/operations/passthrough.metadata.js +6 -0
  424. package/dist/operations/render_variants.d.ts +24 -0
  425. package/dist/operations/render_variants.js +14 -0
  426. package/dist/operations/render_variants.metadata.d.ts +2 -0
  427. package/dist/operations/render_variants.metadata.js +18 -0
  428. package/dist/operations/split.d.ts +8 -1
  429. package/dist/operations/split.js +5 -0
  430. package/dist/operations/split.metadata.js +22 -5
  431. package/dist/operations/video_watermark.metadata.js +2 -2
  432. package/openapi/api.yaml +2566 -288
  433. package/operations/schemas/archive.yaml +1 -1
  434. package/operations/schemas/audio_overlay.yaml +29 -13
  435. package/operations/schemas/audio_to_video.yaml +12 -9
  436. package/operations/schemas/audio_watermark.yaml +18 -16
  437. package/operations/schemas/compress.yaml +34 -32
  438. package/operations/schemas/custom_luma.yaml +21 -7
  439. package/operations/schemas/image_watermark.yaml +22 -7
  440. package/operations/schemas/merge.yaml +94 -41
  441. package/operations/schemas/passthrough.yaml +49 -0
  442. package/operations/schemas/render_variants.yaml +117 -0
  443. package/operations/schemas/split.yaml +76 -15
  444. package/operations/schemas/text_watermark.yaml +6 -6
  445. package/operations/schemas/thumbnail.yaml +1 -1
  446. package/operations/schemas/video_text_watermark.yaml +2 -2
  447. package/operations/schemas/video_watermark.yaml +20 -9
  448. package/package.json +3 -1
  449. package/dist/asyncapi/AnonymousSchema_253.d.ts +0 -5
  450. package/dist/asyncapi/AnonymousSchema_253.js +0 -6
  451. package/dist/openapi/models/LogoutUser200Response.d.ts +0 -50
  452. package/dist/openapi/models/LogoutUser200Response.js +0 -53
  453. /package/dist/asyncapi/{NotificationsOperationsQueue.js → NotificationsJobsQueue.js} +0 -0
package/openapi/api.yaml CHANGED
@@ -14,7 +14,7 @@ info:
14
14
  All mutation and query endpoints return `{ success: true, data: {...} }` on success
15
15
  and `{ success: false, error: "...", details: [...] }` on failure.
16
16
  Exceptions: `GET /api/operations/schema` returns raw JSON (per-tier
17
- private caching with ETag/Last-Modified revalidation per ADR-0002 + I3),
17
+ private caching with ETag revalidation per ADR-0002 + I3),
18
18
  health probes return flat objects, and `POST /api/contact` returns
19
19
  204 with no body.
20
20
 
@@ -89,7 +89,7 @@ info:
89
89
  of truth instead of hardcoding magic numbers. A runtime
90
90
  `GET /api/uploads/limits` endpoint for dynamic discovery
91
91
  (per-tier / per-environment overrides) is a deferred follow-up.
92
- version: 2.21.0
92
+ version: 2.64.0
93
93
  contact:
94
94
  name: API Support
95
95
 
@@ -1133,6 +1133,60 @@ paths:
1133
1133
  # ============================================
1134
1134
 
1135
1135
  /api/workflows:
1136
+ get:
1137
+ summary: List the caller's workflows
1138
+ description: |
1139
+ Cursor-paginated, **user-scoped** summary list of the caller's
1140
+ workflows, most-recent-first (`created_at DESC`, with an id
1141
+ tiebreaker for same-second rows). Each row is a **lightweight
1142
+ summary** (id / status / created_at + per-job type+status + a
1143
+ deliverable-output count) for list rendering — it deliberately does
1144
+ NOT inline per-operation `result_metadata` or output details (avoids
1145
+ N×M×K payload blow-up). Drill in via `GET /api/workflows/{id}/status`
1146
+ (per-op detail incl. `result_metadata`) and
1147
+ `GET /api/workflows/{id}/downloads` (deliverables). Rows persist until
1148
+ GDPR purge — workflows whose 24h download window has expired are still
1149
+ listed. Per ticket [`nLK1IO1k`](https://trello.com/c/nLK1IO1k) (A1).
1150
+ operationId: listWorkflows
1151
+ security: [{bearerAuth: []}, {sessionAuth: []}] # required (explicit 401 — user-scoped list, no anonymous access)
1152
+ x-identity-scoped: true # caller's own workflows — implicit identity-scoped, per ADR-0016 D3 (cf. credits/usage)
1153
+ tags:
1154
+ - Workflows
1155
+ parameters:
1156
+ - name: cursor
1157
+ in: query
1158
+ required: false
1159
+ description: |
1160
+ Opaque pagination cursor from a previous page's `next_cursor`.
1161
+ Omit (or send empty) for the first page; walk pages by passing
1162
+ each `next_cursor` until `is_truncated: false`. Encodes the
1163
+ `(created_at, id)` position — treat as opaque, do not parse.
1164
+ schema:
1165
+ type: string
1166
+ - name: limit
1167
+ in: query
1168
+ required: false
1169
+ description: Maximum rows per page (1-100; default 20).
1170
+ schema:
1171
+ type: integer
1172
+ minimum: 1
1173
+ maximum: 100
1174
+ default: 20
1175
+ responses:
1176
+ '200':
1177
+ description: Cursor-paginated summary list of the caller's workflows.
1178
+ content:
1179
+ application/json:
1180
+ schema:
1181
+ $ref: '#/components/schemas/WorkflowListSuccessEnvelope'
1182
+ '401':
1183
+ description: |
1184
+ Authentication required — anonymous callers cannot list
1185
+ workflows (the list is user-scoped).
1186
+ content:
1187
+ application/json:
1188
+ schema:
1189
+ $ref: '#/components/schemas/ErrorEnvelope'
1136
1190
  post:
1137
1191
  summary: Create a workflow
1138
1192
  description: |
@@ -1146,11 +1200,20 @@ paths:
1146
1200
  discriminated union with 4 leaves: `upload` /
1147
1201
  `external_import` / `connection` / `job_output`).
1148
1202
  - **Multi-input jobs** (merge, archive, image_watermark,
1149
- custom_luma, audio_overlay): provide `inputs[]` with
1203
+ custom_luma, audio_overlay, audio_to_video, video_watermark):
1204
+ provide `inputs[]` with
1150
1205
  `JobInputV2` entries. Each entry has its own `source`
1151
- (same `WorkflowSource` union), plus optional `role`
1152
- (image_watermark, custom_luma, audio_overlay) and
1153
- `per_input_options`.
1206
+ (a `MultiInputSource` the 3-leaf subset of `WorkflowSource`
1207
+ that **excludes `upload`**: `job_output` / `external_import` /
1208
+ `connection`), plus optional `role`
1209
+ (image_watermark, custom_luma, audio_overlay, audio_to_video,
1210
+ video_watermark) and
1211
+ `per_input_options`. To feed an uploaded file into a
1212
+ multi-input op, create a `passthrough` source job (single-input,
1213
+ `operations: [{type: passthrough}]`) and reference it from
1214
+ `inputs[].source` as `{type: job_output, from: <id>}` — see the
1215
+ `v2_merge_two_uploads` example and the `passthrough` bullet in
1216
+ `OperationType`.
1154
1217
 
1155
1218
  Exactly one of `source` or `inputs` is required per job.
1156
1219
 
@@ -1182,10 +1245,31 @@ paths:
1182
1245
  still receives the implicit `compress` — see above). Per
1183
1246
  [ADR-0004](../docs/decisions/0004-job-shape.md).
1184
1247
 
1248
+ **Inert passthrough (lossless source jobs).** A single-input
1249
+ source job whose SOLE operation is `passthrough`
1250
+ (`operations: [{type: passthrough}]`) emits its source bytes
1251
+ UNCHANGED — no compression, no transformation, no Lambda. This
1252
+ is the EXPLICIT lossless path (built on the opt-out rule above:
1253
+ a non-empty `operations[]` without `compress`); it is distinct
1254
+ from `operations: []`, which KEEPS its implicit-compress meaning
1255
+ on a single-input upload job. The API **self-completes** a
1256
+ passthrough job at publish: the job's terminal output IS the
1257
+ upload's `{bucket, key}` unchanged, and `passthrough` is **never
1258
+ published to SNS** (no `ops-passthrough` queue; absent from the
1259
+ AsyncAPI routing enums). Its purpose is to bridge an uploaded
1260
+ file into a multi-input op (`merge` / `archive` /
1261
+ `image_watermark` / …): since `JobInputV2.source` excludes
1262
+ upload-direct, the upload becomes a `passthrough` source job
1263
+ that the downstream multi-input job references via
1264
+ `{type: job_output, from: <id>}` — preserving billing / DAG /
1265
+ lineage. Per ticket
1266
+ [`4som89Uh`](https://trello.com/c/4som89Uh).
1267
+
1185
1268
  **Multi-input constraint:** multi-input jobs (`inputs[]`) must
1186
1269
  have exactly one operation, which must be a multi-input type
1187
- (`merge`, `archive`, `image_watermark`, `custom_luma`, or
1188
- `audio_overlay`). Chaining after a multi-input operation is
1270
+ (`merge`, `archive`, `image_watermark`, `custom_luma`,
1271
+ `audio_overlay`, `audio_to_video`, or `video_watermark`).
1272
+ Chaining after a multi-input operation is
1189
1273
  not valid — create a downstream `job_output`-sourced job
1190
1274
  instead.
1191
1275
 
@@ -1210,7 +1294,14 @@ paths:
1210
1294
  is deferred per ADR-0001 §1.7 row 5.
1211
1295
  operationId: createWorkflow
1212
1296
  security: [{}, {bearerAuth: []}, {sessionAuth: []}] # optional (anon-OK on free-tier baseline; auth required for tier-gated operations)
1213
- x-identity-scoped: true # request body dereferences upload file_ids — cross-identity references return 403 per ADR-0016 D3 (NOTE: §D4 ADR sample showed identity_scoped: false; B3 audit flipped to true per §D3 principle — SDK-acked 2026-05-28)
1297
+ # x-identity-scoped: the request body dereferences identity-bound upload refs.
1298
+ # CARVE-OUT (ADR-0016 Amendment, nGYbgChX): cross-identity/anonymous upload refs
1299
+ # are 404 UPLOAD_NOT_FOUND IDOR/BOLA-masked (NEVER 403) so the response does not
1300
+ # reveal another user's upload exists — see the createWorkflow 404 response.
1301
+ # ADR-0016 D3's generic "identity-scoped -> 403" gloss over-claimed for uploads;
1302
+ # the runtime always 404-masked (CreateWorkflowCommandHandler.php:132-148).
1303
+ # (§D4 sample showed identity_scoped: false; B3 audit flipped to true per §D3 — SDK-acked 2026-05-28.)
1304
+ x-identity-scoped: true
1214
1305
  tags:
1215
1306
  - Workflow
1216
1307
  requestBody:
@@ -1232,6 +1323,26 @@ paths:
1232
1323
  options:
1233
1324
  mode: lossy
1234
1325
  quality: 80
1326
+ v2_flat_single_source:
1327
+ summary: >-
1328
+ V2 flat form (D0Gsri8V) — top-level source + operations,
1329
+ no jobs[] wrapper. Exactly equivalent to wrapping these in
1330
+ jobs:[{source,operations}]. Compress + a thumbnail derived
1331
+ from the original.
1332
+ value:
1333
+ source:
1334
+ type: upload
1335
+ file_id: "019539ab-1111-7000-8000-000000000001"
1336
+ operations:
1337
+ - type: compress
1338
+ options:
1339
+ mode: lossy
1340
+ quality: 80
1341
+ - type: thumbnail
1342
+ base: original
1343
+ options:
1344
+ width: 320
1345
+ height: 240
1235
1346
  v2_chain_compress_thumbnail:
1236
1347
  summary: V2 chained compress -> thumbnail (id required on first job)
1237
1348
  value:
@@ -1251,16 +1362,28 @@ paths:
1251
1362
  width: 320
1252
1363
  height: 240
1253
1364
  v2_merge_two_uploads:
1254
- summary: V2 multi-input merge (two uploads, no role)
1365
+ summary: V2 multi-input merge (two uploads via passthrough source jobs)
1255
1366
  value:
1256
1367
  jobs:
1368
+ - id: src_a
1369
+ source:
1370
+ type: upload
1371
+ file_id: "019539ab-1111-7000-8000-000000000001"
1372
+ operations:
1373
+ - type: passthrough
1374
+ - id: src_b
1375
+ source:
1376
+ type: upload
1377
+ file_id: "019539ab-1111-7000-8000-000000000002"
1378
+ operations:
1379
+ - type: passthrough
1257
1380
  - inputs:
1258
1381
  - source:
1259
- type: upload
1260
- file_id: "019539ab-1111-7000-8000-000000000001"
1382
+ type: job_output
1383
+ from: src_a
1261
1384
  - source:
1262
- type: upload
1263
- file_id: "019539ab-1111-7000-8000-000000000002"
1385
+ type: job_output
1386
+ from: src_b
1264
1387
  operations:
1265
1388
  - type: merge
1266
1389
  v2_external_import:
@@ -1393,11 +1516,59 @@ paths:
1393
1516
  max_parallel_long_form_jobs: 2
1394
1517
  warnings: []
1395
1518
  '400':
1396
- description: Validation failed (malformed request body)
1519
+ description: |
1520
+ Bad request — malformed request body, or a workflow-DAG
1521
+ structural violation caught before semantic validation. The
1522
+ `ErrorEnvelope.error` field carries a specific machine code (per
1523
+ [`g8PPkbNu`](https://trello.com/c/g8PPkbNu) — narrower
1524
+ DAG-validation codes; HTTP statuses unchanged, only the code
1525
+ narrows). Conformance-safe: `error` is a free string (no enum).
1526
+ - `WORKFLOW_EDGE_REFERENCES_UNKNOWN_JOB` — a `workflow_edges`
1527
+ entry, a job-level `source`, or an `inputs[].source` references
1528
+ a job `ref`/`id` not present in the request (one code across
1529
+ all unknown-job-reference sites).
1530
+ - `CYCLIC_JOB_OUTPUT_SOURCE_GRAPH` — an implicit cycle in the
1531
+ `job_output` source graph, detected during effective-input-MIME
1532
+ resolution (distinct from explicit `workflow_edges` cycles,
1533
+ which are 422 `CYCLIC_WORKFLOW_EDGES`).
1534
+ - `RESERVED_JOB_ID_PATTERN` — a user-supplied job `id` matches the
1535
+ reserved `^job_\d+$` pattern **and is the SOLE validation
1536
+ failure**; mixed-violation requests keep the aggregated generic
1537
+ `BAD_REQUEST`.
1538
+ - `BAD_REQUEST` — malformed JSON / generic body validation, or any
1539
+ mixed/aggregated violation set.
1397
1540
  content:
1398
1541
  application/json:
1399
1542
  schema:
1400
1543
  $ref: '#/components/schemas/ErrorEnvelope'
1544
+ examples:
1545
+ edge_references_unknown_job:
1546
+ summary: A workflow reference points at a job that does not exist
1547
+ value:
1548
+ success: false
1549
+ error: "WORKFLOW_EDGE_REFERENCES_UNKNOWN_JOB"
1550
+ message: "A workflow reference points at a job that does not exist."
1551
+ message_key: "error.workflow_edge_references_unknown_job"
1552
+ reserved_job_id_pattern:
1553
+ summary: Sole violation — a job id uses the reserved pattern
1554
+ value:
1555
+ success: false
1556
+ error: "RESERVED_JOB_ID_PATTERN"
1557
+ message: "Job id must not use the reserved pattern (job_<number>)."
1558
+ message_key: "error.reserved_job_id_pattern"
1559
+ cyclic_job_output_source_graph:
1560
+ summary: Implicit cycle in the job_output source graph
1561
+ value:
1562
+ success: false
1563
+ error: "CYCLIC_JOB_OUTPUT_SOURCE_GRAPH"
1564
+ message: "Job output sources form a cycle."
1565
+ message_key: "error.cyclic_job_output_source_graph"
1566
+ bad_request_malformed:
1567
+ summary: Malformed JSON / generic or mixed-violation body
1568
+ value:
1569
+ success: false
1570
+ error: "BAD_REQUEST"
1571
+ message: "Invalid workflow request"
1401
1572
  '403':
1402
1573
  description: |
1403
1574
  Forbidden — tier-quota or feature-tier restriction. Discriminated
@@ -1483,23 +1654,54 @@ paths:
1483
1654
  error_type: "balance_exhausted"
1484
1655
  required_action: "wait_for_renewal"
1485
1656
  '404':
1486
- description: Referenced file_id not found
1657
+ description: |
1658
+ A referenced upload was not found. Returns the stable machine
1659
+ code `UPLOAD_NOT_FOUND` (`message_key: "upload.not_found"`).
1660
+
1661
+ Uploads are referenced via `jobs[].source` (a `WorkflowSource`
1662
+ with `type: upload`), including the `passthrough` source jobs
1663
+ that bridge an uploaded file into a multi-input operation —
1664
+ multi-input `inputs[].source` cannot reference an upload
1665
+ directly (`MultiInputSource` excludes the `upload` leaf).
1666
+
1667
+ **BOLA/IDOR existence-mask (deliberate, security-reasoned):**
1668
+ this 404 is ALSO returned when the upload exists but is owned
1669
+ by a different identity (or an anonymous workflow references an
1670
+ owned upload) — reported as not-found, **never 403**, so the
1671
+ response does not reveal that another user's upload exists.
1672
+ Consistent with the workflow-owner 404s on
1673
+ cancel / resume / get-workflow. Ownerless (anonymous-intake)
1674
+ uploads have no owner to violate and stay usable by any caller.
1675
+ See [ADR-0016](../docs/decisions/0016-per-endpoint-auth-identity-modeling.md)
1676
+ §"Amendment — upload IDOR mask".
1487
1677
  content:
1488
1678
  application/json:
1489
1679
  schema:
1490
1680
  $ref: '#/components/schemas/ErrorEnvelope'
1491
1681
  example:
1492
1682
  success: false
1493
- error: "Upload not found: 019539ab-1111-7000-8000-999999999999"
1683
+ error: "UPLOAD_NOT_FOUND"
1684
+ message: "Upload not found"
1685
+ message_key: "upload.not_found"
1686
+ locale: "en-GB"
1494
1687
  '422':
1495
1688
  description: |
1496
1689
  Semantically invalid request. One of:
1497
1690
 
1498
1691
  - Generic validation error (unknown operation type, invalid
1499
- option combinations, cyclic dependency in workflow_edges,
1500
- multi-input job with multiple operations, source ref
1501
- pointing to a non-existent job) — returned as
1502
- `ValidationErrorEnvelope`. No `error_type` field.
1692
+ option combinations, multi-input job with multiple
1693
+ operations) returned as `ValidationErrorEnvelope`
1694
+ (`error_type: "validation_error"`, per ADR-0018).
1695
+ - Cyclic explicit `workflow_edges` (a cycle or self-edge in the
1696
+ edge DAG) — returned as a `ValidationErrorEnvelope` with
1697
+ `error_type: "validation_error"`, `error:
1698
+ "CYCLIC_WORKFLOW_EDGES"`, `message_key:
1699
+ "error.cyclic_workflow_edges"`, and a `details[0].field =
1700
+ "workflow_edges"` entry (per
1701
+ [`g8PPkbNu`](https://trello.com/c/g8PPkbNu)). Implicit
1702
+ `job_output` source-graph cycles and unknown-job references are
1703
+ narrower **400** codes (`CYCLIC_JOB_OUTPUT_SOURCE_GRAPH` /
1704
+ `WORKFLOW_EDGE_REFERENCES_UNKNOWN_JOB`) — see the 400 response.
1503
1705
  - Feature-availability violation (workflow references one or
1504
1706
  more `planned` / `experimental` / non-enabled-`beta`
1505
1707
  features) — returned as `FeatureNotAvailableResponse`
@@ -1537,29 +1739,34 @@ paths:
1537
1739
  or `always`. The compatibility-probe algorithm is
1538
1740
  server-side and deliberately opaque per plan v5 §F8.2.
1539
1741
 
1540
- Backward-compatibility note: the prior 422 shape
1541
- (`ValidationErrorEnvelope`) is preserved unchanged — its
1542
- `details[]` array is the structural signature consumers
1543
- duck-type against. `FeatureNotAvailableResponse` carries
1544
- `error_type: feature_not_available` + `violations[]`;
1545
- `ProcessingClassExceedsBandResponse` carries `error_type:
1546
- processing_class_exceeds_band` + `violations[]`.
1547
- `ProbePendingResponse` carries `error_type: probe_pending`
1548
- + `job_ref` (and NO `violations[]` / `details[]`), emitted
1549
- when the probe-pending gate is on and a job's upload has
1550
- not yet been probed. The `REQUIRES_REENCODE` flavour reuses
1551
- `ValidationErrorEnvelope`
1552
- (a user-fixable validation error in the same family as
1553
- `INVALID_OPTIONS`) and is distinguished by the stable
1554
- `error: "REQUIRES_REENCODE"` string code rather than a new
1555
- `error_type` enum value. Branches are distinguishable by
1556
- required-field shape (`details` vs `error_type` value), so
1557
- plain `oneOf` matches without a discriminator object see
1558
- `openapi/api.yaml` §"FEATURE AVAILABILITY ENVELOPES"
1559
- commentary for the convention rationale (no discriminator
1560
- here because `ValidationErrorEnvelope` has no `error_type`
1561
- field; OpenAPI 3.1 discriminators require the property to
1562
- be present on every branch).
1742
+ **Branch dispatch via the `error_type` discriminator** (per
1743
+ [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md)).
1744
+ All four branches carry a required `error_type` enum, so the
1745
+ `oneOf` declares an explicit `discriminator`
1746
+ (`propertyName: error_type`):
1747
+ - `validation_error` `ValidationErrorEnvelope` (carries
1748
+ `details[]`; the specific failure is in the `error` machine
1749
+ code `INVALID_OPTIONS`, `REQUIRES_REENCODE`,
1750
+ `CYCLIC_WORKFLOW_EDGES`, …). The `REQUIRES_REENCODE` flavour
1751
+ (per I16-CONS) reuses this same envelope/`error_type` and is
1752
+ distinguished only by its `error` code.
1753
+ - `feature_not_available` → `FeatureNotAvailableResponse`
1754
+ (`violations[]`).
1755
+ - `processing_class_exceeds_band`
1756
+ `ProcessingClassExceedsBandResponse` (`violations[]`).
1757
+ - `probe_pending` `ProbePendingResponse` (`job_ref`, no
1758
+ `violations[]`/`details[]`), emitted when the probe-pending
1759
+ gate is on and a job's upload has not yet been probed.
1760
+
1761
+ The prior 422 shape is otherwise preserved unchanged — the
1762
+ `details[]` / `violations[]` / `job_ref` required-field shapes
1763
+ still hold, so structural duck-typing keeps working; the
1764
+ discriminator simply lets generated SDK clients dispatch on a
1765
+ single property instead of mis-firing `instanceOf` guards
1766
+ (camelCase-vs-snake_case). `error_type: validation_error` is a
1767
+ **new required field** on `ValidationErrorEnvelope` — the
1768
+ server populates it at every emission site (rollout coordinated
1769
+ with the API per ADR-0018).
1563
1770
  headers:
1564
1771
  Retry-After:
1565
1772
  required: false
@@ -1583,11 +1790,19 @@ paths:
1583
1790
  - $ref: '#/components/schemas/FeatureNotAvailableResponse'
1584
1791
  - $ref: '#/components/schemas/ProcessingClassExceedsBandResponse'
1585
1792
  - $ref: '#/components/schemas/ProbePendingResponse'
1793
+ discriminator:
1794
+ propertyName: error_type
1795
+ mapping:
1796
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
1797
+ feature_not_available: '#/components/schemas/FeatureNotAvailableResponse'
1798
+ processing_class_exceeds_band: '#/components/schemas/ProcessingClassExceedsBandResponse'
1799
+ probe_pending: '#/components/schemas/ProbePendingResponse'
1586
1800
  examples:
1587
1801
  validation_error:
1588
1802
  summary: Generic validation error (legacy shape)
1589
1803
  value:
1590
1804
  success: false
1805
+ error_type: "validation_error"
1591
1806
  error: "INVALID_OPTIONS"
1592
1807
  details:
1593
1808
  - operation: "compress"
@@ -1596,6 +1811,17 @@ paths:
1596
1811
  - operation: "thumbnail"
1597
1812
  option: "width"
1598
1813
  message: "Required field"
1814
+ cyclic_workflow_edges:
1815
+ summary: Cyclic or self-referential explicit workflow_edges (g8PPkbNu)
1816
+ value:
1817
+ success: false
1818
+ error_type: "validation_error"
1819
+ error: "CYCLIC_WORKFLOW_EDGES"
1820
+ message: "Workflow edges form a cycle."
1821
+ message_key: "error.cyclic_workflow_edges"
1822
+ details:
1823
+ - field: "workflow_edges"
1824
+ message: "Workflow edges form a cycle."
1599
1825
  feature_not_available:
1600
1826
  summary: Workflow references planned features
1601
1827
  value:
@@ -1663,6 +1889,7 @@ paths:
1663
1889
  summary: merge.video re_encode_mode=never with incompatible inputs (I16-CONS)
1664
1890
  value:
1665
1891
  success: false
1892
+ error_type: "validation_error"
1666
1893
  error: "REQUIRES_REENCODE"
1667
1894
  details:
1668
1895
  - operation: "merge"
@@ -2156,10 +2383,11 @@ paths:
2156
2383
 
2157
2384
  **Per-tier private caching.** Per ADR-0002, the response is
2158
2385
  per-caller; CDN-style public caching is NOT used. Clients
2159
- revalidate via `ETag` (strong) and/or `Last-Modified` headers —
2160
- send `If-None-Match` / `If-Modified-Since` on subsequent
2161
- requests; the server returns `304 Not Modified` when the cached
2162
- response is still fresh.
2386
+ revalidate via the strong `ETag` send `If-None-Match` on
2387
+ subsequent requests; the server returns `304 Not Modified` when
2388
+ the cached response is still fresh. The content `ETag` is the
2389
+ **sole** conditional validator (per `sUyA9ZXD`): `If-Modified-Since`
2390
+ is **not** honored and `Last-Modified` is informational only.
2163
2391
 
2164
2392
  Cache-key composition (server-side): `user_tier + schema_version + capabilities_version + environment`.
2165
2393
 
@@ -2204,9 +2432,11 @@ paths:
2204
2432
  in: header
2205
2433
  required: false
2206
2434
  description: |
2207
- Conditional revalidation: send the previously-received
2208
- `Last-Modified` value (HTTP-date) to receive `304 Not
2209
- Modified` when the response is still fresh.
2435
+ **NOT honored for conditional `304` (per `sUyA9ZXD`).** The
2436
+ server ignores `If-Modified-Since`; the content `ETag`
2437
+ (`If-None-Match`) is the sole conditional validator. Retained
2438
+ for backward compatibility — sending it has no effect on
2439
+ freshness; use `If-None-Match` instead.
2210
2440
  schema:
2211
2441
  type: string
2212
2442
  format: http-date
@@ -2215,7 +2445,7 @@ paths:
2215
2445
  '200':
2216
2446
  description: |
2217
2447
  Operation schema (raw JSON, no ResponseEnvelope). Tier-scoped
2218
- per caller; revalidate via `ETag` / `Last-Modified`.
2448
+ per caller; revalidate via the `ETag` (`If-None-Match`).
2219
2449
  headers:
2220
2450
  Cache-Control:
2221
2451
  schema:
@@ -2238,8 +2468,10 @@ paths:
2238
2468
  type: string
2239
2469
  format: http-date
2240
2470
  description: |
2241
- HTTP-date of last response generation. Clients may send
2242
- `If-Modified-Since: <date>` for revalidation.
2471
+ HTTP-date of last response generation. **Informational
2472
+ only** NOT a conditional validator (the server does not
2473
+ honor `If-Modified-Since`, per `sUyA9ZXD`). Use the `ETag`
2474
+ (`If-None-Match`) for revalidation.
2243
2475
  example: "Sun, 26 Apr 2026 09:00:00 GMT"
2244
2476
  content:
2245
2477
  application/json:
@@ -2300,10 +2532,10 @@ paths:
2300
2532
  values: ["max", "crop", "scale"]
2301
2533
  '304':
2302
2534
  description: |
2303
- Cached response is still fresh. Server emits `ETag` +
2304
- `Last-Modified` headers matching the client's revalidation
2305
- request (`If-None-Match` and/or `If-Modified-Since`). Body
2306
- empty.
2535
+ Cached response is still fresh (the client's `If-None-Match`
2536
+ matched the current content `ETag`). Server emits `ETag` +
2537
+ (informational) `Last-Modified` headers. `If-Modified-Since`
2538
+ does not drive this response (per `sUyA9ZXD`). Body empty.
2307
2539
  headers:
2308
2540
  Cache-Control:
2309
2541
  schema:
@@ -2655,22 +2887,7 @@ paths:
2655
2887
  content:
2656
2888
  application/json:
2657
2889
  schema:
2658
- type: object
2659
- required:
2660
- - success
2661
- - data
2662
- properties:
2663
- success:
2664
- type: boolean
2665
- enum: [true]
2666
- data:
2667
- type: object
2668
- description: |
2669
- Empty object on logout success — preserves the
2670
- `{ success: true, data: {...} }` envelope
2671
- invariant from `info.description`. Logout has
2672
- no return payload; the data slot is always
2673
- `{}`.
2890
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
2674
2891
  example:
2675
2892
  success: true
2676
2893
  data: {}
@@ -2703,208 +2920,1247 @@ paths:
2703
2920
  schema:
2704
2921
  $ref: '#/components/schemas/ErrorEnvelope'
2705
2922
 
2706
- # ============================================
2707
- # EXTERNAL IMPORT ENDPOINT (ticket I10)
2708
- # ============================================
2709
-
2710
- /api/external-imports:
2923
+ /api/auth/register:
2711
2924
  post:
2712
- summary: Register a one-shot external import URL
2925
+ summary: Register a new account
2713
2926
  description: |
2714
- Register a one-shot bearer URL (S3 presigned / GCS signed /
2715
- Azure SAS / Dropbox shared link / public HTTPS) and receive an
2716
- opaque `external_source_id` handle. Subsequent workflows
2717
- reference the handle via `WorkflowSource` of `type:
2718
- external_import`; the original URL + password are encrypted
2719
- server-side and never returned in any response.
2720
-
2721
- Per [ADR-0005](../docs/decisions/0005-external-sources.md) — see
2722
- §"SSRF posture" for the 8 validation rules applied at
2723
- registration time AND again at fetch time.
2724
-
2725
- **Availability:** `planned` — runtime depends on cross-repo
2726
- ticket [`MbosYtJD`](https://trello.com/c/MbosYtJD) (Lambda team
2727
- publishes capability manifest) and the
2728
- [`POST /api/connections`](https://trello.com/c/MbosYtJD) vault
2729
- endpoint shipping. The contract surface ships now (contract-first
2730
- per ADR-0001 §1.3); the runtime endpoint returns `404` until
2731
- cross-repo wiring lands.
2732
- operationId: createExternalImport
2733
- # Auth: REQUIRED defensively. Endpoint is availability:planned (no
2734
- # controller yet); but the endpoint stores encrypted server-side
2735
- # secrets (bearer URLs, passwords) and anon-callable secret
2736
- # storage is almost certainly wrong. Locking the contract here
2737
- # while we still own the source of truth; runtime tightens to
2738
- # match when the controller lands. Per ADR-0016 / karen review
2739
- # on yN309QVb-B2.
2740
- security: [{bearerAuth: []}, {sessionAuth: []}]
2741
- x-availability: planned
2927
+ Creates an unverified account from an email/password pair and
2928
+ dispatches a verification email. The account exists but cannot
2929
+ authenticate until the verification token is redeemed at
2930
+ `POST /api/auth/verify-email`.
2931
+
2932
+ Disposable / blocklisted email domains and other invalid-argument
2933
+ rejections collapse into a non-validation `422` envelope
2934
+ (`error: UNPROCESSABLE_ENTITY`, no `details[]`) — distinct from the
2935
+ structured `ValidationErrorEnvelope` returned for malformed input.
2936
+ operationId: registerUser
2937
+ security: [] # anonymous (sign-up entry point — no creds yet)
2742
2938
  tags:
2743
- - Upload
2939
+ - Auth
2744
2940
  requestBody:
2745
2941
  required: true
2746
2942
  content:
2747
2943
  application/json:
2748
2944
  schema:
2749
- $ref: '#/components/schemas/ExternalImportRequest'
2945
+ type: object
2946
+ required:
2947
+ - email
2948
+ - password
2949
+ properties:
2950
+ email:
2951
+ type: string
2952
+ format: email
2953
+ maxLength: 254
2954
+ password:
2955
+ type: string
2956
+ minLength: 8
2957
+ maxLength: 128
2958
+ example:
2959
+ email: "user@example.com"
2960
+ password: "s3cretpw"
2750
2961
  responses:
2751
2962
  '201':
2752
- description: External-import handle created.
2963
+ description: |
2964
+ Account created. A verification email has been dispatched; the
2965
+ account cannot authenticate until verified.
2753
2966
  content:
2754
2967
  application/json:
2755
2968
  schema:
2756
- $ref: '#/components/schemas/ExternalImportCreatedSuccessEnvelope'
2969
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
2757
2970
  example:
2758
2971
  success: true
2759
- data:
2760
- external_source_id: "019539ab-2222-7000-8000-000000000001"
2761
- expires_at: "2026-04-26T15:00:00Z"
2762
- provider: "s3_presigned"
2972
+ data: {}
2763
2973
  '400':
2764
- description: Validation failed (missing url, malformed URI, scheme not https).
2974
+ description: Request body is invalid (malformed JSON, wrong field types, or body too large).
2765
2975
  content:
2766
2976
  application/json:
2767
2977
  schema:
2768
2978
  $ref: '#/components/schemas/ErrorEnvelope'
2769
2979
  example:
2770
2980
  success: false
2771
- error: "URL is required and must be https://"
2772
- '403':
2773
- description: |
2774
- Forbidden — SSRF policy violation (private IP, loopback,
2775
- cloud metadata endpoint), tier-quota restriction, or
2776
- provider not enabled for caller's tier.
2777
- content:
2778
- application/json:
2779
- schema:
2780
- oneOf:
2781
- - $ref: '#/components/schemas/TierRestrictionResponse'
2782
- - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
2783
- - $ref: '#/components/schemas/ErrorEnvelope'
2981
+ error: "Request body is invalid."
2784
2982
  '422':
2785
2983
  description: |
2786
- Unprocessable URL DNS resolution failed, target
2787
- unreachable, content-type / magic-byte sniff failed,
2788
- `expires_at` already past, or `feature_not_available`
2789
- (provider tagged `planned` for caller's tier).
2984
+ Structured input validation failure (`ValidationErrorEnvelope`,
2985
+ `error_type: validation_error`).
2986
+
2987
+ A non-validation `422` (`error: UNPROCESSABLE_ENTITY`, no
2988
+ `details[]`) is also returned for disposable/blocklisted email
2989
+ domains.
2790
2990
  content:
2791
2991
  application/json:
2792
2992
  schema:
2793
2993
  oneOf:
2794
2994
  - $ref: '#/components/schemas/ValidationErrorEnvelope'
2795
- - $ref: '#/components/schemas/FeatureNotAvailableResponse'
2995
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
2996
+ discriminator:
2997
+ propertyName: error_type
2998
+ mapping:
2999
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
3000
+ unprocessable_entity: '#/components/schemas/AuthRejectionEnvelope'
3001
+ example:
3002
+ success: false
3003
+ error_type: validation_error
3004
+ error: "Validation failed"
3005
+ details:
3006
+ - field: "password"
3007
+ message: "This value is too short."
2796
3008
  '429':
2797
- description: Rate limit exceeded
3009
+ description: |
3010
+ Rate limit exceeded (too many registration attempts in the
3011
+ current window). Carries a `Retry-After` response header per the
3012
+ convention established at `POST /api/auth/login`.
3013
+ headers:
3014
+ Retry-After:
3015
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3016
+ schema:
3017
+ type: integer
3018
+ minimum: 0
3019
+ example: 60
2798
3020
  content:
2799
3021
  application/json:
2800
3022
  schema:
2801
3023
  $ref: '#/components/schemas/ErrorEnvelope'
2802
3024
  '500':
2803
- description: Internal server error
3025
+ description: Internal server error.
2804
3026
  content:
2805
3027
  application/json:
2806
3028
  schema:
2807
3029
  $ref: '#/components/schemas/ErrorEnvelope'
2808
3030
 
2809
- # ============================================
2810
- # AUDIO WATERMARK DECODE
2811
- # ============================================
2812
-
2813
- /api/audio-watermark/decode:
3031
+ /api/auth/verify-email:
2814
3032
  post:
2815
- summary: Decode an embedded audio watermark
3033
+ summary: Verify an account email address
2816
3034
  description: |
2817
- Extract the steganographic watermark embedded by a prior
2818
- `audio_watermark` operation (per ticket
2819
- [I20](https://trello.com/c/omiCq7Vn)). Pairs with the
2820
- `audio_watermark` operation declared in
2821
- `schemas/operations/audio_watermark.yaml` the operation
2822
- embeds; this endpoint decodes.
2823
-
2824
- **Tier-restricted.** This endpoint is `enterprise`-only. Free
2825
- and `pro` callers receive a 403 `feature_tier_restricted`.
2826
- Anonymous callers receive a 401.
2827
-
2828
- **Scope: own watermarks only.** The decoder will refuse to
2829
- extract from media the caller did not mark themselves
2830
- (server-side audit log of watermark origin per
2831
- `watermark_id`). Per spike S11 acceptance — this avoids the
2832
- privacy implications of arbitrary-media decode while still
2833
- serving the forensic-tracking use case (find a leaked asset
2834
- on a third-party platform, decode the watermark, look up the
2835
- distribution channel that received the marked copy).
2836
-
2837
- **Rate-limited.** Request rate per caller is enforced
2838
- independently from the workflow-create rate limit; abusive
2839
- decode patterns flag for audit per [ADR-0001](../docs/decisions/0001-contract-first-availability.md).
2840
-
2841
- **`availability: planned`** — operation Lambda + decode
2842
- Lambda are cross-repo follow-up. The endpoint declaration
2843
- ships now (contract-first); the runtime returns
2844
- `feature_not_available` (422) until the Lambda lands. Per
2845
- Tension 1 (ADR-0001 §1.3).
2846
- operationId: decodeAudioWatermark
2847
- security: [{bearerAuth: []}, {sessionAuth: []}] # required (explicit 401 in response set; tier-gated runtime call)
3035
+ Redeems a verification token emailed at registration time, marking
3036
+ the account verified so it can authenticate. A failed or expired
3037
+ verification collapses into a non-validation `422` envelope
3038
+ (no `details[]`) distinct from the structured
3039
+ `ValidationErrorEnvelope` returned for malformed input.
3040
+ operationId: verifyEmail
3041
+ security: [] # anonymous (token-bearer — pre-authentication)
2848
3042
  tags:
2849
- - AudioWatermark
2850
- x-availability: planned
3043
+ - Auth
2851
3044
  requestBody:
2852
3045
  required: true
2853
3046
  content:
2854
3047
  application/json:
2855
3048
  schema:
2856
- $ref: '#/components/schemas/AudioWatermarkDecodeRequest'
3049
+ type: object
3050
+ required:
3051
+ - token
3052
+ properties:
3053
+ token:
3054
+ type: string
3055
+ minLength: 1
3056
+ maxLength: 128
3057
+ example:
3058
+ token: "8f3c1d9a4b2e6f0c1a7d5e9b3c2f4a6d"
2857
3059
  responses:
2858
3060
  '200':
2859
- description: Watermark successfully extracted.
3061
+ description: Email verified. The account can now authenticate.
2860
3062
  content:
2861
3063
  application/json:
2862
3064
  schema:
2863
- $ref: '#/components/schemas/AudioWatermarkDecodeResponse'
2864
- '401':
2865
- description: Authentication required (anonymous callers).
3065
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
3066
+ example:
3067
+ success: true
3068
+ data: {}
3069
+ '400':
3070
+ description: Request body is invalid (malformed JSON, wrong field types, or body too large).
2866
3071
  content:
2867
3072
  application/json:
2868
3073
  schema:
2869
3074
  $ref: '#/components/schemas/ErrorEnvelope'
2870
- '403':
3075
+ example:
3076
+ success: false
3077
+ error: "Request body is invalid."
3078
+ '422':
2871
3079
  description: |
2872
- Tier insufficient (free / pro caller) — returned as
2873
- `FeatureTierRestrictedResponse` with
2874
- `error_type: feature_tier_restricted`. Per ADR-0001 §1.3.
3080
+ Structured input validation failure (`ValidationErrorEnvelope`,
3081
+ `error_type: validation_error`).
3082
+
3083
+ A non-validation `422` (`error: UNPROCESSABLE_ENTITY`, no
3084
+ `details[]`) is returned when the verification link is invalid
3085
+ or expired.
2875
3086
  content:
2876
3087
  application/json:
2877
3088
  schema:
2878
3089
  oneOf:
2879
- - $ref: '#/components/schemas/TierRestrictionResponse'
2880
- - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
3090
+ - $ref: '#/components/schemas/ValidationErrorEnvelope'
3091
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
2881
3092
  discriminator:
2882
3093
  propertyName: error_type
2883
3094
  mapping:
2884
- tier_restriction: '#/components/schemas/TierRestrictionResponse'
2885
- feature_tier_restricted: '#/components/schemas/FeatureTierRestrictedResponse'
2886
- '404':
3095
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
3096
+ unprocessable_entity: '#/components/schemas/AuthRejectionEnvelope'
3097
+ example:
3098
+ success: false
3099
+ error_type: validation_error
3100
+ error: "Validation failed"
3101
+ details:
3102
+ - field: "token"
3103
+ message: "This value should not be blank."
3104
+ '429':
2887
3105
  description: |
2888
- No watermark detected in the supplied asset, OR the
2889
- detected watermark was not issued by this caller (the
2890
- scope-limit applies — own-watermarks-only).
3106
+ Rate limit exceeded (too many verification attempts in the
3107
+ current window). Carries a `Retry-After` response header.
3108
+ headers:
3109
+ Retry-After:
3110
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3111
+ schema:
3112
+ type: integer
3113
+ minimum: 0
3114
+ example: 60
2891
3115
  content:
2892
3116
  application/json:
2893
3117
  schema:
2894
3118
  $ref: '#/components/schemas/ErrorEnvelope'
2895
- '422':
2896
- description: |
2897
- Either generic validation error
2898
- (`ValidationErrorEnvelope`) or the operation is `planned`
2899
- and the runtime Lambda has not yet shipped
2900
- (`FeatureNotAvailableResponse` with `error_type:
2901
- feature_not_available`).
3119
+ '500':
3120
+ description: Internal server error.
2902
3121
  content:
2903
3122
  application/json:
2904
3123
  schema:
2905
- oneOf:
2906
- - $ref: '#/components/schemas/ValidationErrorEnvelope'
3124
+ $ref: '#/components/schemas/ErrorEnvelope'
3125
+
3126
+ /api/auth/forgot-password:
3127
+ post:
3128
+ summary: Request a password-reset email
3129
+ description: |
3130
+ Dispatches a password-reset email if the address maps to an account.
3131
+
3132
+ **Anti-enumeration: this endpoint ALWAYS returns `200`**, even when
3133
+ the email does not correspond to any account. Callers cannot use the
3134
+ response to discover whether an account exists. The only non-success
3135
+ responses are structured input validation (`422`) and rate-limiting
3136
+ (`429`).
3137
+ operationId: forgotPassword
3138
+ security: [] # anonymous (account-recovery entry point)
3139
+ tags:
3140
+ - Auth
3141
+ requestBody:
3142
+ required: true
3143
+ content:
3144
+ application/json:
3145
+ schema:
3146
+ type: object
3147
+ required:
3148
+ - email
3149
+ properties:
3150
+ email:
3151
+ type: string
3152
+ format: email
3153
+ example:
3154
+ email: "user@example.com"
3155
+ responses:
3156
+ '200':
3157
+ description: |
3158
+ Request accepted. A reset email is dispatched only if the address
3159
+ maps to an account; the response shape is identical either way
3160
+ (anti-enumeration).
3161
+ content:
3162
+ application/json:
3163
+ schema:
3164
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
3165
+ example:
3166
+ success: true
3167
+ data: {}
3168
+ '400':
3169
+ description: Request body is invalid (malformed JSON, wrong field types, or body too large).
3170
+ content:
3171
+ application/json:
3172
+ schema:
3173
+ $ref: '#/components/schemas/ErrorEnvelope'
3174
+ example:
3175
+ success: false
3176
+ error: "Request body is invalid."
3177
+ '422':
3178
+ description: Structured input validation failure (malformed `email`).
3179
+ content:
3180
+ application/json:
3181
+ schema:
3182
+ $ref: '#/components/schemas/ValidationErrorEnvelope'
3183
+ example:
3184
+ success: false
3185
+ error_type: validation_error
3186
+ error: "Validation failed"
3187
+ details:
3188
+ - field: "email"
3189
+ message: "This value is not a valid email address."
3190
+ '429':
3191
+ description: |
3192
+ Rate limit exceeded (too many reset requests in the current
3193
+ window). Carries a `Retry-After` response header.
3194
+ headers:
3195
+ Retry-After:
3196
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3197
+ schema:
3198
+ type: integer
3199
+ minimum: 0
3200
+ example: 60
3201
+ content:
3202
+ application/json:
3203
+ schema:
3204
+ $ref: '#/components/schemas/ErrorEnvelope'
3205
+
3206
+ /api/auth/reset-password:
3207
+ post:
3208
+ summary: Reset a password using a reset token
3209
+ description: |
3210
+ Sets a new password using a token issued by
3211
+ `POST /api/auth/forgot-password`. An invalid or expired token is
3212
+ rejected with a flat `400 BAD_REQUEST` envelope — distinct from the
3213
+ structured `422 ValidationErrorEnvelope` returned for malformed input.
3214
+ operationId: resetPassword
3215
+ security: [] # anonymous (token-bearer — pre-authentication)
3216
+ tags:
3217
+ - Auth
3218
+ requestBody:
3219
+ required: true
3220
+ content:
3221
+ application/json:
3222
+ schema:
3223
+ type: object
3224
+ required:
3225
+ - token
3226
+ - password
3227
+ properties:
3228
+ token:
3229
+ type: string
3230
+ minLength: 1
3231
+ password:
3232
+ type: string
3233
+ minLength: 8
3234
+ example:
3235
+ token: "8f3c1d9a4b2e6f0c1a7d5e9b3c2f4a6d"
3236
+ password: "news3cretpw"
3237
+ responses:
3238
+ '200':
3239
+ description: Password reset. The caller can now authenticate with the new password.
3240
+ content:
3241
+ application/json:
3242
+ schema:
3243
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
3244
+ example:
3245
+ success: true
3246
+ data: {}
3247
+ '400':
3248
+ description: |
3249
+ The reset token is invalid, expired, or already consumed
3250
+ (`error: BAD_REQUEST`). Distinct from the structured `422`
3251
+ returned for malformed input.
3252
+ content:
3253
+ application/json:
3254
+ schema:
3255
+ $ref: '#/components/schemas/ErrorEnvelope'
3256
+ example:
3257
+ success: false
3258
+ error: "BAD_REQUEST"
3259
+ '422':
3260
+ description: Structured input validation failure (e.g. blank `token`, short `password`).
3261
+ content:
3262
+ application/json:
3263
+ schema:
3264
+ $ref: '#/components/schemas/ValidationErrorEnvelope'
3265
+ example:
3266
+ success: false
3267
+ error_type: validation_error
3268
+ error: "Validation failed"
3269
+ details:
3270
+ - field: "password"
3271
+ message: "This value is too short."
3272
+ '429':
3273
+ description: |
3274
+ Rate limit exceeded (too many reset attempts in the current
3275
+ window). Carries a `Retry-After` response header.
3276
+ headers:
3277
+ Retry-After:
3278
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3279
+ schema:
3280
+ type: integer
3281
+ minimum: 0
3282
+ example: 60
3283
+ content:
3284
+ application/json:
3285
+ schema:
3286
+ $ref: '#/components/schemas/ErrorEnvelope'
3287
+
3288
+ /api/auth/change-password:
3289
+ post:
3290
+ summary: Change the authenticated user's password
3291
+ description: |
3292
+ Changes the caller's password after re-verifying the current one.
3293
+ Requires an authenticated principal (session cookie or bearer token).
3294
+
3295
+ A wrong `current_password` is rejected with a flat `400 BAD_REQUEST`
3296
+ envelope — distinct from the structured `422 ValidationErrorEnvelope`
3297
+ returned for malformed input. `details[].field` values use the wire
3298
+ keys `current_password` / `new_password`.
3299
+ operationId: changePassword
3300
+ security: [{bearerAuth: []}, {sessionAuth: []}] # required (authenticated principal)
3301
+ x-identity-scoped: true # identity-bound (caller's own credential)
3302
+ tags:
3303
+ - Auth
3304
+ requestBody:
3305
+ required: true
3306
+ content:
3307
+ application/json:
3308
+ schema:
3309
+ type: object
3310
+ required:
3311
+ - current_password
3312
+ - new_password
3313
+ properties:
3314
+ current_password:
3315
+ type: string
3316
+ minLength: 1
3317
+ new_password:
3318
+ type: string
3319
+ minLength: 8
3320
+ example:
3321
+ current_password: "olds3cretpw"
3322
+ new_password: "news3cretpw"
3323
+ responses:
3324
+ '200':
3325
+ description: Password changed.
3326
+ content:
3327
+ application/json:
3328
+ schema:
3329
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
3330
+ example:
3331
+ success: true
3332
+ data: {}
3333
+ '400':
3334
+ description: |
3335
+ The supplied `current_password` is incorrect
3336
+ (`error: BAD_REQUEST`). Distinct from the structured `422`
3337
+ returned for malformed input.
3338
+ content:
3339
+ application/json:
3340
+ schema:
3341
+ $ref: '#/components/schemas/ErrorEnvelope'
3342
+ example:
3343
+ success: false
3344
+ error: "BAD_REQUEST"
3345
+ '401':
3346
+ description: No authenticated principal.
3347
+ content:
3348
+ application/json:
3349
+ schema:
3350
+ $ref: '#/components/schemas/ErrorEnvelope'
3351
+ example:
3352
+ success: false
3353
+ error: "AUTHENTICATION_REQUIRED"
3354
+ error_type: "authentication_required"
3355
+ message: "Authentication required."
3356
+ '404':
3357
+ description: |
3358
+ The authenticated principal no longer resolves to a stored user
3359
+ (`error: USER_NOT_FOUND`).
3360
+ content:
3361
+ application/json:
3362
+ schema:
3363
+ $ref: '#/components/schemas/ErrorEnvelope'
3364
+ example:
3365
+ success: false
3366
+ error: "USER_NOT_FOUND"
3367
+ '422':
3368
+ description: Structured input validation failure (e.g. blank `current_password`, short `new_password`).
3369
+ content:
3370
+ application/json:
3371
+ schema:
3372
+ $ref: '#/components/schemas/ValidationErrorEnvelope'
3373
+ example:
3374
+ success: false
3375
+ error_type: validation_error
3376
+ error: "Validation failed"
3377
+ details:
3378
+ - field: "new_password"
3379
+ message: "This value is too short."
3380
+ '429':
3381
+ description: |
3382
+ Rate limit exceeded. Carries a `Retry-After` response header.
3383
+ headers:
3384
+ Retry-After:
3385
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3386
+ schema:
3387
+ type: integer
3388
+ minimum: 0
3389
+ example: 60
3390
+ content:
3391
+ application/json:
3392
+ schema:
3393
+ $ref: '#/components/schemas/ErrorEnvelope'
3394
+
3395
+ /api/auth/confirm-email-change:
3396
+ post:
3397
+ summary: Confirm a pending email-address change
3398
+ description: |
3399
+ Redeems a token issued when the caller requested an email change via
3400
+ `PATCH /api/auth/profile`, applying the new address to the account.
3401
+
3402
+ Failure modes carry their own `error_type` discriminators
3403
+ (`token_not_found`, etc.) on the flat `ErrorEnvelope` shape and are
3404
+ distinct from the structured `422 ValidationErrorEnvelope` returned
3405
+ for malformed input.
3406
+ operationId: confirmEmailChange
3407
+ security: [] # anonymous (token-bearer — confirmation link)
3408
+ tags:
3409
+ - Auth
3410
+ requestBody:
3411
+ required: true
3412
+ content:
3413
+ application/json:
3414
+ schema:
3415
+ type: object
3416
+ required:
3417
+ - token
3418
+ properties:
3419
+ token:
3420
+ type: string
3421
+ minLength: 1
3422
+ example:
3423
+ token: "8f3c1d9a4b2e6f0c1a7d5e9b3c2f4a6d"
3424
+ responses:
3425
+ '200':
3426
+ description: Email address changed.
3427
+ content:
3428
+ application/json:
3429
+ schema:
3430
+ type: object
3431
+ required:
3432
+ - success
3433
+ - data
3434
+ properties:
3435
+ success:
3436
+ type: boolean
3437
+ enum: [true]
3438
+ data:
3439
+ type: object
3440
+ required:
3441
+ - message
3442
+ properties:
3443
+ message:
3444
+ type: string
3445
+ example:
3446
+ success: true
3447
+ data:
3448
+ message: "Email address has been changed successfully."
3449
+ '400':
3450
+ description: "Malformed or otherwise rejected confirmation (`error: BAD_REQUEST`)."
3451
+ content:
3452
+ application/json:
3453
+ schema:
3454
+ $ref: '#/components/schemas/ErrorEnvelope'
3455
+ example:
3456
+ success: false
3457
+ error: "BAD_REQUEST"
3458
+ '404':
3459
+ description: |
3460
+ The confirmation token does not match any pending email change
3461
+ (`error: TOKEN_NOT_FOUND`, `error_type: token_not_found`).
3462
+ content:
3463
+ application/json:
3464
+ schema:
3465
+ $ref: '#/components/schemas/ErrorEnvelope'
3466
+ example:
3467
+ success: false
3468
+ error: "TOKEN_NOT_FOUND"
3469
+ error_type: "token_not_found"
3470
+ '409':
3471
+ description: |
3472
+ The target email address was claimed by another account before
3473
+ confirmation (`error: EMAIL_TAKEN`).
3474
+ content:
3475
+ application/json:
3476
+ schema:
3477
+ $ref: '#/components/schemas/ErrorEnvelope'
3478
+ example:
3479
+ success: false
3480
+ error: "EMAIL_TAKEN"
3481
+ '410':
3482
+ description: |
3483
+ The confirmation token has expired or was already used
3484
+ (`error: TOKEN_EXPIRED` or `TOKEN_USED`).
3485
+ content:
3486
+ application/json:
3487
+ schema:
3488
+ $ref: '#/components/schemas/ErrorEnvelope'
3489
+ example:
3490
+ success: false
3491
+ error: "TOKEN_EXPIRED"
3492
+ '422':
3493
+ description: Structured input validation failure (e.g. blank `token`).
3494
+ content:
3495
+ application/json:
3496
+ schema:
3497
+ $ref: '#/components/schemas/ValidationErrorEnvelope'
3498
+ example:
3499
+ success: false
3500
+ error_type: validation_error
3501
+ error: "Validation failed"
3502
+ details:
3503
+ - field: "token"
3504
+ message: "This value should not be blank."
3505
+ '500':
3506
+ description: Internal server error.
3507
+ content:
3508
+ application/json:
3509
+ schema:
3510
+ $ref: '#/components/schemas/ErrorEnvelope'
3511
+
3512
+ /api/auth/api-keys:
3513
+ post:
3514
+ summary: Create an API key for the authenticated user
3515
+ description: |
3516
+ Mints a new API key for the caller. The plaintext `key` is returned
3517
+ **once** in the creation response and never again — store it
3518
+ immediately.
3519
+
3520
+ **Session-auth only.** API-key management requires session
3521
+ authentication; callers authenticated via an API key (bearer) are
3522
+ rejected with `403`. This prevents an API key from minting further
3523
+ keys.
3524
+ operationId: createApiKey
3525
+ security: [{sessionAuth: []}] # session-only (API-key callers get 403)
3526
+ x-identity-scoped: true # identity-bound (caller's own keys)
3527
+ tags:
3528
+ - Auth
3529
+ requestBody:
3530
+ required: true
3531
+ content:
3532
+ application/json:
3533
+ schema:
3534
+ type: object
3535
+ required:
3536
+ - name
3537
+ properties:
3538
+ name:
3539
+ type: string
3540
+ minLength: 1
3541
+ maxLength: 50
3542
+ example:
3543
+ name: "ci-pipeline"
3544
+ responses:
3545
+ '201':
3546
+ description: |
3547
+ API key created. The plaintext `key` is shown here once and is
3548
+ never recoverable afterwards.
3549
+ content:
3550
+ application/json:
3551
+ schema:
3552
+ type: object
3553
+ required:
3554
+ - success
3555
+ - data
3556
+ properties:
3557
+ success:
3558
+ type: boolean
3559
+ enum: [true]
3560
+ data:
3561
+ type: object
3562
+ required:
3563
+ - id
3564
+ - name
3565
+ - prefix
3566
+ - created_at
3567
+ - key
3568
+ properties:
3569
+ id:
3570
+ $ref: '#/components/schemas/UuidV7'
3571
+ name:
3572
+ type: string
3573
+ prefix:
3574
+ type: string
3575
+ description: Short non-secret identifier prefix for the key.
3576
+ created_at:
3577
+ type: string
3578
+ format: date-time
3579
+ key:
3580
+ type: string
3581
+ description: Plaintext API key — shown once, never returned again.
3582
+ example:
3583
+ success: true
3584
+ data:
3585
+ id: "019539ab-1111-7000-8000-000000000010"
3586
+ name: "ci-pipeline"
3587
+ prefix: "gisl_ci"
3588
+ created_at: "2026-06-02T12:00:00Z"
3589
+ key: "gisl_ci_8f3c1d9a4b2e6f0c1a7d5e9b3c2f4a6d"
3590
+ '400':
3591
+ description: Request body is invalid (malformed JSON, wrong field types, or body too large).
3592
+ content:
3593
+ application/json:
3594
+ schema:
3595
+ $ref: '#/components/schemas/ErrorEnvelope'
3596
+ example:
3597
+ success: false
3598
+ error: "Request body is invalid."
3599
+ '401':
3600
+ description: No authenticated principal.
3601
+ content:
3602
+ application/json:
3603
+ schema:
3604
+ $ref: '#/components/schemas/ErrorEnvelope'
3605
+ example:
3606
+ success: false
3607
+ error: "AUTHENTICATION_REQUIRED"
3608
+ error_type: "authentication_required"
3609
+ message: "Authentication required."
3610
+ '403':
3611
+ description: |
3612
+ The caller authenticated via an API key (bearer); key management
3613
+ requires session authentication.
3614
+ content:
3615
+ application/json:
3616
+ schema:
3617
+ $ref: '#/components/schemas/ErrorEnvelope'
3618
+ example:
3619
+ success: false
3620
+ error: "FORBIDDEN"
3621
+ '422':
3622
+ description: |
3623
+ Discriminated `oneOf` on `error_type` (per
3624
+ [ADR-0019](../docs/decisions/0019-auth-422-discriminated-oneof.md)):
3625
+ - **`validation_error`** (`ValidationErrorEnvelope`, with
3626
+ `details[]`) — structured input validation failure (e.g.
3627
+ blank or over-long `name`).
3628
+ - **`unprocessable_entity`** (`AuthRejectionEnvelope`, flat,
3629
+ no `details[]`) — domain rejection: duplicate or otherwise
3630
+ invalid key name (`error: UNPROCESSABLE_ENTITY`).
3631
+ content:
3632
+ application/json:
3633
+ schema:
3634
+ oneOf:
3635
+ - $ref: '#/components/schemas/ValidationErrorEnvelope'
3636
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
3637
+ discriminator:
3638
+ propertyName: error_type
3639
+ mapping:
3640
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
3641
+ unprocessable_entity: '#/components/schemas/AuthRejectionEnvelope'
3642
+ example:
3643
+ success: false
3644
+ error_type: validation_error
3645
+ error: "Validation failed"
3646
+ details:
3647
+ - field: "name"
3648
+ message: "This value should not be blank."
3649
+ '429':
3650
+ description: |
3651
+ Rate limit exceeded. Carries a `Retry-After` response header.
3652
+ headers:
3653
+ Retry-After:
3654
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3655
+ schema:
3656
+ type: integer
3657
+ minimum: 0
3658
+ example: 60
3659
+ content:
3660
+ application/json:
3661
+ schema:
3662
+ $ref: '#/components/schemas/ErrorEnvelope'
3663
+ '500':
3664
+ description: Internal server error.
3665
+ content:
3666
+ application/json:
3667
+ schema:
3668
+ $ref: '#/components/schemas/ErrorEnvelope'
3669
+
3670
+ /api/auth/api-keys/{apiKeyId}:
3671
+ delete:
3672
+ summary: Revoke an API key for the authenticated user
3673
+ description: |
3674
+ Revokes (permanently deletes) one of the caller's API keys by id.
3675
+
3676
+ **Session-auth only.** Like key creation, revocation requires session
3677
+ authentication; callers authenticated via an API key (bearer) are
3678
+ rejected with `403`. This prevents an API key from revoking other
3679
+ keys.
3680
+
3681
+ **Identity-scoped.** The revoke is bound to the caller's own keys, so
3682
+ a key id that belongs to another account is indistinguishable from a
3683
+ non-existent one — both return `404`.
3684
+ operationId: revokeApiKey
3685
+ security: [{sessionAuth: []}] # session-only (API-key callers get 403)
3686
+ x-identity-scoped: true # identity-bound (caller's own keys)
3687
+ tags:
3688
+ - Auth
3689
+ parameters:
3690
+ - name: apiKeyId
3691
+ in: path
3692
+ required: true
3693
+ description: Identifier of the API key to revoke (from the creation response `id`).
3694
+ schema:
3695
+ $ref: '#/components/schemas/UuidV7'
3696
+ responses:
3697
+ '200':
3698
+ description: |
3699
+ API key revoked. The body is the shared empty-success envelope
3700
+ (`data` is an empty object, never `null`).
3701
+ content:
3702
+ application/json:
3703
+ schema:
3704
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
3705
+ example:
3706
+ success: true
3707
+ data: {}
3708
+ '400':
3709
+ description: The `apiKeyId` path segment is not a well-formed identifier.
3710
+ content:
3711
+ application/json:
3712
+ schema:
3713
+ $ref: '#/components/schemas/ErrorEnvelope'
3714
+ example:
3715
+ success: false
3716
+ error: "INVALID_ID"
3717
+ '401':
3718
+ description: No authenticated principal.
3719
+ content:
3720
+ application/json:
3721
+ schema:
3722
+ $ref: '#/components/schemas/ErrorEnvelope'
3723
+ example:
3724
+ success: false
3725
+ error: "AUTHENTICATION_REQUIRED"
3726
+ error_type: "authentication_required"
3727
+ message: "Authentication required."
3728
+ '403':
3729
+ description: |
3730
+ The caller authenticated via an API key (bearer); key management
3731
+ requires session authentication.
3732
+ content:
3733
+ application/json:
3734
+ schema:
3735
+ $ref: '#/components/schemas/ErrorEnvelope'
3736
+ example:
3737
+ success: false
3738
+ error: "FORBIDDEN"
3739
+ '404':
3740
+ description: |
3741
+ No API key with this id belongs to the caller — the key does not
3742
+ exist, or it belongs to another account (identity-scoped, so the
3743
+ two cases are indistinguishable).
3744
+ content:
3745
+ application/json:
3746
+ schema:
3747
+ $ref: '#/components/schemas/ErrorEnvelope'
3748
+ example:
3749
+ success: false
3750
+ error: "API_KEY_NOT_FOUND"
3751
+ '500':
3752
+ description: Internal server error.
3753
+ content:
3754
+ application/json:
3755
+ schema:
3756
+ $ref: '#/components/schemas/ErrorEnvelope'
3757
+
3758
+ /api/auth/profile:
3759
+ patch:
3760
+ summary: Update the authenticated user's profile
3761
+ description: |
3762
+ Updates the caller's display name and/or email after re-verifying
3763
+ the current password. **At least one of `name` or `email` must be
3764
+ provided**, otherwise the request is rejected with `400`.
3765
+
3766
+ Changing the email does not apply it immediately — it triggers an
3767
+ email-change confirmation flow redeemed at
3768
+ `POST /api/auth/confirm-email-change`; the response signals this via
3769
+ `email_change_requested`.
3770
+
3771
+ Note the two distinct `422` shapes: structured input validation
3772
+ uses `ValidationErrorEnvelope`, whereas the `EMAIL_SAME` business
3773
+ rejection (new email equals current) is a flat `ErrorEnvelope` that
3774
+ is also a `422` but **not** the validation envelope.
3775
+ `details[].field` values use the wire keys `current_password` /
3776
+ `name` / `email`.
3777
+ operationId: updateProfile
3778
+ security: [{bearerAuth: []}, {sessionAuth: []}] # required (authenticated principal)
3779
+ x-identity-scoped: true # identity-bound (caller's own profile)
3780
+ tags:
3781
+ - Auth
3782
+ requestBody:
3783
+ required: true
3784
+ content:
3785
+ application/json:
3786
+ schema:
3787
+ type: object
3788
+ required:
3789
+ - current_password
3790
+ properties:
3791
+ current_password:
3792
+ type: string
3793
+ minLength: 1
3794
+ name:
3795
+ type:
3796
+ - string
3797
+ - "null"
3798
+ maxLength: 255
3799
+ email:
3800
+ type:
3801
+ - string
3802
+ - "null"
3803
+ format: email
3804
+ maxLength: 254
3805
+ example:
3806
+ current_password: "s3cretpw"
3807
+ name: "Jane Doe"
3808
+ email: "jane.new@example.com"
3809
+ responses:
3810
+ '200':
3811
+ description: |
3812
+ Profile updated. `name_changed` and `email_change_requested`
3813
+ flag which mutations took effect; an email change is pending
3814
+ confirmation rather than applied.
3815
+ content:
3816
+ application/json:
3817
+ schema:
3818
+ type: object
3819
+ required:
3820
+ - success
3821
+ - data
3822
+ properties:
3823
+ success:
3824
+ type: boolean
3825
+ enum: [true]
3826
+ data:
3827
+ type: object
3828
+ required:
3829
+ - updated
3830
+ properties:
3831
+ updated:
3832
+ type: boolean
3833
+ enum: [true]
3834
+ name_changed:
3835
+ type: boolean
3836
+ enum: [true]
3837
+ email_change_requested:
3838
+ type: boolean
3839
+ enum: [true]
3840
+ message:
3841
+ type: string
3842
+ example:
3843
+ success: true
3844
+ data:
3845
+ updated: true
3846
+ name_changed: true
3847
+ email_change_requested: true
3848
+ message: "A confirmation email has been sent to your new address."
3849
+ '400':
3850
+ description: |
3851
+ No mutable field was provided — at least one of `name` or `email`
3852
+ is required (`error: BAD_REQUEST`).
3853
+ content:
3854
+ application/json:
3855
+ schema:
3856
+ $ref: '#/components/schemas/ErrorEnvelope'
3857
+ example:
3858
+ success: false
3859
+ error: "BAD_REQUEST"
3860
+ '401':
3861
+ description: No authenticated principal.
3862
+ content:
3863
+ application/json:
3864
+ schema:
3865
+ $ref: '#/components/schemas/ErrorEnvelope'
3866
+ example:
3867
+ success: false
3868
+ error: "AUTHENTICATION_REQUIRED"
3869
+ error_type: "authentication_required"
3870
+ message: "Authentication required."
3871
+ '403':
3872
+ description: |
3873
+ The supplied `current_password` is incorrect
3874
+ (`error: INVALID_PASSWORD`).
3875
+ content:
3876
+ application/json:
3877
+ schema:
3878
+ $ref: '#/components/schemas/ErrorEnvelope'
3879
+ example:
3880
+ success: false
3881
+ error: "INVALID_PASSWORD"
3882
+ '404':
3883
+ description: |
3884
+ The authenticated principal no longer resolves to a stored user
3885
+ (`error: USER_NOT_FOUND`).
3886
+ content:
3887
+ application/json:
3888
+ schema:
3889
+ $ref: '#/components/schemas/ErrorEnvelope'
3890
+ example:
3891
+ success: false
3892
+ error: "USER_NOT_FOUND"
3893
+ '409':
3894
+ description: |
3895
+ The requested email is already claimed by another account
3896
+ (`error: EMAIL_TAKEN`).
3897
+ content:
3898
+ application/json:
3899
+ schema:
3900
+ $ref: '#/components/schemas/ErrorEnvelope'
3901
+ example:
3902
+ success: false
3903
+ error: "EMAIL_TAKEN"
3904
+ '422':
3905
+ description: |
3906
+ Structured input validation failure (`ValidationErrorEnvelope`,
3907
+ `error_type: validation_error`).
3908
+
3909
+ A non-validation `422` (`error: EMAIL_SAME`, `error_type:
3910
+ email_same`, no `details[]`) is returned when the new email
3911
+ equals the current one — a flat `ErrorEnvelope` that is also
3912
+ `422` but **not** the validation envelope.
3913
+ content:
3914
+ application/json:
3915
+ schema:
3916
+ oneOf:
3917
+ - $ref: '#/components/schemas/ValidationErrorEnvelope'
3918
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
3919
+ discriminator:
3920
+ propertyName: error_type
3921
+ mapping:
3922
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
3923
+ email_same: '#/components/schemas/AuthRejectionEnvelope'
3924
+ example:
3925
+ success: false
3926
+ error_type: validation_error
3927
+ error: "Validation failed"
3928
+ details:
3929
+ - field: "email"
3930
+ message: "This value is not a valid email address."
3931
+ '429':
3932
+ description: |
3933
+ Rate limit exceeded. Carries a `Retry-After` response header.
3934
+ headers:
3935
+ Retry-After:
3936
+ description: Seconds to wait before retrying. Delta-seconds (RFC 9110).
3937
+ schema:
3938
+ type: integer
3939
+ minimum: 0
3940
+ example: 60
3941
+ content:
3942
+ application/json:
3943
+ schema:
3944
+ $ref: '#/components/schemas/ErrorEnvelope'
3945
+ '500':
3946
+ description: Internal server error.
3947
+ content:
3948
+ application/json:
3949
+ schema:
3950
+ $ref: '#/components/schemas/ErrorEnvelope'
3951
+
3952
+ # ============================================
3953
+ # EXTERNAL IMPORT ENDPOINT (ticket I10)
3954
+ # ============================================
3955
+
3956
+ /api/external-imports:
3957
+ post:
3958
+ summary: Register a one-shot external import URL
3959
+ description: |
3960
+ Register a one-shot bearer URL (S3 presigned / GCS signed /
3961
+ Azure SAS / Dropbox shared link / public HTTPS) and receive an
3962
+ opaque `external_source_id` handle. Subsequent workflows
3963
+ reference the handle via `WorkflowSource` of `type:
3964
+ external_import`; the original URL + password are encrypted
3965
+ server-side and never returned in any response.
3966
+
3967
+ Per [ADR-0005](../docs/decisions/0005-external-sources.md) — see
3968
+ §"SSRF posture" for the 8 validation rules applied at
3969
+ registration time AND again at fetch time.
3970
+
3971
+ **Availability:** `planned` — runtime depends on cross-repo
3972
+ ticket [`MbosYtJD`](https://trello.com/c/MbosYtJD) (Lambda team
3973
+ publishes capability manifest) and the
3974
+ [`POST /api/connections`](https://trello.com/c/MbosYtJD) vault
3975
+ endpoint shipping. The contract surface ships now (contract-first
3976
+ per ADR-0001 §1.3); the runtime endpoint returns `404` until
3977
+ cross-repo wiring lands.
3978
+ operationId: createExternalImport
3979
+ # Auth: REQUIRED defensively. Endpoint is availability:planned (no
3980
+ # controller yet); but the endpoint stores encrypted server-side
3981
+ # secrets (bearer URLs, passwords) and anon-callable secret
3982
+ # storage is almost certainly wrong. Locking the contract here
3983
+ # while we still own the source of truth; runtime tightens to
3984
+ # match when the controller lands. Per ADR-0016 / karen review
3985
+ # on yN309QVb-B2.
3986
+ security: [{bearerAuth: []}, {sessionAuth: []}]
3987
+ x-availability: planned
3988
+ tags:
3989
+ - Upload
3990
+ requestBody:
3991
+ required: true
3992
+ content:
3993
+ application/json:
3994
+ schema:
3995
+ $ref: '#/components/schemas/ExternalImportRequest'
3996
+ responses:
3997
+ '201':
3998
+ description: External-import handle created.
3999
+ content:
4000
+ application/json:
4001
+ schema:
4002
+ $ref: '#/components/schemas/ExternalImportCreatedSuccessEnvelope'
4003
+ example:
4004
+ success: true
4005
+ data:
4006
+ external_source_id: "019539ab-2222-7000-8000-000000000001"
4007
+ expires_at: "2026-04-26T15:00:00Z"
4008
+ provider: "s3_presigned"
4009
+ '400':
4010
+ description: Validation failed (missing url, malformed URI, scheme not https).
4011
+ content:
4012
+ application/json:
4013
+ schema:
4014
+ $ref: '#/components/schemas/ErrorEnvelope'
4015
+ example:
4016
+ success: false
4017
+ error: "URL is required and must be https://"
4018
+ '403':
4019
+ description: |
4020
+ Forbidden — SSRF policy violation (private IP, loopback,
4021
+ cloud metadata endpoint), tier-quota restriction, or
4022
+ provider not enabled for caller's tier.
4023
+ content:
4024
+ application/json:
4025
+ schema:
4026
+ oneOf:
4027
+ - $ref: '#/components/schemas/TierRestrictionResponse'
4028
+ - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
4029
+ - $ref: '#/components/schemas/ErrorEnvelope'
4030
+ '422':
4031
+ description: |
4032
+ Unprocessable URL — DNS resolution failed, target
4033
+ unreachable, content-type / magic-byte sniff failed,
4034
+ `expires_at` already past, or `feature_not_available`
4035
+ (provider tagged `planned` for caller's tier).
4036
+ content:
4037
+ application/json:
4038
+ schema:
4039
+ oneOf:
4040
+ - $ref: '#/components/schemas/ValidationErrorEnvelope'
4041
+ - $ref: '#/components/schemas/FeatureNotAvailableResponse'
4042
+ discriminator:
4043
+ propertyName: error_type
4044
+ mapping:
4045
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
4046
+ feature_not_available: '#/components/schemas/FeatureNotAvailableResponse'
4047
+ '429':
4048
+ description: Rate limit exceeded
4049
+ content:
4050
+ application/json:
4051
+ schema:
4052
+ $ref: '#/components/schemas/ErrorEnvelope'
4053
+ '500':
4054
+ description: Internal server error
4055
+ content:
4056
+ application/json:
4057
+ schema:
4058
+ $ref: '#/components/schemas/ErrorEnvelope'
4059
+
4060
+ # ============================================
4061
+ # AUDIO WATERMARK DECODE
4062
+ # ============================================
4063
+
4064
+ /api/audio-watermark/decode:
4065
+ post:
4066
+ summary: Decode an embedded audio watermark
4067
+ description: |
4068
+ Extract the steganographic watermark embedded by a prior
4069
+ `audio_watermark` operation (per ticket
4070
+ [I20](https://trello.com/c/omiCq7Vn)). Pairs with the
4071
+ `audio_watermark` operation declared in
4072
+ `schemas/operations/audio_watermark.yaml` — the operation
4073
+ embeds; this endpoint decodes.
4074
+
4075
+ **Tier-restricted.** This endpoint is `enterprise`-only. Free
4076
+ and `pro` callers receive a 403 `feature_tier_restricted`.
4077
+ Anonymous callers receive a 401.
4078
+
4079
+ **Scope: own watermarks only.** The decoder will refuse to
4080
+ extract from media the caller did not mark themselves
4081
+ (server-side audit log of watermark origin per
4082
+ `watermark_id`). Per spike S11 acceptance — this avoids the
4083
+ privacy implications of arbitrary-media decode while still
4084
+ serving the forensic-tracking use case (find a leaked asset
4085
+ on a third-party platform, decode the watermark, look up the
4086
+ distribution channel that received the marked copy).
4087
+
4088
+ **Rate-limited.** Request rate per caller is enforced
4089
+ independently from the workflow-create rate limit; abusive
4090
+ decode patterns flag for audit per [ADR-0001](../docs/decisions/0001-contract-first-availability.md).
4091
+
4092
+ **`availability: planned`** — operation Lambda + decode
4093
+ Lambda are cross-repo follow-up. The endpoint declaration
4094
+ ships now (contract-first); the runtime returns
4095
+ `feature_not_available` (422) until the Lambda lands. Per
4096
+ Tension 1 (ADR-0001 §1.3).
4097
+ operationId: decodeAudioWatermark
4098
+ security: [{bearerAuth: []}, {sessionAuth: []}] # required (explicit 401 in response set; tier-gated runtime call)
4099
+ tags:
4100
+ - AudioWatermark
4101
+ x-availability: planned
4102
+ requestBody:
4103
+ required: true
4104
+ content:
4105
+ application/json:
4106
+ schema:
4107
+ $ref: '#/components/schemas/AudioWatermarkDecodeRequest'
4108
+ responses:
4109
+ '200':
4110
+ description: Watermark successfully extracted.
4111
+ content:
4112
+ application/json:
4113
+ schema:
4114
+ $ref: '#/components/schemas/AudioWatermarkDecodeResponse'
4115
+ '401':
4116
+ description: Authentication required (anonymous callers).
4117
+ content:
4118
+ application/json:
4119
+ schema:
4120
+ $ref: '#/components/schemas/ErrorEnvelope'
4121
+ '403':
4122
+ description: |
4123
+ Tier insufficient (free / pro caller) — returned as
4124
+ `FeatureTierRestrictedResponse` with
4125
+ `error_type: feature_tier_restricted`. Per ADR-0001 §1.3.
4126
+ content:
4127
+ application/json:
4128
+ schema:
4129
+ oneOf:
4130
+ - $ref: '#/components/schemas/TierRestrictionResponse'
4131
+ - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
4132
+ discriminator:
4133
+ propertyName: error_type
4134
+ mapping:
4135
+ tier_restriction: '#/components/schemas/TierRestrictionResponse'
4136
+ feature_tier_restricted: '#/components/schemas/FeatureTierRestrictedResponse'
4137
+ '404':
4138
+ description: |
4139
+ No watermark detected in the supplied asset, OR the
4140
+ detected watermark was not issued by this caller (the
4141
+ scope-limit applies — own-watermarks-only).
4142
+ content:
4143
+ application/json:
4144
+ schema:
4145
+ $ref: '#/components/schemas/ErrorEnvelope'
4146
+ '422':
4147
+ description: |
4148
+ Either generic validation error
4149
+ (`ValidationErrorEnvelope`) or the operation is `planned`
4150
+ and the runtime Lambda has not yet shipped
4151
+ (`FeatureNotAvailableResponse` with `error_type:
4152
+ feature_not_available`).
4153
+ content:
4154
+ application/json:
4155
+ schema:
4156
+ oneOf:
4157
+ - $ref: '#/components/schemas/ValidationErrorEnvelope'
2907
4158
  - $ref: '#/components/schemas/FeatureNotAvailableResponse'
4159
+ discriminator:
4160
+ propertyName: error_type
4161
+ mapping:
4162
+ validation_error: '#/components/schemas/ValidationErrorEnvelope'
4163
+ feature_not_available: '#/components/schemas/FeatureNotAvailableResponse'
2908
4164
  '429':
2909
4165
  description: |
2910
4166
  Rate limit exceeded for decode requests. Decode
@@ -3191,6 +4447,7 @@ paths:
3191
4447
  $ref: '#/components/schemas/ValidationErrorEnvelope'
3192
4448
  example:
3193
4449
  success: false
4450
+ error_type: "validation_error"
3194
4451
  error: "VALIDATION_FAILED"
3195
4452
  details:
3196
4453
  - field: "limit"
@@ -3373,6 +4630,37 @@ components:
3373
4630
  success:
3374
4631
  type: boolean
3375
4632
 
4633
+ EmptyData:
4634
+ type: object
4635
+ additionalProperties: false
4636
+ description: |
4637
+ No-payload success `data` slot — always an empty object `{}`,
4638
+ never `null`. Preserves the `{ success: true, data: {...} }`
4639
+ envelope invariant for operations that return no body (auth
4640
+ side-effect endpoints: register, logout, verify-email,
4641
+ forgot-password, reset-password, change-password). Modelled as
4642
+ an explicit empty object (not `type: null`) so every SDK
4643
+ generator emits a clean type — a `null`-typed `data` makes
4644
+ typescript-fetch import a non-existent `Null` model (TS2307)
4645
+ and crashes the python openapi-generator (`getPydanticType`).
4646
+ See ticket [VGqxO3fO](https://trello.com/c/VGqxO3fO).
4647
+
4648
+ EmptySuccessEnvelope:
4649
+ type: object
4650
+ description: |
4651
+ Success envelope for no-payload operations. `data` is always
4652
+ the empty object `{}`. Shared by the auth side-effect endpoints
4653
+ so their codegen stays clean and uniform.
4654
+ required:
4655
+ - success
4656
+ - data
4657
+ properties:
4658
+ success:
4659
+ type: boolean
4660
+ enum: [true]
4661
+ data:
4662
+ $ref: '#/components/schemas/EmptyData'
4663
+
3376
4664
  ErrorEnvelope:
3377
4665
  type: object
3378
4666
  description: |
@@ -3429,6 +4717,17 @@ components:
3429
4717
  would exceed the S3 multipart capacity cap. Pre-S3
3430
4718
  server-side capacity gate; distinct from tier-quota
3431
4719
  rejections (`upload_size_exceeds_tier`).
4720
+
4721
+ Workflow-create code (per ticket
4722
+ [`nGYbgChX`](https://trello.com/c/nGYbgChX) / sdks
4723
+ [`DRjIyMt9`](https://trello.com/c/DRjIyMt9)):
4724
+ - `UPLOAD_NOT_FOUND` (404) — a `POST /api/workflows` request
4725
+ references an upload that does not exist OR exists but is
4726
+ owned by a different identity (deliberate BOLA/IDOR
4727
+ existence-mask: reported as not-found, **never 403**, so the
4728
+ response does not reveal another user's upload exists).
4729
+ `message_key: "upload.not_found"`. See the createWorkflow
4730
+ 404 response + ADR-0016 Amendment.
3432
4731
  message:
3433
4732
  type: string
3434
4733
  description: |
@@ -3489,11 +4788,30 @@ components:
3489
4788
  required:
3490
4789
  - success
3491
4790
  - error
4791
+ - error_type
3492
4792
  - details
3493
4793
  properties:
3494
4794
  success:
3495
4795
  type: boolean
3496
4796
  enum: [false]
4797
+ error_type:
4798
+ type: string
4799
+ enum:
4800
+ - validation_error
4801
+ description: |
4802
+ Discriminator for the multi-branch 422 `oneOf` (per
4803
+ [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md)).
4804
+ Always `validation_error` on this envelope. Added so the four
4805
+ 422 branches (`ValidationErrorEnvelope`,
4806
+ `FeatureNotAvailableResponse`,
4807
+ `ProcessingClassExceedsBandResponse`, `ProbePendingResponse`)
4808
+ share a single discriminator property — generated SDK clients
4809
+ dispatch on `error_type` instead of structural `instanceOf`
4810
+ guards (which mis-fire on camelCase-vs-snake_case property
4811
+ names). Distinct from the `error` machine code below: `error`
4812
+ carries the specific failure code (`INVALID_OPTIONS`,
4813
+ `REQUIRES_REENCODE`, `CYCLIC_WORKFLOW_EDGES`, …) while
4814
+ `error_type` only names the envelope shape.
3497
4815
  error:
3498
4816
  type: string
3499
4817
  description: |
@@ -3502,9 +4820,10 @@ components:
3502
4820
  (per ticket I16-CONS — `merge.video` with
3503
4821
  `re_encode_mode: never` and incompatible inputs; caller
3504
4822
  resolves by switching to `re_encode_mode: auto` or `always`),
3505
- `VALIDATION_FAILED` (request/query-param validation failure
3506
- per the project-wide convention — e.g. `GET /api/v2/credits/usage`
3507
- invalid `limit`/`offset`).
4823
+ `CYCLIC_WORKFLOW_EDGES` (cyclic/self explicit `workflow_edges`,
4824
+ per `g8PPkbNu`), `VALIDATION_FAILED` (request/query-param
4825
+ validation failure per the project-wide convention — e.g.
4826
+ `GET /api/v2/credits/usage` invalid `limit`/`offset`).
3508
4827
  SDKs duck-type on this field for typed error-branch helpers.
3509
4828
  message:
3510
4829
  type: string
@@ -3575,6 +4894,82 @@ components:
3575
4894
  `ErrorEnvelope.message_params`. Excludes cost
3576
4895
  numbers.
3577
4896
 
4897
+ AuthRejectionEnvelope:
4898
+ type: object
4899
+ description: |
4900
+ Flat **domain-rejection** 422 envelope for the auth surface — the
4901
+ non-validation branch of the auth-422 `oneOf` (per
4902
+ [ADR-0019](../docs/decisions/0019-auth-422-discriminated-oneof.md),
4903
+ the sibling adoption of [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md)
4904
+ anticipated for non-workflow `oneOf` sites). Distinct from
4905
+ `ValidationErrorEnvelope`: it has **no `details[]`** (the
4906
+ rejection is a single business-rule failure, not a field-by-field
4907
+ validation report). Carries the same I26 localisation triple as
4908
+ `ErrorEnvelope`.
4909
+
4910
+ Returned alongside `ValidationErrorEnvelope` on the four auth
4911
+ endpoints that have a domain-reject 422 branch: `register`
4912
+ (disposable/blocklisted email), `verify-email` (invalid/expired
4913
+ token), `api-keys` POST (duplicate/invalid key name) — all
4914
+ `error: UNPROCESSABLE_ENTITY`, `error_type: unprocessable_entity`
4915
+ — and `profile` PATCH (`error: EMAIL_SAME`, `error_type:
4916
+ email_same`, new email equals current).
4917
+
4918
+ **`error_type` vs `error`** (per ADR-0018): `error_type` is the
4919
+ `oneOf` branch discriminator; the specific failure stays in the
4920
+ `error` machine code. `email_same` is retained as its own
4921
+ discriminator value (pre-existing wire shape) rather than folded
4922
+ into `unprocessable_entity`; both map to this envelope.
4923
+ required:
4924
+ - success
4925
+ - error
4926
+ - error_type
4927
+ properties:
4928
+ success:
4929
+ type: boolean
4930
+ enum: [false]
4931
+ error:
4932
+ type: string
4933
+ description: |
4934
+ Stable machine-readable failure code. `UNPROCESSABLE_ENTITY`
4935
+ for the generic auth domain rejections (register /
4936
+ verify-email / api-keys); `EMAIL_SAME` for the profile
4937
+ new-email-equals-current rejection. Canonical English; never
4938
+ localised. See `ErrorEnvelope.error`.
4939
+ error_type:
4940
+ type: string
4941
+ enum:
4942
+ - unprocessable_entity
4943
+ - email_same
4944
+ description: |
4945
+ Discriminator for the auth-422 `oneOf` (per ADR-0019). Names
4946
+ the envelope **shape**, not the failure — the failure is in
4947
+ `error`. `unprocessable_entity` for the generic flat auth
4948
+ rejections; `email_same` preserved as the profile-specific
4949
+ pre-existing wire value. Both resolve to this
4950
+ `AuthRejectionEnvelope`. Distinct from
4951
+ `ValidationErrorEnvelope.error_type` (`validation_error`),
4952
+ the other branch of the auth-422 `oneOf`.
4953
+ message:
4954
+ type: string
4955
+ description: |
4956
+ Human-readable message, localised per `Accept-Language`
4957
+ (fallback `en-GB`). Never parse for control flow. See
4958
+ `ErrorEnvelope.message`.
4959
+ message_key:
4960
+ type: string
4961
+ description: |
4962
+ Stable canonical lookup key for the message. Never localised.
4963
+ See `ErrorEnvelope.message_key`.
4964
+ locale:
4965
+ type: string
4966
+ description: BCP 47 locale tag echoing `Content-Language`. See `ErrorEnvelope.locale`.
4967
+ example: "en-GB"
4968
+ message_params:
4969
+ type: object
4970
+ additionalProperties: true
4971
+ description: Optional interpolation values for the localised `message`. See `ErrorEnvelope.message_params`.
4972
+
3578
4973
  ReEncodeDecision:
3579
4974
  type: string
3580
4975
  description: |
@@ -3733,11 +5128,13 @@ components:
3733
5128
  — this is the load-bearing invariant that makes the
3734
5129
  primitive useful.
3735
5130
 
3736
- Absence of `per_role_cardinality` on a role-based op keeps
3737
- the current prose-only semantics (image_watermark /
3738
- audio_overlay / custom_luma encode their role rules in
3739
- prose at `JobInputRole`'s description today; migration to
3740
- predicate form is per-op follow-up work).
5131
+ All five role-based ops now declare `per_role_cardinality`
5132
+ (audio_to_video, video_watermark, audio_overlay,
5133
+ image_watermark, custom_luma the last three migrated from
5134
+ prose-only by ticket oAP5BQOx). Absence of the block keeps
5135
+ prose-only semantics for any future role-based op until it
5136
+ migrates to the predicate form; the `JobInputRole` description
5137
+ remains the human-readable companion to the rules.
3741
5138
 
3742
5139
  Per ticket [`SlluxMBN`](https://trello.com/c/SlluxMBN) /
3743
5140
  ADR-0015. Consumed by `OperationInputRoleValidator` in
@@ -3776,8 +5173,10 @@ components:
3776
5173
  # ============================================
3777
5174
  #
3778
5175
  # Per ADR-0005. WorkflowSource is the discriminated union consumed
3779
- # by V2 JobDefinition.source / JobInputV2.source (wired by I12).
3780
- # ExternalDestination is the write-side counterpart.
5176
+ # by V2 single-input JobDefinition.source (wired by I12). Multi-input
5177
+ # JobInputV2.source consumes the narrower MultiInputSource (excludes
5178
+ # the upload leaf; see MultiInputSource). ExternalDestination is the
5179
+ # write-side counterpart.
3781
5180
 
3782
5181
  UploadSource:
3783
5182
  type: object
@@ -3869,8 +5268,11 @@ components:
3869
5268
 
3870
5269
  WorkflowSource:
3871
5270
  description: |
3872
- Discriminated union of source leaves consumed by V2
3873
- `JobDefinition.source` and `JobInputV2.source`. Per
5271
+ Discriminated union of source leaves (all 4, including
5272
+ `upload`) consumed by V2 single-input `JobDefinition.source`.
5273
+ Multi-input `JobInputV2.source` consumes the narrower
5274
+ `MultiInputSource` (this union minus the `upload` leaf) — see
5275
+ that schema and `JobInputV2`. Per
3874
5276
  [ADR-0005](../docs/decisions/0005-external-sources.md) +
3875
5277
  [ADR-0004](../docs/decisions/0004-job-shape.md).
3876
5278
 
@@ -3889,6 +5291,30 @@ components:
3889
5291
  external_import: '#/components/schemas/ExternalImportToken'
3890
5292
  connection: '#/components/schemas/ConnectionSource'
3891
5293
 
5294
+ MultiInputSource:
5295
+ description: |
5296
+ Discriminated union of source leaves consumed by `JobInputV2.source`
5297
+ (multi-input operation inputs). A 3-leaf subset of `WorkflowSource`
5298
+ that **excludes the `upload` leaf** — uploads cannot be referenced
5299
+ directly inside a multi-input `inputs[]` array. An uploaded file
5300
+ that must feed a multi-input operation enters via a `passthrough`
5301
+ source job (a single-input job with `operations: [{type: passthrough}]`)
5302
+ and is referenced here as `{ type: job_output, from: <id> }`, which
5303
+ preserves billing / DAG / lineage. Structured like the `ExternalSource`
5304
+ subset alias but retaining `job_output` (the passthrough bridge).
5305
+ Per ticket [`4som89Uh`](https://trello.com/c/4som89Uh) + ADR-0004 /
5306
+ ADR-0005.
5307
+ oneOf:
5308
+ - $ref: '#/components/schemas/JobOutputSource'
5309
+ - $ref: '#/components/schemas/ExternalImportToken'
5310
+ - $ref: '#/components/schemas/ConnectionSource'
5311
+ discriminator:
5312
+ propertyName: type
5313
+ mapping:
5314
+ job_output: '#/components/schemas/JobOutputSource'
5315
+ external_import: '#/components/schemas/ExternalImportToken'
5316
+ connection: '#/components/schemas/ConnectionSource'
5317
+
3892
5318
  ExternalSource:
3893
5319
  description: |
3894
5320
  Subset alias of `WorkflowSource` that excludes uploads and
@@ -4208,6 +5634,7 @@ components:
4208
5634
 
4209
5635
  CreditsBalanceSuccessEnvelope:
4210
5636
  type: object
5637
+ additionalProperties: false
4211
5638
  required:
4212
5639
  - success
4213
5640
  - data
@@ -4378,6 +5805,7 @@ components:
4378
5805
 
4379
5806
  CreditsUsageSuccessEnvelope:
4380
5807
  type: object
5808
+ additionalProperties: false
4381
5809
  required:
4382
5810
  - success
4383
5811
  - data
@@ -4535,6 +5963,7 @@ components:
4535
5963
 
4536
5964
  WorkflowCancelSuccessEnvelope:
4537
5965
  type: object
5966
+ additionalProperties: false
4538
5967
  required:
4539
5968
  - success
4540
5969
  - data
@@ -4570,6 +5999,7 @@ components:
4570
5999
 
4571
6000
  WorkflowResumeSuccessEnvelope:
4572
6001
  type: object
6002
+ additionalProperties: false
4573
6003
  required:
4574
6004
  - success
4575
6005
  - data
@@ -4988,13 +6418,12 @@ components:
4988
6418
  with a generic schema error. This envelope is the structured
4989
6419
  rejection.
4990
6420
 
4991
- The 422 response is delivered alongside `ValidationErrorEnvelope`
4992
- and `ProcessingClassExceedsBandResponse` via a naked `oneOf`
4993
- (duck-typed on required-field shape `details` vs `error_type`
4994
- value: `feature_not_available` vs `processing_class_exceeds_band`).
4995
- No discriminator object is used because `ValidationErrorEnvelope`
4996
- has no `error_type` field. See the 422 response on
4997
- `POST /api/workflows`.
6421
+ The 422 response is delivered alongside `ValidationErrorEnvelope`,
6422
+ `ProcessingClassExceedsBandResponse`, and `ProbePendingResponse`
6423
+ via a `oneOf` with an explicit `error_type` discriminator (per
6424
+ [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md));
6425
+ this branch's discriminator value is `feature_not_available`. See
6426
+ the 422 response on `POST /api/workflows`.
4998
6427
  allOf:
4999
6428
  - $ref: '#/components/schemas/ErrorEnvelope'
5000
6429
  - type: object
@@ -5037,12 +6466,13 @@ components:
5037
6466
 
5038
6467
  Delivered alongside `ValidationErrorEnvelope`,
5039
6468
  `FeatureNotAvailableResponse`, and
5040
- `ProcessingClassExceedsBandResponse` via the naked `oneOf` on
5041
- the 422 response — duck-typed on required-field shape: this
5042
- branch is the only one carrying `error_type: probe_pending`
5043
- and it has neither `details` (ValidationErrorEnvelope) nor
5044
- `violations` (the other two typed envelopes), so it matches
5045
- exactly one branch.
6469
+ `ProcessingClassExceedsBandResponse` via the discriminated
6470
+ `oneOf` on the 422 response (per
6471
+ [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md))
6472
+ this branch's `error_type` discriminator value is `probe_pending`;
6473
+ it also remains the only branch carrying `job_ref` and neither
6474
+ `details` (`ValidationErrorEnvelope`) nor `violations` (the other
6475
+ two typed envelopes).
5046
6476
  allOf:
5047
6477
  - $ref: '#/components/schemas/ErrorEnvelope'
5048
6478
  - type: object
@@ -5238,10 +6668,12 @@ components:
5238
6668
  envelope is 422, not an extension of `TierRestrictionResponse`
5239
6669
  on 403).
5240
6670
 
5241
- Delivered on `POST /api/workflows` 422 in a naked `oneOf`
5242
- alongside `ValidationErrorEnvelope` and
5243
- `FeatureNotAvailableResponse` — branches distinguished by
5244
- required-field shape (`details` vs `error_type` value).
6671
+ Delivered on `POST /api/workflows` 422 in a discriminated `oneOf`
6672
+ alongside `ValidationErrorEnvelope`, `FeatureNotAvailableResponse`,
6673
+ and `ProbePendingResponse` (per
6674
+ [ADR-0018](../docs/decisions/0018-universal-422-error-type-discriminator.md))
6675
+ this branch's `error_type` discriminator value is
6676
+ `processing_class_exceeds_band`.
5245
6677
  allOf:
5246
6678
  - $ref: '#/components/schemas/ErrorEnvelope'
5247
6679
  - type: object
@@ -5462,20 +6894,22 @@ components:
5462
6894
  Available operation types:
5463
6895
  - compress: Reduce file size (images, audio, video, documents)
5464
6896
  - thumbnail: Legacy thumbnail value. Generates a preview image
5465
- for any media type via a single Lambda. Currently the only
5466
- thumbnail value the compression_api publisher emits; retirement
5467
- is planned after the publisher adopts the four sub-type values
5468
- below in a follow-up API PR.
6897
+ for any media type via a single Lambda. Retained as a valid
6898
+ routing target during the migration window, but the
6899
+ compression_api publisher no longer emits it it now resolves
6900
+ the per-media sub-type value below on the SNS `operation_type`
6901
+ attribute. The payload `operation_type` field stays `thumbnail`.
5469
6902
  - thumbnail_image: Image thumbnail sub-type. Backed by a dedicated
5470
- Rust image Lambda. Not yet emitted by the publisher.
6903
+ Rust image Lambda. Emitted by the publisher for image inputs.
5471
6904
  - thumbnail_video: Video thumbnail sub-type. Backed by a dedicated
5472
- FFmpeg Lambda. Not yet emitted.
6905
+ FFmpeg Lambda. Emitted by the publisher for video inputs.
5473
6906
  - thumbnail_document: PDF/EPUB thumbnail sub-type. Backed by a
5474
- dedicated Ghostscript Lambda. Not yet emitted.
6907
+ dedicated Ghostscript Lambda. Emitted by the publisher for
6908
+ PDF/EPUB inputs.
5475
6909
  - thumbnail_office: Office document (DOCX/XLSX/PPTX/ODT/ODS/ODP)
5476
6910
  thumbnail sub-type. Backed by a dedicated LibreOffice Lambda.
5477
- Not yet emitted.
5478
- - image_watermark: Image overlay (file or external_source) onto a base IMAGE asset. Multi-input (Path B with role: base + overlay). Stable for static-image bases (jpeg/png/webp); animated GIF is advertised as `planned` via the `image_gif` mime_group — dispatch returns `feature_not_available` (422) until Lambda support ships. Video bases are NOT supported by image_watermark — use the dedicated `video_watermark` operation per ADR-0013. Per ADR-0004 + I4-CONS + I5 (Trello AKZiOXnd).
6911
+ Emitted by the publisher for office-document inputs.
6912
+ - image_watermark: Image overlay onto a base IMAGE asset. Multi-input (Path B with role: base + overlay). Each input is a `MultiInputSource` (external_import / connection / job_output — no upload-direct); an uploaded base or overlay enters via a `passthrough` source job referenced by `job_output`. Stable for static-image bases (jpeg/png/webp); animated GIF is advertised as `planned` via the `image_gif` mime_group — dispatch returns `feature_not_available` (422) until Lambda support ships. Video bases are NOT supported by image_watermark — use the dedicated `video_watermark` operation per ADR-0013. Per ADR-0004 + I4-CONS + I5 (Trello AKZiOXnd).
5479
6913
  - text_watermark: Text overlay rendered onto an image (Liberation Sans). Single-input. Per ADR-0004 + I4-CONS.
5480
6914
  - merge: Concatenate/combine multiple files into one (images, video, audio). Multi-input. Image inputs merge into animated GIF or slideshow video; image collage/grid and PDF concatenation are not supported by the V1 Lambda.
5481
6915
  - archive: Bundle files into ZIP/tar.gz (all types). Multi-input.
@@ -5483,10 +6917,11 @@ components:
5483
6917
  - custom_luma: Apply a caller-uploaded luma matte to a base video for a custom luma-matte transition effect. Multi-input (`role: base` + `role: transition_mask`). `availability: planned` + `required_tier: pro`; dispatch returns `feature_not_available` (422) until Lambda ships. Distinct from FFmpeg `xfade=custom` (which is an expression, not an operation). Per ticket I29 (Trello EPUE5Vs1).
5484
6918
  - audio_overlay: Mix a secondary audio asset over a primary audio or video base (DJ tags, podcast intros/outros, station IDs, jingles). Multi-input (`role: base` + `role: overlay`). `availability: planned`; dispatch returns `feature_not_available` (422) until Lambda ships. **NOT** the same as `audio_watermark` — that operation is steganographic (imperceptible identifier embedded for ownership tracking), tracked separately by I20. Per ticket I19 (Trello Xr3Z4GBF).
5485
6919
  - audio_watermark: Embed a steganographic forensic watermark into an audio asset (or a video's audio track) — Cinavia / Resemble PerTh territory. Single-input. `availability: planned` + `required_tier: enterprise`; dispatch returns `feature_not_available` (422) until Lambda ships. Pairs with `POST /api/audio-watermark/decode` for own-watermarks-only extraction. Per ticket I20 (Trello omiCq7Vn).
5486
- - audio_to_video: Produce a video from an audio input plus an OPTIONAL still image overlay. Multi-input role-based with the first OPTIONAL role on the contract (`role: base` audio required, `role: overlay` image 0..1 — see `per_role_cardinality`). When overlay is omitted, the video uses a solid background colour. `availability: planned`; dispatch returns `feature_not_available` (422) until Lambda ships. Per ticket [`SlluxMBN`](https://trello.com/c/SlluxMBN) + ADR-0015 (introduces `per_role_cardinality` vocab).
5487
- - video_watermark: Apply an image overlay onto a base video via FFmpeg's `overlay` filter. Multi-input role-based (`role: base` video + `role: overlay` image, exactly one of each per `per_role_cardinality`). Re-encode required; audio stream-copy passthrough. Distinct from `image_watermark` (pure-Rust/image-only). `availability: planned`; dispatch returns `feature_not_available` (422) until Lambda ships. Per ticket [`4NrRPCgh`](https://trello.com/c/4NrRPCgh) + ADR-0013.
6920
+ - audio_to_video: Produce a video from an audio input plus an OPTIONAL still image overlay. Multi-input role-based with the first OPTIONAL role on the contract (`role: base` audio required, `role: overlay` image 0..1 — see `per_role_cardinality`). When overlay is omitted, the video uses a solid background colour. `availability: beta` (Wave A — Lambda backend live; opt-in, MUST NOT return `feature_not_available`; may change with notice). Per ticket [`SlluxMBN`](https://trello.com/c/SlluxMBN) + ADR-0015 (introduces `per_role_cardinality` vocab).
6921
+ - video_watermark: Apply an image overlay onto a base video via FFmpeg's `overlay` filter. Multi-input role-based (`role: base` video + `role: overlay` image, exactly one of each per `per_role_cardinality`). Re-encode required; audio stream-copy passthrough. Distinct from `image_watermark` (pure-Rust/image-only). `availability: beta` (Wave A — Lambda backend live; opt-in, MUST NOT 422; `short_form` is beta, `long_form` stays `planned` — no live Fargate worker; `multi_overlay_stack` stays `planned`). Per ticket [`4NrRPCgh`](https://trello.com/c/4NrRPCgh) + ADR-0013.
5488
6922
  - video_text_watermark: Render a text overlay onto a base video via FFmpeg's `drawtext` filter. Single-input — text and styling in options. Same `watermark_mode` (single/tiled), anchor + margin vocab as `text_watermark`. Re-encode required; audio stream-copy passthrough. `availability: planned`; dispatch returns `feature_not_available` (422) until Lambda ships. Per ticket [`4NrRPCgh`](https://trello.com/c/4NrRPCgh) + ADR-0013.
5489
- - split: Fan one input file into N outputs across GIF / PDF / audio / video MIME families. Single-input per-mime-group catalog (mirrors merge/convert): GIF uses `frame_range` (REQUIRED) + `output_format`; PDF uses `page_range` OR `page_groups` (mutually exclusive); audio + video use a `mode` discriminator (interval/count/cut_points) + numeric-seconds wire format + `precision` flag (fast/exact). 200-output hard cap per ADR-0009 §D5 with per-mode preflight math; output naming `output-001..output-200`. Long-form video routes to a separate `split-video-fargate` worker via `processing_class`. `availability: planned`; dispatch returns `feature_not_available` (422) until Lambda ships. Per ticket [`vKI0CFDu`](https://trello.com/c/vKI0CFDu) + ADR-0014.
6923
+ - split: Fan one input file into N outputs across GIF / PDF / audio / video MIME families. Single-input per-mime-group catalog (mirrors merge/convert): GIF uses `frame_range` (REQUIRED) + `output_format`; PDF uses `page_range` OR `page_groups` (mutually exclusive); audio + video use a `mode` discriminator (interval/count/cut_points) + numeric-seconds wire format + `precision` flag (fast/exact). 200-output hard cap per ADR-0009 §D5 with per-mode preflight math; output naming `output-001..output-200`. Long-form video routes to a separate `split-video-fargate` worker via `processing_class`. `availability: beta` for the `audio` and `video` mime_groups (workers live on staging — shape-stable + opt-in, MUST NOT 422); video activates short-form only (`video.processing_class.short_form: beta`; `long_form` / `split-video-fargate` stays `planned`). The `image_gif` and `document_pdf` mime_groups stay `availability: planned` and dispatch returns `feature_not_available` (422) until their workers ship. Per ticket [`vKI0CFDu`](https://trello.com/c/vKI0CFDu) + ADR-0014.
6924
+ - passthrough: Inert lossless source operation. A single-input source job whose SOLE operation is `passthrough` emits its source bytes UNCHANGED — no compression, no Lambda. The API self-completes the job at publish (terminal output = the upload `{bucket, key}` unchanged). Its purpose is to feed an uploaded file into a multi-input operation LOSSLESSLY: because `JobInputV2.source` is narrowed to exclude upload-direct, an upload that must enter a `merge` / `archive` / `image_watermark` op enters via a `passthrough` source job referenced downstream by `{type: job_output, from: <id>}` — preserving billing / DAG / lineage. Distinct from `operations: []` (which keeps its implicit-compress meaning on a single-input upload job); `passthrough` is the EXPLICIT lossless path via the "non-empty `operations[]` without `compress` = compression opt-out" rule. Media-agnostic; no options. `availability: beta` — activated (the inputs[]-narrowing + passthrough self-complete mechanism is deployed API-side); workflow-create accepts `passthrough` source jobs and MUST NOT return `feature_not_available`. **Never published to SNS** — deliberately absent from the AsyncAPI routing enums (API self-completes; no `ops-passthrough` queue). Per ticket [`4som89Uh`](https://trello.com/c/4som89Uh) + ADR-0004 (planned→beta flip).
5490
6925
 
5491
6926
  Both the legacy `thumbnail` value and the four sub-type values
5492
6927
  are valid routing targets today during the thumbnail migration
@@ -5505,6 +6940,7 @@ components:
5505
6940
  - thumbnail_office
5506
6941
  - image_watermark
5507
6942
  - text_watermark
6943
+ - render_variants
5508
6944
  - merge
5509
6945
  - archive
5510
6946
  - convert
@@ -5515,6 +6951,7 @@ components:
5515
6951
  - video_watermark
5516
6952
  - video_text_watermark
5517
6953
  - split
6954
+ - passthrough
5518
6955
 
5519
6956
  OperationInputModel:
5520
6957
  type: string
@@ -5523,7 +6960,7 @@ components:
5523
6960
  - single: One input file (compress, thumbnail, thumbnail_image,
5524
6961
  thumbnail_video, thumbnail_document, thumbnail_office,
5525
6962
  text_watermark, convert, audio_watermark,
5526
- video_text_watermark, split)
6963
+ video_text_watermark, split, passthrough)
5527
6964
  - multi: Multiple input files (merge, archive, image_watermark, custom_luma, audio_overlay, audio_to_video, video_watermark). audio_to_video is the first role-based op with an OPTIONAL role (min_inputs=1, max_inputs=2 — see `per_role_cardinality`); video_watermark mirrors `image_watermark`'s 2-required pattern on video bases.
5528
6965
  enum:
5529
6966
  - single
@@ -5691,6 +7128,7 @@ components:
5691
7128
 
5692
7129
  UploadSuccessEnvelope:
5693
7130
  type: object
7131
+ additionalProperties: false
5694
7132
  required:
5695
7133
  - success
5696
7134
  - data
@@ -6303,6 +7741,7 @@ components:
6303
7741
 
6304
7742
  MetadataSuccessEnvelope:
6305
7743
  type: object
7744
+ additionalProperties: false
6306
7745
  required:
6307
7746
  - success
6308
7747
  - data
@@ -6319,15 +7758,82 @@ components:
6319
7758
 
6320
7759
  WorkflowCreateRequest:
6321
7760
  type: object
6322
- required:
6323
- - jobs
7761
+ description: |
7762
+ Create a workflow. **Two mutually-exclusive request forms:**
7763
+
7764
+ - **Explicit jobs** — `jobs[]` (+ optional `workflow_edges`), the
7765
+ full multi-job DAG form. Use for multi-job workflows, role-based
7766
+ / multi-input operations, output-as-role-input chains, explicit
7767
+ edges, or `delivery.selection.type: explicit`.
7768
+ - **Flat (single-job sugar)** — top-level `source` + `operations[]`,
7769
+ **exactly equivalent to `jobs: [{ source, operations }]`** (per
7770
+ ticket [`D0Gsri8V`](https://trello.com/c/D0Gsri8V)). The server
7771
+ lowers it into one `JobDefinition` and validates it as such —
7772
+ inheriting every `JobDefinition` guard verbatim — then runs the
7773
+ existing single-job canonicalizer. For the file-first
7774
+ dump-and-go case: one input + a set of single-input operations
7775
+ (a chain plus `base`-derived branches like thumbnail / split).
7776
+
7777
+ The two forms are mutually exclusive (a request supplies exactly
7778
+ one — enforced by the `oneOf` below); they are NOT combined.
7779
+
7780
+ Because the flat form lowers to ONE single-input job, its
7781
+ `operations[]` admits only **single-input** operation types. The
7782
+ multi-input / role operations (`merge`, `archive`,
7783
+ `image_watermark`, `custom_luma`, `audio_overlay`,
7784
+ `audio_to_video`, `video_watermark` — the enum at the
7785
+ `JobDefinition` multi-input guard) require `inputs[]` and MUST use
7786
+ the explicit `jobs[]` form; the server rejects them in flat mode
7787
+ via that same `JobDefinition` guard after lowering (not a
7788
+ separate structural rule). The flat form also does **not** support
7789
+ `workflow_edges` or `delivery.selection.type: explicit` — both
7790
+ need a named job id the single implicit job has none of; use
7791
+ explicit `jobs[]` for those. Delivery defaults and selection
7792
+ otherwise behave exactly as for one explicit job.
7793
+ oneOf:
7794
+ - title: Explicit jobs form
7795
+ required: [jobs]
7796
+ not:
7797
+ anyOf:
7798
+ - required: [source]
7799
+ - required: [operations]
7800
+ - title: Flat single-job form
7801
+ required: [source, operations]
7802
+ not:
7803
+ anyOf:
7804
+ - required: [jobs]
7805
+ - required: [workflow_edges]
6324
7806
  properties:
6325
7807
  jobs:
6326
7808
  type: array
6327
- description: List of jobs in this workflow
7809
+ description: |
7810
+ List of jobs in this workflow (the explicit-jobs form).
7811
+ Mutually exclusive with the flat `source` + `operations`
7812
+ form — supply exactly one.
6328
7813
  minItems: 1
6329
7814
  items:
6330
7815
  $ref: '#/components/schemas/JobDefinition'
7816
+ source:
7817
+ $ref: '#/components/schemas/WorkflowSource'
7818
+ description: |
7819
+ Flat-form single input source. Present only in the flat form
7820
+ (with `operations`); equivalent to a single explicit job's
7821
+ `source`. Single-input `WorkflowSource` (4-leaf union incl.
7822
+ `upload`) — multi-input role-based jobs use explicit
7823
+ `jobs[].inputs[]`. See the schema description for the
7824
+ flat/explicit mutex.
7825
+ operations:
7826
+ type: array
7827
+ minItems: 1
7828
+ description: |
7829
+ Flat-form operation set (the unordered operations applied to
7830
+ the top-level `source`). Present only in the flat form (with
7831
+ `source`); equivalent to a single explicit job's
7832
+ `operations[]` and lowered + canonicalized identically.
7833
+ Single-input operation types only (see the schema
7834
+ description). Each entry is an `OperationDefinition`.
7835
+ items:
7836
+ $ref: '#/components/schemas/OperationDefinition'
6331
7837
  workflow_edges:
6332
7838
  type: array
6333
7839
  description: |
@@ -6407,9 +7913,11 @@ components:
6407
7913
  text_watermark, convert).
6408
7914
  - `inputs[]` (multi-input): array of `JobInputV2` entries. Each entry
6409
7915
  carries its own `source` plus an optional `role`
6410
- (image_watermark, custom_luma, audio_overlay) and
7916
+ (image_watermark, custom_luma, audio_overlay, audio_to_video,
7917
+ video_watermark) and
6411
7918
  `per_input_options`. Used by multi-input operations (merge,
6412
- archive, image_watermark, custom_luma, audio_overlay).
7919
+ archive, image_watermark, custom_luma, audio_overlay,
7920
+ audio_to_video, video_watermark).
6413
7921
 
6414
7922
  Exactly one of `source` or `inputs` is required.
6415
7923
 
@@ -6472,8 +7980,12 @@ components:
6472
7980
  type: array
6473
7981
  description: |
6474
7982
  Multi-input list for `merge`, `archive`, `image_watermark`,
6475
- `custom_luma`, `audio_overlay`, and `audio_to_video`. Each
6476
- entry is a `JobInputV2` with its own `WorkflowSource`.
7983
+ `custom_luma`, `audio_overlay`, `audio_to_video`, and
7984
+ `video_watermark`. Each
7985
+ entry is a `JobInputV2` with its own `MultiInputSource` (the
7986
+ 3-leaf subset of `WorkflowSource` that excludes upload-direct;
7987
+ uploads enter via a `passthrough` source job referenced by
7988
+ `job_output`).
6477
7989
  Mutually exclusive with `source` — the V2 shape boundary
6478
7990
  stays `source` (single-input) XOR `inputs[]` (multi-input
6479
7991
  role-based) per ADR-0004 / I12.
@@ -6492,14 +8004,27 @@ components:
6492
8004
  operations:
6493
8005
  type: array
6494
8006
  description: |
6495
- Ordered list of operations. Executed sequentially each
8007
+ Unordered **set** of operations for this job. The API
8008
+ canonicalizes them to a deterministic order — submitted order
8009
+ does not affect the result (same set, any order → same plan →
8010
+ same bytes). The resolved canonical order is echoed in the
8011
+ response `composition_plan` (see
8012
+ `WorkflowCreateResponse.composition_plan`). Each canonical chain
6496
8013
  operation consumes the previous operation's output; all
6497
8014
  intermediate and final outputs are kept.
6498
8015
 
8016
+ Order-independence is over a *well-formed* set: conflicting or
8017
+ duplicate same-stage operations (e.g. two `compress` with
8018
+ different options) are resolved or rejected by the
8019
+ canonicalization engine — the contract does not enumerate the
8020
+ conflict-resolution rules (engine-side, like the opaque
8021
+ processing-time estimator). A rejected set surfaces as a
8022
+ `validation_error` (422); it is not silently ordered.
8023
+
6499
8024
  Multi-input jobs (with `inputs[]`) must have exactly one
6500
8025
  operation, and it must be a multi-input type (`merge`,
6501
8026
  `archive`, `image_watermark`, `custom_luma`,
6502
- `audio_overlay`, or `audio_to_video`).
8027
+ `audio_overlay`, `audio_to_video`, or `video_watermark`).
6503
8028
  items:
6504
8029
  $ref: '#/components/schemas/OperationDefinition'
6505
8030
  deliver:
@@ -6540,25 +8065,72 @@ components:
6540
8065
  `custom_luma` (joined via ticket
6541
8066
  [I29](https://trello.com/c/EPUE5Vs1) — `availability:
6542
8067
  planned` so workflow-create returns 422 until Lambda
6543
- ships), and `audio_overlay` (joined via ticket
8068
+ ships), `audio_overlay` (joined via ticket
6544
8069
  [I19](https://trello.com/c/Xr3Z4GBF) — same `planned`
6545
- gating). The multi-input shape is contract-defined for
6546
- all entries.
8070
+ gating), and `audio_to_video` + `video_watermark` (joined
8071
+ via Wave A [`c3uthIP4`](https://trello.com/c/c3uthIP4) —
8072
+ `availability: beta`, backends live). The multi-input
8073
+ shape is contract-defined for all entries.
8074
+ - if:
8075
+ properties:
8076
+ operations:
8077
+ contains:
8078
+ type: object
8079
+ properties:
8080
+ type:
8081
+ const: passthrough
8082
+ required: [type]
8083
+ required: [operations]
8084
+ then:
8085
+ properties:
8086
+ operations:
8087
+ minItems: 1
8088
+ maxItems: 1
8089
+ items:
8090
+ properties:
8091
+ type:
8092
+ const: passthrough
8093
+ source:
8094
+ properties:
8095
+ type:
8096
+ const: upload
8097
+ required: [type]
8098
+ required: [source]
8099
+ description: |
8100
+ `passthrough` constraint. A `passthrough` operation MUST be
8101
+ the SOLE operation of a SINGLE-INPUT job whose `source.type`
8102
+ is `upload` (i.e. `operations: [{type: passthrough}]` with
8103
+ `source: {type: upload, ...}`): never chained with another
8104
+ operation, never on `inputs[]`, never on a non-`upload`
8105
+ source. `passthrough` is an inert lossless bridge that the
8106
+ API self-completes at publish with the upload's `{bucket,
8107
+ key}` as the job output (see the `passthrough` bullet under
8108
+ `OperationType` and the `POST /api/workflows` description),
8109
+ so it is meaningful only for an uploaded source feeding a
8110
+ downstream multi-input op via `job_output`. The shared
8111
+ `OperationType` enum makes `passthrough` syntactically
8112
+ expressible elsewhere; this guard makes those uses
8113
+ schema-invalid rather than deferring rejection to the
8114
+ backend.
6547
8115
 
6548
8116
  JobInputV2:
6549
8117
  type: object
6550
8118
  description: |
6551
8119
  V2 multi-input entry per [ADR-0004](../docs/decisions/0004-job-shape.md)
6552
8120
  §"JobInputV2". Replaces V1 `JobInput`. Each entry carries its own
6553
- `source` (a `WorkflowSource` value — same union used at
6554
- `JobDefinition.source`), so multi-input jobs can mix uploads,
6555
- external imports, vault connections, and upstream job outputs
6556
- within a single inputs[] array.
8121
+ `source` (a `MultiInputSource` value — the 3-leaf subset of
8122
+ `WorkflowSource` that **excludes upload-direct**), so multi-input
8123
+ jobs can mix external imports, vault connections, and upstream job
8124
+ outputs within a single inputs[] array. An uploaded file enters a
8125
+ multi-input op via a `passthrough` source job (single-input,
8126
+ `operations: [{type: passthrough}]`) referenced here as
8127
+ `{ type: job_output, from: <id> }` — not as a direct `upload`
8128
+ source (per ticket [`4som89Uh`](https://trello.com/c/4som89Uh)).
6557
8129
  required:
6558
8130
  - source
6559
8131
  properties:
6560
8132
  source:
6561
- $ref: '#/components/schemas/WorkflowSource'
8133
+ $ref: '#/components/schemas/MultiInputSource'
6562
8134
  role:
6563
8135
  type: string
6564
8136
  description: |
@@ -6632,6 +8204,24 @@ components:
6632
8204
  rules. For example, `quality` is only valid when `mode: lossy` for
6633
8205
  compress operations.
6634
8206
  additionalProperties: true
8207
+ base:
8208
+ type: string
8209
+ enum:
8210
+ - processed_base
8211
+ - original
8212
+ default: processed_base
8213
+ description: |
8214
+ For **derived artifacts** (`thumbnail`, `split`): which canonical
8215
+ composition node this artifact branches from. Symbolic-only for
8216
+ v1 (no per-operation references — caller-visible op ids do not
8217
+ exist at submit time).
8218
+ - `processed_base` (default): the fully-processed base —
8219
+ post-everything, so the artifact carries the watermark + encode.
8220
+ - `original`: the untouched source file.
8221
+ Ignored for chain operations (compress, convert, watermark, merge,
8222
+ audio). The resolved branch point is echoed as
8223
+ `CompositionPlanOperation.derived_from` (a `node_id`) in
8224
+ `composition_plan`. See the operation-composition model.
6635
8225
 
6636
8226
  # ============================================
6637
8227
  # WORKFLOW DELIVERY (ticket I8-CONS)
@@ -6662,6 +8252,13 @@ components:
6662
8252
  Optional operation type filter when the job has multiple
6663
8253
  operations. Omit to select the last operation's output.
6664
8254
 
8255
+ **`availability: planned`** — per-operation-output grain is
8256
+ not yet honoured: the API currently rejects a non-empty
8257
+ `operation` filter with `feature_not_available` (422), even
8258
+ though `delivery.selection` `all_outputs`/`explicit` are
8259
+ `stable` at JOB grain (per `cse3wt2s` / T5b). Deferred to a
8260
+ follow-up; omit it (job-grain selection) for now.
8261
+
6665
8262
  DeliverySelection:
6666
8263
  type: object
6667
8264
  description: |
@@ -6677,8 +8274,10 @@ components:
6677
8274
  Selection strategy:
6678
8275
  - `terminal` (default): bundle all jobs with no outgoing
6679
8276
  edges (computed from `JobOutputSource.from` references).
6680
- - `all_outputs`: bundle every operation output in the
6681
- workflow, including intermediates.
8277
+ - `all_outputs`: bundle every **job's** output in the
8278
+ workflow, including intermediates (job-grain). Per-operation
8279
+ output selection within a job is NOT yet available — see the
8280
+ `DeliveryOutputRef.operation` filter (`planned`).
6682
8281
  - `explicit`: bundle only the refs listed in `refs[]`.
6683
8282
  Mutually exclusive with per-job `JobDefinition.deliver: true`
6684
8283
  (validator rejects 422 if both are present).
@@ -6688,19 +8287,21 @@ components:
6688
8287
  - explicit
6689
8288
  per_value_availability:
6690
8289
  terminal: { availability: stable }
6691
- all_outputs: { availability: planned }
6692
- explicit: { availability: planned }
6693
- # Availability flipped betaplanned per ticket
6694
- # [`co0CERtJ`](https://trello.com/c/co0CERtJ) the API
6695
- # runtime currently 422s every non-default `selection.type`
6696
- # with `feature_not_available`, which per the
6697
- # `info.description` availability taxonomy IS the `planned`
6698
- # behaviour (`stable`/`beta` MUST NOT 422). When the API
6699
- # threads the request-level `delivery.selection` shape, this
6700
- # flips back. Terminal stays `stable` because the runtime
6701
- # honours it as the implicit default (`DeliveryPlanComputer`
6702
- # computes terminal selection regardless of whether the
6703
- # caller supplied a `delivery` block).
8290
+ all_outputs: { availability: stable }
8291
+ explicit: { availability: stable }
8292
+ # Availability flipped plannedstable per ticket
8293
+ # [`cse3wt2s`](https://trello.com/c/cse3wt2s) (T5b delivery-
8294
+ # selection go-live, co-land with API PR #365). The two-gate
8295
+ # flip: co0CERtJ downgraded these to `planned` to match the
8296
+ # runtime, which 422'd every non-default `selection.type`; the
8297
+ # API now ACCEPTS `all_outputs`/`explicit` at JOB grain
8298
+ # (`DeliveryPlanComputer` threads the request-level
8299
+ # `delivery.selection`), so the contract flips back to `stable`.
8300
+ # The `all_outputs` claim is narrowed to job-grain to stay
8301
+ # conformant; per-operation-output grain + the
8302
+ # `DeliveryOutputRef.operation` filter stay `planned` (the API
8303
+ # still 422s the operation filter — deferred follow-up). Terminal
8304
+ # stays `stable` (the implicit default).
6704
8305
  refs:
6705
8306
  type: array
6706
8307
  description: |
@@ -6828,6 +8429,18 @@ components:
6828
8429
  description: |
6829
8430
  Set when `reason: consumed_by`. The id of the downstream
6830
8431
  job that consumes this output.
8432
+ node_id:
8433
+ type: string
8434
+ description: |
8435
+ Symbolic composition `node_id` correlating this delivered output
8436
+ to its canonical node in `composition_plan` (e.g. `encode`,
8437
+ `thumbnail`, `processed_base`). Lets consumers label and group
8438
+ delivered files by composition role. **Optional** — emitted once
8439
+ the canonicalization engine is live (optional-then-promote,
8440
+ mirroring `composition_plan`); absent until then. This is the
8441
+ additive carrier; the normative
8442
+ `/downloads == delivery_plan.outputs[]` rendezvous invariant is
8443
+ introduced with the delivery-selection promotion, not here.
6831
8444
 
6832
8445
  DeliveryPlan:
6833
8446
  type: object
@@ -7086,6 +8699,205 @@ components:
7086
8699
  class_hint:
7087
8700
  $ref: '#/components/schemas/ProcessingClassHint'
7088
8701
 
8702
+ # ============================================
8703
+ # CANONICAL COMPOSITION PLAN (operation-composition epic)
8704
+ # ============================================
8705
+ # Response-side single source of truth for operation composition.
8706
+ # The API accepts an unordered operation SET (per source) and
8707
+ # canonicalizes it to a deterministic DAG; this plan exposes the
8708
+ # resolved order, per-op chain group/position, derived-artifact
8709
+ # lineage, and image-encode capabilities so FE + SDK read the model
8710
+ # instead of hardcoding it. Additive-first: these schemas ship OPEN
8711
+ # (no `additionalProperties: false`, no `allOf`) — flat objects to
8712
+ # avoid the inherited-field-closure footgun, and OPEN so the
8713
+ # canonicalization engine (later epic phases) can add correlation
8714
+ # fields before a separate tightening cut. All correlation fields
8715
+ # (`node_id`, `operation_id`, `requested_operations`, capability
8716
+ # keys) are pinned now so the eventual `additionalProperties: false`
8717
+ # cut has nothing to break.
8718
+
8719
+ CompositionPlan:
8720
+ type: object
8721
+ description: |
8722
+ Canonical composition plan for a workflow — emitted on
8723
+ `WorkflowCreateResponse.composition_plan`. Tells callers the
8724
+ deterministic order the submitted operation set resolved to, the
8725
+ per-operation chain group/position, derived-artifact lineage, and
8726
+ the image-encode capabilities that gate format/quality choices.
8727
+
8728
+ `canonical_order` is the informational pipeline spine; per-op
8729
+ detail lives on each `CompositionPlanOperation`.
8730
+ required:
8731
+ - canonical_order
8732
+ - jobs
8733
+ - capabilities
8734
+ properties:
8735
+ canonical_order:
8736
+ type: array
8737
+ description: |
8738
+ The canonical pipeline spine (single mutated stream), as an
8739
+ ordered list of `chain_group` stage names. The known image/AV
8740
+ spine is `merge` (fan-in) → `image_watermark` → `text_watermark`
8741
+ → `audio` → `encode` (terminal: convert+compress folded) →
8742
+ `derived` (fan-out: thumbnail/split). Informational — read per-op
8743
+ `chain_group` / `chain_position` for placement. The geometry
8744
+ stage is intentionally not exposed as a node for v1 (image
8745
+ resize/fit live inside the thumbnail node).
8746
+
8747
+ **Open string, NOT a fixed enum** (mirrors
8748
+ `ProcessingPlanJob.execution_pool`): the canonical model still
8749
+ covers operation classes not in the image-pipeline spine
8750
+ (`archive`, `passthrough`, `audio_to_video`, …) and the engine's
8751
+ final stage taxonomy is pinned alongside the canonicalization
8752
+ engine (later epic phases). The values are tightened to an enum
8753
+ in the same gated cut that adds `additionalProperties: false`,
8754
+ once the engine emits the complete set. Consumers MUST treat
8755
+ unknown stage names as forward-compatible.
8756
+ items:
8757
+ type: string
8758
+ jobs:
8759
+ type: array
8760
+ description: Per-job canonical composition. Empty `[]` is permitted.
8761
+ items:
8762
+ $ref: '#/components/schemas/CompositionPlanJob'
8763
+ capabilities:
8764
+ $ref: '#/components/schemas/ImageEncodeCapabilities'
8765
+
8766
+ CompositionPlanJob:
8767
+ type: object
8768
+ description: Per-job entry in a `CompositionPlan` — the canonical operations for one job.
8769
+ required:
8770
+ - job_id
8771
+ - operations
8772
+ properties:
8773
+ job_id:
8774
+ $ref: '#/components/schemas/UuidV7'
8775
+ operations:
8776
+ type: array
8777
+ description: The job's operations in canonical (resolved) order.
8778
+ items:
8779
+ $ref: '#/components/schemas/CompositionPlanOperation'
8780
+
8781
+ CompositionPlanOperation:
8782
+ type: object
8783
+ description: |
8784
+ The canonical view of one operation within a job — its symbolic
8785
+ composition node, resolved chain placement, and (for derived
8786
+ artifacts) the node it branches from.
8787
+ required:
8788
+ - node_id
8789
+ - type
8790
+ - chain_group
8791
+ - chain_position
8792
+ properties:
8793
+ node_id:
8794
+ type: string
8795
+ description: |
8796
+ Stable **symbolic** canonical node id (e.g. `original`,
8797
+ `processed_base`, `encode`, `thumbnail`). The correlation key
8798
+ used by `derived_from`, `DeliveryPlanOutput.node_id`, and
8799
+ `OperationDownload.node_id` to tie delivered files back to a
8800
+ canonical node. **NOT** the operation UUID. (SSE stays
8801
+ per-operation by design — it is not a delivery-plan projection
8802
+ and does not carry `node_id`.)
8803
+ example: encode
8804
+ operation_id:
8805
+ $ref: '#/components/schemas/UuidV7'
8806
+ type:
8807
+ $ref: '#/components/schemas/OperationType'
8808
+ description: |
8809
+ The canonical operation type of this node. For the folded
8810
+ terminal `encode` node (`chain_group: encode`) this is the
8811
+ encode operation `compress` — `convert` folds INTO it (a single
8812
+ terminal encode, never a double-encode), and the folded source
8813
+ operations are listed in `requested_operations`. For all other
8814
+ nodes `type` is the operation as submitted.
8815
+ chain_group:
8816
+ type: string
8817
+ description: |
8818
+ Which canonical stage this operation belongs to. **Open string,
8819
+ NOT a fixed enum** (mirrors `ProcessingPlanJob.execution_pool`) —
8820
+ the stage taxonomy is pinned alongside the canonicalization
8821
+ engine and tightened to an enum in the same gated cut that adds
8822
+ `additionalProperties: false`; consumers MUST treat unknown
8823
+ stage names as forward-compatible. The watermark stages are
8824
+ **media-agnostic overlay stages**, not image-only. Known stages:
8825
+ - `merge`: fan-in.
8826
+ - `image_watermark`: raster/image-overlay watermark stage —
8827
+ covers the `image_watermark` AND `video_watermark` operation
8828
+ types.
8829
+ - `text_watermark`: text-overlay watermark stage — covers the
8830
+ `text_watermark` AND `video_text_watermark` operation types.
8831
+ - `audio`: audio operations (e.g. `audio_overlay`,
8832
+ `audio_to_video`).
8833
+ - `encode`: the folded convert+compress terminal encode stage.
8834
+ - `derived`: fan-out artifact (`thumbnail` / `split`).
8835
+ Operation classes outside the image/AV spine (e.g. `archive`
8836
+ bundling, `passthrough` bridge jobs) carry their own stage names
8837
+ once the engine pins them. (The geometry/resize stage is not
8838
+ exposed as a node for v1 — image resize lives inside the
8839
+ `thumbnail` worker.)
8840
+ chain_position:
8841
+ type: integer
8842
+ minimum: 0
8843
+ description: Deterministic resolved position within the canonical chain.
8844
+ derived_from:
8845
+ type: string
8846
+ description: |
8847
+ Set only for derived operations (`thumbnail`, `split`): the
8848
+ `node_id` this artifact branches from (symbolic; resolved from
8849
+ the request-side `OperationDefinition.base`). Absent for chain
8850
+ operations.
8851
+ example: processed_base
8852
+ requested_operations:
8853
+ type: array
8854
+ description: |
8855
+ Image-fold audit trail: the request operation types that were
8856
+ absorbed into this single canonical node. For the folded
8857
+ `encode` node this is e.g. `[convert, compress]` — the caller
8858
+ still submits both, but composition exposes ONE `encode` node
8859
+ (no false double-encode). Absent when no fold occurred.
8860
+ items:
8861
+ $ref: '#/components/schemas/OperationType'
8862
+
8863
+ ImageEncodeCapabilities:
8864
+ type: object
8865
+ description: |
8866
+ Image-encode-stage capability flags that gate format/quality
8867
+ choices for the canonical `encode` node. **Image-encode-scoped**
8868
+ — these caveats apply to the image encode pass only, not to
8869
+ video / audio / document encoding. Surfaced so FE/SDK do not
8870
+ promise capabilities the worker cannot honour (e.g. a WebP
8871
+ quality slider the convert path silently ignores).
8872
+ required:
8873
+ - webp_quality_supported
8874
+ - background_flatten
8875
+ properties:
8876
+ webp_quality_supported:
8877
+ type: boolean
8878
+ description: |
8879
+ Whether quality-controlled (lossy) WebP encode is available.
8880
+ `false` today: WebP via the convert/encode path is
8881
+ lossless-only and the `quality` option is silently ignored
8882
+ (quality-controlled WebP is only reachable via `compress`).
8883
+ Consumers MUST NOT offer a WebP quality control when this is
8884
+ `false`.
8885
+ example: false
8886
+ background_flatten:
8887
+ type: string
8888
+ description: |
8889
+ Whether alpha→non-alpha background flattening is available at
8890
+ encode time. `conditional` today: flatten fires only when an
8891
+ alpha source is encoded to a non-alpha output (e.g. PNG→JPEG).
8892
+ - `unsupported`: never applied.
8893
+ - `conditional`: applied only on alpha→non-alpha transitions.
8894
+ - `supported`: always available.
8895
+ enum:
8896
+ - unsupported
8897
+ - conditional
8898
+ - supported
8899
+ example: conditional
8900
+
7089
8901
  # ============================================
7090
8902
  # UPLOAD PREFLIGHT PROBE (per ticket I28)
7091
8903
  # ============================================
@@ -7291,6 +9103,7 @@ components:
7291
9103
 
7292
9104
  UploadProbeSuccessEnvelope:
7293
9105
  type: object
9106
+ additionalProperties: false
7294
9107
  description: |
7295
9108
  Success envelope wrapping `UploadProbeResponse` on
7296
9109
  `POST /api/uploads/{id}/probe` 200 responses, per ticket
@@ -7478,6 +9291,24 @@ components:
7478
9291
  frontend can preview routing decisions before the
7479
9292
  workflow runs. Estimation algorithm is server-side and
7480
9293
  deliberately opaque per plan v5 §F8.2.
9294
+ composition_plan:
9295
+ $ref: '#/components/schemas/CompositionPlan'
9296
+ description: |
9297
+ Server-computed canonical composition plan — the single
9298
+ source of truth for how the submitted operation **set** was
9299
+ canonicalized into a deterministic DAG (canonical order,
9300
+ per-op chain group/position, derived-artifact lineage, and
9301
+ image-encode capabilities). Frontend + SDK read this instead
9302
+ of hardcoding the pipeline order (which today lives only in
9303
+ the API), so click-order never changes the result.
9304
+
9305
+ **OPTIONAL (optional-then-promote).** The canonicalization
9306
+ engine that emits it ships in later phases of the
9307
+ operation-composition epic; until it is live the server omits
9308
+ this field rather than emitting a partial plan. Promoted to
9309
+ required once the engine emits reliably (the same
9310
+ optional-then-promote path `delivery_plan` / `processing_plan`
9311
+ took). Consumers MUST treat it as may-be-absent.
7481
9312
  warnings:
7482
9313
  type: array
7483
9314
  description: |
@@ -7515,6 +9346,7 @@ components:
7515
9346
 
7516
9347
  WorkflowCreateSuccessEnvelope:
7517
9348
  type: object
9349
+ additionalProperties: false
7518
9350
  required:
7519
9351
  - success
7520
9352
  - data
@@ -7566,9 +9398,25 @@ components:
7566
9398
  (per ticket [I24](https://trello.com/c/e50uXLcl)). Carries
7567
9399
  `paused_at`, `expires_at`, `required_action`, and
7568
9400
  actionable `links` (resume / top_up / upgrade).
9401
+ composition_plan:
9402
+ $ref: '#/components/schemas/CompositionPlan'
9403
+ description: |
9404
+ Server-computed canonical composition plan, echoed on the
9405
+ status read so a frontend reload / resume can re-derive the
9406
+ canonicalized DAG (canonical order, per-op chain
9407
+ group/position, derived-artifact lineage, image-encode
9408
+ capabilities) without re-submitting. Identical shape and
9409
+ semantics to `WorkflowCreateResponse.composition_plan`.
9410
+
9411
+ **OPTIONAL (optional-then-promote).** The canonicalization
9412
+ engine that emits it ships in later phases of the
9413
+ operation-composition epic; the server omits the field until
9414
+ it is live (mirroring the create side). Per ticket
9415
+ [`RrxdUBGZ`](https://trello.com/c/RrxdUBGZ) / `i2J8AMy6`.
7569
9416
 
7570
9417
  WorkflowStatusSuccessEnvelope:
7571
9418
  type: object
9419
+ additionalProperties: false
7572
9420
  required:
7573
9421
  - success
7574
9422
  - data
@@ -7579,6 +9427,118 @@ components:
7579
9427
  data:
7580
9428
  $ref: '#/components/schemas/WorkflowStatusResponse'
7581
9429
 
9430
+ JobMediaClass:
9431
+ type: string
9432
+ description: |
9433
+ The media class of a job (the kind of media it processes), distinct
9434
+ from an operation's step `type` (`convert` / `compress` / …). `mixed`
9435
+ is a multi-input job spanning more than one class (e.g. an archive or
9436
+ a merge of heterogeneous inputs). Used in the lightweight workflows
9437
+ list row so a UI can label/group rows by media without drilling into
9438
+ per-operation detail.
9439
+ enum:
9440
+ - image
9441
+ - video
9442
+ - audio
9443
+ - document
9444
+ - mixed
9445
+
9446
+ WorkflowSummaryJob:
9447
+ type: object
9448
+ description: |
9449
+ Minimal per-job entry in a `WorkflowSummary` row — `job_type` (media
9450
+ class) + status for list rendering. Per-operation detail (op step
9451
+ types, `result_metadata`) is the drill-in
9452
+ `GET /api/workflows/{id}/status`; outputs are
9453
+ `GET /api/workflows/{id}/downloads`.
9454
+ required:
9455
+ - job_id
9456
+ - ref
9457
+ - job_type
9458
+ - status
9459
+ properties:
9460
+ job_id:
9461
+ $ref: '#/components/schemas/UuidV7'
9462
+ ref:
9463
+ type: string
9464
+ description: Workflow-local job ref (matches `JobResponse.ref`).
9465
+ job_type:
9466
+ $ref: '#/components/schemas/JobMediaClass'
9467
+ status:
9468
+ $ref: '#/components/schemas/JobStatus'
9469
+
9470
+ WorkflowSummary:
9471
+ type: object
9472
+ description: |
9473
+ Lightweight summary row for `GET /api/workflows` (list). Carries
9474
+ just enough for a list view — id / status / created_at + per-job
9475
+ `{ ref, job_type (media class), status }` — and deliberately does
9476
+ NOT inline per-operation detail, `result_metadata`, output counts,
9477
+ or download URLs (those are the drill-in `GET /{id}/status` +
9478
+ `GET /{id}/downloads` reads; keeping rows lean avoids N×M×K payload
9479
+ blow-up). Field naming mirrors `WorkflowStatusResponse`
9480
+ (`workflow_id`, snake_case).
9481
+ required:
9482
+ - workflow_id
9483
+ - status
9484
+ - created_at
9485
+ - jobs
9486
+ properties:
9487
+ workflow_id:
9488
+ $ref: '#/components/schemas/UuidV7'
9489
+ status:
9490
+ $ref: '#/components/schemas/WorkflowStatus'
9491
+ created_at:
9492
+ type: string
9493
+ format: date-time
9494
+ description: ISO-8601 workflow-creation timestamp (the list sort key, DESC).
9495
+ jobs:
9496
+ type: array
9497
+ items:
9498
+ $ref: '#/components/schemas/WorkflowSummaryJob'
9499
+
9500
+ WorkflowListResponse:
9501
+ type: object
9502
+ description: |
9503
+ Cursor-paginated page of workflow summaries (most-recent-first).
9504
+ Walk pages by passing `next_cursor` as the next request's `cursor`
9505
+ query parameter until `is_truncated: false`. Cursor pagination
9506
+ (opaque `(created_at, id)` token) mirrors the multipart-parts
9507
+ listing convention — NOT the offset-based `/api/v2/credits/usage`.
9508
+ required:
9509
+ - workflows
9510
+ - is_truncated
9511
+ properties:
9512
+ workflows:
9513
+ type: array
9514
+ description: This page of workflow summary rows (most-recent-first).
9515
+ items:
9516
+ $ref: '#/components/schemas/WorkflowSummary'
9517
+ next_cursor:
9518
+ description: |
9519
+ Opaque cursor — pass as the next request's `cursor` query
9520
+ parameter to fetch the following page. `null` on the final page
9521
+ (or when `is_truncated: false`). Treat as opaque; do not parse.
9522
+ oneOf:
9523
+ - type: string
9524
+ - type: 'null'
9525
+ is_truncated:
9526
+ type: boolean
9527
+ description: '`true` when more pages remain; `false` on the final page.'
9528
+
9529
+ WorkflowListSuccessEnvelope:
9530
+ type: object
9531
+ additionalProperties: false
9532
+ required:
9533
+ - success
9534
+ - data
9535
+ properties:
9536
+ success:
9537
+ type: boolean
9538
+ enum: [true]
9539
+ data:
9540
+ $ref: '#/components/schemas/WorkflowListResponse'
9541
+
7582
9542
  JobResponse:
7583
9543
  type: object
7584
9544
  description: Job status within a workflow response
@@ -7597,6 +9557,24 @@ components:
7597
9557
  $ref: '#/components/schemas/UuidV7'
7598
9558
  status:
7599
9559
  $ref: '#/components/schemas/JobStatus'
9560
+ processing_class:
9561
+ $ref: '#/components/schemas/ProcessingClass'
9562
+ description: |
9563
+ The logical processing class the server resolved this job to.
9564
+ The status-poll echo of `WorkflowCreateResponse.processing_plan`
9565
+ `.jobs[].processing_class` (`ProcessingPlanJob`) — same enum, same
9566
+ per-job granularity — so consumers can observe the resolved
9567
+ routing decision (`short_form` vs `long_form`, plus the merge-only
9568
+ `*_concat` / `*_re_encode` aliases) after create, not just at
9569
+ create time. The server always knows a job's class once the
9570
+ workflow is resolved and populates this on every status response
9571
+ (mirrors the `ProcessingPlanJob` invariant); it is left
9572
+ schema-OPTIONAL rather than `required` only because `JobResponse`
9573
+ is also embedded in the `WebhookPayload.workflow` callback payload,
9574
+ where a required addition would be a breaking request-contract
9575
+ change for existing webhook consumers. Per ticket
9576
+ [F3dL0UKz](https://trello.com/c/F3dL0UKz); consumed by api
9577
+ [D3yN1SGm](https://trello.com/c/D3yN1SGm).
7600
9578
  depends_on:
7601
9579
  type: array
7602
9580
  description: List of upstream job refs this job depends on
@@ -7628,6 +9606,18 @@ components:
7628
9606
  description: Progress percentage (0-100). Present when in_progress or completed.
7629
9607
  result:
7630
9608
  $ref: '#/components/schemas/OperationResult'
9609
+ result_metadata:
9610
+ $ref: '#/components/schemas/OperationResultMetadata'
9611
+ description: |
9612
+ Whitelisted operation-level metadata (the read projection — what
9613
+ `GET /api/workflows/{id}/status` emits per operation). Optional;
9614
+ present once an operation produces a whitelisted key. Distinct
9615
+ from `result` (the deliverable output): this carries small
9616
+ per-operation metadata, not the file. The `/status` read carries
9617
+ ONLY this `result_metadata` for op-level detail — the output
9618
+ `result` (`download_url`/`size_bytes`) is read via
9619
+ `GET /api/workflows/{id}/downloads` per the rmP7ndhK/ZjVXHkYK
9620
+ narrowing. Per ticket `dw048NKk` (A2) / `EurbZLMH` (B1).
7631
9621
  error_code:
7632
9622
  type: string
7633
9623
  description: |
@@ -7644,6 +9634,32 @@ components:
7644
9634
  absent otherwise. Mirrors `SseOperationFailedData.error_message`.
7645
9635
  example: "output_too_large: Output (12156489 bytes) is not smaller than input (6187609 bytes)"
7646
9636
 
9637
+ OperationResultMetadata:
9638
+ type: object
9639
+ additionalProperties: false
9640
+ description: |
9641
+ Whitelisted, named operation-level result metadata — a **CLOSED**
9642
+ set of declared keys. The API serves ONLY these declared keys (a
9643
+ whitelist projection), never the raw stored metadata bag, so internal
9644
+ diagnostics never leak. New keys are **additive named cuts** (the
9645
+ `additionalProperties: false` closure is the point — an unmodelled
9646
+ key is a coordinated contract change, not a silent rollout). Twin of
9647
+ the AsyncAPI `OperationResultMetadata` (wire ↔ read parity). Distinct
9648
+ from `OperationResult` (the deliverable output file): this carries
9649
+ small per-operation metadata, not the output. Per `EurbZLMH` (B1).
9650
+ properties:
9651
+ watermark_id:
9652
+ type: string
9653
+ format: uuid
9654
+ description: |
9655
+ UUIDv7 of the watermark embedded by an `audio_watermark`
9656
+ operation (for later decode/verify). **Display-only / not yet
9657
+ populated** — gated on the audio_watermark Lambda (`EvPl5fkH`,
9658
+ B3) + the watermark registry (B2, deferred). The field +
9659
+ whitelist ship now (additive) so the result_metadata pipeline +
9660
+ read projection land without a later contract bump; absent in
9661
+ practice until B3 is live.
9662
+
7647
9663
  OperationResult:
7648
9664
  type: object
7649
9665
  description: |
@@ -7665,7 +9681,20 @@ components:
7665
9681
  description: Key in the customer's export destination (if export configured)
7666
9682
  metrics:
7667
9683
  type: object
7668
- description: Operation-specific performance metrics
9684
+ description: |
9685
+ Operation-specific performance metrics, carried on the
9686
+ **completion result** surface (SSE `operation.completed`
9687
+ result / result-on-poll), NOT on the workflow **status**
9688
+ read (confirmed ZjVXHkYK / api). `GET /api/workflows/{id}`
9689
+ serialises each operation as `{id, type, status}` plus a
9690
+ conditional `error_message`/`error_code` — it does NOT carry
9691
+ `result.metrics`, so `re_encode_decision` / `re_encode_reason`
9692
+ are not readable from the status poll. Canonical reads for
9693
+ those: the per-job `processing_class` echo (on the status
9694
+ response) for the route, and `GET /api/workflows/{id}/downloads`
9695
+ (per-output `size_bytes`) for the authoritative output size.
9696
+ Note `metrics` has no `output_size_bytes` field — output size
9697
+ lives on `size_bytes` / the downloads endpoint.
7669
9698
  properties:
7670
9699
  compression_ratio:
7671
9700
  type: number
@@ -7706,6 +9735,7 @@ components:
7706
9735
 
7707
9736
  WorkflowDownloadSuccessEnvelope:
7708
9737
  type: object
9738
+ additionalProperties: false
7709
9739
  required:
7710
9740
  - success
7711
9741
  - data
@@ -7759,9 +9789,10 @@ components:
7759
9789
  not: { required: [page_index] }
7760
9790
  - title: Unindexed
7761
9791
  description: |
7762
- Output without an explicit indexing field — every legacy
7763
- single-output download. Schema-valid; emitted by all
7764
- non-fan-out operations.
9792
+ Output without a page/position indexing field — every legacy
9793
+ single-output download AND `render_variants` variant outputs
9794
+ (which carry the optional `target_id` correlation field but are
9795
+ not page/position indexed).
7765
9796
  not:
7766
9797
  anyOf:
7767
9798
  - required: [page_index]
@@ -7804,6 +9835,32 @@ components:
7804
9835
  any current operation; declared for parity with
7805
9836
  `OperationResultOutputEntry.position`. Per ADR-0009 §D2.
7806
9837
  example: 0
9838
+ target_id:
9839
+ type: string
9840
+ pattern: '^[A-Za-z0-9._-]+$'
9841
+ maxLength: 64
9842
+ description: |
9843
+ The caller-assigned `id` of the `render_variants` target that
9844
+ produced this output, echoed verbatim (the image-fan-out
9845
+ addressing contract). Carried on the `Unindexed` branch (variant
9846
+ outputs are not page/position indexed); no operation emits
9847
+ `target_id` together with `page_index` / `position`. Mirrors
9848
+ `OperationResultOutputEntry.target_id`. Per ticket `w3EwzHYd`.
9849
+ example: "thumb-2x"
9850
+ node_id:
9851
+ type: string
9852
+ description: |
9853
+ Symbolic composition `node_id` correlating this download to its
9854
+ canonical node in `WorkflowCreateResponse.composition_plan` (e.g.
9855
+ `encode`, `thumbnail`, `processed_base`). Lets consumers label and
9856
+ group delivered files by composition role (e.g. sdks `byNode()`,
9857
+ FE "Main image" / "Thumbnail" labels). **Optional** — emitted once
9858
+ the canonicalization engine is live (optional-then-promote,
9859
+ mirroring `composition_plan` and `DeliveryPlanOutput.node_id`);
9860
+ absent until then. Additive carrier only; the normative
9861
+ `/downloads == delivery_plan.outputs[]` rendezvous invariant
9862
+ lands with the delivery-selection promotion, not here.
9863
+ example: thumbnail
7807
9864
 
7808
9865
  # ============================================
7809
9866
  # SSE EVENT SCHEMAS
@@ -7892,12 +9949,27 @@ components:
7892
9949
  Total number of inputs expected in this phase. Pairs with
7893
9950
  `phase_input_index`. For single-input operations this is
7894
9951
  `1` during the relevant phase; for multi-input merge /
7895
- archive / image_watermark / custom_luma / audio_overlay,
9952
+ archive / image_watermark / custom_luma / audio_overlay /
9953
+ audio_to_video / video_watermark,
7896
9954
  it matches the inputs[] count. Per ticket I27.
7897
9955
 
7898
9956
  SseOperationCompletedData:
7899
9957
  type: object
7900
- description: Payload for `operation.completed` events
9958
+ description: |
9959
+ Payload for `operation.completed` events.
9960
+
9961
+ **`result` is intentionally OPTIONAL** (not in `required`) — the
9962
+ API does not always emit it on `operation.completed` (confirmed
9963
+ rmP7ndhK / api `Tu78AGpe`):
9964
+ - **Multi-output completions** (e.g. `split` → N outputs): the
9965
+ flat single-output `result` shape can't carry N outputs, so
9966
+ `result` is omitted by design and the outputs are read from
9967
+ the downloads endpoint (`GET /api/workflows/{id}/downloads`).
9968
+ - **Stale-load race**: a transient frame published before the
9969
+ operation resolves against the snapshot may omit `result`;
9970
+ it is repaired by a refetch.
9971
+ Consumers MUST treat `result` as may-be-absent on
9972
+ `operation.completed` and fall back to the downloads endpoint.
7901
9973
  required:
7902
9974
  - job_ref
7903
9975
  - operation_id
@@ -8324,8 +10396,10 @@ components:
8324
10396
  **Caching.** Per-tier private caching with ETag-based revalidation
8325
10397
  (per ADR-0002). Public CDN caching is NOT used because the cache
8326
10398
  key includes the caller's `user_tier`. Clients send
8327
- `If-None-Match` (and/or `If-Modified-Since`) to revalidate; the
8328
- server returns `304 Not Modified` when fresh.
10399
+ `If-None-Match` to revalidate; the server returns `304 Not
10400
+ Modified` when fresh. The content `ETag` is the sole conditional
10401
+ validator — `If-Modified-Since` is not honored and `Last-Modified`
10402
+ is informational only (per `sUyA9ZXD`).
8329
10403
 
8330
10404
  Cache-key composition: `user_tier + schema_version + capabilities_version + environment`.
8331
10405
  required:
@@ -8424,6 +10498,70 @@ components:
8424
10498
  either source.
8425
10499
  additionalProperties:
8426
10500
  $ref: '#/components/schemas/EndpointProjection'
10501
+ workflow_features:
10502
+ type: object
10503
+ description: |
10504
+ Workflow-level option availability projection (ticket
10505
+ [`1ZQjSm0j`](https://trello.com/c/1ZQjSm0j)). Surfaces
10506
+ `per_value_availability` for workflow-level options that live in
10507
+ OpenAPI **component** schemas (not the per-operation
10508
+ `schemas/operations/*.yaml` that drive the `operations` map),
10509
+ keyed by REQUEST-shape path:
10510
+ - `delivery.mode.per_value_availability` (from the `Delivery.mode`
10511
+ schema)
10512
+ - `delivery.selection.type.per_value_availability` (from the
10513
+ `DeliverySelection.type` schema)
10514
+
10515
+ Closes the co0CERtJ (v2.19.0) authority-hierarchy gap: those
10516
+ `planned` downgrades lived only in the (decorative per ADR-0001
10517
+ §1.5) openapi component tags; this surfaces them in the
10518
+ authoritative sidecar.
10519
+
10520
+ **Optional in the wire envelope** (same incremental-mirroring
10521
+ stance as `endpoints`): the committed sidecar emits this block
10522
+ authoritatively; the runtime `GET /api/operations/schema` MAY
10523
+ mirror it once the API generator catches up. Consumers MUST
10524
+ tolerate its absence from the runtime endpoint (read the sidecar)
10525
+ and its presence from either source.
10526
+ properties:
10527
+ delivery:
10528
+ type: object
10529
+ properties:
10530
+ mode:
10531
+ type: object
10532
+ properties:
10533
+ per_value_availability:
10534
+ $ref: '#/components/schemas/PerValueAvailability'
10535
+ selection:
10536
+ type: object
10537
+ properties:
10538
+ type:
10539
+ type: object
10540
+ properties:
10541
+ per_value_availability:
10542
+ $ref: '#/components/schemas/PerValueAvailability'
10543
+ image_encode_capabilities:
10544
+ description: |
10545
+ Pre-flight image-encode capability matrix
10546
+ (`webp_quality_supported`, `background_flatten`, …) — **IDENTICAL
10547
+ shape to `composition_plan.capabilities`** on the create-workflow
10548
+ 201, surfaced here so SDK/FE can fail-fast guard
10549
+ format/quality/alpha-flatten choices BEFORE submitting a workflow.
10550
+ Reuses the same API-side producer so the two can never disagree.
10551
+
10552
+ **Tier-invariant.** This is a fixed codec/hardware capability fact
10553
+ (what the image encoders CAN do), NOT an entitlement — identical
10554
+ for every tier including anonymous. Deliberately carries no
10555
+ `availability` tag, no `per_value_availability`, and does NOT vary
10556
+ by `user_tier` (even though the enclosing response is tier-scoped).
10557
+
10558
+ **Optional in the wire envelope** (same incremental-mirroring
10559
+ stance as `endpoints` / `workflow_features`): the committed sidecar
10560
+ MAY emit it; the runtime `GET /api/operations/schema` emits it once
10561
+ the API generator catches up. Consumers MUST tolerate its absence
10562
+ and its presence from either source. Per ticket
10563
+ [`kybXQe2S`](https://trello.com/c/kybXQe2S).
10564
+ $ref: '#/components/schemas/ImageEncodeCapabilities'
8427
10565
 
8428
10566
  EndpointProjection:
8429
10567
  type: object
@@ -8454,8 +10592,17 @@ components:
8454
10592
  Value of the `x-identity-scoped` vendor extension on the
8455
10593
  operation (default `false`). True iff the operation
8456
10594
  targets an identity-bound resource and cross-identity
8457
- access returns 403 — OR acts on the caller's implicit
10595
+ access is rejected — OR acts on the caller's implicit
8458
10596
  identity-scoped data (credits balance, own session).
10597
+
10598
+ The rejection code is usually `403`, but some endpoints
10599
+ **existence-mask cross-identity references as `404`** (BOLA/
10600
+ IDOR: a 403 would leak that the resource exists). The
10601
+ `createWorkflow` upload reference is the canonical case —
10602
+ cross-owner uploads return `404 UPLOAD_NOT_FOUND`, never 403
10603
+ (per the ADR-0016 amendment). Consumers MUST NOT assume
10604
+ `identity_scoped: true` implies a 403 specifically; read the
10605
+ per-operation error responses for the exact code.
8459
10606
  required_tier:
8460
10607
  description: |
8461
10608
  Endpoint-level entitlement gate. Reserved/null today —
@@ -8542,9 +10689,10 @@ components:
8542
10689
  Optional per-role input-count overlay for role-based
8543
10690
  multi-input operations. Per ticket
8544
10691
  [`SlluxMBN`](https://trello.com/c/SlluxMBN) / ADR-0015.
8545
- Absent on operations whose role rules are still encoded
8546
- in prose only (image_watermark, audio_overlay,
8547
- custom_luma per-op migration follow-ups).
10692
+ Present on all five role-based ops (audio_to_video,
10693
+ video_watermark, audio_overlay, image_watermark,
10694
+ custom_luma); absent on non-role ops (merge, archive)
10695
+ where all inputs share a single role.
8548
10696
  accepts_mixed_types:
8549
10697
  type: boolean
8550
10698
  description: Whether mixed MIME types are allowed (archive only)
@@ -8628,6 +10776,37 @@ components:
8628
10776
  [I3](https://trello.com/c/eCWIpug8); until then the
8629
10777
  contract advertises the field shape but the endpoint
8630
10778
  does not yet surface the field.
10779
+ processing_class:
10780
+ type: object
10781
+ description: |
10782
+ Optional per-mime-group processing-class block (per ticket
10783
+ [I15-CONS `YZpBKzOM`](https://trello.com/c/YZpBKzOM) + plan
10784
+ v5 §F8 long-form routing). Keyed by `ProcessingClass` enum
10785
+ value (`short_form` / `long_form` / `short_form_concat` /
10786
+ `long_form_re_encode`); each entry advertises that routing
10787
+ class's availability, tier gate, and size/duration caps.
10788
+
10789
+ Present only on mime_groups subject to short-form vs
10790
+ long-form routing — typically `video`; per F8 also `audio`
10791
+ for `audio_overlay` / `audio_watermark`. Absence means "no
10792
+ per-class routing exposed": consumers MUST treat absent as
10793
+ "no routing decision exposed" and MUST NOT coerce it to
10794
+ `short_form` (FORMAT.md §`processing_class` parser
10795
+ obligation 2 / ADR-0001 §1.4).
10796
+
10797
+ The runtime endpoint (`GET /api/operations/schema`) and the
10798
+ `availability/availability.json` sidecar emit this block
10799
+ verbatim from the source operation YAML (including any
10800
+ `per_tier_constraints` overlay — copied through as an
10801
+ opaque subtree). SDK + frontend consumers gate UI on the
10802
+ per-class `availability` + `required_tier` and surface the
10803
+ caller's effective caps as the binding limits. Surfacing
10804
+ this on the typed model retires the raw-ops-schema HTTP
10805
+ workaround per ticket
10806
+ [`yWeBr81O`](https://trello.com/c/yWeBr81O). See
10807
+ `schemas/FORMAT.md` §`processing_class:` block.
10808
+ additionalProperties:
10809
+ $ref: '#/components/schemas/ProcessingClassEntry'
8631
10810
  options:
8632
10811
  type: object
8633
10812
  description: Options specific to this MIME group, keyed by option name
@@ -8731,6 +10910,104 @@ components:
8731
10910
  The "set" sentinel means the option has any value. "logic" can be "and" (default) or "or".
8732
10911
  additionalProperties: true
8733
10912
 
10913
+ ProcessingClassConstraints:
10914
+ type: object
10915
+ additionalProperties: false
10916
+ description: |
10917
+ Numeric size / duration caps for a single processing class (or
10918
+ a per-tier override of those caps). All fields optional; the
10919
+ `max_input_*` keys apply to single-input mime_groups while the
10920
+ `max_total_*` keys apply to multi-input merge-style mime_groups
10921
+ (a merge can have many small inputs whose combined size triggers
10922
+ long-form). Byte caps are positive integers; duration caps are
10923
+ ISO-8601 duration strings (e.g. `PT5M`). Field set is CI-fixed —
10924
+ `scripts/check-per-tier-constraints.py` rejects unknown keys and
10925
+ bad value types. See `schemas/FORMAT.md` §`constraints` sub-block.
10926
+ properties:
10927
+ max_input_duration:
10928
+ type: string
10929
+ description: |
10930
+ Max per-input duration as an ISO-8601 duration string.
10931
+ Single-input mime_groups.
10932
+ example: "PT5M"
10933
+ max_input_size_bytes:
10934
+ type: integer
10935
+ minimum: 1
10936
+ description: Max per-input file size in bytes. Single-input mime_groups.
10937
+ max_output_size_bytes:
10938
+ type: integer
10939
+ minimum: 1
10940
+ description: Max output file size in bytes. Any mime_group.
10941
+ max_total_duration:
10942
+ type: string
10943
+ description: |
10944
+ Max combined duration across all inputs as an ISO-8601
10945
+ duration string. Multi-input (merge) mime_groups.
10946
+ example: "PT1H"
10947
+ max_total_input_size_bytes:
10948
+ type: integer
10949
+ minimum: 1
10950
+ description: Max combined input size in bytes. Multi-input (merge) mime_groups.
10951
+
10952
+ ProcessingClassEntry:
10953
+ type: object
10954
+ description: |
10955
+ Single processing-class entry within a mime_group's
10956
+ `processing_class` map (per ticket
10957
+ [I15-CONS `YZpBKzOM`](https://trello.com/c/YZpBKzOM)). Mirrors
10958
+ `PerValueAvailabilityEntry` (availability + optional tier / eta /
10959
+ documentation_url) plus per-class `constraints` and an optional
10960
+ `per_tier_constraints` overlay. Surfaced on the typed getSchema
10961
+ model per ticket [`yWeBr81O`](https://trello.com/c/yWeBr81O).
10962
+ See `schemas/FORMAT.md` §`processing_class:` block.
10963
+ required:
10964
+ - availability
10965
+ properties:
10966
+ availability:
10967
+ $ref: '#/components/schemas/AvailabilityValue'
10968
+ description: Per-class availability tag.
10969
+ required_tier:
10970
+ description: |
10971
+ Tier required to use this class. Optional — omit when the
10972
+ class is gated by readiness rather than subscription.
10973
+ `null` is equivalent to omitted (no tier restriction).
10974
+ oneOf:
10975
+ - $ref: '#/components/schemas/UserTier'
10976
+ - type: 'null'
10977
+ eta:
10978
+ type: string
10979
+ description: |
10980
+ ISO-8601 date (`2026-09-15`) or quarter (`2026-Q3`) when
10981
+ this class is expected to ship. Only meaningful for
10982
+ `availability: planned`.
10983
+ documentation_url:
10984
+ type: string
10985
+ format: uri
10986
+ description: Optional link to processing-class documentation.
10987
+ constraints:
10988
+ $ref: '#/components/schemas/ProcessingClassConstraints'
10989
+ description: |
10990
+ Baseline caps for the lowest tier this class's
10991
+ `required_tier` permits. Overlaid per-caller by
10992
+ `per_tier_constraints` (see below).
10993
+ per_tier_constraints:
10994
+ type: object
10995
+ description: |
10996
+ Optional per-tier numeric-cap override map (per
10997
+ [ADR-0011](../docs/decisions/0011-per-tier-processing-class-constraints.md),
10998
+ ticket [`z4GDTUMx`](https://trello.com/c/z4GDTUMx)). Keys
10999
+ MUST be a subset of the `UserTier` enum and SHOULD name only
11000
+ tiers HIGHER than the class baseline (CI-enforced by
11001
+ `scripts/check-per-tier-constraints.py`). Each value
11002
+ overrides `constraints` field-by-field for callers of that
11003
+ tier. A consumer that ignores this map reads `constraints`
11004
+ and sees the smallest permitted-tier cap — it can never
11005
+ over-promise. `per_tier_constraints` sizes caps, it does
11006
+ NOT grant access (eligibility stays governed by
11007
+ `availability` + `required_tier`).
11008
+ additionalProperties:
11009
+ $ref: '#/components/schemas/ProcessingClassConstraints'
11010
+
8734
11011
  # ============================================
8735
11012
  # RETRY SCHEMAS
8736
11013
  # ============================================
@@ -8756,6 +11033,7 @@ components:
8756
11033
 
8757
11034
  RetrySuccessEnvelope:
8758
11035
  type: object
11036
+ additionalProperties: false
8759
11037
  required:
8760
11038
  - success
8761
11039
  - data