@biomate/connect 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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > One-line installer that wires BioMate AI into Claude Code, Claude Desktop, Cursor, OpenAI Codex, ChatGPT, and Open Claw (WeChat).
4
4
 
5
- Real bioinformatics — `WGS variant-calling pipeline`, CryoSPARC, AlphaFold, ADMET screens, PBPK — runs from your favorite AI coding assistant. No SaaS lock-in: tokens are stored locally and revocable per-surface from your BioMate account.
5
+ Real bioinformatics — `nf-core/sarek`, CryoSPARC, AlphaFold, ADMET screens, PBPK — runs from your favorite AI coding assistant. No SaaS lock-in: tokens are stored locally in a protected credential file.
6
6
 
7
7
  ## Install
8
8
 
@@ -17,7 +17,7 @@ npx @biomate/connect cursor
17
17
  npx @biomate/connect codex
18
18
  ```
19
19
 
20
- That's it. The CLI opens your browser, runs OAuth 2.1 + PKCE against `https://biomate.ai`, writes the MCP server entry into the right host config file, and stashes a refresh token in your OS keychain.
20
+ The CLI opens your browser for OAuth with PKCE and writes the MCP configuration. Version 0.1.1 stores rotating tokens in a mode-0600 file under `~/.config/biomate/`; host configurations contain only its path. This release defaults explicitly to the verified development deployment at `https://dev-public.biomate.ai`; use `--auth-base` to select a different deployed server. Existing OAuth installations must reconnect with the updated installer.
21
21
 
22
22
  ## Supported surfaces
23
23
 
@@ -32,12 +32,12 @@ That's it. The CLI opens your browser, runs OAuth 2.1 + PKCE against `https://bi
32
32
 
33
33
  ## What you get
34
34
 
35
- After connecting, your AI assistant has access to BioMate's 14-tool MCP surface:
35
+ After connecting, your AI assistant has access to BioMate's 19-tool MCP surface:
36
36
 
37
- - **`biomate_session`** — natural-language goal → streaming workflow run with phase/step/QC/findings events
38
- - **`search_workflow`** + **`get_workflow_spec`** — discover from 2,455 indexed workflows
39
- - **`run_workflow`** + **`get_run`** + **`cancel_run`** — fine control
40
- - **`preview_file`**, **`export_report`**, **`recall_memory`**, **`upload_file`**, **`analyze_results`**, **`explain_error`**, **`query_database`**
37
+ - **`biomate_session`** — natural-language goal → analysis plan, workflow suggestions and clarification requests; review the plan before execution
38
+ - **`search_workflow`** + **`get_workflow_spec`** — discover workflows in the catalog
39
+ - **`run_workflow`** + **`get_run`** + **`watch_run`** + **`cancel_run`** — launch, cancel and poll run status (snapshot updates)
40
+ - **`preview_file`**, **`export_report`**, **`analyze_results`**, **`explain_error`**, **`query_database`**, **`resolve_accession`**, **`browse_data`**, **`fetch_public_data`**, **`search_literature`**, **`recall_memory`**, **`upload_file`**
41
41
 
42
42
  ## Try it
43
43
 
@@ -59,9 +59,9 @@ Look up UniProt P04637.
59
59
 
60
60
  ## Privacy & security
61
61
 
62
- - OAuth 2.1 + PKCE; tokens scoped per surface, revocable individually at
63
- https://biomate.ai/account/connectors
64
- - Refresh tokens hashed at rest with HMAC-SHA256
62
+ - OAuth with PKCE; access tokens carry permission scopes enforced by
63
+ the OAuth service; access is also blocked when the associated personal API key is revoked
64
+ - Server-side refresh-token records are hashed; local credential files contain secrets and must stay private
65
65
  - Access tokens are 30-minute JWTs; refresh tokens rotate on use (OAuth 2.1 §6.1)
66
66
  - No telemetry beyond standard API logs
67
67
 
@@ -76,3 +76,7 @@ npm test
76
76
  ## License
77
77
 
78
78
  MIT
79
+
80
+ ## Supported exports
81
+
82
+ This package exports Markdown findings reports. PDF, DOCX, other report sections, instrument tools and MCP progress notifications are not advertised by the 0.1.1 stdio package. ChatGPT Actions and hosted WeChat setup are separate integrations and are not validated by the desktop installer tests.
package/dist/cli.js CHANGED
@@ -1,42 +1,7 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- Object.defineProperty(exports, "__esModule", { value: true });
37
- const oauth_js_1 = require("./oauth.js");
38
- const surfaces_js_1 = require("./surfaces.js");
39
- const AUTH_BASE = process.env.BIOMATE_AUTH_BASE ?? 'https://api.biomate.ai';
2
+ import { runPKCEFlow } from './oauth.js';
3
+ import { SURFACES } from './surfaces.js';
4
+ const AUTH_BASE = process.env.BIOMATE_AUTH_BASE ?? 'https://dev-public.biomate.ai';
40
5
  const USAGE = `\nUsage: npx @biomate/connect <surface> [--auth-base URL]\n\nSurfaces:\n claude-code Anthropic Claude Code (CLI)\n claude-desktop Anthropic Claude Desktop\n cursor Cursor IDE\n codex OpenAI Codex CLI\n chatgpt ChatGPT (Custom GPT Actions)\n open-claw WeChat / Open Claw\n\nExamples:\n npx @biomate/connect claude-code\n npx @biomate/connect cursor\n\nEnvironment:\n BIOMATE_AUTH_BASE Auth server URL (default ${AUTH_BASE})\n BIOMATE_API_BASE API base URL for installed configs\n`;
