@mcp-z/client 1.0.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.
Files changed (211) hide show
  1. package/AGENTS.md +159 -0
  2. package/LICENSE +21 -0
  3. package/README.md +90 -0
  4. package/dist/cjs/auth/capability-discovery.d.cts +25 -0
  5. package/dist/cjs/auth/capability-discovery.d.ts +25 -0
  6. package/dist/cjs/auth/capability-discovery.js +280 -0
  7. package/dist/cjs/auth/capability-discovery.js.map +1 -0
  8. package/dist/cjs/auth/index.d.cts +9 -0
  9. package/dist/cjs/auth/index.d.ts +9 -0
  10. package/dist/cjs/auth/index.js +28 -0
  11. package/dist/cjs/auth/index.js.map +1 -0
  12. package/dist/cjs/auth/interactive-oauth-flow.d.cts +58 -0
  13. package/dist/cjs/auth/interactive-oauth-flow.d.ts +58 -0
  14. package/dist/cjs/auth/interactive-oauth-flow.js +537 -0
  15. package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -0
  16. package/dist/cjs/auth/oauth-callback-listener.d.cts +56 -0
  17. package/dist/cjs/auth/oauth-callback-listener.d.ts +56 -0
  18. package/dist/cjs/auth/oauth-callback-listener.js +333 -0
  19. package/dist/cjs/auth/oauth-callback-listener.js.map +1 -0
  20. package/dist/cjs/auth/pkce.d.cts +17 -0
  21. package/dist/cjs/auth/pkce.d.ts +17 -0
  22. package/dist/cjs/auth/pkce.js +192 -0
  23. package/dist/cjs/auth/pkce.js.map +1 -0
  24. package/dist/cjs/auth/rfc9728-discovery.d.cts +34 -0
  25. package/dist/cjs/auth/rfc9728-discovery.d.ts +34 -0
  26. package/dist/cjs/auth/rfc9728-discovery.js +436 -0
  27. package/dist/cjs/auth/rfc9728-discovery.js.map +1 -0
  28. package/dist/cjs/auth/types.d.cts +137 -0
  29. package/dist/cjs/auth/types.d.ts +137 -0
  30. package/dist/cjs/auth/types.js +9 -0
  31. package/dist/cjs/auth/types.js.map +1 -0
  32. package/dist/cjs/client-helpers.d.cts +55 -0
  33. package/dist/cjs/client-helpers.d.ts +55 -0
  34. package/dist/cjs/client-helpers.js +128 -0
  35. package/dist/cjs/client-helpers.js.map +1 -0
  36. package/dist/cjs/config/server-loader.d.cts +27 -0
  37. package/dist/cjs/config/server-loader.d.ts +27 -0
  38. package/dist/cjs/config/server-loader.js +111 -0
  39. package/dist/cjs/config/server-loader.js.map +1 -0
  40. package/dist/cjs/config/validate-config.d.cts +15 -0
  41. package/dist/cjs/config/validate-config.d.ts +15 -0
  42. package/dist/cjs/config/validate-config.js +128 -0
  43. package/dist/cjs/config/validate-config.js.map +1 -0
  44. package/dist/cjs/connection/connect-client.d.cts +59 -0
  45. package/dist/cjs/connection/connect-client.d.ts +59 -0
  46. package/dist/cjs/connection/connect-client.js +536 -0
  47. package/dist/cjs/connection/connect-client.js.map +1 -0
  48. package/dist/cjs/connection/existing-process-transport.d.cts +40 -0
  49. package/dist/cjs/connection/existing-process-transport.d.ts +40 -0
  50. package/dist/cjs/connection/existing-process-transport.js +274 -0
  51. package/dist/cjs/connection/existing-process-transport.js.map +1 -0
  52. package/dist/cjs/connection/types.d.cts +61 -0
  53. package/dist/cjs/connection/types.d.ts +61 -0
  54. package/dist/cjs/connection/types.js +53 -0
  55. package/dist/cjs/connection/types.js.map +1 -0
  56. package/dist/cjs/connection/wait-for-http-ready.d.cts +15 -0
  57. package/dist/cjs/connection/wait-for-http-ready.d.ts +15 -0
  58. package/dist/cjs/connection/wait-for-http-ready.js +232 -0
  59. package/dist/cjs/connection/wait-for-http-ready.js.map +1 -0
  60. package/dist/cjs/dcr/dcr-authenticator.d.cts +73 -0
  61. package/dist/cjs/dcr/dcr-authenticator.d.ts +73 -0
  62. package/dist/cjs/dcr/dcr-authenticator.js +655 -0
  63. package/dist/cjs/dcr/dcr-authenticator.js.map +1 -0
  64. package/dist/cjs/dcr/dynamic-client-registrar.d.cts +28 -0
  65. package/dist/cjs/dcr/dynamic-client-registrar.d.ts +28 -0
  66. package/dist/cjs/dcr/dynamic-client-registrar.js +245 -0
  67. package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -0
  68. package/dist/cjs/dcr/index.d.cts +8 -0
  69. package/dist/cjs/dcr/index.d.ts +8 -0
  70. package/dist/cjs/dcr/index.js +24 -0
  71. package/dist/cjs/dcr/index.js.map +1 -0
  72. package/dist/cjs/index.d.cts +21 -0
  73. package/dist/cjs/index.d.ts +21 -0
  74. package/dist/cjs/index.js +94 -0
  75. package/dist/cjs/index.js.map +1 -0
  76. package/dist/cjs/monkey-patches.d.cts +6 -0
  77. package/dist/cjs/monkey-patches.d.ts +6 -0
  78. package/dist/cjs/monkey-patches.js +236 -0
  79. package/dist/cjs/monkey-patches.js.map +1 -0
  80. package/dist/cjs/package.json +1 -0
  81. package/dist/cjs/response-wrappers.d.cts +41 -0
  82. package/dist/cjs/response-wrappers.d.ts +41 -0
  83. package/dist/cjs/response-wrappers.js +443 -0
  84. package/dist/cjs/response-wrappers.js.map +1 -0
  85. package/dist/cjs/search/index.d.cts +6 -0
  86. package/dist/cjs/search/index.d.ts +6 -0
  87. package/dist/cjs/search/index.js +25 -0
  88. package/dist/cjs/search/index.js.map +1 -0
  89. package/dist/cjs/search/search.d.cts +22 -0
  90. package/dist/cjs/search/search.d.ts +22 -0
  91. package/dist/cjs/search/search.js +630 -0
  92. package/dist/cjs/search/search.js.map +1 -0
  93. package/dist/cjs/search/types.d.cts +122 -0
  94. package/dist/cjs/search/types.d.ts +122 -0
  95. package/dist/cjs/search/types.js +10 -0
  96. package/dist/cjs/search/types.js.map +1 -0
  97. package/dist/cjs/spawn/spawn-server.d.cts +83 -0
  98. package/dist/cjs/spawn/spawn-server.d.ts +83 -0
  99. package/dist/cjs/spawn/spawn-server.js +410 -0
  100. package/dist/cjs/spawn/spawn-server.js.map +1 -0
  101. package/dist/cjs/spawn/spawn-servers.d.cts +151 -0
  102. package/dist/cjs/spawn/spawn-servers.d.ts +151 -0
  103. package/dist/cjs/spawn/spawn-servers.js +911 -0
  104. package/dist/cjs/spawn/spawn-servers.js.map +1 -0
  105. package/dist/cjs/types.d.cts +11 -0
  106. package/dist/cjs/types.d.ts +11 -0
  107. package/dist/cjs/types.js +10 -0
  108. package/dist/cjs/types.js.map +1 -0
  109. package/dist/cjs/utils/logger.d.cts +24 -0
  110. package/dist/cjs/utils/logger.d.ts +24 -0
  111. package/dist/cjs/utils/logger.js +80 -0
  112. package/dist/cjs/utils/logger.js.map +1 -0
  113. package/dist/cjs/utils/path-utils.d.cts +45 -0
  114. package/dist/cjs/utils/path-utils.d.ts +45 -0
  115. package/dist/cjs/utils/path-utils.js +158 -0
  116. package/dist/cjs/utils/path-utils.js.map +1 -0
  117. package/dist/cjs/utils/sanitizer.d.cts +30 -0
  118. package/dist/cjs/utils/sanitizer.d.ts +30 -0
  119. package/dist/cjs/utils/sanitizer.js +124 -0
  120. package/dist/cjs/utils/sanitizer.js.map +1 -0
  121. package/dist/esm/auth/capability-discovery.d.ts +25 -0
  122. package/dist/esm/auth/capability-discovery.js +110 -0
  123. package/dist/esm/auth/capability-discovery.js.map +1 -0
  124. package/dist/esm/auth/index.d.ts +9 -0
  125. package/dist/esm/auth/index.js +6 -0
  126. package/dist/esm/auth/index.js.map +1 -0
  127. package/dist/esm/auth/interactive-oauth-flow.d.ts +58 -0
  128. package/dist/esm/auth/interactive-oauth-flow.js +217 -0
  129. package/dist/esm/auth/interactive-oauth-flow.js.map +1 -0
  130. package/dist/esm/auth/oauth-callback-listener.d.ts +56 -0
  131. package/dist/esm/auth/oauth-callback-listener.js +166 -0
  132. package/dist/esm/auth/oauth-callback-listener.js.map +1 -0
  133. package/dist/esm/auth/pkce.d.ts +17 -0
  134. package/dist/esm/auth/pkce.js +41 -0
  135. package/dist/esm/auth/pkce.js.map +1 -0
  136. package/dist/esm/auth/rfc9728-discovery.d.ts +34 -0
  137. package/dist/esm/auth/rfc9728-discovery.js +157 -0
  138. package/dist/esm/auth/rfc9728-discovery.js.map +1 -0
  139. package/dist/esm/auth/types.d.ts +137 -0
  140. package/dist/esm/auth/types.js +7 -0
  141. package/dist/esm/auth/types.js.map +1 -0
  142. package/dist/esm/client-helpers.d.ts +55 -0
  143. package/dist/esm/client-helpers.js +81 -0
  144. package/dist/esm/client-helpers.js.map +1 -0
  145. package/dist/esm/config/server-loader.d.ts +27 -0
  146. package/dist/esm/config/server-loader.js +49 -0
  147. package/dist/esm/config/server-loader.js.map +1 -0
  148. package/dist/esm/config/validate-config.d.ts +15 -0
  149. package/dist/esm/config/validate-config.js +76 -0
  150. package/dist/esm/config/validate-config.js.map +1 -0
  151. package/dist/esm/connection/connect-client.d.ts +59 -0
  152. package/dist/esm/connection/connect-client.js +272 -0
  153. package/dist/esm/connection/connect-client.js.map +1 -0
  154. package/dist/esm/connection/existing-process-transport.d.ts +40 -0
  155. package/dist/esm/connection/existing-process-transport.js +103 -0
  156. package/dist/esm/connection/existing-process-transport.js.map +1 -0
  157. package/dist/esm/connection/types.d.ts +61 -0
  158. package/dist/esm/connection/types.js +34 -0
  159. package/dist/esm/connection/types.js.map +1 -0
  160. package/dist/esm/connection/wait-for-http-ready.d.ts +15 -0
  161. package/dist/esm/connection/wait-for-http-ready.js +43 -0
  162. package/dist/esm/connection/wait-for-http-ready.js.map +1 -0
  163. package/dist/esm/dcr/dcr-authenticator.d.ts +73 -0
  164. package/dist/esm/dcr/dcr-authenticator.js +235 -0
  165. package/dist/esm/dcr/dcr-authenticator.js.map +1 -0
  166. package/dist/esm/dcr/dynamic-client-registrar.d.ts +28 -0
  167. package/dist/esm/dcr/dynamic-client-registrar.js +66 -0
  168. package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -0
  169. package/dist/esm/dcr/index.d.ts +8 -0
  170. package/dist/esm/dcr/index.js +5 -0
  171. package/dist/esm/dcr/index.js.map +1 -0
  172. package/dist/esm/index.d.ts +21 -0
  173. package/dist/esm/index.js +22 -0
  174. package/dist/esm/index.js.map +1 -0
  175. package/dist/esm/monkey-patches.d.ts +6 -0
  176. package/dist/esm/monkey-patches.js +32 -0
  177. package/dist/esm/monkey-patches.js.map +1 -0
  178. package/dist/esm/package.json +1 -0
  179. package/dist/esm/response-wrappers.d.ts +41 -0
  180. package/dist/esm/response-wrappers.js +201 -0
  181. package/dist/esm/response-wrappers.js.map +1 -0
  182. package/dist/esm/search/index.d.ts +6 -0
  183. package/dist/esm/search/index.js +3 -0
  184. package/dist/esm/search/index.js.map +1 -0
  185. package/dist/esm/search/search.d.ts +22 -0
  186. package/dist/esm/search/search.js +236 -0
  187. package/dist/esm/search/search.js.map +1 -0
  188. package/dist/esm/search/types.d.ts +122 -0
  189. package/dist/esm/search/types.js +8 -0
  190. package/dist/esm/search/types.js.map +1 -0
  191. package/dist/esm/spawn/spawn-server.d.ts +83 -0
  192. package/dist/esm/spawn/spawn-server.js +145 -0
  193. package/dist/esm/spawn/spawn-server.js.map +1 -0
  194. package/dist/esm/spawn/spawn-servers.d.ts +151 -0
  195. package/dist/esm/spawn/spawn-servers.js +406 -0
  196. package/dist/esm/spawn/spawn-servers.js.map +1 -0
  197. package/dist/esm/types.d.ts +11 -0
  198. package/dist/esm/types.js +9 -0
  199. package/dist/esm/types.js.map +1 -0
  200. package/dist/esm/utils/logger.d.ts +24 -0
  201. package/dist/esm/utils/logger.js +59 -0
  202. package/dist/esm/utils/logger.js.map +1 -0
  203. package/dist/esm/utils/path-utils.d.ts +45 -0
  204. package/dist/esm/utils/path-utils.js +89 -0
  205. package/dist/esm/utils/path-utils.js.map +1 -0
  206. package/dist/esm/utils/sanitizer.d.ts +30 -0
  207. package/dist/esm/utils/sanitizer.js +43 -0
  208. package/dist/esm/utils/sanitizer.js.map +1 -0
  209. package/package.json +92 -0
  210. package/schemas/servers.d.ts +90 -0
  211. package/schemas/servers.schema.json +104 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * OAuth Callback Server for CLI Authentication
