@myapihq/cli 2.9.0 → 2.11.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.
Files changed (50) hide show
  1. package/dist/commands/container.js +35 -14
  2. package/dist/commands/flag-reachability.test.js +14 -4
  3. package/dist/commands/import-key.test.d.ts +1 -0
  4. package/dist/commands/import-key.test.js +69 -0
  5. package/dist/commands/org.js +5 -1
  6. package/dist/commands/setup.d.ts +5 -1
  7. package/dist/commands/setup.js +67 -6
  8. package/dist/commands/storage.js +151 -14
  9. package/dist/config.d.ts +2 -0
  10. package/dist/errors.d.ts +4 -0
  11. package/dist/errors.js +36 -3
  12. package/dist/helpers.d.ts +20 -1
  13. package/dist/helpers.js +79 -1
  14. package/dist/index.js +6 -1
  15. package/dist/org-not-found.test.d.ts +1 -0
  16. package/dist/org-not-found.test.js +42 -0
  17. package/dist/org-notice.test.d.ts +1 -0
  18. package/dist/org-notice.test.js +37 -0
  19. package/dist/output.d.ts +6 -0
  20. package/dist/output.js +8 -1
  21. package/dist/skills/my-api-hq/SKILL.md +34 -14
  22. package/dist/skills/my-audience-api/SKILL.md +16 -1
  23. package/dist/skills/my-auth-api/SKILL.md +17 -2
  24. package/dist/skills/my-company-api/SKILL.md +16 -1
  25. package/dist/skills/my-container-api/SKILL.md +45 -24
  26. package/dist/skills/my-crm-api/SKILL.md +16 -1
  27. package/dist/skills/my-database-api/SKILL.md +13 -17
  28. package/dist/skills/my-domain-api/SKILL.md +16 -1
  29. package/dist/skills/my-email-api/SKILL.md +12 -16
  30. package/dist/skills/my-email-verify-api/SKILL.md +16 -1
  31. package/dist/skills/my-feedback-api/README.md +33 -0
  32. package/dist/skills/my-feedback-api/SKILL.md +118 -0
  33. package/dist/skills/my-feedback-api/claude/.claude-plugin/plugin.json +7 -0
  34. package/dist/skills/my-function-api/README.md +34 -0
  35. package/dist/skills/my-function-api/SKILL.md +16 -1
  36. package/dist/skills/my-funnel-api/SKILL.md +16 -1
  37. package/dist/skills/my-git-api/SKILL.md +17 -1
  38. package/dist/skills/my-image-api/SKILL.md +17 -1
  39. package/dist/skills/my-llm-api/SKILL.md +20 -6
  40. package/dist/skills/my-payments-api/README.md +33 -0
  41. package/dist/skills/my-payments-api/SKILL.md +16 -1
  42. package/dist/skills/my-people-api/SKILL.md +16 -1
  43. package/dist/skills/my-pixel-api/SKILL.md +26 -3
  44. package/dist/skills/my-queue-api/SKILL.md +35 -2
  45. package/dist/skills/my-storage-api/SKILL.md +37 -29
  46. package/dist/skills/my-task-api/SKILL.md +35 -2
  47. package/dist/skills/my-url-to/SKILL.md +16 -1
  48. package/dist/skills/my-webhook-api/SKILL.md +23 -3
  49. package/dist/skills/my-workflow-api/SKILL.md +32 -6
  50. package/package.json +2 -2
package/dist/helpers.js CHANGED
@@ -1,4 +1,5 @@
1
- import { error } from './output.js';
1
+ import { error, banner } from './output.js';
2
+ import { saveConfig } from './config.js';
2
3
  import { confirm, isNonInteractive } from './prompt.js';
3
4
  // error() returns `never`, so after `if (!x) error(...)` TS narrows x to a
4
5
  // non-falsy value and the casts disappear.
@@ -13,8 +14,85 @@ export function requireOrg(flags, config, usage) {
13
14
  if (!orgId) {
14
15
  error(`Missing required arguments.\nUsage: ${usage}\n(Or set default: myapi config set-org <id>)`);
15
16
  }
17
+ noticeOrgChanged(orgId, config);
18
+ recordResolvedOrg(orgId, config);
16
19
  return orgId;
17
20
  }
