@myapihq/cli 2.16.4 → 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.
- 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 +70 -1
- package/dist/sdk-container.test.js +5 -1
- package/dist/skills/my-auth-api/SKILL.md +8 -2
- package/dist/skills/my-container-api/SKILL.md +14 -9
- 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
|
}
|
|
@@ -11,6 +11,7 @@ export const EXPOSES = [
|
|
|
11
11
|
'GET /container/orgs/{org_id}/containers',
|
|
12
12
|
'GET /container/orgs/{org_id}/containers/{id}',
|
|
13
13
|
'DELETE /container/orgs/{org_id}/containers/{id}',
|
|
14
|
+
'PATCH /container/orgs/{org_id}/containers/{id}',
|
|
14
15
|
'POST /container/orgs/{org_id}/containers/{id}/deploy',
|
|
15
16
|
'GET /container/orgs/{org_id}/containers/{id}/logs',
|
|
16
17
|
'GET /container/orgs/{org_id}/containers/{id}/revisions',
|
|
@@ -28,6 +29,7 @@ export const SCHEMA = {
|
|
|
28
29
|
'max-instances': 'number',
|
|
29
30
|
port: 'number',
|
|
30
31
|
env: 'string',
|
|
32
|
+
unset: 'string',
|
|
31
33
|
tail: 'number',
|
|
32
34
|
scope: 'string',
|
|
33
35
|
'no-promote': 'boolean',
|
|
@@ -73,7 +75,10 @@ function summarizeContainer(c) {
|
|
|
73
75
|
name: c.name,
|
|
74
76
|
type: c.type,
|
|
75
77
|
status: c.status,
|
|
76
|
-
|
|
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)'),
|
|
77
82
|
updated_at: c.updated_at ? formatDate(c.updated_at) : '',
|
|
78
83
|
};
|
|
79
84
|
}
|
|
@@ -587,6 +592,18 @@ Examples:
|
|
|
587
592
|
myapi container create --name api --port 8080
|
|
588
593
|
myapi container create --name nightly --type job --cron "0 3 * * *"
|
|
589
594
|
myapi container create --name queue-worker --type worker --memory 1Gi`,
|
|
595
|
+
'env': `myapi container env <id> --env KEY=VALUE[,K2=V2] [--unset KEY3[,KEY4]] [--org <id>] [--json]
|
|
596
|
+
|
|
597
|
+
Change environment variables on a container that already exists, and roll a new
|
|
598
|
+
revision on the SAME image so they take effect. No rebuild, and the container
|
|
599
|
+
keeps its URL, its scoped key and its custom domain.
|
|
600
|
+
|
|
601
|
+
This is a MERGE. Variables you do not name keep their values, so adding one
|
|
602
|
+
entry to an allowlist does not mean resending every secret. --unset removes.
|
|
603
|
+
|
|
604
|
+
Examples:
|
|
605
|
+
myapi container env c-123 --env ALLOWED=a@x.com,b@y.com
|
|
606
|
+
myapi container env c-123 --env LOG_LEVEL=debug --unset LEGACY_FLAG`,
|
|
590
607
|
'deploy': `myapi container deploy <id> <image-ref> [--org <id>] [--json]
|
|
591
608
|
myapi container deploy <id> --source <dir|tar.gz> [--org <id>] [--json]
|
|
592
609
|
|
|
@@ -673,6 +690,7 @@ Subcommands:
|
|
|
673
690
|
deploy <id> <image> Ship a pre-built image (or --source <dir|tar> to build) and go live
|
|
674
691
|
(--smoke to verify before promoting; --no-promote to hold it back)
|
|
675
692
|
domain <id> <domain> Bind a custom domain (--remove to unbind)
|
|
693
|
+
env <id> Change environment variables (merge; rolls a new revision)
|
|
676
694
|
get <id> Inspect a container
|
|
677
695
|
list List containers in your org
|
|
678
696
|
logs <id> Show recent runtime logs (--tail <n>, --scope all) — see build-logs for build failures
|
|
@@ -691,8 +709,59 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
691
709
|
info(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for the list.`);
|
|
692
710
|
return;
|
|
693
711
|
}
|
|
712
|
+
/**
|
|
713
|
+
* `myapi container env <id> --env K=V[,K2=V2] [--unset K3[,K4]]`
|
|
714
|
+
*
|
|
715
|
+
* Until 2026-08-19 the only way to change a variable was to delete the
|
|
716
|
+
* container and create another, losing the URL, the scoped key and the custom
|
|
717
|
+
* domain. The skill said to do exactly that, and a customer went through three
|
|
718
|
+
* generations of one container in a week because of it.
|
|
719
|
+
*/
|
|
720
|
+
async function envCmd(id, flags) {
|
|
721
|
+
const config = requireConfig();
|
|
722
|
+
const orgId = requireOrg(flags, config, 'myapi container env <id> --env K=V [--unset K2]');
|
|
723
|
+
if (!id)
|
|
724
|
+
error('Missing id.\nUsage: myapi container env <id> --env KEY=VALUE[,K2=V2] [--unset KEY3[,KEY4]]');
|
|
725
|
+
const env = {};
|
|
726
|
+
if (typeof flags.env === 'string') {
|
|
727
|
+
const parsed = _parseEnv(flags.env);
|
|
728
|
+
if (typeof parsed === 'string')
|
|
729
|
+
error(parsed);
|
|
730
|
+
Object.assign(env, parsed);
|
|
731
|
+
}
|
|
732
|
+
if (typeof flags.unset === 'string') {
|
|
733
|
+
for (const k of flags.unset.split(',').map(s => s.trim()).filter(Boolean))
|
|
734
|
+
env[k] = null;
|
|
735
|
+
}
|
|
736
|
+
if (Object.keys(env).length === 0) {
|
|
737
|
+
error('Nothing to change. Pass --env KEY=VALUE to set one, or --unset KEY to remove one.\n' +
|
|
738
|
+
'This is a merge: variables you do not name keep their values.');
|
|
739
|
+
}
|
|
740
|
+
info('› Updating environment…');
|
|
741
|
+
const res = await sdkContainer.updateContainerEnv(config.api_key, orgId, id, env);
|
|
742
|
+
if (flags.json) {
|
|
743
|
+
printJson(res);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (res.set?.length)
|
|
747
|
+
info(` set: ${res.set.join(', ')}`);
|
|
748
|
+
if (res.removed?.length)
|
|
749
|
+
info(` removed: ${res.removed.join(', ')}`);
|
|
750
|
+
// Say plainly whether the running container has the new values, because
|
|
751
|
+
// "stored" and "live" are different states and only one of them is what the
|
|
752
|
+
// caller asked for.
|
|
753
|
+
if (res.applied) {
|
|
754
|
+
success(`Redeployed on the same image — the new values are live${res.revision ? ` (${res.revision})` : ''}.`);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
info(' Stored. This container has no deployment yet, so the values apply on your first deploy.');
|
|
758
|
+
}
|
|
759
|
+
if (res.env)
|
|
760
|
+
info(` ${Object.keys(res.env).length} variable(s) now set: ${Object.keys(res.env).sort().join(', ')}`);
|
|
761
|
+
}
|
|
694
762
|
switch (subcommand) {
|
|
695
763
|
case 'create': return create(args[0], flags);
|
|
764
|
+
case 'env': return envCmd(args[0], flags);
|
|
696
765
|
case 'build-logs': return buildLogs(args[0], flags);
|
|
697
766
|
case 'deploy': return deploy(args[0], args[1], flags);
|
|
698
767
|
case 'list': return list(flags);
|
|
@@ -144,12 +144,16 @@ describe('container.getContainerLogs', () => {
|
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
describe('container.EXPOSES', () => {
|
|
147
|
-
it('covers the
|
|
147
|
+
it('covers the 12 container endpoints', () => {
|
|
148
148
|
expect(container.EXPOSES).toEqual([
|
|
149
149
|
'POST /container/orgs/{org_id}/containers',
|
|
150
150
|
'GET /container/orgs/{org_id}/containers',
|
|
151
151
|
'GET /container/orgs/{org_id}/containers/{id}',
|
|
152
152
|
'DELETE /container/orgs/{org_id}/containers/{id}',
|
|
153
|
+
// Added 2026-08-19. Env is the one create-time setting that is NOT
|
|
154
|
+
// immutable: PATCH merges the map and rolls a revision on the same
|
|
155
|
+
// image, so the container keeps its URL, scoped key and custom domain.
|
|
156
|
+
'PATCH /container/orgs/{org_id}/containers/{id}',
|
|
153
157
|
'POST /container/orgs/{org_id}/containers/{id}/deploy',
|
|
154
158
|
// Added 2026-07-28. Promote and rollback are ONE endpoint: naming a
|
|
155
159
|
// revision promotes it, omitting one rolls back to the previous ready
|
|
@@ -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,21 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-container-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.1.0
|
|
4
4
|
description: >
|
|
5
5
|
Run containers on demand — long-running services, background workers, and scheduled jobs. The heavier-duty sibling of edge functions, for native deps and long execution.
|
|
6
6
|
triggers: [container, cloud run, dynamic app, custom domain app, service, worker, scheduled job, deploy container, docker image]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-0a83428114cbc7065d1a4db4f8a0cf39ff965c51acac78f5eeff159345a08da2
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyContainerAPI
|
|
11
11
|
|
|
12
|
-
A container runs a pre-built image on managed cloud infrastructure. Three types: a **service** (HTTP server, scales to zero), a **worker** (always-on background process), or a **job** (runs to completion — the only type that takes a cron schedule).
|
|
12
|
+
A container runs a pre-built image on managed cloud infrastructure. Three types: a **service** (HTTP server, scales to zero), a **worker** (always-on background process), or a **job** (runs to completion — the only type that takes a cron schedule).
|
|
13
13
|
|
|
14
14
|
## Capabilities
|
|
15
15
|
<!-- llm:start -->
|
|
16
16
|
The lifecycle is **create → deploy → (optionally) bind a custom domain**.
|
|
17
17
|
|
|
18
|
-
- `create` registers the container and issues a **scoped API key**, returned once. The
|
|
18
|
+
- `create` registers the container and issues a **scoped API key**, returned once. The container gets it as `MYAPI_KEY`, so your code calls other MyAPI slots with no token handling. Deploy rotates this key.
|
|
19
19
|
- `deploy` takes **either** a pre-built image reference **or** a source
|
|
20
20
|
directory. `--source ./dir` tars the directory, builds it server-side
|
|
21
21
|
(typically ~4 minutes) and deploys the result — **no Docker on your machine,
|
|
@@ -53,12 +53,12 @@ it, so the probe would never reach your container.
|
|
|
53
53
|
|
|
54
54
|
### Custom domains (dynamic apps)
|
|
55
55
|
|
|
56
|
-
`myapi container domain <id> <domain>` binds a custom domain to a **deployed** container,
|
|
56
|
+
`myapi container domain <id> <domain>` binds a custom domain to a **deployed** container, over HTTPS automatically. The path for a dynamic backend on a real domain — unlike `my-funnel-api`, which serves static sites.
|
|
57
57
|
|
|
58
58
|
Get it right:
|
|
59
59
|
|
|
60
60
|
- **Deploy first.** Binding a domain to a container that has never deployed fails (422) — there is nothing running to route to.
|
|
61
|
-
- **Register the parent domain first
|
|
61
|
+
- **Register the parent domain first** via `my-domain-api`. Binding `app.synthesisdaily.com` requires `synthesisdaily.com` registered in MyAPI; otherwise 422.
|
|
62
62
|
- **One domain per container.** Re-binding, or binding a hostname already taken, fails (409).
|
|
63
63
|
- `--remove` unbinds. `myapi container get <id>` shows the bound `custom_domain`.
|
|
64
64
|
<!-- llm:end -->
|
|
@@ -127,8 +127,13 @@ way that looks like an application bug.
|
|
|
127
127
|
- **Any 5xx from your code is replaced by an edge HTML error page.** A JSON
|
|
128
128
|
error envelope will not reach the caller. Return a 4xx if the reason has to
|
|
129
129
|
survive.
|
|
130
|
-
- **`--
|
|
131
|
-
|
|
130
|
+
- **`--cpu`, `--memory`, `--max-instances` and `--cron` are set at `create` and
|
|
131
|
+
cannot be changed by `deploy`.** Recreate to change those.
|
|
132
|
+
- **`--env` CAN be changed after create**, with `container env` — a merge
|
|
133
|
+
(`--unset` removes) that rolls a new revision on the same image, keeping the
|
|
134
|
+
URL, the scoped key and the custom domain. Do NOT recreate to edit a variable:
|
|
135
|
+
this page said to for months, and it cost one customer three generations of
|
|
136
|
+
the same container in a week.
|
|
132
137
|
|
|
133
138
|
### Keeping a service warm
|
|
134
139
|
|
|
@@ -167,4 +172,4 @@ reply { "success": true, "data": …, "error": null, "meta": {…} }
|
|
|
167
172
|
|
|
168
173
|
Run `myapi container --help` for the full flag reference.
|
|
169
174
|
|
|
170
|
-
**End-to-end example:** `examples/authenticated-app/` in github.com/myapihq/myapi walks the full seam — hosted login → token verification → per-user KV record → deployed container.
|
|
175
|
+
**End-to-end example:** `examples/authenticated-app/` in github.com/myapihq/myapi walks the full seam — hosted login → token verification → per-user KV record → deployed container.
|
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.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": "
|
|
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.18.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|