@mj-biz-apps/orders-server 0.0.1 → 5.2.0

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 (71) hide show
  1. package/LICENSE +183 -0
  2. package/README.md +47 -2
  3. package/dist/CheckoutServerExtension.d.ts +96 -0
  4. package/dist/CheckoutServerExtension.d.ts.map +1 -0
  5. package/dist/CheckoutServerExtension.js +554 -0
  6. package/dist/CheckoutServerExtension.js.map +1 -0
  7. package/dist/PaymentWebhookExtension.d.ts +17 -0
  8. package/dist/PaymentWebhookExtension.d.ts.map +1 -0
  9. package/dist/PaymentWebhookExtension.js +151 -0
  10. package/dist/PaymentWebhookExtension.js.map +1 -0
  11. package/dist/__tests__/CheckoutServerExtension.test.d.ts +2 -0
  12. package/dist/__tests__/CheckoutServerExtension.test.d.ts.map +1 -0
  13. package/dist/__tests__/CheckoutServerExtension.test.js +257 -0
  14. package/dist/__tests__/CheckoutServerExtension.test.js.map +1 -0
  15. package/dist/__tests__/checkout-edge-policy.test.d.ts +2 -0
  16. package/dist/__tests__/checkout-edge-policy.test.d.ts.map +1 -0
  17. package/dist/__tests__/checkout-edge-policy.test.js +70 -0
  18. package/dist/__tests__/checkout-edge-policy.test.js.map +1 -0
  19. package/dist/__tests__/checkout-host-page.test.d.ts +2 -0
  20. package/dist/__tests__/checkout-host-page.test.d.ts.map +1 -0
  21. package/dist/__tests__/checkout-host-page.test.js +86 -0
  22. package/dist/__tests__/checkout-host-page.test.js.map +1 -0
  23. package/dist/__tests__/server-extensions-manifest.test.d.ts +2 -0
  24. package/dist/__tests__/server-extensions-manifest.test.d.ts.map +1 -0
  25. package/dist/__tests__/server-extensions-manifest.test.js +29 -0
  26. package/dist/__tests__/server-extensions-manifest.test.js.map +1 -0
  27. package/dist/checkout-edge-policy.d.ts +44 -0
  28. package/dist/checkout-edge-policy.d.ts.map +1 -0
  29. package/dist/checkout-edge-policy.js +84 -0
  30. package/dist/checkout-edge-policy.js.map +1 -0
  31. package/dist/checkout-host-page.d.ts +43 -0
  32. package/dist/checkout-host-page.d.ts.map +1 -0
  33. package/dist/checkout-host-page.js +445 -0
  34. package/dist/checkout-host-page.js.map +1 -0
  35. package/dist/config.d.ts +6 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +6 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/custom/generate-invoice.action.d.ts +59 -0
  40. package/dist/custom/generate-invoice.action.d.ts.map +1 -0
  41. package/dist/custom/generate-invoice.action.js +208 -0
  42. package/dist/custom/generate-invoice.action.js.map +1 -0
  43. package/dist/custom/open-payment-intent.action.d.ts +39 -0
  44. package/dist/custom/open-payment-intent.action.d.ts.map +1 -0
  45. package/dist/custom/open-payment-intent.action.js +169 -0
  46. package/dist/custom/open-payment-intent.action.js.map +1 -0
  47. package/dist/custom/send-document.action.d.ts +78 -0
  48. package/dist/custom/send-document.action.d.ts.map +1 -0
  49. package/dist/custom/send-document.action.js +277 -0
  50. package/dist/custom/send-document.action.js.map +1 -0
  51. package/dist/generated/class-registrations-manifest.d.ts +7 -0
  52. package/dist/generated/class-registrations-manifest.d.ts.map +1 -0
  53. package/dist/generated/class-registrations-manifest.js +7 -0
  54. package/dist/generated/class-registrations-manifest.js.map +1 -0
  55. package/dist/generated/generated.d.ts +3536 -0
  56. package/dist/generated/generated.d.ts.map +1 -0
  57. package/dist/generated/generated.js +18371 -0
  58. package/dist/generated/generated.js.map +1 -0
  59. package/dist/index.d.ts +28 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +105 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/server-extensions-manifest.d.ts +14 -0
  64. package/dist/server-extensions-manifest.d.ts.map +1 -0
  65. package/dist/server-extensions-manifest.js +15 -0
  66. package/dist/server-extensions-manifest.js.map +1 -0
  67. package/dist/services/invoice-renderer.d.ts +80 -0
  68. package/dist/services/invoice-renderer.d.ts.map +1 -0
  69. package/dist/services/invoice-renderer.js +103 -0
  70. package/dist/services/invoice-renderer.js.map +1 -0
  71. package/package.json +65 -5
