@aglyn/shared-util-email 1.0.0-beta.202 → 1.0.0-beta.204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +3 -3
  2. package/src/index.d.ts +3 -0
  3. package/src/index.js +9 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/email-design-chrome.d.ts +61 -0
  6. package/src/lib/email-design-chrome.js +99 -0
  7. package/src/lib/email-design-chrome.js.map +1 -0
  8. package/src/lib/email-linkify.d.ts +3 -1
  9. package/src/lib/email-linkify.js +2 -2
  10. package/src/lib/email-linkify.js.map +1 -1
  11. package/src/lib/email-palette.d.ts +137 -0
  12. package/src/lib/email-palette.js +756 -0
  13. package/src/lib/email-palette.js.map +1 -0
  14. package/src/lib/email-render.d.ts +25 -0
  15. package/src/lib/email-render.js +27 -9
  16. package/src/lib/email-render.js.map +1 -1
  17. package/src/lib/held-page.d.ts +117 -0
  18. package/src/lib/held-page.js +172 -0
  19. package/src/lib/held-page.js.map +1 -0
  20. package/src/lib/host-email-render.d.ts +34 -7
  21. package/src/lib/host-email-render.js +69 -25
  22. package/src/lib/host-email-render.js.map +1 -1
  23. package/src/lib/hosted-page-screen.js +8 -6
  24. package/src/lib/hosted-page-screen.js.map +1 -1
  25. package/src/lib/outbound-phishing-screen.d.ts +24 -0
  26. package/src/lib/outbound-phishing-screen.js +81 -2
  27. package/src/lib/outbound-phishing-screen.js.map +1 -1
  28. package/src/lib/risk-notice-catalog.d.ts +204 -0
  29. package/src/lib/risk-notice-catalog.js +1702 -0
  30. package/src/lib/risk-notice-catalog.js.map +1 -0
  31. package/src/lib/risk-notice-emails.d.ts +25 -0
  32. package/src/lib/risk-notice-emails.js +182 -0
  33. package/src/lib/risk-notice-emails.js.map +1 -0
  34. package/src/lib/sender-readiness.d.ts +155 -0
  35. package/src/lib/sender-readiness.js +432 -0
  36. package/src/lib/sender-readiness.js.map +1 -0
  37. package/src/lib/system-email-catalog.js +7 -1
  38. package/src/lib/system-email-catalog.js.map +1 -1
  39. package/src/lib/tenant-email-catalog.d.ts +7 -0
  40. package/src/lib/tenant-email-catalog.js +183 -13
  41. package/src/lib/tenant-email-catalog.js.map +1 -1
  42. package/src/lib/text-email-html.d.ts +26 -0
  43. package/src/lib/text-email-html.js +56 -1
  44. package/src/lib/text-email-html.js.map +1 -1
@@ -46,7 +46,7 @@
46
46
  *
47
47
  * Pure: the caller hands over the composed node tree and the workspace's own
48
48
  * names and hosts, and decides what a hold does.
49
- *=========================================*/ import { isWorkspaceOwnBrand, linkHostsIn, lookalikeBrandForHost, PHISHING_LURE_PATTERNS, PHISHING_SCREEN_BRANDS, registrableDomain, isAnyOfficialBrandDomain, isCommonLinkDomain, squashScreenText } from "./outbound-phishing-screen.js";
49
+ *=========================================*/ import { isWorkspaceOwnBrand, linkHostsIn, isBrandsOwnDomain, lookalikeBrandForHost, PHISHING_LURE_PATTERNS, PHISHING_SCREEN_BRANDS, registrableDomain, isAnyOfficialBrandDomain, isCommonLinkDomain, squashScreenText } from "./outbound-phishing-screen.js";
50
50
  /** The most a page's text is read to, so a huge page costs a bounded scan. */ const MAX_PAGE_TEXT = 2000000;
51
51
  /** The most nodes the walk visits. */ const MAX_NODES = 20000;