41
6
  async function main() {
42
7
  const args = process.argv.slice(2);
@@ -45,22 +10,21 @@ async function main() {
45
10
  return 0;
46
11
  }
47
12
  const id = args[0];
48
- const surface = surfaces_js_1.SURFACES[id];
13
+ const surface = SURFACES[id];
49
14
  if (!surface) {
50
15
  console.error(`Unknown surface: ${id}\n${USAGE}`);
51
16
  return 1;
52
17
  }
53
18
  try {
54
- const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, AUTH_BASE);
19
+ const flag = args.indexOf('--auth-base');
20
+ if (flag !== -1 && !args[flag + 1])
21
+ throw new Error('--auth-base requires a URL');
22
+ const authBase = flag === -1 ? AUTH_BASE : args[flag + 1];
23
+ const origin = new URL(authBase);
24
+ if (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(origin.hostname)))
25
+ throw new Error('OAuth requires HTTPS');
26
+ const tokens = await runPKCEFlow(surface, origin.origin);
55
27
  const where = await surface.install(tokens.access_token, tokens.refresh_token);
56
- // Stash the refresh token in the OS keychain too (best-effort).
57
- try {
58
- const keytar = await Promise.resolve().then(() => __importStar(require('keytar')));
59
- await keytar.default.setPassword('biomate', surface.id, tokens.refresh_token);
60
- }
61
- catch {
62
- /* keychain optional */
63
- }
64
28
  console.log(`\n✓ ${surface.label} connected.`);
