@openephemeris/mcp-server 3.13.10 → 3.14.0

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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +3 -3
  3. package/dist/index.js +5 -43
  4. package/dist/oauth/discovery.d.ts +1 -0
  5. package/dist/oauth/discovery.js +8 -3
  6. package/dist/oauth/store.d.ts +20 -0
  7. package/dist/oauth/store.js +117 -3
  8. package/dist/oauth/token.js +4 -2
  9. package/dist/prompts.js +9 -0
  10. package/dist/server-sse.js +71 -64
  11. package/dist/tools/apps/bazi-app.js +2 -0
  12. package/dist/tools/apps/bi-wheel-app.js +55 -6
  13. package/dist/tools/apps/bodygraph-app.js +68 -10
  14. package/dist/tools/apps/chart-wheel-app.js +53 -4
  15. package/dist/tools/apps/location-tools.js +3 -0
  16. package/dist/tools/apps/moon-phase-app.js +75 -21
  17. package/dist/tools/apps/transit-timeline-app.js +75 -23
  18. package/dist/tools/apps/vedic-chart-app.js +2 -0
  19. package/dist/tools/auth.js +4 -0
  20. package/dist/tools/dev.js +3 -0
  21. package/dist/tools/index.d.ts +6 -0
  22. package/dist/tools/index.js +8 -7
  23. package/dist/tools/output-schemas.d.ts +37 -0
  24. package/dist/tools/output-schemas.js +130 -0
  25. package/dist/tools/specialized/acg.js +3 -0
  26. package/dist/tools/specialized/bazi.js +8 -0
  27. package/dist/tools/specialized/bi_wheel.js +2 -0
  28. package/dist/tools/specialized/chart_wheel.js +2 -0
  29. package/dist/tools/specialized/comparative.js +5 -0
  30. package/dist/tools/specialized/eclipse.js +2 -0
  31. package/dist/tools/specialized/electional.js +5 -0
  32. package/dist/tools/specialized/ephemeris_core.js +3 -0
  33. package/dist/tools/specialized/ephemeris_extended.js +9 -0
  34. package/dist/tools/specialized/hd_bodygraph.js +2 -0
  35. package/dist/tools/specialized/hd_cycles.js +3 -0
  36. package/dist/tools/specialized/hd_group.js +3 -0
  37. package/dist/tools/specialized/human_design.js +2 -0
  38. package/dist/tools/specialized/moon.js +3 -0
  39. package/dist/tools/specialized/natal.js +2 -0
  40. package/dist/tools/specialized/progressed.js +2 -0
  41. package/dist/tools/specialized/relocation.js +2 -0
  42. package/dist/tools/specialized/returns.js +4 -0
  43. package/dist/tools/specialized/synastry.js +2 -0
  44. package/dist/tools/specialized/transits.js +52 -43
  45. package/dist/tools/specialized/vedic.js +2 -0
  46. package/dist/tools/specialized/venus_star_points.js +7 -0
  47. package/dist/ui/bazi.html +1 -1
  48. package/dist/ui/bi-wheel.html +3514 -3121
  49. package/dist/ui/bodygraph.html +1308 -1261
  50. package/dist/ui/chart-wheel.html +3404 -3116
  51. package/dist/ui/moon-phase.html +2622 -2616
  52. package/dist/ui/transit-timeline.html +2695 -2656
  53. package/dist/ui/vedic-chart.html +1 -1
  54. package/package.json +14 -11
  55. package/smithery.yaml +16 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.14.0] — 2026-05-24
