@ariobarin/glossa 0.2.0 → 0.2.1

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/app.js +90 -112
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -28,7 +28,7 @@ Expose only a narrow project you trust. Keep credentials and regulated or sensit
28
28
 
29
29
  Pass a directory to expose another project, and add `--label <name>` when several online workspaces need a non-sensitive identifier. The terminal shows the selected project, access profile, connection state, and recent activity. Press `q` or Ctrl+C to disconnect.
30
30
 
31
- The first time a computer runs Glossa, the CLI prints a short pairing code. Approve that code from the Glossa app in an already authenticated ChatGPT conversation; the computer receives only its own revocable device credential, so a headless or SSH-only machine does not need a browser or your Google/Auth0 refresh token. Later workspace sessions reuse that pairing without user sign-in.
31
+ The first time a computer runs Glossa, follow its browser authorization link. A headless or SSH-only machine prints the link so you can open it elsewhere. The CLI uses that authorization once to enroll the computer and stores only its revocable device credential, not a Google or Auth0 refresh token. Later workspace sessions reuse that pairing without user sign-in.
32
32
 
33
33
  Account-level CLI actions such as `glossa status` still open sign-in when needed. Run `glossa unpair` to revoke this computer and remove its local pairing before moving it to another Glossa account.
34
34
 
package/dist/app.js CHANGED
@@ -27147,11 +27147,10 @@ async function postForm(fetchRequest, url2, values, signal) {
27147
27147
  }
27148
27148
  return data;
27149
27149
  }
27150
- async function loginWithDeviceFlow(options, dependencies = {}) {
27150
+ async function authorizeWithDeviceFlow(options, dependencies = {}) {
27151
27151
  const fetchRequest = dependencies.fetch ?? fetch;
27152
27152
  const wait = dependencies.delay ?? defaultDelay;
27153
27153
  const browse = dependencies.openBrowser ?? openBrowser;
27154
- const save = dependencies.saveCredentials ?? saveCredentials;
27155
27154
  const now = dependencies.now ?? Date.now;
27156
27155
  const log = dependencies.log ?? console.log;
27157
27156
  try {
@@ -27189,26 +27188,25 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
27189
27188
  });
27190
27189
  const data = await response.json();
27191
27190
  if (response.ok && "access_token" in data) {
27192
- if (!data.refresh_token) {
27193
- throw new Error("Auth0 did not issue a refresh token.");
27194
- }
27195
27191
  const grantedScope = data.scope ?? options.scope;
27196
- if (!grantedScopesSatisfyRequest(grantedScope, options.scope, true)) {
27192
+ if (!grantedScopesSatisfyRequest(
27193
+ grantedScope,
27194
+ options.scope,
27195
+ Boolean(data.refresh_token)
27196
+ )) {
27197
27197
  throw new Error("Auth0 did not grant the permissions Glossa requires.");
27198
27198
  }
27199
- await save({
27199
+ return {
27200
27200
  issuer: options.issuer,
27201
27201
  clientId: options.clientId,
27202
27202
  audience: options.audience,
27203
27203
  accessToken: data.access_token,
27204
- refreshToken: data.refresh_token,
27204
+ ...data.refresh_token ? { refreshToken: data.refresh_token } : {},
27205
27205
  expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
27206
27206
  tokenType: data.token_type,
27207
27207
  scope: grantedScope,
27208
27208
  requestedScope: options.scope
27209
- });
27210
- log("Signed in to Glossa.");
27211
- return;
27209
+ };
27212
27210
  }
27213
27211
  const error46 = data;
27214
27212
  if (error46.error === "authorization_pending") continue;
@@ -27228,6 +27226,15 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
27228
27226
  throw error46;
27229
27227
  }
27230
27228
  }
