@giveitsmaller/contracts 0.9.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 (443) hide show
  1. package/asyncapi/events.yaml +540 -42
  2. package/availability/availability.json +313 -89
  3. package/dist/asyncapi/Failure.d.ts +2 -0
  4. package/dist/asyncapi/MultiOutputCompletion.d.ts +2 -0
  5. package/dist/asyncapi/{NotificationsOperationsQueue.d.ts → NotificationsJobsQueue.d.ts} +2 -2
  6. package/dist/asyncapi/OperationResultMetadata.d.ts +4 -0
  7. package/dist/asyncapi/OperationResultMetadata.js +1 -0
  8. package/dist/asyncapi/OperationType.d.ts +2 -1
  9. package/dist/asyncapi/OperationType.js +1 -0
  10. package/dist/asyncapi/PageIndexed.d.ts +1 -0
  11. package/dist/asyncapi/PositionIndexed.d.ts +1 -0
  12. package/dist/asyncapi/SingleOutputCompletion.d.ts +2 -0
  13. package/dist/asyncapi/SourceEntry.d.ts +2 -0
  14. package/dist/asyncapi/Unindexed.d.ts +1 -0
  15. package/dist/asyncapi/index.d.ts +2 -1
  16. package/dist/openapi/models/AudioWatermarkDecodeRequest.d.ts +2 -2
  17. package/dist/openapi/models/AudioWatermarkDecodeRequest.js +2 -2
  18. package/dist/openapi/models/AudioWatermarkDecodeResponse.d.ts +2 -2
  19. package/dist/openapi/models/AudioWatermarkDecodeResponse.js +2 -2
  20. package/dist/openapi/models/AuthErrorResponse.d.ts +13 -2
  21. package/dist/openapi/models/AuthErrorResponse.js +2 -2
  22. package/dist/openapi/models/AuthErrorType.d.ts +2 -2
  23. package/dist/openapi/models/AuthErrorType.js +2 -2
  24. package/dist/openapi/models/AuthRejectionEnvelope.d.ts +126 -0
  25. package/dist/openapi/models/AuthRejectionEnvelope.js +72 -0
  26. package/dist/openapi/models/AvailabilityValue.d.ts +2 -2
  27. package/dist/openapi/models/AvailabilityValue.js +2 -2
  28. package/dist/openapi/models/BalanceExhaustedResponse.d.ts +13 -2
  29. package/dist/openapi/models/BalanceExhaustedResponse.js +2 -2
  30. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.d.ts +2 -2
  31. package/dist/openapi/models/BalanceExhaustedResponseAllOfLinks.js +2 -2
  32. package/dist/openapi/models/CallbackEventType.d.ts +2 -2
  33. package/dist/openapi/models/CallbackEventType.js +2 -2
  34. package/dist/openapi/models/ChangePasswordRequest.d.ts +38 -0
  35. package/dist/openapi/models/ChangePasswordRequest.js +47 -0
  36. package/dist/openapi/models/CompositionPlan.d.ts +72 -0
  37. package/dist/openapi/models/CompositionPlan.js +53 -0
  38. package/dist/openapi/models/CompositionPlanJob.d.ts +39 -0
  39. package/dist/openapi/models/CompositionPlanJob.js +48 -0
  40. package/dist/openapi/models/CompositionPlanOperation.d.ts +116 -0
  41. package/dist/openapi/models/CompositionPlanOperation.js +62 -0
  42. package/dist/openapi/models/ConfirmEmailChange200Response.d.ts +46 -0
  43. package/dist/openapi/models/ConfirmEmailChange200Response.js +54 -0
  44. package/dist/openapi/models/ConfirmEmailChange200ResponseData.d.ts +32 -0
  45. package/dist/openapi/models/ConfirmEmailChange200ResponseData.js +43 -0
  46. package/dist/openapi/models/ConfirmEmailChangeRequest.d.ts +32 -0
  47. package/dist/openapi/models/ConfirmEmailChangeRequest.js +43 -0
  48. package/dist/openapi/models/ConnectionSource.d.ts +2 -2
  49. package/dist/openapi/models/ConnectionSource.js +2 -2
  50. package/dist/openapi/models/ContactRequest.d.ts +2 -2
  51. package/dist/openapi/models/ContactRequest.js +2 -2
  52. package/dist/openapi/models/ContactSubject.d.ts +2 -2
  53. package/dist/openapi/models/ContactSubject.js +2 -2
  54. package/dist/openapi/models/ContactValidationErrorResponse.d.ts +2 -2
  55. package/dist/openapi/models/ContactValidationErrorResponse.js +2 -2
  56. package/dist/openapi/models/CreateApiKey201Response.d.ts +46 -0
  57. package/dist/openapi/models/CreateApiKey201Response.js +54 -0
  58. package/dist/openapi/models/CreateApiKey201ResponseData.d.ts +56 -0
  59. package/dist/openapi/models/CreateApiKey201ResponseData.js +59 -0
  60. package/dist/openapi/models/CreateApiKeyRequest.d.ts +32 -0
  61. package/dist/openapi/models/CreateApiKeyRequest.js +43 -0
  62. package/dist/openapi/models/CreateExternalImport403Response.d.ts +2 -2
  63. package/dist/openapi/models/CreateExternalImport403Response.js +2 -2
  64. package/dist/openapi/models/CreateExternalImport422Response.d.ts +2 -2
  65. package/dist/openapi/models/CreateExternalImport422Response.js +2 -2
  66. package/dist/openapi/models/CreateWorkflow422Response.d.ts +2 -2
  67. package/dist/openapi/models/CreateWorkflow422Response.js +2 -2
  68. package/dist/openapi/models/CreditTransaction.d.ts +2 -2
  69. package/dist/openapi/models/CreditTransaction.js +2 -2
  70. package/dist/openapi/models/CreditTransactionSourceBucket.d.ts +2 -2
  71. package/dist/openapi/models/CreditTransactionSourceBucket.js +2 -2
  72. package/dist/openapi/models/CreditsBalanceResponse.d.ts +2 -2
  73. package/dist/openapi/models/CreditsBalanceResponse.js +2 -2
  74. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.d.ts +2 -2
  75. package/dist/openapi/models/CreditsBalanceSuccessEnvelope.js +2 -2
  76. package/dist/openapi/models/CreditsUsageResponse.d.ts +2 -2
  77. package/dist/openapi/models/CreditsUsageResponse.js +2 -2
  78. package/dist/openapi/models/CreditsUsageSuccessEnvelope.d.ts +2 -2
  79. package/dist/openapi/models/CreditsUsageSuccessEnvelope.js +2 -2
  80. package/dist/openapi/models/Delivery.d.ts +2 -2
  81. package/dist/openapi/models/Delivery.js +2 -2
  82. package/dist/openapi/models/DeliveryOutputRef.d.ts +9 -2
  83. package/dist/openapi/models/DeliveryOutputRef.js +2 -2
  84. package/dist/openapi/models/DeliveryPlan.d.ts +2 -2
  85. package/dist/openapi/models/DeliveryPlan.js +2 -2
  86. package/dist/openapi/models/DeliveryPlanOutput.d.ts +17 -2
  87. package/dist/openapi/models/DeliveryPlanOutput.js +4 -2
  88. package/dist/openapi/models/DeliveryPlanReason.d.ts +2 -2
  89. package/dist/openapi/models/DeliveryPlanReason.js +2 -2
  90. package/dist/openapi/models/DeliverySelection.d.ts +6 -4
  91. package/dist/openapi/models/DeliverySelection.js +2 -2
  92. package/dist/openapi/models/EmptySuccessEnvelope.d.ts +58 -0
  93. package/dist/openapi/models/EmptySuccessEnvelope.js +53 -0
  94. package/dist/openapi/models/EndpointProjection.d.ts +12 -3
  95. package/dist/openapi/models/EndpointProjection.js +2 -2
  96. package/dist/openapi/models/ErrorEnvelope.d.ts +13 -2
  97. package/dist/openapi/models/ErrorEnvelope.js +2 -2
  98. package/dist/openapi/models/EstimateQuality.d.ts +2 -2
  99. package/dist/openapi/models/EstimateQuality.js +2 -2
  100. package/dist/openapi/models/EstimateRange.d.ts +2 -2
  101. package/dist/openapi/models/EstimateRange.js +2 -2
  102. package/dist/openapi/models/ExternalDestination.d.ts +2 -2
  103. package/dist/openapi/models/ExternalDestination.js +2 -2
  104. package/dist/openapi/models/ExternalImportCreatedResponse.d.ts +2 -2
  105. package/dist/openapi/models/ExternalImportCreatedResponse.js +2 -2
  106. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.d.ts +2 -2
  107. package/dist/openapi/models/ExternalImportCreatedSuccessEnvelope.js +2 -2
  108. package/dist/openapi/models/ExternalImportRequest.d.ts +2 -2
  109. package/dist/openapi/models/ExternalImportRequest.js +2 -2
  110. package/dist/openapi/models/ExternalImportToken.d.ts +2 -2
  111. package/dist/openapi/models/ExternalImportToken.js +2 -2
  112. package/dist/openapi/models/ExternalSource.d.ts +2 -2
  113. package/dist/openapi/models/ExternalSource.js +2 -2
  114. package/dist/openapi/models/FeatureNotAvailableResponse.d.ts +13 -2
  115. package/dist/openapi/models/FeatureNotAvailableResponse.js +2 -2
  116. package/dist/openapi/models/FeatureTierRestrictedResponse.d.ts +13 -2
  117. package/dist/openapi/models/FeatureTierRestrictedResponse.js +2 -2
  118. package/dist/openapi/models/FeatureViolation.d.ts +2 -2
  119. package/dist/openapi/models/FeatureViolation.js +2 -2
  120. package/dist/openapi/models/ForgotPasswordRequest.d.ts +32 -0
  121. package/dist/openapi/models/ForgotPasswordRequest.js +43 -0
  122. package/dist/openapi/models/ImageEncodeCapabilities.d.ts +65 -0
  123. package/dist/openapi/models/ImageEncodeCapabilities.js +55 -0
  124. package/dist/openapi/models/JobDefinition.d.ts +20 -4
  125. package/dist/openapi/models/JobDefinition.js +2 -2
  126. package/dist/openapi/models/JobDownload.d.ts +2 -2
  127. package/dist/openapi/models/JobDownload.js +2 -2
  128. package/dist/openapi/models/JobInputV2.d.ts +13 -9
  129. package/dist/openapi/models/JobInputV2.js +5 -5
  130. package/dist/openapi/models/JobMediaClass.d.ts +34 -0
  131. package/dist/openapi/models/JobMediaClass.js +52 -0
  132. package/dist/openapi/models/JobOutputSource.d.ts +2 -2
  133. package/dist/openapi/models/JobOutputSource.js +2 -2
  134. package/dist/openapi/models/JobResponse.d.ts +2 -2
  135. package/dist/openapi/models/JobResponse.js +2 -2
  136. package/dist/openapi/models/JobStatus.d.ts +2 -2
  137. package/dist/openapi/models/JobStatus.js +2 -2
  138. package/dist/openapi/models/JobType.d.ts +2 -2
  139. package/dist/openapi/models/JobType.js +2 -2
  140. package/dist/openapi/models/LivenessResponse.d.ts +2 -2
  141. package/dist/openapi/models/LivenessResponse.js +2 -2
  142. package/dist/openapi/models/LoginUser200Response.d.ts +2 -2
  143. package/dist/openapi/models/LoginUser200Response.js +2 -2
  144. package/dist/openapi/models/LoginUser200ResponseData.d.ts +2 -2
  145. package/dist/openapi/models/LoginUser200ResponseData.js +2 -2
  146. package/dist/openapi/models/LoginUser200ResponseDataUser.d.ts +2 -2
  147. package/dist/openapi/models/LoginUser200ResponseDataUser.js +2 -2
  148. package/dist/openapi/models/LoginUserRequest.d.ts +2 -2
  149. package/dist/openapi/models/LoginUserRequest.js +2 -2
  150. package/dist/openapi/models/MetadataResponse.d.ts +2 -2
  151. package/dist/openapi/models/MetadataResponse.js +2 -2
  152. package/dist/openapi/models/MetadataResponseDimensions.d.ts +2 -2
  153. package/dist/openapi/models/MetadataResponseDimensions.js +2 -2
  154. package/dist/openapi/models/MetadataResponseExif.d.ts +2 -2
  155. package/dist/openapi/models/MetadataResponseExif.js +2 -2
  156. package/dist/openapi/models/MetadataResponseExifGps.d.ts +2 -2
  157. package/dist/openapi/models/MetadataResponseExifGps.js +2 -2
  158. package/dist/openapi/models/MetadataSuccessEnvelope.d.ts +2 -2
  159. package/dist/openapi/models/MetadataSuccessEnvelope.js +2 -2
  160. package/dist/openapi/models/MimeGroupSchema.d.ts +37 -2
  161. package/dist/openapi/models/MimeGroupSchema.js +5 -2
  162. package/dist/openapi/models/MultiInputSource.d.ts +41 -0
  163. package/dist/openapi/models/MultiInputSource.js +52 -0
  164. package/dist/openapi/models/MultipartCompleteRequest.d.ts +2 -2
  165. package/dist/openapi/models/MultipartCompleteRequest.js +2 -2
  166. package/dist/openapi/models/MultipartCompleteRequestPartsInner.d.ts +2 -2
  167. package/dist/openapi/models/MultipartCompleteRequestPartsInner.js +2 -2
  168. package/dist/openapi/models/MultipartCompleteResponse.d.ts +2 -2
  169. package/dist/openapi/models/MultipartCompleteResponse.js +2 -2
  170. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.d.ts +2 -2
  171. package/dist/openapi/models/MultipartCompleteSuccessEnvelope.js +2 -2
  172. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.d.ts +2 -2
  173. package/dist/openapi/models/MultipartInitiateRequestMetadataHint.js +2 -2
  174. package/dist/openapi/models/MultipartInitiateResponse.d.ts +2 -2
  175. package/dist/openapi/models/MultipartInitiateResponse.js +2 -2
  176. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.d.ts +2 -2
  177. package/dist/openapi/models/MultipartInitiateSuccessEnvelope.js +2 -2
  178. package/dist/openapi/models/MultipartKeepaliveResponse.d.ts +2 -2
  179. package/dist/openapi/models/MultipartKeepaliveResponse.js +2 -2
  180. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.d.ts +2 -2
  181. package/dist/openapi/models/MultipartKeepaliveSuccessEnvelope.js +2 -2
  182. package/dist/openapi/models/MultipartPartListing.d.ts +2 -2
  183. package/dist/openapi/models/MultipartPartListing.js +2 -2
  184. package/dist/openapi/models/MultipartPresignRequest.d.ts +2 -2
  185. package/dist/openapi/models/MultipartPresignRequest.js +2 -2
  186. package/dist/openapi/models/MultipartPresignResponse.d.ts +2 -2
  187. package/dist/openapi/models/MultipartPresignResponse.js +2 -2
  188. package/dist/openapi/models/MultipartPresignSuccessEnvelope.d.ts +2 -2
  189. package/dist/openapi/models/MultipartPresignSuccessEnvelope.js +2 -2
  190. package/dist/openapi/models/MultipartStatusResponse.d.ts +2 -2
  191. package/dist/openapi/models/MultipartStatusResponse.js +2 -2
  192. package/dist/openapi/models/MultipartStatusSuccessEnvelope.d.ts +2 -2
  193. package/dist/openapi/models/MultipartStatusSuccessEnvelope.js +2 -2
  194. package/dist/openapi/models/OperationDefinition.d.ts +27 -2
  195. package/dist/openapi/models/OperationDefinition.js +11 -2
  196. package/dist/openapi/models/OperationDownload.d.ts +30 -2
  197. package/dist/openapi/models/OperationDownload.js +6 -2
  198. package/dist/openapi/models/OperationInputModel.d.ts +3 -3
  199. package/dist/openapi/models/OperationInputModel.js +3 -3
  200. package/dist/openapi/models/OperationResponse.d.ts +18 -2
  201. package/dist/openapi/models/OperationResponse.js +5 -2
  202. package/dist/openapi/models/OperationResult.d.ts +2 -2
  203. package/dist/openapi/models/OperationResult.js +2 -2
  204. package/dist/openapi/models/OperationResultMetadata.d.ts +48 -0
  205. package/dist/openapi/models/OperationResultMetadata.js +41 -0
  206. package/dist/openapi/models/OperationResultMetrics.d.ts +16 -3
  207. package/dist/openapi/models/OperationResultMetrics.js +2 -2
  208. package/dist/openapi/models/OperationSchemaDefinition.d.ts +6 -5
  209. package/dist/openapi/models/OperationSchemaDefinition.js +2 -2
  210. package/dist/openapi/models/OperationStatus.d.ts +2 -2
  211. package/dist/openapi/models/OperationStatus.js +2 -2
  212. package/dist/openapi/models/OperationType.d.ts +7 -4
  213. package/dist/openapi/models/OperationType.js +8 -5
  214. package/dist/openapi/models/OperationsSchemaResponse.d.ts +32 -4
  215. package/dist/openapi/models/OperationsSchemaResponse.js +5 -2
  216. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.d.ts +2 -2
  217. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeatures.js +2 -2
  218. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.d.ts +2 -2
  219. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDelivery.js +2 -2
  220. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.d.ts +2 -2
  221. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliveryMode.js +2 -2
  222. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.d.ts +2 -2
  223. package/dist/openapi/models/OperationsSchemaResponseWorkflowFeaturesDeliverySelection.js +2 -2
  224. package/dist/openapi/models/OptionSchema.d.ts +2 -2
  225. package/dist/openapi/models/OptionSchema.js +2 -2
  226. package/dist/openapi/models/PerRoleCardinalityEntry.d.ts +2 -2
  227. package/dist/openapi/models/PerRoleCardinalityEntry.js +2 -2
  228. package/dist/openapi/models/PerValueAvailabilityEntry.d.ts +2 -2
  229. package/dist/openapi/models/PerValueAvailabilityEntry.js +2 -2
  230. package/dist/openapi/models/PresignedUrlPart.d.ts +2 -2
  231. package/dist/openapi/models/PresignedUrlPart.js +2 -2
  232. package/dist/openapi/models/ProbePendingResponse.d.ts +13 -2
  233. package/dist/openapi/models/ProbePendingResponse.js +2 -2
  234. package/dist/openapi/models/ProcessingClass.d.ts +2 -2
  235. package/dist/openapi/models/ProcessingClass.js +2 -2
  236. package/dist/openapi/models/ProcessingClassBandViolation.d.ts +2 -2
  237. package/dist/openapi/models/ProcessingClassBandViolation.js +2 -2
  238. package/dist/openapi/models/ProcessingClassConstraints.d.ts +69 -0
  239. package/dist/openapi/models/ProcessingClassConstraints.js +49 -0
  240. package/dist/openapi/models/ProcessingClassEntry.d.ts +93 -0
  241. package/dist/openapi/models/ProcessingClassEntry.js +57 -0
  242. package/dist/openapi/models/ProcessingClassExceedsBandResponse.d.ts +13 -2
  243. package/dist/openapi/models/ProcessingClassExceedsBandResponse.js +2 -2
  244. package/dist/openapi/models/ProcessingClassHint.d.ts +2 -2
  245. package/dist/openapi/models/ProcessingClassHint.js +2 -2
  246. package/dist/openapi/models/ProcessingClassReason.d.ts +2 -2
  247. package/dist/openapi/models/ProcessingClassReason.js +2 -2
  248. package/dist/openapi/models/ProcessingClassRejectReason.d.ts +2 -2
  249. package/dist/openapi/models/ProcessingClassRejectReason.js +2 -2
  250. package/dist/openapi/models/ProcessingPlan.d.ts +2 -2
  251. package/dist/openapi/models/ProcessingPlan.js +2 -2
  252. package/dist/openapi/models/ProcessingPlanJob.d.ts +2 -2
  253. package/dist/openapi/models/ProcessingPlanJob.js +2 -2
  254. package/dist/openapi/models/ReEncodeDecision.d.ts +2 -2
  255. package/dist/openapi/models/ReEncodeDecision.js +2 -2
  256. package/dist/openapi/models/ReadinessResponse.d.ts +2 -2
  257. package/dist/openapi/models/ReadinessResponse.js +2 -2
  258. package/dist/openapi/models/RegisterUser422Response.d.ts +27 -0
  259. package/dist/openapi/models/RegisterUser422Response.js +47 -0
  260. package/dist/openapi/models/RegisterUserRequest.d.ts +38 -0
  261. package/dist/openapi/models/RegisterUserRequest.js +47 -0
  262. package/dist/openapi/models/ResetPasswordRequest.d.ts +38 -0
  263. package/dist/openapi/models/ResetPasswordRequest.js +47 -0
  264. package/dist/openapi/models/ResponseEnvelope.d.ts +2 -2
  265. package/dist/openapi/models/ResponseEnvelope.js +2 -2
  266. package/dist/openapi/models/RetryResponse.d.ts +2 -2
  267. package/dist/openapi/models/RetryResponse.js +2 -2
  268. package/dist/openapi/models/RetrySuccessEnvelope.d.ts +2 -2
  269. package/dist/openapi/models/RetrySuccessEnvelope.js +2 -2
  270. package/dist/openapi/models/SseCompletionBase.d.ts +2 -2
  271. package/dist/openapi/models/SseCompletionBase.js +2 -2
  272. package/dist/openapi/models/SseEventType.d.ts +2 -2
  273. package/dist/openapi/models/SseEventType.js +2 -2
  274. package/dist/openapi/models/SseJobCompletedData.d.ts +2 -2
  275. package/dist/openapi/models/SseJobCompletedData.js +2 -2
  276. package/dist/openapi/models/SseJobFailedData.d.ts +2 -2
  277. package/dist/openapi/models/SseJobFailedData.js +2 -2
  278. package/dist/openapi/models/SseMultiOutputCompletion.d.ts +2 -2
  279. package/dist/openapi/models/SseMultiOutputCompletion.js +2 -2
  280. package/dist/openapi/models/SseMultiOutputCompletionMetrics.d.ts +2 -2
  281. package/dist/openapi/models/SseMultiOutputCompletionMetrics.js +2 -2
  282. package/dist/openapi/models/SseMultiOutputCompletionWithKind.d.ts +2 -2
  283. package/dist/openapi/models/SseMultiOutputCompletionWithKind.js +2 -2
  284. package/dist/openapi/models/SseMultiOutputResultEntry.d.ts +2 -2
  285. package/dist/openapi/models/SseMultiOutputResultEntry.js +2 -2
  286. package/dist/openapi/models/SseOperationCompletedData.d.ts +17 -3
  287. package/dist/openapi/models/SseOperationCompletedData.js +2 -2
  288. package/dist/openapi/models/SseOperationCompletionResult.d.ts +2 -2
  289. package/dist/openapi/models/SseOperationCompletionResult.js +2 -2
  290. package/dist/openapi/models/SseOperationFailedData.d.ts +2 -2
  291. package/dist/openapi/models/SseOperationFailedData.js +2 -2
  292. package/dist/openapi/models/SseOperationProgressData.d.ts +2 -2
  293. package/dist/openapi/models/SseOperationProgressData.js +2 -2
  294. package/dist/openapi/models/SseSingleOutputCompletion.d.ts +2 -2
  295. package/dist/openapi/models/SseSingleOutputCompletion.js +2 -2
  296. package/dist/openapi/models/SseWorkflowTerminalData.d.ts +2 -2
  297. package/dist/openapi/models/SseWorkflowTerminalData.js +2 -2
  298. package/dist/openapi/models/TierRestrictionKind.d.ts +2 -2
  299. package/dist/openapi/models/TierRestrictionKind.js +2 -2
  300. package/dist/openapi/models/TierRestrictionResponse.d.ts +13 -2
  301. package/dist/openapi/models/TierRestrictionResponse.js +2 -2
  302. package/dist/openapi/models/UpdateProfile200Response.d.ts +46 -0
  303. package/dist/openapi/models/UpdateProfile200Response.js +54 -0
  304. package/dist/openapi/models/UpdateProfile200ResponseData.d.ts +71 -0
  305. package/dist/openapi/models/UpdateProfile200ResponseData.js +67 -0
  306. package/dist/openapi/models/UpdateProfile422Response.d.ts +27 -0
  307. package/dist/openapi/models/UpdateProfile422Response.js +47 -0
  308. package/dist/openapi/models/UpdateProfileRequest.d.ts +44 -0
  309. package/dist/openapi/models/UpdateProfileRequest.js +47 -0
  310. package/dist/openapi/models/UploadConstraintsApplied.d.ts +2 -2
  311. package/dist/openapi/models/UploadConstraintsApplied.js +2 -2
  312. package/dist/openapi/models/UploadDurationExceedsTierResponse.d.ts +13 -2
  313. package/dist/openapi/models/UploadDurationExceedsTierResponse.js +2 -2
  314. package/dist/openapi/models/UploadFile403Response.d.ts +2 -2
  315. package/dist/openapi/models/UploadFile403Response.js +2 -2
  316. package/dist/openapi/models/UploadFile422Response.d.ts +2 -2
  317. package/dist/openapi/models/UploadFile422Response.js +2 -2
  318. package/dist/openapi/models/UploadProbeMediaMetadata.d.ts +2 -2
  319. package/dist/openapi/models/UploadProbeMediaMetadata.js +2 -2
  320. package/dist/openapi/models/UploadProbeProcessingClass.d.ts +2 -2
  321. package/dist/openapi/models/UploadProbeProcessingClass.js +2 -2
  322. package/dist/openapi/models/UploadProbeResponse.d.ts +2 -2
  323. package/dist/openapi/models/UploadProbeResponse.js +2 -2
  324. package/dist/openapi/models/UploadProbeStatus.d.ts +2 -2
  325. package/dist/openapi/models/UploadProbeStatus.js +2 -2
  326. package/dist/openapi/models/UploadProbeSuccessEnvelope.d.ts +2 -2
  327. package/dist/openapi/models/UploadProbeSuccessEnvelope.js +2 -2
  328. package/dist/openapi/models/UploadResponse.d.ts +2 -2
  329. package/dist/openapi/models/UploadResponse.js +2 -2
  330. package/dist/openapi/models/UploadSizeExceedsTierResponse.d.ts +13 -2
  331. package/dist/openapi/models/UploadSizeExceedsTierResponse.js +2 -2
  332. package/dist/openapi/models/UploadSource.d.ts +2 -2
  333. package/dist/openapi/models/UploadSource.js +2 -2
  334. package/dist/openapi/models/UploadSuccessEnvelope.d.ts +2 -2
  335. package/dist/openapi/models/UploadSuccessEnvelope.js +2 -2
  336. package/dist/openapi/models/UploadThresholds.d.ts +2 -2
  337. package/dist/openapi/models/UploadThresholds.js +2 -2
  338. package/dist/openapi/models/UserTier.d.ts +2 -2
  339. package/dist/openapi/models/UserTier.js +2 -2
  340. package/dist/openapi/models/ValidationErrorEnvelope.d.ts +2 -2
  341. package/dist/openapi/models/ValidationErrorEnvelope.js +2 -2
  342. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.d.ts +2 -2
  343. package/dist/openapi/models/ValidationErrorEnvelopeDetailsInner.js +2 -2
  344. package/dist/openapi/models/VerifyEmailRequest.d.ts +32 -0
  345. package/dist/openapi/models/VerifyEmailRequest.js +43 -0
  346. package/dist/openapi/models/WarningType.d.ts +2 -2
  347. package/dist/openapi/models/WarningType.js +2 -2
  348. package/dist/openapi/models/WebhookOperationContext.d.ts +2 -2
  349. package/dist/openapi/models/WebhookOperationContext.js +2 -2
  350. package/dist/openapi/models/WebhookPayload.d.ts +2 -2
  351. package/dist/openapi/models/WebhookPayload.js +2 -2
  352. package/dist/openapi/models/WorkflowCancelBillingEffect.d.ts +2 -2
  353. package/dist/openapi/models/WorkflowCancelBillingEffect.js +2 -2
  354. package/dist/openapi/models/WorkflowCancelResponse.d.ts +2 -2
  355. package/dist/openapi/models/WorkflowCancelResponse.js +2 -2
  356. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.d.ts +2 -2
  357. package/dist/openapi/models/WorkflowCancelSuccessEnvelope.js +2 -2
  358. package/dist/openapi/models/WorkflowCreateRequest.d.ts +64 -4
  359. package/dist/openapi/models/WorkflowCreateRequest.js +10 -6
  360. package/dist/openapi/models/WorkflowCreateResponse.d.ts +24 -2
  361. package/dist/openapi/models/WorkflowCreateResponse.js +5 -2
  362. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.d.ts +2 -2
  363. package/dist/openapi/models/WorkflowCreateSuccessEnvelope.js +2 -2
  364. package/dist/openapi/models/WorkflowDownloadResponse.d.ts +2 -2
  365. package/dist/openapi/models/WorkflowDownloadResponse.js +2 -2
  366. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.d.ts +2 -2
  367. package/dist/openapi/models/WorkflowDownloadSuccessEnvelope.js +2 -2
  368. package/dist/openapi/models/WorkflowEdge.d.ts +2 -2
  369. package/dist/openapi/models/WorkflowEdge.js +2 -2
  370. package/dist/openapi/models/WorkflowExpiredResponse.d.ts +13 -2
  371. package/dist/openapi/models/WorkflowExpiredResponse.js +2 -2
  372. package/dist/openapi/models/WorkflowListResponse.d.ts +50 -0
  373. package/dist/openapi/models/WorkflowListResponse.js +50 -0
  374. package/dist/openapi/models/WorkflowListSuccessEnvelope.d.ts +46 -0
  375. package/dist/openapi/models/WorkflowListSuccessEnvelope.js +54 -0
  376. package/dist/openapi/models/WorkflowPauseRequiredAction.d.ts +2 -2
  377. package/dist/openapi/models/WorkflowPauseRequiredAction.js +2 -2
  378. package/dist/openapi/models/WorkflowPausedDetail.d.ts +2 -2
  379. package/dist/openapi/models/WorkflowPausedDetail.js +2 -2
  380. package/dist/openapi/models/WorkflowPausedDetailLinks.d.ts +2 -2
  381. package/dist/openapi/models/WorkflowPausedDetailLinks.js +2 -2
  382. package/dist/openapi/models/WorkflowProcessing.d.ts +2 -2
  383. package/dist/openapi/models/WorkflowProcessing.js +2 -2
  384. package/dist/openapi/models/WorkflowResumeResponse.d.ts +2 -2
  385. package/dist/openapi/models/WorkflowResumeResponse.js +2 -2
  386. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.d.ts +2 -2
  387. package/dist/openapi/models/WorkflowResumeSuccessEnvelope.js +2 -2
  388. package/dist/openapi/models/WorkflowSource.d.ts +7 -4
  389. package/dist/openapi/models/WorkflowSource.js +2 -2
  390. package/dist/openapi/models/WorkflowStatus.d.ts +2 -2
  391. package/dist/openapi/models/WorkflowStatus.js +2 -2
  392. package/dist/openapi/models/WorkflowStatusResponse.d.ts +21 -2
  393. package/dist/openapi/models/WorkflowStatusResponse.js +5 -2
  394. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.d.ts +2 -2
  395. package/dist/openapi/models/WorkflowStatusSuccessEnvelope.js +2 -2
  396. package/dist/openapi/models/WorkflowSummary.d.ts +60 -0
  397. package/dist/openapi/models/WorkflowSummary.js +57 -0
  398. package/dist/openapi/models/WorkflowSummaryJob.d.ts +57 -0
  399. package/dist/openapi/models/WorkflowSummaryJob.js +57 -0
  400. package/dist/openapi/models/WorkflowWarning.d.ts +2 -2
  401. package/dist/openapi/models/WorkflowWarning.js +2 -2
  402. package/dist/openapi/models/WorkflowWarningSeverity.d.ts +2 -2
  403. package/dist/openapi/models/WorkflowWarningSeverity.js +2 -2
  404. package/dist/openapi/models/index.d.ts +31 -1
  405. package/dist/openapi/models/index.js +31 -1
  406. package/dist/openapi/runtime.d.ts +2 -2
  407. package/dist/openapi/runtime.js +2 -2
  408. package/dist/operations/compress.d.ts +0 -9
  409. package/dist/operations/compress.js +0 -6
  410. package/dist/operations/compress.metadata.js +4 -12
  411. package/dist/operations/index.d.ts +3 -0
  412. package/dist/operations/index.js +3 -0
  413. package/dist/operations/merge.d.ts +4 -0
  414. package/dist/operations/merge.metadata.js +12 -0
  415. package/dist/operations/passthrough.metadata.d.ts +2 -0
  416. package/dist/operations/passthrough.metadata.js +6 -0
  417. package/dist/operations/render_variants.d.ts +24 -0
  418. package/dist/operations/render_variants.js +14 -0
  419. package/dist/operations/render_variants.metadata.d.ts +2 -0
  420. package/dist/operations/render_variants.metadata.js +18 -0
  421. package/dist/operations/split.d.ts +8 -1
  422. package/dist/operations/split.js +5 -0
  423. package/dist/operations/split.metadata.js +18 -5
  424. package/openapi/api.yaml +2333 -241
  425. package/operations/schemas/archive.yaml +1 -1
  426. package/operations/schemas/audio_overlay.yaml +29 -13
  427. package/operations/schemas/audio_to_video.yaml +6 -5
  428. package/operations/schemas/audio_watermark.yaml +18 -16
  429. package/operations/schemas/compress.yaml +34 -32
  430. package/operations/schemas/custom_luma.yaml +18 -3
  431. package/operations/schemas/image_watermark.yaml +22 -7
  432. package/operations/schemas/merge.yaml +88 -35
  433. package/operations/schemas/passthrough.yaml +49 -0
  434. package/operations/schemas/render_variants.yaml +117 -0
  435. package/operations/schemas/split.yaml +72 -18
  436. package/operations/schemas/text_watermark.yaml +6 -6
  437. package/operations/schemas/thumbnail.yaml +1 -1
  438. package/operations/schemas/video_text_watermark.yaml +2 -2
  439. package/operations/schemas/video_watermark.yaml +7 -4
  440. package/package.json +3 -1
  441. package/dist/openapi/models/LogoutUser200Response.d.ts +0 -50
  442. package/dist/openapi/models/LogoutUser200Response.js +0 -53
  443. /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.31.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: |
