@plaud-ai/mcp 0.3.9 → 0.3.10
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/{chunk-RUFCT6DQ.js → chunk-DIPROABB.js} +4 -3
- package/dist/{chunk-EY5K2UXG.js → chunk-PLFBTSKM.js} +1 -1
- package/dist/{chunk-FSNFNJXH.js → chunk-ZVOHKRNC.js} +1 -1
- package/dist/index.js +8 -8
- package/dist/{install-RTIXREYV.js → install-SWKIKJ5C.js} +2 -2
- package/dist/{server-NKCNUA6P.js → server-6RAC6S7E.js} +1 -1
- package/dist/{server-SYSE62NO.js → server-SB6RRGY7.js} +466 -89
- package/package.json +1 -1
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics } from "prom-client";
|
|
3
3
|
var registry = new Registry();
|
|
4
4
|
collectDefaultMetrics({ register: registry });
|
|
5
|
+
var LATENCY_BUCKETS = [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1, 1.5, 2, 3, 5, 10, 30];
|
|
5
6
|
var httpRequestDuration = new Histogram({
|
|
6
7
|
name: "http_request_duration_seconds",
|
|
7
8
|
help: "HTTP request latency in seconds",
|
|
8
9
|
labelNames: ["method", "handler", "status"],
|
|
9
|
-
buckets:
|
|
10
|
+
buckets: LATENCY_BUCKETS,
|
|
10
11
|
registers: [registry]
|
|
11
12
|
});
|
|
12
13
|
var httpRequestsInProgress = new Gauge({
|
|
@@ -25,7 +26,7 @@ var mcpToolDuration = new Histogram({
|
|
|
25
26
|
name: "mcp_tool_duration_seconds",
|
|
26
27
|
help: "MCP tool execution latency in seconds",
|
|
27
28
|
labelNames: ["tool"],
|
|
28
|
-
buckets:
|
|
29
|
+
buckets: LATENCY_BUCKETS,
|
|
29
30
|
registers: [registry]
|
|
30
31
|
});
|
|
31
32
|
var oauthTokenRefresh = new Counter({
|
|
@@ -44,7 +45,7 @@ var httpClientDuration = new Histogram({
|
|
|
44
45
|
name: "http_client_duration_seconds",
|
|
45
46
|
help: "Outbound HTTP request latency in seconds",
|
|
46
47
|
labelNames: ["host", "endpoint"],
|
|
47
|
-
buckets:
|
|
48
|
+
buckets: LATENCY_BUCKETS,
|
|
48
49
|
registers: [registry]
|
|
49
50
|
});
|
|
50
51
|
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
getClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PLFBTSKM.js";
|
|
5
5
|
import {
|
|
6
6
|
loadSkills
|
|
7
7
|
} from "./chunk-242FRP4P.js";
|
|
8
8
|
import {
|
|
9
9
|
normalizeMcpHost,
|
|
10
10
|
registerTools
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-ZVOHKRNC.js";
|
|
12
12
|
import {
|
|
13
13
|
capture,
|
|
14
14
|
classifyError,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
shutdown
|
|
24
24
|
} from "./chunk-5NWKLF3V.js";
|
|
25
25
|
import "./chunk-NPCCDRWQ.js";
|
|
26
|
-
import "./chunk-
|
|
26
|
+
import "./chunk-DIPROABB.js";
|
|
27
27
|
|
|
28
28
|
// src/index.ts
|
|
29
29
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -31,7 +31,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
31
31
|
import open from "open";
|
|
32
32
|
var server = new McpServer({
|
|
33
33
|
name: "plaud",
|
|
34
|
-
version: "0.3.
|
|
34
|
+
version: "0.3.10"
|
|
35
35
|
});
|
|
36
36
|
var CALLBACK_PORT = 8199;
|
|
37
37
|
var LOGIN_TIMEOUT_MS = 12e4;
|
|
@@ -158,7 +158,7 @@ async function main() {
|
|
|
158
158
|
const sub = process.argv[2];
|
|
159
159
|
const sub2 = process.argv[3];
|
|
160
160
|
if (sub === "install") {
|
|
161
|
-
const { runInstall } = await import("./install-
|
|
161
|
+
const { runInstall } = await import("./install-SWKIKJ5C.js");
|
|
162
162
|
const args = process.argv.slice(3);
|
|
163
163
|
const yes = args.some((a) => a === "--yes" || a === "-y");
|
|
164
164
|
const noLogin = args.some((a) => a === "--no-login");
|
|
@@ -191,8 +191,8 @@ async function main() {
|
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
if (sub === "http") {
|
|
194
|
-
const { startHttpServer } = await import("./server-
|
|
195
|
-
const { startMetricsServer } = await import("./server-
|
|
194
|
+
const { startHttpServer } = await import("./server-SB6RRGY7.js");
|
|
195
|
+
const { startMetricsServer } = await import("./server-6RAC6S7E.js");
|
|
196
196
|
startMetricsServer();
|
|
197
197
|
startHttpServer();
|
|
198
198
|
return;
|
|
@@ -217,7 +217,7 @@ Usage:
|
|
|
217
217
|
try {
|
|
218
218
|
await initTelemetry({
|
|
219
219
|
surface: "mcp",
|
|
220
|
-
appVersion: "0.3.
|
|
220
|
+
appVersion: "0.3.10",
|
|
221
221
|
transport: "stdio"
|
|
222
222
|
});
|
|
223
223
|
} catch {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getClient
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PLFBTSKM.js";
|
|
4
4
|
import {
|
|
5
5
|
commandPathIsStale,
|
|
6
6
|
copyToClipboard,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
runOAuthCallback
|
|
15
15
|
} from "./chunk-5NWKLF3V.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-DIPROABB.js";
|
|
17
17
|
|
|
18
18
|
// src/install.ts
|
|
19
19
|
import { createInterface } from "readline/promises";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeMcpHost,
|
|
3
3
|
registerTools
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZVOHKRNC.js";
|
|
5
5
|
import {
|
|
6
|
-
PlaudClient
|
|
6
|
+
PlaudClient,
|
|
7
|
+
classifyError
|
|
7
8
|
} from "./chunk-5NWKLF3V.js";
|
|
8
9
|
import {
|
|
9
10
|
logger
|
|
@@ -12,12 +13,12 @@ import {
|
|
|
12
13
|
httpRequestDuration,
|
|
13
14
|
httpRequestsInProgress,
|
|
14
15
|
oauthTokenRefresh
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DIPROABB.js";
|
|
16
17
|
|
|
17
18
|
// src/http/server.ts
|
|
18
19
|
import express from "express";
|
|
19
20
|
import { createServer } from "http";
|
|
20
|
-
import { randomUUID as
|
|
21
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
21
22
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
23
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
24
|
import { createOAuthMetadata, mcpAuthRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
@@ -25,12 +26,13 @@ import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middlew
|
|
|
25
26
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
26
27
|
|
|
27
28
|
// src/http/oauth-provider.ts
|
|
28
|
-
import { createCipheriv, createDecipheriv, createHmac, randomBytes,
|
|
29
|
+
import { createCipheriv, createDecipheriv, createHash as createHash2, createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
29
30
|
import { ProxyOAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js";
|
|
30
31
|
import {
|
|
31
32
|
InvalidGrantError,
|
|
32
33
|
InvalidTokenError,
|
|
33
|
-
ServerError as McpServerError
|
|
34
|
+
ServerError as McpServerError,
|
|
35
|
+
TooManyRequestsError
|
|
34
36
|
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
35
37
|
|
|
36
38
|
// src/http/cimd.ts
|
|
@@ -266,6 +268,245 @@ function validateMetadata(parsed, expectedClientIdUrl) {
|
|
|
266
268
|
};
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
// src/http/token-verifier.ts
|
|
272
|
+
import { createHash } from "crypto";
|
|
273
|
+
function decodeJwtClaims(token) {
|
|
274
|
+
const parts = token.split(".");
|
|
275
|
+
if (parts.length !== 3) return null;
|
|
276
|
+
const payload = parts[1];
|
|
277
|
+
if (!payload) return null;
|
|
278
|
+
try {
|
|
279
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
280
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
281
|
+
return parsed;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function sha256(value) {
|
|
287
|
+
return createHash("sha256").update(value).digest("base64url");
|
|
288
|
+
}
|
|
289
|
+
var Lru = class {
|
|
290
|
+
constructor(max) {
|
|
291
|
+
this.max = max;
|
|
292
|
+
}
|
|
293
|
+
max;
|
|
294
|
+
map = /* @__PURE__ */ new Map();
|
|
295
|
+
get(key) {
|
|
296
|
+
const value = this.map.get(key);
|
|
297
|
+
if (value === void 0) return void 0;
|
|
298
|
+
this.map.delete(key);
|
|
299
|
+
this.map.set(key, value);
|
|
300
|
+
return value;
|
|
301
|
+
}
|
|
302
|
+
set(key, value) {
|
|
303
|
+
if (this.map.has(key)) {
|
|
304
|
+
this.map.delete(key);
|
|
305
|
+
} else if (this.map.size >= this.max) {
|
|
306
|
+
const oldest = this.map.keys().next().value;
|
|
307
|
+
if (oldest !== void 0) this.map.delete(oldest);
|
|
308
|
+
}
|
|
309
|
+
this.map.set(key, value);
|
|
310
|
+
}
|
|
311
|
+
delete(key) {
|
|
312
|
+
this.map.delete(key);
|
|
313
|
+
}
|
|
314
|
+
get size() {
|
|
315
|
+
return this.map.size;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var TokenBucket = class {
|
|
319
|
+
constructor(capacity, refillPerSec, nowMs) {
|
|
320
|
+
this.capacity = capacity;
|
|
321
|
+
this.refillPerSec = refillPerSec;
|
|
322
|
+
this.tokens = capacity;
|
|
323
|
+
this.lastRefillMs = nowMs;
|
|
324
|
+
}
|
|
325
|
+
capacity;
|
|
326
|
+
refillPerSec;
|
|
327
|
+
tokens;
|
|
328
|
+
lastRefillMs;
|
|
329
|
+
take(nowMs) {
|
|
330
|
+
const elapsedSec = (nowMs - this.lastRefillMs) / 1e3;
|
|
331
|
+
if (elapsedSec > 0) {
|
|
332
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec);
|
|
333
|
+
this.lastRefillMs = nowMs;
|
|
334
|
+
}
|
|
335
|
+
if (this.tokens >= 1) {
|
|
336
|
+
this.tokens -= 1;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var DEFAULTS = {
|
|
343
|
+
cacheMaxEntries: 5e3,
|
|
344
|
+
positiveTtlMs: 6e4,
|
|
345
|
+
negativeTtlMs: 3e4,
|
|
346
|
+
strangerBurst: 40,
|
|
347
|
+
strangerRatePerSec: 20,
|
|
348
|
+
knownBurst: 10,
|
|
349
|
+
knownRatePerSec: 5,
|
|
350
|
+
knownSubMaxEntries: 2e4
|
|
351
|
+
};
|
|
352
|
+
var TokenVerifier = class {
|
|
353
|
+
cache;
|
|
354
|
+
subBuckets;
|
|
355
|
+
strangerBucket;
|
|
356
|
+
opts;
|
|
357
|
+
constructor(options) {
|
|
358
|
+
const now = options.now ?? (() => Date.now());
|
|
359
|
+
this.opts = {
|
|
360
|
+
verifyUpstream: options.verifyUpstream,
|
|
361
|
+
cacheEnabled: options.cacheEnabled ?? true,
|
|
362
|
+
cacheMaxEntries: options.cacheMaxEntries ?? DEFAULTS.cacheMaxEntries,
|
|
363
|
+
positiveTtlMs: options.positiveTtlMs ?? DEFAULTS.positiveTtlMs,
|
|
364
|
+
negativeTtlMs: options.negativeTtlMs ?? DEFAULTS.negativeTtlMs,
|
|
365
|
+
strangerBurst: options.strangerBurst ?? DEFAULTS.strangerBurst,
|
|
366
|
+
strangerRatePerSec: options.strangerRatePerSec ?? DEFAULTS.strangerRatePerSec,
|
|
367
|
+
knownBurst: options.knownBurst ?? DEFAULTS.knownBurst,
|
|
368
|
+
knownRatePerSec: options.knownRatePerSec ?? DEFAULTS.knownRatePerSec,
|
|
369
|
+
knownSubMaxEntries: options.knownSubMaxEntries ?? DEFAULTS.knownSubMaxEntries,
|
|
370
|
+
now
|
|
371
|
+
};
|
|
372
|
+
this.cache = new Lru(this.opts.cacheMaxEntries);
|
|
373
|
+
this.subBuckets = new Lru(this.opts.knownSubMaxEntries);
|
|
374
|
+
this.strangerBucket = new TokenBucket(
|
|
375
|
+
this.opts.strangerBurst,
|
|
376
|
+
this.opts.strangerRatePerSec,
|
|
377
|
+
now()
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Seeds the cache with a token that was just minted upstream (OAuth code
|
|
382
|
+
* exchange or refresh).
|
|
383
|
+
*
|
|
384
|
+
* This is what keeps normal users out of the rate-limit tier entirely: their
|
|
385
|
+
* first request after logging in or rotating a token is already a cache hit,
|
|
386
|
+
* so it never draws from the budget that unrecognised tokens consume.
|
|
387
|
+
*
|
|
388
|
+
* clientId is taken from the JWT `sub` rather than the upstream user.id: we
|
|
389
|
+
* have not called upstream at this point and do not need to, since the token
|
|
390
|
+
* came straight from the token endpoint. Downstream only uses clientId for
|
|
391
|
+
* logging, and server.ts derives ctxUserId from `sub` first anyway.
|
|
392
|
+
*/
|
|
393
|
+
prewarm(token) {
|
|
394
|
+
if (!this.opts.cacheEnabled) return;
|
|
395
|
+
const claims = decodeJwtClaims(token);
|
|
396
|
+
if (!claims) return;
|
|
397
|
+
const sub = typeof claims.sub === "string" ? claims.sub : void 0;
|
|
398
|
+
const exp = typeof claims.exp === "number" ? claims.exp : void 0;
|
|
399
|
+
if (!sub) return;
|
|
400
|
+
const nowMs = this.opts.now();
|
|
401
|
+
const expiresAt = exp ?? Math.floor(nowMs / 1e3) + 3600;
|
|
402
|
+
this.putPositive(sha256(token), sub, expiresAt, nowMs);
|
|
403
|
+
this.noteKnownSub(sub, nowMs);
|
|
404
|
+
logger.info({ event: "token_prewarmed", cache_size: this.cache.size });
|
|
405
|
+
}
|
|
406
|
+
async verify(token) {
|
|
407
|
+
const nowMs = this.opts.now();
|
|
408
|
+
const nowSec = Math.floor(nowMs / 1e3);
|
|
409
|
+
const claims = decodeJwtClaims(token);
|
|
410
|
+
if (!claims) {
|
|
411
|
+
logger.warn({ event: "token_rejected_local", reason: "malformed" });
|
|
412
|
+
return { ok: false, kind: "malformed" };
|
|
413
|
+
}
|
|
414
|
+
const sub = typeof claims.sub === "string" && claims.sub ? claims.sub : void 0;
|
|
415
|
+
const exp = typeof claims.exp === "number" ? claims.exp : void 0;
|
|
416
|
+
if (exp !== void 0 && exp <= nowSec) {
|
|
417
|
+
logger.warn({ event: "token_rejected_local", reason: "expired" });
|
|
418
|
+
return { ok: false, kind: "expired" };
|
|
419
|
+
}
|
|
420
|
+
const key = sha256(token);
|
|
421
|
+
if (this.opts.cacheEnabled) {
|
|
422
|
+
const hit = this.cache.get(key);
|
|
423
|
+
if (hit && hit.cacheExpiresAtMs > nowMs) {
|
|
424
|
+
return hit.valid ? { ok: true, clientId: hit.clientId, expiresAt: hit.expiresAt, source: "cache" } : { ok: false, kind: "invalid" };
|
|
425
|
+
}
|
|
426
|
+
if (hit) this.cache.delete(key);
|
|
427
|
+
}
|
|
428
|
+
const looksKnown = sub !== void 0 && sub.startsWith("client_user_");
|
|
429
|
+
const bucket = sub !== void 0 && looksKnown ? this.subBuckets.get(sub) : void 0;
|
|
430
|
+
const allowed = bucket ? bucket.take(nowMs) : this.strangerBucket.take(nowMs);
|
|
431
|
+
if (!allowed) {
|
|
432
|
+
logger.warn({
|
|
433
|
+
event: "token_verify_rate_limited",
|
|
434
|
+
scope: bucket ? "known_sub" : "stranger"
|
|
435
|
+
});
|
|
436
|
+
return { ok: false, kind: "rate_limited" };
|
|
437
|
+
}
|
|
438
|
+
const verdict = await this.opts.verifyUpstream(token);
|
|
439
|
+
if (verdict.kind === "valid") {
|
|
440
|
+
const expiresAt = exp ?? nowSec + 3600;
|
|
441
|
+
if (this.opts.cacheEnabled) this.putPositive(key, verdict.clientId, expiresAt, nowMs);
|
|
442
|
+
if (sub !== void 0) this.noteKnownSub(sub, nowMs);
|
|
443
|
+
logger.info({ event: "token_verified", client_id: verdict.clientId, expires_at: expiresAt });
|
|
444
|
+
return { ok: true, clientId: verdict.clientId, expiresAt, source: "upstream" };
|
|
445
|
+
}
|
|
446
|
+
if (verdict.kind === "invalid") {
|
|
447
|
+
if (this.opts.cacheEnabled) {
|
|
448
|
+
this.cache.set(key, {
|
|
449
|
+
valid: false,
|
|
450
|
+
clientId: "",
|
|
451
|
+
expiresAt: 0,
|
|
452
|
+
cacheExpiresAtMs: nowMs + this.opts.negativeTtlMs
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
logger.warn({ event: "token_verify_failed", reason: "invalid" });
|
|
456
|
+
return { ok: false, kind: "invalid" };
|
|
457
|
+
}
|
|
458
|
+
logger.error({ event: "token_verify_unavailable", reason: verdict.reason });
|
|
459
|
+
return { ok: false, kind: "unavailable" };
|
|
460
|
+
}
|
|
461
|
+
/** For /health and metrics — counts only, no tokens or user identifiers. */
|
|
462
|
+
stats() {
|
|
463
|
+
return {
|
|
464
|
+
cacheSize: this.cache.size,
|
|
465
|
+
knownSubs: this.subBuckets.size,
|
|
466
|
+
cacheEnabled: this.opts.cacheEnabled
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
putPositive(key, clientId, expiresAt, nowMs) {
|
|
470
|
+
const remainingMs = expiresAt * 1e3 - nowMs;
|
|
471
|
+
const ttlMs = Math.min(this.opts.positiveTtlMs, Math.max(0, remainingMs));
|
|
472
|
+
if (ttlMs <= 0) return;
|
|
473
|
+
this.cache.set(key, { valid: true, clientId, expiresAt, cacheExpiresAtMs: nowMs + ttlMs });
|
|
474
|
+
}
|
|
475
|
+
noteKnownSub(sub, nowMs) {
|
|
476
|
+
if (this.subBuckets.get(sub) === void 0) {
|
|
477
|
+
this.subBuckets.set(
|
|
478
|
+
sub,
|
|
479
|
+
new TokenBucket(this.opts.knownBurst, this.opts.knownRatePerSec, nowMs)
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
function tokenVerifierOptionsFromEnv() {
|
|
485
|
+
const num = (name, fallback) => {
|
|
486
|
+
const raw = process.env[name];
|
|
487
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
488
|
+
const parsed = Number(raw);
|
|
489
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
490
|
+
logger.warn({ event: "token_verifier_bad_env", name, value: raw, using: fallback });
|
|
491
|
+
return fallback;
|
|
492
|
+
}
|
|
493
|
+
return parsed;
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
cacheEnabled: !["0", "false", "no", "off"].includes(
|
|
497
|
+
(process.env["PLAUD_TOKEN_CACHE_ENABLED"] ?? "").toLowerCase()
|
|
498
|
+
),
|
|
499
|
+
cacheMaxEntries: num("PLAUD_TOKEN_CACHE_MAX", DEFAULTS.cacheMaxEntries),
|
|
500
|
+
positiveTtlMs: num("PLAUD_TOKEN_CACHE_TTL_MS", DEFAULTS.positiveTtlMs),
|
|
501
|
+
negativeTtlMs: num("PLAUD_TOKEN_NEGATIVE_TTL_MS", DEFAULTS.negativeTtlMs),
|
|
502
|
+
strangerBurst: num("PLAUD_TOKEN_VERIFY_BURST", DEFAULTS.strangerBurst),
|
|
503
|
+
strangerRatePerSec: num("PLAUD_TOKEN_VERIFY_RATE", DEFAULTS.strangerRatePerSec),
|
|
504
|
+
knownBurst: num("PLAUD_TOKEN_KNOWN_BURST", DEFAULTS.knownBurst),
|
|
505
|
+
knownRatePerSec: num("PLAUD_TOKEN_KNOWN_RATE", DEFAULTS.knownRatePerSec),
|
|
506
|
+
knownSubMaxEntries: num("PLAUD_TOKEN_KNOWN_SUB_MAX", DEFAULTS.knownSubMaxEntries)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
269
510
|
// src/http/oauth-provider.ts
|
|
270
511
|
function subFromJwt(token) {
|
|
271
512
|
if (!token) return void 0;
|
|
@@ -301,6 +542,8 @@ var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-par
|
|
|
301
542
|
var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
|
|
302
543
|
var STATELESS_CODE_PREFIX = "pc1_";
|
|
303
544
|
var AUTHORIZATION_CODE_TTL_MS = 10 * 60 * 1e3;
|
|
545
|
+
var STATELESS_STATE_PREFIX = "st1_";
|
|
546
|
+
var AUTHORIZATION_STATE_TTL_MS = 30 * 60 * 1e3;
|
|
304
547
|
function urlParts(value) {
|
|
305
548
|
if (!value || !URL.canParse(value)) {
|
|
306
549
|
return { host: null, path: null };
|
|
@@ -318,57 +561,73 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
318
561
|
_debugOAuthLogs;
|
|
319
562
|
_cimdLoader;
|
|
320
563
|
_tracker;
|
|
564
|
+
/** Local pre-checks + cache + tiered rate limit + upstream call, all in one
|
|
565
|
+
* place. See token-verifier.ts. */
|
|
566
|
+
_verifier;
|
|
321
567
|
_registeredClients = /* @__PURE__ */ new Map();
|
|
322
568
|
// HMAC secret used to sign DCR-issued client_ids so we can recover them
|
|
323
569
|
// across container restarts without persistent storage. See verifyAndRecover.
|
|
324
570
|
_clientIdSecret;
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
571
|
+
// NOTE: there is deliberately no in-process store for pending authorizations.
|
|
572
|
+
// This class used to keep `_pendingStates: Map<state, {clientRedirectUri, …}>`,
|
|
573
|
+
// which meant /authorize and /auth/callback had to be served by the same
|
|
574
|
+
// process — the callback looked the state up and hard-failed with 400 if it
|
|
575
|
+
// wasn't there. That single map is what pinned the deployment to one replica
|
|
576
|
+
// (and it was never swept, so abandoned authorizations accumulated for the
|
|
577
|
+
// lifetime of the pod). The state parameter is now a sealed, self-contained
|
|
578
|
+
// envelope instead; see encodeAuthorizationState.
|
|
579
|
+
//
|
|
580
|
+
// Everything else here was already replica-safe: client_ids are HMAC-signed
|
|
581
|
+
// and recoverable (verifyAndRecover), and authorization codes are sealed the
|
|
582
|
+
// same way. This was the last piece holding it back.
|
|
328
583
|
constructor(options) {
|
|
329
584
|
const authUrl = options.authUrl ?? "https://web.plaud.ai/platform/oauth";
|
|
330
585
|
const tokenUrl = options.tokenUrl ?? DEFAULT_TOKEN_URL;
|
|
331
586
|
const refreshUrl = options.refreshUrl ?? DEFAULT_REFRESH_URL;
|
|
332
587
|
const apiBase = options.apiBase ?? "https://platform.plaud.ai/developer/api";
|
|
588
|
+
const holder = {};
|
|
589
|
+
const verifyUpstream = async (token) => {
|
|
590
|
+
const client = new PlaudClient({
|
|
591
|
+
clientId: options.clientId,
|
|
592
|
+
clientSecret: "",
|
|
593
|
+
redirectUri: "",
|
|
594
|
+
apiBase,
|
|
595
|
+
staticToken: token
|
|
596
|
+
});
|
|
597
|
+
try {
|
|
598
|
+
const user = await client.getCurrentUser();
|
|
599
|
+
return { kind: "valid", clientId: String(user.id ?? "unknown") };
|
|
600
|
+
} catch (err) {
|
|
601
|
+
const type = classifyError(err);
|
|
602
|
+
if (type === "auth") return { kind: "invalid" };
|
|
603
|
+
return { kind: "unavailable", reason: type };
|
|
604
|
+
}
|
|
605
|
+
};
|
|
333
606
|
super({
|
|
334
607
|
endpoints: {
|
|
335
608
|
authorizationUrl: authUrl,
|
|
336
609
|
tokenUrl
|
|
337
610
|
},
|
|
338
611
|
verifyAccessToken: async (token) => {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
);
|
|
353
|
-
expiresAt = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + 3600;
|
|
354
|
-
} catch {
|
|
355
|
-
expiresAt = Math.floor(Date.now() / 1e3) + 3600;
|
|
356
|
-
}
|
|
357
|
-
const authInfo = {
|
|
358
|
-
token,
|
|
359
|
-
clientId: String(user.id ?? "unknown"),
|
|
360
|
-
scopes: [],
|
|
361
|
-
expiresAt
|
|
362
|
-
};
|
|
363
|
-
logger.info({ event: "token_verified", client_id: authInfo.clientId, expires_at: expiresAt });
|
|
364
|
-
return authInfo;
|
|
365
|
-
} catch (err) {
|
|
366
|
-
logger.warn({ event: "token_verify_failed", error: String(err) });
|
|
367
|
-
throw new InvalidTokenError("Invalid or expired token");
|
|
612
|
+
const verifier = holder.verifier;
|
|
613
|
+
if (!verifier) throw new McpServerError("Token verifier not initialised");
|
|
614
|
+
const outcome = await verifier.verify(token);
|
|
615
|
+
if (outcome.ok) {
|
|
616
|
+
return { token, clientId: outcome.clientId, scopes: [], expiresAt: outcome.expiresAt };
|
|
617
|
+
}
|
|
618
|
+
switch (outcome.kind) {
|
|
619
|
+
case "unavailable":
|
|
620
|
+
throw new McpServerError("Upstream token verification temporarily unavailable");
|
|
621
|
+
case "rate_limited":
|
|
622
|
+
throw new TooManyRequestsError("Token verification rate limited");
|
|
623
|
+
default:
|
|
624
|
+
throw new InvalidTokenError("Invalid or expired token");
|
|
368
625
|
}
|
|
369
626
|
},
|
|
370
627
|
getClient: async (id) => this._registeredClients.get(id)
|
|
371
628
|
});
|
|
629
|
+
holder.verifier = new TokenVerifier({ ...tokenVerifierOptionsFromEnv(), verifyUpstream });
|
|
630
|
+
this._verifier = holder.verifier;
|
|
372
631
|
this._plaudClientId = options.clientId;
|
|
373
632
|
this._plaudClientSecret = options.clientSecret ?? "";
|
|
374
633
|
this._plaudTokenUrl = tokenUrl;
|
|
@@ -390,6 +649,10 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
390
649
|
}
|
|
391
650
|
this.skipLocalPkceValidation = true;
|
|
392
651
|
}
|
|
652
|
+
/** For /health and metrics — counts only, no tokens or user identifiers. */
|
|
653
|
+
get verifierStats() {
|
|
654
|
+
return this._verifier.stats();
|
|
655
|
+
}
|
|
393
656
|
// Override clientsStore. registerClient signs the issued client_id with HMAC
|
|
394
657
|
// so verifyAndRecover can later resurrect it across container restarts.
|
|
395
658
|
get clientsStore() {
|
|
@@ -495,49 +758,116 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
495
758
|
if (actual.length !== expected.length) return false;
|
|
496
759
|
return timingSafeEqual(actual, expected);
|
|
497
760
|
}
|
|
498
|
-
|
|
499
|
-
|
|
761
|
+
// Key is derived per purpose so a value sealed for one hop can never be
|
|
762
|
+
// unsealed as the other — an authorization code presented as a state (or vice
|
|
763
|
+
// versa) fails the GCM tag check rather than decoding into the wrong shape.
|
|
764
|
+
envelopeKey(purpose) {
|
|
765
|
+
return createHmac("sha256", this._clientIdSecret).update(purpose).digest();
|
|
500
766
|
}
|
|
501
|
-
|
|
767
|
+
// AES-256-GCM envelope: prefix + base64url(iv[12] | tag[16] | ciphertext).
|
|
768
|
+
// Self-contained, so any pod can open what any other pod sealed — that is what
|
|
769
|
+
// lets us run more than one replica without a shared store.
|
|
770
|
+
seal(purpose, prefix, payload, ttlMs) {
|
|
502
771
|
const iv = randomBytes(12);
|
|
503
|
-
const cipher = createCipheriv("aes-256-gcm", this.
|
|
504
|
-
const plaintext = Buffer.from(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
expiresAt: Date.now() + AUTHORIZATION_CODE_TTL_MS
|
|
509
|
-
}), "utf8");
|
|
772
|
+
const cipher = createCipheriv("aes-256-gcm", this.envelopeKey(purpose), iv);
|
|
773
|
+
const plaintext = Buffer.from(
|
|
774
|
+
JSON.stringify({ ...payload, expiresAt: Date.now() + ttlMs }),
|
|
775
|
+
"utf8"
|
|
776
|
+
);
|
|
510
777
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
511
778
|
const tag = cipher.getAuthTag();
|
|
512
|
-
return `${
|
|
779
|
+
return `${prefix}${Buffer.concat([iv, tag, ciphertext]).toString("base64url")}`;
|
|
513
780
|
}
|
|
514
|
-
|
|
515
|
-
|
|
781
|
+
// Returns null for anything that isn't ours, has been tampered with, or has
|
|
782
|
+
// expired. Callers still validate the payload's own shape.
|
|
783
|
+
unseal(purpose, prefix, value) {
|
|
784
|
+
if (typeof value !== "string" || !value.startsWith(prefix)) return null;
|
|
516
785
|
try {
|
|
517
|
-
const
|
|
518
|
-
const data = Buffer.from(encoded, "base64url");
|
|
786
|
+
const data = Buffer.from(value.slice(prefix.length), "base64url");
|
|
519
787
|
if (data.length <= 28) return null;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
788
|
+
const decipher = createDecipheriv("aes-256-gcm", this.envelopeKey(purpose), data.subarray(0, 12));
|
|
789
|
+
decipher.setAuthTag(data.subarray(12, 28));
|
|
790
|
+
const plaintext = Buffer.concat([
|
|
791
|
+
decipher.update(data.subarray(28)),
|
|
792
|
+
decipher.final()
|
|
793
|
+
]).toString("utf8");
|
|
526
794
|
const payload = JSON.parse(plaintext);
|
|
527
|
-
if (typeof payload
|
|
795
|
+
if (typeof payload["expiresAt"] !== "number" || Date.now() > payload["expiresAt"]) {
|
|
528
796
|
return null;
|
|
529
797
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
798
|
+
return payload;
|
|
799
|
+
} catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
encodeAuthorizationCode(payload) {
|
|
804
|
+
return this.seal(
|
|
805
|
+
"plaud-mcp-authorization-code",
|
|
806
|
+
STATELESS_CODE_PREFIX,
|
|
807
|
+
{
|
|
534
808
|
upstreamCode: payload.upstreamCode,
|
|
535
809
|
upstreamState: payload.upstreamState,
|
|
536
810
|
resource: payload.resource
|
|
537
|
-
}
|
|
538
|
-
|
|
811
|
+
},
|
|
812
|
+
AUTHORIZATION_CODE_TTL_MS
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
decodeAuthorizationCode(code) {
|
|
816
|
+
const payload = this.unseal(
|
|
817
|
+
"plaud-mcp-authorization-code",
|
|
818
|
+
STATELESS_CODE_PREFIX,
|
|
819
|
+
code
|
|
820
|
+
);
|
|
821
|
+
if (!payload) return null;
|
|
822
|
+
if (typeof payload.upstreamCode !== "string" || typeof payload.upstreamState !== "string" || payload.resource !== void 0 && typeof payload.resource !== "string") {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
upstreamCode: payload.upstreamCode,
|
|
827
|
+
upstreamState: payload.upstreamState,
|
|
828
|
+
resource: payload.resource
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
// The authorize→callback hop. Everything the callback needs travels inside the
|
|
832
|
+
// state parameter itself, so the pod that handles the callback need not be the
|
|
833
|
+
// pod that started the flow. This is what replaced the in-process
|
|
834
|
+
// _pendingStates map — see the class docstring for why that map capped us at
|
|
835
|
+
// one replica.
|
|
836
|
+
encodeAuthorizationState(payload) {
|
|
837
|
+
return this.seal(
|
|
838
|
+
"plaud-mcp-authorization-state",
|
|
839
|
+
STATELESS_STATE_PREFIX,
|
|
840
|
+
{
|
|
841
|
+
clientRedirectUri: payload.clientRedirectUri,
|
|
842
|
+
originalState: payload.originalState,
|
|
843
|
+
resource: payload.resource
|
|
844
|
+
},
|
|
845
|
+
AUTHORIZATION_STATE_TTL_MS
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
// Stable short handle for correlating authorize and callback log lines.
|
|
849
|
+
// Not a secret and not reversible — purely a join key for reading logs.
|
|
850
|
+
stateHandle(state) {
|
|
851
|
+
if (typeof state !== "string") return "non-string";
|
|
852
|
+
return createHash2("sha256").update(state).digest("base64url").slice(0, 12);
|
|
853
|
+
}
|
|
854
|
+
decodeAuthorizationState(state) {
|
|
855
|
+
const payload = this.unseal(
|
|
856
|
+
"plaud-mcp-authorization-state",
|
|
857
|
+
STATELESS_STATE_PREFIX,
|
|
858
|
+
state
|
|
859
|
+
);
|
|
860
|
+
if (!payload) return null;
|
|
861
|
+
if (typeof payload.clientRedirectUri !== "string" || !URL.canParse(payload.clientRedirectUri)) {
|
|
539
862
|
return null;
|
|
540
863
|
}
|
|
864
|
+
if (payload.originalState !== void 0 && typeof payload.originalState !== "string") return null;
|
|
865
|
+
if (payload.resource !== void 0 && typeof payload.resource !== "string") return null;
|
|
866
|
+
return {
|
|
867
|
+
clientRedirectUri: payload.clientRedirectUri,
|
|
868
|
+
originalState: payload.originalState,
|
|
869
|
+
resource: payload.resource
|
|
870
|
+
};
|
|
541
871
|
}
|
|
542
872
|
// Recover a client_id that the registry doesn't know about. Directory clients
|
|
543
873
|
// (OpenAI Apps, Claude directory) cache the client_id they got from /register;
|
|
@@ -601,8 +931,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
601
931
|
* Store the client's original redirect_uri keyed by state so we can forward after Plaud calls back.
|
|
602
932
|
*/
|
|
603
933
|
async authorize(_client, params, res) {
|
|
604
|
-
const internalState =
|
|
605
|
-
this._pendingStates.set(internalState, {
|
|
934
|
+
const internalState = this.encodeAuthorizationState({
|
|
606
935
|
clientRedirectUri: params.redirectUri,
|
|
607
936
|
originalState: params.state,
|
|
608
937
|
resource: params.resource?.href
|
|
@@ -610,7 +939,10 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
610
939
|
const redirectParts = urlParts(params.redirectUri);
|
|
611
940
|
logger.info({
|
|
612
941
|
event: "oauth_authorize_start",
|
|
613
|
-
|
|
942
|
+
// Short digest, not the sealed blob — it is 300+ chars and would bloat
|
|
943
|
+
// every line. Same value appears on the matching callback, so the two
|
|
944
|
+
// still correlate.
|
|
945
|
+
state_handle: this.stateHandle(internalState),
|
|
614
946
|
redirect_uri: params.redirectUri,
|
|
615
947
|
...this._debugOAuthLogs ? {
|
|
616
948
|
redirect_uri_host: redirectParts.host,
|
|
@@ -618,7 +950,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
618
950
|
has_original_state: !!params.state,
|
|
619
951
|
has_resource: !!params.resource,
|
|
620
952
|
resource: params.resource?.href ?? null,
|
|
621
|
-
|
|
953
|
+
state_len: internalState.length
|
|
622
954
|
} : {}
|
|
623
955
|
});
|
|
624
956
|
const targetUrl = new URL(this._endpoints.authorizationUrl);
|
|
@@ -644,17 +976,25 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
644
976
|
* Called when Plaud redirects to our /auth/callback (CALLBACK_PATH in
|
|
645
977
|
* http/server.ts). NOT /oauth/callback — this comment said so until 2026-08-19
|
|
646
978
|
* and a downstream client registration was configured from it by mistake.
|
|
647
|
-
*
|
|
979
|
+
* Opens the sealed state to recover the client's redirect_uri and forwards the
|
|
980
|
+
* code+state to it. No lookup — the state is the storage, which is what allows
|
|
981
|
+
* this to be served by a pod that never saw the /authorize request.
|
|
648
982
|
*/
|
|
649
983
|
handleCallback(code, state, res) {
|
|
650
|
-
const pending = this.
|
|
984
|
+
const pending = this.decodeAuthorizationState(state);
|
|
651
985
|
if (!pending) {
|
|
652
|
-
logger.warn({
|
|
986
|
+
logger.warn({
|
|
987
|
+
event: "oauth_callback_unknown_state",
|
|
988
|
+
state_handle: this.stateHandle(state),
|
|
989
|
+
// typeof-guarded: this is the one log line reached by a request whose
|
|
990
|
+
// parameter shape we do not control.
|
|
991
|
+
state_len: typeof state === "string" ? state.length : null,
|
|
992
|
+
sealed_shape: typeof state === "string" && state.startsWith(STATELESS_STATE_PREFIX)
|
|
993
|
+
});
|
|
653
994
|
res.status(400).send("Unknown state \u2014 authorization request not found");
|
|
654
995
|
return;
|
|
655
996
|
}
|
|
656
|
-
this.
|
|
657
|
-
logger.info({ event: "oauth_callback_received", internal_state: state });
|
|
997
|
+
logger.info({ event: "oauth_callback_received", state_handle: this.stateHandle(state) });
|
|
658
998
|
const pendingCode = {
|
|
659
999
|
upstreamCode: code,
|
|
660
1000
|
upstreamState: state,
|
|
@@ -667,10 +1007,12 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
667
1007
|
target.searchParams.set("state", pending.originalState);
|
|
668
1008
|
}
|
|
669
1009
|
const redirectParts = urlParts(pending.clientRedirectUri);
|
|
1010
|
+
const isCustomScheme = !/^https?:$/.test(new URL(target.toString()).protocol);
|
|
670
1011
|
logger.info({
|
|
671
1012
|
event: "oauth_callback_redirect",
|
|
672
|
-
|
|
1013
|
+
state_handle: this.stateHandle(state),
|
|
673
1014
|
redirect_uri: pending.clientRedirectUri,
|
|
1015
|
+
custom_scheme: isCustomScheme,
|
|
674
1016
|
has_original_state: !!pending.originalState,
|
|
675
1017
|
original_state_len: pending.originalState?.length ?? 0,
|
|
676
1018
|
upstream_code_len: code.length,
|
|
@@ -685,7 +1027,37 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
685
1027
|
redirect_has_state: target.searchParams.has("state")
|
|
686
1028
|
} : {}
|
|
687
1029
|
});
|
|
688
|
-
|
|
1030
|
+
if (!isCustomScheme) {
|
|
1031
|
+
res.redirect(target.toString());
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
res.status(200).type("html").send(this.appHandoffPage(target.toString()));
|
|
1035
|
+
}
|
|
1036
|
+
// Landing page for native-app callbacks. The URL goes into a data attribute
|
|
1037
|
+
// rather than inline JS so nothing from `clientRedirectUri` can be parsed as
|
|
1038
|
+
// script; it is only ever assigned to location.href.
|
|
1039
|
+
appHandoffPage(redirectUrl) {
|
|
1040
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1041
|
+
const href = esc(redirectUrl);
|
|
1042
|
+
return `<!doctype html>
|
|
1043
|
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
1044
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
1045
|
+
<title>\u6388\u6743\u6210\u529F \xB7 Plaud</title></head>
|
|
1046
|
+
<body style="font-family:system-ui,-apple-system,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#fafafa;color:#111">
|
|
1047
|
+
<main id="p" data-href="${href}" style="text-align:center;padding:2rem;max-width:28rem">
|
|
1048
|
+
<div style="font-size:2.5rem;line-height:1">\u2713</div>
|
|
1049
|
+
<h1 style="font-size:1.25rem;margin:.75rem 0 .5rem">\u6388\u6743\u6210\u529F</h1>
|
|
1050
|
+
<p style="color:#666;margin:0 0 1.5rem">\u6B63\u5728\u8FD4\u56DE\u5E94\u7528\uFF0C\u53EF\u4EE5\u5173\u95ED\u6B64\u9875\u9762\u3002</p>
|
|
1051
|
+
<p style="font-size:.875rem;color:#888;margin:0">
|
|
1052
|
+
\u6CA1\u6709\u81EA\u52A8\u8DF3\u8F6C\uFF1F<a id="m" href="#" style="color:#0066cc">\u70B9\u6B64\u624B\u52A8\u6253\u5F00</a>
|
|
1053
|
+
</p>
|
|
1054
|
+
</main>
|
|
1055
|
+
<script>
|
|
1056
|
+
var u = document.getElementById("p").dataset.href;
|
|
1057
|
+
document.getElementById("m").href = u;
|
|
1058
|
+
location.href = u;
|
|
1059
|
+
</script>
|
|
1060
|
+
</body></html>`;
|
|
689
1061
|
}
|
|
690
1062
|
// Override to use PKCE public-client flow — no Basic auth, client_id sent in body.
|
|
691
1063
|
async exchangeAuthorizationCode(client, authorizationCode, codeVerifier, _redirectUri, resource) {
|
|
@@ -762,6 +1134,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
762
1134
|
throw new McpServerError("Token endpoint returned non-JSON response");
|
|
763
1135
|
}
|
|
764
1136
|
logger.info({ event: "oauth_token_exchange_ok", has_refresh_token: !!data.refresh_token });
|
|
1137
|
+
this._verifier.prewarm(data.access_token);
|
|
765
1138
|
const authorizedUserId = subFromJwt(data.access_token);
|
|
766
1139
|
this._tracker?.track({
|
|
767
1140
|
name: "auth.oauth_callback_success",
|
|
@@ -834,6 +1207,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
834
1207
|
throw new McpServerError("Refresh endpoint returned non-JSON response");
|
|
835
1208
|
}
|
|
836
1209
|
oauthTokenRefresh.inc({ result: "success" });
|
|
1210
|
+
this._verifier.prewarm(data.access_token);
|
|
837
1211
|
this._tracker?.track({
|
|
838
1212
|
name: "auth.token_refresh_success",
|
|
839
1213
|
actorType: "user",
|
|
@@ -856,7 +1230,7 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
|
|
|
856
1230
|
};
|
|
857
1231
|
|
|
858
1232
|
// src/telemetry-server/tracker.ts
|
|
859
|
-
import { randomUUID
|
|
1233
|
+
import { randomUUID } from "crypto";
|
|
860
1234
|
|
|
861
1235
|
// src/telemetry-server/transport.ts
|
|
862
1236
|
import { PostHog } from "posthog-node";
|
|
@@ -1018,7 +1392,7 @@ var WarehouseTracker = class {
|
|
|
1018
1392
|
service_version: this.common.serviceVersion,
|
|
1019
1393
|
...this.common.buildId ? { build_id: this.common.buildId } : {},
|
|
1020
1394
|
// timing / tracing
|
|
1021
|
-
event_id:
|
|
1395
|
+
event_id: randomUUID(),
|
|
1022
1396
|
...input.requestId ? { request_id: input.requestId } : {},
|
|
1023
1397
|
// (trace_id/span_id omitted — no OpenTelemetry; spec forbids fabricating)
|
|
1024
1398
|
// non-user actors: don't create a PostHog person
|
|
@@ -1283,8 +1657,8 @@ function startHttpServer() {
|
|
|
1283
1657
|
common: {
|
|
1284
1658
|
serviceName: process.env.PLAUD_WAREHOUSE_SERVICE_NAME ?? "plaudmcp",
|
|
1285
1659
|
// matches §1.3 key label `plaudmcp:TRACKING_KEY_PLAUDMCP`; confirm exact string with David
|
|
1286
|
-
serviceVersion: "0.3.
|
|
1287
|
-
buildId: "
|
|
1660
|
+
serviceVersion: "0.3.10",
|
|
1661
|
+
buildId: "e5eb255",
|
|
1288
1662
|
// mcp tsup TODO: inject git short SHA (like CLI)
|
|
1289
1663
|
region: process.env.PLAUD_REGION ?? "US",
|
|
1290
1664
|
env: process.env.PLAUD_ENV ?? process.env.NODE_ENV ?? "development"
|
|
@@ -1374,7 +1748,10 @@ function startHttpServer() {
|
|
|
1374
1748
|
server_url: process.env.PLAUD_SERVER_URL ?? "(default:localhost)",
|
|
1375
1749
|
oauth_debug_logs: OAUTH_DEBUG_LOGS,
|
|
1376
1750
|
cimd_enabled: CIMD_ENABLED
|
|
1377
|
-
}
|
|
1751
|
+
},
|
|
1752
|
+
// Size of the token-verification cache, to confirm it is actually in
|
|
1753
|
+
// effect. Counts only — no tokens or user identifiers.
|
|
1754
|
+
token_verifier: provider.verifierStats
|
|
1378
1755
|
});
|
|
1379
1756
|
});
|
|
1380
1757
|
app.get("/", (_req, res) => {
|
|
@@ -1382,8 +1759,8 @@ function startHttpServer() {
|
|
|
1382
1759
|
});
|
|
1383
1760
|
if (process.env.PLAUD_CALLBACK_URL) {
|
|
1384
1761
|
app.get(CALLBACK_PATH, (req, res) => {
|
|
1385
|
-
const code = req.query["code"];
|
|
1386
|
-
const state = req.query["state"];
|
|
1762
|
+
const code = queryValue(req.query["code"]);
|
|
1763
|
+
const state = queryValue(req.query["state"]);
|
|
1387
1764
|
if (!code || !state) {
|
|
1388
1765
|
logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
|
|
1389
1766
|
res.status(400).send("Missing code or state");
|
|
@@ -1393,7 +1770,7 @@ function startHttpServer() {
|
|
|
1393
1770
|
});
|
|
1394
1771
|
}
|
|
1395
1772
|
app.use((req, res, next) => {
|
|
1396
|
-
const reqId = req.headers["x-request-id"] ??
|
|
1773
|
+
const reqId = req.headers["x-request-id"] ?? randomUUID2();
|
|
1397
1774
|
const startMs = Date.now();
|
|
1398
1775
|
res.locals["reqId"] = reqId;
|
|
1399
1776
|
res.on("finish", () => {
|
|
@@ -1545,7 +1922,7 @@ function startHttpServer() {
|
|
|
1545
1922
|
requireBearerAuth({ verifier: provider, resourceMetadataUrl: protectedResourceMetadataUrl }),
|
|
1546
1923
|
async (req, res) => {
|
|
1547
1924
|
const token = req.auth.token;
|
|
1548
|
-
const reqId = res.locals["reqId"] ??
|
|
1925
|
+
const reqId = res.locals["reqId"] ?? randomUUID2();
|
|
1549
1926
|
const reqLog = logger.child({ req_id: reqId });
|
|
1550
1927
|
const startMs = Date.now();
|
|
1551
1928
|
reqLog.info({ event: "mcp_request_start", client_id: req.auth.clientId });
|
|
@@ -1556,7 +1933,7 @@ function startHttpServer() {
|
|
|
1556
1933
|
apiBase,
|
|
1557
1934
|
staticToken: token
|
|
1558
1935
|
});
|
|
1559
|
-
const mcpServer = new McpServer({ name: "plaud", version: "0.3.
|
|
1936
|
+
const mcpServer = new McpServer({ name: "plaud", version: "0.3.10" });
|
|
1560
1937
|
registerTools(mcpServer, client, warehouseToolHooks);
|
|
1561
1938
|
const transport = new StreamableHTTPServerTransport({
|
|
1562
1939
|
sessionIdGenerator: void 0,
|
|
@@ -1594,8 +1971,8 @@ function startHttpServer() {
|
|
|
1594
1971
|
if (!process.env.PLAUD_CALLBACK_URL) {
|
|
1595
1972
|
const callbackApp = express();
|
|
1596
1973
|
callbackApp.get(CALLBACK_PATH, (req, res) => {
|
|
1597
|
-
const code = req.query["code"];
|
|
1598
|
-
const state = req.query["state"];
|
|
1974
|
+
const code = queryValue(req.query["code"]);
|
|
1975
|
+
const state = queryValue(req.query["state"]);
|
|
1599
1976
|
if (!code || !state) {
|
|
1600
1977
|
logger.warn({ event: "oauth_callback_invalid", code: !!code, state: !!state });
|
|
1601
1978
|
res.status(400).send("Missing code or state");
|