@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.
- package/README.md +165 -65
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +254 -1
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +39 -7
- package/dist/{api-DrAZhxS-.js → api-DNF21MDo.js} +326 -7
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.js +1 -1
- package/dist/{index-CHWqMBs6.d.ts → index-C6ObsYjq.d.ts} +985 -73
- package/dist/{index-CbEivn3S.d.ts → index-CDlwyxdp.d.ts} +7 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/oclif/api-command.js +59 -6
- package/dist/oclif/auth.js +168 -0
- package/dist/oclif/commands/emails-latest.js +18 -4
- package/dist/oclif/commands/functions-deploy.js +108 -0
- package/dist/oclif/commands/functions-redeploy.js +97 -0
- package/dist/oclif/commands/login.js +233 -0
- package/dist/oclif/commands/logout.js +87 -0
- package/dist/oclif/commands/send.js +29 -7
- package/dist/oclif/commands/whoami.js +18 -4
- package/dist/oclif/fish-completion.js +1 -1
- package/dist/oclif/index.js +16 -0
- package/dist/openapi/openapi.generated.js +1303 -49
- package/dist/openapi/operations.generated.js +868 -1
- package/dist/webhook/index.d.ts +1 -1
- package/dist/webhook/index.js +1 -1
- package/dist/{webhook-zkN4wUTs.js → webhook-rUjGV6Zu.js} +4 -4
- package/oclif.manifest.json +1049 -94
- package/package.json +8 -2
|
@@ -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
|
|
234
|
-
* - `aud
|
|
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
|
|
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
|
|
280
|
+
/** Expected email ID. Must match the token payload exactly. */
|
|
281
281
|
emailId: string;
|
|
282
|
-
/** Expected audience
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|