@ekanos/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/dist/auth/credential-store.d.ts +96 -0
  4. package/dist/auth/credential-store.js +274 -0
  5. package/dist/auth/credential-store.js.map +1 -0
  6. package/dist/auth/device-flow.d.ts +57 -0
  7. package/dist/auth/device-flow.js +201 -0
  8. package/dist/auth/device-flow.js.map +1 -0
  9. package/dist/auth/fusion-api.d.ts +48 -0
  10. package/dist/auth/fusion-api.js +310 -0
  11. package/dist/auth/fusion-api.js.map +1 -0
  12. package/dist/auth/session.d.ts +57 -0
  13. package/dist/auth/session.js +114 -0
  14. package/dist/auth/session.js.map +1 -0
  15. package/dist/bin.d.ts +2 -0
  16. package/dist/bin.js +20 -0
  17. package/dist/bin.js.map +1 -0
  18. package/dist/commands/dev.d.ts +36 -0
  19. package/dist/commands/dev.js +207 -0
  20. package/dist/commands/dev.js.map +1 -0
  21. package/dist/commands/init.d.ts +30 -0
  22. package/dist/commands/init.js +178 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/login.d.ts +20 -0
  25. package/dist/commands/login.js +92 -0
  26. package/dist/commands/login.js.map +1 -0
  27. package/dist/commands/logout.d.ts +20 -0
  28. package/dist/commands/logout.js +75 -0
  29. package/dist/commands/logout.js.map +1 -0
  30. package/dist/commands/test.d.ts +16 -0
  31. package/dist/commands/test.js +54 -0
  32. package/dist/commands/test.js.map +1 -0
  33. package/dist/commands/validate.d.ts +14 -0
  34. package/dist/commands/validate.js +98 -0
  35. package/dist/commands/validate.js.map +1 -0
  36. package/dist/commands/whoami.d.ts +18 -0
  37. package/dist/commands/whoami.js +29 -0
  38. package/dist/commands/whoami.js.map +1 -0
  39. package/dist/compatibility.d.ts +81 -0
  40. package/dist/compatibility.js +135 -0
  41. package/dist/compatibility.js.map +1 -0
  42. package/dist/context.d.ts +81 -0
  43. package/dist/context.js +125 -0
  44. package/dist/context.js.map +1 -0
  45. package/dist/errors.d.ts +41 -0
  46. package/dist/errors.js +111 -0
  47. package/dist/errors.js.map +1 -0
  48. package/dist/exit-codes.d.ts +52 -0
  49. package/dist/exit-codes.js +51 -0
  50. package/dist/exit-codes.js.map +1 -0
  51. package/dist/harness-scaffold.d.ts +212 -0
  52. package/dist/harness-scaffold.js +434 -0
  53. package/dist/harness-scaffold.js.map +1 -0
  54. package/dist/index.d.ts +17 -0
  55. package/dist/index.js +262 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/load-definition.d.ts +20 -0
  58. package/dist/load-definition.js +126 -0
  59. package/dist/load-definition.js.map +1 -0
  60. package/dist/merge-manifest.d.ts +44 -0
  61. package/dist/merge-manifest.js +124 -0
  62. package/dist/merge-manifest.js.map +1 -0
  63. package/dist/package-manager.d.ts +25 -0
  64. package/dist/package-manager.js +57 -0
  65. package/dist/package-manager.js.map +1 -0
  66. package/dist/parse-argv.d.ts +27 -0
  67. package/dist/parse-argv.js +74 -0
  68. package/dist/parse-argv.js.map +1 -0
  69. package/dist/project-checks.d.ts +18 -0
  70. package/dist/project-checks.js +77 -0
  71. package/dist/project-checks.js.map +1 -0
  72. package/dist/project.d.ts +140 -0
  73. package/dist/project.js +228 -0
  74. package/dist/project.js.map +1 -0
  75. package/dist/safe-fs.d.ts +55 -0
  76. package/dist/safe-fs.js +209 -0
  77. package/dist/safe-fs.js.map +1 -0
  78. package/dist/templates.d.ts +23 -0
  79. package/dist/templates.js +49 -0
  80. package/dist/templates.js.map +1 -0
  81. package/package.json +62 -0
  82. package/templates/ekanos.json.tmpl +4 -0
  83. package/templates/gitignore.tmpl +12 -0
  84. package/templates/harness/app/[slug]/activation/page.tsx.tmpl +1 -0
  85. package/templates/harness/app/[slug]/layout.tsx.tmpl +1 -0
  86. package/templates/harness/app/[slug]/tile/page.tsx.tmpl +1 -0
  87. package/templates/harness/app/[slug]/triggers/page.tsx.tmpl +1 -0
  88. package/templates/harness/app/[slug]/widgets/[widgetId]/page.tsx.tmpl +1 -0
  89. package/templates/harness/app/[slug]/widgets/page.tsx.tmpl +1 -0
  90. package/templates/harness/app/harness-shell.tsx.tmpl +9 -0
  91. package/templates/harness/app/layout.tsx.tmpl +4 -0
  92. package/templates/harness/app/page.tsx.tmpl +7 -0
  93. package/templates/harness/next.config.mjs.tmpl +46 -0
  94. package/templates/harness/package.json.tmpl +11 -0
  95. package/templates/harness/postcss.config.mjs.tmpl +6 -0
  96. package/templates/harness/styles/globals.css.tmpl +33 -0
  97. package/templates/harness/tsconfig.json.tmpl +31 -0
  98. package/templates/harness/types/team-accounts.d.ts.tmpl +16 -0
  99. package/templates/harness.config.entry.ts.tmpl +65 -0
  100. package/templates/harness.config.ts.tmpl +58 -0
  101. package/templates/package.json.tmpl +28 -0
  102. package/templates/src/integration.test.ts.tmpl +24 -0
  103. package/templates/src/integration.ts.tmpl +83 -0
  104. package/templates/tsconfig.json.tmpl +19 -0
  105. package/templates/vitest.config.ts.tmpl +32 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vastly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # @ekanos/cli
