@rool-dev/sdk 0.12.5 → 2.0.0-dev.03df95a
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 +460 -783
- 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 -302
- 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 -36
- package/dist/auth-node.d.ts.map +1 -1
- package/dist/auth-node.js +338 -265
- 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 +37 -290
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +331 -635
- 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 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -20
- 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 +131 -520
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -16
- package/dist/auth.d.ts +0 -99
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -217
- 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 -471
- 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,302 +1,375 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import open from
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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";
|
|
15
|
+
}
|
|
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;
|
|
13
27
|
constructor(config = {}) {
|
|
14
|
-
this.
|
|
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
|
+
};
|
|
15
40
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this._authUrl = url;
|
|
41
|
+
initialize() {
|
|
42
|
+
return this.isAuthenticated();
|
|
19
43
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.authStateChangedHandler = handler;
|
|
44
|
+
async isAuthenticated() {
|
|
45
|
+
return (await this.readCredentials()) !== null;
|
|
23
46
|
}
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
this.authStateChangedHandler?.(authenticated);
|
|
47
|
+
login(appName, params = {}) {
|
|
48
|
+
return this.authFlow("login", appName, params);
|
|
27
49
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.logger = logger;
|
|
50
|
+
signup(appName, params = {}) {
|
|
51
|
+
return this.authFlow("signup", appName, params);
|
|
31
52
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
get endpointHash() {
|
|
36
|
-
if (!this._authUrl) {
|
|
37
|
-
return 'default';
|
|
38
|
-
}
|
|
39
|
-
return crypto.createHash('sha256').update(this._authUrl).digest('hex').slice(0, 8);
|
|
53
|
+
onAuthStateChanged(listener) {
|
|
54
|
+
this.listeners.add(listener);
|
|
55
|
+
return () => this.listeners.delete(listener);
|
|
40
56
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
logout = async () => {
|
|
58
|
+
try {
|
|
59
|
+
await fs.unlink(this.credentialsPath);
|
|
60
|
+
this.emitAuthState(false);
|
|
44
61
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return path.join(configDir, `credentials-${this.endpointHash}.json`);
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Get the auth endpoint URL (without trailing slash).
|
|
52
|
-
*/
|
|
53
|
-
get authEndpoint() {
|
|
54
|
-
if (!this._authUrl) {
|
|
55
|
-
throw new Error('Auth URL not set. Ensure RoolClient is configured correctly.');
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== "ENOENT")
|
|
64
|
+
throw error;
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!creds)
|
|
67
|
-
return undefined;
|
|
68
|
-
// Refresh if expiring in less than 5 minutes
|
|
69
|
-
if (Date.now() >= creds.expires_at - 5 * 60 * 1000) {
|
|
70
|
-
const refreshed = await this.refreshToken(creds);
|
|
71
|
-
if (!refreshed)
|
|
72
|
-
return undefined;
|
|
73
|
-
const freshCreds = this.readCredentials();
|
|
74
|
-
if (!freshCreds)
|
|
75
|
-
return undefined;
|
|
76
|
-
return { accessToken: freshCreds.access_token, roolToken: freshCreds.rool_token ?? '' };
|
|
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");
|
|
77
75
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
84
85
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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);
|
|
90
96
|
}
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
finally {
|
|
98
|
+
callback.cancel();
|
|
99
|
+
await close(server);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
async login(appName, params) {
|
|
100
|
-
return this.authFlow('login', appName, params);
|
|
102
|
+
emitAuthState(authenticated) {
|
|
103
|
+
for (const listener of this.listeners)
|
|
104
|
+
listener(authenticated);
|
|
101
105
|
}
|
|
102
|
-
async
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const url = new URL(`${origin}/${flow}`);
|
|
112
|
-
url.searchParams.set('redirect_uri', redirectUri);
|
|
113
|
-
url.searchParams.set('app_name', appName);
|
|
114
|
-
if (params) {
|
|
115
|
-
for (const [key, value] of Object.entries(params)) {
|
|
116
|
-
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
|
+
});
|
|
117
115
|
}
|
|
116
|
+
credentials = await this.refreshInFlight;
|
|
118
117
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
const timeout = setTimeout(() => {
|
|
125
|
-
closeAll();
|
|
126
|
-
reject(new Error('Login timed out. Please try again.'));
|
|
127
|
-
}, timeoutMs);
|
|
128
|
-
server.on('authenticated', (tokens) => {
|
|
129
|
-
clearTimeout(timeout);
|
|
130
|
-
const expiresAt = Date.now() + (tokens.expires_in * 1000);
|
|
131
|
-
this.writeCredentials({
|
|
132
|
-
access_token: tokens.id_token, // GCIP returns id_token
|
|
133
|
-
refresh_token: tokens.refresh_token,
|
|
134
|
-
rool_token: tokens.rool_token ?? null,
|
|
135
|
-
expires_at: expiresAt
|
|
136
|
-
});
|
|
137
|
-
this.notifyAuthState(true);
|
|
138
|
-
closeAll();
|
|
139
|
-
resolve();
|
|
140
|
-
});
|
|
141
|
-
server.on('error', (err) => {
|
|
142
|
-
clearTimeout(timeout);
|
|
143
|
-
closeAll();
|
|
144
|
-
reject(err);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
118
|
+
return {
|
|
119
|
+
accessToken: credentials.access_token,
|
|
120
|
+
roolToken: credentials.rool_token ?? "",
|
|
121
|
+
};
|
|
147
122
|
}
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
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
|
+
}),
|
|
157
131
|
});
|
|
132
|
+
if (response.status === 400 || response.status === 401) {
|
|
133
|
+
await this.logout();
|
|
134
|
+
throw new NodeAuthLoginRequiredError("Node authentication expired; call login() again");
|
|
135
|
+
}
|
|
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;
|
|
158
167
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// emit a single sign-out.
|
|
162
|
-
const filePath = this.credentialsPath;
|
|
163
|
-
if (!fs.existsSync(filePath))
|
|
164
|
-
return;
|
|
165
|
-
fs.unlinkSync(filePath);
|
|
166
|
-
this.notifyAuthState(false);
|
|
167
|
-
}
|
|
168
|
-
// ===========================================================================
|
|
169
|
-
// Private Helpers
|
|
170
|
-
// ===========================================================================
|
|
171
|
-
readCredentials() {
|
|
168
|
+
async readCredentials() {
|
|
169
|
+
let serialized;
|
|
172
170
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
serialized = await fs.readFile(this.credentialsPath, "utf8");
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error.code === "ENOENT")
|
|
175
175
|
return null;
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
let value;
|
|
179
|
+
try {
|
|
180
|
+
value = JSON.parse(serialized);
|
|
178
181
|
}
|
|
179
182
|
catch {
|
|
180
|
-
|
|
183
|
+
throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
|
|
181
184
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const filePath = this.credentialsPath;
|
|
185
|
-
const dir = path.dirname(filePath);
|
|
186
|
-
try {
|
|
187
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
-
fs.writeFileSync(filePath, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
185
|
+
if (!isStoredCredentials(value)) {
|
|
186
|
+
throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
|
|
189
187
|
}
|
|
190
|
-
|
|
191
|
-
|
|
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`);
|
|
192
191
|
}
|
|
192
|
+
return value;
|
|
193
193
|
}
|
|
194
|
-
async
|
|
195
|
-
|
|
196
|
-
|
|
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`;
|
|
197
198
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (response.status === 400 || response.status === 401) {
|
|
209
|
-
this.logger.warn('[RoolClient] Refresh token expired or invalid. Please login again.');
|
|
210
|
-
this.logout();
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
this.logger.warn(`[RoolClient] Refresh failed: ${response.status}`);
|
|
214
|
-
}
|
|
215
|
-
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;
|
|
216
209
|
}
|
|
217
|
-
const data = await response.json();
|
|
218
|
-
const newCreds = {
|
|
219
|
-
access_token: data.id_token,
|
|
220
|
-
refresh_token: data.refresh_token || creds.refresh_token,
|
|
221
|
-
rool_token: data.rool_token ?? creds.rool_token,
|
|
222
|
-
expires_at: Date.now() + (Number(data.expires_in) * 1000),
|
|
223
|
-
};
|
|
224
|
-
this.writeCredentials(newCreds);
|
|
225
|
-
return newCreds.access_token;
|
|
226
210
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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));
|
|
230
219
|
}
|
|
220
|
+
return configuredPath;
|
|
231
221
|
}
|
|
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
|
-
// 2. Handle the callback POST
|
|
262
|
-
if (url.pathname === '/callback' && req.method === 'POST') {
|
|
263
|
-
let body = '';
|
|
264
|
-
req.on('data', (chunk) => body += chunk.toString());
|
|
265
|
-
req.on('end', () => {
|
|
266
|
-
const params = new URLSearchParams(body);
|
|
267
|
-
const idToken = params.get('id_token');
|
|
268
|
-
const refreshToken = params.get('refresh_token');
|
|
269
|
-
const roolToken = params.get('rool_token');
|
|
270
|
-
const expiresIn = params.get('expires_in');
|
|
271
|
-
if (idToken && expiresIn) {
|
|
272
|
-
res.writeHead(200);
|
|
273
|
-
res.end('OK');
|
|
274
|
-
server.emit('authenticated', {
|
|
275
|
-
id_token: idToken,
|
|
276
|
-
refresh_token: refreshToken,
|
|
277
|
-
rool_token: roolToken,
|
|
278
|
-
expires_in: Number(expiresIn)
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
res.writeHead(400);
|
|
283
|
-
res.end('Invalid tokens');
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
res.writeHead(404);
|
|
289
|
-
res.end();
|
|
290
|
-
});
|
|
291
|
-
// Listen on random port
|
|
292
|
-
server.listen(0, '127.0.0.1', () => resolve({
|
|
293
|
-
server,
|
|
294
|
-
closeAll: () => {
|
|
295
|
-
server.close();
|
|
296
|
-
server.closeAllConnections();
|
|
297
|
-
},
|
|
298
|
-
}));
|
|
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();
|
|
299
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();
|
|
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");
|
|
300
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
|
+
});
|
|
301
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>`;
|
|
302
375
|
//# sourceMappingURL=auth-node.js.map
|