@coursebuilder/server 0.1.0-canary.0.0c9beb2ae

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actions/checkout.d.ts +6 -0
  3. package/dist/actions/checkout.js +9 -0
  4. package/dist/actions/checkout.js.map +1 -0
  5. package/dist/actions/claimed.d.ts +6 -0
  6. package/dist/actions/claimed.js +8 -0
  7. package/dist/actions/claimed.js.map +1 -0
  8. package/dist/actions/create-magic-link.d.ts +6 -0
  9. package/dist/actions/create-magic-link.js +9 -0
  10. package/dist/actions/create-magic-link.js.map +1 -0
  11. package/dist/actions/index.d.ts +11 -0
  12. package/dist/actions/index.js +18 -0
  13. package/dist/actions/index.js.map +1 -0
  14. package/dist/actions/prices-coupon-for-code.d.ts +6 -0
  15. package/dist/actions/prices-coupon-for-code.js +63 -0
  16. package/dist/actions/prices-coupon-for-code.js.map +1 -0
  17. package/dist/actions/prices-formatted.d.ts +6 -0
  18. package/dist/actions/prices-formatted.js +8 -0
  19. package/dist/actions/prices-formatted.js.map +1 -0
  20. package/dist/actions/process-refund.d.ts +1 -0
  21. package/dist/actions/process-refund.js +8 -0
  22. package/dist/actions/process-refund.js.map +1 -0
  23. package/dist/actions/redeem.d.ts +1 -0
  24. package/dist/actions/redeem.js +8 -0
  25. package/dist/actions/redeem.js.map +1 -0
  26. package/dist/actions/transfer-purchase.d.ts +1 -0
  27. package/dist/actions/transfer-purchase.js +8 -0
  28. package/dist/actions/transfer-purchase.js.map +1 -0
  29. package/dist/actions/user-lookup.d.ts +6 -0
  30. package/dist/actions/user-lookup.js +9 -0
  31. package/dist/actions/user-lookup.js.map +1 -0
  32. package/dist/adapter-BtDS6LDw.d.ts +9 -0
  33. package/dist/chunk-22AMOZYZ.js +67 -0
  34. package/dist/chunk-22AMOZYZ.js.map +1 -0
  35. package/dist/chunk-4EQQSW57.js +32 -0
  36. package/dist/chunk-4EQQSW57.js.map +1 -0
  37. package/dist/chunk-4LZZLN6T.js +52 -0
  38. package/dist/chunk-4LZZLN6T.js.map +1 -0
  39. package/dist/chunk-4QQA3TC2.js +29 -0
  40. package/dist/chunk-4QQA3TC2.js.map +1 -0
  41. package/dist/chunk-4TSEPUSC.js +160 -0
  42. package/dist/chunk-4TSEPUSC.js.map +1 -0
  43. package/dist/chunk-5O4UO4IR.js +93 -0
  44. package/dist/chunk-5O4UO4IR.js.map +1 -0
  45. package/dist/chunk-7FCQOLKL.js +26 -0
  46. package/dist/chunk-7FCQOLKL.js.map +1 -0
  47. package/dist/chunk-AZLUMIIF.js +8 -0
  48. package/dist/chunk-AZLUMIIF.js.map +1 -0
  49. package/dist/chunk-E6HV6TNI.js +38 -0
  50. package/dist/chunk-E6HV6TNI.js.map +1 -0
  51. package/dist/chunk-ECNJPNPU.js +54 -0
  52. package/dist/chunk-ECNJPNPU.js.map +1 -0
  53. package/dist/chunk-F3KUYN5I.js +111 -0
  54. package/dist/chunk-F3KUYN5I.js.map +1 -0
  55. package/dist/chunk-G63YAJF2.js +107 -0
  56. package/dist/chunk-G63YAJF2.js.map +1 -0
  57. package/dist/chunk-JFR7XO72.js +7 -0
  58. package/dist/chunk-JFR7XO72.js.map +1 -0
  59. package/dist/chunk-KNC7QVGZ.js +26 -0
  60. package/dist/chunk-KNC7QVGZ.js.map +1 -0
  61. package/dist/chunk-ODSSRMD2.js +270 -0
  62. package/dist/chunk-ODSSRMD2.js.map +1 -0
  63. package/dist/chunk-VEKYIR7I.js +26 -0
  64. package/dist/chunk-VEKYIR7I.js.map +1 -0
  65. package/dist/chunk-W7YAT4NR.js +12 -0
  66. package/dist/chunk-W7YAT4NR.js.map +1 -0
  67. package/dist/chunk-WGAPYIUP.js +56 -0
  68. package/dist/chunk-WGAPYIUP.js.map +1 -0
  69. package/dist/chunk-WYNH2AWD.js +7 -0
  70. package/dist/chunk-WYNH2AWD.js.map +1 -0
  71. package/dist/chunk-Y5X5J55Z.js +384 -0
  72. package/dist/chunk-Y5X5J55Z.js.map +1 -0
  73. package/dist/chunk-YFLGJIMA.js +216 -0
  74. package/dist/chunk-YFLGJIMA.js.map +1 -0
  75. package/dist/chunk-ZFZNN4E5.js +7 -0
  76. package/dist/chunk-ZFZNN4E5.js.map +1 -0
  77. package/dist/create-inngest-middleware.d.ts +635 -0
  78. package/dist/create-inngest-middleware.js +12 -0
  79. package/dist/create-inngest-middleware.js.map +1 -0
  80. package/dist/http-router.d.ts +20 -0
  81. package/dist/http-router.js +24 -0
  82. package/dist/http-router.js.map +1 -0
  83. package/dist/http.d.ts +38 -0
  84. package/dist/http.js +32 -0
  85. package/dist/http.js.map +1 -0
  86. package/dist/index.d.ts +34 -0
  87. package/dist/index.js +365 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/magic-string.es-ENO43NON.js +1389 -0
  90. package/dist/magic-string.es-ENO43NON.js.map +1 -0
  91. package/dist/runtime/actions.d.ts +5 -0
  92. package/dist/runtime/actions.js +8 -0
  93. package/dist/runtime/actions.js.map +1 -0
  94. package/dist/runtime/assert.d.ts +15 -0
  95. package/dist/runtime/assert.js +8 -0
  96. package/dist/runtime/assert.js.map +1 -0
  97. package/dist/runtime/cookie.d.ts +8 -0
  98. package/dist/runtime/cookie.js +8 -0
  99. package/dist/runtime/cookie.js.map +1 -0
  100. package/dist/runtime/env.d.ts +9 -0
  101. package/dist/runtime/env.js +11 -0
  102. package/dist/runtime/env.js.map +1 -0
  103. package/dist/runtime/env.test.d.ts +2 -0
  104. package/dist/runtime/env.test.js +14956 -0
  105. package/dist/runtime/env.test.js.map +1 -0
  106. package/dist/runtime/init.d.ts +26 -0
  107. package/dist/runtime/init.js +14 -0
  108. package/dist/runtime/init.js.map +1 -0
  109. package/dist/runtime/logger.d.ts +4 -0
  110. package/dist/runtime/logger.js +10 -0
  111. package/dist/runtime/logger.js.map +1 -0
  112. package/dist/runtime/merge.d.ts +4 -0
  113. package/dist/runtime/merge.js +8 -0
  114. package/dist/runtime/merge.js.map +1 -0
  115. package/dist/runtime/providers.d.ts +21 -0
  116. package/dist/runtime/providers.js +9 -0
  117. package/dist/runtime/providers.js.map +1 -0
  118. package/dist/runtime/web.d.ts +29 -0
  119. package/dist/runtime/web.js +16 -0
  120. package/dist/runtime/web.js.map +1 -0
  121. package/package.json +65 -0
  122. package/src/actions/actions.test.ts +232 -0
  123. package/src/actions/checkout.ts +160 -0
  124. package/src/actions/claimed.ts +40 -0
  125. package/src/actions/create-magic-link.ts +78 -0
  126. package/src/actions/index.ts +401 -0
  127. package/src/actions/prices-coupon-for-code.ts +91 -0
  128. package/src/actions/prices-formatted.ts +313 -0
  129. package/src/actions/process-refund.ts +2 -0
  130. package/src/actions/redeem.ts +2 -0
  131. package/src/actions/transfer-purchase.ts +2 -0
  132. package/src/actions/user-lookup.ts +37 -0
  133. package/src/adapter.ts +28 -0
  134. package/src/create-inngest-middleware.ts +157 -0
  135. package/src/http-actions.ts +29 -0
  136. package/src/http-router.ts +79 -0
  137. package/src/http.test.ts +46 -0
  138. package/src/http.ts +83 -0
  139. package/src/index.test.ts +21 -0
  140. package/src/index.ts +44 -0
  141. package/src/mock-adapter.ts +398 -0
  142. package/src/mock-payment-provider.ts +56 -0
  143. package/src/resource-chat.ts +371 -0
  144. package/src/runtime/actions.ts +25 -0
  145. package/src/runtime/assert.ts +55 -0
  146. package/src/runtime/cookie.ts +12 -0
  147. package/src/runtime/env.test.ts +49 -0
  148. package/src/runtime/env.ts +63 -0
  149. package/src/runtime/init.ts +112 -0
  150. package/src/runtime/logger.ts +6 -0
  151. package/src/runtime/merge.ts +25 -0
  152. package/src/runtime/providers.ts +38 -0
  153. package/src/runtime/web.ts +199 -0
  154. package/src/streaming-chat-prompt-executor.ts +43 -0
  155. package/src/type-contracts.test.ts +167 -0
  156. package/src/web-body-parse.test.ts +55 -0
