@topolo/sdk 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- return baseUrl ? { serviceKey, baseUrl } : null;
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 (!isRecord(item)) continue;
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 (entry.method !== "GET" || !entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
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 (!isRecord(input)) return null;
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 isRecord(value) {
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 (!isRecord(value)) return { nextCursor: null, hasMore: false };
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 (!isRecord(item)) return null;
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 (!isRecord2(response)) {
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 (!isRecord2(widget)) {
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
- validateArray(widget.stats, `${path}.stats`, errors);
3838
+ validateArray2(widget.stats, `${path}.stats`, errors);
3459
3839
  break;
3460
3840
  case "recent-activity":
3461
- validateArray(widget.activities, `${path}.activities`, errors);
3841
+ validateArray2(widget.activities, `${path}.activities`, errors);
3462
3842
  break;
3463
3843
  case "quick-actions":
3464
- validateArray(widget.actions, `${path}.actions`, errors);
3844
+ validateArray2(widget.actions, `${path}.actions`, errors);
3465
3845
  break;
3466
3846
  case "notifications":
3467
- validateArray(widget.notifications, `${path}.notifications`, errors);
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 (!isRecord2(widget.data)) {
3855
+ if (!isRecord3(widget.data)) {
3476
3856
  errors.push(`${path}.data must be an object`);
3477
3857
  } else {
3478
- validateArray(widget.data.labels, `${path}.data.labels`, errors);
3479
- validateArray(widget.data.values, `${path}.data.values`, errors);
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 (!isRecord2(snapshot)) {
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 (!isRecord2(metric)) {
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 validateArray(value, path, errors) {
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 isRecord2(value) {
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
  });