@aglyn/plugins-ai 1.0.0-beta.228 → 1.0.0-beta.230

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 (274) hide show
  1. package/README.md +3 -3
  2. package/package.json +17 -17
  3. package/src/lib/activity/ai-activity-actions.js +7 -1
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-automation-zones.d.ts +38 -0
  6. package/src/lib/components/ai-automation-zones.js +5 -1
  7. package/src/lib/components/ai-automation-zones.js.map +1 -1
  8. package/src/lib/components/ai-brief-dialog.component.d.ts +20 -2
  9. package/src/lib/components/ai-brief-dialog.component.js +87 -8
  10. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  11. package/src/lib/components/ai-campaign-create.component.d.ts +54 -0
  12. package/src/lib/components/ai-campaign-create.component.js +270 -0
  13. package/src/lib/components/ai-campaign-create.component.js.map +1 -0
  14. package/src/lib/components/ai-describe-automation.component.d.ts +8 -3
  15. package/src/lib/components/ai-describe-automation.component.js +15 -6
  16. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  17. package/src/lib/components/ai-describe-button.component.d.ts +20 -8
  18. package/src/lib/components/ai-describe-button.component.js +19 -36
  19. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  20. package/src/lib/components/ai-describe-email.component.d.ts +31 -0
  21. package/src/lib/components/ai-describe-email.component.js +22 -0
  22. package/src/lib/components/ai-describe-email.component.js.map +1 -0
  23. package/src/lib/components/ai-describe-org-automation.component.d.ts +36 -0
  24. package/src/lib/components/ai-describe-org-automation.component.js +238 -0
  25. package/src/lib/components/ai-describe-org-automation.component.js.map +1 -0
  26. package/src/lib/components/ai-experiment-cards.component.d.ts +9 -1
  27. package/src/lib/components/ai-experiment-cards.component.js +104 -18
  28. package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
  29. package/src/lib/components/ai-explain-automation.component.js +1 -5
  30. package/src/lib/components/ai-explain-automation.component.js.map +1 -1
  31. package/src/lib/components/ai-funnel-zones.component.d.ts +61 -0
  32. package/src/lib/components/ai-funnel-zones.component.js +178 -0
  33. package/src/lib/components/ai-funnel-zones.component.js.map +1 -0
  34. package/src/lib/components/ai-insight-card.component.d.ts +42 -0
  35. package/src/lib/components/ai-insight-card.component.js +137 -0
  36. package/src/lib/components/ai-insight-card.component.js.map +1 -0
  37. package/src/lib/components/ai-insight-dialog.component.d.ts +6 -0
  38. package/src/lib/components/ai-insight-dialog.component.js +9 -3
  39. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  40. package/src/lib/components/ai-job-plan.component.d.ts +7 -2
  41. package/src/lib/components/ai-job-plan.component.js +35 -5
  42. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  43. package/src/lib/components/ai-job-requests.d.ts +17 -1
  44. package/src/lib/components/ai-job-requests.js +39 -3
  45. package/src/lib/components/ai-job-requests.js.map +1 -1
  46. package/src/lib/components/ai-jobs-list-page.component.js +12 -1
  47. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  48. package/src/lib/components/ai-logic-zones.d.ts +64 -0
  49. package/src/lib/components/ai-logic-zones.js +18 -0
  50. package/src/lib/components/ai-logic-zones.js.map +1 -0
  51. package/src/lib/components/ai-logic.component.d.ts +77 -0
  52. package/src/lib/components/ai-logic.component.js +553 -0
  53. package/src/lib/components/ai-logic.component.js.map +1 -0
  54. package/src/lib/components/ai-marketing-insight.component.d.ts +30 -0
  55. package/src/lib/components/ai-marketing-insight.component.js +80 -0
  56. package/src/lib/components/ai-marketing-insight.component.js.map +1 -0
  57. package/src/lib/components/ai-media-create.component.d.ts +55 -0
  58. package/src/lib/components/ai-media-create.component.js +433 -0
  59. package/src/lib/components/ai-media-create.component.js.map +1 -0
  60. package/src/lib/components/ai-overlay-cards.component.d.ts +83 -0
  61. package/src/lib/components/ai-overlay-cards.component.js +505 -0
  62. package/src/lib/components/ai-overlay-cards.component.js.map +1 -0
  63. package/src/lib/components/ai-product-zones.d.ts +5 -0
  64. package/src/lib/components/ai-product-zones.js +1 -1
  65. package/src/lib/components/ai-product-zones.js.map +1 -1
  66. package/src/lib/components/ai-products-create-button.component.d.ts +42 -0
  67. package/src/lib/components/ai-products-create-button.component.js +67 -0
  68. package/src/lib/components/ai-products-create-button.component.js.map +1 -0
  69. package/src/lib/components/ai-products-hub-card.component.js +100 -81
  70. package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
  71. package/src/lib/components/ai-revise-automation.component.d.ts +16 -0
  72. package/src/lib/components/ai-revise-automation.component.js +207 -0
  73. package/src/lib/components/ai-revise-automation.component.js.map +1 -0
  74. package/src/lib/components/ai-site-build-page.component.d.ts +1 -1
  75. package/src/lib/components/ai-site-build-page.component.js +177 -20
  76. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  77. package/src/lib/components/ai-site-start-card.component.d.ts +10 -1
  78. package/src/lib/components/ai-site-start-card.component.js +7 -4
  79. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  80. package/src/lib/components/ai-upsell-dialog.component.d.ts +53 -0
  81. package/src/lib/components/ai-upsell-dialog.component.js +177 -0
  82. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -0
  83. package/src/lib/components/assist-build-card.component.d.ts +42 -0
  84. package/src/lib/components/assist-build-card.component.js +183 -0
  85. package/src/lib/components/assist-build-card.component.js.map +1 -0
  86. package/src/lib/components/assist-edit-canvas.d.ts +2 -0
  87. package/src/lib/components/assist-edit-canvas.js +56 -1
  88. package/src/lib/components/assist-edit-canvas.js.map +1 -1
  89. package/src/lib/components/assist-jobs-drawer.component.d.ts +11 -0
  90. package/src/lib/components/assist-jobs-drawer.component.js +85 -4
  91. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  92. package/src/lib/components/assist-panel.component.js +51 -2
  93. package/src/lib/components/assist-panel.component.js.map +1 -1
  94. package/src/lib/components/use-ai-job-run.d.ts +3 -14
  95. package/src/lib/components/use-ai-job-run.js +0 -50
  96. package/src/lib/components/use-ai-job-run.js.map +1 -1
  97. package/src/lib/jobs/ai-build-capabilities.d.ts +58 -0
  98. package/src/lib/jobs/ai-build-capabilities.js +256 -0
  99. package/src/lib/jobs/ai-build-capabilities.js.map +1 -0
  100. package/src/lib/jobs/ai-build-unit-outcome.d.ts +52 -0
  101. package/src/lib/jobs/ai-build-unit-outcome.js +138 -0
  102. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -0
  103. package/src/lib/jobs/ai-job-build-step.d.ts +144 -0
  104. package/src/lib/jobs/ai-job-build-step.js +657 -0
  105. package/src/lib/jobs/ai-job-build-step.js.map +1 -0
  106. package/src/lib/jobs/ai-job-draft-ids.js +9 -1
  107. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  108. package/src/lib/jobs/ai-job-drafts.d.ts +14 -4
  109. package/src/lib/jobs/ai-job-drafts.js +25 -4
  110. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  111. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +70 -0
  112. package/src/lib/jobs/ai-job-layout-site-pages.js +216 -0
  113. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -0
  114. package/src/lib/jobs/ai-job-layout-step.d.ts +2 -2
  115. package/src/lib/jobs/ai-job-layout-step.js +19 -1
  116. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  117. package/src/lib/jobs/ai-job-logic-step.d.ts +107 -0
  118. package/src/lib/jobs/ai-job-logic-step.js +395 -0
  119. package/src/lib/jobs/ai-job-logic-step.js.map +1 -0
  120. package/src/lib/jobs/ai-job-overlay-copy.d.ts +70 -0
  121. package/src/lib/jobs/ai-job-overlay-copy.js +178 -0
  122. package/src/lib/jobs/ai-job-overlay-copy.js.map +1 -0
  123. package/src/lib/jobs/ai-job-page-step.js +6 -6
  124. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  125. package/src/lib/jobs/ai-job-plan-step.d.ts +26 -1
  126. package/src/lib/jobs/ai-job-plan-step.js +79 -11
  127. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  128. package/src/lib/jobs/ai-job-site-step.d.ts +15 -1
  129. package/src/lib/jobs/ai-job-site-step.js +207 -58
  130. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  131. package/src/lib/jobs/ai-job-text-step.d.ts +22 -1
  132. package/src/lib/jobs/ai-job-text-step.js +12 -0
  133. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  134. package/src/lib/jobs/ai-job-workflow-step.d.ts +54 -4
  135. package/src/lib/jobs/ai-job-workflow-step.js +318 -13
  136. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  137. package/src/lib/jobs/ai-jobs.d.ts +90 -2
  138. package/src/lib/jobs/ai-jobs.js +495 -61
  139. package/src/lib/jobs/ai-jobs.js.map +1 -1
  140. package/src/lib/jobs/ai-logic-records.d.ts +40 -0
  141. package/src/lib/jobs/ai-logic-records.js +55 -0
  142. package/src/lib/jobs/ai-logic-records.js.map +1 -0
  143. package/src/lib/jobs/ai-site-publish.d.ts +7 -0
  144. package/src/lib/jobs/ai-site-publish.js +9 -0
  145. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  146. package/src/lib/jobs/ai-workflow-records.d.ts +4 -0
  147. package/src/lib/jobs/ai-workflow-records.js +40 -1
  148. package/src/lib/jobs/ai-workflow-records.js.map +1 -1
  149. package/src/lib/model/ai-automation-draft.d.ts +18 -0
  150. package/src/lib/model/ai-automation-draft.js +86 -0
  151. package/src/lib/model/ai-automation-draft.js.map +1 -1
  152. package/src/lib/model/ai-build-job.d.ts +201 -0
  153. package/src/lib/model/ai-build-job.js +471 -0
  154. package/src/lib/model/ai-build-job.js.map +1 -0
  155. package/src/lib/model/ai-build-plan.d.ts +61 -0
  156. package/src/lib/model/ai-build-plan.js +154 -9
  157. package/src/lib/model/ai-build-plan.js.map +1 -1
  158. package/src/lib/model/ai-build-progress.d.ts +56 -0
  159. package/src/lib/model/ai-build-progress.js +128 -0
  160. package/src/lib/model/ai-build-progress.js.map +1 -0
  161. package/src/lib/model/ai-insight.d.ts +20 -10
  162. package/src/lib/model/ai-insight.js +72 -9
  163. package/src/lib/model/ai-insight.js.map +1 -1
  164. package/src/lib/model/ai-job-activity.js +3 -1
  165. package/src/lib/model/ai-job-activity.js.map +1 -1
  166. package/src/lib/model/ai-job-failure-copy.d.ts +20 -1
  167. package/src/lib/model/ai-job-failure-copy.js +28 -1
  168. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  169. package/src/lib/model/ai-job-notice.d.ts +3 -0
  170. package/src/lib/model/ai-job-notice.js +11 -2
  171. package/src/lib/model/ai-job-notice.js.map +1 -1
  172. package/src/lib/model/ai-jobs.types.d.ts +87 -2
  173. package/src/lib/model/ai-jobs.types.js +3 -1
  174. package/src/lib/model/ai-jobs.types.js.map +1 -1
  175. package/src/lib/model/ai-logic-job.d.ts +108 -0
  176. package/src/lib/model/ai-logic-job.js +327 -0
  177. package/src/lib/model/ai-logic-job.js.map +1 -0
  178. package/src/lib/model/ai-media-credits.d.ts +52 -0
  179. package/src/lib/model/ai-media-credits.js +61 -0
  180. package/src/lib/model/ai-media-credits.js.map +1 -0
  181. package/src/lib/model/ai-overlay-copy.d.ts +114 -0
  182. package/src/lib/model/ai-overlay-copy.js +175 -0
  183. package/src/lib/model/ai-overlay-copy.js.map +1 -0
  184. package/src/lib/model/ai-site-build-progress.d.ts +28 -3
  185. package/src/lib/model/ai-site-build-progress.js +231 -17
  186. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  187. package/src/lib/model/ai-site-start-seo.d.ts +16 -0
  188. package/src/lib/model/ai-site-start-seo.js +54 -4
  189. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  190. package/src/lib/model/ai-site-start.d.ts +14 -0
  191. package/src/lib/model/ai-site-start.js +43 -0
  192. package/src/lib/model/ai-site-start.js.map +1 -1
  193. package/src/lib/model/ai-svg.d.ts +69 -0
  194. package/src/lib/model/ai-svg.js +265 -0
  195. package/src/lib/model/ai-svg.js.map +1 -0
  196. package/src/lib/model/ai-usage-by-user.d.ts +3 -2
  197. package/src/lib/model/ai-usage-by-user.js.map +1 -1
  198. package/src/lib/model/ai-workflow-job.d.ts +92 -4
  199. package/src/lib/model/ai-workflow-job.js +124 -7
  200. package/src/lib/model/ai-workflow-job.js.map +1 -1
  201. package/src/lib/model/assist-build.d.ts +58 -0
  202. package/src/lib/model/assist-build.js +52 -0
  203. package/src/lib/model/assist-build.js.map +1 -0
  204. package/src/lib/model/assist-edit.d.ts +48 -3
  205. package/src/lib/model/assist-edit.js +48 -4
  206. package/src/lib/model/assist-edit.js.map +1 -1
  207. package/src/lib/plugin-config.js +2 -0
  208. package/src/lib/plugin-config.js.map +1 -1
  209. package/src/lib/plugin.js +240 -11
  210. package/src/lib/plugin.js.map +1 -1
  211. package/src/lib/providers/catalog.d.ts +81 -4
  212. package/src/lib/providers/catalog.js +80 -9
  213. package/src/lib/providers/catalog.js.map +1 -1
  214. package/src/lib/providers/image-contract.d.ts +116 -0
  215. package/src/lib/providers/image-contract.js +74 -0
  216. package/src/lib/providers/image-contract.js.map +1 -0
  217. package/src/lib/providers/model-choice.js +11 -0
  218. package/src/lib/providers/model-choice.js.map +1 -1
  219. package/src/lib/providers/routing.js +18 -0
  220. package/src/lib/providers/routing.js.map +1 -1
  221. package/src/lib/providers/vertex-image.d.ts +121 -0
  222. package/src/lib/providers/vertex-image.js +312 -0
  223. package/src/lib/providers/vertex-image.js.map +1 -0
  224. package/src/lib/runtime/ai-doctrine-validators.d.ts +19 -0
  225. package/src/lib/runtime/ai-doctrine-validators.js +89 -0
  226. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  227. package/src/lib/runtime/ai-doctrine.d.ts +6 -0
  228. package/src/lib/runtime/ai-doctrine.js +16 -6
  229. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  230. package/src/lib/runtime/ai-eval-live.js +4 -0
  231. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  232. package/src/lib/runtime/ai-eval.d.ts +15 -1
  233. package/src/lib/runtime/ai-eval.js +52 -1
  234. package/src/lib/runtime/ai-eval.js.map +1 -1
  235. package/src/lib/runtime/experiment-variant-copy.d.ts +34 -0
  236. package/src/lib/runtime/experiment-variant-copy.js +102 -0
  237. package/src/lib/runtime/experiment-variant-copy.js.map +1 -0
  238. package/src/lib/server/ai-experiment-versions.d.ts +51 -0
  239. package/src/lib/server/ai-experiment-versions.js +270 -0
  240. package/src/lib/server/ai-experiment-versions.js.map +1 -0
  241. package/src/lib/server/ai-jobs-resume.js +74 -4
  242. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  243. package/src/lib/server/ai-media-image.d.ts +66 -0
  244. package/src/lib/server/ai-media-image.js +521 -0
  245. package/src/lib/server/ai-media-image.js.map +1 -0
  246. package/src/lib/server/ai-media-svg.d.ts +91 -0
  247. package/src/lib/server/ai-media-svg.js +186 -0
  248. package/src/lib/server/ai-media-svg.js.map +1 -0
  249. package/src/lib/server/assist-build.d.ts +54 -0
  250. package/src/lib/server/assist-build.js +121 -0
  251. package/src/lib/server/assist-build.js.map +1 -0
  252. package/src/lib/server/assist-chat.js +108 -12
  253. package/src/lib/server/assist-chat.js.map +1 -1
  254. package/src/lib/server/assist-edit.js +213 -3
  255. package/src/lib/server/assist-edit.js.map +1 -1
  256. package/src/lib/server/assist-view-context.js +242 -4
  257. package/src/lib/server/assist-view-context.js.map +1 -1
  258. package/src/lib/server.js +26 -0
  259. package/src/lib/server.js.map +1 -1
  260. package/src/lib/tools/ai-logic-tool.d.ts +27 -0
  261. package/src/lib/tools/ai-logic-tool.js +207 -0
  262. package/src/lib/tools/ai-logic-tool.js.map +1 -0
  263. package/src/lib/tools/ai-overlay-tool.d.ts +35 -0
  264. package/src/lib/tools/ai-overlay-tool.js +107 -0
  265. package/src/lib/tools/ai-overlay-tool.js.map +1 -0
  266. package/src/lib/tools/ai-workflow-tool.d.ts +11 -0
  267. package/src/lib/tools/ai-workflow-tool.js +31 -0
  268. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  269. package/src/lib/usage/assist-job-refund.d.ts +31 -3
  270. package/src/lib/usage/assist-job-refund.js +46 -10
  271. package/src/lib/usage/assist-job-refund.js.map +1 -1
  272. package/src/lib/usage/assist-usage.d.ts +5 -0
  273. package/src/lib/usage/assist-usage.js +2 -0
  274. package/src/lib/usage/assist-usage.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-resume.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// lockdown-423: via libs/plugins/ai/src/lib/runtime/ai-gate.ts\n// The POST climbs `aiGateLadder`, whose lockdown rung is the verdict.\n\nimport { randomUUID } from 'crypto'\nimport { aiJobAdmissionRefusal, aiJobSiteRefusal } from '../jobs/ai-job-admission'\nimport {\n AI_JOB_INLINE_BUDGET_MS,\n aiJobNextStepMinimumMs,\n aiJobSummary,\n getAiJob,\n resumeAiJob,\n runAiJobStep,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport { aiGateLadder } from '../runtime/ai-gate'\nimport { releaseAssistMessage } from '../usage/assist-usage'\n\n/**\n * Resume a job that stopped for a person (AGL-2935): confirm its plan, or\n * try again the step whose answer broke a building rule on its re-ask.\n *\n * A door that SPENDS — the next step calls the provider — so it climbs the\n * whole ladder the create door climbs: `aiGenerative`,\n * `release_ai_generative`, the `ai-generate` switch, `ai.generate` on the\n * job's site, a per-uid window and a reservation. It then runs the next\n * step inline on that reservation, the way the create door runs the first,\n * and hands the reservation back whenever no step runs here.\n *\n * The body names the org and the job's site. A site that is not the job's\n * is refused, so a collaborator's permission on one site never confirms a\n * job on another. Audited per resume that changed something.\n */\n\nconst AI_JOBS_RESUME_RATE_LIMIT = { key: 'ai-jobs-resume', limit: 10, windowMs: 60_000 }\n\nexport async function POST(\n request: Request,\n context: { params: Promise<{ jobId: string }> },\n): Promise<Response> {\n let payload: Record<string, unknown> | null\n try {\n payload = (await request.json()) as Record<string, unknown>\n } catch {\n payload = null\n }\n const rawHost = payload?.['hostId']\n const hostId = typeof rawHost === 'string' && rawHost ? rawHost : null\n const gate = await aiGateLadder(\n { request, orgId: String(payload?.['orgId'] ?? ''), hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: AI_JOBS_RESUME_RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n // No token is spent until a step runs, so every exit before one hands the\n // message back.\n const release = () =>\n releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(() => undefined)\n\n const { jobId } = await context.params\n const existing = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!existing) {\n await release()\n return Response.json({ error: 'Unknown job' }, { status: 404 })\n }\n if ((existing.hostId ?? null) !== hostId) {\n await release()\n return Response.json({ error: 'That job belongs to another site' }, { status: 400 })\n }\n // A confirmed plan runs the step that writes, and an allowance free when the\n // job was created may be used by now (AGL-2909): the kind is asked again\n // before anything runs, and a refusal leaves the job waiting for review.\n if (existing.status === 'needs_review') {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>>\n try {\n // A site that switched AI off first (AGL-3028), then the kind's own check.\n refusal =\n (await aiJobSiteRefusal({\n firestore: gate.firestore,\n org: gate.org,\n hostId: existing.hostId ?? null,\n })) ??\n (await aiJobAdmissionRefusal(existing.kind, {\n firestore: gate.firestore,\n orgId: gate.orgId,\n hostId: existing.hostId ?? null,\n inputs: existing.inputs ?? {},\n org: gate.org,\n // The plan being confirmed, for a kind that builds only some plans.\n plan: existing.plan ?? null,\n uid: gate.uid,\n }))\n } catch (error) {\n await release()\n console.error('ai job admission failed', { orgId: gate.orgId, jobId, error })\n return Response.json({ error: 'The AI job could not be resumed' }, { status: 500 })\n }\n if (refusal) {\n await release()\n return Response.json(\n { error: refusal.error, job: aiJobSummary(existing, new Date()) },\n { status: refusal.status },\n )\n }\n }\n const now = new Date()\n const { job, changed } = await resumeAiJob(\n gate.firestore,\n gate.orgId,\n jobId,\n { uid: gate.uid },\n now,\n )\n if (!changed) {\n await release()\n return Response.json(\n { error: 'This job is not waiting for review', job: aiJobSummary(job, now) },\n { status: 409 },\n )\n }\n await writeAiJobAudit(gate.firestore, {\n action: 'ai.job.resume',\n actorUid: gate.uid,\n actorEmail: gate.decoded.email ?? null,\n orgId: gate.orgId,\n jobId,\n after: {\n status: job.status,\n reason: existing.review?.reason ?? null,\n kind: existing.kind,\n },\n })\n if (job.status !== 'queued') {\n await release()\n return Response.json({ job: aiJobSummary(job, now) }, { status: 200 })\n }\n // A step that needs more time than this request has (AGL-2907) is left\n // queued for the beat, which starts it with a budget of its own: a provider\n // call this request's timeout cut off would be billed upstream and metered\n // nowhere.\n if (aiJobNextStepMinimumMs(job) > AI_JOB_INLINE_BUDGET_MS) {\n await release()\n return Response.json({ job: aiJobSummary(job, now) }, { status: 200 })\n }\n\n const run = await runAiJobStep(gate.firestore, gate.orgId, jobId, {\n owner: `route:${randomUUID()}`,\n now,\n reservation: gate.reservation,\n org: gate.org,\n signal: AbortSignal.timeout(AI_JOB_INLINE_BUDGET_MS),\n actor: { uid: gate.uid, email: gate.decoded.email ?? null },\n // The workspace's AI pause lets staff through, as the ladder above did.\n staff: gate.staff,\n })\n if (run.outcome === 'not-claimable') {\n // The beat claimed the step between the resume and this run; it runs\n // there, on a reservation of its own.\n await release()\n const latest = await getAiJob(gate.firestore, gate.orgId, jobId)\n return Response.json({ job: aiJobSummary(latest ?? job, now) }, { status: 200 })\n }\n return Response.json({ job: aiJobSummary(run.job) }, { status: 200 })\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["randomUUID","aiJobAdmissionRefusal","aiJobSiteRefusal","AI_JOB_INLINE_BUDGET_MS","aiJobNextStepMinimumMs","aiJobSummary","getAiJob","resumeAiJob","runAiJobStep","writeAiJobAudit","aiGateLadder","releaseAssistMessage","AI_JOBS_RESUME_RATE_LIMIT","key","limit","windowMs","POST","request","context","existing","gate","payload","json","rawHost","hostId","orgId","String","feature","releaseFlag","lockdownFeature","permission","rateLimit","Response","release","firestore","reservation","catch","undefined","jobId","params","error","status","refusal","org","kind","inputs","plan","uid","console","job","Date","now","changed","action","actorUid","actorEmail","decoded","email","after","reason","review","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","latest","dynamic","maxDuration"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-resume.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// lockdown-423: via libs/plugins/ai/src/lib/runtime/ai-gate.ts\n// The POST climbs `aiGateLadder`, whose lockdown rung is the verdict.\n\nimport { randomUUID } from 'crypto'\nimport { aiJobAdmissionRefusal, aiJobSiteRefusal } from '../jobs/ai-job-admission'\nimport {\n AI_JOB_INLINE_BUDGET_MS,\n aiJobNextStepMinimumMs,\n aiJobSummary,\n getAiJob,\n aiBuildRetryable,\n resumeAiJob,\n retryAiBuildJob,\n runAiJobStep,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport { aiBuildFreeBalanceRefusal, aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport { aiBuildCreditEstimate, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from '../model/ai-build-job'\nimport { aiGateLadder } from '../runtime/ai-gate'\nimport { releaseAssistMessage } from '../usage/assist-usage'\n\n/**\n * Resume a job that stopped for a person (AGL-2935): confirm its plan, or\n * try again the step whose answer broke a building rule on its re-ask.\n *\n * A door that SPENDS — the next step calls the provider — so it climbs the\n * whole ladder the create door climbs: `aiGenerative`,\n * `release_ai_generative`, the `ai-generate` switch, `ai.generate` on the\n * job's site, a per-uid window and a reservation. It then runs the next\n * step inline on that reservation, the way the create door runs the first,\n * and hands the reservation back whenever no step runs here.\n *\n * The body names the org and the job's site. A site that is not the job's\n * is refused, so a collaborator's permission on one site never confirms a\n * job on another. Audited per resume that changed something.\n */\n\nconst AI_JOBS_RESUME_RATE_LIMIT = { key: 'ai-jobs-resume', limit: 10, windowMs: 60_000 }\n\nexport async function POST(\n request: Request,\n context: { params: Promise<{ jobId: string }> },\n): Promise<Response> {\n let payload: Record<string, unknown> | null\n try {\n payload = (await request.json()) as Record<string, unknown>\n } catch {\n payload = null\n }\n const rawHost = payload?.['hostId']\n const hostId = typeof rawHost === 'string' && rawHost ? rawHost : null\n const gate = await aiGateLadder(\n { request, orgId: String(payload?.['orgId'] ?? ''), hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: AI_JOBS_RESUME_RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n // No token is spent until a step runs, so every exit before one hands the\n // message back.\n const release = () =>\n releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(() => undefined)\n\n const { jobId } = await context.params\n const existing = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!existing) {\n await release()\n return Response.json({ error: 'Unknown job' }, { status: 404 })\n }\n if ((existing.hostId ?? null) !== hostId) {\n await release()\n return Response.json({ error: 'That job belongs to another site' }, { status: 400 })\n }\n // Try again on a build (AGL-3616): its failed items and what they left\n // unbuilt, never what succeeded, and never its plan again.\n const retryFailedItems = payload?.['retry'] === 'failed-items'\n if (retryFailedItems) {\n const refusal = await aiBuildRetryRefusal(gate, existing).catch((error: unknown) => {\n console.error('ai build retry admission failed', { orgId: gate.orgId, jobId, error })\n return { status: 500 as const, error: 'The AI job could not be resumed' }\n })\n if (refusal) {\n await release()\n return Response.json({ error: refusal.error, job: aiJobSummary(existing, new Date()) }, { status: refusal.status })\n }\n }\n // A confirmed plan runs the step that writes, and an allowance free when the\n // job was created may be used by now (AGL-2909): the kind is asked again\n // before anything runs, and a refusal leaves the job waiting for review.\n if (existing.status === 'needs_review') {\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>>\n try {\n // A site that switched AI off first (AGL-3028), then the kind's own check.\n refusal =\n (await aiJobSiteRefusal({\n firestore: gate.firestore,\n org: gate.org,\n hostId: existing.hostId ?? null,\n })) ??\n (await aiJobAdmissionRefusal(existing.kind, {\n firestore: gate.firestore,\n orgId: gate.orgId,\n hostId: existing.hostId ?? null,\n inputs: existing.inputs ?? {},\n org: gate.org,\n // The plan being confirmed, for a kind that builds only some plans.\n plan: existing.plan ?? null,\n uid: gate.uid,\n }))\n } catch (error) {\n await release()\n console.error('ai job admission failed', { orgId: gate.orgId, jobId, error })\n return Response.json({ error: 'The AI job could not be resumed' }, { status: 500 })\n }\n if (refusal) {\n await release()\n return Response.json(\n { error: refusal.error, job: aiJobSummary(existing, new Date()) },\n { status: refusal.status },\n )\n }\n }\n const now = new Date()\n const { job, changed } = retryFailedItems\n ? await retryAiBuildJob(gate.firestore, gate.orgId, jobId, now)\n : await resumeAiJob(gate.firestore, gate.orgId, jobId, { uid: gate.uid }, now, {\n publishConfirmed: payload?.['publish'] === true,\n })\n if (!changed) {\n await release()\n return Response.json(\n { error: 'This job is not waiting for review', job: aiJobSummary(job, now) },\n { status: 409 },\n )\n }\n await writeAiJobAudit(gate.firestore, {\n action: 'ai.job.resume',\n actorUid: gate.uid,\n actorEmail: gate.decoded.email ?? null,\n orgId: gate.orgId,\n jobId,\n after: {\n status: job.status,\n reason: retryFailedItems ? 'retry-failed-items' : (existing.review?.reason ?? null),\n kind: existing.kind,\n },\n })\n if (job.status !== 'queued') {\n await release()\n return Response.json({ job: aiJobSummary(job, now) }, { status: 200 })\n }\n // A step that needs more time than this request has (AGL-2907) is left\n // queued for the beat, which starts it with a budget of its own: a provider\n // call this request's timeout cut off would be billed upstream and metered\n // nowhere.\n if (aiJobNextStepMinimumMs(job) > AI_JOB_INLINE_BUDGET_MS) {\n await release()\n return Response.json({ job: aiJobSummary(job, now) }, { status: 200 })\n }\n\n const run = await runAiJobStep(gate.firestore, gate.orgId, jobId, {\n owner: `route:${randomUUID()}`,\n now,\n reservation: gate.reservation,\n org: gate.org,\n signal: AbortSignal.timeout(AI_JOB_INLINE_BUDGET_MS),\n actor: { uid: gate.uid, email: gate.decoded.email ?? null },\n // The workspace's AI pause lets staff through, as the ladder above did.\n staff: gate.staff,\n })\n if (run.outcome === 'not-claimable') {\n // The beat claimed the step between the resume and this run; it runs\n // there, on a reservation of its own.\n await release()\n const latest = await getAiJob(gate.firestore, gate.orgId, jobId)\n return Response.json({ job: aiJobSummary(latest ?? job, now) }, { status: 200 })\n }\n return Response.json({ job: aiJobSummary(run.job) }, { status: 200 })\n}\n\n/**\n * Why a build's Try again cannot run (AGL-3616), or `null`: it is not a\n * finished build, the site switched AI off, or — on the Free taste — the\n * month's Free credits do not cover what the retried items are estimated at.\n */\nasync function aiBuildRetryRefusal(\n gate: { firestore: FirebaseFirestore.Firestore; org: object | null },\n job: AiJob,\n): Promise<{ status: 400 | 403 | 404 | 409; error: string } | null> {\n const plan = job.plan?.status === 'confirmed' ? job.plan : null\n if (!aiBuildRetryable(job) || !plan || !job.items?.length) {\n return { status: 409, error: 'Only a finished build can try its failed items again' }\n }\n const site = await aiJobSiteRefusal({ firestore: gate.firestore, org: gate.org, hostId: job.hostId ?? null })\n if (site) return site\n const { retried } = aiBuildRetryLedger(job.items, job.kind === 'site' ? aiLedgerUnits(job.items) : aiBuildUnits(plan))\n if (!retried.length) return { status: 409, error: 'Nothing in this build is left to try again' }\n // A site's retried parts are its own pages and creations, held by the\n // reservation each pass takes; a build's are estimated against the Free taste.\n if (job.kind === 'site' || !aiBuildFreeTaste(gate.org)) return null\n const refusal = await aiBuildFreeBalanceRefusal({\n firestore: gate.firestore,\n org: gate.org,\n estimate: aiBuildCreditEstimate(plan, { slots: new Set(retried) }),\n now: new Date(),\n })\n return refusal ? { status: 403, error: refusal } : null\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["randomUUID","aiJobAdmissionRefusal","aiJobSiteRefusal","AI_JOB_INLINE_BUDGET_MS","aiJobNextStepMinimumMs","aiJobSummary","getAiJob","aiBuildRetryable","resumeAiJob","retryAiBuildJob","runAiJobStep","writeAiJobAudit","aiBuildFreeBalanceRefusal","aiBuildFreeTaste","aiBuildCreditEstimate","aiBuildRetryLedger","aiBuildUnits","aiLedgerUnits","aiGateLadder","releaseAssistMessage","AI_JOBS_RESUME_RATE_LIMIT","key","limit","windowMs","POST","request","context","existing","gate","payload","json","rawHost","hostId","orgId","String","feature","releaseFlag","lockdownFeature","permission","rateLimit","Response","release","firestore","reservation","catch","undefined","jobId","params","error","status","retryFailedItems","refusal","aiBuildRetryRefusal","console","job","Date","org","kind","inputs","plan","uid","now","changed","publishConfirmed","action","actorUid","actorEmail","decoded","email","after","reason","review","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","latest","items","length","site","retried","estimate","slots","Set","dynamic","maxDuration"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type AiImageMode, type AiSvgStyle } from '../providers/image-contract';
