@ariobarin/glossa 0.1.0-beta.3 → 0.1.0-beta.6
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/LICENSE +7 -1
- package/README.md +7 -6
- package/dist/main.js +765 -555
- package/package.json +6 -5
- package/dist/main.js.map +0 -7
package/dist/main.js
CHANGED
|
@@ -6,88 +6,52 @@ var __export = (target, all) => {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// src/config-store.ts
|
|
9
|
+
import path2 from "node:path";
|
|
10
|
+
|
|
11
|
+
// src/secure-store.ts
|
|
9
12
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
13
|
import os from "node:os";
|
|
11
14
|
import path from "node:path";
|
|
12
15
|
var KEYRING_SERVICE = "Glossa";
|
|
13
|
-
var KEYRING_ACCOUNT = "oauth";
|
|
14
|
-
var FILE_CREDENTIAL_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 credential file.";
|
|
15
16
|
function configDirectory() {
|
|
16
17
|
if (process.platform === "win32") {
|
|
17
18
|
return path.join(process.env.APPDATA ?? os.homedir(), "Glossa");
|
|
18
19
|
}
|
|
19
|
-
return path.join(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
async function defaultEntryProvider() {
|
|
25
|
-
try {
|
|
26
|
-
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
27
|
-
return new AsyncEntry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
28
|
-
} catch {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function parseCredentials(value) {
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = JSON.parse(value);
|
|
36
|
-
} catch {
|
|
37
|
-
throw new Error("Stored Glossa credentials are invalid.");
|
|
38
|
-
}
|
|
39
|
-
if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string" || typeof parsed.audience !== "string" || typeof parsed.accessToken !== "string" || typeof parsed.expiresAt !== "string" || !Number.isFinite(Date.parse(parsed.expiresAt)) || typeof parsed.tokenType !== "string" || parsed.refreshToken !== void 0 && typeof parsed.refreshToken !== "string" || parsed.scope !== void 0 && typeof parsed.scope !== "string") {
|
|
40
|
-
throw new Error("Stored Glossa credentials are invalid.");
|
|
41
|
-
}
|
|
42
|
-
return parsed;
|
|
43
|
-
}
|
|
44
|
-
async function readFileCredentials(target) {
|
|
45
|
-
try {
|
|
46
|
-
return parseCredentials(await readFile(target, "utf8"));
|
|
47
|
-
} catch (error46) {
|
|
48
|
-
const code = error46.code;
|
|
49
|
-
if (code === "ENOENT") return null;
|
|
50
|
-
throw error46;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async function writeFileCredentials(target, credentials) {
|
|
54
|
-
await mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
55
|
-
await writeFile(target, `${JSON.stringify(credentials, null, 2)}
|
|
56
|
-
`, {
|
|
57
|
-
encoding: "utf8",
|
|
58
|
-
mode: 384
|
|
59
|
-
});
|
|
60
|
-
if (process.platform !== "win32") await chmod(target, 384);
|
|
20
|
+
return path.join(
|
|
21
|
+
process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"),
|
|
22
|
+
"glossa"
|
|
23
|
+
);
|
|
61
24
|
}
|
|
62
|
-
var
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
constructor(options = {}) {
|
|
67
|
-
this.#credentialsFile = options.credentialsFile ?? credentialsPath();
|
|
68
|
-
this.#entryProvider = options.entryProvider ?? defaultEntryProvider;
|
|
69
|
-
this.#warn = options.warn ?? console.warn;
|
|
70
|
-
}
|
|
71
|
-
async #entry() {
|
|
72
|
-
try {
|
|
73
|
-
return await this.#entryProvider();
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
25
|
+
var SecureStore = class {
|
|
26
|
+
#options;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#options = options;
|
|
77
29
|
}
|
|
78
|
-
async save(
|
|
79
|
-
const serialized = JSON.stringify(
|
|
30
|
+
async save(value) {
|
|
31
|
+
const serialized = JSON.stringify(value);
|
|
80
32
|
const entry = await this.#entry();
|
|
81
33
|
if (entry) {
|
|
82
34
|
try {
|
|
83
35
|
await entry.setPassword(serialized);
|
|
84
|
-
await rm(this.#
|
|
36
|
+
await rm(this.#options.file, { force: true });
|
|
85
37
|
return "keyring";
|
|
86
38
|
} catch {
|
|
87
39
|
}
|
|
88
40
|
}
|
|
89
|
-
this.#warn(
|
|
90
|
-
await
|
|
41
|
+
this.#warn();
|
|
42
|
+
await mkdir(path.dirname(this.#options.file), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
mode: 448
|
|
45
|
+
});
|
|
46
|
+
await writeFile(
|
|
47
|
+
this.#options.file,
|
|
48
|
+
`${JSON.stringify(value, null, 2)}
|
|
49
|
+
`,
|
|
50
|
+
{ encoding: "utf8", mode: 384 }
|
|
51
|
+
);
|
|
52
|
+
if (process.platform !== "win32") {
|
|
53
|
+
await chmod(this.#options.file, 384);
|
|
54
|
+
}
|
|
91
55
|
return "file";
|
|
92
56
|
}
|
|
93
57
|
async load() {
|
|
@@ -98,22 +62,27 @@ var CredentialStore = class {
|
|
|
98
62
|
serialized = await entry.getPassword();
|
|
99
63
|
} catch {
|
|
100
64
|
}
|
|
101
|
-
if (serialized) {
|
|
102
|
-
return {
|
|
65
|
+
if (serialized !== void 0) {
|
|
66
|
+
return { value: this.#options.parse(serialized), backend: "keyring" };
|
|
103
67
|
}
|
|
104
68
|
}
|
|
105
|
-
|
|
106
|
-
|
|
69
|
+
let value;
|
|
70
|
+
try {
|
|
71
|
+
value = this.#options.parse(await readFile(this.#options.file, "utf8"));
|
|
72
|
+
} catch (error46) {
|
|
73
|
+
if (error46.code === "ENOENT") return null;
|
|
74
|
+
throw error46;
|
|
75
|
+
}
|
|
107
76
|
if (entry) {
|
|
108
77
|
try {
|
|
109
|
-
await entry.setPassword(JSON.stringify(
|
|
110
|
-
await rm(this.#
|
|
111
|
-
return {
|
|
78
|
+
await entry.setPassword(JSON.stringify(value));
|
|
79
|
+
await rm(this.#options.file, { force: true });
|
|
80
|
+
return { value, backend: "keyring" };
|
|
112
81
|
} catch {
|
|
113
82
|
}
|
|
114
83
|
}
|
|
115
|
-
this.#warn(
|
|
116
|
-
return {
|
|
84
|
+
this.#warn();
|
|
85
|
+
return { value, backend: "file" };
|
|
117
86
|
}
|
|
118
87
|
async delete() {
|
|
119
88
|
const entry = await this.#entry();
|
|
@@ -123,18 +92,71 @@ var CredentialStore = class {
|
|
|
123
92
|
} catch {
|
|
124
93
|
}
|
|
125
94
|
}
|
|
126
|
-
await rm(this.#
|
|
95
|
+
await rm(this.#options.file, { force: true });
|
|
96
|
+
}
|
|
97
|
+
async #entry() {
|
|
98
|
+
if (this.#options.entryProvider) {
|
|
99
|
+
try {
|
|
100
|
+
return await this.#options.entryProvider();
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
107
|
+
return new AsyncEntry(KEYRING_SERVICE, this.#options.account);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
#warn() {
|
|
113
|
+
(this.#options.warn ?? console.warn)(this.#options.warning);
|
|
127
114
|
}
|
|
128
115
|
};
|
|
129
|
-
|
|
116
|
+
|
|
117
|
+
// src/config-store.ts
|
|
118
|
+
var FILE_CREDENTIAL_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 credential file.";
|
|
119
|
+
function parseCredentials(value) {
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(value);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error("Stored Glossa credentials are invalid.");
|
|
125
|
+
}
|
|
126
|
+
if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string" || typeof parsed.audience !== "string" || typeof parsed.accessToken !== "string" || typeof parsed.expiresAt !== "string" || !Number.isFinite(Date.parse(parsed.expiresAt)) || typeof parsed.tokenType !== "string" || parsed.refreshToken !== void 0 && typeof parsed.refreshToken !== "string" || parsed.scope !== void 0 && typeof parsed.scope !== "string" || parsed.requestedScope !== void 0 && typeof parsed.requestedScope !== "string") {
|
|
127
|
+
throw new Error("Stored Glossa credentials are invalid.");
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
var store = new SecureStore({
|
|
132
|
+
account: "oauth",
|
|
133
|
+
file: path2.join(configDirectory(), "credentials.json"),
|
|
134
|
+
warning: FILE_CREDENTIAL_WARNING,
|
|
135
|
+
parse: parseCredentials
|
|
136
|
+
});
|
|
130
137
|
async function saveCredentials(credentials) {
|
|
131
|
-
return await
|
|
138
|
+
return await store.save(credentials);
|
|
132
139
|
}
|
|
133
140
|
async function loadCredentials() {
|
|
134
|
-
|
|
141
|
+
const loaded = await store.load();
|
|
142
|
+
return loaded ? { credentials: loaded.value, backend: loaded.backend } : null;
|
|
135
143
|
}
|
|
136
144
|
async function deleteCredentials() {
|
|
137
|
-
await
|
|
145
|
+
await store.delete();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/auth-scopes.ts
|
|
149
|
+
function normalizedScopes(value) {
|
|
150
|
+
return [...new Set(value?.trim().split(/\s+/).filter(Boolean) ?? [])].sort();
|
|
151
|
+
}
|
|
152
|
+
function scopesMatch(left, right) {
|
|
153
|
+
return JSON.stringify(normalizedScopes(left)) === JSON.stringify(normalizedScopes(right));
|
|
154
|
+
}
|
|
155
|
+
function grantedScopesSatisfyRequest(granted, requested, hasRefreshToken) {
|
|
156
|
+
const grantedSet = new Set(normalizedScopes(granted));
|
|
157
|
+
return normalizedScopes(requested).every(
|
|
158
|
+
(scope) => grantedSet.has(scope) || scope === "offline_access" && hasRefreshToken
|
|
159
|
+
);
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
// src/auth-session.ts
|
|
@@ -142,8 +164,14 @@ var EXPIRY_BUFFER_MS = 6e4;
|
|
|
142
164
|
function endpoint(issuer, pathname) {
|
|
143
165
|
return new URL(pathname, issuer.endsWith("/") ? issuer : `${issuer}/`).toString();
|
|
144
166
|
}
|
|
167
|
+
var SessionExpiredError = class extends Error {
|
|
168
|
+
constructor() {
|
|
169
|
+
super("Session expired. Run Glossa again to sign in.");
|
|
170
|
+
this.name = "SessionExpiredError";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
145
173
|
function sessionExpiredError() {
|
|
146
|
-
return new
|
|
174
|
+
return new SessionExpiredError();
|
|
147
175
|
}
|
|
148
176
|
function oauthMessage(data, status) {
|
|
149
177
|
return data.error_description ?? data.error ?? `HTTP ${status}`;
|
|
@@ -178,13 +206,22 @@ async function refreshCredentials(credentials, dependencies = {}) {
|
|
|
178
206
|
}
|
|
179
207
|
throw new Error(oauthMessage(oauth, response.status));
|
|
180
208
|
}
|
|
209
|
+
const grantedScope = data.scope ?? credentials.scope;
|
|
210
|
+
if (credentials.requestedScope && !grantedScopesSatisfyRequest(
|
|
211
|
+
grantedScope,
|
|
212
|
+
credentials.requestedScope,
|
|
213
|
+
Boolean(data.refresh_token ?? credentials.refreshToken)
|
|
214
|
+
)) {
|
|
215
|
+
await remove();
|
|
216
|
+
throw sessionExpiredError();
|
|
217
|
+
}
|
|
181
218
|
const refreshed = {
|
|
182
219
|
...credentials,
|
|
183
220
|
accessToken: data.access_token,
|
|
184
221
|
refreshToken: data.refresh_token ?? credentials.refreshToken,
|
|
185
222
|
expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
|
|
186
223
|
tokenType: data.token_type,
|
|
187
|
-
...
|
|
224
|
+
...grantedScope ? { scope: grantedScope } : {}
|
|
188
225
|
};
|
|
189
226
|
await save(refreshed);
|
|
190
227
|
return refreshed;
|
|
@@ -344,6 +381,10 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
|
|
|
344
381
|
if (!data.refresh_token) {
|
|
345
382
|
throw new Error("Auth0 did not issue a refresh token.");
|
|
346
383
|
}
|
|
384
|
+
const grantedScope = data.scope ?? options.scope;
|
|
385
|
+
if (!grantedScopesSatisfyRequest(grantedScope, options.scope, true)) {
|
|
386
|
+
throw new Error("Auth0 did not grant the permissions Glossa requires.");
|
|
387
|
+
}
|
|
347
388
|
await save({
|
|
348
389
|
issuer: options.issuer,
|
|
349
390
|
clientId: options.clientId,
|
|
@@ -352,7 +393,8 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
|
|
|
352
393
|
refreshToken: data.refresh_token,
|
|
353
394
|
expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
|
|
354
395
|
tokenType: data.token_type,
|
|
355
|
-
|
|
396
|
+
scope: grantedScope,
|
|
397
|
+
requestedScope: options.scope
|
|
356
398
|
});
|
|
357
399
|
log("Signed in to Glossa.");
|
|
358
400
|
return;
|
|
@@ -376,6 +418,141 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
|
|
|
376
418
|
}
|
|
377
419
|
}
|
|
378
420
|
|
|
421
|
+
// src/auth-login.ts
|
|
422
|
+
function normalizedIssuer(value) {
|
|
423
|
+
return value.replace(/\/+$/, "");
|
|
424
|
+
}
|
|
425
|
+
function credentialsMatchLoginOptions(credentials, options) {
|
|
426
|
+
return normalizedIssuer(credentials.issuer) === normalizedIssuer(options.issuer) && credentials.clientId === options.clientId && credentials.audience === options.audience && scopesMatch(credentials.requestedScope, options.scope) && grantedScopesSatisfyRequest(
|
|
427
|
+
credentials.scope,
|
|
428
|
+
options.scope,
|
|
429
|
+
Boolean(credentials.refreshToken)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
async function ensureSignedIn(options, dependencies = {}) {
|
|
433
|
+
const load = dependencies.loadCredentials ?? loadCredentials;
|
|
434
|
+
const validate = dependencies.validCredentials ?? validCredentials;
|
|
435
|
+
const login = dependencies.loginWithDeviceFlow ?? loginWithDeviceFlow;
|
|
436
|
+
const loaded = await load();
|
|
437
|
+
if (loaded && credentialsMatchLoginOptions(loaded.credentials, options)) {
|
|
438
|
+
try {
|
|
439
|
+
await validate(loaded.credentials);
|
|
440
|
+
return false;
|
|
441
|
+
} catch (error46) {
|
|
442
|
+
if (!(error46 instanceof SessionExpiredError)) throw error46;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
await login(options);
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/cli-options.ts
|
|
450
|
+
import { existsSync } from "node:fs";
|
|
451
|
+
import path3 from "node:path";
|
|
452
|
+
var UsageError = class extends Error {
|
|
453
|
+
};
|
|
454
|
+
var helpTopics = /* @__PURE__ */ new Set(["start", "status", "devices", "login", "logout"]);
|
|
455
|
+
function parseStart(args) {
|
|
456
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
457
|
+
return { command: "help", topic: "start" };
|
|
458
|
+
}
|
|
459
|
+
let selectedPath;
|
|
460
|
+
let allowBroadRoot = false;
|
|
461
|
+
let optionsEnded = false;
|
|
462
|
+
for (const argument of args) {
|
|
463
|
+
if (!optionsEnded && argument === "--") {
|
|
464
|
+
optionsEnded = true;
|
|
465
|
+
} else if (!optionsEnded && argument === "--allow-broad-root") {
|
|
466
|
+
allowBroadRoot = true;
|
|
467
|
+
} else if (!optionsEnded && argument.startsWith("-")) {
|
|
468
|
+
throw new UsageError(`Unknown start option: ${argument}`);
|
|
469
|
+
} else if (selectedPath) {
|
|
470
|
+
throw new UsageError("Start accepts at most one directory.");
|
|
471
|
+
} else {
|
|
472
|
+
selectedPath = argument;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
command: "start",
|
|
477
|
+
...selectedPath ? { path: selectedPath } : {},
|
|
478
|
+
allowBroadRoot
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function singleJsonOption(command, args) {
|
|
482
|
+
if (args.length === 0) return false;
|
|
483
|
+
if (args.length === 1 && args[0] === "--json") return true;
|
|
484
|
+
throw new UsageError(`${command} accepts only --json.`);
|
|
485
|
+
}
|
|
486
|
+
function parseDevices(args) {
|
|
487
|
+
const [action, ...options] = args;
|
|
488
|
+
if (!action || action === "--help" || action === "-h") {
|
|
489
|
+
return { command: "help", topic: "devices" };
|
|
490
|
+
}
|
|
491
|
+
if (action === "list") {
|
|
492
|
+
return { command: "devices", action, json: singleJsonOption("Devices list", options) };
|
|
493
|
+
}
|
|
494
|
+
if (action === "rename" && options.length === 2) {
|
|
495
|
+
return { command: "devices", action, deviceId: options[0], name: options[1] };
|
|
496
|
+
}
|
|
497
|
+
if (action === "revoke" && options.length === 1) {
|
|
498
|
+
return { command: "devices", action, deviceId: options[0] };
|
|
499
|
+
}
|
|
500
|
+
throw new UsageError("Use: glossa devices list, rename <id> <name>, or revoke <id>.");
|
|
501
|
+
}
|
|
502
|
+
function likelyDirectory(value) {
|
|
503
|
+
return value === "." || value === ".." || path3.isAbsolute(value) || value.includes("/") || value.includes("\\") || existsSync(value);
|
|
504
|
+
}
|
|
505
|
+
function parseInvocation(args) {
|
|
506
|
+
const [command, ...options] = args;
|
|
507
|
+
if (!command) return parseStart([]);
|
|
508
|
+
if (command === "--help" || command === "-h") {
|
|
509
|
+
if (options.length > 0) throw new UsageError("Help accepts one command name.");
|
|
510
|
+
return { command: "help" };
|
|
511
|
+
}
|
|
512
|
+
if (command === "help") {
|
|
513
|
+
if (options.length > 1) throw new UsageError("Help accepts one command name.");
|
|
514
|
+
const topic = options[0];
|
|
515
|
+
if (!topic) return { command: "help" };
|
|
516
|
+
if (!helpTopics.has(topic)) {
|
|
517
|
+
throw new UsageError(`Unknown help topic: ${topic}`);
|
|
518
|
+
}
|
|
519
|
+
return { command: "help", topic };
|
|
520
|
+
}
|
|
521
|
+
if (command === "--version" || command === "-v") {
|
|
522
|
+
if (options.length > 0) throw new UsageError("Version accepts no arguments.");
|
|
523
|
+
return { command: "version" };
|
|
524
|
+
}
|
|
525
|
+
if (command === "start") return parseStart(options);
|
|
526
|
+
if (command === "status") {
|
|
527
|
+
if (options.includes("--help") || options.includes("-h")) {
|
|
528
|
+
return { command: "help", topic: "status" };
|
|
529
|
+
}
|
|
530
|
+
return { command: "status", json: singleJsonOption("Status", options) };
|
|
531
|
+
}
|
|
532
|
+
if (command === "devices") return parseDevices(options);
|
|
533
|
+
if (command === "login") {
|
|
534
|
+
if (options.includes("--help") || options.includes("-h")) {
|
|
535
|
+
return { command: "help", topic: "login" };
|
|
536
|
+
}
|
|
537
|
+
if (options.length > 0) throw new UsageError("Login accepts no arguments.");
|
|
538
|
+
return { command: "login" };
|
|
539
|
+
}
|
|
540
|
+
if (command === "logout") {
|
|
541
|
+
if (options.includes("--help") || options.includes("-h")) {
|
|
542
|
+
return { command: "help", topic: "logout" };
|
|
543
|
+
}
|
|
544
|
+
if (options.length === 0) return { command: "logout", browser: false };
|
|
545
|
+
if (options.length === 1 && options[0] === "--browser") {
|
|
546
|
+
return { command: "logout", browser: true };
|
|
547
|
+
}
|
|
548
|
+
throw new UsageError("Logout accepts only --browser.");
|
|
549
|
+
}
|
|
550
|
+
if (command === "--") return parseStart(options);
|
|
551
|
+
if (command.startsWith("-")) return parseStart(args);
|
|
552
|
+
if (likelyDirectory(command)) return parseStart(args);
|
|
553
|
+
throw new UsageError(`Unknown command: ${command}`);
|
|
554
|
+
}
|
|
555
|
+
|
|
379
556
|
// src/relay-client.ts
|
|
380
557
|
import os2 from "node:os";
|
|
381
558
|
|
|
@@ -1110,10 +1287,10 @@ function mergeDefs(...defs) {
|
|
|
1110
1287
|
function cloneDef(schema) {
|
|
1111
1288
|
return mergeDefs(schema._zod.def);
|
|
1112
1289
|
}
|
|
1113
|
-
function getElementAtPath(obj,
|
|
1114
|
-
if (!
|
|
1290
|
+
function getElementAtPath(obj, path7) {
|
|
1291
|
+
if (!path7)
|
|
1115
1292
|
return obj;
|
|
1116
|
-
return
|
|
1293
|
+
return path7.reduce((acc, key) => acc?.[key], obj);
|
|
1117
1294
|
}
|
|
1118
1295
|
function promiseAllObject(promisesObj) {
|
|
1119
1296
|
const keys = Object.keys(promisesObj);
|
|
@@ -1474,11 +1651,11 @@ function aborted(x, startIndex = 0) {
|
|
|
1474
1651
|
}
|
|
1475
1652
|
return false;
|
|
1476
1653
|
}
|
|
1477
|
-
function prefixIssues(
|
|
1654
|
+
function prefixIssues(path7, issues) {
|
|
1478
1655
|
return issues.map((iss) => {
|
|
1479
1656
|
var _a;
|
|
1480
1657
|
(_a = iss).path ?? (_a.path = []);
|
|
1481
|
-
iss.path.unshift(
|
|
1658
|
+
iss.path.unshift(path7);
|
|
1482
1659
|
return iss;
|
|
1483
1660
|
});
|
|
1484
1661
|
}
|
|
@@ -1640,7 +1817,7 @@ function formatError(error46, mapper = (issue2) => issue2.message) {
|
|
|
1640
1817
|
}
|
|
1641
1818
|
function treeifyError(error46, mapper = (issue2) => issue2.message) {
|
|
1642
1819
|
const result = { errors: [] };
|
|
1643
|
-
const processError = (error47,
|
|
1820
|
+
const processError = (error47, path7 = []) => {
|
|
1644
1821
|
var _a, _b;
|
|
1645
1822
|
for (const issue2 of error47.issues) {
|
|
1646
1823
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
@@ -1650,7 +1827,7 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
|
|
|
1650
1827
|
} else if (issue2.code === "invalid_element") {
|
|
1651
1828
|
processError({ issues: issue2.issues }, issue2.path);
|
|
1652
1829
|
} else {
|
|
1653
|
-
const fullpath = [...
|
|
1830
|
+
const fullpath = [...path7, ...issue2.path];
|
|
1654
1831
|
if (fullpath.length === 0) {
|
|
1655
1832
|
result.errors.push(mapper(issue2));
|
|
1656
1833
|
continue;
|
|
@@ -1682,8 +1859,8 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
|
|
|
1682
1859
|
}
|
|
1683
1860
|
function toDotPath(_path) {
|
|
1684
1861
|
const segs = [];
|
|
1685
|
-
const
|
|
1686
|
-
for (const seg of
|
|
1862
|
+
const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
1863
|
+
for (const seg of path7) {
|
|
1687
1864
|
if (typeof seg === "number")
|
|
1688
1865
|
segs.push(`[${seg}]`);
|
|
1689
1866
|
else if (typeof seg === "symbol")
|
|
@@ -12927,65 +13104,64 @@ var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
|
12927
13104
|
var MAX_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
|
|
12928
13105
|
var MAX_COMMAND_STATUS_WAIT_MS = 15e3;
|
|
12929
13106
|
var deviceNameSchema = external_exports.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f]+$/, "Device name contains control characters");
|
|
12930
|
-
var
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
path:
|
|
12934
|
-
});
|
|
12935
|
-
var readFileJobSchema =
|
|
13107
|
+
var relativePathSchema = external_exports.string().max(4096).describe("Path relative to the exposed workspace root. Absolute paths and parent traversal are rejected.");
|
|
13108
|
+
var boundedTextSchema = external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_TEXT_BYTES);
|
|
13109
|
+
var readFileRequestSchema = external_exports.object({
|
|
13110
|
+
path: relativePathSchema
|
|
13111
|
+
}).strict();
|
|
13112
|
+
var readFileJobSchema = readFileRequestSchema.extend({
|
|
12936
13113
|
type: external_exports.literal("read_file"),
|
|
12937
|
-
requestId: external_exports.string().uuid()
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
13114
|
+
requestId: external_exports.string().uuid()
|
|
13115
|
+
});
|
|
13116
|
+
var writeFileRequestSchema = external_exports.object({
|
|
13117
|
+
path: relativePathSchema,
|
|
13118
|
+
content: boundedTextSchema.describe("Complete UTF-8 text content that will replace the file."),
|
|
13119
|
+
expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("SHA-256 returned by read_file. When provided, the write fails if the file changed.")
|
|
13120
|
+
}).strict();
|
|
13121
|
+
var writeFileJobSchema = writeFileRequestSchema.extend({
|
|
12942
13122
|
type: external_exports.literal("write_file"),
|
|
12943
|
-
requestId: external_exports.string().uuid()
|
|
12944
|
-
workspaceId: external_exports.string().uuid(),
|
|
12945
|
-
path: external_exports.string().max(4096),
|
|
12946
|
-
content: external_exports.string(),
|
|
12947
|
-
expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
13123
|
+
requestId: external_exports.string().uuid()
|
|
12948
13124
|
});
|
|
12949
|
-
|
|
12950
|
-
type: external_exports.literal("run_command"),
|
|
12951
|
-
requestId: external_exports.string().uuid(),
|
|
12952
|
-
workspaceId: external_exports.string().uuid(),
|
|
12953
|
-
argv: external_exports.array(external_exports.string()).min(1).max(256).optional(),
|
|
12954
|
-
shellCommand: external_exports.string().max(64 * 1024).optional(),
|
|
12955
|
-
stdin: external_exports.string().max(MAX_TEXT_BYTES).optional(),
|
|
12956
|
-
timeoutMs: external_exports.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).default(DEFAULT_COMMAND_TIMEOUT_MS)
|
|
12957
|
-
}).superRefine((value, context) => {
|
|
13125
|
+
function requireOneCommand(value, context) {
|
|
12958
13126
|
if ((value.argv ? 1 : 0) + (value.shellCommand ? 1 : 0) !== 1) {
|
|
12959
13127
|
context.addIssue({
|
|
12960
13128
|
code: "custom",
|
|
12961
|
-
message: "Exactly one of argv or shellCommand is required."
|
|
13129
|
+
message: "Exactly one of argv or shellCommand is required.",
|
|
13130
|
+
input: value
|
|
12962
13131
|
});
|
|
12963
13132
|
}
|
|
13133
|
+
}
|
|
13134
|
+
var runCommandRequestSchema = external_exports.object({
|
|
13135
|
+
argv: external_exports.array(external_exports.string()).min(1).max(256).optional().describe("Executable and arguments passed without shell parsing. Provide this or shellCommand, not both."),
|
|
13136
|
+
shellCommand: external_exports.string().max(64 * 1024).optional().describe("PowerShell command text. Provide this or argv, not both."),
|
|
13137
|
+
stdin: boundedTextSchema.optional().describe("Optional UTF-8 text sent to the command standard input."),
|
|
13138
|
+
timeoutMs: external_exports.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).default(DEFAULT_COMMAND_TIMEOUT_MS).describe("Maximum command runtime in milliseconds. Defaults to 900000 and cannot exceed 3600000.")
|
|
13139
|
+
}).strict().superRefine(requireOneCommand);
|
|
13140
|
+
var runCommandJobSchema = runCommandRequestSchema.safeExtend({
|
|
13141
|
+
type: external_exports.literal("run_command"),
|
|
13142
|
+
requestId: external_exports.string().uuid()
|
|
12964
13143
|
});
|
|
12965
|
-
var
|
|
13144
|
+
var getCommandRequestSchema = external_exports.object({
|
|
13145
|
+
commandId: external_exports.string().uuid().describe("Command identifier returned by run_command."),
|
|
13146
|
+
waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional().describe("Optional long-poll duration in milliseconds, from 0 through 15000.")
|
|
13147
|
+
}).strict();
|
|
13148
|
+
var getCommandJobSchema = getCommandRequestSchema.extend({
|
|
12966
13149
|
type: external_exports.literal("get_command"),
|
|
12967
|
-
requestId: external_exports.string().uuid()
|
|
12968
|
-
commandId: external_exports.string().uuid(),
|
|
12969
|
-
waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional()
|
|
13150
|
+
requestId: external_exports.string().uuid()
|
|
12970
13151
|
});
|
|
12971
|
-
var
|
|
13152
|
+
var cancelCommandRequestSchema = external_exports.object({
|
|
13153
|
+
commandId: external_exports.string().uuid().describe("Command identifier returned by run_command.")
|
|
13154
|
+
}).strict();
|
|
13155
|
+
var cancelCommandJobSchema = cancelCommandRequestSchema.extend({
|
|
12972
13156
|
type: external_exports.literal("cancel_command"),
|
|
12973
|
-
requestId: external_exports.string().uuid()
|
|
12974
|
-
commandId: external_exports.string().uuid()
|
|
12975
|
-
});
|
|
12976
|
-
var closeWorkspaceJobSchema = external_exports.object({
|
|
12977
|
-
type: external_exports.literal("close_workspace"),
|
|
12978
|
-
requestId: external_exports.string().uuid(),
|
|
12979
|
-
workspaceId: external_exports.string().uuid()
|
|
13157
|
+
requestId: external_exports.string().uuid()
|
|
12980
13158
|
});
|
|
12981
13159
|
var workerJobSchema = external_exports.discriminatedUnion("type", [
|
|
12982
|
-
openWorkspaceJobSchema,
|
|
12983
13160
|
readFileJobSchema,
|
|
12984
13161
|
writeFileJobSchema,
|
|
12985
13162
|
runCommandJobSchema,
|
|
12986
13163
|
getCommandJobSchema,
|
|
12987
|
-
cancelCommandJobSchema
|
|
12988
|
-
closeWorkspaceJobSchema
|
|
13164
|
+
cancelCommandJobSchema
|
|
12989
13165
|
]);
|
|
12990
13166
|
var workerResultSchema = external_exports.object({
|
|
12991
13167
|
requestId: external_exports.string().uuid(),
|
|
@@ -13038,16 +13214,62 @@ function loadRelayEndpoints(environment = process.env) {
|
|
|
13038
13214
|
function defaultDeviceName() {
|
|
13039
13215
|
return deviceNameSchema.parse(os2.hostname());
|
|
13040
13216
|
}
|
|
13041
|
-
function
|
|
13042
|
-
if (status === 401) return new Error("Glossa login was rejected.
|
|
13217
|
+
function relayError(status, data) {
|
|
13218
|
+
if (status === 401) return new Error("Glossa login was rejected. Sign in again when prompted.");
|
|
13043
13219
|
if (status === 403 && data.error === "account_disabled") {
|
|
13044
13220
|
return new Error("This Glossa account is disabled.");
|
|
13045
13221
|
}
|
|
13222
|
+
if (status === 403 && data.error === "identity_provider_not_allowed") {
|
|
13223
|
+
return new Error(
|
|
13224
|
+
"This Glossa identity provider is not allowed. Sign in with Google."
|
|
13225
|
+
);
|
|
13226
|
+
}
|
|
13046
13227
|
if (status === 409 && data.error === "device_name_conflict") {
|
|
13047
|
-
return new Error("A Glossa device
|
|
13228
|
+
return new Error("A Glossa device already uses this name. Run glossa devices list, then rename or revoke the old device.");
|
|
13229
|
+
}
|
|
13230
|
+
if (status === 404 && data.error === "device_not_found") {
|
|
13231
|
+
return new Error("The Glossa device was not found.");
|
|
13048
13232
|
}
|
|
13049
13233
|
if (status === 429) return new Error("Glossa device enrollment is rate limited. Try again later.");
|
|
13050
|
-
return new Error(`Glossa
|
|
13234
|
+
return new Error(`The Glossa relay returned HTTP ${status}.`);
|
|
13235
|
+
}
|
|
13236
|
+
function validNullableString(value) {
|
|
13237
|
+
return value === null || typeof value === "string";
|
|
13238
|
+
}
|
|
13239
|
+
function parseDevices2(value) {
|
|
13240
|
+
if (!Array.isArray(value)) {
|
|
13241
|
+
throw new Error("The Glossa relay returned an invalid device list response.");
|
|
13242
|
+
}
|
|
13243
|
+
const devices = value;
|
|
13244
|
+
if (devices.some(
|
|
13245
|
+
(device) => typeof device.id !== "string" || typeof device.name !== "string" || !validNullableString(device.platform) || !validNullableString(device.lastSeenAt) || !validNullableString(device.revokedAt) || device.activeWorkers !== void 0 && (!Number.isInteger(device.activeWorkers) || device.activeWorkers < 0)
|
|
13246
|
+
)) {
|
|
13247
|
+
throw new Error("The Glossa relay returned an invalid device list response.");
|
|
13248
|
+
}
|
|
13249
|
+
return devices.map((device) => ({
|
|
13250
|
+
...device,
|
|
13251
|
+
activeWorkers: device.activeWorkers ?? null
|
|
13252
|
+
}));
|
|
13253
|
+
}
|
|
13254
|
+
async function listDevices(endpoints, credentials, fetchRequest = fetch) {
|
|
13255
|
+
const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices`, {
|
|
13256
|
+
headers: {
|
|
13257
|
+
authorization: `${credentials.tokenType} ${credentials.accessToken}`
|
|
13258
|
+
}
|
|
13259
|
+
});
|
|
13260
|
+
let data = {};
|
|
13261
|
+
try {
|
|
13262
|
+
data = await response.json();
|
|
13263
|
+
} catch {
|
|
13264
|
+
}
|
|
13265
|
+
if (!response.ok) throw relayError(response.status, data);
|
|
13266
|
+
return parseDevices2(data.devices);
|
|
13267
|
+
}
|
|
13268
|
+
async function accountOwnsDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
|
|
13269
|
+
const devices = await listDevices(endpoints, credentials, fetchRequest);
|
|
13270
|
+
return devices.some(
|
|
13271
|
+
(device) => device.id === deviceId && device.revokedAt === null
|
|
13272
|
+
);
|
|
13051
13273
|
}
|
|
13052
13274
|
async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
|
|
13053
13275
|
const name = deviceNameSchema.parse(deviceName);
|
|
@@ -13064,7 +13286,7 @@ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = f
|
|
|
13064
13286
|
data = await response.json();
|
|
13065
13287
|
} catch {
|
|
13066
13288
|
}
|
|
13067
|
-
if (!response.ok) throw
|
|
13289
|
+
if (!response.ok) throw relayError(response.status, data);
|
|
13068
13290
|
if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
|
|
13069
13291
|
throw new Error("The Glossa relay returned an invalid device enrollment response.");
|
|
13070
13292
|
}
|
|
@@ -13075,9 +13297,108 @@ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = f
|
|
|
13075
13297
|
token: data.device_token
|
|
13076
13298
|
};
|
|
13077
13299
|
}
|
|
13300
|
+
async function renameDevice(endpoints, credentials, deviceId, name, fetchRequest = fetch) {
|
|
13301
|
+
const validName = deviceNameSchema.parse(name);
|
|
13302
|
+
const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
|
|
13303
|
+
method: "PATCH",
|
|
13304
|
+
headers: {
|
|
13305
|
+
authorization: `${credentials.tokenType} ${credentials.accessToken}`,
|
|
13306
|
+
"content-type": "application/json"
|
|
13307
|
+
},
|
|
13308
|
+
body: JSON.stringify({ name: validName })
|
|
13309
|
+
});
|
|
13310
|
+
const data = await response.json().catch(() => ({}));
|
|
13311
|
+
if (!response.ok) throw relayError(response.status, data);
|
|
13312
|
+
return parseDevices2([data.device])[0];
|
|
13313
|
+
}
|
|
13314
|
+
async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
|
|
13315
|
+
const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
|
|
13316
|
+
method: "DELETE",
|
|
13317
|
+
headers: {
|
|
13318
|
+
authorization: `${credentials.tokenType} ${credentials.accessToken}`
|
|
13319
|
+
}
|
|
13320
|
+
});
|
|
13321
|
+
if (!response.ok) {
|
|
13322
|
+
const data = await response.json().catch(() => ({}));
|
|
13323
|
+
throw relayError(response.status, data);
|
|
13324
|
+
}
|
|
13325
|
+
}
|
|
13078
13326
|
|
|
13079
|
-
// src/
|
|
13080
|
-
|
|
13327
|
+
// src/logout.ts
|
|
13328
|
+
function browserLogoutUrl(issuer) {
|
|
13329
|
+
return new URL(
|
|
13330
|
+
"v2/logout",
|
|
13331
|
+
issuer.endsWith("/") ? issuer : `${issuer}/`
|
|
13332
|
+
).toString();
|
|
13333
|
+
}
|
|
13334
|
+
async function logoutFromGlossa(options, dependencies = {}) {
|
|
13335
|
+
const remove = dependencies.deleteCredentials ?? deleteCredentials;
|
|
13336
|
+
const browse = dependencies.openBrowser ?? openBrowser;
|
|
13337
|
+
const log = dependencies.log ?? console.log;
|
|
13338
|
+
let issuer = dependencies.issuer;
|
|
13339
|
+
if (options.browser && issuer === void 0) {
|
|
13340
|
+
const loadStoredIssuer = dependencies.loadStoredIssuer ?? (async () => (await loadCredentials())?.credentials.issuer);
|
|
13341
|
+
try {
|
|
13342
|
+
issuer = await loadStoredIssuer();
|
|
13343
|
+
} catch {
|
|
13344
|
+
}
|
|
13345
|
+
}
|
|
13346
|
+
await remove();
|
|
13347
|
+
log("Signed out of Glossa locally.");
|
|
13348
|
+
if (!options.browser) return;
|
|
13349
|
+
const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
|
|
13350
|
+
const opened = await browse(url2);
|
|
13351
|
+
if (opened) {
|
|
13352
|
+
log("Opened Glossa browser sign-out.");
|
|
13353
|
+
} else {
|
|
13354
|
+
log("Open this URL to finish signing out in your browser:");
|
|
13355
|
+
log(url2);
|
|
13356
|
+
}
|
|
13357
|
+
log(
|
|
13358
|
+
"Reconnect Glossa in ChatGPT, then choose the same Google account when the CLI signs in."
|
|
13359
|
+
);
|
|
13360
|
+
}
|
|
13361
|
+
|
|
13362
|
+
// src/device-store.ts
|
|
13363
|
+
import path4 from "node:path";
|
|
13364
|
+
var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
|
|
13365
|
+
function parseDeviceCredential(value) {
|
|
13366
|
+
let parsed;
|
|
13367
|
+
try {
|
|
13368
|
+
parsed = JSON.parse(value);
|
|
13369
|
+
} catch {
|
|
13370
|
+
throw new Error("Stored Glossa device credentials are invalid.");
|
|
13371
|
+
}
|
|
13372
|
+
let relayOriginValid = false;
|
|
13373
|
+
if (typeof parsed.relayOrigin === "string") {
|
|
13374
|
+
try {
|
|
13375
|
+
relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
|
|
13376
|
+
} catch {
|
|
13377
|
+
relayOriginValid = false;
|
|
13378
|
+
}
|
|
13379
|
+
}
|
|
13380
|
+
if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
13381
|
+
parsed.deviceId
|
|
13382
|
+
) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
|
|
13383
|
+
throw new Error("Stored Glossa device credentials are invalid.");
|
|
13384
|
+
}
|
|
13385
|
+
return parsed;
|
|
13386
|
+
}
|
|
13387
|
+
var store2 = new SecureStore({
|
|
13388
|
+
account: "device",
|
|
13389
|
+
file: path4.join(configDirectory(), "device.json"),
|
|
13390
|
+
warning: FILE_DEVICE_WARNING,
|
|
13391
|
+
parse: parseDeviceCredential
|
|
13392
|
+
});
|
|
13393
|
+
async function loadDeviceCredential() {
|
|
13394
|
+
return (await store2.load())?.value ?? null;
|
|
13395
|
+
}
|
|
13396
|
+
async function saveDeviceCredential(credential) {
|
|
13397
|
+
await store2.save(credential);
|
|
13398
|
+
}
|
|
13399
|
+
async function deleteDeviceCredential() {
|
|
13400
|
+
await store2.delete();
|
|
13401
|
+
}
|
|
13081
13402
|
|
|
13082
13403
|
// src/worker/command-service.ts
|
|
13083
13404
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -13147,10 +13468,10 @@ async function terminateProcessTree(child) {
|
|
|
13147
13468
|
}
|
|
13148
13469
|
}
|
|
13149
13470
|
var CommandService = class {
|
|
13150
|
-
constructor(
|
|
13151
|
-
this.
|
|
13471
|
+
constructor(policy) {
|
|
13472
|
+
this.policy = policy;
|
|
13152
13473
|
}
|
|
13153
|
-
|
|
13474
|
+
policy;
|
|
13154
13475
|
#commands = /* @__PURE__ */ new Map();
|
|
13155
13476
|
#activeCommandId = null;
|
|
13156
13477
|
async start(options) {
|
|
@@ -13174,12 +13495,12 @@ var CommandService = class {
|
|
|
13174
13495
|
"Command timeout must be between 1 millisecond and 60 minutes."
|
|
13175
13496
|
);
|
|
13176
13497
|
}
|
|
13177
|
-
const cwd =
|
|
13498
|
+
const cwd = this.policy.root;
|
|
13178
13499
|
const invocation = options.argv ? { file: options.argv[0], args: options.argv.slice(1) } : shellInvocation(options.shellCommand);
|
|
13179
13500
|
const child = spawn2(invocation.file, invocation.args, {
|
|
13180
13501
|
cwd,
|
|
13181
13502
|
env: process.env,
|
|
13182
|
-
detached:
|
|
13503
|
+
detached: process.platform !== "win32",
|
|
13183
13504
|
stdio: "pipe",
|
|
13184
13505
|
windowsHide: true
|
|
13185
13506
|
});
|
|
@@ -13249,7 +13570,15 @@ var CommandService = class {
|
|
|
13249
13570
|
throw new WorkerError("invalid_wait", "Status wait must be between 0 and 15 seconds.");
|
|
13250
13571
|
}
|
|
13251
13572
|
if (record2.status === "running" && waitMs > 0) {
|
|
13252
|
-
|
|
13573
|
+
const waitController = new AbortController();
|
|
13574
|
+
try {
|
|
13575
|
+
await Promise.race([
|
|
13576
|
+
record2.completion,
|
|
13577
|
+
delay2(waitMs, void 0, { signal: waitController.signal })
|
|
13578
|
+
]);
|
|
13579
|
+
} finally {
|
|
13580
|
+
waitController.abort();
|
|
13581
|
+
}
|
|
13253
13582
|
}
|
|
13254
13583
|
return this.snapshot(record2);
|
|
13255
13584
|
}
|
|
@@ -13292,19 +13621,17 @@ var CommandService = class {
|
|
|
13292
13621
|
// src/worker/file-service.ts
|
|
13293
13622
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
13294
13623
|
import { lstat, readFile as readFile2, rename, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
13295
|
-
import
|
|
13624
|
+
import path5 from "node:path";
|
|
13296
13625
|
function sha256(content) {
|
|
13297
13626
|
return createHash("sha256").update(content).digest("hex");
|
|
13298
13627
|
}
|
|
13299
13628
|
var FileService = class {
|
|
13300
|
-
constructor(policy
|
|
13629
|
+
constructor(policy) {
|
|
13301
13630
|
this.policy = policy;
|
|
13302
|
-
this.workspaces = workspaces;
|
|
13303
13631
|
}
|
|
13304
13632
|
policy;
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
const target = await this.workspaces.resolve(workspaceId, relativePath);
|
|
13633
|
+
async readText(relativePath) {
|
|
13634
|
+
const target = await this.policy.resolveExisting(relativePath);
|
|
13308
13635
|
const targetStat = await stat(target);
|
|
13309
13636
|
if (!targetStat.isFile()) {
|
|
13310
13637
|
throw new WorkerError("not_file", "The requested path is not a file.");
|
|
@@ -13321,13 +13648,12 @@ var FileService = class {
|
|
|
13321
13648
|
}
|
|
13322
13649
|
return { content: text, sha256: sha256(content), bytes: content.byteLength };
|
|
13323
13650
|
}
|
|
13324
|
-
async writeText(
|
|
13651
|
+
async writeText(relativePath, content, expectedSha256) {
|
|
13325
13652
|
const bytes = Buffer.from(content, "utf8");
|
|
13326
13653
|
if (bytes.byteLength > MAX_TEXT_BYTES) {
|
|
13327
13654
|
throw new WorkerError("file_too_large", "The content exceeds the 1 MiB text limit.");
|
|
13328
13655
|
}
|
|
13329
|
-
|
|
13330
|
-
let target = await this.policy.resolveWritableFile(workspaceRelative);
|
|
13656
|
+
let target = await this.policy.resolveWritableFile(relativePath);
|
|
13331
13657
|
if (expectedSha256) {
|
|
13332
13658
|
let actual = null;
|
|
13333
13659
|
try {
|
|
@@ -13340,10 +13666,10 @@ var FileService = class {
|
|
|
13340
13666
|
throw new WorkerError("stale_revision", "The file revision has changed.");
|
|
13341
13667
|
}
|
|
13342
13668
|
}
|
|
13343
|
-
const temporary =
|
|
13669
|
+
const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
|
|
13344
13670
|
try {
|
|
13345
13671
|
await writeFile2(temporary, bytes, { flag: "wx", mode: 384 });
|
|
13346
|
-
target = await this.policy.resolveWritableFile(
|
|
13672
|
+
target = await this.policy.resolveWritableFile(relativePath);
|
|
13347
13673
|
const tempStat = await lstat(temporary);
|
|
13348
13674
|
if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
|
|
13349
13675
|
throw new WorkerError("unsafe_temporary_file", "The atomic write temporary file changed.");
|
|
@@ -13359,19 +13685,19 @@ var FileService = class {
|
|
|
13359
13685
|
// src/worker/path-policy.ts
|
|
13360
13686
|
import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
|
|
13361
13687
|
import os3 from "node:os";
|
|
13362
|
-
import
|
|
13688
|
+
import path6 from "node:path";
|
|
13363
13689
|
function samePath(left, right) {
|
|
13364
13690
|
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
13365
13691
|
}
|
|
13366
13692
|
function isWithin(root, candidate) {
|
|
13367
|
-
const relative =
|
|
13368
|
-
return relative === "" || !relative.startsWith(`..${
|
|
13693
|
+
const relative = path6.relative(root, candidate);
|
|
13694
|
+
return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
|
|
13369
13695
|
}
|
|
13370
13696
|
function validateRelativePath(value) {
|
|
13371
13697
|
if (value.includes("\0")) {
|
|
13372
13698
|
throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
|
|
13373
13699
|
}
|
|
13374
|
-
if (
|
|
13700
|
+
if (path6.isAbsolute(value) || path6.win32.isAbsolute(value) || path6.posix.isAbsolute(value)) {
|
|
13375
13701
|
throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
|
|
13376
13702
|
}
|
|
13377
13703
|
const segments = value.split(/[\\/]+/);
|
|
@@ -13381,14 +13707,19 @@ function validateRelativePath(value) {
|
|
|
13381
13707
|
return value === "" ? "." : value;
|
|
13382
13708
|
}
|
|
13383
13709
|
async function canonicalizeRoot(candidate, allowBroadRoot = false) {
|
|
13384
|
-
const root = await realpath(
|
|
13710
|
+
const root = await realpath(path6.resolve(candidate)).catch((error46) => {
|
|
13711
|
+
if (error46.code === "ENOENT") {
|
|
13712
|
+
throw new WorkerError("root_not_found", "The workspace directory does not exist.");
|
|
13713
|
+
}
|
|
13714
|
+
throw error46;
|
|
13715
|
+
});
|
|
13385
13716
|
const rootStat = await stat2(root);
|
|
13386
13717
|
if (!rootStat.isDirectory()) {
|
|
13387
13718
|
throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
|
|
13388
13719
|
}
|
|
13389
13720
|
if (!allowBroadRoot) {
|
|
13390
|
-
const filesystemRoot =
|
|
13391
|
-
const home = await realpath(os3.homedir()).catch(() =>
|
|
13721
|
+
const filesystemRoot = path6.parse(root).root;
|
|
13722
|
+
const home = await realpath(os3.homedir()).catch(() => path6.resolve(os3.homedir()));
|
|
13392
13723
|
if (samePath(root, filesystemRoot) || samePath(root, home)) {
|
|
13393
13724
|
throw new WorkerError(
|
|
13394
13725
|
"broad_root_refused",
|
|
@@ -13420,16 +13751,9 @@ var PathPolicy = class _PathPolicy {
|
|
|
13420
13751
|
}
|
|
13421
13752
|
return canonical;
|
|
13422
13753
|
}
|
|
13423
|
-
async resolveDirectory(relativePath) {
|
|
13424
|
-
const resolved = await this.resolveExisting(relativePath);
|
|
13425
|
-
if (!(await stat2(resolved)).isDirectory()) {
|
|
13426
|
-
throw new WorkerError("not_directory", "The requested workspace is not a directory.");
|
|
13427
|
-
}
|
|
13428
|
-
return resolved;
|
|
13429
|
-
}
|
|
13430
13754
|
async resolveWritableFile(relativePath) {
|
|
13431
13755
|
const lexical = this.resolveLexical(relativePath);
|
|
13432
|
-
const parent =
|
|
13756
|
+
const parent = path6.dirname(lexical);
|
|
13433
13757
|
await this.rejectLinkedComponents(parent);
|
|
13434
13758
|
const canonicalParent = await realpath(parent).catch((error46) => {
|
|
13435
13759
|
if (error46.code === "ENOENT") {
|
|
@@ -13455,11 +13779,11 @@ var PathPolicy = class _PathPolicy {
|
|
|
13455
13779
|
if (error46 instanceof WorkerError) throw error46;
|
|
13456
13780
|
if (error46.code !== "ENOENT") throw error46;
|
|
13457
13781
|
}
|
|
13458
|
-
return
|
|
13782
|
+
return path6.join(canonicalParent, path6.basename(lexical));
|
|
13459
13783
|
}
|
|
13460
13784
|
resolveLexical(relativePath) {
|
|
13461
13785
|
const validated = validateRelativePath(relativePath);
|
|
13462
|
-
const candidate =
|
|
13786
|
+
const candidate = path6.resolve(this.root, validated);
|
|
13463
13787
|
if (!isWithin(this.root, candidate)) {
|
|
13464
13788
|
throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
|
|
13465
13789
|
}
|
|
@@ -13469,11 +13793,11 @@ var PathPolicy = class _PathPolicy {
|
|
|
13469
13793
|
if (!isWithin(this.root, candidate)) {
|
|
13470
13794
|
throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
|
|
13471
13795
|
}
|
|
13472
|
-
const relative =
|
|
13796
|
+
const relative = path6.relative(this.root, candidate);
|
|
13473
13797
|
if (!relative) return;
|
|
13474
13798
|
let current = this.root;
|
|
13475
|
-
for (const segment of relative.split(
|
|
13476
|
-
current =
|
|
13799
|
+
for (const segment of relative.split(path6.sep)) {
|
|
13800
|
+
current = path6.join(current, segment);
|
|
13477
13801
|
try {
|
|
13478
13802
|
const currentStat = await lstat2(current);
|
|
13479
13803
|
if (currentStat.isSymbolicLink()) {
|
|
@@ -13491,93 +13815,33 @@ var PathPolicy = class _PathPolicy {
|
|
|
13491
13815
|
}
|
|
13492
13816
|
};
|
|
13493
13817
|
|
|
13494
|
-
// src/worker/workspace-manager.ts
|
|
13495
|
-
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13496
|
-
import path4 from "node:path";
|
|
13497
|
-
var DEFAULT_LEASE_MS = 5 * 60 * 1e3;
|
|
13498
|
-
var WorkspaceManager = class {
|
|
13499
|
-
constructor(policy, leaseMs = DEFAULT_LEASE_MS) {
|
|
13500
|
-
this.policy = policy;
|
|
13501
|
-
this.leaseMs = leaseMs;
|
|
13502
|
-
}
|
|
13503
|
-
policy;
|
|
13504
|
-
leaseMs;
|
|
13505
|
-
#leases = /* @__PURE__ */ new Map();
|
|
13506
|
-
async open(relativePath = ".") {
|
|
13507
|
-
const resolved = await this.policy.resolveDirectory(relativePath);
|
|
13508
|
-
const normalized = path4.relative(this.policy.root, resolved) || ".";
|
|
13509
|
-
const lease = {
|
|
13510
|
-
id: randomUUID3(),
|
|
13511
|
-
relativePath: normalized,
|
|
13512
|
-
expiresAt: Date.now() + this.leaseMs
|
|
13513
|
-
};
|
|
13514
|
-
this.#leases.set(lease.id, lease);
|
|
13515
|
-
return {
|
|
13516
|
-
workspaceId: lease.id,
|
|
13517
|
-
path: normalized,
|
|
13518
|
-
expiresAt: new Date(lease.expiresAt).toISOString()
|
|
13519
|
-
};
|
|
13520
|
-
}
|
|
13521
|
-
async resolve(workspaceId, relativePath = ".") {
|
|
13522
|
-
const lease = this.#leases.get(workspaceId);
|
|
13523
|
-
if (!lease || lease.expiresAt <= Date.now()) {
|
|
13524
|
-
this.#leases.delete(workspaceId);
|
|
13525
|
-
throw new WorkerError("workspace_expired", "The workspace lease has expired.");
|
|
13526
|
-
}
|
|
13527
|
-
lease.expiresAt = Date.now() + this.leaseMs;
|
|
13528
|
-
return await this.policy.resolveExisting(
|
|
13529
|
-
path4.join(lease.relativePath, validateRelativePath(relativePath))
|
|
13530
|
-
);
|
|
13531
|
-
}
|
|
13532
|
-
relativePath(workspaceId, relativePath) {
|
|
13533
|
-
const lease = this.#leases.get(workspaceId);
|
|
13534
|
-
if (!lease || lease.expiresAt <= Date.now()) {
|
|
13535
|
-
this.#leases.delete(workspaceId);
|
|
13536
|
-
throw new WorkerError("workspace_expired", "The workspace lease has expired.");
|
|
13537
|
-
}
|
|
13538
|
-
lease.expiresAt = Date.now() + this.leaseMs;
|
|
13539
|
-
return path4.join(lease.relativePath, validateRelativePath(relativePath));
|
|
13540
|
-
}
|
|
13541
|
-
close(workspaceId) {
|
|
13542
|
-
return this.#leases.delete(workspaceId);
|
|
13543
|
-
}
|
|
13544
|
-
};
|
|
13545
|
-
|
|
13546
13818
|
// src/worker/local-worker.ts
|
|
13547
13819
|
var LocalWorker = class _LocalWorker {
|
|
13548
|
-
constructor(policy,
|
|
13820
|
+
constructor(policy, files, commands) {
|
|
13549
13821
|
this.policy = policy;
|
|
13550
|
-
this.workspaces = workspaces;
|
|
13551
13822
|
this.files = files;
|
|
13552
13823
|
this.commands = commands;
|
|
13553
13824
|
}
|
|
13554
13825
|
policy;
|
|
13555
|
-
workspaces;
|
|
13556
13826
|
files;
|
|
13557
13827
|
commands;
|
|
13558
13828
|
static async create(root, allowBroadRoot = false) {
|
|
13559
13829
|
const policy = await PathPolicy.create(root, allowBroadRoot);
|
|
13560
|
-
const workspaces = new WorkspaceManager(policy);
|
|
13561
13830
|
return new _LocalWorker(
|
|
13562
13831
|
policy,
|
|
13563
|
-
|
|
13564
|
-
new
|
|
13565
|
-
new CommandService(workspaces)
|
|
13832
|
+
new FileService(policy),
|
|
13833
|
+
new CommandService(policy)
|
|
13566
13834
|
);
|
|
13567
13835
|
}
|
|
13568
13836
|
async handle(job) {
|
|
13569
13837
|
try {
|
|
13570
13838
|
let value;
|
|
13571
13839
|
switch (job.type) {
|
|
13572
|
-
case "open_workspace":
|
|
13573
|
-
value = await this.workspaces.open(job.path);
|
|
13574
|
-
break;
|
|
13575
13840
|
case "read_file":
|
|
13576
|
-
value = await this.files.readText(job.
|
|
13841
|
+
value = await this.files.readText(job.path);
|
|
13577
13842
|
break;
|
|
13578
13843
|
case "write_file":
|
|
13579
13844
|
value = await this.files.writeText(
|
|
13580
|
-
job.workspaceId,
|
|
13581
13845
|
job.path,
|
|
13582
13846
|
job.content,
|
|
13583
13847
|
job.expectedSha256
|
|
@@ -13585,7 +13849,6 @@ var LocalWorker = class _LocalWorker {
|
|
|
13585
13849
|
break;
|
|
13586
13850
|
case "run_command":
|
|
13587
13851
|
value = await this.commands.start({
|
|
13588
|
-
workspaceId: job.workspaceId,
|
|
13589
13852
|
...job.argv ? { argv: job.argv } : {},
|
|
13590
13853
|
...job.shellCommand ? { shellCommand: job.shellCommand } : {},
|
|
13591
13854
|
...job.stdin !== void 0 ? { stdin: job.stdin } : {},
|
|
@@ -13598,9 +13861,6 @@ var LocalWorker = class _LocalWorker {
|
|
|
13598
13861
|
case "cancel_command":
|
|
13599
13862
|
value = await this.commands.cancel(job.commandId);
|
|
13600
13863
|
break;
|
|
13601
|
-
case "close_workspace":
|
|
13602
|
-
value = { closed: this.workspaces.close(job.workspaceId) };
|
|
13603
|
-
break;
|
|
13604
13864
|
}
|
|
13605
13865
|
return { requestId: job.requestId, ok: true, value };
|
|
13606
13866
|
} catch (error46) {
|
|
@@ -13617,181 +13877,12 @@ var LocalWorker = class _LocalWorker {
|
|
|
13617
13877
|
}
|
|
13618
13878
|
};
|
|
13619
13879
|
|
|
13620
|
-
// src/worker/local-session.ts
|
|
13621
|
-
var visibleActivity = /* @__PURE__ */ new Set(["write_file", "run_command", "cancel_command"]);
|
|
13622
|
-
async function runLocalSession(root, allowBroadRoot = false) {
|
|
13623
|
-
const worker = await LocalWorker.create(root, allowBroadRoot);
|
|
13624
|
-
console.error(`Glossa local worker root: ${worker.policy.root}`);
|
|
13625
|
-
console.error(
|
|
13626
|
-
"Commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
|
|
13627
|
-
);
|
|
13628
|
-
const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
13629
|
-
const stop = () => lines.close();
|
|
13630
|
-
process.once("SIGINT", stop);
|
|
13631
|
-
process.once("SIGTERM", stop);
|
|
13632
|
-
try {
|
|
13633
|
-
for await (const line of lines) {
|
|
13634
|
-
if (!line.trim()) continue;
|
|
13635
|
-
try {
|
|
13636
|
-
const job = workerJobSchema.parse(JSON.parse(line));
|
|
13637
|
-
if (visibleActivity.has(job.type)) {
|
|
13638
|
-
console.error(`Activity started: ${job.type} (${job.requestId})`);
|
|
13639
|
-
}
|
|
13640
|
-
const result = await worker.handle(job);
|
|
13641
|
-
process.stdout.write(`${JSON.stringify(result)}
|
|
13642
|
-
`);
|
|
13643
|
-
if (visibleActivity.has(job.type)) {
|
|
13644
|
-
console.error(
|
|
13645
|
-
`Activity finished: ${job.type} (${job.requestId}), ${result.ok ? "accepted" : "rejected"}`
|
|
13646
|
-
);
|
|
13647
|
-
}
|
|
13648
|
-
} catch {
|
|
13649
|
-
process.stdout.write(
|
|
13650
|
-
`${JSON.stringify({ ok: false, error: { code: "invalid_job", message: "Invalid worker job." } })}
|
|
13651
|
-
`
|
|
13652
|
-
);
|
|
13653
|
-
}
|
|
13654
|
-
}
|
|
13655
|
-
} finally {
|
|
13656
|
-
process.removeListener("SIGINT", stop);
|
|
13657
|
-
process.removeListener("SIGTERM", stop);
|
|
13658
|
-
await worker.shutdown();
|
|
13659
|
-
}
|
|
13660
|
-
}
|
|
13661
|
-
|
|
13662
|
-
// src/device-store.ts
|
|
13663
|
-
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
|
|
13664
|
-
import path5 from "node:path";
|
|
13665
|
-
var KEYRING_SERVICE2 = "Glossa";
|
|
13666
|
-
var KEYRING_ACCOUNT2 = "device";
|
|
13667
|
-
var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
|
|
13668
|
-
function deviceCredentialPath() {
|
|
13669
|
-
return path5.join(configDirectory(), "device.json");
|
|
13670
|
-
}
|
|
13671
|
-
async function defaultEntryProvider2() {
|
|
13672
|
-
try {
|
|
13673
|
-
const { AsyncEntry } = await import("@napi-rs/keyring");
|
|
13674
|
-
return new AsyncEntry(KEYRING_SERVICE2, KEYRING_ACCOUNT2);
|
|
13675
|
-
} catch {
|
|
13676
|
-
return null;
|
|
13677
|
-
}
|
|
13678
|
-
}
|
|
13679
|
-
function parseDeviceCredential(value) {
|
|
13680
|
-
let parsed;
|
|
13681
|
-
try {
|
|
13682
|
-
parsed = JSON.parse(value);
|
|
13683
|
-
} catch {
|
|
13684
|
-
throw new Error("Stored Glossa device credentials are invalid.");
|
|
13685
|
-
}
|
|
13686
|
-
let relayOriginValid = false;
|
|
13687
|
-
if (typeof parsed.relayOrigin === "string") {
|
|
13688
|
-
try {
|
|
13689
|
-
relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
|
|
13690
|
-
} catch {
|
|
13691
|
-
relayOriginValid = false;
|
|
13692
|
-
}
|
|
13693
|
-
}
|
|
13694
|
-
if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
13695
|
-
parsed.deviceId
|
|
13696
|
-
) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
|
|
13697
|
-
throw new Error("Stored Glossa device credentials are invalid.");
|
|
13698
|
-
}
|
|
13699
|
-
return parsed;
|
|
13700
|
-
}
|
|
13701
|
-
async function readFileCredential(target) {
|
|
13702
|
-
try {
|
|
13703
|
-
return parseDeviceCredential(await readFile3(target, "utf8"));
|
|
13704
|
-
} catch (error46) {
|
|
13705
|
-
if (error46.code === "ENOENT") return null;
|
|
13706
|
-
throw error46;
|
|
13707
|
-
}
|
|
13708
|
-
}
|
|
13709
|
-
async function writeFileCredential(target, credential) {
|
|
13710
|
-
await mkdir2(path5.dirname(target), { recursive: true, mode: 448 });
|
|
13711
|
-
await writeFile3(target, `${JSON.stringify(credential, null, 2)}
|
|
13712
|
-
`, {
|
|
13713
|
-
encoding: "utf8",
|
|
13714
|
-
mode: 384
|
|
13715
|
-
});
|
|
13716
|
-
if (process.platform !== "win32") await chmod2(target, 384);
|
|
13717
|
-
}
|
|
13718
|
-
var DeviceStore = class {
|
|
13719
|
-
#credentialFile;
|
|
13720
|
-
#entryProvider;
|
|
13721
|
-
#warn;
|
|
13722
|
-
constructor(options = {}) {
|
|
13723
|
-
this.#credentialFile = options.credentialFile ?? deviceCredentialPath();
|
|
13724
|
-
this.#entryProvider = options.entryProvider ?? defaultEntryProvider2;
|
|
13725
|
-
this.#warn = options.warn ?? console.warn;
|
|
13726
|
-
}
|
|
13727
|
-
async #entry() {
|
|
13728
|
-
try {
|
|
13729
|
-
return await this.#entryProvider();
|
|
13730
|
-
} catch {
|
|
13731
|
-
return null;
|
|
13732
|
-
}
|
|
13733
|
-
}
|
|
13734
|
-
async save(credential) {
|
|
13735
|
-
const entry = await this.#entry();
|
|
13736
|
-
if (entry) {
|
|
13737
|
-
try {
|
|
13738
|
-
await entry.setPassword(JSON.stringify(credential));
|
|
13739
|
-
await rm3(this.#credentialFile, { force: true });
|
|
13740
|
-
return;
|
|
13741
|
-
} catch {
|
|
13742
|
-
}
|
|
13743
|
-
}
|
|
13744
|
-
this.#warn(FILE_DEVICE_WARNING);
|
|
13745
|
-
await writeFileCredential(this.#credentialFile, credential);
|
|
13746
|
-
}
|
|
13747
|
-
async load() {
|
|
13748
|
-
const entry = await this.#entry();
|
|
13749
|
-
if (entry) {
|
|
13750
|
-
try {
|
|
13751
|
-
const value = await entry.getPassword();
|
|
13752
|
-
if (value) return parseDeviceCredential(value);
|
|
13753
|
-
} catch {
|
|
13754
|
-
}
|
|
13755
|
-
}
|
|
13756
|
-
const credential = await readFileCredential(this.#credentialFile);
|
|
13757
|
-
if (!credential) return null;
|
|
13758
|
-
if (entry) {
|
|
13759
|
-
try {
|
|
13760
|
-
await entry.setPassword(JSON.stringify(credential));
|
|
13761
|
-
await rm3(this.#credentialFile, { force: true });
|
|
13762
|
-
return credential;
|
|
13763
|
-
} catch {
|
|
13764
|
-
}
|
|
13765
|
-
}
|
|
13766
|
-
this.#warn(FILE_DEVICE_WARNING);
|
|
13767
|
-
return credential;
|
|
13768
|
-
}
|
|
13769
|
-
async delete() {
|
|
13770
|
-
const entry = await this.#entry();
|
|
13771
|
-
if (entry) {
|
|
13772
|
-
try {
|
|
13773
|
-
await entry.deleteCredential();
|
|
13774
|
-
} catch {
|
|
13775
|
-
}
|
|
13776
|
-
}
|
|
13777
|
-
await rm3(this.#credentialFile, { force: true });
|
|
13778
|
-
}
|
|
13779
|
-
};
|
|
13780
|
-
var defaultStore2 = new DeviceStore();
|
|
13781
|
-
async function loadDeviceCredential() {
|
|
13782
|
-
return await defaultStore2.load();
|
|
13783
|
-
}
|
|
13784
|
-
async function saveDeviceCredential(credential) {
|
|
13785
|
-
await defaultStore2.save(credential);
|
|
13786
|
-
}
|
|
13787
|
-
async function deleteDeviceCredential() {
|
|
13788
|
-
await defaultStore2.delete();
|
|
13789
|
-
}
|
|
13790
|
-
|
|
13791
13880
|
// src/worker/remote-worker.ts
|
|
13881
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13792
13882
|
var WORKER_REQUEST_TIMEOUT_MS = 19e3;
|
|
13793
13883
|
var DEFAULT_RECONNECT_BASE_MS = 500;
|
|
13794
13884
|
var DEFAULT_RECONNECT_MAX_MS = 1e4;
|
|
13885
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
13795
13886
|
var DeviceRejectedError = class extends Error {
|
|
13796
13887
|
constructor() {
|
|
13797
13888
|
super("The relay rejected the device credential.");
|
|
@@ -13835,6 +13926,9 @@ var RemoteWorker = class {
|
|
|
13835
13926
|
#random;
|
|
13836
13927
|
#reconnectBaseMs;
|
|
13837
13928
|
#reconnectMaxMs;
|
|
13929
|
+
#heartbeatMs;
|
|
13930
|
+
#workerId = randomUUID3();
|
|
13931
|
+
#onStatus;
|
|
13838
13932
|
constructor(options) {
|
|
13839
13933
|
this.#origin = new URL(options.origin);
|
|
13840
13934
|
this.#deviceToken = options.deviceToken;
|
|
@@ -13845,44 +13939,83 @@ var RemoteWorker = class {
|
|
|
13845
13939
|
this.#random = options.random ?? Math.random;
|
|
13846
13940
|
this.#reconnectBaseMs = options.reconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
|
|
13847
13941
|
this.#reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
|
|
13942
|
+
this.#heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
13943
|
+
this.#onStatus = options.onStatus ?? (() => {
|
|
13944
|
+
});
|
|
13848
13945
|
}
|
|
13849
13946
|
async run() {
|
|
13850
13947
|
let failures = 0;
|
|
13851
|
-
|
|
13852
|
-
|
|
13853
|
-
|
|
13854
|
-
|
|
13855
|
-
await this.#pollGeneration(generation);
|
|
13856
|
-
} catch (error46) {
|
|
13857
|
-
if (this.#signal.aborted) return;
|
|
13858
|
-
if (error46 instanceof DeviceRejectedError) throw error46;
|
|
13859
|
-
const delay3 = reconnectDelayMs(
|
|
13860
|
-
failures,
|
|
13861
|
-
this.#random,
|
|
13862
|
-
this.#reconnectBaseMs,
|
|
13863
|
-
this.#reconnectMaxMs
|
|
13864
|
-
);
|
|
13865
|
-
failures += 1;
|
|
13948
|
+
let connectedBefore = false;
|
|
13949
|
+
this.#onStatus({ state: "connecting" });
|
|
13950
|
+
try {
|
|
13951
|
+
while (!this.#signal.aborted) {
|
|
13866
13952
|
try {
|
|
13867
|
-
await this.#
|
|
13868
|
-
|
|
13953
|
+
const session = await this.#register();
|
|
13954
|
+
this.#onStatus({
|
|
13955
|
+
state: "connected",
|
|
13956
|
+
reconnected: connectedBefore,
|
|
13957
|
+
legacyRelay: session.legacyRelay
|
|
13958
|
+
});
|
|
13959
|
+
connectedBefore = true;
|
|
13960
|
+
failures = 0;
|
|
13961
|
+
await this.#pollGeneration(session);
|
|
13962
|
+
} catch (error46) {
|
|
13869
13963
|
if (this.#signal.aborted) return;
|
|
13870
|
-
throw
|
|
13964
|
+
if (error46 instanceof DeviceRejectedError) throw error46;
|
|
13965
|
+
const delay3 = reconnectDelayMs(
|
|
13966
|
+
failures,
|
|
13967
|
+
this.#random,
|
|
13968
|
+
this.#reconnectBaseMs,
|
|
13969
|
+
this.#reconnectMaxMs
|
|
13970
|
+
);
|
|
13971
|
+
failures += 1;
|
|
13972
|
+
this.#onStatus({
|
|
13973
|
+
state: "retrying",
|
|
13974
|
+
error: error46 instanceof Error ? error46 : new Error(String(error46)),
|
|
13975
|
+
retryInMs: delay3
|
|
13976
|
+
});
|
|
13977
|
+
try {
|
|
13978
|
+
await this.#sleep(delay3, this.#signal);
|
|
13979
|
+
} catch (sleepError) {
|
|
13980
|
+
if (this.#signal.aborted) return;
|
|
13981
|
+
throw sleepError;
|
|
13982
|
+
}
|
|
13871
13983
|
}
|
|
13872
13984
|
}
|
|
13985
|
+
} finally {
|
|
13986
|
+
await this.#unregister();
|
|
13987
|
+
this.#onStatus({ state: "disconnected" });
|
|
13873
13988
|
}
|
|
13874
13989
|
}
|
|
13875
13990
|
async #register() {
|
|
13876
|
-
|
|
13991
|
+
let response;
|
|
13992
|
+
try {
|
|
13993
|
+
response = await this.#post("/device/register", {
|
|
13994
|
+
workerId: this.#workerId
|
|
13995
|
+
});
|
|
13996
|
+
} catch (error46) {
|
|
13997
|
+
if (!(error46 instanceof RelayResponseError) || error46.status !== 400) {
|
|
13998
|
+
throw error46;
|
|
13999
|
+
}
|
|
14000
|
+
response = await this.#post("/device/register", {});
|
|
14001
|
+
const legacyValue = await response.json();
|
|
14002
|
+
if (typeof legacyValue !== "object" || legacyValue === null || !("generation" in legacyValue) || typeof legacyValue.generation !== "string") {
|
|
14003
|
+
throw new Error("The relay returned an invalid registration response.");
|
|
14004
|
+
}
|
|
14005
|
+
return { generation: legacyValue.generation, legacyRelay: true };
|
|
14006
|
+
}
|
|
13877
14007
|
const value = await response.json();
|
|
13878
|
-
if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string") {
|
|
14008
|
+
if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string" || !("workerId" in value) || value.workerId !== this.#workerId) {
|
|
13879
14009
|
throw new Error("The relay returned an invalid registration response.");
|
|
13880
14010
|
}
|
|
13881
|
-
return value.generation;
|
|
14011
|
+
return { generation: value.generation, legacyRelay: false };
|
|
13882
14012
|
}
|
|
13883
|
-
async #pollGeneration(
|
|
14013
|
+
async #pollGeneration(session) {
|
|
13884
14014
|
while (!this.#signal.aborted) {
|
|
13885
|
-
const response = await this.#post(
|
|
14015
|
+
const response = await this.#post(
|
|
14016
|
+
"/device/poll",
|
|
14017
|
+
session.legacyRelay ? { generation: session.generation } : { workerId: this.#workerId, generation: session.generation }
|
|
14018
|
+
);
|
|
13886
14019
|
if (response.status === 204) continue;
|
|
13887
14020
|
const value = await response.json();
|
|
13888
14021
|
const parsed = workerJobSchema.safeParse(
|
|
@@ -13891,14 +14024,44 @@ var RemoteWorker = class {
|
|
|
13891
14024
|
if (!parsed.success) {
|
|
13892
14025
|
throw new Error("The relay returned an invalid worker job.");
|
|
13893
14026
|
}
|
|
13894
|
-
const
|
|
13895
|
-
|
|
14027
|
+
const heartbeat = session.legacyRelay ? void 0 : setInterval(() => {
|
|
14028
|
+
void this.#post("/device/heartbeat", {
|
|
14029
|
+
workerId: this.#workerId,
|
|
14030
|
+
generation: session.generation
|
|
14031
|
+
}).catch(() => {
|
|
14032
|
+
});
|
|
14033
|
+
}, this.#heartbeatMs);
|
|
14034
|
+
heartbeat?.unref();
|
|
14035
|
+
let result;
|
|
14036
|
+
try {
|
|
14037
|
+
result = await this.#worker.handle(parsed.data);
|
|
14038
|
+
} finally {
|
|
14039
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
14040
|
+
}
|
|
14041
|
+
await this.#post(
|
|
14042
|
+
"/device/result",
|
|
14043
|
+
session.legacyRelay ? result : { workerId: this.#workerId, result }
|
|
14044
|
+
);
|
|
13896
14045
|
}
|
|
13897
14046
|
}
|
|
13898
|
-
async #
|
|
14047
|
+
async #unregister() {
|
|
14048
|
+
try {
|
|
14049
|
+
await this.#fetcher(new URL("/device/unregister", this.#origin), {
|
|
14050
|
+
method: "POST",
|
|
14051
|
+
headers: {
|
|
14052
|
+
authorization: `Device ${this.#deviceToken}`,
|
|
14053
|
+
"content-type": "application/json"
|
|
14054
|
+
},
|
|
14055
|
+
body: JSON.stringify({ workerId: this.#workerId }),
|
|
14056
|
+
signal: AbortSignal.timeout(3e3)
|
|
14057
|
+
});
|
|
14058
|
+
} catch {
|
|
14059
|
+
}
|
|
14060
|
+
}
|
|
14061
|
+
async #post(path7, body) {
|
|
13899
14062
|
const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
|
|
13900
14063
|
const signal = AbortSignal.any([this.#signal, timeout]);
|
|
13901
|
-
const response = await this.#fetcher(new URL(
|
|
14064
|
+
const response = await this.#fetcher(new URL(path7, this.#origin), {
|
|
13902
14065
|
method: "POST",
|
|
13903
14066
|
headers: {
|
|
13904
14067
|
authorization: `Device ${this.#deviceToken}`,
|
|
@@ -13914,17 +14077,22 @@ var RemoteWorker = class {
|
|
|
13914
14077
|
};
|
|
13915
14078
|
|
|
13916
14079
|
// src/worker/managed-session.ts
|
|
13917
|
-
var
|
|
14080
|
+
var visibleActivity = /* @__PURE__ */ new Set(["write_file", "run_command", "cancel_command"]);
|
|
14081
|
+
function activityLabel(type, finished, ok = true) {
|
|
14082
|
+
if (type === "run_command") return finished ? ok ? "Command started" : "Command rejected" : "Command requested";
|
|
14083
|
+
if (type === "write_file") return finished ? ok ? "File write completed" : "File write rejected" : "File write started";
|
|
14084
|
+
return finished ? ok ? "Command cancellation completed" : "Command cancellation rejected" : "Command cancellation requested";
|
|
14085
|
+
}
|
|
13918
14086
|
function visibleWorker(worker) {
|
|
13919
14087
|
return {
|
|
13920
14088
|
async handle(job) {
|
|
13921
|
-
if (
|
|
13922
|
-
console.error(
|
|
14089
|
+
if (visibleActivity.has(job.type)) {
|
|
14090
|
+
console.error(`${activityLabel(job.type, false)} (${job.requestId}).`);
|
|
13923
14091
|
}
|
|
13924
14092
|
const result = await worker.handle(job);
|
|
13925
|
-
if (
|
|
14093
|
+
if (visibleActivity.has(job.type)) {
|
|
13926
14094
|
console.error(
|
|
13927
|
-
|
|
14095
|
+
`${activityLabel(job.type, true, result.ok)} (${job.requestId}).`
|
|
13928
14096
|
);
|
|
13929
14097
|
}
|
|
13930
14098
|
return result;
|
|
@@ -13935,14 +14103,19 @@ async function deviceForSession(endpoints, dependencies = {}) {
|
|
|
13935
14103
|
const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
|
|
13936
14104
|
const loadLogin = dependencies.loadCredentials ?? loadCredentials;
|
|
13937
14105
|
const validate = dependencies.validCredentials ?? validCredentials;
|
|
14106
|
+
const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
|
|
14107
|
+
const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
|
|
13938
14108
|
const enroll = dependencies.enrollDevice ?? enrollDevice;
|
|
13939
14109
|
const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
|
|
13940
14110
|
const name = dependencies.defaultDeviceName ?? defaultDeviceName;
|
|
13941
14111
|
const stored = await loadDevice();
|
|
13942
|
-
if (stored?.relayOrigin === endpoints.relayOrigin) return stored;
|
|
13943
14112
|
const loaded = await loadLogin();
|
|
13944
|
-
if (!loaded) throw new Error("Not signed in. Run
|
|
14113
|
+
if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
|
|
13945
14114
|
const credentials = await validate(loaded.credentials);
|
|
14115
|
+
if (stored?.relayOrigin === endpoints.relayOrigin) {
|
|
14116
|
+
if (await ownsDevice(endpoints, credentials, stored.deviceId)) return stored;
|
|
14117
|
+
await removeDevice();
|
|
14118
|
+
}
|
|
13946
14119
|
const enrolled = await enroll(
|
|
13947
14120
|
endpoints,
|
|
13948
14121
|
credentials,
|
|
@@ -13963,12 +14136,29 @@ async function runManagedSession(root, endpoints, allowBroadRoot = false) {
|
|
|
13963
14136
|
console.error(
|
|
13964
14137
|
"Files may be modified and commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
|
|
13965
14138
|
);
|
|
14139
|
+
let connectionState;
|
|
13966
14140
|
try {
|
|
13967
14141
|
await new RemoteWorker({
|
|
13968
14142
|
origin: endpoints.workerOrigin,
|
|
13969
14143
|
deviceToken: device.token,
|
|
13970
14144
|
worker: visibleWorker(worker),
|
|
13971
|
-
signal: controller.signal
|
|
14145
|
+
signal: controller.signal,
|
|
14146
|
+
onStatus(status) {
|
|
14147
|
+
if (status.state === "connecting") {
|
|
14148
|
+
console.error("Connecting to Glossa...");
|
|
14149
|
+
} else if (status.state === "connected") {
|
|
14150
|
+
console.error(status.reconnected ? "Reconnected to Glossa." : "Connected to Glossa. ChatGPT can now use this workspace.");
|
|
14151
|
+
if (status.legacyRelay) {
|
|
14152
|
+
console.error("The relay needs an update before this computer can expose several workspaces at once.");
|
|
14153
|
+
}
|
|
14154
|
+
} else if (status.state === "retrying" && connectionState !== "retrying") {
|
|
14155
|
+
const prefix = connectionState === "connecting" ? "Could not connect" : "Connection lost";
|
|
14156
|
+
console.error(`${prefix}: ${status.error.message} Retrying automatically.`);
|
|
14157
|
+
} else if (status.state === "disconnected") {
|
|
14158
|
+
console.error("Disconnected from Glossa.");
|
|
14159
|
+
}
|
|
14160
|
+
connectionState = status.state;
|
|
14161
|
+
}
|
|
13972
14162
|
}).run();
|
|
13973
14163
|
} catch (error46) {
|
|
13974
14164
|
if (error46 instanceof DeviceRejectedError) {
|
|
@@ -14012,127 +14202,147 @@ async function selectExposureRoot(explicitPath, allowBroadRoot = false, cwd = pr
|
|
|
14012
14202
|
}
|
|
14013
14203
|
|
|
14014
14204
|
// src/main.ts
|
|
14015
|
-
var VERSION = "0.1.0-beta.
|
|
14016
|
-
|
|
14017
|
-
|
|
14205
|
+
var VERSION = "0.1.0-beta.6";
|
|
14206
|
+
var helpText = {
|
|
14207
|
+
main: `Glossa ${VERSION}
|
|
14018
14208
|
|
|
14019
14209
|
Usage:
|
|
14020
|
-
glossa
|
|
14210
|
+
glossa
|
|
14211
|
+
glossa [directory]
|
|
14212
|
+
glossa start [directory] [--allow-broad-root]
|
|
14213
|
+
glossa status [--json]
|
|
14214
|
+
glossa devices list [--json]
|
|
14215
|
+
glossa devices rename <id> <name>
|
|
14216
|
+
glossa devices revoke <id>
|
|
14021
14217
|
glossa login
|
|
14022
|
-
glossa logout
|
|
14023
|
-
glossa status
|
|
14024
|
-
glossa whoami
|
|
14025
|
-
glossa [path] --local [--allow-broad-root]
|
|
14218
|
+
glossa logout [--browser]
|
|
14026
14219
|
glossa --version
|
|
14027
14220
|
glossa --help
|
|
14028
14221
|
|
|
14029
|
-
|
|
14030
|
-
|
|
14222
|
+
Glossa signs in automatically and exposes each started workspace through the managed MCP relay.`,
|
|
14223
|
+
start: `Usage: glossa start [directory] [--allow-broad-root]
|
|
14224
|
+
|
|
14225
|
+
Starts a foreground worker. Inside Git, the default directory is the worktree root.
|
|
14226
|
+
Outside Git, provide a directory. Press Ctrl+C to disconnect.`,
|
|
14227
|
+
status: `Usage: glossa status [--json]
|
|
14228
|
+
|
|
14229
|
+
Validates Google login, contacts the relay, and reports enrolled devices and active workers.`,
|
|
14230
|
+
devices: `Usage:
|
|
14231
|
+
glossa devices list [--json]
|
|
14232
|
+
glossa devices rename <id> <name>
|
|
14233
|
+
glossa devices revoke <id>
|
|
14234
|
+
|
|
14235
|
+
Lists, renames, or revokes computers enrolled with the current Google account.`,
|
|
14236
|
+
login: `Usage: glossa login
|
|
14237
|
+
|
|
14238
|
+
Ensures the CLI has a valid Google session. Starting Glossa also signs in automatically.`,
|
|
14239
|
+
logout: `Usage: glossa logout [--browser]
|
|
14240
|
+
|
|
14241
|
+
Removes local OAuth credentials. --browser also opens the browser-session logout used when switching Google accounts. Running workers remain connected until stopped or revoked.`
|
|
14242
|
+
};
|
|
14243
|
+
async function withLoginSignal(action) {
|
|
14244
|
+
const controller = new AbortController();
|
|
14245
|
+
const cancel = () => controller.abort();
|
|
14246
|
+
process.once("SIGINT", cancel);
|
|
14247
|
+
try {
|
|
14248
|
+
return await action(controller.signal);
|
|
14249
|
+
} finally {
|
|
14250
|
+
process.removeListener("SIGINT", cancel);
|
|
14251
|
+
}
|
|
14031
14252
|
}
|
|
14032
|
-
function
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14253
|
+
async function authenticatedCredentials() {
|
|
14254
|
+
const loginPerformed = await withLoginSignal(async (signal) => {
|
|
14255
|
+
return await ensureSignedIn({ ...loadAuthConfig(), signal });
|
|
14256
|
+
});
|
|
14257
|
+
const loaded = await loadCredentials();
|
|
14258
|
+
if (!loaded) throw new Error("Glossa could not load the completed login.");
|
|
14259
|
+
return {
|
|
14260
|
+
credentials: await validCredentials(loaded.credentials),
|
|
14261
|
+
loginPerformed
|
|
14262
|
+
};
|
|
14263
|
+
}
|
|
14264
|
+
async function runExposure(path7, allowBroadRoot) {
|
|
14265
|
+
const root = await selectExposureRoot(path7, allowBroadRoot);
|
|
14266
|
+
await authenticatedCredentials();
|
|
14267
|
+
await runManagedSession(root, loadRelayEndpoints(), allowBroadRoot);
|
|
14268
|
+
}
|
|
14269
|
+
function deviceStatus(device) {
|
|
14270
|
+
if (device.revokedAt) return "revoked";
|
|
14271
|
+
if (device.activeWorkers === null) return "worker count unavailable";
|
|
14272
|
+
if (device.activeWorkers === 0) return "offline";
|
|
14273
|
+
return `${device.activeWorkers} active ${device.activeWorkers === 1 ? "worker" : "workers"}`;
|
|
14274
|
+
}
|
|
14275
|
+
async function showStatus(json2) {
|
|
14276
|
+
const { credentials: initial } = await authenticatedCredentials();
|
|
14277
|
+
const { credentials, profile } = await loadUserProfile(initial);
|
|
14278
|
+
const endpoints = loadRelayEndpoints();
|
|
14279
|
+
const devices = await listDevices(endpoints, credentials);
|
|
14280
|
+
const account = profile.email ?? profile.name ?? profile.sub;
|
|
14281
|
+
const workerCountsCurrent = devices.every((device) => device.activeWorkers !== null);
|
|
14282
|
+
const activeWorkers = workerCountsCurrent ? devices.reduce((sum, device) => sum + device.activeWorkers, 0) : null;
|
|
14283
|
+
const result = {
|
|
14284
|
+
account,
|
|
14285
|
+
relay: endpoints.relayOrigin,
|
|
14286
|
+
connected: true,
|
|
14287
|
+
activeWorkers,
|
|
14288
|
+
devices
|
|
14289
|
+
};
|
|
14290
|
+
if (json2) {
|
|
14291
|
+
console.log(JSON.stringify(result, null, 2));
|
|
14292
|
+
return;
|
|
14042
14293
|
}
|
|
14294
|
+
console.log(`Signed in as ${account}.`);
|
|
14295
|
+
console.log(`Relay connected: ${endpoints.relayOrigin}`);
|
|
14296
|
+
console.log(
|
|
14297
|
+
activeWorkers === null ? "Active workers: unavailable until the relay is updated" : `Active workers: ${activeWorkers}`
|
|
14298
|
+
);
|
|
14299
|
+
if (devices.length === 0) {
|
|
14300
|
+
console.log("No devices enrolled. Run glossa start in a workspace.");
|
|
14301
|
+
return;
|
|
14302
|
+
}
|
|
14303
|
+
for (const device of devices) {
|
|
14304
|
+
console.log(`${device.id} ${device.name} ${deviceStatus(device)}`);
|
|
14305
|
+
}
|
|
14306
|
+
}
|
|
14307
|
+
async function deviceCredentials() {
|
|
14043
14308
|
return {
|
|
14044
|
-
|
|
14045
|
-
|
|
14046
|
-
allowBroadRoot
|
|
14309
|
+
credentials: (await authenticatedCredentials()).credentials,
|
|
14310
|
+
endpoints: loadRelayEndpoints()
|
|
14047
14311
|
};
|
|
14048
14312
|
}
|
|
14049
14313
|
async function main() {
|
|
14050
|
-
const
|
|
14051
|
-
|
|
14052
|
-
|
|
14053
|
-
|
|
14054
|
-
|
|
14055
|
-
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
const cancel = () => controller.abort();
|
|
14079
|
-
process.once("SIGINT", cancel);
|
|
14080
|
-
try {
|
|
14081
|
-
await loginWithDeviceFlow({
|
|
14082
|
-
...authConfig,
|
|
14083
|
-
signal: controller.signal
|
|
14084
|
-
});
|
|
14085
|
-
} finally {
|
|
14086
|
-
process.removeListener("SIGINT", cancel);
|
|
14087
|
-
}
|
|
14088
|
-
}
|
|
14089
|
-
return;
|
|
14090
|
-
case "logout":
|
|
14091
|
-
await deleteCredentials();
|
|
14092
|
-
console.log("Signed out of Glossa.");
|
|
14093
|
-
return;
|
|
14094
|
-
case "status": {
|
|
14095
|
-
const loaded = await loadCredentials();
|
|
14096
|
-
if (!loaded) {
|
|
14097
|
-
console.log("Not signed in. Run: glossa login");
|
|
14098
|
-
process.exitCode = 1;
|
|
14099
|
-
return;
|
|
14100
|
-
}
|
|
14101
|
-
console.log(
|
|
14102
|
-
`Signed in with ${loaded.backend} credentials; access token expires ${loaded.credentials.expiresAt}.`
|
|
14103
|
-
);
|
|
14104
|
-
return;
|
|
14105
|
-
}
|
|
14106
|
-
case "whoami": {
|
|
14107
|
-
const loaded = await loadCredentials();
|
|
14108
|
-
if (!loaded) {
|
|
14109
|
-
console.log("Not signed in. Run: glossa login");
|
|
14110
|
-
process.exitCode = 1;
|
|
14111
|
-
return;
|
|
14112
|
-
}
|
|
14113
|
-
const { credentials, profile } = await loadUserProfile(loaded.credentials);
|
|
14114
|
-
const account = profile.email ?? profile.name ?? profile.sub;
|
|
14115
|
-
console.log(
|
|
14116
|
-
`Signed in as ${account} (${profile.sub}); access token expires ${credentials.expiresAt}.`
|
|
14117
|
-
);
|
|
14118
|
-
return;
|
|
14119
|
-
}
|
|
14120
|
-
default: {
|
|
14121
|
-
const options = exposeOptions(args);
|
|
14122
|
-
const root = await selectExposureRoot(options.path, options.allowBroadRoot);
|
|
14123
|
-
if (options.local) await runLocalSession(root, options.allowBroadRoot);
|
|
14124
|
-
else
|
|
14125
|
-
await runManagedSession(
|
|
14126
|
-
root,
|
|
14127
|
-
loadRelayEndpoints(),
|
|
14128
|
-
options.allowBroadRoot
|
|
14129
|
-
);
|
|
14130
|
-
return;
|
|
14131
|
-
}
|
|
14314
|
+
const invocation = parseInvocation(process.argv.slice(2));
|
|
14315
|
+
if (invocation.command === "help") {
|
|
14316
|
+
console.log(helpText[invocation.topic ?? "main"]);
|
|
14317
|
+
} else if (invocation.command === "version") {
|
|
14318
|
+
console.log(VERSION);
|
|
14319
|
+
} else if (invocation.command === "start") {
|
|
14320
|
+
await runExposure(invocation.path, invocation.allowBroadRoot);
|
|
14321
|
+
} else if (invocation.command === "status") {
|
|
14322
|
+
await showStatus(invocation.json);
|
|
14323
|
+
} else if (invocation.command === "login") {
|
|
14324
|
+
const { loginPerformed } = await authenticatedCredentials();
|
|
14325
|
+
if (!loginPerformed) console.log("Signed in to Glossa.");
|
|
14326
|
+
} else if (invocation.command === "logout") {
|
|
14327
|
+
await logoutFromGlossa({ browser: invocation.browser });
|
|
14328
|
+
} else if (invocation.action === "list") {
|
|
14329
|
+
const { endpoints, credentials } = await deviceCredentials();
|
|
14330
|
+
const devices = await listDevices(endpoints, credentials);
|
|
14331
|
+
if (invocation.json) console.log(JSON.stringify({ devices }, null, 2));
|
|
14332
|
+
else if (devices.length === 0) console.log("No devices enrolled.");
|
|
14333
|
+
else for (const device of devices) console.log(`${device.id} ${device.name} ${deviceStatus(device)}`);
|
|
14334
|
+
} else if (invocation.action === "rename") {
|
|
14335
|
+
const { endpoints, credentials } = await deviceCredentials();
|
|
14336
|
+
const device = await renameDevice(endpoints, credentials, invocation.deviceId, invocation.name);
|
|
14337
|
+
console.log(`Renamed device ${device.id} to ${device.name}.`);
|
|
14338
|
+
} else {
|
|
14339
|
+
const { endpoints, credentials } = await deviceCredentials();
|
|
14340
|
+
await revokeDevice(endpoints, credentials, invocation.deviceId);
|
|
14341
|
+
console.log(`Revoked device ${invocation.deviceId}. Running workers on it are disconnected.`);
|
|
14132
14342
|
}
|
|
14133
14343
|
}
|
|
14134
14344
|
main().catch((error46) => {
|
|
14135
14345
|
console.error(error46 instanceof Error ? error46.message : String(error46));
|
|
14346
|
+
if (error46 instanceof UsageError) console.error("Run glossa --help for usage.");
|
|
14136
14347
|
process.exitCode = 1;
|
|
14137
14348
|
});
|
|
14138
|
-
//# sourceMappingURL=main.js.map
|