@saiteja1123/mcp-server 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +59 -55
  2. package/src/api-scan.mjs +362 -93
  3. package/src/cli.js +771 -322
  4. package/src/deep-scan/contracts.js +201 -0
  5. package/src/deep-scan/deterministic-scan.js +337 -0
  6. package/src/deep-scan/index.js +109 -0
  7. package/src/deep-scan/project-map.js +507 -0
  8. package/src/deep-scan/ralph-accept.js +510 -0
  9. package/src/deep-scan/ralph-compare.js +498 -0
  10. package/src/deep-scan/ralph-tasks.js +598 -0
  11. package/src/deep-scan/ralph-track.js +548 -0
  12. package/src/deep-scan/registry.js +159 -0
  13. package/src/deep-scan/runtime.js +275 -0
  14. package/src/deep-scan/sample-steppers.js +128 -0
  15. package/src/deep-scan/sourceSafe.js +73 -0
  16. package/src/deep-scan/status.js +70 -0
  17. package/src/deep-scan/store.js +57 -0
  18. package/src/deep-scan/test-plan.js +760 -0
  19. package/src/index.js +6 -5
  20. package/src/lock.mjs +55 -14
  21. package/src/mcp-config.mjs +161 -0
  22. package/src/middleware/governance.js +135 -0
  23. package/src/orchestrator/runScan.js +211 -0
  24. package/src/project-bindings.mjs +215 -0
  25. package/src/rule-engine/index.js +2 -1
  26. package/src/rule-engine/localScan.js +39 -12
  27. package/src/rule-engine/metadata.js +20 -0
  28. package/src/rule-engine/prompt.js +6 -5
  29. package/src/rule-engine/rules.js +71 -43
  30. package/src/rule-engine/score.js +5 -4
  31. package/src/security/pathGuard.js +170 -0
  32. package/src/selftest.js +2473 -0
  33. package/src/server.js +109 -150
  34. package/src/tools/deepScan.js +286 -0
  35. package/src/tools/localScan.js +85 -0
  36. package/src/tools/projects.js +124 -0
  37. package/src/tools/scanFile.js +131 -0