@@ -1149,10 +1203,17 @@ paths:
1149
1203
  custom_luma, audio_overlay, audio_to_video, video_watermark):
1150
1204
  provide `inputs[]` with
1151
1205
  `JobInputV2` entries. Each entry has its own `source`
1152
- (same `WorkflowSource` union), plus optional `role`
1206
+ (a `MultiInputSource` the 3-leaf subset of `WorkflowSource`
1207
+ that **excludes `upload`**: `job_output` / `external_import` /
1208
+ `connection`), plus optional `role`
1153
1209
  (image_watermark, custom_luma, audio_overlay, audio_to_video,
1154
1210
  video_watermark) and
1155
- `per_input_options`.
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`.
1156
1217
 
1157
1218
  Exactly one of `source` or `inputs` is required per job.
1158
1219
 
@@ -1184,6 +1245,26 @@ paths:
1184
1245
  still receives the implicit `compress` — see above). Per
1185
1246
  [ADR-0004](../docs/decisions/0004-job-shape.md).
1186
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
+
1187
1268
  **Multi-input constraint:** multi-input jobs (`inputs[]`) must
1188
1269
  have exactly one operation, which must be a multi-input type
1189
1270
  (`merge`, `archive`, `image_watermark`, `custom_luma`,
@@ -1213,7 +1294,14 @@ paths:
1213
1294
  is deferred per ADR-0001 §1.7 row 5.
1214
1295
  operationId: createWorkflow
1215
1296
  security: [{}, {bearerAuth: []}, {sessionAuth: []}] # optional (anon-OK on free-tier baseline; auth required for tier-gated operations)
1216
- 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
1217
1305
  tags:
1218
1306
  - Workflow
1219
1307
  requestBody:
@@ -1235,6 +1323,26 @@ paths:
1235
1323
  options:
1236
1324
  mode: lossy
1237
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
1238
1346
  v2_chain_compress_thumbnail:
1239
1347
  summary: V2 chained compress -> thumbnail (id required on first job)
1240
1348
  value:
@@ -1254,16 +1362,28 @@ paths:
1254
1362
  width: 320
1255
1363
  height: 240
1256
1364
  v2_merge_two_uploads:
1257
- summary: V2 multi-input merge (two uploads, no role)
1365
+ summary: V2 multi-input merge (two uploads via passthrough source jobs)
1258
1366
  value:
1259
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
1260
1380
  - inputs:
1261
1381
  - source:
1262
- type: upload
1263
- file_id: "019539ab-1111-7000-8000-000000000001"
1382
+ type: job_output
1383
+ from: src_a
1264
1384
  - source:
1265
- type: upload
1266
- file_id: "019539ab-1111-7000-8000-000000000002"
1385
+ type: job_output
1386
+ from: src_b
1267
1387
  operations:
1268
1388
  - type: merge
1269
1389
  v2_external_import:
@@ -1534,14 +1654,36 @@ paths:
1534
1654
  error_type: "balance_exhausted"
1535
1655
  required_action: "wait_for_renewal"
1536
1656
  '404':
1537
- 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".
1538
1677
  content:
1539
1678
  application/json:
1540
1679
  schema:
1541
1680
  $ref: '#/components/schemas/ErrorEnvelope'
1542
1681
  example:
1543
1682
  success: false
1544
- 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"
1545
1687
  '422':
1546
1688
  description: |
1547
1689
  Semantically invalid request. One of:
@@ -2241,10 +2383,11 @@ paths:
2241
2383
 
2242
2384
  **Per-tier private caching.** Per ADR-0002, the response is
2243
2385
  per-caller; CDN-style public caching is NOT used. Clients
2244
- revalidate via `ETag` (strong) and/or `Last-Modified` headers —
2245
- send `If-None-Match` / `If-Modified-Since` on subsequent
2246
- requests; the server returns `304 Not Modified` when the cached
2247
- 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.
2248
2391
 
2249
2392
  Cache-key composition (server-side): `user_tier + schema_version + capabilities_version + environment`.
2250
2393
 
@@ -2289,9 +2432,11 @@ paths:
2289
2432
  in: header
2290
2433
  required: false
2291
2434
  description: |
2292
- Conditional revalidation: send the previously-received
2293
- `Last-Modified` value (HTTP-date) to receive `304 Not
2294
- 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.
2295
2440
  schema:
