@myapihq/cli 1.2.4 → 1.2.6

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 (42) hide show
  1. package/dist/commands/billing.d.ts +1 -0
  2. package/dist/commands/billing.js +61 -2
  3. package/dist/commands/domain.js +31 -7
  4. package/dist/commands/fn-validation.test.d.ts +1 -0
  5. package/dist/commands/fn-validation.test.js +60 -0
  6. package/dist/commands/fn.d.ts +16 -0
  7. package/dist/commands/fn.js +271 -0
  8. package/dist/commands/funnel.d.ts +1 -0
  9. package/dist/commands/funnel.js +101 -5
  10. package/dist/commands/keys-validation.test.d.ts +1 -0
  11. package/dist/commands/keys-validation.test.js +87 -0
  12. package/dist/commands/keys.d.ts +6 -2
  13. package/dist/commands/keys.js +189 -55
  14. package/dist/commands/payments-validation.test.d.ts +1 -0
  15. package/dist/commands/payments-validation.test.js +31 -0
  16. package/dist/commands/payments.d.ts +13 -0
  17. package/dist/commands/payments.js +219 -0
  18. package/dist/commands/webhook-validation.test.d.ts +1 -0
  19. package/dist/commands/webhook-validation.test.js +35 -0
  20. package/dist/commands/webhook.d.ts +2 -0
  21. package/dist/commands/webhook.js +72 -2
  22. package/dist/commands/workflow-validation.test.d.ts +1 -0
  23. package/dist/commands/workflow-validation.test.js +137 -0
  24. package/dist/commands/workflow.d.ts +1 -0
  25. package/dist/commands/workflow.js +58 -17
  26. package/dist/exposes.test.js +2 -0
  27. package/dist/index.js +14 -0
  28. package/dist/sdk-domain-assign.test.d.ts +1 -0
  29. package/dist/sdk-domain-assign.test.js +53 -0
  30. package/dist/sdk-function.test.d.ts +1 -0
  31. package/dist/sdk-function.test.js +257 -0
  32. package/dist/sdk-funnel-name.test.d.ts +1 -0
  33. package/dist/sdk-funnel-name.test.js +48 -0
  34. package/dist/sdk-funnel-publish.test.d.ts +1 -0
  35. package/dist/sdk-funnel-publish.test.js +89 -0
  36. package/dist/sdk-iam.test.d.ts +1 -0
  37. package/dist/sdk-iam.test.js +190 -0
  38. package/dist/sdk-payments.test.d.ts +1 -0
  39. package/dist/sdk-payments.test.js +139 -0
  40. package/dist/sdk-webhook.test.d.ts +1 -0
  41. package/dist/sdk-webhook.test.js +86 -0
  42. package/package.json +4 -2
