@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.
- package/LICENSE +21 -0
- package/dist/actions/checkout.d.ts +6 -0
- package/dist/actions/checkout.js +9 -0
- package/dist/actions/checkout.js.map +1 -0
- package/dist/actions/claimed.d.ts +6 -0
- package/dist/actions/claimed.js +8 -0
- package/dist/actions/claimed.js.map +1 -0
- package/dist/actions/create-magic-link.d.ts +6 -0
- package/dist/actions/create-magic-link.js +9 -0
- package/dist/actions/create-magic-link.js.map +1 -0
- package/dist/actions/index.d.ts +11 -0
- package/dist/actions/index.js +18 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/prices-coupon-for-code.d.ts +6 -0
- package/dist/actions/prices-coupon-for-code.js +63 -0
- package/dist/actions/prices-coupon-for-code.js.map +1 -0
- package/dist/actions/prices-formatted.d.ts +6 -0
- package/dist/actions/prices-formatted.js +8 -0
- package/dist/actions/prices-formatted.js.map +1 -0
- package/dist/actions/process-refund.d.ts +1 -0
- package/dist/actions/process-refund.js +8 -0
- package/dist/actions/process-refund.js.map +1 -0
- package/dist/actions/redeem.d.ts +1 -0
- package/dist/actions/redeem.js +8 -0
- package/dist/actions/redeem.js.map +1 -0
- package/dist/actions/transfer-purchase.d.ts +1 -0
- package/dist/actions/transfer-purchase.js +8 -0
- package/dist/actions/transfer-purchase.js.map +1 -0
- package/dist/actions/user-lookup.d.ts +6 -0
- package/dist/actions/user-lookup.js +9 -0
- package/dist/actions/user-lookup.js.map +1 -0
- package/dist/adapter-BtDS6LDw.d.ts +9 -0
- package/dist/chunk-22AMOZYZ.js +67 -0
- package/dist/chunk-22AMOZYZ.js.map +1 -0
- package/dist/chunk-4EQQSW57.js +32 -0
- package/dist/chunk-4EQQSW57.js.map +1 -0
- package/dist/chunk-4LZZLN6T.js +52 -0
- package/dist/chunk-4LZZLN6T.js.map +1 -0
- package/dist/chunk-4QQA3TC2.js +29 -0
- package/dist/chunk-4QQA3TC2.js.map +1 -0
- package/dist/chunk-4TSEPUSC.js +160 -0
- package/dist/chunk-4TSEPUSC.js.map +1 -0
- package/dist/chunk-5O4UO4IR.js +93 -0
- package/dist/chunk-5O4UO4IR.js.map +1 -0
- package/dist/chunk-7FCQOLKL.js +26 -0
- package/dist/chunk-7FCQOLKL.js.map +1 -0
- package/dist/chunk-AZLUMIIF.js +8 -0
- package/dist/chunk-AZLUMIIF.js.map +1 -0
- package/dist/chunk-E6HV6TNI.js +38 -0
- package/dist/chunk-E6HV6TNI.js.map +1 -0
- package/dist/chunk-ECNJPNPU.js +54 -0
- package/dist/chunk-ECNJPNPU.js.map +1 -0
- package/dist/chunk-F3KUYN5I.js +111 -0
- package/dist/chunk-F3KUYN5I.js.map +1 -0
- package/dist/chunk-G63YAJF2.js +107 -0
- package/dist/chunk-G63YAJF2.js.map +1 -0
- package/dist/chunk-JFR7XO72.js +7 -0
- package/dist/chunk-JFR7XO72.js.map +1 -0
- package/dist/chunk-KNC7QVGZ.js +26 -0
- package/dist/chunk-KNC7QVGZ.js.map +1 -0
- package/dist/chunk-ODSSRMD2.js +270 -0
- package/dist/chunk-ODSSRMD2.js.map +1 -0
- package/dist/chunk-VEKYIR7I.js +26 -0
- package/dist/chunk-VEKYIR7I.js.map +1 -0
- package/dist/chunk-W7YAT4NR.js +12 -0
- package/dist/chunk-W7YAT4NR.js.map +1 -0
- package/dist/chunk-WGAPYIUP.js +56 -0
- package/dist/chunk-WGAPYIUP.js.map +1 -0
- package/dist/chunk-WYNH2AWD.js +7 -0
- package/dist/chunk-WYNH2AWD.js.map +1 -0
- package/dist/chunk-Y5X5J55Z.js +384 -0
- package/dist/chunk-Y5X5J55Z.js.map +1 -0
- package/dist/chunk-YFLGJIMA.js +216 -0
- package/dist/chunk-YFLGJIMA.js.map +1 -0
- package/dist/chunk-ZFZNN4E5.js +7 -0
- package/dist/chunk-ZFZNN4E5.js.map +1 -0
- package/dist/create-inngest-middleware.d.ts +635 -0
- package/dist/create-inngest-middleware.js +12 -0
- package/dist/create-inngest-middleware.js.map +1 -0
- package/dist/http-router.d.ts +20 -0
- package/dist/http-router.js +24 -0
- package/dist/http-router.js.map +1 -0
- package/dist/http.d.ts +38 -0
- package/dist/http.js +32 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +365 -0
- package/dist/index.js.map +1 -0
- package/dist/magic-string.es-ENO43NON.js +1389 -0
- package/dist/magic-string.es-ENO43NON.js.map +1 -0
- package/dist/runtime/actions.d.ts +5 -0
- package/dist/runtime/actions.js +8 -0
- package/dist/runtime/actions.js.map +1 -0
- package/dist/runtime/assert.d.ts +15 -0
- package/dist/runtime/assert.js +8 -0
- package/dist/runtime/assert.js.map +1 -0
- package/dist/runtime/cookie.d.ts +8 -0
- package/dist/runtime/cookie.js +8 -0
- package/dist/runtime/cookie.js.map +1 -0
- package/dist/runtime/env.d.ts +9 -0
- package/dist/runtime/env.js +11 -0
- package/dist/runtime/env.js.map +1 -0
- package/dist/runtime/env.test.d.ts +2 -0
- package/dist/runtime/env.test.js +14956 -0
- package/dist/runtime/env.test.js.map +1 -0
- package/dist/runtime/init.d.ts +26 -0
- package/dist/runtime/init.js +14 -0
- package/dist/runtime/init.js.map +1 -0
- package/dist/runtime/logger.d.ts +4 -0
- package/dist/runtime/logger.js +10 -0
- package/dist/runtime/logger.js.map +1 -0
- package/dist/runtime/merge.d.ts +4 -0
- package/dist/runtime/merge.js +8 -0
- package/dist/runtime/merge.js.map +1 -0
- package/dist/runtime/providers.d.ts +21 -0
- package/dist/runtime/providers.js +9 -0
- package/dist/runtime/providers.js.map +1 -0
- package/dist/runtime/web.d.ts +29 -0
- package/dist/runtime/web.js +16 -0
- package/dist/runtime/web.js.map +1 -0
- package/package.json +65 -0
- package/src/actions/actions.test.ts +232 -0
- package/src/actions/checkout.ts +160 -0
- package/src/actions/claimed.ts +40 -0
- package/src/actions/create-magic-link.ts +78 -0
- package/src/actions/index.ts +401 -0
- package/src/actions/prices-coupon-for-code.ts +91 -0
- package/src/actions/prices-formatted.ts +313 -0
- package/src/actions/process-refund.ts +2 -0
- package/src/actions/redeem.ts +2 -0
- package/src/actions/transfer-purchase.ts +2 -0
- package/src/actions/user-lookup.ts +37 -0
- package/src/adapter.ts +28 -0
- package/src/create-inngest-middleware.ts +157 -0
- package/src/http-actions.ts +29 -0
- package/src/http-router.ts +79 -0
- package/src/http.test.ts +46 -0
- package/src/http.ts +83 -0
- package/src/index.test.ts +21 -0
- package/src/index.ts +44 -0
- package/src/mock-adapter.ts +398 -0
- package/src/mock-payment-provider.ts +56 -0
- package/src/resource-chat.ts +371 -0
- package/src/runtime/actions.ts +25 -0
- package/src/runtime/assert.ts +55 -0
- package/src/runtime/cookie.ts +12 -0
- package/src/runtime/env.test.ts +49 -0
- package/src/runtime/env.ts +63 -0
- package/src/runtime/init.ts +112 -0
- package/src/runtime/logger.ts +6 -0
- package/src/runtime/merge.ts +25 -0
- package/src/runtime/providers.ts +38 -0
- package/src/runtime/web.ts +199 -0
- package/src/streaming-chat-prompt-executor.ts +43 -0
- package/src/type-contracts.test.ts +167 -0
- 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
|
+
})
|