11
+
12
+ ### Added
13
+ - **Fullscreen bottom-bar layout** for bodygraph, bi-wheel, and chart-wheel iframes — chart fills the viewport, controls dock to the bottom for a cleaner read on small windows.
14
+ - **Debug highlight click handler** in the bodygraph (`?debugHighlight`) — click any gate to surface its center/channel data via iframe postMessage.
15
+ - **Highlight visualization** — highlighted gates render with CSS glow while non-highlighted gates dim, making activation patterns visually obvious.
16
+ - **Click payload metadata** — bodygraph gate groups now emit `data-center` for richer iframe click events.
17
+ - **OAuth refresh-token persistence** to Supabase, with in-memory expiry enforcement and a 90-day TTL (up from 30 days).
18
+ - **OAuth discovery test suite** with 401-compliance coverage.
19
+
20
+ ### Fixed
21
+ - Auth check now runs before `isInitializeRequest` — resolves the Claude Web `ofid_` initialization error.
22
+ - Light-mode ring tokens wired correctly in bi-wheel (were missing token references).
23
+ - Fullscreen bottom-bar layout polish — edge cases and styling fixes after initial rollout.
24
+
25
+ ### Changed
26
+ - **Server-side SVG is now the only bodygraph render path** — the legacy client-side renderer has been retired. Reduces bundle size and removes a source of cross-environment rendering drift.
27
+ - Claude Web connector docs updated to reflect OAuth (not API key) as the connection method.
28
+
29
+ ### Underlying API improvements (delivered transparently)
30
+ - **LLM-format projection now covers all 7 benchmarked endpoints** (was 4): `/ephemeris/natal-chart`, `/comparative/synastry`, `/comparative/natal-transits`, `/human-design/chart`, `/vedic/chart`, `/chinese/bazi`, `/acg/hits`. Weighted average reduction: 61% (GPT-4o), 60% (Claude). Range: 13%–98% per endpoint.
31
+ - `/acg/hits` newly supports `format=llm` — proximity hits as compact rows. Other `/acg/*` endpoints continue to return precision GeoJSON for map rendering.
32
+
33
+ ---
34
+
35
+ ## [3.13.11] — 2026-05-15
36
+
37
+ ### Added
38
+ - **Bi-Wheel UI Parity** — Unified the Bi-Wheel and Natal Chart Wheel architectures with matching feature sets.
39
+ - **Tally & Dignity Panels** — Integrated element/modality tallies and planetary dignity badges into the Bi-Wheel UI.
40
+ - **House System Controls** — Added dynamic house system selection to the Bi-Wheel aspect panel, triggering live recalculations.
41
+ - **Enhanced Rendering** — Exported dignity/tally logic from core renderer for consistent client-side UI generation.
42
+
43
+ ### Fixed
44
+ - Widened `BiWheelMode` type definitions to prevent TypeScript compilation errors during mode-specific panel rendering.
45
+ - Standardized CSS design tokens and layout containers between natal and comparative chart wheels.
46
+
47
+ ---
48
+
10
49
  ## [3.10.4] — 2026-04-24
11
50
 
12
51
  ### Changed
package/README.md CHANGED
@@ -183,7 +183,7 @@ Cursor deeplink payload:
183
183
 
184
184
  The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable HTTP support (MCP 2025-11-25 spec). Remote-only clients can connect directly — no bridge/proxy required:
185
185
 
