@rool-dev/sdk 0.12.4 → 2.0.0-dev.4adc2c2

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.
Files changed (108) hide show
  1. package/README.md +436 -761
  2. package/dist/agents.d.ts +130 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +348 -0
  5. package/dist/agents.js.map +1 -0
  6. package/dist/auth-base.d.ts +43 -88
  7. package/dist/auth-base.d.ts.map +1 -1
  8. package/dist/auth-base.js +178 -298
  9. package/dist/auth-base.js.map +1 -1
  10. package/dist/auth-browser.d.ts +7 -25
  11. package/dist/auth-browser.d.ts.map +1 -1
  12. package/dist/auth-browser.js +45 -82
  13. package/dist/auth-browser.js.map +1 -1
  14. package/dist/auth-native.d.ts +9 -56
  15. package/dist/auth-native.d.ts.map +1 -1
  16. package/dist/auth-native.js +88 -177
  17. package/dist/auth-native.js.map +1 -1
  18. package/dist/auth-node.d.ts +32 -32
  19. package/dist/auth-node.d.ts.map +1 -1
  20. package/dist/auth-node.js +339 -255
  21. package/dist/auth-node.js.map +1 -1
  22. package/dist/auth-url.d.ts +6 -0
  23. package/dist/auth-url.d.ts.map +1 -0
  24. package/dist/auth-url.js +30 -0
  25. package/dist/auth-url.js.map +1 -0
  26. package/dist/client.d.ts +35 -277
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +275 -620
  29. package/dist/client.js.map +1 -1
  30. package/dist/events.d.ts +40 -0
  31. package/dist/events.d.ts.map +1 -0
  32. package/dist/events.js +175 -0
  33. package/dist/events.js.map +1 -0
  34. package/dist/files.d.ts +170 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +846 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/index.d.ts +16 -17
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -19
  41. package/dist/index.js.map +1 -1
  42. package/dist/machine.d.ts +49 -0
  43. package/dist/machine.d.ts.map +1 -0
  44. package/dist/machine.js +142 -0
  45. package/dist/machine.js.map +1 -0
  46. package/dist/problem.d.ts +11 -0
  47. package/dist/problem.d.ts.map +1 -0
  48. package/dist/problem.js +20 -0
  49. package/dist/problem.js.map +1 -0
  50. package/dist/structured.d.ts +84 -0
  51. package/dist/structured.d.ts.map +1 -0
  52. package/dist/structured.js +405 -0
  53. package/dist/structured.js.map +1 -0
  54. package/dist/types.d.ts +119 -520
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +11 -16
  57. package/dist/auth.d.ts +0 -85
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js +0 -181
  60. package/dist/auth.js.map +0 -1
  61. package/dist/client-info.d.ts +0 -18
  62. package/dist/client-info.d.ts.map +0 -1
  63. package/dist/client-info.js +0 -25
  64. package/dist/client-info.js.map +0 -1
  65. package/dist/event-emitter.d.ts +0 -45
  66. package/dist/event-emitter.d.ts.map +0 -1
  67. package/dist/event-emitter.js +0 -86
  68. package/dist/event-emitter.js.map +0 -1
  69. package/dist/graphql.d.ts +0 -90
  70. package/dist/graphql.d.ts.map +0 -1
  71. package/dist/graphql.js +0 -463
  72. package/dist/graphql.js.map +0 -1
  73. package/dist/logger.d.ts +0 -13
  74. package/dist/logger.d.ts.map +0 -1
  75. package/dist/logger.js +0 -17
  76. package/dist/logger.js.map +0 -1
  77. package/dist/path.d.ts +0 -6
  78. package/dist/path.d.ts.map +0 -1
  79. package/dist/path.js +0 -47
  80. package/dist/path.js.map +0 -1
  81. package/dist/reroute.d.ts +0 -22
  82. package/dist/reroute.d.ts.map +0 -1
  83. package/dist/reroute.js +0 -70
  84. package/dist/reroute.js.map +0 -1
  85. package/dist/rest.d.ts +0 -54
  86. package/dist/rest.d.ts.map +0 -1
  87. package/dist/rest.js +0 -171
  88. package/dist/rest.js.map +0 -1
  89. package/dist/router.d.ts +0 -20
  90. package/dist/router.d.ts.map +0 -1
  91. package/dist/router.js +0 -54
  92. package/dist/router.js.map +0 -1
  93. package/dist/space-session.d.ts +0 -30
  94. package/dist/space-session.d.ts.map +0 -1
  95. package/dist/space-session.js +0 -83
  96. package/dist/space-session.js.map +0 -1
  97. package/dist/space.d.ts +0 -161
  98. package/dist/space.d.ts.map +0 -1
  99. package/dist/space.js +0 -618
  100. package/dist/space.js.map +0 -1
  101. package/dist/subscription.d.ts +0 -36
  102. package/dist/subscription.d.ts.map +0 -1
  103. package/dist/subscription.js +0 -419
  104. package/dist/subscription.js.map +0 -1
  105. package/dist/webdav.d.ts +0 -171
  106. package/dist/webdav.d.ts.map +0 -1
  107. package/dist/webdav.js +0 -481
  108. package/dist/webdav.js.map +0 -1
