@ekanos/cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +41 -4
  2. package/dist/auth/ekanos-api.d.ts +23 -0
  3. package/dist/auth/ekanos-api.js +33 -6
  4. package/dist/auth/ekanos-api.js.map +1 -1
  5. package/dist/commands/agents.js +3 -2
  6. package/dist/commands/agents.js.map +1 -1
  7. package/dist/commands/apps.d.ts +11 -3
  8. package/dist/commands/apps.js +86 -28
  9. package/dist/commands/apps.js.map +1 -1
  10. package/dist/commands/catalog.d.ts +4 -2
  11. package/dist/commands/catalog.js +166 -35
  12. package/dist/commands/catalog.js.map +1 -1
  13. package/dist/commands/login.d.ts +2 -0
  14. package/dist/commands/login.js +17 -0
  15. package/dist/commands/login.js.map +1 -1
  16. package/dist/commands/runs.d.ts +7 -3
  17. package/dist/commands/runs.js +21 -5
  18. package/dist/commands/runs.js.map +1 -1
  19. package/dist/commands/scope-notice.d.ts +7 -0
  20. package/dist/commands/scope-notice.js +4 -11
  21. package/dist/commands/scope-notice.js.map +1 -1
  22. package/dist/commands/sources-roles.d.ts +5 -0
  23. package/dist/commands/sources-roles.js +235 -0
  24. package/dist/commands/sources-roles.js.map +1 -0
  25. package/dist/commands/sources.d.ts +5 -0
  26. package/dist/commands/sources.js +6 -1
  27. package/dist/commands/sources.js.map +1 -1
  28. package/dist/commands/team-agent-readiness.d.ts +18 -2
  29. package/dist/commands/team-agent-readiness.js +39 -39
  30. package/dist/commands/team-agent-readiness.js.map +1 -1
  31. package/dist/commands/team-agents-agents.d.ts +5 -2
  32. package/dist/commands/team-agents-agents.js +152 -15
  33. package/dist/commands/team-agents-agents.js.map +1 -1
  34. package/dist/commands/team-agents-define.d.ts +31 -0
  35. package/dist/commands/team-agents-define.js +176 -0
  36. package/dist/commands/team-agents-define.js.map +1 -0
  37. package/dist/commands/team-agents-mcp.js +112 -10
  38. package/dist/commands/team-agents-mcp.js.map +1 -1
  39. package/dist/commands/team-agents-triggers.d.ts +1 -1
  40. package/dist/commands/team-agents-triggers.js +137 -5
  41. package/dist/commands/team-agents-triggers.js.map +1 -1
  42. package/dist/commands/team-agents.d.ts +4 -1
  43. package/dist/commands/team-agents.js +17 -4
  44. package/dist/commands/team-agents.js.map +1 -1
  45. package/dist/commands/team-llm-key.d.ts +14 -0
  46. package/dist/commands/team-llm-key.js +122 -0
  47. package/dist/commands/team-llm-key.js.map +1 -0
  48. package/dist/commands/teams-connect.js +123 -31
  49. package/dist/commands/teams-connect.js.map +1 -1
  50. package/dist/commands/teams-lifecycle.d.ts +90 -0
  51. package/dist/commands/teams-lifecycle.js +457 -0
  52. package/dist/commands/teams-lifecycle.js.map +1 -0
  53. package/dist/commands/teams-onboard.d.ts +14 -0
  54. package/dist/commands/teams-onboard.js +62 -13
  55. package/dist/commands/teams-onboard.js.map +1 -1
  56. package/dist/commands/teams-overview.js +6 -2
  57. package/dist/commands/teams-overview.js.map +1 -1
  58. package/dist/commands/teams.d.ts +25 -5
  59. package/dist/commands/teams.js +101 -39
  60. package/dist/commands/teams.js.map +1 -1
  61. package/dist/commands/workflows.d.ts +37 -0
  62. package/dist/commands/workflows.js +589 -0
  63. package/dist/commands/workflows.js.map +1 -0
  64. package/dist/connect-fields.d.ts +22 -0
  65. package/dist/connect-fields.js +115 -0
  66. package/dist/connect-fields.js.map +1 -0
  67. package/dist/context.d.ts +12 -0
  68. package/dist/context.js +32 -9
  69. package/dist/context.js.map +1 -1
  70. package/dist/index.d.ts +13 -0
  71. package/dist/index.js +357 -122
  72. package/dist/index.js.map +1 -1
  73. package/dist/invocation.d.ts +31 -0
  74. package/dist/invocation.js +58 -0
  75. package/dist/invocation.js.map +1 -0
  76. package/dist/runs-api.js +8 -3
  77. package/dist/runs-api.js.map +1 -1
  78. package/dist/safe-fs.d.ts +18 -0
  79. package/dist/safe-fs.js +57 -0
  80. package/dist/safe-fs.js.map +1 -1
  81. package/dist/secret-form.d.ts +48 -0
  82. package/dist/secret-form.js +212 -0
  83. package/dist/secret-form.js.map +1 -0
  84. package/dist/secret-input.d.ts +96 -0
  85. package/dist/secret-input.js +299 -0
  86. package/dist/secret-input.js.map +1 -0
  87. package/dist/team-connect-api.d.ts +78 -3
  88. package/dist/team-connect-api.js +17 -2
  89. package/dist/team-connect-api.js.map +1 -1
  90. package/dist/team-lifecycle-api.d.ts +435 -0
  91. package/dist/team-lifecycle-api.js +301 -0
  92. package/dist/team-lifecycle-api.js.map +1 -0
  93. package/dist/teams-api.d.ts +13 -1
  94. package/dist/teams-api.js +88 -27
  95. package/dist/teams-api.js.map +1 -1
  96. package/dist/unknown-command.d.ts +18 -0
  97. package/dist/unknown-command.js +50 -0
  98. package/dist/unknown-command.js.map +1 -0
  99. package/dist/workflows-api.d.ts +69 -0
  100. package/dist/workflows-api.js +182 -0
  101. package/dist/workflows-api.js.map +1 -0
  102. package/package.json +3 -3
  103. package/templates/AGENTS.md.tmpl +6 -4
  104. package/templates/claude-skill.md.tmpl +4 -1
  105. package/templates/package.json.tmpl +4 -4
  106. package/templates/skills/build-agent/SKILL.md +43 -10
  107. package/templates/skills/build-agent/references/agent-json.md +4 -1
  108. package/templates/skills/build-agent/references/app-setup.md +14 -7
  109. package/templates/skills/build-agent/references/rollout.md +26 -2
  110. package/templates/skills/build-agent/references/set-up-an-integration.md +46 -29
  111. package/templates/skills/build-integration/references/widget-design.md +1 -1
  112. package/templates/skills/ekanos-platform/SKILL.md +66 -14
  113. package/templates/skills/polish-widgets/references/patterns.md +3 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invocation.js","sourceRoot":"","sources":["../src/invocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,IAAI,OAAO,GAAe,QAAQ,CAAC;AACnC,IAAI,cAAc,GAAkB,IAAI,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAiD,EACjD,UAAU,GAAuB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAEhD,IAAI,GAAG,CAAC,WAAW,KAAK,MAAM;QAAE,OAAO,iBAAiB,CAAC;IACzD,IAAI,UAAU,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,iBAAiB,CAAC;IAC9E,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,UAAsB,EACtB,QAA2B;IAE3B,OAAO,GAAG,UAAU,CAAC;IACrB,cAAc;QACZ,QAAQ,CAAC,MAAM,GAAG,CAAC;YACjB,CAAC,CAAC,IAAI,MAAM,CACR,8BAA8B,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EACnE,GAAG,CACJ;YACH,CAAC,CAAC,IAAI,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,GAAG;IACjB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,6EAA6E;QAC7E,OAAO,IAAI,CAAC,OAAO,CAAC,yBAAyB,EAAE,QAAQ,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,cAAc;QACnB,CAAC,CAAC,IAAI,CAAC,OAAO,CACV,cAAc,EACd,CAAC,MAAM,EAAE,IAAY,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,OAAO,GAAG,CAC/C;QACH,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC","sourcesContent":["/**\n * How the person ran this CLI, so every command a hint names can be pasted\n * back as-is: `ekanos …` from the installed bin, `npx @ekanos/cli …` only\n * when this run came through npx (an agent or a machine without the bin).\n *\n * Two halves, one rule:\n *\n * - `cli()` is the prefix for a command a hint BUILDS — the readiness\n * `next`, a `fix`, \"run it yourself in your terminal: …\". It lands in\n * the JSON data as well as the human text.\n * - `echoInvocation()` rewrites the prose the context writes (human\n * output, and an error's message and hint in either mode): a hint\n * written as `ekanos teams …` reads `npx @ekanos/cli teams …` under npx,\n * and one written as `npx @ekanos/cli …` reads `ekanos …` from the bin.\n * Only `ekanos` followed by one of the CLI's own commands is touched, so\n * the product name, `ekanos.json` and `@ekanos/sdk` never are.\n *\n * `run()` resolves it once per invocation (`setInvocation`).\n */\n\nexport type Invocation = 'ekanos' | 'npx @ekanos/cli';\n\nlet current: Invocation = 'ekanos';\nlet commandPattern: RegExp | null = null;\n\n/**\n * npx (npm 7+) runs the bin with `npm_command=exec`, from a package it\n * unpacked under the npm cache's `_npx` directory. Either says npx; a global\n * install, a project's own bin and a delegated run say neither.\n */\nexport function detectInvocation(\n env: Readonly<Record<string, string | undefined>>,\n scriptPath: string | undefined = process.argv[1],\n): Invocation {\n if (env.npm_command === 'exec') return 'npx @ekanos/cli';\n if (scriptPath && /[\\\\/]_npx[\\\\/]/.test(scriptPath)) return 'npx @ekanos/cli';\n return 'ekanos';\n}\n\nexport function setInvocation(\n invocation: Invocation,\n commands: readonly string[],\n): void {\n current = invocation;\n commandPattern =\n commands.length > 0\n ? new RegExp(\n `(^|[\\\\s\"'\\`(])ekanos (?=(?:${commands.map(escape).join('|')})\\\\b)`,\n 'g',\n )\n : null;\n}\n\n/** The prefix for a command a hint builds: `ekanos`, or `npx @ekanos/cli`. */\nexport function cli(): Invocation {\n return current;\n}\n\n/** `text` with every command it names written the way this run was invoked. */\nexport function echoInvocation(text: string): string {\n if (current === 'ekanos') {\n // A pinned `npx @ekanos/cli@1.2.3 …` names a version on purpose: left alone.\n return text.replace(/npx @ekanos\\/cli(?=\\s)/g, 'ekanos');\n }\n return commandPattern\n ? text.replace(\n commandPattern,\n (_match, lead: string) => `${lead}${current} `,\n )\n : text;\n}\n\nfunction escape(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"]}
package/dist/runs-api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AgentApprovalDecidedSchema, AgentApprovalListSchema, AgentRunCancelledSchema, AgentRunDetailSchema, AgentRunListSchema, AgentRunStartedSchema, } from '@ekanos/integration-schema';
2
2
  import { request } from './auth/ekanos-api.js';
3
3
  import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
4
+ import { cli } from './invocation.js';
4
5
  export function listRuns(host, accessToken, target) {
5
6
  return call(host, accessToken, AgentRunListSchema, {
6
7
  method: 'GET',
@@ -68,10 +69,14 @@ async function call(host, accessToken, schema, init) {
68
69
  }
69
70
  if (response.status === 403) {
70
71
  throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
71
- ? "Only a Source admin runs its teams' agents and decides their approvals."
72
+ ? `Only a Source admin runs its teams' agents. Approvals are also decided by a seat holder with that authority on the team: "${cli()} approvals list --team <slug>" shows yours.`
72
73
  : code === 'NOT_A_TEAM_APPROVER'
73
- ? "A team's own agent's approvals are the team's: a member who manages its agents decides them, in the app."
74
- : 'Run "ekanos sources" to list the sources you hold a seat on.');
74
+ ? `Someone with that authority on the team decides it: a member who manages its agents (or reviews them, for a reply draft), or a reviewer the Source admin named ("${cli()} reviewers list").`
75
+ : code === 'PERSONAL_AGENT_CREATOR_ONLY'
76
+ ? `A personal agent acts with its creator's own connections: only they start or cancel its runs and decide its approvals, signed in as themselves ("${cli()} login").`
77
+ : code === 'NOT_A_TEAM_MEMBER'
78
+ ? 'Add yourself to the team first ("ekanos teams members add <team> --self"), then run this again.'
79
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
75
80
  }
