@honeyfield/rent2b-mcp 1.2.2 → 1.4.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.
@@ -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
- // GET /zapier/me on every request.
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
- // 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.
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') {
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
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,26 @@ export interface AuthorizePageParams {
7
7
  scopes: string;
8
8
  resource: string;
9
9
  }
10
- export declare function renderAuthorizePage(p: AuthorizePageParams): string;
11
- export interface AuthorizeSubmitFields {
12
- apiKey: string;
10
+ export declare function renderAuthorizePage(p: AuthorizePageParams, errorMessage?: string, showSocial?: boolean): string;
11
+ interface FlowFields {
13
12
  clientId: string;
14
13
  redirectUri: string;
15
14
  state: string;
16
15
  codeChallenge: string;
17
16
  }
17
+ export type AuthorizeSubmitFields = ({
18
+ mode: 'password';
19
+ email: string;
20
+ password: string;
21
+ } & FlowFields) | ({
22
+ mode: 'session';
23
+ accessToken: string;
24
+ refreshToken: string;
25
+ } & FlowFields);
18
26
  export declare function parseAuthorizeSubmit(body: Record<string, unknown>): AuthorizeSubmitFields;
27
+ /** Validate + extract the OAuth-flow fields for the /authorize/social handler. */
28
+ export declare function parseSocialStart(query: Record<string, unknown>): {
29
+ provider: 'google' | 'apple';
30
+ flow: Record<string, string>;
31
+ };
32
+ export {};
@@ -8,37 +8,64 @@ function esc(value) {
8
8
  function hidden(name, value) {
9
9
  return `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`;
10
10
  }
11
- export function renderAuthorizePage(p) {
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
- // action is relative so it resolves under the gateway path prefix (/rent2b/authorize/submit)
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&amp;${q}">Continue with Google</a>
44
+ <a class="sso" href="authorize/social?provider=apple&amp;${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>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>
53
+ <title>Sign in to rent2b</title>
54
+ <style>${STYLE}</style>
27
55
  </head>
28
56
  <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>
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('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>
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
  }
@@ -49,12 +76,37 @@ export function parseAuthorizeSubmit(body) {
49
76
  throw new Error(`Missing field: ${k}`);
50
77
  return v;
51
78
  };
52
- return {
53
- apiKey: str('apiKey'),
79
+ const flow = {
54
80
  clientId: str('clientId'),
55
81
  redirectUri: str('redirectUri'),
56
82
  state: typeof body.state === 'string' ? body.state : '',
57
83
  codeChallenge: str('codeChallenge'),
58
84
  };
85
+ if (typeof body.access_token === 'string' && body.access_token.length > 0) {
86
+ return {
87
+ mode: 'session',
88
+ accessToken: str('access_token'),
89
+ refreshToken: str('refresh_token'),
90
+ ...flow,
91
+ };
92
+ }
93
+ return {
94
+ mode: 'password',
95
+ email: str('email'),
96
+ password: str('password'),
97
+ ...flow,
98
+ };
99
+ }
100
+ /** Validate + extract the OAuth-flow fields for the /authorize/social handler. */
101
+ export function parseSocialStart(query) {
102
+ const provider = query.provider === 'apple' ? 'apple' : 'google';
103
+ const flow = {};
104
+ for (const k of FLOW_FIELDS) {
105
+ flow[k] = typeof query[k] === 'string' ? query[k] : '';
106
+ }
107
+ if (!flow.clientId || !flow.redirectUri || !flow.codeChallenge) {
108
+ throw new Error('Missing OAuth flow parameters');
109
+ }
110
+ return { provider, flow };
59
111
  }
60
112
  //# 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,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"}
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
- 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"');
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&quot;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 valid body', () => {
23
- const fields = parseAuthorizeSubmit({
24
- apiKey: 'r2b_x',
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(fields.apiKey).toBe('r2b_x');
31
- expect(fields.redirectUri).toBe('https://claude.ai/cb');
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('throws when apiKey is missing', () => {
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,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"}
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,EACL,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE,QAAQ;QACpB,WAAW,EAAE,sBAAsB;QACnC,KAAK,EAAE,QAAQ;QACf,aAAa,EAAE,MAAM;QACrB,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,EAAE;KACb,CAAC;IAEF,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,IAAI,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAC1C,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;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QACtE,MAAM,CAAC,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAC5D,kCAAkC,CACnC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,IAAI,GAAG,mBAAmB,CAC9B;YACE,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,GAAG;YAChB,KAAK,EAAE,EAAE;YACT,aAAa,EAAE,GAAG;YAClB,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;SACb,EACD,2BAA2B,CAC5B,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,GAAG,oBAAoB,CAAC;YAC7B,KAAK,EAAE,OAAO;YACd,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,sBAAsB;YACnC,KAAK,EAAE,GAAG;YACV,aAAa,EAAE,MAAM;SACtB,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChC,IAAI,CAAC,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC1B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC9B,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;QACD,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,GAAG,oBAAoB,CAAC;YAC7B,YAAY,EAAE,IAAI;YAClB,aAAa,EAAE,IAAI;YACnB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,GAAG;YAChB,aAAa,EAAE,GAAG;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/B,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,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;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,GAAG,gBAAgB,CAAC;YACzB,QAAQ,EAAE,QAAQ;YAClB,QAAQ,EAAE,IAAI;YACd,WAAW,EAAE,sBAAsB;YACnC,aAAa,EAAE,MAAM;YACrB,KAAK,EAAE,GAAG;YACV,MAAM,EAAE,EAAE;YACV,QAAQ,EAAE,EAAE;SACb,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,23 +1,34 @@
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';
5
+ import { type SupabaseOAuthClient, type SocialFlowStore } from './social.js';
6
+ export interface SocialDeps {
7
+ oauth: SupabaseOAuthClient;
8
+ store: SocialFlowStore;
9
+ }
4
10
  export interface RegisterOAuthOptions {
5
11
  oauthCfg: OAuthRuntimeConfig;
6
12
  provider: Rent2bOAuthProvider;
7
- validateKey: (apiKey: string) => Promise<number>;
13
+ apiAuth: ApiAuthClient;
14
+ /** When set, enables server-side Google/Apple social login. */
15
+ social?: SocialDeps;
8
16
  }
9
17
  /**
10
18
  * Wires the OAuth 2.1 + DCR surface onto an Express app and returns the
11
19
  * bearer-auth middleware to guard the MCP endpoint.
12
20
  *
21
+ * Login is the user's own rent2b account: email/password (delegated to the
22
+ * rent2b-api auth endpoints) or Google/Apple (a server-side Supabase PKCE flow
23
+ * run here). The issued token carries the resulting Supabase session (see
24
+ * provider.mintSessionTokens). Raw `r2b_` bearer tokens and `X-API-Key` headers
25
+ * still work (backward compat).
26
+ *
13
27
  * 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.
28
+ * origin root and advertises them origin-level, dropping any issuer path.
29
+ * Behind the gateway, Caddy strips `/rent2b` before the container sees these
30
+ * paths, so they line up but the SDK's authorization-server metadata would
31
+ * advertise the wrong (origin-level) URLs, so we override that one document.
21
32
  */
22
33
  export declare function registerOAuth(app: Express, opts: RegisterOAuthOptions): RequestHandler;
23
34
  /**