@rool-dev/sdk 0.12.4 → 2.0.0-dev.36748aa
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 +438 -759
- package/dist/agents.d.ts +138 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +563 -0
- package/dist/agents.js.map +1 -0
- package/dist/auth-base.d.ts +43 -88
- package/dist/auth-base.d.ts.map +1 -1
- package/dist/auth-base.js +178 -298
- package/dist/auth-base.js.map +1 -1
- package/dist/auth-browser.d.ts +7 -25
- package/dist/auth-browser.d.ts.map +1 -1
- package/dist/auth-browser.js +45 -82
- package/dist/auth-browser.js.map +1 -1
- package/dist/auth-native.d.ts +9 -56
- package/dist/auth-native.d.ts.map +1 -1
- package/dist/auth-native.js +88 -177
- package/dist/auth-native.js.map +1 -1
- package/dist/auth-node.d.ts +32 -32
- package/dist/auth-node.d.ts.map +1 -1
- package/dist/auth-node.js +339 -255
- package/dist/auth-node.js.map +1 -1
- package/dist/auth-url.d.ts +6 -0
- package/dist/auth-url.d.ts.map +1 -0
- package/dist/auth-url.js +30 -0
- package/dist/auth-url.js.map +1 -0
- package/dist/client.d.ts +35 -277
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +323 -618
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +40 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +175 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +174 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +846 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +16 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -19
- package/dist/index.js.map +1 -1
- package/dist/machine.d.ts +51 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +142 -0
- package/dist/machine.js.map +1 -0
- package/dist/problem.d.ts +11 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +20 -0
- package/dist/problem.js.map +1 -0
- package/dist/structured.d.ts +84 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/structured.js +405 -0
- package/dist/structured.js.map +1 -0
- package/dist/types.d.ts +119 -520
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -16
- package/dist/auth.d.ts +0 -85
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -181
- package/dist/auth.js.map +0 -1
- package/dist/client-info.d.ts +0 -18
- package/dist/client-info.d.ts.map +0 -1
- package/dist/client-info.js +0 -25
- package/dist/client-info.js.map +0 -1
- package/dist/event-emitter.d.ts +0 -45
- package/dist/event-emitter.d.ts.map +0 -1
- package/dist/event-emitter.js +0 -86
- package/dist/event-emitter.js.map +0 -1
- package/dist/graphql.d.ts +0 -90
- package/dist/graphql.d.ts.map +0 -1
- package/dist/graphql.js +0 -463
- package/dist/graphql.js.map +0 -1
- package/dist/logger.d.ts +0 -13
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -17
- package/dist/logger.js.map +0 -1
- package/dist/path.d.ts +0 -6
- package/dist/path.d.ts.map +0 -1
- package/dist/path.js +0 -47
- package/dist/path.js.map +0 -1
- package/dist/reroute.d.ts +0 -22
- package/dist/reroute.d.ts.map +0 -1
- package/dist/reroute.js +0 -70
- package/dist/reroute.js.map +0 -1
- package/dist/rest.d.ts +0 -54
- package/dist/rest.d.ts.map +0 -1
- package/dist/rest.js +0 -171
- package/dist/rest.js.map +0 -1
- package/dist/router.d.ts +0 -20
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js +0 -54
- package/dist/router.js.map +0 -1
- package/dist/space-session.d.ts +0 -30
- package/dist/space-session.d.ts.map +0 -1
- package/dist/space-session.js +0 -83
- package/dist/space-session.js.map +0 -1
- package/dist/space.d.ts +0 -161
- package/dist/space.d.ts.map +0 -1
- package/dist/space.js +0 -618
- package/dist/space.js.map +0 -1
- package/dist/subscription.d.ts +0 -36
- package/dist/subscription.d.ts.map +0 -1
- package/dist/subscription.js +0 -419
- package/dist/subscription.js.map +0 -1
- package/dist/webdav.d.ts +0 -171
- package/dist/webdav.d.ts.map +0 -1
- package/dist/webdav.js +0 -481
- package/dist/webdav.js.map +0 -1
package/dist/auth-node.js
CHANGED
|
@@ -1,291 +1,375 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import open from
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
constructor(
|
|
13
|
-
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import open from "open";
|
|
7
|
+
import { resolveAuthUrl } from "./auth-url.js";
|
|
8
|
+
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
9
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
|
+
const MAX_CALLBACK_BODY_BYTES = 64 * 1024;
|
|
11
|
+
export class NodeAuthLoginRequiredError extends Error {
|
|
12
|
+
constructor(message = "Node authentication is required; call login()") {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "NodeAuthLoginRequiredError";
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
}
|
|
17
|
+
export class NodeAuth {
|
|
18
|
+
credentialsPath;
|
|
19
|
+
getTokens;
|
|
20
|
+
getAccessToken;
|
|
21
|
+
authUrl;
|
|
22
|
+
listeners = new Set();
|
|
23
|
+
loginTimeoutMs;
|
|
24
|
+
fetchRequest;
|
|
25
|
+
openUrl;
|
|
26
|
+
refreshInFlight = null;
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
this.authUrl = resolveAuthUrl(config);
|
|
29
|
+
this.credentialsPath = resolveCredentialsPath(this.authUrl, config.profile, config.credentialsPath);
|
|
30
|
+
this.loginTimeoutMs = config.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS;
|
|
31
|
+
this.fetchRequest = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
32
|
+
this.openUrl = config.openUrl ?? ((url) => open(url).then(() => undefined));
|
|
33
|
+
this.getTokens = () => this.resolveTokens();
|
|
34
|
+
this.getAccessToken = async () => {
|
|
35
|
+
const tokens = await this.getTokens();
|
|
36
|
+
if (!tokens)
|
|
37
|
+
throw new NodeAuthLoginRequiredError();
|
|
38
|
+
return tokens.accessToken;
|
|
39
|
+
};
|
|
18
40
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.logger = logger;
|
|
41
|
+
initialize() {
|
|
42
|
+
return this.isAuthenticated();
|
|
22
43
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
get endpointHash() {
|
|
27
|
-
if (!this._authUrl) {
|
|
28
|
-
return 'default';
|
|
29
|
-
}
|
|
30
|
-
return crypto.createHash('sha256').update(this._authUrl).digest('hex').slice(0, 8);
|
|
44
|
+
async isAuthenticated() {
|
|
45
|
+
return (await this.readCredentials()) !== null;
|
|
31
46
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return this.config.credentialsPath;
|
|
35
|
-
}
|
|
36
|
-
const homeDir = os.homedir();
|
|
37
|
-
const configDir = path.join(homeDir, '.config', 'rool');
|
|
38
|
-
// Scope credentials by endpoint to avoid mixing tokens from different environments
|
|
39
|
-
return path.join(configDir, `credentials-${this.endpointHash}.json`);
|
|
47
|
+
login(appName, params = {}) {
|
|
48
|
+
return this.authFlow("login", appName, params);
|
|
40
49
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*/
|
|
44
|
-
get authEndpoint() {
|
|
45
|
-
if (!this._authUrl) {
|
|
46
|
-
throw new Error('Auth URL not set. Ensure RoolClient is configured correctly.');
|
|
47
|
-
}
|
|
48
|
-
return this._authUrl.replace(/\/+$/, '');
|
|
50
|
+
signup(appName, params = {}) {
|
|
51
|
+
return this.authFlow("signup", appName, params);
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return this.readCredentials() !== null;
|
|
53
|
+
onAuthStateChanged(listener) {
|
|
54
|
+
this.listeners.add(listener);
|
|
55
|
+
return () => this.listeners.delete(listener);
|
|
54
56
|
}
|
|
55
|
-
async
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Refresh if expiring in less than 5 minutes
|
|
60
|
-
if (Date.now() >= creds.expires_at - 5 * 60 * 1000) {
|
|
61
|
-
const refreshed = await this.refreshToken(creds);
|
|
62
|
-
if (!refreshed)
|
|
63
|
-
return undefined;
|
|
64
|
-
const freshCreds = this.readCredentials();
|
|
65
|
-
if (!freshCreds)
|
|
66
|
-
return undefined;
|
|
67
|
-
return { accessToken: freshCreds.access_token, roolToken: freshCreds.rool_token ?? '' };
|
|
57
|
+
logout = async () => {
|
|
58
|
+
try {
|
|
59
|
+
await fs.unlink(this.credentialsPath);
|
|
60
|
+
this.emitAuthState(false);
|
|
68
61
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== "ENOENT")
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
async authFlow(flow, appName, params) {
|
|
68
|
+
const state = randomBytes(32).toString("base64url");
|
|
69
|
+
const server = createServer();
|
|
70
|
+
await listen(server);
|
|
71
|
+
const address = server.address();
|
|
72
|
+
if (!address || typeof address === "string") {
|
|
73
|
+
await close(server);
|
|
74
|
+
throw new Error("Could not start the Node authentication callback");
|
|
75
|
+
}
|
|
76
|
+
const redirectUri = `http://127.0.0.1:${address.port}`;
|
|
77
|
+
const loginUrl = new URL(`/${flow}`, new URL(this.authUrl).origin);
|
|
78
|
+
for (const [name, value] of Object.entries(params)) {
|
|
79
|
+
loginUrl.searchParams.set(name, value);
|
|
80
|
+
}
|
|
81
|
+
loginUrl.searchParams.set("redirect_uri", redirectUri);
|
|
82
|
+
loginUrl.searchParams.set("app_name", appName);
|
|
83
|
+
loginUrl.searchParams.set("state", state);
|
|
84
|
+
const callback = receiveLogin(server, state, this.loginTimeoutMs);
|
|
75
85
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
await this.openUrl(loginUrl.toString());
|
|
87
|
+
const credentials = await callback.result;
|
|
88
|
+
await this.writeCredentials({
|
|
89
|
+
access_token: credentials.accessToken,
|
|
90
|
+
refresh_token: credentials.refreshToken,
|
|
91
|
+
rool_token: credentials.roolToken,
|
|
92
|
+
expires_at: credentials.expiresAt,
|
|
93
|
+
auth_url: this.authUrl,
|
|
94
|
+
});
|
|
95
|
+
this.emitAuthState(true);
|
|
81
96
|
}
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
finally {
|
|
98
|
+
callback.cancel();
|
|
99
|
+
await close(server);
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
async login(appName, params) {
|
|
91
|
-
return this.authFlow('login', appName, params);
|
|
102
|
+
emitAuthState(authenticated) {
|
|
103
|
+
for (const listener of this.listeners)
|
|
104
|
+
listener(authenticated);
|
|
92
105
|
}
|
|
93
|
-
async
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const url = new URL(`${origin}/${flow}`);
|
|
103
|
-
url.searchParams.set('redirect_uri', redirectUri);
|
|
104
|
-
url.searchParams.set('app_name', appName);
|
|
105
|
-
if (params) {
|
|
106
|
-
for (const [key, value] of Object.entries(params)) {
|
|
107
|
-
url.searchParams.set(key, value);
|
|
106
|
+
async resolveTokens() {
|
|
107
|
+
let credentials = await this.readCredentials();
|
|
108
|
+
if (!credentials)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (credentials.expires_at <= Date.now() + REFRESH_BUFFER_MS) {
|
|
111
|
+
if (!this.refreshInFlight) {
|
|
112
|
+
this.refreshInFlight = this.refresh(credentials).finally(() => {
|
|
113
|
+
this.refreshInFlight = null;
|
|
114
|
+
});
|
|
108
115
|
}
|
|
116
|
+
credentials = await this.refreshInFlight;
|
|
109
117
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return new Promise((resolve, reject) => {
|
|
115
|
-
const timeout = setTimeout(() => {
|
|
116
|
-
closeAll();
|
|
117
|
-
reject(new Error('Login timed out. Please try again.'));
|
|
118
|
-
}, timeoutMs);
|
|
119
|
-
server.on('authenticated', (tokens) => {
|
|
120
|
-
clearTimeout(timeout);
|
|
121
|
-
const expiresAt = Date.now() + (tokens.expires_in * 1000);
|
|
122
|
-
this.writeCredentials({
|
|
123
|
-
access_token: tokens.id_token, // GCIP returns id_token
|
|
124
|
-
refresh_token: tokens.refresh_token,
|
|
125
|
-
rool_token: tokens.rool_token ?? null,
|
|
126
|
-
expires_at: expiresAt
|
|
127
|
-
});
|
|
128
|
-
this.config.onAuthStateChanged?.(true);
|
|
129
|
-
closeAll();
|
|
130
|
-
resolve();
|
|
131
|
-
});
|
|
132
|
-
server.on('error', (err) => {
|
|
133
|
-
clearTimeout(timeout);
|
|
134
|
-
closeAll();
|
|
135
|
-
reject(err);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
118
|
+
return {
|
|
119
|
+
accessToken: credentials.access_token,
|
|
120
|
+
roolToken: credentials.rool_token ?? "",
|
|
121
|
+
};
|
|
138
122
|
}
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
expires_at: tokens.expiresAt,
|
|
123
|
+
async refresh(credentials) {
|
|
124
|
+
const response = await this.fetchRequest(`${this.authUrl}/refresh`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json" },
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
refresh_token: credentials.refresh_token,
|
|
129
|
+
rool_token: credentials.rool_token ?? null,
|
|
130
|
+
}),
|
|
148
131
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (fs.existsSync(filePath)) {
|
|
153
|
-
fs.unlinkSync(filePath);
|
|
132
|
+
if (response.status === 400 || response.status === 401) {
|
|
133
|
+
await this.logout();
|
|
134
|
+
throw new NodeAuthLoginRequiredError("Node authentication expired; call login() again");
|
|
154
135
|
}
|
|
155
|
-
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`Could not refresh Node authentication: HTTP ${response.status}`);
|
|
138
|
+
}
|
|
139
|
+
const data = (await response.json());
|
|
140
|
+
const accessToken = data.id_token;
|
|
141
|
+
const refreshToken = data.refresh_token;
|
|
142
|
+
const roolToken = data.rool_token;
|
|
143
|
+
const expiresIn = Number(data.expires_in);
|
|
144
|
+
const hasInvalidRoolToken = roolToken !== undefined &&
|
|
145
|
+
roolToken !== null &&
|
|
146
|
+
typeof roolToken !== "string";
|
|
147
|
+
if (typeof accessToken !== "string" ||
|
|
148
|
+
typeof refreshToken !== "string" ||
|
|
149
|
+
hasInvalidRoolToken ||
|
|
150
|
+
!Number.isFinite(expiresIn) ||
|
|
151
|
+
expiresIn <= 0) {
|
|
152
|
+
throw new Error("The authentication refresh response is invalid");
|
|
153
|
+
}
|
|
154
|
+
const refreshed = {
|
|
155
|
+
access_token: accessToken,
|
|
156
|
+
refresh_token: refreshToken,
|
|
157
|
+
rool_token: typeof roolToken === "string"
|
|
158
|
+
? roolToken
|
|
159
|
+
: roolToken === null
|
|
160
|
+
? null
|
|
161
|
+
: (credentials.rool_token ?? null),
|
|
162
|
+
expires_at: Date.now() + expiresIn * 1000,
|
|
163
|
+
auth_url: this.authUrl,
|
|
164
|
+
};
|
|
165
|
+
await this.writeCredentials(refreshed);
|
|
166
|
+
return refreshed;
|
|
156
167
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// ===========================================================================
|
|
160
|
-
readCredentials() {
|
|
168
|
+
async readCredentials() {
|
|
169
|
+
let serialized;
|
|
161
170
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
serialized = await fs.readFile(this.credentialsPath, "utf8");
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error.code === "ENOENT")
|
|
164
175
|
return null;
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
let value;
|
|
179
|
+
try {
|
|
180
|
+
value = JSON.parse(serialized);
|
|
167
181
|
}
|
|
168
182
|
catch {
|
|
169
|
-
|
|
183
|
+
throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
|
|
170
184
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const filePath = this.credentialsPath;
|
|
174
|
-
const dir = path.dirname(filePath);
|
|
175
|
-
try {
|
|
176
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
177
|
-
fs.writeFileSync(filePath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
185
|
+
if (!isStoredCredentials(value)) {
|
|
186
|
+
throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
|
|
178
187
|
}
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
if (value.auth_url &&
|
|
189
|
+
resolveAuthUrl({ authUrl: value.auth_url }) !== this.authUrl) {
|
|
190
|
+
throw new Error(`Node credentials at ${this.credentialsPath} belong to another auth endpoint`);
|
|
181
191
|
}
|
|
192
|
+
return value;
|
|
182
193
|
}
|
|
183
|
-
async
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
async writeCredentials(credentials) {
|
|
195
|
+
const directory = dirname(this.credentialsPath);
|
|
196
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
197
|
+
const temporaryPath = `${this.credentialsPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
186
198
|
try {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (response.status === 400 || response.status === 401) {
|
|
198
|
-
this.logger.warn('[RoolClient] Refresh token expired or invalid. Please login again.');
|
|
199
|
-
this.logout();
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
this.logger.warn(`[RoolClient] Refresh failed: ${response.status}`);
|
|
203
|
-
}
|
|
204
|
-
return undefined;
|
|
199
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
|
|
200
|
+
await fs.rename(temporaryPath, this.credentialsPath);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
try {
|
|
204
|
+
await fs.unlink(temporaryPath);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error.code !== "ENOENT")
|
|
208
|
+
throw error;
|
|
205
209
|
}
|
|
206
|
-
const data = await response.json();
|
|
207
|
-
const newCreds = {
|
|
208
|
-
access_token: data.id_token,
|
|
209
|
-
refresh_token: data.refresh_token || creds.refresh_token,
|
|
210
|
-
rool_token: data.rool_token ?? creds.rool_token,
|
|
211
|
-
expires_at: Date.now() + (Number(data.expires_in) * 1000),
|
|
212
|
-
};
|
|
213
|
-
this.writeCredentials(newCreds);
|
|
214
|
-
return newCreds.access_token;
|
|
215
210
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function resolveCredentialsPath(authUrl, profile, configuredPath) {
|
|
214
|
+
if (configuredPath) {
|
|
215
|
+
if (configuredPath === "~")
|
|
216
|
+
return homedir();
|
|
217
|
+
if (configuredPath.startsWith("~/")) {
|
|
218
|
+
return join(homedir(), configuredPath.slice(2));
|
|
219
219
|
}
|
|
220
|
+
return configuredPath;
|
|
220
221
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
server.emit('authenticated', {
|
|
264
|
-
id_token: idToken,
|
|
265
|
-
refresh_token: refreshToken,
|
|
266
|
-
rool_token: roolToken,
|
|
267
|
-
expires_in: Number(expiresIn)
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
res.writeHead(400);
|
|
272
|
-
res.end('Invalid tokens');
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
res.writeHead(404);
|
|
278
|
-
res.end();
|
|
279
|
-
});
|
|
280
|
-
// Listen on random port
|
|
281
|
-
server.listen(0, '127.0.0.1', () => resolve({
|
|
282
|
-
server,
|
|
283
|
-
closeAll: () => {
|
|
284
|
-
server.close();
|
|
285
|
-
server.closeAllConnections();
|
|
286
|
-
},
|
|
287
|
-
}));
|
|
222
|
+
if (profile && !/^[A-Za-z0-9._-]+$/.test(profile)) {
|
|
223
|
+
throw new Error("Node authentication profile contains invalid characters");
|
|
224
|
+
}
|
|
225
|
+
const endpointHash = createHash("sha256")
|
|
226
|
+
.update(authUrl)
|
|
227
|
+
.digest("hex")
|
|
228
|
+
.slice(0, 8);
|
|
229
|
+
const profileSuffix = profile ? `-${profile}` : "";
|
|
230
|
+
return join(homedir(), ".config", "rool", `credentials-${endpointHash}${profileSuffix}.json`);
|
|
231
|
+
}
|
|
232
|
+
function isStoredCredentials(value) {
|
|
233
|
+
if (!value || typeof value !== "object")
|
|
234
|
+
return false;
|
|
235
|
+
const credentials = value;
|
|
236
|
+
return (typeof credentials.access_token === "string" &&
|
|
237
|
+
typeof credentials.refresh_token === "string" &&
|
|
238
|
+
(credentials.rool_token === undefined ||
|
|
239
|
+
credentials.rool_token === null ||
|
|
240
|
+
typeof credentials.rool_token === "string") &&
|
|
241
|
+
typeof credentials.expires_at === "number" &&
|
|
242
|
+
(credentials.auth_url === undefined ||
|
|
243
|
+
typeof credentials.auth_url === "string"));
|
|
244
|
+
}
|
|
245
|
+
function listen(server) {
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
server.once("error", reject);
|
|
248
|
+
server.listen(0, "127.0.0.1", () => {
|
|
249
|
+
server.off("error", reject);
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function close(server) {
|
|
255
|
+
if (!server.listening)
|
|
256
|
+
return Promise.resolve();
|
|
257
|
+
server.closeAllConnections();
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
server.close((error) => {
|
|
260
|
+
if (error)
|
|
261
|
+
reject(error);
|
|
262
|
+
else
|
|
263
|
+
resolve();
|
|
288
264
|
});
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function receiveLogin(server, expectedState, timeoutMs) {
|
|
268
|
+
let settled = false;
|
|
269
|
+
let resolveResult;
|
|
270
|
+
let rejectResult;
|
|
271
|
+
const result = new Promise((resolve, reject) => {
|
|
272
|
+
resolveResult = resolve;
|
|
273
|
+
rejectResult = reject;
|
|
274
|
+
});
|
|
275
|
+
const timeout = setTimeout(() => {
|
|
276
|
+
settle(() => rejectResult(new Error("Node authentication timed out")));
|
|
277
|
+
}, timeoutMs);
|
|
278
|
+
const settle = (action) => {
|
|
279
|
+
if (settled)
|
|
280
|
+
return;
|
|
281
|
+
settled = true;
|
|
282
|
+
clearTimeout(timeout);
|
|
283
|
+
action();
|
|
284
|
+
};
|
|
285
|
+
server.on("request", (request, response) => {
|
|
286
|
+
void handleCallbackRequest(request, response, expectedState)
|
|
287
|
+
.then((credentials) => {
|
|
288
|
+
if (credentials)
|
|
289
|
+
settle(() => resolveResult(credentials));
|
|
290
|
+
})
|
|
291
|
+
.catch((error) => settle(() => rejectResult(error)));
|
|
292
|
+
});
|
|
293
|
+
server.once("error", (error) => settle(() => rejectResult(error)));
|
|
294
|
+
return {
|
|
295
|
+
result,
|
|
296
|
+
cancel: () => settle(() => undefined),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function handleCallbackRequest(request, response, expectedState) {
|
|
300
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
301
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
302
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
303
|
+
response.end(callbackPage);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
if (request.method !== "POST" || url.pathname !== "/callback") {
|
|
307
|
+
response.writeHead(404);
|
|
308
|
+
response.end();
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const body = await readRequestBody(request);
|
|
312
|
+
const params = new URLSearchParams(body);
|
|
313
|
+
if (params.get("state") !== expectedState) {
|
|
314
|
+
response.writeHead(400);
|
|
315
|
+
response.end("Invalid authentication state");
|
|
316
|
+
throw new Error("Node authentication state mismatch");
|
|
289
317
|
}
|
|
318
|
+
const accessToken = params.get("id_token");
|
|
319
|
+
const refreshToken = params.get("refresh_token");
|
|
320
|
+
const roolToken = params.get("rool_token");
|
|
321
|
+
const expiresIn = Number(params.get("expires_in"));
|
|
322
|
+
if (!accessToken ||
|
|
323
|
+
!refreshToken ||
|
|
324
|
+
!Number.isFinite(expiresIn) ||
|
|
325
|
+
expiresIn <= 0) {
|
|
326
|
+
response.writeHead(400);
|
|
327
|
+
response.end("Invalid authentication response");
|
|
328
|
+
throw new Error("The Node authentication response is invalid");
|
|
329
|
+
}
|
|
330
|
+
response.writeHead(200);
|
|
331
|
+
response.end("Authentication complete. You can close this window.");
|
|
332
|
+
return {
|
|
333
|
+
accessToken,
|
|
334
|
+
refreshToken,
|
|
335
|
+
roolToken,
|
|
336
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function readRequestBody(request) {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const chunks = [];
|
|
342
|
+
let size = 0;
|
|
343
|
+
request.on("data", (chunk) => {
|
|
344
|
+
size += chunk.length;
|
|
345
|
+
if (size > MAX_CALLBACK_BODY_BYTES) {
|
|
346
|
+
request.destroy();
|
|
347
|
+
reject(new Error("The Node authentication response is too large"));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
chunks.push(chunk);
|
|
351
|
+
});
|
|
352
|
+
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
353
|
+
request.on("error", reject);
|
|
354
|
+
});
|
|
290
355
|
}
|
|
356
|
+
const callbackPage = `<!doctype html>
|
|
357
|
+
<html lang="en">
|
|
358
|
+
<head><meta charset="utf-8"><title>Rool authentication</title></head>
|
|
359
|
+
<body>
|
|
360
|
+
<p>Completing authentication…</p>
|
|
361
|
+
<script>
|
|
362
|
+
const body = window.location.hash.slice(1);
|
|
363
|
+
fetch('/callback', {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
366
|
+
body,
|
|
367
|
+
}).then(async (response) => {
|
|
368
|
+
document.body.textContent = response.ok
|
|
369
|
+
? 'Authentication complete. You can close this window.'
|
|
370
|
+
: await response.text();
|
|
371
|
+
});
|
|
372
|
+
</script>
|
|
373
|
+
</body>
|
|
374
|
+
</html>`;
|
|
291
375
|
//# sourceMappingURL=auth-node.js.map
|