@topolo/cli 0.6.4 → 0.6.5

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +401 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -109,9 +109,13 @@ this too:
109
109
 
110
110
  ```bash
111
111
  topolo actions call app_topolo_mail.messages.send --data '{"mailboxId":"primary","subject":"Hello"}' --confirm
112
+ topolo actions upload app_topolo_brand.brand_assets.upload --file ./logo.png --data '{"assetId":"asset_1"}' --confirm
113
+ topolo actions upload app_topolo_chat.uploads.request --file ./launch.mp4 --data '{}' --complete-data '{"conversationKind":"channel","conversationId":"channel_1"}' --confirm
112
114
  topolo api mail POST /api/mail/mailboxes/primary/messages --data '{"subject":"Hello"}' --confirm
113
115
  ```
114
116
 
117
+ `actions upload` requires the resolved action to publish an upload contract. It reads the file, infers common MIME types or accepts `--content-type`, then follows either the declared inline-base64 mapping or the complete initiate/upload/finish URL-session flow. `--complete-data` supplies only fields required by a declared completion action; the upload/session id is mapped from the initiation response by contract.
118
+
115
119
  ## Commands
116
120
 
117
121
  ### `topolo whoami`
package/dist/index.js CHANGED
@@ -37,19 +37,54 @@ var TopoloSdkError = class extends Error {
37
37
  }
38
38
  };