65
29
  if (where && !where.startsWith('(')) {
66
30
  console.log(` Config written: ${where}`);
@@ -1,47 +1,10 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.upsertJSON = upsertJSON;
37
- exports.upsertTOML = upsertTOML;
38
- const fs_1 = require("fs");
39
- const path = __importStar(require("path"));
40
- async function upsertJSON(filePath, mutate) {
41
- await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ export async function upsertJSON(filePath, mutate) {
4
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
42
5
  let existing = {};
43
6
  try {
44
- const raw = await fs_1.promises.readFile(filePath, 'utf8');
7
+ const raw = await fs.readFile(filePath, 'utf8');
45
8
  existing = JSON.parse(raw);
46
9
  }
47
10
  catch (e) {
@@ -51,11 +14,11 @@ async function upsertJSON(filePath, mutate) {
51
14
  const next = mutate(existing) ?? existing;
52
15
  await atomicWrite(filePath, JSON.stringify(next, null, 2));
53
16
  }
54
- async function upsertTOML(filePath, append) {
55
- await fs_1.promises.mkdir(path.dirname(filePath), { recursive: true });
17
+ export async function upsertTOML(filePath, append) {
18
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
56
19
  let existing = '';
57
20
  try {
58
- existing = await fs_1.promises.readFile(filePath, 'utf8');
21
+ existing = await fs.readFile(filePath, 'utf8');
59
22
  }
60
23
  catch (e) {
61
24
  if (e.code !== 'ENOENT')
@@ -63,12 +26,12 @@ async function upsertTOML(filePath, append) {
63
26
  }
64
27
  if (existing.includes('[mcp_servers.biomate]')) {
65
28
  // Naive replacement: drop the existing block and re-append.
66
- existing = existing.replace(/\n\[mcp_servers\.biomate\][\s\S]*?(?=\n\[|$)/g, '');
29
+ existing = existing.replace(/(?:^|\n)\[mcp_servers\.biomate(?:\.[^\]]+)?\][\s\S]*?(?=\n\[|$)/g, '');
67
30
  }
68
31
  await atomicWrite(filePath, existing + append);
69
32
  }
70
33
  async function atomicWrite(filePath, contents) {
71
34
  const tmp = `${filePath}.${process.pid}.tmp`;
72
- await fs_1.promises.writeFile(tmp, contents, 'utf8');
73
- await fs_1.promises.rename(tmp, filePath);
35
+ await fs.writeFile(tmp, contents, 'utf8');
36
+ await fs.rename(tmp, filePath);
74
37
  }
package/dist/index.js CHANGED
@@ -1,9 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SURFACES = exports.runPKCEFlow = exports.generatePKCE = void 0;
4
- var pkce_js_1 = require("./pkce.js");
5
- Object.defineProperty(exports, "generatePKCE", { enumerable: true, get: function () { return pkce_js_1.generatePKCE; } });
6
- var oauth_js_1 = require("./oauth.js");
7
- Object.defineProperty(exports, "runPKCEFlow", { enumerable: true, get: function () { return oauth_js_1.runPKCEFlow; } });
8
- var surfaces_js_1 = require("./surfaces.js");
9
- Object.defineProperty(exports, "SURFACES", { enumerable: true, get: function () { return surfaces_js_1.SURFACES; } });
1
+ export { generatePKCE } from './pkce.js';
2
+ export { runPKCEFlow } from './oauth.js';
3
+ export { SURFACES } from './surfaces.js';
package/dist/oauth.js CHANGED
@@ -1,26 +1,33 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.runPKCEFlow = runPKCEFlow;
7
- const http_1 = require("http");
8
- const url_1 = require("url");
9
- const open_1 = __importDefault(require("open"));
10
- const pkce_js_1 = require("./pkce.js");
11
- async function runPKCEFlow(surface, authBaseUrl) {
12
- const { verifier, challenge } = (0, pkce_js_1.generatePKCE)();
1
+ import { createServer } from 'http';
2
+ import { URL } from 'url';
3
+ import open from 'open';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { generatePKCE } from './pkce.js';
6
+ export async function runPKCEFlow(surface, authBaseUrl) {
7
+ const { verifier, challenge } = generatePKCE();
13
8
  const port = surface.callbackPort;
14
9
  const redirectUri = `http://127.0.0.1:${port}/callback`;
15
- const state = Math.random().toString(36).slice(2);
10
+ const state = randomBytes(32).toString('base64url');
11
+ const registration = await fetch(`${authBaseUrl}/oauth/register`, {
12
+ method: 'POST', headers: { 'content-type': 'application/json' },
13
+ body: JSON.stringify({ client_name: `BioMate ${surface.label}`, redirect_uris: [redirectUri], token_endpoint_auth_method: 'none' }),
14
+ redirect: 'error', signal: AbortSignal.timeout(15000),
15
+ });
16
+ if (!registration.ok)
17
+ throw new Error(`OAuth registration failed: HTTP ${registration.status}`);
18
+ const { client_id } = await registration.json();
19
+ if (!client_id)
20
+ throw new Error('OAuth registration returned no client ID');
21
+ process.env.BIOMATE_OAUTH_CLIENT_ID = client_id;
22
+ process.env.BIOMATE_AUTH_BASE = authBaseUrl;
16
23
  const codePromise = catchCallback(port, state);
17
24
  // Mark the rejection as handled — the awaiter below will still receive it.
18
25
  // Without this, Node's unhandled-rejection detector fires when the callback
19
26
  // server rejects before `await codePromise` has attached a handler.
20
27
  codePromise.catch(() => undefined);
21
- const authUrl = new url_1.URL(`${authBaseUrl}/oauth/authorize`);
28
+ const authUrl = new URL(`${authBaseUrl}/oauth/authorize`);
22
29
  authUrl.searchParams.set('response_type', 'code');
23
- authUrl.searchParams.set('client_id', surface.clientId);
30
+ authUrl.searchParams.set('client_id', client_id);
24
31
  authUrl.searchParams.set('redirect_uri', redirectUri);
25
32
  authUrl.searchParams.set('code_challenge', challenge);
26
33
  authUrl.searchParams.set('code_challenge_method', 'S256');
@@ -28,13 +35,13 @@ async function runPKCEFlow(surface, authBaseUrl) {
28
35
  authUrl.searchParams.set('state', state);
29
36
  console.log(`\nOpening browser to authorize ${surface.label}…`);
30
37
  console.log(`If it doesn't open, visit:\n ${authUrl.toString()}\n`);
31
- await (0, open_1.default)(authUrl.toString());
38
+ await open(authUrl.toString());
32
39
  const code = await codePromise;
33
40
  const body = new URLSearchParams({
34
41
  grant_type: 'authorization_code',
35
42
  code,
36
43
  redirect_uri: redirectUri,
37
- client_id: surface.clientId,
44
+ client_id,
38
45
  code_verifier: verifier,
39
46
  });
40
47
  const resp = await fetch(`${authBaseUrl}/oauth/token`, {
@@ -49,9 +56,9 @@ async function runPKCEFlow(surface, authBaseUrl) {
49
56
  }
50
57
  function catchCallback(port, expectedState) {
51
58
  return new Promise((resolve, reject) => {
52
- const server = (0, http_1.createServer)((req, res) => {
59
+ const server = createServer((req, res) => {
53
60
  try {
54
- const url = new url_1.URL(req.url ?? '/', `http://127.0.0.1:${port}`);
61
+ const url = new URL(req.url ?? '/', `http://127.0.0.1:${port}`);
55
62
  if (url.pathname !== '/callback') {
56
63
  res.writeHead(404);
57
64
  res.end();
@@ -83,10 +90,11 @@ function catchCallback(port, expectedState) {
83
90
  });
84
91
  server.on('error', reject);
85
92
  server.listen(port, '127.0.0.1');
86
- setTimeout(() => {
93
+ const timeout = setTimeout(() => {
87
94
  server.close();
88
95
  reject(new Error('OAuth callback timed out after 3 minutes'));
89
96
  }, 3 * 60 * 1000);
97
+ server.on('close', () => clearTimeout(timeout));
90
98
  });
91
99
  }
92
100
  function htmlResult(message, ok) {
package/dist/pkce.js CHANGED
@@ -1,10 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generatePKCE = generatePKCE;
4
- const crypto_1 = require("crypto");
5
- function generatePKCE() {
6
- const verifier = base64url((0, crypto_1.randomBytes)(48));
7
- const challenge = base64url((0, crypto_1.createHash)('sha256').update(verifier).digest());
1
+ import { createHash, randomBytes } from 'crypto';
2
+ export function generatePKCE() {
3
+ const verifier = base64url(randomBytes(48));
4
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
8
5
  return { verifier, challenge, method: 'S256' };
9
6
  }
10
7
  function base64url(buf) {
package/dist/surfaces.js CHANGED
@@ -1,41 +1,6 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SURFACES = void 0;
37
- const os = __importStar(require("os"));
38
- const path = __importStar(require("path"));
1
+ import * as os from 'os';
2
+ import { mkdirSync, writeFileSync, renameSync } from 'node:fs';
3
+ import * as path from 'path';
39
4
  const DEFAULT_SCOPES = [
40
5
  'runs:read',
41
6
  'runs:write',
@@ -56,19 +21,25 @@ function platformConfigDir() {
56
21
  return process.env.APPDATA ?? home();
57
22
  return path.join(home(), '.config');
58
23
  }
59
- function mcpServerEntry(refreshToken) {
60
- const apiBase = process.env.BIOMATE_API_BASE ?? 'https://api.biomate.ai';
24
+ function mcpServerEntry(accessToken, refreshToken, clientId) {
25
+ const authBase = process.env.BIOMATE_AUTH_BASE ?? 'https://dev-public.biomate.ai';
26
+ const directory = path.join(home(), '.config', 'biomate');
27
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
28
+ const credentialFile = path.join(directory, clientId + '.json');
29
+ const temporary = credentialFile + '.' + process.pid + '.tmp';
30
+ writeFileSync(temporary, JSON.stringify({
31
+ authBase, clientId: process.env.BIOMATE_OAUTH_CLIENT_ID ?? clientId,
32
+ access_token: accessToken, refresh_token: refreshToken,
33
+ expires_at: 0,
34
+ }), { mode: 0o600 });
35
+ renameSync(temporary, credentialFile);
61
36
  return {
62
- command: 'npx',
63
- args: ['-y', '@biomate/mcp-server'],
64
- env: {
65
- BIOMATE_API_BASE: apiBase,
66
- BIOMATE_REFRESH_TOKEN: refreshToken,
67
- },
37
+ command: 'npx', args: ['-y', '@biomate/mcp-server@0.1.1'],
38
+ env: { BIOMATE_OAUTH_FILE: credentialFile },
68
39
  };
69
40
  }
70
- const configWriters_js_1 = require("./configWriters.js");
71
- exports.SURFACES = {
41
+ import { upsertJSON, upsertTOML } from './configWriters.js';
42
+ export const SURFACES = {
72
43
  'claude-code': {
73
44
  id: 'claude-code',
74
45
  label: 'Claude Code',
@@ -76,11 +47,11 @@ exports.SURFACES = {
76
47
  callbackPort: 53682,
77
48
  scopes: DEFAULT_SCOPES,
78
49
  configPath: () => path.join(home(), '.claude.json'),
79
- install: async (_t, refresh) => {
50
+ install: async (access, refresh) => {
80
51
  const p = path.join(home(), '.claude.json');
81
- await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
52
+ await upsertJSON(p, (cfg) => {
82
53
  cfg.mcpServers ??= {};
83
- cfg.mcpServers.biomate = mcpServerEntry(refresh);
54
+ cfg.mcpServers.biomate = mcpServerEntry(access, refresh, 'biomate-claude-code');
84
55
  return cfg;
85
56
  });
86
57
  return p;
@@ -93,11 +64,11 @@ exports.SURFACES = {
93
64
  callbackPort: 53683,
94
65
  scopes: DEFAULT_SCOPES,
95
66
  configPath: () => path.join(platformConfigDir(), 'Claude', 'claude_desktop_config.json'),
96
- install: async (_t, refresh) => {
67
+ install: async (access, refresh) => {
97
68
  const p = path.join(platformConfigDir(), 'Claude', 'claude_desktop_config.json');
98
- await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
69
+ await upsertJSON(p, (cfg) => {
99
70
  cfg.mcpServers ??= {};
100
- cfg.mcpServers.biomate = mcpServerEntry(refresh);
71
+ cfg.mcpServers.biomate = mcpServerEntry(access, refresh, 'biomate-claude-desktop');
101
72
  return cfg;
102
73
  });
103
74
  return p;
@@ -110,11 +81,11 @@ exports.SURFACES = {
110
81
  callbackPort: 53684,
111
82
  scopes: DEFAULT_SCOPES,
112
83
  configPath: () => path.join(home(), '.cursor', 'mcp.json'),
113
- install: async (_t, refresh) => {
84
+ install: async (access, refresh) => {
114
85
  const p = path.join(home(), '.cursor', 'mcp.json');
115
- await (0, configWriters_js_1.upsertJSON)(p, (cfg) => {
86
+ await upsertJSON(p, (cfg) => {
116
87
  cfg.mcpServers ??= {};
117
- cfg.mcpServers.biomate = mcpServerEntry(refresh);
88
+ cfg.mcpServers.biomate = mcpServerEntry(access, refresh, 'biomate-cursor');
118
89
  return cfg;
119
90
  });
120
91
  return p;
@@ -127,10 +98,10 @@ exports.SURFACES = {
127
98
  callbackPort: 53685,
128
99
  scopes: DEFAULT_SCOPES,
129
100
  configPath: () => path.join(home(), '.codex', 'config.toml'),
130
- install: async (_t, refresh) => {
101
+ install: async (access, refresh) => {
131
102
  const p = path.join(home(), '.codex', 'config.toml');
132
- const apiBase = process.env.BIOMATE_API_BASE ?? 'https://api.biomate.ai';
133
- await (0, configWriters_js_1.upsertTOML)(p, `\n[mcp_servers.biomate]\ncommand = "npx"\nargs = ["-y", "@biomate/mcp-server"]\n\n[mcp_servers.biomate.env]\nBIOMATE_API_BASE = "${apiBase}"\nBIOMATE_REFRESH_TOKEN = "${refresh}"\n`);
103
+ const entry = mcpServerEntry(access, refresh, 'biomate-codex');
104
+ await upsertTOML(p, `\n[mcp_servers.biomate]\ncommand = "npx"\nargs = ["-y", "@biomate/mcp-server@0.1.1"]\n\n[mcp_servers.biomate.env]\nBIOMATE_OAUTH_FILE = ${JSON.stringify(entry.env.BIOMATE_OAUTH_FILE)}\n`);
134
105
  return p;
135
106
  },
136
107
  },
@@ -164,7 +135,7 @@ Install steps:
164
135
  callbackPort: 53687,
165
136
  scopes: DEFAULT_SCOPES,
166
137
  configPath: () => '(Open Claw — hosted by BioMate)',
167
- install: async (_t, refresh) => {
138
+ install: async (access, refresh) => {
168
139
  console.log(`
169
140
  Open Claw runs as a hosted webhook — there's nothing to install locally.
170
141
  We've linked your account; in WeChat search the BioMate Open Claw bot and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biomate/connect",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "One-line installer that connects BioMate to Claude Code, Claude Desktop, Cursor, Codex, and ChatGPT via OAuth 2.1 + PKCE.",
5
5
  "bin": {
6
6
  "biomate-connect": "./dist/cli.js"
@@ -11,7 +11,15 @@
11
11
  "test": "vitest run",
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
- "keywords": ["biomate", "mcp", "claude", "cursor", "codex", "chatgpt", "bioinformatics"],
14
+ "keywords": [
15
+ "biomate",
16
+ "mcp",
17
+ "claude",
18
+ "cursor",
19
+ "codex",
20
+ "chatgpt",
21
+ "bioinformatics"
22
+ ],
15
23
  "author": "BioMate AI",
16
24
  "license": "MIT",
17
25
  "repository": {
@@ -23,7 +31,6 @@
23
31
  "node": ">=18"
24
32
  },
25
33
  "dependencies": {
26
- "keytar": "^7.9.0",
27
34
  "open": "^9.1.0"
28
35
  },
29
36
  "devDependencies": {
@@ -31,8 +38,10 @@
31
38
  "typescript": "^5.4.0",
32
39
  "vitest": "^1.0.0"
33
40
  },
34
- "files": ["dist", "README.md"],
35
- "publishConfig": {
36
- "access": "public"
37
- }
41
+ "files": [
42
+ "dist/*.js",
43
+ "dist/*.d.ts",
44
+ "README.md"
45
+ ],
46
+ "type": "module"
38
47
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,89 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- const vitest_1 = require("vitest");
37
- const fs_1 = require("fs");
38
- const os = __importStar(require("os"));
39
- const path = __importStar(require("path"));
40
- const configWriters_js_1 = require("../configWriters.js");
41
- async function tmpDir() {
42
- return fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'biomate-test-'));
43
- }
44
- (0, vitest_1.describe)('upsertJSON', () => {
45
- (0, vitest_1.it)('creates new file with content', async () => {
46
- const dir = await tmpDir();
47
- const f = path.join(dir, 'a', 'b', 'cfg.json');
48
- await (0, configWriters_js_1.upsertJSON)(f, (cfg) => {
49
- cfg.mcpServers = { biomate: { command: 'npx', args: [] } };
50
- return cfg;
51
- });
52
- const got = JSON.parse(await fs_1.promises.readFile(f, 'utf8'));
53
- (0, vitest_1.expect)(got.mcpServers.biomate.command).toBe('npx');
54
- });
55
- (0, vitest_1.it)('preserves existing keys', async () => {
56
- const dir = await tmpDir();
57
- const f = path.join(dir, 'cfg.json');
58
- await fs_1.promises.writeFile(f, JSON.stringify({ theme: 'dark', mcpServers: { other: { command: 'foo' } } }));
59
- await (0, configWriters_js_1.upsertJSON)(f, (cfg) => {
60
- cfg.mcpServers.biomate = { command: 'npx' };
61
- return cfg;
62
- });
63
- const got = JSON.parse(await fs_1.promises.readFile(f, 'utf8'));
64
- (0, vitest_1.expect)(got.theme).toBe('dark');
65
- (0, vitest_1.expect)(got.mcpServers.other.command).toBe('foo');
66
- (0, vitest_1.expect)(got.mcpServers.biomate.command).toBe('npx');
67
- });
68
- });
69
- (0, vitest_1.describe)('upsertTOML', () => {
70
- (0, vitest_1.it)('appends a new biomate block', async () => {
71
- const dir = await tmpDir();
72
- const f = path.join(dir, 'config.toml');
73
- await fs_1.promises.writeFile(f, '[other]\nfoo = "bar"\n');
74
- await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ncommand = "npx"\n');
75
- const got = await fs_1.promises.readFile(f, 'utf8');
76
- (0, vitest_1.expect)(got).toContain('[other]');
77
- (0, vitest_1.expect)(got).toContain('[mcp_servers.biomate]');
78
- (0, vitest_1.expect)(got).toContain('command = "npx"');
79
- });
80
- (0, vitest_1.it)('replaces existing biomate block on re-install', async () => {
81
- const dir = await tmpDir();
82
- const f = path.join(dir, 'config.toml');
83
- await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ntoken = "old"\n');
84
- await (0, configWriters_js_1.upsertTOML)(f, '\n[mcp_servers.biomate]\ntoken = "new"\n');
85
- const got = await fs_1.promises.readFile(f, 'utf8');
86
- (0, vitest_1.expect)(got).toContain('token = "new"');
87
- (0, vitest_1.expect)(got).not.toContain('token = "old"');
88
- });
89
- });
@@ -1,9 +0,0 @@
1
- /**
2
- * Installer end-to-end flow tests.
3
- *
4
- * Covers TEST_PLAN.md §3.2 (12 cases). Each test spins up a mock OAuth
5
- * server (Node http) that mirrors api.biomate.ai's /oauth/authorize and
6
- * /oauth/token endpoints, then drives the installer against a tmp HOME so
7
- * file writes don't touch the developer's real config.
8
- */
9
- export {};
@@ -1,291 +0,0 @@
1
- "use strict";
2
- /**
3
- * Installer end-to-end flow tests.
4
- *
5
- * Covers TEST_PLAN.md §3.2 (12 cases). Each test spins up a mock OAuth
6
- * server (Node http) that mirrors api.biomate.ai's /oauth/authorize and
7
- * /oauth/token endpoints, then drives the installer against a tmp HOME so
8
- * file writes don't touch the developer's real config.
9
- */
10
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- var desc = Object.getOwnPropertyDescriptor(m, k);
13
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
- desc = { enumerable: true, get: function() { return m[k]; } };
15
- }
16
- Object.defineProperty(o, k2, desc);
17
- }) : (function(o, m, k, k2) {
18
- if (k2 === undefined) k2 = k;
19
- o[k2] = m[k];
20
- }));
21
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
- Object.defineProperty(o, "default", { enumerable: true, value: v });
23
- }) : function(o, v) {
24
- o["default"] = v;
25
- });
26
- var __importStar = (this && this.__importStar) || (function () {
27
- var ownKeys = function(o) {
28
- ownKeys = Object.getOwnPropertyNames || function (o) {
29
- var ar = [];
30
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
- return ar;
32
- };
33
- return ownKeys(o);
34
- };
35
- return function (mod) {
36
- if (mod && mod.__esModule) return mod;
37
- var result = {};
38
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
- __setModuleDefault(result, mod);
40
- return result;
41
- };
42
- })();
43
- Object.defineProperty(exports, "__esModule", { value: true });
44
- const vitest_1 = require("vitest");
45
- const fs_1 = require("fs");
46
- const os = __importStar(require("os"));
47
- const path = __importStar(require("path"));
48
- // os.homedir() in Node is non-configurable; we can't replace it directly.
49
- // Instead, mock the entire 'os' module so surfaces.ts gets our stub.
50
- let __testHome = process.env.HOME ?? '/tmp';
51
- vitest_1.vi.mock('os', async () => {
52
- const actual = await vitest_1.vi.importActual('os');
53
- return { ...actual, homedir: () => __testHome };
54
- });
55
- const http_1 = require("http");
56
- const url_1 = require("url");
57
- // Hold the per-test handler that the mocked `open` will call.
58
- let __openHandler = null;
59
- vitest_1.vi.mock('open', () => ({
60
- default: async (url) => {
61
- if (__openHandler)
62
- await __openHandler(typeof url === 'string' ? url : url.toString());
63
- return { unref: () => undefined };
64
- },
65
- }));
66
- const oauth_js_1 = require("../oauth.js");
67
- const surfaces_js_1 = require("../surfaces.js");
68
- // Assign each test a unique port to avoid TCP TIME_WAIT collisions across tests.
69
- let __portCursor = 60000;
70
- function uniquePort() {
71
- return __portCursor++;
72
- }
73
- function withPort(s) {
74
- return { ...s, callbackPort: uniquePort() };
75
- }
76
- function startMockServer() {
77
- const behavior = {};
78
- let lastVerifier = null;
79
- let issuedCodes = new Map();
80
- const server = (0, http_1.createServer)(async (req, res) => {
81
- const u = new url_1.URL(req.url ?? '/', `http://${req.headers.host}`);
82
- if (u.pathname === '/oauth/authorize' && req.method === 'GET') {
83
- // The installer never hits /oauth/authorize as an HTTP client — its
84
- // browser does. We simulate by auto-redirecting on a different path.
85
- res.writeHead(200);
86
- res.end('ok');
87
- return;
88
- }
89
- if (u.pathname === '/simulate-callback' && req.method === 'GET') {
90
- // Test helper: forwards a code (or error) to the installer's callback port.
91
- const code = u.searchParams.get('code');
92
- const error = u.searchParams.get('error');
93
- const state = u.searchParams.get('state');
94
- const port = u.searchParams.get('port');
95
- const params = new URLSearchParams();
96
- if (code)
97
- params.set('code', code);
98
- if (error)
99
- params.set('error', error);
100
- if (state)
101
- params.set('state', state);
102
- const cbUrl = `http://127.0.0.1:${port}/callback?${params.toString()}`;
103
- await fetch(cbUrl).catch(() => { });
104
- res.writeHead(200);
105
- res.end('forwarded');
106
- return;
107
- }
108
- if (u.pathname === '/oauth/token' && req.method === 'POST') {
109
- const body = await readBody(req);
110
- const form = new URLSearchParams(body);
111
- lastVerifier = form.get('code_verifier');
112
- const status = behavior.tokenStatus ?? 200;
113
- res.writeHead(status, { 'content-type': 'application/json' });
114
- if (status !== 200) {
115
- res.end(JSON.stringify({ error: 'invalid_grant', error_description: 'mock' }));
116
- return;
117
- }
118
- res.end(JSON.stringify({
119
- access_token: 'mock_access_token',
120
- refresh_token: 'mock_refresh_token',
121
- token_type: 'Bearer',
122
- expires_in: 1800,
123
- scope: form.get('scope') ?? 'runs:read runs:write',
124
- }));
125
- return;
126
- }
127
- res.writeHead(404);
128
- res.end();
129
- });
130
- return new Promise((resolve) => {
131
- server.listen(0, '127.0.0.1', () => {
132
- const addr = server.address();
133
- const port = typeof addr === 'object' && addr ? addr.port : 0;
134
- resolve({
135
- url: `http://127.0.0.1:${port}`,
136
- close: () => new Promise((r) => server.close(() => r(undefined))),
137
- lastVerifier: () => lastVerifier,
138
- behavior,
139
- });
140
- });
141
- });
142
- }
143
- function readBody(req) {
144
- return new Promise((resolve, reject) => {
145
- const chunks = [];
146
- req.on('data', (c) => chunks.push(c));
147
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
148
- req.on('error', reject);
149
- });
150
- }
151
- /**
152
- * Replace `open` with a function that, instead of launching a browser, hits
153
- * /simulate-callback on the mock server with the right code + state.
154
- */
155
- function installOpenStub(mockUrl, port, opts) {
156
- __openHandler = async (authUrl) => {
157
- const u = new url_1.URL(authUrl);
158
- const state = opts.stateOverride ?? (u.searchParams.get('state') ?? '');
159
- const params = new URLSearchParams({ port: String(port) });
160
- if (opts.error)
161
- params.set('error', opts.error);
162
- else
163
- params.set('code', 'mock_code_' + Math.random().toString(36).slice(2));
164
- params.set('state', state);
165
- await fetch(`${mockUrl}/simulate-callback?${params.toString()}`);
166
- };
167
- }
168
- (0, vitest_1.describe)('installer end-to-end flow', () => {
169
- let mock;
170
- let tmpHome;
171
- let origHome;
172
- (0, vitest_1.beforeEach)(async () => {
173
- mock = await startMockServer();
174
- tmpHome = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'biomate-flow-'));
175
- origHome = process.env.HOME;
176
- process.env.HOME = tmpHome;
177
- __testHome = tmpHome;
178
- process.env.BIOMATE_API_BASE = 'https://mock.api.example';
179
- });
180
- (0, vitest_1.afterEach)(async () => {
181
- __openHandler = null;
182
- await mock.close();
183
- if (origHome)
184
- process.env.HOME = origHome;
185
- else
186
- delete process.env.HOME;
187
- await fs_1.promises.rm(tmpHome, { recursive: true, force: true });
188
- });
189
- // --- §3.2.1, §3.2.2, §3.2.3, §3.2.4, §3.2.5, §3.2.6, §3.2.7 — per-surface ---
190
- (0, vitest_1.it)('§3.2.1 claude-code: writes ~/.claude.json with mcpServers.biomate on fresh machine', async () => {
191
- const surface = withPort(surfaces_js_1.SURFACES['claude-code']);
192
- installOpenStub(mock.url, surface.callbackPort, {});
193
- const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
194
- await surface.install(tokens.access_token, tokens.refresh_token);
195
- const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
196
- (0, vitest_1.expect)(cfg.mcpServers.biomate.command).toBe('npx');
197
- (0, vitest_1.expect)(cfg.mcpServers.biomate.env.BIOMATE_REFRESH_TOKEN).toBe('mock_refresh_token');
198
- });
199
- (0, vitest_1.it)('§3.2.2 claude-code: preserves existing keys in ~/.claude.json', async () => {
200
- await fs_1.promises.writeFile(path.join(tmpHome, '.claude.json'), JSON.stringify({ theme: 'dark', mcpServers: { other: { command: 'foo' } } }));
201
- const surface = withPort(surfaces_js_1.SURFACES['claude-code']);
202
- installOpenStub(mock.url, surface.callbackPort, {});
203
- const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
204
- await surface.install(tokens.access_token, tokens.refresh_token);
205
- const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
206
- (0, vitest_1.expect)(cfg.theme).toBe('dark');
207
- (0, vitest_1.expect)(cfg.mcpServers.other.command).toBe('foo');
208
- (0, vitest_1.expect)(cfg.mcpServers.biomate.command).toBe('npx');
209
- });
210
- (0, vitest_1.it)('§3.2.3 claude-code: re-install is idempotent (no duplicate biomate entry)', async () => {
211
- const surface1 = withPort(surfaces_js_1.SURFACES['claude-code']);
212
- installOpenStub(mock.url, surface1.callbackPort, {});
213
- let tokens = await (0, oauth_js_1.runPKCEFlow)(surface1, mock.url);
214
- await surface1.install(tokens.access_token, tokens.refresh_token);
215
- __openHandler = null;
216
- const surface2 = withPort(surfaces_js_1.SURFACES['claude-code']); // fresh port
217
- installOpenStub(mock.url, surface2.callbackPort, {});
218
- tokens = await (0, oauth_js_1.runPKCEFlow)(surface2, mock.url);
219
- await surface2.install(tokens.access_token, tokens.refresh_token);
220
- const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.claude.json'), 'utf8'));
221
- // Only one `biomate` key — JSON objects can't have duplicates anyway, but
222
- // we assert structure is unchanged after second install.
223
- (0, vitest_1.expect)(Object.keys(cfg.mcpServers)).toEqual(['biomate']);
224
- });
225
- (0, vitest_1.it)('§3.2.4 cursor: writes ~/.cursor/mcp.json', async () => {
226
- const surface = withPort(surfaces_js_1.SURFACES['cursor']);
227
- installOpenStub(mock.url, surface.callbackPort, {});
228
- const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
229
- await surface.install(tokens.access_token, tokens.refresh_token);
230
- const cfg = JSON.parse(await fs_1.promises.readFile(path.join(tmpHome, '.cursor', 'mcp.json'), 'utf8'));
231
- (0, vitest_1.expect)(cfg.mcpServers.biomate).toBeDefined();
232
- });
233
- (0, vitest_1.it)('§3.2.5 codex: writes [mcp_servers.biomate] to ~/.codex/config.toml', async () => {
234
- const surface = withPort(surfaces_js_1.SURFACES['codex']);
235
- installOpenStub(mock.url, surface.callbackPort, {});
236
- const tokens = await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
237
- await surface.install(tokens.access_token, tokens.refresh_token);
238
- const cfg = await fs_1.promises.readFile(path.join(tmpHome, '.codex', 'config.toml'), 'utf8');
239
- (0, vitest_1.expect)(cfg).toContain('[mcp_servers.biomate]');
240
- (0, vitest_1.expect)(cfg).toContain('mock_refresh_token');
241
- });
242
- (0, vitest_1.it)('§3.2.6 chatgpt: prints instructions, writes no file', async () => {
243
- const surface = surfaces_js_1.SURFACES['chatgpt'];
244
- const logSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => undefined);
245
- const result = await surface.install('access', 'refresh');
246
- (0, vitest_1.expect)(result).toMatch(/ChatGPT/);
247
- // No .chatgpt-style files in $HOME.
248
- const entries = await fs_1.promises.readdir(tmpHome);
249
- (0, vitest_1.expect)(entries).toEqual([]);
250
- logSpy.mockRestore();
251
- });
252
- (0, vitest_1.it)('§3.2.7 open-claw: prints WeChat instructions, writes no file', async () => {
253
- const surface = surfaces_js_1.SURFACES['open-claw'];
254
- const logSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => undefined);
255
- const result = await surface.install('access', 'refresh');
256
- (0, vitest_1.expect)(result).toBe('(linked)');
257
- const entries = await fs_1.promises.readdir(tmpHome);
258
- (0, vitest_1.expect)(entries).toEqual([]);
259
- logSpy.mockRestore();
260
- });
261
- // --- §3.2.9–§3.2.12 — failure modes ---
262
- (0, vitest_1.it)('§3.2.10 OAuth callback error=access_denied surfaces cleanly', async () => {
263
- const surface = withPort(surfaces_js_1.SURFACES['cursor']);
264
- installOpenStub(mock.url, surface.callbackPort, { error: 'access_denied' });
265
- await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/access_denied/);
266
- });
267
- (0, vitest_1.it)('§3.2.11 state mismatch on callback rejected', async () => {
268
- const surface = withPort(surfaces_js_1.SURFACES['cursor']);
269
- installOpenStub(mock.url, surface.callbackPort, { stateOverride: 'wrong-state' });
270
- await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/invalid callback/);
271
- });
272
- (0, vitest_1.it)('§3.2.12 token exchange 400 surfaces the server reply', async () => {
273
- const surface = withPort(surfaces_js_1.SURFACES['cursor']);
274
- mock.behavior.tokenStatus = 400;
275
- installOpenStub(mock.url, surface.callbackPort, {});
276
- await (0, vitest_1.expect)((0, oauth_js_1.runPKCEFlow)(surface, mock.url)).rejects.toThrow(/token exchange failed: 400/);
277
- });
278
- // --- §3.2.8 — unknown surface (CLI-level; verified via lookup) ---
279
- (0, vitest_1.it)('§3.2.8 unknown surface id returns undefined from SURFACES map', () => {
280
- (0, vitest_1.expect)(surfaces_js_1.SURFACES['not-a-surface']).toBeUndefined();
281
- });
282
- // --- bonus: PKCE verifier is forwarded to token endpoint ---
283
- (0, vitest_1.it)('bonus: code_verifier is presented to /oauth/token', async () => {
284
- const surface = withPort(surfaces_js_1.SURFACES['cursor']);
285
- installOpenStub(mock.url, surface.callbackPort, {});
286
- await (0, oauth_js_1.runPKCEFlow)(surface, mock.url);
287
- const v = mock.lastVerifier();
288
- (0, vitest_1.expect)(v).not.toBeNull();
289
- (0, vitest_1.expect)(v.length).toBeGreaterThanOrEqual(43);
290
- });
291
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,28 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const vitest_1 = require("vitest");
4
- const crypto_1 = require("crypto");
5
- const pkce_js_1 = require("../pkce.js");
6
- (0, vitest_1.describe)('PKCE', () => {
7
- (0, vitest_1.it)('verifier length is in [43, 128]', () => {
8
- for (let i = 0; i < 20; i++) {
9
- const p = (0, pkce_js_1.generatePKCE)();
10
- (0, vitest_1.expect)(p.verifier.length).toBeGreaterThanOrEqual(43);
11
- (0, vitest_1.expect)(p.verifier.length).toBeLessThanOrEqual(128);
12
- }
13
- });
14
- (0, vitest_1.it)('challenge equals base64url(sha256(verifier))', () => {
15
- const p = (0, pkce_js_1.generatePKCE)();
16
- const computed = (0, crypto_1.createHash)('sha256').update(p.verifier).digest('base64')
17
- .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
18
- (0, vitest_1.expect)(p.challenge).toBe(computed);
19
- });
20
- (0, vitest_1.it)('uses S256 method', () => {
21
- (0, vitest_1.expect)((0, pkce_js_1.generatePKCE)().method).toBe('S256');
22
- });
23
- (0, vitest_1.it)('verifier and challenge are URL-safe (no +,/,=)', () => {
24
- const p = (0, pkce_js_1.generatePKCE)();
25
- (0, vitest_1.expect)(p.verifier).not.toMatch(/[+/=]/);
26
- (0, vitest_1.expect)(p.challenge).not.toMatch(/[+/=]/);
27
- });
28
- });