@topolo/sdk 0.9.6 → 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) {
@@ -3513,9 +3808,15 @@ function slugify(value) {
3513
3808
  return String(value || "").trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3514
3809
  }
3515
3810
  function singularize(value) {
3811
+ const irregular = /* @__PURE__ */ new Map([
3812
+ ["analyses", "analysis"],
3813
+ ["buses", "bus"],
3814
+ ["statuses", "status"]
3815
+ ]);
3816
+ if (irregular.has(value)) return irregular.get(value);
3516
3817
  if (value.endsWith("ies") && value.length > 3) return `${value.slice(0, -3)}y`;
3517
- if (value.endsWith("sses")) return value.slice(0, -2);
3518
- if (value.endsWith("s") && !value.endsWith("ss")) return value.slice(0, -1);
3818
+ if (/(?:ches|shes|sses|xes|zes)$/.test(value)) return value.slice(0, -2);
3819
+ if (value.endsWith("s") && !/(?:ss|us|is)$/.test(value)) return value.slice(0, -1);
3519
3820
  return value || "resource";
3520
3821
  }
3521
3822
  function operationFor(action) {
@@ -3529,11 +3830,16 @@ function resourceFor(action) {
3529
3830
  const pathToken = String(action.path || "").split("/").filter((token) => token && token !== "api" && !token.startsWith("{")).find((token) => !OPERATION_TOKENS.has(slugify(token)));
3530
3831
  return singularize(slugify(pathToken || "resource").replaceAll("-", "_"));
3531
3832
  }
3833
+ function collectionFor(action, resource) {
3834
+ const named = String(action.name || "").split(/[._:-]+/).filter(Boolean).find((token) => !OPERATION_TOKENS.has(token));
3835
+ if (named) return slugify(named).replaceAll("-", "_");
3836
+ return resource.endsWith("s") ? resource : `${resource}s`;
3837
+ }
3532
3838
  function isConcreteOutputSchema(schema) {
3533
- if (!isRecord3(schema)) return false;
3839
+ if (!isRecord4(schema)) return false;
3534
3840
  if (typeof schema.$ref === "string" && schema.$ref.trim()) return true;
3535
- if (schema.type === "array" && isRecord3(schema.items) && Object.keys(schema.items).length > 0) return true;
3536
- 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;
3537
3843
  return ["oneOf", "anyOf", "allOf"].some((key) => Array.isArray(schema[key]) && schema[key].some(isConcreteOutputSchema));
3538
3844
  }
3539
3845
  function resourceSchema(resource) {
@@ -3559,6 +3865,7 @@ function derivedOutputSchema(action) {
3559
3865
  "x-topolo-contract-precision": "structural"
3560
3866
  };
3561
3867
  if (operation === "list" || operation === "search") {
3868
+ const collection = collectionFor(action, resource);
3562
3869
  return {
3563
3870
  title: `${action.title || action.name} response`,
3564
3871
  description: `Collection response returned by ${action.name}. The service may return the collection directly or in an envelope.`,
@@ -3569,7 +3876,7 @@ function derivedOutputSchema(action) {
3569
3876
  properties: {
3570
3877
  data: { oneOf: [records, { type: "object", additionalProperties: true }] },
3571
3878
  items: records,
3572
- [resource.endsWith("s") ? resource : `${resource}s`]: records,
3879
+ [collection]: records,
3573
3880
  count: { type: "integer", minimum: 0 },
3574
3881
  nextCursor: { type: ["string", "null"] }
3575
3882
  },
@@ -3613,7 +3920,7 @@ function derivedOutputSchema(action) {
3613
3920
  };
3614
3921
  }
3615
3922
  function sampleForSchema(schema, depth = 0) {
3616
- if (!isRecord3(schema) || depth > 5) return null;
3923
+ if (!isRecord4(schema) || depth > 5) return null;
3617
3924
  if (schema.const !== void 0) return schema.const;
3618
3925
  if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
3619
3926
  if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return sampleForSchema(schema.oneOf[0], depth + 1);
@@ -3627,8 +3934,8 @@ function sampleForSchema(schema, depth = 0) {
3627
3934
  return schema.minLength > 1 ? "example".padEnd(schema.minLength, "x") : "example";
3628
3935
  }
3629
3936
  if (schema.type === "null") return null;
3630
- if (schema.type === "object" || isRecord3(schema.properties)) {
3631
- const properties = isRecord3(schema.properties) ? schema.properties : {};
3937
+ if (schema.type === "object" || isRecord4(schema.properties)) {
3938
+ const properties = isRecord4(schema.properties) ? schema.properties : {};
3632
3939
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
3633
3940
  const entries = Object.entries(properties).filter(([key]) => required.has(key)).map(([key, child]) => [key, sampleForSchema(child, depth + 1)]);
3634
3941
  return Object.fromEntries(entries);
@@ -3678,21 +3985,32 @@ function defaultErrors(action) {
3678
3985
  const errors = [
3679
3986
  { status: 401, code: "authentication_required", recovery: "Authenticate again or provide a valid Topolo API key." },
3680
3987
  { status: 403, code: "permission_denied", recovery: `Grant the credential permission ${action.permission || action.permissionName || "required by this action"} and the required resource binding.` },
3681
- { 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." }
3682
3991
  ];
3683
3992
  if (action.method !== "GET") {
3684
3993
  errors.push({ status: 409, code: "state_conflict", recovery: "Read the current resource state, reconcile the intended change, and retry only if still valid." });
3685
3994
  }
3686
3995
  return errors;
3687
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
+ }
3688
4006
  function enrichActionContract(manifest, action, allActions = manifest.actions || []) {
3689
4007
  const appId = String(manifest.appId || manifest.app_id || "").trim();
3690
4008
  const service = slugify(manifest.slug || appId.replace(/^app_/, "").replaceAll("_", "-") || manifest.name);
3691
4009
  const outputSchema = isConcreteOutputSchema(action.outputSchema) ? structuredClone(action.outputSchema) : derivedOutputSchema(action);
3692
4010
  const resource = resourceFor(action);
3693
4011
  const readOnly = action.readOnly !== void 0 ? action.readOnly === true : action.method === "GET";
3694
- const existing = isRecord3(action.agentContract) ? structuredClone(action.agentContract) : {};
3695
- 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);
3696
4014
  const rollbackActionId = typeof existing.rollbackActionId === "string" && existing.rollbackActionId.trim() ? existing.rollbackActionId.trim() : recovery.mode === "compensating_action" ? recovery.actionId : void 0;
3697
4015
  const inputExample = sampleForSchema(action.inputSchema);
3698
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}.` }];
@@ -3704,9 +4022,9 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3704
4022
  ...existing,
3705
4023
  docsUrl,
3706
4024
  ...existingDocsUrl && existingDocsUrl !== docsUrl ? { guideUrl: existingDocsUrl } : {},
3707
- 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" },
3708
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.`],
3709
- errors: Array.isArray(existing.errors) && existing.errors.length > 0 ? existing.errors : defaultErrors(action),
4027
+ errors: mergeErrors(defaultErrors(action), existing.errors),
3710
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"}.`],
3711
4029
  examples,
3712
4030
  recovery,
@@ -3718,15 +4036,15 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
3718
4036
  function actionContractCompleteness(action) {
3719
4037
  const contract = action?.agentContract;
3720
4038
  return {
3721
- inputSchema: isRecord3(action?.inputSchema),
4039
+ inputSchema: isRecord4(action?.inputSchema),
3722
4040
  outputSchema: isConcreteOutputSchema(action?.outputSchema),
3723
4041
  effects: nonEmptyStrings(contract?.effects),
3724
4042
  errors: Array.isArray(contract?.errors) && contract.errors.length > 0,
3725
4043
  docs: typeof contract?.docsUrl === "string" && contract.docsUrl.startsWith("https://"),
3726
4044
  examples: Array.isArray(contract?.examples) && contract.examples.length > 0,
3727
4045
  verification: nonEmptyStrings(contract?.verify),
3728
- recovery: isRecord3(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
3729
- 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
3730
4048
  };
3731
4049
  }
3732
4050
 
@@ -4131,7 +4449,7 @@ function createTopoloWidgetError(error) {
4131
4449
  }
4132
4450
  function validateTopoloWidgetResponse(response) {
4133
4451
  const errors = [];
4134
- if (!isRecord4(response)) {
4452
+ if (!isRecord5(response)) {
4135
4453
  return { ok: false, errors: ["response must be an object"] };
4136
4454
  }
4137
4455
  if (typeof response.success !== "boolean") {
@@ -4150,7 +4468,7 @@ function validateTopoloWidgetResponse(response) {
4150
4468
  return { ok: errors.length === 0, errors };
4151
4469
  }
4152
4470
  function validateWidget(widget, path, errors) {
4153
- if (!isRecord4(widget)) {
4471
+ if (!isRecord5(widget)) {
4154
4472
  errors.push(`${path} must be an object`);
4155
4473
  return;
4156
4474
  }
@@ -4177,7 +4495,7 @@ function validateWidget(widget, path, errors) {
4177
4495
  case "chart":
4178
4496
  validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
4179
4497
  validateNonEmptyString(widget.title, `${path}.title`, errors);
4180
- if (!isRecord4(widget.data)) {
4498
+ if (!isRecord5(widget.data)) {
4181
4499
  errors.push(`${path}.data must be an object`);
4182
4500
  } else {
4183
4501
  validateArray2(widget.data.labels, `${path}.data.labels`, errors);
@@ -4193,7 +4511,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4193
4511
  errors.push(`${path} must be an object`);
4194
4512
  return;
4195
4513
  }
4196
- if (!isRecord4(snapshot)) {
4514
+ if (!isRecord5(snapshot)) {
4197
4515
  errors.push(`${path} must be an object`);
4198
4516
  return;
4199
4517
  }
@@ -4213,7 +4531,7 @@ function validateSnapshotMetadata(snapshot, path, errors) {
4213
4531
  }
4214
4532
  snapshot.availableMetrics.forEach((metric, index) => {
4215
4533
  const metricPath = `${path}.availableMetrics[${index}]`;
4216
- if (!isRecord4(metric)) {
4534
+ if (!isRecord5(metric)) {
4217
4535
  errors.push(`${metricPath} must be an object`);
4218
4536
  return;
4219
4537
  }
@@ -4234,7 +4552,7 @@ function validateArray2(value, path, errors) {
4234
4552
  errors.push(`${path} must be an array`);
4235
4553
  }
4236
4554
  }
4237
- function isRecord4(value) {
4555
+ function isRecord5(value) {
4238
4556
  return typeof value === "object" && value !== null && !Array.isArray(value);
4239
4557
  }
4240
4558
  function toIsoTimestamp(input) {
@@ -4285,6 +4603,9 @@ export {
4285
4603
  defineTopoloStatsWidget,
4286
4604
  digestTopoloActionCatalog,
4287
4605
  enrichActionContract,
4606
+ parseTopoloActionUploadContract,
4607
+ prepareTopoloActionBase64UploadInput,
4608
+ prepareTopoloActionUploadInput,
4288
4609
  requirementsForApplication,
4289
4610
  resolveCatalogServiceUrl,
4290
4611
  resolveServiceUrl,