@topolo/sdk 0.21.2 → 0.21.3
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/dist/index.cjs +246 -171
- package/dist/index.d.cts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +246 -171
- package/dist/node/credential-store.cjs +133 -74
- package/dist/node/credential-store.d.cts +51 -1
- package/dist/node/credential-store.d.ts +51 -1
- package/dist/node/credential-store.js +130 -72
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -18,60 +18,6 @@ function generateRequestId() {
|
|
|
18
18
|
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
// src/dpop.ts
|
|
22
|
-
var encoder = new TextEncoder();
|
|
23
|
-
function encodeBase64Url(value) {
|
|
24
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
25
|
-
let binary = "";
|
|
26
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
27
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
28
|
-
}
|
|
29
|
-
function encodeJson(value) {
|
|
30
|
-
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
31
|
-
}
|
|
32
|
-
async function sha256Base64Url(value) {
|
|
33
|
-
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
34
|
-
}
|
|
35
|
-
function publicJwk(privateJwk) {
|
|
36
|
-
const { crv, kty, x, y } = privateJwk;
|
|
37
|
-
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
38
|
-
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
39
|
-
}
|
|
40
|
-
return { crv, kty, x, y };
|
|
41
|
-
}
|
|
42
|
-
function canonicalTargetUrl(value) {
|
|
43
|
-
const url = new URL(value);
|
|
44
|
-
return `${url.origin}${url.pathname}`;
|
|
45
|
-
}
|
|
46
|
-
async function calculateDpopJkt(privateJwk) {
|
|
47
|
-
const jwk = publicJwk(privateJwk);
|
|
48
|
-
return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
|
|
49
|
-
}
|
|
50
|
-
async function createDpopProof(input) {
|
|
51
|
-
const jwk = publicJwk(input.privateJwk);
|
|
52
|
-
const key = await crypto.subtle.importKey(
|
|
53
|
-
"jwk",
|
|
54
|
-
input.privateJwk,
|
|
55
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
56
|
-
false,
|
|
57
|
-
["sign"]
|
|
58
|
-
);
|
|
59
|
-
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
60
|
-
const claims = encodeJson({
|
|
61
|
-
jti: crypto.randomUUID(),
|
|
62
|
-
iat: Math.floor(Date.now() / 1e3),
|
|
63
|
-
htm: input.method.toUpperCase(),
|
|
64
|
-
htu: canonicalTargetUrl(input.targetUrl),
|
|
65
|
-
ath: await sha256Base64Url(input.accessToken)
|
|
66
|
-
});
|
|
67
|
-
const signature = await crypto.subtle.sign(
|
|
68
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
69
|
-
key,
|
|
70
|
-
encoder.encode(`${header}.${claims}`)
|
|
71
|
-
);
|
|
72
|
-
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
21
|
// src/errors.ts
|
|
76
22
|
var TopoloSdkError = class extends Error {
|
|
77
23
|
code;
|
|
@@ -395,6 +341,139 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
395
341
|
}
|
|
396
342
|
};
|
|
397
343
|
|
|
344
|
+
// src/action-secrets.ts
|
|
345
|
+
var ACTION_SECRET_PREFIX = "topolo-secret:";
|
|
346
|
+
function isActionSecretReference(value) {
|
|
347
|
+
return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
|
|
348
|
+
}
|
|
349
|
+
function isRecord(value) {
|
|
350
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
351
|
+
}
|
|
352
|
+
function sensitive(key, value) {
|
|
353
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
354
|
+
if (normalized === "capability") {
|
|
355
|
+
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
356
|
+
const text2 = value.trim();
|
|
357
|
+
return Boolean(text2) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text2);
|
|
358
|
+
}
|
|
359
|
+
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
360
|
+
}
|
|
361
|
+
function maskActionSecrets(value, secrets) {
|
|
362
|
+
let masked = value;
|
|
363
|
+
for (const [secret, reference] of secrets) {
|
|
364
|
+
const encoded = encodeURIComponent(secret);
|
|
365
|
+
const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
366
|
+
masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
|
|
367
|
+
}
|
|
368
|
+
return masked;
|
|
369
|
+
}
|
|
370
|
+
async function resolveActionSecrets(value, store, secrets) {
|
|
371
|
+
if (isActionSecretReference(value)) {
|
|
372
|
+
if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
373
|
+
const resolved = await store.load(value);
|
|
374
|
+
if (typeof resolved === "string" && resolved) secrets.set(resolved, value);
|
|
375
|
+
return resolved;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
|
|
378
|
+
if (!isRecord(value)) return value;
|
|
379
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
|
|
380
|
+
}
|
|
381
|
+
async function protectActionResult(value, store) {
|
|
382
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
383
|
+
async function protect(item, key = "") {
|
|
384
|
+
if (isActionSecretReference(item)) return item;
|
|
385
|
+
if (sensitive(key, item)) {
|
|
386
|
+
const reference = store ? await store.save(item) : "[REDACTED]";
|
|
387
|
+
if (typeof item === "string" && item) secrets.set(item, reference);
|
|
388
|
+
return reference;
|
|
389
|
+
}
|
|
390
|
+
if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child)));
|
|
391
|
+
if (!isRecord(item)) return item;
|
|
392
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, name)])));
|
|
393
|
+
}
|
|
394
|
+
const protectedValue = await protect(value);
|
|
395
|
+
async function protectEmbedded(item) {
|
|
396
|
+
if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
|
|
397
|
+
return store ? store.save(item) : maskActionSecrets(item, secrets);
|
|
398
|
+
}
|
|
399
|
+
if (Array.isArray(item)) return Promise.all(item.map(protectEmbedded));
|
|
400
|
+
if (!isRecord(item)) return item;
|
|
401
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key, child]) => [key, await protectEmbedded(child)])));
|
|
402
|
+
}
|
|
403
|
+
return protectEmbedded(protectedValue);
|
|
404
|
+
}
|
|
405
|
+
function maskActionSecretValue(value, secrets) {
|
|
406
|
+
if (value instanceof Error) return maskActionSecretError(value, secrets);
|
|
407
|
+
if (typeof value === "string") return maskActionSecrets(value, secrets);
|
|
408
|
+
if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
|
|
409
|
+
if (!isRecord(value)) return value;
|
|
410
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
|
|
411
|
+
}
|
|
412
|
+
function maskActionSecretError(error, secrets) {
|
|
413
|
+
if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
|
|
414
|
+
const masked = Object.create(Object.getPrototypeOf(error));
|
|
415
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
416
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
417
|
+
if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
|
|
418
|
+
Object.defineProperty(masked, key, descriptor);
|
|
419
|
+
}
|
|
420
|
+
return masked;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/dpop.ts
|
|
424
|
+
var encoder = new TextEncoder();
|
|
425
|
+
function encodeBase64Url(value) {
|
|
426
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
427
|
+
let binary = "";
|
|
428
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
429
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
430
|
+
}
|
|
431
|
+
function encodeJson(value) {
|
|
432
|
+
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
433
|
+
}
|
|
434
|
+
async function sha256Base64Url(value) {
|
|
435
|
+
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
436
|
+
}
|
|
437
|
+
function publicJwk(privateJwk) {
|
|
438
|
+
const { crv, kty, x, y } = privateJwk;
|
|
439
|
+
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
440
|
+
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
441
|
+
}
|
|
442
|
+
return { crv, kty, x, y };
|
|
443
|
+
}
|
|
444
|
+
function canonicalTargetUrl(value) {
|
|
445
|
+
const url = new URL(value);
|
|
446
|
+
return `${url.origin}${url.pathname}`;
|
|
447
|
+
}
|
|
448
|
+
async function calculateDpopJkt(privateJwk) {
|
|
449
|
+
const jwk = publicJwk(privateJwk);
|
|
450
|
+
return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
|
|
451
|
+
}
|
|
452
|
+
async function createDpopProof(input) {
|
|
453
|
+
const jwk = publicJwk(input.privateJwk);
|
|
454
|
+
const key = await crypto.subtle.importKey(
|
|
455
|
+
"jwk",
|
|
456
|
+
input.privateJwk,
|
|
457
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
458
|
+
false,
|
|
459
|
+
["sign"]
|
|
460
|
+
);
|
|
461
|
+
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
462
|
+
const claims = encodeJson({
|
|
463
|
+
jti: crypto.randomUUID(),
|
|
464
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
465
|
+
htm: input.method.toUpperCase(),
|
|
466
|
+
htu: canonicalTargetUrl(input.targetUrl),
|
|
467
|
+
ath: await sha256Base64Url(input.accessToken)
|
|
468
|
+
});
|
|
469
|
+
const signature = await crypto.subtle.sign(
|
|
470
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
471
|
+
key,
|
|
472
|
+
encoder.encode(`${header}.${claims}`)
|
|
473
|
+
);
|
|
474
|
+
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
398
477
|
// src/environment.ts
|
|
399
478
|
var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
|
|
400
479
|
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
@@ -700,9 +779,9 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
700
779
|
return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
701
780
|
}
|
|
702
781
|
const children = [];
|
|
703
|
-
if (
|
|
704
|
-
if (
|
|
705
|
-
if (
|
|
782
|
+
if (isRecord2(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
783
|
+
if (isRecord2(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
784
|
+
if (isRecord2(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
706
785
|
children.push(
|
|
707
786
|
["additionalProperties", schema.additionalProperties],
|
|
708
787
|
["propertyNames", schema.propertyNames],
|
|
@@ -712,7 +791,7 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
712
791
|
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
713
792
|
);
|
|
714
793
|
for (const [key, child] of children) {
|
|
715
|
-
if (!
|
|
794
|
+
if (!isRecord2(child)) continue;
|
|
716
795
|
const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
|
|
717
796
|
if (issue) return issue;
|
|
718
797
|
}
|
|
@@ -736,7 +815,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
736
815
|
}
|
|
737
816
|
if (Array.isArray(schema.allOf)) {
|
|
738
817
|
for (const candidate of schema.allOf) {
|
|
739
|
-
if (
|
|
818
|
+
if (isRecord2(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
|
|
740
819
|
}
|
|
741
820
|
}
|
|
742
821
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
@@ -766,7 +845,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
766
845
|
if (typeof value === "string") validateString(schema, value, path, errors);
|
|
767
846
|
if (typeof value === "number") validateNumber(schema, value, path, errors);
|
|
768
847
|
if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
|
|
769
|
-
if (
|
|
848
|
+
if (isRecord2(value)) validateObject(schema, value, path, root, errors, depth);
|
|
770
849
|
}
|
|
771
850
|
function validateObject(schema, value, path, root, errors, depth) {
|
|
772
851
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
@@ -775,25 +854,25 @@ function validateObject(schema, value, path, root, errors, depth) {
|
|
|
775
854
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
776
855
|
errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
777
856
|
}
|
|
778
|
-
const properties =
|
|
857
|
+
const properties = isRecord2(schema.properties) ? schema.properties : {};
|
|
779
858
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
780
859
|
for (const key of required) {
|
|
781
860
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
782
861
|
errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
|
|
783
862
|
}
|
|
784
863
|
}
|
|
785
|
-
if (
|
|
864
|
+
if (isRecord2(schema.propertyNames)) {
|
|
786
865
|
for (const key of Object.keys(value)) {
|
|
787
866
|
validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
|
|
788
867
|
}
|
|
789
868
|
}
|
|
790
869
|
for (const [key, child] of Object.entries(value)) {
|
|
791
870
|
const childSchema = properties[key];
|
|
792
|
-
if (
|
|
871
|
+
if (isRecord2(childSchema)) {
|
|
793
872
|
validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
|
|
794
873
|
} else if (schema.additionalProperties === false) {
|
|
795
874
|
errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
796
|
-
} else if (
|
|
875
|
+
} else if (isRecord2(schema.additionalProperties)) {
|
|
797
876
|
validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
|
|
798
877
|
}
|
|
799
878
|
}
|
|
@@ -809,7 +888,7 @@ function validateArray(schema, value, path, root, errors, depth) {
|
|
|
809
888
|
const unique = new Set(value.map((item) => JSON.stringify(item)));
|
|
810
889
|
if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
|
|
811
890
|
}
|
|
812
|
-
if (
|
|
891
|
+
if (isRecord2(schema.items)) {
|
|
813
892
|
value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
|
|
814
893
|
}
|
|
815
894
|
}
|
|
@@ -847,7 +926,7 @@ function validateNumber(schema, value, path, errors) {
|
|
|
847
926
|
}
|
|
848
927
|
}
|
|
849
928
|
function candidateMatches(candidate, value, root, depth) {
|
|
850
|
-
if (!
|
|
929
|
+
if (!isRecord2(candidate)) return false;
|
|
851
930
|
const issues = [];
|
|
852
931
|
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
853
932
|
return issues.length === 0;
|
|
@@ -861,24 +940,24 @@ function resolveLocalReference(root, reference) {
|
|
|
861
940
|
current = current[Number(token)];
|
|
862
941
|
continue;
|
|
863
942
|
}
|
|
864
|
-
if (!
|
|
943
|
+
if (!isRecord2(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
865
944
|
current = current[token];
|
|
866
945
|
}
|
|
867
|
-
return
|
|
946
|
+
return isRecord2(current) ? current : null;
|
|
868
947
|
}
|
|
869
948
|
function matchesType(type, value) {
|
|
870
949
|
if (type === void 0) return true;
|
|
871
950
|
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
872
951
|
if (type === "null") return value === null;
|
|
873
952
|
if (type === "array") return Array.isArray(value);
|
|
874
|
-
if (type === "object") return
|
|
953
|
+
if (type === "object") return isRecord2(value);
|
|
875
954
|
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
876
955
|
return typeof value === type;
|
|
877
956
|
}
|
|
878
957
|
function sameJsonValue(left, right) {
|
|
879
958
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
880
959
|
}
|
|
881
|
-
function
|
|
960
|
+
function isRecord2(value) {
|
|
882
961
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
883
962
|
}
|
|
884
963
|
function isUrl(value) {
|
|
@@ -1135,7 +1214,7 @@ function fail(path, message) {
|
|
|
1135
1214
|
|
|
1136
1215
|
// src/action-upload.ts
|
|
1137
1216
|
function parseTopoloActionUploadContract(value) {
|
|
1138
|
-
if (!
|
|
1217
|
+
if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
|
|
1139
1218
|
throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
|
|
1140
1219
|
}
|
|
1141
1220
|
const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
|
|
@@ -1214,17 +1293,17 @@ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, op
|
|
|
1214
1293
|
};
|
|
1215
1294
|
}
|
|
1216
1295
|
function topoloUploadResponseValue(response, path) {
|
|
1217
|
-
return path.split(".").reduce((value, segment) =>
|
|
1296
|
+
return path.split(".").reduce((value, segment) => isRecord3(value) ? value[segment] : void 0, response);
|
|
1218
1297
|
}
|
|
1219
1298
|
function redactTopoloUrlSessionResult(response, uploadUrlPath) {
|
|
1220
1299
|
const redacted = cloneUploadValue(response);
|
|
1221
1300
|
let target = redacted;
|
|
1222
1301
|
const segments = uploadUrlPath.split(".");
|
|
1223
1302
|
for (const segment of segments.slice(0, -1)) {
|
|
1224
|
-
target =
|
|
1303
|
+
target = isRecord3(target) ? target[segment] : void 0;
|
|
1225
1304
|
}
|
|
1226
1305
|
const finalSegment = segments.at(-1);
|
|
1227
|
-
if (finalSegment &&
|
|
1306
|
+
if (finalSegment && isRecord3(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
|
|
1228
1307
|
redactUploadHeaders(redacted);
|
|
1229
1308
|
return redacted;
|
|
1230
1309
|
}
|
|
@@ -1248,7 +1327,7 @@ function mapUploadInput(contract, contentBase64, input, options) {
|
|
|
1248
1327
|
}
|
|
1249
1328
|
function cloneUploadValue(value) {
|
|
1250
1329
|
if (Array.isArray(value)) return value.map(cloneUploadValue);
|
|
1251
|
-
if (!
|
|
1330
|
+
if (!isRecord3(value)) return value;
|
|
1252
1331
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUploadValue(item)]));
|
|
1253
1332
|
}
|
|
1254
1333
|
function redactUploadHeaders(value) {
|
|
@@ -1256,9 +1335,9 @@ function redactUploadHeaders(value) {
|
|
|
1256
1335
|
value.forEach(redactUploadHeaders);
|
|
1257
1336
|
return;
|
|
1258
1337
|
}
|
|
1259
|
-
if (!
|
|
1338
|
+
if (!isRecord3(value)) return;
|
|
1260
1339
|
for (const [key, item] of Object.entries(value)) {
|
|
1261
|
-
if (key.toLowerCase() === "headers" &&
|
|
1340
|
+
if (key.toLowerCase() === "headers" && isRecord3(item)) {
|
|
1262
1341
|
for (const header of Object.keys(item)) {
|
|
1263
1342
|
if (["authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key"].includes(header.toLowerCase())) {
|
|
1264
1343
|
item[header] = "[REDACTED]";
|
|
@@ -1287,7 +1366,7 @@ function decodedBase64Size(contentBase64) {
|
|
|
1287
1366
|
return contentBase64.length / 4 * 3 - padding;
|
|
1288
1367
|
}
|
|
1289
1368
|
function parseCompleteContract(value) {
|
|
1290
|
-
if (!
|
|
1369
|
+
if (!isRecord3(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
|
|
1291
1370
|
return {
|
|
1292
1371
|
actionId: requiredPath(value.actionId, "complete.actionId"),
|
|
1293
1372
|
inputField: requiredField(value.inputField, "complete.inputField"),
|
|
@@ -1325,7 +1404,7 @@ function encodeTopoloActionBase64(bytes) {
|
|
|
1325
1404
|
}
|
|
1326
1405
|
return btoa(binary);
|
|
1327
1406
|
}
|
|
1328
|
-
function
|
|
1407
|
+
function isRecord3(value) {
|
|
1329
1408
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1330
1409
|
}
|
|
1331
1410
|
|
|
@@ -1338,6 +1417,8 @@ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
|
1338
1417
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1339
1418
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
1340
1419
|
var TopoloClient = class _TopoloClient {
|
|
1420
|
+
actionSecretStore;
|
|
1421
|
+
actionSecrets = /* @__PURE__ */ new Map();
|
|
1341
1422
|
credential;
|
|
1342
1423
|
agent;
|
|
1343
1424
|
actorContext;
|
|
@@ -1367,6 +1448,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1367
1448
|
_TopoloClient.appIdCache.clear();
|
|
1368
1449
|
}
|
|
1369
1450
|
constructor(options) {
|
|
1451
|
+
this.actionSecretStore = options.actionSecretStore;
|
|
1370
1452
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
1371
1453
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
1372
1454
|
this.credential = options.credential;
|
|
@@ -1388,6 +1470,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1388
1470
|
}
|
|
1389
1471
|
emit(event) {
|
|
1390
1472
|
if (!this.debug) return;
|
|
1473
|
+
event = maskActionSecretValue(event, this.actionSecrets);
|
|
1391
1474
|
try {
|
|
1392
1475
|
this.debug(event);
|
|
1393
1476
|
} catch {
|
|
@@ -1773,7 +1856,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1773
1856
|
}
|
|
1774
1857
|
const entries = [];
|
|
1775
1858
|
for (const item of raw) {
|
|
1776
|
-
if (!
|
|
1859
|
+
if (!isRecord4(item)) continue;
|
|
1777
1860
|
const s = item;
|
|
1778
1861
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
1779
1862
|
if (!appId) continue;
|
|
@@ -1848,8 +1931,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
1848
1931
|
"app_topolo_auth.service_api_key_resources.list",
|
|
1849
1932
|
{ appId: service.appId }
|
|
1850
1933
|
);
|
|
1851
|
-
const payload =
|
|
1852
|
-
if (!
|
|
1934
|
+
const payload = isRecord4(response) && isRecord4(response.data) ? response.data : response;
|
|
1935
|
+
if (!isRecord4(payload) || !Array.isArray(payload.resources)) {
|
|
1853
1936
|
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
1854
1937
|
}
|
|
1855
1938
|
return {
|
|
@@ -2030,17 +2113,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
2030
2113
|
}
|
|
2031
2114
|
async validateActionInput(action, input = {}) {
|
|
2032
2115
|
const entry = await this.getAction(action);
|
|
2033
|
-
|
|
2116
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
2117
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, resolved) };
|
|
2034
2118
|
}
|
|
2035
2119
|
async planAction(action, input = {}, options = {}) {
|
|
2036
2120
|
const entry = await this.getAction(action);
|
|
2037
|
-
|
|
2121
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
2122
|
+
return this.planResolvedAction(entry, resolved, options);
|
|
2038
2123
|
}
|
|
2039
2124
|
async prepareAction(action, input, options = {}) {
|
|
2040
2125
|
const entry = await this.getAction(action);
|
|
2041
2126
|
const example = (entry.agentContract?.examples ?? []).find((candidate) => Boolean(candidate?.input) && typeof candidate.input === "object" && !Array.isArray(candidate.input) && validateTopoloActionInput(entry.inputSchema, candidate.input).valid) ?? null;
|
|
2042
2127
|
const selectedInput = input ?? example?.input ?? {};
|
|
2043
|
-
const
|
|
2128
|
+
const resolved = await resolveActionSecrets(selectedInput, this.actionSecretStore, this.actionSecrets);
|
|
2129
|
+
const plan = this.planResolvedAction(entry, resolved, options);
|
|
2044
2130
|
return {
|
|
2045
2131
|
action: entry,
|
|
2046
2132
|
example,
|
|
@@ -2077,7 +2163,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2077
2163
|
confirmationRequired,
|
|
2078
2164
|
confirmationProvided: options.confirm === true,
|
|
2079
2165
|
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
2080
|
-
request,
|
|
2166
|
+
request: maskActionSecretValue(request, this.actionSecrets),
|
|
2081
2167
|
verification: stringArray(entry.agentContract?.verify),
|
|
2082
2168
|
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
2083
2169
|
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
@@ -2107,6 +2193,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2107
2193
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2108
2194
|
);
|
|
2109
2195
|
}
|
|
2196
|
+
input = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
2110
2197
|
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
2111
2198
|
if (!validation.valid) {
|
|
2112
2199
|
throw new TopoloSdkError(
|
|
@@ -2137,22 +2224,31 @@ var TopoloClient = class _TopoloClient {
|
|
|
2137
2224
|
}
|
|
2138
2225
|
}
|
|
2139
2226
|
const service = entry.appSlug || entry.appId;
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2227
|
+
let result;
|
|
2228
|
+
try {
|
|
2229
|
+
result = await this.request({
|
|
2230
|
+
service,
|
|
2231
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2232
|
+
method: entry.method,
|
|
2233
|
+
path: mapped.path,
|
|
2234
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2235
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2236
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2237
|
+
...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
|
|
2238
|
+
headers: {
|
|
2239
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2240
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2241
|
+
},
|
|
2242
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2243
|
+
});
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
throw maskActionSecretError(error, this.actionSecrets);
|
|
2246
|
+
}
|
|
2247
|
+
return preserveSensitiveResult ? result : await protectActionResult(result, this.actionSecretStore);
|
|
2248
|
+
}
|
|
2249
|
+
async releaseActionSecret(reference) {
|
|
2250
|
+
if (!this.actionSecretStore) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
2251
|
+
await this.actionSecretStore.delete(reference);
|
|
2156
2252
|
}
|
|
2157
2253
|
async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
|
|
2158
2254
|
if (!this.actorContext) return void 0;
|
|
@@ -2299,7 +2395,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2299
2395
|
return null;
|
|
2300
2396
|
}
|
|
2301
2397
|
function normalizeApiKeyResourceMetadata(input) {
|
|
2302
|
-
if (!
|
|
2398
|
+
if (!isRecord4(input)) return null;
|
|
2303
2399
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2304
2400
|
const label = readString(input.label);
|
|
2305
2401
|
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
@@ -2333,7 +2429,7 @@ function normalizeApiKeyResourceMetadataList(input) {
|
|
|
2333
2429
|
return resources;
|
|
2334
2430
|
}
|
|
2335
2431
|
function normalizeBindableResource(input) {
|
|
2336
|
-
if (!
|
|
2432
|
+
if (!isRecord4(input)) return null;
|
|
2337
2433
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2338
2434
|
const resourceId = readString(input.resourceId ?? input.resource_id);
|
|
2339
2435
|
if (!resourceType || !resourceId) return null;
|
|
@@ -2344,13 +2440,13 @@ function normalizeBindableResource(input) {
|
|
|
2344
2440
|
slug: readOptionalString(input.slug),
|
|
2345
2441
|
status: readOptionalString(input.status),
|
|
2346
2442
|
organizationId: readOptionalString(input.organizationId ?? input.organization_id),
|
|
2347
|
-
metadata:
|
|
2443
|
+
metadata: isRecord4(input.metadata) ? input.metadata : {}
|
|
2348
2444
|
};
|
|
2349
2445
|
}
|
|
2350
2446
|
function normalizeResourceBindings(input) {
|
|
2351
2447
|
if (!Array.isArray(input)) return [];
|
|
2352
2448
|
return input.flatMap((binding) => {
|
|
2353
|
-
if (!
|
|
2449
|
+
if (!isRecord4(binding)) return [];
|
|
2354
2450
|
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
2355
2451
|
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
2356
2452
|
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
@@ -2363,30 +2459,9 @@ function readOptionalString(value) {
|
|
|
2363
2459
|
const normalized = readString(value);
|
|
2364
2460
|
return normalized || null;
|
|
2365
2461
|
}
|
|
2366
|
-
function
|
|
2462
|
+
function isRecord4(value) {
|
|
2367
2463
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2368
2464
|
}
|
|
2369
|
-
function redactActionResult(value) {
|
|
2370
|
-
if (Array.isArray(value)) return value.map(redactActionResult);
|
|
2371
|
-
if (!isRecord3(value)) return value;
|
|
2372
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
2373
|
-
key,
|
|
2374
|
-
isSensitiveActionResultKey(key, item) ? "[REDACTED]" : redactActionResult(item)
|
|
2375
|
-
]));
|
|
2376
|
-
}
|
|
2377
|
-
function isSensitiveActionResultKey(key, value) {
|
|
2378
|
-
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2379
|
-
if (normalized === "capability") return isCredentialShaped(value);
|
|
2380
|
-
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
2381
|
-
}
|
|
2382
|
-
var PLAIN_ENUM_VALUE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
2383
|
-
function isCredentialShaped(value) {
|
|
2384
|
-
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
2385
|
-
const trimmed = value.trim();
|
|
2386
|
-
if (!trimmed) return false;
|
|
2387
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return true;
|
|
2388
|
-
return !PLAIN_ENUM_VALUE.test(trimmed);
|
|
2389
|
-
}
|
|
2390
2465
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2391
2466
|
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2392
2467
|
const configured = serviceUrls?.[serviceKey];
|
|
@@ -2456,14 +2531,14 @@ function catalogSearchQuery(options) {
|
|
|
2456
2531
|
return query;
|
|
2457
2532
|
}
|
|
2458
2533
|
function normalizeCatalogPage(value) {
|
|
2459
|
-
if (!
|
|
2534
|
+
if (!isRecord4(value)) return { nextCursor: null, hasMore: false };
|
|
2460
2535
|
return {
|
|
2461
2536
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2462
2537
|
hasMore: value.hasMore === true
|
|
2463
2538
|
};
|
|
2464
2539
|
}
|
|
2465
2540
|
function normalizeServiceCatalogEntry(item) {
|
|
2466
|
-
if (!
|
|
2541
|
+
if (!isRecord4(item)) return null;
|
|
2467
2542
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2468
2543
|
if (!appId) return null;
|
|
2469
2544
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -2928,7 +3003,7 @@ var INVERSE_OPERATIONS = /* @__PURE__ */ new Map([
|
|
|
2928
3003
|
["restore", "archive"],
|
|
2929
3004
|
["unpublish", "publish"]
|
|
2930
3005
|
]);
|
|
2931
|
-
function
|
|
3006
|
+
function isRecord5(value) {
|
|
2932
3007
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
2933
3008
|
}
|
|
2934
3009
|
function nonEmptyStrings(value) {
|
|
@@ -2966,10 +3041,10 @@ function collectionFor(action, resource) {
|
|
|
2966
3041
|
return resource.endsWith("s") ? resource : `${resource}s`;
|
|
2967
3042
|
}
|
|
2968
3043
|
function isConcreteOutputSchema(schema) {
|
|
2969
|
-
if (!
|
|
3044
|
+
if (!isRecord5(schema)) return false;
|
|
2970
3045
|
if (typeof schema.$ref === "string" && schema.$ref.trim()) return true;
|
|
2971
|
-
if (schema.type === "array" &&
|
|
2972
|
-
if (
|
|
3046
|
+
if (schema.type === "array" && isRecord5(schema.items) && Object.keys(schema.items).length > 0) return true;
|
|
3047
|
+
if (isRecord5(schema.properties) && Object.keys(schema.properties).length > 0) return true;
|
|
2973
3048
|
return ["oneOf", "anyOf", "allOf"].some((key) => Array.isArray(schema[key]) && schema[key].some(isConcreteOutputSchema));
|
|
2974
3049
|
}
|
|
2975
3050
|
function resourceSchema(resource) {
|
|
@@ -3096,10 +3171,10 @@ function isDynamicIdentifier(hint) {
|
|
|
3096
3171
|
return words === "id" || words.endsWith(" id");
|
|
3097
3172
|
}
|
|
3098
3173
|
function localReference(rootSchema, reference) {
|
|
3099
|
-
if (!
|
|
3174
|
+
if (!isRecord5(rootSchema) || typeof reference !== "string" || !reference.startsWith("#/")) return null;
|
|
3100
3175
|
return reference.slice(2).split("/").reduce((value, segment) => {
|
|
3101
3176
|
const key = segment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
3102
|
-
return
|
|
3177
|
+
return isRecord5(value) ? value[key] : void 0;
|
|
3103
3178
|
}, rootSchema);
|
|
3104
3179
|
}
|
|
3105
3180
|
function representativeNumber(schema, hint) {
|
|
@@ -3113,9 +3188,9 @@ function representativeNumber(schema, hint) {
|
|
|
3113
3188
|
return integer ? Math.round(value) : value;
|
|
3114
3189
|
}
|
|
3115
3190
|
function sampleForSchema(schema, depth = 0, hint = "value", rootSchema = schema) {
|
|
3116
|
-
if (!
|
|
3191
|
+
if (!isRecord5(schema) || depth > 20) return null;
|
|
3117
3192
|
const referenced = localReference(rootSchema, schema.$ref);
|
|
3118
|
-
if (
|
|
3193
|
+
if (isRecord5(referenced)) return sampleForSchema(referenced, depth + 1, hint, rootSchema);
|
|
3119
3194
|
if (schema.const !== void 0) return schema.const;
|
|
3120
3195
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
3121
3196
|
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return sampleForSchema(schema.oneOf[0], depth + 1, hint, rootSchema);
|
|
@@ -3135,8 +3210,8 @@ function sampleForSchema(schema, depth = 0, hint = "value", rootSchema = schema)
|
|
|
3135
3210
|
}
|
|
3136
3211
|
if (schema.type === "string") return isDynamicIdentifier(hint) ? void 0 : sampleString(schema, hint);
|
|
3137
3212
|
if (schema.type === "null") return null;
|
|
3138
|
-
if (schema.type === "object" ||
|
|
3139
|
-
const properties =
|
|
3213
|
+
if (schema.type === "object" || isRecord5(schema.properties)) {
|
|
3214
|
+
const properties = isRecord5(schema.properties) ? schema.properties : {};
|
|
3140
3215
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
3141
3216
|
const entries = Object.entries(properties).filter(([key]) => required.has(key)).map(([key, child]) => [key, sampleForSchema(child, depth + 1, key, rootSchema)]).filter(([, value]) => value !== void 0);
|
|
3142
3217
|
return Object.fromEntries(entries);
|
|
@@ -3144,9 +3219,9 @@ function sampleForSchema(schema, depth = 0, hint = "value", rootSchema = schema)
|
|
|
3144
3219
|
return null;
|
|
3145
3220
|
}
|
|
3146
3221
|
function hasRequiredDynamicIdentifier(schema, depth = 0, rootSchema = schema) {
|
|
3147
|
-
if (!
|
|
3222
|
+
if (!isRecord5(schema) || depth > 20) return false;
|
|
3148
3223
|
const referenced = localReference(rootSchema, schema.$ref);
|
|
3149
|
-
if (
|
|
3224
|
+
if (isRecord5(referenced)) return hasRequiredDynamicIdentifier(referenced, depth + 1, rootSchema);
|
|
3150
3225
|
if (Array.isArray(schema.oneOf)) {
|
|
3151
3226
|
return schema.oneOf.some((candidate) => hasRequiredDynamicIdentifier(candidate, depth + 1, rootSchema));
|
|
3152
3227
|
}
|
|
@@ -3156,10 +3231,10 @@ function hasRequiredDynamicIdentifier(schema, depth = 0, rootSchema = schema) {
|
|
|
3156
3231
|
if (schema.type === "array") {
|
|
3157
3232
|
return hasRequiredDynamicIdentifier(schema.items, depth + 1, rootSchema);
|
|
3158
3233
|
}
|
|
3159
|
-
if (schema.type !== "object" && !
|
|
3160
|
-
const properties =
|
|
3234
|
+
if (schema.type !== "object" && !isRecord5(schema.properties)) return false;
|
|
3235
|
+
const properties = isRecord5(schema.properties) ? schema.properties : {};
|
|
3161
3236
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
3162
|
-
return Object.entries(properties).some(([key, child]) => required.has(key) && (
|
|
3237
|
+
return Object.entries(properties).some(([key, child]) => required.has(key) && (isRecord5(child) && child.type === "string" && isDynamicIdentifier(key) || hasRequiredDynamicIdentifier(child, depth + 1, rootSchema)));
|
|
3163
3238
|
}
|
|
3164
3239
|
function actionPrefix(name, operation) {
|
|
3165
3240
|
const tokens = String(name || "").split(/[._:-]+/).filter(Boolean);
|
|
@@ -3232,13 +3307,13 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
|
|
|
3232
3307
|
const outputSchema = isConcreteOutputSchema(action.outputSchema) ? structuredClone(action.outputSchema) : derivedOutputSchema(action);
|
|
3233
3308
|
const resource = resourceFor(action);
|
|
3234
3309
|
const readOnly = action.readOnly !== void 0 ? action.readOnly === true : action.method === "GET";
|
|
3235
|
-
const existing =
|
|
3310
|
+
const existing = isRecord5(action.agentContract) ? structuredClone(action.agentContract) : {};
|
|
3236
3311
|
const declaredAgentContract = Object.keys(existing).length > 0;
|
|
3237
|
-
const recovery =
|
|
3312
|
+
const recovery = isRecord5(existing.recovery) ? existing.recovery : recoveryFor(action, allActions, appId);
|
|
3238
3313
|
const rollbackActionId = typeof existing.rollbackActionId === "string" && existing.rollbackActionId.trim() ? existing.rollbackActionId.trim() : recovery.mode === "compensating_action" ? recovery.actionId : void 0;
|
|
3239
3314
|
const inputExample = sampleForSchema(action.inputSchema);
|
|
3240
3315
|
const expectedExample = readOnly ? `A successful response conforming to outputSchema; verify the returned ${resource.replaceAll("_", " ")} belongs to the selected resource context.` : `A successful response conforming to outputSchema; re-read the affected ${resource.replaceAll("_", " ")} to verify the intended change.`;
|
|
3241
|
-
const examples = Array.isArray(existing.examples) && existing.examples.length > 0 ? existing.examples.map((example) =>
|
|
3316
|
+
const examples = Array.isArray(existing.examples) && existing.examples.length > 0 ? existing.examples.map((example) => isRecord5(example) && Object.hasOwn(example, "input") ? { ...example, expected: typeof example.expected === "string" && example.expected.trim() ? example.expected : expectedExample } : { input: example, expected: expectedExample }) : [{
|
|
3242
3317
|
input: inputExample ?? {},
|
|
3243
3318
|
expected: expectedExample
|
|
3244
3319
|
}];
|
|
@@ -3251,7 +3326,7 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
|
|
|
3251
3326
|
...existing,
|
|
3252
3327
|
docsUrl,
|
|
3253
3328
|
...existingDocsUrl && existingDocsUrl !== docsUrl ? { guideUrl: existingDocsUrl } : {},
|
|
3254
|
-
resource:
|
|
3329
|
+
resource: isRecord5(existing.resource) ? existing.resource : { kind: resource, idPath: readOnly && ["list", "search"].includes(operationFor(action)) ? "$.items[].id" : "$.id" },
|
|
3255
3330
|
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.`],
|
|
3256
3331
|
errors: mergeErrors(defaultErrors(action), existing.errors),
|
|
3257
3332
|
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"}.`],
|
|
@@ -3266,15 +3341,15 @@ function enrichActionContract(manifest, action, allActions = manifest.actions ||
|
|
|
3266
3341
|
function actionContractCompleteness(action) {
|
|
3267
3342
|
const contract = action?.agentContract;
|
|
3268
3343
|
return {
|
|
3269
|
-
inputSchema:
|
|
3344
|
+
inputSchema: isRecord5(action?.inputSchema),
|
|
3270
3345
|
outputSchema: isConcreteOutputSchema(action?.outputSchema),
|
|
3271
3346
|
effects: nonEmptyStrings(contract?.effects),
|
|
3272
3347
|
errors: Array.isArray(contract?.errors) && contract.errors.length > 0,
|
|
3273
3348
|
docs: typeof contract?.docsUrl === "string" && contract.docsUrl.startsWith("https://"),
|
|
3274
3349
|
examples: Array.isArray(contract?.examples) && contract.examples.length > 0,
|
|
3275
3350
|
verification: nonEmptyStrings(contract?.verify),
|
|
3276
|
-
recovery:
|
|
3277
|
-
agentContract:
|
|
3351
|
+
recovery: isRecord5(contract?.recovery) && ["not_applicable", "compensating_action", "irreversible"].includes(contract.recovery.mode),
|
|
3352
|
+
agentContract: isRecord5(contract) && isRecord5(contract.resource) && nonEmptyStrings(contract.effects) && Array.isArray(contract.errors) && contract.errors.length > 0
|
|
3278
3353
|
};
|
|
3279
3354
|
}
|
|
3280
3355
|
|
|
@@ -3751,7 +3826,7 @@ var DIMENSION_VALUE_TYPES = ["string", "number", "boolean", "date", "datetime"];
|
|
|
3751
3826
|
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3752
3827
|
function validateTopoloWidgetResponse(response) {
|
|
3753
3828
|
const errors = [];
|
|
3754
|
-
if (!
|
|
3829
|
+
if (!isRecord6(response)) return { ok: false, errors: ["response must be an object"] };
|
|
3755
3830
|
exactValue(response.contractVersion, CONTRACT_VERSION, "contractVersion", errors);
|
|
3756
3831
|
nonEmptyString(response.appId, "appId", errors);
|
|
3757
3832
|
nonEmptyString(response.serviceName, "serviceName", errors);
|
|
@@ -3947,7 +4022,7 @@ function validateContribution(value, path, widget, errors) {
|
|
|
3947
4022
|
nonEmptyString(value.workspaceSelector.resourceType, `${path}.workspaceSelector.resourceType`, errors);
|
|
3948
4023
|
nonEmptyString(value.workspaceSelector.label, `${path}.workspaceSelector.label`, errors);
|
|
3949
4024
|
booleanValue(value.workspaceSelector.supportsMultiple, `${path}.workspaceSelector.supportsMultiple`, errors);
|
|
3950
|
-
if (value.workspaceSelector.mode === "required" && requiresContent(widget) &&
|
|
4025
|
+
if (value.workspaceSelector.mode === "required" && requiresContent(widget) && isRecord6(widget.resource) && typeof widget.resource.resourceId !== "string") {
|
|
3951
4026
|
errors.push(`${path}.workspaceSelector requires a bound resourceId for ready or stale data`);
|
|
3952
4027
|
}
|
|
3953
4028
|
}
|
|
@@ -4099,36 +4174,36 @@ function widgetActionKeys(widget) {
|
|
|
4099
4174
|
const keys = /* @__PURE__ */ new Set();
|
|
4100
4175
|
for (const collection of [widget.controls, widget.actions]) {
|
|
4101
4176
|
if (!Array.isArray(collection)) continue;
|
|
4102
|
-
for (const action of collection) if (
|
|
4177
|
+
for (const action of collection) if (isRecord6(action) && typeof action.id === "string") keys.add(action.id);
|
|
4103
4178
|
}
|
|
4104
4179
|
return keys;
|
|
4105
4180
|
}
|
|
4106
4181
|
function validateRuntimeSemantics(widget, contributionPath, declared, errors) {
|
|
4107
4182
|
const metrics = Array.isArray(widget.metrics) ? widget.metrics : [];
|
|
4108
4183
|
for (const metric of metrics) {
|
|
4109
|
-
if (!
|
|
4184
|
+
if (!isRecord6(metric) || typeof metric.key !== "string") continue;
|
|
4110
4185
|
if (!declared.metricKeys.has(metric.key)) errors.push(`${contributionPath}.metricDefinitions is missing runtime metric ${metric.key}`);
|
|
4111
4186
|
validateRuntimeDimensions(metric.dimensions, declared.dimensionKeys, contributionPath, errors);
|
|
4112
|
-
if (
|
|
4187
|
+
if (isRecord6(metric.series)) {
|
|
4113
4188
|
if (typeof metric.series.id === "string" && !declared.seriesKeys.has(metric.series.id)) errors.push(`${contributionPath}.seriesDefinitions is missing runtime series ${metric.series.id}`);
|
|
4114
4189
|
validateSeriesDimensions(metric.series, declared.dimensionKeys, contributionPath, errors);
|
|
4115
4190
|
}
|
|
4116
4191
|
}
|
|
4117
4192
|
const series2 = Array.isArray(widget.series) ? widget.series : [];
|
|
4118
4193
|
for (const item of series2) {
|
|
4119
|
-
if (!
|
|
4194
|
+
if (!isRecord6(item)) continue;
|
|
4120
4195
|
if (typeof item.id === "string" && !declared.seriesKeys.has(item.id)) errors.push(`${contributionPath}.seriesDefinitions is missing runtime series ${item.id}`);
|
|
4121
4196
|
validateSeriesDimensions(item, declared.dimensionKeys, contributionPath, errors);
|
|
4122
4197
|
}
|
|
4123
4198
|
}
|
|
4124
4199
|
function validateSeriesDimensions(series2, known, path, errors) {
|
|
4125
4200
|
if (!Array.isArray(series2.points)) return;
|
|
4126
|
-
for (const point of series2.points) if (
|
|
4201
|
+
for (const point of series2.points) if (isRecord6(point)) validateRuntimeDimensions(point.dimensions, known, path, errors);
|
|
4127
4202
|
}
|
|
4128
4203
|
function validateRuntimeDimensions(value, known, path, errors) {
|
|
4129
4204
|
if (!Array.isArray(value)) return;
|
|
4130
4205
|
for (const dimension2 of value) {
|
|
4131
|
-
if (
|
|
4206
|
+
if (isRecord6(dimension2) && typeof dimension2.key === "string" && !known.has(dimension2.key)) {
|
|
4132
4207
|
errors.push(`${path}.dimensions is missing runtime dimension ${dimension2.key}`);
|
|
4133
4208
|
}
|
|
4134
4209
|
}
|
|
@@ -4230,10 +4305,10 @@ function validateNotifications(value, path, errors, requireOne) {
|
|
|
4230
4305
|
});
|
|
4231
4306
|
}
|
|
4232
4307
|
function requiresContent(widget) {
|
|
4233
|
-
return
|
|
4308
|
+
return isRecord6(widget.state) && (widget.state.status === "ready" || widget.state.status === "stale");
|
|
4234
4309
|
}
|
|
4235
4310
|
function recordValue(value, path, errors) {
|
|
4236
|
-
if (!
|
|
4311
|
+
if (!isRecord6(value)) {
|
|
4237
4312
|
errors.push(`${path} must be an object`);
|
|
4238
4313
|
return false;
|
|
4239
4314
|
}
|
|
@@ -4298,7 +4373,7 @@ function isIsoTimestamp(value) {
|
|
|
4298
4373
|
function isPositiveNumber(value) {
|
|
4299
4374
|
return typeof value === "number" && value > 0;
|
|
4300
4375
|
}
|
|
4301
|
-
function
|
|
4376
|
+
function isRecord6(value) {
|
|
4302
4377
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4303
4378
|
}
|
|
4304
4379
|
|
|
@@ -4840,19 +4915,19 @@ var TOPOLO_WIDGET_OUTPUT_SCHEMA = Object.freeze({
|
|
|
4840
4915
|
additionalProperties: false
|
|
4841
4916
|
});
|
|
4842
4917
|
function isTopoloWidgetOutputSchema(schema) {
|
|
4843
|
-
if (!
|
|
4918
|
+
if (!isRecord7(schema) || !isRecord7(schema.properties)) return false;
|
|
4844
4919
|
const properties = schema.properties;
|
|
4845
|
-
const widgetItemSchema =
|
|
4846
|
-
const widgetProperties = widgetItemSchema &&
|
|
4847
|
-
const contributionSchema = widgetProperties &&
|
|
4848
|
-
const contractVersion =
|
|
4849
|
-
return schema.type === "object" && contractVersion === "3.0.0" &&
|
|
4920
|
+
const widgetItemSchema = isRecord7(properties.widgets) && isRecord7(properties.widgets.items) ? properties.widgets.items : void 0;
|
|
4921
|
+
const widgetProperties = widgetItemSchema && isRecord7(widgetItemSchema.properties) ? widgetItemSchema.properties : void 0;
|
|
4922
|
+
const contributionSchema = widgetProperties && isRecord7(widgetProperties.contribution) ? widgetProperties.contribution : void 0;
|
|
4923
|
+
const contractVersion = isRecord7(properties.contractVersion) ? properties.contractVersion.const : void 0;
|
|
4924
|
+
return schema.type === "object" && contractVersion === "3.0.0" && isRecord7(properties.widgets) && properties.widgets.type === "array" && widgetItemSchema?.type === "object" && hasRequired(schema, "contractVersion") && hasRequired(schema, "widgets") && hasRequired(widgetItemSchema, "contribution") && hasRequired(contributionSchema, "datasets") && hasRequired(contributionSchema, "compatibleVisualizations") && hasRequired(contributionSchema, "defaultVisualization");
|
|
4850
4925
|
}
|
|
4851
|
-
function
|
|
4926
|
+
function isRecord7(value) {
|
|
4852
4927
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4853
4928
|
}
|
|
4854
4929
|
function hasRequired(schema, field) {
|
|
4855
|
-
return
|
|
4930
|
+
return isRecord7(schema) && Array.isArray(schema.required) && schema.required.includes(field);
|
|
4856
4931
|
}
|
|
4857
4932
|
|
|
4858
4933
|
// src/index.ts
|