2296
2441
  type: string
2297
2442
  format: http-date
@@ -2300,7 +2445,7 @@ paths:
2300
2445
  '200':
2301
2446
  description: |
2302
2447
  Operation schema (raw JSON, no ResponseEnvelope). Tier-scoped
2303
- per caller; revalidate via `ETag` / `Last-Modified`.
2448
+ per caller; revalidate via the `ETag` (`If-None-Match`).
2304
2449
  headers:
2305
2450
  Cache-Control:
2306
2451
  schema:
@@ -2323,8 +2468,10 @@ paths:
2323
2468
  type: string
2324
2469
  format: http-date
2325
2470
  description: |
2326
- HTTP-date of last response generation. Clients may send
2327
- `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.
2328
2475
  example: "Sun, 26 Apr 2026 09:00:00 GMT"
2329
2476
  content:
2330
2477
  application/json:
@@ -2385,10 +2532,10 @@ paths:
2385
2532
  values: ["max", "crop", "scale"]
2386
2533
  '304':
2387
2534
  description: |
2388
- Cached response is still fresh. Server emits `ETag` +
2389
- `Last-Modified` headers matching the client's revalidation
2390
- request (`If-None-Match` and/or `If-Modified-Since`). Body
2391
- 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.
2392
2539
  headers:
2393
2540
  Cache-Control:
2394
2541
  schema:
@@ -2740,22 +2887,7 @@ paths:
2740
2887
  content:
2741
2888
  application/json:
2742
2889
  schema:
2743
- type: object
2744
- required:
2745
- - success
2746
- - data
2747
- properties:
2748
- success:
2749
- type: boolean
2750
- enum: [true]
2751
- data:
2752
- type: object
2753
- description: |
2754
- Empty object on logout success — preserves the
2755
- `{ success: true, data: {...} }` envelope
2756
- invariant from `info.description`. Logout has
2757
- no return payload; the data slot is always
2758
- `{}`.
2890
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
2759
2891
  example:
2760
2892
  success: true
2761
2893
  data: {}
@@ -2788,241 +2920,1270 @@ paths:
2788
2920
  schema:
2789
2921
  $ref: '#/components/schemas/ErrorEnvelope'
2790
2922
 
2791
- # ============================================
2792
- # EXTERNAL IMPORT ENDPOINT (ticket I10)
2793
- # ============================================
2794
-
2795
- /api/external-imports:
2923
+ /api/auth/register:
2796
2924
  post:
2797
- summary: Register a one-shot external import URL
2925
+ summary: Register a new account
2798
2926
  description: |
2799
- Register a one-shot bearer URL (S3 presigned / GCS signed /
2800
- Azure SAS / Dropbox shared link / public HTTPS) and receive an
2801
- opaque `external_source_id` handle. Subsequent workflows
2802
- reference the handle via `WorkflowSource` of `type:
2803
- external_import`; the original URL + password are encrypted
2804
- server-side and never returned in any response.
2805
-
2806
- Per [ADR-0005](../docs/decisions/0005-external-sources.md) — see
2807
- §"SSRF posture" for the 8 validation rules applied at
2808
- registration time AND again at fetch time.
2809
-
2810
- **Availability:** `planned` — runtime depends on cross-repo
2811
- ticket [`MbosYtJD`](https://trello.com/c/MbosYtJD) (Lambda team
2812
- publishes capability manifest) and the
2813
- [`POST /api/connections`](https://trello.com/c/MbosYtJD) vault
2814
- endpoint shipping. The contract surface ships now (contract-first
2815
- per ADR-0001 §1.3); the runtime endpoint returns `404` until
2816
- cross-repo wiring lands.
2817
- operationId: createExternalImport
2818
- # Auth: REQUIRED defensively. Endpoint is availability:planned (no
2819
- # controller yet); but the endpoint stores encrypted server-side
2820
- # secrets (bearer URLs, passwords) and anon-callable secret
2821
- # storage is almost certainly wrong. Locking the contract here
2822
- # while we still own the source of truth; runtime tightens to
2823
- # match when the controller lands. Per ADR-0016 / karen review
2824
- # on yN309QVb-B2.
2825
- security: [{bearerAuth: []}, {sessionAuth: []}]
2826
- 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)
2827
2938
  tags:
2828
- - Upload
2939
+ - Auth
2829
2940
  requestBody:
2830
2941
  required: true
2831
2942
  content:
2832
2943
  application/json:
2833
2944
  schema:
2834
- $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"
2835
2961
  responses:
2836
2962
  '201':
2837
- description: External-import handle created.
2963
+ description: |
2964
+ Account created. A verification email has been dispatched; the
2965
+ account cannot authenticate until verified.
2838
2966
  content:
2839
2967
  application/json:
2840
2968
  schema:
2841
- $ref: '#/components/schemas/ExternalImportCreatedSuccessEnvelope'
2969
+ $ref: '#/components/schemas/EmptySuccessEnvelope'
2842
2970
  example:
2843
2971
  success: true
2844
- data:
2845
- external_source_id: "019539ab-2222-7000-8000-000000000001"
2846
- expires_at: "2026-04-26T15:00:00Z"
2847
- provider: "s3_presigned"
2972
+ data: {}
2848
2973
  '400':
2849
- 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).
2850
2975
  content:
2851
2976
  application/json:
2852
2977
  schema:
2853
2978
  $ref: '#/components/schemas/ErrorEnvelope'
2854
2979
  example:
2855
2980
  success: false
2856
- error: "URL is required and must be https://"
2857
- '403':
2858
- description: |
2859
- Forbidden — SSRF policy violation (private IP, loopback,
2860
- cloud metadata endpoint), tier-quota restriction, or
2861
- provider not enabled for caller's tier.
2862
- content:
2863
- application/json:
2864
- schema:
2865
- oneOf:
2866
- - $ref: '#/components/schemas/TierRestrictionResponse'
2867
- - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
2868
- - $ref: '#/components/schemas/ErrorEnvelope'
2981
+ error: "Request body is invalid."
2869
2982
  '422':
2870
2983
  description: |
2871
- Unprocessable URL DNS resolution failed, target
2872
- unreachable, content-type / magic-byte sniff failed,
2873
- `expires_at` already past, or `feature_not_available`
2874
- (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.
2875
2990
  content:
2876
2991
  application/json:
2877
2992
  schema:
2878
2993
  oneOf:
2879
2994
  - $ref: '#/components/schemas/ValidationErrorEnvelope'
2880
- - $ref: '#/components/schemas/FeatureNotAvailableResponse'
2995
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
2881
2996
  discriminator:
2882
2997
  propertyName: error_type
2883
2998
  mapping:
2884
2999
  validation_error: '#/components/schemas/ValidationErrorEnvelope'
2885
- feature_not_available: '#/components/schemas/FeatureNotAvailableResponse'
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."
2886
3008
  '429':
2887
- 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
2888
3020
  content:
2889
3021
  application/json:
2890
3022
  schema:
2891
3023
  $ref: '#/components/schemas/ErrorEnvelope'
2892
3024
  '500':
2893
- description: Internal server error
3025
+ description: Internal server error.
2894
3026
  content:
2895
3027
  application/json:
2896
3028
  schema:
2897
3029
  $ref: '#/components/schemas/ErrorEnvelope'
2898
3030
 
2899
- # ============================================
2900
- # AUDIO WATERMARK DECODE
2901
- # ============================================
2902
-
2903
- /api/audio-watermark/decode:
3031
+ /api/auth/verify-email:
2904
3032
  post:
2905
- summary: Decode an embedded audio watermark
3033
+ summary: Verify an account email address
2906
3034
  description: |
2907
- Extract the steganographic watermark embedded by a prior
2908
- `audio_watermark` operation (per ticket
2909
- [I20](https://trello.com/c/omiCq7Vn)). Pairs with the
2910
- `audio_watermark` operation declared in
2911
- `schemas/operations/audio_watermark.yaml` the operation
2912
- embeds; this endpoint decodes.
2913
-
2914
- **Tier-restricted.** This endpoint is `enterprise`-only. Free
2915
- and `pro` callers receive a 403 `feature_tier_restricted`.
2916
- Anonymous callers receive a 401.
2917
-
2918
- **Scope: own watermarks only.** The decoder will refuse to
2919
- extract from media the caller did not mark themselves
2920
- (server-side audit log of watermark origin per
2921
- `watermark_id`). Per spike S11 acceptance — this avoids the
2922
- privacy implications of arbitrary-media decode while still
2923
- serving the forensic-tracking use case (find a leaked asset
2924
- on a third-party platform, decode the watermark, look up the
2925
- distribution channel that received the marked copy).
2926
-
2927
- **Rate-limited.** Request rate per caller is enforced
2928
- independently from the workflow-create rate limit; abusive
2929
- decode patterns flag for audit per [ADR-0001](../docs/decisions/0001-contract-first-availability.md).
2930
-
2931
- **`availability: planned`** — operation Lambda + decode
2932
- Lambda are cross-repo follow-up. The endpoint declaration
2933
- ships now (contract-first); the runtime returns
2934
- `feature_not_available` (422) until the Lambda lands. Per
2935
- Tension 1 (ADR-0001 §1.3).
2936
- operationId: decodeAudioWatermark
2937
- 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)
2938
3042
  tags:
2939
- - AudioWatermark
2940
- x-availability: planned
3043
+ - Auth
2941
3044
  requestBody:
2942
3045
  required: true
2943
3046
  content:
2944
3047
  application/json:
2945
3048
  schema:
2946
- $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"
2947
3059
  responses:
2948
3060
  '200':
2949
- description: Watermark successfully extracted.
2950
- content:
2951
- application/json:
2952
- schema:
2953
- $ref: '#/components/schemas/AudioWatermarkDecodeResponse'
2954
- '401':
2955
- description: Authentication required (anonymous callers).
2956
- content:
2957
- application/json:
2958
- schema:
2959
- $ref: '#/components/schemas/ErrorEnvelope'
2960
- '403':
2961
- description: |
2962
- Tier insufficient (free / pro caller) — returned as
2963
- `FeatureTierRestrictedResponse` with
2964
- `error_type: feature_tier_restricted`. Per ADR-0001 §1.3.
3061
+ description: Email verified. The account can now authenticate.
2965
3062
  content:
2966
3063
  application/json:
2967
3064
  schema:
2968
- oneOf:
2969
- - $ref: '#/components/schemas/TierRestrictionResponse'
2970
- - $ref: '#/components/schemas/FeatureTierRestrictedResponse'
2971
- discriminator:
2972
- propertyName: error_type
2973
- mapping:
2974
- tier_restriction: '#/components/schemas/TierRestrictionResponse'
2975
- feature_tier_restricted: '#/components/schemas/FeatureTierRestrictedResponse'
2976
- '404':
2977
- description: |
2978
- No watermark detected in the supplied asset, OR the
2979
- detected watermark was not issued by this caller (the
2980
- scope-limit applies — own-watermarks-only).
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).
2981
3071
  content:
2982
3072
  application/json:
2983
3073
  schema:
2984
3074
  $ref: '#/components/schemas/ErrorEnvelope'
3075
+ example:
3076
+ success: false
3077
+ error: "Request body is invalid."
2985
3078
  '422':
2986
3079
  description: |
2987
- Either generic validation error
2988
- (`ValidationErrorEnvelope`) or the operation is `planned`
2989
- and the runtime Lambda has not yet shipped
2990
- (`FeatureNotAvailableResponse` with `error_type:
2991
- feature_not_available`).
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.
2992
3086
  content:
2993
3087
  application/json:
2994
3088
  schema:
2995
3089
  oneOf:
2996
3090
  - $ref: '#/components/schemas/ValidationErrorEnvelope'
2997
- - $ref: '#/components/schemas/FeatureNotAvailableResponse'
3091
+ - $ref: '#/components/schemas/AuthRejectionEnvelope'
2998
3092
  discriminator:
2999
3093
  propertyName: error_type
3000
3094
  mapping:
3001
3095
  validation_error: '#/components/schemas/ValidationErrorEnvelope'
3002
- feature_not_available: '#/components/schemas/FeatureNotAvailableResponse'
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."
3003
3104
  '429':
3004
3105
  description: |
3005
- Rate limit exceeded for decode requests. Decode
3006
- rate-limit is enforced independently from
3007
- workflow-create.
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
3008
3115
  content:
3009
3116
  application/json:
3010
3117
  schema:
3011
3118
  $ref: '#/components/schemas/ErrorEnvelope'
3012
3119
  '500':
3013
- description: Internal server error
3120
+ description: Internal server error.
3014
3121
  content:
3015
3122
  application/json:
3016
3123
  schema:
3017
3124
  $ref: '#/components/schemas/ErrorEnvelope'
3018
3125
 
3019
- # ============================================
3020
- # CREDITS + BILLING ENDPOINTS (F9 — per ticket I23)
3021
- # ============================================
3022
-
3023
- /api/v2/credits/balance:
3024
- get:
3025
- summary: Get current credit balance
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'
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'
4164
+ '429':
4165
+ description: |
4166
+ Rate limit exceeded for decode requests. Decode
4167
+ rate-limit is enforced independently from
4168
+ workflow-create.
4169
+ content:
4170
+ application/json:
4171
+ schema:
4172
+ $ref: '#/components/schemas/ErrorEnvelope'
4173
+ '500':
4174
+ description: Internal server error
4175
+ content:
4176
+ application/json:
4177
+ schema:
4178
+ $ref: '#/components/schemas/ErrorEnvelope'
4179
+
4180
+ # ============================================
4181
+ # CREDITS + BILLING ENDPOINTS (F9 — per ticket I23)
4182
+ # ============================================
4183
+
4184
+ /api/v2/credits/balance:
4185
+ get:
4186
+ summary: Get current credit balance
3026
4187
  description: |
3027
4188
  Returns a snapshot of the caller's credit position at request
3028
4189
  time. Per ticket [I23 `DffjC3zm`](https://trello.com/c/DffjC3zm)
@@ -3469,6 +4630,37 @@ components:
3469
4630
  success:
3470
4631
  type: boolean
3471
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
+
3472
4664
  ErrorEnvelope:
3473
4665
  type: object
3474
4666
  description: |
@@ -3525,6 +4717,17 @@ components:
3525
4717
  would exceed the S3 multipart capacity cap. Pre-S3
3526
4718
  server-side capacity gate; distinct from tier-quota
3527
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.
3528
4731
  message:
3529
4732
  type: string
3530
4733
  description: |
@@ -3691,6 +4894,82 @@ components:
3691
4894
  `ErrorEnvelope.message_params`. Excludes cost
3692
4895
  numbers.
3693
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
+
3694
4973
  ReEncodeDecision:
3695
4974
  type: string
3696
4975
  description: |
@@ -3849,11 +5128,13 @@ components:
3849
5128
  — this is the load-bearing invariant that makes the
3850
5129
  primitive useful.
3851
5130
 
3852
- Absence of `per_role_cardinality` on a role-based op keeps
3853
- the current prose-only semantics (image_watermark /
3854
- audio_overlay / custom_luma encode their role rules in
3855
- prose at `JobInputRole`'s description today; migration to
3856
- 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.
3857
5138
 
3858
5139
  Per ticket [`SlluxMBN`](https://trello.com/c/SlluxMBN) /
3859
5140
  ADR-0015. Consumed by `OperationInputRoleValidator` in
@@ -3892,8 +5173,10 @@ components:
3892
5173
  # ============================================
3893
5174
  #
3894
5175
  # Per ADR-0005. WorkflowSource is the discriminated union consumed
3895
- # by V2 JobDefinition.source / JobInputV2.source (wired by I12).
3896
- # 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.
3897
5180
 
3898
5181
  UploadSource:
3899
5182
  type: object
@@ -3985,8 +5268,11 @@ components:
3985
5268
 
3986
5269
  WorkflowSource:
3987
5270
  description: |
3988
- Discriminated union of source leaves consumed by V2
3989
- `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
3990
5276
  [ADR-0005](../docs/decisions/0005-external-sources.md) +
