agent-first-data 0.35.0 → 0.35.1
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/format.js +88 -33
- package/package.json +3 -3
package/dist/format.js
CHANGED
|
@@ -869,7 +869,7 @@ function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
|
|
|
869
869
|
* when s is a processable URL, or null when it is not (so callers can keep the
|
|
870
870
|
* original). Only secret spans change; all other bytes are preserved.
|
|
871
871
|
*/
|
|
872
|
-
function redactUrlInStr(s, secretNames) {
|
|
872
|
+
function redactUrlInStr(s, secretNames, depth = 0) {
|
|
873
873
|
// Precondition (spec): a single, whitespace-free, scheme-prefixed URL.
|
|
874
874
|
// The gate is scheme + no-whitespace only — NOT "parses via `new URL`". Span
|
|
875
875
|
// location below is purely byte-wise (we never re-serialize the URL), so a
|
|
@@ -879,6 +879,9 @@ function redactUrlInStr(s, secretNames) {
|
|
|
879
879
|
if (!s.includes("://") || !isSingleUrl(s)) {
|
|
880
880
|
return null;
|
|
881
881
|
}
|
|
882
|
+
// Bound recursive redirect inspection, failing closed before stack growth.
|
|
883
|
+
if (depth >= 16)
|
|
884
|
+
return REDACTED_MARKER;
|
|
882
885
|
const schemeSep = s.indexOf("://");
|
|
883
886
|
const scheme = s.slice(0, schemeSep);
|
|
884
887
|
const rest = s.slice(schemeSep + 3);
|
|
@@ -891,8 +894,31 @@ function redactUrlInStr(s, secretNames) {
|
|
|
891
894
|
break;
|
|
892
895
|
}
|
|
893
896
|
}
|
|
897
|
+
// An unencoded '?' or '#' inside a password ends that authority early and
|
|
898
|
+
// leaves the rest of the password in the query or fragment. When a ':'
|
|
899
|
+
// precedes that early end and an '@' follows it before the first '/', the
|
|
900
|
+
// userinfo runs to that '@' — how libpq reads such a connection string.
|
|
901
|
+
const slash = rest.indexOf("/");
|
|
902
|
+
const firstSlash = slash < 0 ? rest.length : slash;
|
|
903
|
+
const at = rest.slice(0, firstSlash).lastIndexOf("@");
|
|
904
|
+
if (rest.slice(0, authEnd).includes(":") && at > authEnd) {
|
|
905
|
+
authEnd = rest.length;
|
|
906
|
+
for (let i = at; i < rest.length; i++) {
|
|
907
|
+
const c = rest[i];
|
|
908
|
+
if (c === "/" || c === "?" || c === "#") {
|
|
909
|
+
authEnd = i;
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
894
914
|
const authority = rest.slice(0, authEnd);
|
|
895
915
|
const remainder = rest.slice(authEnd);
|
|
916
|
+
// An unencoded '/' inside a password ends the authority at a "port" that is
|
|
917
|
+
// not a number, with the '@' somewhere after it. Where the password ends
|
|
918
|
+
// cannot be known, so the whole value is replaced.
|
|
919
|
+
if (!authority.includes("@") && hasNonNumericPort(authority) && remainder.includes("@")) {
|
|
920
|
+
return REDACTED_MARKER;
|
|
921
|
+
}
|
|
896
922
|
const newAuthority = redactUserinfoPassword(authority);
|
|
897
923
|
// `remainder` is `path[?query][#fragment]`; '#' ends the query, so split the
|
|
898
924
|
// fragment off first and the query out of what is left.
|
|
@@ -901,14 +927,26 @@ function redactUrlInStr(s, secretNames) {
|
|
|
901
927
|
const fragment = hash >= 0 ? remainder.slice(hash + 1) : null;
|
|
902
928
|
const q = beforeFragment.indexOf("?");
|
|
903
929
|
const newBeforeFragment = q >= 0
|
|
904
|
-
? `${beforeFragment.slice(0, q)}?${redactQuery(beforeFragment.slice(q + 1), secretNames)}`
|
|
930
|
+
? `${beforeFragment.slice(0, q)}?${redactQuery(beforeFragment.slice(q + 1), secretNames, depth)}`
|
|
905
931
|
: beforeFragment;
|
|
906
932
|
// A fragment gets the same treatment as the query: `k=v&k=v` after the '#' is
|
|
907
933
|
// exactly how an OAuth implicit-flow response hands back a token, so a
|
|
908
934
|
// secret-named fragment parameter must not survive where the identically
|
|
909
935
|
// named query parameter would not. A fragment that is not in that shape has
|
|
910
936
|
// no '=' in its segments and passes through byte-for-byte.
|
|
911
|
-
|
|
937
|
+
// A hash route (`#/cb?k=v`) carries its parameters after its first '?',
|
|
938
|
+
// while a form reader keeps that '?' inside a value; redact both readings:
|
|
939
|
+
// the fragment as parameters first, so a secret value absorbs its '?',
|
|
940
|
+
// then whatever follows the first '?' that survives.
|
|
941
|
+
let newFragment = "";
|
|
942
|
+
if (fragment !== null) {
|
|
943
|
+
const params = redactQuery(fragment, secretNames, depth);
|
|
944
|
+
const routeQuery = params.indexOf("?");
|
|
945
|
+
newFragment =
|
|
946
|
+
routeQuery >= 0
|
|
947
|
+
? `#${params.slice(0, routeQuery)}?${redactQuery(params.slice(routeQuery + 1), secretNames, depth)}`
|
|
948
|
+
: `#${params}`;
|
|
949
|
+
}
|
|
912
950
|
return `${scheme}://${newAuthority}${newBeforeFragment}${newFragment}`;
|
|
913
951
|
}
|
|
914
952
|
function redactUrlsInTextWithContext(text, context) {
|
|
@@ -1071,7 +1109,7 @@ function redactUserinfoPassword(authority) {
|
|
|
1071
1109
|
* Redact the values of secret-named query parameters, preserving raw bytes of
|
|
1072
1110
|
* every other segment (keys, benign values, encoding, ordering, separators).
|
|
1073
1111
|
*/
|
|
1074
|
-
function redactQuery(query, secretNames) {
|
|
1112
|
+
function redactQuery(query, secretNames, depth) {
|
|
1075
1113
|
return query
|
|
1076
1114
|
.split("&")
|
|
1077
1115
|
.map((segment) => {
|
|
@@ -1083,19 +1121,33 @@ function redactQuery(query, secretNames) {
|
|
|
1083
1121
|
if (isSecretKey(name, secretNames)) {
|
|
1084
1122
|
return `${rawKey}=${REDACTED_MARKER}`;
|
|
1085
1123
|
}
|
|
1124
|
+
// A redirect target carries its own parameters: redact a nested URL in
|
|
1125
|
+
// place, or the whole value when it arrived encoded.
|
|
1126
|
+
const rawValue = segment.slice(eq + 1);
|
|
1127
|
+
const value = formDecode(rawValue);
|
|
1128
|
+
const redacted = redactUrlInStr(value, secretNames, depth + 1);
|
|
1129
|
+
if (redacted !== null && redacted !== value) {
|
|
1130
|
+
return value === rawValue ? `${rawKey}=${redacted}` : `${rawKey}=${REDACTED_MARKER}`;
|
|
1131
|
+
}
|
|
1086
1132
|
return segment;
|
|
1087
1133
|
})
|
|
1088
1134
|
.join("&");
|
|
1089
1135
|
}
|
|
1090
|
-
/**
|
|
1136
|
+
/**
|
|
1137
|
+
* True when the authority's text after its last ':' is not a port number. A
|
|
1138
|
+
* bracketed IPv6 literal without a port ends in ']' and has none.
|
|
1139
|
+
*/
|
|
1140
|
+
function hasNonNumericPort(authority) {
|
|
1141
|
+
const colon = authority.lastIndexOf(":");
|
|
1142
|
+
if (colon < 0 || authority.endsWith("]"))
|
|
1143
|
+
return false;
|
|
1144
|
+
return !/^[0-9]*$/.test(authority.slice(colon + 1));
|
|
1145
|
+
}
|
|
1146
|
+
/** Form-decode a query-parameter name or value: '+' → space, then percent-decode. */
|
|
1091
1147
|
function formDecode(rawKey) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
return withSpaces;
|
|
1098
|
-
}
|
|
1148
|
+
// URLSearchParams keeps malformed escapes literal and replaces invalid
|
|
1149
|
+
// UTF-8, while still decoding valid escapes elsewhere in the value.
|
|
1150
|
+
return new URLSearchParams(`value=${rawKey.replace(/&/g, "%26")}`).get("value") ?? "";
|
|
1099
1151
|
}
|
|
1100
1152
|
/**
|
|
1101
1153
|
* True when s begins with a URL scheme (ALPHA *(ALPHA / DIGIT / "+" / "-" /
|
|
@@ -1182,28 +1234,30 @@ function decimalIntText(value) {
|
|
|
1182
1234
|
}
|
|
1183
1235
|
const MIN_I64 = -9223372036854775808n;
|
|
1184
1236
|
const MAX_I64 = 9223372036854775807n;
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1237
|
+
const MAX_U64 = 18446744073709551615n;
|
|
1238
|
+
// An integer literal stays exact; a decoded integral float (`3.0`) counts as
|
|
1239
|
+
// an integer like any other.
|
|
1240
|
+
function integerValue(value) {
|
|
1241
|
+
if (typeof value === "bigint")
|
|
1242
|
+
return value;
|
|
1243
|
+
if (typeof value === "number")
|
|
1244
|
+
return Number.isInteger(value) ? BigInt(value) : null;
|
|
1245
|
+
if (isLosslessNumber(value)) {
|
|
1194
1246
|
const literal = value.toString();
|
|
1195
1247
|
if (/^-?\d+$/.test(literal))
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
return null;
|
|
1200
|
-
try {
|
|
1201
|
-
const integer = BigInt(text);
|
|
1202
|
-
return integer >= MIN_I64 && integer <= MAX_I64 ? integer : null;
|
|
1203
|
-
}
|
|
1204
|
-
catch {
|
|
1205
|
-
return null;
|
|
1248
|
+
return BigInt(literal);
|
|
1249
|
+
const parsed = Number(literal);
|
|
1250
|
+
return Number.isInteger(parsed) ? BigInt(parsed) : null;
|
|
1206
1251
|
}
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
function signedI64(value) {
|
|
1255
|
+
const integer = integerValue(value);
|
|
1256
|
+
return integer !== null && integer >= MIN_I64 && integer <= MAX_I64 ? integer : null;
|
|
1257
|
+
}
|
|
1258
|
+
function unsignedU64(value) {
|
|
1259
|
+
const integer = integerValue(value);
|
|
1260
|
+
return integer !== null && integer >= 0n && integer <= MAX_U64 ? integer : null;
|
|
1207
1261
|
}
|
|
1208
1262
|
function formatCents(value, symbol) {
|
|
1209
1263
|
const sign = value < 0n ? "-" : "";
|
|
@@ -1355,8 +1409,9 @@ function tryProcessField(key, value) {
|
|
|
1355
1409
|
}
|
|
1356
1410
|
stripped = stripSuffixCI(key, "_bytes");
|
|
1357
1411
|
if (stripped !== null) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1412
|
+
const bytes = unsignedU64(value);
|
|
1413
|
+
if (bytes !== null)
|
|
1414
|
+
return [stripped, formatBytesHuman(Number(bytes))];
|
|
1360
1415
|
return null;
|
|
1361
1416
|
}
|
|
1362
1417
|
stripped = stripSuffixCI(key, "_percent");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-first-data",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"description": "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"test": "tsx --test src/*.test.ts"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.6.
|
|
30
|
+
"@types/node": "^26.6.5",
|
|
31
31
|
"tsx": "^4.23.15",
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"lossless-json": "^4.3.
|
|
35
|
+
"lossless-json": "^4.3.3"
|
|
36
36
|
}
|
|
37
37
|
}
|