@aexhq/cli 0.20.0 → 0.22.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/cli.mjs CHANGED
@@ -350,6 +350,25 @@ var SKILL_BUNDLE_LIMITS = {
350
350
  defaultDirMode: 493
351
351
  };
352
352
  var ASSET_ID_PATTERN = /^asset_[A-Za-z0-9_-]{8,128}$/;
353
+ var MOUNT_PATH_PATTERN = /^\/(?:[^/\0\\]+\/?)*$/;
354
+ var MOUNT_PATH_MAX_LENGTH = 512;
355
+ function assertValidMountPath(value, field) {
356
+ if (value.length === 0 || value.length > MOUNT_PATH_MAX_LENGTH) {
357
+ throw new Error(`${field} must be 1..${MOUNT_PATH_MAX_LENGTH} chars`);
358
+ }
359
+ if (!value.startsWith("/")) {
360
+ throw new Error(`${field} must be an absolute path starting with '/'`);
361
+ }
362
+ if (value.includes("\0") || value.includes("\\")) {
363
+ throw new Error(`${field} must not contain NUL or backslash`);
364
+ }
365
+ if (value.split("/").some((seg) => seg === "..")) {
366
+ throw new Error(`${field} must not contain '..' traversal segments`);
367
+ }
368
+ if (!MOUNT_PATH_PATTERN.test(value)) {
369
+ throw new Error(`${field} must match ${MOUNT_PATH_PATTERN.source}`);
370
+ }
371
+ }
353
372
  function parseSkillRef(input, path) {
354
373
  if (input === null || typeof input !== "object" || Array.isArray(input)) {
355
374
  throw new Error(`${path} must be a SkillRef object`);
@@ -401,8 +420,11 @@ function parseAssetRefFields(record, path) {
401
420
  throw new Error(`${path}.name must be a non-empty string (<= 128 chars)`);
402
421
  }
403
422
  const mountPath = record.mountPath;
404
- if (mountPath !== void 0 && (typeof mountPath !== "string" || mountPath.length === 0)) {
405
- throw new Error(`${path}.mountPath, when provided, must be a non-empty string`);
423
+ if (mountPath !== void 0) {
424
+ if (typeof mountPath !== "string") {
425
+ throw new Error(`${path}.mountPath, when provided, must be a string`);
426
+ }
427
+ assertValidMountPath(mountPath, `${path}.mountPath`);
406
428
  }
407
429
  return {
408
430
  kind: "asset",
@@ -838,7 +860,7 @@ function visitCustodyValue(input, path, findings) {
838
860
  }
839
861
  function scanStringValue(value, path, findings) {
840
862
  for (const pattern of forbiddenStringPatterns) {
841
- if (pattern.regex.test(value)) {
863
+ if (matchesForbiddenPattern(pattern, value)) {
842
864
  findings.push(Object.freeze({
843
865
  path,
844
866
  reason: pattern.reason,
@@ -847,11 +869,32 @@ function scanStringValue(value, path, findings) {
847
869
  }
848
870
  }
849
871
  }
872
+ function matchesForbiddenPattern(pattern, value) {
873
+ if (!pattern.accept) {
874
+ return pattern.regex.test(value);
875
+ }
876
+ const scan = pattern.regex.global ? pattern.regex : new RegExp(pattern.regex.source, `${pattern.regex.flags}g`);
877
+ scan.lastIndex = 0;
878
+ let match;
879
+ while ((match = scan.exec(value)) !== null) {
880
+ if (pattern.accept(match[0])) {
881
+ return true;
882
+ }
883
+ if (match.index === scan.lastIndex) {
884
+ scan.lastIndex++;
885
+ }
886
+ }
887
+ return false;
888
+ }
850
889
  var forbiddenStringPatterns = Object.freeze([
851
890
  { reason: "bearer_token", regex: /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/i },
852
891
  {
853
892
  reason: "provider_key",
854
- regex: /\b(?:sk-(?:ant|proj|live|test|deepseek|openai)|xox[baprs]-|AIza)[A-Za-z0-9_-]{8,}/i
893
+ // Prefixed provider keys (`sk-…`, Slack `xox*-…`, Google `AIza…`). The bare
894
+ // `sk-` body is intentionally generic so an unrecognised vendor's `sk-` key
895
+ // (e.g. DeepSeek `sk-<hex>`, OpenRouter `sk-or-…`) is still caught by shape,
896
+ // not left to the narrowed entropy catch-all below.
897
+ regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{8,}|AIza[A-Za-z0-9_-]{8,})/i
855
898
  },
856
899
  { reason: "signed_url", regex: /[?&](?:X-Amz-Signature|X-Amz-Credential|X-Amz-Algorithm|AWSAccessKeyId)=/i },
857
900
  { reason: "object_store_key", regex: /(^|[\s"'`])(?:runs|assets)\/[^?<#\s"'`]+/i },
@@ -860,8 +903,59 @@ var forbiddenStringPatterns = Object.freeze([
860
903
  reason: "private_resource_handle",
861
904
  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
905
  },
863
- { reason: "high_entropy_token", regex: /\b(?=[A-Za-z0-9_-]{40,}\b)(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{40,}\b/ }
906
+ {
907
+ reason: "high_entropy_token",
908
+ // Catch-all for an unrecognised opaque secret blob. The candidate run
909
+ // EXCLUDES `_` (so `SCREAMING_SNAKE` env names and `slug_with_words` URL
910
+ // segments split instead of fusing into a phantom 40-char run — the live
911
+ // false positive was `ted_season_2_peacock_official_discussion_thread` in
912
+ // web-search result text and a fetched URL), and the `accept` predicate
913
+ // vetoes content-addressed hashes (md5/sha1/sha256 digests — the platform's
914
+ // OWN asset filenames) and low-entropy / single-class runs so only genuine
915
+ // opaque secrets remain. Slash-bearing secrets (signed URLs, connection
916
+ // strings, `Bearer …`) are covered by the named patterns above.
917
+ regex: /\b[A-Za-z0-9-]{40,}\b/,
918
+ accept: isHighEntropySecretRun
919
+ }
864
920
  ]);
921
+ var CONTENT_HASH_RUN = /^(?:[0-9a-f]{32}|[0-9a-f]{40}|[0-9a-f]{64})$/i;
922
+ function isHighEntropySecretRun(run) {
923
+ if (CONTENT_HASH_RUN.test(run)) {
924
+ return false;
925
+ }
926
+ if (!/[A-Za-z]/.test(run) || !/\d/.test(run)) {
927
+ return false;
928
+ }
929
+ if (highEntropyCharClassCount(run) < 2) {
930
+ return false;
931
+ }
932
+ return highEntropyShannonBits(run) >= 3;
933
+ }
934
+ function highEntropyCharClassCount(value) {
935
+ let count = 0;
936
+ if (/[a-z]/.test(value))
937
+ count++;
938
+ if (/[A-Z]/.test(value))
939
+ count++;
940
+ if (/[0-9]/.test(value))
941
+ count++;
942
+ return count;
943
+ }
944
+ function highEntropyShannonBits(value) {
945
+ if (value.length === 0) {
946
+ return 0;
947
+ }
948
+ const counts = /* @__PURE__ */ new Map();
949
+ for (const char of value) {
950
+ counts.set(char, (counts.get(char) ?? 0) + 1);
951
+ }
952
+ let bits = 0;
953
+ for (const count of counts.values()) {
954
+ const p = count / value.length;
955
+ bits -= p * Math.log2(p);
956
+ }
957
+ return bits;
958
+ }
865
959
  function isForbiddenCustodyFieldName(key) {
866
960
  return /^(apiKey|secretValue|bearerHash|signedUrl|objectStoreKey|objectKey|vaultId|providerResponseBody|responseBody|privateResourceHandle|resourceHandle|rawBody)$/i.test(key);
867
961
  }
@@ -1 +1 @@
1
- cbf31722f64f1ca909287456236e1e689291516e227dcdd69935300bbc0994c9 cli.mjs
1
+ bfd0fb573fa113fe08d69fc50f246c821bfc9e0acd9bddd6e92775d38120e201 cli.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexhq/cli",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/aexhq/aex.git"
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "fflate": "^0.8.2",
30
- "@aexhq/contracts": "0.20.0"
30
+ "@aexhq/contracts": "0.22.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "esbuild": "^0.28.0"