@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 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). Every
1409
- * discovery URL below is derived from this value, never guessed. */
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 candidates = [
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
- for (const candidate of candidates) try {
1437
- const response = await fetchImpl(candidate.url, { headers: { accept: "application/json" } });
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 = `${candidate.url}: ${error instanceof Error ? error.message : String(error)}`;
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 the
8
- * expected provider issuer exactly (§3.3), and the deployed host-root
9
- * registration plus the `/api/v1/oauth/.well-known` compatibility alias
10
- * serve as gated fallbacks — discovery failure means OAuth is unavailable;
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. The strict §3 location (the
42
- * metadata URL derived from the provider issuer) is tried first and must echo
43
- * the expected issuer (§3.3 — issuer field required); the deployed host-root
44
- * registration and the versioned compatibility alias follow as gated
45
- * fallbacks. Issuer validation is what keeps a complete document served by a
46
- * *different* authorization server (e.g. the upstream MCP issuer at the host
47
- * root after route B) from being adopted just because it parses. A failed or
48
- * incomplete hunt is a loud failure — the caller hides the OAuth channel
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.0",
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",