@aglyn/aglyn 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/activity-search.d.ts +39 -0
- package/src/lib/app-utils/activity-search.js +85 -0
- package/src/lib/app-utils/activity-search.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
- package/src/lib/app-utils/admin-audit-index.js +177 -0
- package/src/lib/app-utils/admin-audit-index.js.map +1 -0
- package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
- package/src/lib/app-utils/artifact-list-keys.js +154 -0
- package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
- package/src/lib/app-utils/content-query-fields.d.ts +69 -0
- package/src/lib/app-utils/content-query-fields.js +79 -0
- package/src/lib/app-utils/content-query-fields.js.map +1 -0
- package/src/lib/app-utils/crm.d.ts +170 -1
- package/src/lib/app-utils/crm.js +414 -1
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/dataset-models.d.ts +75 -24
- package/src/lib/app-utils/dataset-models.js +107 -40
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
- package/src/lib/app-utils/docs-help.generated.js +10 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +89 -75
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/email-search.d.ts +36 -0
- package/src/lib/app-utils/email-search.js +53 -0
- package/src/lib/app-utils/email-search.js.map +1 -0
- package/src/lib/app-utils/email-state.d.ts +7 -3
- package/src/lib/app-utils/email-state.js +22 -6
- package/src/lib/app-utils/email-state.js.map +1 -1
- package/src/lib/app-utils/forms.d.ts +178 -6
- package/src/lib/app-utils/forms.js +197 -0
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +3 -3
- package/src/lib/app-utils/health-report.js +9 -9
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/host-naming.d.ts +12 -7
- package/src/lib/app-utils/host-naming.js +18 -3
- package/src/lib/app-utils/host-naming.js.map +1 -1
- package/src/lib/app-utils/lockdown.d.ts +19 -0
- package/src/lib/app-utils/lockdown.js +21 -0
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
- package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-fields.js +77 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -1
- package/src/lib/app-utils/media-metadata.d.ts +95 -0
- package/src/lib/app-utils/media-metadata.js +94 -0
- package/src/lib/app-utils/media-metadata.js.map +1 -1
- package/src/lib/app-utils/message-search.d.ts +40 -0
- package/src/lib/app-utils/message-search.js +168 -0
- package/src/lib/app-utils/message-search.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +83 -0
- package/src/lib/app-utils/name-search.js +97 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/app-utils/payment-risk.d.ts +75 -0
- package/src/lib/app-utils/payment-risk.js +139 -0
- package/src/lib/app-utils/payment-risk.js.map +1 -0
- package/src/lib/app-utils/plugin-api-request-host.d.ts +29 -0
- package/src/lib/app-utils/plugin-api-request-host.js +90 -0
- package/src/lib/app-utils/plugin-api-request-host.js.map +1 -0
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +3 -2
- package/src/lib/app-utils/plugin-bundle-checks.js +76 -5
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +2 -0
- package/src/lib/app-utils/server.js +2 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/webhook-delivery.js +4 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +9 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.js +5 -1
- package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +9 -1
- package/src/lib/plugin-manager/plugin-activity-actions.js +11 -1
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.d.ts +22 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-jobs.d.ts +8 -4
- package/src/lib/plugin-manager/plugin-jobs.js +8 -4
- package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
|
@@ -14,12 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
/**
|
|
18
|
-
* Host naming guards (AGL-147): one shared subdomain policy for the create
|
|
19
|
-
* API, the rename/validate API, and the console dialogs — pattern, reserved
|
|
20
|
-
* + profanity blocklists, a display-name → subdomain generator, and
|
|
21
|
-
* taken-name suggestions.
|
|
22
|
-
*/
|
|
23
17
|
/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */
|
|
24
18
|
export declare const SUBDOMAIN_PATTERN: RegExp;
|
|
25
19
|
/** Platform/system names a tenant must not squat. */
|
|
@@ -39,7 +33,18 @@ export declare const RESERVED_SUBDOMAINS: Set<string>;
|
|
|
39
33
|
* owns.
|
|
40
34
|
*/
|
|
41
35
|
export declare function consoleLabelUnder(consoleUrl: string | null | undefined, workspaceDomain: string | null | undefined): string | null;
|
|
42
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* True when the subdomain is reserved, contains a blocked fragment, or wears
|
|
38
|
+
* a brand a phishing kit impersonates (AGL-3356).
|
|
39
|
+
*
|
|
40
|
+
* The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):
|
|
41
|
+
* a brand's name or a lookalike of it (`poshmark`, `paypal-secure`,
|
|
42
|
+
* `paypa1`, `appleid`), or a brand's word joined to an account word
|
|
43
|
+
* (`booking-review`, `apple-support`). Refused for every workspace at create
|
|
44
|
+
* and at rename, since a brand's name on our apex is a disguise that
|
|
45
|
+
* outlives every later screen — while a real business's name that merely
|
|
46
|
+
* contains a common word (`tanyas-booking`, `apple-pie-co`) is not.
|
|
47
|
+
*/
|
|
43
48
|
export declare function isBlockedSubdomain(subdomain: string): boolean;
|
|
44
49
|
/**
|
|
45
50
|
* Best-effort subdomain from a display name: lowercase, spaces/symbols to
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
* API, the rename/validate API, and the console dialogs — pattern, reserved
|
|
19
19
|
* + profanity blocklists, a display-name → subdomain generator, and
|
|
20
20
|
* taken-name suggestions.
|
|
21
|
-
*/
|
|
21
|
+
*/ // The leaf, not the barrel: the brand list and lookalike logic every
|
|
22
|
+
// phishing surface reads (AGL-3356), and nothing else of the email library.
|
|
23
|
+
import { brandForSubdomainLabel } from "@aglyn/shared-util-email/outbound-phishing-screen";
|
|
24
|
+
/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */ export const SUBDOMAIN_PATTERN = /^[a-z0-9][a-z0-9-]{2,29}$/;
|
|
22
25
|
/** Platform/system names a tenant must not squat. */ export const RESERVED_SUBDOMAINS = new Set([
|
|
23
26
|
'www',
|
|
24
27
|
'app',
|
|
@@ -90,11 +93,23 @@
|
|
|
90
93
|
const label = hostname.slice(0, -(domain.length + 1));
|
|
91
94
|
return label && !label.includes('.') ? label : null;
|
|
92
95
|
}
|
|
93
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* True when the subdomain is reserved, contains a blocked fragment, or wears
|
|
98
|
+
* a brand a phishing kit impersonates (AGL-3356).
|
|
99
|
+
*
|
|
100
|
+
* The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):
|
|
101
|
+
* a brand's name or a lookalike of it (`poshmark`, `paypal-secure`,
|
|
102
|
+
* `paypa1`, `appleid`), or a brand's word joined to an account word
|
|
103
|
+
* (`booking-review`, `apple-support`). Refused for every workspace at create
|
|
104
|
+
* and at rename, since a brand's name on our apex is a disguise that
|
|
105
|
+
* outlives every later screen — while a real business's name that merely
|
|
106
|
+
* contains a common word (`tanyas-booking`, `apple-pie-co`) is not.
|
|
107
|
+
*/ export function isBlockedSubdomain(subdomain) {
|
|
94
108
|
const normalized = subdomain.toLowerCase();
|
|
95
109
|
if (RESERVED_SUBDOMAINS.has(normalized)) return true;
|
|
96
110
|
const collapsed = normalized.replace(/[^a-z0-9]/g, '');
|
|
97
|
-
|
|
111
|
+
if (BLOCKED_FRAGMENTS.some((fragment)=>collapsed.includes(fragment))) return true;
|
|
112
|
+
return brandForSubdomainLabel(normalized) !== null;
|
|
98
113
|
}
|
|
99
114
|
/**
|
|
100
115
|
* Best-effort subdomain from a display name: lowercase, spaces/symbols to
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-naming.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 * Host naming guards (AGL-147): one shared subdomain policy for the create\n * API, the rename/validate API, and the console dialogs — pattern, reserved\n * + profanity blocklists, a display-name → subdomain generator, and\n * taken-name suggestions.\n */\n\n/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */\nexport const SUBDOMAIN_PATTERN = /^[a-z0-9][a-z0-9-]{2,29}$/\n\n/** Platform/system names a tenant must not squat. */\nexport const RESERVED_SUBDOMAINS = new Set([\n 'www',\n 'app',\n 'api',\n 'admin',\n 'console',\n 'mail',\n 'demo',\n 'staging',\n 'dev',\n 'test',\n 'docs',\n 'blog',\n 'help',\n 'support',\n 'status',\n 'cdn',\n 'assets',\n 'static',\n 'ftp',\n 'smtp',\n 'ns1',\n 'ns2',\n 'aglyn',\n 'billing',\n 'account',\n 'login',\n 'signup',\n 'auth',\n])\n\n/**\n * Profanity fragments blocked as substrings in generated or typed\n * subdomains. Deliberately short: obvious slurs and vulgarity only —\n * substring matching is aggressive, so entries must be unambiguous.\n */\nconst BLOCKED_FRAGMENTS = [\n 'fuck',\n 'shit',\n 'cunt',\n 'nigger',\n 'faggot',\n 'bitch',\n 'porn',\n 'nazi',\n]\n\n/**\n * The one label a console URL occupies directly under a workspace domain\n * (AGL-3295): `studio` for `https://studio.example.com` under `example.com`.\n *\n * `null` when either setting is missing or unparseable, and when the console\n * is the apex itself, sits deeper, or lies outside the domain — none of those\n * can collide with a workspace address, so none needs reserving.\n *\n * Pure, and in this dependency-free module, so the console's host gate and the\n * organization-slug blocklist derive the same label from the same two\n * settings. An install whose console is not at `app.` or `console.` is the\n * case it exists for: without it, that label is read as a workspace nobody\n * owns.\n */\nexport function consoleLabelUnder(\n consoleUrl: string | null | undefined,\n workspaceDomain: string | null | undefined,\n): string | null {\n const domain = String(workspaceDomain ?? '')\n .trim()\n .toLowerCase()\n const raw = String(consoleUrl ?? '').trim()\n if (!domain || !raw) return null\n let hostname: string\n try {\n hostname = new URL(raw).hostname.toLowerCase()\n } catch {\n return null\n }\n if (!hostname.endsWith(`.${domain}`)) return null\n const label = hostname.slice(0, -(domain.length + 1))\n return label && !label.includes('.') ? label : null\n}\n\n/** True when the subdomain is reserved or contains a blocked fragment. */\nexport function isBlockedSubdomain(subdomain: string): boolean {\n const normalized = subdomain.toLowerCase()\n if (RESERVED_SUBDOMAINS.has(normalized)) return true\n const collapsed = normalized.replace(/[^a-z0-9]/g, '')\n return BLOCKED_FRAGMENTS.some((fragment) => collapsed.includes(fragment))\n}\n\n/**\n * Best-effort subdomain from a display name: lowercase, spaces/symbols to\n * dashes, collapsed, trimmed to the pattern's 30-char cap. Returns '' when\n * nothing usable remains (caller falls back to manual entry).\n */\nexport function generateSubdomain(displayName: string): string {\n const slug = displayName\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 30)\n .replace(/-+$/, '')\n if (!SUBDOMAIN_PATTERN.test(slug) || isBlockedSubdomain(slug)) return ''\n return slug\n}\n\n/**\n * Alternatives for a taken subdomain: `name-2`, `name-<year>`, `name-site`.\n * Candidates are truncated to fit the pattern; blocked names are dropped.\n * Availability is the caller's job (needs a Firestore query).\n */\nexport function suggestSubdomains(base: string, year = new Date().getFullYear()): string[] {\n const stem = base.toLowerCase().replace(/-+$/, '')\n const withSuffix = (suffix: string) =>\n `${stem.slice(0, 30 - suffix.length - 1)}-${suffix}`.replace(/-{2,}/g, '-')\n const candidates = [withSuffix('2'), withSuffix(String(year)), withSuffix('site')]\n return [\n ...new Set(\n candidates.filter(\n (candidate) =>\n SUBDOMAIN_PATTERN.test(candidate) && !isBlockedSubdomain(candidate),\n ),\n ),\n ]\n}\n\n/**\n * The apex tenant sites are served from. Console lives on `app.aglyn.com`.\n *\n * CONFIGURABLE, not a literal (AGL-2121). AGL-2022 moved the console's\n * *display* links onto `NEXT_PUBLIC_TENANT_DOMAIN` and shipped a green spec\n * for it; this constant — which every OTHER consumer reads — stayed a bare\n * `'aglyn.app'`. So a self-hoster who set the variable got correct \"View live\"\n * links in their own console while the software went on emitting our apex\n * everywhere that actually reaches the public: `<link rel=\"canonical\">`,\n * `/api/sitemap`, `/api/robots`, `/api/collections-rss` item links,\n * `/api/manifest`, `og:image`, and the origin inbox-bound `<img src>` resolves\n * against. Their customers' sites told Google, every feed reader and every\n * inbox that they lived at a name the operator does not control and we do not\n * serve for them.\n *\n * Same variable and same default as `tenant-links.ts` — one value, one name\n * (AGL-733). Dot notation is load-bearing: this module reaches client bundles,\n * and Next inlines `NEXT_PUBLIC_*` by substituting `process.env.NAME`\n * textually; the bracket form is never substituted and would read `undefined`\n * in the browser. Trimmed so a half-finished `.env` line reads as absent\n * rather than as a whitespace apex.\n *\n * The asymmetry with `operator-identity.ts` — which refuses to default at all\n * — is deliberate. A wrong apex is visible to the operator on the first click\n * in their own console, so a default they can see and correct is a default. A\n * wrong contact address fails silently and off-site, which is why that one\n * refuses to guess.\n */\nexport const TENANT_APEX =\n (process.env.NEXT_PUBLIC_TENANT_DOMAIN || '').trim() || 'aglyn.app'\n\n/**\n * The absolute origin a published site is reachable at (AGL-1224).\n *\n * A custom domain wins over the platform subdomain, matching what the tenant\n * renderer already emits as `<link rel=\"canonical\">` — a site on a custom\n * domain that advertised its `.aglyn.app` origin would be describing itself\n * by a name its visitors never see.\n *\n * Returns undefined rather than a half-built URL when a host has neither, so\n * a caller that needs an absolute URL can tell it does not have one. Emails\n * are the sharp case: an image `src` that is site-relative resolves against\n * nothing in an inbox, so \"no origin\" has to be answerable.\n */\nexport function hostPublicOrigin(\n host:\n | { cname?: string | null; subdomain?: string | null }\n | null\n | undefined,\n): string | undefined {\n if (host?.cname) return `https://${host.cname}`\n if (host?.subdomain) return `https://${host.subdomain}.${TENANT_APEX}`\n return undefined\n}\n\n/**\n * The custom-domain lifecycle as it is actually persisted on the host doc.\n *\n * There is no `verifiedAt`, no `status`, no `sslReady` — the console's\n * `/api/domains/*` routes write exactly these three fields, so this is the\n * whole vocabulary anything downstream has to reason from.\n */\nexport interface HostCustomDomainState {\n cname?: string | null\n /** Vercel attach failed or was unconfigured — the domain does NOT serve. */\n cnameAttachmentPending?: boolean | null\n /** Vercel detach failed — the customer is mid-disconnect. */\n cnameDetachmentPending?: boolean | null\n subdomain?: string | null\n}\n\n/** A bare hostname: labels of a-z0-9/dash, at least two of them. */\nconst CUSTOM_DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\\.(?!-)[a-z0-9-]{1,63})+$/\n\n/**\n * The custom domain a host is actually **serving on**, or undefined (AGL-1272).\n *\n * Deliberately stricter than `hostPublicOrigin`, which answers \"what name does\n * this site call itself\" and is right to prefer `cname` the moment it exists.\n * This answers a load-bearing question instead — \"is it safe to send visitors\n * away from the platform subdomain and onto this name\" — and the cost of\n * getting it wrong is the whole site, not a wrong URL in an email.\n *\n * So every reason to doubt the domain is a refusal:\n *\n * - `cnameAttachmentPending` means the Vercel attach never landed (missing\n * env, or a 5xx). The domain is claimed in Firestore and serves nothing:\n * no certificate, no routing. This is precisely the \"attached but not\n * live\" state, and redirecting to it strands the site on a dead host.\n * - `cnameDetachmentPending` means the customer asked to disconnect and the\n * platform release failed. The name may still resolve, but the customer's\n * stated intent is to leave it, so we stop advertising it.\n * - A name inside `aglyn.app` is refused outright. `{sub}.aglyn.app` is the\n * very origin we redirect FROM, and the tenant middleware resolves that\n * space by `subdomain` before it ever considers `cname` — so a host whose\n * cname pointed back into it would redirect to itself forever.\n * - A name that is not hostname-shaped is refused. `/api/domains/attach`\n * lowercases and trims what the wizard sends but never pattern-checks it\n * (only `/api/domains/verify` does), so a junk value can reach Firestore.\n * A junk value reaching a `Location:` header is a different class of\n * problem again.\n *\n * Note what this CANNOT tell you: DNS was verified once, at connect time, and\n * nothing re-checks it. A domain whose DNS is later repointed or whose\n * registration lapses still reads as live here. That residual risk is why the\n * redirect built on this is temporary and revocable rather than permanent.\n */\nexport function liveCustomDomain(\n host: HostCustomDomainState | null | undefined,\n): string | undefined {\n if (!host) return undefined\n if (host.cnameAttachmentPending === true) return undefined\n if (host.cnameDetachmentPending === true) return undefined\n const domain = String(host.cname ?? '')\n .trim()\n .toLowerCase()\n if (!CUSTOM_DOMAIN_PATTERN.test(domain)) return undefined\n if (domain === TENANT_APEX || domain.endsWith(`.${TENANT_APEX}`)) {\n return undefined\n }\n return domain\n}\n"],"names":["SUBDOMAIN_PATTERN","RESERVED_SUBDOMAINS","Set","BLOCKED_FRAGMENTS","consoleLabelUnder","consoleUrl","workspaceDomain","domain","String","trim","toLowerCase","raw","hostname","URL","endsWith","label","slice","length","includes","isBlockedSubdomain","subdomain","normalized","has","collapsed","replace","some","fragment","generateSubdomain","displayName","slug","normalize","test","suggestSubdomains","base","year","Date","getFullYear","stem","withSuffix","suffix","candidates","filter","candidate","TENANT_APEX","process","env","NEXT_PUBLIC_TENANT_DOMAIN","hostPublicOrigin","host","cname","undefined","CUSTOM_DOMAIN_PATTERN","liveCustomDomain","cnameAttachmentPending","cnameDetachmentPending"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-naming.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 * Host naming guards (AGL-147): one shared subdomain policy for the create\n * API, the rename/validate API, and the console dialogs — pattern, reserved\n * + profanity blocklists, a display-name → subdomain generator, and\n * taken-name suggestions.\n */\n\n// The leaf, not the barrel: the brand list and lookalike logic every\n// phishing surface reads (AGL-3356), and nothing else of the email library.\nimport { brandForSubdomainLabel } from '@aglyn/shared-util-email/outbound-phishing-screen'\n\n/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */\nexport const SUBDOMAIN_PATTERN = /^[a-z0-9][a-z0-9-]{2,29}$/\n\n/** Platform/system names a tenant must not squat. */\nexport const RESERVED_SUBDOMAINS = new Set([\n 'www',\n 'app',\n 'api',\n 'admin',\n 'console',\n 'mail',\n 'demo',\n 'staging',\n 'dev',\n 'test',\n 'docs',\n 'blog',\n 'help',\n 'support',\n 'status',\n 'cdn',\n 'assets',\n 'static',\n 'ftp',\n 'smtp',\n 'ns1',\n 'ns2',\n 'aglyn',\n 'billing',\n 'account',\n 'login',\n 'signup',\n 'auth',\n])\n\n/**\n * Profanity fragments blocked as substrings in generated or typed\n * subdomains. Deliberately short: obvious slurs and vulgarity only —\n * substring matching is aggressive, so entries must be unambiguous.\n */\nconst BLOCKED_FRAGMENTS = [\n 'fuck',\n 'shit',\n 'cunt',\n 'nigger',\n 'faggot',\n 'bitch',\n 'porn',\n 'nazi',\n]\n\n/**\n * The one label a console URL occupies directly under a workspace domain\n * (AGL-3295): `studio` for `https://studio.example.com` under `example.com`.\n *\n * `null` when either setting is missing or unparseable, and when the console\n * is the apex itself, sits deeper, or lies outside the domain — none of those\n * can collide with a workspace address, so none needs reserving.\n *\n * Pure, and in this dependency-free module, so the console's host gate and the\n * organization-slug blocklist derive the same label from the same two\n * settings. An install whose console is not at `app.` or `console.` is the\n * case it exists for: without it, that label is read as a workspace nobody\n * owns.\n */\nexport function consoleLabelUnder(\n consoleUrl: string | null | undefined,\n workspaceDomain: string | null | undefined,\n): string | null {\n const domain = String(workspaceDomain ?? '')\n .trim()\n .toLowerCase()\n const raw = String(consoleUrl ?? '').trim()\n if (!domain || !raw) return null\n let hostname: string\n try {\n hostname = new URL(raw).hostname.toLowerCase()\n } catch {\n return null\n }\n if (!hostname.endsWith(`.${domain}`)) return null\n const label = hostname.slice(0, -(domain.length + 1))\n return label && !label.includes('.') ? label : null\n}\n\n/**\n * True when the subdomain is reserved, contains a blocked fragment, or wears\n * a brand a phishing kit impersonates (AGL-3356).\n *\n * The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):\n * a brand's name or a lookalike of it (`poshmark`, `paypal-secure`,\n * `paypa1`, `appleid`), or a brand's word joined to an account word\n * (`booking-review`, `apple-support`). Refused for every workspace at create\n * and at rename, since a brand's name on our apex is a disguise that\n * outlives every later screen — while a real business's name that merely\n * contains a common word (`tanyas-booking`, `apple-pie-co`) is not.\n */\nexport function isBlockedSubdomain(subdomain: string): boolean {\n const normalized = subdomain.toLowerCase()\n if (RESERVED_SUBDOMAINS.has(normalized)) return true\n const collapsed = normalized.replace(/[^a-z0-9]/g, '')\n if (BLOCKED_FRAGMENTS.some((fragment) => collapsed.includes(fragment))) return true\n return brandForSubdomainLabel(normalized) !== null\n}\n\n/**\n * Best-effort subdomain from a display name: lowercase, spaces/symbols to\n * dashes, collapsed, trimmed to the pattern's 30-char cap. Returns '' when\n * nothing usable remains (caller falls back to manual entry).\n */\nexport function generateSubdomain(displayName: string): string {\n const slug = displayName\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 30)\n .replace(/-+$/, '')\n if (!SUBDOMAIN_PATTERN.test(slug) || isBlockedSubdomain(slug)) return ''\n return slug\n}\n\n/**\n * Alternatives for a taken subdomain: `name-2`, `name-<year>`, `name-site`.\n * Candidates are truncated to fit the pattern; blocked names are dropped.\n * Availability is the caller's job (needs a Firestore query).\n */\nexport function suggestSubdomains(base: string, year = new Date().getFullYear()): string[] {\n const stem = base.toLowerCase().replace(/-+$/, '')\n const withSuffix = (suffix: string) =>\n `${stem.slice(0, 30 - suffix.length - 1)}-${suffix}`.replace(/-{2,}/g, '-')\n const candidates = [withSuffix('2'), withSuffix(String(year)), withSuffix('site')]\n return [\n ...new Set(\n candidates.filter(\n (candidate) =>\n SUBDOMAIN_PATTERN.test(candidate) && !isBlockedSubdomain(candidate),\n ),\n ),\n ]\n}\n\n/**\n * The apex tenant sites are served from. Console lives on `app.aglyn.com`.\n *\n * CONFIGURABLE, not a literal (AGL-2121). AGL-2022 moved the console's\n * *display* links onto `NEXT_PUBLIC_TENANT_DOMAIN` and shipped a green spec\n * for it; this constant — which every OTHER consumer reads — stayed a bare\n * `'aglyn.app'`. So a self-hoster who set the variable got correct \"View live\"\n * links in their own console while the software went on emitting our apex\n * everywhere that actually reaches the public: `<link rel=\"canonical\">`,\n * `/api/sitemap`, `/api/robots`, `/api/collections-rss` item links,\n * `/api/manifest`, `og:image`, and the origin inbox-bound `<img src>` resolves\n * against. Their customers' sites told Google, every feed reader and every\n * inbox that they lived at a name the operator does not control and we do not\n * serve for them.\n *\n * Same variable and same default as `tenant-links.ts` — one value, one name\n * (AGL-733). Dot notation is load-bearing: this module reaches client bundles,\n * and Next inlines `NEXT_PUBLIC_*` by substituting `process.env.NAME`\n * textually; the bracket form is never substituted and would read `undefined`\n * in the browser. Trimmed so a half-finished `.env` line reads as absent\n * rather than as a whitespace apex.\n *\n * The asymmetry with `operator-identity.ts` — which refuses to default at all\n * — is deliberate. A wrong apex is visible to the operator on the first click\n * in their own console, so a default they can see and correct is a default. A\n * wrong contact address fails silently and off-site, which is why that one\n * refuses to guess.\n */\nexport const TENANT_APEX =\n (process.env.NEXT_PUBLIC_TENANT_DOMAIN || '').trim() || 'aglyn.app'\n\n/**\n * The absolute origin a published site is reachable at (AGL-1224).\n *\n * A custom domain wins over the platform subdomain, matching what the tenant\n * renderer already emits as `<link rel=\"canonical\">` — a site on a custom\n * domain that advertised its `.aglyn.app` origin would be describing itself\n * by a name its visitors never see.\n *\n * Returns undefined rather than a half-built URL when a host has neither, so\n * a caller that needs an absolute URL can tell it does not have one. Emails\n * are the sharp case: an image `src` that is site-relative resolves against\n * nothing in an inbox, so \"no origin\" has to be answerable.\n */\nexport function hostPublicOrigin(\n host:\n | { cname?: string | null; subdomain?: string | null }\n | null\n | undefined,\n): string | undefined {\n if (host?.cname) return `https://${host.cname}`\n if (host?.subdomain) return `https://${host.subdomain}.${TENANT_APEX}`\n return undefined\n}\n\n/**\n * The custom-domain lifecycle as it is actually persisted on the host doc.\n *\n * There is no `verifiedAt`, no `status`, no `sslReady` — the console's\n * `/api/domains/*` routes write exactly these three fields, so this is the\n * whole vocabulary anything downstream has to reason from.\n */\nexport interface HostCustomDomainState {\n cname?: string | null\n /** Vercel attach failed or was unconfigured — the domain does NOT serve. */\n cnameAttachmentPending?: boolean | null\n /** Vercel detach failed — the customer is mid-disconnect. */\n cnameDetachmentPending?: boolean | null\n subdomain?: string | null\n}\n\n/** A bare hostname: labels of a-z0-9/dash, at least two of them. */\nconst CUSTOM_DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\\.(?!-)[a-z0-9-]{1,63})+$/\n\n/**\n * The custom domain a host is actually **serving on**, or undefined (AGL-1272).\n *\n * Deliberately stricter than `hostPublicOrigin`, which answers \"what name does\n * this site call itself\" and is right to prefer `cname` the moment it exists.\n * This answers a load-bearing question instead — \"is it safe to send visitors\n * away from the platform subdomain and onto this name\" — and the cost of\n * getting it wrong is the whole site, not a wrong URL in an email.\n *\n * So every reason to doubt the domain is a refusal:\n *\n * - `cnameAttachmentPending` means the Vercel attach never landed (missing\n * env, or a 5xx). The domain is claimed in Firestore and serves nothing:\n * no certificate, no routing. This is precisely the \"attached but not\n * live\" state, and redirecting to it strands the site on a dead host.\n * - `cnameDetachmentPending` means the customer asked to disconnect and the\n * platform release failed. The name may still resolve, but the customer's\n * stated intent is to leave it, so we stop advertising it.\n * - A name inside `aglyn.app` is refused outright. `{sub}.aglyn.app` is the\n * very origin we redirect FROM, and the tenant middleware resolves that\n * space by `subdomain` before it ever considers `cname` — so a host whose\n * cname pointed back into it would redirect to itself forever.\n * - A name that is not hostname-shaped is refused. `/api/domains/attach`\n * lowercases and trims what the wizard sends but never pattern-checks it\n * (only `/api/domains/verify` does), so a junk value can reach Firestore.\n * A junk value reaching a `Location:` header is a different class of\n * problem again.\n *\n * Note what this CANNOT tell you: DNS was verified once, at connect time, and\n * nothing re-checks it. A domain whose DNS is later repointed or whose\n * registration lapses still reads as live here. That residual risk is why the\n * redirect built on this is temporary and revocable rather than permanent.\n */\nexport function liveCustomDomain(\n host: HostCustomDomainState | null | undefined,\n): string | undefined {\n if (!host) return undefined\n if (host.cnameAttachmentPending === true) return undefined\n if (host.cnameDetachmentPending === true) return undefined\n const domain = String(host.cname ?? '')\n .trim()\n .toLowerCase()\n if (!CUSTOM_DOMAIN_PATTERN.test(domain)) return undefined\n if (domain === TENANT_APEX || domain.endsWith(`.${TENANT_APEX}`)) {\n return undefined\n }\n return domain\n}\n"],"names":["brandForSubdomainLabel","SUBDOMAIN_PATTERN","RESERVED_SUBDOMAINS","Set","BLOCKED_FRAGMENTS","consoleLabelUnder","consoleUrl","workspaceDomain","domain","String","trim","toLowerCase","raw","hostname","URL","endsWith","label","slice","length","includes","isBlockedSubdomain","subdomain","normalized","has","collapsed","replace","some","fragment","generateSubdomain","displayName","slug","normalize","test","suggestSubdomains","base","year","Date","getFullYear","stem","withSuffix","suffix","candidates","filter","candidate","TENANT_APEX","process","env","NEXT_PUBLIC_TENANT_DOMAIN","hostPublicOrigin","host","cname","undefined","CUSTOM_DOMAIN_PATTERN","liveCustomDomain","cnameAttachmentPending","cnameDetachmentPending"],"mappings":"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"}
|
|
@@ -513,6 +513,25 @@ export declare function normalizeHostLockdown(host: {
|
|
|
513
513
|
suspendedMode?: unknown;
|
|
514
514
|
suspendedEnforcement?: unknown;
|
|
515
515
|
} | null | undefined): LockdownState | null;
|
|
516
|
+
/**
|
|
517
|
+
* The active org/host lock on one site, from documents the caller already
|
|
518
|
+
* holds, or null (AGL-3356).
|
|
519
|
+
*
|
|
520
|
+
* For a sender deep in a plugin — the campaign core, the workflow engine,
|
|
521
|
+
* the outreach runtime — that has the org and host documents in hand and
|
|
522
|
+
* must refuse to mail for a suspended workspace. The server verdict
|
|
523
|
+
* (`getSiteLockdown`) adds the platform scope and the takedown ledger on
|
|
524
|
+
* top; this is the pure part both share, so neither restates what an
|
|
525
|
+
* org/host lock is. Unlike the verdict it reads nothing and cannot fail, so
|
|
526
|
+
* a caller that could not read the documents has already failed on its own.
|
|
527
|
+
*
|
|
528
|
+
* Returns the state whatever its mode; a caller that lets a read-only
|
|
529
|
+
* maintenance window through asks `lockdownMode(state)`.
|
|
530
|
+
*/
|
|
531
|
+
export declare function siteLockdownFromDocs(docs: {
|
|
532
|
+
org?: Parameters<typeof normalizeOrgLockdown>[0];
|
|
533
|
+
host?: Parameters<typeof normalizeHostLockdown>[0];
|
|
534
|
+
}, nowMs: number): LockdownState | null;
|
|
516
535
|
/** `lockdowns/{id}` doc → state; refuses malformed docs rather than guess. */
|
|
517
536
|
export declare function normalizeLockdownDoc(doc: Partial<LockdownDoc> | null | undefined, scope: LockdownScope): LockdownState | null;
|
|
518
537
|
/**
|
|
@@ -517,6 +517,27 @@ export const userLockdownDocId = (uid)=>`user--${uid}`;
|
|
|
517
517
|
untilMs: typeof host.suspendedUntilMs === 'number' && Number.isFinite(host.suspendedUntilMs) ? host.suspendedUntilMs : undefined
|
|
518
518
|
});
|
|
519
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* The active org/host lock on one site, from documents the caller already
|
|
522
|
+
* holds, or null (AGL-3356).
|
|
523
|
+
*
|
|
524
|
+
* For a sender deep in a plugin — the campaign core, the workflow engine,
|
|
525
|
+
* the outreach runtime — that has the org and host documents in hand and
|
|
526
|
+
* must refuse to mail for a suspended workspace. The server verdict
|
|
527
|
+
* (`getSiteLockdown`) adds the platform scope and the takedown ledger on
|
|
528
|
+
* top; this is the pure part both share, so neither restates what an
|
|
529
|
+
* org/host lock is. Unlike the verdict it reads nothing and cannot fail, so
|
|
530
|
+
* a caller that could not read the documents has already failed on its own.
|
|
531
|
+
*
|
|
532
|
+
* Returns the state whatever its mode; a caller that lets a read-only
|
|
533
|
+
* maintenance window through asks `lockdownMode(state)`.
|
|
534
|
+
*/ export function siteLockdownFromDocs(docs, nowMs) {
|
|
535
|
+
const state = resolveLockdown({
|
|
536
|
+
org: normalizeOrgLockdown(docs.org),
|
|
537
|
+
host: normalizeHostLockdown(docs.host)
|
|
538
|
+
}, nowMs);
|
|
539
|
+
return isLockdownActive(state, nowMs) ? state : null;
|
|
540
|
+
}
|
|
520
541
|
/** `lockdowns/{id}` doc → state; refuses malformed docs rather than guess. */ export function normalizeLockdownDoc(doc, scope) {
|
|
521
542
|
if (!doc) return null;
|
|
522
543
|
if (!isLockdownReasonCode(doc.reason)) return null;
|