run402 1.18.1 → 1.19.1

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 (3) hide show
  1. package/cli.mjs +6 -0
  2. package/lib/email.mjs +207 -0
  3. 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,207 @@
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 body = await res.json();
75
+ const mailboxes = body.mailboxes || body;
76
+ if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
77
+ console.error(JSON.stringify({ status: "error", message: "No mailbox found. Run: run402 email create <slug>" }));
78
+ process.exit(1);
79
+ }
80
+ const mb = mailboxes[0];
81
+ updateProject(projectId, { mailbox_id: mb.mailbox_id, mailbox_address: mb.address });
82
+ return mb.mailbox_id;
83
+ }
84
+
85
+ async function create(args) {
86
+ const slug = args.find(a => !a.startsWith("--"));
87
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
88
+ const p = findProject(projectId);
89
+
90
+ if (!slug) {
91
+ console.error(JSON.stringify({ status: "error", message: "Missing slug. Usage: run402 email create <slug>" }));
92
+ process.exit(1);
93
+ }
94
+ if (slug.length < 3 || slug.length > 63) {
95
+ console.error(JSON.stringify({ status: "error", message: "Slug must be 3-63 characters." }));
96
+ process.exit(1);
97
+ }
98
+ if (!SLUG_RE.test(slug)) {
99
+ console.error(JSON.stringify({ status: "error", message: "Slug must be lowercase alphanumeric + hyphens, start/end with alphanumeric." }));
100
+ process.exit(1);
101
+ }
102
+ if (slug.includes("--")) {
103
+ console.error(JSON.stringify({ status: "error", message: "Slug must not contain consecutive hyphens." }));
104
+ process.exit(1);
105
+ }
106
+
107
+ const res = await fetch(`${API}/mailboxes/v1`, {
108
+ method: "POST",
109
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
110
+ body: JSON.stringify({ slug, project_id: projectId }),
111
+ });
112
+ const data = await res.json();
113
+ if (!res.ok) {
114
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
115
+ process.exit(1);
116
+ }
117
+
118
+ updateProject(projectId, { mailbox_id: data.mailbox_id, mailbox_address: data.address });
119
+ console.log(JSON.stringify({ status: "ok", mailbox_id: data.mailbox_id, address: data.address, slug: data.slug }));
120
+ }
121
+
122
+ async function send(args) {
123
+ const template = parseFlag(args, "--template");
124
+ const to = parseFlag(args, "--to");
125
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
126
+ const p = findProject(projectId);
127
+ const variables = parseVars(args);
128
+
129
+ if (!template) {
130
+ console.error(JSON.stringify({ status: "error", message: "Missing --template. Options: project_invite, magic_link, notification" }));
131
+ process.exit(1);
132
+ }
133
+ if (!to) {
134
+ console.error(JSON.stringify({ status: "error", message: "Missing --to <email>" }));
135
+ process.exit(1);
136
+ }
137
+
138
+ const mailboxId = await resolveMailboxId(projectId, p.service_key);
139
+
140
+ const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
141
+ method: "POST",
142
+ headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
143
+ body: JSON.stringify({ template, to, variables }),
144
+ });
145
+ const data = await res.json();
146
+ if (!res.ok) {
147
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
148
+ process.exit(1);
149
+ }
150
+
151
+ console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, template: data.template }));
152
+ }
153
+
154
+ async function list(args) {
155
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
156
+ const p = findProject(projectId);
157
+ const mailboxId = await resolveMailboxId(projectId, p.service_key);
158
+
159
+ const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
160
+ headers: { "Authorization": `Bearer ${p.service_key}` },
161
+ });
162
+ const data = await res.json();
163
+ if (!res.ok) {
164
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
165
+ process.exit(1);
166
+ }
167
+
168
+ console.log(JSON.stringify(data, null, 2));
169
+ }
170
+
171
+ async function get(args) {
172
+ const messageId = args.find(a => !a.startsWith("--"));
173
+ const projectId = resolveProjectId(parseFlag(args, "--project"));
174
+ const p = findProject(projectId);
175
+
176
+ if (!messageId) {
177
+ console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email get <message_id>" }));
178
+ process.exit(1);
179
+ }
180
+
181
+ const mailboxId = await resolveMailboxId(projectId, p.service_key);
182
+
183
+ const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}`, {
184
+ headers: { "Authorization": `Bearer ${p.service_key}` },
185
+ });
186
+ const data = await res.json();
187
+ if (!res.ok) {
188
+ console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
189
+ process.exit(1);
190
+ }
191
+
192
+ console.log(JSON.stringify(data, null, 2));
193
+ }
194
+
195
+ export async function run(sub, args) {
196
+ if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
197
+ switch (sub) {
198
+ case "create": await create(args); break;
199
+ case "send": await send(args); break;
200
+ case "list": await list(args); break;
201
+ case "get": await get(args); break;
202
+ default:
203
+ console.error(`Unknown subcommand: ${sub}\n`);
204
+ console.log(HELP);
205
+ process.exit(1);
206
+ }
207
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "run402",
3
- "version": "1.18.1",
3
+ "version": "1.19.1",
4
4
  "description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 and MPP micropayments.",
5
5
  "type": "module",
6
6
  "bin": {