27229
+ async function loginWithDeviceFlow(options, dependencies = {}) {
27230
+ const credentials = await authorizeWithDeviceFlow(options, dependencies);
27231
+ if (!credentials.refreshToken) {
27232
+ throw new Error("Auth0 did not issue a refresh token.");
27233
+ }
27234
+ const save = dependencies.saveCredentials ?? saveCredentials;
27235
+ await save(credentials);
27236
+ (dependencies.log ?? console.log)("Signed in to Glossa.");
27237
+ }
27231
27238
 
27232
27239
  // src/auth-login.ts
27233
27240
  function normalizedIssuer(value) {
@@ -40370,12 +40377,6 @@ function loadRelayEndpoints(environment = process.env) {
40370
40377
  function defaultDeviceName() {
40371
40378
  return deviceNameSchema.parse(os2.hostname());
40372
40379
  }
40373
- var DevicePairingExpiredError = class extends Error {
40374
- constructor() {
40375
- super("The Glossa pairing code expired. Run Glossa again to get a new code.");
40376
- this.name = "DevicePairingExpiredError";
40377
- }
40378
- };
40379
40380
  function relayError(status, data) {
40380
40381
  if (status === 401) return new Error("Glossa login was rejected. Sign in again when prompted.");
40381
40382
  if (status === 403 && data.error === "account_disabled") {
@@ -40429,68 +40430,49 @@ async function listDevices(endpoints, credentials, fetchRequest = fetch) {
40429
40430
  if (!response.ok) throw relayError(response.status, data);
40430
40431
  return parseDevices2(data.devices);
40431
40432
  }
40432
- async function beginDevicePairing(endpoints, deviceName, fetchRequest = fetch) {
40433
- const name = deviceNameSchema.parse(deviceName);
40434
- const response = await fetchRequest(`${endpoints.relayOrigin}/v1/device-pairings`, {
40435
- method: "POST",
40436
- headers: { "content-type": "application/json" },
40437
- body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
40438
- });
40439
- let data = {};
40440
- try {
40441
- data = await response.json();
40442
- } catch {
40443
- }
40444
- if (!response.ok) {
40445
- if (response.status === 429) {
40446
- throw new Error("Glossa pairing is rate limited. Try again later.");
40447
- }
40448
- throw new Error(`The Glossa relay returned HTTP ${response.status}.`);
40449
- }
40450
- if (typeof data.pairing_id !== "string" || typeof data.user_code !== "string" || typeof data.pairing_secret !== "string" || typeof data.expires_at !== "string" || !Number.isInteger(data.poll_interval_ms) || data.poll_interval_ms < 1e3 || data.poll_interval_ms > 1e4 || !/^[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(data.pairing_id) || !/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/.test(data.user_code) || data.pairing_secret.length < 32 || data.pairing_secret.length > 128 || !Number.isFinite(Date.parse(data.expires_at))) {
40451
- throw new Error("The Glossa relay returned an invalid pairing response.");
40452
- }
40453
- return {
40454
- pairingId: data.pairing_id,
40455
- userCode: data.user_code,
40456
- pairingSecret: data.pairing_secret,
40457
- expiresAt: data.expires_at,
40458
- pollIntervalMs: data.poll_interval_ms
40459
- };
40460
- }
40461
- async function completeDevicePairing(endpoints, pairing, fetchRequest = fetch) {
40462
- const response = await fetchRequest(
40463
- `${endpoints.relayOrigin}/v1/device-pairings/${encodeURIComponent(pairing.pairingId)}/complete`,
40464
- {
40433
+ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
40434
+ const baseName = deviceNameSchema.parse(deviceName);
40435
+ let name = baseName;
40436
+ for (let attempt = 0; attempt < 3; attempt += 1) {
40437
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/enroll`, {
40465
40438
  method: "POST",
40466
- headers: { "content-type": "application/json" },
40467
- body: JSON.stringify({ pairing_secret: pairing.pairingSecret })
40439
+ headers: {
40440
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`,
40441
+ "content-type": "application/json"
40442
+ },
40443
+ body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
40444
+ });
40445
+ let data = {};
40446
+ try {
40447
+ data = await response.json();
40448
+ } catch {
40468
40449
  }
40469
- );
40470
- let data = {};
40471
- try {
40472
- data = await response.json();
40473
- } catch {
40474
- }
40475
- if (response.status === 202 && data.status === "authorization_pending") {
40476
- return null;
40477
- }
40478
- if (response.status === 410 && data.error === "pairing_expired" || response.status === 404 && data.error === "pairing_not_found") {
40479
- throw new DevicePairingExpiredError();
40480
- }
40481
- if (response.status === 429) {
40482
- throw new Error("Glossa pairing is rate limited. Try again later.");
40483
- }
40484
- if (!response.ok) throw relayError(response.status, data);
40485
- if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
40486
- throw new Error("The Glossa relay returned an invalid device pairing response.");
40450
+ if (response.status === 409 && data.error === "device_name_conflict" && attempt < 2) {
40451
+ const activeNames = new Set(
40452
+ (await listDevices(endpoints, credentials, fetchRequest)).filter((device) => device.revokedAt === null).map((device) => device.name)
40453
+ );
40454
+ for (let suffix = 2; ; suffix += 1) {
40455
+ const ending = `-${suffix}`;
40456
+ const candidate = `${baseName.slice(0, 80 - ending.length)}${ending}`;
40457
+ if (!activeNames.has(candidate)) {
40458
+ name = deviceNameSchema.parse(candidate);
40459
+ break;
40460
+ }
40461
+ }
40462
+ continue;
40463
+ }
40464
+ if (!response.ok) throw relayError(response.status, data);
40465
+ if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
40466
+ throw new Error("The Glossa relay returned an invalid device enrollment response.");
40467
+ }
40468
+ return {
40469
+ relayOrigin: endpoints.relayOrigin,
40470
+ deviceId: data.device.id,
40471
+ deviceName: data.device.name,
40472
+ token: data.device_token
40473
+ };
40487
40474
  }
40488
- return {
40489
- relayOrigin: endpoints.relayOrigin,
40490
- deviceId: data.device.id,
40491
- deviceName: data.device.name,
40492
- token: data.device_token
40493
- };
40475
+ throw new Error("Glossa could not enroll this computer.");
40494
40476
  }
40495
40477
  async function revokePairedDevice(endpoints, device, fetchRequest = fetch) {
40496
40478
  const response = await fetchRequest(`${endpoints.relayOrigin}/device`, {
@@ -47864,7 +47846,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47864
47846
  for (const subscriber of animationSubscribersRef.current.values()) {
47865
47847
  nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
47866
47848
  }
47867
- const delay6 = Math.max(0, nextDueTime - performance.now());
47849
+ const delay5 = Math.max(0, nextDueTime - performance.now());
47868
47850
  animationTimerRef.current = setTimeout(() => {
47869
47851
  animationTimerRef.current = void 0;
47870
47852
  const currentTime = performance.now();
@@ -47878,7 +47860,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47878
47860
  subscriber.nextDueTime = subscriber.startTime + elapsedFrames * subscriber.interval;
47879
47861
  }
47880
47862
  scheduleAnimationTick();
47881
- }, delay6);
47863
+ }, delay5);
47882
47864
  }, [clearAnimationTimer]);
