@topolo/sdk 0.11.0 → 0.11.1
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/README.md +3 -11
- package/dist/node/credential-store.cjs +75 -44
- package/dist/node/credential-store.js +76 -45
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -191,25 +191,17 @@ Still routes through auth + audit headers. Mutating methods still require
|
|
|
191
191
|
| `TopoloHttpError` | Other non-2xx responses |
|
|
192
192
|
| `TopoloSdkError` | Base class |
|
|
193
193
|
|
|
194
|
-
## Service URL
|
|
194
|
+
## Service URL overrides
|
|
195
195
|
|
|
196
|
-
Defaults target production.
|
|
196
|
+
Defaults target production. Self-hosted installations may override a service:
|
|
197
197
|
|
|
198
198
|
```ts
|
|
199
199
|
createTopolo({
|
|
200
200
|
/* ... */
|
|
201
201
|
serviceUrls: {
|
|
202
|
-
mail: 'https://mail.
|
|
202
|
+
mail: 'https://mail.example.com',
|
|
203
203
|
},
|
|
204
204
|
});
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
Or via env: `TOPOLO_SERVICE_URL_MAIL=...` (falls through `resolveServiceUrl`).
|
|
208
|
-
|
|
209
|
-
## Development
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
npm install
|
|
213
|
-
npm run build
|
|
214
|
-
npm run typecheck
|
|
215
|
-
```
|
|
@@ -571,6 +571,41 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
571
571
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
572
572
|
}
|
|
573
573
|
var REFRESH_SKEW_SECONDS = 60;
|
|
574
|
+
var CONFIG_LOCK_RETRY_MS = 25;
|
|
575
|
+
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
576
|
+
var CONFIG_LOCK_STALE_MS = 12e4;
|
|
577
|
+
async function withConfigWriteLock(operation) {
|
|
578
|
+
const lockPath = `${configPath()}.lock`;
|
|
579
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(lockPath), { recursive: true });
|
|
580
|
+
const startedAt = Date.now();
|
|
581
|
+
while (true) {
|
|
582
|
+
try {
|
|
583
|
+
await (0, import_promises.mkdir)(lockPath);
|
|
584
|
+
break;
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error.code !== "EEXIST") throw error;
|
|
587
|
+
try {
|
|
588
|
+
const lock = await (0, import_promises.stat)(lockPath);
|
|
589
|
+
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
590
|
+
await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
} catch (inspectionError) {
|
|
594
|
+
if (inspectionError.code !== "ENOENT") throw inspectionError;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
598
|
+
throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
|
|
599
|
+
}
|
|
600
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
return await operation();
|
|
605
|
+
} finally {
|
|
606
|
+
await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
574
609
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
575
610
|
const prefix = envVarPrefix(environment);
|
|
576
611
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
@@ -624,51 +659,47 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
624
659
|
}
|
|
625
660
|
}
|
|
626
661
|
async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
serviceUrls
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
[key]: {
|
|
662
|
-
...context,
|
|
663
|
-
credential: refreshed,
|
|
664
|
-
oauthClientId
|
|
662
|
+
return withConfigWriteLock(async () => {
|
|
663
|
+
const config = await readConfig();
|
|
664
|
+
const profile = profileForEnvironment(config, environment);
|
|
665
|
+
const context = profile.contexts?.[key];
|
|
666
|
+
const credential = context?.credential;
|
|
667
|
+
if (!credential || credential.kind !== "access_token") return credential ?? null;
|
|
668
|
+
const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
|
|
669
|
+
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
670
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
671
|
+
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
672
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
673
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
674
|
+
throw new TopoloSdkError(
|
|
675
|
+
"environment_service_url_required",
|
|
676
|
+
"Local Topolo context refresh requires an explicit local Auth service origin."
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const oauth = new TopoloOAuth({ serviceUrls });
|
|
681
|
+
const tokens = await oauth.refreshToken({
|
|
682
|
+
clientId: oauthClientId,
|
|
683
|
+
refreshToken: credential.refreshToken
|
|
684
|
+
});
|
|
685
|
+
const refreshed = {
|
|
686
|
+
kind: "access_token",
|
|
687
|
+
accessToken: tokens.access_token,
|
|
688
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
|
|
689
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
690
|
+
};
|
|
691
|
+
await writeConfig(writeProfileConfig(config, environment, {
|
|
692
|
+
...profile.activeContext === key ? { credential: refreshed } : {},
|
|
693
|
+
contexts: {
|
|
694
|
+
...profile.contexts ?? {},
|
|
695
|
+
[key]: { ...context, credential: refreshed, oauthClientId }
|
|
665
696
|
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
697
|
+
}));
|
|
698
|
+
return refreshed;
|
|
699
|
+
} catch {
|
|
700
|
+
return credential;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
672
703
|
}
|
|
673
704
|
function resolveAgentName(configAgentName) {
|
|
674
705
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/node/credential-store.ts
|
|
2
|
-
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
2
|
+
import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { homedir, platform } from "os";
|
|
5
5
|
import { dirname, join } from "path";
|
|
@@ -524,6 +524,41 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
524
524
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
525
525
|
}
|
|
526
526
|
var REFRESH_SKEW_SECONDS = 60;
|
|
527
|
+
var CONFIG_LOCK_RETRY_MS = 25;
|
|
528
|
+
var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
529
|
+
var CONFIG_LOCK_STALE_MS = 12e4;
|
|
530
|
+
async function withConfigWriteLock(operation) {
|
|
531
|
+
const lockPath = `${configPath()}.lock`;
|
|
532
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
533
|
+
const startedAt = Date.now();
|
|
534
|
+
while (true) {
|
|
535
|
+
try {
|
|
536
|
+
await mkdir(lockPath);
|
|
537
|
+
break;
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (error.code !== "EEXIST") throw error;
|
|
540
|
+
try {
|
|
541
|
+
const lock = await stat(lockPath);
|
|
542
|
+
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
543
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
} catch (inspectionError) {
|
|
547
|
+
if (inspectionError.code !== "ENOENT") throw inspectionError;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
|
|
551
|
+
throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
|
|
552
|
+
}
|
|
553
|
+
await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
return await operation();
|
|
558
|
+
} finally {
|
|
559
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
527
562
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
528
563
|
const prefix = envVarPrefix(environment);
|
|
529
564
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
|
|
@@ -577,51 +612,47 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
|
|
|
577
612
|
}
|
|
578
613
|
}
|
|
579
614
|
async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
serviceUrls
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
[key]: {
|
|
615
|
-
...context,
|
|
616
|
-
credential: refreshed,
|
|
617
|
-
oauthClientId
|
|
615
|
+
return withConfigWriteLock(async () => {
|
|
616
|
+
const config = await readConfig();
|
|
617
|
+
const profile = profileForEnvironment(config, environment);
|
|
618
|
+
const context = profile.contexts?.[key];
|
|
619
|
+
const credential = context?.credential;
|
|
620
|
+
if (!credential || credential.kind !== "access_token") return credential ?? null;
|
|
621
|
+
const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
|
|
622
|
+
if (!credential.refreshToken || !oauthClientId) return credential;
|
|
623
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
624
|
+
if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
|
|
625
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
626
|
+
if (environment === "local" && !serviceUrls?.auth) {
|
|
627
|
+
throw new TopoloSdkError(
|
|
628
|
+
"environment_service_url_required",
|
|
629
|
+
"Local Topolo context refresh requires an explicit local Auth service origin."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const oauth = new TopoloOAuth({ serviceUrls });
|
|
634
|
+
const tokens = await oauth.refreshToken({
|
|
635
|
+
clientId: oauthClientId,
|
|
636
|
+
refreshToken: credential.refreshToken
|
|
637
|
+
});
|
|
638
|
+
const refreshed = {
|
|
639
|
+
kind: "access_token",
|
|
640
|
+
accessToken: tokens.access_token,
|
|
641
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
|
|
642
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
643
|
+
};
|
|
644
|
+
await writeConfig(writeProfileConfig(config, environment, {
|
|
645
|
+
...profile.activeContext === key ? { credential: refreshed } : {},
|
|
646
|
+
contexts: {
|
|
647
|
+
...profile.contexts ?? {},
|
|
648
|
+
[key]: { ...context, credential: refreshed, oauthClientId }
|
|
618
649
|
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
}
|
|
650
|
+
}));
|
|
651
|
+
return refreshed;
|
|
652
|
+
} catch {
|
|
653
|
+
return credential;
|
|
654
|
+
}
|
|
655
|
+
});
|
|
625
656
|
}
|
|
626
657
|
function resolveAgentName(configAgentName) {
|
|
627
658
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/sdk",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"topoloPublishedVersion": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
|
+
"topoloPublishedVersion": "0.11.1",
|
|
5
5
|
"description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "Topolo",
|
|
8
|
+
"homepage": "https://docs.topolo.app",
|
|
6
9
|
"type": "module",
|
|
7
10
|
"main": "./dist/index.js",
|
|
8
11
|
"module": "./dist/index.js",
|