xapi-to 0.1.12 → 0.1.14
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 +9 -2
- package/dist/index.js +1335 -0
- package/package.json +14 -9
- package/src/client.ts +0 -265
- package/src/codegen.ts +0 -307
- package/src/commands/action.ts +0 -328
- package/src/commands/balance.ts +0 -35
- package/src/commands/config.ts +0 -39
- package/src/commands/oauth.ts +0 -297
- package/src/commands/register.ts +0 -59
- package/src/commands/topup.ts +0 -33
- package/src/config.ts +0 -69
- package/src/format.ts +0 -61
- package/src/index.ts +0 -205
package/src/commands/action.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Top-level action commands: list, search, categories, services, get, call
|
|
3
|
-
* Unified interface for all actions (capabilities + APIs).
|
|
4
|
-
* Use --source capability|api to filter by source type.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getConfig, requireApiKey } from '../config.ts';
|
|
8
|
-
import * as client from '../client.ts';
|
|
9
|
-
import { output, err, getFormat } from '../format.ts';
|
|
10
|
-
import { generateCode, buildDefaultInput, resolveTarget } from '../codegen.ts';
|
|
11
|
-
|
|
12
|
-
const VALID_SOURCES = ['capability', 'api'];
|
|
13
|
-
|
|
14
|
-
// ── Subcommand help texts ────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const LIST_HELP = `xapi list - List all actions
|
|
17
|
-
|
|
18
|
-
USAGE
|
|
19
|
-
xapi list [flags]
|
|
20
|
-
|
|
21
|
-
FLAGS
|
|
22
|
-
--source capability|api Filter by source type
|
|
23
|
-
--category <name> Filter by category
|
|
24
|
-
--service-id <id> Filter by service
|
|
25
|
-
--page N Page number (default: 1)
|
|
26
|
-
--page-size N Results per page
|
|
27
|
-
--format json|pretty|table Output format
|
|
28
|
-
|
|
29
|
-
EXAMPLES
|
|
30
|
-
xapi list
|
|
31
|
-
xapi list --source api --format table
|
|
32
|
-
xapi list --category social --page 2
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
const SEARCH_HELP = `xapi search - Search actions by keyword
|
|
36
|
-
|
|
37
|
-
USAGE
|
|
38
|
-
xapi search <query> [flags]
|
|
39
|
-
|
|
40
|
-
FLAGS
|
|
41
|
-
--source capability|api Filter by source type
|
|
42
|
-
--category <name> Filter by category
|
|
43
|
-
--page N Page number (default: 1)
|
|
44
|
-
--page-size N Results per page
|
|
45
|
-
--format json|pretty|table Output format
|
|
46
|
-
|
|
47
|
-
EXAMPLES
|
|
48
|
-
xapi search twitter
|
|
49
|
-
xapi search "tweet detail" --source api
|
|
50
|
-
xapi search weather --category utility --format table
|
|
51
|
-
`;
|
|
52
|
-
|
|
53
|
-
const GET_HELP = `xapi get - Get action schema
|
|
54
|
-
|
|
55
|
-
USAGE
|
|
56
|
-
xapi get <id> [flags]
|
|
57
|
-
|
|
58
|
-
FLAGS
|
|
59
|
-
--method GET|POST|... Filter by HTTP method
|
|
60
|
-
--code <target> Generate code snippet instead of showing schema
|
|
61
|
-
--format json|pretty|table Output format
|
|
62
|
-
|
|
63
|
-
CODE TARGETS
|
|
64
|
-
curl cURL command
|
|
65
|
-
py, python Python (requests)
|
|
66
|
-
python.requests Python with requests
|
|
67
|
-
py.requests alias for python.requests
|
|
68
|
-
python.httpx Python with httpx
|
|
69
|
-
py.httpx alias for python.httpx
|
|
70
|
-
js, javascript JavaScript (fetch)
|
|
71
|
-
javascript.fetch JavaScript with fetch
|
|
72
|
-
js.fetch alias for javascript.fetch
|
|
73
|
-
javascript.axios JavaScript with axios
|
|
74
|
-
js.axios alias for javascript.axios
|
|
75
|
-
ts, typescript TypeScript (fetch)
|
|
76
|
-
typescript.fetch TypeScript with fetch
|
|
77
|
-
ts.fetch alias for typescript.fetch
|
|
78
|
-
go Go (net/http)
|
|
79
|
-
|
|
80
|
-
EXAMPLES
|
|
81
|
-
xapi get twitter.tweet_detail
|
|
82
|
-
xapi get twitter.tweet_detail --method POST
|
|
83
|
-
xapi get twitter.tweet_detail --code curl
|
|
84
|
-
xapi get twitter.tweet_detail --code python.httpx --format pretty
|
|
85
|
-
`;
|
|
86
|
-
|
|
87
|
-
const CALL_HELP = `xapi call - Execute an action
|
|
88
|
-
|
|
89
|
-
USAGE
|
|
90
|
-
xapi call <id> --input '{"key":"val"}' [flags]
|
|
91
|
-
|
|
92
|
-
FLAGS
|
|
93
|
-
--input <json> Input payload as JSON (required for execution)
|
|
94
|
-
--method GET|POST|... Override HTTP method
|
|
95
|
-
--code <target> Generate code snippet instead of executing
|
|
96
|
-
--format json|pretty|table Output format
|
|
97
|
-
|
|
98
|
-
CODE TARGETS
|
|
99
|
-
curl cURL command
|
|
100
|
-
py, python Python (requests)
|
|
101
|
-
python.requests Python with requests
|
|
102
|
-
py.requests alias for python.requests
|
|
103
|
-
python.httpx Python with httpx
|
|
104
|
-
py.httpx alias for python.httpx
|
|
105
|
-
js, javascript JavaScript (fetch)
|
|
106
|
-
javascript.fetch JavaScript with fetch
|
|
107
|
-
js.fetch alias for javascript.fetch
|
|
108
|
-
javascript.axios JavaScript with axios
|
|
109
|
-
js.axios alias for javascript.axios
|
|
110
|
-
ts, typescript TypeScript (fetch)
|
|
111
|
-
typescript.fetch TypeScript with fetch
|
|
112
|
-
ts.fetch alias for typescript.fetch
|
|
113
|
-
go Go (net/http)
|
|
114
|
-
|
|
115
|
-
EXAMPLES
|
|
116
|
-
xapi call twitter.tweet_detail --input '{"tweet_id":"1234567890"}'
|
|
117
|
-
xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code py
|
|
118
|
-
xapi call twitter.tweet_detail --input '{"tweet_id":"123"}' --code curl --format pretty
|
|
119
|
-
`;
|
|
120
|
-
|
|
121
|
-
/** Print subcommand help and exit if --help flag is set */
|
|
122
|
-
function showHelpIfRequested(flags: Record<string, string>, helpText: string): void {
|
|
123
|
-
if (flags.help) {
|
|
124
|
-
console.log(helpText);
|
|
125
|
-
process.exit(0);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Validate --code flag: check for bare flag and unknown target (fail fast before I/O) */
|
|
130
|
-
function validateCodeFlag(flags: Record<string, string>): void {
|
|
131
|
-
if (flags.code === 'true') {
|
|
132
|
-
err('--code requires a target language, e.g. --code curl, --code py, --code js');
|
|
133
|
-
}
|
|
134
|
-
resolveTarget(flags.code);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Output code snippet respecting --format */
|
|
138
|
-
function outputCode(result: { lang: string; lib: string; code: string }, flags: Record<string, string>) {
|
|
139
|
-
const fmt = flags.format || getFormat();
|
|
140
|
-
if (fmt === 'json') {
|
|
141
|
-
output({ language: result.lang, library: result.lib, code: result.code }, 'json');
|
|
142
|
-
} else {
|
|
143
|
-
console.log(result.code);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Validate and return source filter from --source flag */
|
|
148
|
-
function getSource(flags: Record<string, string>): string | undefined {
|
|
149
|
-
if (!flags.source) return undefined;
|
|
150
|
-
if (!VALID_SOURCES.includes(flags.source)) {
|
|
151
|
-
err(`invalid --source value: "${flags.source}". Must be "capability" or "api".`);
|
|
152
|
-
}
|
|
153
|
-
return flags.source;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function actionList(args: string[], flags: Record<string, string>) {
|
|
157
|
-
showHelpIfRequested(flags, LIST_HELP);
|
|
158
|
-
const cfg = getConfig();
|
|
159
|
-
try {
|
|
160
|
-
const res = await client.actionList(cfg, {
|
|
161
|
-
source: getSource(flags),
|
|
162
|
-
page: flags.page ? parseInt(flags.page) : undefined,
|
|
163
|
-
page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
|
|
164
|
-
category: flags.category,
|
|
165
|
-
service_id: flags['service-id'],
|
|
166
|
-
});
|
|
167
|
-
const actions = (res.actions || []) as any[];
|
|
168
|
-
if (flags.format === 'table') {
|
|
169
|
-
output(actions.map((a: any) => ({
|
|
170
|
-
id: a.id,
|
|
171
|
-
method: a.method ?? '',
|
|
172
|
-
displayName: a.displayName ?? '',
|
|
173
|
-
source: a.source ?? '',
|
|
174
|
-
category: a.meta?.category ?? '',
|
|
175
|
-
status: a.status ?? '',
|
|
176
|
-
cost: a.meta?.cost ?? '',
|
|
177
|
-
})), 'table');
|
|
178
|
-
} else {
|
|
179
|
-
output(res, flags.format as any);
|
|
180
|
-
}
|
|
181
|
-
} catch (e: any) {
|
|
182
|
-
err('list failed', e.message);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function actionSearch(args: string[], flags: Record<string, string>) {
|
|
187
|
-
showHelpIfRequested(flags, SEARCH_HELP);
|
|
188
|
-
const query = args[0];
|
|
189
|
-
if (!query) err('usage: xapi search <query>');
|
|
190
|
-
const cfg = getConfig();
|
|
191
|
-
try {
|
|
192
|
-
const res = await client.actionSearch(query, cfg, {
|
|
193
|
-
source: getSource(flags),
|
|
194
|
-
category: flags.category,
|
|
195
|
-
page: flags.page ? parseInt(flags.page) : undefined,
|
|
196
|
-
page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
|
|
197
|
-
});
|
|
198
|
-
const results = (res.results || []) as any[];
|
|
199
|
-
if (flags.format === 'table') {
|
|
200
|
-
output(results.map((a: any) => ({
|
|
201
|
-
id: a.id,
|
|
202
|
-
method: a.method ?? '',
|
|
203
|
-
displayName: a.displayName ?? '',
|
|
204
|
-
source: a.source ?? '',
|
|
205
|
-
category: a.meta?.category ?? '',
|
|
206
|
-
status: a.status ?? '',
|
|
207
|
-
cost: a.meta?.cost ?? '',
|
|
208
|
-
})), 'table');
|
|
209
|
-
} else {
|
|
210
|
-
output(res, flags.format as any);
|
|
211
|
-
}
|
|
212
|
-
} catch (e: any) {
|
|
213
|
-
err('search failed', e.message);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export async function actionCategories(args: string[], flags: Record<string, string>) {
|
|
218
|
-
const cfg = getConfig();
|
|
219
|
-
try {
|
|
220
|
-
const res = await client.actionCategories(cfg, { source: getSource(flags) });
|
|
221
|
-
if (flags.format === 'table') {
|
|
222
|
-
output(res.categories.map(c => ({ category: c })), 'table');
|
|
223
|
-
} else {
|
|
224
|
-
output(res, flags.format as any);
|
|
225
|
-
}
|
|
226
|
-
} catch (e: any) {
|
|
227
|
-
err('categories failed', e.message);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function actionServices(args: string[], flags: Record<string, string>) {
|
|
232
|
-
const cfg = getConfig();
|
|
233
|
-
try {
|
|
234
|
-
const res = await client.actionServices(cfg, {
|
|
235
|
-
page: flags.page ? parseInt(flags.page) : undefined,
|
|
236
|
-
page_size: flags['page-size'] ? parseInt(flags['page-size']) : undefined,
|
|
237
|
-
category: flags.category,
|
|
238
|
-
});
|
|
239
|
-
const services = (res.services || []) as any[];
|
|
240
|
-
if (flags.format === 'table') {
|
|
241
|
-
output(services.map((s: any) => ({
|
|
242
|
-
id: s.id,
|
|
243
|
-
name: s.name ?? '',
|
|
244
|
-
category: s.category ?? '',
|
|
245
|
-
source: s.source ?? '',
|
|
246
|
-
endpoints: s.endpointCount ?? '',
|
|
247
|
-
status: s.status ?? '',
|
|
248
|
-
})), 'table');
|
|
249
|
-
} else {
|
|
250
|
-
output(res, flags.format as any);
|
|
251
|
-
}
|
|
252
|
-
} catch (e: any) {
|
|
253
|
-
err('services failed', e.message);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export async function actionGet(args: string[], flags: Record<string, string>) {
|
|
258
|
-
showHelpIfRequested(flags, GET_HELP);
|
|
259
|
-
const id = args[0];
|
|
260
|
-
if (!id) err('usage: xapi get <id> [--method GET|POST|DELETE|...]');
|
|
261
|
-
if (flags.code) validateCodeFlag(flags);
|
|
262
|
-
const cfg = getConfig();
|
|
263
|
-
try {
|
|
264
|
-
const res = await client.actionGet(id, cfg);
|
|
265
|
-
const actions = Array.isArray(res) ? res : [res];
|
|
266
|
-
const methodFilter = flags.method?.toUpperCase();
|
|
267
|
-
const filtered = methodFilter
|
|
268
|
-
? actions.filter((a: any) => a.method?.toUpperCase() === methodFilter)
|
|
269
|
-
: actions;
|
|
270
|
-
if (filtered.length === 0) {
|
|
271
|
-
err(`no endpoint found for method "${methodFilter}" in action "${id}"`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (flags.code) {
|
|
275
|
-
if (filtered.length > 1) {
|
|
276
|
-
process.stderr.write(
|
|
277
|
-
`Warning: action "${id}" has ${filtered.length} endpoints; using method "${(filtered[0] as any).method}". Use --method to select a specific one.\n`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
const action = filtered[0] as any;
|
|
281
|
-
const { method: _schemaMethod, ...cleanCodeInput } = buildDefaultInput(action.input ?? {});
|
|
282
|
-
const result = generateCode(flags.code, { actionId: id, input: cleanCodeInput, actionHost: cfg.actionHost, method: action.method });
|
|
283
|
-
outputCode(result, flags);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
output(filtered.length === 1 ? filtered[0] : filtered, flags.format as any);
|
|
288
|
-
} catch (e: any) {
|
|
289
|
-
err('get failed', e.message);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export async function actionCall(args: string[], flags: Record<string, string>) {
|
|
294
|
-
showHelpIfRequested(flags, CALL_HELP);
|
|
295
|
-
const id = args[0];
|
|
296
|
-
if (!id) err('usage: xapi call <id> --input \'{"key":"val"}\'');
|
|
297
|
-
if (flags.code) validateCodeFlag(flags);
|
|
298
|
-
const cfg = getConfig();
|
|
299
|
-
let input: Record<string, unknown> = {};
|
|
300
|
-
if (flags.input) {
|
|
301
|
-
try {
|
|
302
|
-
input = JSON.parse(flags.input);
|
|
303
|
-
} catch {
|
|
304
|
-
err('--input must be valid JSON');
|
|
305
|
-
}
|
|
306
|
-
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
307
|
-
err('--input must be a JSON object');
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// method 作为独立参数传递,兼容 input 内的 method
|
|
311
|
-
const { method: inputMethod, ...cleanInput } = input;
|
|
312
|
-
const method = flags.method?.toUpperCase()
|
|
313
|
-
|| (typeof inputMethod === 'string' ? inputMethod.toUpperCase() : undefined);
|
|
314
|
-
|
|
315
|
-
if (flags.code) {
|
|
316
|
-
const result = generateCode(flags.code, { actionId: id, input: cleanInput, actionHost: cfg.actionHost, method });
|
|
317
|
-
outputCode(result, flags);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
requireApiKey(cfg);
|
|
322
|
-
try {
|
|
323
|
-
const res = await client.actionCall(id, cleanInput, cfg, method);
|
|
324
|
-
output(res, flags.format as any);
|
|
325
|
-
} catch (e: any) {
|
|
326
|
-
err('call failed', e.message);
|
|
327
|
-
}
|
|
328
|
-
}
|
package/src/commands/balance.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* balance command
|
|
3
|
-
* Fetches balance from GET /auth/me
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getConfig, requireApiKey, XAPI_API_HOST, scheme } from '../config.ts';
|
|
7
|
-
import { loginWithApiKey, request } from '../client.ts';
|
|
8
|
-
import { output, err } from '../format.ts';
|
|
9
|
-
|
|
10
|
-
export async function balance(args: string[], flags: Record<string, string>) {
|
|
11
|
-
const cfg = getConfig();
|
|
12
|
-
requireApiKey(cfg);
|
|
13
|
-
|
|
14
|
-
let token: string;
|
|
15
|
-
try {
|
|
16
|
-
const res = await loginWithApiKey(cfg.apiKey!, XAPI_API_HOST);
|
|
17
|
-
token = res.accessToken;
|
|
18
|
-
} catch (e: any) {
|
|
19
|
-
err('login failed', e.message);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const me = await request<{ balance: string; accountType: string; tier: string }>(
|
|
24
|
-
`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/me`,
|
|
25
|
-
{ method: 'GET', headers: { Authorization: `Bearer ${token!}` } },
|
|
26
|
-
);
|
|
27
|
-
output({
|
|
28
|
-
balance: me.balance,
|
|
29
|
-
accountType: me.accountType,
|
|
30
|
-
tier: me.tier,
|
|
31
|
-
}, flags.format as any);
|
|
32
|
-
} catch (e: any) {
|
|
33
|
-
err('balance fetch failed', e.message);
|
|
34
|
-
}
|
|
35
|
-
}
|
package/src/commands/config.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* config commands: show, set, health
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { getConfig, saveConfig, showConfig } from '../config.ts';
|
|
6
|
-
import { healthCheck } from '../client.ts';
|
|
7
|
-
import { output, err } from '../format.ts';
|
|
8
|
-
|
|
9
|
-
export async function configShow(args: string[], flags: Record<string, string>) {
|
|
10
|
-
showConfig();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export async function configSet(args: string[], flags: Record<string, string>) {
|
|
14
|
-
// xapi config set apiKey=xapi_xxx
|
|
15
|
-
if (args.length === 0) err('usage: xapi config set apiKey=<key>');
|
|
16
|
-
const updates: { apiKey?: string } = {};
|
|
17
|
-
for (const arg of args) {
|
|
18
|
-
const eq = arg.indexOf('=');
|
|
19
|
-
if (eq < 1) err(`invalid key=value: ${arg}`);
|
|
20
|
-
const key = arg.slice(0, eq);
|
|
21
|
-
if (key === 'host') err('host is built-in and cannot be configured');
|
|
22
|
-
if (key !== 'apiKey') err(`unknown config key: ${key} (only apiKey is configurable)`);
|
|
23
|
-
updates.apiKey = arg.slice(eq + 1);
|
|
24
|
-
}
|
|
25
|
-
saveConfig(updates);
|
|
26
|
-
console.log(JSON.stringify({ ok: true, updated: Object.keys(updates) }));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function configHealth(args: string[], flags: Record<string, string>) {
|
|
30
|
-
const cfg = getConfig();
|
|
31
|
-
const start = Date.now();
|
|
32
|
-
try {
|
|
33
|
-
await healthCheck(cfg);
|
|
34
|
-
output({ status: 'ok', host: cfg.actionHost, latency_ms: Date.now() - start }, flags.format as any);
|
|
35
|
-
} catch (e: any) {
|
|
36
|
-
output({ status: 'error', host: cfg.actionHost, error: e.message }, flags.format as any);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
}
|
package/src/commands/oauth.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* oauth commands: bind, status, unbind
|
|
3
|
-
*
|
|
4
|
-
* Flow for `xapi oauth bind [--provider twitter]`:
|
|
5
|
-
* 1. Login with current API key → get JWT
|
|
6
|
-
* 2. List API keys → find the one matching the current key by prefix
|
|
7
|
-
* 3. Enable OAuth on the key if not already (POST /keys/:id/enable-oauth)
|
|
8
|
-
* 4. List OAuth providers → find the requested provider
|
|
9
|
-
* 5. POST /oauth/authorize → get authorizationUrl
|
|
10
|
-
* 6. Open browser (macOS/Linux/Windows) and poll for binding completion
|
|
11
|
-
*
|
|
12
|
-
* `xapi oauth status`: list current OAuth bindings for the API key
|
|
13
|
-
* `xapi oauth unbind <binding-id>`: delete an OAuth binding
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { spawnSync } from 'child_process';
|
|
17
|
-
import { XAPI_API_HOST, getConfig, requireApiKey } from '../config.ts';
|
|
18
|
-
import {
|
|
19
|
-
loginWithApiKey,
|
|
20
|
-
listKeys,
|
|
21
|
-
enableOAuthForKey,
|
|
22
|
-
listOAuthProviders,
|
|
23
|
-
initiateOAuth,
|
|
24
|
-
listOAuthBindings,
|
|
25
|
-
deleteOAuthBinding,
|
|
26
|
-
} from '../client.ts';
|
|
27
|
-
import { output, err } from '../format.ts';
|
|
28
|
-
|
|
29
|
-
/** Try to open a URL in the default browser. Silent on failure. */
|
|
30
|
-
function openBrowser(url: string): void {
|
|
31
|
-
const cmd = process.platform === 'win32' ? 'start'
|
|
32
|
-
: process.platform === 'darwin' ? 'open'
|
|
33
|
-
: 'xdg-open';
|
|
34
|
-
try {
|
|
35
|
-
spawnSync(cmd, [url], { stdio: 'ignore' });
|
|
36
|
-
} catch {
|
|
37
|
-
// ignore — user can open manually
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Poll bindings until one for (apiKeyId, providerId) appears.
|
|
43
|
-
* Shows a live countdown in TTY mode.
|
|
44
|
-
* Returns the matched binding or null on timeout.
|
|
45
|
-
*/
|
|
46
|
-
async function pollForBinding(
|
|
47
|
-
apiKeyId: string,
|
|
48
|
-
providerId: string,
|
|
49
|
-
jwtToken: string,
|
|
50
|
-
timeoutMs = 5 * 60 * 1000,
|
|
51
|
-
intervalMs = 3000,
|
|
52
|
-
): Promise<{ providerAccountName: string | null } | null> {
|
|
53
|
-
const deadline = Date.now() + timeoutMs;
|
|
54
|
-
const isTTY = process.stdout.isTTY;
|
|
55
|
-
|
|
56
|
-
while (Date.now() < deadline) {
|
|
57
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
61
|
-
const match = Array.isArray(bindings)
|
|
62
|
-
? bindings.find((b) => b.apiKeyId === apiKeyId && b.providerId === providerId)
|
|
63
|
-
: null;
|
|
64
|
-
if (match) return match;
|
|
65
|
-
} catch {
|
|
66
|
-
// transient error — keep polling
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (isTTY) {
|
|
70
|
-
const remaining = Math.ceil((deadline - Date.now()) / 1000);
|
|
71
|
-
process.stdout.write(`\r Waiting for authorization... (${remaining}s remaining) `);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
async function loginAndGetJwt(apiKey: string): Promise<string> {
|
|
82
|
-
const result = await loginWithApiKey(apiKey, XAPI_API_HOST) as any;
|
|
83
|
-
if (!result?.accessToken) {
|
|
84
|
-
throw new Error('Login failed: no access token returned');
|
|
85
|
-
}
|
|
86
|
-
return result.accessToken;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Find the API key record that corresponds to the current plaintext API key.
|
|
91
|
-
* Matches by key prefix (first 7 chars of the plaintext key = keyPrefix).
|
|
92
|
-
*/
|
|
93
|
-
async function findCurrentKeyRecord(
|
|
94
|
-
plaintextKey: string,
|
|
95
|
-
jwtToken: string,
|
|
96
|
-
): Promise<{ id: string; name: string; keyPreview: string; oauthEnabled: boolean }> {
|
|
97
|
-
const keys = await listKeys(jwtToken, XAPI_API_HOST);
|
|
98
|
-
if (!Array.isArray(keys) || keys.length === 0) {
|
|
99
|
-
throw new Error('No API keys found for this account');
|
|
100
|
-
}
|
|
101
|
-
if (keys.length === 1) return keys[0];
|
|
102
|
-
// Match by prefix: keyPreview starts with the key's prefix
|
|
103
|
-
const prefix = plaintextKey.substring(0, 7);
|
|
104
|
-
const match = keys.find((k) => k.keyPreview.startsWith(prefix));
|
|
105
|
-
if (!match) {
|
|
106
|
-
// Fallback: use the first key
|
|
107
|
-
return keys[0];
|
|
108
|
-
}
|
|
109
|
-
return match;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── Help text ──────────────────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
export const OAUTH_HELP = `xapi oauth - Manage OAuth bindings
|
|
115
|
-
|
|
116
|
-
USAGE
|
|
117
|
-
xapi oauth <command> [flags]
|
|
118
|
-
|
|
119
|
-
COMMANDS
|
|
120
|
-
bind [--provider <name>] Bind an OAuth account to your API key
|
|
121
|
-
status List current OAuth bindings
|
|
122
|
-
unbind <binding-id> Remove an OAuth binding
|
|
123
|
-
providers List available OAuth providers
|
|
124
|
-
|
|
125
|
-
FLAGS
|
|
126
|
-
--provider <name> OAuth provider (default: twitter)
|
|
127
|
-
--format json|pretty|table Output format
|
|
128
|
-
|
|
129
|
-
EXAMPLES
|
|
130
|
-
xapi oauth bind
|
|
131
|
-
xapi oauth bind --provider twitter
|
|
132
|
-
xapi oauth status
|
|
133
|
-
xapi oauth status --format pretty
|
|
134
|
-
xapi oauth unbind abc123
|
|
135
|
-
xapi oauth providers
|
|
136
|
-
`;
|
|
137
|
-
|
|
138
|
-
// ── Commands ───────────────────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* xapi oauth bind [--provider twitter]
|
|
142
|
-
*
|
|
143
|
-
* Initiates OAuth binding for the current API key.
|
|
144
|
-
* Prints the authorization URL for the user to open in a browser.
|
|
145
|
-
*/
|
|
146
|
-
export async function oauthBind(args: string[], flags: Record<string, string>) {
|
|
147
|
-
const cfg = getConfig();
|
|
148
|
-
requireApiKey(cfg);
|
|
149
|
-
const apiKey = cfg.apiKey!;
|
|
150
|
-
const providerName = (flags.provider || 'twitter').toLowerCase();
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// 1. Login to get JWT
|
|
154
|
-
const jwtToken = await loginAndGetJwt(apiKey);
|
|
155
|
-
|
|
156
|
-
// 2. Find the API key record
|
|
157
|
-
const keyRecord = await findCurrentKeyRecord(apiKey, jwtToken);
|
|
158
|
-
|
|
159
|
-
// 3. Enable OAuth on the key if needed
|
|
160
|
-
if (!keyRecord.oauthEnabled) {
|
|
161
|
-
await enableOAuthForKey(keyRecord.id, apiKey, jwtToken, XAPI_API_HOST);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 4. Find the requested OAuth provider
|
|
165
|
-
const providers = await listOAuthProviders(XAPI_API_HOST);
|
|
166
|
-
if (!Array.isArray(providers) || providers.length === 0) {
|
|
167
|
-
throw new Error('No OAuth providers available');
|
|
168
|
-
}
|
|
169
|
-
const provider = providers.find(
|
|
170
|
-
(p) =>
|
|
171
|
-
p.type.toLowerCase() === providerName ||
|
|
172
|
-
p.name.toLowerCase().includes(providerName),
|
|
173
|
-
);
|
|
174
|
-
if (!provider) {
|
|
175
|
-
const available = providers.map((p) => p.type).join(', ');
|
|
176
|
-
throw new Error(
|
|
177
|
-
`Provider "${providerName}" not found. Available: ${available}`,
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 5. Initiate OAuth authorization
|
|
182
|
-
const result = await initiateOAuth(keyRecord.id, provider.id, jwtToken, XAPI_API_HOST);
|
|
183
|
-
const { authorizationUrl } = result;
|
|
184
|
-
|
|
185
|
-
const isTTY = process.stdout.isTTY;
|
|
186
|
-
|
|
187
|
-
if (isTTY) {
|
|
188
|
-
// Interactive mode: open browser + poll
|
|
189
|
-
console.error(`\n Provider : ${provider.name}`);
|
|
190
|
-
console.error(` API Key : ${keyRecord.keyPreview}`);
|
|
191
|
-
console.error(`\n Authorization URL:\n ${authorizationUrl}\n`);
|
|
192
|
-
console.error(' Opening browser...');
|
|
193
|
-
openBrowser(authorizationUrl);
|
|
194
|
-
console.error(' Waiting for you to complete authorization in the browser...\n');
|
|
195
|
-
|
|
196
|
-
const binding = await pollForBinding(keyRecord.id, provider.id, jwtToken);
|
|
197
|
-
|
|
198
|
-
if (process.stdout.isTTY) process.stdout.write('\n');
|
|
199
|
-
|
|
200
|
-
if (binding) {
|
|
201
|
-
const account = (binding as any).providerAccountName || 'unknown';
|
|
202
|
-
console.error(`\n Authorization complete! Bound to @${account}\n`);
|
|
203
|
-
output({ status: 'success', provider: provider.name, account }, flags.format as any);
|
|
204
|
-
} else {
|
|
205
|
-
err('oauth bind timed out', 'Authorization was not completed within 5 minutes. Run "xapi oauth bind" again.');
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
// Non-interactive / agent mode: just output the URL
|
|
209
|
-
output({
|
|
210
|
-
status: 'pending',
|
|
211
|
-
provider: provider.name,
|
|
212
|
-
apiKey: keyRecord.keyPreview,
|
|
213
|
-
authorizationUrl,
|
|
214
|
-
}, flags.format as any);
|
|
215
|
-
}
|
|
216
|
-
} catch (e: any) {
|
|
217
|
-
err('oauth bind failed', e.message);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* xapi oauth status
|
|
223
|
-
*
|
|
224
|
-
* Lists all OAuth bindings for the current account.
|
|
225
|
-
*/
|
|
226
|
-
export async function oauthStatus(args: string[], flags: Record<string, string>) {
|
|
227
|
-
const cfg = getConfig();
|
|
228
|
-
requireApiKey(cfg);
|
|
229
|
-
const apiKey = cfg.apiKey!;
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const jwtToken = await loginAndGetJwt(apiKey);
|
|
233
|
-
const bindings = await listOAuthBindings(jwtToken, XAPI_API_HOST);
|
|
234
|
-
|
|
235
|
-
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
236
|
-
output({
|
|
237
|
-
status: 'no_bindings',
|
|
238
|
-
message: 'No OAuth bindings found. Run "xapi oauth bind" to connect an account.',
|
|
239
|
-
}, flags.format as any);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
output({
|
|
244
|
-
status: 'ok',
|
|
245
|
-
count: bindings.length,
|
|
246
|
-
bindings: bindings.map((b) => ({
|
|
247
|
-
id: b.id,
|
|
248
|
-
provider: b.provider.name,
|
|
249
|
-
providerType: b.provider.type,
|
|
250
|
-
account: b.providerAccountName || b.providerAccountId,
|
|
251
|
-
apiKeyId: b.apiKeyId,
|
|
252
|
-
scopes: b.scopes,
|
|
253
|
-
boundAt: b.createdAt,
|
|
254
|
-
})),
|
|
255
|
-
}, flags.format as any);
|
|
256
|
-
} catch (e: any) {
|
|
257
|
-
err('oauth status failed', e.message);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* xapi oauth unbind <binding-id>
|
|
263
|
-
*
|
|
264
|
-
* Deletes an OAuth binding. Get the ID from `xapi oauth status`.
|
|
265
|
-
*/
|
|
266
|
-
export async function oauthUnbind(args: string[], flags: Record<string, string>) {
|
|
267
|
-
const cfg = getConfig();
|
|
268
|
-
requireApiKey(cfg);
|
|
269
|
-
const apiKey = cfg.apiKey!;
|
|
270
|
-
|
|
271
|
-
const bindingId = args[0];
|
|
272
|
-
if (!bindingId) {
|
|
273
|
-
err('usage: xapi oauth unbind <binding-id>', 'Get the binding ID from "xapi oauth status"');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const jwtToken = await loginAndGetJwt(apiKey);
|
|
278
|
-
const result = await deleteOAuthBinding(bindingId, jwtToken, XAPI_API_HOST);
|
|
279
|
-
output({ success: result.success, message: 'OAuth binding removed' }, flags.format as any);
|
|
280
|
-
} catch (e: any) {
|
|
281
|
-
err('oauth unbind failed', e.message);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* xapi oauth providers
|
|
287
|
-
*
|
|
288
|
-
* Lists available OAuth providers.
|
|
289
|
-
*/
|
|
290
|
-
export async function oauthProviders(args: string[], flags: Record<string, string>) {
|
|
291
|
-
try {
|
|
292
|
-
const providers = await listOAuthProviders(XAPI_API_HOST);
|
|
293
|
-
output(providers, flags.format as any);
|
|
294
|
-
} catch (e: any) {
|
|
295
|
-
err('oauth providers failed', e.message);
|
|
296
|
-
}
|
|
297
|
-
}
|