@topolo/sdk 0.7.0 → 0.8.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/dist/index.js CHANGED
@@ -41,6 +41,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
41
41
  this.required = required;
42
42
  }
43
43
  };
44
+ var TopoloConfirmationRequiredError = class extends TopoloSdkError {
45
+ constructor(message) {
46
+ super("confirmation_required", message);
47
+ this.name = "TopoloConfirmationRequiredError";
48
+ }
49
+ };
44
50
  var TopoloHttpError = class extends TopoloSdkError {
45
51
  status;
46
52
  body;
@@ -1107,6 +1113,7 @@ var APPLICATIONS = {
1107
1113
  "topolo.io"
1108
1114
  ],
1109
1115
  "routes": [
1116
+ "one.topolo.io",
1110
1117
  "topolo.io",
1111
1118
  "www.topolo.io"
1112
1119
  ],
@@ -2066,7 +2073,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
2066
2073
  var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
2067
2074
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2068
2075
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2069
- var CATALOG_CACHE_TTL_MS = 60 * 1e3;
2070
2076
  var TopoloClient = class _TopoloClient {
2071
2077
  credential;
2072
2078
  agent;
@@ -2075,9 +2081,9 @@ var TopoloClient = class _TopoloClient {
2075
2081
  requireConfirmForWrites;
2076
2082
  timeoutMs;
2077
2083
  fetchImpl;
2084
+ refreshCredential;
2085
+ refreshInFlight = null;
2078
2086
  debug;
2079
- serviceCatalogCache = null;
2080
- actionCatalogCache = null;
2081
2087
  /**
2082
2088
  * Process-wide cache for slug → app-id lookups. Keyed by
2083
2089
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2102,6 +2108,7 @@ var TopoloClient = class _TopoloClient {
2102
2108
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2103
2109
  this.timeoutMs = options.timeoutMs ?? 3e4;
2104
2110
  this.fetchImpl = options.fetch ?? fetch;
2111
+ this.refreshCredential = options.refreshCredential;
2105
2112
  this.debug = options.debug;
2106
2113
  }
2107
2114
  emit(event) {
@@ -2119,11 +2126,11 @@ var TopoloClient = class _TopoloClient {
2119
2126
  async request(opts) {
2120
2127
  const method = opts.method ?? "GET";
2121
2128
  if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
2122
- throw new TopoloAuthError(
2129
+ throw new TopoloConfirmationRequiredError(
2123
2130
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2124
2131
  );
2125
2132
  }
2126
- const resolvedService = await this.resolveRequestService(opts.service);
2133
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2127
2134
  const baseUrl = resolvedService.baseUrl;
2128
2135
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
2129
2136
  if (opts.query) {
@@ -2154,20 +2161,36 @@ var TopoloClient = class _TopoloClient {
2154
2161
  headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
2155
2162
  headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
2156
2163
  }
2157
- const controller = new AbortController();
2158
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2159
- const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2160
2164
  const urlStr = url.toString();
2161
2165
  const startedAt = Date.now();
2162
2166
  this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
2167
+ const fetchOnce = async () => {
2168
+ const controller = new AbortController();
2169
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2170
+ const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2171
+ try {
2172
+ return await this.fetchImpl(urlStr, {
2173
+ method,
2174
+ headers: new Headers(headers),
2175
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2176
+ signal
2177
+ });
2178
+ } finally {
2179
+ clearTimeout(timeoutId);
2180
+ }
2181
+ };
2163
2182
  let res;
2164
2183
  try {
2165
- res = await this.fetchImpl(urlStr, {
2166
- method,
2167
- headers,
2168
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2169
- signal
2170
- });
2184
+ const attemptedCredential = this.credential;
2185
+ res = await fetchOnce();
2186
+ if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
2187
+ await res.body?.cancel().catch(() => void 0);
2188
+ const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
2189
+ if (refreshed) {
2190
+ applyAuthHeaders(headers, refreshed);
2191
+ res = await fetchOnce();
2192
+ }
2193
+ }
2171
2194
  } catch (err) {
2172
2195
  this.emit({
2173
2196
  phase: "error",
@@ -2180,8 +2203,6 @@ var TopoloClient = class _TopoloClient {
2180
2203
  error: err instanceof Error ? err.message : String(err)
2181
2204
  });
2182
2205
  throw err;
2183
- } finally {
2184
- clearTimeout(timeoutId);
2185
2206
  }
2186
2207
  const contentType = res.headers.get("Content-Type") ?? "";
2187
2208
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
@@ -2220,6 +2241,19 @@ var TopoloClient = class _TopoloClient {
2220
2241
  });
2221
2242
  return parsed;
2222
2243
  }
2244
+ async refreshRejectedAccessToken(attemptedAccessToken) {
2245
+ if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2246
+ return this.credential;
2247
+ }
2248
+ if (!this.refreshCredential) return null;
2249
+ this.refreshInFlight ??= this.refreshCredential().then((credential) => {
2250
+ if (credential) this.credential = credential;
2251
+ return credential;
2252
+ }).catch(() => null).finally(() => {
2253
+ this.refreshInFlight = null;
2254
+ });
2255
+ return this.refreshInFlight;
2256
+ }
2223
2257
  async resolveRequestService(service) {
2224
2258
  const serviceKey = normalizeLookupKey(service);
2225
2259
  if (!serviceKey) {
@@ -2245,9 +2279,8 @@ var TopoloClient = class _TopoloClient {
2245
2279
  );
2246
2280
  }
2247
2281
  async resolveLiveCallableService(serviceKey) {
2248
- const catalog = await this.listServices();
2249
- const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2250
- if (!match) return null;
2282
+ const match = await this.getService(serviceKey).catch(() => null);
2283
+ if (!match || !catalogEntryMatches(match, serviceKey)) return null;
2251
2284
  const baseUrl = resolveCatalogServiceUrl(match, {
2252
2285
  serviceKey,
2253
2286
  overrides: this.serviceUrls
@@ -2321,12 +2354,13 @@ var TopoloClient = class _TopoloClient {
2321
2354
  * The live Auth catalog is the authority because it is scoped to the caller's
2322
2355
  * organization and effective permissions.
2323
2356
  */
2324
- async listServices() {
2325
- const now = Date.now();
2326
- if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2327
- return this.serviceCatalogCache.entries;
2328
- }
2329
- const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2357
+ async searchServices(options = {}) {
2358
+ const res = await this.request({
2359
+ service: "developers",
2360
+ method: "GET",
2361
+ path: "/api/services/catalog",
2362
+ query: catalogSearchQuery(options)
2363
+ });
2330
2364
  const raw = res.data?.services ?? res.services;
2331
2365
  if (!Array.isArray(raw)) {
2332
2366
  throw new TopoloSdkError(
@@ -2336,6 +2370,7 @@ var TopoloClient = class _TopoloClient {
2336
2370
  }
2337
2371
  const entries = [];
2338
2372
  for (const item of raw) {
2373
+ if (!isRecord(item)) continue;
2339
2374
  const s = item;
2340
2375
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2341
2376
  if (!appId) continue;
@@ -2360,11 +2395,30 @@ var TopoloClient = class _TopoloClient {
2360
2395
  apiKeyResources
2361
2396
  });
2362
2397
  }
2363
- this.serviceCatalogCache = {
2364
- entries,
2365
- expiresAt: now + CATALOG_CACHE_TTL_MS
2366
- };
2367
- return entries;
2398
+ const page = normalizeCatalogPage(
2399
+ res.data?.page ?? res.page
2400
+ );
2401
+ return { services: entries, page };
2402
+ }
2403
+ /** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
2404
+ async listServices(options = {}) {
2405
+ return (await this.searchServices(options)).services;
2406
+ }
2407
+ async getService(application) {
2408
+ const needle = application.trim();
2409
+ if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
2410
+ const res = await this.request({
2411
+ service: "developers",
2412
+ method: "GET",
2413
+ path: "/api/services/catalog",
2414
+ query: { application: needle }
2415
+ });
2416
+ const raw = res.data?.application ?? res.application;
2417
+ const entry = normalizeServiceCatalogEntry(raw);
2418
+ if (!entry) {
2419
+ throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
2420
+ }
2421
+ return entry;
2368
2422
  }
2369
2423
  /**
2370
2424
  * List callable actions from the live registry-canonical catalog (definitions
@@ -2372,16 +2426,15 @@ var TopoloClient = class _TopoloClient {
2372
2426
  * caller's organization and effective permissions; no bundled/static app
2373
2427
  * action list is used as an authorization source.
2374
2428
  */
2375
- async listActions(options = {}) {
2376
- const now = Date.now();
2377
- if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2378
- return this.actionCatalogCache.entries;
2429
+ async searchActions(options) {
2430
+ if (!options.service?.trim()) {
2431
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2379
2432
  }
2380
2433
  const res = await this.request({
2381
2434
  service: "developers",
2382
2435
  method: "GET",
2383
2436
  path: "/api/actions/catalog",
2384
- ...options.service ? { query: { service: options.service } } : {}
2437
+ query: { service: options.service, ...catalogSearchQuery(options) }
2385
2438
  });
2386
2439
  const raw = res.data?.actions ?? res.actions;
2387
2440
  if (!Array.isArray(raw)) {
@@ -2391,36 +2444,96 @@ var TopoloClient = class _TopoloClient {
2391
2444
  );
2392
2445
  }
2393
2446
  const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2394
- if (!options.service) {
2395
- this.actionCatalogCache = {
2396
- entries,
2397
- expiresAt: now + CATALOG_CACHE_TTL_MS
2398
- };
2447
+ return {
2448
+ actions: entries,
2449
+ page: normalizeCatalogPage(
2450
+ res.data?.page ?? res.page
2451
+ )
2452
+ };
2453
+ }
2454
+ async getActionCatalogSummary(service) {
2455
+ const boundedService = service.trim();
2456
+ if (!boundedService) {
2457
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2399
2458
  }
2400
- return entries;
2459
+ const res = await this.request({
2460
+ service: "developers",
2461
+ method: "GET",
2462
+ path: "/api/actions/catalog",
2463
+ query: { service: boundedService, summary: "true" }
2464
+ });
2465
+ const raw = res.data?.summary ?? res.summary;
2466
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2467
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
2468
+ }
2469
+ const record = raw;
2470
+ if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
2471
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
2472
+ }
2473
+ return {
2474
+ algorithm: "sha256-v1",
2475
+ digest: record.digest,
2476
+ actionCount: Number(record.actionCount)
2477
+ };
2478
+ }
2479
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
2480
+ async listActions(options) {
2481
+ if (!options) {
2482
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2483
+ }
2484
+ return (await this.searchActions(options)).actions;
2401
2485
  }
2402
2486
  async getAction(action) {
2403
- const needle = normalizeActionLookupKey(action);
2404
- const actions = await this.listActions();
2405
- const match = actions.find((entry) => actionEntryMatches(entry, needle));
2406
- if (!match) {
2487
+ const actionId = action.trim();
2488
+ if (!actionId) {
2407
2489
  throw new TopoloSdkError(
2408
2490
  "unknown_action",
2409
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2491
+ "action id is required"
2410
2492
  );
2411
2493
  }
2494
+ let res;
2495
+ try {
2496
+ res = await this.request({
2497
+ service: "developers",
2498
+ method: "GET",
2499
+ path: "/api/actions/catalog",
2500
+ query: { actionId }
2501
+ });
2502
+ } catch (error) {
2503
+ if (error instanceof TopoloHttpError && error.status === 404) {
2504
+ throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2505
+ }
2506
+ throw error;
2507
+ }
2508
+ const raw = res.data?.action ?? res.action;
2509
+ const match = normalizeActionCatalogEntry(raw);
2510
+ if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2412
2511
  return match;
2413
2512
  }
2414
2513
  async callAction(action, input = {}, options = {}) {
2415
2514
  const entry = await this.getAction(action);
2515
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2516
+ if (agentAccess === "off") {
2517
+ throw new TopoloPermissionError(
2518
+ `Topolo action "${entry.actionId}" is not available for agent invocation.`,
2519
+ entry.requiredPermission ? [entry.requiredPermission] : []
2520
+ );
2521
+ }
2522
+ if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
2523
+ throw new TopoloConfirmationRequiredError(
2524
+ `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2525
+ );
2526
+ }
2416
2527
  const mapped = mapActionInput(entry, input);
2417
2528
  const service = entry.appSlug || entry.appId;
2418
2529
  return this.request({
2419
2530
  service,
2531
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2420
2532
  method: entry.method,
2421
2533
  path: mapped.path,
2422
2534
  ...mapped.query ? { query: mapped.query } : {},
2423
2535
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2536
+ ...options.resource ? { resource: options.resource } : {},
2424
2537
  headers: {
2425
2538
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2426
2539
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
@@ -2558,6 +2671,18 @@ function serviceKeyCandidates2(slug) {
2558
2671
  function normalizeLookupKey(value) {
2559
2672
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2560
2673
  }
2674
+ function requireHttpOrigin(value) {
2675
+ let url;
2676
+ try {
2677
+ url = new URL(value);
2678
+ } catch {
2679
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2680
+ }
2681
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
2682
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2683
+ }
2684
+ return url.origin;
2685
+ }
2561
2686
  function deriveDevelopersFromAuth(authUrl) {
2562
2687
  if (!authUrl) return null;
2563
2688
  try {
@@ -2584,6 +2709,47 @@ function catalogEntryLookupKeys(entry) {
2584
2709
  }
2585
2710
  return [...keys];
2586
2711
  }
2712
+ function catalogSearchQuery(options) {
2713
+ const query = {};
2714
+ if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
2715
+ if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
2716
+ if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
2717
+ return query;
2718
+ }
2719
+ function normalizeCatalogPage(value) {
2720
+ if (!isRecord(value)) return { nextCursor: null, hasMore: false };
2721
+ return {
2722
+ nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2723
+ hasMore: value.hasMore === true
2724
+ };
2725
+ }
2726
+ function normalizeServiceCatalogEntry(item) {
2727
+ if (!isRecord(item)) return null;
2728
+ const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2729
+ if (!appId) return null;
2730
+ const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
2731
+ (permission) => typeof permission === "string" && permission.trim().length > 0
2732
+ ) : [];
2733
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2734
+ item.api_key_resources ?? item.apiKeyResources
2735
+ );
2736
+ const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2737
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2738
+ return {
2739
+ appId,
2740
+ slug: typeof item.app_slug === "string" ? item.app_slug : null,
2741
+ name: typeof item.name === "string" ? item.name : null,
2742
+ description: typeof item.description === "string" ? item.description : null,
2743
+ apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
2744
+ launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
2745
+ status: typeof item.status === "string" ? item.status : null,
2746
+ surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
2747
+ launchable: item.launchable === true,
2748
+ permissions,
2749
+ apiKeyResource,
2750
+ apiKeyResources
2751
+ };
2752
+ }
2587
2753
  function normalizeActionCatalogEntry(item) {
2588
2754
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2589
2755
  const a = item;
@@ -2597,6 +2763,8 @@ function normalizeActionCatalogEntry(item) {
2597
2763
  }
2598
2764
  return {
2599
2765
  actionId,
2766
+ target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
2767
+ apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
2600
2768
  name,
2601
2769
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2602
2770
  title: pickActionString(a.title) ?? name,
@@ -2608,6 +2776,7 @@ function normalizeActionCatalogEntry(item) {
2608
2776
  path,
2609
2777
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2610
2778
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2779
+ authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2611
2780
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2612
2781
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2613
2782
  readOnly: Boolean(a.read_only ?? a.readOnly),
@@ -2627,16 +2796,12 @@ function pickActionString(value) {
2627
2796
  function pickAgentAccess(value) {
2628
2797
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
2629
2798
  }
2799
+ function pickAuthorizationMode(value) {
2800
+ return value === "runtime" ? "runtime" : "static";
2801
+ }
2630
2802
  function plainObject(value) {
2631
2803
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2632
2804
  }
2633
- function normalizeActionLookupKey(value) {
2634
- return String(value || "").trim().toLowerCase();
2635
- }
2636
- function actionEntryMatches(entry, needle) {
2637
- if (!needle) return false;
2638
- return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
2639
- }
2640
2805
  function mapActionInput(action, input) {
2641
2806
  const remaining = { ...input || {} };
2642
2807
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -2715,6 +2880,40 @@ function mergeSignals(a, b) {
2715
2880
  return controller.signal;
2716
2881
  }
2717
2882
 
2883
+ // src/action-catalog-digest.ts
2884
+ var ACTION_CATALOG_DIGEST_ALGORITHM = "sha256-v1";
2885
+ function canonicalPermission(value, appId) {
2886
+ let permission = String(value || "").trim().toLowerCase().replace(/\s+/g, "");
2887
+ const prefix = `${String(appId || "").trim().toLowerCase()}.`;
2888
+ if (prefix !== "." && permission.startsWith(prefix)) permission = permission.slice(prefix.length);
2889
+ return permission.replace(/\./g, ":");
2890
+ }
2891
+ function stableJson(value) {
2892
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
2893
+ if (value && typeof value === "object") {
2894
+ const record = value;
2895
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
2896
+ }
2897
+ return JSON.stringify(value);
2898
+ }
2899
+ async function digestTopoloActionCatalog(actions, appId) {
2900
+ const canonical = actions.filter((action) => action.agentAccess !== "off").map((action) => ({
2901
+ actionId: action.actionId || `${appId}.${action.name}`,
2902
+ authorizationMode: action.authorizationMode === "runtime" ? "runtime" : "static",
2903
+ inputSchema: action.inputSchema,
2904
+ method: action.method,
2905
+ outputSchema: action.outputSchema,
2906
+ path: action.path,
2907
+ permission: canonicalPermission(action.permission ?? action.permissionName ?? "", appId),
2908
+ readOnly: action.readOnly,
2909
+ requiresConfirmation: action.requiresConfirmation,
2910
+ target: action.target ?? ""
2911
+ })).sort((left, right) => left.actionId.localeCompare(right.actionId));
2912
+ const bytes = new TextEncoder().encode(canonical.map(stableJson).join("\n"));
2913
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
2914
+ return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
2915
+ }
2916
+
2718
2917
  // src/modules/crm.ts
2719
2918
  var CrmModule = class {
2720
2919
  constructor(client) {
@@ -3239,6 +3438,7 @@ function createTopolo(options) {
3239
3438
  };
3240
3439
  }
3241
3440
  export {
3441
+ ACTION_CATALOG_DIGEST_ALGORITHM,
3242
3442
  APPLICATIONS,
3243
3443
  APPLICATION_REQUIREMENTS,
3244
3444
  APPLICATION_REQUIREMENTS_VERSION,
@@ -3250,6 +3450,7 @@ export {
3250
3450
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
3251
3451
  TopoloAuthError,
3252
3452
  TopoloClient,
3453
+ TopoloConfirmationRequiredError,
3253
3454
  TopoloHttpError,
3254
3455
  TopoloOAuth,
3255
3456
  TopoloPermissionError,
@@ -3265,6 +3466,7 @@ export {
3265
3466
  defineTopoloQuickActionsWidget,
3266
3467
  defineTopoloRecentActivityWidget,
3267
3468
  defineTopoloStatsWidget,
3469
+ digestTopoloActionCatalog,
3268
3470
  requirementsForApplication,
3269
3471
  resolveCatalogServiceUrl,
3270
3472
  resolveServiceUrl,
@@ -382,7 +382,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
382
382
  return profileForEnvironment(config, environment).credential ?? null;
383
383
  }
384
384
  var REFRESH_SKEW_SECONDS = 60;
385
- async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
385
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
386
386
  const prefix = envVarPrefix(environment);
387
387
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
388
388
  return resolveCredential(environment);
@@ -393,7 +393,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
393
393
  if (!cred || cred.kind !== "access_token") return cred ?? null;
394
394
  if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
395
395
  const now = Math.floor(Date.now() / 1e3);
396
- if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
396
+ if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
397
397
  try {
398
398
  const oauth = new TopoloOAuth({
399
399
  serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
@@ -408,9 +408,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
408
408
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
409
409
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
410
410
  };
411
- await writeConfig(
412
- environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
413
- );
411
+ const activeContext = profile.activeContext;
412
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
413
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
414
+ credential: refreshed,
415
+ contexts: {
416
+ ...profile.contexts ?? {},
417
+ [activeContext]: {
418
+ ...activeEntry,
419
+ credential: refreshed,
420
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
421
+ }
422
+ }
423
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
424
+ await writeConfig(updated);
414
425
  return refreshed;
415
426
  } catch {
416
427
  return cred;
@@ -100,7 +100,9 @@ declare function credentialSourceFromConfig(config: TopoloConfig, environment?:
100
100
  * interactive users get after running `topolo auth login`.
101
101
  */
102
102
  declare function resolveCredential(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
103
- declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
103
+ declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment, options?: {
104
+ force?: boolean;
105
+ }): Promise<TopoloCredential | null>;
104
106
  declare function resolveAgentName(configAgentName?: string): string | undefined;
105
107
  declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
106
108
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
@@ -100,7 +100,9 @@ declare function credentialSourceFromConfig(config: TopoloConfig, environment?:
100
100
  * interactive users get after running `topolo auth login`.
101
101
  */
102
102
  declare function resolveCredential(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
103
- declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
103
+ declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment, options?: {
104
+ force?: boolean;
105
+ }): Promise<TopoloCredential | null>;
104
106
  declare function resolveAgentName(configAgentName?: string): string | undefined;
105
107
  declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
106
108
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
@@ -339,7 +339,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
339
339
  return profileForEnvironment(config, environment).credential ?? null;
340
340
  }
341
341
  var REFRESH_SKEW_SECONDS = 60;
342
- async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
342
+ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
343
343
  const prefix = envVarPrefix(environment);
344
344
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
345
345
  return resolveCredential(environment);
@@ -350,7 +350,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
350
350
  if (!cred || cred.kind !== "access_token") return cred ?? null;
351
351
  if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
352
352
  const now = Math.floor(Date.now() / 1e3);
353
- if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
353
+ if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
354
354
  try {
355
355
  const oauth = new TopoloOAuth({
356
356
  serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
@@ -365,9 +365,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
365
365
  ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
366
366
  expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
367
367
  };
368
- await writeConfig(
369
- environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
370
- );
368
+ const activeContext = profile.activeContext;
369
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
370
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
371
+ credential: refreshed,
372
+ contexts: {
373
+ ...profile.contexts ?? {},
374
+ [activeContext]: {
375
+ ...activeEntry,
376
+ credential: refreshed,
377
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
378
+ }
379
+ }
380
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
381
+ await writeConfig(updated);
371
382
  return refreshed;
372
383
  } catch {
373
384
  return cred;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
+ "topoloPublishedVersion": "0.8.0",
4
5
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
5
6
  "type": "module",
6
7
  "main": "./dist/index.js",
@@ -19,8 +20,7 @@
19
20
  }
20
21
  },
21
22
  "files": [
22
- "dist",
23
- "src"
23
+ "dist"
24
24
  ],
25
25
  "scripts": {
26
26
  "generate:services": "node scripts/generate-services.mjs",
@@ -31,7 +31,8 @@
31
31
  "typecheck": "node scripts/generate-services.mjs && tsc --noEmit",
32
32
  "test": "vitest run",
33
33
  "test:watch": "vitest",
34
- "prepublishOnly": "npm run build",
34
+ "prepublishOnly": "npm run build && npm run verify:package",
35
+ "verify:package": "node ../../tooling/verify-npm-package.mjs .",
35
36
  "coverage": "vitest run --coverage"
36
37
  },
37
38
  "devDependencies": {