@primitivedotdev/sdk 0.18.0 → 0.20.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.
@@ -230,10 +230,10 @@ declare function safeValidateEmailReceivedEvent(input: unknown): ValidationResul
230
230
  * specific email's raw bytes or attachment bundle from a per-deployment
231
231
  * download endpoint. It binds:
232
232
  *
233
- * - `email_id` the specific email the token authorizes.
234
- * - `aud` a caller-chosen audience label (e.g. the resource kind being
233
+ * - `email_id`: the specific email the token authorizes.
234
+ * - `aud`: a caller-chosen audience label (e.g. the resource kind being
235
235
  * downloaded). Tokens minted for one audience will not verify under another.
236
- * - `exp` an absolute expiration time (unix seconds).
236
+ * - `exp`: an absolute expiration time (unix seconds).
237
237
  *
238
238
  * Format: `<base64url(payload)>.<base64url(signature)>` where `signature`
239
239
  * is HMAC-SHA256 over the base64url-encoded payload using the shared secret.
@@ -277,9 +277,9 @@ declare function generateDownloadToken(params: GenerateDownloadTokenOptions): st
277
277
  interface VerifyDownloadTokenOptions {
278
278
  /** The token string to verify. */
279
279
  token: string;
280
- /** Expected email ID must match the token payload exactly. */
280
+ /** Expected email ID. Must match the token payload exactly. */
281
281
  emailId: string;
282
- /** Expected audience must match the token payload exactly. */
282
+ /** Expected audience. Must match the token payload exactly. */
283
283
  audience: string;
284
284
  /** Shared HMAC secret. */
285
285
  secret: string;
@@ -290,7 +290,7 @@ interface VerifyDownloadTokenOptions {
290
290
  * Result of verifying a download token.
291
291
  *
292
292
  * On failure, `error` is a short human-readable reason suitable for logs.
293
- * Do not surface it to untrusted clients it may reveal which check failed.
293
+ * Do not surface it to untrusted clients; it may reveal which check failed.
294
294
  */
295
295
  type VerifyDownloadTokenResult = {
296
296
  valid: true;
@@ -302,7 +302,7 @@ type VerifyDownloadTokenResult = {
302
302
  * Verify a signed download token.
303
303
  *
304
304
  * Returns a discriminated-union result. The function never throws for
305
- * verification failures only malformed inputs at the crypto layer would
305
+ * verification failures. Only malformed inputs at the crypto layer would
306
306
  * surface. Callers should check `result.valid` and log `result.error`.
307
307
  *
308
308
  * @param params - Verification inputs.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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 { a as PrimitiveApiError, c as PrimitiveClientOptions, d as SendResult, f as SendThreadInput, h as createPrimitiveClient, l as ReplyInput, n as ForwardInput, p as client, s as PrimitiveClient, u as SendInput } from "./index-CHWqMBs6.js";
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-CbEivn3S.js";
3
+ import { a as PrimitiveApiError, c as PrimitiveClientOptions, d as SendInput, f as SendResult, g as createPrimitiveClient, l as ReplyInput, m as client, n as ForwardInput, p as SendThreadInput, s as PrimitiveClient } from "./index-C6ObsYjq.js";
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
7
7
  declare const primitive: {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
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 client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-DrAZhxS-.js";
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-zkN4wUTs.js";
2
+ import { a as client, i as PrimitiveClient, r as PrimitiveApiError, s as createPrimitiveClient } from "./api-DNF21MDo.js";
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 = {
6
6
  client,
@@ -1,6 +1,16 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { Command, Errors, Flags } from "@oclif/core";
3
3
  import { operations, PrimitiveApiClient } from "../api/index.js";
4
+ import { deleteCliCredentials, resolveCliAuth, } from "./auth.js";
5
+ export const API_ERROR_CODES = {
6
+ accessDenied: "access_denied",
7
+ authorizationPending: "authorization_pending",
8
+ expiredToken: "expired_token",
9
+ invalidDeviceCode: "invalid_device_code",
10
+ notFound: "not_found",
11
+ slowDown: "slow_down",
12
+ unauthorized: "unauthorized",
13
+ };
4
14
  function flagName(parameterName) {
5
15
  return parameterName.replace(/_/g, "-");
6
16
  }
@@ -214,6 +224,19 @@ export function readJsonBody(flags) {
214
224
  }
215
225
  return undefined;
216
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
+ }
217
240
  export function extractErrorPayload(raw) {
218
241
  if (raw &&
219
242
  typeof raw === "object" &&
@@ -288,7 +311,7 @@ export function extractErrorCode(payload) {
288
311
  // `--api-key` flag; this closes that gap without having to
289
312
  // special-case every command.
290
313
  const ERROR_CODE_HINTS = {
291
- unauthorized: "Hint: pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.",
314
+ [API_ERROR_CODES.unauthorized]: "Hint: run `primitive login`, pass --api-key explicitly, or set PRIMITIVE_API_KEY in your environment. `primitive whoami` is the fastest way to verify a key is live.",
292
315
  };
293
316
  // Write a server / SDK error to stderr in the canonical envelope
294
317
  // shape, plus an actionable hint when the code is one we know how
@@ -298,10 +321,27 @@ const ERROR_CODE_HINTS = {
298
321
  export function writeErrorWithHints(payload) {
299
322
  process.stderr.write(`${formatErrorPayload(payload)}\n`);
300
323
  const code = extractErrorCode(payload);
301
- if (code && ERROR_CODE_HINTS[code]) {
302
- process.stderr.write(`${ERROR_CODE_HINTS[code]}\n`);
324
+ if (code && code in ERROR_CODE_HINTS) {
325
+ const hint = ERROR_CODE_HINTS[code];
326
+ process.stderr.write(`${hint}\n`);
303
327
  }
304
328
  }
329
+ export function removeStaleSavedCredentialOnUnauthorized(params) {
330
+ if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized ||
331
+ params.auth.source !== "stored") {
332
+ return false;
333
+ }
334
+ const baseUrlDiffersFromSaved = params.baseUrlOverridden &&
335
+ params.auth.credentials !== null &&
336
+ params.auth.baseUrl !== params.auth.credentials.base_url;
337
+ if (baseUrlDiffersFromSaved) {
338
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; check --base-url / PRIMITIVE_API_URL, or run `primitive logout` to remove it.\n");
339
+ return false;
340
+ }
341
+ deleteCliCredentials(params.configDir);
342
+ process.stderr.write("Removed saved Primitive CLI credentials because the backing API key is no longer valid. Run `primitive login` to create a new one.\n");
343
+ return true;
344
+ }
305
345
  // Format milliseconds as a short human-readable wall-clock duration.
306
346
  // Sub-second uses 2 decimal places (e.g. `0.18s`); seconds use 2
307
347
  // decimals up to 60s (`12.34s`); minute-plus uses `Mm SS.SSs`.
@@ -395,7 +435,7 @@ function bodyFieldFlag(field) {
395
435
  function buildFlags(operation) {
396
436
  const flags = {
397
437
  "api-key": Flags.string({
398
- description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
438
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
399
439
  env: "PRIMITIVE_API_KEY",
400
440
  }),
401
441
  "base-url": Flags.string({
@@ -503,13 +543,19 @@ export function createOperationCommand(operation) {
503
543
  const { flags } = await this.parse(OperationCommand);
504
544
  const parsedFlags = flags;
505
545
  await runWithTiming(parsedFlags.time === true, async () => {
506
- const apiClient = new PrimitiveApiClient({
546
+ const baseUrlOverridden = typeof parsedFlags["base-url"] === "string";
547
+ const auth = resolveCliAuth({
507
548
  apiKey: typeof parsedFlags["api-key"] === "string"
508
549
  ? parsedFlags["api-key"]
509
550
  : undefined,
510
551
  baseUrl: typeof parsedFlags["base-url"] === "string"
511
552
  ? parsedFlags["base-url"]
512
553
  : undefined,
554
+ configDir: this.config.configDir,
555
+ });
556
+ const apiClient = new PrimitiveApiClient({
557
+ apiKey: auth.apiKey,
558
+ baseUrl: auth.baseUrl,
513
559
  });
514
560
  // Two body sources, merged: explicit JSON via --body /
515
561
  // --body-file (the base) plus per-field flags (the
@@ -564,7 +610,14 @@ export function createOperationCommand(operation) {
564
610
  responseStyle: "fields",
565
611
  });
566
612
  if (result.error) {
567
- writeErrorWithHints(extractErrorPayload(result.error));
613
+ const errorPayload = extractErrorPayload(result.error);
614
+ writeErrorWithHints(errorPayload);
615
+ removeStaleSavedCredentialOnUnauthorized({
616
+ auth,
617
+ baseUrlOverridden,
618
+ configDir: this.config.configDir,
619
+ payload: errorPayload,
620
+ });
568
621
  process.exitCode = 1;
569
622
  return;
570
623
  }
@@ -0,0 +1,168 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { DEFAULT_BASE_URL } from "../api/index.js";
5
+ const CREDENTIALS_FILE = "credentials.json";
6
+ const CREDENTIALS_LOCK_DIR = "credentials.lock";
7
+ const CREDENTIALS_LOCK_STALE_MS = 30 * 60 * 1000;
8
+ const MALFORMED_CREDENTIALS_HINT = "Run `primitive logout` and then `primitive login`.";
9
+ function isRecord(value) {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+ function requireString(value, key) {
13
+ const raw = value[key];
14
+ if (typeof raw !== "string" || raw.trim().length === 0) {
15
+ throw new Error(`Stored Primitive CLI credentials are malformed: ${key} must be a non-empty string. ${MALFORMED_CREDENTIALS_HINT}`);
16
+ }
17
+ return raw;
18
+ }
19
+ function parseCredentials(raw) {
20
+ if (!isRecord(raw)) {
21
+ throw new Error(`Stored Primitive CLI credentials are malformed: expected a JSON object. ${MALFORMED_CREDENTIALS_HINT}`);
22
+ }
23
+ const orgName = raw.org_name;
24
+ if (orgName !== null && typeof orgName !== "string") {
25
+ throw new Error(`Stored Primitive CLI credentials are malformed: org_name must be a string or null. ${MALFORMED_CREDENTIALS_HINT}`);
26
+ }
27
+ return {
28
+ api_key: requireString(raw, "api_key"),
29
+ key_id: requireString(raw, "key_id"),
30
+ key_prefix: requireString(raw, "key_prefix"),
31
+ org_id: requireString(raw, "org_id"),
32
+ org_name: orgName,
33
+ base_url: requireString(raw, "base_url"),
34
+ created_at: requireString(raw, "created_at"),
35
+ };
36
+ }
37
+ export function credentialsPath(configDir) {
38
+ return join(configDir, CREDENTIALS_FILE);
39
+ }
40
+ export function normalizeBaseUrl(baseUrl) {
41
+ const trimmed = baseUrl?.trim();
42
+ if (!trimmed)
43
+ return DEFAULT_BASE_URL;
44
+ return trimmed.replace(/\/+$/, "");
45
+ }
46
+ export function loadCliCredentials(configDir) {
47
+ const path = credentialsPath(configDir);
48
+ let contents;
49
+ try {
50
+ contents = readFileSync(path, "utf8");
51
+ }
52
+ catch (error) {
53
+ if (error &&
54
+ typeof error === "object" &&
55
+ error.code === "ENOENT") {
56
+ return null;
57
+ }
58
+ const detail = error instanceof Error ? error.message : String(error);
59
+ throw new Error(`Could not read Primitive CLI credentials: ${detail}`);
60
+ }
61
+ try {
62
+ return parseCredentials(JSON.parse(contents));
63
+ }
64
+ catch (error) {
65
+ if (error instanceof SyntaxError) {
66
+ throw new Error("Stored Primitive CLI credentials are not valid JSON. Run `primitive logout` and then `primitive login`.");
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+ export function saveCliCredentials(configDir, credentials) {
72
+ mkdirSync(configDir, { mode: 0o700, recursive: true });
73
+ const path = credentialsPath(configDir);
74
+ const tempPath = join(configDir, `${CREDENTIALS_FILE}.${process.pid}.${randomUUID()}.tmp`);
75
+ try {
76
+ writeFileSync(tempPath, `${JSON.stringify(credentials, null, 2)}\n`, {
77
+ mode: 0o600,
78
+ });
79
+ chmodSync(tempPath, 0o600);
80
+ renameSync(tempPath, path);
81
+ chmodSync(path, 0o600);
82
+ }
83
+ catch (error) {
84
+ rmSync(tempPath, { force: true });
85
+ throw error;
86
+ }
87
+ }
88
+ export function deleteCliCredentials(configDir) {
89
+ rmSync(credentialsPath(configDir), { force: true });
90
+ }
91
+ function errorCode(error) {
92
+ return error && typeof error === "object"
93
+ ? error.code
94
+ : undefined;
95
+ }
96
+ function removeStaleCliCredentialsLock(lockPath, staleMs, now) {
97
+ try {
98
+ const stats = statSync(lockPath);
99
+ if (now() - stats.mtimeMs < staleMs)
100
+ return false;
101
+ }
102
+ catch (error) {
103
+ if (errorCode(error) === "ENOENT")
104
+ return true;
105
+ throw error;
106
+ }
107
+ rmSync(lockPath, { force: true, recursive: true });
108
+ return true;
109
+ }
110
+ export function acquireCliCredentialsLock(configDir, options = {}) {
111
+ mkdirSync(configDir, { mode: 0o700, recursive: true });
112
+ const lockPath = join(configDir, CREDENTIALS_LOCK_DIR);
113
+ const now = options.now ?? Date.now;
114
+ const staleMs = options.staleMs ?? CREDENTIALS_LOCK_STALE_MS;
115
+ let acquired = false;
116
+ for (let attempt = 0; attempt < 2; attempt += 1) {
117
+ try {
118
+ mkdirSync(lockPath, { mode: 0o700 });
119
+ acquired = true;
120
+ break;
121
+ }
122
+ catch (error) {
123
+ if (errorCode(error) !== "EEXIST")
124
+ throw error;
125
+ if (removeStaleCliCredentialsLock(lockPath, staleMs, now))
126
+ continue;
127
+ throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
128
+ }
129
+ }
130
+ if (!acquired) {
131
+ throw new Error("Another Primitive CLI credential operation is already in progress. Wait for it to finish, then retry.");
132
+ }
133
+ let released = false;
134
+ return () => {
135
+ if (released)
136
+ return;
137
+ released = true;
138
+ rmSync(lockPath, { force: true, recursive: true });
139
+ };
140
+ }
141
+ export function resolveCliAuth(params) {
142
+ const apiKey = params.apiKey?.trim();
143
+ if (apiKey) {
144
+ return {
145
+ apiKey,
146
+ baseUrl: normalizeBaseUrl(params.baseUrl),
147
+ credentials: null,
148
+ source: "flag-or-env",
149
+ };
150
+ }
151
+ const credentials = loadCliCredentials(params.configDir);
152
+ if (credentials) {
153
+ return {
154
+ apiKey: credentials.api_key,
155
+ baseUrl: params.baseUrl
156
+ ? normalizeBaseUrl(params.baseUrl)
157
+ : credentials.base_url,
158
+ credentials,
159
+ source: "stored",
160
+ };
161
+ }
162
+ return {
163
+ apiKey: undefined,
164
+ baseUrl: normalizeBaseUrl(params.baseUrl),
165
+ credentials: null,
166
+ source: "none",
167
+ };
168
+ }
@@ -1,7 +1,8 @@
1
1
  import { Command, Flags } from "@oclif/core";
2
2
  import { listEmails } from "../../api/generated/sdk.gen.js";
3
3
  import { PrimitiveApiClient } from "../../api/index.js";
4
- import { extractErrorPayload, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
4
+ import { extractErrorPayload, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
+ import { resolveCliAuth } from "../auth.js";
5
6
  // `primitive emails:latest` is the agent-grade shortcut for "show me
6
7
  // the most recent inbound emails as something I can read at a glance."
7
8
  // `emails:list-emails` returns the full JSON envelope which is great
@@ -94,7 +95,7 @@ class EmailsLatestCommand extends Command {
94
95
  ];
95
96
  static flags = {
96
97
  "api-key": Flags.string({
97
- description: "Primitive API key (defaults to PRIMITIVE_API_KEY)",
98
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
98
99
  env: "PRIMITIVE_API_KEY",
99
100
  }),
100
101
  "base-url": Flags.string({
@@ -120,9 +121,15 @@ class EmailsLatestCommand extends Command {
120
121
  async run() {
121
122
  const { flags } = await this.parse(EmailsLatestCommand);
122
123
  await runWithTiming(flags.time, async () => {
123
- const apiClient = new PrimitiveApiClient({
124
+ const baseUrlOverridden = flags["base-url"] !== undefined;
125
+ const auth = resolveCliAuth({
124
126
  apiKey: flags["api-key"],
125
127
  baseUrl: flags["base-url"],
128
+ configDir: this.config.configDir,
129
+ });
130
+ const apiClient = new PrimitiveApiClient({
131
+ apiKey: auth.apiKey,
132
+ baseUrl: auth.baseUrl,
126
133
  });
127
134
  const result = await listEmails({
128
135
  client: apiClient.client,
@@ -130,7 +137,14 @@ class EmailsLatestCommand extends Command {
130
137
  responseStyle: "fields",
131
138
  });
132
139
  if (result.error) {
133
- writeErrorWithHints(extractErrorPayload(result.error));
140
+ const errorPayload = extractErrorPayload(result.error);
141
+ writeErrorWithHints(errorPayload);
142
+ removeStaleSavedCredentialOnUnauthorized({
143
+ auth,
144
+ baseUrlOverridden,
145
+ configDir: this.config.configDir,
146
+ payload: errorPayload,
147
+ });
134
148
  process.exitCode = 1;
135
149
  return;
136
150
  }
@@ -0,0 +1,108 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { createFunction } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
+ import { resolveCliAuth } from "../auth.js";
6
+ // `primitive functions:deploy` is the agent-grade shortcut for
7
+ // `functions:create-function`. The underlying operation takes `code`
8
+ // as a string in the JSON body, which is awkward at the CLI for
9
+ // multi-line bundles: agents would otherwise have to shell-escape an
10
+ // entire ESM file or write a temp body.json. This command reads the
11
+ // bundle straight off disk via --file, so the natural workflow is:
12
+ //
13
+ // esbuild handler.ts --bundle --format=esm --outfile=bundle.js
14
+ // primitive functions:deploy --name myfn --file bundle.js
15
+ //
16
+ // Source maps follow the same shape via --source-map-file. They are
17
+ // stored only on the runtime side (not in our database) so dropping
18
+ // them later in the pipeline is fine; the CLI just hands them through.
19
+ //
20
+ // For full control (raw body, --raw-body JSON, etc.) the underlying
21
+ // `functions:create-function` operation stays available.
22
+ class FunctionsDeployCommand extends Command {
23
+ static description = `Deploy a new function from a bundled handler file. Agent-grade shortcut for functions:create-function.
24
+
25
+ Reads the bundle off disk (--file) instead of forcing the caller to
26
+ serialize the source into a JSON body. Use the underlying operation
27
+ \`functions:create-function\` if you need the full flag surface
28
+ (raw-body JSON, etc.).`;
29
+ static summary = "Deploy a new function from a bundled handler file";
30
+ static examples = [
31
+ "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js",
32
+ "<%= config.bin %> functions:deploy --name forwarder --file ./bundle.js --source-map-file ./bundle.js.map",
33
+ ];
34
+ static flags = {
35
+ "api-key": Flags.string({
36
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
37
+ env: "PRIMITIVE_API_KEY",
38
+ }),
39
+ "base-url": Flags.string({
40
+ description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
41
+ env: "PRIMITIVE_API_URL",
42
+ }),
43
+ name: Flags.string({
44
+ description: "Slug-style name. Lowercase letters, digits, hyphens, underscores. 1-64 chars. Must be unique within the org.",
45
+ required: true,
46
+ }),
47
+ file: Flags.string({
48
+ description: "Path to the bundled ESM handler file (single self-contained module). Loaded as the `code` body field.",
49
+ required: true,
50
+ }),
51
+ "source-map-file": Flags.string({
52
+ description: "Optional path to a source map for the bundle. Stored only on the runtime side and used to symbolicate stack traces.",
53
+ }),
54
+ time: Flags.boolean({
55
+ description: TIME_FLAG_DESCRIPTION,
56
+ }),
57
+ };
58
+ async run() {
59
+ const { flags } = await this.parse(FunctionsDeployCommand);
60
+ await runWithTiming(flags.time, async () => {
61
+ // Reads are inside the timed block so --time captures disk I/O
62
+ // alongside the API call. A pathological filesystem (NFS, slow
63
+ // FUSE mount) showing up here is exactly the kind of latency
64
+ // surprise --time is meant to surface.
65
+ const code = readTextFileFlag(flags.file, "--file");
66
+ const sourceMap = flags["source-map-file"]
67
+ ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
68
+ : undefined;
69
+ const baseUrlOverridden = flags["base-url"] !== undefined;
70
+ const auth = resolveCliAuth({
71
+ apiKey: flags["api-key"],
72
+ baseUrl: flags["base-url"],
73
+ configDir: this.config.configDir,
74
+ });
75
+ const apiClient = new PrimitiveApiClient({
76
+ apiKey: auth.apiKey,
77
+ baseUrl: auth.baseUrl,
78
+ });
79
+ const authFailureContext = {
80
+ auth,
81
+ baseUrlOverridden,
82
+ configDir: this.config.configDir,
83
+ };
84
+ const result = await createFunction({
85
+ body: {
86
+ name: flags.name,
87
+ code,
88
+ ...(sourceMap !== undefined ? { sourceMap } : {}),
89
+ },
90
+ client: apiClient.client,
91
+ responseStyle: "fields",
92
+ });
93
+ if (result.error) {
94
+ const errorPayload = extractErrorPayload(result.error);
95
+ writeErrorWithHints(errorPayload);
96
+ removeStaleSavedCredentialOnUnauthorized({
97
+ ...authFailureContext,
98
+ payload: errorPayload,
99
+ });
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const envelope = result.data;
104
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
105
+ });
106
+ }
107
+ }
108
+ export default FunctionsDeployCommand;
@@ -0,0 +1,97 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { updateFunction } from "../../api/generated/sdk.gen.js";
3
+ import { PrimitiveApiClient } from "../../api/index.js";
4
+ import { extractErrorPayload, readTextFileFlag, removeStaleSavedCredentialOnUnauthorized, runWithTiming, TIME_FLAG_DESCRIPTION, writeErrorWithHints, } from "../api-command.js";
5
+ import { resolveCliAuth } from "../auth.js";
6
+ // `primitive functions:redeploy` is the agent-grade shortcut for
7
+ // `functions:update-function`. Same file-reading ergonomic as
8
+ // functions:deploy but for an existing function. Use this to push a
9
+ // new bundle, OR to refresh secret bindings: passing the
10
+ // previously-deployed bundle (or any equivalent file) re-runs the
11
+ // deploy and refreshes env from the secrets table, which is how
12
+ // secret writes go live.
13
+ class FunctionsRedeployCommand extends Command {
14
+ static description = `Update or redeploy a function from a bundled handler file. Agent-grade shortcut for functions:update-function.
15
+
16
+ Use to push a new bundle OR to refresh secret bindings into the
17
+ running handler. The same file is fine for both: the deploy reads
18
+ the bindings table fresh on every call, so passing the existing
19
+ bundle picks up any secret writes since the last deploy.`;
20
+ static summary = "Redeploy a function from a bundled handler file";
21
+ static examples = [
22
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js",
23
+ "<%= config.bin %> functions:redeploy --id <fn-id> --file ./bundle.js --source-map-file ./bundle.js.map",
24
+ ];
25
+ static flags = {
26
+ "api-key": Flags.string({
27
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
28
+ env: "PRIMITIVE_API_KEY",
29
+ }),
30
+ "base-url": Flags.string({
31
+ description: "API base URL (defaults to PRIMITIVE_API_URL or production)",
32
+ env: "PRIMITIVE_API_URL",
33
+ }),
34
+ id: Flags.string({
35
+ description: "Function id (UUID). The function must already exist.",
36
+ required: true,
37
+ }),
38
+ file: Flags.string({
39
+ description: "Path to the bundled ESM handler file. Loaded as the `code` body field.",
40
+ required: true,
41
+ }),
42
+ "source-map-file": Flags.string({
43
+ description: "Optional path to a source map for the bundle. Used to symbolicate stack traces in the function's logs.",
44
+ }),
45
+ time: Flags.boolean({
46
+ description: TIME_FLAG_DESCRIPTION,
47
+ }),
48
+ };
49
+ async run() {
50
+ const { flags } = await this.parse(FunctionsRedeployCommand);
51
+ await runWithTiming(flags.time, async () => {
52
+ // Reads inside the timed block: --time captures disk I/O too,
53
+ // which is the latency the flag is meant to surface.
54
+ const code = readTextFileFlag(flags.file, "--file");
55
+ const sourceMap = flags["source-map-file"]
56
+ ? readTextFileFlag(flags["source-map-file"], "--source-map-file")
57
+ : undefined;
58
+ const baseUrlOverridden = flags["base-url"] !== undefined;
59
+ const auth = resolveCliAuth({
60
+ apiKey: flags["api-key"],
61
+ baseUrl: flags["base-url"],
62
+ configDir: this.config.configDir,
63
+ });
64
+ const apiClient = new PrimitiveApiClient({
65
+ apiKey: auth.apiKey,
66
+ baseUrl: auth.baseUrl,
67
+ });
68
+ const authFailureContext = {
69
+ auth,
70
+ baseUrlOverridden,
71
+ configDir: this.config.configDir,
72
+ };
73
+ const result = await updateFunction({
74
+ path: { id: flags.id },
75
+ body: {
76
+ code,
77
+ ...(sourceMap !== undefined ? { sourceMap } : {}),
78
+ },
79
+ client: apiClient.client,
80
+ responseStyle: "fields",
81
+ });
82
+ if (result.error) {
83
+ const errorPayload = extractErrorPayload(result.error);
84
+ writeErrorWithHints(errorPayload);
85
+ removeStaleSavedCredentialOnUnauthorized({
86
+ ...authFailureContext,
87
+ payload: errorPayload,
88
+ });
89
+ process.exitCode = 1;
90
+ return;
91
+ }
92
+ const envelope = result.data;
93
+ this.log(JSON.stringify(envelope?.data ?? null, null, 2));
94
+ });
95
+ }
96
+ }
97
+ export default FunctionsRedeployCommand;