shipmail-cli 0.5.3 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +1 -12
  2. package/dist/cli.js +87 -128
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -14,7 +14,7 @@ Most commands require an API key. Create one in the Shipmail dashboard and expor
14
14
  export SHIPMAIL_API_KEY="sm_live_..."
15
15
  ```
16
16
 
17
- You can also pass `--api-key` to any command. Use `SHIPMAIL_BASE_URL` or `--base-url` when targeting a non-production API. Approved partners can set `SHIPMAIL_ORGANIZATION_ID` or pass `--organization-id` for delegated infrastructure calls.
17
+ You can also pass `--api-key` to any command. Use `SHIPMAIL_BASE_URL` or `--base-url` when targeting a non-production API.
18
18
 
19
19
  ## Common Commands
20
20
 
@@ -125,14 +125,6 @@ shipmail webhooks create \
125
125
  --events message.received,message.bounced
126
126
  shipmail webhooks deliveries whk_123 --status failed
127
127
  shipmail webhooks replay-delivery whk_123 dlv_123 --idempotency-key replay-123
128
-
129
- # Partner beta lifecycle and delegated infrastructure
130
- shipmail partner organizations create --name "Operator" --external-reference operator_123 --owner-email owner@example.com --mailbox-limit 3 --data-classification internal_test
131
- shipmail partner usage
132
- shipmail partner credential-grants list
133
- shipmail partner credential-grants consume grant_123 --name "Embedded webmail"
134
- shipmail domains list --organization-id 00000000-0000-4000-8000-000000000123
135
- shipmail mailboxes create --organization-id 00000000-0000-4000-8000-000000000123 --domain-id dom_123 --address support --generate-password
136
128
  ```
137
129
 
138
130
  Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bare text or
@@ -174,9 +166,6 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
174
166
  - `newsletter-domains list`
175
167
  - `newsletters create|update|preview|preflight|test-send|schedule|cancel|resume`
176
168
  - `newsletter-assets list|upload|register-url`
177
- - `partner organizations list|create|get|update|invite|suspend|resume|offboard`
178
- - `partner credential-grants list|consume`
179
- - `partner usage`
180
169
 
181
170
  Configure a publisher-owned Atom self URL and the number of recent issues:
182
171
 
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.6.0";
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,17 +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"]);
128
- }
129
- function organizationId(parsed, env) {
130
- return getString(parsed.options, "organization-id") ?? env["SHIPMAIL_ORGANIZATION_ID"];
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");
131
168
  }
