@aglyn/aglyn 1.0.0-beta.233 → 1.0.0-beta.235

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-labels.d.ts +117 -0
  3. package/src/lib/app-utils/activity-labels.js +343 -0
  4. package/src/lib/app-utils/activity-labels.js.map +1 -0
  5. package/src/lib/app-utils/admin-audit-index.d.ts +11 -0
  6. package/src/lib/app-utils/admin-audit-index.js +12 -1
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  8. package/src/lib/app-utils/advertising-consent.d.ts +111 -0
  9. package/src/lib/app-utils/advertising-consent.js +216 -0
  10. package/src/lib/app-utils/advertising-consent.js.map +1 -0
  11. package/src/lib/app-utils/advertising-events.d.ts +98 -0
  12. package/src/lib/app-utils/advertising-events.js +340 -0
  13. package/src/lib/app-utils/advertising-events.js.map +1 -0
  14. package/src/lib/app-utils/advertising-tag-mounts.js +15 -8
  15. package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -1
  16. package/src/lib/app-utils/advertising-tags.d.ts +54 -8
  17. package/src/lib/app-utils/advertising-tags.js +126 -28
  18. package/src/lib/app-utils/advertising-tags.js.map +1 -1
  19. package/src/lib/app-utils/analytics-events.d.ts +15 -86
  20. package/src/lib/app-utils/analytics-events.js +21 -6
  21. package/src/lib/app-utils/analytics-events.js.map +1 -1
  22. package/src/lib/app-utils/consent-banner-ui.d.ts +14 -0
  23. package/src/lib/app-utils/consent-banner-ui.js +23 -2
  24. package/src/lib/app-utils/consent-banner-ui.js.map +1 -1
  25. package/src/lib/app-utils/docs-help-section-excerpt-text.d.ts +14 -0
  26. package/src/lib/app-utils/docs-help-section-excerpt-text.js +31 -0
  27. package/src/lib/app-utils/docs-help-section-excerpt-text.js.map +1 -0
  28. package/src/lib/app-utils/docs-help-section-excerpt.d.ts +13 -0
  29. package/src/lib/app-utils/docs-help-section-excerpt.js +37 -0
  30. package/src/lib/app-utils/docs-help-section-excerpt.js.map +1 -0
  31. package/src/lib/app-utils/docs-help-sections.generated.d.ts +31 -0
  32. package/src/lib/app-utils/docs-help-sections.generated.js +660 -0
  33. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -0
  34. package/src/lib/app-utils/docs-help.d.ts +16 -4
  35. package/src/lib/app-utils/docs-help.generated.d.ts +171 -34
  36. package/src/lib/app-utils/docs-help.generated.js +1104 -3
  37. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  38. package/src/lib/app-utils/docs-help.js +25 -6
  39. package/src/lib/app-utils/docs-help.js.map +1 -1
  40. package/src/lib/app-utils/docs-index.generated.js +1008 -91
  41. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  42. package/src/lib/app-utils/health-report.js +12 -0
  43. package/src/lib/app-utils/health-report.js.map +1 -1
  44. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  45. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -1
  46. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  47. package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
  48. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  49. package/src/lib/app-utils/variables.d.ts +2 -0
  50. package/src/lib/app-utils/variables.js +3 -0
  51. package/src/lib/app-utils/variables.js.map +1 -1
  52. package/src/lib/app-utils/visitor-consent.d.ts +34 -0
  53. package/src/lib/app-utils/visitor-consent.js +49 -2
  54. package/src/lib/app-utils/visitor-consent.js.map +1 -1
  55. package/src/lib/app-utils/where-used-summary.d.ts +41 -0
  56. package/src/lib/app-utils/where-used-summary.js +33 -0
  57. package/src/lib/app-utils/where-used-summary.js.map +1 -0
  58. package/src/lib/app-utils/where-used.d.ts +2 -20
  59. package/src/lib/app-utils/where-used.js +13 -12
  60. package/src/lib/app-utils/where-used.js.map +1 -1
  61. package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
  62. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  63. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +7 -0
  64. package/src/lib/plugin-manager/first-party-plugins.generated.js +166 -2
  65. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  66. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +51 -0
  67. package/src/lib/plugin-manager/plugin-activity-actions.js +43 -0
  68. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  69. package/src/lib/plugin-manager/plugin-advertising-conversions.d.ts +84 -0
  70. package/src/lib/plugin-manager/plugin-advertising-conversions.js +86 -0
  71. package/src/lib/plugin-manager/plugin-advertising-conversions.js.map +1 -0
  72. package/src/lib/plugin-manager/plugin-config.d.ts +23 -0
  73. package/src/lib/plugin-manager/plugin-config.js.map +1 -1
  74. package/src/lib/plugin-manager/plugin-local-deliveries.d.ts +161 -0
  75. package/src/lib/plugin-manager/plugin-local-deliveries.js +46 -0
  76. package/src/lib/plugin-manager/plugin-local-deliveries.js.map +1 -0
  77. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
  78. package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
  79. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
  80. package/src/lib/plugin-manager/plugin-order-email-copies.d.ts +93 -0
  81. package/src/lib/plugin-manager/plugin-order-email-copies.js +104 -0
  82. package/src/lib/plugin-manager/plugin-order-email-copies.js.map +1 -0
  83. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +7 -0
  84. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -1
  85. package/src/lib/plugin-manager/plugin-site-csp.d.ts +58 -0
  86. package/src/lib/plugin-manager/plugin-site-csp.js +103 -0
  87. package/src/lib/plugin-manager/plugin-site-csp.js.map +1 -0
  88. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
  89. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
  90. package/src/lib/plugin-manager/site-page-hooks.d.ts +13 -0
  91. package/src/lib/plugin-manager/site-page-hooks.js +31 -0
  92. package/src/lib/plugin-manager/site-page-hooks.js.map +1 -1
  93. package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
  94. package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
  95. package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