@@ -0,0 +1,26 @@
1
+ import { CallbacksOptions, InternalOptions, RequestInternal } from '@coursebuilder/core/types';
2
+ import { CourseBuilderConfig } from '../http.js';
3
+ import { Cookie } from './cookie.js';
4
+ import 'inngest';
5
+ import '../adapter-BtDS6LDw.js';
6
+ import '@coursebuilder/core/auth';
7
+ import '@coursebuilder/core/ports';
8
+ import '@coursebuilder/core/providers';
9
+ import '@coursebuilder/core/schemas';
10
+ import '@coursebuilder/utils/logger';
11
+
12
+ interface InitParams {
13
+ url: URL;
14
+ courseBuilderOptions: CourseBuilderConfig;
15
+ providerId?: string;
16
+ action: InternalOptions['action'];
17
+ isPost: boolean;
18
+ cookies: RequestInternal['cookies'];
19
+ }
20
+ declare function init({ url, courseBuilderOptions, providerId, action, isPost, cookies: reqCookies, }: InitParams): Promise<{
21
+ options: InternalOptions;
22
+ cookies: Cookie[];
23
+ }>;
24
+ declare const defaultCallbacks: CallbacksOptions;
25
+
26
+ export { defaultCallbacks, init };
@@ -0,0 +1,14 @@
1
+ import {
2
+ defaultCallbacks,
3
+ init
4
+ } from "../chunk-5O4UO4IR.js";
5
+ import "../chunk-W7YAT4NR.js";
6
+ import "../chunk-4QQA3TC2.js";
7
+ import "../chunk-KNC7QVGZ.js";
8
+ import "../chunk-AZLUMIIF.js";
9
+ import "../chunk-WGAPYIUP.js";
10
+ export {
11
+ defaultCallbacks,
12
+ init
13
+ };
14
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ export { LogAttributes, LoggerInstance, WarningCode, logger, setLogger } from '@coursebuilder/utils/logger';
2
+ import '@coursebuilder/commerce/process-refund';
3
+ import '@coursebuilder/commerce/redeem';
4
+ import '@coursebuilder/commerce/transfer-purchase';
@@ -0,0 +1,10 @@
1
+ import {
2
+ logger,
3
+ setLogger
4
+ } from "../chunk-AZLUMIIF.js";
5
+ import "../chunk-WGAPYIUP.js";
6
+ export {
7
+ logger,
8
+ setLogger
9
+ };
10
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,4 @@
1
+ /** Deep merge two objects */
2
+ declare function merge(target: any, ...sources: any[]): any;
3
+
4
+ export { merge };
@@ -0,0 +1,8 @@
1
+ import {
2
+ merge
3
+ } from "../chunk-KNC7QVGZ.js";
4
+ import "../chunk-WGAPYIUP.js";
5
+ export {
6
+ merge
7
+ };
8
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,21 @@
1
+ import { Provider } from '@coursebuilder/core/providers';
2
+ import { InternalProvider } from '@coursebuilder/core/types';
3
+ import { CourseBuilderConfig } from '../http.js';
4
+ import 'inngest';
5
+ import '../adapter-BtDS6LDw.js';
6
+ import '@coursebuilder/core/auth';
7
+ import '@coursebuilder/core/ports';
8
+ import '@coursebuilder/core/schemas';
9
+ import '@coursebuilder/utils/logger';
10
+
11
+ declare function parseProviders(params: {
12
+ providers: Provider[];
13
+ url: URL;
14
+ providerId?: string;
15
+ options: CourseBuilderConfig;
16
+ }): {
17
+ providers: InternalProvider[];
18
+ provider?: InternalProvider;
19
+ };
20
+
21
+ export { parseProviders as default };
@@ -0,0 +1,9 @@
1
+ import {
2
+ parseProviders
3
+ } from "../chunk-4QQA3TC2.js";
4
+ import "../chunk-KNC7QVGZ.js";
5
+ import "../chunk-WGAPYIUP.js";
6
+ export {
7
+ parseProviders as default
8
+ };
9
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,29 @@
1
+ import { CourseBuilderError } from '@coursebuilder/core/errors';
2
+ import { CourseBuilderAction, RequestInternal, ResponseInternal } from '@coursebuilder/core/types';
3
+ import { CourseBuilderConfig } from '../http.js';
4
+ import 'inngest';
5
+ import '../adapter-BtDS6LDw.js';
6
+ import '@coursebuilder/core/auth';
7
+ import '@coursebuilder/core/ports';
8
+ import '@coursebuilder/core/providers';
9
+ import '@coursebuilder/core/schemas';
10
+ import '@coursebuilder/utils/logger';
11
+
12
+ /**
13
+ * takes a `Request` object and converts it into an internal request object
14
+ * that we use throughout the system
15
+ * @param req
16
+ * @param config
17
+ * @returns
18
+ */
19
+ declare function toInternalRequest(req: Request, config: CourseBuilderConfig): Promise<RequestInternal | undefined>;
20
+ declare function parseActionAndProviderId(pathname: string, base: string): {
21
+ action: CourseBuilderAction;
22
+ providerId?: string;
23
+ };
24
+ declare class UnknownAction extends CourseBuilderError {
25
+ static type: string;
26
+ }
27
+ declare function toResponse(res: ResponseInternal): Response;
28
+
29
+ export { UnknownAction, parseActionAndProviderId, toInternalRequest, toResponse };
@@ -0,0 +1,16 @@
1
+ import {
2
+ UnknownAction,
3
+ parseActionAndProviderId,
4
+ toInternalRequest,
5
+ toResponse
6
+ } from "../chunk-4TSEPUSC.js";
7
+ import "../chunk-VEKYIR7I.js";
8
+ import "../chunk-AZLUMIIF.js";
9
+ import "../chunk-WGAPYIUP.js";
10
+ export {
11
+ UnknownAction,
12
+ parseActionAndProviderId,
13
+ toInternalRequest,
14
+ toResponse
15
+ };
16
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@coursebuilder/server",
3
+ "version": "0.1.0-canary.0.0c9beb2ae",
4
+ "private": false,
5
+ "description": "Server-side composition root for Course Builder",
6
+ "files": [
7
+ "dist",
8
+ "src"
9
+ ],
10
+ "type": "module",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./create-inngest-middleware": {
18
+ "types": "./dist/create-inngest-middleware.d.ts",
19
+ "import": "./dist/create-inngest-middleware.js"
20
+ },
21
+ "./http": {
22
+ "types": "./dist/http.d.ts",
23
+ "import": "./dist/http.js"
24
+ },
25
+ "./http-router": {
26
+ "types": "./dist/http-router.d.ts",
27
+ "import": "./dist/http-router.js"
28
+ },
29
+ "./actions/*": {
30
+ "types": "./dist/actions/*.d.ts",
31
+ "import": "./dist/actions/*.js"
32
+ },
33
+ "./runtime/*": {
34
+ "types": "./dist/runtime/*.d.ts",
35
+ "import": "./dist/runtime/*.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "ai": "5.0.116",
40
+ "cookie": "0.7.0",
41
+ "date-fns": "^2.30.0",
42
+ "inngest": "3.54.2",
43
+ "liquidjs": "^10.21.0",
44
+ "zod": "^4.1.12",
45
+ "@coursebuilder/commerce": "0.1.0-canary.0.0c9beb2ae",
46
+ "@coursebuilder/email": "0.1.0-canary.0.0c9beb2ae",
47
+ "@coursebuilder/core": "3.0.0-canary.0.0c9beb2ae",
48
+ "@coursebuilder/nodash": "0.0.3",
49
+ "@coursebuilder/utils": "1.1.0-canary.0.0c9beb2ae",
50
+ "@coursebuilder/video-processing": "0.1.0-canary.0.0c9beb2ae"
51
+ },
52
+ "devDependencies": {
53
+ "@types/cookie": "0.6.0",
54
+ "tsup": "8.0.2",
55
+ "typescript": "5.4.5",
56
+ "vitest": "1.6.0"
57
+ },
58
+ "peerDependencies": {},
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "dev": "tsup --watch",
62
+ "test": "vitest run",
63
+ "typecheck": "tsc --noEmit"
64
+ }
65
+ }
@@ -0,0 +1,232 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ processRefund as commerceProcessRefund,
5
+ redeem as commerceRedeem,
6
+ transferPurchase as commerceTransferPurchase,
7
+ } from '@coursebuilder/commerce'
8
+ import { checkout as legacyCheckout } from '@coursebuilder/server/actions/checkout'
9
+ import { webhook as legacyWebhook } from '@coursebuilder/server/actions/index'
10
+ import type {
11
+ InternalOptions,
12
+ RequestInternal,
13
+ } from '@coursebuilder/core/types'
14
+
15
+ import { checkout } from './checkout'
16
+ import { claimed } from './claimed'
17
+ import { createMagicLink } from './create-magic-link'
18
+ import { getUserPurchases, session, webhook } from './index'
19
+ import { processRefund } from './process-refund'
20
+ import { redeem } from './redeem'
21
+ import { transferPurchase } from './transfer-purchase'
22
+ import { userLookup } from './user-lookup'
23
+
24
+ const cookies = [{ name: 'session', value: 'cookie', options: {} }]
25
+
26
+ const request = (overrides: Partial<RequestInternal> = {}): RequestInternal =>
27
+ ({
28
+ action: 'session',
29
+ method: 'GET',
30
+ url: new URL('http://test.com/api/coursebuilder/session'),
31
+ cookies: {},
32
+ headers: {},
33
+ query: {},
34
+ ...overrides,
35
+ }) as RequestInternal
36
+
37
+ describe('@coursebuilder/server HTTP actions', () => {
38
+ it('keeps commerce operations behind their engine boundary', () => {
39
+ expect(processRefund).toBe(commerceProcessRefund)
40
+ expect(redeem).toBe(commerceRedeem)
41
+ expect(transferPurchase).toBe(commerceTransferPurchase)
42
+ })
43
+
44
+ it('preserves the session callback response contract', async () => {
45
+ const options = {
46
+ callbacks: {
47
+ session: vi.fn().mockResolvedValue({ user: { id: 'user-1' } }),
48
+ },
49
+ logger: { error: vi.fn() },
50
+ } as unknown as InternalOptions
51
+
52
+ await expect(session(options, cookies)).resolves.toEqual({
53
+ body: { user: { id: 'user-1' } },
54
+ headers: { 'Content-Type': 'application/json' },
55
+ cookies,
56
+ })
57
+ })
58
+
59
+ it('preserves authenticated purchase lookup behavior', async () => {
60
+ const purchases = [{ id: 'purchase-1' }]
61
+ const options = {
62
+ adapter: { getPurchasesForUser: vi.fn().mockResolvedValue(purchases) },
63
+ getCurrentUser: vi.fn().mockResolvedValue({ id: 'user-1' }),
64
+ } as unknown as InternalOptions
65
+
66
+ await expect(
67
+ getUserPurchases(
68
+ request({ query: { userId: 'user-1' } }),
69
+ cookies,
70
+ options,
71
+ ),
72
+ ).resolves.toEqual({
73
+ status: 200,
74
+ body: purchases,
75
+ headers: { 'Content-Type': 'application/json' },
76
+ cookies,
77
+ })
78
+ })
79
+
80
+ it('preserves checkout redirects across the compatibility boundary', async () => {
81
+ const checkoutRequest = request({
82
+ action: 'checkout',
83
+ method: 'POST',
84
+ query: {
85
+ productId: 'product-1',
86
+ cancelUrl: 'http://test.com/cancel',
87
+ bulk: 'false',
88
+ },
89
+ })
90
+ const createOptions = () =>
91
+ ({
92
+ provider: {
93
+ createCheckoutSession: vi
94
+ .fn()
95
+ .mockResolvedValue({ redirect: 'https://checkout.test/session' }),
96
+ },
97
+ adapter: {
98
+ getProduct: vi.fn().mockResolvedValue({ type: 'course' }),
99
+ },
100
+ baseUrl: 'http://test.com',
101
+ }) as unknown as InternalOptions<'payment'>
102
+
103
+ const legacyResponse = await legacyCheckout(
104
+ checkoutRequest,
105
+ cookies,
106
+ createOptions(),
107
+ )
108
+ const serverResponse = await checkout(
109
+ checkoutRequest,
110
+ cookies,
111
+ createOptions(),
112
+ )
113
+
114
+ expect({
115
+ status: (serverResponse as Response).status,
116
+ location: (serverResponse as Response).headers.get('location'),
117
+ }).toEqual({
118
+ status: (legacyResponse as Response).status,
119
+ location: (legacyResponse as Response).headers.get('location'),
120
+ })
121
+ })
122
+
123
+ it('preserves transcription webhook persistence and response behavior', async () => {
124
+ const results = {
125
+ channels: [
126
+ {
127
+ alternatives: [
128
+ {
129
+ transcript: 'Hello',
130
+ words: [
131
+ {
132
+ word: 'hello',
133
+ punctuated_word: 'Hello.',
134
+ start: 0,
135
+ end: 1,
136
+ confidence: 1,
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ }
144
+ const webhookRequest = request({
145
+ action: 'webhook',
146
+ method: 'POST',
147
+ body: { results },
148
+ })
149
+ const createOptions = () => {
150
+ const adapter = {
151
+ getContentResource: vi
152
+ .fn()
153
+ .mockResolvedValueOnce({ id: 'video-1', createdById: 'user-1' })
154
+ .mockResolvedValueOnce(null),
155
+ createContentResource: vi.fn(),
156
+ addResourceToResource: vi.fn(),
157
+ updateContentResourceFields: vi.fn(),
158
+ }
159
+ return {
160
+ options: {
161
+ provider: { type: 'transcription' },
162
+ adapter,
163
+ inngest: { send: vi.fn() },
164
+ url: new URL('http://test.com/webhook?videoResourceId=video-1'),
165
+ } as unknown as InternalOptions<'transcription'>,
166
+ adapter,
167
+ }
168
+ }
169
+ const legacy = createOptions()
170
+ const server = createOptions()
171
+
172
+ const legacyResponse = await legacyWebhook(
173
+ webhookRequest,
174
+ cookies,
175
+ legacy.options,
176
+ )
177
+ const serverResponse = await webhook(
178
+ webhookRequest,
179
+ cookies,
180
+ server.options,
181
+ )
182
+
183
+ expect(serverResponse).toEqual(legacyResponse)
184
+ expect(server.adapter.updateContentResourceFields.mock.calls).toEqual(
185
+ legacy.adapter.updateContentResourceFields.mock.calls,
186
+ )
187
+ })
188
+
189
+ it('preserves claimed-seat projection behavior', async () => {
190
+ const options = {
191
+ adapter: {
192
+ getPurchasesForBulkCouponId: vi
193
+ .fn()
194
+ .mockResolvedValue([
195
+ { user: { name: 'Ada', email: 'ada@example.com' } },
196
+ ]),
197
+ },
198
+ } as unknown as InternalOptions
199
+
200
+ await expect(
201
+ claimed(
202
+ request({ body: { bulkCouponId: 'coupon-1' } }),
203
+ cookies,
204
+ options,
205
+ ),
206
+ ).resolves.toEqual({
207
+ body: [{ name: 'Ada', email: 'ada@example.com' }],
208
+ headers: { 'Content-Type': 'application/json' },
209
+ cookies,
210
+ })
211
+ })
212
+
213
+ it.each([
214
+ ['lookup', userLookup],
215
+ ['create-magic-link', createMagicLink],
216
+ ] as const)('preserves secret protection for %s', async (action, handler) => {
217
+ vi.stubEnv('SKILL_SECRET', 'expected-secret')
218
+ const options = { adapter: {} } as InternalOptions
219
+ await expect(
220
+ handler(
221
+ request({
222
+ action,
223
+ method: 'POST',
224
+ headers: { 'x-skill-secret': 'wrong-secret' },
225
+ body: {},
226
+ }),
227
+ cookies,
228
+ options,
229
+ ),
230
+ ).resolves.toEqual({ status: 401, body: 'unauthorized' })
231
+ })
232
+ })
@@ -0,0 +1,160 @@
1
+ import { buildCheckoutAttribution } from '@coursebuilder/commerce/checkout-attribution'
2
+ import { CheckoutParamsSchema } from '@coursebuilder/commerce/stripe-checkout'
3
+ import type {
4
+ InternalOptions,
5
+ RequestInternal,
6
+ ResponseInternal,
7
+ } from '@coursebuilder/core/types'
8
+
9
+ import type { Cookie } from '../runtime/cookie'
10
+ import { logger } from '../runtime/logger'
11
+
12
+ const searchParamsWithoutNil = (params: Record<string, unknown>) => {
13
+ return new URLSearchParams(
14
+ Object.entries(params).flatMap(([key, value]) => {
15
+ if (value === undefined || value === null) return []
16
+ return [[key, String(value)]]
17
+ }),
18
+ )
19
+ }
20
+
21
+ export async function checkout(
22
+ request: RequestInternal,
23
+ cookies: Cookie[],
24
+ options: InternalOptions<'payment'>,
25
+ ): Promise<ResponseInternal> {
26
+ const response: ResponseInternal<any | null> = {
27
+ body: null,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ cookies,
30
+ }
31
+
32
+ // Extract shortlink reference from cookie
33
+ const shortlinkRefCookie = cookies.find((c) => c.name === 'sl_ref')
34
+ const shortlinkRef = shortlinkRefCookie?.value
35
+
36
+ // Extract first-touch attribution from cookie
37
+ const ftAttrCookie = cookies.find((c) => c.name === 'ft_attr')
38
+ let firstTouch: Record<string, unknown> = {}
39
+ try {
40
+ if (ftAttrCookie?.value) {
41
+ firstTouch = JSON.parse(decodeURIComponent(ftAttrCookie.value))
42
+ }
43
+ } catch {
44
+ // Malformed cookie, ignore
45
+ }
46
+
47
+ // Extract last-touch attribution from cookie
48
+ const ltAttrCookie = cookies.find((c) => c.name === 'lt_attr')
49
+ let lastTouch: Record<string, string> = {}
50
+ try {
51
+ if (ltAttrCookie?.value) {
52
+ lastTouch = JSON.parse(decodeURIComponent(ltAttrCookie.value))
53
+ }
54
+ } catch {
55
+ // Malformed cookie, ignore
56
+ }
57
+
58
+ const selfReportedSource =
59
+ typeof request.query?.selfReportedSource === 'string'
60
+ ? request.query.selfReportedSource.slice(0, 500)
61
+ : undefined
62
+ const checkoutAttribution = buildCheckoutAttribution({
63
+ firstTouch,
64
+ lastTouch,
65
+ shortlinkRef,
66
+ selfReportedSource,
67
+ })
68
+
69
+ logger.debug('checkout.cookies-received', {
70
+ cookieCount: cookies.length,
71
+ cookieNames: cookies.map((c) => c.name),
72
+ hasShortlinkRef: Boolean(shortlinkRef),
73
+ hasAttributionSnapshot: Boolean(checkoutAttribution.attributionSnapshot),
74
+ })
75
+
76
+ const checkoutParamsParsed = CheckoutParamsSchema.safeParse({
77
+ ...request.query,
78
+ country:
79
+ request.query?.country ||
80
+ request.headers?.['x-vercel-ip-country'] ||
81
+ process.env.DEFAULT_COUNTRY ||
82
+ 'US',
83
+ ip_address:
84
+ request.query?.ip_address ||
85
+ request.headers?.['x-forwarded-for'] ||
86
+ '0.0.0.0',
87
+ organizationId: request.query?.organizationId,
88
+ ...(selfReportedSource && { selfReportedSource }),
89
+ ...checkoutAttribution,
90
+ })
91
+
92
+ if (!checkoutParamsParsed.success) {
93
+ const parseError = new Error('Error parsing checkout params')
94
+ logger.error(parseError, {
95
+ context: 'checkout.params-parse',
96
+ issueCount: checkoutParamsParsed.error.issues.length,
97
+ issuePaths: checkoutParamsParsed.error.issues.map((issue) =>
98
+ issue.path.join('.'),
99
+ ),
100
+ productId:
101
+ typeof request.query?.productId === 'string'
102
+ ? request.query.productId
103
+ : undefined,
104
+ organizationId:
105
+ typeof request.query?.organizationId === 'string'
106
+ ? request.query.organizationId
107
+ : undefined,
108
+ })
109
+ throw new Error('Error parsing checkout params')
110
+ }
111
+
112
+ const checkoutParams = checkoutParamsParsed.data
113
+
114
+ try {
115
+ const stripe = await options.provider.createCheckoutSession(
116
+ checkoutParams,
117
+ options.adapter,
118
+ )
119
+
120
+ const product = await options.adapter?.getProduct(checkoutParams.productId)
121
+
122
+ // if the product is a membership or cohort, we need to redirect to the login page
123
+ // if the user isn't logged in. we want to ensure an org id is set.
124
+ if (
125
+ product?.type &&
126
+ ['membership', 'cohort', 'cohort-archive', 'live'].includes(product.type)
127
+ ) {
128
+ return Response.redirect(
129
+ `${options.baseUrl}/subscribe/verify-login?${searchParamsWithoutNil({
130
+ ...request.query,
131
+ country:
132
+ request.query?.country ||
133
+ request.headers?.['x-vercel-ip-country'] ||
134
+ process.env.DEFAULT_COUNTRY ||
135
+ 'US',
136
+ ip_address:
137
+ request.query?.ip_address ||
138
+ request.headers?.['x-forwarded-for'] ||
139
+ '0.0.0.0',
140
+ organizationId: request.query?.organizationId,
141
+ productId: checkoutParams.productId,
142
+ checkoutUrl: stripe.redirect,
143
+ cancelUrl: checkoutParams.cancelUrl,
144
+ selfReportedSource,
145
+ })}`,
146
+ )
147
+ }
148
+
149
+ return Response.redirect(stripe.redirect)
150
+ } catch (e) {
151
+ logger.error(e instanceof Error ? e : new Error(String(e)), {
152
+ context: 'checkout.create-session',
153
+ productId: checkoutParams.productId,
154
+ organizationId: checkoutParams.organizationId,
155
+ country: checkoutParams.country,
156
+ })
157
+ }
158
+
159
+ return response
160
+ }
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod'
2
+
3
+ import type {
4
+ InternalOptions,
5
+ RequestInternal,
6
+ ResponseInternal,
7
+ } from '@coursebuilder/core/types'
8
+
9
+ import type { Cookie } from '../runtime/cookie'
10
+
11
+ export async function claimed(
12
+ request: RequestInternal,
13
+ cookies: Cookie[],
14
+ options: InternalOptions,
15
+ ): Promise<ResponseInternal> {
16
+ if (!options.adapter) throw new Error('Adapter not found')
17
+
18
+ const bulkCouponId = z
19
+ .string()
20
+ .optional()
21
+ .parse(request.query?.bulkCouponId || request.body?.bulkCouponId)
22
+
23
+ if (!bulkCouponId) throw new Error('Bulk coupon ID is required')
24
+
25
+ const purchases =
26
+ await options.adapter.getPurchasesForBulkCouponId(bulkCouponId)
27
+
28
+ const users = purchases.map((purchase) => {
29
+ return {
30
+ name: purchase.user.name,
31
+ email: purchase.user.email,
32
+ }
33
+ })
34
+
35
+ return {
36
+ body: users,
37
+ headers: { 'Content-Type': 'application/json' },
38
+ cookies,
39
+ }
40
+ }