@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.
Files changed (50) hide show
  1. package/dist/auth/build.d.ts +2 -1
  2. package/dist/auth/build.js +11 -5
  3. package/dist/auth/cleanup.d.ts +3 -1
  4. package/dist/auth/cleanup.js +9 -5
  5. package/dist/auth/client-registration.d.ts +3 -1
  6. package/dist/auth/client-registration.js +10 -7
  7. package/dist/auth/dcr-validator.d.ts +9 -2
  8. package/dist/auth/dcr-validator.js +51 -37
  9. package/dist/auth/oidc-client.d.ts +2 -1
  10. package/dist/auth/oidc-client.js +7 -1
  11. package/dist/auth/provider.d.ts +3 -1
  12. package/dist/auth/provider.js +19 -8
  13. package/dist/auth/routes.d.ts +5 -0
  14. package/dist/auth/routes.js +9 -5
  15. package/dist/auth/token-store.d.ts +8 -0
  16. package/dist/auth/token-store.js +2 -0
  17. package/dist/auth/types.d.ts +14 -9
  18. package/dist/cache/disk-cache.d.ts +3 -1
  19. package/dist/cache/disk-cache.js +12 -10
  20. package/dist/features/discover-feature.d.ts +2 -1
  21. package/dist/features/discover-feature.js +7 -7
  22. package/dist/features/embeddings.d.ts +5 -2
  23. package/dist/features/embeddings.js +48 -5
  24. package/dist/features/vector-store.d.ts +3 -1
  25. package/dist/features/vector-store.js +19 -13
  26. package/dist/index.js +5 -4
  27. package/dist/paprika/client.d.ts +6 -1
  28. package/dist/paprika/client.js +65 -22
  29. package/dist/paprika/errors.d.ts +4 -2
  30. package/dist/paprika/errors.js +4 -2
  31. package/dist/paprika/sync.d.ts +1 -1
  32. package/dist/paprika/sync.js +16 -27
  33. package/dist/server/app-context.d.ts +3 -0
  34. package/dist/server/build.js +27 -14
  35. package/dist/tools/create.js +3 -3
  36. package/dist/tools/delete.js +3 -3
  37. package/dist/tools/pantry-add.js +3 -3
  38. package/dist/tools/pantry-delete.js +3 -3
  39. package/dist/tools/pantry-update.js +3 -3
  40. package/dist/tools/update.js +3 -3
  41. package/dist/transport/http.d.ts +26 -1
  42. package/dist/transport/http.js +53 -12
  43. package/dist/transport/stdio.js +5 -6
  44. package/dist/utils/config.d.ts +80 -38
  45. package/dist/utils/config.js +37 -5
  46. package/dist/utils/errors.d.ts +38 -0
  47. package/dist/utils/errors.js +35 -0
  48. package/dist/utils/log.d.ts +47 -18
  49. package/dist/utils/log.js +199 -20
  50. package/package.json +3 -1
@@ -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>;
@@ -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
  }
@@ -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. */
@@ -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
- // Never throws loop must not crash on transient cache errors
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
- // AbortError from stop() exit loop cleanly
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; match() usage per FCIS + project neverthrow rules
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
- // If buffer conversion fails (invalid hex), return false
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
- const RegistrationMetadataSchema = ClientMetadataFieldsSchema.passthrough() // Allow and preserve other RFC 7591 fields
108
- .superRefine((data, ctx) => {
109
- // Validate redirect_uris: must be present, non-empty, valid URLs with our scheme rules
110
- if (!Array.isArray(data.redirect_uris) || data.redirect_uris.length === 0) {
111
- ctx.addIssue({
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 when present",
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
- validateResponseTypes(data, ctx);
140
- validateGrantTypes(data, ctx);
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 = RegistrationMetadataSchema.safeParse(meta);
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 = UpdateMetadataSchema.safeParse(meta);
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>): Promise<DiscoveryDoc>;
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
  *
@@ -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
  // ============================================================================
@@ -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
- constructor(_clientStore: DiskClientRegistrationStore, _tokenStore: TokenStore, _authRequests: AuthRequestStore, _authCodes: AuthCodeStore, _discovery: DiscoveryDoc, _oidcConfig: ResolvedOAuthConfig, _publicUrl: string);
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>;
@@ -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
- constructor(_clientStore, _tokenStore, _authRequests, _authCodes, _discovery, _oidcConfig, _publicUrl) {
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
- access_token: pair.access.plaintext,
114
- refresh_token: pair.refresh.plaintext,
115
- token_type: "Bearer",
116
- expires_in: ACCESS_TOKEN_TTL_SECONDS,
117
- }), (e) => {
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
  }
@@ -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:
@@ -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
- process.stderr.write(`[auth] upstream token exchange failed: ${toMessage(cause)}\n`);
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
- process.stderr.write(`[auth] id_token verification failed: ${toMessage(cause)}\n`);
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 operator stderr;
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
- process.stderr.write(`[auth] allowlist denial: ${denial.message} email=${denial.identity.email ?? "-"} sub=${denial.identity.sub ?? "-"}\n`);
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;
@@ -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
  }