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