@topolo/sdk 0.4.2 → 0.4.3
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 +150 -0
- package/dist/index.d.cts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +150 -0
- package/package.json +1 -1
- package/src/client.test.ts +169 -22
- package/src/client.ts +209 -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
|
@@ -2123,6 +2123,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2123
2123
|
fetchImpl;
|
|
2124
2124
|
debug;
|
|
2125
2125
|
serviceCatalogCache = null;
|
|
2126
|
+
actionCatalogCache = null;
|
|
2126
2127
|
/**
|
|
2127
2128
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2128
2129
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2383,6 +2384,63 @@ var TopoloClient = class _TopoloClient {
|
|
|
2383
2384
|
};
|
|
2384
2385
|
return entries;
|
|
2385
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2389
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2390
|
+
* action list is used as an authorization source.
|
|
2391
|
+
*/
|
|
2392
|
+
async listActions(options = {}) {
|
|
2393
|
+
const now = Date.now();
|
|
2394
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2395
|
+
return this.actionCatalogCache.entries;
|
|
2396
|
+
}
|
|
2397
|
+
const res = await this.request({
|
|
2398
|
+
service: "auth",
|
|
2399
|
+
method: "GET",
|
|
2400
|
+
path: "/api/actions/catalog",
|
|
2401
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2402
|
+
});
|
|
2403
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2404
|
+
if (!Array.isArray(raw)) {
|
|
2405
|
+
throw new TopoloSdkError(
|
|
2406
|
+
"action_catalog_unavailable",
|
|
2407
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2411
|
+
if (!options.service) {
|
|
2412
|
+
this.actionCatalogCache = {
|
|
2413
|
+
entries,
|
|
2414
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
return entries;
|
|
2418
|
+
}
|
|
2419
|
+
async getAction(action) {
|
|
2420
|
+
const needle = normalizeActionLookupKey(action);
|
|
2421
|
+
const actions = await this.listActions();
|
|
2422
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2423
|
+
if (!match) {
|
|
2424
|
+
throw new TopoloSdkError(
|
|
2425
|
+
"unknown_action",
|
|
2426
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
return match;
|
|
2430
|
+
}
|
|
2431
|
+
async callAction(action, input = {}, options = {}) {
|
|
2432
|
+
const entry = await this.getAction(action);
|
|
2433
|
+
const mapped = mapActionInput(entry, input);
|
|
2434
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2435
|
+
return this.request({
|
|
2436
|
+
service,
|
|
2437
|
+
method: entry.method,
|
|
2438
|
+
path: mapped.path,
|
|
2439
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2440
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2441
|
+
confirm: options.confirm === true
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2386
2444
|
/**
|
|
2387
2445
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2388
2446
|
* for deciding what the current credential may use.
|
|
@@ -2492,6 +2550,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2492
2550
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2493
2551
|
);
|
|
2494
2552
|
}
|
|
2553
|
+
function normalizeActionCatalogEntry(item) {
|
|
2554
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2555
|
+
const a = item;
|
|
2556
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2557
|
+
const name = pickActionString(a.name);
|
|
2558
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2559
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2560
|
+
const path = pickActionString(a.path);
|
|
2561
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
actionId,
|
|
2566
|
+
name,
|
|
2567
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2568
|
+
title: pickActionString(a.title) ?? name,
|
|
2569
|
+
description: pickActionString(a.description) ?? "",
|
|
2570
|
+
serviceId,
|
|
2571
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2572
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2573
|
+
method,
|
|
2574
|
+
path,
|
|
2575
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2576
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2577
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2578
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2579
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2580
|
+
destructive: Boolean(a.destructive),
|
|
2581
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2582
|
+
status: pickActionString(a.status),
|
|
2583
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
function isHttpMethod(value) {
|
|
2587
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2588
|
+
}
|
|
2589
|
+
function pickActionString(value) {
|
|
2590
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2591
|
+
}
|
|
2592
|
+
function plainObject(value) {
|
|
2593
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2594
|
+
}
|
|
2595
|
+
function normalizeActionLookupKey(value) {
|
|
2596
|
+
return String(value || "").trim().toLowerCase();
|
|
2597
|
+
}
|
|
2598
|
+
function actionEntryMatches(entry, needle) {
|
|
2599
|
+
if (!needle) return false;
|
|
2600
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2601
|
+
}
|
|
2602
|
+
function mapActionInput(action, input) {
|
|
2603
|
+
const remaining = { ...input || {} };
|
|
2604
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2605
|
+
const value = remaining[key];
|
|
2606
|
+
if (value === void 0 || value === null || value === "") {
|
|
2607
|
+
throw new TopoloSdkError(
|
|
2608
|
+
"invalid_action_input",
|
|
2609
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
delete remaining[key];
|
|
2613
|
+
return encodeURIComponent(String(value));
|
|
2614
|
+
});
|
|
2615
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2616
|
+
delete remaining.query;
|
|
2617
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2618
|
+
delete remaining.body;
|
|
2619
|
+
if (action.method === "GET") {
|
|
2620
|
+
return {
|
|
2621
|
+
path,
|
|
2622
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
const query = objectToQuery(explicitQuery);
|
|
2626
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2627
|
+
return {
|
|
2628
|
+
path,
|
|
2629
|
+
...query ? { query } : {},
|
|
2630
|
+
body
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
function objectToQuery(value) {
|
|
2634
|
+
const query = {};
|
|
2635
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2636
|
+
if (raw === void 0 || raw === null) continue;
|
|
2637
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2638
|
+
query[key] = raw;
|
|
2639
|
+
} else {
|
|
2640
|
+
query[key] = JSON.stringify(raw);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2644
|
+
}
|
|
2495
2645
|
function describeError(body, fallback) {
|
|
2496
2646
|
if (body && typeof body === "object") {
|
|
2497
2647
|
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
|
@@ -2069,6 +2069,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2069
2069
|
fetchImpl;
|
|
2070
2070
|
debug;
|
|
2071
2071
|
serviceCatalogCache = null;
|
|
2072
|
+
actionCatalogCache = null;
|
|
2072
2073
|
/**
|
|
2073
2074
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
2074
2075
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2329,6 +2330,63 @@ var TopoloClient = class _TopoloClient {
|
|
|
2329
2330
|
};
|
|
2330
2331
|
return entries;
|
|
2331
2332
|
}
|
|
2333
|
+
/**
|
|
2334
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
2335
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
2336
|
+
* action list is used as an authorization source.
|
|
2337
|
+
*/
|
|
2338
|
+
async listActions(options = {}) {
|
|
2339
|
+
const now = Date.now();
|
|
2340
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
2341
|
+
return this.actionCatalogCache.entries;
|
|
2342
|
+
}
|
|
2343
|
+
const res = await this.request({
|
|
2344
|
+
service: "auth",
|
|
2345
|
+
method: "GET",
|
|
2346
|
+
path: "/api/actions/catalog",
|
|
2347
|
+
...options.service ? { query: { service: options.service } } : {}
|
|
2348
|
+
});
|
|
2349
|
+
const raw = res.data?.actions ?? res.actions;
|
|
2350
|
+
if (!Array.isArray(raw)) {
|
|
2351
|
+
throw new TopoloSdkError(
|
|
2352
|
+
"action_catalog_unavailable",
|
|
2353
|
+
"TopoloAuth did not return an action catalog for the current credential."
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2357
|
+
if (!options.service) {
|
|
2358
|
+
this.actionCatalogCache = {
|
|
2359
|
+
entries,
|
|
2360
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
return entries;
|
|
2364
|
+
}
|
|
2365
|
+
async getAction(action) {
|
|
2366
|
+
const needle = normalizeActionLookupKey(action);
|
|
2367
|
+
const actions = await this.listActions();
|
|
2368
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2369
|
+
if (!match) {
|
|
2370
|
+
throw new TopoloSdkError(
|
|
2371
|
+
"unknown_action",
|
|
2372
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
return match;
|
|
2376
|
+
}
|
|
2377
|
+
async callAction(action, input = {}, options = {}) {
|
|
2378
|
+
const entry = await this.getAction(action);
|
|
2379
|
+
const mapped = mapActionInput(entry, input);
|
|
2380
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
2381
|
+
return this.request({
|
|
2382
|
+
service,
|
|
2383
|
+
method: entry.method,
|
|
2384
|
+
path: mapped.path,
|
|
2385
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2386
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2387
|
+
confirm: options.confirm === true
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2332
2390
|
/**
|
|
2333
2391
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
2334
2392
|
* for deciding what the current credential may use.
|
|
@@ -2438,6 +2496,98 @@ function isCallableCatalogEntry(entry) {
|
|
|
2438
2496
|
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2439
2497
|
);
|
|
2440
2498
|
}
|
|
2499
|
+
function normalizeActionCatalogEntry(item) {
|
|
2500
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2501
|
+
const a = item;
|
|
2502
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
2503
|
+
const name = pickActionString(a.name);
|
|
2504
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
2505
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
2506
|
+
const path = pickActionString(a.path);
|
|
2507
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
2508
|
+
return null;
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
actionId,
|
|
2512
|
+
name,
|
|
2513
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2514
|
+
title: pickActionString(a.title) ?? name,
|
|
2515
|
+
description: pickActionString(a.description) ?? "",
|
|
2516
|
+
serviceId,
|
|
2517
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
2518
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
2519
|
+
method,
|
|
2520
|
+
path,
|
|
2521
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2522
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2523
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2524
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2525
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2526
|
+
destructive: Boolean(a.destructive),
|
|
2527
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
2528
|
+
status: pickActionString(a.status),
|
|
2529
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision)
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function isHttpMethod(value) {
|
|
2533
|
+
return value === "GET" || value === "POST" || value === "PUT" || value === "PATCH" || value === "DELETE";
|
|
2534
|
+
}
|
|
2535
|
+
function pickActionString(value) {
|
|
2536
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
2537
|
+
}
|
|
2538
|
+
function plainObject(value) {
|
|
2539
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2540
|
+
}
|
|
2541
|
+
function normalizeActionLookupKey(value) {
|
|
2542
|
+
return String(value || "").trim().toLowerCase();
|
|
2543
|
+
}
|
|
2544
|
+
function actionEntryMatches(entry, needle) {
|
|
2545
|
+
if (!needle) return false;
|
|
2546
|
+
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2547
|
+
}
|
|
2548
|
+
function mapActionInput(action, input) {
|
|
2549
|
+
const remaining = { ...input || {} };
|
|
2550
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
2551
|
+
const value = remaining[key];
|
|
2552
|
+
if (value === void 0 || value === null || value === "") {
|
|
2553
|
+
throw new TopoloSdkError(
|
|
2554
|
+
"invalid_action_input",
|
|
2555
|
+
`Action "${action.name}" requires path parameter "${key}".`
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
delete remaining[key];
|
|
2559
|
+
return encodeURIComponent(String(value));
|
|
2560
|
+
});
|
|
2561
|
+
const explicitQuery = plainObject(remaining.query);
|
|
2562
|
+
delete remaining.query;
|
|
2563
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, "body") ? remaining.body : void 0;
|
|
2564
|
+
delete remaining.body;
|
|
2565
|
+
if (action.method === "GET") {
|
|
2566
|
+
return {
|
|
2567
|
+
path,
|
|
2568
|
+
query: objectToQuery({ ...remaining, ...explicitQuery })
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
const query = objectToQuery(explicitQuery);
|
|
2572
|
+
const body = explicitBody !== void 0 ? explicitBody : remaining;
|
|
2573
|
+
return {
|
|
2574
|
+
path,
|
|
2575
|
+
...query ? { query } : {},
|
|
2576
|
+
body
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
function objectToQuery(value) {
|
|
2580
|
+
const query = {};
|
|
2581
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
2582
|
+
if (raw === void 0 || raw === null) continue;
|
|
2583
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
|
2584
|
+
query[key] = raw;
|
|
2585
|
+
} else {
|
|
2586
|
+
query[key] = JSON.stringify(raw);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
return Object.keys(query).length > 0 ? query : void 0;
|
|
2590
|
+
}
|
|
2441
2591
|
function describeError(body, fallback) {
|
|
2442
2592
|
if (body && typeof body === "object") {
|
|
2443
2593
|
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,86 @@ 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
|
+
});
|
|
677
|
+
|
|
678
|
+
it('substitutes action path params and removes them from the query', async () => {
|
|
679
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG, MAIL_CATALOG, { body: { id: 'm1' } }]);
|
|
680
|
+
const client = new TopoloClient({
|
|
681
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
682
|
+
agent: AGENT,
|
|
683
|
+
fetch: impl,
|
|
684
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
685
|
+
});
|
|
686
|
+
await client.callAction('topolo_mail_messages_get', {
|
|
687
|
+
mailboxId: 'primary',
|
|
688
|
+
messageId: 'm 1',
|
|
689
|
+
includeDeleted: false,
|
|
690
|
+
});
|
|
691
|
+
const url = new URL(requestCalls(calls)[0]!.url);
|
|
692
|
+
expect(url.pathname).toBe('/api/mail/mailboxes/primary/messages/m%201');
|
|
693
|
+
expect(url.searchParams.get('includeDeleted')).toBe('false');
|
|
694
|
+
expect(url.searchParams.has('mailboxId')).toBe(false);
|
|
695
|
+
expect(url.searchParams.has('messageId')).toBe(false);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('requires confirmation for mutating action calls', async () => {
|
|
699
|
+
const { impl } = stubFetch([ACTION_CATALOG]);
|
|
700
|
+
const client = new TopoloClient({
|
|
701
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
702
|
+
agent: AGENT,
|
|
703
|
+
fetch: impl,
|
|
704
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
705
|
+
});
|
|
706
|
+
await expect(
|
|
707
|
+
client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }),
|
|
708
|
+
).rejects.toBeInstanceOf(TopoloAuthError);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('sends remaining input as the body when a mutating action is confirmed', async () => {
|
|
712
|
+
const { impl, calls } = stubFetch([ACTION_CATALOG, MAIL_CATALOG, { body: { id: 'm1' } }]);
|
|
713
|
+
const client = new TopoloClient({
|
|
714
|
+
credential: { kind: 'api_key', apiKey: 'dak_xyz' },
|
|
715
|
+
agent: AGENT,
|
|
716
|
+
fetch: impl,
|
|
717
|
+
serviceUrls: { auth: 'https://auth.example.com' },
|
|
718
|
+
});
|
|
719
|
+
await client.callAction('messages.send', { mailboxId: 'primary', subject: 'Hello' }, { confirm: true });
|
|
720
|
+
const call = requestCalls(calls)[0]!;
|
|
721
|
+
expect(call.init.method).toBe('POST');
|
|
722
|
+
expect(JSON.parse(String(call.init.body))).toEqual({ subject: 'Hello' });
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
579
726
|
// Silence unused-import warning if the bundler later tree-shakes these
|
|
580
727
|
void TopoloHttpError;
|
package/src/client.ts
CHANGED
|
@@ -110,6 +110,28 @@ export interface TopoloServiceCatalogEntry {
|
|
|
110
110
|
permissions: string[];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export 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
|
+
}
|
|
134
|
+
|
|
113
135
|
export class TopoloClient {
|
|
114
136
|
private readonly credential: TopoloCredential;
|
|
115
137
|
private readonly agent: AgentIdentity;
|
|
@@ -120,6 +142,7 @@ export class TopoloClient {
|
|
|
120
142
|
private readonly fetchImpl: typeof fetch;
|
|
121
143
|
private readonly debug?: (event: TopoloDebugEvent) => void;
|
|
122
144
|
private serviceCatalogCache: { entries: TopoloServiceCatalogEntry[]; expiresAt: number } | null = null;
|
|
145
|
+
private actionCatalogCache: { entries: TopoloActionCatalogEntry[]; expiresAt: number } | null = null;
|
|
123
146
|
|
|
124
147
|
/**
|
|
125
148
|
* Process-wide cache for slug → service-id lookups. Keyed by
|
|
@@ -437,6 +460,79 @@ export class TopoloClient {
|
|
|
437
460
|
return entries;
|
|
438
461
|
}
|
|
439
462
|
|
|
463
|
+
/**
|
|
464
|
+
* List callable actions from the live Auth catalog. The response is scoped to
|
|
465
|
+
* the caller's organization and effective permissions; no bundled/static app
|
|
466
|
+
* action list is used as an authorization source.
|
|
467
|
+
*/
|
|
468
|
+
async listActions(options: { service?: string } = {}): Promise<TopoloActionCatalogEntry[]> {
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
if (!options.service && this.actionCatalogCache && this.actionCatalogCache.expiresAt > now) {
|
|
471
|
+
return this.actionCatalogCache.entries;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const res = await this.request<
|
|
475
|
+
| { data?: { actions?: unknown[] } }
|
|
476
|
+
| { actions?: unknown[] }
|
|
477
|
+
>({
|
|
478
|
+
service: 'auth',
|
|
479
|
+
method: 'GET',
|
|
480
|
+
path: '/api/actions/catalog',
|
|
481
|
+
...(options.service ? { query: { service: options.service } } : {}),
|
|
482
|
+
});
|
|
483
|
+
const raw =
|
|
484
|
+
(res as { data?: { actions?: unknown[] } }).data?.actions ??
|
|
485
|
+
(res as { actions?: unknown[] }).actions;
|
|
486
|
+
if (!Array.isArray(raw)) {
|
|
487
|
+
throw new TopoloSdkError(
|
|
488
|
+
'action_catalog_unavailable',
|
|
489
|
+
'TopoloAuth did not return an action catalog for the current credential.',
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const entries = raw
|
|
494
|
+
.map((item) => normalizeActionCatalogEntry(item))
|
|
495
|
+
.filter((entry): entry is TopoloActionCatalogEntry => Boolean(entry));
|
|
496
|
+
if (!options.service) {
|
|
497
|
+
this.actionCatalogCache = {
|
|
498
|
+
entries,
|
|
499
|
+
expiresAt: now + CATALOG_CACHE_TTL_MS,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return entries;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async getAction(action: string): Promise<TopoloActionCatalogEntry> {
|
|
506
|
+
const needle = normalizeActionLookupKey(action);
|
|
507
|
+
const actions = await this.listActions();
|
|
508
|
+
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
509
|
+
if (!match) {
|
|
510
|
+
throw new TopoloSdkError(
|
|
511
|
+
'unknown_action',
|
|
512
|
+
`Unknown Topolo action "${action}". Run action discovery first and pass an action id, name, or tool name.`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return match;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async callAction<T = unknown>(
|
|
519
|
+
action: string,
|
|
520
|
+
input: Record<string, unknown> = {},
|
|
521
|
+
options: { confirm?: boolean } = {},
|
|
522
|
+
): Promise<T> {
|
|
523
|
+
const entry = await this.getAction(action);
|
|
524
|
+
const mapped = mapActionInput(entry, input);
|
|
525
|
+
const service = entry.serviceSlug || entry.serviceId;
|
|
526
|
+
return this.request<T>({
|
|
527
|
+
service,
|
|
528
|
+
method: entry.method,
|
|
529
|
+
path: mapped.path,
|
|
530
|
+
...(mapped.query ? { query: mapped.query } : {}),
|
|
531
|
+
...(mapped.body !== undefined ? { body: mapped.body } : {}),
|
|
532
|
+
confirm: options.confirm === true,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
440
536
|
/**
|
|
441
537
|
* Bundled generated registry for tooling that audits platform packaging, not
|
|
442
538
|
* for deciding what the current credential may use.
|
|
@@ -581,6 +677,119 @@ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
|
|
|
581
677
|
);
|
|
582
678
|
}
|
|
583
679
|
|
|
680
|
+
function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry | null {
|
|
681
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
682
|
+
const a = item as Record<string, unknown>;
|
|
683
|
+
const actionId = pickActionString(a.action_id) ?? pickActionString(a.actionId);
|
|
684
|
+
const name = pickActionString(a.name);
|
|
685
|
+
const serviceId = pickActionString(a.service_id) ?? pickActionString(a.serviceId);
|
|
686
|
+
const method = pickActionString(a.method)?.toUpperCase();
|
|
687
|
+
const path = pickActionString(a.path);
|
|
688
|
+
if (!actionId || !name || !serviceId || !method || !path || !isHttpMethod(method)) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
actionId,
|
|
694
|
+
name,
|
|
695
|
+
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
696
|
+
title: pickActionString(a.title) ?? name,
|
|
697
|
+
description: pickActionString(a.description) ?? '',
|
|
698
|
+
serviceId,
|
|
699
|
+
serviceSlug: pickActionString(a.service_slug) ?? pickActionString(a.serviceSlug) ?? null,
|
|
700
|
+
serviceName: pickActionString(a.service_name) ?? pickActionString(a.serviceName) ?? null,
|
|
701
|
+
method,
|
|
702
|
+
path,
|
|
703
|
+
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? '',
|
|
704
|
+
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? '',
|
|
705
|
+
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
706
|
+
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
707
|
+
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
708
|
+
destructive: Boolean(a.destructive),
|
|
709
|
+
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
710
|
+
status: pickActionString(a.status),
|
|
711
|
+
sourceRevision: pickActionString(a.source_revision) ?? pickActionString(a.sourceRevision),
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isHttpMethod(value: string): value is TopoloActionCatalogEntry['method'] {
|
|
716
|
+
return value === 'GET' || value === 'POST' || value === 'PUT' || value === 'PATCH' || value === 'DELETE';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function pickActionString(value: unknown): string | null {
|
|
720
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function plainObject(value: unknown): Record<string, unknown> {
|
|
724
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
725
|
+
? value as Record<string, unknown>
|
|
726
|
+
: {};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function normalizeActionLookupKey(value: string): string {
|
|
730
|
+
return String(value || '').trim().toLowerCase();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function actionEntryMatches(entry: TopoloActionCatalogEntry, needle: string): boolean {
|
|
734
|
+
if (!needle) return false;
|
|
735
|
+
return [entry.actionId, entry.name, entry.toolName]
|
|
736
|
+
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
737
|
+
.some((value) => value.toLowerCase() === needle);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function mapActionInput(
|
|
741
|
+
action: TopoloActionCatalogEntry,
|
|
742
|
+
input: Record<string, unknown>,
|
|
743
|
+
): { path: string; query?: Record<string, string | number | boolean>; body?: unknown } {
|
|
744
|
+
const remaining: Record<string, unknown> = { ...(input || {}) };
|
|
745
|
+
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key: string) => {
|
|
746
|
+
const value = remaining[key];
|
|
747
|
+
if (value === undefined || value === null || value === '') {
|
|
748
|
+
throw new TopoloSdkError(
|
|
749
|
+
'invalid_action_input',
|
|
750
|
+
`Action "${action.name}" requires path parameter "${key}".`,
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
delete remaining[key];
|
|
754
|
+
return encodeURIComponent(String(value));
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const explicitQuery = plainObject(remaining.query);
|
|
758
|
+
delete remaining.query;
|
|
759
|
+
const explicitBody = Object.prototype.hasOwnProperty.call(remaining, 'body')
|
|
760
|
+
? remaining.body
|
|
761
|
+
: undefined;
|
|
762
|
+
delete remaining.body;
|
|
763
|
+
|
|
764
|
+
if (action.method === 'GET') {
|
|
765
|
+
return {
|
|
766
|
+
path,
|
|
767
|
+
query: objectToQuery({ ...remaining, ...explicitQuery }),
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const query = objectToQuery(explicitQuery);
|
|
772
|
+
const body = explicitBody !== undefined ? explicitBody : remaining;
|
|
773
|
+
return {
|
|
774
|
+
path,
|
|
775
|
+
...(query ? { query } : {}),
|
|
776
|
+
body,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function objectToQuery(value: Record<string, unknown>): Record<string, string | number | boolean> | undefined {
|
|
781
|
+
const query: Record<string, string | number | boolean> = {};
|
|
782
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
783
|
+
if (raw === undefined || raw === null) continue;
|
|
784
|
+
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
|
|
785
|
+
query[key] = raw;
|
|
786
|
+
} else {
|
|
787
|
+
query[key] = JSON.stringify(raw);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return Object.keys(query).length > 0 ? query : undefined;
|
|
791
|
+
}
|
|
792
|
+
|
|
584
793
|
interface AuthMePayload {
|
|
585
794
|
credentialType?: 'access_token' | 'api_key';
|
|
586
795
|
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
|