@voxli/cli 0.3.2 → 0.4.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/README.md +14 -4
- package/dist/commands/auth.js +23 -15
- package/dist/commands/listen.js +22 -5
- package/dist/lib/api.d.ts +1 -0
- package/dist/lib/api.js +4 -1
- package/dist/lib/browser-auth.d.ts +3 -2
- package/dist/lib/browser-auth.js +39 -22
- package/dist/lib/config.d.ts +6 -1
- package/dist/lib/config.js +28 -5
- package/dist/lib/oauth.d.ts +21 -0
- package/dist/lib/oauth.js +78 -0
- package/dist/types.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,21 @@ Requires Node.js 18+.
|
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
15
|
-
Authenticate with your Voxli
|
|
15
|
+
Authenticate with your Voxli account:
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
18
|
voxli auth
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
This
|
|
21
|
+
This opens your browser to `app.voxli.io` where you log in and approve access. An API key is created automatically and saved to `~/.voxli/config.json`.
|
|
22
|
+
|
|
23
|
+
To enter an API key manually instead:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
voxli auth --manual
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also set the `VOXLI_API_TOKEN` environment variable instead.
|
|
22
30
|
|
|
23
31
|
## Usage
|
|
24
32
|
|
|
@@ -32,7 +40,7 @@ The CLI polls the Voxli API for pending test batches. When work arrives, it spaw
|
|
|
32
40
|
|
|
33
41
|
| Variable | Description |
|
|
34
42
|
|---|---|
|
|
35
|
-
| `
|
|
43
|
+
| `VOXLI_API_TOKEN` | Your API key |
|
|
36
44
|
| `TEST_RESULT_IDS` | JSON array of test result IDs to run |
|
|
37
45
|
| `RUN_ID` | The run ID (if part of a run) |
|
|
38
46
|
|
|
@@ -40,5 +48,7 @@ The CLI polls the Voxli API for pending test batches. When work arrives, it spaw
|
|
|
40
48
|
|
|
41
49
|
| Command | Description |
|
|
42
50
|
|---|---|
|
|
43
|
-
| `voxli auth` | Authenticate
|
|
51
|
+
| `voxli auth` | Authenticate via browser |
|
|
52
|
+
| `voxli auth --manual` | Authenticate by entering an API key manually |
|
|
44
53
|
| `voxli listen --command <cmd>` | Poll for pending test work and run it locally |
|
|
54
|
+
|
package/dist/commands/auth.js
CHANGED
|
@@ -4,54 +4,62 @@ import { writeConfig } from "../lib/config.js";
|
|
|
4
4
|
import { register, ApiError } from "../lib/api.js";
|
|
5
5
|
import { getStableHostname } from "../lib/hostname.js";
|
|
6
6
|
import { browserAuth } from "../lib/browser-auth.js";
|
|
7
|
-
async function
|
|
7
|
+
async function promptForToken() {
|
|
8
8
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
9
9
|
try {
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
10
|
+
const token = await rl.question("Enter your Voxli API key: ");
|
|
11
|
+
if (!token.trim()) {
|
|
12
12
|
console.error("API key cannot be empty.");
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
-
return
|
|
15
|
+
return token.trim();
|
|
16
16
|
}
|
|
17
17
|
finally {
|
|
18
18
|
rl.close();
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
async function validateAndSave(
|
|
21
|
+
async function validateAndSave(token, extra) {
|
|
22
|
+
const label = extra ? "Access token" : "API key";
|
|
22
23
|
console.log("Validating...");
|
|
23
24
|
try {
|
|
24
25
|
const hostname = getStableHostname();
|
|
25
|
-
await register(
|
|
26
|
+
await register(token, {
|
|
26
27
|
name: hostname,
|
|
27
28
|
unique_identifier: hostname,
|
|
28
29
|
});
|
|
29
|
-
console.log(
|
|
30
|
+
console.log(`${label} is valid.`);
|
|
30
31
|
}
|
|
31
32
|
catch (err) {
|
|
32
33
|
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
|
|
33
|
-
console.error(`Authentication failed (${err.status}). Check your
|
|
34
|
+
console.error(`Authentication failed (${err.status}). Check your ${label.toLowerCase()}.`);
|
|
34
35
|
process.exit(1);
|
|
35
36
|
}
|
|
36
37
|
// Network error or other — warn but still save
|
|
37
|
-
console.warn("Warning: could not validate
|
|
38
|
+
console.warn("Warning: could not validate token (network error). Saving anyway.");
|
|
38
39
|
}
|
|
39
|
-
await writeConfig({
|
|
40
|
-
|
|
40
|
+
await writeConfig({
|
|
41
|
+
accessToken: token,
|
|
42
|
+
refreshToken: extra?.refreshToken,
|
|
43
|
+
clientId: extra?.clientId,
|
|
44
|
+
});
|
|
45
|
+
console.log(`${label} saved to ~/.voxli/config.json`);
|
|
41
46
|
}
|
|
42
47
|
export async function authCommand(opts) {
|
|
43
48
|
if (!opts.manual) {
|
|
44
49
|
try {
|
|
45
50
|
const result = await browserAuth();
|
|
46
|
-
await validateAndSave(result.
|
|
51
|
+
await validateAndSave(result.accessToken, {
|
|
52
|
+
refreshToken: result.refreshToken,
|
|
53
|
+
clientId: result.clientId,
|
|
54
|
+
});
|
|
47
55
|
return;
|
|
48
56
|
}
|
|
49
57
|
catch (err) {
|
|
50
58
|
const msg = err instanceof Error ? err.message : String(err);
|
|
51
59
|
console.log(`\nBrowser auth failed: ${msg}`);
|
|
52
|
-
console.log("Falling back to manual
|
|
60
|
+
console.log("Falling back to manual token entry.\n");
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
|
-
const
|
|
56
|
-
await validateAndSave(
|
|
63
|
+
const token = await promptForToken();
|
|
64
|
+
await validateAndSave(token);
|
|
57
65
|
}
|
package/dist/commands/listen.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { resolveApiKeyAsync } from "../lib/config.js";
|
|
2
|
+
import { resolveApiKeyAsync, resolveApiKey, attemptTokenRefresh, } from "../lib/config.js";
|
|
3
3
|
import { getStableHostname } from "../lib/hostname.js";
|
|
4
4
|
import { register, ApiError } from "../lib/api.js";
|
|
5
5
|
const POLL_INTERVAL = 5_000;
|
|
6
6
|
export async function listenCommand(options) {
|
|
7
|
-
const
|
|
7
|
+
const isEnvToken = !!resolveApiKey();
|
|
8
|
+
let apiKey = await resolveApiKeyAsync();
|
|
8
9
|
if (!apiKey) {
|
|
9
|
-
console.error("Error: No API key found. Set
|
|
10
|
+
console.error("Error: No API key found. Set VOXLI_API_TOKEN or run `voxli auth`.");
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
const hostname = getStableHostname();
|
|
@@ -35,7 +36,7 @@ export async function listenCommand(options) {
|
|
|
35
36
|
console.log(`Spawning subprocess for ${testResultIds.length} test(s) (${label})`);
|
|
36
37
|
const env = {
|
|
37
38
|
...process.env,
|
|
38
|
-
|
|
39
|
+
VOXLI_API_TOKEN: apiKey,
|
|
39
40
|
VOXLI_API_URL: process.env.VOXLI_API_URL,
|
|
40
41
|
VOXLI_APP_URL: process.env.VOXLI_APP_URL,
|
|
41
42
|
TEST_RESULT_IDS: JSON.stringify(testResultIds),
|
|
@@ -64,7 +65,23 @@ export async function listenCommand(options) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
catch (err) {
|
|
67
|
-
if (err instanceof ApiError
|
|
68
|
+
if (err instanceof ApiError &&
|
|
69
|
+
(err.status === 401 || err.status === 403)) {
|
|
70
|
+
if (isEnvToken) {
|
|
71
|
+
console.error(`Error: Authentication failed (${err.status}). Your VOXLI_API_TOKEN environment variable may be expired or invalid.`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
console.log("Access token expired, attempting refresh...");
|
|
75
|
+
const newToken = await attemptTokenRefresh();
|
|
76
|
+
if (newToken) {
|
|
77
|
+
apiKey = newToken;
|
|
78
|
+
console.log("Token refreshed successfully.");
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
console.error("Error: Could not refresh access token. Please re-authenticate with `voxli auth`.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
else if (err instanceof ApiError) {
|
|
68
85
|
console.error(`Poll error: API ${err.status}`);
|
|
69
86
|
}
|
|
70
87
|
else {
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RegisterPayload, RegisterResponse } from "../types.js";
|
|
2
|
+
export declare function getApiBaseUrl(): string;
|
|
2
3
|
export declare function register(apiKey: string, payload: RegisterPayload): Promise<RegisterResponse>;
|
|
3
4
|
export declare class ApiError extends Error {
|
|
4
5
|
status: number;
|
package/dist/lib/api.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const DEFAULT_BASE_URL = "https://api.voxli.io";
|
|
2
|
-
function
|
|
2
|
+
export function getApiBaseUrl() {
|
|
3
3
|
return process.env.VOXLI_API_URL || DEFAULT_BASE_URL;
|
|
4
4
|
}
|
|
5
|
+
function getBaseUrl() {
|
|
6
|
+
return getApiBaseUrl();
|
|
7
|
+
}
|
|
5
8
|
export async function register(apiKey, payload) {
|
|
6
9
|
const url = `${getBaseUrl()}/agents/register`;
|
|
7
10
|
const res = await fetch(url, {
|
package/dist/lib/browser-auth.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
1
|
+
import { createServer, } from "node:http";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
|
-
import {
|
|
6
|
+
import { getApiBaseUrl } from "./api.js";
|
|
7
|
+
import { generatePkceChallenge, registerOAuthClient, exchangeCodeForToken, buildAuthorizeUrl, } from "./oauth.js";
|
|
7
8
|
const AUTH_TIMEOUT_MS = 120_000;
|
|
8
|
-
const DEFAULT_APP_URL = "https://app.voxli.io";
|
|
9
|
-
function getAppUrl() {
|
|
10
|
-
return process.env.VOXLI_APP_URL || DEFAULT_APP_URL;
|
|
11
|
-
}
|
|
12
9
|
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
13
10
|
<html>
|
|
14
11
|
<head><meta charset="utf-8"><title>Voxli CLI</title>
|
|
@@ -46,6 +43,7 @@ export async function browserAuth() {
|
|
|
46
43
|
finally {
|
|
47
44
|
rl.close();
|
|
48
45
|
}
|
|
46
|
+
const baseUrl = getApiBaseUrl();
|
|
49
47
|
return new Promise((resolve, reject) => {
|
|
50
48
|
const state = randomBytes(32).toString("hex");
|
|
51
49
|
const server = createServer((req, res) => {
|
|
@@ -55,15 +53,14 @@ export async function browserAuth() {
|
|
|
55
53
|
res.end("Not found");
|
|
56
54
|
return;
|
|
57
55
|
}
|
|
58
|
-
const
|
|
56
|
+
const returnedCode = url.searchParams.get("code");
|
|
59
57
|
const returnedState = url.searchParams.get("state");
|
|
60
|
-
const returnedUserId = url.searchParams.get("user_id");
|
|
61
58
|
if (returnedState !== state) {
|
|
62
59
|
res.writeHead(403, { "Content-Type": "text/html" });
|
|
63
60
|
res.end(ERROR_HTML);
|
|
64
61
|
return;
|
|
65
62
|
}
|
|
66
|
-
if (!
|
|
63
|
+
if (!returnedCode) {
|
|
67
64
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
68
65
|
res.end(ERROR_HTML);
|
|
69
66
|
return;
|
|
@@ -71,8 +68,13 @@ export async function browserAuth() {
|
|
|
71
68
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
72
69
|
res.end(SUCCESS_HTML);
|
|
73
70
|
cleanup();
|
|
74
|
-
|
|
71
|
+
exchangeCodeForToken(baseUrl, returnedCode, codeVerifier, clientId, redirectUri)
|
|
72
|
+
.then(({ accessToken, refreshToken }) => resolve({ accessToken, refreshToken, clientId }))
|
|
73
|
+
.catch(reject);
|
|
75
74
|
});
|
|
75
|
+
let codeVerifier;
|
|
76
|
+
let clientId;
|
|
77
|
+
let redirectUri;
|
|
76
78
|
const timeout = setTimeout(() => {
|
|
77
79
|
cleanup();
|
|
78
80
|
reject(new Error("Browser authentication timed out after 2 minutes."));
|
|
@@ -81,20 +83,35 @@ export async function browserAuth() {
|
|
|
81
83
|
clearTimeout(timeout);
|
|
82
84
|
server.close();
|
|
83
85
|
}
|
|
84
|
-
server.listen(0, "127.0.0.1", () => {
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
server.listen(0, "127.0.0.1", async () => {
|
|
87
|
+
try {
|
|
88
|
+
const addr = server.address();
|
|
89
|
+
if (!addr || typeof addr === "string") {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(new Error("Failed to start local server."));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const port = addr.port;
|
|
95
|
+
redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
96
|
+
const registration = await registerOAuthClient(baseUrl, redirectUri);
|
|
97
|
+
clientId = registration.clientId;
|
|
98
|
+
const pkce = generatePkceChallenge();
|
|
99
|
+
codeVerifier = pkce.codeVerifier;
|
|
100
|
+
const authUrl = buildAuthorizeUrl(baseUrl, {
|
|
101
|
+
clientId,
|
|
102
|
+
redirectUri,
|
|
103
|
+
codeChallenge: pkce.codeChallenge,
|
|
104
|
+
state,
|
|
105
|
+
});
|
|
106
|
+
console.log("Opening browser to authenticate...");
|
|
107
|
+
openBrowser(authUrl);
|
|
108
|
+
console.log("Waiting for authentication (timeout: 2 min)...");
|
|
109
|
+
console.log(`\nIf the browser didn't open, visit:\n ${authUrl}\n`);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
87
112
|
cleanup();
|
|
88
|
-
reject(
|
|
89
|
-
return;
|
|
113
|
+
reject(err);
|
|
90
114
|
}
|
|
91
|
-
const port = addr.port;
|
|
92
|
-
const hostname = encodeURIComponent(getStableHostname());
|
|
93
|
-
const authUrl = `${getAppUrl()}/cli-auth?port=${port}&state=${state}&hostname=${hostname}`;
|
|
94
|
-
console.log("Opening browser to authenticate...");
|
|
95
|
-
openBrowser(authUrl);
|
|
96
|
-
console.log(`Waiting for authentication (timeout: 2 min)...`);
|
|
97
|
-
console.log(`\nIf the browser didn't open, visit:\n ${authUrl}\n`);
|
|
98
115
|
});
|
|
99
116
|
});
|
|
100
117
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { VoxliConfig } from "../types.js";
|
|
2
2
|
export declare function readConfig(): Promise<VoxliConfig | null>;
|
|
3
|
-
export declare function writeConfig(config:
|
|
3
|
+
export declare function writeConfig(config: {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
refreshToken?: string;
|
|
6
|
+
clientId?: string;
|
|
7
|
+
}): Promise<void>;
|
|
4
8
|
export declare function resolveApiKey(): string | null;
|
|
5
9
|
export declare function resolveApiKeyAsync(): Promise<string | null>;
|
|
10
|
+
export declare function attemptTokenRefresh(): Promise<string | null>;
|
package/dist/lib/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { refreshAccessToken } from "./oauth.js";
|
|
5
|
+
import { getApiBaseUrl } from "./api.js";
|
|
4
6
|
const CONFIG_DIR = join(homedir(), ".voxli");
|
|
5
7
|
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
6
8
|
export async function readConfig() {
|
|
@@ -13,14 +15,17 @@ export async function readConfig() {
|
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
export async function writeConfig(config) {
|
|
18
|
+
const data = { accessToken: config.accessToken };
|
|
19
|
+
if (config.refreshToken)
|
|
20
|
+
data.refreshToken = config.refreshToken;
|
|
21
|
+
if (config.clientId)
|
|
22
|
+
data.clientId = config.clientId;
|
|
16
23
|
await mkdir(CONFIG_DIR, { recursive: true });
|
|
17
|
-
await writeFile(CONFIG_PATH, JSON.stringify(
|
|
18
|
-
mode: 0o600,
|
|
19
|
-
});
|
|
24
|
+
await writeFile(CONFIG_PATH, JSON.stringify(data, null, 2) + "\n", { mode: 0o600 });
|
|
20
25
|
await chmod(CONFIG_PATH, 0o600);
|
|
21
26
|
}
|
|
22
27
|
export function resolveApiKey() {
|
|
23
|
-
const envKey = process.env.
|
|
28
|
+
const envKey = process.env.VOXLI_API_TOKEN;
|
|
24
29
|
if (envKey)
|
|
25
30
|
return envKey;
|
|
26
31
|
// Caller should await readConfig() for the file-based key
|
|
@@ -31,5 +36,23 @@ export async function resolveApiKeyAsync() {
|
|
|
31
36
|
if (envKey)
|
|
32
37
|
return envKey;
|
|
33
38
|
const config = await readConfig();
|
|
34
|
-
return config?.apiKey ?? null;
|
|
39
|
+
return config?.accessToken ?? config?.apiKey ?? null;
|
|
40
|
+
}
|
|
41
|
+
export async function attemptTokenRefresh() {
|
|
42
|
+
try {
|
|
43
|
+
const config = await readConfig();
|
|
44
|
+
if (!config?.refreshToken || !config?.clientId)
|
|
45
|
+
return null;
|
|
46
|
+
const baseUrl = getApiBaseUrl();
|
|
47
|
+
const result = await refreshAccessToken(baseUrl, config.refreshToken, config.clientId);
|
|
48
|
+
await writeConfig({
|
|
49
|
+
accessToken: result.accessToken,
|
|
50
|
+
refreshToken: result.refreshToken ?? config.refreshToken,
|
|
51
|
+
clientId: config.clientId,
|
|
52
|
+
});
|
|
53
|
+
return result.accessToken;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
35
58
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function generatePkceChallenge(): {
|
|
2
|
+
codeVerifier: string;
|
|
3
|
+
codeChallenge: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function registerOAuthClient(baseUrl: string, redirectUri: string): Promise<{
|
|
6
|
+
clientId: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function exchangeCodeForToken(baseUrl: string, code: string, codeVerifier: string, clientId: string, redirectUri: string): Promise<{
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken?: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function refreshAccessToken(baseUrl: string, refreshToken: string, clientId: string): Promise<{
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function buildAuthorizeUrl(baseUrl: string, params: {
|
|
17
|
+
clientId: string;
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
codeChallenge: string;
|
|
20
|
+
state: string;
|
|
21
|
+
}): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
2
|
+
export function generatePkceChallenge() {
|
|
3
|
+
const codeVerifier = randomBytes(32).toString("base64url");
|
|
4
|
+
const codeChallenge = createHash("sha256")
|
|
5
|
+
.update(codeVerifier)
|
|
6
|
+
.digest("base64url");
|
|
7
|
+
return { codeVerifier, codeChallenge };
|
|
8
|
+
}
|
|
9
|
+
export async function registerOAuthClient(baseUrl, redirectUri) {
|
|
10
|
+
const res = await fetch(`${baseUrl}/oauth/register`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({
|
|
14
|
+
client_name: "Voxli CLI",
|
|
15
|
+
redirect_uris: [redirectUri],
|
|
16
|
+
grant_types: ["authorization_code"],
|
|
17
|
+
response_types: ["code"],
|
|
18
|
+
token_endpoint_auth_method: "none",
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`OAuth client registration failed (${res.status})`);
|
|
23
|
+
}
|
|
24
|
+
const data = (await res.json());
|
|
25
|
+
return { clientId: data.client_id };
|
|
26
|
+
}
|
|
27
|
+
export async function exchangeCodeForToken(baseUrl, code, codeVerifier, clientId, redirectUri) {
|
|
28
|
+
const body = new URLSearchParams({
|
|
29
|
+
grant_type: "authorization_code",
|
|
30
|
+
code,
|
|
31
|
+
code_verifier: codeVerifier,
|
|
32
|
+
client_id: clientId,
|
|
33
|
+
redirect_uri: redirectUri,
|
|
34
|
+
});
|
|
35
|
+
const res = await fetch(`${baseUrl}/oauth/token`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
38
|
+
body: body.toString(),
|
|
39
|
+
});
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
throw new Error(`Token exchange failed (${res.status})`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
return {
|
|
45
|
+
accessToken: data.access_token,
|
|
46
|
+
refreshToken: data.refresh_token,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function refreshAccessToken(baseUrl, refreshToken, clientId) {
|
|
50
|
+
const body = new URLSearchParams({
|
|
51
|
+
grant_type: "refresh_token",
|
|
52
|
+
refresh_token: refreshToken,
|
|
53
|
+
client_id: clientId,
|
|
54
|
+
});
|
|
55
|
+
const res = await fetch(`${baseUrl}/oauth/token`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
58
|
+
body: body.toString(),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`Token refresh failed (${res.status})`);
|
|
62
|
+
}
|
|
63
|
+
const data = (await res.json());
|
|
64
|
+
return {
|
|
65
|
+
accessToken: data.access_token,
|
|
66
|
+
refreshToken: data.refresh_token,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function buildAuthorizeUrl(baseUrl, params) {
|
|
70
|
+
const url = new URL("/oauth/authorize", baseUrl);
|
|
71
|
+
url.searchParams.set("response_type", "code");
|
|
72
|
+
url.searchParams.set("client_id", params.clientId);
|
|
73
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
74
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
75
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
76
|
+
url.searchParams.set("state", params.state);
|
|
77
|
+
return url.toString();
|
|
78
|
+
}
|
package/dist/types.d.ts
CHANGED