@astrale-os/cli 1.0.0-beta.30 → 1.0.0-beta.32

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 (43) hide show
  1. package/README.md +5 -1
  2. package/dist/astrale.js +178 -57
  3. package/dist/public/connect-core.js +42 -13
  4. package/dist/public/keys/index.js +14 -0
  5. package/dist/public/paths/index.js +14 -0
  6. package/dist/types/connection/auth.d.ts +1 -0
  7. package/dist/types/connection/credential.d.ts +1 -1
  8. package/dist/types/connection/lifetime.d.ts +6 -0
  9. package/dist/types/lib/credential-lifetime.d.ts +3 -0
  10. package/dist/types/lib/idp-session.d.ts +2 -0
  11. package/dist/types/lib/idp.d.ts +1 -1
  12. package/dist/types/state/exchange-credentials.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/src/commands/__tests__/read-commands.test.ts +2 -2
  15. package/src/commands/query.ts +5 -5
  16. package/src/commands/update.ts +3 -2
  17. package/src/connection/.spec/architecture.md +5 -2
  18. package/src/connection/.spec/flows/session.ts +2 -1
  19. package/src/connection/.spec/laws/connection.ts +9 -1
  20. package/src/connection/.spec/layout.ts +1 -0
  21. package/src/connection/__tests__/credential.test.ts +115 -0
  22. package/src/connection/__tests__/exchange.test.ts +94 -9
  23. package/src/connection/auth.ts +20 -4
  24. package/src/connection/credential.ts +29 -8
  25. package/src/connection/exchange.ts +65 -7
  26. package/src/connection/lifetime.ts +25 -0
  27. package/src/graph/.spec/api.d.ts +1 -1
  28. package/src/graph/.spec/laws/documents.ts +7 -7
  29. package/src/graph/__tests__/query.test.ts +12 -5
  30. package/src/graph/query.ts +9 -11
  31. package/src/lib/__tests__/idp-session.driver.ts +10 -3
  32. package/src/lib/__tests__/idp-session.test.ts +25 -0
  33. package/src/lib/__tests__/skills.test.ts +27 -2
  34. package/src/lib/credential-lifetime.ts +24 -0
  35. package/src/lib/idp-session.ts +26 -4
  36. package/src/lib/idp.ts +24 -4
  37. package/src/lib/skills/sync.ts +72 -8
  38. package/src/program/__tests__/program.test.ts +39 -2
  39. package/src/state/__tests__/exchange-credentials.test.ts +112 -5
  40. package/src/state/exchange-credentials.ts +26 -8
  41. package/studio/server/cli-consumers.test.ts +1 -1
  42. package/studio/server/cli.test.ts +2 -2
  43. package/studio/server/views/target.ts +1 -1
package/README.md CHANGED
@@ -105,7 +105,11 @@ astrale update
105
105
  from `astrale-os/cli` is installed and healthy, and follows the beta channel by default. It reports
106
106
  whether the skill pass was unchanged, installed, updated, or repaired. Use `--check`,
107
107
  `--channel <channel>`, or `--version <version>` to control the CLI release target; skills always
108
- track the repository's current `main` source. `--no-skills` is the explicit opt-out.
108
+ resolve the repository's current `main` once, verify the installed trees against that commit, and
109
+ stamp the exact revision/tree binding into the Astrale-owned receipt. A current
110
+ `astrale update --check --json` result includes the resolved source revision plus every shipped
111
+ skill's Git tree and entrypoint, so runners can retain the actual loadout rather than only
112
+ `status: current`. `--no-skills` is the explicit opt-out.
109
113
 
110
114
  ## Commands
111
115
 
