@aglyn/aglyn 1.0.0-beta.201 → 1.0.0-beta.203
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-presenter.d.ts +29 -3
- package/src/lib/app-utils/activity-presenter.js +32 -4
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +20 -0
- package/src/lib/app-utils/api-plugins.js +9 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/card-payment-velocity.d.ts +57 -0
- package/src/lib/app-utils/card-payment-velocity.js +90 -0
- package/src/lib/app-utils/card-payment-velocity.js.map +1 -0
- package/src/lib/app-utils/crm-digest.d.ts +6 -0
- package/src/lib/app-utils/crm-digest.js +1 -0
- package/src/lib/app-utils/crm-digest.js.map +1 -1
- package/src/lib/app-utils/crm-task-reminders.d.ts +6 -0
- package/src/lib/app-utils/crm-task-reminders.js +1 -0
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +8 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +1 -0
- package/src/lib/app-utils/marketplace-listing-visibility.js +11 -5
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +35 -0
- package/src/lib/app-utils/notifications.js +57 -1
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.js +23 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +8 -2
- package/src/lib/app-utils/platform-brand.js +9 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +2 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +36 -1
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/site-return-url.d.ts +55 -0
- package/src/lib/app-utils/site-return-url.js +129 -0
- package/src/lib/app-utils/site-return-url.js.map +1 -0
- 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-org-lockdown.d.ts +77 -0
- package/src/lib/plugin-manager/plugin-org-lockdown.js +88 -0
- package/src/lib/plugin-manager/plugin-org-lockdown.js.map +1 -0
- package/src/lib/plugin-manager/plugin-recurring-charges.d.ts +39 -0
- package/src/lib/plugin-manager/plugin-recurring-charges.js +49 -0
- package/src/lib/plugin-manager/plugin-recurring-charges.js.map +1 -0
|
@@ -127,8 +127,14 @@ export declare const PLATFORM_MARK_URL: string | null;
|
|
|
127
127
|
*
|
|
128
128
|
* The WORDMARK rather than the square mark: a header is read at a glance, and
|
|
129
129
|
* the wordmark names the sender where the mark would need the reader to
|
|
130
|
-
* already know it.
|
|
131
|
-
*
|
|
130
|
+
* already know it.
|
|
131
|
+
*
|
|
132
|
+
* A PNG sized for the slot (AGL-3367): 396×120, three times the 132×40 the
|
|
133
|
+
* header draws it at, about 15 KB, served as a static file off aglyn.com's
|
|
134
|
+
* edge. The master-resolution file behind the media CDN's 60-second cache is
|
|
135
|
+
* four times the bytes and makes the header the last thing in the email to
|
|
136
|
+
* appear. Not an SVG: Gmail and Outlook do not draw SVG in mail, so every
|
|
137
|
+
* recipient on either would get a broken-image box where the logo belongs.
|
|
132
138
|
*
|
|
133
139
|
* Null unless this is the Aglyn-operated brand, for the reason
|
|
134
140
|
* {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at
|
|
@@ -181,8 +181,14 @@ var _clean4;
|
|
|
181
181
|
*
|
|
182
182
|
* The WORDMARK rather than the square mark: a header is read at a glance, and
|
|
183
183
|
* the wordmark names the sender where the mark would need the reader to
|
|
184
|
-
* already know it.
|
|
185
|
-
*
|
|
184
|
+
* already know it.
|
|
185
|
+
*
|
|
186
|
+
* A PNG sized for the slot (AGL-3367): 396×120, three times the 132×40 the
|
|
187
|
+
* header draws it at, about 15 KB, served as a static file off aglyn.com's
|
|
188
|
+
* edge. The master-resolution file behind the media CDN's 60-second cache is
|
|
189
|
+
* four times the bytes and makes the header the last thing in the email to
|
|
190
|
+
* appear. Not an SVG: Gmail and Outlook do not draw SVG in mail, so every
|
|
191
|
+
* recipient on either would get a broken-image box where the logo belongs.
|
|
186
192
|
*
|
|
187
193
|
* Null unless this is the Aglyn-operated brand, for the reason
|
|
188
194
|
* {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at
|
|
@@ -195,7 +201,7 @@ var _clean4;
|
|
|
195
201
|
* the org set none, and a send that carries one is read as an org's own mail.
|
|
196
202
|
* Putting the platform's logo there would stamp it on every white-label org
|
|
197
203
|
* that left the field blank.
|
|
198
|
-
*/ export const PLATFORM_EMAIL_LOGO_URL = (_httpsUrl = httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL))) != null ? _httpsUrl : isAglynOperatedBrand() ? 'https://aglyn.com/
|
|
204
|
+
*/ export const PLATFORM_EMAIL_LOGO_URL = (_httpsUrl = httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL))) != null ? _httpsUrl : isAglynOperatedBrand() ? 'https://aglyn.com/_static/images/brand/aglyn-email-wordmark.png' : null;
|
|
199
205
|
/**
|
|
200
206
|
* The postal address printed beside {@link PLATFORM_BRAND_LEGAL_NAME} at the
|
|
201
207
|
* foot of the platform's own system email (AGL-3322).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-brand.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 * What this deployment calls itself (AGL-2153).\n *\n * the rule for the self-host path: *\"the self hosted owner should not have\n * to edit the source to update aglyn branded items urls or personal\n * identification items, should move these to env vars\"*. AGL-2016 did that for\n * the values where getting it wrong misroutes a legal notice. This is the\n * other half — the product name itself, which was a bare `'Aglyn'` literal in\n * nine places.\n *\n * ## Why one module rather than nine reads\n *\n * The literals were not merely duplicated, they were duplicated across a\n * boundary: `PLATFORM_GENERATOR_NAME` exists here AND as a hand-copied\n * constant in `apps/tenant/middleware.ts`, because the edge bundle cannot\n * import this lib. A single source they both read is what stops the copy from\n * drifting into a deployment that half-renames itself.\n *\n * ## Why a default at all, when operator-identity.ts refuses to have one\n *\n * `operator-identity.ts` sets out the rule and it is the right one: a value\n * whose wrongness is invisible to the operator and visible only off-site must\n * not be guessed. The brand is the opposite case. An operator who does not set\n * it sees \"Aglyn\" in their own browser tab on their own first page load, in\n * their own console. That is a default they can see and correct, which is a\n * default rather than a trap — the same test AGL-2022 applied to the tenant\n * apex.\n *\n * ## Why `NEXT_PUBLIC_`, and why dot notation is load-bearing\n *\n * The brand is printed to the public — that is what it is for — so\n * `NEXT_PUBLIC_` carries no disclosure risk. Next inlines `NEXT_PUBLIC_*` into\n * the browser bundle by textually substituting `process.env.NAME`; the\n * **bracket** form is never substituted and reads `undefined` in the browser\n * (AGL-2037). Every read below is dot notation on purpose.\n *\n * Build-time consequence for self-hosters: these are baked in when the image\n * is built, so they must be set *before* `docker compose build`, not merely\n * before `up`. `docs/SELF_HOSTING.md` says so.\n */\n\n/** A non-empty trimmed string, else undefined — `' '` is a half-finished `.env`. */\nconst clean = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The product name, as shown to end users.\n *\n * This is the browser-tab affix, the PWA name, the WebAuthn relying-party name\n * the OS credential prompt displays, the `<meta name=\"generator\">` and\n * `x-powered-by` on published sites, and the platform default `productName` /\n * `fromName` every non-white-label org's surfaces resolve to.\n */\nexport const PLATFORM_BRAND_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_NAME) ?? 'Aglyn'\n\n/**\n * The legal entity operating the software's own cloud, for copy that needs a\n * company rather than a product (`BRAND.ORG_NAME_LEGAL`).\n *\n * NOT the same thing as `operatorIdentity().name`, and the distinction matters.\n * This is a *brand* string with a visible default; that one is the identity a\n * statutory notice is addressed to and refuses to default. An operator who\n * sets only this has renamed the product, not published who they are.\n */\nexport const PLATFORM_BRAND_LEGAL_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME) ??\n `${PLATFORM_BRAND_NAME} LLC`\n\n/**\n * Where a \"need help?\" line points.\n *\n * Falls through the operator's OWN support address before it falls back to\n * ours, deliberately: an operator who followed the runbook has already set\n * `NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL`, and making them discover a second\n * variable to stop pointing their customers at Aglyn's support desk — which\n * cannot help those customers with anything — is a trap dressed as a default.\n *\n * Read here rather than imported from `operator-identity.ts` so this module\n * stays dependency-free and safe to import from anywhere, including the places\n * that cannot take the operator module's weight.\n *\n * ## Why the last fallback is the CONSOLE and not the marketing site\n *\n * It was `aglyn.com/support`, which answers 404 and has never existed\n * (AGL-3262) — MEASURED against the live site, not inferred: `/support`,\n * `/help` and `/docs` all render the not-found page and none is in the\n * sitemap. A support URL that 404s is worse than none, because it is printed\n * on a receipt beside a charge the reader is already unsure about.\n *\n * `app.aglyn.com/support` is a real page (AGL-3265): it resolves the\n * workspace from the session, so a customer with one lands on its support\n * umbrella and a customer with several picks. That is the whole reason it\n * exists — it is the one support address that needs nothing interpolated\n * into it, which is exactly what this constant is.\n *\n * The console is THIS install's (AGL-3322). `/support` is a console route, so\n * every console serves its own, and `NEXT_PUBLIC_CONSOLE_URL` is required of\n * every self-hosted install — so one that configured neither variable above\n * sends its customers to its own support page rather than to ours, where they\n * have no account. The line is in the footer of every system email the\n * platform sends. `app.aglyn.com` is left only for a build that names no\n * console at all.\n */\nexport const PLATFORM_SUPPORT_URL: string = (() => {\n const configured = clean(process.env.NEXT_PUBLIC_PLATFORM_SUPPORT_URL)\n if (configured) return configured\n const operatorEmail = clean(process.env.NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL)\n if (operatorEmail) return `mailto:${operatorEmail}`\n const consoleOrigin = clean(process.env.NEXT_PUBLIC_CONSOLE_URL)?.replace(\n /\\/+$/,\n '',\n )\n return `${consoleOrigin || 'https://app.aglyn.com'}/support`\n})()\n\n/** True when this deployment still answers to Aglyn's own brand. */\nexport const isAglynOperatedBrand = (): boolean =>\n PLATFORM_BRAND_NAME === 'Aglyn'\n\n/**\n * Where the platform's own front door is — the marketing home, not the help\n * desk.\n *\n * ## Why this is not `PLATFORM_SUPPORT_URL`\n *\n * The \"Made with …\" badge on a published free-tier site used to link to\n * `supportUrl`, and on this deployment `supportUrl` falls through to\n * `mailto:` + the operator support address. So the one link the platform gets\n * on every free customer's site — its only organic acquisition surface —\n * opened the visitor's mail client addressed to our support desk. A visitor\n * who liked the site and clicked to find out what built it got a blank email,\n * and support got the misdirected replies.\n *\n * They are genuinely different destinations. `supportUrl` answers \"I am a\n * customer and something is wrong\"; this answers \"what is this, and can I have\n * one\". Keeping one value for both guarantees one of them is wrong.\n *\n * ## Why it is null rather than aglyn.com off-brand\n *\n * A self-hosted deployment that renamed the product but set no home URL has\n * nowhere honest to send that visitor: \"Made with Foo\" pointing at aglyn.com\n * advertises the wrong company on Foo's customers' sites. The badge already\n * renders as a plain label when it has no destination (AGL-2428), which is the\n * correct outcome — so the Aglyn default applies only while this really is the\n * Aglyn-operated brand.\n */\nexport const PLATFORM_HOME_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_HOME_URL) ??\n (isAglynOperatedBrand() ? 'https://aglyn.com' : null)\n\n/**\n * The platform's own logo MARK — the compact, square, no-wordmark form — as a\n * URL an `<img>` on a PUBLISHED SITE can load.\n *\n * Site-relative on purpose: every app that renders it serves\n * `public/_static/…` from its own origin, so one path works on\n * `{sub}.aglyn.app` and on a customer's custom domain alike without knowing\n * either. The white variant is the one that survives the badge's dark pill and\n * an org's `primaryColor` behind it.\n *\n * Null unless this is the Aglyn-operated brand, for the same reason as\n * {@link PLATFORM_HOME_URL}: a renamed deployment must not stamp our mark on\n * its customers' sites. Such an operator sets `NEXT_PUBLIC_PLATFORM_MARK_URL`\n * to their own, or gets a text-only badge.\n */\nexport const PLATFORM_MARK_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_MARK_URL) ??\n (isAglynOperatedBrand()\n ? '/_static/images/brand/aglyn-logo-mark-white.svg'\n : null)\n\n/** An absolute `https://` URL, else undefined — anything else has no origin in an inbox. */\nconst httpsUrl = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n try {\n return new URL(value).protocol === 'https:' ? value : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * The platform's logo as its own system email draws it: the header of mail\n * sent under the platform's brand (AGL-3322).\n *\n * ABSOLUTE and https, unlike {@link PLATFORM_MARK_URL}. That mark is a\n * site-relative path because every app that draws it serves the file from its\n * own origin; an inbox has no origin, so a relative path there is a\n * broken-image box. A configured value that is not an absolute `https://` URL\n * is ignored rather than handed on to fail in somebody's inbox.\n *\n * The WORDMARK rather than the square mark: a header is read at a glance, and\n * the wordmark names the sender where the mark would need the reader to\n * already know it. This one is the file the Aglyn marketing site's own email\n * header block draws, served from that site's media CDN.\n *\n * Null unless this is the Aglyn-operated brand, for the reason\n * {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at\n * the top of its customers' mail. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL`, or gets its product name as the\n * header, in bold text.\n *\n * Deliberately NOT `PLATFORM_BRANDING_PROFILE.emailLogoUrl`, which stays null.\n * That field is an ORG's email logo: a white-label profile inherits it when\n * the org set none, and a send that carries one is read as an org's own mail.\n * Putting the platform's logo there would stamp it on every white-label org\n * that left the field blank.\n */\nexport const PLATFORM_EMAIL_LOGO_URL: string | null =\n httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL)) ??\n (isAglynOperatedBrand()\n ? 'https://aglyn.com/api/media/cdn/org:jWmGooWE3L:aglyn-marketing/YwrD-IDzcf'\n : null)\n\n/**\n * The postal address printed beside {@link PLATFORM_BRAND_LEGAL_NAME} at the\n * foot of the platform's own system email (AGL-3322).\n *\n * A BRAND string, beside the legal name whose line it completes, rather than\n * a field of `operatorIdentity()`, and the difference is the default. The\n * operator identity is what a statutory notice is addressed to and refuses to\n * guess; this is a line in a footer the operator sees in their own mail and\n * can correct, and for the Aglyn-operated brand the right value is known: the\n * registered agent's address the Aglyn marketing site's email footer prints.\n *\n * Null unless this is the Aglyn-operated brand: a renamed deployment must\n * never print our address as its own. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS`, or their footer carries the legal\n * name alone. Line breaks in a configured value are folded into commas,\n * because the footer prints the address on one line.\n */\nexport const PLATFORM_POSTAL_ADDRESS: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS)\n ?.split(/\\s*\\n\\s*/)\n .filter(Boolean)\n .join(', ') ??\n (isAglynOperatedBrand()\n ? 'c/o Northwest Registered Agent, LLC, 5900 Balcones Drive STE 100, Austin, TX 78731'\n : null)\n\n/**\n * A branding profile with every field present — what a branded surface\n * renders. Image/color/domain fields are nullable (Aglyn's own surfaces\n * bake those in rather than carry a URL); the remaining text fields always\n * have a value so callers never string-concatenate `undefined`.\n *\n * `supportUrl` is nullable too, and it is the one field where null carries a\n * decision rather than an absence — see {@link resolveBrandingProfile}. A\n * caller must render NOTHING for it, never a placeholder and never a\n * substitute.\n */\nexport interface ResolvedBrandingProfile {\n productName: string\n logoUrl: string | null\n faviconUrl: string | null\n primaryColor: string | null\n supportUrl: string | null\n /**\n * The brand's own FRONT DOOR — where \"Made with <product>\" sends a visitor\n * who wants to know what built the site they are looking at.\n *\n * Deliberately separate from {@link ResolvedBrandingProfile.supportUrl},\n * which the badge used to borrow: a help-desk address is the wrong answer to\n * \"what is this\", and on this deployment it resolves to a `mailto:`, so the\n * badge opened a blank email instead of a web page.\n *\n * Nullable for the same reason `supportUrl` is — a brand with nowhere to\n * send that visitor gets a plain label, never a substitute destination.\n */\n homeUrl: string | null\n fromName: string\n emailLogoUrl: string | null\n customConsoleDomain: string | null\n}\n\n/**\n * The PLATFORM (non-white-label) brand — the fallback every surface gets when\n * an org lacks the `whiteLabel` entitlement, and the gap-filler for a partial\n * agency profile.\n *\n * It lives beside the literals it is assembled from rather than beside the\n * entitlement gate that hands it out, and that placement is load-bearing\n * rather than tidying. A published tenant page renders the attribution badge\n * from this constant and reads nothing else out of `plan-entitlements.ts`;\n * defining it there put that module's whole plan, quota and entitlement table\n * — the single largest first-party module a visitor could reach — into the\n * first load of every anonymous visitor to every customer site.\n * `plan-entitlements.ts` re-exports it, so every existing caller is unchanged.\n *\n * The name says the PLATFORM's brand rather than Aglyn's because on a\n * self-host install that assertion stops being true: which platform this is,\n * is configuration. `resolveBrandingProfile` is the single resolver EVERY\n * branded surface routes through — console chrome, published-site badge and\n * title, transactional email — and this is its fallback, so all of them get a\n * self-host brand without extending the white-label machinery at all.\n */\nexport const PLATFORM_BRANDING_PROFILE: ResolvedBrandingProfile = {\n productName: PLATFORM_BRAND_NAME,\n // The platform's own surfaces still bake their logo in — this is here for\n // the surfaces that CANNOT, which is every surface rendered on somebody\n // else's site. The free-tier attribution badge is the one that exists\n // today, and it carried no mark at all while this was null.\n logoUrl: PLATFORM_MARK_URL,\n faviconUrl: null,\n primaryColor: null,\n supportUrl: PLATFORM_SUPPORT_URL,\n homeUrl: PLATFORM_HOME_URL,\n fromName: PLATFORM_BRAND_NAME,\n emailLogoUrl: null,\n customConsoleDomain: null,\n}\n"],"names":["clean","httpsUrl","value","text","trim","length","undefined","PLATFORM_BRAND_NAME","process","env","NEXT_PUBLIC_PLATFORM_BRAND_NAME","PLATFORM_BRAND_LEGAL_NAME","NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME","PLATFORM_SUPPORT_URL","configured","NEXT_PUBLIC_PLATFORM_SUPPORT_URL","operatorEmail","NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL","consoleOrigin","NEXT_PUBLIC_CONSOLE_URL","replace","isAglynOperatedBrand","PLATFORM_HOME_URL","NEXT_PUBLIC_PLATFORM_HOME_URL","PLATFORM_MARK_URL","NEXT_PUBLIC_PLATFORM_MARK_URL","URL","protocol","PLATFORM_EMAIL_LOGO_URL","NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL","PLATFORM_POSTAL_ADDRESS","NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS","split","filter","Boolean","join","PLATFORM_BRANDING_PROFILE","productName","logoUrl","faviconUrl","primaryColor","supportUrl","homeUrl","fromName","emailLogoUrl","customConsoleDomain"],"mappings":"IAyEEA,QAYAA,SAkFAA,SAmBAA,SA2CAC;IAuBAD;AA5PF;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,oFAAoF,GACpF,MAAMA,QAAQ,CAACE;IACb,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACxD,OAAOD,KAAKE,MAAM,GAAGF,OAAOG;AAC9B;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,uBACXP,SAAAA,MAAMQ,QAAQC,GAAG,CAACC,+BAA+B,aAAjDV,SAAsD,QAAO;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMW,6BACXX,UAAAA,MAAMQ,QAAQC,GAAG,CAACG,qCAAqC,aAAvDZ,UACA,GAAGO,oBAAoB,IAAI,CAAC,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,MAAMM,uBAA+B,AAAC,CAAA;QAKrBb;IAJtB,MAAMc,aAAad,MAAMQ,QAAQC,GAAG,CAACM,gCAAgC;IACrE,IAAID,YAAY,OAAOA;IACvB,MAAME,gBAAgBhB,MAAMQ,QAAQC,GAAG,CAACQ,kCAAkC;IAC1E,IAAID,eAAe,OAAO,CAAC,OAAO,EAAEA,eAAe;IACnD,MAAME,iBAAgBlB,SAAAA,MAAMQ,QAAQC,GAAG,CAACU,uBAAuB,sBAAzCnB,OAA4CoB,OAAO,CACvE,QACA;IAEF,OAAO,GAAGF,iBAAiB,wBAAwB,QAAQ,CAAC;AAC9D,CAAA,IAAI;AAEJ,kEAAkE,GAClE,OAAO,MAAMG,uBAAuB,IAClCd,wBAAwB,QAAO;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMe,qBACXtB,UAAAA,MAAMQ,QAAQC,GAAG,CAACc,6BAA6B,aAA/CvB,UACCqB,yBAAyB,sBAAsB,KAAK;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,qBACXxB,UAAAA,MAAMQ,QAAQC,GAAG,CAACgB,6BAA6B,aAA/CzB,UACCqB,yBACG,oDACA,KAAK;AAEX,0FAA0F,GAC1F,MAAMpB,WAAW,CAACC;IAChB,IAAI,CAACA,OAAO,OAAOI;IACnB,IAAI;QACF,OAAO,IAAIoB,IAAIxB,OAAOyB,QAAQ,KAAK,WAAWzB,QAAQI;IACxD,EAAE,eAAM;QACN,OAAOA;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMsB,2BACX3B,YAAAA,SAASD,MAAMQ,QAAQC,GAAG,CAACoB,mCAAmC,cAA9D5B,YACCoB,yBACG,8EACA,KAAK;AAEX;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMS,mCACX9B,UAAAA,MAAMQ,QAAQC,GAAG,CAACsB,mCAAmC,sBAArD/B,QACIgC,KAAK,CAAC,YACPC,MAAM,CAACC,SACPC,IAAI,CAAC,wBACPd,yBACG,uFACA,KAAK;AAqCX;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMe,4BAAqD;IAChEC,aAAa9B;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,4DAA4D;IAC5D+B,SAASd;IACTe,YAAY;IACZC,cAAc;IACdC,YAAY5B;IACZ6B,SAASpB;IACTqB,UAAUpC;IACVqC,cAAc;IACdC,qBAAqB;AACvB,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/platform-brand.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 * What this deployment calls itself (AGL-2153).\n *\n * the rule for the self-host path: *\"the self hosted owner should not have\n * to edit the source to update aglyn branded items urls or personal\n * identification items, should move these to env vars\"*. AGL-2016 did that for\n * the values where getting it wrong misroutes a legal notice. This is the\n * other half — the product name itself, which was a bare `'Aglyn'` literal in\n * nine places.\n *\n * ## Why one module rather than nine reads\n *\n * The literals were not merely duplicated, they were duplicated across a\n * boundary: `PLATFORM_GENERATOR_NAME` exists here AND as a hand-copied\n * constant in `apps/tenant/middleware.ts`, because the edge bundle cannot\n * import this lib. A single source they both read is what stops the copy from\n * drifting into a deployment that half-renames itself.\n *\n * ## Why a default at all, when operator-identity.ts refuses to have one\n *\n * `operator-identity.ts` sets out the rule and it is the right one: a value\n * whose wrongness is invisible to the operator and visible only off-site must\n * not be guessed. The brand is the opposite case. An operator who does not set\n * it sees \"Aglyn\" in their own browser tab on their own first page load, in\n * their own console. That is a default they can see and correct, which is a\n * default rather than a trap — the same test AGL-2022 applied to the tenant\n * apex.\n *\n * ## Why `NEXT_PUBLIC_`, and why dot notation is load-bearing\n *\n * The brand is printed to the public — that is what it is for — so\n * `NEXT_PUBLIC_` carries no disclosure risk. Next inlines `NEXT_PUBLIC_*` into\n * the browser bundle by textually substituting `process.env.NAME`; the\n * **bracket** form is never substituted and reads `undefined` in the browser\n * (AGL-2037). Every read below is dot notation on purpose.\n *\n * Build-time consequence for self-hosters: these are baked in when the image\n * is built, so they must be set *before* `docker compose build`, not merely\n * before `up`. `docs/SELF_HOSTING.md` says so.\n */\n\n/** A non-empty trimmed string, else undefined — `' '` is a half-finished `.env`. */\nconst clean = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text.length ? text : undefined\n}\n\n/**\n * The product name, as shown to end users.\n *\n * This is the browser-tab affix, the PWA name, the WebAuthn relying-party name\n * the OS credential prompt displays, the `<meta name=\"generator\">` and\n * `x-powered-by` on published sites, and the platform default `productName` /\n * `fromName` every non-white-label org's surfaces resolve to.\n */\nexport const PLATFORM_BRAND_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_NAME) ?? 'Aglyn'\n\n/**\n * The legal entity operating the software's own cloud, for copy that needs a\n * company rather than a product (`BRAND.ORG_NAME_LEGAL`).\n *\n * NOT the same thing as `operatorIdentity().name`, and the distinction matters.\n * This is a *brand* string with a visible default; that one is the identity a\n * statutory notice is addressed to and refuses to default. An operator who\n * sets only this has renamed the product, not published who they are.\n */\nexport const PLATFORM_BRAND_LEGAL_NAME =\n clean(process.env.NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME) ??\n `${PLATFORM_BRAND_NAME} LLC`\n\n/**\n * Where a \"need help?\" line points.\n *\n * Falls through the operator's OWN support address before it falls back to\n * ours, deliberately: an operator who followed the runbook has already set\n * `NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL`, and making them discover a second\n * variable to stop pointing their customers at Aglyn's support desk — which\n * cannot help those customers with anything — is a trap dressed as a default.\n *\n * Read here rather than imported from `operator-identity.ts` so this module\n * stays dependency-free and safe to import from anywhere, including the places\n * that cannot take the operator module's weight.\n *\n * ## Why the last fallback is the CONSOLE and not the marketing site\n *\n * It was `aglyn.com/support`, which answers 404 and has never existed\n * (AGL-3262) — MEASURED against the live site, not inferred: `/support`,\n * `/help` and `/docs` all render the not-found page and none is in the\n * sitemap. A support URL that 404s is worse than none, because it is printed\n * on a receipt beside a charge the reader is already unsure about.\n *\n * `app.aglyn.com/support` is a real page (AGL-3265): it resolves the\n * workspace from the session, so a customer with one lands on its support\n * umbrella and a customer with several picks. That is the whole reason it\n * exists — it is the one support address that needs nothing interpolated\n * into it, which is exactly what this constant is.\n *\n * The console is THIS install's (AGL-3322). `/support` is a console route, so\n * every console serves its own, and `NEXT_PUBLIC_CONSOLE_URL` is required of\n * every self-hosted install — so one that configured neither variable above\n * sends its customers to its own support page rather than to ours, where they\n * have no account. The line is in the footer of every system email the\n * platform sends. `app.aglyn.com` is left only for a build that names no\n * console at all.\n */\nexport const PLATFORM_SUPPORT_URL: string = (() => {\n const configured = clean(process.env.NEXT_PUBLIC_PLATFORM_SUPPORT_URL)\n if (configured) return configured\n const operatorEmail = clean(process.env.NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL)\n if (operatorEmail) return `mailto:${operatorEmail}`\n const consoleOrigin = clean(process.env.NEXT_PUBLIC_CONSOLE_URL)?.replace(\n /\\/+$/,\n '',\n )\n return `${consoleOrigin || 'https://app.aglyn.com'}/support`\n})()\n\n/** True when this deployment still answers to Aglyn's own brand. */\nexport const isAglynOperatedBrand = (): boolean =>\n PLATFORM_BRAND_NAME === 'Aglyn'\n\n/**\n * Where the platform's own front door is — the marketing home, not the help\n * desk.\n *\n * ## Why this is not `PLATFORM_SUPPORT_URL`\n *\n * The \"Made with …\" badge on a published free-tier site used to link to\n * `supportUrl`, and on this deployment `supportUrl` falls through to\n * `mailto:` + the operator support address. So the one link the platform gets\n * on every free customer's site — its only organic acquisition surface —\n * opened the visitor's mail client addressed to our support desk. A visitor\n * who liked the site and clicked to find out what built it got a blank email,\n * and support got the misdirected replies.\n *\n * They are genuinely different destinations. `supportUrl` answers \"I am a\n * customer and something is wrong\"; this answers \"what is this, and can I have\n * one\". Keeping one value for both guarantees one of them is wrong.\n *\n * ## Why it is null rather than aglyn.com off-brand\n *\n * A self-hosted deployment that renamed the product but set no home URL has\n * nowhere honest to send that visitor: \"Made with Foo\" pointing at aglyn.com\n * advertises the wrong company on Foo's customers' sites. The badge already\n * renders as a plain label when it has no destination (AGL-2428), which is the\n * correct outcome — so the Aglyn default applies only while this really is the\n * Aglyn-operated brand.\n */\nexport const PLATFORM_HOME_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_HOME_URL) ??\n (isAglynOperatedBrand() ? 'https://aglyn.com' : null)\n\n/**\n * The platform's own logo MARK — the compact, square, no-wordmark form — as a\n * URL an `<img>` on a PUBLISHED SITE can load.\n *\n * Site-relative on purpose: every app that renders it serves\n * `public/_static/…` from its own origin, so one path works on\n * `{sub}.aglyn.app` and on a customer's custom domain alike without knowing\n * either. The white variant is the one that survives the badge's dark pill and\n * an org's `primaryColor` behind it.\n *\n * Null unless this is the Aglyn-operated brand, for the same reason as\n * {@link PLATFORM_HOME_URL}: a renamed deployment must not stamp our mark on\n * its customers' sites. Such an operator sets `NEXT_PUBLIC_PLATFORM_MARK_URL`\n * to their own, or gets a text-only badge.\n */\nexport const PLATFORM_MARK_URL: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_MARK_URL) ??\n (isAglynOperatedBrand()\n ? '/_static/images/brand/aglyn-logo-mark-white.svg'\n : null)\n\n/** An absolute `https://` URL, else undefined — anything else has no origin in an inbox. */\nconst httpsUrl = (value: string | undefined): string | undefined => {\n if (!value) return undefined\n try {\n return new URL(value).protocol === 'https:' ? value : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * The platform's logo as its own system email draws it: the header of mail\n * sent under the platform's brand (AGL-3322).\n *\n * ABSOLUTE and https, unlike {@link PLATFORM_MARK_URL}. That mark is a\n * site-relative path because every app that draws it serves the file from its\n * own origin; an inbox has no origin, so a relative path there is a\n * broken-image box. A configured value that is not an absolute `https://` URL\n * is ignored rather than handed on to fail in somebody's inbox.\n *\n * The WORDMARK rather than the square mark: a header is read at a glance, and\n * the wordmark names the sender where the mark would need the reader to\n * already know it.\n *\n * A PNG sized for the slot (AGL-3367): 396×120, three times the 132×40 the\n * header draws it at, about 15 KB, served as a static file off aglyn.com's\n * edge. The master-resolution file behind the media CDN's 60-second cache is\n * four times the bytes and makes the header the last thing in the email to\n * appear. Not an SVG: Gmail and Outlook do not draw SVG in mail, so every\n * recipient on either would get a broken-image box where the logo belongs.\n *\n * Null unless this is the Aglyn-operated brand, for the reason\n * {@link PLATFORM_HOME_URL} is: a renamed deployment must not put our logo at\n * the top of its customers' mail. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL`, or gets its product name as the\n * header, in bold text.\n *\n * Deliberately NOT `PLATFORM_BRANDING_PROFILE.emailLogoUrl`, which stays null.\n * That field is an ORG's email logo: a white-label profile inherits it when\n * the org set none, and a send that carries one is read as an org's own mail.\n * Putting the platform's logo there would stamp it on every white-label org\n * that left the field blank.\n */\nexport const PLATFORM_EMAIL_LOGO_URL: string | null =\n httpsUrl(clean(process.env.NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL)) ??\n (isAglynOperatedBrand()\n ? 'https://aglyn.com/_static/images/brand/aglyn-email-wordmark.png'\n : null)\n\n/**\n * The postal address printed beside {@link PLATFORM_BRAND_LEGAL_NAME} at the\n * foot of the platform's own system email (AGL-3322).\n *\n * A BRAND string, beside the legal name whose line it completes, rather than\n * a field of `operatorIdentity()`, and the difference is the default. The\n * operator identity is what a statutory notice is addressed to and refuses to\n * guess; this is a line in a footer the operator sees in their own mail and\n * can correct, and for the Aglyn-operated brand the right value is known: the\n * registered agent's address the Aglyn marketing site's email footer prints.\n *\n * Null unless this is the Aglyn-operated brand: a renamed deployment must\n * never print our address as its own. Such an operator sets\n * `NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS`, or their footer carries the legal\n * name alone. Line breaks in a configured value are folded into commas,\n * because the footer prints the address on one line.\n */\nexport const PLATFORM_POSTAL_ADDRESS: string | null =\n clean(process.env.NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS)\n ?.split(/\\s*\\n\\s*/)\n .filter(Boolean)\n .join(', ') ??\n (isAglynOperatedBrand()\n ? 'c/o Northwest Registered Agent, LLC, 5900 Balcones Drive STE 100, Austin, TX 78731'\n : null)\n\n/**\n * A branding profile with every field present — what a branded surface\n * renders. Image/color/domain fields are nullable (Aglyn's own surfaces\n * bake those in rather than carry a URL); the remaining text fields always\n * have a value so callers never string-concatenate `undefined`.\n *\n * `supportUrl` is nullable too, and it is the one field where null carries a\n * decision rather than an absence — see {@link resolveBrandingProfile}. A\n * caller must render NOTHING for it, never a placeholder and never a\n * substitute.\n */\nexport interface ResolvedBrandingProfile {\n productName: string\n logoUrl: string | null\n faviconUrl: string | null\n primaryColor: string | null\n supportUrl: string | null\n /**\n * The brand's own FRONT DOOR — where \"Made with <product>\" sends a visitor\n * who wants to know what built the site they are looking at.\n *\n * Deliberately separate from {@link ResolvedBrandingProfile.supportUrl},\n * which the badge used to borrow: a help-desk address is the wrong answer to\n * \"what is this\", and on this deployment it resolves to a `mailto:`, so the\n * badge opened a blank email instead of a web page.\n *\n * Nullable for the same reason `supportUrl` is — a brand with nowhere to\n * send that visitor gets a plain label, never a substitute destination.\n */\n homeUrl: string | null\n fromName: string\n emailLogoUrl: string | null\n customConsoleDomain: string | null\n}\n\n/**\n * The PLATFORM (non-white-label) brand — the fallback every surface gets when\n * an org lacks the `whiteLabel` entitlement, and the gap-filler for a partial\n * agency profile.\n *\n * It lives beside the literals it is assembled from rather than beside the\n * entitlement gate that hands it out, and that placement is load-bearing\n * rather than tidying. A published tenant page renders the attribution badge\n * from this constant and reads nothing else out of `plan-entitlements.ts`;\n * defining it there put that module's whole plan, quota and entitlement table\n * — the single largest first-party module a visitor could reach — into the\n * first load of every anonymous visitor to every customer site.\n * `plan-entitlements.ts` re-exports it, so every existing caller is unchanged.\n *\n * The name says the PLATFORM's brand rather than Aglyn's because on a\n * self-host install that assertion stops being true: which platform this is,\n * is configuration. `resolveBrandingProfile` is the single resolver EVERY\n * branded surface routes through — console chrome, published-site badge and\n * title, transactional email — and this is its fallback, so all of them get a\n * self-host brand without extending the white-label machinery at all.\n */\nexport const PLATFORM_BRANDING_PROFILE: ResolvedBrandingProfile = {\n productName: PLATFORM_BRAND_NAME,\n // The platform's own surfaces still bake their logo in — this is here for\n // the surfaces that CANNOT, which is every surface rendered on somebody\n // else's site. The free-tier attribution badge is the one that exists\n // today, and it carried no mark at all while this was null.\n logoUrl: PLATFORM_MARK_URL,\n faviconUrl: null,\n primaryColor: null,\n supportUrl: PLATFORM_SUPPORT_URL,\n homeUrl: PLATFORM_HOME_URL,\n fromName: PLATFORM_BRAND_NAME,\n emailLogoUrl: null,\n customConsoleDomain: null,\n}\n"],"names":["clean","httpsUrl","value","text","trim","length","undefined","PLATFORM_BRAND_NAME","process","env","NEXT_PUBLIC_PLATFORM_BRAND_NAME","PLATFORM_BRAND_LEGAL_NAME","NEXT_PUBLIC_PLATFORM_BRAND_LEGAL_NAME","PLATFORM_SUPPORT_URL","configured","NEXT_PUBLIC_PLATFORM_SUPPORT_URL","operatorEmail","NEXT_PUBLIC_OPERATOR_SUPPORT_EMAIL","consoleOrigin","NEXT_PUBLIC_CONSOLE_URL","replace","isAglynOperatedBrand","PLATFORM_HOME_URL","NEXT_PUBLIC_PLATFORM_HOME_URL","PLATFORM_MARK_URL","NEXT_PUBLIC_PLATFORM_MARK_URL","URL","protocol","PLATFORM_EMAIL_LOGO_URL","NEXT_PUBLIC_PLATFORM_EMAIL_LOGO_URL","PLATFORM_POSTAL_ADDRESS","NEXT_PUBLIC_PLATFORM_POSTAL_ADDRESS","split","filter","Boolean","join","PLATFORM_BRANDING_PROFILE","productName","logoUrl","faviconUrl","primaryColor","supportUrl","homeUrl","fromName","emailLogoUrl","customConsoleDomain"],"mappings":"IAyEEA,QAYAA,SAkFAA,SAmBAA,SAiDAC;IAuBAD;AAlQF;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,oFAAoF,GACpF,MAAMA,QAAQ,CAACE;IACb,MAAMC,OAAO,OAAOD,UAAU,WAAWA,MAAME,IAAI,KAAK;IACxD,OAAOD,KAAKE,MAAM,GAAGF,OAAOG;AAC9B;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMC,uBACXP,SAAAA,MAAMQ,QAAQC,GAAG,CAACC,+BAA+B,aAAjDV,SAAsD,QAAO;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMW,6BACXX,UAAAA,MAAMQ,QAAQC,GAAG,CAACG,qCAAqC,aAAvDZ,UACA,GAAGO,oBAAoB,IAAI,CAAC,CAAA;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,MAAMM,uBAA+B,AAAC,CAAA;QAKrBb;IAJtB,MAAMc,aAAad,MAAMQ,QAAQC,GAAG,CAACM,gCAAgC;IACrE,IAAID,YAAY,OAAOA;IACvB,MAAME,gBAAgBhB,MAAMQ,QAAQC,GAAG,CAACQ,kCAAkC;IAC1E,IAAID,eAAe,OAAO,CAAC,OAAO,EAAEA,eAAe;IACnD,MAAME,iBAAgBlB,SAAAA,MAAMQ,QAAQC,GAAG,CAACU,uBAAuB,sBAAzCnB,OAA4CoB,OAAO,CACvE,QACA;IAEF,OAAO,GAAGF,iBAAiB,wBAAwB,QAAQ,CAAC;AAC9D,CAAA,IAAI;AAEJ,kEAAkE,GAClE,OAAO,MAAMG,uBAAuB,IAClCd,wBAAwB,QAAO;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMe,qBACXtB,UAAAA,MAAMQ,QAAQC,GAAG,CAACc,6BAA6B,aAA/CvB,UACCqB,yBAAyB,sBAAsB,KAAK;AAEvD;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,qBACXxB,UAAAA,MAAMQ,QAAQC,GAAG,CAACgB,6BAA6B,aAA/CzB,UACCqB,yBACG,oDACA,KAAK;AAEX,0FAA0F,GAC1F,MAAMpB,WAAW,CAACC;IAChB,IAAI,CAACA,OAAO,OAAOI;IACnB,IAAI;QACF,OAAO,IAAIoB,IAAIxB,OAAOyB,QAAQ,KAAK,WAAWzB,QAAQI;IACxD,EAAE,eAAM;QACN,OAAOA;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,MAAMsB,2BACX3B,YAAAA,SAASD,MAAMQ,QAAQC,GAAG,CAACoB,mCAAmC,cAA9D5B,YACCoB,yBACG,oEACA,KAAK;AAEX;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMS,mCACX9B,UAAAA,MAAMQ,QAAQC,GAAG,CAACsB,mCAAmC,sBAArD/B,QACIgC,KAAK,CAAC,YACPC,MAAM,CAACC,SACPC,IAAI,CAAC,wBACPd,yBACG,uFACA,KAAK;AAqCX;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMe,4BAAqD;IAChEC,aAAa9B;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,4DAA4D;IAC5D+B,SAASd;IACTe,YAAY;IACZC,cAAc;IACdC,YAAY5B;IACZ6B,SAASpB;IACTqB,UAAUpC;IACVqC,cAAc;IACdC,qBAAqB;AACvB,EAAC"}
|
|
@@ -86,8 +86,9 @@ export declare const MAX_PLUGIN_BUNDLE_BYTES = 1000000;
|
|
|
86
86
|
* 5 — URLs held in a constant are resolved (AGL-1093).
|
|
87
87
|
* 6 — registrations are compared with the declared contributions (AGL-3116).
|
|
88
88
|
* 7 — inputs that collect a password, card or one-time code (AGL-3362).
|
|
89
|
+
* 8 — declared network origins that are a brand's lookalike (AGL-3365).
|
|
89
90
|
*/
|
|
90
|
-
export declare const PLUGIN_VERIFIER_VERSION =
|
|
91
|
+
export declare const PLUGIN_VERIFIER_VERSION = 8;
|
|
91
92
|
/** A verdict as stored on a `pluginVersions` doc (AGL-962). */
|
|
92
93
|
export interface StoredBundleVerdict {
|
|
93
94
|
ok?: boolean;
|
|
@@ -37,6 +37,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
37
37
|
// The leaf, not the barrel: the phishing screen's own reading of an input's
|
|
38
38
|
// type, autocomplete token and raw HTML (AGL-3362).
|
|
39
39
|
import { credentialForInputAttribute, credentialInHtml } from "@aglyn/shared-util-email/hosted-page-screen";
|
|
40
|
+
import { lookalikeBrandForHost } from "@aglyn/shared-util-email/outbound-phishing-screen";
|
|
40
41
|
const CHECK_LABELS = {
|
|
41
42
|
parse: 'Parses as an ES module',
|
|
42
43
|
entry: 'Exports an entry point',
|
|
@@ -83,7 +84,8 @@ export const MAX_PLUGIN_BUNDLE_BYTES = 1000000;
|
|
|
83
84
|
* 5 — URLs held in a constant are resolved (AGL-1093).
|
|
84
85
|
* 6 — registrations are compared with the declared contributions (AGL-3116).
|
|
85
86
|
* 7 — inputs that collect a password, card or one-time code (AGL-3362).
|
|
86
|
-
|
|
87
|
+
* 8 — declared network origins that are a brand's lookalike (AGL-3365).
|
|
88
|
+
*/ export const PLUGIN_VERIFIER_VERSION = 8;
|
|
87
89
|
/**
|
|
88
90
|
* Whether a stored verdict may be served instead of re-running the checks.
|
|
89
91
|
*
|
|
@@ -573,6 +575,36 @@ const BASE64_BLOB = /^[A-Za-z0-9+/\r\n]+={0,2}$/;
|
|
|
573
575
|
...found
|
|
574
576
|
].map((field)=>`builds an input that collects ${words[field]} — confirm what it is for, and that ` + 'it is sent only to the origins the manifest declares');
|
|
575
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Declared network origins that wear a brand without being the brand's
|
|
580
|
+
* (AGL-3365) — a reviewer's QUESTION, read with the phishing screen's own
|
|
581
|
+
* lookalike rule (`lookalikeBrandForHost`), the one that holds a link in an
|
|
582
|
+
* email or on a page.
|
|
583
|
+
*
|
|
584
|
+
* The manifest's origins are what the loader stamps into the plugin frame's
|
|
585
|
+
* `connect-src`, so they are exactly where a plugin's data and a visitor's
|
|
586
|
+
* input can go. A plugin that integrates with PayPal declares
|
|
587
|
+
* `https://api-m.paypal.com`, which is PayPal's own domain and passes; one that
|
|
588
|
+
* declares `https://paypal-verify.net` or `https://aglyn-billing.com` is
|
|
589
|
+
* sending somewhere dressed as somebody else. A warning rather than an error
|
|
590
|
+
* because every plugin version is reviewed before it installs, and the
|
|
591
|
+
* reviewer is the one who can ask whose domain it is.
|
|
592
|
+
*/ function lookalikeDeclaredOrigins(declared) {
|
|
593
|
+
const messages = [];
|
|
594
|
+
for (const origin of declared){
|
|
595
|
+
let host;
|
|
596
|
+
try {
|
|
597
|
+
host = new URL(origin).hostname;
|
|
598
|
+
} catch (unused) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const brand = lookalikeBrandForHost(host);
|
|
602
|
+
if (brand) {
|
|
603
|
+
messages.push(`the manifest declares ${origin}, which wears the ${brand.label} name ` + `but is not ${brand.label}'s domain — confirm whose it is before approving`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return messages;
|
|
607
|
+
}
|
|
576
608
|
function analyseBundle(source, options) {
|
|
577
609
|
var _ref;
|
|
578
610
|
const problems = [];
|
|
@@ -809,6 +841,9 @@ function analyseBundle(source, options) {
|
|
|
809
841
|
return origin;
|
|
810
842
|
}
|
|
811
843
|
}));
|
|
844
|
+
for (const message of lookalikeDeclaredOrigins(declared != null ? declared : [])){
|
|
845
|
+
add('warning', 'network', message);
|
|
846
|
+
}
|
|
812
847
|
/** Level for a network finding: a claim only when we know what was declared. */ const networkLevel = declared ? 'error' : 'warning';
|
|
813
848
|
// What the bundle reaches for, so the summary row can SAY it (AGL-1087) —
|
|
814
849
|
// "calls https://api.stripe.com, declared" is the line a reviewer would
|