@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,371 @@
1
+ import { NonRetriableError } from 'inngest'
2
+ import { Liquid } from 'liquidjs'
3
+ import { z } from 'zod'
4
+
5
+ import type { ServerAdapter } from './adapter'
6
+ import { logger } from './runtime/logger'
7
+ import { LlmProviderConfig } from '@coursebuilder/core/providers/openai'
8
+ import { PartyProviderConfig } from '@coursebuilder/core/providers/partykit'
9
+ import { User } from '@coursebuilder/core/schemas'
10
+ import {
11
+ CoreInngestFunctionInput,
12
+ CoreInngestHandler,
13
+ CoreInngestTrigger,
14
+ } from './create-inngest-middleware'
15
+ import {
16
+ ChatCompletionRequestMessage,
17
+ streamingChatPromptExecutor,
18
+ } from './streaming-chat-prompt-executor'
19
+
20
+ /** @deprecated Import event contracts from `@coursebuilder/core/events`. */
21
+ export {
22
+ ChatResourceSchema,
23
+ type ChatResource,
24
+ RESOURCE_CHAT_REQUEST_EVENT,
25
+ type ResourceChat,
26
+ } from '@coursebuilder/core/events/resource-chat'
27
+ import {
28
+ ChatResourceSchema,
29
+ type ChatResource,
30
+ RESOURCE_CHAT_REQUEST_EVENT,
31
+ } from '@coursebuilder/core/events/resource-chat'
32
+
33
+ export const resourceChatConfig = {
34
+ id: `resource-chat`,
35
+ name: 'Resource Chat',
36
+ rateLimit: {
37
+ key: 'event.user.id',
38
+ limit: 5,
39
+ period: '1m' as `${number}m`,
40
+ },
41
+ }
42
+
43
+ export const resourceChatTrigger: CoreInngestTrigger = {
44
+ event: RESOURCE_CHAT_REQUEST_EVENT,
45
+ }
46
+
47
+ // Define ChatCompletionRequestMessageRoleEnum
48
+ export enum ChatCompletionRequestMessageRoleEnum {
49
+ System = 'system',
50
+ User = 'user',
51
+ Assistant = 'assistant',
52
+ Tool = 'tool',
53
+ }
54
+
55
+ /**
56
+ * TODO: Cancellation conditions need to be added $$
57
+ */
58
+ export const resourceChatHandler: CoreInngestHandler = async ({
59
+ event,
60
+ step,
61
+ openaiProvider,
62
+ partyProvider,
63
+ db,
64
+ }: CoreInngestFunctionInput) => {
65
+ const resourceId = event.data.resourceId
66
+ const workflowTrigger = event.data.selectedWorkflow
67
+
68
+ const currentUserMessage = event.data.messages[event.data.messages.length - 1]
69
+
70
+ if (currentUserMessage?.content) {
71
+ await step.run(
72
+ `partykit broadcast user prompt [${resourceId}]`,
73
+ async () => {
74
+ await partyProvider.broadcastMessage({
75
+ body: {
76
+ body: currentUserMessage.content,
77
+ requestId: resourceId,
78
+ name: 'resource.chat.prompted',
79
+ // @ts-expect-error
80
+ userId: event.user.id,
81
+ },
82
+ roomId: resourceId,
83
+ })
84
+ },
85
+ )
86
+ }
87
+
88
+ const resource = await step.run('get the resource', async () => {
89
+ return db.getContentResource(resourceId)
90
+ })
91
+
92
+ if (!resource) {
93
+ throw new NonRetriableError(`Resource not found for id (${resourceId})`)
94
+ }
95
+
96
+ const videoResource = await step.run('get the video resource', async () => {
97
+ return db.getVideoResource(resource.resources?.[0]?.resource.id)
98
+ })
99
+
100
+ const messages = await resourceChatWorkflowExecutor({
101
+ db,
102
+ openaiProvider,
103
+ partyProvider,
104
+ step,
105
+ workflowTrigger,
106
+ resourceId,
107
+
108
+ resource: {
109
+ ...videoResource,
110
+ ...resource,
111
+ ...resource.fields,
112
+ resources: resource.resources ?? [],
113
+ },
114
+ messages: event.data.messages,
115
+ // @ts-expect-error
116
+ user: event.user,
117
+ })
118
+
119
+ return { resource: { ...videoResource, ...resource }, messages }
120
+ }
121
+
122
+ /**
123
+ * loads the workflow from sanity based on the trigger and then executes the workflow using the `resource` as
124
+ * input that is parsed by liquid into the prompt
125
+ *
126
+ * This treats the system prompt as "special" from the loaded workflow since it is added to the beginning of the prompt
127
+ * in the chat context, but additional steps are executed as normal
128
+ *
129
+ * @param step
130
+ * @param workflowTrigger
131
+ * @param resourceId
132
+ * @param messages
133
+ * @param resource
134
+ * @param currentFeedback
135
+ */
136
+ export async function resourceChatWorkflowExecutor({
137
+ step,
138
+ workflowTrigger,
139
+ resourceId,
140
+ messages,
141
+ resource,
142
+ user,
143
+ openaiProvider,
144
+ partyProvider,
145
+ db,
146
+ model,
147
+ }: {
148
+ openaiProvider: LlmProviderConfig
149
+ partyProvider: PartyProviderConfig
150
+ db: ServerAdapter
151
+ resource: ChatResource
152
+ step: any
153
+ workflowTrigger: string
154
+ resourceId: string
155
+ messages: ChatCompletionRequestMessage[]
156
+ user: User
157
+ model?: string
158
+ }) {
159
+ const basicPrompt = {
160
+ id: 'basic',
161
+ name: 'Basic',
162
+ type: 'prompt',
163
+ fields: {
164
+ body: `# Instructions
165
+ Pause. Take a deep breath and think. This is important. Think step by step.
166
+
167
+ You are serving as a writing assistant for a content creator that is publishing a {% if transcript %}video based{% endif %} post for software developers. The content creator will ask questions and expect concise answers that aren't corny or generic.
168
+
169
+ Keep responses scoped to the post and it's direct contents. Do not include additional information.
170
+ Do not include information that is not directly related to the post or the {% if transcript %}video{% endif %}.
171
+
172
+ Use simple language.
173
+
174
+ Use only most popular 2000 english words {% if transcript %}and words used in the transcript{% endif %}.
175
+ Simple is better.
176
+ Flourish is bad.
177
+ **People will hate you if you try to sound clever.**
178
+
179
+ {% if wordLevelSrt %} add screenshots from the video when they are relevant and would be useful to
180
+ the reader using the following template replacing {{timestampInSeconds}} with the time noted in the transcript:
181
+
182
+ ![{{descriptiveAltTextForVisuallyImpaired}}](https://image.mux.com/{{muxPlaybackId}}/thumbnail.png?time={{timestampInSeconds}})
183
+ be precise with the timestamps! {% if wordLevelSrt %} use this word level SRT to get the exact timestamp:
184
+
185
+ word level srt start
186
+ {{wordLevelSrt}} {% endif %}
187
+
188
+ word level srt end
189
+ {% endif %}
190
+
191
+ Get it right and there's $200 cash gratuity in it for you ;)
192
+
193
+ {% if transcript %}The goal is to build a really good written version of the existing video, not edit the video itself. The video is done.
194
+
195
+ The post transcript is the final representation of the video. The post transcript is the source of truth.{% endif %}
196
+
197
+ Keep the language simple and don't use words not in the post {% if transcript %}transcript{% endif %}.
198
+
199
+ {% if transcript %}Do not make direct references to the video.
200
+ Do not make direct references to the transcript.{% endif %}
201
+ Do not make direct references to the content creator.
202
+ Do not make direct references to 'the post'.
203
+ Jump right to the point.
204
+
205
+ Post Type: {{type}}
206
+
207
+ Post Title: {{title}}
208
+
209
+ {% if transcript %}Post Transcript: {{transcript}}{% endif %}
210
+
211
+ Post Body: {{body}}
212
+ `,
213
+ },
214
+ }
215
+
216
+ let prompt =
217
+ workflowTrigger === 'basic'
218
+ ? basicPrompt
219
+ : await step.run('Load Prompt', async () => {
220
+ return db.getContentResource(workflowTrigger)
221
+ })
222
+
223
+ if (!prompt) {
224
+ prompt = basicPrompt
225
+ }
226
+
227
+ let systemPrompt: ChatCompletionRequestMessage = {
228
+ role: 'system',
229
+ content: prompt.fields.body,
230
+ }
231
+ let seedMessages: ChatCompletionRequestMessage[] = []
232
+
233
+ try {
234
+ const actionParsed = z
235
+ .array(
236
+ z.object({
237
+ role: z.enum([
238
+ ChatCompletionRequestMessageRoleEnum.System,
239
+ ChatCompletionRequestMessageRoleEnum.User,
240
+ ChatCompletionRequestMessageRoleEnum.Assistant,
241
+ ChatCompletionRequestMessageRoleEnum.Tool,
242
+ ]),
243
+ content: z.string(),
244
+ }),
245
+ )
246
+ .parse(JSON.parse(prompt.fields.body)) as Array<{
247
+ role: ChatCompletionRequestMessageRoleEnum
248
+ content: string
249
+ }>
250
+
251
+ const actionMessages: ChatCompletionRequestMessage[] = []
252
+ for (const actionMessage of actionParsed) {
253
+ const liquidParsedContent = await step.run(
254
+ 'parse json content',
255
+ async () => {
256
+ const engine = new Liquid()
257
+ const contentString =
258
+ typeof actionMessage.content === 'string'
259
+ ? actionMessage.content
260
+ : JSON.stringify(actionMessage.content)
261
+ return await engine.parseAndRender(contentString, {
262
+ ...resource,
263
+ })
264
+ },
265
+ )
266
+
267
+ actionMessages.push({
268
+ role: actionMessage.role,
269
+ content: liquidParsedContent,
270
+ })
271
+ }
272
+ if (actionMessages.length > 0) {
273
+ ;[
274
+ systemPrompt = {
275
+ role: 'system',
276
+ content: prompt.fields.body,
277
+ },
278
+ ...seedMessages
279
+ ] = actionMessages
280
+ }
281
+ } catch (e: any) {
282
+ // if the prompt action content is not valid json, we assume it's just text
283
+ systemPrompt = await step.run(`parse system prompt`, async () => {
284
+ try {
285
+ const engine = new Liquid()
286
+ const originalSystemPrompt = systemPrompt // Store reference before reassignment
287
+ const contentString =
288
+ typeof originalSystemPrompt.content === 'string'
289
+ ? originalSystemPrompt.content
290
+ : JSON.stringify(originalSystemPrompt.content)
291
+ return {
292
+ role: originalSystemPrompt.role,
293
+ content: await engine.parseAndRender(contentString || '', resource),
294
+ }
295
+ } catch (e: any) {
296
+ logger.error(e instanceof Error ? e : new Error(String(e)), {
297
+ context: 'co-gardener.resource-chat.system-prompt-render',
298
+ resourceId,
299
+ })
300
+ return {
301
+ role: 'system',
302
+ content: prompt.fields.body,
303
+ }
304
+ }
305
+ })
306
+ }
307
+
308
+ if (messages.length <= 2 && systemPrompt) {
309
+ messages = [systemPrompt, ...seedMessages, ...messages]
310
+ }
311
+
312
+ const currentUserMessage = messages[messages.length - 1]
313
+ const currentResourceMetadata = messages[messages.length - 2]
314
+
315
+ messages = await step.run('answer the user prompt', async () => {
316
+ if (!currentUserMessage) {
317
+ throw new Error('No user message')
318
+ }
319
+
320
+ const engine = new Liquid()
321
+ const currentUserContentString =
322
+ typeof currentUserMessage.content === 'string'
323
+ ? currentUserMessage.content
324
+ : JSON.stringify(currentUserMessage.content)
325
+
326
+ currentUserMessage.content = await engine.parseAndRender(
327
+ currentUserContentString ?? '',
328
+ resource,
329
+ )
330
+
331
+ if (currentResourceMetadata) {
332
+ const metadataContentString =
333
+ typeof currentResourceMetadata.content === 'string'
334
+ ? currentResourceMetadata.content
335
+ : JSON.stringify(currentResourceMetadata.content)
336
+ currentResourceMetadata.content = await engine.parseAndRender(
337
+ metadataContentString ?? '',
338
+ resource,
339
+ )
340
+ }
341
+
342
+ return streamingChatPromptExecutor({
343
+ requestId: resourceId,
344
+ promptMessages: messages,
345
+ model: prompt.fields.model || model || 'gpt-4-turbo',
346
+ provider: openaiProvider,
347
+ })
348
+ })
349
+
350
+ await step.run(`partykit broadcast [${resourceId}]`, async () => {
351
+ return await partyProvider.broadcastMessage({
352
+ body: {
353
+ body: messages,
354
+ requestId: resourceId,
355
+ name: 'resource.chat.completed',
356
+ metadata: {
357
+ workflow: workflowTrigger,
358
+ },
359
+ },
360
+ roomId: resourceId,
361
+ })
362
+ })
363
+
364
+ return messages
365
+ }
366
+
367
+ export const resourceChat = {
368
+ config: resourceChatConfig,
369
+ trigger: resourceChatTrigger,
370
+ handler: resourceChatHandler,
371
+ }
@@ -0,0 +1,25 @@
1
+ import { CourseBuilderAction } from '@coursebuilder/core/types'
2
+
3
+ const actions: CourseBuilderAction[] = [
4
+ 'webhook',
5
+ 'srt',
6
+ 'session',
7
+ 'subscribe-to-list',
8
+ 'checkout',
9
+ 'redeem',
10
+ 'prices-formatted',
11
+ 'subscriber',
12
+ 'purchases',
13
+ 'lookup',
14
+ 'claimed',
15
+ 'nameUpdate',
16
+ 'transfer',
17
+ 'refund',
18
+ 'create-magic-link',
19
+ ]
20
+
21
+ export function isCourseBuilderAction(
22
+ action: string,
23
+ ): action is CourseBuilderAction {
24
+ return actions.includes(action as CourseBuilderAction)
25
+ }
@@ -0,0 +1,55 @@
1
+ import type { ServerAdapter } from '../adapter'
2
+ import {
3
+ MissingAdapter,
4
+ MissingAdapterMethods,
5
+ UnsupportedStrategy,
6
+ } from '@coursebuilder/core/errors'
7
+ import { RequestInternal } from '@coursebuilder/core/types'
8
+
9
+ import { CourseBuilderConfig } from '../http'
10
+ import { WarningCode } from './logger'
11
+
12
+ type ConfigError = MissingAdapter | MissingAdapterMethods | UnsupportedStrategy
13
+
14
+ let hasTranscript = false
15
+ let warned = false
16
+
17
+ export function assertConfig(
18
+ request: RequestInternal,
19
+ options: CourseBuilderConfig,
20
+ ): ConfigError | WarningCode[] {
21
+ const { url } = request
22
+ const warnings: WarningCode[] = []
23
+
24
+ if (!warned && options.debug) warnings.push('debug-enabled')
25
+
26
+ // Keep track of webauthn providers that use conditional UI
27
+
28
+ for (const p of options.providers) {
29
+ const provider = typeof p === 'function' ? p() : p
30
+ if (provider.type === 'transcription') {
31
+ hasTranscript = true
32
+ }
33
+ }
34
+
35
+ const { adapter } = options
36
+
37
+ let requiredMethods: (keyof ServerAdapter)[] = []
38
+
39
+ if (hasTranscript) {
40
+ }
41
+
42
+ if (adapter) {
43
+ const missing = requiredMethods.filter((m) => !(m in adapter))
44
+
45
+ if (missing.length) {
46
+ return new MissingAdapterMethods(
47
+ `Required adapter methods were missing: ${missing.join(', ')}`,
48
+ )
49
+ }
50
+ }
51
+
52
+ if (!warned) warned = true
53
+
54
+ return warnings
55
+ }
@@ -0,0 +1,12 @@
1
+ import { CookieOption, CookiesOptions } from '@coursebuilder/core/types'
2
+
3
+ export interface Cookie extends CookieOption {
4
+ value: string
5
+ }
6
+
7
+ export function defaultCookies(useSecureCookies: boolean) {
8
+ const cookiePrefix = useSecureCookies ? '__Secure-' : ''
9
+ return {
10
+ // default cookie options
11
+ } as const satisfies CookiesOptions
12
+ }
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import type { CourseBuilderConfig } from '../http'
4
+ import { setEnvDefaults } from './env'
5
+ import Deepgram from '@coursebuilder/core/providers/deepgram'
6
+
7
+ const testConfig: CourseBuilderConfig = {
8
+ providers: [Deepgram],
9
+ baseUrl: '/coursebuilder',
10
+ authConfig: {},
11
+ }
12
+
13
+ let courseBuilderConfig: CourseBuilderConfig
14
+
15
+ beforeEach(() => {
16
+ courseBuilderConfig = { ...testConfig } // clone
17
+ })
18
+
19
+ describe('config is inferred from environment variables', () => {
20
+ it('providers (client id, client secret, issuer, api key)', () => {
21
+ const env = {
22
+ COURSEBUILDER_DEEPGRAM_API_KEY: 'fdsa',
23
+ }
24
+ setEnvDefaults(env, courseBuilderConfig)
25
+ const [p1] = courseBuilderConfig.providers
26
+ // @ts-expect-error
27
+ expect(p1.apiKey).toBe(env.COURSEBUILDER_DEEPGRAM_API_KEY)
28
+ })
29
+
30
+ it('COURSEBUILDER_URL', () => {
31
+ const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
32
+ setEnvDefaults(env, courseBuilderConfig)
33
+ expect(courseBuilderConfig.basePath).toBe('/api/coursebuilder')
34
+ })
35
+
36
+ it('COURSEBUILDER_URL + prefer config', () => {
37
+ const env = { COURSEBUILDER_URL: 'http://n/api/coursebuilder' }
38
+ const fromConfig = '/basepath-from-config'
39
+ courseBuilderConfig.basePath = fromConfig
40
+ setEnvDefaults(env, courseBuilderConfig)
41
+ expect(courseBuilderConfig.basePath).toBe(fromConfig)
42
+ })
43
+
44
+ it('COURSEBUILDER_URL, but invalid value', () => {
45
+ const env = { COURSEBUILDER_URL: 'secret' }
46
+ setEnvDefaults(env, courseBuilderConfig)
47
+ expect(courseBuilderConfig.basePath).toBe('/coursebuilder')
48
+ })
49
+ })
@@ -0,0 +1,63 @@
1
+ import { CourseBuilderAction } from '@coursebuilder/core/types'
2
+
3
+ import { CourseBuilderConfig } from '../http'
4
+ import { logger } from './logger.js'
5
+
6
+ /** Set default env variables on the config object */
7
+ export function setEnvDefaults(envObject: any, config: CourseBuilderConfig) {
8
+ try {
9
+ const url = envObject.COURSEBUILDER_URL
10
+ if (url && !config.basePath) config.basePath = new URL(url).pathname
11
+ } catch {
12
+ } finally {
13
+ config.basePath ??= `/coursebuilder`
14
+ }
15
+
16
+ config.providers = config.providers.map((p) => {
17
+ const finalProvider = typeof p === 'function' ? p({}) : p
18
+ const ID = finalProvider.id.toUpperCase()
19
+ if (finalProvider.type === 'transcription') {
20
+ finalProvider.apiKey ??= envObject[`COURSEBUILDER_${ID}_API_KEY`]
21
+ }
22
+ return finalProvider
23
+ })
24
+ }
25
+
26
+ export function createActionURL(
27
+ action: CourseBuilderAction,
28
+ protocol: string,
29
+ headers: Headers,
30
+ envObject: any,
31
+ basePath?: string,
32
+ ): URL {
33
+ let envUrl = envObject.COURSEBUILDER_URL
34
+
35
+ let url: URL
36
+ if (envUrl) {
37
+ url = new URL(envUrl)
38
+ if (basePath && basePath !== '/' && url.pathname !== '/') {
39
+ logger.debug(
40
+ url.pathname === basePath
41
+ ? 'env-url-basepath-redundant'
42
+ : 'env-url-basepath-mismatch',
43
+ )
44
+ url.pathname = '/'
45
+ }
46
+ } else {
47
+ const detectedHost = headers.get('x-forwarded-host') ?? headers.get('host')
48
+ const detectedProtocol =
49
+ headers.get('x-forwarded-proto') ?? protocol ?? 'https'
50
+
51
+ url = new URL(`${detectedProtocol}://${detectedHost}`)
52
+ }
53
+
54
+ // remove trailing slash
55
+ const sanitizedUrl = url.toString().replace(/\/$/, '')
56
+
57
+ if (basePath) {
58
+ // remove leading and trailing slash
59
+ const sanitizedBasePath = basePath?.replace(/(^\/|\/$)/g, '') ?? ''
60
+ return new URL(`${sanitizedUrl}/${sanitizedBasePath}/${action}`)
61
+ }
62
+ return new URL(`${sanitizedUrl}/${action}`)
63
+ }
@@ -0,0 +1,112 @@
1
+ import { AdapterError } from '@coursebuilder/core/errors'
2
+ import {
3
+ CallbacksOptions,
4
+ InternalOptions,
5
+ RequestInternal,
6
+ } from '@coursebuilder/core/types'
7
+
8
+ import { CourseBuilderConfig } from '../http'
9
+ import * as cookie from './cookie.js'
10
+ import { logger, LoggerInstance } from './logger'
11
+ import { merge } from './merge'
12
+ import parseProviders from './providers'
13
+
14
+ interface InitParams {
15
+ url: URL
16
+ courseBuilderOptions: CourseBuilderConfig
17
+ providerId?: string
18
+ action: InternalOptions['action']
19
+ isPost: boolean
20
+ cookies: RequestInternal['cookies']
21
+ }
22
+
23
+ export async function init({
24
+ url,
25
+ courseBuilderOptions,
26
+ providerId,
27
+ action,
28
+ isPost,
29
+ cookies: reqCookies,
30
+ }: InitParams): Promise<{
31
+ options: InternalOptions
32
+ cookies: cookie.Cookie[]
33
+ }> {
34
+ const { providers, provider } = parseProviders({
35
+ providers: courseBuilderOptions.providers,
36
+ url,
37
+ providerId,
38
+ options: courseBuilderOptions,
39
+ })
40
+
41
+ const maxAge = 30 * 24 * 60 * 60
42
+
43
+ const options: InternalOptions = {
44
+ debug: false,
45
+ pages: {},
46
+ ...courseBuilderOptions,
47
+ url,
48
+ action,
49
+ // @ts-expect-errors
50
+ provider,
51
+ cookies: merge(
52
+ {},
53
+ cookie.defaultCookies(
54
+ courseBuilderOptions.useSecureCookies ?? url.protocol === 'https:',
55
+ ),
56
+ courseBuilderOptions.cookies || {},
57
+ reqCookies,
58
+ ),
59
+ providers,
60
+ adapter: adapterErrorHandler(courseBuilderOptions.adapter, logger),
61
+ callbacks: { ...defaultCallbacks, ...courseBuilderOptions.callbacks },
62
+ logger,
63
+ getCurrentUser: courseBuilderOptions.getCurrentUser,
64
+ }
65
+
66
+ // Convert request cookies to Cookie[] format for actions
67
+ const cookies: cookie.Cookie[] = Object.entries(reqCookies || {})
68
+ .filter((entry): entry is [string, string] => typeof entry[1] === 'string')
69
+ .map(([name, value]) => ({
70
+ name,
71
+ value,
72
+ options: {},
73
+ }))
74
+
75
+ logger.debug('init.request-cookies', {
76
+ rawCookieKeys: Object.keys(reqCookies || {}),
77
+ convertedCount: cookies.length,
78
+ hasSl_ref: cookies.some((c) => c.name === 'sl_ref'),
79
+ })
80
+
81
+ return { options, cookies }
82
+ }
83
+
84
+ type Method = (...args: any[]) => Promise<any>
85
+
86
+ export const defaultCallbacks: CallbacksOptions = {
87
+ session(payload) {
88
+ return payload
89
+ },
90
+ }
91
+
92
+ function adapterErrorHandler(
93
+ adapter: CourseBuilderConfig['adapter'],
94
+ logger: LoggerInstance,
95
+ ) {
96
+ if (!adapter) return
97
+
98
+ return Object.keys(adapter).reduce<any>((acc, name) => {
99
+ acc[name] = async (...args: any[]) => {
100
+ try {
101
+ logger.debug(`adapter_${name}`, { args })
102
+ const method: Method = adapter[name as keyof Method]
103
+ return await method(...args)
104
+ } catch (e) {
105
+ const error = new AdapterError(e as Error)
106
+ logger.error(error)
107
+ throw error
108
+ }
109
+ }
110
+ return acc
111
+ }, {})
112
+ }
@@ -0,0 +1,6 @@
1
+ export { logger, setLogger } from '@coursebuilder/utils/logger'
2
+ export type {
3
+ LogAttributes,
4
+ LoggerInstance,
5
+ WarningCode,
6
+ } from '@coursebuilder/utils/logger'