186
- - **Claude Web**: Add `https://mcp.openephemeris.com/mcp` as a remote MCP server with `X-API-Key: your-key` header
186
+ - **Claude Web**: Add `https://mcp.openephemeris.com/mcp` as a custom connector URL — leave OAuth Client ID and Secret **blank**. The server uses OAuth 2.1 + PKCE (Dynamic Client Registration), so Claude handles authentication via a browser popup automatically.
187
187
  - **Via Smithery**: Use the [Smithery listing](https://smithery.ai/servers/open-ephemeris/openephemeris) for managed connections with any client
188
188
  - **Legacy SSE**: `https://mcp.openephemeris.com/sse` remains available for SSE-only clients
189
189
 
@@ -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`): **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`
358
+ - Registered tools (`OPENEPHEMERIS_PROFILE=dev`): **79**
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_bi_wheel`, `explore_human_design`, `explore_moon_phase`, `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`
360
360
  - Generic tools:
361
361
 
362
362
  ### Allowlist Families
package/dist/index.js CHANGED
@@ -7,12 +7,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { initTools, toolRegistry, formatToolResponse, modelVisibleTools } from "./tools/index.js";
9
9
  import { listPrompts, getPromptContent } from "./prompts.js";
10
+ // ── MCP App resource imports ────────────────────────────────────────────────
10
11
  import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
11
12
  import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
12
13
  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
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";
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
16
  function resolveServerVersion() {
17
17
  try {
18
18
  const here = path.dirname(fileURLToPath(import.meta.url));
@@ -42,9 +42,6 @@ const server = new Server({
42
42
  tools: {},
43
43
  prompts: {},
44
44
  resources: {},
45
- experimental: {
46
- "io.modelcontextprotocol/ui": {},
47
- },
48
45
  },
49
46
  });
50
47
  // List available tools
@@ -62,10 +59,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
62
59
  openWorldHint: tool.annotations?.openWorldHint ?? false,
63
60
  },
64
61
  // Expose MCP Apps UI linkage so Claude Desktop can prefetch the resource
65
- // and render the iframe when a tool result arrives.
66
- // Include BOTH the modern nested key (_meta.ui.resourceUri) and the
67
- // legacy flat key (_meta["ui/resourceUri"]) — the official ext-apps SDK
68
- // normalizes both via registerAppTool() for host compatibility.
69
62
  ...(tool._meta?.ui?.resourceUri
70
63
  ? {
71
64
  _meta: {
@@ -127,14 +120,6 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
127
120
  mimeType: BI_WHEEL_MIME_TYPE,
128
121
  });
129
122
  }
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
123
  if (getMoonPhaseBundle()) {
139
124
  resources.push({
140
125
  uri: MOON_PHASE_RESOURCE_URI,
@@ -143,14 +128,6 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
143
128
  mimeType: MOON_PHASE_MIME_TYPE,
144
129
  });
145
130
  }
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
- }
154
131
  return { resources };
155
132
  });
156
133
  // Read resource
@@ -174,24 +151,12 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
174
151
  throw new Error("Bi-Wheel UI bundle not found. Run `npm run build:ui` to build it.");
175
152
  return { contents: [{ uri: BI_WHEEL_RESOURCE_URI, mimeType: BI_WHEEL_MIME_TYPE, text: bundle }] };
176
153
  }
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
154
  if (uri === MOON_PHASE_RESOURCE_URI) {
184
155
  const bundle = getMoonPhaseBundle();
185
156
  if (!bundle)
186
157
  throw new Error("Moon Phase UI bundle not found. Run `npm run build:ui` to build it.");
187
158
  return { contents: [{ uri: MOON_PHASE_RESOURCE_URI, mimeType: MOON_PHASE_MIME_TYPE, text: bundle }] };
188
159
  }
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
- }
195
160
  throw new Error(`Unknown resource: ${uri}`);
196
161
  });
197
162
  // Handle tool calls
@@ -211,19 +176,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
211
176
  catch (error) {
212
177
  const durationMs = Date.now() - startTime;
213
178
  const errorMessage = error instanceof Error ? error.message : String(error);
179
+ const isRetryable = error.retryable === true || error.status === 401 || error.code === "auth_required";
214
180
  console.error(`[MCP] ❌ Failed: ${toolName} (${durationMs}ms) - ${errorMessage}`);
215
181
  return {
216
182
  content: [
217
183
  {
218
184
  type: "text",
219
- text: JSON.stringify({
220
- success: false,
221
- error: "TOOL_EXECUTION_ERROR",
222
- message: errorMessage,
223
- }, null, 2),
185
+ text: `Tool execution failed:\n\n${errorMessage}\n\nPlease read the error above carefully and assist the user (e.g. by providing them the auth link or explaining the issue).`,
224
186
  },
225
187
  ],
226
- isError: true,
188
+ isError: !isRetryable,
227
189
  };
228
190
  }
229
191
  });
@@ -8,4 +8,5 @@
8
8
  * All URLs use https:// per the OAuth 2.1 specification.
9
9
  */
10
10
  import { type Router } from "express";
11
+ export declare const PROTECTED_RESOURCE_METADATA_URL = "https://mcp.openephemeris.com/.well-known/oauth-protected-resource";
11
12
  export declare const oauthDiscoveryRouter: Router;
@@ -10,18 +10,23 @@
10
10
  import express from "express";
11
11
  const MCP_SERVER_ORIGIN = "https://mcp.openephemeris.com";
12
12
  const WEB_APP_ORIGIN = "https://openephemeris.com";
13
+ export const PROTECTED_RESOURCE_METADATA_URL = `${MCP_SERVER_ORIGIN}/.well-known/oauth-protected-resource`;
13
14
  export const oauthDiscoveryRouter = express.Router();
14
15
  // ---------------------------------------------------------------------------
15
16
  // GET /.well-known/oauth-protected-resource
17
+ // Also served at the path-scoped /.well-known/oauth-protected-resource/mcp
18
+ // per the latest MCP Authorization spec draft (some clients probe per-resource).
16
19
  // ---------------------------------------------------------------------------
17
- oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource", (_req, res) => {
20
+ const protectedResourceHandler = (_req, res) => {
18
21
  res.json({
19
- resource: MCP_SERVER_ORIGIN,
22
+ resource: `${MCP_SERVER_ORIGIN}/mcp`,
20
23
  authorization_servers: [MCP_SERVER_ORIGIN],
21
24
  bearer_methods_supported: ["header"],
22
25
  scopes_supported: ["openephemeris:read"],
23
26
  });
24
- });
27
+ };
28
+ oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource", protectedResourceHandler);
29
+ oauthDiscoveryRouter.get("/.well-known/oauth-protected-resource/mcp", protectedResourceHandler);
25
30
  // ---------------------------------------------------------------------------
26
31
  // GET /.well-known/oauth-authorization-server
27
32
  // ---------------------------------------------------------------------------
@@ -59,8 +59,28 @@ export declare class OAuthStore {
59
59
  used: boolean;
60
60
  expires_at: Date;
61
61
  }) | null>;
62
+ /**
63
+ * Refresh-token TTL — 90 days, sliding (reset on each rotation).
64
+ * Matches GitHub's 6-month convention more closely than Supabase's 30d
65
+ * default. Tradeoff: longer-lived refresh tokens = bigger blast radius
66
+ * on leak; rotation + Supabase RLS mitigates this. An active user who
67
+ * touches OE at least once per 90d gets an effectively permanent
68
+ * connection.
69
+ */
70
+ private static REFRESH_TTL_MS;
62
71
  storeRefreshToken(token: string, meta: object): void;
72
+ /**
73
+ * Synchronous in-memory consume. Kept for backward compat with any callers
74
+ * that don't need cross-instance resilience. Token endpoint must use
75
+ * consumeRefreshTokenAsync.
76
+ */
63
77
  consumeRefreshToken(token: string): object | null;
78
+ /**
79
+ * Async consume — checks in-memory first, then Supabase. This is what the
80
+ * token endpoint uses so refresh requests survive machine restarts and
81
+ * load-balancing across multiple Fly instances.
82
+ */
83
+ consumeRefreshTokenAsync(token: string): Promise<object | null>;
64
84
  generateRefreshToken(): string;
65
85
  reset(): void;
66
86
  private purgeExpired;
@@ -85,7 +85,7 @@ function persistCodeToSupabase(code, options, expiresAt) {
85
85
  redirect_uri: options.redirect_uri,
86
86
  code_challenge: options.code_challenge,
87
87
  code_challenge_method: options.code_challenge_method,
88
- scopes: ["mcp:tools"],
88
+ scopes: ["openephemeris:read"],
89
89
  consumed: false,
90
90
  expires_at: expiresAt.toISOString(),
91
91
  })
@@ -104,6 +104,36 @@ function persistCodeToSupabase(code, options, expiresAt) {
104
104
  });
105
105
  }
106
106
  // ---------------------------------------------------------------------------
107
+ // Refresh-token persistence (mirrors persistCodeToSupabase pattern)
108
+ // ---------------------------------------------------------------------------
109
+ function persistRefreshTokenToSupabase(token, meta, expiresAt) {
110
+ const cfg = getSupabaseConfig();
111
+ if (!cfg)
112
+ return; // No-op in tests
113
+ const m = meta;
114
+ postgrestRequest("POST", "/rest/v1/oauth_refresh_tokens", {
115
+ token,
116
+ client_id: m.client_id ?? "unknown",
117
+ user_id: m.user_id ?? null,
118
+ supabase_refresh_token: m.supabase_refresh_token ?? null,
119
+ consumed: false,
120
+ expires_at: expiresAt.toISOString(),
121
+ })
122
+ .then(({ status, data }) => {
123
+ if (status < 200 || status >= 300) {
124
+ console.warn(`[store] Supabase refresh-token persist failed: ${status}`, data);
125
+ return;
126
+ }
127
+ const rows = Array.isArray(data) ? data : [data];
128
+ const id = rows[0]?.id ?? "(unknown)";
129
+ console.info(`[store] refresh token persisted to Supabase id=${id}`);
130
+ })
131
+ .catch((err) => {
132
+ // Non-fatal — in-memory store still serves same-instance refreshes
133
+ console.warn("[store] Supabase refresh-token persist error (non-fatal):", err.message);
134
+ });
135
+ }
136
+ // ---------------------------------------------------------------------------
107
137
  // OAuthStore
108
138
  // ---------------------------------------------------------------------------
109
139
  export class OAuthStore {
@@ -206,18 +236,102 @@ export class OAuthStore {
206
236
  }
207
237
  }
208
238
  // ------------------------------------------------------------------
209
- // Refresh tokens (always in-memory — Supabase JWTs handle their own rotation)
239
+ // Refresh tokens
240
+ //
241
+ // In-memory map is the hot path. When Supabase is configured, we ALSO
242
+ // persist to oauth_refresh_tokens so a refresh request that lands on a
243
+ // different Fly machine (oe-mcp-live has min_machines_running=2) or
244
+ // arrives after a machine restart can still be honored. Without this,
245
+ // ~50% of refresh attempts fail with invalid_grant just from
246
+ // multi-instance load balancing alone.
210
247
  // ------------------------------------------------------------------
248
+ /**
249
+ * Refresh-token TTL — 90 days, sliding (reset on each rotation).
250
+ * Matches GitHub's 6-month convention more closely than Supabase's 30d
251
+ * default. Tradeoff: longer-lived refresh tokens = bigger blast radius
252
+ * on leak; rotation + Supabase RLS mitigates this. An active user who
253
+ * touches OE at least once per 90d gets an effectively permanent
254
+ * connection.
255
+ */
256
+ static REFRESH_TTL_MS = 90 * 24 * 60 * 60 * 1000;
211
257
  storeRefreshToken(token, meta) {
212
- this.refreshTokens.set(token, { ...meta, consumed: false });
258
+ const expires_at = new Date(Date.now() + OAuthStore.REFRESH_TTL_MS);
259
+ this.refreshTokens.set(token, { ...meta, consumed: false, expires_at });
260
+ // Async durability write — non-blocking
261
+ persistRefreshTokenToSupabase(token, meta, expires_at);
213
262
  }
263
+ /**
264
+ * Synchronous in-memory consume. Kept for backward compat with any callers
265
+ * that don't need cross-instance resilience. Token endpoint must use
266
+ * consumeRefreshTokenAsync.
267
+ */
214
268
  consumeRefreshToken(token) {
215
269
  const stored = this.refreshTokens.get(token);
216
270
  if (!stored || stored.consumed)
217
271
  return null;
272
+ if (stored.expires_at < new Date()) {
273
+ // In-memory entry outlived its TTL. Drop it; the Supabase row (if any)
274
+ // is also past its expires_at and will be rejected by the WHERE filter
275
+ // in consumeRefreshTokenAsync's slow path.
276
+ this.refreshTokens.delete(token);
277
+ return null;
278
+ }
218
279
  stored.consumed = true;
219
280
  return { ...stored };
220
281
  }
282
+ /**
283
+ * Async consume — checks in-memory first, then Supabase. This is what the
284
+ * token endpoint uses so refresh requests survive machine restarts and
285
+ * load-balancing across multiple Fly instances.
286
+ */
287
+ async consumeRefreshTokenAsync(token) {
288
+ // Fast path: in-memory hit
289
+ const inMem = this.consumeRefreshToken(token);
290
+ if (inMem)
291
+ return inMem;
292
+ // Slow path: Supabase lookup with atomic mark-consumed
293
+ const cfg = getSupabaseConfig();
294
+ if (!cfg)
295
+ return null;
296
+ try {
297
+ const selectPath = `/rest/v1/oauth_refresh_tokens` +
298
+ `?token=eq.${encodeURIComponent(token)}` +
299
+ `&consumed=eq.false` +
300
+ `&expires_at=gt.${encodeURIComponent(new Date().toISOString())}` +
301
+ `&limit=1`;
302
+ const { status: selStatus, data: selData } = await postgrestRequest("GET", selectPath);
303
+ if (selStatus < 200 || selStatus >= 300)
304
+ return null;
305
+ const rows = Array.isArray(selData) ? selData : [];
306
+ if (rows.length === 0)
307
+ return null;
308
+ const row = rows[0];
309
+ // Optimistic lock: mark consumed=true WHERE consumed=false
310
+ const updatePath = `/rest/v1/oauth_refresh_tokens?id=eq.${row.id}&consumed=eq.false`;
311
+ const { status: updStatus } = await postgrestRequest("PATCH", updatePath, {
312
+ consumed: true,
313
+ });
314
+ if (updStatus < 200 || updStatus >= 300) {
315
+ // Lost the race — another instance consumed it. Treat as invalid.
316
+ console.warn(`[store] Supabase refresh-token consume race lost id=${row.id} status=${updStatus}`);
317
+ return null;
318
+ }
319
+ console.info(`[store] refresh token consumed from Supabase id=${row.id}`);
320
+ const result = {
321
+ consumed: true,
322
+ client_id: row.client_id,
323
+ };
324
+ if (row.user_id)
325
+ result.user_id = row.user_id;
326
+ if (row.supabase_refresh_token)
327
+ result.supabase_refresh_token = row.supabase_refresh_token;
328
+ return result;
329
+ }
330
+ catch (err) {
331
+ console.warn("[store] Supabase refresh-token consume error:", err.message);
332
+ return null;
333
+ }
334
+ }
221
335
  generateRefreshToken() {
222
336
  return randomBytes(32).toString("hex");
223
337
  }
@@ -150,8 +150,10 @@ async function handleRefreshToken(req, res, store) {
150
150
  });
151
151
  return;
152
152
  }
153
- // Consume from local store (rotation enforcement)
154
- const storedMeta = store.consumeRefreshToken(refresh_token);
153
+ // Consume from store (in-memory first, then Supabase). Async so refresh
154
+ // requests survive Fly machine restarts and load-balancing across the
155
+ // two oe-mcp-live instances.
156
+ const storedMeta = (await store.consumeRefreshTokenAsync(refresh_token));
155
157
  if (!storedMeta) {
156
158
  res.status(400).json({
157
159
  error: "invalid_grant",
package/dist/prompts.js CHANGED
@@ -511,6 +511,9 @@ export const PROMPTS = [
511
511
  "- Cautions: [anything to be aware of]\n\n" +
512
512
  "Close with a clear **★ Top Recommendation** with a one-paragraph explanation of why this " +
513
513
  "window is the strongest pick.\n\n" +
514
+ "**Applying Moon aspects are critical**: The Moon's applying aspects (not yet exact) describe what " +
515
+ "unfolds after the event begins. A Moon applying to trine Jupiter is excellent; a Moon applying to " +
516
+ "square Saturn is a warning. Always prioritize applying aspects over separating ones.\n\n" +
514
517
  "### If No Good Windows Exist\n" +
515
518
  "Be direct: 'There are no high-quality windows in your date range because [specific reason]. " +
516
519
  "I'd suggest expanding to [suggested range] where [better conditions] occur.' " +
@@ -563,6 +566,9 @@ export const PROMPTS = [
563
566
  "- **Whole Sign Bhavas**: the Lagna's sign = the entire 1st Bhava. No intercepted signs.\n" +
564
567
  "- **Nakshatras**: 27 lunar mansions of ~13°20' each — the heart of timing and soul-level analysis\n" +
565
568
  "- **The Moon is king**: in Jyotish the Moon holds more weight than the Sun for disposition and timing\n" +
569
+ "- **Vedic aspects (drishti)** differ from Western aspects: they are whole-sign (not degree-orb), and " +
570
+ "Mars, Jupiter, and Saturn have special additional aspects (Mars: 4th/8th; Jupiter: 5th/9th; Saturn: 3rd/10th). " +
571
+ "Use the aspect data as returned by the API \u2014 do not apply Western orb-based aspect rules.\n" +
566
572
  "- **Guna of chart**: Sattvic (clear, spiritual), Rajasic (active, worldly), or Tamasic (inert)?\n\n" +
567
573
  "## Step 5 — Structure the Reading\n\n" +
568
574
  "### The Lagna (Ascendant)\n" +
@@ -613,6 +619,9 @@ export const PROMPTS = [
613
619
  " | 🐉 Dragon | 07:00–09:00 | 🐕 Dog | 19:00–21:00 |\n" +
614
620
  " | 🐍 Snake | 09:00–11:00 | 🐖 Pig | 21:00–23:00 |\n" +
615
621
  " If the hour is unknown, note it — the Hour Pillar will be missing.\n" +
622
+ " If the birth time is approximate (e.g. 'around noon'), note that the Hour Pillar may be wrong — " +
623
+ "each shí block is only 2 hours wide. Offer to calculate for the adjacent pillar as a comparison " +
624
+ "so the user can see which resonates more.\n" +
616
625
  "3. **Birth city** — BaZi uses LOCAL solar time, not clock time. City is needed to detect " +
617
626
  "any significant longitude offset from the timezone meridian.\n\n" +
618
627
  "## Step 2 — Local Solar Time\n" +