@openephemeris/mcp-server 4.17.0 → 4.18.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 +28 -0
- package/README.md +10 -10
- package/dist/backend/client.d.ts +57 -1
- package/dist/backend/client.js +125 -15
- package/dist/prompts.js +26 -24
- package/dist/server-sse.d.ts +18 -0
- package/dist/server-sse.js +41 -6
- package/dist/tools/apps/_render-token.d.ts +34 -0
- package/dist/tools/apps/_render-token.js +50 -0
- package/dist/tools/apps/bazi-app.js +36 -12
- package/dist/tools/apps/bi-wheel-app.d.ts +9 -2
- package/dist/tools/apps/bi-wheel-app.js +76 -117
- package/dist/tools/apps/bodygraph-app.js +88 -41
- package/dist/tools/apps/chart-wheel-app.js +56 -16
- package/dist/tools/apps/location-tools.js +10 -3
- package/dist/tools/apps/moon-phase-app.js +10 -2
- package/dist/tools/apps/transit-timeline-app.js +3 -4
- package/dist/tools/apps/vedic-chart-app.js +10 -1
- package/dist/tools/datetime-historical.js +7 -2
- package/dist/tools/datetime.js +2 -1
- package/dist/tools/dev.js +7 -6
- package/dist/tools/specialized/electional.js +4 -3
- package/dist/tools/specialized/ephemeris_extended.js +19 -4
- package/dist/tools/specialized/hd_group.js +2 -2
- package/dist/tools/specialized/moon.d.ts +1 -1
- package/dist/tools/specialized/moon.js +51 -43
- package/dist/tools/specialized/progressed.js +2 -25
- package/dist/tools/specialized/transits.js +5 -5
- package/dist/ui/bazi.html +1523 -1522
- package/dist/ui/bi-wheel.html +406 -374
- package/dist/ui/bodygraph.html +83 -79
- package/dist/ui/chart-wheel.html +394 -361
- package/dist/ui/transit-timeline.html +1 -1
- package/dist/ui/vedic-chart.html +818 -818
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ Version numbering follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [4.18.0] — 2026-09-26
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`explore_bi_wheel` progressed, solar return and lunar return modes work again.** They sent
|
|
14
|
+
requests the API rejected ("Validation failed" / "birth_datetime is required"); all six modes now
|
|
15
|
+
return a chart.
|
|
16
|
+
- **Solar arc directions are exact.** `explore_bi_wheel` mode `solar_arc` used a flat ~1°-per-year
|
|
17
|
+
estimate and left the houses at their birth positions. It now uses the Sun's real arc from the API:
|
|
18
|
+
every planet, angle and house cusp is directed by the same arc, with correct signs and houses.
|
|
19
|
+
`ephemeris_progressed_chart` with `method: "solar_arc"` directs the angles and house cusps too.
|
|
20
|
+
- **Every lunar node is named Mean or True.** The charts show "North Node (Mean)", "North Node
|
|
21
|
+
(True)", "South Node (Mean)" and "South Node (True)" as four separate points. The two South Nodes
|
|
22
|
+
used to merge into one, and the mean North Node was shown as a bare "North Node". The Mean/True
|
|
23
|
+
toggle now applies to the South Node as well.
|
|
24
|
+
- **Bi-wheel recalculation keeps the house system you choose** for the outer chart too (it was always
|
|
25
|
+
Placidus).
|
|
26
|
+
- **Black Moon Lilith appears on the chart wheel, named Mean, True or Interpolated.** Asking
|
|
27
|
+
`explore_natal_chart` for `lilith` or `lilith_true` returned the chart without it. The three Liliths
|
|
28
|
+
are now shown as "Lilith (Mean)", "Lilith (True)" and "Lilith (Interpolated)" (new slug
|
|
29
|
+
`lilith_interpolated`), never merged into one.
|
|
30
|
+
- **`explore_natal_chart` with `bodies: ["all"]` works.** It was rejected because it asked for Vertex and
|
|
31
|
+
Part of Fortune as extra bodies; "all" now covers every body the chart can add.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
- **`include_visual` on `ephemeris_progressed_chart`.** The API never draws a progressed chart
|
|
35
|
+
image, so the option returned no picture. It was never charged. To see progressions over the natal
|
|
36
|
+
chart, use `explore_bi_wheel` with mode `progressed`.
|
|
37
|
+
|
|
10
38
|
## [4.17.0] — 2026-09-24
|
|
11
39
|
|
|
12
40
|
### Added
|
package/README.md
CHANGED
|
@@ -212,7 +212,7 @@ The server is hosted at `https://mcp.openephemeris.com/mcp` with full Streamable
|
|
|
212
212
|
|
|
213
213
|
- Missing/invalid credentials (`401`): tool call fails with a message that points users to sign up/sign in at `https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount`, then create/manage keys in `https://openephemeris.com/dashboard?tab=account`.
|
|
214
214
|
- Tier-gated endpoint (`403`): tool call returns an upgrade-required message with `https://openephemeris.com/pay` and dashboard billing/key management link.
|
|
215
|
-
-
|
|
215
|
+
- Out of credits (`402`): tool call returns a one-tap top-up link (Explorer's 150 free credits are one-time and do not reset; plan allowances renew each billing period) plus the dashboard usage link. Failed calls (any `4xx`/`5xx`) are refunded.
|
|
216
216
|
- Burst/rate limit (`429`): tool call returns retry guidance and links to dashboard usage monitoring.
|
|
217
217
|
|
|
218
218
|
## What You Can Ask
|
|
@@ -256,13 +256,13 @@ instead of the picture, so nothing breaks, you just don't get the wheel.
|
|
|
256
256
|
| Tool | What opens | What you can click | Credits |
|
|
257
257
|
|---|---|---|---|
|
|
258
258
|
| `explore_natal_chart` | Natal wheel — planets, houses, aspects, angles | Planets, houses, aspect lines; recalculate with new settings | 1 |
|
|
259
|
-
| `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 |
|
|
260
|
-
| `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables |
|
|
261
|
-
| `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels |
|
|
262
|
-
| `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type |
|
|
259
|
+
| `explore_bi_wheel` | Two charts on one wheel: transits, synastry, progressions | Either wheel's planets, houses, and the aspects between them | 2 (6 for solar/lunar return) |
|
|
260
|
+
| `explore_human_design` | Human Design bodygraph, with a mandala view toggle | Centers, gates, channels, planets, variables | 4 |
|
|
261
|
+
| `explore_human_design_transit` | Today's planets laid over a natal bodygraph | Transit-activated channels | 5 |
|
|
262
|
+
| `explore_human_design_connection` | Two bodygraphs combined, every shared channel classified | Connection channels by type | 5 |
|
|
263
263
|
| `explore_vedic_chart` | South Indian Rashi grid — sidereal placements and Lagna | Each rashi, for its placements and nakshatras | 3 |
|
|
264
264
|
| `explore_bazi_chart` | Four Pillars (四柱命盘) — Year, Month, Day, Hour | Each pillar | 3 |
|
|
265
|
-
| `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 |
|
|
265
|
+
| `explore_transit_timeline` | Upcoming transit hits in date order | Individual hits | 6 for up to 1 year (priced by span) |
|
|
266
266
|
| `explore_moon_phase` | Moon dial — illumination, phase, sign, void-of-course | Recalculate for another moment | 3 |
|
|
267
267
|
|
|
268
268
|
Ask for these the way you'd ask a person: *"show me my chart"*, *"put today's transits over my Human Design"*, *"what's the moon doing right now"*. The model picks the app.
|
|
@@ -300,10 +300,10 @@ Screenshots of each are on the way.
|
|
|
300
300
|
| Lunar return | `ephemeris_lunar_return` | Explorer |
|
|
301
301
|
| Planetary return | `ephemeris_planetary_return` | Explorer |
|
|
302
302
|
| Astrocartography lines | `acg_power_lines` | Developer |
|
|
303
|
-
| ACG hits at location | `acg_hits` |
|
|
303
|
+
| ACG hits at location | `acg_hits` | Developer |
|
|
304
304
|
| Venus Star Points | `venus_star_points` + 4 more | Explorer |
|
|
305
|
-
| Chart wheel image | `ephemeris_chart_wheel` |
|
|
306
|
-
| Bi-wheel image | `ephemeris_bi_wheel` |
|
|
305
|
+
| Chart wheel image | `ephemeris_chart_wheel` | Explorer |
|
|
306
|
+
| Bi-wheel image | `ephemeris_bi_wheel` | Explorer |
|
|
307
307
|
| Dignities / Midpoints / Fixed stars | `ephemeris_dignities`, `ephemeris_midpoints`, `ephemeris_fixed_stars` | Explorer |
|
|
308
308
|
|
|
309
309
|
## Tooling Model
|
|
@@ -335,7 +335,7 @@ Screenshots of each are on the way.
|
|
|
335
335
|
| `OPENEPHEMERIS_PROFILE` | No | `dev` by default |
|
|
336
336
|
| `OPENEPHEMERIS_TOOLS` | No | `core` (default) advertises a focused everyday tool set; `full` advertises every tool. See [Tool surface](#tool-surface) |
|
|
337
337
|
| `OPENEPHEMERIS_TELEMETRY` | No | Set to `0`/`false`/`off` to disable anonymous usage reporting. `DO_NOT_TRACK=1` also works. See [Telemetry](#telemetry) |
|
|
338
|
-
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth |
|
|
338
|
+
| `OPENEPHEMERIS_SERVICE_KEY` | No | Internal service auth (stdio only; the hosted server refuses to start with one and always authenticates as the signed-in user) |
|
|
339
339
|
| `OPENEPHEMERIS_JWT` | No | Bearer token auth |
|
|
340
340
|
| `OPENEPHEMERIS_DEV_ALLOWLIST_PATH` | No | Override allowlist file path |
|
|
341
341
|
| `MCP_USER_ID` | No | Per-instance user identifier |
|
package/dist/backend/client.d.ts
CHANGED
|
@@ -6,7 +6,29 @@ export interface BackendConfig {
|
|
|
6
6
|
apiKey?: string;
|
|
7
7
|
/** Back-compat: previous name for JWT token (Bearer). */
|
|
8
8
|
authToken?: string;
|
|
9
|
+
/**
|
|
10
|
+
* When false, the client uses ONLY the credentials passed in this config and
|
|
11
|
+
* never falls back to the process environment (OPENEPHEMERIS_SERVICE_KEY,
|
|
12
|
+
* OPENEPHEMERIS_API_KEY, OPENEPHEMERIS_JWT and their legacy aliases).
|
|
13
|
+
*
|
|
14
|
+
* The hosted HTTP server builds one client per user session and MUST pass
|
|
15
|
+
* false: the interceptor sends X-Service-Key ahead of the user's own key, so
|
|
16
|
+
* an env credential inherited by a per-session client would make every
|
|
17
|
+
* user's calls unmetered (service key) or billed to one account (API key /
|
|
18
|
+
* JWT). Defaults to true so the stdio/local server keeps reading its config
|
|
19
|
+
* from the environment exactly as before.
|
|
20
|
+
*/
|
|
21
|
+
inheritEnvCredentials?: boolean;
|
|
9
22
|
}
|
|
23
|
+
/** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
|
|
24
|
+
export declare const SERVICE_KEY_ENV_VARS: readonly ["OPENEPHEMERIS_SERVICE_KEY", "ASTROMCP_SERVICE_KEY", "MERIDIAN_SERVICE_KEY"];
|
|
25
|
+
/** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
|
|
26
|
+
export declare const USER_CREDENTIAL_ENV_VARS: readonly ["OPENEPHEMERIS_API_KEY", "ASTROMCP_API_KEY", "MERIDIAN_API_KEY", "OPENEPHEMERIS_JWT", "ASTROMCP_JWT", "MERIDIAN_AUTH_TOKEN"];
|
|
27
|
+
/** Names of the credential env vars that are set (non-empty) in `env`. */
|
|
28
|
+
export declare function presentCredentialEnvVars(env?: NodeJS.ProcessEnv): {
|
|
29
|
+
serviceKeys: string[];
|
|
30
|
+
userCredentials: string[];
|
|
31
|
+
};
|
|
10
32
|
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
11
33
|
export interface BackendRequestOptions {
|
|
12
34
|
params?: Record<string, unknown>;
|
|
@@ -25,8 +47,33 @@ export declare const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboar
|
|
|
25
47
|
export declare const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
26
48
|
export declare const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
27
49
|
/** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
|
|
28
|
-
export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
|
|
50
|
+
export declare const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
|
|
51
|
+
/**
|
|
52
|
+
* The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
|
|
53
|
+
* the caller's account so it opens Stripe checkout without a sign-in step
|
|
54
|
+
* (most people hitting the wall are in ChatGPT/Claude and not signed in on the
|
|
55
|
+
* site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
|
|
56
|
+
* clicks from raw API ones; otherwise fall back to the unsigned link.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveTopupUrl(apiTopupUrl: unknown): string;
|
|
29
59
|
export declare const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
60
|
+
/**
|
|
61
|
+
* Whether an error body (typically a 422) is really the credit wall: the
|
|
62
|
+
* detail / type / field codes mention insufficient credits or an exceeded
|
|
63
|
+
* quota. Checked over the whole serialized body because the signal can sit in
|
|
64
|
+
* `detail`, `type`, or a nested `errors[].code`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function looksLikeCreditExhaustion(data: unknown, msg?: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* True for errors that mean "this account cannot be charged / is not allowed"
|
|
69
|
+
* — the credit wall (402), missing auth (401), or a tier gate (403). Tools
|
|
70
|
+
* that fan out or degrade gracefully must rethrow these instead of swallowing
|
|
71
|
+
* them: a placeholder result at zero credits hides the paywall (and its
|
|
72
|
+
* top-up link) from the assistant and reads as real data.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isBillingOrAuthError(err: unknown): err is BackendError;
|
|
75
|
+
/** Rethrows the first billing/auth BackendError among settled results, if any. */
|
|
76
|
+
export declare function rethrowBillingOrAuth(results: PromiseSettledResult<unknown>[]): void;
|
|
30
77
|
export declare class BackendError extends Error {
|
|
31
78
|
readonly status: number;
|
|
32
79
|
readonly code: string;
|
|
@@ -59,6 +106,15 @@ export declare class BackendClient {
|
|
|
59
106
|
* (JWT is the lowest-priority credential in the interceptor).
|
|
60
107
|
*/
|
|
61
108
|
setJwt(jwt: string): void;
|
|
109
|
+
/**
|
|
110
|
+
* Drop every credential this client holds (service key, API key, JWT). The
|
|
111
|
+
* hosted HTTP server calls this on the module singleton at startup: the
|
|
112
|
+
* singleton is what getActiveClient() falls back to outside a session
|
|
113
|
+
* context, and it was built from the environment at import time.
|
|
114
|
+
*/
|
|
115
|
+
clearCredentials(): void;
|
|
116
|
+
/** Which configured credential the interceptor will send (diagnostics/tests). */
|
|
117
|
+
credentialKind(): "service_key" | "api_key" | "jwt" | null;
|
|
62
118
|
/**
|
|
63
119
|
* Start the device auth flow in the background if not already running.
|
|
64
120
|
* Called automatically when no credentials are found in the interceptor.
|
package/dist/backend/client.js
CHANGED
|
@@ -2,6 +2,29 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import axios, { AxiosError } from "axios";
|
|
3
3
|
import { CredentialManager } from "../auth/credentials.js";
|
|
4
4
|
import { DeviceAuthFlow } from "../auth/device-auth.js";
|
|
5
|
+
/** Env vars holding a service key — bypasses metering; never valid on the hosted server. */
|
|
6
|
+
export const SERVICE_KEY_ENV_VARS = [
|
|
7
|
+
"OPENEPHEMERIS_SERVICE_KEY",
|
|
8
|
+
"ASTROMCP_SERVICE_KEY",
|
|
9
|
+
"MERIDIAN_SERVICE_KEY",
|
|
10
|
+
];
|
|
11
|
+
/** Env vars holding one user's credential — on the hosted server they would bill every session to one account. */
|
|
12
|
+
export const USER_CREDENTIAL_ENV_VARS = [
|
|
13
|
+
"OPENEPHEMERIS_API_KEY",
|
|
14
|
+
"ASTROMCP_API_KEY",
|
|
15
|
+
"MERIDIAN_API_KEY",
|
|
16
|
+
"OPENEPHEMERIS_JWT",
|
|
17
|
+
"ASTROMCP_JWT",
|
|
18
|
+
"MERIDIAN_AUTH_TOKEN",
|
|
19
|
+
];
|
|
20
|
+
/** Names of the credential env vars that are set (non-empty) in `env`. */
|
|
21
|
+
export function presentCredentialEnvVars(env = process.env) {
|
|
22
|
+
const isSet = (name) => typeof env[name] === "string" && env[name].trim() !== "";
|
|
23
|
+
return {
|
|
24
|
+
serviceKeys: SERVICE_KEY_ENV_VARS.filter(isSet),
|
|
25
|
+
userCredentials: USER_CREDENTIAL_ENV_VARS.filter(isSet),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
5
28
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
6
29
|
const RETRY_DELAYS_MS = [1_000, 2_000, 4_000]; // Three retries with backoff
|
|
7
30
|
// NOTE: 429 is intentionally NOT retryable. Every expensive compute endpoint is
|
|
@@ -23,8 +46,64 @@ export const DASHBOARD_ACCOUNT_URL = "https://openephemeris.com/dashboard?tab=ac
|
|
|
23
46
|
export const LOGIN_SIGNUP_URL = "https://openephemeris.com/login?signup=true&redirect=%2Fdashboard%3Ftab%3Daccount";
|
|
24
47
|
export const UPGRADE_URL = "https://openephemeris.com/pricing";
|
|
25
48
|
/** One-tap $5 → 150-credit top-up (signs the user in if needed, then redirects to the prefilled Stripe Payment Link). */
|
|
26
|
-
export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5";
|
|
49
|
+
export const TOPUP_URL = "https://openephemeris.com/topup?pack=payg_5&src=mcp_402";
|
|
50
|
+
/**
|
|
51
|
+
* The API's 402 carries `upgrade.topup_url`: the same /topup link, signed with
|
|
52
|
+
* the caller's account so it opens Stripe checkout without a sign-in step
|
|
53
|
+
* (most people hitting the wall are in ChatGPT/Claude and not signed in on the
|
|
54
|
+
* site). Use it when present, re-tagged src=mcp_402 so the funnel can tell MCP
|
|
55
|
+
* clicks from raw API ones; otherwise fall back to the unsigned link.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveTopupUrl(apiTopupUrl) {
|
|
58
|
+
if (typeof apiTopupUrl !== "string")
|
|
59
|
+
return TOPUP_URL;
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(apiTopupUrl);
|
|
62
|
+
if (url.protocol !== "https:" || url.hostname !== "openephemeris.com" || url.pathname !== "/topup") {
|
|
63
|
+
return TOPUP_URL;
|
|
64
|
+
}
|
|
65
|
+
url.searchParams.set("src", "mcp_402");
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return TOPUP_URL;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
27
72
|
export const WALLET_TOPUP_URL = "https://openephemeris.com/wallet";
|
|
73
|
+
const CREDIT_EXHAUSTION_RE = /credits?_insufficient|insufficient[ _-]?credits?|not enough credits|out of credits|credits?[ _-]exhausted|quota[ _-]exceeded|usage_quota|requires \d+ credits/i;
|
|
74
|
+
/**
|
|
75
|
+
* Whether an error body (typically a 422) is really the credit wall: the
|
|
76
|
+
* detail / type / field codes mention insufficient credits or an exceeded
|
|
77
|
+
* quota. Checked over the whole serialized body because the signal can sit in
|
|
78
|
+
* `detail`, `type`, or a nested `errors[].code`.
|
|
79
|
+
*/
|
|
80
|
+
export function looksLikeCreditExhaustion(data, msg) {
|
|
81
|
+
let text = msg ?? "";
|
|
82
|
+
try {
|
|
83
|
+
text += " " + (typeof data === "string" ? data : JSON.stringify(data ?? ""));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// unserializable body — fall back to the message alone
|
|
87
|
+
}
|
|
88
|
+
return CREDIT_EXHAUSTION_RE.test(text);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* True for errors that mean "this account cannot be charged / is not allowed"
|
|
92
|
+
* — the credit wall (402), missing auth (401), or a tier gate (403). Tools
|
|
93
|
+
* that fan out or degrade gracefully must rethrow these instead of swallowing
|
|
94
|
+
* them: a placeholder result at zero credits hides the paywall (and its
|
|
95
|
+
* top-up link) from the assistant and reads as real data.
|
|
96
|
+
*/
|
|
97
|
+
export function isBillingOrAuthError(err) {
|
|
98
|
+
return err instanceof BackendError && (err.status === 402 || err.status === 401 || err.status === 403);
|
|
99
|
+
}
|
|
100
|
+
/** Rethrows the first billing/auth BackendError among settled results, if any. */
|
|
101
|
+
export function rethrowBillingOrAuth(results) {
|
|
102
|
+
for (const r of results) {
|
|
103
|
+
if (r.status === "rejected" && isBillingOrAuthError(r.reason))
|
|
104
|
+
throw r.reason;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
28
107
|
function sleep(ms) {
|
|
29
108
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
109
|
}
|
|
@@ -53,22 +132,24 @@ export class BackendClient {
|
|
|
53
132
|
_authFlowResult = null;
|
|
54
133
|
constructor(config) {
|
|
55
134
|
this.userId = config.userId || "anonymous";
|
|
135
|
+
// See BackendConfig.inheritEnvCredentials — per-session hosted clients opt out.
|
|
136
|
+
const env = config.inheritEnvCredentials === false ? {} : process.env;
|
|
56
137
|
this.jwt =
|
|
57
138
|
config.jwt ||
|
|
58
139
|
config.authToken ||
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
140
|
+
env.OPENEPHEMERIS_JWT ||
|
|
141
|
+
env.ASTROMCP_JWT ||
|
|
142
|
+
env.MERIDIAN_AUTH_TOKEN;
|
|
62
143
|
this.serviceKey =
|
|
63
144
|
config.serviceKey ||
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
env.OPENEPHEMERIS_SERVICE_KEY ||
|
|
146
|
+
env.ASTROMCP_SERVICE_KEY ||
|
|
147
|
+
env.MERIDIAN_SERVICE_KEY;
|
|
67
148
|
this.apiKey =
|
|
68
149
|
config.apiKey ||
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
150
|
+
env.OPENEPHEMERIS_API_KEY ||
|
|
151
|
+
env.ASTROMCP_API_KEY ||
|
|
152
|
+
env.MERIDIAN_API_KEY;
|
|
72
153
|
this.credentialManager = new CredentialManager();
|
|
73
154
|
this.client = axios.create({
|
|
74
155
|
baseURL: config.baseURL ||
|
|
@@ -138,6 +219,27 @@ export class BackendClient {
|
|
|
138
219
|
setJwt(jwt) {
|
|
139
220
|
this.jwt = jwt;
|
|
140
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Drop every credential this client holds (service key, API key, JWT). The
|
|
224
|
+
* hosted HTTP server calls this on the module singleton at startup: the
|
|
225
|
+
* singleton is what getActiveClient() falls back to outside a session
|
|
226
|
+
* context, and it was built from the environment at import time.
|
|
227
|
+
*/
|
|
228
|
+
clearCredentials() {
|
|
229
|
+
this.serviceKey = undefined;
|
|
230
|
+
this.apiKey = undefined;
|
|
231
|
+
this.jwt = undefined;
|
|
232
|
+
}
|
|
233
|
+
/** Which configured credential the interceptor will send (diagnostics/tests). */
|
|
234
|
+
credentialKind() {
|
|
235
|
+
if (this.serviceKey)
|
|
236
|
+
return "service_key";
|
|
237
|
+
if (this.apiKey)
|
|
238
|
+
return "api_key";
|
|
239
|
+
if (this.jwt)
|
|
240
|
+
return "jwt";
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
141
243
|
/**
|
|
142
244
|
* Start the device auth flow in the background if not already running.
|
|
143
245
|
* Called automatically when no credentials are found in the interceptor.
|
|
@@ -270,7 +372,12 @@ export class BackendClient {
|
|
|
270
372
|
`through Claude's connector settings, your session expired: in Claude's connector ` +
|
|
271
373
|
`settings, toggle OpenEphemeris off and on to reconnect — this only takes a moment.`, 401, "auth_required", false, LOGIN_SIGNUP_URL);
|
|
272
374
|
}
|
|
273
|
-
|
|
375
|
+
// A 422 whose body says the caller ran out of credits is the credit wall
|
|
376
|
+
// wearing a validation status (the Go visual add-on reservation returned
|
|
377
|
+
// 422 `credits_insufficient` before it moved to 402). Route it through
|
|
378
|
+
// the same 402 path so the assistant gets the one credit-wall message
|
|
379
|
+
// and top-up link, not "Validation error: ...".
|
|
380
|
+
if (status === 402 || (status === 422 && looksLikeCreditExhaustion(data, msg))) {
|
|
274
381
|
// PRICING SSOT: apps/web/config/billing-plans.ts — Explorer grant
|
|
275
382
|
// (EXPLORER.creditsNumeric = 150, one-time), Pro/`developer`
|
|
276
383
|
// (monthlyPrice = 29, credits = 75,000), and CREDIT_TOPUPS
|
|
@@ -289,18 +396,21 @@ export class BackendClient {
|
|
|
289
396
|
// One direct link, not a dashboard tab: /topup?pack=payg_5 signs the
|
|
290
397
|
// user in if needed and redirects straight to the prefilled Stripe
|
|
291
398
|
// Payment Link for the $5 → 150-credit pack.
|
|
399
|
+
const topupUrl = resolveTopupUrl(dataObj.upgrade?.topup_url);
|
|
292
400
|
let upsellMsg = "";
|
|
293
|
-
let actionUrl =
|
|
401
|
+
let actionUrl = topupUrl;
|
|
294
402
|
if (currentTier === 'explorer') {
|
|
295
403
|
upsellMsg =
|
|
296
|
-
`Let the user know warmly: Their 150 free credits are used up. ` +
|
|
297
|
-
`$5 gets 150 more — one tap: ${
|
|
404
|
+
`Let the user know warmly: Their 150 free credits are used up (they do not reset). ` +
|
|
405
|
+
`$5 gets 150 more — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
|
|
406
|
+
`(show this exact link to the user as a clickable link). ` +
|
|
298
407
|
`Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
|
|
299
408
|
}
|
|
300
409
|
else if (currentTier === 'payg') {
|
|
301
410
|
upsellMsg =
|
|
302
411
|
`Let the user know warmly: Their wallet balance is empty. ` +
|
|
303
|
-
`$5 gets 150 more credits — one tap: ${
|
|
412
|
+
`$5 gets 150 more credits — one tap, no sign-in, opens checkout directly: ${topupUrl} ` +
|
|
413
|
+
`(show this exact link to the user as a clickable link). ` +
|
|
304
414
|
`Or Pro ($29/mo, 75,000 credits): ${UPGRADE_URL}?plan=developer`;
|
|
305
415
|
}
|
|
306
416
|
else {
|
package/dist/prompts.js
CHANGED
|
@@ -36,8 +36,8 @@ export const PROMPTS = [
|
|
|
36
36
|
"| `solar_return_year_ahead` | Annual Solar Return chart — themes for the coming birthday year |\n" +
|
|
37
37
|
"| `developer_api_integration` | Help a dev integrate the Open Ephemeris API into their app |\n\n" +
|
|
38
38
|
"## Before Calling Any Tool — Three Rules\n" +
|
|
39
|
-
"1. **Coordinates first**:
|
|
40
|
-
"
|
|
39
|
+
"1. **Coordinates first**: Resolve place names with `location_search` — never recall " +
|
|
40
|
+
"coordinates from memory. Positive = North/East; negative = South/West.\n" +
|
|
41
41
|
"2. **Datetime format**: Pass birth times as local ISO 8601 + separate `timezone` IANA name " +
|
|
42
42
|
"(e.g. `America/Chicago`), OR as a UTC-offset datetime `1990-04-15T14:30:00-05:00`. " +
|
|
43
43
|
"Never append `Z` to a local birth time — `Z` means UTC.\n" +
|
|
@@ -870,7 +870,7 @@ export const PROMPTS = [
|
|
|
870
870
|
"| Feature | MCP Tool | REST Endpoint | Credits |\n" +
|
|
871
871
|
"|---------|----------|---------------|---------|\n" +
|
|
872
872
|
"| Natal chart | `ephemeris_natal_chart` | `POST /ephemeris/natal-chart` | 1 |\n" +
|
|
873
|
-
"| Natal batch (up to 100) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
|
|
873
|
+
"| Natal batch (up to 100, Startup+) | `ephemeris_natal_batch` | `POST /ephemeris/natal/batch` | 1/subject |\n" +
|
|
874
874
|
"| Vedic / Jyotish chart | `vedic_chart` | `POST /vedic/chart` | 1 |\n" +
|
|
875
875
|
"| Human Design | `human_design_chart` | `POST /human-design/chart` | 2 |\n" +
|
|
876
876
|
"| BaZi Four Pillars | `chinese_bazi` | `POST /chinese/bazi` | 1 |\n" +
|
|
@@ -879,38 +879,39 @@ export const PROMPTS = [
|
|
|
879
879
|
"| Midpoint composite | `ephemeris_composite_midpoint` | `POST /comparative/composite/midpoint` | 3 |\n" +
|
|
880
880
|
"| House overlay | `ephemeris_overlay` | `POST /comparative/overlay` | 3 |\n" +
|
|
881
881
|
"| Natal transits (now) | `ephemeris_natal_transits` | `POST /comparative/natal-transits` | 3 |\n" +
|
|
882
|
-
"| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` |
|
|
882
|
+
"| Transit search (range) | `ephemeris_transits` | `POST /predictive/transits/search` | 5–70 by span (≤1y 5 … ≤40y 70); tool +1 for the natal chart |\n" +
|
|
883
883
|
"| Relocation chart | `ephemeris_relocation` | `POST /ephemeris/relocation` | 1 |\n" +
|
|
884
884
|
"| Solar return | `ephemeris_solar_return` | `POST /predictive/returns/solar` | 5 |\n" +
|
|
885
885
|
"| Lunar return | `ephemeris_lunar_return` | `POST /predictive/returns/lunar` | 5 |\n" +
|
|
886
886
|
"| Progressions | `ephemeris_progressed_chart` | `POST /ephemeris/progressed` | 1 |\n" +
|
|
887
|
-
"| ACG power lines | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
|
|
888
|
-
"| ACG city hits | `acg_hits` | `POST /acg/hits` |
|
|
887
|
+
"| ACG power lines (Pro+) | `acg_power_lines` | `POST /acg/power-lines` | 10 |\n" +
|
|
888
|
+
"| ACG city hits (Pro+) | `acg_hits` | `POST /acg/hits` | 10 |\n" +
|
|
889
889
|
"| Eclipse finder | `ephemeris_next_eclipse` | `GET /eclipse/next-visible` | 1 |\n" +
|
|
890
|
-
"| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` |
|
|
891
|
-
"| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` |
|
|
892
|
-
"| Electional windows | `ephemeris_electional` | `GET /electional/find-window` | 5 |\n" +
|
|
893
|
-
"| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` |
|
|
890
|
+
"| Moon phase (current) | `ephemeris_moon_phase` | `GET /ephemeris/moon/phase` + `/void-of-course` | 2 |\n" +
|
|
891
|
+
"| Next lunar phase | `ephemeris_next_lunar_phase` | `GET /calendar/astrology/moon-phases` | 2 per occurrence |\n" +
|
|
892
|
+
"| Electional windows (Pro+) | `ephemeris_electional` | `GET /electional/find-window` | 5 / 8 / 12 for ≤30 / 60 / 120 days |\n" +
|
|
893
|
+
"| Electional score | `electional_moment_analysis` | `GET /electional/moment-analysis` | 5 |\n" +
|
|
894
894
|
"| Dignities | `ephemeris_dignities` | `POST /ephemeris/dignities` | 1 |\n" +
|
|
895
895
|
"| Hermetic lots | `ephemeris_hermetic_lots` | `POST /ephemeris/hermetic-lots` | 1 |\n" +
|
|
896
896
|
"| Midpoints | `ephemeris_midpoints` | `POST /ephemeris/midpoints` | 1 |\n\n" +
|
|
897
897
|
"## Step 3 — Critical Implementation Patterns\n\n" +
|
|
898
898
|
"### ⚠️ Datetime Handling — Most Common Source of Bugs\n" +
|
|
899
899
|
"```\n" +
|
|
900
|
-
"# CORRECT —
|
|
900
|
+
"# CORRECT — local wall-clock time + IANA zone (server applies historical DST)\n" +
|
|
901
901
|
"POST /ephemeris/natal-chart\n" +
|
|
902
|
-
"{ \"
|
|
903
|
-
"
|
|
904
|
-
"{ \"
|
|
905
|
-
"# CORRECT — UTC required for HD and Vedic\n" +
|
|
902
|
+
"{ \"subject\": { \"name\": \"A\", \"birth_datetime\": { \"iso\": \"1990-04-15T14:30:00\" },\n" +
|
|
903
|
+
" \"birth_location\": { \"latitude\": { \"decimal\": 41.88 }, \"longitude\": { \"decimal\": -87.63 },\n" +
|
|
904
|
+
" \"timezone\": { \"iana_name\": \"America/Chicago\" } } } }\n\n" +
|
|
906
905
|
"POST /human-design/chart\n" +
|
|
907
|
-
"{ \"
|
|
908
|
-
"
|
|
909
|
-
"
|
|
906
|
+
"{ \"birth_datetime_local\": \"1990-04-15T14:30:00\", \"timezone\": { \"iana_name\": \"America/Chicago\" },\n" +
|
|
907
|
+
" \"latitude\": 41.88, \"longitude\": -87.63 }\n\n" +
|
|
908
|
+
"# ALSO CORRECT — an explicit instant (offset or Z): trusted as-is\n" +
|
|
909
|
+
"\"1990-04-15T14:30:00-05:00\" or \"1990-04-15T19:30:00Z\"\n\n" +
|
|
910
|
+
"# WRONG — Z on a local time reads as UTC 2:30pm, not Central 2:30pm (no error raised)\n" +
|
|
911
|
+
"\"1990-04-15T14:30:00Z\"\n" +
|
|
910
912
|
"```\n\n" +
|
|
911
|
-
"**Rule of thumb
|
|
912
|
-
"
|
|
913
|
-
"- Human Design, Vedic: always UTC with Z\n\n" +
|
|
913
|
+
"**Rule of thumb**: on every endpoint, prefer local time + IANA zone. A zone-less clock time " +
|
|
914
|
+
"with no zone is a hard 400; a bare date resolves to 12:00 UTC.\n\n" +
|
|
914
915
|
"### Coordinates\n" +
|
|
915
916
|
"- Decimal degrees, at least 4 decimal places\n" +
|
|
916
917
|
"- Positive = North/East; Negative = South/West\n" +
|
|
@@ -938,9 +939,10 @@ export const PROMPTS = [
|
|
|
938
939
|
"### Credit Optimization\n" +
|
|
939
940
|
"- **Batch natal charts**: use `POST /ephemeris/natal/batch` for multi-user onboarding (1 credit/subject vs N calls)\n" +
|
|
940
941
|
"- **Current transits**: prefer `ephemeris_natal_transits` (3 credits) over " +
|
|
941
|
-
"`ephemeris_transits` (6 credits) when you only need right-now, not a date range\n" +
|
|
942
|
-
"- **
|
|
943
|
-
"
|
|
942
|
+
"`ephemeris_transits` (6+ credits, priced by span) when you only need right-now, not a date range\n" +
|
|
943
|
+
"- **Range searches**: priced by the span you ask for, before compute; a span over the plan cap " +
|
|
944
|
+
"(transit search: Explorer/PAYG 1y, Pro 5y, Startup 10y, Scale 40y) is a 400 `search_span_limit` — split it\n" +
|
|
945
|
+
"- **Failed calls are free**: every 4xx/5xx response is refunded\n" +
|
|
944
946
|
"- **Format matters**: `format: 'llm'` reduces token usage in your LLM calls significantly\n\n" +
|
|
945
947
|
"## Step 4 — Generate a Code Scaffold\n" +
|
|
946
948
|
"Ask: 'What language would you like the scaffold in? And what is the first feature you want to implement?'\n\n" +
|
package/dist/server-sse.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
/**
|
|
3
|
+
* The hosted server is multi-tenant: every tool call must be authenticated
|
|
4
|
+
* and metered as the connecting user. A credential in the process environment
|
|
5
|
+
* breaks that — the client interceptor sends X-Service-Key ahead of the user's
|
|
6
|
+
* key (every user unmetered), and an env API key / JWT would bill every
|
|
7
|
+
* session to one account.
|
|
8
|
+
*
|
|
9
|
+
* - A service key in the environment is a hard startup failure: there is no
|
|
10
|
+
* safe way to run with it present.
|
|
11
|
+
* - A user API key / JWT in the environment is logged loudly and ignored.
|
|
12
|
+
* - The module singleton (built from the environment at import, and what
|
|
13
|
+
* getActiveClient() falls back to outside a session context) is stripped of
|
|
14
|
+
* every credential either way; per-session clients are built with
|
|
15
|
+
* `inheritEnvCredentials: false`.
|
|
16
|
+
*
|
|
17
|
+
* Exported for tests. stdio mode never calls this.
|
|
18
|
+
*/
|
|
19
|
+
export declare function enforceHostedCredentialPolicy(env?: NodeJS.ProcessEnv): void;
|
|
2
20
|
/**
|
|
3
21
|
* Build the full Express app (auth gate, OAuth routes, /mcp Streamable HTTP,
|
|
4
22
|
* health, resources). Exported so tests can drive the REAL handlers with
|
package/dist/server-sse.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Architecture:
|
|
9
9
|
* - The SSE server validates the user's API key at connection time by
|
|
10
10
|
* making a lightweight call to the Go backend.
|
|
11
|
-
* -
|
|
11
|
+
* - Each session gets its own BackendClient built from the validated key
|
|
12
|
+
* (never from env credentials — see enforceHostedCredentialPolicy), so
|
|
12
13
|
* all subsequent tool calls authenticate as the connecting user.
|
|
13
14
|
* - Usage credits are metered against the user's account and tier.
|
|
14
15
|
*/
|
|
@@ -26,7 +27,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
26
27
|
import { initTools, toolRegistry, formatToolResponse, formatToolError, modelVisibleTools, parseToolSurface, describeSurface } from "./tools/index.js";
|
|
27
28
|
import { buildUiMeta } from "./tools/ui-meta.js";
|
|
28
29
|
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
29
|
-
import { BackendClient, runWithClient } from "./backend/client.js";
|
|
30
|
+
import { BackendClient, backendClient, presentCredentialEnvVars, runWithClient } from "./backend/client.js";
|
|
30
31
|
import { CHART_WHEEL_RESOURCE_URI, CHART_WHEEL_MIME_TYPE, getChartWheelBundle, } from "./tools/apps/chart-wheel-app.js";
|
|
31
32
|
import { BODYGRAPH_RESOURCE_URI, BODYGRAPH_MIME_TYPE, getBodygraphBundle, } from "./tools/apps/bodygraph-app.js";
|
|
32
33
|
import { BI_WHEEL_RESOURCE_URI, BI_WHEEL_MIME_TYPE, getBiWheelBundle, } from "./tools/apps/bi-wheel-app.js";
|
|
@@ -557,12 +558,43 @@ function startSseKeepalive(res) {
|
|
|
557
558
|
}, 25_000);
|
|
558
559
|
res.on("close", () => clearInterval(ping));
|
|
559
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* The hosted server is multi-tenant: every tool call must be authenticated
|
|
563
|
+
* and metered as the connecting user. A credential in the process environment
|
|
564
|
+
* breaks that — the client interceptor sends X-Service-Key ahead of the user's
|
|
565
|
+
* key (every user unmetered), and an env API key / JWT would bill every
|
|
566
|
+
* session to one account.
|
|
567
|
+
*
|
|
568
|
+
* - A service key in the environment is a hard startup failure: there is no
|
|
569
|
+
* safe way to run with it present.
|
|
570
|
+
* - A user API key / JWT in the environment is logged loudly and ignored.
|
|
571
|
+
* - The module singleton (built from the environment at import, and what
|
|
572
|
+
* getActiveClient() falls back to outside a session context) is stripped of
|
|
573
|
+
* every credential either way; per-session clients are built with
|
|
574
|
+
* `inheritEnvCredentials: false`.
|
|
575
|
+
*
|
|
576
|
+
* Exported for tests. stdio mode never calls this.
|
|
577
|
+
*/
|
|
578
|
+
export function enforceHostedCredentialPolicy(env = process.env) {
|
|
579
|
+
const { serviceKeys, userCredentials } = presentCredentialEnvVars(env);
|
|
580
|
+
if (serviceKeys.length > 0) {
|
|
581
|
+
throw new Error(`Refusing to start the hosted MCP HTTP server: ${serviceKeys.join(", ")} is set. ` +
|
|
582
|
+
`A service key bypasses per-user metering for every session. Unset it — the ` +
|
|
583
|
+
`hosted server authenticates each session with the user's own API key or OAuth token.`);
|
|
584
|
+
}
|
|
585
|
+
if (userCredentials.length > 0) {
|
|
586
|
+
console.error(`[SECURITY] ${userCredentials.join(", ")} is set in the hosted MCP server environment. ` +
|
|
587
|
+
`IGNORING it — sessions authenticate only with their own credential. Remove it from the deployment.`);
|
|
588
|
+
}
|
|
589
|
+
backendClient.clearCredentials();
|
|
590
|
+
}
|
|
560
591
|
/**
|
|
561
592
|
* Build the full Express app (auth gate, OAuth routes, /mcp Streamable HTTP,
|
|
562
593
|
* health, resources). Exported so tests can drive the REAL handlers with
|
|
563
594
|
* supertest instead of a re-implemented stub. Does not listen.
|
|
564
595
|
*/
|
|
565
596
|
export async function createSseApp() {
|
|
597
|
+
enforceHostedCredentialPolicy();
|
|
566
598
|
await initTools();
|
|
567
599
|
const app = express();
|
|
568
600
|
// NOTE: express.json() is applied per-route (jsonParser on /mcp POST), not
|
|
@@ -700,18 +732,19 @@ export async function createSseApp() {
|
|
|
700
732
|
serverInfo: {
|
|
701
733
|
name: "Open Ephemeris",
|
|
702
734
|
version,
|
|
703
|
-
description:
|
|
735
|
+
description: `NASA JPL DE440-backed astronomical computation engine for AI agents. ${tools.length} typed tools covering ` +
|
|
704
736
|
"natal charts, transit forecasting, Human Design bodygraphs, eclipses, astrocartography " +
|
|
705
737
|
"power lines, Venus Star Points, electional timing, synastry, composite charts, Vedic/Jyotish, " +
|
|
706
738
|
"Chinese BaZi, and more — powered by JPL DE440 ephemerides for sub-arcsecond " +
|
|
707
|
-
"zero-hallucination accuracy. Free Explorer tier
|
|
739
|
+
"zero-hallucination accuracy. Free Explorer tier: 150 one-time credits.",
|
|
708
740
|
iconUrl: "https://mcp.openephemeris.com/icon.png",
|
|
709
741
|
homepage: "https://openephemeris.com",
|
|
710
742
|
},
|
|
711
743
|
authentication: {
|
|
712
744
|
required: true,
|
|
713
745
|
schemes: ["apiKey"],
|
|
714
|
-
instructions: "
|
|
746
|
+
instructions: "Sign in with OAuth 2.1 (PKCE, dynamic client registration), or pass your Open Ephemeris API key " +
|
|
747
|
+
"via the X-API-Key header (or Authorization: Bearer opene-…). Every call is metered to that user. " +
|
|
715
748
|
"Get a free Explorer key at https://openephemeris.com/dashboard — no credit card required.",
|
|
716
749
|
},
|
|
717
750
|
tools,
|
|
@@ -871,7 +904,9 @@ export async function createSseApp() {
|
|
|
871
904
|
console.error(`[HTTP] Session initialized: ${id}`);
|
|
872
905
|
},
|
|
873
906
|
});
|
|
874
|
-
|
|
907
|
+
// inheritEnvCredentials:false — this client must authenticate as THIS
|
|
908
|
+
// session's user only, never as an env service key / API key / JWT.
|
|
909
|
+
const client = new BackendClient({ baseURL: BACKEND_URL, apiKey, jwt, inheritEnvCredentials: false });
|
|
875
910
|
const analyticsId = distinctIdFor(apiKey ?? jwt);
|
|
876
911
|
// Tool surface is fixed for the life of the session: we do not declare
|
|
877
912
|
// `tools.listChanged`, so a host has no obligation to re-fetch the list.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _render-token.ts — plumbing for the API's server-issued render token.
|
|
3
|
+
*
|
|
4
|
+
* The Go API stamps every charged visual on a render-family route
|
|
5
|
+
* (/human-design/chart + /visualization/bodygraph, /human-design/transit-chart,
|
|
6
|
+
* /human-design/composite, /chinese/bazi, /vedic/chart) with a short-lived
|
|
7
|
+
* token bound to the account and the chart-defining inputs
|
|
8
|
+
* (auth/render_token.go). Presenting it on a re-render of the SAME chart —
|
|
9
|
+
* the iframe's host light/dark reconciliation, a layout flip — makes that
|
|
10
|
+
* re-render free (base charge and visual surcharge). Anything else is charged
|
|
11
|
+
* normally, so passing a stale or foreign token is harmless.
|
|
12
|
+
*
|
|
13
|
+
* Flow: explore_* puts the token on the payload as `_render_token`; the iframe
|
|
14
|
+
* hands it back as the `render_token` argument of its recalc / refetch call;
|
|
15
|
+
* the tool sends it in the X-OE-Render-Token header.
|
|
16
|
+
*/
|
|
17
|
+
export declare const RENDER_TOKEN_HEADER = "X-OE-Render-Token";
|
|
18
|
+
/** Input-schema property for the app-only recalc args. Never set by the model. */
|
|
19
|
+
export declare const RENDER_TOKEN_PROPERTY: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly description: string;
|
|
22
|
+
};
|
|
23
|
+
/** Request headers presenting `token`, or undefined when there is none. */
|
|
24
|
+
export declare function renderTokenHeaders(token: unknown): Record<string, string> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The token the API injected on the root <svg> of a /visualization/bodygraph
|
|
27
|
+
* response (a binary route, whose response headers the backend client does
|
|
28
|
+
* not surface).
|
|
29
|
+
*/
|
|
30
|
+
export declare function renderTokenFromSvg(svg: string | null | undefined): string | undefined;
|
|
31
|
+
/** `visual.render_token` from an include_visual JSON response. */
|
|
32
|
+
export declare function renderTokenFromVisual(visual: unknown): string | undefined;
|
|
33
|
+
/** Copy of tool args with the per-call token removed (for `_refetch.args`). */
|
|
34
|
+
export declare function withoutRenderToken<T extends Record<string, unknown>>(args: T): T;
|