@aglyn/plugins-ai 1.0.0-beta.271 → 1.0.0-beta.272
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 +4 -3
- 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-site-build-page.component.js +18 -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 +41 -0
- package/src/lib/components/ai-site-import-job.component.js +707 -0
- package/src/lib/components/ai-site-import-job.component.js.map +1 -0
- package/src/lib/components/ai-site-import-start.component.d.ts +64 -0
- package/src/lib/components/ai-site-import-start.component.js +335 -0
- package/src/lib/components/ai-site-import-start.component.js.map +1 -0
- package/src/lib/components/ai-site-ownership-proof.component.d.ts +42 -0
- package/src/lib/components/ai-site-ownership-proof.component.js +353 -0
- package/src/lib/components/ai-site-ownership-proof.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.js +165 -91
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +14 -0
- package/src/lib/jobs/ai-job-drafts.js +24 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +136 -0
- package/src/lib/jobs/ai-job-import-step.js +809 -0
- package/src/lib/jobs/ai-job-import-step.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +39 -1
- package/src/lib/jobs/ai-job-page-language.js +62 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +2 -2
- package/src/lib/jobs/ai-job-page-step.js +5 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +5 -0
- package/src/lib/jobs/ai-jobs.js +15 -3
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-owner-photos.d.ts +16 -1
- package/src/lib/jobs/ai-layout-owner-photos.js +47 -0
- package/src/lib/jobs/ai-layout-owner-photos.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +61 -3
- package/src/lib/jobs/ai-theme-brand-inputs.js +117 -27
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +20 -0
- package/src/lib/model/ai-credit-estimate.js +13 -0
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +2 -2
- package/src/lib/model/ai-job-activity.js +6 -4
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-notice.js +18 -4
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +29 -2
- package/src/lib/model/ai-jobs.types.js +2 -1
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import-view.d.ts +130 -0
- package/src/lib/model/ai-site-import-view.js +182 -0
- package/src/lib/model/ai-site-import-view.js.map +1 -0
- package/src/lib/model/ai-site-import.d.ts +302 -0
- package/src/lib/model/ai-site-import.js +526 -0
- package/src/lib/model/ai-site-import.js.map +1 -0
- package/src/lib/model/ai-site-start.d.ts +22 -9
- package/src/lib/model/ai-site-start.js +64 -23
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +11 -0
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/ai-site-import-route.d.ts +89 -0
- package/src/lib/server/ai-site-import-route.js +356 -0
- package/src/lib/server/ai-site-import-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +45 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +313 -0
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js +217 -0
- package/src/lib/site-import/fixtures/site-import-wave-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +163 -0
- package/src/lib/site-import/site-import-crawl.js +467 -0
- package/src/lib/site-import/site-import-crawl.js.map +1 -0
- package/src/lib/site-import/site-import-flag.d.ts +23 -0
- package/src/lib/site-import/site-import-flag.js +23 -0
- package/src/lib/site-import/site-import-flag.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +94 -0
- package/src/lib/site-import/site-import-html.js +462 -0
- package/src/lib/site-import/site-import-html.js.map +1 -0
- package/src/lib/site-import/site-import-images.d.ts +54 -0
- package/src/lib/site-import/site-import-images.js +204 -0
- package/src/lib/site-import/site-import-images.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +48 -0
- package/src/lib/site-import/site-import-page.js +150 -0
- package/src/lib/site-import/site-import-page.js.map +1 -0
- package/src/lib/site-import/site-import-review.d.ts +40 -0
- package/src/lib/site-import/site-import-review.js +149 -0
- package/src/lib/site-import/site-import-review.js.map +1 -0
- package/src/lib/site-import/site-import-robots.d.ts +52 -0
- package/src/lib/site-import/site-import-robots.js +126 -0
- package/src/lib/site-import/site-import-robots.js.map +1 -0
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
* EVERY SITE IMPORT IS LOGGED FOR TRUST & SAFETY (AGL-3976).
|
|
19
|
+
*
|
|
20
|
+
* A site import is a one-click copy of a website, which is exactly what a
|
|
21
|
+
* phishing clone is. Proving ownership is the control; this row is how a
|
|
22
|
+
* person checks the control held. Every import job files ONE row in the
|
|
23
|
+
* abuse queue (`abuseReports`), in the shape the outbound screen files a
|
|
24
|
+
* hold (`fileOutboundHold`): the same fields the admin page and the daily
|
|
25
|
+
* review already read, `source: 'site-import'`, the proved domain as text
|
|
26
|
+
* (the queue never renders a reported address as a link), and the proof.
|
|
27
|
+
*
|
|
28
|
+
* The row is written when the site is read and rewritten as each wave
|
|
29
|
+
* starts and finishes, its `reportCount` counting the waves, so the queue
|
|
30
|
+
* shows one import as one row however long it runs. It holds nothing: the
|
|
31
|
+
* import goes on while the row is open. Staff look at the old site and the
|
|
32
|
+
* new one; a workspace bringing in a site that is not its own is locked,
|
|
33
|
+
* and the row closed with what was done.
|
|
34
|
+
*
|
|
35
|
+
* Never throws: a log that cannot be written must not stop a person's
|
|
36
|
+
* import, and the failure is a log line staff alerts read.
|
|
37
|
+
*=========================================*/ import { ABUSE_REPORT_COLLECTION, ABUSE_REPORT_MAX_DETAILS } from "@aglyn/aglyn/app-utils/abuse-report";
|
|
38
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
39
|
+
import { AI_SITE_IMPORT_LEFT_OUT_REASONS, aiSiteImportPagesText } from "../model/ai-site-import.js";
|
|
40
|
+
/** The abuse queue's `source` for a site import's row. */ export const SITE_IMPORT_REVIEW_SOURCE = 'site-import';
|
|
41
|
+
/** One row per import job. */ export function siteImportReviewId(jobId) {
|
|
42
|
+
return `site-import-${jobId}`;
|
|
43
|
+
}
|
|
44
|
+
/** The reference staff quote: `SI-` and the job's id. */ export function siteImportReviewReference(jobId) {
|
|
45
|
+
return `SI-${jobId.slice(0, 10).toUpperCase()}`;
|
|
46
|
+
}
|
|
47
|
+
const PROOF_WORDS = {
|
|
48
|
+
dns: 'a DNS TXT record',
|
|
49
|
+
meta: 'a meta tag on its home page',
|
|
50
|
+
email: 'a code emailed to an address at the domain'
|
|
51
|
+
};
|
|
52
|
+
/** The row's details: what was proved, read, kept, left out and brought in, rebuilt whole each time. */ export function siteImportReviewDetails(input) {
|
|
53
|
+
var _PROOF_WORDS_state_ownership_method, _state_leftOutCount_loginform;
|
|
54
|
+
const { state, items } = input;
|
|
55
|
+
const proof = state.ownership.method ? (_PROOF_WORDS_state_ownership_method = PROOF_WORDS[state.ownership.method]) != null ? _PROOF_WORDS_state_ownership_method : state.ownership.method : 'an unknown method';
|
|
56
|
+
const provenOn = state.ownership.provenAt ? new Date(state.ownership.provenAt).toISOString().slice(0, 10) : 'an unknown day';
|
|
57
|
+
const leftOut = AI_SITE_IMPORT_LEFT_OUT_REASONS.filter((reason)=>{
|
|
58
|
+
var _state_leftOutCount_reason;
|
|
59
|
+
return ((_state_leftOutCount_reason = state.leftOutCount[reason]) != null ? _state_leftOutCount_reason : 0) > 0;
|
|
60
|
+
}).map((reason)=>`${reason} ${state.leftOutCount[reason]}`).join(', ');
|
|
61
|
+
const built = items.filter((row)=>row.op === 'page' && (row.status === 'succeeded' || row.status === 'degraded')).length;
|
|
62
|
+
const started = state.waves.filter((wave)=>wave.status !== 'waiting').length;
|
|
63
|
+
const photos = state.pages.reduce((total, page)=>{
|
|
64
|
+
var _ref;
|
|
65
|
+
var _page_photos;
|
|
66
|
+
return total + ((_ref = (_page_photos = page.photos) == null ? void 0 : _page_photos.brought) != null ? _ref : 0);
|
|
67
|
+
}, 0);
|
|
68
|
+
const lines = [
|
|
69
|
+
`Site import: ${state.domain} into site ${input.hostId} (AI job ${input.jobId}).`,
|
|
70
|
+
`Ownership proven by ${proof}${state.ownership.detail ? ` (${state.ownership.detail})` : ''} on ${provenOn}.`,
|
|
71
|
+
`Read ${aiSiteImportPagesText(state.found)}; bringing in ${aiSiteImportPagesText(state.pages.length)} in ${state.waves.length} ${state.waves.length === 1 ? 'wave' : 'waves'}` + `${state.pageLimit === null ? '' : ` (plan page count ${state.pageLimit}, ${state.existingPages} already on the site)`}.`,
|
|
72
|
+
...leftOut ? [
|
|
73
|
+
`Left out: ${leftOut}.`
|
|
74
|
+
] : [],
|
|
75
|
+
...((_state_leftOutCount_loginform = state.leftOutCount['login-form']) != null ? _state_leftOutCount_loginform : 0) > 0 ? [
|
|
76
|
+
`Sign-in pages refused: ${state.leftOutCount['login-form']}. A site with sign-in pages is worth a closer look.`
|
|
77
|
+
] : [],
|
|
78
|
+
`Waves started: ${started} of ${state.waves.length}. Pages brought in so far: ${built}. Photos copied into the media library: ${photos}.`,
|
|
79
|
+
'Nothing is published by the import: its pages are drafts until a member publishes them.',
|
|
80
|
+
'Look at the old site and the new one. If this copies a site the workspace does not own, lock the workspace, then close this row with what you did; otherwise dismiss it.'
|
|
81
|
+
];
|
|
82
|
+
return lines.join('\n').slice(0, ABUSE_REPORT_MAX_DETAILS);
|
|
83
|
+
}
|
|
84
|
+
/** Writes (or rewrites) the import's row in the abuse queue. Never throws. */ export async function fileSiteImportReview(firestore, input) {
|
|
85
|
+
const reviewId = siteImportReviewId(input.jobId);
|
|
86
|
+
try {
|
|
87
|
+
var _input_state_leftOutCount_loginform;
|
|
88
|
+
const ref = firestore.collection(ABUSE_REPORT_COLLECTION).doc(reviewId);
|
|
89
|
+
const first = !(await ref.get()).exists;
|
|
90
|
+
const built = input.items.filter((row)=>row.op === 'page' && (row.status === 'succeeded' || row.status === 'degraded')).length;
|
|
91
|
+
await ref.set(_extends({
|
|
92
|
+
reference: siteImportReviewReference(input.jobId),
|
|
93
|
+
// "Impersonation or trademark": the harm a copied site does is
|
|
94
|
+
// wearing a business it is not. Normal severity: the proof already
|
|
95
|
+
// held, and this row is the check that it did.
|
|
96
|
+
category: 'impersonation',
|
|
97
|
+
severity: 'normal',
|
|
98
|
+
source: SITE_IMPORT_REVIEW_SOURCE,
|
|
99
|
+
// The proved site as TEXT: the queue never renders it as a link.
|
|
100
|
+
url: `https://${input.state.domain}/`,
|
|
101
|
+
reportedHostname: input.state.domain,
|
|
102
|
+
hostId: input.hostId,
|
|
103
|
+
orgId: input.orgId,
|
|
104
|
+
details: siteImportReviewDetails(input),
|
|
105
|
+
reporterEmail: null,
|
|
106
|
+
reporterName: null,
|
|
107
|
+
dmca: null
|
|
108
|
+
}, input.event === 'wave-started' ? {
|
|
109
|
+
reportCount: FieldValue.increment(1)
|
|
110
|
+
} : first ? {
|
|
111
|
+
reportCount: 0
|
|
112
|
+
} : {}, {
|
|
113
|
+
siteImport: _extends({
|
|
114
|
+
jobId: input.jobId,
|
|
115
|
+
domain: input.state.domain,
|
|
116
|
+
method: input.state.ownership.method,
|
|
117
|
+
provenAt: input.state.ownership.provenAt,
|
|
118
|
+
detail: input.state.ownership.detail,
|
|
119
|
+
found: input.state.found,
|
|
120
|
+
kept: input.state.pages.length,
|
|
121
|
+
waves: input.state.waves.length,
|
|
122
|
+
wavesStarted: input.state.waves.filter((wave)=>wave.status !== 'waiting').length,
|
|
123
|
+
pagesBrought: built,
|
|
124
|
+
loginPagesRefused: (_input_state_leftOutCount_loginform = input.state.leftOutCount['login-form']) != null ? _input_state_leftOutCount_loginform : 0,
|
|
125
|
+
lastEvent: input.event
|
|
126
|
+
}, input.wave !== undefined ? {
|
|
127
|
+
lastWave: input.wave
|
|
128
|
+
} : {}, {
|
|
129
|
+
atMs: input.nowMs
|
|
130
|
+
}),
|
|
131
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
132
|
+
}, first ? {
|
|
133
|
+
status: 'open',
|
|
134
|
+
createdAt: FieldValue.serverTimestamp()
|
|
135
|
+
} : {}), {
|
|
136
|
+
merge: true
|
|
137
|
+
});
|
|
138
|
+
return reviewId;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error('[site-import] trust and safety row failed', {
|
|
141
|
+
orgId: input.orgId,
|
|
142
|
+
jobId: input.jobId,
|
|
143
|
+
error
|
|
144
|
+
});
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=site-import-review.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-review.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 * EVERY SITE IMPORT IS LOGGED FOR TRUST & SAFETY (AGL-3976).\n *\n * A site import is a one-click copy of a website, which is exactly what a\n * phishing clone is. Proving ownership is the control; this row is how a\n * person checks the control held. Every import job files ONE row in the\n * abuse queue (`abuseReports`), in the shape the outbound screen files a\n * hold (`fileOutboundHold`): the same fields the admin page and the daily\n * review already read, `source: 'site-import'`, the proved domain as text\n * (the queue never renders a reported address as a link), and the proof.\n *\n * The row is written when the site is read and rewritten as each wave\n * starts and finishes, its `reportCount` counting the waves, so the queue\n * shows one import as one row however long it runs. It holds nothing: the\n * import goes on while the row is open. Staff look at the old site and the\n * new one; a workspace bringing in a site that is not its own is locked,\n * and the row closed with what was done.\n *\n * Never throws: a log that cannot be written must not stop a person's\n * import, and the failure is a log line staff alerts read.\n *=========================================*/\n\nimport { ABUSE_REPORT_COLLECTION, ABUSE_REPORT_MAX_DETAILS } from '@aglyn/aglyn/app-utils/abuse-report'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n AI_SITE_IMPORT_LEFT_OUT_REASONS,\n aiSiteImportPagesText,\n type AiSiteImportState,\n} from '../model/ai-site-import'\nimport type { AiJobItemLedger } from '../model/ai-jobs.types'\n\n/** The abuse queue's `source` for a site import's row. */\nexport const SITE_IMPORT_REVIEW_SOURCE = 'site-import'\n\n/** One row per import job. */\nexport function siteImportReviewId(jobId: string): string {\n return `site-import-${jobId}`\n}\n\n/** The reference staff quote: `SI-` and the job's id. */\nexport function siteImportReviewReference(jobId: string): string {\n return `SI-${jobId.slice(0, 10).toUpperCase()}`\n}\n\nconst PROOF_WORDS: Record<string, string> = {\n dns: 'a DNS TXT record',\n meta: 'a meta tag on its home page',\n email: 'a code emailed to an address at the domain',\n}\n\nexport type SiteImportReviewEvent = 'read' | 'wave-started' | 'wave-finished'\n\nexport interface SiteImportReviewInput {\n orgId: string\n hostId: string\n jobId: string\n state: AiSiteImportState\n items: readonly AiJobItemLedger[]\n event: SiteImportReviewEvent\n /** The wave the event is about; absent on `read`. */\n wave?: number\n nowMs: number\n}\n\n/** The row's details: what was proved, read, kept, left out and brought in, rebuilt whole each time. */\nexport function siteImportReviewDetails(input: SiteImportReviewInput): string {\n const { state, items } = input\n const proof = state.ownership.method ? (PROOF_WORDS[state.ownership.method] ?? state.ownership.method) : 'an unknown method'\n const provenOn = state.ownership.provenAt ? new Date(state.ownership.provenAt).toISOString().slice(0, 10) : 'an unknown day'\n const leftOut = AI_SITE_IMPORT_LEFT_OUT_REASONS.filter((reason) => (state.leftOutCount[reason] ?? 0) > 0)\n .map((reason) => `${reason} ${state.leftOutCount[reason]}`)\n .join(', ')\n const built = items.filter((row) => row.op === 'page' && (row.status === 'succeeded' || row.status === 'degraded')).length\n const started = state.waves.filter((wave) => wave.status !== 'waiting').length\n const photos = state.pages.reduce((total, page) => total + (page.photos?.brought ?? 0), 0)\n const lines = [\n `Site import: ${state.domain} into site ${input.hostId} (AI job ${input.jobId}).`,\n `Ownership proven by ${proof}${state.ownership.detail ? ` (${state.ownership.detail})` : ''} on ${provenOn}.`,\n `Read ${aiSiteImportPagesText(state.found)}; bringing in ${aiSiteImportPagesText(state.pages.length)} in ${state.waves.length} ${state.waves.length === 1 ? 'wave' : 'waves'}` +\n `${state.pageLimit === null ? '' : ` (plan page count ${state.pageLimit}, ${state.existingPages} already on the site)`}.`,\n ...(leftOut ? [`Left out: ${leftOut}.`] : []),\n ...((state.leftOutCount['login-form'] ?? 0) > 0\n ? [`Sign-in pages refused: ${state.leftOutCount['login-form']}. A site with sign-in pages is worth a closer look.`]\n : []),\n `Waves started: ${started} of ${state.waves.length}. Pages brought in so far: ${built}. Photos copied into the media library: ${photos}.`,\n 'Nothing is published by the import: its pages are drafts until a member publishes them.',\n 'Look at the old site and the new one. If this copies a site the workspace does not own, lock the workspace, then close this row with what you did; otherwise dismiss it.',\n ]\n return lines.join('\\n').slice(0, ABUSE_REPORT_MAX_DETAILS)\n}\n\n/** Writes (or rewrites) the import's row in the abuse queue. Never throws. */\nexport async function fileSiteImportReview(\n firestore: FirebaseFirestore.Firestore,\n input: SiteImportReviewInput,\n): Promise<string | null> {\n const reviewId = siteImportReviewId(input.jobId)\n try {\n const ref = firestore.collection(ABUSE_REPORT_COLLECTION).doc(reviewId)\n const first = !(await ref.get()).exists\n const built = input.items.filter((row) => row.op === 'page' && (row.status === 'succeeded' || row.status === 'degraded')).length\n await ref.set(\n {\n reference: siteImportReviewReference(input.jobId),\n // \"Impersonation or trademark\": the harm a copied site does is\n // wearing a business it is not. Normal severity: the proof already\n // held, and this row is the check that it did.\n category: 'impersonation',\n severity: 'normal',\n source: SITE_IMPORT_REVIEW_SOURCE,\n // The proved site as TEXT: the queue never renders it as a link.\n url: `https://${input.state.domain}/`,\n reportedHostname: input.state.domain,\n hostId: input.hostId,\n orgId: input.orgId,\n details: siteImportReviewDetails(input),\n reporterEmail: null,\n reporterName: null,\n dmca: null,\n // One a wave started, so a long import reads as the volume it is.\n ...(input.event === 'wave-started' ? { reportCount: FieldValue.increment(1) } : first ? { reportCount: 0 } : {}),\n siteImport: {\n jobId: input.jobId,\n domain: input.state.domain,\n method: input.state.ownership.method,\n provenAt: input.state.ownership.provenAt,\n detail: input.state.ownership.detail,\n found: input.state.found,\n kept: input.state.pages.length,\n waves: input.state.waves.length,\n wavesStarted: input.state.waves.filter((wave) => wave.status !== 'waiting').length,\n pagesBrought: built,\n loginPagesRefused: input.state.leftOutCount['login-form'] ?? 0,\n lastEvent: input.event,\n ...(input.wave !== undefined ? { lastWave: input.wave } : {}),\n atMs: input.nowMs,\n },\n updatedAt: FieldValue.serverTimestamp(),\n ...(first ? { status: 'open', createdAt: FieldValue.serverTimestamp() } : {}),\n },\n { merge: true },\n )\n return reviewId\n } catch (error) {\n console.error('[site-import] trust and safety row failed', { orgId: input.orgId, jobId: input.jobId, error })\n return null\n }\n}\n"],"names":["ABUSE_REPORT_COLLECTION","ABUSE_REPORT_MAX_DETAILS","FieldValue","AI_SITE_IMPORT_LEFT_OUT_REASONS","aiSiteImportPagesText","SITE_IMPORT_REVIEW_SOURCE","siteImportReviewId","jobId","siteImportReviewReference","slice","toUpperCase","PROOF_WORDS","dns","meta","email","siteImportReviewDetails","input","state","items","proof","ownership","method","provenOn","provenAt","Date","toISOString","leftOut","filter","reason","leftOutCount","map","join","built","row","op","status","length","started","waves","wave","photos","pages","reduce","total","page","brought","lines","domain","hostId","detail","found","pageLimit","existingPages","fileSiteImportReview","firestore","reviewId","ref","collection","doc","first","get","exists","set","reference","category","severity","source","url","reportedHostname","orgId","details","reporterEmail","reporterName","dmca","event","reportCount","increment","siteImport","kept","wavesStarted","pagesBrought","loginPagesRefused","lastEvent","undefined","lastWave","atMs","nowMs","updatedAt","serverTimestamp","createdAt","merge","error","console"],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
/** The product token AglynBot matches robots.txt groups by. */
|
|
18
|
+
export declare const AGLYN_BOT_TOKEN = "aglynbot";
|
|
19
|
+
export interface RobotsRule {
|
|
20
|
+
allow: boolean;
|
|
21
|
+
/** The path pattern as written, `*` and `$` included. */
|
|
22
|
+
pattern: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RobotsGroup {
|
|
25
|
+
/** Lowercased product tokens, version suffixes removed. */
|
|
26
|
+
agents: string[];
|
|
27
|
+
rules: RobotsRule[];
|
|
28
|
+
}
|
|
29
|
+
export interface RobotsTxt {
|
|
30
|
+
groups: RobotsGroup[];
|
|
31
|
+
/** Every `Sitemap:` address, as written. */
|
|
32
|
+
sitemaps: string[];
|
|
33
|
+
}
|
|
34
|
+
/** robots.txt as groups and sitemaps; unknown lines are skipped. */
|
|
35
|
+
export declare function parseRobotsTxt(text: string): RobotsTxt;
|
|
36
|
+
/**
|
|
37
|
+
* The rules that apply to `token`: every group naming it, merged; when none
|
|
38
|
+
* does, every `*` group, merged; when neither exists, none (all allowed).
|
|
39
|
+
*/
|
|
40
|
+
export declare function robotsRulesFor(robots: RobotsTxt, token?: string): RobotsRule[];
|
|
41
|
+
/**
|
|
42
|
+
* Whether `pathWithQuery` (`/a/b?c=d`) may be crawled under `rules`.
|
|
43
|
+
* `/robots.txt` itself is always allowed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function robotsAllows(rules: readonly RobotsRule[], pathWithQuery: string): boolean;
|
|
46
|
+
export interface SitemapEntries {
|
|
47
|
+
/** True for a `<sitemapindex>`: `locations` are sitemaps, not pages. */
|
|
48
|
+
index: boolean;
|
|
49
|
+
locations: string[];
|
|
50
|
+
}
|
|
51
|
+
/** The `<loc>` addresses of a sitemap or a sitemap index, entities decoded. */
|
|
52
|
+
export declare function parseSitemap(xml: string, limit?: number): SitemapEntries;
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
* ROBOTS.TXT AND SITEMAPS, FOR AGLYNBOT (AGL-3974).
|
|
18
|
+
*
|
|
19
|
+
* The site-import crawler asks a site's robots.txt before every page, as
|
|
20
|
+
* RFC 9309 describes it: the group naming `AglynBot` if there is one, else
|
|
21
|
+
* the `*` group; the longest matching rule decides, and `Allow` wins a tie;
|
|
22
|
+
* `*` matches any run and a trailing `$` anchors the end. A site that keeps
|
|
23
|
+
* a path from crawlers keeps it from this one, even though the person asking
|
|
24
|
+
* owns the site: the file is the site's own statement of what a robot may
|
|
25
|
+
* read, and the person can change it.
|
|
26
|
+
*
|
|
27
|
+
* Sitemaps are read for their `<loc>` addresses, and a sitemap index for the
|
|
28
|
+
* sitemaps it lists. Gzipped sitemaps are not read.
|
|
29
|
+
*=========================================*/ /** The product token AglynBot matches robots.txt groups by. */ export const AGLYN_BOT_TOKEN = 'aglynbot';
|
|
30
|
+
/** robots.txt as groups and sitemaps; unknown lines are skipped. */ export function parseRobotsTxt(text) {
|
|
31
|
+
const groups = [];
|
|
32
|
+
const sitemaps = [];
|
|
33
|
+
let group = null;
|
|
34
|
+
let lastWasAgent = false;
|
|
35
|
+
for (const rawLine of String(text != null ? text : '').split(/\r\n|\r|\n/)){
|
|
36
|
+
const line = rawLine.replace(/#.*$/, '').trim();
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
const colon = line.indexOf(':');
|
|
39
|
+
if (colon <= 0) continue;
|
|
40
|
+
const key = line.slice(0, colon).trim().toLowerCase();
|
|
41
|
+
const value = line.slice(colon + 1).trim();
|
|
42
|
+
if (key === 'sitemap') {
|
|
43
|
+
if (value) sitemaps.push(value);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (key === 'user-agent') {
|
|
47
|
+
const agent = value.toLowerCase().split('/')[0].trim();
|
|
48
|
+
if (!group || !lastWasAgent) {
|
|
49
|
+
group = {
|
|
50
|
+
agents: [],
|
|
51
|
+
rules: []
|
|
52
|
+
};
|
|
53
|
+
groups.push(group);
|
|
54
|
+
}
|
|
55
|
+
if (agent) group.agents.push(agent);
|
|
56
|
+
lastWasAgent = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
lastWasAgent = false;
|
|
60
|
+
if (!group) continue;
|
|
61
|
+
if (key === 'allow' || key === 'disallow') {
|
|
62
|
+
// An empty Disallow allows everything, which is what no rule does.
|
|
63
|
+
if (!value) continue;
|
|
64
|
+
group.rules.push({
|
|
65
|
+
allow: key === 'allow',
|
|
66
|
+
pattern: value
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
groups,
|
|
72
|
+
sitemaps
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The rules that apply to `token`: every group naming it, merged; when none
|
|
77
|
+
* does, every `*` group, merged; when neither exists, none (all allowed).
|
|
78
|
+
*/ export function robotsRulesFor(robots, token = AGLYN_BOT_TOKEN) {
|
|
79
|
+
const own = robots.groups.filter((group)=>group.agents.includes(token));
|
|
80
|
+
const chosen = own.length ? own : robots.groups.filter((group)=>group.agents.includes('*'));
|
|
81
|
+
return chosen.flatMap((group)=>group.rules);
|
|
82
|
+
}
|
|
83
|
+
function patternMatchLength(pattern, path) {
|
|
84
|
+
const anchored = pattern.endsWith('$');
|
|
85
|
+
const body = anchored ? pattern.slice(0, -1) : pattern;
|
|
86
|
+
const source = body.split('*').map((part)=>part.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*');
|
|
87
|
+
const regex = new RegExp(`^${source}${anchored ? '$' : ''}`);
|
|
88
|
+
return regex.test(path) ? body.length : -1;
|
|
89
|
+
}
|
|
90
|
+
/** A path with a percent-encoding case that compares the way robots.txt writes it. */ function comparablePath(path) {
|
|
91
|
+
return path.replace(/%[0-9a-f]{2}/gi, (escape)=>escape.toUpperCase());
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether `pathWithQuery` (`/a/b?c=d`) may be crawled under `rules`.
|
|
95
|
+
* `/robots.txt` itself is always allowed.
|
|
96
|
+
*/ export function robotsAllows(rules, pathWithQuery) {
|
|
97
|
+
const path = comparablePath(pathWithQuery || '/');
|
|
98
|
+
if (path === '/robots.txt') return true;
|
|
99
|
+
let best = -1;
|
|
100
|
+
let allowed = true;
|
|
101
|
+
for (const rule of rules){
|
|
102
|
+
const length = patternMatchLength(comparablePath(rule.pattern), path);
|
|
103
|
+
if (length < 0) continue;
|
|
104
|
+
if (length > best || length === best && rule.allow) {
|
|
105
|
+
best = length;
|
|
106
|
+
allowed = rule.allow;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return allowed;
|
|
110
|
+
}
|
|
111
|
+
/** The `<loc>` addresses of a sitemap or a sitemap index, entities decoded. */ export function parseSitemap(xml, limit = 5000) {
|
|
112
|
+
const source = String(xml != null ? xml : '');
|
|
113
|
+
const index = /<sitemapindex\b/i.test(source);
|
|
114
|
+
const locations = [];
|
|
115
|
+
for (const match of source.matchAll(/<loc\b[^>]*>([\s\S]*?)<\/loc\s*>/gi)){
|
|
116
|
+
const value = match[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
117
|
+
if (value) locations.push(value);
|
|
118
|
+
if (locations.length >= limit) break;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
index,
|
|
122
|
+
locations
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=site-import-robots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-robots.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 * ROBOTS.TXT AND SITEMAPS, FOR AGLYNBOT (AGL-3974).\n *\n * The site-import crawler asks a site's robots.txt before every page, as\n * RFC 9309 describes it: the group naming `AglynBot` if there is one, else\n * the `*` group; the longest matching rule decides, and `Allow` wins a tie;\n * `*` matches any run and a trailing `$` anchors the end. A site that keeps\n * a path from crawlers keeps it from this one, even though the person asking\n * owns the site: the file is the site's own statement of what a robot may\n * read, and the person can change it.\n *\n * Sitemaps are read for their `<loc>` addresses, and a sitemap index for the\n * sitemaps it lists. Gzipped sitemaps are not read.\n *=========================================*/\n\n/** The product token AglynBot matches robots.txt groups by. */\nexport const AGLYN_BOT_TOKEN = 'aglynbot'\n\nexport interface RobotsRule {\n allow: boolean\n /** The path pattern as written, `*` and `$` included. */\n pattern: string\n}\n\nexport interface RobotsGroup {\n /** Lowercased product tokens, version suffixes removed. */\n agents: string[]\n rules: RobotsRule[]\n}\n\nexport interface RobotsTxt {\n groups: RobotsGroup[]\n /** Every `Sitemap:` address, as written. */\n sitemaps: string[]\n}\n\n/** robots.txt as groups and sitemaps; unknown lines are skipped. */\nexport function parseRobotsTxt(text: string): RobotsTxt {\n const groups: RobotsGroup[] = []\n const sitemaps: string[] = []\n let group: RobotsGroup | null = null\n let lastWasAgent = false\n for (const rawLine of String(text ?? '').split(/\\r\\n|\\r|\\n/)) {\n const line = rawLine.replace(/#.*$/, '').trim()\n if (!line) continue\n const colon = line.indexOf(':')\n if (colon <= 0) continue\n const key = line.slice(0, colon).trim().toLowerCase()\n const value = line.slice(colon + 1).trim()\n if (key === 'sitemap') {\n if (value) sitemaps.push(value)\n continue\n }\n if (key === 'user-agent') {\n const agent = value.toLowerCase().split('/')[0].trim()\n if (!group || !lastWasAgent) {\n group = { agents: [], rules: [] }\n groups.push(group)\n }\n if (agent) group.agents.push(agent)\n lastWasAgent = true\n continue\n }\n lastWasAgent = false\n if (!group) continue\n if (key === 'allow' || key === 'disallow') {\n // An empty Disallow allows everything, which is what no rule does.\n if (!value) continue\n group.rules.push({ allow: key === 'allow', pattern: value })\n }\n }\n return { groups, sitemaps }\n}\n\n/**\n * The rules that apply to `token`: every group naming it, merged; when none\n * does, every `*` group, merged; when neither exists, none (all allowed).\n */\nexport function robotsRulesFor(robots: RobotsTxt, token: string = AGLYN_BOT_TOKEN): RobotsRule[] {\n const own = robots.groups.filter((group) => group.agents.includes(token))\n const chosen = own.length ? own : robots.groups.filter((group) => group.agents.includes('*'))\n return chosen.flatMap((group) => group.rules)\n}\n\nfunction patternMatchLength(pattern: string, path: string): number {\n const anchored = pattern.endsWith('$')\n const body = anchored ? pattern.slice(0, -1) : pattern\n const source = body\n .split('*')\n .map((part) => part.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&'))\n .join('.*')\n const regex = new RegExp(`^${source}${anchored ? '$' : ''}`)\n return regex.test(path) ? body.length : -1\n}\n\n/** A path with a percent-encoding case that compares the way robots.txt writes it. */\nfunction comparablePath(path: string): string {\n return path.replace(/%[0-9a-f]{2}/gi, (escape) => escape.toUpperCase())\n}\n\n/**\n * Whether `pathWithQuery` (`/a/b?c=d`) may be crawled under `rules`.\n * `/robots.txt` itself is always allowed.\n */\nexport function robotsAllows(rules: readonly RobotsRule[], pathWithQuery: string): boolean {\n const path = comparablePath(pathWithQuery || '/')\n if (path === '/robots.txt') return true\n let best = -1\n let allowed = true\n for (const rule of rules) {\n const length = patternMatchLength(comparablePath(rule.pattern), path)\n if (length < 0) continue\n if (length > best || (length === best && rule.allow)) {\n best = length\n allowed = rule.allow\n }\n }\n return allowed\n}\n\nexport interface SitemapEntries {\n /** True for a `<sitemapindex>`: `locations` are sitemaps, not pages. */\n index: boolean\n locations: string[]\n}\n\n/** The `<loc>` addresses of a sitemap or a sitemap index, entities decoded. */\nexport function parseSitemap(xml: string, limit = 5_000): SitemapEntries {\n const source = String(xml ?? '')\n const index = /<sitemapindex\\b/i.test(source)\n const locations: string[] = []\n for (const match of source.matchAll(/<loc\\b[^>]*>([\\s\\S]*?)<\\/loc\\s*>/gi)) {\n const value = match[1]\n .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, '$1')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim()\n if (value) locations.push(value)\n if (locations.length >= limit) break\n }\n return { index, locations }\n}\n"],"names":["AGLYN_BOT_TOKEN","parseRobotsTxt","text","groups","sitemaps","group","lastWasAgent","rawLine","String","split","line","replace","trim","colon","indexOf","key","slice","toLowerCase","value","push","agent","agents","rules","allow","pattern","robotsRulesFor","robots","token","own","filter","includes","chosen","length","flatMap","patternMatchLength","path","anchored","endsWith","body","source","map","part","join","regex","RegExp","test","comparablePath","escape","toUpperCase","robotsAllows","pathWithQuery","best","allowed","rule","parseSitemap","xml","limit","index","locations","match","matchAll"],"mappings":"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"}
|