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

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 (88) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +15 -2
  3. package/src/lib/app-utils/account-acquisition.js +20 -1
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/admin-audit-index.d.ts +3 -1
  6. package/src/lib/app-utils/admin-audit-index.js +9 -1
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  8. package/src/lib/app-utils/analytics-summary.d.ts +95 -0
  9. package/src/lib/app-utils/analytics-summary.js +113 -0
  10. package/src/lib/app-utils/analytics-summary.js.map +1 -0
  11. package/src/lib/app-utils/artifact-list-keys.js +12 -0
  12. package/src/lib/app-utils/artifact-list-keys.js.map +1 -1
  13. package/src/lib/app-utils/artifact-list-queries.d.ts +51 -0
  14. package/src/lib/app-utils/artifact-list-queries.js +321 -0
  15. package/src/lib/app-utils/artifact-list-queries.js.map +1 -0
  16. package/src/lib/app-utils/business-profile.d.ts +171 -0
  17. package/src/lib/app-utils/business-profile.js +277 -0
  18. package/src/lib/app-utils/business-profile.js.map +1 -0
  19. package/src/lib/app-utils/child-contract.d.ts +4 -1
  20. package/src/lib/app-utils/child-contract.js +5 -2
  21. package/src/lib/app-utils/child-contract.js.map +1 -1
  22. package/src/lib/app-utils/console-routes.d.ts +5 -0
  23. package/src/lib/app-utils/console-routes.js +1 -0
  24. package/src/lib/app-utils/console-routes.js.map +1 -1
  25. package/src/lib/app-utils/content-schema-type.d.ts +9 -0
  26. package/src/lib/app-utils/content-schema-type.js +4 -0
  27. package/src/lib/app-utils/content-schema-type.js.map +1 -1
  28. package/src/lib/app-utils/crm.d.ts +30 -2
  29. package/src/lib/app-utils/crm.js +77 -15
  30. package/src/lib/app-utils/crm.js.map +1 -1
  31. package/src/lib/app-utils/docs-help.generated.d.ts +58 -10
  32. package/src/lib/app-utils/docs-help.generated.js +118 -4
  33. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  34. package/src/lib/app-utils/docs-index.generated.js +320 -19
  35. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  36. package/src/lib/app-utils/entry-list-declaration.d.ts +33 -0
  37. package/src/lib/app-utils/entry-list-declaration.js +172 -0
  38. package/src/lib/app-utils/entry-list-declaration.js.map +1 -0
  39. package/src/lib/app-utils/first-version-seed.d.ts +19 -0
  40. package/src/lib/app-utils/first-version-seed.js +53 -0
  41. package/src/lib/app-utils/first-version-seed.js.map +1 -0
  42. package/src/lib/app-utils/local-business.d.ts +8 -0
  43. package/src/lib/app-utils/local-business.js +4 -0
  44. package/src/lib/app-utils/local-business.js.map +1 -1
  45. package/src/lib/app-utils/lockdown.js +1 -1
  46. package/src/lib/app-utils/lockdown.js.map +1 -1
  47. package/src/lib/app-utils/message-search.js +5 -2
  48. package/src/lib/app-utils/message-search.js.map +1 -1
  49. package/src/lib/app-utils/name-search.js +15 -3
  50. package/src/lib/app-utils/name-search.js.map +1 -1
  51. package/src/lib/app-utils/page-markdown.js +3 -0
  52. package/src/lib/app-utils/page-markdown.js.map +1 -1
  53. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +70 -0
  54. package/src/lib/app-utils/screen-analytics-aggregate.js +82 -0
  55. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -0
  56. package/src/lib/app-utils/screen-kind.d.ts +25 -0
  57. package/src/lib/app-utils/screen-kind.js +25 -0
  58. package/src/lib/app-utils/screen-kind.js.map +1 -0
  59. package/src/lib/app-utils/screen-route.d.ts +1 -5
  60. package/src/lib/app-utils/screen-route.js +2 -4
  61. package/src/lib/app-utils/screen-route.js.map +1 -1
  62. package/src/lib/app-utils/site-journey-steps.d.ts +38 -0
  63. package/src/lib/app-utils/site-journey-steps.js +54 -0
  64. package/src/lib/app-utils/site-journey-steps.js.map +1 -0
  65. package/src/lib/app-utils/site-journey.d.ts +2 -22
  66. package/src/lib/app-utils/site-journey.js +2 -32
  67. package/src/lib/app-utils/site-journey.js.map +1 -1
  68. package/src/lib/plugin-manager/feature-plugins.d.ts +19 -0
  69. package/src/lib/plugin-manager/feature-plugins.js +8 -0
  70. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  71. package/src/lib/plugin-manager/first-party-plugins.generated.js +124 -2
  72. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-channel-orders.d.ts +262 -0
  74. package/src/lib/plugin-manager/plugin-channel-orders.js +32 -0
  75. package/src/lib/plugin-manager/plugin-channel-orders.js.map +1 -0
  76. package/src/lib/plugin-manager/plugin-checkout-credits.d.ts +288 -0
  77. package/src/lib/plugin-manager/plugin-checkout-credits.js +196 -0
  78. package/src/lib/plugin-manager/plugin-checkout-credits.js.map +1 -0
  79. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +5 -0
  80. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
  81. package/src/lib/plugin-manager/plugin-product-writer.d.ts +166 -0
  82. package/src/lib/plugin-manager/plugin-product-writer.js +31 -0
  83. package/src/lib/plugin-manager/plugin-product-writer.js.map +1 -0
  84. package/src/lib/plugin-manager/plugin-shipping-rates.d.ts +10 -0
  85. package/src/lib/plugin-manager/plugin-shipping-rates.js.map +1 -1
  86. package/src/lib/plugin-manager/plugin-theme-presets.d.ts +40 -0
  87. package/src/lib/plugin-manager/plugin-theme-presets.js +44 -0
  88. package/src/lib/plugin-manager/plugin-theme-presets.js.map +1 -0
