@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.
Files changed (61) hide show
  1. package/README.md +2 -2
  2. package/config/dev-allowlist.json +28 -4
  3. package/dist/index.js +45 -0
  4. package/dist/oauth/dcr.d.ts +11 -0
  5. package/dist/oauth/dcr.js +49 -0
  6. package/dist/oauth/discovery.d.ts +11 -0
  7. package/dist/oauth/discovery.js +40 -0
  8. package/dist/oauth/pkce.d.ts +18 -0
  9. package/dist/oauth/pkce.js +34 -0
  10. package/dist/oauth/rate-limit.d.ts +20 -0
  11. package/dist/oauth/rate-limit.js +65 -0
  12. package/dist/oauth/store.d.ts +68 -0
  13. package/dist/oauth/store.js +247 -0
  14. package/dist/oauth/supabase-jwt.d.ts +39 -0
  15. package/dist/oauth/supabase-jwt.js +194 -0
  16. package/dist/oauth/token.d.ts +21 -0
  17. package/dist/oauth/token.js +213 -0
  18. package/dist/prompts.js +171 -159
  19. package/dist/server-sse.js +134 -36
  20. package/dist/tools/apps/bazi-app.d.ts +23 -0
  21. package/dist/tools/apps/bazi-app.js +224 -0
  22. package/dist/tools/apps/bi-wheel-app.d.ts +14 -7
  23. package/dist/tools/apps/bi-wheel-app.js +293 -108
  24. package/dist/tools/apps/bodygraph-app.js +38 -3
  25. package/dist/tools/apps/moon-phase-app.d.ts +19 -0
  26. package/dist/tools/apps/moon-phase-app.js +190 -0
  27. package/dist/tools/apps/transit-timeline-app.d.ts +20 -0
  28. package/dist/tools/apps/transit-timeline-app.js +243 -0
  29. package/dist/tools/apps/vedic-chart-app.d.ts +17 -0
  30. package/dist/tools/apps/vedic-chart-app.js +226 -0
  31. package/dist/tools/index.js +4 -0
  32. package/dist/tools/specialized/acg.js +2 -2
  33. package/dist/tools/specialized/bazi.js +367 -44
  34. package/dist/tools/specialized/bi_wheel.js +1 -1
  35. package/dist/tools/specialized/chart_wheel.js +1 -1
  36. package/dist/tools/specialized/comparative.js +4 -4
  37. package/dist/tools/specialized/eclipse.js +1 -1
  38. package/dist/tools/specialized/electional.js +4 -4
  39. package/dist/tools/specialized/ephemeris_core.js +2 -2
  40. package/dist/tools/specialized/ephemeris_extended.js +8 -8
  41. package/dist/tools/specialized/hd_bodygraph.js +1 -1
  42. package/dist/tools/specialized/hd_cycles.js +2 -2
  43. package/dist/tools/specialized/hd_group.js +2 -2
  44. package/dist/tools/specialized/human_design.js +1 -1
  45. package/dist/tools/specialized/moon.js +2 -2
  46. package/dist/tools/specialized/natal.js +1 -1
  47. package/dist/tools/specialized/progressed.js +1 -1
  48. package/dist/tools/specialized/relocation.js +1 -1
  49. package/dist/tools/specialized/returns.js +3 -3
  50. package/dist/tools/specialized/synastry.js +1 -1
  51. package/dist/tools/specialized/transits.js +1 -1
  52. package/dist/tools/specialized/vedic.js +1 -1
  53. package/dist/tools/specialized/venus_star_points.js +6 -6
  54. package/dist/ui/bazi.html +213 -0
  55. package/dist/ui/bi-wheel.html +762 -489
  56. package/dist/ui/bodygraph.html +1788 -1649
  57. package/dist/ui/chart-wheel.html +1772 -1593
  58. package/dist/ui/moon-phase.html +6758 -0
  59. package/dist/ui/transit-timeline.html +6835 -0
  60. package/dist/ui/vedic-chart.html +210 -0
  61. 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`): **72**
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-04-25T17:47:00.681Z",
132
- "openapi_sha256": "9b2a79811fef658557233fc649b598178488985708b3ad10f252be598567080a",
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": "calculate_draconic_chart_ephemeris_draconic_post"
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": "calculate_prenatal_lunation_ephemeris_prenatal_lunation_post"
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
+ }