52
52
  /** Props that describe a field the author defined, rather than content. */ const FIELD_PROP_KEYS = [
@@ -244,11 +244,13 @@
244
244
  const linkHosts = linkHostsIn(pageText);
245
245
  for (const host of linkHosts){
246
246
  const brand = lookalikeBrandForHost(host);
247
- if (brand) signals.push({
248
- code: 'lookalike-link',
249
- brand: brand.id,
250
- host
251
- });
247
+ if (brand && !isBrandsOwnDomain(brand, ownRegistrables)) {
248
+ signals.push({
249
+ code: 'lookalike-link',
250
+ brand: brand.id,
251
+ host
252
+ });
253
+ }
252
254
  }
253
255
  // 2. A credential field the page's author defined.
254
256
  const credentialSeen = new Set();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/hosted-page-screen.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 * THE HOSTED PAGE SCREEN (AGL-3362).\n *\n * The outbound phishing screen, read over a PUBLISHED PAGE instead of an\n * email: the same brand list, the same lookalike logic, the same tiers\n * (`signalsThatHold`). A fraud actor who could not mail a lure could still\n * host one, and the incident's sibling site published a page at the\n * document-share path `/reviewfile`.\n *\n * Three shapes, one per requirement:\n *\n * 1. **A lookalike link or embed** (`lookalike-link`, STRONG) — any `href`,\n * `src`, form action or URL in the page's authored content whose host\n * wears a brand without being the brand's domain.\n * 2. **A credential field** (`credential-field`, STRONG) — a field the\n * merchant AUTHORED that asks for a password, a card number or a\n * one-time code: its type, its autocomplete token, or its label,\n * placeholder or name. The platform's own sign-in, account and checkout\n * elements render their inputs in code, not in stored props, so they are\n * never read as a field here — which is exactly the sanctioned/unsanctioned\n * line: what the platform collects through its own components is\n * Aglyn's to protect, and what a page's author collects in a form of\n * their own is not something a site may ask a stranger for.\n * 3. **A brand's call to action** (`brand-action-page`, SOFT) — one element\n * whose text names a brand that is not the workspace's AND asks the reader\n * to sign in, verify, confirm or open a document, on a page that links\n * somewhere that is neither the workspace's, nor a brand's own, nor a\n * common social or maps link. The same element, not the same page: a\n * hotel that says \"find us on Booking.com\" in one place and \"sign in\" in\n * another is two ordinary sentences.\n *\n * Pure: the caller hands over the composed node tree and the workspace's own\n * names and hosts, and decides what a hold does.\n *=========================================*/\n\nimport {\n isWorkspaceOwnBrand,\n linkHostsIn,\n lookalikeBrandForHost,\n PHISHING_LURE_PATTERNS,\n PHISHING_SCREEN_BRANDS,\n type PhishingScreenSignal,\n registrableDomain,\n isAnyOfficialBrandDomain,\n isCommonLinkDomain,\n squashScreenText,\n} from './outbound-phishing-screen'\n\nexport interface HostedPageScreenInput {\n /**\n * The page as it will be served — the composed tree, or any object that\n * holds nodes (`{ componentId, props }`) at any depth. Walked, never\n * trusted to have one shape: a screen, a layout's chrome, a grafted\n * component and a form all arrive together.\n */\n nodes: unknown\n /** The workspace's own names, as `screenOutboundEmail` reads them. */\n ownNames?: readonly (string | null | undefined)[]\n /** The workspace's own hosts — a link to one is never \"elsewhere\". */\n ownDomains?: readonly (string | null | undefined)[]\n}\n\nexport interface HostedPageScreenVerdict {\n signals: PhishingScreenSignal[]\n}\n\n/** A node as the walk finds it. */\ninterface WalkedNode {\n componentId: string\n props: Record<string, unknown>\n}\n\n/** The most a page's text is read to, so a huge page costs a bounded scan. */\nconst MAX_PAGE_TEXT = 2_000_000\n/** The most nodes the walk visits. */\nconst MAX_NODES = 20_000\n\n/** Props that describe a field the author defined, rather than content. */\nconst FIELD_PROP_KEYS = ['fieldType', 'fieldName', 'inputType', 'autoComplete', 'autocomplete']\n\n/** Autocomplete tokens (WHATWG) that name a credential. */\nconst CREDENTIAL_AUTOCOMPLETE: Record<string, 'password' | 'card' | 'otp'> = {\n 'current-password': 'password',\n 'new-password': 'password',\n 'cc-number': 'card',\n 'cc-csc': 'card',\n 'cc-exp': 'card',\n 'one-time-code': 'otp',\n}\n\n/** Field wording that asks for a credential, by kind. */\nconst CREDENTIAL_LABELS: ReadonlyArray<{ field: 'password' | 'card' | 'otp'; pattern: RegExp }> = [\n { field: 'password', pattern: /\\bpass\\s?(?:word|code|phrase)s?\\b|\\bpasswd\\b|\\bpin\\s*(?:code|number)?\\s*$/i },\n {\n field: 'card',\n pattern:\n /\\b(?:credit|debit)\\s*card\\b|\\bcard\\s*(?:number|no\\.?|#)|\\bcardnumber\\b|\\bcvv2?\\b|\\bcvc\\b|\\bcard\\s*security\\s*code\\b|\\bcard\\s*expir/i,\n },\n {\n field: 'otp',\n pattern:\n /\\bone[-\\s]?time\\s*(?:pass\\s?code|password|code|pin)\\b|\\botp\\b|\\bverification\\s*code\\b|\\b2fa\\b|\\btwo[-\\s]?factor\\b|\\bauthenticat(?:ion|or)\\s*code\\b|\\bsms\\s*code\\b/i,\n },\n]\n\n/** Raw HTML an author placed that holds a credential input. */\nconst HTML_CREDENTIAL_INPUTS: ReadonlyArray<{ field: 'password' | 'card' | 'otp'; pattern: RegExp }> = [\n { field: 'password', pattern: /<input\\b[^>]*\\btype\\s*=\\s*[\"']?password/i },\n { field: 'password', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:pass(?:word|wd)?|current-password|new-password)\\b/i },\n { field: 'card', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:card_?number|cc-number|cc-csc|cvv|cvc)\\b/i },\n { field: 'otp', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:otp|one-time-code|verification_?code)\\b/i },\n]\n\n/**\n * What an element asks its reader to DO with an account or a document —\n * counted only beside a brand in the SAME element. The email lure phrasing\n * counts too.\n */\nconst PAGE_ACTION_PATTERNS: readonly RegExp[] = [\n /\\b(?:sign|log)[\\s-]?in\\b/i,\n /\\bverify\\b/i,\n /\\bconfirm\\s+(?:your|the)\\b/i,\n /\\bunlock\\b/i,\n /\\benter\\s+your\\b/i,\n /\\b(?:view|review|open|download)\\s+(?:the\\s+|your\\s+)?(?:shared\\s+)?(?:file|document|invoice|statement)s?\\b/i,\n]\n\n/** Every node in the tree, breadth-first in document order, bounded. */\nfunction walkNodes(root: unknown): WalkedNode[] {\n const found: WalkedNode[] = []\n const seen = new Set<unknown>()\n const stack: unknown[] = [root]\n for (let head = 0; head < stack.length && found.length < MAX_NODES; head += 1) {\n const value = stack[head]\n if (!value || typeof value !== 'object' || seen.has(value)) continue\n seen.add(value)\n const record = value as Record<string, unknown>\n if (typeof record['componentId'] === 'string') {\n const props = record['props']\n found.push({\n componentId: record['componentId'] as string,\n props: props && typeof props === 'object' ? (props as Record<string, unknown>) : {},\n })\n }\n for (const child of Array.isArray(value) ? value : Object.values(record)) {\n if (child && typeof child === 'object') stack.push(child)\n }\n }\n return found\n}\n\n/** Every string a node's props hold, one level of arrays and objects deep. */\nfunction propStrings(props: Record<string, unknown>, skip: ReadonlySet<string> = new Set()): string[] {\n const out: string[] = []\n const visit = (value: unknown, depth: number) => {\n if (typeof value === 'string') out.push(value)\n else if (depth < 3 && value && typeof value === 'object') {\n for (const entry of Array.isArray(value) ? value : Object.values(value as Record<string, unknown>)) {\n visit(entry, depth + 1)\n }\n }\n }\n for (const [key, value] of Object.entries(props)) {\n if (key === 'sx' || skip.has(key)) continue\n visit(value, 0)\n }\n return out\n}\n\n/** Does this node describe a field its author defined? */\nfunction isAuthoredField(node: WalkedNode): boolean {\n if (/field|input/i.test(node.componentId)) return true\n return FIELD_PROP_KEYS.some((key) => node.props[key] !== undefined)\n}\n\n/** The credential a field asks for, or null. */\nfunction credentialOf(node: WalkedNode): { field: 'password' | 'card' | 'otp'; label: string } | null {\n const text = (key: string) => (typeof node.props[key] === 'string' ? (node.props[key] as string) : '')\n const type = [text('fieldType'), text('type'), text('inputType')].join(' ').toLowerCase()\n const label = text('label') || text('placeholder') || text('fieldName') || text('name')\n if (/\\bpassword\\b/.test(type)) return { field: 'password', label: label || 'password' }\n const autocomplete = (text('autoComplete') || text('autocomplete')).toLowerCase().trim()\n const byToken = CREDENTIAL_AUTOCOMPLETE[autocomplete]\n if (byToken) return { field: byToken, label: label || autocomplete }\n const wording = [text('label'), text('placeholder'), text('fieldName'), text('name'), text('helperText')]\n .filter(Boolean)\n .join(' ')\n for (const { field, pattern } of CREDENTIAL_LABELS) {\n if (pattern.test(wording)) return { field, label: (label || wording).slice(0, 80) }\n }\n return null\n}\n\n/**\n * The credential an input asks for, read from the two attributes that decide\n * it in the browser — its `type` and its `autocomplete` token — or null.\n * For code that builds inputs rather than storing them as page props: the\n * marketplace verifier reads a plugin bundle's `{ type: 'password' }` and\n * `setAttribute('autocomplete', 'cc-number')` with this (AGL-3362).\n */\nexport function credentialForInputAttribute(\n attribute: string,\n value: string,\n): 'password' | 'card' | 'otp' | null {\n const name = String(attribute ?? '').toLowerCase()\n const token = String(value ?? '').trim().toLowerCase()\n if ((name === 'type' || name === 'inputtype') && token === 'password') return 'password'\n if (name === 'autocomplete') return CREDENTIAL_AUTOCOMPLETE[token] ?? null\n return null\n}\n\n/** The credential a piece of raw HTML asks for in an input of its own, or null. */\nexport function credentialInHtml(html: string): 'password' | 'card' | 'otp' | null {\n const text = String(html ?? '')\n if (!text.includes('<')) return null\n return HTML_CREDENTIAL_INPUTS.find(({ pattern }) => pattern.test(text))?.field ?? null\n}\n\n/**\n * Does a submitted field's NAME ask for a credential — a password, a card\n * number or security code, a one-time code? The same wording the page\n * screen holds a field for, read over a name as a form posts it\n * (`card_number`, `cardNumber`, `one-time-code`).\n *\n * For the one path that does not pass a publish: a public form endpoint\n * takes whatever field names a request carries, so a page the screen never\n * saw (third-party code, a hand-built request) could still post a password\n * to it. The endpoint drops such a field rather than store it.\n */\nexport function isCredentialFieldName(name: string): boolean {\n const words = String(name ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_\\-.[\\]]+/g, ' ')\n .trim()\n if (!words) return false\n if (CREDENTIAL_AUTOCOMPLETE[words.toLowerCase().replace(/\\s+/g, '-')]) return true\n if (/^(?:pass|pwd|ccnum|cc\\s?num|cc\\s?number|cvv2?|cvc|csc)$/i.test(words)) return true\n return CREDENTIAL_LABELS.some(({ pattern }) => pattern.test(words))\n}\n\n/** URLs a page carries in a form Links-by-shape would miss: `//host/…`. */\nfunction protocolRelativeUrls(strings: readonly string[]): string {\n return strings\n .filter((value) => /^\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)+/i.test(value.trim()))\n .map((value) => `https:${value.trim()}`)\n .join('\\n')\n}\n\n/**\n * Screen one page as it will be served. The caller applies the tiers with\n * `signalsThatHold` and decides what a hold does.\n */\nexport function screenHostedPage(input: HostedPageScreenInput): HostedPageScreenVerdict {\n const signals: PhishingScreenSignal[] = []\n const nodes = walkNodes(input.nodes)\n const ownNames = squashScreenText((input.ownNames ?? []).filter(Boolean).join(' '))\n const ownHosts = (input.ownDomains ?? [])\n .map((domain) => String(domain ?? '').trim().toLowerCase().replace(/\\.+$/, ''))\n .filter((domain) => domain.includes('.'))\n const ownRegistrables = new Set(ownHosts.map(registrableDomain))\n const isOwnHost = (host: string) => ownHosts.some((own) => host === own || host.endsWith(`.${own}`))\n\n const nodeStrings = nodes.map((node) => propStrings(node.props))\n let pageText = nodeStrings.map((strings) => strings.join('\\n')).join('\\n')\n pageText = `${pageText}\\n${protocolRelativeUrls(nodeStrings.flat())}`.slice(0, MAX_PAGE_TEXT)\n\n // 1. A lookalike host anywhere the page links, embeds or posts to.\n const linkHosts = linkHostsIn(pageText)\n for (const host of linkHosts) {\n const brand = lookalikeBrandForHost(host)\n if (brand) signals.push({ code: 'lookalike-link', brand: brand.id, host })\n }\n\n // 2. A credential field the page's author defined.\n const credentialSeen = new Set<string>()\n const noteCredential = (field: 'password' | 'card' | 'otp', label: string) => {\n if (credentialSeen.has(field)) return\n credentialSeen.add(field)\n signals.push({ code: 'credential-field', field, label: label.slice(0, 80) })\n }\n nodes.forEach((node, index) => {\n if (isAuthoredField(node)) {\n const credential = credentialOf(node)\n if (credential) noteCredential(credential.field, credential.label)\n }\n for (const html of nodeStrings[index]) {\n if (!html.includes('<')) continue\n for (const { field, pattern } of HTML_CREDENTIAL_INPUTS) {\n if (pattern.test(html)) noteCredential(field, 'an input in the page’s own HTML')\n }\n }\n })\n\n // 3. A brand's call to action, in one element, on a page that links away.\n const foreign = linkHosts.find((host) => {\n if (isOwnHost(host)) return false\n const registrable = registrableDomain(host)\n return !isAnyOfficialBrandDomain(registrable) && !isCommonLinkDomain(registrable)\n })\n if (foreign) {\n const actions = [...PAGE_ACTION_PATTERNS, ...PHISHING_LURE_PATTERNS]\n const named = new Set<string>()\n for (const strings of nodeStrings) {\n const text = strings.join(' ')\n if (!text) continue\n const action = actions.map((pattern) => text.match(pattern)?.[0]).find(Boolean)\n if (!action) continue\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (named.has(brand.id)) continue\n if (brand.mention.test(text) && !isWorkspaceOwnBrand(brand, ownNames, ownRegistrables)) {\n named.add(brand.id)\n signals.push({ code: 'brand-action-page', brand: brand.id, action: action.slice(0, 80) })\n }\n }\n }\n }\n\n return { signals }\n}\n"],"names":["isWorkspaceOwnBrand","linkHostsIn","lookalikeBrandForHost","PHISHING_LURE_PATTERNS","PHISHING_SCREEN_BRANDS","registrableDomain","isAnyOfficialBrandDomain","isCommonLinkDomain","squashScreenText","MAX_PAGE_TEXT","MAX_NODES","FIELD_PROP_KEYS","CREDENTIAL_AUTOCOMPLETE","CREDENTIAL_LABELS","field","pattern","HTML_CREDENTIAL_INPUTS","PAGE_ACTION_PATTERNS","walkNodes","root","found","seen","Set","stack","head","length","value","has","add","record","props","push","componentId","child","Array","isArray","Object","values","propStrings","skip","out","visit","depth","entry","key","entries","isAuthoredField","node","test","some","undefined","credentialOf","text","type","join","toLowerCase","label","autocomplete","trim","byToken","wording","filter","Boolean","slice","credentialForInputAttribute","attribute","name","String","token","credentialInHtml","html","includes","find","isCredentialFieldName","words","replace","protocolRelativeUrls","strings","map","screenHostedPage","input","signals","nodes","ownNames","ownHosts","ownDomains","domain","ownRegistrables","isOwnHost","host","own","endsWith","nodeStrings","pageText","flat","linkHosts","brand","code","id","credentialSeen","noteCredential","forEach","index","credential","foreign","registrable","actions","named","action","match","mention"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/hosted-page-screen.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 * THE HOSTED PAGE SCREEN (AGL-3362).\n *\n * The outbound phishing screen, read over a PUBLISHED PAGE instead of an\n * email: the same brand list, the same lookalike logic, the same tiers\n * (`signalsThatHold`). A fraud actor who could not mail a lure could still\n * host one, and the incident's sibling site published a page at the\n * document-share path `/reviewfile`.\n *\n * Three shapes, one per requirement:\n *\n * 1. **A lookalike link or embed** (`lookalike-link`, STRONG) — any `href`,\n * `src`, form action or URL in the page's authored content whose host\n * wears a brand without being the brand's domain.\n * 2. **A credential field** (`credential-field`, STRONG) — a field the\n * merchant AUTHORED that asks for a password, a card number or a\n * one-time code: its type, its autocomplete token, or its label,\n * placeholder or name. The platform's own sign-in, account and checkout\n * elements render their inputs in code, not in stored props, so they are\n * never read as a field here — which is exactly the sanctioned/unsanctioned\n * line: what the platform collects through its own components is\n * Aglyn's to protect, and what a page's author collects in a form of\n * their own is not something a site may ask a stranger for.\n * 3. **A brand's call to action** (`brand-action-page`, SOFT) — one element\n * whose text names a brand that is not the workspace's AND asks the reader\n * to sign in, verify, confirm or open a document, on a page that links\n * somewhere that is neither the workspace's, nor a brand's own, nor a\n * common social or maps link. The same element, not the same page: a\n * hotel that says \"find us on Booking.com\" in one place and \"sign in\" in\n * another is two ordinary sentences.\n *\n * Pure: the caller hands over the composed node tree and the workspace's own\n * names and hosts, and decides what a hold does.\n *=========================================*/\n\nimport {\n isWorkspaceOwnBrand,\n linkHostsIn,\n isBrandsOwnDomain,\n lookalikeBrandForHost,\n PHISHING_LURE_PATTERNS,\n PHISHING_SCREEN_BRANDS,\n type PhishingScreenSignal,\n registrableDomain,\n isAnyOfficialBrandDomain,\n isCommonLinkDomain,\n squashScreenText,\n} from './outbound-phishing-screen'\n\nexport interface HostedPageScreenInput {\n /**\n * The page as it will be served — the composed tree, or any object that\n * holds nodes (`{ componentId, props }`) at any depth. Walked, never\n * trusted to have one shape: a screen, a layout's chrome, a grafted\n * component and a form all arrive together.\n */\n nodes: unknown\n /** The workspace's own names, as `screenOutboundEmail` reads them. */\n ownNames?: readonly (string | null | undefined)[]\n /** The workspace's own hosts — a link to one is never \"elsewhere\". */\n ownDomains?: readonly (string | null | undefined)[]\n}\n\nexport interface HostedPageScreenVerdict {\n signals: PhishingScreenSignal[]\n}\n\n/** A node as the walk finds it. */\ninterface WalkedNode {\n componentId: string\n props: Record<string, unknown>\n}\n\n/** The most a page's text is read to, so a huge page costs a bounded scan. */\nconst MAX_PAGE_TEXT = 2_000_000\n/** The most nodes the walk visits. */\nconst MAX_NODES = 20_000\n\n/** Props that describe a field the author defined, rather than content. */\nconst FIELD_PROP_KEYS = ['fieldType', 'fieldName', 'inputType', 'autoComplete', 'autocomplete']\n\n/** Autocomplete tokens (WHATWG) that name a credential. */\nconst CREDENTIAL_AUTOCOMPLETE: Record<string, 'password' | 'card' | 'otp'> = {\n 'current-password': 'password',\n 'new-password': 'password',\n 'cc-number': 'card',\n 'cc-csc': 'card',\n 'cc-exp': 'card',\n 'one-time-code': 'otp',\n}\n\n/** Field wording that asks for a credential, by kind. */\nconst CREDENTIAL_LABELS: ReadonlyArray<{ field: 'password' | 'card' | 'otp'; pattern: RegExp }> = [\n { field: 'password', pattern: /\\bpass\\s?(?:word|code|phrase)s?\\b|\\bpasswd\\b|\\bpin\\s*(?:code|number)?\\s*$/i },\n {\n field: 'card',\n pattern:\n /\\b(?:credit|debit)\\s*card\\b|\\bcard\\s*(?:number|no\\.?|#)|\\bcardnumber\\b|\\bcvv2?\\b|\\bcvc\\b|\\bcard\\s*security\\s*code\\b|\\bcard\\s*expir/i,\n },\n {\n field: 'otp',\n pattern:\n /\\bone[-\\s]?time\\s*(?:pass\\s?code|password|code|pin)\\b|\\botp\\b|\\bverification\\s*code\\b|\\b2fa\\b|\\btwo[-\\s]?factor\\b|\\bauthenticat(?:ion|or)\\s*code\\b|\\bsms\\s*code\\b/i,\n },\n]\n\n/** Raw HTML an author placed that holds a credential input. */\nconst HTML_CREDENTIAL_INPUTS: ReadonlyArray<{ field: 'password' | 'card' | 'otp'; pattern: RegExp }> = [\n { field: 'password', pattern: /<input\\b[^>]*\\btype\\s*=\\s*[\"']?password/i },\n { field: 'password', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:pass(?:word|wd)?|current-password|new-password)\\b/i },\n { field: 'card', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:card_?number|cc-number|cc-csc|cvv|cvc)\\b/i },\n { field: 'otp', pattern: /<input\\b[^>]*\\b(?:name|autocomplete)\\s*=\\s*[\"']?(?:otp|one-time-code|verification_?code)\\b/i },\n]\n\n/**\n * What an element asks its reader to DO with an account or a document —\n * counted only beside a brand in the SAME element. The email lure phrasing\n * counts too.\n */\nconst PAGE_ACTION_PATTERNS: readonly RegExp[] = [\n /\\b(?:sign|log)[\\s-]?in\\b/i,\n /\\bverify\\b/i,\n /\\bconfirm\\s+(?:your|the)\\b/i,\n /\\bunlock\\b/i,\n /\\benter\\s+your\\b/i,\n /\\b(?:view|review|open|download)\\s+(?:the\\s+|your\\s+)?(?:shared\\s+)?(?:file|document|invoice|statement)s?\\b/i,\n]\n\n/** Every node in the tree, breadth-first in document order, bounded. */\nfunction walkNodes(root: unknown): WalkedNode[] {\n const found: WalkedNode[] = []\n const seen = new Set<unknown>()\n const stack: unknown[] = [root]\n for (let head = 0; head < stack.length && found.length < MAX_NODES; head += 1) {\n const value = stack[head]\n if (!value || typeof value !== 'object' || seen.has(value)) continue\n seen.add(value)\n const record = value as Record<string, unknown>\n if (typeof record['componentId'] === 'string') {\n const props = record['props']\n found.push({\n componentId: record['componentId'] as string,\n props: props && typeof props === 'object' ? (props as Record<string, unknown>) : {},\n })\n }\n for (const child of Array.isArray(value) ? value : Object.values(record)) {\n if (child && typeof child === 'object') stack.push(child)\n }\n }\n return found\n}\n\n/** Every string a node's props hold, one level of arrays and objects deep. */\nfunction propStrings(props: Record<string, unknown>, skip: ReadonlySet<string> = new Set()): string[] {\n const out: string[] = []\n const visit = (value: unknown, depth: number) => {\n if (typeof value === 'string') out.push(value)\n else if (depth < 3 && value && typeof value === 'object') {\n for (const entry of Array.isArray(value) ? value : Object.values(value as Record<string, unknown>)) {\n visit(entry, depth + 1)\n }\n }\n }\n for (const [key, value] of Object.entries(props)) {\n if (key === 'sx' || skip.has(key)) continue\n visit(value, 0)\n }\n return out\n}\n\n/** Does this node describe a field its author defined? */\nfunction isAuthoredField(node: WalkedNode): boolean {\n if (/field|input/i.test(node.componentId)) return true\n return FIELD_PROP_KEYS.some((key) => node.props[key] !== undefined)\n}\n\n/** The credential a field asks for, or null. */\nfunction credentialOf(node: WalkedNode): { field: 'password' | 'card' | 'otp'; label: string } | null {\n const text = (key: string) => (typeof node.props[key] === 'string' ? (node.props[key] as string) : '')\n const type = [text('fieldType'), text('type'), text('inputType')].join(' ').toLowerCase()\n const label = text('label') || text('placeholder') || text('fieldName') || text('name')\n if (/\\bpassword\\b/.test(type)) return { field: 'password', label: label || 'password' }\n const autocomplete = (text('autoComplete') || text('autocomplete')).toLowerCase().trim()\n const byToken = CREDENTIAL_AUTOCOMPLETE[autocomplete]\n if (byToken) return { field: byToken, label: label || autocomplete }\n const wording = [text('label'), text('placeholder'), text('fieldName'), text('name'), text('helperText')]\n .filter(Boolean)\n .join(' ')\n for (const { field, pattern } of CREDENTIAL_LABELS) {\n if (pattern.test(wording)) return { field, label: (label || wording).slice(0, 80) }\n }\n return null\n}\n\n/**\n * The credential an input asks for, read from the two attributes that decide\n * it in the browser — its `type` and its `autocomplete` token — or null.\n * For code that builds inputs rather than storing them as page props: the\n * marketplace verifier reads a plugin bundle's `{ type: 'password' }` and\n * `setAttribute('autocomplete', 'cc-number')` with this (AGL-3362).\n */\nexport function credentialForInputAttribute(\n attribute: string,\n value: string,\n): 'password' | 'card' | 'otp' | null {\n const name = String(attribute ?? '').toLowerCase()\n const token = String(value ?? '').trim().toLowerCase()\n if ((name === 'type' || name === 'inputtype') && token === 'password') return 'password'\n if (name === 'autocomplete') return CREDENTIAL_AUTOCOMPLETE[token] ?? null\n return null\n}\n\n/** The credential a piece of raw HTML asks for in an input of its own, or null. */\nexport function credentialInHtml(html: string): 'password' | 'card' | 'otp' | null {\n const text = String(html ?? '')\n if (!text.includes('<')) return null\n return HTML_CREDENTIAL_INPUTS.find(({ pattern }) => pattern.test(text))?.field ?? null\n}\n\n/**\n * Does a submitted field's NAME ask for a credential — a password, a card\n * number or security code, a one-time code? The same wording the page\n * screen holds a field for, read over a name as a form posts it\n * (`card_number`, `cardNumber`, `one-time-code`).\n *\n * For the one path that does not pass a publish: a public form endpoint\n * takes whatever field names a request carries, so a page the screen never\n * saw (third-party code, a hand-built request) could still post a password\n * to it. The endpoint drops such a field rather than store it.\n */\nexport function isCredentialFieldName(name: string): boolean {\n const words = String(name ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_\\-.[\\]]+/g, ' ')\n .trim()\n if (!words) return false\n if (CREDENTIAL_AUTOCOMPLETE[words.toLowerCase().replace(/\\s+/g, '-')]) return true\n if (/^(?:pass|pwd|ccnum|cc\\s?num|cc\\s?number|cvv2?|cvc|csc)$/i.test(words)) return true\n return CREDENTIAL_LABELS.some(({ pattern }) => pattern.test(words))\n}\n\n/** URLs a page carries in a form Links-by-shape would miss: `//host/…`. */\nfunction protocolRelativeUrls(strings: readonly string[]): string {\n return strings\n .filter((value) => /^\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)+/i.test(value.trim()))\n .map((value) => `https:${value.trim()}`)\n .join('\\n')\n}\n\n/**\n * Screen one page as it will be served. The caller applies the tiers with\n * `signalsThatHold` and decides what a hold does.\n */\nexport function screenHostedPage(input: HostedPageScreenInput): HostedPageScreenVerdict {\n const signals: PhishingScreenSignal[] = []\n const nodes = walkNodes(input.nodes)\n const ownNames = squashScreenText((input.ownNames ?? []).filter(Boolean).join(' '))\n const ownHosts = (input.ownDomains ?? [])\n .map((domain) => String(domain ?? '').trim().toLowerCase().replace(/\\.+$/, ''))\n .filter((domain) => domain.includes('.'))\n const ownRegistrables = new Set(ownHosts.map(registrableDomain))\n const isOwnHost = (host: string) => ownHosts.some((own) => host === own || host.endsWith(`.${own}`))\n\n const nodeStrings = nodes.map((node) => propStrings(node.props))\n let pageText = nodeStrings.map((strings) => strings.join('\\n')).join('\\n')\n pageText = `${pageText}\\n${protocolRelativeUrls(nodeStrings.flat())}`.slice(0, MAX_PAGE_TEXT)\n\n // 1. A lookalike host anywhere the page links, embeds or posts to.\n const linkHosts = linkHostsIn(pageText)\n for (const host of linkHosts) {\n const brand = lookalikeBrandForHost(host)\n if (brand && !isBrandsOwnDomain(brand, ownRegistrables)) {\n signals.push({ code: 'lookalike-link', brand: brand.id, host })\n }\n }\n\n // 2. A credential field the page's author defined.\n const credentialSeen = new Set<string>()\n const noteCredential = (field: 'password' | 'card' | 'otp', label: string) => {\n if (credentialSeen.has(field)) return\n credentialSeen.add(field)\n signals.push({ code: 'credential-field', field, label: label.slice(0, 80) })\n }\n nodes.forEach((node, index) => {\n if (isAuthoredField(node)) {\n const credential = credentialOf(node)\n if (credential) noteCredential(credential.field, credential.label)\n }\n for (const html of nodeStrings[index]) {\n if (!html.includes('<')) continue\n for (const { field, pattern } of HTML_CREDENTIAL_INPUTS) {\n if (pattern.test(html)) noteCredential(field, 'an input in the page’s own HTML')\n }\n }\n })\n\n // 3. A brand's call to action, in one element, on a page that links away.\n const foreign = linkHosts.find((host) => {\n if (isOwnHost(host)) return false\n const registrable = registrableDomain(host)\n return !isAnyOfficialBrandDomain(registrable) && !isCommonLinkDomain(registrable)\n })\n if (foreign) {\n const actions = [...PAGE_ACTION_PATTERNS, ...PHISHING_LURE_PATTERNS]\n const named = new Set<string>()\n for (const strings of nodeStrings) {\n const text = strings.join(' ')\n if (!text) continue\n const action = actions.map((pattern) => text.match(pattern)?.[0]).find(Boolean)\n if (!action) continue\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (named.has(brand.id)) continue\n if (brand.mention.test(text) && !isWorkspaceOwnBrand(brand, ownNames, ownRegistrables)) {\n named.add(brand.id)\n signals.push({ code: 'brand-action-page', brand: brand.id, action: action.slice(0, 80) })\n }\n }\n }\n }\n\n return { signals }\n}\n"],"names":["isWorkspaceOwnBrand","linkHostsIn","isBrandsOwnDomain","lookalikeBrandForHost","PHISHING_LURE_PATTERNS","PHISHING_SCREEN_BRANDS","registrableDomain","isAnyOfficialBrandDomain","isCommonLinkDomain","squashScreenText","MAX_PAGE_TEXT","MAX_NODES","FIELD_PROP_KEYS","CREDENTIAL_AUTOCOMPLETE","CREDENTIAL_LABELS","field","pattern","HTML_CREDENTIAL_INPUTS","PAGE_ACTION_PATTERNS","walkNodes","root","found","seen","Set","stack","head","length","value","has","add","record","props","push","componentId","child","Array","isArray","Object","values","propStrings","skip","out","visit","depth","entry","key","entries","isAuthoredField","node","test","some","undefined","credentialOf","text","type","join","toLowerCase","label","autocomplete","trim","byToken","wording","filter","Boolean","slice","credentialForInputAttribute","attribute","name","String","token","credentialInHtml","html","includes","find","isCredentialFieldName","words","replace","protocolRelativeUrls","strings","map","screenHostedPage","input","signals","nodes","ownNames","ownHosts","ownDomains","domain","ownRegistrables","isOwnHost","host","own","endsWith","nodeStrings","pageText","flat","linkHosts","brand","code","id","credentialSeen","noteCredential","forEach","index","credential","foreign","registrable","actions","named","action","match","mention"],"mappings":"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"}
@@ -145,8 +145,22 @@ export interface PhishingScreenVerdict {
145
145
  }
146
146
  /** The registrable domain of a host: `a.b.evil.co.uk` → `evil.co.uk`. */
147
147
  export declare function registrableDomain(host: string): string;
148
+ /**
149
+ * Is the workspace the brand itself, by DOMAIN: one of its own domains is
150
+ * one of the brand's official ones (Aglyn's site on aglyn.com linking to
151
+ * its own account on a video or help-desk vendor). Deliberately not by name — a workspace can call itself
152
+ * "Poshmark", but it cannot serve on poshmark.com.
153
+ */
154
+ export declare function isBrandsOwnDomain(brand: PhishingScreenBrand, ownRegistrables: ReadonlySet<string>): boolean;
148
155
  /** Every brand's real domains, for "is this link just the brand itself". */
149
156
  declare function isAnyOfficialDomain(registrable: string): boolean;
