@ekanos/cli 0.12.0 → 0.14.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 (175) hide show
  1. package/README.md +25 -8
  2. package/dist/agents-api.d.ts +11 -2
  3. package/dist/agents-api.js +39 -3
  4. package/dist/agents-api.js.map +1 -1
  5. package/dist/apps-api.d.ts +3 -2
  6. package/dist/apps-api.js +3 -7
  7. package/dist/apps-api.js.map +1 -1
  8. package/dist/auth/ekanos-api.d.ts +16 -2
  9. package/dist/auth/ekanos-api.js +101 -15
  10. package/dist/auth/ekanos-api.js.map +1 -1
  11. package/dist/auth/host-trust.d.ts +46 -0
  12. package/dist/auth/host-trust.js +79 -0
  13. package/dist/auth/host-trust.js.map +1 -0
  14. package/dist/auth/session.d.ts +10 -1
  15. package/dist/auth/session.js +28 -28
  16. package/dist/auth/session.js.map +1 -1
  17. package/dist/commands/agents-edit.d.ts +6 -2
  18. package/dist/commands/agents-edit.js +57 -26
  19. package/dist/commands/agents-edit.js.map +1 -1
  20. package/dist/commands/agents.d.ts +13 -2
  21. package/dist/commands/agents.js +148 -36
  22. package/dist/commands/agents.js.map +1 -1
  23. package/dist/commands/apps.d.ts +15 -4
  24. package/dist/commands/apps.js +133 -51
  25. package/dist/commands/apps.js.map +1 -1
  26. package/dist/commands/catalog.js +7 -6
  27. package/dist/commands/catalog.js.map +1 -1
  28. package/dist/commands/knowledge.d.ts +56 -0
  29. package/dist/commands/knowledge.js +456 -0
  30. package/dist/commands/knowledge.js.map +1 -0
  31. package/dist/commands/login.d.ts +4 -0
  32. package/dist/commands/login.js +30 -2
  33. package/dist/commands/login.js.map +1 -1
  34. package/dist/commands/logout.d.ts +9 -1
  35. package/dist/commands/logout.js +56 -7
  36. package/dist/commands/logout.js.map +1 -1
  37. package/dist/commands/publish.js +3 -3
  38. package/dist/commands/publish.js.map +1 -1
  39. package/dist/commands/reviewers.d.ts +2 -0
  40. package/dist/commands/reviewers.js +7 -3
  41. package/dist/commands/reviewers.js.map +1 -1
  42. package/dist/commands/routes.d.ts +2 -0
  43. package/dist/commands/routes.js +42 -9
  44. package/dist/commands/routes.js.map +1 -1
  45. package/dist/commands/runs.d.ts +13 -2
  46. package/dist/commands/runs.js +96 -15
  47. package/dist/commands/runs.js.map +1 -1
  48. package/dist/commands/scope-notice.d.ts +21 -0
  49. package/dist/commands/scope-notice.js +35 -0
  50. package/dist/commands/scope-notice.js.map +1 -0
  51. package/dist/commands/skills.d.ts +7 -1
  52. package/dist/commands/skills.js +80 -24
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/sources.d.ts +4 -0
  55. package/dist/commands/sources.js +233 -5
  56. package/dist/commands/sources.js.map +1 -1
  57. package/dist/commands/status.js +5 -4
  58. package/dist/commands/status.js.map +1 -1
  59. package/dist/commands/team-agent-readiness.d.ts +59 -0
  60. package/dist/commands/team-agent-readiness.js +341 -0
  61. package/dist/commands/team-agent-readiness.js.map +1 -0
  62. package/dist/commands/team-agents-agents.d.ts +24 -3
  63. package/dist/commands/team-agents-agents.js +336 -35
  64. package/dist/commands/team-agents-agents.js.map +1 -1
  65. package/dist/commands/team-agents-mcp.js +32 -3
  66. package/dist/commands/team-agents-mcp.js.map +1 -1
  67. package/dist/commands/team-agents-triggers.js +230 -54
  68. package/dist/commands/team-agents-triggers.js.map +1 -1
  69. package/dist/commands/team-agents.d.ts +3 -1
  70. package/dist/commands/team-agents.js +27 -7
  71. package/dist/commands/team-agents.js.map +1 -1
  72. package/dist/commands/teams-channel-install.d.ts +24 -0
  73. package/dist/commands/teams-channel-install.js +94 -0
  74. package/dist/commands/teams-channel-install.js.map +1 -0
  75. package/dist/commands/teams-connect.d.ts +66 -0
  76. package/dist/commands/teams-connect.js +397 -0
  77. package/dist/commands/teams-connect.js.map +1 -0
  78. package/dist/commands/teams-events.d.ts +12 -0
  79. package/dist/commands/teams-events.js +138 -0
  80. package/dist/commands/teams-events.js.map +1 -0
  81. package/dist/commands/teams-onboard.d.ts +16 -5
  82. package/dist/commands/teams-onboard.js +61 -36
  83. package/dist/commands/teams-onboard.js.map +1 -1
  84. package/dist/commands/teams-overview.d.ts +11 -0
  85. package/dist/commands/teams-overview.js +160 -0
  86. package/dist/commands/teams-overview.js.map +1 -0
  87. package/dist/commands/teams-scope.d.ts +10 -0
  88. package/dist/commands/teams-scope.js +109 -0
  89. package/dist/commands/teams-scope.js.map +1 -0
  90. package/dist/commands/teams.d.ts +64 -7
  91. package/dist/commands/teams.js +254 -60
  92. package/dist/commands/teams.js.map +1 -1
  93. package/dist/commands/upgrade.js +12 -2
  94. package/dist/commands/upgrade.js.map +1 -1
  95. package/dist/commands/use.js +2 -2
  96. package/dist/commands/use.js.map +1 -1
  97. package/dist/commands/whoami.js +1 -1
  98. package/dist/commands/whoami.js.map +1 -1
  99. package/dist/connections-api.js +1 -1
  100. package/dist/connections-api.js.map +1 -1
  101. package/dist/delegate.js +11 -4
  102. package/dist/delegate.js.map +1 -1
  103. package/dist/harness-scaffold.js +12 -0
  104. package/dist/harness-scaffold.js.map +1 -1
  105. package/dist/index.d.ts +6 -0
  106. package/dist/index.js +724 -152
  107. package/dist/index.js.map +1 -1
  108. package/dist/knowledge-api.d.ts +175 -0
  109. package/dist/knowledge-api.js +169 -0
  110. package/dist/knowledge-api.js.map +1 -0
  111. package/dist/open-url.d.ts +16 -0
  112. package/dist/open-url.js +73 -0
  113. package/dist/open-url.js.map +1 -0
  114. package/dist/parse-argv.d.ts +2 -0
  115. package/dist/parse-argv.js.map +1 -1
  116. package/dist/publish-api.d.ts +2 -1
  117. package/dist/publish-api.js +4 -3
  118. package/dist/publish-api.js.map +1 -1
  119. package/dist/pull-write.d.ts +27 -0
  120. package/dist/pull-write.js +60 -0
  121. package/dist/pull-write.js.map +1 -0
  122. package/dist/routes-api.d.ts +3 -0
  123. package/dist/routes-api.js +9 -4
  124. package/dist/routes-api.js.map +1 -1
  125. package/dist/runs-api.d.ts +7 -1
  126. package/dist/runs-api.js +21 -6
  127. package/dist/runs-api.js.map +1 -1
  128. package/dist/safe-fs.d.ts +2 -0
  129. package/dist/safe-fs.js +12 -6
  130. package/dist/safe-fs.js.map +1 -1
  131. package/dist/secret-file.d.ts +17 -0
  132. package/dist/secret-file.js +69 -0
  133. package/dist/secret-file.js.map +1 -0
  134. package/dist/skills-api.d.ts +10 -2
  135. package/dist/skills-api.js +37 -2
  136. package/dist/skills-api.js.map +1 -1
  137. package/dist/source-products-api.d.ts +63 -0
  138. package/dist/source-products-api.js +123 -0
  139. package/dist/source-products-api.js.map +1 -0
  140. package/dist/source-target.js +2 -1
  141. package/dist/source-target.js.map +1 -1
  142. package/dist/sources-api.d.ts +2 -1
  143. package/dist/sources-api.js +3 -2
  144. package/dist/sources-api.js.map +1 -1
  145. package/dist/team-connect-api.d.ts +286 -0
  146. package/dist/team-connect-api.js +107 -0
  147. package/dist/team-connect-api.js.map +1 -0
  148. package/dist/teams-api.d.ts +42 -1
  149. package/dist/teams-api.js +98 -20
  150. package/dist/teams-api.js.map +1 -1
  151. package/dist/toolchain-api.d.ts +9 -0
  152. package/dist/toolchain-api.js +17 -2
  153. package/dist/toolchain-api.js.map +1 -1
  154. package/dist/toolchain-resolve.d.ts +3 -0
  155. package/dist/toolchain-resolve.js +15 -1
  156. package/dist/toolchain-resolve.js.map +1 -1
  157. package/dist/toolchain.d.ts +6 -0
  158. package/dist/toolchain.js +19 -0
  159. package/dist/toolchain.js.map +1 -1
  160. package/dist/update-notice.js +2 -1
  161. package/dist/update-notice.js.map +1 -1
  162. package/package.json +2 -2
  163. package/templates/AGENTS.md.tmpl +6 -1
  164. package/templates/claude-skill.md.tmpl +5 -0
  165. package/templates/package.json.tmpl +4 -4
  166. package/templates/skills/build-agent/SKILL.md +115 -16
  167. package/templates/skills/build-agent/references/agent-json.md +26 -8
  168. package/templates/skills/build-agent/references/app-setup.md +2 -1
  169. package/templates/skills/build-agent/references/rollout.md +27 -4
  170. package/templates/skills/build-agent/references/set-up-an-integration.md +74 -15
  171. package/templates/skills/build-agent/references/skills.md +20 -1
  172. package/templates/skills/build-agent/references/triggers.md +84 -20
  173. package/templates/skills/build-integration/references/widget-design.md +3 -3
  174. package/templates/skills/ekanos-platform/SKILL.md +70 -17
  175. package/templates/skills/polish-widgets/references/patterns.md +2 -2