2
+
3
+ The Ekanos partner toolchain. Scaffold, validate, and test a Fusion integration
4
+ against the published `@ekanos` packages — from any Node project, with no
5
+ bundler and no dependency on `@ekanos/sdk` at runtime.
6
+
7
+ ```bash
8
+ npx @ekanos/cli init --slug my-integration --yes # works in an empty dir, or merges into an existing package.json
9
+ npx @ekanos/cli validate
10
+ npx @ekanos/cli test
11
+ npx @ekanos/cli dev
12
+ ```
13
+
14
+ ## Verbs
15
+
16
+ | Verb | What it does |
17
+ |---|---|
18
+ | `init` | Scaffold `ekanos.json`, a working `defineIntegration`, a test, a `tsconfig.json`, a vitest config, `.gitignore`, and a `package.json` with real published deps. Runs in an empty directory or on top of an existing project. |
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. |
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). |
23
+ | `logout` | Revoke the session at Auth0 and delete the local credentials. |
24
+ | `whoami` | Print the identity the stored session authenticates as. |
25
+
26
+ ## Global flags
27
+
28
+ | Flag | What it does |
29
+ |---|---|
30
+ | `--json` | Force the JSON envelope on, even at a terminal. |
31
+ | `--no-json` | Force the human-readable form on, even when piped. |
32
+ | `--version` | Print the CLI version and exit 0. Works with or without a verb. |
33
+ | `--help` | Print usage. Also available per-verb (`ekanos validate --help`). |
34
+
35
+ Short flags are not supported — the parser rejects them rather than guessing.
36
+
37
+ ## Agent-native envelope
38
+
39
+ Every verb speaks JSON. JSON mode is **inferred** from a non-TTY stdout or
40
+ `CLAUDECODE=1`, and an explicit `--json` / `--no-json` overrides that inference
41
+ in either direction. In JSON mode **stdout carries exactly one JSON object** —
42
+ `{ ok, data, error: { code, message, hint } }` — and every progress line goes to
43
+ stderr. Errors always carry an imperative `hint`.
44
+
45
+ The CLI never prompts, in any mode. `init` requires `--slug` and fails with a
46
+ usage error naming the flag rather than asking for it.
47
+
48
+ ### Exit codes (frozen public contract)
49
+
50
+ `0` ok · `1` internal · `2` usage · `3` validation-findings · `4` auth-required ·
51
+ `5` forbidden · `6` not-found · `7` invalid-state · `8` network ·
52
+ `9` precondition-failed · `10` gate-failed.
53
+
54
+ ## `ekanos.json`
55
+
56
+ ```json
57
+ { "slug": "my-integration", "entry": "src/integration.ts" }
58
+ ```
59
+
60
+ More than one integration in a project? Use the list form instead — `validate`
61
+ then cross-checks them against each other (colliding slugs, widget ids, tool
62
+ names), which it cannot do when it can only see one:
63
+
64
+ ```json
65
+ { "integrations": [
66
+ { "slug": "acme-crm", "entry": "src/crm.ts" },
67
+ { "slug": "acme-billing", "entry": "src/billing.ts" }
68
+ ] }
69
+ ```
70
+
71
+ Declare one form or the other, never both. `validate` also checks that each
72
+ `slug` here agrees with the `slug` in the definition it points at — they
73
+ address the same integration everywhere, so a disagreement is an error rather
74
+ than a warning.
75
+
76
+ ### Every field
77
+
78
+ | Field | Required | Where | What |
79
+ |---|---|---|---|
80
+ | `slug` | yes, in the single form | top level | kebab-case; must equal the definition's `slug` |
81
+ | `entry` | yes, in the single form | top level | path to the module exporting your `defineIntegration(...)` result |
82
+ | `integrations[]` | yes, in the list form | top level | `{ slug, entry }` per integration, same rules, slugs unique |
83
+ | `sourceGlobs` | no | **top level only** | extra Tailwind `@source` globs; project-relative, no `..` |
84
+ | `$schema` | no | top level | ignored, for editor tooling |
85
+
86
+ The file is **strict**: an unrecognised key is an error naming the key, not a
87
+ silent no-op. `sourceGlobs` is deliberately not per-integration — Tailwind scans
88
+ one content set for the whole shell, so splitting it per integration would
89
+ imply an isolation that does not exist.
90
+
91
+ ### The generated shell
92
+
93
+ `ekanos dev` writes exactly this, and rewrites any of it whose bytes drift:
94
+
95
+ ```
96
+ .ekanos/harness/
97
+ .ekanos-version { harness, cli, templates } — what generated this
98
+ app/layout.tsx stylesheet + metadata + shell re-export
99
+ app/harness-shell.tsx 'use client' — injects your registry
100
+ app/page.tsx server component — the index
101
+ app/[slug]/layout.tsx ─┐
102
+ app/[slug]/widgets/page.tsx │
103
+ app/[slug]/widgets/[widgetId]/page.tsx
104
+ app/[slug]/tile/page.tsx ├─ one-line re-exports of @ekanos/harness/routes
105
+ app/[slug]/activation/page.tsx
106
+ app/[slug]/triggers/page.tsx ─┘
107
+ next.config.mjs transpiles @ekanos/harness, aliases the workspace mock
108
+ postcss.config.mjs @tailwindcss/postcss
109
+ tsconfig.json no baseUrl, no paths
110
+ styles/globals.css @import '@ekanos/harness/styles.css' + your @source globs
111
+ types/team-accounts.d.ts types the workspace mock through the export map
112
+ package.json scripts only — no deps, everything resolves upward
113
+ ```
114
+
115
+ `next dev`/`next build` add `next-env.d.ts`, `.next/`, and (Next 16)
116
+ `AGENTS.md`/`CLAUDE.md` alongside them. None of that is ours and none of it is
117
+ tracked — `.ekanos/` is gitignored in full.
118
+
119
+ `entry` points at the module exporting your `defineIntegration(...)` result
120
+ (default export, or any named export that is an object with a string `slug`).
121
+
122
+ ## `ekanos dev`
123
+
124
+ ```bash
125
+ pnpm add -D @ekanos/harness next react react-dom tailwindcss @tailwindcss/postcss
126
+ npx @ekanos/cli dev # scaffold, then run on http://localhost:3100
127
+ npx @ekanos/cli dev --port 4000
128
+ npx @ekanos/cli dev --no-start # scaffold only
129
+ npx @ekanos/cli dev --host 0.0.0.0 # expose on your network — read below first
130
+ ```
131
+
132
+ ### The harness binds loopback
133
+
134
+ `dev` passes `--hostname 127.0.0.1` to Next, which by default would bind
135
+ `0.0.0.0`. The harness has no authentication and renders your fixtures — and,
136
+ in live mode, real responses from your vendor API — so on a shared network a
137
+ default wildcard bind hands all of that to anyone who can reach the port.
138
+
139
+ `--host` opts in explicitly (for a phone on the same Wi-Fi, say). It prints a
140
+ warning saying what is now reachable, and the JSON envelope carries
141
+ `server.exposed: true` so a script can refuse to run that way.
142
+
143
+ ### Who owns what
144
+
145
+ | Path | Owner | You edit it? |
146
+ |---|---|---|
147
+ | `harness.config.ts` (project root) | **you** | **yes — the only one.** Written once, git-tracked, never touched again. |
148
+ | your integration source | **you** | yes |
149
+ | `.ekanos/harness/**` | generated | no — gitignored, rewritten on every run |
150
+ | `@ekanos/harness` | us | no — it is an ordinary dependency |
151
+
152
+ That split is the point: a harness bugfix ships as an `@ekanos/harness` version
153
+ bump you pick up with `pnpm up`, with zero edits to any file you own. Every
154
+ file in the generated shell is mechanical — re-exports of
155
+ `@ekanos/harness/routes`, an app shell that injects your registry, and
156
+ configs — so none of it ever needs a judgment call from you;
157
+ [The generated shell](#the-generated-shell) above is the exact list.
158
+ `.ekanos/harness/.ekanos-version` records which `@ekanos/harness` generated it,
159
+ and when that disagrees with the installed version the shell is regenerated
160
+ automatically and `dev` says so on stderr.
161
+
162
+ Re-running is free: files whose bytes already match are not rewritten (so Next's
163
+ watcher stays quiet), and `.ekanos/` is added to `.gitignore` exactly once.
164
+ `--force` rewrites the generated shell; nothing rewrites `harness.config.ts`.
165
+
166
+ `dev` refuses to write through a symlink, to write outside your project, or to
167
+ overwrite a `.ekanos/harness` it did not generate (one with no `.ekanos-version`
168
+ marker) unless you pass `--force`. Each exits 7 having changed nothing.
169
+
170
+ ### One constraint on your own code
171
+
172
+ `harness.config.ts` is imported from both a server component and a client
173
+ component, so everything reachable from it must be importable in both graphs.
174
+ Deriving fixture query keys from a `'use client'` module compiles and then
175
+ fails at request time with `Attempted to call … from the server`. Move the key
176
+ builders into a plain module with no `'use client'` directive and have your
177
+ hooks import that — which is also how you keep fixtures and hooks from
178
+ drifting apart.
179
+
180
+ ### Where your source lives
181
+
182
+ The generated `styles/globals.css` points Tailwind at the usual places
183
+ (`src/`, `app/`, `lib/`, `components/`, `integrations/`, `packages/`). If your
184
+ integration lives somewhere else, add globs to `ekanos.json` — do NOT edit the
185
+ stylesheet, which is regenerated on every run:
186
+
187
+ ```json
188
+ { "slug": "acme-crm", "entry": "src/integration.ts",
189
+ "sourceGlobs": ["vendor/acme/**/*.{ts,tsx}"] }
190
+ ```
191
+
192
+ A glob Tailwind is not pointed at costs you a silently unstyled integration
193
+ with no build error, so these are validated strictly: project-relative, no
194
+ `..`, and a restricted character set, because they are written verbatim into
195
+ generated CSS.
196
+
197
+ ### Version compatibility
198
+
199
+ The shell templates ship with `@ekanos/cli`; the code they import ships with
200
+ `@ekanos/harness`. Both declare a **shell contract** — an integer that changes
201
+ only when the shell a harness requires changes (an export moving, the
202
+ server/client split, the set of files). `dev` compares them before it writes
203
+ anything and names the mismatch — `@ekanos/cli 0.1.0 is too old for
204
+ @ekanos/harness 0.2.0: that harness needs shell contract 2, and this CLI
205
+ generates 1` — rather than letting it surface later as a module-resolution
206
+ error. A version bump that does not change the contract never blocks an
207
+ upgrade.
208
+
209
+ `.ekanos/harness/.ekanos-version` records the harness version, the CLI version
210
+ and a hash of the templates, so a change to any of them regenerates the shell
211
+ and `dev` says which one moved.
212
+
213
+ **For maintainers:** propagation is byte-driven — `dev` rewrites a generated
214
+ file whenever its content differs, marker or not. So a change to the SHELL
215
+ ships in an `@ekanos/cli` release, never a harness one: bumping
216
+ `@ekanos/harness` alone cannot deliver a new template, because the templates
217
+ are not in that package. Bump `SHELL_CONTRACT` in
218
+ `src/compatibility.ts` only when the coupling changes, not for every template
219
+ edit.
220
+
221
+ ### For agents
222
+
223
+ `dev` in JSON mode (`--json`, a pipe, or `CLAUDECODE=1`) **scaffolds and
224
+ returns** rather than blocking on a dev server that never exits — the envelope's
225
+ `data.server` carries `{ started: false, command, url, port }` so the caller can
226
+ start it itself. Pass `--start` to override.
227
+
228
+ ## `ekanos init` on an existing project
229
+
230
+ `npm init -y` first is fine. `init` **merges** into a `package.json` you
231
+ already have rather than skipping it: missing dependencies, devDependencies and
232
+ scripts are added, and anything you already set is left alone — your pinned
233
+ version and your own `test` script are decisions, not omissions.
234
+
235
+ Deliberate exceptions:
236
+
237
+ - `npm init -y`'s stub `test` script (`echo "Error: no test specified" && exit
238
+ 1`) is replaced. It is npm's placeholder, not a choice, and keeping it left
239
+ `ekanos test` failing on a fresh scaffold.
240
+ - `"type"` is set to `module` even if you declare `"commonjs"`, with a loud
241
+ note saying it changed. `npm init -y` writes `"commonjs"` explicitly, so that
242
+ is the mainline rather than an edge case — and the files this scaffolder
243
+ writes are ESM TypeScript that cannot load under it. Leaving it would mean
244
+ writing files that do not run. If the project has CommonJS sources of its
245
+ own, revert that line and put your integration in its own package: the two
246
+ module systems cannot share one `package.json`.
247
+
248
+ Every verb after `init` fails fast with `precondition_failed` (exit 9) if
249
+ `"type"` is not `module`, naming the file and the fix — so missing the note
250
+ costs you one clear error rather than a module-resolution failure from inside
251
+ Node or Next.
252
+
253
+ Anything `init` does not do is reported with a hint saying what to do about it —
254
+ `data.skipped[]` carries `{ file, reason, hint }`, and `data.manifest.notes[]`
255
+ carries the same for manifest fields it could not resolve.
256
+
257
+ ## Authentication
258
+
259
+ ```bash
260
+ ekanos login --host https://your-fusion-host # or set EKANOS_HOST
261
+ ekanos whoami --host https://your-fusion-host
262
+ ekanos logout --host https://your-fusion-host
263
+ ```
264
+
265
+ `login` uses the **OAuth 2.0 device authorization flow**: it prints a short code
266
+ and a URL to **stderr**, and you approve the request in a browser on any device.
267
+ That is deliberate — a loopback/PKCE login needs a browser on the same machine
268
+ and a bindable localhost port, which is exactly what you do not have over SSH or
269
+ inside a container.
270
+
271
+ There is nothing to configure on your side. The CLI asks the host for its Auth0
272
+ domain, client id and scope (`GET /api/auth/cli-config`), so one binary works
273
+ against a sandbox and production.
274
+
275
+ ### Where credentials live
276
+
277
+ `~/.ekanos/credentials.json`, mode **0600**, inside a `~/.ekanos` directory
278
+ forced to **0700**, keyed by host so several deployments can be held at once.
279
+ Set `EKANOS_HOME` to relocate it (containers, CI).
280
+
281
+ If either is found group- or world-accessible, the CLI tightens the permissions
282
+ and warns on stderr — it does not refuse to run, but it does tell you, and you
283
+ should assume the token was exposed and re-run `ekanos logout && ekanos login`.
284
+
285
+ The file holds the rotating Auth0 refresh token plus a short-lived Fusion JWT.
286
+ No token is ever printed: not by `whoami`, not in an error, not in a `--json`
287
+ envelope, not at any log level.
288
+
289
+ ### Idempotence
290
+
291
+ - `login` with a session that still works reports `already-authenticated` and
292
+ never opens a browser. `--force` starts a fresh login regardless.
293
+ - `logout` twice succeeds; the second run reports `cleared: false`.
294
+ - `logout` revokes at Auth0 first, but a failed revocation never stops the local
295
+ delete — it is reported as `revoked: false` with a warning on stderr.
296
+
297
+ ### Rotation safety
298
+
299
+ Auth0 rotates the refresh token the moment the server's exchange succeeds, so
300
+ from that instant the replacement is the only copy in existence — and
301
+ `/api/auth/cli-refresh` can still fail afterwards. It therefore returns the
302
+ rotated token on its error responses too, and the CLI persists it the moment it
303
+ is seen, before surfacing any error. A transient upstream blip costs you a
304
+ retry, not your session.
305
+
306
+ Two hazards, both handled and worth telling apart:
307
+
308
+ - **Rotate-then-fail** (single process): the refresh rotates and a later step
309
+ fails. Handled as above — retry, no re-login.
310
+ - **Concurrent rotation** (two processes sharing one credentials file): both
311
+ present the same token, Auth0 honours the first, the second gets a clean
312
+ exit 4. No file lock; re-run `ekanos login`.
313
+
314
+ ### Exit codes to branch on
315
+
316
+ `4` (auth-required) means "run `ekanos login`" — no session, an expired one that
317
+ could not be renewed, or a revoked one. `8` (network) means the host was
318
+ unreachable, or the refresh rotated but could not complete; it is **not** a
319
+ signal to log in again — retry it.
320
+
321
+ ### What this does and does not grant
322
+
323
+ `login` establishes **identity**. What that identity is permitted to do is a
324
+ separate concern, handled by Fusion's own authorization; the CLI adds no
325
+ permissions of its own and no path to acquire any.
326
+
@@ -0,0 +1,96 @@
1
+ export interface StoredSession {
2
+ /** The Auth0 refresh token. Rotating — replaced on every refresh. */
3
+ auth0RefreshToken: string;
4
+ /** The minted Fusion JWT. Short-lived; re-minted from the refresh token. */
5
+ accessToken: string;
6
+ /** ISO-8601 expiry of `accessToken`, as reported by the server. */
7
+ expiresAt: string;
8
+ userId: string;
9
+ email: string | null;
10
+ /** ISO-8601 timestamp of the last write. */
11
+ updatedAt: string;
12
+ }
13
+ export interface CredentialStoreOptions {
14
+ /**
15
+ * Overrides the home directory. `EKANOS_HOME` is honoured by the caller;
16
+ * tests pass a temp dir. Never read from `process.env` in here so the store
17
+ * is deterministic under test.
18
+ */
19
+ home: string;
20
+ /** Where permission warnings go. Always stderr in production. */
21
+ warn?: (message: string) => void;
22
+ }
23
+ export declare class CredentialStore {
24
+ private readonly dir;
25
+ private readonly file;
26
+ private readonly warn;
27
+ constructor(options: CredentialStoreOptions);
28
+ /** Absolute path to the credentials file — safe to print. */
29
+ get filePath(): string;
30
+ /**
31
+ * Read the session for `host`, or null when there is none.
32
+ *
33
+ * A file that exists but is unreadable or corrupt is a precondition failure,
34
+ * not a silent "logged out": treating a corrupt file as absent would send the
35
+ * user through a fresh browser login that then overwrites whatever was
36
+ * really in there.
37
+ */
38
+ read(host: string): StoredSession | null;
39
+ /** Every host with a stored session. Used by `logout --all` and diagnostics. */
40
+ hosts(): string[];
41
+ write(host: string, session: StoredSession): void;
42
+ /**
43
+ * Remove the session for `host`. Returns whether anything was removed, so
44
+ * `logout` can report accurately while still exiting 0 either way — logging
45
+ * out twice is not an error.
46
+ */
47
+ clear(host: string): boolean;
48
+ /** Remove every session. Returns the number of hosts cleared. */
49
+ clearAll(): number;
50
+ private load;
51
+ /**
52
+ * Write atomically, and never through a mode the umask can widen.
53
+ *
54
+ * `writeFileSync`'s `mode` applies only when the file is CREATED, so a
55
+ * pre-existing world-readable file would keep its mode on every subsequent
56
+ * write. Writing a fresh temp file and renaming over the target sidesteps
57
+ * that, and also means a crash mid-write cannot leave a half-written
58
+ * credentials file behind.
59
+ */
60
+ private persist;
61
+ /**
62
+ * Create `~/.ekanos` at 0700 — and repair it if it already exists wider.
63
+ *
64
+ * A pre-existing directory is the common case (other Ekanos state may live
65
+ * there), and `mkdir`'s mode is ignored when the directory is already
66
+ * present, so the mode has to be asserted separately every time.
67
+ */
68
+ private ensureDir;
69
+ /**
70
+ * Warn about and repair an over-permissive credentials file.
71
+ *
72
+ * Repair rather than refuse: a hard refusal (ssh's behaviour) would leave a
73
+ * partner with a CLI that will not run and no obvious fix, and the token is
74
+ * already exposed by the time we notice — narrowing the window beats
75
+ * blocking. The warning goes to stderr so `--json` stdout stays parseable.
76
+ */
77
+ private auditPermissions;
78
+ }
79
+ /**
80
+ * Resolve the home directory the store lives under. `EKANOS_HOME` exists for
81
+ * containers and CI, where `os.homedir()` can resolve to a non-writable path.
82
+ */
83
+ export declare function resolveEkanosHome(env?: Record<string, string | undefined>): string;
84
+ /**
85
+ * Canonical key for a host: scheme + authority, no trailing slash, lowercased.
86
+ * Without this, `https://app.example.com` and `https://app.example.com/` would
87
+ * be two different sessions.
88
+ */
89
+ export declare function normalizeHost(host: string): string;
90
+ /** A printable summary of a session — never includes a token. */
91
+ export declare function describe(session: StoredSession): {
92
+ userId: string;
93
+ email: string | null;
94
+ expiresAt: string;
95
+ updatedAt: string;
96
+ };