@@ -0,0 +1,161 @@
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 { PluginShippingAddress } from './plugin-shipping-rates';
18
+ /**
19
+ * A seller's local deliveries, as a courier service sees them (AGL-3695).
20
+ *
21
+ * A seller that delivers its own goods in town keeps a delivery on each such
22
+ * record: where it leaves from, where it goes, the window the buyer booked,
23
+ * and where it stands (`scheduled` → `out_for_delivery` → `delivered`, or
24
+ * `failed`). A plugin that books an outside courier for that drop needs to
25
+ * read those facts and to write back what the courier did — and must not
26
+ * read the seller's documents for either. The seller registers
27
+ * {@link PluginLocalDeliveryRecords}; the courier plugin asks it.
28
+ *
29
+ * Every write is the seller's own: its transition rule, its timeline, the
30
+ * buyer's "out for delivery" and "delivered" messages and the handover that
31
+ * fulfills the record all stay inside the seller, so a drop a courier makes
32
+ * and one the store's own driver makes land the same way.
33
+ *
34
+ * The words are a seller's and a courier's, never one catalog's: a record is
35
+ * anything with goods taken to a door — an order, a rental, a catering job.
36
+ *
37
+ * Import this module by its own subpath
38
+ * (`@aglyn/aglyn/plugin-manager/plugin-local-deliveries`); it is not in the
39
+ * barrel.
40
+ */
41
+ /** Where the seller's delivery stands. `failed` is a missed drop that may go out again. */
42
+ export type PluginLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed';
43
+ /**
44
+ * Where an outside courier's run stands, in a courier-neutral word.
45
+ * `returning` and `returned` are a drop that did not happen and is coming
46
+ * back to the store.
47
+ */
48
+ export type PluginCourierState = 'requested' | 'assigned' | 'at_pickup' | 'picked_up' | 'at_dropoff' | 'delivered' | 'cancelled' | 'returning' | 'returned';
49
+ export declare const PLUGIN_COURIER_STATES: readonly PluginCourierState[];
50
+ /** One end of the drop. */
51
+ export interface PluginLocalDeliveryPlace {
52
+ /** A business at the pickup end; the person at the drop-off end. */
53
+ name: string;
54
+ address: PluginShippingAddress;
55
+ /** E.164, when the seller knows one. */
56
+ phone?: string;
57
+ /** What the courier does on arrival, in the seller's or the buyer's words. */
58
+ instructions?: string;
59
+ }
60
+ /** An outside courier's run on a record, as the seller keeps it. */
61
+ export interface PluginRecordCourier {
62
+ /** The courier plugin's provider id: `doordash`. */
63
+ provider: string;
64
+ /** What a person calls it: `DoorDash`. */
65
+ providerLabel: string;
66
+ /** The courier's reference for the run. */
67
+ deliveryRef: string;
68
+ state: PluginCourierState;
69
+ /** The courier's own live tracking page, https. */
70
+ trackingUrl?: string;
71
+ /** When the courier expects to reach the door, epoch ms. */
72
+ etaMs?: number;
73
+ /** When the courier expects to collect, epoch ms. */
74
+ pickupEtaMs?: number;
75
+ /** Why a run was canceled or came back, in the courier's words. */
76
+ reason?: string;
77
+ /** Whether the run is in the courier's test environment: nobody is coming. */
78
+ testMode?: boolean;
79
+ updatedAtMs: number;
80
+ }
81
+ /** A record with a local delivery on it. */
82
+ export interface PluginLocalDeliveryRecord {
83
+ hostId: string;
84
+ recordId: string;
85
+ /** What a person calls it: `#1042`. */
86
+ displayRef: string;
87
+ /** The seller's own status word for the record, shown as is. */
88
+ sellerStatus: string;
89
+ status: PluginLocalDeliveryStatus;
90
+ /**
91
+ * Whether the seller would hand this drop to a courier now: paid, not
92
+ * canceled or refunded, and not already delivered.
93
+ */
94
+ dispatchable: boolean;
95
+ /** ISO-4217, lower case. */
96
+ currency: string;
97
+ /** What the goods are worth, for the courier's cover. */
98
+ valueCents: number;
99
+ /** Units in the drop. */
100
+ itemCount: number;
101
+ /** Where the drop leaves from; `null` when the seller has no full street address for it. */
102
+ pickup: PluginLocalDeliveryPlace | null;
103
+ /** Where it goes; `null` when the record has no street address. */
104
+ dropoff: PluginLocalDeliveryPlace | null;
105
+ /** The window the buyer booked, epoch ms. */
106
+ windowStartMs?: number;
107
+ windowEndMs?: number;
108
+ /** Whether the record was paid in a payment provider's test mode: no money moved. */
109
+ testMode: boolean;
110
+ /** The courier run the seller last recorded, or `null`. */
111
+ courier: PluginRecordCourier | null;
112
+ }
113
+ /** A courier run to record, and the delivery step it brings. */
114
+ export interface PluginRecordCourierWrite {
115
+ hostId: string;
116
+ recordId: string;
117
+ /** The run as it now stands; `null` clears it (the merchant canceled it and will deliver). */
118
+ courier: PluginRecordCourier | null;
119
+ /**
120
+ * The step this run moves the delivery to, under the seller's rules —
121
+ * `failed` with {@link reason} when the courier canceled or brought it
122
+ * back. Absent writes the run and moves nothing.
123
+ */
124
+ move?: Exclude<PluginLocalDeliveryStatus, 'scheduled'>;
125
+ reason?: string;
126
+ }
127
+ export type PluginRecordCourierOutcome = {
128
+ outcome: 'recorded';
129
+ status: PluginLocalDeliveryStatus;
130
+ }
131
+ /** The record already said this, so nothing was written. */
132
+ | {
133
+ outcome: 'unchanged';
134
+ status: PluginLocalDeliveryStatus;
135
+ } | {
136
+ outcome: 'no_such_record';
137
+ }
138
+ /** The record is not a local delivery. */
139
+ | {
140
+ outcome: 'not_local_delivery';
141
+ }
142
+ /** The seller's rules refused the step; the run itself was recorded. `from` is the status that refused. */
143
+ | {
144
+ outcome: 'blocked';
145
+ from: string;
146
+ };
147
+ export interface PluginLocalDeliveryRecords {
148
+ /** One record, or `null` when the site has none by that id or it is not a local delivery. */
149
+ read(hostId: string, recordId: string): Promise<PluginLocalDeliveryRecord | null>;
150
+ /** Writes a courier run, and the step it brings, under the seller's own rules. */
151
+ recordCourier(write: PluginRecordCourierWrite): Promise<PluginRecordCourierOutcome>;
152
+ }
153
+ export declare const PLUGIN_LOCAL_DELIVERY_RECORDS: import("./plugin-services").PluginServiceContract<PluginLocalDeliveryRecords>;
154
+ /** Registers the seller. A second plugin is refused naming both. */
155
+ export declare function registerPluginLocalDeliveryRecords(records: PluginLocalDeliveryRecords, options?: {
156
+ pluginId?: string;
157
+ }): void;
158
+ /** The seller, or `null` when no plugin registered one. */
159
+ export declare function pluginLocalDeliveryRecords(): PluginLocalDeliveryRecords | null;
160
+ /** Whether a courier run is over: nothing more will happen on it. */
161
+ export declare function pluginCourierStateIsFinal(state: PluginCourierState): boolean;
@@ -0,0 +1,46 @@
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
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
18
+ export const PLUGIN_COURIER_STATES = [
19
+ 'requested',
20
+ 'assigned',
21
+ 'at_pickup',
22
+ 'picked_up',
23
+ 'at_dropoff',
24
+ 'delivered',
25
+ 'cancelled',
26
+ 'returning',
27
+ 'returned'
28
+ ];
29
+ export const PLUGIN_LOCAL_DELIVERY_RECORDS = definePluginServiceContract('core.local-delivery-records', {
30
+ multiple: false
31
+ });
32
+ /** Registers the seller. A second plugin is refused naming both. */ export function registerPluginLocalDeliveryRecords(records, options) {
33
+ registerPluginService(PLUGIN_LOCAL_DELIVERY_RECORDS, records, _extends({}, (options == null ? void 0 : options.pluginId) ? {
34
+ pluginId: options.pluginId
35
+ } : {}));
36
+ }
37
+ /** The seller, or `null` when no plugin registered one. */ export function pluginLocalDeliveryRecords() {
38
+ var _ref;
39
+ var _resolvePluginServices_;
40
+ return (_ref = (_resolvePluginServices_ = resolvePluginServices(PLUGIN_LOCAL_DELIVERY_RECORDS)[0]) == null ? void 0 : _resolvePluginServices_.impl) != null ? _ref : null;
41
+ }
42
+ /** Whether a courier run is over: nothing more will happen on it. */ export function pluginCourierStateIsFinal(state) {
43
+ return state === 'delivered' || state === 'cancelled' || state === 'returned';
44
+ }
45
+
46
+ //# sourceMappingURL=plugin-local-deliveries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-local-deliveries.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginShippingAddress } from './plugin-shipping-rates'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A seller's local deliveries, as a courier service sees them (AGL-3695).\n *\n * A seller that delivers its own goods in town keeps a delivery on each such\n * record: where it leaves from, where it goes, the window the buyer booked,\n * and where it stands (`scheduled` → `out_for_delivery` → `delivered`, or\n * `failed`). A plugin that books an outside courier for that drop needs to\n * read those facts and to write back what the courier did — and must not\n * read the seller's documents for either. The seller registers\n * {@link PluginLocalDeliveryRecords}; the courier plugin asks it.\n *\n * Every write is the seller's own: its transition rule, its timeline, the\n * buyer's \"out for delivery\" and \"delivered\" messages and the handover that\n * fulfills the record all stay inside the seller, so a drop a courier makes\n * and one the store's own driver makes land the same way.\n *\n * The words are a seller's and a courier's, never one catalog's: a record is\n * anything with goods taken to a door — an order, a rental, a catering job.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-local-deliveries`); it is not in the\n * barrel.\n */\n\n/** Where the seller's delivery stands. `failed` is a missed drop that may go out again. */\nexport type PluginLocalDeliveryStatus = 'scheduled' | 'out_for_delivery' | 'delivered' | 'failed'\n\n/**\n * Where an outside courier's run stands, in a courier-neutral word.\n * `returning` and `returned` are a drop that did not happen and is coming\n * back to the store.\n */\nexport type PluginCourierState =\n | 'requested'\n | 'assigned'\n | 'at_pickup'\n | 'picked_up'\n | 'at_dropoff'\n | 'delivered'\n | 'cancelled'\n | 'returning'\n | 'returned'\n\nexport const PLUGIN_COURIER_STATES: readonly PluginCourierState[] = [\n 'requested',\n 'assigned',\n 'at_pickup',\n 'picked_up',\n 'at_dropoff',\n 'delivered',\n 'cancelled',\n 'returning',\n 'returned',\n]\n\n/** One end of the drop. */\nexport interface PluginLocalDeliveryPlace {\n /** A business at the pickup end; the person at the drop-off end. */\n name: string\n address: PluginShippingAddress\n /** E.164, when the seller knows one. */\n phone?: string\n /** What the courier does on arrival, in the seller's or the buyer's words. */\n instructions?: string\n}\n\n/** An outside courier's run on a record, as the seller keeps it. */\nexport interface PluginRecordCourier {\n /** The courier plugin's provider id: `doordash`. */\n provider: string\n /** What a person calls it: `DoorDash`. */\n providerLabel: string\n /** The courier's reference for the run. */\n deliveryRef: string\n state: PluginCourierState\n /** The courier's own live tracking page, https. */\n trackingUrl?: string\n /** When the courier expects to reach the door, epoch ms. */\n etaMs?: number\n /** When the courier expects to collect, epoch ms. */\n pickupEtaMs?: number\n /** Why a run was canceled or came back, in the courier's words. */\n reason?: string\n /** Whether the run is in the courier's test environment: nobody is coming. */\n testMode?: boolean\n updatedAtMs: number\n}\n\n/** A record with a local delivery on it. */\nexport interface PluginLocalDeliveryRecord {\n hostId: string\n recordId: string\n /** What a person calls it: `#1042`. */\n displayRef: string\n /** The seller's own status word for the record, shown as is. */\n sellerStatus: string\n status: PluginLocalDeliveryStatus\n /**\n * Whether the seller would hand this drop to a courier now: paid, not\n * canceled or refunded, and not already delivered.\n */\n dispatchable: boolean\n /** ISO-4217, lower case. */\n currency: string\n /** What the goods are worth, for the courier's cover. */\n valueCents: number\n /** Units in the drop. */\n itemCount: number\n /** Where the drop leaves from; `null` when the seller has no full street address for it. */\n pickup: PluginLocalDeliveryPlace | null\n /** Where it goes; `null` when the record has no street address. */\n dropoff: PluginLocalDeliveryPlace | null\n /** The window the buyer booked, epoch ms. */\n windowStartMs?: number\n windowEndMs?: number\n /** Whether the record was paid in a payment provider's test mode: no money moved. */\n testMode: boolean\n /** The courier run the seller last recorded, or `null`. */\n courier: PluginRecordCourier | null\n}\n\n/** A courier run to record, and the delivery step it brings. */\nexport interface PluginRecordCourierWrite {\n hostId: string\n recordId: string\n /** The run as it now stands; `null` clears it (the merchant canceled it and will deliver). */\n courier: PluginRecordCourier | null\n /**\n * The step this run moves the delivery to, under the seller's rules —\n * `failed` with {@link reason} when the courier canceled or brought it\n * back. Absent writes the run and moves nothing.\n */\n move?: Exclude<PluginLocalDeliveryStatus, 'scheduled'>\n reason?: string\n}\n\nexport type PluginRecordCourierOutcome =\n | { outcome: 'recorded'; status: PluginLocalDeliveryStatus }\n /** The record already said this, so nothing was written. */\n | { outcome: 'unchanged'; status: PluginLocalDeliveryStatus }\n | { outcome: 'no_such_record' }\n /** The record is not a local delivery. */\n | { outcome: 'not_local_delivery' }\n /** The seller's rules refused the step; the run itself was recorded. `from` is the status that refused. */\n | { outcome: 'blocked'; from: string }\n\nexport interface PluginLocalDeliveryRecords {\n /** One record, or `null` when the site has none by that id or it is not a local delivery. */\n read(hostId: string, recordId: string): Promise<PluginLocalDeliveryRecord | null>\n /** Writes a courier run, and the step it brings, under the seller's own rules. */\n recordCourier(write: PluginRecordCourierWrite): Promise<PluginRecordCourierOutcome>\n}\n\nexport const PLUGIN_LOCAL_DELIVERY_RECORDS =\n definePluginServiceContract<PluginLocalDeliveryRecords>('core.local-delivery-records', {\n multiple: false,\n })\n\n/** Registers the seller. A second plugin is refused naming both. */\nexport function registerPluginLocalDeliveryRecords(\n records: PluginLocalDeliveryRecords,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_LOCAL_DELIVERY_RECORDS, records, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The seller, or `null` when no plugin registered one. */\nexport function pluginLocalDeliveryRecords(): PluginLocalDeliveryRecords | null {\n return resolvePluginServices(PLUGIN_LOCAL_DELIVERY_RECORDS)[0]?.impl ?? null\n}\n\n/** Whether a courier run is over: nothing more will happen on it. */\nexport function pluginCourierStateIsFinal(state: PluginCourierState): boolean {\n return state === 'delivered' || state === 'cancelled' || state === 'returned'\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_COURIER_STATES","PLUGIN_LOCAL_DELIVERY_RECORDS","multiple","registerPluginLocalDeliveryRecords","records","options","pluginId","pluginLocalDeliveryRecords","impl","pluginCourierStateIsFinal","state"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA6C1B,OAAO,MAAMC,wBAAuD;IAClE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAoGD,OAAO,MAAMC,gCACXJ,4BAAwD,+BAA+B;IACrFK,UAAU;AACZ,GAAE;AAEJ,kEAAkE,GAClE,OAAO,SAASC,mCACdC,OAAmC,EACnCC,OAA+B;IAE/BP,sBAAsBG,+BAA+BG,SAAS,aACxDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPR;IAAP,gBAAOA,0BAAAA,sBAAsBE,8BAA8B,CAAC,EAAE,qBAAvDF,wBAAyDS,IAAI,mBAAI;AAC1E;AAEA,mEAAmE,GACnE,OAAO,SAASC,0BAA0BC,KAAyB;IACjE,OAAOA,UAAU,eAAeA,UAAU,eAAeA,UAAU;AACrE"}
@@ -0,0 +1,87 @@
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 { AglynHostMediaStockSource } from '../foundation/definitions/platform.types';
18
+ /**
19
+ * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).
20
+ *
21
+ * The media library takes bytes through the console's own ingress — the
22
+ * upload routes and the v1 API — which check the caller, the site's
23
+ * lockdown, the bytes' structure, the takedown list, the plan and the
24
+ * storage band before anything is written. A plugin that holds a picture on
25
+ * the server with no browser request behind it (an AI job copying a stock
26
+ * photo) has no route to post to. This is that route's door for it: the
27
+ * console registers the one implementation at boot (`instrumentation.ts`),
28
+ * the same direction as `core.site-cache`, and it applies the ingress checks
29
+ * as the member named, so a plugin never writes a media document or a
30
+ * Storage object itself.
31
+ *
32
+ * Images only: a server-held video or document has no caller yet.
33
+ *
34
+ * A process with no implementation answers `null` from
35
+ * {@link pluginMediaIngest}, and a caller keeps its own fallback.
36
+ */
37
+ export interface PluginMediaIngestRequest {
38
+ hostId: string;
39
+ /**
40
+ * The member the asset is stored as. Their access to the site, the site's
41
+ * lockdown and the workspace's plan and storage band are checked as for
42
+ * an upload they made themselves.
43
+ */
44
+ uid: string;
45
+ fileName: string;
46
+ /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */
47
+ contentType: string;
48
+ bytes: Uint8Array;
49
+ alt?: string;
50
+ description?: string;
51
+ /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */
52
+ stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>;
53
+ }
54
+ /** A stored asset, as a page names it. */
55
+ export interface PluginMediaAsset {
56
+ mediaId: string;
57
+ /** What an `image` node's `src` holds: the asset's media reference, else its URL. */
58
+ src: string;
59
+ width?: number;
60
+ height?: number;
61
+ }
62
+ export type PluginMediaIngestResult = ({
63
+ ok: true;
64
+ } & PluginMediaAsset) | {
65
+ ok: false;
66
+ status: number;
67
+ reason: string;
68
+ };
69
+ export interface PluginMediaIngest {
70
+ /** Stores one picture in the site's own library. */
71
+ ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>;
72
+ /**
73
+ * The site's live, public asset copied from a stock photo with this source
74
+ * key (`{provider}:{id}`), so a photo is never stored twice in one library.
75
+ */
76
+ findStockPhoto(input: {
77
+ hostId: string;
78
+ sourceKey: string;
79
+ }): Promise<PluginMediaAsset | null>;
80
+ }
81
+ /** One implementation: the app that holds the media ingress. */
82
+ export declare const PLUGIN_MEDIA_INGEST: import("./plugin-services").PluginServiceContract<PluginMediaIngest>;
83
+ export declare function registerPluginMediaIngest(ingest: PluginMediaIngest, options?: {
84
+ pluginId?: string;
85
+ }): void;
86
+ /** The registered ingest, or `null` in a process that registered none. */
87
+ export declare function pluginMediaIngest(): PluginMediaIngest | null;
@@ -0,0 +1,34 @@
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
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { definePluginServiceContract, registerPluginService, resolvePluginService } from "./plugin-services.js";
18
+ /** One implementation: the app that holds the media ingress. */ export const PLUGIN_MEDIA_INGEST = definePluginServiceContract('core.media-ingest', {
19
+ multiple: false
20
+ });
21
+ export function registerPluginMediaIngest(ingest, options) {
22
+ if (typeof (ingest == null ? void 0 : ingest.ingest) !== 'function' || typeof (ingest == null ? void 0 : ingest.findStockPhoto) !== 'function') {
23
+ throw new Error('a media ingest needs ingest and findStockPhoto');
24
+ }
25
+ registerPluginService(PLUGIN_MEDIA_INGEST, ingest, _extends({}, (options == null ? void 0 : options.pluginId) ? {
26
+ pluginId: options.pluginId
27
+ } : {}));
28
+ }
29
+ /** The registered ingest, or `null` in a process that registered none. */ export function pluginMediaIngest() {
30
+ var _resolvePluginService;
31
+ return (_resolvePluginService = resolvePluginService(PLUGIN_MEDIA_INGEST)) != null ? _resolvePluginService : null;
32
+ }
33
+
34
+ //# sourceMappingURL=plugin-media-ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-media-ingest.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AglynHostMediaStockSource } from '../foundation/definitions/platform.types'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n} from './plugin-services'\n\n/**\n * A SERVER PROCESS STORES A PICTURE IN A SITE'S MEDIA LIBRARY (AGL-3660).\n *\n * The media library takes bytes through the console's own ingress — the\n * upload routes and the v1 API — which check the caller, the site's\n * lockdown, the bytes' structure, the takedown list, the plan and the\n * storage band before anything is written. A plugin that holds a picture on\n * the server with no browser request behind it (an AI job copying a stock\n * photo) has no route to post to. This is that route's door for it: the\n * console registers the one implementation at boot (`instrumentation.ts`),\n * the same direction as `core.site-cache`, and it applies the ingress checks\n * as the member named, so a plugin never writes a media document or a\n * Storage object itself.\n *\n * Images only: a server-held video or document has no caller yet.\n *\n * A process with no implementation answers `null` from\n * {@link pluginMediaIngest}, and a caller keeps its own fallback.\n */\n\nexport interface PluginMediaIngestRequest {\n hostId: string\n /**\n * The member the asset is stored as. Their access to the site, the site's\n * lockdown and the workspace's plan and storage band are checked as for\n * an upload they made themselves.\n */\n uid: string\n fileName: string\n /** A raster image type: `image/jpeg`, `image/png`, `image/webp` or `image/gif`. */\n contentType: string\n bytes: Uint8Array\n alt?: string\n description?: string\n /** Where a photo copied from a stock library came from (`stockPhoto` on the asset). */\n stockPhoto?: Omit<AglynHostMediaStockSource, 'importedAt'>\n}\n\n/** A stored asset, as a page names it. */\nexport interface PluginMediaAsset {\n mediaId: string\n /** What an `image` node's `src` holds: the asset's media reference, else its URL. */\n src: string\n width?: number\n height?: number\n}\n\nexport type PluginMediaIngestResult =\n | ({ ok: true } & PluginMediaAsset)\n | { ok: false; status: number; reason: string }\n\nexport interface PluginMediaIngest {\n /** Stores one picture in the site's own library. */\n ingest(request: PluginMediaIngestRequest): Promise<PluginMediaIngestResult>\n /**\n * The site's live, public asset copied from a stock photo with this source\n * key (`{provider}:{id}`), so a photo is never stored twice in one library.\n */\n findStockPhoto(input: { hostId: string; sourceKey: string }): Promise<PluginMediaAsset | null>\n}\n\n/** One implementation: the app that holds the media ingress. */\nexport const PLUGIN_MEDIA_INGEST = definePluginServiceContract<PluginMediaIngest>(\n 'core.media-ingest',\n { multiple: false },\n)\n\nexport function registerPluginMediaIngest(\n ingest: PluginMediaIngest,\n options?: { pluginId?: string },\n): void {\n if (typeof ingest?.ingest !== 'function' || typeof ingest?.findStockPhoto !== 'function') {\n throw new Error('a media ingest needs ingest and findStockPhoto')\n }\n registerPluginService(PLUGIN_MEDIA_INGEST, ingest, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered ingest, or `null` in a process that registered none. */\nexport function pluginMediaIngest(): PluginMediaIngest | null {\n return resolvePluginService(PLUGIN_MEDIA_INGEST) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","PLUGIN_MEDIA_INGEST","multiple","registerPluginMediaIngest","ingest","options","findStockPhoto","Error","pluginId","pluginMediaIngest"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,QACf,uBAAmB;AA+D1B,8DAA8D,GAC9D,OAAO,MAAMC,sBAAsBH,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,OAAO,SAASC,0BACdC,MAAyB,EACzBC,OAA+B;IAE/B,IAAI,QAAOD,0BAAAA,OAAQA,MAAM,MAAK,cAAc,QAAOA,0BAAAA,OAAQE,cAAc,MAAK,YAAY;QACxF,MAAM,IAAIC,MAAM;IAClB;IACAR,sBAAsBE,qBAAqBG,QAAQ,aAC7CC,CAAAA,2BAAAA,QAASG,QAAQ,IAAG;QAAEA,UAAUH,QAAQG,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASC;QACPT;IAAP,QAAOA,wBAAAA,qBAAqBC,gCAArBD,wBAA6C;AACtD"}
@@ -0,0 +1,93 @@
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
+ * Copies of a buyer's order email, asked for by another plugin (AGL-3699).
19
+ *
20
+ * Some services learn about a sale by being copied on the mail the buyer
21
+ * already gets: Trustpilot gives every business an invitation address, and
22
+ * an order email blind-copied to it — with a small structured-data block
23
+ * naming the buyer and the order — becomes a review invitation. A plugin
24
+ * that wants that registers here; the seller asks just before it sends a
25
+ * buyer email about an order, adds whatever blind copies and data blocks
26
+ * come back, and tells each provider whether the message left.
27
+ *
28
+ * The words are an order email's: a site, the record, which email (its
29
+ * designable key), which moment, the buyer's address, and the order as the
30
+ * seller's public API shapes it. A provider answers `null` to add nothing.
31
+ * Import this module by its own subpath
32
+ * (`@aglyn/aglyn/plugin-manager/plugin-order-email-copies`); it is not in the
33
+ * barrel.
34
+ */
35
+ export interface PluginOrderEmailCopyRequest {
36
+ hostId: string;
37
+ recordId: string;
38
+ /** The designable email being sent, e.g. `order-shipped`. */
39
+ emailKey: string;
40
+ /** The moment in the order's life, e.g. `shipped`, `delivered`, `picked_up`. */
41
+ moment: string;
42
+ /** The buyer's address the email is to. */
43
+ recipient: string;
44
+ /** The order as the seller's public API returns it. */
45
+ order: Readonly<Record<string, unknown>>;
46
+ signal?: AbortSignal;
47
+ }
48
+ /** A structured-data block a copy's reader looks for in the HTML part. */
49
+ export interface PluginOrderEmailDataBlock {
50
+ /** The `<script type>`: `application/json`, `application/ld+json` or `application/json+<name>`. */
51
+ type: string;
52
+ json: unknown;
53
+ }
54
+ export interface PluginOrderEmailCopy {
55
+ /** Addresses blind-copied on the message. */
56
+ bcc: string[];
57
+ /** Blocks added to the HTML part, where only a machine reads them. */
58
+ dataBlocks?: PluginOrderEmailDataBlock[];
59
+ /**
60
+ * Told once whether the message left with these copies on it, so a
61
+ * provider that claimed the order can keep or release its claim. Never
62
+ * throws into the seller.
63
+ */
64
+ settle?: (sent: boolean) => Promise<void>;
65
+ }
66
+ export type PluginOrderEmailCopyProvider = (request: PluginOrderEmailCopyRequest) => Promise<PluginOrderEmailCopy | null>;
67
+ /** Joins the providers. Re-registering under the same plugin replaces its own. */
68
+ export declare function registerPluginOrderEmailCopies(provider: PluginOrderEmailCopyProvider, options?: {
69
+ pluginId?: string;
70
+ }): void;
71
+ /** The copies every provider asked for on one email, joined. */
72
+ export interface ResolvedOrderEmailCopies {
73
+ bcc: string[];
74
+ /** The data blocks as markup, ready to go before `</body>`; `''` when none. */
75
+ html: string;
76
+ /** Tells every provider that answered whether the message left. Never throws. */
77
+ settle: (sent: boolean) => Promise<void>;
78
+ }
79
+ /**
80
+ * A data block as a `<script>` element. The JSON is escaped so no value can
81
+ * close the element or open markup of its own.
82
+ */
83
+ export declare function orderEmailDataBlockHtml(block: PluginOrderEmailDataBlock): string;
84
+ /** Puts `markup` just before `</body>`, or at the end of a fragment with none. */
85
+ export declare function withOrderEmailDataBlocks(html: string, markup: string): string;
86
+ /**
87
+ * Asks every provider, together, and gives up on any that has not answered
88
+ * after `timeoutMs` — a late answer adds nothing and is told the message
89
+ * did not carry it. A provider that throws adds nothing. Never throws.
90
+ */
91
+ export declare function resolvePluginOrderEmailCopies(request: Omit<PluginOrderEmailCopyRequest, 'signal'>, options: {
92
+ timeoutMs: number;
93
+ }): Promise<ResolvedOrderEmailCopies>;
@@ -0,0 +1,104 @@
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 { getRegisteringPluginId } from "../app-utils/registering-plugin.js";
18
+ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
19
+ const PLUGIN_ORDER_EMAIL_COPIES = definePluginServiceContract('core.order-email-copies', {
20
+ multiple: true
21
+ });
22
+ /** Joins the providers. Re-registering under the same plugin replaces its own. */ export function registerPluginOrderEmailCopies(provider, options) {
23
+ var _getRegisteringPluginId;
24
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
25
+ registerPluginService(PLUGIN_ORDER_EMAIL_COPIES, provider, _extends({}, pluginId ? {
26
+ pluginId
27
+ } : {}));
28
+ }
29
+ const EMAIL = /^[^\s@<>",;]+@[^\s@<>",;]+\.[^\s@<>",;]+$/;
30
+ const DATA_TYPE = /^application\/(ld\+)?json(\+[a-z0-9-]{1,40})?$/;
31
+ /**
32
+ * A data block as a `<script>` element. The JSON is escaped so no value can
33
+ * close the element or open markup of its own.
34
+ */ export function orderEmailDataBlockHtml(block) {
35
+ var _block_json;
36
+ const json = JSON.stringify((_block_json = block.json) != null ? _block_json : null).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
37
+ return `<script type="${block.type}">${json}</script>`;
38
+ }
39
+ /** Puts `markup` just before `</body>`, or at the end of a fragment with none. */ export function withOrderEmailDataBlocks(html, markup) {
40
+ if (!markup) return html;
41
+ const close = html.toLowerCase().lastIndexOf('</body>');
42
+ return close === -1 ? `${html}${markup}` : `${html.slice(0, close)}${markup}${html.slice(close)}`;
43
+ }
44
+ /**
45
+ * Asks every provider, together, and gives up on any that has not answered
46
+ * after `timeoutMs` — a late answer adds nothing and is told the message
47
+ * did not carry it. A provider that throws adds nothing. Never throws.
48
+ */ export async function resolvePluginOrderEmailCopies(request, options) {
49
+ var _request_recipient;
50
+ const providers = resolvePluginServices(PLUGIN_ORDER_EMAIL_COPIES);
51
+ const empty = {
52
+ bcc: [],
53
+ html: '',
54
+ settle: async ()=>undefined
55
+ };
56
+ if (!providers.length || !EMAIL.test(String((_request_recipient = request.recipient) != null ? _request_recipient : '').trim())) return empty;
57
+ const controller = new AbortController();
58
+ const timer = setTimeout(()=>controller.abort(), Math.max(0, options.timeoutMs));
59
+ const late = new Promise((resolve)=>controller.signal.addEventListener('abort', ()=>resolve('late')));
60
+ const recipient = String(request.recipient).trim().toLowerCase();
61
+ try {
62
+ const answers = await Promise.all(providers.map(async (entry)=>{
63
+ const pending = entry.impl(_extends({}, request, {
64
+ signal: controller.signal
65
+ })).catch((error)=>{
66
+ console.error(`[order-email-copies] provider "${entry.pluginId}" failed for ${request.hostId}`, error);
67
+ return null;
68
+ });
69
+ const answer = await Promise.race([
70
+ pending,
71
+ late
72
+ ]);
73
+ if (answer === 'late') {
74
+ // It may still claim something after the deadline: tell it the
75
+ // message went without it once it does.
76
+ void pending.then((copy)=>{
77
+ var _copy_settle;
78
+ return void (copy == null ? void 0 : (_copy_settle = copy.settle) == null ? void 0 : _copy_settle.call(copy, false).catch(()=>undefined));
79
+ });
80
+ return null;
81
+ }
82
+ return answer;
83
+ }));
84
+ const copies = answers.filter((copy)=>Boolean(copy));
85
+ const bcc = [
86
+ ...new Set(copies.flatMap((copy)=>Array.isArray(copy.bcc) ? copy.bcc : []).map((address)=>String(address != null ? address : '').trim()).filter((address)=>EMAIL.test(address) && address.toLowerCase() !== recipient))
87
+ ];
88
+ const html = copies.flatMap((copy)=>Array.isArray(copy.dataBlocks) ? copy.dataBlocks : []).filter((block)=>{
89
+ var _block_type;
90
+ return block && DATA_TYPE.test(String((_block_type = block.type) != null ? _block_type : ''));
91
+ }).map(orderEmailDataBlockHtml).join('');
92
+ return {
93
+ bcc,
94
+ html,
95
+ settle: async (sent)=>{
96
+ await Promise.all(copies.map((copy)=>Promise.resolve().then(()=>copy.settle == null ? void 0 : copy.settle.call(copy, sent)).catch((error)=>console.error('[order-email-copies] settle failed', error))));
97
+ }
98
+ };
99
+ } finally{
100
+ clearTimeout(timer);
101
+ }
102
+ }
103
+
104
+ //# sourceMappingURL=plugin-order-email-copies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-order-email-copies.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 * Copies of a buyer's order email, asked for by another plugin (AGL-3699).\n *\n * Some services learn about a sale by being copied on the mail the buyer\n * already gets: Trustpilot gives every business an invitation address, and\n * an order email blind-copied to it — with a small structured-data block\n * naming the buyer and the order — becomes a review invitation. A plugin\n * that wants that registers here; the seller asks just before it sends a\n * buyer email about an order, adds whatever blind copies and data blocks\n * come back, and tells each provider whether the message left.\n *\n * The words are an order email's: a site, the record, which email (its\n * designable key), which moment, the buyer's address, and the order as the\n * seller's public API shapes it. A provider answers `null` to add nothing.\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-order-email-copies`); it is not in the\n * barrel.\n */\n\nexport interface PluginOrderEmailCopyRequest {\n hostId: string\n recordId: string\n /** The designable email being sent, e.g. `order-shipped`. */\n emailKey: string\n /** The moment in the order's life, e.g. `shipped`, `delivered`, `picked_up`. */\n moment: string\n /** The buyer's address the email is to. */\n recipient: string\n /** The order as the seller's public API returns it. */\n order: Readonly<Record<string, unknown>>\n signal?: AbortSignal\n}\n\n/** A structured-data block a copy's reader looks for in the HTML part. */\nexport interface PluginOrderEmailDataBlock {\n /** The `<script type>`: `application/json`, `application/ld+json` or `application/json+<name>`. */\n type: string\n json: unknown\n}\n\nexport interface PluginOrderEmailCopy {\n /** Addresses blind-copied on the message. */\n bcc: string[]\n /** Blocks added to the HTML part, where only a machine reads them. */\n dataBlocks?: PluginOrderEmailDataBlock[]\n /**\n * Told once whether the message left with these copies on it, so a\n * provider that claimed the order can keep or release its claim. Never\n * throws into the seller.\n */\n settle?: (sent: boolean) => Promise<void>\n}\n\nexport type PluginOrderEmailCopyProvider = (\n request: PluginOrderEmailCopyRequest,\n) => Promise<PluginOrderEmailCopy | null>\n\nconst PLUGIN_ORDER_EMAIL_COPIES = definePluginServiceContract<PluginOrderEmailCopyProvider>(\n 'core.order-email-copies',\n { multiple: true },\n)\n\n/** Joins the providers. Re-registering under the same plugin replaces its own. */\nexport function registerPluginOrderEmailCopies(\n provider: PluginOrderEmailCopyProvider,\n options?: { pluginId?: string },\n): void {\n const pluginId = getRegisteringPluginId() ?? options?.pluginId\n registerPluginService(PLUGIN_ORDER_EMAIL_COPIES, provider, {\n ...(pluginId ? { pluginId } : {}),\n })\n}\n\nconst EMAIL = /^[^\\s@<>\",;]+@[^\\s@<>\",;]+\\.[^\\s@<>\",;]+$/\nconst DATA_TYPE = /^application\\/(ld\\+)?json(\\+[a-z0-9-]{1,40})?$/\n\n/** The copies every provider asked for on one email, joined. */\nexport interface ResolvedOrderEmailCopies {\n bcc: string[]\n /** The data blocks as markup, ready to go before `</body>`; `''` when none. */\n html: string\n /** Tells every provider that answered whether the message left. Never throws. */\n settle: (sent: boolean) => Promise<void>\n}\n\n/**\n * A data block as a `<script>` element. The JSON is escaped so no value can\n * close the element or open markup of its own.\n */\nexport function orderEmailDataBlockHtml(block: PluginOrderEmailDataBlock): string {\n const json = JSON.stringify(block.json ?? null)\n .replace(/</g, '\\\\u003c')\n .replace(/>/g, '\\\\u003e')\n .replace(/&/g, '\\\\u0026')\n .replace(/\\u2028/g, '\\\\u2028')\n .replace(/\\u2029/g, '\\\\u2029')\n return `<script type=\"${block.type}\">${json}</script>`\n}\n\n/** Puts `markup` just before `</body>`, or at the end of a fragment with none. */\nexport function withOrderEmailDataBlocks(html: string, markup: string): string {\n if (!markup) return html\n const close = html.toLowerCase().lastIndexOf('</body>')\n return close === -1 ? `${html}${markup}` : `${html.slice(0, close)}${markup}${html.slice(close)}`\n}\n\n/**\n * Asks every provider, together, and gives up on any that has not answered\n * after `timeoutMs` — a late answer adds nothing and is told the message\n * did not carry it. A provider that throws adds nothing. Never throws.\n */\nexport async function resolvePluginOrderEmailCopies(\n request: Omit<PluginOrderEmailCopyRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<ResolvedOrderEmailCopies> {\n const providers = resolvePluginServices(PLUGIN_ORDER_EMAIL_COPIES)\n const empty: ResolvedOrderEmailCopies = { bcc: [], html: '', settle: async () => undefined }\n if (!providers.length || !EMAIL.test(String(request.recipient ?? '').trim())) return empty\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), Math.max(0, options.timeoutMs))\n const late = new Promise<'late'>((resolve) => controller.signal.addEventListener('abort', () => resolve('late')))\n const recipient = String(request.recipient).trim().toLowerCase()\n try {\n const answers = await Promise.all(\n providers.map(async (entry) => {\n const pending = entry.impl({ ...request, signal: controller.signal }).catch((error: unknown): null => {\n console.error(`[order-email-copies] provider \"${entry.pluginId}\" failed for ${request.hostId}`, error)\n return null\n })\n const answer = await Promise.race([pending, late])\n if (answer === 'late') {\n // It may still claim something after the deadline: tell it the\n // message went without it once it does.\n void pending.then((copy): void => void copy?.settle?.(false).catch((): undefined => undefined))\n return null\n }\n return answer\n }),\n )\n const copies = answers.filter((copy): copy is PluginOrderEmailCopy => Boolean(copy))\n const bcc = [\n ...new Set(\n copies\n .flatMap((copy) => (Array.isArray(copy.bcc) ? copy.bcc : []))\n .map((address) => String(address ?? '').trim())\n .filter((address) => EMAIL.test(address) && address.toLowerCase() !== recipient),\n ),\n ]\n const html = copies\n .flatMap((copy) => (Array.isArray(copy.dataBlocks) ? copy.dataBlocks : []))\n .filter((block) => block && DATA_TYPE.test(String(block.type ?? '')))\n .map(orderEmailDataBlockHtml)\n .join('')\n return {\n bcc,\n html,\n settle: async (sent) => {\n await Promise.all(\n copies.map((copy) =>\n Promise.resolve()\n .then(() => copy.settle?.(sent))\n .catch((error: unknown) => console.error('[order-email-copies] settle failed', error)),\n ),\n )\n },\n }\n } finally {\n clearTimeout(timer)\n }\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_ORDER_EMAIL_COPIES","multiple","registerPluginOrderEmailCopies","provider","options","pluginId","EMAIL","DATA_TYPE","orderEmailDataBlockHtml","block","json","JSON","stringify","replace","type","withOrderEmailDataBlocks","html","markup","close","toLowerCase","lastIndexOf","slice","resolvePluginOrderEmailCopies","request","providers","empty","bcc","settle","undefined","length","test","String","recipient","trim","controller","AbortController","timer","setTimeout","abort","Math","max","timeoutMs","late","Promise","resolve","signal","addEventListener","answers","all","map","entry","pending","impl","catch","error","console","hostId","answer","race","then","copy","copies","filter","Boolean","Set","flatMap","Array","isArray","address","dataBlocks","join","sent","clearTimeout"],"mappings":"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"}
@@ -119,6 +119,13 @@ export interface PluginShipmentWrite {
119
119
  /** The label's file, an https URL the seller may show beside the shipment. */
120
120
  labelUrl?: string;
121
121
  labelRef?: string;
122
+ /**
123
+ * What the label cost the seller, in integer cents of the record's
124
+ * currency (AGL-3693); absent when the label was bought in another
125
+ * currency or the cost is not known. A marketplace that pays the seller
126
+ * back for shipping (Faire's `maker_cost_cents`) is told it.
127
+ */
128
+ labelCostCents?: number;
122
129
  /** The member who bought the label, for the record's timeline. */
123
130
  actorUid?: string;
124
131
  }