@qelos/sdk 4.2.0 → 4.2.1

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.
@@ -0,0 +1,255 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { test } from 'node:test';
4
+ import {
5
+ appendMcpAccessDeniedRedirect,
6
+ buildMcpAuthorizePath,
7
+ buildMcpAuthorizePathFromQuery,
8
+ buildMcpConsentPageUrl,
9
+ buildMcpLoginRedirectUrl,
10
+ buildMcpTokenExchangeBody,
11
+ decodeMcpOAuthStatePayload,
12
+ parseMcpCallbackParams,
13
+ resolveMcpLoginRedirect,
14
+ } from '../src/mcp-auth';
15
+
16
+ function pkceChallenge(verifier: string): string {
17
+ return createHash('sha256').update(verifier).digest('base64url');
18
+ }
19
+
20
+ test('buildMcpAuthorizePath', async (t) => {
21
+ await t.test('builds authorize path with required redirect_uri', () => {
22
+ const path = buildMcpAuthorizePath({
23
+ redirectUri: 'cursor://anysphere.cursor-mcp/oauth/callback',
24
+ });
25
+ assert.match(path, /^\/api\/auth\/mcp\/authorize\?/);
26
+ const parsed = new URL(`https://gateway.example.com${path}`);
27
+ assert.equal(
28
+ parsed.searchParams.get('redirect_uri'),
29
+ 'cursor://anysphere.cursor-mcp/oauth/callback',
30
+ );
31
+ });
32
+
33
+ await t.test('includes optional PKCE and client params', () => {
34
+ const path = buildMcpAuthorizePath({
35
+ redirectUri: 'https://claude.ai/api/mcp/auth_callback',
36
+ state: 'csrf-9',
37
+ codeChallenge: 'abc123',
38
+ codeChallengeMethod: 'S256',
39
+ clientId: 'claude',
40
+ });
41
+ const parsed = new URL(`https://gateway.example.com${path}`);
42
+ assert.equal(parsed.searchParams.get('state'), 'csrf-9');
43
+ assert.equal(parsed.searchParams.get('code_challenge'), 'abc123');
44
+ assert.equal(parsed.searchParams.get('code_challenge_method'), 'S256');
45
+ assert.equal(parsed.searchParams.get('client_id'), 'claude');
46
+ });
47
+ });
48
+
49
+ test('getMcpAuthorizeUrl', async (t) => {
50
+ const { default: QelosSDK } = await import('../src/index');
51
+
52
+ await t.test('returns absolute authorize URL', () => {
53
+ const sdk = new QelosSDK({ appUrl: 'https://gateway.example.com' });
54
+ const url = sdk.authentication.getMcpAuthorizeUrl({
55
+ redirectUri: 'https://claude.ai/api/mcp/auth_callback',
56
+ state: 's1',
57
+ });
58
+ const parsed = new URL(url);
59
+ assert.equal(parsed.origin, 'https://gateway.example.com');
60
+ assert.equal(parsed.pathname, '/api/auth/mcp/authorize');
61
+ assert.equal(parsed.searchParams.get('redirect_uri'), 'https://claude.ai/api/mcp/auth_callback');
62
+ assert.equal(parsed.searchParams.get('state'), 's1');
63
+ });
64
+ });
65
+
66
+ test('parseMcpCallbackParams', async (t) => {
67
+ await t.test('parses code and state from full URL', () => {
68
+ assert.deepEqual(
69
+ parseMcpCallbackParams('https://claude.ai/api/mcp/auth_callback?code=abc&state=xyz'),
70
+ { code: 'abc', state: 'xyz' },
71
+ );
72
+ });
73
+
74
+ await t.test('parses error params from callback URL', () => {
75
+ assert.deepEqual(
76
+ parseMcpCallbackParams('cursor://cb?error=access_denied&state=xyz'),
77
+ { error: 'access_denied', state: 'xyz' },
78
+ );
79
+ });
80
+
81
+ await t.test('parses URL / URLSearchParams / query objects', () => {
82
+ assert.deepEqual(
83
+ parseMcpCallbackParams(new URL('http://_/cb?code=u&state=v')),
84
+ { code: 'u', state: 'v' },
85
+ );
86
+ assert.deepEqual(
87
+ parseMcpCallbackParams(new URLSearchParams('code=w&state=x')),
88
+ { code: 'w', state: 'x' },
89
+ );
90
+ assert.deepEqual(parseMcpCallbackParams({ code: 'y', state: 'z' }), { code: 'y', state: 'z' });
91
+ assert.deepEqual(parseMcpCallbackParams({ error: ['denied'] }), { error: 'denied' });
92
+ });
93
+
94
+ await t.test('parses fragment delivery params', () => {
95
+ assert.deepEqual(
96
+ parseMcpCallbackParams('https://app.example/cb#code=frag-code&state=frag-state'),
97
+ { code: 'frag-code', state: 'frag-state' },
98
+ );
99
+ });
100
+
101
+ await t.test('returns empty object when params missing', () => {
102
+ assert.deepEqual(parseMcpCallbackParams('/no-query'), {});
103
+ assert.deepEqual(parseMcpCallbackParams({}), {});
104
+ });
105
+ });
106
+
107
+ test('exchangeMcpAuthorizationCode', async (t) => {
108
+ const { default: QelosSDK } = await import('../src/index');
109
+
110
+ await t.test('posts PKCE token exchange and returns tokens with user', async () => {
111
+ const codeVerifier = randomBytes(32).toString('base64url');
112
+ const redirectUri = 'https://claude.ai/api/mcp/auth_callback';
113
+ const calls: { url: string; init?: RequestInit }[] = [];
114
+
115
+ const sdk = new QelosSDK({
116
+ appUrl: 'https://gateway.example.com',
117
+ fetch: async (url, init) => {
118
+ calls.push({ url: String(url), init });
119
+ if (String(url).endsWith('/api/auth/mcp/token')) {
120
+ const body = init?.body ? String(init.body) : '';
121
+ const params = new URLSearchParams(body);
122
+ assert.equal(params.get('grant_type'), 'authorization_code');
123
+ assert.equal(params.get('code'), 'auth-code-1');
124
+ assert.equal(params.get('redirect_uri'), redirectUri);
125
+ assert.equal(params.get('code_verifier'), codeVerifier);
126
+ return new Response(
127
+ JSON.stringify({
128
+ access_token: 'at-1',
129
+ refresh_token: 'rt-1',
130
+ token_type: 'Bearer',
131
+ expires_in: 18000,
132
+ }),
133
+ { status: 200, headers: { 'content-type': 'application/json' } },
134
+ );
135
+ }
136
+ if (String(url).endsWith('/api/me')) {
137
+ return new Response(
138
+ JSON.stringify({ _id: 'u1', username: 'jane', email: 'jane@example.com' }),
139
+ { status: 200, headers: { 'content-type': 'application/json' } },
140
+ );
141
+ }
142
+ return new Response('not found', { status: 404 });
143
+ },
144
+ });
145
+
146
+ const result = await sdk.authentication.exchangeMcpAuthorizationCode({
147
+ code: 'auth-code-1',
148
+ redirectUri,
149
+ codeVerifier,
150
+ });
151
+
152
+ assert.equal(result.accessToken, 'at-1');
153
+ assert.equal(result.refreshToken, 'rt-1');
154
+ assert.equal(result.expiresIn, 18000);
155
+ assert.equal(result.user._id, 'u1');
156
+ assert.equal(calls.length, 2);
157
+ assert.match(calls[0]!.url, /\/api\/auth\/mcp\/token$/);
158
+ assert.match(calls[1]!.url, /\/api\/me$/);
159
+ assert.equal(
160
+ (calls[0]!.init?.headers as Record<string, string>)['content-type'],
161
+ 'application/x-www-form-urlencoded',
162
+ );
163
+ });
164
+
165
+ await t.test('buildMcpTokenExchangeBody matches authorize PKCE pair', () => {
166
+ const verifier = randomBytes(32).toString('base64url');
167
+ const challenge = pkceChallenge(verifier);
168
+ const path = buildMcpAuthorizePath({
169
+ redirectUri: 'cursor://anysphere.cursor-mcp/oauth/callback',
170
+ codeChallenge: challenge,
171
+ codeChallengeMethod: 'S256',
172
+ });
173
+ const authorizeParams = new URL(`https://gateway.example.com${path}`).searchParams;
174
+ assert.equal(authorizeParams.get('code_challenge'), challenge);
175
+ assert.equal(authorizeParams.get('code_challenge_method'), 'S256');
176
+
177
+ const body = buildMcpTokenExchangeBody({
178
+ code: 'c1',
179
+ redirectUri: 'cursor://anysphere.cursor-mcp/oauth/callback',
180
+ codeVerifier: verifier,
181
+ });
182
+ assert.equal(body.get('code_verifier'), verifier);
183
+ });
184
+ });
185
+
186
+ test('mcp authorize page helpers', async (t) => {
187
+ await t.test('buildMcpLoginRedirectUrl encodes authorize path', () => {
188
+ const authorizePath = '/api/auth/mcp/authorize?redirect_uri=https%3A%2F%2Fclaude.ai';
189
+ assert.equal(
190
+ buildMcpLoginRedirectUrl(authorizePath),
191
+ `/login?redirect=${encodeURIComponent(authorizePath)}`,
192
+ );
193
+ });
194
+
195
+ await t.test('buildMcpConsentPageUrl encodes mcp_state', () => {
196
+ assert.equal(
197
+ buildMcpConsentPageUrl('signed.jwt.state'),
198
+ `/mcp/authorize?mcp_state=${encodeURIComponent('signed.jwt.state')}`,
199
+ );
200
+ });
201
+
202
+ await t.test('buildMcpAuthorizePathFromQuery mirrors authorize params', () => {
203
+ const path = buildMcpAuthorizePathFromQuery({
204
+ redirect_uri: 'cursor://anysphere.cursor-mcp/oauth/callback',
205
+ state: 's1',
206
+ code_challenge: 'cc',
207
+ code_challenge_method: 'S256',
208
+ client_id: 'cursor',
209
+ });
210
+ assert.ok(path);
211
+ const parsed = new URL(`https://gateway.example.com${path}`);
212
+ assert.equal(parsed.searchParams.get('redirect_uri'), 'cursor://anysphere.cursor-mcp/oauth/callback');
213
+ assert.equal(parsed.searchParams.get('state'), 's1');
214
+ assert.equal(parsed.searchParams.get('code_challenge'), 'cc');
215
+ assert.equal(parsed.searchParams.get('code_challenge_method'), 'S256');
216
+ assert.equal(parsed.searchParams.get('client_id'), 'cursor');
217
+ });
218
+
219
+ await t.test('resolveMcpLoginRedirect prefers redirect, then mcp_state, then authorize query', () => {
220
+ assert.equal(
221
+ resolveMcpLoginRedirect({ redirect: '/api/auth/mcp/authorize?redirect_uri=x' }),
222
+ '/api/auth/mcp/authorize?redirect_uri=x',
223
+ );
224
+ assert.equal(
225
+ resolveMcpLoginRedirect({ mcp_state: 'jwt' }),
226
+ `/mcp/authorize?mcp_state=${encodeURIComponent('jwt')}`,
227
+ );
228
+ assert.equal(
229
+ resolveMcpLoginRedirect({ redirect_uri: 'https://claude.ai/cb' }),
230
+ '/api/auth/mcp/authorize?redirect_uri=https%3A%2F%2Fclaude.ai%2Fcb',
231
+ );
232
+ assert.equal(resolveMcpLoginRedirect({}), null);
233
+ });
234
+
235
+ await t.test('appendMcpAccessDeniedRedirect adds error and state', () => {
236
+ assert.equal(
237
+ appendMcpAccessDeniedRedirect('cursor://cb?foo=1', 'oauth-state'),
238
+ 'cursor://cb?foo=1&error=access_denied&state=oauth-state',
239
+ );
240
+ });
241
+
242
+ await t.test('decodeMcpOAuthStatePayload decodes JWT payload without verification', () => {
243
+ const payload = { ru: 'cursor://cb', s: 'oauth-state', t: 'tenant-1', cid: 'cursor' };
244
+ const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
245
+ const token = `header.${encoded}.signature`;
246
+
247
+ assert.deepEqual(decodeMcpOAuthStatePayload(token), {
248
+ redirectUri: 'cursor://cb',
249
+ state: 'oauth-state',
250
+ tenant: 'tenant-1',
251
+ clientId: 'cursor',
252
+ });
253
+ assert.equal(decodeMcpOAuthStatePayload('not-a-jwt'), null);
254
+ });
255
+ });
@@ -0,0 +1,65 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { test } from 'node:test';
3
+ import QlPayments from '../src/payments';
4
+ import { QelosSDKOptions } from '../src/types';
5
+
6
+ function jsonResponse(body: unknown) {
7
+ const res = new Response(JSON.stringify(body));
8
+ res.headers.set('Content-Type', 'application/json');
9
+ return res;
10
+ }
11
+
12
+ test('QlPayments', async (t) => {
13
+ await t.test('checkout serializes the customer object into the POST body', async () => {
14
+ let init: RequestInit | undefined;
15
+ const options: QelosSDKOptions = {
16
+ appUrl: 'http://localhost:3000',
17
+ fetch: async (_url, requestInit) => {
18
+ init = requestInit;
19
+ return jsonResponse({ subscriptionId: 'sub-1', checkoutUrl: 'https://sumit.example/checkout' });
20
+ },
21
+ };
22
+ const payments = new QlPayments(options);
23
+ await payments.checkout({
24
+ planId: 'plan-1',
25
+ billingCycle: 'monthly',
26
+ successUrl: 'https://app.example/success',
27
+ cancelUrl: 'https://app.example/cancel',
28
+ customer: {
29
+ name: 'Jane Doe',
30
+ nameForInvoice: 'Acme Inc.',
31
+ email: 'jane@acme.example',
32
+ phone: '+1-555-0100',
33
+ address: '1 Main St',
34
+ city: 'Springfield',
35
+ },
36
+ });
37
+
38
+ assert.equal(init?.method, 'post');
39
+ const body = JSON.parse(init?.body as string);
40
+ assert.deepEqual(body.customer, {
41
+ name: 'Jane Doe',
42
+ nameForInvoice: 'Acme Inc.',
43
+ email: 'jane@acme.example',
44
+ phone: '+1-555-0100',
45
+ address: '1 Main St',
46
+ city: 'Springfield',
47
+ });
48
+ });
49
+
50
+ await t.test('checkout omits customer when not provided', async () => {
51
+ let init: RequestInit | undefined;
52
+ const options: QelosSDKOptions = {
53
+ appUrl: 'http://localhost:3000',
54
+ fetch: async (_url, requestInit) => {
55
+ init = requestInit;
56
+ return jsonResponse({ subscriptionId: 'sub-1' });
57
+ },
58
+ };
59
+ const payments = new QlPayments(options);
60
+ await payments.checkout({ planId: 'plan-1', billingCycle: 'monthly' });
61
+
62
+ const body = JSON.parse(init?.body as string);
63
+ assert.equal('customer' in body, false);
64
+ });
65
+ });