@shin1ohno/sage 0.3.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/cli/http-server-with-config.d.ts +38 -0
  2. package/dist/cli/http-server-with-config.d.ts.map +1 -0
  3. package/dist/cli/http-server-with-config.js +458 -0
  4. package/dist/cli/http-server-with-config.js.map +1 -0
  5. package/dist/cli/http-server.d.ts +74 -0
  6. package/dist/cli/http-server.d.ts.map +1 -0
  7. package/dist/cli/http-server.js +407 -0
  8. package/dist/cli/http-server.js.map +1 -0
  9. package/dist/cli/jwt-middleware.d.ts +36 -0
  10. package/dist/cli/jwt-middleware.d.ts.map +1 -0
  11. package/dist/cli/jwt-middleware.js +99 -0
  12. package/dist/cli/jwt-middleware.js.map +1 -0
  13. package/dist/cli/main-entry.d.ts +41 -0
  14. package/dist/cli/main-entry.d.ts.map +1 -0
  15. package/dist/cli/main-entry.js +80 -0
  16. package/dist/cli/main-entry.js.map +1 -0
  17. package/dist/cli/mcp-handler.d.ts +56 -0
  18. package/dist/cli/mcp-handler.d.ts.map +1 -0
  19. package/dist/cli/mcp-handler.js +2189 -0
  20. package/dist/cli/mcp-handler.js.map +1 -0
  21. package/dist/cli/parser.d.ts +43 -0
  22. package/dist/cli/parser.d.ts.map +1 -0
  23. package/dist/cli/parser.js +162 -0
  24. package/dist/cli/parser.js.map +1 -0
  25. package/dist/cli/remote-config-loader.d.ts +85 -0
  26. package/dist/cli/remote-config-loader.d.ts.map +1 -0
  27. package/dist/cli/remote-config-loader.js +129 -0
  28. package/dist/cli/remote-config-loader.js.map +1 -0
  29. package/dist/cli/secret-auth.d.ts +47 -0
  30. package/dist/cli/secret-auth.d.ts.map +1 -0
  31. package/dist/cli/secret-auth.js +165 -0
  32. package/dist/cli/secret-auth.js.map +1 -0
  33. package/dist/cli/sse-stream-handler.d.ts +45 -0
  34. package/dist/cli/sse-stream-handler.d.ts.map +1 -0
  35. package/dist/cli/sse-stream-handler.js +125 -0
  36. package/dist/cli/sse-stream-handler.js.map +1 -0
  37. package/dist/index.js +885 -209
  38. package/dist/index.js.map +1 -1
  39. package/dist/integrations/calendar-event-creator.d.ts +152 -0
  40. package/dist/integrations/calendar-event-creator.d.ts.map +1 -0
  41. package/dist/integrations/calendar-event-creator.js +507 -0
  42. package/dist/integrations/calendar-event-creator.js.map +1 -0
  43. package/dist/integrations/calendar-event-deleter.d.ts +137 -0
  44. package/dist/integrations/calendar-event-deleter.d.ts.map +1 -0
  45. package/dist/integrations/calendar-event-deleter.js +378 -0
  46. package/dist/integrations/calendar-event-deleter.js.map +1 -0
  47. package/dist/integrations/calendar-event-response.d.ts +213 -0
  48. package/dist/integrations/calendar-event-response.d.ts.map +1 -0
  49. package/dist/integrations/calendar-event-response.js +560 -0
  50. package/dist/integrations/calendar-event-response.js.map +1 -0
  51. package/dist/integrations/calendar-service.d.ts +66 -1
  52. package/dist/integrations/calendar-service.d.ts.map +1 -1
  53. package/dist/integrations/calendar-service.js +223 -0
  54. package/dist/integrations/calendar-service.js.map +1 -1
  55. package/dist/oauth/client-store.d.ts +36 -0
  56. package/dist/oauth/client-store.d.ts.map +1 -0
  57. package/dist/oauth/client-store.js +104 -0
  58. package/dist/oauth/client-store.js.map +1 -0
  59. package/dist/oauth/code-store.d.ts +48 -0
  60. package/dist/oauth/code-store.d.ts.map +1 -0
  61. package/dist/oauth/code-store.js +89 -0
  62. package/dist/oauth/code-store.js.map +1 -0
  63. package/dist/oauth/index.d.ts +13 -0
  64. package/dist/oauth/index.d.ts.map +1 -0
  65. package/dist/oauth/index.js +21 -0
  66. package/dist/oauth/index.js.map +1 -0
  67. package/dist/oauth/oauth-handler.d.ts +101 -0
  68. package/dist/oauth/oauth-handler.d.ts.map +1 -0
  69. package/dist/oauth/oauth-handler.js +577 -0
  70. package/dist/oauth/oauth-handler.js.map +1 -0
  71. package/dist/oauth/oauth-server.d.ts +165 -0
  72. package/dist/oauth/oauth-server.d.ts.map +1 -0
  73. package/dist/oauth/oauth-server.js +489 -0
  74. package/dist/oauth/oauth-server.js.map +1 -0
  75. package/dist/oauth/pkce.d.ts +48 -0
  76. package/dist/oauth/pkce.d.ts.map +1 -0
  77. package/dist/oauth/pkce.js +106 -0
  78. package/dist/oauth/pkce.js.map +1 -0
  79. package/dist/oauth/refresh-token-store.d.ts +45 -0
  80. package/dist/oauth/refresh-token-store.d.ts.map +1 -0
  81. package/dist/oauth/refresh-token-store.js +98 -0
  82. package/dist/oauth/refresh-token-store.js.map +1 -0
  83. package/dist/oauth/token-service.d.ts +46 -0
  84. package/dist/oauth/token-service.d.ts.map +1 -0
  85. package/dist/oauth/token-service.js +199 -0
  86. package/dist/oauth/token-service.js.map +1 -0
  87. package/dist/oauth/types.d.ts +264 -0
  88. package/dist/oauth/types.d.ts.map +1 -0
  89. package/dist/oauth/types.js +37 -0
  90. package/dist/oauth/types.js.map +1 -0
  91. package/dist/version.d.ts +9 -0
  92. package/dist/version.d.ts.map +1 -0
  93. package/dist/version.js +11 -0
  94. package/dist/version.js.map +1 -0
  95. package/package.json +1 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * OAuth HTTP Handler
