@aglyn/aglyn 1.0.0-beta.200 → 1.0.0-beta.202

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.
Files changed (85) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-presenter.d.ts +29 -3
  3. package/src/lib/app-utils/activity-presenter.js +32 -4
  4. package/src/lib/app-utils/activity-presenter.js.map +1 -1
  5. package/src/lib/app-utils/api-plugins.d.ts +20 -0
  6. package/src/lib/app-utils/api-plugins.js +9 -0
  7. package/src/lib/app-utils/api-plugins.js.map +1 -1
  8. package/src/lib/app-utils/artifact-list-keys.js +10 -0
  9. package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
  10. package/src/lib/app-utils/card-payment-velocity.d.ts +57 -0
  11. package/src/lib/app-utils/card-payment-velocity.js +90 -0
  12. package/src/lib/app-utils/card-payment-velocity.js.map +1 -0
  13. package/src/lib/app-utils/crm-digest.d.ts +6 -0
  14. package/src/lib/app-utils/crm-digest.js +1 -0
  15. package/src/lib/app-utils/crm-digest.js.map +1 -1
  16. package/src/lib/app-utils/crm-task-reminders.d.ts +6 -0
  17. package/src/lib/app-utils/crm-task-reminders.js +1 -0
  18. package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
  19. package/src/lib/app-utils/crm.d.ts +19 -0
  20. package/src/lib/app-utils/crm.js +27 -1
  21. package/src/lib/app-utils/crm.js.map +1 -1
  22. package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
  23. package/src/lib/app-utils/docs-help.generated.js +12 -0
  24. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  25. package/src/lib/app-utils/docs-index.generated.js +85 -64
  26. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  27. package/src/lib/app-utils/email-state.d.ts +7 -3
  28. package/src/lib/app-utils/email-state.js +22 -6
  29. package/src/lib/app-utils/email-state.js.map +1 -1
  30. package/src/lib/app-utils/forms.d.ts +101 -9
  31. package/src/lib/app-utils/forms.js +137 -7
  32. package/src/lib/app-utils/forms.js.map +1 -1
  33. package/src/lib/app-utils/health-report.d.ts +3 -3
  34. package/src/lib/app-utils/health-report.js +9 -9
  35. package/src/lib/app-utils/health-report.js.map +1 -1
  36. package/src/lib/app-utils/host-naming.d.ts +12 -7
  37. package/src/lib/app-utils/host-naming.js +18 -3
  38. package/src/lib/app-utils/host-naming.js.map +1 -1
  39. package/src/lib/app-utils/lockdown.d.ts +19 -0
  40. package/src/lib/app-utils/lockdown.js +21 -0
  41. package/src/lib/app-utils/lockdown.js.map +1 -1
  42. package/src/lib/app-utils/marketplace-listing-visibility.js +5 -5
  43. package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
  44. package/src/lib/app-utils/media-embedded-fields.d.ts +40 -0
  45. package/src/lib/app-utils/media-embedded-fields.js +77 -0
  46. package/src/lib/app-utils/media-embedded-fields.js.map +1 -1
  47. package/src/lib/app-utils/media-metadata.d.ts +17 -1
  48. package/src/lib/app-utils/media-metadata.js +22 -2
  49. package/src/lib/app-utils/media-metadata.js.map +1 -1
  50. package/src/lib/app-utils/notifications.d.ts +35 -0
  51. package/src/lib/app-utils/notifications.js +57 -1
  52. package/src/lib/app-utils/notifications.js.map +1 -1
  53. package/src/lib/app-utils/payment-risk.d.ts +75 -0
  54. package/src/lib/app-utils/payment-risk.js +139 -0
  55. package/src/lib/app-utils/payment-risk.js.map +1 -0
  56. package/src/lib/app-utils/platform-brand.d.ts +8 -2
  57. package/src/lib/app-utils/platform-brand.js +9 -3
  58. package/src/lib/app-utils/platform-brand.js.map +1 -1
  59. package/src/lib/app-utils/plugin-api-request-host.d.ts +29 -0
  60. package/src/lib/app-utils/plugin-api-request-host.js +90 -0
  61. package/src/lib/app-utils/plugin-api-request-host.js.map +1 -0
  62. package/src/lib/app-utils/plugin-bundle-checks.d.ts +4 -2
  63. package/src/lib/app-utils/plugin-bundle-checks.js +111 -5
  64. package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
  65. package/src/lib/app-utils/site-return-url.d.ts +55 -0
  66. package/src/lib/app-utils/site-return-url.js +129 -0
  67. package/src/lib/app-utils/site-return-url.js.map +1 -0
  68. package/src/lib/app-utils/webhook-delivery.js +4 -1
  69. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  70. package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +18 -0
  71. package/src/lib/plugin-manager/billing-webhook-hooks.js +9 -1
  72. package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +9 -1
  74. package/src/lib/plugin-manager/plugin-activity-actions.js +11 -1
  75. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  76. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
  77. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  78. package/src/lib/plugin-manager/plugin-events.d.ts +22 -0
  79. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  80. package/src/lib/plugin-manager/plugin-jobs.d.ts +8 -4
  81. package/src/lib/plugin-manager/plugin-jobs.js +8 -4
  82. package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
  83. package/src/lib/plugin-manager/plugin-recurring-charges.d.ts +39 -0
  84. package/src/lib/plugin-manager/plugin-recurring-charges.js +49 -0
  85. package/src/lib/plugin-manager/plugin-recurring-charges.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.200",
3
+ "version": "1.0.0-beta.202",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -33,16 +33,16 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@aglyn/shared-data-enums": "1.0.0-beta.200",
37
- "@aglyn/shared-data-mdi": "1.0.0-beta.200",
38
- "@aglyn/shared-data-types": "1.0.0-beta.200",
39
- "@aglyn/shared-util-email": "1.0.0-beta.200",
40
- "@aglyn/shared-util-first-touch": "1.0.0-beta.200",
41
- "@aglyn/shared-util-http": "1.0.0-beta.200",
42
- "@aglyn/shared-util-logger": "1.0.0-beta.200",
43
- "@aglyn/shared-util-timestamp": "1.0.0-beta.200",
44
- "@aglyn/shared-util-tools": "1.0.0-beta.200",
45
- "@aglyn/shared-util-vendor": "1.0.0-beta.200",
36
+ "@aglyn/shared-data-enums": "1.0.0-beta.202",
37
+ "@aglyn/shared-data-mdi": "1.0.0-beta.202",
38
+ "@aglyn/shared-data-types": "1.0.0-beta.202",
39
+ "@aglyn/shared-util-email": "1.0.0-beta.202",
40
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.202",
41
+ "@aglyn/shared-util-http": "1.0.0-beta.202",
42
+ "@aglyn/shared-util-logger": "1.0.0-beta.202",
43
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.202",
44
+ "@aglyn/shared-util-tools": "1.0.0-beta.202",
45
+ "@aglyn/shared-util-vendor": "1.0.0-beta.202",
46
46
  "@data-driven-forms/react-form-renderer": "^4.2.0",
47
47
  "@msgpack/msgpack": "^3.1.3",
48
48
  "@types/unist": "^3.0.3",
@@ -28,8 +28,23 @@ export interface ActivityEntryLike {
28
28
  target?: ActivityTargetLike | null;
29
29
  type?: string;
30
30
  targetId?: string;
31
+ /**
32
+ * The acting uid: a person's, the literal `'api'` for a key, a
33
+ * `system:` id for a platform process, or null when no person acted.
34
+ */
35
+ actorId?: string | null;
31
36
  /** The address the actor had when the entry was written; null for a key. */
32
37
  actorEmail?: string | null;
38
+ /**
39
+ * The address `actorId` holds TODAY, looked up on read by a route with the
40
+ * Admin SDK, for an entry that recorded a uid and no address.
41
+ */
42
+ actorEmailNow?: string | null;
43
+ /**
44
+ * For a STAFF reader only: the staff member behind an entry the workspace
45
+ * did not perform (a staff cancellation), resolved by the route.
46
+ */
47
+ staffActorEmail?: string | null;
33
48
  /** The name of the API key that wrote the entry, when a key did (AGL-2632). */
34
49
  apiKeyName?: string | null;
35
50
  }
@@ -94,9 +109,20 @@ export declare function activityPrimaryText(entry: ActivityEntryLike): string;
94
109
  /** How an integration's key is named wherever an actor is: `API key Zapier`. */
95
110
  export declare function apiKeyActorLabel(name: string): string;
96
111
  /**
97
- * The "who" line for an entry: the actor's address, else the key that wrote
98
- * it by name, else `Someone` — for an entry that recorded neither, which
99
- * every key-written entry was before the name was carried.
112
+ * The "who" line for an entry — always someone a reader can narrow down.
113
+ *
114
+ * In order: the address recorded when the entry was written; the key that
115
+ * wrote it, by name; the address the recorded uid holds now, when the writer
116
+ * held a uid and no address (the billing webhook, before it carried the
117
+ * address stamped at the console act); else the uid itself, for an account
118
+ * that no longer resolves.
119
+ *
120
+ * An entry with no uid at all is not an anonymous one. Every writer that
121
+ * leaves `actorId` null is a platform process, so the feed names it: a
122
+ * workflow run by its workflow, anything else — the billing webhook,
123
+ * inbound CRM mail, a background AI job, a staff cancellation — as the
124
+ * platform; a staff reader is also told which staff member. Never "Someone", which read as a person nobody could identify
125
+ * (AGL-3369).
100
126
  */
101
127
  export declare function activityActorLabel(entry: ActivityEntryLike): string;
102
128
  /**
@@ -26,6 +26,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
26
26
  // `@aglyn/aglyn/app-utils/activity-presenter`.
27
27
  import { Route, buildRoute } from "./console-routes.js";
28
28
  import { CRM_RECORD_SECTIONS, crmRecordHref, crmSectionHref } from "./console-record-links.js";
29
+ import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
29
30
  import { hostEventLabel } from "./workflows.js";
30
31
  import { duplicateActivityActionLabel } from "./duplicate-resource.js";
31
32
  import { pluginActivityActionLabel, pluginActivityGroupForAction, pluginActivityTargetNoun } from "../plugin-manager/plugin-activity-actions.js";
@@ -120,13 +121,40 @@ import { pluginActivityActionLabel, pluginActivityGroupForAction, pluginActivity
120
121
  return `API key ${name}`;
121
122
  }
122
123
  /**
123
- * The "who" line for an entry: the actor's address, else the key that wrote
124
- * it by name, else `Someone` — for an entry that recorded neither, which
125
- * every key-written entry was before the name was carried.
124
+ * The "who" line for an entry — always someone a reader can narrow down.
125
+ *
126
+ * In order: the address recorded when the entry was written; the key that
127
+ * wrote it, by name; the address the recorded uid holds now, when the writer
128
+ * held a uid and no address (the billing webhook, before it carried the
129
+ * address stamped at the console act); else the uid itself, for an account
130
+ * that no longer resolves.
131
+ *
132
+ * An entry with no uid at all is not an anonymous one. Every writer that
133
+ * leaves `actorId` null is a platform process, so the feed names it: a
134
+ * workflow run by its workflow, anything else — the billing webhook,
135
+ * inbound CRM mail, a background AI job, a staff cancellation — as the
136
+ * platform; a staff reader is also told which staff member. Never "Someone", which read as a person nobody could identify
137
+ * (AGL-3369).
126
138
  */ export function activityActorLabel(entry) {
139
+ var _entry_target, _ref;
140
+ var _entry_actorId;
127
141
  if (entry.actorEmail) return entry.actorEmail;
128
142
  if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName);
