@beepflow/mcp 0.1.0 → 2026.4.2

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
@@ -19,9 +19,34 @@ npx -y @beepflow/mcp
19
19
  ## Publish (npm)
20
20
  ```sh
21
21
  make -C mcp publish-dry-run
22
- make -C mcp publish VERSION=0.1.0
22
+ make -C mcp publish
23
23
  ```
24
24
 
25
+ Publish auto tags version using `year.month.counter`:
26
+ - `major` = current year
27
+ - `minor` = current month (no leading zero)
28
+ - `patch` = counter for year+month starting at `0`
29
+
30
+ Check next version:
31
+ ```sh
32
+ make -C mcp compute-next-version
33
+ ```
34
+
35
+ ## Codex MCP install (local)
36
+ ```sh
37
+ make -C mcp codex-install-dev
38
+ make -C mcp codex-install-live
39
+ ```
40
+
41
+ Public install:
42
+ ```sh
43
+ codex mcp add beepflow -- npx -y @beepflow/mcp
44
+ ```
45
+
46
+ Switch easy: both targets install same MCP name `beepflow`.
47
+ - `codex-install-dev`: local `src/index.ts`
48
+ - `codex-install-live`: published `npx -y @beepflow/mcp`
49
+
25
50
  ## Auth tools
26
51
  - `auth_email_request_code` `{ "email": "you@domain.com" }`
27
52
  - `auth_email_verify_code` `{ "email": "you@domain.com", "code": "123456" }`