76
81
  if (response.status === 404) {
77
82
  throw notFoundError(message !== null && message !== void 0 ? message : 'Not found.', code === 'TEAM_NOT_FOUND'
@@ -1 +1 @@
1
- {"version":3,"file":"runs-api.js","sourceRoot":"","sources":["../src/runs-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentApprovalDecideRequest,\n type AgentApprovalDecided,\n AgentApprovalDecidedSchema,\n type AgentApprovalList,\n AgentApprovalListSchema,\n type AgentRunCancelled,\n AgentRunCancelledSchema,\n type AgentRunDetail,\n AgentRunDetailSchema,\n type AgentRunList,\n AgentRunListSchema,\n type AgentRunStartRequest,\n type AgentRunStarted,\n AgentRunStartedSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a team's agent runs and approvals — `ekanos runs`,\n * `ekanos agents test` and `ekanos approvals`.\n *\n * GET /api/partner/teams/<team>/runs?source=<slug>[&agent=<key>] → AgentRunList\n * POST /api/partner/teams/<team>/runs?source=<slug> → AgentRunStarted\n * GET /api/partner/teams/<team>/runs/<id>?source=<slug>[&after=] → AgentRunDetail\n * POST /api/partner/teams/<team>/runs/<id>/cancel?source=<slug> → AgentRunCancelled\n * GET /api/partner/teams/<team>/approvals?source=<slug> → AgentApprovalList\n * POST /api/partner/teams/<team>/approvals/<id>?source=<slug> → AgentApprovalDecided\n */\n\nexport type RunsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\ninterface TeamTarget {\n source: string;\n team: string;\n}\n\nexport function listRuns(\n host: string,\n accessToken: string,\n target: TeamTarget & { agent?: string; status?: string; since?: string },\n): Promise<RunsApiResult<AgentRunList>> {\n return call(host, accessToken, AgentRunListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs`,\n query: {\n source: target.source,\n ...(target.agent ? { agent: target.agent } : {}),\n ...(target.status ? { status: target.status } : {}),\n ...(target.since ? { since: target.since } : {}),\n },\n });\n}\n\nexport function startRun(\n host: string,\n accessToken: string,\n target: TeamTarget,\n body: AgentRunStartRequest,\n): Promise<RunsApiResult<AgentRunStarted>> {\n return call(host, accessToken, AgentRunStartedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs`,\n query: { source: target.source },\n body,\n });\n}\n\nexport function showRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string; after?: number },\n): Promise<RunsApiResult<AgentRunDetail>> {\n return call(host, accessToken, AgentRunDetailSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}`,\n query: {\n source: target.source,\n ...(target.after ? { after: String(target.after) } : {}),\n },\n });\n}\n\nexport function cancelRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string },\n): Promise<RunsApiResult<AgentRunCancelled>> {\n return call(host, accessToken, AgentRunCancelledSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}/cancel`,\n query: { source: target.source },\n });\n}\n\nexport function listApprovals(\n host: string,\n accessToken: string,\n target: TeamTarget,\n): Promise<RunsApiResult<AgentApprovalList>> {\n return call(host, accessToken, AgentApprovalListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/approvals`,\n query: { source: target.source },\n });\n}\n\nexport function decideApproval(\n host: string,\n accessToken: string,\n target: TeamTarget & { approvalId: string },\n body: AgentApprovalDecideRequest,\n): Promise<RunsApiResult<AgentApprovalDecided>> {\n return call(host, accessToken, AgentApprovalDecidedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/approvals/${encodeURIComponent(target.approvalId)}`,\n query: { source: target.source },\n body,\n });\n}\n\nfunction teamPath(target: TeamTarget): string {\n return `/api/partner/teams/${encodeURIComponent(target.team)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n query: Record<string, string>;\n body?: unknown;\n },\n): Promise<RunsApiResult<T>> {\n const response = await request(\n `${host}${init.path}?${new URLSearchParams(init.query)}`,\n {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n },\n );\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? \"Only a Source admin runs its teams' agents and decides their approvals.\"\n : code === 'NOT_A_TEAM_APPROVER'\n ? \"A team's own agent's approvals are the team's: a member who manages its agents decides them, in the app.\"\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? 'Not found.',\n code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : code === 'AGENT_NOT_FOUND' || code === 'TEMPLATE_NOT_FOUND'\n ? 'Run \"ekanos agents list\" for the Source\\'s agents; a team runs one once it is turned on there.'\n : code === 'RUN_NOT_FOUND'\n ? 'Run \"ekanos runs list --team <slug>\" for the team\\'s runs.'\n : code === 'NOT_FOUND'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is waiting.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 || response.status === 502) {\n throw invalidStateError(\n message ?? 'The host refused the request in its current state.',\n code === 'ALREADY_DECIDED'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is still waiting.'\n : code === 'RUN_FINISHED'\n ? 'Nothing to stop. \"ekanos runs show <run-id> --team <slug>\" shows how it ended.'\n : code === 'CANCEL_FAILED'\n ? 'Nothing changed. Retry \"ekanos runs cancel\" shortly.'\n : code === 'RESUME_FAILED'\n ? 'The decision is saved; the run picks it up shortly. Check with \"ekanos runs show <run-id>\".'\n : code === 'AGENTS_EXPERIENCE_OFF'\n ? 'Ask Ekanos to turn on the Agents Experience for this Source: only a super-admin can (\"ekanos sources products\" shows it as off).'\n : 'Run \"ekanos teams agents list <team>\": the agent may be paused or still need a connection.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n code === 'NOT_EDITABLE'\n ? 'Only a reply draft can be edited; approve or reject it instead.'\n : 'See \"ekanos runs --help\" / \"ekanos approvals --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the runs routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${init.path} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the runs contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
1
+ {"version":3,"file":"runs-api.js","sourceRoot":"","sources":["../src/runs-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentApprovalDecideRequest,\n type AgentApprovalDecided,\n AgentApprovalDecidedSchema,\n type AgentApprovalList,\n AgentApprovalListSchema,\n type AgentRunCancelled,\n AgentRunCancelledSchema,\n type AgentRunDetail,\n AgentRunDetailSchema,\n type AgentRunList,\n AgentRunListSchema,\n type AgentRunStartRequest,\n type AgentRunStarted,\n AgentRunStartedSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\nimport { cli } from './invocation';\n\n/**\n * The CLI's client for a team's agent runs and approvals — `ekanos runs`,\n * `ekanos agents test` and `ekanos approvals`.\n *\n * GET /api/partner/teams/<team>/runs?source=<slug>[&agent=<key>] → AgentRunList\n * POST /api/partner/teams/<team>/runs?source=<slug> → AgentRunStarted\n * GET /api/partner/teams/<team>/runs/<id>?source=<slug>[&after=] → AgentRunDetail\n * POST /api/partner/teams/<team>/runs/<id>/cancel?source=<slug> → AgentRunCancelled\n * GET /api/partner/teams/<team>/approvals?source=<slug> → AgentApprovalList\n * POST /api/partner/teams/<team>/approvals/<id>?source=<slug> → AgentApprovalDecided\n */\n\nexport type RunsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\ninterface TeamTarget {\n source: string;\n team: string;\n}\n\nexport function listRuns(\n host: string,\n accessToken: string,\n target: TeamTarget & { agent?: string; status?: string; since?: string },\n): Promise<RunsApiResult<AgentRunList>> {\n return call(host, accessToken, AgentRunListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs`,\n query: {\n source: target.source,\n ...(target.agent ? { agent: target.agent } : {}),\n ...(target.status ? { status: target.status } : {}),\n ...(target.since ? { since: target.since } : {}),\n },\n });\n}\n\nexport function startRun(\n host: string,\n accessToken: string,\n target: TeamTarget,\n body: AgentRunStartRequest,\n): Promise<RunsApiResult<AgentRunStarted>> {\n return call(host, accessToken, AgentRunStartedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs`,\n query: { source: target.source },\n body,\n });\n}\n\nexport function showRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string; after?: number },\n): Promise<RunsApiResult<AgentRunDetail>> {\n return call(host, accessToken, AgentRunDetailSchema, {\n method: 'GET',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}`,\n query: {\n source: target.source,\n ...(target.after ? { after: String(target.after) } : {}),\n },\n });\n}\n\nexport function cancelRun(\n host: string,\n accessToken: string,\n target: TeamTarget & { runId: string },\n): Promise<RunsApiResult<AgentRunCancelled>> {\n return call(host, accessToken, AgentRunCancelledSchema, {\n method: 'POST',\n path: `${teamPath(target)}/runs/${encodeURIComponent(target.runId)}/cancel`,\n query: { source: target.source },\n });\n}\n\nexport function listApprovals(\n host: string,\n accessToken: string,\n target: TeamTarget,\n): Promise<RunsApiResult<AgentApprovalList>> {\n return call(host, accessToken, AgentApprovalListSchema, {\n method: 'GET',\n path: `${teamPath(target)}/approvals`,\n query: { source: target.source },\n });\n}\n\nexport function decideApproval(\n host: string,\n accessToken: string,\n target: TeamTarget & { approvalId: string },\n body: AgentApprovalDecideRequest,\n): Promise<RunsApiResult<AgentApprovalDecided>> {\n return call(host, accessToken, AgentApprovalDecidedSchema, {\n method: 'POST',\n path: `${teamPath(target)}/approvals/${encodeURIComponent(target.approvalId)}`,\n query: { source: target.source },\n body,\n });\n}\n\nfunction teamPath(target: TeamTarget): string {\n return `/api/partner/teams/${encodeURIComponent(target.team)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n query: Record<string, string>;\n body?: unknown;\n },\n): Promise<RunsApiResult<T>> {\n const response = await request(\n `${host}${init.path}?${new URLSearchParams(init.query)}`,\n {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n },\n );\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? `Only a Source admin runs its teams' agents. Approvals are also decided by a seat holder with that authority on the team: \"${cli()} approvals list --team <slug>\" shows yours.`\n : code === 'NOT_A_TEAM_APPROVER'\n ? `Someone with that authority on the team decides it: a member who manages its agents (or reviews them, for a reply draft), or a reviewer the Source admin named (\"${cli()} reviewers list\").`\n : code === 'PERSONAL_AGENT_CREATOR_ONLY'\n ? `A personal agent acts with its creator's own connections: only they start or cancel its runs and decide its approvals, signed in as themselves (\"${cli()} login\").`\n : code === 'NOT_A_TEAM_MEMBER'\n ? 'Add yourself to the team first (\"ekanos teams members add <team> --self\"), then run this again.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? 'Not found.',\n code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : code === 'AGENT_NOT_FOUND' || code === 'TEMPLATE_NOT_FOUND'\n ? 'Run \"ekanos agents list\" for the Source\\'s agents; a team runs one once it is turned on there.'\n : code === 'RUN_NOT_FOUND'\n ? 'Run \"ekanos runs list --team <slug>\" for the team\\'s runs.'\n : code === 'NOT_FOUND'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is waiting.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 || response.status === 502) {\n throw invalidStateError(\n message ?? 'The host refused the request in its current state.',\n code === 'ALREADY_DECIDED'\n ? 'Run \"ekanos approvals list --team <slug>\" for what is still waiting.'\n : code === 'RUN_FINISHED'\n ? 'Nothing to stop. \"ekanos runs show <run-id> --team <slug>\" shows how it ended.'\n : code === 'CANCEL_FAILED'\n ? 'Nothing changed. Retry \"ekanos runs cancel\" shortly.'\n : code === 'RESUME_FAILED'\n ? 'The decision is saved; the run picks it up shortly. Check with \"ekanos runs show <run-id>\".'\n : code === 'AGENTS_EXPERIENCE_OFF'\n ? 'Ask Ekanos to turn on the Agents Experience for this Source: only a super-admin can (\"ekanos sources products\" shows it as off).'\n : 'Run \"ekanos teams agents list <team>\": the agent may be paused or still need a connection.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n code === 'NOT_EDITABLE'\n ? 'Only a reply draft can be edited; approve or reject it instead.'\n : 'See \"ekanos runs --help\" / \"ekanos approvals --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the runs routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${init.path} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the runs contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
package/dist/safe-fs.d.ts CHANGED
@@ -55,3 +55,21 @@ export declare function readBytesNoFollow(root: string, filePath: string): Buffe
55
55
  export declare function writeFileNoFollow(root: string, filePath: string, contents: string | Uint8Array): void;
56
56
  /** Append to an existing file under the same no-follow guarantee. */
57
57
  export declare function appendFileNoFollow(root: string, filePath: string, contents: string): void;
58
+ /** Why `readPrivateFileNoFollow` would not read a file. */
59
+ export type PrivateFileProblem = 'missing' | 'symlink' | 'not-a-file' | 'readable-by-others' | 'not-yours' | 'too-large';
60
+ /**
61
+ * Read a file that holds a secret — a path a person or an agent named, not
62
+ * one inside a tree we write — only if it is a regular file, not a symlink
63
+ * (`O_NOFOLLOW` at open, and an `lstat` first for platforms without it),
64
+ * owned by this user and readable by no one else. Every check after the
65
+ * open runs on the open descriptor, so the file judged is the file read.
66
+ * Returns the bytes, or the reason it was refused.
67
+ */
68
+ export declare function readPrivateFileNoFollow(filePath: string, maxBytes: number): {
69
+ ok: true;
70
+ bytes: Buffer;
71
+ } | {
72
+ ok: false;
73
+ problem: PrivateFileProblem;
74
+ mode?: number;
75
+ };
package/dist/safe-fs.js CHANGED
@@ -205,6 +205,63 @@ export function appendFileNoFollow(root, filePath, contents) {
205
205
  fs.closeSync(fd);
206
206
  }
207
207
  }
208
+ /**
209
+ * Read a file that holds a secret — a path a person or an agent named, not
210
+ * one inside a tree we write — only if it is a regular file, not a symlink
211
+ * (`O_NOFOLLOW` at open, and an `lstat` first for platforms without it),
212
+ * owned by this user and readable by no one else. Every check after the
213
+ * open runs on the open descriptor, so the file judged is the file read.
214
+ * Returns the bytes, or the reason it was refused.
215
+ */
216
+ export function readPrivateFileNoFollow(filePath, maxBytes) {
217
+ try {
218
+ if (fs.lstatSync(filePath).isSymbolicLink()) {
219
+ return { ok: false, problem: 'symlink' };
220
+ }
221
+ }
222
+ catch (error) {
223
+ if (error.code === 'ENOENT') {
224
+ return { ok: false, problem: 'missing' };
225
+ }
226
+ throw error;
227
+ }
228
+ let fd;
229
+ try {
230
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);
231
+ }
232
+ catch (error) {
233
+ const code = error.code;
234
+ if (code === 'ENOENT')
235
+ return { ok: false, problem: 'missing' };
236
+ if (code === 'ELOOP')
237
+ return { ok: false, problem: 'symlink' };
238
+ if (code === 'EISDIR')
239
+ return { ok: false, problem: 'not-a-file' };
240
+ throw error;
241
+ }
242
+ try {
243
+ const stats = fs.fstatSync(fd);
244
+ if (!stats.isFile())
245
+ return { ok: false, problem: 'not-a-file' };
246
+ // Mode bits and owners mean nothing on Windows; there the ACL governs.
247
+ if (process.platform !== 'win32') {
248
+ const mode = stats.mode & 0o777;
249
+ if ((mode & 0o077) !== 0) {
250
+ return { ok: false, problem: 'readable-by-others', mode };
251
+ }
252
+ if (typeof process.getuid === 'function' &&
253
+ stats.uid !== process.getuid()) {
254
+ return { ok: false, problem: 'not-yours' };
255
+ }
256
+ }
257
+ if (stats.size > maxBytes)
258
+ return { ok: false, problem: 'too-large' };
259
+ return { ok: true, bytes: fs.readFileSync(fd) };
260
+ }
261
+ finally {
262
+ fs.closeSync(fd);
263
+ }
264
+ }
208
265
  /**
209
266
  * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0
210
267
  * where the platform has no equivalent (Windows), in which case the explicit
@@ -1 +1 @@
1
- {"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n 'the command.',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `The CLI only writes real files inside the folder it writes to. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so nothing can be ` +\n 'written there.',\n `Move or delete ${current}, then re-run the command.`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n return readBytesNoFollow(root, filePath)?.toString('utf8') ?? null;\n}\n\n/** `readFileNoFollow`, as bytes — for comparing a binary file exactly. */\nexport function readBytesNoFollow(\n root: string,\n filePath: string,\n): Buffer | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
1
+ {"version":3,"file":"safe-fs.js","sourceRoot":"","sources":["../src/safe-fs.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Filesystem primitives for writing into a directory we do not fully trust.\n *\n * `ekanos dev` writes a generated tree into a partner's project, and every\n * path it touches is one an attacker (or a careless `ln -s`) may have created\n * first. Plain `existsSync` + `writeFileSync` follows symlinks, so a\n * `.ekanos/harness` pointing at `~/.ssh`, a generated leaf pointing at a\n * tracked source file, or a DANGLING link (which `existsSync` reports as\n * absent, and which `writeFileSync` then materializes) all let a scaffold\n * write outside the project.\n *\n * Two invariants close that, and both are enforced here rather than at the\n * call sites, because a single unguarded write anywhere reopens the hole:\n *\n * 1. **Containment** — every path resolves inside the project root.\n * 2. **No symlinks** — no component of a path we write through may be a\n * symlink, and the write itself uses `O_NOFOLLOW` so the kernel enforces\n * it at open time rather than trusting our earlier `lstat`. That closes\n * the check→write TOCTOU window: a link swapped in after the check makes\n * `open` fail with ELOOP instead of following.\n */\n\n/** True when `target` is inside `root` (or is `root` itself). */\nexport function isContained(root: string, target: string): boolean {\n const relative = path.relative(root, target);\n if (relative === '') return true;\n return !relative.startsWith('..') && !path.isAbsolute(relative);\n}\n\nfunction containmentError(root: string, target: string): never {\n throw invalidStateError(\n `Refusing to write ${target}: it resolves outside ${root}.`,\n 'Remove the symlink or path component that redirects it, then re-run ' +\n 'the command.',\n );\n}\n\nfunction symlinkError(link: string): never {\n throw invalidStateError(\n `Refusing to write through the symlink ${link}.`,\n `The CLI only writes real files inside the folder it writes to. Delete ${link} ` +\n '(or the tree containing it) and re-run.',\n );\n}\n\n/**\n * Walk every path component from `root` down to `target` and refuse if any\n * existing one is a symlink. `lstat` is deliberate — `stat` would resolve the\n * link and report the target's type, which is exactly what we must not do.\n */\nexport function assertNoSymlinkedComponent(root: string, target: string): void {\n if (!isContained(root, target)) containmentError(root, target);\n\n const relative = path.relative(root, target);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(current);\n } catch {\n // Does not exist yet — nothing to follow, and everything below it is\n // ours to create.\n return;\n }\n if (stats.isSymbolicLink()) symlinkError(current);\n }\n}\n\n/**\n * `mkdir -p`, one guarded component at a time. `fs.mkdirSync(recursive)` is\n * happy to walk through a symlinked ancestor; this is not.\n */\nexport function mkdirSafe(root: string, dir: string): void {\n if (!isContained(root, dir)) containmentError(root, dir);\n\n const relative = path.relative(root, dir);\n if (relative === '') return;\n\n let current = root;\n for (const segment of relative.split(path.sep)) {\n current = path.join(current, segment);\n let stats: fs.Stats | null = null;\n try {\n stats = fs.lstatSync(current);\n } catch {\n stats = null;\n }\n\n if (stats === null) {\n try {\n fs.mkdirSync(current);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n // Lost a race to another process; re-check what landed there.\n if (code !== 'EEXIST') throw error;\n stats = fs.lstatSync(current);\n }\n if (stats === null) continue;\n }\n\n if (stats.isSymbolicLink()) symlinkError(current);\n if (!stats.isDirectory()) {\n throw invalidStateError(\n `${current} exists and is not a directory, so nothing can be ` +\n 'written there.',\n `Move or delete ${current}, then re-run the command.`,\n );\n }\n }\n}\n\n/**\n * What `destPath` currently is, without following anything. `'symlink'` is\n * reported for a dangling link too — the case `existsSync` calls absent and\n * `writeFileSync` would silently materialize outside the project.\n */\nexport type PathKind = 'missing' | 'file' | 'directory' | 'symlink' | 'other';\n\nexport function classifyPath(destPath: string): PathKind {\n let stats: fs.Stats;\n try {\n stats = fs.lstatSync(destPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 'missing';\n throw error;\n }\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isDirectory()) return 'directory';\n if (stats.isFile()) return 'file';\n return 'other';\n}\n\n/**\n * Read a file, refusing to follow a symlink at the final component. Returns\n * null when it does not exist. `O_NOFOLLOW` makes the kernel enforce this, so\n * there is no window between deciding and reading.\n */\nexport function readFileNoFollow(\n root: string,\n filePath: string,\n): string | null {\n return readBytesNoFollow(root, filePath)?.toString('utf8') ?? null;\n}\n\n/** `readFileNoFollow`, as bytes — for comparing a binary file exactly. */\nexport function readBytesNoFollow(\n root: string,\n filePath: string,\n): Buffer | null {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return null;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n return fs.readFileSync(fd);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * Write a file, creating parent directories, refusing every symlink on the\n * way and at the leaf. `O_NOFOLLOW` on the final open is what makes this safe\n * against a link swapped in after our checks.\n */\nexport function writeFileNoFollow(\n root: string,\n filePath: string,\n contents: string | Uint8Array,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n mkdirSafe(root, path.dirname(filePath));\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY |\n fs.constants.O_CREAT |\n fs.constants.O_TRUNC |\n O_NOFOLLOW,\n 0o644,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Append to an existing file under the same no-follow guarantee. */\nexport function appendFileNoFollow(\n root: string,\n filePath: string,\n contents: string,\n): void {\n assertNoSymlinkedComponent(root, filePath);\n\n let fd: number;\n try {\n fd = fs.openSync(\n filePath,\n fs.constants.O_WRONLY | fs.constants.O_APPEND | O_NOFOLLOW,\n );\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ELOOP') symlinkError(filePath);\n throw error;\n }\n\n try {\n fs.writeFileSync(fd, contents);\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/** Why `readPrivateFileNoFollow` would not read a file. */\nexport type PrivateFileProblem =\n | 'missing'\n | 'symlink'\n | 'not-a-file'\n | 'readable-by-others'\n | 'not-yours'\n | 'too-large';\n\n/**\n * Read a file that holds a secret — a path a person or an agent named, not\n * one inside a tree we write — only if it is a regular file, not a symlink\n * (`O_NOFOLLOW` at open, and an `lstat` first for platforms without it),\n * owned by this user and readable by no one else. Every check after the\n * open runs on the open descriptor, so the file judged is the file read.\n * Returns the bytes, or the reason it was refused.\n */\nexport function readPrivateFileNoFollow(\n filePath: string,\n maxBytes: number,\n):\n | { ok: true; bytes: Buffer }\n | { ok: false; problem: PrivateFileProblem; mode?: number } {\n try {\n if (fs.lstatSync(filePath).isSymbolicLink()) {\n return { ok: false, problem: 'symlink' };\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return { ok: false, problem: 'missing' };\n }\n throw error;\n }\n\n let fd: number;\n try {\n fd = fs.openSync(filePath, fs.constants.O_RDONLY | O_NOFOLLOW);\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') return { ok: false, problem: 'missing' };\n if (code === 'ELOOP') return { ok: false, problem: 'symlink' };\n if (code === 'EISDIR') return { ok: false, problem: 'not-a-file' };\n throw error;\n }\n\n try {\n const stats = fs.fstatSync(fd);\n if (!stats.isFile()) return { ok: false, problem: 'not-a-file' };\n // Mode bits and owners mean nothing on Windows; there the ACL governs.\n if (process.platform !== 'win32') {\n const mode = stats.mode & 0o777;\n if ((mode & 0o077) !== 0) {\n return { ok: false, problem: 'readable-by-others', mode };\n }\n if (\n typeof process.getuid === 'function' &&\n stats.uid !== process.getuid()\n ) {\n return { ok: false, problem: 'not-yours' };\n }\n }\n if (stats.size > maxBytes) return { ok: false, problem: 'too-large' };\n return { ok: true, bytes: fs.readFileSync(fd) };\n } finally {\n fs.closeSync(fd);\n }\n}\n\n/**\n * `O_NOFOLLOW` is POSIX and present on Linux and macOS. Node exposes it as 0\n * where the platform has no equivalent (Windows), in which case the explicit\n * `lstat` walk above is the whole guarantee rather than a belt to the kernel's\n * braces.\n */\nconst O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The "little GUI" for a secret: a one-shot form served by the CLI itself on
3
+ * this computer, for a run with no terminal to type into (a coding agent, a
4
+ * script) but a person at the desktop. The person types the secret into
5
+ * their own browser; it goes from there to this process and on to the host,
6
+ * and never to stdout, stderr, a log or the `--json` envelope — so an agent
7
+ * driving the CLI starts the form and never sees what was entered.
8
+ *
9
+ * What keeps it to that one person and that one submission:
10
+ *
11
+ * - **Loopback only.** It listens on 127.0.0.1, on a port the OS picks.
12
+ * - **An unguessable path.** 256 random bits; any other path is a 404.
13
+ * - **Host and Origin.** A request whose `Host` is not exactly
14
+ * `127.0.0.1:<port>` is refused (a DNS-rebinding page names its own
15
+ * host), and a submission must carry this form's `Origin` — so another
16
+ * page in the same browser cannot post to it.
17
+ * - **One use, short life.** The first good submission closes the server;
18
+ * so does the timeout, which fails the command rather than waiting on.
19
+ * - **A page that loads nothing.** No script, no external asset: a CSP of
20
+ * `default-src 'none'` with one nonce'd stylesheet, never framed, never
21
+ * cached, no referrer.
22
+ */
23
+ export interface SecretFormField {
24
+ name: string;
25
+ label: string;
26
+ sensitive: boolean;
27
+ description?: string;
28
+ required?: boolean;
29
+ /** A value is saved already: leaving it empty keeps it. */
30
+ set?: boolean;
31
+ /** A non-secret field's saved value, shown as the placeholder. Never a secret. */
32
+ value?: string | null;
33
+ }
34
+ export interface SecretFormOptions {
35
+ title: string;
36
+ fields: readonly SecretFormField[];
37
+ /** Opens the form's URL in the person's browser. */
38
+ open: (url: string) => void;
39
+ /** How long to wait for the submission (default five minutes). */
40
+ timeoutMs?: number;
41
+ }
42
+ export declare const SECRET_FORM_TIMEOUT_MS: number;
43
+ /**
44
+ * Serve the form, open it, and resolve with what was submitted: each field's
45
+ * value by name, empty ones left out. Rejects on the timeout.
46
+ */
47
+ export declare function collectWithSecretForm(options: SecretFormOptions): Promise<Record<string, string>>;
48
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,212 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import * as http from 'node:http';
3
+ import { validationError } from './errors.js';
4
+ export const SECRET_FORM_TIMEOUT_MS = 5 * 60 * 1000;
5
+ const MAX_BODY_BYTES = 64 * 1024;
6
+ /**
7
+ * Serve the form, open it, and resolve with what was submitted: each field's
8
+ * value by name, empty ones left out. Rejects on the timeout.
9
+ */
10
+ export function collectWithSecretForm(options) {
11
+ var _a;
12
+ const token = randomBytes(32).toString('base64url');
13
+ const nonce = randomBytes(16).toString('base64');
14
+ const timeoutMs = (_a = options.timeoutMs) !== null && _a !== void 0 ? _a : SECRET_FORM_TIMEOUT_MS;
15
+ return new Promise((resolve, reject) => {
16
+ let settled = false;
17
+ let origin = '';
18
+ let hostHeader = '';
19
+ const server = http.createServer((request, response) => {
20
+ handle(request, response).catch(() => {
21
+ if (!response.headersSent)
22
+ send(response, 500, page('Something went wrong', 'Return to your terminal.'));
23
+ });
24
+ });
25
+ const finish = (outcome) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ clearTimeout(timer);
30
+ server.close();
31
+ server.closeAllConnections();
32
+ outcome();
33
+ };
34
+ const timer = setTimeout(() => {
35
+ finish(() => reject(validationError(`The secure form was not submitted within ${duration(timeoutMs)}; nothing was saved.`, 'Run the command again for a fresh form, or pass the secret with --secret-env, --secret-file or --secret-stdin.')));
36
+ }, timeoutMs);
37
+ const send = (response, status, body) => {
38
+ response.writeHead(status, {
39
+ 'content-type': 'text/html; charset=utf-8',
40
+ 'cache-control': 'no-store',
41
+ 'content-security-policy': `default-src 'none'; style-src 'nonce-${nonce}'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`,
42
+ 'x-frame-options': 'DENY',
43
+ 'x-content-type-options': 'nosniff',
44
+ 'referrer-policy': 'no-referrer',
45
+ 'cross-origin-resource-policy': 'same-origin',
46
+ });
47
+ response.end(body);
48
+ };
49
+ const page = (heading, text, inner = '') => [
50
+ '<!doctype html>',
51
+ '<html lang="en"><head><meta charset="utf-8">',
52
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
53
+ '<meta name="referrer" content="no-referrer">',
54
+ `<title>${escapeHtml(heading)} · ekanos</title>`,
55
+ `<style nonce="${nonce}">${STYLE}</style>`,
56
+ '</head><body><main>',
57
+ `<h1>${escapeHtml(heading)}</h1>`,
58
+ `<p class="lede">${escapeHtml(text)}</p>`,
59
+ inner,
60
+ '</main></body></html>',
61
+ ].join('');
62
+ const isOurPath = (pathname) => {
63
+ const expected = Buffer.from(`/${token}`);
64
+ const given = Buffer.from(pathname);
65
+ return (given.length === expected.length && timingSafeEqual(given, expected));
66
+ };
67
+ async function handle(request, response) {
68
+ var _a, _b, _c;
69
+ if (settled) {
70
+ send(response, 410, page('This form is closed', 'Return to your terminal.'));
71
+ return;
72
+ }
73
+ if (request.headers.host !== hostHeader) {
74
+ send(response, 421, page('Wrong address', 'Open the link the ekanos CLI opened.'));
75
+ return;
76
+ }
77
+ const pathname = (_b = ((_a = request.url) !== null && _a !== void 0 ? _a : '').split('?')[0]) !== null && _b !== void 0 ? _b : '';
78
+ if (!isOurPath(pathname)) {
79
+ send(response, 404, page('Not found', 'Open the link the ekanos CLI opened.'));
80
+ return;
81
+ }
82
+ if (request.method === 'GET') {
83
+ send(response, 200, formPage());
84
+ return;
85
+ }
86
+ if (request.method !== 'POST') {
87
+ response.setHeader('allow', 'GET, POST');
88
+ send(response, 405, page('Not allowed', 'Return to your terminal.'));
89
+ return;
90
+ }
91
+ const fetchSite = request.headers['sec-fetch-site'];
92
+ if (request.headers.origin !== origin ||
93
+ (fetchSite !== undefined && fetchSite !== 'same-origin')) {
94
+ send(response, 403, page('Refused', 'This form only accepts what is typed into it.'));
95
+ return;
96
+ }
97
+ if (!((_c = request.headers['content-type']) !== null && _c !== void 0 ? _c : '')
98
+ .toLowerCase()
99
+ .startsWith('application/x-www-form-urlencoded')) {
100
+ send(response, 415, page('Refused', 'Submit the form itself.'));
101
+ return;
102
+ }
103
+ const body = await readBody(request);
104
+ if (body === null) {
105
+ send(response, 413, page('Too large', 'Return to your terminal.'));
106
+ return;
107
+ }
108
+ const params = new URLSearchParams(body);
109
+ const values = {};
110
+ options.fields.forEach((field, index) => {
111
+ var _a;
112
+ const value = ((_a = params.get(`f${index}`)) !== null && _a !== void 0 ? _a : '').trim();
113
+ if (value)
114
+ values[field.name] = value;
115
+ });
116
+ send(response, 200, page('Received', 'The ekanos CLI has what you entered and is saving it. You can close this tab and return to your terminal.'));
117
+ finish(() => resolve(values));
118
+ }
119
+ function formPage() {
120
+ const inputs = options.fields
121
+ .map((field, index) => {
122
+ const id = `f${index}`;
123
+ const placeholder = field.sensitive
124
+ ? field.set
125
+ ? 'Saved — leave empty to keep it'
126
+ : ''
127
+ : field.set
128
+ ? field.value
129
+ ? `Current: ${field.value} — leave empty to keep it`
130
+ : 'Saved — leave empty to keep it'
131
+ : '';
132
+ const required = field.required && !field.set;
133
+ return [
134
+ '<div class="field">',
135
+ `<label for="${id}">${escapeHtml(field.label)}${required ? '' : ' <span class="optional">(optional)</span>'}</label>`,
136
+ field.description
137
+ ? `<p class="help" id="${id}-help">${escapeHtml(field.description)}</p>`
138
+ : '',
139
+ `<input id="${id}" name="${id}" type="${field.sensitive ? 'password' : 'text'}"`,
140
+ ' autocomplete="off" autocapitalize="off" spellcheck="false"',
141
+ placeholder ? ` placeholder="${escapeHtml(placeholder)}"` : '',
142
+ field.description ? ` aria-describedby="${id}-help"` : '',
143
+ required ? ' required' : '',
144
+ index === 0 ? ' autofocus' : '',
145
+ '>',
146
+ '</div>',
147
+ ].join('');
148
+ })
149
+ .join('');
150
+ return page(options.title, 'This page is served by the ekanos CLI on your own computer. What you enter goes only to it, then to Ekanos over HTTPS — never to your terminal, a log or the agent that ran the command. It works once and closes.', `<form method="post" action="/${token}">${inputs}<button type="submit">Save</button></form>`);
151
+ }
152
+ server.on('error', (error) => finish(() => reject(error)));
153
+ server.listen(0, '127.0.0.1', () => {
154
+ const { port } = server.address();
155
+ origin = `http://127.0.0.1:${port}`;
156
+ hostHeader = `127.0.0.1:${port}`;
157
+ try {
158
+ options.open(`${origin}/${token}`);
159
+ }
160
+ catch (_a) {
161
+ // The timeout still ends the wait.
162
+ }
163
+ });
164
+ });
165
+ }
166
+ function readBody(request) {
167
+ return new Promise((resolve, reject) => {
168
+ const chunks = [];
169
+ let size = 0;
170
+ let tooLarge = false;
171
+ request.on('data', (chunk) => {
172
+ size += chunk.length;
173
+ if (size > MAX_BODY_BYTES) {
174
+ tooLarge = true;
175
+ return;
176
+ }
177
+ chunks.push(chunk);
178
+ });
179
+ request.on('end', () => resolve(tooLarge ? null : Buffer.concat(chunks).toString('utf8')));
180
+ request.on('error', reject);
181
+ });
182
+ }
183
+ function duration(ms) {
184
+ if (ms < 60000) {
185
+ const seconds = Math.max(1, Math.ceil(ms / 1000));
186
+ return `${seconds} second${seconds === 1 ? '' : 's'}`;
187
+ }
188
+ const minutes = Math.round(ms / 60000);
189
+ return `${minutes} minute${minutes === 1 ? '' : 's'}`;
190
+ }
191
+ export function escapeHtml(value) {
192
+ return value
193
+ .replace(/&/g, '&amp;')
194
+ .replace(/</g, '&lt;')
195
+ .replace(/>/g, '&gt;')
196
+ .replace(/"/g, '&quot;')
197
+ .replace(/'/g, '&#39;');
198
+ }
199
+ const STYLE = [
200
+ ':root{color-scheme:light dark;font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;line-height:1.5}',
201
+ 'body{margin:0;display:flex;justify-content:center;padding:48px 16px}',
202
+ 'main{width:100%;max-width:440px}',
203
+ 'h1{font-size:1.25rem;margin:0 0 8px}',
204
+ '.lede{margin:0 0 24px;opacity:.75;font-size:.9rem}',
205
+ '.field{margin:0 0 16px}',
206
+ 'label{display:block;font-weight:600;font-size:.9rem;margin:0 0 4px}',
207
+ '.optional{font-weight:400;opacity:.6}',
208
+ '.help{margin:0 0 6px;font-size:.85rem;opacity:.7}',
209
+ 'input{box-sizing:border-box;width:100%;padding:8px 10px;font:inherit;border:1px solid #8888;border-radius:6px;background:transparent;color:inherit}',
210
+ 'button{margin-top:8px;padding:8px 16px;font:inherit;font-weight:600;border:1px solid currentColor;border-radius:6px;background:transparent;color:inherit;cursor:pointer}',
211
+ ].join('');
212
+ //# sourceMappingURL=secret-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secret-form.js","sourceRoot":"","sources":["../src/secret-form.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes, timingSafeEqual } from 'node:crypto';\nimport * as http from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nimport { validationError } from './errors';\n\n/**\n * The \"little GUI\" for a secret: a one-shot form served by the CLI itself on\n * this computer, for a run with no terminal to type into (a coding agent, a\n * script) but a person at the desktop. The person types the secret into\n * their own browser; it goes from there to this process and on to the host,\n * and never to stdout, stderr, a log or the `--json` envelope — so an agent\n * driving the CLI starts the form and never sees what was entered.\n *\n * What keeps it to that one person and that one submission:\n *\n * - **Loopback only.** It listens on 127.0.0.1, on a port the OS picks.\n * - **An unguessable path.** 256 random bits; any other path is a 404.\n * - **Host and Origin.** A request whose `Host` is not exactly\n * `127.0.0.1:<port>` is refused (a DNS-rebinding page names its own\n * host), and a submission must carry this form's `Origin` — so another\n * page in the same browser cannot post to it.\n * - **One use, short life.** The first good submission closes the server;\n * so does the timeout, which fails the command rather than waiting on.\n * - **A page that loads nothing.** No script, no external asset: a CSP of\n * `default-src 'none'` with one nonce'd stylesheet, never framed, never\n * cached, no referrer.\n */\n\nexport interface SecretFormField {\n name: string;\n label: string;\n sensitive: boolean;\n description?: string;\n required?: boolean;\n /** A value is saved already: leaving it empty keeps it. */\n set?: boolean;\n /** A non-secret field's saved value, shown as the placeholder. Never a secret. */\n value?: string | null;\n}\n\nexport interface SecretFormOptions {\n title: string;\n fields: readonly SecretFormField[];\n /** Opens the form's URL in the person's browser. */\n open: (url: string) => void;\n /** How long to wait for the submission (default five minutes). */\n timeoutMs?: number;\n}\n\nexport const SECRET_FORM_TIMEOUT_MS = 5 * 60 * 1000;\nconst MAX_BODY_BYTES = 64 * 1024;\n\n/**\n * Serve the form, open it, and resolve with what was submitted: each field's\n * value by name, empty ones left out. Rejects on the timeout.\n */\nexport function collectWithSecretForm(\n options: SecretFormOptions,\n): Promise<Record<string, string>> {\n const token = randomBytes(32).toString('base64url');\n const nonce = randomBytes(16).toString('base64');\n const timeoutMs = options.timeoutMs ?? SECRET_FORM_TIMEOUT_MS;\n\n return new Promise((resolve, reject) => {\n let settled = false;\n let origin = '';\n let hostHeader = '';\n\n const server = http.createServer((request, response) => {\n handle(request, response).catch(() => {\n if (!response.headersSent)\n send(\n response,\n 500,\n page('Something went wrong', 'Return to your terminal.'),\n );\n });\n });\n\n const finish = (outcome: () => void) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n server.close();\n server.closeAllConnections();\n outcome();\n };\n\n const timer = setTimeout(() => {\n finish(() =>\n reject(\n validationError(\n `The secure form was not submitted within ${duration(timeoutMs)}; nothing was saved.`,\n 'Run the command again for a fresh form, or pass the secret with --secret-env, --secret-file or --secret-stdin.',\n ),\n ),\n );\n }, timeoutMs);\n\n const send = (\n response: http.ServerResponse,\n status: number,\n body: string,\n ) => {\n response.writeHead(status, {\n 'content-type': 'text/html; charset=utf-8',\n 'cache-control': 'no-store',\n 'content-security-policy': `default-src 'none'; style-src 'nonce-${nonce}'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`,\n 'x-frame-options': 'DENY',\n 'x-content-type-options': 'nosniff',\n 'referrer-policy': 'no-referrer',\n 'cross-origin-resource-policy': 'same-origin',\n });\n response.end(body);\n };\n\n const page = (heading: string, text: string, inner = '') =>\n [\n '<!doctype html>',\n '<html lang=\"en\"><head><meta charset=\"utf-8\">',\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n '<meta name=\"referrer\" content=\"no-referrer\">',\n `<title>${escapeHtml(heading)} · ekanos</title>`,\n `<style nonce=\"${nonce}\">${STYLE}</style>`,\n '</head><body><main>',\n `<h1>${escapeHtml(heading)}</h1>`,\n `<p class=\"lede\">${escapeHtml(text)}</p>`,\n inner,\n '</main></body></html>',\n ].join('');\n\n const isOurPath = (pathname: string): boolean => {\n const expected = Buffer.from(`/${token}`);\n const given = Buffer.from(pathname);\n return (\n given.length === expected.length && timingSafeEqual(given, expected)\n );\n };\n\n async function handle(\n request: http.IncomingMessage,\n response: http.ServerResponse,\n ): Promise<void> {\n if (settled) {\n send(\n response,\n 410,\n page('This form is closed', 'Return to your terminal.'),\n );\n return;\n }\n if (request.headers.host !== hostHeader) {\n send(\n response,\n 421,\n page('Wrong address', 'Open the link the ekanos CLI opened.'),\n );\n return;\n }\n const pathname = (request.url ?? '').split('?')[0] ?? '';\n if (!isOurPath(pathname)) {\n send(\n response,\n 404,\n page('Not found', 'Open the link the ekanos CLI opened.'),\n );\n return;\n }\n\n if (request.method === 'GET') {\n send(response, 200, formPage());\n return;\n }\n if (request.method !== 'POST') {\n response.setHeader('allow', 'GET, POST');\n send(response, 405, page('Not allowed', 'Return to your terminal.'));\n return;\n }\n\n const fetchSite = request.headers['sec-fetch-site'];\n if (\n request.headers.origin !== origin ||\n (fetchSite !== undefined && fetchSite !== 'same-origin')\n ) {\n send(\n response,\n 403,\n page('Refused', 'This form only accepts what is typed into it.'),\n );\n return;\n }\n if (\n !(request.headers['content-type'] ?? '')\n .toLowerCase()\n .startsWith('application/x-www-form-urlencoded')\n ) {\n send(response, 415, page('Refused', 'Submit the form itself.'));\n return;\n }\n\n const body = await readBody(request);\n if (body === null) {\n send(response, 413, page('Too large', 'Return to your terminal.'));\n return;\n }\n const params = new URLSearchParams(body);\n const values: Record<string, string> = {};\n options.fields.forEach((field, index) => {\n const value = (params.get(`f${index}`) ?? '').trim();\n if (value) values[field.name] = value;\n });\n\n send(\n response,\n 200,\n page(\n 'Received',\n 'The ekanos CLI has what you entered and is saving it. You can close this tab and return to your terminal.',\n ),\n );\n finish(() => resolve(values));\n }\n\n function formPage(): string {\n const inputs = options.fields\n .map((field, index) => {\n const id = `f${index}`;\n const placeholder = field.sensitive\n ? field.set\n ? 'Saved — leave empty to keep it'\n : ''\n : field.set\n ? field.value\n ? `Current: ${field.value} — leave empty to keep it`\n : 'Saved — leave empty to keep it'\n : '';\n const required = field.required && !field.set;\n return [\n '<div class=\"field\">',\n `<label for=\"${id}\">${escapeHtml(field.label)}${required ? '' : ' <span class=\"optional\">(optional)</span>'}</label>`,\n field.description\n ? `<p class=\"help\" id=\"${id}-help\">${escapeHtml(field.description)}</p>`\n : '',\n `<input id=\"${id}\" name=\"${id}\" type=\"${field.sensitive ? 'password' : 'text'}\"`,\n ' autocomplete=\"off\" autocapitalize=\"off\" spellcheck=\"false\"',\n placeholder ? ` placeholder=\"${escapeHtml(placeholder)}\"` : '',\n field.description ? ` aria-describedby=\"${id}-help\"` : '',\n required ? ' required' : '',\n index === 0 ? ' autofocus' : '',\n '>',\n '</div>',\n ].join('');\n })\n .join('');\n return page(\n options.title,\n 'This page is served by the ekanos CLI on your own computer. What you enter goes only to it, then to Ekanos over HTTPS — never to your terminal, a log or the agent that ran the command. It works once and closes.',\n `<form method=\"post\" action=\"/${token}\">${inputs}<button type=\"submit\">Save</button></form>`,\n );\n }\n\n server.on('error', (error) => finish(() => reject(error)));\n server.listen(0, '127.0.0.1', () => {\n const { port } = server.address() as AddressInfo;\n origin = `http://127.0.0.1:${port}`;\n hostHeader = `127.0.0.1:${port}`;\n try {\n options.open(`${origin}/${token}`);\n } catch {\n // The timeout still ends the wait.\n }\n });\n });\n}\n\nfunction readBody(request: http.IncomingMessage): Promise<string | null> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let tooLarge = false;\n request.on('data', (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n tooLarge = true;\n return;\n }\n chunks.push(chunk);\n });\n request.on('end', () =>\n resolve(tooLarge ? null : Buffer.concat(chunks).toString('utf8')),\n );\n request.on('error', reject);\n });\n}\n\nfunction duration(ms: number): string {\n if (ms < 60_000) {\n const seconds = Math.max(1, Math.ceil(ms / 1000));\n return `${seconds} second${seconds === 1 ? '' : 's'}`;\n }\n const minutes = Math.round(ms / 60_000);\n return `${minutes} minute${minutes === 1 ? '' : 's'}`;\n}\n\nexport function escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\nconst STYLE = [\n ':root{color-scheme:light dark;font-family:ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif;line-height:1.5}',\n 'body{margin:0;display:flex;justify-content:center;padding:48px 16px}',\n 'main{width:100%;max-width:440px}',\n 'h1{font-size:1.25rem;margin:0 0 8px}',\n '.lede{margin:0 0 24px;opacity:.75;font-size:.9rem}',\n '.field{margin:0 0 16px}',\n 'label{display:block;font-weight:600;font-size:.9rem;margin:0 0 4px}',\n '.optional{font-weight:400;opacity:.6}',\n '.help{margin:0 0 6px;font-size:.85rem;opacity:.7}',\n 'input{box-sizing:border-box;width:100%;padding:8px 10px;font:inherit;border:1px solid #8888;border-radius:6px;background:transparent;color:inherit}',\n 'button{margin-top:8px;padding:8px 16px;font:inherit;font-weight:600;border:1px solid currentColor;border-radius:6px;background:transparent;color:inherit;cursor:pointer}',\n].join('');\n"]}