@topolo/sdk 0.21.2 → 0.21.4

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.
@@ -2,7 +2,7 @@
2
2
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3
3
  import { existsSync as existsSync2 } from "fs";
4
4
  import { homedir as homedir2, platform } from "os";
5
- import { dirname as dirname2, join as join2 } from "path";
5
+ import { dirname as dirname2, join as join3 } from "path";
6
6
 
7
7
  // src/errors.ts
8
8
  var TopoloSdkError = class extends Error {
@@ -358,68 +358,12 @@ var TopoloOAuth = class {
358
358
  }
359
359
  };
360
360
 
361
- // src/environment.ts
362
- var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
363
- var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
364
- production: {
365
- auth: "https://auth.topolo.app",
366
- developers: "https://developers.topolo.app"
367
- },
368
- staging: {
369
- auth: "https://auth.stg.topolo.us",
370
- developers: "https://developers.stg.topolo.us"
371
- },
372
- development: {
373
- auth: "https://auth.topolo.dev",
374
- developers: "https://developers.topolo.dev"
375
- },
376
- local: {}
377
- };
378
- function hostnameHasSuffix(hostname, suffix) {
379
- return hostname === suffix || hostname.endsWith(`.${suffix}`);
380
- }
381
- function workersDevAccountMatches(hostname, account) {
382
- if (!hostname.endsWith(".workers.dev")) return false;
383
- const labels = hostname.slice(0, -".workers.dev".length).split(".");
384
- return labels.length >= 2 && labels.at(-1) === account;
385
- }
386
- function originMatchesEnvironment(url, environment) {
387
- const hostname = url.hostname.toLowerCase();
388
- if (environment === "local") {
389
- return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
390
- }
391
- if (url.protocol !== "https:") return false;
392
- if (environment === "production") {
393
- return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches(hostname, "topolo");
394
- }
395
- if (environment === "staging") {
396
- return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
397
- }
398
- return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
399
- }
400
- function assertTopoloServiceUrlEnvironment(service, value, environment) {
401
- let url;
402
- try {
403
- url = new URL(value);
404
- } catch {
405
- throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
406
- }
407
- if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
408
- throw new TopoloSdkError(
409
- "environment_mismatch",
410
- `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
411
- );
412
- }
413
- return url.origin;
414
- }
415
- function topoloServiceOrigins(serviceUrls, environment) {
416
- return Object.fromEntries(
417
- Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
418
- service,
419
- assertTopoloServiceUrlEnvironment(service, value, environment)
420
- ])
421
- );
422
- }
361
+ // src/node/action-secret-store.ts
362
+ import { createHash, randomUUID } from "crypto";
363
+ import { join as join2 } from "path";
364
+
365
+ // src/action-secrets.ts
366
+ var ACTION_SECRET_PREFIX = "topolo-secret:";
423
367
 
424
368
  // src/node/installation-store.ts
425
369
  import { spawnSync } from "child_process";
@@ -469,6 +413,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
469
413
  const platform2 = options.platform ?? hostPlatform();
470
414
  const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
471
415
  const root = options.root ?? installationStorePath();
416
+ const service = options.service ?? KEYCHAIN_SERVICE;
472
417
  if (platform2 === "darwin") {
473
418
  const write = (account, value) => runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
474
419
  input: `${value}
@@ -477,7 +422,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
477
422
  env: {
478
423
  ...process.env,
479
424
  TOPOLO_KEYCHAIN_ACCOUNT: account,
480
- TOPOLO_KEYCHAIN_SERVICE: KEYCHAIN_SERVICE
425
+ TOPOLO_KEYCHAIN_SERVICE: service
481
426
  }
482
427
  }), "macOS Keychain write");
483
428
  const read = (account) => {
@@ -486,7 +431,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
486
431
  "-a",
487
432
  account,
488
433
  "-s",
489
- KEYCHAIN_SERVICE,
434
+ service,
490
435
  "-w"
491
436
  ], { encoding: "utf8" });
492
437
  if (result.status === 44) return null;
@@ -498,7 +443,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
498
443
  "-a",
499
444
  account,
500
445
  "-s",
501
- KEYCHAIN_SERVICE
446
+ service
502
447
  ], { encoding: "utf8" });
503
448
  if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
504
449
  };