132
169
  function requireApiKey(parsed, env) {
133
170
  const key = apiKey(parsed, env);
@@ -141,7 +178,6 @@ function createClient(parsed, options) {
141
178
  const config = {
142
179
  apiKey: requireApiKey(parsed, env),
143
180
  baseUrl: baseUrl(parsed, env),
144
- organizationId: organizationId(parsed, env),
145
181
  ...options.fetch ? { fetch: options.fetch } : {}
146
182
  };
147
183
  return new ShipmailClient(config);
@@ -164,6 +200,23 @@ async function readTextInput(value) {
164
200
  if (value.startsWith("@")) return readFile(value.slice(1), "utf8");
165
201
  return value;
166
202
  }
203
+ function isJsonObject(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+ function isNewsletterBlock(value) {
207
+ return isJsonObject(value) && typeof value["type"] === "string" && NEWSLETTER_BLOCK_TYPES.includes(value["type"]);
208
+ }
209
+ function isAutomationDefinition(value) {
210
+ if (!isJsonObject(value) || !isJsonObject(value["trigger"]) || !isJsonObject(value["scope"])) {
211
+ return false;
212
+ }
213
+ 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"]);
214
+ }
215
+ function isMessageMetadata(value) {
216
+ return isJsonObject(value) && Object.values(value).every(
217
+ (item) => item === null || typeof item === "string" || typeof item === "number" || typeof item === "boolean"
218
+ );
219
+ }
167
220
  async function readNewsletterBlocks(value) {
168
221
  const parsed = await readJsonInput(value);
169
222
  if (!Array.isArray(parsed)) {
@@ -172,6 +225,11 @@ async function readNewsletterBlocks(value) {
172
225
  if (parsed.length === 0) {
173
226
  throw new CliUsageError("--blocks must include at least one block.");
174
227
  }
228
+ if (!parsed.every(isNewsletterBlock)) {
229
+ throw new CliUsageError(
230
+ `Every --blocks item must be an object whose type is one of: ${NEWSLETTER_BLOCK_TYPES.join(", ")}.`
231
+ );
232
+ }
175
233
  return parsed;
176
234
  }
177
235
  function parseArchiveVisibility(options) {
@@ -188,8 +246,10 @@ function parseStylingMode(options) {
188
246
  }
189
247
  async function automationDefinition(value) {
190
248
  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.");
249
+ if (!isAutomationDefinition(parsed)) {
250
+ throw new CliUsageError(
251
+ "--definition must be a JSON object with trigger, conditions, actions, mode (draft or send) and scope { mailbox_ids, calendar_addresses }."
252
+ );
193
253
  }
194
254
  return parsed;
195
255
  }
@@ -294,10 +354,6 @@ async function rawApiRequest(parsed, runOptions, method, path, body, authRequire
294
354
  const key = apiKey(parsed, env);
295
355
  if (key) headers["Authorization"] = `Bearer ${key}`;
296
356
  }
297
- const targetOrganizationId = organizationId(parsed, env);
298
- if (targetOrganizationId) {
299
- headers["X-Shipmail-Organization-Id"] = targetOrganizationId;
300
- }
301
357
  let requestBody;
302
358
  if (body !== void 0) {
303
359
  headers["Content-Type"] = "application/json";
@@ -306,7 +362,8 @@ async function rawApiRequest(parsed, runOptions, method, path, body, authRequire
306
362
  const response = await (runOptions.fetch ?? fetch)(`${baseUrl(parsed, env)}${path}`, {
307
363
  method,
308
364
  headers,
309
- body: requestBody
365
+ body: requestBody,
366
+ signal: AbortSignal.timeout(RAW_REQUEST_TIMEOUT_MS)
310
367
  });
311
368
  if (!response.ok) {
312
369
  const contentType = response.headers.get("content-type") ?? "";
@@ -455,17 +512,6 @@ Usage:
455
512
  shipmail booking-pages get <id>
456
513
  shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
457
514
  shipmail booking-pages delete <id>
458
- shipmail partner organizations list
459
- shipmail partner organizations create --name "Operator" --external-reference op_123 --owner-email owner@example.com [--mailbox-limit 3] [--data-classification internal_test|customer]
460
- shipmail partner organizations get <id>
461
- shipmail partner organizations update <id> [--name "Operator"] [--mailbox-limit 5]
462
- shipmail partner organizations invite <id> [--owner-email owner@example.com]
463
- shipmail partner organizations suspend <id>
464
- shipmail partner organizations resume <id>
465
- shipmail partner organizations offboard <id>
466
- shipmail partner credential-grants list
467
- shipmail partner credential-grants consume <grant-id> [--name "Marketstead webmail"] [--expires-at 2026-10-01T00:00:00Z] [--allowed-cidr 203.0.113.0/24]
468
- shipmail partner usage
469
515
 
470
516
  Booking page conferencing:
471
517
  --conferencing-provider <zoom|google_meet> Create a meeting for every booking.
@@ -487,13 +533,13 @@ Newsletter feeds:
487
533
  Global options:
488
534
  --api-key <key> API key. Defaults to SHIPMAIL_API_KEY.
489
535
  --base-url <url> API base URL. Defaults to SHIPMAIL_BASE_URL or ${DEFAULT_BASE_URL}.
490
- --organization-id Delegated child organization for supported infrastructure commands.
491
536
  --header "K: V" Extra header for the generic api command. Repeatable.
492
537
 
493
538
  Environment:
494
539
  SHIPMAIL_API_KEY API key for authenticated commands.
495
- SHIPMAIL_BASE_URL Optional API base URL.
496
- SHIPMAIL_ORGANIZATION_ID Optional delegated child organization ID.
540
+ SHIPMAIL_BASE_URL Optional API base URL. Must be https on shipmail.to.
541
+ SHIPMAIL_ALLOW_INSECURE_BASE_URL=1
542
+ Permit a non-https or non-shipmail.to base URL (development only).
497
543
  `;
498
544
  }
499
545
  function requirePositional(positional, index, label) {
@@ -557,8 +603,10 @@ async function messageMetadata(options) {
557
603
  const raw = getString(options, "metadata");
558
604
  if (!raw) return void 0;
559
605
  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.");
606
+ if (!isMessageMetadata(parsed)) {
607
+ throw new CliUsageError(
608
+ "--metadata must be a JSON object of string, number, boolean or null values, or @path/to/file.json."
609
+ );
562
610
  }
563
611
  return parsed;
564
612
  }
@@ -717,7 +765,8 @@ async function runMailboxes(client, parsed, out) {
717
765
  throw new CliUsageError("Missing --password, --password-file, or --generate-password.");
718
766
  }
719
767
  const password = passwordFile ? (await readFile(passwordFile, "utf8")).replace(/\r?\n$/, "") : passwordValue;
720
- if (!generatePassword && !password) {
768
+ const credentials = generatePassword ? { generate_password: true } : password ? { password } : void 0;
769
+ if (!credentials) {
721
770
  throw new CliUsageError("Mailbox password must not be empty.");
722
771
  }
723
772
  writeJson(
@@ -725,7 +774,7 @@ async function runMailboxes(client, parsed, out) {
725
774
  await client.mailboxes.create({
726
775
  domain_id: getRequiredString(parsed.options, "domain-id"),
727
776
  address: getRequiredString(parsed.options, "address"),
728
- ...generatePassword ? { generate_password: true } : { password },
777
+ ...credentials,
729
778
  ...displayName ? { display_name: displayName } : {}
730
779
  })
731
780
  );
@@ -887,12 +936,13 @@ async function runMailboxes(client, parsed, out) {
887
936
  }
888
937
  params = { mode: "keep" };
889
938
  } else if (mode === "fixed") {
890
- if (destinationMailboxIds.length !== 1) {
939
+ const [destinationMailboxId] = destinationMailboxIds;
940
+ if (destinationMailboxIds.length !== 1 || destinationMailboxId === void 0) {
891
941
  throw new CliUsageError("--mode fixed requires exactly one --destination-mailbox-id.");
892
942
  }
893
943
  params = {
894
944
  mode: "fixed",
895
- destination_mailbox_id: destinationMailboxIds[0],
945
+ destination_mailbox_id: destinationMailboxId,
896
946
  fallback_mailbox_id: getRequiredString(parsed.options, "fallback-mailbox-id")
897
947
  };
898
948
  } else if (mode === "round-robin") {
@@ -1215,10 +1265,10 @@ async function runMessages(client, parsed, out) {
1215
1265
  if (subcommand === "list") {
1216
1266
  const mailboxId = getString(parsed.options, "mailbox-id");
1217
1267
  const clientReference = getString(parsed.options, "client-reference");
1218
- if (!mailboxId && !clientReference) {
1268
+ const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : clientReference ? { client_reference: clientReference } : void 0;
1269
+ if (!filter) {
1219
1270
  throw new CliUsageError("Provide --mailbox-id or --client-reference.");
1220
1271
  }
1221
- const filter = mailboxId ? { mailbox_id: mailboxId, ...clientReference ? { client_reference: clientReference } : {} } : { client_reference: clientReference };
1222
1272
  writeJson(
1223
1273
  out,
1224
1274
  await client.messages.list({
@@ -2129,96 +2179,6 @@ async function runAutomations(client, parsed, out) {
2129
2179
  }
2130
2180
  throw new CliUsageError("Unknown automations command.");
2131
2181
  }
2132
- async function runPartner(client, parsed, out) {
2133
- const group = parsed.positional[1];
2134
- if (group === "usage") {
2135
- writeJson(out, await client.partner.usage());
2136
- return;
2137
- }
2138
- if (group === "credential-grants") {
2139
- const command2 = requirePositional(parsed.positional, 2, "credential grant command");
2140
- if (command2 === "list") {
2141
- writeJson(out, await client.partner.listMailboxCredentialGrants());
2142
- return;
2143
- }
2144
- if (command2 !== "consume")
2145
- throw new CliUsageError("Unknown partner credential-grants command.");
2146
- const grantId = requirePositional(parsed.positional, 3, "credential grant id");
2147
- const name = getString(parsed.options, "name");
2148
- const expiresAt = getString(parsed.options, "expires-at");
2149
- const allowedCidrs = getStringList(parsed.options, "allowed-cidr");
2150
- writeJson(
2151
- out,
2152
- await client.partner.consumeMailboxCredentialGrant(grantId, {
2153
- ...name ? { name } : {},
2154
- ...expiresAt ? { expires_at: expiresAt } : {},
2155
- ...allowedCidrs.length > 0 ? { allowed_cidrs: allowedCidrs } : {}
2156
- })
2157
- );
2158
- return;
2159
- }
2160
- if (group !== "organizations") throw new CliUsageError("Unknown partner command.");
2161
- const command = parsed.positional[2];
2162
- if (command === "list") {
2163
- writeJson(out, await client.partner.listOrganizations());
2164
- return;
2165
- }
2166
- if (command === "create") {
2167
- const classification = getString(parsed.options, "data-classification") ?? "customer";
2168
- if (classification !== "internal_test" && classification !== "customer") {
2169
- throw new CliUsageError("--data-classification must be internal_test or customer.");
2170
- }
2171
- const params = {
2172
- name: getRequiredString(parsed.options, "name"),
2173
- external_reference: getRequiredString(parsed.options, "external-reference"),
2174
- owner_email: getRequiredString(parsed.options, "owner-email"),
2175
- mailbox_limit: getNumber(parsed.options, "mailbox-limit") ?? 3,
2176
- data_classification: classification
2177
- };
2178
- writeJson(out, await client.partner.createOrganization(params));
2179
- return;
2180
- }
2181
- const id = requirePositional(parsed.positional, 3, "partner organization id");
2182
- if (command === "get") {
2183
- writeJson(out, await client.partner.getOrganization(id));
2184
- return;
2185
- }
2186
- if (command === "update") {
2187
- const name = getString(parsed.options, "name");
2188
- const mailboxLimit = getNumber(parsed.options, "mailbox-limit");
2189
- if (name === void 0 && mailboxLimit === void 0) {
2190
- throw new CliUsageError("Provide --name or --mailbox-limit.");
2191
- }
2192
- const params = {
2193
- ...name !== void 0 ? { name } : {},
2194
- ...mailboxLimit !== void 0 ? { mailbox_limit: mailboxLimit } : {}
2195
- };
2196
- writeJson(out, await client.partner.updateOrganization(id, params));
2197
- return;
2198
- }
2199
- if (command === "invite") {
2200
- writeJson(
2201
- out,
2202
- await client.partner.resendOwnershipInvitation(id, {
2203
- owner_email: getString(parsed.options, "owner-email")
2204
- })
2205
- );
2206
- return;
2207
- }
2208
- if (command === "suspend") {
2209
- writeJson(out, await client.partner.suspendOrganization(id));
2210
- return;
2211
- }
2212
- if (command === "resume") {
2213
- writeJson(out, await client.partner.resumeOrganization(id));
2214
- return;
2215
- }
2216
- if (command === "offboard") {
2217
- writeJson(out, await client.partner.offboardOrganization(id));
2218
- return;
2219
- }
2220
- throw new CliUsageError("Unknown partner organizations command.");
2221
- }
2222
2182
  function writeError(err, errOut) {
2223
2183
  if (err instanceof CliUsageError) {
2224
2184
  errOut.write(`Error: ${err.message}
@@ -2304,7 +2264,6 @@ async function runCli(argv, options = {}) {
2304
2264
  else if (command === "audiences") await runAudiences(client, parsed, out);
2305
2265
  else if (command === "calendar") await runCalendar(client, parsed, out);
2306
2266
  else if (command === "booking-pages") await runBookingPages(client, parsed, out);
2307
- else if (command === "partner") await runPartner(client, parsed, out);
2308
2267
  else throw new CliUsageError(`Unknown command: ${command}.`);
2309
2268
  return 0;
2310
2269
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
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.6.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "tsup": "^8.4.0",