@softure-ai/mcp-access 0.1.6 → 0.1.8
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/CHANGELOG.md +30 -0
- package/README.md +146 -21
- package/adoption/move-app-tables.sql +100 -0
- package/dist/contract.d.ts +13 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +58 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -5
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +28 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +28 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +56 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +28 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/actions.d.ts +3 -1
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +23 -2
- package/dist/next/actions.js.map +1 -1
- package/dist/next/context.d.ts +7 -1
- package/dist/next/context.d.ts.map +1 -1
- package/dist/next/context.js +12 -10
- package/dist/next/context.js.map +1 -1
- package/dist/next/index.d.ts +4 -1
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +5 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/oauth-page.d.ts +7 -0
- package/dist/next/oauth-page.d.ts.map +1 -0
- package/dist/next/oauth-page.js +36 -0
- package/dist/next/oauth-page.js.map +1 -0
- package/dist/next/oauth-routes.d.ts +19 -0
- package/dist/next/oauth-routes.d.ts.map +1 -0
- package/dist/next/oauth-routes.js +53 -0
- package/dist/next/oauth-routes.js.map +1 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +13 -2
- package/dist/next/pages.js.map +1 -1
- package/dist/options.d.ts +40 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +94 -1
- package/dist/options.js.map +1 -1
- package/dist/origins.d.ts +27 -0
- package/dist/origins.d.ts.map +1 -0
- package/dist/origins.js +32 -0
- package/dist/origins.js.map +1 -0
- package/dist/schema.d.ts +486 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +45 -3
- package/dist/schema.js.map +1 -1
- package/dist/server/context.d.ts +4 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +13 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/endpoint.d.ts +2 -4
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +14 -9
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +5 -3
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.d.ts +127 -0
- package/dist/server/oauth-http.d.ts.map +1 -0
- package/dist/server/oauth-http.js +397 -0
- package/dist/server/oauth-http.js.map +1 -0
- package/dist/server/oauth-validation.d.ts +28 -0
- package/dist/server/oauth-validation.d.ts.map +1 -0
- package/dist/server/oauth-validation.js +88 -0
- package/dist/server/oauth-validation.js.map +1 -0
- package/dist/server/oauth.d.ts +106 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +235 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/server/options.d.ts +11 -2
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +11 -4
- package/dist/server/options.js.map +1 -1
- package/dist/server/origins.d.ts +8 -0
- package/dist/server/origins.d.ts.map +1 -0
- package/dist/server/origins.js +15 -0
- package/dist/server/origins.js.map +1 -0
- package/dist/server/pkce.d.ts +7 -0
- package/dist/server/pkce.d.ts.map +1 -0
- package/dist/server/pkce.js +24 -0
- package/dist/server/pkce.js.map +1 -0
- package/dist/server/privacy.d.ts +7 -0
- package/dist/server/privacy.d.ts.map +1 -1
- package/dist/server/privacy.js +19 -9
- package/dist/server/privacy.js.map +1 -1
- package/dist/server/scopes.d.ts +5 -0
- package/dist/server/scopes.d.ts.map +1 -0
- package/dist/server/scopes.js +6 -0
- package/dist/server/scopes.js.map +1 -0
- package/dist/server/tokens.d.ts +18 -6
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js +60 -25
- package/dist/server/tokens.js.map +1 -1
- package/dist/ui/consent-form.d.ts +31 -0
- package/dist/ui/consent-form.d.ts.map +1 -0
- package/dist/ui/consent-form.js +24 -0
- package/dist/ui/consent-form.js.map +1 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/token-manager.d.ts +16 -2
- package/dist/ui/token-manager.d.ts.map +1 -1
- package/dist/ui/token-manager.js +6 -2
- package/dist/ui/token-manager.js.map +1 -1
- package/migrations/0002_create_oauth_grants.sql +68 -0
- package/module.json +18 -4
- package/package.json +2 -1
- package/src/contract.ts +16 -1
- package/src/index.ts +33 -4
- package/src/messages/en.ts +28 -0
- package/src/messages/pl.ts +28 -0
- package/src/next/actions.ts +23 -3
- package/src/next/context.ts +15 -8
- package/src/next/index.ts +12 -2
- package/src/next/oauth-page.tsx +68 -0
- package/src/next/oauth-routes.ts +73 -0
- package/src/next/pages.tsx +16 -3
- package/src/options.ts +107 -1
- package/src/origins.ts +48 -0
- package/src/schema.ts +51 -3
- package/src/server/context.ts +14 -0
- package/src/server/endpoint.ts +14 -9
- package/src/server/health.ts +5 -3
- package/src/server/index.ts +56 -0
- package/src/server/oauth-http.ts +480 -0
- package/src/server/oauth-validation.ts +106 -0
- package/src/server/oauth.ts +315 -0
- package/src/server/options.ts +20 -4
- package/src/server/origins.ts +19 -0
- package/src/server/pkce.ts +27 -0
- package/src/server/privacy.ts +26 -9
- package/src/server/scopes.ts +6 -0
- package/src/server/tokens.ts +62 -25
- package/src/ui/consent-form.tsx +103 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/token-manager.tsx +79 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"endpoint.js","sourceRoot":"","sources":["../../src/server/endpoint.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,oGAAoG;AACpG,gGAAgG;AAChG,kGAAkG;AAClG,4BAA4B;AAC5B,OAAO,EACL,2BAA2B,EAC3B,gBAAgB,EAChB,UAAU,EACV,cAAc,GAIf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAE/E,OAAO,EAAE,+BAA+B,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAClF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAyB,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC;AAE3C,8FAA8F;AAC9F,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,CAAC;AAE3C,6FAA6F;AAC7F,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,4EAA4E;AAC5E,MAAM,aAAa,GAAG,QAAQ,CAAC;AAC/B,MAAM,aAAa,GAAG,gBAAgB,CAAC;AACvC,MAAM,QAAQ,GAAG,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;AAYjD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAE,YAAY,EAAsB;IACpE,MAAM,OAAO,GAAG,gBAAgB,CAC9B,CAAC,cAAc,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,EACvE;QACE,YAAY,EAAE,MAAM;QACpB,kBAAkB,EAAE,qBAAqB;QACzC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,qDAAqD,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;KAC/G,CACF,CAAC;IAEF,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE;QAC5B,qBAAqB,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QAE3G,IAAI,KAAK,CAAC;QACV,IAAI,CAAC;YACH,mFAAmF;YACnF,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC5F,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,0FAA0F;YAC1F,OAAO,CAAC,KAAK,CAAC,sDAAsD,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC5F,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;YACd,OAAO,QAAQ,CAAC,IAAI,CAClB,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAC9B,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,QAAQ,EAAE,aAAa,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,EAAE,CAC1F,CAAC;QACJ,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAClD,IAAI,QAAQ,YAAY,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAClD,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC9C,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,YAAY,CAAC,GAAqB,EAAE,OAAgB;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAC1C,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,cAAc,CAAC,EAAE,mBAAmB,EAAE,+BAA+B,CAAC,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE;QAChJ,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;IACzC,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,OAAO,2BAA2B,CAAC,IAAI,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,sBAAsB,CAAC,EAAE,SAAS,CAAC,CAAC;IACrH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAEvD,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,qDAAqD,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3F,OAAO,2BAA2B,CAAC,IAAI,UAAU,CAAC,cAAc,CAAC,WAAW,EAAE,uBAAuB,CAAC,EAAE,SAAS,CAAC,CAAC;IACrH,CAAC;IACD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtB,OAAO,2BAA2B,CAAC,IAAI,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,sBAAsB,CAAC,EAAE,SAAS,CAAC,CAAC;IACrH,CAAC;IACD,OAAO;QACL,KAAK;QACL,8EAA8E;QAC9E,QAAQ,EAAE,QAAQ,CAAC,OAAO;QAC1B,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;QAChF,qDAAqD;QACrD,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC;QAC1D,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,EAAE;KAC5C,CAAC;AACJ,CAAC;AAED,6FAA6F;AAC7F,SAAS,YAAY,CAAC,QAA8B;IAClD,MAAM,MAAM,GAAG,QAAQ,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,CAAC;IAChD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;QAC1E,4FAA4F;QAC5F,gFAAgF;QAChF,MAAM,IAAI,KAAK,CAAC,wFAAwF,CAAC,CAAC;IAC5G,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC;AACrG,CAAC;AAED,qGAAqG;AACrG,SAAS,qBAAqB,CAAC,GAAqB;IAClD,MAAM,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,OAA4D,CAAC;IAChH,IAAI,OAAO,EAAE,OAAO,EAAE,CAAC,qBAAqB,CAAC,KAAK,SAAS,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CACb,uDAAuD,qBAAqB,oEAAoE,CACjJ,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAGA,OAAO,EAAM,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIxD,eAAO,MAAM,sBAAsB,EAAE,WAMpC,CAAC"}
|
package/dist/server/health.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// The module's readiness probe for `GET /api/health` of `@softure-ai/ops`: the tokens table
|
|
2
|
-
// and
|
|
1
|
+
// The module's readiness probe for `GET /api/health` of `@softure-ai/ops`: the tokens table (with
|
|
2
|
+
// its grant column) and the grants table exist and answer, i.e. `softure migrate` ran every file.
|
|
3
|
+
// It reads no rows.
|
|
3
4
|
import { ok } from "@softure-ai/core";
|
|
4
5
|
import { sql } from "drizzle-orm";
|
|
5
6
|
export const checkAccessTokensTable = async (context) => {
|
|
6
7
|
// Core types `db` as unknown; the health route passes the @softure-ai/db handle.
|
|
7
8
|
const db = context.db;
|
|
8
|
-
await db.execute(sql `select
|
|
9
|
+
await db.execute(sql `select grant_id from mcp.access_tokens limit 0`);
|
|
10
|
+
await db.execute(sql `select 1 from mcp.oauth_grants join mcp.oauth_clients on oauth_clients.id = oauth_grants.client_id limit 0`);
|
|
9
11
|
return ok();
|
|
10
12
|
};
|
|
11
13
|
//# sourceMappingURL=health.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/server/health.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,kGAAkG;AAClG,oBAAoB;AACpB,OAAO,EAAE,EAAE,EAAoB,MAAM,kBAAkB,CAAC;AAExD,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,CAAC,MAAM,sBAAsB,GAAgB,KAAK,EAAE,OAAO,EAAE,EAAE;IACnE,iFAAiF;IACjF,MAAM,EAAE,GAAG,OAAO,CAAC,EAAe,CAAC;IACnC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,gDAAgD,CAAC,CAAC;IACtE,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,4GAA4G,CAAC,CAAC;IAClI,OAAO,EAAE,EAAE,CAAC;AACd,CAAC,CAAC"}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
export { createMcpEndpoint, MAX_MCP_REQUEST_BYTES, MCP_RATE_LIMIT_BUCKET, MCP_READ_SCOPE, MCP_WRITE_SCOPE, type McpEndpoint, type McpEndpointOptions, type McpServerFactory, } from "./endpoint.js";
|
|
2
|
+
export { createMcpAccessContext } from "./context.js";
|
|
2
3
|
export { checkAccessTokensTable } from "./health.js";
|
|
4
|
+
export { CONSENT_CODE_PREFIX, CLIENT_SECRET_PREFIX, createAuthorizationCode, exchangeAuthorizationCode, findOAuthClient, isClientSecretValid, listOAuthGrants, pruneOAuthRecords, REFRESH_TOKEN_PREFIX, refreshMcpGrant, registerMcpClient, revokeOAuthGrant, type CreateAuthorizationCodeInput, type ExchangeAuthorizationCodeInput, type IssuedOAuthTokens, type OAuthClientRegistration, type PrunedOAuthRecords, type RefreshOAuthGrantInput, type RegisteredOAuthClient, type RevokeOAuthGrantInput, } from "./oauth.js";
|
|
5
|
+
export { answerOAuthPreflight, AUTHORIZATION_PARAMS, AUTHORIZATION_SERVER_METADATA_PATH, buildClientRedirect, getAuthorizationServerMetadata, getConsentPath, getOAuthIssuer, getProtectedResourceMetadata, getProtectedResourceMetadataUrl, getRootProtectedResourceMetadata, handleAuthorizationDecision, handleClientRegistration, handleTokenRequest, isAcceptableResource, isOAuthEnabled, isWriteScopeRequested, MCP_OAUTH_RATE_LIMIT_BUCKET, OAUTH_SCOPES, PROTECTED_RESOURCE_METADATA_PATH, readAuthorizationParams, serveDiscoveryDocument, validateAuthorizationRequest, type AuthorizationOutcome, type AuthorizationPageError, type AuthorizationRequest, type DiscoveryDocument, } from "./oauth-http.js";
|
|
6
|
+
export { findServedResourceOrigin, readRequestHost, readRequestOrigin, type McpOriginRequest, type McpOrigins } from "../origins.js";
|
|
7
|
+
export { resolveMcpOrigins } from "./origins.js";
|
|
8
|
+
export type { OAuthClientRow, OAuthTokenEndpointAuthMethod } from "../schema.js";
|
|
9
|
+
export { describeRedirectUri, isAllowedRedirectUri, MAX_CLIENT_NAME_LENGTH, parseClientRegistration, type ClientRegistrationResult, type RegistrationError } from "./oauth-validation.js";
|
|
10
|
+
export { getCodeChallenge, isPkceVerifierValid, isValidCodeChallenge, PKCE_METHOD } from "./pkce.js";
|
|
3
11
|
export { getMcpAccessMessages, getMcpAccessOptions, getMcpAccessRoutes, getMcpEndpointUrl, type McpAccessRoutes } from "./options.js";
|
|
4
12
|
export { deleteMcpAccessUserData, exportMcpAccessUserData, mcpAccessPrivacyContributor, type McpAccessUserData, } from "./privacy.js";
|
|
5
13
|
export { ACCESS_TOKEN_PREFIX, hashAccessToken, isAccessTokenShape, issueAccessToken, listAccessTokens, pruneAccessTokens, revokeAccessToken, verifyAccessToken, type IssueAccessTokenInput, type IssueAccessTokenResult, type IssuedAccessToken, type McpAccessContext, type RevokeAccessTokenInput, type VerifiedAccessToken, } from "./tokens.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACtI,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EACnC,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,GAC3B,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kCAAkC,EAClC,mBAAmB,EACnB,8BAA8B,EAC9B,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,+BAA+B,EAC/B,gCAAgC,EAChC,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,YAAY,EACZ,gCAAgC,EAChC,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AACrI,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,4BAA4B,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,KAAK,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1L,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACtI,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,KAAK,iBAAiB,GACvB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// Server-only API of @softure-ai/mcp-access. Every function receives the module context
|
|
2
2
|
// (`{ db, clock, config }`) and never reads request scope; the endpoint takes a Web `Request`.
|
|
3
3
|
export { createMcpEndpoint, MAX_MCP_REQUEST_BYTES, MCP_RATE_LIMIT_BUCKET, MCP_READ_SCOPE, MCP_WRITE_SCOPE, } from "./endpoint.js";
|
|
4
|
+
export { createMcpAccessContext } from "./context.js";
|
|
4
5
|
export { checkAccessTokensTable } from "./health.js";
|
|
6
|
+
export { CONSENT_CODE_PREFIX, CLIENT_SECRET_PREFIX, createAuthorizationCode, exchangeAuthorizationCode, findOAuthClient, isClientSecretValid, listOAuthGrants, pruneOAuthRecords, REFRESH_TOKEN_PREFIX, refreshMcpGrant, registerMcpClient, revokeOAuthGrant, } from "./oauth.js";
|
|
7
|
+
export { answerOAuthPreflight, AUTHORIZATION_PARAMS, AUTHORIZATION_SERVER_METADATA_PATH, buildClientRedirect, getAuthorizationServerMetadata, getConsentPath, getOAuthIssuer, getProtectedResourceMetadata, getProtectedResourceMetadataUrl, getRootProtectedResourceMetadata, handleAuthorizationDecision, handleClientRegistration, handleTokenRequest, isAcceptableResource, isOAuthEnabled, isWriteScopeRequested, MCP_OAUTH_RATE_LIMIT_BUCKET, OAUTH_SCOPES, PROTECTED_RESOURCE_METADATA_PATH, readAuthorizationParams, serveDiscoveryDocument, validateAuthorizationRequest, } from "./oauth-http.js";
|
|
8
|
+
export { findServedResourceOrigin, readRequestHost, readRequestOrigin } from "../origins.js";
|
|
9
|
+
export { resolveMcpOrigins } from "./origins.js";
|
|
10
|
+
export { describeRedirectUri, isAllowedRedirectUri, MAX_CLIENT_NAME_LENGTH, parseClientRegistration } from "./oauth-validation.js";
|
|
11
|
+
export { getCodeChallenge, isPkceVerifierValid, isValidCodeChallenge, PKCE_METHOD } from "./pkce.js";
|
|
5
12
|
export { getMcpAccessMessages, getMcpAccessOptions, getMcpAccessRoutes, getMcpEndpointUrl } from "./options.js";
|
|
6
13
|
export { deleteMcpAccessUserData, exportMcpAccessUserData, mcpAccessPrivacyContributor, } from "./privacy.js";
|
|
7
14
|
export { ACCESS_TOKEN_PREFIX, hashAccessToken, isAccessTokenShape, issueAccessToken, listAccessTokens, pruneAccessTokens, revokeAccessToken, verifyAccessToken, } from "./tokens.js";
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,+FAA+F;AAC/F,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,GAIhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAwB,MAAM,cAAc,CAAC;AACtI,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,GAE5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAOlB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,+FAA+F;AAC/F,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,cAAc,EACd,eAAe,GAIhB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GASjB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kCAAkC,EAClC,mBAAmB,EACnB,8BAA8B,EAC9B,cAAc,EACd,cAAc,EACd,4BAA4B,EAC5B,+BAA+B,EAC/B,gCAAgC,EAChC,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,EAClB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,2BAA2B,EAC3B,YAAY,EACZ,gCAAgC,EAChC,uBAAuB,EACvB,sBAAsB,EACtB,4BAA4B,GAK7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,wBAAwB,EAAE,eAAe,EAAE,iBAAiB,EAA0C,MAAM,eAAe,CAAC;AACrI,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,uBAAuB,EAAyD,MAAM,uBAAuB,CAAC;AAC1L,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACrG,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,iBAAiB,EAAwB,MAAM,cAAc,CAAC;AACtI,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,GAE5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAOlB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import { type McpOriginRequest, type McpOrigins } from "../origins.js";
|
|
3
|
+
import type { OAuthClientRow } from "../schema.js";
|
|
4
|
+
import type { McpAccessContext } from "./tokens.js";
|
|
5
|
+
/** The security bucket OAuth registration and token requests count in. */
|
|
6
|
+
export declare const MCP_OAUTH_RATE_LIMIT_BUCKET = "mcp-oauth";
|
|
7
|
+
/** RFC 8414 §3: the authorization server metadata, at the root of the issuer. */
|
|
8
|
+
export declare const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
9
|
+
/** RFC 9728 §3: protected resource metadata; the endpoint's own variant appends its path. */
|
|
10
|
+
export declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
11
|
+
export declare const OAUTH_SCOPES: readonly ["mcp:read", "mcp:write"];
|
|
12
|
+
/** The parameters the consent form carries to the decision in hidden fields. */
|
|
13
|
+
export declare const AUTHORIZATION_PARAMS: readonly ["response_type", "client_id", "redirect_uri", "state", "code_challenge", "code_challenge_method", "scope", "resource"];
|
|
14
|
+
/** `OPTIONS` on the registration, token and discovery routes. */
|
|
15
|
+
export declare function answerOAuthPreflight(): Response;
|
|
16
|
+
/**
|
|
17
|
+
* The issuer: the app's origin, without a path or a trailing slash. `origins` are the request's
|
|
18
|
+
* (`resolveMcpOrigins`); without them, the configured `appOrigin`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getOAuthIssuer(config: SoftureConfig, origins?: McpOrigins): string;
|
|
21
|
+
/** The URL the endpoint's `401` names in `resource_metadata` (RFC 9728 §5.1). */
|
|
22
|
+
export declare function getProtectedResourceMetadataUrl(config: SoftureConfig, origins?: McpOrigins): string;
|
|
23
|
+
/** Whether the app turned OAuth on; the OAuth routes answer 404 otherwise. */
|
|
24
|
+
export declare function isOAuthEnabled(config: SoftureConfig): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Authorization server metadata (RFC 8414 §2), with the app's `oauth.metadata.authorizationServer`
|
|
27
|
+
* keys added; generated keys win. Tokens are opaque: no OpenID.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAuthorizationServerMetadata(config: SoftureConfig, origins?: McpOrigins): {
|
|
30
|
+
issuer: string;
|
|
31
|
+
authorization_endpoint: string;
|
|
32
|
+
token_endpoint: string;
|
|
33
|
+
registration_endpoint: string;
|
|
34
|
+
scopes_supported: ("mcp:read" | "mcp:write")[];
|
|
35
|
+
response_types_supported: string[];
|
|
36
|
+
response_modes_supported: string[];
|
|
37
|
+
grant_types_supported: string[];
|
|
38
|
+
token_endpoint_auth_methods_supported: string[];
|
|
39
|
+
code_challenge_methods_supported: string[];
|
|
40
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Protected resource metadata (RFC 9728 §2), with the app's `oauth.metadata.protectedResource`
|
|
44
|
+
* keys added; generated keys win, except the app's `resource_name`. At the endpoint's own path the
|
|
45
|
+
* resource is the endpoint URL; at the root it is the origin the document was asked on
|
|
46
|
+
* (`servedOrigin`, one of `resourceOrigins`) or else the app origin, because RFC 9728 §3.3 ties
|
|
47
|
+
* `resource` to the URL the metadata URL was built from.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getProtectedResourceMetadata(config: SoftureConfig, variant: "endpoint" | "root", origins?: McpOrigins, servedOrigin?: string | null): {
|
|
50
|
+
resource: string;
|
|
51
|
+
authorization_servers: string[];
|
|
52
|
+
scopes_supported: ("mcp:read" | "mcp:write")[];
|
|
53
|
+
bearer_methods_supported: string[];
|
|
54
|
+
resource_name: string;
|
|
55
|
+
};
|
|
56
|
+
/** Builds a discovery document for the request's origins; the request is null outside a route. */
|
|
57
|
+
export type DiscoveryDocument = (config: SoftureConfig, origins: McpOrigins, request: McpOriginRequest | null) => unknown;
|
|
58
|
+
/** `GET` of a discovery document for `request` (its origins); 404 while OAuth is off. */
|
|
59
|
+
export declare function serveDiscoveryDocument(config: SoftureConfig, document: DiscoveryDocument, request?: McpOriginRequest | null): Response;
|
|
60
|
+
/** `serveDiscoveryDocument`'s builder for the root protected resource metadata, on the host it was asked on. */
|
|
61
|
+
export declare const getRootProtectedResourceMetadata: DiscoveryDocument;
|
|
62
|
+
/**
|
|
63
|
+
* Whether `resource` (RFC 8707) names this server: absent (clients from before RFC 8707), the
|
|
64
|
+
* endpoint URL, the app origin or one of `resourceOrigins`, each with or without a trailing slash.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isAcceptableResource(config: SoftureConfig, resource: string | null, origins?: McpOrigins): boolean;
|
|
67
|
+
/** Whether the client asks to write. Unknown scopes are ignored (RFC 6749 §3.3); every grant reads. */
|
|
68
|
+
export declare function isWriteScopeRequested(scope: string | null): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* `POST` dynamic client registration (RFC 7591). Public by definition: claude.ai and ChatGPT
|
|
71
|
+
* register before anyone signs in. A registration grants nothing; access needs a person's consent.
|
|
72
|
+
*/
|
|
73
|
+
export declare function handleClientRegistration(ctx: McpAccessContext, request: Request): Promise<Response>;
|
|
74
|
+
/**
|
|
75
|
+
* `POST` token endpoint (RFC 6749 §3.2): a code with its PKCE verifier, or a refresh token with
|
|
76
|
+
* rotation. Secrets come in the body, never in the URL.
|
|
77
|
+
*/
|
|
78
|
+
export declare function handleTokenRequest(ctx: McpAccessContext, request: Request): Promise<Response>;
|
|
79
|
+
export interface AuthorizationRequest {
|
|
80
|
+
readonly client: OAuthClientRow;
|
|
81
|
+
readonly redirectUri: string;
|
|
82
|
+
readonly state: string | null;
|
|
83
|
+
readonly codeChallenge: string;
|
|
84
|
+
/** The client asked for `mcp:write`. */
|
|
85
|
+
readonly isWriteRequested: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** Why the consent page shows an error instead of sending one back: the client is not confirmed. */
|
|
88
|
+
export type AuthorizationPageError = "unknown_client" | "redirect_mismatch";
|
|
89
|
+
export type AuthorizationOutcome = {
|
|
90
|
+
readonly kind: "show-error";
|
|
91
|
+
readonly reason: AuthorizationPageError;
|
|
92
|
+
} | {
|
|
93
|
+
readonly kind: "redirect-error";
|
|
94
|
+
readonly location: string;
|
|
95
|
+
readonly redirectUri: string;
|
|
96
|
+
} | {
|
|
97
|
+
readonly kind: "valid";
|
|
98
|
+
readonly request: AuthorizationRequest;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* The client's redirect URI with parameters added, keeping the query it registered (RFC 6749
|
|
102
|
+
* §3.1.2), and always `iss` (RFC 9207), so the client knows the answer came from this server.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildClientRedirect(config: SoftureConfig, redirectUri: string, params: Readonly<Record<string, string | null>>, origins?: McpOrigins): string;
|
|
105
|
+
/** The authorization parameters of a query string or form, each at most 2048 characters. */
|
|
106
|
+
export declare function readAuthorizationParams(source: {
|
|
107
|
+
get(name: string): unknown;
|
|
108
|
+
}): URLSearchParams;
|
|
109
|
+
/**
|
|
110
|
+
* Validates an authorization request (RFC 6749 §4.1.1), for the consent page and the decision.
|
|
111
|
+
* Order is a security rule (RFC 6749 §4.1.2.1): until the client and its redirect URI are
|
|
112
|
+
* confirmed, an error is shown to the person, never redirected, or the server would redirect
|
|
113
|
+
* anyone anywhere. Only then do errors go back to the client. `origins` are the request's
|
|
114
|
+
* (the consent page's and the decision's must agree); without them, the configured ones.
|
|
115
|
+
*/
|
|
116
|
+
export declare function validateAuthorizationRequest(ctx: McpAccessContext, params: URLSearchParams, origins?: McpOrigins): Promise<AuthorizationOutcome>;
|
|
117
|
+
/** The consent page with the request's parameters: where the decision sends the person back. */
|
|
118
|
+
export declare function getConsentPath(config: SoftureConfig, params: URLSearchParams): string;
|
|
119
|
+
/**
|
|
120
|
+
* `POST` of the consent form, answered with `303`: a plain form post, not a server action, because
|
|
121
|
+
* the answer is a redirect to the client, possibly a native app scheme, and must work without
|
|
122
|
+
* JavaScript. The form must come from the app's origin (the session cookie's `SameSite=Lax` is the
|
|
123
|
+
* first layer), and the request is validated again from the posted fields. `userId` is the
|
|
124
|
+
* session's user, or null: then the person goes back to the consent page, which asks them to sign in.
|
|
125
|
+
*/
|
|
126
|
+
export declare function handleAuthorizationDecision(ctx: McpAccessContext, request: Request, userId: string | null): Promise<Response>;
|
|
127
|
+
//# sourceMappingURL=oauth-http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-http.d.ts","sourceRoot":"","sources":["../../src/server/oauth-http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// The OAuth 2.1 authorization server's HTTP layer: discovery metadata (RFC 8414, RFC 9728),
|
|
2
|
+
// dynamic client registration (RFC 7591), the authorization request and the consent decision
|
|
3
|
+
// (RFC 6749 §4.1, PKCE, RFC 9207 `iss`), and the token endpoint with rotated refresh tokens. Each
|
|
4
|
+
// handler takes a Web `Request` and returns a `Response`, so any fetch-shaped host can mount it;
|
|
5
|
+
// the Next adapter only supplies the context and the session.
|
|
6
|
+
import { errorLogLabel, getModule } from "@softure-ai/core";
|
|
7
|
+
import { consumeRateLimit, identifyClient } from "@softure-ai/security/server";
|
|
8
|
+
import { findServedResourceOrigin } from "../origins.js";
|
|
9
|
+
import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "./scopes.js";
|
|
10
|
+
import { createAuthorizationCode, exchangeAuthorizationCode, findOAuthClient, isClientSecretValid, refreshMcpGrant, registerMcpClient, } from "./oauth.js";
|
|
11
|
+
import { parseClientRegistration } from "./oauth-validation.js";
|
|
12
|
+
import { getMcpAccessOptions, getMcpAccessRoutes, getMcpEndpointUrl } from "./options.js";
|
|
13
|
+
import { resolveMcpOrigins } from "./origins.js";
|
|
14
|
+
import { isValidCodeChallenge, PKCE_METHOD } from "./pkce.js";
|
|
15
|
+
/** The security bucket OAuth registration and token requests count in. */
|
|
16
|
+
export const MCP_OAUTH_RATE_LIMIT_BUCKET = "mcp-oauth";
|
|
17
|
+
/** RFC 8414 §3: the authorization server metadata, at the root of the issuer. */
|
|
18
|
+
export const AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
19
|
+
/** RFC 9728 §3: protected resource metadata; the endpoint's own variant appends its path. */
|
|
20
|
+
export const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
21
|
+
export const OAUTH_SCOPES = [MCP_READ_SCOPE, MCP_WRITE_SCOPE];
|
|
22
|
+
/** The parameters the consent form carries to the decision in hidden fields. */
|
|
23
|
+
export const AUTHORIZATION_PARAMS = ["response_type", "client_id", "redirect_uri", "state", "code_challenge", "code_challenge_method", "scope", "resource"];
|
|
24
|
+
/** Values longer than this are not echoed into a form or a redirect. */
|
|
25
|
+
const MAX_PARAM_LENGTH = 2048;
|
|
26
|
+
/**
|
|
27
|
+
* CORS `*` is safe on these routes because none reads a cookie: registration and token
|
|
28
|
+
* authenticate the client by what it sent. A browser-based MCP client cannot sign in without it.
|
|
29
|
+
*/
|
|
30
|
+
const CORS_HEADERS = {
|
|
31
|
+
"access-control-allow-origin": "*",
|
|
32
|
+
"access-control-allow-methods": "GET, POST, OPTIONS",
|
|
33
|
+
"access-control-allow-headers": "authorization, content-type, mcp-protocol-version",
|
|
34
|
+
};
|
|
35
|
+
/** Responses that carry credentials must not be stored (RFC 6749 §5.1). */
|
|
36
|
+
const NO_STORE = { "cache-control": "no-store", pragma: "no-cache" };
|
|
37
|
+
/** Discovery documents change only with a deploy, and with the host they were asked on. */
|
|
38
|
+
const DISCOVERY_CACHE = { "cache-control": "public, max-age=300", vary: "host, x-forwarded-proto" };
|
|
39
|
+
function protocolJson(body, status = 200, extra = {}) {
|
|
40
|
+
return Response.json(body, { status, headers: { ...CORS_HEADERS, ...NO_STORE, ...extra } });
|
|
41
|
+
}
|
|
42
|
+
/** An error in the shape of RFC 6749 §5.2 / RFC 7591 §3.2.2. */
|
|
43
|
+
function protocolError(error, description, status = 400, extra = {}) {
|
|
44
|
+
return protocolJson({ error, error_description: description }, status, extra);
|
|
45
|
+
}
|
|
46
|
+
/** `OPTIONS` on the registration, token and discovery routes. */
|
|
47
|
+
export function answerOAuthPreflight() {
|
|
48
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------------------------
|
|
51
|
+
// Discovery
|
|
52
|
+
/**
|
|
53
|
+
* The issuer: the app's origin, without a path or a trailing slash. `origins` are the request's
|
|
54
|
+
* (`resolveMcpOrigins`); without them, the configured `appOrigin`.
|
|
55
|
+
*/
|
|
56
|
+
export function getOAuthIssuer(config, origins = resolveMcpOrigins(config)) {
|
|
57
|
+
return origins.appOrigin;
|
|
58
|
+
}
|
|
59
|
+
/** The URL the endpoint's `401` names in `resource_metadata` (RFC 9728 §5.1). */
|
|
60
|
+
export function getProtectedResourceMetadataUrl(config, origins = resolveMcpOrigins(config)) {
|
|
61
|
+
return `${getOAuthIssuer(config, origins)}${PROTECTED_RESOURCE_METADATA_PATH}${new URL(getMcpEndpointUrl(config, origins)).pathname}`;
|
|
62
|
+
}
|
|
63
|
+
/** Whether the app turned OAuth on; the OAuth routes answer 404 otherwise. */
|
|
64
|
+
export function isOAuthEnabled(config) {
|
|
65
|
+
return getMcpAccessOptions(config).oauth.enabled;
|
|
66
|
+
}
|
|
67
|
+
/** The app's extra keys under the generated ones (`oauth.metadata`). */
|
|
68
|
+
function readExtension(extension, origins) {
|
|
69
|
+
if (extension === undefined)
|
|
70
|
+
return {};
|
|
71
|
+
return typeof extension === "function" ? extension(origins) : extension;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Authorization server metadata (RFC 8414 §2), with the app's `oauth.metadata.authorizationServer`
|
|
75
|
+
* keys added; generated keys win. Tokens are opaque: no OpenID.
|
|
76
|
+
*/
|
|
77
|
+
export function getAuthorizationServerMetadata(config, origins = resolveMcpOrigins(config)) {
|
|
78
|
+
const issuer = getOAuthIssuer(config, origins);
|
|
79
|
+
const routes = getMcpAccessRoutes(config);
|
|
80
|
+
return {
|
|
81
|
+
...readExtension(getMcpAccessOptions(config).oauth.metadata.authorizationServer, origins),
|
|
82
|
+
issuer,
|
|
83
|
+
authorization_endpoint: `${issuer}${routes.oauthConsent}`,
|
|
84
|
+
token_endpoint: `${issuer}${routes.oauthToken}`,
|
|
85
|
+
registration_endpoint: `${issuer}${routes.oauthRegister}`,
|
|
86
|
+
scopes_supported: [...OAUTH_SCOPES],
|
|
87
|
+
response_types_supported: ["code"],
|
|
88
|
+
response_modes_supported: ["query"],
|
|
89
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
90
|
+
token_endpoint_auth_methods_supported: ["none", "client_secret_post", "client_secret_basic"],
|
|
91
|
+
// MCP clients refuse an authorization server that does not list S256.
|
|
92
|
+
code_challenge_methods_supported: [PKCE_METHOD],
|
|
93
|
+
authorization_response_iss_parameter_supported: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Protected resource metadata (RFC 9728 §2), with the app's `oauth.metadata.protectedResource`
|
|
98
|
+
* keys added; generated keys win, except the app's `resource_name`. At the endpoint's own path the
|
|
99
|
+
* resource is the endpoint URL; at the root it is the origin the document was asked on
|
|
100
|
+
* (`servedOrigin`, one of `resourceOrigins`) or else the app origin, because RFC 9728 §3.3 ties
|
|
101
|
+
* `resource` to the URL the metadata URL was built from.
|
|
102
|
+
*/
|
|
103
|
+
export function getProtectedResourceMetadata(config, variant, origins = resolveMcpOrigins(config), servedOrigin = null) {
|
|
104
|
+
const issuer = getOAuthIssuer(config, origins);
|
|
105
|
+
return {
|
|
106
|
+
resource_name: getMcpAccessOptions(config).serverName,
|
|
107
|
+
...readExtension(getMcpAccessOptions(config).oauth.metadata.protectedResource, origins),
|
|
108
|
+
resource: variant === "endpoint" ? getMcpEndpointUrl(config, origins) : (servedOrigin ?? issuer),
|
|
109
|
+
authorization_servers: [issuer],
|
|
110
|
+
scopes_supported: [...OAUTH_SCOPES],
|
|
111
|
+
bearer_methods_supported: ["header"],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** `GET` of a discovery document for `request` (its origins); 404 while OAuth is off. */
|
|
115
|
+
export function serveDiscoveryDocument(config, document, request = null) {
|
|
116
|
+
if (!isOAuthEnabled(config))
|
|
117
|
+
return Response.json({ error: "not_found" }, { status: 404, headers: CORS_HEADERS });
|
|
118
|
+
return Response.json(document(config, resolveMcpOrigins(config, request), request), { headers: { ...CORS_HEADERS, ...DISCOVERY_CACHE } });
|
|
119
|
+
}
|
|
120
|
+
/** `serveDiscoveryDocument`'s builder for the root protected resource metadata, on the host it was asked on. */
|
|
121
|
+
export const getRootProtectedResourceMetadata = (config, origins, request) => getProtectedResourceMetadata(config, "root", origins, request === null ? null : findServedResourceOrigin(origins, request));
|
|
122
|
+
/**
|
|
123
|
+
* Whether `resource` (RFC 8707) names this server: absent (clients from before RFC 8707), the
|
|
124
|
+
* endpoint URL, the app origin or one of `resourceOrigins`, each with or without a trailing slash.
|
|
125
|
+
*/
|
|
126
|
+
export function isAcceptableResource(config, resource, origins = resolveMcpOrigins(config)) {
|
|
127
|
+
if (resource === null || resource === "")
|
|
128
|
+
return true;
|
|
129
|
+
const endpoint = getMcpEndpointUrl(config, origins).replace(/\/+$/, "");
|
|
130
|
+
return [endpoint, getOAuthIssuer(config, origins), ...origins.resourceOrigins].some((accepted) => resource === accepted || resource === `${accepted}/`);
|
|
131
|
+
}
|
|
132
|
+
/** Whether the client asks to write. Unknown scopes are ignored (RFC 6749 §3.3); every grant reads. */
|
|
133
|
+
export function isWriteScopeRequested(scope) {
|
|
134
|
+
return (scope ?? "").split(/\s+/).includes(MCP_WRITE_SCOPE);
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------------------------
|
|
137
|
+
// Rate limit
|
|
138
|
+
/** A missing bucket is a setup bug: thrown, so it reaches the log by name instead of a quiet 503. */
|
|
139
|
+
function assertOAuthRateLimitBucket(config) {
|
|
140
|
+
const options = getModule(config, "security")?.options;
|
|
141
|
+
if (options?.buckets?.[MCP_OAUTH_RATE_LIMIT_BUCKET] === undefined) {
|
|
142
|
+
throw new Error(`@softure-ai/mcp-access: the security module has no "${MCP_OAUTH_RATE_LIMIT_BUCKET}" bucket; spread MCP_RATE_LIMIT_BUCKETS into security({ buckets })`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Counts the request in the OAuth bucket before any work: per client address, and for the token
|
|
147
|
+
* endpoint per address and client id (an assistant platform refreshes the tokens of all its users
|
|
148
|
+
* from a few shared addresses, and each connection registers its own client). A failing counter
|
|
149
|
+
* closes (503) and says nothing.
|
|
150
|
+
*/
|
|
151
|
+
async function limitProtocolRequest(ctx, request, clientId = null) {
|
|
152
|
+
assertOAuthRateLimitBucket(ctx.config);
|
|
153
|
+
const client = identifyClient(ctx, request.headers);
|
|
154
|
+
if (!client.ok)
|
|
155
|
+
return protocolError("invalid_request", "The client address could not be identified.", 400);
|
|
156
|
+
const key = clientId === null ? client.value : `${client.value}|${clientId.slice(0, 64)}`;
|
|
157
|
+
try {
|
|
158
|
+
const limit = await consumeRateLimit(ctx, { bucket: MCP_OAUTH_RATE_LIMIT_BUCKET, key });
|
|
159
|
+
if (limit.ok)
|
|
160
|
+
return null;
|
|
161
|
+
return protocolError("too_many_requests", "Too many requests; try again later.", 429, { "retry-after": String(limit.retryAfterSeconds) });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error(`@softure-ai/mcp-access: counting an OAuth request failed: ${errorLogLabel(error)}`);
|
|
165
|
+
return protocolError("temporarily_unavailable", "The server is temporarily unavailable.", 503);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ---------------------------------------------------------------------------------------------
|
|
169
|
+
// Registration
|
|
170
|
+
/**
|
|
171
|
+
* `POST` dynamic client registration (RFC 7591). Public by definition: claude.ai and ChatGPT
|
|
172
|
+
* register before anyone signs in. A registration grants nothing; access needs a person's consent.
|
|
173
|
+
*/
|
|
174
|
+
export async function handleClientRegistration(ctx, request) {
|
|
175
|
+
if (!isOAuthEnabled(ctx.config))
|
|
176
|
+
return protocolError("not_found", "OAuth is not enabled.", 404);
|
|
177
|
+
const limited = await limitProtocolRequest(ctx, request);
|
|
178
|
+
if (limited !== null)
|
|
179
|
+
return limited;
|
|
180
|
+
let body;
|
|
181
|
+
try {
|
|
182
|
+
body = await request.json();
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return protocolError("invalid_client_metadata", "The request body must be a JSON object.");
|
|
186
|
+
}
|
|
187
|
+
const parsed = parseClientRegistration(body);
|
|
188
|
+
if (!parsed.ok)
|
|
189
|
+
return protocolError(parsed.error.error, parsed.error.description);
|
|
190
|
+
try {
|
|
191
|
+
const { client, clientSecret } = await registerMcpClient(ctx, parsed.value);
|
|
192
|
+
return protocolJson({
|
|
193
|
+
client_id: client.clientId,
|
|
194
|
+
client_id_issued_at: Math.floor(client.createdAt.getTime() / 1000),
|
|
195
|
+
client_name: client.clientName,
|
|
196
|
+
redirect_uris: client.redirectUris,
|
|
197
|
+
token_endpoint_auth_method: client.tokenEndpointAuthMethod,
|
|
198
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
199
|
+
response_types: ["code"],
|
|
200
|
+
...(clientSecret === null ? {} : { client_secret: clientSecret, client_secret_expires_at: 0 }),
|
|
201
|
+
}, 201);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
console.error(`@softure-ai/mcp-access: registering an OAuth client failed: ${errorLogLabel(error)}`);
|
|
205
|
+
return protocolError("server_error", "The client could not be registered.", 500);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** `Basic` (RFC 6749 §2.3.1: both parts form-urlencoded) or the body fields; a public client sends its id only. */
|
|
209
|
+
function readClientCredentials(request, form) {
|
|
210
|
+
const header = request.headers.get("authorization");
|
|
211
|
+
if (header?.toLowerCase().startsWith("basic ") === true) {
|
|
212
|
+
try {
|
|
213
|
+
const decoded = Buffer.from(header.slice(6).trim(), "base64").toString("utf8");
|
|
214
|
+
const separator = decoded.indexOf(":");
|
|
215
|
+
if (separator > 0) {
|
|
216
|
+
const decode = (part) => decodeURIComponent(part.replace(/\+/g, " "));
|
|
217
|
+
return { clientId: decode(decoded.slice(0, separator)), clientSecret: decode(decoded.slice(separator + 1)), isBasic: true };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// A broken header is no credentials; the caller refuses.
|
|
222
|
+
}
|
|
223
|
+
return { clientId: null, clientSecret: null, isBasic: true };
|
|
224
|
+
}
|
|
225
|
+
return { clientId: form.get("client_id"), clientSecret: form.get("client_secret"), isBasic: false };
|
|
226
|
+
}
|
|
227
|
+
function tokenResponse(tokens) {
|
|
228
|
+
return protocolJson({
|
|
229
|
+
access_token: tokens.accessToken,
|
|
230
|
+
token_type: "Bearer",
|
|
231
|
+
expires_in: tokens.expiresIn,
|
|
232
|
+
refresh_token: tokens.refreshToken,
|
|
233
|
+
scope: tokens.canWrite ? `${MCP_READ_SCOPE} ${MCP_WRITE_SCOPE}` : MCP_READ_SCOPE,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** One answer for every refused code or refresh token: the client restarts either way. */
|
|
237
|
+
const INVALID_GRANT = "The code or refresh token is invalid, expired, used or issued to another client.";
|
|
238
|
+
/**
|
|
239
|
+
* `POST` token endpoint (RFC 6749 §3.2): a code with its PKCE verifier, or a refresh token with
|
|
240
|
+
* rotation. Secrets come in the body, never in the URL.
|
|
241
|
+
*/
|
|
242
|
+
export async function handleTokenRequest(ctx, request) {
|
|
243
|
+
if (!isOAuthEnabled(ctx.config))
|
|
244
|
+
return protocolError("not_found", "OAuth is not enabled.", 404);
|
|
245
|
+
const origins = resolveMcpOrigins(ctx.config, request);
|
|
246
|
+
const form = new URLSearchParams(await request.text().catch(() => ""));
|
|
247
|
+
const credentials = readClientCredentials(request, form);
|
|
248
|
+
const limited = await limitProtocolRequest(ctx, request, credentials.clientId);
|
|
249
|
+
if (limited !== null)
|
|
250
|
+
return limited;
|
|
251
|
+
try {
|
|
252
|
+
const client = credentials.clientId === null ? null : await findOAuthClient(ctx, credentials.clientId);
|
|
253
|
+
if (client === null || !isClientSecretValid(client, credentials.clientSecret === "" ? null : credentials.clientSecret)) {
|
|
254
|
+
return protocolError("invalid_client", "Unknown client or wrong client credentials.", 401, credentials.isBasic ? { "www-authenticate": 'Basic realm="mcp"' } : {});
|
|
255
|
+
}
|
|
256
|
+
return await answerGrant(ctx, { form, client, origins });
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error(`@softure-ai/mcp-access: the OAuth token endpoint failed: ${errorLogLabel(error)}`);
|
|
260
|
+
return protocolError("server_error", "The token could not be issued.", 500);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function answerGrant(ctx, { form, client, origins }) {
|
|
264
|
+
if (!isAcceptableResource(ctx.config, form.get("resource"), origins)) {
|
|
265
|
+
return protocolError("invalid_target", "The resource parameter does not name this MCP server.");
|
|
266
|
+
}
|
|
267
|
+
const grantType = form.get("grant_type");
|
|
268
|
+
if (grantType === "authorization_code") {
|
|
269
|
+
const code = form.get("code");
|
|
270
|
+
const codeVerifier = form.get("code_verifier");
|
|
271
|
+
if (code === null || code === "" || codeVerifier === null || codeVerifier === "") {
|
|
272
|
+
return protocolError("invalid_request", "code and code_verifier (PKCE) are required.");
|
|
273
|
+
}
|
|
274
|
+
// A missing redirect_uri is allowed when the client registered exactly one (RFC 6749 §4.1.3).
|
|
275
|
+
const tokens = await exchangeAuthorizationCode(ctx, { code, client, redirectUri: form.get("redirect_uri"), codeVerifier });
|
|
276
|
+
return tokens === null ? protocolError("invalid_grant", INVALID_GRANT) : tokenResponse(tokens);
|
|
277
|
+
}
|
|
278
|
+
if (grantType === "refresh_token") {
|
|
279
|
+
const refreshToken = form.get("refresh_token");
|
|
280
|
+
if (refreshToken === null || refreshToken === "")
|
|
281
|
+
return protocolError("invalid_request", "refresh_token is required.");
|
|
282
|
+
const tokens = await refreshMcpGrant(ctx, { refreshToken, client });
|
|
283
|
+
return tokens === null ? protocolError("invalid_grant", INVALID_GRANT) : tokenResponse(tokens);
|
|
284
|
+
}
|
|
285
|
+
return protocolError("unsupported_grant_type", "Supported grant types: authorization_code and refresh_token.");
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The client's redirect URI with parameters added, keeping the query it registered (RFC 6749
|
|
289
|
+
* §3.1.2), and always `iss` (RFC 9207), so the client knows the answer came from this server.
|
|
290
|
+
*/
|
|
291
|
+
export function buildClientRedirect(config, redirectUri, params, origins = resolveMcpOrigins(config)) {
|
|
292
|
+
const url = new URL(redirectUri);
|
|
293
|
+
for (const [key, value] of Object.entries({ ...params, iss: getOAuthIssuer(config, origins) })) {
|
|
294
|
+
if (value !== null)
|
|
295
|
+
url.searchParams.set(key, value);
|
|
296
|
+
}
|
|
297
|
+
return url.toString();
|
|
298
|
+
}
|
|
299
|
+
/** The authorization parameters of a query string or form, each at most 2048 characters. */
|
|
300
|
+
export function readAuthorizationParams(source) {
|
|
301
|
+
const params = new URLSearchParams();
|
|
302
|
+
for (const name of AUTHORIZATION_PARAMS) {
|
|
303
|
+
const value = source.get(name);
|
|
304
|
+
if (typeof value === "string" && value.length <= MAX_PARAM_LENGTH)
|
|
305
|
+
params.set(name, value);
|
|
306
|
+
}
|
|
307
|
+
return params;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Validates an authorization request (RFC 6749 §4.1.1), for the consent page and the decision.
|
|
311
|
+
* Order is a security rule (RFC 6749 §4.1.2.1): until the client and its redirect URI are
|
|
312
|
+
* confirmed, an error is shown to the person, never redirected, or the server would redirect
|
|
313
|
+
* anyone anywhere. Only then do errors go back to the client. `origins` are the request's
|
|
314
|
+
* (the consent page's and the decision's must agree); without them, the configured ones.
|
|
315
|
+
*/
|
|
316
|
+
export async function validateAuthorizationRequest(ctx, params, origins = resolveMcpOrigins(ctx.config)) {
|
|
317
|
+
const clientId = params.get("client_id");
|
|
318
|
+
const client = clientId === null ? null : await findOAuthClient(ctx, clientId);
|
|
319
|
+
if (client === null)
|
|
320
|
+
return { kind: "show-error", reason: "unknown_client" };
|
|
321
|
+
const redirectUri = params.get("redirect_uri") ?? (client.redirectUris.length === 1 ? (client.redirectUris[0] ?? null) : null);
|
|
322
|
+
if (redirectUri === null || !client.redirectUris.includes(redirectUri))
|
|
323
|
+
return { kind: "show-error", reason: "redirect_mismatch" };
|
|
324
|
+
const state = params.get("state");
|
|
325
|
+
const back = (error, description) => ({
|
|
326
|
+
kind: "redirect-error",
|
|
327
|
+
redirectUri,
|
|
328
|
+
location: buildClientRedirect(ctx.config, redirectUri, { error, error_description: description, state }, origins),
|
|
329
|
+
});
|
|
330
|
+
if (params.get("response_type") !== "code")
|
|
331
|
+
return back("unsupported_response_type", "Only response_type=code is supported.");
|
|
332
|
+
const codeChallenge = params.get("code_challenge");
|
|
333
|
+
if (codeChallenge === null || !isValidCodeChallenge(codeChallenge) || params.get("code_challenge_method") !== PKCE_METHOD) {
|
|
334
|
+
return back("invalid_request", "PKCE is required: code_challenge with code_challenge_method S256.");
|
|
335
|
+
}
|
|
336
|
+
if (!isAcceptableResource(ctx.config, params.get("resource"), origins))
|
|
337
|
+
return back("invalid_target", "The resource parameter does not name this MCP server.");
|
|
338
|
+
return { kind: "valid", request: { client, redirectUri, state, codeChallenge, isWriteRequested: isWriteScopeRequested(params.get("scope")) } };
|
|
339
|
+
}
|
|
340
|
+
/** The consent page with the request's parameters: where the decision sends the person back. */
|
|
341
|
+
export function getConsentPath(config, params) {
|
|
342
|
+
return `${getMcpAccessRoutes(config).oauthConsent}?${params.toString()}`;
|
|
343
|
+
}
|
|
344
|
+
function seeOther(location) {
|
|
345
|
+
return new Response(null, { status: 303, headers: { location, "cache-control": "no-store" } });
|
|
346
|
+
}
|
|
347
|
+
/** Whether the form was posted from this app: `Origin` must be the request's app origin. */
|
|
348
|
+
function isSameOrigin(request, origins) {
|
|
349
|
+
const origin = request.headers.get("origin");
|
|
350
|
+
return origin !== null && origin === origins.appOrigin;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* `POST` of the consent form, answered with `303`: a plain form post, not a server action, because
|
|
354
|
+
* the answer is a redirect to the client, possibly a native app scheme, and must work without
|
|
355
|
+
* JavaScript. The form must come from the app's origin (the session cookie's `SameSite=Lax` is the
|
|
356
|
+
* first layer), and the request is validated again from the posted fields. `userId` is the
|
|
357
|
+
* session's user, or null: then the person goes back to the consent page, which asks them to sign in.
|
|
358
|
+
*/
|
|
359
|
+
export async function handleAuthorizationDecision(ctx, request, userId) {
|
|
360
|
+
if (!isOAuthEnabled(ctx.config))
|
|
361
|
+
return new Response(null, { status: 404 });
|
|
362
|
+
const origins = resolveMcpOrigins(ctx.config, request);
|
|
363
|
+
if (!isSameOrigin(request, origins))
|
|
364
|
+
return new Response(null, { status: 403, headers: { "cache-control": "no-store" } });
|
|
365
|
+
const form = await request.formData().catch(() => null);
|
|
366
|
+
if (form === null)
|
|
367
|
+
return new Response(null, { status: 400, headers: { "cache-control": "no-store" } });
|
|
368
|
+
const params = readAuthorizationParams(form);
|
|
369
|
+
const consent = getConsentPath(ctx.config, params);
|
|
370
|
+
if (userId === null)
|
|
371
|
+
return seeOther(consent);
|
|
372
|
+
try {
|
|
373
|
+
const outcome = await validateAuthorizationRequest(ctx, params, origins);
|
|
374
|
+
if (outcome.kind === "show-error")
|
|
375
|
+
return seeOther(consent);
|
|
376
|
+
if (outcome.kind === "redirect-error")
|
|
377
|
+
return seeOther(outcome.location);
|
|
378
|
+
const { client, redirectUri, state, codeChallenge, isWriteRequested } = outcome.request;
|
|
379
|
+
if (form.get("decision") !== "allow") {
|
|
380
|
+
return seeOther(buildClientRedirect(ctx.config, redirectUri, { error: "access_denied", error_description: "The person denied access.", state }, origins));
|
|
381
|
+
}
|
|
382
|
+
const code = await createAuthorizationCode(ctx, {
|
|
383
|
+
clientRowId: client.id,
|
|
384
|
+
userId,
|
|
385
|
+
redirectUri,
|
|
386
|
+
codeChallenge,
|
|
387
|
+
// Writes need the client's request, the app's allowWrites and the person's tick.
|
|
388
|
+
canWrite: isWriteRequested && getMcpAccessOptions(ctx.config).allowWrites && form.get("canWrite") !== null,
|
|
389
|
+
});
|
|
390
|
+
return seeOther(buildClientRedirect(ctx.config, redirectUri, { code, state }, origins));
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
console.error(`@softure-ai/mcp-access: recording an OAuth decision failed: ${errorLogLabel(error)}`);
|
|
394
|
+
return new Response(null, { status: 500, headers: { "cache-control": "no-store" } });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
//# sourceMappingURL=oauth-http.js.map
|