@topolo/sdk 0.21.1 → 0.21.3

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";
@@ -428,6 +372,8 @@ import { existsSync } from "fs";
428
372
  import { homedir, platform as hostPlatform } from "os";
429
373
  import { dirname, join } from "path";
430
374
  var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
375
+ var MACOS_KEYCHAIN_CHUNK_SIZE = 96;
376
+ var MACOS_KEYCHAIN_CHUNK_PREFIX = "topolo-keychain-chunks:v1:";
431
377
  var MACOS_KEYCHAIN_WRITE_SCRIPT = [
432
378
  "log_user 0",
433
379
  "set timeout 10",
@@ -467,41 +413,75 @@ function createPlatformInstallationSecretBackend(options = {}) {
467
413
  const platform2 = options.platform ?? hostPlatform();
468
414
  const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
469
415
  const root = options.root ?? installationStorePath();
416
+ const service = options.service ?? KEYCHAIN_SERVICE;
470
417
  if (platform2 === "darwin") {
418
+ const write = (account, value) => runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
419
+ input: `${value}
420
+ `,
421
+ encoding: "utf8",
422
+ env: {
423
+ ...process.env,
424
+ TOPOLO_KEYCHAIN_ACCOUNT: account,
425
+ TOPOLO_KEYCHAIN_SERVICE: service
426
+ }
427
+ }), "macOS Keychain write");
428
+ const read = (account) => {
429
+ const result = spawn("security", [
430
+ "find-generic-password",
431
+ "-a",
432
+ account,
433
+ "-s",
434
+ service,
435
+ "-w"
436
+ ], { encoding: "utf8" });
437
+ if (result.status === 44) return null;
438
+ return runOrThrow(result, "macOS Keychain read");
439
+ };
440
+ const remove = (account) => {
441
+ const result = spawn("security", [
442
+ "delete-generic-password",
443
+ "-a",
444
+ account,
445
+ "-s",
446
+ service
447
+ ], { encoding: "utf8" });
448
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
449
+ };
450
+ const chunkAccount = (account, index) => `${account}:chunk:${index}`;
451
+ const chunkCount = (value) => {
452
+ if (!value?.startsWith(MACOS_KEYCHAIN_CHUNK_PREFIX)) return 0;
453
+ const count = Number(value.slice(MACOS_KEYCHAIN_CHUNK_PREFIX.length));
454
+ if (!Number.isSafeInteger(count) || count < 1) throw new Error("macOS Keychain chunk manifest is invalid.");
455
+ return count;
456
+ };
471
457
  return {
472
458
  async set(account, value) {
473
- runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
474
- input: `${value}
475
- `,
476
- encoding: "utf8",
477
- env: {
478
- ...process.env,
479
- TOPOLO_KEYCHAIN_ACCOUNT: account,
480
- TOPOLO_KEYCHAIN_SERVICE: KEYCHAIN_SERVICE
481
- }
482
- }), "macOS Keychain write");
459
+ const previousCount = chunkCount(read(account));
460
+ const chunks = Array.from(
461
+ { length: Math.max(1, Math.ceil(value.length / MACOS_KEYCHAIN_CHUNK_SIZE)) },
462
+ (_, index) => value.slice(index * MACOS_KEYCHAIN_CHUNK_SIZE, (index + 1) * MACOS_KEYCHAIN_CHUNK_SIZE)
463
+ );
464
+ try {
465
+ chunks.forEach((chunk, index) => write(chunkAccount(account, index), chunk));
466
+ write(account, `${MACOS_KEYCHAIN_CHUNK_PREFIX}${chunks.length}`);
467
+ for (let index = chunks.length; index < previousCount; index += 1) remove(chunkAccount(account, index));
468
+ } catch (error) {
469
+ chunks.forEach((_, index) => remove(chunkAccount(account, index)));
470
+ throw error;
471
+ }
483
472
  },
484
473
  async get(account) {
485
- const result = spawn("security", [
486
- "find-generic-password",
487
- "-a",
488
- account,
489
- "-s",
490
- KEYCHAIN_SERVICE,
491
- "-w"
492
- ], { encoding: "utf8" });
493
- if (result.status === 44) return null;
494
- return runOrThrow(result, "macOS Keychain read");
474
+ const manifest = read(account);
475
+ const count = chunkCount(manifest);
476
+ if (!count) return manifest;
477
+ const chunks = Array.from({ length: count }, (_, index) => read(chunkAccount(account, index)));
478
+ if (chunks.some((chunk) => chunk === null)) throw new Error("macOS Keychain installation secret is incomplete.");
479
+ return chunks.join("");
495
480
  },
496
481
  async delete(account) {
497
- const result = spawn("security", [
498
- "delete-generic-password",
499
- "-a",
500
- account,
501
- "-s",
502
- KEYCHAIN_SERVICE
503
- ], { encoding: "utf8" });
504
- if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
482
+ const count = chunkCount(read(account));
483
+ for (let index = 0; index < count; index += 1) remove(chunkAccount(account, index));
484
+ remove(account);
505
485
  }
506
486
  };
507
487
  }
@@ -510,9 +490,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
510
490
  async set(account, value) {
511
491
  runOrThrow(spawn("secret-tool", [
512
492
  "store",
513
- "--label=Topolo external agent installation",
493
+ "--label=Topolo protected credential",
514
494
  "service",
515
- KEYCHAIN_SERVICE,
495
+ service,
516
496
  "account",
517
497
  account
518
498
  ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
@@ -521,7 +501,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
521
501
  const result = spawn("secret-tool", [
522
502
  "lookup",
523
503
  "service",
524
- KEYCHAIN_SERVICE,
504
+ service,
525
505
  "account",
526
506
  account
527
507
  ], { encoding: "utf8" });
@@ -532,7 +512,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
532
512
  const result = spawn("secret-tool", [
533
513
  "clear",
534
514
  "service",
535
- KEYCHAIN_SERVICE,
515
+ service,
536
516
  "account",
537
517
  account
538
518
  ], { encoding: "utf8" });
@@ -603,6 +583,118 @@ async function deleteExternalAgentInstallation(environment, id, backend = create
603
583
  await rm(installationPath(environment, id), { force: true });
604
584
  }
605
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
+
606
698
  // src/node/credential-store.ts
607
699
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
608
700
  function contextKey(contextType, organizationId) {
@@ -612,8 +704,8 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
612
704
  function configPath() {
613
705
  const override = process.env.TOPOLO_CONFIG_PATH;
614
706
  if (override) return override;
615
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
616
- 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");
617
709
  }
618
710
  async function readConfig() {
619
711
  const path = configPath();
@@ -923,6 +1015,7 @@ export {
923
1015
  configPath,
924
1016
  contextKey,
925
1017
  createPlatformInstallationSecretBackend,
1018
+ createProtectedActionSecretStore,
926
1019
  credentialSourceFromConfig,
927
1020
  deleteExternalAgentInstallation,
928
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.1",
5
- "topoloPublishedVersion": "0.21.1",
4
+ "version": "0.21.3",
5
+ "topoloPublishedVersion": "0.21.3",
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",