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