@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.
- package/dist/commands/billing.d.ts +1 -0
- package/dist/commands/billing.js +61 -2
- package/dist/commands/domain.js +31 -7
- package/dist/commands/fn-validation.test.d.ts +1 -0
- package/dist/commands/fn-validation.test.js +60 -0
- package/dist/commands/fn.d.ts +16 -0
- package/dist/commands/fn.js +271 -0
- package/dist/commands/funnel.d.ts +1 -0
- package/dist/commands/funnel.js +101 -5
- package/dist/commands/keys-validation.test.d.ts +1 -0
- package/dist/commands/keys-validation.test.js +87 -0
- package/dist/commands/keys.d.ts +6 -2
- package/dist/commands/keys.js +189 -55
- package/dist/commands/payments-validation.test.d.ts +1 -0
- package/dist/commands/payments-validation.test.js +31 -0
- package/dist/commands/payments.d.ts +13 -0
- package/dist/commands/payments.js +219 -0
- package/dist/commands/webhook-validation.test.d.ts +1 -0
- package/dist/commands/webhook-validation.test.js +35 -0
- package/dist/commands/webhook.d.ts +2 -0
- package/dist/commands/webhook.js +72 -2
- package/dist/commands/workflow-validation.test.d.ts +1 -0
- package/dist/commands/workflow-validation.test.js +137 -0
- package/dist/commands/workflow.d.ts +1 -0
- package/dist/commands/workflow.js +58 -17
- package/dist/exposes.test.js +2 -0
- package/dist/index.js +14 -0
- package/dist/sdk-domain-assign.test.d.ts +1 -0
- package/dist/sdk-domain-assign.test.js +53 -0
- package/dist/sdk-function.test.d.ts +1 -0
- package/dist/sdk-function.test.js +257 -0
- package/dist/sdk-funnel-name.test.d.ts +1 -0
- package/dist/sdk-funnel-name.test.js +48 -0
- package/dist/sdk-funnel-publish.test.d.ts +1 -0
- package/dist/sdk-funnel-publish.test.js +89 -0
- package/dist/sdk-iam.test.d.ts +1 -0
- package/dist/sdk-iam.test.js +190 -0
- package/dist/sdk-payments.test.d.ts +1 -0
- package/dist/sdk-payments.test.js +139 -0
- package/dist/sdk-webhook.test.d.ts +1 -0
- package/dist/sdk-webhook.test.js +86 -0
- 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>;
|
package/dist/commands/webhook.js
CHANGED
|
@@ -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>;
|