39
39
  var TopoloAuthError = class extends TopoloSdkError {
40
- constructor(message, code = "auth_error") {
40
+ status;
41
+ body;
42
+ service;
43
+ path;
44
+ requestId;
45
+ retryAfter;
46
+ constructor(message, code = "auth_error", options = {}) {
41
47
  super(code, message);
42
48
  this.name = "TopoloAuthError";
49
+ this.status = options.status ?? 401;
50
+ this.body = options.body;
51
+ this.service = options.service;
52
+ this.path = options.path;
53
+ this.requestId = options.requestId;
54
+ this.retryAfter = options.retryAfter;
43
55
  }
44
56
  };
45
57
  var TopoloPermissionError = class extends TopoloSdkError {
46
58
  required;
47
- constructor(message, required) {
59
+ status;
60
+ body;
61
+ service;
62
+ path;
63
+ requestId;
64
+ retryAfter;
65
+ constructor(message, required, options = {}) {
48
66
  super("permission_denied", message);
49
67
  this.name = "TopoloPermissionError";
50
68
  this.required = required;
69
+ this.status = options.status ?? 403;
70
+ this.body = options.body;
71
+ this.service = options.service;
72
+ this.path = options.path;
73
+ this.requestId = options.requestId;
74
+ this.retryAfter = options.retryAfter;
51
75
  }
52
76
  };
77
+ function extractHttpErrorCode(body) {
78
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
79
+ const record4 = body;
80
+ if (typeof record4.code === "string" && record4.code.trim()) return record4.code.trim();
81
+ const nested = record4.error;
82
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
83
+ const code = nested.code;
84
+ if (typeof code === "string" && code.trim()) return code.trim();
85
+ }
86
+ return "http_error";
87
+ }
53
88
  var TopoloConfirmationRequiredError = class extends TopoloSdkError {
54
89
  constructor(message) {
55
90
  super("confirmation_required", message);
@@ -61,13 +96,17 @@ var TopoloHttpError = class extends TopoloSdkError {
61
96
  body;
62
97
  service;
63
98
  path;
64
- constructor(service, path8, status, body, message) {
65
- super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
99
+ requestId;
100
+ retryAfter;
101
+ constructor(service, path8, status, body, message, options = {}) {
102
+ super(extractHttpErrorCode(body), message ?? `HTTP ${status} from ${service}${path8}`);
66
103
  this.name = "TopoloHttpError";
67
104
  this.status = status;
68
105
  this.body = body;
69
106
  this.service = service;
70
107
  this.path = path8;
108
+ this.requestId = options.requestId;
109
+ this.retryAfter = options.retryAfter;
71
110
  }
72
111
  };
73
112
  var APP_API_URLS = {
@@ -835,7 +874,9 @@ var APPLICATIONS = {
835
874
  "domains": [
836
875
  "forms.topolo.app"
837
876
  ],
838
- "routes": [],
877
+ "routes": [
878
+ "forms.topolo.app"
879
+ ],
839
880
  "appId": "forms"
840
881
  },
841
882
  {
@@ -1366,7 +1407,9 @@ var APPLICATIONS = {
1366
1407
  "domains": [
1367
1408
  "quro.topolo.app"
1368
1409
  ],
1369
- "routes": [],
1410
+ "routes": [
1411
+ "quro.topolo.app"
1412
+ ],
1370
1413
  "appId": null
1371
1414
  }
1372
1415
  ]
@@ -1650,7 +1693,9 @@ var APPLICATIONS = {
1650
1693
  "domains": [
1651
1694
  "survey.topolo.app"
1652
1695
  ],
1653
- "routes": [],
1696
+ "routes": [
1697
+ "survey.topolo.app"
1698
+ ],
1654
1699
  "appId": "survey"
1655
1700
  },
1656
1701
  {
@@ -2441,6 +2486,166 @@ function isMultipleOf(value, divisor) {
2441
2486
  const quotient = value / divisor;
2442
2487
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
2443
2488
  }
2489
+ function parseTopoloActionUploadContract(value) {
2490
+ if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
2491
+ throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
2492
+ }
2493
+ const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
2494
+ const fileNameField = requiredField(value.fileNameField, "fileNameField");
2495
+ const maxBytes = Number(value.maxBytes);
2496
+ const acceptedContentTypes = Array.isArray(value.acceptedContentTypes) ? value.acceptedContentTypes.map((item) => String(item).trim().toLowerCase()).filter(Boolean) : [];
2497
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0 || acceptedContentTypes.length === 0) {
2498
+ throw new TopoloSdkError("invalid_upload_contract", "The action upload contract must declare maxBytes and acceptedContentTypes.");
2499
+ }
2500
+ if (value.profile === "inline-base64") {
2501
+ return {
2502
+ profile: "inline-base64",
2503
+ contentField: requiredField(value.contentField, "contentField"),
2504
+ contentTypeField,
2505
+ fileNameField,
2506
+ maxBytes,
2507
+ acceptedContentTypes
2508
+ };
2509
+ }
2510
+ const uploadMethod = value.uploadMethod === "PUT" || value.uploadMethod === "POST" ? value.uploadMethod : null;
2511
+ const uploadUrlPath = requiredPath(value.uploadUrlPath, "uploadUrlPath");
2512
+ const complete = value.complete === void 0 ? void 0 : parseCompleteContract(value.complete);
2513
+ if (!uploadMethod) throw new TopoloSdkError("invalid_upload_contract", "The action upload contract has an invalid uploadMethod.");
2514
+ return {
2515
+ profile: "url-session",
2516
+ fileNameField,
2517
+ contentTypeField,
2518
+ sizeField: requiredField(value.sizeField, "sizeField"),
2519
+ maxBytes,
2520
+ acceptedContentTypes,
2521
+ uploadUrlPath,
2522
+ uploadMethod,
2523
+ ...complete ? { complete } : {}
2524
+ };
2525
+ }
2526
+ function prepareTopoloActionUploadInput(contractValue, content, input3, options) {
2527
+ const contract = parseTopoloActionUploadContract(contractValue);
2528
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2529
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
2530
+ if (bytes.byteLength === 0) {
2531
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2532
+ }
2533
+ if (bytes.byteLength > contract.maxBytes) {
2534
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2535
+ }
2536
+ return mapUploadInput(contract, bytesToBase64(bytes), input3, options);
2537
+ }
2538
+ function prepareTopoloActionBase64UploadInput(contractValue, contentBase64, input3, options) {
2539
+ const contract = parseTopoloActionUploadContract(contractValue);
2540
+ if (contract.profile !== "inline-base64") throw unsupportedInlineProfile();
2541
+ if (!isCanonicalBase64(contentBase64)) {
2542
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2543
+ }
2544
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2545
+ const byteLength = contentBase64.length / 4 * 3 - padding;
2546
+ if (byteLength === 0) throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2547
+ if (byteLength > contract.maxBytes) {
2548
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2549
+ }
2550
+ return mapUploadInput(contract, contentBase64, input3, options);
2551
+ }
2552
+ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input3, options) {
2553
+ const contract = parseTopoloActionUploadContract(contractValue);
2554
+ if (contract.profile !== "url-session") {
2555
+ throw new TopoloSdkError("invalid_upload_contract", "This action does not publish a URL-session upload contract.");
2556
+ }
2557
+ validateUploadMetadata(contract, byteLength, options);
2558
+ return {
2559
+ contract,
2560
+ input: {
2561
+ ...input3,
2562
+ [contract.fileNameField]: options.fileName.trim(),
2563
+ [contract.contentTypeField]: options.contentType.trim().toLowerCase(),
2564
+ [contract.sizeField]: byteLength
2565
+ }
2566
+ };
2567
+ }
2568
+ function topoloUploadResponseValue(response, path8) {
2569
+ return path8.split(".").reduce((value, segment) => isRecord2(value) ? value[segment] : void 0, response);
2570
+ }
2571
+ function decodeTopoloActionBase64(contentBase64) {
2572
+ if (!isCanonicalBase64(contentBase64)) {
2573
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must be canonical base64.");
2574
+ }
2575
+ const binary = atob(contentBase64);
2576
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
2577
+ }
2578
+ function mapUploadInput(contract, contentBase64, input3, options) {
2579
+ validateUploadMetadata(contract, Math.max(1, decodedBase64Size(contentBase64)), options);
2580
+ const fileName = options.fileName.trim();
2581
+ const contentType = options.contentType.trim().toLowerCase();
2582
+ return {
2583
+ ...input3,
2584
+ [contract.contentField]: contentBase64,
2585
+ [contract.contentTypeField]: contentType,
2586
+ [contract.fileNameField]: fileName
2587
+ };
2588
+ }
2589
+ function validateUploadMetadata(contract, byteLength, options) {
2590
+ const fileName = options.fileName.trim();
2591
+ const contentType = options.contentType.trim().toLowerCase();
2592
+ if (!fileName) throw new TopoloSdkError("invalid_upload_file_name", "Upload fileName must not be empty.");
2593
+ if (!Number.isSafeInteger(byteLength) || byteLength <= 0) {
2594
+ throw new TopoloSdkError("invalid_upload_content", "Upload content must not be empty.");
2595
+ }
2596
+ if (byteLength > contract.maxBytes) {
2597
+ throw new TopoloSdkError("upload_too_large", `Upload content exceeds the ${contract.maxBytes}-byte action limit.`);
2598
+ }
2599
+ if (!matchesContentType(contentType, contract.acceptedContentTypes)) {
2600
+ throw new TopoloSdkError("unsupported_upload_content_type", `Content type "${contentType}" is not accepted by this action.`);
2601
+ }
2602
+ }
2603
+ function decodedBase64Size(contentBase64) {
2604
+ const padding = contentBase64.endsWith("==") ? 2 : contentBase64.endsWith("=") ? 1 : 0;
2605
+ return contentBase64.length / 4 * 3 - padding;
2606
+ }
2607
+ function parseCompleteContract(value) {
2608
+ if (!isRecord2(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
2609
+ return {
2610
+ actionId: requiredPath(value.actionId, "complete.actionId"),
2611
+ inputField: requiredField(value.inputField, "complete.inputField"),
2612
+ responsePath: requiredPath(value.responsePath, "complete.responsePath")
2613
+ };
2614
+ }
2615
+ function requiredPath(value, name) {
2616
+ const path8 = typeof value === "string" ? value.trim() : "";
2617
+ if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path8)) {
2618
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2619
+ }
2620
+ return path8;
2621
+ }
2622
+ function unsupportedInlineProfile() {
2623
+ return new TopoloSdkError("invalid_upload_contract", "This action does not publish an inline-base64 upload contract.");
2624
+ }
2625
+ function isCanonicalBase64(value) {
2626
+ 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);
2627
+ }
2628
+ function requiredField(value, name) {
2629
+ const field = typeof value === "string" ? value.trim() : "";
2630
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field)) {
2631
+ throw new TopoloSdkError("invalid_upload_contract", `The action upload contract has an invalid ${name}.`);
2632
+ }
2633
+ return field;
2634
+ }
2635
+ function matchesContentType(contentType, accepted) {
2636
+ return accepted.some((candidate) => candidate === "*/*" || candidate === contentType || candidate.endsWith("/*") && contentType.startsWith(candidate.slice(0, -1)));
2637
+ }
2638
+ function bytesToBase64(bytes) {
2639
+ let binary = "";
2640
+ const chunkSize = 32768;
2641
+ for (let offset = 0; offset < bytes.length; offset += chunkSize) {
2642
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
2643
+ }
2644
+ return btoa(binary);
2645
+ }
2646
+ function isRecord2(value) {
2647
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2648
+ }
2444
2649
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2445
2650
  var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2446
