@ronana/mcp 0.1.0 → 0.1.1

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 (2) hide show
  1. package/dist/oauth.js +64 -50
  2. package/package.json +2 -2
package/dist/oauth.js CHANGED
@@ -8,6 +8,9 @@ import { Entry } from "@napi-rs/keyring";
8
8
  const SERVICE = "Ronana MCP";
9
9
  const ACCOUNT = "oauth";
10
10
  const SCOPES = "mcp:read mcp:upload mcp:generate offline_access";
11
+ /** How long a sign-in may sit in the browser. An AI agent often runs login for
12
+ * the user, and a closed tab must end the command rather than hang it. */
13
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
11
14
  const entry = () => new Entry(SERVICE, ACCOUNT);
12
15
  export function readCredentials() {
13
16
  const value = entry().getPassword();
@@ -68,6 +71,8 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
68
71
  let settle;
69
72
  let fail;
70
73
  const code = new Promise((ok, no) => { settle = ok; fail = no; });
74
+ const timer = setTimeout(() => fail(new Error("Sign-in timed out. Run `ronana auth login` again.")), LOGIN_TIMEOUT_MS);
75
+ const close = () => { clearTimeout(timer); server.close(); };
71
76
  const server = createServer((req, res) => {
72
77
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
73
78
  if (url.searchParams.get("state") !== state) {
@@ -82,65 +87,74 @@ export async function login(baseUrl = process.env.RONANA_BASE_URL ?? "https://ro
82
87
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }).end("Ronana is connected. You can close this window.");
83
88
  settle(url.searchParams.get("code") ?? "");
84
89
  }
85
- server.close();
90
+ close();
86
91
  });
87
- server.once("error", reject);
92
+ server.once("error", (error) => { clearTimeout(timer); reject(error); });
88
93
  server.listen(0, "127.0.0.1", () => {
89
94
  const address = server.address();
90
- if (!address || typeof address === "string")
95
+ if (!address || typeof address === "string") {
96
+ close();
91
97
  return reject(new Error("Loopback callback failed."));
92
- resolve({ redirectUri: `http://127.0.0.1:${address.port}/callback`, code });
98
+ }
99
+ resolve({ redirectUri: `http://127.0.0.1:${address.port}/callback`, code, close });
93
100
  });
94
101
  });
95
- const registration = await fetch(`${baseUrl}/api/auth/oauth2/register`, {
96
- method: "POST",
97
- headers: { "Content-Type": "application/json" },
98
- body: JSON.stringify({
99
- client_name: "Ronana CLI",
100
- redirect_uris: [callback.redirectUri],
101
- token_endpoint_auth_method: "none",
102
- grant_types: ["authorization_code", "refresh_token"],
103
- response_types: ["code"],
102
+ // Every exit below (refused registration, timeout, a refused token) frees the
103
+ // loopback port and the timer, so the process can end.
104
+ try {
105
+ const registration = await fetch(`${baseUrl}/api/auth/oauth2/register`, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify({
109
+ client_name: "Ronana CLI",
110
+ redirect_uris: [callback.redirectUri],
111
+ token_endpoint_auth_method: "none",
112
+ grant_types: ["authorization_code", "refresh_token"],
113
+ response_types: ["code"],
114
+ scope: SCOPES,
115
+ }),
116
+ });
117
+ const registered = await registration.json();
118
+ if (!registration.ok || typeof registered.client_id !== "string")
119
+ throw new Error("OAuth client registration failed.");
120
+ const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
121
+ authorize.search = new URLSearchParams({
122
+ response_type: "code",
123
+ client_id: registered.client_id,
124
+ redirect_uri: callback.redirectUri,
104
125
  scope: SCOPES,
105
- }),
106
- });
107
- const registered = await registration.json();
108
- if (!registration.ok || typeof registered.client_id !== "string")
109
- throw new Error("OAuth client registration failed.");
110
- const authorize = new URL(`${baseUrl}/api/auth/oauth2/authorize`);
111
- authorize.search = new URLSearchParams({
112
- response_type: "code",
113
- client_id: registered.client_id,
114
- redirect_uri: callback.redirectUri,
115
- scope: SCOPES,
116
- resource: `${baseUrl}/mcp`,
117
- code_challenge: challenge,
118
- code_challenge_method: "S256",
119
- state,
120
- }).toString();
121
- process.stderr.write(`${openInBrowser ? "Opening your browser to sign in. If it does not open, visit" : "Open this link to sign in"}:
126
+ resource: `${baseUrl}/mcp`,
127
+ code_challenge: challenge,
128
+ code_challenge_method: "S256",
129
+ state,
130
+ }).toString();
131
+ process.stderr.write(`${openInBrowser ? "Opening your browser to sign in. If it does not open, visit" : "Open this link to sign in"}:
122
132
  ${authorize}
123
133
  `);
124
- if (openInBrowser)
125
- openBrowser(authorize.toString());
126
- const code = await callback.code;
127
- const tokens = await tokenRequest(baseUrl, {
128
- grant_type: "authorization_code",
129
- client_id: registered.client_id,
130
- redirect_uri: callback.redirectUri,
131
- code,
132
- code_verifier: verifier,
133
- resource: `${baseUrl}/mcp`,
134
- });
135
- if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string")
136
- throw new Error("OAuth response did not contain tokens.");
137
- writeCredentials({
138
- baseUrl,
139
- clientId: registered.client_id,
140
- accessToken: tokens.access_token,
141
- refreshToken: tokens.refresh_token,
142
- expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
143
- });
134
+ if (openInBrowser)
135
+ openBrowser(authorize.toString());
136
+ const code = await callback.code;
137
+ const tokens = await tokenRequest(baseUrl, {
138
+ grant_type: "authorization_code",
139
+ client_id: registered.client_id,
140
+ redirect_uri: callback.redirectUri,
141
+ code,
142
+ code_verifier: verifier,
143
+ resource: `${baseUrl}/mcp`,
144
+ });
145
+ if (typeof tokens.access_token !== "string" || typeof tokens.refresh_token !== "string")
146
+ throw new Error("OAuth response did not contain tokens.");
147
+ writeCredentials({
148
+ baseUrl,
149
+ clientId: registered.client_id,
150
+ accessToken: tokens.access_token,
151
+ refreshToken: tokens.refresh_token,
152
+ expiresAt: Date.now() + Number(tokens.expires_in ?? 900) * 1000,
153
+ });
154
+ }
155
+ finally {
156
+ callback.close();
157
+ }
144
158
  }
145
159
  const fresh = (c) => c.expiresAt > Date.now() + 60_000;
146
160
  /** Several CLI runs (and the MCP bridge) share one stored refresh token, and the
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ronana/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Ronana command line and local MCP bridge.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://ronana.ai",
7
7
  "keywords": ["ronana", "mcp", "cli", "image generation", "video generation"],
8
8
  "type": "module",
9
- "bin": { "ronana": "./dist/cli.js", "ronana-mcp": "./dist/server.js" },
9
+ "bin": { "ronana": "dist/cli.js", "ronana-mcp": "dist/server.js" },
10
10
  "files": ["dist", "README.md"],
11
11
  "publishConfig": { "access": "public" },
12
12
  "scripts": {