129
- return 'Someone';
143
+ const actorId = (_entry_actorId = entry.actorId) == null ? void 0 : _entry_actorId.trim();
144
+ if (actorId === 'api') return 'API key';
145
+ if (actorId && !actorId.startsWith('system:')) {
146
+ return entry.actorEmailNow || `Account ${actorId}`;
147
+ }
148
+ const target = (_entry_target = entry.target) != null ? _entry_target : undefined;
149
+ if (((_ref = target == null ? void 0 : target.type) != null ? _ref : entry.type) === 'workflow') {
150
+ var _target_name;
151
+ const name = target == null ? void 0 : (_target_name = target.name) == null ? void 0 : _target_name.trim();
152
+ return name ? `Workflow ${name}` : 'Workflow';
153
+ }
154
+ if (entry.staffActorEmail) {
155
+ return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`;
156
+ }
157
+ return PLATFORM_BRAND_NAME;
130
158
  }
131
159
  /**
132
160
  * A deep link to the resource an entry points at, or `undefined` when it
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.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// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately dependency-free (only the string\n// route table) so plugin libs and the console app can both import it via\n// `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n CRM_RECORD_SECTIONS,\n crmRecordHref,\n crmSectionHref,\n type CrmRecordKind,\n} from './console-record-links'\nimport { hostEventLabel } from './workflows'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS: Record<string, string> = {\n host: 'Site',\n screen: 'Screen',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's AI permission flipped (AGL-2929).\n role: 'Role',\n // A generation job (AGL-2904): the org feed names the job, and each of its\n // outputs names the resource it produced.\n aiJob: 'AI generation',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n\n/** The activity target types that are CRM records, for the link switch. */\nconst CRM_TARGET_KINDS = new Set<string>(Object.keys(CRM_RECORD_SECTIONS))\n\n/**\n * The org-level hub's section for each record kind an org feed entry may\n * name (AGL-2634): the four record kinds, and tasks, which have a section\n * and no page of their own.\n */\nconst ORG_CRM_SECTIONS: Record<string, string> = {\n ...CRM_RECORD_SECTIONS,\n task: 'tasks',\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type]\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry: the actor's address, else the key that wrote\n * it by name, else `Someone` — for an entry that recorded neither, which\n * every key-written entry was before the name was carried.\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n return 'Someone'\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A CRM record's own page, or its list when the entry named no id —\n // a deleted record still has a list to land on. The CRM is a plugin\n // hub, so its addresses come from the shared builder rather than\n // the route table, the same door the console's search uses.\n if (type && CRM_TARGET_KINDS.has(type)) {\n const kind = type as CrmRecordKind\n return id\n ? crmRecordHref({ orgSlug, host }, kind, id)\n : crmSectionHref({ orgSlug, host }, CRM_RECORD_SECTIONS[kind])\n }\n return undefined\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default: {\n // A CRM record acted on at the ORGANIZATION level (AGL-2634) — a deal\n // moved from the org board, a merge, a bulk bar's action — opens in\n // the org-level hub. A lead's org address needs its site, which the\n // entry does not carry, and a bulk line names no record: both land\n // on the section.\n const section = type ? ORG_CRM_SECTIONS[type] : undefined\n if (!section) return undefined\n const hub = `${buildRoute(Route.ORG_CRM, { orgSlug })}/${section}`\n return id && type !== 'lead' && type !== 'task'\n ? `${hub}/${encodeURIComponent(id)}`\n : hub\n }\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are `HOST_EVENT_LABELS` in `workflows.ts`\n * — one map, so the picker an author chose from and the run row they read\n * afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","CRM_RECORD_SECTIONS","crmRecordHref","crmSectionHref","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetNoun","TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","aiJob","contact","lead","company","deal","CRM_TARGET_KINDS","Set","Object","keys","ORG_CRM_SECTIONS","task","activityTypeLabel","type","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","id","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","activityHref","ctx","orgSlug","undefined","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","has","kind","ORG_SETTINGS","MANAGE_TEAM_MEMBER","uid","MANAGE_TEAM","section","hub","ORG_CRM","encodeURIComponent","actionTriggerLabel","event","actionRunResult","String","result","startsWith","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.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// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately dependency-free (only the string\n// route table) so plugin libs and the console app can both import it via\n// `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n CRM_RECORD_SECTIONS,\n crmRecordHref,\n crmSectionHref,\n type CrmRecordKind,\n} from './console-record-links'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport { hostEventLabel } from './workflows'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /**\n * The acting uid: a person's, the literal `'api'` for a key, a\n * `system:` id for a platform process, or null when no person acted.\n */\n actorId?: string | null\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /**\n * The address `actorId` holds TODAY, looked up on read by a route with the\n * Admin SDK, for an entry that recorded a uid and no address.\n */\n actorEmailNow?: string | null\n /**\n * For a STAFF reader only: the staff member behind an entry the workspace\n * did not perform (a staff cancellation), resolved by the route.\n */\n staffActorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS: Record<string, string> = {\n host: 'Site',\n screen: 'Screen',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's AI permission flipped (AGL-2929).\n role: 'Role',\n // A generation job (AGL-2904): the org feed names the job, and each of its\n // outputs names the resource it produced.\n aiJob: 'AI generation',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n\n/** The activity target types that are CRM records, for the link switch. */\nconst CRM_TARGET_KINDS = new Set<string>(Object.keys(CRM_RECORD_SECTIONS))\n\n/**\n * The org-level hub's section for each record kind an org feed entry may\n * name (AGL-2634): the four record kinds, and tasks, which have a section\n * and no page of their own.\n */\nconst ORG_CRM_SECTIONS: Record<string, string> = {\n ...CRM_RECORD_SECTIONS,\n task: 'tasks',\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type]\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry — always someone a reader can narrow down.\n *\n * In order: the address recorded when the entry was written; the key that\n * wrote it, by name; the address the recorded uid holds now, when the writer\n * held a uid and no address (the billing webhook, before it carried the\n * address stamped at the console act); else the uid itself, for an account\n * that no longer resolves.\n *\n * An entry with no uid at all is not an anonymous one. Every writer that\n * leaves `actorId` null is a platform process, so the feed names it: a\n * workflow run by its workflow, anything else — the billing webhook,\n * inbound CRM mail, a background AI job, a staff cancellation — as the\n * platform; a staff reader is also told which staff member. Never \"Someone\", which read as a person nobody could identify\n * (AGL-3369).\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n const actorId = entry.actorId?.trim()\n if (actorId === 'api') return 'API key'\n if (actorId && !actorId.startsWith('system:')) {\n return entry.actorEmailNow || `Account ${actorId}`\n }\n const target = entry.target ?? undefined\n if ((target?.type ?? entry.type) === 'workflow') {\n const name = target?.name?.trim()\n return name ? `Workflow ${name}` : 'Workflow'\n }\n if (entry.staffActorEmail) {\n return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`\n }\n return PLATFORM_BRAND_NAME\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A CRM record's own page, or its list when the entry named no id —\n // a deleted record still has a list to land on. The CRM is a plugin\n // hub, so its addresses come from the shared builder rather than\n // the route table, the same door the console's search uses.\n if (type && CRM_TARGET_KINDS.has(type)) {\n const kind = type as CrmRecordKind\n return id\n ? crmRecordHref({ orgSlug, host }, kind, id)\n : crmSectionHref({ orgSlug, host }, CRM_RECORD_SECTIONS[kind])\n }\n return undefined\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default: {\n // A CRM record acted on at the ORGANIZATION level (AGL-2634) — a deal\n // moved from the org board, a merge, a bulk bar's action — opens in\n // the org-level hub. A lead's org address needs its site, which the\n // entry does not carry, and a bulk line names no record: both land\n // on the section.\n const section = type ? ORG_CRM_SECTIONS[type] : undefined\n if (!section) return undefined\n const hub = `${buildRoute(Route.ORG_CRM, { orgSlug })}/${section}`\n return id && type !== 'lead' && type !== 'task'\n ? `${hub}/${encodeURIComponent(id)}`\n : hub\n }\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are `HOST_EVENT_LABELS` in `workflows.ts`\n * — one map, so the picker an author chose from and the run row they read\n * afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","CRM_RECORD_SECTIONS","crmRecordHref","crmSectionHref","PLATFORM_BRAND_NAME","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetNoun","TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","aiJob","contact","lead","company","deal","CRM_TARGET_KINDS","Set","Object","keys","ORG_CRM_SECTIONS","task","activityTypeLabel","type","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","id","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","actorId","startsWith","actorEmailNow","undefined","staffActorEmail","activityHref","ctx","orgSlug","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","has","kind","ORG_SETTINGS","MANAGE_TEAM_MEMBER","uid","MANAGE_TEAM","section","hub","ORG_CRM","encodeURIComponent","actionTriggerLabel","event","actionRunResult","String","result","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
@@ -137,6 +137,20 @@ export interface PluginApiRouteOptions {
137
137
  * authenticates the link itself, by a signature it verifies.
138
138
  */
139
139
  recipientLink?: boolean;
140
+ /**
141
+ * The route OPENS A CARD PAYMENT for whoever calls it: a Checkout Session,
142
+ * a PaymentIntent or a SetupIntent (AGL-3363).
143
+ *
144
+ * The tenant dispatcher holds such a route to the card-testing counters
145
+ * (`card-payment-velocity.ts`) on top of its ordinary write limit, so a
146
+ * plugin's payment door is covered by declaring itself here rather than by
147
+ * carrying a limiter of its own. A console door (a signed-in merchant's
148
+ * POS or draft order) does not declare it: a register ringing up a queue
149
+ * is not a card tester. The 3-D Secure sweep
150
+ * (`card-payments-request-3ds.spec.ts`) classifies every file that creates
151
+ * a payment, so a new visitor door cannot skip the declaration.
152
+ */
153
+ cardPayment?: boolean;
140
154
  }
141
155
  export { setRegisteringPluginId, getRegisteringPluginId, } from './registering-plugin';
142
156
  /**
@@ -180,6 +194,12 @@ export declare function resolvePluginApiMatch(path: string): PluginApiMatch | un
180
194
  * before their enablement and release gates (AGL-2981).
181
195
  */
182
196
  export declare function isPluginRecipientLinkRoute(path: string): boolean;
197
+ /**
198
+ * Whether a request path resolves to a route registered with
199
+ * {@link PluginApiRouteOptions.cardPayment} — what the tenant dispatcher
200
+ * asks before its card-testing counters (AGL-3363).
201
+ */
202
+ export declare function isPluginCardPaymentRoute(path: string): boolean;
183
203
  /**
184
204
  * The subject a request to a registered route names, or `null` (AGL-2978).
185
205
  *
@@ -157,6 +157,15 @@ export function unregisterPluginApiRoute(path) {
157
157
  const matched = matchRegisteredApiKey(path);
158
158
  return matched ? ((_apiRouteOptions_get = apiRouteOptions.get(matched.key)) == null ? void 0 : _apiRouteOptions_get.recipientLink) === true : false;
159
159
  }
160
+ /**
161
+ * Whether a request path resolves to a route registered with
162
+ * {@link PluginApiRouteOptions.cardPayment} — what the tenant dispatcher
163
+ * asks before its card-testing counters (AGL-3363).
164
+ */ export function isPluginCardPaymentRoute(path) {
165
+ var _apiRouteOptions_get;
166
+ const matched = matchRegisteredApiKey(path);
167
+ return matched ? ((_apiRouteOptions_get = apiRouteOptions.get(matched.key)) == null ? void 0 : _apiRouteOptions_get.cardPayment) === true : false;
168
+ }
160
169
  /** An id a subject may carry: a non-empty path segment, not a path. */ const SUBJECT_ID = /^[^/\s]{1,128}$/;
161
170
  const subjectId = (value)=>typeof value === 'string' && SUBJECT_ID.test(value) ? value : null;
162
171
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/api-plugins.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 * Server half of the plugin pattern (AGL-396): the API-route registry, the\n * server counterpart to the ConsoleExtension registry. A feature plugin\n * registers request handlers here from its `/server` entry point (never its\n * client barrel, so firebase-admin and other server-only deps stay out of\n * the browser bundle). The app ships one catch-all dispatcher route per\n * Next app that resolves a request path to a registered handler — so moving\n * a feature's API into its plugin needs no new app route and keeps the same\n * URL. Reference implementation: events-calendar `events/list` (AGL-396).\n *\n * The request/response shapes are structural (not `next` types) so plugins\n * stay framework-light; `NextApiRequest`/`NextApiResponse` satisfy them, so\n * the dispatcher passes Next's objects straight through.\n */\n\nexport interface PluginApiRequest {\n method?: string\n query: Partial<Record<string, string | string[]>>\n // `any` mirrors NextApiRequest.body (parsed JSON/form/text); handlers\n // validate it themselves, as they did as Next routes.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body: any\n /**\n * The unparsed request text, for handlers that verify signatures over the\n * exact payload (Stripe/Svix webhooks registered as plugin API paths).\n * UTF-8 re-encoding of the text is byte-identical for valid UTF-8 JSON.\n */\n rawBody?: string\n headers: Partial<Record<string, string | string[]>>\n cookies: Partial<Record<string, string>>\n /**\n * Node socket — the client-address fallback (`remoteAddress`).\n *\n * On the console's Pages Router this is the real transport peer. Behind the\n * App Router adapter there is no socket, and `api-adapter.ts` fills it from\n * the shared client-address reader so a handler falling back to it gets the\n * same trusted hop rather than a caller-supplied one.\n */\n socket: { remoteAddress?: string }\n}\n\nexport interface PluginApiResponse {\n status(code: number): PluginApiResponse\n json(body: unknown): void\n send(body: unknown): void\n setHeader(name: string, value: string | number | readonly string[]): void\n redirect(url: string): void\n redirect(status: number, url: string): void\n end(): void\n}\n\nexport type PluginApiHandler = (\n req: PluginApiRequest,\n res: PluginApiResponse,\n) => void | Promise<void>\n\n/**\n * A handler in the Web shape (AGL-2939): the dispatcher's own `Request`\n * in, a `Response` out — with a streamed body when the door streams, as\n * the assistant's chat and a job's event feed do. `params` carries the\n * dispatcher's path segments and any `:name` segment the registered path\n * declared.\n */\nexport type PluginWebApiHandler = (\n request: Request,\n context: { params: Record<string, string | string[]> },\n) => Response | Promise<Response>\n\n/** What a route registers: the (req, res) shape, or a Web handler. */\nexport type PluginApiRoute = PluginApiHandler | { web: PluginWebApiHandler }\n\n/** A registered route matched to a request path, with its `:name` segments filled. */\nexport interface PluginApiMatch {\n route: PluginApiRoute\n params: Record<string, string>\n}\n\n/**\n * Who a request is FOR, as far as the dispatchers' release gate is concerned\n * (AGL-2978).\n *\n * The gate buckets a rollout and reads per-org overrides on an org id, and a\n * dispatcher learns that id from the site a request names (`hostId`). Two\n * kinds of request name no site and still belong to one organization:\n *\n * - a request to an ORGANIZATION-level surface (`ConsoleExtension.orgNavItems`,\n * AGL-2974), which names its org rather than a site;\n * - a request that arrives with no bearer token by design — a provider's\n * OAuth redirect back to the platform, which carries only what the platform\n * signed into it.\n *\n * Without a subject the gate treats both as anonymous, so a plugin released to\n * one organization by override refused that organization's own requests, and\n * a staff member previewing a dark plugin could not complete a redirect their\n * own session had started.\n */\nexport interface PluginApiRequestSubject {\n /** The organization: the rollout bucket and the override key. */\n orgId: string | null\n /**\n * The account the request acts for, read ONLY when the request carries no\n * bearer token — a token speaks for itself. A route may name one only when\n * it verified a signature binding the uid to the request, because the gate\n * lets a staff account named here preview a released-off plugin.\n */\n uid?: string | null\n}\n\n/** Reads a request's subject; answers `null` when the request names none. */\nexport type PluginApiSubjectResolver = (\n request: Request,\n) => PluginApiRequestSubject | null | Promise<PluginApiRequestSubject | null>\n\n/** What a registration may say about its route beyond the handler. */\nexport interface PluginApiRouteOptions {\n /**\n * How a request to this route names its subject when it names no site.\n * Consulted by both dispatchers before their release gate, and only for a\n * request with no `hostId`: a named site always decides the org.\n */\n subject?: PluginApiSubjectResolver\n /**\n * The route answers a link the platform MAILED to somebody — an\n * unsubscribe in a message's `List-Unsubscribe` header — rather than a door\n * a member uses (AGL-2981).\n *\n * Such a link has to keep working whether or not its plugin is released,\n * or switched on, for the workspace NOW. A recipient's way out does not\n * close when a rollout is paused or a workspace turns the plugin off:\n * CAN-SPAM holds an opt-out open for thirty days after the send, and an\n * unsubscribe that 404s the day a kill switch is flipped is one nobody can\n * use. So both dispatchers skip their per-site enablement and release\n * gates for it — and nothing else: lockdown and the rate limit still\n * apply. The flag says nothing about who may act, so the route\n * authenticates the link itself, by a signature it verifies.\n */\n recipientLink?: boolean\n}\n\n/** Leading/trailing slashes stripped so '/events/list' and 'events/list' key alike. */\nfunction normalizeApiPath(path: string): string {\n return path.replace(/^\\/+|\\/+$/g, '')\n}\n\nconst apiRoutes = new Map<string, PluginApiRoute>()\nconst apiRouteOwners = new Map<string, string>()\nconst apiRouteOptions = new Map<string, PluginApiRouteOptions>()\n\n/**\n * The \"currently registering plugin\" marker moved to a leaf module\n * (`registering-plugin`) so `site-page-hooks` can read it too — this file is\n * only reachable through the `/server` entry, and importing it from the shared\n * plugin-manager barrel formed a cycle (AGL-1289). Re-exported here so the\n * plugin loader's existing import keeps working unchanged.\n */\nimport { getRegisteringPluginId } from './registering-plugin'\n\nexport {\n setRegisteringPluginId,\n getRegisteringPluginId,\n} from './registering-plugin'\n\n/**\n * Registers a plugin API handler at a host-relative path (e.g.\n * 'events/list'), served by the app dispatcher at `/api/events/list`.\n *\n * Idempotent by path FOR ITS OWNER — re-registration by the plugin that\n * holds the path replaces the previous handler, which is what a hot reload,\n * a second surface, and a repeated init all depend on.\n *\n * A different plugin is REFUSED (AGL-2484). This map is process-global and\n * was last-writer-wins on the path alone, while Aglyn's own commission-taking\n * handlers sit at `marketplace/checkout` and `commerce/checkout` — and the\n * tenant dispatcher calls `ensureRemoteServerBundles()` after the first-party\n * loader has run, so a remote bundle registering either path was simply the\n * later writer. Getting there needs `PLUGIN_REMOTE_SERVER=enabled` (off by\n * default), realm trust and a valid signature, so this is the last of several\n * doors rather than the only one; it is also the one the registry could close\n * by itself, because it already recorded who registered what.\n *\n * Anonymous registration — no loader marker — is an IDENTITY here, not a\n * wildcard: it may replace itself and nothing else may take it.\n */\nexport function registerPluginApiRoute(\n path: string,\n handler: PluginApiRoute,\n options?: PluginApiRouteOptions,\n): void {\n const key = normalizeApiPath(path)\n const owner = getRegisteringPluginId() ?? ANONYMOUS_OWNER\n const incumbent = apiRouteOwners.get(key)\n if (incumbent !== undefined && incumbent !== owner) {\n // Loud, because the silent version of this is a payment route quietly\n // answering from someone else's code. Refusing is the safe direction:\n // the incumbent keeps serving.\n console.error(\n `[plugins] refused API route \"${key}\" to \"${owner}\": already ` +\n `registered by \"${incumbent}\"`,\n )\n return\n }\n apiRoutes.set(key, handler)\n apiRouteOwners.set(key, owner)\n // Replaced with the handler, never merged: a re-registration that drops its\n // subject resolver must not keep answering with the previous one.\n if (options) apiRouteOptions.set(key, options)\n else apiRouteOptions.delete(key)\n}\n\n/** The marker for a registration made outside any loader context. */\nconst ANONYMOUS_OWNER = ''\n\n/** The plugin that registered a path, for per-org enablement gating. */\nexport function pluginIdForRegisteredApiPath(path: string): string | undefined {\n const owner = apiRouteOwners.get(normalizeApiPath(path))\n // An anonymous registration has no plugin to gate on, and this has always\n // answered `undefined` for one — the sentinel must not leak out as an id.\n return owner === undefined || owner === ANONYMOUS_OWNER ? undefined : owner\n}\n\nexport function unregisterPluginApiRoute(path: string): void {\n const key = normalizeApiPath(path)\n apiRoutes.delete(key)\n // Release the CLAIM too, or the refusal above outlives the route and the\n // path can never be re-registered for the life of the process.\n apiRouteOwners.delete(key)\n apiRouteOptions.delete(key)\n}\n\n/**\n * The registered key a request path resolves to, with its `:name` segments.\n *\n * An exact registration wins; otherwise a registration whose path carries\n * `:name` segments matches segment for segment. One matcher for the route and\n * for its options, so a request can never be answered by one registration and\n * gated with another's subject.\n */\nfunction matchRegisteredApiKey(\n path: string,\n): { key: string; params: Record<string, string> } | undefined {\n const key = normalizeApiPath(path)\n if (apiRoutes.has(key)) return { key, params: {} }\n const segments = key.split('/')\n for (const registered of apiRoutes.keys()) {\n if (!registered.includes(':')) continue\n const pattern = registered.split('/')\n if (pattern.length !== segments.length) continue\n const params: Record<string, string> = {}\n let matched = true\n for (let index = 0; index < pattern.length; index += 1) {\n const part = pattern[index]\n if (part.startsWith(':') && segments[index]) {\n params[part.slice(1)] = segments[index]\n } else if (part !== segments[index]) {\n matched = false\n break\n }\n }\n if (matched) return { key: registered, params }\n }\n return undefined\n}\n\n/**\n * The route for a request path, or undefined when nothing is registered.\n *\n * An exact registration wins; otherwise a registration whose path carries\n * `:name` segments matches segment for segment (`ai/jobs/:jobId/cancel`\n * answers `ai/jobs/abc/cancel`), and the named segments come back as\n * `params`. Patterns are tried in registration order and a static segment\n * beats a named one only by being registered exactly.\n */\nexport function resolvePluginApiMatch(path: string): PluginApiMatch | undefined {\n const matched = matchRegisteredApiKey(path)\n if (!matched) return undefined\n const route = apiRoutes.get(matched.key)\n return route ? { route, params: matched.params } : undefined\n}\n\n/**\n * Whether a request path resolves to a route registered as a\n * {@link PluginApiRouteOptions.recipientLink} — what both dispatchers ask\n * before their enablement and release gates (AGL-2981).\n */\nexport function isPluginRecipientLinkRoute(path: string): boolean {\n const matched = matchRegisteredApiKey(path)\n return matched ? apiRouteOptions.get(matched.key)?.recipientLink === true : false\n}\n\n/** An id a subject may carry: a non-empty path segment, not a path. */\nconst SUBJECT_ID = /^[^/\\s]{1,128}$/\n\nconst subjectId = (value: unknown): string | null =>\n typeof value === 'string' && SUBJECT_ID.test(value) ? value : null\n\n/**\n * The subject a request to a registered route names, or `null` (AGL-2978).\n *\n * `null` when the route declared no resolver, when the resolver answered\n * nothing, and when it THREW: a resolver that cannot read its request has\n * named no subject, and the gate then treats the request as anonymous — the\n * conservative direction. The resolver reads a clone, so the handler still\n * receives the untouched body. Ids are held to a plain path segment, so a\n * resolver cannot hand the gate a path to read overrides from.\n */\nexport async function resolvePluginApiRequestSubject(\n path: string,\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n const matched = matchRegisteredApiKey(path)\n const resolve = matched ? apiRouteOptions.get(matched.key)?.subject : undefined\n if (!resolve) return null\n try {\n const subject = await resolve(request.clone())\n if (!subject) return null\n const orgId = subjectId(subject.orgId)\n const uid = subjectId(subject.uid)\n if (!orgId && !uid) return null\n return { orgId, ...(uid ? { uid } : {}) }\n } catch {\n return null\n }\n}\n\n/**\n * The legacy (node-style) handler at a request path, or undefined when\n * nothing is registered there or the route is a web handler — a web route\n * is reached through `resolvePluginApiMatch` and `runPluginApiMatch`, since\n * it takes the `Request` and the filled `:name` segments, not a `req`/`res`\n * pair.\n */\nexport function resolvePluginApiRoute(path: string): PluginApiHandler | undefined {\n const route = resolvePluginApiMatch(path)?.route\n return typeof route === 'function' ? route : undefined\n}\n\n/**\n * Runs a matched route against the dispatcher's request: a Web handler is\n * called as it is, and a (req, res) handler goes through the adapter the\n * caller supplies — the adapter lives on the `/server` entry, which this\n * module may not import.\n */\nexport async function runPluginApiMatch(\n match: PluginApiMatch,\n request: Request,\n params: Record<string, string | string[]>,\n runLegacy: (\n handler: PluginApiHandler,\n request: Request,\n params: Record<string, string | string[]>,\n ) => Promise<Response>,\n): Promise<Response> {\n const merged = { ...params, ...match.params }\n const route = match.route as { web?: PluginWebApiHandler }\n if (typeof route === 'object' && route !== null && typeof route.web === 'function') {\n return route.web(request, { params: merged })\n }\n return runLegacy(match.route as PluginApiHandler, request, merged)\n}\n\n/** Registered paths, for diagnostics. */\nexport function listPluginApiRoutes(): string[] {\n return Array.from(apiRoutes.keys())\n}\n"],"names":["normalizeApiPath","path","replace","apiRoutes","Map","apiRouteOwners","apiRouteOptions","getRegisteringPluginId","setRegisteringPluginId","registerPluginApiRoute","handler","options","key","owner","ANONYMOUS_OWNER","incumbent","get","undefined","console","error","set","delete","pluginIdForRegisteredApiPath","unregisterPluginApiRoute","matchRegisteredApiKey","has","params","segments","split","registered","keys","includes","pattern","length","matched","index","part","startsWith","slice","resolvePluginApiMatch","route","isPluginRecipientLinkRoute","recipientLink","SUBJECT_ID","subjectId","value","test","resolvePluginApiRequestSubject","request","resolve","subject","clone","orgId","uid","resolvePluginApiRoute","runPluginApiMatch","match","runLegacy","merged","web","listPluginApiRoutes","Array","from"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/api-plugins.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 * Server half of the plugin pattern (AGL-396): the API-route registry, the\n * server counterpart to the ConsoleExtension registry. A feature plugin\n * registers request handlers here from its `/server` entry point (never its\n * client barrel, so firebase-admin and other server-only deps stay out of\n * the browser bundle). The app ships one catch-all dispatcher route per\n * Next app that resolves a request path to a registered handler — so moving\n * a feature's API into its plugin needs no new app route and keeps the same\n * URL. Reference implementation: events-calendar `events/list` (AGL-396).\n *\n * The request/response shapes are structural (not `next` types) so plugins\n * stay framework-light; `NextApiRequest`/`NextApiResponse` satisfy them, so\n * the dispatcher passes Next's objects straight through.\n */\n\nexport interface PluginApiRequest {\n method?: string\n query: Partial<Record<string, string | string[]>>\n // `any` mirrors NextApiRequest.body (parsed JSON/form/text); handlers\n // validate it themselves, as they did as Next routes.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n body: any\n /**\n * The unparsed request text, for handlers that verify signatures over the\n * exact payload (Stripe/Svix webhooks registered as plugin API paths).\n * UTF-8 re-encoding of the text is byte-identical for valid UTF-8 JSON.\n */\n rawBody?: string\n headers: Partial<Record<string, string | string[]>>\n cookies: Partial<Record<string, string>>\n /**\n * Node socket — the client-address fallback (`remoteAddress`).\n *\n * On the console's Pages Router this is the real transport peer. Behind the\n * App Router adapter there is no socket, and `api-adapter.ts` fills it from\n * the shared client-address reader so a handler falling back to it gets the\n * same trusted hop rather than a caller-supplied one.\n */\n socket: { remoteAddress?: string }\n}\n\nexport interface PluginApiResponse {\n status(code: number): PluginApiResponse\n json(body: unknown): void\n send(body: unknown): void\n setHeader(name: string, value: string | number | readonly string[]): void\n redirect(url: string): void\n redirect(status: number, url: string): void\n end(): void\n}\n\nexport type PluginApiHandler = (\n req: PluginApiRequest,\n res: PluginApiResponse,\n) => void | Promise<void>\n\n/**\n * A handler in the Web shape (AGL-2939): the dispatcher's own `Request`\n * in, a `Response` out — with a streamed body when the door streams, as\n * the assistant's chat and a job's event feed do. `params` carries the\n * dispatcher's path segments and any `:name` segment the registered path\n * declared.\n */\nexport type PluginWebApiHandler = (\n request: Request,\n context: { params: Record<string, string | string[]> },\n) => Response | Promise<Response>\n\n/** What a route registers: the (req, res) shape, or a Web handler. */\nexport type PluginApiRoute = PluginApiHandler | { web: PluginWebApiHandler }\n\n/** A registered route matched to a request path, with its `:name` segments filled. */\nexport interface PluginApiMatch {\n route: PluginApiRoute\n params: Record<string, string>\n}\n\n/**\n * Who a request is FOR, as far as the dispatchers' release gate is concerned\n * (AGL-2978).\n *\n * The gate buckets a rollout and reads per-org overrides on an org id, and a\n * dispatcher learns that id from the site a request names (`hostId`). Two\n * kinds of request name no site and still belong to one organization:\n *\n * - a request to an ORGANIZATION-level surface (`ConsoleExtension.orgNavItems`,\n * AGL-2974), which names its org rather than a site;\n * - a request that arrives with no bearer token by design — a provider's\n * OAuth redirect back to the platform, which carries only what the platform\n * signed into it.\n *\n * Without a subject the gate treats both as anonymous, so a plugin released to\n * one organization by override refused that organization's own requests, and\n * a staff member previewing a dark plugin could not complete a redirect their\n * own session had started.\n */\nexport interface PluginApiRequestSubject {\n /** The organization: the rollout bucket and the override key. */\n orgId: string | null\n /**\n * The account the request acts for, read ONLY when the request carries no\n * bearer token — a token speaks for itself. A route may name one only when\n * it verified a signature binding the uid to the request, because the gate\n * lets a staff account named here preview a released-off plugin.\n */\n uid?: string | null\n}\n\n/** Reads a request's subject; answers `null` when the request names none. */\nexport type PluginApiSubjectResolver = (\n request: Request,\n) => PluginApiRequestSubject | null | Promise<PluginApiRequestSubject | null>\n\n/** What a registration may say about its route beyond the handler. */\nexport interface PluginApiRouteOptions {\n /**\n * How a request to this route names its subject when it names no site.\n * Consulted by both dispatchers before their release gate, and only for a\n * request with no `hostId`: a named site always decides the org.\n */\n subject?: PluginApiSubjectResolver\n /**\n * The route answers a link the platform MAILED to somebody — an\n * unsubscribe in a message's `List-Unsubscribe` header — rather than a door\n * a member uses (AGL-2981).\n *\n * Such a link has to keep working whether or not its plugin is released,\n * or switched on, for the workspace NOW. A recipient's way out does not\n * close when a rollout is paused or a workspace turns the plugin off:\n * CAN-SPAM holds an opt-out open for thirty days after the send, and an\n * unsubscribe that 404s the day a kill switch is flipped is one nobody can\n * use. So both dispatchers skip their per-site enablement and release\n * gates for it — and nothing else: lockdown and the rate limit still\n * apply. The flag says nothing about who may act, so the route\n * authenticates the link itself, by a signature it verifies.\n */\n recipientLink?: boolean\n /**\n * The route OPENS A CARD PAYMENT for whoever calls it: a Checkout Session,\n * a PaymentIntent or a SetupIntent (AGL-3363).\n *\n * The tenant dispatcher holds such a route to the card-testing counters\n * (`card-payment-velocity.ts`) on top of its ordinary write limit, so a\n * plugin's payment door is covered by declaring itself here rather than by\n * carrying a limiter of its own. A console door (a signed-in merchant's\n * POS or draft order) does not declare it: a register ringing up a queue\n * is not a card tester. The 3-D Secure sweep\n * (`card-payments-request-3ds.spec.ts`) classifies every file that creates\n * a payment, so a new visitor door cannot skip the declaration.\n */\n cardPayment?: boolean\n}\n\n/** Leading/trailing slashes stripped so '/events/list' and 'events/list' key alike. */\nfunction normalizeApiPath(path: string): string {\n return path.replace(/^\\/+|\\/+$/g, '')\n}\n\nconst apiRoutes = new Map<string, PluginApiRoute>()\nconst apiRouteOwners = new Map<string, string>()\nconst apiRouteOptions = new Map<string, PluginApiRouteOptions>()\n\n/**\n * The \"currently registering plugin\" marker moved to a leaf module\n * (`registering-plugin`) so `site-page-hooks` can read it too — this file is\n * only reachable through the `/server` entry, and importing it from the shared\n * plugin-manager barrel formed a cycle (AGL-1289). Re-exported here so the\n * plugin loader's existing import keeps working unchanged.\n */\nimport { getRegisteringPluginId } from './registering-plugin'\n\nexport {\n setRegisteringPluginId,\n getRegisteringPluginId,\n} from './registering-plugin'\n\n/**\n * Registers a plugin API handler at a host-relative path (e.g.\n * 'events/list'), served by the app dispatcher at `/api/events/list`.\n *\n * Idempotent by path FOR ITS OWNER — re-registration by the plugin that\n * holds the path replaces the previous handler, which is what a hot reload,\n * a second surface, and a repeated init all depend on.\n *\n * A different plugin is REFUSED (AGL-2484). This map is process-global and\n * was last-writer-wins on the path alone, while Aglyn's own commission-taking\n * handlers sit at `marketplace/checkout` and `commerce/checkout` — and the\n * tenant dispatcher calls `ensureRemoteServerBundles()` after the first-party\n * loader has run, so a remote bundle registering either path was simply the\n * later writer. Getting there needs `PLUGIN_REMOTE_SERVER=enabled` (off by\n * default), realm trust and a valid signature, so this is the last of several\n * doors rather than the only one; it is also the one the registry could close\n * by itself, because it already recorded who registered what.\n *\n * Anonymous registration — no loader marker — is an IDENTITY here, not a\n * wildcard: it may replace itself and nothing else may take it.\n */\nexport function registerPluginApiRoute(\n path: string,\n handler: PluginApiRoute,\n options?: PluginApiRouteOptions,\n): void {\n const key = normalizeApiPath(path)\n const owner = getRegisteringPluginId() ?? ANONYMOUS_OWNER\n const incumbent = apiRouteOwners.get(key)\n if (incumbent !== undefined && incumbent !== owner) {\n // Loud, because the silent version of this is a payment route quietly\n // answering from someone else's code. Refusing is the safe direction:\n // the incumbent keeps serving.\n console.error(\n `[plugins] refused API route \"${key}\" to \"${owner}\": already ` +\n `registered by \"${incumbent}\"`,\n )\n return\n }\n apiRoutes.set(key, handler)\n apiRouteOwners.set(key, owner)\n // Replaced with the handler, never merged: a re-registration that drops its\n // subject resolver must not keep answering with the previous one.\n if (options) apiRouteOptions.set(key, options)\n else apiRouteOptions.delete(key)\n}\n\n/** The marker for a registration made outside any loader context. */\nconst ANONYMOUS_OWNER = ''\n\n/** The plugin that registered a path, for per-org enablement gating. */\nexport function pluginIdForRegisteredApiPath(path: string): string | undefined {\n const owner = apiRouteOwners.get(normalizeApiPath(path))\n // An anonymous registration has no plugin to gate on, and this has always\n // answered `undefined` for one — the sentinel must not leak out as an id.\n return owner === undefined || owner === ANONYMOUS_OWNER ? undefined : owner\n}\n\nexport function unregisterPluginApiRoute(path: string): void {\n const key = normalizeApiPath(path)\n apiRoutes.delete(key)\n // Release the CLAIM too, or the refusal above outlives the route and the\n // path can never be re-registered for the life of the process.\n apiRouteOwners.delete(key)\n apiRouteOptions.delete(key)\n}\n\n/**\n * The registered key a request path resolves to, with its `:name` segments.\n *\n * An exact registration wins; otherwise a registration whose path carries\n * `:name` segments matches segment for segment. One matcher for the route and\n * for its options, so a request can never be answered by one registration and\n * gated with another's subject.\n */\nfunction matchRegisteredApiKey(\n path: string,\n): { key: string; params: Record<string, string> } | undefined {\n const key = normalizeApiPath(path)\n if (apiRoutes.has(key)) return { key, params: {} }\n const segments = key.split('/')\n for (const registered of apiRoutes.keys()) {\n if (!registered.includes(':')) continue\n const pattern = registered.split('/')\n if (pattern.length !== segments.length) continue\n const params: Record<string, string> = {}\n let matched = true\n for (let index = 0; index < pattern.length; index += 1) {\n const part = pattern[index]\n if (part.startsWith(':') && segments[index]) {\n params[part.slice(1)] = segments[index]\n } else if (part !== segments[index]) {\n matched = false\n break\n }\n }\n if (matched) return { key: registered, params }\n }\n return undefined\n}\n\n/**\n * The route for a request path, or undefined when nothing is registered.\n *\n * An exact registration wins; otherwise a registration whose path carries\n * `:name` segments matches segment for segment (`ai/jobs/:jobId/cancel`\n * answers `ai/jobs/abc/cancel`), and the named segments come back as\n * `params`. Patterns are tried in registration order and a static segment\n * beats a named one only by being registered exactly.\n */\nexport function resolvePluginApiMatch(path: string): PluginApiMatch | undefined {\n const matched = matchRegisteredApiKey(path)\n if (!matched) return undefined\n const route = apiRoutes.get(matched.key)\n return route ? { route, params: matched.params } : undefined\n}\n\n/**\n * Whether a request path resolves to a route registered as a\n * {@link PluginApiRouteOptions.recipientLink} — what both dispatchers ask\n * before their enablement and release gates (AGL-2981).\n */\nexport function isPluginRecipientLinkRoute(path: string): boolean {\n const matched = matchRegisteredApiKey(path)\n return matched ? apiRouteOptions.get(matched.key)?.recipientLink === true : false\n}\n\n/**\n * Whether a request path resolves to a route registered with\n * {@link PluginApiRouteOptions.cardPayment} — what the tenant dispatcher\n * asks before its card-testing counters (AGL-3363).\n */\nexport function isPluginCardPaymentRoute(path: string): boolean {\n const matched = matchRegisteredApiKey(path)\n return matched ? apiRouteOptions.get(matched.key)?.cardPayment === true : false\n}\n\n/** An id a subject may carry: a non-empty path segment, not a path. */\nconst SUBJECT_ID = /^[^/\\s]{1,128}$/\n\nconst subjectId = (value: unknown): string | null =>\n typeof value === 'string' && SUBJECT_ID.test(value) ? value : null\n\n/**\n * The subject a request to a registered route names, or `null` (AGL-2978).\n *\n * `null` when the route declared no resolver, when the resolver answered\n * nothing, and when it THREW: a resolver that cannot read its request has\n * named no subject, and the gate then treats the request as anonymous — the\n * conservative direction. The resolver reads a clone, so the handler still\n * receives the untouched body. Ids are held to a plain path segment, so a\n * resolver cannot hand the gate a path to read overrides from.\n */\nexport async function resolvePluginApiRequestSubject(\n path: string,\n request: Request,\n): Promise<PluginApiRequestSubject | null> {\n const matched = matchRegisteredApiKey(path)\n const resolve = matched ? apiRouteOptions.get(matched.key)?.subject : undefined\n if (!resolve) return null\n try {\n const subject = await resolve(request.clone())\n if (!subject) return null\n const orgId = subjectId(subject.orgId)\n const uid = subjectId(subject.uid)\n if (!orgId && !uid) return null\n return { orgId, ...(uid ? { uid } : {}) }\n } catch {\n return null\n }\n}\n\n/**\n * The legacy (node-style) handler at a request path, or undefined when\n * nothing is registered there or the route is a web handler — a web route\n * is reached through `resolvePluginApiMatch` and `runPluginApiMatch`, since\n * it takes the `Request` and the filled `:name` segments, not a `req`/`res`\n * pair.\n */\nexport function resolvePluginApiRoute(path: string): PluginApiHandler | undefined {\n const route = resolvePluginApiMatch(path)?.route\n return typeof route === 'function' ? route : undefined\n}\n\n/**\n * Runs a matched route against the dispatcher's request: a Web handler is\n * called as it is, and a (req, res) handler goes through the adapter the\n * caller supplies — the adapter lives on the `/server` entry, which this\n * module may not import.\n */\nexport async function runPluginApiMatch(\n match: PluginApiMatch,\n request: Request,\n params: Record<string, string | string[]>,\n runLegacy: (\n handler: PluginApiHandler,\n request: Request,\n params: Record<string, string | string[]>,\n ) => Promise<Response>,\n): Promise<Response> {\n const merged = { ...params, ...match.params }\n const route = match.route as { web?: PluginWebApiHandler }\n if (typeof route === 'object' && route !== null && typeof route.web === 'function') {\n return route.web(request, { params: merged })\n }\n return runLegacy(match.route as PluginApiHandler, request, merged)\n}\n\n/** Registered paths, for diagnostics. */\nexport function listPluginApiRoutes(): string[] {\n return Array.from(apiRoutes.keys())\n}\n"],"names":["normalizeApiPath","path","replace","apiRoutes","Map","apiRouteOwners","apiRouteOptions","getRegisteringPluginId","setRegisteringPluginId","registerPluginApiRoute","handler","options","key","owner","ANONYMOUS_OWNER","incumbent","get","undefined","console","error","set","delete","pluginIdForRegisteredApiPath","unregisterPluginApiRoute","matchRegisteredApiKey","has","params","segments","split","registered","keys","includes","pattern","length","matched","index","part","startsWith","slice","resolvePluginApiMatch","route","isPluginRecipientLinkRoute","recipientLink","isPluginCardPaymentRoute","cardPayment","SUBJECT_ID","subjectId","value","test","resolvePluginApiRequestSubject","request","resolve","subject","clone","orgId","uid","resolvePluginApiRoute","runPluginApiMatch","match","runLegacy","merged","web","listPluginApiRoutes","Array","from"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;CAaC;AA4ID,qFAAqF,GACrF,SAASA,iBAAiBC,IAAY;IACpC,OAAOA,KAAKC,OAAO,CAAC,cAAc;AACpC;AAEA,MAAMC,YAAY,IAAIC;AACtB,MAAMC,iBAAiB,IAAID;AAC3B,MAAME,kBAAkB,IAAIF;AAE5B;;;;;;CAMC,GACD,SAASG,sBAAsB,QAAQ,0BAAsB;AAE7D,SACEC,sBAAsB,EACtBD,sBAAsB,QACjB,0BAAsB;AAE7B;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASE,uBACdR,IAAY,EACZS,OAAuB,EACvBC,OAA+B;QAGjBJ;IADd,MAAMK,MAAMZ,iBAAiBC;IAC7B,MAAMY,SAAQN,0BAAAA,oCAAAA,0BAA4BO;IAC1C,MAAMC,YAAYV,eAAeW,GAAG,CAACJ;IACrC,IAAIG,cAAcE,aAAaF,cAAcF,OAAO;QAClD,sEAAsE;QACtE,sEAAsE;QACtE,+BAA+B;QAC/BK,QAAQC,KAAK,CACX,CAAC,6BAA6B,EAAEP,IAAI,MAAM,EAAEC,MAAM,WAAW,CAAC,GAC5D,CAAC,eAAe,EAAEE,UAAU,CAAC,CAAC;QAElC;IACF;IACAZ,UAAUiB,GAAG,CAACR,KAAKF;IACnBL,eAAee,GAAG,CAACR,KAAKC;IACxB,4EAA4E;IAC5E,kEAAkE;IAClE,IAAIF,SAASL,gBAAgBc,GAAG,CAACR,KAAKD;SACjCL,gBAAgBe,MAAM,CAACT;AAC9B;AAEA,mEAAmE,GACnE,MAAME,kBAAkB;AAExB,sEAAsE,GACtE,OAAO,SAASQ,6BAA6BrB,IAAY;IACvD,MAAMY,QAAQR,eAAeW,GAAG,CAAChB,iBAAiBC;IAClD,0EAA0E;IAC1E,0EAA0E;IAC1E,OAAOY,UAAUI,aAAaJ,UAAUC,kBAAkBG,YAAYJ;AACxE;AAEA,OAAO,SAASU,yBAAyBtB,IAAY;IACnD,MAAMW,MAAMZ,iBAAiBC;IAC7BE,UAAUkB,MAAM,CAACT;IACjB,yEAAyE;IACzE,+DAA+D;IAC/DP,eAAegB,MAAM,CAACT;IACtBN,gBAAgBe,MAAM,CAACT;AACzB;AAEA;;;;;;;CAOC,GACD,SAASY,sBACPvB,IAAY;IAEZ,MAAMW,MAAMZ,iBAAiBC;IAC7B,IAAIE,UAAUsB,GAAG,CAACb,MAAM,OAAO;QAAEA;QAAKc,QAAQ,CAAC;IAAE;IACjD,MAAMC,WAAWf,IAAIgB,KAAK,CAAC;IAC3B,KAAK,MAAMC,cAAc1B,UAAU2B,IAAI,GAAI;QACzC,IAAI,CAACD,WAAWE,QAAQ,CAAC,MAAM;QAC/B,MAAMC,UAAUH,WAAWD,KAAK,CAAC;QACjC,IAAII,QAAQC,MAAM,KAAKN,SAASM,MAAM,EAAE;QACxC,MAAMP,SAAiC,CAAC;QACxC,IAAIQ,UAAU;QACd,IAAK,IAAIC,QAAQ,GAAGA,QAAQH,QAAQC,MAAM,EAAEE,SAAS,EAAG;YACtD,MAAMC,OAAOJ,OAAO,CAACG,MAAM;YAC3B,IAAIC,KAAKC,UAAU,CAAC,QAAQV,QAAQ,CAACQ,MAAM,EAAE;gBAC3CT,MAAM,CAACU,KAAKE,KAAK,CAAC,GAAG,GAAGX,QAAQ,CAACQ,MAAM;YACzC,OAAO,IAAIC,SAAST,QAAQ,CAACQ,MAAM,EAAE;gBACnCD,UAAU;gBACV;YACF;QACF;QACA,IAAIA,SAAS,OAAO;YAAEtB,KAAKiB;YAAYH;QAAO;IAChD;IACA,OAAOT;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASsB,sBAAsBtC,IAAY;IAChD,MAAMiC,UAAUV,sBAAsBvB;IACtC,IAAI,CAACiC,SAAS,OAAOjB;IACrB,MAAMuB,QAAQrC,UAAUa,GAAG,CAACkB,QAAQtB,GAAG;IACvC,OAAO4B,QAAQ;QAAEA;QAAOd,QAAQQ,QAAQR,MAAM;IAAC,IAAIT;AACrD;AAEA;;;;CAIC,GACD,OAAO,SAASwB,2BAA2BxC,IAAY;QAEpCK;IADjB,MAAM4B,UAAUV,sBAAsBvB;IACtC,OAAOiC,UAAU5B,EAAAA,uBAAAA,gBAAgBU,GAAG,CAACkB,QAAQtB,GAAG,sBAA/BN,qBAAkCoC,aAAa,MAAK,OAAO;AAC9E;AAEA;;;;CAIC,GACD,OAAO,SAASC,yBAAyB1C,IAAY;QAElCK;IADjB,MAAM4B,UAAUV,sBAAsBvB;IACtC,OAAOiC,UAAU5B,EAAAA,uBAAAA,gBAAgBU,GAAG,CAACkB,QAAQtB,GAAG,sBAA/BN,qBAAkCsC,WAAW,MAAK,OAAO;AAC5E;AAEA,qEAAqE,GACrE,MAAMC,aAAa;AAEnB,MAAMC,YAAY,CAACC,QACjB,OAAOA,UAAU,YAAYF,WAAWG,IAAI,CAACD,SAASA,QAAQ;AAEhE;;;;;;;;;CASC,GACD,OAAO,eAAeE,+BACpBhD,IAAY,EACZiD,OAAgB;QAGU5C;IAD1B,MAAM4B,UAAUV,sBAAsBvB;IACtC,MAAMkD,UAAUjB,WAAU5B,uBAAAA,gBAAgBU,GAAG,CAACkB,QAAQtB,GAAG,sBAA/BN,qBAAkC8C,OAAO,GAAGnC;IACtE,IAAI,CAACkC,SAAS,OAAO;IACrB,IAAI;QACF,MAAMC,UAAU,MAAMD,QAAQD,QAAQG,KAAK;QAC3C,IAAI,CAACD,SAAS,OAAO;QACrB,MAAME,QAAQR,UAAUM,QAAQE,KAAK;QACrC,MAAMC,MAAMT,UAAUM,QAAQG,GAAG;QACjC,IAAI,CAACD,SAAS,CAACC,KAAK,OAAO;QAC3B,OAAO;YAAED;WAAWC,MAAM;YAAEA;QAAI,IAAI,CAAC;IACvC,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,sBAAsBvD,IAAY;QAClCsC;IAAd,MAAMC,SAAQD,yBAAAA,sBAAsBtC,0BAAtBsC,uBAA6BC,KAAK;IAChD,OAAO,OAAOA,UAAU,aAAaA,QAAQvB;AAC/C;AAEA;;;;;CAKC,GACD,OAAO,eAAewC,kBACpBC,KAAqB,EACrBR,OAAgB,EAChBxB,MAAyC,EACzCiC,SAIsB;IAEtB,MAAMC,SAAS,aAAKlC,QAAWgC,MAAMhC,MAAM;IAC3C,MAAMc,QAAQkB,MAAMlB,KAAK;IACzB,IAAI,OAAOA,UAAU,YAAYA,UAAU,QAAQ,OAAOA,MAAMqB,GAAG,KAAK,YAAY;QAClF,OAAOrB,MAAMqB,GAAG,CAACX,SAAS;YAAExB,QAAQkC;QAAO;IAC7C;IACA,OAAOD,UAAUD,MAAMlB,KAAK,EAAsBU,SAASU;AAC7D;AAEA,uCAAuC,GACvC,OAAO,SAASE;IACd,OAAOC,MAAMC,IAAI,CAAC7D,UAAU2B,IAAI;AAClC"}
@@ -39,6 +39,13 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
39
39
  * so does a deleted template (`artifactDeleteListKeys`), which
40
40
  * is how the library's query leaves tombstones out;
41
41
  *
42
+ * deletedAt on a screen, `null` — the flag a campaign's screens list
43
+ * asks for (`deletedAt == null`) to leave tombstones out. A
44
+ * query cannot ask for a field to be absent, so a live
45
+ * screen must STORE the null; a delete overwrites it with
46
+ * the time (and the security rules let an editor do that
47
+ * only while it is null, so a tombstone stays one);
48
+ *
42
49
  * A DELETED screen gets none of them. The email templates list orders by
43
50
  * `nameLower`, and an email template's delete clears its name keys so the
44
51
  * tombstone leaves that order (a query cannot ask for `deletedAt` to be
@@ -92,6 +99,9 @@ const record = (value)=>value && typeof value === 'object' ? value : {};
92
99
  if (!isListedArtifactCollection(collection)) return {};
93
100
  if (collection === 'screens' && doc['deletedAt'] != null) return {};
94
101
  const keys = _extends({}, displayNameSearchFields(artifactSearchName(collection, doc)));
102
+ // Stored, not omitted: `deletedAt == null` matches only a document that
103
+ // holds the field (AGL-3321, a campaign's screens).
104
+ if (collection === 'screens') keys['deletedAt'] = null;
95
105
  if (collection === 'components' && doc['kind'] !== 'email') keys['kind'] = 'site';
96
106
  if (collection === 'templates') {
97
107
  if (!TEMPLATE_KINDS.includes(String(doc['kind']))) keys['kind'] = 'page';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-keys.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 { displayNameSearchFields } from './name-search'\n\n/*\n * THE KEYS A SITE ARTIFACT'S LISTS QUERY BY (AGL-3321).\n *\n * The screens, layouts, reusable components and templates of a site are\n * listed, searched and filtered by Firestore queries, so every key a list\n * asks about has to be WRITTEN — a query cannot find a document by a value it\n * derives, and cannot find one that lacks the field at all:\n *\n * nameLower / nameTokens / nameReversed\n * the name keys (`displayNameSearchFields`), on all four;\n * kind on a component, `site` or `email` — a component stored\n * before AGL-3287 carries none and is read as `site`, so the\n * \"Used in: Page\" filter needs the word stored;\n * on a template, `page`, `component` or `layout` — a template\n * stored with none is read as a page template;\n * source.type on a template, its provenance — one stored with no\n * `source`, or with the legacy `workspace`, was saved here,\n * which is `authored`;\n * libraryRow on a template, whether it is its own row of the templates\n * library: a multi-page STARTER is one row (AGL-696), led by\n * its first live page, so the other pages carry `false` — and\n * so does a deleted template (`artifactDeleteListKeys`), which\n * is how the library's query leaves tombstones out;\n *\n * A DELETED screen gets none of them. The email templates list orders by\n * `nameLower`, and an email template's delete clears its name keys so the\n * tombstone leaves that order (a query cannot ask for `deletedAt` to be\n * absent) — so no create, restore or backfill may stamp them back.\n *\n * Every CREATE stamps them (`artifactCreateListKeys`) and every rename\n * restamps the name keys (`artifactRenameListKeys`).\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps the documents\n * written before them; a script cannot import this library, so it reads the\n * documents the way this module does, and both sides answer\n * `tools/scripts/lib/artifact-list-keys.fixtures.json` — this module's spec\n * and the backfill's `--self-test`.\n */\n\n/** The site artifact collections whose lists query these keys. */\nexport const LISTED_ARTIFACT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'templates',\n] as const\n\nexport type ListedArtifactCollection =\n (typeof LISTED_ARTIFACT_COLLECTIONS)[number]\n\n/** Whether a host subcollection is one whose lists query these keys. */\nexport const isListedArtifactCollection = (\n collection: string,\n): collection is ListedArtifactCollection =>\n (LISTED_ARTIFACT_COLLECTIONS as readonly string[]).includes(collection)\n\n/** The kinds a template's `kind` holds; absent reads as `page`. */\nconst TEMPLATE_KINDS = ['page', 'component', 'layout']\n\n/**\n * A provenance word no writer stores any more: a detached marketplace copy\n * was stamped `workspace` before it was stamped `authored` (AGL-3321).\n */\nconst LEGACY_AUTHORED_SOURCE = 'workspace'\n\nconst record = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n\n/**\n * The name a list finds an artifact by.\n *\n * Its `displayName` — except a page of a multi-page STARTER, which the\n * templates library lists as ONE row under the starter's name\n * (`source.starterName`), so every page of it carries that name's keys and a\n * search for the starter finds the row whichever page leads it.\n */\nexport function artifactSearchName(\n collection: string,\n doc: Record<string, unknown>,\n): unknown {\n if (collection === 'templates') {\n const starterName = record(doc['source'])['starterName']\n if (typeof starterName === 'string' && starterName.trim()) return starterName\n }\n return doc['displayName']\n}\n\n/**\n * The list keys a CREATE of `doc` into `collection` stamps: the name keys,\n * and the stored default of a kind a reader otherwise infers. Nothing for a\n * collection no artifact list queries.\n *\n * Spread AFTER the document's own fields: the keys are derived from them.\n */\nexport function artifactCreateListKeys(\n collection: string,\n doc: Record<string, unknown>,\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n if (collection === 'screens' && doc['deletedAt'] != null) return {}\n const keys: Record<string, unknown> = {\n ...displayNameSearchFields(artifactSearchName(collection, doc)),\n }\n if (collection === 'components' && doc['kind'] !== 'email') keys['kind'] = 'site'\n if (collection === 'templates') {\n if (!TEMPLATE_KINDS.includes(String(doc['kind']))) keys['kind'] = 'page'\n // Provenance is server-managed (AGL-666), and a writer that states it is\n // never overridden here. None at all, or the legacy word a detached copy\n // carried, was saved here, which is `authored`.\n const sourceType = record(doc['source'])['type']\n if (typeof sourceType !== 'string' || sourceType === LEGACY_AUTHORED_SOURCE) {\n keys['source'] = { ...record(doc['source']), type: 'authored' }\n }\n keys['libraryRow'] = doc['deletedAt'] == null && leadsItsStarter(doc)\n }\n return keys\n}\n\n/**\n * Whether a template leads its starter's library row when the starter is\n * written whole: every template that is not a starter page does, and of a\n * starter's pages the first (`starterOrder` 0). A later delete moves the lead\n * to the next live page — see `artifactDeleteListKeys`.\n */\nfunction leadsItsStarter(doc: Record<string, unknown>): boolean {\n const source = record(doc['source'])\n const starterId = source['starterId']\n if (typeof starterId !== 'string' || !starterId) return true\n return Number(source['starterOrder'] ?? 0) === 0\n}\n\n/**\n * The list keys a SOFT DELETE writes beside `deletedAt`: a deleted template\n * is no longer a library row. A caller deleting the page that LEADS a\n * starter's row gives the lead to the next live page (`libraryRow: true`),\n * or the starter disappears from the library with pages still in it.\n */\nexport function artifactDeleteListKeys(\n collection: string,\n): Record<string, unknown> {\n return collection === 'templates' ? { libraryRow: false } : {}\n}\n\n/**\n * The name keys a RENAME writes beside the new `displayName`. `doc` is the\n * document as read, for the one case whose search name is not its display\n * name (a starter page — see `artifactSearchName`).\n */\nexport function artifactRenameListKeys(\n collection: string,\n displayName: unknown,\n doc: Record<string, unknown> = {},\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n return displayNameSearchFields(\n artifactSearchName(collection, { ...doc, displayName }),\n )\n}\n"],"names":["displayNameSearchFields","LISTED_ARTIFACT_COLLECTIONS","isListedArtifactCollection","collection","includes","TEMPLATE_KINDS","LEGACY_AUTHORED_SOURCE","record","value","artifactSearchName","doc","starterName","trim","artifactCreateListKeys","keys","String","sourceType","type","leadsItsStarter","source","starterId","Number","artifactDeleteListKeys","libraryRow","artifactRenameListKeys","displayName"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,QAAQ,mBAAe;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GAED,gEAAgE,GAChE,OAAO,MAAMC,8BAA8B;IACzC;IACA;IACA;IACA;CACD,CAAS;AAKV,sEAAsE,GACtE,OAAO,MAAMC,6BAA6B,CACxCC,aAEA,AAACF,4BAAkDG,QAAQ,CAACD,YAAW;AAEzE,iEAAiE,GACjE,MAAME,iBAAiB;IAAC;IAAQ;IAAa;CAAS;AAEtD;;;CAGC,GACD,MAAMC,yBAAyB;AAE/B,MAAMC,SAAS,CAACC,QACdA,SAAS,OAAOA,UAAU,WAAYA,QAAoC,CAAC;AAE7E;;;;;;;CAOC,GACD,OAAO,SAASC,mBACdN,UAAkB,EAClBO,GAA4B;IAE5B,IAAIP,eAAe,aAAa;QAC9B,MAAMQ,cAAcJ,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,cAAc;QACxD,IAAI,OAAOC,gBAAgB,YAAYA,YAAYC,IAAI,IAAI,OAAOD;IACpE;IACA,OAAOD,GAAG,CAAC,cAAc;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,uBACdV,UAAkB,EAClBO,GAA4B;IAE5B,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,IAAIA,eAAe,aAAaO,GAAG,CAAC,YAAY,IAAI,MAAM,OAAO,CAAC;IAClE,MAAMI,OAAgC,aACjCd,wBAAwBS,mBAAmBN,YAAYO;IAE5D,IAAIP,eAAe,gBAAgBO,GAAG,CAAC,OAAO,KAAK,SAASI,IAAI,CAAC,OAAO,GAAG;IAC3E,IAAIX,eAAe,aAAa;QAC9B,IAAI,CAACE,eAAeD,QAAQ,CAACW,OAAOL,GAAG,CAAC,OAAO,IAAII,IAAI,CAAC,OAAO,GAAG;QAClE,yEAAyE;QACzE,yEAAyE;QACzE,gDAAgD;QAChD,MAAME,aAAaT,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO;QAChD,IAAI,OAAOM,eAAe,YAAYA,eAAeV,wBAAwB;YAC3EQ,IAAI,CAAC,SAAS,GAAG,aAAKP,OAAOG,GAAG,CAAC,SAAS;gBAAGO,MAAM;;QACrD;QACAH,IAAI,CAAC,aAAa,GAAGJ,GAAG,CAAC,YAAY,IAAI,QAAQQ,gBAAgBR;IACnE;IACA,OAAOI;AACT;AAEA;;;;;CAKC,GACD,SAASI,gBAAgBR,GAA4B;QAIrCS;IAHd,MAAMA,SAASZ,OAAOG,GAAG,CAAC,SAAS;IACnC,MAAMU,YAAYD,MAAM,CAAC,YAAY;IACrC,IAAI,OAAOC,cAAc,YAAY,CAACA,WAAW,OAAO;IACxD,OAAOC,QAAOF,uBAAAA,MAAM,CAAC,eAAe,YAAtBA,uBAA0B,OAAO;AACjD;AAEA;;;;;CAKC,GACD,OAAO,SAASG,uBACdnB,UAAkB;IAElB,OAAOA,eAAe,cAAc;QAAEoB,YAAY;IAAM,IAAI,CAAC;AAC/D;AAEA;;;;CAIC,GACD,OAAO,SAASC,uBACdrB,UAAkB,EAClBsB,WAAoB,EACpBf,MAA+B,CAAC,CAAC;IAEjC,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,OAAOH,wBACLS,mBAAmBN,YAAY,aAAKO;QAAKe;;AAE7C"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-list-keys.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 { displayNameSearchFields } from './name-search'\n\n/*\n * THE KEYS A SITE ARTIFACT'S LISTS QUERY BY (AGL-3321).\n *\n * The screens, layouts, reusable components and templates of a site are\n * listed, searched and filtered by Firestore queries, so every key a list\n * asks about has to be WRITTEN — a query cannot find a document by a value it\n * derives, and cannot find one that lacks the field at all:\n *\n * nameLower / nameTokens / nameReversed\n * the name keys (`displayNameSearchFields`), on all four;\n * kind on a component, `site` or `email` — a component stored\n * before AGL-3287 carries none and is read as `site`, so the\n * \"Used in: Page\" filter needs the word stored;\n * on a template, `page`, `component` or `layout` — a template\n * stored with none is read as a page template;\n * source.type on a template, its provenance — one stored with no\n * `source`, or with the legacy `workspace`, was saved here,\n * which is `authored`;\n * libraryRow on a template, whether it is its own row of the templates\n * library: a multi-page STARTER is one row (AGL-696), led by\n * its first live page, so the other pages carry `false` — and\n * so does a deleted template (`artifactDeleteListKeys`), which\n * is how the library's query leaves tombstones out;\n *\n * deletedAt on a screen, `null` — the flag a campaign's screens list\n * asks for (`deletedAt == null`) to leave tombstones out. A\n * query cannot ask for a field to be absent, so a live\n * screen must STORE the null; a delete overwrites it with\n * the time (and the security rules let an editor do that\n * only while it is null, so a tombstone stays one);\n *\n * A DELETED screen gets none of them. The email templates list orders by\n * `nameLower`, and an email template's delete clears its name keys so the\n * tombstone leaves that order (a query cannot ask for `deletedAt` to be\n * absent) — so no create, restore or backfill may stamp them back.\n *\n * Every CREATE stamps them (`artifactCreateListKeys`) and every rename\n * restamps the name keys (`artifactRenameListKeys`).\n * `tools/scripts/backfill-artifacts-list-keys.mjs` stamps the documents\n * written before them; a script cannot import this library, so it reads the\n * documents the way this module does, and both sides answer\n * `tools/scripts/lib/artifact-list-keys.fixtures.json` — this module's spec\n * and the backfill's `--self-test`.\n */\n\n/** The site artifact collections whose lists query these keys. */\nexport const LISTED_ARTIFACT_COLLECTIONS = [\n 'screens',\n 'layouts',\n 'components',\n 'templates',\n] as const\n\nexport type ListedArtifactCollection =\n (typeof LISTED_ARTIFACT_COLLECTIONS)[number]\n\n/** Whether a host subcollection is one whose lists query these keys. */\nexport const isListedArtifactCollection = (\n collection: string,\n): collection is ListedArtifactCollection =>\n (LISTED_ARTIFACT_COLLECTIONS as readonly string[]).includes(collection)\n\n/** The kinds a template's `kind` holds; absent reads as `page`. */\nconst TEMPLATE_KINDS = ['page', 'component', 'layout']\n\n/**\n * A provenance word no writer stores any more: a detached marketplace copy\n * was stamped `workspace` before it was stamped `authored` (AGL-3321).\n */\nconst LEGACY_AUTHORED_SOURCE = 'workspace'\n\nconst record = (value: unknown): Record<string, unknown> =>\n value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n\n/**\n * The name a list finds an artifact by.\n *\n * Its `displayName` — except a page of a multi-page STARTER, which the\n * templates library lists as ONE row under the starter's name\n * (`source.starterName`), so every page of it carries that name's keys and a\n * search for the starter finds the row whichever page leads it.\n */\nexport function artifactSearchName(\n collection: string,\n doc: Record<string, unknown>,\n): unknown {\n if (collection === 'templates') {\n const starterName = record(doc['source'])['starterName']\n if (typeof starterName === 'string' && starterName.trim()) return starterName\n }\n return doc['displayName']\n}\n\n/**\n * The list keys a CREATE of `doc` into `collection` stamps: the name keys,\n * and the stored default of a kind a reader otherwise infers. Nothing for a\n * collection no artifact list queries.\n *\n * Spread AFTER the document's own fields: the keys are derived from them.\n */\nexport function artifactCreateListKeys(\n collection: string,\n doc: Record<string, unknown>,\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n if (collection === 'screens' && doc['deletedAt'] != null) return {}\n const keys: Record<string, unknown> = {\n ...displayNameSearchFields(artifactSearchName(collection, doc)),\n }\n // Stored, not omitted: `deletedAt == null` matches only a document that\n // holds the field (AGL-3321, a campaign's screens).\n if (collection === 'screens') keys['deletedAt'] = null\n if (collection === 'components' && doc['kind'] !== 'email') keys['kind'] = 'site'\n if (collection === 'templates') {\n if (!TEMPLATE_KINDS.includes(String(doc['kind']))) keys['kind'] = 'page'\n // Provenance is server-managed (AGL-666), and a writer that states it is\n // never overridden here. None at all, or the legacy word a detached copy\n // carried, was saved here, which is `authored`.\n const sourceType = record(doc['source'])['type']\n if (typeof sourceType !== 'string' || sourceType === LEGACY_AUTHORED_SOURCE) {\n keys['source'] = { ...record(doc['source']), type: 'authored' }\n }\n keys['libraryRow'] = doc['deletedAt'] == null && leadsItsStarter(doc)\n }\n return keys\n}\n\n/**\n * Whether a template leads its starter's library row when the starter is\n * written whole: every template that is not a starter page does, and of a\n * starter's pages the first (`starterOrder` 0). A later delete moves the lead\n * to the next live page — see `artifactDeleteListKeys`.\n */\nfunction leadsItsStarter(doc: Record<string, unknown>): boolean {\n const source = record(doc['source'])\n const starterId = source['starterId']\n if (typeof starterId !== 'string' || !starterId) return true\n return Number(source['starterOrder'] ?? 0) === 0\n}\n\n/**\n * The list keys a SOFT DELETE writes beside `deletedAt`: a deleted template\n * is no longer a library row. A caller deleting the page that LEADS a\n * starter's row gives the lead to the next live page (`libraryRow: true`),\n * or the starter disappears from the library with pages still in it.\n */\nexport function artifactDeleteListKeys(\n collection: string,\n): Record<string, unknown> {\n return collection === 'templates' ? { libraryRow: false } : {}\n}\n\n/**\n * The name keys a RENAME writes beside the new `displayName`. `doc` is the\n * document as read, for the one case whose search name is not its display\n * name (a starter page — see `artifactSearchName`).\n */\nexport function artifactRenameListKeys(\n collection: string,\n displayName: unknown,\n doc: Record<string, unknown> = {},\n): Record<string, unknown> {\n if (!isListedArtifactCollection(collection)) return {}\n return displayNameSearchFields(\n artifactSearchName(collection, { ...doc, displayName }),\n )\n}\n"],"names":["displayNameSearchFields","LISTED_ARTIFACT_COLLECTIONS","isListedArtifactCollection","collection","includes","TEMPLATE_KINDS","LEGACY_AUTHORED_SOURCE","record","value","artifactSearchName","doc","starterName","trim","artifactCreateListKeys","keys","String","sourceType","type","leadsItsStarter","source","starterId","Number","artifactDeleteListKeys","libraryRow","artifactRenameListKeys","displayName"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,uBAAuB,QAAQ,mBAAe;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2CC,GAED,gEAAgE,GAChE,OAAO,MAAMC,8BAA8B;IACzC;IACA;IACA;IACA;CACD,CAAS;AAKV,sEAAsE,GACtE,OAAO,MAAMC,6BAA6B,CACxCC,aAEA,AAACF,4BAAkDG,QAAQ,CAACD,YAAW;AAEzE,iEAAiE,GACjE,MAAME,iBAAiB;IAAC;IAAQ;IAAa;CAAS;AAEtD;;;CAGC,GACD,MAAMC,yBAAyB;AAE/B,MAAMC,SAAS,CAACC,QACdA,SAAS,OAAOA,UAAU,WAAYA,QAAoC,CAAC;AAE7E;;;;;;;CAOC,GACD,OAAO,SAASC,mBACdN,UAAkB,EAClBO,GAA4B;IAE5B,IAAIP,eAAe,aAAa;QAC9B,MAAMQ,cAAcJ,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,cAAc;QACxD,IAAI,OAAOC,gBAAgB,YAAYA,YAAYC,IAAI,IAAI,OAAOD;IACpE;IACA,OAAOD,GAAG,CAAC,cAAc;AAC3B;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,uBACdV,UAAkB,EAClBO,GAA4B;IAE5B,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,IAAIA,eAAe,aAAaO,GAAG,CAAC,YAAY,IAAI,MAAM,OAAO,CAAC;IAClE,MAAMI,OAAgC,aACjCd,wBAAwBS,mBAAmBN,YAAYO;IAE5D,wEAAwE;IACxE,oDAAoD;IACpD,IAAIP,eAAe,WAAWW,IAAI,CAAC,YAAY,GAAG;IAClD,IAAIX,eAAe,gBAAgBO,GAAG,CAAC,OAAO,KAAK,SAASI,IAAI,CAAC,OAAO,GAAG;IAC3E,IAAIX,eAAe,aAAa;QAC9B,IAAI,CAACE,eAAeD,QAAQ,CAACW,OAAOL,GAAG,CAAC,OAAO,IAAII,IAAI,CAAC,OAAO,GAAG;QAClE,yEAAyE;QACzE,yEAAyE;QACzE,gDAAgD;QAChD,MAAME,aAAaT,OAAOG,GAAG,CAAC,SAAS,CAAC,CAAC,OAAO;QAChD,IAAI,OAAOM,eAAe,YAAYA,eAAeV,wBAAwB;YAC3EQ,IAAI,CAAC,SAAS,GAAG,aAAKP,OAAOG,GAAG,CAAC,SAAS;gBAAGO,MAAM;;QACrD;QACAH,IAAI,CAAC,aAAa,GAAGJ,GAAG,CAAC,YAAY,IAAI,QAAQQ,gBAAgBR;IACnE;IACA,OAAOI;AACT;AAEA;;;;;CAKC,GACD,SAASI,gBAAgBR,GAA4B;QAIrCS;IAHd,MAAMA,SAASZ,OAAOG,GAAG,CAAC,SAAS;IACnC,MAAMU,YAAYD,MAAM,CAAC,YAAY;IACrC,IAAI,OAAOC,cAAc,YAAY,CAACA,WAAW,OAAO;IACxD,OAAOC,QAAOF,uBAAAA,MAAM,CAAC,eAAe,YAAtBA,uBAA0B,OAAO;AACjD;AAEA;;;;;CAKC,GACD,OAAO,SAASG,uBACdnB,UAAkB;IAElB,OAAOA,eAAe,cAAc;QAAEoB,YAAY;IAAM,IAAI,CAAC;AAC/D;AAEA;;;;CAIC,GACD,OAAO,SAASC,uBACdrB,UAAkB,EAClBsB,WAAoB,EACpBf,MAA+B,CAAC,CAAC;IAEjC,IAAI,CAACR,2BAA2BC,aAAa,OAAO,CAAC;IACrD,OAAOH,wBACLS,mBAAmBN,YAAY,aAAKO;QAAKe;;AAE7C"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /** One fixed window. */
18
+ export interface CardPaymentVelocityWindow {
19
+ limit: number;
20
+ windowMs: number;
21
+ }
22
+ export declare const CARD_PAYMENT_VELOCITY: {
23
+ /** Payment doors one address may open on one site. */
24
+ readonly perVisitor: {
25
+ readonly limit: 10;
26
+ readonly windowMs: number;
27
+ };
28
+ /** …on a site whose workspace is young (`isYoungWorkspaceAge`). */
29
+ readonly perVisitorYoung: {
30
+ readonly limit: 5;
31
+ readonly windowMs: number;
32
+ };
33
+ /** Payment doors one address may open across every site. */
34
+ readonly perAddress: {
35
+ readonly limit: 30;
36
+ readonly windowMs: number;
37
+ };
38
+ /** Payment doors one site may see before staff are told. Refuses nothing. */
39
+ readonly perSite: {
40
+ readonly limit: 150;
41
+ readonly windowMs: number;
42
+ };
43
+ };
44
+ /** The per-visitor key: one bucket per (site, address). */
45
+ export declare function cardPaymentVisitorKey(hostId: string, ip: string): string;
46
+ /** The per-address key, across every site. */
47
+ export declare function cardPaymentAddressKey(ip: string): string;
48
+ /**
49
+ * The per-site key. `''` answers null: a request that named no site has no
50
+ * site to alarm about, and the two counters above still hold it.
51
+ */
52
+ export declare function cardPaymentSiteKey(hostId: string): string | null;
53
+ /**
54
+ * The abuse-queue row id for a site's alarm: one per site per UTC day, so a
55
+ * sustained attack is one row that counts, not a row per window.
56
+ */
57
+ export declare function cardPaymentAlarmDay(nowMs: number): string;