@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/README.md CHANGED
@@ -55,6 +55,11 @@ accepts an `orgId` input. Every request derives the org from the credential —
55
55
  a JWT `orgId` claim for access tokens, or the `organization_id` bound to an
56
56
  API key at issuance.
57
57
 
58
+ OAuth context is credential-bound: refresh rotation preserves the token's
59
+ personal/org selection and never re-resolves it from the user's mutable web
60
+ preference. Separate CLI/MCP credentials can therefore remain in different
61
+ contexts concurrently.
62
+
58
63
  This is enforced at every layer:
59
64
 
60
65
  1. `@topolo/sdk` — no typed method takes an `orgId` arg.
@@ -114,9 +119,12 @@ generated snapshot. Do not use it to decide what an end user or agent may call.
114
119
  `TopoloClient.listActions()` reads TopoloAuth's live action catalog for the
115
120
  current credential. Applications publish their action definitions through
116
121
  Topolo Developers, and Auth returns only actions whose existing service
117
- permission is granted to the credential. The action entry includes the service,
118
- HTTP method/path, required permission, input/output schemas, read-only and
119
- destructive hints, and the stable MCP/agent tool name.
122
+ permission is granted to the credential. Actions with input-dependent policy
123
+ use `authorizationMode: runtime`; they remain typed and discoverable while the
124
+ target service evaluates the supplied organization, application, and resource
125
+ against current RBAC on every call. The action entry includes the service, HTTP
126
+ method/path, authorization mode, required permission when static, input/output
127
+ schemas, read-only and destructive hints, and the stable MCP/agent tool name.
120
128
 
121
129
  `TopoloClient.callAction(actionRef, input, options)` resolves an action by
122
130
  `actionId`, `name`, or `toolName`, maps path parameters and query/body values
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ACTION_CATALOG_DIGEST_ALGORITHM: () => ACTION_CATALOG_DIGEST_ALGORITHM,
23
24
  APPLICATIONS: () => APPLICATIONS,
24
25
  APPLICATION_REQUIREMENTS: () => APPLICATION_REQUIREMENTS,
25
26
  APPLICATION_REQUIREMENTS_VERSION: () => APPLICATION_REQUIREMENTS_VERSION,
