@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,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 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -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 @@
|
|
|
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
|
+
}
|