@aglyn/aglyn 1.0.0 → 1.1.0
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/compose-reusable-components.d.ts +22 -0
- package/src/lib/app-utils/compose-reusable-components.js +94 -32
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +11 -0
- package/src/lib/app-utils/contacts.js +10 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +50 -19
- package/src/lib/app-utils/crm.js +37 -22
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/default-site.d.ts +10 -3
- package/src/lib/app-utils/default-site.js +82 -6
- package/src/lib/app-utils/default-site.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +11 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +5 -5
- package/src/lib/app-utils/docs-help.generated.js +23 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +43 -15
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/host-tokens.d.ts +7 -0
- package/src/lib/app-utils/host-tokens.js +27 -4
- package/src/lib/app-utils/host-tokens.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.js +5 -1
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +1 -0
- package/src/lib/app-utils/server.js +1 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-brand-files.d.ts +87 -0
- package/src/lib/app-utils/site-brand-files.js +157 -0
- package/src/lib/app-utils/site-brand-files.js.map +1 -0
- package/src/lib/app-utils/site-starter-nodes.js +1 -0
- package/src/lib/app-utils/site-starter-nodes.js.map +1 -1
- package/src/lib/app-utils/site-starter-pick.d.ts +2 -2
- package/src/lib/app-utils/site-starter-pick.js +2 -2
- package/src/lib/app-utils/site-starter-pick.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +36 -2
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
|
@@ -77,6 +77,7 @@ export type HostTokenEmptyBehaviour =
|
|
|
77
77
|
export interface HostTokenSource {
|
|
78
78
|
displayName?: string;
|
|
79
79
|
logoUrl?: string;
|
|
80
|
+
logoDarkUrl?: string;
|
|
80
81
|
subdomain?: string;
|
|
81
82
|
cname?: string;
|
|
82
83
|
/** Contact details a publisher template can reference (AGL-1022). */
|
|
@@ -137,6 +138,12 @@ export declare function hostTokenDefinition(key: string): HostTokenDefinition |
|
|
|
137
138
|
* segment. `host.a.b` does not match, so there is no path syntax to abuse.
|
|
138
139
|
*/
|
|
139
140
|
export declare const HOST_TOKEN_PATTERN: RegExp;
|
|
141
|
+
/**
|
|
142
|
+
* Whether a value is exactly one image token, like `{{host.logo}}`: what a
|
|
143
|
+
* picture's `src` may be when it is the installing site's own logo. It becomes
|
|
144
|
+
* that site's stored value at render, so it names nothing of the publisher's.
|
|
145
|
+
*/
|
|
146
|
+
export declare function isHostImageToken(value: string): boolean;
|
|
140
147
|
/** Every `host.*` token in a string, in order, including unknown ones. */
|
|
141
148
|
export declare function hostTokensIn(text: string): string[];
|
|
142
149
|
/** The resolved value of one token, or undefined when the site has none. */
|
|
@@ -42,6 +42,11 @@ function businessName(host) {
|
|
|
42
42
|
var _host_seo_entity, _host_seo;
|
|
43
43
|
return (_trimmed = trimmed((_host_seo = host.seo) == null ? void 0 : (_host_seo_entity = _host_seo.entity) == null ? void 0 : _host_seo_entity.name)) != null ? _trimmed : trimmed(host.displayName);
|
|
44
44
|
}
|
|
45
|
+
function logo(host) {
|
|
46
|
+
var _trimmed;
|
|
47
|
+
var _host_seo_entity, _host_seo;
|
|
48
|
+
return (_trimmed = trimmed(host.logoUrl)) != null ? _trimmed : trimmed((_host_seo = host.seo) == null ? void 0 : (_host_seo_entity = _host_seo.entity) == null ? void 0 : _host_seo_entity.logo);
|
|
49
|
+
}
|
|
45
50
|
/**
|
|
46
51
|
* The closed registry.
|
|
47
52
|
*
|
|
@@ -62,13 +67,22 @@ function businessName(host) {
|
|
|
62
67
|
type: 'image',
|
|
63
68
|
label: 'Logo',
|
|
64
69
|
description: 'The site’s logo image.',
|
|
70
|
+
resolve: logo,
|
|
71
|
+
// No image: the block is dropped and the business name carries the brand
|
|
72
|
+
// instead, which is what the block was for.
|
|
73
|
+
whenEmpty: 'omit-block'
|
|
74
|
+
},
|
|
75
|
+
logoDark: {
|
|
76
|
+
key: 'logoDark',
|
|
77
|
+
type: 'image',
|
|
78
|
+
label: 'Logo for dark backgrounds',
|
|
79
|
+
description: 'The logo to show on a dark background. The site’s logo when it has no dark one.',
|
|
80
|
+
// The pair a header toggles by color scheme (AGL-4119): a site with one
|
|
81
|
+
// logo shows it in both schemes, and one with none shows neither.
|
|
65
82
|
resolve: (host)=>{
|
|
66
83
|
var _trimmed;
|
|
67
|
-
|
|
68
|
-
return (_trimmed = trimmed(host.logoUrl)) != null ? _trimmed : trimmed((_host_seo = host.seo) == null ? void 0 : (_host_seo_entity = _host_seo.entity) == null ? void 0 : _host_seo_entity.logo);
|
|
84
|
+
return (_trimmed = trimmed(host.logoDarkUrl)) != null ? _trimmed : logo(host);
|
|
69
85
|
},
|
|
70
|
-
// No image: the block is dropped and the business name carries the brand
|
|
71
|
-
// instead, which is what the block was for.
|
|
72
86
|
whenEmpty: 'omit-block'
|
|
73
87
|
},
|
|
74
88
|
url: {
|
|
@@ -137,6 +151,15 @@ function businessName(host) {
|
|
|
137
151
|
* `{{host.key}}` — whitespace-tolerant, and deliberately NOT dotted beyond one
|
|
138
152
|
* segment. `host.a.b` does not match, so there is no path syntax to abuse.
|
|
139
153
|
*/ export const HOST_TOKEN_PATTERN = /\{\{\s*host\.([a-zA-Z][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
154
|
+
/**
|
|
155
|
+
* Whether a value is exactly one image token, like `{{host.logo}}`: what a
|
|
156
|
+
* picture's `src` may be when it is the installing site's own logo. It becomes
|
|
157
|
+
* that site's stored value at render, so it names nothing of the publisher's.
|
|
158
|
+
*/ export function isHostImageToken(value) {
|
|
159
|
+
var _hostTokenDefinition;
|
|
160
|
+
const keys = hostTokensIn(value);
|
|
161
|
+
return keys.length === 1 && value.replace(HOST_TOKEN_PATTERN, '').trim() === '' && ((_hostTokenDefinition = hostTokenDefinition(keys[0])) == null ? void 0 : _hostTokenDefinition.type) === 'image';
|
|
162
|
+
}
|
|
140
163
|
/** Every `host.*` token in a string, in order, including unknown ones. */ export function hostTokensIn(text) {
|
|
141
164
|
if (typeof text !== 'string' || !text.includes('{{')) return [];
|
|
142
165
|
const keys = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-tokens.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\nimport { hostPublicOrigin } from './host-naming'\n\n/**\n * Host variables — publisher templates that fill themselves in (AGL-1022).\n *\n * A publisher writing an email template cannot reference the installing site,\n * so every install begins with the same chore: swap the placeholder logo, fix\n * the business name, correct the links. Miss one and the site sends an email\n * carrying someone else's branding.\n *\n * `host.*` gives artifacts a way to NAME the host instead of hard-coding it.\n * The author writes `{{host.businessName}}`; the installing site fills it in,\n * and it stays filled in when that site rebrands.\n *\n * ## The namespace IS the security boundary\n *\n * This is publisher-authored content resolving against tenant data, so the\n * registry is CLOSED and each token reads through a hand-written function\n * rather than a path into the document. That is the whole design: a token\n * cannot address `stripeAccountId`, or `memberRoles`, or anything else, because\n * there is no path syntax to express it — `host.` is followed by a key that\n * either appears in {@link HOST_TOKENS} or resolves to nothing at all. Adding a\n * token is a deliberate edit here, reviewable as a diff.\n *\n * A path-based registry (\"allow `host.seo.title`\") would have been shorter and\n * is the version to refuse: allowlisting prefixes is how a namespace grows a\n * hole, and the first `host.orgId` would be a data leak with a plausible commit\n * message.\n *\n * ## Resolve LATE\n *\n * At render and at send, never at install. Freezing values into the artifact\n * would make a rebrand stop propagating silently — the site would keep sending\n * last year's business name from a template nobody remembers is a template.\n *\n * ## Every token defines what \"empty\" does\n *\n * A site with no logo must produce something deliberate. Never an empty image,\n * and never the literal `{{host.logo}}` reaching a visitor — a leaked token is\n * the most embarrassing possible failure for a feature whose entire purpose is\n * to stop templates looking unfinished.\n */\n\n/**\n * What a token yields, so a renderer knows how to place it and the authoring\n * surface can refuse it in the wrong slot.\n *\n * `text` is the only type safe to drop into arbitrary copy. The others carry a\n * shape a slot has to understand — an `image` in a paragraph is a URL sitting\n * in the middle of a sentence, which is exactly the \"fails in a sent email\"\n * outcome this typing exists to move to authoring time.\n */\nexport type HostTokenType = 'text' | 'url' | 'email' | 'image' | 'address'\n\n/** What a renderer should do when a token resolves to nothing. */\nexport type HostTokenEmptyBehaviour =\n /** Render nothing in its place; the surrounding copy still makes sense. */\n | 'blank'\n /**\n * The block holding it should be dropped entirely. An `<img src=\"\">` is a\n * broken-image icon in every mail client, which is worse than no logo.\n */\n | 'omit-block'\n\n/** The site fields a token may read. A closed, explicit surface. */\nexport interface HostTokenSource {\n displayName?: string\n logoUrl?: string\n subdomain?: string\n cname?: string\n /** Contact details a publisher template can reference (AGL-1022). */\n business?: {\n supportEmail?: string\n address?: string\n socialLinks?: Array<{ label?: string; url?: string }>\n }\n seo?: { entity?: { name?: string; logo?: string } }\n}\n\nexport interface HostTokenDefinition {\n /** The key after `host.` */\n key: string\n type: HostTokenType\n label: string\n description: string\n /** Reads the value. Hand-written per token — never a path into the document. */\n resolve: (host: HostTokenSource) => string | undefined\n /**\n * A deliberate second choice when `resolve` finds nothing.\n *\n * Not a nicety: \"fall back to the business name\" is what turns a missing logo\n * from a hole in the page into a wordmark, which is what a careful designer\n * would have done by hand.\n */\n fallback?: (host: HostTokenSource) => string | undefined\n whenEmpty: HostTokenEmptyBehaviour\n}\n\nconst trimmed = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text || undefined\n}\n\n/**\n * The site's public URL.\n *\n * A custom domain wins when connected, because that is the address the site is\n * actually known by — a template linking to the aglyn.app subdomain of a site\n * with its own domain is technically correct and reads as a mistake.\n */\nfunction siteUrl(host: HostTokenSource): string | undefined {\n const custom = trimmed(host.cname)\n if (custom) return `https://${custom.replace(/^https?:\\/\\//, '')}`\n // `hostPublicOrigin`, not a fourth hand-rolled apex (AGL-2195). The cname\n // branch stays here because this one strips a scheme the token source may\n // carry; the subdomain branch has no such difference and had none — it was\n // the same two lines with `aglyn.app` written in, so an operator's own apex\n // was honoured everywhere except in the tokens a template renders.\n return hostPublicOrigin({ subdomain: trimmed(host.subdomain) })\n}\n\nfunction businessName(host: HostTokenSource): string | undefined {\n return trimmed(host.seo?.entity?.name) ?? trimmed(host.displayName)\n}\n\n/**\n * The closed registry.\n *\n * Ordered as an author would look for them. Every entry is reachable from a\n * field a site can actually set — a token that can never resolve is worse than\n * an absent one, because it teaches people the feature does not work.\n */\nexport const HOST_TOKENS: Record<string, HostTokenDefinition> = {\n businessName: {\n key: 'businessName',\n type: 'text',\n label: 'Business name',\n description: 'The site’s business or brand name.',\n resolve: businessName,\n whenEmpty: 'blank',\n },\n logo: {\n key: 'logo',\n type: 'image',\n label: 'Logo',\n description: 'The site’s logo image.',\n resolve: (host) =>\n trimmed(host.logoUrl) ?? trimmed(host.seo?.entity?.logo),\n // No image: the block is dropped and the business name carries the brand\n // instead, which is what the block was for.\n whenEmpty: 'omit-block',\n },\n url: {\n key: 'url',\n type: 'url',\n label: 'Site address',\n description: 'The site’s public URL — its custom domain when connected.',\n resolve: siteUrl,\n whenEmpty: 'blank',\n },\n supportEmail: {\n key: 'supportEmail',\n type: 'email',\n label: 'Support email',\n description: 'Where visitors should write for help.',\n resolve: (host) => trimmed(host.business?.supportEmail),\n whenEmpty: 'omit-block',\n },\n address: {\n key: 'address',\n type: 'address',\n label: 'Postal address',\n description: 'The business’s postal address, as one block of text.',\n resolve: (host) => trimmed(host.business?.address),\n whenEmpty: 'omit-block',\n },\n socialLinks: {\n key: 'socialLinks',\n type: 'text',\n label: 'Social links',\n description: 'The site’s social profiles, as a list of labeled links.',\n // Rendered as text here so a token in copy degrades to something readable;\n // a renderer wanting real links reads `host.business.socialLinks` itself.\n resolve: (host) => {\n const links = host.business?.socialLinks ?? []\n const labels = links\n .map((link) => trimmed(link?.label) ?? trimmed(link?.url))\n .filter((label): label is string => Boolean(label))\n return labels.length ? labels.join(' · ') : undefined\n },\n whenEmpty: 'omit-block',\n },\n}\n\n/**\n * The registry's entry for `key`, or undefined when it declares none.\n *\n * An OWN entry only. `HOST_TOKENS` is an object literal, so a bare index also\n * finds the `Object.prototype` members — `constructor`, `toString`, `valueOf` —\n * and every one of those names matches {@link HOST_TOKEN_PATTERN}. Read that\n * way, `{{host.constructor}}` answers the `Object` function where a definition\n * belongs, and calling its `resolve` throws in the middle of a render. A name\n * the registry does not declare resolves to nothing, whatever the name is.\n */\nexport function hostTokenDefinition(\n key: string,\n): HostTokenDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(HOST_TOKENS, key)\n ? HOST_TOKENS[key]\n : undefined\n}\n\n/**\n * `{{host.key}}` — whitespace-tolerant, and deliberately NOT dotted beyond one\n * segment. `host.a.b` does not match, so there is no path syntax to abuse.\n */\nexport const HOST_TOKEN_PATTERN = /\\{\\{\\s*host\\.([a-zA-Z][a-zA-Z0-9_]*)\\s*\\}\\}/g\n\n/** Every `host.*` token in a string, in order, including unknown ones. */\nexport function hostTokensIn(text: string): string[] {\n if (typeof text !== 'string' || !text.includes('{{')) return []\n const keys: string[] = []\n for (const match of text.matchAll(HOST_TOKEN_PATTERN)) keys.push(match[1])\n return keys\n}\n\n/** The resolved value of one token, or undefined when the site has none. */\nexport function resolveHostToken(\n key: string,\n host: HostTokenSource | null | undefined,\n): string | undefined {\n const definition = hostTokenDefinition(key)\n if (!definition || !host) return undefined\n return definition.resolve(host) ?? definition.fallback?.(host)\n}\n\n/**\n * Replaces every `host.*` token with the installing site's value.\n *\n * An unresolved token — unknown key, or a site that has not set the field —\n * renders EMPTY, never the literal. Leaking `{{host.logo}}` into a sent email\n * is the failure this feature exists to prevent, so it cannot be the fallback\n * for the feature's own gaps. The authoring-time check\n * ({@link validateHostTokens}) is where a typo is meant to be caught, while the\n * writer can still fix it.\n */\nexport function resolveHostTokens(\n text: string,\n host: HostTokenSource | null | undefined,\n): string {\n if (typeof text !== 'string' || !text.includes('{{')) return text\n return text.replace(HOST_TOKEN_PATTERN, (_token, key) =>\n resolveHostToken(String(key), host) ?? '',\n )\n}\n\nexport interface HostTokenIssue {\n key: string\n /**\n * `unknown` — no such token; it will render empty.\n * `wrong-slot` — the token's type cannot be placed here.\n */\n kind: 'unknown' | 'wrong-slot'\n message: string\n}\n\n/**\n * Authoring-time check: catch a token before it reaches a visitor.\n *\n * The two things worth failing on are a typo (which would silently render\n * nothing) and a type mismatch (an image URL dropped into a sentence). Both are\n * invisible at authoring time without this and obvious afterwards, which is the\n * wrong way round for something that ends up in a sent email.\n *\n * `allow` names the types this slot can take. Omit it for free text, where only\n * `text` and the string-ish types make sense.\n */\nexport function validateHostTokens(\n text: string,\n options?: { allow?: readonly HostTokenType[] },\n): HostTokenIssue[] {\n const allow = options?.allow ?? ['text', 'url', 'email', 'address']\n const issues: HostTokenIssue[] = []\n const seen = new Set<string>()\n for (const key of hostTokensIn(text)) {\n if (seen.has(key)) continue\n seen.add(key)\n const definition = hostTokenDefinition(key)\n if (!definition) {\n issues.push({\n key,\n kind: 'unknown',\n message:\n `There is no host variable called \"${key}\". It will render as ` +\n 'nothing. Available: ' +\n Object.keys(HOST_TOKENS).join(', ') +\n '.',\n })\n continue\n }\n if (!allow.includes(definition.type)) {\n issues.push({\n key,\n kind: 'wrong-slot',\n message:\n `\"${definition.label}\" is ${\n definition.type === 'image' ? 'an image' : `a ${definition.type}`\n }, so it cannot go here. Put it in a slot that takes ` +\n `${definition.type === 'image' ? 'an image' : `a ${definition.type}`}.`,\n })\n }\n }\n return issues\n}\n\n/**\n * Whether a block containing these tokens should be dropped for this site.\n *\n * The other half of \"every token needs a defined empty behaviour\": a logo block\n * on a site with no logo should not render an empty image, and the renderer\n * cannot know that from the resolved string alone — `''` is both \"no value\" and\n * \"a value that happens to be empty\". Asking here keeps the rule with the\n * token definition rather than spread across the surfaces that render it.\n */\nexport function shouldOmitBlock(\n text: string,\n host: HostTokenSource | null | undefined,\n): boolean {\n for (const key of hostTokensIn(text)) {\n const definition = hostTokenDefinition(key)\n if (!definition || definition.whenEmpty !== 'omit-block') continue\n if (!resolveHostToken(key, host)) return true\n }\n return false\n}\n\n/**\n * Every token with its resolved value for one site, for an authoring picker\n * that shows what each will actually produce here.\n *\n * A picker listing token names alone makes the author guess; showing the value\n * this site would render is what turns \"is it businessName or business_name\"\n * into a question nobody has to ask.\n */\nexport function describeHostTokens(\n host: HostTokenSource | null | undefined,\n): Array<HostTokenDefinition & { token: string; value?: string; set: boolean }> {\n return Object.values(HOST_TOKENS).map((definition) => {\n const value = resolveHostToken(definition.key, host)\n return {\n ...definition,\n token: `{{host.${definition.key}}}`,\n value,\n set: Boolean(value),\n }\n })\n}\n\n/**\n * The `host.*` values as a merge-token map, for surfaces that already have a\n * token substituter of their own.\n *\n * This is how EMAIL gets host variables without the email library depending on\n * this one: `substituteMergeTokens` already matches `{{a.b}}`, so a caller\n * spreads this into the merge map it was passing anyway. One registry, two\n * surfaces, no second substitution engine to drift.\n *\n * Every registered token appears, INCLUDING the ones this site has not set —\n * as `''`. Omitting them would leave `{{host.supportEmail}}` for the generic\n * catch-all to blank, which works today and works by accident: the moment a\n * surface without that catch-all uses this map, the literal token ships.\n */\nexport function hostTokenMerge(\n host: HostTokenSource | null | undefined,\n): Record<string, string> {\n const merge: Record<string, string> = {}\n for (const key of Object.keys(HOST_TOKENS)) {\n merge[`host.${key}`] = resolveHostToken(key, host) ?? ''\n }\n return merge\n}\n\n/**\n * The email address a visitor writes to the business at: the one its\n * business listing shows (`seo.entity.email`), else its support email. Null\n * when the site has neither. One answer for every place that names it: the\n * waiver consent, the quote and invoice PDFs, and the Reply-To of the quote\n * email (AGL-4097). Trimmed, not validated; a caller putting it in a header\n * checks it is an address.\n */\nexport function siteContactEmail(\n host:\n | {\n seo?: { entity?: { email?: unknown } | null } | null\n business?: { supportEmail?: unknown } | null\n }\n | null\n | undefined,\n): string | null {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim().slice(0, 320) : '')\n return text(host?.seo?.entity?.email) || text(host?.business?.supportEmail) || null\n}\n\n/**\n * Resolves `host.*` tokens across every string prop of a normalized node map.\n *\n * Mirrors `resolveNodesBindings` deliberately — same shallow-copy discipline,\n * same string-props-only rule — so a screen and an email resolve the same token\n * to the same value. The issue's warning is the reason: two resolvers drift,\n * and then the same token means different things depending on where an author\n * typed it.\n *\n * Returns the input untouched when the site has nothing to substitute, so a\n * binding-free tree costs one `Object.entries`.\n */\nexport function resolveNodesHostTokens<T extends Record<string, any>>(\n nodes: T,\n host: HostTokenSource | null | undefined,\n): T {\n if (!nodes) return nodes\n let changed = false\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const props = node?.props\n if (!props) {\n next[id] = node\n continue\n }\n let nodeChanged = false\n const nextProps: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'string' && value.includes('{{')) {\n const resolved = resolveHostTokens(value, host)\n nextProps[key] = resolved\n if (resolved !== value) nodeChanged = true\n } else {\n nextProps[key] = value\n }\n }\n next[id] = nodeChanged ? { ...node, props: nextProps } : node\n changed = changed || nodeChanged\n }\n return (changed ? next : nodes) as T\n}\n"],"names":["hostPublicOrigin","trimmed","value","text","trim","undefined","siteUrl","host","custom","cname","replace","subdomain","businessName","seo","entity","name","displayName","HOST_TOKENS","key","type","label","description","resolve","whenEmpty","logo","logoUrl","url","supportEmail","business","address","socialLinks","links","labels","map","link","filter","Boolean","length","join","hostTokenDefinition","Object","prototype","hasOwnProperty","call","HOST_TOKEN_PATTERN","hostTokensIn","includes","keys","match","matchAll","push","resolveHostToken","definition","fallback","resolveHostTokens","_token","String","validateHostTokens","options","allow","issues","seen","Set","has","add","kind","message","shouldOmitBlock","describeHostTokens","values","token","set","hostTokenMerge","merge","siteContactEmail","slice","email","resolveNodesHostTokens","nodes","changed","next","id","node","entries","props","nodeChanged","nextProps","resolved"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-tokens.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\nimport { hostPublicOrigin } from './host-naming'\n\n/**\n * Host variables — publisher templates that fill themselves in (AGL-1022).\n *\n * A publisher writing an email template cannot reference the installing site,\n * so every install begins with the same chore: swap the placeholder logo, fix\n * the business name, correct the links. Miss one and the site sends an email\n * carrying someone else's branding.\n *\n * `host.*` gives artifacts a way to NAME the host instead of hard-coding it.\n * The author writes `{{host.businessName}}`; the installing site fills it in,\n * and it stays filled in when that site rebrands.\n *\n * ## The namespace IS the security boundary\n *\n * This is publisher-authored content resolving against tenant data, so the\n * registry is CLOSED and each token reads through a hand-written function\n * rather than a path into the document. That is the whole design: a token\n * cannot address `stripeAccountId`, or `memberRoles`, or anything else, because\n * there is no path syntax to express it — `host.` is followed by a key that\n * either appears in {@link HOST_TOKENS} or resolves to nothing at all. Adding a\n * token is a deliberate edit here, reviewable as a diff.\n *\n * A path-based registry (\"allow `host.seo.title`\") would have been shorter and\n * is the version to refuse: allowlisting prefixes is how a namespace grows a\n * hole, and the first `host.orgId` would be a data leak with a plausible commit\n * message.\n *\n * ## Resolve LATE\n *\n * At render and at send, never at install. Freezing values into the artifact\n * would make a rebrand stop propagating silently — the site would keep sending\n * last year's business name from a template nobody remembers is a template.\n *\n * ## Every token defines what \"empty\" does\n *\n * A site with no logo must produce something deliberate. Never an empty image,\n * and never the literal `{{host.logo}}` reaching a visitor — a leaked token is\n * the most embarrassing possible failure for a feature whose entire purpose is\n * to stop templates looking unfinished.\n */\n\n/**\n * What a token yields, so a renderer knows how to place it and the authoring\n * surface can refuse it in the wrong slot.\n *\n * `text` is the only type safe to drop into arbitrary copy. The others carry a\n * shape a slot has to understand — an `image` in a paragraph is a URL sitting\n * in the middle of a sentence, which is exactly the \"fails in a sent email\"\n * outcome this typing exists to move to authoring time.\n */\nexport type HostTokenType = 'text' | 'url' | 'email' | 'image' | 'address'\n\n/** What a renderer should do when a token resolves to nothing. */\nexport type HostTokenEmptyBehaviour =\n /** Render nothing in its place; the surrounding copy still makes sense. */\n | 'blank'\n /**\n * The block holding it should be dropped entirely. An `<img src=\"\">` is a\n * broken-image icon in every mail client, which is worse than no logo.\n */\n | 'omit-block'\n\n/** The site fields a token may read. A closed, explicit surface. */\nexport interface HostTokenSource {\n displayName?: string\n logoUrl?: string\n logoDarkUrl?: string\n subdomain?: string\n cname?: string\n /** Contact details a publisher template can reference (AGL-1022). */\n business?: {\n supportEmail?: string\n address?: string\n socialLinks?: Array<{ label?: string; url?: string }>\n }\n seo?: { entity?: { name?: string; logo?: string } }\n}\n\nexport interface HostTokenDefinition {\n /** The key after `host.` */\n key: string\n type: HostTokenType\n label: string\n description: string\n /** Reads the value. Hand-written per token — never a path into the document. */\n resolve: (host: HostTokenSource) => string | undefined\n /**\n * A deliberate second choice when `resolve` finds nothing.\n *\n * Not a nicety: \"fall back to the business name\" is what turns a missing logo\n * from a hole in the page into a wordmark, which is what a careful designer\n * would have done by hand.\n */\n fallback?: (host: HostTokenSource) => string | undefined\n whenEmpty: HostTokenEmptyBehaviour\n}\n\nconst trimmed = (value: unknown): string | undefined => {\n const text = typeof value === 'string' ? value.trim() : ''\n return text || undefined\n}\n\n/**\n * The site's public URL.\n *\n * A custom domain wins when connected, because that is the address the site is\n * actually known by — a template linking to the aglyn.app subdomain of a site\n * with its own domain is technically correct and reads as a mistake.\n */\nfunction siteUrl(host: HostTokenSource): string | undefined {\n const custom = trimmed(host.cname)\n if (custom) return `https://${custom.replace(/^https?:\\/\\//, '')}`\n // `hostPublicOrigin`, not a fourth hand-rolled apex (AGL-2195). The cname\n // branch stays here because this one strips a scheme the token source may\n // carry; the subdomain branch has no such difference and had none — it was\n // the same two lines with `aglyn.app` written in, so an operator's own apex\n // was honoured everywhere except in the tokens a template renders.\n return hostPublicOrigin({ subdomain: trimmed(host.subdomain) })\n}\n\nfunction businessName(host: HostTokenSource): string | undefined {\n return trimmed(host.seo?.entity?.name) ?? trimmed(host.displayName)\n}\n\nfunction logo(host: HostTokenSource): string | undefined {\n return trimmed(host.logoUrl) ?? trimmed(host.seo?.entity?.logo)\n}\n\n/**\n * The closed registry.\n *\n * Ordered as an author would look for them. Every entry is reachable from a\n * field a site can actually set — a token that can never resolve is worse than\n * an absent one, because it teaches people the feature does not work.\n */\nexport const HOST_TOKENS: Record<string, HostTokenDefinition> = {\n businessName: {\n key: 'businessName',\n type: 'text',\n label: 'Business name',\n description: 'The site’s business or brand name.',\n resolve: businessName,\n whenEmpty: 'blank',\n },\n logo: {\n key: 'logo',\n type: 'image',\n label: 'Logo',\n description: 'The site’s logo image.',\n resolve: logo,\n // No image: the block is dropped and the business name carries the brand\n // instead, which is what the block was for.\n whenEmpty: 'omit-block',\n },\n logoDark: {\n key: 'logoDark',\n type: 'image',\n label: 'Logo for dark backgrounds',\n description:\n 'The logo to show on a dark background. The site’s logo when it has no dark one.',\n // The pair a header toggles by color scheme (AGL-4119): a site with one\n // logo shows it in both schemes, and one with none shows neither.\n resolve: (host) => trimmed(host.logoDarkUrl) ?? logo(host),\n whenEmpty: 'omit-block',\n },\n url: {\n key: 'url',\n type: 'url',\n label: 'Site address',\n description: 'The site’s public URL — its custom domain when connected.',\n resolve: siteUrl,\n whenEmpty: 'blank',\n },\n supportEmail: {\n key: 'supportEmail',\n type: 'email',\n label: 'Support email',\n description: 'Where visitors should write for help.',\n resolve: (host) => trimmed(host.business?.supportEmail),\n whenEmpty: 'omit-block',\n },\n address: {\n key: 'address',\n type: 'address',\n label: 'Postal address',\n description: 'The business’s postal address, as one block of text.',\n resolve: (host) => trimmed(host.business?.address),\n whenEmpty: 'omit-block',\n },\n socialLinks: {\n key: 'socialLinks',\n type: 'text',\n label: 'Social links',\n description: 'The site’s social profiles, as a list of labeled links.',\n // Rendered as text here so a token in copy degrades to something readable;\n // a renderer wanting real links reads `host.business.socialLinks` itself.\n resolve: (host) => {\n const links = host.business?.socialLinks ?? []\n const labels = links\n .map((link) => trimmed(link?.label) ?? trimmed(link?.url))\n .filter((label): label is string => Boolean(label))\n return labels.length ? labels.join(' · ') : undefined\n },\n whenEmpty: 'omit-block',\n },\n}\n\n/**\n * The registry's entry for `key`, or undefined when it declares none.\n *\n * An OWN entry only. `HOST_TOKENS` is an object literal, so a bare index also\n * finds the `Object.prototype` members — `constructor`, `toString`, `valueOf` —\n * and every one of those names matches {@link HOST_TOKEN_PATTERN}. Read that\n * way, `{{host.constructor}}` answers the `Object` function where a definition\n * belongs, and calling its `resolve` throws in the middle of a render. A name\n * the registry does not declare resolves to nothing, whatever the name is.\n */\nexport function hostTokenDefinition(\n key: string,\n): HostTokenDefinition | undefined {\n return Object.prototype.hasOwnProperty.call(HOST_TOKENS, key)\n ? HOST_TOKENS[key]\n : undefined\n}\n\n/**\n * `{{host.key}}` — whitespace-tolerant, and deliberately NOT dotted beyond one\n * segment. `host.a.b` does not match, so there is no path syntax to abuse.\n */\nexport const HOST_TOKEN_PATTERN = /\\{\\{\\s*host\\.([a-zA-Z][a-zA-Z0-9_]*)\\s*\\}\\}/g\n\n/**\n * Whether a value is exactly one image token, like `{{host.logo}}`: what a\n * picture's `src` may be when it is the installing site's own logo. It becomes\n * that site's stored value at render, so it names nothing of the publisher's.\n */\nexport function isHostImageToken(value: string): boolean {\n const keys = hostTokensIn(value)\n return (\n keys.length === 1 &&\n value.replace(HOST_TOKEN_PATTERN, '').trim() === '' &&\n hostTokenDefinition(keys[0])?.type === 'image'\n )\n}\n\n/** Every `host.*` token in a string, in order, including unknown ones. */\nexport function hostTokensIn(text: string): string[] {\n if (typeof text !== 'string' || !text.includes('{{')) return []\n const keys: string[] = []\n for (const match of text.matchAll(HOST_TOKEN_PATTERN)) keys.push(match[1])\n return keys\n}\n\n/** The resolved value of one token, or undefined when the site has none. */\nexport function resolveHostToken(\n key: string,\n host: HostTokenSource | null | undefined,\n): string | undefined {\n const definition = hostTokenDefinition(key)\n if (!definition || !host) return undefined\n return definition.resolve(host) ?? definition.fallback?.(host)\n}\n\n/**\n * Replaces every `host.*` token with the installing site's value.\n *\n * An unresolved token — unknown key, or a site that has not set the field —\n * renders EMPTY, never the literal. Leaking `{{host.logo}}` into a sent email\n * is the failure this feature exists to prevent, so it cannot be the fallback\n * for the feature's own gaps. The authoring-time check\n * ({@link validateHostTokens}) is where a typo is meant to be caught, while the\n * writer can still fix it.\n */\nexport function resolveHostTokens(\n text: string,\n host: HostTokenSource | null | undefined,\n): string {\n if (typeof text !== 'string' || !text.includes('{{')) return text\n return text.replace(HOST_TOKEN_PATTERN, (_token, key) =>\n resolveHostToken(String(key), host) ?? '',\n )\n}\n\nexport interface HostTokenIssue {\n key: string\n /**\n * `unknown` — no such token; it will render empty.\n * `wrong-slot` — the token's type cannot be placed here.\n */\n kind: 'unknown' | 'wrong-slot'\n message: string\n}\n\n/**\n * Authoring-time check: catch a token before it reaches a visitor.\n *\n * The two things worth failing on are a typo (which would silently render\n * nothing) and a type mismatch (an image URL dropped into a sentence). Both are\n * invisible at authoring time without this and obvious afterwards, which is the\n * wrong way round for something that ends up in a sent email.\n *\n * `allow` names the types this slot can take. Omit it for free text, where only\n * `text` and the string-ish types make sense.\n */\nexport function validateHostTokens(\n text: string,\n options?: { allow?: readonly HostTokenType[] },\n): HostTokenIssue[] {\n const allow = options?.allow ?? ['text', 'url', 'email', 'address']\n const issues: HostTokenIssue[] = []\n const seen = new Set<string>()\n for (const key of hostTokensIn(text)) {\n if (seen.has(key)) continue\n seen.add(key)\n const definition = hostTokenDefinition(key)\n if (!definition) {\n issues.push({\n key,\n kind: 'unknown',\n message:\n `There is no host variable called \"${key}\". It will render as ` +\n 'nothing. Available: ' +\n Object.keys(HOST_TOKENS).join(', ') +\n '.',\n })\n continue\n }\n if (!allow.includes(definition.type)) {\n issues.push({\n key,\n kind: 'wrong-slot',\n message:\n `\"${definition.label}\" is ${\n definition.type === 'image' ? 'an image' : `a ${definition.type}`\n }, so it cannot go here. Put it in a slot that takes ` +\n `${definition.type === 'image' ? 'an image' : `a ${definition.type}`}.`,\n })\n }\n }\n return issues\n}\n\n/**\n * Whether a block containing these tokens should be dropped for this site.\n *\n * The other half of \"every token needs a defined empty behaviour\": a logo block\n * on a site with no logo should not render an empty image, and the renderer\n * cannot know that from the resolved string alone — `''` is both \"no value\" and\n * \"a value that happens to be empty\". Asking here keeps the rule with the\n * token definition rather than spread across the surfaces that render it.\n */\nexport function shouldOmitBlock(\n text: string,\n host: HostTokenSource | null | undefined,\n): boolean {\n for (const key of hostTokensIn(text)) {\n const definition = hostTokenDefinition(key)\n if (!definition || definition.whenEmpty !== 'omit-block') continue\n if (!resolveHostToken(key, host)) return true\n }\n return false\n}\n\n/**\n * Every token with its resolved value for one site, for an authoring picker\n * that shows what each will actually produce here.\n *\n * A picker listing token names alone makes the author guess; showing the value\n * this site would render is what turns \"is it businessName or business_name\"\n * into a question nobody has to ask.\n */\nexport function describeHostTokens(\n host: HostTokenSource | null | undefined,\n): Array<HostTokenDefinition & { token: string; value?: string; set: boolean }> {\n return Object.values(HOST_TOKENS).map((definition) => {\n const value = resolveHostToken(definition.key, host)\n return {\n ...definition,\n token: `{{host.${definition.key}}}`,\n value,\n set: Boolean(value),\n }\n })\n}\n\n/**\n * The `host.*` values as a merge-token map, for surfaces that already have a\n * token substituter of their own.\n *\n * This is how EMAIL gets host variables without the email library depending on\n * this one: `substituteMergeTokens` already matches `{{a.b}}`, so a caller\n * spreads this into the merge map it was passing anyway. One registry, two\n * surfaces, no second substitution engine to drift.\n *\n * Every registered token appears, INCLUDING the ones this site has not set —\n * as `''`. Omitting them would leave `{{host.supportEmail}}` for the generic\n * catch-all to blank, which works today and works by accident: the moment a\n * surface without that catch-all uses this map, the literal token ships.\n */\nexport function hostTokenMerge(\n host: HostTokenSource | null | undefined,\n): Record<string, string> {\n const merge: Record<string, string> = {}\n for (const key of Object.keys(HOST_TOKENS)) {\n merge[`host.${key}`] = resolveHostToken(key, host) ?? ''\n }\n return merge\n}\n\n/**\n * The email address a visitor writes to the business at: the one its\n * business listing shows (`seo.entity.email`), else its support email. Null\n * when the site has neither. One answer for every place that names it: the\n * waiver consent, the quote and invoice PDFs, and the Reply-To of the quote\n * email (AGL-4097). Trimmed, not validated; a caller putting it in a header\n * checks it is an address.\n */\nexport function siteContactEmail(\n host:\n | {\n seo?: { entity?: { email?: unknown } | null } | null\n business?: { supportEmail?: unknown } | null\n }\n | null\n | undefined,\n): string | null {\n const text = (value: unknown) => (typeof value === 'string' ? value.trim().slice(0, 320) : '')\n return text(host?.seo?.entity?.email) || text(host?.business?.supportEmail) || null\n}\n\n/**\n * Resolves `host.*` tokens across every string prop of a normalized node map.\n *\n * Mirrors `resolveNodesBindings` deliberately — same shallow-copy discipline,\n * same string-props-only rule — so a screen and an email resolve the same token\n * to the same value. The issue's warning is the reason: two resolvers drift,\n * and then the same token means different things depending on where an author\n * typed it.\n *\n * Returns the input untouched when the site has nothing to substitute, so a\n * binding-free tree costs one `Object.entries`.\n */\nexport function resolveNodesHostTokens<T extends Record<string, any>>(\n nodes: T,\n host: HostTokenSource | null | undefined,\n): T {\n if (!nodes) return nodes\n let changed = false\n const next: Record<string, any> = {}\n for (const [id, node] of Object.entries(nodes)) {\n const props = node?.props\n if (!props) {\n next[id] = node\n continue\n }\n let nodeChanged = false\n const nextProps: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (typeof value === 'string' && value.includes('{{')) {\n const resolved = resolveHostTokens(value, host)\n nextProps[key] = resolved\n if (resolved !== value) nodeChanged = true\n } else {\n nextProps[key] = value\n }\n }\n next[id] = nodeChanged ? { ...node, props: nextProps } : node\n changed = changed || nodeChanged\n }\n return (changed ? next : nodes) as T\n}\n"],"names":["hostPublicOrigin","trimmed","value","text","trim","undefined","siteUrl","host","custom","cname","replace","subdomain","businessName","seo","entity","name","displayName","logo","logoUrl","HOST_TOKENS","key","type","label","description","resolve","whenEmpty","logoDark","logoDarkUrl","url","supportEmail","business","address","socialLinks","links","labels","map","link","filter","Boolean","length","join","hostTokenDefinition","Object","prototype","hasOwnProperty","call","HOST_TOKEN_PATTERN","isHostImageToken","keys","hostTokensIn","includes","match","matchAll","push","resolveHostToken","definition","fallback","resolveHostTokens","_token","String","validateHostTokens","options","allow","issues","seen","Set","has","add","kind","message","shouldOmitBlock","describeHostTokens","values","token","set","hostTokenMerge","merge","siteContactEmail","slice","email","resolveNodesHostTokens","nodes","changed","next","id","node","entries","props","nodeChanged","nextProps","resolved"],"mappings":"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"}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* marketplace re-exports it under the names its publishers use.
|
|
27
27
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
28
28
|
import { matchComponentPropToken } from "./compose-reusable-components.js";
|
|
29
|
+
import { isHostImageToken } from "./host-tokens.js";
|
|
29
30
|
import { isSafeNodeUrl, SAFE_SRC_PATTERN } from "./node-url-policy.js";
|
|
30
31
|
import { SAFE_HREF_PATTERN } from "./screen-link-value.js";
|
|
31
32
|
/**
|
|
@@ -200,6 +201,9 @@ const KEPT_NODE_KEYS = [
|
|
|
200
201
|
* is published, and whatever value a page sets where the graft substitutes
|
|
201
202
|
* it (`resolveComponentPropTokens`). A token that is only part of the value,
|
|
202
203
|
* or names a property of another kind or none, is refused as before.
|
|
204
|
+
* - A `src` that is exactly one image `{{host.*}}` token is kept too (AGL-4119):
|
|
205
|
+
* it is the installing site's own logo, read from its own document at render
|
|
206
|
+
* (`isHostImageToken`), so a new site's header carries it from a starter.
|
|
203
207
|
*
|
|
204
208
|
* Deliberately shallow: only top-level props are spread onto the DOM. A nested
|
|
205
209
|
* object (`icon: { path }`) is consumed by the component, never spread, so
|
|
@@ -212,7 +216,7 @@ const KEPT_NODE_KEYS = [
|
|
|
212
216
|
if ((key === 'href' || key === 'src') && typeof value === 'string') {
|
|
213
217
|
const trimmed = value.trim();
|
|
214
218
|
const bound = matchComponentPropToken(trimmed);
|
|
215
|
-
const keep = isSafeNodeUrl(key, trimmed) || bound !== null && urlBindings.get(bound) === key;
|
|
219
|
+
const keep = isSafeNodeUrl(key, trimmed) || bound !== null && urlBindings.get(bound) === key || key === 'src' && isHostImageToken(trimmed);
|
|
216
220
|
if (keep) safe[key] = trimmed;
|
|
217
221
|
continue;
|
|
218
222
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/node-definition-sanitizer.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 node-definition sanitizer and the component allowlists (AGL-2939):\n * what a PORTABLE node tree must satisfy — one that lands on a canvas that did\n * not author it. Today that is a tree installed from another workspace and a\n * tree a model generated, and the rule is the same for both: whatever reaches\n * the canvas renders in a site whose author never saw it.\n *\n * The platform's rule, so named for what it guards rather than for who asks\n * (AGL-3080): any plugin that brings a tree in from elsewhere passes it\n * through here, and a plugin may not import another plugin's model. The\n * marketplace re-exports it under the names its publishers use.\n */\n\nimport { isFirstPartyMediaSrc } from './media-ref'\nimport { matchComponentPropToken } from './compose-reusable-components'\nimport {\n isSafeNodeUrl,\n type NodeUrlProp,\n SAFE_SRC_PATTERN,\n} from './node-url-policy'\nimport { SAFE_HREF_PATTERN } from './screen-link-value'\n\n/**\n * Component ids a portable tree may hold. Mirrors the persisted ids in\n * plugins-mui (plugin.spec.ts) minus `reusableInstance` — nested\n * instances would smuggle references to another tenant's private\n * definitions — and minus `layoutSlot`, which is layout chrome. Keep sorted.\n *\n * The rule this list encodes is \"inert and self-contained\": an element that\n * renders from its own props travels to another workspace intact. What stays\n * out is what cannot — raw-HTML escape hatches (`customHtml`), references to\n * another tenant's documents (`reusableInstance`), third-party code\n * (`plugin`), host chrome (`layoutSlot`) and anything bound to the source\n * site's data (`collection`, `product`).\n *\n * It must cover everything the besigner's palette offers under those terms, or\n * the catalogue promises what the exporter refuses — which is how `section`\n * came to be missing (AGL-1033): every Sections & Blocks preset composes it,\n * so the whole category was unpublishable. `blocks-publishable.spec.ts` walks\n * the presets and fails if this list falls behind them again.\n */\nexport const PORTABLE_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'form',\n 'formField',\n 'image',\n 'muiAppBar',\n 'muiButton',\n // MUI's form elements, one element each (they replace the all-in-one formField).\n 'muiCheckbox',\n 'muiDatePicker',\n 'muiFormControl',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiFormLabel',\n 'muiInput',\n 'muiRadio',\n 'muiRadioGroup',\n 'muiRating',\n 'muiSelect',\n 'muiSelectOption',\n 'muiTimePicker',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiListItemText',\n 'muiScreenLink',\n 'muiStack',\n 'muiToolbar',\n 'muiTypography',\n 'searchBox',\n // A semantic wrapper — `<section>`/`<footer>`/`<nav>` plus styling, with no\n // behaviour and no binding to the site it came from.\n 'section',\n 'socialLinks',\n 'videoEmbed',\n]\n\n/**\n * Email block ids publishable as an `emailTemplate` (AGL-657).\n *\n * A separate list from the page allowlist because the two vocabularies don't\n * overlap — an email design is built entirely from `plugins-email` blocks, and\n * page components (MUI, forms, video) don't survive an email client anyway.\n *\n * `emailHtml` is excluded deliberately, for the same reason `reusableInstance`\n * is excluded above: it is a raw-HTML escape hatch, and a published template\n * lands in another org's OUTGOING CUSTOMER EMAIL. Email clients don't run\n * scripts, so this isn't XSS — it's that arbitrary markup sent from someone\n * else's domain is a phishing and tracking-pixel vector that no amount of\n * render-time sanitization makes reviewable. Keep sorted.\n */\nexport const PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'emailButton',\n 'emailDivider',\n 'emailImage',\n 'emailProduct',\n 'emailRichtext',\n 'emailSection',\n 'emailSpacer',\n 'emailText',\n]\n\n/**\n * Email block ids publishable as an `emailStarter` — the transactional list\n * minus `emailRichtext`.\n *\n * Derived rather than written out, so a block added to the list above cannot\n * silently miss this one. What it subtracts is the reason it exists.\n *\n * `emailRichtext` holds its content in an `html` prop, and the email renderer's\n * `sanitize` hook defaults to identity with no production caller supplying one\n * — so that prop reaches the recipient's inbox exactly as authored. On a\n * transactional design that is the site's own author writing their own mail. In\n * a STARTER it is a stranger's markup mailed from the shared sending domain\n * under the tenant's From header, which is the raw-HTML escape hatch\n * `emailHtml` was excluded for, wearing a friendlier block name. A remote\n * `<img>` inside it is a tracking pixel that the src policy on `emailImage`\n * would otherwise have caught.\n *\n * Subtracting the block is deliberate rather than sanitizing its markup: an\n * allowlist over someone else's HTML is a judgment we would have to keep\n * re-making, and everything rich text can express in an email — a heading, a\n * paragraph, a link, a button — the remaining blocks already express as a\n * structured tree we render ourselves.\n */\nexport const PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST: readonly string[] =\n PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST.filter(\n (componentId) => componentId !== 'emailRichtext',\n )\n\n/** Serialized definition size cap (Firestore doc limit is 1 MiB). */\nexport const PORTABLE_DEFINITION_MAX_BYTES = 200 * 1024\n\nconst KEPT_NODE_KEYS = [\n '$id',\n 'componentId',\n 'pluginId',\n 'parentId',\n 'props',\n 'nodes',\n] as const\n\n/**\n * The address rules a published node's `href` and `src` meet — the linking\n * elements' navigable protocols, and https, inline or site-relative images\n * (see `node-url-policy`).\n *\n * Exported under these names for a property sanitizer (the marketplace's is\n * one), which holds a portable property's Link default to the same rule as a\n * portable node's `href`, and an Image default to the `src` rule.\n */\nexport {\n SAFE_HREF_PATTERN as PORTABLE_SAFE_HREF,\n SAFE_SRC_PATTERN as PORTABLE_SAFE_SRC,\n}\n\n/**\n * The property kind whose value may fill each address prop through a binding\n * (AGL-2933): a Link feeds an `href`, an Image a `src`. Each kind's default is\n * held to that prop's own rule wherever a declaration is published, so the\n * binding carries nothing a literal in the same place could not.\n */\nconst URL_PROP_KIND: Readonly<Record<NodeUrlProp, string>> = {\n href: 'href',\n src: 'image',\n}\n\n/**\n * Declared property names by the address prop their kind may fill — the\n * index {@link sanitizePublishedNodeProps} reads a binding against.\n */\nfunction urlBindingsOf(\n declaredProps: ReadonlyArray<{ name?: unknown; type?: unknown }> | undefined,\n): ReadonlyMap<string, NodeUrlProp> {\n const bindings = new Map<string, NodeUrlProp>()\n for (const prop of declaredProps ?? []) {\n if (typeof prop?.name !== 'string' || !prop.name) continue\n if (prop.type === URL_PROP_KIND.href) bindings.set(prop.name, 'href')\n else if (prop.type === URL_PROP_KIND.src) bindings.set(prop.name, 'src')\n }\n return bindings\n}\n\n/**\n * Strips props a published node must never carry into someone else's site\n * (AGL-784).\n *\n * `Leaf` spreads a node's props straight onto the rendered component, and MUI\n * passes unknown props through to its root DOM element, so whatever survives\n * publishing reaches the DOM of every org that installs the listing. Marketplace\n * components are auto-listed with no review, and `hosts/{h}/components` is\n * writable by any org member, so a hand-crafted doc is a realistic input here.\n *\n * What each stripped key actually does, measured rather than assumed:\n * - `dangerouslySetInnerHTML` is NOT the stored-XSS it looks like — `Leaf`\n * always passes a children array, so React throws\n * \"Can only set one of `children` or `props.dangerouslySetInnerHTML`\"\n * (and the void-element variant for self-closing components like `image`).\n * That is worse in practice than it sounds: the throw happens during SSR,\n * which is the AGL-579 failure mode that 500s the page and wedges ISR for\n * the whole site. One published component would take down every consumer.\n * - `on*` handlers can only survive JSON as strings, which React drops with a\n * warning. Removed for hygiene, and so a future renderer that does eval-ish\n * prop handling can't turn them back into a vector.\n * - `href`/`src` get the render-time URL policy applied at publish time too.\n * React already neutralizes `javascript:` hrefs and the mui components run\n * SAFE_HREF themselves, so this is defense in depth for any component that\n * forgets to — cheap, and it keeps the stored artifact honest.\n * - An `href`/`src` that is exactly one `{{prop.*}}` token naming a declared\n * property of the kind that holds that address — a Link for `href`, an\n * Image for `src` — is kept as the binding it is (AGL-2933). The token is no\n * address, so the rule above would refuse it, and every link and image a\n * component's properties feed would arrive unbound. What it becomes is held\n * to the same rule twice over: the property's default where the declaration\n * is published, and whatever value a page sets where the graft substitutes\n * it (`resolveComponentPropTokens`). A token that is only part of the value,\n * or names a property of another kind or none, is refused as before.\n *\n * Deliberately shallow: only top-level props are spread onto the DOM. A nested\n * object (`icon: { path }`) is consumed by the component, never spread, so\n * recursing would strip legitimate data for no security gain.\n */\nfunction sanitizePublishedNodeProps(\n props: Record<string, unknown>,\n urlBindings: ReadonlyMap<string, NodeUrlProp>,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (key === 'dangerouslySetInnerHTML') continue\n if (/^on[A-Z]/.test(key)) continue\n if ((key === 'href' || key === 'src') && typeof value === 'string') {\n const trimmed = value.trim()\n const bound = matchComponentPropToken(trimmed)\n const keep =\n isSafeNodeUrl(key, trimmed) ||\n (bound !== null && urlBindings.get(bound) === key)\n if (keep) safe[key] = trimmed\n continue\n }\n safe[key] = value\n }\n return safe\n}\n\n/**\n * `nodes` that is still in one of its STORAGE forms rather than a node map\n * (AGL-1395).\n *\n * The two forms `decodeStoredNodes` knows about, tested structurally so this\n * stays a model function with no server import: a byte view — what\n * firebase-admin hands back for a `Bytes` field — and the `{type, data}`\n * envelope `JSON.stringify` makes of a Node `Buffer`, which is what a\n * site-export bundle carries. Neither is a map, and neither is anything the\n * publisher did.\n *\n * The envelope test is deliberately exact, for the reason `decodeStoredNodes`\n * spells out: the alternative reading is a node map with nodes called `type`\n * and `data`, which cannot exist because node-map values are objects.\n */\nfunction isUndecodedNodes(nodes: unknown): boolean {\n if (ArrayBuffer.isView(nodes)) return true\n if (typeof nodes !== 'object' || nodes === null || Array.isArray(nodes)) {\n return false\n }\n const value = nodes as { type?: unknown; data?: unknown }\n return (\n value.type === 'Buffer' &&\n Array.isArray(value.data) &&\n Object.keys(value).length === 2\n )\n}\n\nexport type PortableDefinitionNodes = Record<\n string,\n {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n props?: Record<string, unknown>\n nodes?: string[]\n }\n>\n\n/**\n * Validates and strips a host component definition for publishing:\n * - only allowlisted component ids (no reusable instances, no layout chrome)\n * - only the persisted node keys (drops runtime fields like resolvedProps)\n * - the subtree reachable from `rootId` only, and a serialized size cap\n *\n * - per-node prop hardening (see `sanitizePublishedNodeProps`, AGL-784)\n *\n * XSS note: rich-text `html` props stay as-authored here — they are sanitized\n * at render time (sanitize-rich-text allowlist), which also covers definitions\n * written to Firestore directly (see docs/SECURITY_CONTENT_REVIEW.md). Props\n * that are spread onto the DOM (`dangerouslySetInnerHTML`, `on*`, `href`,\n * `src`) are hardened HERE as well, because publishing hands them to a\n * different org's render tree.\n */\nexport function sanitizePortableDefinition(\n definition: {\n rootId: string\n nodes: Record<string, any>\n },\n options?: {\n /**\n * Additional component ids permitted for this artifact type.\n *\n * `layoutSlot` is excluded from the shared allowlist because a slot in\n * page content has nowhere to graft — but a published LAYOUT is\n * meaningless without one (AGL-671). Scoped per call rather than added\n * globally so page and component publishing stay unchanged.\n */\n extraComponentIds?: readonly string[]\n /**\n * Replaces the page allowlist outright, for artifact types built from a\n * disjoint component vocabulary — an email design uses `plugins-email`\n * blocks and nothing else (AGL-657), so EXTENDING the page list would\n * green-light a `videoEmbed` or `form` that no email client can render.\n */\n componentIds?: readonly string[]\n /**\n * The properties the tree declares, as they are published beside it\n * (AGL-2933). A node's `href` or `src` bound whole to one of the kind that\n * holds that address is kept — see `sanitizePublishedNodeProps`. Absent\n * for a tree that declares none, which keeps no binding.\n */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n },\n):\n | { ok: true; rootId: string; nodes: PortableDefinitionNodes }\n | { ok: false; error: string } {\n const { rootId, nodes } = definition\n const base = options?.componentIds ?? PORTABLE_COMPONENT_ID_ALLOWLIST\n const allowed = options?.extraComponentIds?.length\n ? [...base, ...options.extraComponentIds]\n : base\n const urlBindings = urlBindingsOf(options?.declaredProps)\n // An UNDECODED `nodes` is never the author's fault, so it must not share a\n // message with a genuinely rootless definition (AGL-1395). Both undecoded\n // forms — a Node `Buffer` from the Admin SDK, and the `{type:'Buffer'}`\n // envelope `JSON.stringify` makes of one — reach the root check below as\n // something with no `_@_` in it, and the answer was \"Definition has no root\n // node\": a sentence that sends the publisher to redesign a page that is\n // fine. Callers must run `decodeStoredNodes` first; this says so out loud\n // rather than letting the next raw read look like a content problem.\n if (isUndecodedNodes(nodes)) {\n return {\n ok: false,\n error:\n 'This content could not be read — it is stored compressed and was ' +\n 'not decoded before publishing. That is a bug on our side, not a ' +\n 'problem with your design.',\n }\n }\n if (!rootId || !nodes?.[rootId]) {\n return { ok: false, error: 'Definition has no root node' }\n }\n const sanitized: PortableDefinitionNodes = {}\n /** Set while walking; see the empty-definition check after the loop. */\n let rootIsWrapper = false\n const queue = [rootId]\n while (queue.length) {\n const id = queue.shift() as string\n if (sanitized[id]) continue\n const node = nodes[id]\n if (!node) return { ok: false, error: `Missing node \"${id}\"` }\n // The root node is the virtual root-collection wrapper (canvas\n // `NODE_ROOT_ID` = `_@_`): it declares \"everything inside <body>\" for\n // drag/drop mapping, not a rendered component, so a `div`/absent\n // componentId there is structural — not a real component to allowlist\n // (AGL-783). Exempt ONLY that wrapper shape; a root carrying a real\n // component id is still checked, so a disallowed component can't be\n // smuggled in as the root. Every descendant is real content and always\n // checked below.\n const isRootWrapper =\n id === rootId &&\n (node.componentId == null ||\n node.componentId === '' ||\n node.componentId === 'div')\n if (!isRootWrapper && !allowed.includes(node.componentId)) {\n return {\n ok: false,\n // Says why, not just no (AGL-1033). The bare id was doubly unhelpful:\n // it is not what the palette calls the element, and it left the author\n // guessing whether this was a bug or a rule.\n error:\n `Component \"${node.componentId}\" cannot be published — a listing may ` +\n 'only contain self-contained presentational elements. Raw HTML, ' +\n 'reusable-component references, plugin elements and anything bound ' +\n \"to this site's data or layout stay behind.\",\n }\n }\n const plain: any = {}\n for (const key of KEPT_NODE_KEYS) {\n if (node[key] !== undefined) plain[key] = node[key]\n }\n // Per-node prop hardening (AGL-784) — see sanitizePublishedNodeProps.\n if (plain.props && typeof plain.props === 'object') {\n plain.props = sanitizePublishedNodeProps(plain.props, urlBindings)\n }\n plain.$id = id\n plain.parentId = id === rootId ? null : (node.parentId ?? null)\n // Give the wrapper an explicit container id so the installed definition\n // renders its root collection the same way it did on the source site.\n if (isRootWrapper) {\n plain.componentId = 'div'\n rootIsWrapper = true\n }\n sanitized[id] = plain\n if (Array.isArray(node.nodes)) queue.push(...node.nodes)\n }\n let serialized: string\n try {\n serialized = JSON.stringify(sanitized)\n } catch {\n return { ok: false, error: 'Definition is not serializable' }\n }\n if (serialized.length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n // An empty definition is not a listing (AGL-1033). Publishing one used to\n // SUCCEED — the root wrapper alone sanitizes cleanly — so a component whose\n // content had never been published to its document shipped a blank version\n // to everyone who installed it, and said nothing. Refusing here is the\n // \"fail loudly at the point the author understands it\" half.\n //\n // Only the WRAPPER case: a definition that is a single real component is a\n // perfectly good one-element listing, and refusing that would be a different\n // bug in the other direction.\n if (rootIsWrapper && Object.keys(sanitized).length <= 1) {\n return {\n ok: false,\n error:\n 'There is nothing to publish — this is empty. If you have edited it ' +\n 'in the designer, publish those changes first, then publish the listing.',\n }\n }\n return { ok: true, rootId, nodes: sanitized }\n}\n\n/**\n * Converts a sanitized portable definition (normalized map) into the\n * nested node shape `canvas.addNodeFromPreset` grafts — ids regenerate on\n * insert, so collisions with existing canvas nodes are impossible\n * (AGL-169). A seen-set guards malformed self-referencing trees.\n */\nexport function portableDefinitionToNested(\n rootId: string,\n nodes: PortableDefinitionNodes,\n): Record<string, unknown> | null {\n const seen = new Set<string>()\n const build = (id: string): Record<string, unknown> | null => {\n const node = nodes[id]\n if (!node || seen.has(id)) return null\n seen.add(id)\n return {\n componentId: node.componentId,\n ...(node.pluginId ? { pluginId: node.pluginId } : {}),\n ...(node.props ? { props: node.props } : {}),\n nodes: (node.nodes ?? [])\n .map(build)\n .filter((child): child is Record<string, unknown> => Boolean(child)),\n }\n }\n return build(rootId)\n}\n\n/**\n * Field types a portable dataset schema may declare (AGL-657). Mirrors\n * `DATASET_FIELD_TYPES` in core; duplicated rather than imported to keep this\n * module dependency-free (it is imported by API routes and client components\n * alike), and asserted against the source of truth in the spec.\n */\nexport const PORTABLE_DATASET_FIELD_TYPES: readonly string[] = [\n 'bool',\n 'bytes',\n 'coordinates',\n 'float',\n 'int32',\n 'int64',\n 'map',\n 'nil',\n 'reference',\n 'sorted',\n 'text',\n 'timestamp',\n]\n\n"],"names":["matchComponentPropToken","isSafeNodeUrl","SAFE_SRC_PATTERN","SAFE_HREF_PATTERN","PORTABLE_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","filter","componentId","PORTABLE_DEFINITION_MAX_BYTES","KEPT_NODE_KEYS","PORTABLE_SAFE_HREF","PORTABLE_SAFE_SRC","URL_PROP_KIND","href","src","urlBindingsOf","declaredProps","bindings","Map","prop","name","type","set","sanitizePublishedNodeProps","props","urlBindings","safe","key","value","Object","entries","test","trimmed","trim","bound","keep","get","isUndecodedNodes","nodes","ArrayBuffer","isView","Array","isArray","data","keys","length","sanitizePortableDefinition","definition","options","rootId","base","componentIds","allowed","extraComponentIds","ok","error","sanitized","rootIsWrapper","queue","node","id","shift","isRootWrapper","includes","plain","undefined","$id","parentId","push","serialized","JSON","stringify","portableDefinitionToNested","seen","Set","build","has","add","pluginId","map","child","Boolean","PORTABLE_DATASET_FIELD_TYPES"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC;AAGD,SAASA,uBAAuB,QAAQ,mCAA+B;AACvE,SACEC,aAAa,EAEbC,gBAAgB,QACX,uBAAmB;AAC1B,SAASC,iBAAiB,QAAQ,yBAAqB;AAEvD;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,MAAMC,kCAAqD;IAChE;IACA;IACA;IACA;IACA;IACA,iFAAiF;IACjF;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,4EAA4E;IAC5E,qDAAqD;IACrD;IACA;IACA;CACD,CAAA;AAED;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,wCAA2D;IACtE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,MAAMC,gDACXD,sCAAsCE,MAAM,CAC1C,CAACC,cAAgBA,gBAAgB,iBAClC;AAEH,mEAAmE,GACnE,OAAO,MAAMC,gCAAgC,MAAM,KAAI;AAEvD,MAAMC,iBAAiB;IACrB;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;;;;;CAQC,GACD,SACEP,qBAAqBQ,kBAAkB,EACvCT,oBAAoBU,iBAAiB,KACtC;AAED;;;;;CAKC,GACD,MAAMC,gBAAuD;IAC3DC,MAAM;IACNC,KAAK;AACP;AAEA;;;CAGC,GACD,SAASC,cACPC,aAA4E;IAE5E,MAAMC,WAAW,IAAIC;IACrB,KAAK,MAAMC,QAAQH,wBAAAA,gBAAiB,EAAE,CAAE;QACtC,IAAI,QAAOG,wBAAAA,KAAMC,IAAI,MAAK,YAAY,CAACD,KAAKC,IAAI,EAAE;QAClD,IAAID,KAAKE,IAAI,KAAKT,cAAcC,IAAI,EAAEI,SAASK,GAAG,CAACH,KAAKC,IAAI,EAAE;aACzD,IAAID,KAAKE,IAAI,KAAKT,cAAcE,GAAG,EAAEG,SAASK,GAAG,CAACH,KAAKC,IAAI,EAAE;IACpE;IACA,OAAOH;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,SAASM,2BACPC,KAA8B,EAC9BC,WAA6C;IAE7C,MAAMC,OAAgC,CAAC;IACvC,KAAK,MAAM,CAACC,KAAKC,MAAM,IAAIC,OAAOC,OAAO,CAACN,OAAQ;QAChD,IAAIG,QAAQ,2BAA2B;QACvC,IAAI,WAAWI,IAAI,CAACJ,MAAM;QAC1B,IAAI,AAACA,CAAAA,QAAQ,UAAUA,QAAQ,KAAI,KAAM,OAAOC,UAAU,UAAU;YAClE,MAAMI,UAAUJ,MAAMK,IAAI;YAC1B,MAAMC,QAAQnC,wBAAwBiC;YACtC,MAAMG,OACJnC,cAAc2B,KAAKK,YAClBE,UAAU,QAAQT,YAAYW,GAAG,CAACF,WAAWP;YAChD,IAAIQ,MAAMT,IAAI,CAACC,IAAI,GAAGK;YACtB;QACF;QACAN,IAAI,CAACC,IAAI,GAAGC;IACd;IACA,OAAOF;AACT;AAEA;;;;;;;;;;;;;;CAcC,GACD,SAASW,iBAAiBC,KAAc;IACtC,IAAIC,YAAYC,MAAM,CAACF,QAAQ,OAAO;IACtC,IAAI,OAAOA,UAAU,YAAYA,UAAU,QAAQG,MAAMC,OAAO,CAACJ,QAAQ;QACvE,OAAO;IACT;IACA,MAAMV,QAAQU;IACd,OACEV,MAAMP,IAAI,KAAK,YACfoB,MAAMC,OAAO,CAACd,MAAMe,IAAI,KACxBd,OAAOe,IAAI,CAAChB,OAAOiB,MAAM,KAAK;AAElC;AAcA;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,2BACdC,UAGC,EACDC,OAwBC;;QAMeA;IAFhB,MAAM,EAAEC,MAAM,EAAEX,KAAK,EAAE,GAAGS;IAC1B,MAAMG,eAAOF,2BAAAA,QAASG,YAAY,mBAAIhD;IACtC,MAAMiD,UAAUJ,CAAAA,4BAAAA,6BAAAA,QAASK,iBAAiB,qBAA1BL,2BAA4BH,MAAM,IAC9C;WAAIK;WAASF,QAAQK,iBAAiB;KAAC,GACvCH;IACJ,MAAMzB,cAAcV,cAAciC,2BAAAA,QAAShC,aAAa;IACxD,2EAA2E;IAC3E,0EAA0E;IAC1E,wEAAwE;IACxE,yEAAyE;IACzE,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,qEAAqE;IACrE,IAAIqB,iBAAiBC,QAAQ;QAC3B,OAAO;YACLgB,IAAI;YACJC,OACE,sEACA,qEACA;QACJ;IACF;IACA,IAAI,CAACN,UAAU,EAACX,yBAAAA,KAAO,CAACW,OAAO,GAAE;QAC/B,OAAO;YAAEK,IAAI;YAAOC,OAAO;QAA8B;IAC3D;IACA,MAAMC,YAAqC,CAAC;IAC5C,sEAAsE,GACtE,IAAIC,gBAAgB;IACpB,MAAMC,QAAQ;QAACT;KAAO;IACtB,MAAOS,MAAMb,MAAM,CAAE;YAwCsBc;QAvCzC,MAAMC,KAAKF,MAAMG,KAAK;QACtB,IAAIL,SAAS,CAACI,GAAG,EAAE;QACnB,MAAMD,OAAOrB,KAAK,CAACsB,GAAG;QACtB,IAAI,CAACD,MAAM,OAAO;YAAEL,IAAI;YAAOC,OAAO,CAAC,cAAc,EAAEK,GAAG,CAAC,CAAC;QAAC;QAC7D,+DAA+D;QAC/D,sEAAsE;QACtE,iEAAiE;QACjE,sEAAsE;QACtE,oEAAoE;QACpE,oEAAoE;QACpE,uEAAuE;QACvE,iBAAiB;QACjB,MAAME,gBACJF,OAAOX,UACNU,CAAAA,KAAKpD,WAAW,IAAI,QACnBoD,KAAKpD,WAAW,KAAK,MACrBoD,KAAKpD,WAAW,KAAK,KAAI;QAC7B,IAAI,CAACuD,iBAAiB,CAACV,QAAQW,QAAQ,CAACJ,KAAKpD,WAAW,GAAG;YACzD,OAAO;gBACL+C,IAAI;gBACJ,sEAAsE;gBACtE,uEAAuE;gBACvE,6CAA6C;gBAC7CC,OACE,CAAC,WAAW,EAAEI,KAAKpD,WAAW,CAAC,sCAAsC,CAAC,GACtE,oEACA,uEACA;YACJ;QACF;QACA,MAAMyD,QAAa,CAAC;QACpB,KAAK,MAAMrC,OAAOlB,eAAgB;YAChC,IAAIkD,IAAI,CAAChC,IAAI,KAAKsC,WAAWD,KAAK,CAACrC,IAAI,GAAGgC,IAAI,CAAChC,IAAI;QACrD;QACA,sEAAsE;QACtE,IAAIqC,MAAMxC,KAAK,IAAI,OAAOwC,MAAMxC,KAAK,KAAK,UAAU;YAClDwC,MAAMxC,KAAK,GAAGD,2BAA2ByC,MAAMxC,KAAK,EAAEC;QACxD;QACAuC,MAAME,GAAG,GAAGN;QACZI,MAAMG,QAAQ,GAAGP,OAAOX,SAAS,QAAQU,iBAAAA,KAAKQ,QAAQ,YAAbR,iBAAiB;QAC1D,wEAAwE;QACxE,sEAAsE;QACtE,IAAIG,eAAe;YACjBE,MAAMzD,WAAW,GAAG;YACpBkD,gBAAgB;QAClB;QACAD,SAAS,CAACI,GAAG,GAAGI;QAChB,IAAIvB,MAAMC,OAAO,CAACiB,KAAKrB,KAAK,GAAGoB,MAAMU,IAAI,IAAIT,KAAKrB,KAAK;IACzD;IACA,IAAI+B;IACJ,IAAI;QACFA,aAAaC,KAAKC,SAAS,CAACf;IAC9B,EAAE,eAAM;QACN,OAAO;YAAEF,IAAI;YAAOC,OAAO;QAAiC;IAC9D;IACA,IAAIc,WAAWxB,MAAM,GAAGrC,+BAA+B;QACrD,OAAO;YAAE8C,IAAI;YAAOC,OAAO;QAAqC;IAClE;IACA,0EAA0E;IAC1E,4EAA4E;IAC5E,2EAA2E;IAC3E,uEAAuE;IACvE,6DAA6D;IAC7D,EAAE;IACF,2EAA2E;IAC3E,6EAA6E;IAC7E,8BAA8B;IAC9B,IAAIE,iBAAiB5B,OAAOe,IAAI,CAACY,WAAWX,MAAM,IAAI,GAAG;QACvD,OAAO;YACLS,IAAI;YACJC,OACE,wEACA;QACJ;IACF;IACA,OAAO;QAAED,IAAI;QAAML;QAAQX,OAAOkB;IAAU;AAC9C;AAEA;;;;;CAKC,GACD,OAAO,SAASgB,2BACdvB,MAAc,EACdX,KAA8B;IAE9B,MAAMmC,OAAO,IAAIC;IACjB,MAAMC,QAAQ,CAACf;YAQHD;QAPV,MAAMA,OAAOrB,KAAK,CAACsB,GAAG;QACtB,IAAI,CAACD,QAAQc,KAAKG,GAAG,CAAChB,KAAK,OAAO;QAClCa,KAAKI,GAAG,CAACjB;QACT,OAAO;YACLrD,aAAaoD,KAAKpD,WAAW;WACzBoD,KAAKmB,QAAQ,GAAG;YAAEA,UAAUnB,KAAKmB,QAAQ;QAAC,IAAI,CAAC,GAC/CnB,KAAKnC,KAAK,GAAG;YAAEA,OAAOmC,KAAKnC,KAAK;QAAC,IAAI,CAAC;YAC1Cc,OAAO,EAACqB,cAAAA,KAAKrB,KAAK,YAAVqB,cAAc,EAAE,EACrBoB,GAAG,CAACJ,OACJrE,MAAM,CAAC,CAAC0E,QAA4CC,QAAQD;;IAEnE;IACA,OAAOL,MAAM1B;AACf;AAEA;;;;;CAKC,GACD,OAAO,MAAMiC,+BAAkD;IAC7D;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/node-definition-sanitizer.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 node-definition sanitizer and the component allowlists (AGL-2939):\n * what a PORTABLE node tree must satisfy — one that lands on a canvas that did\n * not author it. Today that is a tree installed from another workspace and a\n * tree a model generated, and the rule is the same for both: whatever reaches\n * the canvas renders in a site whose author never saw it.\n *\n * The platform's rule, so named for what it guards rather than for who asks\n * (AGL-3080): any plugin that brings a tree in from elsewhere passes it\n * through here, and a plugin may not import another plugin's model. The\n * marketplace re-exports it under the names its publishers use.\n */\n\nimport { isFirstPartyMediaSrc } from './media-ref'\nimport { matchComponentPropToken } from './compose-reusable-components'\nimport { isHostImageToken } from './host-tokens'\nimport {\n isSafeNodeUrl,\n type NodeUrlProp,\n SAFE_SRC_PATTERN,\n} from './node-url-policy'\nimport { SAFE_HREF_PATTERN } from './screen-link-value'\n\n/**\n * Component ids a portable tree may hold. Mirrors the persisted ids in\n * plugins-mui (plugin.spec.ts) minus `reusableInstance` — nested\n * instances would smuggle references to another tenant's private\n * definitions — and minus `layoutSlot`, which is layout chrome. Keep sorted.\n *\n * The rule this list encodes is \"inert and self-contained\": an element that\n * renders from its own props travels to another workspace intact. What stays\n * out is what cannot — raw-HTML escape hatches (`customHtml`), references to\n * another tenant's documents (`reusableInstance`), third-party code\n * (`plugin`), host chrome (`layoutSlot`) and anything bound to the source\n * site's data (`collection`, `product`).\n *\n * It must cover everything the besigner's palette offers under those terms, or\n * the catalogue promises what the exporter refuses — which is how `section`\n * came to be missing (AGL-1033): every Sections & Blocks preset composes it,\n * so the whole category was unpublishable. `blocks-publishable.spec.ts` walks\n * the presets and fails if this list falls behind them again.\n */\nexport const PORTABLE_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'form',\n 'formField',\n 'image',\n 'muiAppBar',\n 'muiButton',\n // MUI's form elements, one element each (they replace the all-in-one formField).\n 'muiCheckbox',\n 'muiDatePicker',\n 'muiFormControl',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiFormLabel',\n 'muiInput',\n 'muiRadio',\n 'muiRadioGroup',\n 'muiRating',\n 'muiSelect',\n 'muiSelectOption',\n 'muiTimePicker',\n 'muiContainer',\n 'muiList',\n 'muiListItem',\n 'muiListItemText',\n 'muiScreenLink',\n 'muiStack',\n 'muiToolbar',\n 'muiTypography',\n 'searchBox',\n // A semantic wrapper — `<section>`/`<footer>`/`<nav>` plus styling, with no\n // behaviour and no binding to the site it came from.\n 'section',\n 'socialLinks',\n 'videoEmbed',\n]\n\n/**\n * Email block ids publishable as an `emailTemplate` (AGL-657).\n *\n * A separate list from the page allowlist because the two vocabularies don't\n * overlap — an email design is built entirely from `plugins-email` blocks, and\n * page components (MUI, forms, video) don't survive an email client anyway.\n *\n * `emailHtml` is excluded deliberately, for the same reason `reusableInstance`\n * is excluded above: it is a raw-HTML escape hatch, and a published template\n * lands in another org's OUTGOING CUSTOMER EMAIL. Email clients don't run\n * scripts, so this isn't XSS — it's that arbitrary markup sent from someone\n * else's domain is a phishing and tracking-pixel vector that no amount of\n * render-time sanitization makes reviewable. Keep sorted.\n */\nexport const PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST: readonly string[] = [\n 'emailButton',\n 'emailDivider',\n 'emailImage',\n 'emailProduct',\n 'emailRichtext',\n 'emailSection',\n 'emailSpacer',\n 'emailText',\n]\n\n/**\n * Email block ids publishable as an `emailStarter` — the transactional list\n * minus `emailRichtext`.\n *\n * Derived rather than written out, so a block added to the list above cannot\n * silently miss this one. What it subtracts is the reason it exists.\n *\n * `emailRichtext` holds its content in an `html` prop, and the email renderer's\n * `sanitize` hook defaults to identity with no production caller supplying one\n * — so that prop reaches the recipient's inbox exactly as authored. On a\n * transactional design that is the site's own author writing their own mail. In\n * a STARTER it is a stranger's markup mailed from the shared sending domain\n * under the tenant's From header, which is the raw-HTML escape hatch\n * `emailHtml` was excluded for, wearing a friendlier block name. A remote\n * `<img>` inside it is a tracking pixel that the src policy on `emailImage`\n * would otherwise have caught.\n *\n * Subtracting the block is deliberate rather than sanitizing its markup: an\n * allowlist over someone else's HTML is a judgment we would have to keep\n * re-making, and everything rich text can express in an email — a heading, a\n * paragraph, a link, a button — the remaining blocks already express as a\n * structured tree we render ourselves.\n */\nexport const PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST: readonly string[] =\n PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST.filter(\n (componentId) => componentId !== 'emailRichtext',\n )\n\n/** Serialized definition size cap (Firestore doc limit is 1 MiB). */\nexport const PORTABLE_DEFINITION_MAX_BYTES = 200 * 1024\n\nconst KEPT_NODE_KEYS = [\n '$id',\n 'componentId',\n 'pluginId',\n 'parentId',\n 'props',\n 'nodes',\n] as const\n\n/**\n * The address rules a published node's `href` and `src` meet — the linking\n * elements' navigable protocols, and https, inline or site-relative images\n * (see `node-url-policy`).\n *\n * Exported under these names for a property sanitizer (the marketplace's is\n * one), which holds a portable property's Link default to the same rule as a\n * portable node's `href`, and an Image default to the `src` rule.\n */\nexport {\n SAFE_HREF_PATTERN as PORTABLE_SAFE_HREF,\n SAFE_SRC_PATTERN as PORTABLE_SAFE_SRC,\n}\n\n/**\n * The property kind whose value may fill each address prop through a binding\n * (AGL-2933): a Link feeds an `href`, an Image a `src`. Each kind's default is\n * held to that prop's own rule wherever a declaration is published, so the\n * binding carries nothing a literal in the same place could not.\n */\nconst URL_PROP_KIND: Readonly<Record<NodeUrlProp, string>> = {\n href: 'href',\n src: 'image',\n}\n\n/**\n * Declared property names by the address prop their kind may fill — the\n * index {@link sanitizePublishedNodeProps} reads a binding against.\n */\nfunction urlBindingsOf(\n declaredProps: ReadonlyArray<{ name?: unknown; type?: unknown }> | undefined,\n): ReadonlyMap<string, NodeUrlProp> {\n const bindings = new Map<string, NodeUrlProp>()\n for (const prop of declaredProps ?? []) {\n if (typeof prop?.name !== 'string' || !prop.name) continue\n if (prop.type === URL_PROP_KIND.href) bindings.set(prop.name, 'href')\n else if (prop.type === URL_PROP_KIND.src) bindings.set(prop.name, 'src')\n }\n return bindings\n}\n\n/**\n * Strips props a published node must never carry into someone else's site\n * (AGL-784).\n *\n * `Leaf` spreads a node's props straight onto the rendered component, and MUI\n * passes unknown props through to its root DOM element, so whatever survives\n * publishing reaches the DOM of every org that installs the listing. Marketplace\n * components are auto-listed with no review, and `hosts/{h}/components` is\n * writable by any org member, so a hand-crafted doc is a realistic input here.\n *\n * What each stripped key actually does, measured rather than assumed:\n * - `dangerouslySetInnerHTML` is NOT the stored-XSS it looks like — `Leaf`\n * always passes a children array, so React throws\n * \"Can only set one of `children` or `props.dangerouslySetInnerHTML`\"\n * (and the void-element variant for self-closing components like `image`).\n * That is worse in practice than it sounds: the throw happens during SSR,\n * which is the AGL-579 failure mode that 500s the page and wedges ISR for\n * the whole site. One published component would take down every consumer.\n * - `on*` handlers can only survive JSON as strings, which React drops with a\n * warning. Removed for hygiene, and so a future renderer that does eval-ish\n * prop handling can't turn them back into a vector.\n * - `href`/`src` get the render-time URL policy applied at publish time too.\n * React already neutralizes `javascript:` hrefs and the mui components run\n * SAFE_HREF themselves, so this is defense in depth for any component that\n * forgets to — cheap, and it keeps the stored artifact honest.\n * - An `href`/`src` that is exactly one `{{prop.*}}` token naming a declared\n * property of the kind that holds that address — a Link for `href`, an\n * Image for `src` — is kept as the binding it is (AGL-2933). The token is no\n * address, so the rule above would refuse it, and every link and image a\n * component's properties feed would arrive unbound. What it becomes is held\n * to the same rule twice over: the property's default where the declaration\n * is published, and whatever value a page sets where the graft substitutes\n * it (`resolveComponentPropTokens`). A token that is only part of the value,\n * or names a property of another kind or none, is refused as before.\n * - A `src` that is exactly one image `{{host.*}}` token is kept too (AGL-4119):\n * it is the installing site's own logo, read from its own document at render\n * (`isHostImageToken`), so a new site's header carries it from a starter.\n *\n * Deliberately shallow: only top-level props are spread onto the DOM. A nested\n * object (`icon: { path }`) is consumed by the component, never spread, so\n * recursing would strip legitimate data for no security gain.\n */\nfunction sanitizePublishedNodeProps(\n props: Record<string, unknown>,\n urlBindings: ReadonlyMap<string, NodeUrlProp>,\n): Record<string, unknown> {\n const safe: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(props)) {\n if (key === 'dangerouslySetInnerHTML') continue\n if (/^on[A-Z]/.test(key)) continue\n if ((key === 'href' || key === 'src') && typeof value === 'string') {\n const trimmed = value.trim()\n const bound = matchComponentPropToken(trimmed)\n const keep =\n isSafeNodeUrl(key, trimmed) ||\n (bound !== null && urlBindings.get(bound) === key) ||\n (key === 'src' && isHostImageToken(trimmed))\n if (keep) safe[key] = trimmed\n continue\n }\n safe[key] = value\n }\n return safe\n}\n\n/**\n * `nodes` that is still in one of its STORAGE forms rather than a node map\n * (AGL-1395).\n *\n * The two forms `decodeStoredNodes` knows about, tested structurally so this\n * stays a model function with no server import: a byte view — what\n * firebase-admin hands back for a `Bytes` field — and the `{type, data}`\n * envelope `JSON.stringify` makes of a Node `Buffer`, which is what a\n * site-export bundle carries. Neither is a map, and neither is anything the\n * publisher did.\n *\n * The envelope test is deliberately exact, for the reason `decodeStoredNodes`\n * spells out: the alternative reading is a node map with nodes called `type`\n * and `data`, which cannot exist because node-map values are objects.\n */\nfunction isUndecodedNodes(nodes: unknown): boolean {\n if (ArrayBuffer.isView(nodes)) return true\n if (typeof nodes !== 'object' || nodes === null || Array.isArray(nodes)) {\n return false\n }\n const value = nodes as { type?: unknown; data?: unknown }\n return (\n value.type === 'Buffer' &&\n Array.isArray(value.data) &&\n Object.keys(value).length === 2\n )\n}\n\nexport type PortableDefinitionNodes = Record<\n string,\n {\n $id: string\n componentId: string\n pluginId?: string\n parentId: string | null\n props?: Record<string, unknown>\n nodes?: string[]\n }\n>\n\n/**\n * Validates and strips a host component definition for publishing:\n * - only allowlisted component ids (no reusable instances, no layout chrome)\n * - only the persisted node keys (drops runtime fields like resolvedProps)\n * - the subtree reachable from `rootId` only, and a serialized size cap\n *\n * - per-node prop hardening (see `sanitizePublishedNodeProps`, AGL-784)\n *\n * XSS note: rich-text `html` props stay as-authored here — they are sanitized\n * at render time (sanitize-rich-text allowlist), which also covers definitions\n * written to Firestore directly (see docs/SECURITY_CONTENT_REVIEW.md). Props\n * that are spread onto the DOM (`dangerouslySetInnerHTML`, `on*`, `href`,\n * `src`) are hardened HERE as well, because publishing hands them to a\n * different org's render tree.\n */\nexport function sanitizePortableDefinition(\n definition: {\n rootId: string\n nodes: Record<string, any>\n },\n options?: {\n /**\n * Additional component ids permitted for this artifact type.\n *\n * `layoutSlot` is excluded from the shared allowlist because a slot in\n * page content has nowhere to graft — but a published LAYOUT is\n * meaningless without one (AGL-671). Scoped per call rather than added\n * globally so page and component publishing stay unchanged.\n */\n extraComponentIds?: readonly string[]\n /**\n * Replaces the page allowlist outright, for artifact types built from a\n * disjoint component vocabulary — an email design uses `plugins-email`\n * blocks and nothing else (AGL-657), so EXTENDING the page list would\n * green-light a `videoEmbed` or `form` that no email client can render.\n */\n componentIds?: readonly string[]\n /**\n * The properties the tree declares, as they are published beside it\n * (AGL-2933). A node's `href` or `src` bound whole to one of the kind that\n * holds that address is kept — see `sanitizePublishedNodeProps`. Absent\n * for a tree that declares none, which keeps no binding.\n */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n },\n):\n | { ok: true; rootId: string; nodes: PortableDefinitionNodes }\n | { ok: false; error: string } {\n const { rootId, nodes } = definition\n const base = options?.componentIds ?? PORTABLE_COMPONENT_ID_ALLOWLIST\n const allowed = options?.extraComponentIds?.length\n ? [...base, ...options.extraComponentIds]\n : base\n const urlBindings = urlBindingsOf(options?.declaredProps)\n // An UNDECODED `nodes` is never the author's fault, so it must not share a\n // message with a genuinely rootless definition (AGL-1395). Both undecoded\n // forms — a Node `Buffer` from the Admin SDK, and the `{type:'Buffer'}`\n // envelope `JSON.stringify` makes of one — reach the root check below as\n // something with no `_@_` in it, and the answer was \"Definition has no root\n // node\": a sentence that sends the publisher to redesign a page that is\n // fine. Callers must run `decodeStoredNodes` first; this says so out loud\n // rather than letting the next raw read look like a content problem.\n if (isUndecodedNodes(nodes)) {\n return {\n ok: false,\n error:\n 'This content could not be read — it is stored compressed and was ' +\n 'not decoded before publishing. That is a bug on our side, not a ' +\n 'problem with your design.',\n }\n }\n if (!rootId || !nodes?.[rootId]) {\n return { ok: false, error: 'Definition has no root node' }\n }\n const sanitized: PortableDefinitionNodes = {}\n /** Set while walking; see the empty-definition check after the loop. */\n let rootIsWrapper = false\n const queue = [rootId]\n while (queue.length) {\n const id = queue.shift() as string\n if (sanitized[id]) continue\n const node = nodes[id]\n if (!node) return { ok: false, error: `Missing node \"${id}\"` }\n // The root node is the virtual root-collection wrapper (canvas\n // `NODE_ROOT_ID` = `_@_`): it declares \"everything inside <body>\" for\n // drag/drop mapping, not a rendered component, so a `div`/absent\n // componentId there is structural — not a real component to allowlist\n // (AGL-783). Exempt ONLY that wrapper shape; a root carrying a real\n // component id is still checked, so a disallowed component can't be\n // smuggled in as the root. Every descendant is real content and always\n // checked below.\n const isRootWrapper =\n id === rootId &&\n (node.componentId == null ||\n node.componentId === '' ||\n node.componentId === 'div')\n if (!isRootWrapper && !allowed.includes(node.componentId)) {\n return {\n ok: false,\n // Says why, not just no (AGL-1033). The bare id was doubly unhelpful:\n // it is not what the palette calls the element, and it left the author\n // guessing whether this was a bug or a rule.\n error:\n `Component \"${node.componentId}\" cannot be published — a listing may ` +\n 'only contain self-contained presentational elements. Raw HTML, ' +\n 'reusable-component references, plugin elements and anything bound ' +\n \"to this site's data or layout stay behind.\",\n }\n }\n const plain: any = {}\n for (const key of KEPT_NODE_KEYS) {\n if (node[key] !== undefined) plain[key] = node[key]\n }\n // Per-node prop hardening (AGL-784) — see sanitizePublishedNodeProps.\n if (plain.props && typeof plain.props === 'object') {\n plain.props = sanitizePublishedNodeProps(plain.props, urlBindings)\n }\n plain.$id = id\n plain.parentId = id === rootId ? null : (node.parentId ?? null)\n // Give the wrapper an explicit container id so the installed definition\n // renders its root collection the same way it did on the source site.\n if (isRootWrapper) {\n plain.componentId = 'div'\n rootIsWrapper = true\n }\n sanitized[id] = plain\n if (Array.isArray(node.nodes)) queue.push(...node.nodes)\n }\n let serialized: string\n try {\n serialized = JSON.stringify(sanitized)\n } catch {\n return { ok: false, error: 'Definition is not serializable' }\n }\n if (serialized.length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n // An empty definition is not a listing (AGL-1033). Publishing one used to\n // SUCCEED — the root wrapper alone sanitizes cleanly — so a component whose\n // content had never been published to its document shipped a blank version\n // to everyone who installed it, and said nothing. Refusing here is the\n // \"fail loudly at the point the author understands it\" half.\n //\n // Only the WRAPPER case: a definition that is a single real component is a\n // perfectly good one-element listing, and refusing that would be a different\n // bug in the other direction.\n if (rootIsWrapper && Object.keys(sanitized).length <= 1) {\n return {\n ok: false,\n error:\n 'There is nothing to publish — this is empty. If you have edited it ' +\n 'in the designer, publish those changes first, then publish the listing.',\n }\n }\n return { ok: true, rootId, nodes: sanitized }\n}\n\n/**\n * Converts a sanitized portable definition (normalized map) into the\n * nested node shape `canvas.addNodeFromPreset` grafts — ids regenerate on\n * insert, so collisions with existing canvas nodes are impossible\n * (AGL-169). A seen-set guards malformed self-referencing trees.\n */\nexport function portableDefinitionToNested(\n rootId: string,\n nodes: PortableDefinitionNodes,\n): Record<string, unknown> | null {\n const seen = new Set<string>()\n const build = (id: string): Record<string, unknown> | null => {\n const node = nodes[id]\n if (!node || seen.has(id)) return null\n seen.add(id)\n return {\n componentId: node.componentId,\n ...(node.pluginId ? { pluginId: node.pluginId } : {}),\n ...(node.props ? { props: node.props } : {}),\n nodes: (node.nodes ?? [])\n .map(build)\n .filter((child): child is Record<string, unknown> => Boolean(child)),\n }\n }\n return build(rootId)\n}\n\n/**\n * Field types a portable dataset schema may declare (AGL-657). Mirrors\n * `DATASET_FIELD_TYPES` in core; duplicated rather than imported to keep this\n * module dependency-free (it is imported by API routes and client components\n * alike), and asserted against the source of truth in the spec.\n */\nexport const PORTABLE_DATASET_FIELD_TYPES: readonly string[] = [\n 'bool',\n 'bytes',\n 'coordinates',\n 'float',\n 'int32',\n 'int64',\n 'map',\n 'nil',\n 'reference',\n 'sorted',\n 'text',\n 'timestamp',\n]\n\n"],"names":["matchComponentPropToken","isHostImageToken","isSafeNodeUrl","SAFE_SRC_PATTERN","SAFE_HREF_PATTERN","PORTABLE_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_COMPONENT_ID_ALLOWLIST","PORTABLE_EMAIL_STARTER_COMPONENT_ID_ALLOWLIST","filter","componentId","PORTABLE_DEFINITION_MAX_BYTES","KEPT_NODE_KEYS","PORTABLE_SAFE_HREF","PORTABLE_SAFE_SRC","URL_PROP_KIND","href","src","urlBindingsOf","declaredProps","bindings","Map","prop","name","type","set","sanitizePublishedNodeProps","props","urlBindings","safe","key","value","Object","entries","test","trimmed","trim","bound","keep","get","isUndecodedNodes","nodes","ArrayBuffer","isView","Array","isArray","data","keys","length","sanitizePortableDefinition","definition","options","rootId","base","componentIds","allowed","extraComponentIds","ok","error","sanitized","rootIsWrapper","queue","node","id","shift","isRootWrapper","includes","plain","undefined","$id","parentId","push","serialized","JSON","stringify","portableDefinitionToNested","seen","Set","build","has","add","pluginId","map","child","Boolean","PORTABLE_DATASET_FIELD_TYPES"],"mappings":"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"}
|
|
@@ -146,6 +146,7 @@ export * from './attribution-guard';
|
|
|
146
146
|
export * from './node-interactions';
|
|
147
147
|
export * from './element-ui';
|
|
148
148
|
export * from './image-upload-types';
|
|
149
|
+
export * from './site-brand-files';
|
|
149
150
|
export * from './sanitize-svg';
|
|
150
151
|
export * from './media-folders';
|
|
151
152
|
export * from './media-metadata';
|
|
@@ -287,6 +287,7 @@ export * from "./attribution-guard.js";
|
|
|
287
287
|
export * from "./node-interactions.js";
|
|
288
288
|
export * from "./element-ui.js";
|
|
289
289
|
export * from "./image-upload-types.js";
|
|
290
|
+
export * from "./site-brand-files.js";
|
|
290
291
|
export * from "./sanitize-svg.js";
|
|
291
292
|
export * from "./media-folders.js";
|
|
292
293
|
export * from "./media-metadata.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Server-safe app-utils: every app-util EXCEPT the client-only React\n * contexts (see ./contexts). Nothing here calls `createContext` at module\n * scope, so this barrel — re-exported by `@aglyn/aglyn/server` — is safe to\n * import from tenant Server Components (AGL-405). The full `./index` barrel\n * re-adds the contexts for client consumers.\n */\nexport * from './binding-token-catalog'\nexport * from './binding-tokens'\nexport * from './breadcrumb-json-ld'\n// What a search listing holds and how long each field may be (AGL-2910):\n// the SEO editors and anything that proposes a value read one catalog.\nexport * from './seo-listing-fields'\n// `VideoObject` for the Video elements a page actually ships (AGL-2747).\n// Beside the other structured-data builders, and server-only like them: only\n// the route emits JSON-LD, and a crawler-facing block has no client caller.\nexport * from './video-object'\n// `FAQPage` for the FAQ section a page draws (AGL-3574): the same input, the\n// same reason, and server-only for the same one — only the route emits it.\nexport * from './faq-page'\nexport * from './collection-delete'\nexport * from './collection-entries'\nexport * from './collection-kind'\nexport * from './collection-slug'\nexport * from './content-authors'\n// NOT re-exported: `./local-business` and `./site-entity-json-ld` (AGL-3383).\n// This barrel is the whole namespace a realm bundle is handed and part of the\n// runtime a published page loads (`check:tenant-wire-weight`); the site\n// entity's JSON-LD and the LocalBusiness allow-list are read only by the\n// tenant page's server render and the console's Setup → SEO card, which import\n// them by path (`@aglyn/aglyn/app-utils/...`).\nexport * from './content-query-fields'\nexport * from './content-schema-type'\nexport * from './content-author-profile'\nexport * from './url-slug'\nexport * from './child-contract'\nexport * from './child-contract-compose'\n// The one reading of `restrictChildren`/`restrictParent`, shared by the\n// besigner's drop check and the AI plugin's node-tree validator (AGL-2905).\nexport * from './lineal-order'\n// What a portable node tree must satisfy before it lands on a canvas that did\n// not author it (AGL-2939): a tree installed from another workspace and a tree\n// a model generated both pass through it.\nexport * from './node-definition-sanitizer'\nexport * from './console-routes'\n// What a stored screen-link value means, with no React attached — the\n// where-used scan reads these on the server (AGL-703).\nexport * from './screen-link-value'\n// Which entries a page's links name (AGL-3118). The tenant render reads\n// exactly those entry documents, so the walk runs on the server.\nexport * from './link-references'\nexport * from './analytics-path-key'\nexport * from './contacts'\n// A lead's or a contact's email state (AGL-3245): the last verdict on its\n// address, in words a person can act on, mirrored from the senders' lists.\nexport * from './email-state'\n// A field whose choices an admin keeps (AGL-3510): standard values built in,\n// the organization's own beside them, and the judge every write asks. Pure,\n// and named for no plugin — a plugin's registry of picklists sits on top.\nexport * from './picklists'\n// The records that sit beside a contact — companies, pipelines, deals, tasks,\n// activities, field definitions (AGL-2595). Pure like `contacts`, and beside\n// it because `ContactFacet` names its lifecycle stage and custom-value types;\n// the scope stamp every CRM creator writes lives here too.\nexport * from './crm'\n// Email templates, snippets and the merge-field resolver (AGL-2658): the\n// letter a rep keeps under a name, and the one grammar the send route and\n// the dialog fill it in by. Pure like `crm`, and beside it because a\n// template is scoped the way a task is.\nexport * from './crm-email-templates'\n// A person's privacy erasure from one workspace (AGL-2623): the request's\n// shape and id, the marker a record carries while it waits, and the lists\n// the dialog and the docs share. Pure like `crm`; the sweep is server-side.\nexport * from './person-erasure'\n// A task's reminder at its own time (AGL-2659): which reminder a save\n// leaves, whether one is owed, and the words the notification and the mail\n// say. Pure like `crm`, read by the hourly route and by every writer of the\n// field.\n// Reading a received email (AGL-2657): the address in a header, the thread\n// a subject belongs to, the reply above the quoted history — and the domain\n// the platform receives mail on. Pure; every surface that reads mail it was\n// sent reads it this way.\nexport * from './email-text'\nexport * from './inbound-mail-domain'\n// A member's own addresses in a workspace (AGL-2975): the ones they have\n// confirmed count as theirs wherever the roster is asked who wrote a\n// message. Pure; the store and the confirmation link are server-side.\nexport * from './member-email-aliases'\n// The CSV cell grammar the exports write with (AGL-2621), and the target a\n// mapped column names for a custom field — named rather than starred, because\n// an importer built on this file exports the shared ceilings under its own\n// names.\nexport {\n csvCell,\n csvDocument,\n customImportTarget,\n customImportTargetKey,\n importNoMailServerSentence,\n} from './csv-import'\nexport type {\n ImportChunkResult,\n ImportDroppedValue,\n ImportSkippedRow,\n} from './csv-import'\n// What a VALUE under a custom contact field may be, and the coercion every\n// door applies to one (AGL-2601): a mapped form field, an API `custom` body,\n// and the carry-over that keeps a mapping across a publish. Pure like `crm`.\nexport * from './contact-custom-fields'\n// The consent JOIN (`docs/specs/email-overhaul.md` §3f). Pure, and it composes\n// `contacts` for the normalizer, so it sits directly beside it. No Node\n// builtin, which is what keeps it out of the `/server`-only group `person-key`\n// belongs to.\nexport * from './consent-groups'\nexport * from './marketing-consent'\n// A refusal kept after the record it was written on is deleted (AGL-3338),\n// read back through the same parser as the record's own.\nexport * from './retained-refusals'\n// The ENROLLMENT-time half of the same question, beside its reader for the\n// same reason: `marketing-consent` decides whether a recorded basis lets us\n// mail somebody, and this decides what basis putting them on a list may\n// record. Two enrollment surfaces import it — the Inbox assignment route and\n// the Emails console's audience card — and neither could import the other.\nexport * from './enrollment-basis'\n// Reading the FILE a merchant arrives with (`docs/specs/email-competitive-gaps.md`\n// G5/P4), directly after the policy it hands its addresses to: parsing and\n// mechanical screening only, so that an import asks the enrollment question\n// through the same module the one-address add path asks it through rather\n// than answering it a second way.\n// The dynamic-list rule (§3b/§3c), beside it for the same reasons: pure, and\n// it composes `contacts` for the segment vocabulary rather than restating it.\nexport * from './dynamic-list-rule'\n// The subscribable streams a recipient can leave one at a time\n// (`docs/specs/email-competitive-gaps.md` §1f). Pure, and read from all three\n// sides of the feature: the composer picks one, the send path signs it into\n// the unsubscribe link, and the unauthenticated preference page renders the\n// catalog. No Node builtin, so it stays out of the `/server`-only group.\nexport * from './subscription-topics'\nexport * from './compose-layout-nodes'\n// Layout properties applied through the chain (AGL-2893).\nexport * from './compose-layout-props'\n// A screen's per-page restyling of its layout's elements (AGL-3286).\nexport * from './layout-style-overrides'\nexport * from './document-landmark'\nexport * from './functions'\nexport * from './compose-reusable-components'\n// Where a reusable component is placed — pages or emails (AGL-3287).\nexport * from './reusable-component-kind'\nexport * from './compress'\nexport * from './merge-node-sx'\nexport * from './palette-sx'\nexport * from './scheme-sx'\n// An element pinned to one color scheme (AGL-3284).\nexport * from './element-color-scheme'\nexport * from './create-resource-uid'\nexport * from './decompress'\nexport * from './stored-nodes'\nexport * from './strip-undefined'\nexport * from './organizations'\nexport * from './org-permissions'\n// The AI verdict on either membership axis (AGL-2927). After both modules it\n// composes: the org catalog for org-wide members, the host role for site\n// collaborators.\nexport * from './host-permissions'\nexport * from './password-policy'\nexport * from './idp-profile'\nexport * from './onboarding-deep-link'\n// The platform team's ask that a workspace buy a plan (AGL-3466), beside the\n// plan intent its button carries into Billing.\nexport * from './upgrade-proposal'\n// Where an account came from (AGL-1731). Beside the plan intent because\n// they are the same hop — the marketing CTA's query string — and both are\n// remembered on `users/{uid}` across the verification wall.\nexport * from './utm-attribution'\n// What a record is filed under: the edge between a container a plugin keeps\n// (a campaign) and the forms, screens and contacts it gathers. Beside the\n// attribution above because the two answer opposite questions: attribution\n// records where somebody CAME from, this records what a merchant DECLARED\n// belongs together.\nexport * from './container-membership'\nexport * from './deployment-shape'\n// Which browser origins may complete a signed direct-to-GCS upload (AGL-1452).\n// GCS matches the origin list EXACTLY, so every serving console name needs its\n// own entry — the rule nobody could be expected to remember at attach time.\nexport * from './upload-cors'\n// The docs-help subset the first-party plugin consoles read (AGL-2213) — the\n// console's own registry lives in apps/console/constants and a lib cannot\n// import an app.\nexport * from './docs-help'\nexport * from './platform-brand'\nexport * from './plan-entitlements'\n// The free plan's bandwidth hard cap (AGL-1967/2070/2155). After\n// `plan-entitlements`, which owns the predicate it keys off.\nexport * from './bandwidth-cap'\n// Video and file delivery as bandwidth (AGL-3474), in the band's own units.\nexport * from './media-bandwidth'\n// The month every monthly counter is keyed by, and the boundary it lifts on.\nexport * from './utc-month'\nexport * from './forms'\n// What a form's DESIGN must still satisfy for its submissions to arrive.\n// After `forms`, whose field walk it reads the drawn fields with.\nexport * from './form-contract'\nexport * from './visitor-record-ceiling'\nexport * from './health-report'\n// Did a webhook delivery actually DO anything (AGL-1954)? After\n// `health-report`, whose billing verdict consumes the counts this produces.\nexport * from './webhook-delivery'\nexport * from './support-tiers'\n// Site Care requests (AGL-3959): the arithmetic, and the two modules any\n// staff-recorded, approval-gated charge reuses (AGL-3970).\nexport * from './site-care'\nexport * from './owner-approval-record'\nexport * from './append-only-history'\n// Site Care projects (AGL-3970): the flat quote, its split and its record.\nexport * from './site-care-project'\nexport * from './release-flags'\n// Who runs THIS deployment, and where its notices go (AGL-2016). Ordered\n// before the four surfaces that print it, all of which used to declare their\n// own `support@aglyn.com` literal.\nexport * from './operator-identity'\nexport * from './lockdown'\nexport * from './media-quarantine'\nexport * from './media-takedown-reach'\nexport * from './upload-inspection'\nexport * from './abuse-report'\n// The other three quarters of §512 (AGL-1983): the put-back procedure that\n// answers a notice, and the strike ledger that conditions the safe harbour.\nexport * from './dmca-counter-notice'\nexport * from './repeat-infringer'\n// `addBusinessDays` briefly existed TWICE — once here in './support-tiers'\n// and once in './dmca-counter-notice' — and two `export *` made the name\n// ambiguous, TS2308'ing this barrel and every tsconfig that reads it. The\n// interleave was fixed twice in parallel: once by naming a winner here, and\n// once by deleting the second definition so './dmca-counter-notice' imports\n// this one. Only the deletion survives, so the disambiguating re-export that\n// stood on this line is gone with it — `export *` is unambiguous again.\n//\n// The concern that argued for keeping two copies is worth answering rather\n// than dropping: a later tweak to the SUPPORT SLA window must never silently\n// move the §512(g)(2)(C) put-back deadline. It cannot. This function embeds\n// no window — it is \"add N business days\", and N is the argument. The two\n// windows live in their own constants (`SUPPORT_*` there,\n// `COUNTER_NOTICE_*_BUSINESS_DAYS` in the DMCA module) and never meet. What\n// SHOULD stay shared is the day arithmetic itself: two copies of a weekend\n// rule are two chances to disagree about which day a deadline falls on.\nexport * from './org-override-reason'\nexport * from './host-tokens'\nexport * from './variables'\n// What happened on a site that an automation can start on: the platform's\n// own event and the ones plugins declare (AGL-3080).\nexport * from './host-events'\nexport * from './expand-repeatables'\nexport * from './org-roles'\nexport * from './markdown-lite'\nexport * from './notifications'\n// The operator alert registry (AGL-3377); OPERATOR_ALERT_NOTIFICATION_TYPES\n// lives here now, derived from it.\nexport * from './operator-alerts'\nexport * from './definition-canvas-tree'\nexport * from './ensure-canvas-root'\nexport * from './repair-canvas-nodes'\nexport * from './detect-template-placeholders'\nexport * from './guarded-version-save'\nexport * from './version-draft'\nexport * from './local-storage-budget'\nexport * from './measure-node-map'\nexport * from './resolve-named-tokens'\nexport * from './screen-route'\nexport * from './host-naming'\nexport * from './name-search'\nexport * from './artifact-list-keys'\nexport * from './name-match'\nexport * from './api-plugins'\nexport * from './plugin-api-rate-limit'\n// The same-origin gate on visitor-facing plugin writes (AGL-1880). After\n// `plugin-api-rate-limit`, whose `isMachinePluginApiPath` it reuses so the\n// two gates exempt exactly the same machine surfaces.\nexport * from './plugin-api-cross-origin'\n// What a published page does when a visitor does something (AGL-3080): the\n// platform's interaction vocabulary, which the workflows plugin builds its\n// automations on.\nexport * from './site-interactions'\nexport * from './element-animation'\nexport * from './attribution-guard'\nexport * from './node-interactions'\nexport * from './element-ui'\nexport * from './image-upload-types'\nexport * from './sanitize-svg'\nexport * from './media-folders'\nexport * from './media-metadata'\nexport * from './media-ref'\nexport * from './rendered-widths'\n// After `media-ref`, whose reference grammar it reads stored URLs with.\nexport * from './paid-media-source'\nexport * from './author-css'\n// The isomorphic HTML rule (AGL-1901), after the CSS one it depends on.\nexport * from './author-html'\nexport * from './csv'\nexport * from './csv-upload'\nexport * from './artifact-provenance'\nexport * from './repeat-query'\nexport * from './plugin-manifest'\n// After `plugin-manifest`, whose revocation predicates it asks the kill\n// question with, and after `media-ref`, whose grammar decides what counts as a\n// first-party image.\nexport * from './email-starter-policy'\nexport * from './safe-json-ld'\n// Agent readiness (AGL-2716): content negotiation, the Markdown rendering of a\n// page, `/llms.txt` and the per-site OpenAPI document. All pure, all server-safe.\nexport * from './accept-negotiation'\nexport * from './agent-api-catalog'\nexport * from './agent-openapi'\nexport * from './author-html-markdown'\nexport * from './llms-txt'\nexport * from './page-markdown'\nexport * from './request-geo'\nexport * from './request-ip'\nexport * from './search-indexing'\n// Preview: a site or page only its team can see (AGL-4086).\nexport * from './site-preview'\nexport * from './seo-title'\nexport * from './sitemap'\nexport * from './visitor-consent'\nexport * from './social-image'\nexport * from './scope-tokens'\nexport * from './org-billing-doc'\nexport * from './stripe-deployment-mode'\nexport * from './deferred-image'\n"],"names":["csvCell","csvDocument","customImportTarget","customImportTargetKey","importNoMailServerSentence"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2022 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 * Server-safe app-utils: every app-util EXCEPT the client-only React\n * contexts (see ./contexts). Nothing here calls `createContext` at module\n * scope, so this barrel — re-exported by `@aglyn/aglyn/server` — is safe to\n * import from tenant Server Components (AGL-405). The full `./index` barrel\n * re-adds the contexts for client consumers.\n */\nexport * from './binding-token-catalog'\nexport * from './binding-tokens'\nexport * from './breadcrumb-json-ld'\n// What a search listing holds and how long each field may be (AGL-2910):\n// the SEO editors and anything that proposes a value read one catalog.\nexport * from './seo-listing-fields'\n// `VideoObject` for the Video elements a page actually ships (AGL-2747).\n// Beside the other structured-data builders, and server-only like them: only\n// the route emits JSON-LD, and a crawler-facing block has no client caller.\nexport * from './video-object'\n// `FAQPage` for the FAQ section a page draws (AGL-3574): the same input, the\n// same reason, and server-only for the same one — only the route emits it.\nexport * from './faq-page'\nexport * from './collection-delete'\nexport * from './collection-entries'\nexport * from './collection-kind'\nexport * from './collection-slug'\nexport * from './content-authors'\n// NOT re-exported: `./local-business` and `./site-entity-json-ld` (AGL-3383).\n// This barrel is the whole namespace a realm bundle is handed and part of the\n// runtime a published page loads (`check:tenant-wire-weight`); the site\n// entity's JSON-LD and the LocalBusiness allow-list are read only by the\n// tenant page's server render and the console's Setup → SEO card, which import\n// them by path (`@aglyn/aglyn/app-utils/...`).\nexport * from './content-query-fields'\nexport * from './content-schema-type'\nexport * from './content-author-profile'\nexport * from './url-slug'\nexport * from './child-contract'\nexport * from './child-contract-compose'\n// The one reading of `restrictChildren`/`restrictParent`, shared by the\n// besigner's drop check and the AI plugin's node-tree validator (AGL-2905).\nexport * from './lineal-order'\n// What a portable node tree must satisfy before it lands on a canvas that did\n// not author it (AGL-2939): a tree installed from another workspace and a tree\n// a model generated both pass through it.\nexport * from './node-definition-sanitizer'\nexport * from './console-routes'\n// What a stored screen-link value means, with no React attached — the\n// where-used scan reads these on the server (AGL-703).\nexport * from './screen-link-value'\n// Which entries a page's links name (AGL-3118). The tenant render reads\n// exactly those entry documents, so the walk runs on the server.\nexport * from './link-references'\nexport * from './analytics-path-key'\nexport * from './contacts'\n// A lead's or a contact's email state (AGL-3245): the last verdict on its\n// address, in words a person can act on, mirrored from the senders' lists.\nexport * from './email-state'\n// A field whose choices an admin keeps (AGL-3510): standard values built in,\n// the organization's own beside them, and the judge every write asks. Pure,\n// and named for no plugin — a plugin's registry of picklists sits on top.\nexport * from './picklists'\n// The records that sit beside a contact — companies, pipelines, deals, tasks,\n// activities, field definitions (AGL-2595). Pure like `contacts`, and beside\n// it because `ContactFacet` names its lifecycle stage and custom-value types;\n// the scope stamp every CRM creator writes lives here too.\nexport * from './crm'\n// Email templates, snippets and the merge-field resolver (AGL-2658): the\n// letter a rep keeps under a name, and the one grammar the send route and\n// the dialog fill it in by. Pure like `crm`, and beside it because a\n// template is scoped the way a task is.\nexport * from './crm-email-templates'\n// A person's privacy erasure from one workspace (AGL-2623): the request's\n// shape and id, the marker a record carries while it waits, and the lists\n// the dialog and the docs share. Pure like `crm`; the sweep is server-side.\nexport * from './person-erasure'\n// A task's reminder at its own time (AGL-2659): which reminder a save\n// leaves, whether one is owed, and the words the notification and the mail\n// say. Pure like `crm`, read by the hourly route and by every writer of the\n// field.\n// Reading a received email (AGL-2657): the address in a header, the thread\n// a subject belongs to, the reply above the quoted history — and the domain\n// the platform receives mail on. Pure; every surface that reads mail it was\n// sent reads it this way.\nexport * from './email-text'\nexport * from './inbound-mail-domain'\n// A member's own addresses in a workspace (AGL-2975): the ones they have\n// confirmed count as theirs wherever the roster is asked who wrote a\n// message. Pure; the store and the confirmation link are server-side.\nexport * from './member-email-aliases'\n// The CSV cell grammar the exports write with (AGL-2621), and the target a\n// mapped column names for a custom field — named rather than starred, because\n// an importer built on this file exports the shared ceilings under its own\n// names.\nexport {\n csvCell,\n csvDocument,\n customImportTarget,\n customImportTargetKey,\n importNoMailServerSentence,\n} from './csv-import'\nexport type {\n ImportChunkResult,\n ImportDroppedValue,\n ImportSkippedRow,\n} from './csv-import'\n// What a VALUE under a custom contact field may be, and the coercion every\n// door applies to one (AGL-2601): a mapped form field, an API `custom` body,\n// and the carry-over that keeps a mapping across a publish. Pure like `crm`.\nexport * from './contact-custom-fields'\n// The consent JOIN (`docs/specs/email-overhaul.md` §3f). Pure, and it composes\n// `contacts` for the normalizer, so it sits directly beside it. No Node\n// builtin, which is what keeps it out of the `/server`-only group `person-key`\n// belongs to.\nexport * from './consent-groups'\nexport * from './marketing-consent'\n// A refusal kept after the record it was written on is deleted (AGL-3338),\n// read back through the same parser as the record's own.\nexport * from './retained-refusals'\n// The ENROLLMENT-time half of the same question, beside its reader for the\n// same reason: `marketing-consent` decides whether a recorded basis lets us\n// mail somebody, and this decides what basis putting them on a list may\n// record. Two enrollment surfaces import it — the Inbox assignment route and\n// the Emails console's audience card — and neither could import the other.\nexport * from './enrollment-basis'\n// Reading the FILE a merchant arrives with (`docs/specs/email-competitive-gaps.md`\n// G5/P4), directly after the policy it hands its addresses to: parsing and\n// mechanical screening only, so that an import asks the enrollment question\n// through the same module the one-address add path asks it through rather\n// than answering it a second way.\n// The dynamic-list rule (§3b/§3c), beside it for the same reasons: pure, and\n// it composes `contacts` for the segment vocabulary rather than restating it.\nexport * from './dynamic-list-rule'\n// The subscribable streams a recipient can leave one at a time\n// (`docs/specs/email-competitive-gaps.md` §1f). Pure, and read from all three\n// sides of the feature: the composer picks one, the send path signs it into\n// the unsubscribe link, and the unauthenticated preference page renders the\n// catalog. No Node builtin, so it stays out of the `/server`-only group.\nexport * from './subscription-topics'\nexport * from './compose-layout-nodes'\n// Layout properties applied through the chain (AGL-2893).\nexport * from './compose-layout-props'\n// A screen's per-page restyling of its layout's elements (AGL-3286).\nexport * from './layout-style-overrides'\nexport * from './document-landmark'\nexport * from './functions'\nexport * from './compose-reusable-components'\n// Where a reusable component is placed — pages or emails (AGL-3287).\nexport * from './reusable-component-kind'\nexport * from './compress'\nexport * from './merge-node-sx'\nexport * from './palette-sx'\nexport * from './scheme-sx'\n// An element pinned to one color scheme (AGL-3284).\nexport * from './element-color-scheme'\nexport * from './create-resource-uid'\nexport * from './decompress'\nexport * from './stored-nodes'\nexport * from './strip-undefined'\nexport * from './organizations'\nexport * from './org-permissions'\n// The AI verdict on either membership axis (AGL-2927). After both modules it\n// composes: the org catalog for org-wide members, the host role for site\n// collaborators.\nexport * from './host-permissions'\nexport * from './password-policy'\nexport * from './idp-profile'\nexport * from './onboarding-deep-link'\n// The platform team's ask that a workspace buy a plan (AGL-3466), beside the\n// plan intent its button carries into Billing.\nexport * from './upgrade-proposal'\n// Where an account came from (AGL-1731). Beside the plan intent because\n// they are the same hop — the marketing CTA's query string — and both are\n// remembered on `users/{uid}` across the verification wall.\nexport * from './utm-attribution'\n// What a record is filed under: the edge between a container a plugin keeps\n// (a campaign) and the forms, screens and contacts it gathers. Beside the\n// attribution above because the two answer opposite questions: attribution\n// records where somebody CAME from, this records what a merchant DECLARED\n// belongs together.\nexport * from './container-membership'\nexport * from './deployment-shape'\n// Which browser origins may complete a signed direct-to-GCS upload (AGL-1452).\n// GCS matches the origin list EXACTLY, so every serving console name needs its\n// own entry — the rule nobody could be expected to remember at attach time.\nexport * from './upload-cors'\n// The docs-help subset the first-party plugin consoles read (AGL-2213) — the\n// console's own registry lives in apps/console/constants and a lib cannot\n// import an app.\nexport * from './docs-help'\nexport * from './platform-brand'\nexport * from './plan-entitlements'\n// The free plan's bandwidth hard cap (AGL-1967/2070/2155). After\n// `plan-entitlements`, which owns the predicate it keys off.\nexport * from './bandwidth-cap'\n// Video and file delivery as bandwidth (AGL-3474), in the band's own units.\nexport * from './media-bandwidth'\n// The month every monthly counter is keyed by, and the boundary it lifts on.\nexport * from './utc-month'\nexport * from './forms'\n// What a form's DESIGN must still satisfy for its submissions to arrive.\n// After `forms`, whose field walk it reads the drawn fields with.\nexport * from './form-contract'\nexport * from './visitor-record-ceiling'\nexport * from './health-report'\n// Did a webhook delivery actually DO anything (AGL-1954)? After\n// `health-report`, whose billing verdict consumes the counts this produces.\nexport * from './webhook-delivery'\nexport * from './support-tiers'\n// Site Care requests (AGL-3959): the arithmetic, and the two modules any\n// staff-recorded, approval-gated charge reuses (AGL-3970).\nexport * from './site-care'\nexport * from './owner-approval-record'\nexport * from './append-only-history'\n// Site Care projects (AGL-3970): the flat quote, its split and its record.\nexport * from './site-care-project'\nexport * from './release-flags'\n// Who runs THIS deployment, and where its notices go (AGL-2016). Ordered\n// before the four surfaces that print it, all of which used to declare their\n// own `support@aglyn.com` literal.\nexport * from './operator-identity'\nexport * from './lockdown'\nexport * from './media-quarantine'\nexport * from './media-takedown-reach'\nexport * from './upload-inspection'\nexport * from './abuse-report'\n// The other three quarters of §512 (AGL-1983): the put-back procedure that\n// answers a notice, and the strike ledger that conditions the safe harbour.\nexport * from './dmca-counter-notice'\nexport * from './repeat-infringer'\n// `addBusinessDays` briefly existed TWICE — once here in './support-tiers'\n// and once in './dmca-counter-notice' — and two `export *` made the name\n// ambiguous, TS2308'ing this barrel and every tsconfig that reads it. The\n// interleave was fixed twice in parallel: once by naming a winner here, and\n// once by deleting the second definition so './dmca-counter-notice' imports\n// this one. Only the deletion survives, so the disambiguating re-export that\n// stood on this line is gone with it — `export *` is unambiguous again.\n//\n// The concern that argued for keeping two copies is worth answering rather\n// than dropping: a later tweak to the SUPPORT SLA window must never silently\n// move the §512(g)(2)(C) put-back deadline. It cannot. This function embeds\n// no window — it is \"add N business days\", and N is the argument. The two\n// windows live in their own constants (`SUPPORT_*` there,\n// `COUNTER_NOTICE_*_BUSINESS_DAYS` in the DMCA module) and never meet. What\n// SHOULD stay shared is the day arithmetic itself: two copies of a weekend\n// rule are two chances to disagree about which day a deadline falls on.\nexport * from './org-override-reason'\nexport * from './host-tokens'\nexport * from './variables'\n// What happened on a site that an automation can start on: the platform's\n// own event and the ones plugins declare (AGL-3080).\nexport * from './host-events'\nexport * from './expand-repeatables'\nexport * from './org-roles'\nexport * from './markdown-lite'\nexport * from './notifications'\n// The operator alert registry (AGL-3377); OPERATOR_ALERT_NOTIFICATION_TYPES\n// lives here now, derived from it.\nexport * from './operator-alerts'\nexport * from './definition-canvas-tree'\nexport * from './ensure-canvas-root'\nexport * from './repair-canvas-nodes'\nexport * from './detect-template-placeholders'\nexport * from './guarded-version-save'\nexport * from './version-draft'\nexport * from './local-storage-budget'\nexport * from './measure-node-map'\nexport * from './resolve-named-tokens'\nexport * from './screen-route'\nexport * from './host-naming'\nexport * from './name-search'\nexport * from './artifact-list-keys'\nexport * from './name-match'\nexport * from './api-plugins'\nexport * from './plugin-api-rate-limit'\n// The same-origin gate on visitor-facing plugin writes (AGL-1880). After\n// `plugin-api-rate-limit`, whose `isMachinePluginApiPath` it reuses so the\n// two gates exempt exactly the same machine surfaces.\nexport * from './plugin-api-cross-origin'\n// What a published page does when a visitor does something (AGL-3080): the\n// platform's interaction vocabulary, which the workflows plugin builds its\n// automations on.\nexport * from './site-interactions'\nexport * from './element-animation'\nexport * from './attribution-guard'\nexport * from './node-interactions'\nexport * from './element-ui'\nexport * from './image-upload-types'\nexport * from './site-brand-files'\nexport * from './sanitize-svg'\nexport * from './media-folders'\nexport * from './media-metadata'\nexport * from './media-ref'\nexport * from './rendered-widths'\n// After `media-ref`, whose reference grammar it reads stored URLs with.\nexport * from './paid-media-source'\nexport * from './author-css'\n// The isomorphic HTML rule (AGL-1901), after the CSS one it depends on.\nexport * from './author-html'\nexport * from './csv'\nexport * from './csv-upload'\nexport * from './artifact-provenance'\nexport * from './repeat-query'\nexport * from './plugin-manifest'\n// After `plugin-manifest`, whose revocation predicates it asks the kill\n// question with, and after `media-ref`, whose grammar decides what counts as a\n// first-party image.\nexport * from './email-starter-policy'\nexport * from './safe-json-ld'\n// Agent readiness (AGL-2716): content negotiation, the Markdown rendering of a\n// page, `/llms.txt` and the per-site OpenAPI document. All pure, all server-safe.\nexport * from './accept-negotiation'\nexport * from './agent-api-catalog'\nexport * from './agent-openapi'\nexport * from './author-html-markdown'\nexport * from './llms-txt'\nexport * from './page-markdown'\nexport * from './request-geo'\nexport * from './request-ip'\nexport * from './search-indexing'\n// Preview: a site or page only its team can see (AGL-4086).\nexport * from './site-preview'\nexport * from './seo-title'\nexport * from './sitemap'\nexport * from './visitor-consent'\nexport * from './social-image'\nexport * from './scope-tokens'\nexport * from './org-billing-doc'\nexport * from './stripe-deployment-mode'\nexport * from './deferred-image'\n"],"names":["csvCell","csvDocument","customImportTarget","customImportTargetKey","importNoMailServerSentence"],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* The logo, the dark logo and the favicon a new site is created with
|
|
19
|
+
* (AGL-4119).
|
|
20
|
+
*
|
|
21
|
+
* A first-time account has no workspace until the create makes one, so nothing
|
|
22
|
+
* can go into a library while the form is open. The form holds the files, and
|
|
23
|
+
* they go into the new site's own library once it exists. The site is what the
|
|
24
|
+
* person asked for and these are optional, so a file that does not make it
|
|
25
|
+
* never costs them the site: they are told which, and where to add it.
|
|
26
|
+
*/
|
|
27
|
+
/** One of the three fields, and the host field each is written to. */
|
|
28
|
+
export type SiteBrandSlot = 'logo' | 'logoDark' | 'favicon';
|
|
29
|
+
export interface SiteBrandSlotSpec {
|
|
30
|
+
slot: SiteBrandSlot;
|
|
31
|
+
/** The field's label. */
|
|
32
|
+
label: string;
|
|
33
|
+
/** How a sentence names it. */
|
|
34
|
+
noun: string;
|
|
35
|
+
}
|
|
36
|
+
export declare const SITE_BRAND_SLOTS: readonly SiteBrandSlotSpec[];
|
|
37
|
+
/** The file chooser's `accept` for a field. */
|
|
38
|
+
export declare function siteBrandFileAccept(slot: SiteBrandSlot): string;
|
|
39
|
+
/**
|
|
40
|
+
* The content type to upload a file under, or `null` for one the field does
|
|
41
|
+
* not take. Read off the file's name when the browser reports none, because
|
|
42
|
+
* the upload route refuses an empty type.
|
|
43
|
+
*/
|
|
44
|
+
export declare function siteBrandFileType(file: Pick<File, 'name' | 'type'>, slot: SiteBrandSlot): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Why a field will not take this file, said on the field before anything is
|
|
47
|
+
* created, or `null` when it will. `maxBytes` is the upload route's ceiling.
|
|
48
|
+
*/
|
|
49
|
+
export declare function siteBrandFileRefusal(file: Pick<File, 'name' | 'type' | 'size'>, slot: SiteBrandSlot, maxBytes: number): string | null;
|
|
50
|
+
/** The host fields a create writes; `favicon` lands on `seo.favicon`. */
|
|
51
|
+
export interface SiteBrandFields {
|
|
52
|
+
logoUrl?: string;
|
|
53
|
+
logoDarkUrl?: string;
|
|
54
|
+
favicon?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface SiteBrandSaveOutcome {
|
|
57
|
+
/** What was written; `null` when nothing was. */
|
|
58
|
+
saved: SiteBrandFields | null;
|
|
59
|
+
/** The chosen files that did not make it, in field order. */
|
|
60
|
+
missed: SiteBrandSlot[];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Uploads the files a form is holding and writes what they became, one after
|
|
64
|
+
* the other so the storage count stays right. Never rejects: each failure is
|
|
65
|
+
* reported in `missed`.
|
|
66
|
+
*
|
|
67
|
+
* Each field stores `mediaNodeSrc`, as the Site settings cards do. A favicon
|
|
68
|
+
* left blank is the logo, the same asset rather than a second copy, and with
|
|
69
|
+
* no logo and no favicon nothing is written.
|
|
70
|
+
*/
|
|
71
|
+
export declare function saveSiteBrandFiles(files: Readonly<Partial<Record<SiteBrandSlot, File | null>>>, io: {
|
|
72
|
+
upload: (file: File, slot: SiteBrandSlot) => Promise<{
|
|
73
|
+
url?: string | null;
|
|
74
|
+
cdnPath?: string | null;
|
|
75
|
+
}>;
|
|
76
|
+
save: (fields: SiteBrandFields) => Promise<void>;
|
|
77
|
+
}): Promise<SiteBrandSaveOutcome>;
|
|
78
|
+
/** The one line that says which files did not make it, or `null` when all did. */
|
|
79
|
+
export declare function siteBrandMissedNotice(missed: readonly SiteBrandSlot[]): string | null;
|
|
80
|
+
/**
|
|
81
|
+
* The query that carries {@link SiteBrandSaveOutcome.missed} over a full page
|
|
82
|
+
* load: a site made on the auth host lands on the console host, and a notice
|
|
83
|
+
* shown on the way out would go with the page. Field names only.
|
|
84
|
+
*/
|
|
85
|
+
export declare const SITE_BRAND_MISSED_PARAM = "missed";
|
|
86
|
+
/** The fields a landing's {@link SITE_BRAND_MISSED_PARAM} names. Anything else is dropped. */
|
|
87
|
+
export declare function readSiteBrandMissed(value: string | null | undefined): SiteBrandSlot[];
|