@myapihq/cli 2.12.0 → 2.14.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',
@@ -50,14 +56,24 @@ export async function list(flags) {
50
56
  // that explicitly, having been bitten by the opposite in CRM search.
51
57
  const shown = page.items?.length ?? 0;
52
58
  info(shown === page.total ? `${shown} item${shown === 1 ? '' : 's'}` : `${shown} of ${page.total} items`);
59
+ // The table cannot carry a signed URL, a selector and a navigation trace, so
60
+ // it flags what exists and --json carries it. Saying nothing about a
61
+ // screenshot would leave a picture nobody knows to look for.
53
62
  printTable((page.items ?? []).map(i => ({
54
63
  id: i.id,
55
64
  kind: i.kind,
56
65
  status: i.status,
57
66
  body: i.body.length > 60 ? `${i.body.slice(0, 57)}…` : i.body,
58
67
  route: i.route ?? i.page_url ?? '',
68
+ // A failed signature still means a picture is there — distinguish that
69
+ // from having none, rather than showing both as blank.
70
+ shot: i.screenshot_url ? 'yes' : (i.screenshot_asset_id ? 'unsigned' : ''),
59
71
  created: i.created_at ? formatDate(i.created_at) : '',
60
72
  })), { flags, empty: 'No feedback yet. Put a widget on a page: myapi feedback widget create <name>' });
73
+ const withShots = (page.items ?? []).filter(i => i.screenshot_url || i.screenshot_asset_id).length;
74
+ if (withShots > 0 && !flags.json) {
75
+ info(`${withShots} item${withShots === 1 ? ' has' : 's have'} a screenshot — \`--json\` carries the link (signed, ~1h).`);
76
+ }
61
77
  if (page.has_more)
62
78
  info('More available — raise --limit or pass --offset.');
63
79
  }
@@ -130,6 +146,58 @@ export async function widget(sub, arg, flags) {
130
146
  }
131
147
  return;
132
148
  }
149
+ if (sub === 'list') {
150
+ const ws = await sdkFeedback.listWidgets(config.api_key, orgId);
151
+ if (flags.json) {
152
+ printJson(ws);
153
+ return;
154
+ }
155
+ if (!ws.length) {
156
+ info('No widgets yet. Mint one: myapi feedback widget create <name>');
157
+ return;
158
+ }
159
+ for (const w of ws) {
160
+ info(`${w.id} ${w.name || '(unnamed)'}`);
161
+ // The platform builds the tag; printing it beats making anyone assemble
162
+ // the URL from a key and a base by hand.
163
+ info(` ${w.script_tag || `<script src="https://api.myapihq.com/feedback/in/${w.key}/widget.js" async></script>`}`);
164
+ info(` routes: ${w.routes?.length ? w.routes.join(', ') : '(everywhere)'}`);
165
+ info(` origins: ${w.allowed_origins?.length ? w.allowed_origins.join(', ') : '(any — anyone can post with this key)'}`);
166
+ info('');
167
+ }
168
+ return;
169
+ }
170
+ if (sub === 'update') {
171
+ requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"]');
172
+ const patch = {};
173
+ const csv = (v) => typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined;
174
+ if (flags.routes !== undefined)
175
+ patch.routes = csv(flags.routes);
176
+ if (flags.origins !== undefined)
177
+ patch.allowed_origins = csv(flags.origins);
178
+ if (typeof flags.name === 'string')
179
+ patch.name = flags.name;
180
+ const theme = {};
181
+ for (const k of ['accent', 'position', 'label']) {
182
+ if (typeof flags[k] === 'string')
183
+ theme[k] = flags[k];
184
+ }
185
+ if (Object.keys(theme).length)
186
+ patch.theme = theme;
187
+ if (Object.keys(patch).length === 0) {
188
+ error('Nothing to change. Pass at least one of --routes, --origins, --name, --accent, --position, --label.');
189
+ }
190
+ const w = await sdkFeedback.updateWidget(config.api_key, orgId, arg, patch);
191
+ if (flags.json) {
192
+ printJson(w);
193
+ return;
194
+ }
195
+ success(`Widget ${arg} updated`);
196
+ // The key is unchanged, which is the reason to use this instead of
197
+ // revoke + create — the customer's page does not need editing.
198
+ info('Same key, so your page needs no change. Route changes reach browsers within ~5 minutes.');
199
+ return;
200
+ }
133
201
  if (sub === 'revoke') {
134
202
  requireArg(arg, 'id', 'myapi feedback widget revoke <id>');
135
203
  await confirmDestructive(flags, `revoke widget ${arg} (submissions with it stop immediately)`, 'myapi feedback widget revoke <id> [--yes] [--org <id>]');
@@ -138,7 +206,7 @@ export async function widget(sub, arg, flags) {
138
206
  info('Feedback already collected through it is kept.');
139
207
  return;
140
208
  }
141
- error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget revoke <id>');
209
+ 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
210
  }
