@noimosai/platform-mcp 0.1.3 → 0.1.4

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 (93) hide show
  1. package/README.md +17 -48
  2. package/bin/noimos-platform-mcp.mjs +4 -5
  3. package/dist/cli/cli.js +567 -0
  4. package/dist/cli/command-help.js +395 -0
  5. package/dist/cli/download.js +72 -0
  6. package/dist/cli/index.js +3 -0
  7. package/dist/cli/input-error.js +48 -0
  8. package/dist/cli/local-agents.js +199 -0
  9. package/dist/cli/mcp-supervisor.js +136 -0
  10. package/dist/cli/noimos-cli.js +72 -0
  11. package/dist/cli/project-archive.js +27 -0
  12. package/dist/cli/project-files.js +25 -0
  13. package/dist/cli/project-sync.js +259 -0
  14. package/dist/cli/setup-lock.js +1 -0
  15. package/dist/cli/setup.js +347 -0
  16. package/dist/cli/skills.js +142 -0
  17. package/dist/cli/team-harness-targets.js +337 -0
  18. package/dist/cli/team-harness.js +1562 -0
  19. package/dist/cli/team-resources.js +222 -0
  20. package/dist/cli/team-skills.js +457 -0
  21. package/dist/cli/update-bootstrap.js +437 -0
  22. package/dist/cli/update-release.js +212 -0
  23. package/dist/cli/version.js +1 -0
  24. package/dist/cli/wallet.js +85 -0
  25. package/dist/client/agent-run.js +1425 -0
  26. package/dist/client/browser.js +905 -0
  27. package/dist/client/distribution.js +2 -0
  28. package/dist/client/evm-wallet.js +214 -0
  29. package/dist/client/execution-context.js +14 -0
  30. package/dist/client/failure-recovery.js +128 -0
  31. package/dist/client/index.js +4 -225
  32. package/dist/client/local-input.js +33 -12
  33. package/dist/client/local-profile-lock.js +52 -0
  34. package/dist/client/local-profile.js +181 -0
  35. package/dist/client/local-projects.js +131 -0
  36. package/dist/client/local-setup.js +93 -0
  37. package/dist/client/realtime-voice.js +184 -0
  38. package/dist/client/tool-name-hints.js +48 -0
  39. package/dist/client/wallet-access.js +214 -0
  40. package/dist/mcp/agent-server.js +345 -0
  41. package/dist/mcp/context.js +86 -0
  42. package/dist/mcp/host-schema.js +76 -0
  43. package/dist/mcp/icon.js +1 -0
  44. package/dist/mcp/instructions.js +26 -0
  45. package/dist/mcp/local-projects.js +42 -0
  46. package/dist/mcp/local-upload.js +7 -13
  47. package/dist/mcp/noimos-runtime.js +18 -0
  48. package/dist/mcp/noimos-server.js +38 -0
  49. package/dist/mcp/operations.js +40 -0
  50. package/dist/mcp/parallel-result.js +17 -0
  51. package/dist/mcp/project-sync.js +105 -0
  52. package/dist/mcp/results-reply.js +15 -0
  53. package/dist/mcp/runtime.js +65 -11
  54. package/dist/mcp/server.js +382 -40
  55. package/dist/mcp/team-skill.js +63 -0
  56. package/dist/mcp/update-notice.js +75 -0
  57. package/dist/mcp/version.js +1 -0
  58. package/dist/schema/accountRequestMethods.js +1 -0
  59. package/dist/schema/boundedBody.js +30 -0
  60. package/dist/schema/cronValidator.js +150 -0
  61. package/dist/schema/platformApiKey.js +14 -0
  62. package/dist/schema/platformAppAuth.js +11 -0
  63. package/dist/schema/platformBillingConstants.js +4 -0
  64. package/dist/schema/platformCapabilityFamilies.js +293 -0
  65. package/dist/schema/platformConnectionCatalog.js +39 -0
  66. package/dist/schema/platformConnectionPolicy.js +33 -0
  67. package/dist/schema/platformCustomTools.js +148 -0
  68. package/dist/schema/platformEndUserApi.js +62 -0
  69. package/dist/schema/platformFunding.js +10 -0
  70. package/dist/schema/platformHttpConstraints.js +12 -0
  71. package/dist/schema/platformLocalSetup.js +25 -0
  72. package/dist/schema/platformMcpTools.js +7 -0
  73. package/dist/schema/platformMetaTools.js +63 -0
  74. package/dist/schema/platformMonitorSchedule.js +123 -0
  75. package/dist/schema/platformMonitors.js +159 -0
  76. package/dist/schema/platformOAuth.js +13 -0
  77. package/dist/schema/platformProjectPermissions.js +33 -0
  78. package/dist/schema/platformPublicExecution.js +202 -21
  79. package/dist/schema/platformRoutes.js +122 -0
  80. package/dist/schema/platformSkills.js +11 -0
  81. package/dist/schema/platformTeamSecretName.js +3 -0
  82. package/dist/schema/platformTeamSkillSlug.js +1 -0
  83. package/dist/schema/platformTeamSkills.js +302 -0
  84. package/dist/schema/platformToolAccess.js +62 -0
  85. package/dist/schema/platformTriggers.js +153 -0
  86. package/dist/schema/platformWorkflows.js +100 -0
  87. package/dist/schema/scheduleTimeZone.js +15 -0
  88. package/dist/schema/toolDiscovery.js +71 -0
  89. package/dist/schema/toolInputRepair.js +122 -0
  90. package/dist/schema/toolResultContext.js +51 -0
  91. package/dist/schema/untrustedContent.js +35 -0
  92. package/npm-shrinkwrap.json +73 -0
  93. package/package.json +5 -3
