@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
@@ -8,3 +8,4 @@ export declare function balance(flags: Flags): Promise<void>;
8
8
  export declare function history(flags: Flags): Promise<void>;
9
9
  export declare function topup(amountStr: string, flags: Flags): Promise<void>;
10
10
  export declare function setup(_flags: Flags): Promise<void>;
11
+ export declare function spendCap(arg: string | undefined, flags: Flags): Promise<void>;
@@ -3,12 +3,16 @@ import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
4
  import { confirm, isNonInteractive } from '../prompt.js';
5
5
  import { formatDate } from '../utils.js';
6
- export const SCHEMA = {};
6
+ export const SCHEMA = {
7
+ period: 'string', // spend-cap window: month | day
8
+ };
7
9
  export const EXPOSES = [
8
10
  'GET /hq/billing/balance',
9
11
  'GET /hq/billing/history',
10
12
  'POST /hq/billing/setup-payment',
11
13
  'POST /hq/billing/topup',
14
+ 'GET /hq/account/me',
15
+ 'PATCH /hq/account/spend-cap',
12
16
  ];
13
17
  const SUBCOMMAND_USAGE = {
14
18
  'balance': 'myapi billing balance [--json]',
@@ -20,6 +24,18 @@ Confirmation is required for amounts of $50 or more — pass --yes to skip it.
20
24
 
21
25
  Example: myapi billing topup 10`,
22
26
  'setup': 'myapi billing setup',
27
+ 'spend-cap': `myapi billing spend-cap [<amount> | clear] [--period month|day]
28
+
29
+ The account-level spend ceiling (IAM "Layer 2") — a self-imposed limit
30
+ *below* your balance that bounds total spend across every key and function.
31
+
32
+ myapi billing spend-cap Show the current cap + period spend
33
+ myapi billing spend-cap 50 Cap total spend at $50/month
34
+ myapi billing spend-cap 5 --period day Cap at $5/day
35
+ myapi billing spend-cap clear Remove the cap
36
+
37
+ This is distinct from a per-key cap (myapi keys create --spend-cap). The
38
+ account cap is the aggregate backstop; per-key caps bound each credential.`,
23
39
  };
24
40
  export async function run(subcommand, args, flags) {
25
41
  if (!subcommand || (flags.help && !subcommand)) {
@@ -29,7 +45,8 @@ Subcommands:
29
45
  balance Check balance, credits, and payment method status
30
46
  history View recent transactions and top-ups
31
47
  topup Top up your balance (whole dollars)
32
- setup Open a checkout link to add or update payment method`);
48
+ setup Open a checkout link to add or update payment method
49
+ spend-cap Set/show/clear the account-level spend ceiling (IAM Layer 2)`);
33
50
  return;
34
51
  }
35
52
  if (flags.help) {
@@ -45,6 +62,7 @@ Subcommands:
45
62
  case 'history': return history(flags);
46
63
  case 'topup': return topup(args[0], flags);
47
64
  case 'setup': return setup(flags);
65
+ case 'spend-cap': return spendCap(args[0], flags);
48
66
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi billing --help" for available subcommands.`);
49
67
  }
50
68
  }
@@ -114,3 +132,44 @@ export async function setup(_flags) {
114
132
  const result = await hq.setupPayment(config.api_key);
115
133
  success(`Open this URL in your browser to set up payment:\n${result.url}`);
116
134
  }
