@aglyn/aglyn 1.0.0-beta.287 → 1.0.0-beta.289
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/business-profile-values.d.ts +15 -3
- package/src/lib/app-utils/business-profile-values.js +19 -3
- package/src/lib/app-utils/business-profile-values.js.map +1 -1
- package/src/lib/app-utils/business-profile.d.ts +5 -3
- package/src/lib/app-utils/business-profile.js +2 -1
- package/src/lib/app-utils/business-profile.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +5 -0
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +7 -1
- package/src/lib/app-utils/docs-help.generated.js +24 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +111 -27
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/element-motion.js +1 -0
- package/src/lib/app-utils/element-motion.js.map +1 -1
- package/src/lib/app-utils/host-permissions.d.ts +31 -1
- package/src/lib/app-utils/host-permissions.js +45 -5
- package/src/lib/app-utils/host-permissions.js.map +1 -1
- package/src/lib/app-utils/host-status.d.ts +2 -1
- package/src/lib/app-utils/host-status.js +10 -0
- package/src/lib/app-utils/host-status.js.map +1 -1
- package/src/lib/app-utils/org-permissions.d.ts +8 -0
- package/src/lib/app-utils/org-permissions.js +2 -0
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.js +3 -3
- package/src/lib/app-utils/org-role-grants.js.map +1 -1
- package/src/lib/app-utils/screen-slug.d.ts +5 -5
- package/src/lib/app-utils/screen-slug.js +5 -5
- package/src/lib/app-utils/screen-slug.js.map +1 -1
- package/src/lib/app-utils/search-indexing.d.ts +18 -0
- package/src/lib/app-utils/search-indexing.js +16 -2
- package/src/lib/app-utils/search-indexing.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +1 -0
- package/src/lib/app-utils/server.js +2 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-preview.d.ts +153 -0
- package/src/lib/app-utils/site-preview.js +167 -0
- package/src/lib/app-utils/site-preview.js.map +1 -0
- package/src/lib/foundation/definitions/platform.types.d.ts +27 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +17 -1
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +4 -2
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
|
@@ -158,6 +158,8 @@ function replaceEntries(target, entries) {
|
|
|
158
158
|
narrowsPerSite: _extends({}, declared.narrowsPerSite)
|
|
159
159
|
} : {}, declared.presetFromSiteRole && declared.hostRoleDefaults ? {
|
|
160
160
|
presetFromSiteRole: true
|
|
161
|
+
} : {}, declared.presetFromSiteRole && declared.hostRoleDefaults && declared.workspaceMembersOnly ? {
|
|
162
|
+
workspaceMembersOnly: true
|
|
161
163
|
} : {}));
|
|
162
164
|
if (declared.source === 'tier') tierKeys.push(declared.key);
|
|
163
165
|
if (!groups.some((group)=>group.id === declared.group)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permissions.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 type { AglynOrgMember, OrgRole } from '../foundation'\nimport {\n listPluginCatalogPermissions,\n subscribePluginEntitlements,\n} from '../plugin-manager/plugin-entitlements'\nimport {\n permissionRefinementOf,\n refinePermissionMap,\n subscribePluginPermissions,\n type PermissionRefinement,\n} from '../plugin-manager/plugin-permissions'\nimport { ORG_ROLE_PERMISSION_KEYS } from './org-roles'\nimport {\n CORE_ORG_PERMISSION_ENTRIES,\n CORE_ORG_PERMISSION_GROUPS,\n CORE_ORG_ROLE_GRANTS,\n CORE_SITE_ROLE_PRESETS,\n type OrgPermissionGroup,\n type OrgPermissionScope,\n} from './org-role-grants'\n\nexport type { OrgPermissionGroup, OrgPermissionScope }\n\n/**\n * Granular org permissions (AGL-243) layered on the 4 org roles: each\n * role maps to a default permission set; custom roles\n * (`orgs/{orgId}/roles/{roleId}`) and per-member overrides refine it, so\n * e.g. an editor restricted to one host can also be granted billing\n * visibility — or an admin stripped of it — without inventing new roles.\n * The console hides unpermitted surfaces; the org API routes are the\n * enforcement point.\n */\nexport type CoreOrgPermission =\n | 'org.settings'\n | 'org.auditLog'\n | 'org.apiKeys'\n | 'org.privacy'\n | 'org.holds'\n | 'org.export'\n | 'org.sendingDomains'\n | 'org.businessProfile'\n | 'org.media'\n | 'org.eraseRecords'\n | 'billing.view'\n | 'billing.manage'\n | 'members.manage'\n | 'hosts.create'\n | 'hosts.delete'\n | 'data.manage'\n | 'marketplace.publish'\n | 'plugins.install'\n | 'site.edit'\n | 'site.publish'\n | 'site.sensitive'\n | 'site.manage'\n\n/**\n * A key of the catalog: a core key above, or a key a plugin declares into\n * the catalog through `registerPluginEntitlements` (AGL-2984). A declared key\n * is stored on custom roles and member overrides, layered and resolved\n * exactly like a core key.\n */\nexport type OrgPermission = CoreOrgPermission | (string & {})\n\n/**\n * Legacy boolean permission map derived from the granular `OrgPermission`\n * set (AGL-243). Kept for the console surfaces that predate the granular\n * model; lives here so relocated feature plugins can accept it as a prop\n * (AGL-395).\n */\nexport interface OrgPermissions {\n createHosts: boolean\n editHosts: boolean\n editBilling: boolean\n publishToMarketplace: boolean\n installPlugins: boolean\n manageMembers: boolean\n}\n\nexport interface OrgPermissionDefinition {\n key: OrgPermission\n label: string\n description: string\n /** The plugin that declared the key; absent for a core key. */\n pluginId?: string\n /** The `ORG_PERMISSION_GROUPS` id the role editor files it under (AGL-4047). */\n group: string\n /** Where it is decided: `org`, or `site` for a key a site collaborator holds per site. */\n scope: OrgPermissionScope\n /** Each org role's verdict when nothing refines it (`DEFAULT_ROLE_PERMISSIONS`). */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /** Each host role's verdict, for a `site` key. */\n hostRoleDefaults?: Readonly<Record<string, boolean>>\n /**\n * The broader key this one is a finer part of (AGL-4047): never granted\n * without it, and following it where no custom role or override sets this\n * key. Only a plugin's key refines, and only a key the catalog holds.\n */\n refines?: OrgPermission\n /**\n * The key this one follows where no custom role or override sets it,\n * without needing it (AGL-4047): granted on its own, it holds while that\n * key is refused. Only a plugin's key follows, and only a key the catalog\n * holds; a key that refines does not also follow.\n */\n follows?: OrgPermission\n /**\n * Present when a site can narrow the key (AGL-4047): where a member who\n * works across the workspace has narrower access to one site than their\n * workspace role, whether the key follows that site role there is a\n * control, set per key on a custom role (`siteNarrowing`) and per member\n * (`AglynOrgMember.siteNarrowing`), and this is each built-in role's\n * default for it. `narrowPermissionsToSite` applies it. Only a plugin's key.\n */\n narrowsPerSite?: Readonly<Record<OrgRole, boolean>>\n /**\n * The key's preset is the member's role ON THE SITE a request names, for\n * every member (AGL-4047): `hostRoleDefaults` read for the access they hold\n * there, then their custom role, their own permissions and that site's\n * toggle. `roleDefaults` is the answer with no site named.\n * `resolveSiteRolePresetPermissions` decides it. The core's site keys\n * (`CORE_SITE_ROLE_PRESETS`) and a plugin's key with `hostRoleDefaults`.\n */\n presetFromSiteRole?: true\n}\n\n/**\n * The core keys, in display order for role editors.\n *\n * ⚠️ **EVERY KEY HERE MUST BE ENFORCED SERVER-SIDE.** A permission a customer\n * can tick that changes nothing is worse than its absence, because it implies\n * a control that does not exist — an owner unticks \"delete sites\", hands the\n * role out, and the member deletes sites while the console does not even dim\n * the button. Three of the eleven were in exactly that state (AGL-2444).\n *\n * `marketing.manage` was removed rather than wired, and the reason is worth\n * keeping: announcement bars, popups and campaigns live at\n * `hosts/{hostId}/overlays|campaigns|experiments` and are written\n * client-direct against the security rules, which gate on the HOST role.\n * There is no org-level boundary for it to sit on, and the action it named is\n * not org-scoped at all. Wiring it would have meant inventing one and\n * producing a second permission that looks enforced and is not. If a granular\n * marketing permission is wanted it belongs in the plugin-declared mechanism\n * (AGL-435), scoped to a host — a product decision, not this repair.\n *\n * `apps/console/specs/org-permissions-are-enforced.spec.ts` fails the build if\n * a key here, or a key a plugin declares, gains no server-side consumer.\n */\nconst CORE_ORG_PERMISSIONS: readonly OrgPermissionDefinition[] =\n // One table, shared with the native apps through the contracts (AGL-3950).\n CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({\n ...entry,\n key: entry.key as CoreOrgPermission,\n group: entry.group ?? 'organization',\n scope: entry.scope ?? 'org',\n // A site key's preset is the member's role on the site (AGL-4047).\n ...(CORE_SITE_ROLE_PRESETS[entry.key]\n ? { hostRoleDefaults: { ...CORE_SITE_ROLE_PRESETS[entry.key] }, presetFromSiteRole: true as const }\n : {}),\n roleDefaults: {\n owner: CORE_ORG_ROLE_GRANTS.owner.includes(entry.key),\n admin: CORE_ORG_ROLE_GRANTS.admin.includes(entry.key),\n editor: CORE_ORG_ROLE_GRANTS.editor.includes(entry.key),\n viewer: CORE_ORG_ROLE_GRANTS.viewer.includes(entry.key),\n },\n }))\n\n/** The core keys: a plugin may add keys beside them and never redefine one. */\nconst CORE_ORG_PERMISSION_KEYS: ReadonlySet<string> = new Set(\n CORE_ORG_PERMISSIONS.map((definition) => definition.key),\n)\n\n/** The legacy camelCase projection's keys, which no declaration may take either. */\nconst LEGACY_PERMISSION_KEYS: ReadonlySet<string> = new Set<string>(\n ORG_ROLE_PERMISSION_KEYS,\n)\n\n/** The org roles, in the order `DEFAULT_ROLE_PERMISSIONS` lists them. */\nconst ORG_ROLES: readonly OrgRole[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * Every permission, in display order for role editors: the core keys, then\n * the keys plugins declare through `registerPluginEntitlements`, in the\n * plugins' catalog order (AGL-2984).\n *\n * LIVE, and kept in step in place. A plugin's declarations register at\n * module scope — at boot on the server, before the first paint in the\n * console — which can be after a module holding this binding was evaluated.\n * So this array, the key list, the role defaults and the empty map below are\n * rewritten in place on every registration instead of being computed once\n * when this module loads, and whoever holds the binding reads the declared\n * keys with the core ones.\n */\nexport const ORG_PERMISSIONS: readonly OrgPermissionDefinition[] = []\n\nexport const ORG_PERMISSION_KEYS: readonly OrgPermission[] = []\n\n/**\n * The groups the catalog's keys are filed under, in display order (AGL-4047):\n * the core groups, then one per plugin group a declaration names. Live, like\n * `ORG_PERMISSIONS`; a group with no key is left out.\n */\nexport const ORG_PERMISSION_GROUPS: readonly OrgPermissionGroup[] = []\n\n/**\n * The catalog keys that came through `registerPluginPermissions` (AGL-4047):\n * the keys the per-tier resolver (`resolveRolePermissions`) also answers,\n * which a caller holding a catalog-resolved map lays over that resolver's\n * answer so a custom role reaches them. Live.\n */\nexport const TIER_PERMISSION_KEYS: readonly OrgPermission[] = []\n\n/**\n * Every catalog key that refines a broader one, with that key (AGL-4047),\n * and every key that only follows one, marked `'follows'`. Live, like\n * `ORG_PERMISSIONS`. A resolver lays it over its map with\n * `refinePermissionMap`.\n */\nexport const ORG_PERMISSION_REFINEMENTS: readonly PermissionRefinement[] = []\n\nconst NO_PERMISSIONS = {} as Record<OrgPermission, boolean>\n\n/**\n * Role → default permission set. Owner/admin hold everything (owner-only\n * actions like org deletion stay role-checked, not permission-checked);\n * editors work on content but see no money or roster controls; viewers read\n * only. A declared key's default for each role is the one its declaration\n * names.\n */\nexport const DEFAULT_ROLE_PERMISSIONS: Record<\n OrgRole,\n Record<OrgPermission, boolean>\n> = {\n owner: {} as Record<OrgPermission, boolean>,\n admin: {} as Record<OrgPermission, boolean>,\n editor: {} as Record<OrgPermission, boolean>,\n viewer: {} as Record<OrgPermission, boolean>,\n}\n\n/** Refused declarations already logged, so a refusal is reported once. */\nconst reportedRefusals = new Set<string>()\n\nfunction replaceItems<T>(target: readonly T[], items: readonly T[]): void {\n const list = target as T[]\n list.length = 0\n list.push(...items)\n}\n\nfunction replaceEntries(\n target: Record<string, boolean>,\n entries: Record<string, boolean>,\n): void {\n for (const key of Object.keys(target)) delete target[key]\n Object.assign(target, entries)\n}\n\n/**\n * Rebuilds the live catalog: the core keys, then every key a plugin declared\n * through either declaration API (`listPluginCatalogPermissions`, AGL-4047).\n * A declaration naming a core or legacy key is refused — that key keeps its\n * own meaning and defaults — and logged once.\n */\nfunction syncCatalog(): void {\n const definitions: OrgPermissionDefinition[] = [...CORE_ORG_PERMISSIONS]\n const tierKeys: string[] = []\n const groups: OrgPermissionGroup[] = CORE_ORG_PERMISSION_GROUPS.map((group) => ({ ...group }))\n for (const declared of listPluginCatalogPermissions()) {\n if (\n CORE_ORG_PERMISSION_KEYS.has(declared.key) ||\n LEGACY_PERMISSION_KEYS.has(declared.key)\n ) {\n const refusal = `${declared.pluginId}\\x00${declared.key}`\n if (!reportedRefusals.has(refusal)) {\n reportedRefusals.add(refusal)\n console.error(\n `[plugins] refused catalog permission \"${declared.key}\" to ` +\n `\"${declared.pluginId}\": the key belongs to the core catalog`,\n )\n }\n continue\n }\n definitions.push({\n key: declared.key,\n label: declared.label,\n description: declared.description,\n pluginId: declared.pluginId,\n group: declared.group,\n scope: declared.hostRoleDefaults ? 'site' : 'org',\n roleDefaults: declared.roleDefaults,\n ...(declared.hostRoleDefaults ? { hostRoleDefaults: declared.hostRoleDefaults } : {}),\n ...(declared.refines ? { refines: declared.refines } : {}),\n ...(declared.follows && !declared.refines ? { follows: declared.follows } : {}),\n ...(declared.narrowsPerSite ? { narrowsPerSite: { ...declared.narrowsPerSite } } : {}),\n ...(declared.presetFromSiteRole && declared.hostRoleDefaults ? { presetFromSiteRole: true as const } : {}),\n })\n if (declared.source === 'tier') tierKeys.push(declared.key)\n if (!groups.some((group) => group.id === declared.group)) {\n groups.push({ id: declared.group, label: declared.groupLabel, pluginId: declared.pluginId })\n }\n }\n const keys = definitions.map((definition) => definition.key)\n const known = new Set<string>(keys)\n // A refinement of a key the catalog does not hold refines nothing, and is\n // dropped from the definition so no editor offers a link to nowhere.\n for (const definition of definitions) {\n if (definition.refines && (!known.has(definition.refines) || definition.refines === definition.key)) {\n delete definition.refines\n }\n if (definition.follows && (!known.has(definition.follows) || definition.follows === definition.key)) {\n delete definition.follows\n }\n }\n replaceItems(\n ORG_PERMISSION_REFINEMENTS,\n definitions.flatMap((definition) => {\n const refinement = permissionRefinementOf(definition)\n return refinement ? [refinement] : []\n }),\n )\n replaceItems(ORG_PERMISSIONS, definitions)\n replaceItems(ORG_PERMISSION_KEYS, keys)\n replaceItems(\n ORG_PERMISSION_GROUPS,\n groups.filter((group) => definitions.some((definition) => definition.group === group.id)),\n )\n replaceItems(TIER_PERMISSION_KEYS, tierKeys)\n replaceEntries(NO_PERMISSIONS, Object.fromEntries(keys.map((key) => [key, false])))\n for (const role of ORG_ROLES) {\n replaceEntries(\n DEFAULT_ROLE_PERMISSIONS[role],\n Object.fromEntries(\n definitions.map((definition) => [definition.key, definition.roleDefaults[role] === true]),\n ),\n )\n }\n CATALOG_AND_LEGACY_KEYS.clear()\n for (const key of [...keys, ...LEGACY_PERMISSION_KEYS]) {\n CATALOG_AND_LEGACY_KEYS.add(key)\n }\n}\n\n/**\n * The `registerPluginPermissions` keys of a catalog-resolved map (AGL-4047).\n *\n * Those keys are answered twice: by the per-tier resolver\n * (`resolveRolePermissions`, the role's tier default and the member's\n * override) and, now that they are catalog keys, by `resolveOrgPermissions`\n * with every layer — the custom role's included. A caller that builds its\n * map from the first spreads this over it, so the custom role a member\n * carries reaches `managePos` the way it reaches `hosts.create`.\n */\nexport function tierPermissionsOf(\n granted: Readonly<Record<string, boolean>> | null | undefined,\n): Record<string, boolean> {\n const picked: Record<string, boolean> = {}\n if (!granted) return picked\n for (const key of TIER_PERMISSION_KEYS) {\n if (typeof granted[key] === 'boolean') picked[key] = granted[key]\n }\n return picked\n}\n\n/** One catalog key by name, or undefined. */\nexport function orgPermissionDefinition(\n permission: OrgPermission,\n): OrgPermissionDefinition | undefined {\n return ORG_PERMISSIONS.find((definition) => definition.key === permission)\n}\n\n/** `orgs/{orgId}/roles/{roleId}` — a named custom permission set. */\nexport interface AglynOrgCustomRole {\n $id?: string\n name?: string\n description?: string\n /** Full permission map; missing keys read as false. */\n permissions?: Partial<Record<OrgPermission, boolean>>\n /**\n * Per key a site can narrow (AGL-4047): `true` follows the site role on a\n * site where the member's access is narrower than their workspace role,\n * `false` keeps the permission there. A key it leaves out follows the\n * built-in role's default (`OrgPermissionDefinition.narrowsPerSite`), and\n * a member's own `siteNarrowing` wins over it.\n */\n siteNarrowing?: Partial<Record<OrgPermission, boolean>>\n}\n\n/**\n * Effective permission map for a member: the org role's defaults, then\n * the custom role's map (when the member has `roleId` and the role doc\n * is supplied), then per-member overrides — later layers win key-by-key.\n */\nexport function resolveOrgPermissions(\n member:\n | (Partial<AglynOrgMember> & {\n roleId?: string\n permissions?: Partial<Record<OrgPermission, boolean>>\n })\n | null\n | undefined,\n customRole?: AglynOrgCustomRole | null,\n): Record<OrgPermission, boolean> {\n if (!member) return NO_PERMISSIONS\n const role = (member.role ?? 'viewer') as OrgRole\n const base = DEFAULT_ROLE_PERMISSIONS[role] ?? NO_PERMISSIONS\n const merged: Record<OrgPermission, boolean> = { ...base }\n if (member.roleId && customRole?.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = customRole.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n if (member.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = member.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n // A key that refines a broader one (AGL-4047) follows it where neither\n // layer set the key, and is never held without it; a key that only follows\n // one takes its verdict where neither layer set the key, and nothing more.\n return refinePermissionMap(merged, ORG_PERMISSION_REFINEMENTS, [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ])\n}\n\n/**\n * The values a custom role and a per-member override set EXPLICITLY for\n * permission keys outside this catalog — the keys plugins declare through\n * `registerPluginPermissions` (AGL-2974).\n *\n * `resolveOrgPermissions` walks the catalog alone, so a plugin key stored on\n * a custom role or a member document never reached the `resolvedPermissions`\n * stamp the Firestore rules read. A rule gating on one could then honor\n * neither a grant to an editor nor a revocation from an admin. This carries\n * exactly those stored decisions, custom role first and the per-member\n * override over it, which is the precedence the resolver applies.\n *\n * Tier DEFAULTS are deliberately absent. They live in the plugin registry,\n * which is populated per process by whichever plugin bundles that process\n * loaded, and a stamp computed from it would differ by which route happened\n * to write it. A rule supplies the default itself from the member's role,\n * the way `memberResolves` is always conjoined with a role gate.\n *\n * The legacy six camelCase keys are excluded as well: they are the catalog's\n * older projection, not plugin keys, and no rule reads them.\n */\nexport function explicitPluginPermissionValues(\n member:\n | {\n roleId?: string\n permissions?: Partial<Record<string, unknown>> | null\n }\n | null\n | undefined,\n customRole?: { permissions?: Partial<Record<string, unknown>> | null } | null,\n): Record<string, boolean> {\n const values: Record<string, boolean> = {}\n if (!member) return values\n const layers = [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ]\n for (const layer of layers) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (typeof value !== 'boolean') continue\n if (CATALOG_AND_LEGACY_KEYS.has(key)) continue\n values[key] = value\n }\n }\n return values\n}\n\n/**\n * Keys {@link explicitPluginPermissionValues} leaves to the catalog: every\n * catalog key, declared ones included, and the legacy six. Kept in step by\n * `syncCatalog`.\n */\nconst CATALOG_AND_LEGACY_KEYS = new Set<string>()\n\n/**\n * The `resolvedPermissions` map stamped on a member document for the\n * Firestore rules: the catalog verdict with every layer applied, and beside\n * it the plugin keys a custom role or an override set explicitly.\n *\n * One function for every writer of the stamp, so the org-create transaction\n * and `syncOrgAuthProjections` cannot project different maps. The catalog\n * spreads last; the two key spaces do not overlap, and the order keeps it\n * true that nothing but the resolver decides a catalog key.\n */\nexport function projectMemberResolvedPermissions(\n member: Parameters<typeof resolveOrgPermissions>[0],\n customRole?: AglynOrgCustomRole | null,\n): Record<string, boolean> {\n return {\n ...explicitPluginPermissionValues(member, customRole),\n ...resolveOrgPermissions(member, customRole),\n }\n}\n\n/** Single-permission convenience over `resolveOrgPermissions`. */\nexport function hasOrgPermission(\n member: Parameters<typeof resolveOrgPermissions>[0],\n permission: OrgPermission,\n customRole?: AglynOrgCustomRole | null,\n): boolean {\n return resolveOrgPermissions(member, customRole)[permission]\n}\n\n/** The catalog label for a key — the words a refusal names — or the key itself. */\nexport function orgPermissionLabel(permission: OrgPermission): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === permission)?.label ??\n permission\n )\n}\n\n/**\n * The sentence a door answers when a member does not hold a key (AGL-4047):\n * it names the permission by its catalog label and who can change it, never\n * a role.\n */\nexport function permissionRefusalText(permission: OrgPermission): string {\n return `Your role does not include \"${orgPermissionLabel(permission)}\" — ask an organization admin`\n}\n\n/** The keys plugins declared into the catalog, in catalog order. */\nexport function pluginOrgPermissionKeys(): OrgPermission[] {\n return ORG_PERMISSIONS.filter((definition) => definition.pluginId).map(\n (definition) => definition.key,\n )\n}\n\n/** One declared catalog key that moved, and the direction it moved in. */\nexport interface PluginPermissionChange {\n permission: string\n granted: boolean\n}\n\n/**\n * The keys PLUGINS declared into the catalog whose value moved between two\n * maps, in the new map's order — what a member, role or collaborator write\n * raises `org.permissions.changed` for, once per key, so the plugin that owns\n * a key can record who moved it.\n *\n * A key the new map leaves unset is not a change: on a role it defers to the\n * base role, on a site toggle to the host role, and neither is a grant or a\n * revocation of its own. A key set for the first time is a change, because\n * the verdict it produces is no longer the default's. The core keys and the\n * legacy six raise nothing; their writes are recorded by the routes' own\n * activity sentences.\n */\nexport function pluginPermissionChanges(\n before: Partial<Record<string, unknown>> | null | undefined,\n after: Partial<Record<string, unknown>> | null | undefined,\n): PluginPermissionChange[] {\n const changes: PluginPermissionChange[] = []\n for (const [key, next] of Object.entries(after ?? {})) {\n if (typeof next !== 'boolean') continue\n if (CORE_ORG_PERMISSION_KEYS.has(key) || LEGACY_PERMISSION_KEYS.has(key)) continue\n if (before?.[key] === next) continue\n changes.push({ permission: key, granted: next })\n }\n return changes\n}\n\n/**\n * The granular (dotted) set projected onto the legacy boolean map\n * (AGL-2350).\n *\n * ## Why this lives here rather than in the console hook\n *\n * It was a private function in `apps/console/hooks/use-org-permissions.ts`,\n * which meant the CLIENT translated the stored permission model into the\n * legacy flags and the SERVER did not translate at all — it derived the\n * legacy flags straight from the built-in role tier and dropped custom roles\n * and per-member overrides on the floor. The two disagreed exactly on the\n * feature `custom-roles.md` sells, in both directions: a permission granted\n * by a custom role showed in the UI and 403'd on POST, and one revoked by an\n * override was hidden in the UI and still succeeded on POST.\n *\n * One exported copy is what stops them drifting again.\n *\n * ## The key spaces are NOT interchangeable, and that is the trap\n *\n * Two permission vocabularies exist. The stored one is DOTTED\n * (`plugins.install`) — `apps/console/app/api/orgs/roles/route.ts` sanitizes\n * against `ORG_PERMISSION_KEYS` before writing, and `AglynOrgMember`\n * `permissions` is typed to it. The legacy one is camelCase\n * (`installPlugins`).\n *\n * `resolveRolePermissions` in `org-roles.ts` accepts `overrides` and\n * `customRoles` arguments keyed by the CAMELCASE space. Feeding it the real\n * stored documents therefore matches no key and changes nothing — it looks\n * like wiring the feature up while doing precisely nothing. Those two\n * parameters are used by nothing but that module's own spec. Translate\n * through here instead.\n *\n * `editHosts` is derived from the ROLE, not from a dotted key: it has no\n * counterpart in the granular catalog and never had one.\n */\nexport function toLegacyPermissions(\n granted: Record<OrgPermission, boolean>,\n role: OrgRole | null | undefined,\n): OrgPermissions {\n return {\n createHosts: granted['hosts.create'],\n editHosts: role !== 'viewer',\n editBilling: granted['billing.manage'],\n publishToMarketplace: granted['marketplace.publish'],\n installPlugins: granted['plugins.install'],\n manageMembers: granted['members.manage'],\n }\n}\n\nsyncCatalog()\nsubscribePluginEntitlements(syncCatalog)\n// The second declaration API feeds the same catalog (AGL-4047).\nsubscribePluginPermissions(syncCatalog)\n"],"names":["listPluginCatalogPermissions","subscribePluginEntitlements","permissionRefinementOf","refinePermissionMap","subscribePluginPermissions","ORG_ROLE_PERMISSION_KEYS","CORE_ORG_PERMISSION_ENTRIES","CORE_ORG_PERMISSION_GROUPS","CORE_ORG_ROLE_GRANTS","CORE_SITE_ROLE_PRESETS","CORE_ORG_PERMISSIONS","map","entry","key","group","scope","hostRoleDefaults","presetFromSiteRole","roleDefaults","owner","includes","admin","editor","viewer","CORE_ORG_PERMISSION_KEYS","Set","definition","LEGACY_PERMISSION_KEYS","ORG_ROLES","ORG_PERMISSIONS","ORG_PERMISSION_KEYS","ORG_PERMISSION_GROUPS","TIER_PERMISSION_KEYS","ORG_PERMISSION_REFINEMENTS","NO_PERMISSIONS","DEFAULT_ROLE_PERMISSIONS","reportedRefusals","replaceItems","target","items","list","length","push","replaceEntries","entries","Object","keys","assign","syncCatalog","definitions","tierKeys","groups","declared","has","refusal","pluginId","add","console","error","label","description","refines","follows","narrowsPerSite","source","some","id","groupLabel","known","flatMap","refinement","filter","fromEntries","role","CATALOG_AND_LEGACY_KEYS","clear","tierPermissionsOf","granted","picked","orgPermissionDefinition","permission","find","resolveOrgPermissions","member","customRole","base","merged","roleId","permissions","value","undefined","explicitPluginPermissionValues","values","layers","layer","projectMemberResolvedPermissions","hasOrgPermission","orgPermissionLabel","permissionRefusalText","pluginOrgPermissionKeys","pluginPermissionChanges","before","after","changes","next","toLegacyPermissions","createHosts","editHosts","editBilling","publishToMarketplace","installPlugins","manageMembers"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-permissions.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 type { AglynOrgMember, OrgRole } from '../foundation'\nimport {\n listPluginCatalogPermissions,\n subscribePluginEntitlements,\n} from '../plugin-manager/plugin-entitlements'\nimport {\n permissionRefinementOf,\n refinePermissionMap,\n subscribePluginPermissions,\n type PermissionRefinement,\n} from '../plugin-manager/plugin-permissions'\nimport { ORG_ROLE_PERMISSION_KEYS } from './org-roles'\nimport {\n CORE_ORG_PERMISSION_ENTRIES,\n CORE_ORG_PERMISSION_GROUPS,\n CORE_ORG_ROLE_GRANTS,\n CORE_SITE_ROLE_PRESETS,\n type OrgPermissionGroup,\n type OrgPermissionScope,\n} from './org-role-grants'\n\nexport type { OrgPermissionGroup, OrgPermissionScope }\n\n/**\n * Granular org permissions (AGL-243) layered on the 4 org roles: each\n * role maps to a default permission set; custom roles\n * (`orgs/{orgId}/roles/{roleId}`) and per-member overrides refine it, so\n * e.g. an editor restricted to one host can also be granted billing\n * visibility — or an admin stripped of it — without inventing new roles.\n * The console hides unpermitted surfaces; the org API routes are the\n * enforcement point.\n */\nexport type CoreOrgPermission =\n | 'org.settings'\n | 'org.auditLog'\n | 'org.apiKeys'\n | 'org.privacy'\n | 'org.holds'\n | 'org.export'\n | 'org.sendingDomains'\n | 'org.businessProfile'\n | 'org.media'\n | 'org.eraseRecords'\n | 'billing.view'\n | 'billing.manage'\n | 'members.manage'\n | 'hosts.create'\n | 'hosts.delete'\n | 'data.manage'\n | 'marketplace.publish'\n | 'plugins.install'\n | 'site.edit'\n | 'site.publish'\n | 'site.sensitive'\n | 'site.manage'\n\n/**\n * A key of the catalog: a core key above, or a key a plugin declares into\n * the catalog through `registerPluginEntitlements` (AGL-2984). A declared key\n * is stored on custom roles and member overrides, layered and resolved\n * exactly like a core key.\n */\nexport type OrgPermission = CoreOrgPermission | (string & {})\n\n/**\n * Legacy boolean permission map derived from the granular `OrgPermission`\n * set (AGL-243). Kept for the console surfaces that predate the granular\n * model; lives here so relocated feature plugins can accept it as a prop\n * (AGL-395).\n */\nexport interface OrgPermissions {\n createHosts: boolean\n editHosts: boolean\n editBilling: boolean\n publishToMarketplace: boolean\n installPlugins: boolean\n manageMembers: boolean\n}\n\nexport interface OrgPermissionDefinition {\n key: OrgPermission\n label: string\n description: string\n /** The plugin that declared the key; absent for a core key. */\n pluginId?: string\n /** The `ORG_PERMISSION_GROUPS` id the role editor files it under (AGL-4047). */\n group: string\n /** Where it is decided: `org`, or `site` for a key a site collaborator holds per site. */\n scope: OrgPermissionScope\n /** Each org role's verdict when nothing refines it (`DEFAULT_ROLE_PERMISSIONS`). */\n roleDefaults: Readonly<Record<OrgRole, boolean>>\n /** Each host role's verdict, for a `site` key. */\n hostRoleDefaults?: Readonly<Record<string, boolean>>\n /**\n * The broader key this one is a finer part of (AGL-4047): never granted\n * without it, and following it where no custom role or override sets this\n * key. Only a plugin's key refines, and only a key the catalog holds.\n */\n refines?: OrgPermission\n /**\n * The key this one follows where no custom role or override sets it,\n * without needing it (AGL-4047): granted on its own, it holds while that\n * key is refused. Only a plugin's key follows, and only a key the catalog\n * holds; a key that refines does not also follow.\n */\n follows?: OrgPermission\n /**\n * Present when a site can narrow the key (AGL-4047): where a member who\n * works across the workspace has narrower access to one site than their\n * workspace role, whether the key follows that site role there is a\n * control, set per key on a custom role (`siteNarrowing`) and per member\n * (`AglynOrgMember.siteNarrowing`), and this is each built-in role's\n * default for it. `narrowPermissionsToSite` applies it. Only a plugin's key.\n */\n narrowsPerSite?: Readonly<Record<OrgRole, boolean>>\n /**\n * The key's preset is the member's role ON THE SITE a request names, for\n * every member (AGL-4047): `hostRoleDefaults` read for the access they hold\n * there, then their custom role, their own permissions and that site's\n * toggle. `roleDefaults` is the answer with no site named.\n * `resolveSiteRolePresetPermissions` decides it. The core's site keys\n * (`CORE_SITE_ROLE_PRESETS`) and a plugin's key with `hostRoleDefaults`.\n */\n presetFromSiteRole?: true\n /**\n * With `presetFromSiteRole`: the site role is the preset only for a member\n * of the whole workspace (AGL-4047). A site collaborator's preset is\n * refusal, whatever their site role, and only the site's toggle grants it\n * to them. A plugin's key that replaces a gate asking for an admin of the\n * whole workspace who is also the site's admin.\n */\n workspaceMembersOnly?: true\n}\n\n/**\n * The core keys, in display order for role editors.\n *\n * ⚠️ **EVERY KEY HERE MUST BE ENFORCED SERVER-SIDE.** A permission a customer\n * can tick that changes nothing is worse than its absence, because it implies\n * a control that does not exist — an owner unticks \"delete sites\", hands the\n * role out, and the member deletes sites while the console does not even dim\n * the button. Three of the eleven were in exactly that state (AGL-2444).\n *\n * `marketing.manage` was removed rather than wired, and the reason is worth\n * keeping: announcement bars, popups and campaigns live at\n * `hosts/{hostId}/overlays|campaigns|experiments` and are written\n * client-direct against the security rules, which gate on the HOST role.\n * There is no org-level boundary for it to sit on, and the action it named is\n * not org-scoped at all. Wiring it would have meant inventing one and\n * producing a second permission that looks enforced and is not. If a granular\n * marketing permission is wanted it belongs in the plugin-declared mechanism\n * (AGL-435), scoped to a host — a product decision, not this repair.\n *\n * `apps/console/specs/org-permissions-are-enforced.spec.ts` fails the build if\n * a key here, or a key a plugin declares, gains no server-side consumer.\n */\nconst CORE_ORG_PERMISSIONS: readonly OrgPermissionDefinition[] =\n // One table, shared with the native apps through the contracts (AGL-3950).\n CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({\n ...entry,\n key: entry.key as CoreOrgPermission,\n group: entry.group ?? 'organization',\n scope: entry.scope ?? 'org',\n // A site key's preset is the member's role on the site (AGL-4047).\n ...(CORE_SITE_ROLE_PRESETS[entry.key]\n ? { hostRoleDefaults: { ...CORE_SITE_ROLE_PRESETS[entry.key] }, presetFromSiteRole: true as const }\n : {}),\n roleDefaults: {\n owner: CORE_ORG_ROLE_GRANTS.owner.includes(entry.key),\n admin: CORE_ORG_ROLE_GRANTS.admin.includes(entry.key),\n editor: CORE_ORG_ROLE_GRANTS.editor.includes(entry.key),\n viewer: CORE_ORG_ROLE_GRANTS.viewer.includes(entry.key),\n },\n }))\n\n/** The core keys: a plugin may add keys beside them and never redefine one. */\nconst CORE_ORG_PERMISSION_KEYS: ReadonlySet<string> = new Set(\n CORE_ORG_PERMISSIONS.map((definition) => definition.key),\n)\n\n/** The legacy camelCase projection's keys, which no declaration may take either. */\nconst LEGACY_PERMISSION_KEYS: ReadonlySet<string> = new Set<string>(\n ORG_ROLE_PERMISSION_KEYS,\n)\n\n/** The org roles, in the order `DEFAULT_ROLE_PERMISSIONS` lists them. */\nconst ORG_ROLES: readonly OrgRole[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * Every permission, in display order for role editors: the core keys, then\n * the keys plugins declare through `registerPluginEntitlements`, in the\n * plugins' catalog order (AGL-2984).\n *\n * LIVE, and kept in step in place. A plugin's declarations register at\n * module scope — at boot on the server, before the first paint in the\n * console — which can be after a module holding this binding was evaluated.\n * So this array, the key list, the role defaults and the empty map below are\n * rewritten in place on every registration instead of being computed once\n * when this module loads, and whoever holds the binding reads the declared\n * keys with the core ones.\n */\nexport const ORG_PERMISSIONS: readonly OrgPermissionDefinition[] = []\n\nexport const ORG_PERMISSION_KEYS: readonly OrgPermission[] = []\n\n/**\n * The groups the catalog's keys are filed under, in display order (AGL-4047):\n * the core groups, then one per plugin group a declaration names. Live, like\n * `ORG_PERMISSIONS`; a group with no key is left out.\n */\nexport const ORG_PERMISSION_GROUPS: readonly OrgPermissionGroup[] = []\n\n/**\n * The catalog keys that came through `registerPluginPermissions` (AGL-4047):\n * the keys the per-tier resolver (`resolveRolePermissions`) also answers,\n * which a caller holding a catalog-resolved map lays over that resolver's\n * answer so a custom role reaches them. Live.\n */\nexport const TIER_PERMISSION_KEYS: readonly OrgPermission[] = []\n\n/**\n * Every catalog key that refines a broader one, with that key (AGL-4047),\n * and every key that only follows one, marked `'follows'`. Live, like\n * `ORG_PERMISSIONS`. A resolver lays it over its map with\n * `refinePermissionMap`.\n */\nexport const ORG_PERMISSION_REFINEMENTS: readonly PermissionRefinement[] = []\n\nconst NO_PERMISSIONS = {} as Record<OrgPermission, boolean>\n\n/**\n * Role → default permission set. Owner/admin hold everything (owner-only\n * actions like org deletion stay role-checked, not permission-checked);\n * editors work on content but see no money or roster controls; viewers read\n * only. A declared key's default for each role is the one its declaration\n * names.\n */\nexport const DEFAULT_ROLE_PERMISSIONS: Record<\n OrgRole,\n Record<OrgPermission, boolean>\n> = {\n owner: {} as Record<OrgPermission, boolean>,\n admin: {} as Record<OrgPermission, boolean>,\n editor: {} as Record<OrgPermission, boolean>,\n viewer: {} as Record<OrgPermission, boolean>,\n}\n\n/** Refused declarations already logged, so a refusal is reported once. */\nconst reportedRefusals = new Set<string>()\n\nfunction replaceItems<T>(target: readonly T[], items: readonly T[]): void {\n const list = target as T[]\n list.length = 0\n list.push(...items)\n}\n\nfunction replaceEntries(\n target: Record<string, boolean>,\n entries: Record<string, boolean>,\n): void {\n for (const key of Object.keys(target)) delete target[key]\n Object.assign(target, entries)\n}\n\n/**\n * Rebuilds the live catalog: the core keys, then every key a plugin declared\n * through either declaration API (`listPluginCatalogPermissions`, AGL-4047).\n * A declaration naming a core or legacy key is refused — that key keeps its\n * own meaning and defaults — and logged once.\n */\nfunction syncCatalog(): void {\n const definitions: OrgPermissionDefinition[] = [...CORE_ORG_PERMISSIONS]\n const tierKeys: string[] = []\n const groups: OrgPermissionGroup[] = CORE_ORG_PERMISSION_GROUPS.map((group) => ({ ...group }))\n for (const declared of listPluginCatalogPermissions()) {\n if (\n CORE_ORG_PERMISSION_KEYS.has(declared.key) ||\n LEGACY_PERMISSION_KEYS.has(declared.key)\n ) {\n const refusal = `${declared.pluginId}\\x00${declared.key}`\n if (!reportedRefusals.has(refusal)) {\n reportedRefusals.add(refusal)\n console.error(\n `[plugins] refused catalog permission \"${declared.key}\" to ` +\n `\"${declared.pluginId}\": the key belongs to the core catalog`,\n )\n }\n continue\n }\n definitions.push({\n key: declared.key,\n label: declared.label,\n description: declared.description,\n pluginId: declared.pluginId,\n group: declared.group,\n scope: declared.hostRoleDefaults ? 'site' : 'org',\n roleDefaults: declared.roleDefaults,\n ...(declared.hostRoleDefaults ? { hostRoleDefaults: declared.hostRoleDefaults } : {}),\n ...(declared.refines ? { refines: declared.refines } : {}),\n ...(declared.follows && !declared.refines ? { follows: declared.follows } : {}),\n ...(declared.narrowsPerSite ? { narrowsPerSite: { ...declared.narrowsPerSite } } : {}),\n ...(declared.presetFromSiteRole && declared.hostRoleDefaults ? { presetFromSiteRole: true as const } : {}),\n ...(declared.presetFromSiteRole && declared.hostRoleDefaults && declared.workspaceMembersOnly\n ? { workspaceMembersOnly: true as const }\n : {}),\n })\n if (declared.source === 'tier') tierKeys.push(declared.key)\n if (!groups.some((group) => group.id === declared.group)) {\n groups.push({ id: declared.group, label: declared.groupLabel, pluginId: declared.pluginId })\n }\n }\n const keys = definitions.map((definition) => definition.key)\n const known = new Set<string>(keys)\n // A refinement of a key the catalog does not hold refines nothing, and is\n // dropped from the definition so no editor offers a link to nowhere.\n for (const definition of definitions) {\n if (definition.refines && (!known.has(definition.refines) || definition.refines === definition.key)) {\n delete definition.refines\n }\n if (definition.follows && (!known.has(definition.follows) || definition.follows === definition.key)) {\n delete definition.follows\n }\n }\n replaceItems(\n ORG_PERMISSION_REFINEMENTS,\n definitions.flatMap((definition) => {\n const refinement = permissionRefinementOf(definition)\n return refinement ? [refinement] : []\n }),\n )\n replaceItems(ORG_PERMISSIONS, definitions)\n replaceItems(ORG_PERMISSION_KEYS, keys)\n replaceItems(\n ORG_PERMISSION_GROUPS,\n groups.filter((group) => definitions.some((definition) => definition.group === group.id)),\n )\n replaceItems(TIER_PERMISSION_KEYS, tierKeys)\n replaceEntries(NO_PERMISSIONS, Object.fromEntries(keys.map((key) => [key, false])))\n for (const role of ORG_ROLES) {\n replaceEntries(\n DEFAULT_ROLE_PERMISSIONS[role],\n Object.fromEntries(\n definitions.map((definition) => [definition.key, definition.roleDefaults[role] === true]),\n ),\n )\n }\n CATALOG_AND_LEGACY_KEYS.clear()\n for (const key of [...keys, ...LEGACY_PERMISSION_KEYS]) {\n CATALOG_AND_LEGACY_KEYS.add(key)\n }\n}\n\n/**\n * The `registerPluginPermissions` keys of a catalog-resolved map (AGL-4047).\n *\n * Those keys are answered twice: by the per-tier resolver\n * (`resolveRolePermissions`, the role's tier default and the member's\n * override) and, now that they are catalog keys, by `resolveOrgPermissions`\n * with every layer — the custom role's included. A caller that builds its\n * map from the first spreads this over it, so the custom role a member\n * carries reaches `managePos` the way it reaches `hosts.create`.\n */\nexport function tierPermissionsOf(\n granted: Readonly<Record<string, boolean>> | null | undefined,\n): Record<string, boolean> {\n const picked: Record<string, boolean> = {}\n if (!granted) return picked\n for (const key of TIER_PERMISSION_KEYS) {\n if (typeof granted[key] === 'boolean') picked[key] = granted[key]\n }\n return picked\n}\n\n/** One catalog key by name, or undefined. */\nexport function orgPermissionDefinition(\n permission: OrgPermission,\n): OrgPermissionDefinition | undefined {\n return ORG_PERMISSIONS.find((definition) => definition.key === permission)\n}\n\n/** `orgs/{orgId}/roles/{roleId}` — a named custom permission set. */\nexport interface AglynOrgCustomRole {\n $id?: string\n name?: string\n description?: string\n /** Full permission map; missing keys read as false. */\n permissions?: Partial<Record<OrgPermission, boolean>>\n /**\n * Per key a site can narrow (AGL-4047): `true` follows the site role on a\n * site where the member's access is narrower than their workspace role,\n * `false` keeps the permission there. A key it leaves out follows the\n * built-in role's default (`OrgPermissionDefinition.narrowsPerSite`), and\n * a member's own `siteNarrowing` wins over it.\n */\n siteNarrowing?: Partial<Record<OrgPermission, boolean>>\n}\n\n/**\n * Effective permission map for a member: the org role's defaults, then\n * the custom role's map (when the member has `roleId` and the role doc\n * is supplied), then per-member overrides — later layers win key-by-key.\n */\nexport function resolveOrgPermissions(\n member:\n | (Partial<AglynOrgMember> & {\n roleId?: string\n permissions?: Partial<Record<OrgPermission, boolean>>\n })\n | null\n | undefined,\n customRole?: AglynOrgCustomRole | null,\n): Record<OrgPermission, boolean> {\n if (!member) return NO_PERMISSIONS\n const role = (member.role ?? 'viewer') as OrgRole\n const base = DEFAULT_ROLE_PERMISSIONS[role] ?? NO_PERMISSIONS\n const merged: Record<OrgPermission, boolean> = { ...base }\n if (member.roleId && customRole?.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = customRole.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n if (member.permissions) {\n for (const key of ORG_PERMISSION_KEYS) {\n const value = member.permissions[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n // A key that refines a broader one (AGL-4047) follows it where neither\n // layer set the key, and is never held without it; a key that only follows\n // one takes its verdict where neither layer set the key, and nothing more.\n return refinePermissionMap(merged, ORG_PERMISSION_REFINEMENTS, [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ])\n}\n\n/**\n * The values a custom role and a per-member override set EXPLICITLY for\n * permission keys outside this catalog — the keys plugins declare through\n * `registerPluginPermissions` (AGL-2974).\n *\n * `resolveOrgPermissions` walks the catalog alone, so a plugin key stored on\n * a custom role or a member document never reached the `resolvedPermissions`\n * stamp the Firestore rules read. A rule gating on one could then honor\n * neither a grant to an editor nor a revocation from an admin. This carries\n * exactly those stored decisions, custom role first and the per-member\n * override over it, which is the precedence the resolver applies.\n *\n * Tier DEFAULTS are deliberately absent. They live in the plugin registry,\n * which is populated per process by whichever plugin bundles that process\n * loaded, and a stamp computed from it would differ by which route happened\n * to write it. A rule supplies the default itself from the member's role,\n * the way `memberResolves` is always conjoined with a role gate.\n *\n * The legacy six camelCase keys are excluded as well: they are the catalog's\n * older projection, not plugin keys, and no rule reads them.\n */\nexport function explicitPluginPermissionValues(\n member:\n | {\n roleId?: string\n permissions?: Partial<Record<string, unknown>> | null\n }\n | null\n | undefined,\n customRole?: { permissions?: Partial<Record<string, unknown>> | null } | null,\n): Record<string, boolean> {\n const values: Record<string, boolean> = {}\n if (!member) return values\n const layers = [\n member.roleId ? customRole?.permissions : undefined,\n member.permissions,\n ]\n for (const layer of layers) {\n for (const [key, value] of Object.entries(layer ?? {})) {\n if (typeof value !== 'boolean') continue\n if (CATALOG_AND_LEGACY_KEYS.has(key)) continue\n values[key] = value\n }\n }\n return values\n}\n\n/**\n * Keys {@link explicitPluginPermissionValues} leaves to the catalog: every\n * catalog key, declared ones included, and the legacy six. Kept in step by\n * `syncCatalog`.\n */\nconst CATALOG_AND_LEGACY_KEYS = new Set<string>()\n\n/**\n * The `resolvedPermissions` map stamped on a member document for the\n * Firestore rules: the catalog verdict with every layer applied, and beside\n * it the plugin keys a custom role or an override set explicitly.\n *\n * One function for every writer of the stamp, so the org-create transaction\n * and `syncOrgAuthProjections` cannot project different maps. The catalog\n * spreads last; the two key spaces do not overlap, and the order keeps it\n * true that nothing but the resolver decides a catalog key.\n */\nexport function projectMemberResolvedPermissions(\n member: Parameters<typeof resolveOrgPermissions>[0],\n customRole?: AglynOrgCustomRole | null,\n): Record<string, boolean> {\n return {\n ...explicitPluginPermissionValues(member, customRole),\n ...resolveOrgPermissions(member, customRole),\n }\n}\n\n/** Single-permission convenience over `resolveOrgPermissions`. */\nexport function hasOrgPermission(\n member: Parameters<typeof resolveOrgPermissions>[0],\n permission: OrgPermission,\n customRole?: AglynOrgCustomRole | null,\n): boolean {\n return resolveOrgPermissions(member, customRole)[permission]\n}\n\n/** The catalog label for a key — the words a refusal names — or the key itself. */\nexport function orgPermissionLabel(permission: OrgPermission): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === permission)?.label ??\n permission\n )\n}\n\n/**\n * The sentence a door answers when a member does not hold a key (AGL-4047):\n * it names the permission by its catalog label and who can change it, never\n * a role.\n */\nexport function permissionRefusalText(permission: OrgPermission): string {\n return `Your role does not include \"${orgPermissionLabel(permission)}\" — ask an organization admin`\n}\n\n/** The keys plugins declared into the catalog, in catalog order. */\nexport function pluginOrgPermissionKeys(): OrgPermission[] {\n return ORG_PERMISSIONS.filter((definition) => definition.pluginId).map(\n (definition) => definition.key,\n )\n}\n\n/** One declared catalog key that moved, and the direction it moved in. */\nexport interface PluginPermissionChange {\n permission: string\n granted: boolean\n}\n\n/**\n * The keys PLUGINS declared into the catalog whose value moved between two\n * maps, in the new map's order — what a member, role or collaborator write\n * raises `org.permissions.changed` for, once per key, so the plugin that owns\n * a key can record who moved it.\n *\n * A key the new map leaves unset is not a change: on a role it defers to the\n * base role, on a site toggle to the host role, and neither is a grant or a\n * revocation of its own. A key set for the first time is a change, because\n * the verdict it produces is no longer the default's. The core keys and the\n * legacy six raise nothing; their writes are recorded by the routes' own\n * activity sentences.\n */\nexport function pluginPermissionChanges(\n before: Partial<Record<string, unknown>> | null | undefined,\n after: Partial<Record<string, unknown>> | null | undefined,\n): PluginPermissionChange[] {\n const changes: PluginPermissionChange[] = []\n for (const [key, next] of Object.entries(after ?? {})) {\n if (typeof next !== 'boolean') continue\n if (CORE_ORG_PERMISSION_KEYS.has(key) || LEGACY_PERMISSION_KEYS.has(key)) continue\n if (before?.[key] === next) continue\n changes.push({ permission: key, granted: next })\n }\n return changes\n}\n\n/**\n * The granular (dotted) set projected onto the legacy boolean map\n * (AGL-2350).\n *\n * ## Why this lives here rather than in the console hook\n *\n * It was a private function in `apps/console/hooks/use-org-permissions.ts`,\n * which meant the CLIENT translated the stored permission model into the\n * legacy flags and the SERVER did not translate at all — it derived the\n * legacy flags straight from the built-in role tier and dropped custom roles\n * and per-member overrides on the floor. The two disagreed exactly on the\n * feature `custom-roles.md` sells, in both directions: a permission granted\n * by a custom role showed in the UI and 403'd on POST, and one revoked by an\n * override was hidden in the UI and still succeeded on POST.\n *\n * One exported copy is what stops them drifting again.\n *\n * ## The key spaces are NOT interchangeable, and that is the trap\n *\n * Two permission vocabularies exist. The stored one is DOTTED\n * (`plugins.install`) — `apps/console/app/api/orgs/roles/route.ts` sanitizes\n * against `ORG_PERMISSION_KEYS` before writing, and `AglynOrgMember`\n * `permissions` is typed to it. The legacy one is camelCase\n * (`installPlugins`).\n *\n * `resolveRolePermissions` in `org-roles.ts` accepts `overrides` and\n * `customRoles` arguments keyed by the CAMELCASE space. Feeding it the real\n * stored documents therefore matches no key and changes nothing — it looks\n * like wiring the feature up while doing precisely nothing. Those two\n * parameters are used by nothing but that module's own spec. Translate\n * through here instead.\n *\n * `editHosts` is derived from the ROLE, not from a dotted key: it has no\n * counterpart in the granular catalog and never had one.\n */\nexport function toLegacyPermissions(\n granted: Record<OrgPermission, boolean>,\n role: OrgRole | null | undefined,\n): OrgPermissions {\n return {\n createHosts: granted['hosts.create'],\n editHosts: role !== 'viewer',\n editBilling: granted['billing.manage'],\n publishToMarketplace: granted['marketplace.publish'],\n installPlugins: granted['plugins.install'],\n manageMembers: granted['members.manage'],\n }\n}\n\nsyncCatalog()\nsubscribePluginEntitlements(syncCatalog)\n// The second declaration API feeds the same catalog (AGL-4047).\nsubscribePluginPermissions(syncCatalog)\n"],"names":["listPluginCatalogPermissions","subscribePluginEntitlements","permissionRefinementOf","refinePermissionMap","subscribePluginPermissions","ORG_ROLE_PERMISSION_KEYS","CORE_ORG_PERMISSION_ENTRIES","CORE_ORG_PERMISSION_GROUPS","CORE_ORG_ROLE_GRANTS","CORE_SITE_ROLE_PRESETS","CORE_ORG_PERMISSIONS","map","entry","key","group","scope","hostRoleDefaults","presetFromSiteRole","roleDefaults","owner","includes","admin","editor","viewer","CORE_ORG_PERMISSION_KEYS","Set","definition","LEGACY_PERMISSION_KEYS","ORG_ROLES","ORG_PERMISSIONS","ORG_PERMISSION_KEYS","ORG_PERMISSION_GROUPS","TIER_PERMISSION_KEYS","ORG_PERMISSION_REFINEMENTS","NO_PERMISSIONS","DEFAULT_ROLE_PERMISSIONS","reportedRefusals","replaceItems","target","items","list","length","push","replaceEntries","entries","Object","keys","assign","syncCatalog","definitions","tierKeys","groups","declared","has","refusal","pluginId","add","console","error","label","description","refines","follows","narrowsPerSite","workspaceMembersOnly","source","some","id","groupLabel","known","flatMap","refinement","filter","fromEntries","role","CATALOG_AND_LEGACY_KEYS","clear","tierPermissionsOf","granted","picked","orgPermissionDefinition","permission","find","resolveOrgPermissions","member","customRole","base","merged","roleId","permissions","value","undefined","explicitPluginPermissionValues","values","layers","layer","projectMemberResolvedPermissions","hasOrgPermission","orgPermissionLabel","permissionRefusalText","pluginOrgPermissionKeys","pluginPermissionChanges","before","after","changes","next","toLegacyPermissions","createHosts","editHosts","editBilling","publishToMarketplace","installPlugins","manageMembers"],"mappings":"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"}
|
|
@@ -187,21 +187,21 @@ export const ORG_ROLE_NAMES = [
|
|
|
187
187
|
group: 'sites',
|
|
188
188
|
scope: 'site',
|
|
189
189
|
label: 'Publish site content',
|
|
190
|
-
description: 'Make pages and content live, take them down or schedule them, and change
|
|
190
|
+
description: 'Make pages and content live, take them down or schedule them, change redirects and change the site’s fonts.'
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
key: 'site.sensitive',
|
|
194
194
|
group: 'sites',
|
|
195
195
|
scope: 'site',
|
|
196
196
|
label: 'See orders and webhooks',
|
|
197
|
-
description: 'Read a site’s orders and
|
|
197
|
+
description: 'Read a site’s orders, returns and sending address, and set up its webhooks.'
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
key: 'site.manage',
|
|
201
201
|
group: 'sites',
|
|
202
202
|
scope: 'site',
|
|
203
203
|
label: 'Manage site settings',
|
|
204
|
-
description: 'Manage a site’s people, domain, address and plugins, change its plugin settings and security allow-lists, import, export and delete it.'
|
|
204
|
+
description: 'Manage a site’s people, domain, address and plugins, change its plugin settings (live chat, Zapier) and security allow-lists, import, export and delete it.'
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
key: 'data.manage',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-role-grants.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT EACH ORG ROLE MAY DO, AS DATA (AGL-243, AGL-3950).\n *\n * The core permission catalog and the core keys each org role holds when\n * nothing refines it. `org-permissions.ts` builds the console's live\n * catalog from these (adding the keys plugins declare, with their own role\n * defaults), and the native apps read the same grants through the generated\n * contracts, so a native gate (`hosts.create`, `data.manage`) answers as the\n * console does for a member row that carries no `resolvedPermissions` stamp.\n *\n * `ORG_ROLE_GRANTS` in the contracts is this table with every plugin's\n * declared catalog keys added (`assembleOrgRoleGrants`, run by\n * `generate:native-contracts` over each plugin's declarations), which is\n * exactly the set the console's `DEFAULT_ROLE_PERMISSIONS` holds once the\n * plugins have registered.\n *\n * NO IMPORTS, on purpose: the native apps read it through the generated\n * contracts.\n */\n\n/** The four org roles. */\nexport type OrgRoleName = 'owner' | 'admin' | 'editor' | 'viewer'\n\n/**\n * Where a key is decided (AGL-4047). `org`: by the member's org role, custom\n * role and overrides, the same on every site. `site`: as `org` for a member\n * who reaches the whole workspace, and per site for a site collaborator,\n * from the host role they hold there and that site's toggle.\n */\nexport type OrgPermissionScope = 'org' | 'site'\n\n/** One key of the org permission catalog. */\nexport interface OrgPermissionEntry {\n key: string\n label: string\n description: string\n /** The plugin that declared it; absent for a core key. */\n pluginId?: string\n /** The group the role editor files it under (`ORG_PERMISSION_GROUPS`, or the declaring plugin's id). */\n group?: string\n /** Where it is decided; absent reads as `org`. */\n scope?: OrgPermissionScope\n /**\n * The broader key this one is a finer part of (AGL-4047): never granted\n * without it, and following it where no custom role or override sets this\n * key. Absent for a key that refines nothing.\n */\n refines?: string\n /**\n * The key this one follows where no custom role or override sets it,\n * without needing it (AGL-4047): granted on its own, it holds while that\n * key is refused. Absent for a key that follows nothing, and for one that\n * refines.\n */\n follows?: string\n}\n\n/** A heading the role editor groups keys under. */\nexport interface OrgPermissionGroup {\n id: string\n label: string\n /** The plugin whose keys it holds; absent for a core group. */\n pluginId?: string\n}\n\n/** The catalog and each role's default grants. */\nexport interface OrgRoleGrants {\n /** Every key, in the order the role editor lists them. */\n permissions: OrgPermissionEntry[]\n /** The keys each role holds when no custom role or per-member override sets them. */\n grants: Record<OrgRoleName, string[]>\n /** The groups the keys are filed under, in display order; absent in a catalog written before them. */\n groups?: OrgPermissionGroup[]\n}\n\n/**\n * A key a plugin declares into the catalog: `PluginOrgPermissionDeclaration`\n * with org-role defaults, or a `registerPluginPermissions` key (`managePos`),\n * whose `defaults` are per role tier, the owner holding the admin tier's.\n */\nexport interface OrgPermissionDeclaration {\n key: string\n label: string\n description?: string\n roleDefaults?: Record<OrgRoleName, boolean>\n /** The tier defaults of a `registerPluginPermissions` key, used when `roleDefaults` is absent. */\n defaults?: { admin: boolean; editor: boolean; viewer: boolean }\n /** The host-role defaults of a key a site collaborator holds per site. */\n hostRoleDefaults?: Record<string, boolean>\n /** The group it is filed under; absent: a group named by the declaring plugin's id. */\n group?: string\n /** That group's heading, for a group the core does not name. */\n groupLabel?: string\n pluginId?: string\n /** The broader key it refines (AGL-4047). */\n refines?: string\n /** The key it follows without needing it (AGL-4047). */\n follows?: string\n}\n\n/** A custom role (`orgs/{orgId}/roles/{roleId}`) or a member's overrides: explicit verdicts per key. */\nexport interface OrgPermissionLayer {\n permissions?: Record<string, boolean>\n}\n\nexport const ORG_ROLE_NAMES: readonly OrgRoleName[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * The core groups, in display order. A plugin's keys are filed under a group\n * named by the plugin's id unless its declaration names one of these, so no\n * core group id is a plugin id.\n */\nexport const CORE_ORG_PERMISSION_GROUPS: readonly OrgPermissionGroup[] = [\n { id: 'organization', label: 'Organization' },\n { id: 'billing', label: 'Billing' },\n { id: 'team', label: 'Team' },\n { id: 'sites', label: 'Sites' },\n { id: 'datasets', label: 'Data' },\n { id: 'extensions', label: 'Marketplace and plugins' },\n]\n\n/**\n * A declaration's verdict for each org role: its `roleDefaults`, or, for a\n * `registerPluginPermissions` key, its tier `defaults` with the owner on the\n * admin tier (the tier `ORG_ROLE_TIER` resolves the owner onto).\n */\nexport function declarationRoleDefaults(\n declaration: Pick<OrgPermissionDeclaration, 'roleDefaults' | 'defaults'>,\n): Record<OrgRoleName, boolean> {\n if (declaration.roleDefaults) {\n return {\n owner: declaration.roleDefaults.owner === true,\n admin: declaration.roleDefaults.admin === true,\n editor: declaration.roleDefaults.editor === true,\n viewer: declaration.roleDefaults.viewer === true,\n }\n }\n const tiers = declaration.defaults\n return {\n owner: tiers?.admin === true,\n admin: tiers?.admin === true,\n editor: tiers?.editor === true,\n viewer: tiers?.viewer === true,\n }\n}\n\n/**\n * The core keys, in display order for role editors. Every key here is\n * enforced server-side (`apps/console/specs/org-permissions-are-enforced.spec.ts`).\n */\nexport const CORE_ORG_PERMISSION_ENTRIES: readonly OrgPermissionEntry[] = [\n {\n key: 'org.settings',\n group: 'organization',\n label: 'Organization settings',\n description: 'Rename the organization, change its slug, transfer it.',\n },\n {\n key: 'org.auditLog',\n group: 'organization',\n label: 'Activity & audit log',\n description: 'See the organization activity feed.',\n },\n {\n key: 'org.apiKeys',\n group: 'organization',\n label: 'Manage API keys',\n description: 'Create and revoke the workspace’s REST API keys.',\n },\n {\n key: 'org.privacy',\n group: 'organization',\n label: 'Privacy requests',\n description: 'Erase a person from every site in the workspace when they ask.',\n },\n {\n key: 'org.holds',\n group: 'organization',\n label: 'Holds and reviews',\n description: 'See what is held, flagged or locked on the workspace, and request a review.',\n },\n // Workspace acts that asked for the owner or admin role (AGL-4047), each\n // a key of its own whose default is exactly those roles.\n {\n key: 'org.export',\n group: 'organization',\n label: 'Export workspace data',\n description: 'Download a copy of everything the workspace holds.',\n },\n {\n key: 'org.sendingDomains',\n group: 'organization',\n label: 'Sending domains',\n description: 'Add, verify and remove the domains the workspace’s mail is sent from.',\n },\n {\n key: 'org.businessProfile',\n group: 'organization',\n label: 'Business profile',\n description: 'Change the services, audience and tone every site starts from.',\n },\n {\n key: 'billing.view',\n group: 'billing',\n label: 'View billing',\n description: 'See the plan, usage meters, and invoices.',\n },\n {\n key: 'billing.manage',\n group: 'billing',\n label: 'Manage billing',\n description: 'Change plans, buy add-ons, update payment details.',\n },\n {\n key: 'members.manage',\n group: 'team',\n label: 'Manage members',\n description: 'Invite, remove, and re-role organization members.',\n },\n {\n key: 'hosts.create',\n group: 'sites',\n label: 'Create sites',\n description: 'Create new sites in the organization.',\n },\n {\n key: 'hosts.delete',\n group: 'sites',\n label: 'Delete sites',\n description: 'Delete sites the member can access.',\n },\n // The site keys (AGL-4047): each replaced a gate on the member's role on a\n // site, and keeps that role as its preset (`CORE_SITE_ROLE_PRESETS`).\n {\n key: 'site.edit',\n group: 'sites',\n scope: 'site',\n label: 'Edit site content',\n description: 'Create and change pages, content and everything else on a site, without making it live.',\n },\n {\n key: 'site.publish',\n group: 'sites',\n scope: 'site',\n label: 'Publish site content',\n description: 'Make pages and content live, take them down or schedule them, and change redirects.',\n },\n {\n key: 'site.sensitive',\n group: 'sites',\n scope: 'site',\n label: 'See orders and webhooks',\n description: 'Read a site’s orders and returns, and set up its webhooks.',\n },\n {\n key: 'site.manage',\n group: 'sites',\n scope: 'site',\n label: 'Manage site settings',\n description: 'Manage a site’s people, domain, address and plugins, change its plugin settings and security allow-lists, import, export and delete it.',\n },\n {\n key: 'data.manage',\n group: 'datasets',\n label: 'Manage data',\n description: 'Create, edit, and delete organization datasets.',\n },\n // Workspace content that asked for the owner, admin or editor role\n // (AGL-4047), each a key whose default is exactly those roles.\n {\n key: 'org.media',\n group: 'datasets',\n label: 'Edit the workspace media library',\n description: 'Upload, change, move and remove the files in the media library every site shares.',\n },\n {\n key: 'org.eraseRecords',\n group: 'datasets',\n label: 'Delete workspace records',\n description: 'Delete records the whole workspace shares, such as dataset rows, contacts and lists.',\n },\n {\n key: 'marketplace.publish',\n group: 'extensions',\n label: 'Publish to marketplace',\n description: 'Publish listings under the organization profile.',\n },\n {\n key: 'plugins.install',\n group: 'extensions',\n label: 'Install plugins',\n description: 'Install or remove marketplace plugins.',\n },\n]\n\nconst CORE_KEYS = CORE_ORG_PERMISSION_ENTRIES.map((entry) => entry.key)\n\n/**\n * The core keys whose preset is the member's role ON THE SITE (AGL-4047), and\n * each site role's verdict: the sets the rules' `canWriteHostContent`,\n * `canPublishHostContent`, `canReadHostSensitive` and the site-admin checks\n * asked before they asked these keys. A member's preset on a site is the\n * verdict for the role they hold there (the owner and admins are Admin on\n * every site); a custom role, their own permissions and that site's toggle\n * then grant or refuse it. Each workspace role's default below is the verdict\n * for its own site role, which is the answer with no site named.\n */\nexport const CORE_SITE_ROLE_PRESETS: Readonly<Record<string, Readonly<Record<string, boolean>>>> = {\n 'site.edit': { admin: true, editor: true, author: true, viewer: false },\n 'site.publish': { admin: true, editor: true, author: false, viewer: false },\n 'site.sensitive': { admin: true, editor: true, author: false, viewer: false },\n 'site.manage': { admin: true, editor: false, author: false, viewer: false },\n}\n\n/** The workspace editor's site keys: its own site role's preset. */\nconst EDITOR_SITE_KEYS = Object.keys(CORE_SITE_ROLE_PRESETS).filter((key) => CORE_SITE_ROLE_PRESETS[key].editor)\n\n/**\n * The core keys each org role holds when nothing refines it: owners and\n * admins hold every core key (owner-only acts like deleting the\n * organization stay role-checked, not permission-checked); editors work on\n * content and see no money or roster controls; viewers read only.\n */\nexport const CORE_ORG_ROLE_GRANTS: Readonly<Record<OrgRoleName, readonly string[]>> = {\n owner: CORE_KEYS,\n admin: CORE_KEYS,\n editor: ['data.manage', 'org.media', 'org.eraseRecords', 'marketplace.publish', 'plugins.install', ...EDITOR_SITE_KEYS],\n viewer: [],\n}\n\n/** The core's catalog and grants; the contracts' value adds the plugins' keys. */\nexport const ORG_ROLE_GRANTS: OrgRoleGrants = {\n permissions: CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({ ...entry })),\n groups: CORE_ORG_PERMISSION_GROUPS.map((group) => ({ ...group })),\n grants: {\n owner: [...CORE_ORG_ROLE_GRANTS.owner],\n admin: [...CORE_ORG_ROLE_GRANTS.admin],\n editor: [...CORE_ORG_ROLE_GRANTS.editor],\n viewer: [...CORE_ORG_ROLE_GRANTS.viewer],\n },\n}\n\n/**\n * The catalog with plugins' declared keys after the core ones, each role\n * holding a declared key when its declaration's defaults say so. A\n * declaration naming a key already taken is refused, as the console's\n * catalog refuses it: the first declaration keeps its meaning and defaults.\n * Every declaration list after the first is folded in the same way, so the\n * org catalog keys (`PluginOrgPermissionDeclaration`) and the\n * `registerPluginPermissions` keys land in one catalog (AGL-4047).\n */\nexport function assembleOrgRoleGrants(\n core: OrgRoleGrants,\n ...declarationLists: ReadonlyArray<readonly OrgPermissionDeclaration[]>\n): OrgRoleGrants {\n const permissions = [...core.permissions]\n const groups = [...(core.groups ?? [])]\n const grants = Object.fromEntries(ORG_ROLE_NAMES.map((role) => [role, [...core.grants[role]]])) as Record<\n OrgRoleName,\n string[]\n >\n const taken = new Set(permissions.map((entry) => entry.key))\n const groupIds = new Set(groups.map((group) => group.id))\n for (const declaration of declarationLists.flat()) {\n if (!declaration?.key || taken.has(declaration.key)) continue\n taken.add(declaration.key)\n const group = declaration.group ?? declaration.pluginId\n permissions.push({\n key: declaration.key,\n label: declaration.label,\n description: declaration.description ?? '',\n ...(declaration.pluginId ? { pluginId: declaration.pluginId } : {}),\n ...(group ? { group } : {}),\n scope: declaration.hostRoleDefaults ? 'site' : 'org',\n ...(declaration.refines && declaration.refines !== declaration.key ? { refines: declaration.refines } : {}),\n ...(declaration.follows && declaration.follows !== declaration.key && !declaration.refines\n ? { follows: declaration.follows }\n : {}),\n })\n if (group && !groupIds.has(group)) {\n groupIds.add(group)\n groups.push({\n id: group,\n label: declaration.groupLabel ?? group,\n ...(declaration.pluginId ? { pluginId: declaration.pluginId } : {}),\n })\n } else if (group && declaration.pluginId) {\n // A plugin's group opened by a declaration that names no plugin (an org\n // catalog declaration, AGL-4047) is still that plugin's: the first of\n // its declarations that names one says so.\n const index = groups.findIndex((entry) => entry.id === group)\n if (index >= 0 && !groups[index].pluginId) groups[index] = { ...groups[index], pluginId: declaration.pluginId }\n }\n const defaults = declarationRoleDefaults(declaration)\n for (const role of ORG_ROLE_NAMES) {\n if (defaults[role]) grants[role].push(declaration.key)\n }\n }\n return { permissions, grants, groups }\n}\n\n/**\n * A member's effective permissions, as the console's `resolveOrgPermissions`\n * answers them: the role's defaults, then the custom role's explicit\n * verdicts (when the member has one), then the member's own overrides,\n * later layers winning key by key, over the catalog's keys only. A missing\n * role reads as viewer; a role the catalog does not know holds nothing.\n */\nexport function rolePermissions(\n catalog: OrgRoleGrants,\n role: string | null | undefined,\n customRole?: OrgPermissionLayer | null,\n overrides?: OrgPermissionLayer | null,\n): Record<string, boolean> {\n const keys = catalog.permissions.map((entry) => entry.key)\n const name = role ?? 'viewer'\n const held = new Set((catalog.grants as Record<string, string[] | undefined>)[name] ?? [])\n const out: Record<string, boolean> = Object.fromEntries(keys.map((key) => [key, held.has(key)]))\n for (const layer of [customRole, overrides]) {\n const values = layer?.permissions\n if (!values) continue\n for (const key of keys) {\n if (typeof values[key] === 'boolean') out[key] = values[key]\n }\n }\n // A key that refines a broader one (AGL-4047) is never held without it, and\n // follows it where neither layer set the key; a key that only follows one\n // takes its verdict where neither layer set the key, and nothing more.\n // Applied broader keys first, so a chain settles in one pass; a refinement\n // that loops is left alone.\n const broader = new Map<string, string>()\n const followsOnly = new Set<string>()\n for (const entry of catalog.permissions) {\n if (entry.refines && entry.refines in out) broader.set(entry.key, entry.refines)\n else if (entry.follows && entry.follows in out) {\n broader.set(entry.key, entry.follows)\n followsOnly.add(entry.key)\n }\n }\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n let current = key\n while (broader.has(current)) {\n if (seen.has(current)) return -1\n seen.add(current)\n current = broader.get(current) as string\n steps += 1\n }\n return steps\n }\n const ordered = [...broader.keys()]\n .map((key) => ({ key, steps: depth(key) }))\n .filter((entry) => entry.steps > 0)\n .sort((a, b) => a.steps - b.steps)\n for (const { key } of ordered) {\n let stored: boolean | undefined\n for (const layer of [customRole, overrides]) {\n const value = layer?.permissions?.[key]\n if (typeof value === 'boolean') stored = value\n }\n const held = out[broader.get(key) as string] === true\n out[key] = followsOnly.has(key) ? (stored === undefined ? held : stored) : held && (stored === undefined ? true : stored)\n }\n return out\n}\n"],"names":["ORG_ROLE_NAMES","CORE_ORG_PERMISSION_GROUPS","id","label","declarationRoleDefaults","declaration","roleDefaults","owner","admin","editor","viewer","tiers","defaults","CORE_ORG_PERMISSION_ENTRIES","key","group","description","scope","CORE_KEYS","map","entry","CORE_SITE_ROLE_PRESETS","author","EDITOR_SITE_KEYS","Object","keys","filter","CORE_ORG_ROLE_GRANTS","ORG_ROLE_GRANTS","permissions","groups","grants","assembleOrgRoleGrants","core","declarationLists","fromEntries","role","taken","Set","groupIds","flat","has","add","pluginId","push","hostRoleDefaults","refines","follows","groupLabel","index","findIndex","rolePermissions","catalog","customRole","overrides","name","held","out","layer","values","broader","Map","followsOnly","set","depth","seen","steps","current","get","ordered","sort","a","b","stored","value","undefined"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;CAkBC,GAED,wBAAwB;AAqFxB,OAAO,MAAMA,iBAAyC;IAAC;IAAS;IAAS;IAAU;CAAS,CAAA;AAE5F;;;;CAIC,GACD,OAAO,MAAMC,6BAA4D;IACvE;QAAEC,IAAI;QAAgBC,OAAO;IAAe;IAC5C;QAAED,IAAI;QAAWC,OAAO;IAAU;IAClC;QAAED,IAAI;QAAQC,OAAO;IAAO;IAC5B;QAAED,IAAI;QAASC,OAAO;IAAQ;IAC9B;QAAED,IAAI;QAAYC,OAAO;IAAO;IAChC;QAAED,IAAI;QAAcC,OAAO;IAA0B;CACtD,CAAA;AAED;;;;CAIC,GACD,OAAO,SAASC,wBACdC,WAAwE;IAExE,IAAIA,YAAYC,YAAY,EAAE;QAC5B,OAAO;YACLC,OAAOF,YAAYC,YAAY,CAACC,KAAK,KAAK;YAC1CC,OAAOH,YAAYC,YAAY,CAACE,KAAK,KAAK;YAC1CC,QAAQJ,YAAYC,YAAY,CAACG,MAAM,KAAK;YAC5CC,QAAQL,YAAYC,YAAY,CAACI,MAAM,KAAK;QAC9C;IACF;IACA,MAAMC,QAAQN,YAAYO,QAAQ;IAClC,OAAO;QACLL,OAAOI,CAAAA,yBAAAA,MAAOH,KAAK,MAAK;QACxBA,OAAOG,CAAAA,yBAAAA,MAAOH,KAAK,MAAK;QACxBC,QAAQE,CAAAA,yBAAAA,MAAOF,MAAM,MAAK;QAC1BC,QAAQC,CAAAA,yBAAAA,MAAOD,MAAM,MAAK;IAC5B;AACF;AAEA;;;CAGC,GACD,OAAO,MAAMG,8BAA6D;IACxE;QACEC,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA,yEAAyE;IACzE,yDAAyD;IACzD;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA,2EAA2E;IAC3E,sEAAsE;IACtE;QACEF,KAAK;QACLC,OAAO;QACPE,OAAO;QACPd,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPE,OAAO;QACPd,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPE,OAAO;QACPd,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPE,OAAO;QACPd,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA,mEAAmE;IACnE,+DAA+D;IAC/D;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;IACA;QACEF,KAAK;QACLC,OAAO;QACPZ,OAAO;QACPa,aAAa;IACf;CACD,CAAA;AAED,MAAME,YAAYL,4BAA4BM,GAAG,CAAC,CAACC,QAAUA,MAAMN,GAAG;AAEtE;;;;;;;;;CASC,GACD,OAAO,MAAMO,yBAAsF;IACjG,aAAa;QAAEb,OAAO;QAAMC,QAAQ;QAAMa,QAAQ;QAAMZ,QAAQ;IAAM;IACtE,gBAAgB;QAAEF,OAAO;QAAMC,QAAQ;QAAMa,QAAQ;QAAOZ,QAAQ;IAAM;IAC1E,kBAAkB;QAAEF,OAAO;QAAMC,QAAQ;QAAMa,QAAQ;QAAOZ,QAAQ;IAAM;IAC5E,eAAe;QAAEF,OAAO;QAAMC,QAAQ;QAAOa,QAAQ;QAAOZ,QAAQ;IAAM;AAC5E,EAAC;AAED,kEAAkE,GAClE,MAAMa,mBAAmBC,OAAOC,IAAI,CAACJ,wBAAwBK,MAAM,CAAC,CAACZ,MAAQO,sBAAsB,CAACP,IAAI,CAACL,MAAM;AAE/G;;;;;CAKC,GACD,OAAO,MAAMkB,uBAAyE;IACpFpB,OAAOW;IACPV,OAAOU;IACPT,QAAQ;QAAC;QAAe;QAAa;QAAoB;QAAuB;WAAsBc;KAAiB;IACvHb,QAAQ,EAAE;AACZ,EAAC;AAED,gFAAgF,GAChF,OAAO,MAAMkB,kBAAiC;IAC5CC,aAAahB,4BAA4BM,GAAG,CAAC,CAACC,QAAW,aAAKA;IAC9DU,QAAQ7B,2BAA2BkB,GAAG,CAAC,CAACJ,QAAW,aAAKA;IACxDgB,QAAQ;QACNxB,OAAO;eAAIoB,qBAAqBpB,KAAK;SAAC;QACtCC,OAAO;eAAImB,qBAAqBnB,KAAK;SAAC;QACtCC,QAAQ;eAAIkB,qBAAqBlB,MAAM;SAAC;QACxCC,QAAQ;eAAIiB,qBAAqBjB,MAAM;SAAC;IAC1C;AACF,EAAC;AAED;;;;;;;;CAQC,GACD,OAAO,SAASsB,sBACdC,IAAmB,EACnB,GAAGC,gBAAoE;QAGnDD;IADpB,MAAMJ,cAAc;WAAII,KAAKJ,WAAW;KAAC;IACzC,MAAMC,SAAS;YAAKG,eAAAA,KAAKH,MAAM,YAAXG,eAAe,EAAE;KAAE;IACvC,MAAMF,SAASP,OAAOW,WAAW,CAACnC,eAAemB,GAAG,CAAC,CAACiB,OAAS;YAACA;YAAM;mBAAIH,KAAKF,MAAM,CAACK,KAAK;aAAC;SAAC;IAI7F,MAAMC,QAAQ,IAAIC,IAAIT,YAAYV,GAAG,CAAC,CAACC,QAAUA,MAAMN,GAAG;IAC1D,MAAMyB,WAAW,IAAID,IAAIR,OAAOX,GAAG,CAAC,CAACJ,QAAUA,MAAMb,EAAE;IACvD,KAAK,MAAMG,eAAe6B,iBAAiBM,IAAI,GAAI;YAGnCnC,oBAICA;QANf,IAAI,EAACA,+BAAAA,YAAaS,GAAG,KAAIuB,MAAMI,GAAG,CAACpC,YAAYS,GAAG,GAAG;QACrDuB,MAAMK,GAAG,CAACrC,YAAYS,GAAG;QACzB,MAAMC,SAAQV,qBAAAA,YAAYU,KAAK,YAAjBV,qBAAqBA,YAAYsC,QAAQ;QACvDd,YAAYe,IAAI,CAAC;YACf9B,KAAKT,YAAYS,GAAG;YACpBX,OAAOE,YAAYF,KAAK;YACxBa,WAAW,GAAEX,2BAAAA,YAAYW,WAAW,YAAvBX,2BAA2B;WACpCA,YAAYsC,QAAQ,GAAG;YAAEA,UAAUtC,YAAYsC,QAAQ;QAAC,IAAI,CAAC,GAC7D5B,QAAQ;YAAEA;QAAM,IAAI,CAAC;YACzBE,OAAOZ,YAAYwC,gBAAgB,GAAG,SAAS;WAC3CxC,YAAYyC,OAAO,IAAIzC,YAAYyC,OAAO,KAAKzC,YAAYS,GAAG,GAAG;YAAEgC,SAASzC,YAAYyC,OAAO;QAAC,IAAI,CAAC,GACrGzC,YAAY0C,OAAO,IAAI1C,YAAY0C,OAAO,KAAK1C,YAAYS,GAAG,IAAI,CAACT,YAAYyC,OAAO,GACtF;YAAEC,SAAS1C,YAAY0C,OAAO;QAAC,IAC/B,CAAC;QAEP,IAAIhC,SAAS,CAACwB,SAASE,GAAG,CAAC1B,QAAQ;gBAIxBV;YAHTkC,SAASG,GAAG,CAAC3B;YACbe,OAAOc,IAAI,CAAC;gBACV1C,IAAIa;gBACJZ,KAAK,GAAEE,0BAAAA,YAAY2C,UAAU,YAAtB3C,0BAA0BU;eAC7BV,YAAYsC,QAAQ,GAAG;gBAAEA,UAAUtC,YAAYsC,QAAQ;YAAC,IAAI,CAAC;QAErE,OAAO,IAAI5B,SAASV,YAAYsC,QAAQ,EAAE;YACxC,wEAAwE;YACxE,sEAAsE;YACtE,2CAA2C;YAC3C,MAAMM,QAAQnB,OAAOoB,SAAS,CAAC,CAAC9B,QAAUA,MAAMlB,EAAE,KAAKa;YACvD,IAAIkC,SAAS,KAAK,CAACnB,MAAM,CAACmB,MAAM,CAACN,QAAQ,EAAEb,MAAM,CAACmB,MAAM,GAAG,aAAKnB,MAAM,CAACmB,MAAM;gBAAEN,UAAUtC,YAAYsC,QAAQ;;QAC/G;QACA,MAAM/B,WAAWR,wBAAwBC;QACzC,KAAK,MAAM+B,QAAQpC,eAAgB;YACjC,IAAIY,QAAQ,CAACwB,KAAK,EAAEL,MAAM,CAACK,KAAK,CAACQ,IAAI,CAACvC,YAAYS,GAAG;QACvD;IACF;IACA,OAAO;QAAEe;QAAaE;QAAQD;IAAO;AACvC;AAEA;;;;;;CAMC,GACD,OAAO,SAASqB,gBACdC,OAAsB,EACtBhB,IAA+B,EAC/BiB,UAAsC,EACtCC,SAAqC;QAIhB;IAFrB,MAAM7B,OAAO2B,QAAQvB,WAAW,CAACV,GAAG,CAAC,CAACC,QAAUA,MAAMN,GAAG;IACzD,MAAMyC,OAAOnB,eAAAA,OAAQ;IACrB,MAAMoB,OAAO,IAAIlB,KAAI,uBAAA,AAACc,QAAQrB,MAAM,AAAyC,CAACwB,KAAK,YAA9D,uBAAkE,EAAE;IACzF,MAAME,MAA+BjC,OAAOW,WAAW,CAACV,KAAKN,GAAG,CAAC,CAACL,MAAQ;YAACA;YAAK0C,KAAKf,GAAG,CAAC3B;SAAK;IAC9F,KAAK,MAAM4C,SAAS;QAACL;QAAYC;KAAU,CAAE;QAC3C,MAAMK,SAASD,yBAAAA,MAAO7B,WAAW;QACjC,IAAI,CAAC8B,QAAQ;QACb,KAAK,MAAM7C,OAAOW,KAAM;YACtB,IAAI,OAAOkC,MAAM,CAAC7C,IAAI,KAAK,WAAW2C,GAAG,CAAC3C,IAAI,GAAG6C,MAAM,CAAC7C,IAAI;QAC9D;IACF;IACA,4EAA4E;IAC5E,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,4BAA4B;IAC5B,MAAM8C,UAAU,IAAIC;IACpB,MAAMC,cAAc,IAAIxB;IACxB,KAAK,MAAMlB,SAASgC,QAAQvB,WAAW,CAAE;QACvC,IAAIT,MAAM0B,OAAO,IAAI1B,MAAM0B,OAAO,IAAIW,KAAKG,QAAQG,GAAG,CAAC3C,MAAMN,GAAG,EAAEM,MAAM0B,OAAO;aAC1E,IAAI1B,MAAM2B,OAAO,IAAI3B,MAAM2B,OAAO,IAAIU,KAAK;YAC9CG,QAAQG,GAAG,CAAC3C,MAAMN,GAAG,EAAEM,MAAM2B,OAAO;YACpCe,YAAYpB,GAAG,CAACtB,MAAMN,GAAG;QAC3B;IACF;IACA,MAAMkD,QAAQ,CAAClD;QACb,MAAMmD,OAAO,IAAI3B;QACjB,IAAI4B,QAAQ;QACZ,IAAIC,UAAUrD;QACd,MAAO8C,QAAQnB,GAAG,CAAC0B,SAAU;YAC3B,IAAIF,KAAKxB,GAAG,CAAC0B,UAAU,OAAO,CAAC;YAC/BF,KAAKvB,GAAG,CAACyB;YACTA,UAAUP,QAAQQ,GAAG,CAACD;YACtBD,SAAS;QACX;QACA,OAAOA;IACT;IACA,MAAMG,UAAU;WAAIT,QAAQnC,IAAI;KAAG,CAChCN,GAAG,CAAC,CAACL,MAAS,CAAA;YAAEA;YAAKoD,OAAOF,MAAMlD;QAAK,CAAA,GACvCY,MAAM,CAAC,CAACN,QAAUA,MAAM8C,KAAK,GAAG,GAChCI,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEL,KAAK,GAAGM,EAAEN,KAAK;IACnC,KAAK,MAAM,EAAEpD,GAAG,EAAE,IAAIuD,QAAS;QAC7B,IAAII;QACJ,KAAK,MAAMf,SAAS;YAACL;YAAYC;SAAU,CAAE;gBAC7BI;YAAd,MAAMgB,QAAQhB,0BAAAA,qBAAAA,MAAO7B,WAAW,qBAAlB6B,kBAAoB,CAAC5C,IAAI;YACvC,IAAI,OAAO4D,UAAU,WAAWD,SAASC;QAC3C;QACA,MAAMlB,OAAOC,GAAG,CAACG,QAAQQ,GAAG,CAACtD,KAAe,KAAK;QACjD2C,GAAG,CAAC3C,IAAI,GAAGgD,YAAYrB,GAAG,CAAC3B,OAAQ2D,WAAWE,YAAYnB,OAAOiB,SAAUjB,QAASiB,CAAAA,WAAWE,YAAY,OAAOF,MAAK;IACzH;IACA,OAAOhB;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/org-role-grants.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT EACH ORG ROLE MAY DO, AS DATA (AGL-243, AGL-3950).\n *\n * The core permission catalog and the core keys each org role holds when\n * nothing refines it. `org-permissions.ts` builds the console's live\n * catalog from these (adding the keys plugins declare, with their own role\n * defaults), and the native apps read the same grants through the generated\n * contracts, so a native gate (`hosts.create`, `data.manage`) answers as the\n * console does for a member row that carries no `resolvedPermissions` stamp.\n *\n * `ORG_ROLE_GRANTS` in the contracts is this table with every plugin's\n * declared catalog keys added (`assembleOrgRoleGrants`, run by\n * `generate:native-contracts` over each plugin's declarations), which is\n * exactly the set the console's `DEFAULT_ROLE_PERMISSIONS` holds once the\n * plugins have registered.\n *\n * NO IMPORTS, on purpose: the native apps read it through the generated\n * contracts.\n */\n\n/** The four org roles. */\nexport type OrgRoleName = 'owner' | 'admin' | 'editor' | 'viewer'\n\n/**\n * Where a key is decided (AGL-4047). `org`: by the member's org role, custom\n * role and overrides, the same on every site. `site`: as `org` for a member\n * who reaches the whole workspace, and per site for a site collaborator,\n * from the host role they hold there and that site's toggle.\n */\nexport type OrgPermissionScope = 'org' | 'site'\n\n/** One key of the org permission catalog. */\nexport interface OrgPermissionEntry {\n key: string\n label: string\n description: string\n /** The plugin that declared it; absent for a core key. */\n pluginId?: string\n /** The group the role editor files it under (`ORG_PERMISSION_GROUPS`, or the declaring plugin's id). */\n group?: string\n /** Where it is decided; absent reads as `org`. */\n scope?: OrgPermissionScope\n /**\n * The broader key this one is a finer part of (AGL-4047): never granted\n * without it, and following it where no custom role or override sets this\n * key. Absent for a key that refines nothing.\n */\n refines?: string\n /**\n * The key this one follows where no custom role or override sets it,\n * without needing it (AGL-4047): granted on its own, it holds while that\n * key is refused. Absent for a key that follows nothing, and for one that\n * refines.\n */\n follows?: string\n}\n\n/** A heading the role editor groups keys under. */\nexport interface OrgPermissionGroup {\n id: string\n label: string\n /** The plugin whose keys it holds; absent for a core group. */\n pluginId?: string\n}\n\n/** The catalog and each role's default grants. */\nexport interface OrgRoleGrants {\n /** Every key, in the order the role editor lists them. */\n permissions: OrgPermissionEntry[]\n /** The keys each role holds when no custom role or per-member override sets them. */\n grants: Record<OrgRoleName, string[]>\n /** The groups the keys are filed under, in display order; absent in a catalog written before them. */\n groups?: OrgPermissionGroup[]\n}\n\n/**\n * A key a plugin declares into the catalog: `PluginOrgPermissionDeclaration`\n * with org-role defaults, or a `registerPluginPermissions` key (`managePos`),\n * whose `defaults` are per role tier, the owner holding the admin tier's.\n */\nexport interface OrgPermissionDeclaration {\n key: string\n label: string\n description?: string\n roleDefaults?: Record<OrgRoleName, boolean>\n /** The tier defaults of a `registerPluginPermissions` key, used when `roleDefaults` is absent. */\n defaults?: { admin: boolean; editor: boolean; viewer: boolean }\n /** The host-role defaults of a key a site collaborator holds per site. */\n hostRoleDefaults?: Record<string, boolean>\n /** The group it is filed under; absent: a group named by the declaring plugin's id. */\n group?: string\n /** That group's heading, for a group the core does not name. */\n groupLabel?: string\n pluginId?: string\n /** The broader key it refines (AGL-4047). */\n refines?: string\n /** The key it follows without needing it (AGL-4047). */\n follows?: string\n}\n\n/** A custom role (`orgs/{orgId}/roles/{roleId}`) or a member's overrides: explicit verdicts per key. */\nexport interface OrgPermissionLayer {\n permissions?: Record<string, boolean>\n}\n\nexport const ORG_ROLE_NAMES: readonly OrgRoleName[] = ['owner', 'admin', 'editor', 'viewer']\n\n/**\n * The core groups, in display order. A plugin's keys are filed under a group\n * named by the plugin's id unless its declaration names one of these, so no\n * core group id is a plugin id.\n */\nexport const CORE_ORG_PERMISSION_GROUPS: readonly OrgPermissionGroup[] = [\n { id: 'organization', label: 'Organization' },\n { id: 'billing', label: 'Billing' },\n { id: 'team', label: 'Team' },\n { id: 'sites', label: 'Sites' },\n { id: 'datasets', label: 'Data' },\n { id: 'extensions', label: 'Marketplace and plugins' },\n]\n\n/**\n * A declaration's verdict for each org role: its `roleDefaults`, or, for a\n * `registerPluginPermissions` key, its tier `defaults` with the owner on the\n * admin tier (the tier `ORG_ROLE_TIER` resolves the owner onto).\n */\nexport function declarationRoleDefaults(\n declaration: Pick<OrgPermissionDeclaration, 'roleDefaults' | 'defaults'>,\n): Record<OrgRoleName, boolean> {\n if (declaration.roleDefaults) {\n return {\n owner: declaration.roleDefaults.owner === true,\n admin: declaration.roleDefaults.admin === true,\n editor: declaration.roleDefaults.editor === true,\n viewer: declaration.roleDefaults.viewer === true,\n }\n }\n const tiers = declaration.defaults\n return {\n owner: tiers?.admin === true,\n admin: tiers?.admin === true,\n editor: tiers?.editor === true,\n viewer: tiers?.viewer === true,\n }\n}\n\n/**\n * The core keys, in display order for role editors. Every key here is\n * enforced server-side (`apps/console/specs/org-permissions-are-enforced.spec.ts`).\n */\nexport const CORE_ORG_PERMISSION_ENTRIES: readonly OrgPermissionEntry[] = [\n {\n key: 'org.settings',\n group: 'organization',\n label: 'Organization settings',\n description: 'Rename the organization, change its slug, transfer it.',\n },\n {\n key: 'org.auditLog',\n group: 'organization',\n label: 'Activity & audit log',\n description: 'See the organization activity feed.',\n },\n {\n key: 'org.apiKeys',\n group: 'organization',\n label: 'Manage API keys',\n description: 'Create and revoke the workspace’s REST API keys.',\n },\n {\n key: 'org.privacy',\n group: 'organization',\n label: 'Privacy requests',\n description: 'Erase a person from every site in the workspace when they ask.',\n },\n {\n key: 'org.holds',\n group: 'organization',\n label: 'Holds and reviews',\n description: 'See what is held, flagged or locked on the workspace, and request a review.',\n },\n // Workspace acts that asked for the owner or admin role (AGL-4047), each\n // a key of its own whose default is exactly those roles.\n {\n key: 'org.export',\n group: 'organization',\n label: 'Export workspace data',\n description: 'Download a copy of everything the workspace holds.',\n },\n {\n key: 'org.sendingDomains',\n group: 'organization',\n label: 'Sending domains',\n description: 'Add, verify and remove the domains the workspace’s mail is sent from.',\n },\n {\n key: 'org.businessProfile',\n group: 'organization',\n label: 'Business profile',\n description: 'Change the services, audience and tone every site starts from.',\n },\n {\n key: 'billing.view',\n group: 'billing',\n label: 'View billing',\n description: 'See the plan, usage meters, and invoices.',\n },\n {\n key: 'billing.manage',\n group: 'billing',\n label: 'Manage billing',\n description: 'Change plans, buy add-ons, update payment details.',\n },\n {\n key: 'members.manage',\n group: 'team',\n label: 'Manage members',\n description: 'Invite, remove, and re-role organization members.',\n },\n {\n key: 'hosts.create',\n group: 'sites',\n label: 'Create sites',\n description: 'Create new sites in the organization.',\n },\n {\n key: 'hosts.delete',\n group: 'sites',\n label: 'Delete sites',\n description: 'Delete sites the member can access.',\n },\n // The site keys (AGL-4047): each replaced a gate on the member's role on a\n // site, and keeps that role as its preset (`CORE_SITE_ROLE_PRESETS`).\n {\n key: 'site.edit',\n group: 'sites',\n scope: 'site',\n label: 'Edit site content',\n description: 'Create and change pages, content and everything else on a site, without making it live.',\n },\n {\n key: 'site.publish',\n group: 'sites',\n scope: 'site',\n label: 'Publish site content',\n description: 'Make pages and content live, take them down or schedule them, change redirects and change the site’s fonts.',\n },\n {\n key: 'site.sensitive',\n group: 'sites',\n scope: 'site',\n label: 'See orders and webhooks',\n description: 'Read a site’s orders, returns and sending address, and set up its webhooks.',\n },\n {\n key: 'site.manage',\n group: 'sites',\n scope: 'site',\n label: 'Manage site settings',\n description: 'Manage a site’s people, domain, address and plugins, change its plugin settings (live chat, Zapier) and security allow-lists, import, export and delete it.',\n },\n {\n key: 'data.manage',\n group: 'datasets',\n label: 'Manage data',\n description: 'Create, edit, and delete organization datasets.',\n },\n // Workspace content that asked for the owner, admin or editor role\n // (AGL-4047), each a key whose default is exactly those roles.\n {\n key: 'org.media',\n group: 'datasets',\n label: 'Edit the workspace media library',\n description: 'Upload, change, move and remove the files in the media library every site shares.',\n },\n {\n key: 'org.eraseRecords',\n group: 'datasets',\n label: 'Delete workspace records',\n description: 'Delete records the whole workspace shares, such as dataset rows, contacts and lists.',\n },\n {\n key: 'marketplace.publish',\n group: 'extensions',\n label: 'Publish to marketplace',\n description: 'Publish listings under the organization profile.',\n },\n {\n key: 'plugins.install',\n group: 'extensions',\n label: 'Install plugins',\n description: 'Install or remove marketplace plugins.',\n },\n]\n\nconst CORE_KEYS = CORE_ORG_PERMISSION_ENTRIES.map((entry) => entry.key)\n\n/**\n * The core keys whose preset is the member's role ON THE SITE (AGL-4047), and\n * each site role's verdict: the sets the rules' `canWriteHostContent`,\n * `canPublishHostContent`, `canReadHostSensitive` and the site-admin checks\n * asked before they asked these keys. A member's preset on a site is the\n * verdict for the role they hold there (the owner and admins are Admin on\n * every site); a custom role, their own permissions and that site's toggle\n * then grant or refuse it. Each workspace role's default below is the verdict\n * for its own site role, which is the answer with no site named.\n */\nexport const CORE_SITE_ROLE_PRESETS: Readonly<Record<string, Readonly<Record<string, boolean>>>> = {\n 'site.edit': { admin: true, editor: true, author: true, viewer: false },\n 'site.publish': { admin: true, editor: true, author: false, viewer: false },\n 'site.sensitive': { admin: true, editor: true, author: false, viewer: false },\n 'site.manage': { admin: true, editor: false, author: false, viewer: false },\n}\n\n/** The workspace editor's site keys: its own site role's preset. */\nconst EDITOR_SITE_KEYS = Object.keys(CORE_SITE_ROLE_PRESETS).filter((key) => CORE_SITE_ROLE_PRESETS[key].editor)\n\n/**\n * The core keys each org role holds when nothing refines it: owners and\n * admins hold every core key (owner-only acts like deleting the\n * organization stay role-checked, not permission-checked); editors work on\n * content and see no money or roster controls; viewers read only.\n */\nexport const CORE_ORG_ROLE_GRANTS: Readonly<Record<OrgRoleName, readonly string[]>> = {\n owner: CORE_KEYS,\n admin: CORE_KEYS,\n editor: ['data.manage', 'org.media', 'org.eraseRecords', 'marketplace.publish', 'plugins.install', ...EDITOR_SITE_KEYS],\n viewer: [],\n}\n\n/** The core's catalog and grants; the contracts' value adds the plugins' keys. */\nexport const ORG_ROLE_GRANTS: OrgRoleGrants = {\n permissions: CORE_ORG_PERMISSION_ENTRIES.map((entry) => ({ ...entry })),\n groups: CORE_ORG_PERMISSION_GROUPS.map((group) => ({ ...group })),\n grants: {\n owner: [...CORE_ORG_ROLE_GRANTS.owner],\n admin: [...CORE_ORG_ROLE_GRANTS.admin],\n editor: [...CORE_ORG_ROLE_GRANTS.editor],\n viewer: [...CORE_ORG_ROLE_GRANTS.viewer],\n },\n}\n\n/**\n * The catalog with plugins' declared keys after the core ones, each role\n * holding a declared key when its declaration's defaults say so. A\n * declaration naming a key already taken is refused, as the console's\n * catalog refuses it: the first declaration keeps its meaning and defaults.\n * Every declaration list after the first is folded in the same way, so the\n * org catalog keys (`PluginOrgPermissionDeclaration`) and the\n * `registerPluginPermissions` keys land in one catalog (AGL-4047).\n */\nexport function assembleOrgRoleGrants(\n core: OrgRoleGrants,\n ...declarationLists: ReadonlyArray<readonly OrgPermissionDeclaration[]>\n): OrgRoleGrants {\n const permissions = [...core.permissions]\n const groups = [...(core.groups ?? [])]\n const grants = Object.fromEntries(ORG_ROLE_NAMES.map((role) => [role, [...core.grants[role]]])) as Record<\n OrgRoleName,\n string[]\n >\n const taken = new Set(permissions.map((entry) => entry.key))\n const groupIds = new Set(groups.map((group) => group.id))\n for (const declaration of declarationLists.flat()) {\n if (!declaration?.key || taken.has(declaration.key)) continue\n taken.add(declaration.key)\n const group = declaration.group ?? declaration.pluginId\n permissions.push({\n key: declaration.key,\n label: declaration.label,\n description: declaration.description ?? '',\n ...(declaration.pluginId ? { pluginId: declaration.pluginId } : {}),\n ...(group ? { group } : {}),\n scope: declaration.hostRoleDefaults ? 'site' : 'org',\n ...(declaration.refines && declaration.refines !== declaration.key ? { refines: declaration.refines } : {}),\n ...(declaration.follows && declaration.follows !== declaration.key && !declaration.refines\n ? { follows: declaration.follows }\n : {}),\n })\n if (group && !groupIds.has(group)) {\n groupIds.add(group)\n groups.push({\n id: group,\n label: declaration.groupLabel ?? group,\n ...(declaration.pluginId ? { pluginId: declaration.pluginId } : {}),\n })\n } else if (group && declaration.pluginId) {\n // A plugin's group opened by a declaration that names no plugin (an org\n // catalog declaration, AGL-4047) is still that plugin's: the first of\n // its declarations that names one says so.\n const index = groups.findIndex((entry) => entry.id === group)\n if (index >= 0 && !groups[index].pluginId) groups[index] = { ...groups[index], pluginId: declaration.pluginId }\n }\n const defaults = declarationRoleDefaults(declaration)\n for (const role of ORG_ROLE_NAMES) {\n if (defaults[role]) grants[role].push(declaration.key)\n }\n }\n return { permissions, grants, groups }\n}\n\n/**\n * A member's effective permissions, as the console's `resolveOrgPermissions`\n * answers them: the role's defaults, then the custom role's explicit\n * verdicts (when the member has one), then the member's own overrides,\n * later layers winning key by key, over the catalog's keys only. A missing\n * role reads as viewer; a role the catalog does not know holds nothing.\n */\nexport function rolePermissions(\n catalog: OrgRoleGrants,\n role: string | null | undefined,\n customRole?: OrgPermissionLayer | null,\n overrides?: OrgPermissionLayer | null,\n): Record<string, boolean> {\n const keys = catalog.permissions.map((entry) => entry.key)\n const name = role ?? 'viewer'\n const held = new Set((catalog.grants as Record<string, string[] | undefined>)[name] ?? [])\n const out: Record<string, boolean> = Object.fromEntries(keys.map((key) => [key, held.has(key)]))\n for (const layer of [customRole, overrides]) {\n const values = layer?.permissions\n if (!values) continue\n for (const key of keys) {\n if (typeof values[key] === 'boolean') out[key] = values[key]\n }\n }\n // A key that refines a broader one (AGL-4047) is never held without it, and\n // follows it where neither layer set the key; a key that only follows one\n // takes its verdict where neither layer set the key, and nothing more.\n // Applied broader keys first, so a chain settles in one pass; a refinement\n // that loops is left alone.\n const broader = new Map<string, string>()\n const followsOnly = new Set<string>()\n for (const entry of catalog.permissions) {\n if (entry.refines && entry.refines in out) broader.set(entry.key, entry.refines)\n else if (entry.follows && entry.follows in out) {\n broader.set(entry.key, entry.follows)\n followsOnly.add(entry.key)\n }\n }\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n let current = key\n while (broader.has(current)) {\n if (seen.has(current)) return -1\n seen.add(current)\n current = broader.get(current) as string\n steps += 1\n }\n return steps\n }\n const ordered = [...broader.keys()]\n .map((key) => ({ key, steps: depth(key) }))\n .filter((entry) => entry.steps > 0)\n .sort((a, b) => a.steps - b.steps)\n for (const { key } of ordered) {\n let stored: boolean | undefined\n for (const layer of [customRole, overrides]) {\n const value = layer?.permissions?.[key]\n if (typeof value === 'boolean') stored = value\n }\n const held = out[broader.get(key) as string] === true\n out[key] = followsOnly.has(key) ? (stored === undefined ? held : stored) : held && (stored === undefined ? true : stored)\n }\n return out\n}\n"],"names":["ORG_ROLE_NAMES","CORE_ORG_PERMISSION_GROUPS","id","label","declarationRoleDefaults","declaration","roleDefaults","owner","admin","editor","viewer","tiers","defaults","CORE_ORG_PERMISSION_ENTRIES","key","group","description","scope","CORE_KEYS","map","entry","CORE_SITE_ROLE_PRESETS","author","EDITOR_SITE_KEYS","Object","keys","filter","CORE_ORG_ROLE_GRANTS","ORG_ROLE_GRANTS","permissions","groups","grants","assembleOrgRoleGrants","core","declarationLists","fromEntries","role","taken","Set","groupIds","flat","has","add","pluginId","push","hostRoleDefaults","refines","follows","groupLabel","index","findIndex","rolePermissions","catalog","customRole","overrides","name","held","out","layer","values","broader","Map","followsOnly","set","depth","seen","steps","current","get","ordered","sort","a","b","stored","value","undefined"],"mappings":"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"}
|
|
@@ -62,14 +62,14 @@ export declare function normalizeScreenSlug(input: string | null | undefined): s
|
|
|
62
62
|
* the path is parsed as `[host]` with an empty slug.
|
|
63
63
|
* - `aglyn-preview` — the same mechanism as `search`, one route over
|
|
64
64
|
* (AGL-3205). The live-site preview of an unpublished collection entry is
|
|
65
|
-
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[...slug]`, because it
|
|
65
|
+
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[[...slug]]`, because it
|
|
66
66
|
* must be a DIFFERENT Next route from the ISR-cached catch-all; a static
|
|
67
67
|
* folder wins over the optional catch-all beside it, so `/aglyn-preview/x`
|
|
68
68
|
* would reach the preview page and 404 rather than render a screen. The
|
|
69
|
-
* bare `/aglyn-preview`
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
69
|
+
* bare `/aglyn-preview` reaches it too since the team's view of a site in
|
|
70
|
+
* preview (AGL-4086) made its catch-all optional, so a site's home page
|
|
71
|
+
* could be previewed; and a child's path is composed from its parent's
|
|
72
|
+
* slug, which this rule refuses.
|
|
73
73
|
* - `_aglyn` — the platform's own pages on every site, the leaving notice
|
|
74
74
|
* at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path
|
|
75
75
|
* to an api route before the catch-all could see it.
|
|
@@ -73,14 +73,14 @@ import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
|
|
|
73
73
|
* the path is parsed as `[host]` with an empty slug.
|
|
74
74
|
* - `aglyn-preview` — the same mechanism as `search`, one route over
|
|
75
75
|
* (AGL-3205). The live-site preview of an unpublished collection entry is
|
|
76
|
-
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[...slug]`, because it
|
|
76
|
+
* a rewrite to `app/[host]/[scheme]/aglyn-preview/[[...slug]]`, because it
|
|
77
77
|
* must be a DIFFERENT Next route from the ISR-cached catch-all; a static
|
|
78
78
|
* folder wins over the optional catch-all beside it, so `/aglyn-preview/x`
|
|
79
79
|
* would reach the preview page and 404 rather than render a screen. The
|
|
80
|
-
* bare `/aglyn-preview`
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
80
|
+
* bare `/aglyn-preview` reaches it too since the team's view of a site in
|
|
81
|
+
* preview (AGL-4086) made its catch-all optional, so a site's home page
|
|
82
|
+
* could be previewed; and a child's path is composed from its parent's
|
|
83
|
+
* slug, which this rule refuses.
|
|
84
84
|
* - `_aglyn` — the platform's own pages on every site, the leaving notice
|
|
85
85
|
* at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path
|
|
86
86
|
* to an api route before the catch-all could see it.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-slug.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 { ENTRY_PREVIEW_ROUTE_SEGMENT } from './entry-preview-link'\nimport { LEAVING_NOTICE_ROUTE_SEGMENT } from './leaving-notice'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\n\n/*\n * The address rules of a page's slug, in a leaf of their own so the native apps\n * (which cannot import the routing map's reach into the whole foundation) read\n * the same answers: `screen-route.ts` re-exports every name below, so no\n * importer changes. The routing map, tree walks and links stay there.\n */\n\n/**\n * Route path of a host's root screen. The tenant matcher joins the catch-all\n * segments and collapses an empty one to this value (`load-page-data.ts`:\n * `slugSegments.length ? slugSegments.join('/') : SCREEN_ROOT_PATH`), so the\n * root is `'/'` and every other path is slash-joined segments WITHOUT a\n * leading slash (`about`, later `company/about`).\n */\nexport const SCREEN_ROOT_PATH = '/'\n\n/**\n * Normalizes user slug input into the routing-map path format described on\n * {@link SCREEN_ROOT_PATH}. `'/'` normalizes to the root path; anything else\n * becomes a single lowercase url-safe segment.\n *\n * Returns `undefined` for EMPTY input and for anything that sanitizes away\n * (`'###'`) — the two cases are deliberately the same answer, and the caller\n * decides what it means. In the Screens page's slug field an empty string\n * means \"no address typed\"; in a starter template it meant \"home\". Two\n * meanings for one value is what made AGL-1575 possible, so this function\n * refuses to pick one: a caller that wants the root must say `'/'`.\n *\n * This docstring used to claim empty input normalized to the root, which the\n * code has never done (see the spec, which asserts the `undefined`). A fix\n * written against the docstring instead of the behaviour would have shipped\n * broken; if you are here to make the code match the comment, do not.\n */\nexport function normalizeScreenSlug(\n input: string | null | undefined,\n): string | undefined {\n const trimmed = (input ?? '').trim()\n if (!trimmed) return undefined\n if (trimmed === SCREEN_ROOT_PATH) return SCREEN_ROOT_PATH\n\n const segment = trimmed\n .toLowerCase()\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-_]/g, '')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return segment || undefined\n}\n\n/**\n * Route segments the published site CANNOT serve, whatever the routing map\n * says (AGL-2076).\n *\n * Every entry was measured against production on 2026-08-19 with a fresh\n * `x-vercel-cache: MISS`, because a stale negative always passes. Two\n * mechanisms, both of which resolve ahead of `[host]/[scheme]/[[...slug]]`:\n *\n * - `404`, `500` — Next emits `pages/404.html` and `pages/500.html` even for\n * an app-router-only build, and the deployed filesystem answers them as\n * static files (`content-disposition: inline; filename=\"404\"`,\n * `accept-ranges`, an `etag`, and no `x-nextjs-prerender`). The tenant\n * middleware's own CSP headers are on those responses, so the middleware\n * ran and its rewrite still lost. There is no rewrite this app can write\n * that beats them, which is why the honest fix is to refuse the slug at\n * authoring time rather than to keep promising an address that is dead.\n * - `search`, `api`, `_next`, `_static` — routes and exclusions the tenant\n * app owns. `/search` matches `app/[host]/[scheme]/search`; the other three are the\n * middleware matcher's exclusions, so no host rewrite happens at all and\n * the path is parsed as `[host]` with an empty slug.\n * - `aglyn-preview` — the same mechanism as `search`, one route over\n * (AGL-3205). The live-site preview of an unpublished collection entry is\n * a rewrite to `app/[host]/[scheme]/aglyn-preview/[...slug]`, because it\n * must be a DIFFERENT Next route from the ISR-cached catch-all; a static\n * folder wins over the optional catch-all beside it, so `/aglyn-preview/x`\n * would reach the preview page and 404 rather than render a screen. The\n * bare `/aglyn-preview` would in fact still serve — the preview route's\n * catch-all is required, not optional — and it is reserved anyway for the\n * reason given below about child paths: a child's path is composed from\n * its parent's slug, and this rule refuses that slug.\n * - `_aglyn` — the platform's own pages on every site, the leaving notice\n * at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path\n * to an api route before the catch-all could see it.\n *\n * NOT reserved, and deliberately so, because measuring found them fine:\n * `401`, `403`, `503` (`/401` returns 200 through the catch-all — two of the\n * four designed error screens have always been viewable at their slugs, which\n * is what made the `404` case look like a Next.js defect), and `index`.\n *\n * `fonts` and `examples` USED to be dead for the third reason and are not\n * here: they were exclusions inherited from the Vercel platforms starter kit\n * for directories `apps/tenant/public` has never contained. They are given\n * back rather than reserved — see the matcher in `apps/tenant/middleware.ts`.\n */\nexport const RESERVED_SCREEN_ROUTE_SEGMENTS: readonly string[] = [\n '404',\n '500',\n 'search',\n ENTRY_PREVIEW_ROUTE_SEGMENT,\n 'api',\n '_next',\n '_static',\n LEAVING_NOTICE_ROUTE_SEGMENT,\n]\n\n/**\n * The reserved segment a routing-map path would collide with, or `undefined`\n * when the path is servable. The segment rather than a boolean, so a refusal\n * can name what it is refusing over.\n *\n * Reads the FIRST segment only. For `api`/`_next`/`_static` that is exactly\n * right — the middleware excludes the whole subtree, so `api/docs` is as dead\n * as `api`. For `404`/`500`/`search` it over-refuses in principle (`/404/why`\n * would serve), and that is deliberate: a child path is composed from its\n * PARENT's slug, the parent would have to hold `404`, and this same rule\n * refuses that slug wherever one is typed. A second list to express a case\n * that cannot arise would be a second list to keep correct.\n *\n * Takes a composed routing-map path (`about`, `company/about`, `'/'`), not a\n * raw slug field — `'/'` is the home screen and is never reserved.\n */\nexport function reservedScreenRouteSegment(\n path: string | null | undefined,\n): string | undefined {\n if (!path || path === SCREEN_ROOT_PATH) return undefined\n const [first] = path.replace(/^\\/+/, '').split('/')\n return RESERVED_SCREEN_ROUTE_SEGMENTS.includes(first) ? first : undefined\n}\n\n/**\n * The refusal an author reads when {@link reservedScreenRouteSegment} names a\n * segment. One sentence, in one place, because the Screens page, the version\n * view and the besigner all have to say the same thing — three surfaces\n * wording a platform constraint three ways is how AGL-2093's precheck drifted.\n */\nexport function reservedScreenRouteMessage(segment: string): string {\n return `\"/${segment}\" is a reserved address on every ${PLATFORM_BRAND_NAME} site — pick another slug`\n}\n\n/**\n * Does a raw slug value carry a path separator the one segment it describes\n * cannot hold (AGL-2572)?\n *\n * {@link normalizeScreenSlug} answers with a SINGLE segment, and it gets there\n * by deleting every character outside `[a-z0-9-_]` — an interior `/` included.\n * Whitespace becomes a `-`, but a `/` becomes nothing at all, so\n * `alternatives/webflow` normalizes to `alternativeswebflow` and the two words\n * are stored glued. Nothing about that is visible: the field accepts the\n * keystroke, the composed path reads plausibly, and the screen ends up\n * answering an address nobody typed. Two screens on `aglyn-marketing` carry a\n * slug of that shape.\n *\n * So the separator is a question a slug field asks BEFORE normalizing, and the\n * answer is a refusal rather than a silent repair — the caller cannot know\n * which segment the author meant, and guessing is how the glue happened.\n *\n * LEADING and TRAILING slashes are not separators here. `normalizeScreenSlug`\n * strips them deliberately (`/about/` → `about`), an author typing an address\n * writes them without meaning a hierarchy, and refusing them would reject a\n * value the normalizer has always handled correctly. `'/'` alone is the home\n * page and is never a separator.\n */\nexport function screenSlugHasPathSeparator(\n input: string | null | undefined,\n): boolean {\n const trimmed = (input ?? '').trim()\n if (!trimmed || trimmed === SCREEN_ROOT_PATH) return false\n return trimmed.replace(/^\\/+|\\/+$/g, '').includes('/')\n}\n\n/**\n * The refusal an author reads when {@link screenSlugHasPathSeparator} answers\n * true. One sentence, in one place, for the same reason\n * {@link reservedScreenRouteMessage} is: the Screens page, the version view\n * and the besigner all take a slug, and a constraint worded three ways drifts\n * into three different constraints.\n *\n * It names the alternative, because a `/` in this field almost always means\n * \"nest me\" and the hierarchy is what actually composes the longer path.\n */\nexport const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE =\n 'A slug is one path segment — remove the \"/\" and nest the page under a ' +\n 'parent page instead'\n"],"names":["ENTRY_PREVIEW_ROUTE_SEGMENT","LEAVING_NOTICE_ROUTE_SEGMENT","PLATFORM_BRAND_NAME","SCREEN_ROOT_PATH","normalizeScreenSlug","input","trimmed","trim","undefined","segment","toLowerCase","replace","RESERVED_SCREEN_ROUTE_SEGMENTS","reservedScreenRouteSegment","path","first","split","includes","reservedScreenRouteMessage","screenSlugHasPathSeparator","SCREEN_SLUG_PATH_SEPARATOR_MESSAGE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,2BAA2B,QAAQ,0BAAsB;AAClE,SAASC,4BAA4B,QAAQ,sBAAkB;AAC/D,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;CAKC,GAED;;;;;;CAMC,GACD,OAAO,MAAMC,mBAAmB,IAAG;AAEnC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,oBACdC,KAAgC;IAEhC,MAAMC,UAAU,CAACD,gBAAAA,QAAS,IAAIE,IAAI;IAClC,IAAI,CAACD,SAAS,OAAOE;IACrB,IAAIF,YAAYH,kBAAkB,OAAOA;IAEzC,MAAMM,UAAUH,QACbI,WAAW,GACXC,OAAO,CAAC,cAAc,IACtBA,OAAO,CAAC,QAAQ,KAChBA,OAAO,CAAC,gBAAgB,IACxBA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,YAAY;IAEvB,OAAOF,WAAWD;AACpB;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GACD,OAAO,MAAMI,iCAAoD;IAC/D;IACA;IACA;IACAZ;IACA;IACA;IACA;IACAC;CACD,CAAA;AAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASY,2BACdC,IAA+B;IAE/B,IAAI,CAACA,QAAQA,SAASX,kBAAkB,OAAOK;IAC/C,MAAM,CAACO,MAAM,GAAGD,KAAKH,OAAO,CAAC,QAAQ,IAAIK,KAAK,CAAC;IAC/C,OAAOJ,+BAA+BK,QAAQ,CAACF,SAASA,QAAQP;AAClE;AAEA;;;;;CAKC,GACD,OAAO,SAASU,2BAA2BT,OAAe;IACxD,OAAO,CAAC,EAAE,EAAEA,QAAQ,iCAAiC,EAAEP,oBAAoB,yBAAyB,CAAC;AACvG;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASiB,2BACdd,KAAgC;IAEhC,MAAMC,UAAU,CAACD,gBAAAA,QAAS,IAAIE,IAAI;IAClC,IAAI,CAACD,WAAWA,YAAYH,kBAAkB,OAAO;IACrD,OAAOG,QAAQK,OAAO,CAAC,cAAc,IAAIM,QAAQ,CAAC;AACpD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMG,qCACX,2EACA,sBAAqB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-slug.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 { ENTRY_PREVIEW_ROUTE_SEGMENT } from './entry-preview-link'\nimport { LEAVING_NOTICE_ROUTE_SEGMENT } from './leaving-notice'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\n\n/*\n * The address rules of a page's slug, in a leaf of their own so the native apps\n * (which cannot import the routing map's reach into the whole foundation) read\n * the same answers: `screen-route.ts` re-exports every name below, so no\n * importer changes. The routing map, tree walks and links stay there.\n */\n\n/**\n * Route path of a host's root screen. The tenant matcher joins the catch-all\n * segments and collapses an empty one to this value (`load-page-data.ts`:\n * `slugSegments.length ? slugSegments.join('/') : SCREEN_ROOT_PATH`), so the\n * root is `'/'` and every other path is slash-joined segments WITHOUT a\n * leading slash (`about`, later `company/about`).\n */\nexport const SCREEN_ROOT_PATH = '/'\n\n/**\n * Normalizes user slug input into the routing-map path format described on\n * {@link SCREEN_ROOT_PATH}. `'/'` normalizes to the root path; anything else\n * becomes a single lowercase url-safe segment.\n *\n * Returns `undefined` for EMPTY input and for anything that sanitizes away\n * (`'###'`) — the two cases are deliberately the same answer, and the caller\n * decides what it means. In the Screens page's slug field an empty string\n * means \"no address typed\"; in a starter template it meant \"home\". Two\n * meanings for one value is what made AGL-1575 possible, so this function\n * refuses to pick one: a caller that wants the root must say `'/'`.\n *\n * This docstring used to claim empty input normalized to the root, which the\n * code has never done (see the spec, which asserts the `undefined`). A fix\n * written against the docstring instead of the behaviour would have shipped\n * broken; if you are here to make the code match the comment, do not.\n */\nexport function normalizeScreenSlug(\n input: string | null | undefined,\n): string | undefined {\n const trimmed = (input ?? '').trim()\n if (!trimmed) return undefined\n if (trimmed === SCREEN_ROOT_PATH) return SCREEN_ROOT_PATH\n\n const segment = trimmed\n .toLowerCase()\n .replace(/^\\/+|\\/+$/g, '')\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9-_]/g, '')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return segment || undefined\n}\n\n/**\n * Route segments the published site CANNOT serve, whatever the routing map\n * says (AGL-2076).\n *\n * Every entry was measured against production on 2026-08-19 with a fresh\n * `x-vercel-cache: MISS`, because a stale negative always passes. Two\n * mechanisms, both of which resolve ahead of `[host]/[scheme]/[[...slug]]`:\n *\n * - `404`, `500` — Next emits `pages/404.html` and `pages/500.html` even for\n * an app-router-only build, and the deployed filesystem answers them as\n * static files (`content-disposition: inline; filename=\"404\"`,\n * `accept-ranges`, an `etag`, and no `x-nextjs-prerender`). The tenant\n * middleware's own CSP headers are on those responses, so the middleware\n * ran and its rewrite still lost. There is no rewrite this app can write\n * that beats them, which is why the honest fix is to refuse the slug at\n * authoring time rather than to keep promising an address that is dead.\n * - `search`, `api`, `_next`, `_static` — routes and exclusions the tenant\n * app owns. `/search` matches `app/[host]/[scheme]/search`; the other three are the\n * middleware matcher's exclusions, so no host rewrite happens at all and\n * the path is parsed as `[host]` with an empty slug.\n * - `aglyn-preview` — the same mechanism as `search`, one route over\n * (AGL-3205). The live-site preview of an unpublished collection entry is\n * a rewrite to `app/[host]/[scheme]/aglyn-preview/[[...slug]]`, because it\n * must be a DIFFERENT Next route from the ISR-cached catch-all; a static\n * folder wins over the optional catch-all beside it, so `/aglyn-preview/x`\n * would reach the preview page and 404 rather than render a screen. The\n * bare `/aglyn-preview` reaches it too since the team's view of a site in\n * preview (AGL-4086) made its catch-all optional, so a site's home page\n * could be previewed; and a child's path is composed from its parent's\n * slug, which this rule refuses.\n * - `_aglyn` — the platform's own pages on every site, the leaving notice\n * at `/_aglyn/leaving` first (AGL-3452). The middleware rewrites that path\n * to an api route before the catch-all could see it.\n *\n * NOT reserved, and deliberately so, because measuring found them fine:\n * `401`, `403`, `503` (`/401` returns 200 through the catch-all — two of the\n * four designed error screens have always been viewable at their slugs, which\n * is what made the `404` case look like a Next.js defect), and `index`.\n *\n * `fonts` and `examples` USED to be dead for the third reason and are not\n * here: they were exclusions inherited from the Vercel platforms starter kit\n * for directories `apps/tenant/public` has never contained. They are given\n * back rather than reserved — see the matcher in `apps/tenant/middleware.ts`.\n */\nexport const RESERVED_SCREEN_ROUTE_SEGMENTS: readonly string[] = [\n '404',\n '500',\n 'search',\n ENTRY_PREVIEW_ROUTE_SEGMENT,\n 'api',\n '_next',\n '_static',\n LEAVING_NOTICE_ROUTE_SEGMENT,\n]\n\n/**\n * The reserved segment a routing-map path would collide with, or `undefined`\n * when the path is servable. The segment rather than a boolean, so a refusal\n * can name what it is refusing over.\n *\n * Reads the FIRST segment only. For `api`/`_next`/`_static` that is exactly\n * right — the middleware excludes the whole subtree, so `api/docs` is as dead\n * as `api`. For `404`/`500`/`search` it over-refuses in principle (`/404/why`\n * would serve), and that is deliberate: a child path is composed from its\n * PARENT's slug, the parent would have to hold `404`, and this same rule\n * refuses that slug wherever one is typed. A second list to express a case\n * that cannot arise would be a second list to keep correct.\n *\n * Takes a composed routing-map path (`about`, `company/about`, `'/'`), not a\n * raw slug field — `'/'` is the home screen and is never reserved.\n */\nexport function reservedScreenRouteSegment(\n path: string | null | undefined,\n): string | undefined {\n if (!path || path === SCREEN_ROOT_PATH) return undefined\n const [first] = path.replace(/^\\/+/, '').split('/')\n return RESERVED_SCREEN_ROUTE_SEGMENTS.includes(first) ? first : undefined\n}\n\n/**\n * The refusal an author reads when {@link reservedScreenRouteSegment} names a\n * segment. One sentence, in one place, because the Screens page, the version\n * view and the besigner all have to say the same thing — three surfaces\n * wording a platform constraint three ways is how AGL-2093's precheck drifted.\n */\nexport function reservedScreenRouteMessage(segment: string): string {\n return `\"/${segment}\" is a reserved address on every ${PLATFORM_BRAND_NAME} site — pick another slug`\n}\n\n/**\n * Does a raw slug value carry a path separator the one segment it describes\n * cannot hold (AGL-2572)?\n *\n * {@link normalizeScreenSlug} answers with a SINGLE segment, and it gets there\n * by deleting every character outside `[a-z0-9-_]` — an interior `/` included.\n * Whitespace becomes a `-`, but a `/` becomes nothing at all, so\n * `alternatives/webflow` normalizes to `alternativeswebflow` and the two words\n * are stored glued. Nothing about that is visible: the field accepts the\n * keystroke, the composed path reads plausibly, and the screen ends up\n * answering an address nobody typed. Two screens on `aglyn-marketing` carry a\n * slug of that shape.\n *\n * So the separator is a question a slug field asks BEFORE normalizing, and the\n * answer is a refusal rather than a silent repair — the caller cannot know\n * which segment the author meant, and guessing is how the glue happened.\n *\n * LEADING and TRAILING slashes are not separators here. `normalizeScreenSlug`\n * strips them deliberately (`/about/` → `about`), an author typing an address\n * writes them without meaning a hierarchy, and refusing them would reject a\n * value the normalizer has always handled correctly. `'/'` alone is the home\n * page and is never a separator.\n */\nexport function screenSlugHasPathSeparator(\n input: string | null | undefined,\n): boolean {\n const trimmed = (input ?? '').trim()\n if (!trimmed || trimmed === SCREEN_ROOT_PATH) return false\n return trimmed.replace(/^\\/+|\\/+$/g, '').includes('/')\n}\n\n/**\n * The refusal an author reads when {@link screenSlugHasPathSeparator} answers\n * true. One sentence, in one place, for the same reason\n * {@link reservedScreenRouteMessage} is: the Screens page, the version view\n * and the besigner all take a slug, and a constraint worded three ways drifts\n * into three different constraints.\n *\n * It names the alternative, because a `/` in this field almost always means\n * \"nest me\" and the hierarchy is what actually composes the longer path.\n */\nexport const SCREEN_SLUG_PATH_SEPARATOR_MESSAGE =\n 'A slug is one path segment — remove the \"/\" and nest the page under a ' +\n 'parent page instead'\n"],"names":["ENTRY_PREVIEW_ROUTE_SEGMENT","LEAVING_NOTICE_ROUTE_SEGMENT","PLATFORM_BRAND_NAME","SCREEN_ROOT_PATH","normalizeScreenSlug","input","trimmed","trim","undefined","segment","toLowerCase","replace","RESERVED_SCREEN_ROUTE_SEGMENTS","reservedScreenRouteSegment","path","first","split","includes","reservedScreenRouteMessage","screenSlugHasPathSeparator","SCREEN_SLUG_PATH_SEPARATOR_MESSAGE"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,2BAA2B,QAAQ,0BAAsB;AAClE,SAASC,4BAA4B,QAAQ,sBAAkB;AAC/D,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;CAKC,GAED;;;;;;CAMC,GACD,OAAO,MAAMC,mBAAmB,IAAG;AAEnC;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,oBACdC,KAAgC;IAEhC,MAAMC,UAAU,CAACD,gBAAAA,QAAS,IAAIE,IAAI;IAClC,IAAI,CAACD,SAAS,OAAOE;IACrB,IAAIF,YAAYH,kBAAkB,OAAOA;IAEzC,MAAMM,UAAUH,QACbI,WAAW,GACXC,OAAO,CAAC,cAAc,IACtBA,OAAO,CAAC,QAAQ,KAChBA,OAAO,CAAC,gBAAgB,IACxBA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,YAAY;IAEvB,OAAOF,WAAWD;AACpB;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GACD,OAAO,MAAMI,iCAAoD;IAC/D;IACA;IACA;IACAZ;IACA;IACA;IACA;IACAC;CACD,CAAA;AAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASY,2BACdC,IAA+B;IAE/B,IAAI,CAACA,QAAQA,SAASX,kBAAkB,OAAOK;IAC/C,MAAM,CAACO,MAAM,GAAGD,KAAKH,OAAO,CAAC,QAAQ,IAAIK,KAAK,CAAC;IAC/C,OAAOJ,+BAA+BK,QAAQ,CAACF,SAASA,QAAQP;AAClE;AAEA;;;;;CAKC,GACD,OAAO,SAASU,2BAA2BT,OAAe;IACxD,OAAO,CAAC,EAAE,EAAEA,QAAQ,iCAAiC,EAAEP,oBAAoB,yBAAyB,CAAC;AACvG;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASiB,2BACdd,KAAgC;IAEhC,MAAMC,UAAU,CAACD,gBAAAA,QAAS,IAAIE,IAAI;IAClC,IAAI,CAACD,WAAWA,YAAYH,kBAAkB,OAAO;IACrD,OAAOG,QAAQK,OAAO,CAAC,cAAc,IAAIM,QAAQ,CAAC;AACpD;AAEA;;;;;;;;;CASC,GACD,OAAO,MAAMG,qCACX,2EACA,sBAAqB"}
|
|
@@ -54,6 +54,13 @@ export interface SearchIndexingHost {
|
|
|
54
54
|
*/
|
|
55
55
|
discourageSearchEngines?: boolean;
|
|
56
56
|
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Preview (AGL-4086): only the site's team can see it, so nothing about it
|
|
59
|
+
* is offered to a crawler either. Read by {@link isSiteKeptFromSearch},
|
|
60
|
+
* never by {@link isSearchDiscouraged}, which answers for the switch an
|
|
61
|
+
* author flipped and nothing else.
|
|
62
|
+
*/
|
|
63
|
+
preview?: boolean | null;
|
|
57
64
|
}
|
|
58
65
|
/** The screen fields this module reads. */
|
|
59
66
|
export interface SearchIndexingScreen {
|
|
@@ -74,6 +81,17 @@ export interface SearchIndexingCollection {
|
|
|
74
81
|
* "hide this site", or a schema slip would quietly de-index every customer.
|
|
75
82
|
*/
|
|
76
83
|
export declare function isSearchDiscouraged(host: SearchIndexingHost | null | undefined): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether crawlers are asked to stay away from the whole site: search is
|
|
86
|
+
* discouraged, or the site is in preview (AGL-4086).
|
|
87
|
+
*
|
|
88
|
+
* Separate from {@link isSearchDiscouraged} on purpose. That one is the state
|
|
89
|
+
* of a switch, and the console shows it as the switch; a site in preview has
|
|
90
|
+
* not had that switch flipped, and reading it as though it had would show the
|
|
91
|
+
* author a setting they never changed. Every surface that answers a CRAWLER —
|
|
92
|
+
* the head, `robots.txt`, the sitemap, the agent files — asks this one.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isSiteKeptFromSearch(host: SearchIndexingHost | null | undefined): boolean;
|
|
77
95
|
/**
|
|
78
96
|
* Whether a screen may be indexed on its own merits, ignoring the site-level
|
|
79
97
|
* switch.
|
|
@@ -23,6 +23,18 @@ import { SITEMAP_INDEX_PATH } from "./sitemap.js";
|
|
|
23
23
|
var _host_seo;
|
|
24
24
|
return (host == null ? void 0 : (_host_seo = host.seo) == null ? void 0 : _host_seo.discourageSearchEngines) === true;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether crawlers are asked to stay away from the whole site: search is
|
|
28
|
+
* discouraged, or the site is in preview (AGL-4086).
|
|
29
|
+
*
|
|
30
|
+
* Separate from {@link isSearchDiscouraged} on purpose. That one is the state
|
|
31
|
+
* of a switch, and the console shows it as the switch; a site in preview has
|
|
32
|
+
* not had that switch flipped, and reading it as though it had would show the
|
|
33
|
+
* author a setting they never changed. Every surface that answers a CRAWLER —
|
|
34
|
+
* the head, `robots.txt`, the sitemap, the agent files — asks this one.
|
|
35
|
+
*/ export function isSiteKeptFromSearch(host) {
|
|
36
|
+
return isSearchDiscouraged(host) || (host == null ? void 0 : host.preview) === true;
|
|
37
|
+
}
|
|
26
38
|
/**
|
|
27
39
|
* Whether a screen may be indexed on its own merits, ignoring the site-level
|
|
28
40
|
* switch.
|
|
@@ -46,7 +58,7 @@ import { SITEMAP_INDEX_PATH } from "./sitemap.js";
|
|
|
46
58
|
* collection rather than to a screen, so they ask
|
|
47
59
|
* {@link areCollectionEntriesIndexable} alongside {@link isSearchDiscouraged}.
|
|
48
60
|
*/ export function isPageIndexable(options) {
|
|
49
|
-
if (
|
|
61
|
+
if (isSiteKeptFromSearch(options.host)) return false;
|
|
50
62
|
return isScreenIndexable(options.screen);
|
|
51
63
|
}
|
|
52
64
|
/**
|
|
@@ -80,7 +92,9 @@ import { SITEMAP_INDEX_PATH } from "./sitemap.js";
|
|
|
80
92
|
* crawler an index of the site you just asked it not to crawl is the
|
|
81
93
|
* contradiction the rest of this module exists to remove.
|
|
82
94
|
*/ export function buildRobotsTxt(options) {
|
|
83
|
-
|
|
95
|
+
// A site in preview (AGL-4086) closes the file the same way: nobody but
|
|
96
|
+
// its team may see the site, so no crawler is invited in.
|
|
97
|
+
if (isSiteKeptFromSearch(options.host)) {
|
|
84
98
|
/*
|
|
85
99
|
Every named agent group is dropped along with the wildcard. A group that
|
|
86
100
|
named an agent and allowed it would be a LOUDER permission than the
|