@myapihq/cli 2.17.0 → 2.19.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/auth-client-update.test.d.ts +1 -0
- package/dist/commands/auth-client-update.test.js +52 -0
- package/dist/commands/authproduct.d.ts +8 -1
- package/dist/commands/authproduct.js +74 -5
- package/dist/commands/container.js +4 -1
- package/dist/commands/feedback-capture.test.d.ts +1 -0
- package/dist/commands/feedback-capture.test.js +49 -0
- package/dist/commands/feedback.d.ts +8 -0
- package/dist/commands/feedback.js +61 -4
- package/dist/errors.js +6 -0
- package/dist/skills/my-auth-api/SKILL.md +8 -2
- package/dist/skills/my-feedback-api/SKILL.md +98 -81
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Unit tests for the `auth client update` helpers.
|
|
2
|
+
//
|
|
3
|
+
// The subcommand exists because its absence cost a customer a container
|
|
4
|
+
// generation: the PATCH route shipped on 19.08.2026 and the CLI still offered
|
|
5
|
+
// only list|create|delete|rotate, so changing a redirect URI meant delete and
|
|
6
|
+
// recreate — a new client_id, every deployed copy reconfigured, and two
|
|
7
|
+
// audiences accepted through the cutover.
|
|
8
|
+
//
|
|
9
|
+
// Two things here would be silent if they broke: a --redirect that quietly
|
|
10
|
+
// means "replace" while the customer reads "add", and a PATCH with nothing in
|
|
11
|
+
// it answering 200.
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { _parseRedirects, _updateInput } from './authproduct.js';
|
|
14
|
+
describe('_parseRedirects', () => {
|
|
15
|
+
it('takes every URI, comma-separated and trimmed', () => {
|
|
16
|
+
expect(_parseRedirects(' https://a.example.com/cb , https://b.example.com/cb '))
|
|
17
|
+
.toEqual(['https://a.example.com/cb', 'https://b.example.com/cb']);
|
|
18
|
+
});
|
|
19
|
+
it('drops a trailing comma rather than sending a blank URI', () => {
|
|
20
|
+
// The backend refuses an empty entry with INVALID_REDIRECT_URI, so passing
|
|
21
|
+
// one on turns a typo into a rejection the customer cannot read.
|
|
22
|
+
expect(_parseRedirects('https://a.example.com/cb,')).toEqual(['https://a.example.com/cb']);
|
|
23
|
+
});
|
|
24
|
+
it('is the same parser create uses', async () => {
|
|
25
|
+
// create and update must agree on what a redirect list is. They had two
|
|
26
|
+
// copies of this line; the second copy is the one that drifts.
|
|
27
|
+
const src = await import('node:fs').then(fs => fs.readFileSync(new URL('./authproduct.ts', import.meta.url), 'utf8'));
|
|
28
|
+
const splits = src.match(/redirect\.split\(/g) || [];
|
|
29
|
+
expect(splits.length, 'a second inline redirect parser has appeared').toBe(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('_updateInput', () => {
|
|
33
|
+
it('refuses a PATCH that would change nothing', () => {
|
|
34
|
+
const got = _updateInput('cli_123', '', '');
|
|
35
|
+
expect(got).toHaveProperty('error');
|
|
36
|
+
// The refusal names the next command, which is the house rule for refusals.
|
|
37
|
+
expect(got.error).toContain('myapi auth client update cli_123');
|
|
38
|
+
});
|
|
39
|
+
it('refuses a --redirect that parses to nothing', () => {
|
|
40
|
+
// ` , ` is not "keep what is there" — it replaces the list with an empty
|
|
41
|
+
// one, leaving a client that can never complete a sign-in.
|
|
42
|
+
expect(_updateInput('cli_123', ' , ', '')).toHaveProperty('error');
|
|
43
|
+
});
|
|
44
|
+
it('sends only the fields that were given', () => {
|
|
45
|
+
expect(_updateInput('cli_123', 'https://a.example.com/cb', ''))
|
|
46
|
+
.toEqual({ input: { redirect_uris: ['https://a.example.com/cb'] } });
|
|
47
|
+
expect(_updateInput('cli_123', '', 'Renamed'))
|
|
48
|
+
.toEqual({ input: { name: 'Renamed' } });
|
|
49
|
+
expect(_updateInput('cli_123', 'https://a.example.com/cb', 'Both'))
|
|
50
|
+
.toEqual({ input: { redirect_uris: ['https://a.example.com/cb'], name: 'Both' } });
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { auth as sdkAuth } from '@myapihq/sdk';
|
|
1
2
|
import type { FlagSchema } from '../flags.js';
|
|
2
3
|
import { type Flags } from '../helpers.js';
|
|
3
4
|
import type { Exposes } from '../exposes.js';
|
|
4
5
|
export declare const SCHEMA: FlagSchema;
|
|
5
6
|
export declare const EXPOSES: Exposes;
|
|
6
7
|
export declare const SUBCOMMAND_USAGE: Record<string, string>;
|
|
7
|
-
export declare const HELP = "Usage: myapi auth <subcommand>\n\nAuthentication for your app's end users \u2014 a managed OIDC identity provider\n(\u00E0 la Kinde/Auth0). One auth tenant per org; register OIDC clients (apps)\nagainst it; sign users in via the hosted login page or the JS SDK; verify\ntokens locally against the tenant JWKS.\n\nEnd users sign in with managed Google, email/password, or magic links \u2014 pick\nwhich via `tenant create --connections`. (Operator/account commands moved to\n`myapi account`.)\n\nSubcommands:\n client Register, list, delete, and rotate OIDC clients (your apps)\n domain Serve auth on your own domain (auth.acme.com)\n tenant Show or create your org's OIDC auth tenant (+ sign-in methods)\n usage Monthly active users (MAU) for the current period";
|
|
8
|
+
export declare const HELP = "Usage: myapi auth <subcommand>\n\nAuthentication for your app's end users \u2014 a managed OIDC identity provider\n(\u00E0 la Kinde/Auth0). One auth tenant per org; register OIDC clients (apps)\nagainst it; sign users in via the hosted login page or the JS SDK; verify\ntokens locally against the tenant JWKS.\n\nEnd users sign in with managed Google, email/password, or magic links \u2014 pick\nwhich via `tenant create --connections`. (Operator/account commands moved to\n`myapi account`.)\n\nSubcommands:\n client Register, list, update, delete, and rotate OIDC clients (your apps)\n domain Serve auth on your own domain (auth.acme.com)\n tenant Show or create your org's OIDC auth tenant (+ sign-in methods)\n usage Monthly active users (MAU) for the current period";
|
|
8
9
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
10
|
+
export declare function _parseRedirects(raw: string): string[];
|
|
11
|
+
export declare function _updateInput(clientId: string, redirect: string, name: string): {
|
|
12
|
+
error: string;
|
|
13
|
+
} | {
|
|
14
|
+
input: sdkAuth.UpdateClientInput;
|
|
15
|
+
};
|
|
@@ -22,6 +22,7 @@ export const EXPOSES = [
|
|
|
22
22
|
'POST /auth/orgs/{org_id}/clients',
|
|
23
23
|
'GET /auth/orgs/{org_id}/clients',
|
|
24
24
|
'GET /auth/orgs/{org_id}/usage',
|
|
25
|
+
'PATCH /auth/orgs/{org_id}/clients/{client_id}',
|
|
25
26
|
'DELETE /auth/orgs/{org_id}/clients/{client_id}',
|
|
26
27
|
'POST /auth/orgs/{org_id}/clients/{client_id}/rotate',
|
|
27
28
|
'POST /auth/orgs/{org_id}/domain',
|
|
@@ -57,12 +58,13 @@ Three steps: set → publish the ownership TXT → verify → publish the A reco
|
|
|
57
58
|
Flow: 'set' returns a TXT record to prove ownership; create it, then 'verify'.
|
|
58
59
|
Once verified, create the printed A record; TLS provisions automatically (~30 min)
|
|
59
60
|
and the domain becomes your issuer when active.`,
|
|
60
|
-
'client': `myapi auth client <list|create|delete|rotate> [--org <id>] [--json]
|
|
61
|
+
'client': `myapi auth client <list|create|update|delete|rotate> [--org <id>] [--json]
|
|
61
62
|
|
|
62
63
|
OIDC clients are the apps that authenticate against your tenant.
|
|
63
64
|
|
|
64
65
|
myapi auth client list
|
|
65
66
|
myapi auth client create --name "My App" --type spa --redirect https://app.example.com/callback
|
|
67
|
+
myapi auth client update <client_id> --redirect https://app.example.com/callback
|
|
66
68
|
myapi auth client delete <client_id> [--yes]
|
|
67
69
|
myapi auth client rotate <client_id>
|
|
68
70
|
|
|
@@ -71,6 +73,9 @@ OIDC clients are the apps that authenticate against your tenant.
|
|
|
71
73
|
--redirect <urls> Allowed redirect URIs, comma-separated (required for create).
|
|
72
74
|
Absolute https (or http://localhost for dev).
|
|
73
75
|
|
|
76
|
+
update Change the redirect URIs or the name. Keeps the same client_id and
|
|
77
|
+
secret, so nothing deployed has to be reconfigured. --redirect REPLACES
|
|
78
|
+
the list: pass every URI you want, comma-separated.
|
|
74
79
|
delete Revoke a client (irreversible; stops authenticating immediately).
|
|
75
80
|
rotate Re-issue a 'web' client's secret (shown once; old secret stops working).`,
|
|
76
81
|
};
|
|
@@ -86,7 +91,7 @@ which via \`tenant create --connections\`. (Operator/account commands moved to
|
|
|
86
91
|
\`myapi account\`.)
|
|
87
92
|
|
|
88
93
|
Subcommands:
|
|
89
|
-
client Register, list, delete, and rotate OIDC clients (your apps)
|
|
94
|
+
client Register, list, update, delete, and rotate OIDC clients (your apps)
|
|
90
95
|
domain Serve auth on your own domain (auth.acme.com)
|
|
91
96
|
tenant Show or create your org's OIDC auth tenant (+ sign-in methods)
|
|
92
97
|
usage Monthly active users (MAU) for the current period`;
|
|
@@ -287,10 +292,50 @@ async function domain(args, flags) {
|
|
|
287
292
|
}
|
|
288
293
|
error(`Unknown action "${action}". Use: myapi auth domain [show|set|verify|delete]`);
|
|
289
294
|
}
|
|
295
|
+
/* Redirect URIs, as the CLI accepts them: comma-separated, trimmed, empties
|
|
296
|
+
* dropped. A trailing comma is a typo, not a request for a blank URI — passing
|
|
297
|
+
* one through would earn an INVALID_REDIRECT_URI for something the customer did
|
|
298
|
+
* not mean to send.
|
|
299
|
+
*
|
|
300
|
+
* Shared by create and update rather than written twice: they must agree, and
|
|
301
|
+
* the second copy is the one that drifts. */
|
|
302
|
+
export function _parseRedirects(raw) {
|
|
303
|
+
return raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
/* _updateInput builds the PATCH body, or returns the refusal.
|
|
306
|
+
*
|
|
307
|
+
* Sending neither field is refused rather than treated as a no-op: a PATCH that
|
|
308
|
+
* changes nothing and answers 200 reads as a change that was applied, and the
|
|
309
|
+
* customer finds out at the next sign-in.
|
|
310
|
+
*
|
|
311
|
+
* Returns { error } or { input } so the decision is testable without running
|
|
312
|
+
* the command, which is how the rest of this CLI is tested. */
|
|
313
|
+
export function _updateInput(clientId, redirect, name) {
|
|
314
|
+
if (!redirect && !name) {
|
|
315
|
+
return {
|
|
316
|
+
error: 'Nothing to change. Pass --redirect, --name, or both:\n' +
|
|
317
|
+
` myapi auth client update ${clientId} --redirect https://app.example.com/callback`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const input = {};
|
|
321
|
+
if (redirect) {
|
|
322
|
+
const uris = _parseRedirects(redirect);
|
|
323
|
+
if (uris.length === 0) {
|
|
324
|
+
return {
|
|
325
|
+
error: '--redirect had no URI in it. It REPLACES the list, so an empty ' +
|
|
326
|
+
'one would leave a client that can never complete a sign-in.',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
input.redirect_uris = uris;
|
|
330
|
+
}
|
|
331
|
+
if (name)
|
|
332
|
+
input.name = name;
|
|
333
|
+
return { input };
|
|
334
|
+
}
|
|
290
335
|
async function client(args, flags) {
|
|
291
336
|
const action = args[0] || 'list';
|
|
292
337
|
const config = requireConfig();
|
|
293
|
-
const orgId = requireOrg(flags, config, 'myapi auth client <list|create|delete|rotate> [--org <id>]');
|
|
338
|
+
const orgId = requireOrg(flags, config, 'myapi auth client <list|create|update|delete|rotate> [--org <id>]');
|
|
294
339
|
if (action === 'list') {
|
|
295
340
|
const res = await sdkAuth.listClients(config.api_key, orgId);
|
|
296
341
|
if (flags.json) {
|
|
@@ -318,7 +363,7 @@ async function client(args, flags) {
|
|
|
318
363
|
error("--type must be 'spa' (public) or 'web' (confidential)");
|
|
319
364
|
if (!redirect)
|
|
320
365
|
error('--redirect is required (comma-separate multiple URIs)');
|
|
321
|
-
const redirect_uris = redirect
|
|
366
|
+
const redirect_uris = _parseRedirects(redirect);
|
|
322
367
|
const c = await sdkAuth.createClient(config.api_key, orgId, { name, type: type, redirect_uris });
|
|
323
368
|
if (flags.json) {
|
|
324
369
|
printJson(c);
|
|
@@ -361,6 +406,30 @@ async function client(args, flags) {
|
|
|
361
406
|
success(`Client deleted: ${clientId}`);
|
|
362
407
|
return;
|
|
363
408
|
}
|
|
409
|
+
if (action === 'update') {
|
|
410
|
+
const clientId = args[1];
|
|
411
|
+
if (!clientId)
|
|
412
|
+
error('Usage: myapi auth client update <client_id> [--redirect <uri,uri>] [--name <name>]');
|
|
413
|
+
// --redirect REPLACES the list, and the CLI says so in the help and again
|
|
414
|
+
// in the success line: a customer adding a second callback URL naturally
|
|
415
|
+
// reads this as "add", and that failure is silent — the first URI stops
|
|
416
|
+
// working and the sign-in that used it breaks at the next deploy, not here.
|
|
417
|
+
const built = _updateInput(clientId, flags.redirect || '', flags.name || '');
|
|
418
|
+
if ('error' in built)
|
|
419
|
+
error(built.error);
|
|
420
|
+
const input = built.input;
|
|
421
|
+
const c = await sdkAuth.updateClient(config.api_key, orgId, clientId, input);
|
|
422
|
+
if (flags.json) {
|
|
423
|
+
printJson(c);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
success(`Client updated: ${c.client_id || clientId}`);
|
|
427
|
+
if (c.name)
|
|
428
|
+
info(`Name: ${c.name}`);
|
|
429
|
+
info(`Redirects: ${(c.redirect_uris || []).join(', ')}`);
|
|
430
|
+
info('The client_id and secret are unchanged — nothing deployed needs reconfiguring.');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
364
433
|
if (action === 'rotate') {
|
|
365
434
|
const clientId = args[1];
|
|
366
435
|
if (!clientId)
|
|
@@ -379,5 +448,5 @@ async function client(args, flags) {
|
|
|
379
448
|
}
|
|
380
449
|
return;
|
|
381
450
|
}
|
|
382
|
-
error(`Unknown action "${action}". Use: myapi auth client <list|create|delete|rotate>`);
|
|
451
|
+
error(`Unknown action "${action}". Use: myapi auth client <list|create|update|delete|rotate>`);
|
|
383
452
|
}
|
|
@@ -75,7 +75,10 @@ function summarizeContainer(c) {
|
|
|
75
75
|
name: c.name,
|
|
76
76
|
type: c.type,
|
|
77
77
|
status: c.status,
|
|
78
|
-
|
|
78
|
+
// A job has no URL — it runs when triggered. '(not deployed)' in that
|
|
79
|
+
// column read as a broken deploy for a job that was working, which is the
|
|
80
|
+
// column reporting a fault where there is none.
|
|
81
|
+
url: c.url || (c.type === 'job' ? '— (job: runs on trigger)' : '(not deployed)'),
|
|
79
82
|
updated_at: c.updated_at ? formatDate(c.updated_at) : '',
|
|
80
83
|
};
|
|
81
84
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Capture flags send only what the operator named.
|
|
2
|
+
//
|
|
3
|
+
// The rule this file defends: an absent `capture` sub-field means "leave it
|
|
4
|
+
// alone", never "off". ImmoPilot's screens carry tenant names, IBANs and dates
|
|
5
|
+
// of birth; the widget photographs the viewport and reads the text of whatever
|
|
6
|
+
// the reporter points at. If `--no-screenshot` also sent `text: true` because
|
|
7
|
+
// the CLI helpfully filled in the object, it would turn one deliberate
|
|
8
|
+
// restriction into a different, undeclared permission — on exactly those pages.
|
|
9
|
+
//
|
|
10
|
+
// Both fields default to true server-side, so this is also what keeps every
|
|
11
|
+
// widget embedded before capture existed behaving as it always did.
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { _captureFrom } from './feedback.js';
|
|
14
|
+
const f = (o) => o;
|
|
15
|
+
describe('_captureFrom', () => {
|
|
16
|
+
it('is undefined when the operator said nothing', () => {
|
|
17
|
+
// Not `{}` — an empty object would still put `capture` in the PATCH body,
|
|
18
|
+
// and "Nothing to change" would stop being reachable.
|
|
19
|
+
expect(_captureFrom(f({}))).toBeUndefined();
|
|
20
|
+
expect(_captureFrom(f({ routes: '/a' }))).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it('sends only the field that was named', () => {
|
|
23
|
+
expect(_captureFrom(f({ 'no-screenshot': true }))).toEqual({ screenshot: false });
|
|
24
|
+
expect(_captureFrom(f({ 'no-text': true }))).toEqual({ text: false });
|
|
25
|
+
expect(_captureFrom(f({ screenshot: true }))).toEqual({ screenshot: true });
|
|
26
|
+
expect(_captureFrom(f({ text: true }))).toEqual({ text: true });
|
|
27
|
+
});
|
|
28
|
+
it('never infers the field it was not told about', () => {
|
|
29
|
+
// The whole point. Turning screenshots off must not carry an opinion about
|
|
30
|
+
// text, in either direction.
|
|
31
|
+
const only = _captureFrom(f({ 'no-screenshot': true }));
|
|
32
|
+
expect(only).not.toHaveProperty('text');
|
|
33
|
+
expect(Object.keys(only)).toEqual(['screenshot']);
|
|
34
|
+
});
|
|
35
|
+
it('carries both when both were named', () => {
|
|
36
|
+
expect(_captureFrom(f({ 'no-screenshot': true, 'no-text': true })))
|
|
37
|
+
.toEqual({ screenshot: false, text: false });
|
|
38
|
+
expect(_captureFrom(f({ screenshot: true, 'no-text': true })))
|
|
39
|
+
.toEqual({ screenshot: true, text: false });
|
|
40
|
+
});
|
|
41
|
+
it('lets the negative win when both spellings are passed', () => {
|
|
42
|
+
// `--screenshot --no-screenshot` is contradictory input. Resolving it to
|
|
43
|
+
// the restrictive answer is the safe direction: the cost of wrongly not
|
|
44
|
+
// capturing is a less useful bug report, the cost of wrongly capturing is
|
|
45
|
+
// a customer's records in a screenshot.
|
|
46
|
+
expect(_captureFrom(f({ screenshot: true, 'no-screenshot': true }))).toEqual({ screenshot: false });
|
|
47
|
+
expect(_captureFrom(f({ text: true, 'no-text': true }))).toEqual({ text: false });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { feedback as sdkFeedback } from '@myapihq/sdk';
|
|
1
2
|
import type { FlagSchema } from '../flags.js';
|
|
2
3
|
import { type Flags } from '../helpers.js';
|
|
3
4
|
import type { Exposes } from '../exposes.js';
|
|
4
5
|
export declare const EXPOSES: Exposes;
|
|
5
6
|
export declare const SCHEMA: FlagSchema;
|
|
7
|
+
/** Reads the paired --x / --no-x flags into a capture patch.
|
|
8
|
+
*
|
|
9
|
+
* Only the fields the operator actually named end up in the object. Sending a
|
|
10
|
+
* full `capture` would turn "stop taking screenshots" into "stop taking
|
|
11
|
+
* screenshots and start capturing text", silently changing a setting they did
|
|
12
|
+
* not mention — on a widget that may be pointed at tenant records. */
|
|
13
|
+
export declare function _captureFrom(flags: Flags): sdkFeedback.WidgetCapture | undefined;
|
|
6
14
|
export declare function list(flags: Flags): Promise<void>;
|
|
7
15
|
export declare function create(bodyArg: string | undefined, flags: Flags): Promise<void>;
|
|
8
16
|
export declare function resolve(id: string, flags: Flags): Promise<void>;
|
|
@@ -36,7 +36,41 @@ export const SCHEMA = {
|
|
|
36
36
|
'page-url': 'string',
|
|
37
37
|
route: 'string',
|
|
38
38
|
origins: 'string',
|
|
39
|
+
// Capture controls. Paired --x / --no-x rather than --x=true|false so that
|
|
40
|
+
// "unset" stays distinguishable from "set to false": the API treats an absent
|
|
41
|
+
// key as unchanged, and the CLI must be able to express that.
|
|
42
|
+
screenshot: 'boolean',
|
|
43
|
+
'no-screenshot': 'boolean',
|
|
44
|
+
text: 'boolean',
|
|
45
|
+
'no-text': 'boolean',
|
|
39
46
|
};
|
|
47
|
+
/** Reads the paired --x / --no-x flags into a capture patch.
|
|
48
|
+
*
|
|
49
|
+
* Only the fields the operator actually named end up in the object. Sending a
|
|
50
|
+
* full `capture` would turn "stop taking screenshots" into "stop taking
|
|
51
|
+
* screenshots and start capturing text", silently changing a setting they did
|
|
52
|
+
* not mention — on a widget that may be pointed at tenant records. */
|
|
53
|
+
export function _captureFrom(flags) {
|
|
54
|
+
const cap = {};
|
|
55
|
+
if (flags['no-screenshot'])
|
|
56
|
+
cap.screenshot = false;
|
|
57
|
+
else if (flags.screenshot)
|
|
58
|
+
cap.screenshot = true;
|
|
59
|
+
if (flags['no-text'])
|
|
60
|
+
cap.text = false;
|
|
61
|
+
else if (flags.text)
|
|
62
|
+
cap.text = true;
|
|
63
|
+
return Object.keys(cap).length ? cap : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** "on" / "off" for a resolved capture field. The server returns both fields
|
|
66
|
+
* resolved, so an undefined here means an older backend that did not answer —
|
|
67
|
+
* say so rather than printing "off" for something that is on. */
|
|
68
|
+
function captureLine(c) {
|
|
69
|
+
if (!c)
|
|
70
|
+
return '(not reported by this backend)';
|
|
71
|
+
const s = (v) => v === undefined ? '?' : v ? 'on' : 'off';
|
|
72
|
+
return `screenshot ${s(c.screenshot)}, text ${s(c.text)}`;
|
|
73
|
+
}
|
|
40
74
|
// The platform's enum, checked against the schema rather than invented.
|
|
41
75
|
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
42
76
|
// A selector says `div#install > button.save`. The context says the button read
|
|
@@ -96,6 +130,15 @@ export async function list(flags) {
|
|
|
96
130
|
const pointing = describeTarget(i);
|
|
97
131
|
if (pointing)
|
|
98
132
|
info(` pointing at ${pointing}`);
|
|
133
|
+
// Whoever the host app named via window.__myapiFeedback.identify(). Opaque
|
|
134
|
+
// to us — their id for their user, never resolved against anything.
|
|
135
|
+
//
|
|
136
|
+
// Printed only when set: an anonymous visitor is still the normal case, and
|
|
137
|
+
// a blank `from` reads as a failure to capture rather than nobody to name.
|
|
138
|
+
// Every item filed before 2026-08-19 carries '' — the column existed and
|
|
139
|
+
// nothing ever wrote it — so most history will show no line here.
|
|
140
|
+
if (i.end_user_ref)
|
|
141
|
+
info(` from ${i.end_user_ref}`);
|
|
99
142
|
const trace = Array.isArray(i.trace) ? i.trace : [];
|
|
100
143
|
if (trace.length) {
|
|
101
144
|
// Errors and failed requests are the highest-signal part — for most
|
|
@@ -244,7 +287,7 @@ export async function widget(sub, arg, flags) {
|
|
|
244
287
|
return;
|
|
245
288
|
}
|
|
246
289
|
if (sub === 'update') {
|
|
247
|
-
requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"]');
|
|
290
|
+
requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"] [--no-screenshot] [--no-text]');
|
|
248
291
|
const patch = {};
|
|
249
292
|
const csv = (v) => typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
250
293
|
if (flags.routes !== undefined)
|
|
@@ -282,7 +325,7 @@ export async function widget(sub, arg, flags) {
|
|
|
282
325
|
info('Feedback already collected through it is kept.');
|
|
283
326
|
return;
|
|
284
327
|
}
|
|
285
|
-
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>');
|
|
328
|
+
error('Usage: myapi feedback widget create <name> [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget list\n myapi feedback widget update <id> [--routes <list>] [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget revoke <id>');
|
|
286
329
|
}
|
|
287
330
|
export const SUBCOMMAND_USAGE = {
|
|
288
331
|
'delete': `myapi feedback delete <id> [--yes] [--org <id>]
|
|
@@ -300,10 +343,24 @@ Newest first. \`total\` is the number of matches, not the page size.`,
|
|
|
300
343
|
'resolve': 'myapi feedback resolve <id> [--org <id>]',
|
|
301
344
|
'widget': `myapi feedback widget list
|
|
302
345
|
myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
|
|
346
|
+
myapi feedback widget update <id> [--routes <list>] [--no-screenshot] [--no-text] [--org <id>]
|
|
303
347
|
myapi feedback widget revoke <id> [--yes] [--org <id>]
|
|
304
348
|
|
|
305
349
|
The key a widget mints is PUBLIC — it ships in page source and authenticates
|
|
306
|
-
nobody. --origins stops another site posting through it
|
|
350
|
+
nobody. --origins stops another site posting through it.
|
|
351
|
+
|
|
352
|
+
What a report may collect (both on by default):
|
|
353
|
+
--no-screenshot Stop photographing the viewport.
|
|
354
|
+
--no-text Keep the SHAPE of the element pointed at — tag, role, id,
|
|
355
|
+
data-feedback-id — and drop the words inside it. On a table
|
|
356
|
+
row that is "row, id row-8" instead of the customer's name.
|
|
357
|
+
Pass --screenshot / --text to turn one back on. Flags you omit are left alone,
|
|
358
|
+
so turning one off never silently changes the other. Changes reach live
|
|
359
|
+
browsers within ~5 minutes, with no redeploy of your site.
|
|
360
|
+
|
|
361
|
+
To exempt one panel rather than the whole widget, mark it data-feedback-ignore
|
|
362
|
+
in your page: it is skipped in the element context AND blacked out in the
|
|
363
|
+
screenshot.`,
|
|
307
364
|
};
|
|
308
365
|
export async function run(subcommand, args, flags) {
|
|
309
366
|
if (!subcommand || (flags.help && !subcommand)) {
|
|
@@ -320,7 +377,7 @@ Subcommands:
|
|
|
320
377
|
widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
|
|
321
378
|
widget list List widgets with the script tag to paste
|
|
322
379
|
widget revoke <id> Revoke a widget key; collected feedback is kept
|
|
323
|
-
widget update <id> Change routes/origins/theme, keeping the same key
|
|
380
|
+
widget update <id> Change routes/origins/theme/capture, keeping the same key
|
|
324
381
|
|
|
325
382
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
326
383
|
return;
|
package/dist/errors.js
CHANGED
|
@@ -26,6 +26,12 @@ export const ERROR_MESSAGES = {
|
|
|
26
26
|
invalid_org: 'Invalid organization.',
|
|
27
27
|
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
28
28
|
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
|
29
|
+
// The backend refuses an unknown key or a non-boolean rather than ignoring it
|
|
30
|
+
// — `{"screenshots": false}` (plural) is a 422, not a no-op. That is
|
|
31
|
+
// deliberate: a capture setting accepted and ignored reads as "screenshots
|
|
32
|
+
// are off" while screenshots keep being taken, on pages that may carry
|
|
33
|
+
// customer records.
|
|
34
|
+
INVALID_CAPTURE: 'capture accepts only `screenshot` and `text`, both booleans. Check the spelling — an unknown key is refused, not ignored.',
|
|
29
35
|
BODY_TOO_LARGE: 'The request is over 64 KB — usually an oversized feedback `trace`. Nothing was saved. This used to surface as INVALID_JSON, which sent people looking for a syntax error they did not have.',
|
|
30
36
|
INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
|
|
31
37
|
INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-auth-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.1.0
|
|
4
4
|
description: >
|
|
5
5
|
Add authentication to apps you build on MyAPI — a managed OIDC identity provider for your app's END USERS (à la Kinde/Auth0). One auth tenant per org; register OIDC clients; sign users in with managed Google or the hosted login page; verify RS256 tokens against the tenant JWKS.
|
|
6
6
|
triggers: [auth, authentication, login, sign-in, oidc, oauth, jwt, jwks, sso, google sign-in, user accounts, identity provider, kinde, auth0, clerk]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-e6bf7db60560fa6702d7864aaf515a524d3f79cf3eaafe265790005673e3ae29
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyAuthAPI
|
|
@@ -38,6 +38,11 @@ its clients.
|
|
|
38
38
|
client (no secret; for browser/SPA/mobile). `type web` is confidential and
|
|
39
39
|
returns a `client_secret` **once** — store it immediately. `--redirect` lists
|
|
40
40
|
allowed callback URIs (absolute https, or http://localhost for dev).
|
|
41
|
+
`auth client update <id> --redirect <uri,uri>` changes them later, keeping the
|
|
42
|
+
same `client_id` and secret. Do NOT delete and recreate to add a callback
|
|
43
|
+
URL — that mints a new `client_id`, so every deployed copy of the app has to
|
|
44
|
+
be reconfigured and the cutover has to accept two audiences at once.
|
|
45
|
+
`--redirect` REPLACES the list: pass every URI you want, not just the new one.
|
|
41
46
|
- **Usage** — `auth usage` shows monthly active users (auth is billed per MAU).
|
|
42
47
|
- **Custom domain** — serve auth on `auth.acme.com`. Three steps: `auth domain
|
|
43
48
|
set --domain auth.acme.com` prints a **TXT ownership challenge**; publish it,
|
|
@@ -62,6 +67,7 @@ CLI is only the management surface.
|
|
|
62
67
|
| `myapi auth tenant create` | Create/enable the tenant (`--connections google,password,magic`; `--theme <json>`) |
|
|
63
68
|
| `myapi auth client list` | List the OIDC clients (apps) registered to your tenant |
|
|
64
69
|
| `myapi auth client create` | Register an OIDC client (`--name`, `--type spa\|web`, `--redirect`) |
|
|
70
|
+
| `myapi auth client update <id>` | Change redirect URIs (`--redirect`, replaces the list) or `--name`; same client_id and secret |
|
|
65
71
|
| `myapi auth client delete <id>` | Revoke a client (irreversible); `--yes` to skip the confirm |
|
|
66
72
|
| `myapi auth client rotate <id>` | Re-issue a `web` client's secret (shown once) |
|
|
67
73
|
| `myapi auth usage` | Monthly active users (MAU) for the current period (`as_of` shows freshness) |
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-feedback-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.6.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-
|
|
7
|
+
checksum: sha256-f8be1cee17e2dc5e1dcf2310cf18d275982d54460af26a0b642fc46bd8c4691e
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyFeedbackAPI
|
|
@@ -18,111 +18,129 @@ Two halves: a **widget key** a page embeds, and the **items** it produces.
|
|
|
18
18
|
|
|
19
19
|
### The widget key is public, and that is the point
|
|
20
20
|
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
`widget create <name>` mints a key that ships in your page source. It is **not a
|
|
22
|
+
secret** — it names your org so a visitor can submit without signing in, and
|
|
23
|
+
authenticates nobody.
|
|
24
24
|
|
|
25
|
-
Treating it as a credential is the mistake
|
|
26
|
-
|
|
27
|
-
cannot post through it:
|
|
25
|
+
Treating it as a credential is the mistake: people hide it and the widget cannot
|
|
26
|
+
work. It needs `--origins`, so another site cannot post through it:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
29
|
myapi feedback widget create site --origins app.example.com,example.com
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
Revoke with `myapi feedback widget revoke <id
|
|
34
|
-
feedback
|
|
32
|
+
Revoke with `myapi feedback widget revoke <id>`: submissions stop immediately,
|
|
33
|
+
collected feedback is kept.
|
|
35
34
|
|
|
36
35
|
### Embedding it: one script tag
|
|
37
36
|
|
|
38
|
-
The platform hosts the widget, compiled per key with
|
|
39
|
-
|
|
37
|
+
The platform hosts the widget, compiled per key with its routes and theme baked
|
|
38
|
+
in. The intended way in — you build no UI:
|
|
40
39
|
|
|
41
40
|
```html
|
|
42
41
|
<script src="https://api.myapihq.com/feedback/in/<widget_key>/widget.js" async></script>
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
It
|
|
46
|
-
|
|
47
|
-
the change reaches browsers within ~5 minutes with no redeploy of your site.
|
|
44
|
+
It shows only on the widget's routes — set them with `widget update <id>
|
|
45
|
+
--routes /app,/app/**`; live in ~5 min, no redeploy.
|
|
48
46
|
|
|
49
|
-
The person points at an element or drags a region
|
|
47
|
+
The person points at an element or drags a region; it sends:
|
|
50
48
|
|
|
51
49
|
- `kind`, `body`, `page_url`, `route`, `viewport`
|
|
52
|
-
- `target_selector` and `target_region` `{x,y,w,h}` —
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- `target_context` — what the element *was*: its text, role, the heading above it
|
|
50
|
+
- `target_selector` and `target_region` `{x,y,w,h}` — on **every** report: the
|
|
51
|
+
element's bounding box for a click, the dragged box for a drag
|
|
52
|
+
- `target_context` — what the element *was*: text, role, the heading above it
|
|
56
53
|
- `trace` — the last events before the report
|
|
57
54
|
|
|
58
|
-
A wrong key
|
|
59
|
-
|
|
60
|
-
in your app.
|
|
55
|
+
A wrong key cannot break the page: the response is `application/javascript`
|
|
56
|
+
with a comment naming the fix, so a typo throws no syntax error.
|
|
61
57
|
|
|
62
|
-
**Screenshots are automatic — you
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
deliberate: an image that fails to render or upload never costs the report.
|
|
58
|
+
**Screenshots are automatic — you build no capture step.** The widget attaches
|
|
59
|
+
the picture as a second call after the report is saved, so an image that fails
|
|
60
|
+
to render never costs the report.
|
|
66
61
|
|
|
67
|
-
The image is **
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
JPEG or WebP up to 3 MB; over that the feedback is kept without the picture.
|
|
62
|
+
The image is **private in your org's assets**; listing returns a **signed link
|
|
63
|
+
good for ~an hour** (`screenshot_url`) — re-list rather than persisting it.
|
|
64
|
+
PNG/JPEG/WebP up to 3 MB; over that the report is kept without it.
|
|
71
65
|
|
|
72
66
|
### Submitting by hand (only if you cannot use the script)
|
|
73
67
|
|
|
74
|
-
`POST https://api.myapihq.com/feedback/in/<widget_key>` — no auth, no SDK
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```js
|
|
78
|
-
await fetch(`https://api.myapihq.com/feedback/in/${WIDGET_KEY}`, {
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers: { 'Content-Type': 'application/json' },
|
|
81
|
-
body: JSON.stringify({ kind: 'bug', body: text, page_url: location.href }),
|
|
82
|
-
});
|
|
83
|
-
```
|
|
68
|
+
`POST https://api.myapihq.com/feedback/in/<widget_key>` — no auth, no SDK, same
|
|
69
|
+
fields the widget sends (`kind`, `body`, `page_url`, …).
|
|
84
70
|
|
|
85
71
|
Errors: `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys
|
|
86
72
|
cannot be probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`,
|
|
87
|
-
`BODY_REQUIRED`, `BODY_TOO_LONG` (
|
|
88
|
-
`
|
|
89
|
-
`trace`). Nothing is saved in either case. The key only ever writes.
|
|
73
|
+
`BODY_REQUIRED`, `BODY_TOO_LONG` (>8000 chars), `BODY_TOO_LARGE` (>64 KB, usually
|
|
74
|
+
an oversized `trace`). Nothing is saved; the key only writes.
|
|
90
75
|
|
|
91
76
|
### Kind is a claim, not a guess
|
|
92
77
|
|
|
93
|
-
`--kind` is one of `bug`, `issue`, `suggestion`, and it is **what the
|
|
94
|
-
|
|
95
|
-
they believe the product is broken — which is a different and more urgent
|
|
78
|
+
`--kind` is one of `bug`, `issue`, `suggestion`, and it is **what the reporter
|
|
79
|
+
says it is**. A `bug` means they believe the product is broken — a more urgent
|
|
96
80
|
signal than a wish for something new. Do not re-classify from the wording.
|
|
97
81
|
|
|
98
|
-
Classification and duplicate-grouping
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
Classification and duplicate-grouping are designed but not built, so treat
|
|
83
|
+
`kind` as the person's own label, not a processed signal.
|
|
84
|
+
|
|
85
|
+
### Controlling what a report contains
|
|
86
|
+
|
|
87
|
+
A report carries a screenshot of the viewport plus the text of the element
|
|
88
|
+
pointed at. On a page of customer records, that is the page. Both on by default:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
myapi feedback widget update <id> --no-screenshot # stop photographing
|
|
92
|
+
myapi feedback widget update <id> --no-text # keep SHAPE, drop words
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`--no-text` keeps tag, role, id and `data-feedback-id`: on a table row, "row, id
|
|
96
|
+
`row-8`" not the tenant's name. `--screenshot` / `--text` turn one back on.
|
|
97
|
+
Omitted flags are left alone, so one off never changes the other. Live in ~5 min.
|
|
98
|
+
|
|
99
|
+
**In your page**, two levers only you control:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
window.__myapiFeedback.identify("your-user-id"); // when you learn who it is
|
|
103
|
+
window.__myapiFeedback.identify(); // on sign-out
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Callable any time, even long after load. Your id for your user — opaque to us,
|
|
107
|
+
never resolved, posted to a public endpoint: a label, not authentication. Shows
|
|
108
|
+
as `from` in `feedback list`.
|
|
109
|
+
|
|
110
|
+
Mark an element `data-feedback-ignore` to skip it in the context AND black it out
|
|
111
|
+
in the screenshot.
|
|
112
|
+
|
|
113
|
+
| Need | Lever |
|
|
114
|
+
|---|---|
|
|
115
|
+
| No pictures at all | `--no-screenshot` |
|
|
116
|
+
| Pictures, no words | `--no-text` |
|
|
117
|
+
| All but one panel | `data-feedback-ignore` |
|
|
118
|
+
| Different rules per page | two widgets, two route sets |
|
|
119
|
+
|
|
120
|
+
The last row is usually right: capture off on record-bearing routes, on
|
|
121
|
+
elsewhere — not one global switch.
|
|
101
122
|
|
|
102
123
|
### Reading it back
|
|
103
124
|
|
|
104
125
|
Each item carries what was pointed at, how they got there, and a picture.
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
through.
|
|
109
|
-
|
|
110
|
-
Two
|
|
111
|
-
a
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`myapi feedback resolve <id>` closes an item. An unknown id answers the same
|
|
124
|
-
way as an already-resolved one, so a success is not proof the item existed —
|
|
125
|
-
that is deliberate, so ids cannot be probed across orgs.
|
|
126
|
+
`list` summarises it — the element's own text rather than its CSS selector,
|
|
127
|
+
counts of steps/errors/failed requests, whether a screenshot exists, and `from`
|
|
128
|
+
when the page called `identify()`. `--trace` expands the events; `--json` passes
|
|
129
|
+
everything through.
|
|
130
|
+
|
|
131
|
+
Two not to misread: **`trace[].t` is ms since page load**, not clock time — only
|
|
132
|
+
a delta means anything. And **`target_context.filled` says an input had a value,
|
|
133
|
+
never what**.
|
|
134
|
+
|
|
135
|
+
`delete` removes the screenshot with the report — what matters is somebody
|
|
136
|
+
erasing a report because of what is in the picture.
|
|
137
|
+
|
|
138
|
+
`list` is newest first, filterable by `--kind`/`--status`, paged with `--limit`
|
|
139
|
+
/ `--offset`. **`total` is the match count, not the page size**; `has_more` flags
|
|
140
|
+
truncation — both describe the whole match set.
|
|
141
|
+
|
|
142
|
+
`resolve <id>` closes an item. An unknown id answers like an already-resolved
|
|
143
|
+
one, so success is not proof it existed — deliberate, so ids cannot be probed.
|
|
126
144
|
<!-- llm:end -->
|
|
127
145
|
|
|
128
146
|
## Commands
|
|
@@ -135,7 +153,7 @@ that is deliberate, so ids cannot be probed across orgs.
|
|
|
135
153
|
| `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
|
|
136
154
|
| `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
|
|
137
155
|
| `myapi feedback widget list` | List widgets, each with the script tag to paste |
|
|
138
|
-
| `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`,
|
|
156
|
+
| `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`, look (`--accent`/`--position`/`--label`) or capture (`--no-screenshot`/`--no-text`); same key |
|
|
139
157
|
| `myapi feedback widget revoke <id>` | Revoke a key; collected feedback is kept |
|
|
140
158
|
<!-- generated:end -->
|
|
141
159
|
|
|
@@ -149,16 +167,16 @@ myapi feedback widget create marketing --origins example.com,www.example.com
|
|
|
149
167
|
# 2. See your widgets and the exact tag to paste
|
|
150
168
|
myapi feedback widget list
|
|
151
169
|
|
|
152
|
-
# 3. Change where it appears
|
|
170
|
+
# 3. Change where it appears, or what it may collect — same key either way
|
|
153
171
|
myapi feedback widget update <id> --routes /app,/app/**
|
|
172
|
+
myapi feedback widget update <id> --no-screenshot --no-text # record-bearing routes
|
|
154
173
|
|
|
155
|
-
# 4. Read what came back, newest first
|
|
174
|
+
# 4. Read what came back, newest first (`from` shows if the page identify()d)
|
|
156
175
|
myapi feedback list --status open
|
|
157
176
|
myapi feedback list --kind bug --limit 20
|
|
158
177
|
|
|
159
178
|
# 5. Record something yourself (support call, your own testing)
|
|
160
|
-
myapi feedback create "checkout 500s on
|
|
161
|
-
--route /checkout
|
|
179
|
+
myapi feedback create "checkout 500s on retry" --kind bug --route /checkout
|
|
162
180
|
|
|
163
181
|
# 6. Close it — or delete it, which also removes its screenshot
|
|
164
182
|
myapi feedback resolve <id>
|
|
@@ -168,12 +186,11 @@ myapi feedback delete <id> --yes
|
|
|
168
186
|
|
|
169
187
|
## Notes
|
|
170
188
|
|
|
171
|
-
- The widget key is public by design — restrict
|
|
172
|
-
- `resolve` is idempotent and deliberately indistinguishable from an unknown id.
|
|
189
|
+
- The widget key is public by design — restrict with `--origins`, don't hide it.
|
|
173
190
|
- Feedback survives widget revocation.
|
|
174
|
-
- **`resolve` keeps, `delete` erases.** A public inbox collects spam and the
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
- **`resolve` keeps, `delete` erases.** A public inbox collects spam and the odd
|
|
192
|
+
report with personal details in it; `delete` is how those leave. Both are
|
|
193
|
+
idempotent and answer the same for an unknown id, so ids cannot be probed.
|
|
177
194
|
|
|
178
195
|
## HTTP (from deployed code)
|
|
179
196
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.19.0",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"prebuild": "node scripts/copy-skills.js",
|
|
21
|
-
"build": "
|
|
21
|
+
"build": "rm -rf dist && tsc && cp -r src/skills dist/skills",
|
|
22
22
|
"dev": "tsc --watch",
|
|
23
23
|
"test": "vitest run src test/scripts",
|
|
24
24
|
"test:smoke": "npm run build && vitest run src test/smoke test/scripts",
|
|
@@ -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.
|
|
49
|
+
"@myapihq/sdk": "^2.19.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|