package/dist/astrale.js CHANGED
@@ -2578,7 +2578,7 @@ var package_default;
2578
2578
  var init_package = __esm(() => {
2579
2579
  package_default = {
2580
2580
  name: "@astrale-os/cli",
2581
- version: "1.0.0-beta.30",
2581
+ version: "1.0.0-beta.32",
2582
2582
  description: "Astrale CLI — connect to existing Astrale kernels",
2583
2583
  keywords: [
2584
2584
  "astrale",
@@ -34087,6 +34087,20 @@ var init_auth4 = __esm(() => {
34087
34087
  init_auth();
34088
34088
  });
34089
34089
 
34090
+ // src/lib/credential-lifetime.ts
34091
+ function remainingCredentialLifetimeSeconds(expiresAtEpochSeconds, nowEpochSeconds = Math.ceil(Date.now() / 1000)) {
34092
+ if (!Number.isSafeInteger(expiresAtEpochSeconds) || !Number.isSafeInteger(nowEpochSeconds)) {
34093
+ throw new TypeError("Credential expiration and current time must be safe epoch seconds.");
34094
+ }
34095
+ return expiresAtEpochSeconds - nowEpochSeconds - 1;
34096
+ }
34097
+ function credentialLifetimeCovers(expiresAtEpochSeconds, minimumRemainingSeconds, nowEpochSeconds) {
34098
+ if (!Number.isSafeInteger(minimumRemainingSeconds) || minimumRemainingSeconds < 1) {
34099
+ throw new TypeError("Credential minimum lifetime must be a positive safe integer.");
34100
+ }
34101
+ return remainingCredentialLifetimeSeconds(expiresAtEpochSeconds, nowEpochSeconds) >= minimumRemainingSeconds;
34102
+ }
34103
+
34090
34104
  // src/state/paths.ts
34091
34105
  import { homedir } from "node:os";
34092
34106
  import { join } from "node:path";
@@ -34141,15 +34155,19 @@ class ExchangeCredentialCache {
34141
34155
  constructor(path = EXCHANGE_CREDENTIALS_PATH) {
34142
34156
  this.path = path;
34143
34157
  }
34144
- getOrRefresh(key, refresh, now = () => Math.floor(Date.now() / 1000)) {
34158
+ getOrRefresh(key, minimumRemainingSeconds, refresh, now = () => Math.floor(Date.now() / 1000)) {
34159
+ if (!Number.isSafeInteger(minimumRemainingSeconds) || minimumRemainingSeconds < 1) {
34160
+ throw new TypeError("Exchange credential minimum lifetime must be a positive safe integer.");
34161
+ }
34145
34162
  const encoded = encodeKey(key);
34146
- const current = this.refreshing.get(encoded);
34163
+ const pendingKey = `${encoded}\x00${minimumRemainingSeconds}`;
34164
+ const current = this.refreshing.get(pendingKey);
34147
34165
  if (current !== undefined)
34148
34166
  return current;
34149
- const pending = this.getOrRefreshOnce(key, encoded, refresh, now).finally(() => {
34150
- this.refreshing.delete(encoded);
34167
+ const pending = this.getOrRefreshOnce(key, encoded, minimumRemainingSeconds, refresh, now).finally(() => {
34168
+ this.refreshing.delete(pendingKey);
34151
34169
  });
34152
- this.refreshing.set(encoded, pending);
34170
+ this.refreshing.set(pendingKey, pending);
34153
34171
  return pending;
34154
34172
  }
34155
34173
  async deleteKernel(kernelIssuer) {
@@ -34167,20 +34185,20 @@ class ExchangeCredentialCache {
34167
34185
  delete store.entries[encoded];
34168
34186
  });
34169
34187
  }
34170
- async getOrRefreshOnce(key, encoded, refresh, now) {
34188
+ async getOrRefreshOnce(key, encoded, minimumRemainingSeconds, refresh, now) {
34171
34189
  return withFileLock(`${this.path}.lock`, async () => {
34172
34190
  await ensurePrivateDirectory(this.path);
34173
34191
  const store = await readStore(this.path);
34174
34192
  const changed = scrub(store, now());
34175
34193
  const cached3 = store.entries[encoded];
34176
- if (cached3 !== undefined && validEntry(key, cached3, now())) {
34194
+ if (cached3 !== undefined && validEntry(key, cached3, now(), minimumRemainingSeconds)) {
34177
34195
  if (changed)
34178
34196
  await writeStore(this.path, store);
34179
34197
  return cached3.credential;
34180
34198
  }
34181
34199
  const next = await refresh();
34182
- if (!validEntry(key, next, now(), 1)) {
34183
- throw new Error("Token exchange returned a credential inconsistent with its cache key.");
34200
+ if (!validEntry(key, next, now(), minimumRemainingSeconds)) {
34201
+ throw new Error("Token exchange returned a credential inconsistent with its cache key or required lifetime.");
34184
34202
  }
34185
34203
  store.entries[encoded] = Object.freeze({ ...next });
34186
34204
  await writeStore(this.path, store);
@@ -34239,7 +34257,7 @@ function validEntry(key, entry, now, minimumRemaining = MINIMUM_REMAINING_SECOND
34239
34257
  return false;
34240
34258
  }
34241
34259
  exports_grant.accept({ expr: Reflect.get(issued, "expr") });
34242
- return inspected.iss === key.domainIssuer && inspected.aud === key.kernelIssuer && inspected.claims.exp === entry.expiresAt && !Object.hasOwn(inspected.claims, "delegation") && proof.iss === key.kernelIssuer && proof.sub === entry.user && proof.aud === key.kernelIssuer;
34260
+ return inspected.iss === key.domainIssuer && inspected.aud === key.kernelIssuer && inspected.claims.exp === entry.expiresAt && !Object.hasOwn(inspected.claims, "delegation") && proof.iss === key.kernelIssuer && proof.sub === entry.user && proof.aud === key.kernelIssuer && typeof proof.claims.exp === "number" && Number.isSafeInteger(proof.claims.exp) && credentialLifetimeCovers(proof.claims.exp, minimumRemaining, now);
34243
34261
  } catch {
34244
34262
  return false;
34245
34263
  }
@@ -38190,18 +38208,25 @@ function isSessionExpired(session, skewMs = 60000) {
38190
38208
  return false;
38191
38209
  return new Date(expiresAt).getTime() <= Date.now() + skewMs;
38192
38210
  }
38193
- function accessTokenForAudience(session, audience) {
38211
+ function accessTokenForAudience(session, audience, minimumRemainingMs = 60000) {
38194
38212
  if (audience === undefined) {
38195
- return isSessionExpired(session) ? undefined : session.access_token;
38213
+ return tokenHasMinimumLifetime(session, minimumRemainingMs) ? session.access_token : undefined;
38196
38214
  }
38197
38215
  const entry = session.tokens?.[audience];
38198
- if (entry && !isSessionExpired(entry))
38216
+ if (entry && tokenHasMinimumLifetime(entry, minimumRemainingMs))
38199
38217
  return entry.access_token;
38200
- if (!isSessionExpired(session) && tokenAudienceMatches(session.access_token, audience)) {
38218
+ if (tokenHasMinimumLifetime(session, minimumRemainingMs) && tokenAudienceMatches(session.access_token, audience)) {
38201
38219
  return session.access_token;
38202
38220
  }
38203
38221
  return;
38204
38222
  }
38223
+ function tokenHasMinimumLifetime(value2, minimumRemainingMs) {
38224
+ const expiration = decodeTokenClaims(value2.access_token)?.exp;
38225
+ if (typeof expiration === "number" && Number.isSafeInteger(expiration)) {
38226
+ return credentialLifetimeCovers(expiration, Math.ceil(minimumRemainingMs / 1000));
38227
+ }
38228
+ return !isSessionExpired(value2, minimumRemainingMs);
38229
+ }
38205
38230
  function withCachedToken(tokens, accessToken, expiresAt) {
38206
38231
  const next = {};
38207
38232
  for (const [aud2, entry] of Object.entries(tokens ?? {})) {
@@ -61700,7 +61725,7 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
61700
61725
  }
61701
61726
  }));
61702
61727
  if (managed.length === 0 && expectedPresence.every((present2) => !present2)) {
61703
- return { state: "absent", managedNames: [], managedFolders: [] };
61728
+ return { state: "absent", sourceCurrent: false, managedNames: [], managedFolders: [] };
61704
61729
  }
61705
61730
  const folders = managed.flatMap(([name, entry]) => {
61706
61731
  const folder = folderName(entry);
@@ -61742,12 +61767,17 @@ async function inspectAstraleSkills(snapshot2, home, lockPath) {
61742
61767
  }
61743
61768
  }
61744
61769
  }
61745
- const exactCurrent = coherent && managed.length === snapshot2.skills.length && folders.every(({ key, folder, entry }) => {
61770
+ const sourceCurrent = coherent && managed.length === snapshot2.skills.length && folders.every(({ key, folder, entry }) => {
61746
61771
  const skill = expected.get(folder);
61747
61772
  return key === folder && skill !== undefined && entry.ref === snapshot2.ref && actualHashes.get(folder) === skill.tree;
61748
61773
  });
61774
+ const exactCurrent = sourceCurrent && folders.every(({ folder, entry }) => {
61775
+ const skill = expected.get(folder);
61776
+ return skill !== undefined && entry.astraleSourceRevision === snapshot2.revision && entry.astraleSourceTree === skill.tree;
61777
+ });
61749
61778
  return {
61750
61779
  state: exactCurrent ? "current" : coherent ? "outdated" : "unhealthy",
61780
+ sourceCurrent,
61751
61781
  managedNames: managed.map(([name]) => name),
61752
61782
  managedFolders: [...uniqueFolders]
61753
61783
  };
@@ -61768,6 +61798,20 @@ function installerArgs(...args) {
61768
61798
  async function installSnapshot(snapshot2, execute, selectedAgents) {
61769
61799
  return await execute("npx", installerArgs("add", `${ASTRALE_CLI_SKILL_SOURCE}#${snapshot2.ref}`, "-g", "-y", "--skill", ...snapshot2.skills.map((skill) => skill.name), ...selectedAgents.length > 0 ? ["--agent", ...selectedAgents] : []));
61770
61800
  }
61801
+ async function stampAstraleSource(snapshot2, lockPath) {
61802
+ const { lock } = await readSkillLock(lockPath);
61803
+ if (!lock)
61804
+ throw new Error("skill installer receipt is unavailable after installation");
61805
+ for (const skill of snapshot2.skills) {
61806
+ const entry = lock.skills[skill.name];
61807
+ if (!entry || !sourceOwned(entry) || entry.ref !== snapshot2.ref || entry.skillPath !== skill.path) {
61808
+ throw new Error(`skill installer receipt is incomplete for ${skill.name}`);
61809
+ }
61810
+ entry.astraleSourceRevision = snapshot2.revision;
61811
+ entry.astraleSourceTree = skill.tree;
61812
+ }
61813
+ await writeSkillLock(lockPath, lock);
61814
+ }
61771
61815
  async function selectedAgents(home, lockPath) {
61772
61816
  const { lock } = await readSkillLock(lockPath);
61773
61817
  const agents = new Set((lock?.lastSelectedAgents ?? []).filter((agent) => typeof agent === "string" && SAFE_NAME.test(agent)));
@@ -61989,8 +62033,20 @@ async function checkAstraleSkills(overrides = {}) {
61989
62033
  try {
61990
62034
  const snapshot2 = await sourceSnapshot(dependencies);
61991
62035
  const inspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62036
+ const status = inspection.state === "current" ? "current" : inspection.state === "unhealthy" ? "repair-needed" : "update-available";
61992
62037
  return {
61993
- status: inspection.state === "current" ? "current" : inspection.state === "unhealthy" ? "repair-needed" : "update-available"
62038
+ status,
62039
+ ...status === "current" ? {
62040
+ source: {
62041
+ repository: ASTRALE_CLI_SKILL_SOURCE,
62042
+ revision: snapshot2.revision,
62043
+ skills: snapshot2.skills.map(({ name, tree, path: path4 }) => ({
62044
+ name,
62045
+ tree,
62046
+ entrypoint: path4
62047
+ }))
62048
+ }
62049
+ } : {}
61994
62050
  };
61995
62051
  } catch (error52) {
61996
62052
  return { status: "unavailable", error: error52 instanceof Error ? error52.message : String(error52) };
@@ -62040,6 +62096,11 @@ async function syncAstraleSkills(overrides = {}) {
62040
62096
  throw new Error(installed.stderr || installed.stdout || "skill installer failed");
62041
62097
  }
62042
62098
  await pruneObsoleteEntries(snapshot2, dependencies.home, dependencies.lockPath, dependencies.run, knownRetired);
62099
+ const installedInspection = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62100
+ if (!installedInspection.sourceCurrent) {
62101
+ throw new Error("installed Astrale skills do not match the resolved source");
62102
+ }
62103
+ await stampAstraleSource(snapshot2, dependencies.lockPath);
62043
62104
  const verified = await inspectAstraleSkills(snapshot2, dependencies.home, dependencies.lockPath);
62044
62105
  if (verified.state !== "current") {
62045
62106
  throw new Error("installed Astrale skills did not pass verification");
@@ -80744,10 +80805,11 @@ function idpSessionLockPath(identityName) {
80744
80805
  return `${idpSessionPath(identityName)}.lock`;
80745
80806
  }
80746
80807
  async function ensureFreshSession(identityName, opts = {}) {
80808
+ const minimumRemainingMs = minimumLifetimeMs(opts.minimumRemainingSeconds);
80747
80809
  const session3 = await readIdpSession(identityName);
80748
80810
  if (!session3)
80749
80811
  throw new IdpSessionMissingError(identityName);
80750
- if (accessTokenForAudience(session3, opts.audience))
80812
+ if (accessTokenForAudience(session3, opts.audience, minimumRemainingMs))
80751
80813
  return session3;
80752
80814
  if (!session3.refresh_token)
80753
80815
  throw new IdpSessionNoRefreshTokenError(identityName);
@@ -80755,7 +80817,7 @@ async function ensureFreshSession(identityName, opts = {}) {
80755
80817
  const current = await readIdpSession(identityName);
80756
80818
  if (!current)
80757
80819
  throw new IdpSessionMissingError(identityName);
80758
- if (accessTokenForAudience(current, opts.audience))
80820
+ if (accessTokenForAudience(current, opts.audience, minimumRemainingMs))
80759
80821
  return current;
80760
80822
  const bookmarkOrg = opts.organizationId ?? (opts.audience ? await orgIdForAudience(opts.audience) : undefined);
80761
80823
  const organizationId = bookmarkOrg ?? (opts.audience ? await (opts.resolveOrganizationId ?? fetchOrgHint)(opts.audience) : undefined);
@@ -80774,23 +80836,30 @@ async function ensureFreshSession(identityName, opts = {}) {
80774
80836
  });
80775
80837
  }
80776
80838
  }
80777
- const rescued = await rescueAfterInvalidGrant(identityName, current, opts.audience, e);
80839
+ const rescued = await rescueAfterInvalidGrant(identityName, current, opts.audience, minimumRemainingMs, e);
80778
80840
  if (rescued)
80779
80841
  return rescued;
80780
80842
  throw e;
80781
80843
  }
80782
80844
  });
80783
80845
  }
80784
- async function rescueAfterInvalidGrant(identityName, seen, audience, error52) {
80846
+ async function rescueAfterInvalidGrant(identityName, seen, audience, minimumRemainingMs, error52) {
80785
80847
  if (!(error52 instanceof OAuthTokenError) || error52.code !== "invalid_grant")
80786
80848
  return;
80787
80849
  const latest = await readIdpSession(identityName).catch(() => null);
80788
80850
  if (!latest || latest.updatedAt === seen.updatedAt)
80789
80851
  return;
80790
- if (!accessTokenForAudience(latest, audience))
80852
+ if (!accessTokenForAudience(latest, audience, minimumRemainingMs))
80791
80853
  return;
80792
80854
  return latest;
80793
80855
  }
80856
+ function minimumLifetimeMs(input) {
80857
+ const seconds = input ?? 60;
80858
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > Math.floor(Number.MAX_SAFE_INTEGER / 1000)) {
80859
+ throw new TypeError("IdP session minimum lifetime must be a positive safe integer.");
80860
+ }
80861
+ return seconds * 1000;
80862
+ }
80794
80863
  var IdpSessionMissingError, IdpSessionNoRefreshTokenError;
80795
80864
  var init_idp_session = __esm(() => {
80796
80865
  init_state();
@@ -80835,14 +80904,14 @@ async function resolveCredential2(opts, config2, audience = config2.issuer, regi
80835
80904
  resolvedIdentity = identity4;
80836
80905
  resolvedName = identityName;
80837
80906
  if ((identity4.source ?? "key") === "idp")
80838
- return await resolveIdpAccessToken(identityName, identity4, audience);
80907
+ return await resolveIdpAccessToken(identityName, identity4, audience, opts.minimumRemainingSeconds);
80839
80908
  return await signAs(identity4.subject, KEYS_DIR, resolveKeyIdentityAuthOptions(identity4, config2, audience, registrationKey));
80840
80909
  }
80841
80910
  const identity3 = await getDefault();
80842
80911
  resolvedIdentity = identity3;
80843
80912
  resolvedName = identity3.name;
80844
80913
  if ((identity3.source ?? "key") === "idp") {
80845
- return await resolveIdpAccessToken(identity3.name, identity3, audience);
80914
+ return await resolveIdpAccessToken(identity3.name, identity3, audience, opts.minimumRemainingSeconds);
80846
80915
  }
80847
80916
  return await signAs(identity3.subject, KEYS_DIR, resolveKeyIdentityAuthOptions(identity3, config2, audience, registrationKey));
80848
80917
  } catch (e) {
@@ -80884,10 +80953,10 @@ function resolveKeyIdentityAuthOptions(identity3, config2, audience = config2.is
80884
80953
  function systemIdentityIssuer(identity3, audience, config2) {
80885
80954
  return identity3.subject === "system" ? audience : config2.issuer;
80886
80955
  }
80887
- async function resolveIdpAccessToken(identityName, identity3, audience) {
80956
+ async function resolveIdpAccessToken(identityName, identity3, audience, minimumRemainingSeconds) {
80888
80957
  let resolved;
80889
80958
  try {
80890
- resolved = await ensureFreshSession(identityName, { audience });
80959
+ resolved = await ensureFreshSession(identityName, { audience, minimumRemainingSeconds });
80891
80960
  } catch (e) {
80892
80961
  if (e instanceof IdpSessionMissingError) {
80893
80962
  throw new Error(`No cached IdP session for "${identityName}". Run: astrale auth login --idp ${identity3.idp ?? "<idp>"}`);
@@ -80942,9 +81011,26 @@ var init_auth7 = __esm(() => {
80942
81011
  };
80943
81012
  });
80944
81013
 
81014
+ // src/connection/lifetime.ts
81015
+ function invocationCredentialTtlSeconds(timeoutMs) {
81016
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
81017
+ throw new TypeError("Invocation credential timeout must be a positive safe integer.");
81018
+ }
81019
+ return Math.max(MINIMUM_CREDENTIAL_TTL_SECONDS, Math.ceil(timeoutMs / 1000) + INVOCATION_RECEIPT_MARGIN_SECONDS);
81020
+ }
81021
+ function exchangeCredentialTtlSeconds(timeoutMs) {
81022
+ return invocationCredentialTtlSeconds(timeoutMs) + TOKEN_EXCHANGE_SETTLEMENT_MARGIN_SECONDS;
81023
+ }
81024
+ function cachedCredentialTtlSeconds(timeoutMs) {
81025
+ return invocationCredentialTtlSeconds(timeoutMs) + CACHE_HANDOFF_MARGIN_SECONDS;
81026
+ }
81027
+ var MINIMUM_CREDENTIAL_TTL_SECONDS = 60, INVOCATION_RECEIPT_MARGIN_SECONDS = 5, CACHE_HANDOFF_MARGIN_SECONDS = 5, TOKEN_EXCHANGE_SETTLEMENT_MARGIN_SECONDS = 15;
81028
+
80945
81029
  // src/connection/exchange.ts
80946
81030
  function createExchangeCredentialResolver(target2, source2, fetch2, timeoutMs, cache2 = new ExchangeCredentialCache) {
80947
81031
  requireExchangeTransport(target2);
81032
+ const cacheTtlSeconds = cachedCredentialTtlSeconds(timeoutMs);
81033
+ const exchangeTtlSeconds = exchangeCredentialTtlSeconds(timeoutMs);
80948
81034
  return Object.freeze({
80949
81035
  async resolve(kernelIssuer, signal) {
80950
81036
  requireLive3(signal);
@@ -80956,8 +81042,8 @@ function createExchangeCredentialResolver(target2, source2, fetch2, timeoutMs, c
80956
81042
  domainIssuer: target2.domainIssuer,
80957
81043
  sourceIssuer: sourceIdentity.issuer,
80958
81044
  sourceSubject: sourceIdentity.subject
80959
- }), async () => {
80960
- const delegationTtlSeconds = delegationLifetime(sourceToken);
81045
+ }), cacheTtlSeconds, async () => {
81046
+ const delegationTtlSeconds = delegationLifetime(sourceToken, exchangeTtlSeconds);
80961
81047
  const client = new Client({ url: `${kernelIssuer}/invoke`, fetch: fetch2, timeoutMs });
80962
81048
  try {
80963
81049
  const authenticated3 = client.as(sourceToken);
@@ -80986,7 +81072,7 @@ function createExchangeCredentialResolver(target2, source2, fetch2, timeoutMs, c
80986
81072
  if (envelope === undefined)
80987
81073
  throw new Error("Token delegation returned no credential.");
80988
81074
  return {
80989
- ...await exchange(target2.domainIssuer, kernelIssuer, envelope, fetch2, signal),
81075
+ ...await exchange(target2.domainIssuer, kernelIssuer, envelope, cacheTtlSeconds, fetch2, signal),
80990
81076
  user: user.id,
80991
81077
  sourceIssuer: sourceIdentity.issuer,
80992
81078
  sourceSubject: sourceIdentity.subject
@@ -81015,18 +81101,21 @@ function unknownFunctionOutcome(cause) {
81015
81101
  return false;
81016
81102
  return error52.reason.code === "FUNCTION_OUTCOME_UNKNOWN";
81017
81103
  }
81018
- function delegationLifetime(sourceToken) {
81104
+ function delegationLifetime(sourceToken, requiredTtlSeconds) {
81019
81105
  const expiresAt = exports_credential.inspect(sourceToken).claims.exp;
81020
81106
  if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt)) {
81021
81107
  throw new AstraleError("TOKEN_EXCHANGE_SOURCE_INVALID", "The source identity credential has no valid expiration.");
81022
81108
  }
81023
- const remaining = expiresAt - Math.ceil(Date.now() / 1000) - 1;
81109
+ const remaining = remainingCredentialLifetimeSeconds(expiresAt);
81024
81110
  if (!Number.isSafeInteger(remaining) || remaining < 1) {
81025
81111
  throw new AstraleError("TOKEN_EXCHANGE_SOURCE_EXPIRED", "The source identity credential has no lifetime available for token exchange.");
81026
81112
  }
81027
- return Math.min(EXCHANGE_TTL_SECONDS, remaining);
81113
+ if (remaining < requiredTtlSeconds) {
81114
+ throw new AstraleError("TOKEN_EXCHANGE_SOURCE_LIFETIME_INSUFFICIENT", "The source credential cannot cover the requested command timeout.", `Refresh the identity session or use a shorter --timeout; ${remaining} seconds remain but ${requiredTtlSeconds} are required.`);
81115
+ }
81116
+ return requiredTtlSeconds;
81028
81117
  }
81029
- async function exchange(domainIssuer, kernelIssuer, envelope, fetch2, signal) {
81118
+ async function exchange(domainIssuer, kernelIssuer, envelope, requiredTtlSeconds, fetch2, signal) {
81030
81119
  const configurationUrl = new URL(exports_exchange.paths(domainIssuer).configuration, domainIssuer).toString();
81031
81120
  const configurationResponse = await fetchExchange(fetch2, configurationUrl, {
81032
81121
  method: "GET",
@@ -81083,8 +81172,31 @@ async function exchange(domainIssuer, kernelIssuer, envelope, fetch2, signal) {
81083
81172
  if (inspected.iss !== domainIssuer || inspected.aud !== kernelIssuer || inspected.claims.exp !== exchanged.expiresAt) {
81084
81173
  throw new AstraleError("TOKEN_EXCHANGE_PROTOCOL_ERROR", "Token exchange returned a credential inconsistent with the requested Domain and Kernel.");
81085
81174
  }
81175
+ const remaining = effectiveExchangeLifetime(inspected, exchanged.expiresAt);
81176
+ if (remaining < requiredTtlSeconds) {
81177
+ throw new AstraleError("TOKEN_EXCHANGE_LIFETIME_INSUFFICIENT", "The Domain exchange credential cannot cover the requested command timeout.", `The Domain issuer returned ${Math.max(0, remaining)} seconds but ${requiredTtlSeconds} are required. Use a shorter --timeout or update the Domain execution service.`);
81178
+ }
81086
81179
  return Object.freeze({ credential: exchanged.token, expiresAt: exchanged.expiresAt });
81087
81180
  }
81181
+ function effectiveExchangeLifetime(inspected, outerExpiresAt) {
81182
+ let proofExpiresAt;
81183
+ try {
81184
+ const carried = exports_grant.acceptUnresolved(inspected.claims.grant).expr;
81185
+ if (carried.kind !== "identity" || !("credential" in carried) || typeof carried.credential !== "string") {
81186
+ throw new TypeError("Domain credential does not carry an identity proof.");
81187
+ }
81188
+ const value3 = exports_credential.inspect(carried.credential).claims.exp;
81189
+ if (typeof value3 !== "number" || !Number.isSafeInteger(value3)) {
81190
+ throw new TypeError("Domain credential carries an identity proof without an expiration.");
81191
+ }
81192
+ proofExpiresAt = value3;
81193
+ } catch (cause) {
81194
+ if (!(cause instanceof TypeError))
81195
+ throw cause;
81196
+ throw new AstraleError("TOKEN_EXCHANGE_PROTOCOL_ERROR", "Token exchange returned an invalid carried identity proof.", cause.message);
81197
+ }
81198
+ return remainingCredentialLifetimeSeconds(Math.min(outerExpiresAt, proofExpiresAt));
81199
+ }
81088
81200
  async function fetchExchange(fetch2, input, init, message2) {
81089
81201
  try {
81090
81202
  return await fetch2(input, init);
@@ -81155,14 +81267,13 @@ function requireLive3(signal) {
81155
81267
  return;
81156
81268
  throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
81157
81269
  }
81158
- var EXCHANGE_TTL_SECONDS, MAXIMUM_RESPONSE_BYTES;
81270
+ var MAXIMUM_RESPONSE_BYTES;
81159
81271
  var init_exchange4 = __esm(() => {
81160
81272
  init_auth4();
81161
81273
  init_auth4();
81162
81274
  init_client3();
81163
81275
  init_errors2();
81164
81276
  init_exchange_credentials();
81165
- EXCHANGE_TTL_SECONDS = 5 * 60;
81166
81277
  MAXIMUM_RESPONSE_BYTES = 256 * 1024;
81167
81278
  });
81168
81279
 
@@ -81352,28 +81463,36 @@ var init_target2 = __esm(() => {
81352
81463
  });
81353
81464
 
81354
81465
  // src/connection/credential.ts
81355
- function createConnectionCredential(expectedSourceIssuer, source2) {
81466
+ function createConnectionCredential(expectedSourceIssuer, source2, ttlSeconds = DELEGATION_TTL_SECONDS) {
81467
+ if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1) {
81468
+ throw new TypeError("Connection credential ttlSeconds must be a positive safe integer.");
81469
+ }
81356
81470
  const resolveSource = source2.resolve.bind(source2);
81357
81471
  return Object.freeze({
81358
- ttlSeconds: DELEGATION_TTL_SECONDS,
81472
+ ttlSeconds,
81359
81473
  async resolve(_call, signal) {
81360
81474
  const resolved = await resolveSource(expectedSourceIssuer, signal);
81361
- const ttlSeconds = sourceBoundDelegationTtl(resolved);
81475
+ const delegatedTtlSeconds = sourceBoundDelegationTtl(resolved, ttlSeconds);
81362
81476
  return Object.freeze({
81363
81477
  credential: resolved,
81364
- ...ttlSeconds === undefined ? {} : { delegate: { ttlSeconds } }
81478
+ ...delegatedTtlSeconds === undefined ? {} : { delegate: { ttlSeconds: delegatedTtlSeconds } }
81365
81479
  });
81366
81480
  }
81367
81481
  });
81368
81482
  }
81369
- function sourceBoundDelegationTtl(input) {
81483
+ function sourceBoundDelegationTtl(input, requestedTtlSeconds) {
81370
81484
  try {
81371
81485
  const expiresAt = exports_credential.inspect(input).claims.exp;
81372
81486
  if (typeof expiresAt !== "number" || !Number.isSafeInteger(expiresAt))
81373
81487
  return;
81374
- const remaining = expiresAt - Math.ceil(Date.now() / 1000) - 1;
81375
- return Math.max(1, Math.min(DELEGATION_TTL_SECONDS, remaining));
81376
- } catch {
81488
+ const remaining = remainingCredentialLifetimeSeconds(expiresAt);
81489
+ if (remaining < requestedTtlSeconds) {
81490
+ throw new AstraleError("CREDENTIAL_LIFETIME_INSUFFICIENT", "The selected credential cannot cover the requested command timeout.", `Use a fresh identity session or a shorter --timeout; ${Math.max(0, remaining)} seconds remain but ${requestedTtlSeconds} are required.`);
81491
+ }
81492
+ return Math.max(1, Math.min(requestedTtlSeconds, remaining));
81493
+ } catch (cause) {
81494
+ if (cause instanceof AstraleError)
81495
+ throw cause;
81377
81496
  return;
81378
81497
  }
81379
81498
  }
@@ -81384,7 +81503,8 @@ function createCliCredential(target2, options, config2, fetch2 = globalThis.fetc
81384
81503
  const authOptions = Object.freeze({
81385
81504
  ...options.as === undefined ? {} : { as: options.as },
81386
81505
  ...options.creds === undefined ? {} : { creds: options.creds },
81387
- ...target2.defaultIdentity === undefined ? {} : { defaultIdentity: target2.defaultIdentity }
81506
+ ...target2.defaultIdentity === undefined ? {} : { defaultIdentity: target2.defaultIdentity },
81507
+ minimumRemainingSeconds: target2.domainIssuer === undefined ? invocationCredentialTtlSeconds(timeoutMs) : exchangeCredentialTtlSeconds(timeoutMs)
81388
81508
  });
81389
81509
  const source2 = {
81390
81510
  async resolve(audience, signal) {
@@ -81395,7 +81515,8 @@ function createCliCredential(target2, options, config2, fetch2 = globalThis.fetc
81395
81515
  }
81396
81516
  };
81397
81517
  const effective = target2.domainIssuer === undefined || options.creds !== undefined ? source2 : createExchangeCredentialResolver({ ...target2, domainIssuer: target2.domainIssuer }, source2, fetch2, timeoutMs);
81398
- return createConnectionCredential(target2.kernelIssuer, effective);
81518
+ const ttlSeconds = invocationCredentialTtlSeconds(timeoutMs);
81519
+ return createConnectionCredential(target2.kernelIssuer, effective, ttlSeconds);
81399
81520
  }
81400
81521
  function validateCredentialSelection(options) {
81401
81522
  if (options.as !== undefined && options.creds !== undefined) {
@@ -82689,7 +82810,7 @@ Behavior:
82689
82810
  script installs only — if Astrale was installed by another package manager this
82690
82811
  command refuses so that manager stays in charge; downloads are checksum-verified
82691
82812
  before the binary is replaced. (2) The Astrale agent skills: installs every
82692
- top-level skill published from astrale-os/cli main, updates healthy older
82813
+ top-level skill published from one resolved astrale-os/cli main commit, updates healthy older
82693
82814
  installs, repairs inconsistent installs, and verifies the result before
82694
82815
  reporting success. (3) SDK deps: inside a pnpm domain
82695
82816
  project, proposes any @astrale-os/* dependency with a newer release and, on
@@ -83288,20 +83409,20 @@ var init_query5 = __esm(() => {
83288
83409
  function prepareQuery(input) {
83289
83410
  const limit = positiveInteger2(input.limit, "--limit", DEFAULT_LIMIT);
83290
83411
  if (input.ast !== undefined) {
83291
- if (input.sources.length > 0 || input.definition !== undefined || input.edge !== undefined || input.direction !== undefined) {
83292
- throw new TypeError("--ast/--file cannot be combined with sources, --definition, --edge, or --direction");
83412
+ if (input.sources.length > 0 || input.class !== undefined || input.edge !== undefined || input.direction !== undefined) {
83413
+ throw new TypeError("--ast/--file cannot be combined with sources, --class, --edge, or --direction");
83293
83414
  }
83294
83415
  return withPage(QueryAST.decode(input.ast), limit, input.cursor);
83295
83416
  }
83296
- if (input.sources.length === 0 && input.definition === undefined) {
83297
- throw new TypeError("query requires a Path source, --definition, or a canonical Query V6 document");
83417
+ if (input.sources.length === 0 && input.class === undefined) {
83418
+ throw new TypeError("query requires a Path source, --class, or a canonical Query V6 document");
83298
83419
  }
83299
83420
  if (input.direction !== undefined && input.edge === undefined) {
83300
83421
  throw new TypeError("--direction requires --edge");
83301
83422
  }
83302
83423
  const paths3 = input.sources.map((source2) => Path.parse(source2));
83303
- const definition2 = input.definition === undefined ? undefined : classReference(input.definition, "--definition");
83304
- const nodes = definition2 === undefined ? paths3 : [...paths3, definition2];
83424
+ const selectedClass = input.class === undefined ? undefined : classReference(input.class, "--class");
83425
+ const nodes = selectedClass === undefined ? paths3 : [...paths3, selectedClass];
83305
83426
  const query4 = Query.from({
83306
83427
  nodes
83307
83428
  });
@@ -83477,7 +83598,7 @@ async function queryCommand(sources2, opts) {
83477
83598
  input = {
83478
83599
  sources: sources2,
83479
83600
  ...ast === undefined ? {} : { ast },
83480
- definition: opts.definition,
83601
+ class: opts.class,
83481
83602
  edge: opts.edge,
83482
83603
  direction: opts.direction,
83483
83604
  limit: opts.limit,
@@ -83547,7 +83668,7 @@ var init_query7 = __esm(() => {
83547
83668
  description: "Run one canonical Query V6 graph read",
83548
83669
  afterHelpText: `
83549
83670
  Behavior:
83550
- Positional Paths and --definition author a finite Query V6 read. --edge adds
83671
+ Positional Paths and --class author a finite Query V6 read. --edge adds
83551
83672
  one exact Edge-Class expansion; --direction defaults to outgoing. --ast and
83552
83673
  --file accept a complete canonical astrale.graph.query/v6 document, including
83553
83674
  Property ordering and Node or Edge reference/value projections. --cursor resumes
@@ -83559,7 +83680,7 @@ Behavior:
83559
83680
 
83560
83681
  Examples:
83561
83682
  $ astrale query /:notes.example.dev:class.Note --limit 50
83562
- $ astrale query --definition /:notes.example.dev:class.Note --limit 50
83683
+ $ astrale query --class /:notes.example.dev:class.Note --limit 50
83563
83684
  $ astrale query @note --edge /:notes.example.dev:class.references --direction outgoing --limit 25
83564
83685
  $ astrale query --file query.v6.json --cursor "$CURSOR"
83565
83686
  `,
@@ -83568,7 +83689,7 @@ Examples:
83568
83689
  { flags: "--ast <json>", description: "Canonical Query V6 JSON document" },
83569
83690
  { flags: "-f, --file <path>", description: "Read a canonical Query V6 document from a file" },
83570
83691
  {
83571
- flags: "--definition <path>",
83692
+ flags: "--class <path>",
83572
83693
  description: "Select Nodes satisfying one exact Class"
83573
83694
  },
83574
83695
  { flags: "--edge <class>", description: "Expand one exact Edge Class" },