@aglyn/plugins-ai 1.0.0-beta.236 → 1.0.0-beta.238

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 (164) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +117 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  9. package/src/lib/components/ai-job-plan.component.js +48 -12
  10. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  11. package/src/lib/components/ai-job-requests.d.ts +10 -0
  12. package/src/lib/components/ai-job-requests.js +12 -4
  13. package/src/lib/components/ai-job-requests.js.map +1 -1
  14. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  15. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  16. package/src/lib/components/ai-site-build-page.component.js +48 -9
  17. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  18. package/src/lib/components/ai-site-start-card.component.js +68 -22
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  21. package/src/lib/components/ai-usage-strip.component.js +40 -9
  22. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  23. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  24. package/src/lib/components/assist-build-card.component.js +58 -7
  25. package/src/lib/components/assist-build-card.component.js.map +1 -1
  26. package/src/lib/components/assist-jobs-drawer.component.js +66 -22
  27. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  28. package/src/lib/components/assist-panel.component.js +89 -52
  29. package/src/lib/components/assist-panel.component.js.map +1 -1
  30. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  31. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  32. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  33. package/src/lib/jobs/ai-job-build-step.js +54 -35
  34. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  36. package/src/lib/jobs/ai-job-cancel.js +102 -0
  37. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  38. package/src/lib/jobs/ai-job-component-step.js +4 -1
  39. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.js +2 -0
  41. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  42. package/src/lib/jobs/ai-job-form-step.js +4 -1
  43. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  45. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  46. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-language.js +12 -0
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  52. package/src/lib/jobs/ai-job-page-step.js +46 -4
  53. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
  55. package/src/lib/jobs/ai-job-plan-step.js +118 -18
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
  58. package/src/lib/jobs/ai-job-site-content.js +196 -37
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-step.js +49 -16
  61. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-template-step.js +4 -1
  63. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  64. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  65. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  66. package/src/lib/jobs/ai-jobs.d.ts +55 -13
  67. package/src/lib/jobs/ai-jobs.js +213 -41
  68. package/src/lib/jobs/ai-jobs.js.map +1 -1
  69. package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
  70. package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
  71. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  72. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  73. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  74. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  78. package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
  79. package/src/lib/layout-language/ai-layout-compiler.js +381 -18
  80. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  81. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  82. package/src/lib/layout-language/ai-layout-design.js +5 -3
  83. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  84. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  85. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  86. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  87. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  88. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  89. package/src/lib/layout-language/ai-layout-language.js +1 -1
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  92. package/src/lib/layout-language/ai-layout-listings.js +117 -47
  93. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  95. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  96. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  98. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  99. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  100. package/src/lib/model/ai-build-job.d.ts +39 -0
  101. package/src/lib/model/ai-build-job.js +100 -1
  102. package/src/lib/model/ai-build-job.js.map +1 -1
  103. package/src/lib/model/ai-build-progress.d.ts +8 -0
  104. package/src/lib/model/ai-build-progress.js +35 -0
  105. package/src/lib/model/ai-build-progress.js.map +1 -1
  106. package/src/lib/model/ai-credit-estimate.d.ts +249 -0
  107. package/src/lib/model/ai-credit-estimate.js +278 -0
  108. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  109. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  110. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  111. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  112. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  113. package/src/lib/model/ai-job-failure-copy.js +8 -3
  114. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  115. package/src/lib/model/ai-jobs.types.d.ts +35 -0
  116. package/src/lib/model/ai-jobs.types.js.map +1 -1
  117. package/src/lib/model/ai-site-build-progress.js +7 -4
  118. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-site-job.d.ts +76 -22
  120. package/src/lib/model/ai-site-job.js +125 -23
  121. package/src/lib/model/ai-site-job.js.map +1 -1
  122. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  123. package/src/lib/model/ai-site-kinds.js +35 -7
  124. package/src/lib/model/ai-site-kinds.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  126. package/src/lib/runtime/ai-doctrine-validators.js +161 -7
  127. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  128. package/src/lib/runtime/ai-doctrine.js +4 -3
  129. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  130. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  131. package/src/lib/runtime/ai-node-tree.js +26 -0
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.generated.js +592 -14
  134. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  135. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  136. package/src/lib/server/ai-jobs-cancel.js +27 -9
  137. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  138. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  139. package/src/lib/server/ai-jobs-gate.js +20 -9
  140. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  141. package/src/lib/server/ai-jobs-resume.js +93 -35
  142. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  143. package/src/lib/server/ai-jobs-route.js +45 -4
  144. package/src/lib/server/ai-jobs-route.js.map +1 -1
  145. package/src/lib/server/assist-chat.js +4 -1
  146. package/src/lib/server/assist-chat.js.map +1 -1
  147. package/src/lib/server/assist-edit.js +3 -1
  148. package/src/lib/server/assist-edit.js.map +1 -1
  149. package/src/lib/server/assist-music.d.ts +36 -0
  150. package/src/lib/server/assist-music.js +36 -0
  151. package/src/lib/server/assist-music.js.map +1 -0
  152. package/src/lib/server.js +6 -0
  153. package/src/lib/server.js.map +1 -1
  154. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  155. package/src/lib/usage/ai-usage-meter.js +17 -1
  156. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  157. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  158. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  159. package/src/lib/usage/assist-usage.d.ts +7 -0
  160. package/src/lib/usage/assist-usage.js +14 -4
  161. package/src/lib/usage/assist-usage.js.map +1 -1
  162. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  163. package/src/lib/usage/free-ai-credits-left.js +4 -2
  164. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -15,11 +15,19 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  /**
18
- * Cancel a job (AGL-2904). Idempotent: a job already terminal answers as
19
- * it is, unchanged. A step in flight is not interrupted — the runner that
20
- * holds its lease records what it cost and finds the job canceled when it
21
- * goes to complete it — so a cancel is never a way to spend tokens the
22
- * document does not show. Audited per cancel that changed something.
18
+ * Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`
19
+ * `{ orgId }`.
20
+ *
21
+ * Who may: a member who holds `ai.generate` on the job's site (the
22
+ * permission that started it), or staff, member or not. Anyone else is
23
+ * refused 403 and the job is untouched.
24
+ *
25
+ * What it does is `cancelAiJob`'s, in one transaction: a job no step holds
26
+ * ends `canceled` now; a job whose step is in flight records the cancel and
27
+ * ends `canceled` when that step finishes or aborts, which the answer's
28
+ * `job.cancelRequested` says. Idempotent: a job already done, failed or
29
+ * canceled answers as it is, `changed: false`. Audited per cancel that
30
+ * changed something.
23
31
  */
