authflow-cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/client-conformance.d.ts +34 -0
- package/dist/client-conformance.js +379 -0
- package/dist/client-conformance.js.map +1 -0
- package/dist/client-fixtures.d.ts +67 -0
- package/dist/client-fixtures.js +130 -0
- package/dist/client-fixtures.js.map +1 -0
- package/dist/commands/doctor.js +13 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/probe.js +1 -1
- package/dist/commands/probe.js.map +1 -1
- package/dist/doctor.d.ts +26 -2
- package/dist/doctor.js +127 -16
- package/dist/doctor.js.map +1 -1
- package/dist/google-redirect.d.ts +31 -0
- package/dist/google-redirect.js +479 -0
- package/dist/google-redirect.js.map +1 -0
- package/dist/mcp/server.js +34 -11
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ npm run authflow -- --help
|
|
|
47
47
|
| `AUTHFLOW_ORIGIN_API_KEY` | `--origin-key` | `mint-header`, `probe`, `set-origin` (for its probe) |
|
|
48
48
|
| `AUTHFLOW_RESOURCE` / `AUTHFLOW_GATEWAY_URL` | `--gateway-url` / `--receipt` | curl hint on `mint-header` |
|
|
49
49
|
|
|
50
|
-
Issuer is the Origin Protocol issuer origin, with no trailing slash: staging is `https://staging.rails.authflow.ai`, local is typically `https://localhost:
|
|
50
|
+
Issuer is the Origin Protocol issuer origin, with no trailing slash: staging is `https://staging.rails.authflow.ai`, local is typically `https://localhost:7062`. For an already-registered resource it is the `issuer` field of the receipt. See [tenant quickstart §1](../../docs/onboarding/tenant-quickstart.md#environments-and-hostnames).
|
|
51
51
|
|
|
52
52
|
## Commands
|
|
53
53
|
|
|
@@ -237,6 +237,19 @@ Run this first when anything looks broken. It separates the two failures that ar
|
|
|
237
237
|
from the outside: a hostname that is not bound to the rail returns `404` on every path including
|
|
238
238
|
`/healthz`, which reads exactly like a broken endpoint. Exit code is non-zero on any failure.
|
|
239
239
|
|
|
240
|
+
`--client cursor|claude|chatgpt|all` additionally replays that MCP client's real OAuth registration
|
|
241
|
+
against the authorization server (the failure that presents as "this server does not work in
|
|
242
|
+
Cursor"). `--dry-run` is discovery only and does not POST DCR. Live DCR registrations are stamped
|
|
243
|
+
`authflow-cli doctor-connect` so a cleanup sweep can reclaim them.
|
|
244
|
+
|
|
245
|
+
A `400` from the rail's DCR endpoint is the interesting result: paste `error=` and
|
|
246
|
+
`error_description=` verbatim into a rail bug. Do not work around it client-side.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
authflow doctor --issuer https://staging.rails.authflow.ai --slug video --client cursor
|
|
250
|
+
authflow doctor --client all --dry-run
|
|
251
|
+
```
|
|
252
|
+
|
|
240
253
|
### `authflow register`
|
|
241
254
|
|
|
242
255
|
```bash
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay what real MCP clients send at the authorization server, from a tenant's point of view.
|
|
3
|
+
*
|
|
4
|
+
* This exists because of a real connect failure: tenant #1 could not connect from Cursor because
|
|
5
|
+
* our DCR endpoint rejected Cursor's actual redirect_uris (private-use `cursor://` scheme,
|
|
6
|
+
* omitted application_type, three callbacks at once). We only found out because they filed a bug.
|
|
7
|
+
* Nothing on the rail tested against what clients actually send. These checks do.
|
|
8
|
+
*
|
|
9
|
+
* DCR replay creates a real OAuth client row. Registrations are stamped with
|
|
10
|
+
* `client_name` prefix `authflow-cli doctor-connect` so a cleanup sweep can reclaim them; this
|
|
11
|
+
* harness does not delete them. Use `--dry-run` to skip POSTs.
|
|
12
|
+
*/
|
|
13
|
+
import { type ClientId } from "./client-fixtures.js";
|
|
14
|
+
import { type DoctorReport } from "./doctor.js";
|
|
15
|
+
export type ClientConformanceOptions = {
|
|
16
|
+
issuer: string;
|
|
17
|
+
clients: ClientId[];
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
slug?: string;
|
|
20
|
+
};
|
|
21
|
+
export type AuthorizationServerMetadata = {
|
|
22
|
+
issuer?: string;
|
|
23
|
+
registration_endpoint?: string;
|
|
24
|
+
client_id_metadata_document_supported?: boolean;
|
|
25
|
+
token_endpoint_auth_methods_supported?: unknown;
|
|
26
|
+
code_challenge_methods_supported?: unknown;
|
|
27
|
+
authorization_response_iss_parameter_supported?: unknown;
|
|
28
|
+
};
|
|
29
|
+
export declare function runDoctorAndConnect(issuer: string, options?: {
|
|
30
|
+
slug?: string;
|
|
31
|
+
client?: string;
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
}): Promise<DoctorReport>;
|
|
34
|
+
export declare function runClientConformance(options: ClientConformanceOptions): Promise<DoctorReport>;
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay what real MCP clients send at the authorization server, from a tenant's point of view.
|
|
3
|
+
*
|
|
4
|
+
* This exists because of a real connect failure: tenant #1 could not connect from Cursor because
|
|
5
|
+
* our DCR endpoint rejected Cursor's actual redirect_uris (private-use `cursor://` scheme,
|
|
6
|
+
* omitted application_type, three callbacks at once). We only found out because they filed a bug.
|
|
7
|
+
* Nothing on the rail tested against what clients actually send. These checks do.
|
|
8
|
+
*
|
|
9
|
+
* DCR replay creates a real OAuth client row. Registrations are stamped with
|
|
10
|
+
* `client_name` prefix `authflow-cli doctor-connect` so a cleanup sweep can reclaim them; this
|
|
11
|
+
* harness does not delete them. Use `--dry-run` to skip POSTs.
|
|
12
|
+
*/
|
|
13
|
+
import { chatgptCallbackCimdDocument, chatgptDcrVariants, chatgptStableCimdDocument, cursorDcrVariants, parseClientOption, probeClientName, } from "./client-fixtures.js";
|
|
14
|
+
import { checkGatewayChallenge, fetchOrNull, runDoctor, } from "./doctor.js";
|
|
15
|
+
import { checkGoogleRedirectUri } from "./google-redirect.js";
|
|
16
|
+
const metadataPath = "/.well-known/oauth-authorization-server";
|
|
17
|
+
const cimdNoneRemedy = 'Claude uses CIMD only when the AS advertises both client_id_metadata_document_supported: true ' +
|
|
18
|
+
'AND "none" in token_endpoint_auth_methods_supported. Missing "none" means Claude silently ' +
|
|
19
|
+
"ignores CIMD, with no error pointing at the cause.";
|
|
20
|
+
const cimdFetchAdvisoryDetail = "Advisory — not proven. This CLI cannot host a publicly-reachable Client ID Metadata Document " +
|
|
21
|
+
"(HTTPS URL with a path), which is what Claude sends as client_id. AS advertisement of CIMD " +
|
|
22
|
+
"support is checked separately. A pass here does not mean the rail fetches, caches, or " +
|
|
23
|
+
"validates a document.";
|
|
24
|
+
const cimdFetchAdvisoryRemedy = "To fully prove CIMD, host a document at an HTTPS URL whose path is non-empty, use that URL as " +
|
|
25
|
+
"client_id at /authorize, and confirm the AS fetches it and matches redirect_uris. Until then " +
|
|
26
|
+
"this check is advisory and does not fail the run.";
|
|
27
|
+
const chatgptCimdFetchAdvisoryDetail = `Advisory — not proven. ChatGPT's CIMD client_id is ${chatgptStableCimdDocument} when this AS ` +
|
|
28
|
+
`advertises RFC 9207 issuer identification, or ${chatgptCallbackCimdDocument} otherwise. This ` +
|
|
29
|
+
"CLI does not fetch those documents on the AS's behalf. A pass here does not mean the rail " +
|
|
30
|
+
"fetches, caches, or validates ChatGPT's document.";
|
|
31
|
+
const dcr400Footer = "A DCR 400 is the interesting result. Paste error= and error_description= verbatim into a rail " +
|
|
32
|
+
"bug; do not work around it client-side.";
|
|
33
|
+
export async function runDoctorAndConnect(issuer, options = {}) {
|
|
34
|
+
const report = await runDoctor(issuer, options.slug);
|
|
35
|
+
if (!options.client) {
|
|
36
|
+
return report;
|
|
37
|
+
}
|
|
38
|
+
const clients = parseClientOption(options.client);
|
|
39
|
+
const reachable = report.checks.find((c) => c.name === "issuer reachable");
|
|
40
|
+
if (reachable && !reachable.pass) {
|
|
41
|
+
return { ...report, clients, dryRun: options.dryRun };
|
|
42
|
+
}
|
|
43
|
+
const extra = await runClientConformance({
|
|
44
|
+
issuer: report.issuer,
|
|
45
|
+
clients,
|
|
46
|
+
dryRun: options.dryRun,
|
|
47
|
+
});
|
|
48
|
+
const google = await checkGoogleRedirectUri(report.issuer, {
|
|
49
|
+
slug: options.slug,
|
|
50
|
+
dryRun: options.dryRun,
|
|
51
|
+
});
|
|
52
|
+
const checks = [...report.checks, ...extra.checks, ...google];
|
|
53
|
+
return {
|
|
54
|
+
...report,
|
|
55
|
+
clients,
|
|
56
|
+
dryRun: extra.dryRun,
|
|
57
|
+
checks,
|
|
58
|
+
pass: checks.every((c) => c.pass),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function runClientConformance(options) {
|
|
62
|
+
const base = options.issuer.trim().replace(/\/+$/, "");
|
|
63
|
+
const dryRun = !!options.dryRun;
|
|
64
|
+
const checks = [];
|
|
65
|
+
const metadata = await fetchMetadata(base);
|
|
66
|
+
checks.push(metadata.check);
|
|
67
|
+
if (!metadata.document) {
|
|
68
|
+
return {
|
|
69
|
+
issuer: base,
|
|
70
|
+
slug: options.slug,
|
|
71
|
+
clients: options.clients,
|
|
72
|
+
dryRun,
|
|
73
|
+
pass: false,
|
|
74
|
+
checks,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const document = metadata.document;
|
|
78
|
+
const selected = new Set(options.clients);
|
|
79
|
+
checks.push(checkPkceS256(document));
|
|
80
|
+
checks.push(checkRfc9207Iss(document));
|
|
81
|
+
if (selected.has("cursor")) {
|
|
82
|
+
checks.push(checkRegistrationEndpoint(document, "cursor"));
|
|
83
|
+
}
|
|
84
|
+
if (selected.has("chatgpt")) {
|
|
85
|
+
checks.push(checkRegistrationEndpoint(document, "chatgpt"));
|
|
86
|
+
checks.push(...checkChatgptCimd(document));
|
|
87
|
+
}
|
|
88
|
+
if (selected.has("claude")) {
|
|
89
|
+
checks.push(...checkClaudeCimd(document));
|
|
90
|
+
}
|
|
91
|
+
if (!dryRun) {
|
|
92
|
+
const endpoint = resolveRegistrationEndpoint(base, document.registration_endpoint);
|
|
93
|
+
if (selected.has("cursor") && endpoint) {
|
|
94
|
+
for (const variant of cursorDcrVariants) {
|
|
95
|
+
checks.push(await replayDcr(endpoint, "cursor", variant));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (selected.has("chatgpt") && endpoint) {
|
|
99
|
+
for (const variant of chatgptDcrVariants) {
|
|
100
|
+
checks.push(await replayDcr(endpoint, "chatgpt", variant));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (options.slug) {
|
|
105
|
+
checks.push(await checkGatewayChallenge(base, options.slug));
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
issuer: base,
|
|
109
|
+
slug: options.slug,
|
|
110
|
+
clients: options.clients,
|
|
111
|
+
dryRun,
|
|
112
|
+
pass: checks.every((c) => c.pass),
|
|
113
|
+
checks,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function fetchMetadata(base) {
|
|
117
|
+
const url = `${base}${metadataPath}`;
|
|
118
|
+
const response = await fetchOrNull(url);
|
|
119
|
+
if (!response.ok && response.error) {
|
|
120
|
+
return {
|
|
121
|
+
check: {
|
|
122
|
+
name: "authorization server metadata",
|
|
123
|
+
pass: false,
|
|
124
|
+
detail: `${url} could not be reached: ${response.error}`,
|
|
125
|
+
remedy: "Check the hostname and DNS. MCP clients cannot start OAuth without this document.",
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (response.status !== 200) {
|
|
130
|
+
return {
|
|
131
|
+
check: {
|
|
132
|
+
name: "authorization server metadata",
|
|
133
|
+
pass: false,
|
|
134
|
+
detail: `${url} returned ${response.status ?? response.error}, expected 200.`,
|
|
135
|
+
remedy: "MCP clients discover DCR and CIMD from this document. File a rail bug.",
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let document;
|
|
140
|
+
try {
|
|
141
|
+
document = JSON.parse(response.body ?? "");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return {
|
|
145
|
+
check: {
|
|
146
|
+
name: "authorization server metadata",
|
|
147
|
+
pass: false,
|
|
148
|
+
detail: `${url} did not return JSON.`,
|
|
149
|
+
remedy: "File this against the rail.",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
check: {
|
|
155
|
+
name: "authorization server metadata",
|
|
156
|
+
pass: true,
|
|
157
|
+
detail: `${url} returned 200.`,
|
|
158
|
+
},
|
|
159
|
+
document,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function checkPkceS256(document) {
|
|
163
|
+
const methods = asStringArray(document.code_challenge_methods_supported);
|
|
164
|
+
if (methods.includes("S256")) {
|
|
165
|
+
return {
|
|
166
|
+
name: "PKCE S256",
|
|
167
|
+
pass: true,
|
|
168
|
+
detail: `code_challenge_methods_supported includes S256 (${methods.join(", ")}).`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
name: "PKCE S256",
|
|
173
|
+
pass: false,
|
|
174
|
+
detail: `code_challenge_methods_supported is [${methods.join(", ")}], expected to include S256.`,
|
|
175
|
+
remedy: "MCP clients require S256 PKCE. Advertise it in AS metadata.",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function checkRfc9207Iss(document) {
|
|
179
|
+
if (document.authorization_response_iss_parameter_supported === true) {
|
|
180
|
+
return {
|
|
181
|
+
name: "RFC 9207 iss parameter",
|
|
182
|
+
pass: true,
|
|
183
|
+
detail: "authorization_response_iss_parameter_supported is true.",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
name: "RFC 9207 iss parameter",
|
|
188
|
+
pass: false,
|
|
189
|
+
detail: `authorization_response_iss_parameter_supported is ${JSON.stringify(document.authorization_response_iss_parameter_supported)}, expected true.`,
|
|
190
|
+
remedy: "Advertise authorization_response_iss_parameter_supported: true and return iss on every " +
|
|
191
|
+
"authorization response. ChatGPT uses this flag to choose connector_platform_oauth_redirect " +
|
|
192
|
+
"versus connector/oauth/{callback_id}; a regression silently changes the redirect URI.",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function checkRegistrationEndpoint(document, client) {
|
|
196
|
+
const endpoint = (document.registration_endpoint ?? "").trim();
|
|
197
|
+
const name = `${client}: registration_endpoint`;
|
|
198
|
+
if (endpoint) {
|
|
199
|
+
return {
|
|
200
|
+
name,
|
|
201
|
+
pass: true,
|
|
202
|
+
detail: `registration_endpoint is ${endpoint}.`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const remedy = client === "cursor"
|
|
206
|
+
? "Cursor is DCR-only and has no CIMD timeline. Without registration_endpoint in AS metadata, Cursor cannot connect at all."
|
|
207
|
+
: "ChatGPT prefers CIMD when advertised, but still uses DCR when the plugin builder chooses it or CIMD is unavailable. Without registration_endpoint that fallback cannot register.";
|
|
208
|
+
return {
|
|
209
|
+
name,
|
|
210
|
+
pass: false,
|
|
211
|
+
detail: "AS metadata has no registration_endpoint.",
|
|
212
|
+
remedy,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function checkClaudeCimd(document) {
|
|
216
|
+
const advertised = document.client_id_metadata_document_supported === true;
|
|
217
|
+
const methods = asStringArray(document.token_endpoint_auth_methods_supported);
|
|
218
|
+
const hasNone = methods.includes("none");
|
|
219
|
+
const advertisement = advertised
|
|
220
|
+
? {
|
|
221
|
+
name: "claude: CIMD advertisement",
|
|
222
|
+
pass: true,
|
|
223
|
+
detail: "client_id_metadata_document_supported is true.",
|
|
224
|
+
}
|
|
225
|
+
: {
|
|
226
|
+
name: "claude: CIMD advertisement",
|
|
227
|
+
pass: false,
|
|
228
|
+
detail: `client_id_metadata_document_supported is ${JSON.stringify(document.client_id_metadata_document_supported)}, expected true.`,
|
|
229
|
+
remedy: cimdNoneRemedy,
|
|
230
|
+
};
|
|
231
|
+
const noneMethod = hasNone
|
|
232
|
+
? {
|
|
233
|
+
name: "claude: token endpoint auth none",
|
|
234
|
+
pass: true,
|
|
235
|
+
detail: `token_endpoint_auth_methods_supported includes "none" (${methods.join(", ")}).`,
|
|
236
|
+
}
|
|
237
|
+
: {
|
|
238
|
+
name: "claude: token endpoint auth none",
|
|
239
|
+
pass: false,
|
|
240
|
+
detail: `token_endpoint_auth_methods_supported is [${methods.join(", ")}], expected to include "none".`,
|
|
241
|
+
remedy: cimdNoneRemedy,
|
|
242
|
+
};
|
|
243
|
+
const fetchValidate = {
|
|
244
|
+
name: "claude: CIMD fetch-and-validate",
|
|
245
|
+
pass: true,
|
|
246
|
+
detail: cimdFetchAdvisoryDetail,
|
|
247
|
+
remedy: cimdFetchAdvisoryRemedy,
|
|
248
|
+
};
|
|
249
|
+
return [advertisement, noneMethod, fetchValidate];
|
|
250
|
+
}
|
|
251
|
+
function checkChatgptCimd(document) {
|
|
252
|
+
const advertised = document.client_id_metadata_document_supported === true;
|
|
253
|
+
const advertisement = advertised
|
|
254
|
+
? {
|
|
255
|
+
name: "chatgpt: CIMD advertisement",
|
|
256
|
+
pass: true,
|
|
257
|
+
detail: "client_id_metadata_document_supported is true. ChatGPT prefers CIMD when advertised " +
|
|
258
|
+
`(stable document ${chatgptStableCimdDocument} if RFC 9207 iss is also true).`,
|
|
259
|
+
}
|
|
260
|
+
: {
|
|
261
|
+
name: "chatgpt: CIMD advertisement",
|
|
262
|
+
pass: false,
|
|
263
|
+
detail: `client_id_metadata_document_supported is ${JSON.stringify(document.client_id_metadata_document_supported)}, expected true.`,
|
|
264
|
+
remedy: "ChatGPT prefers CIMD when the AS advertises client_id_metadata_document_supported: true. " +
|
|
265
|
+
"Without it ChatGPT falls back to DCR (or cannot register if registration_endpoint is also missing).",
|
|
266
|
+
};
|
|
267
|
+
const fetchValidate = {
|
|
268
|
+
name: "chatgpt: CIMD fetch-and-validate",
|
|
269
|
+
pass: true,
|
|
270
|
+
detail: chatgptCimdFetchAdvisoryDetail,
|
|
271
|
+
remedy: cimdFetchAdvisoryRemedy,
|
|
272
|
+
};
|
|
273
|
+
return [advertisement, fetchValidate];
|
|
274
|
+
}
|
|
275
|
+
async function replayDcr(endpoint, client, variant) {
|
|
276
|
+
const sent = variant.payload.redirect_uris;
|
|
277
|
+
const body = {
|
|
278
|
+
...variant.payload,
|
|
279
|
+
client_name: probeClientName(client, variant.id),
|
|
280
|
+
};
|
|
281
|
+
const response = await fetchOrNull(endpoint, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
284
|
+
body: JSON.stringify(body),
|
|
285
|
+
});
|
|
286
|
+
if (!response.ok && response.error) {
|
|
287
|
+
return {
|
|
288
|
+
name: variant.checkName,
|
|
289
|
+
pass: false,
|
|
290
|
+
detail: `POST ${endpoint} could not be reached: ${response.error}`,
|
|
291
|
+
remedy: "The registration_endpoint was advertised but did not respond. File a rail bug.",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (response.status === 400) {
|
|
295
|
+
return {
|
|
296
|
+
name: variant.checkName,
|
|
297
|
+
pass: false,
|
|
298
|
+
detail: `DCR 400: ${formatDcrError(response.body)}`,
|
|
299
|
+
remedy: dcr400Footer,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (response.status !== 201) {
|
|
303
|
+
return {
|
|
304
|
+
name: variant.checkName,
|
|
305
|
+
pass: false,
|
|
306
|
+
detail: `POST ${endpoint} returned ${response.status ?? response.error}, expected 201. ${formatDcrError(response.body)}`,
|
|
307
|
+
remedy: "DCR must succeed with 201 for this client's real payload. File a rail bug.",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
let echoed;
|
|
311
|
+
try {
|
|
312
|
+
echoed = JSON.parse(response.body ?? "");
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return {
|
|
316
|
+
name: variant.checkName,
|
|
317
|
+
pass: false,
|
|
318
|
+
detail: "DCR returned 201 but the body was not JSON.",
|
|
319
|
+
remedy: "File this against the rail.",
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const dropped = missingRedirectUris(sent, echoed.redirect_uris);
|
|
323
|
+
if (dropped.length > 0) {
|
|
324
|
+
return {
|
|
325
|
+
name: variant.checkName,
|
|
326
|
+
pass: false,
|
|
327
|
+
detail: `DCR returned 201 but dropped redirect_uris: ${dropped.join(", ")}. ` +
|
|
328
|
+
`Sent [${sent.join(", ")}]; echoed ${JSON.stringify(echoed.redirect_uris)}.`,
|
|
329
|
+
remedy: "A silently dropped callback fails later at /authorize with a confusing redirect_uri " +
|
|
330
|
+
"mismatch. Echo every registered URI.",
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const clientId = typeof echoed.client_id === "string" ? echoed.client_id : "(none)";
|
|
334
|
+
return {
|
|
335
|
+
name: variant.checkName,
|
|
336
|
+
pass: true,
|
|
337
|
+
detail: `DCR 201; client_id=${clientId}; redirect_uris echoed in full. Stamped client_name for cleanup sweep.`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function missingRedirectUris(sent, echoed) {
|
|
341
|
+
if (!Array.isArray(echoed)) {
|
|
342
|
+
return [...sent];
|
|
343
|
+
}
|
|
344
|
+
const got = new Set(echoed.map((value) => String(value)));
|
|
345
|
+
return sent.filter((uri) => !got.has(uri));
|
|
346
|
+
}
|
|
347
|
+
function formatDcrError(raw) {
|
|
348
|
+
if (!raw) {
|
|
349
|
+
return "error=(empty) error_description=(empty)";
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const body = JSON.parse(raw);
|
|
353
|
+
const error = body.error !== undefined ? String(body.error) : "(none)";
|
|
354
|
+
const description = body.error_description !== undefined ? String(body.error_description) : "(none)";
|
|
355
|
+
return `error=${error} error_description=${description}`;
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return raw.slice(0, 300);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function resolveRegistrationEndpoint(base, value) {
|
|
362
|
+
const endpoint = (value ?? "").trim();
|
|
363
|
+
if (!endpoint) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
return new URL(endpoint, `${base}/`).href;
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function asStringArray(value) {
|
|
374
|
+
if (!Array.isArray(value)) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
return value.map((entry) => String(entry));
|
|
378
|
+
}
|
|
379
|
+
//# sourceMappingURL=client-conformance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-conformance.js","sourceRoot":"","sources":["../src/client-conformance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical OAuth registration payloads as the named MCP clients actually send them.
|
|
3
|
+
*
|
|
4
|
+
* A wrong fixture is worse than no test: it would certify the rail against a made-up client.
|
|
5
|
+
* Keep `.cursor/skills/mcp-oauth-paywall/references/client-compatibility.md` in sync with this file.
|
|
6
|
+
*/
|
|
7
|
+
export declare const clientIds: readonly ["cursor", "claude", "chatgpt"];
|
|
8
|
+
export type ClientId = (typeof clientIds)[number];
|
|
9
|
+
export type DcrPayload = {
|
|
10
|
+
redirect_uris: string[];
|
|
11
|
+
token_endpoint_auth_method: string;
|
|
12
|
+
grant_types: string[];
|
|
13
|
+
response_types: string[];
|
|
14
|
+
application_type?: string;
|
|
15
|
+
};
|
|
16
|
+
export type DcrVariant = {
|
|
17
|
+
id: string;
|
|
18
|
+
/** Stable check name; included in the doctor report. */
|
|
19
|
+
checkName: string;
|
|
20
|
+
payload: DcrPayload;
|
|
21
|
+
};
|
|
22
|
+
export declare const dcrProbeClientNamePrefix = "authflow-cli doctor-connect";
|
|
23
|
+
/**
|
|
24
|
+
* Cursor is DCR-only. The three-URI set is what it has sent since ~May 2026
|
|
25
|
+
* (https://forum.cursor.com/t/mcp-server-regression-mcp-oauth-registration-fails/159910,
|
|
26
|
+
* https://cursor.com/docs/mcp.md). `application_type` is deliberately absent — Cursor omits it
|
|
27
|
+
* (https://forum.cursor.com/t/cursor-does-not-send-application-type-native-when-registering-mcp-oauth-clients/136907).
|
|
28
|
+
*/
|
|
29
|
+
export declare const cursorCanonicalDcr: DcrVariant;
|
|
30
|
+
/** Same callbacks with `application_type: "web"` stated explicitly — a shape Cursor has also sent. */
|
|
31
|
+
export declare const cursorApplicationTypeWebDcr: DcrVariant;
|
|
32
|
+
/** Older Cursor registrations sent only the private-use callback. */
|
|
33
|
+
export declare const cursorSchemeOnlyDcr: DcrVariant;
|
|
34
|
+
export declare const cursorDcrVariants: DcrVariant[];
|
|
35
|
+
/**
|
|
36
|
+
* Obviously-fake callback id. ChatGPT mints a real one per connector; we cannot capture it.
|
|
37
|
+
* Used only to pin the documented URL shape.
|
|
38
|
+
*/
|
|
39
|
+
export declare const chatgptCallbackIdPlaceholder = "CALLBACK_ID_PLACEHOLDER";
|
|
40
|
+
export declare const chatgptStableRedirectUri = "https://chatgpt.com/connector_platform_oauth_redirect";
|
|
41
|
+
export declare const chatgptCallbackRedirectUri = "https://chatgpt.com/connector/oauth/CALLBACK_ID_PLACEHOLDER";
|
|
42
|
+
export declare const chatgptStableCimdDocument = "https://chatgpt.com/oauth/client.json";
|
|
43
|
+
export declare const chatgptCallbackCimdDocument = "https://chatgpt.com/oauth/CALLBACK_ID_PLACEHOLDER/client.json";
|
|
44
|
+
/**
|
|
45
|
+
* Primary ChatGPT DCR shape for this rail. Our AS advertises
|
|
46
|
+
* `authorization_response_iss_parameter_supported: true` and uses the same issuer in PRM, so
|
|
47
|
+
* ChatGPT selects this stable callback
|
|
48
|
+
* (https://developers.openai.com/plugins/build/auth, https://developers.openai.com/plugins/changelog
|
|
49
|
+
* 2026-08-21). A regression in that metadata flag silently switches ChatGPT to the callback-ID URI
|
|
50
|
+
* below. The rest of the body is the RFC 7591 public-client shape, not a captured ChatGPT POST.
|
|
51
|
+
*
|
|
52
|
+
* The Apps SDK page still calls this URI "legacy" for already-published apps and names
|
|
53
|
+
* `connector/oauth/{callback_id}` as the production allowlist entry
|
|
54
|
+
* (https://developers.openai.com/apps-sdk/build/auth). Both pages agree both URIs exist; they
|
|
55
|
+
* disagree on which is current. We replay both.
|
|
56
|
+
*/
|
|
57
|
+
export declare const chatgptStableRedirectDcr: DcrVariant;
|
|
58
|
+
/**
|
|
59
|
+
* Callback-ID DCR shape. `{callback_id}` is a placeholder, not a captured id. ChatGPT sends this
|
|
60
|
+
* when the AS does not meet RFC 9207 issuer identification, or when the Apps SDK path is in play.
|
|
61
|
+
*/
|
|
62
|
+
export declare const chatgptCallbackIdDcr: DcrVariant;
|
|
63
|
+
export declare const chatgptDcrVariants: DcrVariant[];
|
|
64
|
+
export declare function isClientId(value: string): value is ClientId;
|
|
65
|
+
export declare function parseClientOption(value: string): ClientId[];
|
|
66
|
+
export declare function dcrVariantsFor(client: ClientId): DcrVariant[];
|
|
67
|
+
export declare function probeClientName(client: ClientId, variantId: string): string;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical OAuth registration payloads as the named MCP clients actually send them.
|
|
3
|
+
*
|
|
4
|
+
* A wrong fixture is worse than no test: it would certify the rail against a made-up client.
|
|
5
|
+
* Keep `.cursor/skills/mcp-oauth-paywall/references/client-compatibility.md` in sync with this file.
|
|
6
|
+
*/
|
|
7
|
+
export const clientIds = ["cursor", "claude", "chatgpt"];
|
|
8
|
+
export const dcrProbeClientNamePrefix = "authflow-cli doctor-connect";
|
|
9
|
+
/**
|
|
10
|
+
* Cursor is DCR-only. The three-URI set is what it has sent since ~May 2026
|
|
11
|
+
* (https://forum.cursor.com/t/mcp-server-regression-mcp-oauth-registration-fails/159910,
|
|
12
|
+
* https://cursor.com/docs/mcp.md). `application_type` is deliberately absent — Cursor omits it
|
|
13
|
+
* (https://forum.cursor.com/t/cursor-does-not-send-application-type-native-when-registering-mcp-oauth-clients/136907).
|
|
14
|
+
*/
|
|
15
|
+
export const cursorCanonicalDcr = {
|
|
16
|
+
id: "canonical",
|
|
17
|
+
checkName: "cursor: DCR canonical",
|
|
18
|
+
payload: {
|
|
19
|
+
redirect_uris: [
|
|
20
|
+
"cursor://anysphere.cursor-mcp/oauth/callback",
|
|
21
|
+
"https://www.cursor.com/agents/mcp/oauth/callback",
|
|
22
|
+
"http://localhost:8787/callback",
|
|
23
|
+
],
|
|
24
|
+
token_endpoint_auth_method: "none",
|
|
25
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
26
|
+
response_types: ["code"],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/** Same callbacks with `application_type: "web"` stated explicitly — a shape Cursor has also sent. */
|
|
30
|
+
export const cursorApplicationTypeWebDcr = {
|
|
31
|
+
id: "application_type-web",
|
|
32
|
+
checkName: "cursor: DCR application_type web",
|
|
33
|
+
payload: {
|
|
34
|
+
...cursorCanonicalDcr.payload,
|
|
35
|
+
redirect_uris: [...cursorCanonicalDcr.payload.redirect_uris],
|
|
36
|
+
grant_types: [...cursorCanonicalDcr.payload.grant_types],
|
|
37
|
+
response_types: [...cursorCanonicalDcr.payload.response_types],
|
|
38
|
+
application_type: "web",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
/** Older Cursor registrations sent only the private-use callback. */
|
|
42
|
+
export const cursorSchemeOnlyDcr = {
|
|
43
|
+
id: "cursor-scheme-only",
|
|
44
|
+
checkName: "cursor: DCR cursor-scheme only",
|
|
45
|
+
payload: {
|
|
46
|
+
redirect_uris: ["cursor://anysphere.cursor-mcp/oauth/callback"],
|
|
47
|
+
token_endpoint_auth_method: "none",
|
|
48
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
49
|
+
response_types: ["code"],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
export const cursorDcrVariants = [
|
|
53
|
+
cursorCanonicalDcr,
|
|
54
|
+
cursorApplicationTypeWebDcr,
|
|
55
|
+
cursorSchemeOnlyDcr,
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Obviously-fake callback id. ChatGPT mints a real one per connector; we cannot capture it.
|
|
59
|
+
* Used only to pin the documented URL shape.
|
|
60
|
+
*/
|
|
61
|
+
export const chatgptCallbackIdPlaceholder = "CALLBACK_ID_PLACEHOLDER";
|
|
62
|
+
export const chatgptStableRedirectUri = "https://chatgpt.com/connector_platform_oauth_redirect";
|
|
63
|
+
export const chatgptCallbackRedirectUri = `https://chatgpt.com/connector/oauth/${chatgptCallbackIdPlaceholder}`;
|
|
64
|
+
export const chatgptStableCimdDocument = "https://chatgpt.com/oauth/client.json";
|
|
65
|
+
export const chatgptCallbackCimdDocument = `https://chatgpt.com/oauth/${chatgptCallbackIdPlaceholder}/client.json`;
|
|
66
|
+
/**
|
|
67
|
+
* Primary ChatGPT DCR shape for this rail. Our AS advertises
|
|
68
|
+
* `authorization_response_iss_parameter_supported: true` and uses the same issuer in PRM, so
|
|
69
|
+
* ChatGPT selects this stable callback
|
|
70
|
+
* (https://developers.openai.com/plugins/build/auth, https://developers.openai.com/plugins/changelog
|
|
71
|
+
* 2026-08-21). A regression in that metadata flag silently switches ChatGPT to the callback-ID URI
|
|
72
|
+
* below. The rest of the body is the RFC 7591 public-client shape, not a captured ChatGPT POST.
|
|
73
|
+
*
|
|
74
|
+
* The Apps SDK page still calls this URI "legacy" for already-published apps and names
|
|
75
|
+
* `connector/oauth/{callback_id}` as the production allowlist entry
|
|
76
|
+
* (https://developers.openai.com/apps-sdk/build/auth). Both pages agree both URIs exist; they
|
|
77
|
+
* disagree on which is current. We replay both.
|
|
78
|
+
*/
|
|
79
|
+
export const chatgptStableRedirectDcr = {
|
|
80
|
+
id: "stable-redirect",
|
|
81
|
+
checkName: "chatgpt: DCR stable redirect",
|
|
82
|
+
payload: {
|
|
83
|
+
redirect_uris: [chatgptStableRedirectUri],
|
|
84
|
+
token_endpoint_auth_method: "none",
|
|
85
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
86
|
+
response_types: ["code"],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Callback-ID DCR shape. `{callback_id}` is a placeholder, not a captured id. ChatGPT sends this
|
|
91
|
+
* when the AS does not meet RFC 9207 issuer identification, or when the Apps SDK path is in play.
|
|
92
|
+
*/
|
|
93
|
+
export const chatgptCallbackIdDcr = {
|
|
94
|
+
id: "callback-id",
|
|
95
|
+
checkName: "chatgpt: DCR callback-id redirect",
|
|
96
|
+
payload: {
|
|
97
|
+
redirect_uris: [chatgptCallbackRedirectUri],
|
|
98
|
+
token_endpoint_auth_method: "none",
|
|
99
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
100
|
+
response_types: ["code"],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export const chatgptDcrVariants = [chatgptStableRedirectDcr, chatgptCallbackIdDcr];
|
|
104
|
+
export function isClientId(value) {
|
|
105
|
+
return clientIds.includes(value);
|
|
106
|
+
}
|
|
107
|
+
export function parseClientOption(value) {
|
|
108
|
+
const key = value.trim().toLowerCase();
|
|
109
|
+
if (key === "all") {
|
|
110
|
+
return [...clientIds];
|
|
111
|
+
}
|
|
112
|
+
if (isClientId(key)) {
|
|
113
|
+
return [key];
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Unknown --client '${value}'. Use cursor, claude, chatgpt, or all.`);
|
|
116
|
+
}
|
|
117
|
+
export function dcrVariantsFor(client) {
|
|
118
|
+
switch (client) {
|
|
119
|
+
case "cursor":
|
|
120
|
+
return cursorDcrVariants;
|
|
121
|
+
case "chatgpt":
|
|
122
|
+
return chatgptDcrVariants;
|
|
123
|
+
case "claude":
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function probeClientName(client, variantId) {
|
|
128
|
+
return `${dcrProbeClientNamePrefix} ${client} ${variantId}`;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=client-fixtures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-fixtures.js","sourceRoot":"","sources":["../src/client-fixtures.ts"],"names":[],"mappings":"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"}
|