3
+ * Listens for OAuth authorization callbacks and captures authorization code
4
+ */
5
+ import { type Logger } from '../utils/logger.js';
6
+ import type { CallbackResult } from './types.js';
7
+ export interface OAuthCallbackListenerOptions {
8
+ /** Port to listen on (required - use get-port package to find available port) */
9
+ port: number;
10
+ /** Optional logger for debug output (defaults to singleton logger) */
11
+ logger?: Logger;
12
+ }
13
+ /**
14
+ * OAuthCallbackListener handles OAuth redirect callbacks
15
+ * Starts a temporary HTTP server to receive authorization code
16
+ *
17
+ * Note: Caller is responsible for finding an available port using get-port package
18
+ */
19
+ export declare class OAuthCallbackListener {
20
+ private server;
21
+ private resolveCallback?;
22
+ private rejectCallback?;
23
+ private timeout;
24
+ private port;
25
+ private logger;
26
+ constructor(options: OAuthCallbackListenerOptions);
27
+ /**
28
+ * Start the callback server
29
+ * Fails fast if port is already in use - caller should use get-port to find available port
30
+ */
31
+ start(): Promise<void>;
32
+ /**
33
+ * Listen on a specific port
34
+ */
35
+ private listen;
36
+ /**
37
+ * Handle incoming HTTP requests
38
+ */
39
+ private handleRequest;
40
+ /**
41
+ * Handle OAuth callback
42
+ */
43
+ private handleCallback;
44
+ /**
45
+ * Wait for OAuth callback with timeout
46
+ */
47
+ waitForCallback(timeoutMs?: number): Promise<CallbackResult>;
48
+ /**
49
+ * Stop the callback server and close
50
+ */
51
+ stop(): Promise<void>;
52
+ /**
53
+ * Get the callback URL for this server
54
+ */
55
+ getCallbackUrl(): string;
56
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * OAuth Callback Server for CLI Authentication
3
+ * Listens for OAuth authorization callbacks and captures authorization code
4
+ */ import http from 'node:http';
5
+ import { logger as defaultLogger } from '../utils/logger.js';
6
+ /**
7
+ * OAuthCallbackListener handles OAuth redirect callbacks
8
+ * Starts a temporary HTTP server to receive authorization code
9
+ *
10
+ * Note: Caller is responsible for finding an available port using get-port package
11
+ */ export class OAuthCallbackListener {
12
+ /**
13
+ * Start the callback server
14
+ * Fails fast if port is already in use - caller should use get-port to find available port
15
+ */ async start() {
16
+ await this.listen(this.port);
17
+ this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);
18
+ }
19
+ /**
20
+ * Listen on a specific port
21
+ */ listen(port) {
22
+ return new Promise((resolve, reject)=>{
23
+ this.server = http.createServer((req, res)=>{
24
+ this.handleRequest(req, res);
25
+ });
26
+ this.server.on('error', (error)=>{
27
+ reject(error);
28
+ });
29
+ this.server.listen(port, ()=>{
30
+ resolve();
31
+ });
32
+ });
33
+ }
34
+ /**
35
+ * Handle incoming HTTP requests
36
+ */ handleRequest(req, res) {
37
+ const url = new URL(req.url || '', `http://localhost:${this.port}`);
38
+ if (url.pathname === '/callback') {
39
+ this.handleCallback(url, res);
40
+ } else {
41
+ res.writeHead(404, {
42
+ 'Content-Type': 'text/plain'
43
+ });
44
+ res.end('Not Found');
45
+ }
46
+ }
47
+ /**
48
+ * Handle OAuth callback
49
+ */ handleCallback(url, res) {
50
+ const code = url.searchParams.get('code');
51
+ const state = url.searchParams.get('state');
52
+ const error = url.searchParams.get('error');
53
+ const errorDescription = url.searchParams.get('error_description');
54
+ // Handle OAuth errors
55
+ if (error) {
56
+ const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;
57
+ res.writeHead(400, {
58
+ 'Content-Type': 'text/html'
59
+ });
60
+ res.end(`
61
+ <html>
62
+ <body>
63
+ <h1>Authorization Failed</h1>
64
+ <p>${errorMessage}</p>
65
+ <script>setTimeout(() => window.close(), 3000);</script>
66
+ </body>
67
+ </html>
68
+ `);
69
+ if (this.rejectCallback) {
70
+ this.rejectCallback(new Error(errorMessage));
71
+ }
72
+ return;
73
+ }
74
+ // Validate code parameter
75
+ if (!code) {
76
+ res.writeHead(400, {
77
+ 'Content-Type': 'text/html'
78
+ });
79
+ res.end(`
80
+ <html>
81
+ <body>
82
+ <h1>Invalid Callback</h1>
83
+ <p>Missing authorization code</p>
84
+ <script>setTimeout(() => window.close(), 3000);</script>
85
+ </body>
86
+ </html>
87
+ `);
88
+ if (this.rejectCallback) {
89
+ this.rejectCallback(new Error('Missing authorization code'));
90
+ }
91
+ return;
92
+ }
93
+ // Success - send confirmation page
94
+ res.writeHead(200, {
95
+ 'Content-Type': 'text/html; charset=utf-8'
96
+ });
97
+ res.end(`
98
+ <html>
99
+ <head>
100
+ <meta charset="UTF-8">
101
+ </head>
102
+ <body>
103
+ <h1>Authorization Successful</h1>
104
+ <p>You can close this window and return to the terminal.</p>
105
+ <script>setTimeout(() => window.close(), 2000);</script>
106
+ </body>
107
+ </html>
108
+ `);
109
+ // Resolve the promise with authorization code
110
+ if (this.resolveCallback) {
111
+ const result = {
112
+ code
113
+ };
114
+ if (state) {
115
+ result.state = state;
116
+ }
117
+ this.resolveCallback(result);
118
+ }
119
+ }
120
+ /**
121
+ * Wait for OAuth callback with timeout
122
+ */ async waitForCallback(timeoutMs = 300000) {
123
+ return new Promise((resolve, reject)=>{
124
+ this.resolveCallback = resolve;
125
+ this.rejectCallback = reject;
126
+ // Set timeout to prevent hanging forever
127
+ this.timeout = setTimeout(()=>{
128
+ reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));
129
+ this.stop();
130
+ }, timeoutMs);
131
+ });
132
+ }
133
+ /**
134
+ * Stop the callback server and close
135
+ */ async stop() {
136
+ // Clear the timeout
137
+ if (this.timeout) {
138
+ clearTimeout(this.timeout);
139
+ this.timeout = undefined;
140
+ }
141
+ // Close the server
142
+ if (this.server) {
143
+ await new Promise((resolve)=>{
144
+ var _this_server;
145
+ (_this_server = this.server) === null || _this_server === void 0 ? void 0 : _this_server.close(()=>{
146
+ this.logger.debug('🔒 Callback server closed');
147
+ resolve();
148
+ });
149
+ });
150
+ this.server = undefined;
151
+ }
152
+ }
153
+ /**
154
+ * Get the callback URL for this server
155
+ */ getCallbackUrl() {
156
+ if (!this.port) {
157
+ throw new Error('Server not started - call start() first');
158
+ }
159
+ return `http://localhost:${this.port}/callback`;
160
+ }
161
+ constructor(options){
162
+ var _options_logger;
163
+ this.port = options.port;
164
+ this.logger = (_options_logger = options.logger) !== null && _options_logger !== void 0 ? _options_logger : defaultLogger;
165
+ }
166
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/libs/client/src/auth/oauth-callback-listener.ts"],"sourcesContent":["/**\n * OAuth Callback Server for CLI Authentication\n * Listens for OAuth authorization callbacks and captures authorization code\n */\n\nimport http from 'node:http';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport type { CallbackResult } from './types.ts';\n\nexport interface OAuthCallbackListenerOptions {\n /** Port to listen on (required - use get-port package to find available port) */\n port: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * OAuthCallbackListener handles OAuth redirect callbacks\n * Starts a temporary HTTP server to receive authorization code\n *\n * Note: Caller is responsible for finding an available port using get-port package\n */\nexport class OAuthCallbackListener {\n private server: http.Server | undefined;\n private resolveCallback?: (result: CallbackResult) => void;\n private rejectCallback?: (error: Error) => void;\n private timeout: NodeJS.Timeout | undefined;\n private port: number;\n private logger: Logger;\n\n constructor(options: OAuthCallbackListenerOptions) {\n this.port = options.port;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Start the callback server\n * Fails fast if port is already in use - caller should use get-port to find available port\n */\n async start(): Promise<void> {\n await this.listen(this.port);\n this.logger.debug(`✅ Callback server listening on http://localhost:${this.port}/callback`);\n }\n\n /**\n * Listen on a specific port\n */\n private listen(port: number): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n this.handleRequest(req, res);\n });\n\n this.server.on('error', (error) => {\n reject(error);\n });\n\n this.server.listen(port, () => {\n resolve();\n });\n });\n }\n\n /**\n * Handle incoming HTTP requests\n */\n private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '', `http://localhost:${this.port}`);\n\n if (url.pathname === '/callback') {\n this.handleCallback(url, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n }\n }\n\n /**\n * Handle OAuth callback\n */\n private handleCallback(url: URL, res: http.ServerResponse): void {\n const code = url.searchParams.get('code');\n const state = url.searchParams.get('state');\n const error = url.searchParams.get('error');\n const errorDescription = url.searchParams.get('error_description');\n\n // Handle OAuth errors\n if (error) {\n const errorMessage = errorDescription ? `${error}: ${errorDescription}` : error;\n\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Authorization Failed</h1>\n <p>${errorMessage}</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error(errorMessage));\n }\n return;\n }\n\n // Validate code parameter\n if (!code) {\n res.writeHead(400, { 'Content-Type': 'text/html' });\n res.end(`\n <html>\n <body>\n <h1>Invalid Callback</h1>\n <p>Missing authorization code</p>\n <script>setTimeout(() => window.close(), 3000);</script>\n </body>\n </html>\n `);\n\n if (this.rejectCallback) {\n this.rejectCallback(new Error('Missing authorization code'));\n }\n return;\n }\n\n // Success - send confirmation page\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(`\n <html>\n <head>\n <meta charset=\"UTF-8\">\n </head>\n <body>\n <h1>Authorization Successful</h1>\n <p>You can close this window and return to the terminal.</p>\n <script>setTimeout(() => window.close(), 2000);</script>\n </body>\n </html>\n `);\n\n // Resolve the promise with authorization code\n if (this.resolveCallback) {\n const result: CallbackResult = { code };\n if (state) {\n result.state = state;\n }\n this.resolveCallback(result);\n }\n }\n\n /**\n * Wait for OAuth callback with timeout\n */\n async waitForCallback(timeoutMs = 300000): Promise<CallbackResult> {\n return new Promise((resolve, reject) => {\n this.resolveCallback = resolve;\n this.rejectCallback = reject;\n\n // Set timeout to prevent hanging forever\n this.timeout = setTimeout(() => {\n reject(new Error(`Authorization timeout - no callback received within ${timeoutMs / 1000} seconds`));\n this.stop();\n }, timeoutMs);\n });\n }\n\n /**\n * Stop the callback server and close\n */\n async stop(): Promise<void> {\n // Clear the timeout\n if (this.timeout) {\n clearTimeout(this.timeout);\n this.timeout = undefined;\n }\n\n // Close the server\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server?.close(() => {\n this.logger.debug('🔒 Callback server closed');\n resolve();\n });\n });\n this.server = undefined;\n }\n }\n\n /**\n * Get the callback URL for this server\n */\n getCallbackUrl(): string {\n if (!this.port) {\n throw new Error('Server not started - call start() first');\n }\n return `http://localhost:${this.port}/callback`;\n }\n}\n"],"names":["http","logger","defaultLogger","OAuthCallbackListener","start","listen","port","debug","Promise","resolve","reject","server","createServer","req","res","handleRequest","on","error","url","URL","pathname","handleCallback","writeHead","end","code","searchParams","get","state","errorDescription","errorMessage","rejectCallback","Error","resolveCallback","result","waitForCallback","timeoutMs","timeout","setTimeout","stop","clearTimeout","undefined","close","getCallbackUrl","options"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange) utilities
3
+ * Implements RFC 7636 for OAuth 2.0 public client security
4
+ */
5
+ import type { PkceParams } from './types.js';
6
+ /**
7
+ * Generate PKCE parameters for OAuth 2.0 authorization code flow
8
+ * Uses S256 method (SHA-256 hash) as recommended by RFC 7636
9
+ *
10
+ * @returns PkceParams with code verifier, challenge, and method
11
+ *
12
+ * @example
13
+ * const pkce = await generatePkce();
14
+ * // Use pkce.codeChallenge and pkce.codeChallengeMethod in authorization URL
15
+ * // Store pkce.codeVerifier for token exchange
16
+ */
17
+ export declare function generatePkce(): Promise<PkceParams>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * PKCE (Proof Key for Code Exchange) utilities
3
+ * Implements RFC 7636 for OAuth 2.0 public client security
4
+ */ import { createHash, randomBytes } from 'node:crypto';
5
+ /**
6
+ * Generate random code verifier for PKCE (RFC 7636 Section 4.1)
7
+ * Returns cryptographically random string of 43-128 characters using base64url encoding
8
+ */ function generateRandomCodeVerifier() {
9
+ // RFC 7636 recommends 43-128 characters
10
+ // Using 32 random bytes -> 43 base64url characters
11
+ return randomBytes(32).toString('base64url');
12
+ }
13
+ /**
14
+ * Calculate PKCE code challenge from code verifier (RFC 7636 Section 4.2)
15
+ * Uses S256 method: BASE64URL(SHA256(ASCII(code_verifier)))
16
+ */ async function calculatePKCECodeChallenge(codeVerifier) {
17
+ const hash = createHash('sha256').update(codeVerifier, 'ascii').digest();
18
+ return Buffer.from(hash).toString('base64url');
19
+ }
20
+ /**
21
+ * Generate PKCE parameters for OAuth 2.0 authorization code flow
22
+ * Uses S256 method (SHA-256 hash) as recommended by RFC 7636
23
+ *
24
+ * @returns PkceParams with code verifier, challenge, and method
25
+ *
26
+ * @example
27
+ * const pkce = await generatePkce();
28
+ * // Use pkce.codeChallenge and pkce.codeChallengeMethod in authorization URL
29
+ * // Store pkce.codeVerifier for token exchange
30
+ */ export async function generatePkce() {
31
+ // Generate cryptographically random code verifier (RFC 7636 § 4.1)
32
+ const codeVerifier = generateRandomCodeVerifier();
33
+ // Generate code challenge using S256 method (RFC 7636 § 4.2)
34
+ // S256: BASE64URL(SHA256(ASCII(code_verifier)))
35
+ const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
36
+ return {
37
+ codeVerifier,
38
+ codeChallenge,
39
+ codeChallengeMethod: 'S256'
40
+ };
41
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/libs/client/src/auth/pkce.ts"],"sourcesContent":["/**\n * PKCE (Proof Key for Code Exchange) utilities\n * Implements RFC 7636 for OAuth 2.0 public client security\n */\n\nimport { createHash, randomBytes } from 'node:crypto';\nimport type { PkceParams } from './types.ts';\n\n/**\n * Generate random code verifier for PKCE (RFC 7636 Section 4.1)\n * Returns cryptographically random string of 43-128 characters using base64url encoding\n */\nfunction generateRandomCodeVerifier(): string {\n // RFC 7636 recommends 43-128 characters\n // Using 32 random bytes -> 43 base64url characters\n return randomBytes(32).toString('base64url');\n}\n\n/**\n * Calculate PKCE code challenge from code verifier (RFC 7636 Section 4.2)\n * Uses S256 method: BASE64URL(SHA256(ASCII(code_verifier)))\n */\nasync function calculatePKCECodeChallenge(codeVerifier: string): Promise<string> {\n const hash = createHash('sha256').update(codeVerifier, 'ascii').digest();\n return Buffer.from(hash).toString('base64url');\n}\n\n/**\n * Generate PKCE parameters for OAuth 2.0 authorization code flow\n * Uses S256 method (SHA-256 hash) as recommended by RFC 7636\n *\n * @returns PkceParams with code verifier, challenge, and method\n *\n * @example\n * const pkce = await generatePkce();\n * // Use pkce.codeChallenge and pkce.codeChallengeMethod in authorization URL\n * // Store pkce.codeVerifier for token exchange\n */\nexport async function generatePkce(): Promise<PkceParams> {\n // Generate cryptographically random code verifier (RFC 7636 § 4.1)\n const codeVerifier = generateRandomCodeVerifier();\n\n // Generate code challenge using S256 method (RFC 7636 § 4.2)\n // S256: BASE64URL(SHA256(ASCII(code_verifier)))\n const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);\n\n return {\n codeVerifier,\n codeChallenge,\n codeChallengeMethod: 'S256',\n };\n}\n"],"names":["createHash","randomBytes","generateRandomCodeVerifier","toString","calculatePKCECodeChallenge","codeVerifier","hash","update","digest","Buffer","from","generatePkce","codeChallenge","codeChallengeMethod"],"mappings":"AAAA;;;CAGC,GAED,SAASA,UAAU,EAAEC,WAAW,QAAQ,cAAc;AAGtD;;;CAGC,GACD,SAASC;IACP,wCAAwC;IACxC,mDAAmD;IACnD,OAAOD,YAAY,IAAIE,QAAQ,CAAC;AAClC;AAEA;;;CAGC,GACD,eAAeC,2BAA2BC,YAAoB;IAC5D,MAAMC,OAAON,WAAW,UAAUO,MAAM,CAACF,cAAc,SAASG,MAAM;IACtE,OAAOC,OAAOC,IAAI,CAACJ,MAAMH,QAAQ,CAAC;AACpC;AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAeQ;IACpB,mEAAmE;IACnE,MAAMN,eAAeH;IAErB,6DAA6D;IAC7D,gDAAgD;IAChD,MAAMU,gBAAgB,MAAMR,2BAA2BC;IAEvD,OAAO;QACLA;QACAO;QACAC,qBAAqB;IACvB;AACF"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * RFC 9728 Protected Resource Metadata Discovery
3
+ * Probes .well-known/oauth-protected-resource endpoint
4
+ */
5
+ import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
6
+ /**
7
+ * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
8
+ * Probes .well-known/oauth-protected-resource endpoint
9
+ *
10
+ * Discovery Strategy:
11
+ * 1. Try origin root: {origin}/.well-known/oauth-protected-resource
12
+ * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
13
+ *
14
+ * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
15
+ * @returns ProtectedResourceMetadata if discovered, null otherwise
16
+ *
17
+ * @example
18
+ * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
19
+ * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
20
+ * // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
21
+ */
22
+ export declare function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null>;
23
+ /**
24
+ * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)
25
+ * Probes .well-known/oauth-authorization-server endpoint
26
+ *
27
+ * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
28
+ * @returns AuthorizationServerMetadata if discovered, null otherwise
29
+ *
30
+ * @example
31
+ * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');
32
+ * // Returns: { issuer: "https://todoist.com", authorization_endpoint: "...", ... }
33
+ */
34
+ export declare function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null>;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * RFC 9728 Protected Resource Metadata Discovery
3
+ * Probes .well-known/oauth-protected-resource endpoint
4
+ */ /**
5
+ * Extract origin (protocol + host) from a URL
6
+ * @param url - Full URL that may include a path
7
+ * @returns Origin (e.g., "https://example.com") or original string if invalid URL
8
+ *
9
+ * @example
10
+ * getOrigin('https://example.com/mcp') // → 'https://example.com'
11
+ * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'
12
+ */ function getOrigin(url) {
13
+ try {
14
+ return new URL(url).origin;
15
+ } catch {
16
+ // Invalid URL - return as-is for graceful degradation
17
+ return url;
18
+ }
19
+ }
20
+ /**
21
+ * Extract path from a URL (without origin)
22
+ * @param url - Full URL
23
+ * @returns Path component (e.g., "/mcp", "/api/v1/mcp") or empty string if no path
24
+ */ function getPath(url) {
25
+ try {
26
+ const parsed = new URL(url);
27
+ // pathname includes leading slash, e.g., "/mcp"
28
+ return parsed.pathname === '/' ? '' : parsed.pathname;
29
+ } catch {
30
+ return '';
31
+ }
32
+ }
33
+ /**
34
+ * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
35
+ * Probes .well-known/oauth-protected-resource endpoint
36
+ *
37
+ * Discovery Strategy:
38
+ * 1. Try origin root: {origin}/.well-known/oauth-protected-resource
39
+ * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
40
+ *
41
+ * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
42
+ * @returns ProtectedResourceMetadata if discovered, null otherwise
43
+ *
44
+ * @example
45
+ * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
46
+ * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
47
+ * // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
48
+ */ export async function discoverProtectedResourceMetadata(resourceUrl) {
49
+ try {
50
+ const origin = getOrigin(resourceUrl);
51
+ const path = getPath(resourceUrl);
52
+ // Strategy 1: Try root location (REQUIRED by RFC 9728)
53
+ const rootUrl = `${origin}/.well-known/oauth-protected-resource`;
54
+ try {
55
+ const response = await fetch(rootUrl, {
56
+ method: 'GET',
57
+ headers: {
58
+ Accept: 'application/json',
59
+ Connection: 'close'
60
+ }
61
+ });
62
+ if (response.ok) {
63
+ const metadata = await response.json();
64
+ // Check if the discovered resource matches what we're looking for
65
+ if (metadata.resource === resourceUrl) {
66
+ return metadata;
67
+ }
68
+ // If there's no path component, return root metadata
69
+ // (e.g., looking for http://example.com and found it)
70
+ if (!path) {
71
+ return metadata;
72
+ }
73
+ // If requested URL starts with metadata.resource, the root metadata applies to sub-paths
74
+ // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)
75
+ if (resourceUrl.startsWith(metadata.resource)) {
76
+ // Still try sub-path location to see if there's more specific metadata
77
+ // But save root metadata as fallback
78
+ const rootMetadata = metadata;
79
+ // Try sub-path location for more specific metadata
80
+ const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;
81
+ try {
82
+ const subPathResponse = await fetch(subPathUrl, {
83
+ method: 'GET',
84
+ headers: {
85
+ Accept: 'application/json',
86
+ Connection: 'close'
87
+ }
88
+ });
89
+ if (subPathResponse.ok) {
90
+ return await subPathResponse.json();
91
+ }
92
+ } catch {
93
+ // Sub-path failed, use root metadata
94
+ }
95
+ // Return root metadata as it applies to this resource
96
+ return rootMetadata;
97
+ }
98
+ // Otherwise, try sub-path location before giving up
99
+ }
100
+ } catch {
101
+ // Continue to sub-path location
102
+ }
103
+ // Strategy 2: Try sub-path location (MCP spec extension)
104
+ // Only try if there's a path component
105
+ if (path) {
106
+ const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;
107
+ try {
108
+ const response = await fetch(subPathUrl, {
109
+ method: 'GET',
110
+ headers: {
111
+ Accept: 'application/json',
112
+ Connection: 'close'
113
+ }
114
+ });
115
+ if (response.ok) {
116
+ return await response.json();
117
+ }
118
+ } catch {
119
+ // Fall through to return null
120
+ }
121
+ }
122
+ // Neither location found or resource didn't match
123
+ return null;
124
+ } catch (_error) {
125
+ // Network error, invalid URL, or other failure
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)
131
+ * Probes .well-known/oauth-authorization-server endpoint
132
+ *
133
+ * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
134
+ * @returns AuthorizationServerMetadata if discovered, null otherwise
135
+ *
136
+ * @example
137
+ * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');
138
+ * // Returns: { issuer: "https://todoist.com", authorization_endpoint: "...", ... }
139
+ */ export async function discoverAuthorizationServerMetadata(authServerUrl) {
140
+ try {
141
+ const origin = getOrigin(authServerUrl);
142
+ const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;
143
+ const response = await fetch(wellKnownUrl, {
144
+ method: 'GET',
145
+ headers: {
146
+ Accept: 'application/json',
147
+ Connection: 'close'
148
+ }
149
+ });
150
+ if (!response.ok) {
151
+ return null;
152
+ }
153
+ return await response.json();
154
+ } catch (_error) {
155
+ return null;
156
+ }
157
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/Projects/ai/mcp-z/libs/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const origin = getOrigin(resourceUrl);\n const path = getPath(resourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === resourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (resourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const origin = getOrigin(authServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","path","rootUrl","response","fetch","method","headers","Accept","Connection","ok","metadata","json","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","discoverAuthorizationServerMetadata","authServerUrl","wellKnownUrl"],"mappings":"AAAA;;;CAGC,GAID;;;;;;;;CAQC,GACD,SAASA,UAAUC,GAAW;IAC5B,IAAI;QACF,OAAO,IAAIC,IAAID,KAAKE,MAAM;IAC5B,EAAE,OAAM;QACN,sDAAsD;QACtD,OAAOF;IACT;AACF;AAEA;;;;CAIC,GACD,SAASG,QAAQH,GAAW;IAC1B,IAAI;QACF,MAAMI,SAAS,IAAIH,IAAID;QACvB,gDAAgD;QAChD,OAAOI,OAAOC,QAAQ,KAAK,MAAM,KAAKD,OAAOC,QAAQ;IACvD,EAAE,OAAM;QACN,OAAO;IACT;AACF;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,eAAeC,kCAAkCC,WAAmB;IACzE,IAAI;QACF,MAAML,SAASH,UAAUQ;QACzB,MAAMC,OAAOL,QAAQI;QAErB,uDAAuD;QACvD,MAAME,UAAU,GAAGP,OAAO,qCAAqC,CAAC;QAEhE,IAAI;YACF,MAAMQ,WAAW,MAAMC,MAAMF,SAAS;gBACpCG,QAAQ;gBACRC,SAAS;oBAAEC,QAAQ;oBAAoBC,YAAY;gBAAQ;YAC7D;YAEA,IAAIL,SAASM,EAAE,EAAE;gBACf,MAAMC,WAAY,MAAMP,SAASQ,IAAI;gBACrC,kEAAkE;gBAClE,IAAID,SAASE,QAAQ,KAAKZ,aAAa;oBACrC,OAAOU;gBACT;gBACA,qDAAqD;gBACrD,sDAAsD;gBACtD,IAAI,CAACT,MAAM;oBACT,OAAOS;gBACT;gBACA,yFAAyF;gBACzF,8EAA8E;gBAC9E,IAAIV,YAAYa,UAAU,CAACH,SAASE,QAAQ,GAAG;oBAC7C,uEAAuE;oBACvE,qCAAqC;oBACrC,MAAME,eAAeJ;oBAErB,mDAAmD;oBACnD,MAAMK,aAAa,GAAGpB,OAAO,qCAAqC,EAAEM,MAAM;oBAC1E,IAAI;wBACF,MAAMe,kBAAkB,MAAMZ,MAAMW,YAAY;4BAC9CV,QAAQ;4BACRC,SAAS;gCAAEC,QAAQ;gCAAoBC,YAAY;4BAAQ;wBAC7D;wBACA,IAAIQ,gBAAgBP,EAAE,EAAE;4BACtB,OAAQ,MAAMO,gBAAgBL,IAAI;wBACpC;oBACF,EAAE,OAAM;oBACN,qCAAqC;oBACvC;oBAEA,sDAAsD;oBACtD,OAAOG;gBACT;YACA,oDAAoD;YACtD;QACF,EAAE,OAAM;QACN,gCAAgC;QAClC;QAEA,yDAAyD;QACzD,uCAAuC;QACvC,IAAIb,MAAM;YACR,MAAMc,aAAa,GAAGpB,OAAO,qCAAqC,EAAEM,MAAM;YAE1E,IAAI;gBACF,MAAME,WAAW,MAAMC,MAAMW,YAAY;oBACvCV,QAAQ;oBACRC,SAAS;wBAAEC,QAAQ;wBAAoBC,YAAY;oBAAQ;gBAC7D;gBAEA,IAAIL,SAASM,EAAE,EAAE;oBACf,OAAQ,MAAMN,SAASQ,IAAI;gBAC7B;YACF,EAAE,OAAM;YACN,8BAA8B;YAChC;QACF;QAEA,kDAAkD;QAClD,OAAO;IACT,EAAE,OAAOM,QAAQ;QACf,+CAA+C;QAC/C,OAAO;IACT;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAeC,oCAAoCC,aAAqB;IAC7E,IAAI;QACF,MAAMxB,SAASH,UAAU2B;QACzB,MAAMC,eAAe,GAAGzB,OAAO,uCAAuC,CAAC;QAEvE,MAAMQ,WAAW,MAAMC,MAAMgB,cAAc;YACzCf,QAAQ;YACRC,SAAS;gBAAEC,QAAQ;gBAAoBC,YAAY;YAAQ;QAC7D;QAEA,IAAI,CAACL,SAASM,EAAE,EAAE;YAChB,OAAO;QACT;QAEA,OAAQ,MAAMN,SAASQ,IAAI;IAC7B,EAAE,OAAOM,QAAQ;QACf,OAAO;IACT;AACF"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Shared types for OAuth and DCR authentication
3
+ */
4
+ /**
5
+ * OAuth callback result from authorization server
6
+ */
7
+ export interface CallbackResult {
8
+ /** Authorization code from OAuth server */
9
+ code: string;
10
+ /** State parameter for CSRF protection */
11
+ state?: string;
12
+ }
13
+ /**
14
+ * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)
15
+ * Used to secure OAuth 2.0 authorization code flow for public clients
16
+ */
17
+ export interface PkceParams {
18
+ /** Code verifier - cryptographically random string (43-128 characters) */
19
+ codeVerifier: string;
20
+ /** Code challenge - derived from code verifier using challenge method */
21
+ codeChallenge: string;
22
+ /** Code challenge method - S256 (SHA-256) or plain */
23
+ codeChallengeMethod: 'S256' | 'plain';
24
+ }
25
+ /**
26
+ * OAuth token set with access and refresh tokens
27
+ */
28
+ export interface TokenSet {
29
+ /** Access token for API requests */
30
+ accessToken: string;
31
+ /** Refresh token for obtaining new access tokens */
32
+ refreshToken: string;
33
+ /** Timestamp when access token expires (milliseconds since epoch) */
34
+ expiresAt: number;
35
+ /** Scopes granted for this token set */
36
+ scopes?: string[];
37
+ /** Client ID used for DCR registration (stored for future use) */
38
+ clientId?: string;
39
+ /** Client secret used for DCR registration (stored for future use) */
40
+ clientSecret?: string;
41
+ }
42
+ /**
43
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728)
44
+ * Response from .well-known/oauth-protected-resource endpoint
45
+ */
46
+ export interface ProtectedResourceMetadata {
47
+ /** The protected resource identifier */
48
+ resource: string;
49
+ /** List of authorization server URLs that can issue tokens for this resource */
50
+ authorization_servers: string[];
51
+ /** Optional list of scopes supported by this resource */
52
+ scopes_supported?: string[];
53
+ /** Optional list of bearer token methods supported (header, query, body) */
54
+ bearer_methods_supported?: string[];
55
+ }
56
+ /**
57
+ * OAuth 2.0 Authorization Server Metadata (RFC 8414)
58
+ * Response from .well-known/oauth-authorization-server endpoint
59
+ */
60
+ export interface AuthorizationServerMetadata {
61
+ /** The authorization server's issuer identifier */
62
+ issuer?: string;
63
+ /** URL of the authorization endpoint */
64
+ authorization_endpoint?: string;
65
+ /** URL of the token endpoint */
66
+ token_endpoint?: string;
67
+ /** URL of the client registration endpoint (DCR - RFC 7591) */
68
+ registration_endpoint?: string;
69
+ /** URL of the token introspection endpoint */
70
+ introspection_endpoint?: string;
71
+ /** List of OAuth scopes supported by the authorization server */
72
+ scopes_supported?: string[];
73
+ /** Response types supported (code, token, etc.) */
74
+ response_types_supported?: string[];
75
+ /** Grant types supported (authorization_code, refresh_token, etc.) */
76
+ grant_types_supported?: string[];
77
+ /** Token endpoint authentication methods supported */
78
+ token_endpoint_auth_methods_supported?: string[];
79
+ }
80
+ /**
81
+ * OAuth server capabilities discovered from .well-known endpoint
82
+ */
83
+ export interface AuthCapabilities {
84
+ /** Whether the server supports Dynamic Client Registration (RFC 7591) */
85
+ supportsDcr: boolean;
86
+ /** DCR client registration endpoint */
87
+ registrationEndpoint?: string;
88
+ /** OAuth authorization endpoint */
89
+ authorizationEndpoint?: string;
90
+ /** OAuth token endpoint */
91
+ tokenEndpoint?: string;
92
+ /** Token introspection endpoint */
93
+ introspectionEndpoint?: string;
94
+ /** Supported OAuth scopes */
95
+ scopes?: string[];
96
+ }
97
+ /**
98
+ * Client credentials from DCR registration
99
+ */
100
+ export interface ClientCredentials {
101
+ /** OAuth client ID */
102
+ clientId: string;
103
+ /** OAuth client secret */
104
+ clientSecret: string;
105
+ /** Timestamp when client was registered */
106
+ issuedAt?: number;
107
+ }
108
+ /**
109
+ * Options for DCR client registration
110
+ */
111
+ export interface DcrRegistrationOptions {
112
+ /** Client name to register */
113
+ clientName?: string;
114
+ /** Redirect URI for OAuth callback */
115
+ redirectUri?: string;
116
+ }
117
+ /**
118
+ * Options for OAuth authorization flow
119
+ */
120
+ export interface OAuthFlowOptions {
121
+ /** Port for OAuth callback listener (required - use get-port to find available port) */
122
+ port: number;
123
+ /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */
124
+ redirectUri?: string;
125
+ /** OAuth scopes to request */
126
+ scopes?: string[];
127
+ /** Resource parameter (RFC 8707) - target resource server identifier */
128
+ resource?: string;
129
+ /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */
130
+ pkce?: boolean;
131
+ /** Headless mode (don't open browser) */
132
+ headless?: boolean;
133
+ /** Timeout for callback (milliseconds) */
134
+ timeout?: number;
135
+ /** Optional logger for debug output (defaults to singleton logger) */
136
+ logger?: import('../utils/logger.js').Logger;
137
+ }