@@ -0,0 +1,219 @@
1
+ import { payments as sdkPayments } from '@myapihq/sdk';
2
+ import { requireConfig } from '../config.js';
3
+ import { success, error, printTable, info, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
5
+ import { requireOrg } from '../helpers.js';
6
+ export const EXPOSES = [
7
+ 'POST /payments/orgs/{org_id}/connect',
8
+ 'GET /payments/orgs/{org_id}/connect',
9
+ 'POST /payments/orgs/{org_id}/charges',
10
+ 'GET /payments/orgs/{org_id}/charges',
11
+ 'GET /payments/orgs/{org_id}/charges/{id}',
12
+ 'POST /payments/orgs/{org_id}/charges/{id}/refund',
13
+ 'POST /payments/webhook/{org_id}',
14
+ ];
15
+ export const SCHEMA = {
16
+ 'stripe-key': 'string',
17
+ amount: 'string',
18
+ description: 'string',
19
+ email: 'string',
20
+ every: 'string',
21
+ 'success-url': 'string',
22
+ 'cancel-url': 'string',
23
+ };
24
+ // Dollars → positive cents. The CLI surface is dollars; the API is cents.
25
+ // Pure form returns cents or an error-message string. Charges must be > 0,
26
+ // so $0 is rejected (unlike a spend cap, where $0 is meaningful).
27
+ export function _amountToCents(raw) {
28
+ const trimmed = raw.trim();
29
+ const n = Number(trimmed);
30
+ if (trimmed === '' || !Number.isFinite(n) || n <= 0) {
31
+ return `"${raw}" is not a valid amount — use a positive number, e.g. 19 or 9.99.`;
32
+ }
33
+ return Math.round(n * 100);
34
+ }
35
+ function dollars(cents) {
36
+ return `$${(cents / 100).toFixed(2)}`;
37
+ }
38
+ function summarizeCharge(c) {
39
+ return {
40
+ id: c.id,
41
+ amount: `${dollars(c.amount_cents)} ${c.currency.toUpperCase()}`,
42
+ every: c.every || 'one-off',
43
+ status: c.status,
44
+ email: c.customer_email || '',
45
+ created_at: c.created_at ? formatDate(c.created_at) : '',
46
+ };
47
+ }
48
+ // ── Subcommands ──────────────────────────────────────────────────────────────
49
+ export async function connect(flags) {
50
+ const config = requireConfig();
51
+ const orgId = requireOrg(flags, config, 'myapi payments connect --stripe-key <sk_...> [--org <id>]');
52
+ const key = flags['stripe-key'];
53
+ if (!key) {
54
+ error('Missing --stripe-key.\nUsage: myapi payments connect --stripe-key <sk_...>\n\n→ Your own Stripe secret key (starts with "sk_"). T0 is bring-your-own-Stripe.');
55
+ }
56
+ if (!key.startsWith('sk_'))
57
+ error('--stripe-key must be a Stripe secret key (starts with "sk_").');
58
+ const res = await sdkPayments.connect(config.api_key, orgId, key);
59
+ success('Stripe connected.');
60
+ info(`Tier: ${res.tier}`);
61
+ info(`Stripe account: ${res.stripe_account_id}`);
62
+ info(`Onboarding: ${res.onboarding_status}`);
63
+ }
64
+ export async function status(flags) {
65
+ const config = requireConfig();
66
+ const orgId = requireOrg(flags, config, 'myapi payments status [--org <id>]');
67
+ const res = await sdkPayments.getConnect(config.api_key, orgId);
68
+ if (flags.json) {
69
+ printJson(res);
70
+ return;
71
+ }
72
+ info(`Tier: ${res.tier}`);
73
+ info(`Stripe account: ${res.stripe_account_id}`);
74
+ info(`Onboarding: ${res.onboarding_status}`);
75
+ if (res.application_fee_bps != null) {
76
+ info(`Platform fee: ${res.application_fee_bps} bps`);
77
+ }
78
+ }
79
+ export async function charge(flags) {
80
+ const config = requireConfig();
81
+ const orgId = requireOrg(flags, config, 'myapi payments charge --amount <usd> [--description <text>] [--email <addr>] [--every month|year]');
82
+ const amountRaw = flags.amount;
83
+ if (!amountRaw) {
84
+ error('Missing --amount.\nUsage: myapi payments charge --amount <usd> [--description <text>] [--email <addr>] [--every month|year]\n\n→ --amount is in dollars (e.g. 19 or 9.99).');
85
+ }
86
+ const cents = _amountToCents(amountRaw);
87
+ if (typeof cents === 'string')
88
+ error(`--amount: ${cents}`);
89
+ const every = flags.every;
90
+ if (every && every !== 'month' && every !== 'year') {
91
+ error(`Invalid --every "${every}". Use "month" or "year" for a subscription, or omit for a one-off payment.`);
92
+ }
93
+ const payload = { amount_cents: cents };
94
+ if (flags.description)
95
+ payload.description = flags.description;
96
+ if (flags.email)
97
+ payload.email = flags.email;
98
+ if (every)
99
+ payload.every = every;
100
+ if (flags['success-url'])
101
+ payload.success_url = flags['success-url'];
102
+ if (flags['cancel-url'])
103
+ payload.cancel_url = flags['cancel-url'];
104
+ const res = await sdkPayments.createCharge(config.api_key, orgId, payload);
105
+ if (flags.json) {
106
+ printJson(res);
107
+ return;
108
+ }
109
+ success(`Charge created: ${res.payment_id}`);
110
+ info(`Status: ${res.status}`);
111
+ info('');
112
+ info('Send the customer to this hosted Stripe Checkout URL:');
113
+ info(` ${res.checkout_url}`);
114
+ }
115
+ export async function list(flags) {
116
+ const config = requireConfig();
117
+ const orgId = requireOrg(flags, config, 'myapi payments list [--org <id>]');
118
+ const charges = await sdkPayments.listCharges(config.api_key, orgId);
119
+ if (flags.json) {
120
+ printJson(charges);
121
+ return;
122
+ }
123
+ printTable(charges.map(summarizeCharge), {
124
+ flags,
125
+ empty: 'No charges yet. Create one with: myapi payments charge --amount <usd>',
126
+ });
127
+ }
128
+ export async function get(id, flags) {
129
+ const config = requireConfig();
130
+ const orgId = requireOrg(flags, config, 'myapi payments get <charge_id> [--org <id>]');
131
+ if (!id)
132
+ error('Missing charge id.\nUsage: myapi payments get <charge_id>');
133
+ const c = await sdkPayments.getCharge(config.api_key, orgId, id);
134
+ if (flags.json) {
135
+ printJson(c);
136
+ return;
137
+ }
138
+ info(`ID: ${c.id}`);
139
+ info(`Amount: ${dollars(c.amount_cents)} ${c.currency.toUpperCase()}`);
140
+ info(`Billing: ${c.every ? `every ${c.every}` : 'one-off'}`);
141
+ if (c.description)
142
+ info(`Description: ${c.description}`);
143
+ if (c.customer_email)
144
+ info(`Customer: ${c.customer_email}`);
145
+ info(`Status: ${c.status}`);
146
+ if (c.created_at)
147
+ info(`Created: ${formatDate(c.created_at)}`);
148
+ if (c.succeeded_at)
149
+ info(`Succeeded: ${formatDate(c.succeeded_at)}`);
150
+ if (c.refunded_at)
151
+ info(`Refunded: ${formatDate(c.refunded_at)}`);
152
+ }
153
+ export async function refund(id, flags) {
154
+ const config = requireConfig();
155
+ const orgId = requireOrg(flags, config, 'myapi payments refund <charge_id> [--org <id>]');
156
+ if (!id)
157
+ error('Missing charge id.\nUsage: myapi payments refund <charge_id>');
158
+ const res = await sdkPayments.refundCharge(config.api_key, orgId, id);
159
+ success(`Charge ${res.id} refunded (status: ${res.status}).`);
160
+ }
161
+ // ── Dispatcher ───────────────────────────────────────────────────────────────
162
+ const SUBCOMMAND_USAGE = {
163
+ 'connect': `myapi payments connect --stripe-key <sk_...> [--org <id>]
164
+
165
+ Links your own Stripe account (T0 — bring-your-own-Stripe). The key is
166
+ validated live against Stripe, stored encrypted, and never echoed. Connect
167
+ Express (T1) is not yet available.`,
168
+ 'status': 'myapi payments status [--org <id>] [--json]',
169
+ 'charge': `myapi payments charge --amount <usd> [--description <text>] [--email <addr>] [--every month|year] [--success-url <url>] [--cancel-url <url>] [--org <id>]
170
+
171
+ Opens a Stripe Checkout Session on your connected account and returns a
172
+ hosted checkout URL to send the customer to. --amount is in dollars.
173
+ --every makes it a recurring subscription; omit it for a one-off payment.
174
+
175
+ Examples:
176
+ myapi payments charge --amount 19 --description "Pro plan"
177
+ myapi payments charge --amount 9.99 --every month --email user@example.com`,
178
+ 'list': 'myapi payments list [--org <id>] [--json]',
179
+ 'get': 'myapi payments get <charge_id> [--org <id>] [--json]',
180
+ 'refund': `myapi payments refund <charge_id> [--org <id>]
181
+
182
+ Issues a full refund. Partial refunds are not supported.`,
183
+ };
184
+ export async function run(subcommand, args, flags) {
185
+ if (!subcommand || (flags.help && !subcommand)) {
186
+ info(`Usage: myapi payments <subcommand>
187
+
188
+ Take payments with Stripe Checkout. Connect your Stripe account, then
189
+ create one-off or recurring charges and refund them.
190
+
191
+ Subcommands:
192
+ connect Link your Stripe account (T0 — bring your own key)
193
+ status Show the org's Stripe connection status
194
+ charge Create a charge and get a hosted checkout URL
195
+ list List charges in your org
196
+ get <charge_id> Inspect a charge
197
+ refund <charge_id> Full-refund a charge
198
+
199
+ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
200
+ return;
201
+ }
202
+ if (flags.help) {
203
+ const usage = SUBCOMMAND_USAGE[subcommand];
204
+ if (usage)
205
+ info(`Usage: ${usage}`);
206
+ else
207
+ info(`Unknown subcommand: ${subcommand}. Run "myapi payments --help" for the list.`);
208
+ return;
209
+ }
210
+ switch (subcommand) {
211
+ case 'connect': return connect(flags);
212
+ case 'status': return status(flags);
213
+ case 'charge': return charge(flags);
214
+ case 'list': return list(flags);
215
+ case 'get': return get(args[0], flags);
216
+ case 'refund': return refund(args[0], flags);
217
+ default: error(`Unknown subcommand: ${subcommand}. Run "myapi payments --help" for available subcommands.`);
218
+ }
219
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ // Unit tests for the webhook CLI's forward_url validator (2026-05-15).
2
+ import { describe, it, expect } from 'vitest';
3
+ import { _validateForwardUrl } from './webhook.js';
4
+ describe('_validateForwardUrl', () => {
5
+ describe('accepts', () => {
6
+ it.each([
7
+ ['', 'empty string (means "unset")'],
8
+ ['https://example.com/forward', 'standard https'],
9
+ ['http://example.com/forward', 'http (non-TLS)'],
10
+ ['https://api.stripe.com', 'no path'],
11
+ ['https://example.com:8443/path?q=v#h', 'port, query, fragment'],
12
+ ])('accepts %s (%s)', (url) => {
13
+ expect(_validateForwardUrl(url)).toBeNull();
14
+ });
15
+ });
16
+ describe('rejects non-http(s) protocols', () => {
17
+ it.each([
18
+ ['ftp://example.com', 'ftp'],
19
+ ['javascript:alert(1)', 'javascript: scheme'],
20
+ ['file:///etc/passwd', 'file:'],
21
+ ['data:text/plain,foo', 'data:'],
22
+ ])('rejects %s (%s)', (url) => {
23
+ expect(_validateForwardUrl(url)).toMatch(/http:\/\/ or https:\/\//);
24
+ });
25
+ });
26
+ describe('rejects malformed URLs', () => {
27
+ it.each([
28
+ 'not a url',
29
+ 'http//missing-colon.example.com',
30
+ 'just-some-text',
31
+ ])('rejects %s', (url) => {
32
+ expect(_validateForwardUrl(url)).toMatch(/not a valid URL/);
33
+ });
34
+ });
35
+ });
@@ -5,6 +5,8 @@ export declare const EXPOSES: Exposes;
5
5
  export declare const SCHEMA: FlagSchema;
6
6
  export declare function list(flags: Flags): Promise<void>;
7
7
  export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
8
+ export declare function update(id: string, flags: Flags): Promise<void>;
9
+ export declare function _validateForwardUrl(url: string): string | null;
8
10
  export declare function del(id: string, flags: Flags): Promise<void>;
9
11
  export declare function delivery(id: string, flags: Flags): Promise<void>;
10
12
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -5,6 +5,7 @@ import { requireOrg } from '../helpers.js';
5
5
  export const EXPOSES = [
6
6
  'POST /webhook/orgs/{org_id}/endpoints',
7
7
  'GET /webhook/orgs/{org_id}/endpoints',
8
+ 'PATCH /webhook/orgs/{org_id}/endpoints/{endpoint_id}',
8
9
  'DELETE /webhook/orgs/{org_id}/endpoints/{endpoint_id}',
9
10
  'GET /webhook/orgs/{org_id}/deliveries/{delivery_id}',
10
11
  ];
@@ -13,6 +14,8 @@ export const SCHEMA = {
13
14
  name: 'string',
14
15
  description: 'string',
15
16
  'crm-email-path': 'string',
17
+ // 2026-05-15: backend's POST/PATCH endpoints accept forward_url.
18
+ 'forward-url': 'string',
16
19
  };
17
20
  export async function list(flags) {
18
21
  const config = requireConfig();
@@ -29,21 +32,78 @@ export async function list(flags) {
29
32
  }
30
33
  export async function create(nameArg, flags) {
31
34
  const config = requireConfig();
32
- const orgId = requireOrg(flags, config, 'myapi webhook create <name> [--description <desc>] [--org <id>]');
35
+ const orgId = requireOrg(flags, config, 'myapi webhook create <name> [--description <desc>] [--crm-email-path <path>] [--forward-url <url>] [--org <id>]');
33
36
  const name = nameArg || flags.name;
34
37
  if (!name)
35
- error('Missing required arguments.\nUsage: myapi webhook create <name> [--description <desc>] [--crm-email-path <path>] [--org <id>]\n or: myapi webhook create --name <name> [...]');
38
+ error('Missing required arguments.\nUsage: myapi webhook create <name> [--description <desc>] [--crm-email-path <path>] [--forward-url <url>] [--org <id>]\n or: myapi webhook create --name <name> [...]');
36
39
  const opts = {};
37
40
  if (typeof flags.description === 'string')
38
41
  opts.description = flags.description;
39
42
  if (typeof flags['crm-email-path'] === 'string')
40
43
  opts.crm_email_path = flags['crm-email-path'];
44
+ if (typeof flags['forward-url'] === 'string') {
45
+ validateForwardUrl(flags['forward-url']);
46
+ opts.forward_url = flags['forward-url'];
47
+ }
41
48
  const res = await sdkWebhook.createEndpoint(config.api_key, orgId, name, opts);
42
49
  if (flags.json) {
43
50
  printJson(res);
44
51
  return;
45
52
  }
46
53
  success(`Webhook created! ID: ${res.id}\nInbound URL: ${res.url}`);
54
+ if (opts.forward_url)
55
+ info(`Forwarding to: ${opts.forward_url}`);
56
+ }
57
+ export async function update(id, flags) {
58
+ const config = requireConfig();
59
+ const orgId = requireOrg(flags, config, 'myapi webhook update <id> [--name <name>] [--description <desc>] [--crm-email-path <path>] [--forward-url <url>] [--org <id>]');
60
+ if (!id)
61
+ error('Missing required arguments.\nUsage: myapi webhook update <id> [--name <name>] [--description <desc>] [--crm-email-path <path>] [--forward-url <url>]');
62
+ const payload = {};
63
+ if (typeof flags.name === 'string')
64
+ payload.name = flags.name;
65
+ if (typeof flags.description === 'string')
66
+ payload.description = flags.description;
67
+ // crm-email-path & forward-url accept "" (empty string) to explicitly
68
+ // disable. Don't coerce to undefined — that would mean "leave as-is".
69
+ if (typeof flags['crm-email-path'] === 'string')
70
+ payload.crm_email_path = flags['crm-email-path'];
71
+ if (typeof flags['forward-url'] === 'string') {
72
+ const fu = flags['forward-url'];
73
+ if (fu !== '')
74
+ validateForwardUrl(fu);
75
+ payload.forward_url = fu;
76
+ }
77
+ if (Object.keys(payload).length === 0) {
78
+ error('Nothing to update. Provide at least one of --name, --description, --crm-email-path, --forward-url.');
79
+ }
80
+ const res = await sdkWebhook.updateEndpoint(config.api_key, orgId, id, payload);
81
+ if (flags.json) {
82
+ printJson(res);
83
+ return;
84
+ }
85
+ success(`Webhook ${res.id} updated`);
86
+ }
87
+ // Exported for unit tests. Returns null on success, error message on failure.
88
+ // Matches the backend's documented "Use http:// or https://" constraint.
89
+ export function _validateForwardUrl(url) {
90
+ if (url === '')
91
+ return null; // empty = unset; valid
92
+ try {
93
+ const u = new URL(url);
94
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
95
+ return `--forward-url must use http:// or https:// (got ${u.protocol})`;
96
+ }
97
+ return null;
98
+ }
99
+ catch {
100
+ return `--forward-url is not a valid URL: "${url}"`;
101
+ }
102
+ }
103
+ function validateForwardUrl(url) {
104
+ const err = _validateForwardUrl(url);
105
+ if (err)
106
+ error(err);
47
107
  }
48
108
  export async function del(id, flags) {
49
109
  const config = requireConfig();
@@ -74,6 +134,14 @@ CRM auto-ingest (optional):
74
134
  Default 'email' (top-level). For Stripe:
75
135
  'data.object.customer_email'. For GitHub:
76
136
  'sender.email'. Empty string disables ingest.`,
137
+ 'update': `myapi webhook update <id> [--name <name>] [--description <desc>] [--crm-email-path <path>] [--forward-url <url>] [--org <id>]
138
+
139
+ All fields optional — only the ones you pass get changed. Pass an empty
140
+ string to --crm-email-path or --forward-url to explicitly clear that
141
+ field (e.g., --forward-url "" disables forwarding without deleting the
142
+ endpoint).
143
+
144
+ Slug is immutable; create a new endpoint if you need a different slug.`,
77
145
  'delete': 'myapi webhook delete <id> [--org <id>]',
78
146
  'delivery': 'myapi webhook delivery <delivery_id> [--org <id>]',
79
147
  };
@@ -84,6 +152,7 @@ export async function run(subcommand, args, flags) {
84
152
  Subcommands:
85
153
  list List all inbound webhook endpoints
86
154
  create Create a new endpoint to receive data (returns an inbound URL)
155
+ update Patch endpoint name / description / crm-email-path / forward-url
87
156
  delete Delete a webhook endpoint
88
157
  delivery Inspect a specific webhook delivery (payload, received_at)
89
158
 
@@ -118,6 +187,7 @@ See the my-webhook-api skill for the full HTML + JS recipe.`);
118
187
  switch (subcommand) {
119
188
  case 'list': return list(flags);
120
189
  case 'create': return create(args[0], flags);
190
+ case 'update': return update(args[0], flags);
121
191
  case 'delete': return del(args[0], flags);
122
192
  case 'delivery': return delivery(args[0], flags);
123
193
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi webhook --help" for a list of valid subcommands.`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ // Unit tests for workflow.ts CLI's pure step validator. The 2026-05-15
2
+ // additive contract change adds `http_request` (alias `http`) to the
3
+ // supported step types; this test file pins the new validation rules and
4
+ // regression-tests the prior shapes.
5
+ import { describe, it, expect } from 'vitest';
6
+ import { _validateSteps } from './workflow.js';
7
+ const VALID_EMAIL = { type: 'send_email', from: 'a@x.com', to: 'b@x.com', subject: 'hi', html: '<p>x</p>' };
8
+ const VALID_SLACK = { type: 'slack_message', webhook_url: 'https://hooks.slack.com/services/T1/B1/abc', text: 'hi' };
9
+ const VALID_HTTP = { type: 'http_request', url: 'https://example.com/hook' };
10
+ describe('_validateSteps — input shape', () => {
11
+ it('rejects non-array', () => {
12
+ expect(_validateSteps('not an array')).toMatch(/must be a JSON array/);
13
+ expect(_validateSteps({})).toMatch(/must be a JSON array/);
14
+ expect(_validateSteps(null)).toMatch(/must be a JSON array/);
15
+ });
16
+ it('rejects empty array', () => {
17
+ expect(_validateSteps([])).toMatch(/cannot be an empty array/);
18
+ });
19
+ it('rejects steps that are not objects', () => {
20
+ expect(_validateSteps(['string'])).toMatch(/must be a JSON object/);
21
+ expect(_validateSteps([null])).toMatch(/must be a JSON object/);
22
+ });
23
+ it('rejects unknown step types', () => {
24
+ expect(_validateSteps([{ type: 'pigeon' }])).toMatch(/unknown type "pigeon"/);
25
+ });
26
+ it('rejects steps missing the type field', () => {
27
+ expect(_validateSteps([{ from: 'x' }])).toMatch(/missing required field "type"/);
28
+ });
29
+ });
30
+ describe('_validateSteps — send_email / email', () => {
31
+ it('accepts valid send_email with html body', () => {
32
+ expect(_validateSteps([VALID_EMAIL])).toBeNull();
33
+ });
34
+ it('accepts the `email` alias', () => {
35
+ expect(_validateSteps([{ ...VALID_EMAIL, type: 'email' }])).toBeNull();
36
+ });
37
+ it('rejects send_email missing required fields', () => {
38
+ expect(_validateSteps([{ type: 'send_email', from: 'a@x.com' }]))
39
+ .toMatch(/missing required field "to"/);
40
+ });
41
+ it('rejects send_email with no body form', () => {
42
+ expect(_validateSteps([{ type: 'send_email', from: 'a@x.com', to: 'b@x.com', subject: 's' }]))
43
+ .toMatch(/must include exactly one of "body", "html", or "template_id"/);
44
+ });
45
+ it('rejects send_email with multiple body forms', () => {
46
+ expect(_validateSteps([{ ...VALID_EMAIL, body: 'plain', html: '<p>x</p>' }]))
47
+ .toMatch(/can only use one of/);
48
+ });
49
+ it('rejects template_vars without template_id', () => {
50
+ expect(_validateSteps([{ ...VALID_EMAIL, template_vars: { name: 'x' } }]))
51
+ .toMatch(/"template_vars" only makes sense with "template_id"/);
52
+ });
53
+ });
54
+ describe('_validateSteps — slack_message / slack', () => {
55
+ it('accepts valid slack_message', () => {
56
+ expect(_validateSteps([VALID_SLACK])).toBeNull();
57
+ });
58
+ it('accepts the `slack` alias', () => {
59
+ expect(_validateSteps([{ ...VALID_SLACK, type: 'slack' }])).toBeNull();
60
+ });
61
+ it('rejects slack with non-Slack webhook URL', () => {
62
+ expect(_validateSteps([{ ...VALID_SLACK, webhook_url: 'https://example.com/hook' }]))
63
+ .toMatch(/must look like https:\/\/hooks\.slack\.com/);
64
+ });
65
+ it('rejects slack missing text', () => {
66
+ expect(_validateSteps([{ type: 'slack_message', webhook_url: VALID_SLACK.webhook_url }]))
67
+ .toMatch(/missing required field "text"/);
68
+ });
69
+ });
70
+ describe('_validateSteps — http_request / http (2026-05-15)', () => {
71
+ it('accepts the canonical type name', () => {
72
+ expect(_validateSteps([VALID_HTTP])).toBeNull();
73
+ });
74
+ it('accepts the `http` alias', () => {
75
+ expect(_validateSteps([{ ...VALID_HTTP, type: 'http' }])).toBeNull();
76
+ });
77
+ it('accepts all supported HTTP methods', () => {
78
+ for (const m of ['GET', 'POST', 'PATCH', 'PUT', 'DELETE']) {
79
+ expect(_validateSteps([{ ...VALID_HTTP, method: m }])).toBeNull();
80
+ }
81
+ });
82
+ it('accepts a body string with templating', () => {
83
+ expect(_validateSteps([{ ...VALID_HTTP, body: '{"email":"{{ payload.email }}"}' }])).toBeNull();
84
+ });
85
+ it('accepts a headers map of strings', () => {
86
+ expect(_validateSteps([{ ...VALID_HTTP, headers: { 'X-Token': 'abc', 'X-Other': 'def' } }])).toBeNull();
87
+ });
88
+ it('rejects when url is missing', () => {
89
+ expect(_validateSteps([{ type: 'http_request' }]))
90
+ .toMatch(/missing required field "url"/);
91
+ });
92
+ it('rejects when url is not a string', () => {
93
+ expect(_validateSteps([{ type: 'http_request', url: 12345 }]))
94
+ .toMatch(/missing required field "url"/);
95
+ });
96
+ it('rejects non-http(s) URLs', () => {
97
+ expect(_validateSteps([{ ...VALID_HTTP, url: 'ftp://example.com' }]))
98
+ .toMatch(/must start with http:\/\/ or https:\/\//);
99
+ expect(_validateSteps([{ ...VALID_HTTP, url: 'javascript:alert(1)' }]))
100
+ .toMatch(/must start with http:\/\/ or https:\/\//);
101
+ });
102
+ it('rejects unknown HTTP methods', () => {
103
+ expect(_validateSteps([{ ...VALID_HTTP, method: 'TRACE' }]))
104
+ .toMatch(/"method" must be one of/);
105
+ });
106
+ it('rejects body as a non-string (object)', () => {
107
+ expect(_validateSteps([{ ...VALID_HTTP, body: { not: 'a string' } }]))
108
+ .toMatch(/"body" must be a string/);
109
+ });
110
+ it('rejects headers as a non-object', () => {
111
+ expect(_validateSteps([{ ...VALID_HTTP, headers: 'X-Token: abc' }]))
112
+ .toMatch(/"headers" must be a JSON object/);
113
+ });
114
+ it('rejects headers as an array', () => {
115
+ expect(_validateSteps([{ ...VALID_HTTP, headers: ['X-Token: abc'] }]))
116
+ .toMatch(/"headers" must be a JSON object/);
117
+ });
118
+ it('rejects header value that is not a string', () => {
119
+ expect(_validateSteps([{ ...VALID_HTTP, headers: { 'X-Token': 42 } }]))
120
+ .toMatch(/"headers" value for "X-Token" must be a string/);
121
+ });
122
+ });
123
+ describe('_validateSteps — mixed-type pipelines', () => {
124
+ it('accepts an http_request after a send_email (pipeline shape)', () => {
125
+ expect(_validateSteps([
126
+ VALID_EMAIL,
127
+ { ...VALID_HTTP, body: '{"sent_to":"{{ payload.email }}"}' },
128
+ ])).toBeNull();
129
+ });
130
+ it('reports the failing step index in the error message', () => {
131
+ expect(_validateSteps([
132
+ VALID_EMAIL,
133
+ VALID_SLACK,
134
+ { type: 'http_request' }, // missing url at index 2
135
+ ])).toMatch(/^step 2 \(http_request\):/);
136
+ });
137
+ });
@@ -3,6 +3,7 @@ import { type Flags } from '../helpers.js';
3
3
  import type { Exposes } from '../exposes.js';
4
4
  export declare const EXPOSES: Exposes;
5
5
  export declare const SCHEMA: FlagSchema;
6
+ export declare function _validateSteps(steps: unknown): string | null;
6
7
  export declare function list(flags: Flags): Promise<void>;
7
8
  export declare function get(id: string, flags: Flags): Promise<void>;
8
9
  export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;