@spicrawl/mcp 0.1.0 → 0.1.2
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 +14 -0
- package/README.md +335 -135
- package/dist/chatgpt/http.d.ts +22 -0
- package/dist/chatgpt/http.js +127 -0
- package/dist/chatgpt/http.js.map +1 -0
- package/dist/chatgpt/oauth.d.ts +100 -0
- package/dist/chatgpt/oauth.js +199 -0
- package/dist/chatgpt/oauth.js.map +1 -0
- package/dist/chatgpt/tools.d.ts +33 -0
- package/dist/chatgpt/tools.js +309 -0
- package/dist/chatgpt/tools.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +3 -2
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +7 -0
- package/dist/http.js +38 -26
- package/dist/http.js.map +1 -1
- package/dist/httputil.d.ts +11 -0
- package/dist/httputil.js +33 -0
- package/dist/httputil.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +48 -2
- package/dist/server.js +20 -7
- package/dist/server.js.map +1 -1
- package/dist/tools/batch.d.ts +11 -0
- package/dist/tools/batch.js +44 -38
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/browser.js +7 -2
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/common.d.ts +21 -2
- package/dist/tools/common.js +14 -0
- package/dist/tools/common.js.map +1 -1
- package/dist/tools/docs.js +5 -5
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/scrape.js +23 -8
- package/dist/tools/scrape.js.map +1 -1
- package/dist/tools/sessions.js +13 -11
- package/dist/tools/sessions.js.map +1 -1
- package/package.json +17 -9
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ChatGPT surface's HTTP routes, mounted by http.ts when
|
|
3
|
+
* SPICRAWL_CHATGPT_ENABLED is on (all of them answer 404 otherwise):
|
|
4
|
+
*
|
|
5
|
+
* POST /chatgpt/mcp stateless MCP, OAuth access tokens only
|
|
6
|
+
* GET /.well-known/oauth-protected-resource/chatgpt/mcp protected-resource metadata (RFC 9728)
|
|
7
|
+
* GET /.well-known/oauth-protected-resource the same, only with SPICRAWL_CHATGPT_ROOT_PRM
|
|
8
|
+
* GET /.well-known/openai-apps-challenge OpenAI domain verification token
|
|
9
|
+
*
|
|
10
|
+
* Stateless: every POST is verified on its own and served by a fresh MCP server
|
|
11
|
+
* and transport, so no session can outlive or outrank the token that made it.
|
|
12
|
+
*/
|
|
13
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
14
|
+
import { type ChatgptConfig } from "./oauth.js";
|
|
15
|
+
export declare const CHATGPT_MCP_PATH = "/chatgpt/mcp";
|
|
16
|
+
export interface ChatgptDeps {
|
|
17
|
+
baseURL: string;
|
|
18
|
+
publicBaseURL: string;
|
|
19
|
+
log: (...a: unknown[]) => void;
|
|
20
|
+
}
|
|
21
|
+
/** Returns a router: true when it answered the request, false to let the caller 404 it. */
|
|
22
|
+
export declare function chatgptRoutes(config: ChatgptConfig, deps: ChatgptDeps): (req: IncomingMessage, res: ServerResponse, path: string) => boolean;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ChatGPT surface's HTTP routes, mounted by http.ts when
|
|
3
|
+
* SPICRAWL_CHATGPT_ENABLED is on (all of them answer 404 otherwise):
|
|
4
|
+
*
|
|
5
|
+
* POST /chatgpt/mcp stateless MCP, OAuth access tokens only
|
|
6
|
+
* GET /.well-known/oauth-protected-resource/chatgpt/mcp protected-resource metadata (RFC 9728)
|
|
7
|
+
* GET /.well-known/oauth-protected-resource the same, only with SPICRAWL_CHATGPT_ROOT_PRM
|
|
8
|
+
* GET /.well-known/openai-apps-challenge OpenAI domain verification token
|
|
9
|
+
*
|
|
10
|
+
* Stateless: every POST is verified on its own and served by a fresh MCP server
|
|
11
|
+
* and transport, so no session can outlive or outrank the token that made it.
|
|
12
|
+
*/
|
|
13
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
|
+
import { ILayerClient } from "../client.js";
|
|
15
|
+
import { KEY_RE, bearerToken, readJSON, rpcError } from "../httputil.js";
|
|
16
|
+
import { ACCESS_TOKEN_RE, Introspector, SCOPES, bearerChallenge, prmPath, protectedResourceMetadata, } from "./oauth.js";
|
|
17
|
+
import { TOOL_SCOPES, buildChatgptServer } from "./tools.js";
|
|
18
|
+
export const CHATGPT_MCP_PATH = "/chatgpt/mcp";
|
|
19
|
+
const ROOT_PRM_PATH = "/.well-known/oauth-protected-resource";
|
|
20
|
+
const CHALLENGE_PATH = "/.well-known/openai-apps-challenge";
|
|
21
|
+
/** Scopes a JSON-RPC message (or batch of them) needs: those of every tool it calls. */
|
|
22
|
+
function requiredScopes(body) {
|
|
23
|
+
const out = new Set();
|
|
24
|
+
for (const msg of Array.isArray(body) ? body : [body]) {
|
|
25
|
+
if (!msg || typeof msg !== "object" || msg.method !== "tools/call")
|
|
26
|
+
continue;
|
|
27
|
+
const name = msg.params?.name;
|
|
28
|
+
for (const s of (typeof name === "string" && TOOL_SCOPES[name]) || [])
|
|
29
|
+
out.add(s);
|
|
30
|
+
}
|
|
31
|
+
return SCOPES.filter((s) => out.has(s));
|
|
32
|
+
}
|
|
33
|
+
/** Returns a router: true when it answered the request, false to let the caller 404 it. */
|
|
34
|
+
export function chatgptRoutes(config, deps) {
|
|
35
|
+
const introspector = new Introspector(config, { log: deps.log });
|
|
36
|
+
const prm = prmPath(config.resource);
|
|
37
|
+
const metadata = JSON.stringify(protectedResourceMetadata(config));
|
|
38
|
+
const unauthorized = (res, description) => rpcError(res, 401, -32001, `Unauthorized: ${description ?? "connect with OAuth to use this endpoint"}`, {
|
|
39
|
+
"WWW-Authenticate": bearerChallenge(config.resource, description ? { error: "invalid_token", description } : {}),
|
|
40
|
+
});
|
|
41
|
+
async function handleMcp(req, res) {
|
|
42
|
+
const token = bearerToken(req);
|
|
43
|
+
if (!token)
|
|
44
|
+
return unauthorized(res);
|
|
45
|
+
if (KEY_RE.test(token)) {
|
|
46
|
+
return unauthorized(res, "API keys are not accepted on this endpoint; connect the Spicrawl app with OAuth");
|
|
47
|
+
}
|
|
48
|
+
if (!ACCESS_TOKEN_RE.test(token))
|
|
49
|
+
return unauthorized(res, "The access token is malformed");
|
|
50
|
+
const check = await introspector.introspect(token);
|
|
51
|
+
if (!check.ok) {
|
|
52
|
+
return check.status === 401
|
|
53
|
+
? unauthorized(res, check.description)
|
|
54
|
+
: rpcError(res, 503, -32000, `Service Unavailable: ${check.description}; retry shortly`, { "Retry-After": "5" });
|
|
55
|
+
}
|
|
56
|
+
const grant = check.grant;
|
|
57
|
+
if (req.method !== "POST") {
|
|
58
|
+
return rpcError(res, 405, -32000, "Method not allowed: this endpoint is stateless; POST JSON-RPC requests", { Allow: "POST" });
|
|
59
|
+
}
|
|
60
|
+
let body;
|
|
61
|
+
try {
|
|
62
|
+
body = await readJSON(req);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return rpcError(res, 400, -32700, "Parse error: body must be JSON (max 4 MiB)");
|
|
66
|
+
}
|
|
67
|
+
const missing = requiredScopes(body).filter((s) => !grant.scopes.has(s));
|
|
68
|
+
if (missing.length) {
|
|
69
|
+
const description = `This tool needs the ${missing.join(" and ")} permission; reconnect the Spicrawl app to grant it`;
|
|
70
|
+
return rpcError(res, 403, -32003, `Forbidden: ${description}`, {
|
|
71
|
+
"WWW-Authenticate": bearerChallenge(config.resource, {
|
|
72
|
+
scope: SCOPES.filter((s) => grant.scopes.has(s) || missing.includes(s)).join(" "),
|
|
73
|
+
error: "insufficient_scope",
|
|
74
|
+
description,
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const client = new ILayerClient({ apiKey: grant.apiKey, baseURL: deps.baseURL, publicBaseURL: deps.publicBaseURL });
|
|
79
|
+
const server = buildChatgptServer(client, {
|
|
80
|
+
upstreamChallenge: (scopes) => bearerChallenge(config.resource, {
|
|
81
|
+
scope: (scopes.length ? scopes : SCOPES).join(" "),
|
|
82
|
+
error: "invalid_token",
|
|
83
|
+
description: "The Spicrawl credential behind this access token was refused; reconnect the Spicrawl app",
|
|
84
|
+
}),
|
|
85
|
+
onUpstreamUnauthorized: () => introspector.forget(token),
|
|
86
|
+
});
|
|
87
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
88
|
+
res.on("close", () => {
|
|
89
|
+
void transport.close().catch(() => { });
|
|
90
|
+
void server.close().catch(() => { });
|
|
91
|
+
});
|
|
92
|
+
await server.connect(transport);
|
|
93
|
+
await transport.handleRequest(req, res, body);
|
|
94
|
+
}
|
|
95
|
+
const sendMetadata = (req, res) => {
|
|
96
|
+
res.writeHead(200, {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
"Cache-Control": "public, max-age=300",
|
|
99
|
+
"Access-Control-Allow-Origin": "*",
|
|
100
|
+
});
|
|
101
|
+
res.end(req.method === "HEAD" ? undefined : metadata);
|
|
102
|
+
};
|
|
103
|
+
return (req, res, path) => {
|
|
104
|
+
if (!config.enabled)
|
|
105
|
+
return false;
|
|
106
|
+
const read = req.method === "GET" || req.method === "HEAD";
|
|
107
|
+
if (path === CHATGPT_MCP_PATH) {
|
|
108
|
+
handleMcp(req, res).catch((err) => {
|
|
109
|
+
deps.log("chatgpt request failed:", err instanceof Error ? err.message : String(err));
|
|
110
|
+
rpcError(res, 500, -32603, "Internal error");
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (read && (path === prm || (config.rootPrm && path === ROOT_PRM_PATH))) {
|
|
115
|
+
sendMetadata(req, res);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (read && path === CHALLENGE_PATH && config.appsChallenge) {
|
|
119
|
+
const bytes = Buffer.from(config.appsChallenge, "utf8");
|
|
120
|
+
res.writeHead(200, { "Content-Type": "text/plain", "Content-Length": String(bytes.length) });
|
|
121
|
+
res.end(req.method === "HEAD" ? undefined : bytes);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/chatgpt/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth for the ChatGPT surface (`/chatgpt/mcp`): its configuration, the
|
|
3
|
+
* protected-resource metadata it publishes (RFC 9728), the WWW-Authenticate
|
|
4
|
+
* challenges it answers with, and access-token introspection (RFC 7662) against
|
|
5
|
+
* the Spicrawl authorization server.
|
|
6
|
+
*
|
|
7
|
+
* The surface never takes an API key. A caller presents an OAuth access token
|
|
8
|
+
* (`spicrawl_oat_…`); the authorization server says which project it belongs to
|
|
9
|
+
* and hands back the API key to run that project's calls under. Neither the
|
|
10
|
+
* token nor that key is ever logged.
|
|
11
|
+
*/
|
|
12
|
+
/** Every scope the surface knows, in the order the challenge lists them. */
|
|
13
|
+
export declare const SCOPES: readonly ["scrape", "batch"];
|
|
14
|
+
export type Scope = (typeof SCOPES)[number];
|
|
15
|
+
export declare const DEFAULT_RESOURCE = "https://mcp.spicrawl.com/chatgpt/mcp";
|
|
16
|
+
export declare const DEFAULT_ISSUER = "https://app.spicrawl.com";
|
|
17
|
+
export declare const RESOURCE_DOCUMENTATION = "https://docs.spicrawl.com/agents/mcp";
|
|
18
|
+
/** OAuth access tokens the Spicrawl authorization server issues. */
|
|
19
|
+
export declare const ACCESS_TOKEN_RE: RegExp;
|
|
20
|
+
/** A positive introspection is reused for at most this long, and never past the token's `exp`. */
|
|
21
|
+
export declare const CACHE_MAX_MS = 30000;
|
|
22
|
+
export interface ChatgptConfig {
|
|
23
|
+
/** SPICRAWL_CHATGPT_ENABLED: the kill switch. Off: every route below answers 404. */
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
/** SPICRAWL_CHATGPT_RESOURCE: the resource identifier tokens must be issued for (`aud`). */
|
|
26
|
+
resource: string;
|
|
27
|
+
/** SPICRAWL_OAUTH_ISSUER: the authorization server, no trailing slash. */
|
|
28
|
+
issuer: string;
|
|
29
|
+
/** SPICRAWL_OAUTH_INTROSPECT_SECRET: this server's credential at the introspection endpoint. */
|
|
30
|
+
introspectSecret: string;
|
|
31
|
+
/** SPICRAWL_CHATGPT_ROOT_PRM: also publish the metadata at the root well-known path. */
|
|
32
|
+
rootPrm: boolean;
|
|
33
|
+
/** SPICRAWL_OPENAI_APPS_CHALLENGE (or `…_FILE`): the domain-verification token, or "" for none. */
|
|
34
|
+
appsChallenge: string;
|
|
35
|
+
}
|
|
36
|
+
/** The configuration from the environment. Throws when a set value is unusable. */
|
|
37
|
+
export declare function chatgptConfigFromEnv(env?: NodeJS.ProcessEnv): ChatgptConfig;
|
|
38
|
+
/** Where the resource's metadata lives: `/.well-known/oauth-protected-resource` + the resource's path (RFC 9728 §3.1). */
|
|
39
|
+
export declare function prmPath(resource: string): string;
|
|
40
|
+
/** The metadata's absolute URL, as the challenge names it. */
|
|
41
|
+
export declare function prmURL(resource: string): string;
|
|
42
|
+
/** The protected-resource metadata document (RFC 9728). */
|
|
43
|
+
export declare function protectedResourceMetadata(config: ChatgptConfig): {
|
|
44
|
+
resource: string;
|
|
45
|
+
authorization_servers: string[];
|
|
46
|
+
scopes_supported: ("scrape" | "batch")[];
|
|
47
|
+
resource_documentation: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A `Bearer` challenge (RFC 6750 §3, RFC 9728 §5.1). Without `error` it is the
|
|
51
|
+
* plain "authenticate here" answer to a request that sent no token.
|
|
52
|
+
*/
|
|
53
|
+
export declare function bearerChallenge(resource: string, opts?: {
|
|
54
|
+
scope?: string;
|
|
55
|
+
error?: "invalid_token" | "insufficient_scope";
|
|
56
|
+
description?: string;
|
|
57
|
+
}): string;
|
|
58
|
+
/** What an accepted token grants. */
|
|
59
|
+
export interface Grant {
|
|
60
|
+
scopes: Set<string>;
|
|
61
|
+
/** The project's API key the token stands for; the upstream calls run under it. Never logged. */
|
|
62
|
+
apiKey: string;
|
|
63
|
+
clientId?: string;
|
|
64
|
+
/** Milliseconds since the epoch; undefined when the server sent no `exp`. */
|
|
65
|
+
expiresAt?: number;
|
|
66
|
+
}
|
|
67
|
+
export type Introspection = {
|
|
68
|
+
ok: true;
|
|
69
|
+
grant: Grant;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
status: 401;
|
|
73
|
+
description: string;
|
|
74
|
+
} | {
|
|
75
|
+
ok: false;
|
|
76
|
+
status: 503;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Introspects access tokens at `<issuer>/api/oauth/introspect`, caching
|
|
81
|
+
* positive answers by the token's SHA-256 for at most 30 s and never past the
|
|
82
|
+
* token's expiry. A negative answer is never cached, so a token issued a moment
|
|
83
|
+
* later is not refused from memory.
|
|
84
|
+
*/
|
|
85
|
+
export declare class Introspector {
|
|
86
|
+
private readonly config;
|
|
87
|
+
private readonly cache;
|
|
88
|
+
private readonly log;
|
|
89
|
+
constructor(config: Pick<ChatgptConfig, "issuer" | "resource" | "introspectSecret">, options?: {
|
|
90
|
+
log?: (...a: unknown[]) => void;
|
|
91
|
+
now?: () => number;
|
|
92
|
+
});
|
|
93
|
+
private now;
|
|
94
|
+
get endpoint(): string;
|
|
95
|
+
/** Forgets a token, e.g. once the API refused the key it stood for. */
|
|
96
|
+
forget(token: string): void;
|
|
97
|
+
introspect(token: string): Promise<Introspection>;
|
|
98
|
+
/** Drops expired entries, then the oldest ones, to stay under the cap. */
|
|
99
|
+
private evict;
|
|
100
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth for the ChatGPT surface (`/chatgpt/mcp`): its configuration, the
|
|
3
|
+
* protected-resource metadata it publishes (RFC 9728), the WWW-Authenticate
|
|
4
|
+
* challenges it answers with, and access-token introspection (RFC 7662) against
|
|
5
|
+
* the Spicrawl authorization server.
|
|
6
|
+
*
|
|
7
|
+
* The surface never takes an API key. A caller presents an OAuth access token
|
|
8
|
+
* (`spicrawl_oat_…`); the authorization server says which project it belongs to
|
|
9
|
+
* and hands back the API key to run that project's calls under. Neither the
|
|
10
|
+
* token nor that key is ever logged.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { USER_AGENT } from "../client.js";
|
|
15
|
+
import { isTruthy } from "../server.js";
|
|
16
|
+
import { KEY_RE } from "../httputil.js";
|
|
17
|
+
/** Every scope the surface knows, in the order the challenge lists them. */
|
|
18
|
+
export const SCOPES = ["scrape", "batch"];
|
|
19
|
+
export const DEFAULT_RESOURCE = "https://mcp.spicrawl.com/chatgpt/mcp";
|
|
20
|
+
export const DEFAULT_ISSUER = "https://app.spicrawl.com";
|
|
21
|
+
export const RESOURCE_DOCUMENTATION = "https://docs.spicrawl.com/agents/mcp";
|
|
22
|
+
/** OAuth access tokens the Spicrawl authorization server issues. */
|
|
23
|
+
export const ACCESS_TOKEN_RE = /^spicrawl_oat_[A-Za-z0-9_-]{8,}$/;
|
|
24
|
+
const INTROSPECT_TIMEOUT_MS = 5_000;
|
|
25
|
+
/** A positive introspection is reused for at most this long, and never past the token's `exp`. */
|
|
26
|
+
export const CACHE_MAX_MS = 30_000;
|
|
27
|
+
const CACHE_MAX_ENTRIES = 10_000;
|
|
28
|
+
const trimURL = (s) => (s ?? "").trim().replace(/\/+$/, "");
|
|
29
|
+
/** The configuration from the environment. Throws when a set value is unusable. */
|
|
30
|
+
export function chatgptConfigFromEnv(env = process.env) {
|
|
31
|
+
const resource = trimURL(env.SPICRAWL_CHATGPT_RESOURCE) || DEFAULT_RESOURCE;
|
|
32
|
+
const issuer = trimURL(env.SPICRAWL_OAUTH_ISSUER) || DEFAULT_ISSUER;
|
|
33
|
+
for (const [name, v] of [["SPICRAWL_CHATGPT_RESOURCE", resource], ["SPICRAWL_OAUTH_ISSUER", issuer]]) {
|
|
34
|
+
let u;
|
|
35
|
+
try {
|
|
36
|
+
u = new URL(v);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`${name} '${v}' is not an absolute URL`);
|
|
40
|
+
}
|
|
41
|
+
if (u.protocol !== "https:" && u.protocol !== "http:")
|
|
42
|
+
throw new Error(`${name} '${v}' must be http(s)`);
|
|
43
|
+
}
|
|
44
|
+
let appsChallenge = (env.SPICRAWL_OPENAI_APPS_CHALLENGE ?? "").trim();
|
|
45
|
+
const file = (env.SPICRAWL_OPENAI_APPS_CHALLENGE_FILE ?? "").trim();
|
|
46
|
+
if (!appsChallenge && file)
|
|
47
|
+
appsChallenge = readFileSync(file, "utf8").trim();
|
|
48
|
+
return {
|
|
49
|
+
enabled: isTruthy(env.SPICRAWL_CHATGPT_ENABLED),
|
|
50
|
+
resource,
|
|
51
|
+
issuer,
|
|
52
|
+
introspectSecret: (env.SPICRAWL_OAUTH_INTROSPECT_SECRET ?? "").trim(),
|
|
53
|
+
rootPrm: isTruthy(env.SPICRAWL_CHATGPT_ROOT_PRM),
|
|
54
|
+
appsChallenge,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Where the resource's metadata lives: `/.well-known/oauth-protected-resource` + the resource's path (RFC 9728 §3.1). */
|
|
58
|
+
export function prmPath(resource) {
|
|
59
|
+
const path = new URL(resource).pathname.replace(/\/+$/, "");
|
|
60
|
+
return `/.well-known/oauth-protected-resource${path === "/" ? "" : path}`;
|
|
61
|
+
}
|
|
62
|
+
/** The metadata's absolute URL, as the challenge names it. */
|
|
63
|
+
export function prmURL(resource) {
|
|
64
|
+
return new URL(resource).origin + prmPath(resource);
|
|
65
|
+
}
|
|
66
|
+
/** The protected-resource metadata document (RFC 9728). */
|
|
67
|
+
export function protectedResourceMetadata(config) {
|
|
68
|
+
return {
|
|
69
|
+
resource: config.resource,
|
|
70
|
+
authorization_servers: [config.issuer],
|
|
71
|
+
scopes_supported: [...SCOPES],
|
|
72
|
+
resource_documentation: RESOURCE_DOCUMENTATION,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** A quoted-string parameter value: no quotes, backslashes or control characters. */
|
|
76
|
+
const quotable = (s) => s.replace(/["\\\u0000-\u001f\u007f]/g, " ");
|
|
77
|
+
/**
|
|
78
|
+
* A `Bearer` challenge (RFC 6750 §3, RFC 9728 §5.1). Without `error` it is the
|
|
79
|
+
* plain "authenticate here" answer to a request that sent no token.
|
|
80
|
+
*/
|
|
81
|
+
export function bearerChallenge(resource, opts = {}) {
|
|
82
|
+
const parts = [`resource_metadata="${prmURL(resource)}"`, `scope="${quotable(opts.scope ?? SCOPES.join(" "))}"`];
|
|
83
|
+
if (opts.error)
|
|
84
|
+
parts.push(`error="${opts.error}"`);
|
|
85
|
+
if (opts.error && opts.description)
|
|
86
|
+
parts.push(`error_description="${quotable(opts.description)}"`);
|
|
87
|
+
return `Bearer ${parts.join(", ")}`;
|
|
88
|
+
}
|
|
89
|
+
const tokenHash = (token) => createHash("sha256").update(token).digest("hex");
|
|
90
|
+
/**
|
|
91
|
+
* Introspects access tokens at `<issuer>/api/oauth/introspect`, caching
|
|
92
|
+
* positive answers by the token's SHA-256 for at most 30 s and never past the
|
|
93
|
+
* token's expiry. A negative answer is never cached, so a token issued a moment
|
|
94
|
+
* later is not refused from memory.
|
|
95
|
+
*/
|
|
96
|
+
export class Introspector {
|
|
97
|
+
config;
|
|
98
|
+
cache = new Map();
|
|
99
|
+
log;
|
|
100
|
+
constructor(config, options = {}) {
|
|
101
|
+
this.config = config;
|
|
102
|
+
this.log = options.log ?? (() => { });
|
|
103
|
+
if (options.now)
|
|
104
|
+
this.now = options.now;
|
|
105
|
+
}
|
|
106
|
+
now = () => Date.now();
|
|
107
|
+
get endpoint() {
|
|
108
|
+
return `${this.config.issuer}/api/oauth/introspect`;
|
|
109
|
+
}
|
|
110
|
+
/** Forgets a token, e.g. once the API refused the key it stood for. */
|
|
111
|
+
forget(token) {
|
|
112
|
+
this.cache.delete(tokenHash(token));
|
|
113
|
+
}
|
|
114
|
+
async introspect(token) {
|
|
115
|
+
const key = tokenHash(token);
|
|
116
|
+
const now = this.now();
|
|
117
|
+
const hit = this.cache.get(key);
|
|
118
|
+
if (hit) {
|
|
119
|
+
if (hit.until > now)
|
|
120
|
+
return { ok: true, grant: hit.grant };
|
|
121
|
+
this.cache.delete(key);
|
|
122
|
+
}
|
|
123
|
+
if (!this.config.introspectSecret) {
|
|
124
|
+
this.log("chatgpt: SPICRAWL_OAUTH_INTROSPECT_SECRET is not set; cannot verify access tokens");
|
|
125
|
+
return { ok: false, status: 503, description: "token verification is not configured on this server" };
|
|
126
|
+
}
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await fetch(this.endpoint, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${this.config.introspectSecret}`,
|
|
133
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
134
|
+
Accept: "application/json",
|
|
135
|
+
"User-Agent": USER_AGENT,
|
|
136
|
+
},
|
|
137
|
+
body: new URLSearchParams({ token }).toString(),
|
|
138
|
+
signal: AbortSignal.timeout(INTROSPECT_TIMEOUT_MS),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
this.log("chatgpt: introspection unreachable:", err instanceof Error ? err.message : String(err));
|
|
143
|
+
return { ok: false, status: 503, description: "could not reach the authorization server to verify the token" };
|
|
144
|
+
}
|
|
145
|
+
const text = await res.text().catch(() => "");
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
// A refusal of OUR credential (401/403) or a server error says nothing about the caller's token.
|
|
148
|
+
this.log(`chatgpt: introspection answered HTTP ${res.status}`);
|
|
149
|
+
return { ok: false, status: 503, description: "the authorization server could not verify the token" };
|
|
150
|
+
}
|
|
151
|
+
let body;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(text);
|
|
154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
155
|
+
throw new Error("not an object");
|
|
156
|
+
body = parsed;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
this.log("chatgpt: introspection answered a body that is not a JSON object");
|
|
160
|
+
return { ok: false, status: 503, description: "the authorization server could not verify the token" };
|
|
161
|
+
}
|
|
162
|
+
if (body.active !== true)
|
|
163
|
+
return { ok: false, status: 401, description: "The access token is invalid, expired or revoked" };
|
|
164
|
+
if (body.aud !== this.config.resource) {
|
|
165
|
+
return { ok: false, status: 401, description: "The access token was not issued for this resource" };
|
|
166
|
+
}
|
|
167
|
+
const exp = typeof body.exp === "number" && Number.isFinite(body.exp) ? body.exp * 1000 : undefined;
|
|
168
|
+
if (exp !== undefined && exp <= now)
|
|
169
|
+
return { ok: false, status: 401, description: "The access token has expired" };
|
|
170
|
+
const apiKey = typeof body.api_key === "string" ? body.api_key : "";
|
|
171
|
+
if (!KEY_RE.test(apiKey)) {
|
|
172
|
+
this.log("chatgpt: an active token came back without a usable API credential");
|
|
173
|
+
return { ok: false, status: 401, description: "The access token is not linked to a Spicrawl project" };
|
|
174
|
+
}
|
|
175
|
+
const grant = {
|
|
176
|
+
scopes: new Set(typeof body.scope === "string" ? body.scope.split(/\s+/).filter(Boolean) : []),
|
|
177
|
+
apiKey,
|
|
178
|
+
clientId: typeof body.client_id === "string" ? body.client_id : undefined,
|
|
179
|
+
expiresAt: exp,
|
|
180
|
+
};
|
|
181
|
+
const until = Math.min(now + CACHE_MAX_MS, exp ?? Infinity);
|
|
182
|
+
if (this.cache.size >= CACHE_MAX_ENTRIES)
|
|
183
|
+
this.evict(now);
|
|
184
|
+
this.cache.set(key, { grant, until });
|
|
185
|
+
return { ok: true, grant };
|
|
186
|
+
}
|
|
187
|
+
/** Drops expired entries, then the oldest ones, to stay under the cap. */
|
|
188
|
+
evict(now) {
|
|
189
|
+
for (const [k, e] of this.cache)
|
|
190
|
+
if (e.until <= now)
|
|
191
|
+
this.cache.delete(k);
|
|
192
|
+
for (const k of this.cache.keys()) {
|
|
193
|
+
if (this.cache.size < CACHE_MAX_ENTRIES)
|
|
194
|
+
break;
|
|
195
|
+
this.cache.delete(k);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/chatgpt/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The restricted tool profile served on `/chatgpt/mcp`.
|
|
3
|
+
*
|
|
4
|
+
* A smaller, read-mostly set than `/mcp`, shaped by OpenAI's app guidelines:
|
|
5
|
+
* a scrape is always a GET with no browser actions, custom headers, proxies,
|
|
6
|
+
* sessions or engine pins; a batch takes a short list of URLs with a credit
|
|
7
|
+
* budget; results come back without request ids, timestamps or internal ids.
|
|
8
|
+
* Every tool states its OAuth scopes (`securitySchemes`, top level and in
|
|
9
|
+
* `_meta`) so the client can ask for the right consent.
|
|
10
|
+
*/
|
|
11
|
+
import { ILayerClient } from "../client.js";
|
|
12
|
+
import { SpicrawlMcpServer } from "../server.js";
|
|
13
|
+
import type { Scope } from "./oauth.js";
|
|
14
|
+
export type SecurityScheme = {
|
|
15
|
+
type: "oauth2";
|
|
16
|
+
scopes: Scope[];
|
|
17
|
+
};
|
|
18
|
+
/** Each tool on this surface with the scopes a call to it needs. Anything else registered is dropped. */
|
|
19
|
+
export declare const TOOL_SCOPES: Record<string, Scope[]>;
|
|
20
|
+
export declare function sanitizeScrape(data: unknown): unknown;
|
|
21
|
+
export declare function sanitizeJob(data: unknown): unknown;
|
|
22
|
+
export declare function sanitizeResults(data: unknown): unknown;
|
|
23
|
+
export interface ChatgptContext {
|
|
24
|
+
/**
|
|
25
|
+
* The WWW-Authenticate challenge for a tool error when the API refused the
|
|
26
|
+
* credential behind the token mid-call (`_meta["mcp/www_authenticate"]`).
|
|
27
|
+
*/
|
|
28
|
+
upstreamChallenge: (scopes: Scope[]) => string;
|
|
29
|
+
/** Called when the API refused that credential, so the token is not trusted from cache. */
|
|
30
|
+
onUpstreamUnauthorized: () => void;
|
|
31
|
+
}
|
|
32
|
+
/** One MCP server for one ChatGPT request, running under the API key the token stands for. */
|
|
33
|
+
export declare function buildChatgptServer(client: ILayerClient, ctx: ChatgptContext): SpicrawlMcpServer;
|