@aglyn/plugins-ai 1.0.0-beta.260 → 1.0.0-beta.262

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 (81) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-brief-dialog.component.d.ts +21 -58
  3. package/src/lib/components/ai-brief-dialog.component.js +8 -132
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-job-follow.component.js +2 -35
  6. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  7. package/src/lib/components/ai-job-plan.component.d.ts +2 -8
  8. package/src/lib/components/ai-job-plan.component.js +13 -74
  9. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  10. package/src/lib/components/ai-site-start-card.component.js +43 -34
  11. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  12. package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
  13. package/src/lib/components/ai-template-collection-field.component.js +13 -10
  14. package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
  15. package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
  16. package/src/lib/components/ai-upsell-dialog.component.js +9 -66
  17. package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
  18. package/src/lib/components/use-ai-job-run.d.ts +3 -8
  19. package/src/lib/components/use-ai-job-run.js +4 -15
  20. package/src/lib/components/use-ai-job-run.js.map +1 -1
  21. package/src/lib/insights/ai-figure-readers.js +15 -5
  22. package/src/lib/insights/ai-figure-readers.js.map +1 -1
  23. package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
  24. package/src/lib/insights/ai-figure-summaries.js +79 -0
  25. package/src/lib/insights/ai-figure-summaries.js.map +1 -0
  26. package/src/lib/insights/ai-insight-readers.d.ts +2 -1
  27. package/src/lib/insights/ai-insight-readers.js +11 -26
  28. package/src/lib/insights/ai-insight-readers.js.map +1 -1
  29. package/src/lib/jobs/ai-job-admission.d.ts +6 -0
  30. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  31. package/src/lib/jobs/ai-job-site-step.js +10 -6
  32. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  33. package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -11
  34. package/src/lib/jobs/ai-layout-stock-photos.js +329 -32
  35. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  36. package/src/lib/jobs/ai-stock-photo-picker.js +13 -7
  37. package/src/lib/jobs/ai-stock-photo-picker.js.map +1 -1
  38. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.d.ts +35 -0
  39. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js +404 -0
  40. package/src/lib/jobs/fixtures/ai-stock-photo-agl3856-recordings.js.map +1 -0
  41. package/src/lib/layout-language/ai-layout-motion.d.ts +9 -3
  42. package/src/lib/layout-language/ai-layout-motion.js +9 -3
  43. package/src/lib/layout-language/ai-layout-motion.js.map +1 -1
  44. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  45. package/src/lib/layout-language/ai-layout-pictures.js +12 -6
  46. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  47. package/src/lib/model/ai-brief-copy.d.ts +85 -0
  48. package/src/lib/model/ai-brief-copy.js +152 -0
  49. package/src/lib/model/ai-brief-copy.js.map +1 -0
  50. package/src/lib/model/ai-create-view.d.ts +200 -0
  51. package/src/lib/model/ai-create-view.js +263 -0
  52. package/src/lib/model/ai-create-view.js.map +1 -0
  53. package/src/lib/model/ai-insight.d.ts +1 -1
  54. package/src/lib/model/ai-insight.js +4 -39
  55. package/src/lib/model/ai-insight.js.map +1 -1
  56. package/src/lib/model/ai-job-activity.d.ts +24 -0
  57. package/src/lib/model/ai-job-activity.js +52 -1
  58. package/src/lib/model/ai-job-activity.js.map +1 -1
  59. package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
  60. package/src/lib/model/ai-job-plan-lines.js +70 -0
  61. package/src/lib/model/ai-job-plan-lines.js.map +1 -0
  62. package/src/lib/model/ai-template-collections.d.ts +28 -0
  63. package/src/lib/model/ai-template-collections.js +28 -0
  64. package/src/lib/model/ai-template-collections.js.map +1 -0
  65. package/src/lib/model/ai-upsell-copy.d.ts +41 -0
  66. package/src/lib/model/ai-upsell-copy.js +85 -0
  67. package/src/lib/model/ai-upsell-copy.js.map +1 -0
  68. package/src/lib/runtime/ai-doctrine-validators.d.ts +40 -0
  69. package/src/lib/runtime/ai-doctrine-validators.js +148 -3
  70. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  71. package/src/lib/runtime/ai-doctrine.js +6 -2
  72. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  73. package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
  74. package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
  75. package/src/lib/server/ai-create-route.d.ts +32 -0
  76. package/src/lib/server/ai-create-route.js +295 -0
  77. package/src/lib/server/ai-create-route.js.map +1 -0
  78. package/src/lib/server/ai-jobs-route.js +19 -0
  79. package/src/lib/server/ai-jobs-route.js.map +1 -1
  80. package/src/lib/server.js +14 -0
  81. package/src/lib/server.js.map +1 -1
