riskmodels 2.1.0 → 3.0.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/README.md CHANGED
@@ -27,9 +27,8 @@ node dist/index.js install --dry-run
27
27
  ## Authentication
28
28
 
29
29
  - **API key (recommended):** `riskmodels config init` (billed mode) or `riskmodels config set apiKey <rm_agent_...>`.
30
- - **OAuth client credentials:** `riskmodels config set clientId …` and `config set clientSecret …`, or set `RISKMODELS_CLIENT_ID` / `RISKMODELS_CLIENT_SECRET` (optional `RISKMODELS_OAUTH_SCOPE`; default matches the Python SDK).
31
30
  - **Environment:** `RISKMODELS_API_KEY` works without a config file for REST commands.
32
- - **Direct (Supabase) mode** is for `query` + `schema` only. REST analytics need an API key or OAuth (config or env).
31
+ - **Direct (Supabase) mode** is for `query` + `schema` only. REST analytics need an API key (config or env).
33
32
 
34
33
  Base URL: stored as `apiBaseUrl` (default `https://riskmodels.app`). The CLI calls paths under `…/api/...` (same as `OPENAPI_SPEC.yaml`).
35
34
 
@@ -51,7 +50,7 @@ Config file: `~/.config/riskmodels/config.json`
51
50
 
52
51
  | Command | Description |
53
52
  |--------|-------------|
54
- | `riskmodels config init \| set \| list` | API key, OAuth fields (`clientId`, `clientSecret`, `oauthScope`), `apiBaseUrl`, or Supabase (direct) |
53
+ | `riskmodels config init \| set \| list` | API key, `apiBaseUrl`, or Supabase (direct) |
55
54
  | `riskmodels install` | Detect Claude, Cursor, Codex, and VS Code MCP targets, store the API key in shared config, merge MCP configs with backups, and run a connection test. |
56
55
  | `riskmodels status` | Show shared config and MCP client detection status. |
57
56
  | `riskmodels doctor` | Local install-readiness diagnostics: Node, npx, API credentials, and client detection. |
