ogment 0.2.0 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAsBH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAiCD,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAI1D;AASD,wBAAgB,iBAAiB,SAIhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,sBAMjC;AAoED,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,iBAoB7F;AAgID;;;;;;;;GAQG;AACH,wBAAsB,KAAK,+BA8D1B"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAuBH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAiCD,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAgB1D;AAeD,wBAAgB,iBAAiB,SAIhC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,sBAMjC;AAoED,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,iBAoB7F;AA6ID;;;;;;;;GAQG;AACH,wBAAsB,KAAK,+BA6D1B"}
package/dist/auth.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * 7. Store credentials locally
12
12
  */
13
13
  import { createHash, randomBytes } from 'node:crypto';
14
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
14
+ import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
15
15
  import { createServer } from 'node:http';
16
16
  import open from 'open';
17
17
  import { CLI_CLIENT_NAME, CLI_REDIRECT_HOST, CONFIG_DIR, CREDENTIALS_PATH, OAUTH_AUTHORIZE_URL, OAUTH_REGISTER_URL, OAUTH_REVOKE_URL, OAUTH_TOKEN_URL, } from './config.js';
@@ -31,10 +31,22 @@ export function loadCredentials() {
31
31
  if (!existsSync(CREDENTIALS_PATH))
32
32
  return null;
33
33
  const raw = readFileSync(CREDENTIALS_PATH, 'utf-8');
34
- return JSON.parse(raw);
34
+ const parsed = JSON.parse(raw);
35
+ // Basic shape validation — corrupted files are treated as "not logged in"
36
+ if (typeof parsed.accessToken !== 'string' || !parsed.accessToken ||
37
+ typeof parsed.clientId !== 'string' || !parsed.clientId ||
38
+ !Array.isArray(parsed.orgs)) {
39
+ unlinkSync(CREDENTIALS_PATH);
40
+ return null;
41
+ }
42
+ return parsed;
35
43
  }
36
44
  function saveCredentials(creds) {
37
45
  mkdirSync(CONFIG_DIR, { recursive: true });
46
+ // Prevent symlink attacks — don't follow symlinks when writing credentials
47
+ if (existsSync(CREDENTIALS_PATH) && lstatSync(CREDENTIALS_PATH).isSymbolicLink()) {
48
+ unlinkSync(CREDENTIALS_PATH);
49
+ }
38
50
  writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), {
39
51
  mode: 0o600, // owner read/write only
40
52
  });
