@primitivedotdev/sdk 0.19.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 +235 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +40 -5
- package/dist/{api-C5VR_Opg.js → api-BjzvA2Fy.js} +308 -6
- package/dist/{index-oRkCqj6u.d.ts → index-QTYQpSFt.d.ts} +999 -60
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/oclif/api-command.js +87 -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 +117 -0
- package/dist/oclif/commands/functions-redeploy.js +106 -0
- 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 +16 -0
- package/dist/openapi/openapi.generated.js +1317 -51
- package/dist/openapi/operations.generated.js +995 -1
- package/oclif.manifest.json +2083 -335
- package/package.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as UnknownEvent, C as ParsedDataFailed, D as RawContentDownloadOnly, E as RawContent, M as WebhookAttachment, N as WebhookEvent, O as RawContentInline, S as ParsedDataComplete, T as ParsedStatus, _ as ForwardResultInline, a as DmarcPolicy, b as KnownWebhookEvent, c as EmailAnalysis, d as EventType, f as ForwardAnalysis, g as ForwardResultAttachmentSkipped, h as ForwardResultAttachmentAnalyzed, i as DkimSignature, j as ValidateEmailAuthResult, k as SpfResult, l as EmailAuth, m as ForwardResult, n as AuthVerdict, o as DmarcResult, p as ForwardOriginalSender, r as DkimResult, s as EmailAddress, t as AuthConfidence, u as EmailReceivedEvent, v as ForwardVerdict, w as ParsedError, x as ParsedData, y as ForwardVerification } from "./types-9vXGZjPd.js";
|
|
2
2
|
import { a as buildReplySubject, c as parseHeaderAddress, i as buildForwardSubject, n as ReceivedEmailAddress, o as formatAddress, r as ReceivedEmailThread, s as normalizeReceivedEmail, t as ReceivedEmail } from "./received-email-DNjpq_Wt.js";
|
|
3
|
-
import {
|
|
3
|
+
import { _ as createPrimitiveClient, c as PrimitiveClient, f as SendInput, h as client, l as PrimitiveClientOptions, m as SendThreadInput, o as PrimitiveApiError, p as SendResult, r as ForwardInput, u as ReplyInput } from "./index-QTYQpSFt.js";
|
|
4
4
|
import { A as VerifyOptions, B as PAYLOAD_ERRORS, C as signStandardWebhooksPayload, D as PRIMITIVE_CONFIRMED_HEADER, E as LEGACY_SIGNATURE_HEADER, F as VerifyDownloadTokenResult, G as VERIFICATION_ERRORS, H as RAW_EMAIL_ERRORS, I as generateDownloadToken, J as WebhookPayloadErrorCode, K as WebhookErrorCode, L as verifyDownloadToken, M as verifyWebhookSignature, N as GenerateDownloadTokenOptions, O as PRIMITIVE_SIGNATURE_HEADER, P as VerifyDownloadTokenOptions, Q as WebhookVerificationErrorCode, R as safeValidateEmailReceivedEvent, S as StandardWebhooksVerifyOptions, T as LEGACY_CONFIRMED_HEADER, U as RawEmailDecodeError, V as PrimitiveWebhookError, W as RawEmailDecodeErrorCode, X as WebhookValidationErrorCode, Y as WebhookValidationError, Z as WebhookVerificationError, _ as emailReceivedEventJsonSchema, a as confirmedHeaders, b as STANDARD_WEBHOOK_TIMESTAMP_HEADER, c as handleWebhook, d as isRawIncluded, f as parseWebhookEvent, g as validateEmailAuth, h as WEBHOOK_VERSION, i as WebhookHeaders, j as signWebhookPayload, k as SignResult, l as isDownloadExpired, m as verifyRawEmailDownload, n as HandleWebhookOptions, o as decodeRawEmail, p as receive, q as WebhookPayloadError, r as ReceiveRequestOptions, s as getDownloadTimeRemaining, t as DecodeRawEmailOptions, u as isEmailReceivedEvent, v as STANDARD_WEBHOOK_ID_HEADER, w as verifyStandardWebhooksSignature, x as StandardWebhooksSignResult, y as STANDARD_WEBHOOK_SIGNATURE_HEADER, z as validateEmailReceivedEvent } from "./index-CDlwyxdp.js";
|
|
5
5
|
|
|
6
6
|
//#region src/index.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as parseHeaderAddress, i as normalizeReceivedEmail, n as buildReplySubject, r as formatAddress, t as buildForwardSubject } from "./received-email-D6tKtWwW.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as PrimitiveClient, c as createPrimitiveClient, i as PrimitiveApiError, o as client } from "./api-BjzvA2Fy.js";
|
|
3
3
|
import { A as PRIMITIVE_CONFIRMED_HEADER, B as RAW_EMAIL_ERRORS, C as STANDARD_WEBHOOK_ID_HEADER, D as verifyStandardWebhooksSignature, E as signStandardWebhooksPayload, F as verifyDownloadToken, G as WebhookVerificationError, H as VERIFICATION_ERRORS, I as safeValidateEmailReceivedEvent, L as validateEmailReceivedEvent, M as signWebhookPayload, N as verifyWebhookSignature, O as LEGACY_CONFIRMED_HEADER, P as generateDownloadToken, R as PAYLOAD_ERRORS, S as emailReceivedEventJsonSchema, T as STANDARD_WEBHOOK_TIMESTAMP_HEADER, U as WebhookPayloadError, V as RawEmailDecodeError, W as WebhookValidationError, _ as DmarcResult, a as isDownloadExpired, b as ParsedStatus, c as parseWebhookEvent, d as WEBHOOK_VERSION, f as validateEmailAuth, g as DmarcPolicy, h as DkimResult, i as handleWebhook, j as PRIMITIVE_SIGNATURE_HEADER, k as LEGACY_SIGNATURE_HEADER, l as receive, m as AuthVerdict, n as decodeRawEmail, o as isEmailReceivedEvent, p as AuthConfidence, r as getDownloadTimeRemaining, s as isRawIncluded, t as confirmedHeaders, u as verifyRawEmailDownload, v as EventType, w as STANDARD_WEBHOOK_SIGNATURE_HEADER, x as SpfResult, y as ForwardVerdict, z as PrimitiveWebhookError } from "./webhook-rUjGV6Zu.js";
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
const primitive = {
|
|
@@ -224,6 +224,19 @@ export function readJsonBody(flags) {
|
|
|
224
224
|
}
|
|
225
225
|
return undefined;
|
|
226
226
|
}
|
|
227
|
+
// Read a UTF-8 text file off disk, mapping any failure to a CLIError
|
|
228
|
+
// tagged with the originating flag so the user sees which path failed
|
|
229
|
+
// to open. Used by hand-rolled commands that take a file-input flag
|
|
230
|
+
// (e.g. functions:deploy --file).
|
|
231
|
+
export function readTextFileFlag(path, flagLabel) {
|
|
232
|
+
try {
|
|
233
|
+
return readFileSync(path, "utf8");
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
237
|
+
throw cliError(`Could not read ${flagLabel} ${path}: ${detail}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
227
240
|
export function extractErrorPayload(raw) {
|
|
228
241
|
if (raw &&
|
|
229
242
|
typeof raw === "object" &&
|
|
@@ -320,9 +333,12 @@ export function removeStaleSavedCredentialOnUnauthorized(params) {
|
|
|
320
333
|
}
|
|
321
334
|
const baseUrlDiffersFromSaved = params.baseUrlOverridden &&
|
|
322
335
|
params.auth.credentials !== null &&
|
|
323
|
-
params.auth.
|
|
336
|
+
params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1;
|
|
324
337
|
if (baseUrlDiffersFromSaved) {
|
|
325
|
-
|
|
338
|
+
// Override env vars (PRIMITIVE_API_BASE_URL_1 / _2) are intentionally
|
|
339
|
+
// not advertised in --help; this hint is the only customer-visible
|
|
340
|
+
// mention. They're for internal staging/local testing.
|
|
341
|
+
process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; unset PRIMITIVE_API_BASE_URL_1, or run `primitive logout` to remove the stored credential.\n");
|
|
326
342
|
return false;
|
|
327
343
|
}
|
|
328
344
|
deleteCliCredentials(params.configDir);
|
|
@@ -368,10 +384,45 @@ export async function runWithTiming(enabled, fn) {
|
|
|
368
384
|
// own static flags. Lives here so the flag's description and short
|
|
369
385
|
// name stay consistent across the hand-coded and generated commands.
|
|
370
386
|
export const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
|
|
387
|
+
// Shared description text for the api-base-url override flags. Keeps
|
|
388
|
+
// the wording identical across every command that includes them. The
|
|
389
|
+
// flags themselves are hidden from --help (internal staging/local-only).
|
|
390
|
+
export const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
|
|
391
|
+
export const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
|
|
392
|
+
// Helper: was either api-base-url override set by the caller? Used by
|
|
393
|
+
// removeStaleSavedCredentialOnUnauthorized to decide whether to
|
|
394
|
+
// preserve the saved credential when a 401 comes back.
|
|
395
|
+
export function baseUrlOverriddenFromFlags(flags) {
|
|
396
|
+
return (typeof flags["api-base-url-1"] === "string" ||
|
|
397
|
+
typeof flags["api-base-url-2"] === "string");
|
|
398
|
+
}
|
|
399
|
+
// Helper: resolve auth from a parsed-flags bag. Mirrors what every CLI
|
|
400
|
+
// command does inline so the api-base-url-1 / api-base-url-2 mapping
|
|
401
|
+
// stays in one place as we add more migration knobs.
|
|
402
|
+
export function resolveCliAuthFromFlags(flags, configDir) {
|
|
403
|
+
return resolveCliAuth({
|
|
404
|
+
apiKey: typeof flags["api-key"] === "string"
|
|
405
|
+
? flags["api-key"]
|
|
406
|
+
: undefined,
|
|
407
|
+
apiBaseUrl1: typeof flags["api-base-url-1"] === "string"
|
|
408
|
+
? flags["api-base-url-1"]
|
|
409
|
+
: undefined,
|
|
410
|
+
apiBaseUrl2: typeof flags["api-base-url-2"] === "string"
|
|
411
|
+
? flags["api-base-url-2"]
|
|
412
|
+
: undefined,
|
|
413
|
+
configDir,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// Operations that route to the attachments-supporting host
|
|
417
|
+
// (apiBaseUrl2) instead of the primary API host. Internal to the CLI:
|
|
418
|
+
// as more operations migrate to host 2 over time, add their generated
|
|
419
|
+
// sdkName here. Today it is just /send-mail.
|
|
420
|
+
const HOST_2_OPERATIONS = new Set(["sendEmail"]);
|
|
371
421
|
// Reserved flag names the body-field expander must never overwrite.
|
|
372
422
|
// `--raw-body` and `--body-file` are the JSON escape hatches.
|
|
373
|
-
// `--api-key`, `--base-url`, `--output` are
|
|
374
|
-
// params get added before body fields and take
|
|
423
|
+
// `--api-key`, `--api-base-url-1`, `--api-base-url-2`, `--output` are
|
|
424
|
+
// infra. Path and query params get added before body fields and take
|
|
425
|
+
// precedence.
|
|
375
426
|
//
|
|
376
427
|
// Note: `--body` is intentionally NOT reserved here. The naive
|
|
377
428
|
// agent expectation (per AGX walkthrough) is that --body means
|
|
@@ -386,7 +437,8 @@ export const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this comm
|
|
|
386
437
|
// send` defines its own --body for the message text.
|
|
387
438
|
const RESERVED_FLAG_NAMES = new Set([
|
|
388
439
|
"api-key",
|
|
389
|
-
"base-url",
|
|
440
|
+
"api-base-url-1",
|
|
441
|
+
"api-base-url-2",
|
|
390
442
|
"raw-body",
|
|
391
443
|
"body-file",
|
|
392
444
|
"output",
|
|
@@ -425,9 +477,20 @@ function buildFlags(operation) {
|
|
|
425
477
|
description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
|
|
426
478
|
env: "PRIMITIVE_API_KEY",
|
|
427
479
|
}),
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
480
|
+
// Two override knobs for the dual-host setup. Hidden because they
|
|
481
|
+
// are for internal staging/local testing only. Production users
|
|
482
|
+
// should not override; the defaults route correctly. Env vars
|
|
483
|
+
// PRIMITIVE_API_BASE_URL_1 and PRIMITIVE_API_BASE_URL_2 carry the
|
|
484
|
+
// same semantics. Both are intentionally absent from --help output.
|
|
485
|
+
"api-base-url-1": Flags.string({
|
|
486
|
+
description: "Override the primary API base URL. Internal testing only; not documented to customers.",
|
|
487
|
+
env: "PRIMITIVE_API_BASE_URL_1",
|
|
488
|
+
hidden: true,
|
|
489
|
+
}),
|
|
490
|
+
"api-base-url-2": Flags.string({
|
|
491
|
+
description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
|
|
492
|
+
env: "PRIMITIVE_API_BASE_URL_2",
|
|
493
|
+
hidden: true,
|
|
431
494
|
}),
|
|
432
495
|
time: Flags.boolean({
|
|
433
496
|
description: TIME_FLAG_DESCRIPTION,
|
|
@@ -530,19 +593,24 @@ export function createOperationCommand(operation) {
|
|
|
530
593
|
const { flags } = await this.parse(OperationCommand);
|
|
531
594
|
const parsedFlags = flags;
|
|
532
595
|
await runWithTiming(parsedFlags.time === true, async () => {
|
|
533
|
-
const baseUrlOverridden = typeof parsedFlags["base-url"] === "string"
|
|
596
|
+
const baseUrlOverridden = typeof parsedFlags["api-base-url-1"] === "string" ||
|
|
597
|
+
typeof parsedFlags["api-base-url-2"] === "string";
|
|
534
598
|
const auth = resolveCliAuth({
|
|
535
599
|
apiKey: typeof parsedFlags["api-key"] === "string"
|
|
536
600
|
? parsedFlags["api-key"]
|
|
537
601
|
: undefined,
|
|
538
|
-
|
|
539
|
-
? parsedFlags["base-url"]
|
|
602
|
+
apiBaseUrl1: typeof parsedFlags["api-base-url-1"] === "string"
|
|
603
|
+
? parsedFlags["api-base-url-1"]
|
|
604
|
+
: undefined,
|
|
605
|
+
apiBaseUrl2: typeof parsedFlags["api-base-url-2"] === "string"
|
|
606
|
+
? parsedFlags["api-base-url-2"]
|
|
540
607
|
: undefined,
|
|
541
608
|
configDir: this.config.configDir,
|
|
542
609
|
});
|
|
543
610
|
const apiClient = new PrimitiveApiClient({
|
|
544
611
|
apiKey: auth.apiKey,
|
|
545
|
-
|
|
612
|
+
apiBaseUrl1: auth.apiBaseUrl1,
|
|
613
|
+
apiBaseUrl2: auth.apiBaseUrl2,
|
|
546
614
|
});
|
|
547
615
|
// Two body sources, merged: explicit JSON via --body /
|
|
548
616
|
// --body-file (the base) plus per-field flags (the
|
|
@@ -588,9 +656,15 @@ export function createOperationCommand(operation) {
|
|
|
588
656
|
throw new Errors.CLIError(`Operation ${operation.operationId} requires a body. Pass each field as a --flag (see --help) or supply JSON via --raw-body / --body-file.`);
|
|
589
657
|
}
|
|
590
658
|
const operationFn = operations[operation.sdkName];
|
|
659
|
+
// Operations in HOST_2_OPERATIONS route to the attachments-
|
|
660
|
+
// supporting send host (apiBaseUrl2). Today that's only
|
|
661
|
+
// sendEmail; the list grows as we migrate more endpoints.
|
|
662
|
+
const targetClient = HOST_2_OPERATIONS.has(operation.sdkName)
|
|
663
|
+
? apiClient._sendClient
|
|
664
|
+
: apiClient.client;
|
|
591
665
|
const result = await operationFn({
|
|
592
666
|
body,
|
|
593
|
-
client:
|
|
667
|
+
client: targetClient,
|
|
594
668
|
parseAs: operation.binaryResponse ? "blob" : "auto",
|
|
595
669
|
path: collectValues(operation.pathParams, parsedFlags),
|
|
596
670
|
query: collectValues(operation.queryParams, parsedFlags),
|
package/dist/oclif/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_API_BASE_URL_1, DEFAULT_API_BASE_URL_2, } from "../api/index.js";
|
|
5
5
|
const CREDENTIALS_FILE = "credentials.json";
|
|
6
6
|
const CREDENTIALS_LOCK_DIR = "credentials.lock";
|
|
7
7
|
const CREDENTIALS_LOCK_STALE_MS = 30 * 60 * 1000;
|
|
@@ -16,10 +16,34 @@ function requireString(value, key) {
|
|
|
16
16
|
}
|
|
17
17
|
return raw;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Sentinel returned by parseCredentials when the on-disk credentials
|
|
21
|
+
* were written by a pre-dual-host CLI version (i.e. they have
|
|
22
|
+
* `base_url` instead of `api_base_url_1`). The caller treats this as
|
|
23
|
+
* "no saved credentials" after auto-cleaning the stale file. Defined
|
|
24
|
+
* as a class-tagged error so loadCliCredentials can distinguish it
|
|
25
|
+
* from a genuine malformed-credentials error.
|
|
26
|
+
*/
|
|
27
|
+
class StaleCredentialFormatError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("stale_credential_format");
|
|
30
|
+
this.name = "StaleCredentialFormatError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
function parseCredentials(raw) {
|
|
20
34
|
if (!isRecord(raw)) {
|
|
21
35
|
throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
22
36
|
}
|
|
37
|
+
// Stored credentials from an older CLI version used the field name
|
|
38
|
+
// `base_url`; the dual-host rename moved this to `api_base_url_1`.
|
|
39
|
+
// Detect the old shape specifically so loadCliCredentials can wipe
|
|
40
|
+
// the stale file and emit a clear "you've been logged out" notice
|
|
41
|
+
// instead of every command hard-failing with a generic "malformed"
|
|
42
|
+
// error that doesn't surface the actual fix (re-login).
|
|
43
|
+
if (typeof raw.api_base_url_1 !== "string" &&
|
|
44
|
+
typeof raw.base_url === "string") {
|
|
45
|
+
throw new StaleCredentialFormatError();
|
|
46
|
+
}
|
|
23
47
|
const orgName = raw.org_name;
|
|
24
48
|
if (orgName !== null && typeof orgName !== "string") {
|
|
25
49
|
throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
|
|
@@ -30,19 +54,25 @@ function parseCredentials(raw) {
|
|
|
30
54
|
key_prefix: requireString(raw, "key_prefix"),
|
|
31
55
|
org_id: requireString(raw, "org_id"),
|
|
32
56
|
org_name: orgName,
|
|
33
|
-
|
|
57
|
+
api_base_url_1: requireString(raw, "api_base_url_1"),
|
|
34
58
|
created_at: requireString(raw, "created_at"),
|
|
35
59
|
};
|
|
36
60
|
}
|
|
37
61
|
export function credentialsPath(configDir) {
|
|
38
62
|
return join(configDir, CREDENTIALS_FILE);
|
|
39
63
|
}
|
|
40
|
-
|
|
41
|
-
const trimmed =
|
|
64
|
+
function normalize(url, fallback) {
|
|
65
|
+
const trimmed = url?.trim();
|
|
42
66
|
if (!trimmed)
|
|
43
|
-
return
|
|
67
|
+
return fallback;
|
|
44
68
|
return trimmed.replace(/\/+$/, "");
|
|
45
69
|
}
|
|
70
|
+
export function normalizeApiBaseUrl1(url) {
|
|
71
|
+
return normalize(url, DEFAULT_API_BASE_URL_1);
|
|
72
|
+
}
|
|
73
|
+
export function normalizeApiBaseUrl2(url) {
|
|
74
|
+
return normalize(url, DEFAULT_API_BASE_URL_2);
|
|
75
|
+
}
|
|
46
76
|
export function loadCliCredentials(configDir) {
|
|
47
77
|
const path = credentialsPath(configDir);
|
|
48
78
|
let contents;
|
|
@@ -62,6 +92,24 @@ export function loadCliCredentials(configDir) {
|
|
|
62
92
|
return parseCredentials(JSON.parse(contents));
|
|
63
93
|
}
|
|
64
94
|
catch (error) {
|
|
95
|
+
if (error instanceof StaleCredentialFormatError) {
|
|
96
|
+
// Saved credentials were written by a pre-dual-host CLI version.
|
|
97
|
+
// The format is incompatible (base_url vs api_base_url_1) and
|
|
98
|
+
// cannot be recovered. Clear the file so the caller sees "no
|
|
99
|
+
// saved credentials" and emit a one-shot notice telling the
|
|
100
|
+
// user they need to log back in. Idempotent: once the file is
|
|
101
|
+
// gone, this branch never fires again.
|
|
102
|
+
try {
|
|
103
|
+
rmSync(path, { force: true });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Best-effort cleanup; if the unlink fails (permissions,
|
|
107
|
+
// racing process), the next CLI invocation will hit this
|
|
108
|
+
// path again and try once more.
|
|
109
|
+
}
|
|
110
|
+
process.stderr.write("You've been logged out: your saved Primitive CLI credentials were created by an older CLI version and are no longer compatible. Run `primitive login` to re-authenticate.\n");
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
65
113
|
if (error instanceof SyntaxError) {
|
|
66
114
|
throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive login`.");
|
|
67
115
|
}
|
|
@@ -140,10 +188,15 @@ export function acquireCliCredentialsLock(configDir, options = {}) {
|
|
|
140
188
|
}
|
|
141
189
|
export function resolveCliAuth(params) {
|
|
142
190
|
const apiKey = params.apiKey?.trim();
|
|
191
|
+
// Host 2 (api_base_url_2) is never stored; either set by env/flag or
|
|
192
|
+
// falls back to the production default. The login flow only deals
|
|
193
|
+
// with host 1.
|
|
194
|
+
const apiBaseUrl2 = normalizeApiBaseUrl2(params.apiBaseUrl2);
|
|
143
195
|
if (apiKey) {
|
|
144
196
|
return {
|
|
145
197
|
apiKey,
|
|
146
|
-
|
|
198
|
+
apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
|
|
199
|
+
apiBaseUrl2,
|
|
147
200
|
credentials: null,
|
|
148
201
|
source: "flag-or-env",
|
|
149
202
|
};
|
|
@@ -152,16 +205,18 @@ export function resolveCliAuth(params) {
|
|
|
152
205
|
if (credentials) {
|
|
153
206
|
return {
|
|
154
207
|
apiKey: credentials.api_key,
|
|
155
|
-
|
|
156
|
-
?
|
|
157
|
-
: credentials.
|
|
208
|
+
apiBaseUrl1: params.apiBaseUrl1
|
|
209
|
+
? normalizeApiBaseUrl1(params.apiBaseUrl1)
|
|
210
|
+
: credentials.api_base_url_1,
|
|
211
|
+
apiBaseUrl2,
|
|
158
212
|
credentials,
|
|
159
213
|
source: "stored",
|
|
160
214
|
};
|
|
161
215
|
}
|
|
162
216
|
return {
|
|
163
217
|
apiKey: undefined,
|
|
164
|
-
|
|
218
|
+
apiBaseUrl1: normalizeApiBaseUrl1(params.apiBaseUrl1),
|
|
219
|
+
apiBaseUrl2,
|
|
165
220
|
credentials: null,
|
|
166
221
|
source: "none",
|
|
167
222
|
};
|
|
@@ -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
|
+
}
|