@topolo/sdk 0.4.2 → 0.4.4
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 +31 -7
- package/dist/index.cjs +156 -0
- package/dist/index.d.cts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +156 -0
- package/package.json +1 -1
- package/src/client.test.ts +172 -22
- package/src/client.ts +215 -0
- package/src/index.ts +1 -0
- package/src/services.ts +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,15 @@ const topolo = createTopolo({
|
|
|
35
35
|
const me = await topolo.identity.whoami();
|
|
36
36
|
console.log(me.organization?.slug);
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const actions = await topolo.client.listActions({ service: 'mail' });
|
|
39
|
+
const sendMessage = actions.find((action) => action.name === 'messages.send');
|
|
40
|
+
if (sendMessage) {
|
|
41
|
+
await topolo.client.callAction(sendMessage.actionId, {
|
|
42
|
+
mailboxId: 'primary',
|
|
43
|
+
subject: 'Hello',
|
|
44
|
+
text: '...',
|
|
45
|
+
}, { confirm: true });
|
|
46
|
+
}
|
|
39
47
|
```
|
|
40
48
|
|
|
41
49
|
## Design invariants
|
|
@@ -73,14 +81,16 @@ Every request sends:
|
|
|
73
81
|
Combined with the credential identity, this makes every platform API call
|
|
74
82
|
traceable to a specific agent invocation.
|
|
75
83
|
|
|
76
|
-
## Modules
|
|
84
|
+
## Modules
|
|
77
85
|
|
|
78
|
-
|
|
|
86
|
+
| Surface | Methods |
|
|
79
87
|
| ------------ | ------------------------------------- |
|
|
80
88
|
| `identity` | `whoami()` |
|
|
81
|
-
| `
|
|
89
|
+
| `client` | `listServices()`, `listActions()`, `getAction()`, `callAction()` |
|
|
90
|
+
| app modules | compatibility helpers for stable app-specific contracts |
|
|
82
91
|
|
|
83
|
-
|
|
92
|
+
Agents should use the action catalog for app-specific operations so new
|
|
93
|
+
services and actions become usable without an SDK, CLI, or MCP release.
|
|
84
94
|
|
|
85
95
|
## Application catalog
|
|
86
96
|
|
|
@@ -99,6 +109,20 @@ bundled service ID is only a convenient alias after Auth confirms access.
|
|
|
99
109
|
`listBundledServices()` is available for packaging/audit tooling that needs the
|
|
100
110
|
generated snapshot. Do not use it to decide what an end user or agent may call.
|
|
101
111
|
|
|
112
|
+
## Action catalog
|
|
113
|
+
|
|
114
|
+
`TopoloClient.listActions()` reads TopoloAuth's live action catalog for the
|
|
115
|
+
current credential. Applications publish their action definitions through
|
|
116
|
+
Topolo Developers, and Auth returns only actions whose existing service
|
|
117
|
+
permission is granted to the credential. The action entry includes the service,
|
|
118
|
+
HTTP method/path, required permission, input/output schemas, read-only and
|
|
119
|
+
destructive hints, and the stable MCP/agent tool name.
|
|
120
|
+
|
|
121
|
+
`TopoloClient.callAction(actionRef, input, options)` resolves an action by
|
|
122
|
+
`actionId`, `name`, or `toolName`, maps path parameters and query/body values
|
|
123
|
+
from `input`, and routes the request through the same auth, audit-header, and
|
|
124
|
+
write-confirmation flow as `request()`.
|
|
125
|
+
|
|
102
126
|
`APPLICATION_REQUIREMENTS` is the versioned app-build contract agents should
|
|
103
127
|
read before creating or expanding a Topolo app. `requirementsForApplication()`
|
|
104
128
|
filters the shared contract to the browser/API/tooling scopes implied by the app
|
|
@@ -162,12 +186,12 @@ Defaults target production. Override per-service for staging/dev:
|
|
|
162
186
|
createTopolo({
|
|
163
187
|
/* ... */
|
|
164
188
|
serviceUrls: {
|
|
165
|
-
|
|
189
|
+
mail: 'https://mail.stg.topolo.us',
|
|
166
190
|
},
|
|
167
191
|
});
|
|
168
192
|
```
|
|
169
193
|
|
|
170
|
-
Or via env: `
|
|
194
|
+
Or via env: `TOPOLO_SERVICE_URL_MAIL=...` (falls through `resolveServiceUrl`).
|
|
171
195
|
|
|
172
196
|
## Development
|
|
173
197
|
|
package/dist/index.cjs
CHANGED
|
@@ -2110,6 +2110,8 @@ function resolveServiceUrl(service, overrides) {
|
|
|
2110
2110
|
|
|
2111
2111
|
// src/client.ts
|
|
2112
2112
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2113
|
+
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2114
|
+
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
2113
2115
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2114
2116
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2115
2117
|
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
@@ -2123,6 +2125,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2123
2125
|
fetchImpl;
|
|
2124
2126
|
debug;
|
|
2125
2127
|
serviceCatalogCache = null;
|
|
2128
|
+
actionCatalogCache = null;
|
|
2126
2129
|
/**
|
|
2127
2130
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2128
2131
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2383,6 +2386,67 @@ var TopoloClient = class _TopoloClient {
|
|
|
2383
2386
|
};
|
|
2384
2387
|
return entries;
|
|
2385
2388
|
}
|
|
2389
|
+
/**
|
|
2390
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2391
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2392
|
+
* action list is used as an authorization source.
|
|
2393
|
+
*/
|
|
2394
|
+
async listActions(options = {}) {
|
|
2395
|
+
const now = Date.now();
|
|
2396
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2397
|
+
return this.actionCatalogCache.entries;
|
|
2398
|
+
}
|
|
2399
|
+
const res = await this.request({
|
|
2400
|
+
service: "auth",
|
|
2401
|
+
method: "GET",
|
|
2402
|
+
path: "/api/actions/catalog",
|
|
2403
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2404
|
+
});
|
|
2405
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2406
|
+
if (!Array.isArray(raw)) {
|
|
2407
|
+
throw new TopoloSdkError(
|
|
2408
|
+
"action_catalog_unavailable",
|
|
2409
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2413
|
+
if (!options.service) {
|
|
2414
|
+
this.actionCatalogCache = {
|
|
2415
|
+
entries,
|
|
2416
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
return entries;
|
|
2420
|
+
}
|
|
2421
|
+
async getAction(action) {
|
|
2422
|
+
const needle = normalizeActionLookupKey(action);
|
|
2423
|
+
const actions = await this.listActions();
|
|
2424
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2425
|
+
if (!match) {
|
|
2426
|
+
throw new TopoloSdkError(
|
|
2427
|
+
"unknown_action",
|
|
2428
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
return match;
|
|
2432
|
+
}
|
|
2433
|
+
async callAction(action, input = {}, options = {}) {
|
|
2434
|
+
const entry = await this.getAction(action);
|
|
2435
|
+
const mapped = mapActionInput(entry, input);
|
|
2436
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2437
|
+
return this.request({
|
|
2438
|
+
service,
|
|
2439
|
+
method: entry.method,
|
|
2440
|
+
path: mapped.path,
|
|
2441
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2442
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2443
|
+
headers: {
|
|
2444
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2445
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2446
|
+
},
|
|
2447
|
+
confirm: options.confirm === true
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2386
2450
|
/**
|
|
2387
2451
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2388
2452
|
* for deciding what the current credential may use.
|
|
@@ -2492,6 +2556,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2492
2556
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2493
2557
|
);
|
|
2494
2558
|
}
|
|
2559
|
+
function normalizeActionCatalogEntry(item) {
|
|
2560
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2561
|
+
const a = item;
|
|
2562
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2563
|
+
const name = pickActionString(a.name);
|
|
2564
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2565
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2566
|
+
const path = pickActionString(a.path);
|
|
2567
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
return {
|
|
2571
|
+
actionId,
|
|
2572
|
+
name,
|
|
2573
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2574
|
+
title: pickActionString(a.title) ?? name,
|
|
2575
|
+
description: pickActionString(a.description) ?? "",
|
|
2576
|
+
serviceId,
|
|
2577
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2578
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2579
|
+
method,
|
|
2580
|
+
path,
|
|
2581
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2582
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2583
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2584
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2585
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2586
|
+
destructive: Boolean(a.destructive),
|
|
2587
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2588
|
+
status: pickActionString(a.status),
|
|
2589
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
function isHttpMethod(value) {
|
|
2593
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2594
|
+
}
|
|
2595
|
+
function pickActionString(value) {
|
|
2596
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2597
|
+
}
|
|
2598
|
+
function plainObject(value) {
|
|
2599
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2600
|
+
}
|
|
2601
|
+
function normalizeActionLookupKey(value) {
|
|
2602
|
+
return String(value || "").trim().toLowerCase();
|
|
2603
|
+
}
|
|
2604
|
+
function actionEntryMatches(entry, needle) {
|
|
2605
|
+
if (!needle) return false;
|
|
2606
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2607
|
+
}
|
|
2608
|
+
function mapActionInput(action, input) {
|
|
2609
|
+
const remaining = { ...input || {} };
|
|
2610
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2611
|
+
const value = remaining[key];
|
|
2612
|
+
if (value === void 0 || value === null || value === "") {
|
|
2613
|
+
throw new TopoloSdkError(
|
|
2614
|
+
"invalid_action_input",
|
|
2615
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
delete remaining[key];
|
|
2619
|
+
return encodeURIComponent(String(value));
|
|
2620
|
+
});
|
|
2621
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2622
|
+
delete remaining.query;
|
|
2623
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2624
|
+
delete remaining.body;
|
|
2625
|
+
if (action.method === "GET") {
|
|
2626
|
+
return {
|
|
2627
|
+
path,
|
|
2628
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
const query = objectToQuery(explicitQuery);
|
|
2632
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2633
|
+
return {
|
|
2634
|
+
path,
|
|
2635
|
+
...query ? { query } : {},
|
|
2636
|
+
body
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
function objectToQuery(value) {
|
|
2640
|
+
const query = {};
|
|
2641
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2642
|
+
if (raw === void 0 || raw === null) continue;
|
|
2643
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2644
|
+
query[key] = raw;
|
|
2645
|
+
} else {
|
|
2646
|
+
query[key] = JSON.stringify(raw);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2650
|
+
}
|
|
2495
2651
|
function describeError(body, fallback) {
|
|
2496
2652
|
if (body && typeof body === "object") {
|
|
2497
2653
|
const maybe = body;
|
package/dist/index.d.cts
CHANGED
|
@@ -110,6 +110,27 @@ interface TopoloServiceCatalogEntry {
|
|
|
110
110
|
launchable: boolean;
|
|
111
111
|
permissions: string[];
|
|
112
112
|
}
|
|
113
|
+
interface TopoloActionCatalogEntry {
|
|
114
|
+
actionId: string;
|
|
115
|
+
name: string;
|
|
116
|
+
toolName: string | null;
|
|
117
|
+
title: string;
|
|
118
|
+
description: string;
|
|
119
|
+
serviceId: string;
|
|
120
|
+
serviceSlug: string | null;
|
|
121
|
+
serviceName: string | null;
|
|
122
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
123
|
+
path: string;
|
|
124
|
+
permissionName: string;
|
|
125
|
+
requiredPermission: string;
|
|
126
|
+
inputSchema: Record<string, unknown>;
|
|
127
|
+
outputSchema: Record<string, unknown>;
|
|
128
|
+
readOnly: boolean;
|
|
129
|
+
destructive: boolean;
|
|
130
|
+
requiresConfirmation: boolean;
|
|
131
|
+
status: string | null;
|
|
132
|
+
sourceRevision: string | null;
|
|
133
|
+
}
|
|
113
134
|
declare class TopoloClient {
|
|
114
135
|
private readonly credential;
|
|
115
136
|
private readonly agent;
|
|
@@ -120,6 +141,7 @@ declare class TopoloClient {
|
|
|
120
141
|
private readonly fetchImpl;
|
|
121
142
|
private readonly debug?;
|
|
122
143
|
private serviceCatalogCache;
|
|
144
|
+
private actionCatalogCache;
|
|
123
145
|
/**
|
|
124
146
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
125
147
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -166,6 +188,18 @@ declare class TopoloClient {
|
|
|
166
188
|
* organization and effective permissions.
|
|
167
189
|
*/
|
|
168
190
|
listServices(): Promise<TopoloServiceCatalogEntry[]>;
|
|
191
|
+
/**
|
|
192
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
193
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
194
|
+
* action list is used as an authorization source.
|
|
195
|
+
*/
|
|
196
|
+
listActions(options?: {
|
|
197
|
+
service?: string;
|
|
198
|
+
}): Promise<TopoloActionCatalogEntry[]>;
|
|
199
|
+
getAction(action: string): Promise<TopoloActionCatalogEntry>;
|
|
200
|
+
callAction<T = unknown>(action: string, input?: Record<string, unknown>, options?: {
|
|
201
|
+
confirm?: boolean;
|
|
202
|
+
}): Promise<T>;
|
|
169
203
|
/**
|
|
170
204
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
171
205
|
* for deciding what the current credential may use.
|
|
@@ -1536,7 +1570,7 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
|
|
|
1536
1570
|
* `scripts/generate-services.mjs`; edit the JSON, not the generated file.
|
|
1537
1571
|
*
|
|
1538
1572
|
* Any entry can be overridden per-call via `TopoloClient` options, or globally
|
|
1539
|
-
* via env vars like `
|
|
1573
|
+
* via env vars like `TOPOLO_SERVICE_URL_MAIL` or `TOPOLO_SERVICE_URL_WEB_API`
|
|
1540
1574
|
* (dots in service IDs become underscores in env vars).
|
|
1541
1575
|
*
|
|
1542
1576
|
* IMPORTANT: The SDK NEVER accepts `orgId` as a parameter. Every request is
|
|
@@ -2051,4 +2085,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
2051
2085
|
};
|
|
2052
2086
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
2053
2087
|
|
|
2054
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, 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_SERVICE_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 ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, 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, resolveServiceUrl, validateTopoloWidgetResponse };
|
|
2088
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, 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_SERVICE_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 ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, 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, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -110,6 +110,27 @@ interface TopoloServiceCatalogEntry {
|
|
|
110
110
|
launchable: boolean;
|
|
111
111
|
permissions: string[];
|
|
112
112
|
}
|
|
113
|
+
interface TopoloActionCatalogEntry {
|
|
114
|
+
actionId: string;
|
|
115
|
+
name: string;
|
|
116
|
+
toolName: string | null;
|
|
117
|
+
title: string;
|
|
118
|
+
description: string;
|
|
119
|
+
serviceId: string;
|
|
120
|
+
serviceSlug: string | null;
|
|
121
|
+
serviceName: string | null;
|
|
122
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
123
|
+
path: string;
|
|
124
|
+
permissionName: string;
|
|
125
|
+
requiredPermission: string;
|
|
126
|
+
inputSchema: Record<string, unknown>;
|
|
127
|
+
outputSchema: Record<string, unknown>;
|
|
128
|
+
readOnly: boolean;
|
|
129
|
+
destructive: boolean;
|
|
130
|
+
requiresConfirmation: boolean;
|
|
131
|
+
status: string | null;
|
|
132
|
+
sourceRevision: string | null;
|
|
133
|
+
}
|
|
113
134
|
declare class TopoloClient {
|
|
114
135
|
private readonly credential;
|
|
115
136
|
private readonly agent;
|
|
@@ -120,6 +141,7 @@ declare class TopoloClient {
|
|
|
120
141
|
private readonly fetchImpl;
|
|
121
142
|
private readonly debug?;
|
|
122
143
|
private serviceCatalogCache;
|
|
144
|
+
private actionCatalogCache;
|
|
123
145
|
/**
|
|
124
146
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
125
147
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -166,6 +188,18 @@ declare class TopoloClient {
|
|
|
166
188
|
* organization and effective permissions.
|
|
167
189
|
*/
|
|
168
190
|
listServices(): Promise<TopoloServiceCatalogEntry[]>;
|
|
191
|
+
/**
|
|
192
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
193
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
194
|
+
* action list is used as an authorization source.
|
|
195
|
+
*/
|
|
196
|
+
listActions(options?: {
|
|
197
|
+
service?: string;
|
|
198
|
+
}): Promise<TopoloActionCatalogEntry[]>;
|
|
199
|
+
getAction(action: string): Promise<TopoloActionCatalogEntry>;
|
|
200
|
+
callAction<T = unknown>(action: string, input?: Record<string, unknown>, options?: {
|
|
201
|
+
confirm?: boolean;
|
|
202
|
+
}): Promise<T>;
|
|
169
203
|
/**
|
|
170
204
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
171
205
|
* for deciding what the current credential may use.
|
|
@@ -1536,7 +1570,7 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
|
|
|
1536
1570
|
* `scripts/generate-services.mjs`; edit the JSON, not the generated file.
|
|
1537
1571
|
*
|
|
1538
1572
|
* Any entry can be overridden per-call via `TopoloClient` options, or globally
|
|
1539
|
-
* via env vars like `
|
|
1573
|
+
* via env vars like `TOPOLO_SERVICE_URL_MAIL` or `TOPOLO_SERVICE_URL_WEB_API`
|
|
1540
1574
|
* (dots in service IDs become underscores in env vars).
|
|
1541
1575
|
*
|
|
1542
1576
|
* IMPORTANT: The SDK NEVER accepts `orgId` as a parameter. Every request is
|
|
@@ -2051,4 +2085,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
2051
2085
|
};
|
|
2052
2086
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
2053
2087
|
|
|
2054
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, 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_SERVICE_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 ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, 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, resolveServiceUrl, validateTopoloWidgetResponse };
|
|
2088
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, 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_SERVICE_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 ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, 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, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.js
CHANGED
|
@@ -2056,6 +2056,8 @@ function resolveServiceUrl(service, overrides) {
|
|
|
2056
2056
|
|
|
2057
2057
|
// src/client.ts
|
|
2058
2058
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2059
|
+
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2060
|
+
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
2059
2061
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2060
2062
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2061
2063
|
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
@@ -2069,6 +2071,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2069
2071
|
fetchImpl;
|
|
2070
2072
|
debug;
|
|
2071
2073
|
serviceCatalogCache = null;
|
|
2074
|
+
actionCatalogCache = null;
|
|
2072
2075
|
/**
|
|
2073
2076
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2074
2077
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2329,6 +2332,67 @@ var TopoloClient = class _TopoloClient {
|
|
|
2329
2332
|
};
|
|
2330
2333
|
return entries;
|
|
2331
2334
|
}
|
|
2335
|
+
/**
|
|
2336
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2337
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2338
|
+
* action list is used as an authorization source.
|
|
2339
|
+
*/
|
|
2340
|
+
async listActions(options = {}) {
|
|
2341
|
+
const now = Date.now();
|
|
2342
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2343
|
+
return this.actionCatalogCache.entries;
|
|
2344
|
+
}
|
|
2345
|
+
const res = await this.request({
|
|
2346
|
+
service: "auth",
|
|
2347
|
+
method: "GET",
|
|
2348
|
+
path: "/api/actions/catalog",
|
|
2349
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2350
|
+
});
|
|
2351
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2352
|
+
if (!Array.isArray(raw)) {
|
|
2353
|
+
throw new TopoloSdkError(
|
|
2354
|
+
"action_catalog_unavailable",
|
|
2355
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2359
|
+
if (!options.service) {
|
|
2360
|
+
this.actionCatalogCache = {
|
|
2361
|
+
entries,
|
|
2362
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
return entries;
|
|
2366
|
+
}
|
|
2367
|
+
async getAction(action) {
|
|
2368
|
+
const needle = normalizeActionLookupKey(action);
|
|
2369
|
+
const actions = await this.listActions();
|
|
2370
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2371
|
+
if (!match) {
|
|
2372
|
+
throw new TopoloSdkError(
|
|
2373
|
+
"unknown_action",
|
|
2374
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
return match;
|
|
2378
|
+
}
|
|
2379
|
+
async callAction(action, input = {}, options = {}) {
|
|
2380
|
+
const entry = await this.getAction(action);
|
|
2381
|
+
const mapped = mapActionInput(entry, input);
|
|
2382
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2383
|
+
return this.request({
|
|
2384
|
+
service,
|
|
2385
|
+
method: entry.method,
|
|
2386
|
+
path: mapped.path,
|
|
2387
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2388
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2389
|
+
headers: {
|
|
2390
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2391
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2392
|
+
},
|
|
2393
|
+
confirm: options.confirm === true
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2332
2396
|
/**
|
|
2333
2397
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2334
2398
|
* for deciding what the current credential may use.
|
|
@@ -2438,6 +2502,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2438
2502
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2439
2503
|
);
|
|
2440
2504
|
}
|
|
2505
|
+
function normalizeActionCatalogEntry(item) {
|
|
2506
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2507
|
+
const a = item;
|
|
2508
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2509
|
+
const name = pickActionString(a.name);
|
|
2510
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2511
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2512
|
+
const path = pickActionString(a.path);
|
|
2513
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
return {
|
|
2517
|
+
actionId,
|
|
2518
|
+
name,
|
|
2519
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2520
|
+
title: pickActionString(a.title) ?? name,
|
|
2521
|
+
description: pickActionString(a.description) ?? "",
|
|
2522
|
+
serviceId,
|
|
2523
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2524
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2525
|
+
method,
|
|
2526
|
+
path,
|
|
2527
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2528
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2529
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2530
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2531
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2532
|
+
destructive: Boolean(a.destructive),
|
|
2533
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2534
|
+
status: pickActionString(a.status),
|
|
2535
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
function isHttpMethod(value) {
|
|
2539
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2540
|
+
}
|
|
2541
|
+
function pickActionString(value) {
|
|
2542
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2543
|
+
}
|
|
2544
|
+
function plainObject(value) {
|
|
2545
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2546
|
+
}
|
|
2547
|
+
function normalizeActionLookupKey(value) {
|
|
2548
|
+
return String(value || "").trim().toLowerCase();
|
|
2549
|
+
}
|
|
2550
|
+
function actionEntryMatches(entry, needle) {
|
|
2551
|
+
if (!needle) return false;
|
|
2552
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2553
|
+
}
|
|
2554
|
+
function mapActionInput(action, input) {
|
|
2555
|
+
const remaining = { ...input || {} };
|
|
2556
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2557
|
+
const value = remaining[key];
|
|
2558
|
+
if (value === void 0 || value === null || value === "") {
|
|
2559
|
+
throw new TopoloSdkError(
|
|
2560
|
+
"invalid_action_input",
|
|
2561
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
delete remaining[key];
|
|
2565
|
+
return encodeURIComponent(String(value));
|
|
2566
|
+
});
|
|
2567
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2568
|
+
delete remaining.query;
|
|
2569
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2570
|
+
delete remaining.body;
|
|
2571
|
+
if (action.method === "GET") {
|
|
2572
|
+
return {
|
|
2573
|
+
path,
|
|
2574
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
const query = objectToQuery(explicitQuery);
|
|
2578
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2579
|
+
return {
|
|
2580
|
+
path,
|
|
2581
|
+
...query ? { query } : {},
|
|
2582
|
+
body
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
function objectToQuery(value) {
|
|
2586
|
+
const query = {};
|
|
2587
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2588
|
+
if (raw === void 0 || raw === null) continue;
|
|
2589
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2590
|
+
query[key] = raw;
|
|
2591
|
+
} else {
|
|
2592
|
+
query[key] = JSON.stringify(raw);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2596
|
+
}
|
|
2441
2597
|
function describeError(body, fallback) {
|
|
2442
2598
|
if (body && typeof body === "object") {
|
|
2443
2599
|
const maybe = body;
|
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -17,18 +17,83 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown; contentTy
|
|
|
17
17
|
|
|
18
18
|
const AGENT = { clientName: 'topolo-test', clientVersion: '0.0.0' };
|
|
19
19
|
const SERVICE_LOOKUP = { body: { success: true, data: { service_id: 'service_test' } } };
|
|
20
|
-
const
|
|
20
|
+
const MAIL_CATALOG = {
|
|
21
21
|
body: {
|
|
22
22
|
data: {
|
|
23
23
|
services: [
|
|
24
24
|
{
|
|
25
|
-
service_id: '
|
|
26
|
-
app_slug: 'topolo-
|
|
27
|
-
name: 'Topolo
|
|
28
|
-
base_url: 'https://
|
|
25
|
+
service_id: 'srv_mail',
|
|
26
|
+
app_slug: 'topolo-mail',
|
|
27
|
+
name: 'Topolo Mail',
|
|
28
|
+
base_url: 'https://mail-api.topolo.app',
|
|
29
29
|
surface_kind: 'api',
|
|
30
30
|
status: 'active',
|
|
31
|
-
granted_permissions: ['
|
|
31
|
+
granted_permissions: ['srv_mail.messages:read'],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const ACTION_CATALOG = {
|
|
38
|
+
body: {
|
|
39
|
+
data: {
|
|
40
|
+
actions: [
|
|
41
|
+
{
|
|
42
|
+
action_id: 'srv_mail.messages.list',
|
|
43
|
+
name: 'messages.list',
|
|
44
|
+
tool_name: 'topolo_mail_messages_list',
|
|
45
|
+
title: 'List messages',
|
|
46
|
+
description: 'List Mail messages',
|
|
47
|
+
service_id: 'srv_mail',
|
|
48
|
+
service_slug: 'topolo-mail',
|
|
49
|
+
service_name: 'Topolo Mail',
|
|
50
|
+
method: 'GET',
|
|
51
|
+
path: '/api/mail/mailboxes/{mailboxId}/messages',
|
|
52
|
+
permission_name: 'messages:read',
|
|
53
|
+
required_permission: 'srv_mail.messages:read',
|
|
54
|
+
input_schema: { type: 'object' },
|
|
55
|
+
output_schema: {},
|
|
56
|
+
read_only: true,
|
|
57
|
+
destructive: false,
|
|
58
|
+
requires_confirmation: false,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
action_id: 'srv_mail.messages.get',
|
|
62
|
+
name: 'messages.get',
|
|
63
|
+
tool_name: 'topolo_mail_messages_get',
|
|
64
|
+
title: 'Get message',
|
|
65
|
+
description: 'Get Mail message',
|
|
66
|
+
service_id: 'srv_mail',
|
|
67
|
+
service_slug: 'topolo-mail',
|
|
68
|
+
service_name: 'Topolo Mail',
|
|
69
|
+
method: 'GET',
|
|
70
|
+
path: '/api/mail/mailboxes/{mailboxId}/messages/{messageId}',
|
|
71
|
+
permission_name: 'messages:read',
|
|
72
|
+
required_permission: 'srv_mail.messages:read',
|
|
73
|
+
input_schema: { type: 'object' },
|
|
74
|
+
output_schema: {},
|
|
75
|
+
read_only: true,
|
|
76
|
+
destructive: false,
|
|
77
|
+
requires_confirmation: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
action_id: 'srv_mail.messages.send',
|
|
81
|
+
name: 'messages.send',
|
|
82
|
+
tool_name: 'topolo_mail_messages_send',
|
|
83
|
+
title: 'Send message',
|
|
84
|
+
description: 'Send Mail message',
|
|
85
|
+
service_id: 'srv_mail',
|
|
86
|
+
service_slug: 'topolo-mail',
|
|
87
|
+
service_name: 'Topolo Mail',
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/api/mail/mailboxes/{mailboxId}/messages',
|
|
90
|
+
permission_name: 'messages:send',
|
|
91
|
+
required_permission: 'srv_mail.messages:send',
|
|
92
|
+
input_schema: { type: 'object' },
|
|
93
|
+
output_schema: {},
|
|
94
|
+
read_only: false,
|
|
95
|
+
destructive: false,
|
|
96
|
+
requires_confirmation: true,
|
|
32
97
|
},
|
|
33
98
|
],
|
|
34
99
|
},
|
|
@@ -38,7 +103,8 @@ const CRM_CATALOG = {
|
|
|
38
103
|
function requestCalls(calls: Array<{ url: string; init: RequestInit }>) {
|
|
39
104
|
return calls.filter((call) =>
|
|
40
105
|
!call.url.includes('/api/services/by-slug/') &&
|
|
41
|
-
!call.url.includes('/api/services/catalog')
|
|
106
|
+
!call.url.includes('/api/services/catalog') &&
|
|
107
|
+
!call.url.includes('/api/actions/catalog')
|
|
42
108
|
);
|
|
43
109
|
}
|
|
44
110
|
|
|
@@ -48,7 +114,7 @@ beforeEach(() => {
|
|
|
48
114
|
|
|
49
115
|
describe('TopoloClient.request', () => {
|
|
50
116
|
it('injects Bearer auth + audit headers for access_token credentials', async () => {
|
|
51
|
-
const { impl, calls } = stubFetch([
|
|
117
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { ok: true } }]);
|
|
52
118
|
const client = new TopoloClient({
|
|
53
119
|
credential: { kind: 'access_token', accessToken: 'tok_abc' },
|
|
54
120
|
agent: AGENT,
|
|
@@ -64,13 +130,13 @@ describe('TopoloClient.request', () => {
|
|
|
64
130
|
});
|
|
65
131
|
|
|
66
132
|
it('uses X-Api-Key header for api_key credentials', async () => {
|
|
67
|
-
const { impl, calls } = stubFetch([
|
|
133
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { ok: true } }]);
|
|
68
134
|
const client = new TopoloClient({
|
|
69
135
|
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
70
136
|
agent: AGENT,
|
|
71
137
|
fetch: impl,
|
|
72
138
|
});
|
|
73
|
-
await client.request({ service: '
|
|
139
|
+
await client.request({ service: 'mail', path: '/api/mail/mailboxes/primary/messages' });
|
|
74
140
|
const headers = new Headers(requestCalls(calls)[0]!.init.headers);
|
|
75
141
|
expect(headers.get('X-Api-Key')).toBe('dak_xyz');
|
|
76
142
|
expect(headers.get('Authorization')).toBeNull();
|
|
@@ -84,22 +150,22 @@ describe('TopoloClient.request', () => {
|
|
|
84
150
|
fetch: impl,
|
|
85
151
|
});
|
|
86
152
|
await expect(
|
|
87
|
-
client.request({ service: '
|
|
153
|
+
client.request({ service: 'mail', method: 'POST', path: '/api/mail/mailboxes/primary/messages', body: {} }),
|
|
88
154
|
).rejects.toBeInstanceOf(TopoloAuthError);
|
|
89
155
|
});
|
|
90
156
|
|
|
91
157
|
it('allows mutating requests with confirm: true', async () => {
|
|
92
|
-
const { impl, calls } = stubFetch([
|
|
158
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { id: 'm_1' } }]);
|
|
93
159
|
const client = new TopoloClient({
|
|
94
160
|
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
95
161
|
agent: AGENT,
|
|
96
162
|
fetch: impl,
|
|
97
163
|
});
|
|
98
164
|
await client.request({
|
|
99
|
-
service: '
|
|
165
|
+
service: 'mail',
|
|
100
166
|
method: 'POST',
|
|
101
|
-
path: '/api/
|
|
102
|
-
body: {
|
|
167
|
+
path: '/api/mail/mailboxes/primary/messages',
|
|
168
|
+
body: { subject: 'Hello' },
|
|
103
169
|
confirm: true,
|
|
104
170
|
});
|
|
105
171
|
expect(requestCalls(calls)[0]!.init.method).toBe('POST');
|
|
@@ -142,8 +208,8 @@ describe('TopoloClient.request', () => {
|
|
|
142
208
|
|
|
143
209
|
it('maps 403 → TopoloPermissionError with required scopes extracted', async () => {
|
|
144
210
|
const { impl } = stubFetch([
|
|
145
|
-
|
|
146
|
-
{ status: 403, body: { error: 'forbidden', required: ['
|
|
211
|
+
MAIL_CATALOG,
|
|
212
|
+
{ status: 403, body: { error: 'forbidden', required: ['mail.messages:send'] } },
|
|
147
213
|
]);
|
|
148
214
|
const client = new TopoloClient({
|
|
149
215
|
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
@@ -151,31 +217,31 @@ describe('TopoloClient.request', () => {
|
|
|
151
217
|
fetch: impl,
|
|
152
218
|
});
|
|
153
219
|
await expect(
|
|
154
|
-
client.request({ service: '
|
|
220
|
+
client.request({ service: 'mail', method: 'POST', path: '/api/mail/mailboxes/primary/messages', body: {}, confirm: true }),
|
|
155
221
|
).rejects.toBeInstanceOf(TopoloPermissionError);
|
|
156
222
|
});
|
|
157
223
|
|
|
158
224
|
it('maps non-401/403 failures → TopoloHttpError with status preserved', async () => {
|
|
159
|
-
const { impl } = stubFetch([
|
|
225
|
+
const { impl } = stubFetch([MAIL_CATALOG, { status: 500, body: { error: 'boom' } }]);
|
|
160
226
|
const client = new TopoloClient({
|
|
161
227
|
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
162
228
|
agent: AGENT,
|
|
163
229
|
fetch: impl,
|
|
164
230
|
});
|
|
165
|
-
await expect(client.request({ service: '
|
|
231
|
+
await expect(client.request({ service: 'mail', path: '/api/mail/mailboxes/primary/messages' })).rejects.toMatchObject({
|
|
166
232
|
name: 'TopoloHttpError',
|
|
167
233
|
status: 500,
|
|
168
234
|
});
|
|
169
235
|
});
|
|
170
236
|
|
|
171
237
|
it('appends query params as search string', async () => {
|
|
172
|
-
const { impl, calls } = stubFetch([
|
|
238
|
+
const { impl, calls } = stubFetch([MAIL_CATALOG, { body: { messages: [] } }]);
|
|
173
239
|
const client = new TopoloClient({
|
|
174
240
|
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
175
241
|
agent: AGENT,
|
|
176
242
|
fetch: impl,
|
|
177
243
|
});
|
|
178
|
-
await client.request({ service: '
|
|
244
|
+
await client.request({ service: 'mail', path: '/api/mail/mailboxes/primary/messages', query: { q: 'acme', page: 2 } });
|
|
179
245
|
const u = new URL(requestCalls(calls)[0]!.url);
|
|
180
246
|
expect(u.searchParams.get('q')).toBe('acme');
|
|
181
247
|
expect(u.searchParams.get('page')).toBe('2');
|
|
@@ -576,5 +642,89 @@ describe('TopoloClient.listServices', () => {
|
|
|
576
642
|
});
|
|
577
643
|
});
|
|
578
644
|
|
|
645
|
+
describe('TopoloClient action catalog', () => {
|
|
646
|
+
it('lists scoped actions from Auth', async () => {
|
|
647
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG]);
|
|
648
|
+
const client = new TopoloClient({
|
|
649
|
+
credential: { kind: 'access_token', accessToken: 'tok_abc' },
|
|
650
|
+
agent: AGENT,
|
|
651
|
+
fetch: impl,
|
|
652
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
653
|
+
});
|
|
654
|
+
const actions = await client.listActions();
|
|
655
|
+
expect(actions.map((action) => action.name)).toEqual([
|
|
656
|
+
'messages.list',
|
|
657
|
+
'messages.get',
|
|
658
|
+
'messages.send',
|
|
659
|
+
]);
|
|
660
|
+
expect(calls[0]!.url).toBe('https://auth.example.com/api/actions/catalog');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('calls GET actions by mapping remaining input to query params', async () => {
|
|
664
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG, MAIL_CATALOG, { body: { messages: [] } }]);
|
|
665
|
+
const client = new TopoloClient({
|
|
666
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
667
|
+
agent: AGENT,
|
|
668
|
+
fetch: impl,
|
|
669
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
670
|
+
});
|
|
671
|
+
await client.callAction('messages.list', { mailboxId: 'primary', q: 'Ada', page: 2 });
|
|
672
|
+
const call = requestCalls(calls)[0]!;
|
|
673
|
+
const url = new URL(call.url);
|
|
674
|
+
expect(url.toString()).toBe('https://mail-api.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
|
|
675
|
+
expect(call.init.method).toBe('GET');
|
|
676
|
+
const headers = new Headers(call.init.headers);
|
|
677
|
+
expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
|
|
678
|
+
expect(headers.get('X-Topolo-Action-Name')).toBe('messages.list');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('substitutes action path params and removes them from the query', async () => {
|
|
682
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG, MAIL_CATALOG, { body: { id: 'm1' } }]);
|
|
683
|
+
const client = new TopoloClient({
|
|
684
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
685
|
+
agent: AGENT,
|
|
686
|
+
fetch: impl,
|
|
687
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
688
|
+
});
|
|
689
|
+
await client.callAction('topolo_mail_messages_get', {
|
|
690
|
+
mailboxId: 'primary',
|
|
691
|
+
messageId: 'm 1',
|
|
692
|
+
includeDeleted: false,
|
|
693
|
+
});
|
|
694
|
+
const url = new URL(requestCalls(calls)[0]!.url);
|
|
695
|
+
expect(url.pathname).toBe('/api/mail/mailboxes/primary/messages/m%201');
|
|
696
|
+
expect(url.searchParams.get('includeDeleted')).toBe('false');
|
|
697
|
+
expect(url.searchParams.has('mailboxId')).toBe(false);
|
|
698
|
+
expect(url.searchParams.has('messageId')).toBe(false);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it('requires confirmation for mutating action calls', async () => {
|
|
702
|
+
const { impl } = stubFetch([ACTION_CATALOG]);
|
|
703
|
+
const client = new TopoloClient({
|
|
704
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
705
|
+
agent: AGENT,
|
|
706
|
+
fetch: impl,
|
|
707
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
708
|
+
});
|
|
709
|
+
await expect(
|
|
710
|
+
client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }),
|
|
711
|
+
).rejects.toBeInstanceOf(TopoloAuthError);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('sends remaining input as the body when a mutating action is confirmed', async () => {
|
|
715
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG, MAIL_CATALOG, { body: { id: 'm1' } }]);
|
|
716
|
+
const client = new TopoloClient({
|
|
717
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
718
|
+
agent: AGENT,
|
|
719
|
+
fetch: impl,
|
|
720
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
721
|
+
});
|
|
722
|
+
await client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }, { confirm: true });
|
|
723
|
+
const call = requestCalls(calls)[0]!;
|
|
724
|
+
expect(call.init.method).toBe('POST');
|
|
725
|
+
expect(JSON.parse(String(call.init.body))).toEqual({ subject: 'Hello' });
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
579
729
|
// Silence unused-import warning if the bundler later tree-shakes these
|
|
580
730
|
void TopoloHttpError;
|
package/src/client.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface RequestOptions {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
86
|
+
const TOPOLO_ACTION_ID_HEADER = 'X-Topolo-Action-Id';
|
|
87
|
+
const TOPOLO_ACTION_NAME_HEADER = 'X-Topolo-Action-Name';
|
|
86
88
|
|
|
87
89
|
const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
88
90
|
const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
|
|
@@ -110,6 +112,28 @@ export interface TopoloServiceCatalogEntry {
|
|
|
110
112
|
permissions: string[];
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
export interface TopoloActionCatalogEntry {
|
|
116
|
+
actionId: string;
|
|
117
|
+
name: string;
|
|
118
|
+
toolName: string | null;
|
|
119
|
+
title: string;
|
|
120
|
+
description: string;
|
|
121
|
+
serviceId: string;
|
|
122
|
+
serviceSlug: string | null;
|
|
123
|
+
serviceName: string | null;
|
|
124
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
125
|
+
path: string;
|
|
126
|
+
permissionName: string;
|
|
127
|
+
requiredPermission: string;
|
|
128
|
+
inputSchema: Record<string, unknown>;
|
|
129
|
+
outputSchema: Record<string, unknown>;
|
|
130
|
+
readOnly: boolean;
|
|
131
|
+
destructive: boolean;
|
|
132
|
+
requiresConfirmation: boolean;
|
|
133
|
+
status: string | null;
|
|
134
|
+
sourceRevision: string | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
113
137
|
export class TopoloClient {
|
|
114
138
|
private readonly credential: TopoloCredential;
|
|
115
139
|
private readonly agent: AgentIdentity;
|
|
@@ -120,6 +144,7 @@ export class TopoloClient {
|
|
|
120
144
|
private readonly fetchImpl: typeof fetch;
|
|
121
145
|
private readonly debug?: (event: TopoloDebugEvent) => void;
|
|
122
146
|
private serviceCatalogCache: { entries: TopoloServiceCatalogEntry[]; expiresAt: number } | null = null;
|
|
147
|
+
private actionCatalogCache: { entries: TopoloActionCatalogEntry[]; expiresAt: number } | null = null;
|
|
123
148
|
|
|
124
149
|
/**
|
|
125
150
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
@@ -437,6 +462,83 @@ export class TopoloClient {
|
|
|
437
462
|
return entries;
|
|
438
463
|
}
|
|
439
464
|
|
|
465
|
+
/**
|
|
466
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
467
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
468
|
+
* action list is used as an authorization source.
|
|
469
|
+
*/
|
|
470
|
+
async listActions(options: { service?: string } = {}): Promise<TopoloActionCatalogEntry[]> {
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
473
|
+
return this.actionCatalogCache.entries;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const res = await this.request<
|
|
477
|
+
| { data?: { actions?: unknown[] } }
|
|
478
|
+
| { actions?: unknown[] }
|
|
479
|
+
>({
|
|
480
|
+
service: 'auth',
|
|
481
|
+
method: 'GET',
|
|
482
|
+
path: '/api/actions/catalog',
|
|
483
|
+
...(options.service ? { query: { service: options.service } } : {}),
|
|
484
|
+
});
|
|
485
|
+
const raw =
|
|
486
|
+
(res as { data?: { actions?: unknown[] } }).data?.actions ??
|
|
487
|
+
(res as { actions?: unknown[] }).actions;
|
|
488
|
+
if (!Array.isArray(raw)) {
|
|
489
|
+
throw new TopoloSdkError(
|
|
490
|
+
'action_catalog_unavailable',
|
|
491
|
+
'TopoloAuth did not return an action catalog for the current credential.',
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const entries = raw
|
|
496
|
+
.map((item) => normalizeActionCatalogEntry(item))
|
|
497
|
+
.filter((entry): entry is TopoloActionCatalogEntry => Boolean(entry));
|
|
498
|
+
if (!options.service) {
|
|
499
|
+
this.actionCatalogCache = {
|
|
500
|
+
entries,
|
|
501
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
return entries;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async getAction(action: string): Promise<TopoloActionCatalogEntry> {
|
|
508
|
+
const needle = normalizeActionLookupKey(action);
|
|
509
|
+
const actions = await this.listActions();
|
|
510
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
511
|
+
if (!match) {
|
|
512
|
+
throw new TopoloSdkError(
|
|
513
|
+
'unknown_action',
|
|
514
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return match;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async callAction<T = unknown>(
|
|
521
|
+
action: string,
|
|
522
|
+
input: Record<string, unknown> = {},
|
|
523
|
+
options: { confirm?: boolean } = {},
|
|
524
|
+
): Promise<T> {
|
|
525
|
+
const entry = await this.getAction(action);
|
|
526
|
+
const mapped = mapActionInput(entry, input);
|
|
527
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
528
|
+
return this.request<T>({
|
|
529
|
+
service,
|
|
530
|
+
method: entry.method,
|
|
531
|
+
path: mapped.path,
|
|
532
|
+
...(mapped.query ? { query: mapped.query } : {}),
|
|
533
|
+
...(mapped.body !== undefined ? { body: mapped.body } : {}),
|
|
534
|
+
headers: {
|
|
535
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
536
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name,
|
|
537
|
+
},
|
|
538
|
+
confirm: options.confirm === true,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
440
542
|
/**
|
|
441
543
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
442
544
|
* for deciding what the current credential may use.
|
|
@@ -581,6 +683,119 @@ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
|
|
|
581
683
|
);
|
|
582
684
|
}
|
|
583
685
|
|
|
686
|
+
function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry | null {
|
|
687
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
688
|
+
const a = item as Record<string, unknown>;
|
|
689
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
690
|
+
const name = pickActionString(a.name);
|
|
691
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
692
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
693
|
+
const path = pickActionString(a.path);
|
|
694
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
actionId,
|
|
700
|
+
name,
|
|
701
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
702
|
+
title: pickActionString(a.title) ?? name,
|
|
703
|
+
description: pickActionString(a.description) ?? '',
|
|
704
|
+
serviceId,
|
|
705
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
706
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
707
|
+
method,
|
|
708
|
+
path,
|
|
709
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? '',
|
|
710
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? '',
|
|
711
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
712
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
713
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
714
|
+
destructive: Boolean(a.destructive),
|
|
715
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
716
|
+
status: pickActionString(a.status),
|
|
717
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function isHttpMethod(value: string): value is TopoloActionCatalogEntry['method'] {
|
|
722
|
+
return value === 'GET' || value === 'POST' || value === 'PUT' || value === 'PATCH' || value === 'DELETE';
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function pickActionString(value: unknown): string | null {
|
|
726
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function plainObject(value: unknown): Record<string, unknown> {
|
|
730
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
731
|
+
? value as Record<string, unknown>
|
|
732
|
+
: {};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function normalizeActionLookupKey(value: string): string {
|
|
736
|
+
return String(value || '').trim().toLowerCase();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function actionEntryMatches(entry: TopoloActionCatalogEntry, needle: string): boolean {
|
|
740
|
+
if (!needle) return false;
|
|
741
|
+
return [entry.actionId, entry.name, entry.toolName]
|
|
742
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
743
|
+
.some((value) => value.toLowerCase() === needle);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function mapActionInput(
|
|
747
|
+
action: TopoloActionCatalogEntry,
|
|
748
|
+
input: Record<string, unknown>,
|
|
749
|
+
): { path: string; query?: Record<string, string | number | boolean>; body?: unknown } {
|
|
750
|
+
const remaining: Record<string, unknown> = { ...(input || {}) };
|
|
751
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key: string) => {
|
|
752
|
+
const value = remaining[key];
|
|
753
|
+
if (value === undefined || value === null || value === '') {
|
|
754
|
+
throw new TopoloSdkError(
|
|
755
|
+
'invalid_action_input',
|
|
756
|
+
`Action "${action.name}" requires path parameter "${key}".`,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
delete remaining[key];
|
|
760
|
+
return encodeURIComponent(String(value));
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const explicitQuery = plainObject(remaining.query);
|
|
764
|
+
delete remaining.query;
|
|
765
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, 'body')
|
|
766
|
+
? remaining.body
|
|
767
|
+
: undefined;
|
|
768
|
+
delete remaining.body;
|
|
769
|
+
|
|
770
|
+
if (action.method === 'GET') {
|
|
771
|
+
return {
|
|
772
|
+
path,
|
|
773
|
+
query: objectToQuery({ ...remaining, ...explicitQuery }),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const query = objectToQuery(explicitQuery);
|
|
778
|
+
const body = explicitBody !== undefined ? explicitBody : remaining;
|
|
779
|
+
return {
|
|
780
|
+
path,
|
|
781
|
+
...(query ? { query } : {}),
|
|
782
|
+
body,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function objectToQuery(value: Record<string, unknown>): Record<string, string | number | boolean> | undefined {
|
|
787
|
+
const query: Record<string, string | number | boolean> = {};
|
|
788
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
789
|
+
if (raw === undefined || raw === null) continue;
|
|
790
|
+
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
|
|
791
|
+
query[key] = raw;
|
|
792
|
+
} else {
|
|
793
|
+
query[key] = JSON.stringify(raw);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return Object.keys(query).length > 0 ? query : undefined;
|
|
797
|
+
}
|
|
798
|
+
|
|
584
799
|
interface AuthMePayload {
|
|
585
800
|
credentialType?: 'access_token' | 'api_key';
|
|
586
801
|
user: RawUser;
|
package/src/index.ts
CHANGED
package/src/services.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `scripts/generate-services.mjs`; edit the JSON, not the generated file.
|
|
7
7
|
*
|
|
8
8
|
* Any entry can be overridden per-call via `TopoloClient` options, or globally
|
|
9
|
-
* via env vars like `
|
|
9
|
+
* via env vars like `TOPOLO_SERVICE_URL_MAIL` or `TOPOLO_SERVICE_URL_WEB_API`
|
|
10
10
|
* (dots in service IDs become underscores in env vars).
|
|
11
11
|
*
|
|
12
12
|
* IMPORTANT: The SDK NEVER accepts `orgId` as a parameter. Every request is
|