@wakeshift/sdk 0.0.0-stage → 0.2.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/CHANGELOG.md +25 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +198 -2
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +195 -0
- package/dist/events.js +523 -0
- package/dist/events.js.map +1 -0
- package/dist/guest.d.ts +119 -0
- package/dist/guest.js +506 -0
- package/dist/guest.js.map +1 -0
- package/dist/harness.d.ts +6 -0
- package/dist/harness.js +78 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +29 -0
- package/dist/key.js +107 -0
- package/dist/key.js.map +1 -0
- package/dist/limits.d.ts +19 -0
- package/dist/limits.js +105 -0
- package/dist/limits.js.map +1 -0
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +58 -0
- package/dist/retry.js.map +1 -0
- package/dist/sealed.d.ts +4 -0
- package/dist/sealed.js +38 -0
- package/dist/sealed.js.map +1 -0
- package/dist/spec.d.ts +28 -0
- package/dist/spec.js +43 -0
- package/dist/spec.js.map +1 -0
- package/dist/wakeshift.d.ts +415 -0
- package/dist/wakeshift.js +481 -0
- package/dist/wakeshift.js.map +1 -0
- package/package.json +48 -4
package/dist/guest.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
// GuestAuth signs MCP clients (Claude Code, Codex…) in to a headless app as
|
|
2
|
+
// guests: no Wakeshift account, no account with the app. It's a small OAuth
|
|
3
|
+
// server inside the app (dynamic client registration, authorize with PKCE,
|
|
4
|
+
// tokens), and it keeps no state: clients, codes and tokens are signed with
|
|
5
|
+
// a key derived from the app's key. A port of the Go SDK's appkit.GuestAuth.
|
|
6
|
+
//
|
|
7
|
+
// Each registered client (an install of a harness) is one guest: its own
|
|
8
|
+
// workspace, with an agent named after the harness (claude-code). Sign-in
|
|
9
|
+
// shows nothing: the person added the app to their agent, and that's the
|
|
10
|
+
// consent.
|
|
11
|
+
//
|
|
12
|
+
// Since authorize has no user step, knowing a client_id must not be enough
|
|
13
|
+
// to become its guest (client_ids travel in authorize URLs, which end up in
|
|
14
|
+
// logs and browser history). Registration also gives the client a secret
|
|
15
|
+
// (derived from the client_id, so nothing is stored), and the token
|
|
16
|
+
// endpoint requires it with every code and refresh token. The guest's
|
|
17
|
+
// workspace and agent are made at the code exchange, once the client has
|
|
18
|
+
// authenticated, not at authorize.
|
|
19
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
20
|
+
import { isIP } from "node:net";
|
|
21
|
+
import { WakeshiftError } from "./errors.js";
|
|
22
|
+
import { harnessAgent } from "./harness.js";
|
|
23
|
+
import { RateLimiter, clientAddr } from "./limits.js";
|
|
24
|
+
import { seal, unseal } from "./sealed.js";
|
|
25
|
+
/** The guest a tool call came from, if GuestAuth signed it in (extra: the tool callback's second argument). */
|
|
26
|
+
export function guestOf(extra) {
|
|
27
|
+
const g = extra?.authInfo?.extra?.guest;
|
|
28
|
+
return g && typeof g.user === "string" ? g : undefined;
|
|
29
|
+
}
|
|
30
|
+
// How long an authorization code is good for, in seconds.
|
|
31
|
+
const CODE_TTL = 60;
|
|
32
|
+
// A PKCE code verifier, or an S256 challenge (RFC 7636 4.1, 4.2).
|
|
33
|
+
const PKCE_RE = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
34
|
+
class OAuthFailure extends Error {
|
|
35
|
+
status;
|
|
36
|
+
code;
|
|
37
|
+
constructor(status, code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.code = code;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export class GuestAuth {
|
|
44
|
+
wake;
|
|
45
|
+
mcpUrl;
|
|
46
|
+
base;
|
|
47
|
+
accessTtl;
|
|
48
|
+
refreshTtl;
|
|
49
|
+
revokedFn;
|
|
50
|
+
grants;
|
|
51
|
+
proxyHops;
|
|
52
|
+
registers;
|
|
53
|
+
tokens;
|
|
54
|
+
errorFn;
|
|
55
|
+
now;
|
|
56
|
+
seed;
|
|
57
|
+
constructor(o) {
|
|
58
|
+
if (!o?.wake)
|
|
59
|
+
throw new Error("wakeshift: GuestAuth needs { wake }");
|
|
60
|
+
if (!o.mcpUrl)
|
|
61
|
+
throw new Error("wakeshift: GuestAuth needs { mcpUrl }, the app's MCP URL");
|
|
62
|
+
this.wake = o.wake;
|
|
63
|
+
this.mcpUrl = o.mcpUrl;
|
|
64
|
+
this.base = (o.base ?? o.mcpUrl.replace(/\/mcp$/, "")).replace(/\/+$/, "");
|
|
65
|
+
this.accessTtl = o.accessTtl && o.accessTtl > 0 ? o.accessTtl : 3600;
|
|
66
|
+
this.refreshTtl = o.refreshTtl && o.refreshTtl > 0 ? o.refreshTtl : 30 * 24 * 3600;
|
|
67
|
+
this.revokedFn = o.revoked;
|
|
68
|
+
this.grants = !!o.grants;
|
|
69
|
+
this.proxyHops = o.proxyHops ?? 0;
|
|
70
|
+
this.now = o.now ?? Date.now;
|
|
71
|
+
const window = (o.rateWindow && o.rateWindow > 0 ? o.rateWindow : 600) * 1000;
|
|
72
|
+
this.registers = new RateLimiter(o.registerLimit === undefined || o.registerLimit === 0 ? 20 : o.registerLimit, window);
|
|
73
|
+
this.tokens = new RateLimiter(o.tokenLimit === undefined || o.tokenLimit === 0 ? 60 : o.tokenLimit, window);
|
|
74
|
+
this.errorFn = o.onError ?? ((m, err) => console.error(m, err instanceof WakeshiftError ? `${err.status} ${err.code}` : err));
|
|
75
|
+
this.seed = this.wake.secretSeed();
|
|
76
|
+
}
|
|
77
|
+
/** Where MCP clients read the app's protected-resource metadata (RFC 9728). */
|
|
78
|
+
get resourceMetadataUrl() {
|
|
79
|
+
const u = new URL(this.mcpUrl);
|
|
80
|
+
return u.origin + "/.well-known/oauth-protected-resource";
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Serves the OAuth endpoints and the metadata that points MCP clients at
|
|
84
|
+
* them. Returns false for any other request, for your own routes. Mount it
|
|
85
|
+
* before any body parser (it reads its own bodies), or after one: a parsed
|
|
86
|
+
* req.body is used as it is.
|
|
87
|
+
*/
|
|
88
|
+
async handle(req, res) {
|
|
89
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
90
|
+
const routes = {
|
|
91
|
+
"/.well-known/oauth-protected-resource": ["GET", () => this.resourceMetadata(res)],
|
|
92
|
+
"/.well-known/oauth-protected-resource/mcp": ["GET", () => this.resourceMetadata(res)],
|
|
93
|
+
"/.well-known/oauth-authorization-server": ["GET", () => this.metadata(res)],
|
|
94
|
+
"/oauth/register": ["POST", () => this.register(req, res)],
|
|
95
|
+
"/oauth/authorize": ["GET", () => this.authorize(req, res)],
|
|
96
|
+
"/oauth/token": ["POST", () => this.token(req, res)],
|
|
97
|
+
};
|
|
98
|
+
const r = routes[path];
|
|
99
|
+
if (!r)
|
|
100
|
+
return false;
|
|
101
|
+
const method = req.method === "HEAD" ? "GET" : req.method;
|
|
102
|
+
if (method !== r[0]) {
|
|
103
|
+
res.writeHead(405, { Allow: r[0] === "GET" ? "GET, HEAD" : r[0], "Content-Type": "text/plain" });
|
|
104
|
+
res.end("method not allowed");
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await r[1]();
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (err instanceof OAuthFailure) {
|
|
112
|
+
oauthErr(res, err.status, err.code, err.message);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.errorFn("wakeshift: guest sign-in failed", err);
|
|
116
|
+
if (!res.headersSent)
|
|
117
|
+
oauthErr(res, 500, "server_error", "try again");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Checks the request's bearer token. On success it returns the guest and
|
|
124
|
+
* sets req.auth, which the MCP SDK's Streamable HTTP transport passes to
|
|
125
|
+
* tool callbacks as extra.authInfo (read it with guestOf). On failure it
|
|
126
|
+
* answers 401 with the WWW-Authenticate header MCP clients start signing
|
|
127
|
+
* in from, and returns null.
|
|
128
|
+
*/
|
|
129
|
+
authenticate(req, res) {
|
|
130
|
+
const h = req.headers.authorization ?? "";
|
|
131
|
+
const m = /^Bearer\s+(\S+)$/i.exec(h);
|
|
132
|
+
const g = m ? this.verify(m[1]) : null;
|
|
133
|
+
if (!g) {
|
|
134
|
+
res.writeHead(401, {
|
|
135
|
+
"WWW-Authenticate": `Bearer resource_metadata="${this.resourceMetadataUrl}"${m ? ', error="invalid_token"' : ""}`,
|
|
136
|
+
"Content-Type": "text/plain",
|
|
137
|
+
});
|
|
138
|
+
res.end("sign in: add this app to your agent again");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const info = {
|
|
142
|
+
token: m[1],
|
|
143
|
+
clientId: g.user,
|
|
144
|
+
scopes: [],
|
|
145
|
+
expiresAt: Math.floor(g.expires.getTime() / 1000),
|
|
146
|
+
resource: new URL(this.mcpUrl),
|
|
147
|
+
extra: { guest: g },
|
|
148
|
+
};
|
|
149
|
+
req.auth = info;
|
|
150
|
+
return g;
|
|
151
|
+
}
|
|
152
|
+
/** Checks a guest access token; null if it isn't one of this app's, or has expired. */
|
|
153
|
+
verify(token) {
|
|
154
|
+
const t = this.open("access", token);
|
|
155
|
+
if (!t || typeof t.u !== "string" || typeof t.w !== "string" || typeof t.a !== "string" || typeof t.e !== "number" || this.seconds() > t.e)
|
|
156
|
+
return null;
|
|
157
|
+
return { user: t.u, workspace: t.w, agent: t.a, client: typeof t.c === "string" ? t.c : "", expires: new Date(t.e * 1000) };
|
|
158
|
+
}
|
|
159
|
+
seconds() {
|
|
160
|
+
return Math.floor(this.now() / 1000);
|
|
161
|
+
}
|
|
162
|
+
resourceMetadata(res) {
|
|
163
|
+
writeJSON(res, 200, { resource: this.mcpUrl, authorization_servers: [this.base], bearer_methods_supported: ["header"] });
|
|
164
|
+
}
|
|
165
|
+
metadata(res) {
|
|
166
|
+
const b = this.base;
|
|
167
|
+
writeJSON(res, 200, {
|
|
168
|
+
issuer: b,
|
|
169
|
+
authorization_endpoint: b + "/oauth/authorize",
|
|
170
|
+
token_endpoint: b + "/oauth/token",
|
|
171
|
+
registration_endpoint: b + "/oauth/register",
|
|
172
|
+
response_types_supported: ["code"],
|
|
173
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
174
|
+
code_challenge_methods_supported: ["S256"],
|
|
175
|
+
token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ── Signed values ──────────────────────────────────────────────────────
|
|
179
|
+
// Each kind (client, code, access, refresh) is signed with its own key
|
|
180
|
+
// derived from the app's key, so one can't pass for another.
|
|
181
|
+
mac(purpose) {
|
|
182
|
+
return createHmac("sha256", this.seed).update("appkit-guest:" + purpose).digest();
|
|
183
|
+
}
|
|
184
|
+
sign(purpose, v) {
|
|
185
|
+
return seal(this.mac(purpose), v);
|
|
186
|
+
}
|
|
187
|
+
open(purpose, s) {
|
|
188
|
+
return unseal(this.mac(purpose), s);
|
|
189
|
+
}
|
|
190
|
+
/** The secret registration gave a client: an HMAC of its client_id, so it's never stored and only the app can make it. */
|
|
191
|
+
clientSecret(clientId) {
|
|
192
|
+
return createHmac("sha256", this.mac("client-secret")).update(clientId).digest("base64url");
|
|
193
|
+
}
|
|
194
|
+
// clientAuth is the client a token request authenticated as, by HTTP
|
|
195
|
+
// Basic (RFC 6749 2.3.1: each part form-encoded first) or client_id and
|
|
196
|
+
// client_secret in the form, if its secret is right.
|
|
197
|
+
clientAuth(req, f) {
|
|
198
|
+
let id;
|
|
199
|
+
let secret;
|
|
200
|
+
const basic = /^Basic\s+(\S+)$/i.exec(req.headers.authorization ?? "");
|
|
201
|
+
if (basic) {
|
|
202
|
+
const raw = Buffer.from(basic[1], "base64").toString("utf8");
|
|
203
|
+
const colon = raw.indexOf(":");
|
|
204
|
+
if (colon < 0)
|
|
205
|
+
return null;
|
|
206
|
+
const a = formUnescape(raw.slice(0, colon));
|
|
207
|
+
const b = formUnescape(raw.slice(colon + 1));
|
|
208
|
+
if (a === null || b === null || f.get("client_secret"))
|
|
209
|
+
return null; // one method at a time (RFC 6749 2.3)
|
|
210
|
+
const formId = f.get("client_id");
|
|
211
|
+
if (formId && formId !== a)
|
|
212
|
+
return null;
|
|
213
|
+
id = a;
|
|
214
|
+
secret = b;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
id = f.get("client_id") ?? "";
|
|
218
|
+
secret = f.get("client_secret") ?? "";
|
|
219
|
+
}
|
|
220
|
+
if (!id || !secret || !this.open("client", id))
|
|
221
|
+
return null;
|
|
222
|
+
const want = Buffer.from(this.clientSecret(id));
|
|
223
|
+
const got = Buffer.from(secret);
|
|
224
|
+
if (got.length !== want.length || !timingSafeEqual(got, want))
|
|
225
|
+
return null;
|
|
226
|
+
return id;
|
|
227
|
+
}
|
|
228
|
+
async register(req, res) {
|
|
229
|
+
if (!this.registers.allow(clientAddr(req, this.proxyHops), this.now())) {
|
|
230
|
+
throw new OAuthFailure(429, "slow_down", "too many registrations from this address; try again later");
|
|
231
|
+
}
|
|
232
|
+
let raw;
|
|
233
|
+
try {
|
|
234
|
+
raw = await readJSON(req, 16 << 10);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
throw new OAuthFailure(400, "invalid_client_metadata", "redirect_uris is required");
|
|
238
|
+
}
|
|
239
|
+
const inp = (raw ?? {});
|
|
240
|
+
const uris = inp.redirect_uris;
|
|
241
|
+
if (!Array.isArray(uris) || uris.length === 0 || uris.length > 5 || !uris.every((u) => typeof u === "string")) {
|
|
242
|
+
throw new OAuthFailure(400, "invalid_client_metadata", "redirect_uris is required");
|
|
243
|
+
}
|
|
244
|
+
for (const u of uris) {
|
|
245
|
+
if (u.length > 500 || !validRedirect(u))
|
|
246
|
+
throw new OAuthFailure(400, "invalid_redirect_uri", BAD_REDIRECT);
|
|
247
|
+
}
|
|
248
|
+
let name = typeof inp.client_name === "string" ? inp.client_name.trim() : "";
|
|
249
|
+
if (!name || Buffer.byteLength(name) > 80)
|
|
250
|
+
name = "MCP client";
|
|
251
|
+
const id = this.sign("client", { n: name, r: uris, x: randomBytes(16).toString("base64url") });
|
|
252
|
+
res.setHeader("Cache-Control", "no-store");
|
|
253
|
+
writeJSON(res, 201, {
|
|
254
|
+
client_id: id,
|
|
255
|
+
client_secret: this.clientSecret(id),
|
|
256
|
+
client_secret_expires_at: 0,
|
|
257
|
+
client_name: name,
|
|
258
|
+
redirect_uris: uris,
|
|
259
|
+
token_endpoint_auth_method: "client_secret_post",
|
|
260
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
261
|
+
response_types: ["code"],
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// authorize issues a code to a registered client with no user step (the
|
|
265
|
+
// person added the app to their agent). It creates nothing: the guest is
|
|
266
|
+
// made at the code exchange, once the client proves it holds its secret.
|
|
267
|
+
authorize(req, res) {
|
|
268
|
+
const q = new URL(req.url ?? "/", "http://x").searchParams;
|
|
269
|
+
const c = this.open("client", q.get("client_id") ?? "");
|
|
270
|
+
if (!c || !Array.isArray(c.r)) {
|
|
271
|
+
textErr(res, 400, "Unknown client. Register it again.");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const redirect = q.get("redirect_uri") ?? "";
|
|
275
|
+
if (!c.r.includes(redirect)) {
|
|
276
|
+
textErr(res, 400, "That redirect URI isn't registered for this client.");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const state = q.get("state") ?? "";
|
|
280
|
+
if (q.get("response_type") !== "code" || !PKCE_RE.test(q.get("code_challenge") ?? "") || q.get("code_challenge_method") !== "S256") {
|
|
281
|
+
// Said here, not by redirecting: anyone can register a client with
|
|
282
|
+
// any https redirect, and there's no step for the person first, so
|
|
283
|
+
// redirecting a bad request would make this an open redirect.
|
|
284
|
+
textErr(res, 400, "This sign-in request is incomplete: PKCE (S256) and response_type=code are required.");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const code = { c: q.get("client_id"), r: redirect, s: state, p: q.get("code_challenge"), e: this.seconds() + CODE_TTL, k: true };
|
|
288
|
+
let u;
|
|
289
|
+
try {
|
|
290
|
+
u = new URL(redirect);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
textErr(res, 400, "That redirect URI isn't registered for this client.");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
u.searchParams.set("code", this.sign("code", code));
|
|
297
|
+
if (state)
|
|
298
|
+
u.searchParams.set("state", state);
|
|
299
|
+
res.writeHead(302, { Location: u.toString() });
|
|
300
|
+
res.end();
|
|
301
|
+
}
|
|
302
|
+
async token(req, res) {
|
|
303
|
+
if (!this.tokens.allow(clientAddr(req, this.proxyHops), this.now())) {
|
|
304
|
+
throw new OAuthFailure(429, "slow_down", "too many token requests from this address; try again later");
|
|
305
|
+
}
|
|
306
|
+
let f;
|
|
307
|
+
try {
|
|
308
|
+
f = await readForm(req, 32 << 10);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
throw new OAuthFailure(400, "invalid_request", "bad form");
|
|
312
|
+
}
|
|
313
|
+
const clientId = this.clientAuth(req, f);
|
|
314
|
+
if (!clientId) {
|
|
315
|
+
res.setHeader("WWW-Authenticate", 'Basic realm="oauth"');
|
|
316
|
+
throw new OAuthFailure(401, "invalid_client", "client authentication failed: send the client_id and client_secret registration gave you");
|
|
317
|
+
}
|
|
318
|
+
let workspaceId;
|
|
319
|
+
let agent;
|
|
320
|
+
let name;
|
|
321
|
+
let user;
|
|
322
|
+
switch (f.get("grant_type")) {
|
|
323
|
+
case "authorization_code": {
|
|
324
|
+
const c = this.open("code", f.get("code") ?? "");
|
|
325
|
+
if (!c || c.k !== true || typeof c.e !== "number" || this.seconds() > c.e || c.c !== clientId || c.r !== (f.get("redirect_uri") ?? "")) {
|
|
326
|
+
throw new OAuthFailure(400, "invalid_grant", "the code is invalid or expired");
|
|
327
|
+
}
|
|
328
|
+
const verifier = f.get("code_verifier") ?? "";
|
|
329
|
+
const sum = Buffer.from(createHash("sha256").update(verifier).digest("base64url"));
|
|
330
|
+
const challenge = Buffer.from(String(c.p));
|
|
331
|
+
if (!PKCE_RE.test(verifier) || sum.length !== challenge.length || !timingSafeEqual(sum, challenge)) {
|
|
332
|
+
throw new OAuthFailure(400, "invalid_grant", "PKCE verification failed");
|
|
333
|
+
}
|
|
334
|
+
const cl = this.open("client", clientId);
|
|
335
|
+
// One guest per registered client: its workspace, and its agent
|
|
336
|
+
// named after the harness.
|
|
337
|
+
name = cl.n;
|
|
338
|
+
agent = harnessAgent(cl.n);
|
|
339
|
+
user = "guest:" + guestId(clientId);
|
|
340
|
+
try {
|
|
341
|
+
workspaceId = await this.wake.workspace(user, "Guest (" + cl.n + ")");
|
|
342
|
+
if (this.grants)
|
|
343
|
+
await this.wake.addAgent(workspaceId, agent, cl.n);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
if (err instanceof WakeshiftError && err.code === "workspace_limit")
|
|
347
|
+
throw new OAuthFailure(403, "access_denied", "this app can't take more guests right now");
|
|
348
|
+
this.errorFn("wakeshift: making a guest", err);
|
|
349
|
+
throw new OAuthFailure(503, "temporarily_unavailable", "couldn't sign in; try again");
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "refresh_token": {
|
|
354
|
+
const t = this.open("refresh", f.get("refresh_token") ?? "");
|
|
355
|
+
if (!t || typeof t.e !== "number" || this.seconds() > t.e || t.i !== clientId || typeof t.w !== "string" || typeof t.a !== "string" || typeof t.u !== "string") {
|
|
356
|
+
throw new OAuthFailure(400, "invalid_grant", "the refresh token is invalid or expired");
|
|
357
|
+
}
|
|
358
|
+
const guest = { user: t.u, workspace: t.w, agent: t.a, client: typeof t.c === "string" ? t.c : "", expires: new Date(t.e * 1000) };
|
|
359
|
+
if (this.revokedFn && (await this.revokedFn(guest, new Date((t.t ?? 0) * 1000)))) {
|
|
360
|
+
throw new OAuthFailure(400, "invalid_grant", "the refresh token was revoked; sign in again");
|
|
361
|
+
}
|
|
362
|
+
// The guest must still be there: forgetting the user (and, with
|
|
363
|
+
// grants, revoking the grant) ends its refreshes.
|
|
364
|
+
if (!(await this.stillThere(t.u, t.w, t.a, guest.client))) {
|
|
365
|
+
throw new OAuthFailure(400, "invalid_grant", "this guest's access was revoked; sign in again");
|
|
366
|
+
}
|
|
367
|
+
workspaceId = t.w;
|
|
368
|
+
agent = t.a;
|
|
369
|
+
name = guest.client;
|
|
370
|
+
user = t.u;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
default:
|
|
374
|
+
throw new OAuthFailure(400, "unsupported_grant_type", "use authorization_code or refresh_token");
|
|
375
|
+
}
|
|
376
|
+
const now = this.seconds();
|
|
377
|
+
const access = this.sign("access", { w: workspaceId, a: agent, c: name, u: user, e: now + this.accessTtl });
|
|
378
|
+
// A new refresh token every time (rotation), dated, so revoked can
|
|
379
|
+
// refuse those issued before a cutoff.
|
|
380
|
+
const refresh = this.sign("refresh", {
|
|
381
|
+
w: workspaceId,
|
|
382
|
+
a: agent,
|
|
383
|
+
c: name,
|
|
384
|
+
u: user,
|
|
385
|
+
i: clientId,
|
|
386
|
+
t: now,
|
|
387
|
+
n: randomBytes(16).toString("base64url"),
|
|
388
|
+
e: now + this.refreshTtl,
|
|
389
|
+
});
|
|
390
|
+
res.setHeader("Cache-Control", "no-store");
|
|
391
|
+
writeJSON(res, 200, { access_token: access, token_type: "Bearer", expires_in: this.accessTtl, refresh_token: refresh });
|
|
392
|
+
}
|
|
393
|
+
// stillThere is whether the guest a refresh token names is still there:
|
|
394
|
+
// its workspace is the one Wakeshift keeps for its user (a forgotten
|
|
395
|
+
// user's is gone, so asking makes a new one, which is forgotten again at
|
|
396
|
+
// once), and, with grants, its agent's grant is live. Throws a 503 when
|
|
397
|
+
// Wakeshift can't say.
|
|
398
|
+
async stillThere(user, workspace, agent, client) {
|
|
399
|
+
try {
|
|
400
|
+
if (this.grants) {
|
|
401
|
+
await this.wake.checkAgent(workspace, agent);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
if ((await this.wake.workspace(user, "Guest (" + client + ")")) === workspace)
|
|
405
|
+
return true;
|
|
406
|
+
await this.wake.forgetUser(user);
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
if (err instanceof WakeshiftError && (err.code === "no_grant" || err.code === "unknown_workspace" || err.code === "workspace_limit"))
|
|
411
|
+
return false;
|
|
412
|
+
this.errorFn("wakeshift: checking a guest", err);
|
|
413
|
+
throw new OAuthFailure(503, "temporarily_unavailable", "couldn't refresh; try again");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** The guest ID for a client: a hash of its client_id. */
|
|
418
|
+
function guestId(clientId) {
|
|
419
|
+
return createHash("sha256").update(clientId).digest().subarray(0, 18).toString("base64url");
|
|
420
|
+
}
|
|
421
|
+
// formUnescape is Go's url.QueryUnescape: + is a space, %XX a byte; null
|
|
422
|
+
// if malformed.
|
|
423
|
+
function formUnescape(s) {
|
|
424
|
+
if (/%(?![0-9A-Fa-f]{2})/.test(s))
|
|
425
|
+
return null;
|
|
426
|
+
try {
|
|
427
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const BAD_REDIRECT = "redirect URIs are https, http on loopback (localhost, 127.0.0.1, [::1]) or a private-use scheme (cursor://…), without a fragment";
|
|
434
|
+
// validRedirect accepts https URLs, http on loopback (native apps, RFC
|
|
435
|
+
// 8252) and private-use schemes (cursor://…), never ones a browser would
|
|
436
|
+
// run or that carry a fragment, as the core's OAuth does.
|
|
437
|
+
function validRedirect(raw) {
|
|
438
|
+
const m = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(raw);
|
|
439
|
+
if (!m || m[1] !== m[1].toLowerCase() || raw.includes("#"))
|
|
440
|
+
return false;
|
|
441
|
+
let u;
|
|
442
|
+
try {
|
|
443
|
+
u = new URL(raw);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
switch (m[1]) {
|
|
449
|
+
case "https":
|
|
450
|
+
return u.hostname !== "";
|
|
451
|
+
case "http":
|
|
452
|
+
return u.hostname === "localhost" || (isIP(u.hostname) === 4 && u.hostname.startsWith("127.")) || u.hostname === "[::1]";
|
|
453
|
+
case "javascript":
|
|
454
|
+
case "data":
|
|
455
|
+
case "file":
|
|
456
|
+
case "blob":
|
|
457
|
+
case "about":
|
|
458
|
+
case "vbscript":
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
async function readBody(req, limit) {
|
|
464
|
+
const parsed = req.body;
|
|
465
|
+
if (parsed !== undefined)
|
|
466
|
+
return typeof parsed === "string" ? parsed : JSON.stringify(parsed);
|
|
467
|
+
const chunks = [];
|
|
468
|
+
let n = 0;
|
|
469
|
+
for await (const c of req) {
|
|
470
|
+
n += c.length;
|
|
471
|
+
if (n > limit)
|
|
472
|
+
throw new Error("body too large");
|
|
473
|
+
chunks.push(c);
|
|
474
|
+
}
|
|
475
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
476
|
+
}
|
|
477
|
+
async function readJSON(req, limit) {
|
|
478
|
+
const parsed = req.body;
|
|
479
|
+
if (parsed !== undefined && typeof parsed === "object")
|
|
480
|
+
return parsed;
|
|
481
|
+
return JSON.parse(await readBody(req, limit));
|
|
482
|
+
}
|
|
483
|
+
async function readForm(req, limit) {
|
|
484
|
+
const parsed = req.body;
|
|
485
|
+
if (parsed !== undefined && parsed !== null && typeof parsed === "object") {
|
|
486
|
+
const out = new URLSearchParams();
|
|
487
|
+
for (const [k, v] of Object.entries(parsed))
|
|
488
|
+
if (typeof v === "string")
|
|
489
|
+
out.set(k, v);
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
return new URLSearchParams(await readBody(req, limit));
|
|
493
|
+
}
|
|
494
|
+
function writeJSON(res, status, v) {
|
|
495
|
+
res.statusCode = status;
|
|
496
|
+
res.setHeader("Content-Type", "application/json");
|
|
497
|
+
res.end(JSON.stringify(v) + "\n");
|
|
498
|
+
}
|
|
499
|
+
function oauthErr(res, status, code, desc) {
|
|
500
|
+
writeJSON(res, status, { error: code, error_description: desc });
|
|
501
|
+
}
|
|
502
|
+
function textErr(res, status, msg) {
|
|
503
|
+
res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
|
|
504
|
+
res.end(msg + "\n");
|
|
505
|
+
}
|
|
506
|
+
//# sourceMappingURL=guest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guest.js","sourceRoot":"","sources":["../src/guest.ts"],"names":[],"mappings":"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","sourcesContent":["// GuestAuth signs MCP clients (Claude Code, Codex…) in to a headless app as\n// guests: no Wakeshift account, no account with the app. It's a small OAuth\n// server inside the app (dynamic client registration, authorize with PKCE,\n// tokens), and it keeps no state: clients, codes and tokens are signed with\n// a key derived from the app's key. A port of the Go SDK's appkit.GuestAuth.\n//\n// Each registered client (an install of a harness) is one guest: its own\n// workspace, with an agent named after the harness (claude-code). Sign-in\n// shows nothing: the person added the app to their agent, and that's the\n// consent.\n//\n// Since authorize has no user step, knowing a client_id must not be enough\n// to become its guest (client_ids travel in authorize URLs, which end up in\n// logs and browser history). Registration also gives the client a secret\n// (derived from the client_id, so nothing is stored), and the token\n// endpoint requires it with every code and refresh token. The guest's\n// workspace and agent are made at the code exchange, once the client has\n// authenticated, not at authorize.\n\nimport { createHash, createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { isIP } from \"node:net\";\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\";\nimport { WakeshiftError } from \"./errors.js\";\nimport { harnessAgent } from \"./harness.js\";\nimport { RateLimiter, clientAddr } from \"./limits.js\";\nimport { seal, unseal } from \"./sealed.js\";\nimport type { Wakeshift } from \"./wakeshift.js\";\n\n/** Who a guest access token is for. */\nexport interface Guest {\n /**\n * The app's ID for the guest: pass it as wake.call's user, so topics keep\n * the guest's conversations in its workspace. guest:<id>, one per\n * registered client.\n */\n user: string;\n /** The guest's workspace and agent (named after its harness: claude-code). */\n workspace: string;\n agent: string;\n /** The client's name, e.g. \"Claude Code\". */\n client: string;\n expires: Date;\n}\n\nexport interface GuestAuthOptions {\n wake: Wakeshift;\n /** The app's MCP URL: the OAuth resource (https://…/mcp). */\n mcpUrl: string;\n /** The app's public URL, where the OAuth endpoints are (default: mcpUrl without /mcp). */\n base?: string;\n /** Seconds an access token lasts (default an hour). */\n accessTtl?: number;\n /** Seconds a refresh token lasts (default 30 days); each refresh returns a new one, good for this long from then. */\n refreshTtl?: number;\n /**\n * Asked at each refresh whether the guest's refresh token issued at\n * issuedAt is revoked: an app's own denylist, or a per-guest cutoff. A\n * refresh is also refused once wake.forgetUser(guest.user) has run (and,\n * with grants, once the guest's grant is revoked), so either signs the\n * guest out within accessTtl.\n */\n revoked?: (guest: Guest, issuedAt: Date) => boolean | Promise<boolean>;\n /**\n * Also give each guest's agent a grant for the app's manifest agent scope,\n * as the Go SDK does, and refuse refreshes once it's revoked. Needs a\n * manifest with an agent scope; topics don't use grants, so leave it off\n * for an app that only uses topics.\n */\n grants?: boolean;\n /**\n * How many trusted proxies in front append to X-Forwarded-For (behind\n * Google's load balancer: 2), for the per-address limits. 0 (the\n * default): the connection's own address.\n */\n proxyHops?: number;\n /** Registrations (default 20) and token requests (default 60) per address per rateWindow, per process; negative turns a limit off. */\n registerLimit?: number;\n tokenLimit?: number;\n /** Seconds (default 600). */\n rateWindow?: number;\n /** Called with a problem worth logging (never with a token or secret). Default: console.error. */\n onError?: (message: string, err: unknown) => void;\n /** For tests. */\n now?: () => number;\n}\n\n/** What the MCP transport passes to tool callbacks (extra.authInfo) for a guest. */\nexport type GuestAuthInfo = AuthInfo & { extra: { guest: Guest } };\n\n/** The guest a tool call came from, if GuestAuth signed it in (extra: the tool callback's second argument). */\nexport function guestOf(extra: { authInfo?: AuthInfo } | undefined): Guest | undefined {\n const g = extra?.authInfo?.extra?.guest as Guest | undefined;\n return g && typeof g.user === \"string\" ? g : undefined;\n}\n\n// How long an authorization code is good for, in seconds.\nconst CODE_TTL = 60;\n// A PKCE code verifier, or an S256 challenge (RFC 7636 4.1, 4.2).\nconst PKCE_RE = /^[A-Za-z0-9._~-]{43,128}$/;\n\ninterface GuestClient {\n n: string; // name\n r: string[]; // redirect URIs\n x: string; // makes each registration its own guest (128 bits)\n}\n\ninterface GuestCode {\n c: string; // client_id\n r: string; // redirect URI\n s: string; // state\n p: string; // PKCE challenge\n e: number; // expires (unix seconds)\n k: true;\n}\n\ninterface GuestToken {\n w: string; // workspace\n a: string; // agent\n c: string; // client name\n u: string; // the app's user ID for the guest\n i?: string; // client_id (refresh tokens)\n t?: number; // issued at (refresh tokens)\n n?: string; // nonce: each refresh token its own\n e: number; // expires\n}\n\nclass OAuthFailure extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n }\n}\n\nexport class GuestAuth {\n private readonly wake: Wakeshift;\n readonly mcpUrl: string;\n readonly base: string;\n private readonly accessTtl: number;\n private readonly refreshTtl: number;\n private readonly revokedFn?: GuestAuthOptions[\"revoked\"];\n private readonly grants: boolean;\n private readonly proxyHops: number;\n private readonly registers: RateLimiter;\n private readonly tokens: RateLimiter;\n private readonly errorFn: (message: string, err: unknown) => void;\n private readonly now: () => number;\n private readonly seed: Buffer;\n\n constructor(o: GuestAuthOptions) {\n if (!o?.wake) throw new Error(\"wakeshift: GuestAuth needs { wake }\");\n if (!o.mcpUrl) throw new Error(\"wakeshift: GuestAuth needs { mcpUrl }, the app's MCP URL\");\n this.wake = o.wake;\n this.mcpUrl = o.mcpUrl;\n this.base = (o.base ?? o.mcpUrl.replace(/\\/mcp$/, \"\")).replace(/\\/+$/, \"\");\n this.accessTtl = o.accessTtl && o.accessTtl > 0 ? o.accessTtl : 3600;\n this.refreshTtl = o.refreshTtl && o.refreshTtl > 0 ? o.refreshTtl : 30 * 24 * 3600;\n this.revokedFn = o.revoked;\n this.grants = !!o.grants;\n this.proxyHops = o.proxyHops ?? 0;\n this.now = o.now ?? Date.now;\n const window = (o.rateWindow && o.rateWindow > 0 ? o.rateWindow : 600) * 1000;\n this.registers = new RateLimiter(o.registerLimit === undefined || o.registerLimit === 0 ? 20 : o.registerLimit, window);\n this.tokens = new RateLimiter(o.tokenLimit === undefined || o.tokenLimit === 0 ? 60 : o.tokenLimit, window);\n this.errorFn = o.onError ?? ((m, err) => console.error(m, err instanceof WakeshiftError ? `${err.status} ${err.code}` : err));\n this.seed = this.wake.secretSeed();\n }\n\n /** Where MCP clients read the app's protected-resource metadata (RFC 9728). */\n get resourceMetadataUrl(): string {\n const u = new URL(this.mcpUrl);\n return u.origin + \"/.well-known/oauth-protected-resource\";\n }\n\n /**\n * Serves the OAuth endpoints and the metadata that points MCP clients at\n * them. Returns false for any other request, for your own routes. Mount it\n * before any body parser (it reads its own bodies), or after one: a parsed\n * req.body is used as it is.\n */\n async handle(req: IncomingMessage, res: ServerResponse): Promise<boolean> {\n const path = (req.url ?? \"/\").split(\"?\")[0];\n const routes: Record<string, [string, () => Promise<void> | void]> = {\n \"/.well-known/oauth-protected-resource\": [\"GET\", () => this.resourceMetadata(res)],\n \"/.well-known/oauth-protected-resource/mcp\": [\"GET\", () => this.resourceMetadata(res)],\n \"/.well-known/oauth-authorization-server\": [\"GET\", () => this.metadata(res)],\n \"/oauth/register\": [\"POST\", () => this.register(req, res)],\n \"/oauth/authorize\": [\"GET\", () => this.authorize(req, res)],\n \"/oauth/token\": [\"POST\", () => this.token(req, res)],\n };\n const r = routes[path];\n if (!r) return false;\n const method = req.method === \"HEAD\" ? \"GET\" : req.method;\n if (method !== r[0]) {\n res.writeHead(405, { Allow: r[0] === \"GET\" ? \"GET, HEAD\" : r[0], \"Content-Type\": \"text/plain\" });\n res.end(\"method not allowed\");\n return true;\n }\n try {\n await r[1]();\n } catch (err) {\n if (err instanceof OAuthFailure) {\n oauthErr(res, err.status, err.code, err.message);\n } else {\n this.errorFn(\"wakeshift: guest sign-in failed\", err);\n if (!res.headersSent) oauthErr(res, 500, \"server_error\", \"try again\");\n }\n }\n return true;\n }\n\n /**\n * Checks the request's bearer token. On success it returns the guest and\n * sets req.auth, which the MCP SDK's Streamable HTTP transport passes to\n * tool callbacks as extra.authInfo (read it with guestOf). On failure it\n * answers 401 with the WWW-Authenticate header MCP clients start signing\n * in from, and returns null.\n */\n authenticate(req: IncomingMessage & { auth?: AuthInfo }, res: ServerResponse): Guest | null {\n const h = req.headers.authorization ?? \"\";\n const m = /^Bearer\\s+(\\S+)$/i.exec(h);\n const g = m ? this.verify(m[1]) : null;\n if (!g) {\n res.writeHead(401, {\n \"WWW-Authenticate\": `Bearer resource_metadata=\"${this.resourceMetadataUrl}\"${m ? ', error=\"invalid_token\"' : \"\"}`,\n \"Content-Type\": \"text/plain\",\n });\n res.end(\"sign in: add this app to your agent again\");\n return null;\n }\n const info: GuestAuthInfo = {\n token: m![1],\n clientId: g.user,\n scopes: [],\n expiresAt: Math.floor(g.expires.getTime() / 1000),\n resource: new URL(this.mcpUrl),\n extra: { guest: g },\n };\n req.auth = info;\n return g;\n }\n\n /** Checks a guest access token; null if it isn't one of this app's, or has expired. */\n verify(token: string): Guest | null {\n const t = this.open<GuestToken>(\"access\", token);\n if (!t || typeof t.u !== \"string\" || typeof t.w !== \"string\" || typeof t.a !== \"string\" || typeof t.e !== \"number\" || this.seconds() > t.e) return null;\n return { user: t.u, workspace: t.w, agent: t.a, client: typeof t.c === \"string\" ? t.c : \"\", expires: new Date(t.e * 1000) };\n }\n\n private seconds(): number {\n return Math.floor(this.now() / 1000);\n }\n\n private resourceMetadata(res: ServerResponse): void {\n writeJSON(res, 200, { resource: this.mcpUrl, authorization_servers: [this.base], bearer_methods_supported: [\"header\"] });\n }\n\n private metadata(res: ServerResponse): void {\n const b = this.base;\n writeJSON(res, 200, {\n issuer: b,\n authorization_endpoint: b + \"/oauth/authorize\",\n token_endpoint: b + \"/oauth/token\",\n registration_endpoint: b + \"/oauth/register\",\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"client_secret_post\", \"client_secret_basic\"],\n });\n }\n\n // ── Signed values ──────────────────────────────────────────────────────\n // Each kind (client, code, access, refresh) is signed with its own key\n // derived from the app's key, so one can't pass for another.\n\n private mac(purpose: string): Buffer {\n return createHmac(\"sha256\", this.seed).update(\"appkit-guest:\" + purpose).digest();\n }\n\n private sign(purpose: string, v: unknown): string {\n return seal(this.mac(purpose), v);\n }\n\n private open<T>(purpose: string, s: unknown): T | null {\n return unseal<T>(this.mac(purpose), s);\n }\n\n /** The secret registration gave a client: an HMAC of its client_id, so it's never stored and only the app can make it. */\n private clientSecret(clientId: string): string {\n return createHmac(\"sha256\", this.mac(\"client-secret\")).update(clientId).digest(\"base64url\");\n }\n\n // clientAuth is the client a token request authenticated as, by HTTP\n // Basic (RFC 6749 2.3.1: each part form-encoded first) or client_id and\n // client_secret in the form, if its secret is right.\n private clientAuth(req: IncomingMessage, f: URLSearchParams): string | null {\n let id: string;\n let secret: string;\n const basic = /^Basic\\s+(\\S+)$/i.exec(req.headers.authorization ?? \"\");\n if (basic) {\n const raw = Buffer.from(basic[1], \"base64\").toString(\"utf8\");\n const colon = raw.indexOf(\":\");\n if (colon < 0) return null;\n const a = formUnescape(raw.slice(0, colon));\n const b = formUnescape(raw.slice(colon + 1));\n if (a === null || b === null || f.get(\"client_secret\")) return null; // one method at a time (RFC 6749 2.3)\n const formId = f.get(\"client_id\");\n if (formId && formId !== a) return null;\n id = a;\n secret = b;\n } else {\n id = f.get(\"client_id\") ?? \"\";\n secret = f.get(\"client_secret\") ?? \"\";\n }\n if (!id || !secret || !this.open<GuestClient>(\"client\", id)) return null;\n const want = Buffer.from(this.clientSecret(id));\n const got = Buffer.from(secret);\n if (got.length !== want.length || !timingSafeEqual(got, want)) return null;\n return id;\n }\n\n private async register(req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (!this.registers.allow(clientAddr(req, this.proxyHops), this.now())) {\n throw new OAuthFailure(429, \"slow_down\", \"too many registrations from this address; try again later\");\n }\n let raw: unknown;\n try {\n raw = await readJSON(req, 16 << 10);\n } catch {\n throw new OAuthFailure(400, \"invalid_client_metadata\", \"redirect_uris is required\");\n }\n const inp = (raw ?? {}) as { client_name?: unknown; redirect_uris?: unknown };\n const uris = inp.redirect_uris;\n if (!Array.isArray(uris) || uris.length === 0 || uris.length > 5 || !uris.every((u) => typeof u === \"string\")) {\n throw new OAuthFailure(400, \"invalid_client_metadata\", \"redirect_uris is required\");\n }\n for (const u of uris as string[]) {\n if (u.length > 500 || !validRedirect(u)) throw new OAuthFailure(400, \"invalid_redirect_uri\", BAD_REDIRECT);\n }\n let name = typeof inp.client_name === \"string\" ? inp.client_name.trim() : \"\";\n if (!name || Buffer.byteLength(name) > 80) name = \"MCP client\";\n const id = this.sign(\"client\", { n: name, r: uris, x: randomBytes(16).toString(\"base64url\") } satisfies GuestClient);\n res.setHeader(\"Cache-Control\", \"no-store\");\n writeJSON(res, 201, {\n client_id: id,\n client_secret: this.clientSecret(id),\n client_secret_expires_at: 0,\n client_name: name,\n redirect_uris: uris,\n token_endpoint_auth_method: \"client_secret_post\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n });\n }\n\n // authorize issues a code to a registered client with no user step (the\n // person added the app to their agent). It creates nothing: the guest is\n // made at the code exchange, once the client proves it holds its secret.\n private authorize(req: IncomingMessage, res: ServerResponse): void {\n const q = new URL(req.url ?? \"/\", \"http://x\").searchParams;\n const c = this.open<GuestClient>(\"client\", q.get(\"client_id\") ?? \"\");\n if (!c || !Array.isArray(c.r)) {\n textErr(res, 400, \"Unknown client. Register it again.\");\n return;\n }\n const redirect = q.get(\"redirect_uri\") ?? \"\";\n if (!c.r.includes(redirect)) {\n textErr(res, 400, \"That redirect URI isn't registered for this client.\");\n return;\n }\n const state = q.get(\"state\") ?? \"\";\n if (q.get(\"response_type\") !== \"code\" || !PKCE_RE.test(q.get(\"code_challenge\") ?? \"\") || q.get(\"code_challenge_method\") !== \"S256\") {\n // Said here, not by redirecting: anyone can register a client with\n // any https redirect, and there's no step for the person first, so\n // redirecting a bad request would make this an open redirect.\n textErr(res, 400, \"This sign-in request is incomplete: PKCE (S256) and response_type=code are required.\");\n return;\n }\n const code: GuestCode = { c: q.get(\"client_id\")!, r: redirect, s: state, p: q.get(\"code_challenge\")!, e: this.seconds() + CODE_TTL, k: true };\n let u: URL;\n try {\n u = new URL(redirect);\n } catch {\n textErr(res, 400, \"That redirect URI isn't registered for this client.\");\n return;\n }\n u.searchParams.set(\"code\", this.sign(\"code\", code));\n if (state) u.searchParams.set(\"state\", state);\n res.writeHead(302, { Location: u.toString() });\n res.end();\n }\n\n private async token(req: IncomingMessage, res: ServerResponse): Promise<void> {\n if (!this.tokens.allow(clientAddr(req, this.proxyHops), this.now())) {\n throw new OAuthFailure(429, \"slow_down\", \"too many token requests from this address; try again later\");\n }\n let f: URLSearchParams;\n try {\n f = await readForm(req, 32 << 10);\n } catch {\n throw new OAuthFailure(400, \"invalid_request\", \"bad form\");\n }\n const clientId = this.clientAuth(req, f);\n if (!clientId) {\n res.setHeader(\"WWW-Authenticate\", 'Basic realm=\"oauth\"');\n throw new OAuthFailure(401, \"invalid_client\", \"client authentication failed: send the client_id and client_secret registration gave you\");\n }\n let workspaceId: string;\n let agent: string;\n let name: string;\n let user: string;\n switch (f.get(\"grant_type\")) {\n case \"authorization_code\": {\n const c = this.open<GuestCode>(\"code\", f.get(\"code\") ?? \"\");\n if (!c || c.k !== true || typeof c.e !== \"number\" || this.seconds() > c.e || c.c !== clientId || c.r !== (f.get(\"redirect_uri\") ?? \"\")) {\n throw new OAuthFailure(400, \"invalid_grant\", \"the code is invalid or expired\");\n }\n const verifier = f.get(\"code_verifier\") ?? \"\";\n const sum = Buffer.from(createHash(\"sha256\").update(verifier).digest(\"base64url\"));\n const challenge = Buffer.from(String(c.p));\n if (!PKCE_RE.test(verifier) || sum.length !== challenge.length || !timingSafeEqual(sum, challenge)) {\n throw new OAuthFailure(400, \"invalid_grant\", \"PKCE verification failed\");\n }\n const cl = this.open<GuestClient>(\"client\", clientId)!;\n // One guest per registered client: its workspace, and its agent\n // named after the harness.\n name = cl.n;\n agent = harnessAgent(cl.n);\n user = \"guest:\" + guestId(clientId);\n try {\n workspaceId = await this.wake.workspace(user, \"Guest (\" + cl.n + \")\");\n if (this.grants) await this.wake.addAgent(workspaceId, agent, cl.n);\n } catch (err) {\n if (err instanceof WakeshiftError && err.code === \"workspace_limit\") throw new OAuthFailure(403, \"access_denied\", \"this app can't take more guests right now\");\n this.errorFn(\"wakeshift: making a guest\", err);\n throw new OAuthFailure(503, \"temporarily_unavailable\", \"couldn't sign in; try again\");\n }\n break;\n }\n case \"refresh_token\": {\n const t = this.open<GuestToken>(\"refresh\", f.get(\"refresh_token\") ?? \"\");\n if (!t || typeof t.e !== \"number\" || this.seconds() > t.e || t.i !== clientId || typeof t.w !== \"string\" || typeof t.a !== \"string\" || typeof t.u !== \"string\") {\n throw new OAuthFailure(400, \"invalid_grant\", \"the refresh token is invalid or expired\");\n }\n const guest: Guest = { user: t.u, workspace: t.w, agent: t.a, client: typeof t.c === \"string\" ? t.c : \"\", expires: new Date(t.e * 1000) };\n if (this.revokedFn && (await this.revokedFn(guest, new Date((t.t ?? 0) * 1000)))) {\n throw new OAuthFailure(400, \"invalid_grant\", \"the refresh token was revoked; sign in again\");\n }\n // The guest must still be there: forgetting the user (and, with\n // grants, revoking the grant) ends its refreshes.\n if (!(await this.stillThere(t.u, t.w, t.a, guest.client))) {\n throw new OAuthFailure(400, \"invalid_grant\", \"this guest's access was revoked; sign in again\");\n }\n workspaceId = t.w;\n agent = t.a;\n name = guest.client;\n user = t.u;\n break;\n }\n default:\n throw new OAuthFailure(400, \"unsupported_grant_type\", \"use authorization_code or refresh_token\");\n }\n const now = this.seconds();\n const access = this.sign(\"access\", { w: workspaceId, a: agent, c: name, u: user, e: now + this.accessTtl } satisfies GuestToken);\n // A new refresh token every time (rotation), dated, so revoked can\n // refuse those issued before a cutoff.\n const refresh = this.sign(\"refresh\", {\n w: workspaceId,\n a: agent,\n c: name,\n u: user,\n i: clientId,\n t: now,\n n: randomBytes(16).toString(\"base64url\"),\n e: now + this.refreshTtl,\n } satisfies GuestToken);\n res.setHeader(\"Cache-Control\", \"no-store\");\n writeJSON(res, 200, { access_token: access, token_type: \"Bearer\", expires_in: this.accessTtl, refresh_token: refresh });\n }\n\n // stillThere is whether the guest a refresh token names is still there:\n // its workspace is the one Wakeshift keeps for its user (a forgotten\n // user's is gone, so asking makes a new one, which is forgotten again at\n // once), and, with grants, its agent's grant is live. Throws a 503 when\n // Wakeshift can't say.\n private async stillThere(user: string, workspace: string, agent: string, client: string): Promise<boolean> {\n try {\n if (this.grants) {\n await this.wake.checkAgent(workspace, agent);\n return true;\n }\n if ((await this.wake.workspace(user, \"Guest (\" + client + \")\")) === workspace) return true;\n await this.wake.forgetUser(user);\n return false;\n } catch (err) {\n if (err instanceof WakeshiftError && (err.code === \"no_grant\" || err.code === \"unknown_workspace\" || err.code === \"workspace_limit\")) return false;\n this.errorFn(\"wakeshift: checking a guest\", err);\n throw new OAuthFailure(503, \"temporarily_unavailable\", \"couldn't refresh; try again\");\n }\n }\n}\n\n/** The guest ID for a client: a hash of its client_id. */\nfunction guestId(clientId: string): string {\n return createHash(\"sha256\").update(clientId).digest().subarray(0, 18).toString(\"base64url\");\n}\n\n// formUnescape is Go's url.QueryUnescape: + is a space, %XX a byte; null\n// if malformed.\nfunction formUnescape(s: string): string | null {\n if (/%(?![0-9A-Fa-f]{2})/.test(s)) return null;\n try {\n return decodeURIComponent(s.replace(/\\+/g, \" \"));\n } catch {\n return null;\n }\n}\n\nconst BAD_REDIRECT = \"redirect URIs are https, http on loopback (localhost, 127.0.0.1, [::1]) or a private-use scheme (cursor://…), without a fragment\";\n\n// validRedirect accepts https URLs, http on loopback (native apps, RFC\n// 8252) and private-use schemes (cursor://…), never ones a browser would\n// run or that carry a fragment, as the core's OAuth does.\nfunction validRedirect(raw: string): boolean {\n const m = /^([A-Za-z][A-Za-z0-9+.-]*):/.exec(raw);\n if (!m || m[1] !== m[1].toLowerCase() || raw.includes(\"#\")) return false;\n let u: URL;\n try {\n u = new URL(raw);\n } catch {\n return false;\n }\n switch (m[1]) {\n case \"https\":\n return u.hostname !== \"\";\n case \"http\":\n return u.hostname === \"localhost\" || (isIP(u.hostname) === 4 && u.hostname.startsWith(\"127.\")) || u.hostname === \"[::1]\";\n case \"javascript\":\n case \"data\":\n case \"file\":\n case \"blob\":\n case \"about\":\n case \"vbscript\":\n return false;\n }\n return true;\n}\n\nasync function readBody(req: IncomingMessage, limit: number): Promise<string> {\n const parsed = (req as IncomingMessage & { body?: unknown }).body;\n if (parsed !== undefined) return typeof parsed === \"string\" ? parsed : JSON.stringify(parsed);\n const chunks: Buffer[] = [];\n let n = 0;\n for await (const c of req) {\n n += (c as Buffer).length;\n if (n > limit) throw new Error(\"body too large\");\n chunks.push(c as Buffer);\n }\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\nasync function readJSON(req: IncomingMessage, limit: number): Promise<unknown> {\n const parsed = (req as IncomingMessage & { body?: unknown }).body;\n if (parsed !== undefined && typeof parsed === \"object\") return parsed;\n return JSON.parse(await readBody(req, limit));\n}\n\nasync function readForm(req: IncomingMessage, limit: number): Promise<URLSearchParams> {\n const parsed = (req as IncomingMessage & { body?: unknown }).body;\n if (parsed !== undefined && parsed !== null && typeof parsed === \"object\") {\n const out = new URLSearchParams();\n for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) if (typeof v === \"string\") out.set(k, v);\n return out;\n }\n return new URLSearchParams(await readBody(req, limit));\n}\n\nfunction writeJSON(res: ServerResponse, status: number, v: unknown): void {\n res.statusCode = status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(v) + \"\\n\");\n}\n\nfunction oauthErr(res: ServerResponse, status: number, code: string, desc: string): void {\n writeJSON(res, status, { error: code, error_description: desc });\n}\n\nfunction textErr(res: ServerResponse, status: number, msg: string): void {\n res.writeHead(status, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(msg + \"\\n\");\n}\n\n"]}
|