@myapihq/cli 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,10 +14,16 @@ export const EXPOSES = [
14
14
  'POST /feedback/orgs/{org_id}/items',
15
15
  'POST /feedback/orgs/{org_id}/items/{id}/resolve',
16
16
  'DELETE /feedback/orgs/{org_id}/items/{id}',
17
+ 'GET /feedback/orgs/{org_id}/widgets',
18
+ 'PATCH /feedback/orgs/{org_id}/widgets/{id}',
17
19
  'POST /feedback/orgs/{org_id}/widgets',
18
20
  'DELETE /feedback/orgs/{org_id}/widgets/{id}',
19
21
  ];
20
22
  export const SCHEMA = {
23
+ routes: 'string',
24
+ accent: 'string',
25
+ position: 'string',
26
+ label: 'string',
21
27
  kind: 'string',
22
28
  status: 'string',
23
29
  body: 'string',
@@ -130,6 +136,58 @@ export async function widget(sub, arg, flags) {
130
136
  }
131
137
  return;
132
138
  }
139
+ if (sub === 'list') {
140
+ const ws = await sdkFeedback.listWidgets(config.api_key, orgId);
141
+ if (flags.json) {
142
+ printJson(ws);
143
+ return;
144
+ }
145
+ if (!ws.length) {
146
+ info('No widgets yet. Mint one: myapi feedback widget create <name>');
147
+ return;
148
+ }
149
+ for (const w of ws) {
150
+ info(`${w.id} ${w.name || '(unnamed)'}`);
151
+ // The platform builds the tag; printing it beats making anyone assemble
152
+ // the URL from a key and a base by hand.
153
+ info(` ${w.script_tag || `<script src="https://api.myapihq.com/feedback/in/${w.key}/widget.js" async></script>`}`);
154
+ info(` routes: ${w.routes?.length ? w.routes.join(', ') : '(everywhere)'}`);
155
+ info(` origins: ${w.allowed_origins?.length ? w.allowed_origins.join(', ') : '(any — anyone can post with this key)'}`);
156
+ info('');
157
+ }
158
+ return;
159
+ }
160
+ if (sub === 'update') {
161
+ requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"]');
162
+ const patch = {};
163
+ const csv = (v) => typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined;
164
+ if (flags.routes !== undefined)
165
+ patch.routes = csv(flags.routes);
166
+ if (flags.origins !== undefined)
167
+ patch.allowed_origins = csv(flags.origins);
168
+ if (typeof flags.name === 'string')
169
+ patch.name = flags.name;
170
+ const theme = {};
171
+ for (const k of ['accent', 'position', 'label']) {
172
+ if (typeof flags[k] === 'string')
173
+ theme[k] = flags[k];
174
+ }
175
+ if (Object.keys(theme).length)
176
+ patch.theme = theme;
177
+ if (Object.keys(patch).length === 0) {
178
+ error('Nothing to change. Pass at least one of --routes, --origins, --name, --accent, --position, --label.');
179
+ }
180
+ const w = await sdkFeedback.updateWidget(config.api_key, orgId, arg, patch);
181
+ if (flags.json) {
182
+ printJson(w);
183
+ return;
184
+ }
185
+ success(`Widget ${arg} updated`);
186
+ // The key is unchanged, which is the reason to use this instead of
187
+ // revoke + create — the customer's page does not need editing.
188
+ info('Same key, so your page needs no change. Route changes reach browsers within ~5 minutes.');
189
+ return;
190
+ }
133
191
  if (sub === 'revoke') {
134
192
  requireArg(arg, 'id', 'myapi feedback widget revoke <id>');
135
193
  await confirmDestructive(flags, `revoke widget ${arg} (submissions with it stop immediately)`, 'myapi feedback widget revoke <id> [--yes] [--org <id>]');
@@ -138,7 +196,7 @@ export async function widget(sub, arg, flags) {
138
196
  info('Feedback already collected through it is kept.');
139
197
  return;
140
198
  }
141
- error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget revoke <id>');
199
+ error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget list\n myapi feedback widget update <id> [--routes <list>] [--origins <list>]\n myapi feedback widget revoke <id>');
142
200
  }