@@ -128,9 +140,26 @@ export async function revokeToken(token, clientId, clientSecret) {
128
140
  * Starts a temporary HTTP server on a random port, waits for the OAuth
129
141
  * callback, and returns the authorization code.
130
142
  */
131
- function waitForAuthCallback(port) {
143
+ /**
144
+ * Start a callback server on an OS-assigned port.
145
+ * Port 0 = OS assigns a free port atomically (no TOCTOU race).
146
+ */
147
+ function startCallbackServerWithPort() {
148
+ return new Promise((resolve) => {
149
+ const server = createServer();
150
+ server.listen(0, CLI_REDIRECT_HOST, () => {
151
+ const addr = server.address();
152
+ resolve({ server, port: addr.port });
153
+ });
154
+ });
155
+ }
156
+ /**
157
+ * Wait for the OAuth callback on an already-listening server.
158
+ * Returns the authorization code and state.
159
+ */
160
+ function waitForCallback(server, port) {
132
161
  return new Promise((resolve, reject) => {
133
- const server = createServer((req, res) => {
162
+ server.on('request', (req, res) => {
134
163
  const url = new URL(req.url ?? '/', `http://${CLI_REDIRECT_HOST}:${port}`);
135
164
  if (url.pathname !== '/callback') {
136
165
  res.writeHead(404);
@@ -159,9 +188,6 @@ function waitForAuthCallback(port) {
159
188
  server.close();
160
189
  resolve({ code, state });
161
190
  });
162
- server.listen(port, CLI_REDIRECT_HOST, () => {
163
- // Server is ready
164
- });
165
191
  // Timeout after 5 minutes (unref so it doesn't block process exit)
166
192
  const timeout = setTimeout(() => {
167
193
  server.close();
@@ -170,14 +196,14 @@ function waitForAuthCallback(port) {
170
196
  timeout.unref();
171
197
  });
172
198
  }
173
- function getRandomPort() {
174
- // Use port 0 to let OS assign, but we need to know the port before starting.
175
- // Instead, pick from ephemeral range.
176
- return 49152 + Math.floor(Math.random() * 16384);
177
- }
178
199
  // ---------------------------------------------------------------------------
179
- // HTML pages for the callback
200
+ // HTML helpers
180
201
  // ---------------------------------------------------------------------------
202
+ function escapeHtml(str) {
203
+ if (!str)
204
+ return '';
205
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
206
+ }
181
207
  function successPage() {
182
208
  return `<!DOCTYPE html>
183
209
  <html lang="en">
@@ -224,8 +250,8 @@ function errorPage(error, description) {
224
250
  <div class="card">
225
251
  <div class="icon">&#x26A0;&#xFE0F;</div>
226
252
  <h1>Login failed</h1>
227
- <p>${description ?? 'Something went wrong during authentication.'}</p>
228
- <p class="error">${error}</p>
253
+ <p>${escapeHtml(description) || 'Something went wrong during authentication.'}</p>
254
+ <p class="error">${escapeHtml(error)}</p>
229
255
  </div>
230
256
  </body>
231
257
  </html>`;
@@ -247,14 +273,15 @@ export async function login() {
247
273
  if (existing?.accessToken) {
248
274
  return existing;
249
275
  }
250
- const port = getRandomPort();
276
+ // Step 1 — Start callback server (OS assigns port atomically — no race)
277
+ const { server: callbackServer, port } = await startCallbackServerWithPort();
251
278
  const redirectUri = `http://${CLI_REDIRECT_HOST}:${port}/callback`;
252
- // Step 1 — Register as OAuth client
279
+ // Step 2 — Register as OAuth client (needs redirect URI with real port)
253
280
  const client = await registerClient(redirectUri);
254
- // Step 2 — Generate PKCE pair
281
+ // Step 3 — Generate PKCE pair
255
282
  const codeVerifier = generateCodeVerifier();
256
283
  const codeChallenge = generateCodeChallenge(codeVerifier);
257
- // Step 3 — Build authorization URL (no resource — server-less login)
284
+ // Step 4 — Build authorization URL (no resource — server-less login)
258
285
  const state = randomBytes(16).toString('hex');
259
286
  const authorizeUrl = new URL(OAUTH_AUTHORIZE_URL);
260
287
  authorizeUrl.searchParams.set('client_id', client.client_id);
@@ -263,11 +290,10 @@ export async function login() {
263
290
  authorizeUrl.searchParams.set('code_challenge', codeChallenge);
264
291
  authorizeUrl.searchParams.set('code_challenge_method', 'S256');
265
292
  authorizeUrl.searchParams.set('state', state);
266
- // Step 4Start callback server, then open browser
267
- const callbackPromise = waitForAuthCallback(port);
293
+ // Step 5Open browser, wait for callback
294
+ const codePromise = waitForCallback(callbackServer, port);
268
295
  await open(authorizeUrl.toString());
269
- // Step 5 Wait for callback
270
- const { code, state: returnedState } = await callbackPromise;
296
+ const { code, state: returnedState } = await codePromise;
271
297
  if (returnedState !== state) {
272
298
  throw new Error('OAuth state mismatch — possible CSRF attack.');
273
299
  }
@@ -1 +1 @@
1
- {"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/commands/call.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,iBA6B7B"}
1
+ {"version":3,"file":"call.d.ts","sourceRoot":"","sources":["../../src/commands/call.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,iBAoC7B"}
@@ -20,7 +20,15 @@ export async function callCommand(serverPath, toolName, argsJson) {
20
20
  const available = allServers.map((s) => s.path).join(', ');
21
21
  throw new Error(`Server "${serverPath}" not found. Available: ${available || 'none'}`);
22
22
  }
23
- const args = argsJson ? JSON.parse(argsJson) : {};
23
+ let args = {};
24
+ if (argsJson) {
25
+ try {
26
+ args = JSON.parse(argsJson);
27
+ }
28
+ catch {
29
+ throw new Error(`Invalid JSON arguments: ${argsJson}`);
30
+ }
31
+ }
24
32
  const result = await callTool(server.orgSlug, serverPath, creds.accessToken, toolName, args);
25
33
  console.log(JSON.stringify(result, null, 2));
26
34
  }
package/dist/config.d.ts CHANGED
@@ -19,5 +19,5 @@ export declare const CONFIG_DIR: string;
19
19
  export declare const CREDENTIALS_PATH: string;
20
20
  export declare const CLI_CLIENT_NAME = "Ogment CLI";
21
21
  export declare const CLI_REDIRECT_HOST = "127.0.0.1";
22
- export declare const VERSION = "0.1.0";
22
+ export declare const VERSION = "0.2.1";
23
23
  //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -33,4 +33,4 @@ export const CLI_REDIRECT_HOST = '127.0.0.1';
33
33
  // ---------------------------------------------------------------------------
34
34
  // Version
35
35
  // ---------------------------------------------------------------------------
36
- export const VERSION = '0.1.0';
36
+ export const VERSION = '0.2.1';
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-client.d.ts","sourceRoot":"","sources":["../src/mcp-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAiDD,qDAAqD;AACrD,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,sBAKlF;AAED,qDAAqD;AACrD,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,8BAM9B"}
1
+ {"version":3,"file":"mcp-client.d.ts","sourceRoot":"","sources":["../src/mcp-client.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAQH,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,UAAU,iBAAiB;IACzB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC1C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAmDD,qDAAqD;AACrD,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,sBAKlF;AAED,qDAAqD;AACrD,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,8BAM9B"}
@@ -26,7 +26,9 @@ async function mcpRequest(orgSlug, serverPath, token, method, params) {
26
26
  });
27
27
  if (!res.ok) {
28
28
  const text = await res.text();
29
- throw new Error(`MCP request failed (${res.status}): ${text}`);
29
+ // Truncate error bodies to avoid leaking server internals into agent logs
30
+ const sanitized = text.length > 200 ? text.slice(0, 200) + '…' : text;
31
+ throw new Error(`MCP request failed (${res.status}): ${sanitized}`);
30
32
  }
31
33
  const json = (await res.json());
32
34
  if ('error' in json && json.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ogment",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Ogment Vault CLI — secure your AI agents' SaaS credentials",
5
5
  "type": "module",
6
6
  "bin": {