@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.
- package/dist/index.cjs +246 -171
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +246 -171
- package/dist/node/credential-store.cjs +193 -99
- package/dist/node/credential-store.d.cts +51 -1
- package/dist/node/credential-store.d.ts +51 -1
- package/dist/node/credential-store.js +190 -97
- 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";
|
|
@@ -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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
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
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
493
|
+
"--label=Topolo protected credential",
|
|
514
494
|
"service",
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" ?
|
|
616
|
-
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");
|
|
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.
|
|
5
|
-
"topoloPublishedVersion": "0.21.
|
|
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",
|