posthorn 0.2.0 → 0.2.4
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/README.md +19 -3
- package/dist/index.js +138 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,12 @@ PRINCIPLE: domain purchase ALWAYS happens on the user's own Cloudflare account
|
|
|
106
106
|
and card. Posthorn never owns or bills for domains.
|
|
107
107
|
|
|
108
108
|
1. NEW domain → requires the user's own Cloudflare account (+ payment method).
|
|
109
|
-
- `posthorn domains check name1.com name2.io --cloudflare <
|
|
109
|
+
- `posthorn domains check name1.com name2.io --cloudflare <id>` — fast availability + price
|
|
110
|
+
- `posthorn domains reputation <domain>` — VET it before buying (Spamhaus
|
|
111
|
+
blocklist/score + archive.org prior-use). Verdicts: clean / caution (used
|
|
112
|
+
before) / avoid (blocklisted or poor reputation). Run this on the candidate
|
|
113
|
+
before `buy` — you can't tell a tainted domain from its name. (Slower + uses
|
|
114
|
+
a quota, so it's a separate explicit call, not part of `check`.)
|
|
110
115
|
- `posthorn domains buy <domain> --cloudflare <id> --contact '{...}'`
|
|
111
116
|
|
|
112
117
|
2. EXISTING domain, recommended → user's own Cloudflare account.
|
|
@@ -147,10 +152,21 @@ Anonymous tier can do setup (accounts, domains). To send/warmup, verify:
|
|
|
147
152
|
- `posthorn warmup list` → all campaigns
|
|
148
153
|
- Warmup never fully stops on its own — pause with `posthorn warmup pause <id>`.
|
|
149
154
|
|
|
155
|
+
## Feedback (bugs, feature requests)
|
|
156
|
+
|
|
157
|
+
`posthorn feedback --type bug|feature|question|other "<message>"`
|
|
158
|
+
|
|
159
|
+
If you hit a bug, missing capability, or rough edge while driving this CLI,
|
|
160
|
+
report it — it goes straight to the Posthorn team. Agents: you are encouraged
|
|
161
|
+
to file feedback yourself when something doesn't work the way you expected
|
|
162
|
+
(include the command you ran and what you expected). Works on any tier, no
|
|
163
|
+
verification needed.
|
|
164
|
+
|
|
150
165
|
## Tips for agents
|
|
151
166
|
|
|
152
|
-
-
|
|
153
|
-
|
|
167
|
+
- Read commands auto-detect output: when stdout isn't a TTY (i.e. you're an agent
|
|
168
|
+
capturing output), they emit JSON automatically — no flag needed. Humans in a
|
|
169
|
+
terminal get formatted text. Force either way with `--json` / `--pretty`.
|
|
154
170
|
- Async steps (domain provisioning, nameserver propagation) need polling, not blocking.
|
|
155
171
|
- Every command stores state locally (~/.config), so the user doesn't re-enter keys.
|
|
156
172
|
- Translate everything into plain English for the user. Never show them raw JSON
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk7 from "chalk";
|
|
6
6
|
|
|
7
7
|
// src/commands/auth.ts
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -46,6 +46,13 @@ function getConfigPath() {
|
|
|
46
46
|
return conf.path;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// src/output.ts
|
|
50
|
+
function useJson(options = {}) {
|
|
51
|
+
if (options.json) return true;
|
|
52
|
+
if (options.pretty) return false;
|
|
53
|
+
return !process.stdout.isTTY;
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
// src/commands/auth.ts
|
|
50
57
|
async function register(options) {
|
|
51
58
|
const apiUrl = options.url ?? "https://api-production-08f2.up.railway.app";
|
|
@@ -80,7 +87,7 @@ async function register(options) {
|
|
|
80
87
|
}
|
|
81
88
|
async function status(options = {}) {
|
|
82
89
|
const config = getConfig();
|
|
83
|
-
if (options
|
|
90
|
+
if (useJson(options)) {
|
|
84
91
|
console.log(JSON.stringify({
|
|
85
92
|
loggedIn: !!config.apiKey,
|
|
86
93
|
apiUrl: config.apiUrl,
|
|
@@ -128,7 +135,7 @@ var ApiError = class extends Error {
|
|
|
128
135
|
async function api(path, options = {}) {
|
|
129
136
|
const config = getConfig();
|
|
130
137
|
if (!config.apiKey) {
|
|
131
|
-
throw new Error("Not logged in. Run: posthorn
|
|
138
|
+
throw new Error("Not logged in. Run: posthorn auth register");
|
|
132
139
|
}
|
|
133
140
|
const url = `${config.apiUrl}${path}`;
|
|
134
141
|
const res = await fetch(url, {
|
|
@@ -151,6 +158,21 @@ async function api(path, options = {}) {
|
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
// src/commands/accounts.ts
|
|
161
|
+
var SERVICE_ACCOUNT_CLIENT_ID = "110137377718772968374";
|
|
162
|
+
var DELEGATION_SCOPES = "https://mail.google.com/,https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.domain,https://www.googleapis.com/auth/siteverification";
|
|
163
|
+
function printDelegationHelp(adminEmail) {
|
|
164
|
+
const domain = adminEmail.includes("@") ? adminEmail.split("@")[1] : "your-domain.com";
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(chalk2.bold(" Set up domain-wide delegation first (one-time per Workspace org):"));
|
|
167
|
+
console.log(` 1. Go to ${chalk2.cyan("admin.google.com")} signed in as a ${chalk2.bold("super-admin")} of ${chalk2.bold(domain)}`);
|
|
168
|
+
console.log(" 2. Security \u2192 Access and data control \u2192 API controls \u2192 Domain-wide delegation \u2192 Add new");
|
|
169
|
+
console.log(` 3. Client ID: ${chalk2.bold(SERVICE_ACCOUNT_CLIENT_ID)}`);
|
|
170
|
+
console.log(" 4. OAuth scopes (paste all, comma-separated):");
|
|
171
|
+
console.log(` ${chalk2.green(DELEGATION_SCOPES)}`);
|
|
172
|
+
console.log(" 5. Authorize, then re-run this command.");
|
|
173
|
+
console.log(chalk2.dim(` The admin email you pass must be a super-admin of ${domain}'s own org.`));
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
154
176
|
async function connectCloudflare(token, options) {
|
|
155
177
|
const spinner = ora2("Connecting Cloudflare...").start();
|
|
156
178
|
const data = await api("/api/accounts", {
|
|
@@ -176,10 +198,21 @@ async function connectCloudflare(token, options) {
|
|
|
176
198
|
}
|
|
177
199
|
async function connectWorkspace(adminEmail) {
|
|
178
200
|
const spinner = ora2("Connecting Google Workspace...").start();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
let data;
|
|
202
|
+
try {
|
|
203
|
+
data = await api("/api/accounts/workspace", {
|
|
204
|
+
method: "POST",
|
|
205
|
+
body: { adminEmail }
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
spinner.fail("Could not connect Google Workspace.");
|
|
209
|
+
if (err instanceof ApiError) {
|
|
210
|
+
console.log(chalk2.red(` ${err.message}`));
|
|
211
|
+
}
|
|
212
|
+
printDelegationHelp(adminEmail);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
183
216
|
setConfig({
|
|
184
217
|
workspaceConnected: true,
|
|
185
218
|
workspaceAccountId: data.account.id
|
|
@@ -206,7 +239,7 @@ import chalk3 from "chalk";
|
|
|
206
239
|
import ora3 from "ora";
|
|
207
240
|
async function listDomains(options = {}) {
|
|
208
241
|
const data = await api("/api/domains");
|
|
209
|
-
if (options
|
|
242
|
+
if (useJson(options)) {
|
|
210
243
|
console.log(JSON.stringify(data.domains, null, 2));
|
|
211
244
|
return;
|
|
212
245
|
}
|
|
@@ -291,7 +324,7 @@ async function activateDomain(domainId) {
|
|
|
291
324
|
async function getDomain(domainId, options = {}) {
|
|
292
325
|
const data = await api(`/api/domains/${domainId}`);
|
|
293
326
|
const d = data.domain;
|
|
294
|
-
if (options
|
|
327
|
+
if (useJson(options)) {
|
|
295
328
|
console.log(JSON.stringify(d, null, 2));
|
|
296
329
|
return;
|
|
297
330
|
}
|
|
@@ -302,6 +335,18 @@ async function getDomain(domainId, options = {}) {
|
|
|
302
335
|
console.log(` DNS: ${d.dns_mode === "managed" ? "managed by Posthorn" : "your Cloudflare"}`);
|
|
303
336
|
if (d.cloudflare_zone_id) console.log(` Zone: ${chalk3.dim(d.cloudflare_zone_id)}`);
|
|
304
337
|
}
|
|
338
|
+
function verdictText(rep) {
|
|
339
|
+
switch (rep.verdict) {
|
|
340
|
+
case "avoid":
|
|
341
|
+
return chalk3.red(`\u26A0 ${rep.verdictLabel} \u2014 avoid`);
|
|
342
|
+
case "caution":
|
|
343
|
+
return chalk3.yellow(rep.verdictLabel);
|
|
344
|
+
case "clean":
|
|
345
|
+
return chalk3.green(rep.verdictLabel);
|
|
346
|
+
default:
|
|
347
|
+
return chalk3.dim(rep.verdictLabel);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
305
350
|
async function checkDomains(domains2, options) {
|
|
306
351
|
const spinner = ora3("Checking availability...").start();
|
|
307
352
|
const data = await api("/api/domains/check", {
|
|
@@ -309,11 +354,35 @@ async function checkDomains(domains2, options) {
|
|
|
309
354
|
body: { domains: domains2, cloudflareAccountId: options.cloudflare }
|
|
310
355
|
});
|
|
311
356
|
spinner.stop();
|
|
357
|
+
if (useJson(options)) {
|
|
358
|
+
console.log(JSON.stringify(data.results, null, 2));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
312
361
|
console.log(chalk3.bold("Domain Availability\n"));
|
|
313
362
|
for (const r of data.results) {
|
|
314
363
|
const icon = r.available ? chalk3.green(" \u2713") : chalk3.red(" \u2717");
|
|
315
364
|
const price = r.price ? chalk3.dim(`$${r.price}/yr`) : "";
|
|
316
|
-
console.log(`${icon} ${r.domain}
|
|
365
|
+
console.log(`${icon} ${String(r.domain).padEnd(30)}${price}`);
|
|
366
|
+
}
|
|
367
|
+
console.log(chalk3.dim("\n Vet a domain before buying: posthorn domains reputation <domain>"));
|
|
368
|
+
}
|
|
369
|
+
async function domainReputation(domains2, options = {}) {
|
|
370
|
+
const spinner = ora3("Checking reputation (blocklist + history)...").start();
|
|
371
|
+
const data = await api("/api/domains/reputation", {
|
|
372
|
+
method: "POST",
|
|
373
|
+
body: { domains: domains2 }
|
|
374
|
+
});
|
|
375
|
+
spinner.stop();
|
|
376
|
+
if (useJson(options)) {
|
|
377
|
+
console.log(JSON.stringify(data.results, null, 2));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
console.log(chalk3.bold("Domain Reputation\n"));
|
|
381
|
+
for (const rep of data.results) {
|
|
382
|
+
console.log(` ${chalk3.cyan(rep.domain.padEnd(28))}${verdictText(rep)}`);
|
|
383
|
+
for (const w of rep.warnings) {
|
|
384
|
+
console.log(chalk3.dim(` \u2022 ${w}`));
|
|
385
|
+
}
|
|
317
386
|
}
|
|
318
387
|
}
|
|
319
388
|
|
|
@@ -377,8 +446,8 @@ async function provisionCredentials(mailboxId, options) {
|
|
|
377
446
|
method: "POST",
|
|
378
447
|
body: { authType: options.type ?? "xoauth2" }
|
|
379
448
|
});
|
|
380
|
-
if (data.connectivity.
|
|
381
|
-
spinner.succeed(
|
|
449
|
+
if (data.connectivity.send && data.connectivity.imap) {
|
|
450
|
+
spinner.succeed(`Credentials provisioned \u2014 sending (${data.connectivity.sendMethod}) and IMAP connected!`);
|
|
382
451
|
} else {
|
|
383
452
|
spinner.warn("Credentials provisioned but connectivity issues:");
|
|
384
453
|
for (const err of data.connectivity.errors) {
|
|
@@ -410,7 +479,7 @@ async function pauseWarmup(campaignId) {
|
|
|
410
479
|
}
|
|
411
480
|
async function warmupStats(campaignId, options = {}) {
|
|
412
481
|
const data = await api(`/api/warmup/campaigns/${campaignId}`);
|
|
413
|
-
if (options
|
|
482
|
+
if (useJson(options)) {
|
|
414
483
|
console.log(JSON.stringify(data, null, 2));
|
|
415
484
|
return;
|
|
416
485
|
}
|
|
@@ -450,14 +519,53 @@ async function listCampaigns() {
|
|
|
450
519
|
}
|
|
451
520
|
}
|
|
452
521
|
|
|
453
|
-
// src/commands/
|
|
522
|
+
// src/commands/feedback.ts
|
|
454
523
|
import { readFileSync } from "fs";
|
|
455
524
|
import { fileURLToPath } from "url";
|
|
456
525
|
import { dirname, join } from "path";
|
|
457
|
-
|
|
526
|
+
import chalk6 from "chalk";
|
|
527
|
+
function cliVersion() {
|
|
458
528
|
try {
|
|
459
529
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
460
|
-
|
|
530
|
+
return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8")).version;
|
|
531
|
+
} catch {
|
|
532
|
+
return "unknown";
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function sendFeedback(messageWords, options) {
|
|
536
|
+
const message = messageWords.join(" ").trim();
|
|
537
|
+
if (!message) {
|
|
538
|
+
console.error(chalk6.red("Feedback message is required."));
|
|
539
|
+
console.error(chalk6.dim(' Example: posthorn feedback --type feature "bulk domain checks would save me round trips"'));
|
|
540
|
+
process.exitCode = 1;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const data = await api("/api/feedback", {
|
|
544
|
+
method: "POST",
|
|
545
|
+
body: {
|
|
546
|
+
message,
|
|
547
|
+
type: options.type ?? "other",
|
|
548
|
+
metadata: {
|
|
549
|
+
cliVersion: cliVersion(),
|
|
550
|
+
platform: process.platform,
|
|
551
|
+
agent: !process.stdout.isTTY
|
|
552
|
+
// non-TTY usually means an agent is driving
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
console.log(chalk6.green("Feedback recorded \u2014 thank you!"));
|
|
557
|
+
console.log(` id: ${chalk6.dim(data.feedback.id)}`);
|
|
558
|
+
console.log(` type: ${chalk6.dim(data.feedback.type)}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/commands/guide.ts
|
|
562
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
563
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
564
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
565
|
+
function guide() {
|
|
566
|
+
try {
|
|
567
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
568
|
+
const readme = readFileSync2(join2(here, "..", "README.md"), "utf8");
|
|
461
569
|
console.log(readme);
|
|
462
570
|
} catch {
|
|
463
571
|
console.log(
|
|
@@ -468,7 +576,7 @@ function guide() {
|
|
|
468
576
|
|
|
469
577
|
// src/index.ts
|
|
470
578
|
var program = new Command();
|
|
471
|
-
program.name("posthorn").description("Posthorn \u2014 domain setup, mailbox creation, and email warmup").version("0.
|
|
579
|
+
program.name("posthorn").description("Posthorn \u2014 domain setup, mailbox creation, and email warmup").version("0.2.4");
|
|
472
580
|
program.addHelpText("after", `
|
|
473
581
|
Agents: run 'posthorn guide' first for the full workflow playbook.
|
|
474
582
|
|
|
@@ -483,21 +591,23 @@ Typical flow:
|
|
|
483
591
|
posthorn auth verify unlock sending + warmup
|
|
484
592
|
posthorn warmup start <mailbox-id>
|
|
485
593
|
|
|
486
|
-
|
|
594
|
+
Output: read commands auto-detect \u2014 agents (non-TTY) get JSON, humans get
|
|
595
|
+
formatted text. Force either with --json or --pretty.
|
|
487
596
|
`);
|
|
488
597
|
program.command("guide").description("Print the full agent playbook \u2014 workflow, external steps, and how to drive the CLI. Run this first.").action(guide);
|
|
489
598
|
var auth = program.command("auth").description("Account management");
|
|
490
599
|
auth.command("register").description("Create a new account and get an API key").option("--url <url>", "API server URL", "https://api-production-08f2.up.railway.app").action(register);
|
|
491
|
-
auth.command("status").description("Show current account info and setup progress").option("--json", "
|
|
600
|
+
auth.command("status").description("Show current account info and setup progress").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(status);
|
|
492
601
|
auth.command("logout").description("Clear stored credentials").action(logout);
|
|
493
602
|
var accounts = program.command("accounts").description("Connected accounts (Cloudflare, Workspace)");
|
|
494
603
|
accounts.command("list").description("List connected accounts").action(listAccounts);
|
|
495
604
|
accounts.command("cloudflare <token>").description("Connect a Cloudflare account").option("--label <label>", "Account label", "main").action(connectCloudflare);
|
|
496
605
|
accounts.command("workspace <admin-email>").description("Connect Google Workspace via domain-wide delegation").action(connectWorkspace);
|
|
497
606
|
var domains = program.command("domains").description("Domain management");
|
|
498
|
-
domains.command("list").description("List all domains").option("--json", "
|
|
499
|
-
domains.command("get <domain-id>").description("Get domain details (poll this until status is 'ready')").option("--json", "
|
|
500
|
-
domains.command("check <domains...>").description("Check domain availability").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").action(checkDomains);
|
|
607
|
+
domains.command("list").description("List all domains").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(listDomains);
|
|
608
|
+
domains.command("get <domain-id>").description("Get domain details (poll this until status is 'ready')").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(getDomain);
|
|
609
|
+
domains.command("check <domains...>").description("Check domain availability + pricing (fast)").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(checkDomains);
|
|
610
|
+
domains.command("reputation <domains...>").description("Vet a domain before buying \u2014 blocklist + reputation + prior-use history").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(domainReputation);
|
|
501
611
|
domains.command("buy <domain>").description("Purchase a domain").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--contact <json>", "Registrant contact info as JSON").action(buyDomain);
|
|
502
612
|
domains.command("connect <domain>").description("Connect an existing domain on your own Cloudflare account").requiredOption("--cloudflare <account-id>", "Cloudflare account ID").option("--workspace <account-id>", "Workspace account ID").action(connectDomain);
|
|
503
613
|
domains.command("managed <domain>").description("Add an existing domain to Posthorn-managed DNS (returns nameservers to set at your registrar)").option("--workspace <account-id>", "Workspace account ID").action(managedDomain);
|
|
@@ -511,18 +621,19 @@ var warmup = program.command("warmup").description("Email warmup management");
|
|
|
511
621
|
warmup.command("list").description("List warmup campaigns").action(listCampaigns);
|
|
512
622
|
warmup.command("start <mailbox-id>").description("Start warming a mailbox").action(startWarmup);
|
|
513
623
|
warmup.command("pause <campaign-id>").description("Pause a warmup campaign").action(pauseWarmup);
|
|
514
|
-
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").option("--json", "
|
|
624
|
+
warmup.command("stats <campaign-id>").description("Show warmup stats and daily breakdown").option("--json", "Force JSON output").option("--pretty", "Force human-readable output").action(warmupStats);
|
|
625
|
+
program.command("feedback <message...>").description("Send feedback to the Posthorn team \u2014 bug reports, feature requests, anything").option("-t, --type <type>", "bug | feature | question | other", "other").action(sendFeedback);
|
|
515
626
|
program.hook("preAction", () => {
|
|
516
627
|
});
|
|
517
628
|
program.parseAsync(process.argv).catch((err) => {
|
|
518
629
|
if (err.statusCode === 401) {
|
|
519
|
-
console.log(
|
|
630
|
+
console.log(chalk7.red("\n Authentication failed. Run: posthorn auth register\n"));
|
|
520
631
|
} else if (err.statusCode === 403) {
|
|
521
|
-
console.log(
|
|
632
|
+
console.log(chalk7.red("\n Account not verified. Run: posthorn auth verify\n"));
|
|
522
633
|
} else if (err.statusCode === 429) {
|
|
523
|
-
console.log(
|
|
634
|
+
console.log(chalk7.yellow("\n Rate limit exceeded. Try again in a minute.\n"));
|
|
524
635
|
} else {
|
|
525
|
-
console.error(
|
|
636
|
+
console.error(chalk7.red(`
|
|
526
637
|
Error: ${err.message}
|
|
527
638
|
`));
|
|
528
639
|
}
|