@ekanos/cli 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +13 -13
  2. package/dist/auth/credential-store.d.ts +1 -1
  3. package/dist/auth/credential-store.js +3 -2
  4. package/dist/auth/credential-store.js.map +1 -1
  5. package/dist/auth/device-flow.d.ts +1 -1
  6. package/dist/auth/device-flow.js +11 -10
  7. package/dist/auth/device-flow.js.map +1 -1
  8. package/dist/auth/{fusion-api.d.ts → ekanos-api.d.ts} +9 -9
  9. package/dist/auth/{fusion-api.js → ekanos-api.js} +28 -28
  10. package/dist/auth/ekanos-api.js.map +1 -0
  11. package/dist/auth/session.d.ts +5 -5
  12. package/dist/auth/session.js +8 -7
  13. package/dist/auth/session.js.map +1 -1
  14. package/dist/bin.js.map +1 -1
  15. package/dist/commands/dev.d.ts +2 -2
  16. package/dist/commands/dev.js.map +1 -1
  17. package/dist/commands/init.js +4 -0
  18. package/dist/commands/init.js.map +1 -1
  19. package/dist/commands/login.d.ts +1 -1
  20. package/dist/commands/login.js +3 -3
  21. package/dist/commands/login.js.map +1 -1
  22. package/dist/commands/logout.js +1 -1
  23. package/dist/commands/logout.js.map +1 -1
  24. package/dist/commands/publish.d.ts +2 -2
  25. package/dist/commands/publish.js +3 -3
  26. package/dist/commands/publish.js.map +1 -1
  27. package/dist/commands/sources.js.map +1 -1
  28. package/dist/commands/status.js +5 -7
  29. package/dist/commands/status.js.map +1 -1
  30. package/dist/commands/test.js.map +1 -1
  31. package/dist/commands/upgrade.js +1 -1
  32. package/dist/commands/upgrade.js.map +1 -1
  33. package/dist/commands/use.js +1 -1
  34. package/dist/commands/use.js.map +1 -1
  35. package/dist/commands/validate.js.map +1 -1
  36. package/dist/commands/whoami.d.ts +1 -1
  37. package/dist/commands/whoami.js +1 -1
  38. package/dist/commands/whoami.js.map +1 -1
  39. package/dist/compatibility.js.map +1 -1
  40. package/dist/context.js.map +1 -1
  41. package/dist/delegate.js.map +1 -1
  42. package/dist/did-you-mean.js +26 -15
  43. package/dist/did-you-mean.js.map +1 -1
  44. package/dist/exit-codes.d.ts +10 -10
  45. package/dist/findings-renderer.js +6 -4
  46. package/dist/findings-renderer.js.map +1 -1
  47. package/dist/generated/fa-free-names.d.ts +1 -1
  48. package/dist/generated/fa-free-names.js +1 -1
  49. package/dist/generated/fa-free-names.js.map +1 -1
  50. package/dist/harness-scaffold.d.ts +1 -1
  51. package/dist/harness-scaffold.js +4 -2
  52. package/dist/harness-scaffold.js.map +1 -1
  53. package/dist/icon-findings.d.ts +1 -1
  54. package/dist/icon-findings.js +2 -2
  55. package/dist/icon-findings.js.map +1 -1
  56. package/dist/index.js +2 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/merge-manifest.js.map +1 -1
  59. package/dist/pack.js +1 -1
  60. package/dist/pack.js.map +1 -1
  61. package/dist/project-checks.js.map +1 -1
  62. package/dist/project.d.ts +7 -84
  63. package/dist/project.js +3 -3
  64. package/dist/project.js.map +1 -1
  65. package/dist/publish-api.d.ts +5 -5
  66. package/dist/publish-api.js +27 -33
  67. package/dist/publish-api.js.map +1 -1
  68. package/dist/safe-fs.js.map +1 -1
  69. package/dist/schema-skew.js.map +1 -1
  70. package/dist/source-locator.js.map +1 -1
  71. package/dist/sources-api.d.ts +3 -3
  72. package/dist/sources-api.js +11 -14
  73. package/dist/sources-api.js.map +1 -1
  74. package/dist/templates.js.map +1 -1
  75. package/dist/test-report.js.map +1 -1
  76. package/dist/toolchain-api.d.ts +4 -4
  77. package/dist/toolchain-api.js +1 -1
  78. package/dist/toolchain-api.js.map +1 -1
  79. package/dist/toolchain-resolve.js.map +1 -1
  80. package/dist/toolchain.d.ts +5 -5
  81. package/dist/ui.js +2 -8
  82. package/dist/ui.js.map +1 -1
  83. package/dist/update-notice.d.ts +1 -1
  84. package/dist/update-notice.js.map +1 -1
  85. package/dist/validate-findings.js.map +1 -1
  86. package/dist/wizard.js.map +1 -1
  87. package/package.json +15 -20
  88. package/templates/AGENTS.md.tmpl +13 -8
  89. package/templates/CLAUDE.md.tmpl +1 -1
  90. package/templates/claude-skill.md.tmpl +2 -2
  91. package/templates/harness/next.config.mjs.tmpl +1 -1
  92. package/templates/package.json.tmpl +6 -5
  93. package/templates/pnpm-workspace.yaml.tmpl +8 -0
  94. package/templates/skills/build-integration/SKILL.md +3 -3
  95. package/templates/skills/build-integration/references/api-wiring.md +2 -2
  96. package/templates/skills/build-integration/references/widget-design.md +1 -1
  97. package/dist/auth/fusion-api.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ekanos/cli
2
2
 
3
- The Ekanos partner toolchain. Scaffold, validate, and test a Fusion integration
3
+ The Ekanos partner toolchain. Scaffold, validate, and test an Ekanos integration
4
4
  against the published `@ekanos` packages — from any Node project, with no
5
5
  bundler and no dependency on `@ekanos/sdk` at runtime.
6
6
 
@@ -17,15 +17,15 @@ npx @ekanos/cli dev
17
17
  | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
18
  | `init` | Scaffold `ekanos.json`, a working `defineIntegration`, a test, a `tsconfig.json`, a vitest config, `.gitignore`, a `package.json` with real published deps, and the agent-context files (`AGENTS.md`, `CLAUDE.md`, a Claude Code skill). Defaults to a new `./<slug>` directory; `--dir .` scaffolds in place, on top of an existing project if there is one. |
19
19
  | `validate` | Load the definition via esbuild and validate it with the **real** `@ekanos/integration-schema` zod schemas; add project checks (the required vitest SDK-inline block). |
20
- | `dev` | Scaffold `.ekanos/harness/` and run it with **your** Next, so your widgets, tile, activation form and triggers render in real Fusion chrome. |
20
+ | `dev` | Scaffold `.ekanos/harness/` and run it with **your** Next, so your widgets, tile, activation form and triggers render in real Ekanos chrome. |
21
21
  | `test` | Delegate to the project's own test script via its package manager. |
22
- | `login` | Authenticate this machine against a Fusion deployment (Auth0 device flow). |
22
+ | `login` | Authenticate this machine against an Ekanos deployment (Auth0 device flow). |
23
23
  | `logout` | Revoke the session at Auth0 and delete the local credentials. |
24
24
  | `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. |
25
25
  | `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. |
26
- | `sources` | List every Fusion source you hold a developer or source-admin seat on (`GET /api/partner/sources`), with a marker on this project's current publish target. |
26
+ | `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. |
27
27
  | `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. |
28
- | `publish` | Validate, pack and submit the integration to a Fusion 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). |
28
+ | `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). |
29
29
  | `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). |
30
30
 
31
31
  ### Installing: global is fine
@@ -115,8 +115,8 @@ than a warning.
115
115
  | `entry` | yes, in the single form | top level | path to the module exporting your `defineIntegration(...)` result |
116
116
  | `integrations[]` | yes, in the list form | top level | `{ slug, entry }` per integration, same rules, slugs unique |
117
117
  | `sourceGlobs` | no | **top level only** | extra Tailwind `@source` globs; project-relative, no `..` |
118
- | `source` | no | top level | the Fusion source slug this project publishes to; written by the first successful `publish` |
119
- | `host` | no | top level | the Fusion host (http(s) origin) this project publishes to; written by the first successful `publish`, read by every host-taking verb |
118
+ | `source` | no | top level | the Ekanos source slug this project publishes to; written by the first successful `publish` |
119
+ | `host` | no | top level | the Ekanos host (http(s) origin) this project publishes to; written by the first successful `publish`, read by every host-taking verb |
120
120
  | `$schema` | no | top level | ignored, for editor tooling |
121
121
 
122
122
  The file is **strict**: an unrecognised key is an error naming the key, not a
@@ -191,7 +191,7 @@ gets the refusal, not the exposure — which is the point.
191
191
 
192
192
  An integration that declares `oauth` in `defineIntegration()` can run the whole
193
193
  authorize → callback → code-exchange → `onTokens` loop against **your own
194
- developer app**, in live mode, with no Fusion host involved:
194
+ developer app**, in live mode, with no Ekanos host involved:
195
195
 
196
196
  1. Switch the toolbar's **Data** dropdown to **Live API** (the entry needs a
