@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,25 @@
1
+ // Source: https://stackoverflow.com/a/34749873/5364135
2
+
3
+ /** Simple object check */
4
+ function isObject(item: any): boolean {
5
+ return item && typeof item === 'object' && !Array.isArray(item)
6
+ }
7
+
8
+ /** Deep merge two objects */
9
+ export function merge(target: any, ...sources: any[]): any {
10
+ if (!sources.length) return target
11
+ const source = sources.shift()
12
+
13
+ if (isObject(target) && isObject(source)) {
14
+ for (const key in source) {
15
+ if (isObject(source[key])) {
16
+ if (!target[key]) Object.assign(target, { [key]: {} })
17
+ merge(target[key], source[key])
18
+ } else {
19
+ Object.assign(target, { [key]: source[key] })
20
+ }
21
+ }
22
+ }
23
+
24
+ return merge(target, ...sources)
25
+ }
@@ -0,0 +1,38 @@
1
+ import { Provider } from '@coursebuilder/core/providers'
2
+ import { InternalProvider } from '@coursebuilder/core/types'
3
+
4
+ import { CourseBuilderConfig } from '../http'
5
+ import { merge } from './merge'
6
+
7
+ export default function parseProviders(params: {
8
+ providers: Provider[]
9
+ url: URL
10
+ providerId?: string
11
+ options: CourseBuilderConfig
12
+ }): {
13
+ providers: InternalProvider[]
14
+ provider?: InternalProvider
15
+ } {
16
+ const { providerId, options } = params
17
+
18
+ const providers = params.providers.map((p) => {
19
+ const provider = typeof p === 'function' ? p() : p
20
+ const { options: userOptions, ...defaults } = provider
21
+
22
+ if (!userOptions) {
23
+ throw new Error(`Provider ${provider.id} is missing options`)
24
+ }
25
+
26
+ const { paymentsAdapter, ...userOptionsWithPaymentsAdapter } = userOptions
27
+
28
+ return {
29
+ ...merge(defaults, userOptionsWithPaymentsAdapter),
30
+ ...(paymentsAdapter ? { options: paymentsAdapter } : {}),
31
+ }
32
+ })
33
+
34
+ return {
35
+ providers,
36
+ provider: providers.find(({ id }) => id === providerId),
37
+ }
38
+ }
@@ -0,0 +1,199 @@
1
+ import { parse as parseCookie, serialize } from 'cookie'
2
+
3
+ import { CourseBuilderError } from '@coursebuilder/core/errors'
4
+ import {
5
+ CourseBuilderAction,
6
+ RequestInternal,
7
+ ResponseInternal,
8
+ } from '@coursebuilder/core/types'
9
+
10
+ import { CourseBuilderConfig } from '../http'
11
+ import { isCourseBuilderAction } from './actions'
12
+ import { logger } from './logger'
13
+
14
+ async function getBody(
15
+ req: Request,
16
+ config: CourseBuilderConfig,
17
+ ): Promise<Record<string, any> | undefined> {
18
+ const headers = Object.fromEntries(req.headers)
19
+ const isStripeWebhook = ['stripe-signature'].every((prop: string) => {
20
+ return prop in headers
21
+ })
22
+
23
+ if (isStripeWebhook) {
24
+ let parsedBody
25
+ const stripeProvider = config.providers.find((p: any) => p.id === 'stripe')
26
+ parsedBody = await req.text()
27
+ stripeProvider?.options?.paymentsAdapter.verifyWebhookSignature(
28
+ parsedBody,
29
+ headers['stripe-signature'],
30
+ )
31
+ parsedBody = JSON.parse(parsedBody)
32
+ return parsedBody
33
+ }
34
+
35
+ if (!('body' in req) || !req.body || req.method !== 'POST') return
36
+
37
+ const contentType = req.headers.get('content-type')
38
+ if (contentType?.includes('application/json')) {
39
+ const rawBody = await req.text()
40
+ if (rawBody.trim().length === 0) return undefined
41
+
42
+ try {
43
+ return JSON.parse(rawBody)
44
+ } catch (e) {
45
+ logger.error(e instanceof Error ? e : new Error(String(e)), {
46
+ context: 'web.body-parse',
47
+ method: req.method,
48
+ contentType,
49
+ })
50
+ return undefined
51
+ }
52
+ } else if (contentType?.includes('application/x-www-form-urlencoded')) {
53
+ const params = new URLSearchParams(await req.text())
54
+ return Object.fromEntries(params)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * takes a `Request` object and converts it into an internal request object
60
+ * that we use throughout the system
61
+ * @param req
62
+ * @param config
63
+ * @returns
64
+ */
65
+ export async function toInternalRequest(
66
+ req: Request,
67
+ config: CourseBuilderConfig,
68
+ ): Promise<RequestInternal | undefined> {
69
+ try {
70
+ config.basePath ??= '/coursebuilder'
71
+
72
+ const url = new URL(req.url)
73
+
74
+ const { action, providerId } = parseActionAndProviderId(
75
+ url.pathname,
76
+ config.basePath,
77
+ )
78
+
79
+ logger.debug('web.action-parsed', {
80
+ pathname: url.pathname,
81
+ action,
82
+ providerId: providerId ?? null,
83
+ searchParamCount: url.searchParams.size,
84
+ })
85
+
86
+ const parsedCookies = parseCookie(req.headers.get('cookie') ?? '') ?? {}
87
+
88
+ logger.debug('web.request-parsed', {
89
+ action,
90
+ method: req.method,
91
+ providerId: providerId ?? null,
92
+ hasCookieHeader: Boolean(req.headers.get('cookie')),
93
+ parsedCookieCount: Object.keys(parsedCookies).length,
94
+ hasSl_ref: 'sl_ref' in parsedCookies,
95
+ })
96
+
97
+ return {
98
+ url,
99
+ action,
100
+ providerId,
101
+ method: req.method as 'POST' | 'GET',
102
+ headers: Object.fromEntries(req.headers),
103
+ body: req.body ? await getBody(req, config) : undefined,
104
+ cookies: parsedCookies,
105
+ error: url.searchParams.get('error') ?? undefined,
106
+ query: Object.fromEntries(url.searchParams),
107
+ }
108
+ } catch (e) {
109
+ logger.error(e instanceof Error ? e : new Error(String(e)), {
110
+ context: 'web.to-internal-request',
111
+ method: req.method,
112
+ url: req.url,
113
+ basePath: config.basePath,
114
+ })
115
+ logger.debug('web.request-context', {
116
+ method: req.method,
117
+ url: req.url,
118
+ hasBody: Boolean(req.body),
119
+ hasCookieHeader: Boolean(req.headers.get('cookie')),
120
+ })
121
+ }
122
+ }
123
+
124
+ export function parseActionAndProviderId(
125
+ pathname: string,
126
+ base: string,
127
+ ): {
128
+ action: CourseBuilderAction
129
+ providerId?: string
130
+ } {
131
+ const a = pathname.match(new RegExp(`^${base}(.+)`))
132
+
133
+ if (a === null) throw new UnknownAction(`Cannot parse action at ${pathname}`)
134
+
135
+ const [_, actionAndProviderId] = a
136
+
137
+ const b = actionAndProviderId.replace(/^\//, '').split('/')
138
+
139
+ if (b.length !== 1 && b.length !== 2)
140
+ throw new UnknownAction(`**Cannot parse action at ${pathname}`)
141
+
142
+ const [action, providerId] = b
143
+
144
+ if (!isCourseBuilderAction(action))
145
+ throw new UnknownAction(`***Cannot parse action at ${pathname}`)
146
+
147
+ if (
148
+ providerId &&
149
+ ![
150
+ 'webhook',
151
+ 'srt',
152
+ 'session',
153
+ 'subscribe-to-list',
154
+ 'checkout',
155
+ 'redeem',
156
+ 'subscriber',
157
+ 'lookup',
158
+ 'claimed',
159
+ 'nameUpdate',
160
+ 'transfer',
161
+ 'refund',
162
+ 'create-magic-link',
163
+ ].includes(action)
164
+ )
165
+ throw new UnknownAction(
166
+ `**** Cannot parse action at ${pathname}. add it to the list in web.ts?`,
167
+ )
168
+
169
+ return { action, providerId }
170
+ }
171
+
172
+ export class UnknownAction extends CourseBuilderError {
173
+ static type = 'UnknownAction'
174
+ }
175
+
176
+ export function toResponse(res: ResponseInternal): Response {
177
+ const headers = new Headers(res.headers)
178
+
179
+ res.cookies?.forEach((cookie) => {
180
+ const { name, value, options } = cookie
181
+ const cookieHeader = serialize(name, value, options)
182
+ if (headers.has('Set-Cookie')) headers.append('Set-Cookie', cookieHeader)
183
+ else headers.set('Set-Cookie', cookieHeader)
184
+ })
185
+
186
+ let body = res.body
187
+
188
+ if (headers.get('content-type') === 'application/json')
189
+ body = JSON.stringify(res.body)
190
+ else if (headers.get('content-type') === 'application/x-www-form-urlencoded')
191
+ body = new URLSearchParams(res.body).toString()
192
+
193
+ const status = res.redirect ? 302 : (res.status ?? 200)
194
+ const response = new Response(body, { headers, status })
195
+
196
+ if (res.redirect) response.headers.set('Location', res.redirect)
197
+
198
+ return response
199
+ }
@@ -0,0 +1,43 @@
1
+ import { type ModelMessage } from 'ai'
2
+
3
+ import { LlmProviderConfig } from '@coursebuilder/core/providers/openai'
4
+ import { AIMessage, type AIOutput } from '@coursebuilder/core/types'
5
+
6
+ export type ChatCompletionRequestMessage = ModelMessage
7
+
8
+ type PromptStepOptions = {
9
+ requestId: string
10
+ promptMessages: ModelMessage[]
11
+ model: string
12
+ provider: LlmProviderConfig
13
+ }
14
+
15
+ export async function streamingChatPromptExecutor({
16
+ requestId,
17
+ promptMessages,
18
+ model,
19
+ provider,
20
+ }: PromptStepOptions) {
21
+ const result: AIOutput | null = await provider.createChatCompletion({
22
+ messages: promptMessages,
23
+ chatId: requestId,
24
+ model,
25
+ })
26
+
27
+ if (!result) {
28
+ throw new Error('Chat completion returned null')
29
+ }
30
+
31
+ if ('error' in result) {
32
+ throw new Error(result.error)
33
+ }
34
+
35
+ const message = result as AIMessage
36
+ return [
37
+ ...promptMessages,
38
+ {
39
+ role: 'assistant',
40
+ content: message.content,
41
+ } as ModelMessage,
42
+ ]
43
+ }
@@ -0,0 +1,167 @@
1
+ import { describe, expectTypeOf, it } from 'vitest'
2
+
3
+ import {
4
+ NEW_PURCHASE_CREATED_EVENT,
5
+ PURCHASE_STATUS_UPDATED_EVENT,
6
+ STRIPE_CHECKOUT_SESSION_COMPLETED_EVENT,
7
+ STRIPE_CUSTOMER_SUBSCRIPTION_CREATED_EVENT,
8
+ STRIPE_CUSTOMER_SUBSCRIPTION_UPDATED_EVENT,
9
+ STRIPE_INVOICE_PAYMENT_SUCCEEDED_EVENT,
10
+ VIDEO_STATUS_CHECK_EVENT,
11
+ type CourseBuilderCoreEvents,
12
+ } from '@coursebuilder/core/events'
13
+
14
+ import {
15
+ defineCourseBuilderFunctions,
16
+ type CoreInngestContext,
17
+ type CourseBuilderHandlerInput,
18
+ } from './create-inngest-middleware'
19
+
20
+ type VideoStatusInput = CourseBuilderHandlerInput<
21
+ Pick<CoreInngestContext, 'db' | 'mediaUploadProvider'>,
22
+ typeof VIDEO_STATUS_CHECK_EVENT
23
+ >
24
+
25
+ type VideoStatusEvent = CourseBuilderCoreEvents[typeof VIDEO_STATUS_CHECK_EVENT]
26
+
27
+ type CommercePurchaseInput = CourseBuilderHandlerInput<
28
+ Pick<
29
+ CoreInngestContext,
30
+ 'db' | 'siteRootUrl' | 'emailProvider' | 'getAuthConfig'
31
+ >,
32
+ typeof NEW_PURCHASE_CREATED_EVENT
33
+ >
34
+
35
+ type CommerceSlackInput = CourseBuilderHandlerInput<
36
+ Pick<CoreInngestContext, 'db' | 'notificationProvider'> &
37
+ Required<Pick<CoreInngestContext, 'paymentProvider'>>,
38
+ typeof NEW_PURCHASE_CREATED_EVENT
39
+ >
40
+
41
+ type PurchaseStatusInput = CourseBuilderHandlerInput<
42
+ Pick<CoreInngestContext, 'db'>,
43
+ typeof PURCHASE_STATUS_UPDATED_EVENT
44
+ >
45
+
46
+ type StripeCheckoutInput = CourseBuilderHandlerInput<
47
+ Pick<CoreInngestContext, 'db' | 'notificationProvider'> &
48
+ Required<Pick<CoreInngestContext, 'paymentProvider'>>,
49
+ typeof STRIPE_CHECKOUT_SESSION_COMPLETED_EVENT
50
+ >
51
+
52
+ type StripeInvoiceInput = CourseBuilderHandlerInput<
53
+ Pick<CoreInngestContext, 'db' | 'notificationProvider'>,
54
+ typeof STRIPE_INVOICE_PAYMENT_SUCCEEDED_EVENT
55
+ >
56
+
57
+ type StripeSubscriptionCreatedInput = CourseBuilderHandlerInput<
58
+ Pick<CoreInngestContext, never>,
59
+ typeof STRIPE_CUSTOMER_SUBSCRIPTION_CREATED_EVENT
60
+ >
61
+
62
+ type StripeSubscriptionUpdatedInput = CourseBuilderHandlerInput<
63
+ Pick<CoreInngestContext, never>,
64
+ typeof STRIPE_CUSTOMER_SUBSCRIPTION_UPDATED_EVENT
65
+ >
66
+
67
+ describe('@coursebuilder/server type contracts', () => {
68
+ it('composes provider-neutral event payloads into handler input', () => {
69
+ expectTypeOf<VideoStatusInput['event']>().toEqualTypeOf<VideoStatusEvent>()
70
+ expectTypeOf<
71
+ VideoStatusInput['event']['data']['videoResourceId']
72
+ >().toEqualTypeOf<string>()
73
+ })
74
+
75
+ it('limits video handler middleware context to declared capabilities', () => {
76
+ expectTypeOf<VideoStatusInput['db']>().toEqualTypeOf<
77
+ CoreInngestContext['db']
78
+ >()
79
+ expectTypeOf<VideoStatusInput['mediaUploadProvider']>().toEqualTypeOf<
80
+ CoreInngestContext['mediaUploadProvider']
81
+ >()
82
+ type HasCommerceProvider = 'paymentProvider' extends keyof VideoStatusInput
83
+ ? true
84
+ : false
85
+ expectTypeOf<HasCommerceProvider>().toEqualTypeOf<false>()
86
+ })
87
+
88
+ it('composes commerce and Stripe event payloads into handler inputs', () => {
89
+ expectTypeOf<
90
+ CommercePurchaseInput['event']['data']['purchaseId']
91
+ >().toEqualTypeOf<string>()
92
+ expectTypeOf<
93
+ StripeCheckoutInput['event']['data']['stripeEvent']['type']
94
+ >().toEqualTypeOf<'checkout.session.completed'>()
95
+ expectTypeOf<
96
+ StripeInvoiceInput['event']['data']['stripeEvent']['type']
97
+ >().toEqualTypeOf<'invoice.payment_succeeded'>()
98
+ expectTypeOf<
99
+ StripeSubscriptionCreatedInput['event']['data']['stripeEvent']['type']
100
+ >().toEqualTypeOf<'customer.subscription.created'>()
101
+ expectTypeOf<
102
+ StripeSubscriptionUpdatedInput['event']['data']['stripeEvent']['type']
103
+ >().toEqualTypeOf<'customer.subscription.updated'>()
104
+ })
105
+
106
+ it('limits commerce and Stripe middleware contexts to their capabilities', () => {
107
+ expectTypeOf<CommercePurchaseInput['getAuthConfig']>().toEqualTypeOf<
108
+ CoreInngestContext['getAuthConfig']
109
+ >()
110
+ expectTypeOf<CommerceSlackInput['paymentProvider']>().toEqualTypeOf<
111
+ NonNullable<CoreInngestContext['paymentProvider']>
112
+ >()
113
+ expectTypeOf<CommerceSlackInput['notificationProvider']>().toEqualTypeOf<
114
+ CoreInngestContext['notificationProvider']
115
+ >()
116
+ expectTypeOf<PurchaseStatusInput['db']>().toEqualTypeOf<
117
+ CoreInngestContext['db']
118
+ >()
119
+ expectTypeOf<StripeCheckoutInput['paymentProvider']>().toEqualTypeOf<
120
+ NonNullable<CoreInngestContext['paymentProvider']>
121
+ >()
122
+
123
+ type CommerceHasVideoProvider =
124
+ 'mediaUploadProvider' extends keyof CommercePurchaseInput ? true : false
125
+ type PurchaseStatusHasPaymentProvider =
126
+ 'paymentProvider' extends keyof PurchaseStatusInput ? true : false
127
+ type StripeHasTranscriptionProvider =
128
+ 'transcriptProvider' extends keyof StripeCheckoutInput ? true : false
129
+ type StripeHasAuthConfig = 'getAuthConfig' extends keyof StripeCheckoutInput
130
+ ? true
131
+ : false
132
+ type InvoiceHasPaymentProvider =
133
+ 'paymentProvider' extends keyof StripeInvoiceInput ? true : false
134
+ type SubscriptionCreatedHasDatabase =
135
+ 'db' extends keyof StripeSubscriptionCreatedInput ? true : false
136
+ type SubscriptionUpdatedHasDatabase =
137
+ 'db' extends keyof StripeSubscriptionUpdatedInput ? true : false
138
+
139
+ expectTypeOf<CommerceHasVideoProvider>().toEqualTypeOf<false>()
140
+ expectTypeOf<PurchaseStatusHasPaymentProvider>().toEqualTypeOf<false>()
141
+ expectTypeOf<StripeHasTranscriptionProvider>().toEqualTypeOf<false>()
142
+ expectTypeOf<StripeHasAuthConfig>().toEqualTypeOf<false>()
143
+ expectTypeOf<InvoiceHasPaymentProvider>().toEqualTypeOf<false>()
144
+ expectTypeOf<SubscriptionCreatedHasDatabase>().toEqualTypeOf<false>()
145
+ expectTypeOf<SubscriptionUpdatedHasDatabase>().toEqualTypeOf<false>()
146
+ })
147
+
148
+ it('preserves typed function registration order', () => {
149
+ const functions = defineCourseBuilderFunctions([
150
+ {
151
+ config: { id: 'compile-contract' },
152
+ trigger: { event: VIDEO_STATUS_CHECK_EVENT } as const,
153
+ handler: async () => undefined,
154
+ },
155
+ ])
156
+
157
+ expectTypeOf(functions[0].config.id).toEqualTypeOf<string>()
158
+
159
+ // @ts-expect-error registrations must include a handler
160
+ defineCourseBuilderFunctions([
161
+ {
162
+ config: { id: 'invalid' },
163
+ trigger: { event: VIDEO_STATUS_CHECK_EVENT },
164
+ },
165
+ ])
166
+ })
167
+ })
@@ -0,0 +1,55 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { CourseBuilderConfig } from './http'
4
+ import { logger } from './runtime/logger'
5
+ import { toInternalRequest } from './runtime/web'
6
+
7
+ const createConfig = (): CourseBuilderConfig => ({
8
+ providers: [],
9
+ baseUrl: 'http://test.com',
10
+ basePath: '/api/coursebuilder',
11
+ authConfig: {},
12
+ })
13
+
14
+ afterEach(() => {
15
+ vi.restoreAllMocks()
16
+ })
17
+
18
+ describe('toInternalRequest JSON body parsing', () => {
19
+ it('treats empty JSON request bodies as undefined without logging parse errors', async () => {
20
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {})
21
+ const request = new Request('http://test.com/api/coursebuilder/session', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'content-type': 'application/json',
25
+ },
26
+ body: '',
27
+ })
28
+
29
+ const internalRequest = await toInternalRequest(request, createConfig())
30
+
31
+ expect(internalRequest?.body).toBeUndefined()
32
+ expect(errorSpy).not.toHaveBeenCalled()
33
+ })
34
+
35
+ it('continues logging malformed non-empty JSON request bodies', async () => {
36
+ const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {})
37
+ const request = new Request('http://test.com/api/coursebuilder/session', {
38
+ method: 'POST',
39
+ headers: {
40
+ 'content-type': 'application/json',
41
+ },
42
+ body: '{',
43
+ })
44
+
45
+ const internalRequest = await toInternalRequest(request, createConfig())
46
+
47
+ expect(internalRequest?.body).toBeUndefined()
48
+ expect(errorSpy).toHaveBeenCalledTimes(1)
49
+ expect(errorSpy).toHaveBeenCalledWith(expect.any(SyntaxError), {
50
+ context: 'web.body-parse',
51
+ method: 'POST',
52
+ contentType: 'application/json',
53
+ })
54
+ })
55
+ })