@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.
- package/package.json +17 -17
- package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
- package/src/lib/components/ai-credits-prompt.component.js +117 -0
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +5 -4
- package/src/lib/components/ai-job-plan.component.js +48 -12
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +10 -0
- package/src/lib/components/ai-job-requests.js +12 -4
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +48 -9
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +68 -22
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-build-card.component.d.ts +3 -1
- package/src/lib/components/assist-build-card.component.js +58 -7
- package/src/lib/components/assist-build-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +66 -22
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +89 -52
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +22 -2
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
- package/src/lib/jobs/ai-job-build-step.js +54 -35
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.js +4 -1
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +12 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
- package/src/lib/jobs/ai-job-page-step.js +46 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
- package/src/lib/jobs/ai-job-plan-step.js +118 -18
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
- package/src/lib/jobs/ai-job-site-content.js +196 -37
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +49 -16
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +55 -13
- package/src/lib/jobs/ai-jobs.js +213 -41
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
- package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
- package/src/lib/layout-language/ai-layout-compiler.js +381 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +3 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.js +1 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
- package/src/lib/layout-language/ai-layout-listings.js +117 -47
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
- package/src/lib/layout-language/ai-layout-pictures.js +5 -2
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +39 -0
- package/src/lib/model/ai-build-job.js +100 -1
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +8 -0
- package/src/lib/model/ai-build-progress.js +35 -0
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +249 -0
- package/src/lib/model/ai-credit-estimate.js +278 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -0
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +35 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +76 -22
- package/src/lib/model/ai-site-job.js +125 -23
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +35 -7
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
- package/src/lib/runtime/ai-doctrine-validators.js +161 -7
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +4 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +11 -0
- package/src/lib/runtime/ai-node-tree.js +26 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +592 -14
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +93 -35
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +45 -4
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +4 -1
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.js +3 -1
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-music.d.ts +36 -0
- package/src/lib/server/assist-music.js +36 -0
- package/src/lib/server/assist-music.js.map +1 -0
- package/src/lib/server.js +6 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +4 -2
- 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).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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:
|
|
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).
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 (
|
|
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 {
|
|
22
|
-
import {
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
322
|
+
return aiBuildFreeCreditsRefusal({
|
|
264
323
|
firestore: gate.firestore,
|
|
324
|
+
orgId: gate.orgId,
|
|
265
325
|
org: gate.org,
|
|
266
|
-
|
|
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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"}
|