@myapihq/cli 1.2.5 → 1.2.7
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 +2 -0
- package/dist/commands/billing.js +94 -2
- package/dist/commands/container-validation.test.d.ts +1 -0
- package/dist/commands/container-validation.test.js +45 -0
- package/dist/commands/container.d.ts +15 -0
- package/dist/commands/container.js +242 -0
- package/dist/commands/domain.js +5 -0
- package/dist/commands/email/campaign.js +16 -1
- package/dist/commands/email/message.js +26 -3
- package/dist/commands/email/template.js +8 -1
- package/dist/commands/fn.d.ts +3 -0
- package/dist/commands/fn.js +115 -24
- package/dist/commands/funnel.d.ts +1 -0
- package/dist/commands/funnel.js +83 -0
- 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/pixel.js +15 -2
- package/dist/commands/update.d.ts +1 -1
- package/dist/commands/update.js +68 -45
- package/dist/commands/webhook.js +10 -1
- package/dist/completion.d.ts +3 -1
- package/dist/completion.js +160 -55
- package/dist/exposes.test.js +2 -0
- package/dist/index.js +34 -13
- package/dist/sdk-billing-usage.test.d.ts +1 -0
- package/dist/sdk-billing-usage.test.js +74 -0
- package/dist/sdk-container.test.d.ts +1 -0
- package/dist/sdk-container.test.js +115 -0
- package/dist/sdk-function.test.js +73 -6
- 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/package.json +4 -4
package/dist/commands/fn.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
1
2
|
import { fn as sdkFn } from '@myapihq/sdk';
|
|
2
3
|
import { requireConfig } from '../config.js';
|
|
3
4
|
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
5
|
+
import { formatDate } from '../utils.js';
|
|
4
6
|
import { requireOrg } from '../helpers.js';
|
|
5
7
|
export const EXPOSES = [
|
|
6
8
|
'POST /function/orgs/{org_id}/functions',
|
|
7
9
|
'GET /function/orgs/{org_id}/functions',
|
|
8
10
|
'GET /function/orgs/{org_id}/functions/{id}',
|
|
9
11
|
'DELETE /function/orgs/{org_id}/functions/{id}',
|
|
12
|
+
'POST /function/orgs/{org_id}/functions/{id}/bundle',
|
|
13
|
+
'POST /function/orgs/{org_id}/functions/{id}/env',
|
|
14
|
+
'GET /function/orgs/{org_id}/functions/{id}/runs',
|
|
10
15
|
];
|
|
11
16
|
export const SCHEMA = {
|
|
12
17
|
cron: 'string',
|
|
13
18
|
};
|
|
14
|
-
// Backend: Story 1
|
|
15
|
-
//
|
|
16
|
-
// 2). So this CLI surface is intentionally narrow — `create` registers a
|
|
17
|
-
// function, not "deploy" — keeping verb honesty until bundle upload ships.
|
|
19
|
+
// Backend: Story 1 (function CRUD + scoped key) and Story 2/4/5 (deploy a
|
|
20
|
+
// JS bundle to Cloudflare Workers, list runs, set env secrets).
|
|
18
21
|
// Mirrors validateName in myapi-hq/internal/routes/function/crud.go. We
|
|
19
22
|
// pre-validate client-side so typos fail before the network call; backend
|
|
20
23
|
// runs the same regex as defence in depth.
|
|
@@ -45,7 +48,7 @@ function summarizeFn(f) {
|
|
|
45
48
|
id: f.id,
|
|
46
49
|
name: f.name,
|
|
47
50
|
trigger: f.trigger_type === 'cron' ? `cron ${f.cron_schedule ?? '?'}` : 'http',
|
|
48
|
-
url: f.invocation_url || '(
|
|
51
|
+
url: f.invocation_url || '(not deployed)',
|
|
49
52
|
updated_at: f.updated_at,
|
|
50
53
|
};
|
|
51
54
|
}
|
|
@@ -69,16 +72,15 @@ export async function create(flags) {
|
|
|
69
72
|
info(`Name: ${result.function.name}`);
|
|
70
73
|
info(`Trigger: ${result.function.trigger_type}${result.function.cron_schedule ? ` (${result.function.cron_schedule})` : ''}`);
|
|
71
74
|
// The scoped key is returned ONCE — surface it prominently. It's used by
|
|
72
|
-
// the function runtime shim
|
|
73
|
-
//
|
|
75
|
+
// the function runtime shim to call other slot endpoints without a
|
|
76
|
+
// baked-in auth token. Deploy rotates this key.
|
|
74
77
|
info('');
|
|
75
78
|
info(`Scoped API key (returned once — save it if you need it):`);
|
|
76
79
|
info(` ${result.scoped_api_key}`);
|
|
77
80
|
info(` (id: ${result.scoped_api_key_id}; scopes: slot_call; rejected at /hq/*, /admin/*, /internal/*)`);
|
|
78
|
-
// Story 2 hasn't landed yet — be explicit about what "create" produces today.
|
|
79
81
|
if (!result.function.invocation_url) {
|
|
80
82
|
info('');
|
|
81
|
-
banner(
|
|
83
|
+
banner(`Next: deploy code with myapi fn deploy ${result.function.id} <bundle.js>`);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
export async function list(flags) {
|
|
@@ -107,7 +109,7 @@ export async function get(id, flags) {
|
|
|
107
109
|
info(`ID: ${fn.id}`);
|
|
108
110
|
info(`Name: ${fn.name}`);
|
|
109
111
|
info(`Trigger: ${fn.trigger_type}${fn.cron_schedule ? ` (${fn.cron_schedule})` : ''}`);
|
|
110
|
-
info(`Invocation URL: ${fn.invocation_url || '(
|
|
112
|
+
info(`Invocation URL: ${fn.invocation_url || '(not deployed)'}`);
|
|
111
113
|
info(`Created: ${fn.created_at}`);
|
|
112
114
|
info(`Updated: ${fn.updated_at}`);
|
|
113
115
|
}
|
|
@@ -119,26 +121,113 @@ export async function del(id, flags) {
|
|
|
119
121
|
await sdkFn.deleteFunction(config.api_key, orgId, id);
|
|
120
122
|
success(`Deleted function ${id}`);
|
|
121
123
|
}
|
|
124
|
+
// deploy uploads a single-file JS bundle. The backend wraps it with the
|
|
125
|
+
// MYAPI shim and ships it to Cloudflare Workers. The scoped API key is
|
|
126
|
+
// rotated on every deploy — the fresh value is shown once here.
|
|
127
|
+
export async function deploy(id, bundlePath, flags) {
|
|
128
|
+
const config = requireConfig();
|
|
129
|
+
const orgId = requireOrg(flags, config, 'myapi fn deploy <id> <bundle.js> [--org <id>]');
|
|
130
|
+
if (!id)
|
|
131
|
+
error('Missing id.\nUsage: myapi fn deploy <id> <bundle.js>');
|
|
132
|
+
if (!bundlePath)
|
|
133
|
+
error('Missing bundle file.\nUsage: myapi fn deploy <id> <bundle.js>\n\n→ <bundle.js> is a single-file JavaScript bundle (≤4MB).');
|
|
134
|
+
let data;
|
|
135
|
+
try {
|
|
136
|
+
data = await readFile(bundlePath);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
if (e?.code === 'ENOENT')
|
|
140
|
+
error(`File not found: ${bundlePath}`);
|
|
141
|
+
if (e?.code === 'EACCES')
|
|
142
|
+
error(`Permission denied: ${bundlePath}`);
|
|
143
|
+
error(`Could not read ${bundlePath}: ${e?.message ?? e}`);
|
|
144
|
+
}
|
|
145
|
+
const result = await sdkFn.uploadBundle(config.api_key, orgId, id, data, bundlePath.split('/').pop() || 'bundle.js');
|
|
146
|
+
if (flags.json) {
|
|
147
|
+
printJson(result);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
success(`Deployed function ${id}`);
|
|
151
|
+
info(`Invocation URL: ${result.invocation_url}`);
|
|
152
|
+
info('');
|
|
153
|
+
info(`Scoped API key was rotated. New value (returned once — save it if you need it):`);
|
|
154
|
+
info(` ${result.scoped_api_key}`);
|
|
155
|
+
}
|
|
156
|
+
// env sets a Worker Secret (Stripe key, etc.) on a deployed function.
|
|
157
|
+
export async function setEnv(id, name, value, flags) {
|
|
158
|
+
const config = requireConfig();
|
|
159
|
+
const orgId = requireOrg(flags, config, 'myapi fn env <id> <name> <value> [--org <id>]');
|
|
160
|
+
if (!id)
|
|
161
|
+
error('Missing id.\nUsage: myapi fn env <id> <name> <value>');
|
|
162
|
+
if (!name)
|
|
163
|
+
error('Missing secret name.\nUsage: myapi fn env <id> <name> <value>');
|
|
164
|
+
if (value === undefined)
|
|
165
|
+
error('Missing secret value.\nUsage: myapi fn env <id> <name> <value>');
|
|
166
|
+
await sdkFn.setFunctionEnv(config.api_key, orgId, id, name, value);
|
|
167
|
+
success(`Set ${name} on function ${id}`);
|
|
168
|
+
info('The value is encrypted at rest by Cloudflare and never stored or echoed by MyAPI.');
|
|
169
|
+
}
|
|
170
|
+
// runs lists recent invocation records, most recent first.
|
|
171
|
+
export async function runs(id, flags) {
|
|
172
|
+
const config = requireConfig();
|
|
173
|
+
const orgId = requireOrg(flags, config, 'myapi fn runs <id> [--org <id>]');
|
|
174
|
+
if (!id)
|
|
175
|
+
error('Missing id.\nUsage: myapi fn runs <id>');
|
|
176
|
+
const records = await sdkFn.listFunctionRuns(config.api_key, orgId, id);
|
|
177
|
+
if (flags.json) {
|
|
178
|
+
printJson(records);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
printTable(records.map(r => ({
|
|
182
|
+
id: r.id,
|
|
183
|
+
invoked_at: formatDate(r.invoked_at),
|
|
184
|
+
status: r.status || '?',
|
|
185
|
+
duration_ms: r.duration_ms ?? '',
|
|
186
|
+
error: r.error_message || '',
|
|
187
|
+
})), {
|
|
188
|
+
flags,
|
|
189
|
+
empty: 'No runs recorded yet for this function.',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
122
192
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
123
193
|
const SUBCOMMAND_USAGE = {
|
|
124
194
|
'create': `myapi fn create --name <name> [--cron <expr>] [--org <id>]
|
|
125
195
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
JavaScript code today. When Story 2 lands, the CLI surface will grow to
|
|
129
|
-
accept --bundle <file.js>.
|
|
196
|
+
Persists a function record + issues a scoped API key. Deploy code separately
|
|
197
|
+
with "myapi fn deploy".
|
|
130
198
|
|
|
131
199
|
Triggers:
|
|
132
|
-
(default) HTTP — function
|
|
133
|
-
--cron <expr> Cron — function
|
|
200
|
+
(default) HTTP — function gets a public invocation URL once deployed.
|
|
201
|
+
--cron <expr> Cron — function runs on the schedule (e.g. "0 8 * * *").
|
|
134
202
|
|
|
135
203
|
Examples:
|
|
136
204
|
myapi fn create --name my-app-api
|
|
137
205
|
myapi fn create --name daily-report --cron "0 8 * * *"
|
|
138
206
|
|
|
139
|
-
The response returns the scoped API key ONCE.
|
|
140
|
-
other MyAPI slots
|
|
141
|
-
|
|
207
|
+
The response returns the scoped API key ONCE. It lets the function call
|
|
208
|
+
other MyAPI slots with its own permissions (scopes=slot_call; rejected at
|
|
209
|
+
/hq/*, /admin/*, /internal/*).`,
|
|
210
|
+
'deploy': `myapi fn deploy <id> <bundle.js> [--org <id>] [--json]
|
|
211
|
+
|
|
212
|
+
Uploads a single-file JavaScript bundle (≤4MB) to the edge runtime. The
|
|
213
|
+
backend wraps it with the MYAPI shim and ships it to Cloudflare Workers.
|
|
214
|
+
|
|
215
|
+
The scoped API key is rotated on every deploy — the fresh value is printed
|
|
216
|
+
once. After deploy the function has a live invocation URL.
|
|
217
|
+
|
|
218
|
+
Example:
|
|
219
|
+
myapi fn deploy <id> ./dist/bundle.js`,
|
|
220
|
+
'env': `myapi fn env <id> <name> <value> [--org <id>]
|
|
221
|
+
|
|
222
|
+
Sets a secret (Stripe key, API token, ...) as a Cloudflare Worker Secret on
|
|
223
|
+
a deployed function. The value is encrypted at rest and never stored in
|
|
224
|
+
MyAPI or echoed back. The function must already be deployed.
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
myapi fn env <id> STRIPE_KEY sk_live_...`,
|
|
228
|
+
'runs': `myapi fn runs <id> [--org <id>] [--json]
|
|
229
|
+
|
|
230
|
+
Lists recent invocation records (most recent first, up to 100).`,
|
|
142
231
|
'list': 'myapi fn list [--org <id>] [--json]',
|
|
143
232
|
'get': 'myapi fn get <id> [--org <id>] [--json]',
|
|
144
233
|
'delete': 'myapi fn delete <id> [--org <id>]',
|
|
@@ -147,14 +236,13 @@ export async function run(subcommand, args, flags) {
|
|
|
147
236
|
if (!subcommand || (flags.help && !subcommand)) {
|
|
148
237
|
info(`Usage: myapi fn <subcommand>
|
|
149
238
|
|
|
150
|
-
Create and manage functions on the MyAPI edge runtime.
|
|
151
|
-
|
|
152
|
-
Backend status: Story 1 shipped (metadata + scoped API key). Story 2 (CF
|
|
153
|
-
Workers upload, invocation URL, /logs, /env) lands soon. CLI verbs grow
|
|
154
|
-
once Story 2 ships; today only "create" / "list" / "get" / "delete" work.
|
|
239
|
+
Create, deploy, and manage functions on the MyAPI edge runtime.
|
|
155
240
|
|
|
156
241
|
Subcommands:
|
|
157
242
|
create Register a function and get its scoped API key (returned once)
|
|
243
|
+
deploy <id> <file> Upload a JS bundle and go live
|
|
244
|
+
env <id> <k> <v> Set a Worker Secret on a deployed function
|
|
245
|
+
runs <id> List recent invocation records
|
|
158
246
|
list List functions in your org
|
|
159
247
|
get <id> Inspect a function
|
|
160
248
|
delete <id> Soft-delete and revoke its scoped API key
|
|
@@ -172,6 +260,9 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
172
260
|
}
|
|
173
261
|
switch (subcommand) {
|
|
174
262
|
case 'create': return create(flags);
|
|
263
|
+
case 'deploy': return deploy(args[0], args[1], flags);
|
|
264
|
+
case 'env': return setEnv(args[0], args[1], args[2], flags);
|
|
265
|
+
case 'runs': return runs(args[0], flags);
|
|
175
266
|
case 'list': return list(flags);
|
|
176
267
|
case 'get': return get(args[0], flags);
|
|
177
268
|
case 'delete': return del(args[0], flags);
|
|
@@ -10,4 +10,5 @@ export declare function del(id: string, flags: Flags): Promise<void>;
|
|
|
10
10
|
export declare function pages(funnelArg: string, flags: Flags): Promise<void>;
|
|
11
11
|
export declare function push(slug: string, flags: Flags): Promise<void>;
|
|
12
12
|
export declare function verify(slug: string, flags: Flags): Promise<void>;
|
|
13
|
+
export declare function publish(dir: string, flags: Flags): Promise<void>;
|
|
13
14
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/funnel.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'path';
|
|
1
3
|
import { funnel as sdkFunnel, hq } from '@myapihq/sdk';
|
|
2
4
|
import { requireConfig } from '../config.js';
|
|
3
5
|
import { success, error, printTable, info, printJson } from '../output.js';
|
|
@@ -10,12 +12,15 @@ export const EXPOSES = [
|
|
|
10
12
|
'DELETE /funnel/orgs/{org_id}/funnels/{funnel_id}',
|
|
11
13
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/push-page',
|
|
12
14
|
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/verify',
|
|
15
|
+
'POST /funnel/orgs/{org_id}/funnels/{funnel_id}/files',
|
|
13
16
|
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}/pages',
|
|
14
17
|
'GET /hq/orgs/{org_id}',
|
|
15
18
|
];
|
|
16
19
|
export const SCHEMA = {
|
|
17
20
|
funnel: 'string',
|
|
18
21
|
slug: 'string',
|
|
22
|
+
env: 'string',
|
|
23
|
+
'api-fn': 'string',
|
|
19
24
|
};
|
|
20
25
|
// Backend (2026-05-15): POST /funnels accepts an optional `name` (defaults
|
|
21
26
|
// to the org's preview_subdomain for back-compat). Mirror the backend's
|
|
@@ -193,6 +198,66 @@ export async function verify(slug, flags) {
|
|
|
193
198
|
const v = await sdkFunnel.verifyFunnel(config.api_key, orgId, funnelId, { slug: finalSlug });
|
|
194
199
|
printJson(v);
|
|
195
200
|
}
|
|
201
|
+
// Recursively collect every file under `dir`, returning site-relative
|
|
202
|
+
// paths (POSIX separators) paired with their contents.
|
|
203
|
+
async function collectFiles(dir, root) {
|
|
204
|
+
const out = [];
|
|
205
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
206
|
+
const abs = join(dir, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
out.push(...await collectFiles(abs, root));
|
|
209
|
+
}
|
|
210
|
+
else if (entry.isFile()) {
|
|
211
|
+
out.push({ path: relative(root, abs).split(sep).join('/'), content: await readFile(abs) });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
// publish uploads a whole directory as the funnel's site (my-funnel-api v2).
|
|
217
|
+
// Resolves the funnel the same way push/verify do.
|
|
218
|
+
export async function publish(dir, flags) {
|
|
219
|
+
const config = requireConfig();
|
|
220
|
+
const orgId = requireOrg(flags, config, 'myapi funnel publish <dir> [--funnel <id>] [--env dev|prod] [--org <id>]');
|
|
221
|
+
if (!dir)
|
|
222
|
+
error('Missing directory.\nUsage: myapi funnel publish <dir> [--funnel <id>] [--env dev|prod]');
|
|
223
|
+
let dirStat;
|
|
224
|
+
try {
|
|
225
|
+
dirStat = await stat(dir);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
error(`Directory not found: ${dir}`);
|
|
229
|
+
}
|
|
230
|
+
if (!dirStat.isDirectory())
|
|
231
|
+
error(`Not a directory: ${dir}. Pass the site's root folder.`);
|
|
232
|
+
const env = flags.env;
|
|
233
|
+
if (env && env !== 'dev' && env !== 'prod')
|
|
234
|
+
error(`Invalid --env "${env}". Use "dev" or "prod" (default prod).`);
|
|
235
|
+
let funnelId = flags.funnel || config.default_funnel;
|
|
236
|
+
if (!funnelId) {
|
|
237
|
+
const existing = await sdkFunnel.listFunnels(config.api_key, orgId);
|
|
238
|
+
if (existing.length === 0)
|
|
239
|
+
error('No funnel found for this org. Create one with: myapi funnel create');
|
|
240
|
+
if (existing.length > 1) {
|
|
241
|
+
error(`Multiple funnels exist for this org and no default is set.\nPick one with --funnel <id>, or set a default:\n myapi config set-funnel <id>\n\nFunnels:\n${existing.map(f => ` ${f.id}`).join('\n')}`);
|
|
242
|
+
}
|
|
243
|
+
funnelId = existing[0].id;
|
|
244
|
+
}
|
|
245
|
+
const files = await collectFiles(dir, dir);
|
|
246
|
+
if (files.length === 0)
|
|
247
|
+
error(`No files found under ${dir}.`);
|
|
248
|
+
const result = await sdkFunnel.publishFiles(config.api_key, orgId, funnelId, files, {
|
|
249
|
+
env: env,
|
|
250
|
+
apiFunctionId: flags['api-fn'],
|
|
251
|
+
});
|
|
252
|
+
if (flags.json) {
|
|
253
|
+
printJson(result);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
success(`Published ${result.file_count} file(s) to the ${result.channel} channel`);
|
|
257
|
+
info(`Size: ${(result.size_bytes / 1024).toFixed(1)} KB`);
|
|
258
|
+
info(`SPA: ${result.spa_mode ? 'on' : 'off'}`);
|
|
259
|
+
info(`Live: ${result.published_url}`);
|
|
260
|
+
}
|
|
196
261
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
197
262
|
const SUBCOMMAND_USAGE = {
|
|
198
263
|
'list': 'myapi funnel list [--org <id>] [--json]',
|
|
@@ -200,6 +265,22 @@ const SUBCOMMAND_USAGE = {
|
|
|
200
265
|
'get': 'myapi funnel get <id> [--org <id>] [--json]',
|
|
201
266
|
'delete': 'myapi funnel delete <id> [--org <id>]',
|
|
202
267
|
'pages': 'myapi funnel pages [funnel_id] [--funnel <id>] [--org <id>] [--json]',
|
|
268
|
+
'publish': `myapi funnel publish <dir> [--funnel <id>] [--env dev|prod] [--api-fn <id>] [--org <id>]
|
|
269
|
+
|
|
270
|
+
Uploads a whole local directory as the funnel's site. Each file's path
|
|
271
|
+
within <dir> becomes its path on the site (e.g. dir/about/index.html →
|
|
272
|
+
/about/index.html). 25MB total cap.
|
|
273
|
+
|
|
274
|
+
--env dev|prod Target channel (default prod). dev publishes to
|
|
275
|
+
<name>-dev.makeautonomous.com for preview.
|
|
276
|
+
--api-fn <id> Bind /api/* on the site to a deployed function.
|
|
277
|
+
|
|
278
|
+
SPA fallback is auto-enabled when the publish has a root index.html.
|
|
279
|
+
|
|
280
|
+
Examples:
|
|
281
|
+
myapi funnel publish ./dist
|
|
282
|
+
myapi funnel publish ./dist --env dev
|
|
283
|
+
myapi funnel publish ./site --api-fn <function_id>`,
|
|
203
284
|
'push': `myapi funnel push [slug] [--funnel <id>] [--slug <path>] [--org <id>] < page.html
|
|
204
285
|
|
|
205
286
|
Reads HTML from stdin and publishes to <slug> on your funnel. Funnel is resolved
|
|
@@ -235,6 +316,7 @@ Subcommands:
|
|
|
235
316
|
create Create a funnel (optional --name; backend defaults to org's preview_subdomain)
|
|
236
317
|
get <id> Get funnel details
|
|
237
318
|
delete Delete a funnel
|
|
319
|
+
publish Publish a whole local directory as the site (dev or prod)
|
|
238
320
|
push Push a raw HTML page from stdin to a slug
|
|
239
321
|
pages List the pages currently published to a funnel
|
|
240
322
|
verify Verify a published page is reachable
|
|
@@ -255,6 +337,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
255
337
|
case 'create': return create(flags);
|
|
256
338
|
case 'get': return get(args[0], flags);
|
|
257
339
|
case 'delete': return del(args[0], flags);
|
|
340
|
+
case 'publish': return publish(args[0], flags);
|
|
258
341
|
case 'push': return push(args[0], flags);
|
|
259
342
|
case 'verify': return verify(args[0], flags);
|
|
260
343
|
case 'pages': return pages(args[0], flags);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Unit tests for the keys-command pure validators: `--grant` parsing and
|
|
2
|
+
// dollar→cents conversion. Both mirror server-side rules so a bad value
|
|
3
|
+
// fails before the network call.
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { _parseGrants, _dollarsToCents } from './keys.js';
|
|
6
|
+
import { hq } from '@myapihq/sdk';
|
|
7
|
+
describe('_parseGrants', () => {
|
|
8
|
+
describe('accepts', () => {
|
|
9
|
+
it('a bare slot — write is implied', () => {
|
|
10
|
+
expect(_parseGrants('email')).toEqual({ email: 'write' });
|
|
11
|
+
});
|
|
12
|
+
it('an explicit slot:access pair', () => {
|
|
13
|
+
expect(_parseGrants('crm:read')).toEqual({ crm: 'read' });
|
|
14
|
+
});
|
|
15
|
+
it('multiple comma-separated entries, mixed forms', () => {
|
|
16
|
+
expect(_parseGrants('email:write,crm:read,database')).toEqual({
|
|
17
|
+
email: 'write', crm: 'read', database: 'write',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
it('the "*" wildcard slot', () => {
|
|
21
|
+
expect(_parseGrants('*:read')).toEqual({ '*': 'read' });
|
|
22
|
+
expect(_parseGrants('*')).toEqual({ '*': 'write' });
|
|
23
|
+
});
|
|
24
|
+
it('every grantable slot from the SDK vocabulary', () => {
|
|
25
|
+
for (const slot of hq.GRANTABLE_SLOTS) {
|
|
26
|
+
expect(_parseGrants(slot)).toEqual({ [slot]: 'write' });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
it('tolerates surrounding whitespace', () => {
|
|
30
|
+
expect(_parseGrants(' email : write , crm '.replace(/ /g, ''))).toEqual({
|
|
31
|
+
email: 'write', crm: 'write',
|
|
32
|
+
});
|
|
33
|
+
expect(_parseGrants('email, crm')).toEqual({ email: 'write', crm: 'write' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('rejects (returns an error string)', () => {
|
|
37
|
+
it('an unknown slot', () => {
|
|
38
|
+
const r = _parseGrants('rabbitmq:write');
|
|
39
|
+
expect(typeof r).toBe('string');
|
|
40
|
+
expect(r).toMatch(/Unknown slot "rabbitmq"/);
|
|
41
|
+
});
|
|
42
|
+
it('a management slot — hq is not grantable', () => {
|
|
43
|
+
expect(typeof _parseGrants('hq:write')).toBe('string');
|
|
44
|
+
expect(_parseGrants('hq:write')).toMatch(/Unknown slot "hq"/);
|
|
45
|
+
});
|
|
46
|
+
it('an invalid access level', () => {
|
|
47
|
+
const r = _parseGrants('email:admin');
|
|
48
|
+
expect(typeof r).toBe('string');
|
|
49
|
+
expect(r).toMatch(/Invalid access "admin"/);
|
|
50
|
+
});
|
|
51
|
+
it('an empty string', () => {
|
|
52
|
+
expect(typeof _parseGrants('')).toBe('string');
|
|
53
|
+
expect(_parseGrants('')).toMatch(/--grant was empty/);
|
|
54
|
+
});
|
|
55
|
+
it('a string of only commas/whitespace', () => {
|
|
56
|
+
expect(typeof _parseGrants(' , , ')).toBe('string');
|
|
57
|
+
});
|
|
58
|
+
it('reports the FIRST bad entry when several are present', () => {
|
|
59
|
+
// crm is valid, bogus is not — error names bogus.
|
|
60
|
+
expect(_parseGrants('crm:read,bogus:write')).toMatch(/Unknown slot "bogus"/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('_dollarsToCents', () => {
|
|
65
|
+
describe('accepts', () => {
|
|
66
|
+
it.each([
|
|
67
|
+
['50', 5000],
|
|
68
|
+
['9.99', 999],
|
|
69
|
+
['0', 0],
|
|
70
|
+
['0.5', 50],
|
|
71
|
+
['100', 10000],
|
|
72
|
+
['0.01', 1],
|
|
73
|
+
])('%s → %d cents', (input, cents) => {
|
|
74
|
+
expect(_dollarsToCents(input)).toBe(cents);
|
|
75
|
+
});
|
|
76
|
+
it('rounds sub-cent input', () => {
|
|
77
|
+
expect(_dollarsToCents('9.999')).toBe(1000); // 999.9 → 1000
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('rejects (returns an error string)', () => {
|
|
81
|
+
it.each(['-5', 'abc', '', 'free', '$50', '10usd'])('rejects %s', (input) => {
|
|
82
|
+
const r = _dollarsToCents(input);
|
|
83
|
+
expect(typeof r).toBe('string');
|
|
84
|
+
expect(r).toMatch(/not a valid dollar amount/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
package/dist/commands/keys.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { hq } from '@myapihq/sdk';
|
|
1
2
|
import type { FlagSchema } from '../flags.js';
|
|
2
3
|
import type { Flags } from '../helpers.js';
|
|
3
4
|
import type { Exposes } from '../exposes.js';
|
|
4
5
|
export declare const SCHEMA: FlagSchema;
|
|
5
6
|
export declare const EXPOSES: Exposes;
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
7
|
+
export declare function _parseGrants(raw: string): hq.Grants | string;
|
|
8
|
+
export declare function _dollarsToCents(raw: string): number | string;
|
|
8
9
|
export declare function createNew(flags: Flags): Promise<void>;
|
|
9
10
|
export declare function list(flags: Flags): Promise<void>;
|
|
10
11
|
export declare function revoke(id: string, _flags: Flags): Promise<void>;
|
|
12
|
+
export declare function revokeAll(flags: Flags): Promise<void>;
|
|
13
|
+
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
14
|
+
export declare function runApiKeys(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|