package/dist/index.js CHANGED
@@ -1,10 +1,6 @@
1
- import { createMcpStdioServer } from "./mcp-stdio-server.js";
2
- import { createBeepflowToolDefinitions } from "./mcp-tool-definitions.js";
3
- const { server, closeServer } = createMcpStdioServer({
4
- serverName: 'mcp',
5
- serverVersion: '0.1.0',
6
- tools: createBeepflowToolDefinitions(),
7
- });
8
- process.on('SIGINT', () => closeServer('SIGINT'));
9
- process.on('SIGTERM', () => closeServer('SIGTERM'));
10
- server.start();
1
+ #!/usr/bin/env node
2
+ import{stdin as k,stdout as R}from"node:process";function U(e){let t=new Map;for(let p of e.tools)t.set(p.name,p);let n=!1,o="",s={start:()=>{k.setEncoding("utf8"),k.on("data",p=>c(typeof p=="string"?p:p.toString("utf8"))),k.on("end",()=>r("stdin-end")),k.on("error",()=>r("stdin-error"))}};function r(p){if(!n){n=!0;try{R.write(JSON.stringify({jsonrpc:"2.0",method:"notifications/message",params:{level:"info",data:`server closing: ${p}`}})+`
3
+ `)}catch{}}}function c(p){for(o+=p;;){let d=o.indexOf(`
4
+ `);if(d===-1)return;let u=o.slice(0,d).trim();if(o=o.slice(d+1),!u)continue;let f;try{f=JSON.parse(u)}catch{continue}i(f)}}async function i(p){if(n)return;let d=p.id===void 0,u=p.id??null;try{switch(p.method){case"initialize":{let h={protocolVersion:(p.params??{}).protocolVersion??"2024-11-05",serverInfo:{name:e.serverName,version:e.serverVersion},capabilities:{tools:{}}};if(d)return;a({jsonrpc:"2.0",id:u,result:h});return}case"notifications/initialized":return;case"notifications/cancelled":return;case"logging/setLevel":{if(d)return;a({jsonrpc:"2.0",id:u,result:{}});return}case"ping":{if(d)return;a({jsonrpc:"2.0",id:u,result:{}});return}case"tools/list":{if(d)return;let f=e.tools.map(m=>({name:m.name,description:m.description,inputSchema:m.inputSchema}));a({jsonrpc:"2.0",id:u,result:{tools:f}});return}case"tools/call":{if(d)return;let f=p.params??{},m=typeof f.name=="string"?f.name:"",h=t.get(m);if(!h){a({jsonrpc:"2.0",id:u,error:{code:-32601,message:`unknown tool: ${m}`}});return}let G=await h.call(f.arguments);a({jsonrpc:"2.0",id:u,result:G});return}case"resources/list":{if(d)return;a({jsonrpc:"2.0",id:u,result:{resources:[]}});return}case"prompts/list":{if(d)return;a({jsonrpc:"2.0",id:u,result:{prompts:[]}});return}default:{if(d)return;a({jsonrpc:"2.0",id:u,error:{code:-32601,message:`method not found: ${p.method}`}})}}}catch(f){if(d)return;a({jsonrpc:"2.0",id:u,error:{code:-32603,message:f?.message??"internal error"}})}}function a(p){n||R.write(JSON.stringify(p)+`
5
+ `)}return{server:s,closeServer:r}}import H from"node:os";import E from"node:path";var V={dev:{name:"dev",workspaceUrl:"http://localhost:2451/api",authUrl:"https://auth.fewlogs.com",authDomain:"beepflow.com"},live:{name:"live",workspaceUrl:"https://api.kafkaide.com:30200/api",authUrl:"https://auth.fewlogs.com",authDomain:"beepflow.com"}};function z(e){let n=W(e,"--env")??W(e,"--environment")??process.env.BEEPFLOW_ENV??"live";if(n==="dev"||n==="live")return n;throw new Error(`invalid BEEPFLOW_ENV: ${n}`)}function B(e){let t=z(e);return V[t]}function W(e,t){for(let n=0;n<e.length;n+=1){let o=e[n];if(o===t){let r=e[n+1];if(!r)throw new Error(`missing value for ${t}`);return r}let s=`${t}=`;if(o&&o.startsWith(s)){let r=o.slice(s.length);if(!r)throw new Error(`missing value for ${t}`);return r}}}function l(){let e=process.argv.slice(2),t=B(e),n=b("workspace url",[w(e,"--workspace-url"),process.env.BEEPFLOW_WORKSPACE_URL,t.workspaceUrl]),o=b("auth url",[w(e,"--auth-url"),process.env.BEEPFLOW_AUTH_URL,t.authUrl]),s=b("auth domain",[w(e,"--auth-domain"),process.env.BEEPFLOW_AUTH_DOMAIN,t.authDomain]),r=w(e,"--token-store-path")??process.env.BEEPFLOW_TOKEN_STORE_PATH??Q();return{environmentName:t.name,workspaceUrl:n.replace(/\/+$/,""),authUrl:o.replace(/\/+$/,""),authDomain:s,jwtFromEnvironment:w(e,"--jwt")??process.env.BEEPFLOW_JWT,tokenStorePath:r}}function w(e,t){for(let n=0;n<e.length;n+=1){let o=e[n];if(o===t){let r=e[n+1];if(!r)throw new Error(`missing value for ${t}`);return r}let s=`${t}=`;if(o&&o.startsWith(s)){let r=o.slice(s.length);if(!r)throw new Error(`missing value for ${t}`);return r}}}function Q(){let e=process.env.APPDATA;if(e)return E.join(e,"beepflow-mcp","workspace-access-tokens.json");let t=process.env.XDG_CONFIG_HOME;return t?E.join(t,"beepflow-mcp","workspace-access-tokens.json"):E.join(H.homedir(),".config","beepflow-mcp","workspace-access-tokens.json")}function b(e,t){for(let n of t)if(typeof n=="string"&&n.trim())return n;throw new Error(`missing ${e}`)}import x from"node:fs/promises";import Y from"node:path";import K from"node:crypto";function y(e){let t=e.split(".");if(t.length<2)return{accessToken:e,email:void 0,exp:void 0,provider:void 0};let n=X(t[1]??"");if(!n)return{accessToken:e,email:void 0,exp:void 0,provider:void 0};let o;try{o=JSON.parse(n)}catch{return{accessToken:e,email:void 0,exp:void 0,provider:void 0}}let s=typeof o.email=="string"?o.email.toLowerCase():void 0,r=typeof o.exp=="number"?o.exp:void 0,c=typeof o.provider=="string"?o.provider:void 0;return{accessToken:e,email:s,exp:r,provider:c}}function P(e){return e?e.length<=12?`${e.slice(0,4)}\u2026`:`${e.slice(0,6)}\u2026${e.slice(-4)}`:""}function j(){return K.randomBytes(9).toString("base64url")}function X(e){if(!e)return;let t=e.replace(/-/g,"+").replace(/_/g,"/"),n=(4-t.length%4)%4,o=t+"=".repeat(n);try{return Buffer.from(o,"base64").toString("utf8")}catch{return}}async function J(){let e=l();return e.jwtFromEnvironment?e.jwtFromEnvironment:(await Z(e.tokenStorePath,e.workspaceUrl))?.accessToken}async function g(e){let t=l(),n=y(e),o={accessToken:e,email:n.email,provider:n.provider,exp:n.exp};return await ee(t.tokenStorePath,t.workspaceUrl,o),o}async function _(){let e=l(),t=await T(e.tokenStorePath),n=t[e.workspaceUrl]!==void 0;return delete t[e.workspaceUrl],await $(e.tokenStorePath,t),{workspaceUrl:e.workspaceUrl,deleted:n}}function v(e){return{email:e.email,provider:e.provider,exp:e.exp,accessToken:P(e.accessToken)}}async function Z(e,t){return(await T(e))[t]}async function ee(e,t,n){let o=await T(e);o[t]=n,await $(e,o)}async function T(e){try{let t=await x.readFile(e,"utf8"),n=JSON.parse(t);return!n||typeof n!="object"?{}:n}catch{return{}}}async function $(e,t){await x.mkdir(Y.dirname(e),{recursive:!0}),await x.writeFile(e,JSON.stringify(t,null,2)+`
6
+ `,"utf8")}async function O(e){let t=l(),n=await fetch(`${t.authUrl}/email/request-code`,{method:"POST",headers:{"content-type":"application/json",domain:t.authDomain},body:JSON.stringify({email:e})});if(!n.ok){let o=await n.text().catch(()=>"");throw new Error(`email/request-code failed: ${n.status} ${o}`.trim())}}async function I(e,t){let n=l(),o=await fetch(`${n.authUrl}/email/verify-code`,{method:"POST",headers:{"content-type":"application/json",domain:n.authDomain},body:JSON.stringify({email:e,code:t})}),s=await o.json().catch(()=>{});if(!o.ok)throw new Error(`email/verify-code failed: ${o.status}`);let r=typeof s?.access_token=="string"?s.access_token:"";if(!r)throw new Error("email/verify-code failed: missing access_token");let c=await g(r);return{workspaceUrl:n.workspaceUrl,...v(c)}}import te from"node:crypto";async function A(e){let t=l(),n=te.randomUUID(),o=new URL(`${t.authUrl}/google-form`);return o.searchParams.set("requestId",n),o.searchParams.set("scopes",e),{requestId:n,url:o.toString()}}async function C(e){let t=l(),n=new URL(`${t.authUrl}/google-auth-token`);n.searchParams.set("id",e);let o=await fetch(n.toString(),{headers:{domain:t.authDomain}}),s=await o.json().catch(()=>{});if(!o.ok)throw new Error(`google-auth-token failed: ${o.status}`);let r=typeof s?.access_token=="string"?s.access_token:"";if(!r)throw new Error("google-auth-token failed: missing access_token");let c=await g(r);return{workspaceUrl:t.workspaceUrl,...v(c)}}async function N(e){let t=l(),n=await fetch(`${t.workspaceUrl}/users/is-member`,{method:"GET",headers:{Authorization:`Bearer ${e}`}});if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`users/is-member failed: ${n.status} ${s}`.trim())}if((await n.json().catch(()=>{}))?.isMember!==!0)throw new Error("user not member of workspace")}async function M(e){let t=y(e);if(t.provider!=="google"||!t.exp)return e;let n=Date.now(),o=t.exp*1e3,s=300*1e3;if(o-n>=s)return e;let r=l(),c=await fetch(`${r.authUrl}/google-refresh`,{method:"GET",headers:{Authorization:`Bearer ${e}`}}),i=await c.json().catch(()=>{});if(!c.ok)return e;let a=typeof i?.access_token=="string"?i.access_token:"";return a?(await g(a),a):e}async function q(e,t,n,o,s){let r=l(),c=ne(r.workspaceUrl,e,t),i=await re(c,s);try{let a={resource:n,action:"snapshot",value:o};return i.send(oe("replicated-state",JSON.stringify(a))),await se(i,n,s)}finally{i.close()}}function ne(e,t,n){let o=e.replace(/^http/,"ws"),s=encodeURIComponent(t),r=encodeURIComponent(n);return`${o}/base/${s}/${r}`}function oe(e,t){return`${e};${t}`}async function re(e,t){let n=new WebSocket(e,"echo"),o=await new Promise(s=>{let r=setTimeout(()=>s({ok:!1,reason:"timeout"}),t);n.onopen=()=>{clearTimeout(r),s({ok:!0})},n.onerror=()=>{clearTimeout(r),s({ok:!1,reason:"error"})}});if(!o.ok)throw n.close(),new Error(`websocket open failed: ${o.reason??"unknown"}`);return n}async function se(e,t,n){return await new Promise((o,s)=>{let r=setTimeout(()=>s(new Error("snapshot timeout")),n);e.onerror=()=>{clearTimeout(r),s(new Error("websocket error during snapshot wait"))},e.onclose=()=>{clearTimeout(r),s(new Error("websocket closed during snapshot wait"))},e.onmessage=c=>{let i=typeof c.data=="string"?c.data:"",a=i.indexOf(";");if(a===-1||i.slice(0,a)!=="replicated-state")return;let d=i.slice(a+1),u;try{u=JSON.parse(d)}catch{return}u.action==="snapshot"&&u.resource===t&&(!u.value||typeof u.value!="object"||(clearTimeout(r),o(u.value)))}})}async function D(){let e=await J();if(!e)throw new Error("missing jwt: set BEEPFLOW_JWT or run auth tool to persist token");let t=await M(e);await N(t);let n=j(),o=await q(t,n,"spaces",{all:!0},15e3),s=[];for(let r in o){let c=o[r];if(!c||typeof c!="object")continue;let i=c,a=i.id;if(typeof a!="string"||!a)continue;let p={id:a,updatedAt:typeof i.updatedAt=="number"?i.updatedAt:void 0,label:typeof i.label=="string"?i.label:void 0,thumbnailUrl:typeof i.thumbnailUrl=="string"?i.thumbnailUrl:void 0,description:typeof i.description=="string"?i.description:void 0,isUserSpace:typeof i.isUserSpace=="boolean"?i.isUserSpace:void 0};s.push(p)}return s.sort((r,c)=>{let i=(r.label??"").toLowerCase(),a=(c.label??"").toLowerCase();return i<a?-1:i>a?1:r.id<c.id?-1:r.id>c.id?1:0}),s}function L(){return[{name:"spaces_list",description:"List authorized Beepflow spaces for current JWT.",inputSchema:{type:"object",additionalProperties:!1,properties:{}},call:async()=>{let e=await D();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}},{name:"auth_email_request_code",description:"Request email login code via Beepflow auth service.",inputSchema:{type:"object",additionalProperties:!1,required:["email"],properties:{email:{type:"string"}}},call:async e=>{let t=S(e,"email");return await O(t),{content:[{type:"text",text:`code sent to ${t}`}]}}},{name:"auth_email_verify_code",description:"Verify email login code, persist JWT for workspace.",inputSchema:{type:"object",additionalProperties:!1,required:["email","code"],properties:{email:{type:"string"},code:{type:"string"}}},call:async e=>{let t=S(e,"email"),n=S(e,"code"),o=await I(t,n);return{content:[{type:"text",text:JSON.stringify(o,null,2)}]}}},{name:"auth_google_start",description:"Create Google OAuth login URL + request id.",inputSchema:{type:"object",additionalProperties:!1,properties:{scopes:{type:"string"}}},call:async e=>{let t=ie(e,"scopes")??"openid email profile",n=await A(t);return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}},{name:"auth_google_get_token",description:"Fetch Google OAuth token using request id, persist JWT for workspace.",inputSchema:{type:"object",additionalProperties:!1,required:["requestId"],properties:{requestId:{type:"string"}}},call:async e=>{let t=S(e,"requestId"),n=await C(t);return{content:[{type:"text",text:JSON.stringify(n,null,2)}]}}},{name:"auth_sign_out",description:"Delete stored JWT for current workspace.",inputSchema:{type:"object",additionalProperties:!1,properties:{}},call:async()=>{let e=await _();return{content:[{type:"text",text:JSON.stringify(e,null,2)}]}}}]}function S(e,t){if(!e||typeof e!="object")throw new Error("invalid tool arguments: expected object");let o=e[t];if(typeof o!="string"||!o.trim())throw new Error(`invalid tool arguments: ${t} must be non-empty string`);return o}function ie(e,t){if(e===void 0)return;if(!e||typeof e!="object")throw new Error("invalid tool arguments: expected object");let o=e[t];if(o!=null){if(typeof o!="string")throw new Error(`invalid tool arguments: ${t} must be string`);return o}}var{server:ae,closeServer:F}=U({serverName:"mcp",serverVersion:"0.1.0",tools:L()});process.on("SIGINT",()=>F("SIGINT"));process.on("SIGTERM",()=>F("SIGTERM"));ae.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beepflow/mcp",
3
- "version": "0.1.0",
3
+ "version": "2026.4.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24.6.0"
@@ -17,13 +17,14 @@
17
17
  "scripts": {
18
18
  "start:src": "node --experimental-strip-types ./src/index.ts",
19
19
  "test:src": "node --experimental-strip-types --test ./src/**/*.test.ts",
20
- "build": "tsc -p tsconfig.build.json",
20
+ "build": "node --input-type=module -e \"import fs from 'node:fs'; fs.rmSync('dist', { recursive: true, force: true });\" && tsc -p tsconfig.json --noEmit && esbuild src/index.ts --bundle --platform=node --format=esm --target=node24 --minify --banner:js='#!/usr/bin/env node' --outfile=dist/index.js && node --input-type=module -e \"import fs from 'node:fs'; fs.chmodSync('dist/index.js', 0o755);\"",
21
21
  "start": "node ./dist/index.js",
22
- "test": "yarn build && node --test ./dist/**/*.test.js",
22
+ "test": "yarn test:src && yarn build && node ./dist/index.js < /dev/null",
23
23
  "prepack": "yarn build"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^24.3.0",
27
+ "esbuild": "^0.25.9",
27
28
  "typescript": "~5.7.3"
28
29
  }
