@parix/cli 0.1.5
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 +123 -0
- package/dist/cli.cjs +30 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +28 -0
- package/dist/commands/api.cjs +81 -0
- package/dist/commands/api.d.cts +2 -0
- package/dist/commands/api.d.ts +2 -0
- package/dist/commands/api.js +78 -0
- package/dist/commands/auth.cjs +128 -0
- package/dist/commands/auth.d.cts +2 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +125 -0
- package/dist/commands/database.cjs +259 -0
- package/dist/commands/database.d.cts +2 -0
- package/dist/commands/database.d.ts +2 -0
- package/dist/commands/database.js +256 -0
- package/dist/commands/tb.cjs +242 -0
- package/dist/commands/tb.d.cts +2 -0
- package/dist/commands/tb.d.ts +2 -0
- package/dist/commands/tb.js +239 -0
- package/dist/lib/api-client.cjs +44 -0
- package/dist/lib/api-client.d.cts +10 -0
- package/dist/lib/api-client.d.ts +10 -0
- package/dist/lib/api-client.js +40 -0
- package/dist/lib/config.cjs +24 -0
- package/dist/lib/config.d.cts +10 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/config.js +18 -0
- package/dist/lib/loopback-server.cjs +158 -0
- package/dist/lib/loopback-server.d.cts +20 -0
- package/dist/lib/loopback-server.d.ts +20 -0
- package/dist/lib/loopback-server.js +155 -0
- package/dist/lib/oauth-api.cjs +73 -0
- package/dist/lib/oauth-api.d.cts +31 -0
- package/dist/lib/oauth-api.d.ts +31 -0
- package/dist/lib/oauth-api.js +68 -0
- package/dist/lib/oauth-session.cjs +108 -0
- package/dist/lib/oauth-session.d.cts +32 -0
- package/dist/lib/oauth-session.d.ts +32 -0
- package/dist/lib/oauth-session.js +103 -0
- package/dist/lib/oauth.cjs +58 -0
- package/dist/lib/oauth.d.cts +19 -0
- package/dist/lib/oauth.d.ts +19 -0
- package/dist/lib/oauth.js +49 -0
- package/dist/lib/open-url.cjs +38 -0
- package/dist/lib/open-url.d.cts +1 -0
- package/dist/lib/open-url.d.ts +1 -0
- package/dist/lib/open-url.js +32 -0
- package/dist/lib/output.cjs +20 -0
- package/dist/lib/output.d.cts +6 -0
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.js +15 -0
- package/dist/lib/parix-api.cjs +59 -0
- package/dist/lib/parix-api.d.cts +12 -0
- package/dist/lib/parix-api.d.ts +12 -0
- package/dist/lib/parix-api.js +55 -0
- package/dist/lib/session.cjs +80 -0
- package/dist/lib/session.d.cts +28 -0
- package/dist/lib/session.d.ts +28 -0
- package/dist/lib/session.js +74 -0
- package/dist/lib/tb-payloads.cjs +153 -0
- package/dist/lib/tb-payloads.d.cts +61 -0
- package/dist/lib/tb-payloads.d.ts +61 -0
- package/dist/lib/tb-payloads.js +144 -0
- package/package.json +74 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startLoopbackServer = startLoopbackServer;
|
|
4
|
+
const node_http_1 = require("node:http");
|
|
5
|
+
async function startLoopbackServer(options) {
|
|
6
|
+
const server = (0, node_http_1.createServer)();
|
|
7
|
+
let completed = false;
|
|
8
|
+
let expectedState = options.state ?? null;
|
|
9
|
+
let resolveResult = null;
|
|
10
|
+
let rejectResult = null;
|
|
11
|
+
let timeout = null;
|
|
12
|
+
const clearTimeoutIfNeeded = () => {
|
|
13
|
+
if (timeout) {
|
|
14
|
+
clearTimeout(timeout);
|
|
15
|
+
timeout = null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const settle = (handler, value, reason) => {
|
|
19
|
+
if (completed) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
completed = true;
|
|
23
|
+
clearTimeoutIfNeeded();
|
|
24
|
+
if (!handler) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (reason !== undefined) {
|
|
28
|
+
handler(reason);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (value) {
|
|
32
|
+
handler(value);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const result = new Promise((resolve, reject) => {
|
|
36
|
+
resolveResult = resolve;
|
|
37
|
+
rejectResult = reject;
|
|
38
|
+
});
|
|
39
|
+
server.on('request', (request, response) => {
|
|
40
|
+
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
41
|
+
if (requestUrl.pathname === '/favicon.ico') {
|
|
42
|
+
response.writeHead(204);
|
|
43
|
+
response.end();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (requestUrl.pathname !== options.callbackPath) {
|
|
47
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
48
|
+
response.end('Not found');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const returnedState = requestUrl.searchParams.get('state');
|
|
52
|
+
const code = requestUrl.searchParams.get('code');
|
|
53
|
+
const error = requestUrl.searchParams.get('error');
|
|
54
|
+
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
55
|
+
if (!expectedState || returnedState !== expectedState) {
|
|
56
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
57
|
+
response.end(buildHtml('Authentication failed', 'The callback state did not match.'));
|
|
58
|
+
settle(rejectResult, undefined, new Error('The callback state did not match the original login request.'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (error) {
|
|
62
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
63
|
+
response.end(buildHtml('Authentication failed', errorDescription ?? error));
|
|
64
|
+
settle(rejectResult, undefined, new Error(errorDescription ?? error));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!code) {
|
|
68
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
69
|
+
response.end(buildHtml('Authentication failed', 'No authorization code was returned.'));
|
|
70
|
+
settle(rejectResult, undefined, new Error('No authorization code was returned from the browser callback.'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (options.successPage) {
|
|
74
|
+
response.writeHead(302, {
|
|
75
|
+
'cache-control': 'no-store',
|
|
76
|
+
location: options.successPage.redirectUrl,
|
|
77
|
+
});
|
|
78
|
+
response.end();
|
|
79
|
+
settle(resolveResult, { code });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
response.writeHead(200, {
|
|
83
|
+
'cache-control': 'no-store',
|
|
84
|
+
'content-type': 'text/html; charset=utf-8',
|
|
85
|
+
});
|
|
86
|
+
response.end(buildHtml('Authentication complete', 'You can close the tab and return to the terminal.'));
|
|
87
|
+
settle(resolveResult, { code });
|
|
88
|
+
});
|
|
89
|
+
await listen(server, options.port ?? 0);
|
|
90
|
+
timeout = setTimeout(() => {
|
|
91
|
+
settle(rejectResult, undefined, new Error('Timed out waiting for the browser callback.'));
|
|
92
|
+
void closeServer(server);
|
|
93
|
+
}, options.timeoutMs);
|
|
94
|
+
const address = server.address();
|
|
95
|
+
if (!address || typeof address === 'string') {
|
|
96
|
+
throw new Error('Failed to determine the loopback callback port.');
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
callbackUrl: `http://127.0.0.1:${address.port}${options.callbackPath}`,
|
|
100
|
+
setState: (state) => {
|
|
101
|
+
expectedState = state;
|
|
102
|
+
},
|
|
103
|
+
waitForResult: async () => {
|
|
104
|
+
return await result;
|
|
105
|
+
},
|
|
106
|
+
close: async () => {
|
|
107
|
+
clearTimeoutIfNeeded();
|
|
108
|
+
await closeServer(server);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function listen(server, port) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
server.once('error', reject);
|
|
115
|
+
server.listen({ host: '127.0.0.1', port }, () => {
|
|
116
|
+
server.off('error', reject);
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function closeServer(server) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
server.close((error) => {
|
|
124
|
+
if (error) {
|
|
125
|
+
if ('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING') {
|
|
126
|
+
resolve();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
reject(error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function buildHtml(title, message) {
|
|
137
|
+
return `<!doctype html>
|
|
138
|
+
<html lang="en">
|
|
139
|
+
<head>
|
|
140
|
+
<meta charset="utf-8" />
|
|
141
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
142
|
+
<title>${escapeHtml(title)}</title>
|
|
143
|
+
</head>
|
|
144
|
+
<body style="font-family: sans-serif; padding: 2rem; line-height: 1.5;">
|
|
145
|
+
<h1>${escapeHtml(title)}</h1>
|
|
146
|
+
<p>${escapeHtml(message)}</p>
|
|
147
|
+
<script>window.close()</script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>`;
|
|
150
|
+
}
|
|
151
|
+
function escapeHtml(value) {
|
|
152
|
+
return value
|
|
153
|
+
.replaceAll('&', '&')
|
|
154
|
+
.replaceAll('<', '<')
|
|
155
|
+
.replaceAll('>', '>')
|
|
156
|
+
.replaceAll('"', '"')
|
|
157
|
+
.replaceAll("'", ''');
|
|
158
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface StartLoopbackServerOptions {
|
|
2
|
+
callbackPath: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
successPage?: {
|
|
5
|
+
redirectUrl: string;
|
|
6
|
+
};
|
|
7
|
+
state?: string;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
interface CallbackResult {
|
|
11
|
+
code: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LoopbackServer {
|
|
14
|
+
callbackUrl: string;
|
|
15
|
+
setState: (state: string) => void;
|
|
16
|
+
waitForResult: () => Promise<CallbackResult>;
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function startLoopbackServer(options: StartLoopbackServerOptions): Promise<LoopbackServer>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface StartLoopbackServerOptions {
|
|
2
|
+
callbackPath: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
successPage?: {
|
|
5
|
+
redirectUrl: string;
|
|
6
|
+
};
|
|
7
|
+
state?: string;
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
}
|
|
10
|
+
interface CallbackResult {
|
|
11
|
+
code: string;
|
|
12
|
+
}
|
|
13
|
+
export interface LoopbackServer {
|
|
14
|
+
callbackUrl: string;
|
|
15
|
+
setState: (state: string) => void;
|
|
16
|
+
waitForResult: () => Promise<CallbackResult>;
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function startLoopbackServer(options: StartLoopbackServerOptions): Promise<LoopbackServer>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
export async function startLoopbackServer(options) {
|
|
3
|
+
const server = createServer();
|
|
4
|
+
let completed = false;
|
|
5
|
+
let expectedState = options.state ?? null;
|
|
6
|
+
let resolveResult = null;
|
|
7
|
+
let rejectResult = null;
|
|
8
|
+
let timeout = null;
|
|
9
|
+
const clearTimeoutIfNeeded = () => {
|
|
10
|
+
if (timeout) {
|
|
11
|
+
clearTimeout(timeout);
|
|
12
|
+
timeout = null;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const settle = (handler, value, reason) => {
|
|
16
|
+
if (completed) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
completed = true;
|
|
20
|
+
clearTimeoutIfNeeded();
|
|
21
|
+
if (!handler) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (reason !== undefined) {
|
|
25
|
+
handler(reason);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (value) {
|
|
29
|
+
handler(value);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const result = new Promise((resolve, reject) => {
|
|
33
|
+
resolveResult = resolve;
|
|
34
|
+
rejectResult = reject;
|
|
35
|
+
});
|
|
36
|
+
server.on('request', (request, response) => {
|
|
37
|
+
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
38
|
+
if (requestUrl.pathname === '/favicon.ico') {
|
|
39
|
+
response.writeHead(204);
|
|
40
|
+
response.end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (requestUrl.pathname !== options.callbackPath) {
|
|
44
|
+
response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
45
|
+
response.end('Not found');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const returnedState = requestUrl.searchParams.get('state');
|
|
49
|
+
const code = requestUrl.searchParams.get('code');
|
|
50
|
+
const error = requestUrl.searchParams.get('error');
|
|
51
|
+
const errorDescription = requestUrl.searchParams.get('error_description');
|
|
52
|
+
if (!expectedState || returnedState !== expectedState) {
|
|
53
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
54
|
+
response.end(buildHtml('Authentication failed', 'The callback state did not match.'));
|
|
55
|
+
settle(rejectResult, undefined, new Error('The callback state did not match the original login request.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (error) {
|
|
59
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
60
|
+
response.end(buildHtml('Authentication failed', errorDescription ?? error));
|
|
61
|
+
settle(rejectResult, undefined, new Error(errorDescription ?? error));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!code) {
|
|
65
|
+
response.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
66
|
+
response.end(buildHtml('Authentication failed', 'No authorization code was returned.'));
|
|
67
|
+
settle(rejectResult, undefined, new Error('No authorization code was returned from the browser callback.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (options.successPage) {
|
|
71
|
+
response.writeHead(302, {
|
|
72
|
+
'cache-control': 'no-store',
|
|
73
|
+
location: options.successPage.redirectUrl,
|
|
74
|
+
});
|
|
75
|
+
response.end();
|
|
76
|
+
settle(resolveResult, { code });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
response.writeHead(200, {
|
|
80
|
+
'cache-control': 'no-store',
|
|
81
|
+
'content-type': 'text/html; charset=utf-8',
|
|
82
|
+
});
|
|
83
|
+
response.end(buildHtml('Authentication complete', 'You can close the tab and return to the terminal.'));
|
|
84
|
+
settle(resolveResult, { code });
|
|
85
|
+
});
|
|
86
|
+
await listen(server, options.port ?? 0);
|
|
87
|
+
timeout = setTimeout(() => {
|
|
88
|
+
settle(rejectResult, undefined, new Error('Timed out waiting for the browser callback.'));
|
|
89
|
+
void closeServer(server);
|
|
90
|
+
}, options.timeoutMs);
|
|
91
|
+
const address = server.address();
|
|
92
|
+
if (!address || typeof address === 'string') {
|
|
93
|
+
throw new Error('Failed to determine the loopback callback port.');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
callbackUrl: `http://127.0.0.1:${address.port}${options.callbackPath}`,
|
|
97
|
+
setState: (state) => {
|
|
98
|
+
expectedState = state;
|
|
99
|
+
},
|
|
100
|
+
waitForResult: async () => {
|
|
101
|
+
return await result;
|
|
102
|
+
},
|
|
103
|
+
close: async () => {
|
|
104
|
+
clearTimeoutIfNeeded();
|
|
105
|
+
await closeServer(server);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function listen(server, port) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
server.once('error', reject);
|
|
112
|
+
server.listen({ host: '127.0.0.1', port }, () => {
|
|
113
|
+
server.off('error', reject);
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function closeServer(server) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
server.close((error) => {
|
|
121
|
+
if (error) {
|
|
122
|
+
if ('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING') {
|
|
123
|
+
resolve();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
reject(error);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function buildHtml(title, message) {
|
|
134
|
+
return `<!doctype html>
|
|
135
|
+
<html lang="en">
|
|
136
|
+
<head>
|
|
137
|
+
<meta charset="utf-8" />
|
|
138
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
139
|
+
<title>${escapeHtml(title)}</title>
|
|
140
|
+
</head>
|
|
141
|
+
<body style="font-family: sans-serif; padding: 2rem; line-height: 1.5;">
|
|
142
|
+
<h1>${escapeHtml(title)}</h1>
|
|
143
|
+
<p>${escapeHtml(message)}</p>
|
|
144
|
+
<script>window.close()</script>
|
|
145
|
+
</body>
|
|
146
|
+
</html>`;
|
|
147
|
+
}
|
|
148
|
+
function escapeHtml(value) {
|
|
149
|
+
return value
|
|
150
|
+
.replaceAll('&', '&')
|
|
151
|
+
.replaceAll('<', '<')
|
|
152
|
+
.replaceAll('>', '>')
|
|
153
|
+
.replaceAll('"', '"')
|
|
154
|
+
.replaceAll("'", ''');
|
|
155
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exchangeAuthorizationCode = exchangeAuthorizationCode;
|
|
4
|
+
exports.refreshOAuthTokens = refreshOAuthTokens;
|
|
5
|
+
exports.fetchOAuthUserInfo = fetchOAuthUserInfo;
|
|
6
|
+
const oauth_1 = require("./oauth.cjs");
|
|
7
|
+
const WHITESPACE_REGEX = /\s+/;
|
|
8
|
+
async function exchangeAuthorizationCode(input) {
|
|
9
|
+
const body = new URLSearchParams({
|
|
10
|
+
client_id: oauth_1.PARIX_CLIENT_ID,
|
|
11
|
+
code: input.code,
|
|
12
|
+
code_verifier: input.codeVerifier,
|
|
13
|
+
grant_type: 'authorization_code',
|
|
14
|
+
redirect_uri: (0, oauth_1.buildOAuthCallbackUrl)(input.baseUrl),
|
|
15
|
+
});
|
|
16
|
+
const response = await fetch((0, oauth_1.buildOAuthTokenUrl)(input.baseUrl), {
|
|
17
|
+
body,
|
|
18
|
+
headers: {
|
|
19
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
20
|
+
},
|
|
21
|
+
method: 'POST',
|
|
22
|
+
});
|
|
23
|
+
return await parseTokenResponse(response);
|
|
24
|
+
}
|
|
25
|
+
async function refreshOAuthTokens(input) {
|
|
26
|
+
const body = new URLSearchParams({
|
|
27
|
+
client_id: oauth_1.PARIX_CLIENT_ID,
|
|
28
|
+
grant_type: 'refresh_token',
|
|
29
|
+
refresh_token: input.refreshToken,
|
|
30
|
+
});
|
|
31
|
+
const response = await fetch((0, oauth_1.buildOAuthTokenUrl)(input.baseUrl), {
|
|
32
|
+
body,
|
|
33
|
+
headers: {
|
|
34
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
35
|
+
},
|
|
36
|
+
method: 'POST',
|
|
37
|
+
});
|
|
38
|
+
return await parseTokenResponse(response);
|
|
39
|
+
}
|
|
40
|
+
async function fetchOAuthUserInfo(input) {
|
|
41
|
+
const response = await fetch((0, oauth_1.buildOAuthUserInfoUrl)(input.baseUrl), {
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(await readErrorMessage(response, 'Unable to fetch the authenticated user.'));
|
|
48
|
+
}
|
|
49
|
+
return (await response.json());
|
|
50
|
+
}
|
|
51
|
+
async function parseTokenResponse(response) {
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(await readErrorMessage(response, 'Unable to exchange OAuth tokens.'));
|
|
54
|
+
}
|
|
55
|
+
const payload = (await response.json());
|
|
56
|
+
return {
|
|
57
|
+
accessToken: payload.access_token,
|
|
58
|
+
expiresIn: payload.expires_in,
|
|
59
|
+
refreshToken: payload.refresh_token ?? null,
|
|
60
|
+
scope: payload.scope?.split(WHITESPACE_REGEX).filter(Boolean) ?? [],
|
|
61
|
+
tokenType: payload.token_type,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function readErrorMessage(response, fallback) {
|
|
65
|
+
try {
|
|
66
|
+
const payload = (await response.clone().json());
|
|
67
|
+
return payload.error_description ?? payload.message ?? payload.error ?? fallback;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
return text.trim() || fallback;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface OAuthTokenSet {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresIn: number;
|
|
4
|
+
refreshToken: string | null;
|
|
5
|
+
scope: string[];
|
|
6
|
+
tokenType: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OAuthUserInfo {
|
|
9
|
+
sub: string;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
email_verified?: boolean;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
picture?: string | null;
|
|
14
|
+
organization_id?: string | null;
|
|
15
|
+
organization_name?: string | null;
|
|
16
|
+
organization_slug?: string | null;
|
|
17
|
+
member_role?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function exchangeAuthorizationCode(input: {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
code: string;
|
|
22
|
+
codeVerifier: string;
|
|
23
|
+
}): Promise<OAuthTokenSet>;
|
|
24
|
+
export declare function refreshOAuthTokens(input: {
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
}): Promise<OAuthTokenSet>;
|
|
28
|
+
export declare function fetchOAuthUserInfo(input: {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
}): Promise<OAuthUserInfo>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface OAuthTokenSet {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
expiresIn: number;
|
|
4
|
+
refreshToken: string | null;
|
|
5
|
+
scope: string[];
|
|
6
|
+
tokenType: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OAuthUserInfo {
|
|
9
|
+
sub: string;
|
|
10
|
+
email?: string | null;
|
|
11
|
+
email_verified?: boolean;
|
|
12
|
+
name?: string | null;
|
|
13
|
+
picture?: string | null;
|
|
14
|
+
organization_id?: string | null;
|
|
15
|
+
organization_name?: string | null;
|
|
16
|
+
organization_slug?: string | null;
|
|
17
|
+
member_role?: string | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function exchangeAuthorizationCode(input: {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
code: string;
|
|
22
|
+
codeVerifier: string;
|
|
23
|
+
}): Promise<OAuthTokenSet>;
|
|
24
|
+
export declare function refreshOAuthTokens(input: {
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
refreshToken: string;
|
|
27
|
+
}): Promise<OAuthTokenSet>;
|
|
28
|
+
export declare function fetchOAuthUserInfo(input: {
|
|
29
|
+
accessToken: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
}): Promise<OAuthUserInfo>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildOAuthCallbackUrl, buildOAuthTokenUrl, buildOAuthUserInfoUrl, PARIX_CLIENT_ID } from "./oauth.js";
|
|
2
|
+
const WHITESPACE_REGEX = /\s+/;
|
|
3
|
+
export async function exchangeAuthorizationCode(input) {
|
|
4
|
+
const body = new URLSearchParams({
|
|
5
|
+
client_id: PARIX_CLIENT_ID,
|
|
6
|
+
code: input.code,
|
|
7
|
+
code_verifier: input.codeVerifier,
|
|
8
|
+
grant_type: 'authorization_code',
|
|
9
|
+
redirect_uri: buildOAuthCallbackUrl(input.baseUrl),
|
|
10
|
+
});
|
|
11
|
+
const response = await fetch(buildOAuthTokenUrl(input.baseUrl), {
|
|
12
|
+
body,
|
|
13
|
+
headers: {
|
|
14
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
15
|
+
},
|
|
16
|
+
method: 'POST',
|
|
17
|
+
});
|
|
18
|
+
return await parseTokenResponse(response);
|
|
19
|
+
}
|
|
20
|
+
export async function refreshOAuthTokens(input) {
|
|
21
|
+
const body = new URLSearchParams({
|
|
22
|
+
client_id: PARIX_CLIENT_ID,
|
|
23
|
+
grant_type: 'refresh_token',
|
|
24
|
+
refresh_token: input.refreshToken,
|
|
25
|
+
});
|
|
26
|
+
const response = await fetch(buildOAuthTokenUrl(input.baseUrl), {
|
|
27
|
+
body,
|
|
28
|
+
headers: {
|
|
29
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
30
|
+
},
|
|
31
|
+
method: 'POST',
|
|
32
|
+
});
|
|
33
|
+
return await parseTokenResponse(response);
|
|
34
|
+
}
|
|
35
|
+
export async function fetchOAuthUserInfo(input) {
|
|
36
|
+
const response = await fetch(buildOAuthUserInfoUrl(input.baseUrl), {
|
|
37
|
+
headers: {
|
|
38
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(await readErrorMessage(response, 'Unable to fetch the authenticated user.'));
|
|
43
|
+
}
|
|
44
|
+
return (await response.json());
|
|
45
|
+
}
|
|
46
|
+
async function parseTokenResponse(response) {
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(await readErrorMessage(response, 'Unable to exchange OAuth tokens.'));
|
|
49
|
+
}
|
|
50
|
+
const payload = (await response.json());
|
|
51
|
+
return {
|
|
52
|
+
accessToken: payload.access_token,
|
|
53
|
+
expiresIn: payload.expires_in,
|
|
54
|
+
refreshToken: payload.refresh_token ?? null,
|
|
55
|
+
scope: payload.scope?.split(WHITESPACE_REGEX).filter(Boolean) ?? [],
|
|
56
|
+
tokenType: payload.token_type,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function readErrorMessage(response, fallback) {
|
|
60
|
+
try {
|
|
61
|
+
const payload = (await response.clone().json());
|
|
62
|
+
return payload.error_description ?? payload.message ?? payload.error ?? fallback;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
return text.trim() || fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStoredSession = createStoredSession;
|
|
4
|
+
exports.hydrateStoredSessionOrganization = hydrateStoredSessionOrganization;
|
|
5
|
+
exports.ensureFreshSession = ensureFreshSession;
|
|
6
|
+
const oauth_api_1 = require("./oauth-api.cjs");
|
|
7
|
+
const session_1 = require("./session.cjs");
|
|
8
|
+
const REFRESH_BUFFER_MS = 60_000;
|
|
9
|
+
async function createStoredSession(input) {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const [userInfo, resolvedOrganization] = await Promise.all([
|
|
12
|
+
(0, oauth_api_1.fetchOAuthUserInfo)({
|
|
13
|
+
accessToken: input.tokenSet.accessToken,
|
|
14
|
+
baseUrl: input.baseUrl,
|
|
15
|
+
}),
|
|
16
|
+
fetchResolvedSessionOrganization({
|
|
17
|
+
accessToken: input.tokenSet.accessToken,
|
|
18
|
+
baseUrl: input.baseUrl,
|
|
19
|
+
}).catch(() => null),
|
|
20
|
+
]);
|
|
21
|
+
return {
|
|
22
|
+
version: 2,
|
|
23
|
+
accessToken: input.tokenSet.accessToken,
|
|
24
|
+
accessTokenExpiresAt: new Date(now.getTime() + input.tokenSet.expiresIn * 1000).toISOString(),
|
|
25
|
+
baseUrl: input.baseUrl,
|
|
26
|
+
createdAt: input.createdAt ?? now.toISOString(),
|
|
27
|
+
refreshToken: input.tokenSet.refreshToken,
|
|
28
|
+
scopes: input.tokenSet.scope,
|
|
29
|
+
tokenType: input.tokenSet.tokenType,
|
|
30
|
+
updatedAt: now.toISOString(),
|
|
31
|
+
user: {
|
|
32
|
+
email: userInfo.email ?? null,
|
|
33
|
+
emailVerified: userInfo.email_verified ?? null,
|
|
34
|
+
id: userInfo.sub,
|
|
35
|
+
image: userInfo.picture ?? null,
|
|
36
|
+
name: userInfo.name ?? null,
|
|
37
|
+
},
|
|
38
|
+
organization: {
|
|
39
|
+
id: resolvedOrganization?.id ?? userInfo.organization_id ?? null,
|
|
40
|
+
memberRole: resolvedOrganization?.memberRole ?? userInfo.member_role ?? null,
|
|
41
|
+
name: resolvedOrganization?.name ?? userInfo.organization_name ?? null,
|
|
42
|
+
slug: resolvedOrganization?.slug ?? userInfo.organization_slug ?? null,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function hydrateStoredSessionOrganization(session) {
|
|
47
|
+
const resolvedOrganization = await fetchResolvedSessionOrganization({
|
|
48
|
+
accessToken: session.accessToken,
|
|
49
|
+
baseUrl: session.baseUrl,
|
|
50
|
+
}).catch(() => null);
|
|
51
|
+
if (!resolvedOrganization || organizationsEqual(session.organization, resolvedOrganization)) {
|
|
52
|
+
return session;
|
|
53
|
+
}
|
|
54
|
+
const nextSession = {
|
|
55
|
+
...session,
|
|
56
|
+
organization: resolvedOrganization,
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
};
|
|
59
|
+
await (0, session_1.writeStoredSession)(nextSession);
|
|
60
|
+
return nextSession;
|
|
61
|
+
}
|
|
62
|
+
async function ensureFreshSession(session) {
|
|
63
|
+
if (!shouldRefresh(session)) {
|
|
64
|
+
return session;
|
|
65
|
+
}
|
|
66
|
+
if (!session.refreshToken) {
|
|
67
|
+
throw new Error('The local session has expired and cannot be refreshed. Run `parix auth login` again.');
|
|
68
|
+
}
|
|
69
|
+
const refreshedTokens = await (0, oauth_api_1.refreshOAuthTokens)({
|
|
70
|
+
baseUrl: session.baseUrl,
|
|
71
|
+
refreshToken: session.refreshToken,
|
|
72
|
+
});
|
|
73
|
+
const updatedSession = await createStoredSession({
|
|
74
|
+
baseUrl: session.baseUrl,
|
|
75
|
+
createdAt: session.createdAt,
|
|
76
|
+
tokenSet: refreshedTokens,
|
|
77
|
+
});
|
|
78
|
+
await (0, session_1.writeStoredSession)(updatedSession);
|
|
79
|
+
return updatedSession;
|
|
80
|
+
}
|
|
81
|
+
function shouldRefresh(session) {
|
|
82
|
+
const expiresAt = Date.parse(session.accessTokenExpiresAt);
|
|
83
|
+
return Number.isNaN(expiresAt) || expiresAt <= Date.now() + REFRESH_BUFFER_MS;
|
|
84
|
+
}
|
|
85
|
+
async function fetchResolvedSessionOrganization(input) {
|
|
86
|
+
const response = await fetch(new URL('/api/v1/session', input.baseUrl), {
|
|
87
|
+
headers: {
|
|
88
|
+
authorization: `Bearer ${input.accessToken}`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error('Unable to resolve the authenticated organization.');
|
|
93
|
+
}
|
|
94
|
+
const payload = (await response.json());
|
|
95
|
+
const organization = payload.auth?.organization;
|
|
96
|
+
if (!organization) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
id: typeof organization.id === 'string' ? organization.id : null,
|
|
101
|
+
memberRole: typeof organization.memberRole === 'string' ? organization.memberRole : null,
|
|
102
|
+
name: typeof organization.name === 'string' ? organization.name : null,
|
|
103
|
+
slug: typeof organization.slug === 'string' ? organization.slug : null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function organizationsEqual(left, right) {
|
|
107
|
+
return (left.id === right.id && left.memberRole === right.memberRole && left.name === right.name && left.slug === right.slug);
|
|
108
|
+
}
|