@aglyn/plugins-ai 1.0.0-beta.292 → 1.0.0-beta.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +5 -1
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-page.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 * ONE CRAWLED PAGE, AS AN IMPORT KEEPS IT (AGL-3976).\n *\n * The crawler's page (`SiteImportPage`) holds everything it extracted; an\n * import keeps much less, and keeps it in two shapes:\n *\n * - **Listed**, for the inventory: its address, title, how it was found and\n * how many words and photos it has. No text: an inventory of a whole site\n * rides the job document.\n * - **Read**, for a wave: its text in order, its sections from its headings,\n * and its photos' addresses. The text is kept as the page says it and is\n * fenced as untrusted data only where a prompt shows it; the section names\n * and the title travel outside the fence, so they are neutralized (no\n * fence marker, no hidden character) and cut short. Every block of its\n * main region rides along too (AGL-4083), so a page can be brought in word\n * for word.\n *\n * And from the home page, what the site's header and footer are made of\n * (AGL-4083): its brand as the source draws it (`siteImportHeaderBrand`) and\n * its own facts (`siteImportSiteFacts`), for the header and footer the import\n * builds.\n *=========================================*/\n\nimport { neutralizeUntrustedText } from '@aglyn/shared-util-tools/untrusted-text'\nimport {\n AI_SITE_IMPORT_BLOCKED_STATUSES,\n AI_SITE_IMPORT_IMAGES_PER_PAGE,\n AI_SITE_IMPORT_LABEL_MAX_CHARS,\n AI_SITE_IMPORT_MAX_SECTIONS,\n AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS,\n AI_SITE_IMPORT_READ_MAX_CHARS,\n aiSiteImportCapBlocks,\n type AiSiteImportLeftOutReason,\n type AiSiteImportListedPage,\n type AiSiteImportPageRead,\n} from '../model/ai-site-import'\nimport type { SiteImportPage, SiteImportSkipReason } from './site-import-crawl'\nimport type { SiteImportBlock, SiteImportOrganization } from './site-import-html'\nimport { siteImportLogoCandidates, type SiteImportLogoCandidate } from './site-import-images'\n\n/** A line of untrusted text safe to show outside a fence: neutralized, one line, cut short. */\nexport function siteImportLabel(text: string, max = AI_SITE_IMPORT_LABEL_MAX_CHARS): string {\n const line = neutralizeUntrustedText(text).replace(/\\s+/g, ' ').trim()\n return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line\n}\n\nconst wordsIn = (text: string) => (text.match(/[\\p{L}\\p{N}]+/gu) ?? []).length\n\n/** How many words a page says, its title and headings included; a link inside a sentence is counted once. */\nexport function siteImportPageWords(page: Pick<SiteImportPage, 'title' | 'blocks'>): number {\n return page.blocks.reduce(\n (total, block) => total + (block.kind === 'image' || (block.kind === 'link' && block.inline) ? 0 : wordsIn(block.text)),\n 0,\n )\n}\n\n/** Between a page's own name and its site's in a browser title: \"About | Rosie's Bakery\". */\nconst TITLE_SEPARATOR = /\\s+[|\\u2013\\u2014\\u00b7:-]\\s+/\n\n/**\n * A page's name on the new site: \"Home\" for the home page, else its browser\n * title without the old site's name after a separator (\"About | Rosie's\n * Bakery\" is \"About\"), else its first heading.\n */\nexport function siteImportPageTitle(page: Pick<SiteImportPage, 'path' | 'title' | 'blocks'>): string {\n if ((page.path.replace(/\\/+$/, '') || '/') === '/') return 'Home'\n const own = siteImportLabel((page.title ?? '').split(TITLE_SEPARATOR)[0] ?? '')\n if (own.length >= 2) return own\n return siteImportLabel(page.blocks.find((block) => block.kind === 'heading')?.text ?? '')\n}\n\n/** The page as the inventory lists it. */\nexport function siteImportListedPage(page: SiteImportPage): AiSiteImportListedPage {\n return {\n url: page.url,\n path: page.path,\n title: siteImportPageTitle(page),\n foundBy: page.foundBy,\n words: siteImportPageWords(page),\n images: page.images.length,\n }\n}\n\n/** The page's photos, the ones in its body first, each once. */\nexport function siteImportPageImages(page: Pick<SiteImportPage, 'blocks' | 'images'>): Array<{ url: string; alt: string }> {\n const seen = new Set<string>()\n const images: Array<{ url: string; alt: string }> = []\n const add = (url: string, alt: string) => {\n if (!url || seen.has(url) || images.length >= AI_SITE_IMPORT_IMAGES_PER_PAGE) return\n seen.add(url)\n images.push({ url, alt: siteImportLabel(alt, 250) })\n }\n for (const block of page.blocks) if (block.kind === 'image' && block.region === 'main') add(block.url, block.alt)\n for (const image of page.images) add(image.url, image.alt)\n return images\n}\n\n/**\n * The page's sections, from its headings: the first is its opening (its\n * title or first heading), then one for each second-level heading of its\n * body, at most `AI_SITE_IMPORT_MAX_SECTIONS`. A page with no headings below\n * its title but more to say than an opening gets a second section for it.\n */\nexport function siteImportPageSections(page: Pick<SiteImportPage, 'title' | 'blocks'>): string[] {\n const main = page.blocks.filter((block) => block.region === 'main')\n const headings = main.filter((block): block is Extract<typeof block, { kind: 'heading' }> => block.kind === 'heading')\n const opening = siteImportLabel(headings.find((block) => block.level === 1)?.text || page.title || '') || 'Introduction'\n const sections = [opening]\n const seen = new Set([opening.toLowerCase()])\n for (const heading of headings) {\n if (heading.level !== 2) continue\n const name = siteImportLabel(heading.text)\n if (!name || seen.has(name.toLowerCase())) continue\n seen.add(name.toLowerCase())\n sections.push(name)\n if (sections.length >= AI_SITE_IMPORT_MAX_SECTIONS) break\n }\n if (sections.length === 1 && siteImportPageWords({ title: '', blocks: main }) > 80) sections.push('Details')\n return sections\n}\n\n/** The page's text in order, as a prompt shows it inside its fence. */\nexport function siteImportPageText(page: Pick<SiteImportPage, 'title' | 'description' | 'blocks'>): string {\n const lines: string[] = []\n if (page.title) lines.push(`Title: ${page.title}`)\n if (page.description) lines.push(`Description: ${page.description}`)\n for (const block of page.blocks) {\n const where = block.region === 'main' ? '' : `(${block.region}) `\n if (block.kind === 'heading') lines.push(`${'#'.repeat(block.level)} ${block.text}`)\n else if (block.kind === 'image') lines.push(`${where}[photo: ${block.alt || 'no description'}]`)\n else if (block.kind === 'list-item') lines.push(`${where}- ${block.text}`)\n else if (block.kind === 'quote') lines.push(`${where}> ${block.text}`)\n else if (block.kind === 'link') {\n // A link inside a sentence is in its block's words already.\n if (!block.inline) lines.push(`${where}${block.text}`)\n } else lines.push(`${where}${block.text}`)\n }\n const text = lines.join('\\n')\n return text.length > AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS ? text.slice(0, AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS) : text\n}\n\n/** The page as a wave reads it: its main region's blocks whole, within the read's budget. */\nexport function siteImportPageRead(page: SiteImportPage, at: string): AiSiteImportPageRead {\n const main = page.blocks.filter((block) => block.region === 'main')\n const capped = aiSiteImportCapBlocks(main, AI_SITE_IMPORT_READ_MAX_CHARS)\n return {\n at,\n title: siteImportPageTitle(page),\n text: siteImportPageText(page),\n sections: siteImportPageSections(page),\n images: siteImportPageImages(page),\n blocks: capped.blocks,\n ...(capped.cut ? { cut: true } : {}),\n words: siteImportPageWords({ title: '', blocks: main }),\n forms: page.forms.kept,\n }\n}\n\n/*------------------------------------------\n * The home page's header and footer (AGL-4083)\n *-----------------------------------------*/\n\n/**\n * How the source's header draws its brand: its logo alone (aglyn.com), its\n * logo with its name beside it, or its name alone. `text` is the name as the\n * header prints it, or the site's own name when it prints nothing at all.\n */\nexport interface SiteImportHeaderBrand {\n kind: 'logo-only' | 'logo-and-name' | 'name-only'\n logo: SiteImportLogoCandidate | null\n /** Whether the header prints the name as words. */\n name: boolean\n text: string\n}\n\n/** The site's own name: its head's, its structured data's, else its home page's title before a separator. */\nexport function siteImportSiteName(home: Pick<SiteImportPage, 'siteName' | 'organization' | 'title'>): string {\n return siteImportLabel(home.siteName || home.organization?.name || (home.title ?? '').split(TITLE_SEPARATOR).pop() || '')\n}\n\n/** Words short enough to be a name, not a sentence. */\nconst looksLikeName = (text: string) => text.length >= 2 && text.length <= AI_SITE_IMPORT_LABEL_MAX_CHARS && text.split(/\\s+/).length <= 8\n\n/**\n * The home page's header brand (AGL-4083), as the source shows it: the link\n * home in its header, the picture inside it, and whether words sit beside\n * the picture, inside the link or next to it. A header with no picture link\n * home is the name alone.\n */\nexport function siteImportHeaderBrand(\n home: Pick<SiteImportPage, 'brand' | 'images' | 'icons' | 'siteName' | 'organization' | 'title' | 'blocks'>,\n): SiteImportHeaderBrand {\n const logo = siteImportLogoCandidates(home).find((candidate) => candidate.source === 'header') ?? null\n const siteName = siteImportSiteName(home)\n const linked = siteImportLabel(home.brand?.text ?? '')\n const beside = home.blocks.find(\n (block) =>\n block.region === 'header' &&\n (block.kind === 'paragraph' || block.kind === 'heading' || block.kind === 'link') &&\n !!siteName &&\n block.text.trim().toLowerCase() === siteName.toLowerCase(),\n )\n const printed = looksLikeName(linked) ? linked : beside && beside.kind !== 'image' ? siteImportLabel(beside.text) : ''\n if (logo && printed) return { kind: 'logo-and-name', logo, name: true, text: printed }\n if (logo) return { kind: 'logo-only', logo, name: false, text: '' }\n return { kind: 'name-only', logo: null, name: true, text: printed || siteName }\n}\n\n/** The facts a site's header and footer carry, as the source writes them. */\nexport interface SiteImportSiteFacts {\n header: SiteImportBlock[]\n footer: SiteImportBlock[]\n organization: SiteImportOrganization | null\n /** Email addresses, from its structured data and its `mailto:` links. */\n emails: string[]\n /** Phone numbers as written, from its structured data and its `tel:` links. */\n phones: string[]\n /** Its profiles on other sites: its structured data's `sameAs` and its links to them. */\n social: string[]\n}\n\nconst SOCIAL_HOSTS =\n /(?:^|\\.)(?:twitter\\.com|x\\.com|facebook\\.com|instagram\\.com|linkedin\\.com|youtube\\.com|tiktok\\.com|github\\.com|pinterest\\.com|threads\\.net|yelp\\.com|tripadvisor\\.com|nextdoor\\.com|vimeo\\.com|bsky\\.app|mastodon\\.social)$/i\n\n/**\n * The home page's own facts (AGL-4083), word for word: its header and footer\n * blocks, its Organization structured data, and the email addresses, phone\n * numbers and profiles they hold. Nothing here is invented or reworded; a\n * fact the page does not state is not here.\n */\nexport function siteImportSiteFacts(home: Pick<SiteImportPage, 'blocks' | 'organization'>): SiteImportSiteFacts {\n const header = home.blocks.filter((block) => block.region === 'header')\n const footer = home.blocks.filter((block) => block.region === 'footer')\n const organization = home.organization ?? null\n const emails: string[] = []\n const phones: string[] = []\n const social: string[] = []\n const add = (list: string[], value: string) => {\n const clean = value.trim()\n if (clean && !list.some((entry) => entry.toLowerCase() === clean.toLowerCase())) list.push(clean)\n }\n if (organization?.email) add(emails, organization.email.toLowerCase())\n if (organization?.telephone) add(phones, organization.telephone)\n for (const url of organization?.sameAs ?? []) add(social, url)\n for (const block of [...header, ...footer]) {\n if (block.kind !== 'link') continue\n if (block.href.startsWith('mailto:')) add(emails, block.href.slice('mailto:'.length))\n else if (block.href.startsWith('tel:')) add(phones, block.text)\n else {\n try {\n if (SOCIAL_HOSTS.test(new URL(block.href).hostname)) add(social, block.href)\n } catch {\n // A link that is not an address is no profile.\n }\n }\n }\n return { header, footer, organization, emails, phones, social }\n}\n\n/** Why the crawler could not read a page, as an import leaves it out; `status` is the answer an `http-error` got. */\nexport function siteImportLeftOutReason(reason: SiteImportSkipReason, status?: number): AiSiteImportLeftOutReason {\n switch (reason) {\n case 'login-form':\n return 'login-form'\n case 'robots':\n return 'robots'\n case 'too-large':\n return 'too-large'\n case 'off-origin-redirect':\n return 'off-site'\n case 'http-error':\n return status !== undefined && AI_SITE_IMPORT_BLOCKED_STATUSES.has(status) ? 'blocked' : 'unreadable'\n case 'not-reached':\n return 'not-reached'\n default:\n return 'unreadable'\n }\n}\n"],"names":["neutralizeUntrustedText","AI_SITE_IMPORT_BLOCKED_STATUSES","AI_SITE_IMPORT_IMAGES_PER_PAGE","AI_SITE_IMPORT_LABEL_MAX_CHARS","AI_SITE_IMPORT_MAX_SECTIONS","AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS","AI_SITE_IMPORT_READ_MAX_CHARS","aiSiteImportCapBlocks","siteImportLogoCandidates","siteImportLabel","text","max","line","replace","trim","length","slice","trimEnd","wordsIn","match","siteImportPageWords","page","blocks","reduce","total","block","kind","inline","TITLE_SEPARATOR","siteImportPageTitle","path","own","title","split","find","siteImportListedPage","url","foundBy","words","images","siteImportPageImages","seen","Set","add","alt","has","push","region","image","siteImportPageSections","headings","main","filter","opening","level","sections","toLowerCase","heading","name","siteImportPageText","lines","description","where","repeat","join","siteImportPageRead","at","capped","cut","forms","kept","siteImportSiteName","home","siteName","organization","pop","looksLikeName","siteImportHeaderBrand","logo","candidate","source","linked","brand","beside","printed","SOCIAL_HOSTS","siteImportSiteFacts","header","footer","emails","phones","social","list","value","clean","some","entry","email","telephone","sameAs","href","startsWith","test","URL","hostname","siteImportLeftOutReason","reason","status","undefined"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/site-import/site-import-page.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 * ONE CRAWLED PAGE, AS AN IMPORT KEEPS IT (AGL-3976).\n *\n * The crawler's page (`SiteImportPage`) holds everything it extracted; an\n * import keeps much less, and keeps it in two shapes:\n *\n * - **Listed**, for the inventory: its address, title, how it was found and\n * how many words and photos it has. No text: an inventory of a whole site\n * rides the job document.\n * - **Read**, for a wave: its text in order, its sections from its headings,\n * and its photos' addresses. The text is kept as the page says it and is\n * fenced as untrusted data only where a prompt shows it; the section names\n * and the title travel outside the fence, so they are neutralized (no\n * fence marker, no hidden character) and cut short. Every block of its\n * main region rides along too (AGL-4083), so a page can be brought in word\n * for word.\n *\n * And from the home page, what the site's header and footer are made of\n * (AGL-4083): its brand as the source draws it (`siteImportHeaderBrand`) and\n * its own facts (`siteImportSiteFacts`), for the header and footer the import\n * builds.\n *=========================================*/\n\nimport { neutralizeUntrustedText } from '@aglyn/shared-util-tools/untrusted-text'\nimport {\n AI_SITE_IMPORT_BLOCKED_STATUSES,\n AI_SITE_IMPORT_IMAGES_PER_PAGE,\n AI_SITE_IMPORT_LABEL_MAX_CHARS,\n AI_SITE_IMPORT_MAX_SECTIONS,\n AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS,\n AI_SITE_IMPORT_READ_MAX_CHARS,\n aiSiteImportCapBlocks,\n AI_SITE_IMPORT_LAYOUT_MAX_CHARS,\n siteImportLayoutChars,\n siteImportLayoutWithin,\n siteImportLayoutWithoutDrawings,\n type AiSiteImportLeftOutReason,\n type AiSiteImportListedPage,\n type AiSiteImportPageRead,\n} from '../model/ai-site-import'\nimport type { SiteImportPage, SiteImportSkipReason } from './site-import-crawl'\nimport type { SiteImportBlock, SiteImportOrganization } from './site-import-html'\nimport { siteImportLogoCandidates, type SiteImportLogoCandidate } from './site-import-images'\n\n/** A line of untrusted text safe to show outside a fence: neutralized, one line, cut short. */\nexport function siteImportLabel(text: string, max = AI_SITE_IMPORT_LABEL_MAX_CHARS): string {\n const line = neutralizeUntrustedText(text).replace(/\\s+/g, ' ').trim()\n return line.length > max ? `${line.slice(0, max - 1).trimEnd()}…` : line\n}\n\nconst wordsIn = (text: string) => (text.match(/[\\p{L}\\p{N}]+/gu) ?? []).length\n\n/** How many words a page says, its title and headings included; a link inside a sentence is counted once. */\nexport function siteImportPageWords(page: Pick<SiteImportPage, 'title' | 'blocks'>): number {\n return page.blocks.reduce(\n (total, block) => total + (block.kind === 'image' || (block.kind === 'link' && block.inline) ? 0 : wordsIn(block.text)),\n 0,\n )\n}\n\n/** Between a page's own name and its site's in a browser title: \"About | Rosie's Bakery\". */\nconst TITLE_SEPARATOR = /\\s+[|\\u2013\\u2014\\u00b7:-]\\s+/\n\n/**\n * A page's name on the new site: \"Home\" for the home page, else its browser\n * title without the old site's name after a separator (\"About | Rosie's\n * Bakery\" is \"About\"), else its first heading.\n */\nexport function siteImportPageTitle(page: Pick<SiteImportPage, 'path' | 'title' | 'blocks'>): string {\n if ((page.path.replace(/\\/+$/, '') || '/') === '/') return 'Home'\n const own = siteImportLabel((page.title ?? '').split(TITLE_SEPARATOR)[0] ?? '')\n if (own.length >= 2) return own\n return siteImportLabel(page.blocks.find((block) => block.kind === 'heading')?.text ?? '')\n}\n\n/** The page as the inventory lists it. */\nexport function siteImportListedPage(page: SiteImportPage): AiSiteImportListedPage {\n return {\n url: page.url,\n path: page.path,\n title: siteImportPageTitle(page),\n foundBy: page.foundBy,\n words: siteImportPageWords(page),\n images: page.images.length,\n }\n}\n\n/** The page's photos, the ones in its body first, each once. */\nexport function siteImportPageImages(page: Pick<SiteImportPage, 'blocks' | 'images'>): Array<{ url: string; alt: string }> {\n const seen = new Set<string>()\n const images: Array<{ url: string; alt: string }> = []\n const add = (url: string, alt: string) => {\n if (!url || seen.has(url) || images.length >= AI_SITE_IMPORT_IMAGES_PER_PAGE) return\n seen.add(url)\n images.push({ url, alt: siteImportLabel(alt, 250) })\n }\n for (const block of page.blocks) if (block.kind === 'image' && block.region === 'main') add(block.url, block.alt)\n for (const image of page.images) add(image.url, image.alt)\n return images\n}\n\n/**\n * The page's sections, from its headings: the first is its opening (its\n * title or first heading), then one for each second-level heading of its\n * body, at most `AI_SITE_IMPORT_MAX_SECTIONS`. A page with no headings below\n * its title but more to say than an opening gets a second section for it.\n */\nexport function siteImportPageSections(page: Pick<SiteImportPage, 'title' | 'blocks'>): string[] {\n const main = page.blocks.filter((block) => block.region === 'main')\n const headings = main.filter((block): block is Extract<typeof block, { kind: 'heading' }> => block.kind === 'heading')\n const opening = siteImportLabel(headings.find((block) => block.level === 1)?.text || page.title || '') || 'Introduction'\n const sections = [opening]\n const seen = new Set([opening.toLowerCase()])\n for (const heading of headings) {\n if (heading.level !== 2) continue\n const name = siteImportLabel(heading.text)\n if (!name || seen.has(name.toLowerCase())) continue\n seen.add(name.toLowerCase())\n sections.push(name)\n if (sections.length >= AI_SITE_IMPORT_MAX_SECTIONS) break\n }\n if (sections.length === 1 && siteImportPageWords({ title: '', blocks: main }) > 80) sections.push('Details')\n return sections\n}\n\n/** The page's text in order, as a prompt shows it inside its fence. */\nexport function siteImportPageText(page: Pick<SiteImportPage, 'title' | 'description' | 'blocks'>): string {\n const lines: string[] = []\n if (page.title) lines.push(`Title: ${page.title}`)\n if (page.description) lines.push(`Description: ${page.description}`)\n for (const block of page.blocks) {\n const where = block.region === 'main' ? '' : `(${block.region}) `\n if (block.kind === 'heading') lines.push(`${'#'.repeat(block.level)} ${block.text}`)\n else if (block.kind === 'image') lines.push(`${where}[photo: ${block.alt || 'no description'}]`)\n else if (block.kind === 'list-item') lines.push(`${where}- ${block.text}`)\n else if (block.kind === 'quote') lines.push(`${where}> ${block.text}`)\n else if (block.kind === 'link') {\n // A link inside a sentence is in its block's words already.\n if (!block.inline) lines.push(`${where}${block.text}`)\n } else lines.push(`${where}${block.text}`)\n }\n const text = lines.join('\\n')\n return text.length > AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS ? text.slice(0, AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS) : text\n}\n\n/**\n * The page as a wave reads it: its main region's blocks whole, within the\n * read's budget, and how the source lays them out (AGL-4106) over the blocks\n * kept, its icons' drawings dropped first when it runs past its own budget.\n */\nexport function siteImportPageRead(page: SiteImportPage, at: string): AiSiteImportPageRead {\n const main = page.blocks.filter((block) => block.region === 'main')\n const capped = aiSiteImportCapBlocks(main, AI_SITE_IMPORT_READ_MAX_CHARS)\n let layout = page.layout ? siteImportLayoutWithin(page.layout, capped.blocks.length) : null\n if (layout && siteImportLayoutChars(layout) > AI_SITE_IMPORT_LAYOUT_MAX_CHARS) layout = siteImportLayoutWithoutDrawings(layout)\n if (layout && siteImportLayoutChars(layout) > AI_SITE_IMPORT_LAYOUT_MAX_CHARS) layout = null\n return {\n at,\n title: siteImportPageTitle(page),\n text: siteImportPageText(page),\n sections: siteImportPageSections(page),\n images: siteImportPageImages(page),\n blocks: capped.blocks,\n ...(capped.cut ? { cut: true } : {}),\n words: siteImportPageWords({ title: '', blocks: main }),\n forms: page.forms.kept,\n ...(layout ? { layout } : {}),\n // The source's own title and description, for the page's listing (AGL-4107).\n meta: { title: siteImportMetaText(page.title, SITE_IMPORT_META_TITLE_MAX_CHARS), description: siteImportMetaText(page.description, SITE_IMPORT_META_DESCRIPTION_MAX_CHARS) },\n }\n}\n\n/** The most characters of a source's title and description a read keeps: far past what any listing shows. */\nexport const SITE_IMPORT_META_TITLE_MAX_CHARS = 300\nexport const SITE_IMPORT_META_DESCRIPTION_MAX_CHARS = 1_000\n\n/** A title or a description as the source writes it, one line, neutralized; a longer one is kept to its ceiling. */\nfunction siteImportMetaText(text: string | null | undefined, max: number): string {\n return neutralizeUntrustedText(String(text ?? '')).replace(/\\s+/g, ' ').trim().slice(0, max)\n}\n\n/*------------------------------------------\n * The home page's header and footer (AGL-4083)\n *-----------------------------------------*/\n\n/**\n * How the source's header draws its brand: its logo alone (aglyn.com), its\n * logo with its name beside it, or its name alone. `text` is the name as the\n * header prints it, or the site's own name when it prints nothing at all.\n */\nexport interface SiteImportHeaderBrand {\n kind: 'logo-only' | 'logo-and-name' | 'name-only'\n logo: SiteImportLogoCandidate | null\n /** Whether the header prints the name as words. */\n name: boolean\n text: string\n}\n\n/** The site's own name: its head's, its structured data's, else its home page's title before a separator. */\nexport function siteImportSiteName(home: Pick<SiteImportPage, 'siteName' | 'organization' | 'title'>): string {\n return siteImportLabel(home.siteName || home.organization?.name || (home.title ?? '').split(TITLE_SEPARATOR).pop() || '')\n}\n\n/** Words short enough to be a name, not a sentence. */\nconst looksLikeName = (text: string) => text.length >= 2 && text.length <= AI_SITE_IMPORT_LABEL_MAX_CHARS && text.split(/\\s+/).length <= 8\n\n/**\n * The home page's header brand (AGL-4083), as the source shows it: the link\n * home in its header, the picture inside it, and whether words sit beside\n * the picture, inside the link or next to it. A header with no picture link\n * home is the name alone.\n */\nexport function siteImportHeaderBrand(\n home: Pick<SiteImportPage, 'brand' | 'images' | 'icons' | 'siteName' | 'organization' | 'title' | 'blocks'>,\n): SiteImportHeaderBrand {\n const logo = siteImportLogoCandidates(home).find((candidate) => candidate.source === 'header') ?? null\n const siteName = siteImportSiteName(home)\n const linked = siteImportLabel(home.brand?.text ?? '')\n const beside = home.blocks.find(\n (block) =>\n block.region === 'header' &&\n (block.kind === 'paragraph' || block.kind === 'heading' || block.kind === 'link') &&\n !!siteName &&\n block.text.trim().toLowerCase() === siteName.toLowerCase(),\n )\n const printed = looksLikeName(linked) ? linked : beside && beside.kind !== 'image' ? siteImportLabel(beside.text) : ''\n if (logo && printed) return { kind: 'logo-and-name', logo, name: true, text: printed }\n if (logo) return { kind: 'logo-only', logo, name: false, text: '' }\n return { kind: 'name-only', logo: null, name: true, text: printed || siteName }\n}\n\n/** The facts a site's header and footer carry, as the source writes them. */\nexport interface SiteImportSiteFacts {\n header: SiteImportBlock[]\n footer: SiteImportBlock[]\n organization: SiteImportOrganization | null\n /** Email addresses, from its structured data and its `mailto:` links. */\n emails: string[]\n /** Phone numbers as written, from its structured data and its `tel:` links. */\n phones: string[]\n /** Its profiles on other sites: its structured data's `sameAs` and its links to them. */\n social: string[]\n}\n\nconst SOCIAL_HOSTS =\n /(?:^|\\.)(?:twitter\\.com|x\\.com|facebook\\.com|instagram\\.com|linkedin\\.com|youtube\\.com|tiktok\\.com|github\\.com|pinterest\\.com|threads\\.net|yelp\\.com|tripadvisor\\.com|nextdoor\\.com|vimeo\\.com|bsky\\.app|mastodon\\.social)$/i\n\n/**\n * The home page's own facts (AGL-4083), word for word: its header and footer\n * blocks, its Organization structured data, and the email addresses, phone\n * numbers and profiles they hold. Nothing here is invented or reworded; a\n * fact the page does not state is not here.\n */\nexport function siteImportSiteFacts(home: Pick<SiteImportPage, 'blocks' | 'organization'>): SiteImportSiteFacts {\n const header = home.blocks.filter((block) => block.region === 'header')\n const footer = home.blocks.filter((block) => block.region === 'footer')\n const organization = home.organization ?? null\n const emails: string[] = []\n const phones: string[] = []\n const social: string[] = []\n const add = (list: string[], value: string) => {\n const clean = value.trim()\n if (clean && !list.some((entry) => entry.toLowerCase() === clean.toLowerCase())) list.push(clean)\n }\n if (organization?.email) add(emails, organization.email.toLowerCase())\n if (organization?.telephone) add(phones, organization.telephone)\n for (const url of organization?.sameAs ?? []) add(social, url)\n for (const block of [...header, ...footer]) {\n if (block.kind !== 'link') continue\n if (block.href.startsWith('mailto:')) add(emails, block.href.slice('mailto:'.length))\n else if (block.href.startsWith('tel:')) add(phones, block.text)\n else {\n try {\n if (SOCIAL_HOSTS.test(new URL(block.href).hostname)) add(social, block.href)\n } catch {\n // A link that is not an address is no profile.\n }\n }\n }\n return { header, footer, organization, emails, phones, social }\n}\n\n/** Why the crawler could not read a page, as an import leaves it out; `status` is the answer an `http-error` got. */\nexport function siteImportLeftOutReason(reason: SiteImportSkipReason, status?: number): AiSiteImportLeftOutReason {\n switch (reason) {\n case 'login-form':\n return 'login-form'\n case 'robots':\n return 'robots'\n case 'too-large':\n return 'too-large'\n case 'off-origin-redirect':\n return 'off-site'\n case 'http-error':\n return status !== undefined && AI_SITE_IMPORT_BLOCKED_STATUSES.has(status) ? 'blocked' : 'unreadable'\n case 'not-reached':\n return 'not-reached'\n case 'not-html':\n return 'file'\n default:\n return 'unreadable'\n }\n}\n"],"names":["neutralizeUntrustedText","AI_SITE_IMPORT_BLOCKED_STATUSES","AI_SITE_IMPORT_IMAGES_PER_PAGE","AI_SITE_IMPORT_LABEL_MAX_CHARS","AI_SITE_IMPORT_MAX_SECTIONS","AI_SITE_IMPORT_PAGE_TEXT_MAX_CHARS","AI_SITE_IMPORT_READ_MAX_CHARS","aiSiteImportCapBlocks","AI_SITE_IMPORT_LAYOUT_MAX_CHARS","siteImportLayoutChars","siteImportLayoutWithin","siteImportLayoutWithoutDrawings","siteImportLogoCandidates","siteImportLabel","text","max","line","replace","trim","length","slice","trimEnd","wordsIn","match","siteImportPageWords","page","blocks","reduce","total","block","kind","inline","TITLE_SEPARATOR","siteImportPageTitle","path","own","title","split","find","siteImportListedPage","url","foundBy","words","images","siteImportPageImages","seen","Set","add","alt","has","push","region","image","siteImportPageSections","headings","main","filter","opening","level","sections","toLowerCase","heading","name","siteImportPageText","lines","description","where","repeat","join","siteImportPageRead","at","capped","layout","cut","forms","kept","meta","siteImportMetaText","SITE_IMPORT_META_TITLE_MAX_CHARS","SITE_IMPORT_META_DESCRIPTION_MAX_CHARS","String","siteImportSiteName","home","siteName","organization","pop","looksLikeName","siteImportHeaderBrand","logo","candidate","source","linked","brand","beside","printed","SOCIAL_HOSTS","siteImportSiteFacts","header","footer","emails","phones","social","list","value","clean","some","entry","email","telephone","sameAs","href","startsWith","test","URL","hostname","siteImportLeftOutReason","reason","status","undefined"],"mappings":"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"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
import { type AiSiteImportState } from '../model/ai-site-import';
|
|
18
|
-
import type { AiJobItemLedger } from '../model/ai-jobs.types';
|
|
19
|
-
/** The abuse queue's `source` for a site import's row. */
|
|
20
|
-
export declare const SITE_IMPORT_REVIEW_SOURCE = "site-import";
|
|
21
|
-
/** One row per import job. */
|
|
22
|
-
export declare function siteImportReviewId(jobId: string): string;
|
|
23
|
-
/** The reference staff quote: `SI-` and the job's id. */
|
|
24
|
-
export declare function siteImportReviewReference(jobId: string): string;
|
|
25
|
-
export type SiteImportReviewEvent = 'read' | 'wave-started' | 'wave-finished';
|
|
26
|
-
export interface SiteImportReviewInput {
|
|
27
|
-
orgId: string;
|
|
28
|
-
hostId: string;
|
|
29
|
-
jobId: string;
|
|
30
|
-
state: AiSiteImportState;
|
|
31
|
-
items: readonly AiJobItemLedger[];
|
|
32
|
-
event: SiteImportReviewEvent;
|
|
33
|
-
/** The wave the event is about; absent on `read`. */
|
|
34
|
-
wave?: number;
|
|
35
|
-
nowMs: number;
|
|
36
|
-
}
|
|
37
|
-
/** The row's details: what was proved, read, kept, left out and brought in, rebuilt whole each time. */
|
|
38
|
-
export declare function siteImportReviewDetails(input: SiteImportReviewInput): string;
|
|
39
|
-
/** Writes (or rewrites) the import's row in the abuse queue. Never throws. */
|
|
40
|
-
export declare function fileSiteImportReview(firestore: FirebaseFirestore.Firestore, input: SiteImportReviewInput): Promise<string | null>;
|
|
@@ -1,149 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|