@topolo/sdk 0.9.7 → 0.9.8

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
@@ -51,6 +51,9 @@ __export(index_exports, {
51
51
  defineTopoloStatsWidget: () => defineTopoloStatsWidget,
52
52
  digestTopoloActionCatalog: () => digestTopoloActionCatalog,
53
53
  enrichActionContract: () => enrichActionContract,
54
+ parseTopoloActionUploadContract: () => parseTopoloActionUploadContract,
55
+ prepareTopoloActionBase64UploadInput: () => prepareTopoloActionBase64UploadInput,
56
+ prepareTopoloActionUploadInput: () => prepareTopoloActionUploadInput,
54
57
  requirementsForApplication: () => requirementsForApplication,
55
58
  resolveCatalogServiceUrl: () => resolveCatalogServiceUrl,
56
59
  resolveServiceUrl: () => resolveServiceUrl,
@@ -89,19 +92,54 @@ var TopoloSdkError = class extends Error {
89
92
  }
90
93
  };
91
94
  var TopoloAuthError = class extends TopoloSdkError {
92
- constructor(message, code = "auth_error") {
95
+ status;
96
+ body;
97
+ service;
98
+ path;
99
+ requestId;
100
+ retryAfter;
101
+ constructor(message, code = "auth_error", options = {}) {
93
102
  super(code, message);
94
103
  this.name = "TopoloAuthError";
104
+ this.status = options.status ?? 401;
105
+ this.body = options.body;
106
+ this.service = options.service;
107
+ this.path = options.path;
108
+ this.requestId = options.requestId;
109
+ this.retryAfter = options.retryAfter;
95
110
  }
96
111
  };
97
112
  var TopoloPermissionError = class extends TopoloSdkError {
98
113
  required;
99
- constructor(message, required) {
114
+ status;
115
+ body;
116
+ service;
117
+ path;
118
+ requestId;
119
+ retryAfter;
120
+ constructor(message, required, options = {}) {
100
121
  super("permission_denied", message);
101
122
  this.name = "TopoloPermissionError";
102
123
  this.required = required;
124
+ this.status = options.status ?? 403;
125
+ this.body = options.body;
126
+ this.service = options.service;
127
+ this.path = options.path;
128
+ this.requestId = options.requestId;
129
+ this.retryAfter = options.retryAfter;
103
130
  }
104
131
  };
132
+ function extractHttpErrorCode(body) {
133
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
134
+ const record = body;
135
+ if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
136
+ const nested = record.error;
137
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
138
+ const code = nested.code;
139
+ if (typeof code === "string" && code.trim()) return code.trim();
140
+ }
141
+ return "http_error";
142
+ }
105
143
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
106
144
  constructor(message) {
107
145
  super("confirmation_required", message);
@@ -113,13 +151,17 @@ var TopoloHttpError = class extends TopoloSdkError {
113
151
  body;
114
152
  service;
115
153
  path;
116
- constructor(service, path, status, body, message) {
117
- super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
154
+ requestId;
155
+ retryAfter;
156
+ constructor(service, path, status, body, message, options = {}) {
157
+ super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
118
158
  this.name = "TopoloHttpError";
119
159
  this.status = status;
120
160
  this.body = body;
121
161
  this.service = service;
122
162
  this.path = path;
163
+ this.requestId = options.requestId;
164
+ this.retryAfter = options.retryAfter;
123
165
  }
124
166
  };
125
167
 
@@ -333,7 +375,9 @@ var APPLICATIONS = {
333
375
  "domains": [
334
376
  "brand.topolo.app"
335
377
  ],
336
- "routes": [],
378
+ "routes": [
379
+ "brand.topolo.app"
380
+ ],
337
381
  "appId": "brand"
338
382
  },
339
383
  {
@@ -505,7 +549,9 @@ var APPLICATIONS = {
505
549
  "domains": [
506
550
  "chat.topolo.app"
507
551
  ],
508
- "routes": [],
552
+ "routes": [
553
+ "chat.topolo.app"
554
+ ],
509
555
  "appId": "chat"
510
556
  },
511
557
  {
@@ -1065,7 +1111,9 @@ var APPLICATIONS = {
1065
1111
  "domains": [
1066
1112
  "mail.topolo.app"
1067
1113
  ],
1068
- "routes": [],
1114
+ "routes": [
1115
+ "mail.topolo.app"
1116
+ ],
1069
1117
  "appId": "mail"
1070
1118
  },
1071
1119
  {
@@ -1416,7 +1464,9 @@ var APPLICATIONS = {
1416
1464
  "domains": [
1417
1465
  "quro.topolo.app"
1418
1466
  ],
1419
- "routes": [],
1467
+ "routes": [
1468
+ "quro.topolo.app"
1469
+ ],
1420
1470
  "appId": null
1421
1471
  }
1422
1472
  ]
@@ -2498,6 +2548,168 @@ function isMultipleOf(value, divisor) {
2498
2548
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2499
2549
  }
2500
2550
 
2551
+ // src/action-upload.ts
2552
+ function parseTopoloActionUploadContract(value) {
2553
+ if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
2554
+ throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
2555
+ }
2556
+ const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
2557
+ const fileNameField = requiredField(value.fileNameField, "fileNameField");
2558
+ const maxBytes = Number(value.maxBytes);
2559
+ const acceptedContentTypes = Array.isArray(value.acceptedContentTypes) ? value.acceptedContentTypes.map((item) => String(item).trim().toLowerCase()).filter(Boolean) : [];
2560
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || acceptedContentTypes.length === 0) {
2561
+ throw new TopoloSdkError("invalid_upload_contract", "The action upload contract must declare maxBytes and acceptedContentTypes.");
2562
+ }
2563
+ if (value.profile === "inline-base64") {
2564
+ return {
2565
+ profile: "inline-base64",
2566
+ contentField: requiredField(value.contentField, "contentField"),
2567
+ contentTypeField,
2568
+ fileNameField,
2569
+ maxBytes,
2570
+ acceptedContentTypes
2571
+ };
2572
+ }
2573
+ const uploadMethod = value.uploadMethod === "PUT" || value.uploadMethod === "POST" ? value.uploadMethod : null;
2574
+ const uploadUrlPath = requiredPath(value.uploadUrlPath, "uploadUrlPath");
2575
+ const complete = value.complete === void 0 ? void 0 : parseCompleteContract(value.complete);
2576
+ if (!uploadMethod) throw new TopoloSdkError("invalid_upload_contract", "The action upload contract has an invalid uploadMethod.");
2577
+ return {
2578
+ profile: "url-session",
2579
+ fileNameField,
2580
+ contentTypeField,
2581
+ sizeField: requiredField(value.sizeField, "sizeField"),
2582
+ maxBytes,
2583
+ acceptedContentTypes,
2584
+ uploadUrlPath,
2585
+ uploadMethod,
2586
+ ...complete ? { complete } : {}
2587
+ };
2588
+ }
2589
+ function prepareTopoloActionUploadInput(contractValue, content, input, options) {
2590
+ const contract = parseTopoloActionUploadContract(contractValue);
2591
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2592
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
2593
+ if (bytes.byteLength === 0) {
2594
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2595
+ }
2596
+ if (bytes.byteLength > contract.maxBytes) {
2597
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2598
+ }
2599
+ return mapUploadInput(contract, bytesToBase64(bytes), input, options);
2600
+ }
2601
+ function prepareTopoloActionBase64UploadInput(contractValue, contentBase64, input, options) {
2602
+ const contract = parseTopoloActionUploadContract(contractValue);
2603
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2604
+ if (!isCanonicalBase64(contentBase64)) {
2605
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2606
+ }
2607
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2608
+ const byteLength = contentBase64.length / 4 * 3 - padding;
2609
+ if (byteLength === 0) throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2610
+ if (byteLength > contract.maxBytes) {
2611
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2612
+ }
2613
+ return mapUploadInput(contract, contentBase64, input, options);
2614
+ }
2615
+ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, options) {
2616
+ const contract = parseTopoloActionUploadContract(contractValue);
2617
+ if (contract.profile !== "url-session") {
2618
+ throw new TopoloSdkError("invalid_upload_contract", "This action does not publish a URL-session upload contract.");
2619
+ }
2620
+ validateUploadMetadata(contract, byteLength, options);
2621
+ return {
2622
+ contract,
2623
+ input: {
2624
+ ...input,
2625
+ [contract.fileNameField]: options.fileName.trim(),
2626
+ [contract.contentTypeField]: options.contentType.trim().toLowerCase(),
2627
+ [contract.sizeField]: byteLength
2628
+ }
2629
+ };
2630
+ }
2631
+ function topoloUploadResponseValue(response, path) {
2632
+ return path.split(".").reduce((value, segment) => isRecord2(value) ? value[segment] : void 0, response);
2633
+ }
2634
+ function decodeTopoloActionBase64(contentBase64) {
2635
+ if (!isCanonicalBase64(contentBase64)) {
2636
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2637
+ }
2638
+ const binary = atob(contentBase64);
2639
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
2640
+ }
2641
+ function mapUploadInput(contract, contentBase64, input, options) {
2642
+ validateUploadMetadata(contract, Math.max(1, decodedBase64Size(contentBase64)), options);
2643
+ const fileName = options.fileName.trim();
2644
+ const contentType = options.contentType.trim().toLowerCase();
2645
+ return {
2646
+ ...input,
2647
+ [contract.contentField]: contentBase64,
2648
+ [contract.contentTypeField]: contentType,
2649
+ [contract.fileNameField]: fileName
2650
+ };
2651
+ }
2652
+ function validateUploadMetadata(contract, byteLength, options) {
2653
+ const fileName = options.fileName.trim();
2654
+ const contentType = options.contentType.trim().toLowerCase();
2655
+ if (!fileName) throw new TopoloSdkError("invalid_upload_file_name", "Upload fileName must not be empty.");
2656
+ if (!Number.isSafeInteger(byteLength) || byteLength <= 0) {
2657
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2658
+ }
2659
+ if (byteLength > contract.maxBytes) {
2660
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2661
+ }
2662
+ if (!matchesContentType(contentType, contract.acceptedContentTypes)) {
2663
+ throw new TopoloSdkError("unsupported_upload_content_type", `Content type "${contentType}" is not accepted by this action.`);
2664
+ }
2665
+ }
2666
+ function decodedBase64Size(contentBase64) {
2667
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2668
+ return contentBase64.length / 4 * 3 - padding;
2669
+ }
2670
+ function parseCompleteContract(value) {
2671
+ if (!isRecord2(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
2672
+ return {
2673
+ actionId: requiredPath(value.actionId, "complete.actionId"),
2674
+ inputField: requiredField(value.inputField, "complete.inputField"),
2675
+ responsePath: requiredPath(value.responsePath, "complete.responsePath")
2676
+ };
2677
+ }
2678
+ function requiredPath(value, name) {
2679
+ const path = typeof value === "string" ? value.trim() : "";
2680
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path)) {
2681
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2682
+ }
2683
+ return path;
2684
+ }
2685
+ function unsupportedInlineProfile() {
2686
+ return new TopoloSdkError("invalid_upload_contract", "This action does not publish an inline-base64 upload contract.");
2687
+ }
2688
+ function isCanonicalBase64(value) {
2689
+ return value.length > 0 && value.length % 4 === 0 && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
2690
+ }
2691
+ function requiredField(value, name) {
2692
+ const field = typeof value === "string" ? value.trim() : "";
2693
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field)) {
2694
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2695
+ }
2696
+ return field;
2697
+ }
2698
+ function matchesContentType(contentType, accepted) {
2699
+ return accepted.some((candidate) => candidate === "*/*" || candidate === contentType || candidate.endsWith("/*") && contentType.startsWith(candidate.slice(0, -1)));
2700
+ }
2701
+ function bytesToBase64(bytes) {
2702
+ let binary = "";
2703
+ const chunkSize = 32768;
2704
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
2705
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
2706
+ }
2707
+ return btoa(binary);
2708
+ }
2709
+ function isRecord2(value) {
2710
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2711
+ }
2712
+
2501
2713
  // src/client.ts
