@topolo/sdk 0.19.0 → 0.20.0

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.
@@ -28,11 +28,16 @@ __export(credential_store_exports, {
28
28
  clearProfileCredential: () => clearProfileCredential,
29
29
  configPath: () => configPath,
30
30
  contextKey: () => contextKey,
31
+ createPlatformInstallationSecretBackend: () => createPlatformInstallationSecretBackend,
31
32
  credentialSourceFromConfig: () => credentialSourceFromConfig,
33
+ deleteExternalAgentInstallation: () => deleteExternalAgentInstallation,
32
34
  envVarPrefix: () => envVarPrefix,
35
+ installationStorePath: () => installationStorePath,
36
+ listExternalAgentInstallations: () => listExternalAgentInstallations,
33
37
  normalizeEnvironment: () => normalizeEnvironment,
34
38
  profileForEnvironment: () => profileForEnvironment,
35
39
  readConfig: () => readConfig,
40
+ readExternalAgentInstallation: () => readExternalAgentInstallation,
36
41
  refreshContextCredentialIfNeeded: () => refreshContextCredentialIfNeeded,
37
42
  refreshCredentialIfNeeded: () => refreshCredentialIfNeeded,
38
43
  resolveAgentName: () => resolveAgentName,
@@ -43,13 +48,14 @@ __export(credential_store_exports, {
43
48
  topoloServiceOrigins: () => topoloServiceOrigins,
44
49
  writeConfig: () => writeConfig,
45
50
  writeContextCredential: () => writeContextCredential,
51
+ writeExternalAgentInstallation: () => writeExternalAgentInstallation,
46
52
  writeProfileConfig: () => writeProfileConfig
47
53
  });
48
54
  module.exports = __toCommonJS(credential_store_exports);
49
- var import_promises = require("fs/promises");
50
- var import_node_fs = require("fs");
51
- var import_node_os = require("os");
52
- var import_node_path = require("path");
55
+ var import_promises2 = require("fs/promises");
56
+ var import_node_fs2 = require("fs");
57
+ var import_node_os2 = require("os");
58
+ var import_node_path2 = require("path");
53
59
 
54
60
  // src/errors.ts
55
61
  var TopoloSdkError = class extends Error {
@@ -468,6 +474,172 @@ function topoloServiceOrigins(serviceUrls, environment) {
468
474
  );
469
475
  }
470
476
 
477
+ // src/node/installation-store.ts
478
+ var import_node_child_process = require("child_process");
479
+ var import_promises = require("fs/promises");
480
+ var import_node_fs = require("fs");
481
+ var import_node_os = require("os");
482
+ var import_node_path = require("path");
483
+ var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
484
+ function installationStorePath() {
485
+ const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
486
+ if (override) return override;
487
+ const base = process.env.XDG_CONFIG_HOME || ((0, import_node_os.platform)() === "win32" ? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Roaming") : (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"));
488
+ return (0, import_node_path.join)(base, "topolo", "installations");
489
+ }
490
+ function installationPath(environment, id) {
491
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
492
+ return (0, import_node_path.join)(installationStorePath(), environment, `${id}.json`);
493
+ }
494
+ function accountName(environment, id) {
495
+ return `${environment}:${id}`;
496
+ }
497
+ function runOrThrow(result, operation) {
498
+ if (result.status !== 0) {
499
+ throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
500
+ }
501
+ return result.stdout.trim();
502
+ }
503
+ function createPlatformInstallationSecretBackend(options = {}) {
504
+ const platform2 = options.platform ?? (0, import_node_os.platform)();
505
+ const spawn = options.spawn ?? ((command, args, spawnOptions) => (0, import_node_child_process.spawnSync)(command, args, spawnOptions));
506
+ const root = options.root ?? installationStorePath();
507
+ if (platform2 === "darwin") {
508
+ return {
509
+ async set(account, value) {
510
+ runOrThrow(spawn("security", [
511
+ "add-generic-password",
512
+ "-U",
513
+ "-a",
514
+ account,
515
+ "-s",
516
+ KEYCHAIN_SERVICE,
517
+ "-w"
518
+ ], { input: `${value}
519
+ `, encoding: "utf8" }), "macOS Keychain write");
520
+ },
521
+ async get(account) {
522
+ const result = spawn("security", [
523
+ "find-generic-password",
524
+ "-a",
525
+ account,
526
+ "-s",
527
+ KEYCHAIN_SERVICE,
528
+ "-w"
529
+ ], { encoding: "utf8" });
530
+ if (result.status === 44) return null;
531
+ return runOrThrow(result, "macOS Keychain read");
532
+ },
533
+ async delete(account) {
534
+ const result = spawn("security", [
535
+ "delete-generic-password",
536
+ "-a",
537
+ account,
538
+ "-s",
539
+ KEYCHAIN_SERVICE
540
+ ], { encoding: "utf8" });
541
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
542
+ }
543
+ };
544
+ }
545
+ if (platform2 === "linux") {
546
+ return {
547
+ async set(account, value) {
548
+ runOrThrow(spawn("secret-tool", [
549
+ "store",
550
+ "--label=Topolo external agent installation",
551
+ "service",
552
+ KEYCHAIN_SERVICE,
553
+ "account",
554
+ account
555
+ ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
556
+ },
557
+ async get(account) {
558
+ const result = spawn("secret-tool", [
559
+ "lookup",
560
+ "service",
561
+ KEYCHAIN_SERVICE,
562
+ "account",
563
+ account
564
+ ], { encoding: "utf8" });
565
+ if (result.status === 1) return null;
566
+ return runOrThrow(result, "Linux Secret Service read");
567
+ },
568
+ async delete(account) {
569
+ const result = spawn("secret-tool", [
570
+ "clear",
571
+ "service",
572
+ KEYCHAIN_SERVICE,
573
+ "account",
574
+ account
575
+ ], { encoding: "utf8" });
576
+ if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
577
+ }
578
+ };
579
+ }
580
+ if (platform2 === "win32") {
581
+ const secretPath = (account) => (0, import_node_path.join)(root, ".secrets", `${account.replace(":", "-")}.bin`);
582
+ const powershell = (script, path, input) => spawn(
583
+ "powershell.exe",
584
+ ["-NoProfile", "-NonInteractive", "-Command", script],
585
+ { input, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
586
+ );
587
+ return {
588
+ async set(account, value) {
589
+ const path = secretPath(account);
590
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
591
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[IO.File]::WriteAllBytes($p,$e)";
592
+ runOrThrow(powershell(script, path, value), "Windows DPAPI write");
593
+ },
594
+ async get(account) {
595
+ const path = secretPath(account);
596
+ if (!(0, import_node_fs.existsSync)(path)) return null;
597
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$b=[IO.File]::ReadAllBytes($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
598
+ return runOrThrow(powershell(script, path), "Windows DPAPI read");
599
+ },
600
+ async delete(account) {
601
+ await (0, import_promises.rm)(secretPath(account), { force: true });
602
+ }
603
+ };
604
+ }
605
+ throw new Error(`Secure external-agent installation storage is unsupported on ${platform2}.`);
606
+ }
607
+ async function writeExternalAgentInstallation(installation, secret, backend = createPlatformInstallationSecretBackend()) {
608
+ const path = installationPath(installation.environment, installation.id);
609
+ await backend.set(accountName(installation.environment, installation.id), JSON.stringify(secret));
610
+ const temporary = `${path}.${process.pid}.tmp`;
611
+ try {
612
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true, mode: 448 });
613
+ await (0, import_promises.writeFile)(temporary, `${JSON.stringify(installation, null, 2)}
614
+ `, { encoding: "utf8", mode: 384 });
615
+ await (0, import_promises.rename)(temporary, path);
616
+ if ((0, import_node_os.platform)() !== "win32") await (0, import_promises.chmod)(path, 384);
617
+ } catch (error) {
618
+ await backend.delete(accountName(installation.environment, installation.id)).catch(() => void 0);
619
+ await (0, import_promises.rm)(temporary, { force: true });
620
+ throw error;
621
+ }
622
+ }
623
+ async function readExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
624
+ const path = installationPath(environment, id);
625
+ if (!(0, import_node_fs.existsSync)(path)) return null;
626
+ const installation = JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
627
+ const encodedSecret = await backend.get(accountName(environment, id));
628
+ if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
629
+ return { installation, secret: JSON.parse(encodedSecret) };
630
+ }
631
+ async function listExternalAgentInstallations(environment) {
632
+ const directory = (0, import_node_path.join)(installationStorePath(), environment);
633
+ if (!(0, import_node_fs.existsSync)(directory)) return [];
634
+ const entries = await (0, import_promises.readdir)(directory, { withFileTypes: true });
635
+ const installations = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => JSON.parse(await (0, import_promises.readFile)((0, import_node_path.join)(directory, entry.name), "utf8"))));
636
+ return installations.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
637
+ }
638
+ async function deleteExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
639
+ await backend.delete(accountName(environment, id));
640
+ await (0, import_promises.rm)(installationPath(environment, id), { force: true });
641
+ }
642
+
471
643
  // src/node/credential-store.ts