157
+ /**
158
+ * Video and audio platforms whose account subdomains and embed URLs a page
159
+ * or email plays media from: `player.vimeo.com`, `www.youtube.com/embed/…`.
160
+ * A video plugin's own hosts are the plugin's to declare, not listed here. Anyone may embed or link media from these, so a
161
+ * host on one is never a lookalike, whatever name its subdomain carries.
162
+ */
163
+ export declare const MEDIA_EMBED_HOSTS: ReadonlySet<string>;
150
164
  /** The brand a host impersonates, or null. */
151
165
  export declare function lookalikeBrandForHost(host: string): PhishingScreenBrand | null;
152
166
  /** The hosts every link in the text points at, lowercased. */
@@ -177,8 +191,17 @@ export declare const OUTBOUND_REVIEW_YOUNG_DAYS = 14;
177
191
  export type PhishingSignalTier = 'strong' | 'soft';
178
192
  /** The signal codes that hold for every workspace. */
179
193
  export declare const STRONG_PHISHING_SIGNAL_CODES: ReadonlySet<string>;
194
+ /**
195
+ * Hosts that give each customer an account subdomain named after the
196
+ * customer: `acme.zendesk.com` is Acme's own help desk. A brand's name there is usually the brand itself, so a
197
+ * lookalike on one of these is SOFT — held for a young workspace, never a
198
+ * takedown of an established one's page or mail. A lookalike on any other
199
+ * host (`poshmark.id63835663.shop`) stays STRONG.
200
+ */
201
+ export declare const ACCOUNT_SUBDOMAIN_HOSTS: ReadonlySet<string>;
180
202
  export declare function phishingSignalTier(signal: {
181
203
  code: string;
204
+ host?: string;
182
205
  }): PhishingSignalTier;
183
206
  /** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */
184
207
  export declare function isYoungWorkspaceAge(ageDays: number | null | undefined): boolean;
@@ -200,6 +223,7 @@ export interface OutboundScreenPolicy {
200
223
  /** The signals that HOLD under the tiers, for this workspace and message. */
201
224
  export declare function signalsThatHold<T extends {
202
225
  code: string;
226
+ host?: string;
203
227
  }>(signals: readonly T[], policy: OutboundScreenPolicy): T[];
204
228
  /**
205
229
  * The same brand, lookalike and tier logic applied to a HOST NAME a workspace
@@ -409,6 +409,16 @@ function isOfficialDomain(brand, registrable) {
409
409
  const { label, suffix } = splitRegistrable(registrable);
410
410
  return label === base && (/^[a-z]{2}$/.test(suffix) || TWO_LABEL_SUFFIXES.has(suffix));
411
411
  }
412
+ /**
413
+ * Is the workspace the brand itself, by DOMAIN: one of its own domains is
414
+ * one of the brand's official ones (Aglyn's site on aglyn.com linking to
415
+ * its own account on a video or help-desk vendor). Deliberately not by name — a workspace can call itself
416
+ * "Poshmark", but it cannot serve on poshmark.com.
417
+ */ export function isBrandsOwnDomain(brand, ownRegistrables) {
418
+ return [
419
+ ...ownRegistrables
420
+ ].some((registrable)=>isOfficialDomain(brand, registrable));
421
+ }
412
422
  /** Every brand's real domains, for "is this link just the brand itself". */ function isAnyOfficialDomain(registrable) {
413
423
  return PHISHING_SCREEN_BRANDS.some((brand)=>isOfficialDomain(brand, registrable));
414
424
  }
@@ -434,10 +444,42 @@ function isOfficialDomain(brand, registrable) {
434
444
  }
435
445
  return false;
436
446
  }
