@myapihq/cli 2.4.2 → 2.5.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.
- package/dist/commands/authproduct.js +1 -1
- package/dist/commands/billing.js +8 -2
- package/dist/commands/config.js +16 -0
- package/dist/commands/container.d.ts +2 -1
- package/dist/commands/container.js +30 -5
- package/dist/commands/fn.d.ts +1 -1
- package/dist/commands/fn.js +51 -9
- package/dist/commands/keys.js +5 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +62 -2
- package/dist/commands/payments.js +6 -2
- package/dist/commands/pixel.d.ts +2 -1
- package/dist/commands/pixel.js +28 -1
- package/dist/commands/queue.d.ts +1 -1
- package/dist/commands/queue.js +15 -2
- package/dist/commands/setup.js +6 -2
- package/dist/commands/storage.js +13 -2
- package/dist/commands/webhook.d.ts +1 -0
- package/dist/commands/webhook.js +74 -2
- package/dist/errors.js +6 -0
- package/dist/flags.d.ts +1 -1
- package/dist/flags.js +21 -0
- package/dist/flags.test.js +35 -0
- package/dist/index.js +60 -0
- package/dist/output.js +8 -0
- package/dist/sdk-container.test.js +5 -1
- package/dist/skills/my-audience-api/README.md +1 -1
- package/dist/skills/my-auth-api/SKILL.md +3 -1
- package/dist/skills/my-company-api/README.md +1 -1
- package/dist/skills/my-container-api/README.md +1 -1
- package/dist/skills/my-container-api/SKILL.md +3 -1
- package/dist/skills/my-crm-api/README.md +1 -1
- package/dist/skills/my-crm-api/SKILL.md +6 -4
- package/dist/skills/my-database-api/README.md +1 -1
- package/dist/skills/my-database-api/SKILL.md +3 -1
- package/dist/skills/my-email-api/README.md +1 -1
- package/dist/skills/my-email-verify-api/README.md +1 -1
- package/dist/skills/my-git-api/README.md +1 -1
- package/dist/skills/my-image-api/README.md +1 -1
- package/dist/skills/my-llm-api/README.md +1 -1
- package/dist/skills/my-people-api/README.md +1 -1
- package/dist/skills/my-pixel-api/README.md +1 -1
- package/dist/skills/my-queue-api/README.md +1 -1
- package/dist/skills/my-storage-api/README.md +1 -1
- package/dist/skills/my-task-api/README.md +1 -1
- package/dist/skills/my-webhook-api/README.md +1 -1
- package/dist/skills/my-webhook-api/SKILL.md +7 -3
- package/dist/skills/my-workflow-api/README.md +1 -1
- package/package.json +3 -2
|
@@ -11,7 +11,7 @@ import { requireOrg } from '../helpers.js';
|
|
|
11
11
|
export const SCHEMA = {
|
|
12
12
|
name: 'string',
|
|
13
13
|
type: 'string', // spa | web
|
|
14
|
-
redirect: '
|
|
14
|
+
redirect: 'list', // comma-separated OR repeated; occurrences accumulate
|
|
15
15
|
theme: 'string', // JSON for the hosted login page
|
|
16
16
|
connections: 'string', // comma-separated sign-in methods: google,password,magic
|
|
17
17
|
domain: 'string', // custom auth domain, e.g. auth.acme.com
|
package/dist/commands/billing.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hq } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
|
-
import { success, error, printTable, info, printJson } from '../output.js';
|
|
3
|
+
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
4
4
|
import { confirm, isNonInteractive } from '../prompt.js';
|
|
5
5
|
import { formatDate } from '../utils.js';
|
|
6
6
|
export const SCHEMA = {
|
|
@@ -185,7 +185,13 @@ export async function topup(amountStr, flags) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
const config = requireConfig();
|
|
188
|
-
|
|
188
|
+
// A replay means our retry collapsed into the original request rather than
|
|
189
|
+
// topping up again. Say so — an agent that reads "topped up" twice and adds
|
|
190
|
+
// the amounts up gets the balance wrong.
|
|
191
|
+
let replayed = false;
|
|
192
|
+
const result = await hq.topUp(config.api_key, amount, { onReplay: () => { replayed = true; } });
|
|
193
|
+
if (replayed)
|
|
194
|
+
banner('› This was a replay of an identical earlier top-up — your balance was charged once, not twice.');
|
|
189
195
|
success(`Top up successful! New balance: ${result.new_balance_display}`);
|
|
190
196
|
}
|
|
191
197
|
export async function setup(_flags) {
|
package/dist/commands/config.js
CHANGED
|
@@ -5,6 +5,7 @@ export const SCHEMA = {
|
|
|
5
5
|
_via: 'string',
|
|
6
6
|
};
|
|
7
7
|
export const EXPOSES = [
|
|
8
|
+
'PATCH /hq/account/default-org',
|
|
8
9
|
'GET /hq/orgs/{org_id}',
|
|
9
10
|
'GET /funnel/orgs/{org_id}/funnels/{funnel_id}',
|
|
10
11
|
];
|
|
@@ -34,10 +35,25 @@ export async function setOrg(id, _flags, via = 'auth config') {
|
|
|
34
35
|
}
|
|
35
36
|
catch { /* swallow — clearing alone is still correct */ }
|
|
36
37
|
saveConfig(config);
|
|
38
|
+
// Also pin it account-side. The local default lives in this machine's
|
|
39
|
+
// ~/.myapi/config.json, so signing in anywhere else used to land on whatever
|
|
40
|
+
// org the backend picked ("most recently created") — which is how an agent
|
|
41
|
+
// on a fresh machine ends up writing to the wrong namespace. Best-effort:
|
|
42
|
+
// the local default is already saved and correct, so a failure here is a
|
|
43
|
+
// smaller default not following you, never a broken local setup.
|
|
44
|
+
let pinned = false;
|
|
45
|
+
try {
|
|
46
|
+
await hq.setAccountDefaultOrg(config.api_key, org.id);
|
|
47
|
+
pinned = true;
|
|
48
|
+
}
|
|
49
|
+
catch { /* local default stands */ }
|
|
37
50
|
success(`Default organization set to: ${org.name || org.id}`);
|
|
38
51
|
if (config.default_funnel) {
|
|
39
52
|
info(` Default funnel auto-resolved to ${config.default_funnel}.`);
|
|
40
53
|
}
|
|
54
|
+
info(pinned
|
|
55
|
+
? ' Pinned account-wide — new logins on other machines get this org too.'
|
|
56
|
+
: ' Set on this machine only (could not pin it account-wide).');
|
|
41
57
|
}
|
|
42
58
|
export async function setFunnel(id, _flags, via = 'auth config') {
|
|
43
59
|
if (!id) {
|
|
@@ -7,7 +7,7 @@ export declare const NAME_RE: RegExp;
|
|
|
7
7
|
export declare const RESERVED_NAMES: Set<string>;
|
|
8
8
|
export declare function _validateName(name: string): string | null;
|
|
9
9
|
export declare function _parseEnv(raw: string): Record<string, string> | string;
|
|
10
|
-
export declare function create(flags: Flags): Promise<void>;
|
|
10
|
+
export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
|
|
11
11
|
export declare function list(flags: Flags): Promise<void>;
|
|
12
12
|
export declare function get(id: string, flags: Flags): Promise<void>;
|
|
13
13
|
export declare function del(id: string, flags: Flags): Promise<void>;
|
|
@@ -15,4 +15,5 @@ export declare function _isTarball(p: string): boolean;
|
|
|
15
15
|
export declare function deploy(id: string, image: string, flags: Flags): Promise<void>;
|
|
16
16
|
export declare function logs(id: string, flags: Flags): Promise<void>;
|
|
17
17
|
export declare function domain(id: string, domainArg: string | undefined, flags: Flags): Promise<void>;
|
|
18
|
+
export declare function buildLogs(id: string | undefined, flags: Flags): Promise<void>;
|
|
18
19
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
@@ -6,6 +6,7 @@ import { success, error, printTable, info, printJson, banner } from '../output.j
|
|
|
6
6
|
import { formatDate, pollJob } from '../utils.js';
|
|
7
7
|
import { requireOrg, confirmDestructive } from '../helpers.js';
|
|
8
8
|
export const EXPOSES = [
|
|
9
|
+
'GET /container/orgs/{org_id}/containers/{id}/build-logs',
|
|
9
10
|
'POST /container/orgs/{org_id}/containers',
|
|
10
11
|
'GET /container/orgs/{org_id}/containers',
|
|
11
12
|
'GET /container/orgs/{org_id}/containers/{id}',
|
|
@@ -70,12 +71,15 @@ function summarizeContainer(c) {
|
|
|
70
71
|
updated_at: c.updated_at ? formatDate(c.updated_at) : '',
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
export async function create(flags) {
|
|
74
|
+
export async function create(nameArg, flags) {
|
|
74
75
|
const config = requireConfig();
|
|
75
76
|
const orgId = requireOrg(flags, config, 'myapi container create --name <name> [--type service|worker|job] [--org <id>]');
|
|
76
|
-
|
|
77
|
+
// Positional name is the documented shape across the CLI (`org create`,
|
|
78
|
+
// `queue create`, `git create`, …); `--name` stayed the only accepted form
|
|
79
|
+
// here, which made these two the odd ones out. Both work now.
|
|
80
|
+
const name = nameArg ?? flags.name;
|
|
77
81
|
if (!name) {
|
|
78
|
-
error('Missing --name.\nUsage: myapi container create
|
|
82
|
+
error('Missing --name.\nUsage: myapi container create <name> [--type service|worker|job] [--org <id>]\n\n→ Name is a kebab-case slug, 1-50 chars (e.g. "my-worker").');
|
|
79
83
|
}
|
|
80
84
|
const nameErr = _validateName(name);
|
|
81
85
|
if (nameErr)
|
|
@@ -326,6 +330,25 @@ export async function domain(id, domainArg, flags) {
|
|
|
326
330
|
info(`Origin: ${binding.origin}`);
|
|
327
331
|
info(`Status: ${binding.status}`);
|
|
328
332
|
}
|
|
333
|
+
export async function buildLogs(id, flags) {
|
|
334
|
+
const config = requireConfig();
|
|
335
|
+
const orgId = requireOrg(flags, config, 'myapi container build-logs <id> [--tail N] [--org <id>]');
|
|
336
|
+
if (!id)
|
|
337
|
+
error('Missing id.\nUsage: myapi container build-logs <id> [--tail N]');
|
|
338
|
+
const tail = typeof flags.tail === 'number' ? flags.tail : undefined;
|
|
339
|
+
const res = await sdkContainer.buildLogs(config.api_key, orgId, id, tail);
|
|
340
|
+
if (flags.json) {
|
|
341
|
+
printJson(res);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const lines = res.logs ?? [];
|
|
345
|
+
if (lines.length === 0) {
|
|
346
|
+
info('No build logs yet — has this container been deployed with --source?');
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
for (const l of lines)
|
|
350
|
+
info(l);
|
|
351
|
+
}
|
|
329
352
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
330
353
|
const SUBCOMMAND_USAGE = {
|
|
331
354
|
'create': `myapi container create --name <name> [--type service|worker|job] [--cron <expr>]
|
|
@@ -384,13 +407,14 @@ jobs. The heavier-duty sibling of edge functions (myapi fn), for native
|
|
|
384
407
|
dependencies and long execution.
|
|
385
408
|
|
|
386
409
|
Subcommands:
|
|
410
|
+
build-logs <id> Why the last --source build failed (--tail N; default 100)
|
|
387
411
|
create Register a container and get its scoped API key (returned once)
|
|
388
412
|
delete <id> Soft-delete and revoke its scoped API key
|
|
389
413
|
deploy <id> <image> Ship a pre-built image (or --source <dir|tar> to build) and go live
|
|
390
414
|
domain <id> <domain> Bind a custom domain (--remove to unbind)
|
|
391
415
|
get <id> Inspect a container
|
|
392
416
|
list List containers in your org
|
|
393
|
-
logs <id> Show recent runtime logs (--tail <n>)
|
|
417
|
+
logs <id> Show recent runtime logs (--tail <n>) — see build-logs for build failures
|
|
394
418
|
|
|
395
419
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
396
420
|
return;
|
|
@@ -404,7 +428,8 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
404
428
|
return;
|
|
405
429
|
}
|
|
406
430
|
switch (subcommand) {
|
|
407
|
-
case 'create': return create(flags);
|
|
431
|
+
case 'create': return create(args[0], flags);
|
|
432
|
+
case 'build-logs': return buildLogs(args[0], flags);
|
|
408
433
|
case 'deploy': return deploy(args[0], args[1], flags);
|
|
409
434
|
case 'list': return list(flags);
|
|
410
435
|
case 'get': return get(args[0], flags);
|
package/dist/commands/fn.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare const SCHEMA: FlagSchema;
|
|
|
6
6
|
export declare const NAME_RE: RegExp;
|
|
7
7
|
export declare const RESERVED_NAMES: Set<string>;
|
|
8
8
|
export declare function _validateName(name: string): string | null;
|
|
9
|
-
export declare function create(flags: Flags): Promise<void>;
|
|
9
|
+
export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
|
|
10
10
|
export declare function list(flags: Flags): Promise<void>;
|
|
11
11
|
export declare function get(id: string, flags: Flags): Promise<void>;
|
|
12
12
|
export declare function del(id: string, flags: Flags): Promise<void>;
|
package/dist/commands/fn.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
import { fn as sdkFn } from '@myapihq/sdk';
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
|
-
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
4
|
+
import { success, error, printTable, info, printJson, banner, spinnerFrame, spinnerWrite, clearLine } from '../output.js';
|
|
5
5
|
import { formatDate } from '../utils.js';
|
|
6
6
|
import { requireOrg, confirmDestructive } from '../helpers.js';
|
|
7
7
|
export const EXPOSES = [
|
|
@@ -14,8 +14,12 @@ export const EXPOSES = [
|
|
|
14
14
|
'GET /function/orgs/{org_id}/functions/{id}/runs',
|
|
15
15
|
];
|
|
16
16
|
export const SCHEMA = {
|
|
17
|
+
// `create` takes a positional name; --name stays accepted for
|
|
18
|
+
// back-compat (RELEASING.md's stated convention) and must be declared
|
|
19
|
+
// here so the command-aware flag check in index.ts doesn't flag it.
|
|
20
|
+
name: 'string',
|
|
17
21
|
cron: 'string',
|
|
18
|
-
scope: '
|
|
22
|
+
scope: 'list',
|
|
19
23
|
set: 'string',
|
|
20
24
|
};
|
|
21
25
|
// Backend: Story 1 (function CRUD + scoped key) and Story 2/4/5 (deploy a
|
|
@@ -54,12 +58,15 @@ function summarizeFn(f) {
|
|
|
54
58
|
updated_at: f.updated_at,
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
|
-
export async function create(flags) {
|
|
61
|
+
export async function create(nameArg, flags) {
|
|
58
62
|
const config = requireConfig();
|
|
59
63
|
const orgId = requireOrg(flags, config, 'myapi fn create --name <name> [--cron <expr>] [--org <id>]');
|
|
60
|
-
|
|
64
|
+
// Positional name is the documented shape across the CLI (`org create`,
|
|
65
|
+
// `queue create`, `git create`, …); `--name` stayed the only accepted form
|
|
66
|
+
// here, which made these two the odd ones out. Both work now.
|
|
67
|
+
const name = nameArg ?? flags.name;
|
|
61
68
|
if (!name) {
|
|
62
|
-
error('Missing --name.\nUsage: myapi fn create
|
|
69
|
+
error('Missing --name.\nUsage: myapi fn create <name> [--cron <expr>] [--org <id>]\n\n→ Name is a kebab-case slug, 1-50 chars (e.g. "my-app-api").');
|
|
63
70
|
}
|
|
64
71
|
validateName(name);
|
|
65
72
|
const cron = flags.cron;
|
|
@@ -69,9 +76,10 @@ export async function create(flags) {
|
|
|
69
76
|
};
|
|
70
77
|
if (cron)
|
|
71
78
|
payload.cron_schedule = cron;
|
|
72
|
-
// --scope narrows the minted key's slot grants.
|
|
73
|
-
//
|
|
74
|
-
//
|
|
79
|
+
// --scope narrows the minted key's slot grants. Declared 'list', so repeated
|
|
80
|
+
// occurrences accumulate (`--scope email --scope storage`) and a comma-joined
|
|
81
|
+
// form works too (`--scope email,storage`). It used to keep only the last
|
|
82
|
+
// occurrence silently. Grants can never exceed the caller's.
|
|
75
83
|
if (typeof flags.scope === 'string') {
|
|
76
84
|
const scopes = flags.scope.split(',').map(s => s.trim()).filter(Boolean);
|
|
77
85
|
if (scopes.length > 0)
|
|
@@ -161,10 +169,44 @@ export async function deploy(id, bundlePath, flags) {
|
|
|
161
169
|
}
|
|
162
170
|
success(`Deployed function ${id}`);
|
|
163
171
|
info(`Invocation URL: ${result.invocation_url}`);
|
|
172
|
+
// The edge takes ~15-45s to start serving a freshly-deployed bundle, and
|
|
173
|
+
// until it does the invocation URL returns 404. That is a trap for the
|
|
174
|
+
// correct agent behaviour — deploy, then verify — because the obvious
|
|
175
|
+
// reading of a 404 is "the deploy failed", and the obvious recovery is to
|
|
176
|
+
// deploy again. Redeploying ROTATES THE SCOPED KEY, breaking anything
|
|
177
|
+
// already wired to the old one. So: wait for it here, and if it hasn't come
|
|
178
|
+
// up, say explicitly that redeploying is the wrong move.
|
|
179
|
+
if (result.invocation_url) {
|
|
180
|
+
await waitForLive(result.invocation_url);
|
|
181
|
+
}
|
|
164
182
|
info('');
|
|
165
183
|
info(`Scoped API key was rotated. New value (returned once — save it if you need it):`);
|
|
166
184
|
info(` ${result.scoped_api_key}`);
|
|
167
185
|
}
|
|
186
|
+
// Poll until the edge serves the new bundle. Best-effort and never fatal —
|
|
187
|
+
// the deploy already succeeded, so a slow or unreachable edge must not turn a
|
|
188
|
+
// successful write into a non-zero exit.
|
|
189
|
+
async function waitForLive(url, budgetMs = 75_000) {
|
|
190
|
+
const started = Date.now();
|
|
191
|
+
let frame = 0;
|
|
192
|
+
while (Date.now() - started < budgetMs) {
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(5000) });
|
|
195
|
+
if (res.status !== 404) {
|
|
196
|
+
clearLine();
|
|
197
|
+
info(`Status: live (${Math.round((Date.now() - started) / 1000)}s)`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch { /* edge not answering yet */ }
|
|
202
|
+
spinnerWrite(`\r${spinnerFrame(frame++)} waiting for the edge to serve it…`);
|
|
203
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
204
|
+
}
|
|
205
|
+
clearLine();
|
|
206
|
+
info(`Status: not serving yet after ${Math.round(budgetMs / 1000)}s.`);
|
|
207
|
+
info(` The deploy itself succeeded — give it another minute.`);
|
|
208
|
+
info(` Do NOT redeploy to "fix" it: that rotates the scoped key again.`);
|
|
209
|
+
}
|
|
168
210
|
// _parseSetPairs parses `--set K=V` entries into a map. Accepts a single
|
|
169
211
|
// string (comma-joined: K=V,K2=V2) or an array of strings (when the flag is
|
|
170
212
|
// repeated and the parser preserves them). Returns the map or an error
|
|
@@ -312,7 +354,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
312
354
|
return;
|
|
313
355
|
}
|
|
314
356
|
switch (subcommand) {
|
|
315
|
-
case 'create': return create(flags);
|
|
357
|
+
case 'create': return create(args[0], flags);
|
|
316
358
|
case 'deploy': return deploy(args[0], args[1], flags);
|
|
317
359
|
case 'env': return setEnv(args[0], args[1], args[2], flags);
|
|
318
360
|
case 'runs': return runs(args[0], flags);
|
package/dist/commands/keys.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatDate } from '../utils.js';
|
|
1
2
|
import { hq } from '@myapihq/sdk';
|
|
2
3
|
import { requireConfig } from '../config.js';
|
|
3
4
|
import { success, error, printTable, info, printJson } from '../output.js';
|
|
@@ -132,6 +133,10 @@ export async function list(flags) {
|
|
|
132
133
|
Cap: k.spend_cap_cents != null
|
|
133
134
|
? `$${((k.current_period_spend_cents ?? 0) / 100).toFixed(2)} / $${(k.spend_cap_cents / 100).toFixed(2)} (${k.spend_cap_period})`
|
|
134
135
|
: '—',
|
|
136
|
+
Created: k.created_at ? formatDate(k.created_at) : '',
|
|
137
|
+
// A key that has never authenticated a request is the one that's safe to
|
|
138
|
+
// revoke — so "never" is a useful answer here, not a missing value.
|
|
139
|
+
'Last used': k.last_used_at ? formatDate(k.last_used_at) : 'never',
|
|
135
140
|
}));
|
|
136
141
|
printTable(rows, {
|
|
137
142
|
flags,
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export interface OpenerCommand {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function browserCommand(url: string, platform?: string): OpenerCommand;
|
|
19
19
|
export declare function openBrowser(url: string, opener?: OpenerCommand): Promise<boolean>;
|
|
20
|
+
export declare function deviceLabel(): string;
|
|
20
21
|
export declare function login(flags?: Flags): Promise<void>;
|
package/dist/commands/login.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
// `--mock` still runs the whole browser UX against an in-process fake of the
|
|
11
11
|
// hosted login page (real loopback server, real PKCE, fake identity, persists
|
|
12
12
|
// nothing) for offline UX previews and demos.
|
|
13
|
+
import { hq } from '@myapihq/sdk';
|
|
13
14
|
import * as http from 'http';
|
|
15
|
+
import * as os from 'os';
|
|
14
16
|
import * as crypto from 'crypto';
|
|
15
17
|
import { spawn } from 'child_process';
|
|
16
18
|
import { info, success, error } from '../output.js';
|
|
17
|
-
import { addAccount } from '../config.js';
|
|
19
|
+
import { addAccount, loadConfig } from '../config.js';
|
|
18
20
|
export const EXPOSES = [
|
|
19
21
|
'POST /hq/account/exchange',
|
|
20
22
|
];
|
|
@@ -341,12 +343,62 @@ async function exchangeCodeForTokens(code, verifier, redirectUri) {
|
|
|
341
343
|
}
|
|
342
344
|
return data.id_token;
|
|
343
345
|
}
|
|
346
|
+
// A stable, human-meaningful label for this machine. Signing in on four
|
|
347
|
+
// machines used to leave four indistinguishable account-wide keys with no way
|
|
348
|
+
// to tell which was which — this is the half that makes `keys list`
|
|
349
|
+
// actionable.
|
|
350
|
+
export function deviceLabel() {
|
|
351
|
+
try {
|
|
352
|
+
return os.hostname() || 'cli';
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return 'cli';
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Ids of keys already named for THIS device. Best-effort: if we have no
|
|
359
|
+
// usable key yet (first sign-in on this machine) there is nothing to
|
|
360
|
+
// supersede, and a failed lookup must never block signing in.
|
|
361
|
+
async function priorKeysForThisDevice() {
|
|
362
|
+
const label = `cli@${deviceLabel()}`;
|
|
363
|
+
try {
|
|
364
|
+
const cfg = loadConfig();
|
|
365
|
+
if (!cfg?.api_key)
|
|
366
|
+
return [];
|
|
367
|
+
const keys = await hq.listApiKeys(cfg.api_key);
|
|
368
|
+
return keys.filter(k => k.name === label).map(k => k.id);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Revoke the keys this device superseded. Runs AFTER the new key is saved, so
|
|
375
|
+
// a failure here leaves the user signed in with a working key and one extra
|
|
376
|
+
// stale key — the safe direction. Never fatal.
|
|
377
|
+
async function revokeSuperseded(newKey, ids) {
|
|
378
|
+
if (ids.length === 0)
|
|
379
|
+
return;
|
|
380
|
+
let revoked = 0;
|
|
381
|
+
for (const id of ids) {
|
|
382
|
+
try {
|
|
383
|
+
await hq.revokeApiKey(newKey, id);
|
|
384
|
+
revoked++;
|
|
385
|
+
}
|
|
386
|
+
catch { /* leave it listed */ }
|
|
387
|
+
}
|
|
388
|
+
if (revoked > 0) {
|
|
389
|
+
info(` Replaced ${revoked} previous key${revoked === 1 ? '' : 's'} for this device (cli@${deviceLabel()})`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
344
392
|
// exchangeTokenForKey trades the verified id_token for an hq API key + defaults.
|
|
345
393
|
async function exchangeTokenForKey(idToken) {
|
|
346
394
|
const r = await fetch(`${hqBase()}/hq/account/exchange`, {
|
|
347
395
|
method: 'POST',
|
|
348
396
|
headers: { 'content-type': 'application/json' },
|
|
349
|
-
|
|
397
|
+
// `device` names the minted key `cli@<device>` so `keys list` says which
|
|
398
|
+
// machine each key belongs to. Deterministic per machine, which is what
|
|
399
|
+
// lets us revoke the key this device is replacing (below). Backend
|
|
400
|
+
// sanitises it and falls back to plain `cli` when absent.
|
|
401
|
+
body: JSON.stringify({ id_token: idToken, device: deviceLabel() }),
|
|
350
402
|
signal: AbortSignal.timeout(30_000),
|
|
351
403
|
});
|
|
352
404
|
const env = await r.json().catch(() => ({}));
|
|
@@ -406,6 +458,13 @@ export async function login(flags = {}) {
|
|
|
406
458
|
info('');
|
|
407
459
|
error('Timed out waiting for the browser sign-in (3 minutes). Run: myapi login');
|
|
408
460
|
}, CALLBACK_TIMEOUT_MS);
|
|
461
|
+
// Snapshot the keys this device already owns BEFORE signing in. Taking the
|
|
462
|
+
// list first is what makes the revoke safe: the key we are about to mint
|
|
463
|
+
// cannot be in a set captured before it existed, so there is no way to
|
|
464
|
+
// revoke the credential we just saved. (Matching afterwards would mean
|
|
465
|
+
// identifying the new key, and `prefix` is not derivable from the key
|
|
466
|
+
// string — a wrong guess here locks someone out of their own account.)
|
|
467
|
+
const supersededKeyIds = await priorKeysForThisDevice();
|
|
409
468
|
let auth;
|
|
410
469
|
let email = '';
|
|
411
470
|
try {
|
|
@@ -430,6 +489,7 @@ export async function login(flags = {}) {
|
|
|
430
489
|
default_org: auth.default_org || undefined,
|
|
431
490
|
default_funnel: auth.default_funnel || undefined,
|
|
432
491
|
});
|
|
492
|
+
await revokeSuperseded(auth.api_key, supersededKeyIds);
|
|
433
493
|
success(`› Signed in${email ? ` · ${email}` : ''}`);
|
|
434
494
|
info(` Account: ${auth.account_id}`);
|
|
435
495
|
if (auth.default_org)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { payments as sdkPayments } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
|
-
import { success, error, printTable, info, printJson } from '../output.js';
|
|
3
|
+
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
4
4
|
import { formatDate } from '../utils.js';
|
|
5
5
|
import { requireOrg } from '../helpers.js';
|
|
6
6
|
export const EXPOSES = [
|
|
@@ -111,7 +111,11 @@ export async function charge(flags) {
|
|
|
111
111
|
payload.success_url = flags['success-url'];
|
|
112
112
|
if (flags['cancel-url'])
|
|
113
113
|
payload.cancel_url = flags['cancel-url'];
|
|
114
|
-
|
|
114
|
+
// See billing.ts — a replayed charge did not execute a second time.
|
|
115
|
+
let replayed = false;
|
|
116
|
+
const res = await sdkPayments.createCharge(config.api_key, orgId, payload, { onReplay: () => { replayed = true; } });
|
|
117
|
+
if (replayed)
|
|
118
|
+
banner('› This was a replay of an identical earlier charge — the customer was charged once, not twice.');
|
|
115
119
|
if (flags.json) {
|
|
116
120
|
printJson(res);
|
|
117
121
|
return;
|
package/dist/commands/pixel.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Exposes } from '../exposes.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type Flags } from '../helpers.js';
|
|
3
3
|
import type { FlagSchema } from '../flags.js';
|
|
4
4
|
export declare const SCHEMA: FlagSchema;
|
|
5
5
|
export declare const EXPOSES: Exposes;
|
|
@@ -8,4 +8,5 @@ export declare function visits(flags: Flags): Promise<void>;
|
|
|
8
8
|
export declare function events(flags: Flags): Promise<void>;
|
|
9
9
|
export declare function audience(flags: Flags): Promise<void>;
|
|
10
10
|
export declare function identity(pixelId: string, flags: Flags): Promise<void>;
|
|
11
|
+
export declare function identify(pixelId: string | undefined, flags: Flags): Promise<void>;
|
|
11
12
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/pixel.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { pixel as sdkPixel } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
|
-
import { error, printTable, info, printJson } from '../output.js';
|
|
3
|
+
import { success, error, printTable, info, printJson } from '../output.js';
|
|
4
|
+
import { requireOrg } from '../helpers.js';
|
|
4
5
|
export const SCHEMA = {
|
|
6
|
+
'external-id': 'string',
|
|
5
7
|
org: 'string',
|
|
6
8
|
website: 'string',
|
|
7
9
|
'campaign-id': 'string',
|
|
@@ -12,6 +14,7 @@ export const SCHEMA = {
|
|
|
12
14
|
offset: 'number',
|
|
13
15
|
};
|
|
14
16
|
export const EXPOSES = [
|
|
17
|
+
'POST /pixel/orgs/{org_id}/identify',
|
|
15
18
|
'GET /pixel/orgs/{org_id}/interactions',
|
|
16
19
|
'GET /pixel/orgs/{org_id}/visits',
|
|
17
20
|
'GET /pixel/orgs/{org_id}/events',
|
|
@@ -151,6 +154,28 @@ const SUBCOMMAND_USAGE = {
|
|
|
151
154
|
'audience': 'myapi pixel audience [--org <id>]',
|
|
152
155
|
'identity': 'myapi pixel identity <pixel_id> --website <domain> [--org <id>]',
|
|
153
156
|
};
|
|
157
|
+
// identify — the WRITE half of identity resolution.
|
|
158
|
+
//
|
|
159
|
+
// `identity` reads the graph; without a call to this, that graph only ever
|
|
160
|
+
// contains anonymous nodes. Call it on form submit or right after sign-in and
|
|
161
|
+
// the visitor's whole prior anonymous history resolves to that person.
|
|
162
|
+
export async function identify(pixelId, flags) {
|
|
163
|
+
const config = requireConfig();
|
|
164
|
+
const orgId = requireOrg(flags, config, 'myapi pixel identify <pixel_id> (--email <e> | --external-id <id>) [--org <id>]');
|
|
165
|
+
if (!pixelId)
|
|
166
|
+
error('Missing pixel_id.\nUsage: myapi pixel identify <pixel_id> --email <email>\n\n→ The pixel id comes from `myapi pixel visits` or `pixel interactions`.');
|
|
167
|
+
const email = typeof flags.email === 'string' ? flags.email : undefined;
|
|
168
|
+
const externalId = typeof flags['external-id'] === 'string' ? flags['external-id'] : undefined;
|
|
169
|
+
if (!email && !externalId) {
|
|
170
|
+
error('Nothing to identify with.\nPass --email <address> or --external-id <your-user-id>.');
|
|
171
|
+
}
|
|
172
|
+
const res = await sdkPixel.identify(config.api_key, orgId, pixelId, { email, externalId });
|
|
173
|
+
if (flags.json) {
|
|
174
|
+
printJson(res);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
success(`Linked pixel ${pixelId} to ${email ?? externalId}`);
|
|
178
|
+
}
|
|
154
179
|
export async function run(subcommand, args, flags) {
|
|
155
180
|
if (!subcommand || (flags.help && !subcommand)) {
|
|
156
181
|
info(`Usage: myapi pixel <subcommand>
|
|
@@ -158,6 +183,7 @@ export async function run(subcommand, args, flags) {
|
|
|
158
183
|
Subcommands:
|
|
159
184
|
audience Geographic distribution sample of your pixel audience
|
|
160
185
|
events Engagement events (sent / open / click / page_visit) by campaign or domain
|
|
186
|
+
identify Link a known email/user id to an anonymous pixel visitor
|
|
161
187
|
identity Resolve the identity graph (emails, IPs, profiles) for a pixel ID
|
|
162
188
|
interactions Get a unified timeline of visits and events
|
|
163
189
|
(requires at least one filter: --website, --campaign-id, or --domain)
|
|
@@ -180,6 +206,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
180
206
|
case 'events': return events(flags);
|
|
181
207
|
case 'audience': return audience(flags);
|
|
182
208
|
case 'identity': return identity(args[0], flags);
|
|
209
|
+
case 'identify': return identify(args[0], flags);
|
|
183
210
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi pixel --help" for a list of valid subcommands.`);
|
|
184
211
|
}
|
|
185
212
|
}
|
package/dist/commands/queue.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function _parseDependsOn(raw: unknown): string[] | undefined;
|
|
|
8
8
|
export declare function create(name: string, flags: Flags): Promise<void>;
|
|
9
9
|
export declare function list(flags: Flags): Promise<void>;
|
|
10
10
|
export declare function get(name: string, flags: Flags): Promise<void>;
|
|
11
|
-
export declare function enqueue(name: string, flags: Flags): Promise<void>;
|
|
11
|
+
export declare function enqueue(name: string, flags: Flags, extraArgs?: string[]): Promise<void>;
|
|
12
12
|
export declare function jobs(name: string, flags: Flags): Promise<void>;
|
|
13
13
|
export declare function job(jobId: string, flags: Flags): Promise<void>;
|
|
14
14
|
export declare function del(name: string, flags: Flags): Promise<void>;
|
package/dist/commands/queue.js
CHANGED
|
@@ -101,10 +101,23 @@ export async function get(name, flags) {
|
|
|
101
101
|
info(`Max concurrency: ${q.max_concurrency}`);
|
|
102
102
|
}
|
|
103
103
|
// ── Jobs ─────────────────────────────────────────────────────────────────────
|
|
104
|
-
export async function enqueue(name, flags) {
|
|
104
|
+
export async function enqueue(name, flags, extraArgs = []) {
|
|
105
105
|
const config = requireConfig();
|
|
106
106
|
const orgId = requireOrg(flags, config, 'myapi queue enqueue <name> --payload <json> [--org <id>]');
|
|
107
107
|
requireArg(name, 'name', 'myapi queue enqueue <name> --payload <json>');
|
|
108
|
+
// The payload is a flag, not a positional. Passing it positionally —
|
|
109
|
+
// `queue enqueue fulfilment '{"order":1}'` — used to be discarded in
|
|
110
|
+
// silence, and the command still reported "✓ Job enqueued", leaving a job
|
|
111
|
+
// with an empty payload behind. Refuse rather than warn: a job that ran
|
|
112
|
+
// with no payload is worse than one that never ran, and the caller
|
|
113
|
+
// demonstrably meant to send data.
|
|
114
|
+
if (extraArgs.length > 0) {
|
|
115
|
+
const looksJson = extraArgs[0].trim().startsWith('{') || extraArgs[0].trim().startsWith('[');
|
|
116
|
+
error(`Unexpected extra argument "${extraArgs[0].slice(0, 40)}${extraArgs[0].length > 40 ? '…' : ''}".` +
|
|
117
|
+
(looksJson
|
|
118
|
+
? `\nThe payload is a flag, not a positional: myapi queue enqueue ${name} --payload '<json>'`
|
|
119
|
+
: `\nUsage: myapi queue enqueue <name> --payload <json>`));
|
|
120
|
+
}
|
|
108
121
|
let payload;
|
|
109
122
|
if (typeof flags.payload === 'string') {
|
|
110
123
|
try {
|
|
@@ -219,7 +232,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
219
232
|
case 'list': return list(flags);
|
|
220
233
|
case 'get': return get(args[0], flags);
|
|
221
234
|
case 'delete': return del(args[0], flags);
|
|
222
|
-
case 'enqueue': return enqueue(args[0], flags);
|
|
235
|
+
case 'enqueue': return enqueue(args[0], flags, args.slice(1));
|
|
223
236
|
case 'jobs': return jobs(args[0], flags);
|
|
224
237
|
case 'job': return job(args[0], flags);
|
|
225
238
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi queue --help" for a list of valid subcommands.`);
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import * as crypto from 'crypto';
|
|
5
|
-
import { loadConfig, saveConfig, addAccount, loadFullConfig } from '../config.js';
|
|
5
|
+
import { loadConfig, saveConfig, addAccount, loadFullConfig, CONFIG_FILE } from '../config.js';
|
|
6
6
|
import { info, success, banner } from '../output.js';
|
|
7
7
|
import { ask, confirm } from '../prompt.js';
|
|
8
8
|
import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
|
|
@@ -350,7 +350,11 @@ export async function setup(flags = {}) {
|
|
|
350
350
|
default_funnel: defaultFunnel,
|
|
351
351
|
is_anonymous: isAnonymous,
|
|
352
352
|
}, wantsSkills);
|
|
353
|
-
|
|
353
|
+
// Report where the config ACTUALLY went. This was hardcoded to
|
|
354
|
+
// `~/.myapi/config.json`, which became a lie the moment MYAPI_CONFIG_DIR
|
|
355
|
+
// shipped — a caller running isolated was told its config had landed in the
|
|
356
|
+
// operator's home directory.
|
|
357
|
+
success(`› Saved to ${CONFIG_FILE}`);
|
|
354
358
|
// Validate key and ensure org/funnel defaults are still correct.
|
|
355
359
|
// (resolveDefaults is non-fatal if the API call fails.)
|
|
356
360
|
try {
|
package/dist/commands/storage.js
CHANGED
|
@@ -18,6 +18,7 @@ export const EXPOSES = [
|
|
|
18
18
|
export const SCHEMA = {
|
|
19
19
|
org: 'string',
|
|
20
20
|
name: 'string',
|
|
21
|
+
'content-type': 'string',
|
|
21
22
|
};
|
|
22
23
|
// Mirrors UploadContentType in @myapihq/sdk + backend allowlist.
|
|
23
24
|
const EXT_TO_CT = {
|
|
@@ -63,10 +64,20 @@ async function upload(filePath, flags) {
|
|
|
63
64
|
const config = requireConfig();
|
|
64
65
|
const orgId = requireOrg(flags, config, 'myapi storage upload <file> [--name <name>] [--org <id>]');
|
|
65
66
|
requireArg(filePath, 'file', 'myapi storage upload <file> [--name <name>] [--org <id>]');
|
|
67
|
+
// The extension map is a CLI-side convenience for inferring Content-Type,
|
|
68
|
+
// NOT the API's allowlist — a user found the API happily accepting a CSV the
|
|
69
|
+
// CLI refused. Refusing on the client's behalf for something the server
|
|
70
|
+
// would take is the CLI overreaching, so --content-type is the escape hatch
|
|
71
|
+
// and the message says the limit is ours.
|
|
66
72
|
const ext = extname(filePath).toLowerCase();
|
|
67
|
-
const
|
|
73
|
+
const override = typeof flags['content-type'] === 'string' ? flags['content-type'] : undefined;
|
|
74
|
+
const contentType = override ?? EXT_TO_CT[ext];
|
|
68
75
|
if (!contentType) {
|
|
69
|
-
error(`
|
|
76
|
+
error(`Can't infer a content type for "${ext}".\n` +
|
|
77
|
+
`Known here: ${Object.keys(EXT_TO_CT).join(', ')}.\n\n` +
|
|
78
|
+
`→ This list is the CLI's, not the API's — the API may well accept your file.\n` +
|
|
79
|
+
` Force it: myapi storage upload ${filePath} --content-type <mime>\n` +
|
|
80
|
+
` Or host it and use: myapi storage ingest <url>`);
|
|
70
81
|
}
|
|
71
82
|
let data;
|
|
72
83
|
try {
|
|
@@ -9,4 +9,5 @@ export declare function update(id: string, flags: Flags): Promise<void>;
|
|
|
9
9
|
export declare function _validateForwardUrl(url: string): string | null;
|
|
10
10
|
export declare function del(id: string, flags: Flags): Promise<void>;
|
|
11
11
|
export declare function delivery(id: string, flags: Flags): Promise<void>;
|
|
12
|
+
export declare function deliveries(endpointArg: string | undefined, flags: Flags): Promise<void>;
|
|
12
13
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|