@honeyfield/rent2b-mcp 1.2.0 → 1.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.
- package/dist/api-client.js +14 -5
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.d.ts +1 -0
- package/dist/api-client.test.js +41 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/auth/authorize-page.d.ts +18 -0
- package/dist/auth/authorize-page.js +60 -0
- package/dist/auth/authorize-page.js.map +1 -0
- package/dist/auth/authorize-page.test.d.ts +1 -0
- package/dist/auth/authorize-page.test.js +37 -0
- package/dist/auth/authorize-page.test.js.map +1 -0
- package/dist/auth/http-oauth.d.ts +28 -0
- package/dist/auth/http-oauth.js +93 -0
- package/dist/auth/http-oauth.js.map +1 -0
- package/dist/auth/oauth-config.d.ts +6 -0
- package/dist/auth/oauth-config.js +16 -0
- package/dist/auth/oauth-config.js.map +1 -0
- package/dist/auth/oauth-config.test.d.ts +1 -0
- package/dist/auth/oauth-config.test.js +25 -0
- package/dist/auth/oauth-config.test.js.map +1 -0
- package/dist/auth/provider.d.ts +28 -0
- package/dist/auth/provider.js +127 -0
- package/dist/auth/provider.js.map +1 -0
- package/dist/auth/provider.test.d.ts +1 -0
- package/dist/auth/provider.test.js +80 -0
- package/dist/auth/provider.test.js.map +1 -0
- package/dist/auth/stores.d.ts +27 -0
- package/dist/auth/stores.js +39 -0
- package/dist/auth/stores.js.map +1 -0
- package/dist/auth/stores.test.d.ts +1 -0
- package/dist/auth/stores.test.js +41 -0
- package/dist/auth/stores.test.js.map +1 -0
- package/dist/auth/token-crypto.d.ts +14 -0
- package/dist/auth/token-crypto.js +50 -0
- package/dist/auth/token-crypto.js.map +1 -0
- package/dist/auth/token-crypto.test.d.ts +1 -0
- package/dist/auth/token-crypto.test.js +40 -0
- package/dist/auth/token-crypto.test.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js.map +1 -1
- package/dist/generator/openapi-parser.js +21 -0
- package/dist/generator/openapi-parser.js.map +1 -1
- package/dist/generator/tool-generator.js +19 -1
- package/dist/generator/tool-generator.js.map +1 -1
- package/dist/generator/tool-generator.test.d.ts +1 -0
- package/dist/generator/tool-generator.test.js +35 -0
- package/dist/generator/tool-generator.test.js.map +1 -0
- package/dist/index.js +85 -60
- package/dist/index.js.map +1 -1
- package/dist/overrides/tool-overrides.json +60 -0
- package/generated/tools.json +1395 -215
- package/package.json +8 -3
package/dist/api-client.js
CHANGED
|
@@ -5,6 +5,11 @@ export class Rent2bApiClient {
|
|
|
5
5
|
organizationId = null;
|
|
6
6
|
constructor(config) {
|
|
7
7
|
this.config = config;
|
|
8
|
+
// Reuse an org already resolved by the auth layer to avoid a redundant
|
|
9
|
+
// GET /zapier/me on every request.
|
|
10
|
+
if (config.organizationId != null) {
|
|
11
|
+
this.organizationId = config.organizationId;
|
|
12
|
+
}
|
|
8
13
|
this.client = axios.create({
|
|
9
14
|
baseURL: config.apiUrl,
|
|
10
15
|
headers: {
|
|
@@ -17,12 +22,16 @@ export class Rent2bApiClient {
|
|
|
17
22
|
async resolveOrganization() {
|
|
18
23
|
if (this.organizationId)
|
|
19
24
|
return this.organizationId;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
// Resolve the organization bound to THIS API key. `GET /organizations` is
|
|
26
|
+
// NOT key-scoped (it returns every organization in the system, ordered with
|
|
27
|
+
// org 1 first), so taking [0] picked the wrong org for multi-tenant keys.
|
|
28
|
+
// `GET /zapier/me` is key-scoped and returns the authenticated key's org.
|
|
29
|
+
const response = await this.client.get('/zapier/me');
|
|
30
|
+
const orgId = response.data?.organization?.id ?? response.data?.id;
|
|
31
|
+
if (typeof orgId !== 'number') {
|
|
32
|
+
throw new Error('Could not resolve organization for this API key');
|
|
24
33
|
}
|
|
25
|
-
this.organizationId =
|
|
34
|
+
this.organizationId = orgId;
|
|
26
35
|
return this.organizationId;
|
|
27
36
|
}
|
|
28
37
|
getOrganizationId() {
|
package/dist/api-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAiB,UAAU,EAAE,MAAM,OAAO,CAAC;AAczD,MAAM,OAAO,eAAe;IAIN;IAHZ,MAAM,CAAgB;IACtB,cAAc,GAAkB,IAAI,CAAC;IAE7C,YAAoB,MAAc;QAAd,WAAM,GAAN,MAAM,CAAQ;QAChC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;YACzB,OAAO,EAAE,MAAM,CAAC,MAAM;YACtB,OAAO,EAAE;gBACP,WAAW,EAAE,MAAM,CAAC,MAAM;gBAC1B,cAAc,EAAE,kBAAkB;aACnC;YACD,OAAO,EAAE,MAAM;SAChB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,mBAAmB;QACvB,IAAI,IAAI,CAAC,cAAc;YAAE,OAAO,IAAI,CAAC,cAAc,CAAC;QAEpD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
const mockGet = vi.fn();
|
|
3
|
+
const mockRequest = vi.fn();
|
|
4
|
+
vi.mock('axios', () => ({
|
|
5
|
+
default: {
|
|
6
|
+
create: () => ({ get: mockGet, request: mockRequest }),
|
|
7
|
+
},
|
|
8
|
+
}));
|
|
9
|
+
const { Rent2bApiClient } = await import('./api-client.js');
|
|
10
|
+
describe('resolveOrganization', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockGet.mockReset();
|
|
13
|
+
mockRequest.mockReset();
|
|
14
|
+
});
|
|
15
|
+
it('resolves the key-scoped org via /zapier/me, not GET /organizations[0]', async () => {
|
|
16
|
+
// /zapier/me returns the org bound to THIS api key (Haus Hana = 54).
|
|
17
|
+
mockGet.mockResolvedValueOnce({
|
|
18
|
+
data: { id: 54, organization: { id: 54, name: 'Haus Hana' } },
|
|
19
|
+
});
|
|
20
|
+
const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
|
|
21
|
+
const org = await client.resolveOrganization();
|
|
22
|
+
expect(mockGet).toHaveBeenCalledWith('/zapier/me');
|
|
23
|
+
expect(org).toBe(54);
|
|
24
|
+
// must NOT fall back to the unscoped /organizations list
|
|
25
|
+
expect(mockGet).not.toHaveBeenCalledWith('/organizations');
|
|
26
|
+
});
|
|
27
|
+
it('caches the resolved org (one lookup)', async () => {
|
|
28
|
+
mockGet.mockResolvedValue({ data: { organization: { id: 54 } } });
|
|
29
|
+
const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
|
|
30
|
+
await client.resolveOrganization();
|
|
31
|
+
await client.resolveOrganization();
|
|
32
|
+
expect(mockGet).toHaveBeenCalledTimes(1);
|
|
33
|
+
expect(client.getOrganizationId()).toBe(54);
|
|
34
|
+
});
|
|
35
|
+
it('throws when /zapier/me has no organization id', async () => {
|
|
36
|
+
mockGet.mockResolvedValueOnce({ data: {} });
|
|
37
|
+
const client = new Rent2bApiClient({ apiKey: 'r2b_x', apiUrl: 'http://api' });
|
|
38
|
+
await expect(client.resolveOrganization()).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=api-client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../src/api-client.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AACxB,MAAM,WAAW,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AAE5B,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IACtB,OAAO,EAAE;QACP,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;KACvD;CACF,CAAC,CAAC,CAAC;AAEJ,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAE5D,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,CAAC,SAAS,EAAE,CAAC;QACpB,WAAW,CAAC,SAAS,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,qEAAqE;QACrE,OAAO,CAAC,qBAAqB,CAAC;YAC5B,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE;SAC9D,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QAC/C,MAAM,CAAC,OAAO,CAAC,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAC;QACnD,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrB,yDAAyD;QACzD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,gBAAgB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,KAAK,IAAI,EAAE;QACpD,OAAO,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACnC,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,OAAO,CAAC,qBAAqB,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;QAC9E,MAAM,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface AuthorizePageParams {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientName?: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
state: string;
|
|
6
|
+
codeChallenge: string;
|
|
7
|
+
scopes: string;
|
|
8
|
+
resource: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function renderAuthorizePage(p: AuthorizePageParams): string;
|
|
11
|
+
export interface AuthorizeSubmitFields {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
clientId: string;
|
|
14
|
+
redirectUri: string;
|
|
15
|
+
state: string;
|
|
16
|
+
codeChallenge: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseAuthorizeSubmit(body: Record<string, unknown>): AuthorizeSubmitFields;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function esc(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/"/g, '"')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>');
|
|
7
|
+
}
|
|
8
|
+
function hidden(name, value) {
|
|
9
|
+
return `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`;
|
|
10
|
+
}
|
|
11
|
+
export function renderAuthorizePage(p) {
|
|
12
|
+
const appName = p.clientName ? esc(p.clientName) : 'this application';
|
|
13
|
+
// action is relative so it resolves under the gateway path prefix (/rent2b/authorize/submit)
|
|
14
|
+
return `<!doctype html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
+
<title>Connect rent2b</title>
|
|
20
|
+
<style>
|
|
21
|
+
body { font-family: system-ui, sans-serif; max-width: 28rem; margin: 4rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
22
|
+
h1 { font-size: 1.25rem; } label { display:block; margin: 1rem 0 .25rem; font-weight: 600; }
|
|
23
|
+
input[type=text] { width: 100%; padding: .6rem; font-size: 1rem; border: 1px solid #bbb; border-radius: .4rem; }
|
|
24
|
+
button { margin-top: 1.25rem; padding: .6rem 1.2rem; font-size: 1rem; border: 0; border-radius: .4rem; background: #111; color: #fff; cursor: pointer; }
|
|
25
|
+
p.hint { color: #555; font-size: .85rem; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<h1>Connect ${appName} to rent2b</h1>
|
|
30
|
+
<p class="hint">Enter your rent2b API key to grant access to your organization's data. Your key starts with <code>r2b_</code>.</p>
|
|
31
|
+
<form method="post" action="authorize/submit">
|
|
32
|
+
${hidden('clientId', p.clientId)}
|
|
33
|
+
${hidden('redirectUri', p.redirectUri)}
|
|
34
|
+
${hidden('state', p.state)}
|
|
35
|
+
${hidden('codeChallenge', p.codeChallenge)}
|
|
36
|
+
${hidden('scopes', p.scopes)}
|
|
37
|
+
${hidden('resource', p.resource)}
|
|
38
|
+
<label for="apiKey">rent2b API key</label>
|
|
39
|
+
<input id="apiKey" name="apiKey" type="text" autocomplete="off" spellcheck="false" placeholder="r2b_..." required>
|
|
40
|
+
<button type="submit">Authorize</button>
|
|
41
|
+
</form>
|
|
42
|
+
</body>
|
|
43
|
+
</html>`;
|
|
44
|
+
}
|
|
45
|
+
export function parseAuthorizeSubmit(body) {
|
|
46
|
+
const str = (k) => {
|
|
47
|
+
const v = body[k];
|
|
48
|
+
if (typeof v !== 'string' || v.length === 0)
|
|
49
|
+
throw new Error(`Missing field: ${k}`);
|
|
50
|
+
return v;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
apiKey: str('apiKey'),
|
|
54
|
+
clientId: str('clientId'),
|
|
55
|
+
redirectUri: str('redirectUri'),
|
|
56
|
+
state: typeof body.state === 'string' ? body.state : '',
|
|
57
|
+
codeChallenge: str('codeChallenge'),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=authorize-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize-page.js","sourceRoot":"","sources":["../../src/auth/authorize-page.ts"],"names":[],"mappings":"AAUA,SAAS,GAAG,CAAC,KAAa;IACxB,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,MAAM,CAAC,IAAY,EAAE,KAAa;IACzC,OAAO,8BAA8B,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,CAAsB;IACxD,MAAM,OAAO,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC;IACtE,6FAA6F;IAC7F,OAAO;;;;;;;;;;;;;;;gBAeO,OAAO;;;MAGjB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;MAC9B,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC,WAAW,CAAC;MACpC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC;MACxB,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC,aAAa,CAAC;MACxC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC;MAC1B,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC;;;;;;QAM5B,CAAC;AACT,CAAC;AAUD,MAAM,UAAU,oBAAoB,CAAC,IAA6B;IAChE,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC;QACpF,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;QACrB,QAAQ,EAAE,GAAG,CAAC,UAAU,CAAC;QACzB,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC;QAC/B,KAAK,EAAE,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACvD,aAAa,EAAE,GAAG,CAAC,eAAe,CAAC;KACpC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderAuthorizePage, parseAuthorizeSubmit } from './authorize-page.js';
|
|
3
|
+
describe('renderAuthorizePage', () => {
|
|
4
|
+
it('embeds params as hidden fields and escapes attributes', () => {
|
|
5
|
+
const html = renderAuthorizePage({
|
|
6
|
+
clientId: 'c1',
|
|
7
|
+
clientName: 'Claude',
|
|
8
|
+
redirectUri: 'https://claude.ai/cb',
|
|
9
|
+
state: 'st"ate',
|
|
10
|
+
codeChallenge: 'CHAL',
|
|
11
|
+
scopes: '',
|
|
12
|
+
resource: '',
|
|
13
|
+
});
|
|
14
|
+
expect(html).toContain('name="apiKey"');
|
|
15
|
+
expect(html).toContain('action="authorize/submit"');
|
|
16
|
+
expect(html).toContain('value="c1"');
|
|
17
|
+
expect(html).toContain('value="CHAL"');
|
|
18
|
+
expect(html).toContain('st"ate'); // escaped quote
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('parseAuthorizeSubmit', () => {
|
|
22
|
+
it('parses a valid body', () => {
|
|
23
|
+
const fields = parseAuthorizeSubmit({
|
|
24
|
+
apiKey: 'r2b_x',
|
|
25
|
+
clientId: 'c1',
|
|
26
|
+
redirectUri: 'https://claude.ai/cb',
|
|
27
|
+
state: 's',
|
|
28
|
+
codeChallenge: 'CHAL',
|
|
29
|
+
});
|
|
30
|
+
expect(fields.apiKey).toBe('r2b_x');
|
|
31
|
+
expect(fields.redirectUri).toBe('https://claude.ai/cb');
|
|
32
|
+
});
|
|
33
|
+
it('throws when apiKey is missing', () => {
|
|
34
|
+
expect(() => parseAuthorizeSubmit({ clientId: 'c1', redirectUri: 'x', codeChallenge: 'y' })).toThrow();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
//# sourceMappingURL=authorize-page.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize-page.test.js","sourceRoot":"","sources":["../../src/auth/authorize-page.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,IAAI,GAAG,mBAAmB,CAAC;YAC/B,QAAQ,EAAE,IAAI;YACd,UAAU,EAAE,QAAQ;YACpB,WAAW,EAAE,sBAAsB;YACnC,KAAK,EAAE,QAAQ;YACf,aAAa,EAAE,MAAM;YACrB,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;QACpD,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,oBAAoB,CAAC;YAClC,MAAM,EAAE,OAAO;YACf,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,sBAAsB;YACnC,KAAK,EAAE,GAAG;YACV,aAAa,EAAE,MAAM;SACtB,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,EAAE,CACV,oBAAoB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,CAC/E,CAAC,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Express, type RequestHandler } from 'express';
|
|
2
|
+
import type { OAuthRuntimeConfig } from './oauth-config.js';
|
|
3
|
+
import type { Rent2bOAuthProvider } from './provider.js';
|
|
4
|
+
export interface RegisterOAuthOptions {
|
|
5
|
+
oauthCfg: OAuthRuntimeConfig;
|
|
6
|
+
provider: Rent2bOAuthProvider;
|
|
7
|
+
validateKey: (apiKey: string) => Promise<number>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
|
|
11
|
+
* bearer-auth middleware to guard the MCP endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Note on path-prefixed deployment: the MCP SDK serves the auth handlers at the
|
|
14
|
+
* origin root (`/authorize`, `/token`, `/register`) and advertises them
|
|
15
|
+
* origin-level, dropping any issuer path. Behind the gateway, Caddy strips the
|
|
16
|
+
* `/rent2b` prefix before the container sees these paths, so the handlers line
|
|
17
|
+
* up — but the SDK's authorization-server metadata would advertise the wrong
|
|
18
|
+
* (origin-level) URLs. We therefore override that one document with
|
|
19
|
+
* path-correct endpoint URLs. The SDK's protected-resource metadata is already
|
|
20
|
+
* correct (it advertises the full issuer URL), so we keep it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerOAuth(app: Express, opts: RegisterOAuthOptions): RequestHandler;
|
|
23
|
+
/**
|
|
24
|
+
* Lets existing clients that authenticate with `X-API-Key: r2b_...` (and no
|
|
25
|
+
* `Authorization` header) keep working: copies the key into a Bearer header so
|
|
26
|
+
* `requireBearerAuth` -> `verifyAccessToken`'s raw-key branch accepts it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const xApiKeyToBearer: RequestHandler;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl, } from '@modelcontextprotocol/sdk/server/auth/router.js';
|
|
3
|
+
import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
|
|
4
|
+
import { parseAuthorizeSubmit } from './authorize-page.js';
|
|
5
|
+
/**
|
|
6
|
+
* Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
|
|
7
|
+
* bearer-auth middleware to guard the MCP endpoint.
|
|
8
|
+
*
|
|
9
|
+
* Note on path-prefixed deployment: the MCP SDK serves the auth handlers at the
|
|
10
|
+
* origin root (`/authorize`, `/token`, `/register`) and advertises them
|
|
11
|
+
* origin-level, dropping any issuer path. Behind the gateway, Caddy strips the
|
|
12
|
+
* `/rent2b` prefix before the container sees these paths, so the handlers line
|
|
13
|
+
* up — but the SDK's authorization-server metadata would advertise the wrong
|
|
14
|
+
* (origin-level) URLs. We therefore override that one document with
|
|
15
|
+
* path-correct endpoint URLs. The SDK's protected-resource metadata is already
|
|
16
|
+
* correct (it advertises the full issuer URL), so we keep it.
|
|
17
|
+
*/
|
|
18
|
+
export function registerOAuth(app, opts) {
|
|
19
|
+
const { oauthCfg, provider, validateKey } = opts;
|
|
20
|
+
const issuerHref = oauthCfg.issuerUrl.href.replace(/\/$/, '');
|
|
21
|
+
app.use(express.urlencoded({ extended: false }));
|
|
22
|
+
// Override: path-aware authorization-server metadata (registered before the
|
|
23
|
+
// SDK router so it shadows the SDK's origin-level document).
|
|
24
|
+
const authServerMetadata = {
|
|
25
|
+
issuer: issuerHref,
|
|
26
|
+
authorization_endpoint: `${issuerHref}/authorize`,
|
|
27
|
+
token_endpoint: `${issuerHref}/token`,
|
|
28
|
+
registration_endpoint: `${issuerHref}/register`,
|
|
29
|
+
response_types_supported: ['code'],
|
|
30
|
+
grant_types_supported: ['authorization_code', 'refresh_token'],
|
|
31
|
+
code_challenge_methods_supported: ['S256'],
|
|
32
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
33
|
+
scopes_supported: [],
|
|
34
|
+
};
|
|
35
|
+
app.get('/.well-known/oauth-authorization-server', (_req, res) => {
|
|
36
|
+
res.json(authServerMetadata);
|
|
37
|
+
});
|
|
38
|
+
app.use(mcpAuthRouter({
|
|
39
|
+
provider,
|
|
40
|
+
issuerUrl: oauthCfg.issuerUrl,
|
|
41
|
+
baseUrl: oauthCfg.issuerUrl,
|
|
42
|
+
resourceServerUrl: oauthCfg.resourceUrl,
|
|
43
|
+
scopesSupported: [],
|
|
44
|
+
}));
|
|
45
|
+
// The API-key entry page (provider.authorize) posts here.
|
|
46
|
+
app.post('/authorize/submit', async (req, res) => {
|
|
47
|
+
let fields;
|
|
48
|
+
try {
|
|
49
|
+
fields = parseAuthorizeSubmit(req.body);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
res.status(400).send('Missing required fields');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let orgId;
|
|
56
|
+
try {
|
|
57
|
+
orgId = await validateKey(fields.apiKey);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
res.status(401).setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
61
|
+
res.send('<p>Invalid rent2b API key. <a href="javascript:history.back()">Try again</a>.</p>');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const code = provider.issueAuthCode({
|
|
65
|
+
apiKey: fields.apiKey,
|
|
66
|
+
orgId,
|
|
67
|
+
codeChallenge: fields.codeChallenge,
|
|
68
|
+
redirectUri: fields.redirectUri,
|
|
69
|
+
clientId: fields.clientId,
|
|
70
|
+
});
|
|
71
|
+
const redirect = new URL(fields.redirectUri);
|
|
72
|
+
redirect.searchParams.set('code', code);
|
|
73
|
+
if (fields.state)
|
|
74
|
+
redirect.searchParams.set('state', fields.state);
|
|
75
|
+
res.redirect(302, redirect.toString());
|
|
76
|
+
});
|
|
77
|
+
return requireBearerAuth({
|
|
78
|
+
verifier: provider,
|
|
79
|
+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(oauthCfg.resourceUrl),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Lets existing clients that authenticate with `X-API-Key: r2b_...` (and no
|
|
84
|
+
* `Authorization` header) keep working: copies the key into a Bearer header so
|
|
85
|
+
* `requireBearerAuth` -> `verifyAccessToken`'s raw-key branch accepts it.
|
|
86
|
+
*/
|
|
87
|
+
export const xApiKeyToBearer = (req, _res, next) => {
|
|
88
|
+
if (!req.headers['authorization'] && typeof req.headers['x-api-key'] === 'string') {
|
|
89
|
+
req.headers['authorization'] = `Bearer ${req.headers['x-api-key']}`;
|
|
90
|
+
}
|
|
91
|
+
next();
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=http-oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-oauth.js","sourceRoot":"","sources":["../../src/auth/http-oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function loadOAuthConfig(env = process.env) {
|
|
2
|
+
const issuer = env.OAUTH_ISSUER_URL;
|
|
3
|
+
if (!issuer)
|
|
4
|
+
throw new Error('OAUTH_ISSUER_URL is required to enable OAuth');
|
|
5
|
+
const issuerUrl = new URL(issuer);
|
|
6
|
+
const resourceUrl = new URL(issuerUrl.href.replace(/\/$/, '') + '/mcp');
|
|
7
|
+
const rawKey = env.OAUTH_ENCRYPTION_KEY;
|
|
8
|
+
if (!rawKey)
|
|
9
|
+
throw new Error('OAUTH_ENCRYPTION_KEY is required to enable OAuth');
|
|
10
|
+
const encryptionKey = Buffer.from(rawKey, 'base64');
|
|
11
|
+
if (encryptionKey.length !== 32) {
|
|
12
|
+
throw new Error('OAUTH_ENCRYPTION_KEY must decode (base64) to exactly 32 bytes');
|
|
13
|
+
}
|
|
14
|
+
return { issuerUrl, resourceUrl, encryptionKey };
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=oauth-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-config.js","sourceRoot":"","sources":["../../src/auth/oauth-config.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IAClE,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,CAAC;IACpC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC7E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IAElC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;IAExE,MAAM,MAAM,GAAG,GAAG,CAAC,oBAAoB,CAAC;IACxC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;IACjF,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACpD,IAAI,aAAa,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IAED,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { loadOAuthConfig } from './oauth-config.js';
|
|
4
|
+
const key = randomBytes(32).toString('base64');
|
|
5
|
+
describe('loadOAuthConfig', () => {
|
|
6
|
+
it('loads issuer, derives resource, decodes key', () => {
|
|
7
|
+
const cfg = loadOAuthConfig({
|
|
8
|
+
OAUTH_ISSUER_URL: 'https://mcp.honeyfield.at/rent2b',
|
|
9
|
+
OAUTH_ENCRYPTION_KEY: key,
|
|
10
|
+
});
|
|
11
|
+
expect(cfg.issuerUrl.href).toBe('https://mcp.honeyfield.at/rent2b');
|
|
12
|
+
expect(cfg.resourceUrl.href).toBe('https://mcp.honeyfield.at/rent2b/mcp');
|
|
13
|
+
expect(cfg.encryptionKey.length).toBe(32);
|
|
14
|
+
});
|
|
15
|
+
it('throws when issuer missing', () => {
|
|
16
|
+
expect(() => loadOAuthConfig({ OAUTH_ENCRYPTION_KEY: key })).toThrow();
|
|
17
|
+
});
|
|
18
|
+
it('throws when key is not 32 bytes', () => {
|
|
19
|
+
expect(() => loadOAuthConfig({
|
|
20
|
+
OAUTH_ISSUER_URL: 'https://x/rent2b',
|
|
21
|
+
OAUTH_ENCRYPTION_KEY: 'c2hvcnQ=',
|
|
22
|
+
})).toThrow();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
//# sourceMappingURL=oauth-config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-config.test.js","sourceRoot":"","sources":["../../src/auth/oauth-config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,GAAG,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAE/C,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,eAAe,CAAC;YAC1B,gBAAgB,EAAE,kCAAkC;YACpD,oBAAoB,EAAE,GAAG;SACnB,CAAC,CAAC;QACV,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACpE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QAC1E,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,EAAE,oBAAoB,EAAE,GAAG,EAAS,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IAChF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,GAAG,EAAE,CACV,eAAe,CAAC;YACd,gBAAgB,EAAE,kBAAkB;YACpC,oBAAoB,EAAE,UAAU;SAC1B,CAAC,CACV,CAAC,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import type { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/sdk/server/auth/provider.js';
|
|
3
|
+
import type { OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
4
|
+
import type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js';
|
|
5
|
+
import type { TokenCodec } from './token-crypto.js';
|
|
6
|
+
import type { InMemoryClientsStore, AuthCodeStore, AuthCodeRecord } from './stores.js';
|
|
7
|
+
export interface ProviderOptions {
|
|
8
|
+
codec: TokenCodec;
|
|
9
|
+
clientsStore: InMemoryClientsStore;
|
|
10
|
+
codeStore: AuthCodeStore;
|
|
11
|
+
validateKey: (apiKey: string) => Promise<number>;
|
|
12
|
+
accessTtlSec?: number;
|
|
13
|
+
refreshTtlSec?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class Rent2bOAuthProvider implements OAuthServerProvider {
|
|
16
|
+
private readonly opts;
|
|
17
|
+
private readonly pending;
|
|
18
|
+
constructor(opts: ProviderOptions);
|
|
19
|
+
get clientsStore(): InMemoryClientsStore;
|
|
20
|
+
/** Called by the /authorize/submit handler after a valid r2b_ key is entered. */
|
|
21
|
+
issueAuthCode(input: Omit<AuthCodeRecord, 'expiresAt'>): string;
|
|
22
|
+
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
|
|
23
|
+
challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
|
|
24
|
+
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, _codeVerifier?: string, _redirectUri?: string): Promise<OAuthTokens>;
|
|
25
|
+
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, _scopes?: string[]): Promise<OAuthTokens>;
|
|
26
|
+
verifyAccessToken(token: string): Promise<AuthInfo>;
|
|
27
|
+
private mintTokens;
|
|
28
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { TokenError } from './token-crypto.js';
|
|
3
|
+
import { renderAuthorizePage } from './authorize-page.js';
|
|
4
|
+
export class Rent2bOAuthProvider {
|
|
5
|
+
opts;
|
|
6
|
+
// Holds the code record between challengeForAuthorizationCode (peek) and
|
|
7
|
+
// exchangeAuthorizationCode (consume), since the code store is one-time.
|
|
8
|
+
pending = new Map();
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = { accessTtlSec: 3600, refreshTtlSec: 60 * 60 * 24 * 30, ...opts };
|
|
11
|
+
}
|
|
12
|
+
get clientsStore() {
|
|
13
|
+
return this.opts.clientsStore;
|
|
14
|
+
}
|
|
15
|
+
/** Called by the /authorize/submit handler after a valid r2b_ key is entered. */
|
|
16
|
+
issueAuthCode(input) {
|
|
17
|
+
return this.opts.codeStore.create(input);
|
|
18
|
+
}
|
|
19
|
+
async authorize(client, params, res) {
|
|
20
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
21
|
+
res.send(renderAuthorizePage({
|
|
22
|
+
clientId: client.client_id,
|
|
23
|
+
clientName: client.client_name,
|
|
24
|
+
redirectUri: params.redirectUri,
|
|
25
|
+
state: params.state ?? '',
|
|
26
|
+
codeChallenge: params.codeChallenge,
|
|
27
|
+
scopes: (params.scopes ?? []).join(' '),
|
|
28
|
+
resource: params.resource ? String(params.resource) : '',
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
async challengeForAuthorizationCode(_client, authorizationCode) {
|
|
32
|
+
let rec = this.pending.get(authorizationCode);
|
|
33
|
+
if (!rec) {
|
|
34
|
+
const taken = this.opts.codeStore.take(authorizationCode);
|
|
35
|
+
if (!taken)
|
|
36
|
+
throw new Error('Invalid or expired authorization code');
|
|
37
|
+
this.pending.set(authorizationCode, taken);
|
|
38
|
+
rec = taken;
|
|
39
|
+
}
|
|
40
|
+
return rec.codeChallenge;
|
|
41
|
+
}
|
|
42
|
+
async exchangeAuthorizationCode(client, authorizationCode, _codeVerifier, _redirectUri) {
|
|
43
|
+
let rec = this.pending.get(authorizationCode);
|
|
44
|
+
if (rec) {
|
|
45
|
+
this.pending.delete(authorizationCode);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
rec = this.opts.codeStore.take(authorizationCode);
|
|
49
|
+
}
|
|
50
|
+
if (!rec)
|
|
51
|
+
throw new Error('Invalid or expired authorization code');
|
|
52
|
+
if (rec.clientId !== client.client_id) {
|
|
53
|
+
throw new Error('Authorization code was issued to a different client');
|
|
54
|
+
}
|
|
55
|
+
return this.mintTokens(rec.apiKey, rec.orgId, client.client_id);
|
|
56
|
+
}
|
|
57
|
+
async exchangeRefreshToken(client, refreshToken, _scopes) {
|
|
58
|
+
let payload;
|
|
59
|
+
try {
|
|
60
|
+
payload = this.opts.codec.decrypt(refreshToken);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error('Invalid refresh token');
|
|
64
|
+
}
|
|
65
|
+
if (payload.type !== 'refresh')
|
|
66
|
+
throw new Error('Not a refresh token');
|
|
67
|
+
return this.mintTokens(payload.apiKey, payload.orgId, client.client_id);
|
|
68
|
+
}
|
|
69
|
+
async verifyAccessToken(token) {
|
|
70
|
+
if (token.startsWith('r2b_')) {
|
|
71
|
+
// Backward-compat: a raw rent2b API key used directly as the bearer.
|
|
72
|
+
// requireBearerAuth requires an expiry; raw keys don't expire, so we set
|
|
73
|
+
// a short rolling window (the key is re-validated on each new session).
|
|
74
|
+
const orgId = await this.opts.validateKey(token);
|
|
75
|
+
return {
|
|
76
|
+
token,
|
|
77
|
+
clientId: 'rent2b-direct',
|
|
78
|
+
scopes: [],
|
|
79
|
+
expiresAt: Math.floor(Date.now() / 1000) + this.opts.accessTtlSec,
|
|
80
|
+
extra: { apiKey: token, orgId },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = this.opts.codec.decrypt(token);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (err instanceof TokenError)
|
|
89
|
+
throw new Error('Invalid or expired access token');
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
if (payload.type !== 'access')
|
|
93
|
+
throw new Error('Not an access token');
|
|
94
|
+
return {
|
|
95
|
+
token,
|
|
96
|
+
clientId: 'rent2b-oauth',
|
|
97
|
+
scopes: [],
|
|
98
|
+
expiresAt: payload.exp,
|
|
99
|
+
extra: { apiKey: payload.apiKey, orgId: payload.orgId },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
mintTokens(apiKey, orgId, _clientId) {
|
|
103
|
+
const now = Math.floor(Date.now() / 1000);
|
|
104
|
+
const access = this.opts.codec.encrypt({
|
|
105
|
+
apiKey,
|
|
106
|
+
orgId,
|
|
107
|
+
type: 'access',
|
|
108
|
+
exp: now + this.opts.accessTtlSec,
|
|
109
|
+
jti: randomUUID(),
|
|
110
|
+
});
|
|
111
|
+
const refresh = this.opts.codec.encrypt({
|
|
112
|
+
apiKey,
|
|
113
|
+
orgId,
|
|
114
|
+
type: 'refresh',
|
|
115
|
+
exp: now + this.opts.refreshTtlSec,
|
|
116
|
+
jti: randomUUID(),
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
access_token: access,
|
|
120
|
+
token_type: 'Bearer',
|
|
121
|
+
expires_in: this.opts.accessTtlSec,
|
|
122
|
+
refresh_token: refresh,
|
|
123
|
+
scope: '',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/auth/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|