agent-first-data 0.9.0 → 0.10.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/README.md CHANGED
@@ -92,6 +92,13 @@ redactedValueWith(value: unknown, redactionPolicy: RedactionPolicy): JsonValue
92
92
  redactedValueWithOptions(value: unknown, redactionOptions: RedactionOptions): JsonValue
93
93
  ```
94
94
 
95
+ `_url` fields are scrubbed in place during redaction (userinfo password, plus query parameters whose name follows the `_secret`/`secretNames` rule). For a URL inside a free-form string, redact it directly before interpolating:
96
+
97
+ ```typescript
98
+ redactUrlSecrets(url: string): string
99
+ redactUrlSecretsWithOptions(url: string, redactionOptions: RedactionOptions): string
100
+ ```
101
+
95
102
  **Use case:** structured protocol payloads (frameworks automatically serialize)
96
103
 
97
104
  **Example:**
package/dist/format.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
- * 16 public APIs and 4 types: protocol builders, redacted value helpers,
5
- * output formatters, redaction, parseSize, RedactionPolicy, RedactionOptions,
6
- * OutputStyle, and OutputOptions.
4
+ * 18 public APIs and 4 types: protocol builders, value redactors (copy and
5
+ * in-place; cover _secret and _url fields), output formatters, URL-string
6
+ * redactors (redactUrlSecrets / WithOptions), parseSize, RedactionPolicy,
7
+ * RedactionOptions, OutputStyle, and OutputOptions.
7
8
  */
8
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
9
10
  [key: string]: JsonValue;
@@ -24,7 +25,9 @@ export type RedactionOptions = {
24
25
  policy?: RedactionPolicy;
25
26
  /**
26
27
  * Field names to treat as secrets in addition to _secret suffixes.
27
- * Matching is exact field-name equality at any nesting level.
28
+ * Matching is exact field-name equality at any nesting level. The same
29
+ * list also matches URL query-parameter names inside _url fields (see
30
+ * redactUrlSecrets).
28
31
  */
29
32
  secretNames?: readonly string[];
30
33
  };
@@ -62,6 +65,24 @@ export declare function redactedValue(value: unknown): JsonValue;
62
65
  export declare function redactedValueWith(value: unknown, redactionPolicy: RedactionPolicy): JsonValue;
63
66
  /** Return a JSON-safe copy with explicit redaction options applied. */
64
67
  export declare function redactedValueWithOptions(value: unknown, redactionOptions: RedactionOptions): JsonValue;
68
+ /**
69
+ * Redact secret components of a single URL string, using default options.
70
+ *
71
+ * Returns url with its userinfo password and any _secret-suffixed query
72
+ * parameter values replaced by "***". See redactUrlSecretsWithOptions.
73
+ */
74
+ export declare function redactUrlSecrets(url: string): string;
75
+ /**
76
+ * Redact secret components of a single URL string.
77
+ *
78
+ * A query parameter is redacted iff its (form-decoded) name ends in
79
+ * _secret/_SECRET or matches an exact entry in secretNames. The userinfo
80
+ * password (scheme://user:pass@host) is always redacted as a structural rule.
81
+ * Only the secret spans are replaced with "***"; every other byte is preserved.
82
+ * A string that is not a single, whitespace-free, scheme-prefixed URL
83
+ * (including a URL embedded in surrounding prose) is returned unchanged.
84
+ */
85
+ export declare function redactUrlSecretsWithOptions(url: string, redactionOptions: RedactionOptions): string;
65
86
  /**
66
87
  * Parse a human-readable size string into bytes.
67
88
  * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
package/dist/format.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * AFDATA output formatting and protocol templates.
3
3
  *
4
- * 16 public APIs and 4 types: protocol builders, redacted value helpers,
5
- * output formatters, redaction, parseSize, RedactionPolicy, RedactionOptions,
6
- * OutputStyle, and OutputOptions.
4
+ * 18 public APIs and 4 types: protocol builders, value redactors (copy and
5
+ * in-place; cover _secret and _url fields), output formatters, URL-string
6
+ * redactors (redactUrlSecrets / WithOptions), parseSize, RedactionPolicy,
7
+ * RedactionOptions, OutputStyle, and OutputOptions.
7
8
  */
8
9
  // ═══════════════════════════════════════════
9
10
  // Public API: Protocol Builders
@@ -122,6 +123,29 @@ export function redactedValueWithOptions(value, redactionOptions) {
122
123
  applyRedactionOptions(v, redactionOptions);
123
124
  return v;
124
125
  }
126
+ /**
127
+ * Redact secret components of a single URL string, using default options.
128
+ *
129
+ * Returns url with its userinfo password and any _secret-suffixed query
130
+ * parameter values replaced by "***". See redactUrlSecretsWithOptions.
131
+ */
132
+ export function redactUrlSecrets(url) {
133
+ return redactUrlSecretsWithOptions(url, {});
134
+ }
135
+ /**
136
+ * Redact secret components of a single URL string.
137
+ *
138
+ * A query parameter is redacted iff its (form-decoded) name ends in
139
+ * _secret/_SECRET or matches an exact entry in secretNames. The userinfo
140
+ * password (scheme://user:pass@host) is always redacted as a structural rule.
141
+ * Only the secret spans are replaced with "***"; every other byte is preserved.
142
+ * A string that is not a single, whitespace-free, scheme-prefixed URL
143
+ * (including a URL embedded in surrounding prose) is returned unchanged.
144
+ */
145
+ export function redactUrlSecretsWithOptions(url, redactionOptions) {
146
+ const redacted = redactUrlInStr(url, secretNameSet(redactionOptions));
147
+ return redacted ?? url;
148
+ }
125
149
  /**
126
150
  * Parse a human-readable size string into bytes.
127
151
  * Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
@@ -158,82 +182,240 @@ export function parseSize(s) {
158
182
  return null;
159
183
  return result;
160
184
  }
161
- // ═══════════════════════════════════════════
162
- // Secret Redaction
163
- // ═══════════════════════════════════════════
164
- const EMPTY_SECRET_NAMES = new Set();
185
+ const DEFAULT_CONTEXT = {
186
+ secretNames: new Set(),
187
+ };
165
188
  function secretNameSet(redactionOptions) {
166
189
  return new Set(redactionOptions.secretNames ?? []);
167
190
  }
191
+ function contextFromOptions(redactionOptions) {
192
+ return { secretNames: secretNameSet(redactionOptions) };
193
+ }
168
194
  function keyHasSecretSuffix(key) {
169
195
  return key.endsWith("_secret") || key.endsWith("_SECRET");
170
196
  }
197
+ function keyHasUrlSuffix(key) {
198
+ return key.endsWith("_url") || key.endsWith("_URL");
199
+ }
171
200
  function isSecretKey(key, secretNames) {
172
201
  return keyHasSecretSuffix(key) || secretNames.has(key);
173
202
  }
174
- function redactSecrets(value, secretNames = EMPTY_SECRET_NAMES) {
203
+ function redactSecrets(value, context = DEFAULT_CONTEXT) {
175
204
  if (isObject(value)) {
176
205
  for (const k of Object.keys(value)) {
177
- if (isSecretKey(k, secretNames)) {
178
- const v = value[k];
206
+ const v = value[k];
207
+ if (isSecretKey(k, context.secretNames)) {
179
208
  if (isObject(v) || Array.isArray(v)) {
180
- redactSecrets(v, secretNames);
209
+ redactSecrets(v, context);
181
210
  }
182
211
  else {
183
212
  value[k] = "***";
184
213
  }
185
214
  }
215
+ else if (keyHasUrlSuffix(k) && typeof v === "string") {
216
+ const redacted = redactUrlInStr(v, context.secretNames);
217
+ if (redacted !== null)
218
+ value[k] = redacted;
219
+ }
186
220
  else {
187
- redactSecrets(value[k], secretNames);
221
+ redactSecrets(v, context);
188
222
  }
189
223
  }
190
224
  }
191
225
  else if (Array.isArray(value)) {
192
- for (const item of value) {
193
- redactSecrets(item, secretNames);
226
+ for (let i = 0; i < value.length; i++) {
227
+ redactSecrets(value[i], context);
194
228
  }
195
229
  }
196
230
  }
197
- function redactSecretsStrict(value, secretNames = EMPTY_SECRET_NAMES) {
231
+ function redactSecretsStrict(value, context = DEFAULT_CONTEXT) {
198
232
  if (isObject(value)) {
199
233
  for (const k of Object.keys(value)) {
200
- if (isSecretKey(k, secretNames)) {
234
+ const v = value[k];
235
+ if (isSecretKey(k, context.secretNames)) {
201
236
  value[k] = "***";
202
237
  }
238
+ else if (keyHasUrlSuffix(k) && typeof v === "string") {
239
+ const redacted = redactUrlInStr(v, context.secretNames);
240
+ if (redacted !== null)
241
+ value[k] = redacted;
242
+ }
203
243
  else {
204
- redactSecretsStrict(value[k], secretNames);
244
+ redactSecretsStrict(v, context);
205
245
  }
206
246
  }
207
247
  }
208
248
  else if (Array.isArray(value)) {
209
- for (const item of value) {
210
- redactSecretsStrict(item, secretNames);
249
+ for (let i = 0; i < value.length; i++) {
250
+ redactSecretsStrict(value[i], context);
211
251
  }
212
252
  }
213
253
  }
214
254
  function applyRedactionPolicy(value, redactionPolicy) {
215
- applyRedactionPolicyWithNames(value, redactionPolicy, EMPTY_SECRET_NAMES);
255
+ applyRedactionPolicyWithContext(value, redactionPolicy, DEFAULT_CONTEXT);
216
256
  }
217
257
  function applyRedactionOptions(value, redactionOptions) {
218
- applyRedactionPolicyWithNames(value, redactionOptions.policy, secretNameSet(redactionOptions));
258
+ applyRedactionPolicyWithContext(value, redactionOptions.policy, contextFromOptions(redactionOptions));
219
259
  }
220
- function applyRedactionPolicyWithNames(value, redactionPolicy, secretNames) {
260
+ function applyRedactionPolicyWithContext(value, redactionPolicy, context) {
221
261
  switch (redactionPolicy) {
222
262
  case RedactionPolicy.RedactionTraceOnly:
223
263
  if (isObject(value) && value.trace !== undefined) {
224
- redactSecrets(value.trace, secretNames);
264
+ redactSecrets(value.trace, context);
225
265
  }
226
266
  break;
227
267
  case RedactionPolicy.RedactionNone:
228
268
  break;
229
269
  case RedactionPolicy.RedactionStrict:
230
- redactSecretsStrict(value, secretNames);
270
+ redactSecretsStrict(value, context);
231
271
  break;
232
272
  default:
233
273
  // Empty/unknown policy falls back to default full redaction.
234
- redactSecrets(value, secretNames);
274
+ redactSecrets(value, context);
275
+ break;
276
+ }
277
+ }
278
+ // ═══════════════════════════════════════════
279
+ // URL Secret Redaction
280
+ // ═══════════════════════════════════════════
281
+ /**
282
+ * Redact secret components of a single URL string, returning the redacted URL
283
+ * when s is a processable URL, or null when it is not (so callers can keep the
284
+ * original). Only secret spans change; all other bytes are preserved.
285
+ */
286
+ function redactUrlInStr(s, secretNames) {
287
+ // Fast path + precondition: a single, whitespace-free, scheme-prefixed URL.
288
+ if (!s.includes("://") || !isSingleUrl(s) || !urlParses(s)) {
289
+ return null;
290
+ }
291
+ const schemeSep = s.indexOf("://");
292
+ const scheme = s.slice(0, schemeSep);
293
+ const rest = s.slice(schemeSep + 3);
294
+ // Authority runs from after "://" to the first '/', '?', or '#'.
295
+ let authEnd = rest.length;
296
+ for (let i = 0; i < rest.length; i++) {
297
+ const c = rest[i];
298
+ if (c === "/" || c === "?" || c === "#") {
299
+ authEnd = i;
300
+ break;
301
+ }
302
+ }
303
+ const authority = rest.slice(0, authEnd);
304
+ const remainder = rest.slice(authEnd);
305
+ const newAuthority = redactUserinfoPassword(authority);
306
+ // Query runs from the first '?' to the first '#' (or end).
307
+ let newRemainder;
308
+ const q = remainder.indexOf("?");
309
+ if (q >= 0) {
310
+ const path = remainder.slice(0, q);
311
+ const qOnwards = remainder.slice(q + 1);
312
+ const hash = qOnwards.indexOf("#");
313
+ let query;
314
+ let fragment;
315
+ if (hash >= 0) {
316
+ query = qOnwards.slice(0, hash);
317
+ fragment = qOnwards.slice(hash);
318
+ }
319
+ else {
320
+ query = qOnwards;
321
+ fragment = "";
322
+ }
323
+ newRemainder = `${path}?${redactQuery(query, secretNames)}${fragment}`;
324
+ }
325
+ else {
326
+ newRemainder = remainder;
327
+ }
328
+ return `${scheme}://${newAuthority}${newRemainder}`;
329
+ }
330
+ /**
331
+ * Replace the userinfo password (user:pass@) with "***", preserving the
332
+ * username. Authority without '@', or userinfo without ':', is unchanged.
333
+ */
334
+ function redactUserinfoPassword(authority) {
335
+ const at = authority.indexOf("@");
336
+ if (at < 0)
337
+ return authority;
338
+ const userinfo = authority.slice(0, at);
339
+ const colon = userinfo.indexOf(":");
340
+ if (colon < 0)
341
+ return authority;
342
+ return `${authority.slice(0, colon)}:***${authority.slice(at)}`;
343
+ }
344
+ /**
345
+ * Redact the values of secret-named query parameters, preserving raw bytes of
346
+ * every other segment (keys, benign values, encoding, ordering, separators).
347
+ */
348
+ function redactQuery(query, secretNames) {
349
+ return query
350
+ .split("&")
351
+ .map((segment) => {
352
+ const eq = segment.indexOf("=");
353
+ if (eq < 0)
354
+ return segment;
355
+ const rawKey = segment.slice(0, eq);
356
+ const name = formDecode(rawKey);
357
+ if (isSecretKey(name, secretNames)) {
358
+ return `${rawKey}=***`;
359
+ }
360
+ return segment;
361
+ })
362
+ .join("&");
363
+ }
364
+ /** Form-decode a query-parameter name: '+' → space, then percent-decode. */
365
+ function formDecode(rawKey) {
366
+ const withSpaces = rawKey.replace(/\+/g, " ");
367
+ try {
368
+ return decodeURIComponent(withSpaces);
369
+ }
370
+ catch {
371
+ return withSpaces;
372
+ }
373
+ }
374
+ /** True when s parses as a URL (mirrors Rust's url::Url::parse check). */
375
+ function urlParses(s) {
376
+ try {
377
+ new URL(s);
378
+ return true;
379
+ }
380
+ catch {
381
+ return false;
382
+ }
383
+ }
384
+ /**
385
+ * True when s begins with a URL scheme (ALPHA *(ALPHA / DIGIT / "+" / "-" /
386
+ * ".") "://") and contains no ASCII whitespace — i.e. a single bare URL, not a
387
+ * URL embedded in prose.
388
+ */
389
+ function isSingleUrl(s) {
390
+ for (let i = 0; i < s.length; i++) {
391
+ const c = s.charCodeAt(i);
392
+ // ASCII whitespace: tab, LF, VT, FF, CR, space.
393
+ if (c === 0x09 || c === 0x0a || c === 0x0b || c === 0x0c || c === 0x0d || c === 0x20) {
394
+ return false;
395
+ }
396
+ }
397
+ if (s.length === 0)
398
+ return false;
399
+ const first = s.charCodeAt(0);
400
+ if (!isAsciiAlpha(first))
401
+ return false;
402
+ let i = 1;
403
+ while (i < s.length) {
404
+ const c = s.charCodeAt(i);
405
+ if (isAsciiAlphanumeric(c) || c === 0x2b /* + */ || c === 0x2d /* - */ || c === 0x2e /* . */) {
406
+ i += 1;
407
+ }
408
+ else {
235
409
  break;
410
+ }
236
411
  }
412
+ return s.slice(i).startsWith("://");
413
+ }
414
+ function isAsciiAlpha(c) {
415
+ return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
416
+ }
417
+ function isAsciiAlphanumeric(c) {
418
+ return isAsciiAlpha(c) || (c >= 0x30 && c <= 0x39);
237
419
  }
