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