@aglyn/plugins-ai 1.0.0-beta.288 → 1.0.0-beta.290
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/activity/ai-activity-actions.d.ts +3 -2
- package/src/lib/activity/ai-activity-actions.js +3 -0
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +2 -0
- package/src/lib/components/ai-job-requests.js +2 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +4 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.d.ts +6 -1
- package/src/lib/components/ai-site-import-job.component.js +124 -17
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.d.ts +6 -0
- package/src/lib/components/ai-site-import-start.component.js +18 -4
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +2 -1
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +20 -2
- package/src/lib/jobs/ai-image-reads.js +47 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +53 -3
- package/src/lib/jobs/ai-job-import-step.js +386 -64
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +33 -0
- package/src/lib/jobs/ai-job-layout-language.js +75 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +8 -3
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +29 -0
- package/src/lib/jobs/ai-job-page-language.js +178 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +3 -0
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-site-import-document.d.ts +117 -0
- package/src/lib/jobs/ai-site-import-document.js +710 -0
- package/src/lib/jobs/ai-site-import-document.js.map +1 -0
- package/src/lib/jobs/ai-site-import-look.d.ts +112 -0
- package/src/lib/jobs/ai-site-import-look.js +402 -0
- package/src/lib/jobs/ai-site-import-look.js.map +1 -0
- package/src/lib/jobs/ai-site-import-publish.d.ts +41 -0
- package/src/lib/jobs/ai-site-import-publish.js +92 -0
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -0
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +27 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js +37 -8
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +17 -0
- package/src/lib/layout-language/ai-layout-frame.js +73 -2
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +43 -0
- package/src/lib/layout-language/ai-layout-language.js +199 -0
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +15 -0
- package/src/lib/model/ai-credit-estimate.js +14 -1
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +13 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +47 -3
- package/src/lib/model/ai-site-import-view.js +122 -20
- package/src/lib/model/ai-site-import-view.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +232 -7
- package/src/lib/model/ai-site-import.js +365 -35
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +18 -0
- package/src/lib/runtime/ai-doctrine-validators.js +16 -5
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +31 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.js +24 -1
- package/src/lib/server/ai-image-reads-route.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js +7 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/site-import/fixtures/aglyn-com-recordings.d.ts +57 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js +172 -0
- package/src/lib/site-import/fixtures/aglyn-com-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +32 -7
- package/src/lib/site-import/site-import-crawl.js +133 -44
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-edge-server.d.ts +31 -0
- package/src/lib/site-import/site-import-edge-server.js +31 -0
- package/src/lib/site-import/site-import-edge-server.js.map +1 -0
- package/src/lib/site-import/site-import-edge.d.ts +44 -0
- package/src/lib/site-import/site-import-edge.js +108 -0
- package/src/lib/site-import/site-import-edge.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +73 -3
- package/src/lib/site-import/site-import-html.js +332 -45
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-images.d.ts +22 -0
- package/src/lib/site-import/site-import-images.js +78 -2
- package/src/lib/site-import/site-import-images.js.map +1 -1
- package/src/lib/site-import/site-import-page.d.ts +44 -2
- package/src/lib/site-import/site-import-page.js +116 -16
- package/src/lib/site-import/site-import-page.js.map +1 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
17
|
-
import { AI_IMAGE_READS_SIGNATURE, verifyAiImageReadsRequest } from "../jobs/ai-image-reads.js";
|
|
17
|
+
import { AI_IMAGE_READS_SIGNATURE, AI_LOGO_RASTER_MAX_BYTES, rasterizeAiLogoSvg, verifyAiImageReadsRequest } from "../jobs/ai-image-reads.js";
|
|
18
18
|
import { aiMediaAssetLocation } from "../jobs/ai-media-asset.js";
|
|
19
19
|
import { readAiProductImageAt } from "../jobs/ai-product-image.js";
|
|
20
20
|
import { readAiThemeLogoColorsAt } from "../jobs/ai-theme-brand-inputs.js";
|
|
@@ -28,6 +28,8 @@ import { readAiThemeLogoColorsAt } from "../jobs/ai-theme-brand-inputs.js";
|
|
|
28
28
|
* it, or why it was not read
|
|
29
29
|
* {op: 'logo-colors', ref, hostId, orgId} `{colors}`: the site logo's
|
|
30
30
|
* dominant colors, or null
|
|
31
|
+
* {op: 'logo-raster', svg, height} `{png}`: a site import's SVG
|
|
32
|
+
* logo drawn as a PNG (AGL-4084)
|
|
31
33
|
*
|
|
32
34
|
* The asset is located again here from its reference, the site and the org,
|
|
33
35
|
* under the rule the step used (`aiMediaAssetLocation`): one that is not the
|
|
@@ -69,6 +71,27 @@ export async function aiImageReadsRoute(request) {
|
|
|
69
71
|
error: 'Bad request'
|
|
70
72
|
}, 400);
|
|
71
73
|
const op = body['op'];
|
|
74
|
+
// A site import's SVG logo, drawn as a PNG (AGL-4084): the drawing is in the body, nothing is fetched.
|
|
75
|
+
if (op === 'logo-raster') {
|
|
76
|
+
const svg = typeof body['svg'] === 'string' ? Buffer.from(body['svg'], 'base64') : null;
|
|
77
|
+
const height = Number(body['height']);
|
|
78
|
+
if (!svg || svg.byteLength > AI_LOGO_RASTER_MAX_BYTES || !Number.isFinite(height)) return json({
|
|
79
|
+
error: 'Bad request'
|
|
80
|
+
}, 400);
|
|
81
|
+
try {
|
|
82
|
+
const png = await rasterizeAiLogoSvg(new Uint8Array(svg), height);
|
|
83
|
+
return json({
|
|
84
|
+
png: png ? Buffer.from(png).toString('base64') : null
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('[ai-image-reads]', op, {
|
|
88
|
+
error
|
|
89
|
+
});
|
|
90
|
+
return json({
|
|
91
|
+
error: 'The logo could not be drawn.'
|
|
92
|
+
}, 500);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
72
95
|
if (op !== 'product-photo' && op !== 'logo-colors') return json({
|
|
73
96
|
error: 'Unknown op'
|
|
74
97
|
}, 400);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-image-reads-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\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { AI_IMAGE_READS_SIGNATURE, verifyAiImageReadsRequest } from '../jobs/ai-image-reads'\nimport { aiMediaAssetLocation } from '../jobs/ai-media-asset'\nimport { readAiProductImageAt } from '../jobs/ai-product-image'\nimport { readAiThemeLogoColorsAt } from '../jobs/ai-theme-brand-inputs'\n\n/*==========================================\n * `POST /api/plugin-images/ai/image-reads` (AGL-4048): a job step's image\n * reads, in the one function that ships `sharp`'s native library (the\n * console's plugins' image function; see `jobs/ai-image-reads.ts` for why).\n *\n * {op: 'product-photo', ref, hostId, orgId} `{read}`: the product photo\n * as the products step sends\n * it, or why it was not read\n * {op: 'logo-colors', ref, hostId, orgId} `{colors}`: the site logo's\n * dominant colors, or null\n *\n * The asset is located again here from its reference, the site and the org,\n * under the rule the step used (`aiMediaAssetLocation`): one that is not the\n * site's own library's or its org's is not read. The read is the media\n * library's own (`readAiMediaAssetBytes`), so nothing is fetched from a URL.\n *\n * A machine's door: no session, a signature over the moment and the body\n * (401 without one). A read that fails is a 500, and the step treats it as\n * the unreadable photo or logo it would have been in its own process.\n *=========================================*/\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\nfunction json(body: unknown, status = 200): Response {\n return Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } })\n}\n\nexport async function aiImageReadsRoute(request: Request): Promise<Response> {\n if (request.method !== 'POST') return json({ error: 'Method not allowed' }, 405)\n const raw = await request.text()\n if (!verifyAiImageReadsRequest(request.headers.get(AI_IMAGE_READS_SIGNATURE), raw)) {\n return json({ error: 'Unauthenticated' }, 401)\n }\n let body: Record<string, unknown>\n try {\n body = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return json({ error: 'Bad request' }, 400)\n }\n if (!body || typeof body !== 'object') return json({ error: 'Bad request' }, 400)\n const op = body['op']\n if (op !== 'product-photo' && op !== 'logo-colors') return json({ error: 'Unknown op' }, 400)\n const hostId = String(body['hostId'] ?? '')\n const orgId = String(body['orgId'] ?? '')\n if (!DOCUMENT_ID.test(hostId) || (orgId !== '' && !DOCUMENT_ID.test(orgId))) {\n return json({ error: 'Bad request' }, 400)\n }\n const location = aiMediaAssetLocation(body['ref'], hostId, orgId)\n try {\n const firestore = firebaseAdmin.app().firestore()\n if (op === 'product-photo') {\n return json({\n read: location ? await readAiProductImageAt(firestore, location, hostId) : { status: 'not-in-library' },\n })\n }\n return json({ colors: location ? await readAiThemeLogoColorsAt(firestore, location, hostId) : null })\n } catch (error) {\n console.error('[ai-image-reads]', op, { hostId, error })\n return json({ error: 'The image could not be read.' }, 500)\n }\n}\n"],"names":["firebaseAdmin","AI_IMAGE_READS_SIGNATURE","verifyAiImageReadsRequest","aiMediaAssetLocation","readAiProductImageAt","readAiThemeLogoColorsAt","DOCUMENT_ID","json","body","status","Response","headers","aiImageReadsRoute","request","method","error","raw","text","get","JSON","parse","op","hostId","String","orgId","test","location","firestore","app","read","colors"
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/ai-image-reads-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\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { AI_IMAGE_READS_SIGNATURE, AI_LOGO_RASTER_MAX_BYTES, rasterizeAiLogoSvg, verifyAiImageReadsRequest } from '../jobs/ai-image-reads'\nimport { aiMediaAssetLocation } from '../jobs/ai-media-asset'\nimport { readAiProductImageAt } from '../jobs/ai-product-image'\nimport { readAiThemeLogoColorsAt } from '../jobs/ai-theme-brand-inputs'\n\n/*==========================================\n * `POST /api/plugin-images/ai/image-reads` (AGL-4048): a job step's image\n * reads, in the one function that ships `sharp`'s native library (the\n * console's plugins' image function; see `jobs/ai-image-reads.ts` for why).\n *\n * {op: 'product-photo', ref, hostId, orgId} `{read}`: the product photo\n * as the products step sends\n * it, or why it was not read\n * {op: 'logo-colors', ref, hostId, orgId} `{colors}`: the site logo's\n * dominant colors, or null\n * {op: 'logo-raster', svg, height} `{png}`: a site import's SVG\n * logo drawn as a PNG (AGL-4084)\n *\n * The asset is located again here from its reference, the site and the org,\n * under the rule the step used (`aiMediaAssetLocation`): one that is not the\n * site's own library's or its org's is not read. The read is the media\n * library's own (`readAiMediaAssetBytes`), so nothing is fetched from a URL.\n *\n * A machine's door: no session, a signature over the moment and the body\n * (401 without one). A read that fails is a 500, and the step treats it as\n * the unreadable photo or logo it would have been in its own process.\n *=========================================*/\n\nconst DOCUMENT_ID = /^[A-Za-z0-9_-]{1,128}$/\n\nfunction json(body: unknown, status = 200): Response {\n return Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } })\n}\n\nexport async function aiImageReadsRoute(request: Request): Promise<Response> {\n if (request.method !== 'POST') return json({ error: 'Method not allowed' }, 405)\n const raw = await request.text()\n if (!verifyAiImageReadsRequest(request.headers.get(AI_IMAGE_READS_SIGNATURE), raw)) {\n return json({ error: 'Unauthenticated' }, 401)\n }\n let body: Record<string, unknown>\n try {\n body = JSON.parse(raw) as Record<string, unknown>\n } catch {\n return json({ error: 'Bad request' }, 400)\n }\n if (!body || typeof body !== 'object') return json({ error: 'Bad request' }, 400)\n const op = body['op']\n // A site import's SVG logo, drawn as a PNG (AGL-4084): the drawing is in the body, nothing is fetched.\n if (op === 'logo-raster') {\n const svg = typeof body['svg'] === 'string' ? Buffer.from(body['svg'], 'base64') : null\n const height = Number(body['height'])\n if (!svg || svg.byteLength > AI_LOGO_RASTER_MAX_BYTES || !Number.isFinite(height)) return json({ error: 'Bad request' }, 400)\n try {\n const png = await rasterizeAiLogoSvg(new Uint8Array(svg), height)\n return json({ png: png ? Buffer.from(png).toString('base64') : null })\n } catch (error) {\n console.error('[ai-image-reads]', op, { error })\n return json({ error: 'The logo could not be drawn.' }, 500)\n }\n }\n if (op !== 'product-photo' && op !== 'logo-colors') return json({ error: 'Unknown op' }, 400)\n const hostId = String(body['hostId'] ?? '')\n const orgId = String(body['orgId'] ?? '')\n if (!DOCUMENT_ID.test(hostId) || (orgId !== '' && !DOCUMENT_ID.test(orgId))) {\n return json({ error: 'Bad request' }, 400)\n }\n const location = aiMediaAssetLocation(body['ref'], hostId, orgId)\n try {\n const firestore = firebaseAdmin.app().firestore()\n if (op === 'product-photo') {\n return json({\n read: location ? await readAiProductImageAt(firestore, location, hostId) : { status: 'not-in-library' },\n })\n }\n return json({ colors: location ? await readAiThemeLogoColorsAt(firestore, location, hostId) : null })\n } catch (error) {\n console.error('[ai-image-reads]', op, { hostId, error })\n return json({ error: 'The image could not be read.' }, 500)\n }\n}\n"],"names":["firebaseAdmin","AI_IMAGE_READS_SIGNATURE","AI_LOGO_RASTER_MAX_BYTES","rasterizeAiLogoSvg","verifyAiImageReadsRequest","aiMediaAssetLocation","readAiProductImageAt","readAiThemeLogoColorsAt","DOCUMENT_ID","json","body","status","Response","headers","aiImageReadsRoute","request","method","error","raw","text","get","JSON","parse","op","svg","Buffer","from","height","Number","byteLength","isFinite","png","Uint8Array","toString","console","hostId","String","orgId","test","location","firestore","app","read","colors"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,wBAAwB,EAAEC,wBAAwB,EAAEC,kBAAkB,EAAEC,yBAAyB,QAAQ,4BAAwB;AAC1I,SAASC,oBAAoB,QAAQ,4BAAwB;AAC7D,SAASC,oBAAoB,QAAQ,8BAA0B;AAC/D,SAASC,uBAAuB,QAAQ,mCAA+B;AAEvE;;;;;;;;;;;;;;;;;;;;;2CAqB2C,GAE3C,MAAMC,cAAc;AAEpB,SAASC,KAAKC,IAAa,EAAEC,SAAS,GAAG;IACvC,OAAOC,SAASH,IAAI,CAACC,MAAM;QAAEC;QAAQE,SAAS;YAAE,iBAAiB;QAAW;IAAE;AAChF;AAEA,OAAO,eAAeC,kBAAkBC,OAAgB;QA4BhCL,cACDA;IA5BrB,IAAIK,QAAQC,MAAM,KAAK,QAAQ,OAAOP,KAAK;QAAEQ,OAAO;IAAqB,GAAG;IAC5E,MAAMC,MAAM,MAAMH,QAAQI,IAAI;IAC9B,IAAI,CAACf,0BAA0BW,QAAQF,OAAO,CAACO,GAAG,CAACnB,2BAA2BiB,MAAM;QAClF,OAAOT,KAAK;YAAEQ,OAAO;QAAkB,GAAG;IAC5C;IACA,IAAIP;IACJ,IAAI;QACFA,OAAOW,KAAKC,KAAK,CAACJ;IACpB,EAAE,eAAM;QACN,OAAOT,KAAK;YAAEQ,OAAO;QAAc,GAAG;IACxC;IACA,IAAI,CAACP,QAAQ,OAAOA,SAAS,UAAU,OAAOD,KAAK;QAAEQ,OAAO;IAAc,GAAG;IAC7E,MAAMM,KAAKb,IAAI,CAAC,KAAK;IACrB,uGAAuG;IACvG,IAAIa,OAAO,eAAe;QACxB,MAAMC,MAAM,OAAOd,IAAI,CAAC,MAAM,KAAK,WAAWe,OAAOC,IAAI,CAAChB,IAAI,CAAC,MAAM,EAAE,YAAY;QACnF,MAAMiB,SAASC,OAAOlB,IAAI,CAAC,SAAS;QACpC,IAAI,CAACc,OAAOA,IAAIK,UAAU,GAAG3B,4BAA4B,CAAC0B,OAAOE,QAAQ,CAACH,SAAS,OAAOlB,KAAK;YAAEQ,OAAO;QAAc,GAAG;QACzH,IAAI;YACF,MAAMc,MAAM,MAAM5B,mBAAmB,IAAI6B,WAAWR,MAAMG;YAC1D,OAAOlB,KAAK;gBAAEsB,KAAKA,MAAMN,OAAOC,IAAI,CAACK,KAAKE,QAAQ,CAAC,YAAY;YAAK;QACtE,EAAE,OAAOhB,OAAO;YACdiB,QAAQjB,KAAK,CAAC,oBAAoBM,IAAI;gBAAEN;YAAM;YAC9C,OAAOR,KAAK;gBAAEQ,OAAO;YAA+B,GAAG;QACzD;IACF;IACA,IAAIM,OAAO,mBAAmBA,OAAO,eAAe,OAAOd,KAAK;QAAEQ,OAAO;IAAa,GAAG;IACzF,MAAMkB,SAASC,QAAO1B,eAAAA,IAAI,CAAC,SAAS,YAAdA,eAAkB;IACxC,MAAM2B,QAAQD,QAAO1B,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB;IACtC,IAAI,CAACF,YAAY8B,IAAI,CAACH,WAAYE,UAAU,MAAM,CAAC7B,YAAY8B,IAAI,CAACD,QAAS;QAC3E,OAAO5B,KAAK;YAAEQ,OAAO;QAAc,GAAG;IACxC;IACA,MAAMsB,WAAWlC,qBAAqBK,IAAI,CAAC,MAAM,EAAEyB,QAAQE;IAC3D,IAAI;QACF,MAAMG,YAAYxC,cAAcyC,GAAG,GAAGD,SAAS;QAC/C,IAAIjB,OAAO,iBAAiB;YAC1B,OAAOd,KAAK;gBACViC,MAAMH,WAAW,MAAMjC,qBAAqBkC,WAAWD,UAAUJ,UAAU;oBAAExB,QAAQ;gBAAiB;YACxG;QACF;QACA,OAAOF,KAAK;YAAEkC,QAAQJ,WAAW,MAAMhC,wBAAwBiC,WAAWD,UAAUJ,UAAU;QAAK;IACrG,EAAE,OAAOlB,OAAO;QACdiB,QAAQjB,KAAK,CAAC,oBAAoBM,IAAI;YAAEY;YAAQlB;QAAM;QACtD,OAAOR,KAAK;YAAEQ,OAAO;QAA+B,GAAG;IACzD;AACF"}
|
|
@@ -18,7 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
// The POST climbs `aiGateLadder`, whose lockdown rung is the verdict.
|
|
19
19
|
import { randomUUID } from "crypto";
|
|
20
20
|
import { aiJobAdmissionRefusal, aiJobSiteRefusal } from "../jobs/ai-job-admission.js";
|
|
21
|
-
import { AI_JOB_INLINE_BUDGET_MS, aiJobNextStepMinimumMs, aiJobSummary, getAiJob, aiBuildRetryable, resumeAiJob, retryAiBuildJob, runAiJobStep, writeAiJobAudit } from "../jobs/ai-jobs.js";
|
|
21
|
+
import { AI_JOB_INLINE_BUDGET_MS, AI_JOBS_COLLECTION, aiJobNextStepMinimumMs, aiJobSummary, getAiJob, aiBuildRetryable, resumeAiJob, retryAiBuildJob, runAiJobStep, writeAiJobAudit } from "../jobs/ai-jobs.js";
|
|
22
22
|
import { aiBuildFreeCreditsRefusal, aiBuildFreeTaste } from "../jobs/ai-job-build-step.js";
|
|
23
23
|
import { aiBuildCreditRange, aiBuildFirstPagePlan, aiBuildRetryLedger, aiBuildUnits, aiLedgerUnits } from "../model/ai-build-job.js";
|
|
24
24
|
import { aiCreditsConfirmation } from "../model/ai-credit-estimate.js";
|
|
@@ -229,6 +229,12 @@ export async function POST(request, context) {
|
|
|
229
229
|
}
|
|
230
230
|
if (refusal) return refused(refusal);
|
|
231
231
|
}
|
|
232
|
+
// "Start every remaining wave" (AGL-4084): an import waiting for its next
|
|
233
|
+
// wave starts every one left, with no review between them.
|
|
234
|
+
const startAll = (payload == null ? void 0 : payload['startAll']) === true && existing.kind === 'import' ? (await import("../jobs/ai-job-import-step.js")).aiSiteImportStartAllUpdate(existing) : null;
|
|
235
|
+
if (startAll) {
|
|
236
|
+
await gate.firestore.collection('orgs').doc(gate.orgId).collection(AI_JOBS_COLLECTION).doc(jobId).update(startAll);
|
|
237
|
+
}
|
|
232
238
|
const now = new Date();
|
|
233
239
|
const confirmation = aiCreditsConfirmation(confirmedCredits, gate.uid, now);
|
|
234
240
|
const { job, changed } = retryFailedItems ? await retryAiBuildJob(gate.firestore, gate.orgId, jobId, now, {
|
|
@@ -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, 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 { aiJobInputProblems } from '../model/ai-job-input'\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 — or\n * answer what a job waiting in `needs_input` asks (`input`, AGL-3978), which\n * returns it to its plan card, or a 400 with `fieldErrors` by field.\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 job waiting for a person's answer (AGL-3978): every field must read\n // before anything changes, and a refusal names each field it is about.\n const answering = existing.status === 'needs_input' && Boolean(existing.input?.fields?.length)\n const rawInput = payload?.['input']\n const answers = rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? (rawInput as Record<string, unknown>) : null\n if (answering && existing.input) {\n const fieldErrors = aiJobInputProblems(existing.input, answers)\n const first = Object.values(fieldErrors)[0]\n if (first) {\n await release()\n return Response.json({ error: first, fieldErrors, job: aiJobSummary(existing, new Date()) }, { status: 400 })\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 ...(answering ? { input: answers } : {}),\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' : answering ? 'input' : (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","aiJobInputProblems","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","answering","Boolean","input","fields","length","rawInput","answers","Array","isArray","fieldErrors","first","Object","values","org","inputs","uid","confirmation","changed","publishConfirmed","reducePlan","action","actorUid","actorEmail","decoded","email","after","reason","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","latest","items","site","retried","range","slots","Set","onConfirmed","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 AI_JOBS_COLLECTION,\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 { aiJobInputProblems } from '../model/ai-job-input'\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 — or\n * answer what a job waiting in `needs_input` asks (`input`, AGL-3978), which\n * returns it to its plan card, or a 400 with `fieldErrors` by field.\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 job waiting for a person's answer (AGL-3978): every field must read\n // before anything changes, and a refusal names each field it is about.\n const answering = existing.status === 'needs_input' && Boolean(existing.input?.fields?.length)\n const rawInput = payload?.['input']\n const answers = rawInput && typeof rawInput === 'object' && !Array.isArray(rawInput) ? (rawInput as Record<string, unknown>) : null\n if (answering && existing.input) {\n const fieldErrors = aiJobInputProblems(existing.input, answers)\n const first = Object.values(fieldErrors)[0]\n if (first) {\n await release()\n return Response.json({ error: first, fieldErrors, job: aiJobSummary(existing, new Date()) }, { status: 400 })\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 // \"Start every remaining wave\" (AGL-4084): an import waiting for its next\n // wave starts every one left, with no review between them.\n const startAll =\n payload?.['startAll'] === true && existing.kind === 'import'\n ? (await import('../jobs/ai-job-import-step')).aiSiteImportStartAllUpdate(existing)\n : null\n if (startAll) {\n await gate.firestore.collection('orgs').doc(gate.orgId).collection(AI_JOBS_COLLECTION).doc(jobId).update(startAll)\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 ...(answering ? { input: answers } : {}),\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' : answering ? 'input' : (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","AI_JOBS_COLLECTION","aiJobNextStepMinimumMs","aiJobSummary","getAiJob","aiBuildRetryable","resumeAiJob","retryAiBuildJob","runAiJobStep","writeAiJobAudit","aiBuildFreeCreditsRefusal","aiBuildFreeTaste","aiBuildCreditRange","aiBuildFirstPagePlan","aiBuildRetryLedger","aiBuildUnits","aiLedgerUnits","aiCreditsConfirmation","aiJobInputProblems","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","answering","Boolean","input","fields","length","rawInput","answers","Array","isArray","fieldErrors","first","Object","values","org","inputs","uid","startAll","aiSiteImportStartAllUpdate","collection","doc","update","confirmation","changed","publishConfirmed","reducePlan","action","actorUid","actorEmail","decoded","email","after","reason","run","owner","signal","AbortSignal","timeout","actor","staff","outcome","latest","items","site","retried","range","slots","Set","onConfirmed","dynamic","maxDuration"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export declare const AGLYN_DOMAIN = "aglyn.com";
|
|
18
|
+
export declare const AGLYN_ORIGIN = "https://aglyn.com";
|
|
19
|
+
/** One recorded file, as text. */
|
|
20
|
+
export declare function aglynRecording(name: string): string;
|
|
21
|
+
/** The pages recorded whole, by path. */
|
|
22
|
+
export declare const AGLYN_RECORDED_PAGES: Readonly<Record<string, string>>;
|
|
23
|
+
/** A recorded page's markup. */
|
|
24
|
+
export declare function aglynPage(path: string): string;
|
|
25
|
+
/** What the live pages measured on 10/28, in bytes, before trimming. */
|
|
26
|
+
export declare const AGLYN_PAGE_BYTES: Readonly<Record<string, number>>;
|
|
27
|
+
/** The average of a sample of ten live pages, for every page not recorded. */
|
|
28
|
+
export declare const AGLYN_AVERAGE_PAGE_BYTES: number;
|
|
29
|
+
/** The sitemap index's children, by the address the index names them at. */
|
|
30
|
+
export declare const AGLYN_SITEMAP_CHILDREN: Readonly<Record<string, string>>;
|
|
31
|
+
/** Every page address the six children list, in the index's order. */
|
|
32
|
+
export declare function aglynSitemapUrls(): string[];
|
|
33
|
+
/**
|
|
34
|
+
* A page of aglyn.com that was not recorded, padded to `bytes`: a title, a
|
|
35
|
+
* heading and a paragraph that say which page it is, so the inventory has
|
|
36
|
+
* words to list, then a comment the extractor throws away.
|
|
37
|
+
*/
|
|
38
|
+
export declare function aglynSyntheticPage(path: string, bytes?: number): string;
|
|
39
|
+
export interface AglynAnswer {
|
|
40
|
+
status: number;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
body: string;
|
|
43
|
+
}
|
|
44
|
+
/** What Vercel's managed bot protection answers a client it challenges. */
|
|
45
|
+
export declare const AGLYN_CHALLENGE: AglynAnswer;
|
|
46
|
+
export interface AglynSiteOptions {
|
|
47
|
+
/**
|
|
48
|
+
* The edge as it stood on 10/20: bot protection challenges every page but
|
|
49
|
+
* the store-review bypass paths (/contact, /legal/*) and the crawler
|
|
50
|
+
* metadata (robots.txt, sitemaps).
|
|
51
|
+
*/
|
|
52
|
+
challenged?: boolean;
|
|
53
|
+
/** The byte size every page answers with; the measured sizes when absent. */
|
|
54
|
+
pageBytes?: number;
|
|
55
|
+
}
|
|
56
|
+
/** What aglyn.com answers an address, as recorded. */
|
|
57
|
+
export declare function aglynAnswer(url: string, options?: AglynSiteOptions): AglynAnswer;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* aglyn.com as AglynBot read it on 2026-10-28 (AGL-4083), for the offline
|
|
19
|
+
* replay specs of Bring my site: the first site this importer was pointed at
|
|
20
|
+
* that was larger than a small business's, and the one the 10/20 import of
|
|
21
|
+
* read 10 pages of.
|
|
22
|
+
*
|
|
23
|
+
* `aglyn-com/` holds seven pages recorded whole (home, about, pricing,
|
|
24
|
+
* contact, product, product/ai, legal/privacy), robots.txt, the sitemap index
|
|
25
|
+
* and all six of its children. Every recording was trimmed before it entered
|
|
26
|
+
* the repo: script bodies are gone (one small inline script with a hostile
|
|
27
|
+
* line stands in for them; the Organization and WebSite structured data
|
|
28
|
+
* stay), so are styles, inline SVG bodies, preload links and class names, and
|
|
29
|
+
* every address and phone number is a placeholder. What is left is what the
|
|
30
|
+
* extractor reads: the head's tags and the header, nav, main and footer whole.
|
|
31
|
+
*
|
|
32
|
+
* The real pages are larger than the trimmed ones; `AGLYN_PAGE_BYTES` keeps
|
|
33
|
+
* their measured sizes, and the byte-budget specs serve pages of those sizes.
|
|
34
|
+
*/ import { readFileSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
export const AGLYN_DOMAIN = 'aglyn.com';
|
|
37
|
+
export const AGLYN_ORIGIN = `https://${AGLYN_DOMAIN}`;
|
|
38
|
+
const DIR = join(__dirname, 'aglyn-com');
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
/** One recorded file, as text. */ export function aglynRecording(name) {
|
|
41
|
+
const held = cache.get(name);
|
|
42
|
+
if (held !== undefined) return held;
|
|
43
|
+
const text = readFileSync(join(DIR, name), 'utf8');
|
|
44
|
+
cache.set(name, text);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
/** The pages recorded whole, by path. */ export const AGLYN_RECORDED_PAGES = {
|
|
48
|
+
'/': 'home.html',
|
|
49
|
+
'/about': 'about.html',
|
|
50
|
+
'/pricing': 'pricing.html',
|
|
51
|
+
'/contact': 'contact.html',
|
|
52
|
+
'/product': 'product.html',
|
|
53
|
+
'/product/ai': 'product__ai.html',
|
|
54
|
+
'/legal/privacy': 'legal__privacy.html'
|
|
55
|
+
};
|
|
56
|
+
/** A recorded page's markup. */ export function aglynPage(path) {
|
|
57
|
+
const name = AGLYN_RECORDED_PAGES[path];
|
|
58
|
+
if (!name) throw new Error(`No recording of ${path}`);
|
|
59
|
+
return aglynRecording(name);
|
|
60
|
+
}
|
|
61
|
+
/** What the live pages measured on 10/28, in bytes, before trimming. */ export const AGLYN_PAGE_BYTES = {
|
|
62
|
+
'/': 506077,
|
|
63
|
+
'/about': 312293,
|
|
64
|
+
'/pricing': 925686,
|
|
65
|
+
'/contact': 312917,
|
|
66
|
+
'/product': 516783,
|
|
67
|
+
'/product/ai': 475326,
|
|
68
|
+
'/legal/privacy': 367243
|
|
69
|
+
};
|
|
70
|
+
/** The average of a sample of ten live pages, for every page not recorded. */ export const AGLYN_AVERAGE_PAGE_BYTES = 440 * 1024;
|
|
71
|
+
/** The sitemap index's children, by the address the index names them at. */ export const AGLYN_SITEMAP_CHILDREN = {
|
|
72
|
+
[`${AGLYN_ORIGIN}/sitemaps/pages/1.xml`]: 'sitemaps/pages.xml',
|
|
73
|
+
[`${AGLYN_ORIGIN}/sitemaps/content-blog/1.xml`]: 'sitemaps/content-blog.xml',
|
|
74
|
+
[`${AGLYN_ORIGIN}/sitemaps/content-changelog/1.xml`]: 'sitemaps/content-changelog.xml',
|
|
75
|
+
[`${AGLYN_ORIGIN}/sitemaps/content-videos/1.xml`]: 'sitemaps/content-videos.xml',
|
|
76
|
+
[`${AGLYN_ORIGIN}/sitemaps/content-newsroom/1.xml`]: 'sitemaps/content-newsroom.xml',
|
|
77
|
+
[`${AGLYN_ORIGIN}/sitemaps/authors/1.xml`]: 'sitemaps/authors.xml'
|
|
78
|
+
};
|
|
79
|
+
/** Every page address the six children list, in the index's order. */ export function aglynSitemapUrls() {
|
|
80
|
+
const urls = [];
|
|
81
|
+
for (const file of Object.values(AGLYN_SITEMAP_CHILDREN)){
|
|
82
|
+
for (const match of aglynRecording(file).matchAll(/<loc>([^<]+)<\/loc>/g))urls.push(match[1].trim());
|
|
83
|
+
}
|
|
84
|
+
return urls;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A page of aglyn.com that was not recorded, padded to `bytes`: a title, a
|
|
88
|
+
* heading and a paragraph that say which page it is, so the inventory has
|
|
89
|
+
* words to list, then a comment the extractor throws away.
|
|
90
|
+
*/ export function aglynSyntheticPage(path, bytes = AGLYN_AVERAGE_PAGE_BYTES) {
|
|
91
|
+
var _path_split_filter_pop;
|
|
92
|
+
const name = (_path_split_filter_pop = path.split('/').filter(Boolean).pop()) != null ? _path_split_filter_pop : 'home';
|
|
93
|
+
const head = `<!DOCTYPE html><html lang="en"><head><title>${name} | ${AGLYN_DOMAIN}</title><meta name="description" content="The ${name} page."></head><body><main><h1>${name}</h1><p>This is the ${name} page of ${AGLYN_DOMAIN}, recorded as a stand-in.</p></main>`;
|
|
94
|
+
const tail = '</body></html>';
|
|
95
|
+
const pad = Math.max(0, bytes - head.length - tail.length - 7);
|
|
96
|
+
return `${head}<!--${'x'.repeat(pad)}-->${tail}`;
|
|
97
|
+
}
|
|
98
|
+
const HTML = {
|
|
99
|
+
'content-type': 'text/html; charset=utf-8'
|
|
100
|
+
};
|
|
101
|
+
/** What Vercel's managed bot protection answers a client it challenges. */ export const AGLYN_CHALLENGE = {
|
|
102
|
+
status: 429,
|
|
103
|
+
headers: _extends({}, HTML, {
|
|
104
|
+
server: 'Vercel',
|
|
105
|
+
'x-vercel-mitigated': 'challenge'
|
|
106
|
+
}),
|
|
107
|
+
body: '<!DOCTYPE html><html><head><title>Vercel Security Checkpoint</title></head><body><p>We are verifying your browser.</p></body></html>'
|
|
108
|
+
};
|
|
109
|
+
/** What aglyn.com answers an address, as recorded. */ export function aglynAnswer(url, options = {}) {
|
|
110
|
+
var _ref, _options_pageBytes;
|
|
111
|
+
const parsed = new URL(url);
|
|
112
|
+
if (parsed.origin !== AGLYN_ORIGIN) {
|
|
113
|
+
// The bare name is the one the site answers on; www sends visitors there.
|
|
114
|
+
if (parsed.origin === `https://www.${AGLYN_DOMAIN}`) {
|
|
115
|
+
return {
|
|
116
|
+
status: 308,
|
|
117
|
+
headers: {
|
|
118
|
+
location: `${AGLYN_ORIGIN}${parsed.pathname}${parsed.search}`
|
|
119
|
+
},
|
|
120
|
+
body: ''
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
status: 404,
|
|
125
|
+
headers: HTML,
|
|
126
|
+
body: 'Not found'
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const path = parsed.pathname;
|
|
130
|
+
if (path === '/robots.txt') return {
|
|
131
|
+
status: 200,
|
|
132
|
+
headers: {
|
|
133
|
+
'content-type': 'text/plain'
|
|
134
|
+
},
|
|
135
|
+
body: aglynRecording('robots.txt')
|
|
136
|
+
};
|
|
137
|
+
if (path === '/sitemap.xml') return {
|
|
138
|
+
status: 200,
|
|
139
|
+
headers: {
|
|
140
|
+
'content-type': 'application/xml'
|
|
141
|
+
},
|
|
142
|
+
body: aglynRecording('sitemap.xml')
|
|
143
|
+
};
|
|
144
|
+
const child = AGLYN_SITEMAP_CHILDREN[`${AGLYN_ORIGIN}${path}`];
|
|
145
|
+
if (child) return {
|
|
146
|
+
status: 200,
|
|
147
|
+
headers: {
|
|
148
|
+
'content-type': 'application/xml'
|
|
149
|
+
},
|
|
150
|
+
body: aglynRecording(child)
|
|
151
|
+
};
|
|
152
|
+
const bypassed = path === '/contact' || path.startsWith('/legal/');
|
|
153
|
+
if (options.challenged && !bypassed) return AGLYN_CHALLENGE;
|
|
154
|
+
if (!aglynSitemapUrls().includes(`${AGLYN_ORIGIN}${path}`)) return {
|
|
155
|
+
status: 404,
|
|
156
|
+
headers: HTML,
|
|
157
|
+
body: 'Not found'
|
|
158
|
+
};
|
|
159
|
+
const recorded = AGLYN_RECORDED_PAGES[path];
|
|
160
|
+
if (recorded && options.pageBytes === undefined) return {
|
|
161
|
+
status: 200,
|
|
162
|
+
headers: HTML,
|
|
163
|
+
body: aglynRecording(recorded)
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
status: 200,
|
|
167
|
+
headers: HTML,
|
|
168
|
+
body: aglynSyntheticPage(path, (_ref = (_options_pageBytes = options.pageBytes) != null ? _options_pageBytes : AGLYN_PAGE_BYTES[path]) != null ? _ref : AGLYN_AVERAGE_PAGE_BYTES)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//# sourceMappingURL=aglyn-com-recordings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/ai/src/lib/site-import/fixtures/aglyn-com-recordings.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 * aglyn.com as AglynBot read it on 2026-10-28 (AGL-4083), for the offline\n * replay specs of Bring my site: the first site this importer was pointed at\n * that was larger than a small business's, and the one the 10/20 import of\n * read 10 pages of.\n *\n * `aglyn-com/` holds seven pages recorded whole (home, about, pricing,\n * contact, product, product/ai, legal/privacy), robots.txt, the sitemap index\n * and all six of its children. Every recording was trimmed before it entered\n * the repo: script bodies are gone (one small inline script with a hostile\n * line stands in for them; the Organization and WebSite structured data\n * stay), so are styles, inline SVG bodies, preload links and class names, and\n * every address and phone number is a placeholder. What is left is what the\n * extractor reads: the head's tags and the header, nav, main and footer whole.\n *\n * The real pages are larger than the trimmed ones; `AGLYN_PAGE_BYTES` keeps\n * their measured sizes, and the byte-budget specs serve pages of those sizes.\n */\n\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport const AGLYN_DOMAIN = 'aglyn.com'\nexport const AGLYN_ORIGIN = `https://${AGLYN_DOMAIN}`\n\nconst DIR = join(__dirname, 'aglyn-com')\n\nconst cache = new Map<string, string>()\n\n/** One recorded file, as text. */\nexport function aglynRecording(name: string): string {\n const held = cache.get(name)\n if (held !== undefined) return held\n const text = readFileSync(join(DIR, name), 'utf8')\n cache.set(name, text)\n return text\n}\n\n/** The pages recorded whole, by path. */\nexport const AGLYN_RECORDED_PAGES: Readonly<Record<string, string>> = {\n '/': 'home.html',\n '/about': 'about.html',\n '/pricing': 'pricing.html',\n '/contact': 'contact.html',\n '/product': 'product.html',\n '/product/ai': 'product__ai.html',\n '/legal/privacy': 'legal__privacy.html',\n}\n\n/** A recorded page's markup. */\nexport function aglynPage(path: string): string {\n const name = AGLYN_RECORDED_PAGES[path]\n if (!name) throw new Error(`No recording of ${path}`)\n return aglynRecording(name)\n}\n\n/** What the live pages measured on 10/28, in bytes, before trimming. */\nexport const AGLYN_PAGE_BYTES: Readonly<Record<string, number>> = {\n '/': 506_077,\n '/about': 312_293,\n '/pricing': 925_686,\n '/contact': 312_917,\n '/product': 516_783,\n '/product/ai': 475_326,\n '/legal/privacy': 367_243,\n}\n\n/** The average of a sample of ten live pages, for every page not recorded. */\nexport const AGLYN_AVERAGE_PAGE_BYTES = 440 * 1024\n\n/** The sitemap index's children, by the address the index names them at. */\nexport const AGLYN_SITEMAP_CHILDREN: Readonly<Record<string, string>> = {\n [`${AGLYN_ORIGIN}/sitemaps/pages/1.xml`]: 'sitemaps/pages.xml',\n [`${AGLYN_ORIGIN}/sitemaps/content-blog/1.xml`]: 'sitemaps/content-blog.xml',\n [`${AGLYN_ORIGIN}/sitemaps/content-changelog/1.xml`]: 'sitemaps/content-changelog.xml',\n [`${AGLYN_ORIGIN}/sitemaps/content-videos/1.xml`]: 'sitemaps/content-videos.xml',\n [`${AGLYN_ORIGIN}/sitemaps/content-newsroom/1.xml`]: 'sitemaps/content-newsroom.xml',\n [`${AGLYN_ORIGIN}/sitemaps/authors/1.xml`]: 'sitemaps/authors.xml',\n}\n\n/** Every page address the six children list, in the index's order. */\nexport function aglynSitemapUrls(): string[] {\n const urls: string[] = []\n for (const file of Object.values(AGLYN_SITEMAP_CHILDREN)) {\n for (const match of aglynRecording(file).matchAll(/<loc>([^<]+)<\\/loc>/g)) urls.push(match[1].trim())\n }\n return urls\n}\n\n/**\n * A page of aglyn.com that was not recorded, padded to `bytes`: a title, a\n * heading and a paragraph that say which page it is, so the inventory has\n * words to list, then a comment the extractor throws away.\n */\nexport function aglynSyntheticPage(path: string, bytes = AGLYN_AVERAGE_PAGE_BYTES): string {\n const name = path.split('/').filter(Boolean).pop() ?? 'home'\n const head = `<!DOCTYPE html><html lang=\"en\"><head><title>${name} | ${AGLYN_DOMAIN}</title><meta name=\"description\" content=\"The ${name} page.\"></head><body><main><h1>${name}</h1><p>This is the ${name} page of ${AGLYN_DOMAIN}, recorded as a stand-in.</p></main>`\n const tail = '</body></html>'\n const pad = Math.max(0, bytes - head.length - tail.length - 7)\n return `${head}<!--${'x'.repeat(pad)}-->${tail}`\n}\n\nexport interface AglynAnswer {\n status: number\n headers: Record<string, string>\n body: string\n}\n\nconst HTML = { 'content-type': 'text/html; charset=utf-8' }\n\n/** What Vercel's managed bot protection answers a client it challenges. */\nexport const AGLYN_CHALLENGE: AglynAnswer = {\n status: 429,\n headers: { ...HTML, server: 'Vercel', 'x-vercel-mitigated': 'challenge' },\n body: '<!DOCTYPE html><html><head><title>Vercel Security Checkpoint</title></head><body><p>We are verifying your browser.</p></body></html>',\n}\n\nexport interface AglynSiteOptions {\n /**\n * The edge as it stood on 10/20: bot protection challenges every page but\n * the store-review bypass paths (/contact, /legal/*) and the crawler\n * metadata (robots.txt, sitemaps).\n */\n challenged?: boolean\n /** The byte size every page answers with; the measured sizes when absent. */\n pageBytes?: number\n}\n\n/** What aglyn.com answers an address, as recorded. */\nexport function aglynAnswer(url: string, options: AglynSiteOptions = {}): AglynAnswer {\n const parsed = new URL(url)\n if (parsed.origin !== AGLYN_ORIGIN) {\n // The bare name is the one the site answers on; www sends visitors there.\n if (parsed.origin === `https://www.${AGLYN_DOMAIN}`) {\n return { status: 308, headers: { location: `${AGLYN_ORIGIN}${parsed.pathname}${parsed.search}` }, body: '' }\n }\n return { status: 404, headers: HTML, body: 'Not found' }\n }\n const path = parsed.pathname\n if (path === '/robots.txt') return { status: 200, headers: { 'content-type': 'text/plain' }, body: aglynRecording('robots.txt') }\n if (path === '/sitemap.xml') return { status: 200, headers: { 'content-type': 'application/xml' }, body: aglynRecording('sitemap.xml') }\n const child = AGLYN_SITEMAP_CHILDREN[`${AGLYN_ORIGIN}${path}`]\n if (child) return { status: 200, headers: { 'content-type': 'application/xml' }, body: aglynRecording(child) }\n const bypassed = path === '/contact' || path.startsWith('/legal/')\n if (options.challenged && !bypassed) return AGLYN_CHALLENGE\n if (!aglynSitemapUrls().includes(`${AGLYN_ORIGIN}${path}`)) return { status: 404, headers: HTML, body: 'Not found' }\n const recorded = AGLYN_RECORDED_PAGES[path]\n if (recorded && options.pageBytes === undefined) return { status: 200, headers: HTML, body: aglynRecording(recorded) }\n return { status: 200, headers: HTML, body: aglynSyntheticPage(path, options.pageBytes ?? AGLYN_PAGE_BYTES[path] ?? AGLYN_AVERAGE_PAGE_BYTES) }\n}\n"],"names":["readFileSync","join","AGLYN_DOMAIN","AGLYN_ORIGIN","DIR","__dirname","cache","Map","aglynRecording","name","held","get","undefined","text","set","AGLYN_RECORDED_PAGES","aglynPage","path","Error","AGLYN_PAGE_BYTES","AGLYN_AVERAGE_PAGE_BYTES","AGLYN_SITEMAP_CHILDREN","aglynSitemapUrls","urls","file","Object","values","match","matchAll","push","trim","aglynSyntheticPage","bytes","split","filter","Boolean","pop","head","tail","pad","Math","max","length","repeat","HTML","AGLYN_CHALLENGE","status","headers","server","body","aglynAnswer","url","options","parsed","URL","origin","location","pathname","search","child","bypassed","startsWith","challenged","includes","recorded","pageBytes"],"mappings":"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"}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type PublicFetchOptions, type PublicFetchResult } from '../jobs/ai-theme-brand-inputs';
|
|
18
|
-
import { type SiteImportBlock, type SiteImportForm, type SiteImportImage, type SiteImportNavLink } from './site-import-html';
|
|
17
|
+
import { type PublicFetchEdge, type PublicFetchOptions, type PublicFetchResult } from '../jobs/ai-theme-brand-inputs';
|
|
18
|
+
import { type SiteImportBlock, type SiteImportBrandMark, type SiteImportForm, type SiteImportIcon, type SiteImportImage, type SiteImportNavLink, type SiteImportOrganization } from './site-import-html';
|
|
19
19
|
/**
|
|
20
20
|
* Where the bot's user agent points a site owner who wants to know what it is
|
|
21
21
|
* (AGL-3983): the operator's own page about it. `SITE_IMPORT_BOT_INFO_URL` on
|
|
@@ -32,18 +32,21 @@ export declare const AGLYN_BOT_INFO_URL: string;
|
|
|
32
32
|
export declare const AGLYN_BOT_USER_AGENT: string;
|
|
33
33
|
export declare const SITE_IMPORT_DEFAULTS: {
|
|
34
34
|
readonly maxPageBytes: number;
|
|
35
|
+
/** A whole site's worth (AGL-4083): 54 MB for aglyn.com's 123 pages, with room. */
|
|
35
36
|
readonly maxTotalBytes: number;
|
|
36
37
|
readonly timeBudgetMs: 60000;
|
|
37
38
|
/** One request's own clock, inside the whole crawl's. */
|
|
38
39
|
readonly requestTimeoutMs: 8000;
|
|
39
40
|
readonly robotsMaxBytes: number;
|
|
40
41
|
readonly sitemapMaxBytes: number;
|
|
41
|
-
/** Sitemap documents read, an index's children included. */
|
|
42
|
-
readonly maxSitemaps:
|
|
42
|
+
/** Sitemap documents read, an index's children included: an index of six children is seven. */
|
|
43
|
+
readonly maxSitemaps: 24;
|
|
43
44
|
/** URLs a sitemap may contribute to the queue. */
|
|
44
45
|
readonly maxSitemapUrls: 2000;
|
|
45
|
-
/** Reasons kept in `skipped` before the list stops growing. */
|
|
46
|
-
readonly maxSkipped:
|
|
46
|
+
/** Reasons kept in `skipped` before the list stops growing: every address a sitemap may name. */
|
|
47
|
+
readonly maxSkipped: 2000;
|
|
48
|
+
/** Pages read at once, after robots.txt, the sitemaps and the home page. */
|
|
49
|
+
readonly concurrency: 4;
|
|
47
50
|
};
|
|
48
51
|
export type SiteImportSkipReason =
|
|
49
52
|
/** The page has a password field: a sign-in page, refused outright. */
|
|
@@ -59,13 +62,17 @@ export type SiteImportSkipReason =
|
|
|
59
62
|
/** Answered 4xx or 5xx. */
|
|
60
63
|
| 'http-error'
|
|
61
64
|
/** No answer, a private address, or a refused address. */
|
|
62
|
-
| 'unreachable'
|
|
65
|
+
| 'unreachable'
|
|
66
|
+
/** Never asked for: a stop (time, bytes, the page budget) came first (AGL-4083). */
|
|
67
|
+
| 'not-reached';
|
|
63
68
|
export interface SiteImportSkipped {
|
|
64
69
|
url: string;
|
|
65
70
|
reason: SiteImportSkipReason;
|
|
66
71
|
status?: number;
|
|
67
72
|
/** Where an off-origin redirect pointed. */
|
|
68
73
|
location?: string;
|
|
74
|
+
/** Whose edge turned an error answer away, when it said (AGL-4083). */
|
|
75
|
+
edge?: PublicFetchEdge;
|
|
69
76
|
}
|
|
70
77
|
export interface SiteImportPage {
|
|
71
78
|
/** The address the page was read from, after any same-site redirect. */
|
|
@@ -77,6 +84,14 @@ export interface SiteImportPage {
|
|
|
77
84
|
description: string;
|
|
78
85
|
lang: string | null;
|
|
79
86
|
canonical: string | null;
|
|
87
|
+
/** The share card (AGL-4083). */
|
|
88
|
+
ogImage: string | null;
|
|
89
|
+
/** The site's own name, as its head gives it. */
|
|
90
|
+
siteName: string;
|
|
91
|
+
icons: SiteImportIcon[];
|
|
92
|
+
/** The header's link home: the brand's logo and words. */
|
|
93
|
+
brand: SiteImportBrandMark | null;
|
|
94
|
+
organization: SiteImportOrganization | null;
|
|
80
95
|
blocks: SiteImportBlock[];
|
|
81
96
|
images: SiteImportImage[];
|
|
82
97
|
navLinks: SiteImportNavLink[];
|
|
@@ -138,6 +153,16 @@ export interface SiteImportCrawlOptions {
|
|
|
138
153
|
export interface SiteImportCrawlSeams {
|
|
139
154
|
fetch?: (url: string, options: PublicFetchOptions) => Promise<PublicFetchResult>;
|
|
140
155
|
now?: () => number;
|
|
156
|
+
/**
|
|
157
|
+
* Request headers for the site being read, asked once per crawl with its
|
|
158
|
+
* domain (AGL-4083, AGL-4085). Absent, the crawl asks the edge
|
|
159
|
+
* (`siteImportEdgeHeadersFor`): Aglyn's edge secret for each of the site's
|
|
160
|
+
* two origins this platform serves, decided origin by origin, and nothing
|
|
161
|
+
* anywhere else. Either way the headers go only to the site's two origins,
|
|
162
|
+
* and the fetch asks again on every redirect hop, so a hop anywhere else
|
|
163
|
+
* carries nothing. Always under the crawler's own `Accept` and `User-Agent`.
|
|
164
|
+
*/
|
|
165
|
+
headersFor?: (domain: string) => Promise<Readonly<Record<string, string>>> | Readonly<Record<string, string>>;
|
|
141
166
|
}
|
|
142
167
|
/** The site's two origins for a domain. */
|
|
143
168
|
export declare function siteImportOrigins(domain: string): string[];
|