@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.
- package/README.md +4 -0
- package/dist/index.js +401 -33
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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(
|
|
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 (!
|
|
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 =
|
|
2844
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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:
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
3586
|
-
|
|
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
|
|
12117
|
+
const basename2 = path7.basename(filePath);
|
|
11750
12118
|
const relativePath2 = path7.relative(root, filePath);
|
|
11751
|
-
if (
|
|
11752
|
-
if (
|
|
11753
|
-
if (
|
|
11754
|
-
if (
|
|
11755
|
-
if (/^cloudcontrol-deployment-report-.+\.json$/.test(
|
|
11756
|
-
if (
|
|
11757
|
-
if (
|
|
11758
|
-
if (
|
|
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
|
-
"topoloPublishedVersion": "0.6.
|
|
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": {
|