@jinshuju/cli 0.1.1
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/LICENSE +202 -0
- package/README.md +311 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.js +184 -0
- package/dist/cli-bin.d.ts +2 -0
- package/dist/cli-bin.js +8 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +699 -0
- package/dist/commands.d.ts +84 -0
- package/dist/commands.js +1672 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +99 -0
- package/dist/help.d.ts +15 -0
- package/dist/help.js +98 -0
- package/dist/http.d.ts +29 -0
- package/dist/http.js +127 -0
- package/dist/options.d.ts +102 -0
- package/dist/options.js +232 -0
- package/dist/payload.d.ts +12 -0
- package/dist/payload.js +59 -0
- package/dist/progress.d.ts +16 -0
- package/dist/progress.js +17 -0
- package/package.json +43 -0
package/dist/auth.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { clearOAuthConfig, defaultScopes, loadConfig, saveOAuthConfig } from './config.js';
|
|
5
|
+
function base64Url(bytes) {
|
|
6
|
+
return bytes.toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
7
|
+
}
|
|
8
|
+
export function createPkcePair() {
|
|
9
|
+
const verifier = base64Url(randomBytes(32));
|
|
10
|
+
const challenge = base64Url(createHash('sha256').update(verifier).digest());
|
|
11
|
+
return { verifier, challenge };
|
|
12
|
+
}
|
|
13
|
+
export function buildAuthorizationUrl(params) {
|
|
14
|
+
const url = new URL('/oauth/authorize', params.authHost);
|
|
15
|
+
url.searchParams.set('client_id', params.clientId);
|
|
16
|
+
url.searchParams.set('redirect_uri', params.redirectUri);
|
|
17
|
+
url.searchParams.set('response_type', 'code');
|
|
18
|
+
url.searchParams.set('scope', params.scope);
|
|
19
|
+
url.searchParams.set('state', params.state);
|
|
20
|
+
url.searchParams.set('code_challenge', params.codeChallenge);
|
|
21
|
+
url.searchParams.set('code_challenge_method', 'S256');
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|
|
24
|
+
export async function loginWithOAuth(options = {}, opener = openUrl) {
|
|
25
|
+
const config = loadConfig({ configPath: options.configPath, env: options.env, cli: { host: options.host, authHost: options.authHost, clientId: options.clientId } });
|
|
26
|
+
const clientId = options.clientId ?? config.clientId;
|
|
27
|
+
if (!clientId)
|
|
28
|
+
throw new Error('Missing OAuth client id. Set JINSHUJU_OAUTH_CLIENT_ID or config client_id.');
|
|
29
|
+
const state = base64Url(randomBytes(24));
|
|
30
|
+
const { verifier, challenge } = createPkcePair();
|
|
31
|
+
const scope = options.scopes ?? defaultScopes;
|
|
32
|
+
const callback = await listenForOAuthCallback(state, options.port, options.timeoutMs ?? 120_000);
|
|
33
|
+
const authorizeUrl = buildAuthorizationUrl({
|
|
34
|
+
authHost: config.authHost,
|
|
35
|
+
clientId,
|
|
36
|
+
redirectUri: callback.redirectUri,
|
|
37
|
+
scope,
|
|
38
|
+
state,
|
|
39
|
+
codeChallenge: challenge
|
|
40
|
+
});
|
|
41
|
+
if (options.openBrowser !== false)
|
|
42
|
+
await opener(authorizeUrl);
|
|
43
|
+
const code = await callback.code;
|
|
44
|
+
const token = await exchangeAuthorizationCode(config.authHost, clientId, callback.redirectUri, code, verifier);
|
|
45
|
+
const auth = toOAuthConfig(config, clientId, token);
|
|
46
|
+
saveOAuthConfig(config.configPath, auth);
|
|
47
|
+
return { authorizeUrl, callbackUrl: callback.redirectUri, token: auth };
|
|
48
|
+
}
|
|
49
|
+
export async function refreshOAuthToken(config) {
|
|
50
|
+
if (!config.auth?.refresh_token)
|
|
51
|
+
throw new Error('No OAuth refresh token. Run `jinshuju auth login` again.');
|
|
52
|
+
const token = await tokenRequest(config.auth.auth_host, {
|
|
53
|
+
grant_type: 'refresh_token',
|
|
54
|
+
client_id: config.auth.client_id,
|
|
55
|
+
refresh_token: config.auth.refresh_token
|
|
56
|
+
});
|
|
57
|
+
const auth = toOAuthConfig(config, config.auth.client_id, token, config.auth);
|
|
58
|
+
saveOAuthConfig(config.configPath, auth);
|
|
59
|
+
return auth;
|
|
60
|
+
}
|
|
61
|
+
export async function revokeOAuthToken(config) {
|
|
62
|
+
if (!config.auth?.access_token)
|
|
63
|
+
return;
|
|
64
|
+
const url = new URL('/oauth/revoke', config.auth.auth_host);
|
|
65
|
+
await fetch(url, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
68
|
+
body: new URLSearchParams({ client_id: config.auth.client_id, token: config.auth.access_token })
|
|
69
|
+
}).catch(() => undefined);
|
|
70
|
+
clearOAuthConfig(config.configPath);
|
|
71
|
+
}
|
|
72
|
+
export function shouldRefresh(auth, skewMs = 60_000) {
|
|
73
|
+
if (!auth.expires_at)
|
|
74
|
+
return false;
|
|
75
|
+
return Date.parse(auth.expires_at) - skewMs <= Date.now();
|
|
76
|
+
}
|
|
77
|
+
async function exchangeAuthorizationCode(authHost, clientId, redirectUri, code, codeVerifier) {
|
|
78
|
+
return tokenRequest(authHost, {
|
|
79
|
+
grant_type: 'authorization_code',
|
|
80
|
+
client_id: clientId,
|
|
81
|
+
code,
|
|
82
|
+
redirect_uri: redirectUri,
|
|
83
|
+
code_verifier: codeVerifier
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function tokenRequest(authHost, params) {
|
|
87
|
+
const url = new URL('/oauth/token', authHost);
|
|
88
|
+
const response = await fetch(url, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
91
|
+
body: new URLSearchParams(params)
|
|
92
|
+
});
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
const body = text ? JSON.parse(text) : undefined;
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(body?.error_description ?? body?.error ?? response.statusText);
|
|
97
|
+
}
|
|
98
|
+
if (!body?.access_token)
|
|
99
|
+
throw new Error('OAuth token response is missing access_token');
|
|
100
|
+
return body;
|
|
101
|
+
}
|
|
102
|
+
function toOAuthConfig(config, clientId, token, previous) {
|
|
103
|
+
return {
|
|
104
|
+
type: 'oauth',
|
|
105
|
+
auth_host: previous?.auth_host ?? config.authHost,
|
|
106
|
+
client_id: clientId,
|
|
107
|
+
access_token: token.access_token,
|
|
108
|
+
refresh_token: token.refresh_token ?? previous?.refresh_token,
|
|
109
|
+
expires_at: token.expires_in ? new Date(Date.now() + token.expires_in * 1000).toISOString() : previous?.expires_at,
|
|
110
|
+
scope: token.scope ?? previous?.scope
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function listenForOAuthCallback(expectedState, port = 0, timeoutMs = 120_000) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
let settled = false;
|
|
116
|
+
const server = createServer((req, res) => {
|
|
117
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
118
|
+
if (url.pathname !== '/oauth/callback') {
|
|
119
|
+
res.writeHead(404).end('Not found');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const state = url.searchParams.get('state') ?? '';
|
|
123
|
+
const code = url.searchParams.get('code') ?? '';
|
|
124
|
+
const error = url.searchParams.get('error') ?? '';
|
|
125
|
+
if (!safeEqual(state, expectedState)) {
|
|
126
|
+
res.writeHead(400).end('Invalid OAuth state. You can close this tab.');
|
|
127
|
+
callbackReject(new Error('Invalid OAuth state'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (error) {
|
|
131
|
+
res.writeHead(400).end('OAuth authorization failed. You can close this tab.');
|
|
132
|
+
callbackReject(new Error(error));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!code) {
|
|
136
|
+
res.writeHead(400).end('Missing OAuth code. You can close this tab.');
|
|
137
|
+
callbackReject(new Error('Missing OAuth code'));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }).end('Jinshuju CLI login complete. You can close this tab.');
|
|
141
|
+
callbackResolve(code);
|
|
142
|
+
});
|
|
143
|
+
let callbackResolve;
|
|
144
|
+
let callbackReject;
|
|
145
|
+
const code = new Promise((resolveCode, rejectCode) => {
|
|
146
|
+
callbackResolve = (value) => {
|
|
147
|
+
if (settled)
|
|
148
|
+
return;
|
|
149
|
+
settled = true;
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
server.close();
|
|
152
|
+
resolveCode(value);
|
|
153
|
+
};
|
|
154
|
+
callbackReject = (error) => {
|
|
155
|
+
if (settled)
|
|
156
|
+
return;
|
|
157
|
+
settled = true;
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
server.close();
|
|
160
|
+
rejectCode(error);
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const timer = setTimeout(() => callbackReject(new Error('OAuth login timed out')), timeoutMs);
|
|
164
|
+
server.on('error', reject);
|
|
165
|
+
server.listen(port, '127.0.0.1', () => {
|
|
166
|
+
const address = server.address();
|
|
167
|
+
if (!address || typeof address === 'string') {
|
|
168
|
+
reject(new Error('Failed to open OAuth callback server'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
resolve({ redirectUri: `http://127.0.0.1:${address.port}/oauth/callback`, code });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function safeEqual(a, b) {
|
|
176
|
+
const aBuffer = Buffer.from(a);
|
|
177
|
+
const bBuffer = Buffer.from(b);
|
|
178
|
+
return aBuffer.length === bBuffer.length && timingSafeEqual(aBuffer, bBuffer);
|
|
179
|
+
}
|
|
180
|
+
async function openUrl(url) {
|
|
181
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
182
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
183
|
+
await new Promise((resolve) => execFile(command, args, (error) => (error ? resolve() : resolve())));
|
|
184
|
+
}
|
package/dist/cli-bin.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from './cli.js';
|
|
3
|
+
const result = await runCli(process.argv.slice(2));
|
|
4
|
+
if (result.stdout)
|
|
5
|
+
process.stdout.write(result.stdout);
|
|
6
|
+
if (result.stderr)
|
|
7
|
+
process.stderr.write(result.stderr);
|
|
8
|
+
process.exitCode = result.exitCode;
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type HttpClient } from './http.js';
|
|
2
|
+
export type CliResult = {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
export type CliRuntime = {
|
|
8
|
+
env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
|
|
9
|
+
client?: HttpClient;
|
|
10
|
+
stdin?: () => string;
|
|
11
|
+
/** How wide a table may be. Defaults to the terminal, or 120 through a pipe. */
|
|
12
|
+
width?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const VERSION = "0.1.0";
|
|
15
|
+
export declare function terminalWidth(stream?: NodeJS.WriteStream): number;
|
|
16
|
+
export declare function runCli(args?: string[], runtime?: CliRuntime): Promise<CliResult>;
|