@myapihq/cli 2.15.0 → 2.15.2

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ // saveConfig() writes by api_key, not by the active index.
2
+ //
3
+ // The bug this pins is data loss, not misconfiguration. saveConfig wrote to
4
+ // accounts[active] on the assumption that the Config handed to it belongs to
5
+ // the active account. It does not when MYAPI_API_KEY names a key the file has
6
+ // never seen: loadConfig deliberately returns a bare `{ api_key, account_id:'' }`
7
+ // for an unknown key — inheriting the active account's defaults is the
8
+ // cross-tenant bug config-envkey.test.ts pins — and saving that stub then
9
+ // overwrote the signed-in entry with it. api_key, account_id, default_org and
10
+ // default_funnel, gone from disk.
11
+ //
12
+ // It was reachable from any org-scoped command once requireOrg began persisting
13
+ // `last_org_used`, which turned a config-command bug into a data-loss one on an
14
+ // ordinary `database namespaces`. Reported by the backend team 2026-08-17 while
15
+ // setting up a developer with an org-locked key; reproduced before fixing.
16
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
17
+ import * as fs from 'node:fs';
18
+ import * as os from 'node:os';
19
+ import * as path from 'node:path';
20
+ const SIGNED_IN = 'hq_live_signed_in_account';
21
+ const ORG_LOCKED = 'hq_live_org_locked_key';
22
+ let tmpHome;
23
+ let realHome;
24
+ const FULL_CONFIG = {
25
+ active: 0,
26
+ accounts: [
27
+ {
28
+ api_key: SIGNED_IN,
29
+ account_id: 'acct-real',
30
+ email: 'operator@example.com',
31
+ default_org: 'org-real',
32
+ default_funnel: 'fun-real',
33
+ },
34
+ ],
35
+ };
36
+ async function freshConfigModule() {
37
+ vi.resetModules();
38
+ return import('./config.js');
39
+ }
40
+ function readAccounts() {
41
+ return JSON.parse(fs.readFileSync(path.join(tmpHome, '.myapi', 'config.json'), 'utf-8')).accounts;
42
+ }
43
+ beforeEach(() => {
44
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'myapi-save-'));
45
+ fs.mkdirSync(path.join(tmpHome, '.myapi'), { recursive: true });
46
+ fs.writeFileSync(path.join(tmpHome, '.myapi', 'config.json'), JSON.stringify(FULL_CONFIG));
47
+ realHome = process.env.HOME;
48
+ process.env.HOME = tmpHome;
49
+ delete process.env.MYAPI_API_KEY;
50
+ delete process.env.MYAPI_KEY;
51
+ });
52
+ afterEach(() => {
53
+ if (realHome !== undefined)
54
+ process.env.HOME = realHome;
55
+ delete process.env.MYAPI_API_KEY;
56
+ fs.rmSync(tmpHome, { recursive: true, force: true });
57
+ });
58
+ describe('saveConfig upserts by api_key', () => {
59
+ it('does not destroy the signed-in account when saving an unknown env key', async () => {
60
+ process.env.MYAPI_API_KEY = ORG_LOCKED;
61
+ const { loadConfig, saveConfig } = await freshConfigModule();
62
+ const cfg = loadConfig();
63
+ expect(cfg.api_key).toBe(ORG_LOCKED);
64
+ expect(cfg.account_id).toBe(''); // the stub loadConfig hands back on purpose
65
+ // What any org-scoped command now does via requireOrg.
66
+ saveConfig({ ...cfg, last_org_used: 'org-customer' });
67
+ const accounts = readAccounts();
68
+ const original = accounts.find((a) => a.api_key === SIGNED_IN);
69
+ expect(original, 'the signed-in account must still exist').toBeDefined();
70
+ expect(original.account_id).toBe('acct-real');
71
+ expect(original.default_org).toBe('org-real');
72
+ expect(original.default_funnel).toBe('fun-real');
73
+ });
74
+ it('gives the env key its own entry so its defaults can persist', async () => {
75
+ process.env.MYAPI_API_KEY = ORG_LOCKED;
76
+ const { loadConfig, saveConfig } = await freshConfigModule();
77
+ saveConfig({ ...loadConfig(), default_org: 'org-customer' });
78
+ const entry = readAccounts().find((a) => a.api_key === ORG_LOCKED);
79
+ expect(entry, 'an unknown key should be appended, not merged into another').toBeDefined();
80
+ expect(entry.default_org).toBe('org-customer');
81
+ });
82
+ it('leaves `active` alone — an env key must not become the signed-in account', async () => {
83
+ process.env.MYAPI_API_KEY = ORG_LOCKED;
84
+ const { loadConfig, saveConfig } = await freshConfigModule();
85
+ saveConfig({ ...loadConfig(), default_org: 'org-customer' });
86
+ const full = JSON.parse(fs.readFileSync(path.join(tmpHome, '.myapi', 'config.json'), 'utf-8'));
87
+ expect(full.active).toBe(0);
88
+ expect(full.accounts[full.active].api_key).toBe(SIGNED_IN);
89
+ });
90
+ it('still updates in place for a key the config already knows', async () => {
91
+ const { loadConfig, saveConfig } = await freshConfigModule();
92
+ saveConfig({ ...loadConfig(), default_org: 'org-moved' });
93
+ const accounts = readAccounts();
94
+ expect(accounts).toHaveLength(1); // updated, not duplicated
95
+ expect(accounts[0].default_org).toBe('org-moved');
96
+ expect(accounts[0].account_id).toBe('acct-real');
97
+ });
98
+ });
package/dist/config.js CHANGED
@@ -94,13 +94,37 @@ function writeFullConfig(full) {
94
94
  // configs written by older CLI versions (pre-0o600) that are still 0644.
95
95
  fs.chmodSync(CONFIG_FILE, 0o600);
96
96
  }
