@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.
- package/dist/cli/http-server-with-config.d.ts +38 -0
- package/dist/cli/http-server-with-config.d.ts.map +1 -0
- package/dist/cli/http-server-with-config.js +458 -0
- package/dist/cli/http-server-with-config.js.map +1 -0
- package/dist/cli/http-server.d.ts +74 -0
- package/dist/cli/http-server.d.ts.map +1 -0
- package/dist/cli/http-server.js +407 -0
- package/dist/cli/http-server.js.map +1 -0
- package/dist/cli/jwt-middleware.d.ts +36 -0
- package/dist/cli/jwt-middleware.d.ts.map +1 -0
- package/dist/cli/jwt-middleware.js +99 -0
- package/dist/cli/jwt-middleware.js.map +1 -0
- package/dist/cli/main-entry.d.ts +41 -0
- package/dist/cli/main-entry.d.ts.map +1 -0
- package/dist/cli/main-entry.js +80 -0
- package/dist/cli/main-entry.js.map +1 -0
- package/dist/cli/mcp-handler.d.ts +56 -0
- package/dist/cli/mcp-handler.d.ts.map +1 -0
- package/dist/cli/mcp-handler.js +2189 -0
- package/dist/cli/mcp-handler.js.map +1 -0
- package/dist/cli/parser.d.ts +43 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +162 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/remote-config-loader.d.ts +85 -0
- package/dist/cli/remote-config-loader.d.ts.map +1 -0
- package/dist/cli/remote-config-loader.js +129 -0
- package/dist/cli/remote-config-loader.js.map +1 -0
- package/dist/cli/secret-auth.d.ts +47 -0
- package/dist/cli/secret-auth.d.ts.map +1 -0
- package/dist/cli/secret-auth.js +165 -0
- package/dist/cli/secret-auth.js.map +1 -0
- package/dist/cli/sse-stream-handler.d.ts +45 -0
- package/dist/cli/sse-stream-handler.d.ts.map +1 -0
- package/dist/cli/sse-stream-handler.js +125 -0
- package/dist/cli/sse-stream-handler.js.map +1 -0
- package/dist/index.js +885 -209
- package/dist/index.js.map +1 -1
- package/dist/integrations/calendar-event-creator.d.ts +152 -0
- package/dist/integrations/calendar-event-creator.d.ts.map +1 -0
- package/dist/integrations/calendar-event-creator.js +507 -0
- package/dist/integrations/calendar-event-creator.js.map +1 -0
- package/dist/integrations/calendar-event-deleter.d.ts +137 -0
- package/dist/integrations/calendar-event-deleter.d.ts.map +1 -0
- package/dist/integrations/calendar-event-deleter.js +378 -0
- package/dist/integrations/calendar-event-deleter.js.map +1 -0
- package/dist/integrations/calendar-event-response.d.ts +213 -0
- package/dist/integrations/calendar-event-response.d.ts.map +1 -0
- package/dist/integrations/calendar-event-response.js +560 -0
- package/dist/integrations/calendar-event-response.js.map +1 -0
- package/dist/integrations/calendar-service.d.ts +66 -1
- package/dist/integrations/calendar-service.d.ts.map +1 -1
- package/dist/integrations/calendar-service.js +223 -0
- package/dist/integrations/calendar-service.js.map +1 -1
- package/dist/oauth/client-store.d.ts +36 -0
- package/dist/oauth/client-store.d.ts.map +1 -0
- package/dist/oauth/client-store.js +104 -0
- package/dist/oauth/client-store.js.map +1 -0
- package/dist/oauth/code-store.d.ts +48 -0
- package/dist/oauth/code-store.d.ts.map +1 -0
- package/dist/oauth/code-store.js +89 -0
- package/dist/oauth/code-store.js.map +1 -0
- package/dist/oauth/index.d.ts +13 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +21 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/oauth-handler.d.ts +101 -0
- package/dist/oauth/oauth-handler.d.ts.map +1 -0
- package/dist/oauth/oauth-handler.js +577 -0
- package/dist/oauth/oauth-handler.js.map +1 -0
- package/dist/oauth/oauth-server.d.ts +165 -0
- package/dist/oauth/oauth-server.d.ts.map +1 -0
- package/dist/oauth/oauth-server.js +489 -0
- package/dist/oauth/oauth-server.js.map +1 -0
- package/dist/oauth/pkce.d.ts +48 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +106 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/refresh-token-store.d.ts +45 -0
- package/dist/oauth/refresh-token-store.d.ts.map +1 -0
- package/dist/oauth/refresh-token-store.js +98 -0
- package/dist/oauth/refresh-token-store.js.map +1 -0
- package/dist/oauth/token-service.d.ts +46 -0
- package/dist/oauth/token-service.d.ts.map +1 -0
- package/dist/oauth/token-service.js +199 -0
- package/dist/oauth/token-service.js.map +1 -0
- package/dist/oauth/types.d.ts +264 -0
- package/dist/oauth/types.d.ts.map +1 -0
- package/dist/oauth/types.js +37 -0
- package/dist/oauth/types.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- 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, '&')
|
|
563
|
+
.replace(/</g, '<')
|
|
564
|
+
.replace(/>/g, '>')
|
|
565
|
+
.replace(/"/g, '"')
|
|
566
|
+
.replace(/'/g, ''');
|
|
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
|