135
+ // The account-level spend ceiling. No arg → show; "clear" → remove; a
136
+ // dollar amount → set. Distinct from per-key caps (myapi keys create
137
+ // --spend-cap): this is the aggregate backstop across the whole account.
138
+ export async function spendCap(arg, flags) {
139
+ const config = requireConfig();
140
+ if (!arg) {
141
+ const acct = await hq.getAccount(config.api_key);
142
+ if (flags.json) {
143
+ printJson({
144
+ spend_cap_cents: acct.spend_cap_cents ?? null,
145
+ current_period_spend_cents: acct.current_period_spend_cents ?? null,
146
+ });
147
+ return;
148
+ }
149
+ if (acct.spend_cap_cents == null) {
150
+ info('No account spend cap set.');
151
+ info('Set one with: myapi billing spend-cap <dollars> [--period month|day]');
152
+ }
153
+ else {
154
+ const spent = acct.current_period_spend_cents ?? 0;
155
+ info(`Account spend cap: $${(spent / 100).toFixed(2)} spent / $${(acct.spend_cap_cents / 100).toFixed(2)} this period`);
156
+ }
157
+ return;
158
+ }
159
+ if (arg === 'clear' || arg === 'none') {
160
+ await hq.setAccountSpendCap(config.api_key, null);
161
+ success('Account spend cap cleared.');
162
+ return;
163
+ }
164
+ const dollars = Number(arg);
165
+ if (!Number.isFinite(dollars) || dollars < 0) {
166
+ error(`Invalid amount "${arg}". Use a non-negative dollar amount, or "clear" to remove the cap.\nExample: myapi billing spend-cap 50`);
167
+ }
168
+ const period = flags.period || 'month';
169
+ if (period !== 'month' && period !== 'day') {
170
+ error(`Invalid --period "${period}". Use month or day.`);
171
+ }
172
+ const res = await hq.setAccountSpendCap(config.api_key, Math.round(dollars * 100), period);
173
+ const cents = res.spend_cap_cents ?? Math.round(dollars * 100);
174
+ success(`Account spend cap set: $${(cents / 100).toFixed(2)} per ${res.spend_cap_period}.`);
175
+ }
@@ -16,6 +16,11 @@ export const EXPOSES = [
16
16
  'GET /domain/orgs/{org_id}/{domain}/status',
17
17
  'POST /domain/orgs/{org_id}/{domain}/email-infra',
18
18
  'POST /domain/orgs/{org_id}/{domain}/retry-provisioning',
19
+ 'GET /domain/orgs/{org_id}/{domain}/records',
20
+ 'POST /domain/orgs/{org_id}/{domain}/records',
21
+ 'GET /domain/orgs/{org_id}/{domain}/records/{record_id}',
22
+ 'PATCH /domain/orgs/{org_id}/{domain}/records/{record_id}',
23
+ 'DELETE /domain/orgs/{org_id}/{domain}/records/{record_id}',
19
24
  ];
