stables-mcp-server 2.4.0 → 2.5.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/README.md +27 -3
- package/build/http/handler.d.ts +9 -0
- package/build/http/handler.d.ts.map +1 -1
- package/build/http/handler.js +22 -8
- package/build/http/handler.js.map +1 -1
- package/build/lib/stables-client.d.ts +17 -1
- package/build/lib/stables-client.d.ts.map +1 -1
- package/build/lib/stables-client.js +23 -1
- package/build/lib/stables-client.js.map +1 -1
- package/build/oauth/auth0.d.ts +82 -0
- package/build/oauth/auth0.d.ts.map +1 -0
- package/build/oauth/auth0.js +190 -0
- package/build/oauth/auth0.js.map +1 -0
- package/build/oauth/handlers.d.ts +11 -0
- package/build/oauth/handlers.d.ts.map +1 -0
- package/build/oauth/handlers.js +203 -0
- package/build/oauth/handlers.js.map +1 -0
- package/build/oauth/pages.d.ts +14 -0
- package/build/oauth/pages.d.ts.map +1 -0
- package/build/oauth/pages.js +138 -0
- package/build/oauth/pages.js.map +1 -0
- package/build/oauth/provider.d.ts +21 -0
- package/build/oauth/provider.d.ts.map +1 -0
- package/build/oauth/provider.js +95 -0
- package/build/oauth/provider.js.map +1 -0
- package/build/oauth/types.d.ts +41 -0
- package/build/oauth/types.d.ts.map +1 -0
- package/build/oauth/types.js +14 -0
- package/build/oauth/types.js.map +1 -0
- package/build/tools/webhooks.d.ts +8 -0
- package/build/tools/webhooks.d.ts.map +1 -1
- package/build/tools/webhooks.js +15 -2
- package/build/tools/webhooks.js.map +1 -1
- package/build/version.d.ts +1 -1
- package/build/version.js +1 -1
- package/build/worker.d.ts +20 -7
- package/build/worker.d.ts.map +1 -1
- package/build/worker.js +35 -7
- package/build/worker.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 as the identity step behind the hosted server's OAuth, and the
|
|
3
|
+
* Stables API call that turns a signed-in user into an API key.
|
|
4
|
+
*
|
|
5
|
+
* Sandbox and production are separate Auth0 organisations with separate
|
|
6
|
+
* dashboard API audiences, so the environment is chosen before the redirect
|
|
7
|
+
* to Auth0 and decides which audience is requested and which API mints the
|
|
8
|
+
* key. A token for the dashboard audience is what /dashboard/v1 routes
|
|
9
|
+
* accept, so no api-service change is needed.
|
|
10
|
+
*/
|
|
11
|
+
import { SERVER_VERSION } from "../version.js";
|
|
12
|
+
/**
|
|
13
|
+
* Sent on every server-side call. Cloudflare in front of the Auth0 custom
|
|
14
|
+
* domain answers 403 to requests with no User-Agent, and Worker subrequests
|
|
15
|
+
* carry none by default.
|
|
16
|
+
*/
|
|
17
|
+
export const SERVER_USER_AGENT = `stables-mcp-server/${SERVER_VERSION} (hosted)`;
|
|
18
|
+
export const API_BASE = {
|
|
19
|
+
sandbox: "https://api.sandbox.stables.money",
|
|
20
|
+
production: "https://api.stables.money",
|
|
21
|
+
};
|
|
22
|
+
/** Auth0 API identifier the dashboard uses for this environment. */
|
|
23
|
+
export function dashboardAudience(environment) {
|
|
24
|
+
return `${API_BASE[environment]}/dashboard`;
|
|
25
|
+
}
|
|
26
|
+
export function parseEnvironment(value) {
|
|
27
|
+
return value === "sandbox" || value === "production" ? value : null;
|
|
28
|
+
}
|
|
29
|
+
export function environmentForKey(apiKey) {
|
|
30
|
+
return apiKey.split("_")[1] === "live" ? "production" : "sandbox";
|
|
31
|
+
}
|
|
32
|
+
export function auth0ConfigFrom(env) {
|
|
33
|
+
if (!env.AUTH0_DOMAIN || !env.AUTH0_CLIENT_ID || !env.AUTH0_CLIENT_SECRET)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
domain: env.AUTH0_DOMAIN,
|
|
37
|
+
clientId: env.AUTH0_CLIENT_ID,
|
|
38
|
+
clientSecret: env.AUTH0_CLIENT_SECRET,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** A failure in one step of the sign-in, shown to the user on our own page. */
|
|
42
|
+
export class OAuthStepError extends Error {
|
|
43
|
+
step;
|
|
44
|
+
status;
|
|
45
|
+
detail;
|
|
46
|
+
constructor(step, message, status, detail) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.step = step;
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.detail = detail;
|
|
51
|
+
this.name = "OAuthStepError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---- PKCE towards Auth0 ----------------------------------------------------
|
|
55
|
+
function base64url(bytes) {
|
|
56
|
+
let binary = "";
|
|
57
|
+
for (const b of bytes)
|
|
58
|
+
binary += String.fromCharCode(b);
|
|
59
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
60
|
+
}
|
|
61
|
+
export function randomVerifier() {
|
|
62
|
+
return base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
63
|
+
}
|
|
64
|
+
export async function s256(verifier) {
|
|
65
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
66
|
+
return base64url(new Uint8Array(digest));
|
|
67
|
+
}
|
|
68
|
+
// ---- Auth0 endpoints -------------------------------------------------------
|
|
69
|
+
export function auth0AuthorizeUrl(config, params) {
|
|
70
|
+
const url = new URL(`https://${config.domain}/authorize`);
|
|
71
|
+
url.searchParams.set("response_type", "code");
|
|
72
|
+
url.searchParams.set("client_id", config.clientId);
|
|
73
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
74
|
+
url.searchParams.set("scope", "openid profile email");
|
|
75
|
+
url.searchParams.set("audience", dashboardAudience(params.environment));
|
|
76
|
+
url.searchParams.set("state", params.state);
|
|
77
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
78
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
79
|
+
// Always ask for credentials. An agent connection mints a long-lived API key
|
|
80
|
+
// in whichever account signs in, and a browser can hold an Auth0 session for
|
|
81
|
+
// someone else (staff who logged in as a customer, a shared machine): that
|
|
82
|
+
// session must never be reused silently here.
|
|
83
|
+
url.searchParams.set("prompt", "login");
|
|
84
|
+
return url.toString();
|
|
85
|
+
}
|
|
86
|
+
/** Auth0's own error_description when the body is its JSON; nothing for an HTML block page. */
|
|
87
|
+
function describeAuth0Error(body) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(body);
|
|
90
|
+
const text = parsed.error_description ?? parsed.error;
|
|
91
|
+
return typeof text === "string" && text ? `: ${text.slice(0, 160)}` : "";
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export async function exchangeAuth0Code(config, params, fetchImpl = fetch) {
|
|
98
|
+
const response = await fetchImpl(`https://${config.domain}/oauth/token`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: {
|
|
101
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
102
|
+
"user-agent": SERVER_USER_AGENT,
|
|
103
|
+
},
|
|
104
|
+
body: new URLSearchParams({
|
|
105
|
+
grant_type: "authorization_code",
|
|
106
|
+
client_id: config.clientId,
|
|
107
|
+
client_secret: config.clientSecret,
|
|
108
|
+
code: params.code,
|
|
109
|
+
redirect_uri: params.redirectUri,
|
|
110
|
+
code_verifier: params.verifier,
|
|
111
|
+
}).toString(),
|
|
112
|
+
});
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const detail = await response.text().catch(() => "");
|
|
115
|
+
throw new OAuthStepError("auth0_token", `Sign-in could not be completed (Auth0 answered HTTP ${response.status}${describeAuth0Error(detail)}).`, response.status, detail);
|
|
116
|
+
}
|
|
117
|
+
const body = (await response.json());
|
|
118
|
+
if (!body.access_token || !body.id_token) {
|
|
119
|
+
throw new OAuthStepError("auth0_token", "Sign-in returned no tokens.");
|
|
120
|
+
}
|
|
121
|
+
return { access_token: body.access_token, id_token: body.id_token };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The ID token came straight from Auth0's token endpoint over TLS in the
|
|
125
|
+
* exchange above, so its claims are read without a signature check.
|
|
126
|
+
*/
|
|
127
|
+
export function decodeJwtPayload(jwt) {
|
|
128
|
+
const parts = jwt.split(".");
|
|
129
|
+
if (parts.length < 2)
|
|
130
|
+
throw new OAuthStepError("identity", "Malformed ID token.");
|
|
131
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
132
|
+
const padded = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
|
|
133
|
+
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
134
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
135
|
+
}
|
|
136
|
+
export function identityFromIdToken(idToken) {
|
|
137
|
+
const payload = decodeJwtPayload(idToken);
|
|
138
|
+
const sub = typeof payload.sub === "string" ? payload.sub : "";
|
|
139
|
+
if (!sub)
|
|
140
|
+
throw new OAuthStepError("identity", "ID token has no subject.");
|
|
141
|
+
return {
|
|
142
|
+
sub,
|
|
143
|
+
email: typeof payload.email === "string" ? payload.email.toLowerCase() : undefined,
|
|
144
|
+
name: typeof payload.name === "string" ? payload.name : undefined,
|
|
145
|
+
orgId: typeof payload.org_id === "string" ? payload.org_id : undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function explainMintFailure(status, environment) {
|
|
149
|
+
switch (status) {
|
|
150
|
+
case 401:
|
|
151
|
+
return `Stables did not accept the sign-in for ${environment}. Pick an organisation that exists on ${environment}, or sign in again.`;
|
|
152
|
+
case 403:
|
|
153
|
+
return "Your role in this organisation can't create API keys. Ask an organisation admin to connect, or to grant you API-key access.";
|
|
154
|
+
case 404:
|
|
155
|
+
return `This organisation isn't set up on ${environment} yet.`;
|
|
156
|
+
default:
|
|
157
|
+
return `Stables answered HTTP ${status} while creating the agent's API key. Try again in a moment.`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function mintApiKey(params, fetchImpl = fetch) {
|
|
161
|
+
const response = await fetchImpl(`${API_BASE[params.environment]}/dashboard/v1/api-keys`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
authorization: `Bearer ${params.accessToken}`,
|
|
165
|
+
"content-type": "application/json",
|
|
166
|
+
"idempotency-key": crypto.randomUUID(),
|
|
167
|
+
"user-agent": SERVER_USER_AGENT,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({ name: params.name, metadata: params.metadata }),
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const detail = await response.text().catch(() => "");
|
|
173
|
+
throw new OAuthStepError("mint_key", explainMintFailure(response.status, params.environment), response.status, detail);
|
|
174
|
+
}
|
|
175
|
+
const body = (await response.json());
|
|
176
|
+
if (!body.plaintextKey || !body.apiKey?.apiKeyId) {
|
|
177
|
+
throw new OAuthStepError("mint_key", "Stables returned no API key.");
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
apiKeyId: body.apiKey.apiKeyId,
|
|
181
|
+
prefix: body.apiKey.prefix ?? "",
|
|
182
|
+
plaintextKey: body.plaintextKey,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** API keys are listed by name in the dashboard, so say which agent and who. */
|
|
186
|
+
export function apiKeyName(clientName, identity) {
|
|
187
|
+
const who = identity.email ?? identity.sub;
|
|
188
|
+
return `${clientName} via MCP (${who})`.slice(0, 255);
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=auth0.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../src/oauth/auth0.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,cAAc,WAAW,CAAC;AAEjF,MAAM,CAAC,MAAM,QAAQ,GAAuC;IAC1D,OAAO,EAAE,mCAAmC;IAC5C,UAAU,EAAE,2BAA2B;CACxC,CAAC;AAEF,oEAAoE;AACpE,MAAM,UAAU,iBAAiB,CAAC,WAA+B;IAC/D,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,YAAY,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAQD,MAAM,UAAU,eAAe,CAAC,GAI/B;IACC,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,GAAG,CAAC,mBAAmB;QAAE,OAAO,IAAI,CAAC;IACvF,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,YAAY;QACxB,QAAQ,EAAE,GAAG,CAAC,eAAe;QAC7B,YAAY,EAAE,GAAG,CAAC,mBAAmB;KACtC,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,OAAO,cAAe,SAAQ,KAAK;IAErB;IAEA;IACA;IAJlB,YACkB,IAA0D,EAC1E,OAAe,EACC,MAAe,EACf,MAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QALC,SAAI,GAAJ,IAAI,CAAsD;QAE1D,WAAM,GAAN,MAAM,CAAS;QACf,WAAM,GAAN,MAAM,CAAS;QAG/B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,+EAA+E;AAE/E,SAAS,SAAS,CAAC,KAAiB;IAClC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACxD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,SAAS,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,QAAgB;IACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzF,OAAO,SAAS,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,+EAA+E;AAE/E,MAAM,UAAU,iBAAiB,CAC/B,MAAmB,EACnB,MAKC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,YAAY,CAAC,CAAC;IAC1D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACnD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACzD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC;IACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IACxE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC7D,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;IACtD,6EAA6E;IAC7E,6EAA6E;IAC7E,2EAA2E;IAC3E,8CAA8C;IAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACxC,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED,+FAA+F;AAC/F,SAAS,kBAAkB,CAAC,IAAY;IACtC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAqD,CAAC;QACpF,MAAM,IAAI,GAAG,MAAM,CAAC,iBAAiB,IAAI,MAAM,CAAC,KAAK,CAAC;QACtD,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAOD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAmB,EACnB,MAA+D,EAC/D,YAA0B,KAAK;IAE/B,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,WAAW,MAAM,CAAC,MAAM,cAAc,EAAE;QACvE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,mCAAmC;YACnD,YAAY,EAAE,iBAAiB;SAChC;QACD,IAAI,EAAE,IAAI,eAAe,CAAC;YACxB,UAAU,EAAE,oBAAoB;YAChC,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;YAClC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,YAAY,EAAE,MAAM,CAAC,WAAW;YAChC,aAAa,EAAE,MAAM,CAAC,QAAQ;SAC/B,CAAC,CAAC,QAAQ,EAAE;KACd,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,IAAI,cAAc,CACtB,aAAa,EACb,uDAAuD,QAAQ,CAAC,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,IAAI,EACvG,QAAQ,CAAC,MAAM,EACf,MAAM,CACP,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAyB,CAAC;IAC7D,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzC,MAAM,IAAI,cAAc,CAAC,aAAa,EAAE,6BAA6B,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC;AACtE,CAAC;AAWD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,MAAM,IAAI,cAAc,CAAC,UAAU,EAAE,qBAAqB,CAAC,CAAC;IAClF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAA4B,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,MAAM,OAAO,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/D,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,cAAc,CAAC,UAAU,EAAE,0BAA0B,CAAC,CAAC;IAC3E,OAAO;QACL,GAAG;QACH,KAAK,EAAE,OAAO,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS;QAClF,IAAI,EAAE,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QACjE,KAAK,EAAE,OAAO,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;KACvE,CAAC;AACJ,CAAC;AAUD,SAAS,kBAAkB,CAAC,MAAc,EAAE,WAA+B;IACzE,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,GAAG;YACN,OAAO,0CAA0C,WAAW,yCAAyC,WAAW,qBAAqB,CAAC;QACxI,KAAK,GAAG;YACN,OAAO,6HAA6H,CAAC;QACvI,KAAK,GAAG;YACN,OAAO,qCAAqC,WAAW,OAAO,CAAC;QACjE;YACE,OAAO,yBAAyB,MAAM,6DAA6D,CAAC;IACxG,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAKC,EACD,YAA0B,KAAK;IAE/B,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,wBAAwB,EAAE;QACxF,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,MAAM,CAAC,WAAW,EAAE;YAC7C,cAAc,EAAE,kBAAkB;YAClC,iBAAiB,EAAE,MAAM,CAAC,UAAU,EAAE;YACtC,YAAY,EAAE,iBAAiB;SAChC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;KACvE,CAAC,CAAC;IACH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,IAAI,cAAc,CACtB,UAAU,EACV,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,EACvD,QAAQ,CAAC,MAAM,EACf,MAAM,CACP,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAGlC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;QACjD,MAAM,IAAI,cAAc,CAAC,UAAU,EAAE,8BAA8B,CAAC,CAAC;IACvE,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;QAC9B,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE;QAChC,YAAY,EAAE,IAAI,CAAC,YAAY;KAChC,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,QAAkB;IAC/D,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,GAAG,CAAC;IAC3C,OAAO,GAAG,UAAU,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-facing half of the OAuth flow: the consent page at /authorize
|
|
3
|
+
* and the Auth0 return at /callback. The provider owns everything else
|
|
4
|
+
* (discovery, registration, the token endpoint) and hands requests without
|
|
5
|
+
* a valid token to handleDefault.
|
|
6
|
+
*/
|
|
7
|
+
import { type OAuthEnv } from "./types.js";
|
|
8
|
+
/** The scopes a grant gets: what the client asked for, limited to ours, always the base scope. */
|
|
9
|
+
export declare function grantScope(requested: string[]): string[];
|
|
10
|
+
export declare function handleDefault(request: Request, env: OAuthEnv): Promise<Response>;
|
|
11
|
+
//# sourceMappingURL=handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../../src/oauth/handlers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,EAIL,KAAK,QAAQ,EAEd,MAAM,YAAY,CAAC;AAyBpB,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAIxD;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8CtF"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser-facing half of the OAuth flow: the consent page at /authorize
|
|
3
|
+
* and the Auth0 return at /callback. The provider owns everything else
|
|
4
|
+
* (discovery, registration, the token endpoint) and hands requests without
|
|
5
|
+
* a valid token to handleDefault.
|
|
6
|
+
*/
|
|
7
|
+
import { AuthorizationError, CimdFetchError, authorizationErrorRedirect, } from "@cloudflare/workers-oauth-provider";
|
|
8
|
+
import { MCP_PATH, handleRequest, json, withCors } from "../http/handler.js";
|
|
9
|
+
import { OAuthStepError, apiKeyName, auth0AuthorizeUrl, auth0ConfigFrom, exchangeAuth0Code, identityFromIdToken, mintApiKey, parseEnvironment, randomVerifier, s256, } from "./auth0.js";
|
|
10
|
+
import { consentPage, errorPage } from "./pages.js";
|
|
11
|
+
import { SCOPE, SUPPORTED_SCOPES, } from "./types.js";
|
|
12
|
+
function html(body, status = 200, headers) {
|
|
13
|
+
const h = new Headers(headers);
|
|
14
|
+
h.set("content-type", "text/html; charset=utf-8");
|
|
15
|
+
h.set("cache-control", "no-store");
|
|
16
|
+
return new Response(body, { status, headers: h });
|
|
17
|
+
}
|
|
18
|
+
function redirect(headers, location) {
|
|
19
|
+
if (location)
|
|
20
|
+
headers.set("Location", location);
|
|
21
|
+
return new Response(null, { status: 302, headers });
|
|
22
|
+
}
|
|
23
|
+
function publicOrigin(request, env) {
|
|
24
|
+
return (env.MCP_PUBLIC_URL ?? new URL(request.url).origin).replace(/\/$/, "");
|
|
25
|
+
}
|
|
26
|
+
/** The scopes a grant gets: what the client asked for, limited to ours, always the base scope. */
|
|
27
|
+
export function grantScope(requested) {
|
|
28
|
+
const scope = requested.filter((s) => SUPPORTED_SCOPES.includes(s));
|
|
29
|
+
if (!scope.includes(SCOPE))
|
|
30
|
+
scope.unshift(SCOPE);
|
|
31
|
+
return scope;
|
|
32
|
+
}
|
|
33
|
+
export async function handleDefault(request, env) {
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
const path = url.pathname;
|
|
36
|
+
if (path === "/authorize" || path === "/callback") {
|
|
37
|
+
const oauth = env.OAUTH_PROVIDER;
|
|
38
|
+
if (!oauth) {
|
|
39
|
+
return html(errorPage("Sign-in unavailable", "OAuth is not configured on this server."), 503);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (path === "/authorize" && request.method === "GET") {
|
|
43
|
+
return await authorizeGet(request, env, oauth);
|
|
44
|
+
}
|
|
45
|
+
if (path === "/authorize" && request.method === "POST") {
|
|
46
|
+
return await authorizePost(request, env, oauth);
|
|
47
|
+
}
|
|
48
|
+
if (path === "/callback" && request.method === "GET") {
|
|
49
|
+
return await callback(request, env, oauth);
|
|
50
|
+
}
|
|
51
|
+
return new Response("Method not allowed", { status: 405, headers: { allow: "GET, POST" } });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return renderOAuthError(error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// The provider sends an /mcp request here only when it carries no valid
|
|
58
|
+
// token. Answer with the spec's challenge so clients find the metadata.
|
|
59
|
+
if (path === MCP_PATH || path === `${MCP_PATH}/`) {
|
|
60
|
+
const origin = publicOrigin(request, env);
|
|
61
|
+
return withCors(json({
|
|
62
|
+
error: "unauthorized",
|
|
63
|
+
message: "Send a Stables API key as a bearer token, or connect with OAuth (see WWW-Authenticate).",
|
|
64
|
+
docs: "https://docs.stables.money/get-started/getting-started/quickstart/building-with-ai",
|
|
65
|
+
}, 401, {
|
|
66
|
+
"www-authenticate": `Bearer realm="OAuth", resource_metadata="${origin}/.well-known/oauth-protected-resource/mcp", scope="${SCOPE}"`,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
return handleRequest(request);
|
|
70
|
+
}
|
|
71
|
+
async function authorizeGet(request, env, oauth) {
|
|
72
|
+
if (!auth0ConfigFrom(env)) {
|
|
73
|
+
return html(errorPage("Sign-in unavailable", "This server has no identity provider configured yet."), 503);
|
|
74
|
+
}
|
|
75
|
+
const authRequest = await oauth.parseAuthRequest(request);
|
|
76
|
+
// describeConsent first: a failed client lookup leaves nothing in KV.
|
|
77
|
+
const details = await oauth.describeConsent(authRequest);
|
|
78
|
+
const consent = await oauth.beginConsent(authRequest);
|
|
79
|
+
return html(consentPage(details, consent.handle), 200, consent.headers);
|
|
80
|
+
}
|
|
81
|
+
async function authorizePost(request, env, oauth) {
|
|
82
|
+
const config = auth0ConfigFrom(env);
|
|
83
|
+
if (!config) {
|
|
84
|
+
return html(errorPage("Sign-in unavailable", "This server has no identity provider configured yet."), 503);
|
|
85
|
+
}
|
|
86
|
+
const form = await request.formData();
|
|
87
|
+
const handle = String(form.get("handle") ?? "");
|
|
88
|
+
if (form.get("decision") !== "approve") {
|
|
89
|
+
const denied = await oauth.denyConsent(request, handle);
|
|
90
|
+
return redirect(denied.headers);
|
|
91
|
+
}
|
|
92
|
+
const environment = parseEnvironment(form.get("environment"));
|
|
93
|
+
if (!environment) {
|
|
94
|
+
return html(errorPage("Pick an environment", "Choose Sandbox or Production and try again."), 400);
|
|
95
|
+
}
|
|
96
|
+
const approved = await oauth.approveConsent(request, handle);
|
|
97
|
+
const scope = grantScope(approved.request.scope);
|
|
98
|
+
const verifier = randomVerifier();
|
|
99
|
+
const data = { environment, verifier, scope };
|
|
100
|
+
const { state, headers } = await oauth.beginUpstream(approved.request, {
|
|
101
|
+
data,
|
|
102
|
+
headers: approved.headers,
|
|
103
|
+
});
|
|
104
|
+
const location = auth0AuthorizeUrl(config, {
|
|
105
|
+
redirectUri: `${publicOrigin(request, env)}/callback`,
|
|
106
|
+
environment,
|
|
107
|
+
state,
|
|
108
|
+
codeChallenge: await s256(verifier),
|
|
109
|
+
});
|
|
110
|
+
return redirect(headers, location);
|
|
111
|
+
}
|
|
112
|
+
async function callback(request, env, oauth) {
|
|
113
|
+
const config = auth0ConfigFrom(env);
|
|
114
|
+
if (!config) {
|
|
115
|
+
return html(errorPage("Sign-in unavailable", "This server has no identity provider configured yet."), 503);
|
|
116
|
+
}
|
|
117
|
+
const { request: original, data, headers } = await oauth.finishUpstream(request);
|
|
118
|
+
const params = new URL(request.url).searchParams;
|
|
119
|
+
if (params.get("error")) {
|
|
120
|
+
return redirect(headers, authorizationErrorRedirect(original, "access_denied"));
|
|
121
|
+
}
|
|
122
|
+
const code = params.get("code");
|
|
123
|
+
if (!code)
|
|
124
|
+
throw new OAuthStepError("callback", "Auth0 sent the user back without a code.");
|
|
125
|
+
const origin = publicOrigin(request, env);
|
|
126
|
+
const tokens = await exchangeAuth0Code(config, {
|
|
127
|
+
code,
|
|
128
|
+
redirectUri: `${origin}/callback`,
|
|
129
|
+
verifier: data.verifier,
|
|
130
|
+
});
|
|
131
|
+
const identity = identityFromIdToken(tokens.id_token);
|
|
132
|
+
const clientName = (await clientNameFor(oauth, original)) ?? "MCP client";
|
|
133
|
+
const minted = await mintApiKey({
|
|
134
|
+
environment: data.environment,
|
|
135
|
+
accessToken: tokens.access_token,
|
|
136
|
+
name: apiKeyName(clientName, identity),
|
|
137
|
+
metadata: {
|
|
138
|
+
source: "mcp_oauth",
|
|
139
|
+
client: clientName,
|
|
140
|
+
user: identity.email ?? identity.sub,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const props = {
|
|
144
|
+
apiKey: minted.plaintextKey,
|
|
145
|
+
apiKeyId: minted.apiKeyId,
|
|
146
|
+
environment: data.environment,
|
|
147
|
+
email: identity.email,
|
|
148
|
+
orgId: identity.orgId,
|
|
149
|
+
clientName,
|
|
150
|
+
};
|
|
151
|
+
const { redirectTo } = await oauth.completeAuthorization({
|
|
152
|
+
request: original,
|
|
153
|
+
// One grant per user, organisation and environment, so re-authorising
|
|
154
|
+
// sandbox never revokes a production grant for the same person.
|
|
155
|
+
userId: [data.environment, identity.orgId ?? "-", identity.sub].join("|").replace(/:/g, "_"),
|
|
156
|
+
metadata: {
|
|
157
|
+
environment: data.environment,
|
|
158
|
+
client: clientName,
|
|
159
|
+
email: identity.email,
|
|
160
|
+
orgId: identity.orgId,
|
|
161
|
+
apiKeyId: minted.apiKeyId,
|
|
162
|
+
keyPrefix: minted.prefix,
|
|
163
|
+
},
|
|
164
|
+
scope: data.scope,
|
|
165
|
+
props,
|
|
166
|
+
});
|
|
167
|
+
return redirect(headers, redirectTo);
|
|
168
|
+
}
|
|
169
|
+
async function clientNameFor(oauth, authRequest) {
|
|
170
|
+
try {
|
|
171
|
+
const client = await oauth.lookupClient(authRequest.clientId);
|
|
172
|
+
const name = client?.clientName?.trim();
|
|
173
|
+
return name ? name.slice(0, 80) : null;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function renderOAuthError(error) {
|
|
180
|
+
if (error instanceof AuthorizationError && error.redirectTo) {
|
|
181
|
+
return Response.redirect(error.redirectTo, 302);
|
|
182
|
+
}
|
|
183
|
+
if (error instanceof AuthorizationError) {
|
|
184
|
+
return html(errorPage("Connection request expired", error.description ||
|
|
185
|
+
"This page was already used, opened in another browser, or has expired."), 400);
|
|
186
|
+
}
|
|
187
|
+
if (error instanceof CimdFetchError) {
|
|
188
|
+
return html(errorPage("App could not be verified", "The app's identity document could not be fetched."), 400);
|
|
189
|
+
}
|
|
190
|
+
if (error instanceof OAuthStepError) {
|
|
191
|
+
// Workers Logs is the only place to see why a sign-in failed server-side.
|
|
192
|
+
// eslint-disable-next-line no-console
|
|
193
|
+
console.error(JSON.stringify({
|
|
194
|
+
event: "oauth_step_failed",
|
|
195
|
+
step: error.step,
|
|
196
|
+
status: error.status,
|
|
197
|
+
detail: error.detail?.slice(0, 300),
|
|
198
|
+
}));
|
|
199
|
+
return html(errorPage("Connection failed", error.message), error.status === 403 ? 403 : 400);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/oauth/handlers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two pages the OAuth flow shows in a browser: consent and error. They
|
|
3
|
+
* share the Stables login's design (stables-login, auth.css): coin video on
|
|
4
|
+
* the left, dark card on the right, PP Agrandir headings, DM Sans body, so
|
|
5
|
+
* the hop from here to the Auth0 login screen looks like one flow.
|
|
6
|
+
*
|
|
7
|
+
* Everything that came from the client (its name, domain, scopes) is
|
|
8
|
+
* escaped, since a dynamically registered client chooses those strings.
|
|
9
|
+
*/
|
|
10
|
+
import type { ConsentDescription } from "@cloudflare/workers-oauth-provider";
|
|
11
|
+
export declare function escapeHtml(value: string): string;
|
|
12
|
+
export declare function consentPage(details: ConsentDescription, handle: string): string;
|
|
13
|
+
export declare function errorPage(title: string, message: string, hint?: string): string;
|
|
14
|
+
//# sourceMappingURL=pages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["../../src/oauth/pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAE7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAiGD,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAgC/E;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAU/E"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two pages the OAuth flow shows in a browser: consent and error. They
|
|
3
|
+
* share the Stables login's design (stables-login, auth.css): coin video on
|
|
4
|
+
* the left, dark card on the right, PP Agrandir headings, DM Sans body, so
|
|
5
|
+
* the hop from here to the Auth0 login screen looks like one flow.
|
|
6
|
+
*
|
|
7
|
+
* Everything that came from the client (its name, domain, scopes) is
|
|
8
|
+
* escaped, since a dynamically registered client chooses those strings.
|
|
9
|
+
*/
|
|
10
|
+
export function escapeHtml(value) {
|
|
11
|
+
return value.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
12
|
+
}
|
|
13
|
+
// Same assets the login screens use (Webflow CDN).
|
|
14
|
+
const SIDE_VIDEO = "https://cdn.prod.website-files.com/647fe94790c03420770215ff/6a28dc85362ee9ec135591de_coin1_mp4.mp4";
|
|
15
|
+
const SIDE_POSTER = "https://cdn.prod.website-files.com/647fe94790c03420770215ff/6a2295f0d320c08683feda80_Frame%201000008940.png";
|
|
16
|
+
const LOGO = "https://cdn.prod.website-files.com/647fe94790c03420770215ff/668cbc7303652c18886b3e83_White-logo.svg";
|
|
17
|
+
const DISPLAY_FONT = "https://cdn.prod.website-files.com/647fe94790c03420770215ff/64e6d629d66cf95dd050563e_PPAgrandir-Bold.woff2";
|
|
18
|
+
const STYLE = `
|
|
19
|
+
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap');
|
|
20
|
+
@font-face{font-family:'PP Agrandir';src:url('${DISPLAY_FONT}') format('woff2');font-weight:700;font-style:normal;font-display:swap}
|
|
21
|
+
:root{--bg:#000;--card:#131315;--field:#0e0e10;--border:#2e2e34;--card-border:#1e1e22;--text:#fff;--muted:#8a8a8a;
|
|
22
|
+
--accent:#d0bcff;--accent-text:#1f1033;--neutral300:#c8c8cf;--radius:20px;--pill:31px;
|
|
23
|
+
--font:'DM Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;--display:'PP Agrandir','DM Sans',-apple-system,sans-serif;
|
|
24
|
+
color-scheme:dark}
|
|
25
|
+
*{box-sizing:border-box}
|
|
26
|
+
html,body{height:100%}
|
|
27
|
+
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
|
|
28
|
+
.wrap{display:flex;min-height:100vh;padding:32px;gap:32px}
|
|
29
|
+
.left{flex:1.15 1 0;position:relative;overflow:hidden;border-radius:24px;background:#000}
|
|
30
|
+
.left video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
|
|
31
|
+
.left-overlay{position:absolute;inset:0;background:rgba(0,0,0,.45);z-index:1}
|
|
32
|
+
.hero{position:absolute;top:50%;left:8%;right:8%;transform:translateY(-50%);z-index:2}
|
|
33
|
+
.hero h1{font-family:var(--display);font-size:64px;font-weight:700;line-height:1.1;letter-spacing:-0.03em;margin:0 0 16px}
|
|
34
|
+
.hero p{font-size:18px;line-height:1.2;color:var(--neutral300);margin:0}
|
|
35
|
+
.right{flex:1 1 0;display:flex;align-items:center;justify-content:center}
|
|
36
|
+
.card{width:100%;max-width:560px;background:var(--card);border:1px solid var(--card-border);border-radius:28px;padding:48px}
|
|
37
|
+
.card-inner{width:100%;max-width:420px;margin:0 auto}
|
|
38
|
+
.logo{height:26px;margin-bottom:40px}.logo img{height:100%}
|
|
39
|
+
h2{font-family:var(--display);font-size:26px;font-weight:700;margin:0 0 8px}
|
|
40
|
+
.sub{color:var(--neutral300);font-size:16px;line-height:1.3;margin:0 0 24px}
|
|
41
|
+
.sub strong{color:var(--text);font-weight:500}
|
|
42
|
+
.notice{background:rgba(147,130,255,.10);border:1px solid rgba(147,130,255,.35);color:var(--text);font-size:14px;line-height:1.5;border-radius:14px;padding:12px 14px;margin-bottom:20px}
|
|
43
|
+
.warn{background:rgba(255,196,77,.10);border:1px solid rgba(255,196,77,.4);color:#ffd98a;font-size:14px;line-height:1.5;border-radius:14px;padding:12px 14px;margin-bottom:20px}
|
|
44
|
+
.error{background:rgba(255,77,79,.12);border:1px solid rgba(255,77,79,.4);color:#ffb3b3;font-size:14px;line-height:1.4;border-radius:14px;padding:12px 14px;margin-bottom:20px}
|
|
45
|
+
label.lbl{display:block;font-size:15px;font-weight:500;margin:0 0 10px}
|
|
46
|
+
.env-list{display:flex;flex-direction:column;gap:10px;margin:0 0 24px}
|
|
47
|
+
.env{display:flex;gap:14px;align-items:flex-start;background:var(--field);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px;cursor:pointer;transition:border-color .15s ease}
|
|
48
|
+
.env:hover{border-color:var(--muted)}
|
|
49
|
+
.env:has(input:checked){border-color:var(--accent)}
|
|
50
|
+
.env input{margin:4px 0 0;accent-color:var(--accent);flex:none}
|
|
51
|
+
.env .env-name{display:block;font-size:16px;font-weight:500}
|
|
52
|
+
.env .env-desc{display:block;color:var(--muted);font-size:13px;line-height:1.4;margin-top:2px}
|
|
53
|
+
.env code{background:#1b1b1f;border:1px solid var(--border);padding:1px 6px;border-radius:6px;font-size:12px;color:var(--neutral300)}
|
|
54
|
+
.actions{display:flex;flex-direction:column;gap:10px}
|
|
55
|
+
.btn{width:100%;border:none;cursor:pointer;font-family:var(--font);font-size:16px;font-weight:700;border-radius:var(--pill);padding:16px;transition:opacity .15s ease,transform .05s ease}
|
|
56
|
+
.btn-primary{background:var(--accent);color:var(--accent-text)}
|
|
57
|
+
.btn-primary:hover{opacity:.9}.btn-primary:active{transform:translateY(1px)}
|
|
58
|
+
.btn-secondary{background:transparent;color:var(--text);border:1px solid var(--border);font-weight:500}
|
|
59
|
+
.btn-secondary:hover{border-color:var(--muted)}
|
|
60
|
+
.btn[disabled]{opacity:.55;cursor:default}
|
|
61
|
+
.foot{color:var(--muted);font-size:13px;margin-top:24px;line-height:1.5;text-align:center}
|
|
62
|
+
.foot a{color:var(--muted);text-decoration:none}.foot a:hover{color:var(--accent)}
|
|
63
|
+
@media (max-width:960px){.wrap{flex-direction:column;padding:16px;gap:16px}.left{flex:0 0 220px;min-height:220px}.hero{left:24px;right:24px}.hero h1{font-size:40px}.hero p{font-size:15px}.card{padding:32px 24px}}
|
|
64
|
+
`;
|
|
65
|
+
function layout(options) {
|
|
66
|
+
return `<!doctype html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="utf-8">
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
71
|
+
<meta name="robots" content="noindex">
|
|
72
|
+
<title>${escapeHtml(options.title)}</title>
|
|
73
|
+
<style>${STYLE}</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div class="wrap">
|
|
77
|
+
<div class="left">
|
|
78
|
+
<video autoplay muted loop playsinline poster="${SIDE_POSTER}"><source src="${SIDE_VIDEO}" type="video/mp4"></video>
|
|
79
|
+
<div class="left-overlay"></div>
|
|
80
|
+
<div class="hero"><h1>${escapeHtml(options.heroTitle)}</h1><p>${escapeHtml(options.heroSub)}</p></div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="right">
|
|
83
|
+
<div class="card"><div class="card-inner">
|
|
84
|
+
<div class="logo"><img src="${LOGO}" alt="Stables"></div>
|
|
85
|
+
${options.body}
|
|
86
|
+
</div></div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</body>
|
|
90
|
+
</html>`;
|
|
91
|
+
}
|
|
92
|
+
const HERO_TITLE = "Connect your AI agent";
|
|
93
|
+
const HERO_SUB = "Let it move money and manage customers through Stables, with the access you approve.";
|
|
94
|
+
export function consentPage(details, handle) {
|
|
95
|
+
const name = escapeHtml(details.clientName);
|
|
96
|
+
const origin = details.clientDomain
|
|
97
|
+
? `Published by <strong>${escapeHtml(details.clientDomain)}</strong>.`
|
|
98
|
+
: "This app registered itself, so its name is not verified.";
|
|
99
|
+
const loopback = details.redirectIsLoopback
|
|
100
|
+
? `<p class="warn"><strong>This sends access to an app on your computer.</strong> Continue only if you just started connecting from it.</p>`
|
|
101
|
+
: "";
|
|
102
|
+
return layout({
|
|
103
|
+
title: `Connect ${details.clientName} to Stables`,
|
|
104
|
+
heroTitle: HERO_TITLE,
|
|
105
|
+
heroSub: HERO_SUB,
|
|
106
|
+
body: `<h2>Connect ${name} to Stables?</h2>
|
|
107
|
+
<p class="sub">${origin} Access will be sent to <strong>${escapeHtml(details.redirectHost)}</strong>.</p>
|
|
108
|
+
${loopback}
|
|
109
|
+
<p class="notice">Next you'll sign in with your Stables dashboard account and choose an organisation. Allowing creates an API key there named after ${name}; revoke it any time under Developer → API keys.</p>
|
|
110
|
+
<form method="post" autocomplete="off" onsubmit="if(this.dataset.sent)return false;this.dataset.sent=1;for(const b of this.querySelectorAll('button'))b.disabled=true;">
|
|
111
|
+
<input type="hidden" name="handle" value="${escapeHtml(handle)}">
|
|
112
|
+
<label class="lbl">Environment</label>
|
|
113
|
+
<div class="env-list">
|
|
114
|
+
<label class="env"><input type="radio" name="environment" value="sandbox" checked>
|
|
115
|
+
<span><span class="env-name">Sandbox</span><span class="env-desc">Test data, no real money. Keys start with <code>sti_test_</code>.</span></span></label>
|
|
116
|
+
<label class="env"><input type="radio" name="environment" value="production">
|
|
117
|
+
<span><span class="env-name">Production</span><span class="env-desc">Live organisation. The agent can move real money.</span></span></label>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="actions">
|
|
120
|
+
<button type="submit" name="decision" value="approve" class="btn btn-primary">Allow and sign in</button>
|
|
121
|
+
<button type="submit" name="decision" value="deny" class="btn btn-secondary">Cancel</button>
|
|
122
|
+
</div>
|
|
123
|
+
</form>
|
|
124
|
+
<p class="foot">You can disconnect at any time by revoking the key in your dashboard.</p>`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export function errorPage(title, message, hint) {
|
|
128
|
+
return layout({
|
|
129
|
+
title,
|
|
130
|
+
heroTitle: HERO_TITLE,
|
|
131
|
+
heroSub: HERO_SUB,
|
|
132
|
+
body: `<h2>${escapeHtml(title)}</h2>
|
|
133
|
+
<p class="error">${escapeHtml(message)}</p>
|
|
134
|
+
${hint ? `<p class="sub">${escapeHtml(hint)}</p>` : ""}
|
|
135
|
+
<p class="sub">Go back to the app that sent you here and try connecting again.</p>`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../../src/oauth/pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AACzE,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,GACd,oGAAoG,CAAC;AACvG,MAAM,WAAW,GACf,6GAA6G,CAAC;AAChH,MAAM,IAAI,GACR,qGAAqG,CAAC;AACxG,MAAM,YAAY,GAChB,4GAA4G,CAAC;AAE/G,MAAM,KAAK,GAAG;;gDAEkC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4C3D,CAAC;AAEF,SAAS,MAAM,CAAC,OAKf;IACC,OAAO;;;;;;SAMA,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC;SACzB,KAAK;;;;;qDAKuC,WAAW,kBAAkB,UAAU;;4BAEhE,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,WAAW,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC;;;;oCAI3D,IAAI;EACtC,OAAO,CAAC,IAAI;;;;;QAKN,CAAC;AACT,CAAC;AAED,MAAM,UAAU,GAAG,uBAAuB,CAAC;AAC3C,MAAM,QAAQ,GACZ,sFAAsF,CAAC;AAEzF,MAAM,UAAU,WAAW,CAAC,OAA2B,EAAE,MAAc;IACrE,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY;QACjC,CAAC,CAAC,wBAAwB,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY;QACtE,CAAC,CAAC,0DAA0D,CAAC;IAC/D,MAAM,QAAQ,GAAG,OAAO,CAAC,kBAAkB;QACzC,CAAC,CAAC,0IAA0I;QAC5I,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,MAAM,CAAC;QACZ,KAAK,EAAE,WAAW,OAAO,CAAC,UAAU,aAAa;QACjD,SAAS,EAAE,UAAU;QACrB,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,eAAe,IAAI;iBACZ,MAAM,mCAAmC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC;EACxF,QAAQ;sJAC4I,IAAI;;8CAE5G,UAAU,CAAC,MAAM,CAAC;;;;;;;;;;;;;0FAa0B;KACvF,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,OAAe,EAAE,IAAa;IACrE,OAAO,MAAM,CAAC;QACZ,KAAK;QACL,SAAS,EAAE,UAAU;QACrB,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;mBACf,UAAU,CAAC,OAAO,CAAC;EACpC,IAAI,CAAC,CAAC,CAAC,kBAAkB,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;mFAC6B;KAChF,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OAuth provider the Worker runs: authorization server and protected
|
|
3
|
+
* resource in one, as @cloudflare/workers-oauth-provider's OAuthProvider.
|
|
4
|
+
*
|
|
5
|
+
* - /mcp is the protected resource. A grant's props carry the Stables API key
|
|
6
|
+
* minted at sign-in, and the request is served with it.
|
|
7
|
+
* - A plain Stables API key sent as a bearer token keeps working through
|
|
8
|
+
* resolveExternalToken, so coding agents configured with a key need no
|
|
9
|
+
* OAuth.
|
|
10
|
+
* - /.well-known/oauth-protected-resource/mcp, /.well-known/oauth-authorization-server,
|
|
11
|
+
* /oauth/token and /oauth/register are the provider's; /authorize and
|
|
12
|
+
* /callback are ours (handlers.ts).
|
|
13
|
+
*/
|
|
14
|
+
import { OAuthProvider } from "@cloudflare/workers-oauth-provider";
|
|
15
|
+
import { StablesApiClient } from "../lib/stables-client.js";
|
|
16
|
+
import { type OAuthEnv } from "./types.js";
|
|
17
|
+
export declare function resourceFor(origin: string): string;
|
|
18
|
+
export declare function apiClientFor(apiKey: string, env: OAuthEnv): StablesApiClient | null;
|
|
19
|
+
/** One provider per public origin; metadata and token audiences are bound to it. */
|
|
20
|
+
export declare function providerFor(origin: string): OAuthProvider<OAuthEnv>;
|
|
21
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/oauth/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAsB,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAGvF,OAAO,EAAE,gBAAgB,EAAgB,MAAM,0BAA0B,CAAC;AAG1E,OAAO,EAA4C,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,gBAAgB,GAAG,IAAI,CAOnF;AAyBD,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC,QAAQ,CAAC,CAiDnE"}
|