2502
2714
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2503
2715
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
@@ -2575,6 +2787,9 @@ var TopoloClient = class _TopoloClient {
2575
2787
  if (v !== void 0) url.searchParams.set(k, String(v));
2576
2788
  }
2577
2789
  }
2790
+ if (opts.body !== void 0 && opts.rawBody !== void 0) {
2791
+ throw new TopoloSdkError("invalid_request_body", "body and rawBody are mutually exclusive.");
2792
+ }
2578
2793
  const headers = new Headers();
2579
2794
  headers.set("Accept", "application/json");
2580
2795
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
@@ -2609,7 +2824,7 @@ var TopoloClient = class _TopoloClient {
2609
2824
  return await this.fetchImpl(urlStr, {
2610
2825
  method,
2611
2826
  headers: new Headers(headers),
2612
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2827
+ body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
2613
2828
  signal
2614
2829
  });
2615
2830
  } finally {
@@ -2657,14 +2872,32 @@ var TopoloClient = class _TopoloClient {
2657
2872
  error: message,
2658
2873
  status: res.status
2659
2874
  });
2660
- if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
2875
+ const errorContext = {
2876
+ status: res.status,
2877
+ body: parsed,
2878
+ service: resolvedService.serviceKey,
2879
+ path: opts.path,
2880
+ requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
2881
+ retryAfter: res.headers.get("Retry-After") || void 0
2882
+ };
2883
+ if (res.status === 401) {
2884
+ throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
2885
+ }
2661
2886
  if (res.status === 403) {
2662
2887
  throw new TopoloPermissionError(
2663
2888
  describeError(parsed, "Permission denied"),
2664
- extractRequired(parsed)
2889
+ extractRequired(parsed),
2890
+ errorContext
2665
2891
  );
2666
2892
  }