@@ -1,28 +1,49 @@
1
1
  import { open } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
- import { extname } from "node:path";
4
- import { platformInputMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "./index.js";
5
- const extensions = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".mp4": "video/mp4", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp" };
6
- export async function uploadLocalInput(client, path) {
7
- const mimeType = platformInputMimeSchema.safeParse(extensions[extname(path).toLowerCase()]);
3
+ import { basename, extname } from "node:path";
4
+ import { platformInputMimeSchema, platformInputFileMimeSchema, PLATFORM_INPUT_MAX_BYTES } from "./index.js";
5
+ const sensitiveDirectories = new Set([
6
+ ".ssh", ".aws", ".gcloud", ".kube", ".config", ".git", ".docker", ".gnupg", ".azure", ".password-store",
7
+ ".claude", ".agents", ".codex", ".cursor", "_sessions",
8
+ ]);
9
+ const sensitiveNames = new Set([
10
+ ".env", ".npmrc", ".netrc", ".git-credentials", ".pgpass", ".pypirc",
11
+ "credentials.json", "application_default_credentials.json", "id_rsa", "id_ed25519", "logins.json", "secrets.json",
12
+ ]);
13
+ const sensitiveExtensions = new Set([".key", ".pem", ".p12", ".pfx", ".kdbx", ".db"]);
14
+ export function isSensitiveLocalPath(path) {
15
+ const name = basename(path).toLowerCase();
16
+ return path.split(/[\\/]/).some((part) => sensitiveDirectories.has(part.toLowerCase()))
17
+ || sensitiveNames.has(name) || name.startsWith(".env") || sensitiveExtensions.has(extname(name));
18
+ }
19
+ const extensions = { ".mp3": "audio/mpeg", ".wav": "audio/wav", ".mp4": "video/mp4", ".mov": "video/quicktime", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp",
20
+ ".gif": "image/gif", ".bmp": "image/bmp", ".pdf": "application/pdf", ".csv": "text/csv", ".tsv": "text/tab-separated-values", ".txt": "text/plain", ".json": "application/json", ".xml": "application/xml",
21
+ ".zip": "application/zip", ".gz": "application/gzip", ".glb": "model/gltf-binary", ".gltf": "model/gltf+json", ".srt": "application/x-subrip", ".vtt": "text/vtt", ".bin": "application/octet-stream" };
22
+ export async function uploadLocalInput(client, path, options = {}) {
23
+ const mimeType = platformInputFileMimeSchema.safeParse(options.mimeType ?? extensions[extname(path).toLowerCase()]);
8
24
  if (!mimeType.success)
9
- throw new Error("Supported input files: JPG, PNG, WebP, WAV, MP3, MP4.");
25
+ throw new Error(`Supported input files end in ${Object.keys(extensions).join(", ")}. Copy another file under the one that matches its content, such as .txt for text; only the SDK takes a mimeType.`);
26
+ const range = options.byteRange;
27
+ if (range && (!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end < range.start || range.end - range.start >= PLATFORM_INPUT_MAX_BYTES))
28
+ throw new Error("Input byte range must be nonnegative and at most 12 MiB.");
29
+ const purpose = options.purpose ?? (range || !platformInputMimeSchema.safeParse(mimeType.data).success ? "file" : undefined);
10
30
  let data;
11
31
  try {
12
32
  const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
13
33
  try {
14
34
  const stat = await file.stat();
15
- if (!stat.isFile() || stat.size < 1 || stat.size > PLATFORM_INPUT_MAX_BYTES)
35
+ if (!stat.isFile() || stat.size < 1 || (range ? range.end >= stat.size : stat.size > PLATFORM_INPUT_MAX_BYTES))
16
36
  throw new Error("Invalid file");
17
- const buffer = Buffer.alloc(stat.size + 1);
37
+ const size = range ? range.end - range.start + 1 : stat.size;
38
+ const buffer = Buffer.alloc(size + (range ? 0 : 1));
18
39
  let offset = 0;
19
40
  while (offset < buffer.length) {
20
- const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, null);
41
+ const { bytesRead } = await file.read(buffer, offset, buffer.length - offset, range ? range.start + offset : null);
21
42
  if (!bytesRead)
22
43
  break;
23
44
  offset += bytesRead;
24
45
  }
25
- if (offset !== stat.size)
46
+ if (offset !== size)
26
47
  throw new Error("File changed");
27
48
  data = buffer.subarray(0, offset).toString("base64");
28
49
  }
@@ -31,7 +52,7 @@ export async function uploadLocalInput(client, path) {
31
52
  }
32
53
  }
33
54
  catch {
34
- throw new Error("Input must be an existing regular file, at most 12 MiB. Symlinks are not accepted.");
55
+ throw new Error("Input must be an existing regular file or a selected chunk, at most 12 MiB. Symlinks are not accepted.");
35
56
  }
36
- return client.uploadInput({ mimeType: mimeType.data, data });
57
+ return client.uploadInput({ mimeType: mimeType.data, data, ...(purpose ? { purpose } : {}) });
37
58
  }
@@ -0,0 +1,52 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, unlink } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { z } from "zod";
5
+ import { profileDirectory, readPrivateJson } from "./local-profile.js";
6
+ const lockSchema = z.object({ managedBy: z.literal("relvo-cli"), pid: z.number().int().positive() }).strict();
7
+ export async function withLocalProfileLock(path, operation) {
8
+ await profileDirectory(path, true);
9
+ const lock = join(dirname(path), "setup.lock");
10
+ for (let attempt = 0; attempt < 3; attempt++) {
11
+ let handle;
12
+ try {
13
+ handle = await open(lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
14
+ }
15
+ catch (error) {
16
+ if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
17
+ throw new Error("Relvo local storage could not be locked safely.");
18
+ try {
19
+ const before = await lstat(lock), previous = lockSchema.parse(await readPrivateJson(lock));
20
+ try {
21
+ process.kill(previous.pid, 0);
22
+ }
23
+ catch (failure) {
24
+ if (failure instanceof Error && "code" in failure && failure.code === "ESRCH") {
25
+ const after = await lstat(lock);
26
+ if (before.ino === after.ino && before.dev === after.dev) {
27
+ await unlink(lock);
28
+ continue;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ catch {
34
+ throw new Error("Relvo setup lock is unsafe or invalid. No local configuration was changed.");
35
+ }
36
+ throw new Error("Another Relvo setup, project selection or update is using this profile. Wait for it to finish.");
37
+ }
38
+ const identity = await handle.stat();
39
+ try {
40
+ await handle.writeFile(JSON.stringify({ managedBy: "relvo-cli", pid: process.pid }));
41
+ await handle.sync();
42
+ return await operation();
43
+ }
44
+ finally {
45
+ await handle.close();
46
+ const current = await lstat(lock).catch(() => null);
47
+ if (current?.ino === identity.ino && current.dev === identity.dev)
48
+ await unlink(lock);
49
+ }
50
+ }
51
+ throw new Error("Relvo local storage is busy. Rerun the command.");
52
+ }
@@ -0,0 +1,181 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, open, realpath, rename, unlink } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { z } from "zod";
7
+ import { platformLocalSetupClientSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema } from "./local-setup.js";
8
+ import { configFromEnvironment, hasRelvoEnvironment } from "./index.js";
9
+ const registeredSchema = z.object({ client: z.enum(["claude", "codex"]), name: z.string().regex(/^relvo-[a-f0-9]{24}$/), command: z.string().min(1), args: z.array(z.string()).min(1) }).strict();
10
+ const common = { managedBy: z.literal("relvo-cli"), version: z.literal(1), apiUrl: z.string().url(), consoleUrl: z.string().url(), local: z.boolean(),
11
+ installationId: z.string().regex(/^[a-f0-9]{24}$/), registrations: z.array(registeredSchema).max(2).default([]) };
12
+ export const localProfileSchema = z.discriminatedUnion("state", [
13
+ z.object({ ...common, state: z.literal("pending"), client: platformLocalSetupClientSchema, request: platformLocalSetupStartSchema }).strict(),
14
+ z.object({ ...common, state: z.literal("ready"), grant: platformLocalSetupPollSchema.options[1], pendingAck: platformLocalSetupProofSchema.optional() }).strict(),
15
+ ]);
16
+ const installationSchema = z.object({ managedBy: z.literal("relvo-cli"), version: z.literal(1),
17
+ installationId: common.installationId, registrations: common.registrations }).strict();
18
+ export const localProfilePath = (home = homedir()) => join(home, ".config", "relvo", "profile.json");
19
+ function owned(mode, uid, privateMode) {
20
+ if (typeof process.getuid === "function" && uid !== process.getuid() || mode & (privateMode ? 0o077 : 0o022))
21
+ throw new Error("Relvo local storage has unsafe ownership or permissions.");
22
+ }
23
+ export async function safeDirectory(path, create = false, privateMode = false) {
24
+ if (!isAbsolute(path) || resolve(path) !== path)
25
+ throw new Error("Relvo local paths must be canonical absolute paths.");
26
+ try {
27
+ const info = await lstat(path);
28
+ if (!info.isDirectory() || info.isSymbolicLink())
29
+ throw new Error("Relvo local directories must not be symlinks.");
30
+ owned(info.mode, info.uid, privateMode);
31
+ }
32
+ catch (error) {
33
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT") || !create)
34
+ throw error;
35
+ await mkdir(path, { mode: privateMode ? 0o700 : 0o755 });
36
+ await safeDirectory(path, false, privateMode);
37
+ }
38
+ }
39
+ export async function profileDirectory(path = localProfilePath(), create = false) {
40
+ if (process.platform === "win32")
41
+ throw new Error("Relvo private local profiles currently require POSIX file permissions.");
42
+ if (!isAbsolute(path) || resolve(path) !== path || !/\/\.config\/relvo\/profile\.json$/.test(path))
43
+ throw new Error("Use the canonical managed Relvo profile path.");
44
+ const root = dirname(dirname(dirname(path)));
45
+ if (await realpath(root) !== root)
46
+ throw new Error("Relvo profile home must be a canonical directory.");
47
+ await safeDirectory(root);
48
+ await safeDirectory(join(root, ".config"), create);
49
+ await safeDirectory(dirname(path), create, true);
50
+ return dirname(path);
51
+ }
52
+ export async function readPrivateBytes(path, maximumBytes = 65536) {
53
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
54
+ try {
55
+ const info = await handle.stat();
56
+ owned(info.mode, info.uid, true);
57
+ if (!info.isFile() || info.size > maximumBytes || info.nlink !== 1)
58
+ throw new Error("Relvo private file is invalid.");
59
+ return await handle.readFile();
60
+ }
61
+ finally {
62
+ await handle.close();
63
+ }
64
+ }
65
+ export async function readPrivateText(path, maximumBytes = 65536) { return (await readPrivateBytes(path, maximumBytes)).toString("utf8"); }
66
+ export async function readPrivateJson(path, maximumBytes = 65536) { return JSON.parse(await readPrivateText(path, maximumBytes)); }
67
+ export async function writePrivateBytes(path, value, privateParent = true, maximumBytes = 65536) {
68
+ const directory = dirname(path);
69
+ await safeDirectory(directory, false, privateParent);
70
+ if (value.byteLength > maximumBytes)
71
+ throw new Error("Relvo private file exceeds its size limit.");
72
+ try {
73
+ await readPrivateBytes(path, maximumBytes);
74
+ }
75
+ catch (error) {
76
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
77
+ throw error;
78
+ }
79
+ const temporary = join(directory, `.relvo-${randomUUID()}.tmp`);
80
+ const handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
81
+ let closed = false;
82
+ try {
83
+ await handle.writeFile(value);
84
+ await handle.sync();
85
+ await handle.close();
86
+ closed = true;
87
+ await rename(temporary, path);
88
+ const parent = await open(directory, constants.O_RDONLY);
89
+ try {
90
+ await parent.sync();
91
+ }
92
+ finally {
93
+ await parent.close();
94
+ }
95
+ }
96
+ catch (error) {
97
+ await unlink(temporary).catch(() => { });
98
+ throw error;
99
+ }
100
+ finally {
101
+ if (!closed)
102
+ await handle.close();
103
+ }
104
+ }
105
+ export async function writePrivateText(path, value, privateParent = true, maximumBytes = 65536) { await writePrivateBytes(path, Buffer.from(value, "utf8"), privateParent, maximumBytes); }
106
+ export async function writePrivateJson(path, value, privateParent = true, maximumBytes = 65536) { await writePrivateText(path, JSON.stringify(value) + "\n", privateParent, maximumBytes); }
107
+ export class LocalProfileError extends Error {
108
+ reason;
109
+ name = "LocalProfileError";
110
+ constructor(reason, message) {
111
+ super(message);
112
+ this.reason = reason;
113
+ }
114
+ }
115
+ export const windowsLocalSetupRefusal = "Relvo's saved login and automatic setup need macOS or Linux. On Windows, connect your agent to the MCP URL shown under Getting started in the Relvo console.";
116
+ export async function readLocalProfile(path = localProfilePath()) {
117
+ try {
118
+ await profileDirectory(path);
119
+ const raw = await readPrivateJson(path), parsed = localProfileSchema.safeParse(raw);
120
+ if (parsed.success)
121
+ return parsed.data;
122
+ if (raw === null || typeof raw !== "object" || !("managedBy" in raw) || raw.managedBy !== "relvo-cli")
123
+ throw new Error("Foreign profile");
124
+ throw new SyntaxError("Invalid profile");
125
+ }
126
+ catch (error) {
127
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
128
+ return null;
129
+ if (process.platform === "win32")
130
+ throw new LocalProfileError("unsupported", windowsLocalSetupRefusal);
131
+ if (error instanceof SyntaxError)
132
+ throw new LocalProfileError("unreadable", `Relvo's saved login at ${path} is unreadable. Run relvo logout to remove it, then relvo setup. No credentials were changed.`);
133
+ throw new LocalProfileError("unsafe", `Relvo's saved login at ${path} is unsafe to use: it or a folder above it is a link, belongs to another user, is writable by others, or is not Relvo's file. Keep your home and .config folders writable only by you, then run chmod 700 ${dirname(path)} and chmod 600 ${path}. No credentials were changed.`);
134
+ }
135
+ }
136
+ export async function removeUnreadableProfile(path) {
137
+ await profileDirectory(path);
138
+ const info = await lstat(path);
139
+ if (!info.isFile() || info.isSymbolicLink())
140
+ throw new LocalProfileError("unsafe", `Relvo's saved login at ${path} is not a regular file, so it was left in place.`);
141
+ await unlink(path);
142
+ }
143
+ export async function writeLocalProfile(path, profile) {
144
+ await profileDirectory(path, true);
145
+ await readLocalProfile(path);
146
+ await writePrivateJson(path, localProfileSchema.parse({ ...profile, managedBy: "relvo-cli" }));
147
+ }
148
+ export async function readLocalInstallation(path = localProfilePath()) {
149
+ try {
150
+ const directory = await profileDirectory(path);
151
+ return installationSchema.parse(await readPrivateJson(join(directory, "installation.json")));
152
+ }
153
+ catch (error) {
154
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
155
+ return null;
156
+ throw new Error("Relvo installation ownership is unsafe or invalid. No registrations were changed.");
157
+ }
158
+ }
159
+ export async function removeLocalProfile(path) {
160
+ const profile = await readLocalProfile(path);
161
+ if (profile) {
162
+ const installation = await readLocalInstallation(path);
163
+ if (installation && installation.installationId !== profile.installationId)
164
+ throw new Error("Relvo installation ownership changed. The login was preserved.");
165
+ await writePrivateJson(join(dirname(path), "installation.json"), installationSchema.parse({ managedBy: "relvo-cli", version: 1,
166
+ installationId: profile.installationId, registrations: profile.registrations }));
167
+ await unlink(path);
168
+ }
169
+ }
170
+ export async function runtimeConfig(env, source, profile, requireProfileMatch = false) {
171
+ const hasEnvironment = hasRelvoEnvironment(env) || ["NOIMOS_PLATFORM_API_URL", "NOIMOS_PLATFORM_API_KEY"].some((name) => env[name] !== undefined);
172
+ const environment = hasEnvironment || !profile ? configFromEnvironment(env, source) : null;
173
+ if (environment && (!profile || !requireProfileMatch))
174
+ return environment;
175
+ const saved = await readLocalProfile(profile);
176
+ if (!saved || saved.state !== "ready" || saved.pendingAck || Date.parse(saved.grant.expiresAt) <= Date.now())
177
+ throw new LocalProfileError("missing", "Relvo login is missing, incomplete or expired. Run npx --yes @relvo/cli setup to resume.");
178
+ if (environment && (environment.apiUrl.replace(/\/$/, "") !== saved.apiUrl || environment.apiKey !== saved.grant.apiKey || Boolean(environment.local) !== saved.local))
179
+ throw new Error("Ambient Relvo credentials conflict with the explicitly selected managed profile. No project was selected.");
180
+ return { apiUrl: saved.apiUrl, apiKey: saved.grant.apiKey, local: saved.local, source };
181
+ }
@@ -0,0 +1,131 @@
1
+ import { unlink } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { z } from "zod";
4
+ import { configFromEnvironment, hasRelvoEnvironment, PlatformClient } from "./index.js";
5
+ import { platformLocalSetupPollSchema } from "./local-setup.js";
6
+ import { profileDirectory, readLocalProfile, readPrivateJson, writeLocalProfile, writePrivateJson } from "./local-profile.js";
7
+ import { withLocalProfileLock } from "./local-profile-lock.js";
8
+ export { withLocalProfileLock } from "./local-profile-lock.js";
9
+ const entrySchema = z.object({ grant: platformLocalSetupPollSchema.options[1].refine(grant => grant.expiresAt === grant.approvedExpiresAt, "Only acknowledged approvals may be saved"),
10
+ name: z.string().max(512).optional() }).strict();
11
+ const storeSchema = z.object({ managedBy: z.literal("relvo-cli"), version: z.literal(1), installationId: z.string().regex(/^[a-f0-9]{24}$/),
12
+ apiUrl: z.string().url(), consoleUrl: z.string().url(), local: z.boolean(), projects: z.array(entrySchema).max(1000),
13
+ projectSetup: z.object({ apiKeyId: z.string().min(1).max(256) }).strict().optional() }).strict();
14
+ const maximumBytes = 1024 * 1024;
15
+ const storePath = (path) => join(dirname(path), "projects.json");
16
+ async function readStore(path) {
17
+ await profileDirectory(path);
18
+ try {
19
+ return storeSchema.parse(await readPrivateJson(storePath(path), maximumBytes));
20
+ }
21
+ catch (error) {
22
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
23
+ return null;
24
+ throw new Error("Relvo's saved project approvals are unsafe or unreadable. No credentials were changed.");
25
+ }
26
+ }
27
+ async function entries(path, profile) {
28
+ const store = await readStore(path);
29
+ const sameApi = !store || store.apiUrl === profile.apiUrl || !store.local && !profile.local && /(^|\.)relvo\.si$/.test(new URL(profile.apiUrl).hostname);
30
+ if (store && (store.installationId !== profile.installationId || !sameApi || store.consoleUrl !== profile.consoleUrl || store.local !== profile.local)) {
31
+ throw new Error("Relvo's saved project approvals belong to another installation or server. No credentials were changed.");
32
+ }
33
+ const saved = store?.projects ?? [];
34
+ if (profile.state !== "ready" || profile.pendingAck)
35
+ return saved;
36
+ const previous = saved.find(entry => entry.grant.developerProjectId === profile.grant.developerProjectId);
37
+ return [...saved.filter(entry => entry.grant.developerProjectId !== profile.grant.developerProjectId), { ...previous, grant: profile.grant }];
38
+ }
39
+ export async function rememberLocalProject(path, profile, name) {
40
+ if (profile.pendingAck)
41
+ throw new Error("Complete the pending approval before saving this project.");
42
+ const projects = await entries(path, profile);
43
+ const current = projects.find(entry => entry.grant.developerProjectId === profile.grant.developerProjectId);
44
+ const projectSetup = (await readStore(path))?.projectSetup;
45
+ await writePrivateJson(storePath(path), storeSchema.parse({ managedBy: "relvo-cli", version: 1, installationId: profile.installationId,
46
+ apiUrl: profile.apiUrl, consoleUrl: profile.consoleUrl, local: profile.local,
47
+ ...(projectSetup ? { projectSetup } : {}),
48
+ projects: [...projects.filter(entry => entry !== current), { ...current, grant: profile.grant, ...(name === undefined ? {} : { name }) }] }), true, maximumBytes);
49
+ }
50
+ export async function projectSetupInProgress(path, profile) {
51
+ await entries(path, profile);
52
+ return profile.state === "ready" && (await readStore(path))?.projectSetup?.apiKeyId === profile.grant.apiKeyId;
53
+ }
54
+ export async function markProjectSetup(path, profile, apiKeyId) {
55
+ const projects = await entries(path, profile);
56
+ await writePrivateJson(storePath(path), storeSchema.parse({ managedBy: "relvo-cli", version: 1, installationId: profile.installationId,
57
+ apiUrl: profile.apiUrl, consoleUrl: profile.consoleUrl, local: profile.local, projects, projectSetup: { apiKeyId } }), true, maximumBytes);
58
+ }
59
+ export async function completeProjectSetup(path, profile) {
60
+ await entries(path, profile);
61
+ const store = await readStore(path);
62
+ if (!store || store.projectSetup?.apiKeyId !== profile.grant.apiKeyId)
63
+ return;
64
+ const { projectSetup, ...complete } = store;
65
+ void projectSetup;
66
+ await writePrivateJson(storePath(path), complete, true, maximumBytes);
67
+ }
68
+ export async function listLocalProjects(path, now = Date.now()) {
69
+ const profile = await readLocalProfile(path);
70
+ if (!profile)
71
+ throw new Error("Run relvo setup to create the local login.");
72
+ return (await entries(path, profile)).map(({ grant, name }) => ({ id: grant.developerProjectId, organizationId: grant.developerOrganizationId,
73
+ ...(name === undefined ? {} : { name }), current: profile.state === "ready" && !profile.pendingAck && profile.grant.developerProjectId === grant.developerProjectId && profile.grant.apiKeyId === grant.apiKeyId,
74
+ expiresAt: grant.expiresAt, expired: Date.parse(grant.expiresAt) <= now }));
75
+ }
76
+ async function approvedProject(path, id, env, source, transport, now) {
77
+ const profile = await readLocalProfile(path);
78
+ if (!profile || profile.state !== "ready" || profile.pendingAck)
79
+ throw new Error("Run relvo setup to complete the local login before selecting a project.");
80
+ const entry = (await entries(path, profile)).find(item => item.grant.developerProjectId === id);
81
+ if (!entry)
82
+ throw new Error(`This login has no approved access to another project with that ID. Run relvo project add --project ${id} and approve it in your browser.`);
83
+ if (Date.parse(entry.grant.expiresAt) <= now)
84
+ throw new Error(`This project's saved approval expired. Run relvo project add --project ${id} to approve it again.`);
85
+ const config = { apiUrl: profile.apiUrl, apiKey: entry.grant.apiKey, local: profile.local, source };
86
+ if (hasRelvoEnvironment(env) || ["NOIMOS_PLATFORM_API_URL", "NOIMOS_PLATFORM_API_KEY"].some(name => env[name] !== undefined)) {
87
+ const ambient = configFromEnvironment(env, source);
88
+ if (ambient.apiUrl.replace(/\/$/, "") !== config.apiUrl || ambient.apiKey !== config.apiKey || Boolean(ambient.local) !== config.local)
89
+ throw new Error("Ambient Relvo credentials conflict with the explicitly selected project. No project was selected.");
90
+ }
91
+ const { data } = await new PlatformClient(config, transport).project();
92
+ if (data.developerProjectId !== id || data.developerOrganizationId !== entry.grant.developerOrganizationId)
93
+ throw new Error("The saved approval does not match the requested project. No project was selected.");
94
+ return { profile, entry, config, name: data.name };
95
+ }
96
+ export async function localProjectConfig(path, id, env, source, transport = fetch, now = Date.now()) {
97
+ return (await approvedProject(path, id, env, source, transport, now)).config;
98
+ }
99
+ export async function localProjectProfile(path, id, env, transport = fetch, now = Date.now()) {
100
+ const { profile, entry } = await approvedProject(path, id, env, "cli", transport, now);
101
+ return { ...profile, grant: entry.grant };
102
+ }
103
+ export async function useLocalProject(path, id, env, source, transport = fetch, now = Date.now(), beforeSelect) {
104
+ return withLocalProfileLock(path, async () => {
105
+ const { profile, entry, name } = await approvedProject(path, id, env, source, transport, now);
106
+ const deactivated = await beforeSelect?.(id);
107
+ await rememberLocalProject(path, profile);
108
+ const selected = { ...profile, grant: entry.grant };
109
+ await rememberLocalProject(path, selected, name);
110
+ await writeLocalProfile(path, selected);
111
+ return { id, organizationId: entry.grant.developerOrganizationId, name, ...(deactivated ? { deactivated } : {}) };
112
+ });
113
+ }
114
+ export async function assertLocalProjectConnection(path, config, pinnedProjectId) {
115
+ const profile = await readLocalProfile(path);
116
+ if (!profile || profile.state !== "ready" || profile.pendingAck || profile.apiUrl !== config.apiUrl || profile.local !== Boolean(config.local))
117
+ throw new Error("The Relvo login changed. Reconnect Relvo MCP before continuing. Accepted executions remain in their original project.");
118
+ const matches = pinnedProjectId
119
+ ? (await entries(path, profile)).some(entry => entry.grant.developerProjectId === pinnedProjectId && entry.grant.apiKey === config.apiKey)
120
+ : profile.grant.apiKey === config.apiKey;
121
+ if (!matches)
122
+ throw new Error("The selected Relvo project changed. Reconnect Relvo MCP to load its tools and permissions before continuing. Accepted executions remain in their original project.");
123
+ }
124
+ export async function removeLocalProjects(path, installationId) {
125
+ const store = await readStore(path);
126
+ if (!store)
127
+ return;
128
+ if (installationId && store.installationId !== installationId)
129
+ throw new Error("Relvo's saved projects belong to another installation and were preserved.");
130
+ await unlink(storePath(path));
131
+ }
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ import { PLATFORM_CLIENT_HEADERS } from "../schema/platformApiKey.js";
3
+ import { platformLocalSetupStartInputSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema, platformLocalSetupAcknowledgedSchema } from "../schema/platformLocalSetup.js";
4
+ export { platformLocalSetupClientSchema, platformLocalSetupStartInputSchema, platformLocalSetupStartSchema, platformLocalSetupProofSchema, platformLocalSetupPollSchema, platformLocalSetupAcknowledgedSchema } from "../schema/platformLocalSetup.js";
5
+ export function relvoApiUrl(value, local = false) {
6
+ const url = new URL(value);
7
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== "https:" && !(local && url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))))
8
+ throw new Error("Relvo URLs must use HTTPS; --local permits loopback HTTP only.");
9
+ return url.href.replace(/\/$/, "");
10
+ }
11
+ export function verificationUrl(value, consoleUrl, authorizationRequest, local = false, projectId) {
12
+ const trusted = new URL(relvoApiUrl(consoleUrl, local));
13
+ if (trusted.pathname !== "/")
14
+ throw new Error("Provide the Relvo console origin without a path.");
15
+ const url = new URL(value);
16
+ if (url.origin !== trusted.origin || url.pathname !== "/local-setup" || url.username || url.password || url.hash ||
17
+ [...url.searchParams.keys()].length !== 1 || url.searchParams.get("request") !== authorizationRequest)
18
+ throw new Error("Relvo returned an unexpected verification URL. No browser was opened.");
19
+ if (projectId)
20
+ url.searchParams.set("project", projectId);
21
+ return url.href;
22
+ }
23
+ const publicSetupErrors = {
24
+ platform_local_setup_unavailable: "Local setup is unavailable on this Relvo server.",
25
+ platform_local_setup_invalid: "The local setup proof is invalid. No credentials were replaced.",
26
+ platform_local_setup_expired: "The local setup request expired. Run setup again after checking its saved state.",
27
+ platform_local_setup_conflict: "This setup request was already used for a different approval.",
28
+ platform_local_setup_rate_exceeded: "The local setup request limit was reached. Wait before trying again.",
29
+ };
30
+ export class LocalSetupClient {
31
+ transport;
32
+ baseUrl;
33
+ constructor(apiUrl, local = false, transport = fetch) {
34
+ this.transport = transport;
35
+ this.baseUrl = relvoApiUrl(apiUrl, local);
36
+ }
37
+ async request(operation, body, schema) {
38
+ let response;
39
+ try {
40
+ response = await this.transport(`${this.baseUrl}/tools/setup/${operation}`, { method: "POST", redirect: "error", signal: AbortSignal.timeout(20000),
41
+ headers: { "content-type": "application/json", [PLATFORM_CLIENT_HEADERS.relvo]: "cli" }, body: JSON.stringify(body) });
42
+ }
43
+ catch {
44
+ throw new Error("Relvo setup could not be reached. Rerun setup to resume its saved request; do not create another key manually.");
45
+ }
46
+ let payload;
47
+ try {
48
+ const reader = response.body?.getReader();
49
+ if (!reader)
50
+ throw new Error("Missing body");
51
+ const chunks = [];
52
+ let size = 0;
53
+ try {
54
+ for (;;) {
55
+ const { value, done } = await reader.read();
56
+ if (done)
57
+ break;
58
+ size += value.byteLength;
59
+ if (size > 32768) {
60
+ await reader.cancel();
61
+ throw new Error("Response too large");
62
+ }
63
+ chunks.push(value);
64
+ }
65
+ }
66
+ finally {
67
+ reader.releaseLock();
68
+ }
69
+ const bytes = new Uint8Array(size);
70
+ let offset = 0;
71
+ for (const chunk of chunks) {
72
+ bytes.set(chunk, offset);
73
+ offset += chunk.byteLength;
74
+ }
75
+ payload = JSON.parse(new TextDecoder().decode(bytes));
76
+ }
77
+ catch {
78
+ throw new Error("Relvo setup returned an invalid response. Rerun setup to resume the saved request.");
79
+ }
80
+ if (!response.ok) {
81
+ const error = z.object({ error: z.object({ message: z.string() }) }).safeParse(payload);
82
+ throw new Error(error.success && Object.hasOwn(publicSetupErrors, error.data.error.message) ? publicSetupErrors[error.data.error.message] : `Relvo setup request failed (${response.status}). No server details were printed.`);
83
+ }
84
+ const envelope = z.object({ data: z.unknown() }).safeParse(payload);
85
+ const result = schema.safeParse(envelope.success ? envelope.data.data : undefined);
86
+ if (!result.success)
87
+ throw new Error("Relvo setup returned an unexpected response. Rerun setup to resume the saved request.");
88
+ return result.data;
89
+ }
90
+ start(input) { return this.request("start", platformLocalSetupStartInputSchema.parse(input), platformLocalSetupStartSchema); }
91
+ poll(input) { return this.request("poll", platformLocalSetupProofSchema.parse(input), platformLocalSetupPollSchema); }
92
+ acknowledge(input) { return this.request("ack", platformLocalSetupProofSchema.parse(input), platformLocalSetupAcknowledgedSchema); }
93
+ }