@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.js CHANGED
@@ -2098,6 +2098,264 @@ function firstString(...values) {
2098
2098
  return null;
2099
2099
  }
2100
2100
 
2101
+ // src/action-schema.ts
2102
+ var SUPPORTED_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
2103
+ "$schema",
2104
+ "$id",
2105
+ "$ref",
2106
+ "$defs",
2107
+ "definitions",
2108
+ "title",
2109
+ "description",
2110
+ "default",
2111
+ "examples",
2112
+ "deprecated",
2113
+ "readOnly",
2114
+ "writeOnly",
2115
+ "type",
2116
+ "nullable",
2117
+ "const",
2118
+ "enum",
2119
+ "allOf",
2120
+ "anyOf",
2121
+ "oneOf",
2122
+ "properties",
2123
+ "required",
2124
+ "additionalProperties",
2125
+ "propertyNames",
2126
+ "minProperties",
2127
+ "maxProperties",
2128
+ "items",
2129
+ "minItems",
2130
+ "maxItems",
2131
+ "uniqueItems",
2132
+ "minLength",
2133
+ "maxLength",
2134
+ "pattern",
2135
+ "format",
2136
+ "minimum",
2137
+ "maximum",
2138
+ "exclusiveMinimum",
2139
+ "exclusiveMaximum",
2140
+ "multipleOf"
2141
+ ]);
2142
+ function validateTopoloActionInput(schema, input) {
2143
+ const schemaIssue = inspectSchema(schema, "$", schema, 0);
2144
+ if (schemaIssue) return { valid: false, errors: [schemaIssue] };
2145
+ const errors = [];
2146
+ validateNode(schema, input, "$", schema, errors, 0);
2147
+ return { valid: errors.length === 0, errors };
2148
+ }
2149
+ function inspectSchema(schema, path, root, depth) {
2150
+ if (depth > 128) return { path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." };
2151
+ const unsupportedKeyword = Object.keys(schema).find((keyword) => !SUPPORTED_SCHEMA_KEYWORDS.has(keyword));
2152
+ if (unsupportedKeyword) {
2153
+ return { path, code: "unsupported_schema_keyword", message: `Action schema uses unsupported keyword ${unsupportedKeyword}.` };
2154
+ }
2155
+ if (typeof schema.format === "string" && !["uri", "email", "date-time"].includes(schema.format)) {
2156
+ return { path, code: "unsupported_schema_format", message: `Action schema uses unsupported format ${schema.format}.` };
2157
+ }
2158
+ if (typeof schema.$ref === "string" && !resolveLocalReference(root, schema.$ref)) {
2159
+ return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
2160
+ }
2161
+ const children = [];
2162
+ if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
2163
+ if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
2164
+ if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
2165
+ children.push(
2166
+ ["additionalProperties", schema.additionalProperties],
2167
+ ["propertyNames", schema.propertyNames],
2168
+ ["items", schema.items],
2169
+ ...arraySchemaEntries("allOf", schema.allOf),
2170
+ ...arraySchemaEntries("anyOf", schema.anyOf),
2171
+ ...arraySchemaEntries("oneOf", schema.oneOf)
2172
+ );
2173
+ for (const [key, child] of children) {
2174
+ if (!isRecord(child)) continue;
2175
+ const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
2176
+ if (issue) return issue;
2177
+ }
2178
+ return null;
2179
+ }
2180
+ function arraySchemaEntries(keyword, value) {
2181
+ return Array.isArray(value) ? value.map((candidate, index) => [`${keyword}[${index}]`, candidate]) : [];
2182
+ }
2183
+ function validateNode(schema, value, path, root, errors, depth) {
2184
+ if (depth > 128) {
2185
+ errors.push({ path, code: "schema_depth", message: "Schema recursion exceeded 128 levels." });
2186
+ return;
2187
+ }
2188
+ if (typeof schema.$ref === "string") {
2189
+ const resolved = resolveLocalReference(root, schema.$ref);
2190
+ if (!resolved) {
2191
+ errors.push({ path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` });
2192
+ return;
2193
+ }
2194
+ validateNode(resolved, value, path, root, errors, depth + 1);
2195
+ }
2196
+ if (Array.isArray(schema.allOf)) {
2197
+ for (const candidate of schema.allOf) {
2198
+ if (isRecord(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
2199
+ }
2200
+ }
2201
+ if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
2202
+ errors.push({ path, code: "any_of", message: "Value does not match any allowed schema." });
2203
+ return;
2204
+ }
2205
+ if (Array.isArray(schema.oneOf)) {
2206
+ const matches = schema.oneOf.filter((candidate) => candidateMatches(candidate, value, root, depth)).length;
2207
+ if (matches !== 1) {
2208
+ errors.push({ path, code: "one_of", message: "Value must match exactly one allowed schema." });
2209
+ return;
2210
+ }
2211
+ }
2212
+ if (value === null && schema.nullable === true) return;
2213
+ if (Object.prototype.hasOwnProperty.call(schema, "const") && !sameJsonValue(value, schema.const)) {
2214
+ errors.push({ path, code: "const", message: `Value must equal ${JSON.stringify(schema.const)}.` });
2215
+ return;
2216
+ }
2217
+ if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => sameJsonValue(value, candidate))) {
2218
+ errors.push({ path, code: "enum", message: `Value must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}.` });
2219
+ return;
2220
+ }
2221
+ if (!matchesType(schema.type, value)) {
2222
+ errors.push({ path, code: "type", message: `Expected ${String(schema.type)}.` });
2223
+ return;
2224
+ }
2225
+ if (typeof value === "string") validateString(schema, value, path, errors);
2226
+ if (typeof value === "number") validateNumber(schema, value, path, errors);
2227
+ if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
2228
+ if (isRecord(value)) validateObject(schema, value, path, root, errors, depth);
2229
+ }
2230
+ function validateObject(schema, value, path, root, errors, depth) {
2231
+ if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
2232
+ errors.push({ path, code: "min_properties", message: `Expected at least ${schema.minProperties} properties.` });
2233
+ }
2234
+ if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
2235
+ errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
2236
+ }
2237
+ const properties = isRecord(schema.properties) ? schema.properties : {};
2238
+ const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
2239
+ for (const key of required) {
2240
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
2241
+ errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
2242
+ }
2243
+ }
2244
+ if (isRecord(schema.propertyNames)) {
2245
+ for (const key of Object.keys(value)) {
2246
+ validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
2247
+ }
2248
+ }
2249
+ for (const [key, child] of Object.entries(value)) {
2250
+ const childSchema = properties[key];
2251
+ if (isRecord(childSchema)) {
2252
+ validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
2253
+ } else if (schema.additionalProperties === false) {
2254
+ errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
2255
+ } else if (isRecord(schema.additionalProperties)) {
2256
+ validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
2257
+ }
2258
+ }
2259
+ }
2260
+ function validateArray(schema, value, path, root, errors, depth) {
2261
+ if (typeof schema.minItems === "number" && value.length < schema.minItems) {
2262
+ errors.push({ path, code: "min_items", message: `Expected at least ${schema.minItems} items.` });
2263
+ }
2264
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems) {
2265
+ errors.push({ path, code: "max_items", message: `Expected at most ${schema.maxItems} items.` });
2266
+ }
2267
+ if (schema.uniqueItems === true) {
2268
+ const unique = new Set(value.map((item) => JSON.stringify(item)));
2269
+ if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
2270
+ }
2271
+ if (isRecord(schema.items)) {
2272
+ value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
2273
+ }
2274
+ }
2275
+ function validateString(schema, value, path, errors) {
2276
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
2277
+ errors.push({ path, code: "min_length", message: `Expected at least ${schema.minLength} characters.` });
2278
+ }
2279
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
2280
+ errors.push({ path, code: "max_length", message: `Expected at most ${schema.maxLength} characters.` });
2281
+ }
2282
+ if (typeof schema.pattern === "string") {
2283
+ try {
2284
+ if (!new RegExp(schema.pattern).test(value)) errors.push({ path, code: "pattern", message: "Value does not match the required pattern." });
2285
+ } catch {
2286
+ errors.push({ path, code: "invalid_schema_pattern", message: "Action schema contains an invalid pattern." });
2287
+ }
2288
+ }
2289
+ if (schema.format === "uri" && !isUrl(value)) errors.push({ path, code: "format_uri", message: "Expected an absolute URL." });
2290
+ if (schema.format === "email" && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) errors.push({ path, code: "format_email", message: "Expected an email address." });
2291
+ if (schema.format === "date-time" && !isDateTime(value)) errors.push({ path, code: "format_date_time", message: "Expected an ISO-8601 date-time." });
2292
+ }
2293
+ function validateNumber(schema, value, path, errors) {
2294
+ if (typeof schema.minimum === "number" && value < schema.minimum) errors.push({ path, code: "minimum", message: `Expected a value of at least ${schema.minimum}.` });
2295
+ if (typeof schema.maximum === "number" && value > schema.maximum) errors.push({ path, code: "maximum", message: `Expected a value of at most ${schema.maximum}.` });
2296
+ const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum === true && typeof schema.minimum === "number" ? schema.minimum : null;
2297
+ const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum === true && typeof schema.maximum === "number" ? schema.maximum : null;
2298
+ if (exclusiveMinimum !== null && value <= exclusiveMinimum) {
2299
+ errors.push({ path, code: "exclusive_minimum", message: `Expected a value greater than ${exclusiveMinimum}.` });
2300
+ }
2301
+ if (exclusiveMaximum !== null && value >= exclusiveMaximum) {
2302
+ errors.push({ path, code: "exclusive_maximum", message: `Expected a value less than ${exclusiveMaximum}.` });
2303
+ }
2304
+ if (typeof schema.multipleOf === "number" && !isMultipleOf(value, schema.multipleOf)) {
2305
+ errors.push({ path, code: "multiple_of", message: `Expected a multiple of ${schema.multipleOf}.` });
2306
+ }
2307
+ }
2308
+ function candidateMatches(candidate, value, root, depth) {
2309
+ if (!isRecord(candidate)) return false;
2310
+ const issues = [];
2311
+ validateNode(candidate, value, "$", root, issues, depth + 1);
2312
+ return issues.length === 0;
2313
+ }
2314
+ function resolveLocalReference(root, reference) {
2315
+ if (!reference.startsWith("#/")) return null;
2316
+ let current = root;
2317
+ for (const token of reference.slice(2).split("/").map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"))) {
2318
+ if (Array.isArray(current)) {
2319
+ if (!/^\d+$/.test(token) || Number(token) >= current.length) return null;
2320
+ current = current[Number(token)];
2321
+ continue;
2322
+ }
2323
+ if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
2324
+ current = current[token];
2325
+ }
2326
+ return isRecord(current) ? current : null;
2327
+ }
2328
+ function matchesType(type, value) {
2329
+ if (type === void 0) return true;
2330
+ if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
2331
+ if (type === "null") return value === null;
2332
+ if (type === "array") return Array.isArray(value);
2333
+ if (type === "object") return isRecord(value);
2334
+ if (type === "integer") return typeof value === "number" && Number.isInteger(value);
2335
+ return typeof value === type;
2336
+ }
2337
+ function sameJsonValue(left, right) {
2338
+ return JSON.stringify(left) === JSON.stringify(right);
2339
+ }
2340
+ function isRecord(value) {
2341
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2342
+ }
2343
+ function isUrl(value) {
2344
+ try {
2345
+ return Boolean(new URL(value).protocol);
2346
+ } catch {
2347
+ return false;
2348
+ }
2349
+ }
2350
+ function isDateTime(value) {
2351
+ 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));
2352
+ }
2353
+ function isMultipleOf(value, divisor) {
2354
+ if (!(divisor > 0)) return false;
2355
+ const quotient = value / divisor;
2356
+ return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2357
+ }
2358
+
2101
2359
  // src/client.ts
2102
2360
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2103
2361
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
@@ -2318,7 +2576,10 @@ var TopoloClient = class _TopoloClient {
2318
2576
  serviceKey,
2319
2577
  overrides: this.serviceUrls
2320
2578
  });
2321
- return baseUrl ? { serviceKey, baseUrl } : null;
2579
+ if (baseUrl) return { serviceKey, baseUrl };
2580
+ const actionPage = await this.searchActions({ service: match.appId, limit: 1 }).catch(() => null);
2581
+ const actionBaseUrl = actionPage?.actions.find((action) => action.apiBaseUrl)?.apiBaseUrl ?? null;
2582
+ return actionBaseUrl ? { serviceKey, baseUrl: requireHttpOrigin(actionBaseUrl) } : null;
2322
2583
  }
2323
2584
  /**
2324
2585
  * Resolve a service-slug → opaque app-id via caller config or TopoloAuth.
@@ -2403,7 +2664,7 @@ var TopoloClient = class _TopoloClient {
2403
2664
  }
2404
2665
  const entries = [];
2405
2666
  for (const item of raw) {
2406
- if (!isRecord(item)) continue;
2667
+ if (!isRecord2(item)) continue;
2407
2668
  const s = item;
2408
2669
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2409
2670
  if (!appId) continue;
@@ -2453,6 +2714,44 @@ var TopoloClient = class _TopoloClient {
2453
2714
  }
2454
2715
  return entry;
2455
2716
  }
2717
+ async listBindableResources(application) {
2718
+ const service = await this.getService(application);
2719
+ const identity = await this.introspect();
2720
+ if (identity.apiKey) {
2721
+ return {
2722
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2723
+ organizationId: identity.organization?.id ?? null,
2724
+ resources: identity.apiKey.resourceBindings.map((binding) => ({
2725
+ resourceType: binding.resourceType,
2726
+ resourceId: binding.resourceId,
2727
+ label: null,
2728
+ slug: null,
2729
+ status: "active",
2730
+ organizationId: identity.organization?.id ?? null,
2731
+ metadata: {}
2732
+ })),
2733
+ resourceTypes: service.apiKeyResources,
2734
+ syncError: null,
2735
+ source: "api_key_binding"
2736
+ };
2737
+ }
2738
+ const response = await this.readAction(
2739
+ "app_topolo_auth.service_api_key_resources.list",
2740
+ { appId: service.appId }
2741
+ );
2742
+ const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2743
+ if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
2744
+ throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2745
+ }
2746
+ return {
2747
+ application: { appId: service.appId, slug: service.slug, name: service.name },
2748
+ organizationId: readOptionalString(payload.organizationId ?? payload.organization_id),
2749
+ resources: payload.resources.map(normalizeBindableResource).filter((resource) => Boolean(resource)),
2750
+ resourceTypes: normalizeApiKeyResourceMetadataList(payload.resourceTypes ?? payload.resource_types),
2751
+ syncError: readOptionalString(payload.syncError ?? payload.sync_error),
2752
+ source: "auth_catalog"
2753
+ };
2754
+ }
2456
2755
  /**
2457
2756
  * List callable actions from the live registry-canonical catalog (definitions
2458
2757
  * from the Catalog registry, scoping from Auth). The response is scoped to the
@@ -2547,10 +2846,44 @@ var TopoloClient = class _TopoloClient {
2547
2846
  const entry = await this.getAction(action);
2548
2847
  return this.executeResolvedAction(entry, input, options);
2549
2848
  }
2849
+ async validateActionInput(action, input = {}) {
2850
+ const entry = await this.getAction(action);
2851
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
2852
+ }
2853
+ async planAction(action, input = {}, options = {}) {
2854
+ const entry = await this.getAction(action);
2855
+ const validation = validateTopoloActionInput(entry.inputSchema, input);
2856
+ const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2857
+ const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
2858
+ let request = null;
2859
+ if (validation.valid) {
2860
+ const mapped = mapActionInput(entry, input);
2861
+ request = {
2862
+ method: entry.method,
2863
+ service: entry.appSlug || entry.appId,
2864
+ apiBaseUrl: entry.apiBaseUrl,
2865
+ path: mapped.path,
2866
+ ...mapped.query ? { query: mapped.query } : {},
2867
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2868
+ ...options.resource ? { resource: options.resource } : {}
2869
+ };
2870
+ }
2871
+ return {
2872
+ action: entry,
2873
+ validation,
2874
+ confirmationRequired,
2875
+ confirmationProvided: options.confirm === true,
2876
+ executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
2877
+ request,
2878
+ verification: stringArray(entry.agentContract?.verify),
2879
+ rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
2880
+ nextActionIds: stringArray(entry.agentContract?.nextActionIds)
2881
+ };
2882
+ }
2550
2883
  async readAction(action, input = {}, options = {}) {
2551
2884
  const entry = await this.getAction(action);
2552
2885
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
2553
- if (entry.method !== "GET" || !entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2886
+ if (!entry.readOnly || entry.destructive || entry.requiresConfirmation || agentAccess !== "auto") {
2554
2887
  throw new TopoloPermissionError(
2555
2888
  `Topolo action "${entry.actionId}" is not an automatic read-only action.`,
2556
2889
  entry.requiredPermission ? [entry.requiredPermission] : []
@@ -2571,6 +2904,13 @@ var TopoloClient = class _TopoloClient {
2571
2904
  `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
2572
2905
  );
2573
2906
  }
2907
+ const validation = validateTopoloActionInput(entry.inputSchema, input);
2908
+ if (!validation.valid) {
2909
+ throw new TopoloSdkError(
2910
+ "invalid_action_input",
2911
+ `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
2912
+ );
2913
+ }
2574
2914
  const mapped = mapActionInput(entry, input);
2575
2915
  const service = entry.appSlug || entry.appId;
2576
2916
  return this.request({
@@ -2585,7 +2925,7 @@ var TopoloClient = class _TopoloClient {
2585
2925
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2586
2926
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
2587
2927
  },
2588
- confirm: options.confirm === true
2928
+ confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2589
2929
  });
2590
2930
  }
2591
2931
  /**
@@ -2642,7 +2982,14 @@ var TopoloClient = class _TopoloClient {
2642
2982
  id: organization.id,
2643
2983
  slug: organization.slug,
2644
2984
  name: organization.name ?? organization.slug
2645
- } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null
2985
+ } : user.orgId && user.orgSlug ? { id: user.orgId, slug: user.orgSlug, name: user.orgSlug } : null,
2986
+ apiKey: kind === "api_key" ? {
2987
+ id: readOptionalString(payload.apiKey?.id),
2988
+ name: readOptionalString(payload.apiKey?.name),
2989
+ appId: readOptionalString(payload.apiKey?.appId),
2990
+ scopes: stringArray(payload.apiKey?.scopes),
2991
+ resourceBindings: normalizeResourceBindings(payload.apiKey?.resourceBindings)
2992
+ } : null
2646
2993
  };
2647
2994
  }
2648
2995
  };
@@ -2657,7 +3004,7 @@ function resolveConfiguredAppId(slug, appIds) {
2657
3004
  return null;
2658
3005
  }
2659
3006
  function normalizeApiKeyResourceMetadata(input) {
2660
- if (!isRecord(input)) return null;
3007
+ if (!isRecord2(input)) return null;
2661
3008
  const resourceType = readString(input.resourceType ?? input.resource_type);
2662
3009
  const label = readString(input.label);
2663
3010
  const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
@@ -2690,10 +3037,38 @@ function normalizeApiKeyResourceMetadataList(input) {
2690
3037
  }
2691
3038
  return resources;
2692
3039
  }
3040
+ function normalizeBindableResource(input) {
3041
+ if (!isRecord2(input)) return null;
3042
+ const resourceType = readString(input.resourceType ?? input.resource_type);
3043
+ const resourceId = readString(input.resourceId ?? input.resource_id);
3044
+ if (!resourceType || !resourceId) return null;
3045
+ return {
3046
+ resourceType,
3047
+ resourceId,
3048
+ label: readOptionalString(input.label),
3049
+ slug: readOptionalString(input.slug),
3050
+ status: readOptionalString(input.status),
3051
+ organizationId: readOptionalString(input.organizationId ?? input.organization_id),
3052
+ metadata: isRecord2(input.metadata) ? input.metadata : {}
3053
+ };
3054
+ }
3055
+ function normalizeResourceBindings(input) {
3056
+ if (!Array.isArray(input)) return [];
3057
+ return input.flatMap((binding) => {
3058
+ if (!isRecord2(binding)) return [];
3059
+ const resourceType = readString(binding.resourceType ?? binding.resource_type);
3060
+ const resourceId = readString(binding.resourceId ?? binding.resource_id);
3061
+ return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
3062
+ });
3063
+ }
2693
3064
  function readString(value) {
2694
3065
  return typeof value === "string" ? value.trim() : "";
2695
3066
  }
2696
- function isRecord(value) {
3067
+ function readOptionalString(value) {
3068
+ const normalized = readString(value);
3069
+ return normalized || null;
3070
+ }
3071
+ function isRecord2(value) {
2697
3072
  return typeof value === "object" && value !== null && !Array.isArray(value);
2698
3073
  }
2699
3074
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -2765,14 +3140,14 @@ function catalogSearchQuery(options) {
2765
3140
  return query;
2766
3141
  }
2767
3142
  function normalizeCatalogPage(value) {
2768
- if (!isRecord(value)) return { nextCursor: null, hasMore: false };
3143
+ if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
2769
3144
  return {
2770
3145
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2771
3146
  hasMore: value.hasMore === true
2772
3147
  };
2773
3148
  }
2774
3149
  function normalizeServiceCatalogEntry(item) {
2775
- if (!isRecord(item)) return null;
3150
+ if (!isRecord2(item)) return null;
2776
3151
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2777
3152
  if (!appId) return null;
2778
3153
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -2827,6 +3202,7 @@ function normalizeActionCatalogEntry(item) {
2827
3202
  authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
2828
3203
  inputSchema: plainObject(a.input_schema ?? a.inputSchema),
2829
3204
  outputSchema: plainObject(a.output_schema ?? a.outputSchema),
3205
+ agentContract: plainObject(a.agent_contract ?? a.agentContract),
2830
3206
  readOnly: Boolean(a.read_only ?? a.readOnly),
2831
3207
  destructive: Boolean(a.destructive),
2832
3208
  requiresConfirmation: Boolean(a.requires_confirmation ?? a.requiresConfirmation),
@@ -2850,6 +3226,9 @@ function pickAuthorizationMode(value) {
2850
3226
  function plainObject(value) {
2851
3227
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2852
3228
  }
3229
+ function stringArray(value) {
3230
+ return Array.isArray(value) ? value.map((item) => String(item || "").trim()).filter(Boolean) : [];
3231
+ }
2853
3232
  function mapActionInput(action, input) {
2854
3233
  const remaining = { ...input || {} };
2855
3234
  const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
@@ -3368,7 +3747,7 @@ function createTopoloWidgetError(error) {
3368
3747
  }
3369
3748
  function validateTopoloWidgetResponse(response) {
3370
3749
  const errors = [];
3371
- if (!isRecord2(response)) {
3750
+ if (!isRecord3(response)) {
3372
3751
  return { ok: false, errors: ["response must be an object"] };
3373
3752
  }
3374
3753
  if (typeof response.success !== "boolean") {
@@ -3387,7 +3766,7 @@ function validateTopoloWidgetResponse(response) {
3387
3766
  return { ok: errors.length === 0, errors };
3388
3767
  }
3389
3768
  function validateWidget(widget, path, errors) {
3390
- if (!isRecord2(widget)) {
3769
+ if (!isRecord3(widget)) {
3391
3770
  errors.push(`${path} must be an object`);
3392
3771
  return;
3393
3772
  }
@@ -3397,16 +3776,16 @@ function validateWidget(widget, path, errors) {
3397
3776
  validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
3398
3777
  switch (widget.type) {
3399
3778
  case "stats":
3400
- validateArray(widget.stats, `${path}.stats`, errors);
3779
+ validateArray2(widget.stats, `${path}.stats`, errors);
3401
3780
  break;
3402
3781
  case "recent-activity":
3403
- validateArray(widget.activities, `${path}.activities`, errors);
3782
+ validateArray2(widget.activities, `${path}.activities`, errors);
3404
3783
  break;
3405
3784
  case "quick-actions":
3406
- validateArray(widget.actions, `${path}.actions`, errors);
3785
+ validateArray2(widget.actions, `${path}.actions`, errors);
3407
3786
  break;
3408
3787
  case "notifications":
3409
- validateArray(widget.notifications, `${path}.notifications`, errors);
3788
+ validateArray2(widget.notifications, `${path}.notifications`, errors);
3410
3789
  if (typeof widget.unreadCount !== "number") {
3411
3790
  errors.push(`${path}.unreadCount must be a number`);
3412
3791
  }
@@ -3414,11 +3793,11 @@ function validateWidget(widget, path, errors) {
3414
3793
  case "chart":
3415
3794
  validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
3416
3795
  validateNonEmptyString(widget.title, `${path}.title`, errors);
3417
- if (!isRecord2(widget.data)) {
3796
+ if (!isRecord3(widget.data)) {
3418
3797
  errors.push(`${path}.data must be an object`);
3419
3798
  } else {
3420
- validateArray(widget.data.labels, `${path}.data.labels`, errors);
3421
- validateArray(widget.data.values, `${path}.data.values`, errors);
3799
+ validateArray2(widget.data.labels, `${path}.data.labels`, errors);
3800
+ validateArray2(widget.data.values, `${path}.data.values`, errors);
3422
3801
  }
3423
3802
  break;
3424
3803
  default:
@@ -3430,7 +3809,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
3430
3809
  errors.push(`${path} must be an object`);
3431
3810
  return;
3432
3811
  }
3433
- if (!isRecord2(snapshot)) {
3812
+ if (!isRecord3(snapshot)) {
3434
3813
  errors.push(`${path} must be an object`);
3435
3814
  return;
3436
3815
  }
@@ -3450,7 +3829,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
3450
3829
  }
3451
3830
  snapshot.availableMetrics.forEach((metric, index) => {
3452
3831
  const metricPath = `${path}.availableMetrics[${index}]`;
3453
- if (!isRecord2(metric)) {
3832
+ if (!isRecord3(metric)) {
3454
3833
  errors.push(`${metricPath} must be an object`);
3455
3834
  return;
3456
3835
  }
@@ -3466,12 +3845,12 @@ function validateNonEmptyString(value, path, errors) {
3466
3845
  errors.push(`${path} must be a non-empty string`);
3467
3846
  }
3468
3847
  }
3469
- function validateArray(value, path, errors) {
3848
+ function validateArray2(value, path, errors) {
3470
3849
  if (!Array.isArray(value)) {
3471
3850
  errors.push(`${path} must be an array`);
3472
3851
  }
3473
3852
  }
3474
- function isRecord2(value) {
3853
+ function isRecord3(value) {
3475
3854
  return typeof value === "object" && value !== null && !Array.isArray(value);
3476
3855
  }
3477
3856
  function toIsoTimestamp(input) {
@@ -3523,5 +3902,6 @@ export {
3523
3902
  requirementsForApplication,
3524
3903
  resolveCatalogServiceUrl,
3525
3904
  resolveServiceUrl,
3905
+ validateTopoloActionInput,
3526
3906
  validateTopoloWidgetResponse
3527
3907
  };
@@ -31,6 +31,7 @@ __export(credential_store_exports, {
31
31
  normalizeEnvironment: () => normalizeEnvironment,
32
32
  profileForEnvironment: () => profileForEnvironment,
33
33
  readConfig: () => readConfig,
34
+ refreshContextCredentialIfNeeded: () => refreshContextCredentialIfNeeded,
34
35
  refreshCredentialIfNeeded: () => refreshCredentialIfNeeded,
35
36
  resolveAgentName: () => resolveAgentName,
36
37
  resolveCredential: () => resolveCredential,
@@ -428,6 +429,46 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
428
429
  return cred;
429
430
  }
430
431
  }
432
+ async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
433
+ const config = await readConfig();
434
+ const profile = profileForEnvironment(config, environment);
435
+ const context = profile.contexts?.[key];
436
+ const credential = context?.credential;
437
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
438
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
439
+ if (!credential.refreshToken || !credential.expiresAt || !oauthClientId) return credential;
440
+ const now = Math.floor(Date.now() / 1e3);
441
+ if (!options.force && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
442
+ try {
443
+ const oauth = new TopoloOAuth({
444
+ serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
445
+ });
446
+ const tokens = await oauth.refreshToken({
447
+ clientId: oauthClientId,
448
+ refreshToken: credential.refreshToken
449
+ });
450
+ const refreshed = {
451
+ kind: "access_token",
452
+ accessToken: tokens.access_token,
453
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
454
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
455
+ };
456
+ await writeConfig(writeProfileConfig(config, environment, {
457
+ ...profile.activeContext === key ? { credential: refreshed } : {},
458
+ contexts: {
459
+ ...profile.contexts ?? {},
460
+ [key]: {
461
+ ...context,
462
+ credential: refreshed,
463
+ oauthClientId
464
+ }
465
+ }
466
+ }));
467
+ return refreshed;
468
+ } catch {
469
+ return credential;
470
+ }
471
+ }
431
472
  function resolveAgentName(configAgentName) {
432
473
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
433
474
  }
@@ -471,6 +512,7 @@ function resolvedProfileConfig(config, environment = selectedEnvironment()) {
471
512
  normalizeEnvironment,
472
513
  profileForEnvironment,
473
514
  readConfig,
515
+ refreshContextCredentialIfNeeded,
474
516
  refreshCredentialIfNeeded,
475
517
  resolveAgentName,
476
518
  resolveCredential,
@@ -103,8 +103,12 @@ declare function resolveCredential(environment?: TopoloEnvironment): Promise<Top
103
103
  declare function refreshCredentialIfNeeded(environment?: TopoloEnvironment, options?: {
104
104
  force?: boolean;
105
105
  }): Promise<TopoloCredential | null>;
106
+ /** Refresh one stored context without changing which context is active. */
107
+ declare function refreshContextCredentialIfNeeded(environment: TopoloEnvironment, key: string, options?: {
108
+ force?: boolean;
109
+ }): Promise<TopoloCredential | null>;
106
110
  declare function resolveAgentName(configAgentName?: string): string | undefined;
107
111
  declare function resolveServiceUrls(configUrls?: Record<string, string>, environment?: TopoloEnvironment): Record<string, string> | undefined;
108
112
  declare function resolvedProfileConfig(config: TopoloConfig, environment?: TopoloEnvironment): TopoloEnvironmentProfile;
109
113
 
110
- export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, writeConfig, writeContextCredential, writeProfileConfig };
114
+ export { ENVIRONMENT_DEFAULT_SERVICE_URLS, TOPOLO_ENVIRONMENTS, type TopoloConfig, type TopoloContextCredential, type TopoloEnvironment, type TopoloEnvironmentProfile, activateContext, clearProfileCredential, configPath, contextKey, credentialSourceFromConfig, envVarPrefix, normalizeEnvironment, profileForEnvironment, readConfig, refreshContextCredentialIfNeeded, refreshCredentialIfNeeded, resolveAgentName, resolveCredential, resolveServiceUrls, resolvedProfileConfig, selectedEnvironment, writeConfig, writeContextCredential, writeProfileConfig };