package/README.md CHANGED
@@ -28,7 +28,7 @@ npx @ekanos/cli dev
28
28
  | `status` | The read verb: login state for the resolved host, this project's `ekanos.json`, and its submissions on the host (`GET /api/partner/submissions`). Never fails just because you are logged out — it reports `authenticated: false` instead. |
29
29
  | `sources` | List every Ekanos source you hold a developer or source-admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
30
30
  | `catalog` | `ekanos catalog [<product-slug>]` — what a source can already use (`GET /api/partner/catalog`): every product enabled, switched off or available to enable, with its tools, agent triggers and generated setup steps. Check it before building something the source may already have. |
31
- | `agents` | `ekanos agents init | list | show | pull | apply | publish | rollback` — the source's agents, held by Ekanos (`/api/partner/agents`). `init` sets up a folder for designing agents with Claude (the `build-agent` and `ekanos-platform` skills, the docs MCP server, a CLAUDE.md) without scaffolding an integration. `apply` stores a new version as a draft, `publish` makes a version the one every team runs, `rollback` publishes an earlier one. Either seat applies; only the source admin publishes.
31
+ | `agents` | `ekanos agents init | list | show | pull | apply | publish | rollback | archive | unarchive` — the source's agents, held by Ekanos (`/api/partner/agents`). `init` sets up a folder for designing agents with Claude (the `build-agent` and `ekanos-platform` skills, the docs MCP server, a CLAUDE.md) without scaffolding an integration. `apply` stores a new version as a draft, `publish` makes a version the one every team runs, `rollback` publishes an earlier one, `archive` retires one no team has on. Either seat applies; only the source admin publishes or archives.
32
32
  | `reviewers` | `ekanos reviewers list \| add <email> --reach source\|team [--team <slug>] \| remove <email> [--team <slug>]` — a Source admin names who decides agent reply drafts held under an `approve` policy (`/api/partner/reviewers`). A `source`-reach reviewer decides every team's drafts; a `team`-reach reviewer one team's. The reviewer must be a member of one of the Source's teams. |
33
33
  | `use` | `ekanos use <source-slug>` — set this project's publish target to a source you hold a seat on. Refuses (`forbidden`, with your actual seats in the hint) for a slug you don't hold a seat on, so a typo can't silently repoint a project. |
