@astrale-os/cli 1.0.0-beta.28 → 1.0.0-beta.29

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 (50) hide show
  1. package/README.md +33 -0
  2. package/dist/astrale.js +1629 -1088
  3. package/dist/public/connect-core.js +86 -17
  4. package/dist/public/keys/index.js +69 -2
  5. package/dist/public/paths/index.js +67 -0
  6. package/dist/types/connection/session.d.ts +2 -2
  7. package/dist/types/lib/config.d.ts +6 -0
  8. package/dist/types/state/exchange-credentials.d.ts +6 -2
  9. package/dist/types/state/files.d.ts +2 -0
  10. package/dist/types/state/index.d.ts +3 -2
  11. package/dist/types/state/paths.d.ts +2 -0
  12. package/dist/types/state/session-routes.d.ts +10 -0
  13. package/package.json +2 -2
  14. package/src/commands/auth/logout.ts +2 -1
  15. package/src/commands/browser.ts +29 -0
  16. package/src/connection/.spec/architecture.md +9 -1
  17. package/src/connection/__tests__/auth.test.ts +1 -0
  18. package/src/connection/__tests__/credential.test.ts +1 -0
  19. package/src/connection/__tests__/exchange.test.ts +34 -10
  20. package/src/connection/__tests__/session.test.ts +5 -1
  21. package/src/connection/__tests__/target.test.ts +1 -0
  22. package/src/connection/exchange.ts +63 -38
  23. package/src/connection/session.ts +8 -1
  24. package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
  25. package/src/identity/__tests__/registry.test.ts +4 -0
  26. package/src/identity/registry.ts +2 -0
  27. package/src/lib/__tests__/browser-retention.test.ts +212 -0
  28. package/src/lib/__tests__/config.test.ts +27 -0
  29. package/src/lib/browser-retention.ts +210 -0
  30. package/src/lib/config.ts +12 -1
  31. package/src/state/.spec/api.d.ts +21 -2
  32. package/src/state/.spec/architecture.md +18 -4
  33. package/src/state/.spec/layout.ts +1 -0
  34. package/src/state/__tests__/exchange-credentials.test.ts +88 -42
  35. package/src/state/__tests__/files.test.ts +24 -1
  36. package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
  37. package/src/state/__tests__/paths.test.ts +1 -0
  38. package/src/state/__tests__/session-routes.test.ts +138 -0
  39. package/src/state/exchange-credentials.ts +22 -9
  40. package/src/state/files.ts +41 -0
  41. package/src/state/index.ts +3 -1
  42. package/src/state/paths.ts +3 -0
  43. package/src/state/session-routes.ts +34 -0
  44. package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
  45. package/src/telemetry/__tests__/retention.test.ts +88 -1
  46. package/src/telemetry/analyze.ts +24 -2
  47. package/src/telemetry/retention.ts +53 -6
  48. package/src/telemetry/store.ts +5 -0
  49. package/studio/package.json +1 -1
  50. package/viewer/dist/main.js +28 -28