3991
5277
  [ADR-0004](../docs/decisions/0004-job-shape.md).
3992
5278
 
@@ -4005,6 +5291,30 @@ components:
4005
5291
  external_import: '#/components/schemas/ExternalImportToken'
4006
5292
  connection: '#/components/schemas/ConnectionSource'
4007
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
+
4008
5318
  ExternalSource:
4009
5319
  description: |
4010
5320
  Subset alias of `WorkflowSource` that excludes uploads and
@@ -4324,6 +5634,7 @@ components:
4324
5634
 
4325
5635
  CreditsBalanceSuccessEnvelope:
4326
5636
  type: object
5637
+ additionalProperties: false
4327
5638
  required:
4328
5639
  - success
4329
5640
  - data
@@ -4494,6 +5805,7 @@ components:
4494
5805
 
4495
5806
  CreditsUsageSuccessEnvelope:
4496
5807
  type: object
5808
+ additionalProperties: false
4497
5809
  required:
4498
5810
  - success
4499
5811
  - data
@@ -4651,6 +5963,7 @@ components:
4651
5963
 
4652
5964
  WorkflowCancelSuccessEnvelope:
4653
5965
  type: object
5966
+ additionalProperties: false
4654
5967
  required:
4655
5968
  - success
4656
5969
  - data
