@openephemeris/mcp-server 3.13.9 → 3.13.10
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 +2 -2
- package/config/dev-allowlist.json +28 -4
- package/dist/index.js +45 -0
- package/dist/oauth/dcr.d.ts +11 -0
- package/dist/oauth/dcr.js +49 -0
- package/dist/oauth/discovery.d.ts +11 -0
- package/dist/oauth/discovery.js +40 -0
- package/dist/oauth/pkce.d.ts +18 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/rate-limit.d.ts +20 -0
- package/dist/oauth/rate-limit.js +65 -0
- package/dist/oauth/store.d.ts +68 -0
- package/dist/oauth/store.js +247 -0
- package/dist/oauth/supabase-jwt.d.ts +39 -0
- package/dist/oauth/supabase-jwt.js +194 -0
- package/dist/oauth/token.d.ts +21 -0
- package/dist/oauth/token.js +213 -0
- package/dist/prompts.js +171 -159
- package/dist/server-sse.js +134 -36
- package/dist/tools/apps/bazi-app.d.ts +23 -0
- package/dist/tools/apps/bazi-app.js +224 -0
- package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
- package/dist/tools/apps/bi-wheel-app.js +293 -108
- package/dist/tools/apps/bodygraph-app.js +38 -3
- package/dist/tools/apps/moon-phase-app.d.ts +19 -0
- package/dist/tools/apps/moon-phase-app.js +190 -0
- package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
- package/dist/tools/apps/transit-timeline-app.js +243 -0
- package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
- package/dist/tools/apps/vedic-chart-app.js +226 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/specialized/acg.js +2 -2
- package/dist/tools/specialized/bazi.js +367 -44
- package/dist/tools/specialized/bi_wheel.js +1 -1
- package/dist/tools/specialized/chart_wheel.js +1 -1
- package/dist/tools/specialized/comparative.js +4 -4
- package/dist/tools/specialized/eclipse.js +1 -1
- package/dist/tools/specialized/electional.js +4 -4
- package/dist/tools/specialized/ephemeris_core.js +2 -2
- package/dist/tools/specialized/ephemeris_extended.js +8 -8
- package/dist/tools/specialized/hd_bodygraph.js +1 -1
- package/dist/tools/specialized/hd_cycles.js +2 -2
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/human_design.js +1 -1
- package/dist/tools/specialized/moon.js +2 -2
- package/dist/tools/specialized/natal.js +1 -1
- package/dist/tools/specialized/progressed.js +1 -1
- package/dist/tools/specialized/relocation.js +1 -1
- package/dist/tools/specialized/returns.js +3 -3
- package/dist/tools/specialized/synastry.js +1 -1
- package/dist/tools/specialized/transits.js +1 -1
- package/dist/tools/specialized/vedic.js +1 -1
- package/dist/tools/specialized/venus_star_points.js +6 -6
- package/dist/ui/bazi.html +213 -0
- package/dist/ui/bi-wheel.html +762 -489
- package/dist/ui/bodygraph.html +1788 -1649
- package/dist/ui/chart-wheel.html +1772 -1593
- package/dist/ui/moon-phase.html +6758 -0
- package/dist/ui/transit-timeline.html +6835 -0
- package/dist/ui/vedic-chart.html +210 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -355,8 +355,8 @@ Generated by `npm run sync:readme` from `config/dev-allowlist.json` and the live
|
|
|
355
355
|
|
|
356
356
|
- Allowlisted operations: **28**
|
|
357
357
|
- Methods: `GET=4`, `POST=24`, `PUT=0`, `PATCH=0`, `DELETE=0`
|
|
358
|
-
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **
|
|
359
|
-
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_bi_wheel`, `explore_human_design`, `explore_natal_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `timezone_resolve`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
358
|
+
- Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **82**
|
|
359
|
+
- Typed tools: `acg_hits`, `acg_power_lines`, `auth_login`, `auth_logout`, `auth_status`, `bazi_annual_pillar`, `bazi_chart`, `bazi_compatibility`, `bazi_element_balance`, `bazi_luck_pillars`, `bazi_ten_gods`, `bi_wheel_on_cross_aspect_click`, `bi_wheel_on_house_click`, `bi_wheel_on_planet_click`, `bi_wheel_recalculate`, `bi_wheel_synopsis`, `bodygraph_recalculate`, `chart_wheel_on_aspect_click`, `chart_wheel_on_house_click`, `chart_wheel_on_planet_click`, `chart_wheel_recalculate`, `chinese_bazi`, `dev_call`, `dev_list_allowed`, `electional_aspect_search`, `electional_moment_analysis`, `electional_station_tracker`, `ephemeris_angles_points`, `ephemeris_aspect_check`, `ephemeris_bi_wheel`, `ephemeris_chart_wheel`, `ephemeris_composite`, `ephemeris_composite_midpoint`, `ephemeris_dignities`, `ephemeris_electional`, `ephemeris_fixed_stars`, `ephemeris_hermetic_lots`, `ephemeris_house_cusps`, `ephemeris_lunar_return`, `ephemeris_midpoints`, `ephemeris_moon_phase`, `ephemeris_natal_batch`, `ephemeris_natal_chart`, `ephemeris_natal_transits`, `ephemeris_next_eclipse`, `ephemeris_next_lunar_phase`, `ephemeris_overlay`, `ephemeris_planet_position`, `ephemeris_planetary_return`, `ephemeris_progressed_chart`, `ephemeris_relocation`, `ephemeris_retrograde_status`, `ephemeris_solar_return`, `ephemeris_synastry`, `ephemeris_transits`, `explore_bazi_chart`, `explore_bi_wheel`, `explore_human_design`, `explore_moon_phase`, `explore_natal_chart`, `explore_transit_timeline`, `explore_vedic_chart`, `hd_on_center_click`, `hd_on_channel_click`, `hd_on_gate_click`, `hd_on_planet_click`, `hd_on_variable_click`, `hd_opposition`, `hd_planetary_return`, `human_design_bodygraph`, `human_design_chart`, `human_design_composite`, `human_design_penta`, `location_search`, `timezone_resolve`, `vedic_chart`, `venus_eight_year_star`, `venus_elongations`, `venus_phase`, `venus_star_points`, `venus_star_points_conjunctions`, `venus_stations`
|
|
360
360
|
- Generic tools:
|
|
361
361
|
|
|
362
362
|
### Allowlist Families
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
"path": "/visualization/chart-wheel"
|
|
129
129
|
}
|
|
130
130
|
],
|
|
131
|
-
"last_generated_at": "2026-
|
|
132
|
-
"openapi_sha256": "
|
|
131
|
+
"last_generated_at": "2026-05-14T01:35:40.409Z",
|
|
132
|
+
"openapi_sha256": "50112cd7017466815265d92e7d73cbc29be6bdc729a87db50989f189d562d201",
|
|
133
133
|
"candidates_get": [
|
|
134
134
|
{
|
|
135
135
|
"method": "GET",
|
|
@@ -469,6 +469,14 @@
|
|
|
469
469
|
],
|
|
470
470
|
"operationId": "aspects_acg_aspects_post"
|
|
471
471
|
},
|
|
472
|
+
{
|
|
473
|
+
"method": "POST",
|
|
474
|
+
"path": "/acg/ccg/parans",
|
|
475
|
+
"tags": [
|
|
476
|
+
"ACG"
|
|
477
|
+
],
|
|
478
|
+
"operationId": "acg_ccg_parans_post"
|
|
479
|
+
},
|
|
472
480
|
{
|
|
473
481
|
"method": "POST",
|
|
474
482
|
"path": "/acg/ccg",
|
|
@@ -541,6 +549,14 @@
|
|
|
541
549
|
],
|
|
542
550
|
"operationId": "midpoints_acg_midpoints_post"
|
|
543
551
|
},
|
|
552
|
+
{
|
|
553
|
+
"method": "POST",
|
|
554
|
+
"path": "/acg/parans",
|
|
555
|
+
"tags": [
|
|
556
|
+
"ACG"
|
|
557
|
+
],
|
|
558
|
+
"operationId": "acg_parans_post"
|
|
559
|
+
},
|
|
544
560
|
{
|
|
545
561
|
"method": "POST",
|
|
546
562
|
"path": "/acg/power-lines",
|
|
@@ -549,6 +565,14 @@
|
|
|
549
565
|
],
|
|
550
566
|
"operationId": "power_lines_acg_power_lines_post"
|
|
551
567
|
},
|
|
568
|
+
{
|
|
569
|
+
"method": "POST",
|
|
570
|
+
"path": "/acg/relational-parans",
|
|
571
|
+
"tags": [
|
|
572
|
+
"ACG"
|
|
573
|
+
],
|
|
574
|
+
"operationId": "acg_relational_parans_post"
|
|
575
|
+
},
|
|
552
576
|
{
|
|
553
577
|
"method": "POST",
|
|
554
578
|
"path": "/chinese/bazi/annual-pillar",
|
|
@@ -699,7 +723,7 @@
|
|
|
699
723
|
"tags": [
|
|
700
724
|
"ephemeris"
|
|
701
725
|
],
|
|
702
|
-
"operationId": "
|
|
726
|
+
"operationId": "CalculateDraconicChart"
|
|
703
727
|
},
|
|
704
728
|
{
|
|
705
729
|
"method": "POST",
|
|
@@ -779,7 +803,7 @@
|
|
|
779
803
|
"tags": [
|
|
780
804
|
"ephemeris"
|
|
781
805
|
],
|
|
782
|
-
"operationId": "
|
|
806
|
+
"operationId": "CalculatePrenatalLunation"
|
|
783
807
|
},
|
|
784
808
|
{
|
|
785
809
|
"method": "POST",
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,9 @@ import { listPrompts, getPromptContent } from "./prompts.js";
|
|
|
10
10
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
11
11
|
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
12
12
|
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
13
|
+
import { TRANSIT_TIMELINE_RESOURCE_URI, TRANSIT_TIMELINE_MIME_TYPE, getTransitTimelineBundle, } from "./tools/apps/transit-timeline-app.js";
|
|
14
|
+
import { MOON_PHASE_RESOURCE_URI, MOON_PHASE_MIME_TYPE, getMoonPhaseBundle, } from "./tools/apps/moon-phase-app.js";
|
|
15
|
+
import { BAZI_RESOURCE_URI, BAZI_MIME_TYPE, getBaziBundle, } from "./tools/apps/bazi-app.js";
|
|
13
16
|
function resolveServerVersion() {
|
|
14
17
|
try {
|
|
15
18
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -124,6 +127,30 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
|
124
127
|
mimeType: BI_WHEEL_MIME_TYPE,
|
|
125
128
|
});
|
|
126
129
|
}
|
|
130
|
+
if (getTransitTimelineBundle()) {
|
|
131
|
+
resources.push({
|
|
132
|
+
uri: TRANSIT_TIMELINE_RESOURCE_URI,
|
|
133
|
+
name: "Transit Timeline Explorer",
|
|
134
|
+
description: "Interactive Gantt-style transit timeline showing planetary aspects over time.",
|
|
135
|
+
mimeType: TRANSIT_TIMELINE_MIME_TYPE,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (getMoonPhaseBundle()) {
|
|
139
|
+
resources.push({
|
|
140
|
+
uri: MOON_PHASE_RESOURCE_URI,
|
|
141
|
+
name: "Moon Phase Explorer",
|
|
142
|
+
description: "Interactive lunar phase dial showing illumination, sign, and void-of-course status.",
|
|
143
|
+
mimeType: MOON_PHASE_MIME_TYPE,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (getBaziBundle()) {
|
|
147
|
+
resources.push({
|
|
148
|
+
uri: BAZI_RESOURCE_URI,
|
|
149
|
+
name: "BaZi Four Pillars Explorer",
|
|
150
|
+
description: "Interactive BaZi chart with pillars, Ten Gods, hidden stems, and Wu Xing balance.",
|
|
151
|
+
mimeType: BAZI_MIME_TYPE,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
127
154
|
return { resources };
|
|
128
155
|
});
|
|
129
156
|
// Read resource
|
|
@@ -147,6 +174,24 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
147
174
|
throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
|
|
148
175
|
return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
|
|
149
176
|
}
|
|
177
|
+
if (uri === TRANSIT_TIMELINE_RESOURCE_URI) {
|
|
178
|
+
const bundle = getTransitTimelineBundle();
|
|
179
|
+
if (!bundle)
|
|
180
|
+
throw new Error("Transit Timeline UI bundle not found. Run `npm run build:ui` to build it.");
|
|
181
|
+
return { contents: [{ uri: TRANSIT_TIMELINE_RESOURCE_URI, mimeType: TRANSIT_TIMELINE_MIME_TYPE, text: bundle }] };
|
|
182
|
+
}
|
|
183
|
+
if (uri === MOON_PHASE_RESOURCE_URI) {
|
|
184
|
+
const bundle = getMoonPhaseBundle();
|
|
185
|
+
if (!bundle)
|
|
186
|
+
throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
|
|
187
|
+
return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
|
|
188
|
+
}
|
|
189
|
+
if (uri === BAZI_RESOURCE_URI) {
|
|
190
|
+
const bundle = getBaziBundle();
|
|
191
|
+
if (!bundle)
|
|
192
|
+
throw new Error("BaZi UI bundle not found. Run `npm run build:ui` to build it.");
|
|
193
|
+
return { contents: [{ uri: BAZI_RESOURCE_URI, mimeType: BAZI_MIME_TYPE, text: bundle }] };
|
|
194
|
+
}
|
|
150
195
|
throw new Error(`Unknown resource: ${uri}`);
|
|
151
196
|
});
|
|
152
197
|
// Handle tool calls
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/dcr.ts — Dynamic Client Registration (RFC 7591).
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/register
|
|
5
|
+
*
|
|
6
|
+
* Deterministic client_id: SHA-256 of the sorted, joined redirect_uris.
|
|
7
|
+
* This means the same set of redirect URIs always produces the same client_id,
|
|
8
|
+
* making registration idempotent.
|
|
9
|
+
*/
|
|
10
|
+
import { type Router } from "express";
|
|
11
|
+
export declare const oauthDcrRouter: Router;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/dcr.ts — Dynamic Client Registration (RFC 7591).
|
|
3
|
+
*
|
|
4
|
+
* POST /oauth/register
|
|
5
|
+
*
|
|
6
|
+
* Deterministic client_id: SHA-256 of the sorted, joined redirect_uris.
|
|
7
|
+
* This means the same set of redirect URIs always produces the same client_id,
|
|
8
|
+
* making registration idempotent.
|
|
9
|
+
*/
|
|
10
|
+
import express from "express";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
export const oauthDcrRouter = express.Router();
|
|
13
|
+
oauthDcrRouter.post("/oauth/register", (req, res) => {
|
|
14
|
+
// Validate Content-Type
|
|
15
|
+
const contentType = req.headers["content-type"];
|
|
16
|
+
if (!contentType || !contentType.includes("application/json")) {
|
|
17
|
+
res.status(400).json({
|
|
18
|
+
error: "invalid_request",
|
|
19
|
+
error_description: "Content-Type must be application/json.",
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const body = req.body;
|
|
24
|
+
// Validate redirect_uris
|
|
25
|
+
if (!body ||
|
|
26
|
+
!Array.isArray(body.redirect_uris) ||
|
|
27
|
+
body.redirect_uris.length === 0) {
|
|
28
|
+
res.status(400).json({
|
|
29
|
+
error: "invalid_client_metadata",
|
|
30
|
+
error_description: "redirect_uris must be a non-empty array of strings.",
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const redirect_uris = body.redirect_uris;
|
|
35
|
+
const client_name = body.client_name || "Unknown Client";
|
|
36
|
+
// Deterministic client_id: SHA-256 of sorted redirect URIs
|
|
37
|
+
const uriFingerprint = [...redirect_uris].sort().join("\n");
|
|
38
|
+
const client_id = createHash("sha256")
|
|
39
|
+
.update(uriFingerprint, "utf8")
|
|
40
|
+
.digest("hex");
|
|
41
|
+
res.status(201).json({
|
|
42
|
+
client_id,
|
|
43
|
+
client_name,
|
|
44
|
+
redirect_uris,
|
|
45
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
46
|
+
token_endpoint_auth_method: "none",
|
|
47
|
+
created_at: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/discovery.ts — OAuth 2.1 Discovery endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* GET /.well-known/oauth-protected-resource (RFC 8707)
|
|
6
|
+
* GET /.well-known/oauth-authorization-server (RFC 8414)
|
|
7
|
+
*
|
|
8
|
+
* All URLs use https:// per the OAuth 2.1 specification.
|
|
9
|
+
*/
|
|
10
|
+
import { type Router } from "express";
|
|
11
|
+
export declare const oauthDiscoveryRouter: Router;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/discovery.ts — OAuth 2.1 Discovery endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* GET /.well-known/oauth-protected-resource (RFC 8707)
|
|
6
|
+
* GET /.well-known/oauth-authorization-server (RFC 8414)
|
|
7
|
+
*
|
|
8
|
+
* All URLs use https:// per the OAuth 2.1 specification.
|
|
9
|
+
*/
|
|
10
|
+
import express from "express";
|
|
11
|
+
const MCP_SERVER_ORIGIN = "https://mcp.openephemeris.com";
|
|
12
|
+
const WEB_APP_ORIGIN = "https://openephemeris.com";
|
|
13
|
+
export const oauthDiscoveryRouter = express.Router();
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// GET /.well-known/oauth-protected-resource
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource", (_req, res) => {
|
|
18
|
+
res.json({
|
|
19
|
+
resource: MCP_SERVER_ORIGIN,
|
|
20
|
+
authorization_servers: [MCP_SERVER_ORIGIN],
|
|
21
|
+
bearer_methods_supported: ["header"],
|
|
22
|
+
scopes_supported: ["openephemeris:read"],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// GET /.well-known/oauth-authorization-server
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
oauthDiscoveryRouter.get("/.well-known/oauth-authorization-server", (_req, res) => {
|
|
29
|
+
res.json({
|
|
30
|
+
issuer: MCP_SERVER_ORIGIN,
|
|
31
|
+
authorization_endpoint: `${WEB_APP_ORIGIN}/oauth/authorize`,
|
|
32
|
+
token_endpoint: `${MCP_SERVER_ORIGIN}/oauth/token`,
|
|
33
|
+
registration_endpoint: `${MCP_SERVER_ORIGIN}/oauth/register`,
|
|
34
|
+
response_types_supported: ["code"],
|
|
35
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
36
|
+
code_challenge_methods_supported: ["S256"],
|
|
37
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
38
|
+
scopes_supported: ["openephemeris:read"],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/pkce.ts — PKCE (Proof Key for Code Exchange) utilities.
|
|
3
|
+
*
|
|
4
|
+
* Implements RFC 7636 S256 challenge verification and cryptographically
|
|
5
|
+
* random authorization code generation.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Verify that `code_challenge === base64url(SHA-256(code_verifier))`.
|
|
9
|
+
*
|
|
10
|
+
* Uses crypto.timingSafeEqual for the final comparison to prevent
|
|
11
|
+
* timing side-channel attacks.
|
|
12
|
+
*/
|
|
13
|
+
export declare function verifyCodeChallenge(code_verifier: string, code_challenge: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Generate a cryptographically random authorization code.
|
|
16
|
+
* Returns 64 hex characters (32 random bytes).
|
|
17
|
+
*/
|
|
18
|
+
export declare function generateAuthorizationCode(): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/pkce.ts — PKCE (Proof Key for Code Exchange) utilities.
|
|
3
|
+
*
|
|
4
|
+
* Implements RFC 7636 S256 challenge verification and cryptographically
|
|
5
|
+
* random authorization code generation.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
|
+
/**
|
|
9
|
+
* Verify that `code_challenge === base64url(SHA-256(code_verifier))`.
|
|
10
|
+
*
|
|
11
|
+
* Uses crypto.timingSafeEqual for the final comparison to prevent
|
|
12
|
+
* timing side-channel attacks.
|
|
13
|
+
*/
|
|
14
|
+
export function verifyCodeChallenge(code_verifier, code_challenge) {
|
|
15
|
+
if (!code_verifier || !code_challenge)
|
|
16
|
+
return false;
|
|
17
|
+
// SHA-256 hash the verifier and base64url encode it
|
|
18
|
+
const computed = createHash("sha256")
|
|
19
|
+
.update(code_verifier, "ascii")
|
|
20
|
+
.digest("base64url");
|
|
21
|
+
// Buffers must be equal length for timingSafeEqual
|
|
22
|
+
const computedBuf = Buffer.from(computed, "utf8");
|
|
23
|
+
const challengeBuf = Buffer.from(code_challenge, "utf8");
|
|
24
|
+
if (computedBuf.length !== challengeBuf.length)
|
|
25
|
+
return false;
|
|
26
|
+
return timingSafeEqual(computedBuf, challengeBuf);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a cryptographically random authorization code.
|
|
30
|
+
* Returns 64 hex characters (32 random bytes).
|
|
31
|
+
*/
|
|
32
|
+
export function generateAuthorizationCode() {
|
|
33
|
+
return randomBytes(32).toString("hex");
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/rate-limit.ts — Simple per-IP rate limiter for the token endpoint.
|
|
3
|
+
*
|
|
4
|
+
* No external dependencies — uses a plain Map + setInterval for cleanup.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* app.use("/oauth/token", createRateLimiter({ windowMs: 60_000, maxRequests: 10 }));
|
|
8
|
+
*/
|
|
9
|
+
import type { Request, Response, NextFunction } from "express";
|
|
10
|
+
export interface RateLimiterOptions {
|
|
11
|
+
windowMs: number;
|
|
12
|
+
maxRequests: number;
|
|
13
|
+
trustProxy?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates a rate-limiting Express middleware.
|
|
17
|
+
*
|
|
18
|
+
* Returns 429 with Retry-After header when the per-IP limit is exceeded.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createRateLimiter(options: RateLimiterOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/rate-limit.ts — Simple per-IP rate limiter for the token endpoint.
|
|
3
|
+
*
|
|
4
|
+
* No external dependencies — uses a plain Map + setInterval for cleanup.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* app.use("/oauth/token", createRateLimiter({ windowMs: 60_000, maxRequests: 10 }));
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Creates a rate-limiting Express middleware.
|
|
11
|
+
*
|
|
12
|
+
* Returns 429 with Retry-After header when the per-IP limit is exceeded.
|
|
13
|
+
*/
|
|
14
|
+
export function createRateLimiter(options) {
|
|
15
|
+
const { windowMs, maxRequests, trustProxy = false } = options;
|
|
16
|
+
const store = new Map();
|
|
17
|
+
// Cleanup expired entries every windowMs
|
|
18
|
+
const cleanupTimer = setInterval(() => {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [ip, entry] of store) {
|
|
21
|
+
if (entry.resetAt <= now) {
|
|
22
|
+
store.delete(ip);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, windowMs);
|
|
26
|
+
if (cleanupTimer.unref)
|
|
27
|
+
cleanupTimer.unref();
|
|
28
|
+
return (req, res, next) => {
|
|
29
|
+
// Determine client IP
|
|
30
|
+
let ip;
|
|
31
|
+
if (trustProxy) {
|
|
32
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
33
|
+
if (typeof forwarded === "string") {
|
|
34
|
+
ip = forwarded.split(",")[0].trim();
|
|
35
|
+
}
|
|
36
|
+
else if (Array.isArray(forwarded) && forwarded.length > 0) {
|
|
37
|
+
ip = forwarded[0].split(",")[0].trim();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
ip = req.ip || req.socket.remoteAddress || "unknown";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
ip = req.ip || req.socket.remoteAddress || "unknown";
|
|
45
|
+
}
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
let entry = store.get(ip);
|
|
48
|
+
// Reset window if expired
|
|
49
|
+
if (!entry || entry.resetAt <= now) {
|
|
50
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
51
|
+
store.set(ip, entry);
|
|
52
|
+
}
|
|
53
|
+
entry.count++;
|
|
54
|
+
if (entry.count > maxRequests) {
|
|
55
|
+
const retryAfterSec = Math.ceil((entry.resetAt - now) / 1_000);
|
|
56
|
+
res.setHeader("Retry-After", String(retryAfterSec));
|
|
57
|
+
res.status(429).json({
|
|
58
|
+
error: "rate_limit_exceeded",
|
|
59
|
+
error_description: "Too many requests. Please try again later.",
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
next();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/oauth/store.ts — OAuth authorization code store.
|
|
3
|
+
*
|
|
4
|
+
* Architecture: in-memory is always the source of truth for the MCP server
|
|
5
|
+
* process. When Supabase env vars are present AND valid, codes are ALSO
|
|
6
|
+
* persisted async to Supabase for durability and multi-instance lookup.
|
|
7
|
+
*
|
|
8
|
+
* This means:
|
|
9
|
+
* - Unit tests (no env vars) → pure in-memory, zero network calls ✓
|
|
10
|
+
* - Production (Supabase set) → in-memory for hot-path, Supabase for
|
|
11
|
+
* persistence/audit trail ✓
|
|
12
|
+
*
|
|
13
|
+
* NOTE: The consent page (/api/oauth/approve) writes directly to Supabase
|
|
14
|
+
* using the service role key without going through this store. The store
|
|
15
|
+
* is used by the MCP server's own /oauth/* endpoints.
|
|
16
|
+
*
|
|
17
|
+
* Security:
|
|
18
|
+
* - NEVER log the `code` value. Log only the row `id` (uuid).
|
|
19
|
+
* - consumeCodeAsync marks codes consumed atomically in Supabase when
|
|
20
|
+
* Supabase is reachable; falls through to in-memory otherwise.
|
|
21
|
+
*/
|
|
22
|
+
export interface IssuedCodeOptions {
|
|
23
|
+
client_id: string;
|
|
24
|
+
redirect_uri: string;
|
|
25
|
+
code_challenge: string;
|
|
26
|
+
code_challenge_method: "S256";
|
|
27
|
+
/** Optional user_id — set by consent page; undefined in unit tests */
|
|
28
|
+
user_id?: string;
|
|
29
|
+
expiresInMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class OAuthStore {
|
|
32
|
+
/** In-memory is ALWAYS the hot-path source of truth */
|
|
33
|
+
private codes;
|
|
34
|
+
private refreshTokens;
|
|
35
|
+
private cleanupTimer;
|
|
36
|
+
constructor();
|
|
37
|
+
/**
|
|
38
|
+
* Issue a new authorization code.
|
|
39
|
+
*
|
|
40
|
+
* Always writes to in-memory (fast, test-safe).
|
|
41
|
+
* Also fire-and-forgets a Supabase persist when env vars are set.
|
|
42
|
+
*/
|
|
43
|
+
issueCode(options: IssuedCodeOptions): string;
|
|
44
|
+
/**
|
|
45
|
+
* Consume a code synchronously from in-memory (single-use).
|
|
46
|
+
* This is the fast-path used by the token endpoint.
|
|
47
|
+
*/
|
|
48
|
+
consumeCode(code: string): (IssuedCodeOptions & {
|
|
49
|
+
used: boolean;
|
|
50
|
+
expires_at: Date;
|
|
51
|
+
}) | null;
|
|
52
|
+
/**
|
|
53
|
+
* Async consume — checks in-memory first, then Supabase.
|
|
54
|
+
*
|
|
55
|
+
* Use this from token.ts so production deployments with Supabase
|
|
56
|
+
* can survive process restarts (code issued before restart lives in DB).
|
|
57
|
+
*/
|
|
58
|
+
consumeCodeAsync(code: string): Promise<(IssuedCodeOptions & {
|
|
59
|
+
used: boolean;
|
|
60
|
+
expires_at: Date;
|
|
61
|
+
}) | null>;
|
|
62
|
+
storeRefreshToken(token: string, meta: object): void;
|
|
63
|
+
consumeRefreshToken(token: string): object | null;
|
|
64
|
+
generateRefreshToken(): string;
|
|
65
|
+
reset(): void;
|
|
66
|
+
private purgeExpired;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
}
|