143
211
  const SUBCOMMAND_USAGE = {
144
212
  'delete': `myapi feedback delete <id> [--yes] [--org <id>]
@@ -154,7 +222,8 @@ Newest first. \`total\` is the number of matches, not the page size.`,
154
222
 
155
223
  --kind is what the person reporting says it is, not what the text sounds like.`,
156
224
  'resolve': 'myapi feedback resolve <id> [--org <id>]',
157
- 'widget': `myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
225
+ 'widget': `myapi feedback widget list
226
+ myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
158
227
  myapi feedback widget revoke <id> [--yes] [--org <id>]
159
228
 
160
229
  The key a widget mints is PUBLIC — it ships in page source and authenticates
@@ -173,7 +242,9 @@ Subcommands:
173
242
  list List feedback, newest first (--kind, --status, --limit, --offset)
174
243
  resolve <id> Close a piece of feedback
175
244
  widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
245
+ widget list List widgets with the script tag to paste
176
246
  widget revoke <id> Revoke a widget key; collected feedback is kept
247
+ widget update <id> Change routes/origins/theme, keeping the same key
177
248
 
178
249
  All commands accept --org <id> (or set default: myapi config set-org <id>).`);
179
250
  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.4.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-b8a6dbed6fa4aa393e538225ae611f5f5d8c4fbb1ffea7922e4a0286d984eca4
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
 
@@ -51,9 +52,15 @@ A wrong key does not break the page: the response is `application/javascript`
51
52
  carrying a JS comment that names the fix, so a typo cannot throw a syntax error
52
53
  in your app.
53
54
 
54
- **No screenshots.** The submit body takes no image and there is no multipart
55
- form `target_selector` / `target_region` are the platform's answer to "where
56
- were they pointing". Do not build a capture step expecting somewhere to put it.
55
+ **Screenshots are automatic you do not build a capture step.** When someone
56
+ opens the feedback sheet the widget fetches a renderer on demand and attaches
57
+ the picture itself, as a second call after the report is saved. That ordering is
58
+ deliberate: an image that fails to render or upload never costs the report.
59
+
60
+ The image is **stored private in your org's assets**, and listing feedback
61
+ returns a **signed link good for about an hour** (`screenshot_url`). It is
62
+ signed on read, so it is not stable — re-list rather than persisting it. PNG,
63
+ JPEG or WebP up to 3 MB; over that the feedback is kept without the picture.
57
64
 
58
65
  ### Submitting by hand (only if you cannot use the script)
59
66
 
@@ -85,6 +92,11 @@ processed signal.
85
92
 
86
93
  ### Reading it back
87
94
 
95
+ Each item also carries what was pointed at (`target_selector`, `target_region`,
96
+ `target_context`), how they got there (`trace`), the `viewport`, and
97
+ `screenshot_url` when there is a picture. The table flags which items have one;
98
+ `--json` carries the link.
99
+
88
100
  `myapi feedback list` is newest first, filterable by `--kind` and `--status`
89
101
  (`open` | `resolved`), paged with `--limit` / `--offset`. **`total` is the
90
102
  number of matches, not the size of the page**, and `has_more` flags a truncated
@@ -104,6 +116,8 @@ that is deliberate, so ids cannot be probed across orgs.
104
116
  | `myapi feedback resolve <id>` | Close an item, keeping it |
105
117
  | `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
106
118
  | `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
119
+ | `myapi feedback widget list` | List widgets, each with the script tag to paste |
120
+ | `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`, or the look (`--accent`, `--position`, `--label`), keeping the same key |
107
121
  | `myapi feedback widget revoke <id>` | Revoke a key; collected feedback is kept |
108
122
  <!-- generated:end -->
109
123
 
@@ -114,7 +128,13 @@ that is deliberate, so ids cannot be probed across orgs.
114
128
  myapi feedback widget create marketing --origins example.com,www.example.com
115
129
  # → prints a PUBLIC key to embed in the page
116
130
 
117
- # 2. Read what came back, newest first
131
+ # 2. See your widgets and the exact tag to paste
132
+ myapi feedback widget list
133
+
134
+ # 3. Change where it appears — same key, so your page needs no edit
135
+ myapi feedback widget update <id> --routes /app,/app/**
136
+
137
+ # 4. Read what came back, newest first
118
138
  myapi feedback list --status open
119
139
  myapi feedback list --kind bug --limit 20
120
140
 
@@ -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.14.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.14.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",