@@ -4686,6 +5999,7 @@ components:
4686
5999
 
4687
6000
  WorkflowResumeSuccessEnvelope:
4688
6001
  type: object
6002
+ additionalProperties: false
4689
6003
  required:
4690
6004
  - success
4691
6005
  - data
@@ -5595,7 +6909,7 @@ components:
5595
6909
  - thumbnail_office: Office document (DOCX/XLSX/PPTX/ODT/ODS/ODP)
5596
6910
  thumbnail sub-type. Backed by a dedicated LibreOffice Lambda.
5597
6911
  Emitted by the publisher for office-document inputs.
5598
- - 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).
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).
5599
6913
  - text_watermark: Text overlay rendered onto an image (Liberation Sans). Single-input. Per ADR-0004 + I4-CONS.
5600
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.
5601
6915
  - archive: Bundle files into ZIP/tar.gz (all types). Multi-input.
@@ -5606,7 +6920,8 @@ components:
5606
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).
5607
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.
5608
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.
5609
- - 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` mime_group (worker live on staging — shape-stable + opt-in, MUST NOT 422); the `image_gif`, `document_pdf`, and `video` 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.
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).
5610
6925
 
5611
6926
  Both the legacy `thumbnail` value and the four sub-type values
5612
6927
  are valid routing targets today during the thumbnail migration
@@ -5625,6 +6940,7 @@ components:
5625
6940
  - thumbnail_office
5626
6941
  - image_watermark
5627
6942
  - text_watermark
6943
+ - render_variants
5628
6944
  - merge
5629
6945
  - archive
5630
6946
  - convert
@@ -5635,6 +6951,7 @@ components:
5635
6951
  - video_watermark
5636
6952
  - video_text_watermark
5637
6953
  - split
6954
+ - passthrough
5638
6955
 
5639
6956
  OperationInputModel:
5640
6957
  type: string
@@ -5643,7 +6960,7 @@ components:
5643
6960
  - single: One input file (compress, thumbnail, thumbnail_image,
5644
6961
  thumbnail_video, thumbnail_document, thumbnail_office,
5645
6962
  text_watermark, convert, audio_watermark,
5646
- video_text_watermark, split)
6963
+ video_text_watermark, split, passthrough)
5647
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.
5648
6965
  enum:
5649
6966
  - single
@@ -5811,6 +7128,7 @@ components:
5811
7128
 
5812
7129
  UploadSuccessEnvelope:
5813
7130
  type: object
7131
+ additionalProperties: false
5814
7132
  required:
5815
7133
  - success
5816
7134
  - data
@@ -6423,6 +7741,7 @@ components:
6423
7741
 
6424
7742
  MetadataSuccessEnvelope:
6425
7743
  type: object
7744
+ additionalProperties: false
6426
7745
  required:
6427
7746
  - success
6428
7747
  - data
@@ -6439,15 +7758,82 @@ components:
6439
7758
 
6440
7759
  WorkflowCreateRequest:
6441
7760
  type: object
6442
- required:
6443
- - 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]
6444
7806
  properties:
6445
7807
  jobs:
6446
7808
  type: array
6447
- 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.
6448
7813
  minItems: 1
6449
7814
  items:
6450
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'
6451
7837
  workflow_edges:
6452
7838
  type: array
6453
7839
  description: |
@@ -6596,7 +7982,10 @@ components:
6596
7982
  Multi-input list for `merge`, `archive`, `image_watermark`,
6597
7983
  `custom_luma`, `audio_overlay`, `audio_to_video`, and
6598
7984
  `video_watermark`. Each
6599
- entry is a `JobInputV2` with its own `WorkflowSource`.
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`).
6600
7989
  Mutually exclusive with `source` — the V2 shape boundary