97
+ // Upsert by api_key, NOT by the active index.
98
+ //
99
+ // Writing to accounts[active] assumed the config being saved is the active
100
+ // account's. It is not when MYAPI_API_KEY names a key the file has never seen:
101
+ // loadConfig deliberately returns a bare `{ api_key, account_id: '' }` for an
102
+ // unknown key (see the comment there — inheriting the active account's defaults
103
+ // is the cross-tenant bug it exists to avoid), and saving that stub then
104
+ // overwrote the signed-in entry with it. One ordinary command wiped the stored
105
+ // api_key, account_id, default_org and default_funnel.
106
+ //
107
+ // It became reachable from any org-scoped command once requireOrg started
108
+ // persisting `last_org_used`, which is what made a config-only bug into a
109
+ // data-loss one. Reproduced 2026-08-17.
110
+ //
111
+ // Matching loadConfig's own lookup makes read and write symmetric, and gives an
112
+ // env-supplied key somewhere of its own to keep defaults — which is what lets a
113
+ // developer holding an org-locked key have a default_org at all.
97
114
  export function saveConfig(config) {
98
115
  const full = loadFullConfig() ?? { active: 0, accounts: [] };
99
116
  const { autocomplete_setup, ...account } = config;
100
- if (full.accounts.length === 0)
101
- full.accounts.push(account);
102
- else
103
- full.accounts[full.active] = account;
117
+ const entry = account;
118
+ const existing = full.accounts.findIndex(a => a.api_key === entry.api_key);
119
+ if (existing >= 0) {
120
+ full.accounts[existing] = entry;
121
+ }
122
+ else {
123
+ // Appended, and `active` is deliberately left alone: a key passed through
124
+ // the environment for one command must not silently become the account the
125
+ // user is signed in as.
126
+ full.accounts.push(entry);
127
+ }
104
128
  if (autocomplete_setup !== undefined)
105
129
  full.autocomplete_setup = autocomplete_setup;
106
130
  writeFullConfig(full);
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-feedback-api
3
- version: 1.5.0
3
+ version: 1.5.1
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-fdd06435d3f56b8ff220144771731b60694ed107835b3869d96571f46e3f3653
7
+ checksum: sha256-b1aba273d6d32b4145dcee85fb59fec35f50b257fc41fb8da9f649a0b519e909
8
8
  ---
9
9
 
10
10
  # MyFeedbackAPI
@@ -42,13 +42,18 @@ theme baked in. This is the intended way in — you do not build a UI:
42
42
  <script src="https://api.myapihq.com/feedback/in/<widget_key>/widget.js" async></script>
43
43
  ```
44
44
 
45
- It renders a button, shows itself only on the routes the widget is configured
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`, `body`, `page_url`, `route`, `viewport`, `target_selector`,
48
- and `target_region` `{x,y,w,h}` — the region is on **every** report now, the
49
- element's bounding rectangle for a click and the dragged box for a drag. It also
50
- sends `target_context` (what the element *was* — its text, role, the heading
51
- above it) and `trace`, the last events before the report.
45
+ It renders a button and shows itself only on the routes the widget is
46
+ configured for set them with `widget update <id> --routes /app,/app/**`, and
47
+ the change reaches browsers within ~5 minutes with no redeploy of your site.
48
+
49
+ The person points at an element or drags a region, and it sends:
50
+
51
+ - `kind`, `body`, `page_url`, `route`, `viewport`
52
+ - `target_selector` and `target_region` `{x,y,w,h}` — the region is on **every**
53
+ report now: the element's bounding rectangle for a click, the dragged box for
54
+ a drag
55
+ - `target_context` — what the element *was*: its text, role, the heading above it
56
+ - `trace` — the last events before the report
52
57
 
53
58
  A wrong key does not break the page: the response is `application/javascript`
54
59
  carrying a JS comment that names the fix, so a typo cannot throw a syntax error
@@ -79,7 +84,9 @@ await fetch(`https://api.myapihq.com/feedback/in/${WIDGET_KEY}`, {
79
84
 
80
85
  Errors: `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys
81
86
  cannot be probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`,
82
- `BODY_REQUIRED`, `BODY_TOO_LONG`. The key only ever writes.
87
+ `BODY_REQUIRED`, `BODY_TOO_LONG` (the text is over 8000 chars) and
88
+ `BODY_TOO_LARGE` (the whole request is over 64 KB, usually an oversized
89
+ `trace`). Nothing is saved in either case. The key only ever writes.
83
90
 
84
91
  ### Kind is a claim, not a guess
85
92
 
@@ -123,7 +130,7 @@ that is deliberate, so ids cannot be probed across orgs.
123
130
  | Command | What it does |
124
131
  |---|---|
125
132
  | `myapi feedback create "<text>" --kind <k>` | Record one item (`--page-url`, `--route` for context) |
126
- | `myapi feedback list [--kind bug\|issue\|suggestion] [--status open\|resolved] [--limit N] [--offset N]` | List feedback, newest first |
133
+ | `myapi feedback list [--kind bug\|issue\|suggestion] [--status open\|resolved] [--limit N] [--offset N] [--trace]` | List feedback, newest first. Each report summarises what was pointed at, what happened before, and whether a screenshot exists; `--trace` expands the events |
127
134
  | `myapi feedback resolve <id>` | Close an item, keeping it |
128
135
  | `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
129
136
  | `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
@@ -149,12 +156,13 @@ myapi feedback widget update <id> --routes /app,/app/**
149
156
  myapi feedback list --status open
150
157
  myapi feedback list --kind bug --limit 20
151
158
 
152
- # 3. Record something yourself (support call, your own testing)
159
+ # 5. Record something yourself (support call, your own testing)
153
160
  myapi feedback create "checkout 500s on the second attempt" --kind bug \
154
161
  --route /checkout
155
162
 
156
- # 4. Close it
163
+ # 6. Close it — or delete it, which also removes its screenshot
157
164
  myapi feedback resolve <id>
165
+ myapi feedback delete <id> --yes
158
166
  ```
159
167
  <!-- llm:end -->
160
168
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.15.0",
4
+ "version": "2.15.2",
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.15.0"
49
+ "@myapihq/sdk": "^2.15.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",