3
+ * Requirements: 22-31 (OAuth 2.1 HTTP Endpoints)
4
+ *
5
+ * Handles OAuth HTTP endpoints including metadata, DCR, authorization, and token endpoints.
6
+ */
7
+ import { IncomingMessage, ServerResponse } from 'http';
8
+ import { OAuthServer, OAuthServerConfig } from './oauth-server.js';
9
+ /**
10
+ * OAuth Handler Configuration
11
+ */
12
+ export interface OAuthHandlerConfig extends OAuthServerConfig {
13
+ }
14
+ /**
15
+ * OAuth Handler Class
16
+ */
17
+ export declare class OAuthHandler {
18
+ private server;
19
+ constructor(server: OAuthServer, _config: OAuthHandlerConfig);
20
+ /**
21
+ * Handle an HTTP request
22
+ */
23
+ handleRequest(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
24
+ /**
25
+ * Handle Protected Resource Metadata (RFC 9728)
26
+ * Requirement 22.1-22.3
27
+ */
28
+ private handleProtectedResourceMetadata;
29
+ /**
30
+ * Handle Authorization Server Metadata (RFC 8414)
31
+ * Requirement 23.1-23.9
32
+ */
33
+ private handleAuthorizationServerMetadata;
34
+ /**
35
+ * Handle Dynamic Client Registration (RFC 7591)
36
+ * Requirement 24.1-24.8
37
+ */
38
+ private handleClientRegistration;
39
+ /**
40
+ * Handle Authorization Endpoint (GET)
41
+ * Requirement 25.1-25.10
42
+ */
43
+ private handleAuthorization;
44
+ /**
45
+ * Handle Authorization Submit (POST)
46
+ * Requirement 25.9, 25.10, 28.4, 28.5
47
+ */
48
+ private handleAuthorizationSubmit;
49
+ /**
50
+ * Handle Login Page (GET)
51
+ * Requirement 29.1
52
+ */
53
+ private handleLoginPage;
54
+ /**
55
+ * Handle Login Submit (POST)
56
+ * Requirement 29.1-29.5
57
+ */
58
+ private handleLoginSubmit;
59
+ /**
60
+ * Handle Token Endpoint (POST)
61
+ * Requirement 26.1-26.9
62
+ */
63
+ private handleToken;
64
+ /**
65
+ * Handle authorization_code grant
66
+ * Requirement 26.2, 26.4, 26.5
67
+ */
68
+ private handleAuthorizationCodeGrant;
69
+ /**
70
+ * Handle refresh_token grant
71
+ * Requirement 26.3, 26.8
72
+ */
73
+ private handleRefreshTokenGrant;
74
+ /**
75
+ * Read request body
76
+ */
77
+ private readBody;
78
+ /**
79
+ * Render login page HTML
80
+ * Requirement 29.1
81
+ */
82
+ private renderLoginPage;
83
+ /**
84
+ * Render consent page HTML
85
+ * Requirement 28.1-28.4
86
+ */
87
+ private renderConsentPage;
88
+ /**
89
+ * Render error page HTML
90
+ */
91
+ private renderErrorPage;
92
+ /**
93
+ * Escape HTML special characters
94
+ */
95
+ private escapeHtml;
96
+ }
97
+ /**
98
+ * Create an OAuth Handler instance
99
+ */
100
+ export declare function createOAuthHandler(config: OAuthHandlerConfig): Promise<OAuthHandler>;
101
+ //# sourceMappingURL=oauth-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-handler.d.ts","sourceRoot":"","sources":["../../src/oauth/oauth-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGnE;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,iBAAiB;CAE5D;AA0DD;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAc;gBAEhB,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB;IAK5D;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;IAwDhF;;;OAGG;IACH,OAAO,CAAC,+BAA+B;IAMvC;;;OAGG;IACH,OAAO,CAAC,iCAAiC;IAMzC;;;OAGG;YACW,wBAAwB;IA6BtC;;;OAGG;YACW,mBAAmB;IAmFjC;;;OAGG;YACW,yBAAyB;IAsDvC;;;OAGG;YACW,eAAe;IAM7B;;;OAGG;YACW,iBAAiB;IAyC/B;;;OAGG;YACW,WAAW;IAmBzB;;;OAGG;YACW,4BAA4B;IA+C1C;;;OAGG;YACW,uBAAuB;IAyCrC;;OAEG;IACH,OAAO,CAAC,QAAQ;IAWhB;;;OAGG;IACH,OAAO,CAAC,eAAe;IAyCvB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAiDzB;;OAEG;IACH,OAAO,CAAC,eAAe;IA0BvB;;OAEG;IACH,OAAO,CAAC,UAAU;CAQnB;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAI1F"}
@@ -0,0 +1,577 @@
1
+ /**
2
+ * OAuth HTTP Handler
3
+ * Requirements: 22-31 (OAuth 2.1 HTTP Endpoints)
4
+ *
5
+ * Handles OAuth HTTP endpoints including metadata, DCR, authorization, and token endpoints.
6
+ */
7
+ import { randomBytes } from 'crypto';
8
+ import { OAuthServer } from './oauth-server.js';
9
+ /**
10
+ * Parse URL-encoded form data
11
+ */
12
+ function parseFormData(body) {
13
+ const params = {};
14
+ const pairs = body.split('&');
15
+ for (const pair of pairs) {
16
+ const [key, value] = pair.split('=');
17
+ if (key && value !== undefined) {
18
+ params[decodeURIComponent(key)] = decodeURIComponent(value.replace(/\+/g, ' '));
19
+ }
20
+ }
21
+ return params;
22
+ }
23
+ /**
24
+ * Parse query string from URL
25
+ */
26
+ function parseQueryString(url) {
27
+ const queryStart = url.indexOf('?');
28
+ if (queryStart === -1)
29
+ return {};
30
+ return parseFormData(url.slice(queryStart + 1));
31
+ }
32
+ /**
33
+ * Get cookie value
34
+ */
35
+ function getCookie(req, name) {
36
+ const cookies = req.headers.cookie;
37
+ if (!cookies)
38
+ return null;
39
+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
40
+ return match ? decodeURIComponent(match[1]) : null;
41
+ }
42
+ /**
43
+ * Set cookie header
44
+ */
45
+ function setCookie(res, name, value, options = {}) {
46
+ const parts = [`${name}=${encodeURIComponent(value)}`];
47
+ if (options.httpOnly)
48
+ parts.push('HttpOnly');
49
+ if (options.secure)
50
+ parts.push('Secure');
51
+ if (options.sameSite)
52
+ parts.push(`SameSite=${options.sameSite}`);
53
+ if (options.maxAge !== undefined)
54
+ parts.push(`Max-Age=${options.maxAge}`);
55
+ if (options.path)
56
+ parts.push(`Path=${options.path}`);
57
+ res.setHeader('Set-Cookie', parts.join('; '));
58
+ }
59
+ /**
60
+ * OAuth Handler Class
61
+ */
62
+ export class OAuthHandler {
63
+ server;
64
+ constructor(server, _config) {
65
+ this.server = server;
66
+ // Config reserved for future use (e.g., custom templates)
67
+ }
68
+ /**
69
+ * Handle an HTTP request
70
+ */
71
+ async handleRequest(req, res) {
72
+ const url = req.url || '/';
73
+ const method = req.method || 'GET';
74
+ const path = url.split('?')[0];
75
+ // Protected Resource Metadata (RFC 9728)
76
+ if (path === '/.well-known/oauth-protected-resource' && method === 'GET') {
77
+ this.handleProtectedResourceMetadata(res);
78
+ return true;
79
+ }
80
+ // Authorization Server Metadata (RFC 8414)
81
+ if (path === '/.well-known/oauth-authorization-server' && method === 'GET') {
82
+ this.handleAuthorizationServerMetadata(res);
83
+ return true;
84
+ }
85
+ // Dynamic Client Registration
86
+ if (path === '/oauth/register' && method === 'POST') {
87
+ await this.handleClientRegistration(req, res);
88
+ return true;
89
+ }
90
+ // Authorization Endpoint
91
+ if (path === '/oauth/authorize' && method === 'GET') {
92
+ await this.handleAuthorization(req, res);
93
+ return true;
94
+ }
95
+ // Authorization Consent Submit
96
+ if (path === '/oauth/authorize' && method === 'POST') {
97
+ await this.handleAuthorizationSubmit(req, res);
98
+ return true;
99
+ }
100
+ // Login Page
101
+ if (path === '/oauth/login' && method === 'GET') {
102
+ await this.handleLoginPage(req, res);
103
+ return true;
104
+ }
105
+ // Login Submit
106
+ if (path === '/oauth/login' && method === 'POST') {
107
+ await this.handleLoginSubmit(req, res);
108
+ return true;
109
+ }
110
+ // Token Endpoint
111
+ if (path === '/oauth/token' && method === 'POST') {
112
+ await this.handleToken(req, res);
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ /**
118
+ * Handle Protected Resource Metadata (RFC 9728)
119
+ * Requirement 22.1-22.3
120
+ */
121
+ handleProtectedResourceMetadata(res) {
122
+ const metadata = this.server.getProtectedResourceMetadata();
123
+ res.writeHead(200, { 'Content-Type': 'application/json' });
124
+ res.end(JSON.stringify(metadata));
125
+ }
126
+ /**
127
+ * Handle Authorization Server Metadata (RFC 8414)
128
+ * Requirement 23.1-23.9
129
+ */
130
+ handleAuthorizationServerMetadata(res) {
131
+ const metadata = this.server.getAuthorizationServerMetadata();
132
+ res.writeHead(200, { 'Content-Type': 'application/json' });
133
+ res.end(JSON.stringify(metadata));
134
+ }
135
+ /**
136
+ * Handle Dynamic Client Registration (RFC 7591)
137
+ * Requirement 24.1-24.8
138
+ */
139
+ async handleClientRegistration(req, res) {
140
+ const body = await this.readBody(req);
141
+ let request;
142
+ try {
143
+ request = JSON.parse(body);
144
+ }
145
+ catch {
146
+ res.writeHead(400, { 'Content-Type': 'application/json' });
147
+ res.end(JSON.stringify({
148
+ error: 'invalid_client_metadata',
149
+ error_description: 'Invalid JSON body',
150
+ }));
151
+ return;
152
+ }
153
+ const result = await this.server.registerClient(request);
154
+ if (result.success && result.client) {
155
+ res.writeHead(201, { 'Content-Type': 'application/json' });
156
+ res.end(JSON.stringify(result.client));
157
+ }
158
+ else {
159
+ res.writeHead(400, { 'Content-Type': 'application/json' });
160
+ res.end(JSON.stringify({
161
+ error: result.error,
162
+ error_description: result.errorDescription,
163
+ }));
164
+ }
165
+ }
166
+ /**
167
+ * Handle Authorization Endpoint (GET)
168
+ * Requirement 25.1-25.10
169
+ */
170
+ async handleAuthorization(req, res) {
171
+ const query = parseQueryString(req.url || '');
172
+ const authRequest = {
173
+ response_type: query.response_type,
174
+ client_id: query.client_id || '',
175
+ redirect_uri: query.redirect_uri || '',
176
+ scope: query.scope || '',
177
+ state: query.state || '',
178
+ code_challenge: query.code_challenge || '',
179
+ code_challenge_method: (query.code_challenge_method || 'S256'),
180
+ resource: query.resource,
181
+ };
182
+ // Validate request
183
+ const validation = await this.server.validateAuthorizationRequest(authRequest);
184
+ if (!validation.valid) {
185
+ // If we can redirect (redirect_uri is valid), redirect with error
186
+ if (authRequest.redirect_uri && validation.error) {
187
+ const errorUrl = new URL(authRequest.redirect_uri);
188
+ errorUrl.searchParams.set('error', validation.error.error);
189
+ if (validation.error.error_description) {
190
+ errorUrl.searchParams.set('error_description', validation.error.error_description);
191
+ }
192
+ if (authRequest.state) {
193
+ errorUrl.searchParams.set('state', authRequest.state);
194
+ }
195
+ res.writeHead(302, { Location: errorUrl.toString() });
196
+ res.end();
197
+ return;
198
+ }
199
+ // Otherwise, show error page
200
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
201
+ res.end(this.renderErrorPage(validation.error?.error || 'invalid_request', validation.error?.error_description || 'Invalid authorization request'));
202
+ return;
203
+ }
204
+ // Check if user is logged in
205
+ const sessionId = getCookie(req, 'sage_session');
206
+ const session = sessionId ? this.server.validateSession(sessionId) : null;
207
+ if (!session) {
208
+ // Store pending auth request and redirect to login
209
+ const requestId = randomBytes(16).toString('hex');
210
+ this.server.storePendingAuthRequest(requestId, authRequest, validation.client);
211
+ setCookie(res, 'sage_auth_request', requestId, {
212
+ httpOnly: true,
213
+ secure: false, // Allow HTTP for reverse proxy setups
214
+ sameSite: 'Lax',
215
+ maxAge: 600, // 10 minutes
216
+ path: '/',
217
+ });
218
+ res.writeHead(302, { Location: '/oauth/login' });
219
+ res.end();
220
+ return;
221
+ }
222
+ // User is logged in, show consent page
223
+ const requestId = randomBytes(16).toString('hex');
224
+ this.server.storePendingAuthRequest(requestId, authRequest, validation.client);
225
+ setCookie(res, 'sage_auth_request', requestId, {
226
+ httpOnly: true,
227
+ secure: false, // Allow HTTP for reverse proxy setups
228
+ sameSite: 'Lax',
229
+ maxAge: 600,
230
+ path: '/',
231
+ });
232
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
233
+ res.end(this.renderConsentPage(validation.client.client_name, this.server.getScopeDescriptions(authRequest.scope || 'mcp:read')));
234
+ }
235
+ /**
236
+ * Handle Authorization Submit (POST)
237
+ * Requirement 25.9, 25.10, 28.4, 28.5
238
+ */
239
+ async handleAuthorizationSubmit(req, res) {
240
+ const body = await this.readBody(req);
241
+ const params = parseFormData(body);
242
+ const approved = params.approve === 'true';
243
+ const requestId = getCookie(req, 'sage_auth_request');
244
+ if (!requestId) {
245
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
246
+ res.end(this.renderErrorPage('invalid_request', 'Authorization request expired'));
247
+ return;
248
+ }
249
+ const pending = this.server.getPendingAuthRequest(requestId);
250
+ if (!pending) {
251
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
252
+ res.end(this.renderErrorPage('invalid_request', 'Authorization request expired'));
253
+ return;
254
+ }
255
+ const sessionId = getCookie(req, 'sage_session');
256
+ const session = sessionId ? this.server.validateSession(sessionId) : null;
257
+ if (!session) {
258
+ res.writeHead(302, { Location: '/oauth/login' });
259
+ res.end();
260
+ return;
261
+ }
262
+ const redirectUrl = new URL(pending.request.redirect_uri);
263
+ if (!approved) {
264
+ // Requirement 28.5: Denied authorization
265
+ redirectUrl.searchParams.set('error', 'access_denied');
266
+ redirectUrl.searchParams.set('error_description', 'User denied the request');
267
+ if (pending.request.state) {
268
+ redirectUrl.searchParams.set('state', pending.request.state);
269
+ }
270
+ res.writeHead(302, { Location: redirectUrl.toString() });
271
+ res.end();
272
+ return;
273
+ }
274
+ // Complete authorization
275
+ const code = await this.server.completeAuthorization(pending.request, session.userId);
276
+ redirectUrl.searchParams.set('code', code);
277
+ if (pending.request.state) {
278
+ redirectUrl.searchParams.set('state', pending.request.state);
279
+ }
280
+ res.writeHead(302, { Location: redirectUrl.toString() });
281
+ res.end();
282
+ }
283
+ /**
284
+ * Handle Login Page (GET)
285
+ * Requirement 29.1
286
+ */
287
+ async handleLoginPage(req, res) {
288
+ const error = parseQueryString(req.url || '').error;
289
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
290
+ res.end(this.renderLoginPage(error));
291
+ }
292
+ /**
293
+ * Handle Login Submit (POST)
294
+ * Requirement 29.1-29.5
295
+ */
296
+ async handleLoginSubmit(req, res) {
297
+ const body = await this.readBody(req);
298
+ const params = parseFormData(body);
299
+ const result = await this.server.authenticateUser(params.username || '', params.password || '');
300
+ if (!result.success) {
301
+ res.writeHead(302, { Location: `/oauth/login?error=${encodeURIComponent(result.error || 'Login failed')}` });
302
+ res.end();
303
+ return;
304
+ }
305
+ // Set session cookie
306
+ setCookie(res, 'sage_session', result.session.sessionId, {
307
+ httpOnly: true,
308
+ secure: false, // Allow HTTP for reverse proxy setups
309
+ sameSite: 'Lax',
310
+ maxAge: 24 * 60 * 60, // 24 hours
311
+ path: '/',
312
+ });
313
+ // Check for pending auth request
314
+ const requestId = getCookie(req, 'sage_auth_request');
315
+ if (requestId) {
316
+ const pending = this.server.getPendingAuthRequest(requestId);
317
+ if (pending) {
318
+ // Show consent page
319
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
320
+ res.end(this.renderConsentPage(pending.client.client_name, this.server.getScopeDescriptions(pending.request.scope || 'mcp:read')));
321
+ return;
322
+ }
323
+ }
324
+ // No pending request, redirect to home
325
+ res.writeHead(302, { Location: '/' });
326
+ res.end();
327
+ }
328
+ /**
329
+ * Handle Token Endpoint (POST)
330
+ * Requirement 26.1-26.9
331
+ */
332
+ async handleToken(req, res) {
333
+ const body = await this.readBody(req);
334
+ const params = parseFormData(body);
335
+ const grantType = params.grant_type;
336
+ if (grantType === 'authorization_code') {
337
+ await this.handleAuthorizationCodeGrant(params, res);
338
+ }
339
+ else if (grantType === 'refresh_token') {
340
+ await this.handleRefreshTokenGrant(params, res);
341
+ }
342
+ else {
343
+ res.writeHead(400, { 'Content-Type': 'application/json' });
344
+ res.end(JSON.stringify({
345
+ error: 'unsupported_grant_type',
346
+ error_description: 'Only authorization_code and refresh_token grants are supported',
347
+ }));
348
+ }
349
+ }
350
+ /**
351
+ * Handle authorization_code grant
352
+ * Requirement 26.2, 26.4, 26.5
353
+ */
354
+ async handleAuthorizationCodeGrant(params, res) {
355
+ const { code, client_id, redirect_uri, code_verifier, resource } = params;
356
+ if (!code || !client_id || !redirect_uri || !code_verifier) {
357
+ res.writeHead(400, { 'Content-Type': 'application/json' });
358
+ res.end(JSON.stringify({
359
+ error: 'invalid_request',
360
+ error_description: 'Missing required parameters',
361
+ }));
362
+ return;
363
+ }
364
+ // Check if client exists (Requirement 26.9)
365
+ const client = await this.server.getClient(client_id);
366
+ if (!client) {
367
+ res.writeHead(401, { 'Content-Type': 'application/json' });
368
+ res.end(JSON.stringify({
369
+ error: 'invalid_client',
370
+ error_description: 'Unknown client_id',
371
+ }));
372
+ return;
373
+ }
374
+ const result = await this.server.exchangeAuthorizationCode(code, client_id, redirect_uri, code_verifier, resource);
375
+ if (result.success && result.tokens) {
376
+ res.writeHead(200, {
377
+ 'Content-Type': 'application/json',
378
+ 'Cache-Control': 'no-store',
379
+ 'Pragma': 'no-cache',
380
+ });
381
+ res.end(JSON.stringify(result.tokens));
382
+ }
383
+ else {
384
+ res.writeHead(400, { 'Content-Type': 'application/json' });
385
+ res.end(JSON.stringify(result.error));
386
+ }
387
+ }
388
+ /**
389
+ * Handle refresh_token grant
390
+ * Requirement 26.3, 26.8
391
+ */
392
+ async handleRefreshTokenGrant(params, res) {
393
+ const { refresh_token, client_id, scope } = params;
394
+ if (!refresh_token || !client_id) {
395
+ res.writeHead(400, { 'Content-Type': 'application/json' });
396
+ res.end(JSON.stringify({
397
+ error: 'invalid_request',
398
+ error_description: 'Missing required parameters',
399
+ }));
400
+ return;
401
+ }
402
+ // Check if client exists (Requirement 26.9)
403
+ const client = await this.server.getClient(client_id);
404
+ if (!client) {
405
+ res.writeHead(401, { 'Content-Type': 'application/json' });
406
+ res.end(JSON.stringify({
407
+ error: 'invalid_client',
408
+ error_description: 'Unknown client_id',
409
+ }));
410
+ return;
411
+ }
412
+ const result = await this.server.exchangeRefreshToken(refresh_token, client_id, scope);
413
+ if (result.success && result.tokens) {
414
+ res.writeHead(200, {
415
+ 'Content-Type': 'application/json',
416
+ 'Cache-Control': 'no-store',
417
+ 'Pragma': 'no-cache',
418
+ });
419
+ res.end(JSON.stringify(result.tokens));
420
+ }
421
+ else {
422
+ res.writeHead(400, { 'Content-Type': 'application/json' });
423
+ res.end(JSON.stringify(result.error));
424
+ }
425
+ }
426
+ /**
427
+ * Read request body
428
+ */
429
+ readBody(req) {
430
+ return new Promise((resolve, reject) => {
431
+ let body = '';
432
+ req.on('data', (chunk) => {
433
+ body += chunk.toString();
434
+ });
435
+ req.on('end', () => resolve(body));
436
+ req.on('error', reject);
437
+ });
438
+ }
439
+ /**
440
+ * Render login page HTML
441
+ * Requirement 29.1
442
+ */
443
+ renderLoginPage(error) {
444
+ return `<!DOCTYPE html>
445
+ <html lang="ja">
446
+ <head>
447
+ <meta charset="UTF-8">
448
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
449
+ <title>sage ログイン</title>
450
+ <style>
451
+ * { box-sizing: border-box; margin: 0; padding: 0; }
452
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
453
+ .container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
454
+ h1 { text-align: center; margin-bottom: 1.5rem; color: #333; }
455
+ .error { background: #fee; color: #c00; padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; }
456
+ .form-group { margin-bottom: 1rem; }
457
+ label { display: block; margin-bottom: 0.5rem; color: #666; }
458
+ input { width: 100%; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; font-size: 1rem; }
459
+ input:focus { outline: none; border-color: #007bff; }
460
+ button { width: 100%; padding: 0.75rem; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
461
+ button:hover { background: #0056b3; }
462
+ </style>
463
+ </head>
464
+ <body>
465
+ <div class="container">
466
+ <h1>sage ログイン</h1>
467
+ ${error ? `<div class="error">${this.escapeHtml(error)}</div>` : ''}
468
+ <form method="POST" action="/oauth/login">
469
+ <div class="form-group">
470
+ <label for="username">ユーザー名</label>
471
+ <input type="text" id="username" name="username" required autocomplete="username">
472
+ </div>
473
+ <div class="form-group">
474
+ <label for="password">パスワード</label>
475
+ <input type="password" id="password" name="password" required autocomplete="current-password">
476
+ </div>
477
+ <button type="submit">ログイン</button>
478
+ </form>
479
+ </div>
480
+ </body>
481
+ </html>`;
482
+ }
483
+ /**
484
+ * Render consent page HTML
485
+ * Requirement 28.1-28.4
486
+ */
487
+ renderConsentPage(clientName, scopes) {
488
+ const scopeList = scopes.map(s => `<li><strong>${this.escapeHtml(s.scope)}</strong>: ${this.escapeHtml(s.description)}</li>`).join('\n');
489
+ return `<!DOCTYPE html>
490
+ <html lang="ja">
491
+ <head>
492
+ <meta charset="UTF-8">
493
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
494
+ <title>sage 認可リクエスト</title>
495
+ <style>
496
+ * { box-sizing: border-box; margin: 0; padding: 0; }
497
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
498
+ .container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 450px; }
499
+ h1 { text-align: center; margin-bottom: 1rem; color: #333; font-size: 1.5rem; }
500
+ .client-name { text-align: center; font-size: 1.2rem; color: #007bff; margin-bottom: 1.5rem; }
501
+ p { margin-bottom: 1rem; color: #666; }
502
+ ul { margin: 1rem 0 1.5rem 1.5rem; }
503
+ li { margin-bottom: 0.5rem; }
504
+ .buttons { display: flex; gap: 1rem; }
505
+ button { flex: 1; padding: 0.75rem; border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; }
506
+ .approve { background: #28a745; color: white; }
507
+ .approve:hover { background: #218838; }
508
+ .deny { background: #dc3545; color: white; }
509
+ .deny:hover { background: #c82333; }
510
+ </style>
511
+ </head>
512
+ <body>
513
+ <div class="container">
514
+ <h1>sage 認可リクエスト</h1>
515
+ <div class="client-name">${this.escapeHtml(clientName)}</div>
516
+ <p>上記のアプリケーションがあなたの sage アカウントへのアクセスを要求しています。</p>
517
+ <p><strong>要求されている権限:</strong></p>
518
+ <ul>${scopeList}</ul>
519
+ <form method="POST" action="/oauth/authorize">
520
+ <div class="buttons">
521
+ <button type="submit" name="approve" value="true" class="approve">許可</button>
522
+ <button type="submit" name="approve" value="false" class="deny">拒否</button>
523
+ </div>
524
+ </form>
525
+ </div>
526
+ </body>
527
+ </html>`;
528
+ }
529
+ /**
530
+ * Render error page HTML
531
+ */
532
+ renderErrorPage(error, description) {
533
+ return `<!DOCTYPE html>
534
+ <html lang="ja">
535
+ <head>
536
+ <meta charset="UTF-8">
537
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
538
+ <title>sage エラー</title>
539
+ <style>
540
+ * { box-sizing: border-box; margin: 0; padding: 0; }
541
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
542
+ .container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 400px; text-align: center; }
543
+ h1 { color: #dc3545; margin-bottom: 1rem; }
544
+ .error-code { background: #f8f9fa; padding: 0.5rem 1rem; border-radius: 4px; font-family: monospace; margin-bottom: 1rem; display: inline-block; }
545
+ p { color: #666; }
546
+ </style>
547
+ </head>
548
+ <body>
549
+ <div class="container">
550
+ <h1>エラー</h1>
551
+ <div class="error-code">${this.escapeHtml(error)}</div>
552
+ <p>${this.escapeHtml(description)}</p>
553
+ </div>
554
+ </body>
555
+ </html>`;
556
+ }
557
+ /**
558
+ * Escape HTML special characters
559
+ */
560
+ escapeHtml(str) {
561
+ return str
562
+ .replace(/&/g, '&amp;')
563
+ .replace(/</g, '&lt;')
564
+ .replace(/>/g, '&gt;')
565
+ .replace(/"/g, '&quot;')
566
+ .replace(/'/g, '&#039;');
567
+ }
568
+ }
569
+ /**
570
+ * Create an OAuth Handler instance
571
+ */
572
+ export async function createOAuthHandler(config) {
573
+ const server = new OAuthServer(config);
574
+ await server.initialize();
575
+ return new OAuthHandler(server, config);
576
+ }
577
+ //# sourceMappingURL=oauth-handler.js.map