@@ -545,9 +490,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
545
490
  async set(account, value) {
546
491
  runOrThrow(spawn("secret-tool", [
547
492
  "store",
548
- "--label=Topolo external agent installation",
493
+ "--label=Topolo protected credential",
549
494
  "service",
550
- KEYCHAIN_SERVICE,
495
+ service,
551
496
  "account",
552
497
  account
553
498
  ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
@@ -556,7 +501,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
556
501
  const result = spawn("secret-tool", [
557
502
  "lookup",
558
503
  "service",
559
- KEYCHAIN_SERVICE,
504
+ service,
560
505
  "account",
561
506
  account
562
507
  ], { encoding: "utf8" });
@@ -567,7 +512,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
567
512
  const result = spawn("secret-tool", [
568
513
  "clear",
569
514
  "service",
570
- KEYCHAIN_SERVICE,
515
+ service,
571
516
  "account",
572
517
  account
573
518
  ], { encoding: "utf8" });
@@ -638,6 +583,118 @@ async function deleteExternalAgentInstallation(environment, id, backend = create
638
583
  await rm(installationPath(environment, id), { force: true });
639
584
  }
640
585
 
586
+ // src/node/action-secret-store.ts
587
+ function createProtectedActionSecretStore(options) {
588
+ const backend = options.backend ?? createPlatformInstallationSecretBackend({
589
+ service: "topolo-action-handoff",
590
+ root: join2(installationStorePath(), "action-handoff")
591
+ });
592
+ const now = options.now ?? Date.now;
593
+ async function account(reference) {
594
+ if (!/^topolo-secret:[0-9a-f-]{36}$/.test(reference)) throw new TopoloSdkError("invalid_action_secret_reference", "Invalid protected action reference.");
595
+ const identity = await options.identity();
596
+ if (!identity.user.id) throw new TopoloSdkError("action_secret_identity_required", "A verified identity is required for protected action handoff.");
597
+ if (identity.kind === "api_key" && !identity.apiKey?.id) throw new TopoloSdkError("action_secret_identity_required", "A verified API key identity is required for protected action handoff.");
598
+ const scope = createHash("sha256").update(JSON.stringify([
599
+ options.environment,
600
+ identity.user.principalId ?? identity.user.id,
601
+ identity.organization?.id ?? null,
602
+ identity.apiKey?.id ?? null
603
+ ])).digest("hex");
604
+ return `${scope}-${reference.slice(ACTION_SECRET_PREFIX.length)}`;
605
+ }
606
+ return {
607
+ async save(value) {
608
+ const reference = `${ACTION_SECRET_PREFIX}${randomUUID()}`;
609
+ await backend.set(await account(reference), JSON.stringify({ value, expiresAt: now() + 30 * 60 * 1e3 }));
610
+ return reference;
611
+ },
612
+ async load(reference) {
613
+ const key = await account(reference);
614
+ const encoded = await backend.get(key);
615
+ if (!encoded) throw new TopoloSdkError("action_secret_unavailable", "Protected action reference is missing or belongs to another identity or environment.");
616
+ let record;
617
+ try {
618
+ record = JSON.parse(encoded);
619
+ } catch {
620
+ throw new TopoloSdkError("action_secret_invalid", "Protected action reference is corrupt.");
621
+ }
622
+ if (!Number.isFinite(record?.expiresAt) || !Object.hasOwn(record, "value")) throw new TopoloSdkError("action_secret_invalid", "Protected action reference is corrupt.");
623
+ if (record.expiresAt <= now()) {
624
+ await backend.delete(key);
625
+ throw new TopoloSdkError("action_secret_expired", "Protected action reference expired. Repeat the originating action if its contract permits.");
626
+ }
627
+ return record.value;
628
+ },
629
+ async delete(reference) {
630
+ await backend.delete(await account(reference));
631
+ }
632
+ };
633
+ }
634
+
635
+ // src/environment.ts
636
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
637
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
638
+ production: {
639
+ auth: "https://auth.topolo.app",
640
+ developers: "https://developers.topolo.app"
641
+ },
642
+ staging: {
643
+ auth: "https://auth.stg.topolo.us",
644
+ developers: "https://developers.stg.topolo.us"
645
+ },
646
+ development: {
647
+ auth: "https://auth.topolo.dev",
648
+ developers: "https://developers.topolo.dev"
649
+ },
650
+ local: {}
651
+ };
652
+ function hostnameHasSuffix(hostname, suffix) {
653
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
654
+ }
655
+ function workersDevAccountMatches(hostname, account) {
656
+ if (!hostname.endsWith(".workers.dev")) return false;
657
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
658
+ return labels.length >= 2 && labels.at(-1) === account;
659
+ }
660
+ function originMatchesEnvironment(url, environment) {
661
+ const hostname = url.hostname.toLowerCase();
662
+ if (environment === "local") {
663
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
664
+ }
665
+ if (url.protocol !== "https:") return false;
666
+ if (environment === "production") {
667
+ return hostnameHasSuffix(hostname, "topolo.app") || hostnameHasSuffix(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches(hostname, "topolo");
668
+ }
669
+ if (environment === "staging") {
670
+ return hostnameHasSuffix(hostname, "stg.topolo.us") || workersDevAccountMatches(hostname, "topolo-staging");
671
+ }
672
+ return hostnameHasSuffix(hostname, "topolo.dev") || workersDevAccountMatches(hostname, "topolo-development");
673
+ }
674
+ function assertTopoloServiceUrlEnvironment(service, value, environment) {
675
+ let url;
676
+ try {
677
+ url = new URL(value);
678
+ } catch {
679
+ throw new TopoloSdkError("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
680
+ }
681
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment(url, environment)) {
682
+ throw new TopoloSdkError(
683
+ "environment_mismatch",
684
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
685
+ );
686
+ }
687
+ return url.origin;
688
+ }
689
+ function topoloServiceOrigins(serviceUrls, environment) {
690
+ return Object.fromEntries(
691
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
692
+ service,
693
+ assertTopoloServiceUrlEnvironment(service, value, environment)
694
+ ])
695
+ );
696
+ }
697
+
641
698
  // src/node/credential-store.ts
642
699
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
643
700
  function contextKey(contextType, organizationId) {
@@ -647,8 +704,8 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
647
704
  function configPath() {
648
705
  const override = process.env.TOPOLO_CONFIG_PATH;
649
706
  if (override) return override;
650
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
651
- return join2(base, "topolo", "config.json");
707
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
708
+ return join3(base, "topolo", "config.json");
652
709
  }
653
710
  async function readConfig() {
654
711
  const path = configPath();
@@ -958,6 +1015,7 @@ export {
958
1015
  configPath,
959
1016
  contextKey,
960
1017
  createPlatformInstallationSecretBackend,
1018
+ createProtectedActionSecretStore,
961
1019
  credentialSourceFromConfig,
962
1020
  deleteExternalAgentInstallation,
963
1021
  envVarPrefix,
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.21.2",
5
- "topoloPublishedVersion": "0.21.2",
4
+ "version": "0.21.4",
5
+ "topoloPublishedVersion": "0.21.4",
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",