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