@aglyn/aglyn 1.0.0-beta.268 → 1.0.0-beta.270
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/account-preferences.d.ts +248 -0
- package/src/lib/app-utils/account-preferences.js +434 -0
- package/src/lib/app-utils/account-preferences.js.map +1 -0
- package/src/lib/app-utils/country-options.d.ts +27 -0
- package/src/lib/app-utils/country-options.js +96 -0
- package/src/lib/app-utils/country-options.js.map +1 -0
- package/src/lib/app-utils/docs-help.generated.d.ts +6 -0
- package/src/lib/app-utils/docs-help.generated.js +13 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +17 -10
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/element-motion.d.ts +14 -2
- package/src/lib/app-utils/element-motion.js +33 -13
- package/src/lib/app-utils/element-motion.js.map +1 -1
- package/src/lib/app-utils/mobile-push.d.ts +8 -0
- package/src/lib/app-utils/mobile-push.js +4 -1
- package/src/lib/app-utils/mobile-push.js.map +1 -1
- package/src/lib/app-utils/native-app-association.d.ts +3 -2
- package/src/lib/app-utils/native-app-association.js +4 -2
- package/src/lib/app-utils/native-app-association.js.map +1 -1
- package/src/lib/app-utils/native-nav.d.ts +265 -0
- package/src/lib/app-utils/native-nav.js +741 -0
- package/src/lib/app-utils/native-nav.js.map +1 -0
- package/src/lib/app-utils/native-themes.d.ts +249 -0
- package/src/lib/app-utils/native-themes.js +732 -0
- package/src/lib/app-utils/native-themes.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +1 -1
- package/src/lib/app-utils/notifications.js +10 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/one-box-search.d.ts +108 -0
- package/src/lib/app-utils/one-box-search.js +167 -0
- package/src/lib/app-utils/one-box-search.js.map +1 -0
- package/src/lib/app-utils/org-permissions.js +10 -61
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +95 -0
- package/src/lib/app-utils/org-role-grants.js +193 -0
- package/src/lib/app-utils/org-role-grants.js.map +1 -0
- package/src/lib/app-utils/plan-entitlements.d.ts +41 -1
- package/src/lib/app-utils/plan-entitlements.js +65 -3
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
- package/src/lib/app-utils/tenant-dns-targets.js +80 -0
- package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
- package/src/lib/foundation/definitions/org-billing.types.d.ts +14 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +38 -0
- package/src/lib/plugin-manager/feature-plugins.js +20 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/operator-alerts.js +10 -1
- package/src/lib/plugin-manager/operator-alerts.js.map +1 -1
- package/src/lib/plugin-manager/plugin-config.js +10 -1
- package/src/lib/plugin-manager/plugin-config.js.map +1 -1
- package/src/lib/plugin-manager/plugin-consent-group-change.js +11 -2
- package/src/lib/plugin-manager/plugin-consent-group-change.js.map +1 -1
- package/src/lib/plugin-manager/plugin-console-crons.js +10 -1
- package/src/lib/plugin-manager/plugin-console-crons.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
- package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.js +11 -2
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-lead-conversion.js +10 -1
- package/src/lib/plugin-manager/plugin-lead-conversion.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js +10 -1
- package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-matches.js +10 -1
- package/src/lib/plugin-manager/plugin-person-matches.js.map +1 -1
- package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
- package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
- package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
- package/src/lib/plugin-manager/plugin-services.js +6 -0
- package/src/lib/plugin-manager/plugin-services.js.map +1 -1
- package/src/lib/plugin-manager/plugin-usage-meters.js +10 -1
- package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -1
- package/src/lib/plugin-manager/usage-alert-contributors.js +10 -1
- package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
|
@@ -44,10 +44,12 @@ import { type AnimationEase, type AnimationPreset } from './element-animation';
|
|
|
44
44
|
* sides when the character says {@link ElementMotionCharacter.sides}:
|
|
45
45
|
* the left column from the left, the right from the right, the second a
|
|
46
46
|
* stagger step after the first. A picture whose preset zooms keeps its
|
|
47
|
-
* zoom.
|
|
47
|
+
* zoom. Without sides its columns still arrive one after the other, the
|
|
48
|
+
* picture's in the media preset and the words' in the section's.
|
|
48
49
|
* - A group of like items — a grid of cards, a list, the questions of an
|
|
49
50
|
* FAQ, a dataset repeated over one card — STAGGERS its items, one after
|
|
50
|
-
* another, rather than arriving as one block
|
|
51
|
+
* another, rather than arriving as one block, in the character's {@link
|
|
52
|
+
* ElementMotionCharacter.groupPreset} where it names one.
|
|
51
53
|
* - A picture, and a group whose items each lead with one, takes the
|
|
52
54
|
* character's media preset.
|
|
53
55
|
* - Never touched: forms and their fields, navigation and the site's frame,
|
|
@@ -57,6 +59,10 @@ import { type AnimationEase, type AnimationPreset } from './element-animation';
|
|
|
57
59
|
* `none` included, is never overwritten. A block that HOLDS one of those
|
|
58
60
|
* is not animated as a whole either; its other children are animated one
|
|
59
61
|
* by one instead, so a form never slides in with its section.
|
|
62
|
+
* - Except a picture's FRAME: a box in the flow whose picture is laid over
|
|
63
|
+
* it (`position: absolute`), as the AI's layout compiler draws every
|
|
64
|
+
* picture slot. The frame is the picture, so it moves as one, picture and
|
|
65
|
+
* all, and counts as a picture for the media preset and for a split.
|
|
60
66
|
* - Never nested: once a block animates, nothing inside it does. Two
|
|
61
67
|
* entrances on the same pixels multiply their fades and add their
|
|
62
68
|
* offsets, which reads as a stutter, not as motion.
|
|
@@ -87,6 +93,12 @@ export interface ElementMotionCharacter {
|
|
|
87
93
|
sectionPresets?: readonly AnimationPreset[];
|
|
88
94
|
/** Whether a split's two columns come in from their own sides. */
|
|
89
95
|
sides?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* What a group of like items — a grid of cards, a list, an FAQ — arrives
|
|
98
|
+
* with, whatever its section's turn; the section's own preset when absent.
|
|
99
|
+
* A group whose every item leads with a picture still takes {@link mediaPreset}.
|
|
100
|
+
*/
|
|
101
|
+
groupPreset?: AnimationPreset;
|
|
90
102
|
}
|
|
91
103
|
/** The motion a page gets when nothing says otherwise: a soft rise, as the attributes panel defaults to. */
|
|
92
104
|
export declare const ELEMENT_MOTION_DEFAULT: ElementMotionCharacter;
|
|
@@ -135,25 +135,45 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
135
135
|
};
|
|
136
136
|
const patches = new Map();
|
|
137
137
|
/** The first section's Largest Contentful Paint candidates, painted where they stand. */ const still = new Set();
|
|
138
|
-
/** Left alone
|
|
138
|
+
/** Left alone for what it is, wherever it stands. */ const refused = (id)=>{
|
|
139
139
|
var _at_componentId;
|
|
140
|
-
var _at_sx;
|
|
141
140
|
const at = node(id);
|
|
142
141
|
if (!at) return true;
|
|
143
142
|
if (at.hidden || still.has(id)) return true;
|
|
144
143
|
if (ELEMENT_MOTION_NEVER.has(String((_at_componentId = at.componentId) != null ? _at_componentId : ''))) return true;
|
|
145
|
-
|
|
144
|
+
return !!at.props && NODE_ANIMATION_PROPS.some((prop)=>{
|
|
146
145
|
var _at_props;
|
|
147
146
|
return ((_at_props = at.props) == null ? void 0 : _at_props[prop]) !== undefined;
|
|
148
|
-
})
|
|
149
|
-
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
/** Left alone, and so is everything inside it. */ const closed = (id)=>{
|
|
150
|
+
var _node_sx, _node;
|
|
151
|
+
return refused(id) || pins((_node = node(id)) == null ? void 0 : (_node_sx = _node.sx) == null ? void 0 : _node_sx['position']);
|
|
152
|
+
};
|
|
153
|
+
/** A picture laid over its box, free to move with it. */ const overlay = (id)=>{
|
|
154
|
+
var _ref;
|
|
155
|
+
var _node, _node_sx, _node1;
|
|
156
|
+
return MEDIA.has(String((_ref = (_node = node(id)) == null ? void 0 : _node.componentId) != null ? _ref : '')) && pins((_node1 = node(id)) == null ? void 0 : (_node_sx = _node1.sx) == null ? void 0 : _node_sx['position']) && !refused(id);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* A picture's frame: a box in the flow whose picture is laid over it
|
|
160
|
+
* (`position: absolute`, filling it), beside at most the placeholder icon
|
|
161
|
+
* an empty slot shows. The frame IS the picture, so it moves as one,
|
|
162
|
+
* picture and all; its picture being out of the flow does not hold it still
|
|
163
|
+
* the way a cover's backdrop holds its band, which is itself out of the flow.
|
|
164
|
+
*/ const isFrame = (id)=>{
|
|
165
|
+
const kids = children(id);
|
|
166
|
+
return !closed(id) && kids.some(overlay) && kids.every((child)=>{
|
|
167
|
+
var _node;
|
|
168
|
+
return overlay(child) || ((_node = node(child)) == null ? void 0 : _node.componentId) === 'icon' && !refused(child) && !children(child).length;
|
|
169
|
+
});
|
|
150
170
|
};
|
|
151
171
|
const holdsClosed = new Map();
|
|
152
172
|
/** The element, or one inside it, is left alone: it cannot move as one block. */ const holds = (id)=>{
|
|
153
173
|
const known = holdsClosed.get(id);
|
|
154
174
|
if (known !== undefined) return known;
|
|
155
175
|
holdsClosed.set(id, false);
|
|
156
|
-
const result = closed(id) || children(id).some(holds);
|
|
176
|
+
const result = closed(id) || !isFrame(id) && children(id).some(holds);
|
|
157
177
|
holdsClosed.set(id, result);
|
|
158
178
|
return result;
|
|
159
179
|
};
|
|
@@ -195,12 +215,12 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
195
215
|
belowMemo.set(id, result);
|
|
196
216
|
return result;
|
|
197
217
|
};
|
|
198
|
-
/** Leads with a picture: its first leaf, down its first children, is one. */ const leadsWithMedia = (id)=>{
|
|
218
|
+
/** Leads with a picture: its first leaf, down its first children, is one, or the frame of one. */ const leadsWithMedia = (id)=>{
|
|
199
219
|
let at = id;
|
|
200
220
|
for(let depth = 0; depth < 12; depth += 1){
|
|
201
221
|
var _ref;
|
|
202
222
|
var _node;
|
|
203
|
-
if (MEDIA.has(String((_ref = (_node = node(at)) == null ? void 0 : _node.componentId) != null ? _ref : ''))) return true;
|
|
223
|
+
if (MEDIA.has(String((_ref = (_node = node(at)) == null ? void 0 : _node.componentId) != null ? _ref : '')) || isFrame(at)) return true;
|
|
204
224
|
const kids = children(at);
|
|
205
225
|
if (!kids.length) return false;
|
|
206
226
|
at = kids[0];
|
|
@@ -213,7 +233,7 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
213
233
|
};
|
|
214
234
|
const animate = (id, trigger, delayMs, words = character.preset, side)=>{
|
|
215
235
|
const group = isGroup(id);
|
|
216
|
-
const preset = presetOf(id, group, words);
|
|
236
|
+
const preset = presetOf(id, group, group && character.groupPreset ? character.groupPreset : words);
|
|
217
237
|
patches.set(id, _extends({
|
|
218
238
|
[NODE_ANIMATION_PROP]: side && !preset.startsWith('zoom') ? side : preset,
|
|
219
239
|
[NODE_ANIMATION_TRIGGER_PROP]: trigger,
|
|
@@ -246,10 +266,10 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
246
266
|
};
|
|
247
267
|
/** A later section's content: on scroll, groups staggered, splits from their sides, nothing nested. */ const place = (id, words)=>{
|
|
248
268
|
if (closed(id)) return;
|
|
249
|
-
if (
|
|
269
|
+
if (isSplit(id)) {
|
|
250
270
|
const [start, end] = children(id);
|
|
251
|
-
animate(start, 'scroll', 0, words, 'slide-right');
|
|
252
|
-
animate(end, 'scroll', character.staggerStepMs, words, 'slide-left');
|
|
271
|
+
animate(start, 'scroll', 0, words, character.sides ? 'slide-right' : undefined);
|
|
272
|
+
animate(end, 'scroll', character.staggerStepMs, words, character.sides ? 'slide-left' : undefined);
|
|
253
273
|
return;
|
|
254
274
|
}
|
|
255
275
|
if (holds(id) || !isGroup(id) && holdsGroup(id)) {
|
|
@@ -349,7 +369,7 @@ const clamp = (value, max)=>Math.min(Math.max(Math.round(value), 0), max);
|
|
|
349
369
|
if (content === id && !children(id).length) return;
|
|
350
370
|
const words = (_turns_ = turns[(index - 1) % turns.length]) != null ? _turns_ : character.preset;
|
|
351
371
|
if (index === 0) ladder(content);
|
|
352
|
-
else if (content === id && !isGroup(id) && !
|
|
372
|
+
else if (content === id && !isGroup(id) && !isSplit(id)) for (const child of children(id))place(child, words);
|
|
353
373
|
else place(content, words);
|
|
354
374
|
});
|
|
355
375
|
if (!patches.size) return nodes;
|
|
@@ -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.\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.\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 * - 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\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, and so is everything inside it. */\n const closed = (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 if (at.props && NODE_ANIMATION_PROPS.some((prop) => at.props?.[prop] !== undefined)) return true\n return pins(at.sx?.['position'])\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) || 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. */\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 ?? ''))) 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, 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 (character.sides && isSplit(id)) {\n const [start, end] = children(id)\n animate(start, 'scroll', 0, words, 'slide-right')\n animate(end, 'scroll', character.staggerStepMs, words, 'slide-left')\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) && !(character.sides && 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","closed","at","hidden","has","String","componentId","props","prop","undefined","sx","holdsClosed","holds","known","get","set","result","groupMemo","belowMemo","isGroup","kids","length","first","alike","every","holdsGroup","leadsWithMedia","depth","presetOf","group","words","media","animate","trigger","delayMs","side","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 '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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"}
|
|
@@ -68,12 +68,20 @@ export interface MobilePushData {
|
|
|
68
68
|
link?: string;
|
|
69
69
|
orgId?: string;
|
|
70
70
|
hostId?: string;
|
|
71
|
+
/**
|
|
72
|
+
* The account the push was sent to (AGL-3943). An app signed in to more
|
|
73
|
+
* than one account opens a tapped push in that account, switching to it
|
|
74
|
+
* when it is signed in but not the one in use. A push without it (sent
|
|
75
|
+
* before this field) routes as before, in the account in use.
|
|
76
|
+
*/
|
|
77
|
+
uid?: string;
|
|
71
78
|
}
|
|
72
79
|
export declare function mobilePushData(payload: {
|
|
73
80
|
type: string;
|
|
74
81
|
link?: string | null;
|
|
75
82
|
orgId?: string | null;
|
|
76
83
|
hostId?: string | null;
|
|
84
|
+
uid?: string | null;
|
|
77
85
|
}): MobilePushData;
|
|
78
86
|
/** Read a tapped push's data back, refusing anything that is not ours. */
|
|
79
87
|
export declare function readMobilePushData(data: unknown): MobilePushData | null;
|
|
@@ -53,6 +53,8 @@ export function mobilePushData(payload) {
|
|
|
53
53
|
orgId: payload.orgId
|
|
54
54
|
} : {}, payload.hostId ? {
|
|
55
55
|
hostId: payload.hostId
|
|
56
|
+
} : {}, payload.uid ? {
|
|
57
|
+
uid: payload.uid
|
|
56
58
|
} : {});
|
|
57
59
|
}
|
|
58
60
|
/** Read a tapped push's data back, refusing anything that is not ours. */ export function readMobilePushData(data) {
|
|
@@ -66,7 +68,8 @@ export function mobilePushData(payload) {
|
|
|
66
68
|
// Only a console path or an https URL; never a scheme the app would hand to the OS.
|
|
67
69
|
link: link && (link.startsWith('/') || link.startsWith('https://')) ? link : undefined,
|
|
68
70
|
orgId: text(record['orgId']),
|
|
69
|
-
hostId: text(record['hostId'])
|
|
71
|
+
hostId: text(record['hostId']),
|
|
72
|
+
uid: text(record['uid'])
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/mobile-push.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The mobile push channel's shapes (AGL-3620, AGL-3651), for the server\n * fan-out and the native apps alike.\n *\n * NO IMPORTS, not even types, on purpose: the native apps compile this file\n * with their own compiler, and a type import would drag the core's whole\n * type graph into it. The full per-scope resolution lives in\n * `notification-push.ts`, which the server reads; the app needs only the\n * account scope its settings screen edits, `accountPushSwitch` below, which\n * a spec holds to the full resolver.\n */\n\n/** `users/{uid}/devices/{installId}`: one row per app install, written by its owner. */\nexport const MOBILE_DEVICES_COLLECTION = 'devices'\n\nexport type MobileDevicePlatform = 'ios' | 'android' | 'macos'\n\n/** The two native apps (bundle `com.aglyn.app` and `com.aglyn.pos`). */\nexport type MobileDeviceApp = 'aglyn' | 'aglyn-pos'\n\n/** Each app's bundle id: the APNs topic its pushes are addressed to. */\nexport const MOBILE_APP_BUNDLES: Readonly<Record<MobileDeviceApp, string>> = {\n aglyn: 'com.aglyn.app',\n 'aglyn-pos': 'com.aglyn.pos',\n}\n\n/** How a device is reached: Apple's APNs (iOS, macOS) or Google's FCM (Android). */\nexport type MobilePushTransport = 'apns' | 'fcm'\n\n/** The APNs environment a device token was minted in; a token only works in its own. */\nexport type ApnsEnvironment = 'sandbox' | 'production'\n\nexport interface MobileDevice {\n /** An APNs device token (hex) or an FCM registration token, per `transport`. */\n token: string\n transport: MobilePushTransport\n /** APNs only: which gateway the token belongs to. */\n apnsEnvironment?: ApnsEnvironment\n platform: MobileDevicePlatform\n app: MobileDeviceApp\n appVersion?: string\n /** Server time of the app's last launch while signed in. */\n lastSeen?: unknown\n}\n\n/** A device not seen for this long is not pushed to (and is pruned by the fan-out). */\nexport const MOBILE_DEVICE_STALE_MS = 60 * 24 * 60 * 60 * 1000\n\n/** The most devices one person's push fan-out reads. */\nexport const MOBILE_DEVICES_PER_USER = 20\n\nconst HEX = /^[0-9A-Fa-f]+$/\nconst FCM_TOKEN = /^[A-Za-z0-9_:-]+$/\n\n/** An APNs device token: hex, 64 to 200 characters (the Firestore rules hold the same). */\nexport function isApnsDeviceToken(value: unknown): value is string {\n return typeof value === 'string' && value.length >= 64 && value.length <= 200 && HEX.test(value)\n}\n\n/** An FCM registration token: `[A-Za-z0-9_:-]`, 100 to 4096 characters (the rules hold the same). */\nexport function isFcmRegistrationToken(value: unknown): value is string {\n return typeof value === 'string' && value.length >= 100 && value.length <= 4096 && FCM_TOKEN.test(value)\n}\n\n/** Whether `token` is a token of `transport`'s shape. */\nexport function isMobilePushToken(transport: unknown, token: unknown): boolean {\n if (transport === 'apns') return isApnsDeviceToken(token)\n if (transport === 'fcm') return isFcmRegistrationToken(token)\n return false\n}\n\n/**\n * What a push carries for the app to act on when it is tapped. The app\n * resolves `link` the way it resolves a feed row: natively when a plugin\n * registered a deep link for it, else in the console WebView.\n */\nexport interface MobilePushData {\n type: string\n link?: string\n orgId?: string\n hostId?: string\n}\n\nexport function mobilePushData(payload: {\n type: string\n link?: string | null\n orgId?: string | null\n hostId?: string | null\n}): MobilePushData {\n return {\n type: payload.type,\n ...(payload.link ? { link: payload.link } : {}),\n ...(payload.orgId ? { orgId: payload.orgId } : {}),\n ...(payload.hostId ? { hostId: payload.hostId } : {}),\n }\n}\n\n/** Read a tapped push's data back, refusing anything that is not ours. */\nexport function readMobilePushData(data: unknown): MobilePushData | null {\n if (!data || typeof data !== 'object') return null\n const record = data as Record<string, unknown>\n if (typeof record['type'] !== 'string' || !record['type']) return null\n const text = (value: unknown) => (typeof value === 'string' && value ? value : undefined)\n const link = text(record['link'])\n return mobilePushData({\n type: record['type'],\n // Only a console path or an https URL; never a scheme the app would hand to the OS.\n link: link && (link.startsWith('/') || link.startsWith('https://')) ? link : undefined,\n orgId: text(record['orgId']),\n hostId: text(record['hostId']),\n })\n}\n\ntype ChannelAnswers = { console?: boolean; push?: boolean } | undefined\n\n/** The account-scope slice of `NotificationSettings` the app's switch reads. */\nexport interface AccountPushSettings {\n account?: Record<string, ChannelAnswers>\n accountTypes?: Record<string, ChannelAnswers>\n}\n\n/**\n * What the app's per-type push switch shows: the account's push answer for\n * the type, then for its category, and otherwise what the console feed does\n * for it at the account scope (its console answers, the legacy category\n * mute, then `consoleDefault`, the type's console default from the catalog).\n * Equal to `notificationPushEnabled` with no scope; a spec holds the two\n * together.\n */\nexport function accountPushSwitch(\n settings: AccountPushSettings | null | undefined,\n type: string,\n category: string,\n consoleDefault: boolean,\n legacyPrefs?: Record<string, boolean> | null,\n): boolean {\n for (const answer of [settings?.accountTypes?.[type]?.push, settings?.account?.[category]?.push]) {\n if (typeof answer === 'boolean') return answer\n }\n for (const answer of [settings?.accountTypes?.[type]?.console, settings?.account?.[category]?.console]) {\n if (typeof answer === 'boolean') return answer\n }\n if (legacyPrefs?.[category] === false) return false\n return consoleDefault\n}\n"],"names":["MOBILE_DEVICES_COLLECTION","MOBILE_APP_BUNDLES","aglyn","MOBILE_DEVICE_STALE_MS","MOBILE_DEVICES_PER_USER","HEX","FCM_TOKEN","isApnsDeviceToken","value","length","test","isFcmRegistrationToken","isMobilePushToken","transport","token","mobilePushData","payload","type","link","orgId","hostId","readMobilePushData","data","record","text","undefined","startsWith","accountPushSwitch","settings","category","consoleDefault","legacyPrefs","answer","accountTypes","push","account","console"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,sFAAsF,GACtF,OAAO,MAAMA,4BAA4B,UAAS;AAOlD,sEAAsE,GACtE,OAAO,MAAMC,qBAAgE;IAC3EC,OAAO;IACP,aAAa;AACf,EAAC;AAqBD,qFAAqF,GACrF,OAAO,MAAMC,yBAAyB,KAAK,KAAK,KAAK,KAAK,KAAI;AAE9D,sDAAsD,GACtD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC,MAAMC,MAAM;AACZ,MAAMC,YAAY;AAElB,yFAAyF,GACzF,OAAO,SAASC,kBAAkBC,KAAc;IAC9C,OAAO,OAAOA,UAAU,YAAYA,MAAMC,MAAM,IAAI,MAAMD,MAAMC,MAAM,IAAI,OAAOJ,IAAIK,IAAI,CAACF;AAC5F;AAEA,mGAAmG,GACnG,OAAO,SAASG,uBAAuBH,KAAc;IACnD,OAAO,OAAOA,UAAU,YAAYA,MAAMC,MAAM,IAAI,OAAOD,MAAMC,MAAM,IAAI,QAAQH,UAAUI,IAAI,CAACF;AACpG;AAEA,uDAAuD,GACvD,OAAO,SAASI,kBAAkBC,SAAkB,EAAEC,KAAc;IAClE,IAAID,cAAc,QAAQ,OAAON,kBAAkBO;IACnD,IAAID,cAAc,OAAO,OAAOF,uBAAuBG;IACvD,OAAO;AACT;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/mobile-push.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The mobile push channel's shapes (AGL-3620, AGL-3651), for the server\n * fan-out and the native apps alike.\n *\n * NO IMPORTS, not even types, on purpose: the native apps compile this file\n * with their own compiler, and a type import would drag the core's whole\n * type graph into it. The full per-scope resolution lives in\n * `notification-push.ts`, which the server reads; the app needs only the\n * account scope its settings screen edits, `accountPushSwitch` below, which\n * a spec holds to the full resolver.\n */\n\n/** `users/{uid}/devices/{installId}`: one row per app install, written by its owner. */\nexport const MOBILE_DEVICES_COLLECTION = 'devices'\n\nexport type MobileDevicePlatform = 'ios' | 'android' | 'macos'\n\n/** The two native apps (bundle `com.aglyn.app` and `com.aglyn.pos`). */\nexport type MobileDeviceApp = 'aglyn' | 'aglyn-pos'\n\n/** Each app's bundle id: the APNs topic its pushes are addressed to. */\nexport const MOBILE_APP_BUNDLES: Readonly<Record<MobileDeviceApp, string>> = {\n aglyn: 'com.aglyn.app',\n 'aglyn-pos': 'com.aglyn.pos',\n}\n\n/** How a device is reached: Apple's APNs (iOS, macOS) or Google's FCM (Android). */\nexport type MobilePushTransport = 'apns' | 'fcm'\n\n/** The APNs environment a device token was minted in; a token only works in its own. */\nexport type ApnsEnvironment = 'sandbox' | 'production'\n\nexport interface MobileDevice {\n /** An APNs device token (hex) or an FCM registration token, per `transport`. */\n token: string\n transport: MobilePushTransport\n /** APNs only: which gateway the token belongs to. */\n apnsEnvironment?: ApnsEnvironment\n platform: MobileDevicePlatform\n app: MobileDeviceApp\n appVersion?: string\n /** Server time of the app's last launch while signed in. */\n lastSeen?: unknown\n}\n\n/** A device not seen for this long is not pushed to (and is pruned by the fan-out). */\nexport const MOBILE_DEVICE_STALE_MS = 60 * 24 * 60 * 60 * 1000\n\n/** The most devices one person's push fan-out reads. */\nexport const MOBILE_DEVICES_PER_USER = 20\n\nconst HEX = /^[0-9A-Fa-f]+$/\nconst FCM_TOKEN = /^[A-Za-z0-9_:-]+$/\n\n/** An APNs device token: hex, 64 to 200 characters (the Firestore rules hold the same). */\nexport function isApnsDeviceToken(value: unknown): value is string {\n return typeof value === 'string' && value.length >= 64 && value.length <= 200 && HEX.test(value)\n}\n\n/** An FCM registration token: `[A-Za-z0-9_:-]`, 100 to 4096 characters (the rules hold the same). */\nexport function isFcmRegistrationToken(value: unknown): value is string {\n return typeof value === 'string' && value.length >= 100 && value.length <= 4096 && FCM_TOKEN.test(value)\n}\n\n/** Whether `token` is a token of `transport`'s shape. */\nexport function isMobilePushToken(transport: unknown, token: unknown): boolean {\n if (transport === 'apns') return isApnsDeviceToken(token)\n if (transport === 'fcm') return isFcmRegistrationToken(token)\n return false\n}\n\n/**\n * What a push carries for the app to act on when it is tapped. The app\n * resolves `link` the way it resolves a feed row: natively when a plugin\n * registered a deep link for it, else in the console WebView.\n */\nexport interface MobilePushData {\n type: string\n link?: string\n orgId?: string\n hostId?: string\n /**\n * The account the push was sent to (AGL-3943). An app signed in to more\n * than one account opens a tapped push in that account, switching to it\n * when it is signed in but not the one in use. A push without it (sent\n * before this field) routes as before, in the account in use.\n */\n uid?: string\n}\n\nexport function mobilePushData(payload: {\n type: string\n link?: string | null\n orgId?: string | null\n hostId?: string | null\n uid?: string | null\n}): MobilePushData {\n return {\n type: payload.type,\n ...(payload.link ? { link: payload.link } : {}),\n ...(payload.orgId ? { orgId: payload.orgId } : {}),\n ...(payload.hostId ? { hostId: payload.hostId } : {}),\n ...(payload.uid ? { uid: payload.uid } : {}),\n }\n}\n\n/** Read a tapped push's data back, refusing anything that is not ours. */\nexport function readMobilePushData(data: unknown): MobilePushData | null {\n if (!data || typeof data !== 'object') return null\n const record = data as Record<string, unknown>\n if (typeof record['type'] !== 'string' || !record['type']) return null\n const text = (value: unknown) => (typeof value === 'string' && value ? value : undefined)\n const link = text(record['link'])\n return mobilePushData({\n type: record['type'],\n // Only a console path or an https URL; never a scheme the app would hand to the OS.\n link: link && (link.startsWith('/') || link.startsWith('https://')) ? link : undefined,\n orgId: text(record['orgId']),\n hostId: text(record['hostId']),\n uid: text(record['uid']),\n })\n}\n\ntype ChannelAnswers = { console?: boolean; push?: boolean } | undefined\n\n/** The account-scope slice of `NotificationSettings` the app's switch reads. */\nexport interface AccountPushSettings {\n account?: Record<string, ChannelAnswers>\n accountTypes?: Record<string, ChannelAnswers>\n}\n\n/**\n * What the app's per-type push switch shows: the account's push answer for\n * the type, then for its category, and otherwise what the console feed does\n * for it at the account scope (its console answers, the legacy category\n * mute, then `consoleDefault`, the type's console default from the catalog).\n * Equal to `notificationPushEnabled` with no scope; a spec holds the two\n * together.\n */\nexport function accountPushSwitch(\n settings: AccountPushSettings | null | undefined,\n type: string,\n category: string,\n consoleDefault: boolean,\n legacyPrefs?: Record<string, boolean> | null,\n): boolean {\n for (const answer of [settings?.accountTypes?.[type]?.push, settings?.account?.[category]?.push]) {\n if (typeof answer === 'boolean') return answer\n }\n for (const answer of [settings?.accountTypes?.[type]?.console, settings?.account?.[category]?.console]) {\n if (typeof answer === 'boolean') return answer\n }\n if (legacyPrefs?.[category] === false) return false\n return consoleDefault\n}\n"],"names":["MOBILE_DEVICES_COLLECTION","MOBILE_APP_BUNDLES","aglyn","MOBILE_DEVICE_STALE_MS","MOBILE_DEVICES_PER_USER","HEX","FCM_TOKEN","isApnsDeviceToken","value","length","test","isFcmRegistrationToken","isMobilePushToken","transport","token","mobilePushData","payload","type","link","orgId","hostId","uid","readMobilePushData","data","record","text","undefined","startsWith","accountPushSwitch","settings","category","consoleDefault","legacyPrefs","answer","accountTypes","push","account","console"],"mappings":"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"}
|
|
@@ -31,8 +31,9 @@ export declare function nativeAppAssociationRoute(pathname: string): string | nu
|
|
|
31
31
|
* link to one stays in the browser: the sign-in page and SSO (the system
|
|
32
32
|
* auth session opens them for the app, and a universal link there would loop
|
|
33
33
|
* back into the app), the native hand-off and other `/auth` pages, the
|
|
34
|
-
* sign-out page, the site admin bar's connect page
|
|
35
|
-
* device pages
|
|
34
|
+
* sign-out page, the site admin bar's connect page, a plugin's public
|
|
35
|
+
* device pages and a site's photo link (`/p/<linkId>`, opened on a phone
|
|
36
|
+
* by someone with no account). The sections the apps finish natively are
|
|
36
37
|
* {@link NATIVE_AUTH_LINK_SEGMENTS}.
|
|
37
38
|
*
|
|
38
39
|
* The rest of the console's top-level sections (`admin`, `billing`,
|
|
@@ -51,8 +51,9 @@
|
|
|
51
51
|
* link to one stays in the browser: the sign-in page and SSO (the system
|
|
52
52
|
* auth session opens them for the app, and a universal link there would loop
|
|
53
53
|
* back into the app), the native hand-off and other `/auth` pages, the
|
|
54
|
-
* sign-out page, the site admin bar's connect page
|
|
55
|
-
* device pages
|
|
54
|
+
* sign-out page, the site admin bar's connect page, a plugin's public
|
|
55
|
+
* device pages and a site's photo link (`/p/<linkId>`, opened on a phone
|
|
56
|
+
* by someone with no account). The sections the apps finish natively are
|
|
56
57
|
* {@link NATIVE_AUTH_LINK_SEGMENTS}.
|
|
57
58
|
*
|
|
58
59
|
* The rest of the console's top-level sections (`admin`, `billing`,
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
'auth',
|
|
66
67
|
'edit-access',
|
|
67
68
|
'kiosk',
|
|
69
|
+
'p',
|
|
68
70
|
'signin',
|
|
69
71
|
'signout',
|
|
70
72
|
'sso'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser: the sign-in page and SSO (the system\n * auth session opens them for the app, and a universal link there would loop\n * back into the app), the native hand-off and other `/auth` pages, the\n * sign-out page, the site admin bar's connect page and a plugin's public\n * device pages. The sections the apps finish natively are\n * {@link NATIVE_AUTH_LINK_SEGMENTS}.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'auth',\n 'edit-access',\n 'kiosk',\n 'signin',\n 'signout',\n 'sso',\n]\n\n/**\n * The pre-sign-in sections whose exact page the apps finish natively: sign-up\n * (with its `?plan=`), an emailed verification link, an emailed password\n * reset and account recovery (`AuthFlow.swift`; Android's console intent\n * filter lists the same four paths). Only the page itself opens the app;\n * anything under it stays in the browser, where the console handles it.\n */\nexport const NATIVE_AUTH_LINK_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'reset-password',\n 'signup',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n for (const segment of NATIVE_AUTH_LINK_SEGMENTS) {\n excluded.push({ '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","NATIVE_AUTH_LINK_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser: the sign-in page and SSO (the system\n * auth session opens them for the app, and a universal link there would loop\n * back into the app), the native hand-off and other `/auth` pages, the\n * sign-out page, the site admin bar's connect page, a plugin's public\n * device pages and a site's photo link (`/p/<linkId>`, opened on a phone\n * by someone with no account). The sections the apps finish natively are\n * {@link NATIVE_AUTH_LINK_SEGMENTS}.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'auth',\n 'edit-access',\n 'kiosk',\n 'p',\n 'signin',\n 'signout',\n 'sso',\n]\n\n/**\n * The pre-sign-in sections whose exact page the apps finish natively: sign-up\n * (with its `?plan=`), an emailed verification link, an emailed password\n * reset and account recovery (`AuthFlow.swift`; Android's console intent\n * filter lists the same four paths). Only the page itself opens the app;\n * anything under it stays in the browser, where the console handles it.\n */\nexport const NATIVE_AUTH_LINK_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'reset-password',\n 'signup',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n for (const segment of NATIVE_AUTH_LINK_SEGMENTS) {\n excluded.push({ '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","NATIVE_AUTH_LINK_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
|