skybridge 1.3.1 → 2.0.0-alpha.2797af0

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.
@@ -17,9 +17,11 @@ export type CustomProviderOptions = {
17
17
  * stays the IdP (the token's real `iss`). */
18
18
  serverUrl?: string;
19
19
  /** Advertise this URL as the authorization server (served AS metadata `issuer`
20
- * and PRM `authorization_servers`) while `verify.issuer` stays the IdP's `iss`.
21
- * Use when DCR/authorize/token live at a different URL than the token issuer
22
- * (e.g. Descope's agentic URL vs. its base-project issuer). `serverUrl` wins if
20
+ * and PRM `authorization_servers`). Its own discovery document supplies the
21
+ * advertised endpoints, DCR registration, scopes and the `iss` tokens are
22
+ * verified against; only `jwks_uri` still comes from `issuer`'s document. Use
23
+ * when DCR/authorize/token live at a different URL than the discovery issuer
24
+ * (e.g. Descope's agentic URL vs. its base-project URL). `serverUrl` wins if
23
25
  * both are set. */
24
26
  authorizationServer?: string;
25
27
  scopes?: string[];
@@ -11,11 +11,20 @@ export async function customProvider(opts) {
11
11
  // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)
12
12
  // always comes from validated discovery.
13
13
  const { issuer: _issuer, jwks_uri: _jwks, ...overrides } = opts.metadataOverrides ?? {};
14
- const base = { ...discovered, ...overrides };
14
+ let advertised = discovered;
15
+ if (opts.authorizationServer) {
16
+ try {
17
+ advertised = await discoverAuthorizationServer(opts.authorizationServer);
18
+ }
19
+ catch (err) {
20
+ console.warn(`OAuth discovery failed for authorizationServer ${opts.authorizationServer}; advertising ${opts.issuer} metadata instead: ${err instanceof Error ? err.message : String(err)}`);
21
+ }
22
+ }
23
+ const base = { ...advertised, ...overrides };
15
24
  const scopesSupported = opts.scopes ?? base.scopes_supported;
16
- // serverUrl (skybridge-as-AS) or authorizationServer (a distinct AS URL)
17
- // override the advertised issuer (and keep the served scopes in sync). The
18
- // verifier still trusts the IdP's `iss`. serverUrl wins when both are set.
25
+ // serverUrl (skybridge-as-AS) overrides the advertised issuer, keeping the
26
+ // IdP as the token issuer. authorizationServer re-asserts its own URL so a
27
+ // failed AS discovery still points clients at the AS. serverUrl wins.
19
28
  const advertisedIssuer = opts.serverUrl?.replace(/\/$/, "") ??
20
29
  opts.authorizationServer?.replace(/\/$/, "");
21
30
  const oauthMetadata = advertisedIssuer
@@ -29,9 +38,9 @@ export async function customProvider(opts) {
29
38
  baseUrl: opts.baseUrl,
30
39
  oauthMetadata,
31
40
  verify: {
32
- issuer: discovered.issuer,
41
+ issuer: advertised.issuer,
33
42
  audience: opts.audience,
34
- jwksUri: discovered.jwks_uri,
43
+ jwksUri: advertised.jwks_uri ?? discovered.jwks_uri,
35
44
  },
36
45
  scopesSupported,
37
46
  requiredScopes: opts.requiredScopes,
@@ -1 +1 @@
1
- {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AA8BzB,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,gFAAgF;IAChF,kFAAkF;IAClF,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,yCAAyC;IACzC,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAgC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC9D,MAAM,IAAI,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAE7D,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,MAAM,gBAAgB,GACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAuB,gBAAgB;QACxD,CAAC,CAAC;YACE,GAAG,IAAI;YACP,MAAM,EAAE,gBAAgB;YACxB,gBAAgB,EAAE,eAAe;SAClC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,MAAM,EAAE;YACN,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ;SAC7B;QACD,eAAe;QACf,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig } from \"../index.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n /** Expected token `aud`. Omit to skip audience verification — only for IdPs\n * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does\n * bind an audience re-require it in their own options. */\n audience?: string;\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n /** Advertise THIS server as the authorization server (skybridge-as-AS): the\n * served AS metadata `issuer` and the PRM `authorization_servers` use this URL\n * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0\n * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic\n * injects the registration endpoint). Use the static public URL; `verify.issuer`\n * stays the IdP (the token's real `iss`). */\n serverUrl?: string;\n /** Advertise this URL as the authorization server (served AS metadata `issuer`\n * and PRM `authorization_servers`) while `verify.issuer` stays the IdP's `iss`.\n * Use when DCR/authorize/token live at a different URL than the token issuer\n * (e.g. Descope's agentic URL vs. its base-project issuer). `serverUrl` wins if\n * both are set. */\n authorizationServer?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/** Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document. */\nexport async function customProvider(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // JWKS verification needs a signing-key URL; without it the server can't verify\n // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply\n // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)\n // always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n }: Partial<DiscoveredMetadata> = opts.metadataOverrides ?? {};\n const base: DiscoveredMetadata = { ...discovered, ...overrides };\n const scopesSupported = opts.scopes ?? base.scopes_supported;\n\n // serverUrl (skybridge-as-AS) or authorizationServer (a distinct AS URL)\n // override the advertised issuer (and keep the served scopes in sync). The\n // verifier still trusts the IdP's `iss`. serverUrl wins when both are set.\n const advertisedIssuer =\n opts.serverUrl?.replace(/\\/$/, \"\") ??\n opts.authorizationServer?.replace(/\\/$/, \"\");\n const oauthMetadata: DiscoveredMetadata = advertisedIssuer\n ? {\n ...base,\n issuer: advertisedIssuer,\n scopes_supported: scopesSupported,\n }\n : base;\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verify: {\n issuer: discovered.issuer,\n audience: opts.audience,\n jwksUri: discovered.jwks_uri,\n },\n scopesSupported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
1
+ {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAgCzB,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,gFAAgF;IAChF,kFAAkF;IAClF,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,yCAAyC;IACzC,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAgC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC9D,IAAI,UAAU,GAAG,UAAU,CAAC;IAC5B,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACV,kDAAkD,IAAI,CAAC,mBAAmB,iBAAiB,IAAI,CAAC,MAAM,sBAAsB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/K,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAE7D,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,MAAM,gBAAgB,GACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAuB,gBAAgB;QACxD,CAAC,CAAC;YACE,GAAG,IAAI;YACP,MAAM,EAAE,gBAAgB;YACxB,gBAAgB,EAAE,eAAe;SAClC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,MAAM,EAAE;YACN,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,QAAQ;SACpD;QACD,eAAe;QACf,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig } from \"../index.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n /** Expected token `aud`. Omit to skip audience verification — only for IdPs\n * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does\n * bind an audience re-require it in their own options. */\n audience?: string;\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n /** Advertise THIS server as the authorization server (skybridge-as-AS): the\n * served AS metadata `issuer` and the PRM `authorization_servers` use this URL\n * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0\n * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic\n * injects the registration endpoint). Use the static public URL; `verify.issuer`\n * stays the IdP (the token's real `iss`). */\n serverUrl?: string;\n /** Advertise this URL as the authorization server (served AS metadata `issuer`\n * and PRM `authorization_servers`). Its own discovery document supplies the\n * advertised endpoints, DCR registration, scopes and the `iss` tokens are\n * verified against; only `jwks_uri` still comes from `issuer`'s document. Use\n * when DCR/authorize/token live at a different URL than the discovery issuer\n * (e.g. Descope's agentic URL vs. its base-project URL). `serverUrl` wins if\n * both are set. */\n authorizationServer?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/** Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document. */\nexport async function customProvider(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // JWKS verification needs a signing-key URL; without it the server can't verify\n // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply\n // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)\n // always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n }: Partial<DiscoveredMetadata> = opts.metadataOverrides ?? {};\n let advertised = discovered;\n if (opts.authorizationServer) {\n try {\n advertised = await discoverAuthorizationServer(opts.authorizationServer);\n } catch (err) {\n console.warn(\n `OAuth discovery failed for authorizationServer ${opts.authorizationServer}; advertising ${opts.issuer} metadata instead: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n const base: DiscoveredMetadata = { ...advertised, ...overrides };\n const scopesSupported = opts.scopes ?? base.scopes_supported;\n\n // serverUrl (skybridge-as-AS) overrides the advertised issuer, keeping the\n // IdP as the token issuer. authorizationServer re-asserts its own URL so a\n // failed AS discovery still points clients at the AS. serverUrl wins.\n const advertisedIssuer =\n opts.serverUrl?.replace(/\\/$/, \"\") ??\n opts.authorizationServer?.replace(/\\/$/, \"\");\n const oauthMetadata: DiscoveredMetadata = advertisedIssuer\n ? {\n ...base,\n issuer: advertisedIssuer,\n scopes_supported: scopesSupported,\n }\n : base;\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verify: {\n issuer: advertised.issuer,\n audience: opts.audience,\n jwksUri: advertised.jwks_uri ?? discovered.jwks_uri,\n },\n scopesSupported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
@@ -1,9 +1,13 @@
1
1
  // @vitest-environment node
2
2
  import http from "node:http";
3
- import { afterEach, describe, expect, it } from "vitest";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
4
  import { customProvider } from "./custom.js";
5
- let server;
6
- afterEach(() => server?.close());
5
+ const servers = [];
6
+ afterEach(() => {
7
+ for (const srv of servers.splice(0)) {
8
+ srv.close();
9
+ }
10
+ });
7
11
  function doc(origin, extra = {}) {
8
12
  return {
9
13
  issuer: origin,
@@ -28,7 +32,7 @@ async function serveDiscovery(extra = {}) {
28
32
  res.end(JSON.stringify(doc(origin, extra)));
29
33
  });
30
34
  await new Promise((resolve) => srv.listen(0, resolve));
31
- server = srv;
35
+ servers.push(srv);
32
36
  origin = `http://localhost:${srv.address().port}`;
33
37
  return origin;
34
38
  }
@@ -93,26 +97,50 @@ describe("customProvider", () => {
93
97
  // but the token's issuer is still verified against the IdP.
94
98
  expect(config.verify.issuer).toBe(base);
95
99
  });
96
- it("authorizationServer advertises a distinct AS, keeping the discovered issuer for verification", async () => {
100
+ it("authorizationServer supplies the advertised metadata and the verified issuer", async () => {
97
101
  const base = await serveDiscovery();
102
+ const as = await serveDiscovery({ scopes_supported: ["checkout"] });
98
103
  const config = await customProvider({
99
104
  issuer: base,
100
105
  audience: "a",
101
- authorizationServer: "https://as.example.test/agentic/P1/MS1/",
106
+ authorizationServer: `${as}/`,
107
+ });
108
+ expect(config.oauthMetadata.issuer).toBe(as);
109
+ expect(config.oauthMetadata.authorization_endpoint).toBe(`${as}/authorize`);
110
+ expect(config.oauthMetadata.registration_endpoint).toBe(`${as}/register`);
111
+ expect(config.scopesSupported).toEqual(["checkout"]);
112
+ expect(config.verify).toEqual({
113
+ issuer: as,
114
+ audience: "a",
115
+ jwksUri: `${as}/jwks`,
102
116
  });
103
- expect(config.oauthMetadata.issuer).toBe("https://as.example.test/agentic/P1/MS1");
117
+ });
118
+ it("falls back to the issuer's metadata when the authorizationServer is undiscoverable", async () => {
119
+ const base = await serveDiscovery();
120
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
121
+ const config = await customProvider({
122
+ issuer: base,
123
+ audience: "a",
124
+ authorizationServer: `${base}/agentic/P1/MS1`,
125
+ });
126
+ expect(warn).toHaveBeenCalledOnce();
127
+ expect(config.oauthMetadata.issuer).toBe(`${base}/agentic/P1/MS1`);
128
+ expect(config.oauthMetadata.authorization_endpoint).toBe(`${base}/authorize`);
104
129
  expect(config.verify.issuer).toBe(base);
130
+ warn.mockRestore();
105
131
  });
106
132
  it("serverUrl takes precedence over authorizationServer", async () => {
107
133
  const base = await serveDiscovery();
134
+ const as = await serveDiscovery();
108
135
  const config = await customProvider({
109
136
  issuer: base,
110
137
  audience: "a",
111
138
  serverUrl: "https://app.example.test/",
112
- authorizationServer: "https://as.example.test/agentic/P1/MS1",
139
+ authorizationServer: as,
113
140
  });
114
141
  expect(config.oauthMetadata.issuer).toBe("https://app.example.test");
115
- expect(config.verify.issuer).toBe(base);
142
+ expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);
143
+ expect(config.verify.issuer).toBe(as);
116
144
  });
117
145
  it("applies metadataOverrides over discovered values", async () => {
118
146
  const base = await serveDiscovery();
@@ -1 +1 @@
1
- {"version":3,"file":"custom.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { customProvider } from \"./custom.js\";\n\nlet server: http.Server | undefined;\nafterEach(() => server?.close());\n\nfunction doc(origin: string, extra: Record<string, unknown> = {}) {\n return {\n issuer: origin,\n authorization_endpoint: `${origin}/authorize`,\n token_endpoint: `${origin}/token`,\n registration_endpoint: `${origin}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"email\", \"profile\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves the discovery doc (built from the live origin) at the OIDC well-known path.\nasync function serveDiscovery(extra: Record<string, unknown> = {}) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n if (req.url !== \"/.well-known/openid-configuration\") {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(doc(origin, extra)));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n server = srv;\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"customProvider\", () => {\n it(\"builds an OAuthConfig from discovery\", async () => {\n const base = await serveDiscovery();\n\n const config = await customProvider({\n issuer: base,\n audience: \"my-api\",\n baseUrl: \"https://app.example.test\",\n scopes: [\"openid\"],\n });\n\n expect(config.baseUrl).toBe(\"https://app.example.test\");\n expect(config.verify).toEqual({\n issuer: base,\n audience: \"my-api\",\n jwksUri: `${base}/jwks`,\n });\n expect(config.scopesSupported).toEqual([\"openid\"]);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"ignores a runtime issuer override (keeps the discovered trust anchor)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { issuer: \"https://evil.test\" } as never,\n });\n expect(config.verify.issuer).toBe(base);\n expect(config.oauthMetadata.issuer).toBe(base);\n });\n\n it(\"ignores a runtime jwks_uri override (keeps the discovered signing keys)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { jwks_uri: \"https://evil.test/keys\" } as never,\n });\n expect(config.verify.jwksUri).toBe(`${base}/jwks`);\n });\n\n it(\"serves metadata without a registration_endpoint when the IdP has no DCR\", async () => {\n const base = await serveDiscovery({ registration_endpoint: undefined });\n const config = await customProvider({ issuer: base, audience: \"a\" });\n expect(config.oauthMetadata.registration_endpoint).toBeUndefined();\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"throws when discovery has no jwks_uri (token verification needs it)\", async () => {\n const base = await serveDiscovery({ jwks_uri: undefined });\n await expect(\n customProvider({ issuer: base, audience: \"a\" }),\n ).rejects.toThrow(/jwks_uri/);\n });\n\n it(\"serverUrl advertises this server as the AS, keeping the IdP as the token issuer\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n scopes: [\"openid\"],\n });\n // AS metadata + advertised scopes reflect this server.\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.scopes_supported).toEqual([\"openid\"]);\n // but the token's issuer is still verified against the IdP.\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"authorizationServer advertises a distinct AS, keeping the discovered issuer for verification\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: \"https://as.example.test/agentic/P1/MS1/\",\n });\n expect(config.oauthMetadata.issuer).toBe(\n \"https://as.example.test/agentic/P1/MS1\",\n );\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"serverUrl takes precedence over authorizationServer\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n authorizationServer: \"https://as.example.test/agentic/P1/MS1\",\n });\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"applies metadataOverrides over discovered values\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n baseUrl: \"https://app.example.test\",\n metadataOverrides: { token_endpoint: \"https://override/token\" },\n });\n expect(config.oauthMetadata.token_endpoint).toBe(\"https://override/token\");\n });\n});\n"]}
1
+ {"version":3,"file":"custom.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { customProvider } from \"./custom.js\";\n\nconst servers: http.Server[] = [];\nafterEach(() => {\n for (const srv of servers.splice(0)) {\n srv.close();\n }\n});\n\nfunction doc(origin: string, extra: Record<string, unknown> = {}) {\n return {\n issuer: origin,\n authorization_endpoint: `${origin}/authorize`,\n token_endpoint: `${origin}/token`,\n registration_endpoint: `${origin}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"email\", \"profile\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves the discovery doc (built from the live origin) at the OIDC well-known path.\nasync function serveDiscovery(extra: Record<string, unknown> = {}) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n if (req.url !== \"/.well-known/openid-configuration\") {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(doc(origin, extra)));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n servers.push(srv);\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"customProvider\", () => {\n it(\"builds an OAuthConfig from discovery\", async () => {\n const base = await serveDiscovery();\n\n const config = await customProvider({\n issuer: base,\n audience: \"my-api\",\n baseUrl: \"https://app.example.test\",\n scopes: [\"openid\"],\n });\n\n expect(config.baseUrl).toBe(\"https://app.example.test\");\n expect(config.verify).toEqual({\n issuer: base,\n audience: \"my-api\",\n jwksUri: `${base}/jwks`,\n });\n expect(config.scopesSupported).toEqual([\"openid\"]);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"ignores a runtime issuer override (keeps the discovered trust anchor)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { issuer: \"https://evil.test\" } as never,\n });\n expect(config.verify.issuer).toBe(base);\n expect(config.oauthMetadata.issuer).toBe(base);\n });\n\n it(\"ignores a runtime jwks_uri override (keeps the discovered signing keys)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { jwks_uri: \"https://evil.test/keys\" } as never,\n });\n expect(config.verify.jwksUri).toBe(`${base}/jwks`);\n });\n\n it(\"serves metadata without a registration_endpoint when the IdP has no DCR\", async () => {\n const base = await serveDiscovery({ registration_endpoint: undefined });\n const config = await customProvider({ issuer: base, audience: \"a\" });\n expect(config.oauthMetadata.registration_endpoint).toBeUndefined();\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"throws when discovery has no jwks_uri (token verification needs it)\", async () => {\n const base = await serveDiscovery({ jwks_uri: undefined });\n await expect(\n customProvider({ issuer: base, audience: \"a\" }),\n ).rejects.toThrow(/jwks_uri/);\n });\n\n it(\"serverUrl advertises this server as the AS, keeping the IdP as the token issuer\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n scopes: [\"openid\"],\n });\n // AS metadata + advertised scopes reflect this server.\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.scopes_supported).toEqual([\"openid\"]);\n // but the token's issuer is still verified against the IdP.\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"authorizationServer supplies the advertised metadata and the verified issuer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery({ scopes_supported: [\"checkout\"] });\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${as}/`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(as);\n expect(config.oauthMetadata.authorization_endpoint).toBe(`${as}/authorize`);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${as}/register`);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n expect(config.verify).toEqual({\n issuer: as,\n audience: \"a\",\n jwksUri: `${as}/jwks`,\n });\n });\n\n it(\"falls back to the issuer's metadata when the authorizationServer is undiscoverable\", async () => {\n const base = await serveDiscovery();\n const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${base}/agentic/P1/MS1`,\n });\n\n expect(warn).toHaveBeenCalledOnce();\n expect(config.oauthMetadata.issuer).toBe(`${base}/agentic/P1/MS1`);\n expect(config.oauthMetadata.authorization_endpoint).toBe(\n `${base}/authorize`,\n );\n expect(config.verify.issuer).toBe(base);\n warn.mockRestore();\n });\n\n it(\"serverUrl takes precedence over authorizationServer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n authorizationServer: as,\n });\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);\n expect(config.verify.issuer).toBe(as);\n });\n\n it(\"applies metadataOverrides over discovered values\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n baseUrl: \"https://app.example.test\",\n metadataOverrides: { token_endpoint: \"https://override/token\" },\n });\n expect(config.oauthMetadata.token_endpoint).toBe(\"https://override/token\");\n });\n});\n"]}
@@ -1,4 +1,12 @@
1
1
  import { customProvider } from "./custom.js";
2
+ /**
3
+ * Turns the console's Discovery URL into the authorization server's base URL:
4
+ * drops a `/.well-known/openid-configuration` (or `oauth-authorization-server`)
5
+ * suffix, since discovery appends that path itself, and any trailing slash.
6
+ */
7
+ function toAuthorizationServerUrl(discoveryUrl) {
8
+ return discoveryUrl.replace(/\/\.well-known\/[^?#]*$/, "").replace(/\/$/, "");
9
+ }
2
10
  /**
3
11
  * Derives the Descope Project ID from an MCP Server URL
4
12
  * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the
@@ -23,12 +31,13 @@ function projectIdFromUrl(url) {
23
31
  */
24
32
  export function descopeProvider(opts) {
25
33
  const { url, audience, ...rest } = opts;
26
- const projectId = projectIdFromUrl(url);
27
- const issuer = `${url.slice(0, url.indexOf("/agentic/"))}/${projectId}`;
34
+ const asUrl = toAuthorizationServerUrl(url);
35
+ const projectId = projectIdFromUrl(asUrl);
36
+ const issuer = `${asUrl.slice(0, asUrl.indexOf("/agentic/"))}/${projectId}`;
28
37
  return customProvider({
29
38
  issuer,
30
39
  audience: audience ?? projectId,
31
- authorizationServer: url,
40
+ authorizationServer: asUrl,
32
41
  ...rest,
33
42
  });
34
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"descope.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,mCAAmC,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA6D;IAE7D,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC;IACxE,OAAO,cAAc,CAAC;QACpB,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,mBAAmB,EAAE,GAAG;QACxB,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/**\n * Derives the Descope Project ID from an MCP Server URL\n * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the\n * project id, so it doubles as the audience.\n */\nfunction projectIdFromUrl(url: string): string {\n const projectId = url.match(/\\/agentic\\/([^/]+)\\/[^/]+/)?.[1];\n if (!projectId) {\n throw new Error(\n `Could not derive the Descope project id from \"${url}\"; pass an explicit \\`audience\\`.`,\n );\n }\n return projectId;\n}\n\n/**\n * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery\n * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.\n * `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your\n * custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR\n * disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`\n * instead (see `examples/auth-descope-alpic`). The token `audience` defaults to\n * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client\n * id, project id], not the server URL; pass `audience` to override.\n */\nexport function descopeProvider(\n opts: { url: string } & Omit<CustomProviderOptions, \"issuer\">,\n): Promise<OAuthConfig> {\n const { url, audience, ...rest } = opts;\n const projectId = projectIdFromUrl(url);\n const issuer = `${url.slice(0, url.indexOf(\"/agentic/\"))}/${projectId}`;\n return customProvider({\n issuer,\n audience: audience ?? projectId,\n authorizationServer: url,\n ...rest,\n });\n}\n"]}
1
+ {"version":3,"file":"descope.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,YAAoB;IACpD,OAAO,YAAY,CAAC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,mCAAmC,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA6D;IAE7D,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC;IAC5E,OAAO,cAAc,CAAC;QACpB,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,mBAAmB,EAAE,KAAK;QAC1B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/**\n * Turns the console's Discovery URL into the authorization server's base URL:\n * drops a `/.well-known/openid-configuration` (or `oauth-authorization-server`)\n * suffix, since discovery appends that path itself, and any trailing slash.\n */\nfunction toAuthorizationServerUrl(discoveryUrl: string): string {\n return discoveryUrl.replace(/\\/\\.well-known\\/[^?#]*$/, \"\").replace(/\\/$/, \"\");\n}\n\n/**\n * Derives the Descope Project ID from an MCP Server URL\n * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the\n * project id, so it doubles as the audience.\n */\nfunction projectIdFromUrl(url: string): string {\n const projectId = url.match(/\\/agentic\\/([^/]+)\\/[^/]+/)?.[1];\n if (!projectId) {\n throw new Error(\n `Could not derive the Descope project id from \"${url}\"; pass an explicit \\`audience\\`.`,\n );\n }\n return projectId;\n}\n\n/**\n * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery\n * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.\n * `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your\n * custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR\n * disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`\n * instead (see `examples/auth-descope-alpic`). The token `audience` defaults to\n * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client\n * id, project id], not the server URL; pass `audience` to override.\n */\nexport function descopeProvider(\n opts: { url: string } & Omit<CustomProviderOptions, \"issuer\">,\n): Promise<OAuthConfig> {\n const { url, audience, ...rest } = opts;\n const asUrl = toAuthorizationServerUrl(url);\n const projectId = projectIdFromUrl(asUrl);\n const issuer = `${asUrl.slice(0, asUrl.indexOf(\"/agentic/\"))}/${projectId}`;\n return customProvider({\n issuer,\n audience: audience ?? projectId,\n authorizationServer: asUrl,\n ...rest,\n });\n}\n"]}
@@ -2,37 +2,58 @@
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { descopeProvider } from "./descope.js";
4
4
  afterEach(() => vi.restoreAllMocks());
5
- function discoveryDoc(issuer) {
5
+ function discoveryDoc(issuer, scopes) {
6
6
  return {
7
7
  issuer,
8
8
  authorization_endpoint: `${issuer}/authorize`,
9
9
  token_endpoint: `${issuer}/token`,
10
10
  registration_endpoint: `${issuer}/register`,
11
11
  response_types_supported: ["code"],
12
- scopes_supported: ["openid"],
12
+ scopes_supported: scopes,
13
13
  jwks_uri: `${issuer}/.well-known/jwks.json`,
14
14
  };
15
15
  }
16
- function mockDiscovery(issuer) {
17
- return vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify(discoveryDoc(issuer)), {
18
- headers: { "content-type": "application/json" },
19
- }));
16
+ const PROJECT = "https://api.descope.com/v1/apps/P123";
17
+ const AGENTIC = "https://api.descope.com/v1/apps/agentic/P123/MS456";
18
+ function mockDiscovery(docs) {
19
+ return vi
20
+ .spyOn(globalThis, "fetch")
21
+ .mockImplementation(async (input) => {
22
+ const url = String(input);
23
+ const entry = Object.entries(docs).find(([issuer]) => url === `${issuer}/.well-known/openid-configuration`);
24
+ if (entry === undefined) {
25
+ return new Response(null, { status: 404 });
26
+ }
27
+ return new Response(JSON.stringify(discoveryDoc(...entry)), {
28
+ headers: { "content-type": "application/json" },
29
+ });
30
+ });
20
31
  }
21
32
  describe("descopeProvider", () => {
22
- it("builds the base-project issuer and audience (project id) from the MCP Server URL", async () => {
23
- const url = "https://api.descope.com/v1/apps/agentic/P123/MS456";
24
- const base = "https://api.descope.com/v1/apps/P123";
25
- const fetchSpy = mockDiscovery(base);
26
- const config = await descopeProvider({ url });
27
- expect(fetchSpy).toHaveBeenCalledWith(`${base}/.well-known/openid-configuration`, expect.anything());
28
- expect(config.verify.issuer).toBe(base);
33
+ it("verifies against the agentic issuer it advertises", async () => {
34
+ const fetchSpy = mockDiscovery({
35
+ [PROJECT]: ["openid", "profile", "email", "phone"],
36
+ [AGENTIC]: ["checkout"],
37
+ });
38
+ const config = await descopeProvider({ url: AGENTIC });
39
+ expect(fetchSpy).toHaveBeenCalledWith(`${PROJECT}/.well-known/openid-configuration`, expect.anything());
40
+ expect(config.verify.issuer).toBe(AGENTIC);
29
41
  expect(config.verify.audience).toBe("P123");
30
- expect(config.oauthMetadata.issuer).toBe(url);
42
+ expect(config.oauthMetadata.issuer).toBe(AGENTIC);
43
+ expect(config.oauthMetadata.registration_endpoint).toBe(`${AGENTIC}/register`);
44
+ expect(config.scopesSupported).toEqual(["checkout"]);
45
+ });
46
+ it("accepts a URL carrying the well-known suffix", async () => {
47
+ mockDiscovery({ [PROJECT]: ["openid"], [AGENTIC]: ["checkout"] });
48
+ const config = await descopeProvider({
49
+ url: `${AGENTIC}/.well-known/openid-configuration`,
50
+ });
51
+ expect(config.oauthMetadata.issuer).toBe(AGENTIC);
52
+ expect(config.scopesSupported).toEqual(["checkout"]);
31
53
  });
32
54
  it("lets an explicit audience override the derived project id", async () => {
33
- const url = "https://api.descope.com/v1/apps/agentic/P123/MS456";
34
- mockDiscovery("https://api.descope.com/v1/apps/P123");
35
- const config = await descopeProvider({ url, audience: "custom" });
55
+ mockDiscovery({ [PROJECT]: ["openid"], [AGENTIC]: ["checkout"] });
56
+ const config = await descopeProvider({ url: AGENTIC, audience: "custom" });
36
57
  expect(config.verify.audience).toBe("custom");
37
58
  });
38
59
  });
@@ -1 +1 @@
1
- {"version":3,"file":"descope.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;AAEtC,SAAS,YAAY,CAAC,MAAc;IAClC,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;QAC7C,cAAc,EAAE,GAAG,MAAM,QAAQ;QACjC,qBAAqB,EAAE,GAAG,MAAM,WAAW;QAC3C,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,QAAQ,CAAC;QAC5B,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,MAAc;IACnC,OAAO,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,kBAAkB,CACrD,KAAK,IAAI,EAAE,CACT,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE;QACjD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CACL,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;QAChG,MAAM,GAAG,GAAG,oDAAoD,CAAC;QACjE,MAAM,IAAI,GAAG,sCAAsC,CAAC;QACpD,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QAErC,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;QAE9C,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnC,GAAG,IAAI,mCAAmC,EAC1C,MAAM,CAAC,QAAQ,EAAE,CAClB,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,GAAG,GAAG,oDAAoD,CAAC;QACjE,aAAa,CAAC,sCAAsC,CAAC,CAAC;QAEtD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QAElE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { descopeProvider } from \"./descope.js\";\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction discoveryDoc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/token`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\nfunction mockDiscovery(issuer: string) {\n return vi.spyOn(globalThis, \"fetch\").mockImplementation(\n async () =>\n new Response(JSON.stringify(discoveryDoc(issuer)), {\n headers: { \"content-type\": \"application/json\" },\n }),\n );\n}\n\ndescribe(\"descopeProvider\", () => {\n it(\"builds the base-project issuer and audience (project id) from the MCP Server URL\", async () => {\n const url = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n const base = \"https://api.descope.com/v1/apps/P123\";\n const fetchSpy = mockDiscovery(base);\n\n const config = await descopeProvider({ url });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${base}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(config.verify.issuer).toBe(base);\n expect(config.verify.audience).toBe(\"P123\");\n expect(config.oauthMetadata.issuer).toBe(url);\n });\n\n it(\"lets an explicit audience override the derived project id\", async () => {\n const url = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n mockDiscovery(\"https://api.descope.com/v1/apps/P123\");\n\n const config = await descopeProvider({ url, audience: \"custom\" });\n\n expect(config.verify.audience).toBe(\"custom\");\n });\n});\n"]}
1
+ {"version":3,"file":"descope.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { descopeProvider } from \"./descope.js\";\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction discoveryDoc(issuer: string, scopes: string[]) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/token`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: scopes,\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\nconst PROJECT = \"https://api.descope.com/v1/apps/P123\";\nconst AGENTIC = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n\nfunction mockDiscovery(docs: Record<string, string[]>) {\n return vi\n .spyOn(globalThis, \"fetch\")\n .mockImplementation(async (input: RequestInfo | URL) => {\n const url = String(input);\n const entry = Object.entries(docs).find(\n ([issuer]) => url === `${issuer}/.well-known/openid-configuration`,\n );\n if (entry === undefined) {\n return new Response(null, { status: 404 });\n }\n return new Response(JSON.stringify(discoveryDoc(...entry)), {\n headers: { \"content-type\": \"application/json\" },\n });\n });\n}\n\ndescribe(\"descopeProvider\", () => {\n it(\"verifies against the agentic issuer it advertises\", async () => {\n const fetchSpy = mockDiscovery({\n [PROJECT]: [\"openid\", \"profile\", \"email\", \"phone\"],\n [AGENTIC]: [\"checkout\"],\n });\n\n const config = await descopeProvider({ url: AGENTIC });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${PROJECT}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(config.verify.issuer).toBe(AGENTIC);\n expect(config.verify.audience).toBe(\"P123\");\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.oauthMetadata.registration_endpoint).toBe(\n `${AGENTIC}/register`,\n );\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"accepts a URL carrying the well-known suffix\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n const config = await descopeProvider({\n url: `${AGENTIC}/.well-known/openid-configuration`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"lets an explicit audience override the derived project id\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n const config = await descopeProvider({ url: AGENTIC, audience: \"custom\" });\n\n expect(config.verify.audience).toBe(\"custom\");\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skybridge",
3
- "version": "1.3.1",
3
+ "version": "2.0.0-alpha.2797af0",
4
4
  "description": "Skybridge is a framework for building ChatGPT and MCP Apps",
5
5
  "repository": {
6
6
  "type": "git",