@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.
- package/CHANGELOG.md +39 -0
- package/README.md +3 -3
- package/dist/index.js +5 -43
- package/dist/oauth/discovery.d.ts +1 -0
- package/dist/oauth/discovery.js +8 -3
- package/dist/oauth/store.d.ts +20 -0
- package/dist/oauth/store.js +117 -3
- package/dist/oauth/token.js +4 -2
- package/dist/prompts.js +9 -0
- package/dist/server-sse.js +71 -64
- package/dist/tools/apps/bazi-app.js +2 -0
- package/dist/tools/apps/bi-wheel-app.js +55 -6
- package/dist/tools/apps/bodygraph-app.js +68 -10
- package/dist/tools/apps/chart-wheel-app.js +53 -4
- package/dist/tools/apps/location-tools.js +3 -0
- package/dist/tools/apps/moon-phase-app.js +75 -21
- package/dist/tools/apps/transit-timeline-app.js +75 -23
- package/dist/tools/apps/vedic-chart-app.js +2 -0
- package/dist/tools/auth.js +4 -0
- package/dist/tools/dev.js +3 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +8 -7
- package/dist/tools/output-schemas.d.ts +37 -0
- package/dist/tools/output-schemas.js +130 -0
- package/dist/tools/specialized/acg.js +3 -0
- package/dist/tools/specialized/bazi.js +8 -0
- package/dist/tools/specialized/bi_wheel.js +2 -0
- package/dist/tools/specialized/chart_wheel.js +2 -0
- package/dist/tools/specialized/comparative.js +5 -0
- package/dist/tools/specialized/eclipse.js +2 -0
- package/dist/tools/specialized/electional.js +5 -0
- package/dist/tools/specialized/ephemeris_core.js +3 -0
- package/dist/tools/specialized/ephemeris_extended.js +9 -0
- package/dist/tools/specialized/hd_bodygraph.js +2 -0
- package/dist/tools/specialized/hd_cycles.js +3 -0
- package/dist/tools/specialized/hd_group.js +3 -0
- package/dist/tools/specialized/human_design.js +2 -0
- package/dist/tools/specialized/moon.js +3 -0
- package/dist/tools/specialized/natal.js +2 -0
- package/dist/tools/specialized/progressed.js +2 -0
- package/dist/tools/specialized/relocation.js +2 -0
- package/dist/tools/specialized/returns.js +4 -0
- package/dist/tools/specialized/synastry.js +2 -0
- package/dist/tools/specialized/transits.js +52 -43
- package/dist/tools/specialized/vedic.js +2 -0
- package/dist/tools/specialized/venus_star_points.js +7 -0
- package/dist/ui/bazi.html +1 -1
- package/dist/ui/bi-wheel.html +3514 -3121
- package/dist/ui/bodygraph.html +1308 -1261
- package/dist/ui/chart-wheel.html +3404 -3116
- package/dist/ui/moon-phase.html +2622 -2616
- package/dist/ui/transit-timeline.html +2695 -2656
- package/dist/ui/vedic-chart.html +1 -1
- package/package.json +14 -11
- 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
|
|
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`): **
|
|
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`, `
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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;
|
package/dist/oauth/discovery.js
CHANGED
|
@@ -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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/dist/oauth/store.d.ts
CHANGED
|
@@ -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;
|
package/dist/oauth/store.js
CHANGED
|
@@ -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: ["
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/dist/oauth/token.js
CHANGED
|
@@ -150,8 +150,10 @@ async function handleRefreshToken(req, res, store) {
|
|
|
150
150
|
});
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
|
-
// Consume from
|
|
154
|
-
|
|
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" +
|