@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.
- package/README.md +25 -8
- package/dist/agents-api.d.ts +11 -2
- package/dist/agents-api.js +39 -3
- package/dist/agents-api.js.map +1 -1
- package/dist/apps-api.d.ts +3 -2
- package/dist/apps-api.js +3 -7
- package/dist/apps-api.js.map +1 -1
- package/dist/auth/ekanos-api.d.ts +16 -2
- package/dist/auth/ekanos-api.js +101 -15
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/auth/host-trust.d.ts +46 -0
- package/dist/auth/host-trust.js +79 -0
- package/dist/auth/host-trust.js.map +1 -0
- package/dist/auth/session.d.ts +10 -1
- package/dist/auth/session.js +28 -28
- package/dist/auth/session.js.map +1 -1
- package/dist/commands/agents-edit.d.ts +6 -2
- package/dist/commands/agents-edit.js +57 -26
- package/dist/commands/agents-edit.js.map +1 -1
- package/dist/commands/agents.d.ts +13 -2
- package/dist/commands/agents.js +148 -36
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +15 -4
- package/dist/commands/apps.js +133 -51
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.js +7 -6
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/knowledge.d.ts +56 -0
- package/dist/commands/knowledge.js +456 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +30 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +9 -1
- package/dist/commands/logout.js +56 -7
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +3 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/reviewers.d.ts +2 -0
- package/dist/commands/reviewers.js +7 -3
- package/dist/commands/reviewers.js.map +1 -1
- package/dist/commands/routes.d.ts +2 -0
- package/dist/commands/routes.js +42 -9
- package/dist/commands/routes.js.map +1 -1
- package/dist/commands/runs.d.ts +13 -2
- package/dist/commands/runs.js +96 -15
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +21 -0
- package/dist/commands/scope-notice.js +35 -0
- package/dist/commands/scope-notice.js.map +1 -0
- package/dist/commands/skills.d.ts +7 -1
- package/dist/commands/skills.js +80 -24
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/sources.d.ts +4 -0
- package/dist/commands/sources.js +233 -5
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.js +5 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +59 -0
- package/dist/commands/team-agent-readiness.js +341 -0
- package/dist/commands/team-agent-readiness.js.map +1 -0
- package/dist/commands/team-agents-agents.d.ts +24 -3
- package/dist/commands/team-agents-agents.js +336 -35
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-mcp.js +32 -3
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.js +230 -54
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +3 -1
- package/dist/commands/team-agents.js +27 -7
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/teams-channel-install.d.ts +24 -0
- package/dist/commands/teams-channel-install.js +94 -0
- package/dist/commands/teams-channel-install.js.map +1 -0
- package/dist/commands/teams-connect.d.ts +66 -0
- package/dist/commands/teams-connect.js +397 -0
- package/dist/commands/teams-connect.js.map +1 -0
- package/dist/commands/teams-events.d.ts +12 -0
- package/dist/commands/teams-events.js +138 -0
- package/dist/commands/teams-events.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +16 -5
- package/dist/commands/teams-onboard.js +61 -36
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.d.ts +11 -0
- package/dist/commands/teams-overview.js +160 -0
- package/dist/commands/teams-overview.js.map +1 -0
- package/dist/commands/teams-scope.d.ts +10 -0
- package/dist/commands/teams-scope.js +109 -0
- package/dist/commands/teams-scope.js.map +1 -0
- package/dist/commands/teams.d.ts +64 -7
- package/dist/commands/teams.js +254 -60
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/upgrade.js +12 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +2 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connections-api.js +1 -1
- package/dist/connections-api.js.map +1 -1
- package/dist/delegate.js +11 -4
- package/dist/delegate.js.map +1 -1
- package/dist/harness-scaffold.js +12 -0
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +724 -152
- package/dist/index.js.map +1 -1
- package/dist/knowledge-api.d.ts +175 -0
- package/dist/knowledge-api.js +169 -0
- package/dist/knowledge-api.js.map +1 -0
- package/dist/open-url.d.ts +16 -0
- package/dist/open-url.js +73 -0
- package/dist/open-url.js.map +1 -0
- package/dist/parse-argv.d.ts +2 -0
- package/dist/parse-argv.js.map +1 -1
- package/dist/publish-api.d.ts +2 -1
- package/dist/publish-api.js +4 -3
- package/dist/publish-api.js.map +1 -1
- package/dist/pull-write.d.ts +27 -0
- package/dist/pull-write.js +60 -0
- package/dist/pull-write.js.map +1 -0
- package/dist/routes-api.d.ts +3 -0
- package/dist/routes-api.js +9 -4
- package/dist/routes-api.js.map +1 -1
- package/dist/runs-api.d.ts +7 -1
- package/dist/runs-api.js +21 -6
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +2 -0
- package/dist/safe-fs.js +12 -6
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.js +69 -0
- package/dist/secret-file.js.map +1 -0
- package/dist/skills-api.d.ts +10 -2
- package/dist/skills-api.js +37 -2
- package/dist/skills-api.js.map +1 -1
- package/dist/source-products-api.d.ts +63 -0
- package/dist/source-products-api.js +123 -0
- package/dist/source-products-api.js.map +1 -0
- package/dist/source-target.js +2 -1
- package/dist/source-target.js.map +1 -1
- package/dist/sources-api.d.ts +2 -1
- package/dist/sources-api.js +3 -2
- package/dist/sources-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +286 -0
- package/dist/team-connect-api.js +107 -0
- package/dist/team-connect-api.js.map +1 -0
- package/dist/teams-api.d.ts +42 -1
- package/dist/teams-api.js +98 -20
- package/dist/teams-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +9 -0
- package/dist/toolchain-api.js +17 -2
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.d.ts +3 -0
- package/dist/toolchain-resolve.js +15 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +6 -0
- package/dist/toolchain.js +19 -0
- package/dist/toolchain.js.map +1 -1
- package/dist/update-notice.js +2 -1
- package/dist/update-notice.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +6 -1
- package/templates/claude-skill.md.tmpl +5 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +115 -16
- package/templates/skills/build-agent/references/agent-json.md +26 -8
- package/templates/skills/build-agent/references/app-setup.md +2 -1
- package/templates/skills/build-agent/references/rollout.md +27 -4
- package/templates/skills/build-agent/references/set-up-an-integration.md +74 -15
- package/templates/skills/build-agent/references/skills.md +20 -1
- package/templates/skills/build-agent/references/triggers.md +84 -20
- package/templates/skills/build-integration/references/widget-design.md +3 -3
- package/templates/skills/ekanos-platform/SKILL.md +70 -17
- 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`/`
|
|
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`)
|
|
50
|
-
|
|
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.
|
|
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`
|
|
480
|
-
|
|
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
|
|
package/dist/agents-api.d.ts
CHANGED
|
@@ -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>>;
|
package/dist/agents-api.js
CHANGED
|
@@ -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
|
-
? `
|
|
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 [];
|
package/dist/agents-api.js.map
CHANGED
|
@@ -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"]}
|
package/dist/apps-api.d.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
|
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,
|
|
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".');
|
package/dist/apps-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apps-api.js","sourceRoot":"","sources":["../src/apps-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,0BAA0B,EAE1B,uBAAuB,GACxB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,UAAU,GACX,MAAM,UAAU,CAAC;AAoBlB,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,WAAmB,EACnB,MAAc,EACd,OAAe;IAEf,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,uBAAuB,EAAE,OAAO,EAAE;QAC/D,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;KAC7D,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,IAAY,EACZ,WAAmB,EACnB,
|
|
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
|
-
/**
|
|
41
|
-
|
|
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>;
|
package/dist/auth/ekanos-api.js
CHANGED
|
@@ -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
|
|
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" }
|
|
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
|
|
55
|
-
* Auth0 config, only
|
|
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
|
|
106
|
-
// compromised or misconfigured Ekanos cannot redirect
|
|
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
|
-
/**
|
|
131
|
-
|
|
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({
|
|
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
|
|
149
|
-
`
|
|
150
|
-
`
|
|
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 {
|