@mangoc/dsh-sub2api-sync 0.3.0 → 0.3.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/lib/index.js +31 -47
- package/lib/oauth.d.ts +14 -13
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1405,8 +1405,8 @@ function absoluteHttpUrl(value) {
|
|
|
1405
1405
|
}
|
|
1406
1406
|
}
|
|
1407
1407
|
/** The provider issuer path under the instance root; the sub2api provider
|
|
1408
|
-
* issues `issuer = {instance}/api/v1/oauth` (no trailing slash).
|
|
1409
|
-
*
|
|
1408
|
+
* issues `issuer = {instance}/api/v1/oauth` (no trailing slash). The discovery
|
|
1409
|
+
* URL is derived from this value, never guessed. */
|
|
1410
1410
|
const PROVIDER_ISSUER_PATH = "/api/v1/oauth";
|
|
1411
1411
|
async function discoverOAuthEndpoints(instance, fetchImpl = globalThis.fetch, now = Date.now()) {
|
|
1412
1412
|
const key = instance.trim().replace(/\/+$/, "").toLowerCase();
|
|
@@ -1417,54 +1417,13 @@ async function discoverOAuthEndpoints(instance, fetchImpl = globalThis.fetch, no
|
|
|
1417
1417
|
}
|
|
1418
1418
|
const base = instance.trim().replace(/\/+$/, "");
|
|
1419
1419
|
const expectedIssuer = `${base}${PROVIDER_ISSUER_PATH}`;
|
|
1420
|
-
const
|
|
1421
|
-
{
|
|
1422
|
-
url: `${base}/.well-known/oauth-authorization-server${PROVIDER_ISSUER_PATH}`,
|
|
1423
|
-
requireIssuerField: true
|
|
1424
|
-
},
|
|
1425
|
-
{
|
|
1426
|
-
url: `${base}/.well-known/oauth-authorization-server`,
|
|
1427
|
-
requireIssuerField: false
|
|
1428
|
-
},
|
|
1429
|
-
{
|
|
1430
|
-
url: `${expectedIssuer}/.well-known`,
|
|
1431
|
-
requireIssuerField: false
|
|
1432
|
-
}
|
|
1433
|
-
];
|
|
1420
|
+
const metadataUrl = `${base}/.well-known/oauth-authorization-server${PROVIDER_ISSUER_PATH}`;
|
|
1434
1421
|
let endpoints;
|
|
1435
1422
|
let lastError = "";
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (!response.ok) {
|
|
1439
|
-
lastError = `HTTP ${response.status} at ${candidate.url}`;
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
const payload = await response.json();
|
|
1443
|
-
const documentIssuer = typeof payload.issuer === "string" ? payload.issuer : undefined;
|
|
1444
|
-
if (documentIssuer === undefined) {
|
|
1445
|
-
if (candidate.requireIssuerField) {
|
|
1446
|
-
lastError = `metadata at ${candidate.url} omits the required issuer field`;
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
} else if (documentIssuer !== expectedIssuer) {
|
|
1450
|
-
lastError = `issuer mismatch at ${candidate.url}: ${documentIssuer} (expected ${expectedIssuer})`;
|
|
1451
|
-
continue;
|
|
1452
|
-
}
|
|
1453
|
-
const authorizationEndpoint = absoluteHttpUrl(payload.authorization_endpoint);
|
|
1454
|
-
const tokenEndpoint = absoluteHttpUrl(payload.token_endpoint);
|
|
1455
|
-
if (authorizationEndpoint === undefined || tokenEndpoint === undefined) {
|
|
1456
|
-
lastError = `incomplete metadata at ${candidate.url}`;
|
|
1457
|
-
continue;
|
|
1458
|
-
}
|
|
1459
|
-
const revocationEndpoint = absoluteHttpUrl(payload.revocation_endpoint);
|
|
1460
|
-
endpoints = {
|
|
1461
|
-
authorizationEndpoint,
|
|
1462
|
-
tokenEndpoint,
|
|
1463
|
-
...revocationEndpoint !== undefined ? { revocationEndpoint } : {}
|
|
1464
|
-
};
|
|
1465
|
-
break;
|
|
1423
|
+
try {
|
|
1424
|
+
endpoints = await fetchStrictMetadata(metadataUrl, expectedIssuer, fetchImpl);
|
|
1466
1425
|
} catch (error) {
|
|
1467
|
-
lastError =
|
|
1426
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1468
1427
|
}
|
|
1469
1428
|
discoveryCache.set(key, {
|
|
1470
1429
|
endpoints,
|
|
@@ -1473,6 +1432,31 @@ async function discoverOAuthEndpoints(instance, fetchImpl = globalThis.fetch, no
|
|
|
1473
1432
|
if (endpoints === undefined) throw new OAuthError("discovery_failed", `sub2api OAuth 发现失败:${lastError}`);
|
|
1474
1433
|
return endpoints;
|
|
1475
1434
|
}
|
|
1435
|
+
/** One strict RFC 8414 §3 probe: fetch the metadata document at the derived
|
|
1436
|
+
* URL and validate it, or throw a plain Error carrying the failure reason. */
|
|
1437
|
+
async function fetchStrictMetadata(metadataUrl, expectedIssuer, fetchImpl) {
|
|
1438
|
+
const response = await fetchImpl(metadataUrl, { headers: { accept: "application/json" } });
|
|
1439
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} at ${metadataUrl}`);
|
|
1440
|
+
let payload;
|
|
1441
|
+
try {
|
|
1442
|
+
payload = await response.json();
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1445
|
+
throw new Error(`metadata at ${metadataUrl} is not JSON: ${reason}`);
|
|
1446
|
+
}
|
|
1447
|
+
const documentIssuer = typeof payload.issuer === "string" ? payload.issuer : undefined;
|
|
1448
|
+
if (documentIssuer === undefined) throw new Error(`metadata at ${metadataUrl} omits the required issuer field`);
|
|
1449
|
+
if (documentIssuer !== expectedIssuer) throw new Error(`issuer mismatch at ${metadataUrl}: ${documentIssuer} (expected ${expectedIssuer})`);
|
|
1450
|
+
const authorizationEndpoint = absoluteHttpUrl(payload.authorization_endpoint);
|
|
1451
|
+
const tokenEndpoint = absoluteHttpUrl(payload.token_endpoint);
|
|
1452
|
+
if (authorizationEndpoint === undefined || tokenEndpoint === undefined) throw new Error(`incomplete metadata at ${metadataUrl}`);
|
|
1453
|
+
const revocationEndpoint = absoluteHttpUrl(payload.revocation_endpoint);
|
|
1454
|
+
return {
|
|
1455
|
+
authorizationEndpoint,
|
|
1456
|
+
tokenEndpoint,
|
|
1457
|
+
...revocationEndpoint !== undefined ? { revocationEndpoint } : {}
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1476
1460
|
function resetDiscoveryCache() {
|
|
1477
1461
|
discoveryCache.clear();
|
|
1478
1462
|
}
|
package/lib/oauth.d.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* Implements the plugin-side half of the 2026-10-01 design:
|
|
5
5
|
* - endpoint discovery per RFC 8414: the metadata URL is derived by inserting
|
|
6
6
|
* `/.well-known/oauth-authorization-server` between the host and the
|
|
7
|
-
* provider issuer path (§3), the document's `issuer` field must match
|
|
8
|
-
* expected provider issuer exactly (§3.3)
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* provider issuer path (§3), and the document's `issuer` field must match
|
|
8
|
+
* the expected provider issuer exactly (§3.3) — a single strict location
|
|
9
|
+
* with no fallbacks (the host-root endpoint belongs to the upstream MCP
|
|
10
|
+
* server and the legacy alias is gone); discovery failure means OAuth is
|
|
11
|
+
* unavailable;
|
|
11
12
|
* - PKCE: 64-char base64url verifier, S256 challenge only (never `plain`);
|
|
12
13
|
* - one-shot authorization attempts: `state` is single-use and expires with
|
|
13
14
|
* the attempt, the verifier never leaves the host process, and the final
|
|
@@ -38,15 +39,15 @@ export interface OAuthEndpoints {
|
|
|
38
39
|
revocationEndpoint?: string;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
* Resolve the provider metadata per RFC 8414.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* instead of guessing URLs.
|
|
42
|
+
* Resolve the provider metadata per RFC 8414. Exactly one location is probed:
|
|
43
|
+
* the strict §3 derivation from the provider issuer, and the document must
|
|
44
|
+
* carry an `issuer` field (REQUIRED per §3.2) identical to the expected
|
|
45
|
+
* provider issuer (§3.3). The former host-root and legacy-alias fallbacks were
|
|
46
|
+
* removed by decision (2026-11-10): the host-root endpoint on real deployments
|
|
47
|
+
* belongs to the upstream MCP authorization server (live probes answer "MCP is
|
|
48
|
+
* disabled" or name a different issuer), and the compatibility alias is no
|
|
49
|
+
* longer deployed. A failed probe is a loud failure — the caller hides the
|
|
50
|
+
* OAuth channel instead of guessing URLs.
|
|
50
51
|
*/
|
|
51
52
|
export declare function discoverOAuthEndpoints(instance: string, fetchImpl?: typeof fetch, now?: number): Promise<OAuthEndpoints>;
|
|
52
53
|
/** Test seam: drop the process-wide discovery cache. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mangoc/dsh-sub2api-sync",
|
|
3
3
|
"description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (password / OAuth), one provider per relay key.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shuai-S",
|
|
7
7
|
"homepage": "https://github.com/Shuai-S/dsh-sub2api-sync#readme",
|