@rool-dev/sdk 0.12.5 → 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 +434 -786
  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 -302
  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 -36
  19. package/dist/auth-node.d.ts.map +1 -1
  20. package/dist/auth-node.js +338 -265
  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 -290
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +275 -637
  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 -19
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -20
  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 -99
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js +0 -217
  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 -471
  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,302 +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
- authStateChangedHandler = null;
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.config = 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
+ };
15
40
  }
16
- /** Called by AuthManager to inject the auth URL */
17
- setAuthUrl(url) {
18
- this._authUrl = url;
41
+ initialize() {
42
+ return this.isAuthenticated();
19
43
  }
20
- /** Called by AuthManager to bridge auth state to client events */
21
- setAuthStateChangedHandler(handler) {
22
- this.authStateChangedHandler = handler;
44
+ async isAuthenticated() {
45
+ return (await this.readCredentials()) !== null;
23
46
  }
24
- notifyAuthState(authenticated) {
25
- this.config.onAuthStateChanged?.(authenticated);
26
- this.authStateChangedHandler?.(authenticated);
47
+ login(appName, params = {}) {
48
+ return this.authFlow("login", appName, params);
27
49
  }
28
- /** Called by AuthManager to inject the logger */
29
- setLogger(logger) {
30
- this.logger = logger;
50
+ signup(appName, params = {}) {
51
+ return this.authFlow("signup", appName, params);
31
52
  }
32
- /**
33
- * Get a short hash of the auth URL for scoping credentials by endpoint.
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
- get credentialsPath() {
42
- if (this.config.credentialsPath) {
43
- return this.config.credentialsPath;
57
+ logout = async () => {
58
+ try {
59
+ await fs.unlink(this.credentialsPath);
60
+ this.emitAuthState(false);
44
61
  }
45
- const homeDir = os.homedir();
46
- const configDir = path.join(homeDir, '.config', 'rool');
47
- // Scope credentials by endpoint to avoid mixing tokens from different environments
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
- return this._authUrl.replace(/\/+$/, '');
58
- }
59
- initialize() {
60
- // For Node.js, initialize just checks if credentials file exists
61
- // The actual validation happens in isAuthenticated()
62
- return this.readCredentials() !== null;
63
- }
64
- async getTokens() {
65
- const creds = this.readCredentials();
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
- return { accessToken: creds.access_token, roolToken: creds.rool_token ?? '' };
79
- }
80
- getAuthUser() {
81
- const creds = this.readCredentials();
82
- if (!creds?.access_token)
83
- return { email: null, name: null };
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
- const payload = JSON.parse(Buffer.from(creds.access_token.split('.')[1], 'base64').toString());
86
- return {
87
- email: payload.email || null,
88
- name: payload.name || null,
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
- catch {
92
- return { email: null, name: null };
97
+ finally {
98
+ callback.cancel();
99
+ await close(server);
93
100
  }
94
101
  }
95
- async isAuthenticated() {
96
- const tokens = await this.getTokens();
97
- return tokens !== undefined;
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 signup(appName, params) {
103
- return this.authFlow('signup', appName, params);
104
- }
105
- async authFlow(flow, appName, params) {
106
- const { server, closeAll } = await this.startLoopbackServer();
107
- const port = server.address().port;
108
- const redirectUri = `http://localhost:${port}`;
109
- const state = Math.random().toString(36).substring(2);
110
- const origin = new URL(this.authEndpoint).origin;
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
- url.searchParams.set('state', state);
120
- this.logger.info(`Opening browser to ${flow} to ${appName}:`, url.toString());
121
- await open(url.toString());
122
- const timeoutMs = this.config.loginTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
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
- replaceTokens(tokens) {
149
- const credentials = this.readCredentials();
150
- if (!credentials)
151
- throw new Error('Cannot replace a missing session');
152
- this.writeCredentials({
153
- access_token: tokens.accessToken,
154
- refresh_token: tokens.refreshToken,
155
- rool_token: credentials.rool_token,
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
- logout() {
160
- // Notify only on a real transition, so concurrent 401-driven logouts
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
- const filePath = this.credentialsPath;
174
- if (!fs.existsSync(filePath))
171
+ serialized = await fs.readFile(this.credentialsPath, "utf8");
172
+ }
173
+ catch (error) {
174
+ if (error.code === "ENOENT")
175
175
  return null;
176
- const data = fs.readFileSync(filePath, 'utf-8');
177
- return JSON.parse(data);
176
+ throw error;
177
+ }
178
+ let value;
179
+ try {
180
+ value = JSON.parse(serialized);
178
181
  }
179
182
  catch {
180
- return null;
183
+ throw new Error(`Invalid Node credentials at ${this.credentialsPath}`);
181
184
  }
182
- }
183
- writeCredentials(creds) {
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
- catch (error) {
191
- 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`);
192
191
  }
192
+ return value;
193
193
  }
194
- async refreshToken(creds) {
195
- if (!creds.refresh_token)
196
- 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`;
197
198
  try {
198
- const response = await fetch(`${this.authEndpoint}/refresh`, {
199
- method: 'POST',
200
- headers: { 'Content-Type': 'application/json' },
201
- body: JSON.stringify({
202
- refresh_token: creds.refresh_token,
203
- rool_token: creds.rool_token,
204
- }),
205
- });
206
- if (!response.ok) {
207
- // 400/401 typically means invalid/expired refresh token - clear credentials
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
- catch (error) {
228
- this.logger.error('[RoolClient] Refresh failed:', error);
229
- 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));
230
219
  }
220
+ return configuredPath;
231
221
  }
232
- startLoopbackServer() {
233
- return new Promise((resolve) => {
234
- const server = http.createServer((req, res) => {
235
- const url = new URL(req.url || '/', `http://localhost`);
236
- // 1. Serve the capture page if we just have a root request
237
- if (url.pathname === '/') {
238
- res.writeHead(200, { 'Content-Type': 'text/html' });
239
- res.end(`
240
- <html>
241
- <body>
242
- <h1>Authenticating...</h1>
243
- <script>
244
- // Extract hash and post to /callback
245
- if (window.location.hash) {
246
- const hash = window.location.hash.substring(1);
247
- fetch('/callback', {
248
- method: 'POST',
249
- headers: {'Content-Type': 'application/x-www-form-urlencoded'},
250
- body: hash
251
- })
252
- .then(() => document.body.innerHTML = '<h1>Login Successful. You can close this window.</h1>')
253
- .catch(err => document.body.innerHTML = '<h1>Error: ' + err.message + '</h1>');
254
- }
255
- </script>
256
- </body>
257
- </html>
258
- `);
259
- return;
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