@aglyn/aglyn 1.0.0-beta.230 → 1.0.0-beta.231
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/crm.d.ts +14 -1
- package/src/lib/app-utils/crm.js +19 -2
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +80 -2
- package/src/lib/app-utils/docs-help.generated.js +209 -1
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +517 -20
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/host-status.d.ts +85 -0
- package/src/lib/app-utils/host-status.js +115 -0
- package/src/lib/app-utils/host-status.js.map +1 -0
- package/src/lib/app-utils/lockdown.js +1 -1
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/media-filter.d.ts +136 -0
- package/src/lib/app-utils/media-filter.js +400 -0
- package/src/lib/app-utils/media-filter.js.map +1 -0
- package/src/lib/app-utils/mobile-push.d.ts +98 -0
- package/src/lib/app-utils/mobile-push.js +97 -0
- package/src/lib/app-utils/mobile-push.js.map +1 -0
- package/src/lib/app-utils/notification-push.d.ts +38 -0
- package/src/lib/app-utils/notification-push.js +54 -0
- package/src/lib/app-utils/notification-push.js.map +1 -0
- package/src/lib/app-utils/notifications.d.ts +7 -0
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/organizations.js +5 -2
- package/src/lib/app-utils/organizations.js.map +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-host-events.generated.js +149 -0
- package/src/lib/app-utils/plugin-host-events.generated.js.map +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js +7 -0
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +3 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/release-flags.js +6 -2
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/scope-tokens.d.ts +16 -1
- package/src/lib/app-utils/scope-tokens.js +15 -1
- package/src/lib/app-utils/scope-tokens.js.map +1 -1
- package/src/lib/app-utils/site-list-query.d.ts +47 -0
- package/src/lib/app-utils/site-list-query.js +142 -0
- package/src/lib/app-utils/site-list-query.js.map +1 -0
- package/src/lib/app-utils/site-wide-outbox.d.ts +95 -0
- package/src/lib/app-utils/site-wide-outbox.js +117 -0
- package/src/lib/app-utils/site-wide-outbox.js.map +1 -0
- package/src/lib/app-utils/transfer-launcher-context.d.ts +6 -0
- package/src/lib/app-utils/transfer-launcher-context.js.map +1 -1
- package/src/lib/app-utils/upload-inspection.js +7 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -1
- package/src/lib/app-utils/webhook-delivery.js +4 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +8 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +18 -7
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.d.ts +4 -1
- package/src/lib/foundation/definitions/write-deny-coverage.util.js +12 -2
- package/src/lib/foundation/definitions/write-deny-coverage.util.js.map +1 -1
- package/src/lib/plugin-manager/enabled-plugins.js +4 -2
- package/src/lib/plugin-manager/enabled-plugins.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +88 -0
- package/src/lib/plugin-manager/feature-plugins.js +44 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +222 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-checkout-extras.d.ts +168 -0
- package/src/lib/plugin-manager/plugin-checkout-extras.js +172 -0
- package/src/lib/plugin-manager/plugin-checkout-extras.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contributions.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-contributions.js +1 -1
- package/src/lib/plugin-manager/plugin-contributions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-domain-events.d.ts +138 -0
- package/src/lib/plugin-manager/plugin-domain-events.js +148 -0
- package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -0
- package/src/lib/plugin-manager/plugin-events.d.ts +30 -0
- package/src/lib/plugin-manager/plugin-events.js +4 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-fulfillment-providers.d.ts +101 -0
- package/src/lib/plugin-manager/plugin-fulfillment-providers.js +83 -0
- package/src/lib/plugin-manager/plugin-fulfillment-providers.js.map +1 -0
- package/src/lib/plugin-manager/plugin-permissions.js +21 -5
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-person-records.d.ts +90 -0
- package/src/lib/plugin-manager/plugin-person-records.js +26 -0
- package/src/lib/plugin-manager/plugin-person-records.js.map +1 -1
- package/src/lib/plugin-manager/plugin-product-catalog.d.ts +204 -0
- package/src/lib/plugin-manager/plugin-product-catalog.js +43 -0
- package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipment-records.d.ts +210 -0
- package/src/lib/plugin-manager/plugin-shipment-records.js +63 -0
- package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +151 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.js +62 -0
- package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sms-messaging.d.ts +103 -0
- package/src/lib/plugin-manager/plugin-sms-messaging.js +39 -0
- package/src/lib/plugin-manager/plugin-sms-messaging.js.map +1 -0
- package/src/lib/plugin-manager/plugin-stock-levels.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-stock-levels.js +32 -0
- package/src/lib/plugin-manager/plugin-stock-levels.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tax-profile.d.ts +154 -0
- package/src/lib/plugin-manager/plugin-tax-profile.js +56 -0
- package/src/lib/plugin-manager/plugin-tax-profile.js.map +1 -1
- package/src/lib/plugin-manager/plugin-theme-font-catalog.d.ts +59 -0
- package/src/lib/plugin-manager/plugin-theme-font-catalog.js +40 -0
- package/src/lib/plugin-manager/plugin-theme-font-catalog.js.map +1 -0
- package/src/lib/plugin-manager/plugin-tracking-pages.d.ts +55 -0
- package/src/lib/plugin-manager/plugin-tracking-pages.js +76 -0
- package/src/lib/plugin-manager/plugin-tracking-pages.js.map +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +3 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
|
@@ -21,7 +21,24 @@
|
|
|
21
21
|
* `ConsolePluginPageProps.permissions` and API-side resolution carry them
|
|
22
22
|
* with zero app changes. Register at module scope from both the client
|
|
23
23
|
* barrel and the /server entry (the AGL-428 pattern).
|
|
24
|
-
*/
|
|
24
|
+
*/ var _globalScope_REGISTRY_KEY;
|
|
25
|
+
/*
|
|
26
|
+
* One registry per process, keyed on `globalThis` under `Symbol.for`, like
|
|
27
|
+
* `plugin-services.ts`. Next compiles the boot file and the route handlers
|
|
28
|
+
* apart, so each can hold its own copy of this module: with the map at module
|
|
29
|
+
* scope, a plugin's keys registered by one copy were absent from the copy
|
|
30
|
+
* `resolveOrgPermissions` read, an absent key reads as refused, and the
|
|
31
|
+
* register's `managePos` gate answered 403 to the site's own owner.
|
|
32
|
+
*/ const REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-permissions');
|
|
33
|
+
const globalScope = globalThis;
|
|
34
|
+
const tables = (_globalScope_REGISTRY_KEY = globalScope[REGISTRY_KEY]) != null ? _globalScope_REGISTRY_KEY : globalScope[REGISTRY_KEY] = {
|
|
35
|
+
permissions: new Map(),
|
|
36
|
+
listeners: new Set(),
|
|
37
|
+
version: {
|
|
38
|
+
value: 0
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const pluginPermissions = tables.permissions;
|
|
25
42
|
/*
|
|
26
43
|
* Whoever resolved a permission map before a plugin registered holds a map
|
|
27
44
|
* without that plugin's keys, and a key that is absent reads as refused
|
|
@@ -31,8 +48,7 @@
|
|
|
31
48
|
* they hold by default until the next full render. So registration is
|
|
32
49
|
* observable, the way `subscribePluginEntitlements` makes the org catalog's
|
|
33
50
|
* registrations observable, and the provider re-reads on every change.
|
|
34
|
-
*/
|
|
35
|
-
const listeners = new Set();
|
|
51
|
+
*/ const listeners = tables.listeners;
|
|
36
52
|
/** Calls `listener` after every registration; answers the unsubscribe. */ export function subscribePluginPermissions(listener) {
|
|
37
53
|
listeners.add(listener);
|
|
38
54
|
return ()=>{
|
|
@@ -43,10 +59,10 @@ const listeners = new Set();
|
|
|
43
59
|
* A number that changes whenever the registry does — the snapshot for a
|
|
44
60
|
* `useSyncExternalStore` over `subscribePluginPermissions`.
|
|
45
61
|
*/ export function pluginPermissionsVersion() {
|
|
46
|
-
return
|
|
62
|
+
return tables.version.value;
|
|
47
63
|
}
|
|
48
64
|
function notifyListeners() {
|
|
49
|
-
|
|
65
|
+
tables.version.value += 1;
|
|
50
66
|
for (const listener of [
|
|
51
67
|
...listeners
|
|
52
68
|
])listener();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Plugin-declared permissions (AGL-435, Strapi RBAC-actions parity): a\n * plugin contributes NAMED permission keys with per-role-tier defaults.\n * `resolveRolePermissions` spreads them into every resolved set (custom\n * roles override key-by-key exactly like built-in keys), so\n * `ConsolePluginPageProps.permissions` and API-side resolution carry them\n * with zero app changes. Register at module scope from both the client\n * barrel and the /server entry (the AGL-428 pattern).\n */\n\nexport interface PluginPermission {\n /** Stable key ('managePos') — persisted in custom role docs. */\n key: string\n pluginId: string\n label: string\n description?: string\n /** Verdict per built-in role tier when a role doesn't name the key. */\n defaults: { admin: boolean; editor: boolean; viewer: boolean }\n}\n\nconst
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Plugin-declared permissions (AGL-435, Strapi RBAC-actions parity): a\n * plugin contributes NAMED permission keys with per-role-tier defaults.\n * `resolveRolePermissions` spreads them into every resolved set (custom\n * roles override key-by-key exactly like built-in keys), so\n * `ConsolePluginPageProps.permissions` and API-side resolution carry them\n * with zero app changes. Register at module scope from both the client\n * barrel and the /server entry (the AGL-428 pattern).\n */\n\nexport interface PluginPermission {\n /** Stable key ('managePos') — persisted in custom role docs. */\n key: string\n pluginId: string\n label: string\n description?: string\n /** Verdict per built-in role tier when a role doesn't name the key. */\n defaults: { admin: boolean; editor: boolean; viewer: boolean }\n}\n\n/*\n * One registry per process, keyed on `globalThis` under `Symbol.for`, like\n * `plugin-services.ts`. Next compiles the boot file and the route handlers\n * apart, so each can hold its own copy of this module: with the map at module\n * scope, a plugin's keys registered by one copy were absent from the copy\n * `resolveOrgPermissions` read, an absent key reads as refused, and the\n * register's `managePos` gate answered 403 to the site's own owner.\n */\nconst REGISTRY_KEY = Symbol.for('@aglyn/aglyn:plugin-permissions')\n\ninterface PluginPermissionTables {\n permissions: Map<string, PluginPermission>\n listeners: Set<() => void>\n version: { value: number }\n}\n\nconst globalScope = globalThis as typeof globalThis & {\n [REGISTRY_KEY]?: PluginPermissionTables\n}\n\nconst tables: PluginPermissionTables =\n globalScope[REGISTRY_KEY] ??\n (globalScope[REGISTRY_KEY] = {\n permissions: new Map(),\n listeners: new Set(),\n version: { value: 0 },\n })\nconst pluginPermissions = tables.permissions\n\n/*\n * Whoever resolved a permission map before a plugin registered holds a map\n * without that plugin's keys, and a key that is absent reads as refused\n * (AGL-3228). The console's permission provider resolved its map when the\n * member document arrived; on a cold load of a plugin's deep link the\n * plugin's chunk can register AFTER that, so the owner was refused a surface\n * they hold by default until the next full render. So registration is\n * observable, the way `subscribePluginEntitlements` makes the org catalog's\n * registrations observable, and the provider re-reads on every change.\n */\nconst listeners = tables.listeners\n\n/** Calls `listener` after every registration; answers the unsubscribe. */\nexport function subscribePluginPermissions(listener: () => void): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * A number that changes whenever the registry does — the snapshot for a\n * `useSyncExternalStore` over `subscribePluginPermissions`.\n */\nexport function pluginPermissionsVersion(): number {\n return tables.version.value\n}\n\nfunction notifyListeners(): void {\n tables.version.value += 1\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Idempotent per key FOR ITS OWNER — the declaring plugin may re-register\n * (hot reload, a second surface, a repeated init) and its entry is replaced.\n *\n * A key already declared by a DIFFERENT plugin is refused (AGL-2484). This\n * map keyed on `permission.key` alone, so a later registration could redeclare\n * someone else's key with its own tier defaults — and these defaults are not\n * cosmetic: `managePos` is enforced server-side in\n * `libs/plugins/commerce/src/lib/server/pos-order.ts`, so moving\n * `defaults.viewer` to `true` hands a viewer the register. Reaching this\n * needs `PLUGIN_REMOTE_SERVER=enabled` (off by default), realm trust and a\n * signature; the registry closing its own door is the cheap part of that\n * stack.\n *\n * Per KEY, not per batch: a bundle that collides on one key still gets the\n * keys that are genuinely its own.\n */\nexport function registerPluginPermissions(\n permissions: readonly PluginPermission[],\n): void {\n for (const permission of permissions) {\n const incumbent = pluginPermissions.get(permission.key)\n if (incumbent !== undefined && incumbent.pluginId !== permission.pluginId) {\n console.error(\n `[plugins] refused permission key \"${permission.key}\" to ` +\n `\"${permission.pluginId}\": already declared by ` +\n `\"${incumbent.pluginId}\"`,\n )\n continue\n }\n pluginPermissions.set(permission.key, permission)\n }\n notifyListeners()\n}\n\nexport function listPluginPermissions(): PluginPermission[] {\n return [...pluginPermissions.values()]\n}\n\n/**\n * The registered keys alone (AGL-2474). `resolveRolePermissions` needs the\n * grantable key SPACE, not the tier verdicts: it used to iterate\n * `ORG_ROLE_PERMISSION_KEYS` — the legacy six — when applying overrides and\n * custom roles, so a plugin-declared key materialised into the resolved map\n * and could then only ever hold its tier default. Read live rather than\n * snapshotted: registration happens at plugin module scope, which can run\n * after any module-level constant here has already been evaluated.\n */\nexport function listPluginPermissionKeys(): string[] {\n return [...pluginPermissions.keys()]\n}\n\n/** Tier defaults for every registered plugin permission. */\nexport function pluginPermissionDefaults(\n tier: 'admin' | 'editor' | 'viewer',\n): Record<string, boolean> {\n const defaults: Record<string, boolean> = {}\n for (const permission of pluginPermissions.values()) {\n defaults[permission.key] = permission.defaults[tier]\n }\n return defaults\n}\n"],"names":["globalScope","REGISTRY_KEY","Symbol","for","globalThis","tables","permissions","Map","listeners","Set","version","value","pluginPermissions","subscribePluginPermissions","listener","add","delete","pluginPermissionsVersion","notifyListeners","registerPluginPermissions","permission","incumbent","get","key","undefined","pluginId","console","error","set","listPluginPermissions","values","listPluginPermissionKeys","keys","pluginPermissionDefaults","tier","defaults"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;CAQC,OAiCCA;AArBF;;;;;;;CAOC,GACD,MAAMC,eAAeC,OAAOC,GAAG,CAAC;AAQhC,MAAMH,cAAcI;AAIpB,MAAMC,UACJL,4BAAAA,WAAW,CAACC,aAAa,YAAzBD,4BACCA,WAAW,CAACC,aAAa,GAAG;IAC3BK,aAAa,IAAIC;IACjBC,WAAW,IAAIC;IACfC,SAAS;QAAEC,OAAO;IAAE;AACtB;AACF,MAAMC,oBAAoBP,OAAOC,WAAW;AAE5C;;;;;;;;;CASC,GACD,MAAME,YAAYH,OAAOG,SAAS;AAElC,wEAAwE,GACxE,OAAO,SAASK,2BAA2BC,QAAoB;IAC7DN,UAAUO,GAAG,CAACD;IACd,OAAO;QACLN,UAAUQ,MAAM,CAACF;IACnB;AACF;AAEA;;;CAGC,GACD,OAAO,SAASG;IACd,OAAOZ,OAAOK,OAAO,CAACC,KAAK;AAC7B;AAEA,SAASO;IACPb,OAAOK,OAAO,CAACC,KAAK,IAAI;IACxB,KAAK,MAAMG,YAAY;WAAIN;KAAU,CAAEM;AACzC;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK,0BACdb,WAAwC;IAExC,KAAK,MAAMc,cAAcd,YAAa;QACpC,MAAMe,YAAYT,kBAAkBU,GAAG,CAACF,WAAWG,GAAG;QACtD,IAAIF,cAAcG,aAAaH,UAAUI,QAAQ,KAAKL,WAAWK,QAAQ,EAAE;YACzEC,QAAQC,KAAK,CACX,CAAC,kCAAkC,EAAEP,WAAWG,GAAG,CAAC,KAAK,CAAC,GACxD,CAAC,CAAC,EAAEH,WAAWK,QAAQ,CAAC,uBAAuB,CAAC,GAChD,CAAC,CAAC,EAAEJ,UAAUI,QAAQ,CAAC,CAAC,CAAC;YAE7B;QACF;QACAb,kBAAkBgB,GAAG,CAACR,WAAWG,GAAG,EAAEH;IACxC;IACAF;AACF;AAEA,OAAO,SAASW;IACd,OAAO;WAAIjB,kBAAkBkB,MAAM;KAAG;AACxC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC;IACd,OAAO;WAAInB,kBAAkBoB,IAAI;KAAG;AACtC;AAEA,0DAA0D,GAC1D,OAAO,SAASC,yBACdC,IAAmC;IAEnC,MAAMC,WAAoC,CAAC;IAC3C,KAAK,MAAMf,cAAcR,kBAAkBkB,MAAM,GAAI;QACnDK,QAAQ,CAACf,WAAWG,GAAG,CAAC,GAAGH,WAAWe,QAAQ,CAACD,KAAK;IACtD;IACA,OAAOC;AACT"}
|
|
@@ -190,11 +190,75 @@ export type PluginPersonViewPeople = {
|
|
|
190
190
|
reason: 'not-found' | 'not-people' | 'unsupported';
|
|
191
191
|
unsupported?: string[];
|
|
192
192
|
};
|
|
193
|
+
/**
|
|
194
|
+
* People a site may see whose name, address or phone starts with what was
|
|
195
|
+
* typed (AGL-3609) — a cashier looking a customer up at the register, a
|
|
196
|
+
* picker anywhere a person is chosen by hand. One word: the owner answers it
|
|
197
|
+
* from its own search index and never by scanning its records.
|
|
198
|
+
*/
|
|
199
|
+
export interface PluginPersonSearchRequest {
|
|
200
|
+
/** The site searching: only records it may see are answered. */
|
|
201
|
+
hostId: string;
|
|
202
|
+
/** The organization, when the caller knows it; the owner reads it off `hostId` otherwise. */
|
|
203
|
+
orgId?: string | null;
|
|
204
|
+
/** What was typed. Raw: the owner normalizes it, and reads a run of digits as a phone. */
|
|
205
|
+
text: string;
|
|
206
|
+
/** The most people answered. The owner may answer fewer. */
|
|
207
|
+
limit: number;
|
|
208
|
+
}
|
|
193
209
|
/** Records to ask about, by the refs the owner handed out. */
|
|
194
210
|
export interface PluginPersonWroteInRequest {
|
|
195
211
|
orgId: string;
|
|
196
212
|
records: readonly PluginPersonRecordRef[];
|
|
197
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The people one site holds, oldest change first, after a cursor (AGL-3639)
|
|
216
|
+
* — what a connector that mirrors a workspace's people into another system
|
|
217
|
+
* walks, once from the start as a backfill and then from where it stopped.
|
|
218
|
+
*/
|
|
219
|
+
export interface PluginPersonChangesRequest {
|
|
220
|
+
/** The site the people are taken for: only records it may see are answered. */
|
|
221
|
+
hostId: string;
|
|
222
|
+
orgId?: string | null;
|
|
223
|
+
/**
|
|
224
|
+
* Where the last page stopped, exactly as the owner handed it out, or
|
|
225
|
+
* `null` to start from the earliest change. Opaque: only the owner reads
|
|
226
|
+
* it, so it can hold a change time finer than a millisecond and the id
|
|
227
|
+
* that breaks a tie, and a walk neither skips a person nor repeats one.
|
|
228
|
+
*/
|
|
229
|
+
after: string | null;
|
|
230
|
+
/** The most people answered; the owner may answer fewer. */
|
|
231
|
+
limit: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A person as a connector copies them out: the platform's own fields on the
|
|
235
|
+
* record (`data`, read with the platform's readers — the consent basis
|
|
236
|
+
* above all), and the owner's profile of them as this site holds it, in the
|
|
237
|
+
* platform's words rather than the owner's storage.
|
|
238
|
+
*/
|
|
239
|
+
export interface PluginPersonChange extends PluginPersonRecord {
|
|
240
|
+
/** When the record last changed, epoch ms. */
|
|
241
|
+
changedAtMs: number;
|
|
242
|
+
profile: {
|
|
243
|
+
name: string | null;
|
|
244
|
+
phone: string | null;
|
|
245
|
+
tags: string[];
|
|
246
|
+
/** What the person has spent at this site, integer cents, when the owner keeps it. */
|
|
247
|
+
lifetimeValueCents: number | null;
|
|
248
|
+
ordersCount: number | null;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
export interface PluginPersonChangesPage {
|
|
252
|
+
people: PluginPersonChange[];
|
|
253
|
+
/**
|
|
254
|
+
* Where the next page starts — the cursor to store. It moves past every
|
|
255
|
+
* record the owner READ, including one it left out for the site, so a page
|
|
256
|
+
* can be empty and still move on. `null` when nothing changed after the
|
|
257
|
+
* cursor: the walk has reached the latest change, and the caller keeps the
|
|
258
|
+
* cursor it asked with.
|
|
259
|
+
*/
|
|
260
|
+
next: string | null;
|
|
261
|
+
}
|
|
198
262
|
export interface PluginPersonRecords {
|
|
199
263
|
/** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */
|
|
200
264
|
find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>;
|
|
@@ -218,6 +282,17 @@ export interface PluginPersonRecords {
|
|
|
218
282
|
* could not answer. Optional.
|
|
219
283
|
*/
|
|
220
284
|
wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>;
|
|
285
|
+
/**
|
|
286
|
+
* People a site may see that match one typed word, best first. Optional:
|
|
287
|
+
* an owner that keeps no search index answers nothing. A read; may throw.
|
|
288
|
+
*/
|
|
289
|
+
search?(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[]>;
|
|
290
|
+
/**
|
|
291
|
+
* The people a site holds that changed after a cursor, oldest first.
|
|
292
|
+
* Optional: an owner that cannot walk its records by change answers
|
|
293
|
+
* nothing. A failed read throws.
|
|
294
|
+
*/
|
|
295
|
+
changedSince?(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage>;
|
|
221
296
|
}
|
|
222
297
|
export declare const PLUGIN_PERSON_RECORDS: import("./plugin-services").PluginServiceContract<PluginPersonRecords>;
|
|
223
298
|
/**
|
|
@@ -273,3 +348,18 @@ export declare function pluginPeopleInView(request: PluginPersonViewRequest): Pr
|
|
|
273
348
|
* caller reads an unknown as unknown.
|
|
274
349
|
*/
|
|
275
350
|
export declare function pluginPeopleWroteIn(request: PluginPersonWroteInRequest): Promise<Array<boolean | null> | null>;
|
|
351
|
+
/**
|
|
352
|
+
* People a site may see matching what was typed, or `null` when no plugin
|
|
353
|
+
* keeps people here or the one that does keeps no search — which is not
|
|
354
|
+
* "nobody matched". Blank text answers no one without asking. A failed read
|
|
355
|
+
* throws, and the caller decides how to say so.
|
|
356
|
+
*/
|
|
357
|
+
export declare function searchPluginPeople(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[] | null>;
|
|
358
|
+
/**
|
|
359
|
+
* The people a site holds that changed after a cursor, oldest first, or
|
|
360
|
+
* `null` when no plugin keeps people here or the one that does cannot walk
|
|
361
|
+
* them by change. The limit is clamped to 1–500. A failed read throws: a
|
|
362
|
+
* connector that skipped a page it could not read would advance past people
|
|
363
|
+
* it never copied.
|
|
364
|
+
*/
|
|
365
|
+
export declare function pluginPeopleChangedSince(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage | null>;
|
|
@@ -103,5 +103,31 @@ export const PLUGIN_PERSON_RECORDS = definePluginServiceContract('core.person-re
|
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* People a site may see matching what was typed, or `null` when no plugin
|
|
108
|
+
* keeps people here or the one that does keeps no search — which is not
|
|
109
|
+
* "nobody matched". Blank text answers no one without asking. A failed read
|
|
110
|
+
* throws, and the caller decides how to say so.
|
|
111
|
+
*/ export async function searchPluginPeople(request) {
|
|
112
|
+
var _request_text;
|
|
113
|
+
const resolved = pluginPersonRecords();
|
|
114
|
+
if (!(resolved == null ? void 0 : resolved.records.search)) return null;
|
|
115
|
+
if (!String((_request_text = request.text) != null ? _request_text : '').trim() || !(request.limit > 0)) return [];
|
|
116
|
+
return await resolved.records.search(request);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The people a site holds that changed after a cursor, oldest first, or
|
|
120
|
+
* `null` when no plugin keeps people here or the one that does cannot walk
|
|
121
|
+
* them by change. The limit is clamped to 1–500. A failed read throws: a
|
|
122
|
+
* connector that skipped a page it could not read would advance past people
|
|
123
|
+
* it never copied.
|
|
124
|
+
*/ export async function pluginPeopleChangedSince(request) {
|
|
125
|
+
const resolved = pluginPersonRecords();
|
|
126
|
+
if (!(resolved == null ? void 0 : resolved.records.changedSince)) return null;
|
|
127
|
+
const limit = Math.min(500, Math.max(1, Math.floor(Number(request.limit) || 0)));
|
|
128
|
+
return await resolved.records.changedSince(_extends({}, request, {
|
|
129
|
+
limit
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
106
132
|
|
|
107
133
|
//# sourceMappingURL=plugin-person-records.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-person-records.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 {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * THE PEOPLE A WORKSPACE KEEPS, asked for by the plugins that do not keep\n * them (AGL-3080).\n *\n * `plugin-contact-capture` is how a silo hands somebody it MET to the plugin\n * that keeps people. This is the other half: a plugin that has an address, or\n * a record the owner handed it, and needs the person behind it.\n *\n * - a flow email asking whether the person agreed to marketing mail;\n * - a campaign filling in the name a merge tag reads;\n * - a refund taking money back off what a customer is worth;\n * - an automation filing somebody under a campaign;\n * - a sales sequence reading the people a rep enrolls.\n *\n * Each of those used to open the owner's collection itself — its path, its\n * address index, which site may see which document — and broke the day the\n * owner changed one. None of it is visible to `check:lib-boundaries`, because\n * a collection name is not an import.\n *\n * So the plugin that keeps people registers ONE service here, and every rule\n * stays the owner's: how an address is keyed, what an alternate address a\n * merge folded in still answers to, which site may see which record, and what\n * a refund or a filing writes.\n *\n * ## What a record is\n *\n * A {@link PluginPersonRecord} is a kind and an id in the owner's own words —\n * the words `plugin-record-routes` addresses the record by — the person's\n * primary address, and the record as the owner stores it. The fields the\n * PLATFORM defines on a person record (the consent basis\n * `marketing-consent.ts` reads, the `emailState` verdict, the `visibleTo`\n * scope) are read off it with the platform's own readers; anything else is\n * the owner's and is documented with its registration.\n *\n * ## Not registered is an answer\n *\n * Every reader answers `null` when no plugin keeps people in this process,\n * which is a workspace with no record system and is not \"nobody found\". A\n * caller treats the two differently: a send with no record system has no\n * basis to read, which is not the same as a person who declined.\n *\n * A single-implementation contract: a workspace keeps one set of people, so\n * a second plugin's service is refused naming both and the incumbent keeps\n * serving. Registered from the owner's server declarations, so it is in place\n * in a process that never loaded the owner's API surface.\n *\n * ## The caller proves who is asking\n *\n * Like the capture and timeline seams, the registry authenticates nobody. A\n * caller decides in its own terms, before it asks, that the read or the write\n * is the workspace's to make.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** A person record, by the owner's word for its kind and its id. */\nexport interface PluginPersonRecordRef {\n /** The owner's word for the record — the kind `plugin-record-routes` addresses it by. */\n kind: string\n id: string\n}\n\n/** One person record, as the plugin that keeps people shares it. */\nexport interface PluginPersonRecord extends PluginPersonRecordRef {\n /** The record's primary address, normalized, or `null` when it holds none. */\n email: string | null\n /**\n * The record as the owner stores it. The platform's own fields on it are\n * read with the platform's readers; the rest is the owner's, documented\n * with its registration.\n */\n data: Readonly<Record<string, unknown>>\n}\n\n/** Who to find by address, and where. */\nexport interface PluginPersonFindRequest {\n /**\n * The organization whose people are searched. A caller that knows only the\n * site names `hostId` instead, and the owner reads the organization off it.\n */\n orgId?: string | null\n /** The site asking. Required with {@link onlyVisibleToSite}. */\n hostId?: string | null\n /** Raw: the owner normalizes it, alternate addresses included. */\n email: unknown\n /**\n * Answer only a record `hostId` may see. A record the address names but the\n * site cannot see is `null`, never a reason to look further — one address\n * names one person.\n */\n onlyVisibleToSite?: boolean\n /**\n * Look at every kind of record the owner keeps a person as, in the owner's\n * order, instead of only the one a known person is held as. A send deciding\n * whether it may mail somebody asks for this: a person not yet qualified is\n * still a person whose answer counts.\n */\n anyKind?: boolean\n}\n\n/** Records to read, by the refs the owner handed out. */\nexport interface PluginPersonReadRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * File a person under containers (`plugin-containers.ts`) — the merchant's\n * own act, through an automation step or an enrollment. Filing is not\n * consent.\n */\nexport interface PluginPersonFileRequest {\n /** The site filing them: the person is filed under the containers as this site holds them. */\n hostId: string\n orgId?: string | null\n record: PluginPersonRecordRef\n /** The container kind, as `plugin-containers.ts` declares it. */\n containerKind: string\n ids: readonly string[]\n}\n\n/**\n * Money handed back to a person a silo took it from: a refund, or a dispute\n * lost. The owner records it beside what the person is worth; it never\n * creates a person to hold it.\n */\nexport interface PluginPersonRefundRequest {\n /** The site the money was taken on. */\n hostId: string\n /** The buyer's address as the sale recorded it. Raw: the owner normalizes it. */\n email: unknown\n /** Cents reversed by THIS refund, never the sale's total. */\n amountCents: number\n /** The silo's own document the money went back on — what a timeline entry points at. */\n refId: string\n /** True only for the refund that moved the sale into fully refunded. */\n closedTheSale: boolean\n /** `chargeback` for a dispute lost; a refund otherwise. */\n reason?: 'refund' | 'chargeback'\n}\n\n/**\n * What became of a refund. `recorded`; `no-email` for a sale that never\n * named its buyer; `no-person` for an address the workspace holds nobody at;\n * `gone` for a person removed between the sale and the refund.\n */\nexport type PluginPersonRefundOutcome = 'recorded' | 'no-email' | 'no-person' | 'gone'\n\n/**\n * The people a saved selection the owner keeps names — a saved view of one of\n * its lists — taken for one site, as the people a sequence or a send works\n * through.\n */\nexport interface PluginPersonViewRequest {\n orgId: string\n /** The site the people are taken for: only records it holds are answered. */\n hostId: string\n /** The view, by the id the owner's record list handed out (`plugin-record-lists`). */\n viewId: string\n /** The member asking: a view they may not list does not exist for them. */\n viewerUid: string\n /** The most people answered. */\n limit: number\n}\n\nexport type PluginPersonViewPeople =\n | {\n ok: true\n people: PluginPersonRecordRef[]\n /** How many the view selects, as far as the owner read it. */\n total: number\n /** The view selects more than `limit`, or more than the owner read. */\n truncated: boolean\n }\n | {\n ok: false\n /**\n * `not-found`: no such view, or not this member's to list.\n * `not-people`: a view of something that is not people.\n * `unsupported`: the view narrows by something the owner cannot apply\n * outside its own list, so taking it would select more people than the\n * view shows; `unsupported` names what, in the owner's words.\n */\n reason: 'not-found' | 'not-people' | 'unsupported'\n unsupported?: string[]\n }\n\n/** Records to ask about, by the refs the owner handed out. */\nexport interface PluginPersonWroteInRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\nexport interface PluginPersonRecords {\n /** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */\n find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>\n /** The records named, in the order asked; `null` for one that is gone. */\n read(request: PluginPersonReadRequest): Promise<Array<PluginPersonRecord | null>>\n /**\n * Files the person under the containers. Optional: an owner that keeps no\n * containers answers nothing. `filed: false` is a record the owner could\n * not find for the site.\n */\n fileUnder?(request: PluginPersonFileRequest): Promise<{ filed: boolean }>\n /** Records money handed back. Optional. Never throws: the money has already moved. */\n recordRefund?(request: PluginPersonRefundRequest): Promise<PluginPersonRefundOutcome>\n /** The people a saved view selects for a site. Optional: an owner that keeps no views answers nothing. */\n peopleInView?(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople>\n /**\n * Whether each person has ever written to the workspace — an email they\n * sent, on their record — in the order asked; `null` for one the owner\n * could not answer. Optional.\n */\n wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>\n}\n\nexport const PLUGIN_PERSON_RECORDS = definePluginServiceContract<PluginPersonRecords>(\n 'core.person-records',\n { multiple: false },\n)\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's service is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginPersonRecords(\n records: PluginPersonRecords,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PERSON_RECORDS, records, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginPersonRecords {\n /** The plugin that keeps people. */\n pluginId: string\n records: PluginPersonRecords\n}\n\n/** The service with its owner, or `null` when no plugin keeps people here. */\nexport function pluginPersonRecords(): ResolvedPluginPersonRecords | null {\n const entry = resolvePluginServices(PLUGIN_PERSON_RECORDS)[0]\n return entry ? { pluginId: entry.pluginId, records: entry.impl } : null\n}\n\n/**\n * The person an address belongs to, or `null` — for nobody found, and for a\n * workspace no plugin keeps people for. A failed read is the owner's to\n * throw and the caller's to decide which way it falls.\n */\nexport async function findPluginPerson(\n request: PluginPersonFindRequest,\n): Promise<PluginPersonRecord | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n return await resolved.records.find(request)\n}\n\n/**\n * The records named, in the order asked, `null` for each that is gone — or\n * `null` whole when no plugin keeps people here, which is not \"all gone\".\n */\nexport async function readPluginPeople(\n request: PluginPersonReadRequest,\n): Promise<Array<PluginPersonRecord | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n if (!request.records.length) return []\n return await resolved.records.read(request)\n}\n\n/**\n * Files the person under the containers through whichever plugin keeps\n * people, or answers `null` when none does or the one that does keeps no\n * containers. A failed write throws, as the caller's own write would have.\n */\nexport async function filePluginPersonUnder(\n request: PluginPersonFileRequest,\n): Promise<{ filed: boolean } | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.fileUnder) return null\n if (!request.ids.length) return { filed: false }\n return await resolved.records.fileUnder(request)\n}\n\n/**\n * Hands a refund to whichever plugin keeps people, or answers `null` when\n * none does. Never throws: every caller has already moved the money and\n * recorded it on its own document, and a person's figure is derived from it.\n */\nexport async function recordPluginPersonRefund(\n request: PluginPersonRefundRequest,\n): Promise<PluginPersonRefundOutcome | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.recordRefund) return null\n try {\n return await resolved.records.recordRefund(request)\n } catch (error) {\n console.error('[person-records] the record system could not record a refund', error)\n return null\n }\n}\n\n/**\n * The people a saved view selects for a site, or `null` when no plugin keeps\n * people here or the one that does keeps no views. A failed read throws.\n */\nexport async function pluginPeopleInView(\n request: PluginPersonViewRequest,\n): Promise<PluginPersonViewPeople | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.peopleInView) return null\n return await resolved.records.peopleInView(request)\n}\n\n/**\n * Whether each person has ever written to the workspace, in the order asked\n * — `null` for each the owner could not answer, and `null` whole when no\n * plugin keeps people here or the one that does cannot say. Never throws: a\n * caller reads an unknown as unknown.\n */\nexport async function pluginPeopleWroteIn(\n request: PluginPersonWroteInRequest,\n): Promise<Array<boolean | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.wroteIn) return null\n if (!request.records.length) return []\n try {\n return await resolved.records.wroteIn(request)\n } catch (error) {\n console.error('[person-records] the record system could not say who wrote in', error)\n return null\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PERSON_RECORDS","multiple","registerPluginPersonRecords","records","options","pluginId","pluginPersonRecords","entry","impl","findPluginPerson","request","resolved","find","readPluginPeople","length","read","filePluginPersonUnder","fileUnder","ids","filed","recordPluginPersonRefund","recordRefund","error","console","pluginPeopleInView","peopleInView","pluginPeopleWroteIn","wroteIn"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2N1B,OAAO,MAAMC,wBAAwBH,4BACnC,uBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,4BACdC,OAA4B,EAC5BC,OAA+B;IAE/BN,sBAAsBE,uBAAuBG,SAAS,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,4EAA4E,GAC5E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,sBAAsB,CAAC,EAAE;IAC7D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,SAASI,MAAMC,IAAI;IAAC,IAAI;AACrE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpBC,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,OAAO,MAAMA,SAASR,OAAO,CAACS,IAAI,CAACF;AACrC;AAEA;;;CAGC,GACD,OAAO,eAAeG,iBACpBH,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI,CAACD,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,OAAO,MAAMH,SAASR,OAAO,CAACY,IAAI,CAACL;AACrC;AAEA;;;;CAIC,GACD,OAAO,eAAeM,sBACpBN,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACc,SAAS,GAAE,OAAO;IACzC,IAAI,CAACP,QAAQQ,GAAG,CAACJ,MAAM,EAAE,OAAO;QAAEK,OAAO;IAAM;IAC/C,OAAO,MAAMR,SAASR,OAAO,CAACc,SAAS,CAACP;AAC1C;AAEA;;;;CAIC,GACD,OAAO,eAAeU,yBACpBV,OAAkC;IAElC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACkB,YAAY,GAAE,OAAO;IAC5C,IAAI;QACF,OAAO,MAAMV,SAASR,OAAO,CAACkB,YAAY,CAACX;IAC7C,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,gEAAgEA;QAC9E,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeE,mBACpBd,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACsB,YAAY,GAAE,OAAO;IAC5C,OAAO,MAAMd,SAASR,OAAO,CAACsB,YAAY,CAACf;AAC7C;AAEA;;;;;CAKC,GACD,OAAO,eAAegB,oBACpBhB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACwB,OAAO,GAAE,OAAO;IACvC,IAAI,CAACjB,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,IAAI;QACF,OAAO,MAAMH,SAASR,OAAO,CAACwB,OAAO,CAACjB;IACxC,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,iEAAiEA;QAC/E,OAAO;IACT;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-person-records.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 {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * THE PEOPLE A WORKSPACE KEEPS, asked for by the plugins that do not keep\n * them (AGL-3080).\n *\n * `plugin-contact-capture` is how a silo hands somebody it MET to the plugin\n * that keeps people. This is the other half: a plugin that has an address, or\n * a record the owner handed it, and needs the person behind it.\n *\n * - a flow email asking whether the person agreed to marketing mail;\n * - a campaign filling in the name a merge tag reads;\n * - a refund taking money back off what a customer is worth;\n * - an automation filing somebody under a campaign;\n * - a sales sequence reading the people a rep enrolls.\n *\n * Each of those used to open the owner's collection itself — its path, its\n * address index, which site may see which document — and broke the day the\n * owner changed one. None of it is visible to `check:lib-boundaries`, because\n * a collection name is not an import.\n *\n * So the plugin that keeps people registers ONE service here, and every rule\n * stays the owner's: how an address is keyed, what an alternate address a\n * merge folded in still answers to, which site may see which record, and what\n * a refund or a filing writes.\n *\n * ## What a record is\n *\n * A {@link PluginPersonRecord} is a kind and an id in the owner's own words —\n * the words `plugin-record-routes` addresses the record by — the person's\n * primary address, and the record as the owner stores it. The fields the\n * PLATFORM defines on a person record (the consent basis\n * `marketing-consent.ts` reads, the `emailState` verdict, the `visibleTo`\n * scope) are read off it with the platform's own readers; anything else is\n * the owner's and is documented with its registration.\n *\n * ## Not registered is an answer\n *\n * Every reader answers `null` when no plugin keeps people in this process,\n * which is a workspace with no record system and is not \"nobody found\". A\n * caller treats the two differently: a send with no record system has no\n * basis to read, which is not the same as a person who declined.\n *\n * A single-implementation contract: a workspace keeps one set of people, so\n * a second plugin's service is refused naming both and the incumbent keeps\n * serving. Registered from the owner's server declarations, so it is in place\n * in a process that never loaded the owner's API surface.\n *\n * ## The caller proves who is asking\n *\n * Like the capture and timeline seams, the registry authenticates nobody. A\n * caller decides in its own terms, before it asks, that the read or the write\n * is the workspace's to make.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** A person record, by the owner's word for its kind and its id. */\nexport interface PluginPersonRecordRef {\n /** The owner's word for the record — the kind `plugin-record-routes` addresses it by. */\n kind: string\n id: string\n}\n\n/** One person record, as the plugin that keeps people shares it. */\nexport interface PluginPersonRecord extends PluginPersonRecordRef {\n /** The record's primary address, normalized, or `null` when it holds none. */\n email: string | null\n /**\n * The record as the owner stores it. The platform's own fields on it are\n * read with the platform's readers; the rest is the owner's, documented\n * with its registration.\n */\n data: Readonly<Record<string, unknown>>\n}\n\n/** Who to find by address, and where. */\nexport interface PluginPersonFindRequest {\n /**\n * The organization whose people are searched. A caller that knows only the\n * site names `hostId` instead, and the owner reads the organization off it.\n */\n orgId?: string | null\n /** The site asking. Required with {@link onlyVisibleToSite}. */\n hostId?: string | null\n /** Raw: the owner normalizes it, alternate addresses included. */\n email: unknown\n /**\n * Answer only a record `hostId` may see. A record the address names but the\n * site cannot see is `null`, never a reason to look further — one address\n * names one person.\n */\n onlyVisibleToSite?: boolean\n /**\n * Look at every kind of record the owner keeps a person as, in the owner's\n * order, instead of only the one a known person is held as. A send deciding\n * whether it may mail somebody asks for this: a person not yet qualified is\n * still a person whose answer counts.\n */\n anyKind?: boolean\n}\n\n/** Records to read, by the refs the owner handed out. */\nexport interface PluginPersonReadRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * File a person under containers (`plugin-containers.ts`) — the merchant's\n * own act, through an automation step or an enrollment. Filing is not\n * consent.\n */\nexport interface PluginPersonFileRequest {\n /** The site filing them: the person is filed under the containers as this site holds them. */\n hostId: string\n orgId?: string | null\n record: PluginPersonRecordRef\n /** The container kind, as `plugin-containers.ts` declares it. */\n containerKind: string\n ids: readonly string[]\n}\n\n/**\n * Money handed back to a person a silo took it from: a refund, or a dispute\n * lost. The owner records it beside what the person is worth; it never\n * creates a person to hold it.\n */\nexport interface PluginPersonRefundRequest {\n /** The site the money was taken on. */\n hostId: string\n /** The buyer's address as the sale recorded it. Raw: the owner normalizes it. */\n email: unknown\n /** Cents reversed by THIS refund, never the sale's total. */\n amountCents: number\n /** The silo's own document the money went back on — what a timeline entry points at. */\n refId: string\n /** True only for the refund that moved the sale into fully refunded. */\n closedTheSale: boolean\n /** `chargeback` for a dispute lost; a refund otherwise. */\n reason?: 'refund' | 'chargeback'\n}\n\n/**\n * What became of a refund. `recorded`; `no-email` for a sale that never\n * named its buyer; `no-person` for an address the workspace holds nobody at;\n * `gone` for a person removed between the sale and the refund.\n */\nexport type PluginPersonRefundOutcome = 'recorded' | 'no-email' | 'no-person' | 'gone'\n\n/**\n * The people a saved selection the owner keeps names — a saved view of one of\n * its lists — taken for one site, as the people a sequence or a send works\n * through.\n */\nexport interface PluginPersonViewRequest {\n orgId: string\n /** The site the people are taken for: only records it holds are answered. */\n hostId: string\n /** The view, by the id the owner's record list handed out (`plugin-record-lists`). */\n viewId: string\n /** The member asking: a view they may not list does not exist for them. */\n viewerUid: string\n /** The most people answered. */\n limit: number\n}\n\nexport type PluginPersonViewPeople =\n | {\n ok: true\n people: PluginPersonRecordRef[]\n /** How many the view selects, as far as the owner read it. */\n total: number\n /** The view selects more than `limit`, or more than the owner read. */\n truncated: boolean\n }\n | {\n ok: false\n /**\n * `not-found`: no such view, or not this member's to list.\n * `not-people`: a view of something that is not people.\n * `unsupported`: the view narrows by something the owner cannot apply\n * outside its own list, so taking it would select more people than the\n * view shows; `unsupported` names what, in the owner's words.\n */\n reason: 'not-found' | 'not-people' | 'unsupported'\n unsupported?: string[]\n }\n\n/**\n * People a site may see whose name, address or phone starts with what was\n * typed (AGL-3609) — a cashier looking a customer up at the register, a\n * picker anywhere a person is chosen by hand. One word: the owner answers it\n * from its own search index and never by scanning its records.\n */\nexport interface PluginPersonSearchRequest {\n /** The site searching: only records it may see are answered. */\n hostId: string\n /** The organization, when the caller knows it; the owner reads it off `hostId` otherwise. */\n orgId?: string | null\n /** What was typed. Raw: the owner normalizes it, and reads a run of digits as a phone. */\n text: string\n /** The most people answered. The owner may answer fewer. */\n limit: number\n}\n\n/** Records to ask about, by the refs the owner handed out. */\nexport interface PluginPersonWroteInRequest {\n orgId: string\n records: readonly PluginPersonRecordRef[]\n}\n\n/**\n * The people one site holds, oldest change first, after a cursor (AGL-3639)\n * — what a connector that mirrors a workspace's people into another system\n * walks, once from the start as a backfill and then from where it stopped.\n */\nexport interface PluginPersonChangesRequest {\n /** The site the people are taken for: only records it may see are answered. */\n hostId: string\n orgId?: string | null\n /**\n * Where the last page stopped, exactly as the owner handed it out, or\n * `null` to start from the earliest change. Opaque: only the owner reads\n * it, so it can hold a change time finer than a millisecond and the id\n * that breaks a tie, and a walk neither skips a person nor repeats one.\n */\n after: string | null\n /** The most people answered; the owner may answer fewer. */\n limit: number\n}\n\n/**\n * A person as a connector copies them out: the platform's own fields on the\n * record (`data`, read with the platform's readers — the consent basis\n * above all), and the owner's profile of them as this site holds it, in the\n * platform's words rather than the owner's storage.\n */\nexport interface PluginPersonChange extends PluginPersonRecord {\n /** When the record last changed, epoch ms. */\n changedAtMs: number\n profile: {\n name: string | null\n phone: string | null\n tags: string[]\n /** What the person has spent at this site, integer cents, when the owner keeps it. */\n lifetimeValueCents: number | null\n ordersCount: number | null\n }\n}\n\nexport interface PluginPersonChangesPage {\n people: PluginPersonChange[]\n /**\n * Where the next page starts — the cursor to store. It moves past every\n * record the owner READ, including one it left out for the site, so a page\n * can be empty and still move on. `null` when nothing changed after the\n * cursor: the walk has reached the latest change, and the caller keeps the\n * cursor it asked with.\n */\n next: string | null\n}\n\nexport interface PluginPersonRecords {\n /** The person an address belongs to, or `null`. A read; may throw, and the caller decides which way a failure falls. */\n find(request: PluginPersonFindRequest): Promise<PluginPersonRecord | null>\n /** The records named, in the order asked; `null` for one that is gone. */\n read(request: PluginPersonReadRequest): Promise<Array<PluginPersonRecord | null>>\n /**\n * Files the person under the containers. Optional: an owner that keeps no\n * containers answers nothing. `filed: false` is a record the owner could\n * not find for the site.\n */\n fileUnder?(request: PluginPersonFileRequest): Promise<{ filed: boolean }>\n /** Records money handed back. Optional. Never throws: the money has already moved. */\n recordRefund?(request: PluginPersonRefundRequest): Promise<PluginPersonRefundOutcome>\n /** The people a saved view selects for a site. Optional: an owner that keeps no views answers nothing. */\n peopleInView?(request: PluginPersonViewRequest): Promise<PluginPersonViewPeople>\n /**\n * Whether each person has ever written to the workspace — an email they\n * sent, on their record — in the order asked; `null` for one the owner\n * could not answer. Optional.\n */\n wroteIn?(request: PluginPersonWroteInRequest): Promise<Array<boolean | null>>\n /**\n * People a site may see that match one typed word, best first. Optional:\n * an owner that keeps no search index answers nothing. A read; may throw.\n */\n search?(request: PluginPersonSearchRequest): Promise<PluginPersonRecord[]>\n /**\n * The people a site holds that changed after a cursor, oldest first.\n * Optional: an owner that cannot walk its records by change answers\n * nothing. A failed read throws.\n */\n changedSince?(request: PluginPersonChangesRequest): Promise<PluginPersonChangesPage>\n}\n\nexport const PLUGIN_PERSON_RECORDS = definePluginServiceContract<PluginPersonRecords>(\n 'core.person-records',\n { multiple: false },\n)\n\n/**\n * Registers the plugin that keeps people. The owner is the loader's marker\n * when a register fn is running, else `options.pluginId`; with neither the\n * registration throws, and a second plugin's service is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginPersonRecords(\n records: PluginPersonRecords,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PERSON_RECORDS, records, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nexport interface ResolvedPluginPersonRecords {\n /** The plugin that keeps people. */\n pluginId: string\n records: PluginPersonRecords\n}\n\n/** The service with its owner, or `null` when no plugin keeps people here. */\nexport function pluginPersonRecords(): ResolvedPluginPersonRecords | null {\n const entry = resolvePluginServices(PLUGIN_PERSON_RECORDS)[0]\n return entry ? { pluginId: entry.pluginId, records: entry.impl } : null\n}\n\n/**\n * The person an address belongs to, or `null` — for nobody found, and for a\n * workspace no plugin keeps people for. A failed read is the owner's to\n * throw and the caller's to decide which way it falls.\n */\nexport async function findPluginPerson(\n request: PluginPersonFindRequest,\n): Promise<PluginPersonRecord | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n return await resolved.records.find(request)\n}\n\n/**\n * The records named, in the order asked, `null` for each that is gone — or\n * `null` whole when no plugin keeps people here, which is not \"all gone\".\n */\nexport async function readPluginPeople(\n request: PluginPersonReadRequest,\n): Promise<Array<PluginPersonRecord | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved) return null\n if (!request.records.length) return []\n return await resolved.records.read(request)\n}\n\n/**\n * Files the person under the containers through whichever plugin keeps\n * people, or answers `null` when none does or the one that does keeps no\n * containers. A failed write throws, as the caller's own write would have.\n */\nexport async function filePluginPersonUnder(\n request: PluginPersonFileRequest,\n): Promise<{ filed: boolean } | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.fileUnder) return null\n if (!request.ids.length) return { filed: false }\n return await resolved.records.fileUnder(request)\n}\n\n/**\n * Hands a refund to whichever plugin keeps people, or answers `null` when\n * none does. Never throws: every caller has already moved the money and\n * recorded it on its own document, and a person's figure is derived from it.\n */\nexport async function recordPluginPersonRefund(\n request: PluginPersonRefundRequest,\n): Promise<PluginPersonRefundOutcome | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.recordRefund) return null\n try {\n return await resolved.records.recordRefund(request)\n } catch (error) {\n console.error('[person-records] the record system could not record a refund', error)\n return null\n }\n}\n\n/**\n * The people a saved view selects for a site, or `null` when no plugin keeps\n * people here or the one that does keeps no views. A failed read throws.\n */\nexport async function pluginPeopleInView(\n request: PluginPersonViewRequest,\n): Promise<PluginPersonViewPeople | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.peopleInView) return null\n return await resolved.records.peopleInView(request)\n}\n\n/**\n * Whether each person has ever written to the workspace, in the order asked\n * — `null` for each the owner could not answer, and `null` whole when no\n * plugin keeps people here or the one that does cannot say. Never throws: a\n * caller reads an unknown as unknown.\n */\nexport async function pluginPeopleWroteIn(\n request: PluginPersonWroteInRequest,\n): Promise<Array<boolean | null> | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.wroteIn) return null\n if (!request.records.length) return []\n try {\n return await resolved.records.wroteIn(request)\n } catch (error) {\n console.error('[person-records] the record system could not say who wrote in', error)\n return null\n }\n}\n\n/**\n * People a site may see matching what was typed, or `null` when no plugin\n * keeps people here or the one that does keeps no search — which is not\n * \"nobody matched\". Blank text answers no one without asking. A failed read\n * throws, and the caller decides how to say so.\n */\nexport async function searchPluginPeople(\n request: PluginPersonSearchRequest,\n): Promise<PluginPersonRecord[] | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.search) return null\n if (!String(request.text ?? '').trim() || !(request.limit > 0)) return []\n return await resolved.records.search(request)\n}\n\n/**\n * The people a site holds that changed after a cursor, oldest first, or\n * `null` when no plugin keeps people here or the one that does cannot walk\n * them by change. The limit is clamped to 1–500. A failed read throws: a\n * connector that skipped a page it could not read would advance past people\n * it never copied.\n */\nexport async function pluginPeopleChangedSince(\n request: PluginPersonChangesRequest,\n): Promise<PluginPersonChangesPage | null> {\n const resolved = pluginPersonRecords()\n if (!resolved?.records.changedSince) return null\n const limit = Math.min(500, Math.max(1, Math.floor(Number(request.limit) || 0)))\n return await resolved.records.changedSince({ ...request, limit })\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PERSON_RECORDS","multiple","registerPluginPersonRecords","records","options","pluginId","pluginPersonRecords","entry","impl","findPluginPerson","request","resolved","find","readPluginPeople","length","read","filePluginPersonUnder","fileUnder","ids","filed","recordPluginPersonRefund","recordRefund","error","console","pluginPeopleInView","peopleInView","pluginPeopleWroteIn","wroteIn","searchPluginPeople","search","String","text","trim","limit","pluginPeopleChangedSince","changedSince","Math","min","max","floor","Number"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA0S1B,OAAO,MAAMC,wBAAwBH,4BACnC,uBACA;IAAEI,UAAU;AAAM,GACnB;AAED;;;;;CAKC,GACD,OAAO,SAASC,4BACdC,OAA4B,EAC5BC,OAA+B;IAE/BN,sBAAsBE,uBAAuBG,SAAS,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAQA,4EAA4E,GAC5E,OAAO,SAASC;IACd,MAAMC,QAAQR,sBAAsBC,sBAAsB,CAAC,EAAE;IAC7D,OAAOO,QAAQ;QAAEF,UAAUE,MAAMF,QAAQ;QAAEF,SAASI,MAAMC,IAAI;IAAC,IAAI;AACrE;AAEA;;;;CAIC,GACD,OAAO,eAAeC,iBACpBC,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,OAAO,MAAMA,SAASR,OAAO,CAACS,IAAI,CAACF;AACrC;AAEA;;;CAGC,GACD,OAAO,eAAeG,iBACpBH,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,CAACK,UAAU,OAAO;IACtB,IAAI,CAACD,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,OAAO,MAAMH,SAASR,OAAO,CAACY,IAAI,CAACL;AACrC;AAEA;;;;CAIC,GACD,OAAO,eAAeM,sBACpBN,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACc,SAAS,GAAE,OAAO;IACzC,IAAI,CAACP,QAAQQ,GAAG,CAACJ,MAAM,EAAE,OAAO;QAAEK,OAAO;IAAM;IAC/C,OAAO,MAAMR,SAASR,OAAO,CAACc,SAAS,CAACP;AAC1C;AAEA;;;;CAIC,GACD,OAAO,eAAeU,yBACpBV,OAAkC;IAElC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACkB,YAAY,GAAE,OAAO;IAC5C,IAAI;QACF,OAAO,MAAMV,SAASR,OAAO,CAACkB,YAAY,CAACX;IAC7C,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,gEAAgEA;QAC9E,OAAO;IACT;AACF;AAEA;;;CAGC,GACD,OAAO,eAAeE,mBACpBd,OAAgC;IAEhC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACsB,YAAY,GAAE,OAAO;IAC5C,OAAO,MAAMd,SAASR,OAAO,CAACsB,YAAY,CAACf;AAC7C;AAEA;;;;;CAKC,GACD,OAAO,eAAegB,oBACpBhB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACwB,OAAO,GAAE,OAAO;IACvC,IAAI,CAACjB,QAAQP,OAAO,CAACW,MAAM,EAAE,OAAO,EAAE;IACtC,IAAI;QACF,OAAO,MAAMH,SAASR,OAAO,CAACwB,OAAO,CAACjB;IACxC,EAAE,OAAOY,OAAO;QACdC,QAAQD,KAAK,CAAC,iEAAiEA;QAC/E,OAAO;IACT;AACF;AAEA;;;;;CAKC,GACD,OAAO,eAAeM,mBACpBlB,OAAkC;QAItBA;IAFZ,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAAC0B,MAAM,GAAE,OAAO;IACtC,IAAI,CAACC,QAAOpB,gBAAAA,QAAQqB,IAAI,YAAZrB,gBAAgB,IAAIsB,IAAI,MAAM,CAAEtB,CAAAA,QAAQuB,KAAK,GAAG,CAAA,GAAI,OAAO,EAAE;IACzE,OAAO,MAAMtB,SAASR,OAAO,CAAC0B,MAAM,CAACnB;AACvC;AAEA;;;;;;CAMC,GACD,OAAO,eAAewB,yBACpBxB,OAAmC;IAEnC,MAAMC,WAAWL;IACjB,IAAI,EAACK,4BAAAA,SAAUR,OAAO,CAACgC,YAAY,GAAE,OAAO;IAC5C,MAAMF,QAAQG,KAAKC,GAAG,CAAC,KAAKD,KAAKE,GAAG,CAAC,GAAGF,KAAKG,KAAK,CAACC,OAAO9B,QAAQuB,KAAK,KAAK;IAC5E,OAAO,MAAMtB,SAASR,OAAO,CAACgC,YAAY,CAAC,aAAKzB;QAASuB;;AAC3D"}
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* A site's sellable catalog, read whole and in pages by a plugin that does
|
|
19
|
+
* not own it (AGL-3637).
|
|
20
|
+
*
|
|
21
|
+
* A product feed, a marketplace listing sync and an ad catalog all need the
|
|
22
|
+
* same thing: every offer a store sells, with the facts a shopping channel
|
|
23
|
+
* asks for — price in the store's currency, stock, photos, identifiers,
|
|
24
|
+
* shipping. The plugin that keeps the products answers it here, so a reader
|
|
25
|
+
* never learns where products are stored, how variants are modeled or what a
|
|
26
|
+
* deleted product looks like.
|
|
27
|
+
*
|
|
28
|
+
* ## One OFFER per sellable configuration
|
|
29
|
+
*
|
|
30
|
+
* A product with one configuration is one offer whose `id` is the product's
|
|
31
|
+
* own id. A product with several (sizes, colors) is one offer per
|
|
32
|
+
* configuration, each with its own `id` and the product's id as `groupId`,
|
|
33
|
+
* which is what every shopping channel calls the variant group.
|
|
34
|
+
*
|
|
35
|
+
* ## Money is integer minor units in the store's currency
|
|
36
|
+
*
|
|
37
|
+
* `priceMinor` and every other amount are integers (cents for USD) in
|
|
38
|
+
* {@link CatalogStore.currency}. A reader formats them; it never assumes a
|
|
39
|
+
* currency.
|
|
40
|
+
*
|
|
41
|
+
* ## Pages, not a list
|
|
42
|
+
*
|
|
43
|
+
* A store may hold far more products than one read should load, so the
|
|
44
|
+
* catalog is walked in pages: `page()` answers up to about `limit` offers and
|
|
45
|
+
* a `nextCursor` to continue from, `null` on the last page. A cursor is the
|
|
46
|
+
* owner's own opaque string; a reader passes it back untouched.
|
|
47
|
+
*
|
|
48
|
+
* ## One owner
|
|
49
|
+
*
|
|
50
|
+
* A slot, like the tax profile: a site has one catalog, so a second plugin's
|
|
51
|
+
* registration is refused and the incumbent keeps serving. No owner
|
|
52
|
+
* registered is an ordinary answer — {@link pluginProductCatalog} returns
|
|
53
|
+
* `undefined` and a reader serves nothing.
|
|
54
|
+
*/
|
|
55
|
+
/** The store an offer belongs to, as a channel needs it. */
|
|
56
|
+
export interface CatalogStore {
|
|
57
|
+
hostId: string;
|
|
58
|
+
/** The store's name, as its site calls itself. */
|
|
59
|
+
name: string;
|
|
60
|
+
/**
|
|
61
|
+
* The origin the store's pages are served on (`https://shop.example.com`),
|
|
62
|
+
* or `null` when the site has no address yet.
|
|
63
|
+
*/
|
|
64
|
+
origin: string | null;
|
|
65
|
+
/** ISO 4217, upper case. Every amount in the catalog is in it. */
|
|
66
|
+
currency: string;
|
|
67
|
+
/**
|
|
68
|
+
* Whether `/products/{slug}` pages are served. When `false`, every offer's
|
|
69
|
+
* `path` answers 404, and a channel will refuse the offers.
|
|
70
|
+
*/
|
|
71
|
+
productPagesServed: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Countries the store's checkout prices shipping to by a live carrier
|
|
74
|
+
* quote rather than its own table (ISO 3166-1 alpha-2, upper case). An
|
|
75
|
+
* offer carries no {@link CatalogShippingPrice} for them, because the price
|
|
76
|
+
* depends on the address; a channel that calculates carrier rates itself
|
|
77
|
+
* prices them from the offer's weight and dimensions.
|
|
78
|
+
*/
|
|
79
|
+
carrierPricedCountries: string[];
|
|
80
|
+
}
|
|
81
|
+
export type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder';
|
|
82
|
+
export type CatalogCondition = 'new' | 'refurbished' | 'used';
|
|
83
|
+
export type CatalogProductKind = 'physical' | 'digital' | 'service';
|
|
84
|
+
/** What one offer costs to ship to one country, cheapest rate first chosen. */
|
|
85
|
+
export interface CatalogShippingPrice {
|
|
86
|
+
/** ISO 3166-1 alpha-2, upper case. */
|
|
87
|
+
country: string;
|
|
88
|
+
/** The rate's name as the shopper sees it at checkout. */
|
|
89
|
+
service: string;
|
|
90
|
+
priceMinor: number;
|
|
91
|
+
}
|
|
92
|
+
/** One sellable configuration of a product. */
|
|
93
|
+
export interface CatalogOffer {
|
|
94
|
+
/** Stable, unique within the store: the product id, or product and variant. */
|
|
95
|
+
id: string;
|
|
96
|
+
/** The product's id: the variant group every configuration shares. */
|
|
97
|
+
groupId: string;
|
|
98
|
+
/** Whether the product has more than one configuration. */
|
|
99
|
+
hasVariants: boolean;
|
|
100
|
+
productId: string;
|
|
101
|
+
variantId: string;
|
|
102
|
+
/** The product's name. */
|
|
103
|
+
productName: string;
|
|
104
|
+
/** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */
|
|
105
|
+
title: string;
|
|
106
|
+
/** Plain text: no markup. May be empty. */
|
|
107
|
+
description: string;
|
|
108
|
+
/** Site-relative path of the product page, e.g. `/products/tee`. */
|
|
109
|
+
path: string;
|
|
110
|
+
/** Absolute URL of the photo shown for this configuration. */
|
|
111
|
+
imageUrl?: string;
|
|
112
|
+
/** Absolute URLs of the product's other photos, in order. */
|
|
113
|
+
additionalImageUrls: string[];
|
|
114
|
+
/** What the shopper pays, before any sale: the regular price. */
|
|
115
|
+
priceMinor: number;
|
|
116
|
+
/** A lower price the offer is on sale for now; absent when not on sale. */
|
|
117
|
+
salePriceMinor?: number;
|
|
118
|
+
availability: CatalogAvailability;
|
|
119
|
+
/** Units in stock, or `null` when stock is not tracked. */
|
|
120
|
+
quantity: number | null;
|
|
121
|
+
kind: CatalogProductKind;
|
|
122
|
+
/** Whether the product is sold only as a recurring subscription. */
|
|
123
|
+
subscriptionOnly: boolean;
|
|
124
|
+
/** What the merchant entered for shopping channels; absent when they did not. */
|
|
125
|
+
condition?: CatalogCondition;
|
|
126
|
+
brand?: string;
|
|
127
|
+
/** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */
|
|
128
|
+
gtin?: string;
|
|
129
|
+
mpn?: string;
|
|
130
|
+
/** A Google product taxonomy id or full path, as entered. */
|
|
131
|
+
googleProductCategory?: string;
|
|
132
|
+
/** The store's own category path, e.g. `Apparel > Shirts`. */
|
|
133
|
+
productType?: string;
|
|
134
|
+
/** The configuration's choices, by option name. */
|
|
135
|
+
options: Readonly<Record<string, string>>;
|
|
136
|
+
/** The choice of an option named like a color, when there is one. */
|
|
137
|
+
color?: string;
|
|
138
|
+
/** The choice of an option named like a size, when there is one. */
|
|
139
|
+
size?: string;
|
|
140
|
+
weightGrams?: number;
|
|
141
|
+
/** One unit's packed size, in centimeters, when the merchant entered it. */
|
|
142
|
+
dimensionsCm?: {
|
|
143
|
+
length: number;
|
|
144
|
+
width: number;
|
|
145
|
+
height: number;
|
|
146
|
+
};
|
|
147
|
+
/** What it costs to ship one unit, per country the store ships to. */
|
|
148
|
+
shipping: CatalogShippingPrice[];
|
|
149
|
+
/** When the product last changed, epoch ms; absent when unknown. */
|
|
150
|
+
updatedAtMs?: number;
|
|
151
|
+
}
|
|
152
|
+
export interface CatalogPage {
|
|
153
|
+
offers: CatalogOffer[];
|
|
154
|
+
/** Where the next page starts, or `null` when this is the last one. */
|
|
155
|
+
nextCursor: string | null;
|
|
156
|
+
}
|
|
157
|
+
export interface PluginProductCatalog {
|
|
158
|
+
/** The store, or `null` for a site that does not sell. */
|
|
159
|
+
store(hostId: string): Promise<CatalogStore | null>;
|
|
160
|
+
/**
|
|
161
|
+
* One page of the store's live, listed offers: never a draft, archived or
|
|
162
|
+
* deleted product. About `limit` offers — a product's configurations are
|
|
163
|
+
* never split across pages, so a page may run over by one product's worth.
|
|
164
|
+
*/
|
|
165
|
+
page(request: {
|
|
166
|
+
hostId: string;
|
|
167
|
+
cursor?: string | null;
|
|
168
|
+
limit: number;
|
|
169
|
+
}): Promise<CatalogPage>;
|
|
170
|
+
}
|
|
171
|
+
export declare const PLUGIN_PRODUCT_CATALOG: import("./plugin-services").PluginServiceContract<PluginProductCatalog>;
|
|
172
|
+
/** Registers the store catalog. A second plugin's is refused, naming both. */
|
|
173
|
+
export declare function registerPluginProductCatalog(catalog: PluginProductCatalog, options?: {
|
|
174
|
+
pluginId?: string;
|
|
175
|
+
}): void;
|
|
176
|
+
/** The registered catalog, or `undefined` when no plugin sells. */
|
|
177
|
+
export declare function pluginProductCatalog(): PluginProductCatalog | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* The plugin that publishes the catalog to shopping channels, as the
|
|
180
|
+
* catalog's owner hands it an address of its own (AGL-3637).
|
|
181
|
+
*
|
|
182
|
+
* Before any plugin published feeds, the plugin that keeps the products
|
|
183
|
+
* served one feed itself, and merchants pasted that address into their
|
|
184
|
+
* channel. The address stays the owner's — a request for it reaches the
|
|
185
|
+
* owner's routes — but the feed is written by whoever publishes the catalog
|
|
186
|
+
* now, so the owner answers it from here. Nobody registered is an ordinary
|
|
187
|
+
* answer: {@link pluginCatalogFeed} returns `undefined` and the owner
|
|
188
|
+
* answers 404.
|
|
189
|
+
*/
|
|
190
|
+
export interface PluginCatalogFeed {
|
|
191
|
+
/**
|
|
192
|
+
* Answers a request for the owner's pre-existing feed address for the site
|
|
193
|
+
* `hostId`: the feed, a 404 when the site's publisher has retired the
|
|
194
|
+
* address or does not publish, or a 503 to try later.
|
|
195
|
+
*/
|
|
196
|
+
serveLegacyFeed(request: Request, hostId: string): Promise<Response>;
|
|
197
|
+
}
|
|
198
|
+
export declare const PLUGIN_CATALOG_FEED: import("./plugin-services").PluginServiceContract<PluginCatalogFeed>;
|
|
199
|
+
/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */
|
|
200
|
+
export declare function registerPluginCatalogFeed(feed: PluginCatalogFeed, options?: {
|
|
201
|
+
pluginId?: string;
|
|
202
|
+
}): void;
|
|
203
|
+
/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */
|
|
204
|
+
export declare function pluginCatalogFeed(): PluginCatalogFeed | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
18
|
+
export const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract('core.product-catalog', {
|
|
19
|
+
multiple: false
|
|
20
|
+
});
|
|
21
|
+
/** Registers the store catalog. A second plugin's is refused, naming both. */ export function registerPluginProductCatalog(catalog, options) {
|
|
22
|
+
registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
23
|
+
pluginId: options.pluginId
|
|
24
|
+
} : {}));
|
|
25
|
+
}
|
|
26
|
+
/** The registered catalog, or `undefined` when no plugin sells. */ export function pluginProductCatalog() {
|
|
27
|
+
var _resolvePluginServices_;
|
|
28
|
+
return (_resolvePluginServices_ = resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]) == null ? void 0 : _resolvePluginServices_.impl;
|
|
29
|
+
}
|
|
30
|
+
export const PLUGIN_CATALOG_FEED = definePluginServiceContract('core.catalog-feed', {
|
|
31
|
+
multiple: false
|
|
32
|
+
});
|
|
33
|
+
/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */ export function registerPluginCatalogFeed(feed, options) {
|
|
34
|
+
registerPluginService(PLUGIN_CATALOG_FEED, feed, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
35
|
+
pluginId: options.pluginId
|
|
36
|
+
} : {}));
|
|
37
|
+
}
|
|
38
|
+
/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */ export function pluginCatalogFeed() {
|
|
39
|
+
var _resolvePluginServices_;
|
|
40
|
+
return (_resolvePluginServices_ = resolvePluginServices(PLUGIN_CATALOG_FEED)[0]) == null ? void 0 : _resolvePluginServices_.impl;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=plugin-product-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-catalog.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 {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A site's sellable catalog, read whole and in pages by a plugin that does\n * not own it (AGL-3637).\n *\n * A product feed, a marketplace listing sync and an ad catalog all need the\n * same thing: every offer a store sells, with the facts a shopping channel\n * asks for — price in the store's currency, stock, photos, identifiers,\n * shipping. The plugin that keeps the products answers it here, so a reader\n * never learns where products are stored, how variants are modeled or what a\n * deleted product looks like.\n *\n * ## One OFFER per sellable configuration\n *\n * A product with one configuration is one offer whose `id` is the product's\n * own id. A product with several (sizes, colors) is one offer per\n * configuration, each with its own `id` and the product's id as `groupId`,\n * which is what every shopping channel calls the variant group.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` and every other amount are integers (cents for USD) in\n * {@link CatalogStore.currency}. A reader formats them; it never assumes a\n * currency.\n *\n * ## Pages, not a list\n *\n * A store may hold far more products than one read should load, so the\n * catalog is walked in pages: `page()` answers up to about `limit` offers and\n * a `nextCursor` to continue from, `null` on the last page. A cursor is the\n * owner's own opaque string; a reader passes it back untouched.\n *\n * ## One owner\n *\n * A slot, like the tax profile: a site has one catalog, so a second plugin's\n * registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductCatalog} returns\n * `undefined` and a reader serves nothing.\n */\n\n/** The store an offer belongs to, as a channel needs it. */\nexport interface CatalogStore {\n hostId: string\n /** The store's name, as its site calls itself. */\n name: string\n /**\n * The origin the store's pages are served on (`https://shop.example.com`),\n * or `null` when the site has no address yet.\n */\n origin: string | null\n /** ISO 4217, upper case. Every amount in the catalog is in it. */\n currency: string\n /**\n * Whether `/products/{slug}` pages are served. When `false`, every offer's\n * `path` answers 404, and a channel will refuse the offers.\n */\n productPagesServed: boolean\n /**\n * Countries the store's checkout prices shipping to by a live carrier\n * quote rather than its own table (ISO 3166-1 alpha-2, upper case). An\n * offer carries no {@link CatalogShippingPrice} for them, because the price\n * depends on the address; a channel that calculates carrier rates itself\n * prices them from the offer's weight and dimensions.\n */\n carrierPricedCountries: string[]\n}\n\nexport type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder'\nexport type CatalogCondition = 'new' | 'refurbished' | 'used'\nexport type CatalogProductKind = 'physical' | 'digital' | 'service'\n\n/** What one offer costs to ship to one country, cheapest rate first chosen. */\nexport interface CatalogShippingPrice {\n /** ISO 3166-1 alpha-2, upper case. */\n country: string\n /** The rate's name as the shopper sees it at checkout. */\n service: string\n priceMinor: number\n}\n\n/** One sellable configuration of a product. */\nexport interface CatalogOffer {\n /** Stable, unique within the store: the product id, or product and variant. */\n id: string\n /** The product's id: the variant group every configuration shares. */\n groupId: string\n /** Whether the product has more than one configuration. */\n hasVariants: boolean\n productId: string\n variantId: string\n /** The product's name. */\n productName: string\n /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */\n title: string\n /** Plain text: no markup. May be empty. */\n description: string\n /** Site-relative path of the product page, e.g. `/products/tee`. */\n path: string\n /** Absolute URL of the photo shown for this configuration. */\n imageUrl?: string\n /** Absolute URLs of the product's other photos, in order. */\n additionalImageUrls: string[]\n /** What the shopper pays, before any sale: the regular price. */\n priceMinor: number\n /** A lower price the offer is on sale for now; absent when not on sale. */\n salePriceMinor?: number\n availability: CatalogAvailability\n /** Units in stock, or `null` when stock is not tracked. */\n quantity: number | null\n kind: CatalogProductKind\n /** Whether the product is sold only as a recurring subscription. */\n subscriptionOnly: boolean\n /** What the merchant entered for shopping channels; absent when they did not. */\n condition?: CatalogCondition\n brand?: string\n /** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */\n gtin?: string\n mpn?: string\n /** A Google product taxonomy id or full path, as entered. */\n googleProductCategory?: string\n /** The store's own category path, e.g. `Apparel > Shirts`. */\n productType?: string\n /** The configuration's choices, by option name. */\n options: Readonly<Record<string, string>>\n /** The choice of an option named like a color, when there is one. */\n color?: string\n /** The choice of an option named like a size, when there is one. */\n size?: string\n weightGrams?: number\n /** One unit's packed size, in centimeters, when the merchant entered it. */\n dimensionsCm?: { length: number; width: number; height: number }\n /** What it costs to ship one unit, per country the store ships to. */\n shipping: CatalogShippingPrice[]\n /** When the product last changed, epoch ms; absent when unknown. */\n updatedAtMs?: number\n}\n\nexport interface CatalogPage {\n offers: CatalogOffer[]\n /** Where the next page starts, or `null` when this is the last one. */\n nextCursor: string | null\n}\n\nexport interface PluginProductCatalog {\n /** The store, or `null` for a site that does not sell. */\n store(hostId: string): Promise<CatalogStore | null>\n /**\n * One page of the store's live, listed offers: never a draft, archived or\n * deleted product. About `limit` offers — a product's configurations are\n * never split across pages, so a page may run over by one product's worth.\n */\n page(request: { hostId: string; cursor?: string | null; limit: number }): Promise<CatalogPage>\n}\n\nexport const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract<PluginProductCatalog>(\n 'core.product-catalog',\n { multiple: false },\n)\n\n/** Registers the store catalog. A second plugin's is refused, naming both. */\nexport function registerPluginProductCatalog(\n catalog: PluginProductCatalog,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered catalog, or `undefined` when no plugin sells. */\nexport function pluginProductCatalog(): PluginProductCatalog | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]?.impl\n}\n\n/**\n * The plugin that publishes the catalog to shopping channels, as the\n * catalog's owner hands it an address of its own (AGL-3637).\n *\n * Before any plugin published feeds, the plugin that keeps the products\n * served one feed itself, and merchants pasted that address into their\n * channel. The address stays the owner's — a request for it reaches the\n * owner's routes — but the feed is written by whoever publishes the catalog\n * now, so the owner answers it from here. Nobody registered is an ordinary\n * answer: {@link pluginCatalogFeed} returns `undefined` and the owner\n * answers 404.\n */\nexport interface PluginCatalogFeed {\n /**\n * Answers a request for the owner's pre-existing feed address for the site\n * `hostId`: the feed, a 404 when the site's publisher has retired the\n * address or does not publish, or a 503 to try later.\n */\n serveLegacyFeed(request: Request, hostId: string): Promise<Response>\n}\n\nexport const PLUGIN_CATALOG_FEED = definePluginServiceContract<PluginCatalogFeed>(\n 'core.catalog-feed',\n { multiple: false },\n)\n\n/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */\nexport function registerPluginCatalogFeed(\n feed: PluginCatalogFeed,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CATALOG_FEED, feed, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */\nexport function pluginCatalogFeed(): PluginCatalogFeed | undefined {\n return resolvePluginServices(PLUGIN_CATALOG_FEED)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_CATALOG","multiple","registerPluginProductCatalog","catalog","options","pluginId","pluginProductCatalog","impl","PLUGIN_CATALOG_FEED","registerPluginCatalogFeed","feed","pluginCatalogFeed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2J1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED,4EAA4E,GAC5E,OAAO,SAASC,6BACdC,OAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,SAAS,aACjDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,iEAAiE,GACjE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,uBAAuB,CAAC,EAAE,qBAAhDD,wBAAkDQ,IAAI;AAC/D;AAuBA,OAAO,MAAMC,sBAAsBX,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASQ,0BACdC,IAAuB,EACvBN,OAA+B;IAE/BN,sBAAsBU,qBAAqBE,MAAM,aAC3CN,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,kFAAkF,GAClF,OAAO,SAASM;QACPZ;IAAP,QAAOA,0BAAAA,sBAAsBS,oBAAoB,CAAC,EAAE,qBAA7CT,wBAA+CQ,IAAI;AAC5D"}
|