34
34
  | `publish` | Validate, pack and submit the integration to an Ekanos host — refusing (`forbidden`) a target you hold no seat on, and confirming an ambiguous multi-seat target before submitting. See [Publish target verification](#publish-target-verification). |
@@ -40,14 +40,15 @@ npx @ekanos/cli dev
40
40
  npm i -g @ekanos/cli # convenient — the `ekanos` binary is always on PATH
41
41
  ```
42
42
 
43
- Inside a project, `validate`/`dev`/`publish`/`test` automatically delegate to
43
+ Inside a project, `validate`/`dev`/`test` automatically delegate to
44
44
  **that project's own** pinned `@ekanos/cli` devDependency whenever it differs
45
45
  from the globally invoked binary — the same wrapper pattern as Gradle's
46
46
  `./gradlew` or `npx`. A partner with several integrations on different
47
47
  `@ekanos/sdk` versions gets the right toolchain per project without juggling
48
48
  installs by hand. Machine-level verbs (`init`, `login`, `logout`, `whoami`,
49
- `sources`, `use`, `status`, `upgrade`) never delegate — they always run on the
50
- binary you invoked. Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
49
+ `sources`, `use`, `status`, `upgrade`) and `publish`, which sends your session
50
+ token to the host, never delegate — they always run on the binary you invoked.
51
+ Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
51
52
 
52
53
  ## Global flags
53
54
 
@@ -56,7 +57,7 @@ binary you invoked. Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
56
57
  | `--json` | Force the JSON envelope on, even at a terminal. |
57
58
  | `--no-json` | Force the human-readable form on, even when piped. |
58
59
  | `--version` | Print the CLI version and exit 0. Works with or without a verb. |
59
- | `--help` | Print usage. Also available per-verb (`ekanos validate --help`). |
60
+ | `--help` | Print usage. Per command (`ekanos teams --help`) it prints that command's subcommands and every flag with what it does; with `--json`, as `usage`, `details` and `options`. |
60
61
 
61
62
  Short flags are not supported — the parser rejects them rather than guessing.
62
63
 
@@ -437,7 +438,22 @@ that _creates_ credentials, and `ekanos.json` is a committed file; if login
437
438
  honored it, anyone could point your device flow at their own tenant just by
438
439
  editing a file in a repo you clone. Every other verb is safe reading it,
439
440
  because their bearer tokens are looked up per-host from the credential store —
440
- an unfamiliar host in `ekanos.json` simply has no session to present.
441
+ an unfamiliar host in `ekanos.json` simply has no session to present. Nor will
442
+ the CLI suggest one: a "not logged in" hint names `ekanos login --host <url>`
443
+ only for a host you have logged in to or an Ekanos domain. For any other host
444
+ it says where the host came from and asks you to check it before logging in
445
+ yourself, because a coding agent follows hints.
446
+
447
+ A host you have never logged in to is **new** unless it is loopback or under
448
+ Ekanos's own domains (`vastlyfusion.site` and its subdomains); a Source's
449
+ custom domain is new the first time. `login` to a new host shows the host and
450
+ the Auth0 domain it signs in through and asks before the browser step. With
451
+ nobody to ask (JSON mode, an agent, no TTY) it exits 7 unless you pass
452
+ `--trust-host`. The sign-in domain must be `*.auth0.com`.
453
+
454
+ A remote `http://` host is refused (exit 9) by `login` and every verb that
455
+ sends credentials; `localhost`, `127.0.0.1`, `[::1]` and `*.localhost` stay
456
+ allowed. `logout` still removes a session stored for one, with a warning.
441
457
 
442
458
  So the common single-deployment flow is: `ekanos login --host <url>` once, then
443
459
  every later verb with no flag at all. The first successful `ekanos publish`
@@ -476,8 +492,9 @@ envelope, not at any log level.
476
492
  - `login` with a session that still works reports `already-authenticated` and
477
493
  never opens a browser. `--force` starts a fresh login regardless.
478
494
  - `logout` twice succeeds; the second run reports `cleared: false`.
479
- - `logout` revokes at Auth0 first, but a failed revocation never stops the local
480
- delete — it is reported as `revoked: false` with a warning on stderr.
495
+ - `logout` ends the server-side session (`sessionRevoked`) and revokes at Auth0
496
+ (`revoked`) first, but a failed revocation never stops the local delete — it
497
+ is reported as `false` with a warning on stderr.
481
498
 
482
499
  ### Rotation safety
483
500
 
@@ -1,4 +1,4 @@
1
- import { type AgentTemplateApplyRequest, type AgentTemplateList, type AgentTemplateResult, type AgentTemplateShow } from '@ekanos/integration-schema';
1
+ import { type AgentTemplateApplyRequest, type AgentTemplateArchiveResult, type AgentTemplateList, type AgentTemplateResult, type AgentTemplateShow } from '@ekanos/integration-schema';
2
2
  /**
3
3
  * The CLI's client for Source agent templates (plan 239.0 / 239.2).
4
4
  *
@@ -9,14 +9,17 @@ import { type AgentTemplateApplyRequest, type AgentTemplateList, type AgentTempl
9
9
  * GET /api/partner/agents?source=<slug|id> → { ok, data: AgentTemplateList }
10
10
  * GET /api/partner/agents/<key>?source=<slug|id> → { ok, data: AgentTemplateShow }
11
11
  * POST /api/partner/agents AgentTemplateApplyRequest → { ok, data: AgentTemplateResult }
12
- * POST /api/partner/agents/<key>/publish { source, version? } → { ok, data: AgentTemplateResult }
12
+ * POST /api/partner/agents/<key>/publish { source, version?, confirmAccess? } → { ok, data: AgentTemplateResult }
13
13
  * POST /api/partner/agents/<key>/rollback { source, version } → { ok, data: AgentTemplateResult }
14
+ * POST /api/partner/agents/<key>/archive | unarchive { source } → { ok, data: AgentTemplateArchiveResult }
14
15
  *
15
16
  * → 401 / 3xx refresh once and retry
16
17
  * → 400 { code: "INVALID_AGENT", issues: AgentTemplateIssue[] }
17
18
  * → 403 { code: "MFA_REQUIRED" | "FORBIDDEN" | "SOURCE_ADMIN_REQUIRED" }
18
19
  * → 404 { code: "SOURCE_NOT_FOUND" | "TEMPLATE_NOT_FOUND" | "VERSION_NOT_FOUND" }
19
20
  * → 409 { code: "VERSION_CONFLICT", latestVersion }
21
+ * → 409 { code: "TEMPLATE_ARCHIVED" } apply, publish, rollback
22
+ * → 409 { code: "TEMPLATE_IN_USE", teams } archive: a team has it on
20
23
  * ---------------------------------------------------------------------------
21
24
  */
22
25
  export type AgentsApiResult<T> = {
@@ -32,4 +35,10 @@ export declare function publishAgentTemplate(host: string, accessToken: string,
32
35
  source: string;
33
36
  key: string;
34
37
  version?: number;
38
+ /** Sent only when set: an older host refuses it rather than ignoring it. */
39
+ confirmAccess?: boolean;
35
40
  }): Promise<AgentsApiResult<AgentTemplateResult>>;
41
+ export declare function archiveAgentTemplate(host: string, accessToken: string, action: 'archive' | 'unarchive', params: {
42
+ source: string;
43
+ key: string;
44
+ }): Promise<AgentsApiResult<AgentTemplateArchiveResult>>;
@@ -1,4 +1,4 @@
1
- import { AgentTemplateIssueSchema, AgentTemplateListSchema, AgentTemplateResultSchema, AgentTemplateShowSchema, } from '@ekanos/integration-schema';
1
+ import { AgentTemplateArchiveResultSchema, AgentTemplateIssueSchema, AgentTemplateListSchema, AgentTemplateResultSchema, AgentTemplateShowSchema, AgentTemplateTeamInUseSchema, } from '@ekanos/integration-schema';
2
2
  import { request } from './auth/ekanos-api.js';
3
3
  import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, validationError, } from './errors.js';
4
4
  const ROUTE = '/api/partner/agents';
@@ -28,10 +28,19 @@ export function publishAgentTemplate(host, accessToken, action, params) {
28
28
  return call(host, accessToken, AgentTemplateResultSchema, {
29
29
  method: 'POST',
30
30
  path: `${ROUTE}/${encodeURIComponent(params.key)}/${action}`,
31
- body: { source: params.source, version: params.version },
31
+ body: Object.assign({ source: params.source, version: params.version }, (params.confirmAccess ? { confirmAccess: true } : {})),
32
32
  subject: params.key,
33
33
  });
34
34
  }