@@ -0,0 +1,196 @@
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 KEY = /^[a-z][a-z0-9-]{1,39}$/;
20
+ const PROVIDER_ID = /^([a-z][a-z0-9-]{0,63})\.([a-z][a-z0-9-]{1,39})$/;
21
+ /** The shape of a provider's `reference`: short, and safe in a document path segment. */ export const CHECKOUT_CREDIT_REFERENCE = /^[A-Za-z0-9:_-]{1,128}$/;
22
+ const PLUGIN_CHECKOUT_CREDITS = definePluginServiceContract('core.checkout-credits', {
23
+ multiple: true
24
+ });
25
+ /** Joins the providers. A plugin re-registering a key replaces its own. */ export function registerPluginCheckoutCredit(provider, options) {
26
+ var _ref, _getRegisteringPluginId;
27
+ if (!KEY.test(String((_ref = provider == null ? void 0 : provider.key) != null ? _ref : ''))) {
28
+ throw new Error(`checkout credit provider key "${provider == null ? void 0 : provider.key}" must be lower-case words and dashes`);
29
+ }
30
+ const pluginId = (_getRegisteringPluginId = getRegisteringPluginId()) != null ? _getRegisteringPluginId : options == null ? void 0 : options.pluginId;
31
+ registerPluginService(PLUGIN_CHECKOUT_CREDITS, provider, _extends({}, pluginId ? {
32
+ pluginId
33
+ } : {}, {
34
+ key: provider.key
35
+ }));
36
+ }
37
+ function resolved() {
38
+ return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).map((entry)=>({
39
+ providerId: `${entry.pluginId}.${entry.impl.key}`,
40
+ pluginId: entry.pluginId,
41
+ provider: entry.impl
42
+ }));
43
+ }
44
+ /** Whether any plugin registered a provider: a seller with none skips the field. */ export function hasPluginCheckoutCredits() {
45
+ return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).length > 0;
46
+ }
47
+ /**
48
+ * A code as typed — any case, spaces and dashes — in the one form providers
49
+ * see: upper case, letters, digits and single dashes, at most 40 characters.
50
+ * Empty when nothing usable was typed.
51
+ */ export function normalizeCheckoutCreditCode(value) {
52
+ return String(value != null ? value : '').toUpperCase().replace(/[^A-Z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
53
+ }
54
+ /** The provider that issues a code, or `null`. The first to recognize it answers. */ export function checkoutCreditProviderForCode(code) {
55
+ const normalized = normalizeCheckoutCreditCode(code);
56
+ if (!normalized) return null;
57
+ for (const entry of resolved()){
58
+ try {
59
+ if (entry.provider.recognizes(normalized)) return entry;
60
+ } catch (error) {
61
+ console.error(`[checkout-credits] "${entry.providerId}" failed to read a code`, error);
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /** One provider by the id a sale recorded, or `null` when it is gone. */ export function checkoutCreditProvider(providerId) {
67
+ var _resolved_find;
68
+ return (_resolved_find = resolved().find((entry)=>entry.providerId === providerId)) != null ? _resolved_find : null;
69
+ }
70
+ /**
71
+ * The providers a site offers on a channel, for the cart's field and the
72
+ * register's tender. Never throws: a provider that fails is not offered.
73
+ */ export async function offeredCheckoutCredits(input) {
74
+ const answers = await Promise.all(resolved().map(async (entry)=>{
75
+ const offered = await entry.provider.offered(input).catch((error)=>{
76
+ console.error(`[checkout-credits] "${entry.providerId}" offered() failed for ${input.hostId}`, error);
77
+ return false;
78
+ });
79
+ return offered ? {
80
+ providerId: entry.providerId,
81
+ label: entry.provider.label,
82
+ lookup: typeof entry.provider.lookup === 'function'
83
+ } : null;
84
+ }));
85
+ return answers.filter((answer)=>Boolean(answer));
86
+ }
87
+ /** An account answer held to the contract, or a refusal in its place. */ export function normalizeCheckoutCreditAccount(answer) {
88
+ var _answer_reference;
89
+ if (!answer || typeof answer !== 'object') {
90
+ return {
91
+ ok: false,
92
+ status: 404,
93
+ error: 'That code is not valid.'
94
+ };
95
+ }
96
+ if (answer.ok !== true) {
97
+ const status = Number.isInteger(answer.status) && answer.status >= 400 && answer.status < 500 ? answer.status : 409;
98
+ return {
99
+ ok: false,
100
+ status,
101
+ error: cleanText(answer.error, 160) || 'That code cannot be used.'
102
+ };
103
+ }
104
+ if (!CHECKOUT_CREDIT_REFERENCE.test(String((_answer_reference = answer.reference) != null ? _answer_reference : ''))) {
105
+ return {
106
+ ok: false,
107
+ status: 404,
108
+ error: 'That code is not valid.'
109
+ };
110
+ }
111
+ const available = answer.availableCents;
112
+ return _extends({
113
+ ok: true,
114
+ reference: answer.reference,
115
+ label: cleanText(answer.label, 40) || 'Store credit',
116
+ last4: cleanText(answer.last4, 4).toUpperCase(),
117
+ availableCents: Number.isSafeInteger(available) && available > 0 ? available : 0
118
+ }, answer.detail ? {
119
+ detail: cleanText(answer.detail, 120)
120
+ } : {});
121
+ }
122
+ /** Cents a provider says it held or took, as whole non-negative cents no larger than `ceiling`. */ export function boundedCreditCents(value, ceiling) {
123
+ const cents = typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0;
124
+ const max = Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : 0;
125
+ return Math.min(cents, max);
126
+ }
127
+ function cleanText(value, max) {
128
+ return Array.from(String(value != null ? value : ''), (char)=>char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char).join('').replace(/\s+/g, ' ').trim().slice(0, max);
129
+ }
130
+ /** The metadata key an online redemption rides under on the payment processor. */ export const CHECKOUT_CREDIT_METADATA_KEY = 'credit0';
131
+ /**
132
+ * The hold an online checkout placed, packed for a payment processor's
133
+ * metadata: one key holding `[providerId, reference, holdKey, cents, label, last4]`
134
+ * as JSON, inside a 500-character value. Never the code itself: metadata is
135
+ * readable on the merchant's dashboard, and a code is a bearer secret.
136
+ */ export function encodeCheckoutCreditMetadata(held) {
137
+ const entry = [
138
+ held.providerId,
139
+ held.reference,
140
+ held.holdKey.slice(0, 200),
141
+ held.amountCents,
142
+ cleanText(held.label, 40),
143
+ cleanText(held.last4, 4)
144
+ ];
145
+ return {
146
+ [CHECKOUT_CREDIT_METADATA_KEY]: JSON.stringify(entry)
147
+ };
148
+ }
149
+ /** Reads {@link encodeCheckoutCreditMetadata} back. Anything unreadable is `null`, never guessed. */ export function decodeCheckoutCreditMetadata(metadata) {
150
+ const raw = metadata == null ? void 0 : metadata[CHECKOUT_CREDIT_METADATA_KEY];
151
+ if (raw === undefined || raw === null || raw === '') return null;
152
+ let entry;
153
+ try {
154
+ entry = JSON.parse(String(raw));
155
+ } catch (unused) {
156
+ return null;
157
+ }
158
+ if (!Array.isArray(entry)) return null;
159
+ const [providerId, reference, holdKey, cents, label, last4] = entry;
160
+ if (!PROVIDER_ID.test(String(providerId != null ? providerId : ''))) return null;
161
+ if (!CHECKOUT_CREDIT_REFERENCE.test(String(reference != null ? reference : ''))) return null;
162
+ if (typeof holdKey !== 'string' || !holdKey) return null;
163
+ if (typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) return null;
164
+ return {
165
+ providerId: String(providerId),
166
+ reference: String(reference),
167
+ holdKey,
168
+ amountCents: cents,
169
+ label: cleanText(label, 40) || 'Store credit',
170
+ last4: cleanText(last4, 4)
171
+ };
172
+ }
173
+ /** The plugin id and key of a provider id. */ export function splitCheckoutCreditProviderId(providerId) {
174
+ const match = PROVIDER_ID.exec(String(providerId != null ? providerId : ''));
175
+ return match ? {
176
+ pluginId: match[1],
177
+ key: match[2]
178
+ } : null;
179
+ }
180
+ /** A recorded redemption, as a seller stores it and its events carry it. */ export function checkoutCreditSold(input) {
181
+ const parts = splitCheckoutCreditProviderId(input.providerId);
182
+ if (!parts || !CHECKOUT_CREDIT_REFERENCE.test(input.reference)) return null;
183
+ if (!Number.isSafeInteger(input.amountCents) || input.amountCents <= 0) return null;
184
+ return {
185
+ providerId: input.providerId,
186
+ pluginId: parts.pluginId,
187
+ key: parts.key,
188
+ reference: input.reference,
189
+ label: cleanText(input.label, 40) || 'Store credit',
190
+ last4: cleanText(input.last4, 4),
191
+ amountCents: input.amountCents,
192
+ appliedAs: input.appliedAs === 'tender' ? 'tender' : 'discount'
193
+ };
194
+ }
195
+
196
+ //# sourceMappingURL=plugin-checkout-credits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-checkout-credits.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 * Money another plugin honors at checkout and at the register (AGL-3640).\n *\n * A rewards balance, store credit and a friend's referral credit are all the\n * same thing to a seller: a code the buyer brings that pays part of the sale\n * from an account the SELLER does not keep. The plugin that keeps the account\n * registers a provider here; the seller asks it, by code, how much the account\n * can give, reserves that much while an online buyer pays, and takes it in the\n * same transaction that writes the sale. The seller never reads the provider's\n * documents and the provider never reads the seller's: the sale records which\n * provider paid what, under the provider's own opaque `reference`, and the\n * seller's order events carry that onward.\n *\n * ## Money\n *\n * Integer cents in the sale's currency, always. A provider answers what it\n * CAN give and the seller never takes more than that.\n *\n * - Online, the seller {@link PluginCheckoutCreditProvider.hold | holds}\n * before the payment processor is asked, so a shortfall is a refusal and\n * not an apology. The hold is keyed by the checkout attempt, so a retry\n * re-places the same hold rather than a second one, and it lapses on its\n * own if the checkout is abandoned and nobody releases it.\n * - When the sale is written, the seller {@link PluginCheckoutCreditProvider.stage | stages}\n * the account INSIDE its own transaction: the provider reads (and only\n * reads) what it needs, and the stage's `debit` writes the redemption in\n * the same commit as the sale. A sale that is not written takes nothing;\n * a redemption is never written without its sale.\n * - A refund gives back through {@link PluginCheckoutCreditProvider.restore | restore},\n * idempotent per key, so a retried refund restores once.\n *\n * ## Nobody home\n *\n * With no provider registered, or none offered on a site, the seller shows no\n * field and sells exactly as it did before. Every reader here that a seller\n * calls on a sale path is safe to call with nothing registered.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-checkout-credits`); it is not in the\n * barrel.\n */\n\n/** Where the sale happens. */\nexport type CheckoutCreditChannel = 'online' | 'pos'\n\n/**\n * A server transaction as a provider uses it: the Admin SDK's shape, typed\n * structurally so the platform names no database SDK. Every `get` happens\n * before any write, as the database requires.\n */\nexport interface CheckoutCreditTransaction {\n get(ref: any): Promise<any>\n set(ref: any, data: any, options?: any): unknown\n update(ref: any, data: any): unknown\n create(ref: any, data: any): unknown\n}\n\n/** A provider's refusal, in words the buyer or the cashier can read. */\nexport interface CheckoutCreditRefusal {\n ok: false\n /** 400 for a bad code, 404 for an unknown one, 409 for an empty or busy one. */\n status: number\n error: string\n}\n\n/** The account a code names. */\nexport interface CheckoutCreditAccount {\n ok: true\n /**\n * The provider's own handle on the account, stored on the sale and handed\n * back to it later. Never shown to a buyer and never a bearer secret.\n * {@link CHECKOUT_CREDIT_REFERENCE} holds its shape.\n */\n reference: string\n /** What the buyer and the receipt call it: `Rewards`, `Referral credit`. */\n label: string\n /** The last characters of the code, for the register and the receipt. */\n last4: string\n /** What the account could give right now, integer cents. */\n availableCents: number\n /** One line for the cashier: `1,250 points and $5.00 store credit`. */\n detail?: string\n}\n\n/**\n * The account as the seller's transaction read it. The provider builds it in\n * {@link PluginCheckoutCreditProvider.stage}, which reads; its methods only\n * WRITE, through the same transaction, so the redemption commits with the sale\n * or not at all.\n */\nexport interface CheckoutCreditStage {\n /**\n * What the account may give now, honoring every live hold except the one\n * the stage was opened for (that one is this sale's own).\n */\n availableCents: number\n /**\n * Takes up to `cents` and returns what was taken: never more than\n * `availableCents` plus this sale's own hold. `key` names this redemption\n * (the attempt, or the register payment) and is what a void reverses.\n */\n debit(input: { cents: number; key: string; orderId: string; channel: CheckoutCreditChannel }): number\n /** Gives back exactly what the redemption named by `key` took: a voided register payment. */\n reverse(input: { key: string; orderId: string }): number\n}\n\nexport interface CheckoutCreditHoldRequest {\n hostId: string\n reference: string\n /** The checkout attempt: the same key re-places the same hold. */\n holdKey: string\n /** The most the sale can take: what is left to pay on the goods. */\n maxCents: number\n /** ISO-4217, lower case. */\n currency: string\n customerEmail: string | null\n nowMs: number\n}\n\nexport interface PluginCheckoutCreditProvider {\n /** Stable within the plugin: lower-case words and dashes. */\n key: string\n /** What the field and the register's tender button say: `Rewards`. */\n label: string\n /** Whether a code is one this provider issues. Syntax only: no reads. */\n recognizes(code: string): boolean\n /** Whether the site offers it at all: plugin on, program on, plan carries it. */\n offered(input: { hostId: string; channel: CheckoutCreditChannel }): Promise<boolean>\n /**\n * A buyer's code — or, at the register only, a `reference` a staff\n * {@link lookup} returned — to the account it names. `staff` is true only\n * when the seller has authenticated a staff member for this site; a\n * provider honors a bare `reference` only then.\n */\n resolve(input: {\n hostId: string\n code?: string\n reference?: string\n channel: CheckoutCreditChannel\n customerEmail: string | null\n staff: boolean\n }): Promise<CheckoutCreditAccount | CheckoutCreditRefusal>\n /** Reserves up to `maxCents` for one online checkout attempt. */\n hold(input: CheckoutCreditHoldRequest): Promise<{ ok: true; cents: number } | CheckoutCreditRefusal>\n /** Lets a hold go: an abandoned or refused checkout. Never throws. */\n release(input: { hostId: string; reference: string; holdKey: string }): Promise<void>\n /**\n * Reads the account inside the seller's transaction. `null` when it no\n * longer exists. `holdKey` is this sale's own online hold, when it has one.\n */\n stage(input: {\n transaction: CheckoutCreditTransaction\n hostId: string\n reference: string\n orderId: string\n nowMs: number\n holdKey?: string\n }): Promise<CheckoutCreditStage | null>\n /**\n * Gives back up to `cents` of what the sale took from the account, for a\n * refund. Idempotent per `key`; returns the cents given back (0 for a key\n * already restored, or an account that is gone).\n */\n restore(input: { hostId: string; reference: string; orderId: string; cents: number; key: string }): Promise<number>\n /** Staff search at the register by email or code. Optional. */\n lookup?(input: { hostId: string; query: string }): Promise<CheckoutCreditAccount[]>\n}\n\n/** A provider as the seller resolves it. */\nexport interface ResolvedCheckoutCreditProvider {\n /** `{pluginId}.{key}`: what a sale records. */\n providerId: string\n pluginId: string\n provider: PluginCheckoutCreditProvider\n}\n\n/**\n * What a sale records of one redemption, on the sale and in the events the\n * seller raises about it, so the provider learns what was taken from the\n * seller's own facts. `appliedAs` says how the seller counted it: online it\n * comes off the goods like a discount, at the register it is one of the\n * sale's payments.\n */\nexport interface PluginCheckoutCreditSold {\n providerId: string\n pluginId: string\n key: string\n reference: string\n label: string\n last4: string\n amountCents: number\n appliedAs: 'discount' | 'tender'\n}\n\nconst KEY = /^[a-z][a-z0-9-]{1,39}$/\nconst PROVIDER_ID = /^([a-z][a-z0-9-]{0,63})\\.([a-z][a-z0-9-]{1,39})$/\n\n/** The shape of a provider's `reference`: short, and safe in a document path segment. */\nexport const CHECKOUT_CREDIT_REFERENCE = /^[A-Za-z0-9:_-]{1,128}$/\n\nconst PLUGIN_CHECKOUT_CREDITS = definePluginServiceContract<PluginCheckoutCreditProvider>(\n 'core.checkout-credits',\n { multiple: true },\n)\n\n/** Joins the providers. A plugin re-registering a key replaces its own. */\nexport function registerPluginCheckoutCredit(\n provider: PluginCheckoutCreditProvider,\n options?: { pluginId?: string },\n): void {\n if (!KEY.test(String(provider?.key ?? ''))) {\n throw new Error(`checkout credit provider key \"${provider?.key}\" must be lower-case words and dashes`)\n }\n const pluginId = getRegisteringPluginId() ?? options?.pluginId\n registerPluginService(PLUGIN_CHECKOUT_CREDITS, provider, {\n ...(pluginId ? { pluginId } : {}),\n key: provider.key,\n })\n}\n\nfunction resolved(): ResolvedCheckoutCreditProvider[] {\n return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).map((entry) => ({\n providerId: `${entry.pluginId}.${entry.impl.key}`,\n pluginId: entry.pluginId,\n provider: entry.impl,\n }))\n}\n\n/** Whether any plugin registered a provider: a seller with none skips the field. */\nexport function hasPluginCheckoutCredits(): boolean {\n return resolvePluginServices(PLUGIN_CHECKOUT_CREDITS).length > 0\n}\n\n/**\n * A code as typed — any case, spaces and dashes — in the one form providers\n * see: upper case, letters, digits and single dashes, at most 40 characters.\n * Empty when nothing usable was typed.\n */\nexport function normalizeCheckoutCreditCode(value: unknown): string {\n return String(value ?? '')\n .toUpperCase()\n .replace(/[^A-Z0-9-]/g, '')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 40)\n}\n\n/** The provider that issues a code, or `null`. The first to recognize it answers. */\nexport function checkoutCreditProviderForCode(code: string): ResolvedCheckoutCreditProvider | null {\n const normalized = normalizeCheckoutCreditCode(code)\n if (!normalized) return null\n for (const entry of resolved()) {\n try {\n if (entry.provider.recognizes(normalized)) return entry\n } catch (error) {\n console.error(`[checkout-credits] \"${entry.providerId}\" failed to read a code`, error)\n }\n }\n return null\n}\n\n/** One provider by the id a sale recorded, or `null` when it is gone. */\nexport function checkoutCreditProvider(providerId: string): ResolvedCheckoutCreditProvider | null {\n return resolved().find((entry) => entry.providerId === providerId) ?? null\n}\n\n/**\n * The providers a site offers on a channel, for the cart's field and the\n * register's tender. Never throws: a provider that fails is not offered.\n */\nexport async function offeredCheckoutCredits(input: {\n hostId: string\n channel: CheckoutCreditChannel\n}): Promise<Array<{ providerId: string; label: string; lookup: boolean }>> {\n const answers = await Promise.all(\n resolved().map(async (entry) => {\n const offered = await entry.provider.offered(input).catch((error: unknown) => {\n console.error(`[checkout-credits] \"${entry.providerId}\" offered() failed for ${input.hostId}`, error)\n return false\n })\n return offered\n ? { providerId: entry.providerId, label: entry.provider.label, lookup: typeof entry.provider.lookup === 'function' }\n : null\n }),\n )\n return answers.filter((answer): answer is { providerId: string; label: string; lookup: boolean } => Boolean(answer))\n}\n\n/** An account answer held to the contract, or a refusal in its place. */\nexport function normalizeCheckoutCreditAccount(\n answer: CheckoutCreditAccount | CheckoutCreditRefusal | null | undefined,\n): CheckoutCreditAccount | CheckoutCreditRefusal {\n if (!answer || typeof answer !== 'object') {\n return { ok: false, status: 404, error: 'That code is not valid.' }\n }\n if (answer.ok !== true) {\n const status = Number.isInteger(answer.status) && answer.status >= 400 && answer.status < 500 ? answer.status : 409\n return { ok: false, status, error: cleanText(answer.error, 160) || 'That code cannot be used.' }\n }\n if (!CHECKOUT_CREDIT_REFERENCE.test(String(answer.reference ?? ''))) {\n return { ok: false, status: 404, error: 'That code is not valid.' }\n }\n const available = answer.availableCents\n return {\n ok: true,\n reference: answer.reference,\n label: cleanText(answer.label, 40) || 'Store credit',\n last4: cleanText(answer.last4, 4).toUpperCase(),\n availableCents: Number.isSafeInteger(available) && available > 0 ? available : 0,\n ...(answer.detail ? { detail: cleanText(answer.detail, 120) } : {}),\n }\n}\n\n/** Cents a provider says it held or took, as whole non-negative cents no larger than `ceiling`. */\nexport function boundedCreditCents(value: unknown, ceiling: number): number {\n const cents = typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : 0\n const max = Number.isSafeInteger(ceiling) && ceiling > 0 ? ceiling : 0\n return Math.min(cents, max)\n}\n\nfunction cleanText(value: unknown, max: number): string {\n return Array.from(String(value ?? ''), (char) =>\n char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127 ? ' ' : char,\n )\n .join('')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, max)\n}\n\n/** The metadata key an online redemption rides under on the payment processor. */\nexport const CHECKOUT_CREDIT_METADATA_KEY = 'credit0'\n\n/** What an online checkout carries to its webhook: the hold to settle. */\nexport interface CheckoutCreditHeld {\n providerId: string\n reference: string\n holdKey: string\n amountCents: number\n label: string\n last4: string\n}\n\n/**\n * The hold an online checkout placed, packed for a payment processor's\n * metadata: one key holding `[providerId, reference, holdKey, cents, label, last4]`\n * as JSON, inside a 500-character value. Never the code itself: metadata is\n * readable on the merchant's dashboard, and a code is a bearer secret.\n */\nexport function encodeCheckoutCreditMetadata(held: CheckoutCreditHeld): Record<string, string> {\n const entry = [\n held.providerId,\n held.reference,\n held.holdKey.slice(0, 200),\n held.amountCents,\n cleanText(held.label, 40),\n cleanText(held.last4, 4),\n ]\n return { [CHECKOUT_CREDIT_METADATA_KEY]: JSON.stringify(entry) }\n}\n\n/** Reads {@link encodeCheckoutCreditMetadata} back. Anything unreadable is `null`, never guessed. */\nexport function decodeCheckoutCreditMetadata(\n metadata: Record<string, unknown> | null | undefined,\n): CheckoutCreditHeld | null {\n const raw = metadata?.[CHECKOUT_CREDIT_METADATA_KEY]\n if (raw === undefined || raw === null || raw === '') return null\n let entry: unknown\n try {\n entry = JSON.parse(String(raw))\n } catch {\n return null\n }\n if (!Array.isArray(entry)) return null\n const [providerId, reference, holdKey, cents, label, last4] = entry\n if (!PROVIDER_ID.test(String(providerId ?? ''))) return null\n if (!CHECKOUT_CREDIT_REFERENCE.test(String(reference ?? ''))) return null\n if (typeof holdKey !== 'string' || !holdKey) return null\n if (typeof cents !== 'number' || !Number.isSafeInteger(cents) || cents <= 0) return null\n return {\n providerId: String(providerId),\n reference: String(reference),\n holdKey,\n amountCents: cents,\n label: cleanText(label, 40) || 'Store credit',\n last4: cleanText(last4, 4),\n }\n}\n\n/** The plugin id and key of a provider id. */\nexport function splitCheckoutCreditProviderId(providerId: string): { pluginId: string; key: string } | null {\n const match = PROVIDER_ID.exec(String(providerId ?? ''))\n return match ? { pluginId: match[1], key: match[2] } : null\n}\n\n/** A recorded redemption, as a seller stores it and its events carry it. */\nexport function checkoutCreditSold(\n input: Omit<PluginCheckoutCreditSold, 'pluginId' | 'key'>,\n): PluginCheckoutCreditSold | null {\n const parts = splitCheckoutCreditProviderId(input.providerId)\n if (!parts || !CHECKOUT_CREDIT_REFERENCE.test(input.reference)) return null\n if (!Number.isSafeInteger(input.amountCents) || input.amountCents <= 0) return null\n return {\n providerId: input.providerId,\n pluginId: parts.pluginId,\n key: parts.key,\n reference: input.reference,\n label: cleanText(input.label, 40) || 'Store credit',\n last4: cleanText(input.last4, 4),\n amountCents: input.amountCents,\n appliedAs: input.appliedAs === 'tender' ? 'tender' : 'discount',\n }\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","KEY","PROVIDER_ID","CHECKOUT_CREDIT_REFERENCE","PLUGIN_CHECKOUT_CREDITS","multiple","registerPluginCheckoutCredit","provider","options","test","String","key","Error","pluginId","resolved","map","entry","providerId","impl","hasPluginCheckoutCredits","length","normalizeCheckoutCreditCode","value","toUpperCase","replace","slice","checkoutCreditProviderForCode","code","normalized","recognizes","error","console","checkoutCreditProvider","find","offeredCheckoutCredits","input","answers","Promise","all","offered","catch","hostId","label","lookup","filter","answer","Boolean","normalizeCheckoutCreditAccount","ok","status","Number","isInteger","cleanText","reference","available","availableCents","last4","isSafeInteger","detail","boundedCreditCents","ceiling","cents","max","Math","min","Array","from","char","charCodeAt","join","trim","CHECKOUT_CREDIT_METADATA_KEY","encodeCheckoutCreditMetadata","held","holdKey","amountCents","JSON","stringify","decodeCheckoutCreditMetadata","metadata","raw","undefined","parse","isArray","splitCheckoutCreditProviderId","match","exec","checkoutCreditSold","parts","appliedAs"],"mappings":"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"}
@@ -99,6 +99,11 @@ export interface CatalogOffer {
99
99
  hasVariants: boolean;
100
100
  productId: string;
101
101
  variantId: string;
102
+ /**
103
+ * The merchant's own SKU for this configuration, as entered; absent when
104
+ * they entered none. What a marketplace matches its listing by (AGL-3638).
105
+ */
106
+ sku?: string;
102
107
  /** The product's name. */
103
108
  productName: string;
104
109
  /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A site's sellable catalog, read whole and in pages by a plugin that does\n * not own it (AGL-3637).\n *\n * A product feed, a marketplace listing sync and an ad catalog all need the\n * same thing: every offer a store sells, with the facts a shopping channel\n * asks for — price in the store's currency, stock, photos, identifiers,\n * shipping. The plugin that keeps the products answers it here, so a reader\n * never learns where products are stored, how variants are modeled or what a\n * deleted product looks like.\n *\n * ## One OFFER per sellable configuration\n *\n * A product with one configuration is one offer whose `id` is the product's\n * own id. A product with several (sizes, colors) is one offer per\n * configuration, each with its own `id` and the product's id as `groupId`,\n * which is what every shopping channel calls the variant group.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` and every other amount are integers (cents for USD) in\n * {@link CatalogStore.currency}. A reader formats them; it never assumes a\n * currency.\n *\n * ## Pages, not a list\n *\n * A store may hold far more products than one read should load, so the\n * catalog is walked in pages: `page()` answers up to about `limit` offers and\n * a `nextCursor` to continue from, `null` on the last page. A cursor is the\n * owner's own opaque string; a reader passes it back untouched.\n *\n * ## One owner\n *\n * A slot, like the tax profile: a site has one catalog, so a second plugin's\n * registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductCatalog} returns\n * `undefined` and a reader serves nothing.\n */\n\n/** The store an offer belongs to, as a channel needs it. */\nexport interface CatalogStore {\n hostId: string\n /** The store's name, as its site calls itself. */\n name: string\n /**\n * The origin the store's pages are served on (`https://shop.example.com`),\n * or `null` when the site has no address yet.\n */\n origin: string | null\n /** ISO 4217, upper case. Every amount in the catalog is in it. */\n currency: string\n /**\n * Whether `/products/{slug}` pages are served. When `false`, every offer's\n * `path` answers 404, and a channel will refuse the offers.\n */\n productPagesServed: boolean\n /**\n * Countries the store's checkout prices shipping to by a live carrier\n * quote rather than its own table (ISO 3166-1 alpha-2, upper case). An\n * offer carries no {@link CatalogShippingPrice} for them, because the price\n * depends on the address; a channel that calculates carrier rates itself\n * prices them from the offer's weight and dimensions.\n */\n carrierPricedCountries: string[]\n}\n\nexport type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder'\nexport type CatalogCondition = 'new' | 'refurbished' | 'used'\nexport type CatalogProductKind = 'physical' | 'digital' | 'service'\n\n/** What one offer costs to ship to one country, cheapest rate first chosen. */\nexport interface CatalogShippingPrice {\n /** ISO 3166-1 alpha-2, upper case. */\n country: string\n /** The rate's name as the shopper sees it at checkout. */\n service: string\n priceMinor: number\n}\n\n/** One sellable configuration of a product. */\nexport interface CatalogOffer {\n /** Stable, unique within the store: the product id, or product and variant. */\n id: string\n /** The product's id: the variant group every configuration shares. */\n groupId: string\n /** Whether the product has more than one configuration. */\n hasVariants: boolean\n productId: string\n variantId: string\n /** The product's name. */\n productName: string\n /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */\n title: string\n /** Plain text: no markup. May be empty. */\n description: string\n /** Site-relative path of the product page, e.g. `/products/tee`. */\n path: string\n /** Absolute URL of the photo shown for this configuration. */\n imageUrl?: string\n /** Absolute URLs of the product's other photos, in order. */\n additionalImageUrls: string[]\n /** What the shopper pays, before any sale: the regular price. */\n priceMinor: number\n /** A lower price the offer is on sale for now; absent when not on sale. */\n salePriceMinor?: number\n availability: CatalogAvailability\n /** Units in stock, or `null` when stock is not tracked. */\n quantity: number | null\n kind: CatalogProductKind\n /** Whether the product is sold only as a recurring subscription. */\n subscriptionOnly: boolean\n /** What the merchant entered for shopping channels; absent when they did not. */\n condition?: CatalogCondition\n brand?: string\n /** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */\n gtin?: string\n mpn?: string\n /** A Google product taxonomy id or full path, as entered. */\n googleProductCategory?: string\n /** The store's own category path, e.g. `Apparel > Shirts`. */\n productType?: string\n /** The configuration's choices, by option name. */\n options: Readonly<Record<string, string>>\n /** The choice of an option named like a color, when there is one. */\n color?: string\n /** The choice of an option named like a size, when there is one. */\n size?: string\n weightGrams?: number\n /** One unit's packed size, in centimeters, when the merchant entered it. */\n dimensionsCm?: { length: number; width: number; height: number }\n /** What it costs to ship one unit, per country the store ships to. */\n shipping: CatalogShippingPrice[]\n /** When the product last changed, epoch ms; absent when unknown. */\n updatedAtMs?: number\n}\n\nexport interface CatalogPage {\n offers: CatalogOffer[]\n /** Where the next page starts, or `null` when this is the last one. */\n nextCursor: string | null\n}\n\nexport interface PluginProductCatalog {\n /** The store, or `null` for a site that does not sell. */\n store(hostId: string): Promise<CatalogStore | null>\n /**\n * One page of the store's live, listed offers: never a draft, archived or\n * deleted product. About `limit` offers — a product's configurations are\n * never split across pages, so a page may run over by one product's worth.\n */\n page(request: { hostId: string; cursor?: string | null; limit: number }): Promise<CatalogPage>\n}\n\nexport const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract<PluginProductCatalog>(\n 'core.product-catalog',\n { multiple: false },\n)\n\n/** Registers the store catalog. A second plugin's is refused, naming both. */\nexport function registerPluginProductCatalog(\n catalog: PluginProductCatalog,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered catalog, or `undefined` when no plugin sells. */\nexport function pluginProductCatalog(): PluginProductCatalog | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]?.impl\n}\n\n/**\n * The plugin that publishes the catalog to shopping channels, as the\n * catalog's owner hands it an address of its own (AGL-3637).\n *\n * Before any plugin published feeds, the plugin that keeps the products\n * served one feed itself, and merchants pasted that address into their\n * channel. The address stays the owner's — a request for it reaches the\n * owner's routes — but the feed is written by whoever publishes the catalog\n * now, so the owner answers it from here. Nobody registered is an ordinary\n * answer: {@link pluginCatalogFeed} returns `undefined` and the owner\n * answers 404.\n */\nexport interface PluginCatalogFeed {\n /**\n * Answers a request for the owner's pre-existing feed address for the site\n * `hostId`: the feed, a 404 when the site's publisher has retired the\n * address or does not publish, or a 503 to try later.\n */\n serveLegacyFeed(request: Request, hostId: string): Promise<Response>\n}\n\nexport const PLUGIN_CATALOG_FEED = definePluginServiceContract<PluginCatalogFeed>(\n 'core.catalog-feed',\n { multiple: false },\n)\n\n/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */\nexport function registerPluginCatalogFeed(\n feed: PluginCatalogFeed,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CATALOG_FEED, feed, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */\nexport function pluginCatalogFeed(): PluginCatalogFeed | undefined {\n return resolvePluginServices(PLUGIN_CATALOG_FEED)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_CATALOG","multiple","registerPluginProductCatalog","catalog","options","pluginId","pluginProductCatalog","impl","PLUGIN_CATALOG_FEED","registerPluginCatalogFeed","feed","pluginCatalogFeed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2J1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED,4EAA4E,GAC5E,OAAO,SAASC,6BACdC,OAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,SAAS,aACjDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,iEAAiE,GACjE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,uBAAuB,CAAC,EAAE,qBAAhDD,wBAAkDQ,IAAI;AAC/D;AAuBA,OAAO,MAAMC,sBAAsBX,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASQ,0BACdC,IAAuB,EACvBN,OAA+B;IAE/BN,sBAAsBU,qBAAqBE,MAAM,aAC3CN,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,kFAAkF,GAClF,OAAO,SAASM;QACPZ;IAAP,QAAOA,0BAAAA,sBAAsBS,oBAAoB,CAAC,EAAE,qBAA7CT,wBAA+CQ,IAAI;AAC5D"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-catalog.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * A site's sellable catalog, read whole and in pages by a plugin that does\n * not own it (AGL-3637).\n *\n * A product feed, a marketplace listing sync and an ad catalog all need the\n * same thing: every offer a store sells, with the facts a shopping channel\n * asks for — price in the store's currency, stock, photos, identifiers,\n * shipping. The plugin that keeps the products answers it here, so a reader\n * never learns where products are stored, how variants are modeled or what a\n * deleted product looks like.\n *\n * ## One OFFER per sellable configuration\n *\n * A product with one configuration is one offer whose `id` is the product's\n * own id. A product with several (sizes, colors) is one offer per\n * configuration, each with its own `id` and the product's id as `groupId`,\n * which is what every shopping channel calls the variant group.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` and every other amount are integers (cents for USD) in\n * {@link CatalogStore.currency}. A reader formats them; it never assumes a\n * currency.\n *\n * ## Pages, not a list\n *\n * A store may hold far more products than one read should load, so the\n * catalog is walked in pages: `page()` answers up to about `limit` offers and\n * a `nextCursor` to continue from, `null` on the last page. A cursor is the\n * owner's own opaque string; a reader passes it back untouched.\n *\n * ## One owner\n *\n * A slot, like the tax profile: a site has one catalog, so a second plugin's\n * registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductCatalog} returns\n * `undefined` and a reader serves nothing.\n */\n\n/** The store an offer belongs to, as a channel needs it. */\nexport interface CatalogStore {\n hostId: string\n /** The store's name, as its site calls itself. */\n name: string\n /**\n * The origin the store's pages are served on (`https://shop.example.com`),\n * or `null` when the site has no address yet.\n */\n origin: string | null\n /** ISO 4217, upper case. Every amount in the catalog is in it. */\n currency: string\n /**\n * Whether `/products/{slug}` pages are served. When `false`, every offer's\n * `path` answers 404, and a channel will refuse the offers.\n */\n productPagesServed: boolean\n /**\n * Countries the store's checkout prices shipping to by a live carrier\n * quote rather than its own table (ISO 3166-1 alpha-2, upper case). An\n * offer carries no {@link CatalogShippingPrice} for them, because the price\n * depends on the address; a channel that calculates carrier rates itself\n * prices them from the offer's weight and dimensions.\n */\n carrierPricedCountries: string[]\n}\n\nexport type CatalogAvailability = 'in_stock' | 'out_of_stock' | 'backorder'\nexport type CatalogCondition = 'new' | 'refurbished' | 'used'\nexport type CatalogProductKind = 'physical' | 'digital' | 'service'\n\n/** What one offer costs to ship to one country, cheapest rate first chosen. */\nexport interface CatalogShippingPrice {\n /** ISO 3166-1 alpha-2, upper case. */\n country: string\n /** The rate's name as the shopper sees it at checkout. */\n service: string\n priceMinor: number\n}\n\n/** One sellable configuration of a product. */\nexport interface CatalogOffer {\n /** Stable, unique within the store: the product id, or product and variant. */\n id: string\n /** The product's id: the variant group every configuration shares. */\n groupId: string\n /** Whether the product has more than one configuration. */\n hasVariants: boolean\n productId: string\n variantId: string\n /**\n * The merchant's own SKU for this configuration, as entered; absent when\n * they entered none. What a marketplace matches its listing by (AGL-3638).\n */\n sku?: string\n /** The product's name. */\n productName: string\n /** The product's name with this configuration's choices, e.g. `Tee — Red / M`. */\n title: string\n /** Plain text: no markup. May be empty. */\n description: string\n /** Site-relative path of the product page, e.g. `/products/tee`. */\n path: string\n /** Absolute URL of the photo shown for this configuration. */\n imageUrl?: string\n /** Absolute URLs of the product's other photos, in order. */\n additionalImageUrls: string[]\n /** What the shopper pays, before any sale: the regular price. */\n priceMinor: number\n /** A lower price the offer is on sale for now; absent when not on sale. */\n salePriceMinor?: number\n availability: CatalogAvailability\n /** Units in stock, or `null` when stock is not tracked. */\n quantity: number | null\n kind: CatalogProductKind\n /** Whether the product is sold only as a recurring subscription. */\n subscriptionOnly: boolean\n /** What the merchant entered for shopping channels; absent when they did not. */\n condition?: CatalogCondition\n brand?: string\n /** GTIN (UPC, EAN, ISBN, JAN, ITF-14) as entered, digits only. */\n gtin?: string\n mpn?: string\n /** A Google product taxonomy id or full path, as entered. */\n googleProductCategory?: string\n /** The store's own category path, e.g. `Apparel > Shirts`. */\n productType?: string\n /** The configuration's choices, by option name. */\n options: Readonly<Record<string, string>>\n /** The choice of an option named like a color, when there is one. */\n color?: string\n /** The choice of an option named like a size, when there is one. */\n size?: string\n weightGrams?: number\n /** One unit's packed size, in centimeters, when the merchant entered it. */\n dimensionsCm?: { length: number; width: number; height: number }\n /** What it costs to ship one unit, per country the store ships to. */\n shipping: CatalogShippingPrice[]\n /** When the product last changed, epoch ms; absent when unknown. */\n updatedAtMs?: number\n}\n\nexport interface CatalogPage {\n offers: CatalogOffer[]\n /** Where the next page starts, or `null` when this is the last one. */\n nextCursor: string | null\n}\n\nexport interface PluginProductCatalog {\n /** The store, or `null` for a site that does not sell. */\n store(hostId: string): Promise<CatalogStore | null>\n /**\n * One page of the store's live, listed offers: never a draft, archived or\n * deleted product. About `limit` offers — a product's configurations are\n * never split across pages, so a page may run over by one product's worth.\n */\n page(request: { hostId: string; cursor?: string | null; limit: number }): Promise<CatalogPage>\n}\n\nexport const PLUGIN_PRODUCT_CATALOG = definePluginServiceContract<PluginProductCatalog>(\n 'core.product-catalog',\n { multiple: false },\n)\n\n/** Registers the store catalog. A second plugin's is refused, naming both. */\nexport function registerPluginProductCatalog(\n catalog: PluginProductCatalog,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_CATALOG, catalog, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered catalog, or `undefined` when no plugin sells. */\nexport function pluginProductCatalog(): PluginProductCatalog | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_CATALOG)[0]?.impl\n}\n\n/**\n * The plugin that publishes the catalog to shopping channels, as the\n * catalog's owner hands it an address of its own (AGL-3637).\n *\n * Before any plugin published feeds, the plugin that keeps the products\n * served one feed itself, and merchants pasted that address into their\n * channel. The address stays the owner's — a request for it reaches the\n * owner's routes — but the feed is written by whoever publishes the catalog\n * now, so the owner answers it from here. Nobody registered is an ordinary\n * answer: {@link pluginCatalogFeed} returns `undefined` and the owner\n * answers 404.\n */\nexport interface PluginCatalogFeed {\n /**\n * Answers a request for the owner's pre-existing feed address for the site\n * `hostId`: the feed, a 404 when the site's publisher has retired the\n * address or does not publish, or a 503 to try later.\n */\n serveLegacyFeed(request: Request, hostId: string): Promise<Response>\n}\n\nexport const PLUGIN_CATALOG_FEED = definePluginServiceContract<PluginCatalogFeed>(\n 'core.catalog-feed',\n { multiple: false },\n)\n\n/** Registers the catalog's feed publisher. A second plugin's is refused, naming both. */\nexport function registerPluginCatalogFeed(\n feed: PluginCatalogFeed,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_CATALOG_FEED, feed, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered feed publisher, or `undefined` when no plugin publishes feeds. */\nexport function pluginCatalogFeed(): PluginCatalogFeed | undefined {\n return resolvePluginServices(PLUGIN_CATALOG_FEED)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_CATALOG","multiple","registerPluginProductCatalog","catalog","options","pluginId","pluginProductCatalog","impl","PLUGIN_CATALOG_FEED","registerPluginCatalogFeed","feed","pluginCatalogFeed"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAgK1B,OAAO,MAAMC,yBAAyBH,4BACpC,wBACA;IAAEI,UAAU;AAAM,GACnB;AAED,4EAA4E,GAC5E,OAAO,SAASC,6BACdC,OAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,wBAAwBG,SAAS,aACjDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,iEAAiE,GACjE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,uBAAuB,CAAC,EAAE,qBAAhDD,wBAAkDQ,IAAI;AAC/D;AAuBA,OAAO,MAAMC,sBAAsBX,4BACjC,qBACA;IAAEI,UAAU;AAAM,GACnB;AAED,uFAAuF,GACvF,OAAO,SAASQ,0BACdC,IAAuB,EACvBN,OAA+B;IAE/BN,sBAAsBU,qBAAqBE,MAAM,aAC3CN,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,kFAAkF,GAClF,OAAO,SAASM;QACPZ;IAAP,QAAOA,0BAAAA,sBAAsBS,oBAAoB,CAAC,EAAE,qBAA7CT,wBAA+CQ,IAAI;AAC5D"}
@@ -0,0 +1,166 @@
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
+ * Products that come from somewhere else, written by a plugin that does not
19
+ * keep the store's products (AGL-3641).
20
+ *
21
+ * A print-on-demand service, a dropshipping supplier and a fulfillment
22
+ * network each hold a catalog of their own, and a merchant who sells it wants
23
+ * those products in their store without typing them in again — then kept in
24
+ * step as the source changes what it offers. The plugin that keeps the
25
+ * products answers here, so the plugin that reads the source never learns
26
+ * how products are stored, how their addresses are chosen, how the plan's
27
+ * product allowance is counted or what the store's search keys are. Every
28
+ * write keeps the owner's own rules: a create counts against the allowance in
29
+ * the transaction that makes it, and an edit leaves an entry in the site's
30
+ * activity like any other.
31
+ *
32
+ * ## Variants by the caller's key
33
+ *
34
+ * The caller names every configuration by its own stable key — the source's
35
+ * variant id — and the owner answers with its own variant id for each, so an
36
+ * order line can later be traced back to what the source sells. On an
37
+ * update the caller passes back the variant ids it was given; a key with no
38
+ * id is a new configuration, and an existing variant whose id is not passed
39
+ * is one the source no longer offers, removed.
40
+ *
41
+ * ## Money is integer minor units in the store's currency
42
+ *
43
+ * `priceMinor` is an integer (cents for USD) in the currency the store sells
44
+ * in, the `currency` the store's catalog (`core.product-catalog`) names. The
45
+ * caller converts nothing: a source priced in another currency is the
46
+ * caller's to refuse.
47
+ *
48
+ * ## What an update leaves alone
49
+ *
50
+ * The merchant edits an imported product like any other. An update rewrites
51
+ * only what the caller names: prices when `prices` is set, the description
52
+ * and photos when `content` is set, and always the configurations and their
53
+ * availability, because those are what an order can be filled from.
54
+ *
55
+ * ## One owner
56
+ *
57
+ * A slot, like the catalog: a site's products have one keeper, so a second
58
+ * plugin's registration is refused and the incumbent keeps serving. No owner
59
+ * registered is an ordinary answer — {@link pluginProductWriter} returns
60
+ * `undefined` and a caller imports nothing.
61
+ *
62
+ * Import this module by its own subpath
63
+ * (`@aglyn/aglyn/plugin-manager/plugin-product-writer`); it is not in the
64
+ * barrel.
65
+ */
66
+ /** One axis of variation, e.g. `{ name: 'Size', values: ['S', 'M', 'L'] }`. */
67
+ export interface SourcedProductOption {
68
+ name: string;
69
+ values: string[];
70
+ }
71
+ /** One sellable configuration, as the source offers it. */
72
+ export interface SourcedProductVariant {
73
+ /** The source's own stable id for it. */
74
+ key: string;
75
+ /** The owner's id, when an earlier write returned one. */
76
+ variantId?: string;
77
+ /** Choices by option name; `{}` for a product with one configuration. */
78
+ options: Record<string, string>;
79
+ sku?: string;
80
+ /** What the shopper pays, integer minor units in the store's currency. */
81
+ priceMinor: number;
82
+ weightGrams?: number;
83
+ /** A photo the owner already holds (a media-library URL) for this choice. */
84
+ imageUrl?: string;
85
+ /**
86
+ * Whether the source can make or ship it now. An unavailable variant is
87
+ * kept, sold out, so a product page does not lose a choice for a day's
88
+ * shortage; a variant the source dropped is not passed at all.
89
+ */
90
+ available: boolean;
91
+ }
92
+ /** A product as the source offers it. */
93
+ export interface SourcedProduct {
94
+ /** The source's stable key for the product, e.g. `printful:123`. */
95
+ sourceKey: string;
96
+ name: string;
97
+ /** Plain text or simple HTML, as the source wrote it. */
98
+ description?: string;
99
+ /** Photos the owner already holds (media-library URLs), first is primary. */
100
+ mediaUrls: string[];
101
+ tags?: string[];
102
+ options: SourcedProductOption[];
103
+ variants: SourcedProductVariant[];
104
+ }
105
+ export interface SourcedProductWrite {
106
+ hostId: string;
107
+ product: SourcedProduct;
108
+ /** The owner's product id, when an earlier write returned one. */
109
+ productId?: string;
110
+ /** A new product is born a draft unless the caller says to list it. */
111
+ status?: 'draft' | 'active';
112
+ /** On an update: rewrite prices from the source. */
113
+ prices?: boolean;
114
+ /** On an update: rewrite the name, description and photos from the source. */
115
+ content?: boolean;
116
+ /**
117
+ * Whether a `productId` the store no longer has (the merchant deleted it)
118
+ * is made again. Default true; a background re-sync passes false and is
119
+ * answered `missing`, so a product the merchant deleted stays deleted.
120
+ */
121
+ recreate?: boolean;
122
+ /** The member the write is made for, for the site's activity. */
123
+ actorUid?: string;
124
+ }
125
+ export type SourcedProductWriteOutcome = {
126
+ outcome: 'created' | 'updated' | 'unchanged';
127
+ productId: string;
128
+ /** Every variant passed, with the owner's id for it. */
129
+ variants: Array<{
130
+ key: string;
131
+ variantId: string;
132
+ }>;
133
+ }
134
+ /** The plan's product allowance is used up; nothing was written. */
135
+ | {
136
+ outcome: 'plan_limit';
137
+ limit: number;
138
+ }
139
+ /** The owner's rules refused the product; nothing was written. */
140
+ | {
141
+ outcome: 'invalid';
142
+ message: string;
143
+ }
144
+ /** The site does not sell. */
145
+ | {
146
+ outcome: 'no_store';
147
+ }
148
+ /** `productId` is gone and `recreate` was false; nothing was written. */
149
+ | {
150
+ outcome: 'missing';
151
+ };
152
+ export interface PluginProductWriter {
153
+ /**
154
+ * Creates the product, or updates the one `productId` names. A `productId`
155
+ * the store no longer has (the merchant deleted it) creates a new one,
156
+ * unless `recreate` is false.
157
+ */
158
+ upsertSourced(write: SourcedProductWrite): Promise<SourcedProductWriteOutcome>;
159
+ }
160
+ export declare const PLUGIN_PRODUCT_WRITER: import("./plugin-services").PluginServiceContract<PluginProductWriter>;
161
+ /** Registers the products' keeper. A second plugin's is refused, naming both. */
162
+ export declare function registerPluginProductWriter(writer: PluginProductWriter, options?: {
163
+ pluginId?: string;
164
+ }): void;
165
+ /** The registered keeper, or `undefined` when no plugin keeps products. */
166
+ export declare function pluginProductWriter(): PluginProductWriter | undefined;
@@ -0,0 +1,31 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
18
+ export const PLUGIN_PRODUCT_WRITER = definePluginServiceContract('core.product-writer', {
19
+ multiple: false
20
+ });
21
+ /** Registers the products' keeper. A second plugin's is refused, naming both. */ export function registerPluginProductWriter(writer, options) {
22
+ registerPluginService(PLUGIN_PRODUCT_WRITER, writer, _extends({}, (options == null ? void 0 : options.pluginId) ? {
23
+ pluginId: options.pluginId
24
+ } : {}));
25
+ }
26
+ /** The registered keeper, or `undefined` when no plugin keeps products. */ export function pluginProductWriter() {
27
+ var _resolvePluginServices_;
28
+ return (_resolvePluginServices_ = resolvePluginServices(PLUGIN_PRODUCT_WRITER)[0]) == null ? void 0 : _resolvePluginServices_.impl;
29
+ }
30
+
31
+ //# sourceMappingURL=plugin-product-writer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-product-writer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Products that come from somewhere else, written by a plugin that does not\n * keep the store's products (AGL-3641).\n *\n * A print-on-demand service, a dropshipping supplier and a fulfillment\n * network each hold a catalog of their own, and a merchant who sells it wants\n * those products in their store without typing them in again — then kept in\n * step as the source changes what it offers. The plugin that keeps the\n * products answers here, so the plugin that reads the source never learns\n * how products are stored, how their addresses are chosen, how the plan's\n * product allowance is counted or what the store's search keys are. Every\n * write keeps the owner's own rules: a create counts against the allowance in\n * the transaction that makes it, and an edit leaves an entry in the site's\n * activity like any other.\n *\n * ## Variants by the caller's key\n *\n * The caller names every configuration by its own stable key — the source's\n * variant id — and the owner answers with its own variant id for each, so an\n * order line can later be traced back to what the source sells. On an\n * update the caller passes back the variant ids it was given; a key with no\n * id is a new configuration, and an existing variant whose id is not passed\n * is one the source no longer offers, removed.\n *\n * ## Money is integer minor units in the store's currency\n *\n * `priceMinor` is an integer (cents for USD) in the currency the store sells\n * in, the `currency` the store's catalog (`core.product-catalog`) names. The\n * caller converts nothing: a source priced in another currency is the\n * caller's to refuse.\n *\n * ## What an update leaves alone\n *\n * The merchant edits an imported product like any other. An update rewrites\n * only what the caller names: prices when `prices` is set, the description\n * and photos when `content` is set, and always the configurations and their\n * availability, because those are what an order can be filled from.\n *\n * ## One owner\n *\n * A slot, like the catalog: a site's products have one keeper, so a second\n * plugin's registration is refused and the incumbent keeps serving. No owner\n * registered is an ordinary answer — {@link pluginProductWriter} returns\n * `undefined` and a caller imports nothing.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-product-writer`); it is not in the\n * barrel.\n */\n\n/** One axis of variation, e.g. `{ name: 'Size', values: ['S', 'M', 'L'] }`. */\nexport interface SourcedProductOption {\n name: string\n values: string[]\n}\n\n/** One sellable configuration, as the source offers it. */\nexport interface SourcedProductVariant {\n /** The source's own stable id for it. */\n key: string\n /** The owner's id, when an earlier write returned one. */\n variantId?: string\n /** Choices by option name; `{}` for a product with one configuration. */\n options: Record<string, string>\n sku?: string\n /** What the shopper pays, integer minor units in the store's currency. */\n priceMinor: number\n weightGrams?: number\n /** A photo the owner already holds (a media-library URL) for this choice. */\n imageUrl?: string\n /**\n * Whether the source can make or ship it now. An unavailable variant is\n * kept, sold out, so a product page does not lose a choice for a day's\n * shortage; a variant the source dropped is not passed at all.\n */\n available: boolean\n}\n\n/** A product as the source offers it. */\nexport interface SourcedProduct {\n /** The source's stable key for the product, e.g. `printful:123`. */\n sourceKey: string\n name: string\n /** Plain text or simple HTML, as the source wrote it. */\n description?: string\n /** Photos the owner already holds (media-library URLs), first is primary. */\n mediaUrls: string[]\n tags?: string[]\n options: SourcedProductOption[]\n variants: SourcedProductVariant[]\n}\n\nexport interface SourcedProductWrite {\n hostId: string\n product: SourcedProduct\n /** The owner's product id, when an earlier write returned one. */\n productId?: string\n /** A new product is born a draft unless the caller says to list it. */\n status?: 'draft' | 'active'\n /** On an update: rewrite prices from the source. */\n prices?: boolean\n /** On an update: rewrite the name, description and photos from the source. */\n content?: boolean\n /**\n * Whether a `productId` the store no longer has (the merchant deleted it)\n * is made again. Default true; a background re-sync passes false and is\n * answered `missing`, so a product the merchant deleted stays deleted.\n */\n recreate?: boolean\n /** The member the write is made for, for the site's activity. */\n actorUid?: string\n}\n\nexport type SourcedProductWriteOutcome =\n | {\n outcome: 'created' | 'updated' | 'unchanged'\n productId: string\n /** Every variant passed, with the owner's id for it. */\n variants: Array<{ key: string; variantId: string }>\n }\n /** The plan's product allowance is used up; nothing was written. */\n | { outcome: 'plan_limit'; limit: number }\n /** The owner's rules refused the product; nothing was written. */\n | { outcome: 'invalid'; message: string }\n /** The site does not sell. */\n | { outcome: 'no_store' }\n /** `productId` is gone and `recreate` was false; nothing was written. */\n | { outcome: 'missing' }\n\nexport interface PluginProductWriter {\n /**\n * Creates the product, or updates the one `productId` names. A `productId`\n * the store no longer has (the merchant deleted it) creates a new one,\n * unless `recreate` is false.\n */\n upsertSourced(write: SourcedProductWrite): Promise<SourcedProductWriteOutcome>\n}\n\nexport const PLUGIN_PRODUCT_WRITER = definePluginServiceContract<PluginProductWriter>(\n 'core.product-writer',\n { multiple: false },\n)\n\n/** Registers the products' keeper. A second plugin's is refused, naming both. */\nexport function registerPluginProductWriter(\n writer: PluginProductWriter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_PRODUCT_WRITER, writer, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered keeper, or `undefined` when no plugin keeps products. */\nexport function pluginProductWriter(): PluginProductWriter | undefined {\n return resolvePluginServices(PLUGIN_PRODUCT_WRITER)[0]?.impl\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_PRODUCT_WRITER","multiple","registerPluginProductWriter","writer","options","pluginId","pluginProductWriter","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA4I1B,OAAO,MAAMC,wBAAwBH,4BACnC,uBACA;IAAEI,UAAU;AAAM,GACnB;AAED,+EAA+E,GAC/E,OAAO,SAASC,4BACdC,MAA2B,EAC3BC,OAA+B;IAE/BN,sBAAsBE,uBAAuBG,QAAQ,aAC/CC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yEAAyE,GACzE,OAAO,SAASC;QACPP;IAAP,QAAOA,0BAAAA,sBAAsBC,sBAAsB,CAAC,EAAE,qBAA/CD,wBAAiDQ,IAAI;AAC9D"}
@@ -119,6 +119,16 @@ export interface PluginShippingAddressCheck {
119
119
  suggested?: PluginShippingAddress;
120
120
  /** Why it is not deliverable, in the carrier's words. */
121
121
  messages: string[];
122
+ /**
123
+ * Where the address is on a map, when the provider placed it. Optional:
124
+ * only some providers return it, and only for addresses they verified. A
125
+ * caller measuring distance (local delivery radius zones) treats its
126
+ * absence as "cannot measure", never as zero.
127
+ */
128
+ coordinates?: {
129
+ lat: number;
130
+ lng: number;
131
+ };
122
132
  }
123
133
  export interface PluginShippingRateQuoter {
124
134
  /** Whether quotes can be asked for this site now: configured and switched on. */
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-shipping-rates.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Live parcel rates, answered by the one plugin that talks to carriers\n * (AGL-3612).\n *\n * A plugin that sells goods prices delivery from its own table until the\n * merchant asks for live rates; then it needs a carrier's quote for an\n * address and a parcel, and it must not learn which carrier network, which\n * account or which vendor answers. A plugin that buys labels registers here;\n * a plugin that sells asks here, and neither imports the other.\n *\n * ## The shape is a parcel and an address, nothing more\n *\n * Every type below is postal: where it goes, what it weighs, how big it is,\n * and what it is worth for insurance and customs. Nothing names a catalog, an\n * order or a store, so a booking plugin shipping a rental kit asks the same\n * question in the same words.\n *\n * ## Nobody home is `null`, never a refusal\n *\n * Unlike the tax profile, an absent quoter is a perfectly good answer: the\n * seller falls back to its own rates, which is what it did before anyone\n * registered. {@link pluginShippingRateQuoter} answers `null`, and a quoter\n * whose deployment is not configured answers `available() === false`, which\n * a caller must read the same way.\n *\n * ## A quote is advisory until a label is bought\n *\n * `amountCents` is what the carrier quoted the platform for that parcel at\n * that moment, BEFORE any markup or handling the seller adds. The seller\n * decides what the shopper pays; the quoter never does.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-shipping-rates`); it is not in the\n * barrel.\n */\n\n/** A postal address. `country` is ISO-3166 alpha-2; everything else is optional. */\nexport interface PluginShippingAddress {\n name?: string\n company?: string\n line1?: string\n line2?: string\n city?: string\n /** State, province or region code, as the destination writes it. */\n state?: string\n postalCode?: string\n country: string\n phone?: string\n email?: string\n /** Whether the address is a home, when the caller knows; carriers price it. */\n residential?: boolean\n}\n\n/** One parcel. Metric throughout; an adapter converts to its vendor's units. */\nexport interface PluginShippingParcel {\n weightGrams: number\n lengthCm?: number\n widthCm?: number\n heightCm?: number\n}\n\n/** What a seller asks: one shipment's rates to one address. */\nexport interface PluginShippingQuoteRequest {\n /** The site the shipment leaves from; the quoter resolves its ship-from address. */\n hostId: string\n to: PluginShippingAddress\n parcels: PluginShippingParcel[]\n /** ISO-4217, lower case, of the amounts the caller will charge in. */\n currency: string\n /** The goods' value, for insurance and customs; 0 when unknown. */\n valueCents: number\n /**\n * Service keys the seller offers (`'usps:priority'`), as\n * {@link PluginShippingRateQuoter.listServices} names them. Empty or\n * absent means every service the quoter can price.\n */\n services?: string[]\n /** Aborted when the caller stops waiting; a quoter passes it to its fetches. */\n signal?: AbortSignal\n}\n\n/** One carrier service's price for the shipment. */\nexport interface PluginShippingQuote {\n /** Stable across quotes: `carrier:service`, the key a seller stores. */\n serviceKey: string\n carrier: string\n service: string\n /** What a shopper reads: `'USPS Priority Mail'`. */\n label: string\n /** The carrier's price to the platform, before any markup or handling. */\n amountCents: number\n currency: string\n /** Transit estimate in business days, when the carrier gives one. */\n estimatedDays?: number\n}\n\n/** A service a seller may choose to offer. */\nexport interface PluginShippingService {\n serviceKey: string\n carrier: string\n label: string\n}\n\n/** What address validation answers. */\nexport interface PluginShippingAddressCheck {\n /** `valid` deliverable as written; `corrected` deliverable as `suggested`; `invalid` not deliverable. */\n verdict: 'valid' | 'corrected' | 'invalid' | 'unknown'\n /** The address the carrier would deliver to, when it differs. */\n suggested?: PluginShippingAddress\n /** Why it is not deliverable, in the carrier's words. */\n messages: string[]\n}\n\nexport interface PluginShippingRateQuoter {\n /** Whether quotes can be asked for this site now: configured and switched on. */\n available(hostId: string): Promise<boolean>\n /**\n * The rates. Throws on a provider failure; a caller that cannot wait\n * aborts `signal` and falls back to its own rates.\n */\n quote(request: PluginShippingQuoteRequest): Promise<PluginShippingQuote[]>\n /** The services a seller can pick from for this site. */\n listServices(hostId: string): Promise<PluginShippingService[]>\n /** Whether an address is deliverable, and the carrier's correction. */\n validateAddress?(\n hostId: string,\n address: PluginShippingAddress,\n ): Promise<PluginShippingAddressCheck>\n}\n\nexport const PLUGIN_SHIPPING_RATE_QUOTER =\n definePluginServiceContract<PluginShippingRateQuoter>('core.shipping-rate-quoter', {\n multiple: false,\n })\n\n/** Registers the plugin that quotes carrier rates. A second plugin is refused. */\nexport function registerPluginShippingRateQuoter(\n quoter: PluginShippingRateQuoter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_SHIPPING_RATE_QUOTER, quoter, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The quoter, or `null` when no plugin registered one. */\nexport function pluginShippingRateQuoter(): PluginShippingRateQuoter | null {\n return resolvePluginServices(PLUGIN_SHIPPING_RATE_QUOTER)[0]?.impl ?? null\n}\n\n/**\n * Asks the quoter and gives up after `timeoutMs`: `null` when nobody is\n * registered, the site is not available, the provider failed, or the wait\n * ran out. Never throws. The caller's fallback is its own rates, so every\n * failure reads the same.\n */\nexport async function quotePluginShippingRates(\n request: Omit<PluginShippingQuoteRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<PluginShippingQuote[] | null> {\n const quoter = pluginShippingRateQuoter()\n if (!quoter) return null\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<null>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve(null)\n }, Math.max(0, options.timeoutMs))\n })\n try {\n return await Promise.race([\n (async (): Promise<PluginShippingQuote[] | null> => {\n if (!(await quoter.available(request.hostId))) return null\n return quoter.quote({ ...request, signal: controller.signal })\n })().catch((): null => null),\n deadline,\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_SHIPPING_RATE_QUOTER","multiple","registerPluginShippingRateQuoter","quoter","options","pluginId","pluginShippingRateQuoter","impl","quotePluginShippingRates","request","controller","AbortController","timer","deadline","Promise","resolve","setTimeout","abort","Math","max","timeoutMs","race","available","hostId","quote","signal","catch","clearTimeout"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAoI1B,OAAO,MAAMC,8BACXH,4BAAsD,6BAA6B;IACjFI,UAAU;AACZ,GAAE;AAEJ,gFAAgF,GAChF,OAAO,SAASC,iCACdC,MAAgC,EAChCC,OAA+B;IAE/BN,sBAAsBE,6BAA6BG,QAAQ,aACrDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPP;IAAP,gBAAOA,0BAAAA,sBAAsBC,4BAA4B,CAAC,EAAE,qBAArDD,wBAAuDQ,IAAI,mBAAI;AACxE;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,yBACpBC,OAAmD,EACnDL,OAA8B;IAE9B,MAAMD,SAASG;IACf,IAAI,CAACH,QAAQ,OAAO;IACpB,MAAMO,aAAa,IAAIC;IACvB,IAAIC;IACJ,MAAMC,WAAW,IAAIC,QAAc,CAACC;QAClCH,QAAQI,WAAW;YACjBN,WAAWO,KAAK;YAChBF,QAAQ;QACV,GAAGG,KAAKC,GAAG,CAAC,GAAGf,QAAQgB,SAAS;IAClC;IACA,IAAI;QACF,OAAO,MAAMN,QAAQO,IAAI,CAAC;YACvB,CAAA;gBACC,IAAI,CAAE,MAAMlB,OAAOmB,SAAS,CAACb,QAAQc,MAAM,GAAI,OAAO;gBACtD,OAAOpB,OAAOqB,KAAK,CAAC,aAAKf;oBAASgB,QAAQf,WAAWe,MAAM;;YAC7D,CAAA,IAAKC,KAAK,CAAC,IAAY;YACvBb;SACD;IACH,SAAU;QACR,IAAID,OAAOe,aAAaf;IAC1B;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-shipping-rates.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Live parcel rates, answered by the one plugin that talks to carriers\n * (AGL-3612).\n *\n * A plugin that sells goods prices delivery from its own table until the\n * merchant asks for live rates; then it needs a carrier's quote for an\n * address and a parcel, and it must not learn which carrier network, which\n * account or which vendor answers. A plugin that buys labels registers here;\n * a plugin that sells asks here, and neither imports the other.\n *\n * ## The shape is a parcel and an address, nothing more\n *\n * Every type below is postal: where it goes, what it weighs, how big it is,\n * and what it is worth for insurance and customs. Nothing names a catalog, an\n * order or a store, so a booking plugin shipping a rental kit asks the same\n * question in the same words.\n *\n * ## Nobody home is `null`, never a refusal\n *\n * Unlike the tax profile, an absent quoter is a perfectly good answer: the\n * seller falls back to its own rates, which is what it did before anyone\n * registered. {@link pluginShippingRateQuoter} answers `null`, and a quoter\n * whose deployment is not configured answers `available() === false`, which\n * a caller must read the same way.\n *\n * ## A quote is advisory until a label is bought\n *\n * `amountCents` is what the carrier quoted the platform for that parcel at\n * that moment, BEFORE any markup or handling the seller adds. The seller\n * decides what the shopper pays; the quoter never does.\n *\n * Import this module by its own subpath\n * (`@aglyn/aglyn/plugin-manager/plugin-shipping-rates`); it is not in the\n * barrel.\n */\n\n/** A postal address. `country` is ISO-3166 alpha-2; everything else is optional. */\nexport interface PluginShippingAddress {\n name?: string\n company?: string\n line1?: string\n line2?: string\n city?: string\n /** State, province or region code, as the destination writes it. */\n state?: string\n postalCode?: string\n country: string\n phone?: string\n email?: string\n /** Whether the address is a home, when the caller knows; carriers price it. */\n residential?: boolean\n}\n\n/** One parcel. Metric throughout; an adapter converts to its vendor's units. */\nexport interface PluginShippingParcel {\n weightGrams: number\n lengthCm?: number\n widthCm?: number\n heightCm?: number\n}\n\n/** What a seller asks: one shipment's rates to one address. */\nexport interface PluginShippingQuoteRequest {\n /** The site the shipment leaves from; the quoter resolves its ship-from address. */\n hostId: string\n to: PluginShippingAddress\n parcels: PluginShippingParcel[]\n /** ISO-4217, lower case, of the amounts the caller will charge in. */\n currency: string\n /** The goods' value, for insurance and customs; 0 when unknown. */\n valueCents: number\n /**\n * Service keys the seller offers (`'usps:priority'`), as\n * {@link PluginShippingRateQuoter.listServices} names them. Empty or\n * absent means every service the quoter can price.\n */\n services?: string[]\n /** Aborted when the caller stops waiting; a quoter passes it to its fetches. */\n signal?: AbortSignal\n}\n\n/** One carrier service's price for the shipment. */\nexport interface PluginShippingQuote {\n /** Stable across quotes: `carrier:service`, the key a seller stores. */\n serviceKey: string\n carrier: string\n service: string\n /** What a shopper reads: `'USPS Priority Mail'`. */\n label: string\n /** The carrier's price to the platform, before any markup or handling. */\n amountCents: number\n currency: string\n /** Transit estimate in business days, when the carrier gives one. */\n estimatedDays?: number\n}\n\n/** A service a seller may choose to offer. */\nexport interface PluginShippingService {\n serviceKey: string\n carrier: string\n label: string\n}\n\n/** What address validation answers. */\nexport interface PluginShippingAddressCheck {\n /** `valid` deliverable as written; `corrected` deliverable as `suggested`; `invalid` not deliverable. */\n verdict: 'valid' | 'corrected' | 'invalid' | 'unknown'\n /** The address the carrier would deliver to, when it differs. */\n suggested?: PluginShippingAddress\n /** Why it is not deliverable, in the carrier's words. */\n messages: string[]\n /**\n * Where the address is on a map, when the provider placed it. Optional:\n * only some providers return it, and only for addresses they verified. A\n * caller measuring distance (local delivery radius zones) treats its\n * absence as \"cannot measure\", never as zero.\n */\n coordinates?: { lat: number; lng: number }\n}\n\nexport interface PluginShippingRateQuoter {\n /** Whether quotes can be asked for this site now: configured and switched on. */\n available(hostId: string): Promise<boolean>\n /**\n * The rates. Throws on a provider failure; a caller that cannot wait\n * aborts `signal` and falls back to its own rates.\n */\n quote(request: PluginShippingQuoteRequest): Promise<PluginShippingQuote[]>\n /** The services a seller can pick from for this site. */\n listServices(hostId: string): Promise<PluginShippingService[]>\n /** Whether an address is deliverable, and the carrier's correction. */\n validateAddress?(\n hostId: string,\n address: PluginShippingAddress,\n ): Promise<PluginShippingAddressCheck>\n}\n\nexport const PLUGIN_SHIPPING_RATE_QUOTER =\n definePluginServiceContract<PluginShippingRateQuoter>('core.shipping-rate-quoter', {\n multiple: false,\n })\n\n/** Registers the plugin that quotes carrier rates. A second plugin is refused. */\nexport function registerPluginShippingRateQuoter(\n quoter: PluginShippingRateQuoter,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_SHIPPING_RATE_QUOTER, quoter, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The quoter, or `null` when no plugin registered one. */\nexport function pluginShippingRateQuoter(): PluginShippingRateQuoter | null {\n return resolvePluginServices(PLUGIN_SHIPPING_RATE_QUOTER)[0]?.impl ?? null\n}\n\n/**\n * Asks the quoter and gives up after `timeoutMs`: `null` when nobody is\n * registered, the site is not available, the provider failed, or the wait\n * ran out. Never throws. The caller's fallback is its own rates, so every\n * failure reads the same.\n */\nexport async function quotePluginShippingRates(\n request: Omit<PluginShippingQuoteRequest, 'signal'>,\n options: { timeoutMs: number },\n): Promise<PluginShippingQuote[] | null> {\n const quoter = pluginShippingRateQuoter()\n if (!quoter) return null\n const controller = new AbortController()\n let timer: ReturnType<typeof setTimeout> | undefined\n const deadline = new Promise<null>((resolve) => {\n timer = setTimeout(() => {\n controller.abort()\n resolve(null)\n }, Math.max(0, options.timeoutMs))\n })\n try {\n return await Promise.race([\n (async (): Promise<PluginShippingQuote[] | null> => {\n if (!(await quoter.available(request.hostId))) return null\n return quoter.quote({ ...request, signal: controller.signal })\n })().catch((): null => null),\n deadline,\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_SHIPPING_RATE_QUOTER","multiple","registerPluginShippingRateQuoter","quoter","options","pluginId","pluginShippingRateQuoter","impl","quotePluginShippingRates","request","controller","AbortController","timer","deadline","Promise","resolve","setTimeout","abort","Math","max","timeoutMs","race","available","hostId","quote","signal","catch","clearTimeout"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AA2I1B,OAAO,MAAMC,8BACXH,4BAAsD,6BAA6B;IACjFI,UAAU;AACZ,GAAE;AAEJ,gFAAgF,GAChF,OAAO,SAASC,iCACdC,MAAgC,EAChCC,OAA+B;IAE/BN,sBAAsBE,6BAA6BG,QAAQ,aACrDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,yDAAyD,GACzD,OAAO,SAASC;;QACPP;IAAP,gBAAOA,0BAAAA,sBAAsBC,4BAA4B,CAAC,EAAE,qBAArDD,wBAAuDQ,IAAI,mBAAI;AACxE;AAEA;;;;;CAKC,GACD,OAAO,eAAeC,yBACpBC,OAAmD,EACnDL,OAA8B;IAE9B,MAAMD,SAASG;IACf,IAAI,CAACH,QAAQ,OAAO;IACpB,MAAMO,aAAa,IAAIC;IACvB,IAAIC;IACJ,MAAMC,WAAW,IAAIC,QAAc,CAACC;QAClCH,QAAQI,WAAW;YACjBN,WAAWO,KAAK;YAChBF,QAAQ;QACV,GAAGG,KAAKC,GAAG,CAAC,GAAGf,QAAQgB,SAAS;IAClC;IACA,IAAI;QACF,OAAO,MAAMN,QAAQO,IAAI,CAAC;YACvB,CAAA;gBACC,IAAI,CAAE,MAAMlB,OAAOmB,SAAS,CAACb,QAAQc,MAAM,GAAI,OAAO;gBACtD,OAAOpB,OAAOqB,KAAK,CAAC,aAAKf;oBAASgB,QAAQf,WAAWe,MAAM;;YAC7D,CAAA,IAAKC,KAAK,CAAC,IAAY;YACvBb;SACD;IACH,SAAU;QACR,IAAID,OAAOe,aAAaf;IAC1B;AACF"}