@myapihq/cli 2.27.3 → 2.27.4
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,73 @@
|
|
|
1
|
+
// `storage upload --json` printed prose.
|
|
2
|
+
//
|
|
3
|
+
// Every other storage verb honours --json. Upload did not, so an agent that
|
|
4
|
+
// asked for machine output got "✓ Asset uploaded! ID: obj_…" and had to scrape
|
|
5
|
+
// the id out of a sentence to learn what it had just created — on the one verb
|
|
6
|
+
// whose RESULT it most needs, for a platform whose primary caller is an agent.
|
|
7
|
+
//
|
|
8
|
+
// Found by the probe on its first storage run: the step failed with "no JSON on
|
|
9
|
+
// stdout", which is the probe noticing something a human would have read past.
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as os from 'node:os';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
const ORG = '11111111-1111-4111-8111-111111111111';
|
|
15
|
+
const sdk = vi.hoisted(() => ({
|
|
16
|
+
storage: { uploadAsset: vi.fn() },
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('@myapihq/sdk', () => sdk);
|
|
19
|
+
vi.mock('../config.js', () => ({
|
|
20
|
+
requireConfig: () => ({ api_key: 'hq_live_test', default_org: ORG }),
|
|
21
|
+
loadConfig: () => ({ api_key: 'hq_live_test', default_org: ORG }),
|
|
22
|
+
CONFIG_DIR: '/tmp/nowhere',
|
|
23
|
+
}));
|
|
24
|
+
const RESULT = { asset_id: 'obj_abc', url: 'https://api.myapihq.com/storage/obj_abc' };
|
|
25
|
+
let printed;
|
|
26
|
+
// A real file rather than an fs mock: the handler reads it through whichever
|
|
27
|
+
// fs binding it imports, and a mock that misses that binding fails the test
|
|
28
|
+
// for a reason that has nothing to do with the behaviour under test.
|
|
29
|
+
const FILE = path.join(os.tmpdir(), `storage-json-${process.pid}.txt`);
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
fs.writeFileSync(FILE, 'bytes');
|
|
32
|
+
printed = [];
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
sdk.storage.uploadAsset.mockResolvedValue(RESULT);
|
|
35
|
+
const output = await import('../output.js');
|
|
36
|
+
const cap = ((m) => { printed.push(typeof m === 'string' ? m : JSON.stringify(m)); });
|
|
37
|
+
vi.spyOn(output, 'success').mockImplementation(cap);
|
|
38
|
+
vi.spyOn(output, 'info').mockImplementation(cap);
|
|
39
|
+
vi.spyOn(output, 'printJson').mockImplementation(cap);
|
|
40
|
+
vi.spyOn(output, 'error').mockImplementation(((m) => {
|
|
41
|
+
printed.push(String(m));
|
|
42
|
+
throw new Error('__EXIT__');
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => { vi.restoreAllMocks(); try {
|
|
46
|
+
fs.unlinkSync(FILE);
|
|
47
|
+
}
|
|
48
|
+
catch { /* already gone */ } });
|
|
49
|
+
async function upload(flags) {
|
|
50
|
+
const { run } = await import('./storage.js');
|
|
51
|
+
try {
|
|
52
|
+
await run('upload', [FILE], { org: ORG, 'content-type': 'text/plain', ...flags });
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e?.message !== '__EXIT__')
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
describe('storage upload --json', () => {
|
|
60
|
+
it('emits the asset object, not a sentence about it', async () => {
|
|
61
|
+
await upload({ json: true });
|
|
62
|
+
const out = printed.join('\n');
|
|
63
|
+
expect(out).toContain('obj_abc');
|
|
64
|
+
expect(out).not.toMatch(/Asset uploaded/);
|
|
65
|
+
expect(() => JSON.parse(out)).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
it('still prints the public-URL warning for humans', async () => {
|
|
68
|
+
// That warning is the only thing standing between a customer and a
|
|
69
|
+
// permanently public file, so --json must not be what removes it.
|
|
70
|
+
await upload({});
|
|
71
|
+
expect(printed.join('\n')).toMatch(/public forever/i);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/dist/commands/storage.js
CHANGED
|
@@ -101,6 +101,14 @@ async function upload(filePath, flags) {
|
|
|
101
101
|
const name = flags.name || basename(filePath);
|
|
102
102
|
const visibility = flags.private ? 'private' : undefined;
|
|
103
103
|
const res = await sdkStorage.uploadAsset(config.api_key, orgId, data, contentType, name, visibility);
|
|
104
|
+
// Every other storage verb honours --json; upload did not, so an agent that
|
|
105
|
+
// asked for machine output got prose and had to scrape "ID: obj_…" out of a
|
|
106
|
+
// sentence to learn what it had just created. Agents are the primary caller
|
|
107
|
+
// of this platform, and upload is the verb whose RESULT they most need.
|
|
108
|
+
if (flags.json) {
|
|
109
|
+
printJson(res);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
104
112
|
// A private upload has no URL to print, and saying nothing here would look
|
|
105
113
|
// like a partial failure. Name the next step instead.
|
|
106
114
|
if (visibility) {
|
package/dist/sdk-iam.test.js
CHANGED
|
@@ -163,9 +163,23 @@ describe('hq IAM contract surface', () => {
|
|
|
163
163
|
expect(hq.EXPOSES).toContain('POST /hq/account/keys/revoke-all');
|
|
164
164
|
expect(hq.EXPOSES).toContain('PATCH /hq/account/spend-cap');
|
|
165
165
|
});
|
|
166
|
-
|
|
166
|
+
// NOT a check against the backend, whatever the previous name said. This
|
|
167
|
+
// file cannot reach `iam.GrantableSlots`; it compares one client-side copy
|
|
168
|
+
// against another written here. When the backend added `payments`, both
|
|
169
|
+
// copies stayed stale, this stayed green, and it would have FAILED anyone
|
|
170
|
+
// who tried to fix it — a guard enforcing the bug.
|
|
171
|
+
//
|
|
172
|
+
// Found the only way it could be: the API accepted `--grant payments:write`
|
|
173
|
+
// while the CLI called it an unknown slot. Until then the only way to mint a
|
|
174
|
+
// payments-scoped key was to omit --grant, which mints an UNRESTRICTED key.
|
|
175
|
+
//
|
|
176
|
+
// Kept as a pin, because a change to this list should be deliberate, and
|
|
177
|
+
// renamed so it stops claiming an authority it does not have. The real check
|
|
178
|
+
// is the API: it rejects a grant it does not know.
|
|
179
|
+
it('GRANTABLE_SLOTS is not changed by accident (a pin, not a backend check)', () => {
|
|
167
180
|
expect([...hq.GRANTABLE_SLOTS].sort()).toEqual(['audience', 'company', 'crm', 'database', 'domain', 'email', 'function',
|
|
168
|
-
'funnel', 'image', 'llm', '
|
|
181
|
+
'funnel', 'image', 'llm', 'payments', 'people', 'storage', 'url',
|
|
182
|
+
'webhook', 'workflow']);
|
|
169
183
|
});
|
|
170
184
|
it('GRANTABLE_SLOTS excludes management/non-grantable surfaces', () => {
|
|
171
185
|
for (const s of ['hq', 'admin', 'internal', 'schema', 'ops', 'pixel']) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.27.
|
|
4
|
+
"version": "2.27.4",
|
|
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.27.
|
|
49
|
+
"@myapihq/sdk": "^2.27.4"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|