2667
- throw new TopoloHttpError(resolvedService.serviceKey, opts.path, res.status, parsed);
2893
+ throw new TopoloHttpError(
2894
+ resolvedService.serviceKey,
2895
+ opts.path,
2896
+ res.status,
2897
+ parsed,
2898
+ message,
2899
+ errorContext
2900
+ );
2668
2901
  }
2669
2902
  this.emit({
2670
2903
  phase: "response",
@@ -2823,7 +3056,7 @@ var TopoloClient = class _TopoloClient {
2823
3056
  }
2824
3057
  const entries = [];
2825
3058
  for (const item of raw) {
2826
- if (!isRecord2(item)) continue;
3059
+ if (!isRecord3(item)) continue;
2827
3060
  const s = item;
2828
3061
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2829
3062
  if (!appId) continue;
@@ -2898,8 +3131,8 @@ var TopoloClient = class _TopoloClient {
2898
3131
  "app_topolo_auth.service_api_key_resources.list",
2899
3132
  { appId: service.appId }
2900
3133
  );
2901
- const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2902
- if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
3134
+ const payload = isRecord3(response) && isRecord3(response.data) ? response.data : response;
3135
+ if (!isRecord3(payload) || !Array.isArray(payload.resources)) {
2903
3136
  throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2904
3137
  }
2905
3138
  return {
@@ -3005,6 +3238,71 @@ var TopoloClient = class _TopoloClient {
3005
3238
  const entry = await this.getAction(action);
3006
3239
  return this.executeResolvedAction(entry, input, options);
3007
3240
  }
3241
+ async uploadAction(action, content, input, options) {
3242
+ const entry = await this.getAction(action);
3243
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3244
+ if (contract.profile === "url-session") {
3245
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
3246
+ return this.executeUrlSessionUpload(entry, contract, bytes, input, options);
3247
+ }
3248
+ const uploadInput = prepareTopoloActionUploadInput(entry.agentContract?.upload, content, input, options);
3249
+ return this.executeResolvedAction(entry, uploadInput, options);
3250
+ }
3251
+ async uploadActionBase64(action, contentBase64, input, options) {
3252
+ const entry = await this.getAction(action);
3253
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3254
+ if (contract.profile === "url-session") {
3255
+ return this.executeUrlSessionUpload(entry, contract, decodeTopoloActionBase64(contentBase64), input, options);
3256
+ }
3257
+ const uploadInput = prepareTopoloActionBase64UploadInput(entry.agentContract?.upload, contentBase64, input, options);
3258
+ return this.executeResolvedAction(entry, uploadInput, options);
3259
+ }
3260
+ async executeUrlSessionUpload(entry, contract, bytes, input, options) {
3261
+ const prepared = prepareTopoloUrlSessionUploadInput(contract, bytes.byteLength, input, options);
3262
+ const initiation = await this.executeResolvedAction(entry, prepared.input, options);
3263
+ const uploadUrl = topoloUploadResponseValue(initiation, contract.uploadUrlPath);
3264
+ if (typeof uploadUrl !== "string" || !uploadUrl.trim()) {
3265
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.uploadUrlPath}.`);
3266
+ }
3267
+ await this.putUrlSessionContent(entry, uploadUrl, contract.uploadMethod, bytes, options);
3268
+ if (!contract.complete) return initiation;
3269
+ const completionValue = topoloUploadResponseValue(initiation, contract.complete.responsePath);
3270
+ if (typeof completionValue !== "string" || !completionValue.trim()) {
3271
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.complete.responsePath}.`);
3272
+ }
3273
+ const completion = await this.getAction(contract.complete.actionId);
3274
+ return this.executeResolvedAction(completion, {
3275
+ ...options.completionInput ?? {},
3276
+ [contract.complete.inputField]: completionValue
3277
+ }, options);
3278
+ }
3279
+ async putUrlSessionContent(entry, uploadUrl, method, bytes, options) {
3280
+ const service = entry.appSlug || entry.appId;
3281
+ const resolved = entry.apiBaseUrl ? { serviceKey: normalizeLookupKey(service), baseUrl: requireHttpOrigin(entry.apiBaseUrl) } : await this.resolveRequestService(service);
3282
+ const target = new URL(uploadUrl, resolved.baseUrl.endsWith("/") ? resolved.baseUrl : `${resolved.baseUrl}/`);
3283
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3284
+ if (target.origin === new URL(resolved.baseUrl).origin) {
3285
+ await this.request({
3286
+ service,
3287
+ baseUrl: resolved.baseUrl,
3288
+ path: target.toString(),
3289
+ method,
3290
+ rawBody: body,
3291
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3292
+ resource: options.resource,
3293
+ confirm: options.confirm
3294
+ });
3295
+ return;
3296
+ }
3297
+ const response = await this.fetchImpl(target, {
3298
+ method,
3299
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3300
+ body
3301
+ });
3302
+ if (!response.ok) {
3303
+ throw new TopoloHttpError(service, target.pathname, response.status, await response.text(), `Upload transport failed with HTTP ${response.status}.`);
3304
+ }
3305
+ }
3008
3306
  async validateActionInput(action, input = {}) {
3009
3307
  const entry = await this.getAction(action);
3010
3308
  return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
@@ -3163,7 +3461,7 @@ function resolveConfiguredAppId(slug, appIds) {
3163
3461
  return null;
3164
3462
  }
3165
3463
  function normalizeApiKeyResourceMetadata(input) {
3166
- if (!isRecord2(input)) return null;
3464
+ if (!isRecord3(input)) return null;
3167
3465
  const resourceType = readString(input.resourceType ?? input.resource_type);
3168
3466
  const label = readString(input.label);
3169
3467
  const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
@@ -3197,7 +3495,7 @@ function normalizeApiKeyResourceMetadataList(input) {
3197
3495
  return resources;
3198
3496
  }
3199
3497
  function normalizeBindableResource(input) {
3200
- if (!isRecord2(input)) return null;
3498
+ if (!isRecord3(input)) return null;
3201
3499
  const resourceType = readString(input.resourceType ?? input.resource_type);
3202
3500
  const resourceId = readString(input.resourceId ?? input.resource_id);
3203
3501
  if (!resourceType || !resourceId) return null;
@@ -3208,13 +3506,13 @@ function normalizeBindableResource(input) {
3208
3506
  slug: readOptionalString(input.slug),
3209
3507
  status: readOptionalString(input.status),
3210
3508
  organizationId: readOptionalString(input.organizationId ?? input.organization_id),
3211
- metadata: isRecord2(input.metadata) ? input.metadata : {}
3509
+ metadata: isRecord3(input.metadata) ? input.metadata : {}
3212
3510
  };
3213
3511
  }
3214
3512
  function normalizeResourceBindings(input) {
3215
3513
  if (!Array.isArray(input)) return [];
3216
3514
  return input.flatMap((binding) => {
3217
- if (!isRecord2(binding)) return [];
3515
+ if (!isRecord3(binding)) return [];
3218
3516
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
3219
3517
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
3220
3518
  return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
@@ -3227,7 +3525,7 @@ function readOptionalString(value) {
3227
3525
  const normalized = readString(value);
3228
3526
  return normalized || null;
3229
3527
  }
3230
- function isRecord2(value) {
3528
+ function isRecord3(value) {
3231
3529
  return typeof value === "object" && value !== null && !Array.isArray(value);
3232
3530
  }
3233
3531
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -3299,14 +3597,14 @@ function catalogSearchQuery(options) {
3299
3597
  return query;
3300
3598
  }
3301
3599
  function normalizeCatalogPage(value) {
3302
- if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
3600
+ if (!isRecord3(value)) return { nextCursor: null, hasMore: false };
3303
3601
  return {
3304
3602
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
3305
3603
  hasMore: value.hasMore === true
3306
3604
  };
3307
3605
  }
3308
3606
  function normalizeServiceCatalogEntry(item) {
3309
- if (!isRecord2(item)) return null;
3607
+ if (!isRecord3(item)) return null;
3310
3608
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
3311
3609
  if (!appId) return null;
3312
3610
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -3564,7 +3862,7 @@ var INVERSE_OPERATIONS = /* @__PURE__ */ new Map([
3564
3862
  ["restore", "archive"],
3565
3863
  ["unpublish", "publish"]
3566
3864
  ]);
3567
- function isRecord3(value) {
3865
+ function isRecord4(value) {
3568
3866
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
3569
3867
  }
3570
3868
  function nonEmptyStrings(value) {
@@ -3602,10 +3900,10 @@ function collectionFor(action, resource) {
3602
3900
  return resource.endsWith("s") ? resource : `${resource}s`;
3603
3901
  }
3604
3902
  function isConcreteOutputSchema(schema) {
3605
- if (!isRecord3(schema)) return false;
3903
+ if (!isRecord4(schema)) return false;
3606
3904
  if (typeof schema.$ref === "string" && schema.$ref.trim()) return true;
3607
- if (schema.type === "array" && isRecord3(schema.items) && Object.keys(schema.items).length > 0) return true;
3608
- if (isRecord3(schema.properties) && Object.keys(schema.properties).length > 0) return true;
3905
+ if (schema.type === "array" && isRecord4(schema.items) && Object.keys(schema.items).length > 0) return true;
3906
+ if (isRecord4(schema.properties) && Object.keys(schema.properties).length > 0) return true;
3609
3907
  return ["oneOf", "anyOf", "allOf"].some((key) => Array.isArray(schema[key]) && schema[key].some(isConcreteOutputSchema));
3610
3908
  }
3611
3909
  function resourceSchema(resource) {
@@ -3686,7 +3984,7 @@ function derivedOutputSchema(action) {
3686
3984
  };
3687
3985
  }
3688
3986
  function sampleForSchema(schema, depth = 0) {
3689
- if (!isRecord3(schema) || depth > 5) return null;
3987
+ if (!isRecord4(schema) || depth > 5) return null;
3690
3988
  if (schema.const !== void 0) return schema.const;
3691
3989
  if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
3692
3990
  if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return sampleForSchema(schema.oneOf[0], depth + 1);
@@ -3700,8 +3998,8 @@ function sampleForSchema(schema, depth = 0) {
3700
3998
  return schema.minLength > 1 ? "example".padEnd(schema.minLength, "x") : "example";
3701
3999
  }
3702
4000
  if (schema.type === "null") return null;
3703
- if (schema.type === "object" || isRecord3(schema.properties)) {
3704
- const properties = isRecord3(schema.properties) ? schema.properties : {};
4001
+ if (schema.type === "object" || isRecord4(schema.properties)) {
4002
+ const properties = isRecord4(schema.properties) ? schema.properties : {};
3705
4003
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
3706
4004
  const entries = Object.entries(properties).filter(([key]) => required.has(key)).map(([key, child]) => [key, sampleForSchema(child, depth + 1)]);
3707
4005
  return Object.fromEntries(entries);
@@ -3751,21 +4049,32 @@ function defaultErrors(action) {
3751
4049
  const errors = [
3752
4050
  { status: 401, code: "authentication_required", recovery: "Authenticate again or provide a valid Topolo API key." },
3753
4051
  { status: 403, code: "permission_denied", recovery: `Grant the credential permission ${action.permission || action.permissionName || "required by this action"} and the required resource binding.` },
3754
- { status: 422, code: "invalid_input", recovery: "Correct the payload using inputSchema, then validate before retrying." }
4052
+ { status: 429, code: "rate_limited", recovery: "Wait for Retry-After when present, then retry without parallel duplicate calls." },
4053
+ { status: 422, code: "invalid_input", recovery: "Correct the payload using inputSchema, then validate before retrying." },
4054
+ { status: 503, code: "dependency_unavailable", recovery: "Retry the action. If it continues, inspect the structured error details using the request ID." }
3755
4055
  ];
3756
4056
  if (action.method !== "GET") {
3757
4057
  errors.push({ status: 409, code: "state_conflict", recovery: "Read the current resource state, reconcile the intended change, and retry only if still valid." });
3758
4058
  }
3759
4059
  return errors;
3760
4060
  }
4061
+ function mergeErrors(defaults, declared) {
4062
+ const merged = [...defaults];
4063
+ for (const error of Array.isArray(declared) ? declared : []) {
4064
+ const index = merged.findIndex((candidate) => candidate.status === error?.status && candidate.code === error?.code);
4065
+ if (index === -1) merged.push(error);
4066
+ else merged[index] = error;
4067
+ }
4068
+ return merged;
4069
+ }
3761
4070
  function enrichActionContract(manifest, action, allActions = manifest.actions || []) {
3762
4071
  const appId = String(manifest.appId || manifest.app_id || "").trim();
3763
4072
  const service = slugify(manifest.slug || appId.replace(/^app_/, "").replaceAll("_", "-") || manifest.name);
3764
4073
  const outputSchema = isConcreteOutputSchema(action.outputSchema) ? structuredClone(action.outputSchema) : derivedOutputSchema(action);
3765
4074
  const resource = resourceFor(action);
3766
4075
  const readOnly = action.readOnly !== void 0 ? action.readOnly === true : action.method === "GET";
3767
- const existing = isRecord3(action.agentContract) ? structuredClone(action.agentContract) : {};
3768
- const recovery = isRecord3(existing.recovery) ? existing.recovery : recoveryFor(action, allActions, appId);
4076
+ const existing = isRecord4(action.agentContract) ? structuredClone(action.agentContract) : {};
4077
+ const recovery = isRecord4(existing.recovery) ? existing.recovery : recoveryFor(action, allActions, appId);
3769
4078
  const rollbackActionId = typeof existing.rollbackActionId === "string" && existing.rollbackActionId.trim() ? existing.rollbackActionId.trim() : recovery.mode === "compensating_action" ? recovery.actionId : void 0;
3770
4079
  const inputExample = sampleForSchema(action.inputSchema);
3771
4080
  const examples = Array.isArray(existing.examples) && existing.examples.length > 0 ? existing.examples : [{ input: inputExample ?? {}, expected: `A response matching the published outputSchema for ${action.name}.` }];
@@ -3777,9 +4086,9 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3777
4086
  ...existing,
3778
4087
  docsUrl,
3779
4088
  ...existingDocsUrl && existingDocsUrl !== docsUrl ? { guideUrl: existingDocsUrl } : {},
3780
- resource: isRecord3(existing.resource) ? existing.resource : { kind: resource, idPath: readOnly && ["list", "search"].includes(operationFor(action)) ? "$.items[].id" : "$.id" },
4089
+ resource: isRecord4(existing.resource) ? existing.resource : { kind: resource, idPath: readOnly && ["list", "search"].includes(operationFor(action)) ? "$.items[].id" : "$.id" },
3781
4090
  effects: nonEmptyStrings(existing.effects) ? existing.effects : [readOnly ? `Reads ${resource.replaceAll("_", " ")} data without changing service state.` : action.destructive === true || action.method === "DELETE" ? `Permanently removes or invalidates ${resource.replaceAll("_", " ")} state.` : `Changes ${resource.replaceAll("_", " ")} state in the selected Topolo resource context.`],
3782
- errors: Array.isArray(existing.errors) && existing.errors.length > 0 ? existing.errors : defaultErrors(action),
4091
+ errors: mergeErrors(defaultErrors(action), existing.errors),
3783
4092
  verify: nonEmptyStrings(existing.verify) ? existing.verify : [`Validate the response against outputSchema and ${readOnly ? "confirm it belongs to the selected resource context" : "re-read the affected resource to confirm the intended state"}.`],
3784
4093
  examples,
3785
4094
  recovery,
@@ -3791,15 +4100,15 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3791
4100
  function actionContractCompleteness(action) {
3792
4101
  const contract = action?.agentContract;
3793
4102
  return {
3794
- inputSchema: isRecord3(action?.inputSchema),
4103
+ inputSchema: isRecord4(action?.inputSchema),
3795
4104
  outputSchema: isConcreteOutputSchema(action?.outputSchema),
3796
4105
  effects: nonEmptyStrings(contract?.effects),
3797
4106
  errors: Array.isArray(contract?.errors) && contract.errors.length > 0,
3798
4107
  docs: typeof contract?.docsUrl === "string" && contract.docsUrl.startsWith("https://"),
3799
4108
  examples: Array.isArray(contract?.examples) && contract.examples.length > 0,
3800
4109
  verification: nonEmptyStrings(contract?.verify),
3801
- recovery: isRecord3(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
3802
- agentContract: isRecord3(contract) && isRecord3(contract.resource) && nonEmptyStrings(contract.effects) && Array.isArray(contract.errors) && contract.errors.length > 0
4110
+ recovery: isRecord4(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
4111
+ agentContract: isRecord4(contract) && isRecord4(contract.resource) && nonEmptyStrings(contract.effects) && Array.isArray(contract.errors) && contract.errors.length > 0
3803
4112
  };
3804
4113
  }
3805
4114
 
@@ -4204,7 +4513,7 @@ function createTopoloWidgetError(error) {
4204
4513
  }
4205
4514
  function validateTopoloWidgetResponse(response) {
4206
4515
  const errors = [];
4207
- if (!isRecord4(response)) {
4516
+ if (!isRecord5(response)) {
4208
4517
  return { ok: false, errors: ["response must be an object"] };
4209
4518
  }
4210
4519
  if (typeof response.success !== "boolean") {
@@ -4223,7 +4532,7 @@ function validateTopoloWidgetResponse(response) {
4223
4532
  return { ok: errors.length === 0, errors };
4224
4533
  }
4225
4534
  function validateWidget(widget, path, errors) {
4226
- if (!isRecord4(widget)) {
4535
+ if (!isRecord5(widget)) {
4227
4536
  errors.push(`${path} must be an object`);
4228
4537
  return;
4229
4538
  }
@@ -4250,7 +4559,7 @@ function validateWidget(widget, path, errors) {
4250
4559
  case "chart":
4251
4560
  validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
4252
4561
  validateNonEmptyString(widget.title, `${path}.title`, errors);
4253
- if (!isRecord4(widget.data)) {
4562
+ if (!isRecord5(widget.data)) {
4254
4563
  errors.push(`${path}.data must be an object`);
4255
4564
  } else {
4256
4565
  validateArray2(widget.data.labels, `${path}.data.labels`, errors);
@@ -4266,7 +4575,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4266
4575
  errors.push(`${path} must be an object`);
4267
4576
  return;
4268
4577
  }
4269
- if (!isRecord4(snapshot)) {
4578
+ if (!isRecord5(snapshot)) {
4270
4579
  errors.push(`${path} must be an object`);
4271
4580
  return;
4272
4581
  }
@@ -4286,7 +4595,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4286
4595
  }
4287
4596
  snapshot.availableMetrics.forEach((metric, index) => {
4288
4597
  const metricPath = `${path}.availableMetrics[${index}]`;
4289
- if (!isRecord4(metric)) {
4598
+ if (!isRecord5(metric)) {
4290
4599
  errors.push(`${metricPath} must be an object`);
4291
4600
  return;
4292
4601
  }
@@ -4307,7 +4616,7 @@ function validateArray2(value, path, errors) {
4307
4616
  errors.push(`${path} must be an array`);
4308
4617
  }
4309
4618
  }
4310
- function isRecord4(value) {
4619
+ function isRecord5(value) {
4311
4620
  return typeof value === "object" && value !== null && !Array.isArray(value);
4312
4621
  }
4313
4622
  function toIsoTimestamp(input) {
@@ -4359,6 +4668,9 @@ function createTopolo(options) {
4359
4668
  defineTopoloStatsWidget,
4360
4669
  digestTopoloActionCatalog,
4361
4670
  enrichActionContract,
4671
+ parseTopoloActionUploadContract,
4672
+ prepareTopoloActionBase64UploadInput,
4673
+ prepareTopoloActionUploadInput,
4362
4674
  requirementsForApplication,
4363
4675
  resolveCatalogServiceUrl,
4364
4676
  resolveServiceUrl,