47883
47865
  const animationSubscribe = (0, import_react15.useCallback)((callback, interval) => {
47884
47866
  const startTime = performance.now();
@@ -50109,40 +50091,36 @@ async function withDevicePairingLease(action, signal, directory = updateRuntimeD
50109
50091
  }
50110
50092
 
50111
50093
  // src/device-pairing.ts
50112
- import { setTimeout as delay3 } from "node:timers/promises";
50113
- async function defaultWait(milliseconds, signal) {
50114
- await delay3(milliseconds, void 0, signal ? { signal } : void 0);
50115
- }
50116
50094
  function canceledError2() {
50117
50095
  return new Error("Pairing canceled.");
50118
50096
  }
50097
+ function pairingScope(scope) {
50098
+ return scope.split(/\s+/).filter((value) => value && value !== "offline_access").join(" ");
50099
+ }
50119
50100
  async function pairDevice(endpoints, signal, dependencies = {}) {
50120
- const begin = dependencies.beginDevicePairing ?? beginDevicePairing;
50121
- const complete = dependencies.completeDevicePairing ?? completeDevicePairing;
50101
+ const authorize = dependencies.authorizeWithDeviceFlow ?? authorizeWithDeviceFlow;
50102
+ const enroll = dependencies.enrollDevice ?? enrollDevice;
50103
+ const authConfig = dependencies.loadAuthConfig ?? loadAuthConfig;
50122
50104
  const name = dependencies.defaultDeviceName ?? defaultDeviceName;
50123
- const wait = dependencies.wait ?? defaultWait;
50124
- const now = dependencies.now ?? Date.now;
50125
50105
  const log = dependencies.log ?? console.log;
50126
50106
  const baseFetch = dependencies.fetch ?? fetch;
50127
50107
  const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
50128
50108
  try {
50129
50109
  signal?.throwIfAborted();
50130
- const challenge = await begin(endpoints, name(), fetchRequest);
50131
50110
  log("This computer needs to be paired with Glossa.");
50132
- log("In ChatGPT, send this message:");
50133
- log(`@Glossa pair ${challenge.userCode}`);
50134
- log("Keep this terminal open. The pairing code expires in 5 minutes.");
50135
- const expiresAt = Date.parse(challenge.expiresAt);
50136
- while (now() < expiresAt) {
50137
- signal?.throwIfAborted();
50138
- await wait(challenge.pollIntervalMs, signal);
50139
- signal?.throwIfAborted();
50140
- const paired = await complete(endpoints, challenge, fetchRequest);
50141
- if (!paired) continue;
50142
- log(`Paired with Glossa as ${paired.deviceName}.`);
50143
- return paired;
50144
- }
50145
- throw new Error("The Glossa pairing code expired. Run Glossa again to get a new code.");
50111
+ const auth = authConfig();
50112
+ const credentials = await authorize(
50113
+ {
50114
+ ...auth,
50115
+ scope: pairingScope(auth.scope),
50116
+ ...signal ? { signal } : {}
50117
+ },
50118
+ { fetch: fetchRequest, log }
50119
+ );
50120
+ signal?.throwIfAborted();
50121
+ const paired = await enroll(endpoints, credentials, name(), fetchRequest);
50122
+ log(`Paired with Glossa as ${paired.deviceName}.`);
50123
+ return paired;
50146
50124
  } catch (error46) {
50147
50125
  if (signal?.aborted || error46 instanceof Error && error46.name === "AbortError") {
50148
50126
  throw canceledError2();
@@ -50155,7 +50133,7 @@ async function pairDevice(endpoints, signal, dependencies = {}) {
50155
50133
  import { spawn as spawn2 } from "node:child_process";
50156
50134
  import { randomUUID as randomUUID3 } from "node:crypto";
50157
50135
  import { StringDecoder } from "node:string_decoder";
50158
- import { setTimeout as delay4 } from "node:timers/promises";
50136
+ import { setTimeout as delay3 } from "node:timers/promises";
50159
50137
 
50160
50138
  // src/worker/errors.ts
50161
50139
  var WorkerError = class extends Error {
@@ -50270,7 +50248,7 @@ async function waitForChange(record2, afterSequence, waitMs) {
50270
50248
  try {
50271
50249
  await Promise.race([
50272
50250
  change,
50273
- delay4(waitMs, void 0, { signal: waitController.signal })
50251
+ delay3(waitMs, void 0, { signal: waitController.signal })
50274
50252
  ]);
50275
50253
  } finally {
50276
50254
  record2.changeWaiters.delete(changed);
@@ -50441,7 +50419,7 @@ async function terminateProcessTree(child) {
50441
50419
  } catch {
50442
50420
  child.kill("SIGTERM");
50443
50421
  }
50444
- await delay4(2e3);
50422
+ await delay3(2e3);
50445
50423
  if (child.exitCode === null) {
50446
50424
  try {
50447
50425
  process.kill(-child.pid, "SIGKILL");
@@ -50606,7 +50584,7 @@ var CommandService = class {
50606
50584
  try {
50607
50585
  await Promise.race([
50608
50586
  record2.completion,
50609
- delay4(waitMs, void 0, { signal: waitController.signal })
50587
+ delay3(waitMs, void 0, { signal: waitController.signal })
50610
50588
  ]);
50611
50589
  } finally {
50612
50590
  waitController.abort();
@@ -50632,7 +50610,7 @@ var CommandService = class {
50632
50610
  try {
50633
50611
  await Promise.race([
50634
50612
  record2.completion,
50635
- delay4(waitMs, void 0, { signal: waitController.signal })
50613
+ delay3(waitMs, void 0, { signal: waitController.signal })
50636
50614
  ]);
50637
50615
  } finally {
50638
50616
  waitController.abort();
@@ -52485,7 +52463,7 @@ var RemoteWorker = class {
52485
52463
  if (error46 instanceof DeviceRejectedError || error46 instanceof RelayAccessProfileUnsupportedError) {
52486
52464
  throw error46;
52487
52465
  }
52488
- const delay6 = reconnectDelayMs(
52466
+ const delay5 = reconnectDelayMs(
52489
52467
  failures,
52490
52468
  this.#random,
52491
52469
  this.#reconnectBaseMs,
@@ -52495,10 +52473,10 @@ var RemoteWorker = class {
52495
52473
  this.#onStatus({
52496
52474
  state: "retrying",
52497
52475
  error: error46 instanceof Error ? error46 : new Error(String(error46)),
52498
- retryInMs: delay6
52476
+ retryInMs: delay5
52499
52477
  });
52500
52478
  try {
52501
- await this.#sleep(delay6, this.#signal);
52479
+ await this.#sleep(delay5, this.#signal);
52502
52480
  } catch (sleepError) {
52503
52481
  if (this.#signal.aborted) return;
52504
52482
  throw sleepError;
@@ -54750,7 +54728,7 @@ import { createHash as createHash3 } from "node:crypto";
54750
54728
  import { lstat as lstat3, mkdir as mkdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
54751
54729
  import net from "node:net";
54752
54730
  import path13 from "node:path";
54753
- import { setTimeout as delay5 } from "node:timers/promises";
54731
+ import { setTimeout as delay4 } from "node:timers/promises";
54754
54732
  var GUARD_STALE_MS = 5e3;
54755
54733
  var GUARD_RETRY_MS = 10;
54756
54734
  var GUARD_TIMEOUT_MS = 1e4;
@@ -54822,7 +54800,7 @@ async function acquireGuard2(directory, identity) {
54822
54800
  "Timed out while checking whether this workspace is already active."
54823
54801
  );
54824
54802
  }
54825
- await delay5(GUARD_RETRY_MS);
54803
+ await delay4(GUARD_RETRY_MS);
54826
54804
  }
54827
54805
  }
54828
54806
  }
@@ -54911,7 +54889,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54911
54889
  }
54912
54890
 
54913
54891
  // src/main.ts
54914
- var VERSION = "0.2.0";
54892
+ var VERSION = "0.2.1";
54915
54893
  var DISTRIBUTION = "npm";
54916
54894
  var HELP = `Glossa ${VERSION}
54917
54895
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariobarin/glossa",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "private": false,
5
5
  "description": "Connect an explicitly exposed local coding workspace to the Glossa MCP relay.",
6
6
  "type": "module",