@ekanos/cli 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/dist/auth/credential-store.d.ts +1 -1
- package/dist/auth/credential-store.js +3 -2
- package/dist/auth/credential-store.js.map +1 -1
- package/dist/auth/device-flow.d.ts +1 -1
- package/dist/auth/device-flow.js +11 -10
- package/dist/auth/device-flow.js.map +1 -1
- package/dist/auth/{fusion-api.d.ts → ekanos-api.d.ts} +9 -9
- package/dist/auth/{fusion-api.js → ekanos-api.js} +28 -28
- package/dist/auth/ekanos-api.js.map +1 -0
- package/dist/auth/session.d.ts +5 -5
- package/dist/auth/session.js +21 -11
- package/dist/auth/session.js.map +1 -1
- package/dist/banner.d.ts +1 -1
- package/dist/banner.js +4 -13
- package/dist/banner.js.map +1 -1
- package/dist/bin.js.map +1 -1
- package/dist/commands/dev.d.ts +2 -2
- package/dist/commands/dev.js +11 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.js +72 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +28 -17
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.js +31 -5
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.d.ts +2 -2
- package/dist/commands/publish.js +28 -15
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.js +26 -16
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +56 -45
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.js +15 -5
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/upgrade.js +86 -54
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +11 -3
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/validate.js +21 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/whoami.d.ts +1 -1
- package/dist/commands/whoami.js +17 -7
- package/dist/commands/whoami.js.map +1 -1
- package/dist/compatibility.js.map +1 -1
- package/dist/context.js +10 -3
- package/dist/context.js.map +1 -1
- package/dist/delegate.js.map +1 -1
- package/dist/did-you-mean.js +26 -15
- package/dist/did-you-mean.js.map +1 -1
- package/dist/exit-codes.d.ts +10 -10
- package/dist/findings-renderer.js +6 -4
- package/dist/findings-renderer.js.map +1 -1
- package/dist/generated/fa-free-names.d.ts +1 -1
- package/dist/generated/fa-free-names.js +1 -1
- package/dist/generated/fa-free-names.js.map +1 -1
- package/dist/harness-scaffold.d.ts +1 -1
- package/dist/harness-scaffold.js +4 -2
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/icon-findings.d.ts +1 -1
- package/dist/icon-findings.js +2 -2
- package/dist/icon-findings.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/merge-manifest.js.map +1 -1
- package/dist/pack.js +1 -1
- package/dist/pack.js.map +1 -1
- package/dist/project-checks.js.map +1 -1
- package/dist/project.d.ts +7 -84
- package/dist/project.js +3 -3
- package/dist/project.js.map +1 -1
- package/dist/publish-api.d.ts +5 -5
- package/dist/publish-api.js +27 -33
- package/dist/publish-api.js.map +1 -1
- package/dist/safe-fs.js.map +1 -1
- package/dist/schema-skew.js.map +1 -1
- package/dist/source-locator.js.map +1 -1
- package/dist/sources-api.d.ts +3 -3
- package/dist/sources-api.js +11 -14
- package/dist/sources-api.js.map +1 -1
- package/dist/templates.js.map +1 -1
- package/dist/test-report.js.map +1 -1
- package/dist/toolchain-api.d.ts +4 -4
- package/dist/toolchain-api.js +1 -1
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +5 -5
- package/dist/ui.d.ts +36 -0
- package/dist/ui.js +79 -10
- package/dist/ui.js.map +1 -1
- package/dist/update-notice.d.ts +1 -1
- package/dist/update-notice.js.map +1 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +15 -20
- package/templates/AGENTS.md.tmpl +13 -8
- package/templates/CLAUDE.md.tmpl +1 -1
- package/templates/claude-skill.md.tmpl +2 -2
- package/templates/harness/next.config.mjs.tmpl +1 -1
- package/templates/package.json.tmpl +6 -5
- package/templates/pnpm-workspace.yaml.tmpl +8 -0
- package/templates/skills/build-integration/SKILL.md +3 -3
- package/templates/skills/build-integration/references/api-wiring.md +2 -2
- package/templates/skills/build-integration/references/widget-design.md +1 -1
- package/dist/auth/fusion-api.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ekanos-api.js","sourceRoot":"","sources":["../../src/auth/ekanos-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { authRequiredError, networkError, preconditionError } from '../errors';\n\n/**\n * The CLI's client for the four Ekanos auth routes.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/auth/cli-<verb>` routes, and it ships on\n * a DIFFERENT BRANCH from this file. This block is the single place the shapes\n * are written down on the client side; if you change one side, change the\n * other and update this block. Nothing here imports host code — the two\n * halves are coupled only by HTTP, and the tests stub `fetch`, never the route.\n *\n * GET /api/auth/cli-config [no auth]\n * → 200 { auth0_domain, client_id, scope,\n * device_authorization_endpoint, token_endpoint } all strings\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n *\n * POST /api/auth/cli-session [no auth]\n * ← { auth0_access_token: string }\n * → 200 { access_token, expires_at, user: { id, email } }\n * → 400 { error, code: \"INVALID_REQUEST\" } malformed / wrong shape\n * → 401 { error, code: \"SESSION_EXPIRED\" } Auth0 said no, OR no\n * Ekanos account for it\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 500 { error, code: \"INTERNAL_ERROR\" }\n *\n * POST /api/auth/cli-refresh [no auth]\n * ← { auth0_refresh_token: string }\n * → 200 { access_token, expires_at, user: { id, email },\n * auth0_refresh_token } ← ROTATED; the presented one is dead\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"SESSION_EXPIRED\" } expired or revoked\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\",\n * auth0_refresh_token } no Ekanos account\n * → 502 { error, code: \"TOKEN_EXCHANGE_FAILED\" } Auth0 returned no\n * rotated token\n * → 503 { error, code: \"CLI_NOT_CONFIGURED\" } deployment not provisioned\n * → 503 { error, code: \"REFRESH_INCOMPLETE\",\n * auth0_refresh_token } rotated, but /userinfo\n * did not answer; RETRYABLE\n * → 500 { error, code: \"INTERNAL_ERROR\",\n * auth0_refresh_token? } present iff rotation\n * completed before the throw\n *\n * !! cli-refresh MAY RETURN `auth0_refresh_token` ON ERROR RESPONSES, and a\n * client MUST persist it before surfacing the error. Once the server's\n * exchange with Auth0 succeeds the presented token is dead and the value\n * in that body is the ONLY copy in existence; discarding it on a failure\n * path destroys the session permanently. Note 503 has TWO codes here —\n * branch on `code`, not on the status alone.\n *\n * All three routes above read the Fusion CLI Auth0 application config, so all\n * three can answer 503 CLI_NOT_CONFIGURED. cli-whoami cannot — it reads no\n * Auth0 config, only the caller's bearer token.\n *\n * GET /api/auth/cli-whoami [Authorization: Bearer <access_token>]\n * → 200 { user: { id, email, name } }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or invalid\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"ACCOUNT_NOT_FOUND\" }\n * → 3xx treated as 401, see below\n *\n * `access_token` is an Ekanos HS256 JWT. Every authenticated Ekanos route\n * accepts it as `Authorization: Bearer` — see `resolveRequestSupabaseJwt` in\n * `@kit/supabase`, which both `requireUser` and the RLS server client consume.\n * ---------------------------------------------------------------------------\n *\n * Two rules hold everywhere in this file:\n *\n * - `redirect: 'manual'`. Ekanos answers an unauthenticated page request with\n * a 307 to `/auth/sign-in`; Node's fetch follows redirects by default, so\n * without this a stale token would come back as a 200 full of HTML and be\n * read as success. A 3xx here is treated as \"not authenticated\", full stop.\n *\n * - No token is ever interpolated into an error message. The server's error\n * bodies are `{ error, code }` documents that do not echo credentials, and\n * the request bodies that DO carry credentials are never included in a\n * thrown message.\n */\n\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nexport interface CliAuthConfig {\n auth0Domain: string;\n clientId: string;\n scope: string;\n deviceAuthorizationEndpoint: string;\n tokenEndpoint: string;\n}\n\nexport interface EkanosSession {\n accessToken: string;\n expiresAt: string;\n userId: string;\n email: string | null;\n}\n\nexport interface EkanosIdentity {\n id: string;\n email: string | null;\n name: string | null;\n}\n\n/** Fetch the device-flow parameters for a host. */\nexport async function fetchCliAuthConfig(host: string): Promise<CliAuthConfig> {\n const response = await request(`${host}/api/auth/cli-config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n });\n\n throwIfNotConfigured(response, host);\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-config', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-config`);\n\n const config = {\n auth0Domain: str(body.auth0_domain),\n clientId: str(body.client_id),\n scope: str(body.scope),\n deviceAuthorizationEndpoint: str(body.device_authorization_endpoint),\n tokenEndpoint: str(body.token_endpoint),\n };\n\n if (Object.values(config).some((value) => value === null)) {\n throw preconditionError(\n `${host} returned an incomplete CLI login configuration.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-config route.`,\n );\n }\n\n // The endpoints come from the server, so they are attacker-controlled if the\n // host is. Pin them to the Auth0 domain the same response declares, so a\n // compromised or misconfigured Ekanos cannot redirect the device flow — and\n // the resulting credentials — to an arbitrary origin.\n assertOnDomain(config.deviceAuthorizationEndpoint!, config.auth0Domain!);\n assertOnDomain(config.tokenEndpoint!, config.auth0Domain!);\n\n return config as CliAuthConfig;\n}\n\n/** Exchange a verified Auth0 access token for an Ekanos JWT. */\nexport async function createEkanosSession(\n host: string,\n auth0AccessToken: string,\n): Promise<EkanosSession> {\n const response = await request(`${host}/api/auth/cli-session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_access_token: auth0AccessToken }),\n });\n\n throwIfNotConfigured(response, host);\n\n if (response.status === 401) {\n throw authRequiredError(\n `${host} rejected the Auth0 identity.`,\n `Run \"ekanos login\" again. If it keeps failing, confirm your account ` +\n `exists on ${host} — the browser login can succeed for an identity ` +\n `that has no Ekanos account.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-session', response.status);\n }\n\n return toSession(\n await readJson(response, `${host}/api/auth/cli-session`),\n host,\n );\n}\n\nexport interface RefreshedSession extends EkanosSession {\n /** The ROTATED refresh token. The presented one is already invalid. */\n auth0RefreshToken: string;\n}\n\nexport interface RefreshOptions {\n /**\n * Called the INSTANT a rotated refresh token is seen in the response — on\n * success and on failure alike, before anything is thrown.\n *\n * Persist-on-learn, not persist-on-success. Once the server's exchange with\n * Auth0 succeeds the presented token is dead and the replacement in that\n * body is the only copy in existence, so a client that only saves it on the\n * happy path destroys the partner's credential the first time /userinfo\n * blips. The callback runs before any throw so no error path can skip it.\n */\n onRotatedToken?: (rotated: string) => void;\n}\n\n/** Re-mint an Ekanos JWT from the stored Auth0 refresh token. */\nexport async function refreshEkanosSession(\n host: string,\n auth0RefreshToken: string,\n options: RefreshOptions = {},\n): Promise<RefreshedSession> {\n const response = await request(`${host}/api/auth/cli-refresh`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ auth0_refresh_token: auth0RefreshToken }),\n });\n\n // Read the body FIRST, whatever the status: the rotated token can arrive on\n // a 503, a 404 or a 500 just as readily as on a 200, and it must be captured\n // before any branch below throws.\n const body = await readOptionalJson(response);\n const rotated = str(body.auth0_refresh_token);\n\n if (rotated) {\n options.onRotatedToken?.(rotated);\n }\n\n throwIfNotConfigured(response, host, body);\n\n if (response.status === 401) {\n throw authRequiredError(\n `The stored session for ${host} is no longer valid.`,\n `Run \"ekanos login\" to sign in again. This is expected after the ` +\n `refresh token expires, after it is revoked in Auth0, or if two ` +\n `machines used the same credentials file.`,\n );\n }\n\n if (response.status === 503 && body.code === 'REFRESH_INCOMPLETE') {\n // NOT an auth failure. The credential was renewed — and, thanks to the\n // callback above, already saved. Auth0's /userinfo is simply not answering.\n throw networkError(\n `${host} renewed the session but could not verify the identity with Auth0.`,\n `Retry in a moment — your credentials are still valid and have been ` +\n `updated. You do NOT need to run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no Ekanos account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then retry.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-refresh', response.status);\n }\n\n if (!rotated) {\n throw networkError(\n `${host} refreshed the session without returning a rotated refresh token.`,\n `Run \"ekanos login\" to start a fresh session, and report this — the ` +\n `deployment's Auth0 CLI application may not have rotation enabled.`,\n );\n }\n\n return { ...toSession(body, host), auth0RefreshToken: rotated };\n}\n\n/**\n * Prove the Ekanos JWT authenticates. Returns null on 401 so the caller can\n * decide whether to refresh and retry — an expired access token is an ordinary\n * event, not an error.\n */\nexport async function fetchIdentity(\n host: string,\n accessToken: string,\n): Promise<EkanosIdentity | null> {\n const response = await request(`${host}/api/auth/cli-whoami`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n // 401, or the 307 that an auth-gated Ekanos route answers with. Both mean\n // \"this token did not authenticate\".\n if (response.status === 401 || isRedirect(response.status)) {\n return null;\n }\n\n if (response.status === 403) {\n throw authRequiredError(\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw authRequiredError(\n `${host} has no account for the signed-in identity.`,\n `Sign in to ${host} in a browser once to have the account created, ` +\n `then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw unexpectedStatus(host, '/api/auth/cli-whoami', response.status);\n }\n\n const body = await readJson(response, `${host}/api/auth/cli-whoami`);\n const user = (body.user ?? {}) as Record<string, unknown>;\n const id = str(user.id);\n\n if (!id) {\n throw networkError(\n `${host} returned an identity without a user id.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `/api/auth/cli-whoami route.`,\n );\n }\n\n return { id, email: str(user.email), name: str(user.name) };\n}\n\nfunction toSession(body: Record<string, unknown>, host: string): EkanosSession {\n const user = (body.user ?? {}) as Record<string, unknown>;\n\n const accessToken = str(body.access_token);\n const expiresAt = str(body.expires_at);\n const userId = str(user.id);\n\n if (!accessToken || !expiresAt || !userId) {\n throw networkError(\n `${host} returned an incomplete session.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `CLI auth routes, then retry.`,\n );\n }\n\n return { accessToken, expiresAt, userId, email: str(user.email) };\n}\n\nexport async function request(\n url: string,\n init: RequestInit,\n): Promise<Response> {\n try {\n return await fetch(url, {\n ...init,\n redirect: 'manual',\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n\n throw networkError(\n `Could not reach ${new URL(url).origin}: ${reason}`,\n `Check the host is correct and reachable from this machine ` +\n `(proxy, VPN, DNS), then retry.`,\n );\n }\n}\n\n/**\n * Best-effort body read for a response whose status may be an error. Returns\n * an empty object rather than throwing, because the caller needs to inspect\n * the body BEFORE deciding what the status means — and a 500 from a proxy may\n * carry no JSON at all.\n */\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nasync function readJson(\n response: Response,\n what: string,\n): Promise<Record<string, unknown>> {\n let parsed: unknown;\n\n try {\n parsed = await response.json();\n } catch {\n throw networkError(\n `${what} did not return JSON.`,\n `Confirm the host is an Ekanos deployment and not a proxy or captive ` +\n `portal sitting in front of one.`,\n );\n }\n\n if (typeof parsed !== 'object' || parsed === null) {\n throw networkError(\n `${what} returned an unexpected payload.`,\n `Confirm the host is an Ekanos deployment, then retry.`,\n );\n }\n\n return parsed as Record<string, unknown>;\n}\n\n/**\n * 503 means the deployment is not provisioned for CLI login — an operator\n * action that has not happened, not a fault in the request.\n *\n * Every route that reads the Fusion CLI Auth0 config can answer this, so every\n * caller has to check for it. Falling through to `unexpectedStatus` would tell\n * a partner \"confirm the host is an Ekanos deployment\" when the host is a\n * perfectly good Ekanos deployment that is simply missing one env var — and\n * would exit 8 (network, retryable) instead of 9 (precondition, not).\n */\nfunction throwIfNotConfigured(\n response: Response,\n host: string,\n body?: Record<string, unknown>,\n): void {\n if (response.status !== 503) return;\n\n // 503 carries two meanings on these routes. Only CLI_NOT_CONFIGURED is a\n // precondition; REFRESH_INCOMPLETE is transient and handled by its caller.\n if (body && body.code !== 'CLI_NOT_CONFIGURED') return;\n\n throw preconditionError(\n `${host} does not have CLI login configured.`,\n `Ask the Ekanos operator to provision the \"Fusion CLI\" Auth0 ` +\n `application and set AUTH0_CLI_CLIENT_ID on that deployment.`,\n );\n}\n\nfunction unexpectedStatus(host: string, route: string, status: number) {\n return networkError(\n `${host}${route} responded ${status}.`,\n status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if it ` +\n `persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes ${route}.`,\n );\n}\n\nfunction assertOnDomain(endpoint: string, domain: string): void {\n let url: URL;\n\n try {\n url = new URL(endpoint);\n } catch {\n throw preconditionError(\n `The login configuration contains a malformed endpoint.`,\n `Report this to the operator of the Ekanos deployment.`,\n );\n }\n\n if (\n url.protocol !== 'https:' ||\n url.host.toLowerCase() !== domain.toLowerCase()\n ) {\n throw preconditionError(\n `The login configuration points at an endpoint outside its own Auth0 ` +\n `domain (${url.host}).`,\n `Do not continue. Report this to the operator of the Ekanos ` +\n `deployment — it may be misconfigured or compromised.`,\n );\n }\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
package/dist/auth/session.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CliContext } from '../context.js';
|
|
2
2
|
import { CredentialStore, type StoredSession } from './credential-store.js';
|
|
3
|
-
import { type
|
|
3
|
+
import { type EkanosIdentity } from './ekanos-api.js';
|
|
4
4
|
/**
|
|
5
|
-
* Session plumbing shared by `login`, `logout` and `whoami`: where the
|
|
5
|
+
* Session plumbing shared by `login`, `logout` and `whoami`: where the Ekanos
|
|
6
6
|
* host comes from, where credentials live, and how a stale access token is
|
|
7
7
|
* silently replaced.
|
|
8
8
|
*/
|
|
@@ -11,7 +11,7 @@ export interface AuthEnvironment {
|
|
|
11
11
|
host: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Resolve the
|
|
14
|
+
* Resolve the Ekanos host, in this order:
|
|
15
15
|
*
|
|
16
16
|
* 1. the `--host` flag
|
|
17
17
|
* 2. the `EKANOS_HOST` environment variable
|
|
@@ -52,7 +52,7 @@ export declare function requireSession(store: CredentialStore, host: string): St
|
|
|
52
52
|
* and one retry is the whole recovery.
|
|
53
53
|
*/
|
|
54
54
|
export declare function resolveIdentity(env: AuthEnvironment, session: StoredSession): Promise<{
|
|
55
|
-
identity:
|
|
55
|
+
identity: EkanosIdentity;
|
|
56
56
|
session: StoredSession;
|
|
57
57
|
}>;
|
|
58
58
|
/**
|
|
@@ -67,7 +67,7 @@ export declare function resolveIdentity(env: AuthEnvironment, session: StoredSes
|
|
|
67
67
|
* discard it and lock the partner out for good over a transient upstream
|
|
68
68
|
* hiccup: one process, no concurrency needed.
|
|
69
69
|
*
|
|
70
|
-
* So `onRotatedToken` writes it immediately and `
|
|
70
|
+
* So `onRotatedToken` writes it immediately and `refreshEkanosSession` invokes
|
|
71
71
|
* that before it throws. The write keeps the OLD access token, which is
|
|
72
72
|
* correct: that token is merely expired, while the refresh token is current
|
|
73
73
|
* and is what the next attempt needs.
|
package/dist/auth/session.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { authRequiredError, usageError } from '../errors.js';
|
|
4
|
+
import { shortHost } from '../ui.js';
|
|
4
5
|
import { CredentialStore, normalizeHost, resolveEkanosHome, } from './credential-store.js';
|
|
5
|
-
import { fetchIdentity,
|
|
6
|
+
import { fetchIdentity, refreshEkanosSession, } from './ekanos-api.js';
|
|
6
7
|
/**
|
|
7
|
-
* Resolve the
|
|
8
|
+
* Resolve the Ekanos host, in this order:
|
|
8
9
|
*
|
|
9
10
|
* 1. the `--host` flag
|
|
10
11
|
* 2. the `EKANOS_HOST` environment variable
|
|
@@ -21,7 +22,8 @@ import { fetchIdentity, refreshFusionSession, } from './fusion-api.js';
|
|
|
21
22
|
* ambiguity, so it stays a usage error that LISTS them rather than a guess.
|
|
22
23
|
*/
|
|
23
24
|
export function resolveHost(ctx, flag, env, options = {}) {
|
|
24
|
-
var _a
|
|
25
|
+
var _a;
|
|
26
|
+
var _b;
|
|
25
27
|
const raw = flag !== null && flag !== void 0 ? flag : env.EKANOS_HOST;
|
|
26
28
|
if (raw && raw.trim().length > 0) {
|
|
27
29
|
return normalizeHost(raw.trim());
|
|
@@ -38,22 +40,30 @@ export function resolveHost(ctx, flag, env, options = {}) {
|
|
|
38
40
|
// host can change under a developer via an ordinary merge or an
|
|
39
41
|
// agent-applied patch — and this is where login sends the OAuth flow
|
|
40
42
|
// and publish sends the bearer token and archive.
|
|
41
|
-
ctx.log(
|
|
42
|
-
`
|
|
43
|
+
ctx.log(ctx.jsonMode
|
|
44
|
+
? `ekanos: using ${host} — the host saved in ekanos.json. ` +
|
|
45
|
+
`Pass --host to override for one command.`
|
|
46
|
+
: ctx.ui.dim(`Using ${shortHost(host)} from ekanos.json. ` +
|
|
47
|
+
`Pass --host to override for one command.`));
|
|
43
48
|
return host;
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
const stored = (_b = (_a = options.store) === null || _a === void 0 ? void 0 : _a.hosts()) !== null && _b !== void 0 ? _b : [];
|
|
47
52
|
if (stored.length === 1) {
|
|
48
53
|
const host = stored[0];
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
// A person at a terminal sees this host in the result block of every
|
|
55
|
+
// verb, and it is the only one they are signed in to, so naming it
|
|
56
|
+
// again is noise. JSON callers get no result block, so they keep it.
|
|
57
|
+
if (ctx.jsonMode) {
|
|
58
|
+
ctx.log(`ekanos: using ${host} — the only host you are logged in to. ` +
|
|
59
|
+
`Pass --host to target a different one.`);
|
|
60
|
+
}
|
|
51
61
|
return host;
|
|
52
62
|
}
|
|
53
|
-
throw usageError('No
|
|
63
|
+
throw usageError('No Ekanos host given.', stored.length > 1
|
|
54
64
|
? `You are logged in to ${stored.length} hosts (${stored.join(', ')}) — ` +
|
|
55
65
|
`pass "--host <url>" to choose one, or set EKANOS_HOST.`
|
|
56
|
-
: 'Pass "--host https://<your-
|
|
66
|
+
: 'Pass "--host https://<your-ekanos-host>" or set EKANOS_HOST. ' +
|
|
57
67
|
'"ekanos login --host <url>" stores the host, so later commands ' +
|
|
58
68
|
'can omit it.');
|
|
59
69
|
}
|
|
@@ -163,7 +173,7 @@ export async function resolveIdentity(env, session) {
|
|
|
163
173
|
* discard it and lock the partner out for good over a transient upstream
|
|
164
174
|
* hiccup: one process, no concurrency needed.
|
|
165
175
|
*
|
|
166
|
-
* So `onRotatedToken` writes it immediately and `
|
|
176
|
+
* So `onRotatedToken` writes it immediately and `refreshEkanosSession` invokes
|
|
167
177
|
* that before it throws. The write keeps the OLD access token, which is
|
|
168
178
|
* correct: that token is merely expired, while the refresh token is current
|
|
169
179
|
* and is what the next attempt needs.
|
|
@@ -172,7 +182,7 @@ export async function refreshStoredSession(env, session) {
|
|
|
172
182
|
const persistRotated = (rotated) => {
|
|
173
183
|
env.store.write(env.host, Object.assign(Object.assign({}, session), { auth0RefreshToken: rotated, updatedAt: new Date().toISOString() }));
|
|
174
184
|
};
|
|
175
|
-
const refreshed = await
|
|
185
|
+
const refreshed = await refreshEkanosSession(env.host, session.auth0RefreshToken, { onRotatedToken: persistRotated });
|
|
176
186
|
const next = {
|
|
177
187
|
auth0RefreshToken: refreshed.auth0RefreshToken,
|
|
178
188
|
accessToken: refreshed.accessToken,
|
package/dist/auth/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport {\n CredentialStore,\n type StoredSession,\n normalizeHost,\n resolveEkanosHome,\n} from './credential-store';\nimport {\n type FusionIdentity,\n fetchIdentity,\n refreshFusionSession,\n} from './fusion-api';\n\n/**\n * Session plumbing shared by `login`, `logout` and `whoami`: where the Fusion\n * host comes from, where credentials live, and how a stale access token is\n * silently replaced.\n */\n\nexport interface AuthEnvironment {\n store: CredentialStore;\n host: string;\n}\n\n/**\n * Resolve the Fusion host, in this order:\n *\n * 1. the `--host` flag\n * 2. the `EKANOS_HOST` environment variable\n * 3. the `host` field of the project's `ekanos.json` (written by the first\n * successful `ekanos publish`, or by hand)\n * 4. the credential store, when it holds EXACTLY one host — a partner logged\n * in to a single deployment should not have to repeat its URL on every\n * verb, and one stored host is unambiguous\n *\n * Deliberately no default beyond those. A baked-in default would be wrong for\n * every partner running against their own sandbox, and — worse — a CLI that\n * silently picks a host is a CLI that can send a developer's credentials\n * somewhere they did not intend. Two or more stored hosts is exactly that\n * ambiguity, so it stays a usage error that LISTS them rather than a guess.\n */\nexport function resolveHost(\n ctx: CliContext,\n flag: string | undefined,\n env: Record<string, string | undefined>,\n options: {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n /** The credential store, for the sole-stored-host fallback. */\n store?: CredentialStore;\n } = {},\n): string {\n const raw = flag ?? env.EKANOS_HOST;\n\n if (raw && raw.trim().length > 0) {\n return normalizeHost(raw.trim());\n }\n\n if (options.projectDir) {\n const declared = readProjectHost(options.projectDir);\n\n if (declared) {\n // normalizeHost throws a precondition error for a malformed value —\n // an EXPLICIT host in ekanos.json that is wrong deserves an error\n // naming it, never a silent fall-through to a different host.\n const host = normalizeHost(declared);\n\n // Announce the choice, like the sole-stored-host branch below. This\n // source deserves it MOST: ekanos.json is a committed file, so its\n // host can change under a developer via an ordinary merge or an\n // agent-applied patch — and this is where login sends the OAuth flow\n // and publish sends the bearer token and archive.\n ctx.log(\n `ekanos: using ${host} — the host saved in ekanos.json. ` +\n `Pass --host to override for one command.`,\n );\n\n return host;\n }\n }\n\n const stored = options.store?.hosts() ?? [];\n\n if (stored.length === 1) {\n const host = stored[0]!;\n\n ctx.log(\n `ekanos: using ${host} — the only host you are logged in to. ` +\n `Pass --host to target a different one.`,\n );\n\n return host;\n }\n\n throw usageError(\n 'No Fusion host given.',\n stored.length > 1\n ? `You are logged in to ${stored.length} hosts (${stored.join(', ')}) — ` +\n `pass \"--host <url>\" to choose one, or set EKANOS_HOST.`\n : 'Pass \"--host https://<your-fusion-host>\" or set EKANOS_HOST. ' +\n '\"ekanos login --host <url>\" stores the host, so later commands ' +\n 'can omit it.',\n );\n}\n\n/**\n * Best-effort read of the `host` field from a directory's ekanos.json.\n *\n * Lenient on purpose: host resolution runs for verbs like `login` that are\n * routinely run OUTSIDE a project, so a missing, unparseable, or host-less\n * ekanos.json is simply \"no answer here\", never an error — the verbs that\n * require a valid project (`publish`, `validate`) load it strictly themselves.\n */\nfunction readProjectHost(projectDir: string): string | null {\n const configPath = path.join(projectDir, 'ekanos.json');\n\n let raw: string;\n try {\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const host =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { host?: unknown }).host\n : undefined;\n\n return typeof host === 'string' && host.trim().length > 0\n ? host.trim()\n : null;\n}\n\nexport interface ResolveAuthOptions {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n}\n\nexport function resolveAuthEnvironment(\n ctx: CliContext,\n hostFlag: string | undefined,\n env: Record<string, string | undefined>,\n options: ResolveAuthOptions = {},\n): AuthEnvironment {\n const store = newStore(ctx, env);\n const host = resolveHost(ctx, hostFlag, env, {\n projectDir: options.projectDir,\n store,\n });\n\n warnOnPlaintextHost(ctx, host);\n\n return { host, store };\n}\n\n/** A store with no host attached — for `logout --all`. */\nexport function newStore(\n ctx: CliContext,\n env: Record<string, string | undefined>,\n): CredentialStore {\n return new CredentialStore({\n home: resolveEkanosHome(env),\n // Permission warnings are diagnostics, not results: stderr, always.\n warn: (message) => ctx.warn(message),\n });\n}\n\n/**\n * `http://` is allowed because local development needs it, but sending a\n * bearer token to a plaintext REMOTE host puts it on the wire in the clear.\n * Warn rather than refuse: refusing would break `--host http://localhost:3000`\n * for anyone whose loopback resolves to something other than the names below.\n */\nfunction warnOnPlaintextHost(ctx: CliContext, host: string): void {\n const url = new URL(host);\n\n if (url.protocol !== 'http:') return;\n\n const loopback = ['localhost', '127.0.0.1', '[::1]', '0.0.0.0'];\n\n if (loopback.includes(url.hostname) || url.hostname.endsWith('.localhost')) {\n return;\n }\n\n ctx.warn(\n `ekanos: ${host} is plaintext HTTP. Your credentials will cross the ` +\n `network unencrypted. Use https:// unless this is a local host.`,\n );\n}\n\nexport function requireSession(\n store: CredentialStore,\n host: string,\n): StoredSession {\n const session = store.read(host);\n\n if (!session) {\n throw authRequiredError(\n `Not logged in to ${host}.`,\n `Run \"ekanos login --host ${host}\".`,\n );\n }\n\n return session;\n}\n\n/**\n * Resolve the caller's identity, refreshing the access token once if the\n * stored one no longer authenticates.\n *\n * Validity is decided by the SERVER, not by comparing `expiresAt` to the local\n * clock. A container with a skewed clock — the exact environment this flow\n * exists to support — would otherwise either refuse a perfectly good token or\n * keep presenting a dead one. A 401 is the authoritative answer; one refresh\n * and one retry is the whole recovery.\n */\nexport async function resolveIdentity(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<{ identity: FusionIdentity; session: StoredSession }> {\n const identity = await fetchIdentity(env.host, session.accessToken);\n\n if (identity) {\n return { identity, session };\n }\n\n const refreshed = await refreshStoredSession(env, session);\n const retried = await fetchIdentity(env.host, refreshed.accessToken);\n\n if (!retried) {\n throw authRequiredError(\n `The session for ${env.host} could not be renewed.`,\n `Run \"ekanos logout --host ${env.host}\" then \"ekanos login --host ${env.host}\".`,\n );\n }\n\n return { identity: retried, session: refreshed };\n}\n\n/**\n * Refresh and persist.\n *\n * The rotated refresh token is written the MOMENT the server reports it, not\n * when the refresh finishes. Auth0 rotates as soon as the server's exchange\n * succeeds, so from that instant the presented token is dead and the\n * replacement is the only copy in existence — and `cli-refresh` can still fail\n * afterwards (a `/userinfo` blip, a missing account, an unexpected throw) and\n * return that replacement on an ERROR response. Saving only on success would\n * discard it and lock the partner out for good over a transient upstream\n * hiccup: one process, no concurrency needed.\n *\n * So `onRotatedToken` writes it immediately and `refreshFusionSession` invokes\n * that before it throws. The write keeps the OLD access token, which is\n * correct: that token is merely expired, while the refresh token is current\n * and is what the next attempt needs.\n */\nexport async function refreshStoredSession(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<StoredSession> {\n const persistRotated = (rotated: string) => {\n env.store.write(env.host, {\n ...session,\n auth0RefreshToken: rotated,\n updatedAt: new Date().toISOString(),\n });\n };\n\n const refreshed = await refreshFusionSession(\n env.host,\n session.auth0RefreshToken,\n { onRotatedToken: persistRotated },\n );\n\n const next: StoredSession = {\n auth0RefreshToken: refreshed.auth0RefreshToken,\n accessToken: refreshed.accessToken,\n expiresAt: refreshed.expiresAt,\n userId: refreshed.userId,\n email: refreshed.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, next);\n\n return next;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/auth/session.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport { shortHost } from '../ui';\nimport {\n CredentialStore,\n type StoredSession,\n normalizeHost,\n resolveEkanosHome,\n} from './credential-store';\nimport {\n type EkanosIdentity,\n fetchIdentity,\n refreshEkanosSession,\n} from './ekanos-api';\n\n/**\n * Session plumbing shared by `login`, `logout` and `whoami`: where the Ekanos\n * host comes from, where credentials live, and how a stale access token is\n * silently replaced.\n */\n\nexport interface AuthEnvironment {\n store: CredentialStore;\n host: string;\n}\n\n/**\n * Resolve the Ekanos host, in this order:\n *\n * 1. the `--host` flag\n * 2. the `EKANOS_HOST` environment variable\n * 3. the `host` field of the project's `ekanos.json` (written by the first\n * successful `ekanos publish`, or by hand)\n * 4. the credential store, when it holds EXACTLY one host — a partner logged\n * in to a single deployment should not have to repeat its URL on every\n * verb, and one stored host is unambiguous\n *\n * Deliberately no default beyond those. A baked-in default would be wrong for\n * every partner running against their own sandbox, and — worse — a CLI that\n * silently picks a host is a CLI that can send a developer's credentials\n * somewhere they did not intend. Two or more stored hosts is exactly that\n * ambiguity, so it stays a usage error that LISTS them rather than a guess.\n */\nexport function resolveHost(\n ctx: CliContext,\n flag: string | undefined,\n env: Record<string, string | undefined>,\n options: {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n /** The credential store, for the sole-stored-host fallback. */\n store?: CredentialStore;\n } = {},\n): string {\n const raw = flag ?? env.EKANOS_HOST;\n\n if (raw && raw.trim().length > 0) {\n return normalizeHost(raw.trim());\n }\n\n if (options.projectDir) {\n const declared = readProjectHost(options.projectDir);\n\n if (declared) {\n // normalizeHost throws a precondition error for a malformed value —\n // an EXPLICIT host in ekanos.json that is wrong deserves an error\n // naming it, never a silent fall-through to a different host.\n const host = normalizeHost(declared);\n\n // Announce the choice, like the sole-stored-host branch below. This\n // source deserves it MOST: ekanos.json is a committed file, so its\n // host can change under a developer via an ordinary merge or an\n // agent-applied patch — and this is where login sends the OAuth flow\n // and publish sends the bearer token and archive.\n ctx.log(\n ctx.jsonMode\n ? `ekanos: using ${host} — the host saved in ekanos.json. ` +\n `Pass --host to override for one command.`\n : ctx.ui.dim(\n `Using ${shortHost(host)} from ekanos.json. ` +\n `Pass --host to override for one command.`,\n ),\n );\n\n return host;\n }\n }\n\n const stored = options.store?.hosts() ?? [];\n\n if (stored.length === 1) {\n const host = stored[0]!;\n\n // A person at a terminal sees this host in the result block of every\n // verb, and it is the only one they are signed in to, so naming it\n // again is noise. JSON callers get no result block, so they keep it.\n if (ctx.jsonMode) {\n ctx.log(\n `ekanos: using ${host} — the only host you are logged in to. ` +\n `Pass --host to target a different one.`,\n );\n }\n\n return host;\n }\n\n throw usageError(\n 'No Ekanos host given.',\n stored.length > 1\n ? `You are logged in to ${stored.length} hosts (${stored.join(', ')}) — ` +\n `pass \"--host <url>\" to choose one, or set EKANOS_HOST.`\n : 'Pass \"--host https://<your-ekanos-host>\" or set EKANOS_HOST. ' +\n '\"ekanos login --host <url>\" stores the host, so later commands ' +\n 'can omit it.',\n );\n}\n\n/**\n * Best-effort read of the `host` field from a directory's ekanos.json.\n *\n * Lenient on purpose: host resolution runs for verbs like `login` that are\n * routinely run OUTSIDE a project, so a missing, unparseable, or host-less\n * ekanos.json is simply \"no answer here\", never an error — the verbs that\n * require a valid project (`publish`, `validate`) load it strictly themselves.\n */\nfunction readProjectHost(projectDir: string): string | null {\n const configPath = path.join(projectDir, 'ekanos.json');\n\n let raw: string;\n try {\n raw = fs.readFileSync(configPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n const host =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { host?: unknown }).host\n : undefined;\n\n return typeof host === 'string' && host.trim().length > 0\n ? host.trim()\n : null;\n}\n\nexport interface ResolveAuthOptions {\n /** Directory whose ekanos.json may carry a `host` field. */\n projectDir?: string;\n}\n\nexport function resolveAuthEnvironment(\n ctx: CliContext,\n hostFlag: string | undefined,\n env: Record<string, string | undefined>,\n options: ResolveAuthOptions = {},\n): AuthEnvironment {\n const store = newStore(ctx, env);\n const host = resolveHost(ctx, hostFlag, env, {\n projectDir: options.projectDir,\n store,\n });\n\n warnOnPlaintextHost(ctx, host);\n\n return { host, store };\n}\n\n/** A store with no host attached — for `logout --all`. */\nexport function newStore(\n ctx: CliContext,\n env: Record<string, string | undefined>,\n): CredentialStore {\n return new CredentialStore({\n home: resolveEkanosHome(env),\n // Permission warnings are diagnostics, not results: stderr, always.\n warn: (message) => ctx.warn(message),\n });\n}\n\n/**\n * `http://` is allowed because local development needs it, but sending a\n * bearer token to a plaintext REMOTE host puts it on the wire in the clear.\n * Warn rather than refuse: refusing would break `--host http://localhost:3000`\n * for anyone whose loopback resolves to something other than the names below.\n */\nfunction warnOnPlaintextHost(ctx: CliContext, host: string): void {\n const url = new URL(host);\n\n if (url.protocol !== 'http:') return;\n\n const loopback = ['localhost', '127.0.0.1', '[::1]', '0.0.0.0'];\n\n if (loopback.includes(url.hostname) || url.hostname.endsWith('.localhost')) {\n return;\n }\n\n ctx.warn(\n `ekanos: ${host} is plaintext HTTP. Your credentials will cross the ` +\n `network unencrypted. Use https:// unless this is a local host.`,\n );\n}\n\nexport function requireSession(\n store: CredentialStore,\n host: string,\n): StoredSession {\n const session = store.read(host);\n\n if (!session) {\n throw authRequiredError(\n `Not logged in to ${host}.`,\n `Run \"ekanos login --host ${host}\".`,\n );\n }\n\n return session;\n}\n\n/**\n * Resolve the caller's identity, refreshing the access token once if the\n * stored one no longer authenticates.\n *\n * Validity is decided by the SERVER, not by comparing `expiresAt` to the local\n * clock. A container with a skewed clock — the exact environment this flow\n * exists to support — would otherwise either refuse a perfectly good token or\n * keep presenting a dead one. A 401 is the authoritative answer; one refresh\n * and one retry is the whole recovery.\n */\nexport async function resolveIdentity(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<{ identity: EkanosIdentity; session: StoredSession }> {\n const identity = await fetchIdentity(env.host, session.accessToken);\n\n if (identity) {\n return { identity, session };\n }\n\n const refreshed = await refreshStoredSession(env, session);\n const retried = await fetchIdentity(env.host, refreshed.accessToken);\n\n if (!retried) {\n throw authRequiredError(\n `The session for ${env.host} could not be renewed.`,\n `Run \"ekanos logout --host ${env.host}\" then \"ekanos login --host ${env.host}\".`,\n );\n }\n\n return { identity: retried, session: refreshed };\n}\n\n/**\n * Refresh and persist.\n *\n * The rotated refresh token is written the MOMENT the server reports it, not\n * when the refresh finishes. Auth0 rotates as soon as the server's exchange\n * succeeds, so from that instant the presented token is dead and the\n * replacement is the only copy in existence — and `cli-refresh` can still fail\n * afterwards (a `/userinfo` blip, a missing account, an unexpected throw) and\n * return that replacement on an ERROR response. Saving only on success would\n * discard it and lock the partner out for good over a transient upstream\n * hiccup: one process, no concurrency needed.\n *\n * So `onRotatedToken` writes it immediately and `refreshEkanosSession` invokes\n * that before it throws. The write keeps the OLD access token, which is\n * correct: that token is merely expired, while the refresh token is current\n * and is what the next attempt needs.\n */\nexport async function refreshStoredSession(\n env: AuthEnvironment,\n session: StoredSession,\n): Promise<StoredSession> {\n const persistRotated = (rotated: string) => {\n env.store.write(env.host, {\n ...session,\n auth0RefreshToken: rotated,\n updatedAt: new Date().toISOString(),\n });\n };\n\n const refreshed = await refreshEkanosSession(\n env.host,\n session.auth0RefreshToken,\n { onRotatedToken: persistRotated },\n );\n\n const next: StoredSession = {\n auth0RefreshToken: refreshed.auth0RefreshToken,\n accessToken: refreshed.accessToken,\n expiresAt: refreshed.expiresAt,\n userId: refreshed.userId,\n email: refreshed.email,\n updatedAt: new Date().toISOString(),\n };\n\n env.store.write(env.host, next);\n\n return next;\n}\n"]}
|
package/dist/banner.d.ts
CHANGED
|
@@ -22,4 +22,4 @@ export declare function paintBanner(lines: readonly string[], color: boolean): s
|
|
|
22
22
|
*
|
|
23
23
|
* Call sites stay a single line: all gating lives here, not at the call site.
|
|
24
24
|
*/
|
|
25
|
-
export declare function printBanner(ctx: CliContext
|
|
25
|
+
export declare function printBanner(ctx: CliContext): void;
|
package/dist/banner.js
CHANGED
|
@@ -5,7 +5,7 @@ const GRADIENT_STOPS = [
|
|
|
5
5
|
[0x9f, 0x70, 0x77],
|
|
6
6
|
[0xed, 0x1c, 0x24],
|
|
7
7
|
];
|
|
8
|
-
/** Option C ("colossal") — the
|
|
8
|
+
/** Option C ("colossal") — the banner, shown on every init and login. */
|
|
9
9
|
const FULL_ART = [
|
|
10
10
|
' .d88b. 888 888 8888b. 88888b. .d88b. .d8888b ',
|
|
11
11
|
'd8P Y8b 888 .88P "88b 888 "88b d88""88b 88K ',
|
|
@@ -13,12 +13,6 @@ const FULL_ART = [
|
|
|
13
13
|
'Y8b. 888 "88b 888 888 888 888 Y88..88P X88 ',
|
|
14
14
|
' "Y8888 888 888 "Y888888 888 888 "Y88P" 88888P\' ',
|
|
15
15
|
];
|
|
16
|
-
/** Option D ("pagga") — the mini-mark, shown on a repeat login. */
|
|
17
|
-
const MINI_ART = [
|
|
18
|
-
'░█▀▀░█░█░█▀█░█▀█░█▀█░█▀▀',
|
|
19
|
-
'░█▀▀░█▀▄░█▀█░█░█░█░█░▀▀█',
|
|
20
|
-
'░▀▀▀░▀░▀░▀░▀░▀░▀░▀▀▀░▀▀▀',
|
|
21
|
-
];
|
|
22
16
|
const TAGLINE = 'The SDK with a skill-full DX';
|
|
23
17
|
function lerp(a, b, t) {
|
|
24
18
|
return Math.round(a + (b - a) * t);
|
|
@@ -77,7 +71,7 @@ function dim(text, color) {
|
|
|
77
71
|
*
|
|
78
72
|
* Call sites stay a single line: all gating lives here, not at the call site.
|
|
79
73
|
*/
|
|
80
|
-
export function printBanner(ctx
|
|
74
|
+
export function printBanner(ctx) {
|
|
81
75
|
if (ctx.jsonMode)
|
|
82
76
|
return;
|
|
83
77
|
if (!ctx.stderrIsTTY)
|
|
@@ -85,10 +79,7 @@ export function printBanner(ctx, kind) {
|
|
|
85
79
|
if (ctx.env.EKANOS_NO_BANNER !== undefined)
|
|
86
80
|
return;
|
|
87
81
|
const color = colorsEnabled({ isTTY: ctx.stderrIsTTY, env: ctx.env });
|
|
88
|
-
|
|
89
|
-
ctx.log(
|
|
90
|
-
if (kind === 'full') {
|
|
91
|
-
ctx.log(dim(TAGLINE, color));
|
|
92
|
-
}
|
|
82
|
+
ctx.log(paintBanner(FULL_ART, color));
|
|
83
|
+
ctx.log(dim(TAGLINE, color));
|
|
93
84
|
}
|
|
94
85
|
//# sourceMappingURL=banner.js.map
|
package/dist/banner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,MAAM,CAAC;AAerC,uEAAuE;AACvE,MAAM,cAAc,GAA6B;IAC/C,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;IAClB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;IAClB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC;CACnB,CAAC;AAEF,
|
|
1
|
+
{"version":3,"file":"banner.js","sourceRoot":"","sources":["../src/banner.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CliContext } from './context';\nimport { colorsEnabled } from './ui';\n\n/**\n * The gradient ASCII banner shown on `ekanos init` and `ekanos login`.\n *\n * Every gate here matters more than the art: this is decoration on stderr,\n * and it must never leak into an agent's view of the CLI. In particular it\n * must NEVER print in JSON mode (stdout carries exactly one JSON object for\n * the whole process, and this is stderr noise an agent has no reason to\n * parse) and it must never print when stderr is not a TTY (piped into a log\n * file, captured by a harness, redirected in CI).\n */\n\ntype RGB = readonly [number, number, number];\n\n/** The website's brand gradient: teal → mauve → red, left to right. */\nconst GRADIENT_STOPS: readonly [RGB, RGB, RGB] = [\n [0x70, 0xa5, 0xab],\n [0x9f, 0x70, 0x77],\n [0xed, 0x1c, 0x24],\n];\n\n/** Option C (\"colossal\") — the banner, shown on every init and login. */\nconst FULL_ART: readonly string[] = [\n ' .d88b. 888 888 8888b. 88888b. .d88b. .d8888b ',\n 'd8P Y8b 888 .88P \"88b 888 \"88b d88\"\"88b 88K ',\n '88888888 888888K .d888888 888 888 888 888 \"Y8888b. ',\n 'Y8b. 888 \"88b 888 888 888 888 Y88..88P X88 ',\n ' \"Y8888 888 888 \"Y888888 888 888 \"Y88P\" 88888P\\' ',\n];\n\nconst TAGLINE = 'The SDK with a skill-full DX';\n\nfunction lerp(a: number, b: number, t: number): number {\n return Math.round(a + (b - a) * t);\n}\n\n/** The gradient color at position `t` (0..1) along the three-stop ramp. */\nfunction colorAt(t: number): RGB {\n const [start, mid, end] = GRADIENT_STOPS;\n const [from, to, localT]: [RGB, RGB, number] =\n t < 0.5 ? [start, mid, t * 2] : [mid, end, (t - 0.5) * 2];\n\n return [\n lerp(from[0], to[0], localT),\n lerp(from[1], to[1], localT),\n lerp(from[2], to[2], localT),\n ];\n}\n\n/**\n * Paint ASCII art lines with the gradient, one color per column, left to\n * right. `color: false` renders the raw art with zero escape codes — the\n * `NO_COLOR` and non-TTY paths. Pure and exported so the gradient math is\n * unit-testable independent of the printing gate in `printBanner`.\n */\nexport function paintBanner(lines: readonly string[], color: boolean): string {\n const width = Math.max(...lines.map((line) => line.length));\n\n return lines\n .map((line) => {\n const padded = line.padEnd(width);\n\n if (!color) return padded;\n\n const painted = [...padded]\n .map((ch, x) => {\n if (ch === ' ') return ch;\n const [r, g, b] = colorAt(width <= 1 ? 0 : x / (width - 1));\n return `\\x1b[38;2;${r};${g};${b}m${ch}`;\n })\n .join('');\n\n return `${painted}\\x1b[0m`;\n })\n .join('\\n');\n}\n\nfunction dim(text: string, color: boolean): string {\n return color ? `\\x1b[2m${text}\\x1b[0m` : text;\n}\n\n/**\n * Print the gradient banner to stderr, gated on ALL of:\n *\n * - human mode (never in JSON mode — an agent must never see this)\n * - stderr being a TTY (never when piped, redirected, or captured)\n * - `EKANOS_NO_BANNER` unset (any value at all suppresses it entirely)\n *\n * `NO_COLOR` does not suppress the banner — it renders the same art with zero\n * escape codes, per the standard (https://no-color.org). The actual on/off\n * call is `ui.ts`'s `colorsEnabled` — the one gate every color decision in\n * this package flows through — so the banner also honours `TERM=dumb` and\n * `FORCE_COLOR` the same way the findings renderer and everything else does,\n * without duplicating the rule here.\n *\n * Call sites stay a single line: all gating lives here, not at the call site.\n */\nexport function printBanner(ctx: CliContext): void {\n if (ctx.jsonMode) return;\n if (!ctx.stderrIsTTY) return;\n if (ctx.env.EKANOS_NO_BANNER !== undefined) return;\n\n const color = colorsEnabled({ isTTY: ctx.stderrIsTTY, env: ctx.env });\n ctx.log(paintBanner(FULL_ART, color));\n ctx.log(dim(TAGLINE, color));\n}\n"]}
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAE9B;;;;;;;GAOG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE9D,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;;IACxB,2EAA2E;IAC3E,oEAAoE;IACpE,uCAAuC;IACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAE9B;;;;;;;GAOG;AACH,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAE9D,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;QAC3E,OAAO,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;;IACxB,2EAA2E;IAC3E,oEAAoE;IACpE,uCAAuC;IACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCACE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,OAAC,KAAK,CAAC,KAAK,mCAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACxE,IAAI,CACL,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { announceDelegation, delegate, planDelegation } from './delegate';\nimport { run } from './index';\n\n/**\n * The bin entry — the ONLY place that calls process.exit, so `run()` stays a\n * pure, testable function that returns an exit code. Argv is sliced to drop\n * the node executable and this script path.\n *\n * Before dispatching, check whether this invocation should delegate to the\n * current project's OWN pinned `@ekanos/cli` — see `delegate.ts` for why.\n */\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n const plan = planDelegation(argv, process.env, process.cwd());\n\n if (plan.shouldDelegate) {\n announceDelegation(plan, argv, process.env, process.stdout.isTTY === true);\n return delegate(plan, argv, process.env);\n }\n\n return run(argv);\n}\n\nmain()\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n // run() catches its own errors and emits the envelope; reaching here means\n // the envelope machinery itself threw. Fail loudly on stderr (never\n // stdout) with the internal exit code.\n process.stderr.write(\n `ekanos: fatal internal error: ${\n error instanceof Error ? (error.stack ?? error.message) : String(error)\n }\\n`,\n );\n process.exitCode = 1;\n });\n"]}
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ export interface DevArgs {
|
|
|
8
8
|
* Interface to bind. Defaults to loopback — see `resolveHost`. Anything
|
|
9
9
|
* else is an explicit, warned-about decision to expose the harness.
|
|
10
10
|
*
|
|
11
|
-
* NOTE: unrelated to the
|
|
12
|
-
* this is the local bind address for `next dev`, never
|
|
11
|
+
* NOTE: unrelated to the Ekanos HOST the update notice checks against —
|
|
12
|
+
* this is the local bind address for `next dev`, never an Ekanos origin.
|
|
13
13
|
*/
|
|
14
14
|
host?: string;
|
|
15
15
|
/** Rewrite every generated file even when its bytes already match. */
|
package/dist/commands/dev.js
CHANGED
|
@@ -5,6 +5,7 @@ import { preconditionError, usageError } from '../errors.js';
|
|
|
5
5
|
import { HARNESS_DIR, HARNESS_PACKAGE, assertShellDirIsOurs, bootstrapHarnessConfig, ensureGitignoreEntry, missingDependencies, readInstalledHarness, scaffoldShell, } from '../harness-scaffold.js';
|
|
6
6
|
import { detectPackageManager, execArgs, formatCommand, installCommand, } from '../package-manager.js';
|
|
7
7
|
import { loadProject } from '../project.js';
|
|
8
|
+
import { displayPath } from '../ui.js';
|
|
8
9
|
import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
9
10
|
const DEFAULT_PORT = 3100;
|
|
10
11
|
/**
|
|
@@ -133,9 +134,16 @@ export async function runDev(ctx, args) {
|
|
|
133
134
|
// running it. `--start` overrides; `--no-start` opts out in a terminal.
|
|
134
135
|
const shouldStart = (_c = args.start) !== null && _c !== void 0 ? _c : !ctx.jsonMode;
|
|
135
136
|
if (!shouldStart) {
|
|
136
|
-
return ctx.succeed(Object.assign(Object.assign({}, scaffold), { server: { started: false, command, url, port, host, exposed } }),
|
|
137
|
+
return ctx.succeed(Object.assign(Object.assign({}, scaffold), { server: { started: false, command, url, port, host, exposed } }), ctx.ui.result({
|
|
138
|
+
title: 'Harness ready',
|
|
139
|
+
rows: [
|
|
140
|
+
['Directory', displayPath(shell.harnessDir, process.cwd())],
|
|
141
|
+
['URL', url],
|
|
142
|
+
['Start', command],
|
|
143
|
+
],
|
|
144
|
+
}));
|
|
137
145
|
}
|
|
138
|
-
ctx.log(`Starting the harness on ${url} (${command})…`);
|
|
146
|
+
ctx.log(ctx.ui.dim(`Starting the harness on ${url} (${command})…`));
|
|
139
147
|
const exitCode = await spawnServer({
|
|
140
148
|
ctx,
|
|
141
149
|
bin: pm.bin,
|
|
@@ -145,7 +153,7 @@ export async function runDev(ctx, args) {
|
|
|
145
153
|
});
|
|
146
154
|
const data = Object.assign(Object.assign({}, scaffold), { server: { started: true, command, url, port, host, exposed, exitCode } });
|
|
147
155
|
if (exitCode === 0) {
|
|
148
|
-
return ctx.succeed(data,
|
|
156
|
+
return ctx.succeed(data, ctx.ui.result({ title: 'Harness stopped', rows: [['URL', url]] }));
|
|
149
157
|
}
|
|
150
158
|
return ctx.failWith({
|
|
151
159
|
code: 'dev_server_failed',
|
package/dist/commands/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Fusion HOST the update notice checks against —\n * this is the local bind address for `next dev`, never a Fusion origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * Set for the dev server when `--host` binds beyond loopback. The shell's\n * API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers\n * unless this is `1`: they hold the partner's local OAuth credentials and\n * run handlers on request, so exposing them has to be a decision — this one,\n * made after the warning below — rather than a side effect of the bind.\n */\nconst EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';\n\n/** Where the harness generates, and reads, local dev credentials. */\nconst LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n ctx.log(\n ` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +\n 'use — fill it in and follow \"Local OAuth\" on the Activation surface.',\n );\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const serverEnv: Record<string, string> = exposed\n ? { [EXPOSED_ENV]: '1' }\n : {};\n const command =\n (exposed ? `${EXPOSED_ENV}=1 ` : '') +\n formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Its API routes hold the ' +\n `client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +\n 'and run your handlers on request; they refuse non-loopback callers ' +\n `unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +\n `${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n `dev: harness ready in ${shell.harnessDir}. Start it with: ${command}`,\n );\n }\n\n ctx.log(`Starting the harness on ${url} (${command})…`);\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n env: serverEnv,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(data, `dev: harness stopped cleanly.`);\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n env: Record<string, string>;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n env: { ...process.env, ...params.env },\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dev.js","sourceRoot":"","sources":["../../src/commands/dev.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process';\nimport * as path from 'node:path';\n\nimport { assertHarnessCompatible } from '../compatibility';\nimport type { CliContext } from '../context';\nimport { preconditionError, usageError } from '../errors';\nimport {\n HARNESS_DIR,\n HARNESS_PACKAGE,\n assertShellDirIsOurs,\n bootstrapHarnessConfig,\n ensureGitignoreEntry,\n missingDependencies,\n readInstalledHarness,\n scaffoldShell,\n} from '../harness-scaffold';\nimport {\n detectPackageManager,\n execArgs,\n formatCommand,\n installCommand,\n} from '../package-manager';\nimport { loadProject } from '../project';\nimport { displayPath } from '../ui';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\n\nexport interface DevArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n /** Port for the dev server. */\n port?: string;\n /**\n * Interface to bind. Defaults to loopback — see `resolveHost`. Anything\n * else is an explicit, warned-about decision to expose the harness.\n *\n * NOTE: unrelated to the Ekanos HOST the update notice checks against —\n * this is the local bind address for `next dev`, never an Ekanos origin.\n */\n host?: string;\n /** Rewrite every generated file even when its bytes already match. */\n force: boolean;\n /**\n * Explicit `--start` / `--no-start`. Undefined means \"decide from the\n * output mode\": a JSON caller is an agent or a pipe and must not be handed\n * a process that never exits.\n */\n start?: boolean;\n env?: Record<string, string | undefined>;\n}\n\nconst DEFAULT_PORT = 3100;\n\n/**\n * Loopback, NOT Next's default.\n *\n * `next dev` binds 0.0.0.0 unless told otherwise, so the harness would be\n * reachable by anyone on the same network — a coffee-shop Wi-Fi, a shared\n * office VLAN — while we printed \"http://localhost:3100\" and implied\n * otherwise. It renders a partner's integration with their fixtures and, in\n * live mode, whatever their vendor API returns; there is no auth in front of\n * any of it. Binding loopback by default costs a partner nothing and makes\n * exposure a thing they ask for.\n */\nconst DEFAULT_HOST = '127.0.0.1';\n\n/** Addresses that are only reachable from this machine. */\nconst LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);\n\n/**\n * Set for the dev server when `--host` binds beyond loopback. The shell's\n * API routes (`@ekanos/harness/api`) refuse non-loopback `Host` headers\n * unless this is `1`: they hold the partner's local OAuth credentials and\n * run handlers on request, so exposing them has to be a decision — this one,\n * made after the warning below — rather than a side effect of the bind.\n */\nconst EXPOSED_ENV = 'EKANOS_HARNESS_EXPOSED';\n\n/** Where the harness generates, and reads, local dev credentials. */\nconst LOCAL_SECRETS_PATH = path.join('.ekanos', 'local-secrets.json');\n\n/**\n * `dev` — scaffold the local harness shell into the partner's project, then\n * run it with their own Next.\n *\n * The shape of this verb follows from one rule: we must be able to ship a\n * harness bugfix as an `@ekanos/harness` version bump that a partner picks up\n * with `pnpm up`, with zero edits to any file they own. So everything under\n * `.ekanos/` is generated, gitignored and rewritten freely, `harness.config.ts`\n * at the project root is written once and never touched again, and all actual\n * harness behaviour lives in the installed package.\n *\n * Two idempotency properties matter and are both tested: a second run produces\n * byte-identical files and rewrites nothing (so Next's watcher stays quiet),\n * and `.gitignore` never grows a duplicate line.\n */\nexport async function runDev(ctx: CliContext, args: DevArgs): Promise<number> {\n const loaded = loadProject(args.dir);\n const port = parsePort(args.port);\n const host = parseHost(args.host);\n const exposed = !LOOPBACK_HOSTS.has(host);\n const pm = detectPackageManager(loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n // Fail before writing anything: the shell declares no dependencies of its\n // own, so a missing one would otherwise surface as an opaque Next\n // resolution error minutes later.\n const missing = missingDependencies(loaded.projectDir);\n if (missing.length > 0) {\n throw preconditionError(\n `${missing.length} package${missing.length === 1 ? '' : 's'} the ` +\n `harness needs ${missing.length === 1 ? 'is' : 'are'} not installed ` +\n `in ${loaded.projectDir}: ${missing.join(', ')}.`,\n `Run \"${installCommand(pm, missing)}\" in ${loaded.projectDir}, then ` +\n 're-run \"ekanos dev\".',\n );\n }\n\n const harness = readInstalledHarness(loaded.projectDir);\n const harnessVersion = harness?.version;\n if (!harnessVersion) {\n throw preconditionError(\n `${HARNESS_PACKAGE} is installed but declares no version.`,\n `Reinstall it with \"${installCommand(pm, [HARNESS_PACKAGE])}\" in ` +\n `${loaded.projectDir}, then re-run \"ekanos dev\".`,\n );\n }\n\n // The templates ship with the CLI and the code they import ships with the\n // harness, so a partner can upgrade one without the other. Name that\n // mismatch here rather than letting it surface later as a module-resolution\n // error inside a shell we cheerfully certified as current.\n assertHarnessCompatible({\n harnessVersion,\n harnessManifest: harness?.manifest,\n installHint: (spec) => installCommand(pm, [spec]),\n });\n\n // Validate the shell target BEFORE writing anything at all. Bootstrapping\n // harness.config.ts first and only then discovering that .ekanos/harness is\n // a file or someone else's tree would leave the project half-scaffolded.\n assertShellDirIsOurs(path.join(loaded.projectDir, HARNESS_DIR), args.force);\n\n const config = bootstrapHarnessConfig(loaded.projectDir, loaded.integrations);\n const shell = scaffoldShell({\n projectDir: loaded.projectDir,\n slug: loaded.primary.slug,\n harnessVersion,\n force: args.force,\n sourceGlobs: loaded.project.sourceGlobs,\n });\n const gitignoreUpdated = ensureGitignoreEntry(loaded.projectDir);\n\n if (shell.stale) {\n ctx.log(\n `Regenerating ${HARNESS_DIR} — ${shell.staleReasons.join('; ')}. ` +\n `Nothing you own changed; ${config.configPath} is untouched.`,\n );\n }\n\n if (config.bootstrapped) {\n ctx.log(` created harness.config.ts (yours — edit it, we never will)`);\n }\n for (const file of shell.created) ctx.log(` created ${HARNESS_DIR}/${file}`);\n for (const file of shell.updated) ctx.log(` updated ${HARNESS_DIR}/${file}`);\n if (gitignoreUpdated) ctx.log(' updated .gitignore (.ekanos/)');\n ctx.log(\n ` live-mode OAuth: the harness generates ${LOCAL_SECRETS_PATH} on first ` +\n 'use — fill it in and follow \"Local OAuth\" on the Activation surface.',\n );\n\n // `--hostname` is passed ALWAYS, never left to Next's default, so the URL\n // we print is the URL that is actually served.\n const serverArgs = ['dev', '--port', String(port), '--hostname', host];\n const serverEnv: Record<string, string> = exposed\n ? { [EXPOSED_ENV]: '1' }\n : {};\n const command =\n (exposed ? `${EXPOSED_ENV}=1 ` : '') +\n formatCommand(pm.bin, execArgs(pm, 'next', serverArgs));\n const url = `http://${displayHost(host)}:${port}`;\n\n const scaffold = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => i.slug),\n projectDir: loaded.projectDir,\n harnessDir: shell.harnessDir,\n harnessVersion,\n previousVersion: shell.previousVersion,\n previousMarker: shell.previousMarker,\n stale: shell.stale,\n staleReasons: shell.staleReasons,\n sourceGlobs: loaded.project.sourceGlobs ?? [],\n created: shell.created,\n updated: shell.updated,\n unchanged: shell.unchanged,\n configBootstrapped: config.bootstrapped,\n configPath: config.configPath,\n gitignoreUpdated,\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (exposed) {\n ctx.warn(\n `WARNING: --host ${host} exposes the harness beyond this machine. It ` +\n 'has no authentication, and it renders your fixtures and — in live ' +\n 'mode — real responses from your vendor API. Its API routes hold the ' +\n `client secret from ${LOCAL_SECRETS_PATH}, the tokens onTokens stored, ` +\n 'and run your handlers on request; they refuse non-loopback callers ' +\n `unless ${EXPOSED_ENV}=1, which this sets. Anyone who can reach ` +\n `${url} can read and run all of it. Omit --host to bind ${DEFAULT_HOST}.`,\n );\n }\n\n // A JSON caller is an agent or a pipe. Handing it a dev server that never\n // exits would hang its tool call forever and it would never see the\n // envelope, so JSON mode scaffolds and reports the command instead of\n // running it. `--start` overrides; `--no-start` opts out in a terminal.\n const shouldStart = args.start ?? !ctx.jsonMode;\n if (!shouldStart) {\n return ctx.succeed(\n {\n ...scaffold,\n server: { started: false, command, url, port, host, exposed },\n },\n ctx.ui.result({\n title: 'Harness ready',\n rows: [\n ['Directory', displayPath(shell.harnessDir, process.cwd())],\n ['URL', url],\n ['Start', command],\n ],\n }),\n );\n }\n\n ctx.log(ctx.ui.dim(`Starting the harness on ${url} (${command})…`));\n const exitCode = await spawnServer({\n ctx,\n bin: pm.bin,\n argv: execArgs(pm, 'next', serverArgs),\n cwd: shell.harnessDir,\n env: serverEnv,\n });\n\n const data = {\n ...scaffold,\n server: { started: true, command, url, port, host, exposed, exitCode },\n };\n\n if (exitCode === 0) {\n return ctx.succeed(\n data,\n ctx.ui.result({ title: 'Harness stopped', rows: [['URL', url]] }),\n );\n }\n\n return ctx.failWith({\n code: 'dev_server_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the Next output on stderr. If it cannot resolve @ekanos/harness ' +\n `or next, reinstall dependencies in ${loaded.projectDir}; if a route ` +\n `fails to compile, delete ${shell.harnessDir} and re-run \"ekanos dev\".`,\n // Transparent passthrough of Next's own exit code.\n exitCode,\n data,\n });\n}\n\nfunction spawnServer(params: {\n ctx: CliContext;\n bin: string;\n argv: string[];\n cwd: string;\n env: Record<string, string>;\n}): Promise<number> {\n return new Promise<number>((resolve, reject) => {\n const child = spawn(params.bin, params.argv, {\n cwd: params.cwd,\n env: { ...process.env, ...params.env },\n // stdin inherited so Next's own key handling works; stdout/stderr piped\n // so we can route them and keep OUR stdout reserved for the envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n child.stdout?.on('data', (chunk: Buffer) => {\n (params.ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n}\n\n/**\n * The hostname to show a human. `127.0.0.1` is spelled `localhost` because\n * that is what people type; a wildcard bind is spelled `localhost` too, but\n * only after `exposed` has already been warned about — printing `0.0.0.0` as\n * a URL is not useful.\n */\nfunction displayHost(host: string): string {\n if (host === '127.0.0.1' || host === '0.0.0.0' || host === '::') {\n return 'localhost';\n }\n if (host === '::1') return '[::1]';\n return host;\n}\n\nfunction parseHost(raw: string | undefined): string {\n if (raw === undefined) return DEFAULT_HOST;\n\n const host = raw.trim();\n if (host.length === 0) {\n throw usageError(\n 'Invalid --host \"\".',\n `Pass an interface to bind, e.g. \"--host 0.0.0.0\" to expose the ` +\n `harness on your network, or omit it to bind ${DEFAULT_HOST}.`,\n );\n }\n return host;\n}\n\nfunction parsePort(raw: string | undefined): number {\n if (raw === undefined) return DEFAULT_PORT;\n\n const port = Number(raw);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw usageError(\n `Invalid --port \"${raw}\".`,\n 'Pass an integer between 1 and 65535, e.g. \"--port 3100\".',\n );\n }\n return port;\n}\n"]}
|