@ekanos/cli 0.11.0 → 0.12.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/dist/commands/apps.d.ts +6 -2
- package/dist/commands/apps.js +249 -4
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/routes.d.ts +23 -0
- package/dist/commands/routes.js +277 -0
- package/dist/commands/routes.js.map +1 -0
- package/dist/connections-api.d.ts +125 -0
- package/dist/connections-api.js +156 -0
- package/dist/connections-api.js.map +1 -0
- package/dist/index.js +30 -2
- package/dist/index.js.map +1 -1
- package/dist/routes-api.d.ts +105 -0
- package/dist/routes-api.js +147 -0
- package/dist/routes-api.js.map +1 -0
- package/package.json +2 -2
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +4 -2
- package/templates/skills/build-agent/references/agent-json.md +7 -2
- package/templates/skills/build-agent/references/set-up-an-integration.md +57 -2
- package/templates/skills/build-agent/references/skills.md +7 -4
- package/templates/skills/ekanos-platform/SKILL.md +8 -2
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { refreshStoredSession, requireSession, resolveAuthEnvironment, } from '../auth/session.js';
|
|
2
|
+
import { authRequiredError, usageError } from '../errors.js';
|
|
3
|
+
import { ReplaySchema, RouteListSchema, RouteOptionsSchema, RouteRemoveSchema, RouteSetSchema, RouteWebhookSchema, UnroutedListSchema, routesCall, } from '../routes-api.js';
|
|
4
|
+
import { resolveSourceTarget } from '../source-target.js';
|
|
5
|
+
export const ROUTES_SUBCOMMANDS = [
|
|
6
|
+
'list',
|
|
7
|
+
'options',
|
|
8
|
+
'set',
|
|
9
|
+
'remove',
|
|
10
|
+
'webhook',
|
|
11
|
+
'unrouted',
|
|
12
|
+
'replay',
|
|
13
|
+
];
|
|
14
|
+
const USAGE = 'Run one of: ekanos routes list [<product>] | options <product> | set <product> <value> <team> [--label <name>] | remove <product> <value> | webhook <product> [--webhook <id>] | unrouted [<product>] | replay <event-id>.';
|
|
15
|
+
/**
|
|
16
|
+
* `routes` — a Source admin's routing of a Source connection's events to
|
|
17
|
+
* teams (plan 241.4). One Zendesk connected for the whole Source sends every
|
|
18
|
+
* ticket to one webhook; each ticket's organization decides which team's
|
|
19
|
+
* agents see it. `set` maps a value to a team, `remove` unmaps it, `options`
|
|
20
|
+
* lists the values the integration knows (Zendesk's organizations),
|
|
21
|
+
* `webhook` mints the Source's endpoint URL (shown once), and `unrouted` /
|
|
22
|
+
* `replay` deal with events no team was mapped to — they started nothing.
|
|
23
|
+
*/
|
|
24
|
+
export async function runRoutes(ctx, args) {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const [rawSubcommand, ...operands] = args.positionals;
|
|
27
|
+
const subcommand = parseSubcommand(rawSubcommand);
|
|
28
|
+
const expected = OPERANDS[subcommand];
|
|
29
|
+
if (operands.length < expected.min || operands.length > expected.max) {
|
|
30
|
+
throw usageError(`routes ${subcommand} takes ${expected.usage}.`, `e.g. "ekanos routes ${subcommand} ${expected.example}".`);
|
|
31
|
+
}
|
|
32
|
+
const env = resolveAuthEnvironment(ctx, args.host, args.env, {
|
|
33
|
+
projectDir: args.dir,
|
|
34
|
+
});
|
|
35
|
+
const resolved = await resolveSourceTarget(env, requireSession(env.store, env.host), args);
|
|
36
|
+
const client = new RoutesClient(env, resolved.session);
|
|
37
|
+
const source = resolved.source;
|
|
38
|
+
const send = (schema, init) => client.call((token) => routesCall(env.host, token, schema, Object.assign(Object.assign({}, init), { query: Object.assign({ source }, init.query) })));
|
|
39
|
+
switch (subcommand) {
|
|
40
|
+
case 'list': {
|
|
41
|
+
const product = operands[0] ? requireSlug(operands[0]) : undefined;
|
|
42
|
+
const list = await send(RouteListSchema, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
query: { product },
|
|
45
|
+
});
|
|
46
|
+
if (list.routes.length === 0) {
|
|
47
|
+
return ctx.succeed(Object.assign({ host: env.host }, list), ctx.ui.result({
|
|
48
|
+
title: `No routes on ${list.source.slug}${product ? ` for ${product}` : ''}`,
|
|
49
|
+
tone: 'warn',
|
|
50
|
+
notes: [
|
|
51
|
+
"Events on the Source's endpoint stay unrouted — they start nothing — until their value is mapped to a team.",
|
|
52
|
+
'Map one: "ekanos routes set <product> <value> <team>".',
|
|
53
|
+
],
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
return ctx.succeed(Object.assign({ host: env.host }, list), [
|
|
57
|
+
ctx.ui.bold(`Routes on ${list.source.slug}`),
|
|
58
|
+
ctx.ui.table(['Product', 'Key', 'Value', 'Label', 'Team'], list.routes.map(routeRow)),
|
|
59
|
+
].join('\n'));
|
|
60
|
+
}
|
|
61
|
+
case 'options': {
|
|
62
|
+
const product = requireSlug(operands[0]);
|
|
63
|
+
const result = await send(RouteOptionsSchema, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
path: `/${encodeURIComponent(product)}/options`,
|
|
66
|
+
query: {},
|
|
67
|
+
});
|
|
68
|
+
if (!result.options) {
|
|
69
|
+
return ctx.succeed(Object.assign({ host: env.host, product }, result), ctx.ui.result({
|
|
70
|
+
title: `${product} does not list its ${result.key} values`,
|
|
71
|
+
tone: 'warn',
|
|
72
|
+
notes: [
|
|
73
|
+
`Type the ${result.key} id: "ekanos routes set ${product} <value> <team>".`,
|
|
74
|
+
],
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
return ctx.succeed(Object.assign({ host: env.host, product }, result), [
|
|
78
|
+
ctx.ui.bold(`${product} ${result.key} values`),
|
|
79
|
+
ctx.ui.table(['Value', 'Label'], result.options.map((option) => [option.value, option.label])),
|
|
80
|
+
].join('\n'));
|
|
81
|
+
}
|
|
82
|
+
case 'set': {
|
|
83
|
+
const product = requireSlug(operands[0]);
|
|
84
|
+
const value = requireValue(operands[1]);
|
|
85
|
+
const team = requireSlug(operands[2], 'team');
|
|
86
|
+
const result = await send(RouteSetSchema, {
|
|
87
|
+
method: 'PUT',
|
|
88
|
+
path: `/${encodeURIComponent(product)}`,
|
|
89
|
+
query: {},
|
|
90
|
+
body: Object.assign({ value, team }, (args.label ? { label: args.label } : {})),
|
|
91
|
+
});
|
|
92
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({
|
|
93
|
+
title: `Routed ${product} ${result.route.key} ${value} to ${(_a = result.route.team.slug) !== null && _a !== void 0 ? _a : result.route.team.name}`,
|
|
94
|
+
rows: [
|
|
95
|
+
['Source', source],
|
|
96
|
+
['Label', (_b = result.route.label) !== null && _b !== void 0 ? _b : '—'],
|
|
97
|
+
[
|
|
98
|
+
'Replaced',
|
|
99
|
+
result.replaced ? 'yes (moved from another team)' : 'no',
|
|
100
|
+
],
|
|
101
|
+
],
|
|
102
|
+
notes: [
|
|
103
|
+
'New events with this value start that team\'s agents. Earlier unrouted ones: "ekanos routes unrouted" then "ekanos routes replay <event-id>".',
|
|
104
|
+
],
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
case 'remove': {
|
|
108
|
+
const product = requireSlug(operands[0]);
|
|
109
|
+
const value = requireValue(operands[1]);
|
|
110
|
+
const result = await send(RouteRemoveSchema, {
|
|
111
|
+
method: 'DELETE',
|
|
112
|
+
path: `/${encodeURIComponent(product)}`,
|
|
113
|
+
query: { value },
|
|
114
|
+
});
|
|
115
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({
|
|
116
|
+
title: `Unmapped ${product} ${value}`,
|
|
117
|
+
rows: [['Source', source]],
|
|
118
|
+
notes: ['Its events now stay unrouted and start nothing.'],
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
case 'webhook': {
|
|
122
|
+
const product = requireSlug(operands[0]);
|
|
123
|
+
const result = await send(RouteWebhookSchema, {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
path: `/${encodeURIComponent(product)}/webhook`,
|
|
126
|
+
query: { webhook: args.webhook },
|
|
127
|
+
});
|
|
128
|
+
return ctx.succeed(Object.assign({ host: env.host, product }, result), ctx.ui.result({
|
|
129
|
+
title: `Webhook URL for ${product} on ${source} (shown once)`,
|
|
130
|
+
rows: [
|
|
131
|
+
['Webhook', result.webhookId],
|
|
132
|
+
['URL', result.url],
|
|
133
|
+
],
|
|
134
|
+
notes: [
|
|
135
|
+
'Point the vendor at this URL once, for the whole Source. Minting again revokes the previous URL.',
|
|
136
|
+
...(result.signature
|
|
137
|
+
? [
|
|
138
|
+
`Deliveries must be signed (${result.signature.header}); the Source connection needs the "${result.signature.secretName}" secret.`,
|
|
139
|
+
]
|
|
140
|
+
: []),
|
|
141
|
+
],
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
case 'unrouted': {
|
|
145
|
+
const product = operands[0] ? requireSlug(operands[0]) : undefined;
|
|
146
|
+
const result = await send(UnroutedListSchema, {
|
|
147
|
+
method: 'GET',
|
|
148
|
+
path: '/unrouted',
|
|
149
|
+
query: { product },
|
|
150
|
+
});
|
|
151
|
+
if (result.events.length === 0) {
|
|
152
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), ctx.ui.result({ title: 'No unrouted events', tone: 'ok' }));
|
|
153
|
+
}
|
|
154
|
+
return ctx.succeed(Object.assign({ host: env.host }, result), [
|
|
155
|
+
ctx.ui.bold('Unrouted events (no team mapped; nothing started)'),
|
|
156
|
+
ctx.ui.table(['Id', 'Product', 'Event', 'Value', 'Received'], result.events.map((event) => { var _a; return [
|
|
157
|
+
event.id,
|
|
158
|
+
event.product,
|
|
159
|
+
event.eventId,
|
|
160
|
+
event.routeValue
|
|
161
|
+
? `${(_a = event.routeKey) !== null && _a !== void 0 ? _a : ''} ${event.routeValue}`.trim()
|
|
162
|
+
: '(none)',
|
|
163
|
+
event.createdAt,
|
|
164
|
+
]; })),
|
|
165
|
+
'Map the value with "ekanos routes set", then "ekanos routes replay <id>".',
|
|
166
|
+
].join('\n'));
|
|
167
|
+
}
|
|
168
|
+
case 'replay': {
|
|
169
|
+
const id = requireEventId(operands[0]);
|
|
170
|
+
const result = await send(ReplaySchema, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
path: `/unrouted/${encodeURIComponent(id)}/replay`,
|
|
173
|
+
query: {},
|
|
174
|
+
});
|
|
175
|
+
return ctx.succeed(Object.assign({ host: env.host, id }, result), ctx.ui.result({
|
|
176
|
+
title: result.status === 'routed'
|
|
177
|
+
? `Routed ${id}`
|
|
178
|
+
: result.status === 'already-routed'
|
|
179
|
+
? `${id} was already routed`
|
|
180
|
+
: `${id} is still unrouted`,
|
|
181
|
+
tone: result.status === 'unrouted' ? 'warn' : 'ok',
|
|
182
|
+
notes: result.status === 'unrouted'
|
|
183
|
+
? [
|
|
184
|
+
'No team is mapped to its value yet: "ekanos routes set" first.',
|
|
185
|
+
]
|
|
186
|
+
: [],
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const OPERANDS = {
|
|
192
|
+
list: { min: 0, max: 1, usage: 'an optional product', example: 'zendesk' },
|
|
193
|
+
options: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },
|
|
194
|
+
set: {
|
|
195
|
+
min: 3,
|
|
196
|
+
max: 3,
|
|
197
|
+
usage: '<product> <value> <team>',
|
|
198
|
+
example: 'zendesk 360001234567 first-bank',
|
|
199
|
+
},
|
|
200
|
+
remove: {
|
|
201
|
+
min: 2,
|
|
202
|
+
max: 2,
|
|
203
|
+
usage: '<product> <value>',
|
|
204
|
+
example: 'zendesk 360001234567',
|
|
205
|
+
},
|
|
206
|
+
webhook: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },
|
|
207
|
+
unrouted: {
|
|
208
|
+
min: 0,
|
|
209
|
+
max: 1,
|
|
210
|
+
usage: 'an optional product',
|
|
211
|
+
example: 'zendesk',
|
|
212
|
+
},
|
|
213
|
+
replay: {
|
|
214
|
+
min: 1,
|
|
215
|
+
max: 1,
|
|
216
|
+
usage: 'an event id from "ekanos routes unrouted"',
|
|
217
|
+
example: '<event-id>',
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
function routeRow(route) {
|
|
221
|
+
var _a, _b;
|
|
222
|
+
return [
|
|
223
|
+
route.product,
|
|
224
|
+
route.key,
|
|
225
|
+
route.value,
|
|
226
|
+
(_a = route.label) !== null && _a !== void 0 ? _a : '—',
|
|
227
|
+
(_b = route.team.slug) !== null && _b !== void 0 ? _b : route.team.name,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
class RoutesClient {
|
|
231
|
+
constructor(env, session) {
|
|
232
|
+
this.env = env;
|
|
233
|
+
this.session = session;
|
|
234
|
+
}
|
|
235
|
+
async call(send) {
|
|
236
|
+
let result = await send(this.session.accessToken);
|
|
237
|
+
if (result.status === 'unauthorized') {
|
|
238
|
+
this.session = await refreshStoredSession(this.env, this.session);
|
|
239
|
+
result = await send(this.session.accessToken);
|
|
240
|
+
}
|
|
241
|
+
if (result.status === 'ok')
|
|
242
|
+
return result.data;
|
|
243
|
+
throw authRequiredError(`The session for ${this.env.host} could not authorize the routes call.`, `Run "ekanos login --host ${this.env.host}" and retry.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function parseSubcommand(value) {
|
|
247
|
+
const match = ROUTES_SUBCOMMANDS.find((name) => name === value);
|
|
248
|
+
if (match)
|
|
249
|
+
return match;
|
|
250
|
+
throw usageError(value
|
|
251
|
+
? `Unknown routes subcommand "${value}".`
|
|
252
|
+
: 'routes needs a subcommand.', USAGE);
|
|
253
|
+
}
|
|
254
|
+
function requireSlug(value, what = 'product') {
|
|
255
|
+
var _a;
|
|
256
|
+
const slug = (_a = value === null || value === void 0 ? void 0 : value.trim().toLowerCase()) !== null && _a !== void 0 ? _a : '';
|
|
257
|
+
if (/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug))
|
|
258
|
+
return slug;
|
|
259
|
+
throw usageError(`"${value !== null && value !== void 0 ? value : ''}" is not a ${what} slug.`, what === 'team'
|
|
260
|
+
? 'Run "ekanos teams list" to see the teams on this Source.'
|
|
261
|
+
: 'Run "ekanos catalog" to see what the Source has.');
|
|
262
|
+
}
|
|
263
|
+
function requireValue(value) {
|
|
264
|
+
var _a;
|
|
265
|
+
const trimmed = (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
|
|
266
|
+
if (trimmed.length > 0 && trimmed.length <= 200)
|
|
267
|
+
return trimmed;
|
|
268
|
+
throw usageError('A route value is 1 to 200 characters.', '"ekanos routes options <product>" lists the values the integration knows.');
|
|
269
|
+
}
|
|
270
|
+
function requireEventId(value) {
|
|
271
|
+
var _a;
|
|
272
|
+
const id = (_a = value === null || value === void 0 ? void 0 : value.trim()) !== null && _a !== void 0 ? _a : '';
|
|
273
|
+
if (/^[0-9a-f-]{36}$/i.test(id))
|
|
274
|
+
return id;
|
|
275
|
+
throw usageError(`"${value !== null && value !== void 0 ? value : ''}" is not an event id.`, '"ekanos routes unrouted" lists the events waiting for a mapping.');
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/commands/routes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { z } from 'zod';\n\nimport type { StoredSession } from '../auth/credential-store';\nimport {\n type AuthEnvironment,\n refreshStoredSession,\n requireSession,\n resolveAuthEnvironment,\n} from '../auth/session';\nimport type { CliContext } from '../context';\nimport { authRequiredError, usageError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport {\n ReplaySchema,\n RouteListSchema,\n RouteOptionsSchema,\n RouteRemoveSchema,\n RouteSetSchema,\n RouteWebhookSchema,\n type RoutesApiResult,\n type SourceEventRoute,\n UnroutedListSchema,\n routesCall,\n} from '../routes-api';\nimport { resolveSourceTarget } from '../source-target';\n\nexport const ROUTES_SUBCOMMANDS = [\n 'list',\n 'options',\n 'set',\n 'remove',\n 'webhook',\n 'unrouted',\n 'replay',\n] as const;\ntype RoutesSubcommand = (typeof ROUTES_SUBCOMMANDS)[number];\n\nexport interface RoutesArgs {\n host?: string;\n source?: string;\n /** `list [product]`, `set <product> <value> <team>`, `replay <event-id>`, … */\n positionals: readonly string[];\n label?: string;\n webhook?: string;\n dir: string;\n env: Record<string, string | undefined>;\n}\n\nconst USAGE =\n 'Run one of: ekanos routes list [<product>] | options <product> | set <product> <value> <team> [--label <name>] | remove <product> <value> | webhook <product> [--webhook <id>] | unrouted [<product>] | replay <event-id>.';\n\n/**\n * `routes` — a Source admin's routing of a Source connection's events to\n * teams (plan 241.4). One Zendesk connected for the whole Source sends every\n * ticket to one webhook; each ticket's organization decides which team's\n * agents see it. `set` maps a value to a team, `remove` unmaps it, `options`\n * lists the values the integration knows (Zendesk's organizations),\n * `webhook` mints the Source's endpoint URL (shown once), and `unrouted` /\n * `replay` deal with events no team was mapped to — they started nothing.\n */\nexport async function runRoutes(\n ctx: CliContext,\n args: RoutesArgs,\n): Promise<ExitCode> {\n const [rawSubcommand, ...operands] = args.positionals;\n const subcommand = parseSubcommand(rawSubcommand);\n const expected = OPERANDS[subcommand];\n if (operands.length < expected.min || operands.length > expected.max) {\n throw usageError(\n `routes ${subcommand} takes ${expected.usage}.`,\n `e.g. \"ekanos routes ${subcommand} ${expected.example}\".`,\n );\n }\n\n const env = resolveAuthEnvironment(ctx, args.host, args.env, {\n projectDir: args.dir,\n });\n const resolved = await resolveSourceTarget(\n env,\n requireSession(env.store, env.host),\n args,\n );\n const client = new RoutesClient(env, resolved.session);\n const source = resolved.source;\n const send = <T>(\n schema: z.ZodType<T>,\n init: Parameters<typeof routesCall>[3],\n ) =>\n client.call((token) =>\n routesCall(env.host, token, schema, {\n ...init,\n query: { source, ...init.query },\n }),\n );\n\n switch (subcommand) {\n case 'list': {\n const product = operands[0] ? requireSlug(operands[0]) : undefined;\n const list = await send(RouteListSchema, {\n method: 'GET',\n query: { product },\n });\n if (list.routes.length === 0) {\n return ctx.succeed(\n { host: env.host, ...list },\n ctx.ui.result({\n title: `No routes on ${list.source.slug}${product ? ` for ${product}` : ''}`,\n tone: 'warn',\n notes: [\n \"Events on the Source's endpoint stay unrouted — they start nothing — until their value is mapped to a team.\",\n 'Map one: \"ekanos routes set <product> <value> <team>\".',\n ],\n }),\n );\n }\n return ctx.succeed(\n { host: env.host, ...list },\n [\n ctx.ui.bold(`Routes on ${list.source.slug}`),\n ctx.ui.table(\n ['Product', 'Key', 'Value', 'Label', 'Team'],\n list.routes.map(routeRow),\n ),\n ].join('\\n'),\n );\n }\n\n case 'options': {\n const product = requireSlug(operands[0]);\n const result = await send(RouteOptionsSchema, {\n method: 'GET',\n path: `/${encodeURIComponent(product)}/options`,\n query: {},\n });\n if (!result.options) {\n return ctx.succeed(\n { host: env.host, product, ...result },\n ctx.ui.result({\n title: `${product} does not list its ${result.key} values`,\n tone: 'warn',\n notes: [\n `Type the ${result.key} id: \"ekanos routes set ${product} <value> <team>\".`,\n ],\n }),\n );\n }\n return ctx.succeed(\n { host: env.host, product, ...result },\n [\n ctx.ui.bold(`${product} ${result.key} values`),\n ctx.ui.table(\n ['Value', 'Label'],\n result.options.map((option) => [option.value, option.label]),\n ),\n ].join('\\n'),\n );\n }\n\n case 'set': {\n const product = requireSlug(operands[0]);\n const value = requireValue(operands[1]);\n const team = requireSlug(operands[2], 'team');\n const result = await send(RouteSetSchema, {\n method: 'PUT',\n path: `/${encodeURIComponent(product)}`,\n query: {},\n body: { value, team, ...(args.label ? { label: args.label } : {}) },\n });\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({\n title: `Routed ${product} ${result.route.key} ${value} to ${result.route.team.slug ?? result.route.team.name}`,\n rows: [\n ['Source', source],\n ['Label', result.route.label ?? '—'],\n [\n 'Replaced',\n result.replaced ? 'yes (moved from another team)' : 'no',\n ],\n ],\n notes: [\n 'New events with this value start that team\\'s agents. Earlier unrouted ones: \"ekanos routes unrouted\" then \"ekanos routes replay <event-id>\".',\n ],\n }),\n );\n }\n\n case 'remove': {\n const product = requireSlug(operands[0]);\n const value = requireValue(operands[1]);\n const result = await send(RouteRemoveSchema, {\n method: 'DELETE',\n path: `/${encodeURIComponent(product)}`,\n query: { value },\n });\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({\n title: `Unmapped ${product} ${value}`,\n rows: [['Source', source]],\n notes: ['Its events now stay unrouted and start nothing.'],\n }),\n );\n }\n\n case 'webhook': {\n const product = requireSlug(operands[0]);\n const result = await send(RouteWebhookSchema, {\n method: 'POST',\n path: `/${encodeURIComponent(product)}/webhook`,\n query: { webhook: args.webhook },\n });\n return ctx.succeed(\n { host: env.host, product, ...result },\n ctx.ui.result({\n title: `Webhook URL for ${product} on ${source} (shown once)`,\n rows: [\n ['Webhook', result.webhookId],\n ['URL', result.url],\n ],\n notes: [\n 'Point the vendor at this URL once, for the whole Source. Minting again revokes the previous URL.',\n ...(result.signature\n ? [\n `Deliveries must be signed (${result.signature.header}); the Source connection needs the \"${result.signature.secretName}\" secret.`,\n ]\n : []),\n ],\n }),\n );\n }\n\n case 'unrouted': {\n const product = operands[0] ? requireSlug(operands[0]) : undefined;\n const result = await send(UnroutedListSchema, {\n method: 'GET',\n path: '/unrouted',\n query: { product },\n });\n if (result.events.length === 0) {\n return ctx.succeed(\n { host: env.host, ...result },\n ctx.ui.result({ title: 'No unrouted events', tone: 'ok' }),\n );\n }\n return ctx.succeed(\n { host: env.host, ...result },\n [\n ctx.ui.bold('Unrouted events (no team mapped; nothing started)'),\n ctx.ui.table(\n ['Id', 'Product', 'Event', 'Value', 'Received'],\n result.events.map((event) => [\n event.id,\n event.product,\n event.eventId,\n event.routeValue\n ? `${event.routeKey ?? ''} ${event.routeValue}`.trim()\n : '(none)',\n event.createdAt,\n ]),\n ),\n 'Map the value with \"ekanos routes set\", then \"ekanos routes replay <id>\".',\n ].join('\\n'),\n );\n }\n\n case 'replay': {\n const id = requireEventId(operands[0]);\n const result = await send(ReplaySchema, {\n method: 'POST',\n path: `/unrouted/${encodeURIComponent(id)}/replay`,\n query: {},\n });\n return ctx.succeed(\n { host: env.host, id, ...result },\n ctx.ui.result({\n title:\n result.status === 'routed'\n ? `Routed ${id}`\n : result.status === 'already-routed'\n ? `${id} was already routed`\n : `${id} is still unrouted`,\n tone: result.status === 'unrouted' ? 'warn' : 'ok',\n notes:\n result.status === 'unrouted'\n ? [\n 'No team is mapped to its value yet: \"ekanos routes set\" first.',\n ]\n : [],\n }),\n );\n }\n }\n}\n\nconst OPERANDS: Record<\n RoutesSubcommand,\n { min: number; max: number; usage: string; example: string }\n> = {\n list: { min: 0, max: 1, usage: 'an optional product', example: 'zendesk' },\n options: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },\n set: {\n min: 3,\n max: 3,\n usage: '<product> <value> <team>',\n example: 'zendesk 360001234567 first-bank',\n },\n remove: {\n min: 2,\n max: 2,\n usage: '<product> <value>',\n example: 'zendesk 360001234567',\n },\n webhook: { min: 1, max: 1, usage: 'a product', example: 'zendesk' },\n unrouted: {\n min: 0,\n max: 1,\n usage: 'an optional product',\n example: 'zendesk',\n },\n replay: {\n min: 1,\n max: 1,\n usage: 'an event id from \"ekanos routes unrouted\"',\n example: '<event-id>',\n },\n};\n\nfunction routeRow(route: SourceEventRoute): string[] {\n return [\n route.product,\n route.key,\n route.value,\n route.label ?? '—',\n route.team.slug ?? route.team.name,\n ];\n}\n\nclass RoutesClient {\n constructor(\n private readonly env: AuthEnvironment,\n private session: StoredSession,\n ) {}\n\n async call<T>(\n send: (accessToken: string) => Promise<RoutesApiResult<T>>,\n ): Promise<T> {\n let result = await send(this.session.accessToken);\n if (result.status === 'unauthorized') {\n this.session = await refreshStoredSession(this.env, this.session);\n result = await send(this.session.accessToken);\n }\n if (result.status === 'ok') return result.data;\n throw authRequiredError(\n `The session for ${this.env.host} could not authorize the routes call.`,\n `Run \"ekanos login --host ${this.env.host}\" and retry.`,\n );\n }\n}\n\nfunction parseSubcommand(value: string | undefined): RoutesSubcommand {\n const match = ROUTES_SUBCOMMANDS.find((name) => name === value);\n if (match) return match;\n throw usageError(\n value\n ? `Unknown routes subcommand \"${value}\".`\n : 'routes needs a subcommand.',\n USAGE,\n );\n}\n\nfunction requireSlug(value: string | undefined, what = 'product'): string {\n const slug = value?.trim().toLowerCase() ?? '';\n if (/^[a-z0-9][a-z0-9-]{0,63}$/.test(slug)) return slug;\n throw usageError(\n `\"${value ?? ''}\" is not a ${what} slug.`,\n what === 'team'\n ? 'Run \"ekanos teams list\" to see the teams on this Source.'\n : 'Run \"ekanos catalog\" to see what the Source has.',\n );\n}\n\nfunction requireValue(value: string | undefined): string {\n const trimmed = value?.trim() ?? '';\n if (trimmed.length > 0 && trimmed.length <= 200) return trimmed;\n throw usageError(\n 'A route value is 1 to 200 characters.',\n '\"ekanos routes options <product>\" lists the values the integration knows.',\n );\n}\n\nfunction requireEventId(value: string | undefined): string {\n const id = value?.trim() ?? '';\n if (/^[0-9a-f-]{36}$/i.test(id)) return id;\n throw usageError(\n `\"${value ?? ''}\" is not an event id.`,\n '\"ekanos routes unrouted\" lists the events waiting for a mapping.',\n );\n}\n"]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SourceConnectionSchema: z.ZodObject<{
|
|
3
|
+
slug: z.ZodString;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
connected: z.ZodBoolean;
|
|
6
|
+
connectedBy: z.ZodNullable<z.ZodString>;
|
|
7
|
+
connectedAt: z.ZodNullable<z.ZodString>;
|
|
8
|
+
scope: z.ZodNullable<z.ZodString>;
|
|
9
|
+
app: z.ZodEnum<{
|
|
10
|
+
fields: "fields";
|
|
11
|
+
"source-app": "source-app";
|
|
12
|
+
}>;
|
|
13
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
14
|
+
name: z.ZodString;
|
|
15
|
+
label: z.ZodString;
|
|
16
|
+
description: z.ZodNullable<z.ZodString>;
|
|
17
|
+
sensitive: z.ZodBoolean;
|
|
18
|
+
set: z.ZodBoolean;
|
|
19
|
+
value: z.ZodNullable<z.ZodString>;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
missing: z.ZodArray<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type SourceConnection = z.infer<typeof SourceConnectionSchema>;
|
|
24
|
+
export type ConnectionsApiResult<T> = {
|
|
25
|
+
status: 'ok';
|
|
26
|
+
data: T;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'unauthorized';
|
|
29
|
+
}
|
|
30
|
+
/** The integration supports no Source connection (409 NO_SOURCE_CONNECTION). */
|
|
31
|
+
| {
|
|
32
|
+
status: 'unsupported';
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
/** App fields are still missing (409 NOT_CONFIGURED). */
|
|
36
|
+
| {
|
|
37
|
+
status: 'not-configured';
|
|
38
|
+
missing: string[];
|
|
39
|
+
connection: SourceConnection | null;
|
|
40
|
+
};
|
|
41
|
+
export declare function listConnections(host: string, token: string, source: string): Promise<ConnectionsApiResult<{
|
|
42
|
+
source: {
|
|
43
|
+
id: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
name: string | null;
|
|
46
|
+
};
|
|
47
|
+
connections: {
|
|
48
|
+
slug: string;
|
|
49
|
+
name: string;
|
|
50
|
+
connected: boolean;
|
|
51
|
+
connectedBy: string | null;
|
|
52
|
+
connectedAt: string | null;
|
|
53
|
+
scope: string | null;
|
|
54
|
+
app: "fields" | "source-app";
|
|
55
|
+
fields: {
|
|
56
|
+
name: string;
|
|
57
|
+
label: string;
|
|
58
|
+
description: string | null;
|
|
59
|
+
sensitive: boolean;
|
|
60
|
+
set: boolean;
|
|
61
|
+
value: string | null;
|
|
62
|
+
}[];
|
|
63
|
+
missing: string[];
|
|
64
|
+
}[];
|
|
65
|
+
}>>;
|
|
66
|
+
export declare function showConnection(host: string, token: string, source: string, product: string): Promise<ConnectionsApiResult<{
|
|
67
|
+
source: {
|
|
68
|
+
id: string;
|
|
69
|
+
slug: string;
|
|
70
|
+
name: string | null;
|
|
71
|
+
};
|
|
72
|
+
connection: {
|
|
73
|
+
slug: string;
|
|
74
|
+
name: string;
|
|
75
|
+
connected: boolean;
|
|
76
|
+
connectedBy: string | null;
|
|
77
|
+
connectedAt: string | null;
|
|
78
|
+
scope: string | null;
|
|
79
|
+
app: "fields" | "source-app";
|
|
80
|
+
fields: {
|
|
81
|
+
name: string;
|
|
82
|
+
label: string;
|
|
83
|
+
description: string | null;
|
|
84
|
+
sensitive: boolean;
|
|
85
|
+
set: boolean;
|
|
86
|
+
value: string | null;
|
|
87
|
+
}[];
|
|
88
|
+
missing: string[];
|
|
89
|
+
};
|
|
90
|
+
callbackUrls: string[];
|
|
91
|
+
}>>;
|
|
92
|
+
export declare function startConnection(host: string, token: string, body: {
|
|
93
|
+
source: string;
|
|
94
|
+
product: string;
|
|
95
|
+
fields?: Record<string, string>;
|
|
96
|
+
}): Promise<ConnectionsApiResult<{
|
|
97
|
+
source: {
|
|
98
|
+
id: string;
|
|
99
|
+
slug: string;
|
|
100
|
+
name: string | null;
|
|
101
|
+
};
|
|
102
|
+
connection: {
|
|
103
|
+
slug: string;
|
|
104
|
+
name: string;
|
|
105
|
+
connected: boolean;
|
|
106
|
+
connectedBy: string | null;
|
|
107
|
+
connectedAt: string | null;
|
|
108
|
+
scope: string | null;
|
|
109
|
+
app: "fields" | "source-app";
|
|
110
|
+
fields: {
|
|
111
|
+
name: string;
|
|
112
|
+
label: string;
|
|
113
|
+
description: string | null;
|
|
114
|
+
sensitive: boolean;
|
|
115
|
+
set: boolean;
|
|
116
|
+
value: string | null;
|
|
117
|
+
}[];
|
|
118
|
+
missing: string[];
|
|
119
|
+
};
|
|
120
|
+
authorizeUrl: string;
|
|
121
|
+
callbackUrl: string;
|
|
122
|
+
}>>;
|
|
123
|
+
export declare function deleteConnection(host: string, token: string, source: string, product: string): Promise<ConnectionsApiResult<{
|
|
124
|
+
removed: boolean;
|
|
125
|
+
}>>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { request } from './auth/ekanos-api.js';
|
|
3
|
+
import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* The CLI's client for a Source's connections (plan 241): an integration a
|
|
6
|
+
* Source admin connects ONCE for the whole Source, used by the Source's
|
|
7
|
+
* provided agents in every team whose template marks it `owner: "source"`.
|
|
8
|
+
*
|
|
9
|
+
* GET /api/partner/connections?source=<slug|id>
|
|
10
|
+
* GET /api/partner/connections/<product>?source=<slug|id>
|
|
11
|
+
* POST /api/partner/connections/<product> { source, fields? } → authorizeUrl
|
|
12
|
+
* DELETE /api/partner/connections/<product>?source=<slug|id>
|
|
13
|
+
*
|
|
14
|
+
* The shapes are read here rather than from `@ekanos/integration-schema`, so
|
|
15
|
+
* this CLI works against a host without a schema release.
|
|
16
|
+
*/
|
|
17
|
+
const SourceRefSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
slug: z.string(),
|
|
20
|
+
name: z.string().nullable(),
|
|
21
|
+
});
|
|
22
|
+
export const SourceConnectionSchema = z.object({
|
|
23
|
+
slug: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
connected: z.boolean(),
|
|
26
|
+
connectedBy: z.string().nullable(),
|
|
27
|
+
connectedAt: z.string().nullable(),
|
|
28
|
+
scope: z.string().nullable(),
|
|
29
|
+
app: z.enum(['source-app', 'fields']),
|
|
30
|
+
fields: z.array(z.object({
|
|
31
|
+
name: z.string(),
|
|
32
|
+
label: z.string(),
|
|
33
|
+
description: z.string().nullable(),
|
|
34
|
+
sensitive: z.boolean(),
|
|
35
|
+
set: z.boolean(),
|
|
36
|
+
value: z.string().nullable(),
|
|
37
|
+
})),
|
|
38
|
+
missing: z.array(z.string()),
|
|
39
|
+
});
|
|
40
|
+
const ListSchema = z.object({
|
|
41
|
+
source: SourceRefSchema,
|
|
42
|
+
connections: z.array(SourceConnectionSchema),
|
|
43
|
+
});
|
|
44
|
+
const ShowSchema = z.object({
|
|
45
|
+
source: SourceRefSchema,
|
|
46
|
+
connection: SourceConnectionSchema,
|
|
47
|
+
callbackUrls: z.array(z.string()),
|
|
48
|
+
});
|
|
49
|
+
const ConnectSchema = z.object({
|
|
50
|
+
source: SourceRefSchema,
|
|
51
|
+
connection: SourceConnectionSchema,
|
|
52
|
+
authorizeUrl: z.string(),
|
|
53
|
+
callbackUrl: z.string(),
|
|
54
|
+
});
|
|
55
|
+
const DisconnectSchema = z.object({ removed: z.boolean() });
|
|
56
|
+
export function listConnections(host, token, source) {
|
|
57
|
+
return call(host, token, ListSchema, null, {
|
|
58
|
+
method: 'GET',
|
|
59
|
+
path: `/api/partner/connections?${new URLSearchParams({ source })}`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function showConnection(host, token, source, product) {
|
|
63
|
+
return call(host, token, ShowSchema, product, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
path: `${route(product)}?${new URLSearchParams({ source })}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function startConnection(host, token, body) {
|
|
69
|
+
return call(host, token, ConnectSchema, body.product, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
path: route(body.product),
|
|
72
|
+
body: Object.assign({ source: body.source }, (body.fields ? { fields: body.fields } : {})),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function deleteConnection(host, token, source, product) {
|
|
76
|
+
return call(host, token, DisconnectSchema, product, {
|
|
77
|
+
method: 'DELETE',
|
|
78
|
+
path: `${route(product)}?${new URLSearchParams({ source })}`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function route(product) {
|
|
82
|
+
return `/api/partner/connections/${encodeURIComponent(product)}`;
|
|
83
|
+
}
|
|
84
|
+
async function call(host, accessToken, schema, product, init) {
|
|
85
|
+
const response = await request(`${host}${init.path}`, {
|
|
86
|
+
method: init.method,
|
|
87
|
+
headers: Object.assign({ accept: 'application/json', authorization: `Bearer ${accessToken}` }, (init.body === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { 'content-type': 'application/json' })),
|
|
90
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
91
|
+
});
|
|
92
|
+
if (response.status === 401 ||
|
|
93
|
+
(response.status >= 300 && response.status < 400)) {
|
|
94
|
+
return { status: 'unauthorized' };
|
|
95
|
+
}
|
|
96
|
+
const body = await readOptionalJson(response);
|
|
97
|
+
const code = typeof body.code === 'string' ? body.code : null;
|
|
98
|
+
const message = typeof body.error === 'string' && body.error ? body.error : null;
|
|
99
|
+
if (response.status === 403 && code === 'MFA_REQUIRED') {
|
|
100
|
+
throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 403) {
|
|
103
|
+
throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', code === 'SOURCE_ADMIN_REQUIRED'
|
|
104
|
+
? "Only a Source admin manages the Source's connections. Ask one to run it."
|
|
105
|
+
: 'Run "ekanos sources" to list the sources you hold a seat on.');
|
|
106
|
+
}
|
|
107
|
+
if (response.status === 404) {
|
|
108
|
+
throw notFoundError(message !== null && message !== void 0 ? message : `"${product !== null && product !== void 0 ? product : 'the Source'}" was not found.`, code === 'PRODUCT_NOT_ENTITLED'
|
|
109
|
+
? '"ekanos catalog" lists what this Source has; Ekanos enables a product for a Source.'
|
|
110
|
+
: 'Run "ekanos sources" to list the sources you hold a seat on.');
|
|
111
|
+
}
|
|
112
|
+
if (response.status === 409 && code === 'NO_SOURCE_CONNECTION') {
|
|
113
|
+
return {
|
|
114
|
+
status: 'unsupported',
|
|
115
|
+
message: message !== null && message !== void 0 ? message : `"${product}" does not support one connection for the Source.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (response.status === 409 && code === 'NOT_CONFIGURED') {
|
|
119
|
+
const parsed = SourceConnectionSchema.safeParse(body.connection);
|
|
120
|
+
return {
|
|
121
|
+
status: 'not-configured',
|
|
122
|
+
missing: Array.isArray(body.missing)
|
|
123
|
+
? body.missing.filter((n) => typeof n === 'string')
|
|
124
|
+
: [],
|
|
125
|
+
connection: parsed.success ? parsed.data : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (response.status === 400) {
|
|
129
|
+
throw usageError(message !== null && message !== void 0 ? message : 'The host refused the request.', 'See "ekanos apps --help".');
|
|
130
|
+
}
|
|
131
|
+
if (response.status === 409) {
|
|
132
|
+
throw invalidStateError(message !== null && message !== void 0 ? message : 'The host refused the request.', '');
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
throw networkError(`${host}${init.path.split('?')[0]} responded ${response.status}.`, response.status >= 500
|
|
136
|
+
? 'The Ekanos deployment returned a server error. Retry shortly.'
|
|
137
|
+
: 'Confirm the host runs a build that includes the connections routes (plan 241).');
|
|
138
|
+
}
|
|
139
|
+
const parsed = schema.safeParse(body.data);
|
|
140
|
+
if (!parsed.success) {
|
|
141
|
+
throw networkError(`${host}/api/partner/connections answered in a shape this CLI does not read.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the connections contract.');
|
|
142
|
+
}
|
|
143
|
+
return { status: 'ok', data: parsed.data };
|
|
144
|
+
}
|
|
145
|
+
async function readOptionalJson(response) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = await response.json();
|
|
148
|
+
return typeof parsed === 'object' && parsed !== null
|
|
149
|
+
? parsed
|
|
150
|
+
: {};
|
|
151
|
+
}
|
|
152
|
+
catch (_a) {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=connections-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections-api.js","sourceRoot":"","sources":["../src/connections-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 connections (plan 241): an integration a\n * Source admin connects ONCE for the whole Source, used by the Source's\n * provided agents in every team whose template marks it `owner: \"source\"`.\n *\n * GET /api/partner/connections?source=<slug|id>\n * GET /api/partner/connections/<product>?source=<slug|id>\n * POST /api/partner/connections/<product> { source, fields? } → authorizeUrl\n * DELETE /api/partner/connections/<product>?source=<slug|id>\n *\n * The shapes are read here rather than from `@ekanos/integration-schema`, so\n * this CLI works against a host without a schema release.\n */\n\nconst SourceRefSchema = z.object({\n id: z.string(),\n slug: z.string(),\n name: z.string().nullable(),\n});\n\nexport const SourceConnectionSchema = z.object({\n slug: z.string(),\n name: z.string(),\n connected: z.boolean(),\n connectedBy: z.string().nullable(),\n connectedAt: z.string().nullable(),\n scope: z.string().nullable(),\n app: z.enum(['source-app', 'fields']),\n fields: z.array(\n z.object({\n name: z.string(),\n label: z.string(),\n description: z.string().nullable(),\n sensitive: z.boolean(),\n set: z.boolean(),\n value: z.string().nullable(),\n }),\n ),\n missing: z.array(z.string()),\n});\nexport type SourceConnection = z.infer<typeof SourceConnectionSchema>;\n\nconst ListSchema = z.object({\n source: SourceRefSchema,\n connections: z.array(SourceConnectionSchema),\n});\nconst ShowSchema = z.object({\n source: SourceRefSchema,\n connection: SourceConnectionSchema,\n callbackUrls: z.array(z.string()),\n});\nconst ConnectSchema = z.object({\n source: SourceRefSchema,\n connection: SourceConnectionSchema,\n authorizeUrl: z.string(),\n callbackUrl: z.string(),\n});\nconst DisconnectSchema = z.object({ removed: z.boolean() });\n\nexport type ConnectionsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' }\n /** The integration supports no Source connection (409 NO_SOURCE_CONNECTION). */\n | { status: 'unsupported'; message: string }\n /** App fields are still missing (409 NOT_CONFIGURED). */\n | {\n status: 'not-configured';\n missing: string[];\n connection: SourceConnection | null;\n };\n\nexport function listConnections(host: string, token: string, source: string) {\n return call(host, token, ListSchema, null, {\n method: 'GET',\n path: `/api/partner/connections?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function showConnection(\n host: string,\n token: string,\n source: string,\n product: string,\n) {\n return call(host, token, ShowSchema, product, {\n method: 'GET',\n path: `${route(product)}?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function startConnection(\n host: string,\n token: string,\n body: { source: string; product: string; fields?: Record<string, string> },\n) {\n return call(host, token, ConnectSchema, body.product, {\n method: 'POST',\n path: route(body.product),\n body: {\n source: body.source,\n ...(body.fields ? { fields: body.fields } : {}),\n },\n });\n}\n\nexport function deleteConnection(\n host: string,\n token: string,\n source: string,\n product: string,\n) {\n return call(host, token, DisconnectSchema, product, {\n method: 'DELETE',\n path: `${route(product)}?${new URLSearchParams({ source })}`,\n });\n}\n\nfunction route(product: string): string {\n return `/api/partner/connections/${encodeURIComponent(product)}`;\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n product: string | null,\n init: { method: 'GET' | 'POST' | 'DELETE'; path: string; body?: unknown },\n): Promise<ConnectionsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\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 connections. 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 ?? `\"${product ?? 'the Source'}\" was not found.`,\n code === 'PRODUCT_NOT_ENTITLED'\n ? '\"ekanos catalog\" lists what this Source has; Ekanos enables a product for a Source.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409 && code === 'NO_SOURCE_CONNECTION') {\n return {\n status: 'unsupported',\n message:\n message ??\n `\"${product}\" does not support one connection for the Source.`,\n };\n }\n if (response.status === 409 && code === 'NOT_CONFIGURED') {\n const parsed = SourceConnectionSchema.safeParse(body.connection);\n return {\n status: 'not-configured',\n missing: Array.isArray(body.missing)\n ? body.missing.filter((n): n is string => typeof n === 'string')\n : [],\n connection: parsed.success ? parsed.data : null,\n };\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos apps --help\".',\n );\n }\n if (response.status === 409) {\n throw invalidStateError(message ?? 'The host refused the request.', '');\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} 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 connections routes (plan 241).',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}/api/partner/connections answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the connections 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"]}
|