@aglyn/aglyn 1.0.0-beta.285 → 1.0.0-beta.288
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/account-acquisition.d.ts +14 -0
- package/src/lib/app-utils/account-acquisition.js +19 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +25 -0
- package/src/lib/app-utils/api-plugins.js +13 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/business-profile-values.d.ts +15 -3
- package/src/lib/app-utils/business-profile-values.js +19 -3
- package/src/lib/app-utils/business-profile-values.js.map +1 -1
- package/src/lib/app-utils/business-profile.d.ts +5 -3
- package/src/lib/app-utils/business-profile.js +2 -1
- package/src/lib/app-utils/business-profile.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +11 -0
- package/src/lib/app-utils/contacts.js +18 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +1 -1
- package/src/lib/app-utils/docs-deflection.js +17 -0
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
- package/src/lib/app-utils/docs-help.generated.js +20 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +168 -63
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
- package/src/lib/app-utils/docs-retrieval.js +28 -0
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/host-naming.d.ts +2 -1
- package/src/lib/app-utils/host-naming.js +65 -2
- package/src/lib/app-utils/host-naming.js.map +1 -1
- package/src/lib/app-utils/host-permissions.d.ts +159 -3
- package/src/lib/app-utils/host-permissions.js +384 -12
- package/src/lib/app-utils/host-permissions.js.map +1 -1
- package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
- package/src/lib/app-utils/org-permission-editor.js +176 -1
- package/src/lib/app-utils/org-permission-editor.js.map +1 -1
- package/src/lib/app-utils/org-permissions.d.ts +62 -1
- package/src/lib/app-utils/org-permissions.js +55 -5
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +28 -0
- package/src/lib/app-utils/org-role-grants.js +180 -2
- package/src/lib/app-utils/org-role-grants.js.map +1 -1
- package/src/lib/app-utils/org-roles.js +11 -2
- package/src/lib/app-utils/org-roles.js.map +1 -1
- package/src/lib/app-utils/redirect-query.d.ts +39 -0
- package/src/lib/app-utils/redirect-query.js +57 -0
- package/src/lib/app-utils/redirect-query.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +2 -2
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/site-starter-links.d.ts +42 -0
- package/src/lib/app-utils/site-starter-links.js +79 -0
- package/src/lib/app-utils/site-starter-links.js.map +1 -0
- package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
- package/src/lib/app-utils/site-starter-nodes.js +210 -0
- package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
- package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
- package/src/lib/app-utils/site-starter-theme.js +241 -0
- package/src/lib/app-utils/site-starter-theme.js.map +1 -0
- package/src/lib/app-utils/webhook-delivery.js +6 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
- package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
- package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
- package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
- package/src/lib/plugin-manager/plugin-permissions.js +91 -1
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
/** The closure being asked about. */
|
|
18
|
+
export type ClosureSubject =
|
|
19
|
+
/** A workspace about to be deleted, with every site in it. */
|
|
20
|
+
{
|
|
21
|
+
kind: 'workspace';
|
|
22
|
+
orgId: string;
|
|
23
|
+
}
|
|
24
|
+
/** A person's own account about to be closed. */
|
|
25
|
+
| {
|
|
26
|
+
kind: 'account';
|
|
27
|
+
uid: string;
|
|
28
|
+
};
|
|
29
|
+
/** One thing a notice lists: an order, a booking, a subscription. */
|
|
30
|
+
export interface ClosureNoticeItem {
|
|
31
|
+
/** Stable within the notice. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** The line a person recognizes it by: an order number, a name. */
|
|
34
|
+
primary: string;
|
|
35
|
+
/** What it is: the items in it, its dates. */
|
|
36
|
+
secondary?: string;
|
|
37
|
+
/** An amount, already formatted for the person reading it. */
|
|
38
|
+
amount?: string;
|
|
39
|
+
}
|
|
40
|
+
/** What one plugin says about one closure. */
|
|
41
|
+
export interface ClosureNotice {
|
|
42
|
+
/** Stable, and unique for the plugin. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** `warning` when the closure will change something the person paid for or relies on. */
|
|
45
|
+
severity: 'warning' | 'info';
|
|
46
|
+
/** One line: what will happen. */
|
|
47
|
+
title: string;
|
|
48
|
+
/** The rest of it: what happens to each item, and when. */
|
|
49
|
+
body: string;
|
|
50
|
+
/** What it applies to. A notice with nothing to list is not answered. */
|
|
51
|
+
items: ClosureNoticeItem[];
|
|
52
|
+
}
|
|
53
|
+
/** A notice as the reader answers it, with the plugin that said it. */
|
|
54
|
+
export type ClosureNoticeAnswer = ClosureNotice & {
|
|
55
|
+
pluginId: string;
|
|
56
|
+
};
|
|
57
|
+
export interface ClosureNoticeSource {
|
|
58
|
+
/** The notices for one closure; none when it changes nothing of the plugin's. May throw. */
|
|
59
|
+
notices(subject: ClosureSubject): Promise<readonly ClosureNotice[]>;
|
|
60
|
+
}
|
|
61
|
+
/** Every plugin may answer; one source per plugin. */
|
|
62
|
+
export declare const CLOSURE_NOTICE_SOURCES: import("./plugin-services").PluginServiceContract<ClosureNoticeSource>;
|
|
63
|
+
/**
|
|
64
|
+
* Installs a plugin's notice source. Owner = the loader's marker inside a
|
|
65
|
+
* register fn, else `options.pluginId`. Registering again replaces the
|
|
66
|
+
* plugin's earlier source.
|
|
67
|
+
*/
|
|
68
|
+
export declare function registerClosureNoticeSource(source: ClosureNoticeSource, options?: {
|
|
69
|
+
pluginId?: string;
|
|
70
|
+
}): void;
|
|
71
|
+
/** The plugins with a registered source, in the order they answer. */
|
|
72
|
+
export declare function listClosureNoticeSources(): string[];
|
|
73
|
+
/** Whether a notice can be printed: words, a severity, and items that each have a line. */
|
|
74
|
+
export declare function isPrintableClosureNotice(notice: unknown): notice is ClosureNotice;
|
|
75
|
+
/** What every source answered for one closure. */
|
|
76
|
+
export interface ClosureNoticesRead {
|
|
77
|
+
/** Warnings first, then in the order the sources answered. */
|
|
78
|
+
notices: ClosureNoticeAnswer[];
|
|
79
|
+
/** Plugins whose answer could not be read: the screen says details are missing. */
|
|
80
|
+
unavailable: string[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Asks every source about one closure. NEVER THROWS: a source that threw or
|
|
84
|
+
* answered something unprintable is listed in `unavailable`, and the rest
|
|
85
|
+
* are answered. With no source registered, runs the app's boot step once
|
|
86
|
+
* and asks again.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readClosureNotices(subject: ClosureSubject): Promise<ClosureNoticesRead>;
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
*/ /**
|
|
18
|
+
* WHAT A PERSON SHOULD KNOW BEFORE A WORKSPACE IS DELETED OR AN ACCOUNT IS
|
|
19
|
+
* CLOSED, said by the plugins whose records the closure acts on (AGL-4038).
|
|
20
|
+
*
|
|
21
|
+
* Its siblings do the closure: `plugin-org-erasure` and `plugin-user-erasure`
|
|
22
|
+
* let a plugin finish its share when a workspace is erased or an account is
|
|
23
|
+
* closed — cancel what it holds, refund what it took. This one says so
|
|
24
|
+
* BEFORE the person confirms. A plugin that will act on a closure registers
|
|
25
|
+
* a NOTICE SOURCE, and the console's delete-workspace and close-account
|
|
26
|
+
* screens ask every source and print what they answer above the confirm.
|
|
27
|
+
*
|
|
28
|
+
* The platform owns the question and the screen: who is asking, which
|
|
29
|
+
* closure, where the notices are drawn. What a plugin holds, what the
|
|
30
|
+
* closure will do to it, and the words the person reads, are the plugin's.
|
|
31
|
+
* A notice is information, never a gate: the platform does not hold a
|
|
32
|
+
* closure back on any answer, and a plugin that would refuse a closure has
|
|
33
|
+
* no way to through this contract.
|
|
34
|
+
*
|
|
35
|
+
* ## Not answered is said, not hidden
|
|
36
|
+
*
|
|
37
|
+
* A source that throws, or answers something that cannot be printed, is
|
|
38
|
+
* listed in `unavailable` by plugin id, and the screen says some details
|
|
39
|
+
* could not be checked — so a missing warning never reads as "nothing to
|
|
40
|
+
* warn about". A process where no source is registered at all runs the
|
|
41
|
+
* app's boot step once more first (`plugin-declarations-repair`), as the
|
|
42
|
+
* erasures do.
|
|
43
|
+
*
|
|
44
|
+
* Logic only: the contract, the registry and the reader. The screens that
|
|
45
|
+
* draw a notice are the console's.
|
|
46
|
+
*/ import { runPluginDeclarationsRepair } from "./plugin-declarations-repair.js";
|
|
47
|
+
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
48
|
+
/** Every plugin may answer; one source per plugin. */ export const CLOSURE_NOTICE_SOURCES = definePluginServiceContract('core.closure-notices', {
|
|
49
|
+
multiple: true
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Installs a plugin's notice source. Owner = the loader's marker inside a
|
|
53
|
+
* register fn, else `options.pluginId`. Registering again replaces the
|
|
54
|
+
* plugin's earlier source.
|
|
55
|
+
*/ export function registerClosureNoticeSource(source, options) {
|
|
56
|
+
if (typeof (source == null ? void 0 : source.notices) !== 'function') {
|
|
57
|
+
throw new Error('a closure notice source needs a notices function');
|
|
58
|
+
}
|
|
59
|
+
registerPluginService(CLOSURE_NOTICE_SOURCES, source, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
60
|
+
pluginId: options.pluginId
|
|
61
|
+
} : {}));
|
|
62
|
+
}
|
|
63
|
+
/** The plugins with a registered source, in the order they answer. */ export function listClosureNoticeSources() {
|
|
64
|
+
return resolvePluginServices(CLOSURE_NOTICE_SOURCES).map((entry)=>entry.pluginId);
|
|
65
|
+
}
|
|
66
|
+
const isText = (value)=>typeof value === 'string' && value.trim().length > 0;
|
|
67
|
+
/** Whether a notice can be printed: words, a severity, and items that each have a line. */ export function isPrintableClosureNotice(notice) {
|
|
68
|
+
const candidate = notice;
|
|
69
|
+
if (!candidate || typeof candidate !== 'object') return false;
|
|
70
|
+
if (!isText(candidate.id) || !isText(candidate.title) || !isText(candidate.body)) return false;
|
|
71
|
+
if (candidate.severity !== 'warning' && candidate.severity !== 'info') return false;
|
|
72
|
+
if (!Array.isArray(candidate.items) || candidate.items.length === 0) return false;
|
|
73
|
+
return candidate.items.every((item)=>isText(item == null ? void 0 : item.id) && isText(item == null ? void 0 : item.primary) && (item.secondary === undefined || typeof item.secondary === 'string') && (item.amount === undefined || typeof item.amount === 'string'));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Asks every source about one closure. NEVER THROWS: a source that threw or
|
|
77
|
+
* answered something unprintable is listed in `unavailable`, and the rest
|
|
78
|
+
* are answered. With no source registered, runs the app's boot step once
|
|
79
|
+
* and asks again.
|
|
80
|
+
*/ export async function readClosureNotices(subject) {
|
|
81
|
+
let registered = resolvePluginServices(CLOSURE_NOTICE_SOURCES);
|
|
82
|
+
if (!registered.length) {
|
|
83
|
+
const repaired = await runPluginDeclarationsRepair().catch(()=>false);
|
|
84
|
+
if (repaired) registered = resolvePluginServices(CLOSURE_NOTICE_SOURCES);
|
|
85
|
+
}
|
|
86
|
+
const answers = await Promise.all(registered.map(async ({ pluginId, impl })=>{
|
|
87
|
+
try {
|
|
88
|
+
const notices = await impl.notices(subject);
|
|
89
|
+
if (!Array.isArray(notices) || !notices.every(isPrintableClosureNotice)) {
|
|
90
|
+
console.error(`[closure-notices] "${pluginId}" answered a notice that cannot be printed`);
|
|
91
|
+
return {
|
|
92
|
+
pluginId,
|
|
93
|
+
notices: null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
pluginId,
|
|
98
|
+
notices: notices.map((notice)=>_extends({}, notice, {
|
|
99
|
+
pluginId
|
|
100
|
+
}))
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`[closure-notices] "${pluginId}" could not answer`, error);
|
|
104
|
+
return {
|
|
105
|
+
pluginId,
|
|
106
|
+
notices: null
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
const notices = answers.flatMap((answer)=>{
|
|
111
|
+
var _answer_notices;
|
|
112
|
+
return (_answer_notices = answer.notices) != null ? _answer_notices : [];
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
notices: [
|
|
116
|
+
...notices.filter((notice)=>notice.severity === 'warning'),
|
|
117
|
+
...notices.filter((notice)=>notice.severity !== 'warning')
|
|
118
|
+
],
|
|
119
|
+
unavailable: answers.filter((answer)=>answer.notices === null).map((answer)=>answer.pluginId)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=plugin-closure-notices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-closure-notices.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT A PERSON SHOULD KNOW BEFORE A WORKSPACE IS DELETED OR AN ACCOUNT IS\n * CLOSED, said by the plugins whose records the closure acts on (AGL-4038).\n *\n * Its siblings do the closure: `plugin-org-erasure` and `plugin-user-erasure`\n * let a plugin finish its share when a workspace is erased or an account is\n * closed — cancel what it holds, refund what it took. This one says so\n * BEFORE the person confirms. A plugin that will act on a closure registers\n * a NOTICE SOURCE, and the console's delete-workspace and close-account\n * screens ask every source and print what they answer above the confirm.\n *\n * The platform owns the question and the screen: who is asking, which\n * closure, where the notices are drawn. What a plugin holds, what the\n * closure will do to it, and the words the person reads, are the plugin's.\n * A notice is information, never a gate: the platform does not hold a\n * closure back on any answer, and a plugin that would refuse a closure has\n * no way to through this contract.\n *\n * ## Not answered is said, not hidden\n *\n * A source that throws, or answers something that cannot be printed, is\n * listed in `unavailable` by plugin id, and the screen says some details\n * could not be checked — so a missing warning never reads as \"nothing to\n * warn about\". A process where no source is registered at all runs the\n * app's boot step once more first (`plugin-declarations-repair`), as the\n * erasures do.\n *\n * Logic only: the contract, the registry and the reader. The screens that\n * draw a notice are the console's.\n */\n\nimport { runPluginDeclarationsRepair } from './plugin-declarations-repair'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/** The closure being asked about. */\nexport type ClosureSubject =\n /** A workspace about to be deleted, with every site in it. */\n | { kind: 'workspace'; orgId: string }\n /** A person's own account about to be closed. */\n | { kind: 'account'; uid: string }\n\n/** One thing a notice lists: an order, a booking, a subscription. */\nexport interface ClosureNoticeItem {\n /** Stable within the notice. */\n id: string\n /** The line a person recognizes it by: an order number, a name. */\n primary: string\n /** What it is: the items in it, its dates. */\n secondary?: string\n /** An amount, already formatted for the person reading it. */\n amount?: string\n}\n\n/** What one plugin says about one closure. */\nexport interface ClosureNotice {\n /** Stable, and unique for the plugin. */\n id: string\n /** `warning` when the closure will change something the person paid for or relies on. */\n severity: 'warning' | 'info'\n /** One line: what will happen. */\n title: string\n /** The rest of it: what happens to each item, and when. */\n body: string\n /** What it applies to. A notice with nothing to list is not answered. */\n items: ClosureNoticeItem[]\n}\n\n/** A notice as the reader answers it, with the plugin that said it. */\nexport type ClosureNoticeAnswer = ClosureNotice & { pluginId: string }\n\nexport interface ClosureNoticeSource {\n /** The notices for one closure; none when it changes nothing of the plugin's. May throw. */\n notices(subject: ClosureSubject): Promise<readonly ClosureNotice[]>\n}\n\n/** Every plugin may answer; one source per plugin. */\nexport const CLOSURE_NOTICE_SOURCES = definePluginServiceContract<ClosureNoticeSource>(\n 'core.closure-notices',\n { multiple: true },\n)\n\n/**\n * Installs a plugin's notice source. Owner = the loader's marker inside a\n * register fn, else `options.pluginId`. Registering again replaces the\n * plugin's earlier source.\n */\nexport function registerClosureNoticeSource(\n source: ClosureNoticeSource,\n options?: { pluginId?: string },\n): void {\n if (typeof source?.notices !== 'function') {\n throw new Error('a closure notice source needs a notices function')\n }\n registerPluginService(CLOSURE_NOTICE_SOURCES, source, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The plugins with a registered source, in the order they answer. */\nexport function listClosureNoticeSources(): string[] {\n return resolvePluginServices(CLOSURE_NOTICE_SOURCES).map((entry) => entry.pluginId)\n}\n\nconst isText = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0\n\n/** Whether a notice can be printed: words, a severity, and items that each have a line. */\nexport function isPrintableClosureNotice(notice: unknown): notice is ClosureNotice {\n const candidate = notice as ClosureNotice | null | undefined\n if (!candidate || typeof candidate !== 'object') return false\n if (!isText(candidate.id) || !isText(candidate.title) || !isText(candidate.body)) return false\n if (candidate.severity !== 'warning' && candidate.severity !== 'info') return false\n if (!Array.isArray(candidate.items) || candidate.items.length === 0) return false\n return candidate.items.every(\n (item) =>\n isText(item?.id) &&\n isText(item?.primary) &&\n (item.secondary === undefined || typeof item.secondary === 'string') &&\n (item.amount === undefined || typeof item.amount === 'string'),\n )\n}\n\n/** What every source answered for one closure. */\nexport interface ClosureNoticesRead {\n /** Warnings first, then in the order the sources answered. */\n notices: ClosureNoticeAnswer[]\n /** Plugins whose answer could not be read: the screen says details are missing. */\n unavailable: string[]\n}\n\n/**\n * Asks every source about one closure. NEVER THROWS: a source that threw or\n * answered something unprintable is listed in `unavailable`, and the rest\n * are answered. With no source registered, runs the app's boot step once\n * and asks again.\n */\nexport async function readClosureNotices(subject: ClosureSubject): Promise<ClosureNoticesRead> {\n let registered = resolvePluginServices(CLOSURE_NOTICE_SOURCES)\n if (!registered.length) {\n const repaired = await runPluginDeclarationsRepair().catch(() => false)\n if (repaired) registered = resolvePluginServices(CLOSURE_NOTICE_SOURCES)\n }\n const answers = await Promise.all(\n registered.map(async ({ pluginId, impl }) => {\n try {\n const notices = await impl.notices(subject)\n if (!Array.isArray(notices) || !notices.every(isPrintableClosureNotice)) {\n console.error(`[closure-notices] \"${pluginId}\" answered a notice that cannot be printed`)\n return { pluginId, notices: null }\n }\n return { pluginId, notices: notices.map((notice) => ({ ...notice, pluginId })) }\n } catch (error) {\n console.error(`[closure-notices] \"${pluginId}\" could not answer`, error)\n return { pluginId, notices: null }\n }\n }),\n )\n const notices = answers.flatMap((answer) => answer.notices ?? [])\n return {\n notices: [\n ...notices.filter((notice) => notice.severity === 'warning'),\n ...notices.filter((notice) => notice.severity !== 'warning'),\n ],\n unavailable: answers.filter((answer) => answer.notices === null).map((answer) => answer.pluginId),\n }\n}\n"],"names":["runPluginDeclarationsRepair","definePluginServiceContract","registerPluginService","resolvePluginServices","CLOSURE_NOTICE_SOURCES","multiple","registerClosureNoticeSource","source","options","notices","Error","pluginId","listClosureNoticeSources","map","entry","isText","value","trim","length","isPrintableClosureNotice","notice","candidate","id","title","body","severity","Array","isArray","items","every","item","primary","secondary","undefined","amount","readClosureNotices","subject","registered","repaired","catch","answers","Promise","all","impl","console","error","flatMap","answer","filter","unavailable"],"mappings":"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"}
|
|
@@ -305,3 +305,18 @@ export declare function capturePluginContact(request: PluginContactCaptureReques
|
|
|
305
305
|
export declare function listPluginContactSources(): ResolvedPluginContactSource[];
|
|
306
306
|
/** One declared capture door by its source word, or `null`. */
|
|
307
307
|
export declare function pluginContactSource(source: string): ResolvedPluginContactSource | null;
|
|
308
|
+
/**
|
|
309
|
+
* How a contact's source word reads on screen, wherever it came from: the
|
|
310
|
+
* core's own words from {@link CONTACT_SOURCE_LABELS}, then the label a
|
|
311
|
+
* plugin declared for its door with {@link registerPluginContactSource},
|
|
312
|
+
* then `fallback` when one is given, else the word itself.
|
|
313
|
+
*
|
|
314
|
+
* The core's map is a closed `Record<ContactSource, string>`, and a plugin's
|
|
315
|
+
* door is an open string by design, so a list, a chip or a report that read
|
|
316
|
+
* the map alone showed a plugin's raw word (`waiver` rather than its label).
|
|
317
|
+
* This is the one place both are asked, in that order, so a plugin cannot
|
|
318
|
+
* relabel a core source. It answers from what is registered in THIS process:
|
|
319
|
+
* a plugin that declares its door from its `declarations` entry is known in
|
|
320
|
+
* the console's browser and on both servers before a page renders.
|
|
321
|
+
*/
|
|
322
|
+
export declare function labelContactSource(source: unknown, fallback?: string): string;
|
|
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
17
|
+
*/ import { CONTACT_SOURCE_LABELS } from "../app-utils/contacts.js";
|
|
18
|
+
import { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
|
|
18
19
|
import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
19
20
|
export const PLUGIN_CONTACT_CAPTURE = definePluginServiceContract('core.contact-capture', {
|
|
20
21
|
multiple: false
|
|
@@ -85,5 +86,27 @@ export const PLUGIN_CONTACT_SOURCES = definePluginServiceContract('core.contact-
|
|
|
85
86
|
pluginId: entry.pluginId
|
|
86
87
|
}) : null;
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* How a contact's source word reads on screen, wherever it came from: the
|
|
91
|
+
* core's own words from {@link CONTACT_SOURCE_LABELS}, then the label a
|
|
92
|
+
* plugin declared for its door with {@link registerPluginContactSource},
|
|
93
|
+
* then `fallback` when one is given, else the word itself.
|
|
94
|
+
*
|
|
95
|
+
* The core's map is a closed `Record<ContactSource, string>`, and a plugin's
|
|
96
|
+
* door is an open string by design, so a list, a chip or a report that read
|
|
97
|
+
* the map alone showed a plugin's raw word (`waiver` rather than its label).
|
|
98
|
+
* This is the one place both are asked, in that order, so a plugin cannot
|
|
99
|
+
* relabel a core source. It answers from what is registered in THIS process:
|
|
100
|
+
* a plugin that declares its door from its `declarations` entry is known in
|
|
101
|
+
* the console's browser and on both servers before a page renders.
|
|
102
|
+
*/ export function labelContactSource(source, fallback) {
|
|
103
|
+
var _pluginContactSource;
|
|
104
|
+
const word = typeof source === 'string' ? source.trim() : '';
|
|
105
|
+
if (!word) return fallback != null ? fallback : '';
|
|
106
|
+
if (Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {
|
|
107
|
+
return CONTACT_SOURCE_LABELS[word];
|
|
108
|
+
}
|
|
109
|
+
return ((_pluginContactSource = pluginContactSource(word)) == null ? void 0 : _pluginContactSource.label) || (fallback != null ? fallback : word);
|
|
110
|
+
}
|
|
88
111
|
|
|
89
112
|
//# sourceMappingURL=plugin-contact-capture.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contact-capture.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 { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A person met on a site, handed to whichever plugin keeps people (AGL-3124).\n *\n * Four silos capture the same thing and none of them is the record system: a\n * form submission, a site member signing up, an order, a booking. Each holds\n * an address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each one imports the writer, which\n * makes the highest-fan-in edge in the repo — thirty files in one plugin alone\n * — and makes the record system's plugin something a storefront cannot take a\n * payment without.\n *\n * So the silo stops importing and starts DECLARING: it registers the capture\n * door it owns, and hands a capture to this seam. The plugin that keeps people\n * registers the writer and does every part of the work that is a record\n * system's to do:\n *\n * - normalizing and keying the address, and deciding whether this is a new\n * person or a visit from one the workspace already holds;\n * - the audience band, the erasure rows, and every other refusal — answered,\n * never thrown, so a capture that is refused never costs the silo the order\n * or the submission it was recording;\n * - what a stage, an owner, a company and a tag mean, and what the arrival\n * of a new person sets off.\n *\n * The silo decides none of that. It reports what it saw.\n *\n * ## Why the profile is opaque\n *\n * `profile` is the owner's own field names with scalar values, not a core\n * type. A person's record shape belongs to the plugin that models it, and a\n * typed core shape would be that model spelled in the platform again — which\n * is the defect this seam exists to retire, not a smaller version of it. A key\n * the owner does not document is the owner's to ignore.\n *\n * `source` is the silo's own word for its door, and it is declared rather than\n * enumerated here for the same reason: core listing `form`, `member`, `order`\n * and `booking` would be core holding the plugin set again. A silo registers\n * its source with {@link registerPluginContactSource}, which is what lets a\n * timeline label an entry and a link point back at the thing that captured the\n * person, without the platform knowing what any of them are.\n *\n * ## One writer; many silos\n *\n * A workspace keeps one set of people, so the writer is a slot: a second\n * plugin's writer is refused naming both, and the incumbent keeps serving. A\n * SOURCE is per silo and there are many, but one source word has one owner —\n * two plugins claiming `order` would make the label and the link ambiguous at\n * the point they are read.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-record-timeline`, the registry authenticates nobody. A silo has\n * already decided, in its own terms and before it asks, that the capture is\n * the workspace's to record — that the submission was accepted, that the order\n * was paid.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** The person, as the capturing silo saw them. */\nexport interface PluginContactIdentity {\n /**\n * The address they gave. Raw: the owner normalizes and keys on it, and a\n * silo that normalized first would be keeping a second copy of that rule.\n */\n email: unknown\n name?: string | null\n}\n\n/** What happened, in the capturing silo's own words. */\nexport interface PluginContactInteraction {\n /** The silo's declared source word — see {@link registerPluginContactSource}. */\n source: string\n /** When it happened, epoch ms; the owner uses now when absent. */\n atMs?: number\n /**\n * The document it happened on, in the silo's terms\n * (`formSubmissions/{id}`, `orders/{id}`), so a timeline entry can point\n * back at it. Absent where the door leaves nothing to open.\n */\n refId?: string | null\n /** One line a person reads on the timeline. */\n summary?: string | null\n}\n\nexport interface PluginContactCaptureRequest {\n /**\n * Who caused the capture, when it was not the person captured (AGL-3376) —\n * a member adding someone by hand, an integration's key. A door where the\n * visitor acts for themselves (a form, a purchase) may leave it out.\n */\n actor?: {\n kind: 'member' | 'visitor' | 'apiKey' | 'platform'\n uid?: string | null\n email?: string | null\n apiKeyName?: string | null\n }\n orgId: string\n /** The site the person was met on: the brand whose form or checkout it was. */\n hostId: string\n identity: PluginContactIdentity\n interaction: PluginContactInteraction\n /**\n * An explicit marketing opt-in the capture surface carried, recorded against\n * `hostId`: consent to the MARKETING send purpose (`marketing-consent.ts`),\n * whichever plugin's mail later asks for it. Absent is not consent, and\n * filing a capture under a container is not consent either.\n */\n marketingConsent?: boolean\n /**\n * The disclosure key the capture surface rendered beside that opt-in\n * (`consentGroupDisclosureKey`, AGL-3320), passed through as the surface\n * sent it. The owner records the opt-in for the site's whole consent group\n * only when this is the group's current key, and for `hostId` alone\n * otherwise — so a silo whose surface shows no disclosure sends none.\n */\n disclosedConsentGroup?: string\n /** Tags the silo puts on this capture; the owner adds, never replaces. */\n tags?: readonly string[]\n /**\n * What the capture SURFACE is filed under — the merchant's own act — by\n * container kind (`plugin-containers.ts`): `{ [kind]: ids }`. The owner\n * files the person under the kinds it keeps. Filing is not consent.\n */\n containers?: Readonly<Record<string, readonly string[]>>\n /** Money the capture was worth, in cents, where the silo took some. */\n purchaseCents?: number\n /** The currency {@link purchaseCents} is in, lowercase, when the silo knows. */\n purchaseCurrency?: string\n /**\n * The earliest stage this capture describes, in the OWNER's words. A floor\n * and not a value: the owner fills an empty stage and advances an earlier\n * one, and never moves anybody back, so a repeat customer filling in a\n * contact form is still a customer.\n */\n lifecycleFloor?: string\n /**\n * What kind of door this is, which decides WHICH RECORD the person lands\n * on (AGL-3232) — the owner's rule is Salesforce's: one person is one\n * record, a lead until they are qualified and a contact after.\n *\n * - `lead`: a lead surface — a form whose author routes it to leads, a\n * booking request. The person is filed as a LEAD and nothing else,\n * unless the workspace already holds them as a contact, in which case\n * the capture lands on the contact and no lead is filed.\n * - `relationship`: an act that makes the person a known relationship —\n * a member account, a purchase. The person is filed as a CONTACT, and\n * an open lead the site held for them is stamped converted onto it.\n * - `touch`, the default: everything else — a form without lead routing,\n * a newsletter opt-in. The capture lands on the open lead when the site\n * holds one for the address, and on the contact otherwise.\n */\n surface?: 'lead' | 'relationship' | 'touch'\n /**\n * Profile fields the silo knows, keyed by the owner's own field names.\n * Only the keys given are written, so a silo that knows the phone number\n * does not blank a title somebody typed.\n */\n profile?: Readonly<Record<string, unknown>>\n /**\n * Profile fields written only where the record the person lands on holds\n * none (AGL-3493), keyed as {@link profile} is. For what a door learned\n * in passing — the phone a booking form asked for — and must not put over\n * a value somebody typed on the record or gave the business before.\n * A key also present in `profile` is `profile`'s to write.\n */\n profileFill?: Readonly<Record<string, unknown>>\n /**\n * Facts about THIS CAPTURE that the owner models and the platform does not\n * (AGL-3080) — which form was filled in, which page it was on, where the\n * visitor arrived from.\n *\n * Opaque for the reason {@link profile} is: every one of these is a concept\n * belonging to the plugin that keeps people or to the silo that saw it, and\n * a typed core field for each would be those models spelled out in the\n * platform again. `profile` describes the PERSON and outlives the visit;\n * this describes the visit and does not.\n *\n * A key the owner does not recognize is the owner's to ignore, and a silo\n * that sends nothing here loses nothing: every field is an enrichment of a\n * capture that is already complete without it.\n */\n detail?: Readonly<Record<string, unknown>>\n}\n\n/**\n * The owner's answer. `created: false` is a visit by somebody the workspace\n * already held, which is an interaction and not a new person.\n *\n * A refusal is RETURNED. Every one of these silos has already done the thing\n * it was recording, so a throw here would cost an accepted submission or a\n * paid order for a record it could not keep.\n */\nexport type PluginContactCaptured =\n /** The capture landed on a contact — the record every capture used to make. */\n | { ok: true; record: 'contact'; contactId: string; created: boolean }\n /**\n * The capture landed on a LEAD (AGL-3232): a lead surface met somebody the\n * workspace does not hold as a contact, or a touch reached an open lead.\n * `leadId` is the person key the lead is filed under on `hostId`.\n */\n | {\n ok: true\n record: 'lead'\n leadId: string\n created: boolean\n /**\n * Whether this capture's surface was new on the lead — a new person,\n * or one first met through another door. A form counts its leads by\n * it (AGL-3330): the people it filed, not the times they came back.\n * Absent from a writer that does not say, which a caller reads as\n * `created`.\n */\n sourceAdded?: boolean\n }\n | {\n ok: false\n /**\n * `invalid-email`: nothing usable to key a person on. `band`: the\n * workspace is at the audience it may hold. `erased`: the workspace\n * erased this person and a capture must not quietly rebuild them.\n * `error`: the owner failed, and said so rather than silently.\n */\n reason: 'invalid-email' | 'band' | 'erased' | 'error'\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginContactCaptureWriter {\n /** Records one capture; a refusal is returned, never thrown. */\n capture(\n request: PluginContactCaptureRequest,\n ): Promise<PluginContactCaptured>\n}\n\n/** A capture door a silo owns. */\nexport interface PluginContactSourceDeclaration {\n /** The silo's word for the door, as its captures carry it. */\n source: string\n /** What a timeline entry from this door says: `Form submission`. */\n label: string\n /**\n * What a link from that entry says when there is something to open —\n * `Open submission`. Absent where the door leaves nothing to open, which\n * is the honest answer for an import or a by-hand add.\n */\n openLabel?: string\n /**\n * The record kind the silo's own document is, for\n * `plugin-record-routes` to address it with. Absent where the door has no\n * record of its own to open.\n */\n recordKind?: string\n}\n\n/** A source declaration with the silo that made it. */\nexport type ResolvedPluginContactSource = PluginContactSourceDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_CONTACT_CAPTURE =\n definePluginServiceContract<PluginContactCaptureWriter>(\n 'core.contact-capture',\n { multiple: false },\n )\n\nexport const PLUGIN_CONTACT_SOURCES =\n definePluginServiceContract<PluginContactSourceDeclaration>(\n 'core.contact-capture-sources',\n { multiple: true },\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 writer is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginContactCaptureWriter(\n writer: PluginContactCaptureWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CONTACT_CAPTURE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Declares a capture door. One source word has one owner. */\nexport function registerPluginContactSource(\n declaration: PluginContactSourceDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.source?.trim() ?? ''\n if (!key) throw new Error('a contact capture source needs a source word')\n if (!declaration.label?.trim()) {\n throw new Error(`contact capture source \"${key}\" needs a label`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `contact capture source \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_CONTACT_SOURCES,\n { ...declaration, source: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\nexport interface ResolvedPluginContactCaptureWriter {\n /** The plugin that keeps people. */\n pluginId: string\n writer: PluginContactCaptureWriter\n}\n\n/** The writer with its owner, or `null` when no plugin keeps people here. */\nexport function pluginContactCaptureWriter(): ResolvedPluginContactCaptureWriter | null {\n const entry = resolvePluginServices(PLUGIN_CONTACT_CAPTURE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Hands one capture to the plugin that keeps people, or answers `null` when\n * no plugin does.\n *\n * `null` and a refusal are different answers and a silo treats them\n * differently: `null` is a workspace with no record system, where there is\n * nothing to record and nothing has gone wrong; a refusal is a record system\n * that considered this person and declined.\n */\nexport async function capturePluginContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const found = pluginContactCaptureWriter()\n if (!found) return null\n return found.writer.capture(request)\n}\n\n/** Every declared capture door, with its silo, in registration order. */\nexport function listPluginContactSources(): ResolvedPluginContactSource[] {\n return resolvePluginServices(PLUGIN_CONTACT_SOURCES).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n\n/** One declared capture door by its source word, or `null`. */\nexport function pluginContactSource(\n source: string,\n): ResolvedPluginContactSource | null {\n const key = source.trim()\n const entry = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_CONTACT_CAPTURE","multiple","PLUGIN_CONTACT_SOURCES","registerPluginContactCaptureWriter","writer","options","pluginId","registerPluginContactSource","declaration","key","source","trim","Error","label","incumbent","find","entry","pluginContactCaptureWriter","impl","capturePluginContact","request","found","capture","listPluginContactSources","map","pluginContactSource","one"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAuQ1B,OAAO,MAAMC,yBACXH,4BACE,wBACA;IAAEI,UAAU;AAAM,GACnB;AAEH,OAAO,MAAMC,yBACXL,4BACE,gCACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;;CAKC,GACD,OAAO,SAASE,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BP,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,4DAA4D,GAC5D,OAAO,SAASC,4BACdC,WAA2C,EAC3CH,OAA+B;cAObT,OAAAA;QALNY,qBAEPA;IAFL,MAAMC,eAAMD,sBAAAA,YAAYE,MAAM,qBAAlBF,oBAAoBG,IAAI,qBAAM;IAC1C,IAAI,CAACF,KAAK,MAAM,IAAIG,MAAM;IAC1B,IAAI,GAACJ,qBAAAA,YAAYK,KAAK,qBAAjBL,mBAAmBG,IAAI,KAAI;QAC9B,MAAM,IAAIC,MAAM,CAAC,wBAAwB,EAAEH,IAAI,eAAe,CAAC;IACjE;IACA,MAAMH,WAAW,EAACV,SAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASC,QAAQ,YAA7CV,QAAiD,IAAIe,IAAI;IAC3E,MAAMG,YAAYf,sBAAsBG,wBAAwBa,IAAI,CAClE,CAACC,QAAUA,MAAMP,GAAG,KAAKA;IAE3B,IAAIK,aAAaR,YAAYQ,UAAUR,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIM,MACR,CAAC,wBAAwB,EAAEH,IAAI,yBAAyB,CAAC,GACvD,CAAC,CAAC,EAAEK,UAAUR,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAR,sBACEI,wBACA,aAAKM;QAAaE,QAAQD;QAC1B,aACMJ,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;QAC1DG;;AAGN;AAQA,2EAA2E,GAC3E,OAAO,SAASQ;IACd,MAAMD,QAAQjB,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOgB,QAAQ;QAAEV,UAAUU,MAAMV,QAAQ;QAAEF,QAAQY,MAAME,IAAI;IAAC,IAAI;AACpE;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,qBACpBC,OAAoC;IAEpC,MAAMC,QAAQJ;IACd,IAAI,CAACI,OAAO,OAAO;IACnB,OAAOA,MAAMjB,MAAM,CAACkB,OAAO,CAACF;AAC9B;AAEA,uEAAuE,GACvE,OAAO,SAASG;IACd,OAAOxB,sBAAsBG,wBAAwBsB,GAAG,CAAC,CAACR,QAAW,aAChEA,MAAME,IAAI;YACbZ,UAAUU,MAAMV,QAAQ;;AAE5B;AAEA,6DAA6D,GAC7D,OAAO,SAASmB,oBACdf,MAAc;IAEd,MAAMD,MAAMC,OAAOC,IAAI;IACvB,MAAMK,QAAQjB,sBAAsBG,wBAAwBa,IAAI,CAC9D,CAACW,MAAQA,IAAIjB,GAAG,KAAKA;IAEvB,OAAOO,QAAQ,aAAKA,MAAME,IAAI;QAAEZ,UAAUU,MAAMV,QAAQ;SAAK;AAC/D"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-contact-capture.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 { CONTACT_SOURCE_LABELS } from '../app-utils/contacts'\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A person met on a site, handed to whichever plugin keeps people (AGL-3124).\n *\n * Four silos capture the same thing and none of them is the record system: a\n * form submission, a site member signing up, an order, a booking. Each holds\n * an address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each one imports the writer, which\n * makes the highest-fan-in edge in the repo — thirty files in one plugin alone\n * — and makes the record system's plugin something a storefront cannot take a\n * payment without.\n *\n * So the silo stops importing and starts DECLARING: it registers the capture\n * door it owns, and hands a capture to this seam. The plugin that keeps people\n * registers the writer and does every part of the work that is a record\n * system's to do:\n *\n * - normalizing and keying the address, and deciding whether this is a new\n * person or a visit from one the workspace already holds;\n * - the audience band, the erasure rows, and every other refusal — answered,\n * never thrown, so a capture that is refused never costs the silo the order\n * or the submission it was recording;\n * - what a stage, an owner, a company and a tag mean, and what the arrival\n * of a new person sets off.\n *\n * The silo decides none of that. It reports what it saw.\n *\n * ## Why the profile is opaque\n *\n * `profile` is the owner's own field names with scalar values, not a core\n * type. A person's record shape belongs to the plugin that models it, and a\n * typed core shape would be that model spelled in the platform again — which\n * is the defect this seam exists to retire, not a smaller version of it. A key\n * the owner does not document is the owner's to ignore.\n *\n * `source` is the silo's own word for its door, and it is declared rather than\n * enumerated here for the same reason: core listing `form`, `member`, `order`\n * and `booking` would be core holding the plugin set again. A silo registers\n * its source with {@link registerPluginContactSource}, which is what lets a\n * timeline label an entry and a link point back at the thing that captured the\n * person, without the platform knowing what any of them are.\n *\n * ## One writer; many silos\n *\n * A workspace keeps one set of people, so the writer is a slot: a second\n * plugin's writer is refused naming both, and the incumbent keeps serving. A\n * SOURCE is per silo and there are many, but one source word has one owner —\n * two plugins claiming `order` would make the label and the link ambiguous at\n * the point they are read.\n *\n * ## The caller proves who is asking\n *\n * Like `plugin-record-timeline`, the registry authenticates nobody. A silo has\n * already decided, in its own terms and before it asks, that the capture is\n * the workspace's to record — that the submission was accepted, that the order\n * was paid.\n *\n * Server-side: reached by its own subpath, never through\n * `plugin-manager/index.ts`.\n */\n\n/** The person, as the capturing silo saw them. */\nexport interface PluginContactIdentity {\n /**\n * The address they gave. Raw: the owner normalizes and keys on it, and a\n * silo that normalized first would be keeping a second copy of that rule.\n */\n email: unknown\n name?: string | null\n}\n\n/** What happened, in the capturing silo's own words. */\nexport interface PluginContactInteraction {\n /** The silo's declared source word — see {@link registerPluginContactSource}. */\n source: string\n /** When it happened, epoch ms; the owner uses now when absent. */\n atMs?: number\n /**\n * The document it happened on, in the silo's terms\n * (`formSubmissions/{id}`, `orders/{id}`), so a timeline entry can point\n * back at it. Absent where the door leaves nothing to open.\n */\n refId?: string | null\n /** One line a person reads on the timeline. */\n summary?: string | null\n}\n\nexport interface PluginContactCaptureRequest {\n /**\n * Who caused the capture, when it was not the person captured (AGL-3376) —\n * a member adding someone by hand, an integration's key. A door where the\n * visitor acts for themselves (a form, a purchase) may leave it out.\n */\n actor?: {\n kind: 'member' | 'visitor' | 'apiKey' | 'platform'\n uid?: string | null\n email?: string | null\n apiKeyName?: string | null\n }\n orgId: string\n /** The site the person was met on: the brand whose form or checkout it was. */\n hostId: string\n identity: PluginContactIdentity\n interaction: PluginContactInteraction\n /**\n * An explicit marketing opt-in the capture surface carried, recorded against\n * `hostId`: consent to the MARKETING send purpose (`marketing-consent.ts`),\n * whichever plugin's mail later asks for it. Absent is not consent, and\n * filing a capture under a container is not consent either.\n */\n marketingConsent?: boolean\n /**\n * The disclosure key the capture surface rendered beside that opt-in\n * (`consentGroupDisclosureKey`, AGL-3320), passed through as the surface\n * sent it. The owner records the opt-in for the site's whole consent group\n * only when this is the group's current key, and for `hostId` alone\n * otherwise — so a silo whose surface shows no disclosure sends none.\n */\n disclosedConsentGroup?: string\n /** Tags the silo puts on this capture; the owner adds, never replaces. */\n tags?: readonly string[]\n /**\n * What the capture SURFACE is filed under — the merchant's own act — by\n * container kind (`plugin-containers.ts`): `{ [kind]: ids }`. The owner\n * files the person under the kinds it keeps. Filing is not consent.\n */\n containers?: Readonly<Record<string, readonly string[]>>\n /** Money the capture was worth, in cents, where the silo took some. */\n purchaseCents?: number\n /** The currency {@link purchaseCents} is in, lowercase, when the silo knows. */\n purchaseCurrency?: string\n /**\n * The earliest stage this capture describes, in the OWNER's words. A floor\n * and not a value: the owner fills an empty stage and advances an earlier\n * one, and never moves anybody back, so a repeat customer filling in a\n * contact form is still a customer.\n */\n lifecycleFloor?: string\n /**\n * What kind of door this is, which decides WHICH RECORD the person lands\n * on (AGL-3232) — the owner's rule is Salesforce's: one person is one\n * record, a lead until they are qualified and a contact after.\n *\n * - `lead`: a lead surface — a form whose author routes it to leads, a\n * booking request. The person is filed as a LEAD and nothing else,\n * unless the workspace already holds them as a contact, in which case\n * the capture lands on the contact and no lead is filed.\n * - `relationship`: an act that makes the person a known relationship —\n * a member account, a purchase. The person is filed as a CONTACT, and\n * an open lead the site held for them is stamped converted onto it.\n * - `touch`, the default: everything else — a form without lead routing,\n * a newsletter opt-in. The capture lands on the open lead when the site\n * holds one for the address, and on the contact otherwise.\n */\n surface?: 'lead' | 'relationship' | 'touch'\n /**\n * Profile fields the silo knows, keyed by the owner's own field names.\n * Only the keys given are written, so a silo that knows the phone number\n * does not blank a title somebody typed.\n */\n profile?: Readonly<Record<string, unknown>>\n /**\n * Profile fields written only where the record the person lands on holds\n * none (AGL-3493), keyed as {@link profile} is. For what a door learned\n * in passing — the phone a booking form asked for — and must not put over\n * a value somebody typed on the record or gave the business before.\n * A key also present in `profile` is `profile`'s to write.\n */\n profileFill?: Readonly<Record<string, unknown>>\n /**\n * Facts about THIS CAPTURE that the owner models and the platform does not\n * (AGL-3080) — which form was filled in, which page it was on, where the\n * visitor arrived from.\n *\n * Opaque for the reason {@link profile} is: every one of these is a concept\n * belonging to the plugin that keeps people or to the silo that saw it, and\n * a typed core field for each would be those models spelled out in the\n * platform again. `profile` describes the PERSON and outlives the visit;\n * this describes the visit and does not.\n *\n * A key the owner does not recognize is the owner's to ignore, and a silo\n * that sends nothing here loses nothing: every field is an enrichment of a\n * capture that is already complete without it.\n */\n detail?: Readonly<Record<string, unknown>>\n}\n\n/**\n * The owner's answer. `created: false` is a visit by somebody the workspace\n * already held, which is an interaction and not a new person.\n *\n * A refusal is RETURNED. Every one of these silos has already done the thing\n * it was recording, so a throw here would cost an accepted submission or a\n * paid order for a record it could not keep.\n */\nexport type PluginContactCaptured =\n /** The capture landed on a contact — the record every capture used to make. */\n | { ok: true; record: 'contact'; contactId: string; created: boolean }\n /**\n * The capture landed on a LEAD (AGL-3232): a lead surface met somebody the\n * workspace does not hold as a contact, or a touch reached an open lead.\n * `leadId` is the person key the lead is filed under on `hostId`.\n */\n | {\n ok: true\n record: 'lead'\n leadId: string\n created: boolean\n /**\n * Whether this capture's surface was new on the lead — a new person,\n * or one first met through another door. A form counts its leads by\n * it (AGL-3330): the people it filed, not the times they came back.\n * Absent from a writer that does not say, which a caller reads as\n * `created`.\n */\n sourceAdded?: boolean\n }\n | {\n ok: false\n /**\n * `invalid-email`: nothing usable to key a person on. `band`: the\n * workspace is at the audience it may hold. `erased`: the workspace\n * erased this person and a capture must not quietly rebuild them.\n * `error`: the owner failed, and said so rather than silently.\n */\n reason: 'invalid-email' | 'band' | 'erased' | 'error'\n /** Customer-safe: a caller may show or log it as it stands. */\n error: string\n }\n\nexport interface PluginContactCaptureWriter {\n /** Records one capture; a refusal is returned, never thrown. */\n capture(\n request: PluginContactCaptureRequest,\n ): Promise<PluginContactCaptured>\n}\n\n/** A capture door a silo owns. */\nexport interface PluginContactSourceDeclaration {\n /** The silo's word for the door, as its captures carry it. */\n source: string\n /** What a timeline entry from this door says: `Form submission`. */\n label: string\n /**\n * What a link from that entry says when there is something to open —\n * `Open submission`. Absent where the door leaves nothing to open, which\n * is the honest answer for an import or a by-hand add.\n */\n openLabel?: string\n /**\n * The record kind the silo's own document is, for\n * `plugin-record-routes` to address it with. Absent where the door has no\n * record of its own to open.\n */\n recordKind?: string\n}\n\n/** A source declaration with the silo that made it. */\nexport type ResolvedPluginContactSource = PluginContactSourceDeclaration & {\n pluginId: string\n}\n\nexport const PLUGIN_CONTACT_CAPTURE =\n definePluginServiceContract<PluginContactCaptureWriter>(\n 'core.contact-capture',\n { multiple: false },\n )\n\nexport const PLUGIN_CONTACT_SOURCES =\n definePluginServiceContract<PluginContactSourceDeclaration>(\n 'core.contact-capture-sources',\n { multiple: true },\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 writer is refused naming both —\n * the incumbent keeps serving.\n */\nexport function registerPluginContactCaptureWriter(\n writer: PluginContactCaptureWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CONTACT_CAPTURE, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Declares a capture door. One source word has one owner. */\nexport function registerPluginContactSource(\n declaration: PluginContactSourceDeclaration,\n options?: { pluginId?: string },\n): void {\n const key = declaration.source?.trim() ?? ''\n if (!key) throw new Error('a contact capture source needs a source word')\n if (!declaration.label?.trim()) {\n throw new Error(`contact capture source \"${key}\" needs a label`)\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (entry) => entry.key === key,\n )\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `contact capture source \"${key}\" is already declared by ` +\n `\"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(\n PLUGIN_CONTACT_SOURCES,\n { ...declaration, source: key },\n {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n },\n )\n}\n\nexport interface ResolvedPluginContactCaptureWriter {\n /** The plugin that keeps people. */\n pluginId: string\n writer: PluginContactCaptureWriter\n}\n\n/** The writer with its owner, or `null` when no plugin keeps people here. */\nexport function pluginContactCaptureWriter(): ResolvedPluginContactCaptureWriter | null {\n const entry = resolvePluginServices(PLUGIN_CONTACT_CAPTURE)[0]\n return entry ? { pluginId: entry.pluginId, writer: entry.impl } : null\n}\n\n/**\n * Hands one capture to the plugin that keeps people, or answers `null` when\n * no plugin does.\n *\n * `null` and a refusal are different answers and a silo treats them\n * differently: `null` is a workspace with no record system, where there is\n * nothing to record and nothing has gone wrong; a refusal is a record system\n * that considered this person and declined.\n */\nexport async function capturePluginContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured | null> {\n const found = pluginContactCaptureWriter()\n if (!found) return null\n return found.writer.capture(request)\n}\n\n/** Every declared capture door, with its silo, in registration order. */\nexport function listPluginContactSources(): ResolvedPluginContactSource[] {\n return resolvePluginServices(PLUGIN_CONTACT_SOURCES).map((entry) => ({\n ...entry.impl,\n pluginId: entry.pluginId,\n }))\n}\n\n/** One declared capture door by its source word, or `null`. */\nexport function pluginContactSource(\n source: string,\n): ResolvedPluginContactSource | null {\n const key = source.trim()\n const entry = resolvePluginServices(PLUGIN_CONTACT_SOURCES).find(\n (one) => one.key === key,\n )\n return entry ? { ...entry.impl, pluginId: entry.pluginId } : null\n}\n\n/**\n * How a contact's source word reads on screen, wherever it came from: the\n * core's own words from {@link CONTACT_SOURCE_LABELS}, then the label a\n * plugin declared for its door with {@link registerPluginContactSource},\n * then `fallback` when one is given, else the word itself.\n *\n * The core's map is a closed `Record<ContactSource, string>`, and a plugin's\n * door is an open string by design, so a list, a chip or a report that read\n * the map alone showed a plugin's raw word (`waiver` rather than its label).\n * This is the one place both are asked, in that order, so a plugin cannot\n * relabel a core source. It answers from what is registered in THIS process:\n * a plugin that declares its door from its `declarations` entry is known in\n * the console's browser and on both servers before a page renders.\n */\nexport function labelContactSource(source: unknown, fallback?: string): string {\n const word = typeof source === 'string' ? source.trim() : ''\n if (!word) return fallback ?? ''\n if (Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n return CONTACT_SOURCE_LABELS[word as keyof typeof CONTACT_SOURCE_LABELS]\n }\n return pluginContactSource(word)?.label || (fallback ?? word)\n}\n"],"names":["CONTACT_SOURCE_LABELS","getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_CONTACT_CAPTURE","multiple","PLUGIN_CONTACT_SOURCES","registerPluginContactCaptureWriter","writer","options","pluginId","registerPluginContactSource","declaration","key","source","trim","Error","label","incumbent","find","entry","pluginContactCaptureWriter","impl","capturePluginContact","request","found","capture","listPluginContactSources","map","pluginContactSource","one","labelContactSource","fallback","word","Object","hasOwn"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,qBAAqB,QAAQ,2BAAuB;AAC7D,SAASC,sBAAsB,QAAQ,qCAAiC;AACxE,SACEC,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAuQ1B,OAAO,MAAMC,yBACXH,4BACE,wBACA;IAAEI,UAAU;AAAM,GACnB;AAEH,OAAO,MAAMC,yBACXL,4BACE,gCACA;IAAEI,UAAU;AAAK,GAClB;AAEH;;;;;CAKC,GACD,OAAO,SAASE,mCACdC,MAAkC,EAClCC,OAA+B;IAE/BP,sBAAsBE,wBAAwBI,QAAQ,aAChDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,4DAA4D,GAC5D,OAAO,SAASC,4BACdC,WAA2C,EAC3CH,OAA+B;cAObT,OAAAA;QALNY,qBAEPA;IAFL,MAAMC,eAAMD,sBAAAA,YAAYE,MAAM,qBAAlBF,oBAAoBG,IAAI,qBAAM;IAC1C,IAAI,CAACF,KAAK,MAAM,IAAIG,MAAM;IAC1B,IAAI,GAACJ,qBAAAA,YAAYK,KAAK,qBAAjBL,mBAAmBG,IAAI,KAAI;QAC9B,MAAM,IAAIC,MAAM,CAAC,wBAAwB,EAAEH,IAAI,eAAe,CAAC;IACjE;IACA,MAAMH,WAAW,EAACV,SAAAA,0BAAAA,oCAAAA,0BAA4BS,2BAAAA,QAASC,QAAQ,YAA7CV,QAAiD,IAAIe,IAAI;IAC3E,MAAMG,YAAYf,sBAAsBG,wBAAwBa,IAAI,CAClE,CAACC,QAAUA,MAAMP,GAAG,KAAKA;IAE3B,IAAIK,aAAaR,YAAYQ,UAAUR,QAAQ,KAAKA,UAAU;QAC5D,MAAM,IAAIM,MACR,CAAC,wBAAwB,EAAEH,IAAI,yBAAyB,CAAC,GACvD,CAAC,CAAC,EAAEK,UAAUR,QAAQ,CAAC,YAAY,EAAEA,SAAS,CAAC,CAAC;IAEtD;IACAR,sBACEI,wBACA,aAAKM;QAAaE,QAAQD;QAC1B,aACMJ,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;QAC1DG;;AAGN;AAQA,2EAA2E,GAC3E,OAAO,SAASQ;IACd,MAAMD,QAAQjB,sBAAsBC,uBAAuB,CAAC,EAAE;IAC9D,OAAOgB,QAAQ;QAAEV,UAAUU,MAAMV,QAAQ;QAAEF,QAAQY,MAAME,IAAI;IAAC,IAAI;AACpE;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeC,qBACpBC,OAAoC;IAEpC,MAAMC,QAAQJ;IACd,IAAI,CAACI,OAAO,OAAO;IACnB,OAAOA,MAAMjB,MAAM,CAACkB,OAAO,CAACF;AAC9B;AAEA,uEAAuE,GACvE,OAAO,SAASG;IACd,OAAOxB,sBAAsBG,wBAAwBsB,GAAG,CAAC,CAACR,QAAW,aAChEA,MAAME,IAAI;YACbZ,UAAUU,MAAMV,QAAQ;;AAE5B;AAEA,6DAA6D,GAC7D,OAAO,SAASmB,oBACdf,MAAc;IAEd,MAAMD,MAAMC,OAAOC,IAAI;IACvB,MAAMK,QAAQjB,sBAAsBG,wBAAwBa,IAAI,CAC9D,CAACW,MAAQA,IAAIjB,GAAG,KAAKA;IAEvB,OAAOO,QAAQ,aAAKA,MAAME,IAAI;QAAEZ,UAAUU,MAAMV,QAAQ;SAAK;AAC/D;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASqB,mBAAmBjB,MAAe,EAAEkB,QAAiB;QAM5DH;IALP,MAAMI,OAAO,OAAOnB,WAAW,WAAWA,OAAOC,IAAI,KAAK;IAC1D,IAAI,CAACkB,MAAM,OAAOD,mBAAAA,WAAY;IAC9B,IAAIE,OAAOC,MAAM,CAACpC,uBAAuBkC,OAAO;QAC9C,OAAOlC,qBAAqB,CAACkC,KAA2C;IAC1E;IACA,OAAOJ,EAAAA,uBAAAA,oBAAoBI,0BAApBJ,qBAA2BZ,KAAK,MAAKe,mBAAAA,WAAYC;AAC1D"}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
import { type CustomerDisplayScreen } from './plugin-customer-display';
|
|
18
|
+
/**
|
|
19
|
+
* WHO A PAIRED CUSTOMER DISPLAY IS, for a plugin's server (AGL-4042).
|
|
20
|
+
*
|
|
21
|
+
* A screen's page on a paired display (`plugin-customer-display`) has no
|
|
22
|
+
* staff session. It sends its own plugin's server the credential the display
|
|
23
|
+
* was paired with, and that server asks here who the display is: the store
|
|
24
|
+
* it is paired to, the screen it is showing now, and who put that screen
|
|
25
|
+
* there. The pairing IS the authorization: staff paired the display to one
|
|
26
|
+
* store, so what it shows is that store's to show, for as long as it shows
|
|
27
|
+
* it. Nothing is minted per screen and nothing has to be opened or closed.
|
|
28
|
+
*
|
|
29
|
+
* The plugin that pairs displays registers the one verifier, from its
|
|
30
|
+
* `consoleServerDeclarations`; the plugin asking never learns which plugin
|
|
31
|
+
* that is, and core names neither. With no verifier registered, or one that
|
|
32
|
+
* throws, every credential is refused: a display nobody can vouch for
|
|
33
|
+
* signs nothing.
|
|
34
|
+
*/
|
|
35
|
+
/** A paired display, as its verifier vouches for it. */
|
|
36
|
+
export interface VerifiedCustomerDisplay {
|
|
37
|
+
/** The store (site) the display is paired to. */
|
|
38
|
+
hostId: string;
|
|
39
|
+
/** The workspace that owns that store. */
|
|
40
|
+
orgId: string;
|
|
41
|
+
/** The plugin screen the display is showing now, or `null` for the register's own screens. */
|
|
42
|
+
screen: CustomerDisplayScreen | null;
|
|
43
|
+
/** The staff member (uid) who put {@link screen} on the display, when it is showing one. */
|
|
44
|
+
shownBy: string | null;
|
|
45
|
+
}
|
|
46
|
+
export interface CustomerDisplayVerifier {
|
|
47
|
+
/** The display a credential opens, or `null` for one that opens none. May throw. */
|
|
48
|
+
verify(credential: string): Promise<VerifiedCustomerDisplay | null>;
|
|
49
|
+
}
|
|
50
|
+
export declare const CUSTOMER_DISPLAY_VERIFIER: import("./plugin-services").PluginServiceContract<CustomerDisplayVerifier>;
|
|
51
|
+
/**
|
|
52
|
+
* Fills the slot. The owner is the loader's marker when a register fn is
|
|
53
|
+
* running, else `options.pluginId`; a second plugin's verifier is refused
|
|
54
|
+
* naming both.
|
|
55
|
+
*/
|
|
56
|
+
export declare function registerCustomerDisplayVerifier(verifier: CustomerDisplayVerifier, options?: {
|
|
57
|
+
pluginId?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
/**
|
|
60
|
+
* The display a credential opens, or `null`: a credential out of shape, no
|
|
61
|
+
* verifier in this process, a verifier that refuses it or throws, or an
|
|
62
|
+
* answer out of shape. A screen in the answer is passed through core's own
|
|
63
|
+
* sanitizer, so a caller compares a screen in the shape it asked for.
|
|
64
|
+
*/
|
|
65
|
+
export declare function verifyCustomerDisplay(credential: unknown): Promise<VerifiedCustomerDisplay | null>;
|
|
66
|
+
/**
|
|
67
|
+
* Whether a verified display is showing a plugin's screen: its plugin and
|
|
68
|
+
* page, and every parameter given here with the same value. What a screen's
|
|
69
|
+
* server asks before it acts for the display, and what its page asks to
|
|
70
|
+
* follow the register: once the register shows something else, the answer
|
|
71
|
+
* is `false`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function customerDisplayShows(display: VerifiedCustomerDisplay | null, screen: {
|
|
74
|
+
pluginId: string;
|
|
75
|
+
path: string;
|
|
76
|
+
params?: Record<string, string>;
|
|
77
|
+
}): boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import { sanitizeCustomerDisplayScreen } from "./plugin-customer-display.js";
|
|
19
|
+
export const CUSTOMER_DISPLAY_VERIFIER = definePluginServiceContract('core.customer-display.verifier', {
|
|
20
|
+
multiple: false
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Fills the slot. The owner is the loader's marker when a register fn is
|
|
24
|
+
* running, else `options.pluginId`; a second plugin's verifier is refused
|
|
25
|
+
* naming both.
|
|
26
|
+
*/ export function registerCustomerDisplayVerifier(verifier, options) {
|
|
27
|
+
registerPluginService(CUSTOMER_DISPLAY_VERIFIER, verifier, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
28
|
+
pluginId: options.pluginId
|
|
29
|
+
} : {}));
|
|
30
|
+
}
|
|
31
|
+
const CREDENTIAL = /^[A-Za-z0-9_.~-]{16,512}$/;
|
|
32
|
+
const ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
33
|
+
/**
|
|
34
|
+
* The display a credential opens, or `null`: a credential out of shape, no
|
|
35
|
+
* verifier in this process, a verifier that refuses it or throws, or an
|
|
36
|
+
* answer out of shape. A screen in the answer is passed through core's own
|
|
37
|
+
* sanitizer, so a caller compares a screen in the shape it asked for.
|
|
38
|
+
*/ export async function verifyCustomerDisplay(credential) {
|
|
39
|
+
var _answer_hostId, _answer_orgId;
|
|
40
|
+
var _resolvePluginServices_;
|
|
41
|
+
if (typeof credential !== 'string' || !CREDENTIAL.test(credential)) return null;
|
|
42
|
+
const verifier = (_resolvePluginServices_ = resolvePluginServices(CUSTOMER_DISPLAY_VERIFIER)[0]) == null ? void 0 : _resolvePluginServices_.impl;
|
|
43
|
+
if (!verifier) return null;
|
|
44
|
+
let answer;
|
|
45
|
+
try {
|
|
46
|
+
answer = await verifier.verify(credential);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('[customer-display] the verifier failed', error);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (!answer || !ID.test(String((_answer_hostId = answer.hostId) != null ? _answer_hostId : '')) || !ID.test(String((_answer_orgId = answer.orgId) != null ? _answer_orgId : ''))) return null;
|
|
52
|
+
const screen = answer.screen ? sanitizeCustomerDisplayScreen(answer.screen) : null;
|
|
53
|
+
const shownBy = screen && typeof answer.shownBy === 'string' && ID.test(answer.shownBy) ? answer.shownBy : null;
|
|
54
|
+
return {
|
|
55
|
+
hostId: answer.hostId,
|
|
56
|
+
orgId: answer.orgId,
|
|
57
|
+
screen,
|
|
58
|
+
shownBy
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Whether a verified display is showing a plugin's screen: its plugin and
|
|
63
|
+
* page, and every parameter given here with the same value. What a screen's
|
|
64
|
+
* server asks before it acts for the display, and what its page asks to
|
|
65
|
+
* follow the register: once the register shows something else, the answer
|
|
66
|
+
* is `false`.
|
|
67
|
+
*/ export function customerDisplayShows(display, screen) {
|
|
68
|
+
var _screen_params;
|
|
69
|
+
const showing = display == null ? void 0 : display.screen;
|
|
70
|
+
if (!showing || showing.pluginId !== screen.pluginId || showing.path !== screen.path) return false;
|
|
71
|
+
return Object.entries((_screen_params = screen.params) != null ? _screen_params : {}).every(([key, value])=>showing.params[key] === value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=plugin-customer-display-verifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-customer-display-verifier.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'\nimport { sanitizeCustomerDisplayScreen, type CustomerDisplayScreen } from './plugin-customer-display'\n\n/**\n * WHO A PAIRED CUSTOMER DISPLAY IS, for a plugin's server (AGL-4042).\n *\n * A screen's page on a paired display (`plugin-customer-display`) has no\n * staff session. It sends its own plugin's server the credential the display\n * was paired with, and that server asks here who the display is: the store\n * it is paired to, the screen it is showing now, and who put that screen\n * there. The pairing IS the authorization: staff paired the display to one\n * store, so what it shows is that store's to show, for as long as it shows\n * it. Nothing is minted per screen and nothing has to be opened or closed.\n *\n * The plugin that pairs displays registers the one verifier, from its\n * `consoleServerDeclarations`; the plugin asking never learns which plugin\n * that is, and core names neither. With no verifier registered, or one that\n * throws, every credential is refused: a display nobody can vouch for\n * signs nothing.\n */\n\n/** A paired display, as its verifier vouches for it. */\nexport interface VerifiedCustomerDisplay {\n /** The store (site) the display is paired to. */\n hostId: string\n /** The workspace that owns that store. */\n orgId: string\n /** The plugin screen the display is showing now, or `null` for the register's own screens. */\n screen: CustomerDisplayScreen | null\n /** The staff member (uid) who put {@link screen} on the display, when it is showing one. */\n shownBy: string | null\n}\n\nexport interface CustomerDisplayVerifier {\n /** The display a credential opens, or `null` for one that opens none. May throw. */\n verify(credential: string): Promise<VerifiedCustomerDisplay | null>\n}\n\nexport const CUSTOMER_DISPLAY_VERIFIER = definePluginServiceContract<CustomerDisplayVerifier>(\n 'core.customer-display.verifier',\n { multiple: false },\n)\n\n/**\n * Fills the slot. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; a second plugin's verifier is refused\n * naming both.\n */\nexport function registerCustomerDisplayVerifier(\n verifier: CustomerDisplayVerifier,\n options?: { pluginId?: string },\n): void {\n registerPluginService(CUSTOMER_DISPLAY_VERIFIER, verifier, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\nconst CREDENTIAL = /^[A-Za-z0-9_.~-]{16,512}$/\nconst ID = /^[A-Za-z0-9_-]{1,128}$/\n\n/**\n * The display a credential opens, or `null`: a credential out of shape, no\n * verifier in this process, a verifier that refuses it or throws, or an\n * answer out of shape. A screen in the answer is passed through core's own\n * sanitizer, so a caller compares a screen in the shape it asked for.\n */\nexport async function verifyCustomerDisplay(credential: unknown): Promise<VerifiedCustomerDisplay | null> {\n if (typeof credential !== 'string' || !CREDENTIAL.test(credential)) return null\n const verifier = resolvePluginServices(CUSTOMER_DISPLAY_VERIFIER)[0]?.impl\n if (!verifier) return null\n let answer: VerifiedCustomerDisplay | null\n try {\n answer = await verifier.verify(credential)\n } catch (error) {\n console.error('[customer-display] the verifier failed', error)\n return null\n }\n if (!answer || !ID.test(String(answer.hostId ?? '')) || !ID.test(String(answer.orgId ?? ''))) return null\n const screen = answer.screen ? sanitizeCustomerDisplayScreen(answer.screen) : null\n const shownBy = screen && typeof answer.shownBy === 'string' && ID.test(answer.shownBy) ? answer.shownBy : null\n return { hostId: answer.hostId, orgId: answer.orgId, screen, shownBy }\n}\n\n/**\n * Whether a verified display is showing a plugin's screen: its plugin and\n * page, and every parameter given here with the same value. What a screen's\n * server asks before it acts for the display, and what its page asks to\n * follow the register: once the register shows something else, the answer\n * is `false`.\n */\nexport function customerDisplayShows(\n display: VerifiedCustomerDisplay | null,\n screen: { pluginId: string; path: string; params?: Record<string, string> },\n): boolean {\n const showing = display?.screen\n if (!showing || showing.pluginId !== screen.pluginId || showing.path !== screen.path) return false\n return Object.entries(screen.params ?? {}).every(([key, value]) => showing.params[key] === value)\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","sanitizeCustomerDisplayScreen","CUSTOMER_DISPLAY_VERIFIER","multiple","registerCustomerDisplayVerifier","verifier","options","pluginId","CREDENTIAL","ID","verifyCustomerDisplay","credential","answer","test","impl","verify","error","console","String","hostId","orgId","screen","shownBy","customerDisplayShows","display","showing","path","Object","entries","params","every","key","value"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAC1B,SAASC,6BAA6B,QAAoC,+BAA2B;AAqCrG,OAAO,MAAMC,4BAA4BJ,4BACvC,kCACA;IAAEK,UAAU;AAAM,GACnB;AAED;;;;CAIC,GACD,OAAO,SAASC,gCACdC,QAAiC,EACjCC,OAA+B;IAE/BP,sBAAsBG,2BAA2BG,UAAU,aACrDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,MAAMC,aAAa;AACnB,MAAMC,KAAK;AAEX;;;;;CAKC,GACD,OAAO,eAAeC,sBAAsBC,UAAmB;QAW9BC,gBAAyCA;QATvDZ;IADjB,IAAI,OAAOW,eAAe,YAAY,CAACH,WAAWK,IAAI,CAACF,aAAa,OAAO;IAC3E,MAAMN,YAAWL,0BAAAA,sBAAsBE,0BAA0B,CAAC,EAAE,qBAAnDF,wBAAqDc,IAAI;IAC1E,IAAI,CAACT,UAAU,OAAO;IACtB,IAAIO;IACJ,IAAI;QACFA,SAAS,MAAMP,SAASU,MAAM,CAACJ;IACjC,EAAE,OAAOK,OAAO;QACdC,QAAQD,KAAK,CAAC,0CAA0CA;QACxD,OAAO;IACT;IACA,IAAI,CAACJ,UAAU,CAACH,GAAGI,IAAI,CAACK,QAAON,iBAAAA,OAAOO,MAAM,YAAbP,iBAAiB,QAAQ,CAACH,GAAGI,IAAI,CAACK,QAAON,gBAAAA,OAAOQ,KAAK,YAAZR,gBAAgB,MAAM,OAAO;IACrG,MAAMS,SAAST,OAAOS,MAAM,GAAGpB,8BAA8BW,OAAOS,MAAM,IAAI;IAC9E,MAAMC,UAAUD,UAAU,OAAOT,OAAOU,OAAO,KAAK,YAAYb,GAAGI,IAAI,CAACD,OAAOU,OAAO,IAAIV,OAAOU,OAAO,GAAG;IAC3G,OAAO;QAAEH,QAAQP,OAAOO,MAAM;QAAEC,OAAOR,OAAOQ,KAAK;QAAEC;QAAQC;IAAQ;AACvE;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,qBACdC,OAAuC,EACvCH,MAA2E;QAIrDA;IAFtB,MAAMI,UAAUD,2BAAAA,QAASH,MAAM;IAC/B,IAAI,CAACI,WAAWA,QAAQlB,QAAQ,KAAKc,OAAOd,QAAQ,IAAIkB,QAAQC,IAAI,KAAKL,OAAOK,IAAI,EAAE,OAAO;IAC7F,OAAOC,OAAOC,OAAO,EAACP,iBAAAA,OAAOQ,MAAM,YAAbR,iBAAiB,CAAC,GAAGS,KAAK,CAAC,CAAC,CAACC,KAAKC,MAAM,GAAKP,QAAQI,MAAM,CAACE,IAAI,KAAKC;AAC7F"}
|