@plaud-ai/mcp 0.2.4 → 0.3.0

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/index.js CHANGED
@@ -1,16 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getClient
4
- } from "./chunk-GZ7QPRKV.js";
4
+ } from "./chunk-BU3JMO4H.js";
5
5
  import {
6
6
  loadSkills
7
7
  } from "./chunk-4QBEOJPX.js";
8
8
  import {
9
+ normalizeMcpHost,
9
10
  registerTools
10
- } from "./chunk-BZOIX6WC.js";
11
+ } from "./chunk-QXV7KYGD.js";
11
12
  import {
12
- runOAuthCallback
13
- } from "./chunk-3XRFIJUG.js";
13
+ capture,
14
+ classifyError,
15
+ clearUser,
16
+ extractIdentity,
17
+ initTelemetry,
18
+ oauthCallbackErrorType,
19
+ runOAuthCallback,
20
+ setMcpHost,
21
+ setUser,
22
+ shutdown
23
+ } from "./chunk-TPGKCGTQ.js";
24
+ import "./chunk-NPCCDRWQ.js";
25
+ import "./chunk-RUFCT6DQ.js";
26
+ import "./chunk-MCKGQKYU.js";
14
27
 
15
28
  // src/index.ts
16
29
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -18,7 +31,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
18
31
  import open from "open";
19
32
  var server = new McpServer({
20
33
  name: "plaud",
21
- version: "0.2.4"
34
+ version: "0.3.0"
22
35
  });
23
36
  var CALLBACK_PORT = 8199;
24
37
  var LOGIN_TIMEOUT_MS = 12e4;
@@ -56,8 +69,23 @@ server.registerTool("login", {
56
69
  });
57
70
  }
58
71
  });