238
420
  // ═══════════════════════════════════════════
239
421
  // Suffix Processing
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, type RedactionOptions, OutputStyle, type OutputOptions, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, parseSize, } from "./format.js";
4
+ export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, type RedactionOptions, OutputStyle, type OutputOptions, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, } from "./format.js";
5
5
  export { log, span, initJson, initPlain, initYaml } from "./afdata_logging.js";
6
6
  export { type OutputFormat, cliParseOutput, cliParseLogFilters, cliOutput, cliOutputWithOptions, buildCliError, } from "./cli.js";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates.
3
3
  */
4
- export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, OutputStyle, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, parseSize, } from "./format.js";
4
+ export { buildJsonOk, buildJsonError, buildJson, RedactionPolicy, OutputStyle, outputJson, outputJsonWith, outputJsonWithOptions, outputYaml, outputYamlWithOptions, outputPlain, outputPlainWithOptions, internalRedactSecrets, internalRedactSecretsWithOptions, redactedValue, redactedValueWith, redactedValueWithOptions, redactUrlSecrets, redactUrlSecretsWithOptions, parseSize, } from "./format.js";
5
5
  export { log, span, initJson, initPlain, initYaml } from "./afdata_logging.js";
6
6
  export { cliParseOutput, cliParseLogFilters, cliOutput, cliOutputWithOptions, buildCliError, } from "./cli.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-first-data",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A naming convention that lets AI agents understand your data without being told what it means.",
5
5
  "license": "MIT",
6
6
  "repository": {