@ekanos/cli 0.13.0 → 0.15.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.
- package/README.md +25 -8
- package/dist/agents-api.d.ts +11 -2
- package/dist/agents-api.js +39 -3
- package/dist/agents-api.js.map +1 -1
- package/dist/apps-api.js +2 -2
- package/dist/apps-api.js.map +1 -1
- package/dist/auth/ekanos-api.d.ts +16 -2
- package/dist/auth/ekanos-api.js +101 -15
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/auth/host-trust.d.ts +46 -0
- package/dist/auth/host-trust.js +79 -0
- package/dist/auth/host-trust.js.map +1 -0
- package/dist/auth/session.d.ts +10 -1
- package/dist/auth/session.js +28 -28
- package/dist/auth/session.js.map +1 -1
- package/dist/commands/agents-edit.d.ts +6 -2
- package/dist/commands/agents-edit.js +57 -26
- package/dist/commands/agents-edit.js.map +1 -1
- package/dist/commands/agents.d.ts +13 -2
- package/dist/commands/agents.js +140 -35
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +2 -0
- package/dist/commands/apps.js +16 -12
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.js +7 -6
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/knowledge.d.ts +19 -0
- package/dist/commands/knowledge.js +119 -27
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +30 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +9 -1
- package/dist/commands/logout.js +56 -7
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +3 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/reviewers.d.ts +2 -0
- package/dist/commands/reviewers.js +7 -3
- package/dist/commands/reviewers.js.map +1 -1
- package/dist/commands/routes.d.ts +2 -0
- package/dist/commands/routes.js +42 -9
- package/dist/commands/routes.js.map +1 -1
- package/dist/commands/runs.d.ts +13 -2
- package/dist/commands/runs.js +99 -16
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +21 -0
- package/dist/commands/scope-notice.js +35 -0
- package/dist/commands/scope-notice.js.map +1 -0
- package/dist/commands/skills.d.ts +7 -1
- package/dist/commands/skills.js +80 -24
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/sources.d.ts +1 -1
- package/dist/commands/sources.js +126 -12
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.js +5 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +49 -0
- package/dist/commands/team-agent-readiness.js +308 -0
- package/dist/commands/team-agent-readiness.js.map +1 -0
- package/dist/commands/team-agents-agents.d.ts +24 -3
- package/dist/commands/team-agents-agents.js +314 -35
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-mcp.js +32 -3
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.js +230 -55
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +3 -1
- package/dist/commands/team-agents.js +27 -7
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/teams-channel-install.d.ts +24 -0
- package/dist/commands/teams-channel-install.js +94 -0
- package/dist/commands/teams-channel-install.js.map +1 -0
- package/dist/commands/teams-connect.d.ts +41 -0
- package/dist/commands/teams-connect.js +212 -57
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/commands/teams-events.d.ts +12 -0
- package/dist/commands/teams-events.js +138 -0
- package/dist/commands/teams-events.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +11 -3
- package/dist/commands/teams-onboard.js +60 -7
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.d.ts +11 -0
- package/dist/commands/teams-overview.js +160 -0
- package/dist/commands/teams-overview.js.map +1 -0
- package/dist/commands/teams-scope.d.ts +10 -0
- package/dist/commands/teams-scope.js +109 -0
- package/dist/commands/teams-scope.js.map +1 -0
- package/dist/commands/teams.d.ts +4 -2
- package/dist/commands/teams.js +113 -14
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/upgrade.js +12 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +2 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connections-api.js +1 -1
- package/dist/connections-api.js.map +1 -1
- package/dist/delegate.js +11 -4
- package/dist/delegate.js.map +1 -1
- package/dist/harness-scaffold.js +12 -0
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +682 -167
- package/dist/index.js.map +1 -1
- package/dist/knowledge-api.d.ts +46 -0
- package/dist/knowledge-api.js +32 -1
- package/dist/knowledge-api.js.map +1 -1
- package/dist/open-url.d.ts +16 -0
- package/dist/open-url.js +73 -0
- package/dist/open-url.js.map +1 -0
- package/dist/parse-argv.d.ts +2 -0
- package/dist/parse-argv.js.map +1 -1
- package/dist/publish-api.d.ts +2 -1
- package/dist/publish-api.js +4 -3
- package/dist/publish-api.js.map +1 -1
- package/dist/pull-write.d.ts +27 -0
- package/dist/pull-write.js +60 -0
- package/dist/pull-write.js.map +1 -0
- package/dist/routes-api.d.ts +3 -0
- package/dist/routes-api.js +9 -4
- package/dist/routes-api.js.map +1 -1
- package/dist/runs-api.d.ts +7 -1
- package/dist/runs-api.js +21 -6
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +2 -0
- package/dist/safe-fs.js +12 -6
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.js +69 -0
- package/dist/secret-file.js.map +1 -0
- package/dist/skills-api.d.ts +10 -2
- package/dist/skills-api.js +37 -2
- package/dist/skills-api.js.map +1 -1
- package/dist/source-products-api.d.ts +4 -0
- package/dist/source-products-api.js +14 -4
- package/dist/source-products-api.js.map +1 -1
- package/dist/source-target.js +2 -1
- package/dist/source-target.js.map +1 -1
- package/dist/sources-api.d.ts +2 -1
- package/dist/sources-api.js +3 -2
- package/dist/sources-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +44 -2
- package/dist/team-connect-api.js +19 -7
- package/dist/team-connect-api.js.map +1 -1
- package/dist/teams-api.d.ts +41 -1
- package/dist/teams-api.js +95 -19
- package/dist/teams-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +9 -0
- package/dist/toolchain-api.js +17 -2
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.d.ts +3 -0
- package/dist/toolchain-resolve.js +15 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +6 -0
- package/dist/toolchain.js +19 -0
- package/dist/toolchain.js.map +1 -1
- package/dist/update-notice.js +2 -1
- package/dist/update-notice.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +6 -1
- package/templates/claude-skill.md.tmpl +5 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +104 -17
- package/templates/skills/build-agent/references/agent-json.md +4 -2
- package/templates/skills/build-agent/references/rollout.md +23 -4
- package/templates/skills/build-agent/references/set-up-an-integration.md +55 -7
- package/templates/skills/build-agent/references/skills.md +20 -1
- package/templates/skills/build-agent/references/triggers.md +47 -11
- package/templates/skills/build-integration/references/widget-design.md +3 -3
- package/templates/skills/ekanos-platform/SKILL.md +45 -16
- package/templates/skills/polish-widgets/references/patterns.md +2 -2
package/dist/open-url.js
ADDED
|
@@ -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"]}
|
package/dist/parse-argv.d.ts
CHANGED
|
@@ -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 {
|
package/dist/parse-argv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-argv.js","sourceRoot":"","sources":["../src/parse-argv.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;
|
|
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"]}
|
package/dist/publish-api.d.ts
CHANGED
|
@@ -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(
|
|
161
|
+
export declare function stillUnauthorized(env: AuthEnvironment): never;
|
package/dist/publish-api.js
CHANGED
|
@@ -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
|
|
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(
|
|
192
|
-
throw authRequiredError(`The session for ${host} could not authorize the submission.`,
|
|
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 {
|
package/dist/publish-api.js.map
CHANGED
|
@@ -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"]}
|
package/dist/routes-api.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/routes-api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { request } from './auth/ekanos-api.js';
|
|
3
|
-
import { authRequiredError, forbiddenError, networkError, notFoundError, usageError, } from './errors.js';
|
|
3
|
+
import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
|
|
4
4
|
/**
|
|
5
5
|
* The CLI's client for a Source's event routing (plan 241.4): which team each
|
|
6
6
|
* value of an integration's route key (Zendesk's organization) belongs to,
|
|
@@ -10,7 +10,7 @@ import { authRequiredError, forbiddenError, networkError, notFoundError, usageEr
|
|
|
10
10
|
* PUT /api/partner/routes/<product>?source=<slug> { value, team, label? }
|
|
11
11
|
* DELETE /api/partner/routes/<product>?source=<slug>&value=<value>
|
|
12
12
|
* GET /api/partner/routes/<product>/options?source=<slug>
|
|
13
|
-
* POST /api/partner/routes/<product>/webhook?source=<slug>[&webhook=<id>]
|
|
13
|
+
* POST /api/partner/routes/<product>/webhook?source=<slug>[&webhook=<id>][&replace=true]
|
|
14
14
|
* GET /api/partner/routes/unrouted?source=<slug>[&product=<slug>]
|
|
15
15
|
* POST /api/partner/routes/unrouted/<id>/replay?source=<slug>
|
|
16
16
|
*
|
|
@@ -18,7 +18,7 @@ import { authRequiredError, forbiddenError, networkError, notFoundError, usageEr
|
|
|
18
18
|
* → 403 { code: "SOURCE_ADMIN_REQUIRED" | "MFA_REQUIRED" | "FORBIDDEN" }
|
|
19
19
|
* → 404 { code: "PRODUCT_NOT_ENTITLED" | "TEAM_NOT_FOUND" | "ROUTE_NOT_FOUND"
|
|
20
20
|
* | "EVENT_NOT_FOUND" | "SOURCE_NOT_FOUND" }
|
|
21
|
-
* → 409 { code: "NO_ROUTING" | "NO_WEBHOOK" }
|
|
21
|
+
* → 409 { code: "NO_ROUTING" | "NO_WEBHOOK" | "WEBHOOK_LIVE" }
|
|
22
22
|
*
|
|
23
23
|
* The contract's shapes live here rather than in `@ekanos/integration-schema`,
|
|
24
24
|
* so the routing commands ship with the CLI alone.
|
|
@@ -61,6 +61,8 @@ export const RouteWebhookSchema = z.object({
|
|
|
61
61
|
signature: z
|
|
62
62
|
.object({ header: z.string(), secretName: z.string() })
|
|
63
63
|
.nullable(),
|
|
64
|
+
/** A live URL was revoked by this mint. An older host does not say. */
|
|
65
|
+
replaced: z.boolean().optional(),
|
|
64
66
|
});
|
|
65
67
|
const UnroutedEventSchema = z.object({
|
|
66
68
|
id: z.string(),
|
|
@@ -78,7 +80,7 @@ export const ReplaySchema = z.object({
|
|
|
78
80
|
});
|
|
79
81
|
const BASE = '/api/partner/routes';
|
|
80
82
|
export async function routesCall(host, accessToken, schema, init) {
|
|
81
|
-
var _a;
|
|
83
|
+
var _a, _b;
|
|
82
84
|
const query = Object.fromEntries(Object.entries(init.query).filter((entry) => entry[1] !== undefined));
|
|
83
85
|
const url = `${host}${BASE}${(_a = init.path) !== null && _a !== void 0 ? _a : ''}?${new URLSearchParams(query)}`;
|
|
84
86
|
const response = await request(url, {
|
|
@@ -114,6 +116,9 @@ export async function routesCall(host, accessToken, schema, init) {
|
|
|
114
116
|
? 'Run "ekanos catalog" to see what the Source has.'
|
|
115
117
|
: 'Run "ekanos sources" to list the sources you hold a seat on.');
|
|
116
118
|
}
|
|
119
|
+
if (response.status === 409 && code === 'WEBHOOK_LIVE') {
|
|
120
|
+
throw invalidStateError(message !== null && message !== void 0 ? message : 'The Source already has a live webhook URL.', (_b = init.confirmHint) !== null && _b !== void 0 ? _b : 'Re-run with --yes when you are ready to update the vendor.');
|
|
121
|
+
}
|
|
117
122
|
if (response.status === 409) {
|
|
118
123
|
throw usageError(message !== null && message !== void 0 ? message : 'The integration does not route events.', code === 'NO_WEBHOOK'
|
|
119
124
|
? 'Name the webhook: "ekanos routes webhook <product> --webhook <id>".'
|
package/dist/routes-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes-api.js","sourceRoot":"","sources":["../src/routes-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 event routing (plan 241.4): which team each\n * value of an integration's route key (Zendesk's organization) belongs to,\n * the Source's webhook endpoint, and the events no team is mapped to yet.\n *\n * GET /api/partner/routes?source=<slug>[&product=<slug>]\n * PUT /api/partner/routes/<product>?source=<slug> { value, team, label? }\n * DELETE /api/partner/routes/<product>?source=<slug>&value=<value>\n * GET /api/partner/routes/<product>/options?source=<slug>\n * POST /api/partner/routes/<product>/webhook?source=<slug>[&webhook=<id>]\n * GET /api/partner/routes/unrouted?source=<slug>[&product=<slug>]\n * POST /api/partner/routes/unrouted/<id>/replay?source=<slug>\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"PRODUCT_NOT_ENTITLED\" | \"TEAM_NOT_FOUND\" | \"ROUTE_NOT_FOUND\"\n * | \"EVENT_NOT_FOUND\" | \"SOURCE_NOT_FOUND\" }\n * → 409 { code: \"NO_ROUTING\" | \"NO_WEBHOOK\" }\n *\n * The contract's shapes live here rather than in `@ekanos/integration-schema`,\n * so the routing 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 RouteSchema = z.object({\n product: z.string(),\n key: z.string(),\n value: z.string(),\n label: z.string().nullable(),\n team: z.object({\n id: z.string(),\n slug: z.string().nullable(),\n name: z.string(),\n }),\n updatedAt: z.string(),\n});\nexport type SourceEventRoute = z.infer<typeof RouteSchema>;\n\nexport const RouteListSchema = z.object({\n source: SourceRefSchema,\n routes: z.array(RouteSchema),\n});\n\nexport const RouteSetSchema = z.object({\n route: RouteSchema,\n replaced: z.boolean(),\n});\n\nexport const RouteRemoveSchema = z.object({ removed: z.number() });\n\nexport const RouteOptionsSchema = z.object({\n key: z.string(),\n options: z\n .array(z.object({ value: z.string(), label: z.string() }))\n .nullable(),\n});\n\nexport const RouteWebhookSchema = z.object({\n webhookId: z.string(),\n url: z.string(),\n signature: z\n .object({ header: z.string(), secretName: z.string() })\n .nullable(),\n});\n\nconst UnroutedEventSchema = z.object({\n id: z.string(),\n product: z.string(),\n eventId: z.string(),\n routeKey: z.string().nullable(),\n routeValue: z.string().nullable(),\n createdAt: z.string(),\n});\nexport type UnroutedEvent = z.infer<typeof UnroutedEventSchema>;\n\nexport const UnroutedListSchema = z.object({\n events: z.array(UnroutedEventSchema),\n});\n\nexport const ReplaySchema = z.object({\n status: z.enum(['routed', 'already-routed', 'unrouted']),\n});\n\nexport type RoutesApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst BASE = '/api/partner/routes';\n\nexport async function routesCall<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'PUT' | 'POST' | 'DELETE';\n path?: string;\n query: Record<string, string | undefined>;\n body?: unknown;\n },\n): Promise<RoutesApiResult<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 the Source's routing. 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 : code === 'ROUTE_NOT_FOUND'\n ? '\"ekanos routes list\" shows the mapped values.'\n : code === 'EVENT_NOT_FOUND'\n ? '\"ekanos routes unrouted\" lists the events waiting for a mapping.'\n : code === 'PRODUCT_NOT_ENTITLED'\n ? 'Run \"ekanos catalog\" to see what the Source has.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409) {\n throw usageError(\n message ?? 'The integration does not route events.',\n code === 'NO_WEBHOOK'\n ? 'Name the webhook: \"ekanos routes webhook <product> --webhook <id>\".'\n : 'Only an integration that declares a Source connection and a route key routes events.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos routes --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 routes 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 routes 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":"routes-api.js","sourceRoot":"","sources":["../src/routes-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 invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's event routing (plan 241.4): which team each\n * value of an integration's route key (Zendesk's organization) belongs to,\n * the Source's webhook endpoint, and the events no team is mapped to yet.\n *\n * GET /api/partner/routes?source=<slug>[&product=<slug>]\n * PUT /api/partner/routes/<product>?source=<slug> { value, team, label? }\n * DELETE /api/partner/routes/<product>?source=<slug>&value=<value>\n * GET /api/partner/routes/<product>/options?source=<slug>\n * POST /api/partner/routes/<product>/webhook?source=<slug>[&webhook=<id>][&replace=true]\n * GET /api/partner/routes/unrouted?source=<slug>[&product=<slug>]\n * POST /api/partner/routes/unrouted/<id>/replay?source=<slug>\n *\n * → 401 / 3xx refresh once and retry\n * → 403 { code: \"SOURCE_ADMIN_REQUIRED\" | \"MFA_REQUIRED\" | \"FORBIDDEN\" }\n * → 404 { code: \"PRODUCT_NOT_ENTITLED\" | \"TEAM_NOT_FOUND\" | \"ROUTE_NOT_FOUND\"\n * | \"EVENT_NOT_FOUND\" | \"SOURCE_NOT_FOUND\" }\n * → 409 { code: \"NO_ROUTING\" | \"NO_WEBHOOK\" | \"WEBHOOK_LIVE\" }\n *\n * The contract's shapes live here rather than in `@ekanos/integration-schema`,\n * so the routing 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 RouteSchema = z.object({\n product: z.string(),\n key: z.string(),\n value: z.string(),\n label: z.string().nullable(),\n team: z.object({\n id: z.string(),\n slug: z.string().nullable(),\n name: z.string(),\n }),\n updatedAt: z.string(),\n});\nexport type SourceEventRoute = z.infer<typeof RouteSchema>;\n\nexport const RouteListSchema = z.object({\n source: SourceRefSchema,\n routes: z.array(RouteSchema),\n});\n\nexport const RouteSetSchema = z.object({\n route: RouteSchema,\n replaced: z.boolean(),\n});\n\nexport const RouteRemoveSchema = z.object({ removed: z.number() });\n\nexport const RouteOptionsSchema = z.object({\n key: z.string(),\n options: z\n .array(z.object({ value: z.string(), label: z.string() }))\n .nullable(),\n});\n\nexport const RouteWebhookSchema = z.object({\n webhookId: z.string(),\n url: z.string(),\n signature: z\n .object({ header: z.string(), secretName: z.string() })\n .nullable(),\n /** A live URL was revoked by this mint. An older host does not say. */\n replaced: z.boolean().optional(),\n});\n\nconst UnroutedEventSchema = z.object({\n id: z.string(),\n product: z.string(),\n eventId: z.string(),\n routeKey: z.string().nullable(),\n routeValue: z.string().nullable(),\n createdAt: z.string(),\n});\nexport type UnroutedEvent = z.infer<typeof UnroutedEventSchema>;\n\nexport const UnroutedListSchema = z.object({\n events: z.array(UnroutedEventSchema),\n});\n\nexport const ReplaySchema = z.object({\n status: z.enum(['routed', 'already-routed', 'unrouted']),\n});\n\nexport type RoutesApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst BASE = '/api/partner/routes';\n\nexport async function routesCall<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'PUT' | 'POST' | 'DELETE';\n path?: string;\n query: Record<string, string | undefined>;\n body?: unknown;\n /** The hint when the host refuses to replace a live URL unconfirmed. */\n confirmHint?: string;\n },\n): Promise<RoutesApiResult<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 the Source's routing. 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 : code === 'ROUTE_NOT_FOUND'\n ? '\"ekanos routes list\" shows the mapped values.'\n : code === 'EVENT_NOT_FOUND'\n ? '\"ekanos routes unrouted\" lists the events waiting for a mapping.'\n : code === 'PRODUCT_NOT_ENTITLED'\n ? 'Run \"ekanos catalog\" to see what the Source has.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 && code === 'WEBHOOK_LIVE') {\n throw invalidStateError(\n message ?? 'The Source already has a live webhook URL.',\n init.confirmHint ??\n 'Re-run with --yes when you are ready to update the vendor.',\n );\n }\n if (response.status === 409) {\n throw usageError(\n message ?? 'The integration does not route events.',\n code === 'NO_WEBHOOK'\n ? 'Name the webhook: \"ekanos routes webhook <product> --webhook <id>\".'\n : 'Only an integration that declares a Source connection and a route key routes events.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos routes --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 routes 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 routes 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/runs-api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AgentApprovalDecideRequest, type AgentApprovalDecided, type AgentApprovalList, type AgentRunDetail, type AgentRunList, type AgentRunStartRequest, type AgentRunStarted } from '@ekanos/integration-schema';
|
|
1
|
+
import { type AgentApprovalDecideRequest, type AgentApprovalDecided, type AgentApprovalList, type AgentRunCancelled, type AgentRunDetail, type AgentRunList, type AgentRunStartRequest, type AgentRunStarted } from '@ekanos/integration-schema';
|
|
2
2
|
/**
|
|
3
3
|
* The CLI's client for a team's agent runs and approvals — `ekanos runs`,
|
|
4
4
|
* `ekanos agents test` and `ekanos approvals`.
|
|
@@ -6,6 +6,7 @@ import { type AgentApprovalDecideRequest, type AgentApprovalDecided, type AgentA
|
|
|
6
6
|
* GET /api/partner/teams/<team>/runs?source=<slug>[&agent=<key>] → AgentRunList
|
|
7
7
|
* POST /api/partner/teams/<team>/runs?source=<slug> → AgentRunStarted
|
|
8
8
|
* GET /api/partner/teams/<team>/runs/<id>?source=<slug>[&after=] → AgentRunDetail
|
|
9
|
+
* POST /api/partner/teams/<team>/runs/<id>/cancel?source=<slug> → AgentRunCancelled
|
|
9
10
|
* GET /api/partner/teams/<team>/approvals?source=<slug> → AgentApprovalList
|
|
10
11
|
* POST /api/partner/teams/<team>/approvals/<id>?source=<slug> → AgentApprovalDecided
|
|
11
12
|
*/
|
|
@@ -21,12 +22,17 @@ interface TeamTarget {
|
|
|
21
22
|
}
|
|
22
23
|
export declare function listRuns(host: string, accessToken: string, target: TeamTarget & {
|
|
23
24
|
agent?: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
since?: string;
|
|
24
27
|
}): Promise<RunsApiResult<AgentRunList>>;
|
|
25
28
|
export declare function startRun(host: string, accessToken: string, target: TeamTarget, body: AgentRunStartRequest): Promise<RunsApiResult<AgentRunStarted>>;
|
|
26
29
|
export declare function showRun(host: string, accessToken: string, target: TeamTarget & {
|
|
27
30
|
runId: string;
|
|
28
31
|
after?: number;
|
|
29
32
|
}): Promise<RunsApiResult<AgentRunDetail>>;
|
|
33
|
+
export declare function cancelRun(host: string, accessToken: string, target: TeamTarget & {
|
|
34
|
+
runId: string;
|
|
35
|
+
}): Promise<RunsApiResult<AgentRunCancelled>>;
|
|
30
36
|
export declare function listApprovals(host: string, accessToken: string, target: TeamTarget): Promise<RunsApiResult<AgentApprovalList>>;
|
|
31
37
|
export declare function decideApproval(host: string, accessToken: string, target: TeamTarget & {
|
|
32
38
|
approvalId: string;
|