skybridge 0.0.0-dev.f3a1db3 → 0.0.0-dev.f3a8932
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 +2 -9
- package/dist/cli/build-helpers.d.ts +6 -2
- package/dist/cli/build-helpers.js +37 -12
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +69 -3
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +73 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.d.ts +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +5 -0
- package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
- package/dist/cli/resolve-skybridge-config.js.map +1 -0
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +89 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +14 -0
- package/dist/cli/use-nodemon.js +71 -60
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.d.ts +8 -2
- package/dist/cli/use-typescript-check.js +70 -67
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.d.ts +0 -2
- package/dist/commands/build.js +2 -61
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +34 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/auth/providers/authplane.d.ts +47 -0
- package/dist/server/auth/providers/authplane.js +57 -0
- package/dist/server/auth/providers/authplane.js.map +1 -0
- package/dist/server/auth/providers/authplane.test.d.ts +1 -0
- package/dist/server/auth/providers/authplane.test.js +138 -0
- package/dist/server/auth/providers/authplane.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +8 -0
- package/dist/server/auth/providers/custom.js +19 -7
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +53 -4
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.js +13 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +38 -15
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/auth/security-schemes.d.ts +23 -0
- package/dist/server/auth/security-schemes.js +64 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.d.ts +1 -0
- package/dist/server/auth/security-schemes.test.js +99 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +3 -1
- package/dist/server/auth/setup.js +62 -21
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/setup.test.js +291 -2
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/host.d.ts +2 -0
- package/dist/server/host.js +11 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/host.test.d.ts +1 -0
- package/dist/server/host.test.js +13 -0
- package/dist/server/host.test.js.map +1 -0
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +49 -5
- package/dist/server/server.js +142 -19
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.d.ts +1 -0
- package/dist/server/skills-integration.test.js +63 -0
- package/dist/server/skills-integration.test.js.map +1 -0
- package/dist/server/skills.d.ts +23 -0
- package/dist/server/skills.js +178 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/skills.test.d.ts +1 -0
- package/dist/server/skills.test.js +210 -0
- package/dist/server/skills.test.js.map +1 -0
- package/dist/server/view-name.test-d.d.ts +1 -0
- package/dist/server/view-name.test-d.js +8 -0
- package/dist/server/view-name.test-d.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +69 -1
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/server/viewsDevServer.js +4 -1
- package/dist/server/viewsDevServer.js.map +1 -1
- package/dist/test/view.test.js +3 -3
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.js +36 -5
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +31 -4
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +5 -3
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.test.js +8 -0
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +1 -0
- package/dist/web/hooks/index.js +1 -0
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.d.ts +2 -1
- package/dist/web/hooks/test/utils.js +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-host-info.d.ts +22 -0
- package/dist/web/hooks/use-host-info.js +30 -0
- package/dist/web/hooks/use-host-info.js.map +1 -0
- package/dist/web/hooks/use-host-info.test.d.ts +1 -0
- package/dist/web/hooks/use-host-info.test.js +59 -0
- package/dist/web/hooks/use-host-info.test.js.map +1 -0
- package/dist/web/hooks/use-view-state.js +19 -9
- package/dist/web/hooks/use-view-state.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +31 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/plugin/plugin.d.ts +6 -0
- package/dist/web/plugin/plugin.js +3 -3
- package/dist/web/plugin/plugin.js.map +1 -1
- package/package.json +21 -20
- package/dist/cli/resolve-views-dir.d.ts +0 -1
- package/dist/cli/resolve-views-dir.js.map +0 -1
|
@@ -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
|
-
|
|
6
|
-
afterEach(() =>
|
|
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
|
-
|
|
35
|
+
servers.push(srv);
|
|
32
36
|
origin = `http://localhost:${srv.address().port}`;
|
|
33
37
|
return origin;
|
|
34
38
|
}
|
|
@@ -93,6 +97,51 @@ 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
|
});
|
|
100
|
+
it("authorizationServer supplies the advertised metadata and the verified issuer", async () => {
|
|
101
|
+
const base = await serveDiscovery();
|
|
102
|
+
const as = await serveDiscovery({ scopes_supported: ["checkout"] });
|
|
103
|
+
const config = await customProvider({
|
|
104
|
+
issuer: base,
|
|
105
|
+
audience: "a",
|
|
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`,
|
|
116
|
+
});
|
|
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`);
|
|
129
|
+
expect(config.verify.issuer).toBe(base);
|
|
130
|
+
warn.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
it("serverUrl takes precedence over authorizationServer", async () => {
|
|
133
|
+
const base = await serveDiscovery();
|
|
134
|
+
const as = await serveDiscovery();
|
|
135
|
+
const config = await customProvider({
|
|
136
|
+
issuer: base,
|
|
137
|
+
audience: "a",
|
|
138
|
+
serverUrl: "https://app.example.test/",
|
|
139
|
+
authorizationServer: as,
|
|
140
|
+
});
|
|
141
|
+
expect(config.oauthMetadata.issuer).toBe("https://app.example.test");
|
|
142
|
+
expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);
|
|
143
|
+
expect(config.verify.issuer).toBe(as);
|
|
144
|
+
});
|
|
96
145
|
it("applies metadataOverrides over discovered values", async () => {
|
|
97
146
|
const base = await serveDiscovery();
|
|
98
147
|
const config = await customProvider({
|
|
@@ -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(\"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,10 +31,13 @@ function projectIdFromUrl(url) {
|
|
|
23
31
|
*/
|
|
24
32
|
export function descopeProvider(opts) {
|
|
25
33
|
const { url, audience, ...rest } = opts;
|
|
26
|
-
const
|
|
34
|
+
const asUrl = toAuthorizationServerUrl(url);
|
|
35
|
+
const projectId = projectIdFromUrl(asUrl);
|
|
36
|
+
const issuer = `${asUrl.slice(0, asUrl.indexOf("/agentic/"))}/${projectId}`;
|
|
27
37
|
return customProvider({
|
|
28
38
|
issuer,
|
|
29
|
-
audience: audience ??
|
|
39
|
+
audience: audience ?? projectId,
|
|
40
|
+
authorizationServer: asUrl,
|
|
30
41
|
...rest,
|
|
31
42
|
});
|
|
32
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,MAAM,GAAG,GAAG,CAAC,
|
|
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,35 +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:
|
|
12
|
+
scopes_supported: scopes,
|
|
13
13
|
jwks_uri: `${issuer}/.well-known/jwks.json`,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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("
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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);
|
|
28
41
|
expect(config.verify.audience).toBe("P123");
|
|
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"]);
|
|
29
53
|
});
|
|
30
54
|
it("lets an explicit audience override the derived project id", async () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const config = await descopeProvider({ url: issuer, audience: "custom" });
|
|
55
|
+
mockDiscovery({ [PROJECT]: ["openid"], [AGENTIC]: ["checkout"] });
|
|
56
|
+
const config = await descopeProvider({ url: AGENTIC, audience: "custom" });
|
|
34
57
|
expect(config.verify.audience).toBe("custom");
|
|
35
58
|
});
|
|
36
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;
|
|
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"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AuthInfo } from "../auth.js";
|
|
2
|
+
import type { SecurityScheme, ToolAuth } from "../server.js";
|
|
3
|
+
export declare function authToSecuritySchemes(auth: ToolAuth): SecurityScheme[];
|
|
4
|
+
export declare function securitySchemesAllowAnonymous(schemes: SecurityScheme[] | undefined): boolean;
|
|
5
|
+
export type SchemeFailure = {
|
|
6
|
+
error: "invalid_token" | "insufficient_scope";
|
|
7
|
+
description: string;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
};
|
|
10
|
+
export declare function evaluateSecuritySchemes(schemes: SecurityScheme[] | undefined, authInfo: AuthInfo | undefined): SchemeFailure | undefined;
|
|
11
|
+
export declare function httpStatusForFailure(failure: SchemeFailure): 401 | 403;
|
|
12
|
+
export declare function clientPrefersInBandChallenge(userAgent: string | undefined): boolean;
|
|
13
|
+
export declare function wwwAuthenticateHeader(failure: SchemeFailure, resourceMetadataUrl?: string): string;
|
|
14
|
+
export declare function inBandChallengeResult(failure: SchemeFailure, resourceMetadataUrl?: string): {
|
|
15
|
+
isError: boolean;
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
_meta: {
|
|
21
|
+
"mcp/www_authenticate": string[];
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { hostFromUserAgent } from "../host.js";
|
|
2
|
+
export function authToSecuritySchemes(auth) {
|
|
3
|
+
const oauth2 = {
|
|
4
|
+
type: "oauth2",
|
|
5
|
+
...(auth.scopes?.length ? { scopes: auth.scopes } : {}),
|
|
6
|
+
};
|
|
7
|
+
return auth.allowsAnonymous ? [{ type: "noauth" }, oauth2] : [oauth2];
|
|
8
|
+
}
|
|
9
|
+
export function securitySchemesAllowAnonymous(schemes) {
|
|
10
|
+
return !!schemes?.some((scheme) => scheme.type === "noauth");
|
|
11
|
+
}
|
|
12
|
+
export function evaluateSecuritySchemes(schemes, authInfo) {
|
|
13
|
+
if (securitySchemesAllowAnonymous(schemes) && !authInfo) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const oauth2 = (schemes ?? []).filter((scheme) => scheme.type === "oauth2");
|
|
17
|
+
const required = oauth2.length ? oauth2 : [{ type: "oauth2" }];
|
|
18
|
+
const scopes = [
|
|
19
|
+
...new Set(required.flatMap((scheme) => scheme.scopes ?? [])),
|
|
20
|
+
];
|
|
21
|
+
if (!authInfo) {
|
|
22
|
+
return {
|
|
23
|
+
error: "invalid_token",
|
|
24
|
+
description: "Sign in to use this tool.",
|
|
25
|
+
scopes,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const satisfied = required.some((scheme) => (scheme.scopes ?? []).every((scope) => authInfo.scopes.includes(scope)));
|
|
29
|
+
return satisfied
|
|
30
|
+
? undefined
|
|
31
|
+
: {
|
|
32
|
+
error: "insufficient_scope",
|
|
33
|
+
description: "Missing required scope for this tool.",
|
|
34
|
+
scopes,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function httpStatusForFailure(failure) {
|
|
38
|
+
return failure.error === "insufficient_scope" ? 403 : 401;
|
|
39
|
+
}
|
|
40
|
+
export function clientPrefersInBandChallenge(userAgent) {
|
|
41
|
+
return hostFromUserAgent(userAgent) === "openai";
|
|
42
|
+
}
|
|
43
|
+
export function wwwAuthenticateHeader(failure, resourceMetadataUrl) {
|
|
44
|
+
let header = `Bearer error="${failure.error}", error_description="${failure.description}"`;
|
|
45
|
+
if (failure.scopes.length) {
|
|
46
|
+
header += `, scope="${failure.scopes.join(" ")}"`;
|
|
47
|
+
}
|
|
48
|
+
if (resourceMetadataUrl) {
|
|
49
|
+
header += `, resource_metadata="${resourceMetadataUrl}"`;
|
|
50
|
+
}
|
|
51
|
+
return header;
|
|
52
|
+
}
|
|
53
|
+
export function inBandChallengeResult(failure, resourceMetadataUrl) {
|
|
54
|
+
return {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: [{ type: "text", text: failure.description }],
|
|
57
|
+
_meta: {
|
|
58
|
+
"mcp/www_authenticate": [
|
|
59
|
+
wwwAuthenticateHeader(failure, resourceMetadataUrl),
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=security-schemes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-schemes.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AuthInfo } from \"../auth.js\";\nimport { hostFromUserAgent } from \"../host.js\";\nimport type { SecurityScheme, ToolAuth } from \"../server.js\";\n\nexport function authToSecuritySchemes(auth: ToolAuth): SecurityScheme[] {\n const oauth2: SecurityScheme = {\n type: \"oauth2\",\n ...(auth.scopes?.length ? { scopes: auth.scopes } : {}),\n };\n return auth.allowsAnonymous ? [{ type: \"noauth\" }, oauth2] : [oauth2];\n}\n\nexport function securitySchemesAllowAnonymous(\n schemes: SecurityScheme[] | undefined,\n): boolean {\n return !!schemes?.some((scheme) => scheme.type === \"noauth\");\n}\n\nexport type SchemeFailure = {\n error: \"invalid_token\" | \"insufficient_scope\";\n description: string;\n scopes: string[];\n};\n\nexport function evaluateSecuritySchemes(\n schemes: SecurityScheme[] | undefined,\n authInfo: AuthInfo | undefined,\n): SchemeFailure | undefined {\n if (securitySchemesAllowAnonymous(schemes) && !authInfo) {\n return undefined;\n }\n const oauth2 = (schemes ?? []).filter((scheme) => scheme.type === \"oauth2\");\n const required = oauth2.length ? oauth2 : [{ type: \"oauth2\" as const }];\n const scopes = [\n ...new Set(required.flatMap((scheme) => scheme.scopes ?? [])),\n ];\n if (!authInfo) {\n return {\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes,\n };\n }\n const satisfied = required.some((scheme) =>\n (scheme.scopes ?? []).every((scope) => authInfo.scopes.includes(scope)),\n );\n return satisfied\n ? undefined\n : {\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes,\n };\n}\n\nexport function httpStatusForFailure(failure: SchemeFailure): 401 | 403 {\n return failure.error === \"insufficient_scope\" ? 403 : 401;\n}\n\nexport function clientPrefersInBandChallenge(\n userAgent: string | undefined,\n): boolean {\n return hostFromUserAgent(userAgent) === \"openai\";\n}\n\nexport function wwwAuthenticateHeader(\n failure: SchemeFailure,\n resourceMetadataUrl?: string,\n): string {\n let header = `Bearer error=\"${failure.error}\", error_description=\"${failure.description}\"`;\n if (failure.scopes.length) {\n header += `, scope=\"${failure.scopes.join(\" \")}\"`;\n }\n if (resourceMetadataUrl) {\n header += `, resource_metadata=\"${resourceMetadataUrl}\"`;\n }\n return header;\n}\n\nexport function inBandChallengeResult(\n failure: SchemeFailure,\n resourceMetadataUrl?: string,\n) {\n return {\n isError: true,\n content: [{ type: \"text\" as const, text: failure.description }],\n _meta: {\n \"mcp/www_authenticate\": [\n wwwAuthenticateHeader(failure, resourceMetadataUrl),\n ],\n },\n };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { authToSecuritySchemes, clientPrefersInBandChallenge, evaluateSecuritySchemes, securitySchemesAllowAnonymous, wwwAuthenticateHeader, } from "./security-schemes.js";
|
|
3
|
+
describe("clientPrefersInBandChallenge", () => {
|
|
4
|
+
it("detects ChatGPT / OpenAI user agents", () => {
|
|
5
|
+
expect(clientPrefersInBandChallenge("openai-mcp/1.0")).toBe(true);
|
|
6
|
+
expect(clientPrefersInBandChallenge("ChatGPT")).toBe(true);
|
|
7
|
+
expect(clientPrefersInBandChallenge("Claude-User/1.0")).toBe(false);
|
|
8
|
+
expect(clientPrefersInBandChallenge(undefined)).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe("authToSecuritySchemes", () => {
|
|
12
|
+
it("maps the auth shorthand", () => {
|
|
13
|
+
expect(authToSecuritySchemes({ allowsAnonymous: true })).toEqual([
|
|
14
|
+
{ type: "noauth" },
|
|
15
|
+
{ type: "oauth2" },
|
|
16
|
+
]);
|
|
17
|
+
expect(authToSecuritySchemes({})).toEqual([{ type: "oauth2" }]);
|
|
18
|
+
expect(authToSecuritySchemes({ scopes: ["checkout"] })).toEqual([
|
|
19
|
+
{ type: "oauth2", scopes: ["checkout"] },
|
|
20
|
+
]);
|
|
21
|
+
expect(authToSecuritySchemes({ allowsAnonymous: true, scopes: ["checkout"] })).toEqual([{ type: "noauth" }, { type: "oauth2", scopes: ["checkout"] }]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
const authInfo = (scopes) => ({
|
|
25
|
+
token: "t",
|
|
26
|
+
clientId: "c",
|
|
27
|
+
scopes,
|
|
28
|
+
});
|
|
29
|
+
describe("securitySchemesAllowAnonymous", () => {
|
|
30
|
+
it("is true only when a noauth scheme is present", () => {
|
|
31
|
+
expect(securitySchemesAllowAnonymous([{ type: "noauth" }])).toBe(true);
|
|
32
|
+
expect(securitySchemesAllowAnonymous([{ type: "noauth" }, { type: "oauth2" }])).toBe(true);
|
|
33
|
+
expect(securitySchemesAllowAnonymous([{ type: "oauth2" }])).toBe(false);
|
|
34
|
+
expect(securitySchemesAllowAnonymous(undefined)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("evaluateSecuritySchemes", () => {
|
|
38
|
+
it("lets anonymous callers through a noauth tool", () => {
|
|
39
|
+
const schemes = [{ type: "noauth" }, { type: "oauth2" }];
|
|
40
|
+
expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
it("enforces oauth2 scopes on a public tool once a token is present", () => {
|
|
43
|
+
const schemes = [
|
|
44
|
+
{ type: "noauth" },
|
|
45
|
+
{ type: "oauth2", scopes: ["checkout"] },
|
|
46
|
+
];
|
|
47
|
+
expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();
|
|
48
|
+
expect(evaluateSecuritySchemes(schemes, authInfo(["openid"]))?.error).toBe("insufficient_scope");
|
|
49
|
+
expect(evaluateSecuritySchemes(schemes, authInfo(["checkout"]))).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
it("fails an oauth2-only tool with no token (invalid_token / 401)", () => {
|
|
52
|
+
expect(evaluateSecuritySchemes([{ type: "oauth2" }], undefined)?.error).toBe("invalid_token");
|
|
53
|
+
});
|
|
54
|
+
it("fails when the token is missing a required scope (insufficient_scope / 403)", () => {
|
|
55
|
+
const schemes = [
|
|
56
|
+
{ type: "oauth2", scopes: ["checkout"] },
|
|
57
|
+
];
|
|
58
|
+
expect(evaluateSecuritySchemes(schemes, authInfo(["openid"]))?.error).toBe("insufficient_scope");
|
|
59
|
+
});
|
|
60
|
+
it("passes when the token carries every required scope", () => {
|
|
61
|
+
const schemes = [
|
|
62
|
+
{ type: "oauth2", scopes: ["checkout"] },
|
|
63
|
+
];
|
|
64
|
+
expect(evaluateSecuritySchemes(schemes, authInfo(["openid", "checkout"]))).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
it("treats an undeclared tool as auth-required, satisfied by any valid token", () => {
|
|
67
|
+
expect(evaluateSecuritySchemes(undefined, undefined)?.error).toBe("invalid_token");
|
|
68
|
+
expect(evaluateSecuritySchemes(undefined, authInfo([]))).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
it("carries the tool's required scopes on the failure", () => {
|
|
71
|
+
const schemes = [
|
|
72
|
+
{ type: "oauth2", scopes: ["checkout"] },
|
|
73
|
+
];
|
|
74
|
+
expect(evaluateSecuritySchemes(schemes, undefined)?.scopes).toEqual([
|
|
75
|
+
"checkout",
|
|
76
|
+
]);
|
|
77
|
+
expect(evaluateSecuritySchemes(schemes, authInfo(["openid"]))?.scopes).toEqual(["checkout"]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("wwwAuthenticateHeader", () => {
|
|
81
|
+
it("names the required scopes so the client can step up", () => {
|
|
82
|
+
const header = wwwAuthenticateHeader({
|
|
83
|
+
error: "insufficient_scope",
|
|
84
|
+
description: "Missing required scope for this tool.",
|
|
85
|
+
scopes: ["checkout"],
|
|
86
|
+
});
|
|
87
|
+
expect(header).toContain('error="insufficient_scope"');
|
|
88
|
+
expect(header).toContain('scope="checkout"');
|
|
89
|
+
});
|
|
90
|
+
it("omits the scope parameter when there are none", () => {
|
|
91
|
+
const header = wwwAuthenticateHeader({
|
|
92
|
+
error: "invalid_token",
|
|
93
|
+
description: "Sign in to use this tool.",
|
|
94
|
+
scopes: [],
|
|
95
|
+
});
|
|
96
|
+
expect(header).not.toContain("scope=");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
//# sourceMappingURL=security-schemes.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-schemes.test.js","sourceRoot":"","sources":["../../../src/server/auth/security-schemes.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport type { AuthInfo } from \"../auth.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport {\n authToSecuritySchemes,\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\n\ndescribe(\"clientPrefersInBandChallenge\", () => {\n it(\"detects ChatGPT / OpenAI user agents\", () => {\n expect(clientPrefersInBandChallenge(\"openai-mcp/1.0\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"ChatGPT\")).toBe(true);\n expect(clientPrefersInBandChallenge(\"Claude-User/1.0\")).toBe(false);\n expect(clientPrefersInBandChallenge(undefined)).toBe(false);\n });\n});\n\ndescribe(\"authToSecuritySchemes\", () => {\n it(\"maps the auth shorthand\", () => {\n expect(authToSecuritySchemes({ allowsAnonymous: true })).toEqual([\n { type: \"noauth\" },\n { type: \"oauth2\" },\n ]);\n expect(authToSecuritySchemes({})).toEqual([{ type: \"oauth2\" }]);\n expect(authToSecuritySchemes({ scopes: [\"checkout\"] })).toEqual([\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ]);\n expect(\n authToSecuritySchemes({ allowsAnonymous: true, scopes: [\"checkout\"] }),\n ).toEqual([{ type: \"noauth\" }, { type: \"oauth2\", scopes: [\"checkout\"] }]);\n });\n});\n\nconst authInfo = (scopes: string[]): AuthInfo => ({\n token: \"t\",\n clientId: \"c\",\n scopes,\n});\n\ndescribe(\"securitySchemesAllowAnonymous\", () => {\n it(\"is true only when a noauth scheme is present\", () => {\n expect(securitySchemesAllowAnonymous([{ type: \"noauth\" }])).toBe(true);\n expect(\n securitySchemesAllowAnonymous([{ type: \"noauth\" }, { type: \"oauth2\" }]),\n ).toBe(true);\n expect(securitySchemesAllowAnonymous([{ type: \"oauth2\" }])).toBe(false);\n expect(securitySchemesAllowAnonymous(undefined)).toBe(false);\n });\n});\n\ndescribe(\"evaluateSecuritySchemes\", () => {\n it(\"lets anonymous callers through a noauth tool\", () => {\n const schemes: SecurityScheme[] = [{ type: \"noauth\" }, { type: \"oauth2\" }];\n expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();\n });\n\n it(\"enforces oauth2 scopes on a public tool once a token is present\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"noauth\" },\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, undefined)).toBeUndefined();\n expect(evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.error).toBe(\n \"insufficient_scope\",\n );\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"checkout\"])),\n ).toBeUndefined();\n });\n\n it(\"fails an oauth2-only tool with no token (invalid_token / 401)\", () => {\n expect(\n evaluateSecuritySchemes([{ type: \"oauth2\" }], undefined)?.error,\n ).toBe(\"invalid_token\");\n });\n\n it(\"fails when the token is missing a required scope (insufficient_scope / 403)\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.error).toBe(\n \"insufficient_scope\",\n );\n });\n\n it(\"passes when the token carries every required scope\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\", \"checkout\"])),\n ).toBeUndefined();\n });\n\n it(\"treats an undeclared tool as auth-required, satisfied by any valid token\", () => {\n expect(evaluateSecuritySchemes(undefined, undefined)?.error).toBe(\n \"invalid_token\",\n );\n expect(evaluateSecuritySchemes(undefined, authInfo([]))).toBeUndefined();\n });\n\n it(\"carries the tool's required scopes on the failure\", () => {\n const schemes: SecurityScheme[] = [\n { type: \"oauth2\", scopes: [\"checkout\"] },\n ];\n expect(evaluateSecuritySchemes(schemes, undefined)?.scopes).toEqual([\n \"checkout\",\n ]);\n expect(\n evaluateSecuritySchemes(schemes, authInfo([\"openid\"]))?.scopes,\n ).toEqual([\"checkout\"]);\n });\n});\n\ndescribe(\"wwwAuthenticateHeader\", () => {\n it(\"names the required scopes so the client can step up\", () => {\n const header = wwwAuthenticateHeader({\n error: \"insufficient_scope\",\n description: \"Missing required scope for this tool.\",\n scopes: [\"checkout\"],\n });\n expect(header).toContain('error=\"insufficient_scope\"');\n expect(header).toContain('scope=\"checkout\"');\n });\n\n it(\"omits the scope parameter when there are none\", () => {\n const header = wwwAuthenticateHeader({\n error: \"invalid_token\",\n description: \"Sign in to use this tool.\",\n scopes: [],\n });\n expect(header).not.toContain(\"scope=\");\n });\n});\n"]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
|
+
import type { SecurityScheme } from "../server.js";
|
|
2
3
|
import type { OAuthConfig } from "./index.js";
|
|
4
|
+
export type ResourceMetadataUrlResolver = (getHeader: (key: string) => string | undefined) => string;
|
|
3
5
|
/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
|
|
4
|
-
export declare function setupOAuth(app: Express, config: OAuthConfig):
|
|
6
|
+
export declare function setupOAuth(app: Express, config: OAuthConfig, schemesByTool: Map<string, SecurityScheme[] | undefined>): ResourceMetadataUrlResolver;
|