18
+ /** What a person reads when a picture failed on our side. */
19
+ export declare const AI_MEDIA_OURS_COPY = "This one's on us \u2014 you weren't charged. The picture didn't come out right; try again, or describe it a little differently.";
20
+ /** What the door answers on a deployment that makes no photos. */
21
+ export declare const AI_MEDIA_NO_PHOTOS_COPY = "Photos aren't available here yet. Choose Illustration to draw one instead.";
22
+ /** Every control character as a space, squeezed, trimmed and cut. */
23
+ export declare function cleanAiImagePrompt(value: unknown): string;
24
+ /**
25
+ * The alt text a picture is stored with: the sentence given, as a sentence,
26
+ * cut at a word inside the library's alt limit. A person edits it like any
27
+ * other.
28
+ */
29
+ export declare function aiImageAltText(text: string): string;
30
+ /** A file name from the description's first words: `ai-a-red-barn-2.png`. */
31
+ export declare function aiImageFileName(prompt: string, index: number, mimeType: string): string;
32
+ /** What the library asked for, read and bounded. */
33
+ export interface AiMediaImageInput {
34
+ orgId: string;
35
+ library: 'host' | 'org';
36
+ /** The site whose library this is, or the site on screen for the org's. */
37
+ hostId: string | null;
38
+ folderId: string | null;
39
+ mode: AiImageMode;
40
+ prompt: string;
41
+ aspectRatio: string;
42
+ count: number;
43
+ /** An illustration's kind; unread for a photo. */
44
+ style: string;
45
+ /** An illustration's colors: the site theme's, or these. */
46
+ palette: {
47
+ source: 'theme';
48
+ } | {
49
+ source: 'custom';
50
+ colors: string[];
51
+ };
52
+ }
53
+ export declare function parseAiMediaImageInput(body: Record<string, unknown> | null): AiMediaImageInput;
54
+ /** The record a generated picture keeps of how it was made. */
55
+ export interface AiMediaProvenance {
56
+ model: string;
57
+ prompt: string;
58
+ mode: AiImageMode;
59
+ /** An illustration's kind; absent on a photo. */
60
+ style?: AiSvgStyle;
61
+ aspectRatio: string;
62
+ /** The meter's signal id for the request that made it. */
63
+ signalId: string | null;
64
+ generatedBy: string;
65
+ }
66
+ export declare function POST(request: Request): Promise<Response>;
@@ -0,0 +1,521 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ // lockdown-423: via libs/plugins/ai/src/lib/runtime/ai-gate.ts
18
+ // The POST climbs `aiGateLadder`, whose lockdown rung is the verdict, and
19
+ // every stored picture also passes the upload route's.
20
+ import { mediaFilterKeys } from "@aglyn/aglyn/app-utils/media-metadata";
21
+ import { MEDIA_ALT_MAX_LENGTH } from "@aglyn/aglyn/app-utils/media-alt";
22
+ import { aiMediaCreditsPerPicture } from "../model/ai-media-credits.js";
23
+ import { aiSvgThemePalette, isAiSvgColor } from "../model/ai-svg.js";
24
+ import { estimateAiBilledUsd } from "../providers/catalog.js";
25
+ import { AI_UPSTREAM_FAILURE_COPY, AiUpstreamError } from "../providers/contract.js";
26
+ import { AI_IMAGE_ASPECT_RATIOS, AI_IMAGE_MAX_COUNT, AI_IMAGE_PROMPT_MAX_CHARS, AI_SVG_MAX_COLORS, AI_SVG_STYLES, AiImageSafetyRefusal, isAiImageAspectRatio } from "../providers/image-contract.js";
27
+ import { vertexImageProvider } from "../providers/vertex-image.js";
28
+ import { aiGateLadder } from "../runtime/ai-gate.js";
29
+ import { aiInventoryTheme } from "../runtime/site-inventory.js";
30
+ import { assistBandRefuses, assistCreditsFromUsd } from "../usage/assist-credits.js";
31
+ import { publicAssistQuota, recordAssistCost, releaseAssistMessage } from "../usage/assist-usage.js";
32
+ import { aiMediaSvgModel, generateAiMediaSvg } from "./ai-media-svg.js";
33
+ /**
34
+ * "Create with AI" in Media (AGL-3602): pictures from a description, stored
35
+ * in the library that asked.
36
+ *
37
+ * `POST /api/ai/media/images { orgId, library, hostId?, folderId?, mode,
38
+ * prompt, aspectRatio, count, style?, palette? }` answers
39
+ * `{ mediaIds, filtered, failed, credits, warning? }`.
40
+ *
41
+ * Two modes:
42
+ *
43
+ * - `photo` — Google's image models on Vertex AI (`vertex-image.ts`). Off
44
+ * unless the deployment configured them; the door answers 404 with a
45
+ * sentence the dialog shows.
46
+ * - `illustration` — an SVG drawn by the text provider every other AI door
47
+ * uses (`ai-media-svg.ts`): an illustration, an icon, a tileable pattern or
48
+ * a simple logo mark, in the site's theme colors or the person's own. No
49
+ * new vendor, so it runs wherever text AI does.
50
+ *
51
+ * There is no GET: the button draws from the shell's own gates, and this door
52
+ * decides when someone asks for a picture.
53
+ *
54
+ * ## A request, not a job
55
+ *
56
+ * Both modes answer inside the request, the way "Save as a component with
57
+ * AI" does, and are metered on the same meter every door uses.
58
+ *
59
+ * ## The money, in order
60
+ *
61
+ * 1. `aiGateLadder` RESERVES before anything is spent: the `aiGenerative`
62
+ * entitlement, `release_ai_generative`, the `ai-generate` kill switch,
63
+ * `ai.generate` on the site, the per-account and per-address windows, the
64
+ * Free taste's own rungs and the band.
65
+ * 2. A workspace whose band is a wall (Free, or a hard cap) is refused before
66
+ * the provider is called when the pictures' estimate is more than it has
67
+ * left.
68
+ * 3. The provider is called. A fault of ours or the provider's hands the
69
+ * reservation back and charges nothing. A declined description charges
70
+ * only what the decline spent, recorded as a declined request (which a
71
+ * Free workspace is never charged for, and which its daily refusal pause
72
+ * counts).
73
+ * 4. Each picture is stored, and only what was stored is SETTLED, at billed
74
+ * rates above the provider's: a photo per picture plus its prompt and
75
+ * thinking, an illustration as the tokens that drew it. A picture held
76
+ * back, one the library refused, and an illustration that failed its
77
+ * safety check twice are never charged — that last one is ours.
78
+ *
79
+ * ## Stored the way any upload is stored
80
+ *
81
+ * Each picture is posted to the console's own `/api/media/upload` with the
82
+ * caller's credential, so the scope, the uploads lockdown, the structural
83
+ * inspection, SVG sanitizing, the quarantine list, the storage band, the CDN
84
+ * variants and the storage counter all apply exactly as they do to a file
85
+ * the person dropped in. This door writes no media document of its own; it
86
+ * only adds, to the documents that route made, the alt text and the record
87
+ * of how the picture was made.
88
+ */ /** The per-account window: a person makes a few sets a minute, not dozens. */ const RATE_LIMIT = {
89
+ key: 'ai-media-image',
90
+ limit: 6,
91
+ windowMs: 60000
92
+ };
93
+ /** What a person reads when a picture failed on our side. */ export const AI_MEDIA_OURS_COPY = "This one's on us — you weren't charged. The picture didn't come out right; try again, or describe it a little differently.";
94
+ /** What the door answers on a deployment that makes no photos. */ export const AI_MEDIA_NO_PHOTOS_COPY = "Photos aren't available here yet. Choose Illustration to draw one instead.";
95
+ /** The provider photos come from; a spec stands one in through jest. */ function imageProvider() {
96
+ return vertexImageProvider;
97
+ }
98
+ /** Every control character as a space, squeezed, trimmed and cut. */ export function cleanAiImagePrompt(value) {
99
+ let out = '';
100
+ for (const char of String(value != null ? value : '')){
101
+ const code = char.charCodeAt(0);
102
+ out += code < 32 || code === 127 || code === 0x2028 || code === 0x2029 ? ' ' : char;
103
+ }
104
+ return out.replace(/\s+/g, ' ').trim().slice(0, AI_IMAGE_PROMPT_MAX_CHARS);
105
+ }
106
+ /**
107
+ * The alt text a picture is stored with: the sentence given, as a sentence,
108
+ * cut at a word inside the library's alt limit. A person edits it like any
109
+ * other.
110
+ */ export function aiImageAltText(text) {
111
+ const clean = text.replace(/\s+/g, ' ').trim();
112
+ if (!clean) return '';
113
+ const sentence = clean.charAt(0).toUpperCase() + clean.slice(1);
114
+ if (sentence.length <= MEDIA_ALT_MAX_LENGTH) return sentence;
115
+ const cut = sentence.slice(0, MEDIA_ALT_MAX_LENGTH - 1);
116
+ const space = cut.lastIndexOf(' ');
117
+ return `${(space > 40 ? cut.slice(0, space) : cut).trim()}…`;
118
+ }
119
+ const EXTENSIONS = {
120
+ 'image/png': 'png',
121
+ 'image/jpeg': 'jpg',
122
+ 'image/webp': 'webp',
123
+ 'image/svg+xml': 'svg'
124
+ };
125
+ /** A file name from the description's first words: `ai-a-red-barn-2.png`. */ export function aiImageFileName(prompt, index, mimeType) {
126
+ var _EXTENSIONS_mimeType;
127
+ const words = prompt.toLowerCase().normalize('NFKD').replace(/[^a-z0-9\s-]/g, ' ').split(/\s+/).filter(Boolean).slice(0, 6).join('-').slice(0, 60).replace(/-+$/, '');
128
+ return `ai-${words || 'image'}-${index + 1}.${(_EXTENSIONS_mimeType = EXTENSIONS[mimeType]) != null ? _EXTENSIONS_mimeType : 'png'}`;
129
+ }
130
+ export function parseAiMediaImageInput(body) {
131
+ var _ref;
132
+ const text = (key, limit = 128)=>{
133
+ const value = body == null ? void 0 : body[key];
134
+ return typeof value === 'string' ? value.trim().slice(0, limit) : '';
135
+ };
136
+ const count = Math.floor(Number((_ref = body == null ? void 0 : body['count']) != null ? _ref : 1));
137
+ const palette = body == null ? void 0 : body['palette'];
138
+ const colors = Array.isArray(palette == null ? void 0 : palette['colors']) ? palette == null ? void 0 : palette['colors'] : [];
139
+ return {
140
+ orgId: text('orgId'),
141
+ library: (body == null ? void 0 : body['library']) === 'org' ? 'org' : 'host',
142
+ hostId: text('hostId') || null,
143
+ folderId: text('folderId', 64) || null,
144
+ mode: (body == null ? void 0 : body['mode']) === 'illustration' ? 'illustration' : 'photo',
145
+ prompt: cleanAiImagePrompt(body == null ? void 0 : body['prompt']),
146
+ aspectRatio: text('aspectRatio', 8),
147
+ count: Number.isFinite(count) ? count : 0,
148
+ style: text('style', 24) || 'illustration',
149
+ palette: (palette == null ? void 0 : palette['source']) === 'custom' ? {
150
+ source: 'custom',
151
+ colors: colors.map((color)=>String(color != null ? color : '').trim().toLowerCase()).slice(0, AI_SVG_MAX_COLORS + 1)
152
+ } : {
153
+ source: 'theme'
154
+ }
155
+ };
156
+ }
157
+ /**
158
+ * Posts one picture to the console's own upload route, as the caller. The
159
+ * same body the library's Upload button sends, so the route cannot tell the
160
+ * two apart and applies every check it applies to that one.
161
+ */ async function storeThroughUploadRoute(request, input, image, fileName) {
162
+ const scope = input.library === 'org' ? _extends({
163
+ orgId: input.orgId
164
+ }, input.hostId ? {
165
+ forHostId: input.hostId
166
+ } : {}) : {
167
+ hostId: input.hostId
168
+ };
169
+ let response;
170
+ try {
171
+ var _request_headers_get;
172
+ response = await fetch(new URL('/api/media/upload', request.url), {
173
+ method: 'POST',
174
+ headers: {
175
+ Authorization: (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '',
176
+ 'Content-Type': 'application/json'
177
+ },
178
+ body: JSON.stringify(_extends({}, scope, {
179
+ fileName,
180
+ contentType: image.mimeType,
181
+ folderId: input.folderId,
182
+ data: image.base64
183
+ }))
184
+ });
185
+ } catch (unused) {
186
+ return {
187
+ error: 'The picture could not be saved to the media library.',
188
+ status: 502
189
+ };
190
+ }
191
+ const payload = await response.json().catch(()=>({}));
192
+ const mediaId = typeof payload['mediaId'] === 'string' ? payload['mediaId'] : '';
193
+ if (response.ok && mediaId) return {
194
+ mediaId
195
+ };
196
+ return {
197
+ error: typeof payload['error'] === 'string' ? payload['error'] : 'The picture could not be saved to the media library.',
198
+ status: response.ok ? 502 : response.status
199
+ };
200
+ }
201
+ /**
202
+ * Adds the alt text and the provenance to a document the upload route made.
203
+ * An `update`, which cannot mint a document: the route minted it.
204
+ */ async function annotateStoredImage(gate, input, mediaId, alt, provenance) {
205
+ var _data;
206
+ const scopeRef = input.library === 'org' ? gate.firestore.collection('orgs').doc(input.orgId) : gate.firestore.collection('hosts').doc(String(input.hostId));
207
+ const mediaRef = scopeRef.collection('media').doc(mediaId);
208
+ const stored = (_data = (await mediaRef.get()).data()) != null ? _data : {};
209
+ await mediaRef.update(_extends({
210
+ alt,
211
+ description: 'Created with AI from a description.',
212
+ aiGenerated: _extends({}, provenance, {
213
+ createdAt: new Date()
214
+ })
215
+ }, mediaFilterKeys(_extends({}, stored, {
216
+ alt
217
+ }))));
218
+ }
219
+ /** The colors an illustration is drawn in. */ async function illustrationPalette(gate, input) {
220
+ var _snapshot_data;
221
+ var _aiInventoryTheme;
222
+ if (input.palette.source === 'custom') return input.palette.colors;
223
+ if (!input.hostId) return [];
224
+ const snapshot = await gate.firestore.collection('hosts').doc(input.hostId).get();
225
+ return aiSvgThemePalette((_aiInventoryTheme = aiInventoryTheme(snapshot.exists ? (_snapshot_data = snapshot.data()) != null ? _snapshot_data : null : null)) == null ? void 0 : _aiInventoryTheme.colors);
226
+ }
227
+ const ZERO_USAGE = {
228
+ inputTokens: 0,
229
+ outputTokens: 0,
230
+ cacheReadTokens: 0,
231
+ cacheWriteTokens: 0
232
+ };
233
+ const sumUsage = (usages)=>usages.reduce((sum, usage)=>({
234
+ inputTokens: sum.inputTokens + usage.inputTokens,
235
+ outputTokens: sum.outputTokens + usage.outputTokens,
236
+ cacheReadTokens: sum.cacheReadTokens + usage.cacheReadTokens,
237
+ cacheWriteTokens: sum.cacheWriteTokens + usage.cacheWriteTokens
238
+ }), ZERO_USAGE);
239
+ export async function POST(request) {
240
+ var _ref;
241
+ let body;
242
+ try {
243
+ body = await request.json();
244
+ } catch (unused) {
245
+ body = null;
246
+ }
247
+ const input = parseAiMediaImageInput(body);
248
+ const provider = imageProvider();
249
+ const svgModel = input.mode === 'illustration' ? aiMediaSvgModel() : undefined;
250
+ // A deployment that makes no pictures of this kind has no such door, so
251
+ // nothing about the caller or the workspace is read first.
252
+ if (input.mode === 'photo' && !provider.configured()) {
253
+ return Response.json({
254
+ error: AI_MEDIA_NO_PHOTOS_COPY,
255
+ reason: 'unavailable'
256
+ }, {
257
+ status: 404
258
+ });
259
+ }
260
+ if (input.mode === 'illustration' && !svgModel) {
261
+ return Response.json({
262
+ error: 'Not found'
263
+ }, {
264
+ status: 404
265
+ });
266
+ }
267
+ const gate = await aiGateLadder({
268
+ request,
269
+ orgId: input.orgId,
270
+ hostId: input.hostId
271
+ }, {
272
+ feature: 'aiGenerative',
273
+ releaseFlag: 'release_ai_generative',
274
+ lockdownFeature: 'ai-generate',
275
+ permission: 'ai.generate',
276
+ rateLimit: RATE_LIMIT
277
+ });
278
+ if (gate instanceof Response) return gate;
279
+ // Nothing is spent until a provider answers, so every exit before it
280
+ // hands the reservation back.
281
+ const release = ()=>releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(()=>undefined);
282
+ const refuse = async (error, status, extra = {})=>{
283
+ await release();
284
+ return Response.json(_extends({
285
+ error
286
+ }, extra), {
287
+ status
288
+ });
289
+ };
290
+ if (input.library === 'host' && !input.hostId) {
291
+ return refuse('Open the site whose media library the pictures go in.', 400);
292
+ }
293
+ if (!input.prompt) return refuse('Describe the picture you want first.', 400);
294
+ if (!isAiImageAspectRatio(input.aspectRatio)) {
295
+ return refuse(`Choose a shape: ${AI_IMAGE_ASPECT_RATIOS.join(', ')}.`, 400);
296
+ }
297
+ const aspectRatio = input.aspectRatio;
298
+ if (input.count < 1 || input.count > AI_IMAGE_MAX_COUNT) {
299
+ return refuse(`Make between 1 and ${AI_IMAGE_MAX_COUNT} pictures at a time.`, 400);
300
+ }
301
+ if (input.mode === 'illustration') {
302
+ if (!AI_SVG_STYLES.includes(input.style)) {
303
+ return refuse('Choose an illustration, an icon, a pattern or a logo mark.', 400);
304
+ }
305
+ if (input.palette.source === 'custom' && (!input.palette.colors.length || input.palette.colors.length > AI_SVG_MAX_COLORS || !input.palette.colors.every(isAiSvgColor))) {
306
+ return refuse(`Give between 1 and ${AI_SVG_MAX_COLORS} colors as hex values, such as #1a73e8.`, 400);
307
+ }
308
+ }
309
+ const model = input.mode === 'photo' ? provider.defaultModel() : svgModel;
310
+ const perPicture = aiMediaCreditsPerPicture(input.mode, model);
311
+ const estimate = perPicture * input.count;
312
+ const credits = publicAssistQuota(gate.reservation).credits;
313
+ if (credits && assistBandRefuses(gate.org) && credits.remaining < estimate) {
314
+ return refuse(credits.remaining < perPicture ? 'This workspace has used its AI credits for the month.' : `That needs about ${estimate} credits and this workspace has ${credits.remaining} left this month. Make fewer pictures.`, 429, {
315
+ reason: 'quota',
316
+ quota: publicAssistQuota(gate.reservation)
317
+ });
318
+ }
319
+ /** Records what a decline spent: a declined request, never a picture. */ const recordDecline = (usage, declinedModel)=>{
320
+ var _gate_reservation_free;
321
+ return recordAssistCost(gate.firestore, gate.orgId, {
322
+ route: '/api/ai/media/images',
323
+ hostId: input.hostId,
324
+ model: declinedModel,
325
+ tier: 'entitled',
326
+ usage: _extends({}, usage, {
327
+ images: 0
328
+ }),
329
+ docsPaths: [],
330
+ stopReason: 'refusal',
331
+ free: (_gate_reservation_free = gate.reservation.free) != null ? _gate_reservation_free : null,
332
+ uid: gate.uid,
333
+ kind: 'image'
334
+ }).catch((meterError)=>console.error('ai media refusal not recorded', {
335
+ orgId: gate.orgId,
336
+ meterError
337
+ }));
338
+ };
339
+ let made;
340
+ /** Pictures asked for and not made: held back, declined or failed on our side. */ let unmade;
341
+ /** Tokens a photo request spent beside its pictures, charged once any is stored. */ let photoUsage = ZERO_USAGE;
342
+ let declineReason = null;
343
+ let style;
344
+ if (input.mode === 'photo') {
345
+ try {
346
+ const result = await provider.generate({
347
+ model,
348
+ prompt: input.prompt,
349
+ aspectRatio,
350
+ count: input.count
351
+ });
352
+ photoUsage = result.usage;
353
+ unmade = result.filtered;
354
+ made = result.images.map((image)=>_extends({}, image, {
355
+ alt: aiImageAltText(input.prompt),
356
+ usage: ZERO_USAGE
357
+ }));
358
+ } catch (error) {
359
+ if (error instanceof AiImageSafetyRefusal) {
360
+ // Declined, not failed: no picture is charged, and the request is
361
+ // recorded as a declined one so the Free taste's refusal pause
362
+ // counts it.
363
+ await recordDecline(ZERO_USAGE, model);
364
+ return Response.json({
365
+ error: error.message,
366
+ reason: 'safety'
367
+ }, {
368
+ status: 422
369
+ });
370
+ }
371
+ await release();
372
+ console.error('ai photo generation failed', {
373
+ orgId: gate.orgId,
374
+ error
375
+ });
376
+ const status = error instanceof AiUpstreamError && error.accountProblem ? 503 : 502;
377
+ return Response.json({
378
+ error: AI_UPSTREAM_FAILURE_COPY
379
+ }, {
380
+ status
381
+ });
382
+ }
383
+ } else {
384
+ var _ref1;
385
+ var _declined_;
386
+ style = input.style;
387
+ const palette = await illustrationPalette(gate, input);
388
+ const settled = await Promise.allSettled(Array.from({
389
+ length: input.count
390
+ }, (_unused, variant)=>generateAiMediaSvg({
391
+ model,
392
+ prompt: input.prompt,
393
+ style: style,
394
+ aspectRatio,
395
+ palette,
396
+ variant,
397
+ count: input.count
398
+ })));
399
+ const outcomes = settled.flatMap((entry)=>entry.status === 'fulfilled' ? [
400
+ entry.value
401
+ ] : []);
402
+ if (!outcomes.length) {
403
+ var _settled_;
404
+ await release();
405
+ console.error('ai illustration generation failed', {
406
+ orgId: gate.orgId,
407
+ error: (_settled_ = settled[0]) == null ? void 0 : _settled_.reason
408
+ });
409
+ return Response.json({
410
+ error: AI_UPSTREAM_FAILURE_COPY
411
+ }, {
412
+ status: 502
413
+ });
414
+ }
415
+ const drawn = outcomes.filter((outcome)=>outcome.kind === 'drawn');
416
+ const declined = outcomes.filter((outcome)=>outcome.kind === 'declined');
417
+ unmade = input.count - drawn.length;
418
+ declineReason = (_ref1 = (_declined_ = declined[0]) == null ? void 0 : _declined_.reason) != null ? _ref1 : null;
419
+ made = drawn.map((outcome)=>({
420
+ base64: Buffer.from(outcome.svg, 'utf8').toString('base64'),
421
+ mimeType: 'image/svg+xml',
422
+ alt: aiImageAltText(outcome.alt || input.prompt),
423
+ usage: outcome.usage
424
+ }));
425
+ if (!made.length) {
426
+ if (declined.length) {
427
+ // The model declined: what it spent is a declined request.
428
+ await recordDecline(sumUsage(declined.map((outcome)=>outcome.usage)), model);
429
+ return Response.json({
430
+ error: declineReason,
431
+ reason: 'safety'
432
+ }, {
433
+ status: 422
434
+ });
435
+ }
436
+ // Every picture failed its check twice. Ours, so nothing is charged.
437
+ await release();
438
+ return Response.json({
439
+ error: AI_MEDIA_OURS_COPY,
440
+ reason: 'ours'
441
+ }, {
442
+ status: 502
443
+ });
444
+ }
445
+ }
446
+ const stored = [];
447
+ let refusal = null;
448
+ for (const [index, picture] of made.entries()){
449
+ const outcome = await storeThroughUploadRoute(request, input, picture, aiImageFileName(input.prompt, index, picture.mimeType));
450
+ if ('mediaId' in outcome) stored.push({
451
+ mediaId: outcome.mediaId,
452
+ picture
453
+ });
454
+ else refusal != null ? refusal : refusal = outcome;
455
+ }
456
+ if (!stored.length) {
457
+ var _ref2, _ref3;
458
+ // Made and not kept: the library refused every picture (its storage
459
+ // band, a lockdown, a permission), so nothing is charged.
460
+ await release();
461
+ return Response.json({
462
+ error: (_ref2 = refusal == null ? void 0 : refusal.error) != null ? _ref2 : 'The pictures could not be saved.'
463
+ }, {
464
+ status: (_ref3 = refusal == null ? void 0 : refusal.status) != null ? _ref3 : 502
465
+ });
466
+ }
467
+ // Only what was stored is settled: a photo's pictures at the per-picture
468
+ // rate with the request's prompt and thinking, an illustration's tokens.
469
+ const usage = input.mode === 'photo' ? _extends({}, photoUsage, {
470
+ images: stored.length
471
+ }) : sumUsage(stored.map((entry)=>entry.picture.usage));
472
+ let signalId = null;
473
+ try {
474
+ var _gate_reservation_free;
475
+ signalId = await recordAssistCost(gate.firestore, gate.orgId, {
476
+ route: '/api/ai/media/images',
477
+ hostId: input.hostId,
478
+ model,
479
+ tier: 'entitled',
480
+ usage,
481
+ docsPaths: [],
482
+ stopReason: 'end_turn',
483
+ free: (_gate_reservation_free = gate.reservation.free) != null ? _gate_reservation_free : null,
484
+ uid: gate.uid,
485
+ kind: 'image'
486
+ });
487
+ } catch (error) {
488
+ // The pictures are in the library and were paid for by us; a meter
489
+ // that could not be written is not a reason to hide them.
490
+ console.error('ai media usage not recorded', {
491
+ orgId: gate.orgId,
492
+ error
493
+ });
494
+ }
495
+ await Promise.all(stored.map(({ mediaId, picture })=>annotateStoredImage(gate, input, mediaId, picture.alt, _extends({
496
+ model,
497
+ prompt: input.prompt,
498
+ mode: input.mode
499
+ }, style ? {
500
+ style
501
+ } : {}, {
502
+ aspectRatio,
503
+ signalId,
504
+ generatedBy: gate.uid
505
+ })).catch((error)=>console.error('ai media alt text not written', {
506
+ mediaId,
507
+ error
508
+ }))));
509
+ const warning = (_ref = refusal == null ? void 0 : refusal.error) != null ? _ref : declineReason && unmade > 0 ? declineReason : unmade > 0 && input.mode === 'illustration' ? AI_MEDIA_OURS_COPY : null;
510
+ return Response.json(_extends({
511
+ mediaIds: stored.map((entry)=>entry.mediaId),
512
+ filtered: unmade,
513
+ failed: made.length - stored.length,
514
+ // What was charged, as the meter priced it.
515
+ credits: assistCreditsFromUsd(estimateAiBilledUsd(usage, model))
516
+ }, warning ? {
517
+ warning
518
+ } : {}));
519
+ }
520
+
521
+ //# sourceMappingURL=ai-media-image.js.map