@topolo/sdk 0.6.0 → 0.8.0

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.d.ts CHANGED
@@ -47,6 +47,12 @@ interface TopoloClientOptions {
47
47
  timeoutMs?: number;
48
48
  /** Injected fetch, for testing. Defaults to global fetch. */
49
49
  fetch?: typeof fetch;
50
+ /**
51
+ * Refresh a rejected access-token credential. When supplied, the client
52
+ * retries one 401 response with the returned credential. Concurrent 401s
53
+ * share one refresh operation. API keys are never refreshed.
54
+ */
55
+ refreshCredential?: () => Promise<TopoloCredential | null>;
50
56
  /**
51
57
  * Observability hook. Fires once per request lifecycle (`request` at start,
52
58
  * then exactly one of `response` or `error`). Safe to leave unset — off by
@@ -84,6 +90,8 @@ type TopoloDebugEvent = {
84
90
  };
85
91
  interface RequestOptions {
86
92
  service: string;
93
+ /** Catalog-resolved origin for an action target. */
94
+ baseUrl?: string;
87
95
  path: string;
88
96
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
89
97
  query?: Record<string, string | number | boolean | undefined>;
@@ -129,6 +137,8 @@ interface TopoloApiKeyResourceMetadata {
129
137
  }
130
138
  interface TopoloActionCatalogEntry {
131
139
  actionId: string;
140
+ target: string | null;
141
+ apiBaseUrl: string | null;
132
142
  name: string;
133
143
  toolName: string | null;
134
144
  title: string;
@@ -140,6 +150,8 @@ interface TopoloActionCatalogEntry {
140
150
  path: string;
141
151
  permissionName: string;
142
152
  requiredPermission: string;
153
+ /** Static catalog permission or target-enforced input-dependent authorization. */
154
+ authorizationMode?: 'static' | 'runtime';
143
155
  inputSchema: Record<string, unknown>;
144
156
  outputSchema: Record<string, unknown>;
145
157
  readOnly: boolean;
@@ -150,17 +162,34 @@ interface TopoloActionCatalogEntry {
150
162
  status: string | null;
151
163
  sourceRevision: string | null;
152
164
  }
165
+ interface TopoloCatalogPage {
166
+ nextCursor: string | null;
167
+ hasMore: boolean;
168
+ }
169
+ interface TopoloServiceCatalogPage {
170
+ services: TopoloServiceCatalogEntry[];
171
+ page: TopoloCatalogPage;
172
+ }
173
+ interface TopoloActionCatalogPage {
174
+ actions: TopoloActionCatalogEntry[];
175
+ page: TopoloCatalogPage;
176
+ }
177
+ interface TopoloCatalogSearchOptions {
178
+ query?: string;
179
+ cursor?: string;
180
+ limit?: number;
181
+ }
153
182
  declare class TopoloClient {
154
- private readonly credential;
183
+ private credential;
155
184
  private readonly agent;
156
185
  private readonly serviceUrls?;
157
186
  private readonly appIds?;
158
187
  private readonly requireConfirmForWrites;
159
188
  private readonly timeoutMs;
160
189
  private readonly fetchImpl;
190
+ private readonly refreshCredential?;
191
+ private refreshInFlight;
161
192
  private readonly debug?;
162
- private serviceCatalogCache;
163
- private actionCatalogCache;
164
193
  /**
165
194
  * Process-wide cache for slug → app-id lookups. Keyed by
166
195
  * `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
@@ -181,6 +210,7 @@ declare class TopoloClient {
181
210
  * use and for the generic `topolo api` / MCP passthrough tool.
182
211
  */
183
212
  request<T = unknown>(opts: RequestOptions): Promise<T>;
213
+ private refreshRejectedAccessToken;
184
214
  private resolveRequestService;
185
215
  private resolveLiveCallableService;
186
216
  /**
@@ -206,19 +236,30 @@ declare class TopoloClient {
206
236
  * The live Auth catalog is the authority because it is scoped to the caller's
207
237
  * organization and effective permissions.
208
238
  */
209
- listServices(): Promise<TopoloServiceCatalogEntry[]>;
239
+ searchServices(options?: TopoloCatalogSearchOptions): Promise<TopoloServiceCatalogPage>;
240
+ /** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
241
+ listServices(options?: TopoloCatalogSearchOptions): Promise<TopoloServiceCatalogEntry[]>;
242
+ getService(application: string): Promise<TopoloServiceCatalogEntry>;
210
243
  /**
211
244
  * List callable actions from the live registry-canonical catalog (definitions
212
245
  * from the Catalog registry, scoping from Auth). The response is scoped to the
213
246
  * caller's organization and effective permissions; no bundled/static app
214
247
  * action list is used as an authorization source.
215
248
  */
216
- listActions(options?: {
217
- service?: string;
249
+ searchActions(options: TopoloCatalogSearchOptions & {
250
+ service: string;
251
+ }): Promise<TopoloActionCatalogPage>;
252
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
253
+ listActions(options?: TopoloCatalogSearchOptions & {
254
+ service: string;
218
255
  }): Promise<TopoloActionCatalogEntry[]>;
219
256
  getAction(action: string): Promise<TopoloActionCatalogEntry>;
220
257
  callAction<T = unknown>(action: string, input?: Record<string, unknown>, options?: {
221
258
  confirm?: boolean;
259
+ resource?: {
260
+ resourceType: string;
261
+ resourceId: string;
262
+ };
222
263
  }): Promise<T>;
223
264
  /**
224
265
  * Bundled generated registry for tooling that audits platform packaging, not
@@ -1052,7 +1093,7 @@ declare const APPLICATIONS: {
1052
1093
  readonly kind: "worker";
1053
1094
  readonly apiUrl: "https://topolo.io";
1054
1095
  readonly domains: readonly ["topolo.io"];
1055
- readonly routes: readonly ["topolo.io", "www.topolo.io"];
1096
+ readonly routes: readonly ["one.topolo.io", "topolo.io", "www.topolo.io"];
1056
1097
  readonly appId: null;
1057
1098
  }];
1058
1099
  };
@@ -1601,6 +1642,9 @@ declare class TopoloPermissionError extends TopoloSdkError {
1601
1642
  readonly required: string[];
1602
1643
  constructor(message: string, required: string[]);
1603
1644
  }
1645
+ declare class TopoloConfirmationRequiredError extends TopoloSdkError {
1646
+ constructor(message: string);
1647
+ }
1604
1648
  declare class TopoloHttpError extends TopoloSdkError {
1605
1649
  readonly status: number;
1606
1650
  readonly body: unknown;
@@ -2092,4 +2136,4 @@ declare function createTopolo(options: TopoloClientOptions): {
2092
2136
  };
2093
2137
  type Topolo = ReturnType<typeof createTopolo>;
2094
2138
 
2095
- export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type AppApiId, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_APP_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, type TopoloApiKeyResourceMetadata, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
2139
+ export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type AppApiId, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_APP_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, type TopoloApiKeyResourceMetadata, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, TopoloConfirmationRequiredError, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
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,63 @@ 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
+ /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
2455
+ async listActions(options) {
2456
+ if (!options) {
2457
+ throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
2399
2458
  }
2400
- return entries;
2459
+ return (await this.searchActions(options)).actions;
2401
2460
  }
2402
2461
  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) {
2462
+ const actionId = action.trim();
2463
+ if (!actionId) {
2407
2464
  throw new TopoloSdkError(
2408
2465
  "unknown_action",
2409
- `Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
2466
+ "action id is required"
2410
2467
  );
2411
2468
  }
2469
+ const res = await this.request({
2470
+ service: "developers",
2471
+ method: "GET",
2472
+ path: "/api/actions/catalog",
2473
+ query: { actionId }
2474
+ });
2475
+ const raw = res.data?.action ?? res.action;
2476
+ const match = normalizeActionCatalogEntry(raw);
2477
+ if (!match) throw new TopoloSdkError("unknown_action", `Unknown Topolo action "${action}".`);
2412
2478
  return match;
2413
2479
  }
2414
2480
  async callAction(action, input = {}, options = {}) {
2415
2481
  const entry = await this.getAction(action);
2482
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2483
+ if (agentAccess === "off") {
2484
+ throw new TopoloPermissionError(
2485
+ `Topolo action "${entry.actionId}" is not available for agent invocation.`,
2486
+ entry.requiredPermission ? [entry.requiredPermission] : []
2487
+ );
2488
+ }
2489
+ if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
2490
+ throw new TopoloConfirmationRequiredError(
2491
+ `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2492
+ );
2493
+ }
2416
2494
  const mapped = mapActionInput(entry, input);
2417
2495
  const service = entry.appSlug || entry.appId;
2418
2496
  return this.request({
2419
2497
  service,
2498
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2420
2499
  method: entry.method,
2421
2500
  path: mapped.path,
2422
2501
  ...mapped.query ? { query: mapped.query } : {},
2423
2502
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2503
+ ...options.resource ? { resource: options.resource } : {},
2424
2504
  headers: {
2425
2505
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2426
2506
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
@@ -2558,6 +2638,18 @@ function serviceKeyCandidates2(slug) {
2558
2638
  function normalizeLookupKey(value) {
2559
2639
  return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
2560
2640
  }
2641
+ function requireHttpOrigin(value) {
2642
+ let url;
2643
+ try {
2644
+ url = new URL(value);
2645
+ } catch {
2646
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2647
+ }
2648
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
2649
+ throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
2650
+ }
2651
+ return url.origin;
2652
+ }
2561
2653
  function deriveDevelopersFromAuth(authUrl) {
2562
2654
  if (!authUrl) return null;
2563
2655
  try {
@@ -2584,6 +2676,47 @@ function catalogEntryLookupKeys(entry) {
2584
2676
  }
2585
2677
  return [...keys];
2586
2678
  }
2679
+ function catalogSearchQuery(options) {
2680
+ const query = {};
2681
+ if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
2682
+ if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
2683
+ if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
2684
+ return query;
2685
+ }
2686
+ function normalizeCatalogPage(value) {
2687
+ if (!isRecord(value)) return { nextCursor: null, hasMore: false };
2688
+ return {
2689
+ nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2690
+ hasMore: value.hasMore === true
2691
+ };
2692
+ }
2693
+ function normalizeServiceCatalogEntry(item) {
2694
+ if (!isRecord(item)) return null;
2695
+ const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2696
+ if (!appId) return null;
2697
+ const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
2698
+ (permission) => typeof permission === "string" && permission.trim().length > 0
2699
+ ) : [];
2700
+ const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
2701
+ item.api_key_resources ?? item.apiKeyResources
2702
+ );
2703
+ const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
2704
+ const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
2705
+ return {
2706
+ appId,
2707
+ slug: typeof item.app_slug === "string" ? item.app_slug : null,
2708
+ name: typeof item.name === "string" ? item.name : null,
2709
+ description: typeof item.description === "string" ? item.description : null,
2710
+ apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
2711
+ launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
2712
+ status: typeof item.status === "string" ? item.status : null,
2713
+ surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
2714
+ launchable: item.launchable === true,
2715
+ permissions,
2716
+ apiKeyResource,
2717
+ apiKeyResources
2718
+ };
2719
+ }
2587
2720
  function normalizeActionCatalogEntry(item) {
2588
2721
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2589
2722
  const a = item;
@@ -2597,6 +2730,8 @@ function normalizeActionCatalogEntry(item) {
2597
2730
  }
2598
2731
  return {
2599
2732
  actionId,
2733
+ target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
2734
+ apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
2600
2735
  name,
2601
2736
  toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
2602
2737
  title: pickActionString(a.title) ?? name,
@@ -2608,6 +2743,7 @@ function normalizeActionCatalogEntry(item) {
2608
2743
  path,
2609
2744
  permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
2610
2745
  requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
2746
+ authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2611
2747
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2612
2748
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
2613
2749
  readOnly: Boolean(a.read_only ?? a.readOnly),
@@ -2627,16 +2763,12 @@ function pickActionString(value) {
2627
2763
  function pickAgentAccess(value) {
2628
2764
  return value === "auto" || value === "confirm" || value === "off" ? value : null;
2629
2765
  }
2766
+ function pickAuthorizationMode(value) {
2767
+ return value === "runtime" ? "runtime" : "static";
2768
+ }
2630
2769
  function plainObject(value) {
2631
2770
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2632
2771
  }
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
2772
  function mapActionInput(action, input) {
2641
2773
  const remaining = { ...input || {} };
2642
2774
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -3250,6 +3382,7 @@ export {
3250
3382
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
3251
3383
  TopoloAuthError,
3252
3384
  TopoloClient,
3385
+ TopoloConfirmationRequiredError,
3253
3386
  TopoloHttpError,
3254
3387
  TopoloOAuth,
3255
3388
  TopoloPermissionError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
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",
@@ -31,7 +31,6 @@
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",
35
34
  "coverage": "vitest run --coverage"
36
35
  },
37
36
  "devDependencies": {
@@ -1001,6 +1001,7 @@ export const APPLICATIONS = {
1001
1001
  "topolo.io"
1002
1002
  ],
1003
1003
  "routes": [
1004
+ "one.topolo.io",
1004
1005
  "topolo.io",
1005
1006
  "www.topolo.io"
1006
1007
  ],