24
32
  export declare function POST(request: Request, context: {
25
33
  params: Promise<{
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -15,13 +16,22 @@
15
16
  * limitations under the License.
16
17
  */ // lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts
17
18
  import { aiJobSummary, cancelAiJob, getAiJob, writeAiJobAudit } from "../jobs/ai-jobs.js";
19
+ import { memberHasPermissionOnHost, permissionRefusal } from "@aglyn/tenant-data-admin/server/organizations";
18
20
  import { aiJobsGate } from "./ai-jobs-gate.js";
19
21
  /**
20
- * Cancel a job (AGL-2904). Idempotent: a job already terminal answers as
21
- * it is, unchanged. A step in flight is not interrupted — the runner that
22
- * holds its lease records what it cost and finds the job canceled when it
23
- * goes to complete it — so a cancel is never a way to spend tokens the
24
- * document does not show. Audited per cancel that changed something.
22
+ * Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`
23
+ * `{ orgId }`.
24
+ *
25
+ * Who may: a member who holds `ai.generate` on the job's site (the
26
+ * permission that started it), or staff, member or not. Anyone else is
27
+ * refused 403 and the job is untouched.
28
+ *
29
+ * What it does is `cancelAiJob`'s, in one transaction: a job no step holds
30
+ * ends `canceled` now; a job whose step is in flight records the cancel and
31
+ * ends `canceled` when that step finishes or aborts, which the answer's
32
+ * `job.cancelRequested` says. Idempotent: a job already done, failed or
33
+ * canceled answers as it is, `changed: false`. Audited per cancel that
34
+ * changed something.
25
35
  */ export async function POST(request, context) {
26
36
  var _ref, _gate_decoded_email;
27
37
  let payload;
@@ -30,7 +40,9 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
30
40
  } catch (unused) {
31
41
  payload = null;
32
42
  }
33
- const gate = await aiJobsGate(request, String((_ref = payload == null ? void 0 : payload.orgId) != null ? _ref : ''));
43
+ const gate = await aiJobsGate(request, String((_ref = payload == null ? void 0 : payload.orgId) != null ? _ref : ''), {
44
+ staffWithoutMembership: true
45
+ });
34
46
  if (gate instanceof Response) return gate;
35
47
  const { jobId } = await context.params;
36
48
  const existing = await getAiJob(gate.firestore, gate.orgId, jobId);
@@ -39,6 +51,10 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
39
51
  }, {
40
52
  status: 404
41
53
  });
54
+ // The job's own permission, on the job's own site (AGL-3616).
55
+ if (!gate.staff && !await memberHasPermissionOnHost(gate.orgId, existing.hostId, gate.member, 'ai.generate')) {
56
+ return permissionRefusal('ai.generate');
57
+ }
42
58
  const now = new Date();
43
59
  // The machine writes the org feed's row with this actor when the cancel
44
60
  // changed something; the staff audit row below is this route's own.
@@ -54,11 +70,13 @@ import { aiJobsGate } from "./ai-jobs-gate.js";
54
70
  actorEmail: (_gate_decoded_email1 = gate.decoded.email) != null ? _gate_decoded_email1 : null,
55
71
  orgId: gate.orgId,
56
72
  jobId,
57
- after: {
58
- status: 'canceled',
73
+ after: _extends({
74
+ status: job.status,
59
75
  wasStatus: existing.status,
60
76
  kind: existing.kind
61
- }
77
+ }, gate.staff ? {
78
+ staff: true
79
+ } : {})
62
80
  });
63
81
  }