72
+ if (result.status === "success") {
73
+ capture("auth:oauth_callback:success", { passive: true });
74
+ } else {
75
+ capture("auth:oauth_callback:error", {
76
+ passive: true,
77
+ error_type: oauthCallbackErrorType(result.status)
78
+ });
79
+ }
59
80
  switch (result.status) {
60
81
  case "success":
82
+ try {
83
+ const user = await client.getCurrentUser();
84
+ if (user && typeof user.id === "string") {
85
+ await setUser(user.id, extractIdentity(user));
86
+ }
87
+ } catch {
88
+ }
61
89
  return { content: [{ type: "text", text: "Successfully authenticated with Plaud!" }] };
62
90
  case "timeout":
63
91
  return {
@@ -97,10 +125,19 @@ server.registerTool("logout", {
97
125
  return { content: [{ type: "text", text: "Already logged out." }] };
98
126
  }
99
127
  try {
100
- await client.revokeCurrentUser();
128
+ try {
129
+ await client.revokeCurrentUser();
130
+ } catch {
131
+ }
132
+ await client.auth.logout();
133
+ capture("auth:logout:success", { passive: true });
134
+ } catch (err) {
135
+ capture("auth:logout:error", { passive: true, error_type: classifyError(err) });
136
+ }
137
+ try {
138
+ await clearUser();
101
139
  } catch {
102
140
  }
103
- await client.auth.logout();
104
141
  return {
105
142
  content: [{ type: "text", text: "Logged out and revoked authorization." }]
106
143
  };
@@ -117,7 +154,7 @@ async function main() {
117
154
  const sub = process.argv[2];
118
155
  const sub2 = process.argv[3];
119
156
  if (sub === "install") {
120
- const { runInstall } = await import("./install-ALLWR7CL.js");
157
+ const { runInstall } = await import("./install-2EIB7ZP3.js");
121
158
  const args = process.argv.slice(3);
122
159
  const yes = args.some((a) => a === "--yes" || a === "-y");
123
160
  const noLogin = args.some((a) => a === "--no-login");
@@ -125,32 +162,34 @@ async function main() {
125
162
  return;
126
163
  }
127
164
  if (sub === "clean-plugin") {
128
- const { runCleanPlugin } = await import("./setup-QXZHVVDP.js");
165
+ const { runCleanPlugin } = await import("./setup-GLWZYNRW.js");
129
166
  await runCleanPlugin();
130
167
  return;
131
168
  }
132
169
  if (sub === "setup" && sub2 === "codex") {
133
- const { runSetupCodex } = await import("./setup-QXZHVVDP.js");
170
+ const { runSetupCodex } = await import("./setup-GLWZYNRW.js");
134
171
  await runSetupCodex();
135
172
  return;
136
173
  }
137
174
  if (sub === "unsetup" && sub2 === "codex") {
138
- const { runUnsetupCodex } = await import("./setup-QXZHVVDP.js");
175
+ const { runUnsetupCodex } = await import("./setup-GLWZYNRW.js");
139
176
  await runUnsetupCodex();
140
177
  return;
141
178
  }
142
179
  if (sub === "setup") {
143
- const { runSetup } = await import("./setup-QXZHVVDP.js");
180
+ const { runSetup } = await import("./setup-GLWZYNRW.js");
144
181
  await runSetup();
145
182
  return;
146
183
  }
147
184
  if (sub === "unsetup") {
148
- const { runUnsetup } = await import("./setup-QXZHVVDP.js");
185
+ const { runUnsetup } = await import("./setup-GLWZYNRW.js");
149
186
  await runUnsetup();
150
187
  return;
151
188
  }
152
189
  if (sub === "http") {
153
- const { startHttpServer } = await import("./server-VY7ANVCH.js");
190
+ const { startHttpServer } = await import("./server-ZVB7U2VZ.js");
191
+ const { startMetricsServer } = await import("./server-BOR5EZLF.js");
192
+ startMetricsServer();
154
193
  startHttpServer();
155
194
  return;
156
195
  }
@@ -171,8 +210,37 @@ Usage:
171
210
  `);
172
211
  return;
173
212
  }
213
+ try {
214
+ await initTelemetry({
215
+ surface: "mcp",
216
+ appVersion: "0.3.0",
217
+ transport: "stdio"
218
+ });
219
+ } catch {
220
+ }
221
+ const exitWithFlush = async (code) => {
222
+ try {
223
+ await shutdown();
224
+ } catch {
225
+ }
226
+ process.exit(code);
227
+ };
228
+ process.on("SIGINT", () => void exitWithFlush(130));
229
+ process.on("SIGTERM", () => void exitWithFlush(143));
174
230
  await registerSkillPrompts();
231
+ server.server.oninitialized = () => {
232
+ try {
233
+ setMcpHost(normalizeMcpHost(server.server.getClientVersion()?.name));
234
+ } catch {
235
+ }
236
+ };
175
237
  const transport = new StdioServerTransport();
176
- await server.connect(transport);
238
+ try {
239
+ await server.connect(transport);
240
+ capture("mcp:server:success", { passive: true });
241
+ } catch (err) {
242
+ capture("mcp:server:error", { passive: true, error_type: classifyError(err) });
243
+ await exitWithFlush(1);
244
+ }
177
245
  }
178
246
  main().catch(console.error);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-GZ7QPRKV.js";
3
+ } from "./chunk-BU3JMO4H.js";
4
4
  import {
5
5
  copyToClipboard,
6
6
  getMcpEntry,
@@ -11,7 +11,9 @@ import {
11
11
  } from "./chunk-4QBEOJPX.js";
12
12
  import {
13
13
  runOAuthCallback
14
- } from "./chunk-3XRFIJUG.js";
14
+ } from "./chunk-TPGKCGTQ.js";
15
+ import "./chunk-RUFCT6DQ.js";
16
+ import "./chunk-MCKGQKYU.js";
15
17
 
16
18
  // src/install.ts
17
19
  import { createInterface } from "readline/promises";
@@ -0,0 +1,35 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-NPCCDRWQ.js";
4
+ import {
5
+ registry
6
+ } from "./chunk-RUFCT6DQ.js";
7
+ import "./chunk-MCKGQKYU.js";
8
+
9
+ // src/metrics/server.ts
10
+ import express from "express";
11
+ var METRICS_PORT = Number(process.env.PLAUD_METRICS_PORT ?? 9080);
12
+ var METRICS_HOST = process.env.PLAUD_METRICS_HOST ?? "0.0.0.0";
13
+ function startMetricsServer() {
14
+ const app = express();
15
+ app.get("/metrics", async (_req, res, next) => {
16
+ try {
17
+ res.setHeader("Content-Type", registry.contentType);
18
+ res.end(await registry.metrics());
19
+ } catch (err) {
20
+ next(err);
21
+ }
22
+ });
23
+ app.get("/healthz", (_req, res) => {
24
+ res.status(200).send("ok");
25
+ });
26
+ const server = app.listen(METRICS_PORT, METRICS_HOST, () => {
27
+ logger.info({ event: "metrics_server_started", port: METRICS_PORT, host: METRICS_HOST });
28
+ });
29
+ server.on("error", (err) => {
30
+ logger.error({ event: "metrics_server_error", err: err.message });
31
+ });
32
+ }
33
+ export {
34
+ startMetricsServer
35
+ };
@@ -1,10 +1,18 @@
1
1
  import {
2
- logger,
3
2
  registerTools
4
- } from "./chunk-BZOIX6WC.js";
3
+ } from "./chunk-QXV7KYGD.js";
5
4
  import {
6
5
  PlaudClient
7
- } from "./chunk-3XRFIJUG.js";
6
+ } from "./chunk-TPGKCGTQ.js";
7
+ import {
8
+ logger
9
+ } from "./chunk-NPCCDRWQ.js";
10
+ import {
11
+ httpRequestDuration,
12
+ httpRequestsInProgress,
13
+ oauthTokenRefresh
14
+ } from "./chunk-RUFCT6DQ.js";
15
+ import "./chunk-MCKGQKYU.js";
8
16
 
9
17
  // src/http/server.ts
10
18
  import express from "express";
@@ -680,17 +688,21 @@ var PlaudOAuthProvider = class extends ProxyOAuthServerProvider {
680
688
  }
681
689
  logger.error({ event: "oauth_token_refresh_failed", status: fetchRes.status, body: text.slice(0, 500) });
682
690
  if (fetchRes.status >= 500) {
691
+ oauthTokenRefresh.inc({ result: "upstream_error" });
683
692
  throw new McpServerError(`Upstream refresh endpoint error: ${fetchRes.status} ${text.slice(0, 200)}`);
684
693
  }
694
+ oauthTokenRefresh.inc({ result: "invalid_grant" });
685
695
  throw new InvalidGrantError(`Plaud token refresh: ${fetchRes.status} ${text.slice(0, 200)}`);
686
696
  }
687
697
  let data;
688
698
  try {
689
699
  data = await fetchRes.json();
690
700
  } catch (err) {
701
+ oauthTokenRefresh.inc({ result: "parse_failed" });
691
702
  logger.error({ event: "oauth_refresh_json_parse_failed", error: String(err) });
692
703
  throw new McpServerError("Refresh endpoint returned non-JSON response");
693
704
  }
705
+ oauthTokenRefresh.inc({ result: "success" });
694
706
  logger.info({
695
707
  event: "oauth_token_refresh_ok",
696
708
  has_refresh_token: !!data.refresh_token,
@@ -805,6 +817,22 @@ function startHttpServer() {
805
817
  };
806
818
  const app = createMcpExpressApp({ host: HTTP_HOST });
807
819
  app.set("trust proxy", 1);
820
+ app.use((req, res, next) => {
821
+ const start = process.hrtime.bigint();
822
+ const method = req.method;
823
+ const inProgressLabels = { method, handler: req.path };
824
+ httpRequestsInProgress.inc(inProgressLabels);
825
+ res.on("finish", () => {
826
+ const handler = req.route?.path ?? req.path;
827
+ const durSec = Number(process.hrtime.bigint() - start) / 1e9;
828
+ httpRequestDuration.observe(
829
+ { method, handler, status: String(res.statusCode) },
830
+ durSec
831
+ );
832
+ httpRequestsInProgress.dec(inProgressLabels);
833
+ });
834
+ next();
835
+ });
808
836
  app.get("/.well-known/openai-apps-challenge", (_req, res) => {
809
837
  res.type("text/plain").send(OPENAI_APPS_CHALLENGE_TOKEN);
810
838
  });
@@ -982,7 +1010,7 @@ function startHttpServer() {
982
1010
  apiBase,
983
1011
  staticToken: token
984
1012
  });
985
- const mcpServer = new McpServer({ name: "plaud", version: "0.2.4" });
1013
+ const mcpServer = new McpServer({ name: "plaud", version: "0.3.0" });
986
1014
  registerTools(mcpServer, client);
987
1015
  const transport = new StreamableHTTPServerTransport({
988
1016
  sessionIdGenerator: void 0,
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  skillsCombined
9
9
  } from "./chunk-4QBEOJPX.js";
10
+ import "./chunk-MCKGQKYU.js";
10
11
 
11
12
  // src/setup.ts
12
13
  import { readFile, writeFile, mkdir, rm } from "fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plaud-ai/mcp",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -25,10 +25,12 @@
25
25
  "express": "^5.2.1",
26
26
  "open": "^10.2.0",
27
27
  "pino": "^10.3.1",
28
+ "prom-client": "^15.1.3",
28
29
  "zod": "^4.3.6"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@plaud-ai/shared": "workspace:*",
33
+ "@plaud-ai/telemetry": "workspace:*",
32
34
  "@types/express": "^5.0.6",
33
35
  "@types/node": "^25.5.0",
34
36
  "typescript": "^5.7.0"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plaud",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Access your Plaud recordings in Claude",
5
5
  "author": {
6
6
  "name": "Plaud AI"
@@ -1,352 +0,0 @@
1
- // ../shared/dist/oauth.js
2
- import { randomBytes, createHash } from "crypto";
3
-
4
- // ../shared/dist/token-store.js
5
- import { readFile, writeFile, mkdir, rm } from "fs/promises";
6
- import { join } from "path";
7
- import { homedir } from "os";
8
- var TokenStore = class {
9
- configDir;
10
- tokenPath;
11
- constructor(filename = "tokens.json") {
12
- this.configDir = join(homedir(), ".plaud");
13
- this.tokenPath = join(this.configDir, filename);
14
- }
15
- async save(tokenSet) {
16
- await mkdir(this.configDir, { recursive: true });
17
- await writeFile(this.tokenPath, JSON.stringify(tokenSet, null, 2), "utf-8");
18
- }
19
- async load() {
20
- try {
21
- const data = await readFile(this.tokenPath, "utf-8");
22
- return JSON.parse(data);
23
- } catch {
24
- return null;
25
- }
26
- }
27
- async clear() {
28
- try {
29
- await rm(this.tokenPath);
30
- } catch {
31
- }
32
- }
33
- };
34
-
35
- // ../shared/dist/oauth.js
36
- var DEFAULT_AUTHORIZATION_URL = "https://web.plaud.ai/platform/oauth";
37
- var DEFAULT_TOKEN_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token";
38
- var DEFAULT_REFRESH_URL = "https://platform.plaud.ai/developer/api/oauth/third-party/access-token/refresh";
39
- function generateCodeVerifier() {
40
- return randomBytes(32).toString("base64url");
41
- }
42
- function generateCodeChallenge(verifier) {
43
- return createHash("sha256").update(verifier).digest("base64url");
44
- }
45
- function generateState() {
46
- return randomBytes(16).toString("base64url");
47
- }
48
- var OAuth = class {
49
- config;
50
- tokenStore;
51
- authorizationUrl;
52
- tokenUrl;
53
- refreshUrl;
54
- constructor(config) {
55
- this.config = config;
56
- this.tokenStore = new TokenStore(config.tokenFile);
57
- this.authorizationUrl = config.authorizationUrl ?? DEFAULT_AUTHORIZATION_URL;
58
- this.tokenUrl = config.tokenUrl ?? DEFAULT_TOKEN_URL;
59
- this.refreshUrl = config.refreshUrl ?? DEFAULT_REFRESH_URL;
60
- }
61
- createAuthorizationRequest() {
62
- const codeVerifier = generateCodeVerifier();
63
- const codeChallenge = generateCodeChallenge(codeVerifier);
64
- const state = generateState();
65
- const params = new URLSearchParams({
66
- client_id: this.config.clientId,
67
- redirect_uri: this.config.redirectUri,
68
- response_type: "code",
69
- code_challenge: codeChallenge,
70
- code_challenge_method: "S256",
71
- state
72
- });
73
- return {
74
- url: `${this.authorizationUrl}?${params.toString()}`,
75
- codeVerifier,
76
- state
77
- };
78
- }
79
- /**
80
- * @deprecated Use createAuthorizationRequest() for PKCE flow
81
- */
82
- getAuthorizationUrl() {
83
- return this.createAuthorizationRequest().url;
84
- }
85
- async exchangeCode(code, codeVerifier, state) {
86
- const basicAuth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString("base64");
87
- const body = {
88
- code,
89
- redirect_uri: this.config.redirectUri
90
- };
91
- if (codeVerifier) {
92
- body.code_verifier = codeVerifier;
93
- }
94
- if (state) {
95
- body.state = state;
96
- }
97
- const res = await fetch(this.tokenUrl, {
98
- method: "POST",
99
- headers: {
100
- "Content-Type": "application/x-www-form-urlencoded",
101
- Accept: "application/json",
102
- Authorization: `Basic ${basicAuth}`,
103
- ...this.config.extraHeaders
104
- },
105
- body: new URLSearchParams(body)
106
- });
107
- if (!res.ok) {
108
- throw new Error(`Token exchange failed: ${res.status} ${await res.text()}`);
109
- }
110
- const data = await res.json();
111
- const tokenSet = {
112
- access_token: data.access_token,
113
- refresh_token: data.refresh_token,
114
- token_type: data.token_type ?? "Bearer",
115
- expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
116
- };
117
- await this.tokenStore.save(tokenSet);
118
- return tokenSet;
119
- }
120
- async getAccessToken() {
121
- const tokenSet = await this.tokenStore.load();
122
- if (!tokenSet)
123
- return null;
124
- if (tokenSet.expires_at && Date.now() > tokenSet.expires_at - 6e4) {
125
- if (tokenSet.refresh_token) {
126
- try {
127
- const refreshed = await this.refresh(tokenSet.refresh_token);
128
- return refreshed.access_token;
129
- } catch {
130
- return null;
131
- }
132
- }
133
- return null;
134
- }
135
- return tokenSet.access_token;
136
- }
137
- async refresh(refreshToken) {
138
- const res = await fetch(this.refreshUrl, {
139
- method: "POST",
140
- headers: {
141
- "Content-Type": "application/x-www-form-urlencoded",
142
- Accept: "application/json",
143
- ...this.config.extraHeaders
144
- },
145
- body: new URLSearchParams({
146
- refresh_token: refreshToken
147
- })
148
- });
149
- if (!res.ok) {
150
- const body = await res.text();
151
- throw new Error(`Token refresh failed: ${res.status} ${body}`);
152
- }
153
- const data = await res.json();
154
- const tokenSet = {
155
- access_token: data.access_token,
156
- refresh_token: data.refresh_token ?? refreshToken,
157
- token_type: data.token_type ?? "Bearer",
158
- expires_at: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
159
- };
160
- await this.tokenStore.save(tokenSet);
161
- return tokenSet;
162
- }
163
- async logout() {
164
- await this.tokenStore.clear();
165
- }
166
- };
167
-
168
- // ../shared/dist/client.js
169
- var DEFAULT_API_BASE = "https://platform.plaud.ai/developer/api";
170
- var PlaudClient = class {
171
- oauth;
172
- apiBase;
173
- extraHeaders;
174
- staticToken;
175
- constructor(config) {
176
- this.oauth = new OAuth(config);
177
- this.apiBase = config.apiBase ?? DEFAULT_API_BASE;
178
- this.extraHeaders = config.extraHeaders ?? {};
179
- this.staticToken = config.staticToken;
180
- }
181
- get auth() {
182
- return this.oauth;
183
- }
184
- async request(path, init) {
185
- const token = this.staticToken ?? await this.oauth.getAccessToken();
186
- if (!token) {
187
- throw new Error("Not authenticated. Please login first.");
188
- }
189
- const url = `${this.apiBase}${path}`;
190
- const method = init?.method ?? "GET";
191
- const headers = {
192
- Authorization: `Bearer ${token}`,
193
- Accept: "application/json",
194
- ...this.extraHeaders,
195
- ...init?.headers
196
- };
197
- const res = await fetch(url, { ...init, headers });
198
- if (!res.ok) {
199
- const body = await res.text();
200
- if (res.status === 422) {
201
- try {
202
- const parsed = JSON.parse(body);
203
- const messages = parsed.detail.map((d) => `${d.loc.at(-1)}: ${d.msg}`).join("; ");
204
- throw new Error(messages);
205
- } catch (e) {
206
- if (e instanceof SyntaxError)
207
- throw new Error(`API error: ${res.status} ${res.statusText}`);
208
- throw e;
209
- }
210
- }
211
- throw new Error(`API error: ${res.status} ${res.statusText}`);
212
- }
213
- const json = await res.json();
214
- return json;
215
- }
216
- async getCurrentUser() {
217
- return this.request("/open/third-party/users/current");
218
- }
219
- async revokeCurrentUser() {
220
- await this.request("/open/third-party/users/current/revoke", {
221
- method: "POST"
222
- });
223
- }
224
- async listFiles(page = 1, pageSize = 20) {
225
- return this.request(`/open/third-party/files/?page=${page}&page_size=${pageSize}`);
226
- }
227
- async getFile(fileId) {
228
- return this.request(`/open/third-party/files/${fileId}`);
229
- }
230
- };
231
-
232
- // ../shared/dist/oauth-callback-server.js
233
- import { createServer } from "http";
234
- var SUCCESS_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization successful!</h1><p>You can close this tab.</p></body></html>';
235
- var NEUTRAL_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Continue authorization in the original window.</h1><p>This page can be closed.</p></body></html>';
236
- function errorHtml(message) {
237
- const escaped = message.replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
238
- return '<!doctype html><html><head><meta charset="utf-8"><title>Plaud</title></head><body style="font-family:system-ui;padding:2rem;text-align:center;"><h1>Authorization failed</h1><pre style="white-space:pre-wrap;">' + escaped + "</pre></body></html>";
239
- }
240
- var CORS_HEADERS = {
241
- "Access-Control-Allow-Origin": "*",
242
- "Access-Control-Allow-Methods": "GET, OPTIONS",
243
- "Access-Control-Allow-Headers": "*"
244
- };
245
- function runOAuthCallback(opts) {
246
- const { port, expectedState, exchangeCode, timeoutMs = 12e4, onListening, postSuccessDelayMs = 1500 } = opts;
247
- return new Promise((resolve) => {
248
- let settled = false;
249
- let exchangeStarted = false;
250
- let exchangeSucceeded = false;
251
- let timeoutId = null;
252
- let closeTimeoutId = null;
253
- const server = createServer((req, res) => {
254
- if (req.method === "OPTIONS") {
255
- res.writeHead(204, CORS_HEADERS);
256
- res.end();
257
- return;
258
- }
259
- const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
260
- if (reqUrl.pathname !== "/auth/callback") {
261
- res.writeHead(404, CORS_HEADERS);
262
- res.end();
263
- return;
264
- }
265
- const params = reqUrl.searchParams;
266
- const error = params.get("error");
267
- const state = params.get("state");
268
- const code = params.get("code");
269
- if (error) {
270
- const desc = params.get("error_description") ?? error;
271
- res.writeHead(400, { "Content-Type": "text/html", ...CORS_HEADERS });
272
- res.end(errorHtml(`Authorization denied: ${desc}`));
273
- finalize({ status: "denied", error: new Error(desc) });
274
- return;
275
- }
276
- if (!state || state !== expectedState) {
277
- respondNeutral(res);
278
- return;
279
- }
280
- if (exchangeSucceeded) {
281
- respondSuccess(res);
282
- return;
283
- }
284
- if (!code) {
285
- respondNeutral(res);
286
- return;
287
- }
288
- if (exchangeStarted) {
289
- respondNeutral(res);
290
- return;
291
- }
292
- exchangeStarted = true;
293
- exchangeCode(code).then(() => {
294
- exchangeSucceeded = true;
295
- respondSuccess(res);
296
- finalize({ status: "success" });
297
- }, (err) => {
298
- const e = err instanceof Error ? err : new Error(String(err));
299
- res.writeHead(500, { "Content-Type": "text/html", ...CORS_HEADERS });
300
- res.end(errorHtml(e.message));
301
- finalize({ status: "exchange-failed", error: e });
302
- });
303
- });
304
- server.on("error", (err) => {
305
- if (settled)
306
- return;
307
- const message = err.code === "EADDRINUSE" ? `port ${port} is in use \u2014 another \`plaud login\` may still be running. Wait a few seconds and retry.` : `callback server error: ${err.message}`;
308
- finalize({ status: "listen-failed", error: new Error(message) }, true);
309
- });
310
- timeoutId = setTimeout(() => {
311
- finalize({ status: "timeout" }, true);
312
- }, timeoutMs);
313
- server.listen(port, () => {
314
- onListening?.();
315
- });
316
- function respondSuccess(res) {
317
- res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
318
- res.end(SUCCESS_HTML);
319
- }
320
- function respondNeutral(res) {
321
- res.writeHead(200, { "Content-Type": "text/html", ...CORS_HEADERS });
322
- res.end(NEUTRAL_HTML);
323
- }
324
- function finalize(result, immediate = false) {
325
- if (settled)
326
- return;
327
- settled = true;
328
- if (timeoutId) {
329
- clearTimeout(timeoutId);
330
- timeoutId = null;
331
- }
332
- const close = () => {
333
- try {
334
- server.closeAllConnections?.();
335
- } catch {
336
- }
337
- server.close(() => resolve(result));
338
- };
339
- if (immediate || result.status !== "success") {
340
- close();
341
- } else {
342
- closeTimeoutId = setTimeout(close, postSuccessDelayMs);
343
- closeTimeoutId.unref?.();
344
- }
345
- }
346
- });
347
- }
348
-
349
- export {
350
- PlaudClient,
351
- runOAuthCallback
352
- };