@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.
Files changed (72) hide show
  1. package/README.md +84 -412
  2. package/dist/auth.js +37 -36
  3. package/dist/auth.js.map +1 -1
  4. package/dist/crypto.js +100 -61
  5. package/dist/crypto.js.map +1 -1
  6. package/dist/formatters.d.ts +18 -2
  7. package/dist/formatters.d.ts.map +1 -1
  8. package/dist/handlers/attachments.d.ts +6 -0
  9. package/dist/handlers/attachments.d.ts.map +1 -0
  10. package/dist/handlers/bookings.d.ts +1 -1
  11. package/dist/handlers/bookings.d.ts.map +1 -1
  12. package/dist/handlers/budgets.d.ts +9 -0
  13. package/dist/handlers/budgets.d.ts.map +1 -0
  14. package/dist/handlers/comments.d.ts +1 -1
  15. package/dist/handlers/comments.d.ts.map +1 -1
  16. package/dist/handlers/companies.d.ts +6 -2
  17. package/dist/handlers/companies.d.ts.map +1 -1
  18. package/dist/handlers/deals.d.ts +6 -2
  19. package/dist/handlers/deals.d.ts.map +1 -1
  20. package/dist/handlers/discussions.d.ts +13 -0
  21. package/dist/handlers/discussions.d.ts.map +1 -0
  22. package/dist/handlers/help.d.ts.map +1 -1
  23. package/dist/handlers/index.d.ts.map +1 -1
  24. package/dist/handlers/pages.d.ts +13 -0
  25. package/dist/handlers/pages.d.ts.map +1 -0
  26. package/dist/handlers/people.d.ts +6 -2
  27. package/dist/handlers/people.d.ts.map +1 -1
  28. package/dist/handlers/projects.d.ts +6 -2
  29. package/dist/handlers/projects.d.ts.map +1 -1
  30. package/dist/handlers/reports.d.ts +1 -4
  31. package/dist/handlers/reports.d.ts.map +1 -1
  32. package/dist/handlers/resolve.d.ts +24 -0
  33. package/dist/handlers/resolve.d.ts.map +1 -0
  34. package/dist/handlers/services.d.ts +1 -1
  35. package/dist/handlers/services.d.ts.map +1 -1
  36. package/dist/handlers/tasks.d.ts +6 -2
  37. package/dist/handlers/tasks.d.ts.map +1 -1
  38. package/dist/handlers/time.d.ts +10 -2
  39. package/dist/handlers/time.d.ts.map +1 -1
  40. package/dist/handlers/timers.d.ts +1 -1
  41. package/dist/handlers/timers.d.ts.map +1 -1
  42. package/dist/handlers/types.d.ts +42 -3
  43. package/dist/handlers/types.d.ts.map +1 -1
  44. package/dist/handlers-BYE2INiR.js +2681 -0
  45. package/dist/handlers-BYE2INiR.js.map +1 -0
  46. package/dist/handlers.js +2 -5
  47. package/dist/hints.d.ts +16 -0
  48. package/dist/hints.d.ts.map +1 -1
  49. package/dist/http.js +139 -160
  50. package/dist/http.js.map +1 -1
  51. package/dist/index.js +74 -54
  52. package/dist/index.js.map +1 -1
  53. package/dist/oauth.js +285 -255
  54. package/dist/oauth.js.map +1 -1
  55. package/dist/schema.d.ts +17 -0
  56. package/dist/schema.d.ts.map +1 -1
  57. package/dist/server.js +67 -50
  58. package/dist/server.js.map +1 -1
  59. package/dist/stdio.js +85 -105
  60. package/dist/stdio.js.map +1 -1
  61. package/dist/tools.js +155 -145
  62. package/dist/tools.js.map +1 -1
  63. package/dist/version-D3sSBq_j.js +29 -0
  64. package/dist/version-D3sSBq_j.js.map +1 -0
  65. package/package.json +10 -10
  66. package/skills/SKILL.md +209 -13
  67. package/Dockerfile +0 -36
  68. package/dist/handlers.js.map +0 -1
  69. package/dist/index-CZpVCEu4.js +0 -1681
  70. package/dist/index-CZpVCEu4.js.map +0 -1
  71. package/dist/version-BPy06P7x.js +0 -21
  72. 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
