@ekanos/cli 0.12.0 → 0.14.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 (175) hide show
  1. package/README.md +25 -8
  2. package/dist/agents-api.d.ts +11 -2
  3. package/dist/agents-api.js +39 -3
  4. package/dist/agents-api.js.map +1 -1
  5. package/dist/apps-api.d.ts +3 -2
  6. package/dist/apps-api.js +3 -7
  7. package/dist/apps-api.js.map +1 -1
  8. package/dist/auth/ekanos-api.d.ts +16 -2
  9. package/dist/auth/ekanos-api.js +101 -15
  10. package/dist/auth/ekanos-api.js.map +1 -1
  11. package/dist/auth/host-trust.d.ts +46 -0
  12. package/dist/auth/host-trust.js +79 -0
  13. package/dist/auth/host-trust.js.map +1 -0
  14. package/dist/auth/session.d.ts +10 -1
  15. package/dist/auth/session.js +28 -28
  16. package/dist/auth/session.js.map +1 -1
  17. package/dist/commands/agents-edit.d.ts +6 -2
  18. package/dist/commands/agents-edit.js +57 -26
  19. package/dist/commands/agents-edit.js.map +1 -1
  20. package/dist/commands/agents.d.ts +13 -2
  21. package/dist/commands/agents.js +148 -36
  22. package/dist/commands/agents.js.map +1 -1
  23. package/dist/commands/apps.d.ts +15 -4
  24. package/dist/commands/apps.js +133 -51
  25. package/dist/commands/apps.js.map +1 -1
  26. package/dist/commands/catalog.js +7 -6
  27. package/dist/commands/catalog.js.map +1 -1
  28. package/dist/commands/knowledge.d.ts +56 -0
  29. package/dist/commands/knowledge.js +456 -0
  30. package/dist/commands/knowledge.js.map +1 -0
  31. package/dist/commands/login.d.ts +4 -0
  32. package/dist/commands/login.js +30 -2
  33. package/dist/commands/login.js.map +1 -1
  34. package/dist/commands/logout.d.ts +9 -1
  35. package/dist/commands/logout.js +56 -7
  36. package/dist/commands/logout.js.map +1 -1
  37. package/dist/commands/publish.js +3 -3
  38. package/dist/commands/publish.js.map +1 -1
  39. package/dist/commands/reviewers.d.ts +2 -0
  40. package/dist/commands/reviewers.js +7 -3
  41. package/dist/commands/reviewers.js.map +1 -1
  42. package/dist/commands/routes.d.ts +2 -0
  43. package/dist/commands/routes.js +42 -9
  44. package/dist/commands/routes.js.map +1 -1
  45. package/dist/commands/runs.d.ts +13 -2
  46. package/dist/commands/runs.js +96 -15
  47. package/dist/commands/runs.js.map +1 -1
  48. package/dist/commands/scope-notice.d.ts +21 -0
  49. package/dist/commands/scope-notice.js +35 -0
  50. package/dist/commands/scope-notice.js.map +1 -0
  51. package/dist/commands/skills.d.ts +7 -1
  52. package/dist/commands/skills.js +80 -24
  53. package/dist/commands/skills.js.map +1 -1
  54. package/dist/commands/sources.d.ts +4 -0
  55. package/dist/commands/sources.js +233 -5
  56. package/dist/commands/sources.js.map +1 -1
  57. package/dist/commands/status.js +5 -4
  58. package/dist/commands/status.js.map +1 -1
  59. package/dist/commands/team-agent-readiness.d.ts +59 -0
  60. package/dist/commands/team-agent-readiness.js +341 -0
  61. package/dist/commands/team-agent-readiness.js.map +1 -0
  62. package/dist/commands/team-agents-agents.d.ts +24 -3
  63. package/dist/commands/team-agents-agents.js +336 -35
  64. package/dist/commands/team-agents-agents.js.map +1 -1
  65. package/dist/commands/team-agents-mcp.js +32 -3
  66. package/dist/commands/team-agents-mcp.js.map +1 -1
  67. package/dist/commands/team-agents-triggers.js +230 -54
  68. package/dist/commands/team-agents-triggers.js.map +1 -1
  69. package/dist/commands/team-agents.d.ts +3 -1
  70. package/dist/commands/team-agents.js +27 -7
  71. package/dist/commands/team-agents.js.map +1 -1
  72. package/dist/commands/teams-channel-install.d.ts +24 -0
  73. package/dist/commands/teams-channel-install.js +94 -0
  74. package/dist/commands/teams-channel-install.js.map +1 -0
  75. package/dist/commands/teams-connect.d.ts +66 -0
  76. package/dist/commands/teams-connect.js +397 -0
  77. package/dist/commands/teams-connect.js.map +1 -0
  78. package/dist/commands/teams-events.d.ts +12 -0
  79. package/dist/commands/teams-events.js +138 -0
  80. package/dist/commands/teams-events.js.map +1 -0
  81. package/dist/commands/teams-onboard.d.ts +16 -5
  82. package/dist/commands/teams-onboard.js +61 -36
  83. package/dist/commands/teams-onboard.js.map +1 -1
  84. package/dist/commands/teams-overview.d.ts +11 -0
  85. package/dist/commands/teams-overview.js +160 -0
  86. package/dist/commands/teams-overview.js.map +1 -0
  87. package/dist/commands/teams-scope.d.ts +10 -0
  88. package/dist/commands/teams-scope.js +109 -0
  89. package/dist/commands/teams-scope.js.map +1 -0
  90. package/dist/commands/teams.d.ts +64 -7
  91. package/dist/commands/teams.js +254 -60
  92. package/dist/commands/teams.js.map +1 -1
  93. package/dist/commands/upgrade.js +12 -2
  94. package/dist/commands/upgrade.js.map +1 -1
  95. package/dist/commands/use.js +2 -2
  96. package/dist/commands/use.js.map +1 -1
  97. package/dist/commands/whoami.js +1 -1
  98. package/dist/commands/whoami.js.map +1 -1
  99. package/dist/connections-api.js +1 -1
  100. package/dist/connections-api.js.map +1 -1
  101. package/dist/delegate.js +11 -4
  102. package/dist/delegate.js.map +1 -1
  103. package/dist/harness-scaffold.js +12 -0
  104. package/dist/harness-scaffold.js.map +1 -1
  105. package/dist/index.d.ts +6 -0
  106. package/dist/index.js +724 -152
  107. package/dist/index.js.map +1 -1
  108. package/dist/knowledge-api.d.ts +175 -0
  109. package/dist/knowledge-api.js +169 -0
  110. package/dist/knowledge-api.js.map +1 -0
  111. package/dist/open-url.d.ts +16 -0
  112. package/dist/open-url.js +73 -0
  113. package/dist/open-url.js.map +1 -0
  114. package/dist/parse-argv.d.ts +2 -0
  115. package/dist/parse-argv.js.map +1 -1
  116. package/dist/publish-api.d.ts +2 -1
  117. package/dist/publish-api.js +4 -3
  118. package/dist/publish-api.js.map +1 -1
  119. package/dist/pull-write.d.ts +27 -0
  120. package/dist/pull-write.js +60 -0
  121. package/dist/pull-write.js.map +1 -0
  122. package/dist/routes-api.d.ts +3 -0
  123. package/dist/routes-api.js +9 -4
  124. package/dist/routes-api.js.map +1 -1
  125. package/dist/runs-api.d.ts +7 -1
  126. package/dist/runs-api.js +21 -6
  127. package/dist/runs-api.js.map +1 -1
  128. package/dist/safe-fs.d.ts +2 -0
  129. package/dist/safe-fs.js +12 -6
  130. package/dist/safe-fs.js.map +1 -1
  131. package/dist/secret-file.d.ts +17 -0
  132. package/dist/secret-file.js +69 -0
  133. package/dist/secret-file.js.map +1 -0
  134. package/dist/skills-api.d.ts +10 -2
  135. package/dist/skills-api.js +37 -2
  136. package/dist/skills-api.js.map +1 -1
  137. package/dist/source-products-api.d.ts +63 -0
  138. package/dist/source-products-api.js +123 -0
  139. package/dist/source-products-api.js.map +1 -0
  140. package/dist/source-target.js +2 -1
  141. package/dist/source-target.js.map +1 -1
  142. package/dist/sources-api.d.ts +2 -1
  143. package/dist/sources-api.js +3 -2
  144. package/dist/sources-api.js.map +1 -1
  145. package/dist/team-connect-api.d.ts +286 -0
  146. package/dist/team-connect-api.js +107 -0
  147. package/dist/team-connect-api.js.map +1 -0
  148. package/dist/teams-api.d.ts +42 -1
  149. package/dist/teams-api.js +98 -20
  150. package/dist/teams-api.js.map +1 -1
  151. package/dist/toolchain-api.d.ts +9 -0
  152. package/dist/toolchain-api.js +17 -2
  153. package/dist/toolchain-api.js.map +1 -1
  154. package/dist/toolchain-resolve.d.ts +3 -0
  155. package/dist/toolchain-resolve.js +15 -1
  156. package/dist/toolchain-resolve.js.map +1 -1
  157. package/dist/toolchain.d.ts +6 -0
  158. package/dist/toolchain.js +19 -0
  159. package/dist/toolchain.js.map +1 -1
  160. package/dist/update-notice.js +2 -1
  161. package/dist/update-notice.js.map +1 -1
  162. package/package.json +2 -2
  163. package/templates/AGENTS.md.tmpl +6 -1
  164. package/templates/claude-skill.md.tmpl +5 -0
  165. package/templates/package.json.tmpl +4 -4
  166. package/templates/skills/build-agent/SKILL.md +115 -16
  167. package/templates/skills/build-agent/references/agent-json.md +26 -8
  168. package/templates/skills/build-agent/references/app-setup.md +2 -1
  169. package/templates/skills/build-agent/references/rollout.md +27 -4
  170. package/templates/skills/build-agent/references/set-up-an-integration.md +74 -15
  171. package/templates/skills/build-agent/references/skills.md +20 -1
  172. package/templates/skills/build-agent/references/triggers.md +84 -20
  173. package/templates/skills/build-integration/references/widget-design.md +3 -3
  174. package/templates/skills/ekanos-platform/SKILL.md +70 -17
  175. package/templates/skills/polish-widgets/references/patterns.md +2 -2
