@wakeshift/sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/guest.d.ts DELETED
@@ -1,119 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
3
- import type { Wakeshift } from "./wakeshift.js";
4
- /** Who a guest access token is for. */
5
- export interface Guest {
6
- /**
7
- * The app's ID for the guest: pass it as wake.call's user, so topics keep
8
- * the guest's conversations in its workspace. guest:<id>, one per
9
- * registered client.
10
- */
11
- user: string;
12
- /** The guest's workspace and agent (named after its harness: claude-code). */
13
- workspace: string;
14
- agent: string;
15
- /** The client's name, e.g. "Claude Code". */
16
- client: string;
17
- expires: Date;
18
- }
19
- export interface GuestAuthOptions {
20
- wake: Wakeshift;
21
- /** The app's MCP URL: the OAuth resource (https://…/mcp). */
22
- mcpUrl: string;
23
- /** The app's public URL, where the OAuth endpoints are (default: mcpUrl without /mcp). */
24
- base?: string;
25
- /** Seconds an access token lasts (default an hour). */
26
- accessTtl?: number;
27
- /** Seconds a refresh token lasts (default 30 days); each refresh returns a new one, good for this long from then. */
28
- refreshTtl?: number;
29
- /**
30
- * Asked at each refresh whether the guest's refresh token issued at
31
- * issuedAt is revoked: an app's own denylist, or a per-guest cutoff. A
32
- * refresh is also refused once wake.forgetUser(guest.user) has run (and,
33
- * with grants, once the guest's grant is revoked), so either signs the
34
- * guest out within accessTtl.
35
- */
36
- revoked?: (guest: Guest, issuedAt: Date) => boolean | Promise<boolean>;
37
- /**
38
- * Also give each guest's agent a grant for the app's manifest agent scope,
39
- * as the Go SDK does, and refuse refreshes once it's revoked. Needs a
40
- * manifest with an agent scope; topics don't use grants, so leave it off
41
- * for an app that only uses topics.
42
- */
43
- grants?: boolean;
44
- /**
45
- * How many trusted proxies in front append to X-Forwarded-For (behind
46
- * Google's load balancer: 2), for the per-address limits. 0 (the
47
- * default): the connection's own address.
48
- */
49
- proxyHops?: number;
50
- /** Registrations (default 20) and token requests (default 60) per address per rateWindow, per process; negative turns a limit off. */
51
- registerLimit?: number;
52
- tokenLimit?: number;
53
- /** Seconds (default 600). */
54
- rateWindow?: number;
55
- /** Called with a problem worth logging (never with a token or secret). Default: console.error. */
56
- onError?: (message: string, err: unknown) => void;
57
- /** For tests. */
58
- now?: () => number;
59
- }
60
- /** What the MCP transport passes to tool callbacks (extra.authInfo) for a guest. */
61
- export type GuestAuthInfo = AuthInfo & {
62
- extra: {
63
- guest: Guest;
64
- };
65
- };
66
- /** The guest a tool call came from, if GuestAuth signed it in (extra: the tool callback's second argument). */
67
- export declare function guestOf(extra: {
68
- authInfo?: AuthInfo;
69
- } | undefined): Guest | undefined;
70
- export declare class GuestAuth {
71
- private readonly wake;
72
- readonly mcpUrl: string;
73
- readonly base: string;
74
- private readonly accessTtl;
75
- private readonly refreshTtl;
76
- private readonly revokedFn?;
77
- private readonly grants;
78
- private readonly proxyHops;
79
- private readonly registers;
80
- private readonly tokens;
81
- private readonly errorFn;
82
- private readonly now;
83
- private readonly seed;
84
- constructor(o: GuestAuthOptions);
85
- /** Where MCP clients read the app's protected-resource metadata (RFC 9728). */
86
- get resourceMetadataUrl(): string;
87
- /**
88
- * Serves the OAuth endpoints and the metadata that points MCP clients at
89
- * them. Returns false for any other request, for your own routes. Mount it
90
- * before any body parser (it reads its own bodies), or after one: a parsed
91
- * req.body is used as it is.
92
- */
93
- handle(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
94
- /**
95
- * Checks the request's bearer token. On success it returns the guest and
96
- * sets req.auth, which the MCP SDK's Streamable HTTP transport passes to
97
- * tool callbacks as extra.authInfo (read it with guestOf). On failure it
98
- * answers 401 with the WWW-Authenticate header MCP clients start signing
99
- * in from, and returns null.
100
- */
101
- authenticate(req: IncomingMessage & {
102
- auth?: AuthInfo;
103
- }, res: ServerResponse): Guest | null;
104
- /** Checks a guest access token; null if it isn't one of this app's, or has expired. */
105
- verify(token: string): Guest | null;
106
- private seconds;
107
- private resourceMetadata;
108
- private metadata;
109
- private mac;
110
- private sign;
111
- private open;
112
- /** The secret registration gave a client: an HMAC of its client_id, so it's never stored and only the app can make it. */
113
- private clientSecret;
114
- private clientAuth;
115
- private register;
116
- private authorize;
117
- private token;
118
- private stillThere;
119
- }
package/dist/guest.js DELETED
@@ -1,506 +0,0 @@
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