@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/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
- const overrideUrl = resolveConfiguredServiceUrl(serviceKey, this.serviceUrls);
2208
- if (overrideUrl) return { serviceKey, baseUrl: overrideUrl };
2209
- const staticUrl = DEFAULT_SERVICE_URLS[serviceKey];
2210
- if (staticUrl) return { serviceKey, baseUrl: staticUrl };
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) return 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 an authenticated caller in an
2288
- * entitled org also receives the `developer_only` services its role
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
- * Resilient: on any network/HTTP failure this falls back to the
2292
- * build-time generated `APPLICATIONS` snapshot so offline/degraded
2293
- * callers still get a usable (if static) catalog. The live result is
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
- try {
2302
- const res = await this.request({ service: "auth", method: "GET", path: "/api/services/catalog" });
2303
- const raw = res.data?.services ?? res.services;
2304
- if (Array.isArray(raw) && raw.length > 0) {
2305
- const entries = [];
2306
- for (const item of raw) {
2307
- const s = item;
2308
- const serviceId = typeof s.service_id === "string" ? s.service_id : typeof s.id === "string" ? s.id : null;
2309
- if (!serviceId) continue;
2310
- entries.push({
2311
- serviceId,
2312
- slug: typeof s.app_slug === "string" ? s.app_slug : null,
2313
- name: typeof s.name === "string" ? s.name : null,
2314
- description: typeof s.description === "string" ? s.description : null,
2315
- baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2316
- status: typeof s.status === "string" ? s.status : null,
2317
- surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2318
- launchable: s.launchable === true
2319
- });
2320
- }
2321
- if (entries.length > 0) {
2322
- this.serviceCatalogCache = {
2323
- entries,
2324
- expiresAt: now + CATALOG_CACHE_TTL_MS
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
- return this.generatedCatalogFallback();
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
- generatedCatalogFallback() {
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: "active",
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
- keys.add(normalized);
2425
- keys.add(normalized.replace(/-/g, "_"));
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(entry.slug && Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, catalogEntryLookupKeys(entry)[0] ?? ""));
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",