@aglyn/aglyn 1.0.0-beta.201 → 1.0.0-beta.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/activity-presenter.d.ts +29 -3
- package/src/lib/app-utils/activity-presenter.js +32 -4
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +20 -0
- package/src/lib/app-utils/api-plugins.js +9 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/card-payment-velocity.d.ts +57 -0
- package/src/lib/app-utils/card-payment-velocity.js +90 -0
- package/src/lib/app-utils/card-payment-velocity.js.map +1 -0
- package/src/lib/app-utils/crm-digest.d.ts +6 -0
- package/src/lib/app-utils/crm-digest.js +1 -0
- package/src/lib/app-utils/crm-digest.js.map +1 -1
- package/src/lib/app-utils/crm-task-reminders.d.ts +6 -0
- package/src/lib/app-utils/crm-task-reminders.js +1 -0
- package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +2 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +8 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/marketplace-listing-visibility.js +5 -5
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +35 -0
- package/src/lib/app-utils/notifications.js +57 -1
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/platform-brand.d.ts +8 -2
- package/src/lib/app-utils/platform-brand.js +9 -3
- package/src/lib/app-utils/platform-brand.js.map +1 -1
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +2 -1
- package/src/lib/app-utils/plugin-bundle-checks.js +36 -1
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/site-return-url.d.ts +55 -0
- package/src/lib/app-utils/site-return-url.js +129 -0
- package/src/lib/app-utils/site-return-url.js.map +1 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +9 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.js +5 -1
- package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -1
- package/src/lib/plugin-manager/plugin-recurring-charges.d.ts +39 -0
- package/src/lib/plugin-manager/plugin-recurring-charges.js +49 -0
- package/src/lib/plugin-manager/plugin-recurring-charges.js.map +1 -0
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
* WHERE A PAYMENT SENDS THE SHOPPER BACK TO (AGL-3363).
|
|
18
|
+
*
|
|
19
|
+
* Every visitor payment door — buy-now, cart, reservation, booking deposit,
|
|
20
|
+
* the subscription portal — hands Stripe the URL the shopper returns to
|
|
21
|
+
* after paying or backing out. They built it from the request's `Referer`
|
|
22
|
+
* (or `Origin`), falling back to its `Host`. All three are headers the
|
|
23
|
+
* caller writes: a script that posts to a real shop's checkout with
|
|
24
|
+
* `Referer: https://paypal-verify.top/` got back a GENUINE Stripe Checkout
|
|
25
|
+
* link, for a real merchant, that lands the payer on the phishing page. And
|
|
26
|
+
* the webhook later reads that same `success_url` as the site's origin for
|
|
27
|
+
* the receipt's download links, so the lure reached the owed receipt too.
|
|
28
|
+
*
|
|
29
|
+
* So a return URL is accepted only when its host is the SITE'S OWN: its
|
|
30
|
+
* subdomain on the tenant apex, its custom domain, or `www.` of that domain.
|
|
31
|
+
* Anything else — another tenant's site, an arbitrary domain, a lookalike —
|
|
32
|
+
* is replaced by the site's public origin. A host that is shaped like a
|
|
33
|
+
* brand it is not (`lookalikeBrandForHost`, the phishing screen's rule) is
|
|
34
|
+
* never accepted, even as the site's own custom domain: the lookalike tier
|
|
35
|
+
* holds for everyone, and the site's apex subdomain still works.
|
|
36
|
+
*
|
|
37
|
+
* `localhost` is accepted for development; a stranger who sends a shopper
|
|
38
|
+
* to their own machine has sent them nowhere.
|
|
39
|
+
*
|
|
40
|
+
* Pure: every door calls it with the host document it already read.
|
|
41
|
+
*=========================================*/ import { lookalikeBrandForHost } from "@aglyn/shared-util-email/outbound-phishing-screen";
|
|
42
|
+
import { hostPublicOrigin, TENANT_APEX } from "./host-naming.js";
|
|
43
|
+
const clean = (value)=>typeof value === 'string' ? value.trim().toLowerCase().replace(/\.+$/, '') : '';
|
|
44
|
+
/** The hosts a site answers on: apex subdomain, custom domain, `www.` of it. */ export function siteOwnHosts(site) {
|
|
45
|
+
const subdomain = clean(site == null ? void 0 : site.subdomain);
|
|
46
|
+
const cname = clean(site == null ? void 0 : site.cname);
|
|
47
|
+
const hosts = new Set();
|
|
48
|
+
if (subdomain) hosts.add(`${subdomain}.${TENANT_APEX}`);
|
|
49
|
+
if (cname) {
|
|
50
|
+
hosts.add(cname);
|
|
51
|
+
hosts.add(cname.startsWith('www.') ? cname.slice(4) : `www.${cname}`);
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
...hosts
|
|
55
|
+
].filter((host)=>!lookalikeBrandForHost(host));
|
|
56
|
+
}
|
|
57
|
+
function isLocalHost(hostname) {
|
|
58
|
+
return hostname === 'localhost' || hostname.endsWith('.localhost');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Is `candidate` a URL on one of the site's own hosts? https only, except
|
|
62
|
+
* on localhost; no credentials in the authority (`https://shop@evil.top`).
|
|
63
|
+
*/ export function isSiteOwnUrl(candidate, site) {
|
|
64
|
+
if (typeof candidate !== 'string' || !candidate) return false;
|
|
65
|
+
let url;
|
|
66
|
+
try {
|
|
67
|
+
url = new URL(candidate);
|
|
68
|
+
} catch (unused) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (url.username || url.password) return false;
|
|
72
|
+
const hostname = url.hostname.toLowerCase();
|
|
73
|
+
if (isLocalHost(hostname)) {
|
|
74
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== 'https:') return false;
|
|
77
|
+
return siteOwnHosts(site).includes(hostname);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The URL a payment returns the shopper to: the first candidate on the
|
|
81
|
+
* site's own hosts (fragment dropped), else the site's public origin, else
|
|
82
|
+
* — for a site document with neither a subdomain nor a domain, which no live
|
|
83
|
+
* site has — the request's own host, as before.
|
|
84
|
+
*
|
|
85
|
+
* `candidates` in preference order: the `Referer` (the page the shopper was
|
|
86
|
+
* on), then `https://<Host>`.
|
|
87
|
+
*/ export function siteReturnUrl(input) {
|
|
88
|
+
var _input_site;
|
|
89
|
+
for (const candidate of input.candidates){
|
|
90
|
+
if (isSiteOwnUrl(candidate, input.site)) {
|
|
91
|
+
const url = new URL(String(candidate));
|
|
92
|
+
url.hash = '';
|
|
93
|
+
return url.toString();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const own = siteOwnHosts(input.site);
|
|
97
|
+
const publicOrigin = hostPublicOrigin(input.site);
|
|
98
|
+
if (publicOrigin && own.includes(new URL(publicOrigin).hostname)) {
|
|
99
|
+
return `${publicOrigin}/`;
|
|
100
|
+
}
|
|
101
|
+
const subdomain = clean((_input_site = input.site) == null ? void 0 : _input_site.subdomain);
|
|
102
|
+
if (subdomain) return `https://${subdomain}.${TENANT_APEX}/`;
|
|
103
|
+
const requestHost = clean(input.requestHost);
|
|
104
|
+
return requestHost ? `https://${requestHost}/` : '/';
|
|
105
|
+
}
|
|
106
|
+
/** {@link siteReturnUrl}'s origin, for a door that returns to the site root. */ export function siteReturnOrigin(input) {
|
|
107
|
+
const url = siteReturnUrl(input);
|
|
108
|
+
try {
|
|
109
|
+
return new URL(url).origin;
|
|
110
|
+
} catch (unused) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The site fields, read from a host document reference by a door that has
|
|
116
|
+
* not read it yet. A failed or missing read answers null, which keeps the
|
|
117
|
+
* request's own host as the only acceptable return — never a `Referer`.
|
|
118
|
+
*/ export async function readSiteReturnHost(ref) {
|
|
119
|
+
try {
|
|
120
|
+
var _snapshot_data;
|
|
121
|
+
const snapshot = await (ref == null ? void 0 : ref.get());
|
|
122
|
+
const data = snapshot == null ? void 0 : (_snapshot_data = snapshot.data) == null ? void 0 : _snapshot_data.call(snapshot);
|
|
123
|
+
return data && typeof data === 'object' ? data : null;
|
|
124
|
+
} catch (unused) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=site-return-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-return-url.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * WHERE A PAYMENT SENDS THE SHOPPER BACK TO (AGL-3363).\n *\n * Every visitor payment door — buy-now, cart, reservation, booking deposit,\n * the subscription portal — hands Stripe the URL the shopper returns to\n * after paying or backing out. They built it from the request's `Referer`\n * (or `Origin`), falling back to its `Host`. All three are headers the\n * caller writes: a script that posts to a real shop's checkout with\n * `Referer: https://paypal-verify.top/` got back a GENUINE Stripe Checkout\n * link, for a real merchant, that lands the payer on the phishing page. And\n * the webhook later reads that same `success_url` as the site's origin for\n * the receipt's download links, so the lure reached the owed receipt too.\n *\n * So a return URL is accepted only when its host is the SITE'S OWN: its\n * subdomain on the tenant apex, its custom domain, or `www.` of that domain.\n * Anything else — another tenant's site, an arbitrary domain, a lookalike —\n * is replaced by the site's public origin. A host that is shaped like a\n * brand it is not (`lookalikeBrandForHost`, the phishing screen's rule) is\n * never accepted, even as the site's own custom domain: the lookalike tier\n * holds for everyone, and the site's apex subdomain still works.\n *\n * `localhost` is accepted for development; a stranger who sends a shopper\n * to their own machine has sent them nowhere.\n *\n * Pure: every door calls it with the host document it already read.\n *=========================================*/\n\nimport { lookalikeBrandForHost } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { hostPublicOrigin, TENANT_APEX } from './host-naming'\n\n/** The site fields a return URL is judged against. */\nexport interface SiteReturnHost {\n subdomain?: string | null\n cname?: string | null\n}\n\nconst clean = (value: unknown): string =>\n typeof value === 'string' ? value.trim().toLowerCase().replace(/\\.+$/, '') : ''\n\n/** The hosts a site answers on: apex subdomain, custom domain, `www.` of it. */\nexport function siteOwnHosts(site: SiteReturnHost | null | undefined): string[] {\n const subdomain = clean(site?.subdomain)\n const cname = clean(site?.cname)\n const hosts = new Set<string>()\n if (subdomain) hosts.add(`${subdomain}.${TENANT_APEX}`)\n if (cname) {\n hosts.add(cname)\n hosts.add(cname.startsWith('www.') ? cname.slice(4) : `www.${cname}`)\n }\n return [...hosts].filter((host) => !lookalikeBrandForHost(host))\n}\n\nfunction isLocalHost(hostname: string): boolean {\n return hostname === 'localhost' || hostname.endsWith('.localhost')\n}\n\n/**\n * Is `candidate` a URL on one of the site's own hosts? https only, except\n * on localhost; no credentials in the authority (`https://shop@evil.top`).\n */\nexport function isSiteOwnUrl(\n candidate: unknown,\n site: SiteReturnHost | null | undefined,\n): boolean {\n if (typeof candidate !== 'string' || !candidate) return false\n let url: URL\n try {\n url = new URL(candidate)\n } catch {\n return false\n }\n if (url.username || url.password) return false\n const hostname = url.hostname.toLowerCase()\n if (isLocalHost(hostname)) {\n return url.protocol === 'http:' || url.protocol === 'https:'\n }\n if (url.protocol !== 'https:') return false\n return siteOwnHosts(site).includes(hostname)\n}\n\n/**\n * The URL a payment returns the shopper to: the first candidate on the\n * site's own hosts (fragment dropped), else the site's public origin, else\n * — for a site document with neither a subdomain nor a domain, which no live\n * site has — the request's own host, as before.\n *\n * `candidates` in preference order: the `Referer` (the page the shopper was\n * on), then `https://<Host>`.\n */\nexport function siteReturnUrl(input: {\n candidates: readonly (string | null | undefined)[]\n site: SiteReturnHost | null | undefined\n /** The request's `Host` header, the last resort. */\n requestHost?: string | null\n}): string {\n for (const candidate of input.candidates) {\n if (isSiteOwnUrl(candidate, input.site)) {\n const url = new URL(String(candidate))\n url.hash = ''\n return url.toString()\n }\n }\n const own = siteOwnHosts(input.site)\n const publicOrigin = hostPublicOrigin(input.site)\n if (publicOrigin && own.includes(new URL(publicOrigin).hostname)) {\n return `${publicOrigin}/`\n }\n const subdomain = clean(input.site?.subdomain)\n if (subdomain) return `https://${subdomain}.${TENANT_APEX}/`\n const requestHost = clean(input.requestHost)\n return requestHost ? `https://${requestHost}/` : '/'\n}\n\n/** {@link siteReturnUrl}'s origin, for a door that returns to the site root. */\nexport function siteReturnOrigin(input: Parameters<typeof siteReturnUrl>[0]): string {\n const url = siteReturnUrl(input)\n try {\n return new URL(url).origin\n } catch {\n return ''\n }\n}\n\n/**\n * The site fields, read from a host document reference by a door that has\n * not read it yet. A failed or missing read answers null, which keeps the\n * request's own host as the only acceptable return — never a `Referer`.\n */\nexport async function readSiteReturnHost(\n ref:\n | { get(): Promise<{ data?: () => unknown } | null | undefined> }\n | null\n | undefined,\n): Promise<SiteReturnHost | null> {\n try {\n const snapshot = await ref?.get()\n const data = snapshot?.data?.()\n return data && typeof data === 'object' ? (data as SiteReturnHost) : null\n } catch {\n return null\n }\n}\n"],"names":["lookalikeBrandForHost","hostPublicOrigin","TENANT_APEX","clean","value","trim","toLowerCase","replace","siteOwnHosts","site","subdomain","cname","hosts","Set","add","startsWith","slice","filter","host","isLocalHost","hostname","endsWith","isSiteOwnUrl","candidate","url","URL","username","password","protocol","includes","siteReturnUrl","input","candidates","String","hash","toString","own","publicOrigin","requestHost","siteReturnOrigin","origin","readSiteReturnHost","ref","snapshot","get","data"],"mappings":"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"}
|
|
@@ -61,6 +61,13 @@ export interface BillingWebhookHandlerResult {
|
|
|
61
61
|
* them, without the route knowing what an order or a booking is.
|
|
62
62
|
*/
|
|
63
63
|
hostId?: string;
|
|
64
|
+
/**
|
|
65
|
+
* The workspace that SOLD the claimed record, when the seller is a
|
|
66
|
+
* workspace rather than a site (AGL-3365) — a marketplace publisher. The
|
|
67
|
+
* seller's fraud-pattern row names it, so a publisher's pattern reads as
|
|
68
|
+
* the publisher's and never as the buyer's.
|
|
69
|
+
*/
|
|
70
|
+
orgId?: string;
|
|
64
71
|
}
|
|
65
72
|
export type BillingWebhookHandler = (event: BillingWebhookEvent) => Promise<void | BillingWebhookHandlerResult> | void | BillingWebhookHandlerResult;
|
|
66
73
|
/** What `runBillingWebhookHandlers` reports back about a whole dispatch. */
|
|
@@ -69,6 +76,8 @@ export interface BillingWebhookDispatchResult {
|
|
|
69
76
|
claimed: boolean;
|
|
70
77
|
/** Every distinct `hostId` a handler reported, in handler order. */
|
|
71
78
|
hostIds: string[];
|
|
79
|
+
/** Every distinct seller `orgId` a handler reported, in handler order. */
|
|
80
|
+
orgIds: string[];
|
|
72
81
|
}
|
|
73
82
|
/**
|
|
74
83
|
* Registers a webhook handler. The plugin loader guarantees each plugin's
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/ export async function runBillingWebhookHandlers(event) {
|
|
41
41
|
let claimed = false;
|
|
42
42
|
const hostIds = [];
|
|
43
|
+
const orgIds = [];
|
|
43
44
|
for (const handler of handlers){
|
|
44
45
|
// Narrowed through the union rather than read off it: the handler's
|
|
45
46
|
// return type includes `void` — the shape every handler that does not
|
|
@@ -49,10 +50,13 @@
|
|
|
49
50
|
if ((result == null ? void 0 : result.claimed) === true) claimed = true;
|
|
50
51
|
const hostId = typeof (result == null ? void 0 : result.hostId) === 'string' ? result.hostId : '';
|
|
51
52
|
if (hostId && !hostIds.includes(hostId)) hostIds.push(hostId);
|
|
53
|
+
const orgId = typeof (result == null ? void 0 : result.orgId) === 'string' ? result.orgId : '';
|
|
54
|
+
if (orgId && !orgIds.includes(orgId)) orgIds.push(orgId);
|
|
52
55
|
}
|
|
53
56
|
return {
|
|
54
57
|
claimed,
|
|
55
|
-
hostIds
|
|
58
|
+
hostIds,
|
|
59
|
+
orgIds
|
|
56
60
|
};
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/billing-webhook-hooks.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Billing-webhook extension point (AGL-418): plugins register handlers for\n * the platform Stripe webhook from their `/server` entries — the console's\n * webhook route verifies the signature, syncs the org's plan/subscription\n * (the only core-billing concern), then fans the event out here. Handlers\n * self-select on `type` + `object.metadata.type` exactly as the old inline\n * sections did (commerce orders/carts/drafts/reservations/subscriptions,\n * booking payments, marketplace purchases). Errors PROPAGATE: a throwing\n * handler fails the webhook with a 500 so Stripe redelivers — identical to\n * the pre-extraction behavior, and every section is idempotent by doc key.\n */\n\nexport interface BillingWebhookEvent {\n /** Stripe event type, e.g. 'checkout.session.completed'. */\n type: string\n /** `event.data.object` — the session/subscription/invoice payload. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n object: any\n /** The full parsed Stripe event, for handlers that need ids/metadata. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n event: any\n /** The webhook request's Host header — for absolute callback URLs. */\n requestHost?: string\n}\n\n/**\n * What a handler may tell the route about the event it just saw (AGL-2429).\n *\n * Handlers have always returned `void`, which made \"a plugin handled this\"\n * and \"every plugin ignored this\" the same observation from the route's side.\n * On most events that is harmless — the route does not care who consumed a\n * `checkout.session.completed`. On a CHARGEBACK it is the whole question: an\n * event nobody claimed is money that moved with nothing recording it, and it\n * is indistinguishable from the ordinary case where a plugin quietly did its\n * job. See the `charge.dispute.*` section of the console's webhook route.\n *\n * `claimed` is deliberately opt-in and deliberately narrow. A handler sets it\n * when it RECOGNISED the event as its own — found the order, the purchase,\n * the booking — not when it merely ran. Returning nothing keeps the old\n * meaning, so every handler that does not care is unaffected.\n */\nexport interface BillingWebhookHandlerResult {\n /** True when this handler recognised the event as belonging to it. */\n claimed?: boolean\n /**\n * The site the claimed record belongs to (AGL-3360), when the handler\n * knows it. The route reads it for a charge's fraud signal: the seller's\n * pattern row names the sites the charges were for, whichever plugin sold\n * them, without the route knowing what an order or a booking is.\n */\n hostId?: string\n}\n\nexport type BillingWebhookHandler = (\n event: BillingWebhookEvent,\n) =>\n | Promise<void | BillingWebhookHandlerResult>\n | void\n | BillingWebhookHandlerResult\n\n/** What `runBillingWebhookHandlers` reports back about a whole dispatch. */\nexport interface BillingWebhookDispatchResult {\n /** True when ANY handler claimed the event. */\n claimed: boolean\n /** Every distinct `hostId` a handler reported, in handler order. */\n hostIds: string[]\n}\n\nconst handlers: BillingWebhookHandler[] = []\n\n/**\n * Registers a webhook handler. The plugin loader guarantees each plugin's\n * register fn runs once per process, so no dedupe is needed here.\n */\nexport function registerBillingWebhookHandler(\n handler: BillingWebhookHandler,\n): void {\n handlers.push(handler)\n}\n\n/**\n * Runs every registered handler sequentially; the first throw propagates.\n *\n * EVERY handler runs even after one has claimed the event: two plugins may\n * legitimately care about the same event, and short-circuiting on the first\n * claim would turn a reporting signal into a dispatch rule and silently drop\n * side effects. The claim is a fold over all of them, never a break.\n */\nexport async function runBillingWebhookHandlers(\n event: BillingWebhookEvent,\n): Promise<BillingWebhookDispatchResult> {\n let claimed = false\n const hostIds: string[] = []\n for (const handler of handlers) {\n // Narrowed through the union rather than read off it: the handler's\n // return type includes `void` — the shape every handler that does not\n // care still returns — and a property read on `void` is a type error even\n // with `strictNullChecks` off.\n const result = (await handler(event)) as\n | BillingWebhookHandlerResult\n | undefined\n if (result?.claimed === true) claimed = true\n const hostId = typeof result?.hostId === 'string' ? result.hostId : ''\n if (hostId && !hostIds.includes(hostId)) hostIds.push(hostId)\n }\n return { claimed, hostIds }\n}\n"],"names":["handlers","registerBillingWebhookHandler","handler","push","runBillingWebhookHandlers","event","claimed","hostIds","result","hostId","includes"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/billing-webhook-hooks.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Billing-webhook extension point (AGL-418): plugins register handlers for\n * the platform Stripe webhook from their `/server` entries — the console's\n * webhook route verifies the signature, syncs the org's plan/subscription\n * (the only core-billing concern), then fans the event out here. Handlers\n * self-select on `type` + `object.metadata.type` exactly as the old inline\n * sections did (commerce orders/carts/drafts/reservations/subscriptions,\n * booking payments, marketplace purchases). Errors PROPAGATE: a throwing\n * handler fails the webhook with a 500 so Stripe redelivers — identical to\n * the pre-extraction behavior, and every section is idempotent by doc key.\n */\n\nexport interface BillingWebhookEvent {\n /** Stripe event type, e.g. 'checkout.session.completed'. */\n type: string\n /** `event.data.object` — the session/subscription/invoice payload. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n object: any\n /** The full parsed Stripe event, for handlers that need ids/metadata. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n event: any\n /** The webhook request's Host header — for absolute callback URLs. */\n requestHost?: string\n}\n\n/**\n * What a handler may tell the route about the event it just saw (AGL-2429).\n *\n * Handlers have always returned `void`, which made \"a plugin handled this\"\n * and \"every plugin ignored this\" the same observation from the route's side.\n * On most events that is harmless — the route does not care who consumed a\n * `checkout.session.completed`. On a CHARGEBACK it is the whole question: an\n * event nobody claimed is money that moved with nothing recording it, and it\n * is indistinguishable from the ordinary case where a plugin quietly did its\n * job. See the `charge.dispute.*` section of the console's webhook route.\n *\n * `claimed` is deliberately opt-in and deliberately narrow. A handler sets it\n * when it RECOGNISED the event as its own — found the order, the purchase,\n * the booking — not when it merely ran. Returning nothing keeps the old\n * meaning, so every handler that does not care is unaffected.\n */\nexport interface BillingWebhookHandlerResult {\n /** True when this handler recognised the event as belonging to it. */\n claimed?: boolean\n /**\n * The site the claimed record belongs to (AGL-3360), when the handler\n * knows it. The route reads it for a charge's fraud signal: the seller's\n * pattern row names the sites the charges were for, whichever plugin sold\n * them, without the route knowing what an order or a booking is.\n */\n hostId?: string\n /**\n * The workspace that SOLD the claimed record, when the seller is a\n * workspace rather than a site (AGL-3365) — a marketplace publisher. The\n * seller's fraud-pattern row names it, so a publisher's pattern reads as\n * the publisher's and never as the buyer's.\n */\n orgId?: string\n}\n\nexport type BillingWebhookHandler = (\n event: BillingWebhookEvent,\n) =>\n | Promise<void | BillingWebhookHandlerResult>\n | void\n | BillingWebhookHandlerResult\n\n/** What `runBillingWebhookHandlers` reports back about a whole dispatch. */\nexport interface BillingWebhookDispatchResult {\n /** True when ANY handler claimed the event. */\n claimed: boolean\n /** Every distinct `hostId` a handler reported, in handler order. */\n hostIds: string[]\n /** Every distinct seller `orgId` a handler reported, in handler order. */\n orgIds: string[]\n}\n\nconst handlers: BillingWebhookHandler[] = []\n\n/**\n * Registers a webhook handler. The plugin loader guarantees each plugin's\n * register fn runs once per process, so no dedupe is needed here.\n */\nexport function registerBillingWebhookHandler(\n handler: BillingWebhookHandler,\n): void {\n handlers.push(handler)\n}\n\n/**\n * Runs every registered handler sequentially; the first throw propagates.\n *\n * EVERY handler runs even after one has claimed the event: two plugins may\n * legitimately care about the same event, and short-circuiting on the first\n * claim would turn a reporting signal into a dispatch rule and silently drop\n * side effects. The claim is a fold over all of them, never a break.\n */\nexport async function runBillingWebhookHandlers(\n event: BillingWebhookEvent,\n): Promise<BillingWebhookDispatchResult> {\n let claimed = false\n const hostIds: string[] = []\n const orgIds: string[] = []\n for (const handler of handlers) {\n // Narrowed through the union rather than read off it: the handler's\n // return type includes `void` — the shape every handler that does not\n // care still returns — and a property read on `void` is a type error even\n // with `strictNullChecks` off.\n const result = (await handler(event)) as\n | BillingWebhookHandlerResult\n | undefined\n if (result?.claimed === true) claimed = true\n const hostId = typeof result?.hostId === 'string' ? result.hostId : ''\n if (hostId && !hostIds.includes(hostId)) hostIds.push(hostId)\n const orgId = typeof result?.orgId === 'string' ? result.orgId : ''\n if (orgId && !orgIds.includes(orgId)) orgIds.push(orgId)\n }\n return { claimed, hostIds, orgIds }\n}\n"],"names":["handlers","registerBillingWebhookHandler","handler","push","runBillingWebhookHandlers","event","claimed","hostIds","orgIds","result","hostId","includes","orgId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAmED,MAAMA,WAAoC,EAAE;AAE5C;;;CAGC,GACD,OAAO,SAASC,8BACdC,OAA8B;IAE9BF,SAASG,IAAI,CAACD;AAChB;AAEA;;;;;;;CAOC,GACD,OAAO,eAAeE,0BACpBC,KAA0B;IAE1B,IAAIC,UAAU;IACd,MAAMC,UAAoB,EAAE;IAC5B,MAAMC,SAAmB,EAAE;IAC3B,KAAK,MAAMN,WAAWF,SAAU;QAC9B,oEAAoE;QACpE,sEAAsE;QACtE,0EAA0E;QAC1E,+BAA+B;QAC/B,MAAMS,SAAU,MAAMP,QAAQG;QAG9B,IAAII,CAAAA,0BAAAA,OAAQH,OAAO,MAAK,MAAMA,UAAU;QACxC,MAAMI,SAAS,QAAOD,0BAAAA,OAAQC,MAAM,MAAK,WAAWD,OAAOC,MAAM,GAAG;QACpE,IAAIA,UAAU,CAACH,QAAQI,QAAQ,CAACD,SAASH,QAAQJ,IAAI,CAACO;QACtD,MAAME,QAAQ,QAAOH,0BAAAA,OAAQG,KAAK,MAAK,WAAWH,OAAOG,KAAK,GAAG;QACjE,IAAIA,SAAS,CAACJ,OAAOG,QAAQ,CAACC,QAAQJ,OAAOL,IAAI,CAACS;IACpD;IACA,OAAO;QAAEN;QAASC;QAASC;IAAO;AACpC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** One subscription a site sells, as its plugin recorded it. */
|
|
18
|
+
export interface RecurringChargeRecord {
|
|
19
|
+
/** The payment provider's subscription id (Stripe `sub_…`). */
|
|
20
|
+
subscriptionId: string;
|
|
21
|
+
/** The site that sells it. */
|
|
22
|
+
hostId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface RecurringChargeSource {
|
|
25
|
+
/** Every subscription this plugin sells on `hostIds` that can still bill. */
|
|
26
|
+
listLiveSubscriptions(input: {
|
|
27
|
+
hostIds: readonly string[];
|
|
28
|
+
}): Promise<RecurringChargeRecord[]>;
|
|
29
|
+
}
|
|
30
|
+
export declare const PLUGIN_RECURRING_CHARGE_SOURCES: import("./plugin-services").PluginServiceContract<RecurringChargeSource>;
|
|
31
|
+
/** Declares a plugin's recurring charges to the lockdown. */
|
|
32
|
+
export declare function registerRecurringChargeSource(source: RecurringChargeSource, options?: {
|
|
33
|
+
pluginId?: string;
|
|
34
|
+
}): void;
|
|
35
|
+
/** Every declared source, with the plugin that declared it. */
|
|
36
|
+
export declare function listRecurringChargeSources(): Array<{
|
|
37
|
+
pluginId: string;
|
|
38
|
+
source: RecurringChargeSource;
|
|
39
|
+
}>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /*==========================================
|
|
18
|
+
* RECURRING CHARGES A SITE SELLS (AGL-3364).
|
|
19
|
+
*
|
|
20
|
+
* A security lockdown pauses the renewals a locked site sells to its own
|
|
21
|
+
* customers — memberships, subscriptions, anything that bills on a
|
|
22
|
+
* schedule — so a suspected fraudster's storefront stops taking money on
|
|
23
|
+
* cards it may have stolen. The lockdown is core; the records of what a
|
|
24
|
+
* site sells are each plugin's own. This contract is the seam between
|
|
25
|
+
* them: a plugin that sells recurring charges declares a source, and the
|
|
26
|
+
* lockdown asks every source for the subscriptions it holds for the locked
|
|
27
|
+
* sites. Core never reads a plugin's collection, and a marketplace plugin
|
|
28
|
+
* that sells subscriptions joins the same way.
|
|
29
|
+
*
|
|
30
|
+
* A source answers from its OWN stored records (the ids it saved when the
|
|
31
|
+
* sale completed), never from a broad search of the payment provider, and
|
|
32
|
+
* only for subscriptions that can still bill.
|
|
33
|
+
*=========================================*/ import { definePluginServiceContract, registerPluginService, resolvePluginServices } from "./plugin-services.js";
|
|
34
|
+
export const PLUGIN_RECURRING_CHARGE_SOURCES = definePluginServiceContract('core.recurring-charge-sources', {
|
|
35
|
+
multiple: true
|
|
36
|
+
});
|
|
37
|
+
/** Declares a plugin's recurring charges to the lockdown. */ export function registerRecurringChargeSource(source, options) {
|
|
38
|
+
registerPluginService(PLUGIN_RECURRING_CHARGE_SOURCES, source, _extends({}, (options == null ? void 0 : options.pluginId) ? {
|
|
39
|
+
pluginId: options.pluginId
|
|
40
|
+
} : {}));
|
|
41
|
+
}
|
|
42
|
+
/** Every declared source, with the plugin that declared it. */ export function listRecurringChargeSources() {
|
|
43
|
+
return resolvePluginServices(PLUGIN_RECURRING_CHARGE_SOURCES).map((entry)=>({
|
|
44
|
+
pluginId: entry.pluginId,
|
|
45
|
+
source: entry.impl
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//# sourceMappingURL=plugin-recurring-charges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-recurring-charges.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * RECURRING CHARGES A SITE SELLS (AGL-3364).\n *\n * A security lockdown pauses the renewals a locked site sells to its own\n * customers — memberships, subscriptions, anything that bills on a\n * schedule — so a suspected fraudster's storefront stops taking money on\n * cards it may have stolen. The lockdown is core; the records of what a\n * site sells are each plugin's own. This contract is the seam between\n * them: a plugin that sells recurring charges declares a source, and the\n * lockdown asks every source for the subscriptions it holds for the locked\n * sites. Core never reads a plugin's collection, and a marketplace plugin\n * that sells subscriptions joins the same way.\n *\n * A source answers from its OWN stored records (the ids it saved when the\n * sale completed), never from a broad search of the payment provider, and\n * only for subscriptions that can still bill.\n *=========================================*/\n\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/** One subscription a site sells, as its plugin recorded it. */\nexport interface RecurringChargeRecord {\n /** The payment provider's subscription id (Stripe `sub_…`). */\n subscriptionId: string\n /** The site that sells it. */\n hostId: string\n}\n\nexport interface RecurringChargeSource {\n /** Every subscription this plugin sells on `hostIds` that can still bill. */\n listLiveSubscriptions(input: {\n hostIds: readonly string[]\n }): Promise<RecurringChargeRecord[]>\n}\n\nexport const PLUGIN_RECURRING_CHARGE_SOURCES =\n definePluginServiceContract<RecurringChargeSource>(\n 'core.recurring-charge-sources',\n { multiple: true },\n )\n\n/** Declares a plugin's recurring charges to the lockdown. */\nexport function registerRecurringChargeSource(\n source: RecurringChargeSource,\n options?: { pluginId?: string },\n): void {\n registerPluginService(PLUGIN_RECURRING_CHARGE_SOURCES, source, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** Every declared source, with the plugin that declared it. */\nexport function listRecurringChargeSources(): Array<{\n pluginId: string\n source: RecurringChargeSource\n}> {\n return resolvePluginServices(PLUGIN_RECURRING_CHARGE_SOURCES).map(\n (entry) => ({ pluginId: entry.pluginId, source: entry.impl }),\n )\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_RECURRING_CHARGE_SOURCES","multiple","registerRecurringChargeSource","source","options","pluginId","listRecurringChargeSources","map","entry","impl"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;2CAgB2C,GAE3C,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,qBAAqB,QAChB,uBAAmB;AAiB1B,OAAO,MAAMC,kCACXH,4BACE,iCACA;IAAEI,UAAU;AAAK,GAClB;AAEH,2DAA2D,GAC3D,OAAO,SAASC,8BACdC,MAA6B,EAC7BC,OAA+B;IAE/BN,sBAAsBE,iCAAiCG,QAAQ,aACzDC,CAAAA,2BAAAA,QAASC,QAAQ,IAAG;QAAEA,UAAUD,QAAQC,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,6DAA6D,GAC7D,OAAO,SAASC;IAId,OAAOP,sBAAsBC,iCAAiCO,GAAG,CAC/D,CAACC,QAAW,CAAA;YAAEH,UAAUG,MAAMH,QAAQ;YAAEF,QAAQK,MAAMC,IAAI;QAAC,CAAA;AAE/D"}
|