@topolo/sdk 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +403 -22
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +402 -22
- package/dist/node/credential-store.cjs +42 -0
- package/dist/node/credential-store.d.cts +5 -1
- package/dist/node/credential-store.d.ts +5 -1
- package/dist/node/credential-store.js +41 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -52,6 +52,7 @@ __export(index_exports, {
|
|
|
52
52
|
requirementsForApplication: () => requirementsForApplication,
|
|
53
53
|
resolveCatalogServiceUrl: () => resolveCatalogServiceUrl,
|
|
54
54
|
resolveServiceUrl: () => resolveServiceUrl,
|
|
55
|
+
validateTopoloActionInput: () => validateTopoloActionInput,
|
|
55
56
|
validateTopoloWidgetResponse: () => validateTopoloWidgetResponse
|
|
56
57
|
});
|
|
57
58
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2156,6 +2157,264 @@ function firstString(...values) {
|
|
|
2156
2157
|
return null;
|
|
2157
2158
|
}
|
|
2158
2159
|
|
|
2160
|
+
// src/action-schema.ts
|
|
2161
|
+
var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2162
|
+
"$schema",
|
|
2163
|
+
"$id",
|
|
2164
|
+
"$ref",
|
|
2165
|
+
"$defs",
|
|
2166
|
+
"definitions",
|
|
2167
|
+
"title",
|
|
2168
|
+
"description",
|
|
2169
|
+
"default",
|
|
2170
|
+
"examples",
|
|
2171
|
+
"deprecated",
|
|
2172
|
+
"readOnly",
|
|
2173
|
+
"writeOnly",
|
|
2174
|
+
"type",
|
|
2175
|
+
"nullable",
|
|
2176
|
+
"const",
|
|
2177
|
+
"enum",
|
|
2178
|
+
"allOf",
|
|
2179
|
+
"anyOf",
|
|
2180
|
+
"oneOf",
|
|
2181
|
+
"properties",
|
|
2182
|
+
"required",
|
|
2183
|
+
"additionalProperties",
|
|
2184
|
+
"propertyNames",
|
|
2185
|
+
"minProperties",
|
|
2186
|
+
"maxProperties",
|
|
2187
|
+
"items",
|
|
2188
|
+
"minItems",
|
|
2189
|
+
"maxItems",
|
|
2190
|
+
"uniqueItems",
|
|
2191
|
+
"minLength",
|
|
2192
|
+
"maxLength",
|
|
2193
|
+
"pattern",
|
|
2194
|
+
"format",
|
|
2195
|
+
"minimum",
|
|
2196
|
+
"maximum",
|
|
2197
|
+
"exclusiveMinimum",
|
|
2198
|
+
"exclusiveMaximum",
|
|
2199
|
+
"multipleOf"
|
|
2200
|
+
]);
|
|
2201
|
+
function validateTopoloActionInput(schema, input) {
|
|
2202
|
+
const schemaIssue = inspectSchema(schema, "$", schema, 0);
|
|
2203
|
+
if (schemaIssue) return { valid: false, errors: [schemaIssue] };
|
|
2204
|
+
const errors = [];
|
|
2205
|
+
validateNode(schema, input, "$", schema, errors, 0);
|
|
2206
|
+
return { valid: errors.length === 0, errors };
|
|
2207
|
+
}
|
|
2208
|
+
function inspectSchema(schema, path, root, depth) {
|
|
2209
|
+
if (depth > 128) return { path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
|
|
2210
|
+
const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
|
|
2211
|
+
if (unsupportedKeyword) {
|
|
2212
|
+
return { path, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
|
|
2213
|
+
}
|
|
2214
|
+
if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
|
|
2215
|
+
return { path, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
|
|
2216
|
+
}
|
|
2217
|
+
if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
|
|
2218
|
+
return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
2219
|
+
}
|
|
2220
|
+
const children = [];
|
|
2221
|
+
if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
2222
|
+
if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
2223
|
+
if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
2224
|
+
children.push(
|
|
2225
|
+
["additionalProperties", schema.additionalProperties],
|
|
2226
|
+
["propertyNames", schema.propertyNames],
|
|
2227
|
+
["items", schema.items],
|
|
2228
|
+
...arraySchemaEntries("allOf", schema.allOf),
|
|
2229
|
+
...arraySchemaEntries("anyOf", schema.anyOf),
|
|
2230
|
+
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
2231
|
+
);
|
|
2232
|
+
for (const [key, child] of children) {
|
|
2233
|
+
if (!isRecord(child)) continue;
|
|
2234
|
+
const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
|
|
2235
|
+
if (issue) return issue;
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
function arraySchemaEntries(keyword, value) {
|
|
2240
|
+
return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
|
|
2241
|
+
}
|
|
2242
|
+
function validateNode(schema, value, path, root, errors, depth) {
|
|
2243
|
+
if (depth > 128) {
|
|
2244
|
+
errors.push({ path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (typeof schema.$ref === "string") {
|
|
2248
|
+
const resolved = resolveLocalReference(root, schema.$ref);
|
|
2249
|
+
if (!resolved) {
|
|
2250
|
+
errors.push({ path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
validateNode(resolved, value, path, root, errors, depth + 1);
|
|
2254
|
+
}
|
|
2255
|
+
if (Array.isArray(schema.allOf)) {
|
|
2256
|
+
for (const candidate of schema.allOf) {
|
|
2257
|
+
if (isRecord(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
2261
|
+
errors.push({ path, code: "any_of", message: "Value does not match any allowed schema." });
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (Array.isArray(schema.oneOf)) {
|
|
2265
|
+
const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
|
|
2266
|
+
if (matches !== 1) {
|
|
2267
|
+
errors.push({ path, code: "one_of", message: "Value must match exactly one allowed schema." });
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
if (value === null && schema.nullable === true) return;
|
|
2272
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
|
|
2273
|
+
errors.push({ path, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
2276
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
|
|
2277
|
+
errors.push({ path, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (!matchesType(schema.type, value)) {
|
|
2281
|
+
errors.push({ path, code: "type", message: `Expected ${String(schema.type)}.` });
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (typeof value === "string") validateString(schema, value, path, errors);
|
|
2285
|
+
if (typeof value === "number") validateNumber(schema, value, path, errors);
|
|
2286
|
+
if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
|
|
2287
|
+
if (isRecord(value)) validateObject(schema, value, path, root, errors, depth);
|
|
2288
|
+
}
|
|
2289
|
+
function validateObject(schema, value, path, root, errors, depth) {
|
|
2290
|
+
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
2291
|
+
errors.push({ path, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
|
|
2292
|
+
}
|
|
2293
|
+
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
2294
|
+
errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
2295
|
+
}
|
|
2296
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
2297
|
+
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
2298
|
+
for (const key of required) {
|
|
2299
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
2300
|
+
errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (isRecord(schema.propertyNames)) {
|
|
2304
|
+
for (const key of Object.keys(value)) {
|
|
2305
|
+
validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2309
|
+
const childSchema = properties[key];
|
|
2310
|
+
if (isRecord(childSchema)) {
|
|
2311
|
+
validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
|
|
2312
|
+
} else if (schema.additionalProperties === false) {
|
|
2313
|
+
errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
2314
|
+
} else if (isRecord(schema.additionalProperties)) {
|
|
2315
|
+
validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
function validateArray(schema, value, path, root, errors, depth) {
|
|
2320
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
2321
|
+
errors.push({ path, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
|
|
2322
|
+
}
|
|
2323
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
|
|
2324
|
+
errors.push({ path, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
|
|
2325
|
+
}
|
|
2326
|
+
if (schema.uniqueItems === true) {
|
|
2327
|
+
const unique = new Set(value.map((item) => JSON.stringify(item)));
|
|
2328
|
+
if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
|
|
2329
|
+
}
|
|
2330
|
+
if (isRecord(schema.items)) {
|
|
2331
|
+
value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function validateString(schema, value, path, errors) {
|
|
2335
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
2336
|
+
errors.push({ path, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
|
|
2337
|
+
}
|
|
2338
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
|
|
2339
|
+
errors.push({ path, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
|
|
2340
|
+
}
|
|
2341
|
+
if (typeof schema.pattern === "string") {
|
|
2342
|
+
try {
|
|
2343
|
+
if (!new RegExp(schema.pattern).test(value)) errors.push({ path, code: "pattern", message: "Value does not match the required pattern." });
|
|
2344
|
+
} catch {
|
|
2345
|
+
errors.push({ path, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (schema.format === "uri" && !isUrl(value)) errors.push({ path, code: "format_uri", message: "Expected an absolute URL." });
|
|
2349
|
+
if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path, code: "format_email", message: "Expected an email address." });
|
|
2350
|
+
if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
|
|
2351
|
+
}
|
|
2352
|
+
function validateNumber(schema, value, path, errors) {
|
|
2353
|
+
if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
|
|
2354
|
+
if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
|
|
2355
|
+
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
|
|
2356
|
+
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
|
|
2357
|
+
if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
|
|
2358
|
+
errors.push({ path, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
|
|
2359
|
+
}
|
|
2360
|
+
if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
|
|
2361
|
+
errors.push({ path, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
|
|
2362
|
+
}
|
|
2363
|
+
if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
|
|
2364
|
+
errors.push({ path, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
function candidateMatches(candidate, value, root, depth) {
|
|
2368
|
+
if (!isRecord(candidate)) return false;
|
|
2369
|
+
const issues = [];
|
|
2370
|
+
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
2371
|
+
return issues.length === 0;
|
|
2372
|
+
}
|
|
2373
|
+
function resolveLocalReference(root, reference) {
|
|
2374
|
+
if (!reference.startsWith("#/")) return null;
|
|
2375
|
+
let current = root;
|
|
2376
|
+
for (const token of reference.slice(2).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
|
|
2377
|
+
if (Array.isArray(current)) {
|
|
2378
|
+
if (!/^\d+$/.test(token) || Number(token) >= current.length) return null;
|
|
2379
|
+
current = current[Number(token)];
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
2383
|
+
current = current[token];
|
|
2384
|
+
}
|
|
2385
|
+
return isRecord(current) ? current : null;
|
|
2386
|
+
}
|
|
2387
|
+
function matchesType(type, value) {
|
|
2388
|
+
if (type === void 0) return true;
|
|
2389
|
+
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
2390
|
+
if (type === "null") return value === null;
|
|
2391
|
+
if (type === "array") return Array.isArray(value);
|
|
2392
|
+
if (type === "object") return isRecord(value);
|
|
2393
|
+
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
2394
|
+
return typeof value === type;
|
|
2395
|
+
}
|
|
2396
|
+
function sameJsonValue(left, right) {
|
|
2397
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
2398
|
+
}
|
|
2399
|
+
function isRecord(value) {
|
|
2400
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2401
|
+
}
|
|
2402
|
+
function isUrl(value) {
|
|
2403
|
+
try {
|
|
2404
|
+
return Boolean(new URL(value).protocol);
|
|
2405
|
+
} catch {
|
|
2406
|
+
return false;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
function isDateTime(value) {
|
|
2410
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && !Number.isNaN(Date.parse(value));
|
|
2411
|
+
}
|
|
2412
|
+
function isMultipleOf(value, divisor) {
|
|
2413
|
+
if (!(divisor > 0)) return false;
|
|
2414
|
+
const quotient = value / divisor;
|
|
2415
|
+
return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2159
2418
|
// src/client.ts
|
|
2160
2419
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2161
2420
|
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
@@ -2376,7 +2635,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2376
2635
|
serviceKey,
|
|
2377
2636
|
overrides: this.serviceUrls
|
|
2378
2637
|
});
|
|
2379
|
-
|
|
2638
|
+
if (baseUrl) return { serviceKey, baseUrl };
|
|
2639
|
+
const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
|
|
2640
|
+
const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
|
|
2641
|
+
return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
|
|
2380
2642
|
}
|
|
2381
2643
|
/**
|
|
2382
2644
|
* Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
|
|
@@ -2461,7 +2723,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2461
2723
|
}
|
|
2462
2724
|
const entries = [];
|
|
2463
2725
|
for (const item of raw) {
|
|
2464
|
-
if (!
|
|
2726
|
+
if (!isRecord2(item)) continue;
|
|
2465
2727
|
const s = item;
|
|
2466
2728
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2467
2729
|
if (!appId) continue;
|
|
@@ -2511,6 +2773,44 @@ var TopoloClient = class _TopoloClient {
|
|
|
2511
2773
|
}
|
|
2512
2774
|
return entry;
|
|
2513
2775
|
}
|
|
2776
|
+
async listBindableResources(application) {
|
|
2777
|
+
const service = await this.getService(application);
|
|
2778
|
+
const identity = await this.introspect();
|
|
2779
|
+
if (identity.apiKey) {
|
|
2780
|
+
return {
|
|
2781
|
+
application: { appId: service.appId, slug: service.slug, name: service.name },
|
|
2782
|
+
organizationId: identity.organization?.id ?? null,
|
|
2783
|
+
resources: identity.apiKey.resourceBindings.map((binding) => ({
|
|
2784
|
+
resourceType: binding.resourceType,
|
|
2785
|
+
resourceId: binding.resourceId,
|
|
2786
|
+
label: null,
|
|
2787
|
+
slug: null,
|
|
2788
|
+
status: "active",
|
|
2789
|
+
organizationId: identity.organization?.id ?? null,
|
|
2790
|
+
metadata: {}
|
|
2791
|
+
})),
|
|
2792
|
+
resourceTypes: service.apiKeyResources,
|
|
2793
|
+
syncError: null,
|
|
2794
|
+
source: "api_key_binding"
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
const response = await this.readAction(
|
|
2798
|
+
"app_topolo_auth.service_api_key_resources.list",
|
|
2799
|
+
{ appId: service.appId }
|
|
2800
|
+
);
|
|
2801
|
+
const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
|
|
2802
|
+
if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
|
|
2803
|
+
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
2804
|
+
}
|
|
2805
|
+
return {
|
|
2806
|
+
application: { appId: service.appId, slug: service.slug, name: service.name },
|
|
2807
|
+
organizationId: readOptionalString(payload.organizationId ?? payload.organization_id),
|
|
2808
|
+
resources: payload.resources.map(normalizeBindableResource).filter((resource) => Boolean(resource)),
|
|
2809
|
+
resourceTypes: normalizeApiKeyResourceMetadataList(payload.resourceTypes ?? payload.resource_types),
|
|
2810
|
+
syncError: readOptionalString(payload.syncError ?? payload.sync_error),
|
|
2811
|
+
source: "auth_catalog"
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2514
2814
|
/**
|
|
2515
2815
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
2516
2816
|
* from the Catalog registry, scoping from Auth). The response is scoped to the
|
|
@@ -2605,10 +2905,44 @@ var TopoloClient = class _TopoloClient {
|
|
|
2605
2905
|
const entry = await this.getAction(action);
|
|
2606
2906
|
return this.executeResolvedAction(entry, input, options);
|
|
2607
2907
|
}
|
|
2908
|
+
async validateActionInput(action, input = {}) {
|
|
2909
|
+
const entry = await this.getAction(action);
|
|
2910
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
|
|
2911
|
+
}
|
|
2912
|
+
async planAction(action, input = {}, options = {}) {
|
|
2913
|
+
const entry = await this.getAction(action);
|
|
2914
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
2915
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2916
|
+
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
2917
|
+
let request = null;
|
|
2918
|
+
if (validation.valid) {
|
|
2919
|
+
const mapped = mapActionInput(entry, input);
|
|
2920
|
+
request = {
|
|
2921
|
+
method: entry.method,
|
|
2922
|
+
service: entry.appSlug || entry.appId,
|
|
2923
|
+
apiBaseUrl: entry.apiBaseUrl,
|
|
2924
|
+
path: mapped.path,
|
|
2925
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2926
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2927
|
+
...options.resource ? { resource: options.resource } : {}
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
return {
|
|
2931
|
+
action: entry,
|
|
2932
|
+
validation,
|
|
2933
|
+
confirmationRequired,
|
|
2934
|
+
confirmationProvided: options.confirm === true,
|
|
2935
|
+
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
2936
|
+
request,
|
|
2937
|
+
verification: stringArray(entry.agentContract?.verify),
|
|
2938
|
+
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
2939
|
+
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2608
2942
|
async readAction(action, input = {}, options = {}) {
|
|
2609
2943
|
const entry = await this.getAction(action);
|
|
2610
2944
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2611
|
-
if (
|
|
2945
|
+
if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
|
|
2612
2946
|
throw new TopoloPermissionError(
|
|
2613
2947
|
`Topolo action "${entry.actionId}" is not an automatic read-only action.`,
|
|
2614
2948
|
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
@@ -2629,6 +2963,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2629
2963
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2630
2964
|
);
|
|
2631
2965
|
}
|
|
2966
|
+
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
2967
|
+
if (!validation.valid) {
|
|
2968
|
+
throw new TopoloSdkError(
|
|
2969
|
+
"invalid_action_input",
|
|
2970
|
+
`Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2632
2973
|
const mapped = mapActionInput(entry, input);
|
|
2633
2974
|
const service = entry.appSlug || entry.appId;
|
|
2634
2975
|
return this.request({
|
|
@@ -2643,7 +2984,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2643
2984
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2644
2985
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2645
2986
|
},
|
|
2646
|
-
confirm: options.confirm === true
|
|
2987
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2647
2988
|
});
|
|
2648
2989
|
}
|
|
2649
2990
|
/**
|
|
@@ -2700,7 +3041,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
2700
3041
|
id: organization.id,
|
|
2701
3042
|
slug: organization.slug,
|
|
2702
3043
|
name: organization.name ?? organization.slug
|
|
2703
|
-
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
|
|
3044
|
+
} : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null,
|
|
3045
|
+
apiKey: kind === "api_key" ? {
|
|
3046
|
+
id: readOptionalString(payload.apiKey?.id),
|
|
3047
|
+
name: readOptionalString(payload.apiKey?.name),
|
|
3048
|
+
appId: readOptionalString(payload.apiKey?.appId),
|
|
3049
|
+
scopes: stringArray(payload.apiKey?.scopes),
|
|
3050
|
+
resourceBindings: normalizeResourceBindings(payload.apiKey?.resourceBindings)
|
|
3051
|
+
} : null
|
|
2704
3052
|
};
|
|
2705
3053
|
}
|
|
2706
3054
|
};
|
|
@@ -2715,7 +3063,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2715
3063
|
return null;
|
|
2716
3064
|
}
|
|
2717
3065
|
function normalizeApiKeyResourceMetadata(input) {
|
|
2718
|
-
if (!
|
|
3066
|
+
if (!isRecord2(input)) return null;
|
|
2719
3067
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2720
3068
|
const label = readString(input.label);
|
|
2721
3069
|
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
@@ -2748,10 +3096,38 @@ function normalizeApiKeyResourceMetadataList(input) {
|
|
|
2748
3096
|
}
|
|
2749
3097
|
return resources;
|
|
2750
3098
|
}
|
|
3099
|
+
function normalizeBindableResource(input) {
|
|
3100
|
+
if (!isRecord2(input)) return null;
|
|
3101
|
+
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
3102
|
+
const resourceId = readString(input.resourceId ?? input.resource_id);
|
|
3103
|
+
if (!resourceType || !resourceId) return null;
|
|
3104
|
+
return {
|
|
3105
|
+
resourceType,
|
|
3106
|
+
resourceId,
|
|
3107
|
+
label: readOptionalString(input.label),
|
|
3108
|
+
slug: readOptionalString(input.slug),
|
|
3109
|
+
status: readOptionalString(input.status),
|
|
3110
|
+
organizationId: readOptionalString(input.organizationId ?? input.organization_id),
|
|
3111
|
+
metadata: isRecord2(input.metadata) ? input.metadata : {}
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
function normalizeResourceBindings(input) {
|
|
3115
|
+
if (!Array.isArray(input)) return [];
|
|
3116
|
+
return input.flatMap((binding) => {
|
|
3117
|
+
if (!isRecord2(binding)) return [];
|
|
3118
|
+
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
3119
|
+
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
3120
|
+
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
2751
3123
|
function readString(value) {
|
|
2752
3124
|
return typeof value === "string" ? value.trim() : "";
|
|
2753
3125
|
}
|
|
2754
|
-
function
|
|
3126
|
+
function readOptionalString(value) {
|
|
3127
|
+
const normalized = readString(value);
|
|
3128
|
+
return normalized || null;
|
|
3129
|
+
}
|
|
3130
|
+
function isRecord2(value) {
|
|
2755
3131
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2756
3132
|
}
|
|
2757
3133
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
@@ -2823,14 +3199,14 @@ function catalogSearchQuery(options) {
|
|
|
2823
3199
|
return query;
|
|
2824
3200
|
}
|
|
2825
3201
|
function normalizeCatalogPage(value) {
|
|
2826
|
-
if (!
|
|
3202
|
+
if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
|
|
2827
3203
|
return {
|
|
2828
3204
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2829
3205
|
hasMore: value.hasMore === true
|
|
2830
3206
|
};
|
|
2831
3207
|
}
|
|
2832
3208
|
function normalizeServiceCatalogEntry(item) {
|
|
2833
|
-
if (!
|
|
3209
|
+
if (!isRecord2(item)) return null;
|
|
2834
3210
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2835
3211
|
if (!appId) return null;
|
|
2836
3212
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -2885,6 +3261,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2885
3261
|
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2886
3262
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2887
3263
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
3264
|
+
agentContract: plainObject(a.agent_contract ?? a.agentContract),
|
|
2888
3265
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
2889
3266
|
destructive: Boolean(a.destructive),
|
|
2890
3267
|
requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
|
|
@@ -2908,6 +3285,9 @@ function pickAuthorizationMode(value) {
|
|
|
2908
3285
|
function plainObject(value) {
|
|
2909
3286
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2910
3287
|
}
|
|
3288
|
+
function stringArray(value) {
|
|
3289
|
+
return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
|
|
3290
|
+
}
|
|
2911
3291
|
function mapActionInput(action, input) {
|
|
2912
3292
|
const remaining = { ...input || {} };
|
|
2913
3293
|
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -3426,7 +3806,7 @@ function createTopoloWidgetError(error) {
|
|
|
3426
3806
|
}
|
|
3427
3807
|
function validateTopoloWidgetResponse(response) {
|
|
3428
3808
|
const errors = [];
|
|
3429
|
-
if (!
|
|
3809
|
+
if (!isRecord3(response)) {
|
|
3430
3810
|
return { ok: false, errors: ["response must be an object"] };
|
|
3431
3811
|
}
|
|
3432
3812
|
if (typeof response.success !== "boolean") {
|
|
@@ -3445,7 +3825,7 @@ function validateTopoloWidgetResponse(response) {
|
|
|
3445
3825
|
return { ok: errors.length === 0, errors };
|
|
3446
3826
|
}
|
|
3447
3827
|
function validateWidget(widget, path, errors) {
|
|
3448
|
-
if (!
|
|
3828
|
+
if (!isRecord3(widget)) {
|
|
3449
3829
|
errors.push(`${path} must be an object`);
|
|
3450
3830
|
return;
|
|
3451
3831
|
}
|
|
@@ -3455,16 +3835,16 @@ function validateWidget(widget, path, errors) {
|
|
|
3455
3835
|
validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
|
|
3456
3836
|
switch (widget.type) {
|
|
3457
3837
|
case "stats":
|
|
3458
|
-
|
|
3838
|
+
validateArray2(widget.stats, `${path}.stats`, errors);
|
|
3459
3839
|
break;
|
|
3460
3840
|
case "recent-activity":
|
|
3461
|
-
|
|
3841
|
+
validateArray2(widget.activities, `${path}.activities`, errors);
|
|
3462
3842
|
break;
|
|
3463
3843
|
case "quick-actions":
|
|
3464
|
-
|
|
3844
|
+
validateArray2(widget.actions, `${path}.actions`, errors);
|
|
3465
3845
|
break;
|
|
3466
3846
|
case "notifications":
|
|
3467
|
-
|
|
3847
|
+
validateArray2(widget.notifications, `${path}.notifications`, errors);
|
|
3468
3848
|
if (typeof widget.unreadCount !== "number") {
|
|
3469
3849
|
errors.push(`${path}.unreadCount must be a number`);
|
|
3470
3850
|
}
|
|
@@ -3472,11 +3852,11 @@ function validateWidget(widget, path, errors) {
|
|
|
3472
3852
|
case "chart":
|
|
3473
3853
|
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
3474
3854
|
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
3475
|
-
if (!
|
|
3855
|
+
if (!isRecord3(widget.data)) {
|
|
3476
3856
|
errors.push(`${path}.data must be an object`);
|
|
3477
3857
|
} else {
|
|
3478
|
-
|
|
3479
|
-
|
|
3858
|
+
validateArray2(widget.data.labels, `${path}.data.labels`, errors);
|
|
3859
|
+
validateArray2(widget.data.values, `${path}.data.values`, errors);
|
|
3480
3860
|
}
|
|
3481
3861
|
break;
|
|
3482
3862
|
default:
|
|
@@ -3488,7 +3868,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
|
|
|
3488
3868
|
errors.push(`${path} must be an object`);
|
|
3489
3869
|
return;
|
|
3490
3870
|
}
|
|
3491
|
-
if (!
|
|
3871
|
+
if (!isRecord3(snapshot)) {
|
|
3492
3872
|
errors.push(`${path} must be an object`);
|
|
3493
3873
|
return;
|
|
3494
3874
|
}
|
|
@@ -3508,7 +3888,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
|
|
|
3508
3888
|
}
|
|
3509
3889
|
snapshot.availableMetrics.forEach((metric, index) => {
|
|
3510
3890
|
const metricPath = `${path}.availableMetrics[${index}]`;
|
|
3511
|
-
if (!
|
|
3891
|
+
if (!isRecord3(metric)) {
|
|
3512
3892
|
errors.push(`${metricPath} must be an object`);
|
|
3513
3893
|
return;
|
|
3514
3894
|
}
|
|
@@ -3524,12 +3904,12 @@ function validateNonEmptyString(value, path, errors) {
|
|
|
3524
3904
|
errors.push(`${path} must be a non-empty string`);
|
|
3525
3905
|
}
|
|
3526
3906
|
}
|
|
3527
|
-
function
|
|
3907
|
+
function validateArray2(value, path, errors) {
|
|
3528
3908
|
if (!Array.isArray(value)) {
|
|
3529
3909
|
errors.push(`${path} must be an array`);
|
|
3530
3910
|
}
|
|
3531
3911
|
}
|
|
3532
|
-
function
|
|
3912
|
+
function isRecord3(value) {
|
|
3533
3913
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3534
3914
|
}
|
|
3535
3915
|
function toIsoTimestamp(input) {
|
|
@@ -3582,5 +3962,6 @@ function createTopolo(options) {
|
|
|
3582
3962
|
requirementsForApplication,
|
|
3583
3963
|
resolveCatalogServiceUrl,
|
|
3584
3964
|
resolveServiceUrl,
|
|
3965
|
+
validateTopoloActionInput,
|
|
3585
3966
|
validateTopoloWidgetResponse
|
|
3586
3967
|
});
|