@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 +11 -3
- package/dist/index.cjs +258 -53
- package/dist/index.d.cts +76 -8
- package/dist/index.d.ts +76 -8
- package/dist/index.js +255 -53
- package/dist/node/credential-store.cjs +16 -5
- package/dist/node/credential-store.d.cts +3 -1
- package/dist/node/credential-store.d.ts +3 -1
- package/dist/node/credential-store.js +16 -5
- package/package.json +5 -4
- package/src/application-requirements.ts +0 -517
- package/src/applications.generated.ts +0 -1509
- package/src/auth.ts +0 -48
- package/src/client.test.ts +0 -894
- package/src/client.ts +0 -1025
- package/src/errors.ts +0 -39
- package/src/index.test.ts +0 -69
- package/src/index.ts +0 -125
- package/src/modules/crm.test.ts +0 -208
- package/src/modules/crm.ts +0 -110
- package/src/modules/identity.test.ts +0 -165
- package/src/modules/identity.ts +0 -18
- package/src/modules/p2p.test.ts +0 -540
- package/src/modules/p2p.ts +0 -448
- package/src/node/credential-store.ts +0 -378
- package/src/oauth.test.ts +0 -216
- package/src/oauth.ts +0 -138
- package/src/services.generated.ts +0 -55
- package/src/services.test.ts +0 -243
- package/src/services.ts +0 -138
- package/src/widgets.test.ts +0 -416
- package/src/widgets.ts +0 -362
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,96 @@ 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
|
+
async getActionCatalogSummary(service) {
|
|
2455
|
+
const boundedService = service.trim();
|
|
2456
|
+
if (!boundedService) {
|
|
2457
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2399
2458
|
}
|
|
2400
|
-
|
|
2459
|
+
const res = await this.request({
|
|
2460
|
+
service: "developers",
|
|
2461
|
+
method: "GET",
|
|
2462
|
+
path: "/api/actions/catalog",
|
|
2463
|
+
query: { service: boundedService, summary: "true" }
|
|
2464
|
+
});
|
|
2465
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2466
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2467
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2468
|
+
}
|
|
2469
|
+
const record = raw;
|
|
2470
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2471
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2472
|
+
}
|
|
2473
|
+
return {
|
|
2474
|
+
algorithm: "sha256-v1",
|
|
2475
|
+
digest: record.digest,
|
|
2476
|
+
actionCount: Number(record.actionCount)
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2480
|
+
async listActions(options) {
|
|
2481
|
+
if (!options) {
|
|
2482
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2483
|
+
}
|
|
2484
|
+
return (await this.searchActions(options)).actions;
|
|
2401
2485
|
}
|
|
2402
2486
|
async getAction(action) {
|
|
2403
|
-
const
|
|
2404
|
-
|
|
2405
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2406
|
-
if (!match) {
|
|
2487
|
+
const actionId = action.trim();
|
|
2488
|
+
if (!actionId) {
|
|
2407
2489
|
throw new TopoloSdkError(
|
|
2408
2490
|
"unknown_action",
|
|
2409
|
-
|
|
2491
|
+
"action id is required"
|
|
2410
2492
|
);
|
|
2411
2493
|
}
|
|
2494
|
+
let res;
|
|
2495
|
+
try {
|
|
2496
|
+
res = await this.request({
|
|
2497
|
+
service: "developers",
|
|
2498
|
+
method: "GET",
|
|
2499
|
+
path: "/api/actions/catalog",
|
|
2500
|
+
query: { actionId }
|
|
2501
|
+
});
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2504
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2505
|
+
}
|
|
2506
|
+
throw error;
|
|
2507
|
+
}
|
|
2508
|
+
const raw = res.data?.action ?? res.action;
|
|
2509
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2510
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2412
2511
|
return match;
|
|
2413
2512
|
}
|
|
2414
2513
|
async callAction(action, input = {}, options = {}) {
|
|
2415
2514
|
const entry = await this.getAction(action);
|
|
2515
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2516
|
+
if (agentAccess === "off") {
|
|
2517
|
+
throw new TopoloPermissionError(
|
|
2518
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2519
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2523
|
+
throw new TopoloConfirmationRequiredError(
|
|
2524
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2416
2527
|
const mapped = mapActionInput(entry, input);
|
|
2417
2528
|
const service = entry.appSlug || entry.appId;
|
|
2418
2529
|
return this.request({
|
|
2419
2530
|
service,
|
|
2531
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2420
2532
|
method: entry.method,
|
|
2421
2533
|
path: mapped.path,
|
|
2422
2534
|
...mapped.query ? { query: mapped.query } : {},
|
|
2423
2535
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2536
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2424
2537
|
headers: {
|
|
2425
2538
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2426
2539
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2558,6 +2671,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2558
2671
|
function normalizeLookupKey(value) {
|
|
2559
2672
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2560
2673
|
}
|
|
2674
|
+
function requireHttpOrigin(value) {
|
|
2675
|
+
let url;
|
|
2676
|
+
try {
|
|
2677
|
+
url = new URL(value);
|
|
2678
|
+
} catch {
|
|
2679
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2680
|
+
}
|
|
2681
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2682
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2683
|
+
}
|
|
2684
|
+
return url.origin;
|
|
2685
|
+
}
|
|
2561
2686
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2562
2687
|
if (!authUrl) return null;
|
|
2563
2688
|
try {
|
|
@@ -2584,6 +2709,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2584
2709
|
}
|
|
2585
2710
|
return [...keys];
|
|
2586
2711
|
}
|
|
2712
|
+
function catalogSearchQuery(options) {
|
|
2713
|
+
const query = {};
|
|
2714
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2715
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2716
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2717
|
+
return query;
|
|
2718
|
+
}
|
|
2719
|
+
function normalizeCatalogPage(value) {
|
|
2720
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2721
|
+
return {
|
|
2722
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2723
|
+
hasMore: value.hasMore === true
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2727
|
+
if (!isRecord(item)) return null;
|
|
2728
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2729
|
+
if (!appId) return null;
|
|
2730
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2731
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2732
|
+
) : [];
|
|
2733
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2734
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2735
|
+
);
|
|
2736
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2737
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2738
|
+
return {
|
|
2739
|
+
appId,
|
|
2740
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2741
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2742
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2743
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2744
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2745
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2746
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2747
|
+
launchable: item.launchable === true,
|
|
2748
|
+
permissions,
|
|
2749
|
+
apiKeyResource,
|
|
2750
|
+
apiKeyResources
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2587
2753
|
function normalizeActionCatalogEntry(item) {
|
|
2588
2754
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2589
2755
|
const a = item;
|
|
@@ -2597,6 +2763,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2597
2763
|
}
|
|
2598
2764
|
return {
|
|
2599
2765
|
actionId,
|
|
2766
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2767
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2600
2768
|
name,
|
|
2601
2769
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2602
2770
|
title: pickActionString(a.title) ?? name,
|
|
@@ -2608,6 +2776,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2608
2776
|
path,
|
|
2609
2777
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2610
2778
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2779
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2611
2780
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2612
2781
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2613
2782
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2627,16 +2796,12 @@ function pickActionString(value) {
|
|
|
2627
2796
|
function pickAgentAccess(value) {
|
|
2628
2797
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2629
2798
|
}
|
|
2799
|
+
function pickAuthorizationMode(value) {
|
|
2800
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2801
|
+
}
|
|
2630
2802
|
function plainObject(value) {
|
|
2631
2803
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2632
2804
|
}
|
|
2633
|
-
function normalizeActionLookupKey(value) {
|
|
2634
|
-
return String(value || "").trim().toLowerCase();
|
|
2635
|
-
}
|
|
2636
|
-
function actionEntryMatches(entry, needle) {
|
|
2637
|
-
if (!needle) return false;
|
|
2638
|
-
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2639
|
-
}
|
|
2640
2805
|
function mapActionInput(action, input) {
|
|
2641
2806
|
const remaining = { ...input || {} };
|
|
2642
2807
|
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -2715,6 +2880,40 @@ function mergeSignals(a, b) {
|
|
|
2715
2880
|
return controller.signal;
|
|
2716
2881
|
}
|
|
2717
2882
|
|
|
2883
|
+
// src/action-catalog-digest.ts
|
|
2884
|
+
var ACTION_CATALOG_DIGEST_ALGORITHM = "sha256-v1";
|
|
2885
|
+
function canonicalPermission(value, appId) {
|
|
2886
|
+
let permission = String(value || "").trim().toLowerCase().replace(/\s+/g, "");
|
|
2887
|
+
const prefix = `${String(appId || "").trim().toLowerCase()}.`;
|
|
2888
|
+
if (prefix !== "." && permission.startsWith(prefix)) permission = permission.slice(prefix.length);
|
|
2889
|
+
return permission.replace(/\./g, ":");
|
|
2890
|
+
}
|
|
2891
|
+
function stableJson(value) {
|
|
2892
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
2893
|
+
if (value && typeof value === "object") {
|
|
2894
|
+
const record = value;
|
|
2895
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
2896
|
+
}
|
|
2897
|
+
return JSON.stringify(value);
|
|
2898
|
+
}
|
|
2899
|
+
async function digestTopoloActionCatalog(actions, appId) {
|
|
2900
|
+
const canonical = actions.filter((action) => action.agentAccess !== "off").map((action) => ({
|
|
2901
|
+
actionId: action.actionId || `${appId}.${action.name}`,
|
|
2902
|
+
authorizationMode: action.authorizationMode === "runtime" ? "runtime" : "static",
|
|
2903
|
+
inputSchema: action.inputSchema,
|
|
2904
|
+
method: action.method,
|
|
2905
|
+
outputSchema: action.outputSchema,
|
|
2906
|
+
path: action.path,
|
|
2907
|
+
permission: canonicalPermission(action.permission ?? action.permissionName ?? "", appId),
|
|
2908
|
+
readOnly: action.readOnly,
|
|
2909
|
+
requiresConfirmation: action.requiresConfirmation,
|
|
2910
|
+
target: action.target ?? ""
|
|
2911
|
+
})).sort((left, right) => left.actionId.localeCompare(right.actionId));
|
|
2912
|
+
const bytes = new TextEncoder().encode(canonical.map(stableJson).join("\n"));
|
|
2913
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
2914
|
+
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2718
2917
|
// src/modules/crm.ts
|
|
2719
2918
|
var CrmModule = class {
|
|
2720
2919
|
constructor(client) {
|
|
@@ -3239,6 +3438,7 @@ function createTopolo(options) {
|
|
|
3239
3438
|
};
|
|
3240
3439
|
}
|
|
3241
3440
|
export {
|
|
3441
|
+
ACTION_CATALOG_DIGEST_ALGORITHM,
|
|
3242
3442
|
APPLICATIONS,
|
|
3243
3443
|
APPLICATION_REQUIREMENTS,
|
|
3244
3444
|
APPLICATION_REQUIREMENTS_VERSION,
|
|
@@ -3250,6 +3450,7 @@ export {
|
|
|
3250
3450
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
3251
3451
|
TopoloAuthError,
|
|
3252
3452
|
TopoloClient,
|
|
3453
|
+
TopoloConfirmationRequiredError,
|
|
3253
3454
|
TopoloHttpError,
|
|
3254
3455
|
TopoloOAuth,
|
|
3255
3456
|
TopoloPermissionError,
|
|
@@ -3265,6 +3466,7 @@ export {
|
|
|
3265
3466
|
defineTopoloQuickActionsWidget,
|
|
3266
3467
|
defineTopoloRecentActivityWidget,
|
|
3267
3468
|
defineTopoloStatsWidget,
|
|
3469
|
+
digestTopoloActionCatalog,
|
|
3268
3470
|
requirementsForApplication,
|
|
3269
3471
|
resolveCatalogServiceUrl,
|
|
3270
3472
|
resolveServiceUrl,
|
|
@@ -382,7 +382,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
382
382
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
383
383
|
}
|
|
384
384
|
var REFRESH_SKEW_SECONDS = 60;
|
|
385
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
385
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
386
386
|
const prefix = envVarPrefix(environment);
|
|
387
387
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
388
388
|
return resolveCredential(environment);
|
|
@@ -393,7 +393,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
393
393
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
394
394
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
395
395
|
const now = Math.floor(Date.now() / 1e3);
|
|
396
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
396
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
397
397
|
try {
|
|
398
398
|
const oauth = new TopoloOAuth({
|
|
399
399
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -408,9 +408,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
408
408
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
409
409
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
410
410
|
};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
const activeContext = profile.activeContext;
|
|
412
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
413
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
414
|
+
credential: refreshed,
|
|
415
|
+
contexts: {
|
|
416
|
+
...profile.contexts ?? {},
|
|
417
|
+
[activeContext]: {
|
|
418
|
+
...activeEntry,
|
|
419
|
+
credential: refreshed,
|
|
420
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
424
|
+
await writeConfig(updated);
|
|
414
425
|
return refreshed;
|
|
415
426
|
} catch {
|
|
416
427
|
return cred;
|
|
@@ -100,7 +100,9 @@ declare function credentialSourceFromConfig(config: TopoloConfig, environment?:
|
|
|
100
100
|
* interactive users get after running `topolo auth login`.
|
|
101
101
|
*/
|
|
102
102
|
declare function resolveCredential(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
|
|
103
|
-
declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment
|
|
103
|
+
declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment, options?: {
|
|
104
|
+
force?: boolean;
|
|
105
|
+
}): Promise<TopoloCredential | null>;
|
|
104
106
|
declare function resolveAgentName(configAgentName?: string): string | undefined;
|
|
105
107
|
declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
|
|
106
108
|
declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
|
|
@@ -100,7 +100,9 @@ declare function credentialSourceFromConfig(config: TopoloConfig, environment?:
|
|
|
100
100
|
* interactive users get after running `topolo auth login`.
|
|
101
101
|
*/
|
|
102
102
|
declare function resolveCredential(environment?: TopoloEnvironment): Promise<TopoloCredential | null>;
|
|
103
|
-
declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment
|
|
103
|
+
declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment, options?: {
|
|
104
|
+
force?: boolean;
|
|
105
|
+
}): Promise<TopoloCredential | null>;
|
|
104
106
|
declare function resolveAgentName(configAgentName?: string): string | undefined;
|
|
105
107
|
declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
|
|
106
108
|
declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
|
|
@@ -339,7 +339,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
339
339
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
340
340
|
}
|
|
341
341
|
var REFRESH_SKEW_SECONDS = 60;
|
|
342
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
342
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
343
343
|
const prefix = envVarPrefix(environment);
|
|
344
344
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
345
345
|
return resolveCredential(environment);
|
|
@@ -350,7 +350,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
350
350
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
351
351
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
352
352
|
const now = Math.floor(Date.now() / 1e3);
|
|
353
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
353
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
354
354
|
try {
|
|
355
355
|
const oauth = new TopoloOAuth({
|
|
356
356
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -365,9 +365,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
365
365
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
366
366
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
367
367
|
};
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
const activeContext = profile.activeContext;
|
|
369
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
370
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
371
|
+
credential: refreshed,
|
|
372
|
+
contexts: {
|
|
373
|
+
...profile.contexts ?? {},
|
|
374
|
+
[activeContext]: {
|
|
375
|
+
...activeEntry,
|
|
376
|
+
credential: refreshed,
|
|
377
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
381
|
+
await writeConfig(updated);
|
|
371
382
|
return refreshed;
|
|
372
383
|
} catch {
|
|
373
384
|
return cred;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"topoloPublishedVersion": "0.8.0",
|
|
4
5
|
"description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "./dist/index.js",
|
|
@@ -19,8 +20,7 @@
|
|
|
19
20
|
}
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
"src"
|
|
23
|
+
"dist"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"generate:services": "node scripts/generate-services.mjs",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"typecheck": "node scripts/generate-services.mjs && tsc --noEmit",
|
|
32
32
|
"test": "vitest run",
|
|
33
33
|
"test:watch": "vitest",
|
|
34
|
-
"prepublishOnly": "npm run build",
|
|
34
|
+
"prepublishOnly": "npm run build && npm run verify:package",
|
|
35
|
+
"verify:package": "node ../../tooling/verify-npm-package.mjs .",
|
|
35
36
|
"coverage": "vitest run --coverage"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|