shipmail-cli 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +87 -16
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -15,10 +15,28 @@ import {
15
15
  } from "shipmail";
16
16
 
17
17
  // src/version.ts
18
- var VERSION = "0.5.3";
18
+ var VERSION = "0.5.4";
19
19
 
20
20
  // src/cli.ts
21
21
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
22
+ var ALLOWED_BASE_URL_HOSTS = ["shipmail.to"];
23
+ var RAW_REQUEST_TIMEOUT_MS = 3e4;
24
+ var NEWSLETTER_BLOCK_TYPES = [
25
+ "heading",
26
+ "paragraph",
27
+ "list",
28
+ "callout",
29
+ "button",
30
+ "image",
31
+ "video",
32
+ "columns",
33
+ "link_list",
34
+ "quote",
35
+ "divider",
36
+ "spacer",
37
+ "code",
38
+ "custom_html"
39
+ ];
22
40
  var WEBHOOK_EVENT_TYPE_SET = new Set(WEBHOOK_EVENT_TYPES);
23
41
  var CliUsageError = class extends Error {
24
42
  constructor(message) {
@@ -39,6 +57,7 @@ function parseCliArgs(argv) {
39
57
  const options = {};
40
58
  for (let i = 0; i < argv.length; i += 1) {
41
59
  const token = argv[i];
60
+ if (token === void 0) break;
42
61
  if (token === "--") {
43
62
  positional.push(...argv.slice(i + 1));
44
63
  break;
@@ -117,14 +136,35 @@ function getStringList(options, key) {
117
136
  if (isStringArray(value)) return value.flatMap(splitValues);
118
137
  return splitValues(value);
119
138
  }
120
- function normalizeBaseUrl(value) {
121
- return (value ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
139
+ function validateBaseUrl(rawValue, allowInsecure) {
140
+ let parsed;
141
+ try {
142
+ parsed = new URL(rawValue);
143
+ } catch {
144
+ throw new CliUsageError(`Base URL is not a valid URL: ${rawValue}`);
145
+ }
146
+ const normalized = parsed.toString().replace(/\/+$/, "");
147
+ if (allowInsecure) return normalized;
148
+ if (parsed.protocol !== "https:") {
149
+ throw new CliUsageError(
150
+ "Base URL must use https. Set SHIPMAIL_ALLOW_INSECURE_BASE_URL=1 for development."
151
+ );
152
+ }
153
+ const host = parsed.hostname.toLowerCase();
154
+ if (!ALLOWED_BASE_URL_HOSTS.some((allowed) => host === allowed || host.endsWith(`.${allowed}`))) {
155
+ throw new CliUsageError(
156
+ `Base URL host "${host}" is not allowed. Set SHIPMAIL_ALLOW_INSECURE_BASE_URL=1 for development.`
157
+ );
158
+ }
159
+ return normalized;
122
160
  }
123
161
  function apiKey(parsed, env) {
124
162
  return getString(parsed.options, "api-key") ?? env["SHIPMAIL_API_KEY"];
125
163
  }
126
164
  function baseUrl(parsed, env) {
127
- return normalizeBaseUrl(getString(parsed.options, "base-url") ?? env["SHIPMAIL_BASE_URL"]);
165
+ const raw = getString(parsed.options, "base-url") ?? env["SHIPMAIL_BASE_URL"];
166
+ if (!raw) return DEFAULT_BASE_URL;
167
+ return validateBaseUrl(raw, env["SHIPMAIL_ALLOW_INSECURE_BASE_URL"] === "1");
128
168
  }
129
169
  function organizationId(parsed, env) {
130
170
  return getString(parsed.options, "organization-id") ?? env["SHIPMAIL_ORGANIZATION_ID"];
@@ -164,6 +204,23 @@ async function readTextInput(value) {
164
204
  if (value.startsWith("@")) return readFile(value.slice(1), "utf8");
165
205
  return value;
166
206
  }
207
+ function isJsonObject(value) {
208
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209
+ }
210
+ function isNewsletterBlock(value) {
211
+ return isJsonObject(value) && typeof value["type"] === "string" && NEWSLETTER_BLOCK_TYPES.includes(value["type"]);
212
+ }
213
+ function isAutomationDefinition(value) {
214
+ if (!isJsonObject(value) || !isJsonObject(value["trigger"]) || !isJsonObject(value["scope"])) {
215
+ return false;
216
+ }
217
+ return typeof value["trigger"]["type"] === "string" && Array.isArray(value["conditions"]) && Array.isArray(value["actions"]) && (value["mode"] === "draft" || value["mode"] === "send") && Array.isArray(value["scope"]["mailbox_ids"]) && Array.isArray(value["scope"]["calendar_addresses"]);
218
+ }
219
+ function isMessageMetadata(value) {
220
+ return isJsonObject(value) && Object.values(value).every(
221
+ (item) => item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean"
222
+ );
223
+ }
167
224
  async function readNewsletterBlocks(value) {
168
225
  const parsed = await readJsonInput(value);
169
226
  if (!Array.isArray(parsed)) {
@@ -172,6 +229,11 @@ async function readNewsletterBlocks(value) {
172
229
  if (parsed.length === 0) {
173
230
  throw new CliUsageError("--blocks must include at least one block.");
174
231
  }
232
+ if (!parsed.every(isNewsletterBlock)) {
233
+ throw new CliUsageError(
234
+ `Every --blocks item must be an object whose type is one of: ${NEWSLETTER_BLOCK_TYPES.join(", ")}.`
235
+ );
236
+ }
175
237
  return parsed;
176
238
  }
177
239
  function parseArchiveVisibility(options) {
@@ -188,8 +250,10 @@ function parseStylingMode(options) {
188
250
  }
189
251
  async function automationDefinition(value) {
190
252
  const parsed = await readJsonInput(value);
191
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
192
- throw new CliUsageError("--definition must be a JSON automation definition object.");
253
+ if (!isAutomationDefinition(parsed)) {
254
+ throw new CliUsageError(
255
+ "--definition must be a JSON object with trigger, conditions, actions, mode (draft or send) and scope { mailbox_ids, calendar_addresses }."
256
+ );
193
257
  }
194
258
  return parsed;
195
259
  }
@@ -306,7 +370,8 @@ async function rawApiRequest(parsed, runOptions, method, path, body, authRequire
306
370
  const response = await (runOptions.fetch ?? fetch)(`${baseUrl(parsed, env)}${path}`, {
307
371
  method,
308
372
  headers,
309
- body: requestBody
373
+ body: requestBody,
374
+ signal: AbortSignal.timeout(RAW_REQUEST_TIMEOUT_MS)
310
375
  });
311
376
  if (!response.ok) {
312
377
  const contentType = response.headers.get("content-type") ?? "";
@@ -492,7 +557,9 @@ Global options:
492
557
 
493
558
  Environment:
494
559
  SHIPMAIL_API_KEY API key for authenticated commands.
495
- SHIPMAIL_BASE_URL Optional API base URL.
560
+ SHIPMAIL_BASE_URL Optional API base URL. Must be https on shipmail.to.
561
+ SHIPMAIL_ALLOW_INSECURE_BASE_URL=1
562
+ Permit a non-https or non-shipmail.to base URL (development only).
496
563
  SHIPMAIL_ORGANIZATION_ID Optional delegated child organization ID.
497
564
  `;
498
565
  }
@@ -557,8 +624,10 @@ async function messageMetadata(options) {
557
624
  const raw = getString(options, "metadata");
558
625
  if (!raw) return void 0;
559
626
  const parsed = await readJsonInput(raw);
560
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
561
- throw new CliUsageError("--metadata must be a JSON object or @path/to/file.json.");
627
+ if (!isMessageMetadata(parsed)) {
628
+ throw new CliUsageError(
629
+ "--metadata must be a JSON object of string, number, boolean or null values, or @path/to/file.json."
630
+ );
562
631
  }
563
632
  return parsed;
564
633
  }
@@ -717,7 +786,8 @@ async function runMailboxes(client, parsed, out) {
717
786
  throw new CliUsageError("Missing --password, --password-file, or --generate-password.");
718
787
  }
719
788
  const password = passwordFile ? (await readFile(passwordFile, "utf8")).replace(/\r?\n$/, "") : passwordValue;
720
- if (!generatePassword && !password) {
789
+ const credentials = generatePassword ? { generate_password: true } : password ? { password } : void 0;
790
+ if (!credentials) {
721
791
  throw new CliUsageError("Mailbox password must not be empty.");
722
792
  }
723
793
  writeJson(
@@ -725,7 +795,7 @@ async function runMailboxes(client, parsed, out) {
725
795
  await client.mailboxes.create({
726
796
  domain_id: getRequiredString(parsed.options, "domain-id"),
727
797
  address: getRequiredString(parsed.options, "address"),
728
- ...generatePassword ? { generate_password: true } : { password },
798
+ ...credentials,
729
799
  ...displayName ? { display_name: displayName } : {}
730
800
  })
731
801
  );
@@ -887,12 +957,13 @@ async function runMailboxes(client, parsed, out) {
887
957
  }
888
958
  params = { mode: "keep" };
889
959
  } else if (mode === "fixed") {
890
- if (destinationMailboxIds.length !== 1) {
960
+ const [destinationMailboxId] = destinationMailboxIds;
961
+ if (destinationMailboxIds.length !== 1 || destinationMailboxId === void 0) {
891
962
  throw new CliUsageError("--mode fixed requires exactly one --destination-mailbox-id.");
892
963
  }
893
964
  params = {
894
965
  mode: "fixed",
895
- destination_mailbox_id: destinationMailboxIds[0],
966
+ destination_mailbox_id: destinationMailboxId,
896
967
  fallback_mailbox_id: getRequiredString(parsed.options, "fallback-mailbox-id")
897
968
  };
898
969
  } else if (mode === "round-robin") {
@@ -1215,10 +1286,10 @@ async function runMessages(client, parsed, out) {
1215
1286
  if (subcommand === "list") {
1216
1287
  const mailboxId = getString(parsed.options, "mailbox-id");
1217
1288
  const clientReference = getString(parsed.options, "client-reference");
1218
- if (!mailboxId && !clientReference) {
1289
+ const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : clientReference ? { client_reference: clientReference } : void 0;
1290
+ if (!filter) {
1219
1291
  throw new CliUsageError("Provide --mailbox-id or --client-reference.");
1220
1292
  }
1221
- const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : { client_reference: clientReference };
1222
1293
  writeJson(
1223
1294
  out,
1224
1295
  await client.messages.list({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Official CLI for the Shipmail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "shipmail": "0.5.8"
44
+ "shipmail": "0.5.10"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",