@topolo/cli 0.13.11 → 0.13.12
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/dist/index.js
CHANGED
|
@@ -8,6 +8,28 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
function registerAgentPacksCommand(program2) {
|
|
10
10
|
const packs = program2.command("agent-packs").description("Install and operate Topolo Agent Packs in supported local hosts").option("--store <path>", "Portable pack library (default ~/AgentPacks)");
|
|
11
|
+
packs.command("runtime-adoption").description("Plan or apply an owned private runtime configuration; never starts or authenticates a consumer").argument("<operation>", "plan or apply").requiredOption("--manifest <path>", "Closed owner-scoped manifest").option("--plan-digest <sha256>", "Exact reviewed plan digest required for apply").option("--confirm", "Confirm the configuration-only apply").allowExcessArguments(false).action((operation, options) => {
|
|
12
|
+
if (!["plan", "apply"].includes(operation) || !options.manifest.startsWith("/") || (operation === "apply" ? !/^[a-f0-9]{64}$/.test(options.planDigest ?? "") || !options.confirm : options.planDigest !== void 0 || options.confirm)) throw new Error("Exact runtime-adoption plan or confirmed apply required.");
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const adapter = [resolve(here, "../agent-packs/topolo-agent-pack"), resolve(here, "../../agent-packs/topolo-agent-pack")].find(existsSync);
|
|
15
|
+
if (!adapter) throw new Error("Installed bundled pack adapter unavailable.");
|
|
16
|
+
const result = spawnSync(
|
|
17
|
+
"python3",
|
|
18
|
+
[
|
|
19
|
+
adapter,
|
|
20
|
+
"--store",
|
|
21
|
+
packs.opts().store ?? resolve(homedir(), "AgentPacks"),
|
|
22
|
+
"runtime-adoption",
|
|
23
|
+
operation,
|
|
24
|
+
"--manifest",
|
|
25
|
+
options.manifest,
|
|
26
|
+
...operation === "apply" ? ["--plan-digest", options.planDigest, "--confirm"] : []
|
|
27
|
+
],
|
|
28
|
+
{ stdio: "inherit", env: { PATH: process.env.PATH, HOME: homedir(), PYTHONDONTWRITEBYTECODE: "1" } }
|
|
29
|
+
);
|
|
30
|
+
if (result.error) throw new Error("Python 3.11 or later is required for runtime adoption.");
|
|
31
|
+
if (result.status !== 0) process.exitCode = result.status ?? 2;
|
|
32
|
+
});
|
|
11
33
|
packs.command("smoke").description("Start or read one closed, bound native acceptance case").argument("<operation>", "start or read").argument("<identity>", "Owned role key for start, existing operation UUID for read").option("--case <id>", "Finite compiled acceptance case").option("--operation-id <id>", "Unique native operation UUID for start").option("--harness <name>", "codex or claude", "codex").allowExcessArguments(false).action(async (operation, identity, options) => {
|
|
12
34
|
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
13
35
|
if (!["start", "read"].includes(operation) || !["codex", "claude"].includes(options.harness) || (operation === "start" ? !/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(identity) || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(options.case ?? "") || !uuid.test(options.operationId ?? "") : !uuid.test(identity) || options.case !== void 0 || options.operationId !== void 0))
|
|
@@ -209,7 +231,7 @@ import { platform as platform2 } from "os";
|
|
|
209
231
|
import { createInterface } from "readline/promises";
|
|
210
232
|
import { stdin as input } from "process";
|
|
211
233
|
|
|
212
|
-
// node_modules/.pnpm/@topolo+sdk@0.23.
|
|
234
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.22/node_modules/@topolo/sdk/dist/index.js
|
|
213
235
|
function record(value) {
|
|
214
236
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
215
237
|
}
|
|
@@ -288,6 +310,7 @@ var PUBLIC_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
288
310
|
"organization_enrollment_required",
|
|
289
311
|
"permission_denied",
|
|
290
312
|
"rate_limited",
|
|
313
|
+
"reauth_required",
|
|
291
314
|
"request_failed",
|
|
292
315
|
"request_timeout",
|
|
293
316
|
"service_catalog_unavailable",
|
|
@@ -649,6 +672,22 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
649
672
|
);
|
|
650
673
|
}
|
|
651
674
|
};
|
|
675
|
+
function topoloRetryAfterSeconds(value, now = Date.now()) {
|
|
676
|
+
if (!value) return void 0;
|
|
677
|
+
if (/^\d{1,10}$/.test(value)) return Number(value);
|
|
678
|
+
const at = Date.parse(value);
|
|
679
|
+
return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - now) / 1e3));
|
|
680
|
+
}
|
|
681
|
+
var TopoloRateLimitError = class extends TopoloHttpError {
|
|
682
|
+
retryable = true;
|
|
683
|
+
retryAfterSeconds;
|
|
684
|
+
constructor(service, path, body, message2, options = {}) {
|
|
685
|
+
super(service, path, 429, body, message2, options);
|
|
686
|
+
this.name = "TopoloRateLimitError";
|
|
687
|
+
if (this.code === "http_error") this.code = "rate_limited";
|
|
688
|
+
this.retryAfterSeconds = topoloRetryAfterSeconds(this.retryAfter);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
652
691
|
var ACTION_SECRET_PREFIX = "topolo-secret:";
|
|
653
692
|
function isActionSecretReference(value) {
|
|
654
693
|
return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
|
|
@@ -1619,6 +1658,10 @@ function isRecord3(value) {
|
|
|
1619
1658
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1620
1659
|
}
|
|
1621
1660
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1661
|
+
var IDEMPOTENT_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1662
|
+
var RATE_LIMIT_READ_RETRIES = 2;
|
|
1663
|
+
var RATE_LIMIT_READ_RETRY_MAX_DELAY_MS = 1e4;
|
|
1664
|
+
var RATE_LIMIT_READ_DEFAULT_DELAY_MS = 1e3;
|
|
1622
1665
|
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
1623
1666
|
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
1624
1667
|
var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
@@ -1815,6 +1858,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
1815
1858
|
res = await fetchWithReadRetry();
|
|
1816
1859
|
}
|
|
1817
1860
|
}
|
|
1861
|
+
for (let retry = 0; res.status === 429 && IDEMPOTENT_READ_METHODS.has(method) && retry < RATE_LIMIT_READ_RETRIES; retry += 1) {
|
|
1862
|
+
const header = canonicalTopoloRetryAfter(res.headers.get("Retry-After"));
|
|
1863
|
+
const seconds = topoloRetryAfterSeconds(header);
|
|
1864
|
+
const delayMs = header === void 0 ? RATE_LIMIT_READ_DEFAULT_DELAY_MS * (retry + 1) : (seconds ?? Infinity) * 1e3;
|
|
1865
|
+
if (delayMs > RATE_LIMIT_READ_RETRY_MAX_DELAY_MS || opts.signal?.aborted) break;
|
|
1866
|
+
await res.body?.cancel().catch(() => void 0);
|
|
1867
|
+
await new Promise((resolve5) => setTimeout(resolve5, delayMs));
|
|
1868
|
+
res = await fetchWithReadRetry();
|
|
1869
|
+
}
|
|
1818
1870
|
} catch (err) {
|
|
1819
1871
|
this.emit({
|
|
1820
1872
|
phase: "error",
|
|
@@ -1880,6 +1932,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
1880
1932
|
{ ...errorContext, code: extractTopoloServiceErrorCode(parsed) ?? "permission_denied" }
|
|
1881
1933
|
);
|
|
1882
1934
|
}
|
|
1935
|
+
if (res.status === 429) {
|
|
1936
|
+
throw new TopoloRateLimitError(
|
|
1937
|
+
resolvedService.serviceKey,
|
|
1938
|
+
opts.path,
|
|
1939
|
+
parsed,
|
|
1940
|
+
message2,
|
|
1941
|
+
errorContext
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1883
1944
|
throw new TopoloHttpError(
|
|
1884
1945
|
resolvedService.serviceKey,
|
|
1885
1946
|
opts.path,
|
|
@@ -3825,8 +3886,8 @@ async function pollUntilApproved(params) {
|
|
|
3825
3886
|
throw new Error("Device code expired. Run `topolo auth login` again.");
|
|
3826
3887
|
}
|
|
3827
3888
|
|
|
3828
|
-
// node_modules/.pnpm/@topolo+sdk@0.23.
|
|
3829
|
-
import { mkdir as mkdir2, readFile as readFile2,
|
|
3889
|
+
// node_modules/.pnpm/@topolo+sdk@0.23.22/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3890
|
+
import { mkdir as mkdir2, readFile as readFile2, chmod as chmod2, lstat, readdir as readdir2 } from "fs/promises";
|
|
3830
3891
|
import { existsSync as existsSync2, mkdirSync, lstatSync, readdirSync, readFileSync as readFileSync2, openSync, writeFileSync, fsyncSync, closeSync, renameSync, unlinkSync, rmdirSync, linkSync } from "fs";
|
|
3831
3892
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
3832
3893
|
import { constants, fstatSync, readSync } from "fs";
|
|
@@ -3882,6 +3943,7 @@ var PUBLIC_ERROR_CODES2 = /* @__PURE__ */ new Set([
|
|
|
3882
3943
|
"organization_enrollment_required",
|
|
3883
3944
|
"permission_denied",
|
|
3884
3945
|
"rate_limited",
|
|
3946
|
+
"reauth_required",
|
|
3885
3947
|
"request_failed",
|
|
3886
3948
|
"request_timeout",
|
|
3887
3949
|
"service_catalog_unavailable",
|
|
@@ -4141,6 +4203,30 @@ var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
|
4141
4203
|
);
|
|
4142
4204
|
}
|
|
4143
4205
|
};
|
|
4206
|
+
function topoloRetryAfterSeconds2(value, now = Date.now()) {
|
|
4207
|
+
if (!value) return void 0;
|
|
4208
|
+
if (/^\d{1,10}$/.test(value)) return Number(value);
|
|
4209
|
+
const at = Date.parse(value);
|
|
4210
|
+
return Number.isNaN(at) ? void 0 : Math.max(0, Math.ceil((at - now) / 1e3));
|
|
4211
|
+
}
|
|
4212
|
+
var TopoloRateLimitError2 = class extends TopoloHttpError2 {
|
|
4213
|
+
retryable = true;
|
|
4214
|
+
retryAfterSeconds;
|
|
4215
|
+
constructor(service, path, body, message2, options = {}) {
|
|
4216
|
+
super(service, path, 429, body, message2, options);
|
|
4217
|
+
this.name = "TopoloRateLimitError";
|
|
4218
|
+
if (this.code === "http_error") this.code = "rate_limited";
|
|
4219
|
+
this.retryAfterSeconds = topoloRetryAfterSeconds2(this.retryAfter);
|
|
4220
|
+
}
|
|
4221
|
+
};
|
|
4222
|
+
var TopoloReauthRequiredError = class extends TopoloAuthError2 {
|
|
4223
|
+
reauthorization;
|
|
4224
|
+
constructor(message2, reauthorization, options = {}) {
|
|
4225
|
+
super(message2, "reauth_required", { status: 401, service: "auth", path: "/api/developer-oauth/token", ...options });
|
|
4226
|
+
this.name = "TopoloReauthRequiredError";
|
|
4227
|
+
this.reauthorization = Object.freeze({ ...reauthorization });
|
|
4228
|
+
}
|
|
4229
|
+
};
|
|
4144
4230
|
var APP_API_URLS2 = {
|
|
4145
4231
|
auth: "https://auth.topolo.app",
|
|
4146
4232
|
developers: "https://developers.topolo.app"
|
|
@@ -4589,7 +4675,9 @@ async function readConfig() {
|
|
|
4589
4675
|
async function writeConfig(config) {
|
|
4590
4676
|
const path = configPath();
|
|
4591
4677
|
await mkdir2(dirname2(path), { recursive: true });
|
|
4592
|
-
await
|
|
4678
|
+
await withConfigWriteLock(async (_assertOwned, persist) => {
|
|
4679
|
+
await persist((current) => protectStoredGrants(config, current), { allowCreate: true });
|
|
4680
|
+
});
|
|
4593
4681
|
if (platform() !== "win32") {
|
|
4594
4682
|
try {
|
|
4595
4683
|
await chmod2(path, 384);
|
|
@@ -4597,6 +4685,142 @@ async function writeConfig(config) {
|
|
|
4597
4685
|
}
|
|
4598
4686
|
}
|
|
4599
4687
|
}
|
|
4688
|
+
var DEFAULT_SLOT = "@default";
|
|
4689
|
+
var RETIRED_MAX_ENTRIES = 512;
|
|
4690
|
+
var RETIRED_RETENTION_SECONDS = 30 * 24 * 3600;
|
|
4691
|
+
var TERMINAL_RENEWAL_CODES = /* @__PURE__ */ new Set(["invalid_grant", "expired_token"]);
|
|
4692
|
+
function refreshTokenFingerprint(refreshToken) {
|
|
4693
|
+
return createHash2("sha256").update("topolo-refresh-token:v1:" + refreshToken).digest("hex").slice(0, 32);
|
|
4694
|
+
}
|
|
4695
|
+
function clone(value) {
|
|
4696
|
+
return value === void 0 ? value : JSON.parse(JSON.stringify(value));
|
|
4697
|
+
}
|
|
4698
|
+
function nowSeconds() {
|
|
4699
|
+
return Math.floor(Date.now() / 1e3);
|
|
4700
|
+
}
|
|
4701
|
+
function retiredEntry(config, refreshToken) {
|
|
4702
|
+
if (!refreshToken) return void 0;
|
|
4703
|
+
const entry = config.retiredRefreshTokens?.[refreshTokenFingerprint(refreshToken)];
|
|
4704
|
+
return entry && typeof entry === "object" ? entry : void 0;
|
|
4705
|
+
}
|
|
4706
|
+
function retire(config, refreshToken, reason) {
|
|
4707
|
+
const now = nowSeconds();
|
|
4708
|
+
const entries = Object.entries({ ...config.retiredRefreshTokens ?? {}, [refreshTokenFingerprint(refreshToken)]: { reason, at: now } }).filter(([key, value]) => /^[a-f0-9]{32}$/.test(key) && value && Number.isFinite(value.at) && value.at > now - RETIRED_RETENTION_SECONDS).sort(([, a], [, b]) => b.at - a.at).slice(0, RETIRED_MAX_ENTRIES);
|
|
4709
|
+
config.retiredRefreshTokens = Object.fromEntries(entries);
|
|
4710
|
+
}
|
|
4711
|
+
function environmentSlots(config, environment) {
|
|
4712
|
+
const slots = [];
|
|
4713
|
+
const profile = config.profiles?.[environment];
|
|
4714
|
+
if (environment === "production" && (config.credential || !profile)) {
|
|
4715
|
+
slots.push({ holder: config, key: profile?.activeContext ?? DEFAULT_SLOT, path: "credential" });
|
|
4716
|
+
}
|
|
4717
|
+
if (profile) {
|
|
4718
|
+
slots.push({ holder: profile, key: profile.activeContext ?? DEFAULT_SLOT, path: `profiles.${environment}.credential` });
|
|
4719
|
+
for (const [key, context] of Object.entries(profile.contexts ?? {})) {
|
|
4720
|
+
if (context) slots.push({ holder: context, key, path: `profiles.${environment}.contexts.${key}.credential` });
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
return slots;
|
|
4724
|
+
}
|
|
4725
|
+
function allSlots(config) {
|
|
4726
|
+
const environments = /* @__PURE__ */ new Set(["production", ...Object.keys(config.profiles ?? {})]);
|
|
4727
|
+
return [...environments].flatMap((environment) => environmentSlots(config, environment));
|
|
4728
|
+
}
|
|
4729
|
+
function slotRefreshToken(slot) {
|
|
4730
|
+
const credential = slot.holder.credential;
|
|
4731
|
+
return credential?.kind === "access_token" ? credential.refreshToken : void 0;
|
|
4732
|
+
}
|
|
4733
|
+
function replaceGrant(config, environment, refreshToken, replacement) {
|
|
4734
|
+
const touched = /* @__PURE__ */ new Set();
|
|
4735
|
+
for (const slot of environmentSlots(config, environment)) {
|
|
4736
|
+
if (slotRefreshToken(slot) !== refreshToken) continue;
|
|
4737
|
+
if (replacement) slot.holder.credential = clone(replacement);
|
|
4738
|
+
else delete slot.holder.credential;
|
|
4739
|
+
touched.add(slot.key);
|
|
4740
|
+
}
|
|
4741
|
+
return touched;
|
|
4742
|
+
}
|
|
4743
|
+
function usableCredential(config, credential) {
|
|
4744
|
+
if (!credential) return false;
|
|
4745
|
+
if (credential.kind !== "access_token" || !credential.refreshToken) return true;
|
|
4746
|
+
return !retiredEntry(config, credential.refreshToken);
|
|
4747
|
+
}
|
|
4748
|
+
function slotCredential(profile, key) {
|
|
4749
|
+
if (key === DEFAULT_SLOT) return profile.credential;
|
|
4750
|
+
return profile.contexts?.[key]?.credential ?? (profile.activeContext === key ? profile.credential : void 0);
|
|
4751
|
+
}
|
|
4752
|
+
function protectStoredGrants(next, current) {
|
|
4753
|
+
const result = clone(next) ?? {};
|
|
4754
|
+
const retired = current.retiredRefreshTokens;
|
|
4755
|
+
const currentSlots = new Map(allSlots(current).map((slot) => [slot.path, slot.holder.credential]));
|
|
4756
|
+
for (const slot of allSlots(result)) {
|
|
4757
|
+
const token = slotRefreshToken(slot);
|
|
4758
|
+
if (!token || !retired?.[refreshTokenFingerprint(token)]) continue;
|
|
4759
|
+
const stored = currentSlots.get(slot.path);
|
|
4760
|
+
if (stored) slot.holder.credential = clone(stored);
|
|
4761
|
+
else delete slot.holder.credential;
|
|
4762
|
+
}
|
|
4763
|
+
if (retired) result.retiredRefreshTokens = clone(retired);
|
|
4764
|
+
else delete result.retiredRefreshTokens;
|
|
4765
|
+
for (const environment of /* @__PURE__ */ new Set([...Object.keys(result.profiles ?? {}), ...Object.keys(current.profiles ?? {})])) {
|
|
4766
|
+
const stored = current.profiles?.[environment]?.reauthorization;
|
|
4767
|
+
const profile = result.profiles?.[environment];
|
|
4768
|
+
if (!profile) continue;
|
|
4769
|
+
const kept = Object.fromEntries(Object.entries(stored ?? {}).filter(([key]) => !usableCredential(result, slotCredential(profile, key))));
|
|
4770
|
+
if (Object.keys(kept).length) profile.reauthorization = clone(kept);
|
|
4771
|
+
else delete profile.reauthorization;
|
|
4772
|
+
}
|
|
4773
|
+
return result;
|
|
4774
|
+
}
|
|
4775
|
+
function setReauthorization(config, environment, key, update) {
|
|
4776
|
+
const profiles = config.profiles ??= {};
|
|
4777
|
+
const profile = profiles[environment] ??= {};
|
|
4778
|
+
const map = { ...profile.reauthorization ?? {} };
|
|
4779
|
+
const value = update(map[key]);
|
|
4780
|
+
if (value) map[key] = value;
|
|
4781
|
+
else delete map[key];
|
|
4782
|
+
if (Object.keys(map).length) profile.reauthorization = map;
|
|
4783
|
+
else delete profile.reauthorization;
|
|
4784
|
+
}
|
|
4785
|
+
function loginCommand(environment, key) {
|
|
4786
|
+
const base = environment === "production" ? "topolo auth login" : `topolo --env ${environment} auth login`;
|
|
4787
|
+
if (!key.startsWith("org:")) return base;
|
|
4788
|
+
const use = environment === "production" ? "topolo context use" : `topolo --env ${environment} context use`;
|
|
4789
|
+
return `${base} && ${use} ${key.slice(4)}`;
|
|
4790
|
+
}
|
|
4791
|
+
function contextLabel(key) {
|
|
4792
|
+
return key === DEFAULT_SLOT ? "default" : key;
|
|
4793
|
+
}
|
|
4794
|
+
function reauthRequired(environment, key, entry, state) {
|
|
4795
|
+
const pending = entry?.pending;
|
|
4796
|
+
const command = loginCommand(environment, key);
|
|
4797
|
+
const reason = entry?.reason ?? "invalid_grant";
|
|
4798
|
+
const head = `The saved Topolo sign-in for ${environment} context ${contextLabel(key)} has ended (${reason}) and is not renewed again.`;
|
|
4799
|
+
let action;
|
|
4800
|
+
if (state === "awaiting_approval" && pending) {
|
|
4801
|
+
action = ` Approve the new sign-in: open ${pending.verificationUriComplete ?? pending.verificationUri} and confirm code ${pending.userCode} (expires ${new Date(pending.expiresAt * 1e3).toISOString()}). The next call resumes once approved.`;
|
|
4802
|
+
} else if (state === "organization_mismatch") {
|
|
4803
|
+
action = ` The last approval was for ${entry?.mismatch?.approvedContext ?? "another context"}, not ${contextLabel(key)}; approve again with that organization selected.`;
|
|
4804
|
+
} else if (state === "denied") {
|
|
4805
|
+
action = " The last approval was denied.";
|
|
4806
|
+
} else {
|
|
4807
|
+
action = "";
|
|
4808
|
+
}
|
|
4809
|
+
return new TopoloReauthRequiredError(`${head}${action} Or sign in again: ${command} (context ${contextLabel(key)}).`, {
|
|
4810
|
+
environment,
|
|
4811
|
+
context: contextLabel(key),
|
|
4812
|
+
reason,
|
|
4813
|
+
state,
|
|
4814
|
+
...state === "awaiting_approval" && pending ? {
|
|
4815
|
+
verificationUri: pending.verificationUri,
|
|
4816
|
+
...pending.verificationUriComplete ? { verificationUriComplete: pending.verificationUriComplete } : {},
|
|
4817
|
+
userCode: pending.userCode,
|
|
4818
|
+
expiresAt: new Date(pending.expiresAt * 1e3).toISOString(),
|
|
4819
|
+
pollIntervalSeconds: pending.interval
|
|
4820
|
+
} : {},
|
|
4821
|
+
loginCommand: command
|
|
4822
|
+
});
|
|
4823
|
+
}
|
|
4600
4824
|
function normalizeEnvironment(value) {
|
|
4601
4825
|
const normalized = (value ?? "production").trim().toLowerCase();
|
|
4602
4826
|
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
@@ -4694,7 +4918,19 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
4694
4918
|
}
|
|
4695
4919
|
var REFRESH_SKEW_SECONDS = 60;
|
|
4696
4920
|
var CONFIG_LOCK_RETRY_MS = 25;
|
|
4697
|
-
var
|
|
4921
|
+
var CONFIG_LOCK_DEFAULT_TIMEOUT_MS = 15e3;
|
|
4922
|
+
var AUTH_EXCHANGE_TIMEOUT_MS = 1e4;
|
|
4923
|
+
function configLockTimeoutMs() {
|
|
4924
|
+
const value = Number(process.env.TOPOLO_CONFIG_LOCK_TIMEOUT_MS);
|
|
4925
|
+
return Number.isFinite(value) && value >= 1e3 && value <= 12e4 ? value : CONFIG_LOCK_DEFAULT_TIMEOUT_MS;
|
|
4926
|
+
}
|
|
4927
|
+
function boundedOAuth(serviceUrls) {
|
|
4928
|
+
const fetchImpl = (input2, init) => globalThis.fetch(input2, {
|
|
4929
|
+
...init,
|
|
4930
|
+
signal: init?.signal ?? AbortSignal.timeout(AUTH_EXCHANGE_TIMEOUT_MS)
|
|
4931
|
+
});
|
|
4932
|
+
return new TopoloOAuth2({ serviceUrls, fetch: fetchImpl });
|
|
4933
|
+
}
|
|
4698
4934
|
var CONFIG_LOCK_DEAD_OWNER_MIN_AGE_MS = 2e3;
|
|
4699
4935
|
var ConfigOwnershipError = class extends TopoloSdkError2 {
|
|
4700
4936
|
constructor() {
|
|
@@ -4822,6 +5058,7 @@ function reclaimDeadOwnerLock(lockPath, markerName, marker) {
|
|
|
4822
5058
|
return reclaimed;
|
|
4823
5059
|
}
|
|
4824
5060
|
var RELEASE_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
5061
|
+
var PERSIST_ATTEMPTS = 3;
|
|
4825
5062
|
async function withConfigWriteLock(operation) {
|
|
4826
5063
|
const lockPath = `${configPath()}.lock`;
|
|
4827
5064
|
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
@@ -4842,7 +5079,7 @@ async function withConfigWriteLock(operation) {
|
|
|
4842
5079
|
} catch (error) {
|
|
4843
5080
|
if (error.code !== "EEXIST") throw error;
|
|
4844
5081
|
if (reclaimDeadOwnerLock(lockPath, markerName, marker)) break;
|
|
4845
|
-
if (performance.now() - startedAt >=
|
|
5082
|
+
if (performance.now() - startedAt >= configLockTimeoutMs()) {
|
|
4846
5083
|
throw new TopoloSdkError2("state_conflict", "Credential configuration is still held or unresolved. This waiter did not dispatch a renewal.");
|
|
4847
5084
|
}
|
|
4848
5085
|
await new Promise((resolve22) => setTimeout(resolve22, CONFIG_LOCK_RETRY_MS));
|
|
@@ -4881,31 +5118,54 @@ async function withConfigWriteLock(operation) {
|
|
|
4881
5118
|
throw new ConfigOwnershipError();
|
|
4882
5119
|
}
|
|
4883
5120
|
};
|
|
4884
|
-
const persist = async (update) => {
|
|
5121
|
+
const persist = async (update, options = {}) => {
|
|
4885
5122
|
const path = configPath();
|
|
4886
5123
|
await mkdir2(dirname2(path), { recursive: true });
|
|
4887
|
-
const
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
5124
|
+
const snapshot = () => {
|
|
5125
|
+
let entry;
|
|
5126
|
+
try {
|
|
5127
|
+
entry = lstatSync(path);
|
|
5128
|
+
} catch (error) {
|
|
5129
|
+
if (options.allowCreate && error.code === "ENOENT") return null;
|
|
5130
|
+
throw new ConfigOwnershipError();
|
|
5131
|
+
}
|
|
4892
5132
|
if (!entry.isFile() || entry.isSymbolicLink()) throw new ConfigOwnershipError();
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
5133
|
+
return readFileSync2(path, "utf8");
|
|
5134
|
+
};
|
|
5135
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
5136
|
+
const temporary = join3(dirname2(path), ".credential-write-" + randomUUID2());
|
|
5137
|
+
let temporaryOwned = false;
|
|
4897
5138
|
try {
|
|
4898
|
-
|
|
4899
|
-
|
|
5139
|
+
assertOwnedSync();
|
|
5140
|
+
const before = snapshot();
|
|
5141
|
+
let current = {};
|
|
5142
|
+
if (before !== null) {
|
|
5143
|
+
try {
|
|
5144
|
+
current = JSON.parse(before);
|
|
5145
|
+
} catch (error) {
|
|
5146
|
+
if (!options.allowCreate) throw error;
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
const updated = update(current);
|
|
5150
|
+
const fd = openSync(temporary, "wx", 384);
|
|
5151
|
+
temporaryOwned = true;
|
|
5152
|
+
try {
|
|
5153
|
+
writeFileSync(fd, JSON.stringify(updated, null, 2) + "\n");
|
|
5154
|
+
fsyncSync(fd);
|
|
5155
|
+
} finally {
|
|
5156
|
+
closeSync(fd);
|
|
5157
|
+
}
|
|
5158
|
+
assertOwnedSync();
|
|
5159
|
+
if (snapshot() !== before) {
|
|
5160
|
+
if (attempt < PERSIST_ATTEMPTS) continue;
|
|
5161
|
+
throw new ConfigOwnershipError();
|
|
5162
|
+
}
|
|
5163
|
+
renameSync(temporary, path);
|
|
5164
|
+
temporaryOwned = false;
|
|
5165
|
+
return;
|
|
4900
5166
|
} finally {
|
|
4901
|
-
|
|
5167
|
+
if (temporaryOwned) unlinkSync(temporary);
|
|
4902
5168
|
}
|
|
4903
|
-
assertOwnedSync();
|
|
4904
|
-
if (readFileSync2(path, "utf8") !== before) throw new ConfigOwnershipError();
|
|
4905
|
-
renameSync(temporary, path);
|
|
4906
|
-
temporaryOwned = false;
|
|
4907
|
-
} finally {
|
|
4908
|
-
if (temporaryOwned) unlinkSync(temporary);
|
|
4909
5169
|
}
|
|
4910
5170
|
};
|
|
4911
5171
|
const release = () => {
|
|
@@ -4966,6 +5226,17 @@ async function withConfigWriteLock(operation) {
|
|
|
4966
5226
|
function renewalFailure(error) {
|
|
4967
5227
|
const path = "/api/developer-oauth/token";
|
|
4968
5228
|
if (error instanceof ConfigOwnershipError) return new ConfigOwnershipError();
|
|
5229
|
+
if (error instanceof TopoloReauthRequiredError) return error;
|
|
5230
|
+
if ((error instanceof TopoloAuthError2 || error instanceof TopoloHttpError2) && error.status === 429) {
|
|
5231
|
+
const message2 = "Topolo sign-in renewal is temporarily rate limited by Auth. Retry after the Retry-After interval; the saved sign-in has been retained.";
|
|
5232
|
+
return new TopoloRateLimitError2(
|
|
5233
|
+
"auth",
|
|
5234
|
+
path,
|
|
5235
|
+
{ code: "rate_limited", retryable: true, message: message2 },
|
|
5236
|
+
message2,
|
|
5237
|
+
{ requestId: error.requestId, retryAfter: error.retryAfter }
|
|
5238
|
+
);
|
|
5239
|
+
}
|
|
4969
5240
|
if (error instanceof TopoloAuthError2 && error.code === "invalid_grant") {
|
|
4970
5241
|
return new TopoloAuthError2("The saved Topolo sign-in can no longer be renewed. Sign in again.", "invalid_grant", {
|
|
4971
5242
|
status: error.status,
|
|
@@ -4990,120 +5261,266 @@ function renewalFailure(error) {
|
|
|
4990
5261
|
"Topolo sign-in renewal could not reach Auth. Retry the request; the saved sign-in has been retained."
|
|
4991
5262
|
);
|
|
4992
5263
|
}
|
|
4993
|
-
|
|
5264
|
+
function isTerminalRefusal(error) {
|
|
5265
|
+
return error instanceof TopoloAuthError2 && TERMINAL_RENEWAL_CODES.has(error.code) && (error.status === 400 || error.status === 401);
|
|
5266
|
+
}
|
|
5267
|
+
function resolveTarget(profile, target) {
|
|
5268
|
+
if (target.kind === "active") {
|
|
5269
|
+
return {
|
|
5270
|
+
key: profile.activeContext ?? DEFAULT_SLOT,
|
|
5271
|
+
credential: profile.credential,
|
|
5272
|
+
oauthClientId: profile.oauthClientId,
|
|
5273
|
+
context: profile.activeContext ? profile.contexts?.[profile.activeContext] : void 0
|
|
5274
|
+
};
|
|
5275
|
+
}
|
|
5276
|
+
const context = profile.contexts?.[target.key];
|
|
5277
|
+
return {
|
|
5278
|
+
key: target.key,
|
|
5279
|
+
credential: context?.credential,
|
|
5280
|
+
oauthClientId: context?.oauthClientId ?? profile.oauthClientId,
|
|
5281
|
+
context
|
|
5282
|
+
};
|
|
5283
|
+
}
|
|
5284
|
+
function envCredentialConfigured(environment) {
|
|
4994
5285
|
const prefix = envVarPrefix(environment);
|
|
4995
|
-
|
|
4996
|
-
|
|
5286
|
+
return Boolean(process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN);
|
|
5287
|
+
}
|
|
5288
|
+
function authServiceUrls(profile, environment, purpose) {
|
|
5289
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
5290
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
5291
|
+
throw new TopoloSdkError2("environment_service_url_required", `Local Topolo ${purpose} requires an explicit local Auth service origin.`);
|
|
4997
5292
|
}
|
|
5293
|
+
return serviceUrls;
|
|
5294
|
+
}
|
|
5295
|
+
var renewalsInFlight = /* @__PURE__ */ new Map();
|
|
5296
|
+
function singleFlight(key, run) {
|
|
5297
|
+
const existing = renewalsInFlight.get(key);
|
|
5298
|
+
if (existing) return existing;
|
|
5299
|
+
const started = run().finally(() => {
|
|
5300
|
+
renewalsInFlight.delete(key);
|
|
5301
|
+
});
|
|
5302
|
+
renewalsInFlight.set(key, started);
|
|
5303
|
+
return started;
|
|
5304
|
+
}
|
|
5305
|
+
async function renewTarget(environment, target, options) {
|
|
4998
5306
|
const observedProfile = profileForEnvironment(await readConfig(), environment);
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
const
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5307
|
+
const observedTarget = resolveTarget(observedProfile, target);
|
|
5308
|
+
const observed = observedTarget.credential;
|
|
5309
|
+
if (!observed) {
|
|
5310
|
+
return observedProfile.reauthorization?.[observedTarget.key] ? continueReauthorization(environment, observedTarget.key) : null;
|
|
5311
|
+
}
|
|
5312
|
+
if (observed.kind !== "access_token" || !observed.refreshToken || !observedTarget.oauthClientId || !options.force && observed.expiresAt && observed.expiresAt - REFRESH_SKEW_SECONDS > nowSeconds()) return observed;
|
|
5313
|
+
const flightKey = JSON.stringify([environment, target.kind === "active" ? "@active" : target.key]);
|
|
5314
|
+
const run = async () => {
|
|
5315
|
+
const outcome = await withConfigWriteLock(async (_assertOwned, persist) => {
|
|
5316
|
+
const config = await readConfig();
|
|
5317
|
+
const profile = profileForEnvironment(config, environment);
|
|
5318
|
+
const resolved = resolveTarget(profile, target);
|
|
5319
|
+
const credential = resolved.credential;
|
|
5320
|
+
if (!credential) return profile.reauthorization?.[resolved.key] ? { reauthorize: resolved.key } : { credential: null };
|
|
5321
|
+
if (credential.kind !== "access_token" || !credential.refreshToken || !resolved.oauthClientId) return { credential };
|
|
5322
|
+
const now = nowSeconds();
|
|
5323
|
+
if (options.force && observed.kind === "access_token" && observed.accessToken !== credential.accessToken && credential.expiresAt !== void 0 && credential.expiresAt > now) return { credential };
|
|
5324
|
+
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return { credential };
|
|
5325
|
+
const sent = credential.refreshToken;
|
|
5326
|
+
const endGrant = async (reason) => {
|
|
5327
|
+
await persist((current) => {
|
|
5328
|
+
const next = clone(current);
|
|
5329
|
+
const touched = replaceGrant(next, environment, sent, null);
|
|
5330
|
+
touched.add(resolved.key);
|
|
5331
|
+
retire(next, sent, reason);
|
|
5332
|
+
for (const key of touched) {
|
|
5333
|
+
setReauthorization(next, environment, key, (existing) => ({ ...existing, reason, since: existing?.since ?? nowSeconds() }));
|
|
5334
|
+
}
|
|
5335
|
+
return next;
|
|
5336
|
+
});
|
|
5337
|
+
return { reauthorize: resolved.key };
|
|
5338
|
+
};
|
|
5339
|
+
const retired = retiredEntry(config, sent);
|
|
5340
|
+
if (retired) {
|
|
5341
|
+
return endGrant(retired.reason === "rotated" ? "rotated_grant_lost" : retired.reason);
|
|
5342
|
+
}
|
|
5343
|
+
const serviceUrls = authServiceUrls(profile, environment, target.kind === "active" ? "credential refresh" : "context refresh");
|
|
5344
|
+
let tokens;
|
|
5345
|
+
try {
|
|
5346
|
+
tokens = await boundedOAuth(serviceUrls).refreshToken({ clientId: resolved.oauthClientId, refreshToken: sent });
|
|
5347
|
+
} catch (error) {
|
|
5348
|
+
if (isTerminalRefusal(error)) return endGrant(error.code);
|
|
5349
|
+
throw renewalFailure(error);
|
|
5350
|
+
}
|
|
5027
5351
|
const refreshed = {
|
|
5028
5352
|
kind: "access_token",
|
|
5029
5353
|
accessToken: tokens.access_token,
|
|
5030
|
-
|
|
5031
|
-
expiresAt:
|
|
5354
|
+
refreshToken: tokens.refresh_token ?? sent,
|
|
5355
|
+
expiresAt: nowSeconds() + tokens.expires_in
|
|
5032
5356
|
};
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
throw renewalFailure(error);
|
|
5054
|
-
}
|
|
5055
|
-
});
|
|
5357
|
+
try {
|
|
5358
|
+
await persist((current) => {
|
|
5359
|
+
const next = clone(current);
|
|
5360
|
+
replaceGrant(next, environment, sent, refreshed);
|
|
5361
|
+
if (tokens.refresh_token && tokens.refresh_token !== sent) retire(next, sent, "rotated");
|
|
5362
|
+
return next;
|
|
5363
|
+
});
|
|
5364
|
+
} catch (error) {
|
|
5365
|
+
throw renewalFailure(error);
|
|
5366
|
+
}
|
|
5367
|
+
return { credential: refreshed };
|
|
5368
|
+
});
|
|
5369
|
+
if ("reauthorize" in outcome) return continueReauthorization(environment, outcome.reauthorize);
|
|
5370
|
+
return outcome.credential;
|
|
5371
|
+
};
|
|
5372
|
+
return options.force ? run() : singleFlight(flightKey, run);
|
|
5373
|
+
}
|
|
5374
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
5375
|
+
if (envCredentialConfigured(environment)) return resolveCredential(environment);
|
|
5376
|
+
return renewTarget(environment, { kind: "active" }, options);
|
|
5056
5377
|
}
|
|
5057
5378
|
async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5379
|
+
return renewTarget(environment, { kind: "context", key }, options);
|
|
5380
|
+
}
|
|
5381
|
+
function decodeTokenOrganization(accessToken) {
|
|
5382
|
+
try {
|
|
5383
|
+
const payload = accessToken.split(".")[1];
|
|
5384
|
+
if (!payload) return { known: false, organizationId: null };
|
|
5385
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
5386
|
+
const org = claims.orgId ?? claims.organizationId ?? claims.org_id ?? null;
|
|
5387
|
+
return { known: true, organizationId: typeof org === "string" && org ? org : null };
|
|
5388
|
+
} catch {
|
|
5389
|
+
return { known: false, organizationId: null };
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
async function continueReauthorization(environment, key) {
|
|
5062
5393
|
return withConfigWriteLock(async (_assertOwned, persist) => {
|
|
5063
5394
|
const config = await readConfig();
|
|
5064
5395
|
const profile = profileForEnvironment(config, environment);
|
|
5065
|
-
const
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
const
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5396
|
+
const credential = slotCredential(profile, key);
|
|
5397
|
+
if (credential && usableCredential(config, credential)) return credential;
|
|
5398
|
+
const entry = profile.reauthorization?.[key];
|
|
5399
|
+
if (!entry) return credential ?? null;
|
|
5400
|
+
const clientId = (key === DEFAULT_SLOT ? void 0 : profile.contexts?.[key]?.oauthClientId) ?? profile.oauthClientId;
|
|
5401
|
+
if (!clientId) throw reauthRequired(environment, key, entry, "unavailable");
|
|
5402
|
+
const serviceUrls = authServiceUrls(profile, environment, "sign-in renewal");
|
|
5403
|
+
const oauth = boundedOAuth(serviceUrls);
|
|
5404
|
+
const now = nowSeconds();
|
|
5405
|
+
let state = clone(entry);
|
|
5406
|
+
const save = async (update) => {
|
|
5407
|
+
await persist((current) => {
|
|
5408
|
+
const next = clone(current);
|
|
5409
|
+
setReauthorization(next, environment, key, update);
|
|
5410
|
+
return next;
|
|
5411
|
+
});
|
|
5412
|
+
};
|
|
5413
|
+
let pending = state.pending && state.pending.clientId === clientId && state.pending.expiresAt > now ? state.pending : void 0;
|
|
5414
|
+
if (pending && !pending.denied && pending.nextPollAt <= now) {
|
|
5415
|
+
let tokens;
|
|
5416
|
+
try {
|
|
5417
|
+
tokens = await oauth.pollDeviceToken({ clientId, deviceCode: pending.deviceCode });
|
|
5418
|
+
} catch (error) {
|
|
5419
|
+
const code = error instanceof TopoloAuthError2 ? error.code : void 0;
|
|
5420
|
+
if (code === "authorization_pending") {
|
|
5421
|
+
pending = { ...pending, nextPollAt: now + pending.interval };
|
|
5422
|
+
} else if (code === "slow_down") {
|
|
5423
|
+
const interval = pending.interval + 5;
|
|
5424
|
+
const retryAfter = Number(error.retryAfter);
|
|
5425
|
+
pending = { ...pending, interval, nextPollAt: now + Math.max(interval, Number.isFinite(retryAfter) ? retryAfter : 0) };
|
|
5426
|
+
} else if (code === "access_denied") {
|
|
5427
|
+
pending = { ...pending, denied: true };
|
|
5428
|
+
} else if (code === "expired_token" || code === "invalid_grant") {
|
|
5429
|
+
pending = void 0;
|
|
5430
|
+
} else {
|
|
5431
|
+
const retryAfter = Number(error.retryAfter);
|
|
5432
|
+
pending = { ...pending, nextPollAt: now + Math.max(pending.interval * 2, Number.isFinite(retryAfter) ? retryAfter : 0) };
|
|
5433
|
+
}
|
|
5434
|
+
state = { ...state, pending };
|
|
5435
|
+
if (!pending) delete state.pending;
|
|
5436
|
+
await save(() => state);
|
|
5437
|
+
if (pending) throw reauthRequired(environment, key, state, pending.denied ? "denied" : "awaiting_approval");
|
|
5438
|
+
}
|
|
5439
|
+
if (tokens) {
|
|
5440
|
+
const approved = {
|
|
5441
|
+
kind: "access_token",
|
|
5442
|
+
accessToken: tokens.access_token,
|
|
5443
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
5444
|
+
expiresAt: nowSeconds() + tokens.expires_in
|
|
5445
|
+
};
|
|
5446
|
+
const organization = decodeTokenOrganization(tokens.access_token);
|
|
5447
|
+
const approvedKey = organization.known ? organization.organizationId ? `org:${organization.organizationId}` : "personal" : null;
|
|
5448
|
+
const matches = key === DEFAULT_SLOT || approvedKey === key;
|
|
5449
|
+
await persist((current) => {
|
|
5450
|
+
const next = clone(current);
|
|
5451
|
+
const nextProfile = (next.profiles ??= {})[environment] ??= {};
|
|
5452
|
+
const legacyProduction = environment === "production" && !current.profiles?.[environment]?.contexts && key === DEFAULT_SLOT && Boolean(current.credential);
|
|
5453
|
+
if (matches) {
|
|
5454
|
+
if (key === DEFAULT_SLOT) {
|
|
5455
|
+
if (legacyProduction) next.credential = approved;
|
|
5456
|
+
else nextProfile.credential = approved;
|
|
5457
|
+
} else {
|
|
5458
|
+
const existing = nextProfile.contexts?.[key];
|
|
5459
|
+
nextProfile.contexts = { ...nextProfile.contexts ?? {}, [key]: {
|
|
5460
|
+
...existing ?? (key.startsWith("org:") ? { contextType: "organization", organizationId: key.slice(4) } : { contextType: "personal" }),
|
|
5461
|
+
credential: approved,
|
|
5462
|
+
oauthClientId: clientId
|
|
5463
|
+
} };
|
|
5464
|
+
if (nextProfile.activeContext === key) nextProfile.credential = approved;
|
|
5465
|
+
}
|
|
5466
|
+
setReauthorization(next, environment, key, () => void 0);
|
|
5467
|
+
} else {
|
|
5468
|
+
if (approvedKey && !(approvedKey in (nextProfile.reauthorization ?? {}))) {
|
|
5469
|
+
const existing = nextProfile.contexts?.[approvedKey];
|
|
5470
|
+
nextProfile.contexts = { ...nextProfile.contexts ?? {}, [approvedKey]: {
|
|
5471
|
+
...existing ?? (approvedKey.startsWith("org:") ? { contextType: "organization", organizationId: approvedKey.slice(4) } : { contextType: "personal" }),
|
|
5472
|
+
credential: approved,
|
|
5473
|
+
oauthClientId: clientId
|
|
5474
|
+
} };
|
|
5475
|
+
if (nextProfile.activeContext === approvedKey) nextProfile.credential = approved;
|
|
5476
|
+
}
|
|
5477
|
+
setReauthorization(next, environment, key, (existing) => {
|
|
5478
|
+
const updated = {
|
|
5479
|
+
...existing ?? state,
|
|
5480
|
+
mismatch: { approvedContext: approvedKey ?? "an unverifiable context", at: nowSeconds() }
|
|
5481
|
+
};
|
|
5482
|
+
delete updated.pending;
|
|
5483
|
+
return updated;
|
|
5484
|
+
});
|
|
5485
|
+
}
|
|
5486
|
+
return next;
|
|
5487
|
+
});
|
|
5488
|
+
if (matches) return approved;
|
|
5489
|
+
const latest = profileForEnvironment(await readConfig(), environment).reauthorization?.[key];
|
|
5490
|
+
throw reauthRequired(environment, key, latest ?? state, "organization_mismatch");
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
if (pending) {
|
|
5494
|
+
throw reauthRequired(environment, key, state, pending.denied ? "denied" : "awaiting_approval");
|
|
5495
|
+
}
|
|
5496
|
+
if (state.startRetryAt && state.startRetryAt > now) {
|
|
5497
|
+
throw reauthRequired(environment, key, state, state.mismatch ? "organization_mismatch" : "unavailable");
|
|
5079
5498
|
}
|
|
5080
5499
|
try {
|
|
5081
|
-
const
|
|
5082
|
-
const
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
expiresAt: Math.floor(
|
|
5500
|
+
const started = await oauth.requestDeviceCode({ clientId });
|
|
5501
|
+
const interval = Math.max(1, Math.floor(Number(started.interval) || 5));
|
|
5502
|
+
const startedPending = {
|
|
5503
|
+
clientId,
|
|
5504
|
+
deviceCode: started.device_code,
|
|
5505
|
+
userCode: started.user_code,
|
|
5506
|
+
verificationUri: started.verification_uri,
|
|
5507
|
+
...started.verification_uri_complete ? { verificationUriComplete: started.verification_uri_complete } : {},
|
|
5508
|
+
startedAt: now,
|
|
5509
|
+
expiresAt: now + Math.max(1, Math.floor(Number(started.expires_in) || 600)),
|
|
5510
|
+
interval,
|
|
5511
|
+
nextPollAt: now + interval
|
|
5091
5512
|
};
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
throw new ConfigOwnershipError();
|
|
5097
|
-
if (latest.activeContext === key && JSON.stringify(latest.credential) !== JSON.stringify(profile.activeContext === key ? profile.credential : credential))
|
|
5098
|
-
throw new ConfigOwnershipError();
|
|
5099
|
-
return writeProfileConfig(current, environment, {
|
|
5100
|
-
...latest.activeContext === key ? { credential: refreshed } : {},
|
|
5101
|
-
contexts: { ...latest.contexts, [key]: { ...latestContext, credential: refreshed, oauthClientId } }
|
|
5102
|
-
});
|
|
5103
|
-
});
|
|
5104
|
-
return refreshed;
|
|
5513
|
+
state = { ...state, pending: startedPending };
|
|
5514
|
+
delete state.startRetryAt;
|
|
5515
|
+
await save(() => state);
|
|
5516
|
+
throw reauthRequired(environment, key, state, "awaiting_approval");
|
|
5105
5517
|
} catch (error) {
|
|
5106
|
-
|
|
5518
|
+
if (error instanceof TopoloReauthRequiredError || error instanceof ConfigOwnershipError) throw error;
|
|
5519
|
+
const retryAfter = Number(error.retryAfter);
|
|
5520
|
+
state = { ...state, startRetryAt: now + Math.max(30, Number.isFinite(retryAfter) ? retryAfter : 0) };
|
|
5521
|
+
delete state.pending;
|
|
5522
|
+
await save(() => state);
|
|
5523
|
+
throw reauthRequired(environment, key, state, "unavailable");
|
|
5107
5524
|
}
|
|
5108
5525
|
});
|
|
5109
5526
|
}
|
|
@@ -7079,7 +7496,7 @@ function message(error) {
|
|
|
7079
7496
|
|
|
7080
7497
|
// src/commands/agent-config.ts
|
|
7081
7498
|
import { existsSync as existsSync5 } from "fs";
|
|
7082
|
-
import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as
|
|
7499
|
+
import { lstat as lstat2, mkdir as mkdir3, open, readFile as readFile5, rename as rename2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
7083
7500
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
7084
7501
|
import { dirname as dirname5 } from "path";
|
|
7085
7502
|
async function installAgentSkill(agent, source, homeDir, projectDir, codexHome) {
|
|
@@ -7315,7 +7732,7 @@ async function atomicWrite(path, contents, before) {
|
|
|
7315
7732
|
const created = await handle.stat();
|
|
7316
7733
|
temporaryIdentity = [created.dev, created.ino].join(":");
|
|
7317
7734
|
await assertSnapshot(path, destination);
|
|
7318
|
-
await
|
|
7735
|
+
await writeFile2(handle, contents, { encoding: "utf8" });
|
|
7319
7736
|
await handle.close();
|
|
7320
7737
|
handleClosed = true;
|
|
7321
7738
|
await assertSnapshot(path, destination);
|