@@ -0,0 +1,169 @@
1
+ import { z } from 'zod';
2
+ import { request } from './auth/ekanos-api.js';
3
+ import { authRequiredError, forbiddenError, networkError, notFoundError, usageError, } from './errors.js';
4
+ /**
5
+ * The CLI's client for a Source's Knowledge (plan 244): the General
6
+ * collection every team reads, and each team's own.
7
+ *
8
+ * GET /api/partner/knowledge?source=<slug>[&team=<slug>][&body=1]
9
+ * POST /api/partner/knowledge { source, team?, origin?, articles: [...] }
10
+ * POST /api/partner/knowledge/prune { source, team?, origin, keep, delete? }
11
+ * GET /api/partner/knowledge/search?source=<slug>&team=<slug>&q=<query>[&limit=<n>]
12
+ *
13
+ * → 401 / 3xx refresh once and retry
14
+ * → 403 { code: "SOURCE_ADMIN_REQUIRED" | "MFA_REQUIRED" | "FORBIDDEN" }
15
+ * → 404 { code: "TEAM_NOT_FOUND" | "SOURCE_NOT_FOUND" }
16
+ * → 400 { code: "INVALID_KNOWLEDGE" | "TEAM_REQUIRED" | "QUERY_REQUIRED" }
17
+ *
18
+ * The contract's shapes live here rather than in `@ekanos/integration-schema`,
19
+ * so the knowledge commands ship with the CLI alone.
20
+ */
21
+ const SourceRefSchema = z.object({
22
+ id: z.string(),
23
+ slug: z.string(),
24
+ name: z.string().nullable(),
25
+ });
26
+ const TeamRefSchema = z
27
+ .object({ slug: z.string(), name: z.string() })
28
+ .nullable();
29
+ /**
30
+ * A slug `knowledge pull` may turn into `<slug>.md` under its folder: the
31
+ * host's `knowledge_articles.slug` shape (`[a-z0-9][a-z0-9._/-]*`, at most
32
+ * 200), with no empty, `.`-led (so no `.` or `..`) folder. The DB CHECK alone
33
+ * admits `a/../b`; a slug is host-supplied, so the CLI holds the line itself.
34
+ */
35
+ export function isSafeKnowledgeSlug(slug) {
36
+ if (!/^[a-z0-9][a-z0-9._/-]{0,199}$/.test(slug))
37
+ return false;
38
+ return slug.split('/').every((part) => part !== '' && !part.startsWith('.'));
39
+ }
40
+ const ArticleSchema = z.object({
41
+ id: z.string(),
42
+ slug: z.string(),
43
+ title: z.string(),
44
+ tags: z.array(z.string()),
45
+ version: z.number(),
46
+ updatedAt: z.string(),
47
+ body: z.string().optional(),
48
+ });
49
+ export const KnowledgeListSchema = z.object({
50
+ source: SourceRefSchema,
51
+ team: TeamRefSchema,
52
+ articles: z.array(ArticleSchema),
53
+ });
54
+ const PushResultSchema = z.object({
55
+ slug: z.string(),
56
+ id: z.string().optional(),
57
+ status: z.enum(['created', 'updated', 'unchanged', 'refused']),
58
+ version: z.number().optional(),
59
+ message: z.string().optional(),
60
+ });
61
+ export const KnowledgePushSchema = z.object({
62
+ source: SourceRefSchema,
63
+ team: TeamRefSchema,
64
+ results: z.array(PushResultSchema),
65
+ });
66
+ const HitSchema = z.object({
67
+ id: z.string(),
68
+ slug: z.string(),
69
+ title: z.string(),
70
+ tags: z.array(z.string()),
71
+ collection: z.enum(['team', 'general']),
72
+ snippet: z.string(),
73
+ rank: z.number(),
74
+ updatedAt: z.string(),
75
+ });
76
+ export const KnowledgeSearchSchema = z.object({
77
+ source: SourceRefSchema,
78
+ team: TeamRefSchema,
79
+ query: z.string(),
80
+ results: z.array(HitSchema),
81
+ });
82
+ const PruneArticleSchema = z.object({
83
+ slug: z.string(),
84
+ title: z.string(),
85
+ version: z.number(),
86
+ });
87
+ /**
88
+ * `push --prune`: `candidates` are the articles an earlier push of `origin`
89
+ * made whose files are gone and that nobody edited since; `edited` the ones
90
+ * kept because someone did; `deleted` what this call removed (none for a
91
+ * preview, which sends no `delete`).
92
+ */
93
+ export const KnowledgePruneSchema = z.object({
94
+ source: SourceRefSchema,
95
+ team: TeamRefSchema,
96
+ origin: z.string(),
97
+ candidates: z.array(PruneArticleSchema),
98
+ edited: z.array(PruneArticleSchema),
99
+ deleted: z.array(PruneArticleSchema),
100
+ });
101
+ /** Articles per POST; the host refuses more. */
102
+ export const KNOWLEDGE_PUSH_BATCH = 50;
103
+ const BASE = '/api/partner/knowledge';
104
+ export async function knowledgeCall(host, accessToken, schema, init) {
105
+ var _a;
106
+ const query = Object.fromEntries(Object.entries(init.query).filter((entry) => entry[1] !== undefined));
107
+ const url = `${host}${BASE}${(_a = init.path) !== null && _a !== void 0 ? _a : ''}?${new URLSearchParams(query)}`;
108
+ const response = await request(url, {
109
+ method: init.method,
110
+ headers: Object.assign({ accept: 'application/json', authorization: `Bearer ${accessToken}` }, (init.body === undefined
111
+ ? {}
112
+ : { 'content-type': 'application/json' })),
113
+ body: init.body === undefined ? undefined : JSON.stringify(init.body),
114
+ });
115
+ if (response.status === 401 ||
116
+ (response.status >= 300 && response.status < 400)) {
117
+ return { status: 'unauthorized' };
118
+ }
119
+ const body = await readOptionalJson(response);
120
+ const code = typeof body.code === 'string' ? body.code : null;
121
+ const message = typeof body.error === 'string' && body.error ? body.error : null;
122
+ if (response.status === 403 && code === 'MFA_REQUIRED') {
123
+ throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
124
+ }
125
+ if (response.status === 403) {
126
+ throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
127
+ ? 'Only a Source admin manages Knowledge from the CLI. Ask one to run it.'
128
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
129
+ }
130
+ if (response.status === 404) {
131
+ throw notFoundError(message !== null && message !== void 0 ? message : 'Not found.', code === 'TEAM_NOT_FOUND'
132
+ ? 'Run "ekanos teams list" to see the teams on this Source.'
133
+ : 'Run "ekanos sources" to list the sources you hold a seat on.');
134
+ }
135
+ if (response.status === 400) {
136
+ const issues = Array.isArray(body.issues)
137
+ ? body.issues
138
+ .map((issue) => `${String(issue.path)}: ${String(issue.message)}`)
139
+ .join('; ')
140
+ : null;
141
+ throw usageError([message !== null && message !== void 0 ? message : 'The host refused the request.', issues]
142
+ .filter(Boolean)
143
+ .join(' — '), code === 'TEAM_REQUIRED'
144
+ ? 'Pass --team <slug>: search runs as that team sees it.'
145
+ : 'See "ekanos knowledge --help".');
146
+ }
147
+ if (!response.ok) {
148
+ throw networkError(`${host}${BASE} responded ${response.status}.`, response.status >= 500
149
+ ? 'The Ekanos deployment returned a server error. Retry shortly.'
150
+ : 'Confirm the host runs a build that includes the knowledge route.');
151
+ }
152
+ const parsed = schema.safeParse(body.data);
153
+ if (!parsed.success) {
154
+ throw networkError(`${host}${BASE} answered in a shape this CLI does not read.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the knowledge contract.');
155
+ }
156
+ return { status: 'ok', data: parsed.data };
157
+ }
158
+ async function readOptionalJson(response) {
159
+ try {
160
+ const parsed = await response.json();
161
+ return typeof parsed === 'object' && parsed !== null
162
+ ? parsed
163
+ : {};
164
+ }
165
+ catch (_a) {
166
+ return {};
167
+ }
168
+ }
169
+ //# sourceMappingURL=knowledge-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-api.js","sourceRoot":"","sources":["../src/knowledge-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's Knowledge (plan 244): the General\n * collection every team reads, and each team's own.\n *\n * GET /api/partner/knowledge?source=<slug>[&team=<slug>][&body=1]\n * POST /api/partner/knowledge { source, team?, origin?, articles: [...] }\n * POST /api/partner/knowledge/prune { source, team?, origin, keep, delete? }\n * GET /api/partner/knowledge/search?source=<slug>&team=<slug>&q=<query>[&limit=<n>]\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"TEAM_NOT_FOUND\" | \"SOURCE_NOT_FOUND\" }\n * → 400 { code: \"INVALID_KNOWLEDGE\" | \"TEAM_REQUIRED\" | \"QUERY_REQUIRED\" }\n *\n * The contract's shapes live here rather than in `@ekanos/integration-schema`,\n * so the knowledge commands ship with the CLI alone.\n */\n\nconst SourceRefSchema = z.object({\n id: z.string(),\n slug: z.string(),\n name: z.string().nullable(),\n});\n\nconst TeamRefSchema = z\n .object({ slug: z.string(), name: z.string() })\n .nullable();\n\n/**\n * A slug `knowledge pull` may turn into `<slug>.md` under its folder: the\n * host's `knowledge_articles.slug` shape (`[a-z0-9][a-z0-9._/-]*`, at most\n * 200), with no empty, `.`-led (so no `.` or `..`) folder. The DB CHECK alone\n * admits `a/../b`; a slug is host-supplied, so the CLI holds the line itself.\n */\nexport function isSafeKnowledgeSlug(slug: string): boolean {\n if (!/^[a-z0-9][a-z0-9._/-]{0,199}$/.test(slug)) return false;\n return slug.split('/').every((part) => part !== '' && !part.startsWith('.'));\n}\n\nconst ArticleSchema = z.object({\n id: z.string(),\n slug: z.string(),\n title: z.string(),\n tags: z.array(z.string()),\n version: z.number(),\n updatedAt: z.string(),\n body: z.string().optional(),\n});\nexport type KnowledgeArticle = z.infer<typeof ArticleSchema>;\n\nexport const KnowledgeListSchema = z.object({\n source: SourceRefSchema,\n team: TeamRefSchema,\n articles: z.array(ArticleSchema),\n});\nexport type KnowledgeList = z.infer<typeof KnowledgeListSchema>;\n\nconst PushResultSchema = z.object({\n slug: z.string(),\n id: z.string().optional(),\n status: z.enum(['created', 'updated', 'unchanged', 'refused']),\n version: z.number().optional(),\n message: z.string().optional(),\n});\nexport type KnowledgePushResult = z.infer<typeof PushResultSchema>;\n\nexport const KnowledgePushSchema = z.object({\n source: SourceRefSchema,\n team: TeamRefSchema,\n results: z.array(PushResultSchema),\n});\nexport type KnowledgePush = z.infer<typeof KnowledgePushSchema>;\n\nconst HitSchema = z.object({\n id: z.string(),\n slug: z.string(),\n title: z.string(),\n tags: z.array(z.string()),\n collection: z.enum(['team', 'general']),\n snippet: z.string(),\n rank: z.number(),\n updatedAt: z.string(),\n});\nexport type KnowledgeHit = z.infer<typeof HitSchema>;\n\nexport const KnowledgeSearchSchema = z.object({\n source: SourceRefSchema,\n team: TeamRefSchema,\n query: z.string(),\n results: z.array(HitSchema),\n});\n\nconst PruneArticleSchema = z.object({\n slug: z.string(),\n title: z.string(),\n version: z.number(),\n});\nexport type KnowledgePruneArticle = z.infer<typeof PruneArticleSchema>;\n\n/**\n * `push --prune`: `candidates` are the articles an earlier push of `origin`\n * made whose files are gone and that nobody edited since; `edited` the ones\n * kept because someone did; `deleted` what this call removed (none for a\n * preview, which sends no `delete`).\n */\nexport const KnowledgePruneSchema = z.object({\n source: SourceRefSchema,\n team: TeamRefSchema,\n origin: z.string(),\n candidates: z.array(PruneArticleSchema),\n edited: z.array(PruneArticleSchema),\n deleted: z.array(PruneArticleSchema),\n});\n\nexport interface KnowledgePushArticle {\n slug: string;\n title: string;\n body: string;\n tags?: string[];\n}\n\n/** Articles per POST; the host refuses more. */\nexport const KNOWLEDGE_PUSH_BATCH = 50;\n\nexport type KnowledgeApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst BASE = '/api/partner/knowledge';\n\nexport async function knowledgeCall<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 | undefined>;\n body?: unknown;\n },\n): Promise<KnowledgeApiResult<T>> {\n const query = Object.fromEntries(\n Object.entries(init.query).filter(\n (entry): entry is [string, string] => entry[1] !== undefined,\n ),\n );\n const url = `${host}${BASE}${init.path ?? ''}?${new URLSearchParams(query)}`;\n const response = await request(url, {\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 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 manages Knowledge from the CLI. Ask one to run it.'\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 : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 400) {\n const issues = Array.isArray(body.issues)\n ? (body.issues as { path?: unknown; message?: unknown }[])\n .map((issue) => `${String(issue.path)}: ${String(issue.message)}`)\n .join('; ')\n : null;\n throw usageError(\n [message ?? 'The host refused the request.', issues]\n .filter(Boolean)\n .join(' — '),\n code === 'TEAM_REQUIRED'\n ? 'Pass --team <slug>: search runs as that team sees it.'\n : 'See \"ekanos knowledge --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${BASE} 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 knowledge route.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${BASE} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the knowledge 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"]}
@@ -0,0 +1,16 @@
1
+ /** True when `url` is a link the CLI is willing to open in a browser. */
2
+ export declare function isBrowserSafeUrl(url: string): boolean;
3
+ /**
4
+ * Refuse a host-supplied link the CLI would not open. Called before the link
5
+ * is printed or opened, so a hostile host cannot hand the person a `file:`
6
+ * link either way.
7
+ */
8
+ export declare function assertBrowserSafeUrl(url: string, what: string): void;
9
+ /** The command and argv that open `url` on `platform` — never a shell. */
10
+ export declare function openerFor(url: string, platform?: NodeJS.Platform): [string, string[]];
11
+ /**
12
+ * Best effort: callers always print the link, so a failed open costs nothing.
13
+ * An unsafe URL is never handed to the OS, even if a caller skipped
14
+ * `assertBrowserSafeUrl`.
15
+ */
16
+ export declare function openInBrowser(url: string): void;
@@ -0,0 +1,73 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { invalidStateError } from './errors.js';
3
+ /**
4
+ * Opening a link the host handed us. The URL is server-supplied, so two things
5
+ * must hold before it reaches the OS:
6
+ *
7
+ * 1. **Scheme.** Only `https:` — or `http:` to a loopback host, for a local
8
+ * dev server. A `file:`, `javascript:` or custom-scheme link opened by the
9
+ * platform handler can launch a local program.
10
+ * 2. **No shell.** Every opener is spawned with an argv array. On Windows
11
+ * `cmd /c start` would reparse `& | ^` in the URL — running whatever
12
+ * follows, and truncating a real OAuth URL at its first `&` — so the
13
+ * link goes to `rundll32 url.dll,FileProtocolHandler` instead.
14
+ */
15
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
16
+ function isLoopbackHost(hostname) {
17
+ return LOOPBACK_HOSTS.has(hostname) || hostname.endsWith('.localhost');
18
+ }
19
+ /** True when `url` is a link the CLI is willing to open in a browser. */
20
+ export function isBrowserSafeUrl(url) {
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(url);
24
+ }
25
+ catch (_a) {
26
+ return false;
27
+ }
28
+ if (parsed.protocol === 'https:')
29
+ return true;
30
+ return parsed.protocol === 'http:' && isLoopbackHost(parsed.hostname);
31
+ }
32
+ /**
33
+ * Refuse a host-supplied link the CLI would not open. Called before the link
34
+ * is printed or opened, so a hostile host cannot hand the person a `file:`
35
+ * link either way.
36
+ */
37
+ export function assertBrowserSafeUrl(url, what) {
38
+ if (isBrowserSafeUrl(url))
39
+ return;
40
+ throw invalidStateError(`The host returned a ${what} that is not an https link, so it was not opened.`, '"ekanos whoami" shows which host you are signed in to; an Ekanos host only ever returns https links.');
41
+ }
42
+ /** The command and argv that open `url` on `platform` — never a shell. */
43
+ export function openerFor(url, platform = process.platform) {
44
+ if (platform === 'darwin')
45
+ return ['open', [url]];
46
+ if (platform === 'win32') {
47
+ return ['rundll32', ['url.dll,FileProtocolHandler', url]];
48
+ }
49
+ return ['xdg-open', [url]];
50
+ }
51
+ /**
52
+ * Best effort: callers always print the link, so a failed open costs nothing.
53
+ * An unsafe URL is never handed to the OS, even if a caller skipped
54
+ * `assertBrowserSafeUrl`.
55
+ */
56
+ export function openInBrowser(url) {
57
+ if (!isBrowserSafeUrl(url))
58
+ return;
59
+ const [command, argv] = openerFor(url);
60
+ try {
61
+ const child = spawn(command, argv, {
62
+ detached: true,
63
+ stdio: 'ignore',
64
+ shell: false,
65
+ });
66
+ child.on('error', () => { });
67
+ child.unref();
68
+ }
69
+ catch (_a) {
70
+ // Printed by the caller; nothing to do.
71
+ }
72
+ }
73
+ //# sourceMappingURL=open-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-url.js","sourceRoot":"","sources":["../src/open-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;;;;;GAWG;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC;AAEpE,SAAS,cAAc,CAAC,QAAgB;IACtC,OAAO,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;AACzE,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;eAAO,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,IAAY;IAC5D,IAAI,gBAAgB,CAAC,GAAG,CAAC;QAAE,OAAO;IAClC,MAAM,iBAAiB,CACrB,uBAAuB,IAAI,mDAAmD,EAC9E,sGAAsG,CACvG,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CACvB,GAAW,EACX,QAAQ,GAAoB,OAAO,CAAC,QAAQ;IAE5C,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,CAAC,UAAU,EAAE,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC;QAAE,OAAO;IACnC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YACjC,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,QAAQ;YACf,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;QACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5B,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;eAAO,CAAC;QACP,wCAAwC;IAC1C,CAAC;AACH,CAAC","sourcesContent":["import { spawn } from 'node:child_process';\n\nimport { invalidStateError } from './errors';\n\n/**\n * Opening a link the host handed us. The URL is server-supplied, so two things\n * must hold before it reaches the OS:\n *\n * 1. **Scheme.** Only `https:` — or `http:` to a loopback host, for a local\n * dev server. A `file:`, `javascript:` or custom-scheme link opened by the\n * platform handler can launch a local program.\n * 2. **No shell.** Every opener is spawned with an argv array. On Windows\n * `cmd /c start` would reparse `& | ^` in the URL — running whatever\n * follows, and truncating a real OAuth URL at its first `&` — so the\n * link goes to `rundll32 url.dll,FileProtocolHandler` instead.\n */\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);\n\nfunction isLoopbackHost(hostname: string): boolean {\n return LOOPBACK_HOSTS.has(hostname) || hostname.endsWith('.localhost');\n}\n\n/** True when `url` is a link the CLI is willing to open in a browser. */\nexport function isBrowserSafeUrl(url: string): boolean {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return false;\n }\n if (parsed.protocol === 'https:') return true;\n return parsed.protocol === 'http:' && isLoopbackHost(parsed.hostname);\n}\n\n/**\n * Refuse a host-supplied link the CLI would not open. Called before the link\n * is printed or opened, so a hostile host cannot hand the person a `file:`\n * link either way.\n */\nexport function assertBrowserSafeUrl(url: string, what: string): void {\n if (isBrowserSafeUrl(url)) return;\n throw invalidStateError(\n `The host returned a ${what} that is not an https link, so it was not opened.`,\n '\"ekanos whoami\" shows which host you are signed in to; an Ekanos host only ever returns https links.',\n );\n}\n\n/** The command and argv that open `url` on `platform` — never a shell. */\nexport function openerFor(\n url: string,\n platform: NodeJS.Platform = process.platform,\n): [string, string[]] {\n if (platform === 'darwin') return ['open', [url]];\n if (platform === 'win32') {\n return ['rundll32', ['url.dll,FileProtocolHandler', url]];\n }\n return ['xdg-open', [url]];\n}\n\n/**\n * Best effort: callers always print the link, so a failed open costs nothing.\n * An unsafe URL is never handed to the OS, even if a caller skipped\n * `assertBrowserSafeUrl`.\n */\nexport function openInBrowser(url: string): void {\n if (!isBrowserSafeUrl(url)) return;\n const [command, argv] = openerFor(url);\n try {\n const child = spawn(command, argv, {\n detached: true,\n stdio: 'ignore',\n shell: false,\n });\n child.on('error', () => {});\n child.unref();\n } catch {\n // Printed by the caller; nothing to do.\n }\n}\n"]}
@@ -20,6 +20,8 @@ export interface FlagSpec {
20
20
  * flag has no `default` — "absent" and "empty" must stay distinguishable.
21
21
  */
22
22
  repeatable?: boolean;
23
+ /** One line for `<command> --help`. */
24
+ description?: string;
23
25
  }
24
26
  export type FlagSpecs = Record<string, FlagSpec>;
25
27
  export interface ParsedArgs {
@@ -1 +1 @@
1
- {"version":3,"file":"parse-argv.js","sourceRoot":"","sources":["../src/parse-argv.ts"],"names":[],"mappings":"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","sourcesContent":["import { usageError } from './errors';\n\n/**\n * A hand-rolled argv parser — deliberately NOT commander/yargs/citty. The\n * agent-native envelope needs total control over how an unknown flag is\n * reported (exit 2 through the JSON envelope with a hint, never a library's own\n * `console.error` + `process.exit`), and it must never abbreviate, never\n * auto-print help, and never guess. Every off-ramp routes through `usageError`.\n *\n * Supports: `--flag` (boolean), `--key=value`, `--key value`, `--no-<bool>`\n * negation, positionals, and a `--` passthrough where everything after the\n * first bare `--` is captured verbatim (that argv belongs to `test`'s child).\n */\n\nexport type FlagType = 'boolean' | 'string';\n\nexport interface FlagSpec {\n type: FlagType;\n /** Optional default applied when the flag is absent. */\n default?: string | boolean;\n /**\n * String flags only: every occurrence appends, and the parsed value is a\n * `string[]` (absent entirely when the flag was never passed). A repeatable\n * flag has no `default` — \"absent\" and \"empty\" must stay distinguishable.\n */\n repeatable?: boolean;\n}\n\nexport type FlagSpecs = Record<string, FlagSpec>;\n\nexport interface ParsedArgs {\n positionals: string[];\n flags: Record<string, string | boolean | string[]>;\n /** Raw argv after the first standalone `--`, unparsed. */\n passthrough: string[];\n /** True when a standalone `--` was present (even with nothing after it). */\n hasPassthrough: boolean;\n}\n\nexport function parseArgv(\n argv: readonly string[],\n specs: FlagSpecs,\n): ParsedArgs {\n const positionals: string[] = [];\n const flags: Record<string, string | boolean | string[]> = {};\n const passthrough: string[] = [];\n let hasPassthrough = false;\n\n const assignString = (name: string, spec: FlagSpec, value: string) => {\n if (!spec.repeatable) {\n flags[name] = value;\n return;\n }\n const existing = flags[name];\n flags[name] = Array.isArray(existing) ? [...existing, value] : [value];\n };\n\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n\n // Everything after the first standalone `--` is passthrough, verbatim.\n if (token === '--') {\n hasPassthrough = true;\n passthrough.push(...argv.slice(i + 1));\n break;\n }\n\n // A bare positional (does not start with `-`), or a lone `-`.\n if (!token.startsWith('--')) {\n if (token.startsWith('-') && token !== '-') {\n throw usageError(\n `Unknown flag \"${token}\". Short flags are not supported.`,\n `Use the long form (e.g. \"--json\"). Run \"ekanos --help\" for the ` +\n `full flag list.`,\n );\n }\n positionals.push(token);\n continue;\n }\n\n // A long flag. Split an inline `--key=value`.\n const body = token.slice(2);\n const eq = body.indexOf('=');\n const hasInlineValue = eq !== -1;\n let name = hasInlineValue ? body.slice(0, eq) : body;\n const inlineValue = hasInlineValue ? body.slice(eq + 1) : undefined;\n\n // `--no-<bool>` negation.\n let negated = false;\n if (!hasInlineValue && name.startsWith('no-')) {\n const candidate = name.slice(3);\n if (specs[candidate]?.type === 'boolean') {\n negated = true;\n name = candidate;\n }\n }\n\n const spec = specs[name];\n if (!spec) {\n throw usageError(\n `Unknown flag \"--${hasInlineValue ? name : body}\".`,\n `Remove it or check the spelling. Run \"ekanos --help\" (or ` +\n `\"ekanos <command> --help\") for the accepted flags.`,\n );\n }\n\n if (spec.type === 'boolean') {\n if (hasInlineValue) {\n throw usageError(\n `Flag \"--${name}\" is a boolean and does not take a value.`,\n `Pass \"--${name}\" to enable it or \"--no-${name}\" to disable it — ` +\n `not \"--${name}=${inlineValue ?? ''}\".`,\n );\n }\n flags[name] = !negated;\n continue;\n }\n\n // String flag: value is inline (`--key=value`) or the next token\n // (`--key value`).\n if (hasInlineValue) {\n assignString(name, spec, inlineValue!);\n continue;\n }\n\n const next = argv[i + 1];\n if (next === undefined || next === '--') {\n throw usageError(\n `Flag \"--${name}\" expects a value.`,\n `Provide one as \"--${name} <value>\" or \"--${name}=<value>\".`,\n );\n }\n assignString(name, spec, next);\n i++;\n }\n\n // Apply declared defaults for flags the user did not pass.\n for (const [name, spec] of Object.entries(specs)) {\n if (spec.default !== undefined && !(name in flags)) {\n flags[name] = spec.default;\n }\n }\n\n return { positionals, flags, passthrough, hasPassthrough };\n}\n"]}
1
+ {"version":3,"file":"parse-argv.js","sourceRoot":"","sources":["../src/parse-argv.ts"],"names":[],"mappings":"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","sourcesContent":["import { usageError } from './errors';\n\n/**\n * A hand-rolled argv parser — deliberately NOT commander/yargs/citty. The\n * agent-native envelope needs total control over how an unknown flag is\n * reported (exit 2 through the JSON envelope with a hint, never a library's own\n * `console.error` + `process.exit`), and it must never abbreviate, never\n * auto-print help, and never guess. Every off-ramp routes through `usageError`.\n *\n * Supports: `--flag` (boolean), `--key=value`, `--key value`, `--no-<bool>`\n * negation, positionals, and a `--` passthrough where everything after the\n * first bare `--` is captured verbatim (that argv belongs to `test`'s child).\n */\n\nexport type FlagType = 'boolean' | 'string';\n\nexport interface FlagSpec {\n type: FlagType;\n /** Optional default applied when the flag is absent. */\n default?: string | boolean;\n /**\n * String flags only: every occurrence appends, and the parsed value is a\n * `string[]` (absent entirely when the flag was never passed). A repeatable\n * flag has no `default` — \"absent\" and \"empty\" must stay distinguishable.\n */\n repeatable?: boolean;\n /** One line for `<command> --help`. */\n description?: string;\n}\n\nexport type FlagSpecs = Record<string, FlagSpec>;\n\nexport interface ParsedArgs {\n positionals: string[];\n flags: Record<string, string | boolean | string[]>;\n /** Raw argv after the first standalone `--`, unparsed. */\n passthrough: string[];\n /** True when a standalone `--` was present (even with nothing after it). */\n hasPassthrough: boolean;\n}\n\nexport function parseArgv(\n argv: readonly string[],\n specs: FlagSpecs,\n): ParsedArgs {\n const positionals: string[] = [];\n const flags: Record<string, string | boolean | string[]> = {};\n const passthrough: string[] = [];\n let hasPassthrough = false;\n\n const assignString = (name: string, spec: FlagSpec, value: string) => {\n if (!spec.repeatable) {\n flags[name] = value;\n return;\n }\n const existing = flags[name];\n flags[name] = Array.isArray(existing) ? [...existing, value] : [value];\n };\n\n for (let i = 0; i < argv.length; i++) {\n const token = argv[i]!;\n\n // Everything after the first standalone `--` is passthrough, verbatim.\n if (token === '--') {\n hasPassthrough = true;\n passthrough.push(...argv.slice(i + 1));\n break;\n }\n\n // A bare positional (does not start with `-`), or a lone `-`.\n if (!token.startsWith('--')) {\n if (token.startsWith('-') && token !== '-') {\n throw usageError(\n `Unknown flag \"${token}\". Short flags are not supported.`,\n `Use the long form (e.g. \"--json\"). Run \"ekanos --help\" for the ` +\n `full flag list.`,\n );\n }\n positionals.push(token);\n continue;\n }\n\n // A long flag. Split an inline `--key=value`.\n const body = token.slice(2);\n const eq = body.indexOf('=');\n const hasInlineValue = eq !== -1;\n let name = hasInlineValue ? body.slice(0, eq) : body;\n const inlineValue = hasInlineValue ? body.slice(eq + 1) : undefined;\n\n // `--no-<bool>` negation.\n let negated = false;\n if (!hasInlineValue && name.startsWith('no-')) {\n const candidate = name.slice(3);\n if (specs[candidate]?.type === 'boolean') {\n negated = true;\n name = candidate;\n }\n }\n\n const spec = specs[name];\n if (!spec) {\n throw usageError(\n `Unknown flag \"--${hasInlineValue ? name : body}\".`,\n `Remove it or check the spelling. Run \"ekanos --help\" (or ` +\n `\"ekanos <command> --help\") for the accepted flags.`,\n );\n }\n\n if (spec.type === 'boolean') {\n if (hasInlineValue) {\n throw usageError(\n `Flag \"--${name}\" is a boolean and does not take a value.`,\n `Pass \"--${name}\" to enable it or \"--no-${name}\" to disable it — ` +\n `not \"--${name}=${inlineValue ?? ''}\".`,\n );\n }\n flags[name] = !negated;\n continue;\n }\n\n // String flag: value is inline (`--key=value`) or the next token\n // (`--key value`).\n if (hasInlineValue) {\n assignString(name, spec, inlineValue!);\n continue;\n }\n\n const next = argv[i + 1];\n if (next === undefined || next === '--') {\n throw usageError(\n `Flag \"--${name}\" expects a value.`,\n `Provide one as \"--${name} <value>\" or \"--${name}=<value>\".`,\n );\n }\n assignString(name, spec, next);\n i++;\n }\n\n // Apply declared defaults for flags the user did not pass.\n for (const [name, spec] of Object.entries(specs)) {\n if (spec.default !== undefined && !(name in flags)) {\n flags[name] = spec.default;\n }\n }\n\n return { positionals, flags, passthrough, hasPassthrough };\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { AuthEnvironment } from './auth/session.js';
1
2
  /**
2
3
  * The CLI's client for the Ekanos submission intake route.
3
4
  *
@@ -157,4 +158,4 @@ export declare function listSubmissions(host: string, accessToken: string, sourc
157
158
  detail?: 'findings';
158
159
  }): Promise<ListResult>;
159
160
  /** Guard for the "still unauthorized after one refresh" terminal case. */
160
- export declare function stillUnauthorized(host: string): never;
161
+ export declare function stillUnauthorized(env: AuthEnvironment): never;
@@ -1,4 +1,5 @@
1
1
  import { request } from './auth/ekanos-api.js';
2
+ import { loginHint } from './auth/host-trust.js';
2
3
  import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, validationError, } from './errors.js';
3
4
  /**
4
5
  * The CLI's client for the Ekanos submission intake route.
@@ -152,7 +153,7 @@ export async function listSubmissions(host, accessToken, source, options = {}) {
152
153
  const serverMessage = str(body.error);
153
154
  if (response.status === 403) {
154
155
  throw forbiddenError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `${host} refused to list submissions for "${source}".`, `Complete MFA in the browser if the message names it, then run ` +
155
- `"ekanos login --host ${host}" again.`);
156
+ `"ekanos login" again.`);
156
157
  }
157
158
  if (response.status === 404) {
158
159
  throw notFoundError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `${host} has no source with slug "${source}".`, `Check the "source" field in ekanos.json (or --source) against the ` +
@@ -188,8 +189,8 @@ export async function listSubmissions(host, accessToken, source, options = {}) {
188
189
  return { status: 'ok', integrations };
189
190
  }
190
191
  /** Guard for the "still unauthorized after one refresh" terminal case. */
191
- export function stillUnauthorized(host) {
192
- throw authRequiredError(`The session for ${host} could not authorize the submission.`, `Run "ekanos login --host ${host}" and retry.`);
192
+ export function stillUnauthorized(env) {
193
+ throw authRequiredError(`The session for ${env.host} could not authorize the submission.`, loginHint(env));
193
194
  }
194
195
  async function readOptionalJson(response) {
195
196
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Ekanos submission intake route.\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/partner/submissions` route. As with\n * the auth routes (see `auth/ekanos-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Ekanos source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked,\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Ekanos route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n *\n * `origin`/`id`/`createdAt`/`resolvedAt` are the additive fields the host's\n * findings-merge endpoint puts on the wire once host-filed findings — CI\n * failures, host observations, a revocation reason — are merged in alongside\n * the gate's own verdict. A gate-only finding still omits them entirely\n * (this CLI's own `validate`/`publish` findings never set `origin`, which\n * the renderer reads as \"gate\"); they only appear on findings read back from\n * `GET /api/partner/submissions?detail=findings`.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire; kept as `string` — see the doc comment on `HostFindingRow` in `merge-findings.ts` for why the host stays defensive rather than a narrower literal union here. */\n origin?: string;\n id?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login --host ${host}\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\n );\n}\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\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\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
1
+ {"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport { loginHint } from './auth/host-trust';\nimport type { AuthEnvironment } from './auth/session';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Ekanos submission intake route.\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/partner/submissions` route. As with\n * the auth routes (see `auth/ekanos-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Ekanos source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked,\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Ekanos route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n *\n * `origin`/`id`/`createdAt`/`resolvedAt` are the additive fields the host's\n * findings-merge endpoint puts on the wire once host-filed findings — CI\n * failures, host observations, a revocation reason — are merged in alongside\n * the gate's own verdict. A gate-only finding still omits them entirely\n * (this CLI's own `validate`/`publish` findings never set `origin`, which\n * the renderer reads as \"gate\"); they only appear on findings read back from\n * `GET /api/partner/submissions?detail=findings`.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire; kept as `string` — see the doc comment on `HostFindingRow` in `merge-findings.ts` for why the host stays defensive rather than a narrower literal union here. */\n origin?: string;\n id?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(env: AuthEnvironment): never {\n throw authRequiredError(\n `The session for ${env.host} could not authorize the submission.`,\n loginHint(env),\n );\n}\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\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\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Writing what `skills pull`, `knowledge pull` and `agents pull` fetched.
3
+ * Every path comes from the host, and the folder they land in may hold the
4
+ * person's own edits, so:
5
+ *
6
+ * - **Contained.** Each file resolves strictly inside `outDir`.
7
+ * - **No symlinks.** `outDir` itself is lstat'd, every component below the
8
+ * project (or below `outDir`, for an `--out` outside it) is checked, and
9
+ * each write is `O_NOFOLLOW` — a symlinked `skills/` or a planted link at
10
+ * a leaf cannot redirect a write.
11
+ * - **No silent overwrite.** An existing file whose bytes differ is a local
12
+ * edit; the pull refuses before writing anything unless `force`.
13
+ */
14
+ export interface PulledFile {
15
+ /** `/`-separated, relative to `outDir`. */
16
+ path: string;
17
+ contents: string | Uint8Array;
18
+ }
19
+ export interface WritePulledFilesParams {
20
+ projectDir: string;
21
+ outDir: string;
22
+ files: readonly PulledFile[];
23
+ force: boolean;
24
+ /** The command to re-run with `--force`, for the refusal's hint. */
25
+ rerun: string;
26
+ }
27
+ export declare function writePulledFiles(params: WritePulledFilesParams): string[];
@@ -0,0 +1,60 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { invalidStateError, validationError } from './errors.js';
4
+ import { assertNoSymlinkedComponent, classifyPath, isContained, mkdirSafe, readBytesNoFollow, writeFileNoFollow, } from './safe-fs.js';
5
+ export function writePulledFiles(params) {
6
+ const { outDir } = params;
7
+ prepareOutDir(params.projectDir, outDir);
8
+ const planned = params.files.map((file) => {
9
+ const target = path.resolve(outDir, ...file.path.split('/'));
10
+ if (target === outDir || !isContained(outDir, target)) {
11
+ throw validationError(`Refusing to write ${JSON.stringify(file.path)} outside ${outDir}.`, 'The host sent a path that leaves the folder; nothing was written.');
12
+ }
13
+ assertNoSymlinkedComponent(outDir, target);
14
+ const kind = classifyPath(target);
15
+ if (kind !== 'missing' && kind !== 'file') {
16
+ throw validationError(`Refusing to write through ${target} (${kind}).`, 'Remove it, or pass --out <dir>.');
17
+ }
18
+ const bytes = Buffer.from(file.contents);
19
+ const existing = kind === 'file' ? readBytesNoFollow(outDir, target) : null;
20
+ return {
21
+ target,
22
+ bytes,
23
+ unchanged: existing !== null && existing.equals(bytes),
24
+ overwrites: existing !== null && !existing.equals(bytes),
25
+ };
26
+ });
27
+ const conflicts = planned.filter((file) => file.overwrites);
28
+ if (conflicts.length > 0 && !params.force) {
29
+ throw invalidStateError([
30
+ `Refusing to overwrite ${conflicts.length === 1 ? 'a local file that differs' : `${conflicts.length} local files that differ`} from the host's copy:`,
31
+ ...conflicts.map((file) => ` ${file.target}`),
32
+ ].join('\n'), `Nothing was written. Re-run with --force to replace them (${params.rerun} --force), or pass --out <dir> to pull somewhere else.`);
33
+ }
34
+ for (const file of planned) {
35
+ if (!file.unchanged)
36
+ writeFileNoFollow(outDir, file.target, file.bytes);
37
+ }
38
+ return planned.map((file) => file.target);
39
+ }
40
+ /**
41
+ * Create `outDir` without following a symlink on the way. Inside the project
42
+ * every component is guarded; an `--out` outside it is the person's own
43
+ * choice of parent, so only `outDir` itself is lstat'd.
44
+ */
45
+ function prepareOutDir(projectDir, outDir) {
46
+ if (isContained(projectDir, outDir)) {
47
+ mkdirSafe(projectDir, outDir);
48
+ }
49
+ else if (classifyPath(outDir) === 'missing') {
50
+ fs.mkdirSync(outDir, { recursive: true });
51
+ }
52
+ const kind = classifyPath(outDir);
53
+ if (kind === 'symlink') {
54
+ throw invalidStateError(`Refusing to pull into ${outDir}: it is a symlink.`, 'Pass --out <dir> with a real folder.');
55
+ }
56
+ if (kind !== 'directory') {
57
+ throw invalidStateError(`Refusing to pull into ${outDir}: it exists and is not a folder.`, 'Move it, or pass --out <dir>.');
58
+ }
59
+ }
60
+ //# sourceMappingURL=pull-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull-write.js","sourceRoot":"","sources":["../src/pull-write.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport { invalidStateError, validationError } from './errors';\nimport {\n assertNoSymlinkedComponent,\n classifyPath,\n isContained,\n mkdirSafe,\n readBytesNoFollow,\n writeFileNoFollow,\n} from './safe-fs';\n\n/**\n * Writing what `skills pull`, `knowledge pull` and `agents pull` fetched.\n * Every path comes from the host, and the folder they land in may hold the\n * person's own edits, so:\n *\n * - **Contained.** Each file resolves strictly inside `outDir`.\n * - **No symlinks.** `outDir` itself is lstat'd, every component below the\n * project (or below `outDir`, for an `--out` outside it) is checked, and\n * each write is `O_NOFOLLOW` — a symlinked `skills/` or a planted link at\n * a leaf cannot redirect a write.\n * - **No silent overwrite.** An existing file whose bytes differ is a local\n * edit; the pull refuses before writing anything unless `force`.\n */\n\nexport interface PulledFile {\n /** `/`-separated, relative to `outDir`. */\n path: string;\n contents: string | Uint8Array;\n}\n\nexport interface WritePulledFilesParams {\n projectDir: string;\n outDir: string;\n files: readonly PulledFile[];\n force: boolean;\n /** The command to re-run with `--force`, for the refusal's hint. */\n rerun: string;\n}\n\nexport function writePulledFiles(params: WritePulledFilesParams): string[] {\n const { outDir } = params;\n prepareOutDir(params.projectDir, outDir);\n\n const planned = params.files.map((file) => {\n const target = path.resolve(outDir, ...file.path.split('/'));\n if (target === outDir || !isContained(outDir, target)) {\n throw validationError(\n `Refusing to write ${JSON.stringify(file.path)} outside ${outDir}.`,\n 'The host sent a path that leaves the folder; nothing was written.',\n );\n }\n assertNoSymlinkedComponent(outDir, target);\n const kind = classifyPath(target);\n if (kind !== 'missing' && kind !== 'file') {\n throw validationError(\n `Refusing to write through ${target} (${kind}).`,\n 'Remove it, or pass --out <dir>.',\n );\n }\n const bytes = Buffer.from(file.contents);\n const existing = kind === 'file' ? readBytesNoFollow(outDir, target) : null;\n return {\n target,\n bytes,\n unchanged: existing !== null && existing.equals(bytes),\n overwrites: existing !== null && !existing.equals(bytes),\n };\n });\n\n const conflicts = planned.filter((file) => file.overwrites);\n if (conflicts.length > 0 && !params.force) {\n throw invalidStateError(\n [\n `Refusing to overwrite ${conflicts.length === 1 ? 'a local file that differs' : `${conflicts.length} local files that differ`} from the host's copy:`,\n ...conflicts.map((file) => ` ${file.target}`),\n ].join('\\n'),\n `Nothing was written. Re-run with --force to replace them (${params.rerun} --force), or pass --out <dir> to pull somewhere else.`,\n );\n }\n\n for (const file of planned) {\n if (!file.unchanged) writeFileNoFollow(outDir, file.target, file.bytes);\n }\n return planned.map((file) => file.target);\n}\n\n/**\n * Create `outDir` without following a symlink on the way. Inside the project\n * every component is guarded; an `--out` outside it is the person's own\n * choice of parent, so only `outDir` itself is lstat'd.\n */\nfunction prepareOutDir(projectDir: string, outDir: string): void {\n if (isContained(projectDir, outDir)) {\n mkdirSafe(projectDir, outDir);\n } else if (classifyPath(outDir) === 'missing') {\n fs.mkdirSync(outDir, { recursive: true });\n }\n\n const kind = classifyPath(outDir);\n if (kind === 'symlink') {\n throw invalidStateError(\n `Refusing to pull into ${outDir}: it is a symlink.`,\n 'Pass --out <dir> with a real folder.',\n );\n }\n if (kind !== 'directory') {\n throw invalidStateError(\n `Refusing to pull into ${outDir}: it exists and is not a folder.`,\n 'Move it, or pass --out <dir>.',\n );\n }\n}\n"]}
@@ -63,6 +63,7 @@ export declare const RouteWebhookSchema: z.ZodObject<{
63
63
  header: z.ZodString;
64
64
  secretName: z.ZodString;
65
65
  }, z.core.$strip>>;
66
+ replaced: z.ZodOptional<z.ZodBoolean>;
66
67
  }, z.core.$strip>;
67
68
  declare const UnroutedEventSchema: z.ZodObject<{
68
69
  id: z.ZodString;
@@ -101,5 +102,7 @@ export declare function routesCall<T>(host: string, accessToken: string, schema:
101
102
  path?: string;
102
103
  query: Record<string, string | undefined>;
103
104
  body?: unknown;
105
+ /** The hint when the host refuses to replace a live URL unconfirmed. */
106
+ confirmHint?: string;
104
107
  }): Promise<RoutesApiResult<T>>;
105
108
  export {};