run402 1.32.1 → 1.33.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/email.mjs +17 -0
- package/lib/webhooks.mjs +221 -0
- package/package.json +1 -1
- package/core-dist/wallet-auth.js +0 -62
- package/core-dist/wallet.js +0 -25
package/lib/email.mjs
CHANGED
|
@@ -13,6 +13,16 @@ Subcommands:
|
|
|
13
13
|
get <message_id> [--project <id>] Get a message with replies
|
|
14
14
|
get-raw <message_id> [--project <id>] [--output <file>]
|
|
15
15
|
Fetch raw RFC-822 bytes (inbound only)
|
|
16
|
+
webhooks <action> [args...] Manage webhooks (see below)
|
|
17
|
+
|
|
18
|
+
Webhook subcommands:
|
|
19
|
+
webhooks list [--project <id>] List webhooks
|
|
20
|
+
webhooks get <webhook_id> [--project <id>] Get a webhook
|
|
21
|
+
webhooks delete <webhook_id> [--project <id>] Delete a webhook
|
|
22
|
+
webhooks update <webhook_id> [--url <url>] [--events <e1,e2>] [--project <id>]
|
|
23
|
+
Update a webhook
|
|
24
|
+
webhooks register --url <url> --events <e1,e2> [--project <id>]
|
|
25
|
+
Register a new webhook
|
|
16
26
|
|
|
17
27
|
Send modes:
|
|
18
28
|
Template: --template <name> --var key=value [--var ...]
|
|
@@ -35,6 +45,8 @@ Examples:
|
|
|
35
45
|
run402 email list
|
|
36
46
|
run402 email get msg_abc123
|
|
37
47
|
run402 email get-raw msg_abc123 --output reply.eml
|
|
48
|
+
run402 email webhooks list
|
|
49
|
+
run402 email webhooks register --url https://example.com/hook --events delivery,bounced
|
|
38
50
|
|
|
39
51
|
Notes:
|
|
40
52
|
- One mailbox per project
|
|
@@ -322,6 +334,11 @@ export async function run(sub, args) {
|
|
|
322
334
|
case "list": await list(args); break;
|
|
323
335
|
case "get": await get(args); break;
|
|
324
336
|
case "get-raw": await getRaw(args); break;
|
|
337
|
+
case "webhooks": {
|
|
338
|
+
const { run: runWebhooks } = await import("./webhooks.mjs");
|
|
339
|
+
await runWebhooks(args[0], args.slice(1));
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
325
342
|
default:
|
|
326
343
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
327
344
|
console.log(HELP);
|
package/lib/webhooks.mjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { findProject, resolveProjectId, API, loadKeyStore, updateProject } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 email webhooks — Manage mailbox webhooks
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
run402 email webhooks <action> [args...]
|
|
7
|
+
|
|
8
|
+
Actions:
|
|
9
|
+
list [--project <id>] List webhooks
|
|
10
|
+
get <webhook_id> [--project <id>] Get a webhook
|
|
11
|
+
delete <webhook_id> [--project <id>] Delete a webhook
|
|
12
|
+
update <webhook_id> [--url <url>] [--events <e1,e2>] Update a webhook
|
|
13
|
+
register --url <url> --events <e1,e2> [--project <id>] Register a new webhook
|
|
14
|
+
|
|
15
|
+
Valid events: delivery, bounced, complained, reply_received
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
run402 email webhooks list
|
|
19
|
+
run402 email webhooks register --url https://example.com/hook --events delivery,bounced
|
|
20
|
+
run402 email webhooks update whk_123 --url https://new.example.com/hook
|
|
21
|
+
run402 email webhooks delete whk_123
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function parseFlag(args, flag) {
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
if (args[i] === flag && args[i + 1]) return args[i + 1];
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function resolveMailboxId(projectId, serviceKey) {
|
|
32
|
+
const store = loadKeyStore();
|
|
33
|
+
const proj = store.projects[projectId];
|
|
34
|
+
if (proj && proj.mailbox_id) return proj.mailbox_id;
|
|
35
|
+
|
|
36
|
+
const res = await fetch(`${API}/mailboxes/v1`, {
|
|
37
|
+
headers: { "Authorization": `Bearer ${serviceKey}` },
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const data = await res.json().catch(() => ({}));
|
|
41
|
+
throw Object.assign(new Error("Failed to resolve mailbox"), { http: res.status, ...data });
|
|
42
|
+
}
|
|
43
|
+
const body = await res.json();
|
|
44
|
+
const mailboxes = body.mailboxes || body;
|
|
45
|
+
if (!Array.isArray(mailboxes) || mailboxes.length === 0) {
|
|
46
|
+
throw new Error("No mailbox found. Run: run402 email create <slug>");
|
|
47
|
+
}
|
|
48
|
+
const mb = mailboxes[0];
|
|
49
|
+
updateProject(projectId, { mailbox_id: mb.mailbox_id, mailbox_address: mb.address });
|
|
50
|
+
return mb.mailbox_id;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function requireMailboxId(projectId, serviceKey) {
|
|
54
|
+
try {
|
|
55
|
+
return await resolveMailboxId(projectId, serviceKey);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const out = { status: "error", message: err.message };
|
|
58
|
+
if (err.http) out.http = err.http;
|
|
59
|
+
console.error(JSON.stringify(out));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function list(args) {
|
|
65
|
+
const projectId = resolveProjectId(parseFlag(args, "--project"));
|
|
66
|
+
const p = findProject(projectId);
|
|
67
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
68
|
+
|
|
69
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/webhooks`, {
|
|
70
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
71
|
+
});
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(JSON.stringify(data, null, 2));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function get(args) {
|
|
81
|
+
let webhookId = null;
|
|
82
|
+
let projectOpt = null;
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
|
|
85
|
+
else if (!args[i].startsWith("--") && !webhookId) { webhookId = args[i]; }
|
|
86
|
+
}
|
|
87
|
+
const projectId = resolveProjectId(projectOpt);
|
|
88
|
+
const p = findProject(projectId);
|
|
89
|
+
|
|
90
|
+
if (!webhookId) {
|
|
91
|
+
console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks get <webhook_id>" }));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
96
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/webhooks/${webhookId}`, {
|
|
97
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
98
|
+
});
|
|
99
|
+
const data = await res.json();
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
console.log(JSON.stringify(data, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function del(args) {
|
|
108
|
+
let webhookId = null;
|
|
109
|
+
let projectOpt = null;
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
|
|
112
|
+
else if (!args[i].startsWith("--") && !webhookId) { webhookId = args[i]; }
|
|
113
|
+
}
|
|
114
|
+
const projectId = resolveProjectId(projectOpt);
|
|
115
|
+
const p = findProject(projectId);
|
|
116
|
+
|
|
117
|
+
if (!webhookId) {
|
|
118
|
+
console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks delete <webhook_id>" }));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
123
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/webhooks/${webhookId}`, {
|
|
124
|
+
method: "DELETE",
|
|
125
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
126
|
+
});
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
let errBody;
|
|
129
|
+
try { errBody = await res.json(); } catch { errBody = {}; }
|
|
130
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...errBody }));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
console.log(JSON.stringify({ status: "ok", webhook_id: webhookId, deleted: true }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function update(args) {
|
|
137
|
+
let webhookId = null;
|
|
138
|
+
let projectOpt = null;
|
|
139
|
+
const url = parseFlag(args, "--url");
|
|
140
|
+
const eventsRaw = parseFlag(args, "--events");
|
|
141
|
+
|
|
142
|
+
for (let i = 0; i < args.length; i++) {
|
|
143
|
+
if (args[i] === "--project" && args[i + 1]) { projectOpt = args[++i]; }
|
|
144
|
+
else if (args[i] === "--url" || args[i] === "--events") { i++; }
|
|
145
|
+
else if (!args[i].startsWith("--") && !webhookId) { webhookId = args[i]; }
|
|
146
|
+
}
|
|
147
|
+
const projectId = resolveProjectId(projectOpt);
|
|
148
|
+
const p = findProject(projectId);
|
|
149
|
+
|
|
150
|
+
if (!webhookId) {
|
|
151
|
+
console.error(JSON.stringify({ status: "error", message: "Missing webhook_id. Usage: run402 email webhooks update <webhook_id> [--url <url>] [--events <e1,e2>]" }));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
if (!url && !eventsRaw) {
|
|
155
|
+
console.error(JSON.stringify({ status: "error", message: "Provide at least --url or --events" }));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const body = {};
|
|
160
|
+
if (url) body.url = url;
|
|
161
|
+
if (eventsRaw) body.events = eventsRaw.split(",").map(e => e.trim());
|
|
162
|
+
|
|
163
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
164
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/webhooks/${webhookId}`, {
|
|
165
|
+
method: "PATCH",
|
|
166
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
167
|
+
body: JSON.stringify(body),
|
|
168
|
+
});
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
console.log(JSON.stringify({ status: "ok", ...data }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function register(args) {
|
|
178
|
+
const url = parseFlag(args, "--url");
|
|
179
|
+
const eventsRaw = parseFlag(args, "--events");
|
|
180
|
+
const projectOpt = parseFlag(args, "--project");
|
|
181
|
+
const projectId = resolveProjectId(projectOpt);
|
|
182
|
+
const p = findProject(projectId);
|
|
183
|
+
|
|
184
|
+
if (!url) {
|
|
185
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --url. Usage: run402 email webhooks register --url <url> --events <e1,e2>" }));
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
if (!eventsRaw) {
|
|
189
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --events. Valid events: delivery, bounced, complained, reply_received" }));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const events = eventsRaw.split(",").map(e => e.trim());
|
|
194
|
+
const mailboxId = await requireMailboxId(projectId, p.service_key);
|
|
195
|
+
const res = await fetch(`${API}/mailboxes/v1/${mailboxId}/webhooks`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
198
|
+
body: JSON.stringify({ url, events }),
|
|
199
|
+
});
|
|
200
|
+
const data = await res.json();
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
console.log(JSON.stringify({ status: "ok", ...data }));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function run(sub, args) {
|
|
209
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
210
|
+
switch (sub) {
|
|
211
|
+
case "list": await list(args); break;
|
|
212
|
+
case "get": await get(args); break;
|
|
213
|
+
case "delete": await del(args); break;
|
|
214
|
+
case "update": await update(args); break;
|
|
215
|
+
case "register": await register(args); break;
|
|
216
|
+
default:
|
|
217
|
+
console.error(`Unknown webhooks action: ${sub}\n`);
|
|
218
|
+
console.log(HELP);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
package/package.json
CHANGED
package/core-dist/wallet-auth.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wallet auth helper — generates EIP-191 signature headers for Run402 API.
|
|
3
|
-
* Uses @noble/curves (lighter than viem) for signing.
|
|
4
|
-
*/
|
|
5
|
-
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
7
|
-
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
8
|
-
import { readWallet } from "./wallet.js";
|
|
9
|
-
/**
|
|
10
|
-
* EIP-191 personal_sign: sign a message with the wallet's private key.
|
|
11
|
-
*/
|
|
12
|
-
function personalSign(privateKeyHex, address, message) {
|
|
13
|
-
const msgBytes = new TextEncoder().encode(message);
|
|
14
|
-
const prefix = new TextEncoder().encode(`\x19Ethereum Signed Message:\n${msgBytes.length}`);
|
|
15
|
-
const prefixed = new Uint8Array(prefix.length + msgBytes.length);
|
|
16
|
-
prefixed.set(prefix);
|
|
17
|
-
prefixed.set(msgBytes, prefix.length);
|
|
18
|
-
const hash = keccak_256(prefixed);
|
|
19
|
-
const pkHex = privateKeyHex.startsWith("0x")
|
|
20
|
-
? privateKeyHex.slice(2)
|
|
21
|
-
: privateKeyHex;
|
|
22
|
-
const pkBytes = Uint8Array.from(Buffer.from(pkHex, "hex"));
|
|
23
|
-
const rawSig = secp256k1.sign(hash, pkBytes);
|
|
24
|
-
const sig = secp256k1.Signature.fromBytes(rawSig);
|
|
25
|
-
// Determine recovery bit by trying both and matching the address
|
|
26
|
-
let recovery = 0;
|
|
27
|
-
for (const v of [0, 1]) {
|
|
28
|
-
try {
|
|
29
|
-
const recovered = sig.addRecoveryBit(v).recoverPublicKey(hash);
|
|
30
|
-
const pubBytes = recovered.toBytes(false).slice(1); // uncompressed, drop 04 prefix
|
|
31
|
-
const addrBytes = keccak_256(pubBytes).slice(-20);
|
|
32
|
-
if ("0x" + bytesToHex(addrBytes) === address.toLowerCase()) {
|
|
33
|
-
recovery = v;
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const r = sig.r.toString(16).padStart(64, "0");
|
|
42
|
-
const s = sig.s.toString(16).padStart(64, "0");
|
|
43
|
-
const vHex = (recovery + 27).toString(16).padStart(2, "0");
|
|
44
|
-
return "0x" + r + s + vHex;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Get wallet auth headers for the Run402 API.
|
|
48
|
-
* Returns null if no wallet is configured.
|
|
49
|
-
*/
|
|
50
|
-
export function getWalletAuthHeaders(walletPath) {
|
|
51
|
-
const wallet = readWallet(walletPath);
|
|
52
|
-
if (!wallet || !wallet.address || !wallet.privateKey)
|
|
53
|
-
return null;
|
|
54
|
-
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
55
|
-
const signature = personalSign(wallet.privateKey, wallet.address, `run402:${timestamp}`);
|
|
56
|
-
return {
|
|
57
|
-
"X-Run402-Wallet": wallet.address,
|
|
58
|
-
"X-Run402-Signature": signature,
|
|
59
|
-
"X-Run402-Timestamp": timestamp,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
//# sourceMappingURL=wallet-auth.js.map
|
package/core-dist/wallet.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { getWalletPath } from "./config.js";
|
|
5
|
-
export function readWallet(path) {
|
|
6
|
-
const p = path ?? getWalletPath();
|
|
7
|
-
if (!existsSync(p))
|
|
8
|
-
return null;
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
export function saveWallet(data, path) {
|
|
17
|
-
const p = path ?? getWalletPath();
|
|
18
|
-
const dir = dirname(p);
|
|
19
|
-
mkdirSync(dir, { recursive: true });
|
|
20
|
-
const tmp = join(dir, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
|
|
21
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
22
|
-
renameSync(tmp, p);
|
|
23
|
-
chmodSync(p, 0o600);
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=wallet.js.map
|