xapi-to 0.1.13 → 0.1.15

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.
@@ -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
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * register command: create a new user account
3
- *
4
- * POST /auth/register — no auth required
5
- * Returns apiKey (shown once), claimCode, referralCode, claimUrl, tweetTemplate
6
- * Automatically saves apiKey to ~/.xapi/config.json
7
- *
8
- * Optional referral code (please replace xapito to the actual referral code):
9
- * xapi register --referral-code xapito
10
- * xapi register --referralCode xapito # alias
11
- * xapi register xapito # positional shorthand
12
- */
13
-
14
- import { XAPI_API_HOST, saveConfig, scheme } from '../config.ts';
15
- import { output, err } from '../format.ts';
16
-
17
- interface RegisterResponse {
18
- apiKey: string;
19
- claimCode: string;
20
- referralCode?: string;
21
- claimSessionId: string;
22
- claimUrl: string;
23
- tweetTemplate: string;
24
- user: { id: string; accountType: string };
25
- }
26
-
27
- async function registerAccount(referralCode?: string): Promise<RegisterResponse> {
28
- const controller = new AbortController();
29
- const timer = setTimeout(() => controller.abort(), 15_000);
30
- try {
31
- const res = await fetch(`${scheme(XAPI_API_HOST)}://${XAPI_API_HOST}/api/auth/register`, {
32
- method: 'POST',
33
- headers: { 'Content-Type': 'application/json' },
34
- body: JSON.stringify(referralCode ? { referralCode } : {}),
35
- signal: controller.signal,
36
- });
37
- if (!res.ok) {
38
- const text = await res.text();
39
- throw new Error(`HTTP ${res.status}: ${text.slice(0, 300)}`);
40
- }
41
- return res.json() as Promise<RegisterResponse>;
42
- } finally {
43
- clearTimeout(timer);
44
- }
45
- }
46
-
47
- export async function register(args: string[], flags: Record<string, string>) {
48
- try {
49
- // 邀请码来源优先级:--referral-code > --referralCode > 第一个位置参数
50
- const rawReferral =
51
- flags['referral-code'] ?? flags['referralCode'] ?? args[0];
52
- const referralCode =
53
- typeof rawReferral === 'string' && rawReferral !== 'true' && rawReferral.length > 0
54
- ? rawReferral
55
- : undefined;
56
-
57
- const res = await registerAccount(referralCode);
58
-
59
- saveConfig({ apiKey: res.apiKey });
60
-
61
- output({
62
- apiKey: res.apiKey,
63
- user: res.user,
64
- referralCode: res.referralCode,
65
- claim: {
66
- code: res.claimCode,
67
- sessionId: res.claimSessionId,
68
- url: res.claimUrl,
69
- },
70
- tweetTemplate: res.tweetTemplate,
71
- ...(referralCode ? { referredBy: referralCode } : {}),
72
- note: 'apiKey saved to ~/.xapi/config.json',
73
- }, flags.format as any);
74
- } catch (e: any) {
75
- err('register failed', e.message);
76
- }
77
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * topup command
3
- *
4
- * Generates a payment URL pointing to the xapi frontend topup page.
5
- * All params are optional.
6
- *
7
- * Usage:
8
- * xapi topup [--amount <usd>] [--method stripe|x402]
9
- */
10
-
11
- import { getConfig } from '../config.ts';
12
- import { output } from '../format.ts';
13
-
14
- const TOPUP_BASE_URL = 'https://www.xapi.to/topup/payment';
15
-
16
- export async function topup(args: string[], flags: Record<string, string>) {
17
- const cfg = getConfig();
18
-
19
- const url = new URL(TOPUP_BASE_URL);
20
-
21
- if (cfg.apiKey) url.searchParams.set('apikey', cfg.apiKey);
22
- if (flags.method) url.searchParams.set('method', flags.method);
23
-
24
- const amountStr = flags.amount || args[0];
25
- if (amountStr) {
26
- const amountUsd = parseFloat(amountStr);
27
- if (!isNaN(amountUsd) && amountUsd > 0) {
28
- url.searchParams.set('amount', String(amountUsd));
29
- }
30
- }
31
-
32
- output({ url: url.toString() }, flags.format as any);
33
- }
package/src/config.ts DELETED
@@ -1,69 +0,0 @@
1
- /**
2
- * Config management
3
- * Only apiKey is user-configurable. Host is built-in.
4
- * Reads from env var XAPI_KEY or ~/.xapi/config.json
5
- */
6
-
7
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
8
- import { err } from './format.ts';
9
- import { homedir } from 'os';
10
- import { join } from 'path';
11
-
12
- export const XAPI_ACTION_HOST = process.env.XAPI_ACTION_HOST || 'action.xapi.to'; // action service (capabilities + APIs)
13
- export const XAPI_API_HOST = process.env.XAPI_API_HOST || 'api.xapi.to'; // auth + agent API
14
-
15
- /** Returns https:// for remote hosts, http:// for localhost */
16
- export function scheme(host: string): string {
17
- return host.startsWith('localhost') || host.startsWith('127.') ? 'http' : 'https';
18
- }
19
-
20
- export interface XapiConfig {
21
- actionHost: string;
22
- apiKey?: string;
23
- }
24
-
25
- const CONFIG_DIR = join(homedir(), '.xapi');
26
- const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
27
-
28
- function loadFileConfig(): { apiKey?: string } {
29
- if (!existsSync(CONFIG_FILE)) return {};
30
- try {
31
- return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
32
- } catch {
33
- return {};
34
- }
35
- }
36
-
37
- export function getConfig(): XapiConfig {
38
- const file = loadFileConfig();
39
- return {
40
- actionHost: XAPI_ACTION_HOST,
41
- apiKey: process.env.XAPI_KEY || process.env.XAPI_API_KEY || file.apiKey,
42
- };
43
- }
44
-
45
- export function requireApiKey(cfg: XapiConfig): void {
46
- if (!cfg.apiKey) {
47
- err('API key not configured', 'Run "npx xapi-to register" to create an account, or "npx xapi-to config set apiKey=<key>" to set an existing key.');
48
- }
49
- }
50
-
51
- export function saveConfig(updates: { apiKey?: string }): void {
52
- const current = loadFileConfig();
53
- const merged = { ...current, ...updates };
54
- if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
55
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
56
- }
57
-
58
- export function showConfig(): void {
59
- const cfg = getConfig();
60
- const file = loadFileConfig();
61
- console.log(JSON.stringify({
62
- actionHost: cfg.actionHost,
63
- apiKey: cfg.apiKey ? `${cfg.apiKey.slice(0, 8)}...` : undefined,
64
- source: {
65
- apiKey: (process.env.XAPI_KEY || process.env.XAPI_API_KEY) ? 'env' : file.apiKey ? 'file' : 'none',
66
- },
67
- configFile: CONFIG_FILE,
68
- }, null, 2));
69
- }
package/src/format.ts DELETED
@@ -1,61 +0,0 @@
1
- /**
2
- * Output formatting
3
- * Supports: json (default, machine-readable), pretty (human-readable), table
4
- */
5
-
6
- export type OutputFormat = 'json' | 'pretty' | 'table';
7
-
8
- export function getFormat(): OutputFormat {
9
- const f = process.env.XAPI_OUTPUT || 'json';
10
- if (f === 'pretty' || f === 'table') return f;
11
- return 'json';
12
- }
13
-
14
- export function output(data: unknown, format?: OutputFormat): void {
15
- const fmt = format || getFormat();
16
- if (fmt === 'json') {
17
- console.log(JSON.stringify(data));
18
- return;
19
- }
20
- if (fmt === 'pretty') {
21
- console.log(JSON.stringify(data, null, 2));
22
- return;
23
- }
24
- // table: try to render arrays of objects as a table
25
- if (fmt === 'table' && Array.isArray(data)) {
26
- printTable(data as Record<string, unknown>[]);
27
- return;
28
- }
29
- console.log(JSON.stringify(data, null, 2));
30
- }
31
-
32
- function printTable(rows: Record<string, unknown>[]): void {
33
- if (rows.length === 0) {
34
- console.log('(empty)');
35
- return;
36
- }
37
- const keys = Object.keys(rows[0]);
38
- const widths = keys.map(k =>
39
- Math.min(40, Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length)))
40
- );
41
- const sep = widths.map(w => '-'.repeat(w)).join(' ');
42
- const header = keys.map((k, i) => k.padEnd(widths[i])).join(' ');
43
- console.log(header);
44
- console.log(sep);
45
- for (const row of rows) {
46
- const line = keys.map((k, i) => String(row[k] ?? '').slice(0, widths[i]).padEnd(widths[i])).join(' ');
47
- console.log(line);
48
- }
49
- }
50
-
51
- export function err(msg: string, detail?: unknown): never {
52
- if (process.stderr.isTTY) {
53
- console.error(`Error: ${msg}`);
54
- if (detail !== undefined) console.error(` ${detail}`);
55
- } else {
56
- const out: Record<string, unknown> = { error: msg };
57
- if (detail !== undefined) out.detail = detail;
58
- console.error(JSON.stringify(out));
59
- }
60
- process.exit(1);
61
- }