29
30
  }
@@ -1,45 +0,0 @@
1
- export const beepflowEnvironmentByName = {
2
- dev: {
3
- name: 'dev',
4
- workspaceUrl: 'http://localhost:2451/api',
5
- authUrl: 'https://auth.fewlogs.com',
6
- authDomain: 'beepflow.com',
7
- },
8
- live: {
9
- name: 'live',
10
- workspaceUrl: 'https://api.kafkaide.com:30200/api',
11
- authUrl: 'https://auth.fewlogs.com',
12
- authDomain: 'beepflow.com',
13
- },
14
- };
15
- export function readBeepflowEnvironmentName(args) {
16
- const flagValue = readStringFlagValue(args, '--env') ??
17
- readStringFlagValue(args, '--environment');
18
- const envValue = flagValue ?? process.env.BEEPFLOW_ENV ?? 'live';
19
- if (envValue === 'dev' || envValue === 'live') {
20
- return envValue;
21
- }
22
- throw new Error(`invalid BEEPFLOW_ENV: ${envValue}`);
23
- }
24
- export function readBeepflowEnvironment(args) {
25
- const name = readBeepflowEnvironmentName(args);
26
- return beepflowEnvironmentByName[name];
27
- }
28
- function readStringFlagValue(args, flagName) {
29
- for (let i = 0; i < args.length; i += 1) {
30
- const arg = args[i];
31
- if (arg === flagName) {
32
- const value = args[i + 1];
33
- if (!value)
34
- throw new Error(`missing value for ${flagName}`);
35
- return value;
36
- }
37
- const equalsPrefix = `${flagName}=`;
38
- if (arg && arg.startsWith(equalsPrefix)) {
39
- const value = arg.slice(equalsPrefix.length);
40
- if (!value)
41
- throw new Error(`missing value for ${flagName}`);
42
- return value;
43
- }
44
- }
45
- }
@@ -1,34 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { readBeepflowEnvironmentName, readBeepflowEnvironment } from "./environment.js";
4
- test('readBeepflowEnvironmentName defaults to live', () => {
5
- const originalEnv = process.env.BEEPFLOW_ENV;
6
- try {
7
- delete process.env.BEEPFLOW_ENV;
8
- assert.equal(readBeepflowEnvironmentName([]), 'live');
9
- }
10
- finally {
11
- if (originalEnv === undefined)
12
- delete process.env.BEEPFLOW_ENV;
13
- else
14
- process.env.BEEPFLOW_ENV = originalEnv;
15
- }
16
- });
17
- test('readBeepflowEnvironment reads live config', () => {
18
- const originalEnv = process.env.BEEPFLOW_ENV;
19
- try {
20
- process.env.BEEPFLOW_ENV = 'live';
21
- const env = readBeepflowEnvironment([]);
22
- assert.equal(env.name, 'live');
23
- assert.ok(env.workspaceUrl.includes('/api'));
24
- }
25
- finally {
26
- if (originalEnv === undefined)
27
- delete process.env.BEEPFLOW_ENV;
28
- else
29
- process.env.BEEPFLOW_ENV = originalEnv;
30
- }
31
- });
32
- test('readBeepflowEnvironmentName rejects invalid value', () => {
33
- assert.throws(() => readBeepflowEnvironmentName(['--env', 'nope']), /invalid BEEPFLOW_ENV/);
34
- });
@@ -1,157 +0,0 @@
1
- import { stdin as inputStream, stdout as outputStream } from 'node:process';
2
- export function createMcpStdioServer(args) {
3
- const toolByName = new Map();
4
- for (const tool of args.tools)
5
- toolByName.set(tool.name, tool);
6
- let isClosed = false;
7
- let inputBuffer = '';
8
- const server = {
9
- start: () => {
10
- inputStream.setEncoding('utf8');
11
- inputStream.on('data', (chunk) => processInboundJsonRpcChunk(typeof chunk === 'string' ? chunk : chunk.toString('utf8')));
12
- inputStream.on('end', () => closeServer('stdin-end'));
13
- inputStream.on('error', () => closeServer('stdin-error'));
14
- },
15
- };
16
- function closeServer(reason) {
17
- if (isClosed)
18
- return;
19
- isClosed = true;
20
- try {
21
- outputStream.write(JSON.stringify({
22
- jsonrpc: '2.0',
23
- method: 'notifications/message',
24
- params: { level: 'info', data: `server closing: ${reason}` },
25
- }) + '\n');
26
- }
27
- catch {
28
- // ignore
29
- }
30
- }
31
- function processInboundJsonRpcChunk(chunk) {
32
- inputBuffer += chunk;
33
- while (true) {
34
- const newlineIndex = inputBuffer.indexOf('\n');
35
- if (newlineIndex === -1)
36
- return;
37
- const rawLine = inputBuffer.slice(0, newlineIndex).trim();
38
- inputBuffer = inputBuffer.slice(newlineIndex + 1);
39
- if (!rawLine)
40
- continue;
41
- let message;
42
- try {
43
- message = JSON.parse(rawLine);
44
- }
45
- catch {
46
- continue;
47
- }
48
- void processJsonRpcMessage(message);
49
- }
50
- }
51
- async function processJsonRpcMessage(message) {
52
- if (isClosed)
53
- return;
54
- const isNotification = message.id === undefined;
55
- const requestId = message.id ?? null;
56
- try {
57
- switch (message.method) {
58
- case 'initialize': {
59
- const params = (message.params ?? {});
60
- const protocolVersion = params.protocolVersion ?? '2024-11-05';
61
- const result = {
62
- protocolVersion,
63
- serverInfo: { name: args.serverName, version: args.serverVersion },
64
- capabilities: { tools: {} },
65
- };
66
- if (isNotification)
67
- return;
68
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result });
69
- return;
70
- }
71
- case 'notifications/initialized': {
72
- return;
73
- }
74
- case 'notifications/cancelled': {
75
- return;
76
- }
77
- case 'logging/setLevel': {
78
- if (isNotification)
79
- return;
80
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result: {} });
81
- return;
82
- }
83
- case 'ping': {
84
- if (isNotification)
85
- return;
86
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result: {} });
87
- return;
88
- }
89
- case 'tools/list': {
90
- if (isNotification)
91
- return;
92
- const tools = args.tools.map((t) => ({
93
- name: t.name,
94
- description: t.description,
95
- inputSchema: t.inputSchema,
96
- }));
97
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result: { tools } });
98
- return;
99
- }
100
- case 'tools/call': {
101
- if (isNotification)
102
- return;
103
- const params = (message.params ?? {});
104
- const toolName = typeof params.name === 'string' ? params.name : '';
105
- const tool = toolByName.get(toolName);
106
- if (!tool) {
107
- writeJsonRpcResponse({
108
- jsonrpc: '2.0',
109
- id: requestId,
110
- error: { code: -32601, message: `unknown tool: ${toolName}` },
111
- });
112
- return;
113
- }
114
- const result = await tool.call(params.arguments);
115
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result });
116
- return;
117
- }
118
- case 'resources/list': {
119
- if (isNotification)
120
- return;
121
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result: { resources: [] } });
122
- return;
123
- }
124
- case 'prompts/list': {
125
- if (isNotification)
126
- return;
127
- writeJsonRpcResponse({ jsonrpc: '2.0', id: requestId, result: { prompts: [] } });
128
- return;
129
- }
130
- default: {
131
- if (isNotification)
132
- return;
133
- writeJsonRpcResponse({
134
- jsonrpc: '2.0',
135
- id: requestId,
136
- error: { code: -32601, message: `method not found: ${message.method}` },
137
- });
138
- }
139
- }
140
- }
141
- catch (error) {
142
- if (isNotification)
143
- return;
144
- writeJsonRpcResponse({
145
- jsonrpc: '2.0',
146
- id: requestId,
147
- error: { code: -32603, message: error?.message ?? 'internal error' },
148
- });
149
- }
150
- }
151
- function writeJsonRpcResponse(response) {
152
- if (isClosed)
153
- return;
154
- outputStream.write(JSON.stringify(response) + '\n');
155
- }
156
- return { server, closeServer };
157
- }
@@ -1,112 +0,0 @@
1
- import { requestEmailLoginCode, verifyEmailLoginCode } from "./workspace-auth-email.js";
2
- import { createGoogleLoginUrl, fetchGoogleLoginToken } from "./workspace-auth-google.js";
3
- import { listAuthorizedSpaces } from "./workspace-spaces-list.js";
4
- import { deleteWorkspaceToken } from "./workspace-token-store.js";
5
- export function createBeepflowToolDefinitions() {
6
- return [
7
- {
8
- name: 'spaces_list',
9
- description: 'List authorized Beepflow spaces for current JWT.',
10
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
11
- call: async () => {
12
- const spaces = await listAuthorizedSpaces();
13
- return { content: [{ type: 'text', text: JSON.stringify(spaces, null, 2) }] };
14
- },
15
- },
16
- {
17
- name: 'auth_email_request_code',
18
- description: 'Request email login code via Beepflow auth service.',
19
- inputSchema: {
20
- type: 'object',
21
- additionalProperties: false,
22
- required: ['email'],
23
- properties: { email: { type: 'string' } },
24
- },
25
- call: async (argumentsValue) => {
26
- const email = readStringField(argumentsValue, 'email');
27
- await requestEmailLoginCode(email);
28
- return { content: [{ type: 'text', text: `code sent to ${email}` }] };
29
- },
30
- },
31
- {
32
- name: 'auth_email_verify_code',
33
- description: 'Verify email login code, persist JWT for workspace.',
34
- inputSchema: {
35
- type: 'object',
36
- additionalProperties: false,
37
- required: ['email', 'code'],
38
- properties: { email: { type: 'string' }, code: { type: 'string' } },
39
- },
40
- call: async (argumentsValue) => {
41
- const email = readStringField(argumentsValue, 'email');
42
- const code = readStringField(argumentsValue, 'code');
43
- const tokenSummary = await verifyEmailLoginCode(email, code);
44
- return { content: [{ type: 'text', text: JSON.stringify(tokenSummary, null, 2) }] };
45
- },
46
- },
47
- {
48
- name: 'auth_google_start',
49
- description: 'Create Google OAuth login URL + request id.',
50
- inputSchema: {
51
- type: 'object',
52
- additionalProperties: false,
53
- properties: { scopes: { type: 'string' } },
54
- },
55
- call: async (argumentsValue) => {
56
- const scopes = readOptionalStringField(argumentsValue, 'scopes') ?? 'openid email profile';
57
- const flow = await createGoogleLoginUrl(scopes);
58
- return { content: [{ type: 'text', text: JSON.stringify(flow, null, 2) }] };
59
- },
60
- },
61
- {
62
- name: 'auth_google_get_token',
63
- description: 'Fetch Google OAuth token using request id, persist JWT for workspace.',
64
- inputSchema: {
65
- type: 'object',
66
- additionalProperties: false,
67
- required: ['requestId'],
68
- properties: { requestId: { type: 'string' } },
69
- },
70
- call: async (argumentsValue) => {
71
- const requestId = readStringField(argumentsValue, 'requestId');
72
- const tokenSummary = await fetchGoogleLoginToken(requestId);
73
- return { content: [{ type: 'text', text: JSON.stringify(tokenSummary, null, 2) }] };
74
- },
75
- },
76
- {
77
- name: 'auth_sign_out',
78
- description: 'Delete stored JWT for current workspace.',
79
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
80
- call: async () => {
81
- const outcome = await deleteWorkspaceToken();
82
- return { content: [{ type: 'text', text: JSON.stringify(outcome, null, 2) }] };
83
- },
84
- },
85
- ];
86
- }
87
- function readStringField(argumentsValue, fieldName) {
88
- if (!argumentsValue || typeof argumentsValue !== 'object') {
89
- throw new Error(`invalid tool arguments: expected object`);
90
- }
91
- const record = argumentsValue;
92
- const value = record[fieldName];
93
- if (typeof value !== 'string' || !value.trim()) {
94
- throw new Error(`invalid tool arguments: ${fieldName} must be non-empty string`);
95
- }
96
- return value;
97
- }
98
- function readOptionalStringField(argumentsValue, fieldName) {
99
- if (argumentsValue === undefined)
100
- return undefined;
101
- if (!argumentsValue || typeof argumentsValue !== 'object') {
102
- throw new Error(`invalid tool arguments: expected object`);
103
- }
104
- const record = argumentsValue;
105
- const value = record[fieldName];
106
- if (value === undefined || value === null)
107
- return undefined;
108
- if (typeof value !== 'string') {
109
- throw new Error(`invalid tool arguments: ${fieldName} must be string`);
110
- }
111
- return value;
112
- }
@@ -1,40 +0,0 @@
1
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
2
- import { persistWorkspaceAccessToken, summarizeStoredToken } from "./workspace-token-store.js";
3
- export async function requestEmailLoginCode(email) {
4
- const config = readBeepflowWorkspaceConfig();
5
- const response = await fetch(`${config.authUrl}/email/request-code`, {
6
- method: 'POST',
7
- headers: {
8
- 'content-type': 'application/json',
9
- domain: config.authDomain,
10
- },
11
- body: JSON.stringify({ email }),
12
- });
13
- if (!response.ok) {
14
- const body = await response.text().catch(() => '');
15
- throw new Error(`email/request-code failed: ${response.status} ${body}`.trim());
16
- }
17
- }
18
- export async function verifyEmailLoginCode(email, code) {
19
- const config = readBeepflowWorkspaceConfig();
20
- const response = await fetch(`${config.authUrl}/email/verify-code`, {
21
- method: 'POST',
22
- headers: {
23
- 'content-type': 'application/json',
24
- domain: config.authDomain,
25
- },
26
- body: JSON.stringify({ email, code }),
27
- });
28
- const payload = await response.json().catch(() => undefined);
29
- if (!response.ok) {
30
- throw new Error(`email/verify-code failed: ${response.status}`);
31
- }
32
- const accessToken = typeof payload?.access_token === 'string' ? payload.access_token : '';
33
- if (!accessToken)
34
- throw new Error('email/verify-code failed: missing access_token');
35
- const storedToken = await persistWorkspaceAccessToken(accessToken);
36
- return {
37
- workspaceUrl: config.workspaceUrl,
38
- ...summarizeStoredToken(storedToken),
39
- };
40
- }
@@ -1,31 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
3
- import { persistWorkspaceAccessToken, summarizeStoredToken } from "./workspace-token-store.js";
4
- export async function createGoogleLoginUrl(scopes) {
5
- const config = readBeepflowWorkspaceConfig();
6
- const requestId = crypto.randomUUID();
7
- const url = new URL(`${config.authUrl}/google-form`);
8
- url.searchParams.set('requestId', requestId);
9
- url.searchParams.set('scopes', scopes);
10
- return { requestId, url: url.toString() };
11
- }
12
- export async function fetchGoogleLoginToken(requestId) {
13
- const config = readBeepflowWorkspaceConfig();
14
- const url = new URL(`${config.authUrl}/google-auth-token`);
15
- url.searchParams.set('id', requestId);
16
- const response = await fetch(url.toString(), {
17
- headers: { domain: config.authDomain },
18
- });
19
- const payload = await response.json().catch(() => undefined);
20
- if (!response.ok) {
21
- throw new Error(`google-auth-token failed: ${response.status}`);
22
- }
23
- const accessToken = typeof payload?.access_token === 'string' ? payload.access_token : '';
24
- if (!accessToken)
25
- throw new Error('google-auth-token failed: missing access_token');
26
- const storedToken = await persistWorkspaceAccessToken(accessToken);
27
- return {
28
- workspaceUrl: config.workspaceUrl,
29
- ...summarizeStoredToken(storedToken),
30
- };
31
- }
@@ -1,31 +0,0 @@
1
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
2
- import { decodeJwtWithoutVerification } from "./workspace-jwt.js";
3
- import { persistWorkspaceAccessToken } from "./workspace-token-store.js";
4
- export async function refreshAccessTokenIfNeeded(accessToken) {
5
- const jwtDetails = decodeJwtWithoutVerification(accessToken);
6
- if (jwtDetails.provider !== 'google')
7
- return accessToken;
8
- if (!jwtDetails.exp)
9
- return accessToken;
10
- const nowMs = Date.now();
11
- const expiresAtMs = jwtDetails.exp * 1000;
12
- const refreshThresholdMs = 5 * 60 * 1000;
13
- if (expiresAtMs - nowMs >= refreshThresholdMs) {
14
- return accessToken;
15
- }
16
- const config = readBeepflowWorkspaceConfig();
17
- const response = await fetch(`${config.authUrl}/google-refresh`, {
18
- method: 'GET',
19
- headers: {
20
- Authorization: `Bearer ${accessToken}`,
21
- },
22
- });
23
- const payload = await response.json().catch(() => undefined);
24
- if (!response.ok)
25
- return accessToken;
26
- const refreshed = typeof payload?.access_token === 'string' ? payload.access_token : '';
27
- if (!refreshed)
28
- return accessToken;
29
- await persistWorkspaceAccessToken(refreshed);
30
- return refreshed;
31
- }
@@ -1,71 +0,0 @@
1
- import os from 'node:os';
2
- import path from 'node:path';
3
- import { readBeepflowEnvironment } from "./environment.js";
4
- export function readBeepflowWorkspaceConfig() {
5
- const args = process.argv.slice(2);
6
- const environment = readBeepflowEnvironment(args);
7
- const workspaceUrl = coalesceNonEmptyString('workspace url', [
8
- readFlagValue(args, '--workspace-url'),
9
- process.env.BEEPFLOW_WORKSPACE_URL,
10
- environment.workspaceUrl,
11
- ]);
12
- const authUrl = coalesceNonEmptyString('auth url', [
13
- readFlagValue(args, '--auth-url'),
14
- process.env.BEEPFLOW_AUTH_URL,
15
- environment.authUrl,
16
- ]);
17
- const authDomain = coalesceNonEmptyString('auth domain', [
18
- readFlagValue(args, '--auth-domain'),
19
- process.env.BEEPFLOW_AUTH_DOMAIN,
20
- environment.authDomain,
21
- ]);
22
- const tokenStorePath = readFlagValue(args, '--token-store-path') ??
23
- process.env.BEEPFLOW_TOKEN_STORE_PATH ??
24
- computeDefaultTokenStorePath();
25
- return {
26
- environmentName: environment.name,
27
- workspaceUrl: workspaceUrl.replace(/\/+$/, ''),
28
- authUrl: authUrl.replace(/\/+$/, ''),
29
- authDomain,
30
- jwtFromEnvironment: readFlagValue(args, '--jwt') ?? process.env.BEEPFLOW_JWT,
31
- tokenStorePath,
32
- };
33
- }
34
- function readFlagValue(args, flagName) {
35
- for (let i = 0; i < args.length; i += 1) {
36
- const arg = args[i];
37
- if (arg === flagName) {
38
- const value = args[i + 1];
39
- if (!value) {
40
- throw new Error(`missing value for ${flagName}`);
41
- }
42
- return value;
43
- }
44
- const equalsPrefix = `${flagName}=`;
45
- if (arg && arg.startsWith(equalsPrefix)) {
46
- const value = arg.slice(equalsPrefix.length);
47
- if (!value)
48
- throw new Error(`missing value for ${flagName}`);
49
- return value;
50
- }
51
- }
52
- }
53
- function computeDefaultTokenStorePath() {
54
- const appData = process.env.APPDATA;
55
- if (appData) {
56
- return path.join(appData, 'beepflow-mcp', 'workspace-access-tokens.json');
57
- }
58
- const xdgConfigHome = process.env.XDG_CONFIG_HOME;
59
- if (xdgConfigHome) {
60
- return path.join(xdgConfigHome, 'beepflow-mcp', 'workspace-access-tokens.json');
61
- }
62
- return path.join(os.homedir(), '.config', 'beepflow-mcp', 'workspace-access-tokens.json');
63
- }
64
- function coalesceNonEmptyString(valueName, values) {
65
- for (const value of values) {
66
- if (typeof value === 'string' && value.trim()) {
67
- return value;
68
- }
69
- }
70
- throw new Error(`missing ${valueName}`);
71
- }
@@ -1,16 +0,0 @@
1
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
2
- export async function assertWorkspaceMembership(accessToken) {
3
- const config = readBeepflowWorkspaceConfig();
4
- const response = await fetch(`${config.workspaceUrl}/users/is-member`, {
5
- method: 'GET',
6
- headers: { Authorization: `Bearer ${accessToken}` },
7
- });
8
- if (!response.ok) {
9
- const body = await response.text().catch(() => '');
10
- throw new Error(`users/is-member failed: ${response.status} ${body}`.trim());
11
- }
12
- const payload = (await response.json().catch(() => undefined));
13
- if (payload?.isMember !== true) {
14
- throw new Error('user not member of workspace');
15
- }
16
- }
@@ -1,46 +0,0 @@
1
- import crypto from 'node:crypto';
2
- export function decodeJwtWithoutVerification(accessToken) {
3
- const parts = accessToken.split('.');
4
- if (parts.length < 2) {
5
- return { accessToken, email: undefined, exp: undefined, provider: undefined };
6
- }
7
- const payloadJson = decodeBase64UrlToUtf8(parts[1] ?? '');
8
- if (!payloadJson) {
9
- return { accessToken, email: undefined, exp: undefined, provider: undefined };
10
- }
11
- let claims;
12
- try {
13
- claims = JSON.parse(payloadJson);
14
- }
15
- catch {
16
- return { accessToken, email: undefined, exp: undefined, provider: undefined };
17
- }
18
- const email = typeof claims.email === 'string' ? claims.email.toLowerCase() : undefined;
19
- const exp = typeof claims.exp === 'number' ? claims.exp : undefined;
20
- const provider = typeof claims.provider === 'string' ? claims.provider : undefined;
21
- return { accessToken, email, exp, provider };
22
- }
23
- export function maskToken(accessToken) {
24
- if (!accessToken)
25
- return '';
26
- if (accessToken.length <= 12)
27
- return `${accessToken.slice(0, 4)}…`;
28
- return `${accessToken.slice(0, 6)}…${accessToken.slice(-4)}`;
29
- }
30
- export function createSessionId() {
31
- const bytes = crypto.randomBytes(9);
32
- return bytes.toString('base64url');
33
- }
34
- function decodeBase64UrlToUtf8(value) {
35
- if (!value)
36
- return undefined;
37
- const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
38
- const paddingLength = (4 - (base64.length % 4)) % 4;
39
- const padded = base64 + '='.repeat(paddingLength);
40
- try {
41
- return Buffer.from(padded, 'base64').toString('utf8');
42
- }
43
- catch {
44
- return undefined;
45
- }
46
- }
@@ -1,27 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { decodeJwtWithoutVerification, maskToken } from "./workspace-jwt.js";
4
- test('decodeJwtWithoutVerification reads email, exp, provider', () => {
5
- const payload = { email: 'USER@EXAMPLE.COM', exp: 123, provider: 'google' };
6
- const token = [
7
- toBase64Url(JSON.stringify({ alg: 'none', typ: 'JWT' })),
8
- toBase64Url(JSON.stringify(payload)),
9
- '',
10
- ].join('.');
11
- const decoded = decodeJwtWithoutVerification(token);
12
- assert.equal(decoded.email, 'user@example.com');
13
- assert.equal(decoded.exp, 123);
14
- assert.equal(decoded.provider, 'google');
15
- });
16
- test('maskToken hides most token bytes', () => {
17
- assert.equal(maskToken(''), '');
18
- assert.equal(maskToken('short'), 'shor…');
19
- assert.equal(maskToken('abcdefghijklmnopqrstuvwxyz'), 'abcdef…wxyz');
20
- });
21
- function toBase64Url(value) {
22
- return Buffer.from(value, 'utf8')
23
- .toString('base64')
24
- .replace(/=/g, '')
25
- .replace(/\+/g, '-')
26
- .replace(/\//g, '_');
27
- }
@@ -1,44 +0,0 @@
1
- import { assertWorkspaceMembership } from "./workspace-http-membership.js";
2
- import { readWorkspaceAccessToken } from "./workspace-token-store.js";
3
- import { createSessionId } from "./workspace-jwt.js";
4
- import { refreshAccessTokenIfNeeded } from "./workspace-auth-refresh.js";
5
- import { requestReplicatedResourceSnapshot } from "./workspace-websocket.js";
6
- export async function listAuthorizedSpaces() {
7
- const accessTokenFromStore = await readWorkspaceAccessToken();
8
- if (!accessTokenFromStore) {
9
- throw new Error('missing jwt: set BEEPFLOW_JWT or run auth tool to persist token');
10
- }
11
- const accessToken = await refreshAccessTokenIfNeeded(accessTokenFromStore);
12
- await assertWorkspaceMembership(accessToken);
13
- const sessionId = createSessionId();
14
- const snapshot = await requestReplicatedResourceSnapshot(accessToken, sessionId, 'spaces', { all: true }, 15_000);
15
- const spaces = [];
16
- for (const key in snapshot) {
17
- const value = snapshot[key];
18
- if (!value || typeof value !== 'object')
19
- continue;
20
- const record = value;
21
- const idValue = record['id'];
22
- if (typeof idValue !== 'string' || !idValue)
23
- continue;
24
- const space = {
25
- id: idValue,
26
- updatedAt: typeof record['updatedAt'] === 'number' ? record['updatedAt'] : undefined,
27
- label: typeof record['label'] === 'string' ? record['label'] : undefined,
28
- thumbnailUrl: typeof record['thumbnailUrl'] === 'string' ? record['thumbnailUrl'] : undefined,
29
- description: typeof record['description'] === 'string' ? record['description'] : undefined,
30
- isUserSpace: typeof record['isUserSpace'] === 'boolean' ? record['isUserSpace'] : undefined,
31
- };
32
- spaces.push(space);
33
- }
34
- spaces.sort((a, b) => {
35
- const labelA = (a.label ?? '').toLowerCase();
36
- const labelB = (b.label ?? '').toLowerCase();
37
- if (labelA < labelB)
38
- return -1;
39
- if (labelA > labelB)
40
- return 1;
41
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
42
- });
43
- return spaces;
44
- }
@@ -1,64 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { decodeJwtWithoutVerification, maskToken } from "./workspace-jwt.js";
4
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
5
- export async function readWorkspaceAccessToken() {
6
- const config = readBeepflowWorkspaceConfig();
7
- if (config.jwtFromEnvironment)
8
- return config.jwtFromEnvironment;
9
- const stored = await readStoredTokenForWorkspaceUrl(config.tokenStorePath, config.workspaceUrl);
10
- return stored?.accessToken;
11
- }
12
- export async function persistWorkspaceAccessToken(accessToken) {
13
- const config = readBeepflowWorkspaceConfig();
14
- const jwtDetails = decodeJwtWithoutVerification(accessToken);
15
- const storedToken = {
16
- accessToken,
17
- email: jwtDetails.email,
18
- provider: jwtDetails.provider,
19
- exp: jwtDetails.exp,
20
- };
21
- await upsertStoredTokenForWorkspaceUrl(config.tokenStorePath, config.workspaceUrl, storedToken);
22
- return storedToken;
23
- }
24
- export async function deleteWorkspaceToken() {
25
- const config = readBeepflowWorkspaceConfig();
26
- const tokens = await readTokenMap(config.tokenStorePath);
27
- const hadToken = tokens[config.workspaceUrl] !== undefined;
28
- delete tokens[config.workspaceUrl];
29
- await writeTokenMap(config.tokenStorePath, tokens);
30
- return { workspaceUrl: config.workspaceUrl, deleted: hadToken };
31
- }
32
- export function summarizeStoredToken(storedToken) {
33
- return {
34
- email: storedToken.email,
35
- provider: storedToken.provider,
36
- exp: storedToken.exp,
37
- accessToken: maskToken(storedToken.accessToken),
38
- };
39
- }
40
- async function readStoredTokenForWorkspaceUrl(tokenStorePath, workspaceUrl) {
41
- const tokens = await readTokenMap(tokenStorePath);
42
- return tokens[workspaceUrl];
43
- }
44
- async function upsertStoredTokenForWorkspaceUrl(tokenStorePath, workspaceUrl, storedToken) {
45
- const tokens = await readTokenMap(tokenStorePath);
46
- tokens[workspaceUrl] = storedToken;
47
- await writeTokenMap(tokenStorePath, tokens);
48
- }
49
- async function readTokenMap(tokenStorePath) {
50
- try {
51
- const raw = await fs.readFile(tokenStorePath, 'utf8');
52
- const parsed = JSON.parse(raw);
53
- if (!parsed || typeof parsed !== 'object')
54
- return {};
55
- return parsed;
56
- }
57
- catch {
58
- return {};
59
- }
60
- }
61
- async function writeTokenMap(tokenStorePath, tokens) {
62
- await fs.mkdir(path.dirname(tokenStorePath), { recursive: true });
63
- await fs.writeFile(tokenStorePath, JSON.stringify(tokens, null, 2) + '\n', 'utf8');
64
- }
@@ -1,85 +0,0 @@
1
- import { readBeepflowWorkspaceConfig } from "./workspace-config.js";
2
- export async function requestReplicatedResourceSnapshot(accessToken, sessionId, resourceName, query, timeoutMs) {
3
- const config = readBeepflowWorkspaceConfig();
4
- const websocketUrl = computeWorkspaceWebsocketUrl(config.workspaceUrl, accessToken, sessionId);
5
- const websocket = await openWebsocket(websocketUrl, timeoutMs);
6
- try {
7
- const requestEvent = {
8
- resource: resourceName,
9
- action: 'snapshot',
10
- value: query,
11
- };
12
- websocket.send(formatChannelMessage('replicated-state', JSON.stringify(requestEvent)));
13
- const snapshot = await waitForSnapshotResponse(websocket, resourceName, timeoutMs);
14
- return snapshot;
15
- }
16
- finally {
17
- websocket.close();
18
- }
19
- }
20
- export function computeWorkspaceWebsocketUrl(workspaceUrl, accessToken, sessionId) {
21
- const wsBase = workspaceUrl.replace(/^http/, 'ws');
22
- const tokenPath = encodeURIComponent(accessToken);
23
- const sessionPath = encodeURIComponent(sessionId);
24
- return `${wsBase}/base/${tokenPath}/${sessionPath}`;
25
- }
26
- function formatChannelMessage(channel, jsonData) {
27
- return `${channel};${jsonData}`;
28
- }
29
- async function openWebsocket(websocketUrl, timeoutMs) {
30
- const websocket = new WebSocket(websocketUrl, 'echo');
31
- const openOutcome = await new Promise((resolve) => {
32
- const timeout = setTimeout(() => resolve({ ok: false, reason: 'timeout' }), timeoutMs);
33
- websocket.onopen = () => {
34
- clearTimeout(timeout);
35
- resolve({ ok: true });
36
- };
37
- websocket.onerror = () => {
38
- clearTimeout(timeout);
39
- resolve({ ok: false, reason: 'error' });
40
- };
41
- });
42
- if (!openOutcome.ok) {
43
- websocket.close();
44
- throw new Error(`websocket open failed: ${openOutcome.reason ?? 'unknown'}`);
45
- }
46
- return websocket;
47
- }
48
- async function waitForSnapshotResponse(websocket, resourceName, timeoutMs) {
49
- return await new Promise((resolve, reject) => {
50
- const timeout = setTimeout(() => reject(new Error('snapshot timeout')), timeoutMs);
51
- websocket.onerror = () => {
52
- clearTimeout(timeout);
53
- reject(new Error('websocket error during snapshot wait'));
54
- };
55
- websocket.onclose = () => {
56
- clearTimeout(timeout);
57
- reject(new Error('websocket closed during snapshot wait'));
58
- };
59
- websocket.onmessage = (event) => {
60
- const data = typeof event.data === 'string' ? event.data : '';
61
- const separatorIndex = data.indexOf(';');
62
- if (separatorIndex === -1)
63
- return;
64
- const channel = data.slice(0, separatorIndex);
65
- if (channel !== 'replicated-state')
66
- return;
67
- const jsonData = data.slice(separatorIndex + 1);
68
- let parsed;
69
- try {
70
- parsed = JSON.parse(jsonData);
71
- }
72
- catch {
73
- return;
74
- }
75
- if (parsed.action !== 'snapshot')
76
- return;
77
- if (parsed.resource !== resourceName)
78
- return;
79
- if (!parsed.value || typeof parsed.value !== 'object')
80
- return;
81
- clearTimeout(timeout);
82
- resolve(parsed.value);
83
- };
84
- });
85
- }
@@ -1,11 +0,0 @@
1
- import test from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { computeWorkspaceWebsocketUrl } from "./workspace-websocket.js";
4
- test('computeWorkspaceWebsocketUrl converts http to ws', () => {
5
- const url = computeWorkspaceWebsocketUrl('http://localhost:2451/api', 'token', 'sid');
6
- assert.equal(url, 'ws://localhost:2451/api/base/token/sid');
7
- });
8
- test('computeWorkspaceWebsocketUrl converts https to wss', () => {
9
- const url = computeWorkspaceWebsocketUrl('https://x.example/api', 'token', 'sid');
10
- assert.equal(url, 'wss://x.example/api/base/token/sid');
11
- });