- const host = event.node.req.headers.host || "localhost:3000";
7
- const protocol = event.node.req.headers["x-forwarded-proto"] || "http";
8
- const baseUrl = `${protocol}://${host}`;
9
- setResponseHeader(event, "Content-Type", "application/json");
10
- setResponseHeader(event, "Cache-Control", "public, max-age=3600");
11
- return {
12
- // Required fields per RFC 8414
13
- issuer: baseUrl,
14
- authorization_endpoint: `${baseUrl}/authorize`,
15
- token_endpoint: `${baseUrl}/token`,
16
- response_types_supported: ["code"],
17
- // OAuth 2.1 / MCP requirements
18
- grant_types_supported: ["authorization_code", "refresh_token"],
19
- code_challenge_methods_supported: ["S256"],
20
- token_endpoint_auth_methods_supported: ["none"],
21
- // Public client
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
- setResponseHeader(event, "Content-Type", "application/json");
30
- let body;
31
- try {
32
- body = await readBody(event);
33
- } catch {
34
- event.node.res.statusCode = 400;
35
- return {
36
- error: "invalid_request",
37
- error_description: "Invalid JSON body"
38
- };
39
- }
40
- const clientName = body.client_name || "MCP Client";
41
- const redirectUris = body.redirect_uris || [];
42
- const clientId = Buffer.from(
43
- JSON.stringify({
44
- name: clientName,
45
- ts: Date.now()
46
- })
47
- ).toString("base64url");
48
- event.node.res.statusCode = 201;
49
- return {
50
- client_id: clientId,
51
- client_name: clientName,
52
- redirect_uris: redirectUris,
53
- token_endpoint_auth_method: "none",
54
- grant_types: ["authorization_code", "refresh_token"],
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
- const query = getQuery(event);
60
- query.client_id;
61
- const redirectUri = query.redirect_uri;
62
- const state = query.state;
63
- const codeChallenge = query.code_challenge;
64
- const codeChallengeMethod = query.code_challenge_method;
65
- query.scope;
66
- if (!redirectUri) {
67
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
68
- event.node.res.statusCode = 400;
69
- return renderErrorPage("Missing required parameter: redirect_uri");
70
- }
71
- if (!codeChallenge) {
72
- const errorUrl = new URL(redirectUri);
73
- errorUrl.searchParams.set("error", "invalid_request");
74
- errorUrl.searchParams.set("error_description", "code_challenge is required");
75
- if (state) errorUrl.searchParams.set("state", state);
76
- return sendRedirect(event, errorUrl.toString());
77
- }
78
- if (codeChallengeMethod && codeChallengeMethod !== "S256") {
79
- const errorUrl = new URL(redirectUri);
80
- errorUrl.searchParams.set("error", "invalid_request");
81
- errorUrl.searchParams.set("error_description", "Only S256 code_challenge_method is supported");
82
- if (state) errorUrl.searchParams.set("state", state);
83
- return sendRedirect(event, errorUrl.toString());
84
- }
85
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
86
- return renderLoginForm({
87
- redirectUri,
88
- state,
89
- codeChallenge,
90
- codeChallengeMethod: codeChallengeMethod || "S256"
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
- const body = await readBody(event);
95
- const { orgId, apiToken, userId, redirectUri, state, codeChallenge, codeChallengeMethod } = body;
96
- if (!redirectUri) {
97
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
98
- event.node.res.statusCode = 400;
99
- return renderErrorPage("Missing redirect_uri parameter");
100
- }
101
- try {
102
- const uri = new URL(redirectUri);
103
- const isLocalhost = uri.hostname === "localhost" || uri.hostname === "127.0.0.1";
104
- const isHttps = uri.protocol === "https:";
105
- if (!isLocalhost && !isHttps) {
106
- event.node.res.statusCode = 400;
107
- return renderErrorPage("redirect_uri must be HTTPS or localhost");
108
- }
109
- } catch {
110
- event.node.res.statusCode = 400;
111
- return renderErrorPage("Invalid redirect_uri format");
112
- }
113
- if (!orgId || !apiToken) {
114
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
115
- return renderLoginForm({
116
- redirectUri,
117
- state,
118
- codeChallenge,
119
- codeChallengeMethod,
120
- error: "Organization ID and API Token are required"
121
- });
122
- }
123
- const code = createAuthCode({
124
- orgId,
125
- apiToken,
126
- userId: userId || void 0,
127
- codeChallenge,
128
- codeChallengeMethod: codeChallengeMethod || "S256"
129
- });
130
- const redirectUrl = new URL(redirectUri);
131
- redirectUrl.searchParams.set("code", code);
132
- if (state) {
133
- redirectUrl.searchParams.set("state", state);
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
- setResponseHeader(event, "Content-Type", "application/json");
140
- let body;
141
- const contentType = event.node.req.headers["content-type"] || "";
142
- if (contentType.includes("application/x-www-form-urlencoded")) {
143
- const rawBody = await readBody(event);
144
- if (typeof rawBody === "string") {
145
- body = Object.fromEntries(new URLSearchParams(rawBody));
146
- } else {
147
- body = rawBody;
148
- }
149
- } else {
150
- body = await readBody(event);
151
- }
152
- const { grant_type, code, code_verifier, refresh_token } = body;
153
- if (grant_type === "refresh_token") {
154
- return handleRefreshToken(event, refresh_token);
155
- }
156
- if (grant_type !== "authorization_code") {
157
- event.node.res.statusCode = 400;
158
- return {
159
- error: "unsupported_grant_type",
160
- error_description: "Supported grant types: authorization_code, refresh_token"
161
- };
162
- }
163
- if (!code) {
164
- event.node.res.statusCode = 400;
165
- return {
166
- error: "invalid_request",
167
- error_description: "Missing authorization code"
168
- };
169
- }
170
- if (!code_verifier) {
171
- event.node.res.statusCode = 400;
172
- return {
173
- error: "invalid_request",
174
- error_description: "Missing code_verifier (PKCE required)"
175
- };
176
- }
177
- try {
178
- const payload = decodeAuthCode(code);
179
- if (payload.codeChallenge) {
180
- const expectedChallenge = createS256Challenge(code_verifier);
181
- if (expectedChallenge !== payload.codeChallenge) {
182
- event.node.res.statusCode = 400;
183
- return {
184
- error: "invalid_grant",
185
- error_description: "Invalid code_verifier"
186
- };
187
- }
188
- }
189
- const accessToken = createAuthToken({
190
- organizationId: payload.orgId,
191
- apiToken: payload.apiToken,
192
- userId: payload.userId
193
- });
194
- const refreshToken = createAuthCode(
195
- {
196
- orgId: payload.orgId,
197
- apiToken: payload.apiToken,
198
- userId: payload.userId
199
- },
200
- 86400 * 30
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
- if (!refreshToken) {
220
- event.node.res.statusCode = 400;
221
- return {
222
- error: "invalid_request",
223
- error_description: "Missing refresh_token"
224
- };
225
- }
226
- try {
227
- const payload = decodeAuthCode(refreshToken);
228
- const accessToken = createAuthToken({
229
- organizationId: payload.orgId,
230
- apiToken: payload.apiToken,
231
- userId: payload.userId
232
- });
233
- const newRefreshToken = createAuthCode(
234
- {
235
- orgId: payload.orgId,
236
- apiToken: payload.apiToken,
237
- userId: payload.userId
238
- },
239
- 86400 * 30
240
- // 30 days
241
- );
242
- return {
243
- access_token: accessToken,
244
- token_type: "Bearer",
245
- expires_in: 3600,
246
- refresh_token: newRefreshToken
247
- };
248
- } catch (error) {
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
- return createHash("sha256").update(codeVerifier).digest("base64url");
280
+ return createHash("sha256").update(codeVerifier).digest("base64url");
258
281
  }
282
+ /**
283
+ * Render the login form HTML
284
+ */
259
285
  function renderLoginForm(params) {
260
- const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;
261
- return `<!DOCTYPE html>
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
- return `<!DOCTYPE html>
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
- return `<!DOCTYPE html>
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
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
617
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
583
618
  }
584
- export {
585
- authorizeGetHandler,
586
- authorizePostHandler,
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