@honeyfield/rent2b-mcp 1.2.2 → 1.3.0
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 +25 -9
- package/dist/api-client.js.map +1 -1
- package/dist/auth/api-auth.d.ts +29 -0
- package/dist/auth/api-auth.js +88 -0
- package/dist/auth/api-auth.js.map +1 -0
- package/dist/auth/authorize-page.d.ts +24 -3
- package/dist/auth/authorize-page.js +111 -23
- package/dist/auth/authorize-page.js.map +1 -1
- package/dist/auth/authorize-page.test.js +72 -18
- package/dist/auth/authorize-page.test.js.map +1 -1
- package/dist/auth/http-oauth.d.ts +13 -8
- package/dist/auth/http-oauth.js +87 -18
- package/dist/auth/http-oauth.js.map +1 -1
- package/dist/auth/provider.d.ts +8 -2
- package/dist/auth/provider.js +60 -9
- package/dist/auth/provider.js.map +1 -1
- package/dist/auth/provider.test.js +69 -37
- package/dist/auth/provider.test.js.map +1 -1
- package/dist/auth/stores.d.ts +3 -1
- package/dist/auth/stores.js.map +1 -1
- package/dist/auth/stores.test.js +2 -2
- package/dist/auth/stores.test.js.map +1 -1
- package/dist/auth/token-crypto.d.ts +15 -1
- package/dist/auth/token-crypto.js +13 -0
- package/dist/auth/token-crypto.js.map +1 -1
- package/dist/config.d.ts +11 -4
- package/dist/config.js.map +1 -1
- package/dist/index.js +22 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/api-client.js
CHANGED
|
@@ -6,26 +6,42 @@ export class Rent2bApiClient {
|
|
|
6
6
|
constructor(config) {
|
|
7
7
|
this.config = config;
|
|
8
8
|
// Reuse an org already resolved by the auth layer to avoid a redundant
|
|
9
|
-
//
|
|
9
|
+
// lookup on every request.
|
|
10
10
|
if (config.organizationId != null) {
|
|
11
11
|
this.organizationId = config.organizationId;
|
|
12
12
|
}
|
|
13
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
14
|
+
// JWT mode (user logged in via OAuth) authenticates as the user so RLS
|
|
15
|
+
// applies; key mode keeps the X-API-Key path for stdio/scripting.
|
|
16
|
+
if (config.accessToken) {
|
|
17
|
+
headers['Authorization'] = `Bearer ${config.accessToken}`;
|
|
18
|
+
}
|
|
19
|
+
else if (config.apiKey) {
|
|
20
|
+
headers['X-API-Key'] = config.apiKey;
|
|
21
|
+
}
|
|
13
22
|
this.client = axios.create({
|
|
14
23
|
baseURL: config.apiUrl,
|
|
15
|
-
headers
|
|
16
|
-
'X-API-Key': config.apiKey,
|
|
17
|
-
'Content-Type': 'application/json',
|
|
18
|
-
},
|
|
24
|
+
headers,
|
|
19
25
|
timeout: 30_000,
|
|
20
26
|
});
|
|
21
27
|
}
|
|
22
28
|
async resolveOrganization() {
|
|
23
29
|
if (this.organizationId)
|
|
24
30
|
return this.organizationId;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
if (this.config.accessToken) {
|
|
32
|
+
// JWT mode: `GET /settings` is user-scoped (RLS) and returns the caller's
|
|
33
|
+
// organization. (`/zapier/me` is API-key-only.)
|
|
34
|
+
const response = await this.client.get('/settings');
|
|
35
|
+
const orgId = response.data?.organization?.id;
|
|
36
|
+
if (typeof orgId !== 'number') {
|
|
37
|
+
throw new Error('Could not resolve organization for this account');
|
|
38
|
+
}
|
|
39
|
+
this.organizationId = orgId;
|
|
40
|
+
return this.organizationId;
|
|
41
|
+
}
|
|
42
|
+
// Key mode: `GET /organizations` is NOT key-scoped (it returns every
|
|
43
|
+
// organization, org 1 first), so taking [0] picked the wrong org. `GET
|
|
44
|
+
// /zapier/me` is key-scoped and returns the authenticated key's org.
|
|
29
45
|
const response = await this.client.get('/zapier/me');
|
|
30
46
|
const orgId = response.data?.organization?.id ?? response.data?.id;
|
|
31
47
|
if (typeof orgId !== 'number') {
|
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,uEAAuE;QACvE,
|
|
1
|
+
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the rent2b-api auth surface, used by the OAuth layer to log a
|
|
3
|
+
* user in (Supabase session) instead of requiring a pasted `r2b_` API key.
|
|
4
|
+
*
|
|
5
|
+
* The MCP never talks to Supabase directly — it goes through the existing
|
|
6
|
+
* rent2b-api endpoints (`POST /auth/signin`, `POST /auth/refresh`,
|
|
7
|
+
* `GET /auth/{provider}`), so all Supabase config stays server-side in the API.
|
|
8
|
+
*/
|
|
9
|
+
export interface SupabaseSession {
|
|
10
|
+
access_token: string;
|
|
11
|
+
refresh_token: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LoginResult {
|
|
14
|
+
session: SupabaseSession;
|
|
15
|
+
/** Resolved from the signin response when present, else looked up later. */
|
|
16
|
+
orgId: number | null;
|
|
17
|
+
}
|
|
18
|
+
export declare class ApiAuthClient {
|
|
19
|
+
private readonly http;
|
|
20
|
+
constructor(apiUrl: string);
|
|
21
|
+
/** Email + password → Supabase session (+ org if the response carries it). */
|
|
22
|
+
signInWithPassword(email: string, password: string): Promise<LoginResult>;
|
|
23
|
+
/** Exchange a (rotating) Supabase refresh token for a fresh session. */
|
|
24
|
+
refreshSession(refreshToken: string): Promise<SupabaseSession>;
|
|
25
|
+
/** Returns the provider's hosted login URL to redirect the browser to. */
|
|
26
|
+
getSocialUrl(provider: 'google' | 'apple', redirectTo: string): Promise<string>;
|
|
27
|
+
/** Resolve the caller's organization id from a Supabase access token. */
|
|
28
|
+
resolveOrg(accessToken: string): Promise<number>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import axios, { AxiosError } from 'axios';
|
|
2
|
+
function coerceSession(raw) {
|
|
3
|
+
if (!raw ||
|
|
4
|
+
typeof raw.access_token !== 'string' ||
|
|
5
|
+
typeof raw.refresh_token !== 'string') {
|
|
6
|
+
throw new Error('Auth response did not contain a usable session');
|
|
7
|
+
}
|
|
8
|
+
return { access_token: raw.access_token, refresh_token: raw.refresh_token };
|
|
9
|
+
}
|
|
10
|
+
function orgIdFrom(data) {
|
|
11
|
+
const org = data?.organization;
|
|
12
|
+
return typeof org?.id === 'number' ? org.id : null;
|
|
13
|
+
}
|
|
14
|
+
export class ApiAuthClient {
|
|
15
|
+
http;
|
|
16
|
+
constructor(apiUrl) {
|
|
17
|
+
this.http = axios.create({
|
|
18
|
+
baseURL: apiUrl,
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
timeout: 15_000,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Email + password → Supabase session (+ org if the response carries it). */
|
|
24
|
+
async signInWithPassword(email, password) {
|
|
25
|
+
try {
|
|
26
|
+
const res = await this.http.post('/auth/signin', { email, password });
|
|
27
|
+
return {
|
|
28
|
+
session: coerceSession(res.data?.session),
|
|
29
|
+
orgId: orgIdFrom(res.data),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
throw toAuthError(err, 'Invalid email or password');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Exchange a (rotating) Supabase refresh token for a fresh session. */
|
|
37
|
+
async refreshSession(refreshToken) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await this.http.post('/auth/refresh', {
|
|
40
|
+
refresh_token: refreshToken,
|
|
41
|
+
});
|
|
42
|
+
return coerceSession(res.data?.session);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw toAuthError(err, 'Session expired, please reconnect');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Returns the provider's hosted login URL to redirect the browser to. */
|
|
49
|
+
async getSocialUrl(provider, redirectTo) {
|
|
50
|
+
try {
|
|
51
|
+
const res = await this.http.get(`/auth/${provider}`, {
|
|
52
|
+
params: { redirectTo },
|
|
53
|
+
});
|
|
54
|
+
const url = res.data?.url;
|
|
55
|
+
if (typeof url !== 'string')
|
|
56
|
+
throw new Error('No url in response');
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw toAuthError(err, `Could not start ${provider} sign-in`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Resolve the caller's organization id from a Supabase access token. */
|
|
64
|
+
async resolveOrg(accessToken) {
|
|
65
|
+
try {
|
|
66
|
+
const res = await this.http.get('/settings', {
|
|
67
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
68
|
+
});
|
|
69
|
+
const id = orgIdFrom(res.data);
|
|
70
|
+
if (typeof id !== 'number') {
|
|
71
|
+
throw new Error('No organization for this account');
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw toAuthError(err, 'Could not resolve organization for this account');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function toAuthError(err, fallback) {
|
|
81
|
+
if (err instanceof AxiosError && err.response) {
|
|
82
|
+
const msg = err.response.data?.message ??
|
|
83
|
+
fallback;
|
|
84
|
+
return new Error(typeof msg === 'string' ? msg : fallback);
|
|
85
|
+
}
|
|
86
|
+
return new Error(err instanceof Error ? err.message : fallback);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=api-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-auth.js","sourceRoot":"","sources":["../../src/auth/api-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,12 +7,33 @@ export interface AuthorizePageParams {
|
|
|
7
7
|
scopes: string;
|
|
8
8
|
resource: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function renderAuthorizePage(p: AuthorizePageParams): string;
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export declare function renderAuthorizePage(p: AuthorizePageParams, errorMessage?: string, showSocial?: boolean): string;
|
|
11
|
+
/**
|
|
12
|
+
* Page shown after the social provider redirects back. The Supabase session
|
|
13
|
+
* arrives in the URL fragment (`#access_token=…&refresh_token=…`), which is only
|
|
14
|
+
* readable client-side, so this tiny script lifts it out and re-submits it (with
|
|
15
|
+
* the preserved OAuth-flow fields) to /authorize/submit as a normal form post.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderSocialCallbackPage(params: Record<string, string>): string;
|
|
18
|
+
interface FlowFields {
|
|
13
19
|
clientId: string;
|
|
14
20
|
redirectUri: string;
|
|
15
21
|
state: string;
|
|
16
22
|
codeChallenge: string;
|
|
17
23
|
}
|
|
24
|
+
export type AuthorizeSubmitFields = ({
|
|
25
|
+
mode: 'password';
|
|
26
|
+
email: string;
|
|
27
|
+
password: string;
|
|
28
|
+
} & FlowFields) | ({
|
|
29
|
+
mode: 'session';
|
|
30
|
+
accessToken: string;
|
|
31
|
+
refreshToken: string;
|
|
32
|
+
} & FlowFields);
|
|
18
33
|
export declare function parseAuthorizeSubmit(body: Record<string, unknown>): AuthorizeSubmitFields;
|
|
34
|
+
/** Validate + extract the OAuth-flow fields for the /authorize/social handler. */
|
|
35
|
+
export declare function parseSocialStart(query: Record<string, unknown>): {
|
|
36
|
+
provider: 'google' | 'apple';
|
|
37
|
+
flow: Record<string, string>;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -8,40 +8,103 @@ function esc(value) {
|
|
|
8
8
|
function hidden(name, value) {
|
|
9
9
|
return `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
/** The OAuth-flow fields that must survive the login (form post or social round-trip). */
|
|
12
|
+
const FLOW_FIELDS = [
|
|
13
|
+
'clientId',
|
|
14
|
+
'redirectUri',
|
|
15
|
+
'state',
|
|
16
|
+
'codeChallenge',
|
|
17
|
+
'scopes',
|
|
18
|
+
'resource',
|
|
19
|
+
];
|
|
20
|
+
function flowQuery(p) {
|
|
21
|
+
const q = new URLSearchParams();
|
|
22
|
+
for (const k of FLOW_FIELDS)
|
|
23
|
+
q.set(k, p[k] ?? '');
|
|
24
|
+
return q.toString();
|
|
25
|
+
}
|
|
26
|
+
const STYLE = `
|
|
27
|
+
body { font-family: system-ui, sans-serif; max-width: 26rem; margin: 3.5rem auto; padding: 0 1rem; color: #1a1a1a; }
|
|
28
|
+
h1 { font-size: 1.25rem; } label { display:block; margin: .9rem 0 .25rem; font-weight: 600; }
|
|
29
|
+
input { width: 100%; padding: .6rem; font-size: 1rem; border: 1px solid #bbb; border-radius: .4rem; box-sizing: border-box; }
|
|
30
|
+
button { margin-top: 1.1rem; width: 100%; padding: .65rem 1.2rem; font-size: 1rem; border: 0; border-radius: .4rem; background: #111; color: #fff; cursor: pointer; }
|
|
31
|
+
.sso { display:block; text-align:center; margin-top:.6rem; padding:.6rem; border:1px solid #ccc; border-radius:.4rem; text-decoration:none; color:#111; background:#fff; }
|
|
32
|
+
.divider { text-align:center; color:#999; margin:1.2rem 0 .4rem; font-size:.8rem; }
|
|
33
|
+
p.hint { color: #555; font-size: .85rem; } .err { color:#b00020; font-size:.9rem; margin:.5rem 0; }
|
|
34
|
+
`;
|
|
35
|
+
export function renderAuthorizePage(p, errorMessage, showSocial = false) {
|
|
12
36
|
const appName = p.clientName ? esc(p.clientName) : 'this application';
|
|
13
|
-
|
|
37
|
+
const q = esc(flowQuery(p));
|
|
38
|
+
const err = errorMessage ? `<p class="err">${esc(errorMessage)}</p>` : '';
|
|
39
|
+
// Social login depends on the API returning a fragment-based session; gated
|
|
40
|
+
// off until that path is verified (see RENT2B_MCP_SOCIAL_LOGIN).
|
|
41
|
+
const social = showSocial
|
|
42
|
+
? `<div class="divider">or</div>
|
|
43
|
+
<a class="sso" href="authorize/social?provider=google&${q}">Continue with Google</a>
|
|
44
|
+
<a class="sso" href="authorize/social?provider=apple&${q}">Continue with Apple</a>`
|
|
45
|
+
: '';
|
|
46
|
+
// action/href are relative so they resolve under the gateway path prefix
|
|
47
|
+
// (/rent2b/authorize/submit, /rent2b/authorize/social).
|
|
14
48
|
return `<!doctype html>
|
|
15
49
|
<html lang="en">
|
|
16
50
|
<head>
|
|
17
51
|
<meta charset="utf-8">
|
|
18
52
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
-
<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>
|
|
53
|
+
<title>Sign in to rent2b</title>
|
|
54
|
+
<style>${STYLE}</style>
|
|
27
55
|
</head>
|
|
28
56
|
<body>
|
|
29
|
-
<h1>
|
|
30
|
-
<p class="hint">
|
|
57
|
+
<h1>Sign in to connect ${appName}</h1>
|
|
58
|
+
<p class="hint">Use your rent2b account to grant access to your organization's data.</p>
|
|
59
|
+
${err}
|
|
31
60
|
<form method="post" action="authorize/submit">
|
|
32
|
-
${hidden(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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>
|
|
61
|
+
${FLOW_FIELDS.map((k) => hidden(k, p[k] ?? '')).join('\n ')}
|
|
62
|
+
<label for="email">Email</label>
|
|
63
|
+
<input id="email" name="email" type="email" autocomplete="username" required>
|
|
64
|
+
<label for="password">Password</label>
|
|
65
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
|
66
|
+
<button type="submit">Sign in</button>
|
|
41
67
|
</form>
|
|
68
|
+
${social}
|
|
42
69
|
</body>
|
|
43
70
|
</html>`;
|
|
44
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Page shown after the social provider redirects back. The Supabase session
|
|
74
|
+
* arrives in the URL fragment (`#access_token=…&refresh_token=…`), which is only
|
|
75
|
+
* readable client-side, so this tiny script lifts it out and re-submits it (with
|
|
76
|
+
* the preserved OAuth-flow fields) to /authorize/submit as a normal form post.
|
|
77
|
+
*/
|
|
78
|
+
export function renderSocialCallbackPage(params) {
|
|
79
|
+
const fields = Object.entries(params)
|
|
80
|
+
.map(([k, v]) => hidden(k, v))
|
|
81
|
+
.join('\n ');
|
|
82
|
+
return `<!doctype html>
|
|
83
|
+
<html lang="en"><head><meta charset="utf-8"><title>Signing you in…</title>
|
|
84
|
+
<style>${STYLE}</style></head>
|
|
85
|
+
<body>
|
|
86
|
+
<p class="hint">Completing sign-in…</p>
|
|
87
|
+
<form id="f" method="post" action="submit">
|
|
88
|
+
${fields}
|
|
89
|
+
<input type="hidden" name="access_token" id="at">
|
|
90
|
+
<input type="hidden" name="refresh_token" id="rt">
|
|
91
|
+
<noscript><p class="err">JavaScript is required to finish signing in.</p></noscript>
|
|
92
|
+
</form>
|
|
93
|
+
<script>
|
|
94
|
+
(function () {
|
|
95
|
+
var h = new URLSearchParams((location.hash || '').replace(/^#/, ''));
|
|
96
|
+
var at = h.get('access_token'), rt = h.get('refresh_token');
|
|
97
|
+
if (at && rt) {
|
|
98
|
+
document.getElementById('at').value = at;
|
|
99
|
+
document.getElementById('rt').value = rt;
|
|
100
|
+
document.getElementById('f').submit();
|
|
101
|
+
} else {
|
|
102
|
+
document.body.textContent = 'Sign-in did not return a session. Please try again.';
|
|
103
|
+
}
|
|
104
|
+
})();
|
|
105
|
+
</script>
|
|
106
|
+
</body></html>`;
|
|
107
|
+
}
|
|
45
108
|
export function parseAuthorizeSubmit(body) {
|
|
46
109
|
const str = (k) => {
|
|
47
110
|
const v = body[k];
|
|
@@ -49,12 +112,37 @@ export function parseAuthorizeSubmit(body) {
|
|
|
49
112
|
throw new Error(`Missing field: ${k}`);
|
|
50
113
|
return v;
|
|
51
114
|
};
|
|
52
|
-
|
|
53
|
-
apiKey: str('apiKey'),
|
|
115
|
+
const flow = {
|
|
54
116
|
clientId: str('clientId'),
|
|
55
117
|
redirectUri: str('redirectUri'),
|
|
56
118
|
state: typeof body.state === 'string' ? body.state : '',
|
|
57
119
|
codeChallenge: str('codeChallenge'),
|
|
58
120
|
};
|
|
121
|
+
if (typeof body.access_token === 'string' && body.access_token.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
mode: 'session',
|
|
124
|
+
accessToken: str('access_token'),
|
|
125
|
+
refreshToken: str('refresh_token'),
|
|
126
|
+
...flow,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
mode: 'password',
|
|
131
|
+
email: str('email'),
|
|
132
|
+
password: str('password'),
|
|
133
|
+
...flow,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Validate + extract the OAuth-flow fields for the /authorize/social handler. */
|
|
137
|
+
export function parseSocialStart(query) {
|
|
138
|
+
const provider = query.provider === 'apple' ? 'apple' : 'google';
|
|
139
|
+
const flow = {};
|
|
140
|
+
for (const k of FLOW_FIELDS) {
|
|
141
|
+
flow[k] = typeof query[k] === 'string' ? query[k] : '';
|
|
142
|
+
}
|
|
143
|
+
if (!flow.clientId || !flow.redirectUri || !flow.codeChallenge) {
|
|
144
|
+
throw new Error('Missing OAuth flow parameters');
|
|
145
|
+
}
|
|
146
|
+
return { provider, flow };
|
|
59
147
|
}
|
|
60
148
|
//# sourceMappingURL=authorize-page.js.map
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"authorize-page.js","sourceRoot":"","sources":["../../src/auth/authorize-page.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,37 +1,91 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderAuthorizePage, parseAuthorizeSubmit } from './authorize-page.js';
|
|
2
|
+
import { renderAuthorizePage, parseAuthorizeSubmit, parseSocialStart, } from './authorize-page.js';
|
|
3
3
|
describe('renderAuthorizePage', () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
const params = {
|
|
5
|
+
clientId: 'c1',
|
|
6
|
+
clientName: 'Claude',
|
|
7
|
+
redirectUri: 'https://claude.ai/cb',
|
|
8
|
+
state: 'st"ate',
|
|
9
|
+
codeChallenge: 'CHAL',
|
|
10
|
+
scopes: '',
|
|
11
|
+
resource: '',
|
|
12
|
+
};
|
|
13
|
+
it('renders a login form with flow fields, escaping attrs', () => {
|
|
14
|
+
const html = renderAuthorizePage(params);
|
|
15
|
+
expect(html).toContain('name="email"');
|
|
16
|
+
expect(html).toContain('name="password"');
|
|
15
17
|
expect(html).toContain('action="authorize/submit"');
|
|
16
18
|
expect(html).toContain('value="c1"');
|
|
17
19
|
expect(html).toContain('value="CHAL"');
|
|
18
20
|
expect(html).toContain('st"ate'); // escaped quote
|
|
19
21
|
});
|
|
22
|
+
it('hides social buttons by default and shows them when enabled', () => {
|
|
23
|
+
expect(renderAuthorizePage(params)).not.toContain('authorize/social');
|
|
24
|
+
expect(renderAuthorizePage(params, undefined, true)).toContain('authorize/social?provider=google');
|
|
25
|
+
});
|
|
26
|
+
it('shows an error message when provided', () => {
|
|
27
|
+
const html = renderAuthorizePage({
|
|
28
|
+
clientId: 'c1',
|
|
29
|
+
redirectUri: 'x',
|
|
30
|
+
state: '',
|
|
31
|
+
codeChallenge: 'y',
|
|
32
|
+
scopes: '',
|
|
33
|
+
resource: '',
|
|
34
|
+
}, 'Invalid email or password');
|
|
35
|
+
expect(html).toContain('Invalid email or password');
|
|
36
|
+
});
|
|
20
37
|
});
|
|
21
38
|
describe('parseAuthorizeSubmit', () => {
|
|
22
|
-
it('parses a
|
|
23
|
-
const
|
|
24
|
-
|
|
39
|
+
it('parses a password login', () => {
|
|
40
|
+
const f = parseAuthorizeSubmit({
|
|
41
|
+
email: 'a@b.c',
|
|
42
|
+
password: 'pw',
|
|
25
43
|
clientId: 'c1',
|
|
26
44
|
redirectUri: 'https://claude.ai/cb',
|
|
27
45
|
state: 's',
|
|
28
46
|
codeChallenge: 'CHAL',
|
|
29
47
|
});
|
|
30
|
-
expect(
|
|
31
|
-
|
|
48
|
+
expect(f.mode).toBe('password');
|
|
49
|
+
if (f.mode === 'password') {
|
|
50
|
+
expect(f.email).toBe('a@b.c');
|
|
51
|
+
expect(f.password).toBe('pw');
|
|
52
|
+
}
|
|
53
|
+
expect(f.redirectUri).toBe('https://claude.ai/cb');
|
|
32
54
|
});
|
|
33
|
-
it('
|
|
55
|
+
it('parses a social session submit', () => {
|
|
56
|
+
const f = parseAuthorizeSubmit({
|
|
57
|
+
access_token: 'AT',
|
|
58
|
+
refresh_token: 'RT',
|
|
59
|
+
clientId: 'c1',
|
|
60
|
+
redirectUri: 'x',
|
|
61
|
+
codeChallenge: 'y',
|
|
62
|
+
});
|
|
63
|
+
expect(f.mode).toBe('session');
|
|
64
|
+
if (f.mode === 'session') {
|
|
65
|
+
expect(f.accessToken).toBe('AT');
|
|
66
|
+
expect(f.refreshToken).toBe('RT');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it('throws when required fields are missing', () => {
|
|
34
70
|
expect(() => parseAuthorizeSubmit({ clientId: 'c1', redirectUri: 'x', codeChallenge: 'y' })).toThrow();
|
|
35
71
|
});
|
|
36
72
|
});
|
|
73
|
+
describe('parseSocialStart', () => {
|
|
74
|
+
it('extracts provider + flow fields', () => {
|
|
75
|
+
const r = parseSocialStart({
|
|
76
|
+
provider: 'google',
|
|
77
|
+
clientId: 'c1',
|
|
78
|
+
redirectUri: 'https://claude.ai/cb',
|
|
79
|
+
codeChallenge: 'CHAL',
|
|
80
|
+
state: 's',
|
|
81
|
+
scopes: '',
|
|
82
|
+
resource: '',
|
|
83
|
+
});
|
|
84
|
+
expect(r.provider).toBe('google');
|
|
85
|
+
expect(r.flow.clientId).toBe('c1');
|
|
86
|
+
});
|
|
87
|
+
it('throws on missing flow params', () => {
|
|
88
|
+
expect(() => parseSocialStart({ provider: 'google' })).toThrow();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
37
91
|
//# sourceMappingURL=authorize-page.test.js.map
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"authorize-page.test.js","sourceRoot":"","sources":["../../src/auth/authorize-page.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { type Express, type RequestHandler } from 'express';
|
|
2
2
|
import type { OAuthRuntimeConfig } from './oauth-config.js';
|
|
3
3
|
import type { Rent2bOAuthProvider } from './provider.js';
|
|
4
|
+
import type { ApiAuthClient } from './api-auth.js';
|
|
4
5
|
export interface RegisterOAuthOptions {
|
|
5
6
|
oauthCfg: OAuthRuntimeConfig;
|
|
6
7
|
provider: Rent2bOAuthProvider;
|
|
7
|
-
|
|
8
|
+
apiAuth: ApiAuthClient;
|
|
9
|
+
/** Enable the Google/Apple social-login endpoints + buttons. */
|
|
10
|
+
socialLogin?: boolean;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
13
|
* Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
|
|
11
14
|
* bearer-auth middleware to guard the MCP endpoint.
|
|
12
15
|
*
|
|
16
|
+
* Login is the user's own rent2b account (email/password or Google/Apple),
|
|
17
|
+
* delegated to the rent2b-api auth endpoints; the issued token carries the
|
|
18
|
+
* resulting Supabase session (see provider.mintSessionTokens). Raw `r2b_`
|
|
19
|
+
* bearer tokens and `X-API-Key` headers still work (backward compat).
|
|
20
|
+
*
|
|
13
21
|
* Note on path-prefixed deployment: the MCP SDK serves the auth handlers at the
|
|
14
|
-
* origin root
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
22
|
+
* origin root and advertises them origin-level, dropping any issuer path.
|
|
23
|
+
* Behind the gateway, Caddy strips `/rent2b` before the container sees these
|
|
24
|
+
* paths, so they line up — but the SDK's authorization-server metadata would
|
|
25
|
+
* advertise the wrong (origin-level) URLs, so we override that one document.
|
|
21
26
|
*/
|
|
22
27
|
export declare function registerOAuth(app: Express, opts: RegisterOAuthOptions): RequestHandler;
|
|
23
28
|
/**
|