@@ -0,0 +1,295 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts
17
+ // The follow view climbs `aiJobsGate`, whose lockdown rung is the verdict. The
18
+ // door answers what a list draws and starts nothing: a locked workspace's
19
+ // start is refused by the jobs route's own ladder, in its own words.
20
+ import { _ as _extends } from "@swc/helpers/_/_extends";
21
+ import { checkEntitlement } from "@aglyn/aglyn/server";
22
+ import { COLLECTIONS_MAX_PER_HOST } from "@aglyn/aglyn/app-utils/collection-entries";
23
+ import { aiAddonName, isUncappedPlanComp, orgSubscriptionState, resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
24
+ import { listPluginSeatAddons } from "@aglyn/aglyn/plugin-manager/plugin-entitlements";
25
+ import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin/server/firebase-admin";
26
+ import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
27
+ import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-admin/server/organizations";
28
+ import { isServerReleaseFlagOnForOrg } from "@aglyn/tenant-data-admin/server/release-flags";
29
+ import { aiJobSiteRefusal } from "../jobs/ai-job-admission.js";
30
+ import { aiJobSummary, getAiJob, listAiJobs } from "../jobs/ai-jobs.js";
31
+ import { aiCreateDoorView, aiJobFollowView, isAiCreateKind } from "../model/ai-create-view.js";
32
+ import { AI_JOB_ACTIVE_STATUSES, AI_PAGE_BUILDING_JOB_KINDS, aiSiteBuildInFlight } from "../model/ai-job-activity.js";
33
+ import { aiTemplateCollectionOptions } from "../model/ai-template-collections.js";
34
+ import { assistSpendAfterReturnsUsd, ASSIST_RETURNED_USD_FIELD } from "../usage/assist-credit-returns.js";
35
+ import { assistBandRefuses, publicAssistCredits, resolveAssistBudgetUsd } from "../usage/assist-credits.js";
36
+ import { readFreeAiCreditsLeft } from "../usage/free-ai-credits-left.js";
37
+ import { aiJobsGate } from "./ai-jobs-gate.js";
38
+ /**
39
+ * `GET /api/ai/create?orgId&hostId&kind` — the door one list's "Create with
40
+ * AI" reads in the apps (AGL-3854), and `GET /api/ai/create/:jobId?orgId` —
41
+ * the job it started, as the dialog's follow card reads it.
42
+ *
43
+ * The console draws each entry from its shell's gates and its own components
44
+ * (`ai-describe-button`, `ai-brief-dialog`, `ai-job-follow`); the apps have
45
+ * neither, so these answer the same verdicts and the same words as data
46
+ * (`model/ai-create-view.ts`). Both only read. Starting, confirming and
47
+ * canceling go through the jobs routes the console calls, whose ladder decides
48
+ * every request exactly as it does for the console.
49
+ */ function bearerToken(request) {
50
+ var _request_headers_get;
51
+ const authorization = (_request_headers_get = request.headers.get('authorization')) != null ? _request_headers_get : '';
52
+ return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
53
+ }
54
+ async function doorCaller(request, orgId) {
55
+ var _resolved_org, _resolved_member;
56
+ const idToken = bearerToken(request);
57
+ if (!idToken) return Response.json({
58
+ error: 'Unauthenticated'
59
+ }, {
60
+ status: 401
61
+ });
62
+ let decoded;
63
+ try {
64
+ decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
65
+ } catch (error) {
66
+ const unauthenticated = invalidIdTokenResponse(error);
67
+ if (unauthenticated) return unauthenticated;
68
+ throw error;
69
+ }
70
+ if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse();
71
+ if (!orgId) return Response.json({
72
+ error: 'Open a workspace before using this feature'
73
+ }, {
74
+ status: 400
75
+ });
76
+ const resolved = await getOrgForUser(decoded.uid, orgId);
77
+ if (!resolved || resolved.orgId !== orgId) {
78
+ return Response.json({
79
+ error: 'You are not a member of that organization'
80
+ }, {
81
+ status: 403
82
+ });
83
+ }
84
+ return {
85
+ decoded,
86
+ staff: decoded['staff'] === true,
87
+ orgId,
88
+ org: (_resolved_org = resolved.org) != null ? _resolved_org : {},
89
+ member: (_resolved_member = resolved.member) != null ? _resolved_member : null
90
+ };
91
+ }
92
+ /**
93
+ * Whether an add-on this workspace can buy grants `aiGenerative`: the
94
+ * console's `entitlementPurchasable`, over the add-ons the AI plugin declares
95
+ * on the server.
96
+ */ export function aiGenerativePurchasable(org) {
97
+ const billing = org;
98
+ if (orgSubscriptionState(billing) !== 'live') return false;
99
+ const plan = resolveEffectivePlan(billing);
100
+ return listPluginSeatAddons().some((addon)=>{
101
+ var _addon_quota_perUnitByPlan_plan;
102
+ var _addon_features;
103
+ return ((_addon_features = addon.features) == null ? void 0 : _addon_features.includes('aiGenerative')) && (!addon.quota || ((_addon_quota_perUnitByPlan_plan = addon.quota.perUnitByPlan[plan]) != null ? _addon_quota_perUnitByPlan_plan : 0) > 0);
104
+ });
105
+ }
106
+ /** What is left of the month's credits, as the AI credits meter reads it. */ async function creditsFacts(firestore, orgId, org, now) {
107
+ if (resolveEffectivePlan(org) === 'free') {
108
+ const left = await readFreeAiCreditsLeft(firestore, {
109
+ orgId,
110
+ org: org,
111
+ now
112
+ });
113
+ return left ? {
114
+ kind: 'free',
115
+ left
116
+ } : {
117
+ kind: 'none'
118
+ };
119
+ }
120
+ const budgetUsd = resolveAssistBudgetUsd(org);
121
+ if (budgetUsd === null) return isUncappedPlanComp(org) ? {
122
+ kind: 'unlimited'
123
+ } : {
124
+ kind: 'none'
125
+ };
126
+ const usage = await firestore.collection('orgs').doc(orgId).collection('assistUsage').doc(now.toISOString().slice(0, 7)).get();
127
+ const credits = publicAssistCredits(assistSpendAfterReturnsUsd(usage.get('estCostUsd'), usage.get(ASSIST_RETURNED_USD_FIELD)), budgetUsd);
128
+ if (credits.limit === null || credits.remaining === null) return {
129
+ kind: 'none'
130
+ };
131
+ return {
132
+ kind: 'band',
133
+ used: credits.used,
134
+ limit: credits.limit,
135
+ remaining: credits.remaining,
136
+ stopsAtBand: assistBandRefuses(org)
137
+ };
138
+ }
139
+ const ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/;
140
+ /** The door: whether a list draws Create with AI, as what, and every word its brief says. */ export async function GET(request) {
141
+ var _url_searchParams_get, _url_searchParams_get1, _url_searchParams_get2;
142
+ const url = new URL(request.url);
143
+ const kind = (_url_searchParams_get = url.searchParams.get('kind')) != null ? _url_searchParams_get : '';
144
+ if (!isAiCreateKind(kind)) {
145
+ return Response.json({
146
+ error: 'kind must be one of page, template, layout, form, component'
147
+ }, {
148
+ status: 400
149
+ });
150
+ }
151
+ const hostId = (_url_searchParams_get1 = url.searchParams.get('hostId')) != null ? _url_searchParams_get1 : '';
152
+ if (!ID_CHARS.test(hostId)) return Response.json({
153
+ error: 'hostId is not a site id'
154
+ }, {
155
+ status: 400
156
+ });
157
+ const caller = await doorCaller(request, ((_url_searchParams_get2 = url.searchParams.get('orgId')) != null ? _url_searchParams_get2 : '').trim());
158
+ if (caller instanceof Response) return caller;
159
+ const { staff, orgId, org, member } = caller;
160
+ const firestore = firebaseAdmin.app().firestore();
161
+ const now = new Date();
162
+ const hidden = ()=>Response.json(aiCreateDoorView({
163
+ kind,
164
+ shown: false,
165
+ entitled: false,
166
+ purchasable: false,
167
+ canManageBilling: false,
168
+ credits: {
169
+ kind: 'none'
170
+ },
171
+ addonName: aiAddonName()
172
+ }), {
173
+ headers: {
174
+ 'Cache-Control': 'no-store'
175
+ }
176
+ });
177
+ // The shell's rungs, in the jobs gate's order: the reader's `ai.generate`
178
+ // on this site, the release flag, then the site's own AI switch. Any of
179
+ // them draws no entry at all; staff pass the first two, as they do there.
180
+ if (!staff && !await memberHasPermissionOnHost(orgId, hostId, member, 'ai.generate')) return hidden();
181
+ if (!staff && !await isServerReleaseFlagOnForOrg('release_ai_generative', orgId)) return hidden();
182
+ if (await aiJobSiteRefusal({
183
+ firestore,
184
+ org,
185
+ hostId
186
+ })) return hidden();
187
+ const entitled = checkEntitlement(org, 'aiGenerative');
188
+ if (!entitled) {
189
+ // Its own upsell where only the add-on is missing (AGL-3601): the add-on
190
+ // store open to this workspace, and an add-on it can buy that grants it.
191
+ const storeOpen = staff || await isServerReleaseFlagOnForOrg('release_addon_store', orgId);
192
+ const purchasable = !staff && storeOpen && aiGenerativePurchasable(org);
193
+ const canManageBilling = purchasable && await memberHasPermissionOnHost(orgId, null, member, 'billing.manage');
194
+ return Response.json(aiCreateDoorView({
195
+ kind,
196
+ shown: true,
197
+ entitled: false,
198
+ purchasable,
199
+ canManageBilling,
200
+ credits: {
201
+ kind: 'none'
202
+ },
203
+ addonName: aiAddonName()
204
+ }), {
205
+ headers: {
206
+ 'Cache-Control': 'no-store'
207
+ }
208
+ });
209
+ }
210
+ const [credits, collections, buildInFlight] = await Promise.all([
211
+ creditsFacts(firestore, orgId, org, now).catch((error)=>{
212
+ console.error('[ai-create] credits read failed', orgId, error);
213
+ return {
214
+ kind: 'none'
215
+ };
216
+ }),
217
+ kind === 'template' ? firestore.collection('hosts').doc(hostId).collection('collections').limit(COLLECTIONS_MAX_PER_HOST).get().then((snapshot)=>({
218
+ status: 'success',
219
+ options: aiTemplateCollectionOptions(snapshot.docs.map((doc)=>_extends({}, doc.data(), {
220
+ $id: doc.id
221
+ })))
222
+ })).catch(()=>({
223
+ status: 'error',
224
+ options: []
225
+ })) : Promise.resolve(null),
226
+ kind === 'page' ? listAiJobs(firestore, orgId, {
227
+ statuses: AI_JOB_ACTIVE_STATUSES
228
+ }).then((jobs)=>aiSiteBuildInFlight(jobs.map((job)=>aiJobSummary(job, now)), {
229
+ hostId
230
+ }) !== null).catch(()=>false) : Promise.resolve(false)
231
+ ]);
232
+ return Response.json(aiCreateDoorView({
233
+ kind: kind,
234
+ shown: true,
235
+ entitled: true,
236
+ purchasable: false,
237
+ canManageBilling: false,
238
+ credits,
239
+ collections,
240
+ buildInFlight,
241
+ addonName: aiAddonName()
242
+ }), {
243
+ headers: {
244
+ 'Cache-Control': 'no-store'
245
+ }
246
+ });
247
+ }
248
+ /** The follow view: one job of the workspace's, as the dialog that started it reads it. */ export async function GET_FOLLOW(request, { params }) {
249
+ var _url_searchParams_get;
250
+ var _summary_review;
251
+ const { jobId } = await params;
252
+ if (!ID_CHARS.test(jobId != null ? jobId : '')) return Response.json({
253
+ error: 'Not found'
254
+ }, {
255
+ status: 404
256
+ });
257
+ const url = new URL(request.url);
258
+ const gate = await aiJobsGate(request, (_url_searchParams_get = url.searchParams.get('orgId')) != null ? _url_searchParams_get : '');
259
+ if (gate instanceof Response) return gate;
260
+ const now = new Date();
261
+ const job = await getAiJob(gate.firestore, gate.orgId, jobId);
262
+ if (!job) return Response.json({
263
+ error: 'Not found'
264
+ }, {
265
+ status: 404
266
+ });
267
+ const summary = aiJobSummary(job, now);
268
+ // Confirming builds pages while another job is building on the site
269
+ // (AGL-3765): asked only while this one waits on its plan.
270
+ const askInFlight = summary.status === 'needs_review' && ((_summary_review = summary.review) == null ? void 0 : _summary_review.reason) === 'plan' && AI_PAGE_BUILDING_JOB_KINDS.includes(summary.kind);
271
+ const buildInFlight = askInFlight ? await listAiJobs(gate.firestore, gate.orgId, {
272
+ statuses: AI_JOB_ACTIVE_STATUSES
273
+ }).then((jobs)=>aiSiteBuildInFlight(jobs.map((one)=>aiJobSummary(one, now)), {
274
+ hostId: summary.hostId,
275
+ exceptJobId: summary.id
276
+ }) !== null).catch(()=>false) : false;
277
+ return Response.json({
278
+ job: summary,
279
+ view: aiJobFollowView(summary, {
280
+ // Console addresses name the workspace by its slug, as every link the apps resolve does.
281
+ orgSlug: typeof gate.org.slug === 'string' ? gate.org.slug : null,
282
+ staff: gate.staff,
283
+ buildInFlight,
284
+ now: now.getTime()
285
+ })
286
+ }, {
287
+ status: 200,
288
+ headers: {
289
+ 'Cache-Control': 'no-store'
290
+ }
291
+ });
292
+ }
293
+ export const dynamic = 'force-dynamic';
294
+
295
+ //# sourceMappingURL=ai-create-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-create-route.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\n// lockdown-423: via libs/plugins/ai/src/lib/server/ai-jobs-gate.ts\n// The follow view climbs `aiJobsGate`, whose lockdown rung is the verdict. The\n// door answers what a list draws and starts nothing: a locked workspace's\n// start is refused by the jobs route's own ladder, in its own words.\n\nimport type { DecodedIdToken } from 'firebase-admin/auth'\nimport { checkEntitlement } from '@aglyn/aglyn/server'\nimport { COLLECTIONS_MAX_PER_HOST } from '@aglyn/aglyn/app-utils/collection-entries'\nimport {\n aiAddonName,\n isUncappedPlanComp,\n orgSubscriptionState,\n resolveEffectivePlan,\n} from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { listPluginSeatAddons } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport type { AglynOrganization, AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { getOrgForUser, memberHasPermissionOnHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\nimport { aiJobSiteRefusal } from '../jobs/ai-job-admission'\nimport { aiJobSummary, getAiJob, listAiJobs } from '../jobs/ai-jobs'\nimport {\n aiCreateDoorView,\n aiJobFollowView,\n isAiCreateKind,\n type AiCreateCreditsFacts,\n type AiCreateKind,\n} from '../model/ai-create-view'\nimport { AI_JOB_ACTIVE_STATUSES, AI_PAGE_BUILDING_JOB_KINDS, aiSiteBuildInFlight } from '../model/ai-job-activity'\nimport { aiTemplateCollectionOptions } from '../model/ai-template-collections'\nimport { assistSpendAfterReturnsUsd, ASSIST_RETURNED_USD_FIELD } from '../usage/assist-credit-returns'\nimport { assistBandRefuses, publicAssistCredits, resolveAssistBudgetUsd } from '../usage/assist-credits'\nimport { readFreeAiCreditsLeft } from '../usage/free-ai-credits-left'\nimport { aiJobsGate } from './ai-jobs-gate'\n\n/**\n * `GET /api/ai/create?orgId&hostId&kind` — the door one list's \"Create with\n * AI\" reads in the apps (AGL-3854), and `GET /api/ai/create/:jobId?orgId` —\n * the job it started, as the dialog's follow card reads it.\n *\n * The console draws each entry from its shell's gates and its own components\n * (`ai-describe-button`, `ai-brief-dialog`, `ai-job-follow`); the apps have\n * neither, so these answer the same verdicts and the same words as data\n * (`model/ai-create-view.ts`). Both only read. Starting, confirming and\n * canceling go through the jobs routes the console calls, whose ladder decides\n * every request exactly as it does for the console.\n */\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n}\n\n/** Every rung the console's shell climbs before it draws the entry, in the jobs gate's order. */\ninterface DoorCaller {\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n member: AglynOrgMember | null\n}\n\nasync function doorCaller(request: Request, orgId: string): Promise<Response | DoorCaller> {\n const idToken = bearerToken(request)\n if (!idToken) return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n let decoded: DecodedIdToken\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse()\n if (!orgId) return Response.json({ error: 'Open a workspace before using this feature' }, { status: 400 })\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return Response.json({ error: 'You are not a member of that organization' }, { status: 403 })\n }\n return {\n decoded,\n staff: decoded['staff'] === true,\n orgId,\n org: resolved.org ?? {},\n member: resolved.member ?? null,\n }\n}\n\n/**\n * Whether an add-on this workspace can buy grants `aiGenerative`: the\n * console's `entitlementPurchasable`, over the add-ons the AI plugin declares\n * on the server.\n */\nexport function aiGenerativePurchasable(org: Partial<AglynOrganization>): boolean {\n const billing = org as Parameters<typeof checkEntitlement>[0]\n if (orgSubscriptionState(billing as never) !== 'live') return false\n const plan = resolveEffectivePlan(billing as never)\n return listPluginSeatAddons().some(\n (addon) =>\n addon.features?.includes('aiGenerative') && (!addon.quota || (addon.quota.perUnitByPlan[plan] ?? 0) > 0),\n )\n}\n\n/** What is left of the month's credits, as the AI credits meter reads it. */\nasync function creditsFacts(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n org: Partial<AglynOrganization>,\n now: Date,\n): Promise<AiCreateCreditsFacts> {\n if (resolveEffectivePlan(org as never) === 'free') {\n const left = await readFreeAiCreditsLeft(firestore, { orgId, org: org as object, now })\n return left ? { kind: 'free', left } : { kind: 'none' }\n }\n const budgetUsd = resolveAssistBudgetUsd(org as never)\n if (budgetUsd === null) return isUncappedPlanComp(org as never) ? { kind: 'unlimited' } : { kind: 'none' }\n const usage = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('assistUsage')\n .doc(now.toISOString().slice(0, 7))\n .get()\n const credits = publicAssistCredits(\n assistSpendAfterReturnsUsd(usage.get('estCostUsd'), usage.get(ASSIST_RETURNED_USD_FIELD)),\n budgetUsd,\n )\n if (credits.limit === null || credits.remaining === null) return { kind: 'none' }\n return {\n kind: 'band',\n used: credits.used,\n limit: credits.limit,\n remaining: credits.remaining,\n stopsAtBand: assistBandRefuses(org as never),\n }\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The door: whether a list draws Create with AI, as what, and every word its brief says. */\nexport async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const kind = url.searchParams.get('kind') ?? ''\n if (!isAiCreateKind(kind)) {\n return Response.json({ error: 'kind must be one of page, template, layout, form, component' }, { status: 400 })\n }\n const hostId = url.searchParams.get('hostId') ?? ''\n if (!ID_CHARS.test(hostId)) return Response.json({ error: 'hostId is not a site id' }, { status: 400 })\n const caller = await doorCaller(request, (url.searchParams.get('orgId') ?? '').trim())\n if (caller instanceof Response) return caller\n const { staff, orgId, org, member } = caller\n const firestore = firebaseAdmin.app().firestore()\n const now = new Date()\n const hidden = () =>\n Response.json(\n aiCreateDoorView({\n kind,\n shown: false,\n entitled: false,\n purchasable: false,\n canManageBilling: false,\n credits: { kind: 'none' },\n addonName: aiAddonName(),\n }),\n { headers: { 'Cache-Control': 'no-store' } },\n )\n\n // The shell's rungs, in the jobs gate's order: the reader's `ai.generate`\n // on this site, the release flag, then the site's own AI switch. Any of\n // them draws no entry at all; staff pass the first two, as they do there.\n if (!staff && !(await memberHasPermissionOnHost(orgId, hostId, member, 'ai.generate'))) return hidden()\n if (!staff && !(await isServerReleaseFlagOnForOrg('release_ai_generative', orgId))) return hidden()\n if (await aiJobSiteRefusal({ firestore, org, hostId })) return hidden()\n\n const entitled = checkEntitlement(org as never, 'aiGenerative')\n if (!entitled) {\n // Its own upsell where only the add-on is missing (AGL-3601): the add-on\n // store open to this workspace, and an add-on it can buy that grants it.\n const storeOpen = staff || (await isServerReleaseFlagOnForOrg('release_addon_store', orgId))\n const purchasable = !staff && storeOpen && aiGenerativePurchasable(org)\n const canManageBilling = purchasable && (await memberHasPermissionOnHost(orgId, null, member, 'billing.manage'))\n return Response.json(\n aiCreateDoorView({\n kind,\n shown: true,\n entitled: false,\n purchasable,\n canManageBilling,\n credits: { kind: 'none' },\n addonName: aiAddonName(),\n }),\n { headers: { 'Cache-Control': 'no-store' } },\n )\n }\n\n const [credits, collections, buildInFlight] = await Promise.all([\n creditsFacts(firestore, orgId, org, now).catch((error) => {\n console.error('[ai-create] credits read failed', orgId, error)\n return { kind: 'none' } as const\n }),\n kind === 'template'\n ? firestore\n .collection('hosts')\n .doc(hostId)\n .collection('collections')\n .limit(COLLECTIONS_MAX_PER_HOST)\n .get()\n .then((snapshot) => ({\n status: 'success' as const,\n options: aiTemplateCollectionOptions(\n snapshot.docs.map((doc) => ({ ...(doc.data() as Record<string, unknown>), $id: doc.id })),\n ),\n }))\n .catch(() => ({ status: 'error' as const, options: [] }))\n : Promise.resolve(null),\n kind === 'page'\n ? listAiJobs(firestore, orgId, { statuses: AI_JOB_ACTIVE_STATUSES })\n .then((jobs) => aiSiteBuildInFlight(jobs.map((job) => aiJobSummary(job, now)), { hostId }) !== null)\n .catch(() => false)\n : Promise.resolve(false),\n ])\n return Response.json(\n aiCreateDoorView({\n kind: kind as AiCreateKind,\n shown: true,\n entitled: true,\n purchasable: false,\n canManageBilling: false,\n credits,\n collections,\n buildInFlight,\n addonName: aiAddonName(),\n }),\n { headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\n/** The follow view: one job of the workspace's, as the dialog that started it reads it. */\nexport async function GET_FOLLOW(request: Request, { params }: { params: Promise<{ jobId: string }> }): Promise<Response> {\n const { jobId } = await params\n if (!ID_CHARS.test(jobId ?? '')) return Response.json({ error: 'Not found' }, { status: 404 })\n const url = new URL(request.url)\n const gate = await aiJobsGate(request, url.searchParams.get('orgId') ?? '')\n if (gate instanceof Response) return gate\n const now = new Date()\n const job = await getAiJob(gate.firestore, gate.orgId, jobId)\n if (!job) return Response.json({ error: 'Not found' }, { status: 404 })\n const summary = aiJobSummary(job, now)\n // Confirming builds pages while another job is building on the site\n // (AGL-3765): asked only while this one waits on its plan.\n const askInFlight =\n summary.status === 'needs_review' &&\n summary.review?.reason === 'plan' &&\n AI_PAGE_BUILDING_JOB_KINDS.includes(summary.kind)\n const buildInFlight = askInFlight\n ? await listAiJobs(gate.firestore, gate.orgId, { statuses: AI_JOB_ACTIVE_STATUSES })\n .then(\n (jobs) =>\n aiSiteBuildInFlight(\n jobs.map((one) => aiJobSummary(one, now)),\n { hostId: summary.hostId, exceptJobId: summary.id },\n ) !== null,\n )\n .catch(() => false)\n : false\n return Response.json(\n {\n job: summary,\n view: aiJobFollowView(summary, {\n // Console addresses name the workspace by its slug, as every link the apps resolve does.\n orgSlug: typeof gate.org.slug === 'string' ? gate.org.slug : null,\n staff: gate.staff,\n buildInFlight,\n now: now.getTime(),\n }),\n },\n { status: 200, headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\nexport const dynamic = 'force-dynamic'\n"],"names":["checkEntitlement","COLLECTIONS_MAX_PER_HOST","aiAddonName","isUncappedPlanComp","orgSubscriptionState","resolveEffectivePlan","listPluginSeatAddons","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","invalidIdTokenResponse","getOrgForUser","memberHasPermissionOnHost","isServerReleaseFlagOnForOrg","aiJobSiteRefusal","aiJobSummary","getAiJob","listAiJobs","aiCreateDoorView","aiJobFollowView","isAiCreateKind","AI_JOB_ACTIVE_STATUSES","AI_PAGE_BUILDING_JOB_KINDS","aiSiteBuildInFlight","aiTemplateCollectionOptions","assistSpendAfterReturnsUsd","ASSIST_RETURNED_USD_FIELD","assistBandRefuses","publicAssistCredits","resolveAssistBudgetUsd","readFreeAiCreditsLeft","aiJobsGate","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","doorCaller","orgId","resolved","idToken","Response","json","error","status","decoded","app","auth","verifyIdToken","unauthenticated","email_verified","uid","staff","org","member","aiGenerativePurchasable","billing","plan","some","addon","features","includes","quota","perUnitByPlan","creditsFacts","firestore","now","left","kind","budgetUsd","usage","collection","doc","toISOString","credits","limit","remaining","used","stopsAtBand","ID_CHARS","GET","url","URL","searchParams","hostId","test","caller","trim","Date","hidden","shown","entitled","purchasable","canManageBilling","addonName","storeOpen","collections","buildInFlight","Promise","all","catch","console","then","snapshot","options","docs","map","data","$id","id","resolve","statuses","jobs","job","GET_FOLLOW","params","summary","jobId","gate","askInFlight","review","reason","one","exceptJobId","view","orgSlug","slug","getTime","dynamic"],"mappings":"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"}
@@ -71,6 +71,13 @@ import { reportServerError } from "@aglyn/tenant-data-admin/server/client-error-
71
71
  * caller gets the job document, and the events route is how it watches
72
72
  * from there.
73
73
  *
74
+ * `POST` with `check: true` climbs the same ladder and asks the same
75
+ * admission, then hands the reservation back and creates nothing: a 200
76
+ * with `ok` true, or the refusal the create would have answered. "Create site"
77
+ * asks it before it makes the site a guided start builds on (AGL-3859), so
78
+ * a start that would be refused, or that asks for the Free go-ahead, leaves
79
+ * no empty site behind. A checked job may name no site yet.
80
+ *
74
81
  * `GET` lists the org's jobs, newest first, through the read gate: the
75
82
  * same rungs up to the lockdown verdict, no reservation. A job naming a
76
83
  * site that no longer exists is left out, so a page can come back shorter
@@ -191,6 +198,8 @@ export async function POST(request) {
191
198
  // A Free start past what is left, which the person chose to build as far as
192
199
  // it goes (AGL-3722): the prompt they confirmed, kept on the job.
193
200
  let confirmedCredits = null;
201
+ // Asked before the site exists (AGL-3859): answers, creates nothing.
202
+ const check = (payload == null ? void 0 : payload['check']) === true;
194
203
  try {
195
204
  var _ref1;
196
205
  // A site that switched AI off first (AGL-3028), then the kind's own check.
@@ -202,6 +211,7 @@ export async function POST(request) {
202
211
  firestore: gate.firestore,
203
212
  orgId: gate.orgId,
204
213
  hostId: parsed.hostId,
214
+ siteToCome: check && !parsed.hostId,
205
215
  inputs: parsed.inputs,
206
216
  org: gate.org,
207
217
  uid: gate.uid,
@@ -238,6 +248,15 @@ export async function POST(request) {
238
248
  status: refusal.status
239
249
  });
240
250
  }
251
+ if (check) {
252
+ // Admitted, and nothing to run: the reservation goes back.
253
+ await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(()=>undefined);
254
+ return Response.json({
255
+ ok: true
256
+ }, {
257
+ status: 200
258
+ });
259
+ }
241
260
  const now = new Date();
242
261
  let created;
243
262
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-route.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; the\n// GET climbs `ai-jobs-gate.ts`, which carries the same rung.\n\nimport { randomUUID } from 'crypto'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n AI_JOB_KINDS,\n type AiJobKind,\n type AiJobStatus,\n} from '../model/ai-jobs.types'\n// By their own entry points rather than the barrel (AGL-2903): the specs\n// stub the barrel closed-world, and the ladder, the machine and the runtime\n// are the things under test here, not things to be stubbed away.\nimport { aiGateLadder } from '../runtime/ai-gate'\nimport { aiJobAdmissionRefusal, aiJobSiteRefusal } from '../jobs/ai-job-admission'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobAdmittedInputs } from '../jobs/ai-job-auto-confirm'\nimport {\n AI_JOB_INLINE_BUDGET_MS,\n aiJobNextStepMinimumMs,\n aiJobSummary,\n createAiJob,\n listAiJobs,\n runAiJobStep,\n} from '../jobs/ai-jobs'\nimport { releaseAssistMessage } from '../usage/assist-usage'\nimport { readFreeAiCreditsLeft } from '../usage/free-ai-credits-left'\nimport { aiCreditsConfirmation, type AiCreditsPrompt } from '../model/ai-credit-estimate'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport { aiJobsGate } from './ai-jobs-gate'\nimport { withoutErasedSites } from './ai-jobs-live-sites'\nimport { AI_JOB_ACTIVE_STATUSES } from '../model/ai-job-activity'\nimport { reportServerError } from '@aglyn/tenant-data-admin/server/client-error-report'\n\n/**\n * A caught fault on this door, reported as well as logged (AGL-1921).\n *\n * `onRequestError` sees only an UNCAUGHT throw, and the Vercel log drain\n * forwards the status line but not a `console.error`, so the three 500s\n * this route answered on 2026-10-06 at 17:41–17:47Z reached the alert as\n * START/END/REPORT with no reason anywhere. Never throws.\n */\nasync function reportJobFault(what: string, error: unknown): Promise<void> {\n try {\n await reportServerError(\n {\n message: `${what}: ${error instanceof Error ? error.message : String(error)}`,\n stack: error instanceof Error ? error.stack : undefined,\n route: '/api/ai/jobs',\n routeType: 'route',\n method: 'POST',\n },\n { service: 'console-web' },\n )\n } catch {\n // The console line beside each call is the record of last resort.\n }\n}\n\n/**\n * AI generation jobs: create and list (AGL-2904).\n *\n * `POST` is a door that spends, so it climbs the whole ladder (AGL-2903) —\n * `aiGenerative`, `release_ai_generative`, the `ai-generate` switch, a\n * per-uid window, and a reservation — and then creates the job and runs\n * its first step INLINE under `AI_JOB_INLINE_BUDGET_MS`. A step that\n * finishes in time answers with the job `done` (or `queued` behind its\n * next step); one that does not is aborted, its reservation handed back,\n * and the job answers `queued` for the beat to resume. Either way the\n * caller gets the job document, and the events route is how it watches\n * from there.\n *\n * `GET` lists the org's jobs, newest first, through the read gate: the\n * same rungs up to the lockdown verdict, no reservation. A job naming a\n * site that no longer exists is left out, so a page can come back shorter\n * than its `limit`.\n *\n * The request must NAME the org it is metered against (AGL-1934). There\n * is no fallback to \"the caller's first org\".\n */\n\nconst AI_JOBS_RATE_LIMIT = { key: 'ai-jobs', limit: 10, windowMs: 60_000 }\n\n/** Kind-specific inputs, bounded: a runner reads scalars, not a document. */\nconst MAX_INPUTS_JSON_CHARS = 8_000\n\n/** The admitted statuses for the list filter, as the union spells them. */\nconst AI_JOB_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport interface CreateAiJobBody {\n orgId: string\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string | number | boolean>\n /** A catalog model the creator picked (AGL-2942), or `null` for Auto. */\n model: string | null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The longest model id a body may carry; the catalog's ids are far shorter. */\nconst MAX_MODEL_CHARS = 100\n\n/** Validate + clamp the request body; a string names what is wrong. */\nexport function parseCreateAiJobBody(payload: unknown): CreateAiJobBody | string {\n const body = (payload ?? {}) as Record<string, unknown>\n const orgId = String(body.orgId ?? '').trim()\n if (!orgId) return 'Open a workspace before using this feature'\n const kind = String(body.kind ?? '')\n if (!(AI_JOB_KINDS as readonly string[]).includes(kind)) {\n return `kind must be one of ${AI_JOB_KINDS.join(', ')}`\n }\n const brief = String(body.brief ?? '').trim()\n if (!brief) return 'Write a brief for the job'\n if (brief.length > AI_JOB_BRIEF_MAX_CHARS) {\n return `Keep the brief under ${AI_JOB_BRIEF_MAX_CHARS.toLocaleString('en-US')} characters`\n }\n const rawHost = body.hostId\n const hostId =\n rawHost === undefined || rawHost === null || rawHost === ''\n ? null\n : typeof rawHost === 'string' && ID_CHARS.test(rawHost)\n ? rawHost\n : undefined\n if (hostId === undefined) return 'hostId is not a site id'\n // A theme is fields on one site's document, so a theme job that names no\n // site has nothing to propose a change to (AGL-2938).\n if (kind === 'theme' && !hostId) return 'Open a site before changing its theme'\n const rawInputs = body.inputs\n const inputs: Record<string, string | number | boolean> = {}\n if (rawInputs !== undefined && rawInputs !== null) {\n if (typeof rawInputs !== 'object' || Array.isArray(rawInputs)) {\n return 'inputs must be an object'\n }\n for (const [key, value] of Object.entries(rawInputs as Record<string, unknown>)) {\n if (\n typeof value !== 'string' &&\n typeof value !== 'number' &&\n typeof value !== 'boolean'\n ) {\n return `inputs.${key} must be a string, number or boolean`\n }\n inputs[key] = value\n }\n if (JSON.stringify(inputs).length > MAX_INPUTS_JSON_CHARS) {\n return 'inputs are too large'\n }\n }\n const rawModel = typeof body.model === 'string' ? body.model.trim() : ''\n if (rawModel.length > MAX_MODEL_CHARS) return 'model is not a model id'\n // Validated against the plan and the allotment allowlists when each step\n // runs, not here: the allowlists are read inside the step's reservation.\n const model = rawModel && rawModel !== 'auto' ? rawModel : null\n return { orgId, hostId, kind: kind as AiJobKind, brief, inputs, model }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const parsed = parseCreateAiJobBody(payload)\n // The org is parsed BEFORE the ladder so a request that named none is\n // refused with the ladder's 400 rather than a shape error; every other\n // shape fault waits until the caller has proven who they are.\n const orgId = typeof parsed === 'string' ? String((payload as Record<string, unknown> | null)?.orgId ?? '') : parsed.orgId\n // The site rides along for the same reason: a collaborator's `ai.generate`\n // is decided per site (AGL-2927), so the ladder needs it before it can say\n // whether this member may generate here at all.\n const hostId = typeof parsed === 'string' ? null : parsed.hostId\n const gate = await aiGateLadder(\n { request, orgId, hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: AI_JOBS_RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n if (typeof parsed === 'string') {\n // Admitted, reserved, and then found to have sent nothing runnable: the\n // reservation goes back, because no token was spent.\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n return Response.json({ error: parsed }, { status: 400 })\n }\n // What only this kind can say about the request (AGL-2909): a site it\n // needs, inputs it reads, an allowance its draft counts against. Asked\n // before the job exists, so a refusal spends nothing.\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>>\n // A Free start past what is left, which the person chose to build as far as\n // it goes (AGL-3722): the prompt they confirmed, kept on the job.\n let confirmedCredits: AiCreditsPrompt | null = null\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: parsed.hostId,\n })) ??\n (await aiJobAdmissionRefusal(parsed.kind, {\n firestore: gate.firestore,\n orgId: gate.orgId,\n hostId: parsed.hostId,\n inputs: parsed.inputs,\n org: gate.org,\n uid: gate.uid,\n creditsConfirmed: (payload as Record<string, unknown> | null)?.['creditsConfirmed'] === true,\n onCreditsConfirmed: (prompt) => {\n confirmedCredits = prompt\n },\n }))\n } catch (error) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job admission failed', { orgId: gate.orgId, kind: parsed.kind, error })\n await reportJobFault('ai job admission failed', error)\n return Response.json({ error: 'The AI job could not be created' }, { status: 500 })\n }\n if (refusal) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n // A Free start past what is left asks before it starts (AGL-3722): the\n // prompt rides the 409, so the dialog offers its choices.\n return Response.json(\n { error: refusal.error, ...(refusal.code ? { code: refusal.code } : {}), ...(refusal.credits ? { credits: refusal.credits } : {}) },\n { status: refusal.status },\n )\n }\n\n const now = new Date()\n let created: Awaited<ReturnType<typeof createAiJob>>\n try {\n // The guided start's own plan confirmation (AGL-3594), kept only for a\n // site job on a site that has published nothing of the owner's yet.\n const inputs = await aiJobAdmittedInputs(gate.firestore, {\n kind: parsed.kind,\n hostId: parsed.hostId,\n inputs: parsed.inputs,\n })\n created = await createAiJob(\n gate.firestore,\n {\n orgId: gate.orgId,\n hostId: parsed.hostId,\n kind: parsed.kind,\n brief: parsed.brief,\n inputs,\n model: parsed.model,\n createdBy: gate.uid,\n createdByEmail: gate.decoded.email ?? null,\n creditsConfirmed: aiCreditsConfirmation(confirmedCredits, gate.uid, now),\n },\n now,\n )\n } catch (error) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job create failed', { orgId: gate.orgId, error })\n await reportJobFault('ai job create failed', error)\n return Response.json({ error: 'The AI job could not be created' }, { status: 500 })\n }\n const jobId = created.$id\n\n // A first 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. Nothing ran, so the reservation goes back.\n if (aiJobNextStepMinimumMs(created) > AI_JOB_INLINE_BUDGET_MS) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n return Response.json(\n {\n job: aiJobSummary(created, now),\n meter: aiUsageMeter(gate.reservation, { lastCredits: null }),\n },\n { status: 200 },\n )\n }\n\n // The first step, inline, on the reservation the ladder already holds.\n // The runner hands that reservation back itself if the provider is never\n // reached, and re-queues the step when the budget ends first.\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 // The caller is on the request, so what the inline step produces is\n // attributed with their address; the beat's steps carry the uid alone.\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 // A job created a moment ago has a claimable first step; anything else\n // is a fault in the machine, and the reservation must not be stranded.\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job first step not claimable', { orgId: gate.orgId, jobId })\n await reportJobFault('ai job first step not claimable', new Error(`job ${jobId}`))\n return Response.json({ error: 'The AI job could not be started' }, { status: 500 })\n }\n return Response.json(\n {\n job: aiJobSummary(run.job),\n // The usage strip's envelope (AGL-2942): the reservation the ladder\n // took, with what the inline step spent.\n meter: aiUsageMeter(gate.reservation, {\n lastCredits: run.job.creditsSpent > 0 ? run.job.creditsSpent : null,\n }),\n },\n { status: 200 },\n )\n}\n\nexport async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const gate = await aiJobsGate(request, url.searchParams.get('orgId') ?? '')\n if (gate instanceof Response) return gate\n const rawStatus = url.searchParams.get('status') ?? ''\n const status = (AI_JOB_STATUSES as readonly string[]).includes(rawStatus)\n ? (rawStatus as AiJobStatus)\n : undefined\n // `active` (AGL-3593): every job not yet settled, which is what the\n // console's top-bar indicator and the Assist launcher count.\n const active = rawStatus === 'active'\n const limit = Number(url.searchParams.get('limit') ?? '')\n // `hostId` (AGL-3596): one site's jobs, for the site's AI jobs page — a\n // query on the field under the (hostId, createdAt) index, never a filter\n // of a loaded window. An id that is not one is refused rather than ignored,\n // which would answer with every site's jobs.\n const rawHost = url.searchParams.get('hostId')\n if (rawHost !== null && !ID_CHARS.test(rawHost)) {\n return Response.json({ error: 'hostId is not a site id' }, { status: 400 })\n }\n const now = new Date()\n // A job naming a site that has been deleted is not listed (AGL-3596), so\n // no surface counts or shows work on a site nobody can open.\n const jobs = await withoutErasedSites(\n gate.firestore,\n await listAiJobs(gate.firestore, gate.orgId, {\n ...(status ? { status } : {}),\n ...(active ? { statuses: AI_JOB_ACTIVE_STATUSES } : {}),\n ...(rawHost ? { hostId: rawHost } : {}),\n ...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),\n }),\n )\n const freeTaste = resolveEffectivePlan(gate.org as never) === 'free'\n // What the Free workspace has left this month (AGL-3660): the less of its\n // own band and its owner's allowance across their Free workspaces, which is\n // what the steps' reservations refuse at — never the 300 a month starts with.\n const freeCredits = freeTaste\n ? await readFreeAiCreditsLeft(gate.firestore, { orgId: gate.orgId, org: gate.org as object | null, now })\n : null\n return Response.json(\n {\n jobs: jobs.map((job) => aiJobSummary(job, now)),\n // Whether the workspace spends the Free taste (AGL-3594), so the guided\n // start offers the Free page band from the request it already makes.\n freeTaste,\n ...(freeCredits ? { freeCredits } : {}),\n },\n { status: 200, headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["randomUUID","resolveEffectivePlan","AI_JOB_KINDS","aiGateLadder","aiJobAdmissionRefusal","aiJobSiteRefusal","AI_JOB_BRIEF_MAX_CHARS","aiJobAdmittedInputs","AI_JOB_INLINE_BUDGET_MS","aiJobNextStepMinimumMs","aiJobSummary","createAiJob","listAiJobs","runAiJobStep","releaseAssistMessage","readFreeAiCreditsLeft","aiCreditsConfirmation","aiUsageMeter","aiJobsGate","withoutErasedSites","AI_JOB_ACTIVE_STATUSES","reportServerError","reportJobFault","what","error","message","Error","String","stack","undefined","route","routeType","method","service","AI_JOBS_RATE_LIMIT","key","limit","windowMs","MAX_INPUTS_JSON_CHARS","AI_JOB_STATUSES","ID_CHARS","MAX_MODEL_CHARS","parseCreateAiJobBody","payload","body","orgId","trim","kind","includes","join","brief","length","toLocaleString","rawHost","hostId","test","rawInputs","inputs","Array","isArray","value","Object","entries","JSON","stringify","rawModel","model","POST","request","gate","json","parsed","feature","releaseFlag","lockdownFeature","permission","rateLimit","Response","firestore","reservation","catch","status","refusal","confirmedCredits","org","uid","creditsConfirmed","onCreditsConfirmed","prompt","console","code","credits","now","Date","created","createdBy","createdByEmail","decoded","email","jobId","$id","job","meter","lastCredits","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","creditsSpent","GET","url","URL","searchParams","get","rawStatus","active","Number","jobs","statuses","isFinite","freeTaste","freeCredits","map","headers","dynamic","maxDuration"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,+DAA+D;AAC/D,0EAA0E;AAC1E,6DAA6D;AAE7D,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,oBAAoB,QAAQ,2CAA0C;AAC/E,SACEC,YAAY,QAGP,4BAAwB;AAC/B,yEAAyE;AACzE,4EAA4E;AAC5E,iEAAiE;AACjE,SAASC,YAAY,QAAQ,wBAAoB;AACjD,SAASC,qBAAqB,EAAEC,gBAAgB,QAAQ,8BAA0B;AAClF,SAASC,sBAAsB,QAAQ,8BAA0B;AACjE,SAASC,mBAAmB,QAAQ,iCAA6B;AACjE,SACEC,uBAAuB,EACvBC,sBAAsB,EACtBC,YAAY,EACZC,WAAW,EACXC,UAAU,EACVC,YAAY,QACP,qBAAiB;AACxB,SAASC,oBAAoB,QAAQ,2BAAuB;AAC5D,SAASC,qBAAqB,QAAQ,mCAA+B;AACrE,SAASC,qBAAqB,QAA8B,iCAA6B;AACzF,SAASC,YAAY,QAAQ,6BAAyB;AACtD,SAASC,UAAU,QAAQ,oBAAgB;AAC3C,SAASC,kBAAkB,QAAQ,0BAAsB;AACzD,SAASC,sBAAsB,QAAQ,8BAA0B;AACjE,SAASC,iBAAiB,QAAQ,sDAAqD;AAEvF;;;;;;;CAOC,GACD,eAAeC,eAAeC,IAAY,EAAEC,KAAc;IACxD,IAAI;QACF,MAAMH,kBACJ;YACEI,SAAS,GAAGF,KAAK,EAAE,EAAEC,iBAAiBE,QAAQF,MAAMC,OAAO,GAAGE,OAAOH,QAAQ;YAC7EI,OAAOJ,iBAAiBE,QAAQF,MAAMI,KAAK,GAAGC;YAC9CC,OAAO;YACPC,WAAW;YACXC,QAAQ;QACV,GACA;YAAEC,SAAS;QAAc;IAE7B,EAAE,eAAM;IACN,kEAAkE;IACpE;AACF;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GAED,MAAMC,qBAAqB;IAAEC,KAAK;IAAWC,OAAO;IAAIC,UAAU;AAAO;AAEzE,2EAA2E,GAC3E,MAAMC,wBAAwB;AAE9B,yEAAyE,GACzE,MAAMC,kBAA0C;IAC9C;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAYD,MAAMC,WAAW;AAEjB,8EAA8E,GAC9E,MAAMC,kBAAkB;AAExB,qEAAqE,GACrE,OAAO,SAASC,qBAAqBC,OAAgB;QAE9BC,aAEDA,YAICA;IAPrB,MAAMA,OAAQD,kBAAAA,UAAW,CAAC;IAC1B,MAAME,QAAQlB,QAAOiB,cAAAA,KAAKC,KAAK,YAAVD,cAAc,IAAIE,IAAI;IAC3C,IAAI,CAACD,OAAO,OAAO;IACnB,MAAME,OAAOpB,QAAOiB,aAAAA,KAAKG,IAAI,YAATH,aAAa;IACjC,IAAI,CAAC,AAAC1C,aAAmC8C,QAAQ,CAACD,OAAO;QACvD,OAAO,CAAC,oBAAoB,EAAE7C,aAAa+C,IAAI,CAAC,OAAO;IACzD;IACA,MAAMC,QAAQvB,QAAOiB,cAAAA,KAAKM,KAAK,YAAVN,cAAc,IAAIE,IAAI;IAC3C,IAAI,CAACI,OAAO,OAAO;IACnB,IAAIA,MAAMC,MAAM,GAAG7C,wBAAwB;QACzC,OAAO,CAAC,qBAAqB,EAAEA,uBAAuB8C,cAAc,CAAC,SAAS,WAAW,CAAC;IAC5F;IACA,MAAMC,UAAUT,KAAKU,MAAM;IAC3B,MAAMA,SACJD,YAAYxB,aAAawB,YAAY,QAAQA,YAAY,KACrD,OACA,OAAOA,YAAY,YAAYb,SAASe,IAAI,CAACF,WAC3CA,UACAxB;IACR,IAAIyB,WAAWzB,WAAW,OAAO;IACjC,yEAAyE;IACzE,sDAAsD;IACtD,IAAIkB,SAAS,WAAW,CAACO,QAAQ,OAAO;IACxC,MAAME,YAAYZ,KAAKa,MAAM;IAC7B,MAAMA,SAAoD,CAAC;IAC3D,IAAID,cAAc3B,aAAa2B,cAAc,MAAM;QACjD,IAAI,OAAOA,cAAc,YAAYE,MAAMC,OAAO,CAACH,YAAY;YAC7D,OAAO;QACT;QACA,KAAK,MAAM,CAACrB,KAAKyB,MAAM,IAAIC,OAAOC,OAAO,CAACN,WAAuC;YAC/E,IACE,OAAOI,UAAU,YACjB,OAAOA,UAAU,YACjB,OAAOA,UAAU,WACjB;gBACA,OAAO,CAAC,OAAO,EAAEzB,IAAI,oCAAoC,CAAC;YAC5D;YACAsB,MAAM,CAACtB,IAAI,GAAGyB;QAChB;QACA,IAAIG,KAAKC,SAAS,CAACP,QAAQN,MAAM,GAAGb,uBAAuB;YACzD,OAAO;QACT;IACF;IACA,MAAM2B,WAAW,OAAOrB,KAAKsB,KAAK,KAAK,WAAWtB,KAAKsB,KAAK,CAACpB,IAAI,KAAK;IACtE,IAAImB,SAASd,MAAM,GAAGV,iBAAiB,OAAO;IAC9C,yEAAyE;IACzE,yEAAyE;IACzE,MAAMyB,QAAQD,YAAYA,aAAa,SAASA,WAAW;IAC3D,OAAO;QAAEpB;QAAOS;QAAQP,MAAMA;QAAmBG;QAAOO;QAAQS;IAAM;AACxE;AAEA,OAAO,eAAeC,KAAKC,OAAgB;cAiJRC;IAhJjC,IAAI1B;IACJ,IAAI;QACFA,UAAU,MAAMyB,QAAQE,IAAI;IAC9B,EAAE,eAAM;QACN3B,UAAU;IACZ;IACA,MAAM4B,SAAS7B,qBAAqBC;IACpC,sEAAsE;IACtE,uEAAuE;IACvE,8DAA8D;IAC9D,MAAME,QAAQ,OAAO0B,WAAW,WAAW5C,eAAQgB,2BAAD,AAACA,QAA4CE,KAAK,mBAAI,MAAM0B,OAAO1B,KAAK;IAC1H,2EAA2E;IAC3E,2EAA2E;IAC3E,gDAAgD;IAChD,MAAMS,SAAS,OAAOiB,WAAW,WAAW,OAAOA,OAAOjB,MAAM;IAChE,MAAMe,OAAO,MAAMlE,aACjB;QAAEiE;QAASvB;QAAOS;IAAO,GACzB;QACEkB,SAAS;QACTC,aAAa;QACbC,iBAAiB;QACjBC,YAAY;QACZC,WAAW1C;IACb;IAEF,IAAImC,gBAAgBQ,UAAU,OAAOR;IACrC,IAAI,OAAOE,WAAW,UAAU;QAC9B,wEAAwE;QACxE,qDAAqD;QACrD,MAAMzD,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER,OAAOgD,SAASP,IAAI,CAAC;YAAE9C,OAAO+C;QAAO,GAAG;YAAEU,QAAQ;QAAI;IACxD;IACA,sEAAsE;IACtE,uEAAuE;IACvE,sDAAsD;IACtD,IAAIC;IACJ,4EAA4E;IAC5E,kEAAkE;IAClE,IAAIC,mBAA2C;IAC/C,IAAI;YAGC;QAFH,2EAA2E;QAC3ED,WACG,QAAA,MAAM7E,iBAAiB;YACtByE,WAAWT,KAAKS,SAAS;YACzBM,KAAKf,KAAKe,GAAG;YACb9B,QAAQiB,OAAOjB,MAAM;QACvB,cAJC,QAKA,MAAMlD,sBAAsBmE,OAAOxB,IAAI,EAAE;YACxC+B,WAAWT,KAAKS,SAAS;YACzBjC,OAAOwB,KAAKxB,KAAK;YACjBS,QAAQiB,OAAOjB,MAAM;YACrBG,QAAQc,OAAOd,MAAM;YACrB2B,KAAKf,KAAKe,GAAG;YACbC,KAAKhB,KAAKgB,GAAG;YACbC,kBAAkB,CAAC3C,2BAAD,AAACA,OAA4C,CAAC,mBAAmB,MAAK;YACxF4C,oBAAoB,CAACC;gBACnBL,mBAAmBK;YACrB;QACF;IACJ,EAAE,OAAOhE,OAAO;QACd,MAAMV,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER4D,QAAQjE,KAAK,CAAC,2BAA2B;YAAEqB,OAAOwB,KAAKxB,KAAK;YAAEE,MAAMwB,OAAOxB,IAAI;YAAEvB;QAAM;QACvF,MAAMF,eAAe,2BAA2BE;QAChD,OAAOqD,SAASP,IAAI,CAAC;YAAE9C,OAAO;QAAkC,GAAG;YAAEyD,QAAQ;QAAI;IACnF;IACA,IAAIC,SAAS;QACX,MAAMpE,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER,uEAAuE;QACvE,0DAA0D;QAC1D,OAAOgD,SAASP,IAAI,CAClB;YAAE9C,OAAO0D,QAAQ1D,KAAK;WAAM0D,QAAQQ,IAAI,GAAG;YAAEA,MAAMR,QAAQQ,IAAI;QAAC,IAAI,CAAC,GAAQR,QAAQS,OAAO,GAAG;YAAEA,SAAST,QAAQS,OAAO;QAAC,IAAI,CAAC,IAC/H;YAAEV,QAAQC,QAAQD,MAAM;QAAC;IAE7B;IAEA,MAAMW,MAAM,IAAIC;IAChB,IAAIC;IACJ,IAAI;YAkBkBzB;QAjBpB,uEAAuE;QACvE,oEAAoE;QACpE,MAAMZ,SAAS,MAAMlD,oBAAoB8D,KAAKS,SAAS,EAAE;YACvD/B,MAAMwB,OAAOxB,IAAI;YACjBO,QAAQiB,OAAOjB,MAAM;YACrBG,QAAQc,OAAOd,MAAM;QACvB;QACAqC,UAAU,MAAMnF,YACd0D,KAAKS,SAAS,EACd;YACEjC,OAAOwB,KAAKxB,KAAK;YACjBS,QAAQiB,OAAOjB,MAAM;YACrBP,MAAMwB,OAAOxB,IAAI;YACjBG,OAAOqB,OAAOrB,KAAK;YACnBO;YACAS,OAAOK,OAAOL,KAAK;YACnB6B,WAAW1B,KAAKgB,GAAG;YACnBW,cAAc,GAAE3B,uBAAAA,KAAK4B,OAAO,CAACC,KAAK,YAAlB7B,uBAAsB;YACtCiB,kBAAkBtE,sBAAsBmE,kBAAkBd,KAAKgB,GAAG,EAAEO;QACtE,GACAA;IAEJ,EAAE,OAAOpE,OAAO;QACd,MAAMV,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER4D,QAAQjE,KAAK,CAAC,wBAAwB;YAAEqB,OAAOwB,KAAKxB,KAAK;YAAErB;QAAM;QACjE,MAAMF,eAAe,wBAAwBE;QAC7C,OAAOqD,SAASP,IAAI,CAAC;YAAE9C,OAAO;QAAkC,GAAG;YAAEyD,QAAQ;QAAI;IACnF;IACA,MAAMkB,QAAQL,QAAQM,GAAG;IAEzB,6EAA6E;IAC7E,4EAA4E;IAC5E,2EAA2E;IAC3E,sDAAsD;IACtD,IAAI3F,uBAAuBqF,WAAWtF,yBAAyB;QAC7D,MAAMM,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER,OAAOgD,SAASP,IAAI,CAClB;YACE+B,KAAK3F,aAAaoF,SAASF;YAC3BU,OAAOrF,aAAaoD,KAAKU,WAAW,EAAE;gBAAEwB,aAAa;YAAK;QAC5D,GACA;YAAEtB,QAAQ;QAAI;IAElB;IAEA,uEAAuE;IACvE,yEAAyE;IACzE,8DAA8D;IAC9D,MAAMuB,MAAM,MAAM3F,aAAawD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEsD,OAAO;QAChEM,OAAO,CAAC,MAAM,EAAEzG,cAAc;QAC9B4F;QACAb,aAAaV,KAAKU,WAAW;QAC7BK,KAAKf,KAAKe,GAAG;QACbsB,QAAQC,YAAYC,OAAO,CAACpG;QAC5B,oEAAoE;QACpE,uEAAuE;QACvEqG,OAAO;YAAExB,KAAKhB,KAAKgB,GAAG;YAAEa,KAAK,GAAE7B,sBAAAA,KAAK4B,OAAO,CAACC,KAAK,YAAlB7B,sBAAsB;QAAK;QAC1D,wEAAwE;QACxEyC,OAAOzC,KAAKyC,KAAK;IACnB;IACA,IAAIN,IAAIO,OAAO,KAAK,iBAAiB;QACnC,uEAAuE;QACvE,uEAAuE;QACvE,MAAMjG,qBAAqBuD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAEwB,KAAKU,WAAW,EAAEC,KAAK,CAC5E,IAAMnD;QAER4D,QAAQjE,KAAK,CAAC,mCAAmC;YAAEqB,OAAOwB,KAAKxB,KAAK;YAAEsD;QAAM;QAC5E,MAAM7E,eAAe,mCAAmC,IAAII,MAAM,CAAC,IAAI,EAAEyE,OAAO;QAChF,OAAOtB,SAASP,IAAI,CAAC;YAAE9C,OAAO;QAAkC,GAAG;YAAEyD,QAAQ;QAAI;IACnF;IACA,OAAOJ,SAASP,IAAI,CAClB;QACE+B,KAAK3F,aAAa8F,IAAIH,GAAG;QACzB,oEAAoE;QACpE,yCAAyC;QACzCC,OAAOrF,aAAaoD,KAAKU,WAAW,EAAE;YACpCwB,aAAaC,IAAIH,GAAG,CAACW,YAAY,GAAG,IAAIR,IAAIH,GAAG,CAACW,YAAY,GAAG;QACjE;IACF,GACA;QAAE/B,QAAQ;IAAI;AAElB;AAEA,OAAO,eAAegC,IAAI7C,OAAgB;QAED8C,uBAErBA,wBAOGA;IAVrB,MAAMA,MAAM,IAAIC,IAAI/C,QAAQ8C,GAAG;IAC/B,MAAM7C,OAAO,MAAMnD,WAAWkD,UAAS8C,wBAAAA,IAAIE,YAAY,CAACC,GAAG,CAAC,oBAArBH,wBAAiC;IACxE,IAAI7C,gBAAgBQ,UAAU,OAAOR;IACrC,MAAMiD,aAAYJ,yBAAAA,IAAIE,YAAY,CAACC,GAAG,CAAC,qBAArBH,yBAAkC;IACpD,MAAMjC,SAAS,AAAC1C,gBAAsCS,QAAQ,CAACsE,aAC1DA,YACDzF;IACJ,oEAAoE;IACpE,6DAA6D;IAC7D,MAAM0F,SAASD,cAAc;IAC7B,MAAMlF,QAAQoF,QAAON,yBAAAA,IAAIE,YAAY,CAACC,GAAG,CAAC,oBAArBH,yBAAiC;IACtD,wEAAwE;IACxE,yEAAyE;IACzE,4EAA4E;IAC5E,6CAA6C;IAC7C,MAAM7D,UAAU6D,IAAIE,YAAY,CAACC,GAAG,CAAC;IACrC,IAAIhE,YAAY,QAAQ,CAACb,SAASe,IAAI,CAACF,UAAU;QAC/C,OAAOwB,SAASP,IAAI,CAAC;YAAE9C,OAAO;QAA0B,GAAG;YAAEyD,QAAQ;QAAI;IAC3E;IACA,MAAMW,MAAM,IAAIC;IAChB,yEAAyE;IACzE,6DAA6D;IAC7D,MAAM4B,OAAO,MAAMtG,mBACjBkD,KAAKS,SAAS,EACd,MAAMlE,WAAWyD,KAAKS,SAAS,EAAET,KAAKxB,KAAK,EAAE,aACvCoC,SAAS;QAAEA;IAAO,IAAI,CAAC,GACvBsC,SAAS;QAAEG,UAAUtG;IAAuB,IAAI,CAAC,GACjDiC,UAAU;QAAEC,QAAQD;IAAQ,IAAI,CAAC,GACjCmE,OAAOG,QAAQ,CAACvF,UAAUA,QAAQ,IAAI;QAAEA;IAAM,IAAI,CAAC;IAG3D,MAAMwF,YAAY3H,qBAAqBoE,KAAKe,GAAG,MAAe;IAC9D,0EAA0E;IAC1E,4EAA4E;IAC5E,8EAA8E;IAC9E,MAAMyC,cAAcD,YAChB,MAAM7G,sBAAsBsD,KAAKS,SAAS,EAAE;QAAEjC,OAAOwB,KAAKxB,KAAK;QAAEuC,KAAKf,KAAKe,GAAG;QAAmBQ;IAAI,KACrG;IACJ,OAAOf,SAASP,IAAI,CAClB;QACEmD,MAAMA,KAAKK,GAAG,CAAC,CAACzB,MAAQ3F,aAAa2F,KAAKT;QAC1C,wEAAwE;QACxE,qEAAqE;QACrEgC;OACIC,cAAc;QAAEA;IAAY,IAAI,CAAC,IAEvC;QAAE5C,QAAQ;QAAK8C,SAAS;YAAE,iBAAiB;QAAW;IAAE;AAE5D;AAEA,OAAO,MAAMC,UAAU,gBAAe;AACtC,OAAO,MAAMC,cAAc,GAAE"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-jobs-route.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; the\n// GET climbs `ai-jobs-gate.ts`, which carries the same rung.\n\nimport { randomUUID } from 'crypto'\nimport { resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n AI_JOB_KINDS,\n type AiJobKind,\n type AiJobStatus,\n} from '../model/ai-jobs.types'\n// By their own entry points rather than the barrel (AGL-2903): the specs\n// stub the barrel closed-world, and the ladder, the machine and the runtime\n// are the things under test here, not things to be stubbed away.\nimport { aiGateLadder } from '../runtime/ai-gate'\nimport { aiJobAdmissionRefusal, aiJobSiteRefusal } from '../jobs/ai-job-admission'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { aiJobAdmittedInputs } from '../jobs/ai-job-auto-confirm'\nimport {\n AI_JOB_INLINE_BUDGET_MS,\n aiJobNextStepMinimumMs,\n aiJobSummary,\n createAiJob,\n listAiJobs,\n runAiJobStep,\n} from '../jobs/ai-jobs'\nimport { releaseAssistMessage } from '../usage/assist-usage'\nimport { readFreeAiCreditsLeft } from '../usage/free-ai-credits-left'\nimport { aiCreditsConfirmation, type AiCreditsPrompt } from '../model/ai-credit-estimate'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport { aiJobsGate } from './ai-jobs-gate'\nimport { withoutErasedSites } from './ai-jobs-live-sites'\nimport { AI_JOB_ACTIVE_STATUSES } from '../model/ai-job-activity'\nimport { reportServerError } from '@aglyn/tenant-data-admin/server/client-error-report'\n\n/**\n * A caught fault on this door, reported as well as logged (AGL-1921).\n *\n * `onRequestError` sees only an UNCAUGHT throw, and the Vercel log drain\n * forwards the status line but not a `console.error`, so the three 500s\n * this route answered on 2026-10-06 at 17:41–17:47Z reached the alert as\n * START/END/REPORT with no reason anywhere. Never throws.\n */\nasync function reportJobFault(what: string, error: unknown): Promise<void> {\n try {\n await reportServerError(\n {\n message: `${what}: ${error instanceof Error ? error.message : String(error)}`,\n stack: error instanceof Error ? error.stack : undefined,\n route: '/api/ai/jobs',\n routeType: 'route',\n method: 'POST',\n },\n { service: 'console-web' },\n )\n } catch {\n // The console line beside each call is the record of last resort.\n }\n}\n\n/**\n * AI generation jobs: create and list (AGL-2904).\n *\n * `POST` is a door that spends, so it climbs the whole ladder (AGL-2903) —\n * `aiGenerative`, `release_ai_generative`, the `ai-generate` switch, a\n * per-uid window, and a reservation — and then creates the job and runs\n * its first step INLINE under `AI_JOB_INLINE_BUDGET_MS`. A step that\n * finishes in time answers with the job `done` (or `queued` behind its\n * next step); one that does not is aborted, its reservation handed back,\n * and the job answers `queued` for the beat to resume. Either way the\n * caller gets the job document, and the events route is how it watches\n * from there.\n *\n * `POST` with `check: true` climbs the same ladder and asks the same\n * admission, then hands the reservation back and creates nothing: a 200\n * with `ok` true, or the refusal the create would have answered. \"Create site\"\n * asks it before it makes the site a guided start builds on (AGL-3859), so\n * a start that would be refused, or that asks for the Free go-ahead, leaves\n * no empty site behind. A checked job may name no site yet.\n *\n * `GET` lists the org's jobs, newest first, through the read gate: the\n * same rungs up to the lockdown verdict, no reservation. A job naming a\n * site that no longer exists is left out, so a page can come back shorter\n * than its `limit`.\n *\n * The request must NAME the org it is metered against (AGL-1934). There\n * is no fallback to \"the caller's first org\".\n */\n\nconst AI_JOBS_RATE_LIMIT = { key: 'ai-jobs', limit: 10, windowMs: 60_000 }\n\n/** Kind-specific inputs, bounded: a runner reads scalars, not a document. */\nconst MAX_INPUTS_JSON_CHARS = 8_000\n\n/** The admitted statuses for the list filter, as the union spells them. */\nconst AI_JOB_STATUSES: readonly AiJobStatus[] = [\n 'queued',\n 'running',\n 'needs_input',\n 'needs_review',\n 'done',\n 'failed',\n 'canceled',\n]\n\nexport interface CreateAiJobBody {\n orgId: string\n hostId: string | null\n kind: AiJobKind\n brief: string\n inputs: Record<string, string | number | boolean>\n /** A catalog model the creator picked (AGL-2942), or `null` for Auto. */\n model: string | null\n}\n\nconst ID_CHARS = /^[A-Za-z0-9_-]{1,100}$/\n\n/** The longest model id a body may carry; the catalog's ids are far shorter. */\nconst MAX_MODEL_CHARS = 100\n\n/** Validate + clamp the request body; a string names what is wrong. */\nexport function parseCreateAiJobBody(payload: unknown): CreateAiJobBody | string {\n const body = (payload ?? {}) as Record<string, unknown>\n const orgId = String(body.orgId ?? '').trim()\n if (!orgId) return 'Open a workspace before using this feature'\n const kind = String(body.kind ?? '')\n if (!(AI_JOB_KINDS as readonly string[]).includes(kind)) {\n return `kind must be one of ${AI_JOB_KINDS.join(', ')}`\n }\n const brief = String(body.brief ?? '').trim()\n if (!brief) return 'Write a brief for the job'\n if (brief.length > AI_JOB_BRIEF_MAX_CHARS) {\n return `Keep the brief under ${AI_JOB_BRIEF_MAX_CHARS.toLocaleString('en-US')} characters`\n }\n const rawHost = body.hostId\n const hostId =\n rawHost === undefined || rawHost === null || rawHost === ''\n ? null\n : typeof rawHost === 'string' && ID_CHARS.test(rawHost)\n ? rawHost\n : undefined\n if (hostId === undefined) return 'hostId is not a site id'\n // A theme is fields on one site's document, so a theme job that names no\n // site has nothing to propose a change to (AGL-2938).\n if (kind === 'theme' && !hostId) return 'Open a site before changing its theme'\n const rawInputs = body.inputs\n const inputs: Record<string, string | number | boolean> = {}\n if (rawInputs !== undefined && rawInputs !== null) {\n if (typeof rawInputs !== 'object' || Array.isArray(rawInputs)) {\n return 'inputs must be an object'\n }\n for (const [key, value] of Object.entries(rawInputs as Record<string, unknown>)) {\n if (\n typeof value !== 'string' &&\n typeof value !== 'number' &&\n typeof value !== 'boolean'\n ) {\n return `inputs.${key} must be a string, number or boolean`\n }\n inputs[key] = value\n }\n if (JSON.stringify(inputs).length > MAX_INPUTS_JSON_CHARS) {\n return 'inputs are too large'\n }\n }\n const rawModel = typeof body.model === 'string' ? body.model.trim() : ''\n if (rawModel.length > MAX_MODEL_CHARS) return 'model is not a model id'\n // Validated against the plan and the allotment allowlists when each step\n // runs, not here: the allowlists are read inside the step's reservation.\n const model = rawModel && rawModel !== 'auto' ? rawModel : null\n return { orgId, hostId, kind: kind as AiJobKind, brief, inputs, model }\n}\n\nexport async function POST(request: Request): Promise<Response> {\n let payload: unknown\n try {\n payload = await request.json()\n } catch {\n payload = null\n }\n const parsed = parseCreateAiJobBody(payload)\n // The org is parsed BEFORE the ladder so a request that named none is\n // refused with the ladder's 400 rather than a shape error; every other\n // shape fault waits until the caller has proven who they are.\n const orgId = typeof parsed === 'string' ? String((payload as Record<string, unknown> | null)?.orgId ?? '') : parsed.orgId\n // The site rides along for the same reason: a collaborator's `ai.generate`\n // is decided per site (AGL-2927), so the ladder needs it before it can say\n // whether this member may generate here at all.\n const hostId = typeof parsed === 'string' ? null : parsed.hostId\n const gate = await aiGateLadder(\n { request, orgId, hostId },\n {\n feature: 'aiGenerative',\n releaseFlag: 'release_ai_generative',\n lockdownFeature: 'ai-generate',\n permission: 'ai.generate',\n rateLimit: AI_JOBS_RATE_LIMIT,\n },\n )\n if (gate instanceof Response) return gate\n if (typeof parsed === 'string') {\n // Admitted, reserved, and then found to have sent nothing runnable: the\n // reservation goes back, because no token was spent.\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n return Response.json({ error: parsed }, { status: 400 })\n }\n // What only this kind can say about the request (AGL-2909): a site it\n // needs, inputs it reads, an allowance its draft counts against. Asked\n // before the job exists, so a refusal spends nothing.\n let refusal: Awaited<ReturnType<typeof aiJobAdmissionRefusal>>\n // A Free start past what is left, which the person chose to build as far as\n // it goes (AGL-3722): the prompt they confirmed, kept on the job.\n let confirmedCredits: AiCreditsPrompt | null = null\n // Asked before the site exists (AGL-3859): answers, creates nothing.\n const check = (payload as Record<string, unknown> | null)?.['check'] === true\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: parsed.hostId,\n })) ??\n (await aiJobAdmissionRefusal(parsed.kind, {\n firestore: gate.firestore,\n orgId: gate.orgId,\n hostId: parsed.hostId,\n siteToCome: check && !parsed.hostId,\n inputs: parsed.inputs,\n org: gate.org,\n uid: gate.uid,\n creditsConfirmed: (payload as Record<string, unknown> | null)?.['creditsConfirmed'] === true,\n onCreditsConfirmed: (prompt) => {\n confirmedCredits = prompt\n },\n }))\n } catch (error) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job admission failed', { orgId: gate.orgId, kind: parsed.kind, error })\n await reportJobFault('ai job admission failed', error)\n return Response.json({ error: 'The AI job could not be created' }, { status: 500 })\n }\n if (refusal) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n // A Free start past what is left asks before it starts (AGL-3722): the\n // prompt rides the 409, so the dialog offers its choices.\n return Response.json(\n { error: refusal.error, ...(refusal.code ? { code: refusal.code } : {}), ...(refusal.credits ? { credits: refusal.credits } : {}) },\n { status: refusal.status },\n )\n }\n if (check) {\n // Admitted, and nothing to run: the reservation goes back.\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n return Response.json({ ok: true }, { status: 200 })\n }\n\n const now = new Date()\n let created: Awaited<ReturnType<typeof createAiJob>>\n try {\n // The guided start's own plan confirmation (AGL-3594), kept only for a\n // site job on a site that has published nothing of the owner's yet.\n const inputs = await aiJobAdmittedInputs(gate.firestore, {\n kind: parsed.kind,\n hostId: parsed.hostId,\n inputs: parsed.inputs,\n })\n created = await createAiJob(\n gate.firestore,\n {\n orgId: gate.orgId,\n hostId: parsed.hostId,\n kind: parsed.kind,\n brief: parsed.brief,\n inputs,\n model: parsed.model,\n createdBy: gate.uid,\n createdByEmail: gate.decoded.email ?? null,\n creditsConfirmed: aiCreditsConfirmation(confirmedCredits, gate.uid, now),\n },\n now,\n )\n } catch (error) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job create failed', { orgId: gate.orgId, error })\n await reportJobFault('ai job create failed', error)\n return Response.json({ error: 'The AI job could not be created' }, { status: 500 })\n }\n const jobId = created.$id\n\n // A first 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. Nothing ran, so the reservation goes back.\n if (aiJobNextStepMinimumMs(created) > AI_JOB_INLINE_BUDGET_MS) {\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n return Response.json(\n {\n job: aiJobSummary(created, now),\n meter: aiUsageMeter(gate.reservation, { lastCredits: null }),\n },\n { status: 200 },\n )\n }\n\n // The first step, inline, on the reservation the ladder already holds.\n // The runner hands that reservation back itself if the provider is never\n // reached, and re-queues the step when the budget ends first.\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 // The caller is on the request, so what the inline step produces is\n // attributed with their address; the beat's steps carry the uid alone.\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 // A job created a moment ago has a claimable first step; anything else\n // is a fault in the machine, and the reservation must not be stranded.\n await releaseAssistMessage(gate.firestore, gate.orgId, gate.reservation).catch(\n () => undefined,\n )\n console.error('ai job first step not claimable', { orgId: gate.orgId, jobId })\n await reportJobFault('ai job first step not claimable', new Error(`job ${jobId}`))\n return Response.json({ error: 'The AI job could not be started' }, { status: 500 })\n }\n return Response.json(\n {\n job: aiJobSummary(run.job),\n // The usage strip's envelope (AGL-2942): the reservation the ladder\n // took, with what the inline step spent.\n meter: aiUsageMeter(gate.reservation, {\n lastCredits: run.job.creditsSpent > 0 ? run.job.creditsSpent : null,\n }),\n },\n { status: 200 },\n )\n}\n\nexport async function GET(request: Request): Promise<Response> {\n const url = new URL(request.url)\n const gate = await aiJobsGate(request, url.searchParams.get('orgId') ?? '')\n if (gate instanceof Response) return gate\n const rawStatus = url.searchParams.get('status') ?? ''\n const status = (AI_JOB_STATUSES as readonly string[]).includes(rawStatus)\n ? (rawStatus as AiJobStatus)\n : undefined\n // `active` (AGL-3593): every job not yet settled, which is what the\n // console's top-bar indicator and the Assist launcher count.\n const active = rawStatus === 'active'\n const limit = Number(url.searchParams.get('limit') ?? '')\n // `hostId` (AGL-3596): one site's jobs, for the site's AI jobs page — a\n // query on the field under the (hostId, createdAt) index, never a filter\n // of a loaded window. An id that is not one is refused rather than ignored,\n // which would answer with every site's jobs.\n const rawHost = url.searchParams.get('hostId')\n if (rawHost !== null && !ID_CHARS.test(rawHost)) {\n return Response.json({ error: 'hostId is not a site id' }, { status: 400 })\n }\n const now = new Date()\n // A job naming a site that has been deleted is not listed (AGL-3596), so\n // no surface counts or shows work on a site nobody can open.\n const jobs = await withoutErasedSites(\n gate.firestore,\n await listAiJobs(gate.firestore, gate.orgId, {\n ...(status ? { status } : {}),\n ...(active ? { statuses: AI_JOB_ACTIVE_STATUSES } : {}),\n ...(rawHost ? { hostId: rawHost } : {}),\n ...(Number.isFinite(limit) && limit > 0 ? { limit } : {}),\n }),\n )\n const freeTaste = resolveEffectivePlan(gate.org as never) === 'free'\n // What the Free workspace has left this month (AGL-3660): the less of its\n // own band and its owner's allowance across their Free workspaces, which is\n // what the steps' reservations refuse at — never the 300 a month starts with.\n const freeCredits = freeTaste\n ? await readFreeAiCreditsLeft(gate.firestore, { orgId: gate.orgId, org: gate.org as object | null, now })\n : null\n return Response.json(\n {\n jobs: jobs.map((job) => aiJobSummary(job, now)),\n // Whether the workspace spends the Free taste (AGL-3594), so the guided\n // start offers the Free page band from the request it already makes.\n freeTaste,\n ...(freeCredits ? { freeCredits } : {}),\n },\n { status: 200, headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\nexport const dynamic = 'force-dynamic'\nexport const maxDuration = 60\n"],"names":["randomUUID","resolveEffectivePlan","AI_JOB_KINDS","aiGateLadder","aiJobAdmissionRefusal","aiJobSiteRefusal","AI_JOB_BRIEF_MAX_CHARS","aiJobAdmittedInputs","AI_JOB_INLINE_BUDGET_MS","aiJobNextStepMinimumMs","aiJobSummary","createAiJob","listAiJobs","runAiJobStep","releaseAssistMessage","readFreeAiCreditsLeft","aiCreditsConfirmation","aiUsageMeter","aiJobsGate","withoutErasedSites","AI_JOB_ACTIVE_STATUSES","reportServerError","reportJobFault","what","error","message","Error","String","stack","undefined","route","routeType","method","service","AI_JOBS_RATE_LIMIT","key","limit","windowMs","MAX_INPUTS_JSON_CHARS","AI_JOB_STATUSES","ID_CHARS","MAX_MODEL_CHARS","parseCreateAiJobBody","payload","body","orgId","trim","kind","includes","join","brief","length","toLocaleString","rawHost","hostId","test","rawInputs","inputs","Array","isArray","value","Object","entries","JSON","stringify","rawModel","model","POST","request","gate","json","parsed","feature","releaseFlag","lockdownFeature","permission","rateLimit","Response","firestore","reservation","catch","status","refusal","confirmedCredits","check","org","siteToCome","uid","creditsConfirmed","onCreditsConfirmed","prompt","console","code","credits","ok","now","Date","created","createdBy","createdByEmail","decoded","email","jobId","$id","job","meter","lastCredits","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","creditsSpent","GET","url","URL","searchParams","get","rawStatus","active","Number","jobs","statuses","isFinite","freeTaste","freeCredits","map","headers","dynamic","maxDuration"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -52,6 +52,7 @@ import { POST as cancelAiJob } from "./server/ai-jobs-cancel.js";
52
52
  import { GET as aiJobEvents } from "./server/ai-jobs-events-route.js";
53
53
  import { POST as createAiSiteBatch } from "./server/ai-jobs-batch.js";
54
54
  import { GET as listAiJobs, POST as createAiJob } from "./server/ai-jobs-route.js";
55
+ import { GET as aiCreateDoor, GET_FOLLOW as aiCreateFollow } from "./server/ai-create-route.js";
55
56
  import { POST as resumeAiJob } from "./server/ai-jobs-resume.js";
56
57
  import { POST as aiGenerateComponent } from "./server/ai-generate-component.js";
57
58
  import { POST as createAiMediaImages } from "./server/ai-media-image.js";
@@ -241,6 +242,19 @@ export * from "./runtime/ai-runtime.js";
241
242
  })
242
243
  })
243
244
  });
245
+ // Create with AI in the apps (AGL-3854): whether a list draws the entry and
246
+ // every word its brief says, and the job it started as the dialog's follow
247
+ // card reads it. Both read; the jobs doors above start, confirm and cancel.
248
+ registerPluginApiRoute('ai/create', {
249
+ web: aiCreateDoor
250
+ });
251
+ registerPluginApiRoute('ai/create/:jobId', {
252
+ web: (request, context)=>aiCreateFollow(request, {
253
+ params: Promise.resolve({
254
+ jobId: String(context.params['jobId'])
255
+ })
256
+ })
257
+ });
244
258
  registerPluginApiRoute('ai/jobs/:jobId/events', {
245
259
  web: (request, context)=>aiJobEvents(request, {
246
260
  params: Promise.resolve({