64
82
  return Response.json({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-cancel.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/server/ai-jobs-gate.ts\n\nimport {\n aiJobSummary,\n cancelAiJob,\n getAiJob,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * Cancel a job (AGL-2904). Idempotent: a job already terminal answers as\n * it is, unchanged. A step in flight is not interrupted — the runner that\n * holds its lease records what it cost and finds the job canceled when it\n * goes to complete it — so a cancel is never a way to spend tokens the\n * document does not show. Audited per cancel that changed something.\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 gate = await aiJobsGate(request, String(payload?.orgId ?? ''))\n if (gate instanceof Response) return gate\n const { jobId } = await context.params\n const existing = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!existing) return Response.json({ error: 'Unknown job' }, { status: 404 })\n const now = new Date()\n // The machine writes the org feed's row with this actor when the cancel\n // changed something; the staff audit row below is this route's own.\n const { job, changed } = await cancelAiJob(gate.firestore, gate.orgId, jobId, now, {\n uid: gate.uid,\n email: gate.decoded.email ?? null,\n })\n if (changed) {\n await writeAiJobAudit(gate.firestore, {\n action: 'ai.job.cancel',\n actorUid: gate.uid,\n actorEmail: gate.decoded.email ?? null,\n orgId: gate.orgId,\n jobId,\n after: { status: 'canceled', wasStatus: existing.status, kind: existing.kind },\n })\n }\n return Response.json({ job: aiJobSummary(job, now), changed }, { status: 200 })\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["aiJobSummary","cancelAiJob","getAiJob","writeAiJobAudit","aiJobsGate","POST","request","context","gate","payload","json","String","orgId","Response","jobId","params","existing","firestore","error","status","now","Date","job","changed","uid","email","decoded","action","actorUid","actorEmail","after","wasStatus","kind","dynamic","maxDuration"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,mEAAmE;AAEnE,SACEA,YAAY,EACZC,WAAW,EACXC,QAAQ,EACRC,eAAe,QACV,qBAAiB;AACxB,SAASC,UAAU,QAAQ,oBAAgB;AAE3C;;;;;;CAMC,GACD,OAAO,eAAeC,KACpBC,OAAgB,EAChBC,OAA+C;cAkBtCC;IAhBT,IAAIC;IACJ,IAAI;QACFA,UAAW,MAAMH,QAAQI,IAAI;IAC/B,EAAE,eAAM;QACND,UAAU;IACZ;IACA,MAAMD,OAAO,MAAMJ,WAAWE,SAASK,eAAOF,2BAAAA,QAASG,KAAK,mBAAI;IAChE,IAAIJ,gBAAgBK,UAAU,OAAOL;IACrC,MAAM,EAAEM,KAAK,EAAE,GAAG,MAAMP,QAAQQ,MAAM;IACtC,MAAMC,WAAW,MAAMd,SAASM,KAAKS,SAAS,EAAET,KAAKI,KAAK,EAAEE;IAC5D,IAAI,CAACE,UAAU,OAAOH,SAASH,IAAI,CAAC;QAAEQ,OAAO;IAAc,GAAG;QAAEC,QAAQ;IAAI;IAC5E,MAAMC,MAAM,IAAIC;IAChB,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,EAAEC,GAAG,EAAEC,OAAO,EAAE,GAAG,MAAMtB,YAAYO,KAAKS,SAAS,EAAET,KAAKI,KAAK,EAAEE,OAAOM,KAAK;QACjFI,KAAKhB,KAAKgB,GAAG;QACbC,KAAK,GAAEjB,sBAAAA,KAAKkB,OAAO,CAACD,KAAK,YAAlBjB,sBAAsB;IAC/B;IACA,IAAIe,SAAS;YAIGf;QAHd,MAAML,gBAAgBK,KAAKS,SAAS,EAAE;YACpCU,QAAQ;YACRC,UAAUpB,KAAKgB,GAAG;YAClBK,UAAU,GAAErB,uBAAAA,KAAKkB,OAAO,CAACD,KAAK,YAAlBjB,uBAAsB;YAClCI,OAAOJ,KAAKI,KAAK;YACjBE;YACAgB,OAAO;gBAAEX,QAAQ;gBAAYY,WAAWf,SAASG,MAAM;gBAAEa,MAAMhB,SAASgB,IAAI;YAAC;QAC/E;IACF;IACA,OAAOnB,SAASH,IAAI,CAAC;QAAEY,KAAKtB,aAAasB,KAAKF;QAAMG;IAAQ,GAAG;QAAEJ,QAAQ;IAAI;AAC/E;AAEA,OAAO,MAAMc,UAAU,gBAAe;AACtC,OAAO,MAAMC,cAAc,GAAE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-cancel.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/server/ai-jobs-gate.ts\n\nimport {\n aiJobSummary,\n cancelAiJob,\n getAiJob,\n writeAiJobAudit,\n} from '../jobs/ai-jobs'\nimport { memberHasPermissionOnHost, permissionRefusal } from '@aglyn/tenant-data-admin/server/organizations'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * Cancel a job (AGL-2904, AGL-3616). `POST /api/ai/jobs/{jobId}/cancel`\n * `{ orgId }`.\n *\n * Who may: a member who holds `ai.generate` on the job's site (the\n * permission that started it), or staff, member or not. Anyone else is\n * refused 403 and the job is untouched.\n *\n * What it does is `cancelAiJob`'s, in one transaction: a job no step holds\n * ends `canceled` now; a job whose step is in flight records the cancel and\n * ends `canceled` when that step finishes or aborts, which the answer's\n * `job.cancelRequested` says. Idempotent: a job already done, failed or\n * canceled answers as it is, `changed: false`. Audited per cancel that\n * changed something.\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 gate = await aiJobsGate(request, String(payload?.orgId ?? ''), { staffWithoutMembership: true })\n if (gate instanceof Response) return gate\n const { jobId } = await context.params\n const existing = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!existing) return Response.json({ error: 'Unknown job' }, { status: 404 })\n // The job's own permission, on the job's own site (AGL-3616).\n if (!gate.staff && !(await memberHasPermissionOnHost(gate.orgId, existing.hostId, gate.member, 'ai.generate'))) {\n return permissionRefusal('ai.generate')\n }\n const now = new Date()\n // The machine writes the org feed's row with this actor when the cancel\n // changed something; the staff audit row below is this route's own.\n const { job, changed } = await cancelAiJob(gate.firestore, gate.orgId, jobId, now, {\n uid: gate.uid,\n email: gate.decoded.email ?? null,\n })\n if (changed) {\n await writeAiJobAudit(gate.firestore, {\n action: 'ai.job.cancel',\n actorUid: gate.uid,\n actorEmail: gate.decoded.email ?? null,\n orgId: gate.orgId,\n jobId,\n after: {\n status: job.status,\n wasStatus: existing.status,\n kind: existing.kind,\n ...(gate.staff ? { staff: true } : {}),\n },\n })\n }\n return Response.json({ job: aiJobSummary(job, now), changed }, { status: 200 })\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["aiJobSummary","cancelAiJob","getAiJob","writeAiJobAudit","memberHasPermissionOnHost","permissionRefusal","aiJobsGate","POST","request","context","gate","payload","json","String","orgId","staffWithoutMembership","Response","jobId","params","existing","firestore","error","status","staff","hostId","member","now","Date","job","changed","uid","email","decoded","action","actorUid","actorEmail","after","wasStatus","kind","dynamic","maxDuration"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,mEAAmE;AAEnE,SACEA,YAAY,EACZC,WAAW,EACXC,QAAQ,EACRC,eAAe,QACV,qBAAiB;AACxB,SAASC,yBAAyB,EAAEC,iBAAiB,QAAQ,gDAA+C;AAC5G,SAASC,UAAU,QAAQ,oBAAgB;AAE3C;;;;;;;;;;;;;;CAcC,GACD,OAAO,eAAeC,KACpBC,OAAgB,EAChBC,OAA+C;cAsBtCC;IApBT,IAAIC;IACJ,IAAI;QACFA,UAAW,MAAMH,QAAQI,IAAI;IAC/B,EAAE,eAAM;QACND,UAAU;IACZ;IACA,MAAMD,OAAO,MAAMJ,WAAWE,SAASK,eAAOF,2BAAAA,QAASG,KAAK,mBAAI,KAAK;QAAEC,wBAAwB;IAAK;IACpG,IAAIL,gBAAgBM,UAAU,OAAON;IACrC,MAAM,EAAEO,KAAK,EAAE,GAAG,MAAMR,QAAQS,MAAM;IACtC,MAAMC,WAAW,MAAMjB,SAASQ,KAAKU,SAAS,EAAEV,KAAKI,KAAK,EAAEG;IAC5D,IAAI,CAACE,UAAU,OAAOH,SAASJ,IAAI,CAAC;QAAES,OAAO;IAAc,GAAG;QAAEC,QAAQ;IAAI;IAC5E,8DAA8D;IAC9D,IAAI,CAACZ,KAAKa,KAAK,IAAI,CAAE,MAAMnB,0BAA0BM,KAAKI,KAAK,EAAEK,SAASK,MAAM,EAAEd,KAAKe,MAAM,EAAE,gBAAiB;QAC9G,OAAOpB,kBAAkB;IAC3B;IACA,MAAMqB,MAAM,IAAIC;IAChB,wEAAwE;IACxE,oEAAoE;IACpE,MAAM,EAAEC,GAAG,EAAEC,OAAO,EAAE,GAAG,MAAM5B,YAAYS,KAAKU,SAAS,EAAEV,KAAKI,KAAK,EAAEG,OAAOS,KAAK;QACjFI,KAAKpB,KAAKoB,GAAG;QACbC,KAAK,GAAErB,sBAAAA,KAAKsB,OAAO,CAACD,KAAK,YAAlBrB,sBAAsB;IAC/B;IACA,IAAImB,SAAS;YAIGnB;QAHd,MAAMP,gBAAgBO,KAAKU,SAAS,EAAE;YACpCa,QAAQ;YACRC,UAAUxB,KAAKoB,GAAG;YAClBK,UAAU,GAAEzB,uBAAAA,KAAKsB,OAAO,CAACD,KAAK,YAAlBrB,uBAAsB;YAClCI,OAAOJ,KAAKI,KAAK;YACjBG;YACAmB,OAAO;gBACLd,QAAQM,IAAIN,MAAM;gBAClBe,WAAWlB,SAASG,MAAM;gBAC1BgB,MAAMnB,SAASmB,IAAI;eACf5B,KAAKa,KAAK,GAAG;gBAAEA,OAAO;YAAK,IAAI,CAAC;QAExC;IACF;IACA,OAAOP,SAASJ,IAAI,CAAC;QAAEgB,KAAK5B,aAAa4B,KAAKF;QAAMG;IAAQ,GAAG;QAAEP,QAAQ;IAAI;AAC/E;AAEA,OAAO,MAAMiB,UAAU,gBAAe;AACtC,OAAO,MAAMC,cAAc,GAAE"}
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
  import type { DecodedIdToken } from 'firebase-admin/auth';
18
- import type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types';
18
+ import type { AglynOrganization, AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types';
19
19
  /**
20
20
  * The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).
21
21
  *
@@ -42,6 +42,16 @@ export interface AiJobsGateContext {
42
42
  staff: boolean;
43
43
  orgId: string;
44
44
  org: Partial<AglynOrganization>;
45
+ /** The caller's membership; `null` only for staff let in by `staffWithoutMembership`. */
46
+ member: AglynOrgMember | null;
45
47
  firestore: FirebaseFirestore.Firestore;
46
48
  }
47
- export declare function aiJobsGate(request: Request, rawOrgId: string): Promise<Response | AiJobsGateContext>;
49
+ export interface AiJobsGateOptions {
50
+ /**
51
+ * A verified staff claim passes the membership rung for an org it is not a
52
+ * member of (AGL-3616): staff stop a customer's job from the job's own
53
+ * page. Every rung after it is climbed as for anyone else.
54
+ */
55
+ staffWithoutMembership?: boolean;
56
+ }
57
+ export declare function aiJobsGate(request: Request, rawOrgId: string, options?: AiJobsGateOptions): Promise<Response | AiJobsGateContext>;
@@ -28,8 +28,7 @@ function bearerToken(request) {
28
28
  const authorization = (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '';
29
29
  return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
30
30
  }
31
- export async function aiJobsGate(request, rawOrgId) {
32
- var _resolved_org;
31
+ export async function aiJobsGate(request, rawOrgId, options = {}) {
33
32
  const idToken = bearerToken(request);
34
33
  if (!idToken) {
35
34
  return Response.json({
@@ -63,14 +62,25 @@ export async function aiJobsGate(request, rawOrgId) {
63
62
  });
64
63
  }
65
64
  const resolved = await getOrgForUser(decoded.uid, orgId);
66
- if (!resolved || resolved.orgId !== orgId) {
67
- return Response.json({
68
- error: 'You are not a member of that organization'
69
- }, {
70
- status: 403
71
- });
65
+ let org;
66
+ let member;
67
+ if (resolved && resolved.orgId === orgId) {
68
+ var _resolved_org, _resolved_member;
69
+ org = (_resolved_org = resolved.org) != null ? _resolved_org : {};
70
+ member = (_resolved_member = resolved.member) != null ? _resolved_member : null;
71
+ } else {
72
+ var _staffOrg_data;
73
+ const staffOrg = staff && options.staffWithoutMembership ? await app.firestore().collection('orgs').doc(orgId).get() : null;
74
+ if (!(staffOrg == null ? void 0 : staffOrg.exists)) {
75
+ return Response.json({
76
+ error: 'You are not a member of that organization'
77
+ }, {
78
+ status: 403
79
+ });
80
+ }
81
+ org = (_staffOrg_data = staffOrg.data()) != null ? _staffOrg_data : {};
82
+ member = null;
72
83
  }
73
- const org = (_resolved_org = resolved.org) != null ? _resolved_org : {};
74
84
  if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', orgId)) {
75
85
  return Response.json({
76
86
  error: 'Not found'
@@ -99,6 +109,7 @@ export async function aiJobsGate(request, rawOrgId) {
99
109
  staff,
100
110
  orgId,
101
111
  org,
112
+ member,
102
113
  firestore: app.firestore()
103
114
  };
104
115
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-gate.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\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types'\n// By their own entry points rather than the barrel, for the reason the chat\n// route gives for the runtime: the route specs replace the barrel with a\n// closed-world factory, and a leaf that is mocked at its own seam is one\n// the spec can drive rung by rung.\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getOrgForUser } from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).\n *\n * `aiGateLadder` is the ladder a door that SPENDS climbs: it is POST-only\n * and ends by taking a reservation, because its whole purpose is to bound\n * provider spend. Listing a workspace's jobs, watching one, or canceling\n * one spends nothing and answers a GET, so those doors climb the same\n * rungs up to the lockdown verdict and stop there — no rate window and no\n * reservation. The rungs are in the ladder's order, for the ladder's\n * reason: each one refused discloses nothing a lower rung would.\n *\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the request did not NAME the org (AGL-1934)\n * 403 the caller is not a member of that org\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks `aiGenerative`\n * 423 lockdown: the scope verdict (platform/org/user)\n */\nexport interface AiJobsGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n firestore: FirebaseFirestore.Firestore\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\nexport async function aiJobsGate(\n request: Request,\n rawOrgId: string,\n): Promise<Response | AiJobsGateContext> {\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // is ours and propagates so the door answers 500 rather than telling\n // every user their credential is bad during an outage (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = rawOrgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n const org = resolved.org ?? {}\n\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n if (!checkEntitlement(org, 'aiGenerative')) {\n return Response.json(\n {\n error: \"This workspace's plan does not include that feature\",\n reason: 'entitlement',\n },\n { status: 403 },\n )\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n decoded,\n staff,\n orgId,\n org,\n firestore: app.firestore(),\n }\n}\n"],"names":["checkEntitlement","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","lockdownRefusal","getOrgForUser","isServerReleaseFlagOnForOrg","invalidIdTokenResponse","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","aiJobsGate","rawOrgId","resolved","idToken","Response","json","error","status","app","decoded","auth","verifyIdToken","unauthenticated","email_verified","staff","orgId","trim","uid","org","reason","locked","firestore"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,gBAAgB,QAAQ,sBAAqB;AAEtD,4EAA4E;AAC5E,yEAAyE;AACzE,yEAAyE;AACzE,mCAAmC;AACnC,SACEC,uBAAuB,EACvBC,aAAa,EACbC,sBAAsB,QACjB,iDAAgD;AACvD,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,aAAa,QAAQ,gDAA+C;AAC7E,SAASC,2BAA2B,QAAQ,gDAA+C;AAC3F,SAASC,sBAAsB,QAAQ,mDAAkD;AA+BzF,SAASC,YAAYC,OAAgB;QACbA;IAAtB,MAAMC,iBAAgBD,uBAAAA,QAAQE,OAAO,CAACC,GAAG,CAAC,4BAApBH,uBAAwC;IAC9D,OAAOC,cAAcG,UAAU,CAAC,aAC5BH,cAAcI,KAAK,CAAC,UAAUC,MAAM,IACpCC;AACN;AAEA,OAAO,eAAeC,WACpBR,OAAgB,EAChBS,QAAgB;QAqCJC;IAnCZ,MAAMC,UAAUZ,YAAYC;IAC5B,IAAI,CAACW,SAAS;QACZ,OAAOC,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAkB,GAAG;YAAEC,QAAQ;QAAI;IACnE;IACA,MAAMC,MAAMvB,cAAcuB,GAAG;IAC7B,IAAIC;IACJ,IAAI;QACFA,UAAU,MAAMD,IAAIE,IAAI,GAAGC,aAAa,CAACR;IAC3C,EAAE,OAAOG,OAAO;QACd,sEAAsE;QACtE,qEAAqE;QACrE,kEAAkE;QAClE,MAAMM,kBAAkBtB,uBAAuBgB;QAC/C,IAAIM,iBAAiB,OAAOA;QAC5B,MAAMN;IACR;IACA,IAAI,CAACG,QAAQI,cAAc,IAAI,CAAC3B,uBAAuBuB,UAAU;QAC/D,OAAOzB;IACT;IACA,MAAM8B,QAAQL,OAAO,CAAC,QAAQ,KAAK;IAEnC,MAAMM,QAAQd,SAASe,IAAI;IAC3B,IAAI,CAACD,OAAO;QACV,OAAOX,SAASC,IAAI,CAClB;YAAEC,OAAO;QAA6C,GACtD;YAAEC,QAAQ;QAAI;IAElB;IACA,MAAML,WAAW,MAAMd,cAAcqB,QAAQQ,GAAG,EAAEF;IAClD,IAAI,CAACb,YAAYA,SAASa,KAAK,KAAKA,OAAO;QACzC,OAAOX,SAASC,IAAI,CAClB;YAAEC,OAAO;QAA4C,GACrD;YAAEC,QAAQ;QAAI;IAElB;IACA,MAAMW,OAAMhB,gBAAAA,SAASgB,GAAG,YAAZhB,gBAAgB,CAAC;IAE7B,IAAI,CAACY,SAAS,CAAE,MAAMzB,4BAA4B,yBAAyB0B,QAAS;QAClF,OAAOX,SAASC,IAAI,CAAC;YAAEC,OAAO;QAAY,GAAG;YAAEC,QAAQ;QAAI;IAC7D;IACA,IAAI,CAACxB,iBAAiBmC,KAAK,iBAAiB;QAC1C,OAAOd,SAASC,IAAI,CAClB;YACEC,OAAO;YACPa,QAAQ;QACV,GACA;YAAEZ,QAAQ;QAAI;IAElB;IACA,MAAMa,SAAS,MAAMjC,gBAAgB;QACnCK;QACAsB;QACAG,KAAKR,QAAQQ,GAAG;QAChBC,KAAKA;IACP;IACA,IAAIE,QAAQ,OAAOA;IAEnB,OAAO;QACLH,KAAKR,QAAQQ,GAAG;QAChBR;QACAK;QACAC;QACAG;QACAG,WAAWb,IAAIa,SAAS;IAC1B;AACF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-gate.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\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport type { AglynOrganization, AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\n// By their own entry points rather than the barrel, for the reason the chat\n// route gives for the runtime: the route specs replace the barrel with a\n// closed-world factory, and a leaf that is mocked at its own seam is one\n// the spec can drive rung by rung.\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport { getOrgForUser } from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * The gate the READ and CANCEL doors of AI jobs climb (AGL-2904).\n *\n * `aiGateLadder` is the ladder a door that SPENDS climbs: it is POST-only\n * and ends by taking a reservation, because its whole purpose is to bound\n * provider spend. Listing a workspace's jobs, watching one, or canceling\n * one spends nothing and answers a GET, so those doors climb the same\n * rungs up to the lockdown verdict and stop there — no rate window and no\n * reservation. The rungs are in the ladder's order, for the ladder's\n * reason: each one refused discloses nothing a lower rung would.\n *\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the request did not NAME the org (AGL-1934)\n * 403 the caller is not a member of that org\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks `aiGenerative`\n * 423 lockdown: the scope verdict (platform/org/user)\n */\nexport interface AiJobsGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n /** The caller's membership; `null` only for staff let in by `staffWithoutMembership`. */\n member: AglynOrgMember | null\n firestore: FirebaseFirestore.Firestore\n}\n\nexport interface AiJobsGateOptions {\n /**\n * A verified staff claim passes the membership rung for an org it is not a\n * member of (AGL-3616): staff stop a customer's job from the job's own\n * page. Every rung after it is climbed as for anyone else.\n */\n staffWithoutMembership?: boolean\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\nexport async function aiJobsGate(\n request: Request,\n rawOrgId: string,\n options: AiJobsGateOptions = {},\n): Promise<Response | AiJobsGateContext> {\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // is ours and propagates so the door answers 500 rather than telling\n // every user their credential is bad during an outage (AGL-1993).\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = rawOrgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n const resolved = await getOrgForUser(decoded.uid, orgId)\n let org: Partial<AglynOrganization>\n let member: AglynOrgMember | null\n if (resolved && resolved.orgId === orgId) {\n org = resolved.org ?? {}\n member = resolved.member ?? null\n } else {\n const staffOrg =\n staff && options.staffWithoutMembership\n ? await app.firestore().collection('orgs').doc(orgId).get()\n : null\n if (!staffOrg?.exists) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n org = (staffOrg.data() ?? {}) as Partial<AglynOrganization>\n member = null\n }\n\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n if (!checkEntitlement(org, 'aiGenerative')) {\n return Response.json(\n {\n error: \"This workspace's plan does not include that feature\",\n reason: 'entitlement',\n },\n { status: 403 },\n )\n }\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return locked\n\n return {\n uid: decoded.uid,\n decoded,\n staff,\n orgId,\n org,\n member,\n firestore: app.firestore(),\n }\n}\n"],"names":["checkEntitlement","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","lockdownRefusal","getOrgForUser","isServerReleaseFlagOnForOrg","invalidIdTokenResponse","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","aiJobsGate","rawOrgId","options","idToken","Response","json","error","status","app","decoded","auth","verifyIdToken","unauthenticated","email_verified","staff","orgId","trim","resolved","uid","org","member","staffOrg","staffWithoutMembership","firestore","collection","doc","exists","data","reason","locked"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -18,8 +19,10 @@
18
19
  import { randomUUID } from "crypto";
19
20
  import { aiJobAdmissionRefusal, aiJobSiteRefusal } from "../jobs/ai-job-admission.js";
20
21
  import { AI_JOB_INLINE_BUDGET_MS, aiJobNextStepMinimumMs, aiJobSummary, getAiJob, aiBuildRetryable, resumeAiJob, retryAiBuildJob, runAiJobStep, writeAiJobAudit } from "../jobs/ai-jobs.js";
21
- import { aiBuildFreeBalanceRefusal, aiBuildFreeTaste } from "../jobs/ai-job-build-step.js";
22
- import { aiBuildCreditEstimate, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from "../model/ai-build-job.js";
22
+ import { aiBuildFreeCreditsRefusal, aiBuildFreeTaste } from "../jobs/ai-job-build-step.js";
23
+ import { aiBuildCreditRange, aiBuildFirstPagePlan, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from "../model/ai-build-job.js";
24
+ import { aiCreditsConfirmation } from "../model/ai-credit-estimate.js";
25
+ import { FREE_AI_TASTE_CREDITS_PER_MONTH } from "../plan-entitlements.js";
23
26
  import { aiGateLadder } from "../runtime/ai-gate.js";
24
27
  import { releaseAssistMessage } from "../usage/assist-usage.js";
25
28
  /**
@@ -85,11 +88,62 @@ export async function POST(request, context) {
85
88
  status: 400
86
89
  });
87
90
  }
91
+ // "Build what fits" (AGL-3722): a Free job past what is left, started on
92
+ // the person's go-ahead, which the job keeps.
93
+ const creditsConfirmed = (payload == null ? void 0 : payload['creditsConfirmed']) === true;
94
+ let confirmedCredits = null;
95
+ const onCreditsConfirmed = (prompt)=>{
96
+ confirmedCredits = prompt;
97
+ };
98
+ // "Build the home page first" (AGL-3722): the plan waiting for its
99
+ // confirmation narrowed to its home page and what that page needs.
100
+ const reduceToFirstPage = (payload == null ? void 0 : payload['reduce']) === 'first-page' && existing.kind === 'build' && existing.status === 'needs_review';
101
+ const reduced = reduceToFirstPage && existing.plan ? aiBuildFirstPagePlan(existing.plan) : null;
102
+ if (reduceToFirstPage && !reduced) {
103
+ await release();
104
+ return Response.json({
105
+ error: 'This plan has no smaller first build',
106
+ job: aiJobSummary(existing, new Date())
107
+ }, {
108
+ status: 409
109
+ });
110
+ }
111
+ // A refusal as the door answers it: a Free job past what is left carries
112
+ // its prompt, and a plan card the figures it should now show.
113
+ const refused = async (refusal)=>{
114
+ await release();
115
+ const now = new Date();
116
+ const summary = aiJobSummary(existing, now);
117
+ const fresh = refusal.credits ? {
118
+ left: refusal.credits.left,
119
+ total: FREE_AI_TASTE_CREDITS_PER_MONTH,
120
+ resetsOn: refusal.credits.resetsOn
121
+ } : null;
122
+ const job = fresh && summary.review ? _extends({}, summary, {
123
+ review: _extends({}, summary.review, {
124
+ freeCredits: fresh
125
+ })
126
+ }) : summary;
127
+ return Response.json(_extends({
128
+ error: refusal.error
129
+ }, refusal.code ? {
130
+ code: refusal.code
131
+ } : {}, refusal.credits ? {
132
+ credits: refusal.credits
133
+ } : {}, {
134
+ job
135
+ }), {
136
+ status: refusal.status
137
+ });
138
+ };
88
139
  // Try again on a build (AGL-3616): its failed items and what they left
89
140
  // unbuilt, never what succeeded, and never its plan again.
90
141
  const retryFailedItems = (payload == null ? void 0 : payload['retry']) === 'failed-items';
91
142
  if (retryFailedItems) {
92
- const refusal = await aiBuildRetryRefusal(gate, existing).catch((error)=>{
143
+ const refusal = await aiBuildRetryRefusal(gate, existing, {
144
+ confirmed: creditsConfirmed,
145
+ onCreditsConfirmed
146
+ }).catch((error)=>{
93
147
  console.error('ai build retry admission failed', {
94
148
  orgId: gate.orgId,
95
149
  jobId,
@@ -101,13 +155,16 @@ export async function POST(request, context) {
101
155
  };
102
156
  });
103
157
  if (refusal) {
104
- await release();
105
- return Response.json({
106
- error: refusal.error,
107
- job: aiJobSummary(existing, new Date())
108
- }, {
109
- status: refusal.status
110
- });
158
+ if (refusal.status === 500) {
159
+ await release();
160
+ return Response.json({
161
+ error: refusal.error,
162
+ job: aiJobSummary(existing, new Date())
163
+ }, {
164
+ status: 500
165
+ });
166
+ }
167
+ return refused(refusal);
111
168
  }
112
169
  }
113
170
  // A confirmed plan runs the step that writes, and an allowance free when the
@@ -116,7 +173,7 @@ export async function POST(request, context) {
116
173
  if (existing.status === 'needs_review') {
117
174
  let refusal;
118
175
  try {
119
- var _ref2, _existing_hostId1, _existing_hostId2, _existing_inputs, _existing_plan;
176
+ var _ref2, _existing_hostId1, _existing_hostId2, _existing_inputs, _ref3;
120
177
  // A site that switched AI off first (AGL-3028), then the kind's own check.
121
178
  refusal = (_ref2 = await aiJobSiteRefusal({
122
179
  firestore: gate.firestore,
@@ -128,9 +185,12 @@ export async function POST(request, context) {
128
185
  hostId: (_existing_hostId2 = existing.hostId) != null ? _existing_hostId2 : null,
129
186
  inputs: (_existing_inputs = existing.inputs) != null ? _existing_inputs : {},
130
187
  org: gate.org,
131
- // The plan being confirmed, for a kind that builds only some plans.
132
- plan: (_existing_plan = existing.plan) != null ? _existing_plan : null,
133
- uid: gate.uid
188
+ // The plan being confirmed, for a kind that builds only some plans —
189
+ // the smaller first build where the person chose it (AGL-3722).
190
+ plan: (_ref3 = reduced != null ? reduced : existing.plan) != null ? _ref3 : null,
191
+ uid: gate.uid,
192
+ creditsConfirmed,
193
+ onCreditsConfirmed
134
194
  });
135
195
  } catch (error) {
136
196
  await release();
@@ -145,22 +205,20 @@ export async function POST(request, context) {
145
205
  status: 500
146
206
  });
147
207
  }
148
- if (refusal) {
149
- await release();
150
- return Response.json({
151
- error: refusal.error,
152
- job: aiJobSummary(existing, new Date())
153
- }, {
154
- status: refusal.status
155
- });
156
- }
208
+ if (refusal) return refused(refusal);
157
209
  }
158
210
  const now = new Date();
159
- const { job, changed } = retryFailedItems ? await retryAiBuildJob(gate.firestore, gate.orgId, jobId, now) : await resumeAiJob(gate.firestore, gate.orgId, jobId, {
211
+ const confirmation = aiCreditsConfirmation(confirmedCredits, gate.uid, now);
212
+ const { job, changed } = retryFailedItems ? await retryAiBuildJob(gate.firestore, gate.orgId, jobId, now, {
213
+ creditsConfirmed: confirmation
214
+ }) : await resumeAiJob(gate.firestore, gate.orgId, jobId, {
160
215
  uid: gate.uid
161
- }, now, {
162
- publishConfirmed: (payload == null ? void 0 : payload['publish']) === true
163
- });
216
+ }, now, _extends({
217
+ publishConfirmed: (payload == null ? void 0 : payload['publish']) === true,
218
+ creditsConfirmed: confirmation
219
+ }, reduced ? {
220
+ reducePlan: aiBuildFirstPagePlan
221
+ } : {}));
164
222
  if (!changed) {
165
223
  await release();
166
224
  return Response.json({
@@ -236,7 +294,7 @@ export async function POST(request, context) {
236
294
  * Why a build's Try again cannot run (AGL-3616), or `null`: it is not a
237
295
  * finished build, the site switched AI off, or — on the Free taste — the
238
296
  * month's Free credits do not cover what the retried items are estimated at.
239
- */ async function aiBuildRetryRefusal(gate, job) {
297
+ */ async function aiBuildRetryRefusal(gate, job, credits) {
240
298
  var _job_hostId;
241
299
  var _job_plan, _job_items;
242
300
  const plan = ((_job_plan = job.plan) == null ? void 0 : _job_plan.status) === 'confirmed' ? job.plan : null;
@@ -258,20 +316,20 @@ export async function POST(request, context) {
258
316
  error: 'Nothing in this build is left to try again'
259
317
  };
260
318
  // A site's retried parts are its own pages and creations, held by the
261
- // reservation each pass takes; a build's are estimated against the Free taste.
319
+ // reservation each pass takes; a build's are admitted on their measured p90
320
+ // against the Free taste, or on the person's go-ahead (AGL-3722).
262
321
  if (job.kind === 'site' || !aiBuildFreeTaste(gate.org)) return null;
263
- const refusal = await aiBuildFreeBalanceRefusal({
322
+ return aiBuildFreeCreditsRefusal({
264
323
  firestore: gate.firestore,
324
+ orgId: gate.orgId,
265
325
  org: gate.org,
266
- estimate: aiBuildCreditEstimate(plan, {
326
+ range: aiBuildCreditRange(plan, {
267
327
  slots: new Set(retried)
268
328
  }),
329
+ confirmed: credits.confirmed,
330
+ onConfirmed: credits.onCreditsConfirmed,
269
331
  now: new Date()
270
332
  });
271
- return refusal ? {
272
- status: 403,
273
- error: refusal
274
- } : null;
275
333
  }
276
334
  export const dynamic = 'force-dynamic';
277
335
  export const maxDuration = 60;
@@ -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 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 | 429; 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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"}
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, type AiJobAdmissionRefusal } 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 { aiBuildFreeCreditsRefusal, aiBuildFreeTaste } from '../jobs/ai-job-build-step'\nimport {\n aiBuildCreditRange,\n aiBuildFirstPagePlan,\n aiBuildRetryLedger,\n aiBuildUnits,\n aiLedgerUnits,\n} from '../model/ai-build-job'\nimport { aiCreditsConfirmation, type AiCreditsPrompt } from '../model/ai-credit-estimate'\nimport { FREE_AI_TASTE_CREDITS_PER_MONTH } from '../plan-entitlements'\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 // \"Build what fits\" (AGL-3722): a Free job past what is left, started on\n // the person's go-ahead, which the job keeps.\n const creditsConfirmed = payload?.['creditsConfirmed'] === true\n let confirmedCredits: AiCreditsPrompt | null = null\n const onCreditsConfirmed = (prompt: AiCreditsPrompt) => {\n confirmedCredits = prompt\n }\n // \"Build the home page first\" (AGL-3722): the plan waiting for its\n // confirmation narrowed to its home page and what that page needs.\n const reduceToFirstPage =\n payload?.['reduce'] === 'first-page' && existing.kind === 'build' && existing.status === 'needs_review'\n const reduced = reduceToFirstPage && existing.plan ? aiBuildFirstPagePlan(existing.plan) : null\n if (reduceToFirstPage && !reduced) {\n await release()\n return Response.json(\n { error: 'This plan has no smaller first build', job: aiJobSummary(existing, new Date()) },\n { status: 409 },\n )\n }\n // A refusal as the door answers it: a Free job past what is left carries\n // its prompt, and a plan card the figures it should now show.\n const refused = async (refusal: AiJobAdmissionRefusal) => {\n await release()\n const now = new Date()\n const summary = aiJobSummary(existing, now)\n const fresh = refusal.credits\n ? { left: refusal.credits.left, total: FREE_AI_TASTE_CREDITS_PER_MONTH, resetsOn: refusal.credits.resetsOn }\n : null\n const job = fresh && summary.review ? { ...summary, review: { ...summary.review, freeCredits: fresh } } : summary\n return Response.json(\n {\n error: refusal.error,\n ...(refusal.code ? { code: refusal.code } : {}),\n ...(refusal.credits ? { credits: refusal.credits } : {}),\n job,\n },\n { status: refusal.status },\n )\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, { confirmed: creditsConfirmed, onCreditsConfirmed }).catch(\n (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 )\n if (refusal) {\n if (refusal.status === 500) {\n await release()\n return Response.json({ error: refusal.error, job: aiJobSummary(existing, new Date()) }, { status: 500 })\n }\n return refused(refusal)\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 // the smaller first build where the person chose it (AGL-3722).\n plan: reduced ?? existing.plan ?? null,\n uid: gate.uid,\n creditsConfirmed,\n onCreditsConfirmed,\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) return refused(refusal)\n }\n const now = new Date()\n const confirmation = aiCreditsConfirmation(confirmedCredits, gate.uid, now)\n const { job, changed } = retryFailedItems\n ? await retryAiBuildJob(gate.firestore, gate.orgId, jobId, now, { creditsConfirmed: confirmation })\n : await resumeAiJob(gate.firestore, gate.orgId, jobId, { uid: gate.uid }, now, {\n publishConfirmed: payload?.['publish'] === true,\n creditsConfirmed: confirmation,\n ...(reduced ? { reducePlan: aiBuildFirstPagePlan } : {}),\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; orgId: string; org: object | null },\n job: AiJob,\n credits: { confirmed: boolean; onCreditsConfirmed: (prompt: AiCreditsPrompt) => void },\n): Promise<AiJobAdmissionRefusal | 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 admitted on their measured p90\n // against the Free taste, or on the person's go-ahead (AGL-3722).\n if (job.kind === 'site' || !aiBuildFreeTaste(gate.org)) return null\n return aiBuildFreeCreditsRefusal({\n firestore: gate.firestore,\n orgId: gate.orgId,\n org: gate.org,\n range: aiBuildCreditRange(plan, { slots: new Set(retried) }),\n confirmed: credits.confirmed,\n onConfirmed: credits.onCreditsConfirmed,\n now: new Date(),\n })\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","aiBuildFreeCreditsRefusal","aiBuildFreeTaste","aiBuildCreditRange","aiBuildFirstPagePlan","aiBuildRetryLedger","aiBuildUnits","aiLedgerUnits","aiCreditsConfirmation","FREE_AI_TASTE_CREDITS_PER_MONTH","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","creditsConfirmed","confirmedCredits","onCreditsConfirmed","prompt","reduceToFirstPage","kind","reduced","plan","job","Date","refused","refusal","now","summary","fresh","credits","left","total","resetsOn","review","freeCredits","code","retryFailedItems","aiBuildRetryRefusal","confirmed","console","org","inputs","uid","confirmation","changed","publishConfirmed","reducePlan","action","actorUid","actorEmail","decoded","email","after","reason","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","latest","items","length","site","retried","range","slots","Set","onConfirmed","dynamic","maxDuration"],"mappings":"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"}