197
197
  `live` block). An integration whose widgets never call the vendor from the
@@ -408,14 +408,14 @@ carries the same for manifest fields it could not resolve.
408
408
  ## Authentication
409
409
 
410
410
  ```bash
411
- ekanos login --host https://your-fusion-host # or set EKANOS_HOST
411
+ ekanos login --host https://your-ekanos-host # or set EKANOS_HOST
412
412
  ekanos whoami # host resolves — see below
413
413
  ekanos logout
414
414
  ```
415
415
 
416
416
  ### Host resolution
417
417
 
418
- `logout`, `whoami`, `status`, `sources`, `use` and `publish` resolve the Fusion
418
+ `logout`, `whoami`, `status`, `sources`, `use` and `publish` resolve the Ekanos
419
419
  host in this order:
420
420
 
421
421
  1. the `--host` flag
@@ -460,7 +460,7 @@ If either is found group- or world-accessible, the CLI tightens the permissions
460
460
  and warns on stderr — it does not refuse to run, but it does tell you, and you
461
461
  should assume the token was exposed and re-run `ekanos logout && ekanos login`.
462
462
 
463
- The file holds the rotating Auth0 refresh token plus a short-lived Fusion JWT.
463
+ The file holds the rotating Auth0 refresh token plus a short-lived Ekanos JWT.
464
464
  No token is ever printed: not by `whoami`, not in an error, not in a `--json`
465
465
  envelope, not at any log level.
466
466
 
@@ -499,12 +499,12 @@ signal to log in again — retry it.
499
499
  ### What this does and does not grant
500
500
 
501
501
  `login` establishes **identity**. What that identity is permitted to do is a
502
- separate concern, handled by Fusion's own authorization; the CLI adds no
502
+ separate concern, handled by Ekanos's own authorization; the CLI adds no
503
503
  permissions of its own and no path to acquire any.
504
504
 
505
505
  ## Publish target verification
506
506
 
507
- A logged-in developer often holds a seat on more than one Fusion source. The
507
+ A logged-in developer often holds a seat on more than one Ekanos source. The
508
508
  owner's requirement this mechanism exists for: **it must be impossible to
509
509
  publish to the wrong source by accident.**
510
510
 
