@primitivedotdev/sdk 0.20.0 → 0.21.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/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +30 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +40 -5
- package/dist/{api-DNF21MDo.js → api-BjzvA2Fy.js} +63 -6
- package/dist/{index-C6ObsYjq.d.ts → index-QTYQpSFt.d.ts} +215 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/api-command.js +74 -13
- package/dist/oclif/auth.js +65 -10
- package/dist/oclif/commands/emails-latest.js +23 -12
- package/dist/oclif/commands/emails-poll.js +121 -0
- package/dist/oclif/commands/emails-wait.js +171 -0
- package/dist/oclif/commands/emails-watch.js +165 -0
- package/dist/oclif/commands/functions-deploy.js +15 -6
- package/dist/oclif/commands/functions-redeploy.js +15 -6
- package/dist/oclif/commands/login.js +18 -14
- package/dist/oclif/commands/logout.js +9 -8
- package/dist/oclif/commands/send.js +21 -7
- package/dist/oclif/commands/whoami.js +15 -6
- package/dist/oclif/fish-completion.js +1 -1
- package/dist/oclif/index.js +6 -0
- package/dist/openapi/openapi.generated.js +397 -2
- package/dist/openapi/operations.generated.js +305 -1
- package/oclif.manifest.json +1327 -281
- package/package.json +1 -1
|
@@ -80,6 +80,9 @@ export function formatRow(email, idWidth) {
|
|
|
80
80
|
const subjectCol = truncate(subject, SUBJECT_DISPLAY_WIDTH);
|
|
81
81
|
return `${id} ${received} ${from} ${to} ${subjectCol}`;
|
|
82
82
|
}
|
|
83
|
+
export function formatHeader(idWidth) {
|
|
84
|
+
return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
|
|
85
|
+
}
|
|
83
86
|
class EmailsLatestCommand extends Command {
|
|
84
87
|
static description = `Print the N most recent inbound emails as a one-line-per-row text table. Designed for quick triage and visual scanning. For programmatic access, use \`primitive emails:list-emails\` (full JSON envelope, cursor pagination, filters) or pass \`--json\` here for the same raw shape without pagination/filters.
|
|
85
88
|
|
|
@@ -88,19 +91,25 @@ class EmailsLatestCommand extends Command {
|
|
|
88
91
|
Output streams: the column header line is written to STDERR so the row data on STDOUT stays grep/awk-friendly. \`--json\` writes everything (including the envelope) to STDOUT and is equivalent to running \`emails:list-emails --limit N\` for the same N.`;
|
|
89
92
|
static summary = "Show the most recent inbound emails as a compact table";
|
|
90
93
|
static examples = [
|
|
91
|
-
"<%= config.bin %> emails
|
|
92
|
-
"<%= config.bin %> emails
|
|
93
|
-
"<%= config.bin %> emails
|
|
94
|
-
"<%= config.bin %> emails
|
|
94
|
+
"<%= config.bin %> emails latest",
|
|
95
|
+
"<%= config.bin %> emails latest --limit 25",
|
|
96
|
+
"<%= config.bin %> emails latest | head -1 | awk '{print $1}' # full UUID since piped",
|
|
97
|
+
"<%= config.bin %> emails latest --json | jq '.data[0].id'",
|
|
95
98
|
];
|
|
96
99
|
static flags = {
|
|
97
100
|
"api-key": Flags.string({
|
|
98
101
|
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
99
102
|
env: "PRIMITIVE_API_KEY",
|
|
100
103
|
}),
|
|
101
|
-
"base-url": Flags.string({
|
|
102
|
-
description: "API base URL
|
|
103
|
-
env: "
|
|
104
|
+
"api-base-url-1": Flags.string({
|
|
105
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
106
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
107
|
+
hidden: true,
|
|
108
|
+
}),
|
|
109
|
+
"api-base-url-2": Flags.string({
|
|
110
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
111
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
112
|
+
hidden: true,
|
|
104
113
|
}),
|
|
105
114
|
limit: Flags.integer({
|
|
106
115
|
description: `Number of rows to print (1-${MAX_LIMIT}, default ${DEFAULT_LIMIT}).`,
|
|
@@ -121,15 +130,18 @@ class EmailsLatestCommand extends Command {
|
|
|
121
130
|
async run() {
|
|
122
131
|
const { flags } = await this.parse(EmailsLatestCommand);
|
|
123
132
|
await runWithTiming(flags.time, async () => {
|
|
124
|
-
const baseUrlOverridden = flags["base-url"] !== undefined
|
|
133
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
134
|
+
flags["api-base-url-2"] !== undefined;
|
|
125
135
|
const auth = resolveCliAuth({
|
|
126
136
|
apiKey: flags["api-key"],
|
|
127
|
-
|
|
137
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
138
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
128
139
|
configDir: this.config.configDir,
|
|
129
140
|
});
|
|
130
141
|
const apiClient = new PrimitiveApiClient({
|
|
131
142
|
apiKey: auth.apiKey,
|
|
132
|
-
|
|
143
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
144
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
133
145
|
});
|
|
134
146
|
const result = await listEmails({
|
|
135
147
|
client: apiClient.client,
|
|
@@ -163,8 +175,7 @@ class EmailsLatestCommand extends Command {
|
|
|
163
175
|
}
|
|
164
176
|
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
165
177
|
// Header on stderr so the table itself stays grep-friendly.
|
|
166
|
-
|
|
167
|
-
process.stderr.write(`${header}\n`);
|
|
178
|
+
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
168
179
|
for (const row of rows) {
|
|
169
180
|
this.log(formatRow(row, idWidth));
|
|
170
181
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { searchEmails } from "../../api/generated/sdk.gen.js";
|
|
2
|
+
export const DEFAULT_EMAIL_POLL_INTERVAL_SECONDS = 2;
|
|
3
|
+
export const DEFAULT_EMAIL_POLL_PAGE_SIZE = 50;
|
|
4
|
+
export const MAX_EMAIL_POLL_PAGE_SIZE = 100;
|
|
5
|
+
function quoteDslValue(value) {
|
|
6
|
+
if (/^[^\s"]+$/.test(value))
|
|
7
|
+
return value;
|
|
8
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
9
|
+
}
|
|
10
|
+
function combineQ(q, domain) {
|
|
11
|
+
const parts = [
|
|
12
|
+
q?.trim(),
|
|
13
|
+
domain ? `domain:${quoteDslValue(domain.trim())}` : undefined,
|
|
14
|
+
].filter((part) => Boolean(part));
|
|
15
|
+
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
16
|
+
}
|
|
17
|
+
export function normalizeIsoDate(value, label) {
|
|
18
|
+
const parsed = new Date(value);
|
|
19
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
20
|
+
throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
|
|
21
|
+
}
|
|
22
|
+
return parsed.toISOString();
|
|
23
|
+
}
|
|
24
|
+
export function filtersFromFlags(flags) {
|
|
25
|
+
return {
|
|
26
|
+
body: flags.body,
|
|
27
|
+
domain: flags.domain,
|
|
28
|
+
domainId: flags["domain-id"],
|
|
29
|
+
from: flags.from,
|
|
30
|
+
hasAttachment: flags["has-attachment"],
|
|
31
|
+
q: flags.q,
|
|
32
|
+
spamScoreGte: flags["spam-score-gte"],
|
|
33
|
+
spamScoreLt: flags["spam-score-lt"],
|
|
34
|
+
subject: flags.subject,
|
|
35
|
+
to: flags.to,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function sinceFromFlags(flags) {
|
|
39
|
+
if (flags.since)
|
|
40
|
+
return normalizeIsoDate(flags.since, "--since");
|
|
41
|
+
return flags["include-existing"] ? undefined : new Date().toISOString();
|
|
42
|
+
}
|
|
43
|
+
export function buildEmailSearchQuery(params) {
|
|
44
|
+
const query = {
|
|
45
|
+
include_facets: "false",
|
|
46
|
+
limit: params.pageSize,
|
|
47
|
+
snippet: "false",
|
|
48
|
+
sort: "received_at_asc",
|
|
49
|
+
};
|
|
50
|
+
const q = combineQ(params.filters.q, params.filters.domain);
|
|
51
|
+
if (q)
|
|
52
|
+
query.q = q;
|
|
53
|
+
if (params.filters.body)
|
|
54
|
+
query.body = params.filters.body;
|
|
55
|
+
if (params.filters.domainId)
|
|
56
|
+
query.domain_id = params.filters.domainId;
|
|
57
|
+
if (params.filters.from)
|
|
58
|
+
query.from = params.filters.from;
|
|
59
|
+
if (params.filters.hasAttachment !== undefined) {
|
|
60
|
+
query.has_attachment = params.filters.hasAttachment ? "true" : "false";
|
|
61
|
+
}
|
|
62
|
+
if (params.filters.spamScoreGte !== undefined) {
|
|
63
|
+
query.spam_score_gte = params.filters.spamScoreGte;
|
|
64
|
+
}
|
|
65
|
+
if (params.filters.spamScoreLt !== undefined) {
|
|
66
|
+
query.spam_score_lt = params.filters.spamScoreLt;
|
|
67
|
+
}
|
|
68
|
+
if (params.filters.subject)
|
|
69
|
+
query.subject = params.filters.subject;
|
|
70
|
+
if (params.filters.to)
|
|
71
|
+
query.to = params.filters.to;
|
|
72
|
+
if (params.since)
|
|
73
|
+
query.date_from = params.since;
|
|
74
|
+
if (params.cursor)
|
|
75
|
+
query.cursor = params.cursor;
|
|
76
|
+
return query;
|
|
77
|
+
}
|
|
78
|
+
export function encodeReceivedAtSearchCursor(email) {
|
|
79
|
+
const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
|
|
80
|
+
return Buffer.from(raw, "utf8").toString("base64url");
|
|
81
|
+
}
|
|
82
|
+
export function cursorFromRows(rows) {
|
|
83
|
+
const last = rows.at(-1);
|
|
84
|
+
return last ? encodeReceivedAtSearchCursor(last) : null;
|
|
85
|
+
}
|
|
86
|
+
export function collectNewAcceptedEmails(rows, seenIds) {
|
|
87
|
+
const fresh = [];
|
|
88
|
+
for (const row of rows) {
|
|
89
|
+
if (row.status !== "accepted" && row.status !== "completed")
|
|
90
|
+
continue;
|
|
91
|
+
if (seenIds.has(row.id))
|
|
92
|
+
continue;
|
|
93
|
+
seenIds.add(row.id);
|
|
94
|
+
fresh.push(row);
|
|
95
|
+
}
|
|
96
|
+
return fresh;
|
|
97
|
+
}
|
|
98
|
+
export async function fetchEmailSearchPage(params) {
|
|
99
|
+
const result = await searchEmails({
|
|
100
|
+
client: params.apiClient.client,
|
|
101
|
+
query: buildEmailSearchQuery({
|
|
102
|
+
cursor: params.cursor,
|
|
103
|
+
filters: params.filters,
|
|
104
|
+
pageSize: params.pageSize,
|
|
105
|
+
since: params.since,
|
|
106
|
+
}),
|
|
107
|
+
responseStyle: "fields",
|
|
108
|
+
});
|
|
109
|
+
if (result.error)
|
|
110
|
+
return { ok: false, error: result.error };
|
|
111
|
+
const envelope = result.data;
|
|
112
|
+
const rows = envelope?.data ?? [];
|
|
113
|
+
return {
|
|
114
|
+
ok: true,
|
|
115
|
+
cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
|
|
116
|
+
rows,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function sleep(ms) {
|
|
120
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
121
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
3
|
+
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
+
import { resolveCliAuth } from "../auth.js";
|
|
5
|
+
import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
|
|
6
|
+
import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
|
|
7
|
+
const DEFAULT_WAIT_TIMEOUT_SECONDS = 300;
|
|
8
|
+
function cliError(message) {
|
|
9
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
10
|
+
}
|
|
11
|
+
class EmailsWaitCommand extends Command {
|
|
12
|
+
static description = "Poll until matching inbound emails arrive, printing each match as it is found.";
|
|
13
|
+
static summary = "Wait for matching inbound emails";
|
|
14
|
+
static examples = [
|
|
15
|
+
"<%= config.bin %> emails wait --to test@example.com",
|
|
16
|
+
"<%= config.bin %> emails wait --subject verify --number 5 --timeout 120",
|
|
17
|
+
"<%= config.bin %> emails wait --q 'domain:example.com' --table",
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
"api-key": Flags.string({
|
|
21
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
22
|
+
env: "PRIMITIVE_API_KEY",
|
|
23
|
+
}),
|
|
24
|
+
"api-base-url-1": Flags.string({
|
|
25
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
26
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
27
|
+
hidden: true,
|
|
28
|
+
}),
|
|
29
|
+
"api-base-url-2": Flags.string({
|
|
30
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
31
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
32
|
+
hidden: true,
|
|
33
|
+
}),
|
|
34
|
+
body: Flags.string({
|
|
35
|
+
description: "Full-text body filter",
|
|
36
|
+
}),
|
|
37
|
+
domain: Flags.string({
|
|
38
|
+
description: "Filter by inbound email domain",
|
|
39
|
+
}),
|
|
40
|
+
"domain-id": Flags.string({
|
|
41
|
+
description: "Filter by domain UUID",
|
|
42
|
+
}),
|
|
43
|
+
from: Flags.string({
|
|
44
|
+
description: "Filter by sender address or domain",
|
|
45
|
+
}),
|
|
46
|
+
"has-attachment": Flags.boolean({
|
|
47
|
+
description: "Only match emails with one or more attachments",
|
|
48
|
+
}),
|
|
49
|
+
"include-existing": Flags.boolean({
|
|
50
|
+
description: "Start from existing matching emails instead of only new arrivals",
|
|
51
|
+
}),
|
|
52
|
+
interval: Flags.integer({
|
|
53
|
+
default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
|
|
54
|
+
description: "Seconds to wait between empty polls",
|
|
55
|
+
min: 1,
|
|
56
|
+
}),
|
|
57
|
+
number: Flags.integer({
|
|
58
|
+
char: "n",
|
|
59
|
+
default: 1,
|
|
60
|
+
description: "Exit successfully after this many matching emails",
|
|
61
|
+
min: 1,
|
|
62
|
+
}),
|
|
63
|
+
"page-size": Flags.integer({
|
|
64
|
+
default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
|
|
65
|
+
description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
|
|
66
|
+
max: MAX_EMAIL_POLL_PAGE_SIZE,
|
|
67
|
+
min: 1,
|
|
68
|
+
}),
|
|
69
|
+
q: Flags.string({
|
|
70
|
+
description: "Full-text search DSL query",
|
|
71
|
+
}),
|
|
72
|
+
since: Flags.string({
|
|
73
|
+
description: "Only match emails received on or after this date/time",
|
|
74
|
+
}),
|
|
75
|
+
"spam-score-gte": Flags.integer({
|
|
76
|
+
description: "Only match emails with spam score greater than or equal to this value",
|
|
77
|
+
}),
|
|
78
|
+
"spam-score-lt": Flags.integer({
|
|
79
|
+
description: "Only match emails with spam score below this value",
|
|
80
|
+
}),
|
|
81
|
+
subject: Flags.string({
|
|
82
|
+
description: "Full-text subject filter",
|
|
83
|
+
}),
|
|
84
|
+
table: Flags.boolean({
|
|
85
|
+
description: "Print a human-readable table instead of JSONL",
|
|
86
|
+
}),
|
|
87
|
+
timeout: Flags.integer({
|
|
88
|
+
default: DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
89
|
+
description: "Seconds to wait before exiting nonzero; 0 waits forever",
|
|
90
|
+
min: 0,
|
|
91
|
+
}),
|
|
92
|
+
to: Flags.string({
|
|
93
|
+
description: "Filter by recipient address or domain",
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
async run() {
|
|
97
|
+
const { flags } = await this.parse(EmailsWaitCommand);
|
|
98
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
99
|
+
flags["api-base-url-2"] !== undefined;
|
|
100
|
+
const auth = resolveCliAuth({
|
|
101
|
+
apiKey: flags["api-key"],
|
|
102
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
103
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
104
|
+
configDir: this.config.configDir,
|
|
105
|
+
});
|
|
106
|
+
const apiClient = new PrimitiveApiClient({
|
|
107
|
+
apiKey: auth.apiKey,
|
|
108
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
109
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
110
|
+
});
|
|
111
|
+
let since;
|
|
112
|
+
try {
|
|
113
|
+
since = sinceFromFlags(flags);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throw cliError(error instanceof Error ? error.message : String(error));
|
|
117
|
+
}
|
|
118
|
+
const filters = filtersFromFlags(flags);
|
|
119
|
+
const deadline = flags.timeout === 0 ? null : Date.now() + flags.timeout * 1000;
|
|
120
|
+
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
121
|
+
const seenIds = new Set();
|
|
122
|
+
let cursor = null;
|
|
123
|
+
let matched = 0;
|
|
124
|
+
let headerPrinted = false;
|
|
125
|
+
while (deadline === null || Date.now() < deadline) {
|
|
126
|
+
const page = await fetchEmailSearchPage({
|
|
127
|
+
apiClient,
|
|
128
|
+
cursor,
|
|
129
|
+
filters,
|
|
130
|
+
pageSize: flags["page-size"],
|
|
131
|
+
since,
|
|
132
|
+
});
|
|
133
|
+
if (!page.ok) {
|
|
134
|
+
const payload = extractErrorPayload(page.error);
|
|
135
|
+
writeErrorWithHints(payload);
|
|
136
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
137
|
+
auth,
|
|
138
|
+
baseUrlOverridden,
|
|
139
|
+
configDir: this.config.configDir,
|
|
140
|
+
payload,
|
|
141
|
+
});
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
cursor = page.cursor ?? cursor;
|
|
146
|
+
for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
|
|
147
|
+
if (flags.table) {
|
|
148
|
+
if (!headerPrinted) {
|
|
149
|
+
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
150
|
+
headerPrinted = true;
|
|
151
|
+
}
|
|
152
|
+
this.log(formatRow(email, idWidth));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.log(JSON.stringify(email));
|
|
156
|
+
}
|
|
157
|
+
matched += 1;
|
|
158
|
+
if (matched >= flags.number)
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (page.rows.length > 0)
|
|
162
|
+
continue;
|
|
163
|
+
if (deadline !== null && Date.now() >= deadline)
|
|
164
|
+
break;
|
|
165
|
+
await sleep(flags.interval * 1000);
|
|
166
|
+
}
|
|
167
|
+
process.stderr.write(`Timed out waiting for ${flags.number} matching email${flags.number === 1 ? "" : "s"}; received ${matched}.\n`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export default EmailsWaitCommand;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
2
|
+
import { PrimitiveApiClient } from "../../api/index.js";
|
|
3
|
+
import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, writeErrorWithHints, } from "../api-command.js";
|
|
4
|
+
import { resolveCliAuth } from "../auth.js";
|
|
5
|
+
import { formatHeader, formatRow, pickIdWidth } from "./emails-latest.js";
|
|
6
|
+
import { collectNewAcceptedEmails, DEFAULT_EMAIL_POLL_INTERVAL_SECONDS, DEFAULT_EMAIL_POLL_PAGE_SIZE, fetchEmailSearchPage, filtersFromFlags, MAX_EMAIL_POLL_PAGE_SIZE, sinceFromFlags, sleep, } from "./emails-poll.js";
|
|
7
|
+
function cliError(message) {
|
|
8
|
+
return new Errors.CLIError(message, { exit: 1 });
|
|
9
|
+
}
|
|
10
|
+
class EmailsWatchCommand extends Command {
|
|
11
|
+
static description = "Poll for new inbound emails and print matching messages as they arrive.";
|
|
12
|
+
static summary = "Watch inbound emails with filters";
|
|
13
|
+
static examples = [
|
|
14
|
+
"<%= config.bin %> emails watch --to support@example.com",
|
|
15
|
+
"<%= config.bin %> emails watch --subject verify --seconds 300",
|
|
16
|
+
"<%= config.bin %> emails watch --number 20 --jsonl",
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
"api-key": Flags.string({
|
|
20
|
+
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
21
|
+
env: "PRIMITIVE_API_KEY",
|
|
22
|
+
}),
|
|
23
|
+
"api-base-url-1": Flags.string({
|
|
24
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
25
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
26
|
+
hidden: true,
|
|
27
|
+
}),
|
|
28
|
+
"api-base-url-2": Flags.string({
|
|
29
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
30
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
31
|
+
hidden: true,
|
|
32
|
+
}),
|
|
33
|
+
body: Flags.string({
|
|
34
|
+
description: "Full-text body filter",
|
|
35
|
+
}),
|
|
36
|
+
domain: Flags.string({
|
|
37
|
+
description: "Filter by inbound email domain",
|
|
38
|
+
}),
|
|
39
|
+
"domain-id": Flags.string({
|
|
40
|
+
description: "Filter by domain UUID",
|
|
41
|
+
}),
|
|
42
|
+
from: Flags.string({
|
|
43
|
+
description: "Filter by sender address or domain",
|
|
44
|
+
}),
|
|
45
|
+
"has-attachment": Flags.boolean({
|
|
46
|
+
description: "Only show emails with one or more attachments",
|
|
47
|
+
}),
|
|
48
|
+
"include-existing": Flags.boolean({
|
|
49
|
+
description: "Start from existing matching emails instead of only new arrivals",
|
|
50
|
+
}),
|
|
51
|
+
interval: Flags.integer({
|
|
52
|
+
default: DEFAULT_EMAIL_POLL_INTERVAL_SECONDS,
|
|
53
|
+
description: "Seconds to wait between empty polls",
|
|
54
|
+
min: 1,
|
|
55
|
+
}),
|
|
56
|
+
jsonl: Flags.boolean({
|
|
57
|
+
description: "Print each email as one JSON object per line",
|
|
58
|
+
}),
|
|
59
|
+
number: Flags.integer({
|
|
60
|
+
description: "Exit after printing this many matching emails",
|
|
61
|
+
min: 1,
|
|
62
|
+
}),
|
|
63
|
+
"page-size": Flags.integer({
|
|
64
|
+
default: DEFAULT_EMAIL_POLL_PAGE_SIZE,
|
|
65
|
+
description: `Emails to fetch per poll (1-${MAX_EMAIL_POLL_PAGE_SIZE})`,
|
|
66
|
+
max: MAX_EMAIL_POLL_PAGE_SIZE,
|
|
67
|
+
min: 1,
|
|
68
|
+
}),
|
|
69
|
+
q: Flags.string({
|
|
70
|
+
description: "Full-text search DSL query",
|
|
71
|
+
}),
|
|
72
|
+
seconds: Flags.integer({
|
|
73
|
+
description: "Exit after this many seconds",
|
|
74
|
+
min: 1,
|
|
75
|
+
}),
|
|
76
|
+
since: Flags.string({
|
|
77
|
+
description: "Only show emails received on or after this date/time",
|
|
78
|
+
}),
|
|
79
|
+
"spam-score-gte": Flags.integer({
|
|
80
|
+
description: "Only show emails with spam score greater than or equal to this value",
|
|
81
|
+
}),
|
|
82
|
+
"spam-score-lt": Flags.integer({
|
|
83
|
+
description: "Only show emails with spam score below this value",
|
|
84
|
+
}),
|
|
85
|
+
subject: Flags.string({
|
|
86
|
+
description: "Full-text subject filter",
|
|
87
|
+
}),
|
|
88
|
+
to: Flags.string({
|
|
89
|
+
description: "Filter by recipient address or domain",
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
async run() {
|
|
93
|
+
const { flags } = await this.parse(EmailsWatchCommand);
|
|
94
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
95
|
+
flags["api-base-url-2"] !== undefined;
|
|
96
|
+
const auth = resolveCliAuth({
|
|
97
|
+
apiKey: flags["api-key"],
|
|
98
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
99
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
100
|
+
configDir: this.config.configDir,
|
|
101
|
+
});
|
|
102
|
+
const apiClient = new PrimitiveApiClient({
|
|
103
|
+
apiKey: auth.apiKey,
|
|
104
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
105
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
106
|
+
});
|
|
107
|
+
let since;
|
|
108
|
+
try {
|
|
109
|
+
since = sinceFromFlags(flags);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
throw cliError(error instanceof Error ? error.message : String(error));
|
|
113
|
+
}
|
|
114
|
+
const filters = filtersFromFlags(flags);
|
|
115
|
+
const deadline = flags.seconds ? Date.now() + flags.seconds * 1000 : null;
|
|
116
|
+
const idWidth = pickIdWidth(Boolean(process.stdout.isTTY));
|
|
117
|
+
const seenIds = new Set();
|
|
118
|
+
let cursor = null;
|
|
119
|
+
let printed = 0;
|
|
120
|
+
let headerPrinted = false;
|
|
121
|
+
while (deadline === null || Date.now() < deadline) {
|
|
122
|
+
const page = await fetchEmailSearchPage({
|
|
123
|
+
apiClient,
|
|
124
|
+
cursor,
|
|
125
|
+
filters,
|
|
126
|
+
pageSize: flags["page-size"],
|
|
127
|
+
since,
|
|
128
|
+
});
|
|
129
|
+
if (!page.ok) {
|
|
130
|
+
const payload = extractErrorPayload(page.error);
|
|
131
|
+
writeErrorWithHints(payload);
|
|
132
|
+
removeStaleSavedCredentialOnUnauthorized({
|
|
133
|
+
auth,
|
|
134
|
+
baseUrlOverridden,
|
|
135
|
+
configDir: this.config.configDir,
|
|
136
|
+
payload,
|
|
137
|
+
});
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
cursor = page.cursor ?? cursor;
|
|
142
|
+
for (const email of collectNewAcceptedEmails(page.rows, seenIds)) {
|
|
143
|
+
if (flags.jsonl) {
|
|
144
|
+
this.log(JSON.stringify(email));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (!headerPrinted) {
|
|
148
|
+
process.stderr.write(`${formatHeader(idWidth)}\n`);
|
|
149
|
+
headerPrinted = true;
|
|
150
|
+
}
|
|
151
|
+
this.log(formatRow(email, idWidth));
|
|
152
|
+
}
|
|
153
|
+
printed += 1;
|
|
154
|
+
if (flags.number && printed >= flags.number)
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (page.rows.length > 0)
|
|
158
|
+
continue;
|
|
159
|
+
if (deadline !== null && Date.now() >= deadline)
|
|
160
|
+
break;
|
|
161
|
+
await sleep(flags.interval * 1000);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export default EmailsWatchCommand;
|
|
@@ -36,9 +36,15 @@ class FunctionsDeployCommand extends Command {
|
|
|
36
36
|
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
37
37
|
env: "PRIMITIVE_API_KEY",
|
|
38
38
|
}),
|
|
39
|
-
"base-url": Flags.string({
|
|
40
|
-
description: "API base URL
|
|
41
|
-
env: "
|
|
39
|
+
"api-base-url-1": Flags.string({
|
|
40
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
41
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
42
|
+
hidden: true,
|
|
43
|
+
}),
|
|
44
|
+
"api-base-url-2": Flags.string({
|
|
45
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
46
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
47
|
+
hidden: true,
|
|
42
48
|
}),
|
|
43
49
|
name: Flags.string({
|
|
44
50
|
description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
|
|
@@ -66,15 +72,18 @@ class FunctionsDeployCommand extends Command {
|
|
|
66
72
|
const sourceMap = flags["source-map-file"]
|
|
67
73
|
? readTextFileFlag(flags["source-map-file"], "--source-map-file")
|
|
68
74
|
: undefined;
|
|
69
|
-
const baseUrlOverridden = flags["base-url"] !== undefined
|
|
75
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
76
|
+
flags["api-base-url-2"] !== undefined;
|
|
70
77
|
const auth = resolveCliAuth({
|
|
71
78
|
apiKey: flags["api-key"],
|
|
72
|
-
|
|
79
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
80
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
73
81
|
configDir: this.config.configDir,
|
|
74
82
|
});
|
|
75
83
|
const apiClient = new PrimitiveApiClient({
|
|
76
84
|
apiKey: auth.apiKey,
|
|
77
|
-
|
|
85
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
86
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
78
87
|
});
|
|
79
88
|
const authFailureContext = {
|
|
80
89
|
auth,
|
|
@@ -27,9 +27,15 @@ class FunctionsRedeployCommand extends Command {
|
|
|
27
27
|
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
28
28
|
env: "PRIMITIVE_API_KEY",
|
|
29
29
|
}),
|
|
30
|
-
"base-url": Flags.string({
|
|
31
|
-
description: "API base URL
|
|
32
|
-
env: "
|
|
30
|
+
"api-base-url-1": Flags.string({
|
|
31
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
32
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
33
|
+
hidden: true,
|
|
34
|
+
}),
|
|
35
|
+
"api-base-url-2": Flags.string({
|
|
36
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
37
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
38
|
+
hidden: true,
|
|
33
39
|
}),
|
|
34
40
|
id: Flags.string({
|
|
35
41
|
description: "Function id (UUID). The function must already exist.",
|
|
@@ -55,15 +61,18 @@ class FunctionsRedeployCommand extends Command {
|
|
|
55
61
|
const sourceMap = flags["source-map-file"]
|
|
56
62
|
? readTextFileFlag(flags["source-map-file"], "--source-map-file")
|
|
57
63
|
: undefined;
|
|
58
|
-
const baseUrlOverridden = flags["base-url"] !== undefined
|
|
64
|
+
const baseUrlOverridden = flags["api-base-url-1"] !== undefined ||
|
|
65
|
+
flags["api-base-url-2"] !== undefined;
|
|
59
66
|
const auth = resolveCliAuth({
|
|
60
67
|
apiKey: flags["api-key"],
|
|
61
|
-
|
|
68
|
+
apiBaseUrl1: flags["api-base-url-1"],
|
|
69
|
+
apiBaseUrl2: flags["api-base-url-2"],
|
|
62
70
|
configDir: this.config.configDir,
|
|
63
71
|
});
|
|
64
72
|
const apiClient = new PrimitiveApiClient({
|
|
65
73
|
apiKey: auth.apiKey,
|
|
66
|
-
|
|
74
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
75
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
67
76
|
});
|
|
68
77
|
const authFailureContext = {
|
|
69
78
|
auth,
|