6601
7990
  stays `source` (single-input) XOR `inputs[]` (multi-input
6602
7991
  role-based) per ADR-0004 / I12.
@@ -6615,10 +8004,23 @@ components:
6615
8004
  operations:
6616
8005
  type: array
6617
8006
  description: |
6618
- 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
6619
8013
  operation consumes the previous operation's output; all
6620
8014
  intermediate and final outputs are kept.
6621
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
+
6622
8024
  Multi-input jobs (with `inputs[]`) must have exactly one
6623
8025
  operation, and it must be a multi-input type (`merge`,
6624
8026
  `archive`, `image_watermark`, `custom_luma`,
@@ -6669,21 +8071,66 @@ components:
6669
8071
  via Wave A [`c3uthIP4`](https://trello.com/c/c3uthIP4) —
6670
8072
  `availability: beta`, backends live). The multi-input
6671
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.
6672
8115
 
6673
8116
  JobInputV2:
6674
8117
  type: object
6675
8118
  description: |
6676
8119
  V2 multi-input entry per [ADR-0004](../docs/decisions/0004-job-shape.md)
6677
8120
  §"JobInputV2". Replaces V1 `JobInput`. Each entry carries its own
6678
- `source` (a `WorkflowSource` value — same union used at
6679
- `JobDefinition.source`), so multi-input jobs can mix uploads,
6680
- external imports, vault connections, and upstream job outputs
6681
- 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)).
6682
8129
  required:
