@topolo/sdk 0.4.1 → 0.4.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/README.md +40 -13
- package/dist/index.cjs +209 -57
- package/dist/index.d.cts +47 -10
- package/dist/index.d.ts +47 -10
- package/dist/index.js +209 -57
- package/package.json +1 -1
- package/src/client.test.ts +231 -28
- package/src/client.ts +285 -70
- package/src/index.ts +1 -0
- package/src/modules/p2p.test.ts +25 -2
- package/src/services.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -2069,6 +2069,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2069
2069
|
fetchImpl;
|
|
2070
2070
|
debug;
|
|
2071
2071
|
serviceCatalogCache = null;
|
|
2072
|
+
actionCatalogCache = null;
|
|
2072
2073
|
/**
|
|
2073
2074
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2074
2075
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2204,12 +2205,16 @@ var TopoloClient = class _TopoloClient {
|
|
|
2204
2205
|
if (!serviceKey) {
|
|
2205
2206
|
throw new TopoloSdkError("unknown_service", "service is required");
|
|
2206
2207
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2208
|
+
if (serviceKey === "auth") {
|
|
2209
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
2210
|
+
if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
|
|
2211
|
+
return { serviceKey, baseUrl: DEFAULT_SERVICE_URLS.auth };
|
|
2212
|
+
}
|
|
2211
2213
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
2212
|
-
if (live)
|
|
2214
|
+
if (live) {
|
|
2215
|
+
const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
|
|
2216
|
+
return overrideUrl ? { ...live, baseUrl: overrideUrl } : live;
|
|
2217
|
+
}
|
|
2213
2218
|
throw new TopoloSdkError(
|
|
2214
2219
|
"unknown_service",
|
|
2215
2220
|
`Unknown Topolo service "${service}". Run service discovery first and pass a callable service id or slug.`
|
|
@@ -2219,10 +2224,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2219
2224
|
const catalog = await this.listServices();
|
|
2220
2225
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2221
2226
|
if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
|
|
2222
|
-
return {
|
|
2223
|
-
serviceKey: normalizeLookupKey(match.slug || match.name || match.serviceId) || serviceKey,
|
|
2224
|
-
baseUrl: match.baseUrl
|
|
2225
|
-
};
|
|
2227
|
+
return { serviceKey, baseUrl: match.baseUrl };
|
|
2226
2228
|
}
|
|
2227
2229
|
/**
|
|
2228
2230
|
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
@@ -2284,53 +2286,112 @@ var TopoloClient = class _TopoloClient {
|
|
|
2284
2286
|
/**
|
|
2285
2287
|
* List the platform service catalog from the live registry
|
|
2286
2288
|
* (`GET /api/services/catalog` on TopoloAuth). Credential-aware: the
|
|
2287
|
-
* caller's credential is attached, so
|
|
2288
|
-
*
|
|
2289
|
-
* permits; an unauthenticated/agent caller receives `active` only.
|
|
2289
|
+
* caller's credential is attached, so the result only includes services the
|
|
2290
|
+
* credential's organization and effective permissions can use.
|
|
2290
2291
|
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2293
|
-
*
|
|
2294
|
-
* authoritative; the snapshot is a floor, not a cap.
|
|
2292
|
+
* Security boundary: this never falls back to the generated global snapshot.
|
|
2293
|
+
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2294
|
+
* organization and effective permissions.
|
|
2295
2295
|
*/
|
|
2296
2296
|
async listServices() {
|
|
2297
2297
|
const now = Date.now();
|
|
2298
2298
|
if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
|
|
2299
2299
|
return this.serviceCatalogCache.entries;
|
|
2300
2300
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
return entries;
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
} catch {
|
|
2301
|
+
const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
|
|
2302
|
+
const raw = res.data?.services ?? res.services;
|
|
2303
|
+
if (!Array.isArray(raw)) {
|
|
2304
|
+
throw new TopoloSdkError(
|
|
2305
|
+
"service_catalog_unavailable",
|
|
2306
|
+
"TopoloAuth did not return a service catalog for the current credential."
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2309
|
+
const entries = [];
|
|
2310
|
+
for (const item of raw) {
|
|
2311
|
+
const s = item;
|
|
2312
|
+
const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
|
|
2313
|
+
if (!serviceId) continue;
|
|
2314
|
+
const permissions = Array.isArray(s.granted_permissions) ? s.granted_permissions.filter((permission) => typeof permission === "string" && permission.trim().length > 0) : [];
|
|
2315
|
+
entries.push({
|
|
2316
|
+
serviceId,
|
|
2317
|
+
slug: typeof s.app_slug === "string" ? s.app_slug : null,
|
|
2318
|
+
name: typeof s.name === "string" ? s.name : null,
|
|
2319
|
+
description: typeof s.description === "string" ? s.description : null,
|
|
2320
|
+
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2321
|
+
status: typeof s.status === "string" ? s.status : null,
|
|
2322
|
+
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2323
|
+
launchable: s.launchable === true,
|
|
2324
|
+
permissions
|
|
2325
|
+
});
|
|
2330
2326
|
}
|
|
2331
|
-
|
|
2327
|
+
this.serviceCatalogCache = {
|
|
2328
|
+
entries,
|
|
2329
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2330
|
+
};
|
|
2331
|
+
return entries;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2335
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2336
|
+
* action list is used as an authorization source.
|
|
2337
|
+
*/
|
|
2338
|
+
async listActions(options = {}) {
|
|
2339
|
+
const now = Date.now();
|
|
2340
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2341
|
+
return this.actionCatalogCache.entries;
|
|
2342
|
+
}
|
|
2343
|
+
const res = await this.request({
|
|
2344
|
+
service: "auth",
|
|
2345
|
+
method: "GET",
|
|
2346
|
+
path: "/api/actions/catalog",
|
|
2347
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2348
|
+
});
|
|
2349
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2350
|
+
if (!Array.isArray(raw)) {
|
|
2351
|
+
throw new TopoloSdkError(
|
|
2352
|
+
"action_catalog_unavailable",
|
|
2353
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2357
|
+
if (!options.service) {
|
|
2358
|
+
this.actionCatalogCache = {
|
|
2359
|
+
entries,
|
|
2360
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
return entries;
|
|
2364
|
+
}
|
|
2365
|
+
async getAction(action) {
|
|
2366
|
+
const needle = normalizeActionLookupKey(action);
|
|
2367
|
+
const actions = await this.listActions();
|
|
2368
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2369
|
+
if (!match) {
|
|
2370
|
+
throw new TopoloSdkError(
|
|
2371
|
+
"unknown_action",
|
|
2372
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
return match;
|
|
2376
|
+
}
|
|
2377
|
+
async callAction(action, input = {}, options = {}) {
|
|
2378
|
+
const entry = await this.getAction(action);
|
|
2379
|
+
const mapped = mapActionInput(entry, input);
|
|
2380
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2381
|
+
return this.request({
|
|
2382
|
+
service,
|
|
2383
|
+
method: entry.method,
|
|
2384
|
+
path: mapped.path,
|
|
2385
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2386
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2387
|
+
confirm: options.confirm === true
|
|
2388
|
+
});
|
|
2332
2389
|
}
|
|
2333
|
-
|
|
2390
|
+
/**
|
|
2391
|
+
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2392
|
+
* for deciding what the current credential may use.
|
|
2393
|
+
*/
|
|
2394
|
+
listBundledServices() {
|
|
2334
2395
|
const apps = Object.values(APPLICATIONS);
|
|
2335
2396
|
return apps.map((app) => ({
|
|
2336
2397
|
serviceId: app.id,
|
|
@@ -2338,9 +2399,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2338
2399
|
name: app.name ?? app.id,
|
|
2339
2400
|
description: null,
|
|
2340
2401
|
baseUrl: app.productionUrl ?? null,
|
|
2341
|
-
status: "
|
|
2402
|
+
status: "bundled",
|
|
2342
2403
|
surfaceKind: "application",
|
|
2343
|
-
launchable: Boolean(app.productionUrl)
|
|
2404
|
+
launchable: Boolean(app.productionUrl),
|
|
2405
|
+
permissions: []
|
|
2344
2406
|
}));
|
|
2345
2407
|
}
|
|
2346
2408
|
/**
|
|
@@ -2410,7 +2472,7 @@ function serviceKeyCandidates(slug) {
|
|
|
2410
2472
|
])];
|
|
2411
2473
|
}
|
|
2412
2474
|
function normalizeLookupKey(value) {
|
|
2413
|
-
return String(value || "").trim().toLowerCase();
|
|
2475
|
+
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2414
2476
|
}
|
|
2415
2477
|
function catalogEntryMatches(entry, needle) {
|
|
2416
2478
|
return catalogEntryLookupKeys(entry).includes(needle);
|
|
@@ -2421,12 +2483,8 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2421
2483
|
for (const value of values) {
|
|
2422
2484
|
const normalized = normalizeLookupKey(value);
|
|
2423
2485
|
if (!normalized) continue;
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
if (normalized.startsWith("topolo-")) {
|
|
2427
|
-
const withoutTopolo = normalized.slice("topolo-".length);
|
|
2428
|
-
keys.add(withoutTopolo);
|
|
2429
|
-
keys.add(withoutTopolo.replace(/-/g, "_"));
|
|
2486
|
+
for (const candidate of serviceKeyCandidates(normalized)) {
|
|
2487
|
+
keys.add(candidate);
|
|
2430
2488
|
}
|
|
2431
2489
|
}
|
|
2432
2490
|
return [...keys];
|
|
@@ -2434,7 +2492,101 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2434
2492
|
function isCallableCatalogEntry(entry) {
|
|
2435
2493
|
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2436
2494
|
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2437
|
-
return Boolean(
|
|
2495
|
+
return Boolean(
|
|
2496
|
+
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
function normalizeActionCatalogEntry(item) {
|
|
2500
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2501
|
+
const a = item;
|
|
2502
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2503
|
+
const name = pickActionString(a.name);
|
|
2504
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2505
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2506
|
+
const path = pickActionString(a.path);
|
|
2507
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
actionId,
|
|
2512
|
+
name,
|
|
2513
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2514
|
+
title: pickActionString(a.title) ?? name,
|
|
2515
|
+
description: pickActionString(a.description) ?? "",
|
|
2516
|
+
serviceId,
|
|
2517
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2518
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2519
|
+
method,
|
|
2520
|
+
path,
|
|
2521
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2522
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2523
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2524
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2525
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2526
|
+
destructive: Boolean(a.destructive),
|
|
2527
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2528
|
+
status: pickActionString(a.status),
|
|
2529
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function isHttpMethod(value) {
|
|
2533
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2534
|
+
}
|
|
2535
|
+
function pickActionString(value) {
|
|
2536
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2537
|
+
}
|
|
2538
|
+
function plainObject(value) {
|
|
2539
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2540
|
+
}
|
|
2541
|
+
function normalizeActionLookupKey(value) {
|
|
2542
|
+
return String(value || "").trim().toLowerCase();
|
|
2543
|
+
}
|
|
2544
|
+
function actionEntryMatches(entry, needle) {
|
|
2545
|
+
if (!needle) return false;
|
|
2546
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2547
|
+
}
|
|
2548
|
+
function mapActionInput(action, input) {
|
|
2549
|
+
const remaining = { ...input || {} };
|
|
2550
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2551
|
+
const value = remaining[key];
|
|
2552
|
+
if (value === void 0 || value === null || value === "") {
|
|
2553
|
+
throw new TopoloSdkError(
|
|
2554
|
+
"invalid_action_input",
|
|
2555
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
delete remaining[key];
|
|
2559
|
+
return encodeURIComponent(String(value));
|
|
2560
|
+
});
|
|
2561
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2562
|
+
delete remaining.query;
|
|
2563
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2564
|
+
delete remaining.body;
|
|
2565
|
+
if (action.method === "GET") {
|
|
2566
|
+
return {
|
|
2567
|
+
path,
|
|
2568
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
const query = objectToQuery(explicitQuery);
|
|
2572
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2573
|
+
return {
|
|
2574
|
+
path,
|
|
2575
|
+
...query ? { query } : {},
|
|
2576
|
+
body
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
function objectToQuery(value) {
|
|
2580
|
+
const query = {};
|
|
2581
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2582
|
+
if (raw === void 0 || raw === null) continue;
|
|
2583
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2584
|
+
query[key] = raw;
|
|
2585
|
+
} else {
|
|
2586
|
+
query[key] = JSON.stringify(raw);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2438
2590
|
}
|
|
2439
2591
|
function describeError(body, fallback) {
|
|
2440
2592
|
if (body && typeof body === "object") {
|