@@ -31,6 +32,7 @@ __export(index_exports, {
31
32
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
32
33
  TopoloAuthError: () => TopoloAuthError,
33
34
  TopoloClient: () => TopoloClient,
35
+ TopoloConfirmationRequiredError: () => TopoloConfirmationRequiredError,
34
36
  TopoloHttpError: () => TopoloHttpError,
35
37
  TopoloOAuth: () => TopoloOAuth,
36
38
  TopoloPermissionError: () => TopoloPermissionError,
@@ -46,6 +48,7 @@ __export(index_exports, {
46
48
  defineTopoloQuickActionsWidget: () => defineTopoloQuickActionsWidget,
47
49
  defineTopoloRecentActivityWidget: () => defineTopoloRecentActivityWidget,
48
50
  defineTopoloStatsWidget: () => defineTopoloStatsWidget,
51
+ digestTopoloActionCatalog: () => digestTopoloActionCatalog,
49
52
  requirementsForApplication: () => requirementsForApplication,
50
53
  resolveCatalogServiceUrl: () => resolveCatalogServiceUrl,
51
54
  resolveServiceUrl: () => resolveServiceUrl,
@@ -96,6 +99,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
96
99
  this.required = required;
97
100
  }
98
101
  };
102
+ var TopoloConfirmationRequiredError = class extends TopoloSdkError {
103
+ constructor(message) {
104
+ super("confirmation_required", message);
105
+ this.name = "TopoloConfirmationRequiredError";
106
+ }
107
+ };
99
108
  var TopoloHttpError = class extends TopoloSdkError {
100
109
  status;
101
110
  body;
@@ -1162,6 +1171,7 @@ var APPLICATIONS = {
1162
1171
  "topolo.io"
1163
1172
  ],
1164
1173
  "routes": [
1174
+ "one.topolo.io",
1165
1175
  "topolo.io",
1166
1176
  "www.topolo.io"
1167
1177
  ],
@@ -2121,7 +2131,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
2121
2131
  var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
2122
2132
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2123
2133
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2124
- var CATALOG_CACHE_TTL_MS = 60 * 1e3;
2125
2134
  var TopoloClient = class _TopoloClient {
2126
2135
  credential;
2127
2136
  agent;
@@ -2130,9 +2139,9 @@ var TopoloClient = class _TopoloClient {
2130
2139
  requireConfirmForWrites;
2131
2140
  timeoutMs;
2132
2141
  fetchImpl;
2142
+ refreshCredential;
2143
+ refreshInFlight = null;
2133
2144
  debug;
2134
- serviceCatalogCache = null;
2135
- actionCatalogCache = null;
2136
2145
  /**
2137
2146
  * Process-wide cache for slug → app-id lookups. Keyed by
2138
2147
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -2157,6 +2166,7 @@ var TopoloClient = class _TopoloClient {
2157
2166
  this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
2158
2167
  this.timeoutMs = options.timeoutMs ?? 3e4;
2159
2168
  this.fetchImpl = options.fetch ?? fetch;
2169
+ this.refreshCredential = options.refreshCredential;
2160
2170
  this.debug = options.debug;
2161
2171
  }
2162
2172
  emit(event) {
@@ -2174,11 +2184,11 @@ var TopoloClient = class _TopoloClient {
2174
2184
  async request(opts) {
2175
2185
  const method = opts.method ?? "GET";
2176
2186
  if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
2177
- throw new TopoloAuthError(
2187
+ throw new TopoloConfirmationRequiredError(
2178
2188
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
2179
2189
  );
2180
2190
  }
2181
- const resolvedService = await this.resolveRequestService(opts.service);
2191
+ const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
2182
2192
  const baseUrl = resolvedService.baseUrl;
2183
2193
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
2184
2194
  if (opts.query) {
@@ -2209,20 +2219,36 @@ var TopoloClient = class _TopoloClient {
2209
2219
  headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
2210
2220
  headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
2211
2221
  }
2212
- const controller = new AbortController();
2213
- const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2214
- const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2215
2222
  const urlStr = url.toString();
2216
2223
  const startedAt = Date.now();
2217
2224
  this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
2225
+ const fetchOnce = async () => {
2226
+ const controller = new AbortController();
2227
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
2228
+ const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
2229
+ try {
2230
+ return await this.fetchImpl(urlStr, {
2231
+ method,
2232
+ headers: new Headers(headers),
2233
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2234
+ signal
2235
+ });
2236
+ } finally {
2237
+ clearTimeout(timeoutId);
2238
+ }
2239
+ };
2218
2240
  let res;
2219
2241
  try {
2220
- res = await this.fetchImpl(urlStr, {
2221
- method,
2222
- headers,
2223
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2224
- signal
2225
- });
2242
+ const attemptedCredential = this.credential;
2243
+ res = await fetchOnce();
2244
+ if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
2245
+ await res.body?.cancel().catch(() => void 0);
2246
+ const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
2247
+ if (refreshed) {
2248
+ applyAuthHeaders(headers, refreshed);
2249
+ res = await fetchOnce();
2250
+ }
2251
+ }
2226
2252
  } catch (err) {
2227
2253
  this.emit({
2228
2254
  phase: "error",
@@ -2235,8 +2261,6 @@ var TopoloClient = class _TopoloClient {
2235
2261
  error: err instanceof Error ? err.message : String(err)
2236
2262
  });
2237
2263
  throw err;
2238
- } finally {
2239
- clearTimeout(timeoutId);
2240
2264
  }
2241
2265
  const contentType = res.headers.get("Content-Type") ?? "";
2242
2266
  const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
@@ -2275,6 +2299,19 @@ var TopoloClient = class _TopoloClient {
2275
2299
  });
2276
2300
  return parsed;
2277
2301
  }
2302
+ async refreshRejectedAccessToken(attemptedAccessToken) {
2303
+ if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
2304
+ return this.credential;
2305
+ }
2306
+ if (!this.refreshCredential) return null;
2307
+ this.refreshInFlight ??= this.refreshCredential().then((credential) => {
2308
+ if (credential) this.credential = credential;
2309
+ return credential;
2310
+ }).catch(() => null).finally(() => {
2311
+ this.refreshInFlight = null;
2312
+ });
2313
+ return this.refreshInFlight;
2314
+ }
2278
2315
  async resolveRequestService(service) {
2279
2316
  const serviceKey = normalizeLookupKey(service);
2280
2317
  if (!serviceKey) {
@@ -2300,9 +2337,8 @@ var TopoloClient = class _TopoloClient {
2300
2337
  );
2301
2338
  }
2302
2339
  async resolveLiveCallableService(serviceKey) {
2303
- const catalog = await this.listServices();
2304
- const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2305
- if (!match) return null;
2340
+ const match = await this.getService(serviceKey).catch(() => null);
2341
+ if (!match || !catalogEntryMatches(match, serviceKey)) return null;
2306
2342
  const baseUrl = resolveCatalogServiceUrl(match, {
2307
2343
  serviceKey,
2308
2344
  overrides: this.serviceUrls
@@ -2376,12 +2412,13 @@ var TopoloClient = class _TopoloClient {
2376
2412
  * The live Auth catalog is the authority because it is scoped to the caller's
2377
2413
  * organization and effective permissions.
2378
2414
  */
2379
- async listServices() {
2380
- const now = Date.now();
2381
- if (this.serviceCatalogCache && this.serviceCatalogCache.expiresAt > now) {
2382
- return this.serviceCatalogCache.entries;
2383
- }
2384
- const res = await this.request({ service: "developers", method: "GET", path: "/api/services/catalog" });
2415
+ async searchServices(options = {}) {
2416
+ const res = await this.request({
2417
+ service: "developers",
2418
+ method: "GET",
2419
+ path: "/api/services/catalog",
2420
+ query: catalogSearchQuery(options)
2421
+ });
2385
2422
  const raw = res.data?.services ?? res.services;
2386
2423
  if (!Array.isArray(raw)) {
2387
2424
  throw new TopoloSdkError(
@@ -2391,6 +2428,7 @@ var TopoloClient = class _TopoloClient {
2391
2428
  }
2392
2429
  const entries = [];
2393
2430
  for (const item of raw) {
2431
+ if (!isRecord(item)) continue;
2394
2432
  const s = item;
2395
2433
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2396
2434
  if (!appId) continue;
@@ -2415,11 +2453,30 @@ var TopoloClient = class _TopoloClient {
2415
2453
  apiKeyResources
2416
2454
  });
2417
2455
  }
2418
- this.serviceCatalogCache = {
2419
- entries,
2420
- expiresAt: now + CATALOG_CACHE_TTL_MS
2421
- };
2422
- return entries;
2456
+ const page = normalizeCatalogPage(
2457
+ res.data?.page ?? res.page
2458
+ );
2459
+ return { services: entries, page };
2460
+ }
2461
+ /** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
2462
+ async listServices(options = {}) {
2463
+ return (await this.searchServices(options)).services;
2464
+ }
2465
+ async getService(application) {
2466
+ const needle = application.trim();
2467
+ if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
2468
+ const res = await this.request({
2469
+ service: "developers",
2470
+ method: "GET",
2471
+ path: "/api/services/catalog",
2472
+ query: { application: needle }
2473
+ });
2474
+ const raw = res.data?.application ?? res.application;
2475
+ const entry = normalizeServiceCatalogEntry(raw);
2476
+ if (!entry) {
2477
+ throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
2478
+ }
2479
+ return entry;
2423
2480
  }
2424
2481
  /**
2425
2482
  * List callable actions from the live registry-canonical catalog (definitions
@@ -2427,16 +2484,15 @@ var TopoloClient = class _TopoloClient {
2427
2484
  * caller's organization and effective permissions; no bundled/static app
2428
2485
  * action list is used as an authorization source.
2429
2486
  */
2430
- async listActions(options = {}) {
2431
- const now = Date.now();
2432
- if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
2433
- return this.actionCatalogCache.entries;
2487
+ async searchActions(options) {
2488
+ if (!options.service?.trim()) {
2489
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2434
2490
  }
2435
2491
  const res = await this.request({
2436
2492
  service: "developers",
2437
2493
  method: "GET",
2438
2494
  path: "/api/actions/catalog",
2439
- ...options.service ? { query: { service: options.service } } : {}
2495
+ query: { service: options.service, ...catalogSearchQuery(options) }
2440
2496
  });
2441
2497
  const raw = res.data?.actions ?? res.actions;
2442
2498
  if (!Array.isArray(raw)) {
@@ -2446,36 +2502,96 @@ var TopoloClient = class _TopoloClient {
2446
2502
  );
2447
2503
  }
2448
2504
  const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
2449
- if (!options.service) {
2450
- this.actionCatalogCache = {
2451
- entries,
2452
- expiresAt: now + CATALOG_CACHE_TTL_MS
2453
- };
2505
+ return {
2506
+ actions: entries,
2507
+ page: normalizeCatalogPage(
2508
+ res.data?.page ?? res.page
2509
+ )
2510
+ };
2511
+ }
2512
+ async getActionCatalogSummary(service) {
2513
+ const boundedService = service.trim();
2514
+ if (!boundedService) {
2515
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2454
2516
  }
2455
- return entries;
2517
+ const res = await this.request({
2518
+ service: "developers",
2519
+ method: "GET",
2520
+ path: "/api/actions/catalog",
2521
+ query: { service: boundedService, summary: "true" }
2522
+ });
2523
+ const raw = res.data?.summary ?? res.summary;
2524
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2525
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
2526
+ }
2527
+ const record = raw;
2528
+ if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
2529
+ throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
2530
+ }
2531
+ return {
2532
+ algorithm: "sha256-v1",
2533
+ digest: record.digest,
2534
+ actionCount: Number(record.actionCount)
2535
+ };
2536
+ }
2537
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
2538
+ async listActions(options) {
2539
+ if (!options) {
2540
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2541
+ }
2542
+ return (await this.searchActions(options)).actions;
2456
2543
  }
2457
2544
  async getAction(action) {
2458
- const needle = normalizeActionLookupKey(action);
2459
- const actions = await this.listActions();
2460
- const match = actions.find((entry) => actionEntryMatches(entry, needle));
2461
- if (!match) {
2545
+ const actionId = action.trim();
2546
+ if (!actionId) {
2462
2547
  throw new TopoloSdkError(
2463
2548
  "unknown_action",
2464
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2549
+ "action id is required"
2465
2550
  );
2466
2551
  }
2552
+ let res;
2553
+ try {
2554
+ res = await this.request({
2555
+ service: "developers",
2556
+ method: "GET",
2557
+ path: "/api/actions/catalog",
2558
+ query: { actionId }
2559
+ });
2560
+ } catch (error) {
2561
+ if (error instanceof TopoloHttpError && error.status === 404) {
2562
+ throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2563
+ }
2564
+ throw error;
2565
+ }
2566
+ const raw = res.data?.action ?? res.action;
2567
+ const match = normalizeActionCatalogEntry(raw);
2568
+ if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
2467
2569
  return match;
2468
2570
  }
2469
2571
  async callAction(action, input = {}, options = {}) {
2470
2572
  const entry = await this.getAction(action);
2573
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2574
+ if (agentAccess === "off") {
2575
+ throw new TopoloPermissionError(
2576
+ `Topolo action "${entry.actionId}" is not available for agent invocation.`,
2577
+ entry.requiredPermission ? [entry.requiredPermission] : []
2578
+ );
2579
+ }
2580
+ if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
2581
+ throw new TopoloConfirmationRequiredError(
2582
+ `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2583
+ );
2584
+ }
2471
2585
  const mapped = mapActionInput(entry, input);
2472
2586
  const service = entry.appSlug || entry.appId;
2473
2587
  return this.request({
2474
2588
  service,
2589
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2475
2590
  method: entry.method,
2476
2591
  path: mapped.path,
2477
2592
  ...mapped.query ? { query: mapped.query } : {},
2478
2593
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2594
+ ...options.resource ? { resource: options.resource } : {},
2479
2595
  headers: {
2480
2596
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2481
2597
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
@@ -2613,6 +2729,18 @@ function serviceKeyCandidates2(slug) {
2613
2729
  function normalizeLookupKey(value) {
2614
2730
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2615
2731
  }
2732
+ function requireHttpOrigin(value) {
2733
+ let url;
2734
+ try {
2735
+ url = new URL(value);
2736
+ } catch {
2737
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2738
+ }
2739
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
2740
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2741
+ }
2742
+ return url.origin;
2743
+ }
2616
2744
  function deriveDevelopersFromAuth(authUrl) {
2617
2745
  if (!authUrl) return null;
2618
2746
  try {
@@ -2639,6 +2767,47 @@ function catalogEntryLookupKeys(entry) {
2639
2767
  }
2640
2768
  return [...keys];
2641
2769
  }
2770
+ function catalogSearchQuery(options) {
2771
+ const query = {};
2772
+ if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
2773
+ if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
2774
+ if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
2775
+ return query;
2776
+ }
2777
+ function normalizeCatalogPage(value) {
2778
+ if (!isRecord(value)) return { nextCursor: null, hasMore: false };
2779
+ return {
2780
+ nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2781
+ hasMore: value.hasMore === true
2782
+ };
2783
+ }
2784
+ function normalizeServiceCatalogEntry(item) {
2785
+ if (!isRecord(item)) return null;
2786
+ const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2787
+ if (!appId) return null;
2788
+ const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
2789
+ (permission) => typeof permission === "string" && permission.trim().length > 0
2790
+ ) : [];
2791
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2792
+ item.api_key_resources ?? item.apiKeyResources
2793
+ );
2794
+ const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2795
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2796
+ return {
2797
+ appId,
2798
+ slug: typeof item.app_slug === "string" ? item.app_slug : null,
2799
+ name: typeof item.name === "string" ? item.name : null,
2800
+ description: typeof item.description === "string" ? item.description : null,
2801
+ apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
2802
+ launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
2803
+ status: typeof item.status === "string" ? item.status : null,
2804
+ surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
2805
+ launchable: item.launchable === true,
2806
+ permissions,
2807
+ apiKeyResource,
2808
+ apiKeyResources
2809
+ };
2810
+ }
2642
2811
  function normalizeActionCatalogEntry(item) {
2643
2812
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2644
2813
  const a = item;
@@ -2652,6 +2821,8 @@ function normalizeActionCatalogEntry(item) {
2652
2821
  }
2653
2822
  return {
2654
2823
  actionId,
2824
+ target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
2825
+ apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
2655
2826
  name,
2656
2827
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2657
2828
  title: pickActionString(a.title) ?? name,
@@ -2663,6 +2834,7 @@ function normalizeActionCatalogEntry(item) {
2663
2834
  path,
2664
2835
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2665
2836
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2837
+ authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2666
2838
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2667
2839
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2668
2840
  readOnly: Boolean(a.read_only ?? a.readOnly),
@@ -2682,16 +2854,12 @@ function pickActionString(value) {
2682
2854
  function pickAgentAccess(value) {
2683
2855
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
2684
2856
  }
2857
+ function pickAuthorizationMode(value) {
2858
+ return value === "runtime" ? "runtime" : "static";
2859
+ }
2685
2860
  function plainObject(value) {
2686
2861
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2687
2862
  }
2688
- function normalizeActionLookupKey(value) {
2689
- return String(value || "").trim().toLowerCase();
2690
- }
2691
- function actionEntryMatches(entry, needle) {
2692
- if (!needle) return false;
2693
- return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
2694
- }
2695
2863
  function mapActionInput(action, input) {
2696
2864
  const remaining = { ...input || {} };
2697
2865
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -2770,6 +2938,40 @@ function mergeSignals(a, b) {
2770
2938
  return controller.signal;
2771
2939
  }
2772
2940
 
2941
+ // src/action-catalog-digest.ts
2942
+ var ACTION_CATALOG_DIGEST_ALGORITHM = "sha256-v1";
2943
+ function canonicalPermission(value, appId) {
2944
+ let permission = String(value || "").trim().toLowerCase().replace(/\s+/g, "");
2945
+ const prefix = `${String(appId || "").trim().toLowerCase()}.`;
2946
+ if (prefix !== "." && permission.startsWith(prefix)) permission = permission.slice(prefix.length);
2947
+ return permission.replace(/\./g, ":");
2948
+ }
2949
+ function stableJson(value) {
2950
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
2951
+ if (value && typeof value === "object") {
2952
+ const record = value;
2953
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
2954
+ }
2955
+ return JSON.stringify(value);
2956
+ }
2957
+ async function digestTopoloActionCatalog(actions, appId) {
2958
+ const canonical = actions.filter((action) => action.agentAccess !== "off").map((action) => ({
2959
+ actionId: action.actionId || `${appId}.${action.name}`,
2960
+ authorizationMode: action.authorizationMode === "runtime" ? "runtime" : "static",
2961
+ inputSchema: action.inputSchema,
2962
+ method: action.method,
2963
+ outputSchema: action.outputSchema,
2964
+ path: action.path,
2965
+ permission: canonicalPermission(action.permission ?? action.permissionName ?? "", appId),
2966
+ readOnly: action.readOnly,
2967
+ requiresConfirmation: action.requiresConfirmation,
2968
+ target: action.target ?? ""
2969
+ })).sort((left, right) => left.actionId.localeCompare(right.actionId));
2970
+ const bytes = new TextEncoder().encode(canonical.map(stableJson).join("\n"));
2971
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
2972
+ return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
2973
+ }
2974
+
2773
2975
  // src/modules/crm.ts
2774
2976
  var CrmModule = class {
2775
2977
  constructor(client) {
@@ -3295,6 +3497,7 @@ function createTopolo(options) {
3295
3497
  }
3296
3498
  // Annotate the CommonJS export names for ESM import in node:
3297
3499
  0 && (module.exports = {
3500
+ ACTION_CATALOG_DIGEST_ALGORITHM,
3298
3501
  APPLICATIONS,
3299
3502
  APPLICATION_REQUIREMENTS,
3300
3503
  APPLICATION_REQUIREMENTS_VERSION,
@@ -3306,6 +3509,7 @@ function createTopolo(options) {
3306
3509
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
3307
3510
  TopoloAuthError,
3308
3511
  TopoloClient,
3512
+ TopoloConfirmationRequiredError,
3309
3513
  TopoloHttpError,
3310
3514
  TopoloOAuth,
3311
3515
  TopoloPermissionError,
@@ -3321,6 +3525,7 @@ function createTopolo(options) {
3321
3525
  defineTopoloQuickActionsWidget,
3322
3526
  defineTopoloRecentActivityWidget,
3323
3527
  defineTopoloStatsWidget,
3528
+ digestTopoloActionCatalog,
3324
3529
  requirementsForApplication,
3325
3530
  resolveCatalogServiceUrl,
3326
3531
  resolveServiceUrl,