6683
8130
  - source
6684
8131
  properties:
6685
8132
  source:
6686
- $ref: '#/components/schemas/WorkflowSource'
8133
+ $ref: '#/components/schemas/MultiInputSource'
6687
8134
  role:
6688
8135
  type: string
6689
8136
  description: |
@@ -6757,6 +8204,24 @@ components:
6757
8204
  rules. For example, `quality` is only valid when `mode: lossy` for
6758
8205
  compress operations.
6759
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.
6760
8225
 
6761
8226
  # ============================================
6762
8227
  # WORKFLOW DELIVERY (ticket I8-CONS)
@@ -6787,6 +8252,13 @@ components:
6787
8252
  Optional operation type filter when the job has multiple
6788
8253
  operations. Omit to select the last operation's output.
6789
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
+
6790
8262
  DeliverySelection:
6791
8263
  type: object
6792
8264
  description: |
@@ -6802,8 +8274,10 @@ components:
6802
8274
  Selection strategy:
6803
8275
  - `terminal` (default): bundle all jobs with no outgoing
6804
8276
  edges (computed from `JobOutputSource.from` references).
6805
- - `all_outputs`: bundle every operation output in the
6806
- 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`).
6807
8281
  - `explicit`: bundle only the refs listed in `refs[]`.
6808
8282
  Mutually exclusive with per-job `JobDefinition.deliver: true`
6809
8283
  (validator rejects 422 if both are present).
@@ -6813,19 +8287,21 @@ components:
6813
8287
  - explicit
6814
8288
  per_value_availability:
6815
8289
  terminal: { availability: stable }
6816
- all_outputs: { availability: planned }
6817
- explicit: { availability: planned }
6818
- # Availability flipped betaplanned per ticket
6819
- # [`co0CERtJ`](https://trello.com/c/co0CERtJ) the API
6820
- # runtime currently 422s every non-default `selection.type`
6821
- # with `feature_not_available`, which per the
6822
- # `info.description` availability taxonomy IS the `planned`
6823
- # behaviour (`stable`/`beta` MUST NOT 422). When the API
6824
- # threads the request-level `delivery.selection` shape, this
6825
- # flips back. Terminal stays `stable` because the runtime
6826
- # honours it as the implicit default (`DeliveryPlanComputer`
6827
- # computes terminal selection regardless of whether the
6828
- # 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).
6829
8305
  refs:
6830
8306
  type: array
6831
8307
  description: |
@@ -6953,6 +8429,18 @@ components:
6953
8429
  description: |
6954
8430
  Set when `reason: consumed_by`. The id of the downstream
6955
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.
6956
8444
 
6957
8445
  DeliveryPlan:
6958
8446
  type: object
@@ -7211,6 +8699,205 @@ components:
7211
8699
  class_hint:
7212
8700
  $ref: '#/components/schemas/ProcessingClassHint'
7213
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
+
7214
8901
  # ============================================
7215
8902
  # UPLOAD PREFLIGHT PROBE (per ticket I28)
7216
8903
  # ============================================
@@ -7416,6 +9103,7 @@ components:
7416
9103
 
7417
9104
  UploadProbeSuccessEnvelope:
7418
9105
  type: object
9106
+ additionalProperties: false
7419
9107
  description: |
7420
9108
  Success envelope wrapping `UploadProbeResponse` on
7421
9109
  `POST /api/uploads/{id}/probe` 200 responses, per ticket
@@ -7603,6 +9291,24 @@ components:
7603
9291
  frontend can preview routing decisions before the
7604
9292
  workflow runs. Estimation algorithm is server-side and
7605
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.
7606
9312
  warnings:
7607
9313
  type: array
7608
9314
  description: |
@@ -7640,6 +9346,7 @@ components:
7640
9346
 
7641
9347
  WorkflowCreateSuccessEnvelope:
7642
9348
  type: object
9349
+ additionalProperties: false
7643
9350
  required:
7644
9351
  - success
7645
9352
  - data
@@ -7691,9 +9398,25 @@ components:
7691
9398
  (per ticket [I24](https://trello.com/c/e50uXLcl)). Carries
7692
9399
  `paused_at`, `expires_at`, `required_action`, and
7693
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`.
7694
9416
 
