@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.
- package/dist/index.cjs +247 -171
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +247 -171
- package/dist/node/credential-store.cjs +133 -74
- package/dist/node/credential-store.d.cts +51 -1
- package/dist/node/credential-store.d.ts +51 -1
- package/dist/node/credential-store.js +130 -72
- package/package.json +2 -2
|
@@ -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
|
|
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/
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
493
|
+
"--label=Topolo protected credential",
|
|
549
494
|
"service",
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" ?
|
|
651
|
-
return
|
|
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.
|
|
5
|
-
"topoloPublishedVersion": "0.21.
|
|
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",
|