143
201
  const SUBCOMMAND_USAGE = {
144
202
  'delete': `myapi feedback delete <id> [--yes] [--org <id>]
@@ -154,7 +212,8 @@ Newest first. \`total\` is the number of matches, not the page size.`,
154
212
 
155
213
  --kind is what the person reporting says it is, not what the text sounds like.`,
156
214
  'resolve': 'myapi feedback resolve <id> [--org <id>]',
157
- 'widget': `myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
215
+ 'widget': `myapi feedback widget list
216
+ myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
158
217
  myapi feedback widget revoke <id> [--yes] [--org <id>]
159
218
 
160
219
  The key a widget mints is PUBLIC — it ships in page source and authenticates
@@ -173,7 +232,9 @@ Subcommands:
173
232
  list List feedback, newest first (--kind, --status, --limit, --offset)
174
233
  resolve <id> Close a piece of feedback
175
234
  widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
235
+ widget list List widgets with the script tag to paste
176
236
  widget revoke <id> Revoke a widget key; collected feedback is kept
237
+ widget update <id> Change routes/origins/theme, keeping the same key
177
238
 
178
239
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
179
240
  return;
@@ -12,6 +12,7 @@ export declare function get(id: string, flags: Flags): Promise<void>;
12
12
  export declare function del(id: string, flags: Flags): Promise<void>;
13
13
  export declare function deploy(id: string, bundlePath: string, flags: Flags): Promise<void>;
14
14
  export declare function _parseSetPairs(raw: string | string[]): Record<string, string> | string;
15
+ export declare function scopes(id: string | undefined, flags: Flags): Promise<void>;
15
16
  export declare function setEnv(id: string, name: string, value: string, flags: Flags): Promise<void>;
16
17
  export declare function runs(id: string, flags: Flags): Promise<void>;
17
18
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -3,7 +3,7 @@ import { fn as sdkFn } from '@myapihq/sdk';
3
3
  import { requireConfig } from '../config.js';
4
4
  import { success, error, printTable, info, printJson, banner, spinnerFrame, spinnerWrite, clearLine } from '../output.js';
5
5
  import { formatDate } from '../utils.js';
6
- import { requireOrg, confirmDestructive } from '../helpers.js';
6
+ import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
7
7
  export const EXPOSES = [
8
8
  'POST /function/orgs/{org_id}/functions',
9
9
  'GET /function/orgs/{org_id}/functions',
@@ -12,6 +12,7 @@ export const EXPOSES = [
12
12
  'POST /function/orgs/{org_id}/functions/{id}/bundle',
13
13
  'POST /function/orgs/{org_id}/functions/{id}/env',
14
14
  'GET /function/orgs/{org_id}/functions/{id}/runs',
15
+ 'PATCH /function/orgs/{org_id}/functions/{id}/scopes',
15
16
  ];
16
17
  export const SCHEMA = {
17
18
  // `create` takes a positional name; --name stays accepted for
@@ -231,6 +232,42 @@ export function _parseSetPairs(raw) {
231
232
  }
232
233
  return env;
233
234
  }
235
+ // scopes changes which slots a deployed function may call, in place.
236
+ //
237
+ // This closes the trap the skill used to have to warn about: --scope was
238
+ // create-only, so adding a slot meant delete + recreate, which minted a NEW
239
+ // function id and invocation URL and broke every reference already handed out.
240
+ // One customer hit that twice in a day and was left over-provisioning scopes
241
+ // up front — the opposite of the least privilege the flag exists to encourage.
242
+ export async function scopes(id, flags) {
243
+ const usage = "myapi fn scopes <id> --set <slot,slot> [--org <id>]";
244
+ const config = requireConfig();
245
+ const orgId = requireOrg(flags, config, usage);
246
+ requireArg(id, 'id', usage);
247
+ const raw = typeof flags.set === 'string' ? flags.set : '';
248
+ const list = raw.split(',').map(s => s.trim()).filter(Boolean);
249
+ if (list.length === 0) {
250
+ error(`Missing --set.\nUsage: ${usage}\n\n` +
251
+ `→ --set REPLACES the scope list; name every slot the function should reach.\n` +
252
+ ` See the current list with: myapi fn get ${id ?? '<id>'}`);
253
+ }
254
+ const res = await sdkFn.setFunctionScopes(config.api_key, orgId, id, list);
255
+ if (flags.json) {
256
+ printJson(res);
257
+ return;
258
+ }
259
+ success(`Scopes for ${id}: ${list.join(', ')}`);
260
+ info('The function id and invocation URL are unchanged — references already handed out keep working.');
261
+ // Same rotation as a deploy, and for the same reason: a narrowing that left
262
+ // the old key alive would narrow nothing. Say so, because a caller holding a
263
+ // manual copy of the key needs to know it just went stale.
264
+ info('');
265
+ info('The scoped key was replaced and pushed to the running function.');
266
+ if (res.scoped_api_key)
267
+ info(` New value (returned once): ${res.scoped_api_key}`);
268
+ else
269
+ info(' env.__MYAPI_KEY inside the function is already current; a manual copy is now stale.');
270
+ }
234
271
  // env sets encrypted secret(s) (Stripe key, etc.) on a deployed function.
235
272
  // Single form: myapi fn env <id> <name> <value>
236
273
  // Bulk form: myapi fn env <id> --set K=V[,K2=V2 ...]
@@ -325,6 +362,14 @@ in MyAPI or echoed back. The function must already be deployed.
325
362
 
326
363
  Single: myapi fn env <id> STRIPE_KEY sk_live_...
327
364
  Bulk: myapi fn env <id> --set STRIPE_KEY=sk_live_...,WEBHOOK_SECRET=whsec_...`,
365
+ 'scopes': `myapi fn scopes <id> --set <slot,slot> [--org <id>]
366
+
367
+ Change which slots a deployed function may call, in place. The function id and
368
+ invocation URL are unchanged, so references already handed out keep working.
369
+
370
+ --set REPLACES the list: name every slot the function should reach, not just
371
+ the new one. The scoped key is replaced and pushed to the running function, the
372
+ same as a deploy does — env.__MYAPI_KEY stays current, a manual copy goes stale.`,
328
373
  'runs': `myapi fn runs <id> [--org <id>] [--json]
329
374
 
330
375
  Lists recent invocation records (most recent first, up to 100).`,
@@ -346,6 +391,7 @@ Subcommands:
346
391
  get <id> Inspect a function
347
392
  list List functions in your org
348
393
  runs <id> List recent invocation records
394
+ scopes <id> Change which slots the function may call (--set a,b)
349
395
 
350
396
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
351
397
  return;
@@ -362,6 +408,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
362
408
  case 'create': return create(args[0], flags);
363
409
  case 'deploy': return deploy(args[0], args[1], flags);
364
410
  case 'env': return setEnv(args[0], args[1], args[2], flags);
411
+ case 'scopes': return scopes(args[0], flags);
365
412
  case 'runs': return runs(args[0], flags);
366
413
  case 'list': return list(flags);
367
414
  case 'get': return get(args[0], flags);
@@ -213,7 +213,7 @@ describe('fn.listFunctionRuns', () => {
213
213
  });
214
214
  });