@@ -0,0 +1,151 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ /**
8
+ * The route that lets a gateway tell us money moved — finally mounted.
9
+ *
10
+ * `MountPaymentWebhook` has existed since the payment drivers landed and was called by nobody. That was
11
+ * survivable while every provider settled synchronously: a card capture books at the moment it is
12
+ * asked, so the webhook only ever confirmed something already recorded, and its absence cost a little
13
+ * reconciliation detail. It stops being survivable the instant a bank debit is configured. A debit is
14
+ * captured BY the webhook — there is no other moment at which the money is known to have arrived — so
15
+ * without this route every ACH payment sits `Pending` for ever and no cash is ever booked.
16
+ *
17
+ * WHY AN EXTENSION AND NOT A CHANGE TO MJ. MJServer loads `serverExtensions[]` from `mj.config.cjs`
18
+ * BEFORE it installs the auth middleware, which is precisely the window an unauthenticated webhook
19
+ * needs. So the route can be mounted from this repo, in this app's own config, with no upstream change
20
+ * — and an operator can turn it off by flipping `Enabled` rather than by redeploying.
21
+ *
22
+ * ═══ TWO THINGS THE MOUNT MUST GET RIGHT, both of them silent when wrong ═══
23
+ *
24
+ * BEFORE AUTH. Stripe presents no bearer token; the signature IS the credential. Behind the auth
25
+ * middleware every delivery would be rejected with a 401 that Stripe retries and eventually gives up
26
+ * on, and the only symptom is payments that never capture.
27
+ *
28
+ * RAW BODY, ON THIS PATH ONLY. The signature covers the exact bytes sent. Any JSON round-trip — key
29
+ * reordering, whitespace, unicode normalisation — changes them, so a `express.json()` upstream turns
30
+ * every legitimate webhook into a signature failure. `express.raw()` is scoped to this route so it
31
+ * cannot affect anything else the app serves.
32
+ *
33
+ * THE PROVIDER ID IS IN THE PATH, and one endpoint per configured provider is deliberate. It is what
34
+ * tells the handler WHICH secret to verify against before it has read a byte of the payload — a single
35
+ * shared endpoint would have to parse untrusted JSON to work out whose signature to check, which is
36
+ * the one thing `HandlePaymentWebhook` is ordered to avoid.
37
+ *
38
+ * THE SYSTEM USER, AND WHY IT IS RESOLVED PER REQUEST. There is no human behind a webhook, so the
39
+ * writes are attributed to MJ's system user — the same one MJ's own unauthenticated eSignature webhook
40
+ * uses, via the same `UserCache`, rather than a bespoke email setting this app would then have to keep
41
+ * correct. It is looked up on each delivery rather than cached at startup because the extension
42
+ * initialises before the data provider is necessarily ready, and a user captured too early would be
43
+ * absent for the lifetime of the process.
44
+ *
45
+ * CONNECTS TO:
46
+ * HANDLER: @mj-biz-apps/orders-core-entities-server → PaymentWebhookHandler
47
+ * CONFIG: mj.config.cjs → serverExtensions[]
48
+ * DOC: plans/archive/bizapps-orders-master.md D19
49
+ */
50
+ import BodyParser from 'body-parser';
51
+ import { LogError, LogStatus, Metadata } from '@memberjunction/core';
52
+ import { UserCache } from '@memberjunction/generic-database-provider';
53
+ import { RegisterClass } from '@memberjunction/global';
54
+ import { BaseServerExtension, } from '@memberjunction/server-extensions-core';
55
+ import { MountPaymentWebhook } from '@mj-biz-apps/orders-core-entities-server';
56
+ /** Stripe's largest documented event payload is well under this; the cap keeps the door narrow. */
57
+ const MAX_BODY = '1mb';
58
+ let PaymentWebhookExtension = class PaymentWebhookExtension extends BaseServerExtension {
59
+ async Initialize(app, config) {
60
+ const route = `${config.RootPath.replace(/\/+$/, '')}/:providerId`;
61
+ app.post(route,
62
+ // RAW, and scoped to this path. See the header — a JSON parser anywhere upstream of this
63
+ // invalidates every signature.
64
+ BodyParser.raw({ type: 'application/json', limit: MAX_BODY }), async (req, res) => {
65
+ const user = this.resolveSystemUser();
66
+ if (!user) {
67
+ // 500, NOT 400. The gateway did nothing wrong and the event is real — this is our
68
+ // configuration being incomplete, and a 4xx would tell Stripe to stop retrying a
69
+ // notification we very much want back once someone fixes it.
70
+ LogError('A payment webhook arrived but MJ\'s system user could not be resolved, so it ' +
71
+ 'cannot be recorded. The user cache is not populated — this is a startup ' +
72
+ 'problem, not a problem with the delivery.');
73
+ res.status(500).json({ received: false });
74
+ return;
75
+ }
76
+ const handler = MountPaymentWebhook(() => ({ provider: Metadata.Provider, user }));
77
+ await handler({
78
+ body: req.body,
79
+ headers: flattenHeaders(req.headers),
80
+ // Only the one parameter the handler reads, narrowed to a string here rather
81
+ // than passed through as Express's `string | string[]` dictionary.
82
+ params: { providerId: String(req.params?.providerId ?? '') },
83
+ }, res);
84
+ });
85
+ LogStatus(`[Orders] Payment webhook route registered at POST ${route}`);
86
+ return {
87
+ Success: true,
88
+ Message: 'Orders payment webhook mounted (unauthenticated, raw body, signature-verified).',
89
+ RegisteredRoutes: [`POST ${route}`],
90
+ };
91
+ }
92
+ async Shutdown() {
93
+ // Nothing held open — the route owns no connections, timers or sockets.
94
+ }
95
+ async HealthCheck() {
96
+ // Healthy means "this route can do its job", which is exactly "the system user resolves".
97
+ // Reporting healthy without checking would hide the one misconfiguration that silently drops
98
+ // real payment notifications.
99
+ const user = this.resolveSystemUser();
100
+ return user
101
+ ? { Healthy: true, Name: 'OrdersPaymentWebhook' }
102
+ : {
103
+ Healthy: false,
104
+ Name: 'OrdersPaymentWebhook',
105
+ Details: {
106
+ Reason: 'MJ\'s system user does not resolve, so every webhook delivery will be refused.',
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * The user the webhook's writes are attributed to.
112
+ *
113
+ * Returns undefined rather than throwing, so the caller decides the status code. Looked up fresh
114
+ * each time: the metadata provider may not have been ready when this extension initialised.
115
+ */
116
+ resolveSystemUser() {
117
+ try {
118
+ return UserCache.Instance.GetSystemUser() ?? undefined;
119
+ }
120
+ catch {
121
+ // The cache is not populated yet. Undefined rather than a throw, so the caller can answer
122
+ // 500 and the gateway retries once startup has finished.
123
+ return undefined;
124
+ }
125
+ }
126
+ };
127
+ PaymentWebhookExtension = __decorate([
128
+ RegisterClass(BaseServerExtension, 'OrdersPaymentWebhook')
129
+ ], PaymentWebhookExtension);
130
+ export { PaymentWebhookExtension };
131
+ /**
132
+ * Express types a header value as `string | string[]` because a header CAN legitimately repeat.
133
+ *
134
+ * The handler wants a flat map, and the flattening matters for exactly one header: `stripe-signature`.
135
+ * Joining a repeated value with a comma would be right by luck — Stripe's own scheme is already
136
+ * comma-separated (`t=…,v1=…,v1=…`) so a proxy that split it across two headers rejoins correctly —
137
+ * whereas taking the first would silently drop half a signature during a secret rotation and fail
138
+ * verification for events that are perfectly valid.
139
+ */
140
+ function flattenHeaders(headers) {
141
+ const flat = {};
142
+ for (const [key, value] of Object.entries(headers)) {
143
+ flat[key] = Array.isArray(value) ? value.join(',') : value;
144
+ }
145
+ return flat;
146
+ }
147
+ /** Tree-shaking anchor — call from the server bootstrap so @RegisterClass is retained. */
148
+ export function LoadPaymentWebhookExtension() {
149
+ // intentionally empty
150
+ }
151
+ //# sourceMappingURL=PaymentWebhookExtension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentWebhookExtension.js","sourceRoot":"","sources":["../src/PaymentWebhookExtension.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=CheckoutServerExtension.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CheckoutServerExtension.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/CheckoutServerExtension.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,257 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const mockRunView = vi.fn();
3
+ const mockGetSystemUser = vi.fn();
4
+ vi.mock('@memberjunction/core', () => ({
5
+ LogError: vi.fn(),
6
+ LogStatus: vi.fn(),
7
+ RunView: class {
8
+ constructor() {
9
+ this.RunView = mockRunView;
10
+ }
11
+ },
12
+ }));
13
+ vi.mock('@memberjunction/generic-database-provider', () => ({
14
+ UserCache: {
15
+ Instance: {
16
+ Users: [],
17
+ GetSystemUser: () => mockGetSystemUser(),
18
+ },
19
+ },
20
+ }));
21
+ vi.mock('@memberjunction/global', () => ({
22
+ RegisterClass: () => (cls) => cls,
23
+ }));
24
+ vi.mock('@mj-biz-apps/orders-core-entities-server', () => ({
25
+ CheckoutSessionService: {
26
+ InitializeSession: vi.fn(),
27
+ UpdateDraft: vi.fn(),
28
+ OpenPaymentIntentForSession: vi.fn(),
29
+ CompleteCheckout: vi.fn(),
30
+ ReapExpiredOpenSessions: vi.fn().mockResolvedValue(0),
31
+ },
32
+ EscapeText: (value) => value.replace(/'/g, "''"),
33
+ }));
34
+ import { CheckoutSessionService } from '@mj-biz-apps/orders-core-entities-server';
35
+ import { CheckoutServerExtension, shouldServeCheckoutElementSourceMap } from '../CheckoutServerExtension.js';
36
+ function mockApp() {
37
+ const routes = { get: {}, post: {}, options: {} };
38
+ const app = {
39
+ get: vi.fn((path, handler) => {
40
+ routes.get[path] = handler;
41
+ }),
42
+ post: vi.fn((path, ...handlers) => {
43
+ routes.post[path] = handlers;
44
+ }),
45
+ options: vi.fn((path, handler) => {
46
+ routes.options[path] = handler;
47
+ }),
48
+ };
49
+ return { app, routes };
50
+ }
51
+ function mockRes() {
52
+ const res = {
53
+ statusCode: 200,
54
+ body: '',
55
+ headers: {},
56
+ headersSent: false,
57
+ status(code) {
58
+ this.statusCode = code;
59
+ return this;
60
+ },
61
+ setHeader(name, value) {
62
+ this.headers[name.toLowerCase()] = value;
63
+ return this;
64
+ },
65
+ send(body) {
66
+ this.body = body;
67
+ this.headersSent = true;
68
+ return this;
69
+ },
70
+ json(body) {
71
+ this.body = JSON.stringify(body);
72
+ this.headersSent = true;
73
+ return this;
74
+ },
75
+ end() {
76
+ this.headersSent = true;
77
+ return this;
78
+ },
79
+ };
80
+ return res;
81
+ }
82
+ describe('shouldServeCheckoutElementSourceMap', () => {
83
+ it('defaults off — the public payment route must not publish TypeScript', () => {
84
+ expect(shouldServeCheckoutElementSourceMap({}, {})).toBe(false);
85
+ expect(shouldServeCheckoutElementSourceMap({}, { NODE_ENV: 'development' })).toBe(false);
86
+ expect(shouldServeCheckoutElementSourceMap({}, { NODE_ENV: 'production' })).toBe(false);
87
+ });
88
+ it('opts in via Settings or CHECKOUT_ELEMENT_SOURCEMAP=1', () => {
89
+ expect(shouldServeCheckoutElementSourceMap({ ServeElementSourceMap: true }, {})).toBe(true);
90
+ expect(shouldServeCheckoutElementSourceMap({}, { CHECKOUT_ELEMENT_SOURCEMAP: '1' })).toBe(true);
91
+ expect(shouldServeCheckoutElementSourceMap({ ServeElementSourceMap: false }, { CHECKOUT_ELEMENT_SOURCEMAP: '1' })).toBe(false);
92
+ });
93
+ });
94
+ describe('CheckoutServerExtension', () => {
95
+ beforeEach(() => {
96
+ vi.clearAllMocks();
97
+ mockGetSystemUser.mockReturnValue({ ID: 'sys', Email: 'system@local' });
98
+ mockRunView.mockResolvedValue({ Success: true, Results: [{ ID: 'dist-1', CheckoutWidgetID: 'widget-1' }] });
99
+ });
100
+ it('registers POST verbs plus GET /:slug and reports both in RegisteredRoutes', async () => {
101
+ const { app, routes } = mockApp();
102
+ const ext = new CheckoutServerExtension();
103
+ const result = await ext.Initialize(app, {
104
+ Enabled: true,
105
+ DriverClass: 'OrdersCheckoutEdge',
106
+ RootPath: '/checkout',
107
+ Settings: {},
108
+ });
109
+ expect(result.Success).toBe(true);
110
+ expect(Object.keys(routes.post).sort()).toEqual([
111
+ '/checkout/complete',
112
+ '/checkout/draft',
113
+ '/checkout/initialize',
114
+ '/checkout/payment-intent',
115
+ ]);
116
+ expect(Object.keys(routes.get).sort()).toEqual(['/checkout/:slug', '/checkout/element/main.js'].sort());
117
+ expect(routes.get['/checkout/element/main.js.map']).toBeUndefined();
118
+ expect(result.RegisteredRoutes).toEqual([
119
+ 'POST /checkout/initialize',
120
+ 'POST /checkout/draft',
121
+ 'POST /checkout/payment-intent',
122
+ 'POST /checkout/complete',
123
+ 'GET /checkout/:slug',
124
+ ]);
125
+ });
126
+ it('GET 404s reserved slugs without looking up a distribution', async () => {
127
+ const { app, routes } = mockApp();
128
+ await new CheckoutServerExtension().Initialize(app, {
129
+ Enabled: true,
130
+ DriverClass: 'OrdersCheckoutEdge',
131
+ RootPath: '/checkout',
132
+ Settings: {},
133
+ });
134
+ const res = mockRes();
135
+ await routes.get['/checkout/:slug']({ params: { slug: 'initialize' }, headers: {}, socket: {} }, res);
136
+ expect(res.statusCode).toBe(404);
137
+ expect(res.body).toContain('not valid');
138
+ expect(mockRunView).not.toHaveBeenCalled();
139
+ });
140
+ it('GET 404s an unknown slug', async () => {
141
+ mockRunView.mockResolvedValueOnce({ Success: true, Results: [] });
142
+ const { app, routes } = mockApp();
143
+ await new CheckoutServerExtension().Initialize(app, {
144
+ Enabled: true,
145
+ DriverClass: 'OrdersCheckoutEdge',
146
+ RootPath: '/checkout',
147
+ Settings: {},
148
+ });
149
+ const res = mockRes();
150
+ await routes.get['/checkout/:slug']({ params: { slug: 'missing-event' }, headers: {}, socket: { remoteAddress: '127.0.0.1' } }, res);
151
+ expect(res.statusCode).toBe(404);
152
+ expect(res.body).toContain('not available');
153
+ expect(mockRunView).toHaveBeenCalled();
154
+ });
155
+ it('GET 503s when no acting user resolves', async () => {
156
+ mockGetSystemUser.mockReturnValue(undefined);
157
+ const { app, routes } = mockApp();
158
+ await new CheckoutServerExtension().Initialize(app, {
159
+ Enabled: true,
160
+ DriverClass: 'OrdersCheckoutEdge',
161
+ RootPath: '/checkout',
162
+ Settings: {},
163
+ });
164
+ const res = mockRes();
165
+ await routes.get['/checkout/:slug']({ params: { slug: 'summit-2027' }, headers: {}, socket: { remoteAddress: '127.0.0.1' } }, res);
166
+ expect(res.statusCode).toBe(503);
167
+ expect(mockRunView).not.toHaveBeenCalled();
168
+ });
169
+ it('GET 200s an Active distribution with escaped slug and no-store HTML', async () => {
170
+ const { app, routes } = mockApp();
171
+ await new CheckoutServerExtension().Initialize(app, {
172
+ Enabled: true,
173
+ DriverClass: 'OrdersCheckoutEdge',
174
+ RootPath: '/checkout/',
175
+ Settings: {},
176
+ });
177
+ const res = mockRes();
178
+ await routes.get['/checkout/:slug']({ params: { slug: 'summit-2027' }, headers: {}, socket: { remoteAddress: '127.0.0.1' } }, res);
179
+ expect(res.statusCode).toBe(200);
180
+ expect(res.headers['content-type']).toBe('text/html; charset=utf-8');
181
+ expect(res.headers['cache-control']).toBe('no-store');
182
+ expect(res.headers['x-frame-options']).toBe('DENY');
183
+ expect(res.headers['referrer-policy']).toBe('no-referrer');
184
+ expect(res.headers['content-security-policy']).toContain("frame-ancestors 'none'");
185
+ expect(res.headers['content-security-policy']).toContain("default-src 'none'");
186
+ expect(res.body).toContain('slug="summit-2027"');
187
+ expect(res.body).toContain('api-root="/checkout"');
188
+ expect(res.body).toContain('src="/checkout/element/main.js"');
189
+ const filter = mockRunView.mock.calls[0][0].ExtraFilter;
190
+ expect(filter).toContain("Slug = 'summit-2027'");
191
+ expect(filter).toContain("Status = 'Active'");
192
+ expect(CheckoutSessionService.ReapExpiredOpenSessions).toHaveBeenCalledTimes(1);
193
+ });
194
+ it('does not re-reap within the interval, then reaps again after it elapses', async () => {
195
+ vi.useFakeTimers();
196
+ try {
197
+ vi.setSystemTime(new Date('2026-08-26T12:00:00Z'));
198
+ const { app, routes } = mockApp();
199
+ await new CheckoutServerExtension().Initialize(app, {
200
+ Enabled: true,
201
+ DriverClass: 'OrdersCheckoutEdge',
202
+ RootPath: '/checkout',
203
+ Settings: {},
204
+ });
205
+ const req = { params: { slug: 'summit-2027' }, headers: {}, socket: { remoteAddress: '127.0.0.1' } };
206
+ await routes.get['/checkout/:slug'](req, mockRes());
207
+ await routes.get['/checkout/:slug'](req, mockRes());
208
+ expect(CheckoutSessionService.ReapExpiredOpenSessions).toHaveBeenCalledTimes(1);
209
+ vi.setSystemTime(new Date('2026-08-26T12:01:01Z'));
210
+ await routes.get['/checkout/:slug'](req, mockRes());
211
+ expect(CheckoutSessionService.ReapExpiredOpenSessions).toHaveBeenCalledTimes(2);
212
+ }
213
+ finally {
214
+ vi.useRealTimers();
215
+ }
216
+ });
217
+ it('escapes a hostile slug in the GET ExtraFilter', async () => {
218
+ const { app, routes } = mockApp();
219
+ await new CheckoutServerExtension().Initialize(app, {
220
+ Enabled: true,
221
+ DriverClass: 'OrdersCheckoutEdge',
222
+ RootPath: '/checkout',
223
+ Settings: {},
224
+ });
225
+ const res = mockRes();
226
+ // Passes the URL-safe pattern but still contains a quote if we ever loosen it —
227
+ // use a valid slug here and assert EscapeText is applied via the apostrophe case
228
+ // by calling with a slug that includes underscore/dot only; SQL quoting is
229
+ // covered by EscapeText unit tests. This asserts the filter is parameterized
230
+ // through EscapeText for the accepted slug.
231
+ await routes.get['/checkout/:slug']({ params: { slug: 'summit_2027' }, headers: {}, socket: { remoteAddress: '127.0.0.1' } }, res);
232
+ expect(mockRunView.mock.calls[0][0].ExtraFilter).toContain("Slug = 'summit_2027'");
233
+ });
234
+ it('does not key rate limits on a spoofed leftmost X-Forwarded-For (default TrustedProxyHops=0)', async () => {
235
+ const { app, routes } = mockApp();
236
+ const ext = new CheckoutServerExtension();
237
+ await ext.Initialize(app, {
238
+ Enabled: true,
239
+ DriverClass: 'OrdersCheckoutEdge',
240
+ RootPath: '/checkout',
241
+ Settings: { RateLimitMax: 2, RateLimitMaxGlobal: 2, RateLimitWindowMs: 60_000 },
242
+ });
243
+ const hit = async (xff) => {
244
+ const res = mockRes();
245
+ await routes.get['/checkout/:slug']({
246
+ params: { slug: 'summit-2027' },
247
+ headers: { 'x-forwarded-for': xff },
248
+ socket: { remoteAddress: '10.0.0.9' },
249
+ }, res);
250
+ return res.statusCode;
251
+ };
252
+ expect(await hit('1.1.1.1')).toBe(200);
253
+ expect(await hit('2.2.2.2')).toBe(200);
254
+ expect(await hit('3.3.3.3')).toBe(429);
255
+ });
256
+ });
257
+ //# sourceMappingURL=CheckoutServerExtension.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CheckoutServerExtension.test.js","sourceRoot":"","sources":["../../src/__tests__/CheckoutServerExtension.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=checkout-edge-policy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout-edge-policy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/checkout-edge-policy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { CHECKOUT_RESERVED_SLUGS, isSameOrigin, isValidCheckoutSlug, originAllowed, resolveClientIp, } from '../checkout-edge-policy.js';
3
+ describe('isValidCheckoutSlug', () => {
4
+ it('accepts typical vanity slugs', () => {
5
+ expect(isValidCheckoutSlug('summit-2027')).toBe(true);
6
+ expect(isValidCheckoutSlug('A')).toBe(true);
7
+ expect(isValidCheckoutSlug('event_1.2')).toBe(true);
8
+ });
9
+ it('rejects empty, too long, unsafe, and reserved POST verbs', () => {
10
+ expect(isValidCheckoutSlug('')).toBe(false);
11
+ expect(isValidCheckoutSlug('initialize')).toBe(false);
12
+ expect(isValidCheckoutSlug('Draft')).toBe(false);
13
+ expect(isValidCheckoutSlug('payment-intent')).toBe(false);
14
+ expect(isValidCheckoutSlug('complete')).toBe(false);
15
+ expect(isValidCheckoutSlug('../etc')).toBe(false);
16
+ expect(isValidCheckoutSlug('has space')).toBe(false);
17
+ expect(isValidCheckoutSlug('<script>')).toBe(false);
18
+ expect(isValidCheckoutSlug('a'.repeat(129))).toBe(false);
19
+ });
20
+ it('reserved set covers every POST suffix on the edge', () => {
21
+ expect([...CHECKOUT_RESERVED_SLUGS].sort()).toEqual(['complete', 'draft', 'initialize', 'payment-intent'].sort());
22
+ });
23
+ });
24
+ describe('isSameOrigin', () => {
25
+ it('matches scheme-less Host against Origin host:port', () => {
26
+ expect(isSameOrigin('http://localhost:4103', 'localhost:4103')).toBe(true);
27
+ expect(isSameOrigin('https://api.example.com', 'api.example.com')).toBe(true);
28
+ });
29
+ it('rejects missing values, bad URLs, and different hosts', () => {
30
+ expect(isSameOrigin('', 'localhost:4103')).toBe(false);
31
+ expect(isSameOrigin('http://localhost:4103', undefined)).toBe(false);
32
+ expect(isSameOrigin('not-a-url', 'localhost:4103')).toBe(false);
33
+ expect(isSameOrigin('http://localhost:4103', 'localhost:4303')).toBe(false);
34
+ expect(isSameOrigin('https://evil.example', 'api.example.com')).toBe(false);
35
+ });
36
+ });
37
+ describe('originAllowed', () => {
38
+ it('allows same-origin even when the widget allowlist names another site', () => {
39
+ expect(originAllowed('http://localhost:4103', { allowedOrigins: ['https://events.example.com'] }, 'localhost:4103')).toBe(true);
40
+ });
41
+ it('allows any origin when the allowlist is absent or empty', () => {
42
+ expect(originAllowed('https://anywhere.example', {}, 'api.example.com')).toBe(true);
43
+ expect(originAllowed('https://anywhere.example', { allowedOrigins: [] }, 'api.example.com')).toBe(true);
44
+ });
45
+ it('enforces the allowlist for cross-origin embeds (trailing slash insensitive)', () => {
46
+ const policy = { allowedOrigins: ['https://events.example.com/'] };
47
+ expect(originAllowed('https://events.example.com', policy, 'api.example.com')).toBe(true);
48
+ expect(originAllowed('https://other.example.com', policy, 'api.example.com')).toBe(false);
49
+ });
50
+ });
51
+ describe('resolveClientIp', () => {
52
+ const req = (xff, socket = '10.0.0.9') => ({
53
+ headers: { 'x-forwarded-for': xff },
54
+ socket: { remoteAddress: socket },
55
+ });
56
+ it('ignores X-Forwarded-For when no proxy hops are declared (default)', () => {
57
+ expect(resolveClientIp(req('1.2.3.4, 10.0.0.1'))).toBe('10.0.0.9');
58
+ expect(resolveClientIp(req('1.2.3.4'))).toBe('10.0.0.9');
59
+ });
60
+ it('takes the Nth-from-right hop when TrustedProxyHops is set', () => {
61
+ // client, proxy1, proxy2 — with 1 trusted proxy the last hop is what that proxy saw
62
+ expect(resolveClientIp(req('9.9.9.9, 203.0.113.10'), 1)).toBe('203.0.113.10');
63
+ expect(resolveClientIp(req('9.9.9.9, 198.51.100.2, 203.0.113.10'), 2)).toBe('198.51.100.2');
64
+ });
65
+ it('falls back to the socket when XFF is missing or too short for the declared hop count', () => {
66
+ expect(resolveClientIp(req(undefined), 1)).toBe('10.0.0.9');
67
+ expect(resolveClientIp(req('only-one'), 2)).toBe('10.0.0.9');
68
+ });
69
+ });
70
+ //# sourceMappingURL=checkout-edge-policy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout-edge-policy.test.js","sourceRoot":"","sources":["../../src/__tests__/checkout-edge-policy.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=checkout-host-page.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout-host-page.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/checkout-host-page.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { checkoutHostSecurityHeaders, escapeAttr, escapeHtml, renderCheckoutHostErrorPage, renderCheckoutHostPage, } from '../checkout-host-page.js';
3
+ describe('escapeHtml / escapeAttr', () => {
4
+ it('escapes markup and quotes', () => {
5
+ expect(escapeHtml('<script>alert(1)</script>')).toBe('&lt;script&gt;alert(1)&lt;/script&gt;');
6
+ expect(escapeAttr('x" onclick="alert(1)')).toBe('x&quot; onclick=&quot;alert(1)');
7
+ expect(escapeAttr('a&b<c>')).toBe('a&amp;b&lt;c&gt;');
8
+ });
9
+ });
10
+ describe('renderCheckoutHostPage', () => {
11
+ it('hosts the Angular Element when elementSrc is provided', () => {
12
+ const html = renderCheckoutHostPage({
13
+ slug: 'summit-2027',
14
+ apiRoot: '/checkout',
15
+ elementSrc: '/checkout/element/main.js',
16
+ cspNonce: 'nOnce+/1',
17
+ });
18
+ expect(html).toContain('slug="summit-2027"');
19
+ expect(html).toContain('api-root="/checkout"');
20
+ expect(html).toContain('csp-nonce="nOnce+/1"');
21
+ expect(html).toContain('src="/checkout/element/main.js"');
22
+ expect(html).toContain('type="module"');
23
+ expect(html).toContain('nonce="nOnce+/1"');
24
+ expect(html).not.toContain("post('/initialize'");
25
+ });
26
+ it('bakes slug and api root into data-* attributes, never into the script block', () => {
27
+ const evil = 'summit"</script><script>alert(1)</script>';
28
+ const html = renderCheckoutHostPage({ slug: evil, apiRoot: '/checkout', cspNonce: 'nOnce+/1' });
29
+ expect(html).toContain('data-slug="summit&quot;&lt;/script&gt;&lt;script&gt;alert(1)&lt;/script&gt;"');
30
+ expect(html).toContain('data-api-root="/checkout"');
31
+ const scriptOpen = html.indexOf('<script');
32
+ const scriptTagEnd = html.indexOf('>', scriptOpen);
33
+ const script = html.slice(scriptTagEnd + 1, html.indexOf('</script>'));
34
+ expect(script).not.toContain(evil);
35
+ expect(script).not.toContain('summit"');
36
+ expect(script).toContain("host.getAttribute('data-slug')");
37
+ expect(script).toContain("host.getAttribute('data-api-root')");
38
+ });
39
+ it('drives only the existing POST verbs (no amount/price/provider in the request shape)', () => {
40
+ const html = renderCheckoutHostPage({ slug: 'summit-2027', apiRoot: '/checkout', cspNonce: 'nOnce+/1' });
41
+ expect(html).toContain("post('/initialize'");
42
+ expect(html).toContain("post('/draft'");
43
+ expect(html).toContain("post('/payment-intent'");
44
+ expect(html).toContain("post('/complete'");
45
+ expect(html).toContain('if (stripe) { resolve(stripe); return; }');
46
+ expect(html).toContain('crypto.getRandomValues');
47
+ expect(html).not.toContain('Math.random');
48
+ expect(html).not.toContain('amount:');
49
+ expect(html).not.toContain('UnitPrice');
50
+ expect(html).not.toContain('paymentProviderId');
51
+ });
52
+ it('is a complete HTML document with noindex and utf-8', () => {
53
+ const html = renderCheckoutHostPage({ slug: 'summit-2027', apiRoot: '/checkout', pageTitle: 'Buy <tickets>', cspNonce: 'nOnce+/1' });
54
+ expect(html.startsWith('<!doctype html>')).toBe(true);
55
+ expect(html).toContain('<title>Buy &lt;tickets&gt;</title>');
56
+ expect(html).toContain('name="robots" content="noindex"');
57
+ expect(html).toContain('charset="utf-8"');
58
+ });
59
+ });
60
+ describe('renderCheckoutHostErrorPage', () => {
61
+ it('escapes the operator-facing message and has no boot script', () => {
62
+ const html = renderCheckoutHostErrorPage({ message: 'Not <found> & gone', cspNonce: 'nOnce+/1' });
63
+ expect(html).toContain('Not &lt;found&gt; &amp; gone');
64
+ expect(html).not.toContain('<script>');
65
+ expect(html).toContain('role="alert"');
66
+ });
67
+ it('stamps a CSP nonce onto style and script when provided', () => {
68
+ const html = renderCheckoutHostPage({ slug: 's', apiRoot: '/checkout', cspNonce: 'nOnce+/1' });
69
+ expect(html).toContain('nonce="nOnce+/1"');
70
+ expect(html).toMatch(/<style nonce="nOnce\+\/1">/);
71
+ expect(html).toMatch(/<script nonce="nOnce\+\/1">/);
72
+ });
73
+ });
74
+ describe('checkoutHostSecurityHeaders', () => {
75
+ it('denies framing and scopes scripts to the nonce plus Stripe', () => {
76
+ const h = checkoutHostSecurityHeaders('abc123');
77
+ expect(h['X-Frame-Options']).toBe('DENY');
78
+ expect(h['Referrer-Policy']).toBe('no-referrer');
79
+ expect(h['Content-Security-Policy']).toContain("frame-ancestors 'none'");
80
+ expect(h['Content-Security-Policy']).toContain("'nonce-abc123'");
81
+ expect(h['Content-Security-Policy']).toContain("'self'");
82
+ expect(h['Content-Security-Policy']).toContain('https://js.stripe.com');
83
+ expect(h['Content-Security-Policy']).toContain("default-src 'none'");
84
+ });
85
+ });
86
+ //# sourceMappingURL=checkout-host-page.test.js.map