@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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
full.accounts[
|
|
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.
|
|
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-
|
|
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
|
|
46
|
-
for
|
|
47
|
-
browsers within ~5 minutes with no redeploy of your site
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
49
|
+
"@myapihq/sdk": "^2.15.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|