@@ -1,7 +1,7 @@
1
1
  export interface StoredSession {
2
2
  /** The Auth0 refresh token. Rotating — replaced on every refresh. */
3
3
  auth0RefreshToken: string;
4
- /** The minted Fusion JWT. Short-lived; re-minted from the refresh token. */
4
+ /** The minted Ekanos JWT. Short-lived; re-minted from the refresh token. */
5
5
  accessToken: string;
6
6
  /** ISO-8601 expiry of `accessToken`, as reported by the server. */
7
7
  expiresAt: string;
@@ -7,7 +7,7 @@ import { preconditionError } from '../errors.js';
7
7
  * inside a directory forced to 0700.
8
8
  *
9
9
  * Keyed by host, because a partner routinely holds sessions against more than
10
- * one Fusion deployment (their sandbox and production) and a single-session
10
+ * one Ekanos deployment (their sandbox and production) and a single-session
11
11
  * file would silently swap one for the other.
12
12
  *
13
13
  * NOTHING in this module ever writes a token to a stream. `describe()` exists
@@ -47,7 +47,8 @@ export class CredentialStore {
47
47
  }
48
48
  /** Every host with a stored session. Used by `logout --all` and diagnostics. */
49
49
  hosts() {
50
- var _a, _b;
50
+ var _a;
51
+ var _b;
51
52
  return Object.keys((_b = (_a = this.load()) === null || _a === void 0 ? void 0 : _a.hosts) !== null && _b !== void 0 ? _b : {});
52
53
  }
53
54
  write(host, session) {
@@ -1 +1 @@
1
- {"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../../src/auth/credential-store.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport { preconditionError } from '../errors';\n\n/**\n * The on-disk credential store: `~/.ekanos/credentials.json`, mode 0600,\n * inside a directory forced to 0700.\n *\n * Keyed by host, because a partner routinely holds sessions against more than\n * one Fusion deployment (their sandbox and production) and a single-session\n * file would silently swap one for the other.\n *\n * NOTHING in this module ever writes a token to a stream. `describe()` exists\n * so callers have a safe thing to print; the tokens themselves leave here only\n * as return values.\n */\n\nconst CREDENTIALS_FILENAME = 'credentials.json';\nconst DIR_MODE = 0o700;\nconst FILE_MODE = 0o600;\nconst CURRENT_VERSION = 1;\n\n/** POSIX permission bits that must not be set on the credentials file. */\nconst FORBIDDEN_FILE_BITS = 0o077;\nconst FORBIDDEN_DIR_BITS = 0o077;\n\nexport interface StoredSession {\n /** The Auth0 refresh token. Rotating — replaced on every refresh. */\n auth0RefreshToken: string;\n /** The minted Fusion JWT. Short-lived; re-minted from the refresh token. */\n accessToken: string;\n /** ISO-8601 expiry of `accessToken`, as reported by the server. */\n expiresAt: string;\n userId: string;\n email: string | null;\n /** ISO-8601 timestamp of the last write. */\n updatedAt: string;\n}\n\ninterface CredentialsFile {\n version: number;\n hosts: Record<string, StoredSession>;\n}\n\nexport interface CredentialStoreOptions {\n /**\n * Overrides the home directory. `EKANOS_HOME` is honoured by the caller;\n * tests pass a temp dir. Never read from `process.env` in here so the store\n * is deterministic under test.\n */\n home: string;\n /** Where permission warnings go. Always stderr in production. */\n warn?: (message: string) => void;\n}\n\nexport class CredentialStore {\n private readonly dir: string;\n private readonly file: string;\n private readonly warn: (message: string) => void;\n\n constructor(options: CredentialStoreOptions) {\n this.dir = path.join(options.home, '.ekanos');\n this.file = path.join(this.dir, CREDENTIALS_FILENAME);\n this.warn = options.warn ?? (() => {});\n }\n\n /** Absolute path to the credentials file — safe to print. */\n get filePath(): string {\n return this.file;\n }\n\n /**\n * Read the session for `host`, or null when there is none.\n *\n * A file that exists but is unreadable or corrupt is a precondition failure,\n * not a silent \"logged out\": treating a corrupt file as absent would send the\n * user through a fresh browser login that then overwrites whatever was\n * really in there.\n */\n read(host: string): StoredSession | null {\n const file = this.load();\n\n return file?.hosts[normalizeHost(host)] ?? null;\n }\n\n /** Every host with a stored session. Used by `logout --all` and diagnostics. */\n hosts(): string[] {\n return Object.keys(this.load()?.hosts ?? {});\n }\n\n write(host: string, session: StoredSession): void {\n const file = this.load() ?? { version: CURRENT_VERSION, hosts: {} };\n\n file.version = CURRENT_VERSION;\n file.hosts[normalizeHost(host)] = session;\n\n this.persist(file);\n }\n\n /**\n * Remove the session for `host`. Returns whether anything was removed, so\n * `logout` can report accurately while still exiting 0 either way — logging\n * out twice is not an error.\n */\n clear(host: string): boolean {\n const file = this.load();\n\n if (!file) return false;\n\n const key = normalizeHost(host);\n\n if (!(key in file.hosts)) return false;\n\n delete file.hosts[key];\n this.persist(file);\n\n return true;\n }\n\n /** Remove every session. Returns the number of hosts cleared. */\n clearAll(): number {\n const file = this.load();\n\n if (!file) return 0;\n\n const count = Object.keys(file.hosts).length;\n\n if (count === 0) return 0;\n\n file.hosts = {};\n this.persist(file);\n\n return count;\n }\n\n private load(): CredentialsFile | null {\n if (!fs.existsSync(this.file)) return null;\n\n this.auditPermissions();\n\n let raw: string;\n\n try {\n raw = fs.readFileSync(this.file, 'utf8');\n } catch (error) {\n throw preconditionError(\n `Could not read ${this.file}: ${errorMessage(error)}`,\n `Check the file's ownership and permissions, or delete it and run ` +\n `\"ekanos login\" again.`,\n );\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw preconditionError(\n `${this.file} is not valid JSON.`,\n `Delete it and run \"ekanos login\" again — it holds only credentials, ` +\n `nothing you need to keep.`,\n );\n }\n\n if (!isCredentialsFile(parsed)) {\n throw preconditionError(\n `${this.file} is not a recognised credentials file.`,\n `Delete it and run \"ekanos login\" again.`,\n );\n }\n\n return parsed;\n }\n\n /**\n * Write atomically, and never through a mode the umask can widen.\n *\n * `writeFileSync`'s `mode` applies only when the file is CREATED, so a\n * pre-existing world-readable file would keep its mode on every subsequent\n * write. Writing a fresh temp file and renaming over the target sidesteps\n * that, and also means a crash mid-write cannot leave a half-written\n * credentials file behind.\n */\n private persist(file: CredentialsFile): void {\n this.ensureDir();\n\n const tmp = `${this.file}.${process.pid}.tmp`;\n const body = `${JSON.stringify(file, null, 2)}\\n`;\n\n try {\n fs.writeFileSync(tmp, body, { mode: FILE_MODE });\n // Belt and braces: `mode` above is masked by the process umask, so set\n // the mode explicitly rather than trusting it.\n chmodIfPosix(tmp, FILE_MODE);\n fs.renameSync(tmp, this.file);\n chmodIfPosix(this.file, FILE_MODE);\n } catch (error) {\n safeUnlink(tmp);\n\n throw preconditionError(\n `Could not write ${this.file}: ${errorMessage(error)}`,\n `Ensure ${this.dir} is writable by the current user, then retry.`,\n );\n }\n }\n\n /**\n * Create `~/.ekanos` at 0700 — and repair it if it already exists wider.\n *\n * A pre-existing directory is the common case (other Ekanos state may live\n * there), and `mkdir`'s mode is ignored when the directory is already\n * present, so the mode has to be asserted separately every time.\n */\n private ensureDir(): void {\n try {\n fs.mkdirSync(this.dir, { recursive: true, mode: DIR_MODE });\n } catch (error) {\n throw preconditionError(\n `Could not create ${this.dir}: ${errorMessage(error)}`,\n `Create it manually with \"mkdir -p ${this.dir} && chmod 700 ${this.dir}\", then retry.`,\n );\n }\n\n if (process.platform === 'win32') return;\n\n const mode = statMode(this.dir);\n\n if (mode !== null && (mode & FORBIDDEN_DIR_BITS) !== 0) {\n this.warn(\n `ekanos: ${this.dir} was group/world accessible (${formatMode(mode)}); tightening it to 0700.`,\n );\n chmodIfPosix(this.dir, DIR_MODE);\n }\n }\n\n /**\n * Warn about and repair an over-permissive credentials file.\n *\n * Repair rather than refuse: a hard refusal (ssh's behaviour) would leave a\n * partner with a CLI that will not run and no obvious fix, and the token is\n * already exposed by the time we notice — narrowing the window beats\n * blocking. The warning goes to stderr so `--json` stdout stays parseable.\n */\n private auditPermissions(): void {\n if (process.platform === 'win32') return;\n\n const mode = statMode(this.file);\n\n if (mode === null) return;\n\n if ((mode & FORBIDDEN_FILE_BITS) !== 0) {\n this.warn(\n `ekanos: ${this.file} was readable by other users (${formatMode(mode)}); ` +\n `tightening it to 0600. Rotate your session with \"ekanos logout && ekanos login\" ` +\n `if this machine is shared.`,\n );\n chmodIfPosix(this.file, FILE_MODE);\n }\n }\n}\n\n/**\n * Resolve the home directory the store lives under. `EKANOS_HOME` exists for\n * containers and CI, where `os.homedir()` can resolve to a non-writable path.\n */\nexport function resolveEkanosHome(\n env: Record<string, string | undefined> = process.env,\n): string {\n const override = env.EKANOS_HOME?.trim();\n\n return override && override.length > 0 ? override : os.homedir();\n}\n\n/**\n * Canonical key for a host: scheme + authority, no trailing slash, lowercased.\n * Without this, `https://app.example.com` and `https://app.example.com/` would\n * be two different sessions.\n */\nexport function normalizeHost(host: string): string {\n let url: URL;\n\n try {\n url = new URL(host);\n } catch {\n throw preconditionError(\n `\"${host}\" is not a valid URL.`,\n `Pass a full origin, e.g. \"--host https://app.example.com\".`,\n );\n }\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw preconditionError(\n `\"${host}\" must be an http(s) URL.`,\n `Pass a full origin, e.g. \"--host https://app.example.com\".`,\n );\n }\n\n return `${url.protocol}//${url.host}`.toLowerCase();\n}\n\n/** A printable summary of a session — never includes a token. */\nexport function describe(session: StoredSession): {\n userId: string;\n email: string | null;\n expiresAt: string;\n updatedAt: string;\n} {\n return {\n userId: session.userId,\n email: session.email,\n expiresAt: session.expiresAt,\n updatedAt: session.updatedAt,\n };\n}\n\nfunction isCredentialsFile(value: unknown): value is CredentialsFile {\n if (typeof value !== 'object' || value === null) return false;\n\n const candidate = value as Partial<CredentialsFile>;\n\n if (typeof candidate.version !== 'number') return false;\n if (typeof candidate.hosts !== 'object' || candidate.hosts === null) {\n return false;\n }\n\n return Object.values(candidate.hosts).every(isStoredSession);\n}\n\nfunction isStoredSession(value: unknown): value is StoredSession {\n if (typeof value !== 'object' || value === null) return false;\n\n const candidate = value as Partial<StoredSession>;\n\n return (\n typeof candidate.auth0RefreshToken === 'string' &&\n typeof candidate.accessToken === 'string' &&\n typeof candidate.expiresAt === 'string' &&\n typeof candidate.userId === 'string' &&\n (candidate.email === null || typeof candidate.email === 'string') &&\n typeof candidate.updatedAt === 'string'\n );\n}\n\nfunction statMode(target: string): number | null {\n try {\n return fs.statSync(target).mode & 0o777;\n } catch {\n return null;\n }\n}\n\nfunction chmodIfPosix(target: string, mode: number): void {\n if (process.platform === 'win32') return;\n\n try {\n fs.chmodSync(target, mode);\n } catch {\n // Best effort. A filesystem that rejects chmod (some mounted volumes,\n // WSL interop paths) must not break login; the warning already fired.\n }\n}\n\nfunction safeUnlink(target: string): void {\n try {\n fs.unlinkSync(target);\n } catch {\n // The temp file may never have been created.\n }\n}\n\nfunction formatMode(mode: number): string {\n return `0${mode.toString(8).padStart(3, '0')}`;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
1
+ {"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../../src/auth/credential-store.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\n\nimport { preconditionError } from '../errors';\n\n/**\n * The on-disk credential store: `~/.ekanos/credentials.json`, mode 0600,\n * inside a directory forced to 0700.\n *\n * Keyed by host, because a partner routinely holds sessions against more than\n * one Ekanos deployment (their sandbox and production) and a single-session\n * file would silently swap one for the other.\n *\n * NOTHING in this module ever writes a token to a stream. `describe()` exists\n * so callers have a safe thing to print; the tokens themselves leave here only\n * as return values.\n */\n\nconst CREDENTIALS_FILENAME = 'credentials.json';\nconst DIR_MODE = 0o700;\nconst FILE_MODE = 0o600;\nconst CURRENT_VERSION = 1;\n\n/** POSIX permission bits that must not be set on the credentials file. */\nconst FORBIDDEN_FILE_BITS = 0o077;\nconst FORBIDDEN_DIR_BITS = 0o077;\n\nexport interface StoredSession {\n /** The Auth0 refresh token. Rotating — replaced on every refresh. */\n auth0RefreshToken: string;\n /** The minted Ekanos JWT. Short-lived; re-minted from the refresh token. */\n accessToken: string;\n /** ISO-8601 expiry of `accessToken`, as reported by the server. */\n expiresAt: string;\n userId: string;\n email: string | null;\n /** ISO-8601 timestamp of the last write. */\n updatedAt: string;\n}\n\ninterface CredentialsFile {\n version: number;\n hosts: Record<string, StoredSession>;\n}\n\nexport interface CredentialStoreOptions {\n /**\n * Overrides the home directory. `EKANOS_HOME` is honoured by the caller;\n * tests pass a temp dir. Never read from `process.env` in here so the store\n * is deterministic under test.\n */\n home: string;\n /** Where permission warnings go. Always stderr in production. */\n warn?: (message: string) => void;\n}\n\nexport class CredentialStore {\n private readonly dir: string;\n private readonly file: string;\n private readonly warn: (message: string) => void;\n\n constructor(options: CredentialStoreOptions) {\n this.dir = path.join(options.home, '.ekanos');\n this.file = path.join(this.dir, CREDENTIALS_FILENAME);\n this.warn = options.warn ?? (() => {});\n }\n\n /** Absolute path to the credentials file — safe to print. */\n get filePath(): string {\n return this.file;\n }\n\n /**\n * Read the session for `host`, or null when there is none.\n *\n * A file that exists but is unreadable or corrupt is a precondition failure,\n * not a silent \"logged out\": treating a corrupt file as absent would send the\n * user through a fresh browser login that then overwrites whatever was\n * really in there.\n */\n read(host: string): StoredSession | null {\n const file = this.load();\n\n return file?.hosts[normalizeHost(host)] ?? null;\n }\n\n /** Every host with a stored session. Used by `logout --all` and diagnostics. */\n hosts(): string[] {\n return Object.keys(this.load()?.hosts ?? {});\n }\n\n write(host: string, session: StoredSession): void {\n const file = this.load() ?? { version: CURRENT_VERSION, hosts: {} };\n\n file.version = CURRENT_VERSION;\n file.hosts[normalizeHost(host)] = session;\n\n this.persist(file);\n }\n\n /**\n * Remove the session for `host`. Returns whether anything was removed, so\n * `logout` can report accurately while still exiting 0 either way — logging\n * out twice is not an error.\n */\n clear(host: string): boolean {\n const file = this.load();\n\n if (!file) return false;\n\n const key = normalizeHost(host);\n\n if (!(key in file.hosts)) return false;\n\n delete file.hosts[key];\n this.persist(file);\n\n return true;\n }\n\n /** Remove every session. Returns the number of hosts cleared. */\n clearAll(): number {\n const file = this.load();\n\n if (!file) return 0;\n\n const count = Object.keys(file.hosts).length;\n\n if (count === 0) return 0;\n\n file.hosts = {};\n this.persist(file);\n\n return count;\n }\n\n private load(): CredentialsFile | null {\n if (!fs.existsSync(this.file)) return null;\n\n this.auditPermissions();\n\n let raw: string;\n\n try {\n raw = fs.readFileSync(this.file, 'utf8');\n } catch (error) {\n throw preconditionError(\n `Could not read ${this.file}: ${errorMessage(error)}`,\n `Check the file's ownership and permissions, or delete it and run ` +\n `\"ekanos login\" again.`,\n );\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw preconditionError(\n `${this.file} is not valid JSON.`,\n `Delete it and run \"ekanos login\" again — it holds only credentials, ` +\n `nothing you need to keep.`,\n );\n }\n\n if (!isCredentialsFile(parsed)) {\n throw preconditionError(\n `${this.file} is not a recognised credentials file.`,\n `Delete it and run \"ekanos login\" again.`,\n );\n }\n\n return parsed;\n }\n\n /**\n * Write atomically, and never through a mode the umask can widen.\n *\n * `writeFileSync`'s `mode` applies only when the file is CREATED, so a\n * pre-existing world-readable file would keep its mode on every subsequent\n * write. Writing a fresh temp file and renaming over the target sidesteps\n * that, and also means a crash mid-write cannot leave a half-written\n * credentials file behind.\n */\n private persist(file: CredentialsFile): void {\n this.ensureDir();\n\n const tmp = `${this.file}.${process.pid}.tmp`;\n const body = `${JSON.stringify(file, null, 2)}\\n`;\n\n try {\n fs.writeFileSync(tmp, body, { mode: FILE_MODE });\n // Belt and braces: `mode` above is masked by the process umask, so set\n // the mode explicitly rather than trusting it.\n chmodIfPosix(tmp, FILE_MODE);\n fs.renameSync(tmp, this.file);\n chmodIfPosix(this.file, FILE_MODE);\n } catch (error) {\n safeUnlink(tmp);\n\n throw preconditionError(\n `Could not write ${this.file}: ${errorMessage(error)}`,\n `Ensure ${this.dir} is writable by the current user, then retry.`,\n );\n }\n }\n\n /**\n * Create `~/.ekanos` at 0700 — and repair it if it already exists wider.\n *\n * A pre-existing directory is the common case (other Ekanos state may live\n * there), and `mkdir`'s mode is ignored when the directory is already\n * present, so the mode has to be asserted separately every time.\n */\n private ensureDir(): void {\n try {\n fs.mkdirSync(this.dir, { recursive: true, mode: DIR_MODE });\n } catch (error) {\n throw preconditionError(\n `Could not create ${this.dir}: ${errorMessage(error)}`,\n `Create it manually with \"mkdir -p ${this.dir} && chmod 700 ${this.dir}\", then retry.`,\n );\n }\n\n if (process.platform === 'win32') return;\n\n const mode = statMode(this.dir);\n\n if (mode !== null && (mode & FORBIDDEN_DIR_BITS) !== 0) {\n this.warn(\n `ekanos: ${this.dir} was group/world accessible (${formatMode(mode)}); tightening it to 0700.`,\n );\n chmodIfPosix(this.dir, DIR_MODE);\n }\n }\n\n /**\n * Warn about and repair an over-permissive credentials file.\n *\n * Repair rather than refuse: a hard refusal (ssh's behaviour) would leave a\n * partner with a CLI that will not run and no obvious fix, and the token is\n * already exposed by the time we notice — narrowing the window beats\n * blocking. The warning goes to stderr so `--json` stdout stays parseable.\n */\n private auditPermissions(): void {\n if (process.platform === 'win32') return;\n\n const mode = statMode(this.file);\n\n if (mode === null) return;\n\n if ((mode & FORBIDDEN_FILE_BITS) !== 0) {\n this.warn(\n `ekanos: ${this.file} was readable by other users (${formatMode(mode)}); ` +\n `tightening it to 0600. Rotate your session with \"ekanos logout && ekanos login\" ` +\n `if this machine is shared.`,\n );\n chmodIfPosix(this.file, FILE_MODE);\n }\n }\n}\n\n/**\n * Resolve the home directory the store lives under. `EKANOS_HOME` exists for\n * containers and CI, where `os.homedir()` can resolve to a non-writable path.\n */\nexport function resolveEkanosHome(\n env: Record<string, string | undefined> = process.env,\n): string {\n const override = env.EKANOS_HOME?.trim();\n\n return override && override.length > 0 ? override : os.homedir();\n}\n\n/**\n * Canonical key for a host: scheme + authority, no trailing slash, lowercased.\n * Without this, `https://app.example.com` and `https://app.example.com/` would\n * be two different sessions.\n */\nexport function normalizeHost(host: string): string {\n let url: URL;\n\n try {\n url = new URL(host);\n } catch {\n throw preconditionError(\n `\"${host}\" is not a valid URL.`,\n `Pass a full origin, e.g. \"--host https://app.example.com\".`,\n );\n }\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') {\n throw preconditionError(\n `\"${host}\" must be an http(s) URL.`,\n `Pass a full origin, e.g. \"--host https://app.example.com\".`,\n );\n }\n\n return `${url.protocol}//${url.host}`.toLowerCase();\n}\n\n/** A printable summary of a session — never includes a token. */\nexport function describe(session: StoredSession): {\n userId: string;\n email: string | null;\n expiresAt: string;\n updatedAt: string;\n} {\n return {\n userId: session.userId,\n email: session.email,\n expiresAt: session.expiresAt,\n updatedAt: session.updatedAt,\n };\n}\n\nfunction isCredentialsFile(value: unknown): value is CredentialsFile {\n if (typeof value !== 'object' || value === null) return false;\n\n const candidate = value as Partial<CredentialsFile>;\n\n if (typeof candidate.version !== 'number') return false;\n if (typeof candidate.hosts !== 'object' || candidate.hosts === null) {\n return false;\n }\n\n return Object.values(candidate.hosts).every(isStoredSession);\n}\n\nfunction isStoredSession(value: unknown): value is StoredSession {\n if (typeof value !== 'object' || value === null) return false;\n\n const candidate = value as Partial<StoredSession>;\n\n return (\n typeof candidate.auth0RefreshToken === 'string' &&\n typeof candidate.accessToken === 'string' &&\n typeof candidate.expiresAt === 'string' &&\n typeof candidate.userId === 'string' &&\n (candidate.email === null || typeof candidate.email === 'string') &&\n typeof candidate.updatedAt === 'string'\n );\n}\n\nfunction statMode(target: string): number | null {\n try {\n return fs.statSync(target).mode & 0o777;\n } catch {\n return null;\n }\n}\n\nfunction chmodIfPosix(target: string, mode: number): void {\n if (process.platform === 'win32') return;\n\n try {\n fs.chmodSync(target, mode);\n } catch {\n // Best effort. A filesystem that rejects chmod (some mounted volumes,\n // WSL interop paths) must not break login; the warning already fired.\n }\n}\n\nfunction safeUnlink(target: string): void {\n try {\n fs.unlinkSync(target);\n } catch {\n // The temp file may never have been created.\n }\n}\n\nfunction formatMode(mode: number): string {\n return `0${mode.toString(8).padStart(3, '0')}`;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type CliAuthConfig } from './fusion-api.js';
1
+ import { type CliAuthConfig } from './ekanos-api.js';
2
2
  /**
3
3
  * RFC 8628 device authorization flow against Auth0.
4
4
  *
@@ -1,5 +1,5 @@
1
1
  import { authRequiredError, networkError } from '../errors.js';
2
- import { request } from './fusion-api.js';
2
+ import { request } from './ekanos-api.js';
3
3
  /**
4
4
  * RFC 8628 device authorization flow against Auth0.
5
5
  *
@@ -46,7 +46,7 @@ export async function startDeviceAuthorization(config, now = Date.now) {
46
46
  `Device Code.`);
47
47
  }
48
48
  throw networkError(`Auth0 refused to start the device flow (${response.status}${body.error ? `, ${body.error}` : ''}).`, `Retry shortly. If it persists, report the error code to the operator ` +
49
- `of this Fusion deployment.`);
49
+ `of this Ekanos deployment.`);
50
50
  }
51
51
  const deviceCode = requiredString(body, 'device_code');
52
52
  const userCode = requiredString(body, 'user_code');
@@ -72,9 +72,10 @@ export async function startDeviceAuthorization(config, now = Date.now) {
72
72
  * suspended laptop it would keep polling a code that died an hour ago.
73
73
  */
74
74
  export async function pollForDeviceTokens(config, authorization, options = {}) {
75
- var _a, _b, _c;
76
- const sleep = (_a = options.sleep) !== null && _a !== void 0 ? _a : defaultSleep;
77
- const now = (_b = options.now) !== null && _b !== void 0 ? _b : Date.now;
75
+ var _a;
76
+ var _b, _c;
77
+ const sleep = (_b = options.sleep) !== null && _b !== void 0 ? _b : defaultSleep;
78
+ const now = (_c = options.now) !== null && _c !== void 0 ? _c : Date.now;
78
79
  let intervalSeconds = authorization.intervalSeconds;
79
80
  let attempt = 0;
80
81
  for (;;) {
@@ -84,7 +85,7 @@ export async function pollForDeviceTokens(config, authorization, options = {}) {
84
85
  }
85
86
  await sleep(intervalSeconds * 1000);
86
87
  attempt += 1;
87
- (_c = options.onPoll) === null || _c === void 0 ? void 0 : _c.call(options, attempt);
88
+ (_a = options.onPoll) === null || _a === void 0 ? void 0 : _a.call(options, attempt);
88
89
  const response = await request(config.tokenEndpoint, {
89
90
  method: 'POST',
90
91
  headers: {
@@ -122,10 +123,10 @@ export async function pollForDeviceTokens(config, authorization, options = {}) {
122
123
  'time shown.');
123
124
  case 'access_denied':
124
125
  throw authRequiredError('The login request was denied in the browser.', 'Run "ekanos login" again and approve the request, or sign in as ' +
125
- 'an account that has access to this Fusion deployment.');
126
+ 'an account that has access to this Ekanos deployment.');
126
127
  default:
127
128
  throw networkError(`Auth0 rejected the device token request (${response.status}${body.error ? `, ${body.error}` : ''}).`, 'Run "ekanos login" again. If it persists, report the error code ' +
128
- 'to the operator of this Fusion deployment.');
129
+ 'to the operator of this Ekanos deployment.');
129
130
  }
130
131
  }
131
132
  }
@@ -142,7 +143,7 @@ async function parseOAuthBody(response, what) {
142
143
  catch (_a) {
143
144
  if (response.ok) {
144
145
  throw networkError(`Auth0 returned a non-JSON ${what} response.`, 'Retry shortly; if it persists, report it to the operator of this ' +
145
- 'Fusion deployment.');
146
+ 'Ekanos deployment.');
146
147
  }
147
148
  return {};
148
149
  }
@@ -154,7 +155,7 @@ function requiredString(body, key) {
154
155
  const value = body[key];
155
156
  if (typeof value !== 'string' || value.length === 0) {
156
157
  throw networkError(`Auth0 omitted "${key}" from its response.`, 'Retry shortly; if it persists, report it to the operator of this ' +
157
- 'Fusion deployment.');
158
+ 'Ekanos deployment.');
158
159
  }
159
160
  return value;
160
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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","sourcesContent":["import { authRequiredError, networkError } from '../errors';\nimport { type CliAuthConfig, request } from './fusion-api';\n\n/**\n * RFC 8628 device authorization flow against Auth0.\n *\n * Chosen over loopback-PKCE deliberately: loopback needs a browser on the same\n * machine and a bindable localhost port, and partners debug integrations over\n * SSH and inside containers where neither holds. The device flow moves the\n * browser step to whatever device the developer actually has one on.\n *\n * The user code and verification URL are returned, not printed — the caller\n * owns output, and in this CLI that means stderr, so `--json` stdout stays a\n * single parseable object.\n */\n\nexport const DEVICE_CODE_GRANT_TYPE =\n 'urn:ietf:params:oauth:grant-type:device_code';\n\n/** RFC 8628 §3.5 default when the server omits `interval`. */\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n\n/** RFC 8628 §3.5: back off by 5s each time the server says `slow_down`. */\nconst SLOW_DOWN_INCREMENT_SECONDS = 5;\n\n/** Never poll faster than this, whatever the server claims. */\nconst MIN_POLL_INTERVAL_SECONDS = 1;\n\n/** Cap our own patience even if Auth0 hands back an implausible expires_in. */\nconst MAX_FLOW_LIFETIME_SECONDS = 15 * 60;\n\nexport interface DeviceAuthorization {\n deviceCode: string;\n /** The short code the human types. Safe to display — useless on its own. */\n userCode: string;\n verificationUri: string;\n /** Pre-filled URL; absent on IdPs that do not support it. */\n verificationUriComplete: string | null;\n /** Absolute deadline (epoch ms) after which the device code is dead. */\n expiresAt: number;\n intervalSeconds: number;\n}\n\nexport interface DeviceTokens {\n accessToken: string;\n refreshToken: string;\n}\n\nexport interface PollOptions {\n /** Injected in tests. Defaults to a real timer. */\n sleep?: (ms: number) => Promise<void>;\n /** Injected in tests. Defaults to Date.now. */\n now?: () => number;\n /** Called once per poll attempt, for progress output. */\n onPoll?: (attempt: number) => void;\n}\n\n/** Step 1 — ask Auth0 for a device code and a user code. */\nexport async function startDeviceAuthorization(\n config: CliAuthConfig,\n now: () => number = Date.now,\n): Promise<DeviceAuthorization> {\n const response = await request(config.deviceAuthorizationEndpoint, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n },\n body: new URLSearchParams({\n client_id: config.clientId,\n scope: config.scope,\n }).toString(),\n });\n\n const body = await parseOAuthBody(response, 'device authorization');\n\n if (!response.ok) {\n // The single most likely failure, and the one worth naming precisely: the\n // Auth0 application does not have the device-code grant enabled.\n if (body.error === 'unauthorized_client') {\n throw networkError(\n `Auth0 rejected the device flow for this application ` +\n `(unauthorized_client).`,\n `The \"Fusion CLI\" Auth0 application needs the Device Code grant ` +\n `enabled. Ask the operator to run the provisioner for this tenant, ` +\n `or tick Applications → Fusion CLI → Advanced → Grant Types → ` +\n `Device Code.`,\n );\n }\n\n throw networkError(\n `Auth0 refused to start the device flow (${response.status}${\n body.error ? `, ${body.error}` : ''\n }).`,\n `Retry shortly. If it persists, report the error code to the operator ` +\n `of this Fusion deployment.`,\n );\n }\n\n const deviceCode = requiredString(body, 'device_code');\n const userCode = requiredString(body, 'user_code');\n const verificationUri = requiredString(body, 'verification_uri');\n\n const expiresIn = clampSeconds(\n numberOr(body.expires_in, MAX_FLOW_LIFETIME_SECONDS),\n 1,\n MAX_FLOW_LIFETIME_SECONDS,\n );\n\n return {\n deviceCode,\n userCode,\n verificationUri,\n verificationUriComplete:\n typeof body.verification_uri_complete === 'string'\n ? body.verification_uri_complete\n : null,\n expiresAt: now() + expiresIn * 1000,\n intervalSeconds: clampSeconds(\n numberOr(body.interval, DEFAULT_POLL_INTERVAL_SECONDS),\n MIN_POLL_INTERVAL_SECONDS,\n MAX_FLOW_LIFETIME_SECONDS,\n ),\n };\n}\n\n/**\n * Step 2 — poll the token endpoint until the human approves, denies, or the\n * device code expires.\n *\n * Expiry is enforced against `authorization.expiresAt`, not against a count of\n * attempts: an attempt count would drift with every `slow_down`, and on a\n * suspended laptop it would keep polling a code that died an hour ago.\n */\nexport async function pollForDeviceTokens(\n config: CliAuthConfig,\n authorization: DeviceAuthorization,\n options: PollOptions = {},\n): Promise<DeviceTokens> {\n const sleep = options.sleep ?? defaultSleep;\n const now = options.now ?? Date.now;\n\n let intervalSeconds = authorization.intervalSeconds;\n let attempt = 0;\n\n for (;;) {\n if (now() >= authorization.expiresAt) {\n throw authRequiredError(\n 'The login code expired before it was approved.',\n 'Run \"ekanos login\" again and complete the browser step within the ' +\n 'time shown.',\n );\n }\n\n await sleep(intervalSeconds * 1000);\n\n attempt += 1;\n options.onPoll?.(attempt);\n\n const response = await request(config.tokenEndpoint, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: authorization.deviceCode,\n client_id: config.clientId,\n }).toString(),\n });\n\n const body = await parseOAuthBody(response, 'device token');\n\n if (response.ok) {\n const accessToken = requiredString(body, 'access_token');\n const refreshToken =\n typeof body.refresh_token === 'string' ? body.refresh_token : null;\n\n if (!refreshToken) {\n // Without offline_access actually granted, `ekanos login` would work\n // once and then silently require a browser again within the hour.\n // Better to fail now, while the cause is still on screen.\n throw authRequiredError(\n 'Auth0 completed the login but issued no refresh token.',\n 'The \"Fusion CLI\" Auth0 application needs the Refresh Token grant ' +\n 'and the login must request the `offline_access` scope. Ask the ' +\n 'operator to re-run the provisioner for this tenant.',\n );\n }\n\n return { accessToken, refreshToken };\n }\n\n switch (body.error) {\n case 'authorization_pending':\n continue;\n\n case 'slow_down':\n intervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;\n continue;\n\n case 'expired_token':\n throw authRequiredError(\n 'The login code expired before it was approved.',\n 'Run \"ekanos login\" again and complete the browser step within the ' +\n 'time shown.',\n );\n\n case 'access_denied':\n throw authRequiredError(\n 'The login request was denied in the browser.',\n 'Run \"ekanos login\" again and approve the request, or sign in as ' +\n 'an account that has access to this Fusion deployment.',\n );\n\n default:\n throw networkError(\n `Auth0 rejected the device token request (${response.status}${\n body.error ? `, ${body.error}` : ''\n }).`,\n 'Run \"ekanos login\" again. If it persists, report the error code ' +\n 'to the operator of this Fusion deployment.',\n );\n }\n }\n}\n\n/**\n * Parse an OAuth response body without ever throwing on shape. OAuth error\n * documents are `{ error, error_description }`; neither field echoes the\n * device code or any token, so they are safe to surface.\n */\nasync function parseOAuthBody(\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 if (response.ok) {\n throw networkError(\n `Auth0 returned a non-JSON ${what} response.`,\n 'Retry shortly; if it persists, report it to the operator of this ' +\n 'Fusion deployment.',\n );\n }\n\n return {};\n }\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n const value = body[key];\n\n if (typeof value !== 'string' || value.length === 0) {\n throw networkError(\n `Auth0 omitted \"${key}\" from its response.`,\n 'Retry shortly; if it persists, report it to the operator of this ' +\n 'Fusion deployment.',\n );\n }\n\n return value;\n}\n\nfunction numberOr(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n}\n\nfunction clampSeconds(value: number, min: number, max: number): number {\n return Math.min(Math.max(Math.floor(value), min), max);\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/**\n * Best-effort revocation of a refresh token at Auth0 (RFC 7009).\n *\n * `logout` must not depend on this succeeding — a developer on a plane still\n * needs the local credentials removed — but a logout that only forgets the\n * token leaves a live, rotating credential valid for weeks. So: try, report\n * the outcome, never throw.\n */\nexport async function revokeRefreshToken(\n config: CliAuthConfig,\n refreshToken: string,\n): Promise<boolean> {\n try {\n const response = await request(\n `https://${config.auth0Domain}/oauth/revoke`,\n {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: config.clientId,\n token: refreshToken,\n }),\n },\n );\n\n return response.ok;\n } catch {\n // `request` throws a CliError on transport failure. Swallowed on purpose:\n // see the note above. Nothing here can carry the token into a log line.\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"device-flow.js","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"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","sourcesContent":["import { authRequiredError, networkError } from '../errors';\nimport { type CliAuthConfig, request } from './ekanos-api';\n\n/**\n * RFC 8628 device authorization flow against Auth0.\n *\n * Chosen over loopback-PKCE deliberately: loopback needs a browser on the same\n * machine and a bindable localhost port, and partners debug integrations over\n * SSH and inside containers where neither holds. The device flow moves the\n * browser step to whatever device the developer actually has one on.\n *\n * The user code and verification URL are returned, not printed — the caller\n * owns output, and in this CLI that means stderr, so `--json` stdout stays a\n * single parseable object.\n */\n\nexport const DEVICE_CODE_GRANT_TYPE =\n 'urn:ietf:params:oauth:grant-type:device_code';\n\n/** RFC 8628 §3.5 default when the server omits `interval`. */\nconst DEFAULT_POLL_INTERVAL_SECONDS = 5;\n\n/** RFC 8628 §3.5: back off by 5s each time the server says `slow_down`. */\nconst SLOW_DOWN_INCREMENT_SECONDS = 5;\n\n/** Never poll faster than this, whatever the server claims. */\nconst MIN_POLL_INTERVAL_SECONDS = 1;\n\n/** Cap our own patience even if Auth0 hands back an implausible expires_in. */\nconst MAX_FLOW_LIFETIME_SECONDS = 15 * 60;\n\nexport interface DeviceAuthorization {\n deviceCode: string;\n /** The short code the human types. Safe to display — useless on its own. */\n userCode: string;\n verificationUri: string;\n /** Pre-filled URL; absent on IdPs that do not support it. */\n verificationUriComplete: string | null;\n /** Absolute deadline (epoch ms) after which the device code is dead. */\n expiresAt: number;\n intervalSeconds: number;\n}\n\nexport interface DeviceTokens {\n accessToken: string;\n refreshToken: string;\n}\n\nexport interface PollOptions {\n /** Injected in tests. Defaults to a real timer. */\n sleep?: (ms: number) => Promise<void>;\n /** Injected in tests. Defaults to Date.now. */\n now?: () => number;\n /** Called once per poll attempt, for progress output. */\n onPoll?: (attempt: number) => void;\n}\n\n/** Step 1 — ask Auth0 for a device code and a user code. */\nexport async function startDeviceAuthorization(\n config: CliAuthConfig,\n now: () => number = Date.now,\n): Promise<DeviceAuthorization> {\n const response = await request(config.deviceAuthorizationEndpoint, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n },\n body: new URLSearchParams({\n client_id: config.clientId,\n scope: config.scope,\n }).toString(),\n });\n\n const body = await parseOAuthBody(response, 'device authorization');\n\n if (!response.ok) {\n // The single most likely failure, and the one worth naming precisely: the\n // Auth0 application does not have the device-code grant enabled.\n if (body.error === 'unauthorized_client') {\n throw networkError(\n `Auth0 rejected the device flow for this application ` +\n `(unauthorized_client).`,\n `The \"Fusion CLI\" Auth0 application needs the Device Code grant ` +\n `enabled. Ask the operator to run the provisioner for this tenant, ` +\n `or tick Applications → Fusion CLI → Advanced → Grant Types → ` +\n `Device Code.`,\n );\n }\n\n throw networkError(\n `Auth0 refused to start the device flow (${response.status}${\n body.error ? `, ${body.error}` : ''\n }).`,\n `Retry shortly. If it persists, report the error code to the operator ` +\n `of this Ekanos deployment.`,\n );\n }\n\n const deviceCode = requiredString(body, 'device_code');\n const userCode = requiredString(body, 'user_code');\n const verificationUri = requiredString(body, 'verification_uri');\n\n const expiresIn = clampSeconds(\n numberOr(body.expires_in, MAX_FLOW_LIFETIME_SECONDS),\n 1,\n MAX_FLOW_LIFETIME_SECONDS,\n );\n\n return {\n deviceCode,\n userCode,\n verificationUri,\n verificationUriComplete:\n typeof body.verification_uri_complete === 'string'\n ? body.verification_uri_complete\n : null,\n expiresAt: now() + expiresIn * 1000,\n intervalSeconds: clampSeconds(\n numberOr(body.interval, DEFAULT_POLL_INTERVAL_SECONDS),\n MIN_POLL_INTERVAL_SECONDS,\n MAX_FLOW_LIFETIME_SECONDS,\n ),\n };\n}\n\n/**\n * Step 2 — poll the token endpoint until the human approves, denies, or the\n * device code expires.\n *\n * Expiry is enforced against `authorization.expiresAt`, not against a count of\n * attempts: an attempt count would drift with every `slow_down`, and on a\n * suspended laptop it would keep polling a code that died an hour ago.\n */\nexport async function pollForDeviceTokens(\n config: CliAuthConfig,\n authorization: DeviceAuthorization,\n options: PollOptions = {},\n): Promise<DeviceTokens> {\n const sleep = options.sleep ?? defaultSleep;\n const now = options.now ?? Date.now;\n\n let intervalSeconds = authorization.intervalSeconds;\n let attempt = 0;\n\n for (;;) {\n if (now() >= authorization.expiresAt) {\n throw authRequiredError(\n 'The login code expired before it was approved.',\n 'Run \"ekanos login\" again and complete the browser step within the ' +\n 'time shown.',\n );\n }\n\n await sleep(intervalSeconds * 1000);\n\n attempt += 1;\n options.onPoll?.(attempt);\n\n const response = await request(config.tokenEndpoint, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n },\n body: new URLSearchParams({\n grant_type: DEVICE_CODE_GRANT_TYPE,\n device_code: authorization.deviceCode,\n client_id: config.clientId,\n }).toString(),\n });\n\n const body = await parseOAuthBody(response, 'device token');\n\n if (response.ok) {\n const accessToken = requiredString(body, 'access_token');\n const refreshToken =\n typeof body.refresh_token === 'string' ? body.refresh_token : null;\n\n if (!refreshToken) {\n // Without offline_access actually granted, `ekanos login` would work\n // once and then silently require a browser again within the hour.\n // Better to fail now, while the cause is still on screen.\n throw authRequiredError(\n 'Auth0 completed the login but issued no refresh token.',\n 'The \"Fusion CLI\" Auth0 application needs the Refresh Token grant ' +\n 'and the login must request the `offline_access` scope. Ask the ' +\n 'operator to re-run the provisioner for this tenant.',\n );\n }\n\n return { accessToken, refreshToken };\n }\n\n switch (body.error) {\n case 'authorization_pending':\n continue;\n\n case 'slow_down':\n intervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;\n continue;\n\n case 'expired_token':\n throw authRequiredError(\n 'The login code expired before it was approved.',\n 'Run \"ekanos login\" again and complete the browser step within the ' +\n 'time shown.',\n );\n\n case 'access_denied':\n throw authRequiredError(\n 'The login request was denied in the browser.',\n 'Run \"ekanos login\" again and approve the request, or sign in as ' +\n 'an account that has access to this Ekanos deployment.',\n );\n\n default:\n throw networkError(\n `Auth0 rejected the device token request (${response.status}${\n body.error ? `, ${body.error}` : ''\n }).`,\n 'Run \"ekanos login\" again. If it persists, report the error code ' +\n 'to the operator of this Ekanos deployment.',\n );\n }\n }\n}\n\n/**\n * Parse an OAuth response body without ever throwing on shape. OAuth error\n * documents are `{ error, error_description }`; neither field echoes the\n * device code or any token, so they are safe to surface.\n */\nasync function parseOAuthBody(\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 if (response.ok) {\n throw networkError(\n `Auth0 returned a non-JSON ${what} response.`,\n 'Retry shortly; if it persists, report it to the operator of this ' +\n 'Ekanos deployment.',\n );\n }\n\n return {};\n }\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n}\n\nfunction requiredString(body: Record<string, unknown>, key: string): string {\n const value = body[key];\n\n if (typeof value !== 'string' || value.length === 0) {\n throw networkError(\n `Auth0 omitted \"${key}\" from its response.`,\n 'Retry shortly; if it persists, report it to the operator of this ' +\n 'Ekanos deployment.',\n );\n }\n\n return value;\n}\n\nfunction numberOr(value: unknown, fallback: number): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : fallback;\n}\n\nfunction clampSeconds(value: number, min: number, max: number): number {\n return Math.min(Math.max(Math.floor(value), min), max);\n}\n\nfunction defaultSleep(ms: number): Promise<void> {\n return new Promise((resolve) => {\n setTimeout(resolve, ms);\n });\n}\n\n/**\n * Best-effort revocation of a refresh token at Auth0 (RFC 7009).\n *\n * `logout` must not depend on this succeeding — a developer on a plane still\n * needs the local credentials removed — but a logout that only forgets the\n * token leaves a live, rotating credential valid for weeks. So: try, report\n * the outcome, never throw.\n */\nexport async function revokeRefreshToken(\n config: CliAuthConfig,\n refreshToken: string,\n): Promise<boolean> {\n try {\n const response = await request(\n `https://${config.auth0Domain}/oauth/revoke`,\n {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: config.clientId,\n token: refreshToken,\n }),\n },\n );\n\n return response.ok;\n } catch {\n // `request` throws a CliError on transport failure. Swallowed on purpose:\n // see the note above. Nothing here can carry the token into a log line.\n return false;\n }\n}\n"]}
@@ -5,22 +5,22 @@ export interface CliAuthConfig {
5
5
  deviceAuthorizationEndpoint: string;
6
6
  tokenEndpoint: string;
7
7
  }
8
- export interface FusionSession {
8
+ export interface EkanosSession {
9
9
  accessToken: string;
10
10
  expiresAt: string;
11
11
  userId: string;
12
12
  email: string | null;
13
13
  }
14
- export interface FusionIdentity {
14
+ export interface EkanosIdentity {
15
15
  id: string;
16
16
  email: string | null;
17
17
  name: string | null;
18
18
  }
19
19
  /** Fetch the device-flow parameters for a host. */
20
20
  export declare function fetchCliAuthConfig(host: string): Promise<CliAuthConfig>;
21
- /** Exchange a verified Auth0 access token for a Fusion JWT. */
22
- export declare function createFusionSession(host: string, auth0AccessToken: string): Promise<FusionSession>;
23
- export interface RefreshedSession extends FusionSession {
21
+ /** Exchange a verified Auth0 access token for an Ekanos JWT. */
22
+ export declare function createEkanosSession(host: string, auth0AccessToken: string): Promise<EkanosSession>;
23
+ export interface RefreshedSession extends EkanosSession {
24
24
  /** The ROTATED refresh token. The presented one is already invalid. */
25
25
  auth0RefreshToken: string;
26
26
  }
@@ -37,12 +37,12 @@ export interface RefreshOptions {
37
37
  */
38
38
  onRotatedToken?: (rotated: string) => void;
39
39
  }
40
- /** Re-mint a Fusion JWT from the stored Auth0 refresh token. */
41
- export declare function refreshFusionSession(host: string, auth0RefreshToken: string, options?: RefreshOptions): Promise<RefreshedSession>;
40
+ /** Re-mint an Ekanos JWT from the stored Auth0 refresh token. */
41
+ export declare function refreshEkanosSession(host: string, auth0RefreshToken: string, options?: RefreshOptions): Promise<RefreshedSession>;
42
42
  /**
43
- * Prove the Fusion JWT authenticates. Returns null on 401 so the caller can
43
+ * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can
44
44
  * decide whether to refresh and retry — an expired access token is an ordinary
45
45
  * event, not an error.
46
46
  */
47
- export declare function fetchIdentity(host: string, accessToken: string): Promise<FusionIdentity | null>;
47
+ export declare function fetchIdentity(host: string, accessToken: string): Promise<EkanosIdentity | null>;
48
48
  export declare function request(url: string, init: RequestInit): Promise<Response>;
@@ -1,9 +1,9 @@
1
1
  import { authRequiredError, networkError, preconditionError } from '../errors.js';
2
2
  /**
3
- * The CLI's client for the four Fusion auth routes.
3
+ * The CLI's client for the four Ekanos auth routes.
4
4
  *
5
5
  * ---------------------------------------------------------------------------
6
- * WIRE CONTRACT — the seam between this package and the Fusion web app.
6
+ * WIRE CONTRACT — the seam between this package and the Ekanos web app.
7
7
  *
8
8
  * The server side is the host's `/api/auth/cli-<verb>` routes, and it ships on
9
9
  * a DIFFERENT BRANCH from this file. This block is the single place the shapes
@@ -21,7 +21,7 @@ import { authRequiredError, networkError, preconditionError } from '../errors.js
21
21
  * → 200 { access_token, expires_at, user: { id, email } }
22
22
  * → 400 { error, code: "INVALID_REQUEST" } malformed / wrong shape
23
23
  * → 401 { error, code: "SESSION_EXPIRED" } Auth0 said no, OR no
24
- * Fusion account for it
24
+ * Ekanos account for it
25
25
  * → 503 { error, code: "CLI_NOT_CONFIGURED" } deployment not provisioned
26
26
  * → 500 { error, code: "INTERNAL_ERROR" }
27
27
  *
@@ -32,7 +32,7 @@ import { authRequiredError, networkError, preconditionError } from '../errors.js
32
32
  * → 400 { error, code: "INVALID_REQUEST" }
33
33
  * → 401 { error, code: "SESSION_EXPIRED" } expired or revoked
34
34
  * → 404 { error, code: "ACCOUNT_NOT_FOUND",
35
- * auth0_refresh_token } no Fusion account
35
+ * auth0_refresh_token } no Ekanos account
36
36
  * → 502 { error, code: "TOKEN_EXCHANGE_FAILED" } Auth0 returned no
37
37
  * rotated token
38
38
  * → 503 { error, code: "CLI_NOT_CONFIGURED" } deployment not provisioned
@@ -61,14 +61,14 @@ import { authRequiredError, networkError, preconditionError } from '../errors.js
61
61
  * → 404 { error, code: "ACCOUNT_NOT_FOUND" }
62
62
  * → 3xx treated as 401, see below
63
63
  *
64
- * `access_token` is a Fusion HS256 JWT. Every authenticated Fusion route
64
+ * `access_token` is an Ekanos HS256 JWT. Every authenticated Ekanos route
65
65
  * accepts it as `Authorization: Bearer` — see `resolveRequestSupabaseJwt` in
66
66
  * `@kit/supabase`, which both `requireUser` and the RLS server client consume.
67
67
  * ---------------------------------------------------------------------------
68
68
  *
69
69
  * Two rules hold everywhere in this file:
70
70
  *
71
- * - `redirect: 'manual'`. Fusion answers an unauthenticated page request with
71
+ * - `redirect: 'manual'`. Ekanos answers an unauthenticated page request with
72
72
  * a 307 to `/auth/sign-in`; Node's fetch follows redirects by default, so
73
73
  * without this a stale token would come back as a 200 full of HTML and be
74
74
  * read as success. A 3xx here is treated as "not authenticated", full stop.
@@ -98,19 +98,19 @@ export async function fetchCliAuthConfig(host) {
98
98
  tokenEndpoint: str(body.token_endpoint),
99
99
  };
100
100
  if (Object.values(config).some((value) => value === null)) {
101
- throw preconditionError(`${host} returned an incomplete CLI login configuration.`, `Confirm the deployment is running a Fusion build that includes the ` +
101
+ throw preconditionError(`${host} returned an incomplete CLI login configuration.`, `Confirm the deployment is running an Ekanos build that includes the ` +
102
102
  `/api/auth/cli-config route.`);
103
103
  }
104
104
  // The endpoints come from the server, so they are attacker-controlled if the
105
105
  // host is. Pin them to the Auth0 domain the same response declares, so a
106
- // compromised or misconfigured Fusion cannot redirect the device flow — and
106
+ // compromised or misconfigured Ekanos cannot redirect the device flow — and
107
107
  // the resulting credentials — to an arbitrary origin.
108
108
  assertOnDomain(config.deviceAuthorizationEndpoint, config.auth0Domain);
109
109
  assertOnDomain(config.tokenEndpoint, config.auth0Domain);
110
110
  return config;
111
111
  }
112
- /** Exchange a verified Auth0 access token for a Fusion JWT. */
113
- export async function createFusionSession(host, auth0AccessToken) {
112
+ /** Exchange a verified Auth0 access token for an Ekanos JWT. */
113
+ export async function createEkanosSession(host, auth0AccessToken) {
114
114
  const response = await request(`${host}/api/auth/cli-session`, {
115
115
  method: 'POST',
116
116
  headers: { 'content-type': 'application/json', accept: 'application/json' },
@@ -120,15 +120,15 @@ export async function createFusionSession(host, auth0AccessToken) {
120
120
  if (response.status === 401) {
121
121
  throw authRequiredError(`${host} rejected the Auth0 identity.`, `Run "ekanos login" again. If it keeps failing, confirm your account ` +
122
122
  `exists on ${host} — the browser login can succeed for an identity ` +
123
- `that has no Fusion account.`);
123
+ `that has no Ekanos account.`);
124
124
  }
125
125
  if (!response.ok) {
126
126
  throw unexpectedStatus(host, '/api/auth/cli-session', response.status);
127
127
  }
128
128
  return toSession(await readJson(response, `${host}/api/auth/cli-session`), host);
129
129
  }
130
- /** Re-mint a Fusion JWT from the stored Auth0 refresh token. */
131
- export async function refreshFusionSession(host, auth0RefreshToken, options = {}) {
130
+ /** Re-mint an Ekanos JWT from the stored Auth0 refresh token. */
131
+ export async function refreshEkanosSession(host, auth0RefreshToken, options = {}) {
132
132
  var _a;
133
133
  const response = await request(`${host}/api/auth/cli-refresh`, {
134
134
  method: 'POST',
@@ -156,7 +156,7 @@ export async function refreshFusionSession(host, auth0RefreshToken, options = {}
156
156
  `updated. You do NOT need to run "ekanos login" again.`);
157
157
  }
158
158
  if (response.status === 404) {
159
- throw authRequiredError(`${host} has no Fusion account for the signed-in identity.`, `Sign in to ${host} in a browser once to have the account created, ` +
159
+ throw authRequiredError(`${host} has no Ekanos account for the signed-in identity.`, `Sign in to ${host} in a browser once to have the account created, ` +
160
160
  `then retry.`);
161
161
  }
162
162
  if (!response.ok) {
@@ -169,7 +169,7 @@ export async function refreshFusionSession(host, auth0RefreshToken, options = {}
169
169
  return Object.assign(Object.assign({}, toSession(body, host)), { auth0RefreshToken: rotated });
170
170
  }
171
171
  /**
172
- * Prove the Fusion JWT authenticates. Returns null on 401 so the caller can
172
+ * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can
173
173
  * decide whether to refresh and retry — an expired access token is an ordinary
174
174
  * event, not an error.
175
175
  */
@@ -182,7 +182,7 @@ export async function fetchIdentity(host, accessToken) {
182
182
  authorization: `Bearer ${accessToken}`,
183
183
  },
184
184
  });
185
- // 401, or the 307 that an auth-gated Fusion route answers with. Both mean
185
+ // 401, or the 307 that an auth-gated Ekanos route answers with. Both mean
186
186
  // "this token did not authenticate".
187
187
  if (response.status === 401 || isRedirect(response.status)) {
188
188
  return null;
@@ -201,7 +201,7 @@ export async function fetchIdentity(host, accessToken) {
201
201
  const user = ((_a = body.user) !== null && _a !== void 0 ? _a : {});
202
202
  const id = str(user.id);
203
203
  if (!id) {
204
- throw networkError(`${host} returned an identity without a user id.`, `Confirm the deployment is running a Fusion build that includes the ` +
204
+ throw networkError(`${host} returned an identity without a user id.`, `Confirm the deployment is running an Ekanos build that includes the ` +
205
205
  `/api/auth/cli-whoami route.`);
206
206
  }
207
207
  return { id, email: str(user.email), name: str(user.name) };
@@ -213,7 +213,7 @@ function toSession(body, host) {
213
213
  const expiresAt = str(body.expires_at);
214
214
  const userId = str(user.id);
215
215
  if (!accessToken || !expiresAt || !userId) {
216
- throw networkError(`${host} returned an incomplete session.`, `Confirm the deployment is running a Fusion build that includes the ` +
216
+ throw networkError(`${host} returned an incomplete session.`, `Confirm the deployment is running an Ekanos build that includes the ` +
217
217
  `CLI auth routes, then retry.`);
218
218
  }
219
219
  return { accessToken, expiresAt, userId, email: str(user.email) };
@@ -251,11 +251,11 @@ async function readJson(response, what) {
251
251
  parsed = await response.json();
252
252
  }
253
253
  catch (_a) {
254
- throw networkError(`${what} did not return JSON.`, `Confirm the host is a Fusion deployment and not a proxy or captive ` +
254
+ throw networkError(`${what} did not return JSON.`, `Confirm the host is an Ekanos deployment and not a proxy or captive ` +
255
255
  `portal sitting in front of one.`);
256
256
  }
257
257
  if (typeof parsed !== 'object' || parsed === null) {
258
- throw networkError(`${what} returned an unexpected payload.`, `Confirm the host is a Fusion deployment, then retry.`);
258
+ throw networkError(`${what} returned an unexpected payload.`, `Confirm the host is an Ekanos deployment, then retry.`);
259
259
  }
260
260
  return parsed;
261
261
  }
@@ -265,8 +265,8 @@ async function readJson(response, what) {
265
265
  *
266
266
  * Every route that reads the Fusion CLI Auth0 config can answer this, so every
267
267
  * caller has to check for it. Falling through to `unexpectedStatus` would tell
268
- * a partner "confirm the host is a Fusion deployment" when the host is a
269
- * perfectly good Fusion deployment that is simply missing one env var — and
268
+ * a partner "confirm the host is an Ekanos deployment" when the host is a
269
+ * perfectly good Ekanos deployment that is simply missing one env var — and
270
270
  * would exit 8 (network, retryable) instead of 9 (precondition, not).
271
271
  */
272
272
  function throwIfNotConfigured(response, host, body) {
@@ -276,14 +276,14 @@ function throwIfNotConfigured(response, host, body) {
276
276
  // precondition; REFRESH_INCOMPLETE is transient and handled by its caller.
277
277
  if (body && body.code !== 'CLI_NOT_CONFIGURED')
278
278
  return;
279
- throw preconditionError(`${host} does not have CLI login configured.`, `Ask the Fusion operator to provision the "Fusion CLI" Auth0 ` +
279
+ throw preconditionError(`${host} does not have CLI login configured.`, `Ask the Ekanos operator to provision the "Fusion CLI" Auth0 ` +
280
280
  `application and set AUTH0_CLI_CLIENT_ID on that deployment.`);
281
281
  }
282
282
  function unexpectedStatus(host, route, status) {
283
283
  return networkError(`${host}${route} responded ${status}.`, status >= 500
284
- ? `The Fusion deployment returned a server error. Retry shortly; if it ` +
284
+ ? `The Ekanos deployment returned a server error. Retry shortly; if it ` +
285
285
  `persists, report it with the status code.`
286
- : `Confirm the host is a Fusion deployment running a build that ` +
286
+ : `Confirm the host is an Ekanos deployment running a build that ` +
287
287
  `includes ${route}.`);
288
288
  }
289
289
  function assertOnDomain(endpoint, domain) {
@@ -292,12 +292,12 @@ function assertOnDomain(endpoint, domain) {
292
292
  url = new URL(endpoint);
293
293
  }
294
294
  catch (_a) {
295
- throw preconditionError(`The login configuration contains a malformed endpoint.`, `Report this to the operator of the Fusion deployment.`);
295
+ throw preconditionError(`The login configuration contains a malformed endpoint.`, `Report this to the operator of the Ekanos deployment.`);
296
296
  }
297
297
  if (url.protocol !== 'https:' ||
298
298
  url.host.toLowerCase() !== domain.toLowerCase()) {
299
299
  throw preconditionError(`The login configuration points at an endpoint outside its own Auth0 ` +
300
- `domain (${url.host}).`, `Do not continue. Report this to the operator of the Fusion ` +
300
+ `domain (${url.host}).`, `Do not continue. Report this to the operator of the Ekanos ` +
301
301
  `deployment — it may be misconfigured or compromised.`);
302
302
  }
303
303
  }
@@ -307,4 +307,4 @@ function isRedirect(status) {
307
307
  function str(value) {
308
308
  return typeof value === 'string' && value.length > 0 ? value : null;
309
309
  }
310
- //# sourceMappingURL=fusion-api.js.map
310
+ //# sourceMappingURL=ekanos-api.js.map