35
+ export function archiveAgentTemplate(host, accessToken, action, params) {
36
+ return call(host, accessToken, AgentTemplateArchiveResultSchema, {
37
+ method: 'POST',
38
+ path: `${ROUTE}/${encodeURIComponent(params.key)}/${action}`,
39
+ body: { source: params.source },
40
+ subject: params.key,
41
+ source: params.source,
42
+ });
43
+ }
35
44
  async function call(host, accessToken, schema, init) {
36
45
  const response = await request(`${host}${init.path}`, {
37
46
  method: init.method,
@@ -62,7 +71,7 @@ async function call(host, accessToken, schema, init) {
62
71
  }
63
72
  if (response.status === 403) {
64
73
  throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
65
- ? `Only a Source admin publishes. Ask one to run it, or apply as a draft.`
74
+ ? `Ask a Source admin to run it. A developer seat applies drafts; a Source admin publishes, rolls back and archives.`
66
75
  : `Run "ekanos sources" to list the sources you hold a seat on.`);
67
76
  }
68
77
  if (response.status === 404) {
@@ -76,6 +85,25 @@ async function call(host, accessToken, schema, init) {
76
85
  (latest ? ` (now version ${latest}).` : '.'), `Run "ekanos agents show ${init.subject}" to see the latest, then apply ` +
77
86
  `again — or pass --overwrite to replace it.`);
78
87
  }
88
+ if (response.status === 409 && code === 'TEMPLATE_ARCHIVED') {
89
+ throw invalidStateError(message !== null && message !== void 0 ? message : `"${init.subject}" is archived.`, `Run "ekanos agents unarchive ${init.subject}" first.`);
90
+ }
91
+ if (response.status === 409 && code === 'TEMPLATE_IN_USE') {
92
+ const teams = readTeamsInUse(body.teams);
93
+ const source = init.source ? ` --source ${init.source}` : '';
94
+ throw invalidStateError([
95
+ message !== null && message !== void 0 ? message : `"${init.subject}" is turned on for a team.`,
96
+ ...teams.map((team) => ` ${team.team}: ${team.agent} on v${team.version}` +
97
+ (team.draft
98
+ ? ' (testing the draft)'
99
+ : team.pinned
100
+ ? ' (pinned)'
101
+ : '')),
102
+ ].join('\n'), [
103
+ 'Turn it off on each team, then archive again:',
104
+ ...teams.map((team) => ` ekanos teams agents disable ${team.team} ${team.agent}${source} --yes`),
105
+ ].join('\n'));
106
+ }
79
107
  if (!response.ok) {
80
108
  throw networkError(`${host}${init.path.split('?')[0]} responded ${response.status}.`, response.status >= 500
81
109
  ? `The Ekanos deployment returned a server error. Retry shortly.`
@@ -87,6 +115,14 @@ async function call(host, accessToken, schema, init) {
87
115
  }
88
116
  return { status: 'ok', data: parsed.data };
89
117
  }
118
+ function readTeamsInUse(value) {
119
+ if (!Array.isArray(value))
120
+ return [];
121
+ return value.flatMap((team) => {
122
+ const parsed = AgentTemplateTeamInUseSchema.safeParse(team);
123
+ return parsed.success ? [parsed.data] : [];
124
+ });
125
+ }
90
126
  function readIssues(value) {
91
127
  if (!Array.isArray(value))
92
128
  return [];
@@ -1 +1 @@
1
- {"version":3,"file":"agents-api.js","sourceRoot":"","sources":["../src/agents-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentTemplateApplyRequest,\n type AgentTemplateIssue,\n AgentTemplateIssueSchema,\n type AgentTemplateList,\n AgentTemplateListSchema,\n type AgentTemplateResult,\n AgentTemplateResultSchema,\n type AgentTemplateShow,\n AgentTemplateShowSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for Source agent templates (plan 239.0 / 239.2).\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `AgentTemplate*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/agents` routes build and this parses.\n *\n * GET /api/partner/agents?source=<slug|id> → { ok, data: AgentTemplateList }\n * GET /api/partner/agents/<key>?source=<slug|id> → { ok, data: AgentTemplateShow }\n * POST /api/partner/agents AgentTemplateApplyRequest → { ok, data: AgentTemplateResult }\n * POST /api/partner/agents/<key>/publish { source, version? } → { ok, data: AgentTemplateResult }\n * POST /api/partner/agents/<key>/rollback { source, version } → { ok, data: AgentTemplateResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_AGENT\", issues: AgentTemplateIssue[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEMPLATE_NOT_FOUND\" | \"VERSION_NOT_FOUND\" }\n * → 409 { code: \"VERSION_CONFLICT\", latestVersion }\n * ---------------------------------------------------------------------------\n */\n\nexport type AgentsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/agents';\n\nexport function listAgentTemplates(\n host: string,\n accessToken: string,\n source: string,\n): Promise<AgentsApiResult<AgentTemplateList>> {\n return call(host, accessToken, AgentTemplateListSchema, {\n method: 'GET',\n path: `${ROUTE}?${new URLSearchParams({ source })}`,\n subject: source,\n });\n}\n\nexport function showAgentTemplate(\n host: string,\n accessToken: string,\n source: string,\n key: string,\n): Promise<AgentsApiResult<AgentTemplateShow>> {\n return call(host, accessToken, AgentTemplateShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(key)}?${new URLSearchParams({ source })}`,\n subject: key,\n });\n}\n\nexport function applyAgentTemplate(\n host: string,\n accessToken: string,\n body: AgentTemplateApplyRequest,\n): Promise<AgentsApiResult<AgentTemplateResult>> {\n return call(host, accessToken, AgentTemplateResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.key,\n });\n}\n\nexport function publishAgentTemplate(\n host: string,\n accessToken: string,\n action: 'publish' | 'rollback',\n params: { source: string; key: string; version?: number },\n): Promise<AgentsApiResult<AgentTemplateResult>> {\n return call(host, accessToken, AgentTemplateResultSchema, {\n method: 'POST',\n path: `${ROUTE}/${encodeURIComponent(params.key)}/${action}`,\n body: { source: params.source, version: params.version },\n subject: params.key,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n /** What a 404 is about, for the message. */\n subject: string;\n },\n): Promise<AgentsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_AGENT') {\n const issues = readIssues(body.issues);\n throw validationError(\n [\n message ?? 'The host refused the agent.',\n ...issues.map((issue) => ` ${issue.path}: ${issue.message}`),\n ].join('\\n'),\n `Fix the agent and apply again. \"ekanos catalog\" lists the products ` +\n `and tools this Source has.`,\n );\n }\n\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos agents --help\" for each subcommand\\'s arguments.',\n );\n }\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? `Only a Source admin publishes. Ask one to run it, or apply as a draft.`\n : `Run \"ekanos sources\" to list the sources you hold a seat on.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? `Run \"ekanos sources\" to list the sources you hold a seat on.`\n : `Run \"ekanos agents list\" to see this Source's agents and versions.`,\n );\n }\n\n if (response.status === 409 && code === 'VERSION_CONFLICT') {\n const latest =\n typeof body.latestVersion === 'number' ? body.latestVersion : null;\n throw invalidStateError(\n message ??\n `\"${init.subject}\" changed since you read it` +\n (latest ? ` (now version ${latest}).` : '.'),\n `Run \"ekanos agents show ${init.subject}\" to see the latest, then apply ` +\n `again — or pass --overwrite to replace it.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly.`\n : `Confirm the host runs a build that includes the agents routes.`,\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n `Run \"ekanos upgrade\" — the CLI and the host disagree on the agents contract.`,\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\nfunction readIssues(value: unknown): AgentTemplateIssue[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap((issue) => {\n const parsed = AgentTemplateIssueSchema.safeParse(issue);\n return parsed.success ? [parsed.data] : [];\n });\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
1
+ {"version":3,"file":"agents-api.js","sourceRoot":"","sources":["../src/agents-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentTemplateApplyRequest,\n type AgentTemplateArchiveResult,\n AgentTemplateArchiveResultSchema,\n type AgentTemplateIssue,\n AgentTemplateIssueSchema,\n type AgentTemplateList,\n AgentTemplateListSchema,\n type AgentTemplateResult,\n AgentTemplateResultSchema,\n type AgentTemplateShow,\n AgentTemplateShowSchema,\n type AgentTemplateTeamInUse,\n AgentTemplateTeamInUseSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for Source agent templates (plan 239.0 / 239.2).\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `AgentTemplate*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/agents` routes build and this parses.\n *\n * GET /api/partner/agents?source=<slug|id> → { ok, data: AgentTemplateList }\n * GET /api/partner/agents/<key>?source=<slug|id> → { ok, data: AgentTemplateShow }\n * POST /api/partner/agents AgentTemplateApplyRequest → { ok, data: AgentTemplateResult }\n * POST /api/partner/agents/<key>/publish { source, version?, confirmAccess? } → { ok, data: AgentTemplateResult }\n * POST /api/partner/agents/<key>/rollback { source, version } → { ok, data: AgentTemplateResult }\n * POST /api/partner/agents/<key>/archive | unarchive { source } → { ok, data: AgentTemplateArchiveResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_AGENT\", issues: AgentTemplateIssue[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEMPLATE_NOT_FOUND\" | \"VERSION_NOT_FOUND\" }\n * → 409 { code: \"VERSION_CONFLICT\", latestVersion }\n * → 409 { code: \"TEMPLATE_ARCHIVED\" } apply, publish, rollback\n * → 409 { code: \"TEMPLATE_IN_USE\", teams } archive: a team has it on\n * ---------------------------------------------------------------------------\n */\n\nexport type AgentsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/agents';\n\nexport function listAgentTemplates(\n host: string,\n accessToken: string,\n source: string,\n): Promise<AgentsApiResult<AgentTemplateList>> {\n return call(host, accessToken, AgentTemplateListSchema, {\n method: 'GET',\n path: `${ROUTE}?${new URLSearchParams({ source })}`,\n subject: source,\n });\n}\n\nexport function showAgentTemplate(\n host: string,\n accessToken: string,\n source: string,\n key: string,\n): Promise<AgentsApiResult<AgentTemplateShow>> {\n return call(host, accessToken, AgentTemplateShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(key)}?${new URLSearchParams({ source })}`,\n subject: key,\n });\n}\n\nexport function applyAgentTemplate(\n host: string,\n accessToken: string,\n body: AgentTemplateApplyRequest,\n): Promise<AgentsApiResult<AgentTemplateResult>> {\n return call(host, accessToken, AgentTemplateResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.key,\n });\n}\n\nexport function publishAgentTemplate(\n host: string,\n accessToken: string,\n action: 'publish' | 'rollback',\n params: {\n source: string;\n key: string;\n version?: number;\n /** Sent only when set: an older host refuses it rather than ignoring it. */\n confirmAccess?: boolean;\n },\n): Promise<AgentsApiResult<AgentTemplateResult>> {\n return call(host, accessToken, AgentTemplateResultSchema, {\n method: 'POST',\n path: `${ROUTE}/${encodeURIComponent(params.key)}/${action}`,\n body: {\n source: params.source,\n version: params.version,\n ...(params.confirmAccess ? { confirmAccess: true } : {}),\n },\n subject: params.key,\n });\n}\n\nexport function archiveAgentTemplate(\n host: string,\n accessToken: string,\n action: 'archive' | 'unarchive',\n params: { source: string; key: string },\n): Promise<AgentsApiResult<AgentTemplateArchiveResult>> {\n return call(host, accessToken, AgentTemplateArchiveResultSchema, {\n method: 'POST',\n path: `${ROUTE}/${encodeURIComponent(params.key)}/${action}`,\n body: { source: params.source },\n subject: params.key,\n source: params.source,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n /** What a 404 is about, for the message. */\n subject: string;\n /** For the commands a refusal suggests. */\n source?: string;\n },\n): Promise<AgentsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_AGENT') {\n const issues = readIssues(body.issues);\n throw validationError(\n [\n message ?? 'The host refused the agent.',\n ...issues.map((issue) => ` ${issue.path}: ${issue.message}`),\n ].join('\\n'),\n `Fix the agent and apply again. \"ekanos catalog\" lists the products ` +\n `and tools this Source has.`,\n );\n }\n\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos agents --help\" for each subcommand\\'s arguments.',\n );\n }\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? `Ask a Source admin to run it. A developer seat applies drafts; a Source admin publishes, rolls back and archives.`\n : `Run \"ekanos sources\" to list the sources you hold a seat on.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? `Run \"ekanos sources\" to list the sources you hold a seat on.`\n : `Run \"ekanos agents list\" to see this Source's agents and versions.`,\n );\n }\n\n if (response.status === 409 && code === 'VERSION_CONFLICT') {\n const latest =\n typeof body.latestVersion === 'number' ? body.latestVersion : null;\n throw invalidStateError(\n message ??\n `\"${init.subject}\" changed since you read it` +\n (latest ? ` (now version ${latest}).` : '.'),\n `Run \"ekanos agents show ${init.subject}\" to see the latest, then apply ` +\n `again — or pass --overwrite to replace it.`,\n );\n }\n\n if (response.status === 409 && code === 'TEMPLATE_ARCHIVED') {\n throw invalidStateError(\n message ?? `\"${init.subject}\" is archived.`,\n `Run \"ekanos agents unarchive ${init.subject}\" first.`,\n );\n }\n\n if (response.status === 409 && code === 'TEMPLATE_IN_USE') {\n const teams = readTeamsInUse(body.teams);\n const source = init.source ? ` --source ${init.source}` : '';\n throw invalidStateError(\n [\n message ?? `\"${init.subject}\" is turned on for a team.`,\n ...teams.map(\n (team) =>\n ` ${team.team}: ${team.agent} on v${team.version}` +\n (team.draft\n ? ' (testing the draft)'\n : team.pinned\n ? ' (pinned)'\n : ''),\n ),\n ].join('\\n'),\n [\n 'Turn it off on each team, then archive again:',\n ...teams.map(\n (team) =>\n ` ekanos teams agents disable ${team.team} ${team.agent}${source} --yes`,\n ),\n ].join('\\n'),\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly.`\n : `Confirm the host runs a build that includes the agents routes.`,\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n `Run \"ekanos upgrade\" — the CLI and the host disagree on the agents contract.`,\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\nfunction readTeamsInUse(value: unknown): AgentTemplateTeamInUse[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap((team) => {\n const parsed = AgentTemplateTeamInUseSchema.safeParse(team);\n return parsed.success ? [parsed.data] : [];\n });\n}\n\nfunction readIssues(value: unknown): AgentTemplateIssue[] {\n if (!Array.isArray(value)) return [];\n return value.flatMap((issue) => {\n const parsed = AgentTemplateIssueSchema.safeParse(issue);\n return parsed.success ? [parsed.data] : [];\n });\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
@@ -22,6 +22,7 @@ export declare function showProviderApp(host: string, accessToken: string, sourc
22
22
  export declare function setProviderApp(host: string, accessToken: string, body: {
23
23
  source: string;
24
24
  product: string;
25
- clientId: string;
26
- clientSecret: string;
25
+ /** Either or both: one left out keeps what is stored. */
26
+ clientId?: string;
27
+ clientSecret?: string;
27
28
  }): Promise<AppsApiResult<ProviderAppSetResult>>;
package/dist/apps-api.js CHANGED
@@ -11,11 +11,7 @@ export function setProviderApp(host, accessToken, body) {
11
11
  return call(host, accessToken, ProviderAppSetResultSchema, body.product, {
12
12
  method: 'PUT',
13
13
  path: route(body.product),
14
- body: {
15
- source: body.source,
16
- clientId: body.clientId,
17
- clientSecret: body.clientSecret,
18
- },
14
+ body: Object.assign(Object.assign({ source: body.source }, (body.clientId ? { clientId: body.clientId } : {})), (body.clientSecret ? { clientSecret: body.clientSecret } : {})),
19
15
  });
20
16
  }
21
17
  function route(product) {
@@ -46,11 +42,11 @@ async function call(host, accessToken, schema, product, init) {
46
42
  }
47
43
  if (response.status === 404) {
48
44
  throw notFoundError(message !== null && message !== void 0 ? message : `"${product}" was not found.`, code === 'PRODUCT_NOT_ENTITLED'
49
- ? `"ekanos catalog" lists what this Source has; Ekanos enables a product for a Source.`
45
+ ? `"ekanos sources products" shows what this Source has; only a super-admin enables a product for a Source (in /admin).`
50
46
  : 'Run "ekanos sources" to list the sources you hold a seat on.');
51
47
  }
52
48
  if (response.status === 409 && code === 'NO_PROVIDER_APP') {
53
- throw invalidStateError(message !== null && message !== void 0 ? message : `"${product}" has no app for the Source to set up.`, `Each team connects ${product} with its own app, from the team's Integrations page. "ekanos catalog ${product}" shows its setup steps.`);
49
+ throw invalidStateError(message !== null && message !== void 0 ? message : `"${product}" has no app for the Source to set up.`, `Each team connects ${product} with its own app: "ekanos teams app set <team> ${product}", then "ekanos teams connect <team> ${product}". "ekanos catalog ${product}" shows its setup steps.`);
54
50
  }
55
51
  if (response.status === 400) {
56
52
  throw usageError(message !== null && message !== void 0 ? message : 'The host refused the request.', 'See "ekanos apps --help".');
@@ -1 +1 @@
1
- {"version":3,"file":"apps-api.js","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ProviderAppSetResult,\n ProviderAppSetResultSchema,\n type ProviderAppStatus,\n ProviderAppStatusSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's provider apps — the OAuth client an\n * integration signs every team in through.\n *\n * GET /api/partner/apps/<product>?source=<slug|id> → { ok, data: ProviderAppStatus }\n * PUT /api/partner/apps/<product> { source, clientId, clientSecret }\n * → { ok, data: ProviderAppStatus & { replaced } }\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"PRODUCT_NOT_ENTITLED\" | \"SOURCE_NOT_FOUND\" }\n * → 409 { code: \"NO_PROVIDER_APP\" }\n */\n\nexport type AppsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nexport function showProviderApp(\n host: string,\n accessToken: string,\n source: string,\n product: string,\n): Promise<AppsApiResult<ProviderAppStatus>> {\n return call(host, accessToken, ProviderAppStatusSchema, product, {\n method: 'GET',\n path: `${route(product)}?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function setProviderApp(\n host: string,\n accessToken: string,\n body: {\n source: string;\n product: string;\n clientId: string;\n clientSecret: string;\n },\n): Promise<AppsApiResult<ProviderAppSetResult>> {\n return call(host, accessToken, ProviderAppSetResultSchema, body.product, {\n method: 'PUT',\n path: route(body.product),\n body: {\n source: body.source,\n clientId: body.clientId,\n clientSecret: body.clientSecret,\n },\n });\n}\n\nfunction route(product: string): string {\n return `/api/partner/apps/${encodeURIComponent(product)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n product: string,\n init: { method: 'GET' | 'PUT'; path: string; body?: unknown },\n): Promise<AppsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? \"Only a Source admin manages the Source's apps. Ask one to run it.\"\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${product}\" was not found.`,\n code === 'PRODUCT_NOT_ENTITLED'\n ? `\"ekanos catalog\" lists what this Source has; Ekanos enables a product for a Source.`\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 && code === 'NO_PROVIDER_APP') {\n throw invalidStateError(\n message ?? `\"${product}\" has no app for the Source to set up.`,\n `Each team connects ${product} with its own app, from the team's Integrations page. \"ekanos catalog ${product}\" shows its setup steps.`,\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos apps --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the apps routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}/api/partner/apps answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the apps contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
1
+ {"version":3,"file":"apps-api.js","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type ProviderAppSetResult,\n ProviderAppSetResultSchema,\n type ProviderAppStatus,\n ProviderAppStatusSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's provider apps — the OAuth client an\n * integration signs every team in through.\n *\n * GET /api/partner/apps/<product>?source=<slug|id> → { ok, data: ProviderAppStatus }\n * PUT /api/partner/apps/<product> { source, clientId, clientSecret }\n * → { ok, data: ProviderAppStatus & { replaced } }\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"PRODUCT_NOT_ENTITLED\" | \"SOURCE_NOT_FOUND\" }\n * → 409 { code: \"NO_PROVIDER_APP\" }\n */\n\nexport type AppsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nexport function showProviderApp(\n host: string,\n accessToken: string,\n source: string,\n product: string,\n): Promise<AppsApiResult<ProviderAppStatus>> {\n return call(host, accessToken, ProviderAppStatusSchema, product, {\n method: 'GET',\n path: `${route(product)}?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function setProviderApp(\n host: string,\n accessToken: string,\n body: {\n source: string;\n product: string;\n /** Either or both: one left out keeps what is stored. */\n clientId?: string;\n clientSecret?: string;\n },\n): Promise<AppsApiResult<ProviderAppSetResult>> {\n return call(host, accessToken, ProviderAppSetResultSchema, body.product, {\n method: 'PUT',\n path: route(body.product),\n body: {\n source: body.source,\n ...(body.clientId ? { clientId: body.clientId } : {}),\n ...(body.clientSecret ? { clientSecret: body.clientSecret } : {}),\n },\n });\n}\n\nfunction route(product: string): string {\n return `/api/partner/apps/${encodeURIComponent(product)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n product: string,\n init: { method: 'GET' | 'PUT'; path: string; body?: unknown },\n): Promise<AppsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? \"Only a Source admin manages the Source's apps. Ask one to run it.\"\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${product}\" was not found.`,\n code === 'PRODUCT_NOT_ENTITLED'\n ? `\"ekanos sources products\" shows what this Source has; only a super-admin enables a product for a Source (in /admin).`\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 && code === 'NO_PROVIDER_APP') {\n throw invalidStateError(\n message ?? `\"${product}\" has no app for the Source to set up.`,\n `Each team connects ${product} with its own app: \"ekanos teams app set <team> ${product}\", then \"ekanos teams connect <team> ${product}\". \"ekanos catalog ${product}\" shows its setup steps.`,\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos apps --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the apps routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}/api/partner/apps answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the apps contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
@@ -37,12 +37,26 @@ export interface RefreshOptions {
37
37
  */
38
38
  onRotatedToken?: (rotated: string) => void;
39
39
  }
40
- /** Re-mint an Ekanos JWT from the stored Auth0 refresh token. */
41
- export declare function refreshEkanosSession(host: string, auth0RefreshToken: string, options?: RefreshOptions): Promise<RefreshedSession>;
40
+ /**
41
+ * Re-mint an Ekanos JWT from the stored Auth0 refresh token. `accessToken` is
42
+ * the previous Ekanos JWT (usually expired): the server reads the session id
43
+ * from it to enforce that session's idle timeout, maximum age and revocation.
44
+ */
45
+ export declare function refreshEkanosSession(host: string, credentials: {
46
+ auth0RefreshToken: string;
47
+ accessToken: string;
48
+ }, options?: RefreshOptions): Promise<RefreshedSession>;
42
49
  /**
43
50
  * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can
44
51
  * decide whether to refresh and retry — an expired access token is an ordinary
45
52
  * event, not an error.
46
53
  */
47
54
  export declare function fetchIdentity(host: string, accessToken: string): Promise<EkanosIdentity | null>;
55
+ /**
56
+ * End the server-side session behind an Ekanos JWT (`ekanos logout`). Returns
57
+ * whether the server answered — `true` also when the session was already
58
+ * revoked, since either way it can no longer be refreshed. Never throws for
59
+ * an HTTP status: logout must always reach the local delete.
60
+ */
61
+ export declare function revokeEkanosSession(host: string, accessToken: string): Promise<boolean>;
48
62
  export declare function request(url: string, init: RequestInit): Promise<Response>;
@@ -1,6 +1,6 @@
1
- import { authRequiredError, networkError, preconditionError } from '../errors.js';
1
+ import { authRequiredError, forbiddenError, networkError, preconditionError, } from '../errors.js';
2
2
  /**
3
- * The CLI's client for the four Ekanos auth routes.
3
+ * The CLI's client for the five Ekanos auth routes.
4
4
  *
5
5
  * ---------------------------------------------------------------------------
6
6
  * WIRE CONTRACT — the seam between this package and the Ekanos web app.
@@ -22,17 +22,33 @@ import { authRequiredError, networkError, preconditionError } from '../errors.js
22
22
  * → 400 { error, code: "INVALID_REQUEST" } malformed / wrong shape
23
23
  * → 401 { error, code: "SESSION_EXPIRED" } Auth0 said no, OR no
24
24
  * Ekanos account for it
25
+ * → 403 { error, code: "NO_DEVELOPER_SEAT" } no developer/admin seat
26
+ * → 403 { error, code: "EMAIL_UNVERIFIED" } Auth0 email unverified
27
+ * → 429 { error, code: "RATE_LIMITED" } + Retry-After header
25
28
  * → 503 { error, code: "CLI_NOT_CONFIGURED" } deployment not provisioned
26
29
  * → 500 { error, code: "INTERNAL_ERROR" }
30
+ * `error` on a 403 is a sentence written for the person; show it.
27
31
  *
28
32
  * POST /api/auth/cli-refresh [no auth]
29
- * ← { auth0_refresh_token: string }
33
+ * ← { auth0_refresh_token: string, access_token: string }
34
+ * access_token is the PREVIOUS Ekanos JWT (expired is fine): it names
35
+ * the server-side session this refresh continues. Required: without
36
+ * it the server answers 401 SESSION_EXPIRED, reason
37
+ * "previous_token_missing" (an older CLI is told to log in again).
30
38
  * → 200 { access_token, expires_at, user: { id, email },
31
39
  * auth0_refresh_token } ← ROTATED; the presented one is dead
32
40
  * → 400 { error, code: "INVALID_REQUEST" }
33
- * → 401 { error, code: "SESSION_EXPIRED" } expired or revoked
41
+ * → 401 { error, code: "SESSION_EXPIRED", reason? }
42
+ * expired, revoked, idle
43
+ * or past the session's
44
+ * maximum age; checked
45
+ * BEFORE rotation, so no
46
+ * rotated token comes back
47
+ * → 403 { error, code: "EMAIL_UNVERIFIED",
48
+ * auth0_refresh_token }
34
49
  * → 404 { error, code: "ACCOUNT_NOT_FOUND",
35
50
  * auth0_refresh_token } no Ekanos account
51
+ * → 429 { error, code: "RATE_LIMITED" } + Retry-After header
36
52
  * → 502 { error, code: "TOKEN_EXCHANGE_FAILED" } Auth0 returned no
37
53
  * rotated token
38
54
  * → 503 { error, code: "CLI_NOT_CONFIGURED" } deployment not provisioned
@@ -51,8 +67,16 @@ import { authRequiredError, networkError, preconditionError } from '../errors.js
51
67
  * branch on `code`, not on the status alone.
52
68
  *
53
69
  * All three routes above read the Fusion CLI Auth0 application config, so all
54
- * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami cannot — it reads no
55
- * Auth0 config, only the caller's bearer token.
70
+ * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami and cli-logout cannot
71
+ * — they read no Auth0 config, only an Ekanos token.
72
+ *
73
+ * POST /api/auth/cli-logout [no auth]
74
+ * ← { access_token: string } the stored Ekanos JWT; expired is fine
75
+ * → 200 { revoked: boolean } false = already revoked / no session row
76
+ * → 400 { error, code: "INVALID_REQUEST" }
77
+ * → 401 { error, code: "AUTH_REQUIRED" } not a valid Ekanos token
78
+ * → 429 { error, code: "RATE_LIMITED" }
79
+ * → 500 { error, code: "INTERNAL_ERROR" }
56
80
  *
57
81
  * GET /api/auth/cli-whoami [Authorization: Bearer <access_token>]
58
82
  * → 200 { user: { id, email, name } }
@@ -102,15 +126,19 @@ export async function fetchCliAuthConfig(host) {
102
126
  `/api/auth/cli-config route.`);
103
127
  }
104
128
  // The endpoints come from the server, so they are attacker-controlled if the
105
- // host is. Pin them to the Auth0 domain the same response declares, so a
106
- // compromised or misconfigured Ekanos cannot redirect the device flow — and
107
- // the resulting credentials — to an arbitrary origin.
129
+ // host is. Pin the declared domain to Auth0 itself, then pin the endpoints
130
+ // to that domain, so a compromised or misconfigured Ekanos cannot redirect
131
+ // the device flow — and the resulting credentials — to an arbitrary origin.
132
+ // This does not prove the host is genuine: any Auth0 tenant passes, and a
133
+ // hostile host can name Ekanos's own. That is `login`'s new-host question.
134
+ assertAuth0Domain(config.auth0Domain);
108
135
  assertOnDomain(config.deviceAuthorizationEndpoint, config.auth0Domain);
109
136
  assertOnDomain(config.tokenEndpoint, config.auth0Domain);
110
137
  return config;
111
138
  }
112
139
  /** Exchange a verified Auth0 access token for an Ekanos JWT. */
113
140
  export async function createEkanosSession(host, auth0AccessToken) {
141
+ var _a;
114
142
  const response = await request(`${host}/api/auth/cli-session`, {
115
143
  method: 'POST',
116
144
  headers: { 'content-type': 'application/json', accept: 'application/json' },
@@ -122,18 +150,34 @@ export async function createEkanosSession(host, auth0AccessToken) {
122
150
  `exists on ${host} — the browser login can succeed for an identity ` +
123
151
  `that has no Ekanos account.`);
124
152
  }
153
+ if (response.status === 403) {
154
+ const body = await readOptionalJson(response);
155
+ throw forbiddenError((_a = str(body.error)) !== null && _a !== void 0 ? _a : `${host} refused a CLI session for this account.`, body.code === 'EMAIL_UNVERIFIED'
156
+ ? `Verify your email address, then run "ekanos login" again.`
157
+ : `The Ekanos CLI needs a developer seat. Ask your administrator ` +
158
+ `for one, then run "ekanos login" again.`);
159
+ }
160
+ throwIfRateLimited(response, host);
125
161
  if (!response.ok) {
126
162
  throw unexpectedStatus(host, '/api/auth/cli-session', response.status);
127
163
  }
128
164
  return toSession(await readJson(response, `${host}/api/auth/cli-session`), host);
129
165
  }
130
- /** Re-mint an Ekanos JWT from the stored Auth0 refresh token. */
131
- export async function refreshEkanosSession(host, auth0RefreshToken, options = {}) {
166
+ /**
167
+ * Re-mint an Ekanos JWT from the stored Auth0 refresh token. `accessToken` is
168
+ * the previous Ekanos JWT (usually expired): the server reads the session id
169
+ * from it to enforce that session's idle timeout, maximum age and revocation.
170
+ */
171
+ export async function refreshEkanosSession(host, credentials, options = {}) {
132
172
  var _a;
173
+ var _b;
133
174
  const response = await request(`${host}/api/auth/cli-refresh`, {
134
175
  method: 'POST',
135
176
  headers: { 'content-type': 'application/json', accept: 'application/json' },
136
- body: JSON.stringify({ auth0_refresh_token: auth0RefreshToken }),
177
+ body: JSON.stringify({
178
+ auth0_refresh_token: credentials.auth0RefreshToken,
179
+ access_token: credentials.accessToken,
180
+ }),
137
181
  });
138
182
  // Read the body FIRST, whatever the status: the rotated token can arrive on
139
183
  // a 503, a 404 or a 500 just as readily as on a 200, and it must be captured
@@ -145,10 +189,16 @@ export async function refreshEkanosSession(host, auth0RefreshToken, options = {}
145
189
  }
146
190
  throwIfNotConfigured(response, host, body);
147
191
  if (response.status === 401) {
148
- throw authRequiredError(`The stored session for ${host} is no longer valid.`, `Run "ekanos login" to sign in again. This is expected after the ` +
149
- `refresh token expires, after it is revoked in Auth0, or if two ` +
150
- `machines used the same credentials file.`);
192
+ throw authRequiredError(`The stored session for ${host} is no longer valid` +
193
+ (str(body.error) ? ` (${str(body.error)}).` : '.'), `Run "ekanos login" to sign in again. This is expected after the ` +
194
+ `session's idle timeout or maximum age, after "ekanos logout", after ` +
195
+ `the refresh token is revoked in Auth0, or if two machines used the ` +
196
+ `same credentials file.`);
197
+ }
198
+ if (response.status === 403) {
199
+ throw forbiddenError((_b = str(body.error)) !== null && _b !== void 0 ? _b : `${host} refused to renew the session.`, `Verify your email address, then run "ekanos login" again.`);
151
200
  }
201
+ throwIfRateLimited(response, host);
152
202
  if (response.status === 503 && body.code === 'REFRESH_INCOMPLETE') {
153
203
  // NOT an auth failure. The credential was renewed — and, thanks to the
154
204
  // callback above, already saved. Auth0's /userinfo is simply not answering.
@@ -206,6 +256,20 @@ export async function fetchIdentity(host, accessToken) {
206
256
  }
207
257
  return { id, email: str(user.email), name: str(user.name) };
208
258
  }
259
+ /**
260
+ * End the server-side session behind an Ekanos JWT (`ekanos logout`). Returns
261
+ * whether the server answered — `true` also when the session was already
262
+ * revoked, since either way it can no longer be refreshed. Never throws for
263
+ * an HTTP status: logout must always reach the local delete.
264
+ */
265
+ export async function revokeEkanosSession(host, accessToken) {
266
+ const response = await request(`${host}/api/auth/cli-logout`, {
267
+ method: 'POST',
268
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
269
+ body: JSON.stringify({ access_token: accessToken }),
270
+ });
271
+ return response.ok;
272
+ }
209
273
  function toSession(body, host) {
210
274
  var _a;
211
275
  const user = ((_a = body.user) !== null && _a !== void 0 ? _a : {});
@@ -279,6 +343,16 @@ function throwIfNotConfigured(response, host, body) {
279
343
  throw preconditionError(`${host} does not have CLI login configured.`, `Ask the Ekanos operator to provision the "Fusion CLI" Auth0 ` +
280
344
  `application and set AUTH0_CLI_CLIENT_ID on that deployment.`);
281
345
  }
346
+ /** 429: the deployment is throttling this machine's address. Retryable. */
347
+ function throwIfRateLimited(response, host) {
348
+ var _a;
349
+ if (response.status !== 429)
350
+ return;
351
+ const retryAfter = Number.parseInt((_a = response.headers.get('retry-after')) !== null && _a !== void 0 ? _a : '', 10);
352
+ throw networkError(`${host} is rate-limiting sign-in requests from this address.`, Number.isFinite(retryAfter) && retryAfter > 0
353
+ ? `Retry in ${retryAfter} seconds.`
354
+ : `Retry in a few minutes.`);
355
+ }
282
356
  function unexpectedStatus(host, route, status) {
283
357
  return networkError(`${host}${route} responded ${status}.`, status >= 500
284
358
  ? `The Ekanos deployment returned a server error. Retry shortly; if it ` +
@@ -286,6 +360,18 @@ function unexpectedStatus(host, route, status) {
286
360
  : `Confirm the host is an Ekanos deployment running a build that ` +
287
361
  `includes ${route}.`);
288
362
  }
363
+ /**
364
+ * Every Ekanos tenant signs in on an `*.auth0.com` domain today
365
+ * (`fusion-{dev,stage,prod}.us.auth0.com`). If one moves to an Auth0 custom
366
+ * domain, add that domain here.
367
+ */
368
+ function assertAuth0Domain(domain) {
369
+ const hostname = domain.toLowerCase();
370
+ if (/^[a-z0-9-]+(\.[a-z0-9-]+)*\.auth0\.com$/.test(hostname))
371
+ return;
372
+ throw preconditionError(`The login configuration names a sign-in domain that is not Auth0 (${domain}).`, `Do not continue. Report this to the operator of the Ekanos ` +
373
+ `deployment — it may be misconfigured or compromised.`);
374
+ }
289
375
  function assertOnDomain(endpoint, domain) {
290
376
  let url;
291
377
  try {