20
25
  export const SCHEMA = {
21
26
  org: 'string',
@@ -50,6 +55,8 @@ export const SCHEMA = {
50
55
  yes: 'boolean',
51
56
  // Email-infra opt-in
52
57
  subdomain: 'string',
58
+ // Assign: opt out of binding the www Worker route alongside apex.
59
+ 'no-www': 'boolean',
53
60
  };
54
61
  export async function check(domainArg, flags) {
55
62
  const config = requireConfig();
@@ -159,11 +166,20 @@ function summarizeDomain(d) {
159
166
  }
160
167
  export async function assign(domainArg, flags) {
161
168
  const config = requireConfig();
162
- const orgId = requireOrg(flags, config, 'myapi domain assign <domain> [--org <id>]');
169
+ const orgId = requireOrg(flags, config, 'myapi domain assign <domain> [--no-www] [--org <id>]');
163
170
  if (!domainArg)
164
- error('Missing required arguments.\nUsage: myapi domain assign <domain> [--org <id>]');
165
- await sdkDomain.assignDomain(config.api_key, orgId, domainArg);
171
+ error('Missing required arguments.\nUsage: myapi domain assign <domain> [--no-www] [--org <id>]');
172
+ const includeWww = flags['no-www'] ? false : undefined; // omitted → backend default (true).
173
+ const res = await sdkDomain.assignDomain(config.api_key, orgId, domainArg, { includeWww });
174
+ if (flags.json) {
175
+ printJson(res);
176
+ return;
177
+ }
166
178
  success(`Assigned ${domainArg} to org ${orgId}`);
179
+ info(` Worker routes bound: ${res.routes_bound.join(' and ')}`);
180
+ if (res.include_www)
181
+ info(` www.${domainArg} 301-redirects to https://${domainArg}/ (canonical apex)`);
182
+ info(` Funnel will be live on https://${domainArg} once DNS propagates.`);
167
183
  }
168
184
  export async function unassign(domainArg, flags) {
169
185
  const config = requireConfig();
@@ -510,12 +526,20 @@ Charged against the org's billing balance — confirm with "myapi billing balanc
510
526
  before running.
511
527
 
512
528
  Track expiry afterward with: myapi domain status <domain>`,
513
- 'assign': `myapi domain assign <domain> [--org <id>]
529
+ 'assign': `myapi domain assign <domain> [--no-www] [--org <id>]
530
+
531
+ Assigns a registered or imported domain to an org. Backend binds Worker
532
+ routes for both <domain>/* and www.<domain>/* by default; www returns a 301
533
+ redirect to the apex (canonical URL). Once assigned, your funnel is served
534
+ at https://<domain> after DNS propagation completes.
514
535
 
515
- Assigns a registered domain to an org. Once assigned, your funnel is served at
516
- https://<domain> after DNS propagation completes.
536
+ --no-www Skip the www route binding. The www form will not resolve at the
537
+ edge unless you create your own DNS + Worker setup for it.
517
538
 
518
- To transfer to a different org: unassign first, then assign to the new org.`,
539
+ WARNING: This is also the reassign path. If you previously assigned <domain>
540
+ to a different org, re-running assign moves it. Pre-flight with:
541
+ myapi domain list --filter all
542
+ to confirm the current binding before reassigning.`,
519
543
  'unassign': 'myapi domain unassign <domain> [--org <id>]',
520
544
  'status': `myapi domain status <domain> [--org <id>] [--watch] [--json]
521
545
 
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ // Unit tests for fn.ts CLI's pure name validator. Mirrors the regex +
2
+ // reserved set in myapi-hq/internal/routes/function/crud.go so client-side
3
+ // rejection matches server-side rejection.
4
+ import { describe, it, expect } from 'vitest';
5
+ import { _validateName, NAME_RE, RESERVED_NAMES } from './fn.js';
6
+ describe('_validateName — pure helper', () => {
7
+ describe('valid names', () => {
8
+ it.each([
9
+ 'a', // single char ok
10
+ 'a1', // letters + digits
11
+ 'my-app', // hyphen ok
12
+ 'my-app-api',
13
+ '0', // digit-leading
14
+ '0app',
15
+ 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij', // 50 chars (max)
16
+ 'fn-2026-05-15',
17
+ ])('accepts %s', (name) => {
18
+ expect(_validateName(name)).toBeNull();
19
+ });
20
+ });
21
+ describe('regex rejections', () => {
22
+ it.each([
23
+ ['empty string', ''],
24
+ ['UPPERCASE', 'BAD'],
25
+ ['mixed case', 'badName'],
26
+ ['underscore', 'my_app'],
27
+ ['leading hyphen', '-app'],
28
+ ['trailing hyphen ok but disallowed length too long', 'a'.repeat(51)],
29
+ ['spaces', 'my app'],
30
+ ['period', 'my.app'],
31
+ ['unicode', 'mōji'],
32
+ ['slash', 'a/b'],
33
+ ])('rejects %s (%s)', (_label, name) => {
34
+ expect(_validateName(name)).toMatch(/Invalid --name/);
35
+ });
36
+ });
37
+ describe('reserved names', () => {
38
+ it.each([...RESERVED_NAMES])('rejects reserved name %s', (name) => {
39
+ const err = _validateName(name);
40
+ expect(err).toMatch(/is reserved/);
41
+ });
42
+ it('regex check runs before reserved check (a value matching neither passes)', () => {
43
+ // 'ADMIN' fails the regex (uppercase) — not the reserved check.
44
+ expect(_validateName('ADMIN')).toMatch(/Invalid --name/);
45
+ });
46
+ });
47
+ describe('NAME_RE alignment with backend', () => {
48
+ it('matches the backend regex exactly: ^[a-z0-9][a-z0-9-]{0,49}$', () => {
49
+ // String equality on the source so any future drift surfaces here.
50
+ expect(NAME_RE.source).toBe('^[a-z0-9][a-z0-9-]{0,49}$');
51
+ });
52
+ });
53
+ describe('RESERVED_NAMES alignment with backend', () => {
54
+ it('matches the backend reservedNames map', () => {
55
+ // Sorted for stable comparison against the backend's list in
56
+ // myapi-hq/internal/routes/function/crud.go:21-28.
57
+ expect([...RESERVED_NAMES].sort()).toEqual(['admin', 'api', 'default', 'system', 'www']);
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,16 @@
1
+ import type { FlagSchema } from '../flags.js';
2
+ import { type Flags } from '../helpers.js';
3
+ import type { Exposes } from '../exposes.js';
4
+ export declare const EXPOSES: Exposes;
5
+ export declare const SCHEMA: FlagSchema;
6
+ export declare const NAME_RE: RegExp;
7
+ export declare const RESERVED_NAMES: Set<string>;
8
+ export declare function _validateName(name: string): string | null;
9
+ export declare function create(flags: Flags): Promise<void>;
10
+ export declare function list(flags: Flags): Promise<void>;
11
+ export declare function get(id: string, flags: Flags): Promise<void>;
12
+ export declare function del(id: string, flags: Flags): Promise<void>;
13
+ export declare function deploy(id: string, bundlePath: string, flags: Flags): Promise<void>;
14
+ export declare function setEnv(id: string, name: string, value: string, flags: Flags): Promise<void>;
15
+ export declare function runs(id: string, flags: Flags): Promise<void>;
16
+ export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -0,0 +1,271 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { fn as sdkFn } from '@myapihq/sdk';
3
+ import { requireConfig } from '../config.js';
4
+ import { success, error, printTable, info, printJson, banner } from '../output.js';
5
+ import { formatDate } from '../utils.js';
6
+ import { requireOrg } from '../helpers.js';
7
+ export const EXPOSES = [
8
+ 'POST /function/orgs/{org_id}/functions',
9
+ 'GET /function/orgs/{org_id}/functions',
10
+ 'GET /function/orgs/{org_id}/functions/{id}',
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',
15
+ ];
16
+ export const SCHEMA = {
17
+ cron: 'string',
18
+ };
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).
21
+ // Mirrors validateName in myapi-hq/internal/routes/function/crud.go. We
22
+ // pre-validate client-side so typos fail before the network call; backend
23
+ // runs the same regex as defence in depth.
24
+ //
25
+ // Exported (alongside _validateNameOrThrow) so the unit test suite can hit
26
+ // the regex and reserved list directly without spawning a subprocess.
27
+ export const NAME_RE = /^[a-z0-9][a-z0-9-]{0,49}$/;
28
+ export const RESERVED_NAMES = new Set(['www', 'api', 'admin', 'system', 'default']);
29
+ // Returns an error message string on failure, or null on success. Pure —
30
+ // no I/O, no process.exit. The caller in this file wraps it in `error()`
31
+ // (which terminates); tests use the pure form.
32
+ export function _validateName(name) {
33
+ if (!NAME_RE.test(name)) {
34
+ return `Invalid --name "${name}". Lowercase letters, digits, hyphens; 1-50 chars; starts with a letter or digit.`;
35
+ }
36
+ if (RESERVED_NAMES.has(name)) {
37
+ return `Name "${name}" is reserved. Pick a different one.`;
38
+ }
39
+ return null;
40
+ }
41
+ function validateName(name) {
42
+ const err = _validateName(name);
43
+ if (err)
44
+ error(err);
45
+ }
46
+ function summarizeFn(f) {
47
+ return {
48
+ id: f.id,
49
+ name: f.name,
50
+ trigger: f.trigger_type === 'cron' ? `cron ${f.cron_schedule ?? '?'}` : 'http',
51
+ url: f.invocation_url || '(not deployed)',
52
+ updated_at: f.updated_at,
53
+ };
54
+ }
55
+ export async function create(flags) {
56
+ const config = requireConfig();
57
+ const orgId = requireOrg(flags, config, 'myapi fn create --name <name> [--cron <expr>] [--org <id>]');
58
+ const name = flags.name;
59
+ if (!name) {
60
+ error('Missing --name.\nUsage: myapi fn create --name <name> [--cron <expr>] [--org <id>]\n\n→ Name is a kebab-case slug, 1-50 chars (e.g. "my-app-api").');
61
+ }
62
+ validateName(name);
63
+ const cron = flags.cron;
64
+ const payload = {
65
+ name,
66
+ trigger_type: cron ? 'cron' : 'http',
67
+ };
68
+ if (cron)
69
+ payload.cron_schedule = cron;
70
+ const result = await sdkFn.createFunction(config.api_key, orgId, payload);
71
+ success(`Function created: ${result.function.id}`);
72
+ info(`Name: ${result.function.name}`);
73
+ info(`Trigger: ${result.function.trigger_type}${result.function.cron_schedule ? ` (${result.function.cron_schedule})` : ''}`);
74
+ // The scoped key is returned ONCE — surface it prominently. It's used by
75
+ // the function runtime shim to call other slot endpoints without a
76
+ // baked-in auth token. Deploy rotates this key.
77
+ info('');
78
+ info(`Scoped API key (returned once — save it if you need it):`);
79
+ info(` ${result.scoped_api_key}`);
80
+ info(` (id: ${result.scoped_api_key_id}; scopes: slot_call; rejected at /hq/*, /admin/*, /internal/*)`);
81
+ if (!result.function.invocation_url) {
82
+ info('');
83
+ banner(`Next: deploy code with myapi fn deploy ${result.function.id} <bundle.js>`);
84
+ }
85
+ }
86
+ export async function list(flags) {
87
+ const config = requireConfig();
88
+ const orgId = requireOrg(flags, config, 'myapi fn list [--org <id>]');
89
+ const fns = await sdkFn.listFunctions(config.api_key, orgId);
90
+ if (flags.json) {
91
+ printJson(fns);
92
+ return;
93
+ }
94
+ printTable(fns.map(summarizeFn), {
95
+ flags,
96
+ empty: 'No functions yet. Create one with: myapi fn create --name <name>',
97
+ });
98
+ }
99
+ export async function get(id, flags) {
100
+ const config = requireConfig();
101
+ const orgId = requireOrg(flags, config, 'myapi fn get <id> [--org <id>]');
102
+ if (!id)
103
+ error('Missing id.\nUsage: myapi fn get <id>');
104
+ const fn = await sdkFn.getFunction(config.api_key, orgId, id);
105
+ if (flags.json) {
106
+ printJson(fn);
107
+ return;
108
+ }
109
+ info(`ID: ${fn.id}`);
110
+ info(`Name: ${fn.name}`);
111
+ info(`Trigger: ${fn.trigger_type}${fn.cron_schedule ? ` (${fn.cron_schedule})` : ''}`);
112
+ info(`Invocation URL: ${fn.invocation_url || '(not deployed)'}`);
113
+ info(`Created: ${fn.created_at}`);
114
+ info(`Updated: ${fn.updated_at}`);
115
+ }
116
+ export async function del(id, flags) {
117
+ const config = requireConfig();
118
+ const orgId = requireOrg(flags, config, 'myapi fn delete <id> [--org <id>]');
119
+ if (!id)
120
+ error('Missing id.\nUsage: myapi fn delete <id>');
121
+ await sdkFn.deleteFunction(config.api_key, orgId, id);
122
+ success(`Deleted function ${id}`);
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
+ }
192
+ // ── Dispatcher ───────────────────────────────────────────────────────────────
193
+ const SUBCOMMAND_USAGE = {
194
+ 'create': `myapi fn create --name <name> [--cron <expr>] [--org <id>]
195
+
196
+ Persists a function record + issues a scoped API key. Deploy code separately
197
+ with "myapi fn deploy".
198
+
199
+ Triggers:
200
+ (default) HTTP — function gets a public invocation URL once deployed.
201
+ --cron <expr> Cron — function runs on the schedule (e.g. "0 8 * * *").
202
+
203
+ Examples:
204
+ myapi fn create --name my-app-api
205
+ myapi fn create --name daily-report --cron "0 8 * * *"
206
+
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).`,
231
+ 'list': 'myapi fn list [--org <id>] [--json]',
232
+ 'get': 'myapi fn get <id> [--org <id>] [--json]',
233
+ 'delete': 'myapi fn delete <id> [--org <id>]',
234
+ };
235
+ export async function run(subcommand, args, flags) {
236
+ if (!subcommand || (flags.help && !subcommand)) {
237
+ info(`Usage: myapi fn <subcommand>
238
+
239
+ Create, deploy, and manage functions on the MyAPI edge runtime.
240
+
241
+ Subcommands:
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
246
+ list List functions in your org
247
+ get <id> Inspect a function
248
+ delete <id> Soft-delete and revoke its scoped API key
249
+
250
+ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
251
+ return;
252
+ }
253
+ if (flags.help) {
254
+ const usage = SUBCOMMAND_USAGE[subcommand];
255
+ if (usage)
256
+ info(`Usage: ${usage}`);
257
+ else
258
+ info(`Unknown subcommand: ${subcommand}. Run "myapi fn --help" for the list.`);
259
+ return;
260
+ }
261
+ switch (subcommand) {
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);
266
+ case 'list': return list(flags);
267
+ case 'get': return get(args[0], flags);
268
+ case 'delete': return del(args[0], flags);
269
+ default: error(`Unknown subcommand: ${subcommand}. Run "myapi fn --help" for a list of valid subcommands.`);
270
+ }
271
+ }
@@ -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>;