215
215
  describe('fn.EXPOSES', () => {
216
- it('matches the Story 1 + Story 2/4/5 contract — exactly 7 endpoints', () => {
216
+ it('matches the Story 1 + Story 2/4/5 contract, plus in-place scopes — exactly 8 endpoints', () => {
217
217
  expect(fn.EXPOSES).toEqual([
218
218
  'POST /function/orgs/{org_id}/functions',
219
219
  'GET /function/orgs/{org_id}/functions',
@@ -222,6 +222,10 @@ describe('fn.EXPOSES', () => {
222
222
  'POST /function/orgs/{org_id}/functions/{id}/bundle',
223
223
  'POST /function/orgs/{org_id}/functions/{id}/env',
224
224
  'GET /function/orgs/{org_id}/functions/{id}/runs',
225
+ // Added 2026-08-13. Scopes became changeable in place, which is what
226
+ // ends the delete-and-recreate trap that cost a customer two invocation
227
+ // URLs in one day.
228
+ 'PATCH /function/orgs/{org_id}/functions/{id}/scopes',
225
229
  ]);
226
230
  });
227
231
  it('exposes deploy/env/runs but not /logs (still pending)', () => {
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-feedback-api
3
- version: 1.2.0
3
+ version: 1.3.0
4
4
  description: >
5
5
  Collect feedback from the people using what you built. A public widget key lets a page submit without a credential; you list, filter and resolve the results. Kind is chosen by the person reporting, not inferred from their wording.
6
6
  triggers: [feedback, bug report, user feedback, feature request, widget, support, complaints, praise]
7
- checksum: sha256-199fba185e85ccc91a6858bce4a60e20167a6591dee28cbc475606c72cb8d787
7
+ checksum: sha256-de137a7db55ba3173aecdd489b4568e66166a0d6c145f5a9fa165c5ef252bc01
8
8
  ---
9
9
 
10
10
  # MyFeedbackAPI
@@ -43,7 +43,8 @@ theme baked in. This is the intended way in — you do not build a UI:
43
43
  ```
44
44
 
45
45
  It renders a button, shows itself only on the routes the widget is configured
46
- for, and lets the person point at an element or drag a region. It sends `kind`,
46
+ for (set them with `widget update <id> --routes /app,/app/**`; changes reach
47
+ browsers within ~5 minutes with no redeploy of your site), and lets the person point at an element or drag a region. It sends `kind`,
47
48
  `body`, `page_url`, `route`, `viewport`, plus **either** `target_selector` (they
48
49
  clicked something) **or** `target_region` `{x,y,w,h}` (they dragged a box).
49
50
 
@@ -104,6 +105,8 @@ that is deliberate, so ids cannot be probed across orgs.
104
105
  | `myapi feedback resolve <id>` | Close an item, keeping it |
105
106
  | `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
106
107
  | `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
108
+ | `myapi feedback widget list` | List widgets, each with the script tag to paste |
109
+ | `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`, or the look (`--accent`, `--position`, `--label`), keeping the same key |
107
110
  | `myapi feedback widget revoke <id>` | Revoke a key; collected feedback is kept |
108
111
  <!-- generated:end -->
109
112
 
@@ -114,7 +117,13 @@ that is deliberate, so ids cannot be probed across orgs.
114
117
  myapi feedback widget create marketing --origins example.com,www.example.com
115
118
  # → prints a PUBLIC key to embed in the page
116
119
 
117
- # 2. Read what came back, newest first
120
+ # 2. See your widgets and the exact tag to paste
121
+ myapi feedback widget list
122
+
123
+ # 3. Change where it appears — same key, so your page needs no edit
124
+ myapi feedback widget update <id> --routes /app,/app/**
125
+
126
+ # 4. Read what came back, newest first
118
127
  myapi feedback list --status open
119
128
  myapi feedback list --kind bug --limit 20
120
129
 
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-function-api
3
- version: 1.0.0
3
+ version: 1.1.0
4
4
  description: >
5
5
  Deploy JavaScript functions to the MyAPI edge runtime. Register a function, upload a single-file JS bundle, get a live HTTP invocation URL or run it on a cron schedule. Each function gets a scoped capability key for cross-slot calls.
6
6
  triggers: [function, deploy function, edge function, serverless, cloudflare worker, cron, scoped api key, capability key, invocation url, bundle]
7
- checksum: sha256-0ab1d08fb6c9cc917fd3d1aa5d730ff3e4fe5dd37e8e30ebe225085593139cca
7
+ checksum: sha256-3702cfd630b5ff8a892704c4f6aee9f6678881c73fd3522ce4bd52be865e639e
8
8
  ---
9
9
 
10
10
  # MyFunctionAPI
@@ -40,6 +40,7 @@ Name rules (validated client- and server-side, kept identical):
40
40
  | `myapi fn runs <id>` | List recent invocation records (status, duration, errors) |
41
41
  | `myapi fn list` | List functions in your org |
42
42
  | `myapi fn get <id>` | Inspect a function (name, trigger, invocation URL) |
43
+ | `myapi fn scopes <id> --set <slot,slot>` | Change which slots the function may call, in place. Replaces the list; rotates the scoped key |
43
44
  | `myapi fn delete <id>` | Soft-delete the record + revoke the scoped API key |
44
45
  <!-- generated:end -->
45
46
 
@@ -70,6 +71,9 @@ myapi fn create --name daily-report --cron "0 8 * * *"
70
71
  # Register with a narrowed key — the function can only call email + storage
71
72
  myapi fn create --name mailer --scope email,storage
72
73
 
74
+ # Need another slot later? Change it in place — same id, same URL.
75
+ myapi fn scopes fn_abc123 --set email,storage,crm
76
+
73
77
  # List + inspect
74
78
  myapi fn list
75
79
  myapi fn get fn_abc123
@@ -129,11 +133,18 @@ curl -H "Authorization: Bearer $SCOPED_KEY" \
129
133
  - Deploy rotates the scoped API key on every call — re-capture the printed value if other systems use it.
130
134
  - `myapi fn env <id> --set KEY=VALUE,OTHER=VALUE` sets several secrets in one call instead of one command each.
131
135
 
132
- **`--scope` is create-only, so treat it as permanent.** There is no
133
- `fn scope --add`. Adding a slot later means delete + recreate, which mints a
134
- **new function id and a new invocation URL**, breaking every reference already
135
- handed out docs, front-end config, webhooks, anything given to a third
136
- party. Decide the full slot list before you publish the URL.
136
+ **Scopes are changeable in place `myapi fn scopes <id> --set <slot,slot>`.**
137
+ The function id and invocation URL are unchanged, so references already handed
138
+ out keep working. This used to be create-only, and adding a slot meant delete +
139
+ recreate with a new id and URL; that is no longer the trade-off.
140
+
141
+ Two things to know:
142
+
143
+ - **`--set` REPLACES the list**, it does not add to it. Name every slot the
144
+ function should reach. `myapi fn get <id>` shows the current set.
145
+ - **It rotates the scoped key**, exactly as a deploy does — a narrowing that
146
+ left the old key alive would narrow nothing. `env.__MYAPI_KEY` inside the
147
+ function is updated for you; a copy you saved elsewhere goes stale.
137
148
 
138
149
  ## HTTP (from deployed code)
139
150
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.12.0",
4
+ "version": "2.13.0",
5
5
  "description": "MyAPI command-line interface",
6
6
  "repository": {
7
7
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
47
47
  },
48
48
  "dependencies": {
49
- "@myapihq/sdk": "^2.12.0"
49
+ "@myapihq/sdk": "^2.13.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",