472
644
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
473
645
  function contextKey(contextType, organizationId) {
@@ -477,14 +649,14 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
477
649
  function configPath() {
478
650
  const override = process.env.TOPOLO_CONFIG_PATH;
479
651
  if (override) return override;
480
- const base = process.env.XDG_CONFIG_HOME || ((0, import_node_os.platform)() === "win32" ? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Roaming") : (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"));
481
- return (0, import_node_path.join)(base, "topolo", "config.json");
652
+ const base = process.env.XDG_CONFIG_HOME || ((0, import_node_os2.platform)() === "win32" ? (0, import_node_path2.join)((0, import_node_os2.homedir)(), "AppData", "Roaming") : (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".config"));
653
+ return (0, import_node_path2.join)(base, "topolo", "config.json");
482
654
  }
483
655
  async function readConfig() {
484
656
  const path = configPath();
485
- if (!(0, import_node_fs.existsSync)(path)) return {};
657
+ if (!(0, import_node_fs2.existsSync)(path)) return {};
486
658
  try {
487
- const raw = await (0, import_promises.readFile)(path, "utf8");
659
+ const raw = await (0, import_promises2.readFile)(path, "utf8");
488
660
  return JSON.parse(raw);
489
661
  } catch {
490
662
  return {};
@@ -492,11 +664,11 @@ async function readConfig() {
492
664
  }
493
665
  async function writeConfig(config) {
494
666
  const path = configPath();
495
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
496
- await (0, import_promises.writeFile)(path, JSON.stringify(config, null, 2) + "\n", "utf8");
497
- if ((0, import_node_os.platform)() !== "win32") {
667
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(path), { recursive: true });
668
+ await (0, import_promises2.writeFile)(path, JSON.stringify(config, null, 2) + "\n", "utf8");
669
+ if ((0, import_node_os2.platform)() !== "win32") {
498
670
  try {
499
- await (0, import_promises.chmod)(path, 384);
671
+ await (0, import_promises2.chmod)(path, 384);
500
672
  } catch {
501
673
  }
502
674
  }
@@ -612,18 +784,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
612
784
  var CONFIG_LOCK_STALE_MS = 12e4;
613
785
  async function withConfigWriteLock(operation) {
614
786
  const lockPath = `${configPath()}.lock`;
615
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(lockPath), { recursive: true });
787
+ await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(lockPath), { recursive: true });
616
788
  const startedAt = Date.now();
617
789
  while (true) {
618
790
  try {
619
- await (0, import_promises.mkdir)(lockPath);
791
+ await (0, import_promises2.mkdir)(lockPath);
620
792
  break;
621
793
  } catch (error) {
622
794
  if (error.code !== "EEXIST") throw error;
623
795
  try {
624
- const lock = await (0, import_promises.stat)(lockPath);
796
+ const lock = await (0, import_promises2.stat)(lockPath);
625
797
  if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
626
- await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
798
+ await (0, import_promises2.rm)(lockPath, { recursive: true, force: true });
627
799
  continue;
628
800
  }
629
801
  } catch (inspectionError) {
@@ -639,7 +811,7 @@ async function withConfigWriteLock(operation) {
639
811
  try {
640
812
  return await operation();
641
813
  } finally {
642
- await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
814
+ await (0, import_promises2.rm)(lockPath, { recursive: true, force: true });
643
815
  }
644
816
  }
645
817
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
@@ -788,11 +960,16 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
788
960
  clearProfileCredential,
789
961
  configPath,
790
962
  contextKey,
963
+ createPlatformInstallationSecretBackend,
791
964
  credentialSourceFromConfig,
965
+ deleteExternalAgentInstallation,
792
966
  envVarPrefix,
967
+ installationStorePath,
968
+ listExternalAgentInstallations,
793
969
  normalizeEnvironment,
794
970
  profileForEnvironment,
795
971
  readConfig,
972
+ readExternalAgentInstallation,
796
973
  refreshContextCredentialIfNeeded,
797
974
  refreshCredentialIfNeeded,
798
975
  resolveAgentName,
@@ -803,5 +980,6 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
803
980
  topoloServiceOrigins,
804
981
  writeConfig,
805
982
  writeContextCredential,
983
+ writeExternalAgentInstallation,
806
984
  writeProfileConfig
807
985
  });
@@ -1,3 +1,5 @@
1
+ import { SpawnSyncReturns } from 'node:child_process';
2
+
1
3
  /**
2
4
  * Auth credential types used by the SDK.
3
5
  *
@@ -29,6 +31,48 @@ type TopoloRuntimeEnvironment = (typeof TOPOLO_RUNTIME_ENVIRONMENTS)[number];
29
31
  declare function assertTopoloServiceUrlEnvironment(service: string, value: string, environment: TopoloRuntimeEnvironment): string;
30
32
  declare function topoloServiceOrigins(serviceUrls: Record<string, string> | undefined, environment: TopoloRuntimeEnvironment): Record<string, string>;
31
33
 
34
+ interface ExternalAgentInstallation {
35
+ id: string;
36
+ environment: TopoloRuntimeEnvironment;
37
+ organizationId: string;
38
+ principalId: string;
39
+ credentialId: string;
40
+ clientId: string;
41
+ displayName: string;
42
+ provider: string;
43
+ host: string;
44
+ model?: string;
45
+ dpopJkt: string;
46
+ createdAt: string;
47
+ }
48
+ interface ExternalAgentInstallationSecret {
49
+ clientSecret: string;
50
+ privateJwk: JsonWebKey;
51
+ }
52
+ interface InstallationSecretBackend {
53
+ set(account: string, value: string): Promise<void>;
54
+ get(account: string): Promise<string | null>;
55
+ delete(account: string): Promise<void>;
56
+ }
57
+ type Spawn = (command: string, args: string[], options: {
58
+ input?: string;
59
+ encoding: 'utf8';
60
+ env?: NodeJS.ProcessEnv;
61
+ }) => SpawnSyncReturns<string>;
62
+ declare function installationStorePath(): string;
63
+ declare function createPlatformInstallationSecretBackend(options?: {
64
+ platform?: NodeJS.Platform;
65
+ spawn?: Spawn;
66
+ root?: string;
67
+ }): InstallationSecretBackend;
68
+ declare function writeExternalAgentInstallation(installation: ExternalAgentInstallation, secret: ExternalAgentInstallationSecret, backend?: InstallationSecretBackend): Promise<void>;
69
+ declare function readExternalAgentInstallation(environment: TopoloRuntimeEnvironment, id: string, backend?: InstallationSecretBackend): Promise<{
70
+ installation: ExternalAgentInstallation;
71
+ secret: ExternalAgentInstallationSecret;
72
+ } | null>;
73
+ declare function listExternalAgentInstallations(environment: TopoloRuntimeEnvironment): Promise<ExternalAgentInstallation[]>;
74
+ declare function deleteExternalAgentInstallation(environment: TopoloRuntimeEnvironment, id: string, backend?: InstallationSecretBackend): Promise<void>;
75
+
32
76
  /**
33
77
  * Node-only credential store shared by the `topolo` CLI and `@topolo/mcp`.
34
78
  *
@@ -122,4 +166,4 @@ declare function resolveServiceUrls(configUrls?: Record<string, string>, environ
122
166
  declare function assertServiceUrlForEnvironment(service: string, serviceUrl: string | undefined, environment?: TopoloEnvironment): void;
123
167
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
124
168
 
125
- export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeProfileConfig };
169
+ export { ENVIRONMENT_DEFAULT_SERVICE_URLS, type ExternalAgentInstallation, type ExternalAgentInstallationSecret, type InstallationSecretBackend, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, createPlatformInstallationSecretBackend, credentialSourceFromConfig, deleteExternalAgentInstallation, envVarPrefix, installationStorePath, listExternalAgentInstallations, normalizeEnvironment, profileForEnvironment, readConfig, readExternalAgentInstallation, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeExternalAgentInstallation, writeProfileConfig };
@@ -1,3 +1,5 @@
1
+ import { SpawnSyncReturns } from 'node:child_process';
2
+
1
3
  /**
2
4
  * Auth credential types used by the SDK.
3
5
  *
@@ -29,6 +31,48 @@ type TopoloRuntimeEnvironment = (typeof TOPOLO_RUNTIME_ENVIRONMENTS)[number];
29
31
  declare function assertTopoloServiceUrlEnvironment(service: string, value: string, environment: TopoloRuntimeEnvironment): string;
30
32
  declare function topoloServiceOrigins(serviceUrls: Record<string, string> | undefined, environment: TopoloRuntimeEnvironment): Record<string, string>;
31
33
 
34
+ interface ExternalAgentInstallation {
35
+ id: string;
36
+ environment: TopoloRuntimeEnvironment;
37
+ organizationId: string;
38
+ principalId: string;
39
+ credentialId: string;
40
+ clientId: string;
41
+ displayName: string;
42
+ provider: string;
43
+ host: string;
44
+ model?: string;
45
+ dpopJkt: string;
46
+ createdAt: string;
47
+ }
48
+ interface ExternalAgentInstallationSecret {
49
+ clientSecret: string;
50
+ privateJwk: JsonWebKey;
51
+ }
52
+ interface InstallationSecretBackend {
53
+ set(account: string, value: string): Promise<void>;
54
+ get(account: string): Promise<string | null>;
55
+ delete(account: string): Promise<void>;
56
+ }
57
+ type Spawn = (command: string, args: string[], options: {
58
+ input?: string;
59
+ encoding: 'utf8';
60
+ env?: NodeJS.ProcessEnv;
61
+ }) => SpawnSyncReturns<string>;
62
+ declare function installationStorePath(): string;
63
+ declare function createPlatformInstallationSecretBackend(options?: {
64
+ platform?: NodeJS.Platform;
65
+ spawn?: Spawn;
66
+ root?: string;
67
+ }): InstallationSecretBackend;
68
+ declare function writeExternalAgentInstallation(installation: ExternalAgentInstallation, secret: ExternalAgentInstallationSecret, backend?: InstallationSecretBackend): Promise<void>;
69
+ declare function readExternalAgentInstallation(environment: TopoloRuntimeEnvironment, id: string, backend?: InstallationSecretBackend): Promise<{
70
+ installation: ExternalAgentInstallation;
71
+ secret: ExternalAgentInstallationSecret;
72
+ } | null>;
73
+ declare function listExternalAgentInstallations(environment: TopoloRuntimeEnvironment): Promise<ExternalAgentInstallation[]>;
74
+ declare function deleteExternalAgentInstallation(environment: TopoloRuntimeEnvironment, id: string, backend?: InstallationSecretBackend): Promise<void>;
75
+
32
76
  /**
33
77
  * Node-only credential store shared by the `topolo` CLI and `@topolo/mcp`.
34
78
  *
@@ -122,4 +166,4 @@ declare function resolveServiceUrls(configUrls?: Record<string, string>, environ
122
166
  declare function assertServiceUrlForEnvironment(service: string, serviceUrl: string | undefined, environment?: TopoloEnvironment): void;
123
167
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
124
168
 
125
- export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeProfileConfig };
169
+ export { ENVIRONMENT_DEFAULT_SERVICE_URLS, type ExternalAgentInstallation, type ExternalAgentInstallationSecret, type InstallationSecretBackend, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, assertServiceUrlForEnvironment, assertTopoloServiceUrlEnvironment, clearProfileCredential, configPath, contextKey, createPlatformInstallationSecretBackend, credentialSourceFromConfig, deleteExternalAgentInstallation, envVarPrefix, installationStorePath, listExternalAgentInstallations, normalizeEnvironment, profileForEnvironment, readConfig, readExternalAgentInstallation, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, topoloServiceOrigins, writeConfig, writeContextCredential, writeExternalAgentInstallation, writeProfileConfig };
@@ -1,8 +1,8 @@
1
1
  // src/node/credential-store.ts
2
- import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
3
- import { existsSync } from "fs";
4
- import { homedir, platform } from "os";
5
- import { dirname, join } from "path";
2
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import { homedir as homedir2, platform } from "os";
5
+ import { dirname as dirname2, join as join2 } from "path";
6
6
 
7
7
  // src/errors.ts
8
8
  var TopoloSdkError = class extends Error {
@@ -421,6 +421,172 @@ function topoloServiceOrigins(serviceUrls, environment) {
421
421
  );
422
422
  }
423
423
 
424
+ // src/node/installation-store.ts
425
+ import { spawnSync } from "child_process";
426
+ import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
427
+ import { existsSync } from "fs";
428
+ import { homedir, platform as hostPlatform } from "os";
429
+ import { dirname, join } from "path";
430
+ var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
431
+ function installationStorePath() {
432
+ const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
433
+ if (override) return override;
434
+ const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
435
+ return join(base, "topolo", "installations");
436
+ }
437
+ function installationPath(environment, id) {
438
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
439
+ return join(installationStorePath(), environment, `${id}.json`);
440
+ }
441
+ function accountName(environment, id) {
442
+ return `${environment}:${id}`;
443
+ }
444
+ function runOrThrow(result, operation) {
445
+ if (result.status !== 0) {
446
+ throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
447
+ }
448
+ return result.stdout.trim();
449
+ }
450
+ function createPlatformInstallationSecretBackend(options = {}) {
451
+ const platform2 = options.platform ?? hostPlatform();
452
+ const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
453
+ const root = options.root ?? installationStorePath();
454
+ if (platform2 === "darwin") {
455
+ return {
456
+ async set(account, value) {
457
+ runOrThrow(spawn("security", [
458
+ "add-generic-password",
459
+ "-U",
460
+ "-a",
461
+ account,
462
+ "-s",
463
+ KEYCHAIN_SERVICE,
464
+ "-w"
465
+ ], { input: `${value}
466
+ `, encoding: "utf8" }), "macOS Keychain write");
467
+ },
468
+ async get(account) {
469
+ const result = spawn("security", [
470
+ "find-generic-password",
471
+ "-a",
472
+ account,
473
+ "-s",
474
+ KEYCHAIN_SERVICE,
475
+ "-w"
476
+ ], { encoding: "utf8" });
477
+ if (result.status === 44) return null;
478
+ return runOrThrow(result, "macOS Keychain read");
479
+ },
480
+ async delete(account) {
481
+ const result = spawn("security", [
482
+ "delete-generic-password",
483
+ "-a",
484
+ account,
485
+ "-s",
486
+ KEYCHAIN_SERVICE
487
+ ], { encoding: "utf8" });
488
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
489
+ }
490
+ };
491
+ }
492
+ if (platform2 === "linux") {
493
+ return {
494
+ async set(account, value) {
495
+ runOrThrow(spawn("secret-tool", [
496
+ "store",
497
+ "--label=Topolo external agent installation",
498
+ "service",
499
+ KEYCHAIN_SERVICE,
500
+ "account",
501
+ account
502
+ ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
503
+ },
504
+ async get(account) {
505
+ const result = spawn("secret-tool", [
506
+ "lookup",
507
+ "service",
508
+ KEYCHAIN_SERVICE,
509
+ "account",
510
+ account
511
+ ], { encoding: "utf8" });
512
+ if (result.status === 1) return null;
513
+ return runOrThrow(result, "Linux Secret Service read");
514
+ },
515
+ async delete(account) {
516
+ const result = spawn("secret-tool", [
517
+ "clear",
518
+ "service",
519
+ KEYCHAIN_SERVICE,
520
+ "account",
521
+ account
522
+ ], { encoding: "utf8" });
523
+ if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
524
+ }
525
+ };
526
+ }
527
+ if (platform2 === "win32") {
528
+ const secretPath = (account) => join(root, ".secrets", `${account.replace(":", "-")}.bin`);
529
+ const powershell = (script, path, input) => spawn(
530
+ "powershell.exe",
531
+ ["-NoProfile", "-NonInteractive", "-Command", script],
532
+ { input, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
533
+ );
534
+ return {
535
+ async set(account, value) {
536
+ const path = secretPath(account);
537
+ await mkdir(dirname(path), { recursive: true });
538
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[IO.File]::WriteAllBytes($p,$e)";
539
+ runOrThrow(powershell(script, path, value), "Windows DPAPI write");
540
+ },
541
+ async get(account) {
542
+ const path = secretPath(account);
543
+ if (!existsSync(path)) return null;
544
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$b=[IO.File]::ReadAllBytes($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
545
+ return runOrThrow(powershell(script, path), "Windows DPAPI read");
546
+ },
547
+ async delete(account) {
548
+ await rm(secretPath(account), { force: true });
549
+ }
550
+ };
551
+ }
552
+ throw new Error(`Secure external-agent installation storage is unsupported on ${platform2}.`);
553
+ }
554
+ async function writeExternalAgentInstallation(installation, secret, backend = createPlatformInstallationSecretBackend()) {
555
+ const path = installationPath(installation.environment, installation.id);
556
+ await backend.set(accountName(installation.environment, installation.id), JSON.stringify(secret));
557
+ const temporary = `${path}.${process.pid}.tmp`;
558
+ try {
559
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
560
+ await writeFile(temporary, `${JSON.stringify(installation, null, 2)}
561
+ `, { encoding: "utf8", mode: 384 });
562
+ await rename(temporary, path);
563
+ if (hostPlatform() !== "win32") await chmod(path, 384);
564
+ } catch (error) {
565
+ await backend.delete(accountName(installation.environment, installation.id)).catch(() => void 0);
566
+ await rm(temporary, { force: true });
567
+ throw error;
568
+ }
569
+ }
570
+ async function readExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
571
+ const path = installationPath(environment, id);
572
+ if (!existsSync(path)) return null;
573
+ const installation = JSON.parse(await readFile(path, "utf8"));
574
+ const encodedSecret = await backend.get(accountName(environment, id));
575
+ if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
576
+ return { installation, secret: JSON.parse(encodedSecret) };
577
+ }
578
+ async function listExternalAgentInstallations(environment) {
579
+ const directory = join(installationStorePath(), environment);
580
+ if (!existsSync(directory)) return [];
581
+ const entries = await readdir(directory, { withFileTypes: true });
582
+ const installations = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => JSON.parse(await readFile(join(directory, entry.name), "utf8"))));
583
+ return installations.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
584
+ }
585
+ async function deleteExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
586
+ await backend.delete(accountName(environment, id));
587
+ await rm(installationPath(environment, id), { force: true });
588
+ }
589
+
424
590
  // src/node/credential-store.ts
425
591
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
426
592
  function contextKey(contextType, organizationId) {
@@ -430,14 +596,14 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
430
596
  function configPath() {
431
597
  const override = process.env.TOPOLO_CONFIG_PATH;
432
598
  if (override) return override;
433
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
434
- return join(base, "topolo", "config.json");
599
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
600
+ return join2(base, "topolo", "config.json");
435
601
  }
436
602
  async function readConfig() {
437
603
  const path = configPath();
438
- if (!existsSync(path)) return {};
604
+ if (!existsSync2(path)) return {};
439
605
  try {
440
- const raw = await readFile(path, "utf8");
606
+ const raw = await readFile2(path, "utf8");
441
607
  return JSON.parse(raw);
442
608
  } catch {
443
609
  return {};
@@ -445,11 +611,11 @@ async function readConfig() {
445
611
  }
446
612
  async function writeConfig(config) {
447
613
  const path = configPath();
448
- await mkdir(dirname(path), { recursive: true });
449
- await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
614
+ await mkdir2(dirname2(path), { recursive: true });
615
+ await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
450
616
  if (platform() !== "win32") {
451
617
  try {
452
- await chmod(path, 384);
618
+ await chmod2(path, 384);
453
619
  } catch {
454
620
  }
455
621
  }
@@ -565,18 +731,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
565
731
  var CONFIG_LOCK_STALE_MS = 12e4;
566
732
  async function withConfigWriteLock(operation) {
567
733
  const lockPath = `${configPath()}.lock`;
568
- await mkdir(dirname(lockPath), { recursive: true });
734
+ await mkdir2(dirname2(lockPath), { recursive: true });
569
735
  const startedAt = Date.now();
570
736
  while (true) {
571
737
  try {
572
- await mkdir(lockPath);
738
+ await mkdir2(lockPath);
573
739
  break;
574
740
  } catch (error) {
575
741
  if (error.code !== "EEXIST") throw error;
576
742
  try {
577
743
  const lock = await stat(lockPath);
578
744
  if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
579
- await rm(lockPath, { recursive: true, force: true });
745
+ await rm2(lockPath, { recursive: true, force: true });
580
746
  continue;
581
747
  }
582
748
  } catch (inspectionError) {
@@ -592,7 +758,7 @@ async function withConfigWriteLock(operation) {
592
758
  try {
593
759
  return await operation();
594
760
  } finally {
595
- await rm(lockPath, { recursive: true, force: true });
761
+ await rm2(lockPath, { recursive: true, force: true });
596
762
  }
597
763
  }
598
764
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
@@ -740,11 +906,16 @@ export {
740
906
  clearProfileCredential,
741
907
  configPath,
742
908
  contextKey,
909
+ createPlatformInstallationSecretBackend,
743
910
  credentialSourceFromConfig,
911
+ deleteExternalAgentInstallation,
744
912
  envVarPrefix,
913
+ installationStorePath,
914
+ listExternalAgentInstallations,
745
915
  normalizeEnvironment,
746
916
  profileForEnvironment,
747
917
  readConfig,
918
+ readExternalAgentInstallation,
748
919
  refreshContextCredentialIfNeeded,
749
920
  refreshCredentialIfNeeded,
750
921
  resolveAgentName,
@@ -755,5 +926,6 @@ export {
755
926
  topoloServiceOrigins,
756
927
  writeConfig,
757
928
  writeContextCredential,
929
+ writeExternalAgentInstallation,
758
930
  writeProfileConfig
759
931
  };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
3
  "packageManager": "pnpm@10.17.1",
4
- "version": "0.19.0",
5
- "topoloPublishedVersion": "0.19.0",
4
+ "version": "0.20.0",
5
+ "topoloPublishedVersion": "0.20.0",
6
6
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
7
7
  "license": "UNLICENSED",
8
8
  "author": "Topolo",