run402 1.18.0 → 1.19.0
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/cli.mjs +6 -0
- package/lib/email.mjs +206 -0
- package/lib/functions.mjs +3 -22
- package/lib/paid-fetch.mjs +65 -1
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -33,6 +33,7 @@ Commands:
|
|
|
33
33
|
subdomains Manage custom subdomains (claim, list, delete)
|
|
34
34
|
apps Browse and manage the app marketplace
|
|
35
35
|
image Generate AI images via x402 or MPP micropayments
|
|
36
|
+
email Send template-based emails from your project
|
|
36
37
|
message Send messages to Run402 developers
|
|
37
38
|
agent Manage agent identity (contact info)
|
|
38
39
|
|
|
@@ -131,6 +132,11 @@ switch (cmd) {
|
|
|
131
132
|
await run(sub, rest);
|
|
132
133
|
break;
|
|
133
134
|
}
|
|
135
|
+
case "email": {
|
|
136
|
+
const { run } = await import("./lib/email.mjs");
|
|
137
|
+
await run(sub, rest);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
134
140
|
case "message": {
|
|
135
141
|
const { run } = await import("./lib/message.mjs");
|
|
136
142
|
await run(sub, rest);
|
package/lib/email.mjs
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { findProject, resolveProjectId, API, updateProject, loadKeyStore, saveKeyStore } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 email — Send template-based emails from your project
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 email <subcommand> [args...]
|
|
7
|
+
|
|
8
|
+
Subcommands:
|
|
9
|
+
create <slug> [--project <id>] Create a mailbox (<slug>@mail.run402.com)
|
|
10
|
+
send --template <name> --to <email> [--var key=value ...] [--project <id>]
|
|
11
|
+
Send a template email
|
|
12
|
+
list [--project <id>] List sent emails
|
|
13
|
+
get <message_id> [--project <id>] Get a message with replies
|
|
14
|
+
|
|
15
|
+
Templates:
|
|
16
|
+
project_invite — requires --var project_name=... --var invite_url=...
|
|
17
|
+
magic_link — requires --var project_name=... --var link_url=... --var expires_in=...
|
|
18
|
+
notification — requires --var project_name=... --var message=... (max 500 chars)
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
run402 email create my-app
|
|
22
|
+
run402 email send --template project_invite --to user@example.com \\
|
|
23
|
+
--var project_name="My App" --var invite_url="https://example.com/invite/abc"
|
|
24
|
+
run402 email send --template notification --to admin@example.com \\
|
|
25
|
+
--var project_name="My App" --var message="Deploy complete"
|
|
26
|
+
run402 email list
|
|
27
|
+
run402 email get msg_abc123
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
- One mailbox per project
|
|
31
|
+
- Single recipient per send (no CC/BCC)
|
|
32
|
+
- Slug: 3-63 chars, lowercase alphanumeric + hyphens, no consecutive hyphens
|
|
33
|
+
- Rate limits vary by tier (prototype: 10/day, hobby: 50/day, team: 200/day)
|
|
34
|
+
- --project defaults to the active project
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
38
|
+
|
|
39
|
+
function parseFlag(args, flag) {
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
if (args[i] === flag && args[i + 1]) return args[i + 1];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseVars(args) {
|
|
47
|
+
const vars = {};
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
if (args[i] === "--var" && args[i + 1]) {
|
|
50
|
+
const raw = args[++i];
|
|
51
|
+
const eq = raw.indexOf("=");
|
|
52
|
+
if (eq > 0) {
|
|
53
|
+
vars[raw.slice(0, eq)] = raw.slice(eq + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return vars;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function resolveMailboxId(projectId, serviceKey) {
|
|
61
|
+
const store = loadKeyStore();
|
|
62
|
+
const proj = store.projects[projectId];
|
|
63
|
+
if (proj && proj.mailbox_id) return proj.mailbox_id;
|
|
64
|
+
|
|
65
|
+
// Fallback: discover via API
|
|
66
|
+
const res = await fetch(`${API}/mailboxes/v1`, {
|
|
67
|
+
headers: { "Authorization": `Bearer ${serviceKey}` },
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const data = await res.json().catch(() => ({}));
|
|
71
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const mailboxes = await res.json();
|
|
75
|
+
if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
|
|
76
|
+
console.error(JSON.stringify({ status: "error", message: "No mailbox found. Run: run402 email create <slug>" }));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const mb = mailboxes[0];
|
|
80
|
+
updateProject(projectId, { mailbox_id: mb.id, mailbox_address: mb.address });
|
|
81
|
+
return mb.id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function create(args) {
|
|
85
|
+
const slug = args.find(a => !a.startsWith("--"));
|
|
86
|
+
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
87
|
+
const p = findProject(projectId);
|
|
88
|
+
|
|
89
|
+
if (!slug) {
|
|
90
|
+
console.error(JSON.stringify({ status: "error", message: "Missing slug. Usage: run402 email create <slug>" }));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if (slug.length < 3 || slug.length > 63) {
|
|
94
|
+
console.error(JSON.stringify({ status: "error", message: "Slug must be 3-63 characters." }));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (!SLUG_RE.test(slug)) {
|
|
98
|
+
console.error(JSON.stringify({ status: "error", message: "Slug must be lowercase alphanumeric + hyphens, start/end with alphanumeric." }));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (slug.includes("--")) {
|
|
102
|
+
console.error(JSON.stringify({ status: "error", message: "Slug must not contain consecutive hyphens." }));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const res = await fetch(`${API}/mailboxes/v1`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
109
|
+
body: JSON.stringify({ slug, project_id: projectId }),
|
|
110
|
+
});
|
|
111
|
+
const data = await res.json();
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateProject(projectId, { mailbox_id: data.id, mailbox_address: data.address });
|
|
118
|
+
console.log(JSON.stringify({ status: "ok", mailbox_id: data.id, address: data.address, slug: data.slug }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function send(args) {
|
|
122
|
+
const template = parseFlag(args, "--template");
|
|
123
|
+
const to = parseFlag(args, "--to");
|
|
124
|
+
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
125
|
+
const p = findProject(projectId);
|
|
126
|
+
const variables = parseVars(args);
|
|
127
|
+
|
|
128
|
+
if (!template) {
|
|
129
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --template. Options: project_invite, magic_link, notification" }));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (!to) {
|
|
133
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --to <email>" }));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mailboxId = await resolveMailboxId(projectId, p.service_key);
|
|
138
|
+
|
|
139
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify({ template, to, variables }),
|
|
143
|
+
});
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template }));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function list(args) {
|
|
154
|
+
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
155
|
+
const p = findProject(projectId);
|
|
156
|
+
const mailboxId = await resolveMailboxId(projectId, p.service_key);
|
|
157
|
+
|
|
158
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
|
|
159
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
160
|
+
});
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(JSON.stringify(data, null, 2));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function get(args) {
|
|
171
|
+
const messageId = args.find(a => !a.startsWith("--"));
|
|
172
|
+
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
173
|
+
const p = findProject(projectId);
|
|
174
|
+
|
|
175
|
+
if (!messageId) {
|
|
176
|
+
console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email get <message_id>" }));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mailboxId = await resolveMailboxId(projectId, p.service_key);
|
|
181
|
+
|
|
182
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}`, {
|
|
183
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
184
|
+
});
|
|
185
|
+
const data = await res.json();
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(JSON.stringify(data, null, 2));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function run(sub, args) {
|
|
195
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
196
|
+
switch (sub) {
|
|
197
|
+
case "create": await create(args); break;
|
|
198
|
+
case "send": await send(args); break;
|
|
199
|
+
case "list": await list(args); break;
|
|
200
|
+
case "get": await get(args); break;
|
|
201
|
+
default:
|
|
202
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
203
|
+
console.log(HELP);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/lib/functions.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync
|
|
2
|
-
import { findProject,
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { findProject, API } from "./config.mjs";
|
|
3
|
+
import { setupPaidFetch } from "./paid-fetch.mjs";
|
|
3
4
|
|
|
4
5
|
const HELP = `run402 functions — Manage serverless functions
|
|
5
6
|
|
|
@@ -27,26 +28,6 @@ Notes:
|
|
|
27
28
|
- Deploy may require payment if the project lease has expired
|
|
28
29
|
`;
|
|
29
30
|
|
|
30
|
-
async function setupPaidFetch() {
|
|
31
|
-
if (!existsSync(ALLOWANCE_FILE)) {
|
|
32
|
-
console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create && run402 allowance fund" }));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
const allowance = readAllowance();
|
|
36
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
37
|
-
const { createPublicClient, http } = await import("viem");
|
|
38
|
-
const { baseSepolia } = await import("viem/chains");
|
|
39
|
-
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
40
|
-
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
41
|
-
const { toClientEvmSigner } = await import("@x402/evm");
|
|
42
|
-
const account = privateKeyToAccount(allowance.privateKey);
|
|
43
|
-
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
44
|
-
const signer = toClientEvmSigner(account, publicClient);
|
|
45
|
-
const client = new x402Client();
|
|
46
|
-
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
47
|
-
return wrapFetchWithPayment(fetch, client);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
31
|
async function deploy(projectId, name, args) {
|
|
51
32
|
const p = findProject(projectId);
|
|
52
33
|
const opts = { file: null, timeout: undefined, memory: undefined, deps: undefined };
|
package/lib/paid-fetch.mjs
CHANGED
|
@@ -3,11 +3,33 @@
|
|
|
3
3
|
* Branches on allowance rail:
|
|
4
4
|
* - "mpp": uses mppx.fetch (Tempo pathUSD)
|
|
5
5
|
* - "x402" (default): uses @x402/fetch (Base USDC)
|
|
6
|
+
*
|
|
7
|
+
* Checks on-chain balances at setup time and selects funded networks.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { readAllowance, ALLOWANCE_FILE } from "./config.mjs";
|
|
9
11
|
import { existsSync } from "fs";
|
|
10
12
|
|
|
13
|
+
const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
|
|
14
|
+
const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
15
|
+
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
16
|
+
const PATH_USD = "0x20c0000000000000000000000000000000000000";
|
|
17
|
+
const TEMPO_RPC = "https://rpc.moderato.tempo.xyz/";
|
|
18
|
+
|
|
19
|
+
async function checkBalance(publicClient, tokenAddress, walletAddress) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await publicClient.readContract({
|
|
22
|
+
address: tokenAddress,
|
|
23
|
+
abi: USDC_ABI,
|
|
24
|
+
functionName: "balanceOf",
|
|
25
|
+
args: [walletAddress],
|
|
26
|
+
});
|
|
27
|
+
return Number(raw);
|
|
28
|
+
} catch {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
export async function setupPaidFetch() {
|
|
12
34
|
if (!existsSync(ALLOWANCE_FILE)) {
|
|
13
35
|
console.error(JSON.stringify({ status: "error", message: "No agent allowance found. Run: run402 allowance create && run402 allowance fund" }));
|
|
@@ -18,6 +40,23 @@ export async function setupPaidFetch() {
|
|
|
18
40
|
const account = privateKeyToAccount(allowance.privateKey);
|
|
19
41
|
|
|
20
42
|
if (allowance.rail === "mpp") {
|
|
43
|
+
const { createPublicClient, http, defineChain } = await import("viem");
|
|
44
|
+
const tempoModerato = defineChain({
|
|
45
|
+
id: 42431,
|
|
46
|
+
name: "Tempo Moderato",
|
|
47
|
+
nativeCurrency: { name: "pathUSD", symbol: "pathUSD", decimals: 6 },
|
|
48
|
+
rpcUrls: { default: { http: [TEMPO_RPC] } },
|
|
49
|
+
});
|
|
50
|
+
const tempoClient = createPublicClient({ chain: tempoModerato, transport: http() });
|
|
51
|
+
const balance = await checkBalance(tempoClient, PATH_USD, allowance.address);
|
|
52
|
+
if (balance === 0) {
|
|
53
|
+
console.error(JSON.stringify({
|
|
54
|
+
status: "error",
|
|
55
|
+
message: `No pathUSD balance on Tempo Moderato (0). Fund your wallet: run402 allowance fund`,
|
|
56
|
+
}));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
21
60
|
const { Mppx, tempo } = await import("mppx/client");
|
|
22
61
|
const mppx = Mppx.create({
|
|
23
62
|
polyfill: false,
|
|
@@ -26,7 +65,7 @@ export async function setupPaidFetch() {
|
|
|
26
65
|
return mppx.fetch;
|
|
27
66
|
}
|
|
28
67
|
|
|
29
|
-
// Default: x402
|
|
68
|
+
// Default: x402
|
|
30
69
|
const { createPublicClient, http } = await import("viem");
|
|
31
70
|
const { base, baseSepolia } = await import("viem/chains");
|
|
32
71
|
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
@@ -36,8 +75,33 @@ export async function setupPaidFetch() {
|
|
|
36
75
|
const mainnetClient = createPublicClient({ chain: base, transport: http() });
|
|
37
76
|
const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
38
77
|
|
|
78
|
+
// Check balances in parallel
|
|
79
|
+
const [mainnetBalance, sepoliaBalance] = await Promise.all([
|
|
80
|
+
checkBalance(mainnetClient, USDC_MAINNET, allowance.address),
|
|
81
|
+
checkBalance(sepoliaClient, USDC_SEPOLIA, allowance.address),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
if (mainnetBalance === 0 && sepoliaBalance === 0) {
|
|
85
|
+
console.error(JSON.stringify({
|
|
86
|
+
status: "error",
|
|
87
|
+
message: `No USDC balance on any supported network (Base: $${(mainnetBalance / 1e6).toFixed(2)}, Base Sepolia: $${(sepoliaBalance / 1e6).toFixed(2)}). Fund your wallet or run: run402 allowance fund`,
|
|
88
|
+
}));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
39
92
|
const client = new x402Client();
|
|
40
93
|
client.register("eip155:8453", new ExactEvmScheme(toClientEvmSigner(account, mainnetClient)));
|
|
41
94
|
client.register("eip155:84532", new ExactEvmScheme(toClientEvmSigner(account, sepoliaClient)));
|
|
95
|
+
|
|
96
|
+
// Policy: only allow networks where the wallet has funds
|
|
97
|
+
client.registerPolicy((_version, reqs) => {
|
|
98
|
+
const funded = reqs.filter((r) => {
|
|
99
|
+
if (r.network === "eip155:8453") return mainnetBalance > 0;
|
|
100
|
+
if (r.network === "eip155:84532") return sepoliaBalance > 0;
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
return funded.length > 0 ? funded : reqs;
|
|
104
|
+
});
|
|
105
|
+
|
|
42
106
|
return wrapFetchWithPayment(fetch, client);
|
|
43
107
|
}
|
package/package.json
CHANGED