@studiometa/productive-mcp 0.8.5 → 0.9.1
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/README.md +84 -412
- package/dist/auth.js +37 -36
- package/dist/auth.js.map +1 -1
- package/dist/crypto.js +100 -61
- package/dist/crypto.js.map +1 -1
- package/dist/formatters.d.ts +18 -2
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/attachments.d.ts +6 -0
- package/dist/handlers/attachments.d.ts.map +1 -0
- package/dist/handlers/bookings.d.ts +1 -1
- package/dist/handlers/bookings.d.ts.map +1 -1
- package/dist/handlers/budgets.d.ts +9 -0
- package/dist/handlers/budgets.d.ts.map +1 -0
- package/dist/handlers/comments.d.ts +1 -1
- package/dist/handlers/comments.d.ts.map +1 -1
- package/dist/handlers/companies.d.ts +6 -2
- package/dist/handlers/companies.d.ts.map +1 -1
- package/dist/handlers/deals.d.ts +6 -2
- package/dist/handlers/deals.d.ts.map +1 -1
- package/dist/handlers/discussions.d.ts +13 -0
- package/dist/handlers/discussions.d.ts.map +1 -0
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/pages.d.ts +13 -0
- package/dist/handlers/pages.d.ts.map +1 -0
- package/dist/handlers/people.d.ts +6 -2
- package/dist/handlers/people.d.ts.map +1 -1
- package/dist/handlers/projects.d.ts +6 -2
- package/dist/handlers/projects.d.ts.map +1 -1
- package/dist/handlers/reports.d.ts +1 -4
- package/dist/handlers/reports.d.ts.map +1 -1
- package/dist/handlers/resolve.d.ts +24 -0
- package/dist/handlers/resolve.d.ts.map +1 -0
- package/dist/handlers/services.d.ts +1 -1
- package/dist/handlers/services.d.ts.map +1 -1
- package/dist/handlers/tasks.d.ts +6 -2
- package/dist/handlers/tasks.d.ts.map +1 -1
- package/dist/handlers/time.d.ts +10 -2
- package/dist/handlers/time.d.ts.map +1 -1
- package/dist/handlers/timers.d.ts +1 -1
- package/dist/handlers/timers.d.ts.map +1 -1
- package/dist/handlers/types.d.ts +42 -3
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/handlers-BYE2INiR.js +2681 -0
- package/dist/handlers-BYE2INiR.js.map +1 -0
- package/dist/handlers.js +2 -5
- package/dist/hints.d.ts +16 -0
- package/dist/hints.d.ts.map +1 -1
- package/dist/http.js +139 -160
- package/dist/http.js.map +1 -1
- package/dist/index.js +74 -54
- package/dist/index.js.map +1 -1
- package/dist/oauth.js +285 -255
- package/dist/oauth.js.map +1 -1
- package/dist/schema.d.ts +17 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.js +67 -50
- package/dist/server.js.map +1 -1
- package/dist/stdio.js +85 -105
- package/dist/stdio.js.map +1 -1
- package/dist/tools.js +155 -145
- package/dist/tools.js.map +1 -1
- package/dist/version-D3sSBq_j.js +29 -0
- package/dist/version-D3sSBq_j.js.map +1 -0
- package/package.json +10 -10
- package/skills/SKILL.md +209 -13
- package/Dockerfile +0 -36
- package/dist/handlers.js.map +0 -1
- package/dist/index-CZpVCEu4.js +0 -1681
- package/dist/index-CZpVCEu4.js.map +0 -1
- package/dist/version-BPy06P7x.js +0 -21
- package/dist/version-BPy06P7x.js.map +0 -1
package/dist/oauth.js
CHANGED
|
@@ -1,264 +1,290 @@
|
|
|
1
|
-
import { defineEventHandler, setResponseHeader, readBody, getQuery, sendRedirect } from "h3";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
1
|
import { createAuthToken } from "./auth.js";
|
|
4
2
|
import { createAuthCode, decodeAuthCode } from "./crypto.js";
|
|
3
|
+
import { defineEventHandler, getQuery, readBody, sendRedirect, setResponseHeader } from "h3";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
/**
|
|
6
|
+
* OAuth 2.0 endpoints for Claude Desktop integration
|
|
7
|
+
*
|
|
8
|
+
* Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.
|
|
9
|
+
* Uses stateless encrypted tokens - no server-side storage required.
|
|
10
|
+
*
|
|
11
|
+
* Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. Claude redirects user to /authorize with OAuth params (including PKCE)
|
|
15
|
+
* 2. User enters Productive credentials in login form
|
|
16
|
+
* 3. Server encrypts credentials + PKCE challenge into authorization code
|
|
17
|
+
* 4. Redirects back to Claude with the code
|
|
18
|
+
* 5. Claude exchanges code for access token via /token (with code_verifier)
|
|
19
|
+
* 6. Server validates PKCE and returns access token
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* OAuth metadata for discovery (RFC 8414)
|
|
23
|
+
* GET /.well-known/oauth-authorization-server
|
|
24
|
+
*
|
|
25
|
+
* MCP clients MUST check this endpoint first for server capabilities.
|
|
26
|
+
*/
|
|
5
27
|
const oauthMetadataHandler = defineEventHandler((event) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Optional but useful
|
|
23
|
-
registration_endpoint: `${baseUrl}/register`,
|
|
24
|
-
scopes_supported: ["productive"],
|
|
25
|
-
service_documentation: "https://github.com/studiometa/productive-tools"
|
|
26
|
-
};
|
|
28
|
+
const host = event.node.req.headers.host || "localhost:3000";
|
|
29
|
+
const baseUrl = `${event.node.req.headers["x-forwarded-proto"] || "http"}://${host}`;
|
|
30
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
31
|
+
setResponseHeader(event, "Cache-Control", "public, max-age=3600");
|
|
32
|
+
return {
|
|
33
|
+
issuer: baseUrl,
|
|
34
|
+
authorization_endpoint: `${baseUrl}/authorize`,
|
|
35
|
+
token_endpoint: `${baseUrl}/token`,
|
|
36
|
+
response_types_supported: ["code"],
|
|
37
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
38
|
+
code_challenge_methods_supported: ["S256"],
|
|
39
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
40
|
+
registration_endpoint: `${baseUrl}/register`,
|
|
41
|
+
scopes_supported: ["productive"],
|
|
42
|
+
service_documentation: "https://github.com/studiometa/productive-tools"
|
|
43
|
+
};
|
|
27
44
|
});
|
|
45
|
+
/**
|
|
46
|
+
* Dynamic Client Registration endpoint (RFC 7591)
|
|
47
|
+
* POST /register
|
|
48
|
+
*
|
|
49
|
+
* MCP servers SHOULD support DCR to allow clients to register automatically.
|
|
50
|
+
* Since we use stateless tokens, we accept any registration and return
|
|
51
|
+
* a generated client_id.
|
|
52
|
+
*/
|
|
28
53
|
const registerHandler = defineEventHandler(async (event) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
response_types: ["code"]
|
|
56
|
-
};
|
|
54
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
55
|
+
let body;
|
|
56
|
+
try {
|
|
57
|
+
body = await readBody(event);
|
|
58
|
+
} catch {
|
|
59
|
+
event.node.res.statusCode = 400;
|
|
60
|
+
return {
|
|
61
|
+
error: "invalid_request",
|
|
62
|
+
error_description: "Invalid JSON body"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const clientName = body.client_name || "MCP Client";
|
|
66
|
+
const redirectUris = body.redirect_uris || [];
|
|
67
|
+
const clientId = Buffer.from(JSON.stringify({
|
|
68
|
+
name: clientName,
|
|
69
|
+
ts: Date.now()
|
|
70
|
+
})).toString("base64url");
|
|
71
|
+
event.node.res.statusCode = 201;
|
|
72
|
+
return {
|
|
73
|
+
client_id: clientId,
|
|
74
|
+
client_name: clientName,
|
|
75
|
+
redirect_uris: redirectUris,
|
|
76
|
+
token_endpoint_auth_method: "none",
|
|
77
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
78
|
+
response_types: ["code"]
|
|
79
|
+
};
|
|
57
80
|
});
|
|
81
|
+
/**
|
|
82
|
+
* Authorization endpoint - shows login form
|
|
83
|
+
* GET /authorize
|
|
84
|
+
*/
|
|
58
85
|
const authorizeGetHandler = defineEventHandler((event) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
const query = getQuery(event);
|
|
87
|
+
const clientId = query.client_id;
|
|
88
|
+
const redirectUri = query.redirect_uri;
|
|
89
|
+
const state = query.state;
|
|
90
|
+
const codeChallenge = query.code_challenge;
|
|
91
|
+
const codeChallengeMethod = query.code_challenge_method;
|
|
92
|
+
const scope = query.scope;
|
|
93
|
+
if (!redirectUri) {
|
|
94
|
+
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
95
|
+
event.node.res.statusCode = 400;
|
|
96
|
+
return renderErrorPage("Missing required parameter: redirect_uri");
|
|
97
|
+
}
|
|
98
|
+
if (!codeChallenge) {
|
|
99
|
+
const errorUrl = new URL(redirectUri);
|
|
100
|
+
errorUrl.searchParams.set("error", "invalid_request");
|
|
101
|
+
errorUrl.searchParams.set("error_description", "code_challenge is required");
|
|
102
|
+
if (state) errorUrl.searchParams.set("state", state);
|
|
103
|
+
return sendRedirect(event, errorUrl.toString());
|
|
104
|
+
}
|
|
105
|
+
if (codeChallengeMethod && codeChallengeMethod !== "S256") {
|
|
106
|
+
const errorUrl = new URL(redirectUri);
|
|
107
|
+
errorUrl.searchParams.set("error", "invalid_request");
|
|
108
|
+
errorUrl.searchParams.set("error_description", "Only S256 code_challenge_method is supported");
|
|
109
|
+
if (state) errorUrl.searchParams.set("state", state);
|
|
110
|
+
return sendRedirect(event, errorUrl.toString());
|
|
111
|
+
}
|
|
112
|
+
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
113
|
+
return renderLoginForm({
|
|
114
|
+
clientId,
|
|
115
|
+
redirectUri,
|
|
116
|
+
state,
|
|
117
|
+
codeChallenge,
|
|
118
|
+
codeChallengeMethod: codeChallengeMethod || "S256",
|
|
119
|
+
scope
|
|
120
|
+
});
|
|
92
121
|
});
|
|
122
|
+
/**
|
|
123
|
+
* Authorization endpoint - process login
|
|
124
|
+
* POST /authorize
|
|
125
|
+
*/
|
|
93
126
|
const authorizePostHandler = defineEventHandler(async (event) => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
136
|
-
return renderSuccessPage(redirectUrl.toString());
|
|
127
|
+
const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = await readBody(event);
|
|
128
|
+
if (!redirectUri) {
|
|
129
|
+
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
130
|
+
event.node.res.statusCode = 400;
|
|
131
|
+
return renderErrorPage("Missing redirect_uri parameter");
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const uri = new URL(redirectUri);
|
|
135
|
+
const isLocalhost = uri.hostname === "localhost" || uri.hostname === "127.0.0.1";
|
|
136
|
+
const isHttps = uri.protocol === "https:";
|
|
137
|
+
if (!isLocalhost && !isHttps) {
|
|
138
|
+
event.node.res.statusCode = 400;
|
|
139
|
+
return renderErrorPage("redirect_uri must be HTTPS or localhost");
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
event.node.res.statusCode = 400;
|
|
143
|
+
return renderErrorPage("Invalid redirect_uri format");
|
|
144
|
+
}
|
|
145
|
+
if (!orgId || !apiToken) {
|
|
146
|
+
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
147
|
+
return renderLoginForm({
|
|
148
|
+
redirectUri,
|
|
149
|
+
state,
|
|
150
|
+
codeChallenge,
|
|
151
|
+
codeChallengeMethod,
|
|
152
|
+
error: "Organization ID and API Token are required"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const code = createAuthCode({
|
|
156
|
+
orgId,
|
|
157
|
+
apiToken,
|
|
158
|
+
userId: userId || void 0,
|
|
159
|
+
codeChallenge,
|
|
160
|
+
codeChallengeMethod: codeChallengeMethod || "S256"
|
|
161
|
+
});
|
|
162
|
+
const redirectUrl = new URL(redirectUri);
|
|
163
|
+
redirectUrl.searchParams.set("code", code);
|
|
164
|
+
if (state) redirectUrl.searchParams.set("state", state);
|
|
165
|
+
setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
|
|
166
|
+
return renderSuccessPage(redirectUrl.toString());
|
|
137
167
|
});
|
|
168
|
+
/**
|
|
169
|
+
* Token endpoint - exchange code for access token
|
|
170
|
+
* POST /token
|
|
171
|
+
*
|
|
172
|
+
* Supports:
|
|
173
|
+
* - authorization_code grant (with PKCE validation)
|
|
174
|
+
* - refresh_token grant
|
|
175
|
+
*/
|
|
138
176
|
const tokenHandler = defineEventHandler(async (event) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// 30 days
|
|
202
|
-
);
|
|
203
|
-
return {
|
|
204
|
-
access_token: accessToken,
|
|
205
|
-
token_type: "Bearer",
|
|
206
|
-
expires_in: 3600,
|
|
207
|
-
// 1 hour (access tokens should be short-lived)
|
|
208
|
-
refresh_token: refreshToken
|
|
209
|
-
};
|
|
210
|
-
} catch (error) {
|
|
211
|
-
event.node.res.statusCode = 400;
|
|
212
|
-
return {
|
|
213
|
-
error: "invalid_grant",
|
|
214
|
-
error_description: error instanceof Error ? error.message : "Invalid authorization code"
|
|
215
|
-
};
|
|
216
|
-
}
|
|
177
|
+
setResponseHeader(event, "Content-Type", "application/json");
|
|
178
|
+
let body;
|
|
179
|
+
if ((event.node.req.headers["content-type"] || "").includes("application/x-www-form-urlencoded")) {
|
|
180
|
+
const rawBody = await readBody(event);
|
|
181
|
+
if (typeof rawBody === "string") body = Object.fromEntries(new URLSearchParams(rawBody));
|
|
182
|
+
else body = rawBody;
|
|
183
|
+
} else body = await readBody(event);
|
|
184
|
+
const { grant_type, code, code_verifier, refresh_token } = body;
|
|
185
|
+
if (grant_type === "refresh_token") return handleRefreshToken(event, refresh_token);
|
|
186
|
+
if (grant_type !== "authorization_code") {
|
|
187
|
+
event.node.res.statusCode = 400;
|
|
188
|
+
return {
|
|
189
|
+
error: "unsupported_grant_type",
|
|
190
|
+
error_description: "Supported grant types: authorization_code, refresh_token"
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!code) {
|
|
194
|
+
event.node.res.statusCode = 400;
|
|
195
|
+
return {
|
|
196
|
+
error: "invalid_request",
|
|
197
|
+
error_description: "Missing authorization code"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!code_verifier) {
|
|
201
|
+
event.node.res.statusCode = 400;
|
|
202
|
+
return {
|
|
203
|
+
error: "invalid_request",
|
|
204
|
+
error_description: "Missing code_verifier (PKCE required)"
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const payload = decodeAuthCode(code);
|
|
209
|
+
if (payload.codeChallenge) {
|
|
210
|
+
if (createS256Challenge(code_verifier) !== payload.codeChallenge) {
|
|
211
|
+
event.node.res.statusCode = 400;
|
|
212
|
+
return {
|
|
213
|
+
error: "invalid_grant",
|
|
214
|
+
error_description: "Invalid code_verifier"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
access_token: createAuthToken({
|
|
220
|
+
organizationId: payload.orgId,
|
|
221
|
+
apiToken: payload.apiToken,
|
|
222
|
+
userId: payload.userId
|
|
223
|
+
}),
|
|
224
|
+
token_type: "Bearer",
|
|
225
|
+
expires_in: 3600,
|
|
226
|
+
refresh_token: createAuthCode({
|
|
227
|
+
orgId: payload.orgId,
|
|
228
|
+
apiToken: payload.apiToken,
|
|
229
|
+
userId: payload.userId
|
|
230
|
+
}, 86400 * 30)
|
|
231
|
+
};
|
|
232
|
+
} catch (error) {
|
|
233
|
+
event.node.res.statusCode = 400;
|
|
234
|
+
return {
|
|
235
|
+
error: "invalid_grant",
|
|
236
|
+
error_description: error instanceof Error ? error.message : "Invalid authorization code"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
217
239
|
});
|
|
240
|
+
/**
|
|
241
|
+
* Handle refresh token grant
|
|
242
|
+
*/
|
|
218
243
|
function handleRefreshToken(event, refreshToken) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
event.node.res.statusCode = 400;
|
|
250
|
-
return {
|
|
251
|
-
error: "invalid_grant",
|
|
252
|
-
error_description: error instanceof Error ? error.message : "Invalid refresh token"
|
|
253
|
-
};
|
|
254
|
-
}
|
|
244
|
+
if (!refreshToken) {
|
|
245
|
+
event.node.res.statusCode = 400;
|
|
246
|
+
return {
|
|
247
|
+
error: "invalid_request",
|
|
248
|
+
error_description: "Missing refresh_token"
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const payload = decodeAuthCode(refreshToken);
|
|
253
|
+
return {
|
|
254
|
+
access_token: createAuthToken({
|
|
255
|
+
organizationId: payload.orgId,
|
|
256
|
+
apiToken: payload.apiToken,
|
|
257
|
+
userId: payload.userId
|
|
258
|
+
}),
|
|
259
|
+
token_type: "Bearer",
|
|
260
|
+
expires_in: 3600,
|
|
261
|
+
refresh_token: createAuthCode({
|
|
262
|
+
orgId: payload.orgId,
|
|
263
|
+
apiToken: payload.apiToken,
|
|
264
|
+
userId: payload.userId
|
|
265
|
+
}, 86400 * 30)
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
event.node.res.statusCode = 400;
|
|
269
|
+
return {
|
|
270
|
+
error: "invalid_grant",
|
|
271
|
+
error_description: error instanceof Error ? error.message : "Invalid refresh token"
|
|
272
|
+
};
|
|
273
|
+
}
|
|
255
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Create S256 PKCE challenge from verifier
|
|
277
|
+
* SHA256(code_verifier) encoded as base64url
|
|
278
|
+
*/
|
|
256
279
|
function createS256Challenge(codeVerifier) {
|
|
257
|
-
|
|
280
|
+
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
258
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Render the login form HTML
|
|
284
|
+
*/
|
|
259
285
|
function renderLoginForm(params) {
|
|
260
|
-
|
|
261
|
-
|
|
286
|
+
const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;
|
|
287
|
+
return `<!DOCTYPE html>
|
|
262
288
|
<html lang="en">
|
|
263
289
|
<head>
|
|
264
290
|
<meta charset="UTF-8">
|
|
@@ -430,8 +456,11 @@ function renderLoginForm(params) {
|
|
|
430
456
|
</body>
|
|
431
457
|
</html>`;
|
|
432
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Render success page with auto-redirect
|
|
461
|
+
*/
|
|
433
462
|
function renderSuccessPage(redirectUrl) {
|
|
434
|
-
|
|
463
|
+
return `<!DOCTYPE html>
|
|
435
464
|
<html lang="en">
|
|
436
465
|
<head>
|
|
437
466
|
<meta charset="UTF-8">
|
|
@@ -536,8 +565,11 @@ function renderSuccessPage(redirectUrl) {
|
|
|
536
565
|
</body>
|
|
537
566
|
</html>`;
|
|
538
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Render error page
|
|
570
|
+
*/
|
|
539
571
|
function renderErrorPage(message) {
|
|
540
|
-
|
|
572
|
+
return `<!DOCTYPE html>
|
|
541
573
|
<html lang="en">
|
|
542
574
|
<head>
|
|
543
575
|
<meta charset="UTF-8">
|
|
@@ -578,14 +610,12 @@ function renderErrorPage(message) {
|
|
|
578
610
|
</body>
|
|
579
611
|
</html>`;
|
|
580
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Escape HTML special characters
|
|
615
|
+
*/
|
|
581
616
|
function escapeHtml(str) {
|
|
582
|
-
|
|
617
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
583
618
|
}
|
|
584
|
-
export {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
oauthMetadataHandler,
|
|
588
|
-
registerHandler,
|
|
589
|
-
tokenHandler
|
|
590
|
-
};
|
|
591
|
-
//# sourceMappingURL=oauth.js.map
|
|
619
|
+
export { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, registerHandler, tokenHandler };
|
|
620
|
+
|
|
621
|
+
//# sourceMappingURL=oauth.js.map
|