@qelos/sdk 4.2.0 → 4.2.2
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/.gitignore +4 -0
- package/.npmignore +3 -0
- package/README.md +81 -0
- package/dist/administrator/integrations.d.ts +2 -0
- package/dist/administrator/integrations.js.map +1 -1
- package/dist/administrator/payments.d.ts +7 -1
- package/dist/administrator/payments.js.map +1 -1
- package/dist/authentication.d.ts +5 -0
- package/dist/authentication.js +39 -0
- package/dist/authentication.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-auth.d.ts +76 -0
- package/dist/mcp-auth.js +216 -0
- package/dist/mcp-auth.js.map +1 -0
- package/dist/payments.d.ts +7 -1
- package/dist/payments.js.map +1 -1
- package/package.json +2 -2
- package/src/administrator/integrations.ts +2 -0
- package/src/administrator/payments.ts +7 -1
- package/src/authentication.ts +64 -0
- package/src/index.ts +20 -0
- package/src/mcp-auth.ts +293 -0
- package/src/payments.ts +7 -1
- package/test/mcp-auth.test.ts +255 -0
- package/test/payments.test.ts +65 -0
package/dist/payments.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payments.js","sourceRoot":"","sources":["../src/payments.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"payments.js","sourceRoot":"","sources":["../src/payments.ts"],"names":[],"mappings":"AACA,OAAO,OAAO,MAAM,YAAY,CAAC;AAsCjC,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,OAAO;IACrC,YAAY,GAAG,MAAM,CAAC;IAE9B,YAAY,OAAwB;QAClC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjB,CAAC;IAED,QAAQ,CAAC,KAA8B;QACrC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,eAAe,CAAC,KAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,OAAO,IAAI,CAAC,WAAW,CAAU,GAAG,IAAI,CAAC,YAAY,gBAAgB,EAAE,EAAE,CAAC,CAAC;IAC7E,CAAC;IAED,OAAO,CAAC,MAAc;QACpB,OAAO,IAAI,CAAC,WAAW,CAAQ,GAAG,IAAI,CAAC,YAAY,UAAU,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED;;;;;OAKG;IACH,eAAe,CAAC,MAAc,EAAE,YAA0B,EAAE,UAAmB;QAC7E,OAAO,IAAI,CAAC,WAAW,CACrB,GAAG,IAAI,CAAC,YAAY,gBAAgB,EACpC;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;SAC3D,CACF,CAAC;IACJ,CAAC;IAED;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAuB;QAC9B,OAAO,IAAI,CAAC,WAAW,CACrB,GAAG,IAAI,CAAC,YAAY,WAAW,EAC/B;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAC7B,CACF,CAAC;IACJ,CAAC;IAED,iBAAiB;QACf,OAAO,IAAI,CAAC,WAAW,CAAgB,GAAG,IAAI,CAAC,YAAY,mBAAmB,CAAC,CAAC;IAClF,CAAC;IAED,kBAAkB,CAAC,cAAsB;QACvC,OAAO,IAAI,CAAC,WAAW,CACrB,GAAG,IAAI,CAAC,YAAY,kBAAkB,cAAc,SAAS,EAC7D,EAAE,MAAM,EAAE,KAAK,EAAE,CAClB,CAAC;IACJ,CAAC;IAED,WAAW,CAAC,KAA2B;QACrC,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,eAAe,CAAC,KAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,OAAO,IAAI,CAAC,WAAW,CAAa,GAAG,IAAI,CAAC,YAAY,YAAY,EAAE,EAAE,CAAC,CAAC;IAC5E,CAAC;IAED,UAAU,CAAC,SAAiB;QAC1B,OAAO,IAAI,CAAC,WAAW,CAAW,GAAG,IAAI,CAAC,YAAY,aAAa,SAAS,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,cAAc,CAAC,IAAY,EAAE,MAAe;QAC1C,OAAO,IAAI,CAAC,WAAW,CACrB,GAAG,IAAI,CAAC,YAAY,mBAAmB,EACvC;YACE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;SACvC,CACF,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/sdk",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "SDK for Qelos API",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"access": "public"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@qelos/global-types": "^4.2.
|
|
105
|
+
"@qelos/global-types": "^4.2.2"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
108
|
"tsx": "^4.21.0",
|
|
@@ -38,6 +38,7 @@ export default class QlIntegrations extends BaseSDK {
|
|
|
38
38
|
trigger?: any;
|
|
39
39
|
target?: any;
|
|
40
40
|
dataManipulation?: any;
|
|
41
|
+
targetManipulation?: any;
|
|
41
42
|
}): Promise<IIntegration> {
|
|
42
43
|
return this.callJsonApi<IIntegration>(
|
|
43
44
|
`${this.relativePath}/${integrationId}`,
|
|
@@ -53,6 +54,7 @@ export default class QlIntegrations extends BaseSDK {
|
|
|
53
54
|
trigger: any;
|
|
54
55
|
target: any;
|
|
55
56
|
dataManipulation?: any;
|
|
57
|
+
targetManipulation?: any;
|
|
56
58
|
active?: boolean;
|
|
57
59
|
}): Promise<IIntegration> {
|
|
58
60
|
return this.callJsonApi<IIntegration>(this.relativePath, {
|
|
@@ -3,7 +3,7 @@ import BaseSDK from '../base-sdk';
|
|
|
3
3
|
import {
|
|
4
4
|
IPlan, ISubscription, IInvoice, ICoupon,
|
|
5
5
|
BillableEntityType, SubscriptionStatus, InvoiceStatus, BillingCycle,
|
|
6
|
-
IPaymentsConfigurationMetadata,
|
|
6
|
+
IPaymentsConfigurationMetadata, CheckoutCustomer,
|
|
7
7
|
} from '@qelos/global-types';
|
|
8
8
|
|
|
9
9
|
export interface PaymentsConfigurationRecord {
|
|
@@ -28,6 +28,12 @@ export interface AdminCheckoutRequest {
|
|
|
28
28
|
* use `setSubscriptionDynamicAmount()` + a separate `checkout()` call instead.
|
|
29
29
|
*/
|
|
30
30
|
amount?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Real-world identity of the billable entity. Passed through to the payment
|
|
33
|
+
* provider (e.g. Sumit) so it can create a properly-named customer record
|
|
34
|
+
* instead of falling back to the raw billable entity ID.
|
|
35
|
+
*/
|
|
36
|
+
customer?: CheckoutCustomer;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
export interface CreateSubscriptionData {
|
package/src/authentication.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { QelosSDKOptions } from './types';
|
|
2
2
|
import BaseSDK from './base-sdk';
|
|
3
3
|
import { decodeMeUser } from './services/decode-uri-fields';
|
|
4
|
+
import {
|
|
5
|
+
buildMcpAuthorizePath,
|
|
6
|
+
buildMcpTokenExchangeBody,
|
|
7
|
+
type McpAuthorizeOptions,
|
|
8
|
+
type McpOAuthTokenResponse,
|
|
9
|
+
type McpTokenExchangeOptions,
|
|
10
|
+
type McpTokenExchangeResult,
|
|
11
|
+
} from './mcp-auth';
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
McpAuthorizeOptions,
|
|
15
|
+
McpCallbackInput,
|
|
16
|
+
McpCallbackParams,
|
|
17
|
+
McpTokenExchangeOptions,
|
|
18
|
+
McpTokenExchangeResult,
|
|
19
|
+
DecodedMcpOAuthStatePayload,
|
|
20
|
+
McpPagePathOptions,
|
|
21
|
+
} from './mcp-auth';
|
|
22
|
+
|
|
23
|
+
export { parseMcpCallbackParams } from './mcp-auth';
|
|
4
24
|
|
|
5
25
|
export interface IUser {
|
|
6
26
|
_id: string;
|
|
@@ -376,6 +396,50 @@ export default class QlAuthentication extends BaseSDK {
|
|
|
376
396
|
return this.exchangeAuthCallback(refreshToken);
|
|
377
397
|
}
|
|
378
398
|
|
|
399
|
+
getMcpAuthorizeUrl(options: McpAuthorizeOptions): string {
|
|
400
|
+
if (!options.redirectUri) {
|
|
401
|
+
throw new Error('redirectUri is required');
|
|
402
|
+
}
|
|
403
|
+
return this.buildUrl(buildMcpAuthorizePath(options));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async exchangeMcpAuthorizationCode(options: McpTokenExchangeOptions): Promise<McpTokenExchangeResult> {
|
|
407
|
+
const { code, redirectUri, codeVerifier } = options;
|
|
408
|
+
if (!code || !redirectUri || !codeVerifier) {
|
|
409
|
+
throw new Error('code, redirectUri, and codeVerifier are required');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const res = await this.callApi('/api/auth/mcp/token', {
|
|
413
|
+
method: 'post',
|
|
414
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
415
|
+
body: buildMcpTokenExchangeBody(options).toString(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (!res.ok) {
|
|
419
|
+
let detail = '';
|
|
420
|
+
try {
|
|
421
|
+
detail = (await res.text()).slice(0, 512);
|
|
422
|
+
} catch {
|
|
423
|
+
/* ignore */
|
|
424
|
+
}
|
|
425
|
+
throw new Error(
|
|
426
|
+
`failed to exchange MCP authorization code: HTTP ${res.status}${detail ? ` — ${detail}` : ''}`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const body = (await res.json()) as McpOAuthTokenResponse;
|
|
431
|
+
this.#accessToken = body.access_token;
|
|
432
|
+
this.#refreshToken = body.refresh_token;
|
|
433
|
+
|
|
434
|
+
const user = await this.getLoggedInUser();
|
|
435
|
+
return {
|
|
436
|
+
accessToken: body.access_token,
|
|
437
|
+
refreshToken: body.refresh_token,
|
|
438
|
+
expiresIn: body.expires_in,
|
|
439
|
+
user,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
379
443
|
getLoggedInUser() {
|
|
380
444
|
return this.callJsonApi<IUser>('/api/me').then(decodeMeUser);
|
|
381
445
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ const noExtraHeadersUrls = new Set([
|
|
|
16
16
|
'/api/signin',
|
|
17
17
|
'/api/signup',
|
|
18
18
|
'/api/auth/callback',
|
|
19
|
+
'/api/auth/mcp/token',
|
|
19
20
|
])
|
|
20
21
|
|
|
21
22
|
export type {
|
|
@@ -23,6 +24,13 @@ export type {
|
|
|
23
24
|
SocialAuthCallbackPayload,
|
|
24
25
|
SocialProvider,
|
|
25
26
|
SocialLoginOptions,
|
|
27
|
+
McpAuthorizeOptions,
|
|
28
|
+
McpCallbackInput,
|
|
29
|
+
McpCallbackParams,
|
|
30
|
+
McpTokenExchangeOptions,
|
|
31
|
+
McpTokenExchangeResult,
|
|
32
|
+
DecodedMcpOAuthStatePayload,
|
|
33
|
+
McpPagePathOptions,
|
|
26
34
|
} from './authentication';
|
|
27
35
|
|
|
28
36
|
export type { BlueprintEntitiesRegistry } from './blueprints';
|
|
@@ -31,8 +39,20 @@ export {
|
|
|
31
39
|
parseSocialCallbackRefreshToken,
|
|
32
40
|
getSocialAuthSetCookieParts,
|
|
33
41
|
applySocialAuthCookiesToServerResponse,
|
|
42
|
+
parseMcpCallbackParams,
|
|
34
43
|
} from './authentication';
|
|
35
44
|
|
|
45
|
+
export {
|
|
46
|
+
buildMcpAuthorizePath,
|
|
47
|
+
buildMcpAuthorizePathFromQuery,
|
|
48
|
+
buildMcpLoginRedirectUrl,
|
|
49
|
+
buildMcpConsentPageUrl,
|
|
50
|
+
resolveMcpLoginRedirect,
|
|
51
|
+
appendMcpAccessDeniedRedirect,
|
|
52
|
+
decodeMcpOAuthStatePayload,
|
|
53
|
+
buildMcpTokenExchangeBody,
|
|
54
|
+
} from './mcp-auth';
|
|
55
|
+
|
|
36
56
|
export default class QelosSDK extends BaseSDK {
|
|
37
57
|
#customHeaders = {}
|
|
38
58
|
|
package/src/mcp-auth.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import type { IUser } from './authentication';
|
|
2
|
+
|
|
3
|
+
export interface McpAuthorizeOptions {
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
state?: string;
|
|
6
|
+
codeChallenge?: string;
|
|
7
|
+
codeChallengeMethod?: string;
|
|
8
|
+
clientId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpTokenExchangeOptions {
|
|
12
|
+
code: string;
|
|
13
|
+
redirectUri: string;
|
|
14
|
+
codeVerifier: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface McpTokenExchangeResult {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
refreshToken: string;
|
|
20
|
+
expiresIn: number;
|
|
21
|
+
user: IUser;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Callback URL or query bag from an MCP OAuth redirect. */
|
|
25
|
+
export type McpCallbackInput =
|
|
26
|
+
| string
|
|
27
|
+
| URL
|
|
28
|
+
| URLSearchParams
|
|
29
|
+
| Record<string, string | string[] | undefined>;
|
|
30
|
+
|
|
31
|
+
export interface McpCallbackParams {
|
|
32
|
+
code?: string;
|
|
33
|
+
state?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
errorDescription?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Decoded MCP OAuth state JWT payload (display only — not verified). */
|
|
39
|
+
export interface DecodedMcpOAuthStatePayload {
|
|
40
|
+
redirectUri?: string;
|
|
41
|
+
state?: string;
|
|
42
|
+
codeChallenge?: string;
|
|
43
|
+
codeChallengeMethod?: string;
|
|
44
|
+
tenant?: string;
|
|
45
|
+
clientId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface McpPagePathOptions {
|
|
49
|
+
loginPath?: string;
|
|
50
|
+
consentPath?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readQueryValue(
|
|
54
|
+
input: Record<string, string | string[] | undefined>,
|
|
55
|
+
key: string,
|
|
56
|
+
): string | undefined {
|
|
57
|
+
const raw = input[key];
|
|
58
|
+
if (Array.isArray(raw)) {
|
|
59
|
+
return raw[0] && typeof raw[0] === 'string' ? raw[0] : undefined;
|
|
60
|
+
}
|
|
61
|
+
return typeof raw === 'string' ? raw : undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function searchParamsFromString(input: string): URLSearchParams | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const url = new URL(input);
|
|
67
|
+
if (url.search) {
|
|
68
|
+
return url.searchParams;
|
|
69
|
+
}
|
|
70
|
+
if (url.hash.length > 1) {
|
|
71
|
+
return new URLSearchParams(url.hash.slice(1));
|
|
72
|
+
}
|
|
73
|
+
return url.searchParams;
|
|
74
|
+
} catch {
|
|
75
|
+
const trimmed = input.trim();
|
|
76
|
+
if (!trimmed) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const queryPart = trimmed.startsWith('?')
|
|
80
|
+
? trimmed.slice(1)
|
|
81
|
+
: trimmed.startsWith('#')
|
|
82
|
+
? trimmed.slice(1)
|
|
83
|
+
: trimmed.includes('=')
|
|
84
|
+
? trimmed
|
|
85
|
+
: undefined;
|
|
86
|
+
return queryPart ? new URLSearchParams(queryPart) : undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function searchParamsFromInput(input: McpCallbackInput): URLSearchParams | undefined {
|
|
91
|
+
if (input instanceof URLSearchParams) {
|
|
92
|
+
return input;
|
|
93
|
+
}
|
|
94
|
+
if (input instanceof URL) {
|
|
95
|
+
if (input.search.length > 1) {
|
|
96
|
+
return input.searchParams;
|
|
97
|
+
}
|
|
98
|
+
if (input.hash.length > 1) {
|
|
99
|
+
return new URLSearchParams(input.hash.slice(1));
|
|
100
|
+
}
|
|
101
|
+
return input.searchParams;
|
|
102
|
+
}
|
|
103
|
+
if (typeof input === 'string') {
|
|
104
|
+
return searchParamsFromString(input);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const params = new URLSearchParams();
|
|
108
|
+
const code = readQueryValue(input, 'code');
|
|
109
|
+
const state = readQueryValue(input, 'state');
|
|
110
|
+
const error = readQueryValue(input, 'error');
|
|
111
|
+
const errorDescription = readQueryValue(input, 'error_description');
|
|
112
|
+
if (code) params.set('code', code);
|
|
113
|
+
if (state) params.set('state', state);
|
|
114
|
+
if (error) params.set('error', error);
|
|
115
|
+
if (errorDescription) params.set('error_description', errorDescription);
|
|
116
|
+
return params.toString() ? params : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Parse authorization callback parameters from an MCP OAuth redirect.
|
|
121
|
+
* Accepts a full URL, {@link URL}, `URLSearchParams`, a query/fragment string,
|
|
122
|
+
* or a query object such as `req.query` from Next.js or Express.
|
|
123
|
+
*/
|
|
124
|
+
export function parseMcpCallbackParams(input: McpCallbackInput): McpCallbackParams {
|
|
125
|
+
const params = searchParamsFromInput(input);
|
|
126
|
+
if (!params) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result: McpCallbackParams = {};
|
|
131
|
+
const code = params.get('code');
|
|
132
|
+
const state = params.get('state');
|
|
133
|
+
const error = params.get('error');
|
|
134
|
+
const errorDescription = params.get('error_description');
|
|
135
|
+
if (code) result.code = code;
|
|
136
|
+
if (state) result.state = state;
|
|
137
|
+
if (error) result.error = error;
|
|
138
|
+
if (errorDescription) result.errorDescription = errorDescription;
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Relative authorize path with query string for {@link QlAuthentication.getMcpAuthorizeUrl}. */
|
|
143
|
+
export function buildMcpAuthorizePath(options: McpAuthorizeOptions): string {
|
|
144
|
+
const search = new URLSearchParams({ redirect_uri: options.redirectUri });
|
|
145
|
+
if (options.state) search.set('state', options.state);
|
|
146
|
+
if (options.codeChallenge) search.set('code_challenge', options.codeChallenge);
|
|
147
|
+
if (options.codeChallengeMethod) search.set('code_challenge_method', options.codeChallengeMethod);
|
|
148
|
+
if (options.clientId) search.set('client_id', options.clientId);
|
|
149
|
+
return `/api/auth/mcp/authorize?${search.toString()}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build `/api/auth/mcp/authorize?…` from OAuth authorize query params
|
|
154
|
+
* (e.g. when `/mcp/authorize` receives a direct client redirect).
|
|
155
|
+
*/
|
|
156
|
+
export function buildMcpAuthorizePathFromQuery(
|
|
157
|
+
query: Record<string, string | string[] | undefined>,
|
|
158
|
+
): string | null {
|
|
159
|
+
const redirectUri = readQueryValue(query, 'redirect_uri');
|
|
160
|
+
if (!redirectUri) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const options: McpAuthorizeOptions = { redirectUri };
|
|
165
|
+
const state = readQueryValue(query, 'state');
|
|
166
|
+
const codeChallenge = readQueryValue(query, 'code_challenge');
|
|
167
|
+
const codeChallengeMethod = readQueryValue(query, 'code_challenge_method');
|
|
168
|
+
const clientId = readQueryValue(query, 'client_id');
|
|
169
|
+
if (state) options.state = state;
|
|
170
|
+
if (codeChallenge) options.codeChallenge = codeChallenge;
|
|
171
|
+
if (codeChallengeMethod) options.codeChallengeMethod = codeChallengeMethod;
|
|
172
|
+
if (clientId) options.clientId = clientId;
|
|
173
|
+
return buildMcpAuthorizePath(options);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Login redirect used by Qelos after an unauthenticated MCP authorize request. */
|
|
177
|
+
export function buildMcpLoginRedirectUrl(
|
|
178
|
+
authorizePath: string,
|
|
179
|
+
options?: Pick<McpPagePathOptions, 'loginPath'>,
|
|
180
|
+
): string {
|
|
181
|
+
const loginPath = options?.loginPath ?? '/login';
|
|
182
|
+
return `${loginPath}?redirect=${encodeURIComponent(authorizePath)}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Consent page URL issued after the user signs in during MCP OAuth. */
|
|
186
|
+
export function buildMcpConsentPageUrl(
|
|
187
|
+
mcpState: string,
|
|
188
|
+
options?: Pick<McpPagePathOptions, 'consentPath'>,
|
|
189
|
+
): string {
|
|
190
|
+
const consentPath = options?.consentPath ?? '/mcp/authorize';
|
|
191
|
+
return `${consentPath}?mcp_state=${encodeURIComponent(mcpState)}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolve the post-login redirect target from login-page query params.
|
|
196
|
+
* Matches the resume logic used by the Qelos admin login flow.
|
|
197
|
+
*/
|
|
198
|
+
export function resolveMcpLoginRedirect(
|
|
199
|
+
query: Record<string, string | string[] | undefined>,
|
|
200
|
+
options?: McpPagePathOptions,
|
|
201
|
+
): string | null {
|
|
202
|
+
const redirect = readQueryValue(query, 'redirect');
|
|
203
|
+
if (redirect) {
|
|
204
|
+
return redirect;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const mcpState = readQueryValue(query, 'mcp_state');
|
|
208
|
+
if (mcpState) {
|
|
209
|
+
return buildMcpConsentPageUrl(mcpState, options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return buildMcpAuthorizePathFromQuery(query);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Redirect URI with `error=access_denied` for MCP consent denial. */
|
|
216
|
+
export function appendMcpAccessDeniedRedirect(
|
|
217
|
+
redirectUri: string,
|
|
218
|
+
oauthState?: string | null,
|
|
219
|
+
): string {
|
|
220
|
+
try {
|
|
221
|
+
const url = new URL(redirectUri);
|
|
222
|
+
url.searchParams.set('error', 'access_denied');
|
|
223
|
+
if (oauthState) {
|
|
224
|
+
url.searchParams.set('state', oauthState);
|
|
225
|
+
}
|
|
226
|
+
return url.toString();
|
|
227
|
+
} catch {
|
|
228
|
+
const separator = redirectUri.includes('?') ? '&' : '?';
|
|
229
|
+
let result = `${redirectUri}${separator}error=access_denied`;
|
|
230
|
+
if (oauthState) {
|
|
231
|
+
result += `&state=${encodeURIComponent(oauthState)}`;
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Decode the signed `mcp_state` JWT payload for display on a consent page.
|
|
239
|
+
* Does not verify the signature — use only for UI labels, not authorization.
|
|
240
|
+
*/
|
|
241
|
+
export function decodeMcpOAuthStatePayload(mcpState: string): DecodedMcpOAuthStatePayload | null {
|
|
242
|
+
if (!mcpState) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const parts = mcpState.split('.');
|
|
248
|
+
if (parts.length !== 3 || !parts[1]) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const base64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
253
|
+
const json =
|
|
254
|
+
typeof globalThis.atob === 'function'
|
|
255
|
+
? globalThis.atob(base64)
|
|
256
|
+
: Buffer.from(base64, 'base64').toString('utf8');
|
|
257
|
+
const decoded = JSON.parse(json) as {
|
|
258
|
+
ru?: string;
|
|
259
|
+
s?: string;
|
|
260
|
+
cc?: string;
|
|
261
|
+
ccm?: string;
|
|
262
|
+
t?: string;
|
|
263
|
+
cid?: string;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const payload: DecodedMcpOAuthStatePayload = {};
|
|
267
|
+
if (decoded.ru) payload.redirectUri = decoded.ru;
|
|
268
|
+
if (decoded.s) payload.state = decoded.s;
|
|
269
|
+
if (decoded.cc) payload.codeChallenge = decoded.cc;
|
|
270
|
+
if (decoded.ccm) payload.codeChallengeMethod = decoded.ccm;
|
|
271
|
+
if (decoded.t) payload.tenant = decoded.t;
|
|
272
|
+
if (decoded.cid) payload.clientId = decoded.cid;
|
|
273
|
+
return payload.redirectUri ? payload : null;
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface McpOAuthTokenResponse {
|
|
280
|
+
access_token: string;
|
|
281
|
+
refresh_token: string;
|
|
282
|
+
token_type: string;
|
|
283
|
+
expires_in: number;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function buildMcpTokenExchangeBody(options: McpTokenExchangeOptions): URLSearchParams {
|
|
287
|
+
return new URLSearchParams({
|
|
288
|
+
grant_type: 'authorization_code',
|
|
289
|
+
code: options.code,
|
|
290
|
+
redirect_uri: options.redirectUri,
|
|
291
|
+
code_verifier: options.codeVerifier,
|
|
292
|
+
});
|
|
293
|
+
}
|
package/src/payments.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { QelosSDKOptions } from './types';
|
|
2
2
|
import BaseSDK from './base-sdk';
|
|
3
|
-
import { IPlan, ISubscription, IInvoice, ICoupon, BillingCycle } from '@qelos/global-types';
|
|
3
|
+
import { IPlan, ISubscription, IInvoice, ICoupon, BillingCycle, CheckoutCustomer } from '@qelos/global-types';
|
|
4
4
|
|
|
5
5
|
export interface CheckoutRequest {
|
|
6
6
|
/** Required when not providing `subscriptionId`. Must be a non-dynamic plan. */
|
|
@@ -10,6 +10,12 @@ export interface CheckoutRequest {
|
|
|
10
10
|
couponCode?: string;
|
|
11
11
|
successUrl?: string;
|
|
12
12
|
cancelUrl?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Real-world identity of the billable entity. Passed through to the payment
|
|
15
|
+
* provider (e.g. Sumit) so it can create a properly-named customer record
|
|
16
|
+
* instead of falling back to the raw billable entity ID.
|
|
17
|
+
*/
|
|
18
|
+
customer?: CheckoutCustomer;
|
|
13
19
|
/**
|
|
14
20
|
* When true, cancels any existing active or trialing subscription for the
|
|
15
21
|
* authenticated billable entity before starting checkout (e.g. plan change).
|