@myapihq/cli 2.17.0 → 2.18.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.
@@ -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.split(',').map(s => s.trim()).filter(Boolean);
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
- url: c.url || '(not deployed)',
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
  }
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-auth-api
3
- version: 1.0.0
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-07690e940717e8f65c195f9fbecdc1def9372edfd52dea19f6570227008d28ad
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) |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.17.0",
4
+ "version": "2.18.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": "tsc && rm -rf dist/skills && cp -r src/skills dist/skills",
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.17.0"
49
+ "@myapihq/sdk": "^2.18.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",