@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.js CHANGED
@@ -28,19 +28,54 @@ var TopoloSdkError = class extends Error {
28
28
  }
29
29
  };
30
30
  var TopoloAuthError = class extends TopoloSdkError {
31
- constructor(message, code = "auth_error") {
31
+ status;
32
+ body;
33
+ service;
34
+ path;
35
+ requestId;
36
+ retryAfter;
37
+ constructor(message, code = "auth_error", options = {}) {
32
38
  super(code, message);
33
39
  this.name = "TopoloAuthError";
40
+ this.status = options.status ?? 401;
41
+ this.body = options.body;
42
+ this.service = options.service;
43
+ this.path = options.path;
44
+ this.requestId = options.requestId;
45
+ this.retryAfter = options.retryAfter;
34
46
  }
35
47
  };
36
48
  var TopoloPermissionError = class extends TopoloSdkError {
37
49
  required;
38
- constructor(message, required) {
50
+ status;
51
+ body;
52
+ service;
53
+ path;
54
+ requestId;
55
+ retryAfter;
56
+ constructor(message, required, options = {}) {
39
57
  super("permission_denied", message);
40
58
  this.name = "TopoloPermissionError";
41
59
  this.required = required;
60
+ this.status = options.status ?? 403;
61
+ this.body = options.body;
62
+ this.service = options.service;
63
+ this.path = options.path;
64
+ this.requestId = options.requestId;
65
+ this.retryAfter = options.retryAfter;
42
66
  }
43
67
  };
68
+ function extractHttpErrorCode(body) {
69
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
70
+ const record = body;
71
+ if (typeof record.code === "string" && record.code.trim()) return record.code.trim();
72
+ const nested = record.error;
73
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
74
+ const code = nested.code;
75
+ if (typeof code === "string" && code.trim()) return code.trim();
76
+ }
77
+ return "http_error";
78
+ }
44
79
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
45
80
  constructor(message) {
46
81
  super("confirmation_required", message);
@@ -52,13 +87,17 @@ var TopoloHttpError = class extends TopoloSdkError {
52
87
  body;
53
88
  service;
54
89
  path;
55
- constructor(service, path, status, body, message) {
56
- super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
90
+ requestId;
91
+ retryAfter;
92
+ constructor(service, path, status, body, message, options = {}) {
93
+ super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path}`);
57
94
  this.name = "TopoloHttpError";
58
95
  this.status = status;
59
96
  this.body = body;
60
97
  this.service = service;
61
98
  this.path = path;
99
+ this.requestId = options.requestId;
100
+ this.retryAfter = options.retryAfter;
62
101
  }
63
102
  };
64
103
 
@@ -272,7 +311,9 @@ var APPLICATIONS = {
272
311
  "domains": [
273
312
  "brand.topolo.app"
274
313
  ],
275
- "routes": [],
314
+ "routes": [
315
+ "brand.topolo.app"
316
+ ],
276
317
  "appId": "brand"
277
318
  },
278
319
  {
@@ -444,7 +485,9 @@ var APPLICATIONS = {
444
485
  "domains": [
445
486
  "chat.topolo.app"
446
487
  ],
447
- "routes": [],
488
+ "routes": [
489
+ "chat.topolo.app"
490
+ ],
448
491
  "appId": "chat"
449
492
  },
450
493
  {
@@ -1004,7 +1047,9 @@ var APPLICATIONS = {
1004
1047
  "domains": [
1005
1048
  "mail.topolo.app"
1006
1049
  ],
1007
- "routes": [],
1050
+ "routes": [
1051
+ "mail.topolo.app"
1052
+ ],
1008
1053
  "appId": "mail"
1009
1054
  },
1010
1055
  {
@@ -1355,7 +1400,9 @@ var APPLICATIONS = {
1355
1400
  "domains": [
1356
1401
  "quro.topolo.app"
1357
1402
  ],
1358
- "routes": [],
1403
+ "routes": [
1404
+ "quro.topolo.app"
1405
+ ],
1359
1406
  "appId": null
1360
1407
  }
1361
1408
  ]
@@ -2437,6 +2484,168 @@ function isMultipleOf(value, divisor) {
2437
2484
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2438
2485
  }
2439
2486
 
2487
+ // src/action-upload.ts
2488
+ function parseTopoloActionUploadContract(value) {
2489
+ if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
2490
+ throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
2491
+ }
2492
+ const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
2493
+ const fileNameField = requiredField(value.fileNameField, "fileNameField");
2494
+ const maxBytes = Number(value.maxBytes);
2495
+ const acceptedContentTypes = Array.isArray(value.acceptedContentTypes) ? value.acceptedContentTypes.map((item) => String(item).trim().toLowerCase()).filter(Boolean) : [];
2496
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || acceptedContentTypes.length === 0) {
2497
+ throw new TopoloSdkError("invalid_upload_contract", "The action upload contract must declare maxBytes and acceptedContentTypes.");
2498
+ }
2499
+ if (value.profile === "inline-base64") {
2500
+ return {
2501
+ profile: "inline-base64",
2502
+ contentField: requiredField(value.contentField, "contentField"),
2503
+ contentTypeField,
2504
+ fileNameField,
2505
+ maxBytes,
2506
+ acceptedContentTypes
2507
+ };
2508
+ }
2509
+ const uploadMethod = value.uploadMethod === "PUT" || value.uploadMethod === "POST" ? value.uploadMethod : null;
2510
+ const uploadUrlPath = requiredPath(value.uploadUrlPath, "uploadUrlPath");
2511
+ const complete = value.complete === void 0 ? void 0 : parseCompleteContract(value.complete);
2512
+ if (!uploadMethod) throw new TopoloSdkError("invalid_upload_contract", "The action upload contract has an invalid uploadMethod.");
2513
+ return {
2514
+ profile: "url-session",
2515
+ fileNameField,
2516
+ contentTypeField,
2517
+ sizeField: requiredField(value.sizeField, "sizeField"),
2518
+ maxBytes,
2519
+ acceptedContentTypes,
2520
+ uploadUrlPath,
2521
+ uploadMethod,
2522
+ ...complete ? { complete } : {}
2523
+ };
2524
+ }
2525
+ function prepareTopoloActionUploadInput(contractValue, content, input, options) {
2526
+ const contract = parseTopoloActionUploadContract(contractValue);
2527
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2528
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
2529
+ if (bytes.byteLength === 0) {
2530
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2531
+ }
2532
+ if (bytes.byteLength > contract.maxBytes) {
2533
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2534
+ }
2535
+ return mapUploadInput(contract, bytesToBase64(bytes), input, options);
2536
+ }
2537
+ function prepareTopoloActionBase64UploadInput(contractValue, contentBase64, input, options) {
2538
+ const contract = parseTopoloActionUploadContract(contractValue);
2539
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2540
+ if (!isCanonicalBase64(contentBase64)) {
2541
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2542
+ }
2543
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2544
+ const byteLength = contentBase64.length / 4 * 3 - padding;
2545
+ if (byteLength === 0) throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2546
+ if (byteLength > contract.maxBytes) {
2547
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2548
+ }
2549
+ return mapUploadInput(contract, contentBase64, input, options);
2550
+ }
2551
+ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, options) {
2552
+ const contract = parseTopoloActionUploadContract(contractValue);
2553
+ if (contract.profile !== "url-session") {
2554
+ throw new TopoloSdkError("invalid_upload_contract", "This action does not publish a URL-session upload contract.");
2555
+ }
2556
+ validateUploadMetadata(contract, byteLength, options);
2557
+ return {
2558
+ contract,
2559
+ input: {
2560
+ ...input,
2561
+ [contract.fileNameField]: options.fileName.trim(),
2562
+ [contract.contentTypeField]: options.contentType.trim().toLowerCase(),
2563
+ [contract.sizeField]: byteLength
2564
+ }
2565
+ };
2566
+ }
2567
+ function topoloUploadResponseValue(response, path) {
2568
+ return path.split(".").reduce((value, segment) => isRecord2(value) ? value[segment] : void 0, response);
2569
+ }
2570
+ function decodeTopoloActionBase64(contentBase64) {
2571
+ if (!isCanonicalBase64(contentBase64)) {
2572
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2573
+ }
2574
+ const binary = atob(contentBase64);
2575
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
2576
+ }
2577
+ function mapUploadInput(contract, contentBase64, input, options) {
2578
+ validateUploadMetadata(contract, Math.max(1, decodedBase64Size(contentBase64)), options);
2579
+ const fileName = options.fileName.trim();
2580
+ const contentType = options.contentType.trim().toLowerCase();
2581
+ return {
2582
+ ...input,
2583
+ [contract.contentField]: contentBase64,
2584
+ [contract.contentTypeField]: contentType,
2585
+ [contract.fileNameField]: fileName
2586
+ };
2587
+ }
2588
+ function validateUploadMetadata(contract, byteLength, options) {
2589
+ const fileName = options.fileName.trim();
2590
+ const contentType = options.contentType.trim().toLowerCase();
2591
+ if (!fileName) throw new TopoloSdkError("invalid_upload_file_name", "Upload fileName must not be empty.");
2592
+ if (!Number.isSafeInteger(byteLength) || byteLength <= 0) {
2593
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2594
+ }
2595
+ if (byteLength > contract.maxBytes) {
2596
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2597
+ }
2598
+ if (!matchesContentType(contentType, contract.acceptedContentTypes)) {
2599
+ throw new TopoloSdkError("unsupported_upload_content_type", `Content type "${contentType}" is not accepted by this action.`);
2600
+ }
2601
+ }
2602
+ function decodedBase64Size(contentBase64) {
2603
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2604
+ return contentBase64.length / 4 * 3 - padding;
2605
+ }
2606
+ function parseCompleteContract(value) {
2607
+ if (!isRecord2(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
2608
+ return {
2609
+ actionId: requiredPath(value.actionId, "complete.actionId"),
2610
+ inputField: requiredField(value.inputField, "complete.inputField"),
2611
+ responsePath: requiredPath(value.responsePath, "complete.responsePath")
2612
+ };
2613
+ }
2614
+ function requiredPath(value, name) {
2615
+ const path = typeof value === "string" ? value.trim() : "";
2616
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path)) {
2617
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2618
+ }
2619
+ return path;
2620
+ }
2621
+ function unsupportedInlineProfile() {
2622
+ return new TopoloSdkError("invalid_upload_contract", "This action does not publish an inline-base64 upload contract.");
2623
+ }
2624
+ function isCanonicalBase64(value) {
2625
+ 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);
2626
+ }
2627
+ function requiredField(value, name) {
2628
+ const field = typeof value === "string" ? value.trim() : "";
2629
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field)) {
2630
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2631
+ }
2632
+ return field;
2633
+ }
2634
+ function matchesContentType(contentType, accepted) {
2635
+ return accepted.some((candidate) => candidate === "*/*" || candidate === contentType || candidate.endsWith("/*") && contentType.startsWith(candidate.slice(0, -1)));
2636
+ }
2637
+ function bytesToBase64(bytes) {
2638
+ let binary = "";
2639
+ const chunkSize = 32768;
2640
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
2641
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
2642
+ }
2643
+ return btoa(binary);
2644
+ }
2645
+ function isRecord2(value) {
2646
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2647
+ }
2648
+
2440
2649
  // src/client.ts
2441
2650
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2442
2651
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
@@ -2514,6 +2723,9 @@ var TopoloClient = class _TopoloClient {
2514
2723
  if (v !== void 0) url.searchParams.set(k, String(v));
2515
2724
  }
2516
2725
  }
2726
+ if (opts.body !== void 0 && opts.rawBody !== void 0) {
2727
+ throw new TopoloSdkError("invalid_request_body", "body and rawBody are mutually exclusive.");
2728
+ }
2517
2729
  const headers = new Headers();
2518
2730
  headers.set("Accept", "application/json");
2519
2731
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
@@ -2548,7 +2760,7 @@ var TopoloClient = class _TopoloClient {
2548
2760
  return await this.fetchImpl(urlStr, {
2549
2761
  method,
2550
2762
  headers: new Headers(headers),
2551
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2763
+ body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
2552
2764
  signal
2553
2765
  });
2554
2766
  } finally {
@@ -2596,14 +2808,32 @@ var TopoloClient = class _TopoloClient {
2596
2808
  error: message,
2597
2809
  status: res.status
2598
2810
  });
2599
- if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
2811
+ const errorContext = {
2812
+ status: res.status,
2813
+ body: parsed,
2814
+ service: resolvedService.serviceKey,
2815
+ path: opts.path,
2816
+ requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
2817
+ retryAfter: res.headers.get("Retry-After") || void 0
2818
+ };
2819
+ if (res.status === 401) {
2820
+ throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
2821
+ }
2600
2822
  if (res.status === 403) {
2601
2823
  throw new TopoloPermissionError(
2602
2824
  describeError(parsed, "Permission denied"),
2603
- extractRequired(parsed)
2825
+ extractRequired(parsed),
2826
+ errorContext
2604
2827
  );
2605
2828
  }
2606
- throw new TopoloHttpError(resolvedService.serviceKey, opts.path, res.status, parsed);
2829
+ throw new TopoloHttpError(
2830
+ resolvedService.serviceKey,
2831
+ opts.path,
2832
+ res.status,
2833
+ parsed,
2834
+ message,
2835
+ errorContext
2836
+ );
2607
2837
  }
2608
2838
  this.emit({
2609
2839
  phase: "response",
@@ -2762,7 +2992,7 @@ var TopoloClient = class _TopoloClient {
2762
2992
  }
2763
2993
  const entries = [];
2764
2994
  for (const item of raw) {
2765
- if (!isRecord2(item)) continue;
2995
+ if (!isRecord3(item)) continue;
2766
2996
  const s = item;
2767
2997
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2768
2998
  if (!appId) continue;
@@ -2837,8 +3067,8 @@ var TopoloClient = class _TopoloClient {
2837
3067
  "app_topolo_auth.service_api_key_resources.list",
2838
3068
  { appId: service.appId }
2839
3069
  );
2840
- const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2841
- if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
3070
+ const payload = isRecord3(response) && isRecord3(response.data) ? response.data : response;
3071
+ if (!isRecord3(payload) || !Array.isArray(payload.resources)) {
2842
3072
  throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2843
3073
  }
2844
3074
  return {
@@ -2944,6 +3174,71 @@ var TopoloClient = class _TopoloClient {
2944
3174
  const entry = await this.getAction(action);
2945
3175
  return this.executeResolvedAction(entry, input, options);
2946
3176
  }
3177
+ async uploadAction(action, content, input, options) {
3178
+ const entry = await this.getAction(action);
3179
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3180
+ if (contract.profile === "url-session") {
3181
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
3182
+ return this.executeUrlSessionUpload(entry, contract, bytes, input, options);
3183
+ }
3184
+ const uploadInput = prepareTopoloActionUploadInput(entry.agentContract?.upload, content, input, options);
3185
+ return this.executeResolvedAction(entry, uploadInput, options);
3186
+ }
3187
+ async uploadActionBase64(action, contentBase64, input, options) {
3188
+ const entry = await this.getAction(action);
3189
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3190
+ if (contract.profile === "url-session") {
3191
+ return this.executeUrlSessionUpload(entry, contract, decodeTopoloActionBase64(contentBase64), input, options);
3192
+ }
3193
+ const uploadInput = prepareTopoloActionBase64UploadInput(entry.agentContract?.upload, contentBase64, input, options);
3194
+ return this.executeResolvedAction(entry, uploadInput, options);
3195
+ }
3196
+ async executeUrlSessionUpload(entry, contract, bytes, input, options) {
3197
+ const prepared = prepareTopoloUrlSessionUploadInput(contract, bytes.byteLength, input, options);
3198
+ const initiation = await this.executeResolvedAction(entry, prepared.input, options);
3199
+ const uploadUrl = topoloUploadResponseValue(initiation, contract.uploadUrlPath);
3200
+ if (typeof uploadUrl !== "string" || !uploadUrl.trim()) {
3201
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.uploadUrlPath}.`);
3202
+ }
3203
+ await this.putUrlSessionContent(entry, uploadUrl, contract.uploadMethod, bytes, options);
3204
+ if (!contract.complete) return initiation;
3205
+ const completionValue = topoloUploadResponseValue(initiation, contract.complete.responsePath);
3206
+ if (typeof completionValue !== "string" || !completionValue.trim()) {
3207
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.complete.responsePath}.`);
3208
+ }
3209
+ const completion = await this.getAction(contract.complete.actionId);
3210
+ return this.executeResolvedAction(completion, {
3211
+ ...options.completionInput ?? {},
3212
+ [contract.complete.inputField]: completionValue
3213
+ }, options);
3214
+ }
3215
+ async putUrlSessionContent(entry, uploadUrl, method, bytes, options) {
3216
+ const service = entry.appSlug || entry.appId;
3217
+ const resolved = entry.apiBaseUrl ? { serviceKey: normalizeLookupKey(service), baseUrl: requireHttpOrigin(entry.apiBaseUrl) } : await this.resolveRequestService(service);
3218
+ const target = new URL(uploadUrl, resolved.baseUrl.endsWith("/") ? resolved.baseUrl : `${resolved.baseUrl}/`);
3219
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3220
+ if (target.origin === new URL(resolved.baseUrl).origin) {
3221
+ await this.request({
3222
+ service,
3223
+ baseUrl: resolved.baseUrl,
3224
+ path: target.toString(),
3225
+ method,
3226
+ rawBody: body,
3227
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3228
+ resource: options.resource,
3229
+ confirm: options.confirm
3230
+ });
3231
+ return;
3232
+ }
3233
+ const response = await this.fetchImpl(target, {
3234
+ method,
3235
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3236
+ body
3237
+ });
3238
+ if (!response.ok) {
3239
+ throw new TopoloHttpError(service, target.pathname, response.status, await response.text(), `Upload transport failed with HTTP ${response.status}.`);
3240
+ }
3241
+ }
2947
3242
  async validateActionInput(action, input = {}) {
2948
3243
  const entry = await this.getAction(action);
2949
3244
  return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input) };
@@ -3102,7 +3397,7 @@ function resolveConfiguredAppId(slug, appIds) {
3102
3397
  return null;
3103
3398
  }
3104
3399
  function normalizeApiKeyResourceMetadata(input) {
3105
- if (!isRecord2(input)) return null;
3400
+ if (!isRecord3(input)) return null;
3106
3401
  const resourceType = readString(input.resourceType ?? input.resource_type);
3107
3402
  const label = readString(input.label);
3108
3403
  const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
@@ -3136,7 +3431,7 @@ function normalizeApiKeyResourceMetadataList(input) {
3136
3431
  return resources;
3137
3432
  }
3138
3433
  function normalizeBindableResource(input) {
3139
- if (!isRecord2(input)) return null;
3434
+ if (!isRecord3(input)) return null;
3140
3435
  const resourceType = readString(input.resourceType ?? input.resource_type);
3141
3436
  const resourceId = readString(input.resourceId ?? input.resource_id);
3142
3437
  if (!resourceType || !resourceId) return null;
@@ -3147,13 +3442,13 @@ function normalizeBindableResource(input) {
3147
3442
  slug: readOptionalString(input.slug),
3148
3443
  status: readOptionalString(input.status),
3149
3444
  organizationId: readOptionalString(input.organizationId ?? input.organization_id),
3150
- metadata: isRecord2(input.metadata) ? input.metadata : {}
3445
+ metadata: isRecord3(input.metadata) ? input.metadata : {}
3151
3446
  };
3152
3447
  }
3153
3448
  function normalizeResourceBindings(input) {
3154
3449
  if (!Array.isArray(input)) return [];
3155
3450
  return input.flatMap((binding) => {
3156
- if (!isRecord2(binding)) return [];
3451
+ if (!isRecord3(binding)) return [];
3157
3452
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
3158
3453
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
3159
3454
  return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
@@ -3166,7 +3461,7 @@ function readOptionalString(value) {
3166
3461
  const normalized = readString(value);
3167
3462
  return normalized || null;
3168
3463
  }
3169
- function isRecord2(value) {
3464
+ function isRecord3(value) {
3170
3465
  return typeof value === "object" && value !== null && !Array.isArray(value);
3171
3466
  }
3172
3467
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -3238,14 +3533,14 @@ function catalogSearchQuery(options) {
3238
3533
  return query;
3239
3534
  }
3240
3535
  function normalizeCatalogPage(value) {
3241
- if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
3536
+ if (!isRecord3(value)) return { nextCursor: null, hasMore: false };
3242
3537
  return {
3243
3538
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
3244
3539
  hasMore: value.hasMore === true
3245
3540
  };
3246
3541
  }
3247
3542
  function normalizeServiceCatalogEntry(item) {
3248
- if (!isRecord2(item)) return null;
3543
+ if (!isRecord3(item)) return null;
3249
3544
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
3250
3545
  if (!appId) return null;
3251
3546
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -3503,7 +3798,7 @@ var INVERSE_OPERATIONS = /* @__PURE__ */ new Map([
3503
3798
  ["restore", "archive"],
3504
3799
  ["unpublish", "publish"]
3505
3800
  ]);
3506
- function isRecord3(value) {
3801
+ function isRecord4(value) {
3507
3802
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
3508
3803
  }
3509
3804
  function nonEmptyStrings(value) {
@@ -3541,10 +3836,10 @@ function collectionFor(action, resource) {
3541
3836
  return resource.endsWith("s") ? resource : `${resource}s`;
3542
3837
  }
3543
3838
  function isConcreteOutputSchema(schema) {
3544
- if (!isRecord3(schema)) return false;
3839
+ if (!isRecord4(schema)) return false;
3545
3840
  if (typeof schema.$ref === "string" && schema.$ref.trim()) return true;
3546
- if (schema.type === "array" && isRecord3(schema.items) && Object.keys(schema.items).length > 0) return true;
3547
- if (isRecord3(schema.properties) && Object.keys(schema.properties).length > 0) return true;
3841
+ if (schema.type === "array" && isRecord4(schema.items) && Object.keys(schema.items).length > 0) return true;
3842
+ if (isRecord4(schema.properties) && Object.keys(schema.properties).length > 0) return true;
3548
3843
  return ["oneOf", "anyOf", "allOf"].some((key) => Array.isArray(schema[key]) && schema[key].some(isConcreteOutputSchema));
3549
3844
  }
3550
3845
  function resourceSchema(resource) {
@@ -3625,7 +3920,7 @@ function derivedOutputSchema(action) {
3625
3920
  };
3626
3921
  }
3627
3922
  function sampleForSchema(schema, depth = 0) {
3628
- if (!isRecord3(schema) || depth > 5) return null;
3923
+ if (!isRecord4(schema) || depth > 5) return null;
3629
3924
  if (schema.const !== void 0) return schema.const;
3630
3925
  if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
3631
3926
  if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return sampleForSchema(schema.oneOf[0], depth + 1);
@@ -3639,8 +3934,8 @@ function sampleForSchema(schema, depth = 0) {
3639
3934
  return schema.minLength > 1 ? "example".padEnd(schema.minLength, "x") : "example";
3640
3935
  }
3641
3936
  if (schema.type === "null") return null;
3642
- if (schema.type === "object" || isRecord3(schema.properties)) {
3643
- const properties = isRecord3(schema.properties) ? schema.properties : {};
3937
+ if (schema.type === "object" || isRecord4(schema.properties)) {
3938
+ const properties = isRecord4(schema.properties) ? schema.properties : {};
3644
3939
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
3645
3940
  const entries = Object.entries(properties).filter(([key]) => required.has(key)).map(([key, child]) => [key, sampleForSchema(child, depth + 1)]);
3646
3941
  return Object.fromEntries(entries);
@@ -3690,21 +3985,32 @@ function defaultErrors(action) {
3690
3985
  const errors = [
3691
3986
  { status: 401, code: "authentication_required", recovery: "Authenticate again or provide a valid Topolo API key." },
3692
3987
  { status: 403, code: "permission_denied", recovery: `Grant the credential permission ${action.permission || action.permissionName || "required by this action"} and the required resource binding.` },
3693
- { status: 422, code: "invalid_input", recovery: "Correct the payload using inputSchema, then validate before retrying." }
3988
+ { status: 429, code: "rate_limited", recovery: "Wait for Retry-After when present, then retry without parallel duplicate calls." },
3989
+ { status: 422, code: "invalid_input", recovery: "Correct the payload using inputSchema, then validate before retrying." },
3990
+ { status: 503, code: "dependency_unavailable", recovery: "Retry the action. If it continues, inspect the structured error details using the request ID." }
3694
3991
  ];
3695
3992
  if (action.method !== "GET") {
3696
3993
  errors.push({ status: 409, code: "state_conflict", recovery: "Read the current resource state, reconcile the intended change, and retry only if still valid." });
3697
3994
  }
3698
3995
  return errors;
3699
3996
  }
3997
+ function mergeErrors(defaults, declared) {
3998
+ const merged = [...defaults];
3999
+ for (const error of Array.isArray(declared) ? declared : []) {
4000
+ const index = merged.findIndex((candidate) => candidate.status === error?.status && candidate.code === error?.code);
4001
+ if (index === -1) merged.push(error);
4002
+ else merged[index] = error;
4003
+ }
4004
+ return merged;
4005
+ }
3700
4006
  function enrichActionContract(manifest, action, allActions = manifest.actions || []) {
3701
4007
  const appId = String(manifest.appId || manifest.app_id || "").trim();
3702
4008
  const service = slugify(manifest.slug || appId.replace(/^app_/, "").replaceAll("_", "-") || manifest.name);
3703
4009
  const outputSchema = isConcreteOutputSchema(action.outputSchema) ? structuredClone(action.outputSchema) : derivedOutputSchema(action);
3704
4010
  const resource = resourceFor(action);
3705
4011
  const readOnly = action.readOnly !== void 0 ? action.readOnly === true : action.method === "GET";
3706
- const existing = isRecord3(action.agentContract) ? structuredClone(action.agentContract) : {};
3707
- const recovery = isRecord3(existing.recovery) ? existing.recovery : recoveryFor(action, allActions, appId);
4012
+ const existing = isRecord4(action.agentContract) ? structuredClone(action.agentContract) : {};
4013
+ const recovery = isRecord4(existing.recovery) ? existing.recovery : recoveryFor(action, allActions, appId);
3708
4014
  const rollbackActionId = typeof existing.rollbackActionId === "string" && existing.rollbackActionId.trim() ? existing.rollbackActionId.trim() : recovery.mode === "compensating_action" ? recovery.actionId : void 0;
3709
4015
  const inputExample = sampleForSchema(action.inputSchema);
3710
4016
  const examples = Array.isArray(existing.examples) && existing.examples.length > 0 ? existing.examples : [{ input: inputExample ?? {}, expected: `A response matching the published outputSchema for ${action.name}.` }];
@@ -3716,9 +4022,9 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3716
4022
  ...existing,
3717
4023
  docsUrl,
3718
4024
  ...existingDocsUrl && existingDocsUrl !== docsUrl ? { guideUrl: existingDocsUrl } : {},
3719
- resource: isRecord3(existing.resource) ? existing.resource : { kind: resource, idPath: readOnly && ["list", "search"].includes(operationFor(action)) ? "$.items[].id" : "$.id" },
4025
+ resource: isRecord4(existing.resource) ? existing.resource : { kind: resource, idPath: readOnly && ["list", "search"].includes(operationFor(action)) ? "$.items[].id" : "$.id" },
3720
4026
  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.`],
3721
- errors: Array.isArray(existing.errors) && existing.errors.length > 0 ? existing.errors : defaultErrors(action),
4027
+ errors: mergeErrors(defaultErrors(action), existing.errors),
3722
4028
  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"}.`],
3723
4029
  examples,
3724
4030
  recovery,
@@ -3730,15 +4036,15 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3730
4036
  function actionContractCompleteness(action) {
3731
4037
  const contract = action?.agentContract;
3732
4038
  return {
3733
- inputSchema: isRecord3(action?.inputSchema),
4039
+ inputSchema: isRecord4(action?.inputSchema),
3734
4040
  outputSchema: isConcreteOutputSchema(action?.outputSchema),
3735
4041
  effects: nonEmptyStrings(contract?.effects),
3736
4042
  errors: Array.isArray(contract?.errors) && contract.errors.length > 0,
3737
4043
  docs: typeof contract?.docsUrl === "string" && contract.docsUrl.startsWith("https://"),
3738
4044
  examples: Array.isArray(contract?.examples) && contract.examples.length > 0,
3739
4045
  verification: nonEmptyStrings(contract?.verify),
3740
- recovery: isRecord3(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
3741
- agentContract: isRecord3(contract) && isRecord3(contract.resource) && nonEmptyStrings(contract.effects) && Array.isArray(contract.errors) && contract.errors.length > 0
4046
+ recovery: isRecord4(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
4047
+ agentContract: isRecord4(contract) && isRecord4(contract.resource) && nonEmptyStrings(contract.effects) && Array.isArray(contract.errors) && contract.errors.length > 0
3742
4048
  };
3743
4049
  }
3744
4050
 
@@ -4143,7 +4449,7 @@ function createTopoloWidgetError(error) {
4143
4449
  }
4144
4450
  function validateTopoloWidgetResponse(response) {
4145
4451
  const errors = [];
4146
- if (!isRecord4(response)) {
4452
+ if (!isRecord5(response)) {
4147
4453
  return { ok: false, errors: ["response must be an object"] };
4148
4454
  }
4149
4455
  if (typeof response.success !== "boolean") {
@@ -4162,7 +4468,7 @@ function validateTopoloWidgetResponse(response) {
4162
4468
  return { ok: errors.length === 0, errors };
4163
4469
  }
4164
4470
  function validateWidget(widget, path, errors) {
4165
- if (!isRecord4(widget)) {
4471
+ if (!isRecord5(widget)) {
4166
4472
  errors.push(`${path} must be an object`);
4167
4473
  return;
4168
4474
  }
@@ -4189,7 +4495,7 @@ function validateWidget(widget, path, errors) {
4189
4495
  case "chart":
4190
4496
  validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
4191
4497
  validateNonEmptyString(widget.title, `${path}.title`, errors);
4192
- if (!isRecord4(widget.data)) {
4498
+ if (!isRecord5(widget.data)) {
4193
4499
  errors.push(`${path}.data must be an object`);
4194
4500
  } else {
4195
4501
  validateArray2(widget.data.labels, `${path}.data.labels`, errors);
@@ -4205,7 +4511,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4205
4511
  errors.push(`${path} must be an object`);
4206
4512
  return;
4207
4513
  }
4208
- if (!isRecord4(snapshot)) {
4514
+ if (!isRecord5(snapshot)) {
4209
4515
  errors.push(`${path} must be an object`);
4210
4516
  return;
4211
4517
  }
@@ -4225,7 +4531,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4225
4531
  }
4226
4532
  snapshot.availableMetrics.forEach((metric, index) => {
4227
4533
  const metricPath = `${path}.availableMetrics[${index}]`;
4228
- if (!isRecord4(metric)) {
4534
+ if (!isRecord5(metric)) {
4229
4535
  errors.push(`${metricPath} must be an object`);
4230
4536
  return;
4231
4537
  }
@@ -4246,7 +4552,7 @@ function validateArray2(value, path, errors) {
4246
4552
  errors.push(`${path} must be an array`);
4247
4553
  }
4248
4554
  }
4249
- function isRecord4(value) {
4555
+ function isRecord5(value) {
4250
4556
  return typeof value === "object" && value !== null && !Array.isArray(value);
4251
4557
  }
4252
4558
  function toIsoTimestamp(input) {
@@ -4297,6 +4603,9 @@ export {
4297
4603
  defineTopoloStatsWidget,
4298
4604
  digestTopoloActionCatalog,
4299
4605
  enrichActionContract,
4606
+ parseTopoloActionUploadContract,
4607
+ prepareTopoloActionBase64UploadInput,
4608
+ prepareTopoloActionUploadInput,
4300
4609
  requirementsForApplication,
4301
4610
  resolveCatalogServiceUrl,
4302
4611
  resolveServiceUrl,