@bojanrajkovic/mcp-paprika 1.2.0-beta.2 → 1.2.0-beta.3
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/dist/auth/build.d.ts +2 -1
- package/dist/auth/build.js +11 -5
- package/dist/auth/cleanup.d.ts +3 -1
- package/dist/auth/cleanup.js +9 -5
- package/dist/auth/client-registration.d.ts +3 -1
- package/dist/auth/client-registration.js +10 -7
- package/dist/auth/dcr-validator.d.ts +9 -2
- package/dist/auth/dcr-validator.js +51 -37
- package/dist/auth/oidc-client.d.ts +2 -1
- package/dist/auth/oidc-client.js +7 -1
- package/dist/auth/provider.d.ts +3 -1
- package/dist/auth/provider.js +19 -8
- package/dist/auth/routes.d.ts +5 -0
- package/dist/auth/routes.js +9 -5
- package/dist/auth/token-store.d.ts +8 -0
- package/dist/auth/token-store.js +2 -0
- package/dist/auth/types.d.ts +14 -9
- package/dist/cache/disk-cache.d.ts +3 -1
- package/dist/cache/disk-cache.js +12 -10
- package/dist/features/discover-feature.d.ts +2 -1
- package/dist/features/discover-feature.js +7 -7
- package/dist/features/embeddings.d.ts +5 -2
- package/dist/features/embeddings.js +48 -5
- package/dist/features/vector-store.d.ts +3 -1
- package/dist/features/vector-store.js +19 -13
- package/dist/index.js +5 -4
- package/dist/paprika/client.d.ts +6 -1
- package/dist/paprika/client.js +65 -22
- package/dist/paprika/errors.d.ts +4 -2
- package/dist/paprika/errors.js +4 -2
- package/dist/paprika/sync.d.ts +1 -1
- package/dist/paprika/sync.js +16 -27
- package/dist/server/app-context.d.ts +3 -0
- package/dist/server/build.js +27 -14
- package/dist/tools/create.js +3 -3
- package/dist/tools/delete.js +3 -3
- package/dist/tools/pantry-add.js +3 -3
- package/dist/tools/pantry-delete.js +3 -3
- package/dist/tools/pantry-update.js +3 -3
- package/dist/tools/update.js +3 -3
- package/dist/transport/http.d.ts +26 -1
- package/dist/transport/http.js +53 -12
- package/dist/transport/stdio.js +5 -6
- package/dist/utils/config.d.ts +80 -38
- package/dist/utils/config.js +37 -5
- package/dist/utils/errors.d.ts +38 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/log.d.ts +47 -18
- package/dist/utils/log.js +199 -20
- package/package.json +3 -1
package/dist/auth/build.d.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Called once per process from buildAppContext (src/server/build.ts)
|
|
10
10
|
* after cache.init() completes.
|
|
11
11
|
*/
|
|
12
|
+
import type { Logger } from "pino";
|
|
12
13
|
import type { DiskCache } from "../cache/disk-cache.js";
|
|
13
14
|
import type { PaprikaConfig } from "../utils/config.js";
|
|
14
15
|
import type { AuthContext } from "./types.js";
|
|
15
|
-
export declare function buildAuthContext(config: PaprikaConfig, cache: DiskCache): Promise<AuthContext | null>;
|
|
16
|
+
export declare function buildAuthContext(config: PaprikaConfig, cache: DiskCache, parentLog: Logger): Promise<AuthContext | null>;
|
package/dist/auth/build.js
CHANGED
|
@@ -18,7 +18,7 @@ import { AuthCodeStore } from "./auth-code-store.js";
|
|
|
18
18
|
import { MintingOAuthServerProvider } from "./provider.js";
|
|
19
19
|
import { AuthCleanup } from "./cleanup.js";
|
|
20
20
|
import { MAX_REGISTERED_CLIENTS } from "./routes.js";
|
|
21
|
-
export async function buildAuthContext(config, cache) {
|
|
21
|
+
export async function buildAuthContext(config, cache, parentLog) {
|
|
22
22
|
if (config.transport !== "http")
|
|
23
23
|
return null;
|
|
24
24
|
if (config.oauth === undefined) {
|
|
@@ -61,15 +61,17 @@ export async function buildAuthContext(config, cache) {
|
|
|
61
61
|
trustProxy: config.oauth.trustProxy,
|
|
62
62
|
allowlist: config.oauth.allowlist,
|
|
63
63
|
};
|
|
64
|
+
const authLog = parentLog.child({ component: "auth" });
|
|
65
|
+
const oidcClientLog = parentLog.child({ component: "oidc-client" });
|
|
64
66
|
// Fetch + validate upstream discovery document (rejects http:// endpoints; checks alg overlap)
|
|
65
|
-
const discovery = await loadDiscovery(resolved.discoveryUrl, resolved.allowedAlgs);
|
|
67
|
+
const discovery = await loadDiscovery(resolved.discoveryUrl, resolved.allowedAlgs, oidcClientLog);
|
|
66
68
|
const jwks = createJwksFor(discovery);
|
|
67
|
-
const clientStore = new DiskClientRegistrationStore(cache, resolved.publicUrl, MAX_REGISTERED_CLIENTS);
|
|
69
|
+
const clientStore = new DiskClientRegistrationStore(cache, resolved.publicUrl, authLog, MAX_REGISTERED_CLIENTS);
|
|
68
70
|
const tokenStore = new TokenStore(cache);
|
|
69
71
|
const requestStore = new AuthRequestStore();
|
|
70
72
|
const codeStore = new AuthCodeStore();
|
|
71
|
-
const provider = new MintingOAuthServerProvider(clientStore, tokenStore, requestStore, codeStore, discovery, resolved, resolved.publicUrl);
|
|
72
|
-
const cleanup = new AuthCleanup(clientStore, tokenStore, cache, requestStore, codeStore);
|
|
73
|
+
const provider = new MintingOAuthServerProvider(clientStore, tokenStore, requestStore, codeStore, discovery, resolved, resolved.publicUrl, authLog);
|
|
74
|
+
const cleanup = new AuthCleanup(clientStore, tokenStore, cache, requestStore, codeStore, authLog);
|
|
73
75
|
return {
|
|
74
76
|
provider,
|
|
75
77
|
config: resolved,
|
|
@@ -80,5 +82,9 @@ export async function buildAuthContext(config, cache) {
|
|
|
80
82
|
tokenStore,
|
|
81
83
|
clientStore,
|
|
82
84
|
cleanup,
|
|
85
|
+
log: {
|
|
86
|
+
auth: authLog,
|
|
87
|
+
oidcClient: oidcClientLog,
|
|
88
|
+
},
|
|
83
89
|
};
|
|
84
90
|
}
|
package/dist/auth/cleanup.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* Public `sweepOnce()` is exposed for direct testing and for startup use.
|
|
17
17
|
*/
|
|
18
|
+
import type { Logger } from "pino";
|
|
18
19
|
import type { DiskCache } from "../cache/disk-cache.js";
|
|
19
20
|
import type { AuthRequestStore } from "./auth-request-store.js";
|
|
20
21
|
import type { AuthCodeStore } from "./auth-code-store.js";
|
|
@@ -26,10 +27,11 @@ export declare class AuthCleanup {
|
|
|
26
27
|
private readonly _cache;
|
|
27
28
|
private readonly _authRequests;
|
|
28
29
|
private readonly _authCodes;
|
|
30
|
+
private readonly log;
|
|
29
31
|
private readonly _now;
|
|
30
32
|
private readonly _intervalMs;
|
|
31
33
|
private _ac;
|
|
32
|
-
constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _cache: DiskCache, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, _now?: () => number, _intervalMs?: number);
|
|
34
|
+
constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _cache: DiskCache, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, log: Logger, _now?: () => number, _intervalMs?: number);
|
|
33
35
|
/** Start the background cleanup loop. Idempotent — second call is a no-op. */
|
|
34
36
|
start(): void;
|
|
35
37
|
/** Stop the background cleanup loop. Idempotent — second call is a no-op. */
|
package/dist/auth/cleanup.js
CHANGED
|
@@ -24,15 +24,17 @@ export class AuthCleanup {
|
|
|
24
24
|
_cache;
|
|
25
25
|
_authRequests;
|
|
26
26
|
_authCodes;
|
|
27
|
+
log;
|
|
27
28
|
_now;
|
|
28
29
|
_intervalMs;
|
|
29
30
|
_ac = null;
|
|
30
|
-
constructor(_clientStore, _tokenStore, _cache, _authRequests, _authCodes, _now = () => nowSeconds(), _intervalMs = CLEANUP_INTERVAL_MS) {
|
|
31
|
+
constructor(_clientStore, _tokenStore, _cache, _authRequests, _authCodes, log, _now = () => nowSeconds(), _intervalMs = CLEANUP_INTERVAL_MS) {
|
|
31
32
|
this._clientStore = _clientStore;
|
|
32
33
|
this._tokenStore = _tokenStore;
|
|
33
34
|
this._cache = _cache;
|
|
34
35
|
this._authRequests = _authRequests;
|
|
35
36
|
this._authCodes = _authCodes;
|
|
37
|
+
this.log = log;
|
|
36
38
|
this._now = _now;
|
|
37
39
|
this._intervalMs = _intervalMs;
|
|
38
40
|
}
|
|
@@ -112,14 +114,16 @@ export class AuthCleanup {
|
|
|
112
114
|
try {
|
|
113
115
|
await this.sweepOnce();
|
|
114
116
|
}
|
|
115
|
-
catch {
|
|
116
|
-
|
|
117
|
+
catch (err) {
|
|
118
|
+
this.log.debug({ err }, "auth cleanup sweep failed; continuing");
|
|
117
119
|
}
|
|
118
120
|
try {
|
|
119
121
|
await wait(this._intervalMs, undefined, { signal: this._ac.signal });
|
|
120
122
|
}
|
|
121
|
-
catch {
|
|
122
|
-
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (!(err instanceof Error && err.name === "AbortError")) {
|
|
125
|
+
this.log.debug({ err }, "auth cleanup wait failed unexpectedly");
|
|
126
|
+
}
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
129
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - deleteClient: DELETE /register/{client_id} — removes from disk (cascade handled by caller)
|
|
10
10
|
* - verifyRegistrationAccessToken: used by route handlers to gate PUT/DELETE access
|
|
11
11
|
*/
|
|
12
|
+
import type { Logger } from "pino";
|
|
12
13
|
import { DiskCache } from "../cache/disk-cache.js";
|
|
13
14
|
/**
|
|
14
15
|
* Wire format for client information (RFC 7591 response format).
|
|
@@ -30,6 +31,7 @@ interface OAuthClientInformationFull {
|
|
|
30
31
|
export declare class DiskClientRegistrationStore {
|
|
31
32
|
private readonly _cache;
|
|
32
33
|
private readonly _publicUrl;
|
|
34
|
+
private readonly log;
|
|
33
35
|
/**
|
|
34
36
|
* Hard cap on the number of registered clients. Enforced atomically
|
|
35
37
|
* inside `registerClient` (via `DiskCache.tryPutOAuthClient`) so concurrent
|
|
@@ -39,7 +41,7 @@ export declare class DiskClientRegistrationStore {
|
|
|
39
41
|
* middleware's fast-path 429 limit).
|
|
40
42
|
*/
|
|
41
43
|
private readonly _maxClients;
|
|
42
|
-
constructor(_cache: DiskCache, _publicUrl: string,
|
|
44
|
+
constructor(_cache: DiskCache, _publicUrl: string, log: Logger,
|
|
43
45
|
/**
|
|
44
46
|
* Hard cap on the number of registered clients. Enforced atomically
|
|
45
47
|
* inside `registerClient` (via `DiskCache.tryPutOAuthClient`) so concurrent
|
|
@@ -41,8 +41,9 @@ function storedToWire(stored, extras) {
|
|
|
41
41
|
export class DiskClientRegistrationStore {
|
|
42
42
|
_cache;
|
|
43
43
|
_publicUrl;
|
|
44
|
+
log;
|
|
44
45
|
_maxClients;
|
|
45
|
-
constructor(_cache, _publicUrl,
|
|
46
|
+
constructor(_cache, _publicUrl, log,
|
|
46
47
|
/**
|
|
47
48
|
* Hard cap on the number of registered clients. Enforced atomically
|
|
48
49
|
* inside `registerClient` (via `DiskCache.tryPutOAuthClient`) so concurrent
|
|
@@ -54,6 +55,7 @@ export class DiskClientRegistrationStore {
|
|
|
54
55
|
_maxClients = Number.POSITIVE_INFINITY) {
|
|
55
56
|
this._cache = _cache;
|
|
56
57
|
this._publicUrl = _publicUrl;
|
|
58
|
+
this.log = log;
|
|
57
59
|
this._maxClients = _maxClients;
|
|
58
60
|
}
|
|
59
61
|
/**
|
|
@@ -72,8 +74,8 @@ export class DiskClientRegistrationStore {
|
|
|
72
74
|
* Throws OAuthMetadataValidationError on invalid metadata.
|
|
73
75
|
*/
|
|
74
76
|
async registerClient(metaIn) {
|
|
75
|
-
// Validate via dcr-validator;
|
|
76
|
-
const validated = validateRegistration(metaIn).match((v) => v, (e) => {
|
|
77
|
+
// Validate via dcr-validator; pass logger for URL-parse debug diagnosability
|
|
78
|
+
const validated = validateRegistration(metaIn, this.log).match((v) => v, (e) => {
|
|
77
79
|
throw e;
|
|
78
80
|
});
|
|
79
81
|
const clientId = randomUUID();
|
|
@@ -104,6 +106,7 @@ export class DiskClientRegistrationStore {
|
|
|
104
106
|
throw new InvalidRequestError(`client registration cap reached (${result.currentCount.toString()} clients)`);
|
|
105
107
|
}
|
|
106
108
|
await this._cache.flush();
|
|
109
|
+
this.log.info({ clientId: stored.clientId, redirectUriCount: stored.redirectUris.length }, "client registered via DCR");
|
|
107
110
|
return storedToWire(stored, {
|
|
108
111
|
registrationAccessToken,
|
|
109
112
|
registrationClientUri: `${this._publicUrl}/register/${clientId}`,
|
|
@@ -120,8 +123,8 @@ export class DiskClientRegistrationStore {
|
|
|
120
123
|
const existing = await this._cache.getOAuthClient(clientId);
|
|
121
124
|
if (existing === null)
|
|
122
125
|
throw OAuthClientNotFoundError.forId(clientId);
|
|
123
|
-
// Validate patch via dcr-validator
|
|
124
|
-
const validated = validateUpdate(metaIn).match((v) => v, (e) => {
|
|
126
|
+
// Validate patch via dcr-validator; pass logger for URL-parse debug diagnosability
|
|
127
|
+
const validated = validateUpdate(metaIn, this.log).match((v) => v, (e) => {
|
|
125
128
|
throw e;
|
|
126
129
|
});
|
|
127
130
|
const now = nowSeconds();
|
|
@@ -168,8 +171,8 @@ export class DiskClientRegistrationStore {
|
|
|
168
171
|
try {
|
|
169
172
|
return timingSafeEqual(Buffer.from(presentedHash, "hex"), Buffer.from(storedHash, "hex"));
|
|
170
173
|
}
|
|
171
|
-
catch {
|
|
172
|
-
|
|
174
|
+
catch (err) {
|
|
175
|
+
this.log.debug({ err, clientId }, "RAT timing-safe equality failed (likely invalid hex)");
|
|
173
176
|
return false;
|
|
174
177
|
}
|
|
175
178
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - scope preserved but non-empty
|
|
11
11
|
*/
|
|
12
12
|
import type { Result } from "neverthrow";
|
|
13
|
+
import type { Logger } from "pino";
|
|
13
14
|
import { OAuthMetadataValidationError } from "./errors.js";
|
|
14
15
|
export interface ValidatedClientMetadata {
|
|
15
16
|
readonly tokenEndpointAuthMethod: "none";
|
|
@@ -36,12 +37,18 @@ export interface ValidatedClientMetadataPatch {
|
|
|
36
37
|
* grant_types defaults to ["authorization_code", "refresh_token"].
|
|
37
38
|
* response_types defaults to ["code"].
|
|
38
39
|
* scope defaults to empty string.
|
|
40
|
+
*
|
|
41
|
+
* @param log — optional logger; when provided, URL parse failures on redirect_uris
|
|
42
|
+
* emit debug records for diagnosability in production.
|
|
39
43
|
*/
|
|
40
|
-
export declare function validateRegistration(meta: unknown): Result<ValidatedClientMetadata, OAuthMetadataValidationError>;
|
|
44
|
+
export declare function validateRegistration(meta: unknown, log?: Logger): Result<ValidatedClientMetadata, OAuthMetadataValidationError>;
|
|
41
45
|
/**
|
|
42
46
|
* Validates client metadata for RFC 7592 updates.
|
|
43
47
|
* All fields are optional (partial updates per RFC 7592 §2.2).
|
|
44
48
|
* Present fields pass the same validation as registration.
|
|
45
49
|
* Omitted fields are NOT synthesized in the output (unlike registration).
|
|
50
|
+
*
|
|
51
|
+
* @param log — optional logger; when provided, URL parse failures on redirect_uris
|
|
52
|
+
* emit debug records for diagnosability in production.
|
|
46
53
|
*/
|
|
47
|
-
export declare function validateUpdate(meta: unknown): Result<ValidatedClientMetadataPatch, OAuthMetadataValidationError>;
|
|
54
|
+
export declare function validateUpdate(meta: unknown, log?: Logger): Result<ValidatedClientMetadataPatch, OAuthMetadataValidationError>;
|
|
@@ -16,7 +16,7 @@ import { OAuthMetadataValidationError } from "./errors.js";
|
|
|
16
16
|
// Validation Logic
|
|
17
17
|
// ============================================================================
|
|
18
18
|
// Helper: validate redirect URI scheme and hostname
|
|
19
|
-
function isValidRedirectUri(uri) {
|
|
19
|
+
function isValidRedirectUri(uri, log) {
|
|
20
20
|
try {
|
|
21
21
|
const url = new URL(uri);
|
|
22
22
|
// https is always OK
|
|
@@ -33,7 +33,8 @@ function isValidRedirectUri(uri) {
|
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
catch {
|
|
36
|
+
catch (err) {
|
|
37
|
+
log?.debug({ err, uri }, "invalid redirect_uri rejected by parser");
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
39
40
|
}
|
|
@@ -48,7 +49,7 @@ const ClientMetadataFieldsSchema = z.object({
|
|
|
48
49
|
id_token_signed_response_alg: z.enum(["RS256", "ES256"]).optional(),
|
|
49
50
|
});
|
|
50
51
|
// Shared helper: validate each redirect URI item in a non-empty array
|
|
51
|
-
function validateRedirectUriItems(uris, ctx) {
|
|
52
|
+
function validateRedirectUriItems(uris, ctx, log) {
|
|
52
53
|
for (let i = 0; i < uris.length; i++) {
|
|
53
54
|
const uri = uris[i];
|
|
54
55
|
if (typeof uri !== "string") {
|
|
@@ -62,7 +63,8 @@ function validateRedirectUriItems(uris, ctx) {
|
|
|
62
63
|
try {
|
|
63
64
|
new URL(uri);
|
|
64
65
|
}
|
|
65
|
-
catch {
|
|
66
|
+
catch (err) {
|
|
67
|
+
log?.debug({ err, uri, index: i }, "invalid redirect_uri item in DCR request");
|
|
66
68
|
ctx.addIssue({
|
|
67
69
|
code: z.ZodIssueCode.custom,
|
|
68
70
|
path: ["redirect_uris", i],
|
|
@@ -70,7 +72,7 @@ function validateRedirectUriItems(uris, ctx) {
|
|
|
70
72
|
});
|
|
71
73
|
continue;
|
|
72
74
|
}
|
|
73
|
-
if (!isValidRedirectUri(uri)) {
|
|
75
|
+
if (!isValidRedirectUri(uri, log)) {
|
|
74
76
|
ctx.addIssue({
|
|
75
77
|
code: z.ZodIssueCode.custom,
|
|
76
78
|
path: ["redirect_uris", i],
|
|
@@ -103,42 +105,48 @@ function validateGrantTypes(data, ctx) {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
|
-
// Schema for validateRegistration: redirect_uris REQUIRED (must be present and non-empty)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
code: z.ZodIssueCode.custom,
|
|
113
|
-
path: ["redirect_uris"],
|
|
114
|
-
message: "redirect_uris is required and must be a non-empty array of valid URLs",
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
validateRedirectUriItems(data.redirect_uris, ctx);
|
|
119
|
-
}
|
|
120
|
-
validateResponseTypes(data, ctx);
|
|
121
|
-
validateGrantTypes(data, ctx);
|
|
122
|
-
});
|
|
123
|
-
// Schema for validateUpdate: redirect_uris OPTIONAL (whole block skipped when absent)
|
|
124
|
-
const UpdateMetadataSchema = ClientMetadataFieldsSchema.passthrough() // Allow and preserve other RFC 7591 fields
|
|
125
|
-
.superRefine((data, ctx) => {
|
|
126
|
-
// Validate redirect_uris if present: must be non-empty with valid scheme
|
|
127
|
-
if (data.redirect_uris !== undefined) {
|
|
108
|
+
// Schema factory for validateRegistration: redirect_uris REQUIRED (must be present and non-empty).
|
|
109
|
+
// Accepts an optional logger to emit debug records on URL parse failures.
|
|
110
|
+
function makeRegistrationSchema(log) {
|
|
111
|
+
return ClientMetadataFieldsSchema.passthrough() // Allow and preserve other RFC 7591 fields
|
|
112
|
+
.superRefine((data, ctx) => {
|
|
113
|
+
// Validate redirect_uris: must be present, non-empty, valid URLs with our scheme rules
|
|
128
114
|
if (!Array.isArray(data.redirect_uris) || data.redirect_uris.length === 0) {
|
|
129
115
|
ctx.addIssue({
|
|
130
116
|
code: z.ZodIssueCode.custom,
|
|
131
117
|
path: ["redirect_uris"],
|
|
132
|
-
message: "redirect_uris must be a non-empty array
|
|
118
|
+
message: "redirect_uris is required and must be a non-empty array of valid URLs",
|
|
133
119
|
});
|
|
134
120
|
}
|
|
135
121
|
else {
|
|
136
|
-
validateRedirectUriItems(data.redirect_uris, ctx);
|
|
122
|
+
validateRedirectUriItems(data.redirect_uris, ctx, log);
|
|
137
123
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
124
|
+
validateResponseTypes(data, ctx);
|
|
125
|
+
validateGrantTypes(data, ctx);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Schema factory for validateUpdate: redirect_uris OPTIONAL (whole block skipped when absent).
|
|
129
|
+
// Accepts an optional logger to emit debug records on URL parse failures.
|
|
130
|
+
function makeUpdateSchema(log) {
|
|
131
|
+
return ClientMetadataFieldsSchema.passthrough() // Allow and preserve other RFC 7591 fields
|
|
132
|
+
.superRefine((data, ctx) => {
|
|
133
|
+
// Validate redirect_uris if present: must be non-empty with valid scheme
|
|
134
|
+
if (data.redirect_uris !== undefined) {
|
|
135
|
+
if (!Array.isArray(data.redirect_uris) || data.redirect_uris.length === 0) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: z.ZodIssueCode.custom,
|
|
138
|
+
path: ["redirect_uris"],
|
|
139
|
+
message: "redirect_uris must be a non-empty array when present",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
validateRedirectUriItems(data.redirect_uris, ctx, log);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
validateResponseTypes(data, ctx);
|
|
147
|
+
validateGrantTypes(data, ctx);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
142
150
|
// Helper: convert parsed data to ValidatedClientMetadata with defaults for registration
|
|
143
151
|
function buildRegistrationMetadata(data) {
|
|
144
152
|
return {
|
|
@@ -191,10 +199,13 @@ function buildUpdateMetadataPatch(data) {
|
|
|
191
199
|
* grant_types defaults to ["authorization_code", "refresh_token"].
|
|
192
200
|
* response_types defaults to ["code"].
|
|
193
201
|
* scope defaults to empty string.
|
|
202
|
+
*
|
|
203
|
+
* @param log — optional logger; when provided, URL parse failures on redirect_uris
|
|
204
|
+
* emit debug records for diagnosability in production.
|
|
194
205
|
*/
|
|
195
|
-
export function validateRegistration(meta) {
|
|
206
|
+
export function validateRegistration(meta, log) {
|
|
196
207
|
// Parse with Zod schema (includes field validation and redirect_uri scheme checks)
|
|
197
|
-
const parseResult =
|
|
208
|
+
const parseResult = makeRegistrationSchema(log).safeParse(meta);
|
|
198
209
|
if (!parseResult.success) {
|
|
199
210
|
// Translate first Zod error to OAuth error
|
|
200
211
|
const issue = parseResult.error.issues[0];
|
|
@@ -213,10 +224,13 @@ export function validateRegistration(meta) {
|
|
|
213
224
|
* All fields are optional (partial updates per RFC 7592 §2.2).
|
|
214
225
|
* Present fields pass the same validation as registration.
|
|
215
226
|
* Omitted fields are NOT synthesized in the output (unlike registration).
|
|
227
|
+
*
|
|
228
|
+
* @param log — optional logger; when provided, URL parse failures on redirect_uris
|
|
229
|
+
* emit debug records for diagnosability in production.
|
|
216
230
|
*/
|
|
217
|
-
export function validateUpdate(meta) {
|
|
231
|
+
export function validateUpdate(meta, log) {
|
|
218
232
|
// Parse with Zod schema (includes field validation and redirect_uri scheme checks)
|
|
219
|
-
const parseResult =
|
|
233
|
+
const parseResult = makeUpdateSchema(log).safeParse(meta);
|
|
220
234
|
if (!parseResult.success) {
|
|
221
235
|
// Translate first Zod error to OAuth error
|
|
222
236
|
const issue = parseResult.error.issues[0];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `loadDiscovery` is one-shot (no caching); `verifyIdToken` uses jose's built-in JWKS cache.
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
|
+
import type { Logger } from "pino";
|
|
12
13
|
import { type JWTVerifyGetKey } from "jose";
|
|
13
14
|
import { type IdTokenPayload } from "./types.js";
|
|
14
15
|
declare const DiscoveryDocSchema: z.ZodObject<{
|
|
@@ -63,7 +64,7 @@ export type DiscoveryDoc = z.infer<typeof DiscoveryDocSchema>;
|
|
|
63
64
|
* @returns Validated discovery document
|
|
64
65
|
* @throws OAuthMetadataValidationError if validation fails
|
|
65
66
|
*/
|
|
66
|
-
export declare function loadDiscovery(discoveryUrl: string, allowedAlgs: ReadonlyArray<string
|
|
67
|
+
export declare function loadDiscovery(discoveryUrl: string, allowedAlgs: ReadonlyArray<string>, log?: Logger): Promise<DiscoveryDoc>;
|
|
67
68
|
/**
|
|
68
69
|
* Creates a remote JWKS fetcher with caching.
|
|
69
70
|
*
|
package/dist/auth/oidc-client.js
CHANGED
|
@@ -57,12 +57,14 @@ const DiscoveryDocSchema = z.object({
|
|
|
57
57
|
* @returns Validated discovery document
|
|
58
58
|
* @throws OAuthMetadataValidationError if validation fails
|
|
59
59
|
*/
|
|
60
|
-
export async function loadDiscovery(discoveryUrl, allowedAlgs) {
|
|
60
|
+
export async function loadDiscovery(discoveryUrl, allowedAlgs, log) {
|
|
61
61
|
// Step 1: Verify discoveryUrl itself is https://
|
|
62
62
|
const url = new URL(discoveryUrl);
|
|
63
63
|
if (url.protocol !== "https:") {
|
|
64
64
|
throw OAuthMetadataValidationError.nonHttps("discoveryUrl", discoveryUrl);
|
|
65
65
|
}
|
|
66
|
+
const t0 = performance.now();
|
|
67
|
+
log?.debug({ method: "GET", url: url.toString(), attempt: 1 }, "oidc discovery start");
|
|
66
68
|
// Step 2: Fetch with timeout
|
|
67
69
|
let response;
|
|
68
70
|
try {
|
|
@@ -72,10 +74,13 @@ export async function loadDiscovery(discoveryUrl, allowedAlgs) {
|
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
catch (cause) {
|
|
77
|
+
log?.error({ err: cause, method: "GET", url: url.toString(), attempt: 1 }, "oidc discovery fetch failed");
|
|
75
78
|
throw new OAuthMetadataValidationError("failed to fetch OIDC discovery document", { cause });
|
|
76
79
|
}
|
|
80
|
+
const attemptDurationMs = Math.round(performance.now() - t0);
|
|
77
81
|
// Step 3: Check response status
|
|
78
82
|
if (!response.ok) {
|
|
83
|
+
log?.error({ method: "GET", url: url.toString(), attempt: 1, status: response.status, attemptDurationMs }, "oidc discovery returned non-ok");
|
|
79
84
|
throw OAuthMetadataValidationError.discoveryFetchFailed(url.toString(), response.status);
|
|
80
85
|
}
|
|
81
86
|
// Step 4: Parse and validate JSON
|
|
@@ -112,6 +117,7 @@ export async function loadDiscovery(discoveryUrl, allowedAlgs) {
|
|
|
112
117
|
if (overlap.length === 0) {
|
|
113
118
|
throw OAuthMetadataValidationError.noAlgOverlap(doc.id_token_signing_alg_values_supported, Array.from(allowedAlgs));
|
|
114
119
|
}
|
|
120
|
+
log?.debug({ method: "GET", url: url.toString(), attempt: 1, status: response.status, attemptDurationMs }, "oidc discovery ok");
|
|
115
121
|
return doc;
|
|
116
122
|
}
|
|
117
123
|
// ============================================================================
|
package/dist/auth/provider.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { DiskClientRegistrationStore } from "./client-registration.js";
|
|
|
6
6
|
import type { TokenStore } from "./token-store.js";
|
|
7
7
|
import type { AuthRequestStore } from "./auth-request-store.js";
|
|
8
8
|
import type { AuthCodeStore } from "./auth-code-store.js";
|
|
9
|
+
import type { Logger } from "pino";
|
|
9
10
|
import type { Context } from "hono";
|
|
10
11
|
import type { DiscoveryDoc } from "./oidc-client.js";
|
|
11
12
|
import type { ResolvedOAuthConfig } from "./types.js";
|
|
@@ -29,7 +30,8 @@ export declare class MintingOAuthServerProvider implements OAuthServerProvider {
|
|
|
29
30
|
private readonly _discovery;
|
|
30
31
|
private readonly _oidcConfig;
|
|
31
32
|
private readonly _publicUrl;
|
|
32
|
-
|
|
33
|
+
private readonly log;
|
|
34
|
+
constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, _discovery: DiscoveryDoc, _oidcConfig: ResolvedOAuthConfig, _publicUrl: string, log: Logger);
|
|
33
35
|
get clientsStore(): OAuthRegisteredClientsStore;
|
|
34
36
|
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Context | unknown): Promise<void>;
|
|
35
37
|
challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
|
package/dist/auth/provider.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InvalidGrantError, InvalidTokenError, InvalidTargetError, } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
2
|
-
import { generateOpaqueToken, ACCESS_TOKEN_TTL_SECONDS, nowSeconds } from "./tokens.js";
|
|
2
|
+
import { generateOpaqueToken, ACCESS_TOKEN_TTL_SECONDS, nowSeconds, hashTokenForStorage } from "./tokens.js";
|
|
3
3
|
/**
|
|
4
4
|
* Minting OAuth 2.1 server provider implementing OAuthServerProvider.
|
|
5
5
|
*
|
|
@@ -20,7 +20,8 @@ export class MintingOAuthServerProvider {
|
|
|
20
20
|
_discovery;
|
|
21
21
|
_oidcConfig;
|
|
22
22
|
_publicUrl;
|
|
23
|
-
|
|
23
|
+
log;
|
|
24
|
+
constructor(_clientStore, _tokenStore, _authRequests, _authCodes, _discovery, _oidcConfig, _publicUrl, log) {
|
|
24
25
|
this._clientStore = _clientStore;
|
|
25
26
|
this._tokenStore = _tokenStore;
|
|
26
27
|
this._authRequests = _authRequests;
|
|
@@ -28,6 +29,7 @@ export class MintingOAuthServerProvider {
|
|
|
28
29
|
this._discovery = _discovery;
|
|
29
30
|
this._oidcConfig = _oidcConfig;
|
|
30
31
|
this._publicUrl = _publicUrl;
|
|
32
|
+
this.log = log;
|
|
31
33
|
}
|
|
32
34
|
get clientsStore() {
|
|
33
35
|
// Type mismatch: SDK's OAuthRegisteredClientsStore interface requires mutable arrays
|
|
@@ -96,6 +98,8 @@ export class MintingOAuthServerProvider {
|
|
|
96
98
|
scope: state.scope,
|
|
97
99
|
resource: state.resource,
|
|
98
100
|
});
|
|
101
|
+
const tokenHash = hashTokenForStorage(pair.access.plaintext);
|
|
102
|
+
this.log.info({ tokenHash, clientId: state.clientId, sub: state.identity.sub }, "access token minted (authorization_code grant)");
|
|
99
103
|
return {
|
|
100
104
|
access_token: pair.access.plaintext,
|
|
101
105
|
refresh_token: pair.refresh.plaintext,
|
|
@@ -108,13 +112,19 @@ export class MintingOAuthServerProvider {
|
|
|
108
112
|
// RFC 6749 §6 / OAuth 2.1 §4.3.1 — a refresh_token belongs to the client
|
|
109
113
|
// it was issued to. TokenStore.rotateRefresh enforces that by comparing
|
|
110
114
|
// expectedClientId to the stored record (atomically under its mutex).
|
|
115
|
+
// The returned IssuedPair.identity carries the rotated-out token's
|
|
116
|
+
// identity so we can log `sub` without a separate disk read.
|
|
111
117
|
const result = await this._tokenStore.rotateRefresh(refreshToken, client.client_id, scopes, resource?.toString());
|
|
112
|
-
return result.match((pair) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
return result.match((pair) => {
|
|
119
|
+
const tokenHash = hashTokenForStorage(pair.access.plaintext);
|
|
120
|
+
this.log.info({ tokenHash, clientId: client.client_id, sub: pair.identity.sub }, "access token minted (refresh_token grant)");
|
|
121
|
+
return {
|
|
122
|
+
access_token: pair.access.plaintext,
|
|
123
|
+
refresh_token: pair.refresh.plaintext,
|
|
124
|
+
token_type: "Bearer",
|
|
125
|
+
expires_in: ACCESS_TOKEN_TTL_SECONDS,
|
|
126
|
+
};
|
|
127
|
+
}, (e) => {
|
|
118
128
|
throw e; // OAuthTokenError factories return SDK error subclasses, which the library serializes
|
|
119
129
|
});
|
|
120
130
|
}
|
|
@@ -138,5 +148,6 @@ export class MintingOAuthServerProvider {
|
|
|
138
148
|
if (record.clientId !== client.client_id)
|
|
139
149
|
return;
|
|
140
150
|
await this._tokenStore.revoke(request.token);
|
|
151
|
+
this.log.info({ tokenHash: record.tokenHash, clientId: client.client_id, sub: record.identity.sub }, "access token revoked");
|
|
141
152
|
}
|
|
142
153
|
}
|
package/dist/auth/routes.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import type { Logger } from "pino";
|
|
2
3
|
import type { DiskClientRegistrationStore } from "./client-registration.js";
|
|
3
4
|
import type { TokenStore } from "./token-store.js";
|
|
4
5
|
import type { AuthRequestStore } from "./auth-request-store.js";
|
|
@@ -16,6 +17,10 @@ export interface AuthRoutesDeps {
|
|
|
16
17
|
readonly discovery: DiscoveryDoc;
|
|
17
18
|
readonly jwks: JWTVerifyGetKey;
|
|
18
19
|
readonly publicUrl: string;
|
|
20
|
+
readonly log: {
|
|
21
|
+
readonly auth: Logger;
|
|
22
|
+
readonly oidcClient: Logger;
|
|
23
|
+
};
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
26
|
* Builds custom auth routes not mounted by @hono/mcp:
|
package/dist/auth/routes.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { toMessage } from "../utils/log.js";
|
|
2
1
|
import { Hono } from "hono";
|
|
3
2
|
import { rateLimiter } from "hono-rate-limiter";
|
|
4
3
|
import { z } from "zod";
|
|
@@ -83,7 +82,7 @@ export function buildAuthRoutes(deps) {
|
|
|
83
82
|
idToken = validated.data.id_token;
|
|
84
83
|
}
|
|
85
84
|
catch (cause) {
|
|
86
|
-
|
|
85
|
+
deps.log.oidcClient.error({ err: cause }, "upstream token exchange failed");
|
|
87
86
|
return redirectToClient(c, stored.redirectUri, {
|
|
88
87
|
error: "server_error",
|
|
89
88
|
error_description: "upstream code exchange failed",
|
|
@@ -102,7 +101,7 @@ export function buildAuthRoutes(deps) {
|
|
|
102
101
|
});
|
|
103
102
|
}
|
|
104
103
|
catch (cause) {
|
|
105
|
-
|
|
104
|
+
deps.log.oidcClient.error({ err: cause }, "id_token verification failed");
|
|
106
105
|
return redirectToClient(c, stored.redirectUri, {
|
|
107
106
|
error: "access_denied",
|
|
108
107
|
error_description: "id_token verification failed",
|
|
@@ -116,6 +115,7 @@ export function buildAuthRoutes(deps) {
|
|
|
116
115
|
subs: new Set(deps.oidcConfig.allowlist.subs),
|
|
117
116
|
});
|
|
118
117
|
return identityResult.match(async (identity) => {
|
|
118
|
+
deps.log.auth.info({ email: identity.email ?? null, sub: identity.sub ?? null }, "allowlist accepted identity");
|
|
119
119
|
const ourAuthCode = generateOpaqueToken("mcp_ac_");
|
|
120
120
|
deps.authCodes.put(ourAuthCode, {
|
|
121
121
|
clientId: stored.clientId,
|
|
@@ -134,11 +134,15 @@ export function buildAuthRoutes(deps) {
|
|
|
134
134
|
});
|
|
135
135
|
}, (denial) => {
|
|
136
136
|
// AC3.4: deny alert — log identity claims only, never the id_token.
|
|
137
|
-
// The full denial reason (including email/sub) goes to
|
|
137
|
+
// The full denial reason (including email/sub) goes to the auth logger;
|
|
138
138
|
// the redirect-back error_description is generic so we don't leak the
|
|
139
139
|
// user's email or subject id through claude.ai (or the user's browser
|
|
140
140
|
// history) on a denial.
|
|
141
|
-
|
|
141
|
+
deps.log.auth.warn({
|
|
142
|
+
reason: denial.message,
|
|
143
|
+
email: denial.identity.email ?? null,
|
|
144
|
+
sub: denial.identity.sub ?? null,
|
|
145
|
+
}, "allowlist denied identity");
|
|
142
146
|
return redirectToClient(c, stored.redirectUri, {
|
|
143
147
|
error: "access_denied",
|
|
144
148
|
error_description: "identity not allowed by server policy",
|
|
@@ -23,6 +23,14 @@ export interface IssuedPair {
|
|
|
23
23
|
readonly plaintext: string;
|
|
24
24
|
readonly expiresAt: number;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Identity bound to the issued/rotated pair. For `issueAccessRefreshPair`,
|
|
28
|
+
* this echoes the input identity; for `rotateRefresh`, it's the rotated-out
|
|
29
|
+
* token's identity (the new pair carries the same identity by definition).
|
|
30
|
+
* Surfacing it on the result lets the provider log refresh-grant state
|
|
31
|
+
* transitions without a separate disk read.
|
|
32
|
+
*/
|
|
33
|
+
readonly identity: VerifiedIdentity;
|
|
26
34
|
}
|
|
27
35
|
export declare class TokenStore {
|
|
28
36
|
private readonly _cache;
|
package/dist/auth/token-store.js
CHANGED
|
@@ -69,6 +69,7 @@ export class TokenStore {
|
|
|
69
69
|
return {
|
|
70
70
|
access: { plaintext: accessPlain, expiresAt: accessExpiresAt },
|
|
71
71
|
refresh: { plaintext: refreshPlain, expiresAt: refreshExpiresAt },
|
|
72
|
+
identity: input.identity,
|
|
72
73
|
};
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
@@ -199,6 +200,7 @@ export class TokenStore {
|
|
|
199
200
|
return ok({
|
|
200
201
|
access: { plaintext: accessPlain, expiresAt: accessExpiresAt },
|
|
201
202
|
refresh: { plaintext: refreshPlain, expiresAt: refreshExpiresAt },
|
|
203
|
+
identity: existing.identity,
|
|
202
204
|
});
|
|
203
205
|
});
|
|
204
206
|
}
|