@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/README.md +11 -3
- package/dist/index.cjs +187 -53
- package/dist/index.d.cts +52 -8
- package/dist/index.d.ts +52 -8
- package/dist/index.js +186 -53
- package/package.json +1 -2
- package/src/applications.generated.ts +1 -0
- package/src/client.test.ts +294 -31
- package/src/client.ts +280 -59
- package/src/errors.ts +7 -0
- package/src/index.ts +1 -0
- package/src/modules/crm.test.ts +1 -3
- package/src/modules/p2p.test.ts +1 -3
- package/src/node/credential-store.test.ts +337 -0
- package/src/node/credential-store.ts +86 -5
- package/dist/node/credential-store.cjs +0 -440
- package/dist/node/credential-store.d.cts +0 -81
- package/dist/node/credential-store.d.ts +0 -81
- package/dist/node/credential-store.js +0 -399
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
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
service
|
|
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
|
|
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
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
|
2249
|
-
|
|
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
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
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
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
};
|
|
2367
|
-
|
|
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
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
|
2459
|
+
return (await this.searchActions(options)).actions;
|
|
2401
2460
|
}
|
|
2402
2461
|
async getAction(action) {
|
|
2403
|
-
const
|
|
2404
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|