@@ -76,15 +76,12 @@ export function configCommand() {
76
76
  cmd
77
77
  .command("set")
78
78
  .description("Set a config value")
79
- .argument("<key>", "apiKey | apiBaseUrl | clientId | clientSecret | oauthScope | supabaseUrl | serviceRoleKey")
79
+ .argument("<key>", "apiKey | apiBaseUrl | supabaseUrl | serviceRoleKey")
80
80
  .argument("<value>", "value to store")
81
81
  .action(async (key, value) => {
82
82
  const allowed = new Set([
83
83
  "apiKey",
84
84
  "apiBaseUrl",
85
- "clientId",
86
- "clientSecret",
87
- "oauthScope",
88
85
  "supabaseUrl",
89
86
  "serviceRoleKey",
90
87
  ]);
@@ -101,18 +98,6 @@ export function configCommand() {
101
98
  else if (key === "apiBaseUrl") {
102
99
  cfg.apiBaseUrl = value.replace(/\/$/, "");
103
100
  }
104
- else if (key === "clientId") {
105
- cfg.mode = "billed";
106
- cfg.clientId = value;
107
- }
108
- else if (key === "clientSecret") {
109
- cfg.mode = "billed";
110
- cfg.clientSecret = value;
111
- }
112
- else if (key === "oauthScope") {
113
- cfg.mode = "billed";
114
- cfg.oauthScope = value;
115
- }
116
101
  else if (key === "supabaseUrl") {
117
102
  cfg.mode = "direct";
118
103
  cfg.supabaseUrl = value.replace(/\/$/, "");
@@ -140,9 +125,6 @@ export function configCommand() {
140
125
  mode: cfg.mode,
141
126
  apiBaseUrl: cfg.apiBaseUrl ?? DEFAULT_API_BASE,
142
127
  apiKey: maskSecret(cfg.apiKey),
143
- clientId: maskSecret(cfg.clientId),
144
- clientSecret: maskSecret(cfg.clientSecret),
145
- oauthScope: cfg.oauthScope ?? "(not set)",
146
128
  supabaseUrl: cfg.supabaseUrl ?? "(not set)",
147
129
  serviceRoleKey: maskSecret(cfg.serviceRoleKey),
148
130
  };
@@ -31,7 +31,7 @@ export function doctorCommand() {
31
31
  },
32
32
  {
33
33
  id: "api_credentials",
34
- ok: !!cfg?.apiKey || !!process.env.RISKMODELS_API_KEY || (!!cfg?.clientId && !!cfg?.clientSecret),
34
+ ok: !!cfg?.apiKey || !!process.env.RISKMODELS_API_KEY,
35
35
  detail: cfg?.apiKey
36
36
  ? `Config API key ${maskSecret(cfg.apiKey)} in ${abbreviatePath(configPath())}`
37
37
  : process.env.RISKMODELS_API_KEY
@@ -20,7 +20,6 @@ export function statusCommand() {
20
20
  configFound: !!cfg,
21
21
  apiBaseUrl: cfg?.apiBaseUrl ?? "https://riskmodels.app",
22
22
  apiKey: maskSecret(cfg?.apiKey),
23
- oauthConfigured: !!cfg?.clientId && !!cfg?.clientSecret,
24
23
  mcpClients: detections,
25
24
  };
26
25
  if (json) {
@@ -1,4 +1,4 @@
1
- import { getAuthorizationHeader, invalidateAuthForRetry } from "./credentials.js";
1
+ import { getAuthorizationHeader } from "./credentials.js";
2
2
  /** Thrown on non-2xx API responses so callers can inspect `status` (e.g. retries on 503). */
3
3
  export class ApiHttpError extends Error {
4
4
  status;
@@ -22,38 +22,33 @@ function errorMessageFromBody(body) {
22
22
  }
23
23
  export async function apiFetchJson(auth, method, path, options) {
24
24
  const url = buildUrl(auth.apiRoot, path, options?.query);
25
- for (let attempt = 1; attempt <= 2; attempt += 1) {
26
- const headers = await getAuthorizationHeader(auth);
27
- const init = {
28
- method,
29
- headers: {
30
- ...headers,
31
- Accept: "application/json",
32
- ...(options?.jsonBody !== undefined ? { "Content-Type": "application/json" } : {}),
33
- },
34
- body: options?.jsonBody !== undefined ? JSON.stringify(options.jsonBody) : undefined,
35
- };
36
- const res = await fetch(url, init);
37
- if (res.status === 401 && attempt === 1 && auth.oauth) {
38
- invalidateAuthForRetry(auth);
39
- continue;
40
- }
41
- const costUsd = res.headers.get("x-api-cost-usd") ?? undefined;
42
- const text = await res.text();
43
- let body;
44
- try {
45
- body = text ? JSON.parse(text) : null;
46
- }
47
- catch {
48
- body = { raw: text };
49
- }
50
- if (!res.ok) {
51
- const msg = errorMessageFromBody(body) || `HTTP ${res.status}`;
52
- throw new ApiHttpError(res.status, msg);
53
- }
54
- return { body, costUsd, status: res.status, headers: res.headers };
25
+ // Single attempt: the retry existed only to refresh an expiring OAuth access
26
+ // token, and auth is now a static API key with nothing to refresh.
27
+ const headers = await getAuthorizationHeader(auth);
28
+ const init = {
29
+ method,
30
+ headers: {
31
+ ...headers,
32
+ Accept: "application/json",
33
+ ...(options?.jsonBody !== undefined ? { "Content-Type": "application/json" } : {}),
34
+ },
35
+ body: options?.jsonBody !== undefined ? JSON.stringify(options.jsonBody) : undefined,
36
+ };
37
+ const res = await fetch(url, init);
38
+ const costUsd = res.headers.get("x-api-cost-usd") ?? undefined;
39
+ const text = await res.text();
40
+ let body;
41
+ try {
42
+ body = text ? JSON.parse(text) : null;
55
43
  }
56
- throw new Error("Unreachable");
44
+ catch {
45
+ body = { raw: text };
46
+ }
47
+ if (!res.ok) {
48
+ const msg = errorMessageFromBody(body) || `HTTP ${res.status}`;
49
+ throw new ApiHttpError(res.status, msg);
50
+ }
51
+ return { body, costUsd, status: res.status, headers: res.headers };
57
52
  }
58
53
  export async function apiFetchOptionalAuth(apiRoot, method, path, options) {
59
54
  const url = buildUrl(apiRoot, path, options?.query);
@@ -15,7 +15,6 @@ export type StatusPayload = {
15
15
  configFound: boolean;
16
16
  apiBaseUrl: string;
17
17
  apiKey: string;
18
- oauthConfigured: boolean;
19
18
  mcpClients: ClientDetection[];
20
19
  };
21
20
  export declare function printStatusHuman(payload: StatusPayload): void;
@@ -38,7 +38,6 @@ export function printStatusHuman(payload) {
38
38
  logLine(` ${BULLET} ${payload.configFound ? chalk.green("file present") : chalk.dim("file missing (run install or config init)")}`);
39
39
  logLine(` ${BULLET} ${chalk.bold("apiBaseUrl")} ${chalk.dim(payload.apiBaseUrl)}`);
40
40
  logLine(` ${BULLET} ${chalk.bold("API key")} ${payload.apiKey}`);
41
- logLine(` ${BULLET} ${chalk.bold("OAuth")} ${payload.oauthConfigured ? chalk.green("configured") : chalk.dim("not configured")}`);
42
41
  logLine("");
43
42
  logLine(chalk.bold("MCP-related clients:"));
44
43
  for (const d of payload.mcpClients) {
@@ -5,9 +5,6 @@ export interface RiskmodelsConfig {
5
5
  /** Base URL without trailing slash, e.g. https://riskmodels.app */
6
6
  apiBaseUrl?: string;
7
7
  /** OAuth client credentials (billed mode); scope defaults match the Python SDK. */
8
- clientId?: string;
9
- clientSecret?: string;
10
- oauthScope?: string;
11
8
  supabaseUrl?: string;
12
9
  serviceRoleKey?: string;
13
10
  }
@@ -42,7 +42,7 @@ export function maskSecret(value, visible = 6) {
42
42
  export function isBilledReady(cfg) {
43
43
  return (!!cfg &&
44
44
  cfg.mode === "billed" &&
45
- (!!cfg.apiKey?.trim() || (!!cfg.clientId?.trim() && !!cfg.clientSecret?.trim())));
45
+ !!cfg.apiKey?.trim());
46
46
  }
47
47
  export function isDirectReady(cfg) {
48
48
  return (!!cfg &&
@@ -1,15 +1,18 @@
1
1
  import type { RiskmodelsConfig } from "./config.js";
2
- /** Matches sdk/riskmodels/client.py DEFAULT_SCOPE. */
3
- export declare const DEFAULT_OAUTH_SCOPE = "ticker-returns risk-decomposition batch-analysis factor-correlation macro-factor-series rankings";
2
+ /**
3
+ * Auth is a static Bearer API key. There is no token exchange.
4
+ *
5
+ * Removed in 3.0.0: the OAuth client-credentials path. It POSTed a
6
+ * `client_credentials` grant to `{apiRoot}/auth/token`, an endpoint that was
7
+ * documented but never implemented — it returns 404, so any invocation
8
+ * configured that way failed on its first request. The API's only OAuth flow is
9
+ * authorization-code + PKCE for MCP clients, which issues an `rm_user_*` key
10
+ * that is used here as a plain API key.
11
+ */
4
12
  export type ResolvedApiAuth = {
5
13
  apiRoot: string;
6
- /** Static Bearer (API key) — preferred when set */
7
- apiKey?: string;
8
- oauth?: {
9
- clientId: string;
10
- clientSecret: string;
11
- scope: string;
12
- };
14
+ /** Static Bearer (`rm_agent_*` or `rm_user_*`). */
15
+ apiKey: string;
13
16
  };
14
17
  export declare function resolveApiAuth(cfg: RiskmodelsConfig | null): ResolvedApiAuth | null;
15
18
  /** True when user has API key or OAuth credentials (config and/or env). */
@@ -21,6 +24,5 @@ export declare function assertRestApiAuth(cfg: RiskmodelsConfig | null, chalkYel
21
24
  /** Returns auth or sets process.exitCode and prints via assertRestApiAuth. */
22
25
  export declare function requireResolvedAuth(cfg: RiskmodelsConfig | null, chalkYellow: (s: string) => string): ResolvedApiAuth | null;
23
26
  export declare function getAuthorizationHeader(auth: ResolvedApiAuth): Promise<Record<string, string>>;
24
- export declare function invalidateAuthForRetry(auth: ResolvedApiAuth): void;
25
27
  /** Public origin for docs (no `/api` suffix). */
26
28
  export declare function displayApiOrigin(cfg: RiskmodelsConfig | null): string;
@@ -1,8 +1,5 @@
1
1
  import { apiRootFromUserBase } from "./api-url.js";
2
2
  import { DEFAULT_API_BASE } from "./config.js";
3
- import { fetchOAuthAccessToken, invalidateOAuthToken } from "./oauth.js";
4
- /** Matches sdk/riskmodels/client.py DEFAULT_SCOPE. */
5
- export const DEFAULT_OAUTH_SCOPE = "ticker-returns risk-decomposition batch-analysis factor-correlation macro-factor-series rankings";
6
3
  function trimEnv(key) {
7
4
  const v = process.env[key];
8
5
  return v?.trim() || undefined;
@@ -13,12 +10,6 @@ export function resolveApiAuth(cfg) {
13
10
  if (apiKey) {
14
11
  return { apiRoot, apiKey };
15
12
  }
16
- const clientId = cfg?.clientId?.trim() || trimEnv("RISKMODELS_CLIENT_ID");
17
- const clientSecret = cfg?.clientSecret?.trim() || trimEnv("RISKMODELS_CLIENT_SECRET");
18
- const scope = cfg?.oauthScope?.trim() || trimEnv("RISKMODELS_OAUTH_SCOPE") || DEFAULT_OAUTH_SCOPE;
19
- if (clientId && clientSecret) {
20
- return { apiRoot, oauth: { clientId, clientSecret, scope } };
21
- }
22
13
  return null;
23
14
  }
24
15
  /** True when user has API key or OAuth credentials (config and/or env). */
@@ -33,7 +24,7 @@ export function assertRestApiAuth(cfg, chalkYellow) {
33
24
  return;
34
25
  if (cfg?.mode === "direct") {
35
26
  console.error(chalkYellow("REST API commands need an API key or OAuth client credentials. " +
36
- "Set RISKMODELS_API_KEY (or RISKMODELS_CLIENT_ID + RISKMODELS_CLIENT_SECRET), " +
27
+ "Set RISKMODELS_API_KEY get a key at https://riskmodels.app/get-key — " +
37
28
  "or run `riskmodels config init` in billed mode."));
38
29
  process.exitCode = 1;
39
30
  return;
@@ -50,19 +41,7 @@ export function requireResolvedAuth(cfg, chalkYellow) {
50
41
  return null;
51
42
  }
52
43
  export async function getAuthorizationHeader(auth) {
53
- if (auth.apiKey) {
54
- return { Authorization: `Bearer ${auth.apiKey}` };
55
- }
56
- if (auth.oauth) {
57
- const token = await fetchOAuthAccessToken(auth.apiRoot, auth.oauth.clientId, auth.oauth.clientSecret, auth.oauth.scope);
58
- return { Authorization: `Bearer ${token}` };
59
- }
60
- throw new Error("No API credentials");
61
- }
62
- export function invalidateAuthForRetry(auth) {
63
- if (auth.oauth) {
64
- invalidateOAuthToken(auth.apiRoot, auth.oauth.clientId, auth.oauth.scope);
65
- }
44
+ return { Authorization: `Bearer ${auth.apiKey}` };
66
45
  }
67
46
  /** Public origin for docs (no `/api` suffix). */
68
47
  export function displayApiOrigin(cfg) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "riskmodels",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "RiskModels CLI — REST API, SQL query, schema, billing, and agent manifests",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc",
14
+ "build": "rm -rf dist && tsc",
15
15
  "prepublishOnly": "npm run build",
16
16
  "install:global": "npm run build && npm link"
17
17
  },
@@ -1,2 +0,0 @@
1
- export declare function invalidateOAuthToken(apiRoot: string, clientId: string, scope: string): void;
2
- export declare function fetchOAuthAccessToken(apiRoot: string, clientId: string, clientSecret: string, scope: string): Promise<string>;
package/dist/lib/oauth.js DELETED
@@ -1,47 +0,0 @@
1
- const skewSeconds = 60;
2
- const cache = new Map();
3
- function cacheKey(apiRoot, clientId, scope) {
4
- return `${apiRoot}\0${clientId}\0${scope}`;
5
- }
6
- export function invalidateOAuthToken(apiRoot, clientId, scope) {
7
- cache.delete(cacheKey(apiRoot, clientId, scope));
8
- }
9
- export async function fetchOAuthAccessToken(apiRoot, clientId, clientSecret, scope) {
10
- const key = cacheKey(apiRoot, clientId, scope);
11
- const now = performance.now();
12
- const hit = cache.get(key);
13
- if (hit && now < hit.expiresAtMono - skewSeconds * 1000) {
14
- return hit.token;
15
- }
16
- const url = `${apiRoot.replace(/\/$/, "")}/auth/token`;
17
- const res = await fetch(url, {
18
- method: "POST",
19
- headers: { "Content-Type": "application/json" },
20
- body: JSON.stringify({
21
- grant_type: "client_credentials",
22
- client_id: clientId,
23
- client_secret: clientSecret,
24
- scope,
25
- }),
26
- });
27
- const text = await res.text();
28
- let data;
29
- try {
30
- data = JSON.parse(text);
31
- }
32
- catch {
33
- throw new Error(`OAuth token: HTTP ${res.status}: ${text.slice(0, 200)}`);
34
- }
35
- if (!res.ok) {
36
- throw new Error(data?.error ?? `OAuth token: HTTP ${res.status}: ${text.slice(0, 200)}`);
37
- }
38
- if (!data.access_token) {
39
- throw new Error("OAuth token response missing access_token");
40
- }
41
- const expiresIn = Math.max(30, Number(data.expires_in ?? 900));
42
- cache.set(key, {
43
- token: data.access_token,
44
- expiresAtMono: now + expiresIn * 1000,
45
- });
46
- return data.access_token;
47
- }