@@ -8092,7 +8092,7 @@ async function generateKeyPair(alg, options) {
8092
8092
  // src/keys/pair.ts
8093
8093
  import { randomUUID as randomUUID2 } from "node:crypto";
8094
8094
  import { access, mkdir as mkdir2, readFile as readFile3, unlink as unlink2 } from "node:fs/promises";
8095
- import { dirname as dirname2, join as join2, resolve } from "node:path";
8095
+ import { dirname as dirname3, join as join2, resolve } from "node:path";
8096
8096
 
8097
8097
  // src/errors.ts
8098
8098
  class AstraleError extends Error {
@@ -8126,6 +8126,15 @@ class IdentityKeyMissingError extends AstraleError {
8126
8126
 
8127
8127
  // src/state/files.ts
8128
8128
  import { randomUUID } from "node:crypto";
8129
+ import {
8130
+ closeSync,
8131
+ fsyncSync,
8132
+ mkdirSync,
8133
+ openSync,
8134
+ renameSync,
8135
+ unlinkSync,
8136
+ writeFileSync
8137
+ } from "node:fs";
8129
8138
  import { mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
8130
8139
  import { dirname } from "node:path";
8131
8140
  async function atomicWrite(path, data) {
@@ -8156,6 +8165,33 @@ async function atomicWrite(path, data) {
8156
8165
  throw error;
8157
8166
  }
8158
8167
  }
8168
+ function atomicWriteSync(path, data) {
8169
+ const directory = dirname(path);
8170
+ const temporary = `${path}.${randomUUID()}.tmp`;
8171
+ mkdirSync(directory, { recursive: true });
8172
+ let descriptor;
8173
+ try {
8174
+ descriptor = openSync(temporary, "wx", 384);
8175
+ writeFileSync(descriptor, data);
8176
+ fsyncSync(descriptor);
8177
+ closeSync(descriptor);
8178
+ descriptor = undefined;
8179
+ renameSync(temporary, path);
8180
+ const directoryDescriptor = openSync(directory, "r");
8181
+ try {
8182
+ fsyncSync(directoryDescriptor);
8183
+ } finally {
8184
+ closeSync(directoryDescriptor);
8185
+ }
8186
+ } catch (error) {
8187
+ if (descriptor !== undefined)
8188
+ closeSync(descriptor);
8189
+ try {
8190
+ unlinkSync(temporary);
8191
+ } catch {}
8192
+ throw error;
8193
+ }
8194
+ }
8159
8195
  async function withFileLock(lockPath, transition, options = {}) {
8160
8196
  const pollIntervalMs = options.pollIntervalMs ?? 100;
8161
8197
  const staleAfterMs = options.staleAfterMs ?? 30000;
@@ -8242,14 +8278,14 @@ function isPidAlive(pid) {
8242
8278
  function sleep(milliseconds) {
8243
8279
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
8244
8280
  }
8245
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8281
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.14/node_modules/@astrale-os/kernel-dsl/dist/value/limits.js
8246
8282
  var defaultLimits = Object.freeze({
8247
8283
  maxBytes: 16 * 1024 * 1024,
8248
8284
  maxDepth: 64,
8249
8285
  maxMembers: 1e5,
8250
8286
  maxStringBytes: 1024 * 1024
8251
8287
  });
8252
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8288
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.14/node_modules/@astrale-os/kernel-dsl/dist/value/canonical.js
8253
8289
  var encoder2 = new TextEncoder;
8254
8290
  // node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
8255
8291
  var CharacterCodes;
@@ -8429,7 +8465,7 @@ var ParseErrorCode;
8429
8465
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
8430
8466
  })(ParseErrorCode || (ParseErrorCode = {}));
8431
8467
 
8432
- // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.13/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8468
+ // node_modules/.pnpm/@astrale-os+kernel-dsl@0.2.0-beta.14/node_modules/@astrale-os/kernel-dsl/dist/value/decode.js
8433
8469
  var decoder2 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
8434
8470
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
8435
8471
  var exports_external = {};
@@ -22730,6 +22766,7 @@ function createPaths(home, environment) {
22730
22766
  idps: join(idpsDir, "index.json"),
22731
22767
  idpSessionsDir,
22732
22768
  exchangeCredentials: join(base, "exchange", "credentials.json"),
22769
+ sessionRoutes: join(base, "session", "routes.json"),
22733
22770
  idpDir: (name) => join(idpsDir, name),
22734
22771
  idpSession: (identityName) => join(idpSessionsDir, `${identityName}.json`)
22735
22772
  });
@@ -22745,6 +22782,7 @@ var INSTANCES_PATH = paths.instances;
22745
22782
  var IDPS_PATH = paths.idps;
22746
22783
  var IDP_SESSIONS_DIR = paths.idpSessionsDir;
22747
22784
  var EXCHANGE_CREDENTIALS_PATH = paths.exchangeCredentials;
22785
+ var SESSION_ROUTES_PATH = paths.sessionRoutes;
22748
22786
  // src/state/identities.ts
22749
22787
  import { readFile as readFile2 } from "node:fs/promises";
22750
22788
  var IDENTITY_STORE_VERSION = 1;
@@ -22866,6 +22904,35 @@ async function preserveLegacyBackup(path, legacyBytes) {
22866
22904
  function invalidState(path, cause) {
22867
22905
  return new IdentityStateError("IDENTITY_STATE_INVALID", path, `Identity state at ${path} is malformed`, cause);
22868
22906
  }
22907
+ // src/state/session-routes.ts
22908
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync, unlinkSync as unlinkSync2 } from "node:fs";
22909
+ import { dirname as dirname2 } from "node:path";
22910
+ class FileSessionRouteStore {
22911
+ path;
22912
+ constructor(path = SESSION_ROUTES_PATH) {
22913
+ this.path = path;
22914
+ }
22915
+ read() {
22916
+ return JSON.parse(readFileSync(this.path, "utf8"));
22917
+ }
22918
+ write(artifact) {
22919
+ const directory = dirname2(this.path);
22920
+ mkdirSync2(directory, { recursive: true, mode: 448 });
22921
+ chmodSync(directory, 448);
22922
+ atomicWriteSync(this.path, `${JSON.stringify(artifact)}
22923
+ `);
22924
+ chmodSync(this.path, 384);
22925
+ }
22926
+ clear() {
22927
+ try {
22928
+ unlinkSync2(this.path);
22929
+ } catch (error51) {
22930
+ if (error51.code !== "ENOENT")
22931
+ throw error51;
22932
+ }
22933
+ }
22934
+ }
22935
+ var SESSION_ROUTE_STORE = Object.freeze(new FileSessionRouteStore);
22869
22936
  // src/keys/algorithm.ts
22870
22937
  function inferAlg(privateJwk, keyPath) {
22871
22938
  const explicit = privateJwk.alg;
@@ -22903,7 +22970,7 @@ function keypairPaths(subject, keysDir = KEYS_DIR) {
22903
22970
  }
22904
22971
  function confinedKeyPath(keysDir, filename, subject) {
22905
22972
  const path = join2(keysDir, filename);
22906
- if (dirname2(resolve(path)) !== resolve(keysDir))
22973
+ if (dirname3(resolve(path)) !== resolve(keysDir))
22907
22974
  throw invalidKeySubject(subject);
22908
22975
  return path;
22909
22976
  }
@@ -23106,7 +23173,7 @@ async function signAs(subject, keysDir = KEYS_DIR, opts) {
23106
23173
  }
23107
23174
  // src/lib/idp.ts
23108
23175
  import { mkdir as mkdir3, readFile as readFile4, readdir, unlink as unlink3 } from "node:fs/promises";
23109
- import { dirname as dirname3, join as join3 } from "node:path";
23176
+ import { dirname as dirname4, join as join3 } from "node:path";
23110
23177
 
23111
23178
  // node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
23112
23179
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -23659,7 +23726,7 @@ var log = {
23659
23726
  dim: (msg) => console.log(source_default.dim(msg))
23660
23727
  };
23661
23728
  var IS_CI = !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.NO_SPINNER);
23662
- // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.16/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23729
+ // node_modules/.pnpm/@astrale-os+kernel-core@0.9.0-beta.17_zod@4.4.3/node_modules/@astrale-os/kernel-core/dist/dns-label.js
23663
23730
  var DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
23664
23731
  function isDnsLabel(value) {
23665
23732
  return DNS_LABEL_RE.test(value);
@@ -23830,7 +23897,7 @@ async function readIdpStore() {
23830
23897
  }
23831
23898
  }
23832
23899
  async function writeIdpStore(store) {
23833
- await mkdir3(dirname3(IDPS_PATH), { recursive: true });
23900
+ await mkdir3(dirname4(IDPS_PATH), { recursive: true });
23834
23901
  await atomicWrite(IDPS_PATH, JSON.stringify(store, null, 2) + `
23835
23902
  `);
23836
23903
  }
@@ -24040,7 +24107,7 @@ function identityNameFromClaims(claims, fallback) {
24040
24107
  }
24041
24108
  async function saveIdpSession(session) {
24042
24109
  const path = idpSessionPath(session.identity);
24043
- await mkdir3(dirname3(path), { recursive: true });
24110
+ await mkdir3(dirname4(path), { recursive: true });
24044
24111
  await atomicWrite(path, JSON.stringify(session, null, 2) + `
24045
24112
  `);
24046
24113
  }
@@ -24300,7 +24367,7 @@ async function upsertIdpIdentity(name, options) {
24300
24367
  }
24301
24368
  // src/lib/instance.ts
24302
24369
  import { readFile as readFile5, writeFile, mkdir as mkdir4 } from "node:fs/promises";
24303
- import { dirname as dirname4 } from "node:path";
24370
+ import { dirname as dirname5 } from "node:path";
24304
24371
  var InstanceKindSchema = exports_external.enum(["bookmark"]);
24305
24372
  var InstanceEntrySchema = exports_external.object({
24306
24373
  url: exports_external.string().optional(),
@@ -24385,7 +24452,7 @@ async function readInstances(_config, opts = {}) {
24385
24452
  return store;
24386
24453
  }
24387
24454
  async function writeInstances(store) {
24388
- await mkdir4(dirname4(INSTANCES_PATH), { recursive: true });
24455
+ await mkdir4(dirname5(INSTANCES_PATH), { recursive: true });
24389
24456
  await writeFile(INSTANCES_PATH, JSON.stringify(store, null, 2) + `
24390
24457
  `);
24391
24458
  instancesMemo = store;
@@ -24487,7 +24554,7 @@ import {
24487
24554
  rm,
24488
24555
  writeFile as writeFile2
24489
24556
  } from "node:fs/promises";
24490
- import { dirname as dirname5, join as join4 } from "node:path";
24557
+ import { dirname as dirname6, join as join4 } from "node:path";
24491
24558
  var DEFAULT_REPO = "astrale-os/cli";
24492
24559
  var DEFAULT_UPDATE_CHANNEL = "beta";
24493
24560
  var InstallMetadataSchema = exports_external.object({
@@ -24519,7 +24586,7 @@ function isMissingFile(error51) {
24519
24586
  async function writeInstallMetadata(meta3, path = INSTALL_PATH, filesystem = { mkdir: mkdir5, rename: rename2, rm, writeFile: writeFile2 }) {
24520
24587
  const staged = `${path}.next`;
24521
24588
  const previous = `${path}.previous`;
24522
- await filesystem.mkdir(dirname5(path), { recursive: true });
24589
+ await filesystem.mkdir(dirname6(path), { recursive: true });
24523
24590
  await filesystem.rm(staged, { force: true });
24524
24591
  await filesystem.rm(previous, { force: true });
24525
24592
  await filesystem.writeFile(staged, JSON.stringify(meta3, null, 2) + `
@@ -24550,7 +24617,7 @@ async function writeInstallMetadata(meta3, path = INSTALL_PATH, filesystem = { m
24550
24617
  var defaultCohortFilesystem = { chmod, copyFile, mkdir: mkdir5, rename: rename2, rm };
24551
24618
  async function replaceStandaloneCohort(installedBinary, nextBinary, nextViewerDist, filesystem = {}) {
24552
24619
  const fs = { ...defaultCohortFilesystem, ...filesystem };
24553
- const binDirectory = dirname5(installedBinary);
24620
+ const binDirectory = dirname6(installedBinary);
24554
24621
  const previousBinary = `${installedBinary}.previous`;
24555
24622
  const stagedBinary = `${installedBinary}.next`;
24556
24623
  const viewer = join4(binDirectory, "viewer");
@@ -24925,10 +24992,12 @@ function isManagedInstanceNotFound(error51) {
24925
24992
  }
24926
24993
  // src/lib/config.ts
24927
24994
  import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir6 } from "node:fs/promises";
24995
+ var bound = exports_external.number().positive().finite().optional().catch(undefined);
24928
24996
  var AstraleConfigSchema = exports_external.object({
24929
24997
  issuer: exports_external.string().url().default("https://unregistered.invalid"),
24930
24998
  admin: AdminTargetConfigSchema.default(DEFAULT_ADMIN_TARGET_CONFIG),
24931
- telemetry: exports_external.object({ enabled: exports_external.boolean().default(true) }).default({ enabled: true })
24999
+ telemetry: exports_external.object({ enabled: exports_external.boolean().default(true), maxAgeDays: bound, maxBytes: bound }).default({ enabled: true }),
25000
+ browser: exports_external.object({ maxCacheBytes: bound, maxProfileAgeDays: bound }).default({})
24932
25001
  });
24933
25002
  var DEFAULT_CONFIG = AstraleConfigSchema.parse({});
24934
25003
  async function readConfig() {
@@ -25263,11 +25332,11 @@ async function obtainToken(idp, opts, scope) {
25263
25332
  }
25264
25333
  // src/lib/ca-fetch.ts
25265
25334
  import { Buffer } from "node:buffer";
25266
- import { readFileSync } from "node:fs";
25335
+ import { readFileSync as readFileSync2 } from "node:fs";
25267
25336
  import { request as httpsRequest } from "node:https";
25268
25337
  import { rootCertificates } from "node:tls";
25269
25338
  function fetchWithCaFile(caFile, fallback = globalThis.fetch) {
25270
- const ca = readFileSync(caFile);
25339
+ const ca = readFileSync2(caFile);
25271
25340
  const fallbackFetch = fallback.bind(globalThis);
25272
25341
  return async (input, init) => {
25273
25342
  const url2 = requestUrl(input);
@@ -1151,7 +1151,7 @@ async function generateKeyPair(alg, options) {
1151
1151
  // src/keys/pair.ts
1152
1152
  import { randomUUID as randomUUID2 } from "node:crypto";
1153
1153
  import { access, mkdir as mkdir2, readFile as readFile3, unlink as unlink2 } from "node:fs/promises";
1154
- import { dirname as dirname2, join as join2, resolve } from "node:path";
1154
+ import { dirname as dirname3, join as join2, resolve } from "node:path";
1155
1155
 
1156
1156
  // src/errors.ts
1157
1157
  class AstraleError extends Error {
@@ -1185,6 +1185,15 @@ class IdentityKeyMissingError extends AstraleError {
1185
1185
 
1186
1186
  // src/state/files.ts
1187
1187
  import { randomUUID } from "node:crypto";
1188
+ import {
1189
+ closeSync,
1190
+ fsyncSync,
1191
+ mkdirSync,
1192
+ openSync,
1193
+ renameSync,
1194
+ unlinkSync,
1195
+ writeFileSync
1196
+ } from "node:fs";
1188
1197
  import { mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
1189
1198
  import { dirname } from "node:path";
1190
1199
  async function atomicWrite(path, data) {
@@ -1215,6 +1224,33 @@ async function atomicWrite(path, data) {
1215
1224
  throw error;
1216
1225
  }
1217
1226
  }
1227
+ function atomicWriteSync(path, data) {
1228
+ const directory = dirname(path);
1229
+ const temporary = `${path}.${randomUUID()}.tmp`;
1230
+ mkdirSync(directory, { recursive: true });
1231
+ let descriptor;
1232
+ try {
1233
+ descriptor = openSync(temporary, "wx", 384);
1234
+ writeFileSync(descriptor, data);
1235
+ fsyncSync(descriptor);
1236
+ closeSync(descriptor);
1237
+ descriptor = undefined;
1238
+ renameSync(temporary, path);
1239
+ const directoryDescriptor = openSync(directory, "r");
1240
+ try {
1241
+ fsyncSync(directoryDescriptor);
1242
+ } finally {
1243
+ closeSync(directoryDescriptor);
1244
+ }
1245
+ } catch (error) {
1246
+ if (descriptor !== undefined)
1247
+ closeSync(descriptor);
1248
+ try {
1249
+ unlinkSync(temporary);
1250
+ } catch {}
1251
+ throw error;
1252
+ }
1253
+ }
1218
1254
  async function withFileLock(lockPath, transition, options = {}) {
1219
1255
  const pollIntervalMs = options.pollIntervalMs ?? 100;
1220
1256
  const staleAfterMs = options.staleAfterMs ?? 30000;
@@ -15600,6 +15636,7 @@ function createPaths(home, environment) {
15600
15636
  idps: join(idpsDir, "index.json"),
15601
15637
  idpSessionsDir,
15602
15638
  exchangeCredentials: join(base, "exchange", "credentials.json"),
15639
+ sessionRoutes: join(base, "session", "routes.json"),
15603
15640
  idpDir: (name) => join(idpsDir, name),
15604
15641
  idpSession: (identityName) => join(idpSessionsDir, `${identityName}.json`)
15605
15642
  });
@@ -15615,6 +15652,7 @@ var INSTANCES_PATH = paths.instances;
15615
15652
  var IDPS_PATH = paths.idps;
15616
15653
  var IDP_SESSIONS_DIR = paths.idpSessionsDir;
15617
15654
  var EXCHANGE_CREDENTIALS_PATH = paths.exchangeCredentials;
15655
+ var SESSION_ROUTES_PATH = paths.sessionRoutes;
15618
15656
  // src/state/identities.ts
15619
15657
  import { readFile as readFile2 } from "node:fs/promises";
15620
15658
  var IDENTITY_STORE_VERSION = 1;
@@ -15736,6 +15774,35 @@ async function preserveLegacyBackup(path, legacyBytes) {
15736
15774
  function invalidState(path, cause) {
15737
15775
  return new IdentityStateError("IDENTITY_STATE_INVALID", path, `Identity state at ${path} is malformed`, cause);
15738
15776
  }
15777
+ // src/state/session-routes.ts
15778
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync, unlinkSync as unlinkSync2 } from "node:fs";
15779
+ import { dirname as dirname2 } from "node:path";
15780
+ class FileSessionRouteStore {
15781
+ path;
15782
+ constructor(path = SESSION_ROUTES_PATH) {
15783
+ this.path = path;
15784
+ }
15785
+ read() {
15786
+ return JSON.parse(readFileSync(this.path, "utf8"));
15787
+ }
15788
+ write(artifact) {
15789
+ const directory = dirname2(this.path);
15790
+ mkdirSync2(directory, { recursive: true, mode: 448 });
15791
+ chmodSync(directory, 448);
15792
+ atomicWriteSync(this.path, `${JSON.stringify(artifact)}
15793
+ `);
15794
+ chmodSync(this.path, 384);
15795
+ }
15796
+ clear() {
15797
+ try {
15798
+ unlinkSync2(this.path);
15799
+ } catch (error51) {
15800
+ if (error51.code !== "ENOENT")
15801
+ throw error51;
15802
+ }
15803
+ }
15804
+ }
15805
+ var SESSION_ROUTE_STORE = Object.freeze(new FileSessionRouteStore);
15739
15806
  // src/keys/algorithm.ts
15740
15807
  function inferAlg(privateJwk, keyPath) {
15741
15808
  const explicit = privateJwk.alg;
@@ -15773,7 +15840,7 @@ function keypairPaths(subject, keysDir = KEYS_DIR) {
15773
15840
  }
15774
15841
  function confinedKeyPath(keysDir, filename, subject) {
15775
15842
  const path = join2(keysDir, filename);
15776
- if (dirname2(resolve(path)) !== resolve(keysDir))
15843
+ if (dirname3(resolve(path)) !== resolve(keysDir))
15777
15844
  throw invalidKeySubject(subject);
15778
15845
  return path;
15779
15846
  }
@@ -18,6 +18,15 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
19
  // src/state/files.ts
20
20
  import { randomUUID } from "node:crypto";
21
+ import {
22
+ closeSync,
23
+ fsyncSync,
24
+ mkdirSync,
25
+ openSync,
26
+ renameSync,
27
+ unlinkSync,
28
+ writeFileSync
29
+ } from "node:fs";
21
30
  import { mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
22
31
  import { dirname } from "node:path";
23
32
  async function atomicWrite(path, data) {
@@ -48,6 +57,33 @@ async function atomicWrite(path, data) {
48
57
  throw error;
49
58
  }
50
59
  }
60
+ function atomicWriteSync(path, data) {
61
+ const directory = dirname(path);
62
+ const temporary = `${path}.${randomUUID()}.tmp`;
63
+ mkdirSync(directory, { recursive: true });
64
+ let descriptor;
65
+ try {
66
+ descriptor = openSync(temporary, "wx", 384);
67
+ writeFileSync(descriptor, data);
68
+ fsyncSync(descriptor);
69
+ closeSync(descriptor);
70
+ descriptor = undefined;
71
+ renameSync(temporary, path);
72
+ const directoryDescriptor = openSync(directory, "r");
73
+ try {
74
+ fsyncSync(directoryDescriptor);
75
+ } finally {
76
+ closeSync(directoryDescriptor);
77
+ }
78
+ } catch (error) {
79
+ if (descriptor !== undefined)
80
+ closeSync(descriptor);
81
+ try {
82
+ unlinkSync(temporary);
83
+ } catch {}
84
+ throw error;
85
+ }
86
+ }
51
87
  async function withFileLock(lockPath, transition, options = {}) {
52
88
  const pollIntervalMs = options.pollIntervalMs ?? 100;
53
89
  const staleAfterMs = options.staleAfterMs ?? 30000;
@@ -14433,6 +14469,7 @@ function createPaths(home, environment) {
14433
14469
  idps: join(idpsDir, "index.json"),
14434
14470
  idpSessionsDir,
14435
14471
  exchangeCredentials: join(base, "exchange", "credentials.json"),
14472
+ sessionRoutes: join(base, "session", "routes.json"),
14436
14473
  idpDir: (name) => join(idpsDir, name),
14437
14474
  idpSession: (identityName) => join(idpSessionsDir, `${identityName}.json`)
14438
14475
  });
@@ -14448,6 +14485,7 @@ var INSTANCES_PATH = paths.instances;
14448
14485
  var IDPS_PATH = paths.idps;
14449
14486
  var IDP_SESSIONS_DIR = paths.idpSessionsDir;
14450
14487
  var EXCHANGE_CREDENTIALS_PATH = paths.exchangeCredentials;
14488
+ var SESSION_ROUTES_PATH = paths.sessionRoutes;
14451
14489
  // src/state/identities.ts
14452
14490
  import { readFile as readFile2 } from "node:fs/promises";
14453
14491
  var IDENTITY_STORE_VERSION = 1;
@@ -14569,6 +14607,35 @@ async function preserveLegacyBackup(path, legacyBytes) {
14569
14607
  function invalidState(path, cause) {
14570
14608
  return new IdentityStateError("IDENTITY_STATE_INVALID", path, `Identity state at ${path} is malformed`, cause);
14571
14609
  }
14610
+ // src/state/session-routes.ts
14611
+ import { chmodSync, mkdirSync as mkdirSync2, readFileSync, unlinkSync as unlinkSync2 } from "node:fs";
14612
+ import { dirname as dirname2 } from "node:path";
14613
+ class FileSessionRouteStore {
14614
+ path;
14615
+ constructor(path = SESSION_ROUTES_PATH) {
14616
+ this.path = path;
14617
+ }
14618
+ read() {
14619
+ return JSON.parse(readFileSync(this.path, "utf8"));
14620
+ }
14621
+ write(artifact) {
14622
+ const directory = dirname2(this.path);
14623
+ mkdirSync2(directory, { recursive: true, mode: 448 });
14624
+ chmodSync(directory, 448);
14625
+ atomicWriteSync(this.path, `${JSON.stringify(artifact)}
14626
+ `);
14627
+ chmodSync(this.path, 384);
14628
+ }
14629
+ clear() {
14630
+ try {
14631
+ unlinkSync2(this.path);
14632
+ } catch (error51) {
14633
+ if (error51.code !== "ENOENT")
14634
+ throw error51;
14635
+ }
14636
+ }
14637
+ }
14638
+ var SESSION_ROUTE_STORE = Object.freeze(new FileSessionRouteStore);
14572
14639
  export {
14573
14640
  ASTRALE_HOME,
14574
14641
  CONFIG_PATH,
@@ -1,6 +1,6 @@
1
1
  import type { AuthApi } from '@astrale-os/sdk/auth';
2
2
  import type { GraphApi } from '@astrale-os/sdk/client';
3
- import type { ClientSessionOptions, SessionAuth } from '@astrale-os/sdk/client/session';
3
+ import type { ClientSessionOptions, SessionAuth, SessionRouteStore } from '@astrale-os/sdk/client/session';
4
4
  import { ClientSession } from '@astrale-os/sdk/client/session';
5
5
  import type { AstraleConfig } from '../lib/config';
6
6
  import type { AdminConnectionOptions, ConnectionOptions, ConnectionTarget } from './target';
@@ -26,6 +26,6 @@ export declare function withAdminClientSession<Value>(options: AdminConnectionOp
26
26
  export declare function withResolvedClientSession<Value>(target: ConnectionTarget, options: ConnectionOptions, config: AstraleConfig, action: (context: ConnectionContext) => Promise<Value>, open?: ConnectionFactory): Promise<Value>;
27
27
  export declare function resolveTimeoutMs(raw: string | undefined): number;
28
28
  /** Owner-private construction seam proving that transport and source identity stay distinct. */
29
- export declare function createClientSessionOptions(target: ConnectionTarget, fetch: NonNullable<ClientSessionOptions['fetch']>, auth: SessionAuth | undefined, timeoutMs: number): ClientSessionOptions;
29
+ export declare function createClientSessionOptions(target: ConnectionTarget, fetch: NonNullable<ClientSessionOptions['fetch']>, auth: SessionAuth | undefined, timeoutMs: number, routeStore?: SessionRouteStore): ClientSessionOptions;
30
30
  export declare function lookupManagedInstance(slug: string, options: ConnectionOptions, openAdmin?: typeof withAdminClientSession, connect?: typeof connectAdminInstances): Promise<InstanceInfo>;
31
31
  export {};
@@ -10,6 +10,12 @@ export declare const AstraleConfigSchema: z.ZodObject<{
10
10
  }, z.core.$strip>>;
11
11
  telemetry: z.ZodDefault<z.ZodObject<{
12
12
  enabled: z.ZodDefault<z.ZodBoolean>;
13
+ maxAgeDays: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
14
+ maxBytes: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
15
+ }, z.core.$strip>>;
16
+ browser: z.ZodDefault<z.ZodObject<{
17
+ maxCacheBytes: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
18
+ maxProfileAgeDays: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
13
19
  }, z.core.$strip>>;
14
20
  }, z.core.$strip>;
15
21
  export type AstraleConfig = z.infer<typeof AstraleConfigSchema>;
@@ -1,16 +1,20 @@
1
1
  export declare namespace exchange {
2
2
  interface Artifact {
3
- readonly version: 1;
3
+ readonly version: 2;
4
4
  readonly entries: Record<string, Entry>;
5
5
  }
6
6
  interface Key {
7
7
  readonly kernelIssuer: string;
8
8
  readonly domainIssuer: string;
9
- readonly user: string;
9
+ readonly sourceIssuer: string;
10
+ readonly sourceSubject: string;
10
11
  }
11
12
  interface Entry {
12
13
  readonly credential: string;
13
14
  readonly expiresAt: number;
15
+ readonly user: string;
16
+ readonly sourceIssuer: string;
17
+ readonly sourceSubject: string;
14
18
  }
15
19
  }
16
20
  export declare class ExchangeCredentialCache {
@@ -1,5 +1,7 @@
1
1
  /** Atomically publish one complete private state file through a same-directory temporary file. */
2
2
  export declare function atomicWrite(path: string, data: string): Promise<void>;
3
+ /** Atomically publish one complete private state file for synchronous consumer capabilities. */
4
+ export declare function atomicWriteSync(path: string, data: string): void;
3
5
  export interface FileLockOptions {
4
6
  readonly pollIntervalMs?: number;
5
7
  readonly staleAfterMs?: number;
@@ -1,8 +1,9 @@
1
- export { atomicWrite, withFileLock } from './files';
1
+ export { atomicWrite, atomicWriteSync, withFileLock } from './files';
2
2
  export type { FileLockOptions } from './files';
3
3
  export { ExchangeCredentialCache } from './exchange-credentials';
4
4
  export type { exchange } from './exchange-credentials';
5
5
  export { IDENTITY_STORE_VERSION, readIdentityStore, updateIdentityStore } from './identities';
6
6
  export type { Identity, IdentityMode, IdentitySource, IdentityStore, IdentityStoreOptions, IdentityUpdate, Registration, } from './identities';
7
- export { ASTRALE_HOME, CONFIG_PATH, DATA_DIR, IDENTITIES_PATH, IDPS_PATH, IDP_SESSIONS_DIR, EXCHANGE_CREDENTIALS_PATH, INSTALL_PATH, INSTANCES_PATH, KEYS_DIR, createPaths, paths, } from './paths';
7
+ export { ASTRALE_HOME, CONFIG_PATH, DATA_DIR, IDENTITIES_PATH, IDPS_PATH, IDP_SESSIONS_DIR, EXCHANGE_CREDENTIALS_PATH, INSTALL_PATH, INSTANCES_PATH, KEYS_DIR, SESSION_ROUTES_PATH, createPaths, paths, } from './paths';
8
8
  export type { PathEnvironment, Paths } from './paths';
9
+ export { FileSessionRouteStore, SESSION_ROUTE_STORE } from './session-routes';
@@ -14,6 +14,7 @@ export interface Paths {
14
14
  readonly idps: string;
15
15
  readonly idpSessionsDir: string;
16
16
  readonly exchangeCredentials: string;
17
+ readonly sessionRoutes: string;
17
18
  idpDir(name: string): string;
18
19
  idpSession(identityName: string): string;
19
20
  }
@@ -30,3 +31,4 @@ export declare const INSTANCES_PATH: string;
30
31
  export declare const IDPS_PATH: string;
31
32
  export declare const IDP_SESSIONS_DIR: string;
32
33
  export declare const EXCHANGE_CREDENTIALS_PATH: string;
34
+ export declare const SESSION_ROUTES_PATH: string;
@@ -0,0 +1,10 @@
1
+ import type { SessionRouteArtifact, SessionRouteStore } from '@astrale-os/sdk/client/session';
2
+ /** CLI filesystem representation for Kernel Client's admitted confidential route artifact. */
3
+ export declare class FileSessionRouteStore implements SessionRouteStore {
4
+ private readonly path;
5
+ constructor(path?: string);
6
+ read(): unknown;
7
+ write(artifact: SessionRouteArtifact): void;
8
+ clear(): void;
9
+ }
10
+ export declare const SESSION_ROUTE_STORE: Readonly<FileSessionRouteStore>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.28",
3
+ "version": "1.0.0-beta.29",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@astrale-os/ox": ">=0.1.3 <1.0.0",
66
- "@astrale-os/sdk": "0.5.0-beta.51",
66
+ "@astrale-os/sdk": "0.5.0-beta.54",
67
67
  "@astrale-os/shell": "0.4.2-beta.6",
68
68
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
69
69
  "@commitlint/cli": "21.2.2",
@@ -4,7 +4,7 @@ import { getDefault, readIdentities } from '../../identity/index'
4
4
  import { deleteIdpSession, listIdpSessions } from '../../lib/idp'
5
5
  import { log } from '../../lib/log'
6
6
  import { output, RAW_OUTPUT_OPTIONS } from '../../lib/output'
7
- import { ExchangeCredentialCache } from '../../state/index'
7
+ import { ExchangeCredentialCache, SESSION_ROUTE_STORE } from '../../state/index'
8
8
 
9
9
  export default {
10
10
  name: 'logout',
@@ -30,6 +30,7 @@ export default {
30
30
 
31
31
  for (const name of names) await deleteIdpSession(name)
32
32
  await new ExchangeCredentialCache().clear()
33
+ SESSION_ROUTE_STORE.clear()
33
34
 
34
35
  if (opts.raw || opts.json) {
35
36
  output({ cleared: names }, opts)
@@ -13,6 +13,7 @@ import {
13
13
  profileDirFor,
14
14
  saveSession,
15
15
  } from '../lib/browser'
16
+ import { sweepBrowserProfiles } from '../lib/browser-retention'
16
17
  import { readLocalStatus } from '../lib/local-status'
17
18
  import { fatal, log } from '../lib/log'
18
19
  import { isMachine, output, RAW_OUTPUT_OPTIONS, type RawOutputOpts } from '../lib/output'
@@ -25,6 +26,29 @@ type BrowserOpts = RawOutputOpts & {
25
26
  check?: boolean
26
27
  }
27
28
 
29
+ /**
30
+ * Opportunistic profile retention, the same `git gc --auto` shape the session
31
+ * store uses: no daemon, no cron, it just rides on the command that created the
32
+ * mess. Silent when there is nothing to do, and never fatal — losing a sweep is
33
+ * strictly better than losing the browser command.
34
+ */
35
+ async function reportSweep(machine: boolean): Promise<void> {
36
+ try {
37
+ const swept = await sweepBrowserProfiles()
38
+ if (machine || swept.bytesFreed === 0) return
39
+ const freed = `${(swept.bytesFreed / 1024 / 1024).toFixed(0)} MB`
40
+ const what = [
41
+ swept.removed.length > 0 ? `${swept.removed.length} dormant profile(s) removed` : null,
42
+ swept.purged.length > 0 ? `${swept.purged.length} cache(s) trimmed` : null,
43
+ ]
44
+ .filter(Boolean)
45
+ .join(', ')
46
+ log.dim(`retention: ${what} — ${freed} freed`)
47
+ } catch {
48
+ /* retention must never break the browser command */
49
+ }
50
+ }
51
+
28
52
  const LOGIN_TIMEOUT_MS = 180_000
29
53
  const POLL_INTERVAL_MS = 2500
30
54
 
@@ -125,6 +149,11 @@ Examples:
125
149
  const host = new URL(gui).host
126
150
  await requireAgentBrowser(machine, opts)
127
151
 
152
+ // Retention runs BEFORE anything launches, so no profile we touch can be in
153
+ // use by a browser this command started. Profiles held by someone else's
154
+ // live browser are skipped by the sweep itself.
155
+ await reportSweep(machine)
156
+
128
157
  const usingCdp = !!opts.cdp
129
158
  const profile = usingCdp ? null : (opts.profile ?? profileDirFor(host))
130
159
  const target = usingCdp ? { cdp: opts.cdp } : { profile: profile! }