@aglyn/aglyn 1.0.0-beta.214 → 1.0.0-beta.216
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/deferred-image.d.ts +3 -3
- package/src/lib/app-utils/deferred-image.js +3 -3
- package/src/lib/app-utils/deferred-image.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 +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +24 -10
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/marketplace-theme.d.ts +9 -4
- package/src/lib/app-utils/marketplace-theme.js +11 -1
- package/src/lib/app-utils/marketplace-theme.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +7 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.js +14 -8
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +4 -0
- package/src/lib/app-utils/platform-brand.js +2 -0
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/publisher-agreement.d.ts +36 -1
- package/src/lib/app-utils/publisher-agreement.js +48 -1
- package/src/lib/app-utils/publisher-agreement.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +2 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/theme-library.d.ts +206 -0
- package/src/lib/app-utils/theme-library.js +566 -0
- package/src/lib/app-utils/theme-library.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +12 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.js +2 -1
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +44 -0
- package/src/lib/plugin-manager/feature-plugins.js +25 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +11 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +2 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
|
@@ -181,7 +181,11 @@ export declare const PLATFORM_POSTAL_ADDRESS: string | null;
|
|
|
181
181
|
export interface ResolvedBrandingProfile {
|
|
182
182
|
productName: string;
|
|
183
183
|
logoUrl: string | null;
|
|
184
|
+
/** The logo for dark grounds; null means {@link logoUrl} serves both. */
|
|
185
|
+
logoDarkUrl: string | null;
|
|
184
186
|
faviconUrl: string | null;
|
|
187
|
+
/** The favicon for a dark tab strip; null means {@link faviconUrl} serves both. */
|
|
188
|
+
faviconDarkUrl: string | null;
|
|
185
189
|
primaryColor: string | null;
|
|
186
190
|
supportUrl: string | null;
|
|
187
191
|
/**
|
|
@@ -246,7 +246,9 @@ var _clean4;
|
|
|
246
246
|
// else's site. The free-tier attribution badge is the one that exists
|
|
247
247
|
// today, and it carried no mark at all while this was null.
|
|
248
248
|
logoUrl: PLATFORM_MARK_URL,
|
|
249
|
+
logoDarkUrl: null,
|
|
249
250
|
faviconUrl: null,
|
|
251
|
+
faviconDarkUrl: null,
|
|
250
252
|
primaryColor: null,
|
|
251
253
|
supportUrl: PLATFORM_SUPPORT_URL,
|
|
252
254
|
homeUrl: PLATFORM_HOME_URL,
|
|
@@ -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.\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"}
|
|
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 /** The logo for dark grounds; null means {@link logoUrl} serves both. */\n logoDarkUrl: string | null\n faviconUrl: string | null\n /** The favicon for a dark tab strip; null means {@link faviconUrl} serves both. */\n faviconDarkUrl: 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 logoDarkUrl: null,\n faviconUrl: null,\n faviconDarkUrl: 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","logoDarkUrl","faviconUrl","faviconDarkUrl","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;AAyCX;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,MAAMe,4BAAqD;IAChEC,aAAa9B;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,4DAA4D;IAC5D+B,SAASd;IACTe,aAAa;IACbC,YAAY;IACZC,gBAAgB;IAChBC,cAAc;IACdC,YAAY9B;IACZ+B,SAAStB;IACTuB,UAAUtC;IACVuC,cAAc;IACdC,qBAAqB;AACvB,EAAC"}
|
|
@@ -139,7 +139,8 @@ export declare const PUBLISHER_AGREEMENT_TITLE = "Marketplace Publisher Agreemen
|
|
|
139
139
|
*
|
|
140
140
|
* TO PUBLISH A CHANGE: publish the besigner edit first, confirm the live page
|
|
141
141
|
* serves it, capture a new snapshot under the new version's directory, then
|
|
142
|
-
* bump `PUBLISHER_AGREEMENT_VERSION` and these two values together
|
|
142
|
+
* bump `PUBLISHER_AGREEMENT_VERSION` and these two values together, and add
|
|
143
|
+
* the version's entry to `PUBLISHER_AGREEMENT_CHANGES` below. Never
|
|
143
144
|
* hand-write a snapshot: it is evidence of what a publisher was shown, and a
|
|
144
145
|
* snapshot of unpublished text is a false record.
|
|
145
146
|
*/
|
|
@@ -195,6 +196,40 @@ export declare const PUBLISHER_AGREEMENT_POINTS: readonly {
|
|
|
195
196
|
label: string;
|
|
196
197
|
detail: string;
|
|
197
198
|
}[];
|
|
199
|
+
/**
|
|
200
|
+
* What each version changed from the one before it, in the words a publisher
|
|
201
|
+
* re-accepting needs (AGL-3407).
|
|
202
|
+
*
|
|
203
|
+
* An org that accepted an older version is asked again, and "the terms
|
|
204
|
+
* changed" with nothing more is a request to re-read thirteen sections to
|
|
205
|
+
* find two edited clauses. Each entry names the sections that moved, read off
|
|
206
|
+
* the archived snapshots beside this module (`legal/publisher-agreement/`) —
|
|
207
|
+
* never written from memory of the edit, for the same reason a snapshot is
|
|
208
|
+
* never hand-written.
|
|
209
|
+
*
|
|
210
|
+
* Oldest first. The last entry is always {@link PUBLISHER_AGREEMENT_VERSION}:
|
|
211
|
+
* a bump adds its entry in the same change, and
|
|
212
|
+
* `publisher-agreement-version.spec.ts` fails a bump that did not. The first
|
|
213
|
+
* published version has no entry, because nothing preceded it that anyone
|
|
214
|
+
* accepted.
|
|
215
|
+
*/
|
|
216
|
+
export declare const PUBLISHER_AGREEMENT_CHANGES: readonly {
|
|
217
|
+
version: string;
|
|
218
|
+
changes: readonly string[];
|
|
219
|
+
}[];
|
|
220
|
+
/**
|
|
221
|
+
* The changes an org has not yet accepted, oldest first.
|
|
222
|
+
*
|
|
223
|
+
* Versions are dated strings (`YYYY-MM-DD.n`), so they order as text. An
|
|
224
|
+
* acceptance of a version this list does not name — one older than the first
|
|
225
|
+
* entry — gets every entry, which is the honest answer: all of it is new to
|
|
226
|
+
* them. No acceptance at all gets none, because a first acceptance is of the
|
|
227
|
+
* whole document, not a set of edits.
|
|
228
|
+
*/
|
|
229
|
+
export declare function publisherAgreementChangesSince(acceptedVersion: string | null | undefined, currentVersion?: string): readonly {
|
|
230
|
+
version: string;
|
|
231
|
+
changes: readonly string[];
|
|
232
|
+
}[];
|
|
198
233
|
/**
|
|
199
234
|
* A recorded acceptance: which version, of which exact bytes, by whom, when.
|
|
200
235
|
*
|
|
@@ -154,7 +154,8 @@ export const PUBLISHER_AGREEMENT_TITLE = 'Marketplace Publisher Agreement';
|
|
|
154
154
|
*
|
|
155
155
|
* TO PUBLISH A CHANGE: publish the besigner edit first, confirm the live page
|
|
156
156
|
* serves it, capture a new snapshot under the new version's directory, then
|
|
157
|
-
* bump `PUBLISHER_AGREEMENT_VERSION` and these two values together
|
|
157
|
+
* bump `PUBLISHER_AGREEMENT_VERSION` and these two values together, and add
|
|
158
|
+
* the version's entry to `PUBLISHER_AGREEMENT_CHANGES` below. Never
|
|
158
159
|
* hand-write a snapshot: it is evidence of what a publisher was shown, and a
|
|
159
160
|
* snapshot of unpublished text is a false record.
|
|
160
161
|
*/ export const PUBLISHER_AGREEMENT_SHA256 = '2df6e940f037b4630e53c8f63bad8604ef4bf370374b89afcd4ea7f9fe8e8d63';
|
|
@@ -238,6 +239,52 @@ export const PUBLISHER_AGREEMENT_TITLE = 'Marketplace Publisher Agreement';
|
|
|
238
239
|
detail: 'Including infringement claims and disputes with the organizations that install it.'
|
|
239
240
|
}
|
|
240
241
|
];
|
|
242
|
+
/**
|
|
243
|
+
* What each version changed from the one before it, in the words a publisher
|
|
244
|
+
* re-accepting needs (AGL-3407).
|
|
245
|
+
*
|
|
246
|
+
* An org that accepted an older version is asked again, and "the terms
|
|
247
|
+
* changed" with nothing more is a request to re-read thirteen sections to
|
|
248
|
+
* find two edited clauses. Each entry names the sections that moved, read off
|
|
249
|
+
* the archived snapshots beside this module (`legal/publisher-agreement/`) —
|
|
250
|
+
* never written from memory of the edit, for the same reason a snapshot is
|
|
251
|
+
* never hand-written.
|
|
252
|
+
*
|
|
253
|
+
* Oldest first. The last entry is always {@link PUBLISHER_AGREEMENT_VERSION}:
|
|
254
|
+
* a bump adds its entry in the same change, and
|
|
255
|
+
* `publisher-agreement-version.spec.ts` fails a bump that did not. The first
|
|
256
|
+
* published version has no entry, because nothing preceded it that anyone
|
|
257
|
+
* accepted.
|
|
258
|
+
*/ export const PUBLISHER_AGREEMENT_CHANGES = [
|
|
259
|
+
{
|
|
260
|
+
version: '2026-08-18.1',
|
|
261
|
+
changes: [
|
|
262
|
+
'§1 names the marketplace and the End User License Agreement by their current names.',
|
|
263
|
+
'§3(h), new: listing conduct — what an Artifact and its listing may not claim about the platform, its plans or its prices.',
|
|
264
|
+
'§8.6, new: a paid Artifact, tier or license key must be sold through the Marketplace.',
|
|
265
|
+
'§13.6, new: how you may use the platform’s names and marks.'
|
|
266
|
+
]
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
version: '2026-08-24.1',
|
|
270
|
+
changes: [
|
|
271
|
+
'§13.3: venue moves to Travis County, Texas, and governing law now cites Terms of Service Section 18.6.'
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
/**
|
|
276
|
+
* The changes an org has not yet accepted, oldest first.
|
|
277
|
+
*
|
|
278
|
+
* Versions are dated strings (`YYYY-MM-DD.n`), so they order as text. An
|
|
279
|
+
* acceptance of a version this list does not name — one older than the first
|
|
280
|
+
* entry — gets every entry, which is the honest answer: all of it is new to
|
|
281
|
+
* them. No acceptance at all gets none, because a first acceptance is of the
|
|
282
|
+
* whole document, not a set of edits.
|
|
283
|
+
*/ export function publisherAgreementChangesSince(acceptedVersion, currentVersion = PUBLISHER_AGREEMENT_VERSION) {
|
|
284
|
+
const accepted = typeof acceptedVersion === 'string' ? acceptedVersion.trim() : '';
|
|
285
|
+
if (!accepted) return [];
|
|
286
|
+
return PUBLISHER_AGREEMENT_CHANGES.filter((entry)=>entry.version > accepted && entry.version <= currentVersion);
|
|
287
|
+
}
|
|
241
288
|
export function publisherAgreementState(acceptance, currentVersion = PUBLISHER_AGREEMENT_VERSION) {
|
|
242
289
|
const accepted = acceptance == null ? void 0 : acceptance.version;
|
|
243
290
|
if (typeof accepted !== 'string' || !accepted.trim()) return 'none';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/publisher-agreement.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 marketplace publisher agreement, and whether an org has accepted the\n * current one (AGL-1077).\n *\n * Deliberately NOT the per-version attestation (AGL-969). An attestation is\n * a statement about *specific bytes* and re-asks on every republish, because\n * the bytes changed. This is about the *relationship*: what we may do with a\n * listing, what the publisher warrants, indemnity, takedown, payouts, what\n * survives termination. It is accepted once, by an org, and re-asked only\n * when we change it.\n *\n * Shared like the attestation model is, and for the same reason: the publish\n * route enforces it, the profile page collects it, and the review page shows\n * staff which terms a publisher is actually under. Three copies of a version\n * string would drift, and a drifted version is one that either re-asks\n * forever or never re-asks at all.\n */\n\nimport { LEGAL_ORIGIN, isPublishedLegalUrl } from './published-legal-pages'\n\n/**\n * The version of the agreement currently in force.\n *\n * Matches the `Agreement version` header of the document itself, which is now\n * besigner content at {@link PUBLISHER_AGREEMENT_URL} rather than a repo file\n * (same practice as the Terms of Service). Bump this **only** when the\n * substance changes in a way a publisher should re-read — a typo fix is not a\n * bump. Bumping it stops every publisher at their next publish until they\n * accept again.\n *\n * Moved off `2026-07-28.1` on 2026-08-14 (AGL-1674). That string named the\n * July draft, which was never published and whose §8 said the publisher is the\n * seller of record. The published text says the opposite — Aglyn processes the\n * sale and remits transaction tax — so it is a different document in\n * substance, not a reissue, and re-asking is the point rather than a cost.\n *\n * Moved to `2026-08-18.1` on 2026-08-18 (AGL-1840 + AGL-1661). Two body-text\n * changes: §1 now says \"the Aglyn marketplace\" (the AGL-975 rename, and the\n * reason this snapshot must NOT need the AGL-975 naming-guard exemption that\n * the `2026-08-14.1` one still carries), and §1's cross-reference to\n * the \"End User Licence Agreement\"\n * becomes \"License\" to match the document that agreement actually is, which\n * was renamed on the same day.\n *\n * ⚠️ Read against the \"a typo fix is not a bump\" rule above, both edits are\n * closer to a typo than to substance, and the tension is real. The bump was\n * taken anyway on the decision (2026-08-18), for a reason the rule does not\n * cover: the served page changed, so keeping `2026-08-14.1` would leave the\n * pin below asserting a hash for text nobody is shown any more — a FALSE\n * RECORD, which is the exact failure the pin exists to detect. The choice is\n * therefore between a bump and a broken pin, not between a bump and nothing.\n *\n * ## 2026-08-22 — the page changed and this string DELIBERATELY did not\n *\n * Three clauses were added on 2026-08-22 (§3(h) listing conduct, §8.6 sales\n * through the Marketplace, §13.6 marks) and the pin below moved with them, but\n * `2026-08-18.1` stayed. That is not the third option the paragraph above says\n * does not exist — it is the same choice with one input changed. A bump exists\n * to re-ask publishers who accepted the OLD text; production holds exactly one\n * acceptance record and it is the own, so there is nobody to re-ask and the\n * bump would buy nothing. the decision, 2026-08-22.\n *\n * The pin still had to move, for precisely the reason given above: the served\n * bytes changed, so leaving the old hash here would assert a document nobody\n * is shown. Version unchanged + pin re-captured is coherent ONLY while the\n * acceptance set is empty. The first real publisher acceptance ends that, and\n * from then on any body-text change costs a bump — see the same rule in\n * `apps/console/constants/legal-documents.ts`, where the clickwrap snapshots\n * collapsed back to `v1` on the identical argument.\n *\n * ## 2026-08-24 — `2026-08-24.1`, and why this one CANNOT be folded (AGL-1956)\n *\n * §13.3 changed twice in one sentence. The venue moved from Williamson County\n * to TRAVIS County (decided) — Williamson entered from the former\n * Jarrell residential address, while every published Aglyn address is 5900\n * Balcones Dr STE 100, Austin, TX 78731, which is Travis. And the clause cited\n * \"Terms of Service Section 19\" for governing law and venue when those live at\n * §18.6; §19 is General. A wrong cross-reference in a forum-selection clause is\n * the kind of defect that gets argued about, so it was corrected in the same\n * pass.\n *\n * Unlike 2026-08-22, this one takes the bump rather than only re-pinning, and\n * the deciding reason is mechanical rather than editorial: THIS VERSION STRING\n * IS PUBLISHED IN THE DOCUMENT. The page at {@link PUBLISHER_AGREEMENT_URL}\n * serves `Agreement version: 2026-08-24.1` in its own second line, so the\n * capture below contains it, and `publisher-agreement-version.spec.ts` asserts\n * the snapshot contains `Agreement version: ${PUBLISHER_AGREEMENT_VERSION}`.\n * Holding the constant at `2026-08-18.1` against that text would need the page\n * republished to say so, and would otherwise record a publisher as having\n * accepted `2026-08-18.1` while naming bytes that call themselves\n * `2026-08-24.1`. That is a false record of exactly the shape the pin exists\n * to prevent.\n *\n * ⚠️ THIS IS THE ONE PLACE THE CLICKWRAP ARGUMENT DOES NOT REACH. The signup\n * clickwrap folded its 2026-08-24 corrections into `v1` and did not bump\n * (`apps/console/constants/legal-documents.ts`) — decided: no v2\n * before launch. That works there because `v1` is a REPO-SIDE label: the\n * published Terms carry a \"Last updated\" date and no version string at all, so\n * the label can stay still while the text moves. Here the label is part of the\n * document a publisher reads. The two schemes are not the same kind of thing\n * and must not be reasoned about as one — that difference is the whole content\n * of this paragraph.\n *\n * On the merits it would have been a bump anyway: a forum-selection clause is\n * substance, not a typo — it names the court a publisher would have to travel\n * to. The 2026-08-22 cost argument is unchanged and still cheap, production\n * holding exactly one acceptance record which is the own.\n *\n * §8.1 and §8.3 are UNTOUCHED and were already correct: they have said Aglyn is\n * merchant of record and determines, collects and remits transaction tax since\n * 2026-08-14. The Terms of Service moved TO them in this pass (new ToS §10.7),\n * not the other way around.\n */\nexport const PUBLISHER_AGREEMENT_VERSION = '2026-08-24.1'\n\nexport const PUBLISHER_AGREEMENT_TITLE = 'Marketplace Publisher Agreement'\n\n/**\n * The snapshot pin for the version above (AGL-1678).\n *\n * A version string is a label we control and can reuse, so on its own it\n * proves nothing about what a publisher actually read. These two values pin\n * the archived snapshot at\n * `legal/publisher-agreement/{PUBLISHER_AGREEMENT_VERSION}/marketplace-publisher-agreement.txt`\n * (beside this module), captured FROM THE LIVE PAGE after publication — the\n * same publication-first machinery as the signup clickwrap\n * (`apps/console/constants/legal-documents.ts`, AGL-1497), and deliberately a\n * PARALLEL manifest rather than an entry in `LEGAL_DOCUMENTS`: ToS §19.1 makes\n * that set the entire signup agreement, and the publisher agreement is\n * deliberately not part of it. Mixing them would widen what a signup\n * acceptance covers.\n *\n * Both values go onto every acceptance record, so a record is self-contained\n * evidence of content and later tampering with the archive is detectable\n * rather than invisible. `publisher-agreement-version.spec.ts` re-hashes the\n * snapshot, so one version string can never come to mean two different\n * documents.\n *\n * Capture method (proven byte-for-byte against the clickwrap v4 hashes before\n * this snapshot was taken): fetch the live page, walk its HTML text nodes\n * skipping script/style, trim each, drop empties, take the document's content\n * block (first line `Last updated: …` through its closing `© …` line,\n * excluding site chrome and the table of contents), join with `\\n`, and end\n * with a trailing newline. UTF-8 throughout.\n *\n * TO PUBLISH A CHANGE: publish the besigner edit first, confirm the live page\n * serves it, capture a new snapshot under the new version's directory, then\n * bump `PUBLISHER_AGREEMENT_VERSION` and these two values together. Never\n * hand-write a snapshot: it is evidence of what a publisher was shown, and a\n * snapshot of unpublished text is a false record.\n */\nexport const PUBLISHER_AGREEMENT_SHA256 =\n '2df6e940f037b4630e53c8f63bad8604ef4bf370374b89afcd4ea7f9fe8e8d63'\n\n/** Byte length of the same snapshot — a cheap second check on the content. */\nexport const PUBLISHER_AGREEMENT_BYTES = 13730\n\n/**\n * Canonical document, on the operator's legal origin beside the other terms.\n *\n * Built from `LEGAL_ORIGIN` (AGL-2196), which this module already depends on:\n * `publisherAgreementIsPublished` below calls `isPublishedLegalUrl`, and that\n * accepts ONLY `LEGAL_ORIGIN`. So the gate was operator-aware while the link\n * it gates was a bare `aglyn.com` literal — meaning on a self-host install\n * with `NEXT_PUBLIC_OPERATOR_LEGAL_ORIGIN` set, the gate rejected the very URL\n * this file handed it, and every publisher was blocked from accepting. One\n * origin for both closes that, and leaves Aglyn's own URL byte-identical.\n */\nexport const PUBLISHER_AGREEMENT_URL = `${LEGAL_ORIGIN}/legal/marketplace-publisher-agreement`\n\n/**\n * Whether that document can actually be read (AGL-1660).\n *\n * It could not, from the day this gate shipped until the day this check did:\n * the URL above 404ed, and the console still rendered it as \"Read the full\n * Marketplace Publisher Agreement\" above an Accept button. An acceptance\n * collected that way is a signature on a blank page — the same\n * publication-first principle the clickwrap snapshots are built on, failing in\n * the one place the text was never published at all.\n *\n * Derived, not a second flag: it reads `PUBLISHED_LEGAL_PATHS`, so the ONE\n * edit that adds the path when the page goes live is what re-opens acceptance\n * everywhere. There is no boolean to forget to flip, and no way for the link\n * and the gate to disagree.\n */\nexport function publisherAgreementIsPublished(\n url: string = PUBLISHER_AGREEMENT_URL,\n): boolean {\n return isPublishedLegalUrl(url)\n}\n\n/**\n * What a publisher is told while the document has no page.\n *\n * Deliberately not \"try again later\" — it names the reason, because the fix is\n * ours and the publisher can do nothing about it. It must never read as though\n * accepting is something they merely have not got round to.\n */\nexport const PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE =\n `The ${PUBLISHER_AGREEMENT_TITLE} is not published yet, so there is ` +\n 'nothing here for you to read — and we will not ask you to agree to a ' +\n 'document we have not shown you. Acceptance is paused until it is ' +\n 'available, which also means marketplace publishing is paused. Nothing is ' +\n 'required of you in the meantime.'\n\n/**\n * What the agreement actually commits a publisher to.\n *\n * Shown above the accept control. A link alone is the pattern that produces\n * agreements nobody has read; these are the terms most likely to surprise\n * someone later, in the words they would need at that moment. They summarise\n * the document — they are not it, and the copy says so.\n */\nexport const PUBLISHER_AGREEMENT_POINTS: readonly {\n id: string\n label: string\n detail: string\n}[] = [\n {\n id: 'hosting-license',\n label: 'We may host, verify and distribute what you publish',\n detail:\n 'Including keeping an immutable copy of every version, inspecting its code, and showing your listing content in the marketplace and console.',\n },\n {\n id: 'warranties',\n label: 'You warrant each version as you publish it',\n detail:\n 'That you have the right to publish it, that its license is accurate and compatible, that it carries no malware or hidden telemetry, and that its documentation describes what it does with data.',\n },\n {\n id: 'immutable',\n label: 'You cannot recall code that is already installed',\n detail:\n 'Installs pin an exact version and hash. A mistake ships until an installer chooses to upgrade — publishing a new version is the only remedy you have.',\n },\n {\n id: 'revocation',\n label: 'We can disable a version everywhere, without notice',\n detail:\n 'If a version looks like a security, legal or privacy risk we stop it on every site running it, and tell you afterwards. Operating a platform that executes other people’s code requires this.',\n },\n {\n id: 'review',\n label: 'Review is a safety screen, not an endorsement',\n detail:\n 'Approval is not an audit and not a guarantee your plugin works. We may decline, delist or unsign anything, for reasons that may have nothing to do with your code.',\n },\n {\n id: 'payouts',\n label: 'On paid listings we process the sale and take a fee',\n detail:\n 'We process the purchase and pay your share into your own Stripe Connect account, less a platform fee that may change. We collect and remit sales tax on Marketplace sales; refunds, chargebacks and support on your Artifacts are yours.',\n },\n {\n id: 'indemnity',\n label: 'You indemnify us for claims arising from what you publish',\n detail:\n 'Including infringement claims and disputes with the organizations that install it.',\n },\n]\n\n/**\n * A recorded acceptance: which version, of which exact bytes, by whom, when.\n *\n * `sha256`/`bytes` (AGL-1678) pin the acceptance to the archived snapshot of\n * the text the publisher was shown. Optional because records written before\n * the pin existed carry only the version string — absence means \"accepted\n * before 2026-08-17\", not \"accepted nothing\".\n */\nexport interface PublisherAgreementAcceptance {\n version?: string\n sha256?: string\n bytes?: number\n acceptedBy?: string\n acceptedAt?: unknown\n}\n\n/**\n * Where an org stands with the agreement.\n *\n * `none` and `outdated` are deliberately different answers. \"You have never\n * agreed to anything\" and \"the terms changed since you agreed\" are different\n * situations with different copy, and collapsing them into a boolean is how\n * a re-acceptance ends up reading like a first-time setup step.\n */\nexport type PublisherAgreementState = 'none' | 'outdated' | 'current'\n\nexport function publisherAgreementState(\n acceptance: PublisherAgreementAcceptance | null | undefined,\n currentVersion: string = PUBLISHER_AGREEMENT_VERSION,\n): PublisherAgreementState {\n const accepted = acceptance?.version\n if (typeof accepted !== 'string' || !accepted.trim()) return 'none'\n return accepted === currentVersion ? 'current' : 'outdated'\n}\n\n/**\n * Whether a publish may proceed.\n *\n * An older acceptance is NOT carried forward. Whoever publishes next reads\n * the changed agreement and accepts it, or does not publish — silently\n * inheriting an acceptance across a rewrite is precisely what makes the\n * record worthless.\n */\nexport function hasCurrentPublisherAgreement(\n acceptance: PublisherAgreementAcceptance | null | undefined,\n currentVersion: string = PUBLISHER_AGREEMENT_VERSION,\n): boolean {\n return publisherAgreementState(acceptance, currentVersion) === 'current'\n}\n\n/**\n * What the acceptance UI may show (AGL-1660).\n *\n * The gate lives HERE rather than in the panel, for the reason every \"check it\n * in the component\" control eventually fails: the component is not the only\n * caller. The accept route consults the same function, so a client that skips\n * the UI entirely meets the same answer.\n *\n * `documentUrl` is null, not the URL, when the document is unpublished — the\n * console reads the link target from this, so an unreadable document is\n * structurally unable to reach an href.\n */\nexport interface PublisherAgreementPresentation {\n /** The document to link, or null when there is no page to link to. */\n documentUrl: string | null\n /** Whether the accept control may be offered at all. */\n canAccept: boolean\n /** Why acceptance is unavailable, or null when it is available. */\n unavailableNotice: string | null\n}\n\nexport function publisherAgreementPresentation(\n state: PublisherAgreementState,\n published: boolean = publisherAgreementIsPublished(),\n): PublisherAgreementPresentation {\n return {\n documentUrl: published ? PUBLISHER_AGREEMENT_URL : null,\n // Already current is not \"cannot accept because unpublished\" — it is\n // nothing left to do, and the panel says so separately.\n canAccept: published && state !== 'current',\n unavailableNotice: published ? null : PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE,\n }\n}\n\n/** The refusal a publisher sees, saying which of the two problems it is. */\nexport function publisherAgreementRefusal(\n state: PublisherAgreementState,\n published: boolean = publisherAgreementIsPublished(),\n): string {\n // Sending someone to a control we have disabled is worse than saying\n // nothing: they go, find no button, and conclude the console is broken.\n if (!published) {\n return (\n `The ${PUBLISHER_AGREEMENT_TITLE} is not published yet, so it cannot ` +\n 'be accepted and publishing is paused for everyone. This is on us, not ' +\n 'on your organization.'\n )\n }\n return state === 'outdated'\n ? `The ${PUBLISHER_AGREEMENT_TITLE} has changed since your organization ` +\n 'accepted it. Review and accept the current version in Marketplace → ' +\n 'Publisher Profile, then publish.'\n : `Your organization has not accepted the ${PUBLISHER_AGREEMENT_TITLE}. ` +\n 'Accept it in Marketplace → Publisher Profile, then publish.'\n}\n"],"names":["LEGAL_ORIGIN","isPublishedLegalUrl","PUBLISHER_AGREEMENT_VERSION","PUBLISHER_AGREEMENT_TITLE","PUBLISHER_AGREEMENT_SHA256","PUBLISHER_AGREEMENT_BYTES","PUBLISHER_AGREEMENT_URL","publisherAgreementIsPublished","url","PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE","PUBLISHER_AGREEMENT_POINTS","id","label","detail","publisherAgreementState","acceptance","currentVersion","accepted","version","trim","hasCurrentPublisherAgreement","publisherAgreementPresentation","state","published","documentUrl","canAccept","unavailableNotice","publisherAgreementRefusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GAED,SAASA,YAAY,EAAEC,mBAAmB,QAAQ,6BAAyB;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FC,GACD,OAAO,MAAMC,8BAA8B,eAAc;AAEzD,OAAO,MAAMC,4BAA4B,kCAAiC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCC,GACD,OAAO,MAAMC,6BACX,mEAAkE;AAEpE,4EAA4E,GAC5E,OAAO,MAAMC,4BAA4B,MAAK;AAE9C;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,0BAA0B,GAAGN,aAAa,sCAAsC,CAAC,CAAA;AAE9F;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASO,8BACdC,MAAcF,uBAAuB;IAErC,OAAOL,oBAAoBO;AAC7B;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,yCACX,CAAC,IAAI,EAAEN,0BAA0B,mCAAmC,CAAC,GACrE,0EACA,sEACA,8EACA,mCAAkC;AAEpC;;;;;;;CAOC,GACD,OAAO,MAAMO,6BAIP;IACJ;QACEC,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;CACD,CAAA;AA4BD,OAAO,SAASC,wBACdC,UAA2D,EAC3DC,iBAAyBd,2BAA2B;IAEpD,MAAMe,WAAWF,8BAAAA,WAAYG,OAAO;IACpC,IAAI,OAAOD,aAAa,YAAY,CAACA,SAASE,IAAI,IAAI,OAAO;IAC7D,OAAOF,aAAaD,iBAAiB,YAAY;AACnD;AAEA;;;;;;;CAOC,GACD,OAAO,SAASI,6BACdL,UAA2D,EAC3DC,iBAAyBd,2BAA2B;IAEpD,OAAOY,wBAAwBC,YAAYC,oBAAoB;AACjE;AAuBA,OAAO,SAASK,+BACdC,KAA8B,EAC9BC,YAAqBhB,+BAA+B;IAEpD,OAAO;QACLiB,aAAaD,YAAYjB,0BAA0B;QACnD,qEAAqE;QACrE,wDAAwD;QACxDmB,WAAWF,aAAaD,UAAU;QAClCI,mBAAmBH,YAAY,OAAOd;IACxC;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASkB,0BACdL,KAA8B,EAC9BC,YAAqBhB,+BAA+B;IAEpD,qEAAqE;IACrE,wEAAwE;IACxE,IAAI,CAACgB,WAAW;QACd,OACE,CAAC,IAAI,EAAEpB,0BAA0B,oCAAoC,CAAC,GACtE,2EACA;IAEJ;IACA,OAAOmB,UAAU,aACb,CAAC,IAAI,EAAEnB,0BAA0B,qCAAqC,CAAC,GACrE,yEACA,qCACF,CAAC,uCAAuC,EAAEA,0BAA0B,EAAE,CAAC,GACrE;AACR"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/publisher-agreement.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 marketplace publisher agreement, and whether an org has accepted the\n * current one (AGL-1077).\n *\n * Deliberately NOT the per-version attestation (AGL-969). An attestation is\n * a statement about *specific bytes* and re-asks on every republish, because\n * the bytes changed. This is about the *relationship*: what we may do with a\n * listing, what the publisher warrants, indemnity, takedown, payouts, what\n * survives termination. It is accepted once, by an org, and re-asked only\n * when we change it.\n *\n * Shared like the attestation model is, and for the same reason: the publish\n * route enforces it, the profile page collects it, and the review page shows\n * staff which terms a publisher is actually under. Three copies of a version\n * string would drift, and a drifted version is one that either re-asks\n * forever or never re-asks at all.\n */\n\nimport { LEGAL_ORIGIN, isPublishedLegalUrl } from './published-legal-pages'\n\n/**\n * The version of the agreement currently in force.\n *\n * Matches the `Agreement version` header of the document itself, which is now\n * besigner content at {@link PUBLISHER_AGREEMENT_URL} rather than a repo file\n * (same practice as the Terms of Service). Bump this **only** when the\n * substance changes in a way a publisher should re-read — a typo fix is not a\n * bump. Bumping it stops every publisher at their next publish until they\n * accept again.\n *\n * Moved off `2026-07-28.1` on 2026-08-14 (AGL-1674). That string named the\n * July draft, which was never published and whose §8 said the publisher is the\n * seller of record. The published text says the opposite — Aglyn processes the\n * sale and remits transaction tax — so it is a different document in\n * substance, not a reissue, and re-asking is the point rather than a cost.\n *\n * Moved to `2026-08-18.1` on 2026-08-18 (AGL-1840 + AGL-1661). Two body-text\n * changes: §1 now says \"the Aglyn marketplace\" (the AGL-975 rename, and the\n * reason this snapshot must NOT need the AGL-975 naming-guard exemption that\n * the `2026-08-14.1` one still carries), and §1's cross-reference to\n * the \"End User Licence Agreement\"\n * becomes \"License\" to match the document that agreement actually is, which\n * was renamed on the same day.\n *\n * ⚠️ Read against the \"a typo fix is not a bump\" rule above, both edits are\n * closer to a typo than to substance, and the tension is real. The bump was\n * taken anyway on the decision (2026-08-18), for a reason the rule does not\n * cover: the served page changed, so keeping `2026-08-14.1` would leave the\n * pin below asserting a hash for text nobody is shown any more — a FALSE\n * RECORD, which is the exact failure the pin exists to detect. The choice is\n * therefore between a bump and a broken pin, not between a bump and nothing.\n *\n * ## 2026-08-22 — the page changed and this string DELIBERATELY did not\n *\n * Three clauses were added on 2026-08-22 (§3(h) listing conduct, §8.6 sales\n * through the Marketplace, §13.6 marks) and the pin below moved with them, but\n * `2026-08-18.1` stayed. That is not the third option the paragraph above says\n * does not exist — it is the same choice with one input changed. A bump exists\n * to re-ask publishers who accepted the OLD text; production holds exactly one\n * acceptance record and it is the own, so there is nobody to re-ask and the\n * bump would buy nothing. the decision, 2026-08-22.\n *\n * The pin still had to move, for precisely the reason given above: the served\n * bytes changed, so leaving the old hash here would assert a document nobody\n * is shown. Version unchanged + pin re-captured is coherent ONLY while the\n * acceptance set is empty. The first real publisher acceptance ends that, and\n * from then on any body-text change costs a bump — see the same rule in\n * `apps/console/constants/legal-documents.ts`, where the clickwrap snapshots\n * collapsed back to `v1` on the identical argument.\n *\n * ## 2026-08-24 — `2026-08-24.1`, and why this one CANNOT be folded (AGL-1956)\n *\n * §13.3 changed twice in one sentence. The venue moved from Williamson County\n * to TRAVIS County (decided) — Williamson entered from the former\n * Jarrell residential address, while every published Aglyn address is 5900\n * Balcones Dr STE 100, Austin, TX 78731, which is Travis. And the clause cited\n * \"Terms of Service Section 19\" for governing law and venue when those live at\n * §18.6; §19 is General. A wrong cross-reference in a forum-selection clause is\n * the kind of defect that gets argued about, so it was corrected in the same\n * pass.\n *\n * Unlike 2026-08-22, this one takes the bump rather than only re-pinning, and\n * the deciding reason is mechanical rather than editorial: THIS VERSION STRING\n * IS PUBLISHED IN THE DOCUMENT. The page at {@link PUBLISHER_AGREEMENT_URL}\n * serves `Agreement version: 2026-08-24.1` in its own second line, so the\n * capture below contains it, and `publisher-agreement-version.spec.ts` asserts\n * the snapshot contains `Agreement version: ${PUBLISHER_AGREEMENT_VERSION}`.\n * Holding the constant at `2026-08-18.1` against that text would need the page\n * republished to say so, and would otherwise record a publisher as having\n * accepted `2026-08-18.1` while naming bytes that call themselves\n * `2026-08-24.1`. That is a false record of exactly the shape the pin exists\n * to prevent.\n *\n * ⚠️ THIS IS THE ONE PLACE THE CLICKWRAP ARGUMENT DOES NOT REACH. The signup\n * clickwrap folded its 2026-08-24 corrections into `v1` and did not bump\n * (`apps/console/constants/legal-documents.ts`) — decided: no v2\n * before launch. That works there because `v1` is a REPO-SIDE label: the\n * published Terms carry a \"Last updated\" date and no version string at all, so\n * the label can stay still while the text moves. Here the label is part of the\n * document a publisher reads. The two schemes are not the same kind of thing\n * and must not be reasoned about as one — that difference is the whole content\n * of this paragraph.\n *\n * On the merits it would have been a bump anyway: a forum-selection clause is\n * substance, not a typo — it names the court a publisher would have to travel\n * to. The 2026-08-22 cost argument is unchanged and still cheap, production\n * holding exactly one acceptance record which is the own.\n *\n * §8.1 and §8.3 are UNTOUCHED and were already correct: they have said Aglyn is\n * merchant of record and determines, collects and remits transaction tax since\n * 2026-08-14. The Terms of Service moved TO them in this pass (new ToS §10.7),\n * not the other way around.\n */\nexport const PUBLISHER_AGREEMENT_VERSION = '2026-08-24.1'\n\nexport const PUBLISHER_AGREEMENT_TITLE = 'Marketplace Publisher Agreement'\n\n/**\n * The snapshot pin for the version above (AGL-1678).\n *\n * A version string is a label we control and can reuse, so on its own it\n * proves nothing about what a publisher actually read. These two values pin\n * the archived snapshot at\n * `legal/publisher-agreement/{PUBLISHER_AGREEMENT_VERSION}/marketplace-publisher-agreement.txt`\n * (beside this module), captured FROM THE LIVE PAGE after publication — the\n * same publication-first machinery as the signup clickwrap\n * (`apps/console/constants/legal-documents.ts`, AGL-1497), and deliberately a\n * PARALLEL manifest rather than an entry in `LEGAL_DOCUMENTS`: ToS §19.1 makes\n * that set the entire signup agreement, and the publisher agreement is\n * deliberately not part of it. Mixing them would widen what a signup\n * acceptance covers.\n *\n * Both values go onto every acceptance record, so a record is self-contained\n * evidence of content and later tampering with the archive is detectable\n * rather than invisible. `publisher-agreement-version.spec.ts` re-hashes the\n * snapshot, so one version string can never come to mean two different\n * documents.\n *\n * Capture method (proven byte-for-byte against the clickwrap v4 hashes before\n * this snapshot was taken): fetch the live page, walk its HTML text nodes\n * skipping script/style, trim each, drop empties, take the document's content\n * block (first line `Last updated: …` through its closing `© …` line,\n * excluding site chrome and the table of contents), join with `\\n`, and end\n * with a trailing newline. UTF-8 throughout.\n *\n * TO PUBLISH A CHANGE: publish the besigner edit first, confirm the live page\n * serves it, capture a new snapshot under the new version's directory, then\n * bump `PUBLISHER_AGREEMENT_VERSION` and these two values together, and add\n * the version's entry to `PUBLISHER_AGREEMENT_CHANGES` below. Never\n * hand-write a snapshot: it is evidence of what a publisher was shown, and a\n * snapshot of unpublished text is a false record.\n */\nexport const PUBLISHER_AGREEMENT_SHA256 =\n '2df6e940f037b4630e53c8f63bad8604ef4bf370374b89afcd4ea7f9fe8e8d63'\n\n/** Byte length of the same snapshot — a cheap second check on the content. */\nexport const PUBLISHER_AGREEMENT_BYTES = 13730\n\n/**\n * Canonical document, on the operator's legal origin beside the other terms.\n *\n * Built from `LEGAL_ORIGIN` (AGL-2196), which this module already depends on:\n * `publisherAgreementIsPublished` below calls `isPublishedLegalUrl`, and that\n * accepts ONLY `LEGAL_ORIGIN`. So the gate was operator-aware while the link\n * it gates was a bare `aglyn.com` literal — meaning on a self-host install\n * with `NEXT_PUBLIC_OPERATOR_LEGAL_ORIGIN` set, the gate rejected the very URL\n * this file handed it, and every publisher was blocked from accepting. One\n * origin for both closes that, and leaves Aglyn's own URL byte-identical.\n */\nexport const PUBLISHER_AGREEMENT_URL = `${LEGAL_ORIGIN}/legal/marketplace-publisher-agreement`\n\n/**\n * Whether that document can actually be read (AGL-1660).\n *\n * It could not, from the day this gate shipped until the day this check did:\n * the URL above 404ed, and the console still rendered it as \"Read the full\n * Marketplace Publisher Agreement\" above an Accept button. An acceptance\n * collected that way is a signature on a blank page — the same\n * publication-first principle the clickwrap snapshots are built on, failing in\n * the one place the text was never published at all.\n *\n * Derived, not a second flag: it reads `PUBLISHED_LEGAL_PATHS`, so the ONE\n * edit that adds the path when the page goes live is what re-opens acceptance\n * everywhere. There is no boolean to forget to flip, and no way for the link\n * and the gate to disagree.\n */\nexport function publisherAgreementIsPublished(\n url: string = PUBLISHER_AGREEMENT_URL,\n): boolean {\n return isPublishedLegalUrl(url)\n}\n\n/**\n * What a publisher is told while the document has no page.\n *\n * Deliberately not \"try again later\" — it names the reason, because the fix is\n * ours and the publisher can do nothing about it. It must never read as though\n * accepting is something they merely have not got round to.\n */\nexport const PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE =\n `The ${PUBLISHER_AGREEMENT_TITLE} is not published yet, so there is ` +\n 'nothing here for you to read — and we will not ask you to agree to a ' +\n 'document we have not shown you. Acceptance is paused until it is ' +\n 'available, which also means marketplace publishing is paused. Nothing is ' +\n 'required of you in the meantime.'\n\n/**\n * What the agreement actually commits a publisher to.\n *\n * Shown above the accept control. A link alone is the pattern that produces\n * agreements nobody has read; these are the terms most likely to surprise\n * someone later, in the words they would need at that moment. They summarise\n * the document — they are not it, and the copy says so.\n */\nexport const PUBLISHER_AGREEMENT_POINTS: readonly {\n id: string\n label: string\n detail: string\n}[] = [\n {\n id: 'hosting-license',\n label: 'We may host, verify and distribute what you publish',\n detail:\n 'Including keeping an immutable copy of every version, inspecting its code, and showing your listing content in the marketplace and console.',\n },\n {\n id: 'warranties',\n label: 'You warrant each version as you publish it',\n detail:\n 'That you have the right to publish it, that its license is accurate and compatible, that it carries no malware or hidden telemetry, and that its documentation describes what it does with data.',\n },\n {\n id: 'immutable',\n label: 'You cannot recall code that is already installed',\n detail:\n 'Installs pin an exact version and hash. A mistake ships until an installer chooses to upgrade — publishing a new version is the only remedy you have.',\n },\n {\n id: 'revocation',\n label: 'We can disable a version everywhere, without notice',\n detail:\n 'If a version looks like a security, legal or privacy risk we stop it on every site running it, and tell you afterwards. Operating a platform that executes other people’s code requires this.',\n },\n {\n id: 'review',\n label: 'Review is a safety screen, not an endorsement',\n detail:\n 'Approval is not an audit and not a guarantee your plugin works. We may decline, delist or unsign anything, for reasons that may have nothing to do with your code.',\n },\n {\n id: 'payouts',\n label: 'On paid listings we process the sale and take a fee',\n detail:\n 'We process the purchase and pay your share into your own Stripe Connect account, less a platform fee that may change. We collect and remit sales tax on Marketplace sales; refunds, chargebacks and support on your Artifacts are yours.',\n },\n {\n id: 'indemnity',\n label: 'You indemnify us for claims arising from what you publish',\n detail:\n 'Including infringement claims and disputes with the organizations that install it.',\n },\n]\n\n/**\n * What each version changed from the one before it, in the words a publisher\n * re-accepting needs (AGL-3407).\n *\n * An org that accepted an older version is asked again, and \"the terms\n * changed\" with nothing more is a request to re-read thirteen sections to\n * find two edited clauses. Each entry names the sections that moved, read off\n * the archived snapshots beside this module (`legal/publisher-agreement/`) —\n * never written from memory of the edit, for the same reason a snapshot is\n * never hand-written.\n *\n * Oldest first. The last entry is always {@link PUBLISHER_AGREEMENT_VERSION}:\n * a bump adds its entry in the same change, and\n * `publisher-agreement-version.spec.ts` fails a bump that did not. The first\n * published version has no entry, because nothing preceded it that anyone\n * accepted.\n */\nexport const PUBLISHER_AGREEMENT_CHANGES: readonly {\n version: string\n changes: readonly string[]\n}[] = [\n {\n version: '2026-08-18.1',\n changes: [\n '§1 names the marketplace and the End User License Agreement by their current names.',\n '§3(h), new: listing conduct — what an Artifact and its listing may not claim about the platform, its plans or its prices.',\n '§8.6, new: a paid Artifact, tier or license key must be sold through the Marketplace.',\n '§13.6, new: how you may use the platform’s names and marks.',\n ],\n },\n {\n version: '2026-08-24.1',\n changes: [\n '§13.3: venue moves to Travis County, Texas, and governing law now cites Terms of Service Section 18.6.',\n ],\n },\n]\n\n/**\n * The changes an org has not yet accepted, oldest first.\n *\n * Versions are dated strings (`YYYY-MM-DD.n`), so they order as text. An\n * acceptance of a version this list does not name — one older than the first\n * entry — gets every entry, which is the honest answer: all of it is new to\n * them. No acceptance at all gets none, because a first acceptance is of the\n * whole document, not a set of edits.\n */\nexport function publisherAgreementChangesSince(\n acceptedVersion: string | null | undefined,\n currentVersion: string = PUBLISHER_AGREEMENT_VERSION,\n): readonly { version: string; changes: readonly string[] }[] {\n const accepted = typeof acceptedVersion === 'string' ? acceptedVersion.trim() : ''\n if (!accepted) return []\n return PUBLISHER_AGREEMENT_CHANGES.filter(\n (entry) => entry.version > accepted && entry.version <= currentVersion,\n )\n}\n\n/**\n * A recorded acceptance: which version, of which exact bytes, by whom, when.\n *\n * `sha256`/`bytes` (AGL-1678) pin the acceptance to the archived snapshot of\n * the text the publisher was shown. Optional because records written before\n * the pin existed carry only the version string — absence means \"accepted\n * before 2026-08-17\", not \"accepted nothing\".\n */\nexport interface PublisherAgreementAcceptance {\n version?: string\n sha256?: string\n bytes?: number\n acceptedBy?: string\n acceptedAt?: unknown\n}\n\n/**\n * Where an org stands with the agreement.\n *\n * `none` and `outdated` are deliberately different answers. \"You have never\n * agreed to anything\" and \"the terms changed since you agreed\" are different\n * situations with different copy, and collapsing them into a boolean is how\n * a re-acceptance ends up reading like a first-time setup step.\n */\nexport type PublisherAgreementState = 'none' | 'outdated' | 'current'\n\nexport function publisherAgreementState(\n acceptance: PublisherAgreementAcceptance | null | undefined,\n currentVersion: string = PUBLISHER_AGREEMENT_VERSION,\n): PublisherAgreementState {\n const accepted = acceptance?.version\n if (typeof accepted !== 'string' || !accepted.trim()) return 'none'\n return accepted === currentVersion ? 'current' : 'outdated'\n}\n\n/**\n * Whether a publish may proceed.\n *\n * An older acceptance is NOT carried forward. Whoever publishes next reads\n * the changed agreement and accepts it, or does not publish — silently\n * inheriting an acceptance across a rewrite is precisely what makes the\n * record worthless.\n */\nexport function hasCurrentPublisherAgreement(\n acceptance: PublisherAgreementAcceptance | null | undefined,\n currentVersion: string = PUBLISHER_AGREEMENT_VERSION,\n): boolean {\n return publisherAgreementState(acceptance, currentVersion) === 'current'\n}\n\n/**\n * What the acceptance UI may show (AGL-1660).\n *\n * The gate lives HERE rather than in the panel, for the reason every \"check it\n * in the component\" control eventually fails: the component is not the only\n * caller. The accept route consults the same function, so a client that skips\n * the UI entirely meets the same answer.\n *\n * `documentUrl` is null, not the URL, when the document is unpublished — the\n * console reads the link target from this, so an unreadable document is\n * structurally unable to reach an href.\n */\nexport interface PublisherAgreementPresentation {\n /** The document to link, or null when there is no page to link to. */\n documentUrl: string | null\n /** Whether the accept control may be offered at all. */\n canAccept: boolean\n /** Why acceptance is unavailable, or null when it is available. */\n unavailableNotice: string | null\n}\n\nexport function publisherAgreementPresentation(\n state: PublisherAgreementState,\n published: boolean = publisherAgreementIsPublished(),\n): PublisherAgreementPresentation {\n return {\n documentUrl: published ? PUBLISHER_AGREEMENT_URL : null,\n // Already current is not \"cannot accept because unpublished\" — it is\n // nothing left to do, and the panel says so separately.\n canAccept: published && state !== 'current',\n unavailableNotice: published ? null : PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE,\n }\n}\n\n/** The refusal a publisher sees, saying which of the two problems it is. */\nexport function publisherAgreementRefusal(\n state: PublisherAgreementState,\n published: boolean = publisherAgreementIsPublished(),\n): string {\n // Sending someone to a control we have disabled is worse than saying\n // nothing: they go, find no button, and conclude the console is broken.\n if (!published) {\n return (\n `The ${PUBLISHER_AGREEMENT_TITLE} is not published yet, so it cannot ` +\n 'be accepted and publishing is paused for everyone. This is on us, not ' +\n 'on your organization.'\n )\n }\n return state === 'outdated'\n ? `The ${PUBLISHER_AGREEMENT_TITLE} has changed since your organization ` +\n 'accepted it. Review and accept the current version in Marketplace → ' +\n 'Publisher Profile, then publish.'\n : `Your organization has not accepted the ${PUBLISHER_AGREEMENT_TITLE}. ` +\n 'Accept it in Marketplace → Publisher Profile, then publish.'\n}\n"],"names":["LEGAL_ORIGIN","isPublishedLegalUrl","PUBLISHER_AGREEMENT_VERSION","PUBLISHER_AGREEMENT_TITLE","PUBLISHER_AGREEMENT_SHA256","PUBLISHER_AGREEMENT_BYTES","PUBLISHER_AGREEMENT_URL","publisherAgreementIsPublished","url","PUBLISHER_AGREEMENT_UNAVAILABLE_NOTICE","PUBLISHER_AGREEMENT_POINTS","id","label","detail","PUBLISHER_AGREEMENT_CHANGES","version","changes","publisherAgreementChangesSince","acceptedVersion","currentVersion","accepted","trim","filter","entry","publisherAgreementState","acceptance","hasCurrentPublisherAgreement","publisherAgreementPresentation","state","published","documentUrl","canAccept","unavailableNotice","publisherAgreementRefusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GAED,SAASA,YAAY,EAAEC,mBAAmB,QAAQ,6BAAyB;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FC,GACD,OAAO,MAAMC,8BAA8B,eAAc;AAEzD,OAAO,MAAMC,4BAA4B,kCAAiC;AAE1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCC,GACD,OAAO,MAAMC,6BACX,mEAAkE;AAEpE,4EAA4E,GAC5E,OAAO,MAAMC,4BAA4B,MAAK;AAE9C;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,0BAA0B,GAAGN,aAAa,sCAAsC,CAAC,CAAA;AAE9F;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASO,8BACdC,MAAcF,uBAAuB;IAErC,OAAOL,oBAAoBO;AAC7B;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,yCACX,CAAC,IAAI,EAAEN,0BAA0B,mCAAmC,CAAC,GACrE,0EACA,sEACA,8EACA,mCAAkC;AAEpC;;;;;;;CAOC,GACD,OAAO,MAAMO,6BAIP;IACJ;QACEC,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;IACA;QACEF,IAAI;QACJC,OAAO;QACPC,QACE;IACJ;CACD,CAAA;AAED;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,MAAMC,8BAGP;IACJ;QACEC,SAAS;QACTC,SAAS;YACP;YACA;YACA;YACA;SACD;IACH;IACA;QACED,SAAS;QACTC,SAAS;YACP;SACD;IACH;CACD,CAAA;AAED;;;;;;;;CAQC,GACD,OAAO,SAASC,+BACdC,eAA0C,EAC1CC,iBAAyBjB,2BAA2B;IAEpD,MAAMkB,WAAW,OAAOF,oBAAoB,WAAWA,gBAAgBG,IAAI,KAAK;IAChF,IAAI,CAACD,UAAU,OAAO,EAAE;IACxB,OAAON,4BAA4BQ,MAAM,CACvC,CAACC,QAAUA,MAAMR,OAAO,GAAGK,YAAYG,MAAMR,OAAO,IAAII;AAE5D;AA4BA,OAAO,SAASK,wBACdC,UAA2D,EAC3DN,iBAAyBjB,2BAA2B;IAEpD,MAAMkB,WAAWK,8BAAAA,WAAYV,OAAO;IACpC,IAAI,OAAOK,aAAa,YAAY,CAACA,SAASC,IAAI,IAAI,OAAO;IAC7D,OAAOD,aAAaD,iBAAiB,YAAY;AACnD;AAEA;;;;;;;CAOC,GACD,OAAO,SAASO,6BACdD,UAA2D,EAC3DN,iBAAyBjB,2BAA2B;IAEpD,OAAOsB,wBAAwBC,YAAYN,oBAAoB;AACjE;AAuBA,OAAO,SAASQ,+BACdC,KAA8B,EAC9BC,YAAqBtB,+BAA+B;IAEpD,OAAO;QACLuB,aAAaD,YAAYvB,0BAA0B;QACnD,qEAAqE;QACrE,wDAAwD;QACxDyB,WAAWF,aAAaD,UAAU;QAClCI,mBAAmBH,YAAY,OAAOpB;IACxC;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASwB,0BACdL,KAA8B,EAC9BC,YAAqBtB,+BAA+B;IAEpD,qEAAqE;IACrE,wEAAwE;IACxE,IAAI,CAACsB,WAAW;QACd,OACE,CAAC,IAAI,EAAE1B,0BAA0B,oCAAoC,CAAC,GACtE,2EACA;IAEJ;IACA,OAAOyB,UAAU,aACb,CAAC,IAAI,EAAEzB,0BAA0B,qCAAqC,CAAC,GACrE,yEACA,qCACF,CAAC,uCAAuC,EAAEA,0BAA0B,EAAE,CAAC,GACrE;AACR"}
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"RICH_TEXT_COMMANDS",
|
|
35
35
|
"ScreenLinkContext",
|
|
36
36
|
"SiteContext",
|
|
37
|
+
"THEME_PRESETS_LOAD_POINT",
|
|
37
38
|
"VARIABLE_ID_TOKEN_PATTERN",
|
|
38
39
|
"VARIABLE_NAME_PATTERN",
|
|
39
40
|
"aglyn",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"listConsoleOrgNavItems",
|
|
67
68
|
"listConsoleProviders",
|
|
68
69
|
"listConsoleStaffPages",
|
|
70
|
+
"listConsoleThemePresets",
|
|
69
71
|
"listConsoleWidgets",
|
|
70
72
|
"logger",
|
|
71
73
|
"nodesReferenceBinding",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/realm-host-surface.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-realm-host-exports.mjs\n *\n * The names each realm host module holds (AGL-3392), for the bundle\n * verifier: a bundle reading any other name from the host is refused,\n * because it would read `undefined` on every site.\n */\n/* eslint-disable */\n\nexport const REALM_HOST_SURFACE_NAMES: Readonly<\n Record<'aglyn' | 'mui' | 'muiStyles', ReadonlySet<string>>\n> = {\n aglyn: new Set([\"Aglyn\",\"CATEGORY_REQUIRED_CAPABILITY\",\"COMPONENT_CATEGORY_ORDER\",\"CONSOLE_STAFF_WIDGET_SLOTS\",\"CONSOLE_WIDGET_SLOTS\",\"ComponentCategory\",\"EnabledPluginsContext\",\"FEATURE_FLAG\",\"FUNCTION_BUILTIN_NAMES\",\"FUNCTION_MAX_OPERATIONS\",\"FUNCTION_TOKEN_PATTERN\",\"FieldComponentType\",\"FieldValidatorType\",\"HOST_VARIABLE_TYPE_LABELS\",\"LinealDirectiveFlag\",\"MISSING_BINDING_LABEL\",\"MUI_BUNDLE_ID\",\"NAME_TOKEN_PATTERN\",\"NodeIdentityContext\",\"NodeType\",\"PREVIEW_REFUSED_STATUS\",\"PREVIEW_WRITE_BLOCKED_EVENT\",\"REUSABLE_COMPONENT_CATEGORY\",\"REUSABLE_EMAIL_BLOCK_CATEGORY\",\"RICH_TEXT_COMMANDS\",\"ScreenLinkContext\",\"SiteContext\",\"VARIABLE_ID_TOKEN_PATTERN\",\"VARIABLE_NAME_PATTERN\",\"aglyn\",\"attachFunctionDefinitions\",\"bindingTokenNeedsDeep\",\"canvas\",\"components\",\"defineUiFeatureBundle\",\"displayBindingTokens\",\"editableBindingTokens\",\"emitter\",\"evaluateExpression\",\"evaluateHostFunction\",\"expressionIdentifiers\",\"formatFunctionIdToken\",\"formatFunctionParameterOptions\",\"formatVariableIdToken\",\"formatVariableValue\",\"functionGlobals\",\"functionReferencedNames\",\"hasBindings\",\"isCategoryCapabilityEnabled\",\"isConsoleStaffWidgetSlot\",\"isFromEnabledPlugin\",\"isPreviewRefusal\",\"isSwitchedOffForRenderedSite\",\"keyById\",\"listConsoleExtensions\",\"listConsoleNavItems\",\"listConsoleOrgNavItems\",\"listConsoleProviders\",\"listConsoleStaffPages\",\"listConsoleWidgets\",\"logger\",\"nodesReferenceBinding\",\"normalizeBindingTokens\",\"parseFunctionParameterOptions\",\"plugins\",\"registerConsoleExtension\",\"resolveBindings\",\"resolveConsoleOrgPluginPage\",\"resolveConsolePluginPage\",\"resolveConsoleStaffPage\",\"resolveNodesBindings\",\"rewriteBindingTokensDeep\",\"textReferencesBinding\",\"unregisterConsoleExtension\",\"useEnabledPlugins\",\"useNodeId\",\"useSite\",\"useSiteFetch\"]),\n mui: new Set([\"Alert\",\"Box\",\"Button\",\"Card\",\"CardContent\",\"Checkbox\",\"Chip\",\"CircularProgress\",\"Collapse\",\"Divider\",\"FormControl\",\"FormControlLabel\",\"FormHelperText\",\"IconButton\",\"InputAdornment\",\"InputLabel\",\"Link\",\"List\",\"ListItem\",\"ListItemText\",\"MenuItem\",\"Paper\",\"Radio\",\"RadioGroup\",\"Select\",\"Stack\",\"SvgIcon\",\"Switch\",\"Table\",\"TableBody\",\"TableCell\",\"TableHead\",\"TableRow\",\"TextField\",\"Tooltip\",\"Typography\",\"createSvgIcon\",\"useMediaQuery\"]),\n muiStyles: new Set([\"alpha\",\"css\",\"darken\",\"emphasize\",\"getContrastRatio\",\"keyframes\",\"lighten\",\"styled\",\"useColorScheme\",\"useTheme\"]),\n}\n"],"names":["REALM_HOST_SURFACE_NAMES","aglyn","Set","mui","muiStyles"],"mappings":"AAAA;;;;;;;CAOC,GACD,kBAAkB,GAElB,OAAO,MAAMA,2BAET;IACFC,OAAO,IAAIC,IAAI;QAAC;QAAQ;QAA+B;QAA2B;QAA6B;QAAuB;QAAoB;QAAwB;QAAe;QAAyB;QAA0B;QAAyB;QAAqB;QAAqB;QAA4B;QAAsB;QAAwB;QAAgB;QAAqB;QAAsB;QAAW;QAAyB;QAA8B;QAA8B;QAAgC;QAAqB;QAAoB;QAAc;QAA4B;QAAwB;QAAQ;QAA4B;QAAwB;QAAS;QAAa;QAAwB;QAAuB;QAAwB;QAAU;QAAqB;QAAuB;QAAwB;QAAwB;QAAiC;QAAwB;QAAsB;QAAkB;QAA0B;QAAc;QAA8B;QAA2B;QAAsB;QAAmB;QAA+B;QAAU;QAAwB;QAAsB;QAAyB;QAAuB;QAAwB;QAAqB;QAAS;QAAwB;QAAyB;QAAgC;QAAU;QAA2B;QAAkB;QAA8B;QAA2B;QAA0B;QAAuB;QAA2B;QAAwB;QAA6B;QAAoB;QAAY;QAAU;KAAe;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/realm-host-surface.generated.ts"],"sourcesContent":["/**\n * GENERATED FILE — do not edit. Regenerate with:\n * node tools/scripts/generate-realm-host-exports.mjs\n *\n * The names each realm host module holds (AGL-3392), for the bundle\n * verifier: a bundle reading any other name from the host is refused,\n * because it would read `undefined` on every site.\n */\n/* eslint-disable */\n\nexport const REALM_HOST_SURFACE_NAMES: Readonly<\n Record<'aglyn' | 'mui' | 'muiStyles', ReadonlySet<string>>\n> = {\n aglyn: new Set([\"Aglyn\",\"CATEGORY_REQUIRED_CAPABILITY\",\"COMPONENT_CATEGORY_ORDER\",\"CONSOLE_STAFF_WIDGET_SLOTS\",\"CONSOLE_WIDGET_SLOTS\",\"ComponentCategory\",\"EnabledPluginsContext\",\"FEATURE_FLAG\",\"FUNCTION_BUILTIN_NAMES\",\"FUNCTION_MAX_OPERATIONS\",\"FUNCTION_TOKEN_PATTERN\",\"FieldComponentType\",\"FieldValidatorType\",\"HOST_VARIABLE_TYPE_LABELS\",\"LinealDirectiveFlag\",\"MISSING_BINDING_LABEL\",\"MUI_BUNDLE_ID\",\"NAME_TOKEN_PATTERN\",\"NodeIdentityContext\",\"NodeType\",\"PREVIEW_REFUSED_STATUS\",\"PREVIEW_WRITE_BLOCKED_EVENT\",\"REUSABLE_COMPONENT_CATEGORY\",\"REUSABLE_EMAIL_BLOCK_CATEGORY\",\"RICH_TEXT_COMMANDS\",\"ScreenLinkContext\",\"SiteContext\",\"THEME_PRESETS_LOAD_POINT\",\"VARIABLE_ID_TOKEN_PATTERN\",\"VARIABLE_NAME_PATTERN\",\"aglyn\",\"attachFunctionDefinitions\",\"bindingTokenNeedsDeep\",\"canvas\",\"components\",\"defineUiFeatureBundle\",\"displayBindingTokens\",\"editableBindingTokens\",\"emitter\",\"evaluateExpression\",\"evaluateHostFunction\",\"expressionIdentifiers\",\"formatFunctionIdToken\",\"formatFunctionParameterOptions\",\"formatVariableIdToken\",\"formatVariableValue\",\"functionGlobals\",\"functionReferencedNames\",\"hasBindings\",\"isCategoryCapabilityEnabled\",\"isConsoleStaffWidgetSlot\",\"isFromEnabledPlugin\",\"isPreviewRefusal\",\"isSwitchedOffForRenderedSite\",\"keyById\",\"listConsoleExtensions\",\"listConsoleNavItems\",\"listConsoleOrgNavItems\",\"listConsoleProviders\",\"listConsoleStaffPages\",\"listConsoleThemePresets\",\"listConsoleWidgets\",\"logger\",\"nodesReferenceBinding\",\"normalizeBindingTokens\",\"parseFunctionParameterOptions\",\"plugins\",\"registerConsoleExtension\",\"resolveBindings\",\"resolveConsoleOrgPluginPage\",\"resolveConsolePluginPage\",\"resolveConsoleStaffPage\",\"resolveNodesBindings\",\"rewriteBindingTokensDeep\",\"textReferencesBinding\",\"unregisterConsoleExtension\",\"useEnabledPlugins\",\"useNodeId\",\"useSite\",\"useSiteFetch\"]),\n mui: new Set([\"Alert\",\"Box\",\"Button\",\"Card\",\"CardContent\",\"Checkbox\",\"Chip\",\"CircularProgress\",\"Collapse\",\"Divider\",\"FormControl\",\"FormControlLabel\",\"FormHelperText\",\"IconButton\",\"InputAdornment\",\"InputLabel\",\"Link\",\"List\",\"ListItem\",\"ListItemText\",\"MenuItem\",\"Paper\",\"Radio\",\"RadioGroup\",\"Select\",\"Stack\",\"SvgIcon\",\"Switch\",\"Table\",\"TableBody\",\"TableCell\",\"TableHead\",\"TableRow\",\"TextField\",\"Tooltip\",\"Typography\",\"createSvgIcon\",\"useMediaQuery\"]),\n muiStyles: new Set([\"alpha\",\"css\",\"darken\",\"emphasize\",\"getContrastRatio\",\"keyframes\",\"lighten\",\"styled\",\"useColorScheme\",\"useTheme\"]),\n}\n"],"names":["REALM_HOST_SURFACE_NAMES","aglyn","Set","mui","muiStyles"],"mappings":"AAAA;;;;;;;CAOC,GACD,kBAAkB,GAElB,OAAO,MAAMA,2BAET;IACFC,OAAO,IAAIC,IAAI;QAAC;QAAQ;QAA+B;QAA2B;QAA6B;QAAuB;QAAoB;QAAwB;QAAe;QAAyB;QAA0B;QAAyB;QAAqB;QAAqB;QAA4B;QAAsB;QAAwB;QAAgB;QAAqB;QAAsB;QAAW;QAAyB;QAA8B;QAA8B;QAAgC;QAAqB;QAAoB;QAAc;QAA2B;QAA4B;QAAwB;QAAQ;QAA4B;QAAwB;QAAS;QAAa;QAAwB;QAAuB;QAAwB;QAAU;QAAqB;QAAuB;QAAwB;QAAwB;QAAiC;QAAwB;QAAsB;QAAkB;QAA0B;QAAc;QAA8B;QAA2B;QAAsB;QAAmB;QAA+B;QAAU;QAAwB;QAAsB;QAAyB;QAAuB;QAAwB;QAA0B;QAAqB;QAAS;QAAwB;QAAyB;QAAgC;QAAU;QAA2B;QAAkB;QAA8B;QAA2B;QAA0B;QAAuB;QAA2B;QAAwB;QAA6B;QAAoB;QAAY;QAAU;KAAe;IAC9vDC,KAAK,IAAID,IAAI;QAAC;QAAQ;QAAM;QAAS;QAAO;QAAc;QAAW;QAAO;QAAmB;QAAW;QAAU;QAAc;QAAmB;QAAiB;QAAa;QAAiB;QAAa;QAAO;QAAO;QAAW;QAAe;QAAW;QAAQ;QAAQ;QAAa;QAAS;QAAQ;QAAU;QAAS;QAAQ;QAAY;QAAY;QAAY;QAAW;QAAY;QAAU;QAAa;QAAgB;KAAgB;IAC/bE,WAAW,IAAIF,IAAI;QAAC;QAAQ;QAAM;QAAS;QAAY;QAAmB;QAAY;QAAU;QAAS;QAAiB;KAAW;AACvI,EAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { HostTheme } from '@aglyn/shared-data-types';
|
|
18
|
+
import { type ThemeHostDocument } from './marketplace-theme';
|
|
19
|
+
/**
|
|
20
|
+
* A site's theme library (AGL-3404): the themes a site can switch between,
|
|
21
|
+
* and the rule that switching, editing and saving never destroy one.
|
|
22
|
+
*
|
|
23
|
+
* ## Three layers, one of them picked
|
|
24
|
+
*
|
|
25
|
+
* The site renders `theme ⊕ themeOverride` (`resolveSiteTheme`), exactly as
|
|
26
|
+
* it did before there was a library. What the library adds is WHERE `theme`
|
|
27
|
+
* came from, named by `themeSelection`:
|
|
28
|
+
*
|
|
29
|
+
* - `default` — the platform default. `theme` is empty.
|
|
30
|
+
* - `preset` — a built-in theme a plugin contributes. `theme` is a copy.
|
|
31
|
+
* - `custom` — one of the site's own saved themes, kept in the library.
|
|
32
|
+
* - `installed` — a marketplace theme, kept in the library with the
|
|
33
|
+
* provenance an update needs.
|
|
34
|
+
*
|
|
35
|
+
* Every edit is an override patch over the picked theme and never a write to
|
|
36
|
+
* it, so "Restore" is deleting the patch and the theme it restores to is
|
|
37
|
+
* still exactly the one that was picked. "Save as custom theme" copies the
|
|
38
|
+
* resolved result into a new library entry; the theme it started from stays
|
|
39
|
+
* as it was.
|
|
40
|
+
*
|
|
41
|
+
* ## Switching keeps each theme's own edits
|
|
42
|
+
*
|
|
43
|
+
* Leaving a theme stashes its override on its library entry, and coming back
|
|
44
|
+
* restores it — so trying another theme is not a way to lose work. A preset
|
|
45
|
+
* or the default has no content to keep, so its entry holds only the stash.
|
|
46
|
+
*
|
|
47
|
+
* ## The site's own settings travel with the site
|
|
48
|
+
*
|
|
49
|
+
* `darkScheme` is a decision about the site's content, not about a design
|
|
50
|
+
* (`THEME_ARTIFACT_FIELDS` says the same for publishing), so it survives every
|
|
51
|
+
* switch, save and restore: whichever theme is picked, a site that switched
|
|
52
|
+
* dark off stays light.
|
|
53
|
+
*
|
|
54
|
+
* Pure: the console route and the marketplace install route both plan here
|
|
55
|
+
* and write the plan in their own transaction, so neither can file a switch
|
|
56
|
+
* differently from the other.
|
|
57
|
+
*/
|
|
58
|
+
/** The host subcollection the library lives in. */
|
|
59
|
+
export declare const THEME_LIBRARY_COLLECTION = "themes";
|
|
60
|
+
/**
|
|
61
|
+
* The most custom themes one site keeps. A client can read the library, so an
|
|
62
|
+
* unbounded one is an unbounded read on every visit to the theme page.
|
|
63
|
+
*/
|
|
64
|
+
export declare const THEME_LIBRARY_MAX_CUSTOM = 25;
|
|
65
|
+
/** The longest name a saved theme takes. */
|
|
66
|
+
export declare const THEME_NAME_MAX = 60;
|
|
67
|
+
export type ThemeLibraryKind = 'default' | 'preset' | 'custom' | 'installed';
|
|
68
|
+
/** What `themeSelection` on a host names. */
|
|
69
|
+
export interface HostThemeSelection {
|
|
70
|
+
kind: ThemeLibraryKind;
|
|
71
|
+
/** The library entry id (or the preset's entry id). */
|
|
72
|
+
id: string;
|
|
73
|
+
/** Shown in the picker; a copy, so a renamed entry renames the selection too. */
|
|
74
|
+
name: string;
|
|
75
|
+
}
|
|
76
|
+
/** A document in `hosts/{hostId}/themes`. */
|
|
77
|
+
export interface ThemeLibraryEntry {
|
|
78
|
+
kind: ThemeLibraryKind;
|
|
79
|
+
name: string;
|
|
80
|
+
/** The theme itself, for `custom` and `installed`. Never edited in place by the editor. */
|
|
81
|
+
theme?: HostTheme | null;
|
|
82
|
+
/** The override stashed while this theme is not the picked one. */
|
|
83
|
+
override?: unknown;
|
|
84
|
+
/** Marketplace provenance, for `installed` — what `themeInstalledFrom` is restored from. */
|
|
85
|
+
installedFrom?: Record<string, unknown> | null;
|
|
86
|
+
createdAt?: unknown;
|
|
87
|
+
updatedAt?: unknown;
|
|
88
|
+
}
|
|
89
|
+
/** The entry id the platform default's stash lives under. */
|
|
90
|
+
export declare const DEFAULT_THEME_ENTRY_ID = "default";
|
|
91
|
+
/**
|
|
92
|
+
* The entry id a pre-library site's own theme is filed under when it first
|
|
93
|
+
* leaves it. Before the library a site had exactly one theme of its own, so
|
|
94
|
+
* a fixed id cannot collide with another.
|
|
95
|
+
*/
|
|
96
|
+
export declare const SITE_THEME_ENTRY_ID = "site-theme";
|
|
97
|
+
/** The default's name when nothing else names it. */
|
|
98
|
+
export declare const DEFAULT_THEME_NAME = "Material UI";
|
|
99
|
+
/** A preset's entry id, which holds only its stash. */
|
|
100
|
+
export declare function presetEntryId(presetId: string): string;
|
|
101
|
+
/** A marketplace theme's entry id: one per listing, so reinstalling updates it. */
|
|
102
|
+
export declare function installedEntryId(listingId: string): string;
|
|
103
|
+
/** A host document as the library reads it. */
|
|
104
|
+
export interface ThemeLibraryHost extends ThemeHostDocument {
|
|
105
|
+
themeSelection?: unknown;
|
|
106
|
+
themeInstalledFrom?: ({
|
|
107
|
+
sha256?: string | null;
|
|
108
|
+
listingId?: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
} & Record<string, unknown>) | null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The theme a site has picked.
|
|
114
|
+
*
|
|
115
|
+
* A site from before the library has no `themeSelection`, and reads as what
|
|
116
|
+
* it was: a marketplace theme, its own theme, or the default. That is what
|
|
117
|
+
* lets the library ship without touching a single existing host document.
|
|
118
|
+
*/
|
|
119
|
+
export declare function readThemeSelection(host: ThemeLibraryHost | null | undefined): HostThemeSelection;
|
|
120
|
+
/** A name as stored: trimmed, bounded, and never empty. */
|
|
121
|
+
export declare function normalizeThemeName(name: unknown): string | null;
|
|
122
|
+
/** Marks a field for deletion; the route writing the plan maps it to its store's delete. */
|
|
123
|
+
export declare const THEME_FIELD_DELETE = "__aglynThemeFieldDelete";
|
|
124
|
+
export type ThemeLibraryEntryWrite = {
|
|
125
|
+
id: string;
|
|
126
|
+
op: 'create';
|
|
127
|
+
data: ThemeLibraryEntry;
|
|
128
|
+
} | {
|
|
129
|
+
id: string;
|
|
130
|
+
op: 'update';
|
|
131
|
+
data: Record<string, unknown>;
|
|
132
|
+
} | {
|
|
133
|
+
id: string;
|
|
134
|
+
op: 'delete';
|
|
135
|
+
};
|
|
136
|
+
export type ThemeLibraryPlan = {
|
|
137
|
+
ok: true;
|
|
138
|
+
/**
|
|
139
|
+
* Host fields to replace WHOLESALE (`update`, never a deep merge: a
|
|
140
|
+
* merged override is the union of two patches, and a merged theme keeps
|
|
141
|
+
* the tokens of the one it replaced).
|
|
142
|
+
*/
|
|
143
|
+
host: Record<string, unknown>;
|
|
144
|
+
entries: ThemeLibraryEntryWrite[];
|
|
145
|
+
/** The theme picked once the plan is written. */
|
|
146
|
+
selection: HostThemeSelection;
|
|
147
|
+
} | {
|
|
148
|
+
ok: false;
|
|
149
|
+
status: number;
|
|
150
|
+
error: string;
|
|
151
|
+
};
|
|
152
|
+
export type ThemeLibraryTarget = {
|
|
153
|
+
kind: 'default';
|
|
154
|
+
}
|
|
155
|
+
/** A plugin's theme, sent by the console that listed it. */
|
|
156
|
+
| {
|
|
157
|
+
kind: 'preset';
|
|
158
|
+
id: string;
|
|
159
|
+
name: string;
|
|
160
|
+
theme: HostTheme;
|
|
161
|
+
} | {
|
|
162
|
+
kind: 'custom' | 'installed';
|
|
163
|
+
id: string;
|
|
164
|
+
};
|
|
165
|
+
export type ThemeLibraryAction = {
|
|
166
|
+
action: 'select';
|
|
167
|
+
target: ThemeLibraryTarget;
|
|
168
|
+
} | {
|
|
169
|
+
action: 'save-as';
|
|
170
|
+
name: string;
|
|
171
|
+
} | {
|
|
172
|
+
action: 'update';
|
|
173
|
+
} | {
|
|
174
|
+
action: 'restore';
|
|
175
|
+
} | {
|
|
176
|
+
action: 'rename';
|
|
177
|
+
id: string;
|
|
178
|
+
name: string;
|
|
179
|
+
} | {
|
|
180
|
+
action: 'delete';
|
|
181
|
+
id: string;
|
|
182
|
+
} | {
|
|
183
|
+
action: 'install';
|
|
184
|
+
listingId: string;
|
|
185
|
+
name: string;
|
|
186
|
+
theme: HostTheme;
|
|
187
|
+
installedFrom: Record<string, unknown>;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Plans one library action against the host document and its library as
|
|
191
|
+
* read in the same transaction.
|
|
192
|
+
*
|
|
193
|
+
* `newId` mints a library entry id for a new custom theme.
|
|
194
|
+
*/
|
|
195
|
+
export declare function planThemeLibraryAction(input: {
|
|
196
|
+
host: ThemeLibraryHost | null | undefined;
|
|
197
|
+
entries: Readonly<Record<string, ThemeLibraryEntry>>;
|
|
198
|
+
action: ThemeLibraryAction;
|
|
199
|
+
newId: () => string;
|
|
200
|
+
}): ThemeLibraryPlan;
|
|
201
|
+
/**
|
|
202
|
+
* Whether the picked theme carries edits of this site's own — what "Restore"
|
|
203
|
+
* and "Update" act on. The site's dark-scheme setting is not an edit to the
|
|
204
|
+
* theme, so an override holding only that does not count.
|
|
205
|
+
*/
|
|
206
|
+
export declare function hasThemeEdits(host: ThemeLibraryHost | null | undefined): boolean;
|