@aglyn/aglyn 1.0.0-beta.255 → 1.0.0-beta.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/activity-labels.js +2 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +9 -3
- package/src/lib/app-utils/docs-help.generated.js +19 -2
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +110 -5
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/forms.d.ts +6 -3
- package/src/lib/app-utils/forms.js +14 -3
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/native-app-association.d.ts +14 -4
- package/src/lib/app-utils/native-app-association.js +23 -7
- package/src/lib/app-utils/native-app-association.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.js +2 -0
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +37 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
|
@@ -48,10 +48,12 @@
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* The console's top-level sections the native apps have no screen for, so a
|
|
51
|
-
* link to one stays in the browser
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* connect page and a plugin's public
|
|
51
|
+
* link to one stays in the browser: the sign-in page and SSO (the system
|
|
52
|
+
* auth session opens them for the app, and a universal link there would loop
|
|
53
|
+
* back into the app), the native hand-off and other `/auth` pages, the
|
|
54
|
+
* sign-out page, the site admin bar's connect page and a plugin's public
|
|
55
|
+
* device pages. The sections the apps finish natively are
|
|
56
|
+
* {@link NATIVE_AUTH_LINK_SEGMENTS}.
|
|
55
57
|
*
|
|
56
58
|
* The rest of the console's top-level sections (`admin`, `billing`,
|
|
57
59
|
* `manage`, `support`) and every workspace path are what the native
|
|
@@ -60,15 +62,23 @@
|
|
|
60
62
|
* native screen. `apps/console/specs/native-app-association.spec.ts` holds
|
|
61
63
|
* this list to the console's route set and the native link specs.
|
|
62
64
|
*/ export const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS = [
|
|
63
|
-
'account-recovery',
|
|
64
65
|
'auth',
|
|
65
66
|
'edit-access',
|
|
66
67
|
'kiosk',
|
|
67
|
-
'reset-password',
|
|
68
68
|
'signin',
|
|
69
69
|
'signout',
|
|
70
|
+
'sso'
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* The pre-sign-in sections whose exact page the apps finish natively: sign-up
|
|
74
|
+
* (with its `?plan=`), an emailed verification link, an emailed password
|
|
75
|
+
* reset and account recovery (`AuthFlow.swift`; Android's console intent
|
|
76
|
+
* filter lists the same four paths). Only the page itself opens the app;
|
|
77
|
+
* anything under it stays in the browser, where the console handles it.
|
|
78
|
+
*/ export const NATIVE_AUTH_LINK_SEGMENTS = [
|
|
79
|
+
'account-recovery',
|
|
80
|
+
'reset-password',
|
|
70
81
|
'signup',
|
|
71
|
-
'sso',
|
|
72
82
|
'verify-email'
|
|
73
83
|
];
|
|
74
84
|
/**
|
|
@@ -105,6 +115,12 @@
|
|
|
105
115
|
exclude: true
|
|
106
116
|
});
|
|
107
117
|
}
|
|
118
|
+
for (const segment of NATIVE_AUTH_LINK_SEGMENTS){
|
|
119
|
+
excluded.push({
|
|
120
|
+
'/': `/${segment}/*`,
|
|
121
|
+
exclude: true
|
|
122
|
+
});
|
|
123
|
+
}
|
|
108
124
|
return [
|
|
109
125
|
...excluded,
|
|
110
126
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.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 DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser. They are the sign-in and verification\n * flows (an emailed verify or reset link opened by an app that cannot finish\n * it would strand the person), the sign-out page, the site admin bar's\n * connect page and a plugin's public device pages.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'auth',\n 'edit-access',\n 'kiosk',\n 'reset-password',\n 'signin',\n 'signout',\n 'signup',\n 'sso',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.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 DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser: the sign-in page and SSO (the system\n * auth session opens them for the app, and a universal link there would loop\n * back into the app), the native hand-off and other `/auth` pages, the\n * sign-out page, the site admin bar's connect page and a plugin's public\n * device pages. The sections the apps finish natively are\n * {@link NATIVE_AUTH_LINK_SEGMENTS}.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'auth',\n 'edit-access',\n 'kiosk',\n 'signin',\n 'signout',\n 'sso',\n]\n\n/**\n * The pre-sign-in sections whose exact page the apps finish natively: sign-up\n * (with its `?plan=`), an emailed verification link, an emailed password\n * reset and account recovery (`AuthFlow.swift`; Android's console intent\n * filter lists the same four paths). Only the page itself opens the app;\n * anything under it stays in the browser, where the console handles it.\n */\nexport const NATIVE_AUTH_LINK_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'reset-password',\n 'signup',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n for (const segment of NATIVE_AUTH_LINK_SEGMENTS) {\n excluded.push({ '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","NATIVE_AUTH_LINK_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
|
|
@@ -54,6 +54,7 @@ import { SAFE_HREF_PATTERN } from "./screen-link-value.js";
|
|
|
54
54
|
'muiButton',
|
|
55
55
|
// MUI's form elements, one element each (they replace the all-in-one formField).
|
|
56
56
|
'muiCheckbox',
|
|
57
|
+
'muiDatePicker',
|
|
57
58
|
'muiFormControl',
|
|
58
59
|
'muiFormControlLabel',
|
|
59
60
|
'muiFormHelperText',
|
|
@@ -64,6 +65,7 @@ import { SAFE_HREF_PATTERN } from "./screen-link-value.js";
|
|
|
64
65
|
'muiRating',
|
|
65
66
|
'muiSelect',
|
|
66
67
|
'muiSelectOption',
|
|
68
|
+
'muiTimePicker',
|
|
67
69
|
'muiContainer',
|
|
68
70
|
'muiList',
|
|
69
71
|
'muiListItem',
|
|
@@ -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 'muiFormControl',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiFormLabel',\n 'muiInput',\n 'muiRadio',\n 'muiRadioGroup',\n 'muiRating',\n 'muiSelect',\n 'muiSelectOption',\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":"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"}
|
|
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":"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"}
|
|
@@ -241,9 +241,9 @@
|
|
|
241
241
|
"id": "inventory-sync",
|
|
242
242
|
"label": "Inventory sync",
|
|
243
243
|
"alwaysOnForWorkspace": true,
|
|
244
|
-
"description": "Stock, products and paid orders kept in step with your own Cin7 Core, inFlow or
|
|
244
|
+
"description": "Stock, products and paid orders kept in step with your own Cin7 Core, inFlow, Brightpearl or Ordoro account.",
|
|
245
245
|
"siteOff": {
|
|
246
|
-
"stops": "Switching Inventory sync off for this site stops its stock counts, products and paid orders syncing with Cin7 Core, inFlow or
|
|
246
|
+
"stops": "Switching Inventory sync off for this site stops its stock counts, products and paid orders syncing with Cin7 Core, inFlow, Brightpearl or Ordoro.",
|
|
247
247
|
"keeps": "The connection and its settings are kept, and everything already synced stays on both sides."
|
|
248
248
|
}
|
|
249
249
|
},
|
|
@@ -325,6 +325,16 @@
|
|
|
325
325
|
"keeps": "The audio in your media library is kept, and players keep working on the workspace's other sites."
|
|
326
326
|
}
|
|
327
327
|
},
|
|
328
|
+
{
|
|
329
|
+
"id": "help-desks",
|
|
330
|
+
"label": "Help desks",
|
|
331
|
+
"alwaysOnForWorkspace": true,
|
|
332
|
+
"description": "Your buyers and their recent orders in your own Gorgias, Re:amaze, Kustomer, Gladly or Richpanel account, and contact form messages as tickets.",
|
|
333
|
+
"siteOff": {
|
|
334
|
+
"stops": "Switching Help desks off for this site stops its buyers, orders and contact form messages reaching the help desks it connected.",
|
|
335
|
+
"keeps": "The connections are kept, and everything already sent stays in the help desk."
|
|
336
|
+
}
|
|
337
|
+
},
|
|
328
338
|
{
|
|
329
339
|
"id": "ad-conversions",
|
|
330
340
|
"label": "Ad conversions",
|
|
@@ -375,6 +385,7 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
375
385
|
"review-platforms": "console-only",
|
|
376
386
|
"weglot": "elements",
|
|
377
387
|
"music": "elements",
|
|
388
|
+
"help-desks": "console-only",
|
|
378
389
|
"ad-conversions": "console-only"
|
|
379
390
|
};
|
|
380
391
|
/**
|
|
@@ -1924,6 +1935,15 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
1924
1935
|
* Where a person reads each record kind a first-party plugin's console page
|
|
1925
1936
|
* shows, declared by that plugin (AGL-3080), for a server's notification link.
|
|
1926
1937
|
*/ export const PLUGIN_RECORD_PAGES_DECLARED = [
|
|
1938
|
+
{
|
|
1939
|
+
"pluginId": "commerce",
|
|
1940
|
+
"kind": "order",
|
|
1941
|
+
"path": "/products/orders",
|
|
1942
|
+
"record": {
|
|
1943
|
+
"path": "/products/orders",
|
|
1944
|
+
"param": "order"
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1927
1947
|
{
|
|
1928
1948
|
"pluginId": "inbox",
|
|
1929
1949
|
"kind": "formSubmission",
|
|
@@ -2326,6 +2346,21 @@ export const PUBLISHED_SITE_IMPACT = {
|
|
|
2326
2346
|
"name": "marketingPlatformEvents",
|
|
2327
2347
|
"orgField": "orgId"
|
|
2328
2348
|
},
|
|
2349
|
+
{
|
|
2350
|
+
"pluginId": "help-desks",
|
|
2351
|
+
"name": "helpDeskConnections",
|
|
2352
|
+
"orgField": "orgId"
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
"pluginId": "help-desks",
|
|
2356
|
+
"name": "helpDeskOutbox",
|
|
2357
|
+
"orgField": "orgId"
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
"pluginId": "help-desks",
|
|
2361
|
+
"name": "helpDeskCustomers",
|
|
2362
|
+
"orgField": "orgId"
|
|
2363
|
+
},
|
|
2329
2364
|
{
|
|
2330
2365
|
"pluginId": "ad-conversions",
|
|
2331
2366
|
"name": "adConversionConnections",
|