package/dist/auth-node.js CHANGED
@@ -1,291 +1,375 @@
1
- import * as http from 'node:http';
2
- import * as fs from 'node:fs';
3
- import * as path from 'node:path';
4
- import * as os from 'node:os';
5
- import * as crypto from 'node:crypto';
6
- import open from 'open';
7
- import { defaultLogger } from './logger.js';
8
- export class NodeAuthProvider {
9
- config;
10
- _authUrl = null;
11
- logger = defaultLogger;
12
- constructor(config = {}) {
13
- this.config = config;
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
- /** Called by AuthManager to inject the auth URL */
16
- setAuthUrl(url) {
17
- this._authUrl = url;
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
- /** Called by AuthManager to inject the logger */
20
- setLogger(logger) {
21
- this.logger = logger;
41
+ initialize() {
42
+ return this.isAuthenticated();
22
43
  }
23
- /**
24
- * Get a short hash of the auth URL for scoping credentials by endpoint.
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
- get credentialsPath() {
33
- if (this.config.credentialsPath) {
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
- * Get the auth endpoint URL (without trailing slash).
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
- initialize() {
51
- // For Node.js, initialize just checks if credentials file exists
52
- // The actual validation happens in isAuthenticated()
53
- return this.readCredentials() !== null;
53
+ onAuthStateChanged(listener) {
54
+ this.listeners.add(listener);
55
+ return () => this.listeners.delete(listener);
54
56
  }
55
- async getTokens() {
56
- const creds = this.readCredentials();
57
- if (!creds)
58
- return undefined;
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
- return { accessToken: creds.access_token, roolToken: creds.rool_token ?? '' };
70
- }
71
- getAuthUser() {
72
- const creds = this.readCredentials();
73
- if (!creds?.access_token)
74
- return { email: null, name: null };
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
- const payload = JSON.parse(Buffer.from(creds.access_token.split('.')[1], 'base64').toString());
77
- return {
78
- email: payload.email || null,
79
- name: payload.name || null,
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
- catch {
83
- return { email: null, name: null };
97
+ finally {
98
+ callback.cancel();
99
+ await close(server);
84
100
  }
85
101
  }
86
- async isAuthenticated() {
87
- const tokens = await this.getTokens();
88
- return tokens !== undefined;
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 signup(appName, params) {
94
- return this.authFlow('signup', appName, params);
95
- }
96
- async authFlow(flow, appName, params) {
97
- const { server, closeAll } = await this.startLoopbackServer();
98
- const port = server.address().port;
99
- const redirectUri = `http://localhost:${port}`;
100
- const state = Math.random().toString(36).substring(2);
101
- const origin = new URL(this.authEndpoint).origin;
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
- url.searchParams.set('state', state);
111
- this.logger.info(`Opening browser to ${flow} to ${appName}:`, url.toString());
112
- await open(url.toString());
113
- const timeoutMs = this.config.loginTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
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
- replaceTokens(tokens) {
140
- const credentials = this.readCredentials();
141
- if (!credentials)
142
- throw new Error('Cannot replace a missing session');
143
- this.writeCredentials({
144
- access_token: tokens.accessToken,
145
- refresh_token: tokens.refreshToken,
146
- rool_token: credentials.rool_token,
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
- logout() {
151
- const filePath = this.credentialsPath;
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
- this.config.onAuthStateChanged?.(false);
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
- // Private Helpers
159
- // ===========================================================================
160
- readCredentials() {
168
+ async readCredentials() {
169
+ let serialized;
161
170
  try {
162
- const filePath = this.credentialsPath;
163
- if (!fs.existsSync(filePath))
171
+ serialized = await fs.readFile(this.credentialsPath, "utf8");
172
+ }
173
+ catch (error) {
174
+ if (error.code === "ENOENT")
164
175
  return null;
165
- const data = fs.readFileSync(filePath, 'utf-8');
166
- return JSON.parse(data);
176
+ throw error;
177
+ }
178
+ let value;
179
+ try {
180
+ value = JSON.parse(serialized);
167
181
  }
168
182
  catch {
169
- return null;
183
+ throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
170
184
  }
171
- }
172
- writeCredentials(creds) {
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
- catch (error) {
180
- this.logger.error('[RoolClient] Failed to save credentials:', error);
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 refreshToken(creds) {
184
- if (!creds.refresh_token)
185
- return undefined;
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
- const response = await fetch(`${this.authEndpoint}/refresh`, {
188
- method: 'POST',
189
- headers: { 'Content-Type': 'application/json' },
190
- body: JSON.stringify({
191
- refresh_token: creds.refresh_token,
192
- rool_token: creds.rool_token,
193
- }),
194
- });
195
- if (!response.ok) {
196
- // 400/401 typically means invalid/expired refresh token - clear credentials
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
- catch (error) {
217
- this.logger.error('[RoolClient] Refresh failed:', error);
218
- return undefined;
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
- startLoopbackServer() {
222
- return new Promise((resolve) => {
223
- const server = http.createServer((req, res) => {
224
- const url = new URL(req.url || '/', `http://localhost`);
225
- // 1. Serve the capture page if we just have a root request
226
- if (url.pathname === '/') {
227
- res.writeHead(200, { 'Content-Type': 'text/html' });
228
- res.end(`
229
- <html>
230
- <body>
231
- <h1>Authenticating...</h1>
232
- <script>
233
- // Extract hash and post to /callback
234
- if (window.location.hash) {
235
- const hash = window.location.hash.substring(1);
236
- fetch('/callback', {
237
- method: 'POST',
238
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
239
- body: hash
240
- })
241
- .then(() => document.body.innerHTML = '<h1>Login Successful. You can close this window.</h1>')
242
- .catch(err => document.body.innerHTML = '<h1>Error: ' + err.message + '</h1>');
243
- }
244
- </script>
245
- </body>
246
- </html>
247
- `);
248
- return;
249
- }
250
- // 2. Handle the callback POST
251
- if (url.pathname === '/callback' && req.method === 'POST') {
252
- let body = '';
253
- req.on('data', (chunk) => body += chunk.toString());
254
- req.on('end', () => {
255
- const params = new URLSearchParams(body);
256
- const idToken = params.get('id_token');
257
- const refreshToken = params.get('refresh_token');
258
- const roolToken = params.get('rool_token');
259
- const expiresIn = params.get('expires_in');
260
- if (idToken && expiresIn) {
261
- res.writeHead(200);
262
- res.end('OK');
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