2651
  var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
@@ -2517,6 +2722,9 @@ var TopoloClient = class _TopoloClient {
2517
2722
  if (v !== void 0) url.searchParams.set(k, String(v));
2518
2723
  }
2519
2724
  }
2725
+ if (opts.body !== void 0 && opts.rawBody !== void 0) {
2726
+ throw new TopoloSdkError("invalid_request_body", "body and rawBody are mutually exclusive.");
2727
+ }
2520
2728
  const headers = new Headers();
2521
2729
  headers.set("Accept", "application/json");
2522
2730
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
@@ -2551,7 +2759,7 @@ var TopoloClient = class _TopoloClient {
2551
2759
  return await this.fetchImpl(urlStr, {
2552
2760
  method,
2553
2761
  headers: new Headers(headers),
2554
- body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
2762
+ body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
2555
2763
  signal
2556
2764
  });
2557
2765
  } finally {
@@ -2599,14 +2807,32 @@ var TopoloClient = class _TopoloClient {
2599
2807
  error: message,
2600
2808
  status: res.status
2601
2809
  });
2602
- if (res.status === 401) throw new TopoloAuthError(describeError(parsed, "Unauthorized"));
2810
+ const errorContext = {
2811
+ status: res.status,
2812
+ body: parsed,
2813
+ service: resolvedService.serviceKey,
2814
+ path: opts.path,
2815
+ requestId: res.headers.get("X-Topolo-Request-Id") || requestId,
2816
+ retryAfter: res.headers.get("Retry-After") || void 0
2817
+ };
2818
+ if (res.status === 401) {
2819
+ throw new TopoloAuthError(describeError(parsed, "Unauthorized"), "auth_error", errorContext);
2820
+ }
2603
2821
  if (res.status === 403) {
2604
2822
  throw new TopoloPermissionError(
2605
2823
  describeError(parsed, "Permission denied"),
2606
- extractRequired(parsed)
2824
+ extractRequired(parsed),
2825
+ errorContext
2607
2826
  );
2608
2827
  }
