@noimosai/platform-mcp 0.1.3 → 0.1.5

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 +565 -0
  4. package/dist/cli/command-help.js +395 -0
  5. package/dist/cli/download.js +79 -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 +265 -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 +911 -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 +41 -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
@@ -0,0 +1,184 @@
1
+ import { z } from "zod";
2
+ import { platformJsonObjectSchema } from "../schema/platformPublicExecution.js";
3
+ const protocol = z.enum(["gemini-live", "openai-live", "openai-realtime"]);
4
+ export const realtimeVoiceConfigSchema = z.object({
5
+ model: z.string().min(1).max(100), priceVersion: z.string().min(1).max(40),
6
+ maximumChargeMicros: z.number().int().safe().min(50000).max(10000000).optional(),
7
+ maxDurationSeconds: z.number().int().min(1).max(300).optional(), maxOutputTokens: z.number().int().min(1).max(2048).optional(),
8
+ instructions: z.string().max(8000).optional(), locale: z.string().max(40).optional(), voice: z.string().max(80).optional(),
9
+ tools: z.array(z.object({ name: z.string(), description: z.string().optional(), parameters: platformJsonObjectSchema }).strict()).max(16).optional(),
10
+ clientOrigin: z.string().max(300).optional(),
11
+ }).strict();
12
+ export const realtimeVoiceModelsSchema = z.object({
13
+ priceVersion: z.string(), executionEnabled: z.boolean(),
14
+ models: z.array(z.object({ id: z.string(), provider: z.string(), protocol, inputSampleRate: z.number().int().positive(),
15
+ outputSampleRate: z.number().int().positive(), tools: z.enum(["functions", "delegation"]), configured: z.boolean() })),
16
+ });
17
+ export const realtimeVoiceGrantSchema = z.object({
18
+ clientSecret: z.string().min(1).max(2048), expiresAt: z.number().int(), path: z.literal("/v1/realtime"), protocol: z.literal("relvo-realtime"),
19
+ config: realtimeVoiceConfigSchema,
20
+ });
21
+ const startedSchema = z.object({ type: z.literal("relvo.session.started"), executionId: z.string().uuid(), model: z.string(), protocol,
22
+ inputSampleRate: z.number().int().positive(), outputSampleRate: z.number().int().positive() });
23
+ const closedSchema = z.object({ type: z.literal("relvo.session.closed"), executionId: z.string().uuid().nullable(), usageConfirmed: z.boolean(),
24
+ status: z.enum(["succeeded", "failed", "cancelled", "needs_review"]), providerCostMicros: z.number().int().nonnegative() });
25
+ export class RealtimeVoiceError extends Error {
26
+ executionId;
27
+ constructor(message, executionId) {
28
+ super(message);
29
+ this.executionId = executionId;
30
+ this.name = "RealtimeVoiceError";
31
+ }
32
+ }
33
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
+ export class RealtimeVoiceSession {
35
+ socket;
36
+ session;
37
+ requestClose;
38
+ closed;
39
+ constructor(socket, session, closed, requestClose) {
40
+ this.socket = socket;
41
+ this.session = session;
42
+ this.requestClose = requestClose;
43
+ this.closed = closed;
44
+ }
45
+ static async connect(apiUrl, rawGrant, options = {}) {
46
+ const grant = realtimeVoiceGrantSchema.parse(rawGrant), url = new URL(grant.path, apiUrl), base = new URL(apiUrl);
47
+ if (base.username || base.password || base.search || base.hash || !["http:", "https:"].includes(base.protocol)
48
+ || (base.protocol === "http:" && !["127.0.0.1", "localhost", "[::1]"].includes(base.hostname)))
49
+ throw new Error("Use an HTTPS voice API URL or loopback HTTP.");
50
+ if (grant.expiresAt <= Date.now() / 1000)
51
+ throw new Error("Voice connection token expired. Request a new one.");
52
+ options.signal?.throwIfAborted();
53
+ url.protocol = base.protocol === "https:" ? "wss:" : "ws:";
54
+ const socket = (options.createWebSocket ?? ((target, protocols) => new WebSocket(target, protocols)))(url.href, [grant.protocol, `relvo-session.${grant.clientSecret}`]);
55
+ let complete = false, ready = false, opened = false, closing = false, executionId = null, closeTimer, rejectReady = () => { };
56
+ let resolveClosed = () => { }, rejectClosed = () => { };
57
+ const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; });
58
+ void closed.catch(() => { });
59
+ const failure = (message) => { const error = new RealtimeVoiceError(message, executionId); rejectReady(error); rejectClosed(error); socket.close(); };
60
+ const cancelled = () => {
61
+ if (closing || complete)
62
+ return;
63
+ closing = true;
64
+ if (socket.readyState === 1)
65
+ socket.send(JSON.stringify({ type: "relvo.session.close" }));
66
+ else
67
+ socket.close();
68
+ closeTimer = setTimeout(() => failure("Voice finalization timed out. Read the execution receipt before retrying."), 25000);
69
+ };
70
+ options.signal?.addEventListener("abort", cancelled, { once: true });
71
+ socket.addEventListener("close", () => {
72
+ clearTimeout(timer);
73
+ clearTimeout(closeTimer);
74
+ options.signal?.removeEventListener("abort", cancelled);
75
+ if (!complete)
76
+ failure(opened ? "Voice disconnected before final usage was confirmed. Read the execution receipt before retrying." : "The voice connection closed before it opened, so no session started.");
77
+ }, { once: true });
78
+ socket.addEventListener("error", () => failure("The Relvo voice connection failed."), { once: true });
79
+ const timer = setTimeout(() => failure("Voice session setup timed out."), 25000);
80
+ return new Promise((resolve, reject) => {
81
+ rejectReady = reject;
82
+ socket.addEventListener("open", () => { opened = true; socket.send(JSON.stringify({ type: "relvo.session.configure", config: grant.config })); }, { once: true });
83
+ socket.addEventListener("message", event => {
84
+ let value;
85
+ try {
86
+ if (typeof event.data !== "string" || event.data.length > 1500000)
87
+ throw new Error("Invalid voice event");
88
+ value = JSON.parse(event.data);
89
+ }
90
+ catch {
91
+ failure("Invalid Relvo voice event.");
92
+ return;
93
+ }
94
+ if (!isRecord(value)) {
95
+ failure("Invalid Relvo voice event.");
96
+ return;
97
+ }
98
+ if (typeof value.executionId === "string" && z.string().uuid().safeParse(value.executionId).success)
99
+ executionId = value.executionId;
100
+ const started = startedSchema.safeParse(value), ended = closedSchema.safeParse(value);
101
+ if (started.success && !ready) {
102
+ ready = true;
103
+ clearTimeout(timer);
104
+ resolve(new RealtimeVoiceSession(socket, started.data, closed, cancelled));
105
+ }
106
+ if (ended.success) {
107
+ complete = true;
108
+ clearTimeout(closeTimer);
109
+ resolveClosed(ended.data);
110
+ if (!ready)
111
+ reject(new RealtimeVoiceError(`Voice session ended during setup with status ${ended.data.status}.${executionId ? " Read the execution receipt before retrying." : ""}`, executionId));
112
+ socket.close();
113
+ }
114
+ if (value.type === "relvo.error" && !ready)
115
+ reject(new RealtimeVoiceError(`Voice session rejected: ${typeof value.code === "string" ? value.code : "unknown"}`, executionId));
116
+ try {
117
+ if (typeof value.delta === "string" && ["session.output_audio.delta", "response.output_audio.delta", "response.audio.delta"].includes(String(value.type)))
118
+ options.onAudio?.(value.delta, 24000);
119
+ if (isRecord(value.serverContent) && isRecord(value.serverContent.modelTurn) && Array.isArray(value.serverContent.modelTurn.parts)) {
120
+ for (const part of value.serverContent.modelTurn.parts)
121
+ if (isRecord(part) && isRecord(part.inlineData)
122
+ && typeof part.inlineData.data === "string" && typeof part.inlineData.mimeType === "string" && part.inlineData.mimeType.startsWith("audio/pcm"))
123
+ options.onAudio?.(part.inlineData.data, 24000);
124
+ }
125
+ options.onEvent?.(value);
126
+ }
127
+ catch {
128
+ failure("The voice event handler failed. Read the execution receipt before retrying.");
129
+ }
130
+ });
131
+ if (options.signal?.aborted)
132
+ cancelled();
133
+ });
134
+ }
135
+ send(event) {
136
+ if (this.socket.readyState !== 1 || this.socket.bufferedAmount > 1024 * 1024)
137
+ throw new Error("Voice is closed or its send buffer is full.");
138
+ const data = JSON.stringify(event);
139
+ if (new TextEncoder().encode(data).length > 65536)
140
+ throw new Error("Voice frame exceeds 64 KiB.");
141
+ this.socket.send(data);
142
+ }
143
+ sendAudio(audio) {
144
+ if (!audio || audio.length > 64000 || audio.length % 4 || !/^[A-Za-z0-9+/]+={0,2}$/.test(audio))
145
+ throw new Error("Provide a bounded base64 PCM16 frame.");
146
+ if (this.session.protocol === "gemini-live")
147
+ this.send({ realtimeInput: { audio: { data: audio, mimeType: `audio/pcm;rate=${this.session.inputSampleRate}` } } });
148
+ else
149
+ this.send({ type: this.session.protocol === "openai-live" ? "session.input_audio.append" : "input_audio_buffer.append", audio });
150
+ }
151
+ sendText(text) {
152
+ if (this.session.protocol === "gemini-live")
153
+ this.send({ clientContent: { turns: [{ role: "user", parts: [{ text }] }], turnComplete: true } });
154
+ else if (this.session.protocol === "openai-live")
155
+ this.sendDelegationResult(null, text);
156
+ else {
157
+ this.send({ type: "conversation.item.create", item: { type: "message", role: "user", content: [{ type: "input_text", text }] } });
158
+ this.send({ type: "response.create" });
159
+ }
160
+ }
161
+ sendToolResult(call) {
162
+ this.sendToolResults([call]);
163
+ }
164
+ sendToolResults(calls) {
165
+ if (!calls.length || calls.length > 16)
166
+ throw new Error("Return between one and sixteen tool results.");
167
+ if (this.session.protocol === "openai-live")
168
+ throw new Error("GPT-Live uses sendDelegationResult instead of function calls.");
169
+ if (this.session.protocol === "gemini-live")
170
+ this.send({ toolResponse: { functionResponses: calls.map(call => ({ id: call.id, name: call.name, response: call.output })) } });
171
+ else {
172
+ for (const call of calls)
173
+ this.send({ type: "conversation.item.create", item: { type: "function_call_output", call_id: call.id, output: JSON.stringify(call.output) } });
174
+ this.send({ type: "response.create" });
175
+ }
176
+ }
177
+ sendDelegationResult(delegationId, content) {
178
+ if (this.session.protocol !== "openai-live")
179
+ throw new Error("This model uses function calls instead of client delegation.");
180
+ this.send({ type: "session.commentary.append", delegation_id: delegationId, content });
181
+ }
182
+ close() { this.requestClose(); return this.closed; }
183
+ }
184
+ export const connectRealtimeVoice = RealtimeVoiceSession.connect;
@@ -0,0 +1,48 @@
1
+ const MAX_REMEMBERED_NAMES = 64, MAX_SUGGESTIONS = 3;
2
+ const normalized = (name) => name.split("_").map(word => word.length > 4 ? word.replace(/ies$/, "y").replace(/s$/, "") : word).join("_");
3
+ function oneEditApart(left, right) {
4
+ if (Math.abs(left.length - right.length) > 1)
5
+ return false;
6
+ let a = 0, b = 0, edits = 0;
7
+ while (a < left.length && b < right.length) {
8
+ if (left[a] === right[b]) {
9
+ a++;
10
+ b++;
11
+ continue;
12
+ }
13
+ if (++edits > 1)
14
+ return false;
15
+ if (left.length <= right.length)
16
+ b++;
17
+ if (left.length >= right.length)
18
+ a++;
19
+ }
20
+ return edits + Number(a < left.length || b < right.length) <= 1;
21
+ }
22
+ export class ToolNameHints {
23
+ names = new Set();
24
+ credential;
25
+ remember(names, credential) {
26
+ if (this.credential !== credential) {
27
+ this.names.clear();
28
+ this.credential = credential;
29
+ }
30
+ for (const name of names) {
31
+ this.names.delete(name);
32
+ this.names.add(name);
33
+ if (this.names.size > MAX_REMEMBERED_NAMES) {
34
+ const oldest = this.names.values().next().value;
35
+ if (oldest !== undefined)
36
+ this.names.delete(oldest);
37
+ }
38
+ }
39
+ }
40
+ hint(requested, credential) {
41
+ if (credential !== this.credential)
42
+ return undefined;
43
+ const query = normalized(requested), prefix = requested.split("_")[0];
44
+ const found = [...this.names].filter(name => name !== requested && name.split("_")[0] === prefix && oneEditApart(query, normalized(name)))
45
+ .sort((a, b) => Number(normalized(a) !== query) - Number(normalized(b) !== query) || a.localeCompare(b)).slice(0, MAX_SUGGESTIONS);
46
+ return found.length ? `Loaded tool names: ${found.join(", ")}. Inspect the intended definition before quoting.` : undefined;
47
+ }
48
+ }
@@ -0,0 +1,214 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { PLATFORM_WALLET_ROUTES } from "../schema/platformRoutes.js";
4
+ import { PLATFORM_WALLET_TOP_UP_MIN_CENTS } from "../schema/platformFunding.js";
5
+ import { checksumAddress, loadEvmWallet } from "./evm-wallet.js";
6
+ import { relvoApiUrl } from "./local-setup.js";
7
+ import { PLATFORM_DISTRIBUTION } from "./distribution.js";
8
+ const MICROS_PER_CENT = 10_000;
9
+ const REQUEST_TIMEOUT_MS = 60_000;
10
+ const SIGN_IN_HEADER = "SIGN-IN-WITH-X";
11
+ const PAYMENT_REQUIRED_HEADER = "PAYMENT-REQUIRED";
12
+ const PAYMENT_SIGNATURE_HEADER = "PAYMENT-SIGNATURE";
13
+ const NETWORKS = {
14
+ "eip155:8453": { chainId: 8453, usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" },
15
+ "eip155:84532": { chainId: 84532, usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" },
16
+ };
17
+ const isNetwork = (value) => Object.hasOwn(NETWORKS, value);
18
+ export class WalletAccessError extends Error {
19
+ status;
20
+ constructor(message, status) {
21
+ super(message);
22
+ this.status = status;
23
+ this.name = "WalletAccessError";
24
+ }
25
+ }
26
+ const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/);
27
+ const resourceSchema = z.object({ url: z.string(), description: z.string().optional(), mimeType: z.string().optional() }).passthrough();
28
+ const requirementSchema = z.object({
29
+ scheme: z.string(), network: z.string(), amount: z.string().regex(/^\d+$/), asset: z.string(), payTo: z.string(),
30
+ maxTimeoutSeconds: z.number().int().positive(), extra: z.record(z.string(), z.unknown()).optional(),
31
+ }).passthrough();
32
+ const paymentRequiredSchema = z.object({
33
+ x402Version: z.literal(2), error: z.string().optional(), resource: resourceSchema, accepts: z.array(requirementSchema),
34
+ extensions: z.record(z.string(), z.unknown()).optional(),
35
+ }).passthrough();
36
+ const signInInfoSchema = z.object({
37
+ domain: z.string().min(1), uri: z.string().min(1), version: z.literal("1"), nonce: z.string().min(1), issuedAt: z.string().min(1),
38
+ expirationTime: z.string().optional(), notBefore: z.string().optional(), statement: z.string().optional(),
39
+ resources: z.array(z.string()).optional(), requestId: z.string().optional(),
40
+ }).passthrough();
41
+ const signInExtensionSchema = z.object({
42
+ info: signInInfoSchema, supportedChains: z.array(z.object({ chainId: z.string(), type: z.string() }).passthrough()),
43
+ }).passthrough();
44
+ const signInResultSchema = z.object({ data: z.object({
45
+ apiKey: z.string(), expiresAt: z.string(), address: z.string(), organizationId: z.string(), projectId: z.string(), created: z.boolean(),
46
+ }) });
47
+ const topUpResultSchema = z.object({ data: z.object({
48
+ creditedCents: z.number().int().positive(), transaction: z.string().nullable(), network: z.string(), payer: z.string(),
49
+ }) });
50
+ const base64Json = (value) => Buffer.from(JSON.stringify(value), "utf8").toString("base64");
51
+ function api(apiUrl, local) {
52
+ const url = new URL(relvoApiUrl(apiUrl, local));
53
+ return { url, base: url.href.replace(/\/$/, "") };
54
+ }
55
+ async function serverMessage(response) {
56
+ try {
57
+ const body = z.object({ error: z.object({ message: z.string().regex(/^[a-z0-9_]{1,80}$/) }) }).safeParse(await response.json());
58
+ return body.success ? body.data.error.message : "no error code";
59
+ }
60
+ catch {
61
+ return "no error code";
62
+ }
63
+ }
64
+ async function send(fetchImpl, url, init, what) {
65
+ try {
66
+ return await fetchImpl(url, { ...init, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
67
+ }
68
+ catch {
69
+ throw new WalletAccessError(`The Relvo API could not be reached for ${what}.`);
70
+ }
71
+ }
72
+ async function readPaymentRequired(response) {
73
+ const header = response.headers.get(PAYMENT_REQUIRED_HEADER);
74
+ let json;
75
+ try {
76
+ json = header ? JSON.parse(Buffer.from(header, "base64").toString("utf8")) : (await response.json()).paymentRequired;
77
+ }
78
+ catch {
79
+ throw new WalletAccessError("The Relvo API returned an unreadable payment requirement.", response.status);
80
+ }
81
+ const parsed = paymentRequiredSchema.safeParse(json);
82
+ if (!parsed.success)
83
+ throw new WalletAccessError("The Relvo API returned an unexpected payment requirement.", response.status);
84
+ return parsed.data;
85
+ }
86
+ function siweMessage(fields) {
87
+ const lines = [`${fields.domain} wants you to sign in with your Ethereum account:`, checksumAddress(fields.address), ""];
88
+ if (fields.statement)
89
+ lines.push(fields.statement, "");
90
+ lines.push(`URI: ${fields.uri}`, `Version: ${fields.version}`, `Chain ID: ${fields.chainId.replace(/^eip155:/, "")}`, `Nonce: ${fields.nonce}`, `Issued At: ${fields.issuedAt}`);
91
+ if (fields.expirationTime)
92
+ lines.push(`Expiration Time: ${fields.expirationTime}`);
93
+ if (fields.notBefore)
94
+ lines.push(`Not Before: ${fields.notBefore}`);
95
+ if (fields.requestId)
96
+ lines.push(`Request ID: ${fields.requestId}`);
97
+ if (fields.resources?.length)
98
+ lines.push("Resources:", ...fields.resources.map((resource) => `- ${resource}`));
99
+ return lines.join("\n");
100
+ }
101
+ export async function walletSignIn({ apiUrl, wallet, local, fetch: fetchImpl = fetch }) {
102
+ const { url, base } = api(apiUrl, local), endpoint = `${base}${PLATFORM_WALLET_ROUTES.signIn}`;
103
+ const challenge = await send(fetchImpl, endpoint, { method: "POST" }, "sign-in");
104
+ if (challenge.status !== 402)
105
+ throw new WalletAccessError(`Wallet sign-in is unavailable (${await serverMessage(challenge)}).`, challenge.status);
106
+ const extension = signInExtensionSchema.safeParse((await readPaymentRequired(challenge)).extensions?.["sign-in-with-x"]);
107
+ if (!extension.success)
108
+ throw new WalletAccessError("The Relvo API did not offer wallet sign-in.", 402);
109
+ const { info, supportedChains } = extension.data;
110
+ let uriOrigin;
111
+ try {
112
+ uriOrigin = new URL(info.uri).origin;
113
+ }
114
+ catch {
115
+ uriOrigin = undefined;
116
+ }
117
+ if (info.domain !== url.host || uriOrigin !== url.origin)
118
+ throw new WalletAccessError("The sign-in challenge names a different site than the API URL; nothing was signed.");
119
+ if (info.expirationTime && Date.parse(info.expirationTime) <= Date.now())
120
+ throw new WalletAccessError("The sign-in challenge has already expired; nothing was signed.");
121
+ const chain = supportedChains.find((candidate) => candidate.type === "eip191" && isNetwork(candidate.chainId));
122
+ if (!chain)
123
+ throw new WalletAccessError("The sign-in challenge offers no supported chain.");
124
+ const fields = { ...info, address: wallet.address, chainId: chain.chainId };
125
+ const proof = { domain: info.domain, address: wallet.address, uri: info.uri, version: info.version, chainId: chain.chainId, type: "eip191", nonce: info.nonce,
126
+ issuedAt: info.issuedAt, ...(info.statement ? { statement: info.statement } : {}), ...(info.expirationTime ? { expirationTime: info.expirationTime } : {}),
127
+ ...(info.notBefore ? { notBefore: info.notBefore } : {}), ...(info.requestId ? { requestId: info.requestId } : {}),
128
+ ...(info.resources ? { resources: info.resources } : {}), signature: wallet.signPersonalMessage(siweMessage(fields)) };
129
+ const response = await send(fetchImpl, endpoint, { method: "POST", headers: { [SIGN_IN_HEADER]: base64Json(proof) } }, "sign-in");
130
+ if (!response.ok)
131
+ throw new WalletAccessError(`Wallet sign-in was rejected (${await serverMessage(response)}).`, response.status);
132
+ const parsed = signInResultSchema.safeParse(await response.json().catch(() => null));
133
+ if (!parsed.success)
134
+ throw new WalletAccessError("The Relvo API returned an unexpected sign-in response.", response.status);
135
+ return parsed.data.data;
136
+ }
137
+ export function walletAutoTopUp(options) {
138
+ let committedCents = 0;
139
+ return async (shortfall) => {
140
+ if (shortfall.reason !== "balance")
141
+ return null;
142
+ const deficit = BigInt(shortfall.requiredMicros) - BigInt(shortfall.availableMicros);
143
+ if (deficit <= 0n)
144
+ return null;
145
+ const amountCents = Math.max(PLATFORM_WALLET_TOP_UP_MIN_CENTS, Number((deficit + BigInt(MICROS_PER_CENT) - 1n) / BigInt(MICROS_PER_CENT)));
146
+ if (committedCents + amountCents > options.maximumTotalCents)
147
+ return null;
148
+ committedCents += amountCents;
149
+ return walletTopUp({ ...options, amountCents, maximumCents: amountCents });
150
+ };
151
+ }
152
+ export function walletAutoTopUpFromEnvironment(env, api) {
153
+ if (PLATFORM_DISTRIBUTION.product !== "relvo")
154
+ return undefined;
155
+ const key = env.RELVO_WALLET_PRIVATE_KEY?.trim(), cap = env.RELVO_WALLET_AUTO_TOP_UP_USD?.trim();
156
+ if (!key || !cap)
157
+ return undefined;
158
+ if (!/^\d{1,6}(?:\.\d{1,2})?$/.test(cap))
159
+ throw new WalletAccessError("RELVO_WALLET_AUTO_TOP_UP_USD must be a USD amount such as 5 or 5.00.");
160
+ const [dollars, cents = ""] = cap.split("."), maximumTotalCents = Number(dollars) * 100 + Number(cents.padEnd(2, "0"));
161
+ if (maximumTotalCents === 0)
162
+ return undefined;
163
+ if (maximumTotalCents < PLATFORM_WALLET_TOP_UP_MIN_CENTS)
164
+ throw new WalletAccessError(`RELVO_WALLET_AUTO_TOP_UP_USD must be at least ${(PLATFORM_WALLET_TOP_UP_MIN_CENTS / 100).toFixed(2)}, the smallest top-up.`);
165
+ return walletAutoTopUp({ ...api, wallet: loadEvmWallet(key), maximumTotalCents });
166
+ }
167
+ export async function walletTopUp({ apiUrl, wallet, local, fetch: fetchImpl = fetch, apiKey, amountCents, maximumCents }) {
168
+ if (!Number.isSafeInteger(amountCents) || amountCents <= 0 || !Number.isSafeInteger(maximumCents) || maximumCents < amountCents)
169
+ throw new WalletAccessError("Use a positive whole-cent amount that does not exceed the maximum.");
170
+ const { base } = api(apiUrl, local), endpoint = `${base}${PLATFORM_WALLET_ROUTES.topUp}`;
171
+ const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
172
+ const body = JSON.stringify({ amountCents });
173
+ const quote = await send(fetchImpl, endpoint, { method: "POST", headers, body }, "top-up");
174
+ if (quote.status !== 402)
175
+ throw new WalletAccessError(`Top-up is unavailable (${await serverMessage(quote)}).`, quote.status);
176
+ const required = await readPaymentRequired(quote), accepted = required.accepts[0];
177
+ if (!accepted)
178
+ throw new WalletAccessError("The Relvo API offered no payment requirement.", 402);
179
+ if (accepted.scheme !== "exact")
180
+ throw new WalletAccessError("The payment requirement is not the exact scheme; nothing was signed.");
181
+ if (!isNetwork(accepted.network) || accepted.asset.toLowerCase() !== NETWORKS[accepted.network].usdc.toLowerCase())
182
+ throw new WalletAccessError("The payment requirement names an unknown network or token; nothing was signed.");
183
+ const micros = BigInt(accepted.amount);
184
+ if (micros > BigInt(maximumCents) * BigInt(MICROS_PER_CENT) || micros !== BigInt(amountCents) * BigInt(MICROS_PER_CENT))
185
+ throw new WalletAccessError("The payment requirement differs from the requested amount or exceeds the maximum; nothing was signed.");
186
+ if (!address.safeParse(accepted.payTo).success)
187
+ throw new WalletAccessError("The payment requirement has an invalid recipient; nothing was signed.");
188
+ const { name, version } = accepted.extra ?? {};
189
+ if (typeof name !== "string" || typeof version !== "string")
190
+ throw new WalletAccessError("The payment requirement lacks the token's signing domain; nothing was signed.");
191
+ const nonce = `0x${randomBytes(32).toString("hex")}`;
192
+ const authorization = { from: wallet.address, to: accepted.payTo, value: accepted.amount, validAfter: "0",
193
+ validBefore: String(Math.floor(Date.now() / 1000) + accepted.maxTimeoutSeconds), nonce };
194
+ const signature = wallet.signTypedData({ name, version, chainId: NETWORKS[accepted.network].chainId, verifyingContract: accepted.asset }, { TransferWithAuthorization: [
195
+ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" },
196
+ { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" },
197
+ ] }, "TransferWithAuthorization", authorization);
198
+ const payment = { x402Version: 2, resource: required.resource, accepted, payload: { signature, authorization } };
199
+ let response;
200
+ try {
201
+ response = await send(fetchImpl, endpoint, { method: "POST", headers: { ...headers, [PAYMENT_SIGNATURE_HEADER]: base64Json(payment) }, body }, "top-up");
202
+ }
203
+ catch {
204
+ throw new WalletAccessError("The payment was signed but the reply was lost; it may have been settled. Check the balance with relvo usage before paying again.");
205
+ }
206
+ if (response.status === 402)
207
+ throw new WalletAccessError(`The payment was rejected (${(await readPaymentRequired(response)).error ?? "no reason given"}).`, 402);
208
+ if (!response.ok)
209
+ throw new WalletAccessError(`Top-up failed (${await serverMessage(response)}); the payment may be pending, so check the balance before paying again.`, response.status);
210
+ const parsed = topUpResultSchema.safeParse(await response.json().catch(() => null));
211
+ if (!parsed.success)
212
+ throw new WalletAccessError("The Relvo API returned an unexpected top-up response; check the balance before paying again.", response.status);
213
+ return parsed.data.data;
214
+ }