package/package.json CHANGED
@@ -1,55 +1,59 @@
1
- {
2
- "name": "@saiteja1123/mcp-server",
3
- "version": "1.1.4",
4
- "private": false,
5
- "license": "MIT",
6
- "description": "Vibesecur MCP security scanner - one-folder locking, cross-IDE, Cursor/VSCode/Windsurf",
7
- "type": "module",
8
- "main": "./src/index.js",
9
- "bin": {
10
- "vibesecur-mcp": "./src/cli.js"
11
- },
12
- "files": [
13
- "src",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "start": "node ./src/server.js",
18
- "dev": "node --watch ./src/server.js",
19
- "bind": "node ./src/cli.js bind",
20
- "init": "node ./src/cli.js init",
21
- "doctor": "node ./src/cli.js doctor",
22
- "test": "node --input-type=module --eval \"import('./src/server.js')\""
23
- },
24
- "exports": {
25
- ".": "./src/index.js",
26
- "./server": "./src/server.js",
27
- "./lock": "./src/lock.mjs"
28
- },
29
- "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.29.0",
31
- "fast-glob": "^3.3.3",
32
- "zod": "^4.3.6"
33
- },
34
- "bundledDependencies": [],
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
- "engines": {
39
- "node": ">=20.0.0"
40
- },
41
- "keywords": [
42
- "mcp",
43
- "security",
44
- "vibesecur",
45
- "cursor",
46
- "windsurf",
47
- "scanner",
48
- "supabase-rls"
49
- ],
50
- "repository": {
51
- "type": "git",
52
- "url": "https://github.com/vibesecur/vibesecur"
53
- },
54
- "homepage": "https://vibesecur.com/docs/mcp-setup"
55
- }
1
+ {
2
+ "name": "@saiteja1123/mcp-server",
3
+ "version": "1.1.6",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "description": "Vibesecur MCP security scanner — universal account config, multi-project CRU, folder locking, cross-IDE",
7
+ "type": "module",
8
+ "main": "./src/index.js",
9
+ "bin": {
10
+ "vibesecur-mcp": "./src/cli.js"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node --env-file=.env ./src/server.js",
18
+ "dev": "node --env-file=.env --watch ./src/server.js",
19
+ "bind": "node ./src/cli.js bind",
20
+ "init": "node ./src/cli.js init",
21
+ "doctor": "node ./src/cli.js doctor",
22
+ "test": "node ./src/selftest.js",
23
+ "benchmark": "node ../../tests/security/benchmark-runner.js",
24
+ "benchmark:ci": "node ../../tests/security/benchmark-runner.js --ci",
25
+ "benchmark:json": "node ../../tests/security/benchmark-runner.js --json",
26
+ "benchmark:verbose": "node ../../tests/security/benchmark-runner.js --verbose"
27
+ },
28
+ "exports": {
29
+ ".": "./src/index.js",
30
+ "./server": "./src/server.js",
31
+ "./lock": "./src/lock.mjs"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "fast-glob": "^3.3.3",
36
+ "zod": "^4.3.6"
37
+ },
38
+ "bundledDependencies": [],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=20.0.0"
44
+ },
45
+ "keywords": [
46
+ "mcp",
47
+ "security",
48
+ "vibesecur",
49
+ "cursor",
50
+ "windsurf",
51
+ "scanner",
52
+ "supabase-rls"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/vibesecur/vibesecur"
57
+ },
58
+ "homepage": "https://vibesecur.com/docs/mcp-setup"
59
+ }
package/src/api-scan.mjs CHANGED
@@ -1,93 +1,362 @@
1
- import crypto from 'crypto';
2
- import path from 'path';
3
-
4
- function sha256Hex(input) {
5
- return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
6
- }
7
-
8
- export function getMcpSessionId() {
9
- const fromEnv = process.env.VIBESECUR_SESSION_ID;
10
- if (typeof fromEnv === 'string' && fromEnv.trim().length > 0) {
11
- return fromEnv.trim().slice(0, 64);
12
- }
13
- const basis = `mcp:${process.env.USER || process.env.USERNAME || 'anon'}:${process.cwd()}`;
14
- return sha256Hex(basis);
15
- }
16
-
17
- export function getProjectHashForPath(rootPath) {
18
- const resolved = path.resolve(rootPath || '.');
19
- return sha256Hex(`vibesecur:mcp:${resolved}`);
20
- }
21
-
22
- export function normalizeApiBase(raw) {
23
- if (!raw || typeof raw !== 'string') return '';
24
- const u = raw.trim().replace(/\/$/, '');
25
- return u.endsWith('/api/v1') ? u : `${u}/api/v1`;
26
- }
27
-
28
- /** GET origin (no /api/v1) for lightweight health checks. */
29
- export function getApiOriginFromBase(raw) {
30
- const normalized = normalizeApiBase(raw);
31
- if (!normalized) return '';
32
- return normalized.replace(/\/api\/v1\/?$/, '') || normalized;
33
- }
34
-
35
- /**
36
- * Quick reachability check (GET app root). Does not run a scan.
37
- * @param {string} [baseUrl] - e.g. https://vibesecur.onrender.com (optional /api/v1 ok)
38
- */
39
- export async function pingBackend(baseUrl) {
40
- const raw = (
41
- baseUrl
42
- || process.env.VIBESECUR_API_BASE
43
- || process.env.VIBESECUR_API_URL
44
- || 'https://vibesecur.onrender.com'
45
- ).trim();
46
- const origin = getApiOriginFromBase(raw);
47
- if (!origin) {
48
- return { ok: false, skipped: true, message: 'No API base URL' };
49
- }
50
- const url = origin.endsWith('/') ? origin.slice(0, -1) : origin;
51
- try {
52
- const ctrl = typeof AbortSignal !== 'undefined' && AbortSignal.timeout
53
- ? AbortSignal.timeout(12000)
54
- : undefined;
55
- const res = await fetch(`${url}/`, { method: 'GET', signal: ctrl });
56
- const ok = true;
57
- return { ok, status: res.status, url: `${url}/` };
58
- } catch (e) {
59
- return { ok: false, error: e.message };
60
- }
61
- }
62
-
63
- export async function postRemoteLocalScan({
64
- code,
65
- lang = 'auto',
66
- projectRoot = '.',
67
- platform = 'mcp',
68
- token,
69
- } = {}) {
70
- const apiBase = normalizeApiBase(process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '');
71
- if (!apiBase) {
72
- return { skipped: true, reason: 'VIBESECUR_API_BASE not set' };
73
- }
74
-
75
- const projectHash = getProjectHashForPath(projectRoot);
76
- const headers = {
77
- 'Content-Type': 'application/json',
78
- 'x-session-id': getMcpSessionId(),
79
- };
80
- const bearer = token || process.env.VIBESECUR_AUTH_TOKEN || process.env.VIBESECUR_TOKEN;
81
- if (bearer) {
82
- headers.Authorization = bearer.startsWith('Bearer ') ? bearer : `Bearer ${bearer}`;
83
- }
84
-
85
- const res = await fetch(`${apiBase}/scan/local`, {
86
- method: 'POST',
87
- headers,
88
- body: JSON.stringify({ code, lang, platform, projectHash }),
89
- });
90
-
91
- const json = await res.json().catch(() => ({}));
92
- return { apiBase, status: res.status, ok: res.ok, json };
93
- }
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+
4
+ function sha256Hex(input) {
5
+ return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
6
+ }
7
+
8
+ export function getMcpSessionId() {
9
+ const fromEnv = process.env.VIBESECUR_SESSION_ID;
10
+ if (typeof fromEnv === 'string' && fromEnv.trim().length > 0) {
11
+ return fromEnv.trim().slice(0, 64);
12
+ }
13
+ const basis = `mcp:${process.env.USER || process.env.USERNAME || 'anon'}:${process.cwd()}`;
14
+ return sha256Hex(basis);
15
+ }
16
+
17
+ export function getProjectHashForPath(rootPath) {
18
+ const resolved = path.resolve(rootPath || '.');
19
+ return sha256Hex(`vibesecur:mcp:${resolved}`);
20
+ }
21
+
22
+ export function normalizeLockedRoot(rawPath = '') {
23
+ if (typeof rawPath !== 'string') return '';
24
+ const trimmed = rawPath.trim();
25
+ if (!trimmed) return '';
26
+ const slashed = trimmed.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
27
+ const withoutTrailing = slashed.replace(/\/+$/, '');
28
+ return withoutTrailing.toLowerCase();
29
+ }
30
+
31
+ export function buildLockedRootHash(lockedRootPath) {
32
+ return sha256Hex(`vibesecur:mcp:root:${normalizeLockedRoot(lockedRootPath)}`);
33
+ }
34
+
35
+ export function buildLockedProjectHash(lockedRootHash) {
36
+ return sha256Hex(`vibesecur:mcp:project:${String(lockedRootHash || '').toLowerCase()}`);
37
+ }
38
+
39
+ export function normalizeApiBase(raw) {
40
+ if (!raw || typeof raw !== 'string') return '';
41
+ const u = raw.trim().replace(/\/$/, '');
42
+ return u.endsWith('/api/v1') ? u : `${u}/api/v1`;
43
+ }
44
+
45
+ /** GET origin (no /api/v1) for lightweight health checks. */
46
+ export function getApiOriginFromBase(raw) {
47
+ const normalized = normalizeApiBase(raw);
48
+ if (!normalized) return '';
49
+ return normalized.replace(/\/api\/v1\/?$/, '') || normalized;
50
+ }
51
+
52
+ /**
53
+ * Quick reachability check (GET app root). Does not run a scan.
54
+ * @param {string} [baseUrl] - e.g. https://vibesecur.onrender.com (optional /api/v1 ok)
55
+ */
56
+ export async function pingBackend(baseUrl) {
57
+ const raw = (
58
+ baseUrl
59
+ || process.env.VIBESECUR_API_BASE
60
+ || process.env.VIBESECUR_API_URL
61
+ || 'https://vibesecur.onrender.com'
62
+ ).trim();
63
+ const origin = getApiOriginFromBase(raw);
64
+ if (!origin) {
65
+ return { ok: false, skipped: true, message: 'No API base URL' };
66
+ }
67
+ const url = origin.endsWith('/') ? origin.slice(0, -1) : origin;
68
+ try {
69
+ const ctrl = typeof AbortSignal !== 'undefined' && AbortSignal.timeout
70
+ ? AbortSignal.timeout(12000)
71
+ : undefined;
72
+ const res = await fetch(`${url}/`, { method: 'GET', signal: ctrl });
73
+ const ok = true;
74
+ return { ok, status: res.status, url: `${url}/` };
75
+ } catch (e) {
76
+ return { ok: false, error: e.message };
77
+ }
78
+ }
79
+
80
+ function resolveAuthToken(authToken) {
81
+ const token = (
82
+ authToken
83
+ || process.env.VIBESECUR_AUTH_TOKEN
84
+ || process.env.VIBESECUR_TOKEN
85
+ );
86
+ if (!token || typeof token !== 'string') return '';
87
+ return token.trim();
88
+ }
89
+
90
+ function debugLog(message, payload) {
91
+ if (process.env.VIBESECUR_DEBUG !== '1') return;
92
+ process.stderr.write(`[vibesecur-debug] ${message}: ${JSON.stringify(payload)}\n`);
93
+ }
94
+
95
+ export async function requestServerBind({
96
+ lockedRootPath,
97
+ authToken,
98
+ apiBase,
99
+ } = {}) {
100
+ const normalizedApiBase = normalizeApiBase(
101
+ apiBase || process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '',
102
+ );
103
+ if (!normalizedApiBase) {
104
+ return { ok: false, skipped: true, error: 'VIBESECUR_API_BASE not set' };
105
+ }
106
+ const bearer = resolveAuthToken(authToken);
107
+ if (!bearer) {
108
+ return { ok: false, skipped: true, error: 'Missing auth token (set VIBESECUR_AUTH_TOKEN)' };
109
+ }
110
+ try {
111
+ const res = await fetch(`${normalizedApiBase}/mcp/bind`, {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ Authorization: bearer.startsWith('Bearer ') ? bearer : `Bearer ${bearer}`,
116
+ },
117
+ body: JSON.stringify({ lockedRootPath }),
118
+ });
119
+ const json = await res.json().catch(() => ({}));
120
+ return { apiBase: normalizedApiBase, status: res.status, ok: res.ok, json };
121
+ } catch (e) {
122
+ return { apiBase: normalizedApiBase, ok: false, error: e.message };
123
+ }
124
+ }
125
+
126
+ export async function verifyInstallBinding({
127
+ installToken,
128
+ lockedRootHash,
129
+ apiBase,
130
+ } = {}) {
131
+ const normalizedApiBase = normalizeApiBase(
132
+ apiBase || process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '',
133
+ );
134
+ if (!normalizedApiBase) {
135
+ return { ok: false, skipped: true, error: 'VIBESECUR_API_BASE not set' };
136
+ }
137
+ debugLog('verifyInstallBinding payload', {
138
+ apiBase: normalizedApiBase,
139
+ lockedRootHash,
140
+ installTokenHashPrefix: installToken ? sha256Hex(installToken).slice(0, 12) : null,
141
+ });
142
+ try {
143
+ const res = await fetch(`${normalizedApiBase}/mcp/verify-install`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({ installToken, lockedRootHash }),
147
+ });
148
+ const json = await res.json().catch(() => ({}));
149
+ debugLog('verifyInstallBinding response', {
150
+ apiBase: normalizedApiBase,
151
+ status: res.status,
152
+ ok: res.ok,
153
+ body: json,
154
+ });
155
+ return { apiBase: normalizedApiBase, status: res.status, ok: res.ok, json };
156
+ } catch (e) {
157
+ return { apiBase: normalizedApiBase, ok: false, error: e.message };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Persist scan metadata to /scan/log so the Projects dashboard reflects the
163
+ * latest MCP scan. Metadata only — never sends raw source. Best-effort: the
164
+ * caller treats failures as non-fatal because the scan itself already ran.
165
+ *
166
+ * Reuses the codeHash + scanReceipt from the preceding /scan/local response so
167
+ * the backend skips a second quota increment (same project + code within the
168
+ * receipt window).
169
+ */
170
+ export async function postScanLog({
171
+ result,
172
+ lang = 'auto',
173
+ projectRoot = '.',
174
+ platform = 'mcp',
175
+ installToken,
176
+ lockedRootHash,
177
+ authToken,
178
+ } = {}) {
179
+ const apiBase = normalizeApiBase(process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '');
180
+ if (!apiBase) {
181
+ return { skipped: true, reason: 'VIBESECUR_API_BASE not set' };
182
+ }
183
+ if (!result || typeof result !== 'object') {
184
+ return { skipped: true, reason: 'no scan result to log' };
185
+ }
186
+
187
+ const projectHash = /^[a-f0-9]{64}$/i.test(String(lockedRootHash || ''))
188
+ ? buildLockedProjectHash(String(lockedRootHash).toLowerCase())
189
+ : getProjectHashForPath(projectRoot);
190
+
191
+ const findings = Array.isArray(result.findings) ? result.findings : [];
192
+ const countSeverity = (sev) => findings.filter((f) => f && f.severity === sev).length;
193
+
194
+ const body = {
195
+ score: result.score,
196
+ grade: result.grade,
197
+ platform,
198
+ lang,
199
+ engine: result.engine === 'claude_ai' ? 'claude_ai' : 'local',
200
+ source: 'mcp',
201
+ projectHash,
202
+ installToken,
203
+ lockedRootHash,
204
+ criticalCount: countSeverity('critical'),
205
+ highCount: countSeverity('high'),
206
+ mediumCount: countSeverity('medium'),
207
+ findings,
208
+ };
209
+ if (/^[a-f0-9]{64}$/i.test(String(result.codeHash || ''))) {
210
+ body.codeHash = String(result.codeHash).toLowerCase();
211
+ }
212
+ if (typeof result.scanReceipt === 'string' && result.scanReceipt.length === 64) {
213
+ body.scanReceipt = result.scanReceipt;
214
+ }
215
+
216
+ const headers = {
217
+ 'Content-Type': 'application/json',
218
+ 'x-session-id': getMcpSessionId(),
219
+ };
220
+ const bearer = resolveAuthToken(authToken);
221
+ if (bearer) {
222
+ headers.Authorization = bearer.startsWith('Bearer ') ? bearer : `Bearer ${bearer}`;
223
+ }
224
+
225
+ try {
226
+ const res = await fetch(`${apiBase}/scan/log`, {
227
+ method: 'POST',
228
+ headers,
229
+ body: JSON.stringify(body),
230
+ });
231
+ const json = await res.json().catch(() => ({}));
232
+ return { apiBase, status: res.status, ok: res.ok, json };
233
+ } catch (e) {
234
+ return { apiBase, ok: false, error: e.message };
235
+ }
236
+ }
237
+
238
+ /**
239
+ * Persist aggregated repo/workspace scan metadata to /scan/log for the
240
+ * Projects dashboard. Used by scanRepo and scanCurrentWorkspace (not localScan).
241
+ */
242
+ export async function persistRepoScanLog({
243
+ aggregate,
244
+ findings = [],
245
+ projectRoot,
246
+ installToken,
247
+ lockedRootHash,
248
+ lang = 'auto',
249
+ authToken,
250
+ } = {}) {
251
+ const { getGrade } = await import('./rule-engine/index.js');
252
+ const score = aggregate?.summary?.score ?? 0;
253
+ return postScanLog({
254
+ result: {
255
+ score,
256
+ grade: getGrade(score),
257
+ findings,
258
+ },
259
+ lang,
260
+ projectRoot,
261
+ platform: 'mcp',
262
+ installToken,
263
+ lockedRootHash,
264
+ authToken,
265
+ });
266
+ }
267
+
268
+ export async function postRemoteLocalScan({
269
+ code,
270
+ lang = 'auto',
271
+ projectRoot = '.',
272
+ platform = 'mcp',
273
+ installToken,
274
+ lockedRootHash,
275
+ authToken,
276
+ } = {}) {
277
+ const apiBase = normalizeApiBase(process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '');
278
+ if (!apiBase) {
279
+ return { skipped: true, reason: 'VIBESECUR_API_BASE not set' };
280
+ }
281
+
282
+ const projectHash = /^[a-f0-9]{64}$/i.test(String(lockedRootHash || ''))
283
+ ? buildLockedProjectHash(String(lockedRootHash).toLowerCase())
284
+ : getProjectHashForPath(projectRoot);
285
+ const headers = {
286
+ 'Content-Type': 'application/json',
287
+ 'x-session-id': getMcpSessionId(),
288
+ };
289
+ const bearer = resolveAuthToken(authToken);
290
+ if (bearer) {
291
+ headers.Authorization = bearer.startsWith('Bearer ') ? bearer : `Bearer ${bearer}`;
292
+ }
293
+
294
+ try {
295
+ const res = await fetch(`${apiBase}/scan/local`, {
296
+ method: 'POST',
297
+ headers,
298
+ body: JSON.stringify({
299
+ code,
300
+ lang,
301
+ platform,
302
+ projectHash,
303
+ installToken,
304
+ lockedRootHash,
305
+ }),
306
+ });
307
+
308
+ const json = await res.json().catch(() => ({}));
309
+ return { apiBase, status: res.status, ok: res.ok, json };
310
+ } catch (e) {
311
+ return { apiBase, ok: false, error: e.message };
312
+ }
313
+ }
314
+
315
+ async function authedFetch(path, { method = 'GET', authToken, body } = {}) {
316
+ const normalizedApiBase = normalizeApiBase(
317
+ process.env.VIBESECUR_API_BASE || process.env.VIBESECUR_API_URL || '',
318
+ );
319
+ if (!normalizedApiBase) {
320
+ return { ok: false, skipped: true, error: 'VIBESECUR_API_BASE not set' };
321
+ }
322
+ const bearer = resolveAuthToken(authToken);
323
+ if (!bearer) {
324
+ return { ok: false, skipped: true, error: 'Missing auth token (set VIBESECUR_AUTH_TOKEN)' };
325
+ }
326
+ try {
327
+ const res = await fetch(`${normalizedApiBase}${path}`, {
328
+ method,
329
+ headers: {
330
+ 'Content-Type': 'application/json',
331
+ Authorization: bearer.startsWith('Bearer ') ? bearer : `Bearer ${bearer}`,
332
+ },
333
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
334
+ });
335
+ const json = await res.json().catch(() => ({}));
336
+ return { apiBase: normalizedApiBase, status: res.status, ok: res.ok, json };
337
+ } catch (e) {
338
+ return { apiBase: normalizedApiBase, ok: false, error: e.message };
339
+ }
340
+ }
341
+
342
+ /** List account projects (read). */
343
+ export async function listProjects({ authToken } = {}) {
344
+ return authedFetch('/projects', { authToken });
345
+ }
346
+
347
+ /**
348
+ * Create or update a project for a codebase folder (CRU — never delete).
349
+ * Used by MCP projectUpsert and universal scan binding resolution.
350
+ */
351
+ export async function upsertProject({
352
+ lockedRootPath,
353
+ name,
354
+ projectId,
355
+ authToken,
356
+ } = {}) {
357
+ return authedFetch('/projects/upsert', {
358
+ method: 'POST',
359
+ authToken,
360
+ body: { lockedRootPath, name, projectId },
361
+ });
362
+ }