7695
9417
  WorkflowStatusSuccessEnvelope:
7696
9418
  type: object
9419
+ additionalProperties: false
7697
9420
  required:
7698
9421
  - success
7699
9422
  - data
@@ -7704,6 +9427,118 @@ components:
7704
9427
  data:
7705
9428
  $ref: '#/components/schemas/WorkflowStatusResponse'
7706
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
+
7707
9542
  JobResponse:
7708
9543
  type: object
7709
9544
  description: Job status within a workflow response
@@ -7771,6 +9606,18 @@ components:
7771
9606
  description: Progress percentage (0-100). Present when in_progress or completed.
7772
9607
  result:
7773
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).
7774
9621
  error_code:
7775
9622
  type: string
7776
9623
  description: |
@@ -7787,6 +9634,32 @@ components:
7787
9634
  absent otherwise. Mirrors `SseOperationFailedData.error_message`.
7788
9635
  example: "output_too_large: Output (12156489 bytes) is not smaller than input (6187609 bytes)"
7789
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
+
7790
9663
  OperationResult:
7791
9664
  type: object
7792
9665
  description: |
@@ -7808,7 +9681,20 @@ components:
7808
9681
  description: Key in the customer's export destination (if export configured)
7809
9682
  metrics:
7810
9683
  type: object
7811
- 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.
7812
9698
  properties:
7813
9699
  compression_ratio:
7814
9700
  type: number
@@ -7849,6 +9735,7 @@ components:
7849
9735
 
7850
9736
  WorkflowDownloadSuccessEnvelope:
7851
9737
  type: object
9738
+ additionalProperties: false
7852
9739
  required:
7853
9740
  - success
7854
9741
  - data
@@ -7902,9 +9789,10 @@ components:
7902
9789
  not: { required: [page_index] }
7903
9790
  - title: Unindexed
7904
9791
  description: |
7905
- Output without an explicit indexing field — every legacy
7906
- single-output download. Schema-valid; emitted by all
7907
- 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).
7908
9796
  not:
7909
9797
  anyOf:
7910
9798
  - required: [page_index]
@@ -7947,6 +9835,32 @@ components:
7947
9835
  any current operation; declared for parity with
7948
9836
  `OperationResultOutputEntry.position`. Per ADR-0009 §D2.
7949
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
7950
9864
 
7951
9865
  # ============================================
7952
9866
  # SSE EVENT SCHEMAS
@@ -8041,7 +9955,21 @@ components:
8041
9955
 
8042
9956
  SseOperationCompletedData:
8043
9957
  type: object
8044
- 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.
8045
9973
  required:
8046
9974
  - job_ref
8047
9975
  - operation_id
@@ -8468,8 +10396,10 @@ components:
8468
10396
  **Caching.** Per-tier private caching with ETag-based revalidation
8469
10397
  (per ADR-0002). Public CDN caching is NOT used because the cache
8470
10398
  key includes the caller's `user_tier`. Clients send
8471
- `If-None-Match` (and/or `If-Modified-Since`) to revalidate; the
8472
- 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`).
8473
10403
 
8474
10404
  Cache-key composition: `user_tier + schema_version + capabilities_version + environment`.
8475
10405
  required:
@@ -8610,6 +10540,28 @@ components:
8610
10540
  properties:
8611
10541
  per_value_availability:
8612
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'
8613
10565
 
8614
10566
  EndpointProjection:
8615
10567
  type: object
@@ -8640,8 +10592,17 @@ components:
8640
10592
  Value of the `x-identity-scoped` vendor extension on the
8641
10593
  operation (default `false`). True iff the operation
8642
10594
  targets an identity-bound resource and cross-identity
8643
- access returns 403 — OR acts on the caller's implicit
10595
+ access is rejected — OR acts on the caller's implicit
8644
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.
8645
10606
  required_tier:
8646
10607
  description: |
8647
10608
  Endpoint-level entitlement gate. Reserved/null today —
@@ -8728,9 +10689,10 @@ components:
8728
10689
  Optional per-role input-count overlay for role-based
8729
10690
  multi-input operations. Per ticket
8730
10691
  [`SlluxMBN`](https://trello.com/c/SlluxMBN) / ADR-0015.
8731
- Absent on operations whose role rules are still encoded
8732
- in prose only (image_watermark, audio_overlay,
8733
- 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.
8734
10696
  accepts_mixed_types:
8735
10697
  type: boolean
8736
10698
  description: Whether mixed MIME types are allowed (archive only)
@@ -8814,6 +10776,37 @@ components:
8814
10776
  [I3](https://trello.com/c/eCWIpug8); until then the
8815
10777
  contract advertises the field shape but the endpoint
8816
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'
8817
10810
  options:
8818
10811
  type: object
8819
10812
  description: Options specific to this MIME group, keyed by option name
@@ -8917,6 +10910,104 @@ components:
8917
10910
  The "set" sentinel means the option has any value. "logic" can be "and" (default) or "or".
8918
10911
  additionalProperties: true
8919
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
+
8920
11011
  # ============================================
8921
11012
  # RETRY SCHEMAS
8922
11013
  # ============================================
@@ -8942,6 +11033,7 @@ components:
8942
11033
 
8943
11034
  RetrySuccessEnvelope:
8944
11035
  type: object
11036
+ additionalProperties: false
8945
11037
  required:
8946
11038
  - success
8947
11039
  - data