21
+ // ── Which org am I in? ───────────────────────────────────────────────────────
22
+ //
23
+ // The org is ambient, sticky and invisible: it comes from a saved default that
24
+ // survives across turns, tasks and days, and a stale one looks exactly like a
25
+ // correct one. Agents lose whole writes to this — a demo pushed onto a live
26
+ // site, a domain reassigned away from the org that was serving it.
27
+ //
28
+ // The fix is NOT to announce the org on every command. A line that is always
29
+ // there carries no information, gets filtered out within a day, and costs
30
+ // tokens on every call in an agent loop. It only carries signal when it could
31
+ // have been different — so it fires on CHANGE and is silent otherwise. Steady
32
+ // single-org work never sees it; the moment the target moves, it says so once.
33
+ //
34
+ // Goes to stderr, like the spinner, so `--json | jq` and piped output stay
35
+ // clean.
36
+ let resolvedOrgId;
37
+ let resolvedOrgName;
38
+ /** The notice for a move, or undefined when there is nothing to say.
39
+ *
40
+ * Pure and exported because both failure directions are silent in production:
41
+ * return a string always and it becomes noise everyone filters out; return
42
+ * undefined always and the wrong-org write it exists to catch goes through
43
+ * unannounced. Neither shows up as a broken build, so it is pinned by a test.
44
+ */
45
+ export function orgChangeNotice(previous, next, names = {}) {
46
+ if (!previous || previous === next)
47
+ return undefined;
48
+ const label = (id) => (names[id] ? `${names[id]} ` : '') + short(id);
49
+ return `→ org changed: ${label(previous)} → ${label(next)}`;
50
+ }
51
+ function noticeOrgChanged(orgId, config) {
52
+ const msg = orgChangeNotice(config.last_org_used, orgId, config.org_names ?? {});
53
+ if (msg)
54
+ banner(msg);
55
+ }
56
+ function recordResolvedOrg(orgId, config) {
57
+ resolvedOrgId = orgId;
58
+ resolvedOrgName = config.org_names?.[orgId];
59
+ if (config.last_org_used === orgId)
60
+ return;
61
+ // Written only on change, so the common path does no disk I/O. Failure here
62
+ // must never break the command the user actually asked for — the worst case
63
+ // is that the next run re-notices the same change.
64
+ try {
65
+ config.last_org_used = orgId;
66
+ saveConfig(config);
67
+ }
68
+ catch { /* advisory only */ }
69
+ }
70
+ function short(id) {
71
+ return id.length > 12 ? `${id.slice(0, 8)}…` : id;
72
+ }
73
+ /** The org this invocation resolved to, for `--json` consumers. */
74
+ export function currentOrg() {
75
+ return resolvedOrgId ? { org_id: resolvedOrgId, org_name: resolvedOrgName } : undefined;
76
+ }
77
+ /** Cache id → name so later output can name an org instead of printing a UUID.
78
+ * Called by the commands that already hold the mapping (`org list`, `status`). */
79
+ export function rememberOrgNames(config, orgs) {
80
+ const names = { ...(config.org_names ?? {}) };
81
+ let changed = false;
82
+ for (const o of orgs) {
83
+ if (!o?.id || !o?.name || names[o.id] === o.name)
84
+ continue;
85
+ names[o.id] = o.name;
86
+ changed = true;
87
+ }
88
+ if (!changed)
89
+ return;
90
+ try {
91
+ config.org_names = names;
92
+ saveConfig(config);
93
+ }
94
+ catch { /* advisory only */ }
95
+ }
18
96
  export function requireDomain(arg, flags, config, usage) {
19
97
  const fromFlag = typeof flags.domain === 'string' ? flags.domain : '';
20
98
  const fromConfig = typeof config.default_domain === 'string' ? config.default_domain : '';
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { error, info, success, banner } from './output.js';
2
+ import { error, info, success, banner, setResolvedContextSource } from './output.js';
3
3
  import { loadConfig } from './config.js';
4
4
  import { MyApiError, setUserAgent } from '@myapihq/sdk';
5
5
  import { friendlyError } from './errors.js';
6
+ import { currentOrg } from './helpers.js';
6
7
  import * as fs from 'fs';
7
8
  import { parseFlags } from './flags.js';
8
9
  const pkgPath = new URL('../package.json', import.meta.url);
@@ -12,6 +13,10 @@ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
12
13
  // tell which client versions are still in the wild — which is what you need
13
14
  // before you can deprecate anything server-side.
14
15
  setUserAgent(`myapi-cli/${pkg.version} node/${process.versions.node}`);
16
+ // Let `--json` output carry the org the call actually resolved to. Wired here
17
+ // rather than imported inside output.ts so that module stays free of command
18
+ // and config state. See printJson / requireOrg.
19
+ setResolvedContextSource(currentOrg);
15
20
  import * as accountCmd from './commands/account.js';
16
21
  import * as keysCmd from './commands/keys.js';
17
22
  import * as billingCmd from './commands/billing.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ // A resource sitting in another org answers exactly like a typo'd id. "Not
2
+ // found" sends the reader to check the id — which is correct, and which is why
3
+ // the org never gets questioned. Naming the org that was searched is what
4
+ // turns the commonest org-drift symptom into its own diagnosis.
5
+ //
6
+ // The risk on the other side is appending it to everything, which is the
7
+ // always-on banner in a different costume. These pin both edges.
8
+ import { describe, it, expect } from 'vitest';
9
+ import { withOrgContext } from './errors.js';
10
+ const ORG = { org_id: 'org-aaaaaaaa-1111', org_name: 'Acme' };
11
+ describe('withOrgContext', () => {
12
+ it('names the org searched on a 404', () => {
13
+ const out = withOrgContext('Funnel not found.', { status: 404, code: 'FUNNEL_NOT_FOUND' }, ORG);
14
+ expect(out).toContain('Funnel not found.');
15
+ expect(out).toContain('Acme');
16
+ expect(out).toContain('myapi org list');
17
+ });
18
+ it('fires on a not-found CODE even when the status is not 404', () => {
19
+ const out = withOrgContext('Resource not found.', { status: 400, code: 'not_found' }, ORG);
20
+ expect(out).toContain('Acme');
21
+ });
22
+ it('stays out of the way on unrelated errors', () => {
23
+ const msg = 'Too many requests. Please wait a moment and try again.';
24
+ expect(withOrgContext(msg, { status: 429, code: 'RATE_LIMITED' }, ORG)).toBe(msg);
25
+ });
26
+ it('says nothing when the org itself is what was not found', () => {
27
+ // "Looked in org X" where X is the id that does not exist restates the
28
+ // failure as though it were a clue.
29
+ const msg = 'Organization not found.';
30
+ expect(withOrgContext(msg, { status: 404, code: 'ORG_NOT_FOUND' }, ORG)).toBe(msg);
31
+ });
32
+ it('says nothing when no org was resolved', () => {
33
+ // Account-level commands (whoami, billing) never resolve an org; there is
34
+ // no org to blame and claiming one would be a fabrication.
35
+ const msg = 'Resource not found.';
36
+ expect(withOrgContext(msg, { status: 404, code: 'NOT_FOUND' }, undefined)).toBe(msg);
37
+ });
38
+ it('does not repeat an org the message already names', () => {
39
+ const msg = `Funnel not found in org ${ORG.org_id}.`;
40
+ expect(withOrgContext(msg, { status: 404, code: 'FUNNEL_NOT_FOUND' }, ORG)).toBe(msg);
41
+ });
42
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ // The org-change notice has two silent failure modes and no build breaks for
2
+ // either: always-speak turns it into noise that gets filtered out within a day
3
+ // (and costs tokens on every call in an agent loop), always-silent lets the
4
+ // wrong-org write it exists to catch go through unannounced. Pin both edges.
5
+ import { describe, it, expect } from 'vitest';
6
+ import { orgChangeNotice } from './helpers.js';
7
+ const NAMES = { 'org-a': 'Acme', 'org-b': 'Demo Corp' };
8
+ describe('orgChangeNotice', () => {
9
+ it('says nothing in steady state — the same org twice', () => {
10
+ expect(orgChangeNotice('org-a', 'org-a', NAMES)).toBeUndefined();
11
+ });
12
+ it('says nothing on the first ever resolution', () => {
13
+ // No prior means nothing has moved; announcing here would fire on every
14
+ // fresh config and train the reader to ignore the line.
15
+ expect(orgChangeNotice(undefined, 'org-a', NAMES)).toBeUndefined();
16
+ });
17
+ it('speaks when the org moves, naming both sides', () => {
18
+ const msg = orgChangeNotice('org-a', 'org-b', NAMES);
19
+ expect(msg).toBeDefined();
20
+ expect(msg).toContain('Acme');
21
+ expect(msg).toContain('Demo Corp');
22
+ });
23
+ it('still speaks when no name is cached, falling back to the id', () => {
24
+ // The cache is populated opportunistically by `org list` / `status`, so an
25
+ // agent that never ran either still has to be told the target moved.
26
+ const msg = orgChangeNotice('org-a', 'org-b', {});
27
+ expect(msg).toBeDefined();
28
+ expect(msg).toContain('org-a');
29
+ expect(msg).toContain('org-b');
30
+ });
31
+ it('shortens long ids so the line stays scannable', () => {
32
+ const long = 'org-7f1aa7f1-f6eb-4a6d-87b7-1bbe6584f5d6';
33
+ const msg = orgChangeNotice(long, 'org-b', {});
34
+ expect(msg).not.toContain(long);
35
+ expect(msg).toContain('org-7f1a');
36
+ });
37
+ });
package/dist/output.d.ts CHANGED
@@ -5,6 +5,11 @@ export declare function success(message: string): void;
5
5
  export declare function error(message: string): never;
6
6
  export declare function info(message: string): void;
7
7
  export declare function banner(message: string): void;
8
+ type ResolvedContext = {
9
+ org_id: string;
10
+ org_name?: string;
11
+ };
12
+ export declare function setResolvedContextSource(fn: () => ResolvedContext | undefined): void;
8
13
  export declare function printJson(data: unknown): void;
9
14
  export declare function spinnerFrame(i: number): string;
10
15
  export declare function spinnerWrite(s: string): void;
@@ -16,3 +21,4 @@ export interface PrintTableOptions {
16
21
  empty?: string;
17
22
  }
18
23
  export declare function printTable<T extends object>(rows: T[], opts?: PrintTableOptions): void;
24
+ export {};
package/dist/output.js CHANGED
@@ -12,8 +12,15 @@ export function info(message) {
12
12
  export function banner(message) {
13
13
  process.stderr.write(message + '\n');
14
14
  }
15
+ let resolvedContext;
16
+ export function setResolvedContextSource(fn) {
17
+ resolvedContext = fn;
18
+ }
15
19
  export function printJson(data) {
16
- console.log(JSON.stringify(data, null, 2));
20
+ const ctx = resolvedContext?.();
21
+ const stampable = ctx && data !== null && typeof data === 'object' && !Array.isArray(data);
22
+ const payload = stampable ? { ...data, _resolved: ctx } : data;
23
+ console.log(JSON.stringify(payload, null, 2));
17
24
  }
18
25
  // Spinner / line-clear primitives. Used by polling helpers (utils.pollJob)
19
26
  // and any handler that wants its own progress UI.
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-api-hq
3
- version: 1.0.0
3
+ version: 1.1.0
4
4
  description: >
5
5
  Auth, organizations, and billing hub. Start here to get an api_key and org_id — every other service depends on both.
6
6
  triggers: [api key, account, organization, org, billing, balance, topup, credits, setup, defaults, brand, sync brand, doctor, health check, is my org healthy]
7
- checksum: sha256-fab324491446026e1600ac531bfbb164c10d46a617e528e6886be74b7dbdb4b1
7
+ checksum: sha256-6f54c719facde1ddcdee04ffcf9527ed42550cbfc67679ced5aea55fc7adba7d
8
8
  ---
9
9
 
10
10
  # MyApiHQ
@@ -15,14 +15,6 @@ The root service. It manages accounts, API keys, organizations, and billing. No
15
15
  <!-- llm:start -->
16
16
  MyApiHQ is the platform's foundation. Every other service (domain, funnel, auth, payments, fn, workflow, database, storage, email, webhook, crm, llm, image, pixel, url) requires both an `api_key` and (for org-scoped resources) an `org_id` minted here. Setup is one command — `myapi account setup` — which provisions an account, generates an api_key, creates a default org, and stores everything in `~/.myapi/config.json`. Subsequent commands pick up those defaults automatically.
17
17
 
18
- ```
19
- myapihq ──► org_id + api_key
20
-
21
- ┌───────┴────────┐
22
- mydomainapi myfunnelapi
23
- (domains) (websites)
24
- ```
25
-
26
18
  ### Anonymous vs registered accounts
27
19
 
28
20
  Two tiers, chosen at setup time:
@@ -42,6 +34,7 @@ An anonymous account can upgrade at any time via `myapi account link <email>`
42
34
  | Command | What it does |
43
35
  |---|---|
44
36
  | `myapi account setup` | Interactive setup: creates account, generates api_key, sets defaults |
37
+ | `myapi status` | Single-screen orientation: account + every resource in the default org. Start here when you don't know what already exists |
45
38
  | `myapi account whoami` | Show current account, default org/funnel, balance, free-tier usage |
46
39
  | `myapi account link [email]` | Upgrade anonymous account to registered (or add a second session) |
47
40
  | `myapi account switch [index]` | Switch active account |
@@ -72,6 +65,7 @@ myapi account setup
72
65
  myapi org create "Acme" --yes
73
66
 
74
67
  # Day-to-day
68
+ myapi status # what exists already, in one screen
75
69
  myapi account whoami # confirm what's active
76
70
  myapi billing balance # before doing anything that costs credits
77
71
  myapi billing topup 20 # add $20
@@ -98,28 +92,54 @@ Each org gets a free preview subdomain (`*.makeautonomous.com`) usable before re
98
92
  - API keys have format `hq_live_...` and are sent as `Authorization: Bearer <key>`.
99
93
  - `org sync-brand` is async (scrapes the site, polls the job).
100
94
 
101
- ## Minting a least-privilege API key
95
+ ## Scoping a key and why `--org` is the one that saves you
102
96
 
103
97
  A key's authority is inline and always a subset of the key that mints it, so
104
98
  you can hand work a key that cannot exceed its job:
105
99
 
106
100
  ```bash
101
+ myapi keys create --name agent --org <id> --grant '*' # locked to ONE org
107
102
  myapi keys create --name ci --grant funnel:write,storage:read
108
103
  myapi keys create --name readonly --grant '*:read' # read anything, write nothing
109
104
  myapi keys create --name billing-fn --org <id> --grant email --spend-cap 25
110
105
  ```
111
106
 
107
+ - `--org <id>` — **lock it to one org.** Omit for account-wide.
112
108
  - `--grant <list>` — `slot:read` / `slot:write`; a bare slot means write, `*`
113
109
  means all. **Omitting `--grant` mints an unrestricted key.**
114
- - `--org <id>` — lock it to one org. Omit for account-wide.
115
110
  - `--spend-cap <usd>` — hard ceiling; `0` means the key cannot spend at all.
116
111
  - `keys revoke-all --kind function|manual|account` narrows the kill switch.
117
112
 
113
+ **Agents working for one org: use an org-locked key.** The org is otherwise
114
+ ambient — a saved default that survives across turns, where a stale one looks
115
+ exactly like a correct one. That is how a demo lands on a live site. Locking
116
+ turns a wrong-org write into a `403`. Keep your *own* key account-wide though:
117
+ an org-locked one cannot create orgs or manage billing.
118
+
119
+ Two more signals, no flags needed: resolving to a different org than last time
120
+ prints `→ org changed: <from> → <to>` on stderr, once — silence means it has
121
+ not moved. Under `--json`, object responses carry `_resolved: {org_id,
122
+ org_name}`; assert on that rather than assuming.
123
+
118
124
  ## Org profile fields
119
125
 
120
126
  `myapi org create <name>` also takes `--tagline`, `--description`,
121
127
  `--business-sector` and `--logo-url`. They populate the org's public profile
122
- and the funnel created alongside it, so setting them at create avoids editing
123
- two places later.
128
+ and the funnel created alongside it.
129
+
130
+ ## HTTP (from deployed code)
131
+
132
+ <!-- http:start -->
133
+ <!-- generated by `npm run canonical-sync` — do not edit -->
134
+ ```
135
+ base https://api.myapihq.com
136
+ path GET /hq/orgs/{org_id}
137
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
138
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
139
+ ```
140
+
141
+ - **Per-slot host** — do not assume one host serves every slot.
142
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
143
+ <!-- http:end -->
124
144
 
125
145
  Run `myapi --help` or `myapi <command> --help` for full flag reference.
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Saved audiences = named Goldfox-filter snapshots over the people or company database. Build a target list once, name it, reuse it across campaigns, refresh to re-evaluate against current data. The persistence layer on top of my-people-api + my-company-api.
6
6
  triggers: [audience, segment, target list, saved filter, goldfox, lead list, abm list, refresh, members, prospect database]
7
- checksum: sha256-a2c7142e880932ac2aa53cdb29b8632ce2c4d5ef33c3f8b6df705f7e987ab6af
7
+ checksum: sha256-3fc9f2966804381cb9e2b5f52d44a0c12c857f243b151a93a813c6345574f07f
8
8
  ---
9
9
 
10
10
  # MyAudienceAPI
@@ -136,4 +136,19 @@ myapi audience create "EU growth-stage SaaS accounts" \
136
136
  - Audiences are per-org. Two orgs can have audiences with the same name; ids are globally unique.
137
137
  - Look before you delete: confirm the target with `myapi audience get <id>`, pass `--org` explicitly; delete verbs require `--yes` in non-interactive runs.
138
138
 
139
+ ## HTTP (from deployed code)
140
+
141
+ <!-- http:start -->
142
+ <!-- generated by `npm run canonical-sync` — do not edit -->
143
+ ```
144
+ base https://api.myapihq.com
145
+ path POST /audience/orgs/{org_id}/audiences
146
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
147
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
148
+ ```
149
+
150
+ - **Per-slot host** — do not assume one host serves every slot.
151
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
152
+ <!-- http:end -->
153
+
139
154
  Run `myapi audience --help` for full flag reference.
@@ -4,7 +4,7 @@ version: 1.0.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-5eeacc463d7323f85a330639b7073f735ccc80bddfb8217c9c15c56989b8a99a
7
+ checksum: sha256-07690e940717e8f65c195f9fbecdc1def9372edfd52dea19f6570227008d28ad
8
8
  ---
9
9
 
10
10
  # MyAuthAPI
@@ -121,4 +121,19 @@ with no email — for throwaway or machine-owned orgs. It cannot receive
121
121
  password resets or magic links, so attach a real identity before anything
122
122
  depends on it. `myapi status` shows `Type: anonymous`.
123
123
 
124
- **End-to-end example:** `examples/authenticated-app/` walks the full seam — hosted login → token verification → per-user KV record → deployed container — including the parts that cost real users hours (verify the id_token for identity; the access token is a bearer credential for `<issuer>/userinfo`).
124
+ **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 — including the parts that cost real users hours (verify the id_token for identity; the access token is a bearer credential for `<issuer>/userinfo`).
125
+
126
+ ## HTTP (from deployed code)
127
+
128
+ <!-- http:start -->
129
+ <!-- generated by `npm run canonical-sync` — do not edit -->
130
+ ```
131
+ base https://api.myapihq.com
132
+ path POST /auth/orgs/{org_id}/tenant
133
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
134
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
135
+ ```
136
+
137
+ - **Per-slot host** — do not assume one host serves every slot.
138
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
139
+ <!-- http:end -->
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Company database backed by the Goldfox crawl. Filter companies by Goldfox confidence tier, country/TLD consistency, behavioral page signals (has_careers_page, has_investors_page, has_shop_page, has_c_level, has_decision_maker), legal-entity status, headcount, and source-URL count. Account-based targeting and B2B firmographics.
6
6
  triggers: [companies, accounts, firmographics, abm, search, filter, goldfox, careers signal, investors, c-level, shop, b2b targeting]
7
- checksum: sha256-4754de8a66bf13e7e2f2a23acc407ccec147e142abf8adc670ae7962ca860a74
7
+ checksum: sha256-5204c6218af2d4c52b1b81aa0d917b69c863be1d3c0384d2d2f7d9dda1587534
8
8
  ---
9
9
 
10
10
  # MyCompanyAPI
@@ -107,4 +107,19 @@ myapi audience members $AID --limit 50 --json > accounts.json
107
107
  - `keyword` is a substring match on the company's **domain** — use `--keyword stripe` to find domains containing "stripe".
108
108
  - For a persistent account list, use `my-audience-api` with `--from company`.
109
109
 
110
+ ## HTTP (from deployed code)
111
+
112
+ <!-- http:start -->
113
+ <!-- generated by `npm run canonical-sync` — do not edit -->
114
+ ```
115
+ base https://api.myapihq.com
116
+ path POST /company/orgs/{org_id}/search
117
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
118
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
119
+ ```
120
+
121
+ - **Per-slot host** — do not assume one host serves every slot.
122
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
123
+ <!-- http:end -->
124
+
110
125
  Run `myapi company --help` for full flag reference.
@@ -4,7 +4,7 @@ version: 1.0.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-47eda6baa9055dadbd3e9d04df9fc425f1a7e96a9cd80c971c4951b97f1e94ff
7
+ checksum: sha256-a5aabfe95b85f1e6ceb2b9254441183d540e81f888cefbdcf302385b95db2fd0
8
8
  ---
9
9
 
10
10
  # MyContainerAPI
@@ -23,26 +23,33 @@ The lifecycle is **create → deploy → (optionally) bind a custom domain**.
23
23
  can deploy; you do not need to be able to run one.
24
24
  - `domain` puts the container on a **custom domain** — how you serve a dynamic app at `app.yourbrand.com`.
25
25
 
26
- ### Deploying safely — NOT YET POSSIBLE ON THIS PLATFORM
26
+ ### Deploying safely
27
27
 
28
- A deploy takes 100% of traffic the moment it lands. There is no dry run, no
29
- definition of correct beyond "something is listening on the port", and no way
30
- back. Plan for that.
28
+ A plain deploy takes 100% of traffic the moment it lands.
31
29
 
32
- `--no-promote` and `--smoke` exist as flags and **the CLI refuses them**: they
33
- shipped before the platform could honour them, and a guard that silently
34
- passes is worse than no guard. `--health-check` is accepted at create but does
35
- not appear on the container afterwards, so do not rely on it either.
30
+ **`--smoke '<assertion>'`** deploys the revision with no traffic, checks the
31
+ assertion, and promotes only if it holds. A failure returns `SMOKE_FAILED`,
32
+ leaves the previous revision serving, and returns a revision URL to inspect.
36
33
 
37
- Until they work, the only safe sequence is:
34
+ ```bash
35
+ myapi container deploy <id> --source ./app --smoke 'GET / contains assets/'
36
+ ```
37
+
38
+ **Assert on content, not status.** A build whose frontend never bundled still
39
+ binds its port and answers `200` — that is how a placeholder page reached
40
+ production and served a dead page for fifteen minutes.
38
41
 
39
- 1. deploy to a **non-production** container
40
- 2. verify it yourself `curl` for a string only a real build emits, not just
41
- a 200, because a broken build returns 200 too
42
- 3. deploy the same image to production
42
+ **`--no-promote`** holds the revision back and prints a URL to test yourself,
43
+ then `myapi container promote <id> <revision>`. Use it when the check is more
44
+ than one assertion. On a container's *first* deploy nothing is serving yet, so
45
+ traffic is not withheld and the output says so.
43
46
 
44
- `myapi container promote <id> <revision>` currently fails, so a bad deploy
45
- must be fixed by deploying forward. Keep a known-good image reference to hand.
47
+ **`myapi container rollback <id>`** returns traffic to the previous ready
48
+ revision in seconds, no rebuild.
49
+
50
+ `--health-check /livez` at create makes the startup probe an HTTP request
51
+ rather than a bare TCP connect. `/healthz` is refused — the runtime intercepts
52
+ it, so the probe would never reach your container.
46
53
 
47
54
  ### Custom domains (dynamic apps)
48
55
 
@@ -85,14 +92,14 @@ myapi container deploy <id> --source ./my-app
85
92
  # 2b. Or ship an image you already built and pushed.
86
93
  myapi container deploy <id> registry.example.com/my-app:v1
87
94
 
88
- # Either way this takes 100% of traffic immediately there is no staging
89
- # step, so verify on a non-production container FIRST.
95
+ # A plain deploy takes 100% of traffic the moment it lands. Gate it with
96
+ # --smoke, or hold it back with --no-promote and test the revision yourself.
90
97
 
91
- # 3. Check what you actually shipped. Assert on content: a build whose
92
- # frontend never bundled still binds its port and returns 200.
98
+ # 3. Verify content, not status: a build whose frontend never bundled
99
+ # still binds its port and returns 200.
93
100
  curl -s https://<your-domain>/ | grep -q 'assets/' || echo "BROKEN BUILD"
94
101
 
95
- # 3. Serve it on a custom domain. The parent domain must already be
102
+ # 4. Serve it on a custom domain. The parent domain must already be
96
103
  # registered: myapi domain register synthesisdaily.com
97
104
  myapi container domain <id> app.synthesisdaily.com
98
105
  # → app.synthesisdaily.com now serves the container over HTTPS
@@ -121,8 +128,7 @@ way that looks like an application bug.
121
128
  error envelope will not reach the caller. Return a 4xx if the reason has to
122
129
  survive.
123
130
  - **`--env`, `--cpu`, `--memory`, `--max-instances` and `--cron` are set at
124
- `create` and cannot be changed by `deploy`.** Passing them to `deploy` does
125
- nothing. Recreate the container to change them.
131
+ `create` and cannot be changed by `deploy`.** Recreate to change them.
126
132
 
127
133
  ### Keeping a service warm
128
134
 
@@ -144,6 +150,21 @@ service costs nothing while idle.
144
150
  `invalid_json_response`. Build the tarball yourself and pass
145
151
  `--source ctx.tar.gz`, or keep the directory clean.
146
152
 
153
+ ## HTTP (from deployed code)
154
+
155
+ <!-- http:start -->
156
+ <!-- generated by `npm run canonical-sync` — do not edit -->
157
+ ```
158
+ base https://api.myapihq.com
159
+ path POST /container/orgs/{org_id}/containers
160
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
161
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
162
+ ```
163
+
164
+ - **Per-slot host** — do not assume one host serves every slot.
165
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
166
+ <!-- http:end -->
167
+
147
168
  Run `myapi container --help` for the full flag reference.
148
169
 
149
- **End-to-end example:** `examples/authenticated-app/` walks the full seam — hosted login → token verification → per-user KV record → deployed container including the parts that cost real users hours (env vars are create-only; a service is multi-instance; health-check `/livez` not `/healthz`).
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. Every constraint it hits is in **Runtime constraints** above.
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  The canonical store of engaged contacts + companies for an org. Auto-ingests from inbound webhooks via a configurable dot-path. Fixed lifecycle_stage enum (cold | warm | qualified | customer | churned). Append-only event timeline with reserved kinds. Soft delete + restore. Promote-from-Goldfox closes the discovery → engagement loop.
6
6
  triggers: [crm, contact, company, lead, engagement, pipeline, lifecycle, qualified, customer, webhook ingest, promote]
7
- checksum: sha256-dfadb20670d9d978503549d330c2c9717fbe6d13ac35b53d9e1a8874d2136fde
7
+ checksum: sha256-3f840c8d7205d6a3eacc7c2be3ba2580a52a1964c085af241fd9ad67d48e19b6
8
8
  ---
9
9
 
10
10
  # MyCRMAPI
@@ -167,4 +167,19 @@ myapi crm contacts events <id> --kind webhook_received
167
167
  - **Only `webhook_received` fires today.** Email and pixel auto-ingest are coming; the CLI surface will not change.
168
168
  - **Free in v1.** Metered later if usage warrants.
169
169
 
170
+ ## HTTP (from deployed code)
171
+
172
+ <!-- http:start -->
173
+ <!-- generated by `npm run canonical-sync` — do not edit -->
174
+ ```
175
+ base https://api.myapihq.com
176
+ path POST /crm/orgs/{org_id}/contacts
177
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
178
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
179
+ ```
180
+
181
+ - **Per-slot host** — do not assume one host serves every slot.
182
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
183
+ <!-- http:end -->
184
+
170
185
  Run `myapi crm --help` or `myapi crm <namespace> --help` for inline reference.
@@ -4,7 +4,7 @@ version: 1.0.0
4
4
  description: >
5
5
  Per-org KV store with named namespaces, JSON values up to 256 KB, prefix-scan listing, and compare-and-swap via etag. The substrate for any stateful agent-built app on MyAPI — user tables, session stores, idempotency keys, per-user lookup maps.
6
6
  triggers: [database, kv, key value, namespace, store, state, etag, cas, session, idempotency]
7
- checksum: sha256-924071245c33bf3e43f085fbd756af003e19c990f4522f658de33f42504452d7
7
+ checksum: sha256-9b6f14fa5b0115ce042c71f861cb71ec8993aa68447d740b63f5b3689c23b629
8
8
  ---
9
9
 
10
10
  # MyDatabaseAPI
@@ -105,26 +105,22 @@ myapi database get "by-email:$EMAIL" --ns users --json | jq -r .value
105
105
  - **Free in v1.** Metered later if usage shows a need. Cost discipline still applies — store data, not blobs.
106
106
 
107
107
 
108
- ## Calling this from deployed code (HTTP)
109
-
110
- The CLI is not what runs in production — a deployed function or container calls
111
- the HTTP API directly. That surface was previously only discoverable by
112
- grepping the CLI bundle, which cost one team an hour per slot.
108
+ ## HTTP (from deployed code)
113
109
 
110
+ <!-- http:start -->
111
+ <!-- generated by `npm run canonical-sync` — do not edit -->
114
112
  ```
115
- base https://api.myapihq.com
116
- path /database/orgs/{org_id}/namespaces/{ns}/keys/{key}
117
- auth Authorization: Bearer <api key>
118
- (inside a function: env.__MYAPI_KEY · inside a container: env.MYAPI_KEY)
119
- body writes take {"value": <json>} the value is WRAPPED
120
- reply { "success": true, "data": …, "error": null, "meta": {…} }
121
- Unwrap `data`. On failure `success` is false and `error` is
122
- { code, message }.
113
+ base https://api.myapihq.com
114
+ path PUT /database/orgs/{org_id}/namespaces/{ns}/keys/{key}
115
+ body writes are WRAPPED: {"value": <json>} — not the bare value
116
+ auth Authorization: Bearer <key> (fn: env.__MYAPI_KEY · container: env.MYAPI_KEY)
117
+ reply { "success": true, "data": …, "error": null, "meta": {…} }
123
118
  ```
124
119
 
125
- **The org id goes in the PATH, not a header.** There is no `X-Org-Id`.
126
- **Base URLs differ per slot** — do not assume one host for everything.
120
+ - **Per-slot host** do not assume one host serves every slot.
121
+ - **Org id goes in the PATH** — there is no `X-Org-Id` header.
122
+ <!-- http:end -->
127
123
 
128
124
  Run `myapi database --help` for inline reference.
129
125
 
130
- **End-to-end example:** `examples/authenticated-app/` walks the full seam — hosted login → token verification → per-user KV record → deployed container — including the parts that cost real users hours (KV writes must be wrapped as `{"value": …}`).
126
+ **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 — including the parts that cost real users hours (KV writes must be wrapped as `{"value": …}`).