@anchrd/intel-api 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/openid/openid.js +17 -3
- package/dist/auth/auth.js +10 -1
- package/dist/http/http.js +4 -0
- package/dist/intel/intel.js +5 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/package.json +1 -1
|
@@ -59,11 +59,25 @@ export function createOpenId(deps) {
|
|
|
59
59
|
...(insecure ? { execute: [client.allowInsecureRequests] } : {}),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
// openid-client resolves issuer metadata via OIDC discovery unless told otherwise. Gate serves
|
|
63
|
+
// that document, but a plain OAuth 2.0 authorization server — the MCP portal on its custom
|
|
64
|
+
// domain is one — publishes only RFC 8414's oauth-authorization-server path and answers the OIDC
|
|
65
|
+
// one with a 404, which surfaced as a bare 500 on /auth/connect (#93). The fallback repeats the
|
|
66
|
+
// operation once with the OAuth document; when both fail, the second error is thrown because it
|
|
67
|
+
// belongs to the attempt that got further for the issuer that needed the fallback at all.
|
|
68
|
+
async function withAlgorithmFallback(run) {
|
|
69
|
+
try {
|
|
70
|
+
return await run();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return await run("oauth2");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
62
76
|
async function discover(issuer, clientId) {
|
|
63
77
|
const key = `${issuer}#${clientId}`;
|
|
64
78
|
let configuration = configurations.get(key);
|
|
65
79
|
if (!configuration) {
|
|
66
|
-
configuration = client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), options(issuer));
|
|
80
|
+
configuration = withAlgorithmFallback((algorithm) => client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), { ...options(issuer), ...(algorithm ? { algorithm } : {}) }));
|
|
67
81
|
configurations.set(key, configuration);
|
|
68
82
|
void configuration.catch(() => configurations.delete(key));
|
|
69
83
|
}
|
|
@@ -118,7 +132,7 @@ export function createOpenId(deps) {
|
|
|
118
132
|
throw new Error("MCP resource did not publish valid OAuth metadata");
|
|
119
133
|
},
|
|
120
134
|
async register(input) {
|
|
121
|
-
const configuration = await client.dynamicClientRegistration(new URL(input.issuer), {
|
|
135
|
+
const configuration = await withAlgorithmFallback((algorithm) => client.dynamicClientRegistration(new URL(input.issuer), {
|
|
122
136
|
client_name: input.clientName,
|
|
123
137
|
redirect_uris: [input.redirectUri],
|
|
124
138
|
response_types: ["code"],
|
|
@@ -128,7 +142,7 @@ export function createOpenId(deps) {
|
|
|
128
142
|
: { scope: "openid profile email offline_access" }),
|
|
129
143
|
token_endpoint_auth_method: "none",
|
|
130
144
|
...(isCloudflareAccess(input.issuer) ? { resource: input.resource } : {}),
|
|
131
|
-
}, client.None(), options(input.issuer));
|
|
145
|
+
}, client.None(), { ...options(input.issuer), ...(algorithm ? { algorithm } : {}) }));
|
|
132
146
|
return configuration.clientMetadata().client_id;
|
|
133
147
|
},
|
|
134
148
|
async authorizationUrl(input) {
|
package/dist/auth/auth.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { calculatePKCECodeChallenge, randomPKCECodeVerifier, randomState } from "openid-client";
|
|
2
2
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
3
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
3
4
|
import { SafeReturnPath } from "../shared/safe-return-path/safe-return-path.js";
|
|
4
5
|
const cookieName = "intel_session";
|
|
5
6
|
// What an authorization server answers when `prompt=none` would have worked, but only with somebody
|
|
@@ -54,11 +55,19 @@ export function createBrowserAuth(deps) {
|
|
|
54
55
|
const stored = await deps.clients.get(registrationKey, redirectUri);
|
|
55
56
|
if (stored)
|
|
56
57
|
return stored;
|
|
57
|
-
|
|
58
|
+
// A refused registration becomes a named state instead of a bare 500 (#93). The conversion
|
|
59
|
+
// must not eat the reason: an IntelError is an expected refusal nobody logs, so the library's
|
|
60
|
+
// own exception is written here — it is the only place that still holds it.
|
|
61
|
+
const registered = await deps.oauth
|
|
62
|
+
.register({
|
|
58
63
|
issuer,
|
|
59
64
|
redirectUri,
|
|
60
65
|
clientName: "Intel",
|
|
61
66
|
resource,
|
|
67
|
+
})
|
|
68
|
+
.catch((error) => {
|
|
69
|
+
reportUnexpectedError(error);
|
|
70
|
+
throw new IntelError(502, "client_registration_failed", "The authorization server refused the client registration");
|
|
62
71
|
});
|
|
63
72
|
return await deps.clients.put({
|
|
64
73
|
issuer: registrationKey,
|
package/dist/http/http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
5
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
6
|
import { problemDetails as problem } from "../shared/problem-details/problem-details.js";
|
|
7
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
7
8
|
// A query string is a door to the outside like a body is, so what arrives through it is closed
|
|
8
9
|
// rather than tolerated: `z.strictObject` refuses an unknown field, and this is that refusal for the
|
|
9
10
|
// half of the input Zod never sees. A silently ignored parameter is how a caller believes it asked
|
|
@@ -64,6 +65,9 @@ export function createHttp(deps) {
|
|
|
64
65
|
if (error instanceof z.ZodError) {
|
|
65
66
|
return context.json(problem(400, "invalid_request", "Request validation failed", z.prettifyError(error)), 400);
|
|
66
67
|
}
|
|
68
|
+
// Same rule as the outer app: an expected refusal explains itself, an unknown exception must
|
|
69
|
+
// leave a trace — otherwise the 500 is a fact without a reason anywhere (#93).
|
|
70
|
+
reportUnexpectedError(error);
|
|
67
71
|
return context.json(problem(500, "internal_error", "Internal server error"), 500);
|
|
68
72
|
});
|
|
69
73
|
app.use("*", async (context, next) => {
|
package/dist/intel/intel.js
CHANGED
|
@@ -4,6 +4,7 @@ import { handleMcp } from "../mcp/mcp.js";
|
|
|
4
4
|
import { authorize, authorizeBearer } from "../shared/gate-authorization/gate-authorization.js";
|
|
5
5
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
6
6
|
import { problemDetails } from "../shared/problem-details/problem-details.js";
|
|
7
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
7
8
|
export function createIntel(deps) {
|
|
8
9
|
const baseUrl = deps.baseUrl.replace(/\/+$/, "");
|
|
9
10
|
const resource = `${baseUrl}/mcp`;
|
|
@@ -17,6 +18,10 @@ export function createIntel(deps) {
|
|
|
17
18
|
if (error instanceof IntelError) {
|
|
18
19
|
return context.json(problemDetails(error.status, error.code, error.message), error.status);
|
|
19
20
|
}
|
|
21
|
+
// An IntelError is an expected refusal and explains itself; the unknown exception must leave a
|
|
22
|
+
// trace, or the 500 is undiagnosable — the worker answered, so the platform records no
|
|
23
|
+
// exception of its own (#93). The body stays generic: the log is the operator's channel.
|
|
24
|
+
reportUnexpectedError(error);
|
|
20
25
|
return context.json(problemDetails(500, "internal_error", "Internal server error"), 500);
|
|
21
26
|
});
|
|
22
27
|
app.get("/health", (context) => context.json({ status: "ok" }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function reportUnexpectedError(error: unknown): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// A library error can quote whatever the peer sent — an echoed Authorization header, a token in a
|
|
2
|
+
// URL, a response body. The log must show the failure and never the credential, so everything
|
|
3
|
+
// shaped like one is cut before the line is written. The patterns match shapes, not sources: a JWT
|
|
4
|
+
// is recognizable on its own, and other tokens only ever appear behind their label.
|
|
5
|
+
function redact(text) {
|
|
6
|
+
// ⚠️ The order is load-bearing: "Authorization: Bearer x" must hit the bearer rule first — a
|
|
7
|
+
// combined alternation would let the label rule win the leftmost match, swallow the word
|
|
8
|
+
// "Bearer" as the value, and leave the token itself standing.
|
|
9
|
+
return text
|
|
10
|
+
.replace(/\beyJ[\w-]{4,}\.[\w-]+\.[\w-]*/g, "[redacted]")
|
|
11
|
+
.replace(/\b(bearer\s+)[\w.~+/-]+=*/gi, "$1[redacted]")
|
|
12
|
+
.replace(/\b((?:access_token|refresh_token|id_token|client_secret|api_key|authorization)"?\s*[:=]\s*"?)[\w.~+/-]+=*/gi, "$1[redacted]");
|
|
13
|
+
}
|
|
14
|
+
function describe(error) {
|
|
15
|
+
if (error instanceof Error)
|
|
16
|
+
return error.stack ?? `${error.name}: ${error.message}`;
|
|
17
|
+
if (typeof error === "string")
|
|
18
|
+
return error;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(error);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return String(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// openid-client wraps the informative part — the HTTP response, the OAuth error body — into
|
|
27
|
+
// `cause`, so a log line without the cause chain would name the wrapper and hide the reason (#93).
|
|
28
|
+
const MaxCauseDepth = 5;
|
|
29
|
+
export function reportUnexpectedError(error) {
|
|
30
|
+
const parts = [];
|
|
31
|
+
let current = error;
|
|
32
|
+
for (let depth = 0; depth < MaxCauseDepth && current !== undefined; depth += 1) {
|
|
33
|
+
parts.push(describe(current));
|
|
34
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
35
|
+
}
|
|
36
|
+
console.error(redact(parts.join("\ncaused by: ")));
|
|
37
|
+
}
|