run402 1.36.0 → 1.37.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/lib/ai.mjs +27 -0
- package/lib/allowance.mjs +31 -0
- package/lib/apps.mjs +73 -0
- package/lib/auth.mjs +89 -2
- package/lib/billing.mjs +64 -0
- package/lib/blob.mjs +84 -1
- package/lib/contracts.mjs +139 -0
- package/lib/domains.mjs +1 -0
- package/lib/email.mjs +307 -9
- package/lib/functions.mjs +90 -1
- package/lib/init.mjs +16 -6
- package/lib/projects.mjs +111 -28
- package/lib/secrets.mjs +26 -1
- package/lib/sender-domain.mjs +1 -0
- package/lib/service.mjs +4 -4
- package/lib/sites.mjs +36 -0
- package/lib/storage.mjs +24 -0
- package/lib/subdomains.mjs +26 -0
- package/lib/tier.mjs +8 -1
- package/lib/webhooks.mjs +42 -0
- package/package.json +1 -1
package/lib/email.mjs
CHANGED
|
@@ -7,12 +7,18 @@ Usage:
|
|
|
7
7
|
|
|
8
8
|
Subcommands:
|
|
9
9
|
create <slug> [--project <id>] Create a mailbox (<slug>@mail.run402.com)
|
|
10
|
-
|
|
10
|
+
info [--project <id>] Show mailbox info (ID, address, slug)
|
|
11
|
+
status [--project <id>] Alias for 'info' (prefer 'info')
|
|
11
12
|
send --to <email> [mode flags] Send an email (template or raw HTML)
|
|
12
|
-
list [--project <id>]
|
|
13
|
+
list [--limit <n>] [--after <cursor>] [--project <id>]
|
|
14
|
+
List sent/received messages (paginated)
|
|
13
15
|
get <message_id> [--project <id>] Get a message with replies
|
|
14
16
|
get-raw <message_id> [--project <id>] [--output <file>]
|
|
15
|
-
|
|
17
|
+
Fetch raw RFC-822 bytes (inbound only)
|
|
18
|
+
reply <message_id> --html "..." [--text "..."] [--subject "..."] [--from-name "..."] [--project <id>]
|
|
19
|
+
Reply to an inbound message (threads via In-Reply-To)
|
|
20
|
+
delete [<mailbox_id>] --confirm [--project <id>]
|
|
21
|
+
Delete the project's mailbox (irreversible)
|
|
16
22
|
webhooks <action> [args...] Manage webhooks (see below)
|
|
17
23
|
|
|
18
24
|
Webhook subcommands:
|
|
@@ -25,8 +31,8 @@ Webhook subcommands:
|
|
|
25
31
|
Register a new webhook
|
|
26
32
|
|
|
27
33
|
Send modes:
|
|
28
|
-
Template: --template <name> --var key=value [--var ...]
|
|
29
|
-
Raw HTML: --subject "..." --html "..." [--text "..."]
|
|
34
|
+
Template: --template <name> --var key=value [--var ...] OR --vars '{"k":"v",...}'
|
|
35
|
+
Raw HTML: --subject "..." --html "..." [--text "..."] (both --subject and --html required)
|
|
30
36
|
Both modes support: --from-name "Display Name" --project <id>
|
|
31
37
|
|
|
32
38
|
Templates:
|
|
@@ -38,13 +44,19 @@ Examples:
|
|
|
38
44
|
run402 email create my-app
|
|
39
45
|
run402 email send --template project_invite --to user@example.com \\
|
|
40
46
|
--var project_name="My App" --var invite_url="https://example.com/invite/abc"
|
|
47
|
+
run402 email send --template project_invite --to user@example.com \\
|
|
48
|
+
--vars '{"project_name":"My App","invite_url":"https://example.com/invite/abc"}'
|
|
41
49
|
run402 email send --to user@example.com --subject "Welcome!" \\
|
|
42
50
|
--html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
|
|
43
51
|
run402 email send --template notification --to admin@example.com \\
|
|
44
52
|
--var project_name="My App" --var message="Deploy complete"
|
|
45
|
-
run402 email list
|
|
53
|
+
run402 email list --limit 50
|
|
54
|
+
run402 email list --limit 50 --after msg_abc123
|
|
55
|
+
run402 email info
|
|
46
56
|
run402 email get msg_abc123
|
|
47
57
|
run402 email get-raw msg_abc123 --output reply.eml
|
|
58
|
+
run402 email reply msg_abc123 --html "<p>Thanks!</p>"
|
|
59
|
+
run402 email delete --confirm
|
|
48
60
|
run402 email webhooks list
|
|
49
61
|
run402 email webhooks register --url https://example.com/hook --events delivery,bounced
|
|
50
62
|
|
|
@@ -56,6 +68,138 @@ Notes:
|
|
|
56
68
|
- --project defaults to the active project
|
|
57
69
|
`;
|
|
58
70
|
|
|
71
|
+
const SUB_HELP = {
|
|
72
|
+
send: `run402 email send — Send an email (template or raw HTML)
|
|
73
|
+
|
|
74
|
+
Usage:
|
|
75
|
+
run402 email send --to <email> --template <name> --var key=value [--var ...]
|
|
76
|
+
run402 email send --to <email> --template <name> --vars '{"k":"v",...}'
|
|
77
|
+
run402 email send --to <email> --subject "..." --html "..." [--text "..."]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--to <email> Recipient email address (required; single recipient)
|
|
81
|
+
--template <name> Template name (template mode): project_invite, magic_link,
|
|
82
|
+
notification
|
|
83
|
+
--var key=value Template variable (repeatable; required keys vary by
|
|
84
|
+
template)
|
|
85
|
+
--vars '<json>' All template variables as a single JSON object
|
|
86
|
+
(alternative to multiple --var). Later --var overrides.
|
|
87
|
+
--subject "..." Subject line (raw HTML mode; required with --html)
|
|
88
|
+
--html "..." HTML body (raw HTML mode; required with --subject)
|
|
89
|
+
--text "..." Plain-text body (raw HTML mode; optional)
|
|
90
|
+
--from-name "..." Display name for the From header
|
|
91
|
+
--project <id> Project ID (defaults to the active project)
|
|
92
|
+
|
|
93
|
+
Templates:
|
|
94
|
+
project_invite project_name, invite_url
|
|
95
|
+
magic_link project_name, link_url, expires_in
|
|
96
|
+
notification project_name, message (max 500 chars)
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
run402 email send --template project_invite --to user@example.com \\
|
|
100
|
+
--var project_name="My App" --var invite_url="https://example.com/invite/abc"
|
|
101
|
+
run402 email send --template project_invite --to user@example.com \\
|
|
102
|
+
--vars '{"project_name":"My App","invite_url":"https://example.com/invite/abc"}'
|
|
103
|
+
run402 email send --to user@example.com --subject "Welcome!" \\
|
|
104
|
+
--html "<h1>Hello</h1><p>Welcome aboard.</p>" --from-name "My App"
|
|
105
|
+
`,
|
|
106
|
+
list: `run402 email list — List messages in the mailbox
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
run402 email list [--limit <n>] [--after <cursor>] [--project <id>]
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
--limit <n> Max messages to return (server caps at 200)
|
|
113
|
+
--after <cursor> Pagination cursor (message id from prior page)
|
|
114
|
+
--project <id> Project ID (defaults to the active project)
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
run402 email list
|
|
118
|
+
run402 email list --limit 50
|
|
119
|
+
run402 email list --limit 50 --after msg_abc123
|
|
120
|
+
`,
|
|
121
|
+
reply: `run402 email reply — Reply to an inbound message (threaded via In-Reply-To)
|
|
122
|
+
|
|
123
|
+
Usage:
|
|
124
|
+
run402 email reply <message_id> --html "..." [--text "..."] [options]
|
|
125
|
+
|
|
126
|
+
Arguments:
|
|
127
|
+
<message_id> Inbound message ID to reply to
|
|
128
|
+
|
|
129
|
+
Options:
|
|
130
|
+
--html "..." HTML reply body (required unless --text is given)
|
|
131
|
+
--text "..." Plain-text reply body (required unless --html is given)
|
|
132
|
+
--subject "..." Override the reply subject (default: "Re: <original>")
|
|
133
|
+
--from-name "..." Display name for the From header
|
|
134
|
+
--project <id> Project ID (defaults to the active project)
|
|
135
|
+
|
|
136
|
+
Notes:
|
|
137
|
+
The CLI fetches the original message to derive the reply-to address and
|
|
138
|
+
subject, then POSTs a new message with in_reply_to = <message_id> so the
|
|
139
|
+
server can wire the RFC-822 In-Reply-To / References headers.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
run402 email reply msg_abc123 --html "<p>Thanks, here's the info you asked for.</p>"
|
|
143
|
+
run402 email reply msg_abc123 --subject "Re: invoice #42" --text "Paid, thanks."
|
|
144
|
+
`,
|
|
145
|
+
delete: `run402 email delete — Delete the project's mailbox (irreversible)
|
|
146
|
+
|
|
147
|
+
Usage:
|
|
148
|
+
run402 email delete [<mailbox_id>] --confirm [--project <id>]
|
|
149
|
+
|
|
150
|
+
Arguments:
|
|
151
|
+
<mailbox_id> Mailbox ID to delete (defaults to the project's mailbox)
|
|
152
|
+
|
|
153
|
+
Options:
|
|
154
|
+
--confirm Required: explicit confirmation flag
|
|
155
|
+
--project <id> Project ID (defaults to the active project)
|
|
156
|
+
|
|
157
|
+
Notes:
|
|
158
|
+
Destructive. Drops all messages and webhook subscriptions. Cached
|
|
159
|
+
mailbox_id in the local keystore is cleared on success.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
run402 email delete --confirm
|
|
163
|
+
run402 email delete mbx_abc123 --confirm
|
|
164
|
+
`,
|
|
165
|
+
info: `run402 email info — Show mailbox info (ID, address, slug)
|
|
166
|
+
|
|
167
|
+
Usage:
|
|
168
|
+
run402 email info [--project <id>]
|
|
169
|
+
|
|
170
|
+
Options:
|
|
171
|
+
--project <id> Project ID (defaults to the active project)
|
|
172
|
+
|
|
173
|
+
Notes:
|
|
174
|
+
Same output as 'run402 email status' (kept as an alias for backward
|
|
175
|
+
compatibility). 'info' is the preferred name.
|
|
176
|
+
`,
|
|
177
|
+
status: `run402 email status — Alias for 'run402 email info' (prefer 'info')
|
|
178
|
+
|
|
179
|
+
Usage:
|
|
180
|
+
run402 email status [--project <id>]
|
|
181
|
+
|
|
182
|
+
See 'run402 email info --help' for details. 'status' is kept for backward
|
|
183
|
+
compatibility; new code should use 'info'.
|
|
184
|
+
`,
|
|
185
|
+
"get-raw": `run402 email get-raw — Fetch raw RFC-822 bytes for an inbound message
|
|
186
|
+
|
|
187
|
+
Usage:
|
|
188
|
+
run402 email get-raw <message_id> [--output <file>] [--project <id>]
|
|
189
|
+
|
|
190
|
+
Arguments:
|
|
191
|
+
<message_id> Message ID to fetch (inbound messages only)
|
|
192
|
+
|
|
193
|
+
Options:
|
|
194
|
+
--output <file> Write raw bytes to this file; omit to stream to stdout
|
|
195
|
+
--project <id> Project ID (defaults to the active project)
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
run402 email get-raw msg_abc123 --output reply.eml
|
|
199
|
+
run402 email get-raw msg_abc123 > reply.eml
|
|
200
|
+
`,
|
|
201
|
+
};
|
|
202
|
+
|
|
59
203
|
const SLUG_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
60
204
|
|
|
61
205
|
function parseFlag(args, flag) {
|
|
@@ -67,6 +211,23 @@ function parseFlag(args, flag) {
|
|
|
67
211
|
|
|
68
212
|
function parseVars(args) {
|
|
69
213
|
const vars = {};
|
|
214
|
+
// Apply --vars '<json>' first so later --var can override on key collision.
|
|
215
|
+
for (let i = 0; i < args.length; i++) {
|
|
216
|
+
if (args[i] === "--vars" && args[i + 1]) {
|
|
217
|
+
const raw = args[++i];
|
|
218
|
+
let parsed;
|
|
219
|
+
try { parsed = JSON.parse(raw); } catch {
|
|
220
|
+
console.error(JSON.stringify({ status: "error", message: "Invalid JSON for --vars. Expected a JSON object, e.g. '{\"key\":\"value\"}'" }));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
224
|
+
console.error(JSON.stringify({ status: "error", message: "--vars must be a JSON object, e.g. '{\"key\":\"value\"}'" }));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
for (const [k, v] of Object.entries(parsed)) vars[k] = typeof v === "string" ? v : String(v);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Then --var key=value (later wins).
|
|
70
231
|
for (let i = 0; i < args.length; i++) {
|
|
71
232
|
if (args[i] === "--var" && args[i + 1]) {
|
|
72
233
|
const raw = args[++i];
|
|
@@ -181,10 +342,21 @@ async function send(args) {
|
|
|
181
342
|
process.exit(1);
|
|
182
343
|
}
|
|
183
344
|
|
|
184
|
-
const
|
|
345
|
+
const hasSubject = !!subject;
|
|
346
|
+
const hasHtml = !!html;
|
|
347
|
+
const isRaw = hasSubject || hasHtml;
|
|
185
348
|
const isTemplate = !!template;
|
|
186
349
|
if (!isRaw && !isTemplate) {
|
|
187
|
-
console.error(JSON.stringify({ status: "error", message: "Provide --template (template mode) or --subject
|
|
350
|
+
console.error(JSON.stringify({ status: "error", message: "Provide --template (template mode) or both --subject and --html (raw HTML mode)" }));
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
if (isRaw && isTemplate) {
|
|
354
|
+
console.error(JSON.stringify({ status: "error", message: "Provide --template OR raw mode (--subject + --html), not both" }));
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
if (isRaw && !(hasSubject && hasHtml)) {
|
|
358
|
+
const missing = hasSubject ? "--html" : "--subject";
|
|
359
|
+
console.error(JSON.stringify({ status: "error", message: `Raw mode requires both --subject and --html (missing ${missing})` }));
|
|
188
360
|
process.exit(1);
|
|
189
361
|
}
|
|
190
362
|
|
|
@@ -218,9 +390,16 @@ async function send(args) {
|
|
|
218
390
|
async function list(args) {
|
|
219
391
|
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
220
392
|
const p = findProject(projectId);
|
|
393
|
+
const limit = parseFlag(args, "--limit");
|
|
394
|
+
const after = parseFlag(args, "--after");
|
|
221
395
|
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
222
396
|
|
|
223
|
-
const
|
|
397
|
+
const qs = new URLSearchParams();
|
|
398
|
+
if (limit) qs.set("limit", limit);
|
|
399
|
+
if (after) qs.set("after", after);
|
|
400
|
+
const url = `${API}/mailboxes/v1/${mailboxId}/messages${qs.toString() ? "?" + qs.toString() : ""}`;
|
|
401
|
+
|
|
402
|
+
const res = await fetch(url, {
|
|
224
403
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
225
404
|
});
|
|
226
405
|
const data = await res.json();
|
|
@@ -302,6 +481,121 @@ async function getRaw(args) {
|
|
|
302
481
|
}
|
|
303
482
|
}
|
|
304
483
|
|
|
484
|
+
async function reply(args) {
|
|
485
|
+
let messageId = null;
|
|
486
|
+
let projectOpt = null;
|
|
487
|
+
let outputFile = null;
|
|
488
|
+
void outputFile;
|
|
489
|
+
for (let i = 0; i < args.length; i++) {
|
|
490
|
+
const a = args[i];
|
|
491
|
+
if (a === "--project" && args[i + 1]) { projectOpt = args[++i]; }
|
|
492
|
+
else if (a === "--html" || a === "--text" || a === "--subject" || a === "--from-name") { i++; }
|
|
493
|
+
else if (!a.startsWith("--") && !messageId) { messageId = a; }
|
|
494
|
+
}
|
|
495
|
+
const html = parseFlag(args, "--html");
|
|
496
|
+
const text = parseFlag(args, "--text");
|
|
497
|
+
const subjectOverride = parseFlag(args, "--subject");
|
|
498
|
+
const fromName = parseFlag(args, "--from-name");
|
|
499
|
+
const projectId = resolveProjectId(projectOpt);
|
|
500
|
+
const p = findProject(projectId);
|
|
501
|
+
|
|
502
|
+
if (!messageId) {
|
|
503
|
+
console.error(JSON.stringify({ status: "error", message: "Missing message_id. Usage: run402 email reply <message_id> --html \"...\"" }));
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
if (!html && !text) {
|
|
507
|
+
console.error(JSON.stringify({ status: "error", message: "Provide --html and/or --text for the reply body" }));
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
512
|
+
|
|
513
|
+
const getRes = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages/${messageId}`, {
|
|
514
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
515
|
+
});
|
|
516
|
+
const original = await getRes.json().catch(() => ({}));
|
|
517
|
+
if (!getRes.ok) {
|
|
518
|
+
console.error(JSON.stringify({ status: "error", http: getRes.status, message: "Failed to fetch original message", ...original }));
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const replyTo = original.from || original.from_address || original.sender || null;
|
|
523
|
+
if (!replyTo) {
|
|
524
|
+
console.error(JSON.stringify({ status: "error", message: "Original message has no from address to reply to", original_keys: Object.keys(original) }));
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
const origSubject = typeof original.subject === "string" ? original.subject : "";
|
|
528
|
+
const defaultSubject = origSubject && origSubject.toLowerCase().startsWith("re:")
|
|
529
|
+
? origSubject
|
|
530
|
+
: `Re: ${origSubject || "(no subject)"}`;
|
|
531
|
+
const replySubject = subjectOverride || defaultSubject;
|
|
532
|
+
|
|
533
|
+
const body = { to: replyTo, subject: replySubject, in_reply_to: messageId };
|
|
534
|
+
if (html) body.html = html;
|
|
535
|
+
if (text) body.text = text;
|
|
536
|
+
if (fromName) body.from_name = fromName;
|
|
537
|
+
|
|
538
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/messages`, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
541
|
+
body: JSON.stringify(body),
|
|
542
|
+
});
|
|
543
|
+
const data = await res.json().catch(() => ({}));
|
|
544
|
+
if (!res.ok) {
|
|
545
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
console.log(JSON.stringify({ status: "ok", message_id: data.id, to: data.to, subject: replySubject, in_reply_to: messageId }));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function deleteMailbox(args) {
|
|
553
|
+
let positional = null;
|
|
554
|
+
let projectOpt = null;
|
|
555
|
+
for (let i = 0; i < args.length; i++) {
|
|
556
|
+
const a = args[i];
|
|
557
|
+
if (a === "--project" && args[i + 1]) { projectOpt = args[++i]; }
|
|
558
|
+
else if (a === "--confirm") { /* flag */ }
|
|
559
|
+
else if (!a.startsWith("--") && !positional) { positional = a; }
|
|
560
|
+
}
|
|
561
|
+
const projectId = resolveProjectId(projectOpt);
|
|
562
|
+
const p = findProject(projectId);
|
|
563
|
+
const confirmed = args.includes("--confirm");
|
|
564
|
+
|
|
565
|
+
if (!confirmed) {
|
|
566
|
+
console.error(JSON.stringify({
|
|
567
|
+
status: "error",
|
|
568
|
+
message: "Destructive: deleting a mailbox is irreversible (drops all messages and webhook subscriptions). Re-run with --confirm to proceed.",
|
|
569
|
+
}));
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const mailboxId = positional || await requireMailboxId(projectId, p.service_key);
|
|
574
|
+
|
|
575
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}`, {
|
|
576
|
+
method: "DELETE",
|
|
577
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
578
|
+
});
|
|
579
|
+
if (res.status !== 204 && !res.ok) {
|
|
580
|
+
let errBody;
|
|
581
|
+
try { errBody = await res.json(); } catch { errBody = {}; }
|
|
582
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...errBody }));
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Clear the cached mailbox_id/address from the local keystore so future
|
|
587
|
+
// email commands re-discover (or fail-fast with "no mailbox found").
|
|
588
|
+
const store = loadKeyStore();
|
|
589
|
+
const proj = store.projects[projectId];
|
|
590
|
+
if (proj) {
|
|
591
|
+
delete proj.mailbox_id;
|
|
592
|
+
delete proj.mailbox_address;
|
|
593
|
+
saveKeyStore(store);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
console.log(JSON.stringify({ status: "ok", mailbox_id: mailboxId, deleted: true }));
|
|
597
|
+
}
|
|
598
|
+
|
|
305
599
|
async function status(args) {
|
|
306
600
|
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
307
601
|
const p = findProject(projectId);
|
|
@@ -327,13 +621,17 @@ async function status(args) {
|
|
|
327
621
|
|
|
328
622
|
export async function run(sub, args) {
|
|
329
623
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
624
|
+
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h")) && sub !== "webhooks") { console.log(SUB_HELP[sub] || HELP); process.exit(0); }
|
|
330
625
|
switch (sub) {
|
|
331
626
|
case "create": await create(args); break;
|
|
627
|
+
case "info": // fall through — 'info' is the preferred name; 'status' is a backward-compat alias
|
|
332
628
|
case "status": await status(args); break;
|
|
333
629
|
case "send": await send(args); break;
|
|
334
630
|
case "list": await list(args); break;
|
|
335
631
|
case "get": await get(args); break;
|
|
336
632
|
case "get-raw": await getRaw(args); break;
|
|
633
|
+
case "reply": await reply(args); break;
|
|
634
|
+
case "delete": await deleteMailbox(args); break;
|
|
337
635
|
case "webhooks": {
|
|
338
636
|
const { run: runWebhooks } = await import("./webhooks.mjs");
|
|
339
637
|
await runWebhooks(args[0], args.slice(1));
|
package/lib/functions.mjs
CHANGED
|
@@ -38,6 +38,95 @@ Notes:
|
|
|
38
38
|
- Deploy may require payment if the project lease has expired
|
|
39
39
|
`;
|
|
40
40
|
|
|
41
|
+
const SUB_HELP = {
|
|
42
|
+
deploy: `run402 functions deploy — Deploy a function to a project
|
|
43
|
+
|
|
44
|
+
Usage:
|
|
45
|
+
run402 functions deploy <project_id> <name> --file <file> [options]
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
<project_id> Target project ID
|
|
49
|
+
<name> Function name (used in the invoke URL path)
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--file <file> Required: path to the function source file
|
|
53
|
+
--timeout <s> Runtime timeout in seconds
|
|
54
|
+
--memory <mb> Memory in MB
|
|
55
|
+
--deps <pkg,...> Comma-separated npm deps to bundle
|
|
56
|
+
--schedule <cron> Cron schedule; pass '' to clear an existing schedule
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
Code must export a default async function:
|
|
60
|
+
export default async (req: Request) => Response
|
|
61
|
+
Deploy may require payment if the project lease has expired.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
run402 functions deploy abc123 stripe-webhook --file handler.ts
|
|
65
|
+
run402 functions deploy abc123 send-reminders --file remind.ts \\
|
|
66
|
+
--schedule '*/15 * * * *'
|
|
67
|
+
run402 functions deploy abc123 send-reminders --file remind.ts --schedule ''
|
|
68
|
+
`,
|
|
69
|
+
invoke: `run402 functions invoke — Invoke a deployed function
|
|
70
|
+
|
|
71
|
+
Usage:
|
|
72
|
+
run402 functions invoke <project_id> <name> [options]
|
|
73
|
+
|
|
74
|
+
Arguments:
|
|
75
|
+
<project_id> Target project ID
|
|
76
|
+
<name> Function name
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--method <M> HTTP method (default POST)
|
|
80
|
+
--body <json> Request body (ignored for GET/HEAD)
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
run402 functions invoke abc123 stripe-webhook --body '{"event":"test"}'
|
|
84
|
+
run402 functions invoke abc123 ping --method GET
|
|
85
|
+
`,
|
|
86
|
+
logs: `run402 functions logs — Fetch or tail function logs
|
|
87
|
+
|
|
88
|
+
Usage:
|
|
89
|
+
run402 functions logs <project_id> <name> [options]
|
|
90
|
+
|
|
91
|
+
Arguments:
|
|
92
|
+
<project_id> Target project ID
|
|
93
|
+
<name> Function name
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--tail <n> Number of most-recent entries (default 50)
|
|
97
|
+
--since <ts> ISO timestamp or epoch ms; only entries after this
|
|
98
|
+
--follow Poll every 3s and stream new entries (Ctrl-C to stop)
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
run402 functions logs abc123 stripe-webhook --tail 100
|
|
102
|
+
run402 functions logs abc123 stripe-webhook --since 2026-03-29T14:00:00Z
|
|
103
|
+
run402 functions logs abc123 stripe-webhook --follow
|
|
104
|
+
`,
|
|
105
|
+
update: `run402 functions update — Update function config without re-deploying
|
|
106
|
+
|
|
107
|
+
Usage:
|
|
108
|
+
run402 functions update <project_id> <name> [options]
|
|
109
|
+
|
|
110
|
+
Arguments:
|
|
111
|
+
<project_id> Target project ID
|
|
112
|
+
<name> Function name
|
|
113
|
+
|
|
114
|
+
Options:
|
|
115
|
+
--schedule <cron> New cron schedule (pass '' to clear)
|
|
116
|
+
--schedule-remove Explicitly remove the schedule
|
|
117
|
+
--timeout <s> Runtime timeout in seconds
|
|
118
|
+
--memory <mb> Memory in MB
|
|
119
|
+
|
|
120
|
+
Notes:
|
|
121
|
+
Must provide at least one of the options above.
|
|
122
|
+
|
|
123
|
+
Examples:
|
|
124
|
+
run402 functions update abc123 send-reminders --schedule '0 */4 * * *'
|
|
125
|
+
run402 functions update abc123 send-reminders --schedule-remove
|
|
126
|
+
run402 functions update abc123 my-func --timeout 15 --memory 256
|
|
127
|
+
`,
|
|
128
|
+
};
|
|
129
|
+
|
|
41
130
|
async function deploy(projectId, name, args) {
|
|
42
131
|
const p = findProject(projectId);
|
|
43
132
|
const opts = { file: null, timeout: undefined, memory: undefined, deps: undefined, schedule: undefined };
|
|
@@ -213,7 +302,7 @@ async function deleteFunction(projectId, name) {
|
|
|
213
302
|
export async function run(sub, args) {
|
|
214
303
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
215
304
|
if (Array.isArray(args) && (args.includes("--help") || args.includes("-h"))) {
|
|
216
|
-
console.log(HELP);
|
|
305
|
+
console.log(SUB_HELP[sub] || HELP);
|
|
217
306
|
process.exit(0);
|
|
218
307
|
}
|
|
219
308
|
switch (sub) {
|
package/lib/init.mjs
CHANGED
|
@@ -112,16 +112,23 @@ export async function run(args = []) {
|
|
|
112
112
|
});
|
|
113
113
|
const data = await res.json();
|
|
114
114
|
if (data.result) {
|
|
115
|
-
// Tempo faucet is instant
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
// Tempo faucet is "instant" on-chain, but the client RPC read can be
|
|
116
|
+
// racy relative to faucet settlement — poll up to 30s (GH-81), mirroring
|
|
117
|
+
// the x402 path below.
|
|
118
|
+
for (let i = 0; i < 30; i++) {
|
|
119
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
120
|
+
try {
|
|
121
|
+
const raw = await client.readContract({ address: PATH_USD, abi: USDC_ABI, functionName: "balanceOf", args: [allowance.address] });
|
|
122
|
+
balance = Number(raw);
|
|
123
|
+
if (balance > 0) break;
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
120
126
|
saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
|
|
127
|
+
summary.allowance.funded = true;
|
|
121
128
|
if (balance > 0) {
|
|
122
129
|
line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD (funded)`);
|
|
123
130
|
} else {
|
|
124
|
-
line("Balance", "faucet sent —
|
|
131
|
+
line("Balance", "faucet sent — not yet confirmed on-chain");
|
|
125
132
|
}
|
|
126
133
|
} else {
|
|
127
134
|
line("Balance", `faucet failed: ${data.error?.message || "unknown error"}`);
|
|
@@ -131,6 +138,7 @@ export async function run(args = []) {
|
|
|
131
138
|
}
|
|
132
139
|
} else {
|
|
133
140
|
line("Balance", `${(balance / 1e6).toFixed(2)} pathUSD`);
|
|
141
|
+
summary.allowance.funded = balance > 0;
|
|
134
142
|
}
|
|
135
143
|
summary.balance = { symbol: "pathUSD", usd_micros: balance };
|
|
136
144
|
} else {
|
|
@@ -162,6 +170,7 @@ export async function run(args = []) {
|
|
|
162
170
|
} catch {}
|
|
163
171
|
}
|
|
164
172
|
saveAllowance({ ...allowance, funded: true, lastFaucet: new Date().toISOString() });
|
|
173
|
+
summary.allowance.funded = true;
|
|
165
174
|
if (balance > 0) {
|
|
166
175
|
line("Balance", `${(balance / 1e6).toFixed(2)} USDC (funded)`);
|
|
167
176
|
} else {
|
|
@@ -174,6 +183,7 @@ export async function run(args = []) {
|
|
|
174
183
|
}
|
|
175
184
|
} else {
|
|
176
185
|
line("Balance", `${(balance / 1e6).toFixed(2)} USDC`);
|
|
186
|
+
summary.allowance.funded = balance > 0;
|
|
177
187
|
}
|
|
178
188
|
summary.balance = { symbol: "USDC", usd_micros: balance };
|
|
179
189
|
}
|