@aexhq/sdk 0.20.0 → 0.21.0
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/_contracts/run-custody.js +94 -3
- package/dist/cli.mjs +75 -3
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -397,7 +397,7 @@ function visitCustodyValue(input, path, findings) {
|
|
|
397
397
|
}
|
|
398
398
|
function scanStringValue(value, path, findings) {
|
|
399
399
|
for (const pattern of forbiddenStringPatterns) {
|
|
400
|
-
if (pattern
|
|
400
|
+
if (matchesForbiddenPattern(pattern, value)) {
|
|
401
401
|
findings.push(Object.freeze({
|
|
402
402
|
path,
|
|
403
403
|
reason: pattern.reason,
|
|
@@ -406,11 +406,39 @@ function scanStringValue(value, path, findings) {
|
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* A pattern fires on a value if its `regex` matches AND — when the pattern
|
|
411
|
+
* carries an `accept` predicate — at least one matched run is accepted by it.
|
|
412
|
+
* The predicate lets a shape-matched run be VETOED per-match (the entropy
|
|
413
|
+
* catch-all uses it to skip content-addressed hashes and low-entropy slugs that
|
|
414
|
+
* its coarse regex would otherwise flag); shape-only patterns have no predicate.
|
|
415
|
+
*/
|
|
416
|
+
function matchesForbiddenPattern(pattern, value) {
|
|
417
|
+
if (!pattern.accept) {
|
|
418
|
+
return pattern.regex.test(value);
|
|
419
|
+
}
|
|
420
|
+
const scan = pattern.regex.global ? pattern.regex : new RegExp(pattern.regex.source, `${pattern.regex.flags}g`);
|
|
421
|
+
scan.lastIndex = 0;
|
|
422
|
+
let match;
|
|
423
|
+
while ((match = scan.exec(value)) !== null) {
|
|
424
|
+
if (pattern.accept(match[0])) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
if (match.index === scan.lastIndex) {
|
|
428
|
+
scan.lastIndex++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
409
433
|
const forbiddenStringPatterns = Object.freeze([
|
|
410
434
|
{ reason: "bearer_token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/i },
|
|
411
435
|
{
|
|
412
436
|
reason: "provider_key",
|
|
413
|
-
|
|
437
|
+
// Prefixed provider keys (`sk-…`, Slack `xox*-…`, Google `AIza…`). The bare
|
|
438
|
+
// `sk-` body is intentionally generic so an unrecognised vendor's `sk-` key
|
|
439
|
+
// (e.g. DeepSeek `sk-<hex>`, OpenRouter `sk-or-…`) is still caught by shape,
|
|
440
|
+
// not left to the narrowed entropy catch-all below.
|
|
441
|
+
regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{8,}|AIza[A-Za-z0-9_-]{8,})/i
|
|
414
442
|
},
|
|
415
443
|
{ reason: "signed_url", regex: /[?&](?:X-Amz-Signature|X-Amz-Credential|X-Amz-Algorithm|AWSAccessKeyId)=/i },
|
|
416
444
|
{ reason: "object_store_key", regex: /(^|[\s"'`])(?:runs|assets)\/[^?<#\s"'`]+/i },
|
|
@@ -419,8 +447,71 @@ const forbiddenStringPatterns = Object.freeze([
|
|
|
419
447
|
reason: "private_resource_handle",
|
|
420
448
|
regex: /\b(?:machine|session|agent|file|skill|env|resource|handle|token_hash|bearer_hash)[_:-][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/i
|
|
421
449
|
},
|
|
422
|
-
{
|
|
450
|
+
{
|
|
451
|
+
reason: "high_entropy_token",
|
|
452
|
+
// Catch-all for an unrecognised opaque secret blob. The candidate run
|
|
453
|
+
// EXCLUDES `_` (so `SCREAMING_SNAKE` env names and `slug_with_words` URL
|
|
454
|
+
// segments split instead of fusing into a phantom 40-char run — the live
|
|
455
|
+
// false positive was `ted_season_2_peacock_official_discussion_thread` in
|
|
456
|
+
// web-search result text and a fetched URL), and the `accept` predicate
|
|
457
|
+
// vetoes content-addressed hashes (md5/sha1/sha256 digests — the platform's
|
|
458
|
+
// OWN asset filenames) and low-entropy / single-class runs so only genuine
|
|
459
|
+
// opaque secrets remain. Slash-bearing secrets (signed URLs, connection
|
|
460
|
+
// strings, `Bearer …`) are covered by the named patterns above.
|
|
461
|
+
regex: /\b[A-Za-z0-9-]{40,}\b/,
|
|
462
|
+
accept: isHighEntropySecretRun
|
|
463
|
+
}
|
|
423
464
|
]);
|
|
465
|
+
/** A content-addressed hash (md5/sha1/sha256 hex digest) — the platform's own
|
|
466
|
+
* asset filenames and content references. Exempt from the entropy catch-all so
|
|
467
|
+
* a captured output named after its sha256 (or a hash echoed in tool-result
|
|
468
|
+
* text) is not misclassified as a leaked secret. */
|
|
469
|
+
const CONTENT_HASH_RUN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
470
|
+
/**
|
|
471
|
+
* Decide whether a coarse `[A-Za-z0-9-]{40,}` run is a genuine opaque secret.
|
|
472
|
+
* Rejects content-addressed hashes, then requires both character-class
|
|
473
|
+
* diversity (≥2 of lower/upper/digit) and high Shannon entropy — the property
|
|
474
|
+
* that separates an opaque key blob from a long dictionary-ish identifier. A
|
|
475
|
+
* real prefixless secret (base64url/alnum-mixed) clears both gates; a hash, a
|
|
476
|
+
* hyphenated slug, or a single-class run does not.
|
|
477
|
+
*/
|
|
478
|
+
function isHighEntropySecretRun(run) {
|
|
479
|
+
if (CONTENT_HASH_RUN.test(run)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (!/[A-Za-z]/.test(run) || !/\d/.test(run)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (highEntropyCharClassCount(run) < 2) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return highEntropyShannonBits(run) >= 3.0;
|
|
489
|
+
}
|
|
490
|
+
function highEntropyCharClassCount(value) {
|
|
491
|
+
let count = 0;
|
|
492
|
+
if (/[a-z]/.test(value))
|
|
493
|
+
count++;
|
|
494
|
+
if (/[A-Z]/.test(value))
|
|
495
|
+
count++;
|
|
496
|
+
if (/[0-9]/.test(value))
|
|
497
|
+
count++;
|
|
498
|
+
return count;
|
|
499
|
+
}
|
|
500
|
+
function highEntropyShannonBits(value) {
|
|
501
|
+
if (value.length === 0) {
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
const counts = new Map();
|
|
505
|
+
for (const char of value) {
|
|
506
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
507
|
+
}
|
|
508
|
+
let bits = 0;
|
|
509
|
+
for (const count of counts.values()) {
|
|
510
|
+
const p = count / value.length;
|
|
511
|
+
bits -= p * Math.log2(p);
|
|
512
|
+
}
|
|
513
|
+
return bits;
|
|
514
|
+
}
|
|
424
515
|
function isForbiddenCustodyFieldName(key) {
|
|
425
516
|
return /^(apiKey|secretValue|bearerHash|signedUrl|objectStoreKey|objectKey|vaultId|providerResponseBody|responseBody|privateResourceHandle|resourceHandle|rawBody)$/i.test(key);
|
|
426
517
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -838,7 +838,7 @@ function visitCustodyValue(input, path, findings) {
|
|
|
838
838
|
}
|
|
839
839
|
function scanStringValue(value, path, findings) {
|
|
840
840
|
for (const pattern of forbiddenStringPatterns) {
|
|
841
|
-
if (pattern
|
|
841
|
+
if (matchesForbiddenPattern(pattern, value)) {
|
|
842
842
|
findings.push(Object.freeze({
|
|
843
843
|
path,
|
|
844
844
|
reason: pattern.reason,
|
|
@@ -847,11 +847,32 @@ function scanStringValue(value, path, findings) {
|
|
|
847
847
|
}
|
|
848
848
|
}
|
|
849
849
|
}
|
|
850
|
+
function matchesForbiddenPattern(pattern, value) {
|
|
851
|
+
if (!pattern.accept) {
|
|
852
|
+
return pattern.regex.test(value);
|
|
853
|
+
}
|
|
854
|
+
const scan = pattern.regex.global ? pattern.regex : new RegExp(pattern.regex.source, `${pattern.regex.flags}g`);
|
|
855
|
+
scan.lastIndex = 0;
|
|
856
|
+
let match;
|
|
857
|
+
while ((match = scan.exec(value)) !== null) {
|
|
858
|
+
if (pattern.accept(match[0])) {
|
|
859
|
+
return true;
|
|
860
|
+
}
|
|
861
|
+
if (match.index === scan.lastIndex) {
|
|
862
|
+
scan.lastIndex++;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
850
867
|
var forbiddenStringPatterns = Object.freeze([
|
|
851
868
|
{ reason: "bearer_token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/i },
|
|
852
869
|
{
|
|
853
870
|
reason: "provider_key",
|
|
854
|
-
|
|
871
|
+
// Prefixed provider keys (`sk-…`, Slack `xox*-…`, Google `AIza…`). The bare
|
|
872
|
+
// `sk-` body is intentionally generic so an unrecognised vendor's `sk-` key
|
|
873
|
+
// (e.g. DeepSeek `sk-<hex>`, OpenRouter `sk-or-…`) is still caught by shape,
|
|
874
|
+
// not left to the narrowed entropy catch-all below.
|
|
875
|
+
regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{8,}|AIza[A-Za-z0-9_-]{8,})/i
|
|
855
876
|
},
|
|
856
877
|
{ reason: "signed_url", regex: /[?&](?:X-Amz-Signature|X-Amz-Credential|X-Amz-Algorithm|AWSAccessKeyId)=/i },
|
|
857
878
|
{ reason: "object_store_key", regex: /(^|[\s"'`])(?:runs|assets)\/[^?<#\s"'`]+/i },
|
|
@@ -860,8 +881,59 @@ var forbiddenStringPatterns = Object.freeze([
|
|
|
860
881
|
reason: "private_resource_handle",
|
|
861
882
|
regex: /\b(?:machine|session|agent|file|skill|env|resource|handle|token_hash|bearer_hash)[_:-][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/i
|
|
862
883
|
},
|
|
863
|
-
{
|
|
884
|
+
{
|
|
885
|
+
reason: "high_entropy_token",
|
|
886
|
+
// Catch-all for an unrecognised opaque secret blob. The candidate run
|
|
887
|
+
// EXCLUDES `_` (so `SCREAMING_SNAKE` env names and `slug_with_words` URL
|
|
888
|
+
// segments split instead of fusing into a phantom 40-char run — the live
|
|
889
|
+
// false positive was `ted_season_2_peacock_official_discussion_thread` in
|
|
890
|
+
// web-search result text and a fetched URL), and the `accept` predicate
|
|
891
|
+
// vetoes content-addressed hashes (md5/sha1/sha256 digests — the platform's
|
|
892
|
+
// OWN asset filenames) and low-entropy / single-class runs so only genuine
|
|
893
|
+
// opaque secrets remain. Slash-bearing secrets (signed URLs, connection
|
|
894
|
+
// strings, `Bearer …`) are covered by the named patterns above.
|
|
895
|
+
regex: /\b[A-Za-z0-9-]{40,}\b/,
|
|
896
|
+
accept: isHighEntropySecretRun
|
|
897
|
+
}
|
|
864
898
|
]);
|
|
899
|
+
var CONTENT_HASH_RUN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/i;
|
|
900
|
+
function isHighEntropySecretRun(run) {
|
|
901
|
+
if (CONTENT_HASH_RUN.test(run)) {
|
|
902
|
+
return false;
|
|
903
|
+
}
|
|
904
|
+
if (!/[A-Za-z]/.test(run) || !/\d/.test(run)) {
|
|
905
|
+
return false;
|
|
906
|
+
}
|
|
907
|
+
if (highEntropyCharClassCount(run) < 2) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
return highEntropyShannonBits(run) >= 3;
|
|
911
|
+
}
|
|
912
|
+
function highEntropyCharClassCount(value) {
|
|
913
|
+
let count = 0;
|
|
914
|
+
if (/[a-z]/.test(value))
|
|
915
|
+
count++;
|
|
916
|
+
if (/[A-Z]/.test(value))
|
|
917
|
+
count++;
|
|
918
|
+
if (/[0-9]/.test(value))
|
|
919
|
+
count++;
|
|
920
|
+
return count;
|
|
921
|
+
}
|
|
922
|
+
function highEntropyShannonBits(value) {
|
|
923
|
+
if (value.length === 0) {
|
|
924
|
+
return 0;
|
|
925
|
+
}
|
|
926
|
+
const counts = /* @__PURE__ */ new Map();
|
|
927
|
+
for (const char of value) {
|
|
928
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
929
|
+
}
|
|
930
|
+
let bits = 0;
|
|
931
|
+
for (const count of counts.values()) {
|
|
932
|
+
const p = count / value.length;
|
|
933
|
+
bits -= p * Math.log2(p);
|
|
934
|
+
}
|
|
935
|
+
return bits;
|
|
936
|
+
}
|
|
865
937
|
function isForbiddenCustodyFieldName(key) {
|
|
866
938
|
return /^(apiKey|secretValue|bearerHash|signedUrl|objectStoreKey|objectKey|vaultId|providerResponseBody|responseBody|privateResourceHandle|resourceHandle|rawBody)$/i.test(key);
|
|
867
939
|
}
|
package/dist/cli.mjs.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d05e5e074b577553d20fa3680f834b7941fe0cb7692c0a743975fcf36c4ec9b5 cli.mjs
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aexhq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "TypeScript SDK for running autonomous agent sessions across providers (Anthropic, OpenAI, DeepSeek, Gemini, Mistral) behind one interface.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"examples"
|
|
27
27
|
],
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@aexhq/contracts": "0.
|
|
29
|
+
"@aexhq/contracts": "0.21.0"
|
|
30
30
|
},
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=20"
|