@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-motion.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 { CANVAS_ROOT_ELEMENT_ID } from '../foundation/constants/canvas'\nimport {\n ANIMATION_MAX_DELAY_MS,\n ANIMATION_MAX_DURATION_MS,\n ANIMATION_MAX_STAGGER_STEP_MS,\n NODE_ANIMATION_DELAY_PROP,\n NODE_ANIMATION_DURATION_PROP,\n NODE_ANIMATION_EASE_PROP,\n NODE_ANIMATION_PROPS,\n NODE_ANIMATION_PROP,\n NODE_ANIMATION_STAGGER_PROP,\n NODE_ANIMATION_STAGGER_STEP_PROP,\n NODE_ANIMATION_TRIGGER_PROP,\n type AnimationEase,\n type AnimationPreset,\n type AnimationTrigger,\n} from './element-animation'\n\n/**\n * Entrance motion for a whole page (AGL-3838): the pass that writes the\n * existing animation props (`element-animation.ts`) onto a page built by\n * code — a starter, a new site's home, a page the AI composed — so it moves\n * the way an author would have set it up by hand in the attributes panel.\n *\n * Nothing new is rendered or shipped: the pass only writes the reserved\n * `aglynAnimation*` props the renderer and the tenant's stylesheet already\n * read, with values from their own vocabulary. A page that moves pays the\n * stylesheet and the scroll observer it always paid; a page this pass leaves\n * alone pays nothing.\n *\n * ## The rules\n *\n * - The FIRST section's secondary content — its eyebrow, its lede, its\n * buttons, a small picture — plays on `load`, as a short ladder: each\n * block starts {@link ElementMotionCharacter.heroStepMs} after the one\n * before. Its main heading and its largest picture, the candidates for the\n * page's Largest Contentful Paint, never move: they are painted where\n * they stand from the first frame. Its band — a background color or a\n * cover photo — never moves either.\n * - Every later section's content slides in on `scroll`, each section in\n * turn taking the next of the character's {@link\n * ElementMotionCharacter.sectionPresets}, so a page does not arrive as\n * one repeated move.\n * - A split — a column of words beside a picture — comes in from its\n * sides when the character says {@link ElementMotionCharacter.sides}:\n * the left column from the left, the right from the right, the second a\n * stagger step after the first. A picture whose preset zooms keeps its\n * zoom. Without sides its columns still arrive one after the other, the\n * picture's in the media preset and the words' in the section's.\n * - A group of like items — a grid of cards, a list, the questions of an\n * FAQ, a dataset repeated over one card — STAGGERS its items, one after\n * another, rather than arriving as one block, in the character's {@link\n * ElementMotionCharacter.groupPreset} where it names one.\n * - A picture, and a group whose items each lead with one, takes the\n * character's media preset.\n * - Never touched: forms and their fields, navigation and the site's frame,\n * anything out of the flow (`position: fixed | sticky | absolute` — a\n * cover photo's backdrop is its band's), anything hidden, and\n * anything that already carries an animation prop — an author's choice,\n * `none` included, is never overwritten. A block that HOLDS one of those\n * is not animated as a whole either; its other children are animated one\n * by one instead, so a form never slides in with its section.\n * - Except a picture's FRAME: a box in the flow whose picture is laid over\n * it (`position: absolute`), as the AI's layout compiler draws every\n * picture slot. The frame is the picture, so it moves as one, picture and\n * all, and counts as a picture for the media preset and for a split.\n * - Never nested: once a block animates, nothing inside it does. Two\n * entrances on the same pixels multiply their fades and add their\n * offsets, which reads as a stutter, not as motion.\n *\n * Pure and idempotent: a page it already moved comes back as the same\n * object, which is what lets a caller compare by identity to skip a write.\n *\n * Leaf imports only, the same as `starter-template-nodes.ts`: the starter\n * kit reads this from the console's client gallery and its seed route alike.\n */\n\n/** A page's motion: the presets, curve and timing every entrance on it shares. */\nexport interface ElementMotionCharacter {\n /** Words and blocks: a section's content, a group of cards. */\n preset: AnimationPreset\n /** A picture, or a group whose every item leads with one. */\n mediaPreset: AnimationPreset\n ease: AnimationEase\n durationMs: number\n /** Gap between the items of a staggered group. */\n staggerStepMs: number\n /** Gap between the rungs of the first section's load ladder. */\n heroStepMs: number\n /**\n * The presets later sections take in turn, the second section the first of\n * them; {@link preset} alone when absent. A section's pictures still take\n * {@link mediaPreset}.\n */\n sectionPresets?: readonly AnimationPreset[]\n /** Whether a split's two columns come in from their own sides. */\n sides?: boolean\n /**\n * What a group of like items — a grid of cards, a list, an FAQ — arrives\n * with, whatever its section's turn; the section's own preset when absent.\n * A group whose every item leads with a picture still takes {@link mediaPreset}.\n */\n groupPreset?: AnimationPreset\n}\n\n/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */\nexport const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter = {\n preset: 'slide-up',\n mediaPreset: 'fade',\n ease: 'smooth',\n durationMs: 600,\n staggerStepMs: 90,\n heroStepMs: 120,\n sectionPresets: ['slide-up', 'fade'],\n sides: true,\n}\n\n/** The most rungs the first section's ladder climbs; later blocks share the last. */\nexport const ELEMENT_MOTION_HERO_RUNGS = 6\n\n/**\n * Elements that are never animated, nor animated as part of a block that\n * holds them: forms and every field of one, and the site's navigation and\n * frame. A field that slides in under a visitor's cursor is a field they\n * miss, and a header that moves is a site that looks broken.\n */\nexport const ELEMENT_MOTION_NEVER: ReadonlySet<string> = new Set([\n // Forms and their fields.\n 'form',\n 'formField',\n 'muiFormControl',\n 'muiFormLabel',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiInput',\n 'muiSelect',\n 'muiSelectOption',\n 'muiRadioGroup',\n 'muiRadio',\n 'muiCheckbox',\n 'muiRating',\n 'muiDatePicker',\n 'muiTimePicker',\n 'newsletter-signup',\n 'member-signin',\n 'member-signup',\n 'member-recovery',\n 'booking',\n 'reservation-widget',\n 'customer-account',\n 'return-request',\n 'order-status',\n 'searchBox',\n 'collectionSearch',\n // Navigation and the site's frame.\n 'layoutSlot',\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiDrawerToggle',\n 'muiNavMenu',\n 'muiMegaMenu',\n 'muiBreadcrumbs',\n 'muiTabs',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'cart',\n])\n\n/** Pictures: what the media preset is for. */\nconst MEDIA: ReadonlySet<string> = new Set(['image', 'video', 'videoEmbed', 'gated-video'])\n\n/** Components that list like items whatever their children are. */\nconst LISTS: ReadonlySet<string> = new Set(['muiList', 'muiImageList', 'collectionEntries'])\n\n/**\n * What a group's items are made of when they are alike: cards, cells,\n * panels, pictures. Words and buttons never count, however alike — a row of\n * two buttons is a call to action, not a group of cards.\n */\nconst ITEMS: ReadonlySet<string> = new Set([\n 'muiCard',\n 'muiPaper',\n 'muiGrid',\n 'muiStack',\n 'muiBox',\n 'muiLinkBox',\n 'muiListItem',\n 'muiImageListItem',\n 'muiAccordion',\n 'reusableInstance',\n 'div',\n 'image',\n 'video',\n])\n\ninterface MotionNode {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n hidden?: boolean\n}\n\nexport interface ElementMotionOptions {\n /** The page's root; the canvas root when absent. */\n rootId?: string\n /** The page's sections in order; the root's own children when absent. */\n sectionIds?: readonly string[]\n character?: ElementMotionCharacter\n}\n\nconst clamp = (value: number, max: number): number => Math.min(Math.max(Math.round(value), 0), max)\n\n/**\n * Whether a style value — plain or one per breakpoint — takes the element out\n * of the flow: pinned to the screen, or laid over its band, as a cover\n * photo's backdrop and its scrim are. Either is part of the band, not of what\n * the band holds.\n */\nfunction pins(value: unknown): boolean {\n if (value === 'fixed' || value === 'sticky' || value === 'absolute') return true\n return typeof value === 'object' && value !== null && Object.values(value).some(pins)\n}\n\n/**\n * Writes entrance motion onto a page's nodes by the rules above, and returns\n * the new node map — or `nodes` itself, unchanged, when there was nothing to\n * move.\n */\nexport function applyElementMotion<T extends Record<string, any>>(nodes: T, options: ElementMotionOptions = {}): T {\n if (!nodes) return nodes\n const character = options.character ?? ELEMENT_MOTION_DEFAULT\n const rootId = options.rootId ?? CANVAS_ROOT_ELEMENT_ID\n const node = (id: string): MotionNode | undefined => nodes[id] as MotionNode | undefined\n const children = (id: string): string[] => (node(id)?.nodes ?? []).filter((child) => typeof child === 'string' && !!node(child))\n const patches = new Map<string, Record<string, unknown>>()\n\n /** The first section's Largest Contentful Paint candidates, painted where they stand. */\n const still = new Set<string>()\n /** Left alone for what it is, wherever it stands. */\n const refused = (id: string): boolean => {\n const at = node(id)\n if (!at) return true\n if (at.hidden || still.has(id)) return true\n if (ELEMENT_MOTION_NEVER.has(String(at.componentId ?? ''))) return true\n return !!at.props && NODE_ANIMATION_PROPS.some((prop) => at.props?.[prop] !== undefined)\n }\n /** Left alone, and so is everything inside it. */\n const closed = (id: string): boolean => refused(id) || pins(node(id)?.sx?.['position'])\n /** A picture laid over its box, free to move with it. */\n const overlay = (id: string): boolean =>\n MEDIA.has(String(node(id)?.componentId ?? '')) && pins(node(id)?.sx?.['position']) && !refused(id)\n /**\n * A picture's frame: a box in the flow whose picture is laid over it\n * (`position: absolute`, filling it), beside at most the placeholder icon\n * an empty slot shows. The frame IS the picture, so it moves as one,\n * picture and all; its picture being out of the flow does not hold it still\n * the way a cover's backdrop holds its band, which is itself out of the flow.\n */\n const isFrame = (id: string): boolean => {\n const kids = children(id)\n return (\n !closed(id) &&\n kids.some(overlay) &&\n kids.every((child) => overlay(child) || (node(child)?.componentId === 'icon' && !refused(child) && !children(child).length))\n )\n }\n const holdsClosed = new Map<string, boolean>()\n /** The element, or one inside it, is left alone: it cannot move as one block. */\n const holds = (id: string): boolean => {\n const known = holdsClosed.get(id)\n if (known !== undefined) return known\n holdsClosed.set(id, false)\n const result = closed(id) || (!isFrame(id) && children(id).some(holds))\n holdsClosed.set(id, result)\n return result\n }\n const groupMemo = new Map<string, boolean>()\n const belowMemo = new Map<string, boolean>()\n /**\n * A group of like items, whose items stagger. A grid or a row of panels\n * that holds a group of its own is the LAYOUT around it — a heading beside\n * its cards — so it is opened up and the group inside staggers instead.\n */\n const isGroup = (id: string): boolean => {\n const known = groupMemo.get(id)\n if (known !== undefined) return known\n const at = node(id) as MotionNode\n const componentId = String(at.componentId ?? '')\n const kids = children(id)\n let result: boolean\n // A dataset repeated over one card renders one child per record.\n if ((at.props?.['repeatDataset'] || LISTS.has(componentId)) && kids.length) result = true\n else if (kids.length < 2) result = false\n else {\n const first = String(node(kids[0])?.componentId ?? '')\n const alike =\n (componentId === 'muiGrid' && !!at.props?.['container']) ||\n (ITEMS.has(first) && kids.every((child) => node(child)?.componentId === first))\n result = alike && !holdsGroup(id)\n }\n groupMemo.set(id, result)\n return result\n }\n /** A group sits somewhere inside the element. */\n const holdsGroup = (id: string): boolean => {\n const known = belowMemo.get(id)\n if (known !== undefined) return known\n const result = children(id).some((child) => isGroup(child) || holdsGroup(child))\n belowMemo.set(id, result)\n return result\n }\n /** Leads with a picture: its first leaf, down its first children, is one, or the frame of one. */\n const leadsWithMedia = (id: string): boolean => {\n let at = id\n for (let depth = 0; depth < 12; depth += 1) {\n if (MEDIA.has(String(node(at)?.componentId ?? '')) || isFrame(at)) return true\n const kids = children(at)\n if (!kids.length) return false\n at = kids[0]\n }\n return false\n }\n const presetOf = (id: string, group: boolean, words: AnimationPreset): AnimationPreset => {\n const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id)\n return media ? character.mediaPreset : words\n }\n\n const animate = (\n id: string,\n trigger: AnimationTrigger,\n delayMs: number,\n words: AnimationPreset = character.preset,\n side?: AnimationPreset,\n ): void => {\n const group = isGroup(id)\n const preset = presetOf(id, group, group && character.groupPreset ? character.groupPreset : words)\n patches.set(id, {\n [NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,\n [NODE_ANIMATION_TRIGGER_PROP]: trigger,\n [NODE_ANIMATION_DURATION_PROP]: clamp(character.durationMs, ANIMATION_MAX_DURATION_MS),\n ...(delayMs > 0 ? { [NODE_ANIMATION_DELAY_PROP]: clamp(delayMs, ANIMATION_MAX_DELAY_MS) } : {}),\n [NODE_ANIMATION_EASE_PROP]: character.ease,\n ...(group\n ? {\n [NODE_ANIMATION_STAGGER_PROP]: true,\n [NODE_ANIMATION_STAGGER_STEP_PROP]: clamp(character.staggerStepMs, ANIMATION_MAX_STAGGER_STEP_MS),\n }\n : {}),\n })\n }\n\n /** Down through wrappers of one child, to where a section's content starts. */\n const contentOf = (id: string): string => {\n let at = id\n while (!closed(at) && !isGroup(at)) {\n const kids = children(at)\n if (kids.length !== 1) break\n at = kids[0]\n }\n return at\n }\n\n /**\n * A column of words beside a picture: two blocks, one of which leads with a\n * picture and one of which does not, neither left alone. Two cards that\n * both lead with one are a group, and stagger.\n */\n const isSplit = (id: string): boolean => {\n const kids = children(id)\n return kids.length === 2 && leadsWithMedia(kids[0]) !== leadsWithMedia(kids[1]) && !kids.some(holds)\n }\n\n /** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */\n const place = (id: string, words: AnimationPreset): void => {\n if (closed(id)) return\n if (isSplit(id)) {\n const [start, end] = children(id)\n animate(start, 'scroll', 0, words, character.sides ? 'slide-right' : undefined)\n animate(end, 'scroll', character.staggerStepMs, words, character.sides ? 'slide-left' : undefined)\n return\n }\n if (holds(id) || (!isGroup(id) && holdsGroup(id))) {\n for (const child of children(id)) place(child, words)\n return\n }\n animate(id, 'scroll', 0, words)\n }\n\n /** Down through wrappers of one child that are free to move. */\n const descend = (id: string): string => {\n let at = id\n for (let kids = children(at); kids.length === 1 && !closed(kids[0]); kids = children(at)) at = kids[0]\n return at\n }\n\n /**\n * The first section's content: on load, one rung per block, around the\n * blocks that stay still. A rung that holds one is opened up, down to the\n * parts beside it, so the eyebrow, the lede and the buttons still climb.\n */\n const ladder = (id: string): void => {\n if (closed(id)) return\n // A repeat's one child is the card every record is drawn with: the\n // repeat staggers its records rather than each record playing at once.\n if (isGroup(id) && children(id).length < 2) return animate(id, 'load', 0)\n const around = (at: string): string[] =>\n children(at).flatMap((child) => (closed(child) ? [] : holds(child) ? around(child) : [child]))\n const base = children(id).length ? around(id) : [id]\n // A column of words beside a picture is opened up, so its lines climb one\n // after another before the picture lands.\n const rungs: string[] = []\n base.forEach((rung, index) => {\n const inner = descend(rung)\n const kids = children(inner)\n const opens =\n kids.length >= 2 &&\n !isGroup(inner) &&\n !leadsWithMedia(inner) &&\n rungs.length + kids.length + (base.length - index - 1) <= ELEMENT_MOTION_HERO_RUNGS\n rungs.push(...(opens ? kids : [rung]))\n })\n // A rung left alone takes no step of the ladder: the first block that moves starts at once.\n rungs\n .filter((rung) => !closed(rung))\n .forEach((rung, index) => animate(rung, 'load', Math.min(index, ELEMENT_MOTION_HERO_RUNGS - 1) * character.heroStepMs))\n }\n\n /**\n * What the first section paints first, and so what Lighthouse times as its\n * Largest Contentful Paint (AGL-3581): its main heading — the h1, else its\n * first heading — and its largest picture. Neither ever starts at opacity 0,\n * which would hold the paint back by the length of the entrance.\n */\n const markStill = (sectionId: string): void => {\n const order: string[] = []\n const walk = (id: string): void => {\n order.push(id)\n for (const child of children(id)) walk(child)\n }\n walk(sectionId)\n const typeOf = (id: string) => {\n const at = node(id)\n return at?.componentId === 'muiTypography' ? String(at.props?.['variant'] ?? '') : null\n }\n const main = (id: string) => /^(h1|display)/i.test(typeOf(id) ?? '') || (typeOf(id) !== null && node(id)?.props?.['component'] === 'h1')\n const heading = order.find(main) ?? order.find((id) => /^(h[1-6]|display)/i.test(typeOf(id) ?? ''))\n if (heading) still.add(heading)\n const size = (id: string): number => {\n const at = node(id)\n const read = (value: unknown) => (typeof value === 'number' ? value : parseFloat(String(value ?? '')) || 0)\n return Math.max(read(at?.props?.['height']), read(at?.props?.['width']), read(at?.sx?.['height']), read(at?.sx?.['width']))\n }\n let picture: string | undefined\n for (const id of order) {\n if (MEDIA.has(String(node(id)?.componentId ?? '')) && (picture === undefined || size(id) > size(picture))) picture = id\n }\n if (picture) still.add(picture)\n }\n\n const sections = (options.sectionIds ?? children(rootId)).filter((id) => !!node(id))\n if (sections[0]) markStill(sections[0])\n const turns = character.sectionPresets?.length ? character.sectionPresets : [character.preset]\n sections.forEach((id, index) => {\n if (closed(id)) return\n const content = contentOf(id)\n // The band itself never moves — only what it holds — so a section that\n // is nothing but its band has nothing to move.\n if (content === id && !children(id).length) return\n const words = turns[(index - 1) % turns.length] ?? character.preset\n if (index === 0) ladder(content)\n else if (content === id && !isGroup(id) && !isSplit(id))\n for (const child of children(id)) place(child, words)\n else place(content, words)\n })\n\n if (!patches.size) return nodes\n const next: Record<string, any> = { ...nodes }\n for (const [id, props] of patches) {\n next[id] = { ...nodes[id], props: { ...(nodes[id]?.props ?? {}), ...props } }\n }\n return next as T\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ANIMATION_MAX_DELAY_MS","ANIMATION_MAX_DURATION_MS","ANIMATION_MAX_STAGGER_STEP_MS","NODE_ANIMATION_DELAY_PROP","NODE_ANIMATION_DURATION_PROP","NODE_ANIMATION_EASE_PROP","NODE_ANIMATION_PROPS","NODE_ANIMATION_PROP","NODE_ANIMATION_STAGGER_PROP","NODE_ANIMATION_STAGGER_STEP_PROP","NODE_ANIMATION_TRIGGER_PROP","ELEMENT_MOTION_DEFAULT","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","sectionPresets","sides","ELEMENT_MOTION_HERO_RUNGS","ELEMENT_MOTION_NEVER","Set","MEDIA","LISTS","ITEMS","clamp","value","max","Math","min","round","pins","Object","values","some","applyElementMotion","nodes","options","character","rootId","node","id","children","filter","child","patches","Map","still","refused","at","hidden","has","String","componentId","props","prop","undefined","closed","sx","overlay","isFrame","kids","every","length","holdsClosed","holds","known","get","set","result","groupMemo","belowMemo","isGroup","first","alike","holdsGroup","leadsWithMedia","depth","presetOf","group","words","media","animate","trigger","delayMs","side","groupPreset","startsWith","contentOf","isSplit","place","start","end","descend","ladder","around","flatMap","base","rungs","forEach","rung","index","inner","opens","push","markStill","sectionId","order","walk","typeOf","main","test","heading","find","add","size","read","parseFloat","picture","sections","sectionIds","turns","content","next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/element-motion.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 { CANVAS_ROOT_ELEMENT_ID } from '../foundation/constants/canvas'\nimport {\n ANIMATION_MAX_DELAY_MS,\n ANIMATION_MAX_DURATION_MS,\n ANIMATION_MAX_STAGGER_STEP_MS,\n NODE_ANIMATION_DELAY_PROP,\n NODE_ANIMATION_DURATION_PROP,\n NODE_ANIMATION_EASE_PROP,\n NODE_ANIMATION_PROPS,\n NODE_ANIMATION_PROP,\n NODE_ANIMATION_STAGGER_PROP,\n NODE_ANIMATION_STAGGER_STEP_PROP,\n NODE_ANIMATION_TRIGGER_PROP,\n type AnimationEase,\n type AnimationPreset,\n type AnimationTrigger,\n} from './element-animation'\n\n/**\n * Entrance motion for a whole page (AGL-3838): the pass that writes the\n * existing animation props (`element-animation.ts`) onto a page built by\n * code — a starter, a new site's home, a page the AI composed — so it moves\n * the way an author would have set it up by hand in the attributes panel.\n *\n * Nothing new is rendered or shipped: the pass only writes the reserved\n * `aglynAnimation*` props the renderer and the tenant's stylesheet already\n * read, with values from their own vocabulary. A page that moves pays the\n * stylesheet and the scroll observer it always paid; a page this pass leaves\n * alone pays nothing.\n *\n * ## The rules\n *\n * - The FIRST section's secondary content — its eyebrow, its lede, its\n * buttons, a small picture — plays on `load`, as a short ladder: each\n * block starts {@link ElementMotionCharacter.heroStepMs} after the one\n * before. Its main heading and its largest picture, the candidates for the\n * page's Largest Contentful Paint, never move: they are painted where\n * they stand from the first frame. Its band — a background color or a\n * cover photo — never moves either.\n * - Every later section's content slides in on `scroll`, each section in\n * turn taking the next of the character's {@link\n * ElementMotionCharacter.sectionPresets}, so a page does not arrive as\n * one repeated move.\n * - A split — a column of words beside a picture — comes in from its\n * sides when the character says {@link ElementMotionCharacter.sides}:\n * the left column from the left, the right from the right, the second a\n * stagger step after the first. A picture whose preset zooms keeps its\n * zoom. Without sides its columns still arrive one after the other, the\n * picture's in the media preset and the words' in the section's.\n * - A group of like items — a grid of cards, a list, the questions of an\n * FAQ, a dataset repeated over one card — STAGGERS its items, one after\n * another, rather than arriving as one block, in the character's {@link\n * ElementMotionCharacter.groupPreset} where it names one.\n * - A picture, and a group whose items each lead with one, takes the\n * character's media preset.\n * - Never touched: forms and their fields, navigation and the site's frame,\n * anything out of the flow (`position: fixed | sticky | absolute` — a\n * cover photo's backdrop is its band's), anything hidden, and\n * anything that already carries an animation prop — an author's choice,\n * `none` included, is never overwritten. A block that HOLDS one of those\n * is not animated as a whole either; its other children are animated one\n * by one instead, so a form never slides in with its section.\n * - Except a picture's FRAME: a box in the flow whose picture is laid over\n * it (`position: absolute`), as the AI's layout compiler draws every\n * picture slot. The frame is the picture, so it moves as one, picture and\n * all, and counts as a picture for the media preset and for a split.\n * - Never nested: once a block animates, nothing inside it does. Two\n * entrances on the same pixels multiply their fades and add their\n * offsets, which reads as a stutter, not as motion.\n *\n * Pure and idempotent: a page it already moved comes back as the same\n * object, which is what lets a caller compare by identity to skip a write.\n *\n * Leaf imports only, the same as `starter-template-nodes.ts`: the starter\n * kit reads this from the console's client gallery and its seed route alike.\n */\n\n/** A page's motion: the presets, curve and timing every entrance on it shares. */\nexport interface ElementMotionCharacter {\n /** Words and blocks: a section's content, a group of cards. */\n preset: AnimationPreset\n /** A picture, or a group whose every item leads with one. */\n mediaPreset: AnimationPreset\n ease: AnimationEase\n durationMs: number\n /** Gap between the items of a staggered group. */\n staggerStepMs: number\n /** Gap between the rungs of the first section's load ladder. */\n heroStepMs: number\n /**\n * The presets later sections take in turn, the second section the first of\n * them; {@link preset} alone when absent. A section's pictures still take\n * {@link mediaPreset}.\n */\n sectionPresets?: readonly AnimationPreset[]\n /** Whether a split's two columns come in from their own sides. */\n sides?: boolean\n /**\n * What a group of like items — a grid of cards, a list, an FAQ — arrives\n * with, whatever its section's turn; the section's own preset when absent.\n * A group whose every item leads with a picture still takes {@link mediaPreset}.\n */\n groupPreset?: AnimationPreset\n}\n\n/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */\nexport const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter = {\n preset: 'slide-up',\n mediaPreset: 'fade',\n ease: 'smooth',\n durationMs: 600,\n staggerStepMs: 90,\n heroStepMs: 120,\n sectionPresets: ['slide-up', 'fade'],\n sides: true,\n}\n\n/** The most rungs the first section's ladder climbs; later blocks share the last. */\nexport const ELEMENT_MOTION_HERO_RUNGS = 6\n\n/**\n * Elements that are never animated, nor animated as part of a block that\n * holds them: forms and every field of one, and the site's navigation and\n * frame. A field that slides in under a visitor's cursor is a field they\n * miss, and a header that moves is a site that looks broken.\n */\nexport const ELEMENT_MOTION_NEVER: ReadonlySet<string> = new Set([\n // Forms and their fields.\n 'form',\n 'formField',\n 'muiFormControl',\n 'muiFormLabel',\n 'muiFormControlLabel',\n 'muiFormHelperText',\n 'muiInput',\n 'muiSelect',\n 'muiSelectOption',\n 'muiRadioGroup',\n 'muiRadio',\n 'muiCheckbox',\n 'muiRating',\n 'muiDatePicker',\n 'muiTimePicker',\n 'newsletter-signup',\n 'member-signin',\n 'member-signup',\n 'member-recovery',\n 'booking',\n 'reservation-widget',\n 'customer-account',\n 'return-request',\n 'order-status',\n 'quote-page',\n 'searchBox',\n 'collectionSearch',\n // Navigation and the site's frame.\n 'layoutSlot',\n 'muiAppBar',\n 'muiToolbar',\n 'muiDrawer',\n 'muiDrawerToggle',\n 'muiNavMenu',\n 'muiMegaMenu',\n 'muiBreadcrumbs',\n 'muiTabs',\n 'languageSwitcher',\n 'themeModeSwitcher',\n 'cart',\n])\n\n/** Pictures: what the media preset is for. */\nconst MEDIA: ReadonlySet<string> = new Set(['image', 'video', 'videoEmbed', 'gated-video'])\n\n/** Components that list like items whatever their children are. */\nconst LISTS: ReadonlySet<string> = new Set(['muiList', 'muiImageList', 'collectionEntries'])\n\n/**\n * What a group's items are made of when they are alike: cards, cells,\n * panels, pictures. Words and buttons never count, however alike — a row of\n * two buttons is a call to action, not a group of cards.\n */\nconst ITEMS: ReadonlySet<string> = new Set([\n 'muiCard',\n 'muiPaper',\n 'muiGrid',\n 'muiStack',\n 'muiBox',\n 'muiLinkBox',\n 'muiListItem',\n 'muiImageListItem',\n 'muiAccordion',\n 'reusableInstance',\n 'div',\n 'image',\n 'video',\n])\n\ninterface MotionNode {\n componentId?: string\n props?: Record<string, unknown>\n sx?: Record<string, unknown>\n nodes?: string[]\n hidden?: boolean\n}\n\nexport interface ElementMotionOptions {\n /** The page's root; the canvas root when absent. */\n rootId?: string\n /** The page's sections in order; the root's own children when absent. */\n sectionIds?: readonly string[]\n character?: ElementMotionCharacter\n}\n\nconst clamp = (value: number, max: number): number => Math.min(Math.max(Math.round(value), 0), max)\n\n/**\n * Whether a style value — plain or one per breakpoint — takes the element out\n * of the flow: pinned to the screen, or laid over its band, as a cover\n * photo's backdrop and its scrim are. Either is part of the band, not of what\n * the band holds.\n */\nfunction pins(value: unknown): boolean {\n if (value === 'fixed' || value === 'sticky' || value === 'absolute') return true\n return typeof value === 'object' && value !== null && Object.values(value).some(pins)\n}\n\n/**\n * Writes entrance motion onto a page's nodes by the rules above, and returns\n * the new node map — or `nodes` itself, unchanged, when there was nothing to\n * move.\n */\nexport function applyElementMotion<T extends Record<string, any>>(nodes: T, options: ElementMotionOptions = {}): T {\n if (!nodes) return nodes\n const character = options.character ?? ELEMENT_MOTION_DEFAULT\n const rootId = options.rootId ?? CANVAS_ROOT_ELEMENT_ID\n const node = (id: string): MotionNode | undefined => nodes[id] as MotionNode | undefined\n const children = (id: string): string[] => (node(id)?.nodes ?? []).filter((child) => typeof child === 'string' && !!node(child))\n const patches = new Map<string, Record<string, unknown>>()\n\n /** The first section's Largest Contentful Paint candidates, painted where they stand. */\n const still = new Set<string>()\n /** Left alone for what it is, wherever it stands. */\n const refused = (id: string): boolean => {\n const at = node(id)\n if (!at) return true\n if (at.hidden || still.has(id)) return true\n if (ELEMENT_MOTION_NEVER.has(String(at.componentId ?? ''))) return true\n return !!at.props && NODE_ANIMATION_PROPS.some((prop) => at.props?.[prop] !== undefined)\n }\n /** Left alone, and so is everything inside it. */\n const closed = (id: string): boolean => refused(id) || pins(node(id)?.sx?.['position'])\n /** A picture laid over its box, free to move with it. */\n const overlay = (id: string): boolean =>\n MEDIA.has(String(node(id)?.componentId ?? '')) && pins(node(id)?.sx?.['position']) && !refused(id)\n /**\n * A picture's frame: a box in the flow whose picture is laid over it\n * (`position: absolute`, filling it), beside at most the placeholder icon\n * an empty slot shows. The frame IS the picture, so it moves as one,\n * picture and all; its picture being out of the flow does not hold it still\n * the way a cover's backdrop holds its band, which is itself out of the flow.\n */\n const isFrame = (id: string): boolean => {\n const kids = children(id)\n return (\n !closed(id) &&\n kids.some(overlay) &&\n kids.every((child) => overlay(child) || (node(child)?.componentId === 'icon' && !refused(child) && !children(child).length))\n )\n }\n const holdsClosed = new Map<string, boolean>()\n /** The element, or one inside it, is left alone: it cannot move as one block. */\n const holds = (id: string): boolean => {\n const known = holdsClosed.get(id)\n if (known !== undefined) return known\n holdsClosed.set(id, false)\n const result = closed(id) || (!isFrame(id) && children(id).some(holds))\n holdsClosed.set(id, result)\n return result\n }\n const groupMemo = new Map<string, boolean>()\n const belowMemo = new Map<string, boolean>()\n /**\n * A group of like items, whose items stagger. A grid or a row of panels\n * that holds a group of its own is the LAYOUT around it — a heading beside\n * its cards — so it is opened up and the group inside staggers instead.\n */\n const isGroup = (id: string): boolean => {\n const known = groupMemo.get(id)\n if (known !== undefined) return known\n const at = node(id) as MotionNode\n const componentId = String(at.componentId ?? '')\n const kids = children(id)\n let result: boolean\n // A dataset repeated over one card renders one child per record.\n if ((at.props?.['repeatDataset'] || LISTS.has(componentId)) && kids.length) result = true\n else if (kids.length < 2) result = false\n else {\n const first = String(node(kids[0])?.componentId ?? '')\n const alike =\n (componentId === 'muiGrid' && !!at.props?.['container']) ||\n (ITEMS.has(first) && kids.every((child) => node(child)?.componentId === first))\n result = alike && !holdsGroup(id)\n }\n groupMemo.set(id, result)\n return result\n }\n /** A group sits somewhere inside the element. */\n const holdsGroup = (id: string): boolean => {\n const known = belowMemo.get(id)\n if (known !== undefined) return known\n const result = children(id).some((child) => isGroup(child) || holdsGroup(child))\n belowMemo.set(id, result)\n return result\n }\n /** Leads with a picture: its first leaf, down its first children, is one, or the frame of one. */\n const leadsWithMedia = (id: string): boolean => {\n let at = id\n for (let depth = 0; depth < 12; depth += 1) {\n if (MEDIA.has(String(node(at)?.componentId ?? '')) || isFrame(at)) return true\n const kids = children(at)\n if (!kids.length) return false\n at = kids[0]\n }\n return false\n }\n const presetOf = (id: string, group: boolean, words: AnimationPreset): AnimationPreset => {\n const media = group ? children(id).every(leadsWithMedia) : leadsWithMedia(id)\n return media ? character.mediaPreset : words\n }\n\n const animate = (\n id: string,\n trigger: AnimationTrigger,\n delayMs: number,\n words: AnimationPreset = character.preset,\n side?: AnimationPreset,\n ): void => {\n const group = isGroup(id)\n const preset = presetOf(id, group, group && character.groupPreset ? character.groupPreset : words)\n patches.set(id, {\n [NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,\n [NODE_ANIMATION_TRIGGER_PROP]: trigger,\n [NODE_ANIMATION_DURATION_PROP]: clamp(character.durationMs, ANIMATION_MAX_DURATION_MS),\n ...(delayMs > 0 ? { [NODE_ANIMATION_DELAY_PROP]: clamp(delayMs, ANIMATION_MAX_DELAY_MS) } : {}),\n [NODE_ANIMATION_EASE_PROP]: character.ease,\n ...(group\n ? {\n [NODE_ANIMATION_STAGGER_PROP]: true,\n [NODE_ANIMATION_STAGGER_STEP_PROP]: clamp(character.staggerStepMs, ANIMATION_MAX_STAGGER_STEP_MS),\n }\n : {}),\n })\n }\n\n /** Down through wrappers of one child, to where a section's content starts. */\n const contentOf = (id: string): string => {\n let at = id\n while (!closed(at) && !isGroup(at)) {\n const kids = children(at)\n if (kids.length !== 1) break\n at = kids[0]\n }\n return at\n }\n\n /**\n * A column of words beside a picture: two blocks, one of which leads with a\n * picture and one of which does not, neither left alone. Two cards that\n * both lead with one are a group, and stagger.\n */\n const isSplit = (id: string): boolean => {\n const kids = children(id)\n return kids.length === 2 && leadsWithMedia(kids[0]) !== leadsWithMedia(kids[1]) && !kids.some(holds)\n }\n\n /** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */\n const place = (id: string, words: AnimationPreset): void => {\n if (closed(id)) return\n if (isSplit(id)) {\n const [start, end] = children(id)\n animate(start, 'scroll', 0, words, character.sides ? 'slide-right' : undefined)\n animate(end, 'scroll', character.staggerStepMs, words, character.sides ? 'slide-left' : undefined)\n return\n }\n if (holds(id) || (!isGroup(id) && holdsGroup(id))) {\n for (const child of children(id)) place(child, words)\n return\n }\n animate(id, 'scroll', 0, words)\n }\n\n /** Down through wrappers of one child that are free to move. */\n const descend = (id: string): string => {\n let at = id\n for (let kids = children(at); kids.length === 1 && !closed(kids[0]); kids = children(at)) at = kids[0]\n return at\n }\n\n /**\n * The first section's content: on load, one rung per block, around the\n * blocks that stay still. A rung that holds one is opened up, down to the\n * parts beside it, so the eyebrow, the lede and the buttons still climb.\n */\n const ladder = (id: string): void => {\n if (closed(id)) return\n // A repeat's one child is the card every record is drawn with: the\n // repeat staggers its records rather than each record playing at once.\n if (isGroup(id) && children(id).length < 2) return animate(id, 'load', 0)\n const around = (at: string): string[] =>\n children(at).flatMap((child) => (closed(child) ? [] : holds(child) ? around(child) : [child]))\n const base = children(id).length ? around(id) : [id]\n // A column of words beside a picture is opened up, so its lines climb one\n // after another before the picture lands.\n const rungs: string[] = []\n base.forEach((rung, index) => {\n const inner = descend(rung)\n const kids = children(inner)\n const opens =\n kids.length >= 2 &&\n !isGroup(inner) &&\n !leadsWithMedia(inner) &&\n rungs.length + kids.length + (base.length - index - 1) <= ELEMENT_MOTION_HERO_RUNGS\n rungs.push(...(opens ? kids : [rung]))\n })\n // A rung left alone takes no step of the ladder: the first block that moves starts at once.\n rungs\n .filter((rung) => !closed(rung))\n .forEach((rung, index) => animate(rung, 'load', Math.min(index, ELEMENT_MOTION_HERO_RUNGS - 1) * character.heroStepMs))\n }\n\n /**\n * What the first section paints first, and so what Lighthouse times as its\n * Largest Contentful Paint (AGL-3581): its main heading — the h1, else its\n * first heading — and its largest picture. Neither ever starts at opacity 0,\n * which would hold the paint back by the length of the entrance.\n */\n const markStill = (sectionId: string): void => {\n const order: string[] = []\n const walk = (id: string): void => {\n order.push(id)\n for (const child of children(id)) walk(child)\n }\n walk(sectionId)\n const typeOf = (id: string) => {\n const at = node(id)\n return at?.componentId === 'muiTypography' ? String(at.props?.['variant'] ?? '') : null\n }\n const main = (id: string) => /^(h1|display)/i.test(typeOf(id) ?? '') || (typeOf(id) !== null && node(id)?.props?.['component'] === 'h1')\n const heading = order.find(main) ?? order.find((id) => /^(h[1-6]|display)/i.test(typeOf(id) ?? ''))\n if (heading) still.add(heading)\n const size = (id: string): number => {\n const at = node(id)\n const read = (value: unknown) => (typeof value === 'number' ? value : parseFloat(String(value ?? '')) || 0)\n return Math.max(read(at?.props?.['height']), read(at?.props?.['width']), read(at?.sx?.['height']), read(at?.sx?.['width']))\n }\n let picture: string | undefined\n for (const id of order) {\n if (MEDIA.has(String(node(id)?.componentId ?? '')) && (picture === undefined || size(id) > size(picture))) picture = id\n }\n if (picture) still.add(picture)\n }\n\n const sections = (options.sectionIds ?? children(rootId)).filter((id) => !!node(id))\n if (sections[0]) markStill(sections[0])\n const turns = character.sectionPresets?.length ? character.sectionPresets : [character.preset]\n sections.forEach((id, index) => {\n if (closed(id)) return\n const content = contentOf(id)\n // The band itself never moves — only what it holds — so a section that\n // is nothing but its band has nothing to move.\n if (content === id && !children(id).length) return\n const words = turns[(index - 1) % turns.length] ?? character.preset\n if (index === 0) ladder(content)\n else if (content === id && !isGroup(id) && !isSplit(id))\n for (const child of children(id)) place(child, words)\n else place(content, words)\n })\n\n if (!patches.size) return nodes\n const next: Record<string, any> = { ...nodes }\n for (const [id, props] of patches) {\n next[id] = { ...nodes[id], props: { ...(nodes[id]?.props ?? {}), ...props } }\n }\n return next as T\n}\n"],"names":["CANVAS_ROOT_ELEMENT_ID","ANIMATION_MAX_DELAY_MS","ANIMATION_MAX_DURATION_MS","ANIMATION_MAX_STAGGER_STEP_MS","NODE_ANIMATION_DELAY_PROP","NODE_ANIMATION_DURATION_PROP","NODE_ANIMATION_EASE_PROP","NODE_ANIMATION_PROPS","NODE_ANIMATION_PROP","NODE_ANIMATION_STAGGER_PROP","NODE_ANIMATION_STAGGER_STEP_PROP","NODE_ANIMATION_TRIGGER_PROP","ELEMENT_MOTION_DEFAULT","preset","mediaPreset","ease","durationMs","staggerStepMs","heroStepMs","sectionPresets","sides","ELEMENT_MOTION_HERO_RUNGS","ELEMENT_MOTION_NEVER","Set","MEDIA","LISTS","ITEMS","clamp","value","max","Math","min","round","pins","Object","values","some","applyElementMotion","nodes","options","character","rootId","node","id","children","filter","child","patches","Map","still","refused","at","hidden","has","String","componentId","props","prop","undefined","closed","sx","overlay","isFrame","kids","every","length","holdsClosed","holds","known","get","set","result","groupMemo","belowMemo","isGroup","first","alike","holdsGroup","leadsWithMedia","depth","presetOf","group","words","media","animate","trigger","delayMs","side","groupPreset","startsWith","contentOf","isSplit","place","start","end","descend","ladder","around","flatMap","base","rungs","forEach","rung","index","inner","opens","push","markStill","sectionId","order","walk","typeOf","main","test","heading","find","add","size","read","parseFloat","picture","sections","sectionIds","turns","content","next"],"mappings":"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"}
|
|
@@ -42,7 +42,12 @@ import { type AglynOrgCustomRole } from './org-permissions';
|
|
|
42
42
|
* plugins declare.
|
|
43
43
|
*/
|
|
44
44
|
export declare function hostPermissionKeys(): HostPermissionKey[];
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* What a host role grants a site collaborator for every per-site key, before
|
|
47
|
+
* any toggle. A key whose site-role preset holds for members of the whole
|
|
48
|
+
* workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator
|
|
49
|
+
* nothing, whatever their site role.
|
|
50
|
+
*/
|
|
46
51
|
export declare function hostRolePermissionDefaults(role: HostAccessRole): Record<HostPermissionKey, boolean>;
|
|
47
52
|
/**
|
|
48
53
|
* A collaborator's verdict on ONE site for every per-site key, or null where
|
|
@@ -89,6 +94,23 @@ export declare function hostStampedSitePermission(host: {
|
|
|
89
94
|
memberPermissions?: unknown;
|
|
90
95
|
memberRoles?: unknown;
|
|
91
96
|
} | null | undefined, uid: string, permission: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* The same for a key whose preset holds for members of the whole workspace
|
|
99
|
+
* only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot
|
|
100
|
+
* answer: an admin of one site and an admin of the whole workspace project
|
|
101
|
+
* the same `'admin'` there. The site's stamp when the projection wrote one;
|
|
102
|
+
* else the preset of the role `memberRoles` holds for them AND of the role
|
|
103
|
+
* the fresh resolution gives them on the site, for a member of the whole
|
|
104
|
+
* workspace only. Exactly the gate these keys replaced: the projection's
|
|
105
|
+
* role, `orgWide` and the fresh site role, all three.
|
|
106
|
+
*/
|
|
107
|
+
export declare function hostStampedWorkspaceSitePermission(host: {
|
|
108
|
+
memberPermissions?: unknown;
|
|
109
|
+
memberRoles?: unknown;
|
|
110
|
+
} | null | undefined, uid: string, permission: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>, membership: {
|
|
111
|
+
orgWide: boolean;
|
|
112
|
+
hostRole: string | null | undefined;
|
|
113
|
+
}): boolean;
|
|
92
114
|
/** How one site narrows a workspace-wide member: the access they hold there, the site's verdicts, and which keys follow it. */
|
|
93
115
|
export interface SiteNarrowing {
|
|
94
116
|
/** Their role on the site, narrower than their workspace role. */
|
|
@@ -195,6 +217,14 @@ export type HostDocumentLike = {
|
|
|
195
217
|
* the host document already; it costs no read.
|
|
196
218
|
*/
|
|
197
219
|
export declare function holdsSiteKey(hostDoc: HostDocumentLike, uid: string, key: CoreSiteKey): boolean;
|
|
220
|
+
/**
|
|
221
|
+
* The same for a plugin's key whose preset is the site role (AGL-4047): the
|
|
222
|
+
* site's `memberPermissions` stamp, else `presetFor` read for the member's
|
|
223
|
+
* `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the
|
|
224
|
+
* answer does not depend on which plugins this process loaded. For the doors
|
|
225
|
+
* and the cards that hold the host document already; it costs no read.
|
|
226
|
+
*/
|
|
227
|
+
export declare function holdsStampedSiteKey(hostDoc: HostDocumentLike, uid: string | null | undefined, key: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>): boolean;
|
|
198
228
|
/**
|
|
199
229
|
* The refusal a door answers when a member does not hold a site key
|
|
200
230
|
* (AGL-4047): it names the permission and who can change it, never a role.
|
|
@@ -45,11 +45,16 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
45
45
|
*/ export function hostPermissionKeys() {
|
|
46
46
|
return ORG_PERMISSIONS.filter((definition)=>definition.hostRoleDefaults !== undefined).map((definition)=>definition.key);
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* What a host role grants a site collaborator for every per-site key, before
|
|
50
|
+
* any toggle. A key whose site-role preset holds for members of the whole
|
|
51
|
+
* workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator
|
|
52
|
+
* nothing, whatever their site role.
|
|
53
|
+
*/ export function hostRolePermissionDefaults(role) {
|
|
49
54
|
const defaults = {};
|
|
50
55
|
for (const definition of ORG_PERMISSIONS){
|
|
51
56
|
if (!definition.hostRoleDefaults) continue;
|
|
52
|
-
defaults[definition.key] = definition.hostRoleDefaults[role] === true;
|
|
57
|
+
defaults[definition.key] = !definition.workspaceMembersOnly && definition.hostRoleDefaults[role] === true;
|
|
53
58
|
}
|
|
54
59
|
return defaults;
|
|
55
60
|
}
|
|
@@ -115,6 +120,12 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
115
120
|
* workspace standing to refine, so only the site's toggle moves their
|
|
116
121
|
* verdict, as for every other per-site key.
|
|
117
122
|
*
|
|
123
|
+
* A key declared `workspaceMembersOnly` replaces a gate that asked for an
|
|
124
|
+
* admin of the whole workspace who is also this site's admin: its preset at
|
|
125
|
+
* 2 is the site role's for a member of the whole workspace and refusal for a
|
|
126
|
+
* collaborator, whose site role (even Admin) never carried it. The site's
|
|
127
|
+
* toggle at 4 still grants it to them.
|
|
128
|
+
*
|
|
118
129
|
* These keys narrow nothing (`narrowsPerSite` is dropped): the site role is
|
|
119
130
|
* already their preset. With no site named, the org catalog's verdict
|
|
120
131
|
* (`roleDefaults`, then the layers) stands, which is the same answer for a
|
|
@@ -142,7 +153,8 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
142
153
|
]));
|
|
143
154
|
}
|
|
144
155
|
const toggles = (_member_hostPermissions = member.hostPermissions) == null ? void 0 : _member_hostPermissions[hostId];
|
|
145
|
-
const
|
|
156
|
+
const workspaceMember = isOrgWideMember(member);
|
|
157
|
+
const layers = workspaceMember ? [
|
|
146
158
|
member.roleId ? customRole == null ? void 0 : customRole.permissions : undefined,
|
|
147
159
|
member.permissions,
|
|
148
160
|
toggles
|
|
@@ -152,7 +164,7 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
152
164
|
const out = {};
|
|
153
165
|
for (const definition of definitions){
|
|
154
166
|
var _definition_hostRoleDefaults;
|
|
155
|
-
let verdict = ((_definition_hostRoleDefaults = definition.hostRoleDefaults) == null ? void 0 : _definition_hostRoleDefaults[role]) === true;
|
|
167
|
+
let verdict = (workspaceMember || !definition.workspaceMembersOnly) && ((_definition_hostRoleDefaults = definition.hostRoleDefaults) == null ? void 0 : _definition_hostRoleDefaults[role]) === true;
|
|
156
168
|
for (const layer of layers){
|
|
157
169
|
const value = layer == null ? void 0 : layer[definition.key];
|
|
158
170
|
if (typeof value === 'boolean') verdict = value;
|
|
@@ -188,6 +200,25 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
188
200
|
const role = host == null ? void 0 : (_host_memberRoles = host.memberRoles) == null ? void 0 : _host_memberRoles[uid];
|
|
189
201
|
return typeof role === 'string' && presetFor[role] === true;
|
|
190
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The same for a key whose preset holds for members of the whole workspace
|
|
205
|
+
* only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot
|
|
206
|
+
* answer: an admin of one site and an admin of the whole workspace project
|
|
207
|
+
* the same `'admin'` there. The site's stamp when the projection wrote one;
|
|
208
|
+
* else the preset of the role `memberRoles` holds for them AND of the role
|
|
209
|
+
* the fresh resolution gives them on the site, for a member of the whole
|
|
210
|
+
* workspace only. Exactly the gate these keys replaced: the projection's
|
|
211
|
+
* role, `orgWide` and the fresh site role, all three.
|
|
212
|
+
*/ export function hostStampedWorkspaceSitePermission(host, uid, permission, presetFor, membership) {
|
|
213
|
+
var _host_memberPermissions_uid, _host_memberPermissions;
|
|
214
|
+
const stamped = host == null ? void 0 : (_host_memberPermissions = host.memberPermissions) == null ? void 0 : (_host_memberPermissions_uid = _host_memberPermissions[uid]) == null ? void 0 : _host_memberPermissions_uid[permission];
|
|
215
|
+
if (typeof stamped === 'boolean') return stamped;
|
|
216
|
+
if (membership.orgWide !== true) return false;
|
|
217
|
+
const fresh = membership.hostRole;
|
|
218
|
+
return typeof fresh === 'string' && presetFor[fresh] === true && hostStampedSitePermission({
|
|
219
|
+
memberRoles: host == null ? void 0 : host.memberRoles
|
|
220
|
+
}, uid, permission, presetFor);
|
|
221
|
+
}
|
|
191
222
|
/*
|
|
192
223
|
* A SITE NARROWS A WORKSPACE-WIDE MEMBER (AGL-4047).
|
|
193
224
|
*
|
|
@@ -432,6 +463,15 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
432
463
|
* the host document already; it costs no read.
|
|
433
464
|
*/ export function holdsSiteKey(hostDoc, uid, key) {
|
|
434
465
|
var _CORE_SITE_ROLE_PRESETS_key;
|
|
466
|
+
return holdsStampedSiteKey(hostDoc, uid, key, (_CORE_SITE_ROLE_PRESETS_key = CORE_SITE_ROLE_PRESETS[key]) != null ? _CORE_SITE_ROLE_PRESETS_key : {});
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* The same for a plugin's key whose preset is the site role (AGL-4047): the
|
|
470
|
+
* site's `memberPermissions` stamp, else `presetFor` read for the member's
|
|
471
|
+
* `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the
|
|
472
|
+
* answer does not depend on which plugins this process loaded. For the doors
|
|
473
|
+
* and the cards that hold the host document already; it costs no read.
|
|
474
|
+
*/ export function holdsStampedSiteKey(hostDoc, uid, key, presetFor) {
|
|
435
475
|
if (!hostDoc || !uid) return false;
|
|
436
476
|
// `hostDoc`, not `host`: the host-field deny guard reads every `host.…`
|
|
437
477
|
// in this folder as a field of the host document, and `get` is none.
|
|
@@ -439,7 +479,7 @@ import { orgRoleTier } from "./org-roles.js";
|
|
|
439
479
|
return hostStampedSitePermission({
|
|
440
480
|
memberPermissions: read('memberPermissions'),
|
|
441
481
|
memberRoles: read('memberRoles')
|
|
442
|
-
}, uid, key,
|
|
482
|
+
}, uid, key, presetFor);
|
|
443
483
|
}
|
|
444
484
|
/**
|
|
445
485
|
* The refusal a door answers when a member does not hold a site key
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-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 {\n AglynOrgMember,\n HostAccessRole,\n HostPermissionKey,\n HostUid,\n OrgRole,\n} from '../foundation'\nimport { refinePermissionMap, type PermissionRefinement } from '../plugin-manager/plugin-permissions'\nimport { hostRoleFor, isOrgWideMember } from './organizations'\nimport { CORE_SITE_ROLE_PRESETS } from './org-role-grants'\nimport {\n permissionRefusalText,\n ORG_PERMISSION_REFINEMENTS,\n ORG_PERMISSIONS,\n resolveOrgPermissions,\n type AglynOrgCustomRole,\n type OrgPermissionDefinition,\n} from './org-permissions'\nimport { orgRoleTier } from './org-roles'\n\n/**\n * Catalog keys a site collaborator holds PER SITE (AGL-2984), resolved on one\n * of two axes.\n *\n * A plugin that declares a catalog key with `hostRoleDefaults` makes it a\n * per-site key. For the people whose membership spans the workspace, the key\n * is an ordinary catalog key: their org role's default, refined by a custom\n * role and by per-member overrides, the same on every site. A site\n * collaborator has no org standing to refine — their membership exists only\n * to carry access to named sites — so for them the key is decided PER SITE,\n * from the host role they hold there, refined by `hostPermissions[hostId]` on\n * their member document.\n *\n * Which axis applies is `isOrgWideMember`, the predicate the rules projection\n * and the scope resolver already share. A collaborator asked about a site\n * they were never granted resolves to nothing, and so does one whose request\n * names no site at all: there is no host role to read a default from, and\n * guessing one would let a request reach around the per-site toggle by\n * omitting the site.\n */\n\n/**\n * The catalog keys a collaborator can hold per site, in catalog order: every\n * key with host-role defaults, the core's site keys (AGL-4047) and the ones\n * plugins declare.\n */\nexport function hostPermissionKeys(): HostPermissionKey[] {\n return ORG_PERMISSIONS.filter((definition) => definition.hostRoleDefaults !== undefined).map(\n (definition) => definition.key,\n )\n}\n\n/** What a host role grants for every per-site key, before any toggle. */\nexport function hostRolePermissionDefaults(\n role: HostAccessRole,\n): Record<HostPermissionKey, boolean> {\n const defaults: Record<HostPermissionKey, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.hostRoleDefaults) continue\n defaults[definition.key] = definition.hostRoleDefaults[role] === true\n }\n return defaults\n}\n\n/** Every per-site key, refused. */\nfunction noHostPermissions(): Record<HostPermissionKey, boolean> {\n return Object.fromEntries(hostPermissionKeys().map((key) => [key, false]))\n}\n\n/**\n * The catalog's refinements whose key and broader key are both in `map`\n * (AGL-4047): the links a per-site map can settle on its own. A link to a\n * key the map does not carry would read that key as refused and add it.\n */\nfunction refinementsWithin(map: Readonly<Record<string, boolean>>): PermissionRefinement[] {\n return ORG_PERMISSION_REFINEMENTS.filter(([key, parent]) => key in map && parent in map)\n}\n\n/**\n * A collaborator's verdict on ONE site for every per-site key, or null where\n * they have no access to that site at all. An absent toggle reads as the host\n * role's default, which is what makes a member document written before\n * `hostPermissions` existed resolve exactly as it did before.\n */\nexport function resolveCollaboratorHostPermissions(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): Record<HostPermissionKey, boolean> | null {\n if (!member || !hostId) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const merged = hostRolePermissionDefaults(role)\n const toggles = member.hostPermissions?.[hostId]\n if (toggles) {\n for (const key of Object.keys(merged)) {\n const value = toggles[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n // A per-site key that follows or refines another per-site key (AGL-4047)\n // takes its verdict from that key on this site where the site sets nothing\n // for it, as the org catalog's resolver does with a custom role.\n return refinePermissionMap(merged, refinementsWithin(merged), [toggles])\n}\n\n/*\n * KEYS WHOSE PRESET IS THE SITE ROLE (AGL-4047).\n *\n * Some checks were written against the member's role on a site: who may book\n * a walk-in, change a stay or edit a site's reservations asked whether their\n * `memberRoles` entry was Admin, Editor or Author. Roles are only presets of\n * permissions, so those checks became keys, and a key that replaces a gate\n * on the site role keeps that role as its preset (`presetFromSiteRole`):\n *\n * 1. the member's role ON THE SITE (`hostRoleFor`): Admin for the owner and\n * admins, a workspace-wide editor's or viewer's own role unless the site\n * gives them other access there, narrower or wider, and a collaborator's\n * site role; no access to the site holds nothing;\n * 2. that role's `hostRoleDefaults` verdict for the key;\n * 3. then, for a workspace-wide member, their custom role's verdict and\n * their own permissions' verdict, when either sets the key;\n * 4. then the site's own toggle for them (`hostPermissions[hostId][key]`).\n *\n * With nothing set at 3 or 4, the verdict is exactly what the role gate\n * answered, for every member on every site, which is how the built-in roles\n * keep the access they had. A collaborator has no custom role and no\n * workspace standing to refine, so only the site's toggle moves their\n * verdict, as for every other per-site key.\n *\n * These keys narrow nothing (`narrowsPerSite` is dropped): the site role is\n * already their preset. With no site named, the org catalog's verdict\n * (`roleDefaults`, then the layers) stands, which is the same answer for a\n * workspace-wide member on a site where their access is their workspace role.\n */\n\n/** The catalog keys whose preset is the member's role on the site, in catalog order. */\nexport function siteRolePresetPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole).map(\n (definition) => definition.key,\n )\n}\n\n/** Is this a key whose preset is the member's role on the site? */\nexport function isSiteRolePresetPermission(permission: string): boolean {\n return ORG_PERMISSIONS.some(\n (definition) => definition.key === permission && definition.presetFromSiteRole === true,\n )\n}\n\n/**\n * A member's verdict on ONE site for every key whose preset is their role\n * there (see above). `customRole` is their custom role document when they\n * carry a `roleId` and the caller has it, and is ignored for a collaborator.\n * No site named, or no access to it, refuses every such key.\n */\nexport function resolveSiteRolePresetPermissions(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): Record<string, boolean> {\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole)\n if (!definitions.length) return {}\n const role = member && hostId ? hostRoleFor(member, hostId) : null\n if (!member || !hostId || !role) {\n return Object.fromEntries(definitions.map((definition) => [definition.key, false]))\n }\n const toggles = member.hostPermissions?.[hostId]\n const layers: ReadonlyArray<Readonly<Partial<Record<string, unknown>>> | null | undefined> =\n isOrgWideMember(member)\n ? [member.roleId ? customRole?.permissions : undefined, member.permissions, toggles]\n : [toggles]\n const out: Record<string, boolean> = {}\n for (const definition of definitions) {\n let verdict = definition.hostRoleDefaults?.[role] === true\n for (const layer of layers) {\n const value = layer?.[definition.key]\n if (typeof value === 'boolean') verdict = value\n }\n out[definition.key] = verdict\n }\n return refinePermissionMap(out, refinementsWithin(out), layers)\n}\n\n/**\n * `granted` with every key whose preset is the site role decided for this\n * site. With no site named it is `granted` as given, so a resolver can call\n * it unconditionally.\n */\nexport function applySiteRolePresets<T extends Record<string, boolean>>(\n granted: T,\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): T {\n if (!hostId) return granted\n return { ...granted, ...resolveSiteRolePresetPermissions(member, hostId, customRole) }\n}\n\n/**\n * The verdict the site's own stamp gives a member for one key whose preset\n * is the site role, as the Firestore rules read it (AGL-4047): the site's\n * `memberPermissions[uid][key]` when the projection wrote one, else the\n * preset of the role the site's `memberRoles` projection holds for them.\n *\n * For a door that has the host document in hand and asks what the rules\n * ask, so the two cannot disagree: the stamp is written beside\n * `memberRoles` by the same projection, with every layer applied, and a\n * site the projection has not reached since the key existed answers exactly\n * as the role gate it replaced. `presetFor` is the declaring plugin's own\n * table, so the answer does not depend on which plugins this process loaded.\n */\nexport function hostStampedSitePermission(\n host: { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n permission: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n): boolean {\n const stamped = (host?.memberPermissions as Record<string, Record<string, unknown> | undefined> | undefined)?.[uid]?.[\n permission\n ]\n if (typeof stamped === 'boolean') return stamped\n const role = (host?.memberRoles as Record<string, unknown> | undefined)?.[uid]\n return typeof role === 'string' && presetFor[role as HostAccessRole] === true\n}\n\n/*\n * A SITE NARROWS A WORKSPACE-WIDE MEMBER (AGL-4047).\n *\n * An editor who works across the workspace can be given Viewer or Author\n * access to one site (`hostAccess[hostId]`, which `hostRoleFor` reads ahead\n * of `allHosts`). That narrowed their content rights there and nothing else,\n * so a key the workspace grants an editor, such as working a site's\n * register, still held on the site they were narrowed on.\n *\n * A plugin marks the keys a site CAN narrow with `narrowsPerSite`, and\n * whether each one does is a control with three layers, the nearest set one\n * winning (`siteNarrowingChoices`):\n *\n * - the member's own choice (`member.siteNarrowing[key]`);\n * - else their custom role's (`customRole.siteNarrowing[key]`);\n * - else the built-in role's default the declaration names.\n *\n * `true` is \"Follow the site role\", `false` is \"Keep this permission\". On a\n * site that narrows the member, each key that follows the site role is\n * decided for that site:\n *\n * 1. the site's own verdict for the key (`hostPermissions[hostId][key]`),\n * when the site set one;\n * 2. else, for a key that refines another key that also follows the site\n * role, that key's verdict on this site, never more than the workspace\n * grants;\n * 3. else the narrower access's default for the key, never more than the\n * workspace grants;\n *\n * and a key they keep is the workspace's verdict, as everywhere else. A finer\n * key they hold on the site (one the site grants, or one they keep and the\n * workspace grants) brings the key it refines on this site with it, unless\n * the site refuses that key, because a refining key is never held without\n * it. The other finer keys do not follow that implied grant: allowing \"sell\"\n * on a site is not allowing refunds.\n *\n * A member whose access to every site matches their workspace role (the\n * owner and admins always do), one who keeps every key a site can narrow,\n * and a site collaborator, whose keys come from their site role already, all\n * resolve exactly as before.\n */\n\n/** Host roles from the widest down, for \"is this site's access narrower?\". */\nconst HOST_ROLE_RANK: Readonly<Record<HostAccessRole, number>> = {\n admin: 3,\n editor: 2,\n author: 1,\n viewer: 0,\n}\n\n/** How one site narrows a workspace-wide member: the access they hold there, the site's verdicts, and which keys follow it. */\nexport interface SiteNarrowing {\n /** Their role on the site, narrower than their workspace role. */\n role: HostAccessRole\n /** `hostPermissions[hostId]`: the verdicts the site set for them. */\n grants: Readonly<Partial<Record<string, unknown>>>\n /**\n * Every key a site can narrow, `true` where it follows the site role for\n * this member and `false` where they keep it (`siteNarrowingChoices`).\n */\n narrows: Readonly<Record<string, boolean>>\n}\n\n/** The catalog keys a site CAN narrow (`narrowsPerSite`), in catalog order. */\nexport function siteNarrowedPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite).map(\n (definition) => definition.key,\n )\n}\n\n/**\n * Could any site narrow a member of this org role? Not the owner or an\n * admin, who are admins of every site, and not a role whose site access has\n * nothing narrower below it (a viewer). The editors' control is the one that\n * changes anything; the role editor says so beside it.\n */\nexport function roleCanBeNarrowed(role: string | null | undefined): boolean {\n if (role === 'owner' || role === 'admin') return false\n return HOST_ROLE_RANK[orgRoleTier(role) as HostAccessRole] > 0\n}\n\n/**\n * Whether each key a site can narrow follows the site role for this member\n * (`true`) or is kept (`false`): their own choice, else their custom role's\n * (when they carry its id and it was supplied), else the built-in role's\n * default.\n */\nexport function siteNarrowingChoices(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): Record<string, boolean> {\n const role = (member?.role ?? 'viewer') as OrgRole\n const own = member?.siteNarrowing ?? {}\n const fromRole = member?.roleId ? (customRole?.siteNarrowing ?? {}) : {}\n const out: Record<string, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.narrowsPerSite) continue\n const key = definition.key\n const mine = own[key]\n const theirRole = fromRole[key]\n out[key] =\n typeof mine === 'boolean'\n ? mine\n : typeof theirRole === 'boolean'\n ? theirRole\n : definition.narrowsPerSite[role] === true\n }\n return out\n}\n\n/**\n * Is this member's access to this site narrower than their workspace role?\n * The cheap half of `siteNarrowingFor`, which needs no custom role: a caller\n * that has to read one asks this first, and reads nothing for everyone else.\n */\nexport function siteMayNarrow(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): HostAccessRole | null {\n if (!member || !hostId || !isOrgWideMember(member)) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const workspaceRole = orgRoleTier(member.role) as HostAccessRole\n if (HOST_ROLE_RANK[role] >= HOST_ROLE_RANK[workspaceRole]) return null\n return role\n}\n\n/**\n * How this site narrows this member, or null where it does not: a member\n * who is not workspace-wide, has no access to the site, whose access there\n * is as wide as their workspace role (owners and admins always), or who\n * keeps every key a site can narrow. `customRole` is their custom role\n * document when they carry a `roleId`; without it, only their own choices\n * and the built-in defaults decide.\n */\nexport function siteNarrowingFor(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): SiteNarrowing | null {\n const role = siteMayNarrow(member, hostId)\n if (!role) return null\n const narrows = siteNarrowingChoices(member, customRole)\n if (!Object.values(narrows).some(Boolean)) return null\n return { role, grants: member?.hostPermissions?.[hostId as HostUid] ?? {}, narrows }\n}\n\n/** The keys that follow the site role under a narrowing, in catalog order; none without one. */\nexport function narrowedPermissionKeysOf(narrowing: SiteNarrowing | null | undefined): string[] {\n if (!narrowing) return []\n return siteNarrowedPermissionKeys().filter((key) => narrowing.narrows[key] === true)\n}\n\n/** What a site access holds by default for a key: its site default, else its tier's (Author on the Viewer tier). */\nfunction narrowedDefault(definition: OrgPermissionDefinition, role: HostAccessRole): boolean {\n const site = definition.hostRoleDefaults?.[role]\n if (typeof site === 'boolean') return site\n return definition.roleDefaults[orgRoleTier(role)] === true\n}\n\n/**\n * A workspace-wide member's resolved map with the keys that follow the site\n * role decided for that site (see above). With no narrowing it is the map\n * as given, so every resolver can call it unconditionally.\n */\nexport function narrowPermissionsToSite<T extends Record<string, boolean>>(\n granted: T,\n narrowing: SiteNarrowing | null | undefined,\n): T {\n if (!narrowing) return granted\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite)\n if (!definitions.length) return granted\n const narrowable = new Set(definitions.map((definition) => definition.key))\n const follows = (key: string): boolean => narrowing.narrows[key] === true\n // A finer key's broader key, among the keys a site can narrow.\n const broader = new Map<string, string>()\n for (const [key, parent, kind] of ORG_PERMISSION_REFINEMENTS) {\n if (kind !== 'follows' && narrowable.has(key) && narrowable.has(parent)) broader.set(key, parent)\n }\n const siteVerdict = (key: string): boolean | undefined => {\n const value = narrowing.grants[key]\n return typeof value === 'boolean' ? value : undefined\n }\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n for (let current = key; broader.has(current) && !seen.has(current); steps += 1) {\n seen.add(current)\n current = broader.get(current) as string\n }\n return steps\n }\n const out: Record<string, boolean> = { ...granted }\n for (const definition of [...definitions].sort((a, b) => depth(a.key) - depth(b.key))) {\n const key = definition.key\n const workspace = granted[key] === true\n // Kept: the workspace's verdict, as on every other site.\n if (!follows(key)) {\n out[key] = workspace\n continue\n }\n const own = siteVerdict(key)\n const parent = broader.get(key)\n if (own !== undefined) out[key] = own\n else if (parent && follows(parent)) out[key] = out[parent] === true && workspace\n else out[key] = workspace && narrowedDefault(definition, narrowing.role)\n }\n for (const definition of definitions) {\n const key = definition.key\n const held = follows(key) ? siteVerdict(key) === true : granted[key] === true\n if (!held) continue\n const seen = new Set<string>()\n for (let parent = broader.get(key); parent && !seen.has(parent); parent = broader.get(parent)) {\n seen.add(parent)\n // A kept broader key is the workspace's verdict already.\n if (!follows(parent) || siteVerdict(parent) === false) break\n out[parent] = true\n }\n }\n return refinePermissionMap(out, ORG_PERMISSION_REFINEMENTS) as T\n}\n\n/**\n * A member's verdict for every per-site key, on whichever axis their\n * membership is on.\n *\n * `customRole` is the member's custom role document when they carry a\n * `roleId` and the caller has it in hand, and is ignored for a collaborator,\n * whose org role is a seat-accounting fact rather than a grant. `hostId` is\n * the site the request is about; an org-wide member resolves the same on\n * every site and with none named, except on a site that narrows them.\n *\n * An org-wide member's map also carries every key a site can narrow\n * (AGL-4047), decided for `hostId`, so the site's `memberPermissions`\n * projection answers the rules what the server answers. A collaborator's\n * map does not: their register and waivers keys come from their site role\n * and their own overrides, which the rules read from their member document\n * as before.\n */\nexport function resolveMemberHostPermissions(options: {\n member: Partial<AglynOrgMember> | null | undefined\n customRole?: AglynOrgCustomRole | null\n hostId?: HostUid | null\n}): Record<HostPermissionKey, boolean> {\n const { member, customRole, hostId } = options\n if (!member) return noHostPermissions()\n if (isOrgWideMember(member)) {\n // The keys whose preset is the site role (AGL-4047) are decided for this\n // site last; they narrow nothing, so the narrowing pass leaves them be.\n const granted = applySiteRolePresets(\n narrowPermissionsToSite(\n resolveOrgPermissions(member, customRole ?? null),\n siteNarrowingFor(member, hostId, customRole ?? null),\n ),\n member,\n hostId,\n customRole ?? null,\n )\n return Object.fromEntries(\n [...hostPermissionKeys(), ...siteNarrowedPermissionKeys()].map((key) => [key, granted[key] === true]),\n )\n }\n return resolveCollaboratorHostPermissions(member, hostId) ?? noHostPermissions()\n}\n\n/**\n * The `memberPermissions` projection stamped on a host document beside\n * `memberRoles`: every member who can reach the site, with the verdict for\n * every per-site key they hold ON it. Org-wide members are resolved against\n * the custom roles the caller loaded (one read per distinct role, as\n * `syncOrgAuthProjections` already does for `resolvedPermissions`);\n * collaborators against their host role. A member who cannot reach the site\n * has no entry, matching `projectHostMemberRoles`.\n */\nexport function projectHostMemberPermissions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n hostId: HostUid,\n customRoles: ReadonlyMap<string, AglynOrgCustomRole | null>,\n): Record<string, Record<HostPermissionKey, boolean>> {\n const projection: Record<string, Record<HostPermissionKey, boolean>> = {}\n for (const member of members) {\n if (!hostRoleFor(member, hostId)) continue\n projection[member.$id] = resolveMemberHostPermissions({\n member,\n customRole: member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n hostId,\n })\n }\n return projection\n}\n\n/** The core's site keys (AGL-4047): `CORE_SITE_ROLE_PRESETS`. */\nexport type CoreSiteKey = 'site.edit' | 'site.publish' | 'site.sensitive' | 'site.manage'\n\n/** A host document, as a snapshot (`get`) or as its data. */\nexport type HostDocumentLike =\n | { get(field: string): unknown }\n | { memberPermissions?: unknown; memberRoles?: unknown }\n | null\n | undefined\n\n/**\n * Does this member hold one of the core's site keys on this site (AGL-4047)?\n * The door's twin of the rules' `canWriteHostContent`,\n * `canPublishHostContent`, `canReadHostSensitive` and site-settings checks:\n * the site's `memberPermissions` stamp, else the preset of the member's\n * `memberRoles` entry (`hostStampedSitePermission`), so a route that asks it\n * answers what the rules answer for the same write. For the routes that read\n * the host document already; it costs no read.\n */\nexport function holdsSiteKey(hostDoc: HostDocumentLike, uid: string, key: CoreSiteKey): boolean {\n if (!hostDoc || !uid) return false\n // `hostDoc`, not `host`: the host-field deny guard reads every `host.…`\n // in this folder as a field of the host document, and `get` is none.\n const read = (field: string): unknown =>\n typeof (hostDoc as { get?: unknown }).get === 'function'\n ? (hostDoc as { get(field: string): unknown }).get(field)\n : (hostDoc as Record<string, unknown>)[field]\n return hostStampedSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n CORE_SITE_ROLE_PRESETS[key] ?? {},\n )\n}\n\n/**\n * The refusal a door answers when a member does not hold a site key\n * (AGL-4047): it names the permission and who can change it, never a role.\n */\nexport function siteKeyRefusal(key: string): string {\n return permissionRefusalText(key)\n}\n\n"],"names":["refinePermissionMap","hostRoleFor","isOrgWideMember","CORE_SITE_ROLE_PRESETS","permissionRefusalText","ORG_PERMISSION_REFINEMENTS","ORG_PERMISSIONS","resolveOrgPermissions","orgRoleTier","hostPermissionKeys","filter","definition","hostRoleDefaults","undefined","map","key","hostRolePermissionDefaults","role","defaults","noHostPermissions","Object","fromEntries","refinementsWithin","parent","resolveCollaboratorHostPermissions","member","hostId","merged","toggles","hostPermissions","keys","value","siteRolePresetPermissionKeys","presetFromSiteRole","isSiteRolePresetPermission","permission","some","resolveSiteRolePresetPermissions","customRole","definitions","length","layers","roleId","permissions","out","verdict","layer","applySiteRolePresets","granted","hostStampedSitePermission","host","uid","presetFor","stamped","memberPermissions","memberRoles","HOST_ROLE_RANK","admin","editor","author","viewer","siteNarrowedPermissionKeys","narrowsPerSite","roleCanBeNarrowed","siteNarrowingChoices","own","siteNarrowing","fromRole","mine","theirRole","siteMayNarrow","workspaceRole","siteNarrowingFor","narrows","values","Boolean","grants","narrowedPermissionKeysOf","narrowing","narrowedDefault","site","roleDefaults","narrowPermissionsToSite","narrowable","Set","follows","broader","Map","kind","has","set","siteVerdict","depth","seen","steps","current","add","get","sort","a","b","workspace","held","resolveMemberHostPermissions","options","projectHostMemberPermissions","members","customRoles","projection","$id","holdsSiteKey","hostDoc","read","field","siteKeyRefusal"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-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 {\n AglynOrgMember,\n HostAccessRole,\n HostPermissionKey,\n HostUid,\n OrgRole,\n} from '../foundation'\nimport { refinePermissionMap, type PermissionRefinement } from '../plugin-manager/plugin-permissions'\nimport { hostRoleFor, isOrgWideMember } from './organizations'\nimport { CORE_SITE_ROLE_PRESETS } from './org-role-grants'\nimport {\n permissionRefusalText,\n ORG_PERMISSION_REFINEMENTS,\n ORG_PERMISSIONS,\n resolveOrgPermissions,\n type AglynOrgCustomRole,\n type OrgPermissionDefinition,\n} from './org-permissions'\nimport { orgRoleTier } from './org-roles'\n\n/**\n * Catalog keys a site collaborator holds PER SITE (AGL-2984), resolved on one\n * of two axes.\n *\n * A plugin that declares a catalog key with `hostRoleDefaults` makes it a\n * per-site key. For the people whose membership spans the workspace, the key\n * is an ordinary catalog key: their org role's default, refined by a custom\n * role and by per-member overrides, the same on every site. A site\n * collaborator has no org standing to refine — their membership exists only\n * to carry access to named sites — so for them the key is decided PER SITE,\n * from the host role they hold there, refined by `hostPermissions[hostId]` on\n * their member document.\n *\n * Which axis applies is `isOrgWideMember`, the predicate the rules projection\n * and the scope resolver already share. A collaborator asked about a site\n * they were never granted resolves to nothing, and so does one whose request\n * names no site at all: there is no host role to read a default from, and\n * guessing one would let a request reach around the per-site toggle by\n * omitting the site.\n */\n\n/**\n * The catalog keys a collaborator can hold per site, in catalog order: every\n * key with host-role defaults, the core's site keys (AGL-4047) and the ones\n * plugins declare.\n */\nexport function hostPermissionKeys(): HostPermissionKey[] {\n return ORG_PERMISSIONS.filter((definition) => definition.hostRoleDefaults !== undefined).map(\n (definition) => definition.key,\n )\n}\n\n/**\n * What a host role grants a site collaborator for every per-site key, before\n * any toggle. A key whose site-role preset holds for members of the whole\n * workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator\n * nothing, whatever their site role.\n */\nexport function hostRolePermissionDefaults(\n role: HostAccessRole,\n): Record<HostPermissionKey, boolean> {\n const defaults: Record<HostPermissionKey, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.hostRoleDefaults) continue\n defaults[definition.key] = !definition.workspaceMembersOnly && definition.hostRoleDefaults[role] === true\n }\n return defaults\n}\n\n/** Every per-site key, refused. */\nfunction noHostPermissions(): Record<HostPermissionKey, boolean> {\n return Object.fromEntries(hostPermissionKeys().map((key) => [key, false]))\n}\n\n/**\n * The catalog's refinements whose key and broader key are both in `map`\n * (AGL-4047): the links a per-site map can settle on its own. A link to a\n * key the map does not carry would read that key as refused and add it.\n */\nfunction refinementsWithin(map: Readonly<Record<string, boolean>>): PermissionRefinement[] {\n return ORG_PERMISSION_REFINEMENTS.filter(([key, parent]) => key in map && parent in map)\n}\n\n/**\n * A collaborator's verdict on ONE site for every per-site key, or null where\n * they have no access to that site at all. An absent toggle reads as the host\n * role's default, which is what makes a member document written before\n * `hostPermissions` existed resolve exactly as it did before.\n */\nexport function resolveCollaboratorHostPermissions(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): Record<HostPermissionKey, boolean> | null {\n if (!member || !hostId) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const merged = hostRolePermissionDefaults(role)\n const toggles = member.hostPermissions?.[hostId]\n if (toggles) {\n for (const key of Object.keys(merged)) {\n const value = toggles[key]\n if (typeof value === 'boolean') merged[key] = value\n }\n }\n // A per-site key that follows or refines another per-site key (AGL-4047)\n // takes its verdict from that key on this site where the site sets nothing\n // for it, as the org catalog's resolver does with a custom role.\n return refinePermissionMap(merged, refinementsWithin(merged), [toggles])\n}\n\n/*\n * KEYS WHOSE PRESET IS THE SITE ROLE (AGL-4047).\n *\n * Some checks were written against the member's role on a site: who may book\n * a walk-in, change a stay or edit a site's reservations asked whether their\n * `memberRoles` entry was Admin, Editor or Author. Roles are only presets of\n * permissions, so those checks became keys, and a key that replaces a gate\n * on the site role keeps that role as its preset (`presetFromSiteRole`):\n *\n * 1. the member's role ON THE SITE (`hostRoleFor`): Admin for the owner and\n * admins, a workspace-wide editor's or viewer's own role unless the site\n * gives them other access there, narrower or wider, and a collaborator's\n * site role; no access to the site holds nothing;\n * 2. that role's `hostRoleDefaults` verdict for the key;\n * 3. then, for a workspace-wide member, their custom role's verdict and\n * their own permissions' verdict, when either sets the key;\n * 4. then the site's own toggle for them (`hostPermissions[hostId][key]`).\n *\n * With nothing set at 3 or 4, the verdict is exactly what the role gate\n * answered, for every member on every site, which is how the built-in roles\n * keep the access they had. A collaborator has no custom role and no\n * workspace standing to refine, so only the site's toggle moves their\n * verdict, as for every other per-site key.\n *\n * A key declared `workspaceMembersOnly` replaces a gate that asked for an\n * admin of the whole workspace who is also this site's admin: its preset at\n * 2 is the site role's for a member of the whole workspace and refusal for a\n * collaborator, whose site role (even Admin) never carried it. The site's\n * toggle at 4 still grants it to them.\n *\n * These keys narrow nothing (`narrowsPerSite` is dropped): the site role is\n * already their preset. With no site named, the org catalog's verdict\n * (`roleDefaults`, then the layers) stands, which is the same answer for a\n * workspace-wide member on a site where their access is their workspace role.\n */\n\n/** The catalog keys whose preset is the member's role on the site, in catalog order. */\nexport function siteRolePresetPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole).map(\n (definition) => definition.key,\n )\n}\n\n/** Is this a key whose preset is the member's role on the site? */\nexport function isSiteRolePresetPermission(permission: string): boolean {\n return ORG_PERMISSIONS.some(\n (definition) => definition.key === permission && definition.presetFromSiteRole === true,\n )\n}\n\n/**\n * A member's verdict on ONE site for every key whose preset is their role\n * there (see above). `customRole` is their custom role document when they\n * carry a `roleId` and the caller has it, and is ignored for a collaborator.\n * No site named, or no access to it, refuses every such key.\n */\nexport function resolveSiteRolePresetPermissions(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): Record<string, boolean> {\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.presetFromSiteRole)\n if (!definitions.length) return {}\n const role = member && hostId ? hostRoleFor(member, hostId) : null\n if (!member || !hostId || !role) {\n return Object.fromEntries(definitions.map((definition) => [definition.key, false]))\n }\n const toggles = member.hostPermissions?.[hostId]\n const workspaceMember = isOrgWideMember(member)\n const layers: ReadonlyArray<Readonly<Partial<Record<string, unknown>>> | null | undefined> =\n workspaceMember\n ? [member.roleId ? customRole?.permissions : undefined, member.permissions, toggles]\n : [toggles]\n const out: Record<string, boolean> = {}\n for (const definition of definitions) {\n let verdict =\n (workspaceMember || !definition.workspaceMembersOnly) && definition.hostRoleDefaults?.[role] === true\n for (const layer of layers) {\n const value = layer?.[definition.key]\n if (typeof value === 'boolean') verdict = value\n }\n out[definition.key] = verdict\n }\n return refinePermissionMap(out, refinementsWithin(out), layers)\n}\n\n/**\n * `granted` with every key whose preset is the site role decided for this\n * site. With no site named it is `granted` as given, so a resolver can call\n * it unconditionally.\n */\nexport function applySiteRolePresets<T extends Record<string, boolean>>(\n granted: T,\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null,\n): T {\n if (!hostId) return granted\n return { ...granted, ...resolveSiteRolePresetPermissions(member, hostId, customRole) }\n}\n\n/**\n * The verdict the site's own stamp gives a member for one key whose preset\n * is the site role, as the Firestore rules read it (AGL-4047): the site's\n * `memberPermissions[uid][key]` when the projection wrote one, else the\n * preset of the role the site's `memberRoles` projection holds for them.\n *\n * For a door that has the host document in hand and asks what the rules\n * ask, so the two cannot disagree: the stamp is written beside\n * `memberRoles` by the same projection, with every layer applied, and a\n * site the projection has not reached since the key existed answers exactly\n * as the role gate it replaced. `presetFor` is the declaring plugin's own\n * table, so the answer does not depend on which plugins this process loaded.\n */\nexport function hostStampedSitePermission(\n host: { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n permission: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n): boolean {\n const stamped = (host?.memberPermissions as Record<string, Record<string, unknown> | undefined> | undefined)?.[uid]?.[\n permission\n ]\n if (typeof stamped === 'boolean') return stamped\n const role = (host?.memberRoles as Record<string, unknown> | undefined)?.[uid]\n return typeof role === 'string' && presetFor[role as HostAccessRole] === true\n}\n\n/**\n * The same for a key whose preset holds for members of the whole workspace\n * only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot\n * answer: an admin of one site and an admin of the whole workspace project\n * the same `'admin'` there. The site's stamp when the projection wrote one;\n * else the preset of the role `memberRoles` holds for them AND of the role\n * the fresh resolution gives them on the site, for a member of the whole\n * workspace only. Exactly the gate these keys replaced: the projection's\n * role, `orgWide` and the fresh site role, all three.\n */\nexport function hostStampedWorkspaceSitePermission(\n host: { memberPermissions?: unknown; memberRoles?: unknown } | null | undefined,\n uid: string,\n permission: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n membership: { orgWide: boolean; hostRole: string | null | undefined },\n): boolean {\n const stamped = (host?.memberPermissions as Record<string, Record<string, unknown> | undefined> | undefined)?.[uid]?.[\n permission\n ]\n if (typeof stamped === 'boolean') return stamped\n if (membership.orgWide !== true) return false\n const fresh = membership.hostRole\n return (\n typeof fresh === 'string' &&\n presetFor[fresh as HostAccessRole] === true &&\n hostStampedSitePermission({ memberRoles: host?.memberRoles }, uid, permission, presetFor)\n )\n}\n\n/*\n * A SITE NARROWS A WORKSPACE-WIDE MEMBER (AGL-4047).\n *\n * An editor who works across the workspace can be given Viewer or Author\n * access to one site (`hostAccess[hostId]`, which `hostRoleFor` reads ahead\n * of `allHosts`). That narrowed their content rights there and nothing else,\n * so a key the workspace grants an editor, such as working a site's\n * register, still held on the site they were narrowed on.\n *\n * A plugin marks the keys a site CAN narrow with `narrowsPerSite`, and\n * whether each one does is a control with three layers, the nearest set one\n * winning (`siteNarrowingChoices`):\n *\n * - the member's own choice (`member.siteNarrowing[key]`);\n * - else their custom role's (`customRole.siteNarrowing[key]`);\n * - else the built-in role's default the declaration names.\n *\n * `true` is \"Follow the site role\", `false` is \"Keep this permission\". On a\n * site that narrows the member, each key that follows the site role is\n * decided for that site:\n *\n * 1. the site's own verdict for the key (`hostPermissions[hostId][key]`),\n * when the site set one;\n * 2. else, for a key that refines another key that also follows the site\n * role, that key's verdict on this site, never more than the workspace\n * grants;\n * 3. else the narrower access's default for the key, never more than the\n * workspace grants;\n *\n * and a key they keep is the workspace's verdict, as everywhere else. A finer\n * key they hold on the site (one the site grants, or one they keep and the\n * workspace grants) brings the key it refines on this site with it, unless\n * the site refuses that key, because a refining key is never held without\n * it. The other finer keys do not follow that implied grant: allowing \"sell\"\n * on a site is not allowing refunds.\n *\n * A member whose access to every site matches their workspace role (the\n * owner and admins always do), one who keeps every key a site can narrow,\n * and a site collaborator, whose keys come from their site role already, all\n * resolve exactly as before.\n */\n\n/** Host roles from the widest down, for \"is this site's access narrower?\". */\nconst HOST_ROLE_RANK: Readonly<Record<HostAccessRole, number>> = {\n admin: 3,\n editor: 2,\n author: 1,\n viewer: 0,\n}\n\n/** How one site narrows a workspace-wide member: the access they hold there, the site's verdicts, and which keys follow it. */\nexport interface SiteNarrowing {\n /** Their role on the site, narrower than their workspace role. */\n role: HostAccessRole\n /** `hostPermissions[hostId]`: the verdicts the site set for them. */\n grants: Readonly<Partial<Record<string, unknown>>>\n /**\n * Every key a site can narrow, `true` where it follows the site role for\n * this member and `false` where they keep it (`siteNarrowingChoices`).\n */\n narrows: Readonly<Record<string, boolean>>\n}\n\n/** The catalog keys a site CAN narrow (`narrowsPerSite`), in catalog order. */\nexport function siteNarrowedPermissionKeys(): string[] {\n return ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite).map(\n (definition) => definition.key,\n )\n}\n\n/**\n * Could any site narrow a member of this org role? Not the owner or an\n * admin, who are admins of every site, and not a role whose site access has\n * nothing narrower below it (a viewer). The editors' control is the one that\n * changes anything; the role editor says so beside it.\n */\nexport function roleCanBeNarrowed(role: string | null | undefined): boolean {\n if (role === 'owner' || role === 'admin') return false\n return HOST_ROLE_RANK[orgRoleTier(role) as HostAccessRole] > 0\n}\n\n/**\n * Whether each key a site can narrow follows the site role for this member\n * (`true`) or is kept (`false`): their own choice, else their custom role's\n * (when they carry its id and it was supplied), else the built-in role's\n * default.\n */\nexport function siteNarrowingChoices(\n member: (Partial<AglynOrgMember> & { roleId?: string | null }) | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): Record<string, boolean> {\n const role = (member?.role ?? 'viewer') as OrgRole\n const own = member?.siteNarrowing ?? {}\n const fromRole = member?.roleId ? (customRole?.siteNarrowing ?? {}) : {}\n const out: Record<string, boolean> = {}\n for (const definition of ORG_PERMISSIONS) {\n if (!definition.narrowsPerSite) continue\n const key = definition.key\n const mine = own[key]\n const theirRole = fromRole[key]\n out[key] =\n typeof mine === 'boolean'\n ? mine\n : typeof theirRole === 'boolean'\n ? theirRole\n : definition.narrowsPerSite[role] === true\n }\n return out\n}\n\n/**\n * Is this member's access to this site narrower than their workspace role?\n * The cheap half of `siteNarrowingFor`, which needs no custom role: a caller\n * that has to read one asks this first, and reads nothing for everyone else.\n */\nexport function siteMayNarrow(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n): HostAccessRole | null {\n if (!member || !hostId || !isOrgWideMember(member)) return null\n const role = hostRoleFor(member, hostId)\n if (!role) return null\n const workspaceRole = orgRoleTier(member.role) as HostAccessRole\n if (HOST_ROLE_RANK[role] >= HOST_ROLE_RANK[workspaceRole]) return null\n return role\n}\n\n/**\n * How this site narrows this member, or null where it does not: a member\n * who is not workspace-wide, has no access to the site, whose access there\n * is as wide as their workspace role (owners and admins always), or who\n * keeps every key a site can narrow. `customRole` is their custom role\n * document when they carry a `roleId`; without it, only their own choices\n * and the built-in defaults decide.\n */\nexport function siteNarrowingFor(\n member: Partial<AglynOrgMember> | null | undefined,\n hostId: HostUid | null | undefined,\n customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null,\n): SiteNarrowing | null {\n const role = siteMayNarrow(member, hostId)\n if (!role) return null\n const narrows = siteNarrowingChoices(member, customRole)\n if (!Object.values(narrows).some(Boolean)) return null\n return { role, grants: member?.hostPermissions?.[hostId as HostUid] ?? {}, narrows }\n}\n\n/** The keys that follow the site role under a narrowing, in catalog order; none without one. */\nexport function narrowedPermissionKeysOf(narrowing: SiteNarrowing | null | undefined): string[] {\n if (!narrowing) return []\n return siteNarrowedPermissionKeys().filter((key) => narrowing.narrows[key] === true)\n}\n\n/** What a site access holds by default for a key: its site default, else its tier's (Author on the Viewer tier). */\nfunction narrowedDefault(definition: OrgPermissionDefinition, role: HostAccessRole): boolean {\n const site = definition.hostRoleDefaults?.[role]\n if (typeof site === 'boolean') return site\n return definition.roleDefaults[orgRoleTier(role)] === true\n}\n\n/**\n * A workspace-wide member's resolved map with the keys that follow the site\n * role decided for that site (see above). With no narrowing it is the map\n * as given, so every resolver can call it unconditionally.\n */\nexport function narrowPermissionsToSite<T extends Record<string, boolean>>(\n granted: T,\n narrowing: SiteNarrowing | null | undefined,\n): T {\n if (!narrowing) return granted\n const definitions = ORG_PERMISSIONS.filter((definition) => definition.narrowsPerSite)\n if (!definitions.length) return granted\n const narrowable = new Set(definitions.map((definition) => definition.key))\n const follows = (key: string): boolean => narrowing.narrows[key] === true\n // A finer key's broader key, among the keys a site can narrow.\n const broader = new Map<string, string>()\n for (const [key, parent, kind] of ORG_PERMISSION_REFINEMENTS) {\n if (kind !== 'follows' && narrowable.has(key) && narrowable.has(parent)) broader.set(key, parent)\n }\n const siteVerdict = (key: string): boolean | undefined => {\n const value = narrowing.grants[key]\n return typeof value === 'boolean' ? value : undefined\n }\n const depth = (key: string): number => {\n const seen = new Set<string>()\n let steps = 0\n for (let current = key; broader.has(current) && !seen.has(current); steps += 1) {\n seen.add(current)\n current = broader.get(current) as string\n }\n return steps\n }\n const out: Record<string, boolean> = { ...granted }\n for (const definition of [...definitions].sort((a, b) => depth(a.key) - depth(b.key))) {\n const key = definition.key\n const workspace = granted[key] === true\n // Kept: the workspace's verdict, as on every other site.\n if (!follows(key)) {\n out[key] = workspace\n continue\n }\n const own = siteVerdict(key)\n const parent = broader.get(key)\n if (own !== undefined) out[key] = own\n else if (parent && follows(parent)) out[key] = out[parent] === true && workspace\n else out[key] = workspace && narrowedDefault(definition, narrowing.role)\n }\n for (const definition of definitions) {\n const key = definition.key\n const held = follows(key) ? siteVerdict(key) === true : granted[key] === true\n if (!held) continue\n const seen = new Set<string>()\n for (let parent = broader.get(key); parent && !seen.has(parent); parent = broader.get(parent)) {\n seen.add(parent)\n // A kept broader key is the workspace's verdict already.\n if (!follows(parent) || siteVerdict(parent) === false) break\n out[parent] = true\n }\n }\n return refinePermissionMap(out, ORG_PERMISSION_REFINEMENTS) as T\n}\n\n/**\n * A member's verdict for every per-site key, on whichever axis their\n * membership is on.\n *\n * `customRole` is the member's custom role document when they carry a\n * `roleId` and the caller has it in hand, and is ignored for a collaborator,\n * whose org role is a seat-accounting fact rather than a grant. `hostId` is\n * the site the request is about; an org-wide member resolves the same on\n * every site and with none named, except on a site that narrows them.\n *\n * An org-wide member's map also carries every key a site can narrow\n * (AGL-4047), decided for `hostId`, so the site's `memberPermissions`\n * projection answers the rules what the server answers. A collaborator's\n * map does not: their register and waivers keys come from their site role\n * and their own overrides, which the rules read from their member document\n * as before.\n */\nexport function resolveMemberHostPermissions(options: {\n member: Partial<AglynOrgMember> | null | undefined\n customRole?: AglynOrgCustomRole | null\n hostId?: HostUid | null\n}): Record<HostPermissionKey, boolean> {\n const { member, customRole, hostId } = options\n if (!member) return noHostPermissions()\n if (isOrgWideMember(member)) {\n // The keys whose preset is the site role (AGL-4047) are decided for this\n // site last; they narrow nothing, so the narrowing pass leaves them be.\n const granted = applySiteRolePresets(\n narrowPermissionsToSite(\n resolveOrgPermissions(member, customRole ?? null),\n siteNarrowingFor(member, hostId, customRole ?? null),\n ),\n member,\n hostId,\n customRole ?? null,\n )\n return Object.fromEntries(\n [...hostPermissionKeys(), ...siteNarrowedPermissionKeys()].map((key) => [key, granted[key] === true]),\n )\n }\n return resolveCollaboratorHostPermissions(member, hostId) ?? noHostPermissions()\n}\n\n/**\n * The `memberPermissions` projection stamped on a host document beside\n * `memberRoles`: every member who can reach the site, with the verdict for\n * every per-site key they hold ON it. Org-wide members are resolved against\n * the custom roles the caller loaded (one read per distinct role, as\n * `syncOrgAuthProjections` already does for `resolvedPermissions`);\n * collaborators against their host role. A member who cannot reach the site\n * has no entry, matching `projectHostMemberRoles`.\n */\nexport function projectHostMemberPermissions(\n members: ReadonlyArray<Partial<AglynOrgMember> & { $id: string }>,\n hostId: HostUid,\n customRoles: ReadonlyMap<string, AglynOrgCustomRole | null>,\n): Record<string, Record<HostPermissionKey, boolean>> {\n const projection: Record<string, Record<HostPermissionKey, boolean>> = {}\n for (const member of members) {\n if (!hostRoleFor(member, hostId)) continue\n projection[member.$id] = resolveMemberHostPermissions({\n member,\n customRole: member.roleId ? (customRoles.get(member.roleId) ?? null) : null,\n hostId,\n })\n }\n return projection\n}\n\n/** The core's site keys (AGL-4047): `CORE_SITE_ROLE_PRESETS`. */\nexport type CoreSiteKey = 'site.edit' | 'site.publish' | 'site.sensitive' | 'site.manage'\n\n/** A host document, as a snapshot (`get`) or as its data. */\nexport type HostDocumentLike =\n | { get(field: string): unknown }\n | { memberPermissions?: unknown; memberRoles?: unknown }\n | null\n | undefined\n\n/**\n * Does this member hold one of the core's site keys on this site (AGL-4047)?\n * The door's twin of the rules' `canWriteHostContent`,\n * `canPublishHostContent`, `canReadHostSensitive` and site-settings checks:\n * the site's `memberPermissions` stamp, else the preset of the member's\n * `memberRoles` entry (`hostStampedSitePermission`), so a route that asks it\n * answers what the rules answer for the same write. For the routes that read\n * the host document already; it costs no read.\n */\nexport function holdsSiteKey(hostDoc: HostDocumentLike, uid: string, key: CoreSiteKey): boolean {\n return holdsStampedSiteKey(hostDoc, uid, key, CORE_SITE_ROLE_PRESETS[key] ?? {})\n}\n\n/**\n * The same for a plugin's key whose preset is the site role (AGL-4047): the\n * site's `memberPermissions` stamp, else `presetFor` read for the member's\n * `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the\n * answer does not depend on which plugins this process loaded. For the doors\n * and the cards that hold the host document already; it costs no read.\n */\nexport function holdsStampedSiteKey(\n hostDoc: HostDocumentLike,\n uid: string | null | undefined,\n key: string,\n presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>,\n): boolean {\n if (!hostDoc || !uid) return false\n // `hostDoc`, not `host`: the host-field deny guard reads every `host.…`\n // in this folder as a field of the host document, and `get` is none.\n const read = (field: string): unknown =>\n typeof (hostDoc as { get?: unknown }).get === 'function'\n ? (hostDoc as { get(field: string): unknown }).get(field)\n : (hostDoc as Record<string, unknown>)[field]\n return hostStampedSitePermission(\n { memberPermissions: read('memberPermissions'), memberRoles: read('memberRoles') },\n uid,\n key,\n presetFor,\n )\n}\n\n/**\n * The refusal a door answers when a member does not hold a site key\n * (AGL-4047): it names the permission and who can change it, never a role.\n */\nexport function siteKeyRefusal(key: string): string {\n return permissionRefusalText(key)\n}\n\n"],"names":["refinePermissionMap","hostRoleFor","isOrgWideMember","CORE_SITE_ROLE_PRESETS","permissionRefusalText","ORG_PERMISSION_REFINEMENTS","ORG_PERMISSIONS","resolveOrgPermissions","orgRoleTier","hostPermissionKeys","filter","definition","hostRoleDefaults","undefined","map","key","hostRolePermissionDefaults","role","defaults","workspaceMembersOnly","noHostPermissions","Object","fromEntries","refinementsWithin","parent","resolveCollaboratorHostPermissions","member","hostId","merged","toggles","hostPermissions","keys","value","siteRolePresetPermissionKeys","presetFromSiteRole","isSiteRolePresetPermission","permission","some","resolveSiteRolePresetPermissions","customRole","definitions","length","workspaceMember","layers","roleId","permissions","out","verdict","layer","applySiteRolePresets","granted","hostStampedSitePermission","host","uid","presetFor","stamped","memberPermissions","memberRoles","hostStampedWorkspaceSitePermission","membership","orgWide","fresh","hostRole","HOST_ROLE_RANK","admin","editor","author","viewer","siteNarrowedPermissionKeys","narrowsPerSite","roleCanBeNarrowed","siteNarrowingChoices","own","siteNarrowing","fromRole","mine","theirRole","siteMayNarrow","workspaceRole","siteNarrowingFor","narrows","values","Boolean","grants","narrowedPermissionKeysOf","narrowing","narrowedDefault","site","roleDefaults","narrowPermissionsToSite","narrowable","Set","follows","broader","Map","kind","has","set","siteVerdict","depth","seen","steps","current","add","get","sort","a","b","workspace","held","resolveMemberHostPermissions","options","projectHostMemberPermissions","members","customRoles","projection","$id","holdsSiteKey","hostDoc","holdsStampedSiteKey","read","field","siteKeyRefusal"],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
export interface HostStatus {
|
|
18
|
-
/** `Live`, `Draft`, `Maintenance`, `Suspended`. */
|
|
18
|
+
/** `Live`, `Draft`, `In preview`, `Maintenance`, `Suspended`. */
|
|
19
19
|
label: string;
|
|
20
20
|
color: 'success' | 'default' | 'warning' | 'error';
|
|
21
21
|
/** Why, in one sentence — the pill's tooltip. */
|
|
@@ -65,6 +65,7 @@ export declare function publishedScreenCount(host: {
|
|
|
65
65
|
export declare function describeHostStatus(host: {
|
|
66
66
|
screens?: Record<string, unknown>;
|
|
67
67
|
maintenance?: boolean;
|
|
68
|
+
preview?: boolean;
|
|
68
69
|
suspendedAt?: number;
|
|
69
70
|
suspendedUntilMs?: number;
|
|
70
71
|
}): HostStatus;
|
|
@@ -73,6 +73,16 @@
|
|
|
73
73
|
detail: 'Every path serves the maintenance page.'
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
// A site in preview (AGL-4086) is not live to its visitors whatever it has
|
|
77
|
+
// published: they get the 503 page or "Coming soon", and only the team
|
|
78
|
+
// sees the pages.
|
|
79
|
+
if ((host == null ? void 0 : host.preview) === true) {
|
|
80
|
+
return {
|
|
81
|
+
label: 'In preview',
|
|
82
|
+
color: 'default',
|
|
83
|
+
detail: 'Only your team can see this site. Visitors see the 503 page or "Coming soon".'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
76
86
|
const published = publishedScreenCount(host);
|
|
77
87
|
if (published > 0) {
|
|
78
88
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-status.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\nexport interface HostStatus {\n /** `Live`, `Draft`, `Maintenance`, `Suspended`. */\n label: string\n color: 'success' | 'default' | 'warning' | 'error'\n /** Why, in one sentence — the pill's tooltip. */\n detail: string\n}\n\n/**\n * How many pages this site publishes.\n *\n * Read off `host.screens` — the map publishing writes into the host document\n * (`hosts/{hostId}.screens[screenId] = slug`, see `template-screens.ts`) and\n * the tenant reads to route a request. So a site with an entry has at least\n * one page a visitor can reach, and one without has none. That makes this a\n * fact already sitting in a document its readers hold: **no extra read**, on\n * a page that can list a hundred sites.\n *\n * ⚠️ It counts PUBLISHED ROUTES, not authored work. A site with five\n * unpublished drafts counts 0 — the map is written by publishing and by\n * nothing else. Every caller is asking \"what can a visitor reach?\"; a caller\n * that means \"has anyone worked on this?\" is asking a different question and\n * this is the wrong number for it.\n *\n * The placeholder home page a new site is created with (AGL-3408) COUNTS\n * here: it is a page a visitor reaches, so a new site is Live. The first-run\n * offer asks a different question — has the OWNER published anything? — and\n * reads `hostIsBlankSite`, which discounts it.\n */\nexport function publishedScreenCount(\n host: { screens?: Record<string, unknown> } | null | undefined,\n): number {\n return Object.keys(host?.screens ?? {}).length\n}\n\n/**\n * The `Live` / `Draft` pill the console Sites mockup puts on every card\n * (AGL-2166).\n *\n * Derived from {@link publishedScreenCount}: the pill reports what a visitor\n * can reach, placeholder home page included.\n *\n * The staff admin host page has rendered a `published` / `draft` chip since\n * AGL-390 off `host.published`, which is not a field on `AglynHost` and is\n * written by nothing — so that chip has always said `draft`. Deriving from\n * `screens` is what it should have done.\n *\n * Order matters. A suspended site is not \"live\" whatever it has published,\n * and a site in maintenance is serving the 503 screen rather than its\n * pages — reporting either as `Live` would be the console agreeing with a\n * customer who thinks their site is up.\n */\nexport function describeHostStatus(host: {\n screens?: Record<string, unknown>\n maintenance?: boolean\n suspendedAt?: number\n suspendedUntilMs?: number\n}): HostStatus {\n if (host?.suspendedAt) {\n // A timed suspension that has elapsed is over, even though the fields\n // are still on the document — the tenant makes the same check.\n const until = host.suspendedUntilMs\n if (!until || until > Date.now()) {\n return {\n label: 'Suspended',\n color: 'error',\n detail: 'This site is serving a lockdown notice instead of content.',\n }\n }\n }\n if (host?.maintenance) {\n return {\n label: 'Maintenance',\n color: 'warning',\n detail: 'Every path serves the maintenance page.',\n }\n }\n const published = publishedScreenCount(host)\n if (published > 0) {\n return {\n label: 'Live',\n color: 'success',\n detail: `${published} published page${published === 1 ? '' : 's'}.`,\n }\n }\n return {\n label: 'Draft',\n color: 'default',\n detail: 'Nothing published yet — visitors see the placeholder.',\n }\n}\n\n/**\n * `6 of 10 sites · Business plan` — the meta line the Sites mockup puts\n * opposite the section heading.\n *\n * Returns `undefined` rather than a partial line while the org or its plan\n * is still resolving. A count against the FREE limit is what an unresolved\n * org produces, and telling a Business customer they are at \"1 of 1 sites\"\n * is how a correct page delivers a false upgrade prompt.\n */\nexport function describeSiteAllowance(options: {\n used: number\n limit: number | undefined\n planLabel: string | undefined\n ready: boolean\n}): string | undefined {\n const { used, limit, planLabel, ready } = options\n if (!ready || !planLabel) return undefined\n /*\n * `Number.isFinite`, not `limit < 0` (AGL-2223).\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, and Enterprise's `hostLimit`\n * is exactly that — so the `< 0` test was never true for the one plan the\n * Unlimited branch existed for, and an Enterprise organization read\n * `4 of Infinity sites · Enterprise plan`. The negative case is kept: an\n * entitlement override can carry a negative sentinel, and reading that as a\n * cap of \"-1 sites\" would be the same class of nonsense.\n */\n const cap =\n limit === undefined || limit === null\n ? undefined\n : !Number.isFinite(limit) || limit < 0\n ? 'Unlimited'\n : String(limit)\n const sites =\n cap === undefined\n ? `${used} site${used === 1 ? '' : 's'}`\n : `${used} of ${cap} site${cap === '1' ? '' : 's'}`\n return `${sites} · ${planLabel} plan`\n}\n"],"names":["publishedScreenCount","host","Object","keys","screens","length","describeHostStatus","suspendedAt","until","suspendedUntilMs","Date","now","label","color","detail","maintenance","published","describeSiteAllowance","options","used","limit","planLabel","ready","undefined","cap","Number","isFinite","String","sites"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAUD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASA,qBACdC,IAA8D;;IAE9D,OAAOC,OAAOC,IAAI,SAACF,wBAAAA,KAAMG,OAAO,mBAAI,CAAC,GAAGC,MAAM;AAChD;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,mBAAmBL,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-status.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\nexport interface HostStatus {\n /** `Live`, `Draft`, `In preview`, `Maintenance`, `Suspended`. */\n label: string\n color: 'success' | 'default' | 'warning' | 'error'\n /** Why, in one sentence — the pill's tooltip. */\n detail: string\n}\n\n/**\n * How many pages this site publishes.\n *\n * Read off `host.screens` — the map publishing writes into the host document\n * (`hosts/{hostId}.screens[screenId] = slug`, see `template-screens.ts`) and\n * the tenant reads to route a request. So a site with an entry has at least\n * one page a visitor can reach, and one without has none. That makes this a\n * fact already sitting in a document its readers hold: **no extra read**, on\n * a page that can list a hundred sites.\n *\n * ⚠️ It counts PUBLISHED ROUTES, not authored work. A site with five\n * unpublished drafts counts 0 — the map is written by publishing and by\n * nothing else. Every caller is asking \"what can a visitor reach?\"; a caller\n * that means \"has anyone worked on this?\" is asking a different question and\n * this is the wrong number for it.\n *\n * The placeholder home page a new site is created with (AGL-3408) COUNTS\n * here: it is a page a visitor reaches, so a new site is Live. The first-run\n * offer asks a different question — has the OWNER published anything? — and\n * reads `hostIsBlankSite`, which discounts it.\n */\nexport function publishedScreenCount(\n host: { screens?: Record<string, unknown> } | null | undefined,\n): number {\n return Object.keys(host?.screens ?? {}).length\n}\n\n/**\n * The `Live` / `Draft` pill the console Sites mockup puts on every card\n * (AGL-2166).\n *\n * Derived from {@link publishedScreenCount}: the pill reports what a visitor\n * can reach, placeholder home page included.\n *\n * The staff admin host page has rendered a `published` / `draft` chip since\n * AGL-390 off `host.published`, which is not a field on `AglynHost` and is\n * written by nothing — so that chip has always said `draft`. Deriving from\n * `screens` is what it should have done.\n *\n * Order matters. A suspended site is not \"live\" whatever it has published,\n * and a site in maintenance is serving the 503 screen rather than its\n * pages — reporting either as `Live` would be the console agreeing with a\n * customer who thinks their site is up.\n */\nexport function describeHostStatus(host: {\n screens?: Record<string, unknown>\n maintenance?: boolean\n preview?: boolean\n suspendedAt?: number\n suspendedUntilMs?: number\n}): HostStatus {\n if (host?.suspendedAt) {\n // A timed suspension that has elapsed is over, even though the fields\n // are still on the document — the tenant makes the same check.\n const until = host.suspendedUntilMs\n if (!until || until > Date.now()) {\n return {\n label: 'Suspended',\n color: 'error',\n detail: 'This site is serving a lockdown notice instead of content.',\n }\n }\n }\n if (host?.maintenance) {\n return {\n label: 'Maintenance',\n color: 'warning',\n detail: 'Every path serves the maintenance page.',\n }\n }\n // A site in preview (AGL-4086) is not live to its visitors whatever it has\n // published: they get the 503 page or \"Coming soon\", and only the team\n // sees the pages.\n if (host?.preview === true) {\n return {\n label: 'In preview',\n color: 'default',\n detail:\n 'Only your team can see this site. Visitors see the 503 page or \"Coming soon\".',\n }\n }\n const published = publishedScreenCount(host)\n if (published > 0) {\n return {\n label: 'Live',\n color: 'success',\n detail: `${published} published page${published === 1 ? '' : 's'}.`,\n }\n }\n return {\n label: 'Draft',\n color: 'default',\n detail: 'Nothing published yet — visitors see the placeholder.',\n }\n}\n\n/**\n * `6 of 10 sites · Business plan` — the meta line the Sites mockup puts\n * opposite the section heading.\n *\n * Returns `undefined` rather than a partial line while the org or its plan\n * is still resolving. A count against the FREE limit is what an unresolved\n * org produces, and telling a Business customer they are at \"1 of 1 sites\"\n * is how a correct page delivers a false upgrade prompt.\n */\nexport function describeSiteAllowance(options: {\n used: number\n limit: number | undefined\n planLabel: string | undefined\n ready: boolean\n}): string | undefined {\n const { used, limit, planLabel, ready } = options\n if (!ready || !planLabel) return undefined\n /*\n * `Number.isFinite`, not `limit < 0` (AGL-2223).\n *\n * `UNLIMITED` is `Number.POSITIVE_INFINITY`, and Enterprise's `hostLimit`\n * is exactly that — so the `< 0` test was never true for the one plan the\n * Unlimited branch existed for, and an Enterprise organization read\n * `4 of Infinity sites · Enterprise plan`. The negative case is kept: an\n * entitlement override can carry a negative sentinel, and reading that as a\n * cap of \"-1 sites\" would be the same class of nonsense.\n */\n const cap =\n limit === undefined || limit === null\n ? undefined\n : !Number.isFinite(limit) || limit < 0\n ? 'Unlimited'\n : String(limit)\n const sites =\n cap === undefined\n ? `${used} site${used === 1 ? '' : 's'}`\n : `${used} of ${cap} site${cap === '1' ? '' : 's'}`\n return `${sites} · ${planLabel} plan`\n}\n"],"names":["publishedScreenCount","host","Object","keys","screens","length","describeHostStatus","suspendedAt","until","suspendedUntilMs","Date","now","label","color","detail","maintenance","preview","published","describeSiteAllowance","options","used","limit","planLabel","ready","undefined","cap","Number","isFinite","String","sites"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAUD;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASA,qBACdC,IAA8D;;IAE9D,OAAOC,OAAOC,IAAI,SAACF,wBAAAA,KAAMG,OAAO,mBAAI,CAAC,GAAGC,MAAM;AAChD;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC,mBAAmBL,IAMlC;IACC,IAAIA,wBAAAA,KAAMM,WAAW,EAAE;QACrB,sEAAsE;QACtE,+DAA+D;QAC/D,MAAMC,QAAQP,KAAKQ,gBAAgB;QACnC,IAAI,CAACD,SAASA,QAAQE,KAAKC,GAAG,IAAI;YAChC,OAAO;gBACLC,OAAO;gBACPC,OAAO;gBACPC,QAAQ;YACV;QACF;IACF;IACA,IAAIb,wBAAAA,KAAMc,WAAW,EAAE;QACrB,OAAO;YACLH,OAAO;YACPC,OAAO;YACPC,QAAQ;QACV;IACF;IACA,2EAA2E;IAC3E,uEAAuE;IACvE,kBAAkB;IAClB,IAAIb,CAAAA,wBAAAA,KAAMe,OAAO,MAAK,MAAM;QAC1B,OAAO;YACLJ,OAAO;YACPC,OAAO;YACPC,QACE;QACJ;IACF;IACA,MAAMG,YAAYjB,qBAAqBC;IACvC,IAAIgB,YAAY,GAAG;QACjB,OAAO;YACLL,OAAO;YACPC,OAAO;YACPC,QAAQ,GAAGG,UAAU,eAAe,EAAEA,cAAc,IAAI,KAAK,IAAI,CAAC,CAAC;QACrE;IACF;IACA,OAAO;QACLL,OAAO;QACPC,OAAO;QACPC,QAAQ;IACV;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,sBAAsBC,OAKrC;IACC,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEC,SAAS,EAAEC,KAAK,EAAE,GAAGJ;IAC1C,IAAI,CAACI,SAAS,CAACD,WAAW,OAAOE;IACjC;;;;;;;;;GASC,GACD,MAAMC,MACJJ,UAAUG,aAAaH,UAAU,OAC7BG,YACA,CAACE,OAAOC,QAAQ,CAACN,UAAUA,QAAQ,IACjC,cACAO,OAAOP;IACf,MAAMQ,QACJJ,QAAQD,YACJ,GAAGJ,KAAK,KAAK,EAAEA,SAAS,IAAI,KAAK,KAAK,GACtC,GAAGA,KAAK,IAAI,EAAEK,IAAI,KAAK,EAAEA,QAAQ,MAAM,KAAK,KAAK;IACvD,OAAO,GAAGI,MAAM,GAAG,EAAEP,UAAU,KAAK,CAAC;AACvC"}
|
|
@@ -94,6 +94,14 @@ export interface OrgPermissionDefinition {
|
|
|
94
94
|
* (`CORE_SITE_ROLE_PRESETS`) and a plugin's key with `hostRoleDefaults`.
|
|
95
95
|
*/
|
|
96
96
|
presetFromSiteRole?: true;
|
|
97
|
+
/**
|
|
98
|
+
* With `presetFromSiteRole`: the site role is the preset only for a member
|
|
99
|
+
* of the whole workspace (AGL-4047). A site collaborator's preset is
|
|
100
|
+
* refusal, whatever their site role, and only the site's toggle grants it
|
|
101
|
+
* to them. A plugin's key that replaces a gate asking for an admin of the
|
|
102
|
+
* whole workspace who is also the site's admin.
|
|
103
|
+
*/
|
|
104
|
+
workspaceMembersOnly?: true;
|
|
97
105
|
}
|
|
98
106
|
/**
|
|
99
107
|
* Every permission, in display order for role editors: the core keys, then
|