447
+ /**
448
+ * Video and audio platforms whose account subdomains and embed URLs a page
449
+ * or email plays media from: `player.vimeo.com`, `www.youtube.com/embed/…`.
450
+ * A video plugin's own hosts are the plugin's to declare, not listed here. Anyone may embed or link media from these, so a
451
+ * host on one is never a lookalike, whatever name its subdomain carries.
452
+ */ export const MEDIA_EMBED_HOSTS = new Set([
453
+ 'vimeo.com',
454
+ 'vimeocdn.com',
455
+ 'youtube.com',
456
+ 'youtu.be',
457
+ 'youtube-nocookie.com',
458
+ 'ytimg.com',
459
+ 'loom.com',
460
+ 'vidyard.com',
461
+ 'brightcove.net',
462
+ 'brightcove.com',
463
+ 'jwplayer.com',
464
+ 'jwpcdn.com',
465
+ 'mux.com',
466
+ 'cloudflarestream.com',
467
+ 'videodelivery.net',
468
+ 'dailymotion.com',
469
+ 'twitch.tv',
470
+ 'spotify.com',
471
+ 'soundcloud.com',
472
+ 'podbean.com',
473
+ 'buzzsprout.com',
474
+ 'simplecast.com',
475
+ 'transistor.fm',
476
+ 'anchor.fm'
477
+ ]);
437
478
  /** The brand a host impersonates, or null. */ export function lookalikeBrandForHost(host) {
438
479
  const normalized = String(host != null ? host : '').toLowerCase().replace(/\.+$/, '');
439
480
  if (!normalized.includes('.')) return null;
440
481
  const registrable = registrableDomain(normalized);
482
+ if (MEDIA_EMBED_HOSTS.has(registrable)) return null;
441
483
  for (const brand of PHISHING_SCREEN_BRANDS){
442
484
  var _brand_hostedDomains;
443
485
  if (isOfficialDomain(brand, registrable)) continue;
@@ -555,7 +597,7 @@ const ADDRESS_HOST_PATTERN = /[a-z0-9._%+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)/gi;
555
597
  ]){
556
598
  if (seenLookalike.has(host)) continue;
557
599
  const brand = lookalikeBrandForHost(host);
558
- if (brand) {
600
+ if (brand && !isBrandsOwnDomain(brand, ownRegistrables)) {
559
601
  seenLookalike.add(host);
560
602
  signals.push({
561
603
  code: 'lookalike-link',
@@ -659,8 +701,45 @@ const ADDRESS_HOST_PATTERN = /[a-z0-9._%+-]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)/gi;
659
701
  'lookalike-link',
660
702
  'credential-field'
661
703
  ]);
704
+ /**
705
+ * Hosts that give each customer an account subdomain named after the
706
+ * customer: `acme.zendesk.com` is Acme's own help desk. A brand's name there is usually the brand itself, so a
707
+ * lookalike on one of these is SOFT — held for a young workspace, never a
708
+ * takedown of an established one's page or mail. A lookalike on any other
709
+ * host (`poshmark.id63835663.shop`) stays STRONG.
710
+ */ export const ACCOUNT_SUBDOMAIN_HOSTS = new Set([
711
+ 'vimeo.com',
712
+ 'vimeocdn.com',
713
+ 'zendesk.com',
714
+ 'freshdesk.com',
715
+ 'helpscoutdocs.com',
716
+ 'intercom.help',
717
+ 'statuspage.io',
718
+ 'atlassian.net',
719
+ 'myshopify.com',
720
+ 'squarespace.com',
721
+ 'wixsite.com',
722
+ 'webflow.io',
723
+ 'hubspotpagebuilder.com',
724
+ 'substack.com',
725
+ 'typeform.com',
726
+ 'calendly.com',
727
+ 'gitbook.io',
728
+ 'readme.io',
729
+ 'notion.site',
730
+ 'github.io',
731
+ 'cloudfront.net',
732
+ 'kajabi.com',
733
+ 'thinkific.com',
734
+ 'teachable.com',
735
+ 'mailchimpsites.com'
736
+ ]);
662
737
  export function phishingSignalTier(signal) {
663
- return STRONG_PHISHING_SIGNAL_CODES.has(signal == null ? void 0 : signal.code) ? 'strong' : 'soft';
738
+ if (!STRONG_PHISHING_SIGNAL_CODES.has(signal == null ? void 0 : signal.code)) return 'soft';
739
+ if (signal.code === 'lookalike-link' && typeof signal.host === 'string' && ACCOUNT_SUBDOMAIN_HOSTS.has(registrableDomain(signal.host))) {
740
+ return 'soft';
741
+ }
742
+ return 'strong';
664
743
  }
665
744
  /** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */ export function isYoungWorkspaceAge(ageDays) {
666
745
  return typeof ageDays === 'number' && Number.isFinite(ageDays) && ageDays < OUTBOUND_REVIEW_YOUNG_DAYS;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/outbound-phishing-screen.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 * THE OUTBOUND PHISHING SCREEN (AGL-3356).\n *\n * A fraud actor signed up, paid with a card that never met 3-D Secure, and\n * used a workspace to send two kinds of mail: a campaign of Booking.com-style\n * \"guest feedback regarding your property\" lures to hotels, and a workflow\n * that mailed \"Poshmark Order — one of the items from your Seller Account has\n * finally sold\", linking to `poshmark.id63835663.shop`.\n *\n * This module answers one question about a message a YOUNG workspace is\n * about to send: does its content carry a STRONG phishing signal? The caller\n * decides who is young and what happens to a positive answer — the answer is\n * a hold for staff review, never a drop.\n *\n * ## Conservative by construction\n *\n * A false positive costs a real merchant's first campaign a wait for a\n * human; a miss costs strangers their bank logins. Both are real, so only\n * three shapes hold, and each is one a legitimate merchant essentially never\n * produces:\n *\n * 1. **A lookalike link** — a link or reply address whose host carries a\n * brand's name without being the brand's own domain: the brand as a label\n * of an unrelated registrable domain (`poshmark.id63835663.shop`), hyphen-\n * joined to one (`paypal-secure.com`, `secure-paypal.net`), the brand's\n * real domain embedded in a longer host (`booking.com.guest-review.top`),\n * or leetspeak for it (`paypa1.com`). Holds on its own.\n * 2. **A brand in the sender's display name** that is not the workspace's\n * own name — mail that says it is FROM PayPal.\n * 3. **The three-part lure**: a brand named in the copy, AND credential or\n * account-action phrasing (\"verify your account\", \"has finally sold\",\n * \"feedback regarding your property\"), AND a link to a domain that is\n * neither the brand's, nor the workspace's, nor a common social or maps\n * link. Any two of the three are ordinary marketing — \"our book is on\n * Amazon\", \"sign in to your account\", a link to the shop — and pass.\n *\n * Mentioning a brand, alone, never holds.\n *\n * ## The brand list is small on purpose\n *\n * The brands phishing kits most often wear, plus the two in the incident. A\n * brand is added when it has been seen impersonated, not because it is big:\n * every entry is a set of words that can now hold somebody's newsletter.\n * Each carries its REAL domains, so a link to the brand itself is never\n * suspicious, and host tokens chosen so a common word (`booking`, `apple`,\n * `outlook`) does not match an unrelated host by itself.\n *\n * Pure: no DNS, no store, no clock. Everything the caller knows about the\n * workspace arrives as input.\n *=========================================*/\n\n/** One brand the screen recognizes. */\nexport interface PhishingScreenBrand {\n /** Stable id, as stored on a hold. */\n id: string\n /** How staff read it. */\n label: string\n /** Copy that names the brand — subject, body, sender. Case-insensitive. */\n mention: RegExp\n /**\n * Host labels that name the brand. Matched against each dot-separated\n * label of a link's host, whole or hyphen-joined, after leetspeak is\n * folded — never as a bare substring, which is how `amazonaws` or\n * `bookingengine` would read as a brand.\n */\n hostTokens: readonly string[]\n /**\n * Registrable domains the brand genuinely links from. The FIRST is its\n * primary domain, the one a kit embeds in a longer host.\n */\n officialDomains: readonly string[]\n /**\n * The brand's name is an ordinary word (`apple`, `amazon`), so a host\n * label names the brand only when it IS the word — `apple.example.top` —\n * and never hyphen-joined or leet-folded, which `apple-orchard-farm.com`\n * would otherwise be.\n */\n wordToken?: boolean\n /**\n * Whether `<brand>.<country code>` (and `<brand>.co.<cc>`) is the brand's\n * too — true for the marketplaces that run a site per country.\n */\n countryDomains?: boolean\n /**\n * Registrable domains the brand runs but where OTHER people's content lives —\n * the platform's own tenant apex, where every site is `<name>.<apex>`. A\n * host there never wears the brand as a disguise, so the lookalike rule\n * skips it; but it is not the brand's own domain either, so a link to a\n * neighbor's site still counts as \"somewhere else\" to the lure rules, and a\n * workspace hosted there does not own the brand by being hosted there.\n */\n hostedDomains?: readonly string[]\n}\n\nexport const PHISHING_SCREEN_BRANDS: readonly PhishingScreenBrand[] = [\n {\n id: 'poshmark',\n label: 'Poshmark',\n mention: /\\bposhmark\\b/i,\n hostTokens: ['poshmark'],\n officialDomains: ['poshmark.com', 'posh.mk'],\n },\n {\n id: 'booking',\n label: 'Booking.com',\n // Never the bare word: \"booking\" is every hotel's and every salon's.\n mention: /\\bbooking\\.com\\b/i,\n hostTokens: ['bookingcom', 'booking-com'],\n officialDomains: ['booking.com', 'bstatic.com'],\n },\n {\n id: 'airbnb',\n label: 'Airbnb',\n mention: /\\bairbnb\\b/i,\n hostTokens: ['airbnb'],\n officialDomains: ['airbnb.com', 'abnb.me', 'muscache.com'],\n countryDomains: true,\n },\n {\n id: 'paypal',\n label: 'PayPal',\n mention: /\\bpay\\s?pal\\b/i,\n hostTokens: ['paypal'],\n officialDomains: ['paypal.com', 'paypal.me', 'paypalobjects.com'],\n countryDomains: true,\n },\n {\n id: 'amazon',\n label: 'Amazon',\n mention: /\\bamazon\\b/i,\n hostTokens: ['amazon', 'amzn'],\n wordToken: true,\n officialDomains: [\n 'amazon.com',\n 'amzn.to',\n 'amzn.com',\n 'amazonaws.com',\n 'media-amazon.com',\n 'ssl-images-amazon.com',\n ],\n countryDomains: true,\n },\n {\n id: 'apple',\n label: 'Apple',\n // The fruit is on every bakery's menu; the account is what gets phished.\n mention: /\\bapple\\s?(?:id|support|account)\\b|\\bicloud\\b|\\bitunes\\b/i,\n hostTokens: ['apple', 'appleid', 'icloud', 'itunes'],\n wordToken: true,\n officialDomains: ['apple.com', 'icloud.com', 'apple.co', 'itunes.com', 'me.com', 'mzstatic.com'],\n countryDomains: true,\n },\n {\n id: 'microsoft',\n label: 'Microsoft',\n mention: /\\bmicrosoft\\b|\\boffice\\s?365\\b|\\bonedrive\\b|\\bsharepoint\\b/i,\n hostTokens: ['microsoft', 'office365', 'onedrive', 'sharepoint', 'microsoftonline', 'msonline'],\n officialDomains: [\n 'microsoft.com',\n 'office.com',\n 'office365.com',\n 'live.com',\n 'outlook.com',\n 'microsoftonline.com',\n 'sharepoint.com',\n 'onedrive.com',\n '1drv.ms',\n 'aka.ms',\n ],\n },\n {\n id: 'docusign',\n label: 'DocuSign',\n mention: /\\bdocu\\s?sign\\b/i,\n hostTokens: ['docusign'],\n officialDomains: ['docusign.com', 'docusign.net'],\n },\n {\n id: 'netflix',\n label: 'Netflix',\n mention: /\\bnetflix\\b/i,\n hostTokens: ['netflix'],\n officialDomains: ['netflix.com', 'nflxext.com', 'nflximg.net'],\n },\n {\n id: 'wellsfargo',\n label: 'Wells Fargo',\n mention: /\\bwells\\s?fargo\\b/i,\n hostTokens: ['wellsfargo', 'wells-fargo'],\n officialDomains: ['wellsfargo.com'],\n },\n {\n id: 'bankofamerica',\n label: 'Bank of America',\n mention: /\\bbank\\s+of\\s+america\\b/i,\n hostTokens: ['bankofamerica', 'bofa'],\n officialDomains: ['bankofamerica.com', 'bofa.com'],\n },\n {\n id: 'coinbase',\n label: 'Coinbase',\n mention: /\\bcoinbase\\b/i,\n hostTokens: ['coinbase'],\n officialDomains: ['coinbase.com'],\n },\n {\n id: 'usps',\n label: 'USPS',\n mention: /\\busps\\b/i,\n hostTokens: ['usps'],\n officialDomains: ['usps.com'],\n },\n {\n id: 'fedex',\n label: 'FedEx',\n mention: /\\bfed\\s?ex\\b/i,\n hostTokens: ['fedex'],\n officialDomains: ['fedex.com'],\n },\n {\n id: 'dhl',\n label: 'DHL',\n mention: /\\bdhl\\b/i,\n hostTokens: ['dhl'],\n officialDomains: ['dhl.com'],\n countryDomains: true,\n },\n {\n // The platform itself (AGL-3365). A marketplace listing, a page or an\n // email that wears the platform's name — \"Aglyn Support\", a link to\n // `aglyn-billing.com`, \"official Aglyn plugin\" — is the one impersonation\n // every customer is primed to trust. The workspace named for it (the\n // platform's own org) owns it by name, as any brand's own workspace does.\n id: 'aglyn',\n label: 'Aglyn',\n mention: /\\baglyn\\b/i,\n hostTokens: ['aglyn'],\n officialDomains: ['aglyn.com', 'aglyn.io', 'aglyn.dev'],\n hostedDomains: ['aglyn.app'],\n },\n]\n\n/**\n * Account-action and credential phrasing — the half of a lure that asks the\n * reader to DO something with an account. Only ever counted beside a brand\n * AND a foreign link; alone, several of these are ordinary transactional copy.\n */\nexport const PHISHING_LURE_PATTERNS: readonly RegExp[] = [\n /\\bverify\\s+your\\s+(?:account|identity|information|details|payment|email)\\b/i,\n /\\bconfirm\\s+your\\s+(?:account|identity|details|payment|password|billing|information)\\b/i,\n /\\b(?:account|payment|card)\\s+(?:has\\s+been\\s+|is\\s+|was\\s+)?(?:suspended|locked|limited|restricted|disabled|on\\s+hold)\\b/i,\n /\\bunusual\\s+(?:sign[-\\s]?in|login|activity)\\b/i,\n /\\bupdate\\s+your\\s+(?:payment|billing|card|account)\\s*(?:details|information|method)?\\b/i,\n /\\b(?:sign|log)\\s?in\\s+to\\s+(?:your|view|confirm|claim|release)\\b/i,\n /\\b(?:has|have)\\s+(?:finally\\s+)?sold\\b/i,\n /\\bseller\\s+account\\b/i,\n /\\b(?:guest|customer)\\s+(?:complaint|review|feedback)\\b/i,\n /\\bfeedback\\s+regarding\\s+your\\s+(?:property|listing|stay|booking|reservation)\\b/i,\n /\\b(?:document|invoice|agreement)s?\\s+(?:is\\s+|are\\s+)?(?:ready|waiting|pending)\\s+(?:for\\s+)?(?:your\\s+)?(?:review|signature|to\\s+sign)\\b/i,\n /\\bpassword\\s+(?:will\\s+)?expire/i,\n /\\bclaim\\s+your\\s+(?:refund|reward|prize|package|payment|funds)\\b/i,\n /\\b(?:delivery|shipment)\\s+(?:failed|attempt|suspended|on\\s+hold)\\b/i,\n /\\b(?:package|parcel)\\s+(?:is\\s+)?(?:on\\s+hold|held|awaiting|undeliverable)\\b/i,\n /\\bsecurity\\s+alert\\b/i,\n]\n\n/**\n * Link targets a merchant's mail ordinarily carries that are not theirs:\n * social profiles, maps, review sites, video. Counted as neither foreign nor\n * suspicious in the three-part lure, so \"leave us a review on Google\" with a\n * brand named elsewhere does not become a hold. Never consulted by the\n * lookalike check, which holds on the host's own shape.\n */\nconst COMMON_LINK_DOMAINS: ReadonlySet<string> = new Set([\n 'facebook.com',\n 'fb.com',\n 'instagram.com',\n 'x.com',\n 'twitter.com',\n 'linkedin.com',\n 'youtube.com',\n 'youtu.be',\n 'tiktok.com',\n 'pinterest.com',\n 'google.com',\n 'goo.gl',\n 'g.page',\n 'yelp.com',\n 'tripadvisor.com',\n 'wa.me',\n 'whatsapp.com',\n])\n\n/**\n * Two-label public suffixes, so `evil.co.uk` registers as `evil.co.uk` and\n * not as `co.uk`. Deliberately a short list of the ones mail actually\n * arrives from rather than the whole public suffix list: a suffix missing\n * here makes the registrable domain one label too short, which can only\n * widen what counts as \"the same domain\" for a link — never invent a brand\n * label that is not in the host.\n */\nconst TWO_LABEL_SUFFIXES: ReadonlySet<string> = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'gov.uk', 'me.uk',\n 'com.au', 'net.au', 'org.au',\n 'co.nz', 'co.jp', 'co.kr', 'co.in', 'co.za', 'co.il', 'co.id', 'co.th',\n 'com.br', 'com.mx', 'com.ar', 'com.tr', 'com.cn', 'com.hk', 'com.sg',\n 'com.tw', 'com.my', 'com.ph', 'com.co', 'com.pe', 'com.sa', 'com.eg',\n])\n\n/** A signal the screen found. */\nexport type PhishingScreenSignal =\n | { code: 'lookalike-link'; brand: string; host: string }\n | { code: 'brand-sender'; brand: string; fromName: string }\n | { code: 'brand-lure-link'; brand: string; lure: string; host: string }\n /**\n * A published page asks for a password, a card number or a one-time code\n * in a field that is not one of the platform's own sign-in or checkout\n * elements (`hosted-page-screen.ts`). Strong.\n */\n | { code: 'credential-field'; field: 'password' | 'card' | 'otp'; label: string }\n /**\n * A published page names a brand that is not the workspace's beside a\n * sign-in, verify or payment call to action (`hosted-page-screen.ts`).\n * Soft.\n */\n | { code: 'brand-action-page'; brand: string; action: string }\n\nexport interface PhishingScreenInput {\n subject?: string | null\n /** The display name in front of the sending address. */\n fromName?: string | null\n /**\n * The ADDRESS the message leaves from (AGL-3362) — a `From:` domain is a\n * link the recipient never has to click, so its host is read by the\n * lookalike rule exactly like a link's: mail from `support@paypa1.com` is\n * the disguise whoever sends it. Not an own domain for the lure's \"is this\n * elsewhere\" test either way; it only ever adds a lookalike signal.\n */\n fromAddress?: string | null\n replyTo?: string | readonly string[] | null\n preheader?: string | null\n /**\n * Everything else the recipient reads or clicks: the body, the plain-text\n * part, a design's nodes serialized — any string. Links are found by\n * shape, so a JSON-encoded design is as readable here as HTML.\n */\n bodies?: readonly (string | null | undefined)[]\n /**\n * The workspace's own names — org, site, subdomain. A brand in one of them\n * is the workspace's own, and `brand-sender` / `brand-lure-link` do not\n * fire for it. The lookalike check does not consult names: a workspace\n * NAMED \"Poshmark\" linking to `poshmark.id63835663.shop` is the incident.\n */\n ownNames?: readonly (string | null | undefined)[]\n /**\n * The workspace's own hosts — its subdomain on the tenant apex, custom\n * domain, sending domain. A link to one of them, or below one, is never\n * the \"somewhere else\" of a lure. Matched by host, never by registrable\n * domain: every site's subdomain shares the tenant apex, and a link to a\n * NEIGHBOR's site is somewhere else.\n *\n * Not an exemption from the lookalike check. A host shaped like\n * `paypal-secure.com` is a lookalike whoever attached it, and a workspace\n * that attached it has attached the disguise.\n */\n ownDomains?: readonly (string | null | undefined)[]\n}\n\nexport interface PhishingScreenVerdict {\n /** True when the message should be held for staff review. */\n hold: boolean\n signals: PhishingScreenSignal[]\n}\n\n/** The registrable domain of a host: `a.b.evil.co.uk` → `evil.co.uk`. */\nexport function registrableDomain(host: string): string {\n const labels = String(host ?? '')\n .toLowerCase()\n .replace(/\\.+$/, '')\n .split('.')\n .filter(Boolean)\n if (labels.length <= 2) return labels.join('.')\n const lastTwo = labels.slice(-2).join('.')\n return TWO_LABEL_SUFFIXES.has(lastTwo)\n ? labels.slice(-3).join('.')\n : lastTwo\n}\n\n/** `<label>.<suffix>` of a registrable domain, split once. */\nfunction splitRegistrable(registrable: string): { label: string; suffix: string } {\n const dot = registrable.indexOf('.')\n return dot < 0\n ? { label: registrable, suffix: '' }\n : { label: registrable.slice(0, dot), suffix: registrable.slice(dot + 1) }\n}\n\nfunction isOfficialDomain(brand: PhishingScreenBrand, registrable: string): boolean {\n if (brand.officialDomains.includes(registrable)) return true\n if (!brand.countryDomains) return false\n const base = splitRegistrable(brand.officialDomains[0]).label\n const { label, suffix } = splitRegistrable(registrable)\n return label === base && (/^[a-z]{2}$/.test(suffix) || TWO_LABEL_SUFFIXES.has(suffix))\n}\n\n/** Every brand's real domains, for \"is this link just the brand itself\". */\nfunction isAnyOfficialDomain(registrable: string): boolean {\n return PHISHING_SCREEN_BRANDS.some((brand) => isOfficialDomain(brand, registrable))\n}\n\n/** Leetspeak folded, so `paypa1` reads as `paypal`. */\nfunction foldLeet(label: string): string {\n return label\n .replace(/0/g, 'o')\n .replace(/1/g, 'l')\n .replace(/3/g, 'e')\n .replace(/4/g, 'a')\n .replace(/5/g, 's')\n .replace(/7/g, 't')\n}\n\n/** Does one host label name the brand — whole, or hyphen-joined? */\nfunction labelNamesBrand(label: string, token: string, wordToken: boolean): boolean {\n if (wordToken) return label === token\n for (const candidate of new Set([label, foldLeet(label)])) {\n if (candidate === token) return true\n const parts = candidate.split('-')\n // A hyphen-joined token (`booking-com`) spans parts, so it is matched\n // as a run of parts rather than one of them.\n const tokenParts = token.split('-')\n for (let at = 0; at + tokenParts.length <= parts.length; at += 1) {\n if (tokenParts.every((part, offset) => parts[at + offset] === part)) {\n return true\n }\n }\n }\n return false\n}\n\n/** The brand a host impersonates, or null. */\nexport function lookalikeBrandForHost(host: string): PhishingScreenBrand | null {\n const normalized = String(host ?? '').toLowerCase().replace(/\\.+$/, '')\n if (!normalized.includes('.')) return null\n const registrable = registrableDomain(normalized)\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (isOfficialDomain(brand, registrable)) continue\n if (brand.hostedDomains?.includes(registrable)) continue\n const labels = normalized.split('.')\n if (\n labels.some((label) =>\n brand.hostTokens.some((token) => labelNamesBrand(label, token, brand.wordToken === true)),\n )\n ) {\n return brand\n }\n // The brand's primary domain embedded in a longer, different one:\n // `booking.com.guest-review.top`, `paypal.com-secure.net`. Only the\n // primary: a short secondary (`me.com`, `aka.ms`) would match too much.\n const primary = brand.officialDomains[0]\n if (\n normalized.startsWith(`${primary}.`) ||\n normalized.startsWith(`${primary}-`) ||\n normalized.includes(`.${primary}.`) ||\n normalized.includes(`.${primary}-`)\n ) {\n return brand\n }\n }\n return null\n}\n\n/**\n * The few entity forms a phishing kit uses to keep a URL away from a naive\n * scanner (`&#46;` for a dot, `&amp;` inside a query). Decoded before links\n * are read; everything else is left alone.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]{1,6});/gi, (_, hex: string) => safeCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d{1,7});/g, (_, dec: string) => safeCodePoint(parseInt(dec, 10)))\n .replace(/&amp;/gi, '&')\n}\n\nfunction safeCodePoint(value: number): string {\n return Number.isFinite(value) && value > 0 && value <= 0x10ffff\n ? String.fromCodePoint(value)\n : ''\n}\n\nconst URL_PATTERN = /\\b(?:https?:\\/\\/|www\\.)[^\\s\"'<>()\\\\[\\]{}]+/gi\n\n/** The hosts every link in the text points at, lowercased. */\nexport function linkHostsIn(text: string): string[] {\n const hosts = new Set<string>()\n for (const match of decodeEntities(String(text ?? '')).match(URL_PATTERN) ?? []) {\n let rest = match.replace(/^https?:\\/\\//i, '')\n rest = rest.split(/[/?#]/)[0]\n // `https://paypal.com@evil.top/` resolves to evil.top — and the part\n // before the `@` is exactly the disguise, so it is kept as a host too.\n const at = rest.lastIndexOf('@')\n const disguise = at >= 0 ? rest.slice(0, at) : ''\n rest = at >= 0 ? rest.slice(at + 1) : rest\n for (const candidate of [rest, disguise]) {\n const host = candidate.replace(/:\\d+$/, '').replace(/\\.+$/, '').toLowerCase()\n // A merge tag inside a URL is not a host anybody can visit yet.\n if (host && host.includes('.') && !host.includes('{') && !host.includes('%7b')) {\n hosts.add(host)\n }\n }\n }\n return [...hosts]\n}\n\nconst ADDRESS_HOST_PATTERN = /[a-z0-9._%+-]+@([a-z0-9-]+(?:\\.[a-z0-9-]+)+)/gi\n\n/** The host of every email address in the text — a reply-to, a `mailto:`. */\nfunction addressHostsIn(text: string): string[] {\n const hosts = new Set<string>()\n for (const match of decodeEntities(String(text ?? '')).matchAll(ADDRESS_HOST_PATTERN)) {\n hosts.add(match[1].toLowerCase().replace(/\\.+$/, ''))\n }\n return [...hosts]\n}\n\n/** Lowercased alphanumerics only, so \"Wells-Fargo Advisors\" contains `wellsfargo`. */\nfunction squash(text: string): string {\n return text.toLowerCase().replace(/[^a-z0-9]/g, '')\n}\n\n/** Is this brand the workspace's own, by name or by a domain it holds? */\nfunction isOwnBrand(\n brand: PhishingScreenBrand,\n ownNames: string,\n ownRegistrables: ReadonlySet<string>,\n): boolean {\n const labels = [brand.id, squash(brand.label), ...brand.hostTokens.map(squash)]\n if (labels.some((label) => label && ownNames.includes(label))) return true\n return [...ownRegistrables].some((registrable) => isOfficialDomain(brand, registrable))\n}\n\n/*\n * The same judgements, for the other surfaces that read content with this\n * screen's data (`hosted-page-screen.ts`). Exported under these names rather\n * than copied, so a page and an email can never disagree about what a\n * brand's own domain is or whose brand a name is.\n */\n\n/** `squash`: lowercased alphanumerics, the form own-name matching reads. */\nexport const squashScreenText = squash\n/** Is this brand the workspace's own, by name (squashed) or by a domain it holds? */\nexport const isWorkspaceOwnBrand = isOwnBrand\n/** Is this registrable domain one of any listed brand's own? */\nexport const isAnyOfficialBrandDomain = isAnyOfficialDomain\n/** Is this registrable domain a social, maps or review link merchants ordinarily carry? */\nexport function isCommonLinkDomain(registrable: string): boolean {\n return COMMON_LINK_DOMAINS.has(registrable)\n}\n\n/**\n * Screen one outbound message. See the module header for what holds and why.\n */\nexport function screenOutboundEmail(input: PhishingScreenInput): PhishingScreenVerdict {\n const signals: PhishingScreenSignal[] = []\n const ownNames = squash((input.ownNames ?? []).filter(Boolean).join(' '))\n const ownHosts = (input.ownDomains ?? [])\n .map((domain) => String(domain ?? '').trim().toLowerCase().replace(/\\.+$/, ''))\n .filter((domain) => domain.includes('.'))\n const ownRegistrables = new Set(ownHosts.map(registrableDomain))\n const isOwnHost = (host: string) =>\n ownHosts.some((own) => host === own || host.endsWith(`.${own}`))\n\n const copy = [input.subject, input.preheader, ...(input.bodies ?? [])]\n .filter((part): part is string => typeof part === 'string' && part.length > 0)\n .join('\\n')\n const decodedCopy = decodeEntities(copy)\n const replyTo = Array.isArray(input.replyTo)\n ? input.replyTo.join(' ')\n : String(input.replyTo ?? '')\n const linkHosts = linkHostsIn(copy)\n\n // 1. A lookalike host, anywhere a recipient could click, write back, or\n // read as the sender.\n const seenLookalike = new Set<string>()\n const fromAddress = String(input.fromAddress ?? '')\n for (const host of [...linkHosts, ...addressHostsIn(`${copy}\\n${replyTo}\\n${fromAddress}`)]) {\n if (seenLookalike.has(host)) continue\n const brand = lookalikeBrandForHost(host)\n if (brand) {\n seenLookalike.add(host)\n signals.push({ code: 'lookalike-link', brand: brand.id, host })\n }\n }\n\n // 2. A brand in the sender's display name that is not the workspace's.\n const fromName = String(input.fromName ?? '').trim()\n if (fromName) {\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (brand.mention.test(fromName) && !isOwnBrand(brand, ownNames, ownRegistrables)) {\n signals.push({ code: 'brand-sender', brand: brand.id, fromName: fromName.slice(0, 120) })\n }\n }\n }\n\n // 3. Brand + lure + a link to somewhere that is nobody's we know.\n const lure = PHISHING_LURE_PATTERNS.map((pattern) => decodedCopy.match(pattern)?.[0])\n .find(Boolean)\n if (lure) {\n const foreign = linkHosts.find((host) => {\n if (isOwnHost(host)) return false\n const registrable = registrableDomain(host)\n return !isAnyOfficialDomain(registrable) && !COMMON_LINK_DOMAINS.has(registrable)\n })\n if (foreign) {\n const namedInCopy = `${decodedCopy}\\n${fromName}`\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (brand.mention.test(namedInCopy) && !isOwnBrand(brand, ownNames, ownRegistrables)) {\n signals.push({\n code: 'brand-lure-link',\n brand: brand.id,\n lure: lure.slice(0, 120),\n host: foreign,\n })\n break\n }\n }\n }\n }\n\n return { hold: signals.length > 0, signals }\n}\n\n/** The brand's label for a stored id, for the staff surface. */\nexport function phishingScreenBrandLabel(id: string): string {\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.id === id)?.label ?? id\n}\n\n/** One line per signal, in words staff read on the review row. */\nexport function describePhishingScreenSignals(\n signals: readonly PhishingScreenSignal[],\n): string[] {\n return signals.map((signal) => {\n const brand = 'brand' in signal ? phishingScreenBrandLabel(signal.brand) : ''\n switch (signal.code) {\n case 'lookalike-link':\n return `Links to ${signal.host}, which wears the ${brand} name but is not ${brand}'s domain.`\n case 'brand-sender':\n return `Sends as \"${signal.fromName}\", naming ${brand}, which is not this workspace.`\n case 'brand-lure-link':\n return `Names ${brand}, asks the reader to act (\"${signal.lure}\"), and links to ${signal.host}.`\n case 'credential-field':\n return `Asks for a ${\n signal.field === 'card' ? 'card number' : signal.field === 'otp' ? 'one-time code' : 'password'\n } in its own field (\"${signal.label}\"), outside the platform's sign-in and checkout elements.`\n case 'brand-action-page':\n return `Names ${brand} beside a call to action (\"${signal.action}\"), and this workspace is not ${brand}.`\n default:\n return 'Phishing signal.'\n }\n })\n}\n\n/*==========================================\n * RISK TIERS: who a signal holds for (AGL-3356, widened to every surface).\n *\n * The screen above finds signals; this decides which of them hold, the same\n * way on every surface that asks — an email at the send seam, a campaign\n * before it is claimed, a page at publish, a subdomain at creation.\n *\n * - STRONG: a lookalike host. A link to `poshmark.id63835663.shop` or\n * `paypa1.com` is a disguise whoever wears it and however old the\n * workspace is, so it holds for EVERY workspace, on every message and\n * page, transactional mail included. A merchant has no ordinary reason to\n * link to a host shaped like somebody else's brand.\n * - SOFT: a brand in the sender name, the three-part lure, a brand's name\n * beside a sign-in or payment call to action on a page. Each is shaped\n * like phishing but also like a clumsy legitimate message, so they hold\n * only for a workspace in its first {@link OUTBOUND_REVIEW_YOUNG_DAYS}\n * days — the incident's window — and NEVER for mail the recipient's own\n * act made owed ({@link OutboundScreenPolicy.owed}).\n *\n * A credential-harvest page (a password, card or one-time-code field that\n * is not one of the platform's own sign-in or checkout elements) is STRONG:\n * see `hosted-page-screen.ts`.\n *=========================================*/\n\n/** A workspace younger than this many days has the soft rules applied. */\nexport const OUTBOUND_REVIEW_YOUNG_DAYS = 14\n\n/** Which tier a signal belongs to. */\nexport type PhishingSignalTier = 'strong' | 'soft'\n\n/** The signal codes that hold for every workspace. */\nexport const STRONG_PHISHING_SIGNAL_CODES: ReadonlySet<string> = new Set([\n 'lookalike-link',\n 'credential-field',\n])\n\nexport function phishingSignalTier(signal: { code: string }): PhishingSignalTier {\n return STRONG_PHISHING_SIGNAL_CODES.has(signal?.code) ? 'strong' : 'soft'\n}\n\n/** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */\nexport function isYoungWorkspaceAge(ageDays: number | null | undefined): boolean {\n return typeof ageDays === 'number' && Number.isFinite(ageDays) && ageDays < OUTBOUND_REVIEW_YOUNG_DAYS\n}\n\nexport interface OutboundScreenPolicy {\n /**\n * The workspace's age in days, or `null` when its creation date cannot be\n * read — an org that predates the field, which is an EXISTING customer and\n * is not young.\n */\n ageDays: number | null\n /**\n * The recipient's own act made this message owed: a receipt for their\n * order, a confirmation of their booking, a reset of their password. The\n * soft rules never hold it. The lookalike rule still does — a receipt that\n * links to a Poshmark lookalike is the disguise, not the receipt.\n */\n owed?: boolean\n}\n\n/** The signals that HOLD under the tiers, for this workspace and message. */\nexport function signalsThatHold<T extends { code: string }>(\n signals: readonly T[],\n policy: OutboundScreenPolicy,\n): T[] {\n const soft = isYoungWorkspaceAge(policy.ageDays) && !policy.owed\n return signals.filter((signal) => phishingSignalTier(signal) === 'strong' || soft)\n}\n\n/**\n * The same brand, lookalike and tier logic applied to a HOST NAME a workspace\n * asks for — a subdomain on the tenant apex, or a custom domain it attaches.\n *\n * Words a brand's impersonator hyphen-joins to its name. A subdomain that\n * joins a brand's word to one of these (`booking-review`, `apple-support`,\n * `amazon-account`) is the incident's shape even where the brand's word is\n * an ordinary one — which the lookalike rule alone deliberately lets pass,\n * so a salon can be `tanyas-booking` and a bakery `apple-pie-co`.\n */\nexport const BRAND_IMPERSONATION_WORDS: ReadonlySet<string> = new Set([\n 'account', 'accounts', 'auth', 'billing', 'help', 'helpdesk', 'id', 'login',\n 'logon', 'order', 'orders', 'pay', 'payment', 'payments', 'refund', 'resolution',\n 'review', 'reviews', 'secure', 'security', 'seller', 'service', 'signin',\n 'support', 'update', 'verify', 'verification', 'wallet',\n])\n\n/**\n * The brand a requested subdomain label wears, or null.\n *\n * Refused for EVERY workspace, at creation and at rename — the tier is\n * strong, because nothing is lost by asking a real bakery to pick\n * `apple-pie-co` instead of `apple-support`, and a brand's name on our apex\n * is a disguise that outlives every later screen. Two shapes:\n *\n * 1. the label is a lookalike host label ({@link lookalikeBrandForHost} on\n * `<label>.<apex>`): `poshmark`, `paypal-secure`, `paypa1`, `appleid`;\n * 2. a brand's word hyphen-joined to an impersonation word\n * ({@link BRAND_IMPERSONATION_WORDS}): `booking-review`, `apple-support`.\n */\nexport function brandForSubdomainLabel(\n label: string,\n apex = 'example.invalid',\n): PhishingScreenBrand | null {\n const normalized = String(label ?? '').trim().toLowerCase()\n if (!normalized) return null\n const byHost = lookalikeBrandForHost(`${normalized}.${apex}`)\n if (byHost) return byHost\n const parts = foldLeet(normalized).split('-').filter(Boolean)\n if (parts.length < 2 || !parts.some((part) => BRAND_IMPERSONATION_WORDS.has(part))) {\n return null\n }\n for (const brand of PHISHING_SCREEN_BRANDS) {\n const words = [brand.id, ...brand.hostTokens].filter((word) => !word.includes('-'))\n if (parts.some((part) => words.includes(part))) return brand\n }\n return null\n}\n\n/**\n * The platform's own entry in {@link PHISHING_SCREEN_BRANDS} (AGL-3365). A\n * workspace owns every other brand by carrying its name or domain; the\n * platform's is owned only by the platform's staff, because a workspace NAMED\n * for the platform is the cheapest disguise there is.\n */\nexport const PLATFORM_PHISHING_BRAND_ID = 'aglyn'\n\n/** The platform's brand entry. */\nexport function platformPhishingBrand(): PhishingScreenBrand {\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.id === PLATFORM_PHISHING_BRAND_ID) as PhishingScreenBrand\n}\n\n/**\n * Words that make a name claim to SPEAK FOR a brand rather than mention it:\n * \"PayPal Support\", \"Aglyn Official\", \"Apple ID Security Team\".\n */\nconst OFFICIAL_CLAIM_PATTERN =\n /\\b(?:official|staff|team|verified|certified|support|helpdesk|admin|administrator|security|billing|trust|safety|compliance|core|headquarters|hq)\\b/i\n\n/**\n * The brand a display NAME claims to speak for (AGL-3365), or null.\n *\n * For names a stranger reads as an identity — a marketplace publisher, a\n * listing's title — rather than copy. A name that merely mentions a brand\n * (\"Sync for PayPal\", \"Bookings for Airbnb hosts\") passes: that is how an\n * integration is named. A name that pairs the brand with a claim of\n * officialness (\"PayPal Support\", \"Aglyn Official Plugins\") is the disguise.\n */\nexport function brandClaimedByName(name: string): PhishingScreenBrand | null {\n const text = String(name ?? '')\n if (!text.trim() || !OFFICIAL_CLAIM_PATTERN.test(text)) return null\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.mention.test(text)) ?? null\n}\n"],"names":["PHISHING_SCREEN_BRANDS","id","label","mention","hostTokens","officialDomains","countryDomains","wordToken","hostedDomains","PHISHING_LURE_PATTERNS","COMMON_LINK_DOMAINS","Set","TWO_LABEL_SUFFIXES","registrableDomain","host","labels","String","toLowerCase","replace","split","filter","Boolean","length","join","lastTwo","slice","has","splitRegistrable","registrable","dot","indexOf","suffix","isOfficialDomain","brand","includes","base","test","isAnyOfficialDomain","some","foldLeet","labelNamesBrand","token","candidate","parts","tokenParts","at","every","part","offset","lookalikeBrandForHost","normalized","primary","startsWith","decodeEntities","text","_","hex","safeCodePoint","parseInt","dec","value","Number","isFinite","fromCodePoint","URL_PATTERN","linkHostsIn","hosts","match","rest","lastIndexOf","disguise","add","ADDRESS_HOST_PATTERN","addressHostsIn","matchAll","squash","isOwnBrand","ownNames","ownRegistrables","map","squashScreenText","isWorkspaceOwnBrand","isAnyOfficialBrandDomain","isCommonLinkDomain","screenOutboundEmail","input","signals","ownHosts","ownDomains","domain","trim","isOwnHost","own","endsWith","copy","subject","preheader","bodies","decodedCopy","replyTo","Array","isArray","linkHosts","seenLookalike","fromAddress","push","code","fromName","lure","pattern","find","foreign","namedInCopy","hold","phishingScreenBrandLabel","describePhishingScreenSignals","signal","field","action","OUTBOUND_REVIEW_YOUNG_DAYS","STRONG_PHISHING_SIGNAL_CODES","phishingSignalTier","isYoungWorkspaceAge","ageDays","signalsThatHold","policy","soft","owed","BRAND_IMPERSONATION_WORDS","brandForSubdomainLabel","apex","byHost","words","word","PLATFORM_PHISHING_BRAND_ID","platformPhishingBrand","OFFICIAL_CLAIM_PATTERN","brandClaimedByName","name"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/shared/util/email/src/lib/outbound-phishing-screen.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 * THE OUTBOUND PHISHING SCREEN (AGL-3356).\n *\n * A fraud actor signed up, paid with a card that never met 3-D Secure, and\n * used a workspace to send two kinds of mail: a campaign of Booking.com-style\n * \"guest feedback regarding your property\" lures to hotels, and a workflow\n * that mailed \"Poshmark Order — one of the items from your Seller Account has\n * finally sold\", linking to `poshmark.id63835663.shop`.\n *\n * This module answers one question about a message a YOUNG workspace is\n * about to send: does its content carry a STRONG phishing signal? The caller\n * decides who is young and what happens to a positive answer — the answer is\n * a hold for staff review, never a drop.\n *\n * ## Conservative by construction\n *\n * A false positive costs a real merchant's first campaign a wait for a\n * human; a miss costs strangers their bank logins. Both are real, so only\n * three shapes hold, and each is one a legitimate merchant essentially never\n * produces:\n *\n * 1. **A lookalike link** — a link or reply address whose host carries a\n * brand's name without being the brand's own domain: the brand as a label\n * of an unrelated registrable domain (`poshmark.id63835663.shop`), hyphen-\n * joined to one (`paypal-secure.com`, `secure-paypal.net`), the brand's\n * real domain embedded in a longer host (`booking.com.guest-review.top`),\n * or leetspeak for it (`paypa1.com`). Holds on its own.\n * 2. **A brand in the sender's display name** that is not the workspace's\n * own name — mail that says it is FROM PayPal.\n * 3. **The three-part lure**: a brand named in the copy, AND credential or\n * account-action phrasing (\"verify your account\", \"has finally sold\",\n * \"feedback regarding your property\"), AND a link to a domain that is\n * neither the brand's, nor the workspace's, nor a common social or maps\n * link. Any two of the three are ordinary marketing — \"our book is on\n * Amazon\", \"sign in to your account\", a link to the shop — and pass.\n *\n * Mentioning a brand, alone, never holds.\n *\n * ## The brand list is small on purpose\n *\n * The brands phishing kits most often wear, plus the two in the incident. A\n * brand is added when it has been seen impersonated, not because it is big:\n * every entry is a set of words that can now hold somebody's newsletter.\n * Each carries its REAL domains, so a link to the brand itself is never\n * suspicious, and host tokens chosen so a common word (`booking`, `apple`,\n * `outlook`) does not match an unrelated host by itself.\n *\n * Pure: no DNS, no store, no clock. Everything the caller knows about the\n * workspace arrives as input.\n *=========================================*/\n\n/** One brand the screen recognizes. */\nexport interface PhishingScreenBrand {\n /** Stable id, as stored on a hold. */\n id: string\n /** How staff read it. */\n label: string\n /** Copy that names the brand — subject, body, sender. Case-insensitive. */\n mention: RegExp\n /**\n * Host labels that name the brand. Matched against each dot-separated\n * label of a link's host, whole or hyphen-joined, after leetspeak is\n * folded — never as a bare substring, which is how `amazonaws` or\n * `bookingengine` would read as a brand.\n */\n hostTokens: readonly string[]\n /**\n * Registrable domains the brand genuinely links from. The FIRST is its\n * primary domain, the one a kit embeds in a longer host.\n */\n officialDomains: readonly string[]\n /**\n * The brand's name is an ordinary word (`apple`, `amazon`), so a host\n * label names the brand only when it IS the word — `apple.example.top` —\n * and never hyphen-joined or leet-folded, which `apple-orchard-farm.com`\n * would otherwise be.\n */\n wordToken?: boolean\n /**\n * Whether `<brand>.<country code>` (and `<brand>.co.<cc>`) is the brand's\n * too — true for the marketplaces that run a site per country.\n */\n countryDomains?: boolean\n /**\n * Registrable domains the brand runs but where OTHER people's content lives —\n * the platform's own tenant apex, where every site is `<name>.<apex>`. A\n * host there never wears the brand as a disguise, so the lookalike rule\n * skips it; but it is not the brand's own domain either, so a link to a\n * neighbor's site still counts as \"somewhere else\" to the lure rules, and a\n * workspace hosted there does not own the brand by being hosted there.\n */\n hostedDomains?: readonly string[]\n}\n\nexport const PHISHING_SCREEN_BRANDS: readonly PhishingScreenBrand[] = [\n {\n id: 'poshmark',\n label: 'Poshmark',\n mention: /\\bposhmark\\b/i,\n hostTokens: ['poshmark'],\n officialDomains: ['poshmark.com', 'posh.mk'],\n },\n {\n id: 'booking',\n label: 'Booking.com',\n // Never the bare word: \"booking\" is every hotel's and every salon's.\n mention: /\\bbooking\\.com\\b/i,\n hostTokens: ['bookingcom', 'booking-com'],\n officialDomains: ['booking.com', 'bstatic.com'],\n },\n {\n id: 'airbnb',\n label: 'Airbnb',\n mention: /\\bairbnb\\b/i,\n hostTokens: ['airbnb'],\n officialDomains: ['airbnb.com', 'abnb.me', 'muscache.com'],\n countryDomains: true,\n },\n {\n id: 'paypal',\n label: 'PayPal',\n mention: /\\bpay\\s?pal\\b/i,\n hostTokens: ['paypal'],\n officialDomains: ['paypal.com', 'paypal.me', 'paypalobjects.com'],\n countryDomains: true,\n },\n {\n id: 'amazon',\n label: 'Amazon',\n mention: /\\bamazon\\b/i,\n hostTokens: ['amazon', 'amzn'],\n wordToken: true,\n officialDomains: [\n 'amazon.com',\n 'amzn.to',\n 'amzn.com',\n 'amazonaws.com',\n 'media-amazon.com',\n 'ssl-images-amazon.com',\n ],\n countryDomains: true,\n },\n {\n id: 'apple',\n label: 'Apple',\n // The fruit is on every bakery's menu; the account is what gets phished.\n mention: /\\bapple\\s?(?:id|support|account)\\b|\\bicloud\\b|\\bitunes\\b/i,\n hostTokens: ['apple', 'appleid', 'icloud', 'itunes'],\n wordToken: true,\n officialDomains: ['apple.com', 'icloud.com', 'apple.co', 'itunes.com', 'me.com', 'mzstatic.com'],\n countryDomains: true,\n },\n {\n id: 'microsoft',\n label: 'Microsoft',\n mention: /\\bmicrosoft\\b|\\boffice\\s?365\\b|\\bonedrive\\b|\\bsharepoint\\b/i,\n hostTokens: ['microsoft', 'office365', 'onedrive', 'sharepoint', 'microsoftonline', 'msonline'],\n officialDomains: [\n 'microsoft.com',\n 'office.com',\n 'office365.com',\n 'live.com',\n 'outlook.com',\n 'microsoftonline.com',\n 'sharepoint.com',\n 'onedrive.com',\n '1drv.ms',\n 'aka.ms',\n ],\n },\n {\n id: 'docusign',\n label: 'DocuSign',\n mention: /\\bdocu\\s?sign\\b/i,\n hostTokens: ['docusign'],\n officialDomains: ['docusign.com', 'docusign.net'],\n },\n {\n id: 'netflix',\n label: 'Netflix',\n mention: /\\bnetflix\\b/i,\n hostTokens: ['netflix'],\n officialDomains: ['netflix.com', 'nflxext.com', 'nflximg.net'],\n },\n {\n id: 'wellsfargo',\n label: 'Wells Fargo',\n mention: /\\bwells\\s?fargo\\b/i,\n hostTokens: ['wellsfargo', 'wells-fargo'],\n officialDomains: ['wellsfargo.com'],\n },\n {\n id: 'bankofamerica',\n label: 'Bank of America',\n mention: /\\bbank\\s+of\\s+america\\b/i,\n hostTokens: ['bankofamerica', 'bofa'],\n officialDomains: ['bankofamerica.com', 'bofa.com'],\n },\n {\n id: 'coinbase',\n label: 'Coinbase',\n mention: /\\bcoinbase\\b/i,\n hostTokens: ['coinbase'],\n officialDomains: ['coinbase.com'],\n },\n {\n id: 'usps',\n label: 'USPS',\n mention: /\\busps\\b/i,\n hostTokens: ['usps'],\n officialDomains: ['usps.com'],\n },\n {\n id: 'fedex',\n label: 'FedEx',\n mention: /\\bfed\\s?ex\\b/i,\n hostTokens: ['fedex'],\n officialDomains: ['fedex.com'],\n },\n {\n id: 'dhl',\n label: 'DHL',\n mention: /\\bdhl\\b/i,\n hostTokens: ['dhl'],\n officialDomains: ['dhl.com'],\n countryDomains: true,\n },\n {\n // The platform itself (AGL-3365). A marketplace listing, a page or an\n // email that wears the platform's name — \"Aglyn Support\", a link to\n // `aglyn-billing.com`, \"official Aglyn plugin\" — is the one impersonation\n // every customer is primed to trust. The workspace named for it (the\n // platform's own org) owns it by name, as any brand's own workspace does.\n id: 'aglyn',\n label: 'Aglyn',\n mention: /\\baglyn\\b/i,\n hostTokens: ['aglyn'],\n officialDomains: ['aglyn.com', 'aglyn.io', 'aglyn.dev'],\n hostedDomains: ['aglyn.app'],\n },\n]\n\n/**\n * Account-action and credential phrasing — the half of a lure that asks the\n * reader to DO something with an account. Only ever counted beside a brand\n * AND a foreign link; alone, several of these are ordinary transactional copy.\n */\nexport const PHISHING_LURE_PATTERNS: readonly RegExp[] = [\n /\\bverify\\s+your\\s+(?:account|identity|information|details|payment|email)\\b/i,\n /\\bconfirm\\s+your\\s+(?:account|identity|details|payment|password|billing|information)\\b/i,\n /\\b(?:account|payment|card)\\s+(?:has\\s+been\\s+|is\\s+|was\\s+)?(?:suspended|locked|limited|restricted|disabled|on\\s+hold)\\b/i,\n /\\bunusual\\s+(?:sign[-\\s]?in|login|activity)\\b/i,\n /\\bupdate\\s+your\\s+(?:payment|billing|card|account)\\s*(?:details|information|method)?\\b/i,\n /\\b(?:sign|log)\\s?in\\s+to\\s+(?:your|view|confirm|claim|release)\\b/i,\n /\\b(?:has|have)\\s+(?:finally\\s+)?sold\\b/i,\n /\\bseller\\s+account\\b/i,\n /\\b(?:guest|customer)\\s+(?:complaint|review|feedback)\\b/i,\n /\\bfeedback\\s+regarding\\s+your\\s+(?:property|listing|stay|booking|reservation)\\b/i,\n /\\b(?:document|invoice|agreement)s?\\s+(?:is\\s+|are\\s+)?(?:ready|waiting|pending)\\s+(?:for\\s+)?(?:your\\s+)?(?:review|signature|to\\s+sign)\\b/i,\n /\\bpassword\\s+(?:will\\s+)?expire/i,\n /\\bclaim\\s+your\\s+(?:refund|reward|prize|package|payment|funds)\\b/i,\n /\\b(?:delivery|shipment)\\s+(?:failed|attempt|suspended|on\\s+hold)\\b/i,\n /\\b(?:package|parcel)\\s+(?:is\\s+)?(?:on\\s+hold|held|awaiting|undeliverable)\\b/i,\n /\\bsecurity\\s+alert\\b/i,\n]\n\n/**\n * Link targets a merchant's mail ordinarily carries that are not theirs:\n * social profiles, maps, review sites, video. Counted as neither foreign nor\n * suspicious in the three-part lure, so \"leave us a review on Google\" with a\n * brand named elsewhere does not become a hold. Never consulted by the\n * lookalike check, which holds on the host's own shape.\n */\nconst COMMON_LINK_DOMAINS: ReadonlySet<string> = new Set([\n 'facebook.com',\n 'fb.com',\n 'instagram.com',\n 'x.com',\n 'twitter.com',\n 'linkedin.com',\n 'youtube.com',\n 'youtu.be',\n 'tiktok.com',\n 'pinterest.com',\n 'google.com',\n 'goo.gl',\n 'g.page',\n 'yelp.com',\n 'tripadvisor.com',\n 'wa.me',\n 'whatsapp.com',\n])\n\n/**\n * Two-label public suffixes, so `evil.co.uk` registers as `evil.co.uk` and\n * not as `co.uk`. Deliberately a short list of the ones mail actually\n * arrives from rather than the whole public suffix list: a suffix missing\n * here makes the registrable domain one label too short, which can only\n * widen what counts as \"the same domain\" for a link — never invent a brand\n * label that is not in the host.\n */\nconst TWO_LABEL_SUFFIXES: ReadonlySet<string> = new Set([\n 'co.uk', 'org.uk', 'ac.uk', 'gov.uk', 'me.uk',\n 'com.au', 'net.au', 'org.au',\n 'co.nz', 'co.jp', 'co.kr', 'co.in', 'co.za', 'co.il', 'co.id', 'co.th',\n 'com.br', 'com.mx', 'com.ar', 'com.tr', 'com.cn', 'com.hk', 'com.sg',\n 'com.tw', 'com.my', 'com.ph', 'com.co', 'com.pe', 'com.sa', 'com.eg',\n])\n\n/** A signal the screen found. */\nexport type PhishingScreenSignal =\n | { code: 'lookalike-link'; brand: string; host: string }\n | { code: 'brand-sender'; brand: string; fromName: string }\n | { code: 'brand-lure-link'; brand: string; lure: string; host: string }\n /**\n * A published page asks for a password, a card number or a one-time code\n * in a field that is not one of the platform's own sign-in or checkout\n * elements (`hosted-page-screen.ts`). Strong.\n */\n | { code: 'credential-field'; field: 'password' | 'card' | 'otp'; label: string }\n /**\n * A published page names a brand that is not the workspace's beside a\n * sign-in, verify or payment call to action (`hosted-page-screen.ts`).\n * Soft.\n */\n | { code: 'brand-action-page'; brand: string; action: string }\n\nexport interface PhishingScreenInput {\n subject?: string | null\n /** The display name in front of the sending address. */\n fromName?: string | null\n /**\n * The ADDRESS the message leaves from (AGL-3362) — a `From:` domain is a\n * link the recipient never has to click, so its host is read by the\n * lookalike rule exactly like a link's: mail from `support@paypa1.com` is\n * the disguise whoever sends it. Not an own domain for the lure's \"is this\n * elsewhere\" test either way; it only ever adds a lookalike signal.\n */\n fromAddress?: string | null\n replyTo?: string | readonly string[] | null\n preheader?: string | null\n /**\n * Everything else the recipient reads or clicks: the body, the plain-text\n * part, a design's nodes serialized — any string. Links are found by\n * shape, so a JSON-encoded design is as readable here as HTML.\n */\n bodies?: readonly (string | null | undefined)[]\n /**\n * The workspace's own names — org, site, subdomain. A brand in one of them\n * is the workspace's own, and `brand-sender` / `brand-lure-link` do not\n * fire for it. The lookalike check does not consult names: a workspace\n * NAMED \"Poshmark\" linking to `poshmark.id63835663.shop` is the incident.\n */\n ownNames?: readonly (string | null | undefined)[]\n /**\n * The workspace's own hosts — its subdomain on the tenant apex, custom\n * domain, sending domain. A link to one of them, or below one, is never\n * the \"somewhere else\" of a lure. Matched by host, never by registrable\n * domain: every site's subdomain shares the tenant apex, and a link to a\n * NEIGHBOR's site is somewhere else.\n *\n * Not an exemption from the lookalike check. A host shaped like\n * `paypal-secure.com` is a lookalike whoever attached it, and a workspace\n * that attached it has attached the disguise.\n */\n ownDomains?: readonly (string | null | undefined)[]\n}\n\nexport interface PhishingScreenVerdict {\n /** True when the message should be held for staff review. */\n hold: boolean\n signals: PhishingScreenSignal[]\n}\n\n/** The registrable domain of a host: `a.b.evil.co.uk` → `evil.co.uk`. */\nexport function registrableDomain(host: string): string {\n const labels = String(host ?? '')\n .toLowerCase()\n .replace(/\\.+$/, '')\n .split('.')\n .filter(Boolean)\n if (labels.length <= 2) return labels.join('.')\n const lastTwo = labels.slice(-2).join('.')\n return TWO_LABEL_SUFFIXES.has(lastTwo)\n ? labels.slice(-3).join('.')\n : lastTwo\n}\n\n/** `<label>.<suffix>` of a registrable domain, split once. */\nfunction splitRegistrable(registrable: string): { label: string; suffix: string } {\n const dot = registrable.indexOf('.')\n return dot < 0\n ? { label: registrable, suffix: '' }\n : { label: registrable.slice(0, dot), suffix: registrable.slice(dot + 1) }\n}\n\nfunction isOfficialDomain(brand: PhishingScreenBrand, registrable: string): boolean {\n if (brand.officialDomains.includes(registrable)) return true\n if (!brand.countryDomains) return false\n const base = splitRegistrable(brand.officialDomains[0]).label\n const { label, suffix } = splitRegistrable(registrable)\n return label === base && (/^[a-z]{2}$/.test(suffix) || TWO_LABEL_SUFFIXES.has(suffix))\n}\n\n/**\n * Is the workspace the brand itself, by DOMAIN: one of its own domains is\n * one of the brand's official ones (Aglyn's site on aglyn.com linking to\n * its own account on a video or help-desk vendor). Deliberately not by name — a workspace can call itself\n * \"Poshmark\", but it cannot serve on poshmark.com.\n */\nexport function isBrandsOwnDomain(\n brand: PhishingScreenBrand,\n ownRegistrables: ReadonlySet<string>,\n): boolean {\n return [...ownRegistrables].some((registrable) => isOfficialDomain(brand, registrable))\n}\n\n/** Every brand's real domains, for \"is this link just the brand itself\". */\nfunction isAnyOfficialDomain(registrable: string): boolean {\n return PHISHING_SCREEN_BRANDS.some((brand) => isOfficialDomain(brand, registrable))\n}\n\n/** Leetspeak folded, so `paypa1` reads as `paypal`. */\nfunction foldLeet(label: string): string {\n return label\n .replace(/0/g, 'o')\n .replace(/1/g, 'l')\n .replace(/3/g, 'e')\n .replace(/4/g, 'a')\n .replace(/5/g, 's')\n .replace(/7/g, 't')\n}\n\n/** Does one host label name the brand — whole, or hyphen-joined? */\nfunction labelNamesBrand(label: string, token: string, wordToken: boolean): boolean {\n if (wordToken) return label === token\n for (const candidate of new Set([label, foldLeet(label)])) {\n if (candidate === token) return true\n const parts = candidate.split('-')\n // A hyphen-joined token (`booking-com`) spans parts, so it is matched\n // as a run of parts rather than one of them.\n const tokenParts = token.split('-')\n for (let at = 0; at + tokenParts.length <= parts.length; at += 1) {\n if (tokenParts.every((part, offset) => parts[at + offset] === part)) {\n return true\n }\n }\n }\n return false\n}\n\n/**\n * Video and audio platforms whose account subdomains and embed URLs a page\n * or email plays media from: `player.vimeo.com`, `www.youtube.com/embed/…`.\n * A video plugin's own hosts are the plugin's to declare, not listed here. Anyone may embed or link media from these, so a\n * host on one is never a lookalike, whatever name its subdomain carries.\n */\nexport const MEDIA_EMBED_HOSTS: ReadonlySet<string> = new Set([\n 'vimeo.com', 'vimeocdn.com', 'youtube.com', 'youtu.be',\n 'youtube-nocookie.com', 'ytimg.com', 'loom.com', 'vidyard.com', 'brightcove.net',\n 'brightcove.com', 'jwplayer.com', 'jwpcdn.com', 'mux.com', 'cloudflarestream.com',\n 'videodelivery.net', 'dailymotion.com', 'twitch.tv', 'spotify.com', 'soundcloud.com',\n 'podbean.com', 'buzzsprout.com', 'simplecast.com', 'transistor.fm', 'anchor.fm',\n])\n\n/** The brand a host impersonates, or null. */\nexport function lookalikeBrandForHost(host: string): PhishingScreenBrand | null {\n const normalized = String(host ?? '').toLowerCase().replace(/\\.+$/, '')\n if (!normalized.includes('.')) return null\n const registrable = registrableDomain(normalized)\n if (MEDIA_EMBED_HOSTS.has(registrable)) return null\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (isOfficialDomain(brand, registrable)) continue\n if (brand.hostedDomains?.includes(registrable)) continue\n const labels = normalized.split('.')\n if (\n labels.some((label) =>\n brand.hostTokens.some((token) => labelNamesBrand(label, token, brand.wordToken === true)),\n )\n ) {\n return brand\n }\n // The brand's primary domain embedded in a longer, different one:\n // `booking.com.guest-review.top`, `paypal.com-secure.net`. Only the\n // primary: a short secondary (`me.com`, `aka.ms`) would match too much.\n const primary = brand.officialDomains[0]\n if (\n normalized.startsWith(`${primary}.`) ||\n normalized.startsWith(`${primary}-`) ||\n normalized.includes(`.${primary}.`) ||\n normalized.includes(`.${primary}-`)\n ) {\n return brand\n }\n }\n return null\n}\n\n/**\n * The few entity forms a phishing kit uses to keep a URL away from a naive\n * scanner (`&#46;` for a dot, `&amp;` inside a query). Decoded before links\n * are read; everything else is left alone.\n */\nfunction decodeEntities(text: string): string {\n return text\n .replace(/&#x([0-9a-f]{1,6});/gi, (_, hex: string) => safeCodePoint(parseInt(hex, 16)))\n .replace(/&#(\\d{1,7});/g, (_, dec: string) => safeCodePoint(parseInt(dec, 10)))\n .replace(/&amp;/gi, '&')\n}\n\nfunction safeCodePoint(value: number): string {\n return Number.isFinite(value) && value > 0 && value <= 0x10ffff\n ? String.fromCodePoint(value)\n : ''\n}\n\nconst URL_PATTERN = /\\b(?:https?:\\/\\/|www\\.)[^\\s\"'<>()\\\\[\\]{}]+/gi\n\n/** The hosts every link in the text points at, lowercased. */\nexport function linkHostsIn(text: string): string[] {\n const hosts = new Set<string>()\n for (const match of decodeEntities(String(text ?? '')).match(URL_PATTERN) ?? []) {\n let rest = match.replace(/^https?:\\/\\//i, '')\n rest = rest.split(/[/?#]/)[0]\n // `https://paypal.com@evil.top/` resolves to evil.top — and the part\n // before the `@` is exactly the disguise, so it is kept as a host too.\n const at = rest.lastIndexOf('@')\n const disguise = at >= 0 ? rest.slice(0, at) : ''\n rest = at >= 0 ? rest.slice(at + 1) : rest\n for (const candidate of [rest, disguise]) {\n const host = candidate.replace(/:\\d+$/, '').replace(/\\.+$/, '').toLowerCase()\n // A merge tag inside a URL is not a host anybody can visit yet.\n if (host && host.includes('.') && !host.includes('{') && !host.includes('%7b')) {\n hosts.add(host)\n }\n }\n }\n return [...hosts]\n}\n\nconst ADDRESS_HOST_PATTERN = /[a-z0-9._%+-]+@([a-z0-9-]+(?:\\.[a-z0-9-]+)+)/gi\n\n/** The host of every email address in the text — a reply-to, a `mailto:`. */\nfunction addressHostsIn(text: string): string[] {\n const hosts = new Set<string>()\n for (const match of decodeEntities(String(text ?? '')).matchAll(ADDRESS_HOST_PATTERN)) {\n hosts.add(match[1].toLowerCase().replace(/\\.+$/, ''))\n }\n return [...hosts]\n}\n\n/** Lowercased alphanumerics only, so \"Wells-Fargo Advisors\" contains `wellsfargo`. */\nfunction squash(text: string): string {\n return text.toLowerCase().replace(/[^a-z0-9]/g, '')\n}\n\n/** Is this brand the workspace's own, by name or by a domain it holds? */\nfunction isOwnBrand(\n brand: PhishingScreenBrand,\n ownNames: string,\n ownRegistrables: ReadonlySet<string>,\n): boolean {\n const labels = [brand.id, squash(brand.label), ...brand.hostTokens.map(squash)]\n if (labels.some((label) => label && ownNames.includes(label))) return true\n return [...ownRegistrables].some((registrable) => isOfficialDomain(brand, registrable))\n}\n\n/*\n * The same judgements, for the other surfaces that read content with this\n * screen's data (`hosted-page-screen.ts`). Exported under these names rather\n * than copied, so a page and an email can never disagree about what a\n * brand's own domain is or whose brand a name is.\n */\n\n/** `squash`: lowercased alphanumerics, the form own-name matching reads. */\nexport const squashScreenText = squash\n/** Is this brand the workspace's own, by name (squashed) or by a domain it holds? */\nexport const isWorkspaceOwnBrand = isOwnBrand\n/** Is this registrable domain one of any listed brand's own? */\nexport const isAnyOfficialBrandDomain = isAnyOfficialDomain\n/** Is this registrable domain a social, maps or review link merchants ordinarily carry? */\nexport function isCommonLinkDomain(registrable: string): boolean {\n return COMMON_LINK_DOMAINS.has(registrable)\n}\n\n/**\n * Screen one outbound message. See the module header for what holds and why.\n */\nexport function screenOutboundEmail(input: PhishingScreenInput): PhishingScreenVerdict {\n const signals: PhishingScreenSignal[] = []\n const ownNames = squash((input.ownNames ?? []).filter(Boolean).join(' '))\n const ownHosts = (input.ownDomains ?? [])\n .map((domain) => String(domain ?? '').trim().toLowerCase().replace(/\\.+$/, ''))\n .filter((domain) => domain.includes('.'))\n const ownRegistrables = new Set(ownHosts.map(registrableDomain))\n const isOwnHost = (host: string) =>\n ownHosts.some((own) => host === own || host.endsWith(`.${own}`))\n\n const copy = [input.subject, input.preheader, ...(input.bodies ?? [])]\n .filter((part): part is string => typeof part === 'string' && part.length > 0)\n .join('\\n')\n const decodedCopy = decodeEntities(copy)\n const replyTo = Array.isArray(input.replyTo)\n ? input.replyTo.join(' ')\n : String(input.replyTo ?? '')\n const linkHosts = linkHostsIn(copy)\n\n // 1. A lookalike host, anywhere a recipient could click, write back, or\n // read as the sender.\n const seenLookalike = new Set<string>()\n const fromAddress = String(input.fromAddress ?? '')\n for (const host of [...linkHosts, ...addressHostsIn(`${copy}\\n${replyTo}\\n${fromAddress}`)]) {\n if (seenLookalike.has(host)) continue\n const brand = lookalikeBrandForHost(host)\n if (brand && !isBrandsOwnDomain(brand, ownRegistrables)) {\n seenLookalike.add(host)\n signals.push({ code: 'lookalike-link', brand: brand.id, host })\n }\n }\n\n // 2. A brand in the sender's display name that is not the workspace's.\n const fromName = String(input.fromName ?? '').trim()\n if (fromName) {\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (brand.mention.test(fromName) && !isOwnBrand(brand, ownNames, ownRegistrables)) {\n signals.push({ code: 'brand-sender', brand: brand.id, fromName: fromName.slice(0, 120) })\n }\n }\n }\n\n // 3. Brand + lure + a link to somewhere that is nobody's we know.\n const lure = PHISHING_LURE_PATTERNS.map((pattern) => decodedCopy.match(pattern)?.[0])\n .find(Boolean)\n if (lure) {\n const foreign = linkHosts.find((host) => {\n if (isOwnHost(host)) return false\n const registrable = registrableDomain(host)\n return !isAnyOfficialDomain(registrable) && !COMMON_LINK_DOMAINS.has(registrable)\n })\n if (foreign) {\n const namedInCopy = `${decodedCopy}\\n${fromName}`\n for (const brand of PHISHING_SCREEN_BRANDS) {\n if (brand.mention.test(namedInCopy) && !isOwnBrand(brand, ownNames, ownRegistrables)) {\n signals.push({\n code: 'brand-lure-link',\n brand: brand.id,\n lure: lure.slice(0, 120),\n host: foreign,\n })\n break\n }\n }\n }\n }\n\n return { hold: signals.length > 0, signals }\n}\n\n/** The brand's label for a stored id, for the staff surface. */\nexport function phishingScreenBrandLabel(id: string): string {\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.id === id)?.label ?? id\n}\n\n/** One line per signal, in words staff read on the review row. */\nexport function describePhishingScreenSignals(\n signals: readonly PhishingScreenSignal[],\n): string[] {\n return signals.map((signal) => {\n const brand = 'brand' in signal ? phishingScreenBrandLabel(signal.brand) : ''\n switch (signal.code) {\n case 'lookalike-link':\n return `Links to ${signal.host}, which wears the ${brand} name but is not ${brand}'s domain.`\n case 'brand-sender':\n return `Sends as \"${signal.fromName}\", naming ${brand}, which is not this workspace.`\n case 'brand-lure-link':\n return `Names ${brand}, asks the reader to act (\"${signal.lure}\"), and links to ${signal.host}.`\n case 'credential-field':\n return `Asks for a ${\n signal.field === 'card' ? 'card number' : signal.field === 'otp' ? 'one-time code' : 'password'\n } in its own field (\"${signal.label}\"), outside the platform's sign-in and checkout elements.`\n case 'brand-action-page':\n return `Names ${brand} beside a call to action (\"${signal.action}\"), and this workspace is not ${brand}.`\n default:\n return 'Phishing signal.'\n }\n })\n}\n\n/*==========================================\n * RISK TIERS: who a signal holds for (AGL-3356, widened to every surface).\n *\n * The screen above finds signals; this decides which of them hold, the same\n * way on every surface that asks — an email at the send seam, a campaign\n * before it is claimed, a page at publish, a subdomain at creation.\n *\n * - STRONG: a lookalike host. A link to `poshmark.id63835663.shop` or\n * `paypa1.com` is a disguise whoever wears it and however old the\n * workspace is, so it holds for EVERY workspace, on every message and\n * page, transactional mail included. A merchant has no ordinary reason to\n * link to a host shaped like somebody else's brand.\n * - SOFT: a brand in the sender name, the three-part lure, a brand's name\n * beside a sign-in or payment call to action on a page. Each is shaped\n * like phishing but also like a clumsy legitimate message, so they hold\n * only for a workspace in its first {@link OUTBOUND_REVIEW_YOUNG_DAYS}\n * days — the incident's window — and NEVER for mail the recipient's own\n * act made owed ({@link OutboundScreenPolicy.owed}).\n *\n * A credential-harvest page (a password, card or one-time-code field that\n * is not one of the platform's own sign-in or checkout elements) is STRONG:\n * see `hosted-page-screen.ts`.\n *=========================================*/\n\n/** A workspace younger than this many days has the soft rules applied. */\nexport const OUTBOUND_REVIEW_YOUNG_DAYS = 14\n\n/** Which tier a signal belongs to. */\nexport type PhishingSignalTier = 'strong' | 'soft'\n\n/** The signal codes that hold for every workspace. */\nexport const STRONG_PHISHING_SIGNAL_CODES: ReadonlySet<string> = new Set([\n 'lookalike-link',\n 'credential-field',\n])\n\n/**\n * Hosts that give each customer an account subdomain named after the\n * customer: `acme.zendesk.com` is Acme's own help desk. A brand's name there is usually the brand itself, so a\n * lookalike on one of these is SOFT — held for a young workspace, never a\n * takedown of an established one's page or mail. A lookalike on any other\n * host (`poshmark.id63835663.shop`) stays STRONG.\n */\nexport const ACCOUNT_SUBDOMAIN_HOSTS: ReadonlySet<string> = new Set([\n 'vimeo.com', 'vimeocdn.com', 'zendesk.com', 'freshdesk.com',\n 'helpscoutdocs.com', 'intercom.help', 'statuspage.io', 'atlassian.net', 'myshopify.com',\n 'squarespace.com', 'wixsite.com', 'webflow.io', 'hubspotpagebuilder.com', 'substack.com',\n 'typeform.com', 'calendly.com', 'gitbook.io', 'readme.io', 'notion.site', 'github.io',\n 'cloudfront.net', 'kajabi.com', 'thinkific.com', 'teachable.com', 'mailchimpsites.com',\n])\n\nexport function phishingSignalTier(signal: { code: string; host?: string }): PhishingSignalTier {\n if (!STRONG_PHISHING_SIGNAL_CODES.has(signal?.code)) return 'soft'\n if (\n signal.code === 'lookalike-link' &&\n typeof signal.host === 'string' &&\n ACCOUNT_SUBDOMAIN_HOSTS.has(registrableDomain(signal.host))\n ) {\n return 'soft'\n }\n return 'strong'\n}\n\n/** Is a workspace of this age (days, `null` = unreadable) screened by the soft rules? */\nexport function isYoungWorkspaceAge(ageDays: number | null | undefined): boolean {\n return typeof ageDays === 'number' && Number.isFinite(ageDays) && ageDays < OUTBOUND_REVIEW_YOUNG_DAYS\n}\n\nexport interface OutboundScreenPolicy {\n /**\n * The workspace's age in days, or `null` when its creation date cannot be\n * read — an org that predates the field, which is an EXISTING customer and\n * is not young.\n */\n ageDays: number | null\n /**\n * The recipient's own act made this message owed: a receipt for their\n * order, a confirmation of their booking, a reset of their password. The\n * soft rules never hold it. The lookalike rule still does — a receipt that\n * links to a Poshmark lookalike is the disguise, not the receipt.\n */\n owed?: boolean\n}\n\n/** The signals that HOLD under the tiers, for this workspace and message. */\nexport function signalsThatHold<T extends { code: string; host?: string }>(\n signals: readonly T[],\n policy: OutboundScreenPolicy,\n): T[] {\n const soft = isYoungWorkspaceAge(policy.ageDays) && !policy.owed\n return signals.filter((signal) => phishingSignalTier(signal) === 'strong' || soft)\n}\n\n/**\n * The same brand, lookalike and tier logic applied to a HOST NAME a workspace\n * asks for — a subdomain on the tenant apex, or a custom domain it attaches.\n *\n * Words a brand's impersonator hyphen-joins to its name. A subdomain that\n * joins a brand's word to one of these (`booking-review`, `apple-support`,\n * `amazon-account`) is the incident's shape even where the brand's word is\n * an ordinary one — which the lookalike rule alone deliberately lets pass,\n * so a salon can be `tanyas-booking` and a bakery `apple-pie-co`.\n */\nexport const BRAND_IMPERSONATION_WORDS: ReadonlySet<string> = new Set([\n 'account', 'accounts', 'auth', 'billing', 'help', 'helpdesk', 'id', 'login',\n 'logon', 'order', 'orders', 'pay', 'payment', 'payments', 'refund', 'resolution',\n 'review', 'reviews', 'secure', 'security', 'seller', 'service', 'signin',\n 'support', 'update', 'verify', 'verification', 'wallet',\n])\n\n/**\n * The brand a requested subdomain label wears, or null.\n *\n * Refused for EVERY workspace, at creation and at rename — the tier is\n * strong, because nothing is lost by asking a real bakery to pick\n * `apple-pie-co` instead of `apple-support`, and a brand's name on our apex\n * is a disguise that outlives every later screen. Two shapes:\n *\n * 1. the label is a lookalike host label ({@link lookalikeBrandForHost} on\n * `<label>.<apex>`): `poshmark`, `paypal-secure`, `paypa1`, `appleid`;\n * 2. a brand's word hyphen-joined to an impersonation word\n * ({@link BRAND_IMPERSONATION_WORDS}): `booking-review`, `apple-support`.\n */\nexport function brandForSubdomainLabel(\n label: string,\n apex = 'example.invalid',\n): PhishingScreenBrand | null {\n const normalized = String(label ?? '').trim().toLowerCase()\n if (!normalized) return null\n const byHost = lookalikeBrandForHost(`${normalized}.${apex}`)\n if (byHost) return byHost\n const parts = foldLeet(normalized).split('-').filter(Boolean)\n if (parts.length < 2 || !parts.some((part) => BRAND_IMPERSONATION_WORDS.has(part))) {\n return null\n }\n for (const brand of PHISHING_SCREEN_BRANDS) {\n const words = [brand.id, ...brand.hostTokens].filter((word) => !word.includes('-'))\n if (parts.some((part) => words.includes(part))) return brand\n }\n return null\n}\n\n/**\n * The platform's own entry in {@link PHISHING_SCREEN_BRANDS} (AGL-3365). A\n * workspace owns every other brand by carrying its name or domain; the\n * platform's is owned only by the platform's staff, because a workspace NAMED\n * for the platform is the cheapest disguise there is.\n */\nexport const PLATFORM_PHISHING_BRAND_ID = 'aglyn'\n\n/** The platform's brand entry. */\nexport function platformPhishingBrand(): PhishingScreenBrand {\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.id === PLATFORM_PHISHING_BRAND_ID) as PhishingScreenBrand\n}\n\n/**\n * Words that make a name claim to SPEAK FOR a brand rather than mention it:\n * \"PayPal Support\", \"Aglyn Official\", \"Apple ID Security Team\".\n */\nconst OFFICIAL_CLAIM_PATTERN =\n /\\b(?:official|staff|team|verified|certified|support|helpdesk|admin|administrator|security|billing|trust|safety|compliance|core|headquarters|hq)\\b/i\n\n/**\n * The brand a display NAME claims to speak for (AGL-3365), or null.\n *\n * For names a stranger reads as an identity — a marketplace publisher, a\n * listing's title — rather than copy. A name that merely mentions a brand\n * (\"Sync for PayPal\", \"Bookings for Airbnb hosts\") passes: that is how an\n * integration is named. A name that pairs the brand with a claim of\n * officialness (\"PayPal Support\", \"Aglyn Official Plugins\") is the disguise.\n */\nexport function brandClaimedByName(name: string): PhishingScreenBrand | null {\n const text = String(name ?? '')\n if (!text.trim() || !OFFICIAL_CLAIM_PATTERN.test(text)) return null\n return PHISHING_SCREEN_BRANDS.find((brand) => brand.mention.test(text)) ?? null\n}\n"],"names":["PHISHING_SCREEN_BRANDS","id","label","mention","hostTokens","officialDomains","countryDomains","wordToken","hostedDomains","PHISHING_LURE_PATTERNS","COMMON_LINK_DOMAINS","Set","TWO_LABEL_SUFFIXES","registrableDomain","host","labels","String","toLowerCase","replace","split","filter","Boolean","length","join","lastTwo","slice","has","splitRegistrable","registrable","dot","indexOf","suffix","isOfficialDomain","brand","includes","base","test","isBrandsOwnDomain","ownRegistrables","some","isAnyOfficialDomain","foldLeet","labelNamesBrand","token","candidate","parts","tokenParts","at","every","part","offset","MEDIA_EMBED_HOSTS","lookalikeBrandForHost","normalized","primary","startsWith","decodeEntities","text","_","hex","safeCodePoint","parseInt","dec","value","Number","isFinite","fromCodePoint","URL_PATTERN","linkHostsIn","hosts","match","rest","lastIndexOf","disguise","add","ADDRESS_HOST_PATTERN","addressHostsIn","matchAll","squash","isOwnBrand","ownNames","map","squashScreenText","isWorkspaceOwnBrand","isAnyOfficialBrandDomain","isCommonLinkDomain","screenOutboundEmail","input","signals","ownHosts","ownDomains","domain","trim","isOwnHost","own","endsWith","copy","subject","preheader","bodies","decodedCopy","replyTo","Array","isArray","linkHosts","seenLookalike","fromAddress","push","code","fromName","lure","pattern","find","foreign","namedInCopy","hold","phishingScreenBrandLabel","describePhishingScreenSignals","signal","field","action","OUTBOUND_REVIEW_YOUNG_DAYS","STRONG_PHISHING_SIGNAL_CODES","ACCOUNT_SUBDOMAIN_HOSTS","phishingSignalTier","isYoungWorkspaceAge","ageDays","signalsThatHold","policy","soft","owed","BRAND_IMPERSONATION_WORDS","brandForSubdomainLabel","apex","byHost","words","word","PLATFORM_PHISHING_BRAND_ID","platformPhishingBrand","OFFICIAL_CLAIM_PATTERN","brandClaimedByName","name"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAiD2C,GAE3C,qCAAqC,GA2CrC,OAAO,MAAMA,yBAAyD;IACpE;QACEC,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAW;QACxBC,iBAAiB;YAAC;YAAgB;SAAU;IAC9C;IACA;QACEJ,IAAI;QACJC,OAAO;QACP,qEAAqE;QACrEC,SAAS;QACTC,YAAY;YAAC;YAAc;SAAc;QACzCC,iBAAiB;YAAC;YAAe;SAAc;IACjD;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAS;QACtBC,iBAAiB;YAAC;YAAc;YAAW;SAAe;QAC1DC,gBAAgB;IAClB;IACA;QACEL,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAS;QACtBC,iBAAiB;YAAC;YAAc;YAAa;SAAoB;QACjEC,gBAAgB;IAClB;IACA;QACEL,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;YAAU;SAAO;QAC9BG,WAAW;QACXF,iBAAiB;YACf;YACA;YACA;YACA;YACA;YACA;SACD;QACDC,gBAAgB;IAClB;IACA;QACEL,IAAI;QACJC,OAAO;QACP,yEAAyE;QACzEC,SAAS;QACTC,YAAY;YAAC;YAAS;YAAW;YAAU;SAAS;QACpDG,WAAW;QACXF,iBAAiB;YAAC;YAAa;YAAc;YAAY;YAAc;YAAU;SAAe;QAChGC,gBAAgB;IAClB;IACA;QACEL,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;YAAa;YAAa;YAAY;YAAc;YAAmB;SAAW;QAC/FC,iBAAiB;YACf;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;IACH;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAW;QACxBC,iBAAiB;YAAC;YAAgB;SAAe;IACnD;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAU;QACvBC,iBAAiB;YAAC;YAAe;YAAe;SAAc;IAChE;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;YAAc;SAAc;QACzCC,iBAAiB;YAAC;SAAiB;IACrC;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;YAAiB;SAAO;QACrCC,iBAAiB;YAAC;YAAqB;SAAW;IACpD;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAW;QACxBC,iBAAiB;YAAC;SAAe;IACnC;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAO;QACpBC,iBAAiB;YAAC;SAAW;IAC/B;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAQ;QACrBC,iBAAiB;YAAC;SAAY;IAChC;IACA;QACEJ,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAM;QACnBC,iBAAiB;YAAC;SAAU;QAC5BC,gBAAgB;IAClB;IACA;QACE,sEAAsE;QACtE,oEAAoE;QACpE,0EAA0E;QAC1E,qEAAqE;QACrE,0EAA0E;QAC1EL,IAAI;QACJC,OAAO;QACPC,SAAS;QACTC,YAAY;YAAC;SAAQ;QACrBC,iBAAiB;YAAC;YAAa;YAAY;SAAY;QACvDG,eAAe;YAAC;SAAY;IAC9B;CACD,CAAA;AAED;;;;CAIC,GACD,OAAO,MAAMC,yBAA4C;IACvD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED;;;;;;CAMC,GACD,MAAMC,sBAA2C,IAAIC,IAAI;IACvD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;;;;CAOC,GACD,MAAMC,qBAA0C,IAAID,IAAI;IACtD;IAAS;IAAU;IAAS;IAAU;IACtC;IAAU;IAAU;IACpB;IAAS;IAAS;IAAS;IAAS;IAAS;IAAS;IAAS;IAC/D;IAAU;IAAU;IAAU;IAAU;IAAU;IAAU;IAC5D;IAAU;IAAU;IAAU;IAAU;IAAU;IAAU;CAC7D;AAmED,uEAAuE,GACvE,OAAO,SAASE,kBAAkBC,IAAY;IAC5C,MAAMC,SAASC,OAAOF,eAAAA,OAAQ,IAC3BG,WAAW,GACXC,OAAO,CAAC,QAAQ,IAChBC,KAAK,CAAC,KACNC,MAAM,CAACC;IACV,IAAIN,OAAOO,MAAM,IAAI,GAAG,OAAOP,OAAOQ,IAAI,CAAC;IAC3C,MAAMC,UAAUT,OAAOU,KAAK,CAAC,CAAC,GAAGF,IAAI,CAAC;IACtC,OAAOX,mBAAmBc,GAAG,CAACF,WAC1BT,OAAOU,KAAK,CAAC,CAAC,GAAGF,IAAI,CAAC,OACtBC;AACN;AAEA,4DAA4D,GAC5D,SAASG,iBAAiBC,WAAmB;IAC3C,MAAMC,MAAMD,YAAYE,OAAO,CAAC;IAChC,OAAOD,MAAM,IACT;QAAE3B,OAAO0B;QAAaG,QAAQ;IAAG,IACjC;QAAE7B,OAAO0B,YAAYH,KAAK,CAAC,GAAGI;QAAME,QAAQH,YAAYH,KAAK,CAACI,MAAM;IAAG;AAC7E;AAEA,SAASG,iBAAiBC,KAA0B,EAAEL,WAAmB;IACvE,IAAIK,MAAM5B,eAAe,CAAC6B,QAAQ,CAACN,cAAc,OAAO;IACxD,IAAI,CAACK,MAAM3B,cAAc,EAAE,OAAO;IAClC,MAAM6B,OAAOR,iBAAiBM,MAAM5B,eAAe,CAAC,EAAE,EAAEH,KAAK;IAC7D,MAAM,EAAEA,KAAK,EAAE6B,MAAM,EAAE,GAAGJ,iBAAiBC;IAC3C,OAAO1B,UAAUiC,QAAS,CAAA,aAAaC,IAAI,CAACL,WAAWnB,mBAAmBc,GAAG,CAACK,OAAM;AACtF;AAEA;;;;;CAKC,GACD,OAAO,SAASM,kBACdJ,KAA0B,EAC1BK,eAAoC;IAEpC,OAAO;WAAIA;KAAgB,CAACC,IAAI,CAAC,CAACX,cAAgBI,iBAAiBC,OAAOL;AAC5E;AAEA,0EAA0E,GAC1E,SAASY,oBAAoBZ,WAAmB;IAC9C,OAAO5B,uBAAuBuC,IAAI,CAAC,CAACN,QAAUD,iBAAiBC,OAAOL;AACxE;AAEA,qDAAqD,GACrD,SAASa,SAASvC,KAAa;IAC7B,OAAOA,MACJgB,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,MAAM,KACdA,OAAO,CAAC,MAAM;AACnB;AAEA,kEAAkE,GAClE,SAASwB,gBAAgBxC,KAAa,EAAEyC,KAAa,EAAEpC,SAAkB;IACvE,IAAIA,WAAW,OAAOL,UAAUyC;IAChC,KAAK,MAAMC,aAAa,IAAIjC,IAAI;QAACT;QAAOuC,SAASvC;KAAO,EAAG;QACzD,IAAI0C,cAAcD,OAAO,OAAO;QAChC,MAAME,QAAQD,UAAUzB,KAAK,CAAC;QAC9B,sEAAsE;QACtE,6CAA6C;QAC7C,MAAM2B,aAAaH,MAAMxB,KAAK,CAAC;QAC/B,IAAK,IAAI4B,KAAK,GAAGA,KAAKD,WAAWxB,MAAM,IAAIuB,MAAMvB,MAAM,EAAEyB,MAAM,EAAG;YAChE,IAAID,WAAWE,KAAK,CAAC,CAACC,MAAMC,SAAWL,KAAK,CAACE,KAAKG,OAAO,KAAKD,OAAO;gBACnE,OAAO;YACT;QACF;IACF;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,MAAME,oBAAyC,IAAIxC,IAAI;IAC5D;IAAa;IAAgB;IAAe;IAC5C;IAAwB;IAAa;IAAY;IAAe;IAChE;IAAkB;IAAgB;IAAc;IAAW;IAC3D;IAAqB;IAAmB;IAAa;IAAe;IACpE;IAAe;IAAkB;IAAkB;IAAiB;CACrE,EAAC;AAEF,4CAA4C,GAC5C,OAAO,SAASyC,sBAAsBtC,IAAY;IAChD,MAAMuC,aAAarC,OAAOF,eAAAA,OAAQ,IAAIG,WAAW,GAAGC,OAAO,CAAC,QAAQ;IACpE,IAAI,CAACmC,WAAWnB,QAAQ,CAAC,MAAM,OAAO;IACtC,MAAMN,cAAcf,kBAAkBwC;IACtC,IAAIF,kBAAkBzB,GAAG,CAACE,cAAc,OAAO;IAC/C,KAAK,MAAMK,SAASjC,uBAAwB;YAEtCiC;QADJ,IAAID,iBAAiBC,OAAOL,cAAc;QAC1C,KAAIK,uBAAAA,MAAMzB,aAAa,qBAAnByB,qBAAqBC,QAAQ,CAACN,cAAc;QAChD,MAAMb,SAASsC,WAAWlC,KAAK,CAAC;QAChC,IACEJ,OAAOwB,IAAI,CAAC,CAACrC,QACX+B,MAAM7B,UAAU,CAACmC,IAAI,CAAC,CAACI,QAAUD,gBAAgBxC,OAAOyC,OAAOV,MAAM1B,SAAS,KAAK,SAErF;YACA,OAAO0B;QACT;QACA,kEAAkE;QAClE,oEAAoE;QACpE,wEAAwE;QACxE,MAAMqB,UAAUrB,MAAM5B,eAAe,CAAC,EAAE;QACxC,IACEgD,WAAWE,UAAU,CAAC,GAAGD,QAAQ,CAAC,CAAC,KACnCD,WAAWE,UAAU,CAAC,GAAGD,QAAQ,CAAC,CAAC,KACnCD,WAAWnB,QAAQ,CAAC,CAAC,CAAC,EAAEoB,QAAQ,CAAC,CAAC,KAClCD,WAAWnB,QAAQ,CAAC,CAAC,CAAC,EAAEoB,QAAQ,CAAC,CAAC,GAClC;YACA,OAAOrB;QACT;IACF;IACA,OAAO;AACT;AAEA;;;;CAIC,GACD,SAASuB,eAAeC,IAAY;IAClC,OAAOA,KACJvC,OAAO,CAAC,yBAAyB,CAACwC,GAAGC,MAAgBC,cAAcC,SAASF,KAAK,MACjFzC,OAAO,CAAC,iBAAiB,CAACwC,GAAGI,MAAgBF,cAAcC,SAASC,KAAK,MACzE5C,OAAO,CAAC,WAAW;AACxB;AAEA,SAAS0C,cAAcG,KAAa;IAClC,OAAOC,OAAOC,QAAQ,CAACF,UAAUA,QAAQ,KAAKA,SAAS,WACnD/C,OAAOkD,aAAa,CAACH,SACrB;AACN;AAEA,MAAMI,cAAc;AAEpB,4DAA4D,GAC5D,OAAO,SAASC,YAAYX,IAAY;QAElBD;IADpB,MAAMa,QAAQ,IAAI1D;IAClB,KAAK,MAAM2D,UAASd,wBAAAA,eAAexC,OAAOyC,eAAAA,OAAQ,KAAKa,KAAK,CAACH,wBAAzCX,wBAAyD,EAAE,CAAE;QAC/E,IAAIe,OAAOD,MAAMpD,OAAO,CAAC,iBAAiB;QAC1CqD,OAAOA,KAAKpD,KAAK,CAAC,QAAQ,CAAC,EAAE;QAC7B,qEAAqE;QACrE,uEAAuE;QACvE,MAAM4B,KAAKwB,KAAKC,WAAW,CAAC;QAC5B,MAAMC,WAAW1B,MAAM,IAAIwB,KAAK9C,KAAK,CAAC,GAAGsB,MAAM;QAC/CwB,OAAOxB,MAAM,IAAIwB,KAAK9C,KAAK,CAACsB,KAAK,KAAKwB;QACtC,KAAK,MAAM3B,aAAa;YAAC2B;YAAME;SAAS,CAAE;YACxC,MAAM3D,OAAO8B,UAAU1B,OAAO,CAAC,SAAS,IAAIA,OAAO,CAAC,QAAQ,IAAID,WAAW;YAC3E,gEAAgE;YAChE,IAAIH,QAAQA,KAAKoB,QAAQ,CAAC,QAAQ,CAACpB,KAAKoB,QAAQ,CAAC,QAAQ,CAACpB,KAAKoB,QAAQ,CAAC,QAAQ;gBAC9EmC,MAAMK,GAAG,CAAC5D;YACZ;QACF;IACF;IACA,OAAO;WAAIuD;KAAM;AACnB;AAEA,MAAMM,uBAAuB;AAE7B,2EAA2E,GAC3E,SAASC,eAAenB,IAAY;IAClC,MAAMY,QAAQ,IAAI1D;IAClB,KAAK,MAAM2D,SAASd,eAAexC,OAAOyC,eAAAA,OAAQ,KAAKoB,QAAQ,CAACF,sBAAuB;QACrFN,MAAMK,GAAG,CAACJ,KAAK,CAAC,EAAE,CAACrD,WAAW,GAAGC,OAAO,CAAC,QAAQ;IACnD;IACA,OAAO;WAAImD;KAAM;AACnB;AAEA,oFAAoF,GACpF,SAASS,OAAOrB,IAAY;IAC1B,OAAOA,KAAKxC,WAAW,GAAGC,OAAO,CAAC,cAAc;AAClD;AAEA,wEAAwE,GACxE,SAAS6D,WACP9C,KAA0B,EAC1B+C,QAAgB,EAChB1C,eAAoC;IAEpC,MAAMvB,SAAS;QAACkB,MAAMhC,EAAE;QAAE6E,OAAO7C,MAAM/B,KAAK;WAAM+B,MAAM7B,UAAU,CAAC6E,GAAG,CAACH;KAAQ;IAC/E,IAAI/D,OAAOwB,IAAI,CAAC,CAACrC,QAAUA,SAAS8E,SAAS9C,QAAQ,CAAChC,SAAS,OAAO;IACtE,OAAO;WAAIoC;KAAgB,CAACC,IAAI,CAAC,CAACX,cAAgBI,iBAAiBC,OAAOL;AAC5E;AAEA;;;;;CAKC,GAED,0EAA0E,GAC1E,OAAO,MAAMsD,mBAAmBJ,OAAM;AACtC,mFAAmF,GACnF,OAAO,MAAMK,sBAAsBJ,WAAU;AAC7C,8DAA8D,GAC9D,OAAO,MAAMK,2BAA2B5C,oBAAmB;AAC3D,yFAAyF,GACzF,OAAO,SAAS6C,mBAAmBzD,WAAmB;IACpD,OAAOlB,oBAAoBgB,GAAG,CAACE;AACjC;AAEA;;CAEC,GACD,OAAO,SAAS0D,oBAAoBC,KAA0B;QAEnCA,iBACPA,mBAOgCA,eAMvCA,gBAMgBA,oBAWHA;IAhCxB,MAAMC,UAAkC,EAAE;IAC1C,MAAMR,WAAWF,OAAO,EAACS,kBAAAA,MAAMP,QAAQ,YAAdO,kBAAkB,EAAE,EAAEnE,MAAM,CAACC,SAASE,IAAI,CAAC;IACpE,MAAMkE,WAAW,EAACF,oBAAAA,MAAMG,UAAU,YAAhBH,oBAAoB,EAAE,EACrCN,GAAG,CAAC,CAACU,SAAW3E,OAAO2E,iBAAAA,SAAU,IAAIC,IAAI,GAAG3E,WAAW,GAAGC,OAAO,CAAC,QAAQ,KAC1EE,MAAM,CAAC,CAACuE,SAAWA,OAAOzD,QAAQ,CAAC;IACtC,MAAMI,kBAAkB,IAAI3B,IAAI8E,SAASR,GAAG,CAACpE;IAC7C,MAAMgF,YAAY,CAAC/E,OACjB2E,SAASlD,IAAI,CAAC,CAACuD,MAAQhF,SAASgF,OAAOhF,KAAKiF,QAAQ,CAAC,CAAC,CAAC,EAAED,KAAK;IAEhE,MAAME,OAAO;QAACT,MAAMU,OAAO;QAAEV,MAAMW,SAAS;YAAMX,gBAAAA,MAAMY,MAAM,YAAZZ,gBAAgB,EAAE;KAAE,CACnEnE,MAAM,CAAC,CAAC6B,OAAyB,OAAOA,SAAS,YAAYA,KAAK3B,MAAM,GAAG,GAC3EC,IAAI,CAAC;IACR,MAAM6E,cAAc5C,eAAewC;IACnC,MAAMK,UAAUC,MAAMC,OAAO,CAAChB,MAAMc,OAAO,IACvCd,MAAMc,OAAO,CAAC9E,IAAI,CAAC,OACnBP,QAAOuE,iBAAAA,MAAMc,OAAO,YAAbd,iBAAiB;IAC5B,MAAMiB,YAAYpC,YAAY4B;IAE9B,wEAAwE;IACxE,yBAAyB;IACzB,MAAMS,gBAAgB,IAAI9F;IAC1B,MAAM+F,cAAc1F,QAAOuE,qBAAAA,MAAMmB,WAAW,YAAjBnB,qBAAqB;IAChD,KAAK,MAAMzE,QAAQ;WAAI0F;WAAc5B,eAAe,GAAGoB,KAAK,EAAE,EAAEK,QAAQ,EAAE,EAAEK,aAAa;KAAE,CAAE;QAC3F,IAAID,cAAc/E,GAAG,CAACZ,OAAO;QAC7B,MAAMmB,QAAQmB,sBAAsBtC;QACpC,IAAImB,SAAS,CAACI,kBAAkBJ,OAAOK,kBAAkB;YACvDmE,cAAc/B,GAAG,CAAC5D;YAClB0E,QAAQmB,IAAI,CAAC;gBAAEC,MAAM;gBAAkB3E,OAAOA,MAAMhC,EAAE;gBAAEa;YAAK;QAC/D;IACF;IAEA,uEAAuE;IACvE,MAAM+F,WAAW7F,QAAOuE,kBAAAA,MAAMsB,QAAQ,YAAdtB,kBAAkB,IAAIK,IAAI;IAClD,IAAIiB,UAAU;QACZ,KAAK,MAAM5E,SAASjC,uBAAwB;YAC1C,IAAIiC,MAAM9B,OAAO,CAACiC,IAAI,CAACyE,aAAa,CAAC9B,WAAW9C,OAAO+C,UAAU1C,kBAAkB;gBACjFkD,QAAQmB,IAAI,CAAC;oBAAEC,MAAM;oBAAgB3E,OAAOA,MAAMhC,EAAE;oBAAE4G,UAAUA,SAASpF,KAAK,CAAC,GAAG;gBAAK;YACzF;QACF;IACF;IAEA,kEAAkE;IAClE,MAAMqF,OAAOrG,uBAAuBwE,GAAG,CAAC,CAAC8B;YAAYX;gBAAAA,qBAAAA,YAAY9B,KAAK,CAACyC,6BAAlBX,kBAA4B,CAAC,EAAE;OACjFY,IAAI,CAAC3F;IACR,IAAIyF,MAAM;QACR,MAAMG,UAAUT,UAAUQ,IAAI,CAAC,CAAClG;YAC9B,IAAI+E,UAAU/E,OAAO,OAAO;YAC5B,MAAMc,cAAcf,kBAAkBC;YACtC,OAAO,CAAC0B,oBAAoBZ,gBAAgB,CAAClB,oBAAoBgB,GAAG,CAACE;QACvE;QACA,IAAIqF,SAAS;YACX,MAAMC,cAAc,GAAGd,YAAY,EAAE,EAAES,UAAU;YACjD,KAAK,MAAM5E,SAASjC,uBAAwB;gBAC1C,IAAIiC,MAAM9B,OAAO,CAACiC,IAAI,CAAC8E,gBAAgB,CAACnC,WAAW9C,OAAO+C,UAAU1C,kBAAkB;oBACpFkD,QAAQmB,IAAI,CAAC;wBACXC,MAAM;wBACN3E,OAAOA,MAAMhC,EAAE;wBACf6G,MAAMA,KAAKrF,KAAK,CAAC,GAAG;wBACpBX,MAAMmG;oBACR;oBACA;gBACF;YACF;QACF;IACF;IAEA,OAAO;QAAEE,MAAM3B,QAAQlE,MAAM,GAAG;QAAGkE;IAAQ;AAC7C;AAEA,8DAA8D,GAC9D,OAAO,SAAS4B,yBAAyBnH,EAAU;;QAC1CD;IAAP,gBAAOA,+BAAAA,uBAAuBgH,IAAI,CAAC,CAAC/E,QAAUA,MAAMhC,EAAE,KAAKA,wBAApDD,6BAAyDE,KAAK,mBAAID;AAC3E;AAEA,gEAAgE,GAChE,OAAO,SAASoH,8BACd7B,OAAwC;IAExC,OAAOA,QAAQP,GAAG,CAAC,CAACqC;QAClB,MAAMrF,QAAQ,WAAWqF,SAASF,yBAAyBE,OAAOrF,KAAK,IAAI;QAC3E,OAAQqF,OAAOV,IAAI;YACjB,KAAK;gBACH,OAAO,CAAC,SAAS,EAAEU,OAAOxG,IAAI,CAAC,kBAAkB,EAAEmB,MAAM,iBAAiB,EAAEA,MAAM,UAAU,CAAC;YAC/F,KAAK;gBACH,OAAO,CAAC,UAAU,EAAEqF,OAAOT,QAAQ,CAAC,UAAU,EAAE5E,MAAM,8BAA8B,CAAC;YACvF,KAAK;gBACH,OAAO,CAAC,MAAM,EAAEA,MAAM,2BAA2B,EAAEqF,OAAOR,IAAI,CAAC,iBAAiB,EAAEQ,OAAOxG,IAAI,CAAC,CAAC,CAAC;YAClG,KAAK;gBACH,OAAO,CAAC,WAAW,EACjBwG,OAAOC,KAAK,KAAK,SAAS,gBAAgBD,OAAOC,KAAK,KAAK,QAAQ,kBAAkB,WACtF,oBAAoB,EAAED,OAAOpH,KAAK,CAAC,yDAAyD,CAAC;YAChG,KAAK;gBACH,OAAO,CAAC,MAAM,EAAE+B,MAAM,2BAA2B,EAAEqF,OAAOE,MAAM,CAAC,8BAA8B,EAAEvF,MAAM,CAAC,CAAC;YAC3G;gBACE,OAAO;QACX;IACF;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;2CAsB2C,GAE3C,wEAAwE,GACxE,OAAO,MAAMwF,6BAA6B,GAAE;AAK5C,oDAAoD,GACpD,OAAO,MAAMC,+BAAoD,IAAI/G,IAAI;IACvE;IACA;CACD,EAAC;AAEF;;;;;;CAMC,GACD,OAAO,MAAMgH,0BAA+C,IAAIhH,IAAI;IAClE;IAAa;IAAgB;IAAe;IAC5C;IAAqB;IAAiB;IAAiB;IAAiB;IACxE;IAAmB;IAAe;IAAc;IAA0B;IAC1E;IAAgB;IAAgB;IAAc;IAAa;IAAe;IAC1E;IAAkB;IAAc;IAAiB;IAAiB;CACnE,EAAC;AAEF,OAAO,SAASiH,mBAAmBN,MAAuC;IACxE,IAAI,CAACI,6BAA6BhG,GAAG,CAAC4F,0BAAAA,OAAQV,IAAI,GAAG,OAAO;IAC5D,IACEU,OAAOV,IAAI,KAAK,oBAChB,OAAOU,OAAOxG,IAAI,KAAK,YACvB6G,wBAAwBjG,GAAG,CAACb,kBAAkByG,OAAOxG,IAAI,IACzD;QACA,OAAO;IACT;IACA,OAAO;AACT;AAEA,uFAAuF,GACvF,OAAO,SAAS+G,oBAAoBC,OAAkC;IACpE,OAAO,OAAOA,YAAY,YAAY9D,OAAOC,QAAQ,CAAC6D,YAAYA,UAAUL;AAC9E;AAkBA,2EAA2E,GAC3E,OAAO,SAASM,gBACdvC,OAAqB,EACrBwC,MAA4B;IAE5B,MAAMC,OAAOJ,oBAAoBG,OAAOF,OAAO,KAAK,CAACE,OAAOE,IAAI;IAChE,OAAO1C,QAAQpE,MAAM,CAAC,CAACkG,SAAWM,mBAAmBN,YAAY,YAAYW;AAC/E;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAME,4BAAiD,IAAIxH,IAAI;IACpE;IAAW;IAAY;IAAQ;IAAW;IAAQ;IAAY;IAAM;IACpE;IAAS;IAAS;IAAU;IAAO;IAAW;IAAY;IAAU;IACpE;IAAU;IAAW;IAAU;IAAY;IAAU;IAAW;IAChE;IAAW;IAAU;IAAU;IAAgB;CAChD,EAAC;AAEF;;;;;;;;;;;;CAYC,GACD,OAAO,SAASyH,uBACdlI,KAAa,EACbmI,OAAO,iBAAiB;IAExB,MAAMhF,aAAarC,OAAOd,gBAAAA,QAAS,IAAI0F,IAAI,GAAG3E,WAAW;IACzD,IAAI,CAACoC,YAAY,OAAO;IACxB,MAAMiF,SAASlF,sBAAsB,GAAGC,WAAW,CAAC,EAAEgF,MAAM;IAC5D,IAAIC,QAAQ,OAAOA;IACnB,MAAMzF,QAAQJ,SAASY,YAAYlC,KAAK,CAAC,KAAKC,MAAM,CAACC;IACrD,IAAIwB,MAAMvB,MAAM,GAAG,KAAK,CAACuB,MAAMN,IAAI,CAAC,CAACU,OAASkF,0BAA0BzG,GAAG,CAACuB,QAAQ;QAClF,OAAO;IACT;IACA,KAAK,MAAMhB,SAASjC,uBAAwB;QAC1C,MAAMuI,QAAQ;YAACtG,MAAMhC,EAAE;eAAKgC,MAAM7B,UAAU;SAAC,CAACgB,MAAM,CAAC,CAACoH,OAAS,CAACA,KAAKtG,QAAQ,CAAC;QAC9E,IAAIW,MAAMN,IAAI,CAAC,CAACU,OAASsF,MAAMrG,QAAQ,CAACe,QAAQ,OAAOhB;IACzD;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,MAAMwG,6BAA6B,QAAO;AAEjD,gCAAgC,GAChC,OAAO,SAASC;IACd,OAAO1I,uBAAuBgH,IAAI,CAAC,CAAC/E,QAAUA,MAAMhC,EAAE,KAAKwI;AAC7D;AAEA;;;CAGC,GACD,MAAME,yBACJ;AAEF;;;;;;;;CAQC,GACD,OAAO,SAASC,mBAAmBC,IAAY;QAGtC7I;IAFP,MAAMyD,OAAOzC,OAAO6H,eAAAA,OAAQ;IAC5B,IAAI,CAACpF,KAAKmC,IAAI,MAAM,CAAC+C,uBAAuBvG,IAAI,CAACqB,OAAO,OAAO;IAC/D,QAAOzD,+BAAAA,uBAAuBgH,IAAI,CAAC,CAAC/E,QAAUA,MAAM9B,OAAO,CAACiC,IAAI,CAACqB,kBAA1DzD,+BAAoE;AAC7E"}