@ekanos/cli 0.15.0 → 0.16.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 +41 -4
- package/dist/auth/ekanos-api.d.ts +23 -0
- package/dist/auth/ekanos-api.js +33 -6
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/commands/agents.js +3 -2
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +11 -3
- package/dist/commands/apps.js +86 -28
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.d.ts +4 -2
- package/dist/commands/catalog.js +166 -35
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +17 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/runs.d.ts +7 -3
- package/dist/commands/runs.js +21 -5
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +7 -0
- package/dist/commands/scope-notice.js +4 -11
- package/dist/commands/scope-notice.js.map +1 -1
- package/dist/commands/sources-roles.d.ts +5 -0
- package/dist/commands/sources-roles.js +235 -0
- package/dist/commands/sources-roles.js.map +1 -0
- package/dist/commands/sources.d.ts +5 -0
- package/dist/commands/sources.js +6 -1
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +18 -2
- package/dist/commands/team-agent-readiness.js +39 -39
- package/dist/commands/team-agent-readiness.js.map +1 -1
- package/dist/commands/team-agents-agents.d.ts +5 -2
- package/dist/commands/team-agents-agents.js +152 -15
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-define.d.ts +31 -0
- package/dist/commands/team-agents-define.js +176 -0
- package/dist/commands/team-agents-define.js.map +1 -0
- package/dist/commands/team-agents-mcp.js +112 -10
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.d.ts +1 -1
- package/dist/commands/team-agents-triggers.js +137 -5
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +4 -1
- package/dist/commands/team-agents.js +17 -4
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/team-llm-key.d.ts +14 -0
- package/dist/commands/team-llm-key.js +122 -0
- package/dist/commands/team-llm-key.js.map +1 -0
- package/dist/commands/teams-connect.js +123 -31
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/commands/teams-lifecycle.d.ts +90 -0
- package/dist/commands/teams-lifecycle.js +457 -0
- package/dist/commands/teams-lifecycle.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +14 -0
- package/dist/commands/teams-onboard.js +62 -13
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.js +6 -2
- package/dist/commands/teams-overview.js.map +1 -1
- package/dist/commands/teams.d.ts +25 -5
- package/dist/commands/teams.js +101 -39
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/workflows.d.ts +37 -0
- package/dist/commands/workflows.js +589 -0
- package/dist/commands/workflows.js.map +1 -0
- package/dist/connect-fields.d.ts +22 -0
- package/dist/connect-fields.js +115 -0
- package/dist/connect-fields.js.map +1 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +32 -9
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +357 -122
- package/dist/index.js.map +1 -1
- package/dist/invocation.d.ts +31 -0
- package/dist/invocation.js +58 -0
- package/dist/invocation.js.map +1 -0
- package/dist/runs-api.js +8 -3
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +18 -0
- package/dist/safe-fs.js +57 -0
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-form.d.ts +48 -0
- package/dist/secret-form.js +212 -0
- package/dist/secret-form.js.map +1 -0
- package/dist/secret-input.d.ts +96 -0
- package/dist/secret-input.js +299 -0
- package/dist/secret-input.js.map +1 -0
- package/dist/team-connect-api.d.ts +78 -3
- package/dist/team-connect-api.js +17 -2
- package/dist/team-connect-api.js.map +1 -1
- package/dist/team-lifecycle-api.d.ts +435 -0
- package/dist/team-lifecycle-api.js +301 -0
- package/dist/team-lifecycle-api.js.map +1 -0
- package/dist/teams-api.d.ts +13 -1
- package/dist/teams-api.js +88 -27
- package/dist/teams-api.js.map +1 -1
- package/dist/unknown-command.d.ts +18 -0
- package/dist/unknown-command.js +50 -0
- package/dist/unknown-command.js.map +1 -0
- package/dist/workflows-api.d.ts +69 -0
- package/dist/workflows-api.js +182 -0
- package/dist/workflows-api.js.map +1 -0
- package/package.json +3 -3
- package/templates/AGENTS.md.tmpl +6 -4
- package/templates/claude-skill.md.tmpl +4 -1
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +43 -10
- package/templates/skills/build-agent/references/agent-json.md +4 -1
- package/templates/skills/build-agent/references/app-setup.md +14 -7
- package/templates/skills/build-agent/references/rollout.md +26 -2
- package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
- package/templates/skills/build-integration/references/widget-design.md +1 -1
- package/templates/skills/ekanos-platform/SKILL.md +66 -14
- package/templates/skills/polish-widgets/references/patterns.md +3 -3
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ npx @ekanos/cli dev
|
|
|
26
26
|
| `logout` | Revoke the session at Auth0 and delete the local credentials. |
|
|
27
27
|
| `whoami` | Print the identity the stored session authenticates as, plus a compact `seats` list (slug + roles) from the same endpoint `sources` uses — degrades to `seats: null` rather than failing if that lookup is unavailable. |
|
|
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
|
-
| `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
|
-
| `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. |
|
|
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; for a Source admin also `products`, `scopes`, `roles [create\|update\|delete <role>]` (the Source's team roles) and `people` (everyone on the Source, one row per person). |
|
|
30
|
+
| `catalog` | `ekanos catalog [<product-slug>]` — what a source can already use (`GET /api/partner/catalog`): every product, Experiences then Integrations as `sources products` splits them (the two list the same products), enabled, switched off or available to enable, with its tools and their input schemas, whose connection an agent run uses, agent triggers and generated setup steps. Check it before building something the source may already have. |
|
|
31
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
|
+
| `teams` | A Source admin's teams (`/api/partner/teams`): members, connections, and each team's agents. `ekanos teams agents create <team> --from <dir> [--personal]` builds the team's own agent — or, with `--personal`, your own, which acts for you and only you change — from the same `agent.json` (+ `AGENT.md`, + `skills/<slug>/`) that `agents apply` reads; `teams agents apply <team> <dir> [--base-version <n>]` creates or changes it from that file. You become its creator, so you must be an active member of the team. `teams triggers add <team> <key> schedule\|webhook …` starts it; a webhook token goes to a `0600` file under `~/.ekanos/secrets`, never the output. Someone else's personal agent is refused for every write (`PERSONAL_AGENT_CREATOR_ONLY`). |
|
|
32
33
|
| `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. |
|
|
34
|
+
| `workflows` | `ekanos workflows list \| show \| pull \| apply \| enable \| disable \| run \| runs list\|show\|cancel \| approvals list\|approve\|reject --team <slug>` — a team's workflows, for its Source admin (`/api/partner/teams/<team>/workflows`). A workflow is the JSON the visual editor saves; `apply` creates a draft (or replaces one with `--update`) through the editor's own checks, including the Custom Code security review and approval gates before writes; `enable` re-checks it and asks you to approve what Custom Code reaches (`--confirm-access` in a script); `run --dry-run` rehearses it with writes recorded, not sent. Needs the Workflows Experience on for the Source; a member's personal workflow is never reachable. |
|
|
33
35
|
| `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
36
|
| `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). |
|
|
35
37
|
| `upgrade` | Move this toolchain to a coherent target version, with verify-and-rollback. `--check` reports current vs. target and changes nothing; a bare `upgrade` installs, verifies (`validate` + the test script), and rolls back automatically on any failure. See [Keeping the toolchain current](#keeping-the-toolchain-current). |
|
|
@@ -50,6 +52,10 @@ installs by hand. Machine-level verbs (`init`, `login`, `logout`, `whoami`,
|
|
|
50
52
|
token to the host, never delegate — they always run on the binary you invoked.
|
|
51
53
|
Set `EKANOS_NO_DELEGATE=1` to opt out for one invocation.
|
|
52
54
|
|
|
55
|
+
Every command a hint or a next step names is written the way you ran the
|
|
56
|
+
CLI, so it can be pasted back as-is: `ekanos …` from the installed binary,
|
|
57
|
+
`npx @ekanos/cli …` when the run came through `npx`.
|
|
58
|
+
|
|
53
59
|
## Global flags
|
|
54
60
|
|
|
55
61
|
| Flag | What it does |
|
|
@@ -69,6 +75,15 @@ in either direction. In JSON mode **stdout carries exactly one JSON object** —
|
|
|
69
75
|
`{ ok, data, error: { code, message, hint } }` — and every progress line goes to
|
|
70
76
|
stderr. Errors always carry an imperative `hint`.
|
|
71
77
|
|
|
78
|
+
On `--help`, an unknown command and an out-of-date toolchain the CLI also
|
|
79
|
+
writes one short `claude-code-hint: {…}` line to stderr — the command list, the
|
|
80
|
+
suggested fix, or the one command's usage, never the whole manual. It is
|
|
81
|
+
written only when `--json` is passed or a coding agent is detected
|
|
82
|
+
(`CLAUDECODE=1`, `CODEX_SANDBOX`, `CURSOR_AGENT=1`, `GEMINI_CLI=1`, `AI_AGENT`);
|
|
83
|
+
a person at a terminal, or piping into another program, gets the readable error
|
|
84
|
+
alone. An unknown command suggests the closest one, and a whole command passed
|
|
85
|
+
as one argument (`ekanos "teams list"`) is answered with `ekanos teams list`.
|
|
86
|
+
|
|
72
87
|
The CLI never prompts, with **one deliberate exception**: `publish` asks for a
|
|
73
88
|
y/N confirm at a real terminal when the publish target is _ambiguous_ — an
|
|
74
89
|
implicit (ekanos.json) source and more than one developer seat. See
|
|
@@ -90,6 +105,27 @@ this CLI is not current. Absent when current, unresolvable, or disabled via
|
|
|
90
105
|
`5` forbidden · `6` not-found · `7` invalid-state · `8` network ·
|
|
91
106
|
`9` precondition-failed · `10` gate-failed.
|
|
92
107
|
|
|
108
|
+
### Secrets without a terminal
|
|
109
|
+
|
|
110
|
+
Every command that takes a secret (`apps set`, `apps connect`, `teams app set`,
|
|
111
|
+
`teams connect`, `teams mcp token`, `teams llm-key set`) asks for it with
|
|
112
|
+
hidden input at a terminal, and otherwise — a script, CI, `--json`, a coding
|
|
113
|
+
agent — reads it from one of these. Never from a value on the command line.
|
|
114
|
+
|
|
115
|
+
| Flag | The secret comes from |
|
|
116
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
117
|
+
| `--secret-env <NAME>` | the environment variable `NAME` (its name, never its value) |
|
|
118
|
+
| `--secret-file <path>` | a file of yours readable only by you (`chmod 600`); symlinks are refused |
|
|
119
|
+
| `--secret-stdin` | stdin, piped in |
|
|
120
|
+
| `--set-secret <field>=env:<NAME>\|file:<path>\|stdin` | one field of a form with several secrets; repeat per field |
|
|
121
|
+
| `--browser` | a one-time form the CLI serves on `127.0.0.1` and opens in your browser; you type it there |
|
|
122
|
+
|
|
123
|
+
A field that is not a secret is `--set <field>=<value>`. `--browser` is never
|
|
124
|
+
the default (an unattended run would wait on it), is refused in CI, over SSH or
|
|
125
|
+
with no display, and gives up after five minutes. No output, `--json` envelope
|
|
126
|
+
or error carries the secret. Details:
|
|
127
|
+
[Secrets without a terminal](https://sdk.ekanos.dev/cli/commands#secrets-without-a-terminal).
|
|
128
|
+
|
|
93
129
|
## `ekanos.json`
|
|
94
130
|
|
|
95
131
|
```json
|
|
@@ -378,8 +414,9 @@ turned into an uninstallable tree.
|
|
|
378
414
|
code) whether a newer `@ekanos/cli` exists. Human mode gets one stderr line;
|
|
379
415
|
JSON mode never prints prose to stdout, but the envelope's `data.toolchain`
|
|
380
416
|
carries `{ status: 'behind' | 'unsupported', current, target, upgradeCommand
|
|
381
|
-
}` when you're not current, and
|
|
382
|
-
stderr line so an agent can act on it
|
|
417
|
+
}` when you're not current, and (with `--json` or a detected agent) the same
|
|
418
|
+
object rides the `claude-code-hint` stderr line so an agent can act on it
|
|
419
|
+
without parsing prose. `unsupported`
|
|
383
420
|
(below the host's declared minimum) makes `publish` fail fast — the host would
|
|
384
421
|
reject the submission anyway. Set `EKANOS_NO_UPDATE_CHECK=1` to disable
|
|
385
422
|
entirely.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CliError } from '../errors.js';
|
|
1
2
|
export interface CliAuthConfig {
|
|
2
3
|
auth0Domain: string;
|
|
3
4
|
clientId: string;
|
|
@@ -60,3 +61,25 @@ export declare function fetchIdentity(host: string, accessToken: string): Promis
|
|
|
60
61
|
*/
|
|
61
62
|
export declare function revokeEkanosSession(host: string, accessToken: string): Promise<boolean>;
|
|
62
63
|
export declare function request(url: string, init: RequestInit): Promise<Response>;
|
|
64
|
+
/**
|
|
65
|
+
* 503 means the deployment is not provisioned for CLI login — an operator
|
|
66
|
+
* action that has not happened, not a fault in the request.
|
|
67
|
+
*
|
|
68
|
+
* Every route that reads the Fusion CLI Auth0 config can answer this, so every
|
|
69
|
+
* caller has to check for it. Falling through to `unexpectedStatus` would tell
|
|
70
|
+
* a partner "confirm the host is an Ekanos deployment" when the host is a
|
|
71
|
+
* perfectly good Ekanos deployment that is simply missing one env var — and
|
|
72
|
+
* would exit 8 (network, retryable) instead of 9 (precondition, not).
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* What the server's CLI session policy is, said once for every "log in
|
|
76
|
+
* again" message. The server owns the numbers (`CLI_SESSION_LIFETIME` in the
|
|
77
|
+
* web app, equal to the Fusion CLI Auth0 refresh token); this is the sentence.
|
|
78
|
+
*/
|
|
79
|
+
export declare const CLI_SESSION_LIFETIME_TEXT: string;
|
|
80
|
+
/**
|
|
81
|
+
* A 401 from `cli-refresh`: the session really is over (or Auth0 asked for
|
|
82
|
+
* something a refresh cannot give). Says why, how long sessions last, and the
|
|
83
|
+
* one command that fixes it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function sessionExpiredError(host: string, serverMessage: string | null, reason: string | null): CliError;
|
package/dist/auth/ekanos-api.js
CHANGED
|
@@ -43,7 +43,11 @@ import { authRequiredError, forbiddenError, networkError, preconditionError, } f
|
|
|
43
43
|
* or past the session's
|
|
44
44
|
* maximum age; checked
|
|
45
45
|
* BEFORE rotation, so no
|
|
46
|
-
* rotated token comes back
|
|
46
|
+
* rotated token comes back.
|
|
47
|
+
* reason
|
|
48
|
+
* "auth0_mfa_required":
|
|
49
|
+
* Auth0 demanded MFA on
|
|
50
|
+
* the refresh grant
|
|
47
51
|
* → 403 { error, code: "EMAIL_UNVERIFIED",
|
|
48
52
|
* auth0_refresh_token }
|
|
49
53
|
* → 404 { error, code: "ACCOUNT_NOT_FOUND",
|
|
@@ -189,11 +193,7 @@ export async function refreshEkanosSession(host, credentials, options = {}) {
|
|
|
189
193
|
}
|
|
190
194
|
throwIfNotConfigured(response, host, body);
|
|
191
195
|
if (response.status === 401) {
|
|
192
|
-
throw
|
|
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.`);
|
|
196
|
+
throw sessionExpiredError(host, str(body.error), str(body.reason));
|
|
197
197
|
}
|
|
198
198
|
if (response.status === 403) {
|
|
199
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.`);
|
|
@@ -333,6 +333,33 @@ async function readJson(response, what) {
|
|
|
333
333
|
* perfectly good Ekanos deployment that is simply missing one env var — and
|
|
334
334
|
* would exit 8 (network, retryable) instead of 9 (precondition, not).
|
|
335
335
|
*/
|
|
336
|
+
/**
|
|
337
|
+
* What the server's CLI session policy is, said once for every "log in
|
|
338
|
+
* again" message. The server owns the numbers (`CLI_SESSION_LIFETIME` in the
|
|
339
|
+
* web app, equal to the Fusion CLI Auth0 refresh token); this is the sentence.
|
|
340
|
+
*/
|
|
341
|
+
export const CLI_SESSION_LIFETIME_TEXT = 'A CLI session lasts up to 30 days and ends after 7 days without use; ' +
|
|
342
|
+
'within that window an expired access token is renewed silently.';
|
|
343
|
+
/**
|
|
344
|
+
* A 401 from `cli-refresh`: the session really is over (or Auth0 asked for
|
|
345
|
+
* something a refresh cannot give). Says why, how long sessions last, and the
|
|
346
|
+
* one command that fixes it.
|
|
347
|
+
*/
|
|
348
|
+
export function sessionExpiredError(host, serverMessage, reason) {
|
|
349
|
+
const why = reason === 'auth0_mfa_required'
|
|
350
|
+
? 'Auth0 asked for multi-factor authentication to renew it, which a ' +
|
|
351
|
+
'refresh cannot answer.'
|
|
352
|
+
: reason === 'session_revoked'
|
|
353
|
+
? 'It was signed out ("ekanos logout" or an administrator).'
|
|
354
|
+
: reason === 'session_idle_expired'
|
|
355
|
+
? 'It went unused for longer than 7 days.'
|
|
356
|
+
: reason === 'session_max_age_exceeded'
|
|
357
|
+
? 'It reached its 30-day maximum age.'
|
|
358
|
+
: serverMessage
|
|
359
|
+
? `${serverMessage}.`
|
|
360
|
+
: 'The server no longer accepts it.';
|
|
361
|
+
return authRequiredError(`The CLI session for ${host} has ended. ${why} ${CLI_SESSION_LIFETIME_TEXT}`, `Run: ekanos login --host ${host}`);
|
|
362
|
+
}
|
|
336
363
|
function throwIfNotConfigured(response, host, body) {
|
|
337
364
|
if (response.status !== 503)
|
|
338
365
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ekanos-api.js","sourceRoot":"","sources":["../../src/auth/ekanos-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n authRequiredError,\n forbiddenError,\n networkError,\n preconditionError,\n} from '../errors';\n\n/**\n * The CLI's client for the five Ekanos auth routes.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/auth/cli-<verb>` routes, and it ships on\n * a DIFFERENT BRANCH from this file. This block is the single place the shapes\n * are written down on the client side; if you change one side, change the\n * other and update this block. Nothing here imports host code — the two\n * halves are coupled only by HTTP, and the tests stub `fetch`, never the route.\n *\n * GET /api/auth/cli-config [no auth]\n * → 200 { auth0_domain, client_id, scope,\n * device_authorization_endpoint, token_endpoint } all strings\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n *\n * POST /api/auth/cli-session [no auth]\n * ← { auth0_access_token: string }\n * → 200 { access_token, expires_at, user: { id, email } }\n * → 400 { error, code: \"INVALID_REQUEST\" } malformed / wrong shape\n * → 401 { error, code: \"SESSION_EXPIRED\" } Auth0 said no, OR no\n * Ekanos account for it\n * → 403 { error, code: \"NO_DEVELOPER_SEAT\" } no developer/admin seat\n * → 403 { error, code: \"EMAIL_UNVERIFIED\" } Auth0 email unverified\n * → 429 { error, code: \"RATE_LIMITED\" } + Retry-After header\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n * `error` on a 403 is a sentence written for the person; show it.\n *\n * POST /api/auth/cli-refresh [no auth]\n * ← { auth0_refresh_token: string, access_token: string }\n * access_token is the PREVIOUS Ekanos JWT (expired is fine): it names\n * the server-side session this refresh continues. Required: without\n * it the server answers 401 SESSION_EXPIRED, reason\n * \"previous_token_missing\" (an older CLI is told to log in again).\n * → 200 { access_token, expires_at, user: { id, email },\n * auth0_refresh_token } ← ROTATED; the presented one is dead\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"SESSION_EXPIRED\", reason? }\n * expired, revoked, idle\n * or past the session's\n * maximum age; checked\n * BEFORE rotation, so no\n * rotated token comes back\n * → 403 { error, code: \"EMAIL_UNVERIFIED\",\n * auth0_refresh_token }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\",\n * auth0_refresh_token } no Ekanos account\n * → 429 { error, code: \"RATE_LIMITED\" } + Retry-After header\n * → 502 { error, code: \"TOKEN_EXCHANGE_FAILED\" } Auth0 returned no\n * rotated token\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 503 { error, code: \"REFRESH_INCOMPLETE\",\n * auth0_refresh_token } rotated, but /userinfo\n * did not answer; RETRYABLE\n * → 500 { error, code: \"INTERNAL_ERROR\",\n * auth0_refresh_token? } present iff rotation\n * completed before the throw\n *\n * !! cli-refresh MAY RETURN `auth0_refresh_token` ON ERROR RESPONSES, and a\n * client MUST persist it before surfacing the error. Once the server's\n * exchange with Auth0 succeeds the presented token is dead and the value\n * in that body is the ONLY copy in existence; discarding it on a failure\n * path destroys the session permanently. Note 503 has TWO codes here —\n * branch on `code`, not on the status alone.\n *\n * All three routes above read the Fusion CLI Auth0 application config, so all\n * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami and cli-logout cannot\n * — they read no Auth0 config, only an Ekanos token.\n *\n * POST /api/auth/cli-logout [no auth]\n * ← { access_token: string } the stored Ekanos JWT; expired is fine\n * → 200 { revoked: boolean } false = already revoked / no session row\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } not a valid Ekanos token\n * → 429 { error, code: \"RATE_LIMITED\" }\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n *\n * GET /api/auth/cli-whoami [Authorization: Bearer <access_token>]\n * → 200 { user: { id, email, name } }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or invalid\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\" }\n * → 3xx treated as 401, see below\n *\n * `access_token` is an Ekanos HS256 JWT. Every authenticated Ekanos route\n * accepts it as `Authorization: Bearer` — see `resolveRequestSupabaseJwt` in\n * `@kit/supabase`, which both `requireUser` and the RLS server client consume.\n * ---------------------------------------------------------------------------\n *\n * Two rules hold everywhere in this file:\n *\n * - `redirect: 'manual'`. Ekanos answers an unauthenticated page request with\n * a 307 to `/auth/sign-in`; Node's fetch follows redirects by default, so\n * without this a stale token would come back as a 200 full of HTML and be\n * read as success. A 3xx here is treated as \"not authenticated\", full stop.\n *\n * - No token is ever interpolated into an error message. The server's error\n * bodies are `{ error, code }` documents that do not echo credentials, and\n * the request bodies that DO carry credentials are never included in a\n * thrown message.\n */\n\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nexport interface CliAuthConfig {\n auth0Domain: string;\n clientId: string;\n scope: string;\n deviceAuthorizationEndpoint: string;\n tokenEndpoint: string;\n}\n\nexport interface EkanosSession {\n accessToken: string;\n expiresAt: string;\n userId: string;\n email: string | null;\n}\n\nexport interface EkanosIdentity {\n id: string;\n email: string | null;\n name: string | null;\n}\n\n/** Fetch the device-flow parameters for a host. */\nexport async function fetchCliAuthConfig(host: string): Promise<CliAuthConfig> {\n const response = await request(`${host}/api/auth/cli-config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n });\n\n throwIfNotConfigured(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-config', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-config`);\n\n const config = {\n auth0Domain: str(body.auth0_domain),\n clientId: str(body.client_id),\n scope: str(body.scope),\n deviceAuthorizationEndpoint: str(body.device_authorization_endpoint),\n tokenEndpoint: str(body.token_endpoint),\n };\n\n if (Object.values(config).some((value) => value === null)) {\n throw preconditionError(\n `${host} returned an incomplete CLI login configuration.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-config route.`,\n );\n }\n\n // The endpoints come from the server, so they are attacker-controlled if the\n // host is. Pin the declared domain to Auth0 itself, then pin the endpoints\n // to that domain, so a compromised or misconfigured Ekanos cannot redirect\n // the device flow — and the resulting credentials — to an arbitrary origin.\n // This does not prove the host is genuine: any Auth0 tenant passes, and a\n // hostile host can name Ekanos's own. That is `login`'s new-host question.\n assertAuth0Domain(config.auth0Domain!);\n assertOnDomain(config.deviceAuthorizationEndpoint!, config.auth0Domain!);\n assertOnDomain(config.tokenEndpoint!, config.auth0Domain!);\n\n return config as CliAuthConfig;\n}\n\n/** Exchange a verified Auth0 access token for an Ekanos JWT. */\nexport async function createEkanosSession(\n host: string,\n auth0AccessToken: string,\n): Promise<EkanosSession> {\n const response = await request(`${host}/api/auth/cli-session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_access_token: auth0AccessToken }),\n });\n\n throwIfNotConfigured(response, host);\n\n if (response.status === 401) {\n throw authRequiredError(\n `${host} rejected the Auth0 identity.`,\n `Run \"ekanos login\" again. If it keeps failing, confirm your account ` +\n `exists on ${host} — the browser login can succeed for an identity ` +\n `that has no Ekanos account.`,\n );\n }\n\n if (response.status === 403) {\n const body = await readOptionalJson(response);\n\n throw forbiddenError(\n str(body.error) ?? `${host} refused a CLI session for this account.`,\n body.code === 'EMAIL_UNVERIFIED'\n ? `Verify your email address, then run \"ekanos login\" again.`\n : `The Ekanos CLI needs a developer seat. Ask your administrator ` +\n `for one, then run \"ekanos login\" again.`,\n );\n }\n\n throwIfRateLimited(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-session', response.status);\n }\n\n return toSession(\n await readJson(response, `${host}/api/auth/cli-session`),\n host,\n );\n}\n\nexport interface RefreshedSession extends EkanosSession {\n /** The ROTATED refresh token. The presented one is already invalid. */\n auth0RefreshToken: string;\n}\n\nexport interface RefreshOptions {\n /**\n * Called the INSTANT a rotated refresh token is seen in the response — on\n * success and on failure alike, before anything is thrown.\n *\n * Persist-on-learn, not persist-on-success. Once the server's exchange with\n * Auth0 succeeds the presented token is dead and the replacement in that\n * body is the only copy in existence, so a client that only saves it on the\n * happy path destroys the partner's credential the first time /userinfo\n * blips. The callback runs before any throw so no error path can skip it.\n */\n onRotatedToken?: (rotated: string) => void;\n}\n\n/**\n * Re-mint an Ekanos JWT from the stored Auth0 refresh token. `accessToken` is\n * the previous Ekanos JWT (usually expired): the server reads the session id\n * from it to enforce that session's idle timeout, maximum age and revocation.\n */\nexport async function refreshEkanosSession(\n host: string,\n credentials: { auth0RefreshToken: string; accessToken: string },\n options: RefreshOptions = {},\n): Promise<RefreshedSession> {\n const response = await request(`${host}/api/auth/cli-refresh`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({\n auth0_refresh_token: credentials.auth0RefreshToken,\n access_token: credentials.accessToken,\n }),\n });\n\n // Read the body FIRST, whatever the status: the rotated token can arrive on\n // a 503, a 404 or a 500 just as readily as on a 200, and it must be captured\n // before any branch below throws.\n const body = await readOptionalJson(response);\n const rotated = str(body.auth0_refresh_token);\n\n if (rotated) {\n options.onRotatedToken?.(rotated);\n }\n\n throwIfNotConfigured(response, host, body);\n\n if (response.status === 401) {\n throw authRequiredError(\n `The stored session for ${host} is no longer valid` +\n (str(body.error) ? ` (${str(body.error)}).` : '.'),\n `Run \"ekanos login\" to sign in again. This is expected after the ` +\n `session's idle timeout or maximum age, after \"ekanos logout\", after ` +\n `the refresh token is revoked in Auth0, or if two machines used the ` +\n `same credentials file.`,\n );\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n str(body.error) ?? `${host} refused to renew the session.`,\n `Verify your email address, then run \"ekanos login\" again.`,\n );\n }\n\n throwIfRateLimited(response, host);\n\n if (response.status === 503 && body.code === 'REFRESH_INCOMPLETE') {\n // NOT an auth failure. The credential was renewed — and, thanks to the\n // callback above, already saved. Auth0's /userinfo is simply not answering.\n throw networkError(\n `${host} renewed the session but could not verify the identity with Auth0.`,\n `Retry in a moment — your credentials are still valid and have been ` +\n `updated. You do NOT need to run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no Ekanos account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then retry.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-refresh', response.status);\n }\n\n if (!rotated) {\n throw networkError(\n `${host} refreshed the session without returning a rotated refresh token.`,\n `Run \"ekanos login\" to start a fresh session, and report this — the ` +\n `deployment's Auth0 CLI application may not have rotation enabled.`,\n );\n }\n\n return { ...toSession(body, host), auth0RefreshToken: rotated };\n}\n\n/**\n * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can\n * decide whether to refresh and retry — an expired access token is an ordinary\n * event, not an error.\n */\nexport async function fetchIdentity(\n host: string,\n accessToken: string,\n): Promise<EkanosIdentity | null> {\n const response = await request(`${host}/api/auth/cli-whoami`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n // 401, or the 307 that an auth-gated Ekanos route answers with. Both mean\n // \"this token did not authenticate\".\n if (response.status === 401 || isRedirect(response.status)) {\n return null;\n }\n\n if (response.status === 403) {\n throw authRequiredError(\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-whoami', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-whoami`);\n const user = (body.user ?? {}) as Record<string, unknown>;\n const id = str(user.id);\n\n if (!id) {\n throw networkError(\n `${host} returned an identity without a user id.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-whoami route.`,\n );\n }\n\n return { id, email: str(user.email), name: str(user.name) };\n}\n\n/**\n * End the server-side session behind an Ekanos JWT (`ekanos logout`). Returns\n * whether the server answered — `true` also when the session was already\n * revoked, since either way it can no longer be refreshed. Never throws for\n * an HTTP status: logout must always reach the local delete.\n */\nexport async function revokeEkanosSession(\n host: string,\n accessToken: string,\n): Promise<boolean> {\n const response = await request(`${host}/api/auth/cli-logout`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ access_token: accessToken }),\n });\n\n return response.ok;\n}\n\nfunction toSession(body: Record<string, unknown>, host: string): EkanosSession {\n const user = (body.user ?? {}) as Record<string, unknown>;\n\n const accessToken = str(body.access_token);\n const expiresAt = str(body.expires_at);\n const userId = str(user.id);\n\n if (!accessToken || !expiresAt || !userId) {\n throw networkError(\n `${host} returned an incomplete session.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `CLI auth routes, then retry.`,\n );\n }\n\n return { accessToken, expiresAt, userId, email: str(user.email) };\n}\n\nexport async function request(\n url: string,\n init: RequestInit,\n): Promise<Response> {\n try {\n return await fetch(url, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n\n throw networkError(\n `Could not reach ${new URL(url).origin}: ${reason}`,\n `Check the host is correct and reachable from this machine ` +\n `(proxy, VPN, DNS), then retry.`,\n );\n }\n}\n\n/**\n * Best-effort body read for a response whose status may be an error. Returns\n * an empty object rather than throwing, because the caller needs to inspect\n * the body BEFORE deciding what the status means — and a 500 from a proxy may\n * carry no JSON at all.\n */\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nasync function readJson(\n response: Response,\n what: string,\n): Promise<Record<string, unknown>> {\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch {\n throw networkError(\n `${what} did not return JSON.`,\n `Confirm the host is an Ekanos deployment and not a proxy or captive ` +\n `portal sitting in front of one.`,\n );\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw networkError(\n `${what} returned an unexpected payload.`,\n `Confirm the host is an Ekanos deployment, then retry.`,\n );\n }\n\n return parsed as Record<string, unknown>;\n}\n\n/**\n * 503 means the deployment is not provisioned for CLI login — an operator\n * action that has not happened, not a fault in the request.\n *\n * Every route that reads the Fusion CLI Auth0 config can answer this, so every\n * caller has to check for it. Falling through to `unexpectedStatus` would tell\n * a partner \"confirm the host is an Ekanos deployment\" when the host is a\n * perfectly good Ekanos deployment that is simply missing one env var — and\n * would exit 8 (network, retryable) instead of 9 (precondition, not).\n */\nfunction throwIfNotConfigured(\n response: Response,\n host: string,\n body?: Record<string, unknown>,\n): void {\n if (response.status !== 503) return;\n\n // 503 carries two meanings on these routes. Only CLI_NOT_CONFIGURED is a\n // precondition; REFRESH_INCOMPLETE is transient and handled by its caller.\n if (body && body.code !== 'CLI_NOT_CONFIGURED') return;\n\n throw preconditionError(\n `${host} does not have CLI login configured.`,\n `Ask the Ekanos operator to provision the \"Fusion CLI\" Auth0 ` +\n `application and set AUTH0_CLI_CLIENT_ID on that deployment.`,\n );\n}\n\n/** 429: the deployment is throttling this machine's address. Retryable. */\nfunction throwIfRateLimited(response: Response, host: string): void {\n if (response.status !== 429) return;\n\n const retryAfter = Number.parseInt(\n response.headers.get('retry-after') ?? '',\n 10,\n );\n\n throw networkError(\n `${host} is rate-limiting sign-in requests from this address.`,\n Number.isFinite(retryAfter) && retryAfter > 0\n ? `Retry in ${retryAfter} seconds.`\n : `Retry in a few minutes.`,\n );\n}\n\nfunction unexpectedStatus(host: string, route: string, status: number) {\n return networkError(\n `${host}${route} responded ${status}.`,\n status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if it ` +\n `persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes ${route}.`,\n );\n}\n\n/**\n * Every Ekanos tenant signs in on an `*.auth0.com` domain today\n * (`fusion-{dev,stage,prod}.us.auth0.com`). If one moves to an Auth0 custom\n * domain, add that domain here.\n */\nfunction assertAuth0Domain(domain: string): void {\n const hostname = domain.toLowerCase();\n\n if (/^[a-z0-9-]+(\\.[a-z0-9-]+)*\\.auth0\\.com$/.test(hostname)) return;\n\n throw preconditionError(\n `The login configuration names a sign-in domain that is not Auth0 (${domain}).`,\n `Do not continue. Report this to the operator of the Ekanos ` +\n `deployment — it may be misconfigured or compromised.`,\n );\n}\n\nfunction assertOnDomain(endpoint: string, domain: string): void {\n let url: URL;\n\n try {\n url = new URL(endpoint);\n } catch {\n throw preconditionError(\n `The login configuration contains a malformed endpoint.`,\n `Report this to the operator of the Ekanos deployment.`,\n );\n }\n\n if (\n url.protocol !== 'https:' ||\n url.host.toLowerCase() !== domain.toLowerCase()\n ) {\n throw preconditionError(\n `The login configuration points at an endpoint outside its own Auth0 ` +\n `domain (${url.host}).`,\n `Do not continue. Report this to the operator of the Ekanos ` +\n `deployment — it may be misconfigured or compromised.`,\n );\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":"ekanos-api.js","sourceRoot":"","sources":["../../src/auth/ekanos-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,iBAAiB,GAClB,MAAM,WAAW,CAAC;AAEnB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AAEH,MAAM,kBAAkB,GAAG,KAAM,CAAC;AAuBlC,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAAY;IACnD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KACxC,CAAC,CAAC;IAEH,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAErC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,gBAAgB,CAAC,IAAI,EAAE,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,IAAI,sBAAsB,CAAC,CAAC;IAErE,MAAM,MAAM,GAAG;QACb,WAAW,EAAE,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;QAC7B,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QACtB,2BAA2B,EAAE,GAAG,CAAC,IAAI,CAAC,6BAA6B,CAAC;QACpE,aAAa,EAAE,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC;KACxC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1D,MAAM,iBAAiB,CACrB,GAAG,IAAI,kDAAkD,EACzD,sEAAsE;YACpE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,0EAA0E;IAC1E,2EAA2E;IAC3E,iBAAiB,CAAC,MAAM,CAAC,WAAY,CAAC,CAAC;IACvC,cAAc,CAAC,MAAM,CAAC,2BAA4B,EAAE,MAAM,CAAC,WAAY,CAAC,CAAC;IACzE,cAAc,CAAC,MAAM,CAAC,aAAc,EAAE,MAAM,CAAC,WAAY,CAAC,CAAC;IAE3D,OAAO,MAAuB,CAAC;AACjC,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY,EACZ,gBAAwB;;IAExB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,uBAAuB,EAAE;QAC7D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,CAAC;KAC/D,CAAC,CAAC;IAEH,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAErC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,GAAG,IAAI,+BAA+B,EACtC,sEAAsE;YACpE,aAAa,IAAI,mDAAmD;YACpE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAE9C,MAAM,cAAc,OAClB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mCAAI,GAAG,IAAI,0CAA0C,EACpE,IAAI,CAAC,IAAI,KAAK,kBAAkB;YAC9B,CAAC,CAAC,2DAA2D;YAC7D,CAAC,CAAC,gEAAgE;gBAC9D,yCAAyC,CAChD,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAEnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzE,CAAC;IAED,OAAO,SAAS,CACd,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,IAAI,uBAAuB,CAAC,EACxD,IAAI,CACL,CAAC;AACJ,CAAC;AAqBD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,IAAY,EACZ,WAA+D,EAC/D,OAAO,GAAmB,EAAE;;;IAE5B,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,uBAAuB,EAAE;QAC7D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;YACnB,mBAAmB,EAAE,WAAW,CAAC,iBAAiB;YAClD,YAAY,EAAE,WAAW,CAAC,WAAW;SACtC,CAAC;KACH,CAAC,CAAC;IAEH,4EAA4E;IAC5E,6EAA6E;IAC7E,kCAAkC;IAClC,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAE9C,IAAI,OAAO,EAAE,CAAC;QACZ,MAAA,OAAO,CAAC,cAAc,+CAAtB,OAAO,EAAkB,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,oBAAoB,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAE3C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,mBAAmB,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,cAAc,OAClB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mCAAI,GAAG,IAAI,gCAAgC,EAC1D,2DAA2D,CAC5D,CAAC;IACJ,CAAC;IAED,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAEnC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;QAClE,uEAAuE;QACvE,4EAA4E;QAC5E,MAAM,YAAY,CAChB,GAAG,IAAI,oEAAoE,EAC3E,qEAAqE;YACnE,uDAAuD,CAC1D,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,GAAG,IAAI,oDAAoD,EAC3D,cAAc,IAAI,kDAAkD;YAClE,aAAa,CAChB,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,YAAY,CAChB,GAAG,IAAI,mEAAmE,EAC1E,qEAAqE;YACnE,mEAAmE,CACtE,CAAC;IACJ,CAAC;IAED,uCAAY,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,KAAE,iBAAiB,EAAE,OAAO,IAAG;AAClE,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAY,EACZ,WAAmB;;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAC;IAEH,0EAA0E;IAC1E,qCAAqC;IACrC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,GAAG,IAAI,yDAAyD,EAChE,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,GAAG,IAAI,6CAA6C,EACpD,cAAc,IAAI,kDAAkD;YAClE,gCAAgC,CACnC,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,gBAAgB,CAAC,IAAI,EAAE,sBAAsB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,IAAI,sBAAsB,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,OAAC,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAExB,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,YAAY,CAChB,GAAG,IAAI,0CAA0C,EACjD,sEAAsE;YACpE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,IAAY,EACZ,WAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,kBAAkB,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;KACpD,CAAC,CAAC;IAEH,OAAO,QAAQ,CAAC,EAAE,CAAC;AACrB,CAAC;AAED,SAAS,SAAS,CAAC,IAA6B,EAAE,IAAY;;IAC5D,MAAM,IAAI,GAAG,OAAC,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAE1D,MAAM,WAAW,GAAG,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAE5B,IAAI,CAAC,WAAW,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC;QAC1C,MAAM,YAAY,CAChB,GAAG,IAAI,kCAAkC,EACzC,sEAAsE;YACpE,8BAA8B,CACjC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAW,EACX,IAAiB;IAEjB,IAAI,CAAC;QACH,OAAO,MAAM,KAAK,CAAC,GAAG,kCACjB,IAAI,KACP,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,kBAAkB,CAAC,IAC/C,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEtE,MAAM,YAAY,CAChB,mBAAmB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,EACnD,4DAA4D;YAC1D,gCAAgC,CACnC,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAElB,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE9C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAClD,CAAC,CAAE,MAAkC;YACrC,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;eAAO,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CACrB,QAAkB,EAClB,IAAY;IAEZ,IAAI,MAAe,CAAC;IAEpB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC;eAAO,CAAC;QACP,MAAM,YAAY,CAChB,GAAG,IAAI,uBAAuB,EAC9B,sEAAsE;YACpE,iCAAiC,CACpC,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,YAAY,CAChB,GAAG,IAAI,kCAAkC,EACzC,uDAAuD,CACxD,CAAC;IACJ,CAAC;IAED,OAAO,MAAiC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GACpC,uEAAuE;IACvE,iEAAiE,CAAC;AAEpE;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,aAA4B,EAC5B,MAAqB;IAErB,MAAM,GAAG,GACP,MAAM,KAAK,oBAAoB;QAC7B,CAAC,CAAC,mEAAmE;YACnE,wBAAwB;QAC1B,CAAC,CAAC,MAAM,KAAK,iBAAiB;YAC5B,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,MAAM,KAAK,sBAAsB;gBACjC,CAAC,CAAC,wCAAwC;gBAC1C,CAAC,CAAC,MAAM,KAAK,0BAA0B;oBACrC,CAAC,CAAC,oCAAoC;oBACtC,CAAC,CAAC,aAAa;wBACb,CAAC,CAAC,GAAG,aAAa,GAAG;wBACrB,CAAC,CAAC,kCAAkC,CAAC;IAEjD,OAAO,iBAAiB,CACtB,uBAAuB,IAAI,eAAe,GAAG,IAAI,yBAAyB,EAAE,EAC5E,4BAA4B,IAAI,EAAE,CACnC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAC3B,QAAkB,EAClB,IAAY,EACZ,IAA8B;IAE9B,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO;IAEpC,yEAAyE;IACzE,2EAA2E;IAC3E,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,oBAAoB;QAAE,OAAO;IAEvD,MAAM,iBAAiB,CACrB,GAAG,IAAI,sCAAsC,EAC7C,8DAA8D;QAC5D,6DAA6D,CAChE,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,SAAS,kBAAkB,CAAC,QAAkB,EAAE,IAAY;;IAC1D,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO;IAEpC,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,OAChC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,mCAAI,EAAE,EACzC,EAAE,CACH,CAAC;IAEF,MAAM,YAAY,CAChB,GAAG,IAAI,uDAAuD,EAC9D,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC;QAC3C,CAAC,CAAC,YAAY,UAAU,WAAW;QACnC,CAAC,CAAC,yBAAyB,CAC9B,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,IAAY,EAAE,KAAa,EAAE,MAAc;IACnE,OAAO,YAAY,CACjB,GAAG,IAAI,GAAG,KAAK,cAAc,MAAM,GAAG,EACtC,MAAM,IAAI,GAAG;QACX,CAAC,CAAC,sEAAsE;YACpE,2CAA2C;QAC/C,CAAC,CAAC,gEAAgE;YAC9D,YAAY,KAAK,GAAG,CAC3B,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,MAAc;IACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAEtC,IAAI,yCAAyC,CAAC,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO;IAErE,MAAM,iBAAiB,CACrB,qEAAqE,MAAM,IAAI,EAC/E,6DAA6D;QAC3D,sDAAsD,CACzD,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB,EAAE,MAAc;IACtD,IAAI,GAAQ,CAAC;IAEb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;eAAO,CAAC;QACP,MAAM,iBAAiB,CACrB,wDAAwD,EACxD,uDAAuD,CACxD,CAAC;IACJ,CAAC;IAED,IACE,GAAG,CAAC,QAAQ,KAAK,QAAQ;QACzB,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,WAAW,EAAE,EAC/C,CAAC;QACD,MAAM,iBAAiB,CACrB,sEAAsE;YACpE,WAAW,GAAG,CAAC,IAAI,IAAI,EACzB,6DAA6D;YAC3D,sDAAsD,CACzD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC","sourcesContent":["import {\n type CliError,\n authRequiredError,\n forbiddenError,\n networkError,\n preconditionError,\n} from '../errors';\n\n/**\n * The CLI's client for the five Ekanos auth routes.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/auth/cli-<verb>` routes, and it ships on\n * a DIFFERENT BRANCH from this file. This block is the single place the shapes\n * are written down on the client side; if you change one side, change the\n * other and update this block. Nothing here imports host code — the two\n * halves are coupled only by HTTP, and the tests stub `fetch`, never the route.\n *\n * GET /api/auth/cli-config [no auth]\n * → 200 { auth0_domain, client_id, scope,\n * device_authorization_endpoint, token_endpoint } all strings\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n *\n * POST /api/auth/cli-session [no auth]\n * ← { auth0_access_token: string }\n * → 200 { access_token, expires_at, user: { id, email } }\n * → 400 { error, code: \"INVALID_REQUEST\" } malformed / wrong shape\n * → 401 { error, code: \"SESSION_EXPIRED\" } Auth0 said no, OR no\n * Ekanos account for it\n * → 403 { error, code: \"NO_DEVELOPER_SEAT\" } no developer/admin seat\n * → 403 { error, code: \"EMAIL_UNVERIFIED\" } Auth0 email unverified\n * → 429 { error, code: \"RATE_LIMITED\" } + Retry-After header\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n * `error` on a 403 is a sentence written for the person; show it.\n *\n * POST /api/auth/cli-refresh [no auth]\n * ← { auth0_refresh_token: string, access_token: string }\n * access_token is the PREVIOUS Ekanos JWT (expired is fine): it names\n * the server-side session this refresh continues. Required: without\n * it the server answers 401 SESSION_EXPIRED, reason\n * \"previous_token_missing\" (an older CLI is told to log in again).\n * → 200 { access_token, expires_at, user: { id, email },\n * auth0_refresh_token } ← ROTATED; the presented one is dead\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"SESSION_EXPIRED\", reason? }\n * expired, revoked, idle\n * or past the session's\n * maximum age; checked\n * BEFORE rotation, so no\n * rotated token comes back.\n * reason\n * \"auth0_mfa_required\":\n * Auth0 demanded MFA on\n * the refresh grant\n * → 403 { error, code: \"EMAIL_UNVERIFIED\",\n * auth0_refresh_token }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\",\n * auth0_refresh_token } no Ekanos account\n * → 429 { error, code: \"RATE_LIMITED\" } + Retry-After header\n * → 502 { error, code: \"TOKEN_EXCHANGE_FAILED\" } Auth0 returned no\n * rotated token\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 503 { error, code: \"REFRESH_INCOMPLETE\",\n * auth0_refresh_token } rotated, but /userinfo\n * did not answer; RETRYABLE\n * → 500 { error, code: \"INTERNAL_ERROR\",\n * auth0_refresh_token? } present iff rotation\n * completed before the throw\n *\n * !! cli-refresh MAY RETURN `auth0_refresh_token` ON ERROR RESPONSES, and a\n * client MUST persist it before surfacing the error. Once the server's\n * exchange with Auth0 succeeds the presented token is dead and the value\n * in that body is the ONLY copy in existence; discarding it on a failure\n * path destroys the session permanently. Note 503 has TWO codes here —\n * branch on `code`, not on the status alone.\n *\n * All three routes above read the Fusion CLI Auth0 application config, so all\n * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami and cli-logout cannot\n * — they read no Auth0 config, only an Ekanos token.\n *\n * POST /api/auth/cli-logout [no auth]\n * ← { access_token: string } the stored Ekanos JWT; expired is fine\n * → 200 { revoked: boolean } false = already revoked / no session row\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } not a valid Ekanos token\n * → 429 { error, code: \"RATE_LIMITED\" }\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n *\n * GET /api/auth/cli-whoami [Authorization: Bearer <access_token>]\n * → 200 { user: { id, email, name } }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or invalid\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\" }\n * → 3xx treated as 401, see below\n *\n * `access_token` is an Ekanos HS256 JWT. Every authenticated Ekanos route\n * accepts it as `Authorization: Bearer` — see `resolveRequestSupabaseJwt` in\n * `@kit/supabase`, which both `requireUser` and the RLS server client consume.\n * ---------------------------------------------------------------------------\n *\n * Two rules hold everywhere in this file:\n *\n * - `redirect: 'manual'`. Ekanos answers an unauthenticated page request with\n * a 307 to `/auth/sign-in`; Node's fetch follows redirects by default, so\n * without this a stale token would come back as a 200 full of HTML and be\n * read as success. A 3xx here is treated as \"not authenticated\", full stop.\n *\n * - No token is ever interpolated into an error message. The server's error\n * bodies are `{ error, code }` documents that do not echo credentials, and\n * the request bodies that DO carry credentials are never included in a\n * thrown message.\n */\n\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nexport interface CliAuthConfig {\n auth0Domain: string;\n clientId: string;\n scope: string;\n deviceAuthorizationEndpoint: string;\n tokenEndpoint: string;\n}\n\nexport interface EkanosSession {\n accessToken: string;\n expiresAt: string;\n userId: string;\n email: string | null;\n}\n\nexport interface EkanosIdentity {\n id: string;\n email: string | null;\n name: string | null;\n}\n\n/** Fetch the device-flow parameters for a host. */\nexport async function fetchCliAuthConfig(host: string): Promise<CliAuthConfig> {\n const response = await request(`${host}/api/auth/cli-config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n });\n\n throwIfNotConfigured(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-config', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-config`);\n\n const config = {\n auth0Domain: str(body.auth0_domain),\n clientId: str(body.client_id),\n scope: str(body.scope),\n deviceAuthorizationEndpoint: str(body.device_authorization_endpoint),\n tokenEndpoint: str(body.token_endpoint),\n };\n\n if (Object.values(config).some((value) => value === null)) {\n throw preconditionError(\n `${host} returned an incomplete CLI login configuration.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-config route.`,\n );\n }\n\n // The endpoints come from the server, so they are attacker-controlled if the\n // host is. Pin the declared domain to Auth0 itself, then pin the endpoints\n // to that domain, so a compromised or misconfigured Ekanos cannot redirect\n // the device flow — and the resulting credentials — to an arbitrary origin.\n // This does not prove the host is genuine: any Auth0 tenant passes, and a\n // hostile host can name Ekanos's own. That is `login`'s new-host question.\n assertAuth0Domain(config.auth0Domain!);\n assertOnDomain(config.deviceAuthorizationEndpoint!, config.auth0Domain!);\n assertOnDomain(config.tokenEndpoint!, config.auth0Domain!);\n\n return config as CliAuthConfig;\n}\n\n/** Exchange a verified Auth0 access token for an Ekanos JWT. */\nexport async function createEkanosSession(\n host: string,\n auth0AccessToken: string,\n): Promise<EkanosSession> {\n const response = await request(`${host}/api/auth/cli-session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_access_token: auth0AccessToken }),\n });\n\n throwIfNotConfigured(response, host);\n\n if (response.status === 401) {\n throw authRequiredError(\n `${host} rejected the Auth0 identity.`,\n `Run \"ekanos login\" again. If it keeps failing, confirm your account ` +\n `exists on ${host} — the browser login can succeed for an identity ` +\n `that has no Ekanos account.`,\n );\n }\n\n if (response.status === 403) {\n const body = await readOptionalJson(response);\n\n throw forbiddenError(\n str(body.error) ?? `${host} refused a CLI session for this account.`,\n body.code === 'EMAIL_UNVERIFIED'\n ? `Verify your email address, then run \"ekanos login\" again.`\n : `The Ekanos CLI needs a developer seat. Ask your administrator ` +\n `for one, then run \"ekanos login\" again.`,\n );\n }\n\n throwIfRateLimited(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-session', response.status);\n }\n\n return toSession(\n await readJson(response, `${host}/api/auth/cli-session`),\n host,\n );\n}\n\nexport interface RefreshedSession extends EkanosSession {\n /** The ROTATED refresh token. The presented one is already invalid. */\n auth0RefreshToken: string;\n}\n\nexport interface RefreshOptions {\n /**\n * Called the INSTANT a rotated refresh token is seen in the response — on\n * success and on failure alike, before anything is thrown.\n *\n * Persist-on-learn, not persist-on-success. Once the server's exchange with\n * Auth0 succeeds the presented token is dead and the replacement in that\n * body is the only copy in existence, so a client that only saves it on the\n * happy path destroys the partner's credential the first time /userinfo\n * blips. The callback runs before any throw so no error path can skip it.\n */\n onRotatedToken?: (rotated: string) => void;\n}\n\n/**\n * Re-mint an Ekanos JWT from the stored Auth0 refresh token. `accessToken` is\n * the previous Ekanos JWT (usually expired): the server reads the session id\n * from it to enforce that session's idle timeout, maximum age and revocation.\n */\nexport async function refreshEkanosSession(\n host: string,\n credentials: { auth0RefreshToken: string; accessToken: string },\n options: RefreshOptions = {},\n): Promise<RefreshedSession> {\n const response = await request(`${host}/api/auth/cli-refresh`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({\n auth0_refresh_token: credentials.auth0RefreshToken,\n access_token: credentials.accessToken,\n }),\n });\n\n // Read the body FIRST, whatever the status: the rotated token can arrive on\n // a 503, a 404 or a 500 just as readily as on a 200, and it must be captured\n // before any branch below throws.\n const body = await readOptionalJson(response);\n const rotated = str(body.auth0_refresh_token);\n\n if (rotated) {\n options.onRotatedToken?.(rotated);\n }\n\n throwIfNotConfigured(response, host, body);\n\n if (response.status === 401) {\n throw sessionExpiredError(host, str(body.error), str(body.reason));\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n str(body.error) ?? `${host} refused to renew the session.`,\n `Verify your email address, then run \"ekanos login\" again.`,\n );\n }\n\n throwIfRateLimited(response, host);\n\n if (response.status === 503 && body.code === 'REFRESH_INCOMPLETE') {\n // NOT an auth failure. The credential was renewed — and, thanks to the\n // callback above, already saved. Auth0's /userinfo is simply not answering.\n throw networkError(\n `${host} renewed the session but could not verify the identity with Auth0.`,\n `Retry in a moment — your credentials are still valid and have been ` +\n `updated. You do NOT need to run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no Ekanos account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then retry.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-refresh', response.status);\n }\n\n if (!rotated) {\n throw networkError(\n `${host} refreshed the session without returning a rotated refresh token.`,\n `Run \"ekanos login\" to start a fresh session, and report this — the ` +\n `deployment's Auth0 CLI application may not have rotation enabled.`,\n );\n }\n\n return { ...toSession(body, host), auth0RefreshToken: rotated };\n}\n\n/**\n * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can\n * decide whether to refresh and retry — an expired access token is an ordinary\n * event, not an error.\n */\nexport async function fetchIdentity(\n host: string,\n accessToken: string,\n): Promise<EkanosIdentity | null> {\n const response = await request(`${host}/api/auth/cli-whoami`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n // 401, or the 307 that an auth-gated Ekanos route answers with. Both mean\n // \"this token did not authenticate\".\n if (response.status === 401 || isRedirect(response.status)) {\n return null;\n }\n\n if (response.status === 403) {\n throw authRequiredError(\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-whoami', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-whoami`);\n const user = (body.user ?? {}) as Record<string, unknown>;\n const id = str(user.id);\n\n if (!id) {\n throw networkError(\n `${host} returned an identity without a user id.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-whoami route.`,\n );\n }\n\n return { id, email: str(user.email), name: str(user.name) };\n}\n\n/**\n * End the server-side session behind an Ekanos JWT (`ekanos logout`). Returns\n * whether the server answered — `true` also when the session was already\n * revoked, since either way it can no longer be refreshed. Never throws for\n * an HTTP status: logout must always reach the local delete.\n */\nexport async function revokeEkanosSession(\n host: string,\n accessToken: string,\n): Promise<boolean> {\n const response = await request(`${host}/api/auth/cli-logout`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ access_token: accessToken }),\n });\n\n return response.ok;\n}\n\nfunction toSession(body: Record<string, unknown>, host: string): EkanosSession {\n const user = (body.user ?? {}) as Record<string, unknown>;\n\n const accessToken = str(body.access_token);\n const expiresAt = str(body.expires_at);\n const userId = str(user.id);\n\n if (!accessToken || !expiresAt || !userId) {\n throw networkError(\n `${host} returned an incomplete session.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `CLI auth routes, then retry.`,\n );\n }\n\n return { accessToken, expiresAt, userId, email: str(user.email) };\n}\n\nexport async function request(\n url: string,\n init: RequestInit,\n): Promise<Response> {\n try {\n return await fetch(url, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n\n throw networkError(\n `Could not reach ${new URL(url).origin}: ${reason}`,\n `Check the host is correct and reachable from this machine ` +\n `(proxy, VPN, DNS), then retry.`,\n );\n }\n}\n\n/**\n * Best-effort body read for a response whose status may be an error. Returns\n * an empty object rather than throwing, because the caller needs to inspect\n * the body BEFORE deciding what the status means — and a 500 from a proxy may\n * carry no JSON at all.\n */\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nasync function readJson(\n response: Response,\n what: string,\n): Promise<Record<string, unknown>> {\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch {\n throw networkError(\n `${what} did not return JSON.`,\n `Confirm the host is an Ekanos deployment and not a proxy or captive ` +\n `portal sitting in front of one.`,\n );\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw networkError(\n `${what} returned an unexpected payload.`,\n `Confirm the host is an Ekanos deployment, then retry.`,\n );\n }\n\n return parsed as Record<string, unknown>;\n}\n\n/**\n * 503 means the deployment is not provisioned for CLI login — an operator\n * action that has not happened, not a fault in the request.\n *\n * Every route that reads the Fusion CLI Auth0 config can answer this, so every\n * caller has to check for it. Falling through to `unexpectedStatus` would tell\n * a partner \"confirm the host is an Ekanos deployment\" when the host is a\n * perfectly good Ekanos deployment that is simply missing one env var — and\n * would exit 8 (network, retryable) instead of 9 (precondition, not).\n */\n/**\n * What the server's CLI session policy is, said once for every \"log in\n * again\" message. The server owns the numbers (`CLI_SESSION_LIFETIME` in the\n * web app, equal to the Fusion CLI Auth0 refresh token); this is the sentence.\n */\nexport const CLI_SESSION_LIFETIME_TEXT =\n 'A CLI session lasts up to 30 days and ends after 7 days without use; ' +\n 'within that window an expired access token is renewed silently.';\n\n/**\n * A 401 from `cli-refresh`: the session really is over (or Auth0 asked for\n * something a refresh cannot give). Says why, how long sessions last, and the\n * one command that fixes it.\n */\nexport function sessionExpiredError(\n host: string,\n serverMessage: string | null,\n reason: string | null,\n): CliError {\n const why =\n reason === 'auth0_mfa_required'\n ? 'Auth0 asked for multi-factor authentication to renew it, which a ' +\n 'refresh cannot answer.'\n : reason === 'session_revoked'\n ? 'It was signed out (\"ekanos logout\" or an administrator).'\n : reason === 'session_idle_expired'\n ? 'It went unused for longer than 7 days.'\n : reason === 'session_max_age_exceeded'\n ? 'It reached its 30-day maximum age.'\n : serverMessage\n ? `${serverMessage}.`\n : 'The server no longer accepts it.';\n\n return authRequiredError(\n `The CLI session for ${host} has ended. ${why} ${CLI_SESSION_LIFETIME_TEXT}`,\n `Run: ekanos login --host ${host}`,\n );\n}\n\nfunction throwIfNotConfigured(\n response: Response,\n host: string,\n body?: Record<string, unknown>,\n): void {\n if (response.status !== 503) return;\n\n // 503 carries two meanings on these routes. Only CLI_NOT_CONFIGURED is a\n // precondition; REFRESH_INCOMPLETE is transient and handled by its caller.\n if (body && body.code !== 'CLI_NOT_CONFIGURED') return;\n\n throw preconditionError(\n `${host} does not have CLI login configured.`,\n `Ask the Ekanos operator to provision the \"Fusion CLI\" Auth0 ` +\n `application and set AUTH0_CLI_CLIENT_ID on that deployment.`,\n );\n}\n\n/** 429: the deployment is throttling this machine's address. Retryable. */\nfunction throwIfRateLimited(response: Response, host: string): void {\n if (response.status !== 429) return;\n\n const retryAfter = Number.parseInt(\n response.headers.get('retry-after') ?? '',\n 10,\n );\n\n throw networkError(\n `${host} is rate-limiting sign-in requests from this address.`,\n Number.isFinite(retryAfter) && retryAfter > 0\n ? `Retry in ${retryAfter} seconds.`\n : `Retry in a few minutes.`,\n );\n}\n\nfunction unexpectedStatus(host: string, route: string, status: number) {\n return networkError(\n `${host}${route} responded ${status}.`,\n status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if it ` +\n `persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes ${route}.`,\n );\n}\n\n/**\n * Every Ekanos tenant signs in on an `*.auth0.com` domain today\n * (`fusion-{dev,stage,prod}.us.auth0.com`). If one moves to an Auth0 custom\n * domain, add that domain here.\n */\nfunction assertAuth0Domain(domain: string): void {\n const hostname = domain.toLowerCase();\n\n if (/^[a-z0-9-]+(\\.[a-z0-9-]+)*\\.auth0\\.com$/.test(hostname)) return;\n\n throw preconditionError(\n `The login configuration names a sign-in domain that is not Auth0 (${domain}).`,\n `Do not continue. Report this to the operator of the Ekanos ` +\n `deployment — it may be misconfigured or compromised.`,\n );\n}\n\nfunction assertOnDomain(endpoint: string, domain: string): void {\n let url: URL;\n\n try {\n url = new URL(endpoint);\n } catch {\n throw preconditionError(\n `The login configuration contains a malformed endpoint.`,\n `Report this to the operator of the Ekanos deployment.`,\n );\n }\n\n if (\n url.protocol !== 'https:' ||\n url.host.toLowerCase() !== domain.toLowerCase()\n ) {\n throw preconditionError(\n `The login configuration points at an endpoint outside its own Auth0 ` +\n `domain (${url.host}).`,\n `Do not continue. Report this to the operator of the Ekanos ` +\n `deployment — it may be misconfigured or compromised.`,\n );\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/commands/agents.js
CHANGED
|
@@ -16,6 +16,7 @@ import { applyAgentTemplate, archiveAgentTemplate, listAgentTemplates, publishAg
|
|
|
16
16
|
import { loginHint } from '../auth/host-trust.js';
|
|
17
17
|
import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
|
|
18
18
|
import { authRequiredError, invalidStateError, notFoundError, usageError, validationError, } from '../errors.js';
|
|
19
|
+
import { cli } from '../invocation.js';
|
|
19
20
|
import { writePulledFiles } from '../pull-write.js';
|
|
20
21
|
import { classifyPath } from '../safe-fs.js';
|
|
21
22
|
import { resolveSourceTarget } from '../source-target.js';
|
|
@@ -601,13 +602,13 @@ function resultSummary(ui, result, source) {
|
|
|
601
602
|
if (pinned.length > 0) {
|
|
602
603
|
rows.push([
|
|
603
604
|
'Pinned',
|
|
604
|
-
pinned.map((team) => `${team.team} stays on v${team.version} — v${team.hold.waiting} ${describeHold(team.hold)}; to move it:
|
|
605
|
+
pinned.map((team) => `${team.team} stays on v${team.version} — v${team.hold.waiting} ${describeHold(team.hold)}; to move it: ${cli()} teams agents unpin ${team.team} ${template.key} --source ${source}`),
|
|
605
606
|
]);
|
|
606
607
|
}
|
|
607
608
|
if (access && access.needed.length > 0) {
|
|
608
609
|
rows.push([
|
|
609
610
|
'Needs confirming',
|
|
610
|
-
access.needed.map((team) => `${team.team}:
|
|
611
|
+
access.needed.map((team) => `${team.team}: ${cli()} teams agents confirm ${team.team} ${template.key} --source ${source}`),
|
|
611
612
|
]);
|
|
612
613
|
}
|
|
613
614
|
const notes = (result.outcome === 'created' || result.outcome === 'updated') &&
|