2609
- throw new TopoloHttpError(resolvedService.serviceKey, opts.path, res.status, parsed);
2828
+ throw new TopoloHttpError(
2829
+ resolvedService.serviceKey,
2830
+ opts.path,
2831
+ res.status,
2832
+ parsed,
2833
+ message,
2834
+ errorContext
2835
+ );
2610
2836
  }
2611
2837
  this.emit({
2612
2838
  phase: "response",
@@ -2765,7 +2991,7 @@ var TopoloClient = class _TopoloClient {
2765
2991
  }
2766
2992
  const entries = [];
2767
2993
  for (const item of raw) {
2768
- if (!isRecord2(item)) continue;
2994
+ if (!isRecord3(item)) continue;
2769
2995
  const s = item;
2770
2996
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
2771
2997
  if (!appId) continue;
@@ -2840,8 +3066,8 @@ var TopoloClient = class _TopoloClient {
2840
3066
  "app_topolo_auth.service_api_key_resources.list",
2841
3067
  { appId: service.appId }
2842
3068
  );
2843
- const payload = isRecord2(response) && isRecord2(response.data) ? response.data : response;
2844
- if (!isRecord2(payload) || !Array.isArray(payload.resources)) {
3069
+ const payload = isRecord3(response) && isRecord3(response.data) ? response.data : response;
3070
+ if (!isRecord3(payload) || !Array.isArray(payload.resources)) {
2845
3071
  throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
2846
3072
  }
2847
3073
  return {
@@ -2947,6 +3173,71 @@ var TopoloClient = class _TopoloClient {
2947
3173
  const entry = await this.getAction(action);
2948
3174
  return this.executeResolvedAction(entry, input3, options);
2949
3175
  }
3176
+ async uploadAction(action, content, input3, options) {
3177
+ const entry = await this.getAction(action);
3178
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3179
+ if (contract.profile === "url-session") {
3180
+ const bytes = content instanceof Uint8Array ? content : new Uint8Array(content);
3181
+ return this.executeUrlSessionUpload(entry, contract, bytes, input3, options);
3182
+ }
3183
+ const uploadInput = prepareTopoloActionUploadInput(entry.agentContract?.upload, content, input3, options);
3184
+ return this.executeResolvedAction(entry, uploadInput, options);
3185
+ }
3186
+ async uploadActionBase64(action, contentBase64, input3, options) {
3187
+ const entry = await this.getAction(action);
3188
+ const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
3189
+ if (contract.profile === "url-session") {
3190
+ return this.executeUrlSessionUpload(entry, contract, decodeTopoloActionBase64(contentBase64), input3, options);
3191
+ }
3192
+ const uploadInput = prepareTopoloActionBase64UploadInput(entry.agentContract?.upload, contentBase64, input3, options);
3193
+ return this.executeResolvedAction(entry, uploadInput, options);
3194
+ }
3195
+ async executeUrlSessionUpload(entry, contract, bytes, input3, options) {
3196
+ const prepared = prepareTopoloUrlSessionUploadInput(contract, bytes.byteLength, input3, options);
3197
+ const initiation = await this.executeResolvedAction(entry, prepared.input, options);
3198
+ const uploadUrl = topoloUploadResponseValue(initiation, contract.uploadUrlPath);
3199
+ if (typeof uploadUrl !== "string" || !uploadUrl.trim()) {
3200
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.uploadUrlPath}.`);
3201
+ }
3202
+ await this.putUrlSessionContent(entry, uploadUrl, contract.uploadMethod, bytes, options);
3203
+ if (!contract.complete) return initiation;
3204
+ const completionValue = topoloUploadResponseValue(initiation, contract.complete.responsePath);
3205
+ if (typeof completionValue !== "string" || !completionValue.trim()) {
3206
+ throw new TopoloSdkError("invalid_upload_response", `Upload action response is missing ${contract.complete.responsePath}.`);
3207
+ }
3208
+ const completion = await this.getAction(contract.complete.actionId);
3209
+ return this.executeResolvedAction(completion, {
3210
+ ...options.completionInput ?? {},
3211
+ [contract.complete.inputField]: completionValue
3212
+ }, options);
3213
+ }
3214
+ async putUrlSessionContent(entry, uploadUrl, method, bytes, options) {
3215
+ const service = entry.appSlug || entry.appId;
3216
+ const resolved = entry.apiBaseUrl ? { serviceKey: normalizeLookupKey(service), baseUrl: requireHttpOrigin(entry.apiBaseUrl) } : await this.resolveRequestService(service);
3217
+ const target = new URL(uploadUrl, resolved.baseUrl.endsWith("/") ? resolved.baseUrl : `${resolved.baseUrl}/`);
3218
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3219
+ if (target.origin === new URL(resolved.baseUrl).origin) {
3220
+ await this.request({
3221
+ service,
3222
+ baseUrl: resolved.baseUrl,
3223
+ path: target.toString(),
3224
+ method,
3225
+ rawBody: body,
3226
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3227
+ resource: options.resource,
3228
+ confirm: options.confirm
3229
+ });
3230
+ return;
3231
+ }
3232
+ const response = await this.fetchImpl(target, {
3233
+ method,
3234
+ headers: { "Content-Type": options.contentType.trim().toLowerCase() },
3235
+ body
3236
+ });
3237
+ if (!response.ok) {
3238
+ throw new TopoloHttpError(service, target.pathname, response.status, await response.text(), `Upload transport failed with HTTP ${response.status}.`);
3239
+ }
3240
+ }
2950
3241
  async validateActionInput(action, input3 = {}) {
2951
3242
  const entry = await this.getAction(action);
2952
3243
  return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input3) };
@@ -3105,7 +3396,7 @@ function resolveConfiguredAppId(slug, appIds) {
3105
3396
  return null;
3106
3397
  }
3107
3398
  function normalizeApiKeyResourceMetadata(input3) {
3108
- if (!isRecord2(input3)) return null;
3399
+ if (!isRecord3(input3)) return null;
3109
3400
  const resourceType = readString(input3.resourceType ?? input3.resource_type);
3110
3401
  const label = readString(input3.label);
3111
3402
  const emptyLabel = readString(input3.emptyLabel ?? input3.empty_label);
@@ -3139,7 +3430,7 @@ function normalizeApiKeyResourceMetadataList(input3) {
3139
3430
  return resources;
3140
3431
  }
3141
3432
  function normalizeBindableResource(input3) {
3142
- if (!isRecord2(input3)) return null;
3433
+ if (!isRecord3(input3)) return null;
3143
3434
  const resourceType = readString(input3.resourceType ?? input3.resource_type);
3144
3435
  const resourceId = readString(input3.resourceId ?? input3.resource_id);
3145
3436
  if (!resourceType || !resourceId) return null;
@@ -3150,13 +3441,13 @@ function normalizeBindableResource(input3) {
3150
3441
  slug: readOptionalString(input3.slug),
3151
3442
  status: readOptionalString(input3.status),
3152
3443
  organizationId: readOptionalString(input3.organizationId ?? input3.organization_id),
3153
- metadata: isRecord2(input3.metadata) ? input3.metadata : {}
3444
+ metadata: isRecord3(input3.metadata) ? input3.metadata : {}
3154
3445
  };
3155
3446
  }
3156
3447
  function normalizeResourceBindings(input3) {
3157
3448
  if (!Array.isArray(input3)) return [];
3158
3449
  return input3.flatMap((binding) => {
3159
- if (!isRecord2(binding)) return [];
3450
+ if (!isRecord3(binding)) return [];
3160
3451
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
3161
3452
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
3162
3453
  return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
@@ -3169,7 +3460,7 @@ function readOptionalString(value) {
3169
3460
  const normalized = readString(value);
3170
3461
  return normalized || null;
3171
3462
  }
3172
- function isRecord2(value) {
3463
+ function isRecord3(value) {
3173
3464
  return typeof value === "object" && value !== null && !Array.isArray(value);
3174
3465
  }
3175
3466
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
@@ -3241,14 +3532,14 @@ function catalogSearchQuery(options) {
3241
3532
  return query;
3242
3533
  }
3243
3534
  function normalizeCatalogPage(value) {
3244
- if (!isRecord2(value)) return { nextCursor: null, hasMore: false };
3535
+ if (!isRecord3(value)) return { nextCursor: null, hasMore: false };
3245
3536
  return {
3246
3537
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
3247
3538
  hasMore: value.hasMore === true
3248
3539
  };
3249
3540
  }
3250
3541
  function normalizeServiceCatalogEntry(item) {
3251
- if (!isRecord2(item)) return null;
3542
+ if (!isRecord3(item)) return null;
3252
3543
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
3253
3544
  if (!appId) return null;
3254
3545
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -3572,23 +3863,50 @@ var TopoloSdkError2 = class extends Error {
3572
3863
  }
3573
3864
  };
3574
3865
  var TopoloAuthError2 = class extends TopoloSdkError2 {
3575
- constructor(message, code = "auth_error") {
3866
+ status;
3867
+ body;
3868
+ service;
3869
+ path;
3870
+ requestId;
3871
+ retryAfter;
3872
+ constructor(message, code = "auth_error", options = {}) {
3576
3873
  super(code, message);
3577
3874
  this.name = "TopoloAuthError";
3875
+ this.status = options.status ?? 401;
3876
+ this.body = options.body;
3877
+ this.service = options.service;
3878
+ this.path = options.path;
3879
+ this.requestId = options.requestId;
3880
+ this.retryAfter = options.retryAfter;
3578
3881
  }
3579
3882
  };
3883
+ function extractHttpErrorCode2(body) {
3884
+ if (!body || typeof body !== "object" || Array.isArray(body)) return "http_error";
3885
+ const record4 = body;
3886
+ if (typeof record4.code === "string" && record4.code.trim()) return record4.code.trim();
3887
+ const nested = record4.error;
3888
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
3889
+ const code = nested.code;
3890
+ if (typeof code === "string" && code.trim()) return code.trim();
3891
+ }
3892
+ return "http_error";
3893
+ }
3580
3894
  var TopoloHttpError2 = class extends TopoloSdkError2 {
3581
3895
  status;
3582
3896
  body;
3583
3897
  service;
3584
3898
  path;
3585
- constructor(service, path8, status, body, message) {
3586
- super("http_error", message ?? `HTTP ${status} from ${service}${path8}`);
3899
+ requestId;
3900
+ retryAfter;
3901
+ constructor(service, path8, status, body, message, options = {}) {
3902
+ super(extractHttpErrorCode2(body), message ?? `HTTP ${status} from ${service}${path8}`);
3587
3903
  this.name = "TopoloHttpError";
3588
3904
  this.status = status;
3589
3905
  this.body = body;
3590
3906
  this.service = service;
3591
3907
  this.path = path8;
3908
+ this.requestId = options.requestId;
3909
+ this.retryAfter = options.retryAfter;
3592
3910
  }
3593
3911
  };
3594
3912
  var APP_API_URLS2 = {
@@ -4065,6 +4383,10 @@ function fail(err, mode) {
4065
4383
  process.stderr.write(`Error: ${payload.message}
4066
4384
  `);
4067
4385
  if (payload.code) process.stderr.write(`Code: ${payload.code}
4386
+ `);
4387
+ if (payload.status) process.stderr.write(`HTTP: ${payload.status}
4388
+ `);
4389
+ if (payload.requestId) process.stderr.write(`Request: ${payload.requestId}
4068
4390
  `);
4069
4391
  if (payload.hint) process.stderr.write(`Hint: ${payload.hint}
4070
4392
  `);
@@ -4083,6 +4405,12 @@ function normalizeError(err) {
4083
4405
  return {
4084
4406
  message: err.message,
4085
4407
  code,
4408
+ status: err.status,
4409
+ service: err.service,
4410
+ path: err.path,
4411
+ requestId: err.requestId,
4412
+ retryAfter: err.retryAfter,
4413
+ details: err.body,
4086
4414
  hint: code ? hintMap[code] : void 0,
4087
4415
  exitCode: code === "auth_error" || code === "confirmation_required" ? 2 : code === "permission_denied" ? 3 : code === "immutable_drift" ? 4 : 1
4088
4416
  };
@@ -4451,6 +4779,7 @@ function formatPretty(info) {
4451
4779
 
4452
4780
  // src/commands/actions.ts
4453
4781
  import { readFileSync as readFileSync2 } from "fs";
4782
+ import { basename, extname } from "path";
4454
4783
 
4455
4784
  // src/commands/api.ts
4456
4785
  var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
@@ -4605,6 +4934,30 @@ function registerActionsCommand(program2) {
4605
4934
  fail(err, mode);
4606
4935
  }
4607
4936
  });
4937
+ actions.command("upload <action>").description("Upload one local file through an action-declared upload contract").requiredOption("--file <path>", "Local file to upload").option("--content-type <type>", "MIME type; inferred for common file extensions").option("-d, --data <json-or-@file>", "Additional JSON action input or @path to a JSON file", "{}").option("--complete-data <json-or-@file>", "Additional input for a declared completion action", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating upload action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4938
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4939
+ try {
4940
+ const topolo = await requiredClient();
4941
+ const contentType = opts.contentType?.trim().toLowerCase() || inferContentType(opts.file);
4942
+ if (!contentType) throw new Error("--content-type is required for this file extension");
4943
+ const resource = parseResourceOption(opts.resourceType, opts.resourceId);
4944
+ const result = await topolo.client.uploadAction(
4945
+ action,
4946
+ readFileSync2(opts.file),
4947
+ parseJsonObject(opts.data || "{}"),
4948
+ {
4949
+ fileName: basename(opts.file),
4950
+ contentType,
4951
+ completionInput: parseJsonObject(opts.completeData || "{}"),
4952
+ confirm: opts.confirm === true,
4953
+ ...resource ? { resource } : {}
4954
+ }
4955
+ );
4956
+ emit(result, mode, () => JSON.stringify(result, null, 2));
4957
+ } catch (err) {
4958
+ fail(err, mode);
4959
+ }
4960
+ });
4608
4961
  actions.command("validate <action>").description("Validate action input locally against the published JSON Schema").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4609
4962
  const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4610
4963
  try {
@@ -4685,6 +5038,21 @@ function parseJsonObject(value) {
4685
5038
  }
4686
5039
  return parsed;
4687
5040
  }
5041
+ function inferContentType(path8) {
5042
+ return {
5043
+ ".avif": "image/avif",
5044
+ ".gif": "image/gif",
5045
+ ".heic": "image/heic",
5046
+ ".jpeg": "image/jpeg",
5047
+ ".jpg": "image/jpeg",
5048
+ ".mov": "video/quicktime",
5049
+ ".mp4": "video/mp4",
5050
+ ".pdf": "application/pdf",
5051
+ ".png": "image/png",
5052
+ ".svg": "image/svg+xml",
5053
+ ".webp": "image/webp"
5054
+ }[extname(path8).toLowerCase()] ?? null;
5055
+ }
4688
5056
  function formatActionRows(rows, nextCursor = null) {
4689
5057
  if (rows.length === 0) return "No actions available for this credential.";
4690
5058
  const serviceWidth = Math.max(7, ...rows.map((row) => (row.appSlug || row.appId).length));
@@ -11746,16 +12114,16 @@ function scanStagingProductionUrls(rootInput, allowPatterns) {
11746
12114
  const allowlist = allowPatterns.map((pattern) => new RegExp(pattern));
11747
12115
  const violations = [];
11748
12116
  function shouldSkipFile(filePath) {
11749
- const basename = path7.basename(filePath);
12117
+ const basename2 = path7.basename(filePath);
11750
12118
  const relativePath2 = path7.relative(root, filePath);
11751
- if (basename === "package-lock.json" || basename === "pnpm-lock.yaml" || basename === "yarn.lock") return true;
11752
- if (basename === "topolo.cloudcontrol.json") return true;
11753
- if (basename === "wrangler.toml" || basename === "wrangler.jsonc") return true;
11754
- if (basename === "consumer-graph.json") return true;
11755
- if (/^cloudcontrol-deployment-report-.+\.json$/.test(basename)) return true;
11756
- if (basename.endsWith(".generated.ts")) return true;
11757
- if (basename.endsWith(".test.ts") || basename.endsWith(".test.tsx") || basename.endsWith(".spec.ts")) return true;
11758
- if (basename === ".env.production") return true;
12119
+ if (basename2 === "package-lock.json" || basename2 === "pnpm-lock.yaml" || basename2 === "yarn.lock") return true;
12120
+ if (basename2 === "topolo.cloudcontrol.json") return true;
12121
+ if (basename2 === "wrangler.toml" || basename2 === "wrangler.jsonc") return true;
12122
+ if (basename2 === "consumer-graph.json") return true;
12123
+ if (/^cloudcontrol-deployment-report-.+\.json$/.test(basename2)) return true;
12124
+ if (basename2.endsWith(".generated.ts")) return true;
12125
+ if (basename2.endsWith(".test.ts") || basename2.endsWith(".test.tsx") || basename2.endsWith(".spec.ts")) return true;
12126
+ if (basename2 === ".env.production") return true;
11759
12127
  if (relativePath2 === path7.join("src", "environment.ts") || relativePath2 === path7.join("src", "environmentUrls.ts") || relativePath2.endsWith(path7.join("src", "environment.ts")) || relativePath2.endsWith(path7.join("src", "lib", "environment.ts")) || relativePath2 === path7.join("packages", "topolo-ui-kit", "src", "environmentUrls.ts")) return true;
11760
12128
  if (relativePath2.includes(`${path7.sep}test${path7.sep}`) || relativePath2.includes(`${path7.sep}tests${path7.sep}`) || relativePath2.includes(`${path7.sep}e2e${path7.sep}`)) return true;
11761
12129
  if (relativePath2.startsWith(`scripts${path7.sep}`) || relativePath2.includes(`${path7.sep}scripts${path7.sep}`)) return true;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
- "version": "0.6.4",
4
- "topoloPublishedVersion": "0.6.4",
3
+ "version": "0.6.5",
4
+ "topoloPublishedVersion": "0.6.5",
5
5
  "description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
6
6
  "type": "module",
7
7
  "bin": {