@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.
- package/README.md +5 -1
- package/dist/astrale.js +178 -57
- package/dist/public/connect-core.js +42 -13
- package/dist/public/keys/index.js +14 -0
- package/dist/public/paths/index.js +14 -0
- package/dist/types/connection/auth.d.ts +1 -0
- package/dist/types/connection/credential.d.ts +1 -1
- package/dist/types/connection/lifetime.d.ts +6 -0
- package/dist/types/lib/credential-lifetime.d.ts +3 -0
- package/dist/types/lib/idp-session.d.ts +2 -0
- package/dist/types/lib/idp.d.ts +1 -1
- package/dist/types/state/exchange-credentials.d.ts +1 -1
- package/package.json +1 -1
- package/src/commands/__tests__/read-commands.test.ts +2 -2
- package/src/commands/query.ts +5 -5
- package/src/commands/update.ts +3 -2
- package/src/connection/.spec/architecture.md +5 -2
- package/src/connection/.spec/flows/session.ts +2 -1
- package/src/connection/.spec/laws/connection.ts +9 -1
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +115 -0
- package/src/connection/__tests__/exchange.test.ts +94 -9
- package/src/connection/auth.ts +20 -4
- package/src/connection/credential.ts +29 -8
- package/src/connection/exchange.ts +65 -7
- package/src/connection/lifetime.ts +25 -0
- package/src/graph/.spec/api.d.ts +1 -1
- package/src/graph/.spec/laws/documents.ts +7 -7
- package/src/graph/__tests__/query.test.ts +12 -5
- package/src/graph/query.ts +9 -11
- package/src/lib/__tests__/idp-session.driver.ts +10 -3
- package/src/lib/__tests__/idp-session.test.ts +25 -0
- package/src/lib/__tests__/skills.test.ts +27 -2
- package/src/lib/credential-lifetime.ts +24 -0
- package/src/lib/idp-session.ts +26 -4
- package/src/lib/idp.ts +24 -4
- package/src/lib/skills/sync.ts +72 -8
- package/src/program/__tests__/program.test.ts +39 -2
- package/src/state/__tests__/exchange-credentials.test.ts +112 -5
- package/src/state/exchange-credentials.ts +26 -8
- package/studio/server/cli-consumers.test.ts +1 -1
- package/studio/server/cli.test.ts +2 -2
- 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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
34167
|
+
const pending = this.getOrRefreshOnce(key, encoded, minimumRemainingSeconds, refresh, now).finally(() => {
|
|
34168
|
+
this.refreshing.delete(pendingKey);
|
|
34151
34169
|
});
|
|
34152
|
-
this.refreshing.set(
|
|
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(),
|
|
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
|
|
38213
|
+
return tokenHasMinimumLifetime(session, minimumRemainingMs) ? session.access_token : undefined;
|
|
38196
38214
|
}
|
|
38197
38215
|
const entry = session.tokens?.[audience];
|
|
38198
|
-
if (entry &&
|
|
38216
|
+
if (entry && tokenHasMinimumLifetime(entry, minimumRemainingMs))
|
|
38199
38217
|
return entry.access_token;
|
|
38200
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
81472
|
+
ttlSeconds,
|
|
81359
81473
|
async resolve(_call, signal) {
|
|
81360
81474
|
const resolved = await resolveSource(expectedSourceIssuer, signal);
|
|
81361
|
-
const
|
|
81475
|
+
const delegatedTtlSeconds = sourceBoundDelegationTtl(resolved, ttlSeconds);
|
|
81362
81476
|
return Object.freeze({
|
|
81363
81477
|
credential: resolved,
|
|
81364
|
-
...
|
|
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
|
|
81375
|
-
|
|
81376
|
-
|
|
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
|
-
|
|
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.
|
|
83292
|
-
throw new TypeError("--ast/--file cannot be combined with sources, --
|
|
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.
|
|
83297
|
-
throw new TypeError("query requires a Path source, --
|
|
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
|
|
83304
|
-
const nodes =
|
|
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
|
-
|
|
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 --
|
|
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 --
|
|
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: "--
|
|
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" },
|