@tollbooth-dpyc/web 1.4.0 → 1.4.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/client.js CHANGED
@@ -12,7 +12,7 @@
12
12
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
13
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
14
14
  import { tollboothConfig, toolName } from "./config.js";
15
- import { debugPush } from "./debugLog.js";
15
+ import { debugPush, redact } from "./debugLog.js";
16
16
  import { clearStoredProof, forgetRecentLogin, getStoredNpub, getStoredProof, } from "./identity.js";
17
17
  import { signInlineProof } from "./inlineProof.js";
18
18
  import { isTransportFailure, NetworkError } from "./networkError.js";
@@ -129,7 +129,7 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
129
129
  // `args` holds only the caller's own parameters — the envelope is added
130
130
  // below — so it is safe to log.
131
131
  if (!quiet)
132
- debugPush("call", `${name}(${JSON.stringify(args).slice(0, 140)})`);
132
+ debugPush("call", `${name}(${redact(JSON.stringify(args)).slice(0, 140)})`);
133
133
  const bootstrap = STANDARD_BOOTSTRAP.includes(tool) || cfg.extraBootstrapTools.includes(tool);
134
134
  const merged = bootstrap
135
135
  ? { ...args }
@@ -154,14 +154,14 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
154
154
  if (result.isError) {
155
155
  const text = errorText(result);
156
156
  if (!quiet)
157
- debugPush("error", `${name}: ${text.slice(0, 200)}`);
157
+ debugPush("error", `${name}: ${redact(text).slice(0, 200)}`);
158
158
  throw new Error(text);
159
159
  }
160
160
  const { payload, images } = readToolResult(result);
161
161
  if (!quiet) {
162
162
  const preview = typeof payload === "string" ? payload : JSON.stringify(payload);
163
163
  const tail = images.length ? ` +${images.length} image` : "";
164
- debugPush(looksFailed(payload) ? "error" : "result", `${name} → ${String(preview).slice(0, 220)}${tail}`);
164
+ debugPush(looksFailed(payload) ? "error" : "result", `${name} → ${redact(String(preview)).slice(0, 220)}${tail}`);
165
165
  }
166
166
  if (!opts.bestEffort) {
167
167
  const bounced = proofBounceMessage(payload);
@@ -7,8 +7,10 @@
7
7
  * that flips the view can still be read and copied afterward.
8
8
  *
9
9
  * Every message is scrubbed on the way in: an nsec, a 64-hex key, a bearer
10
- * token, and the value of any secret-named field (dpop_token, proof, poison,
11
- * password, api_key, …) never reach the buffer, the clipboard or storage.
10
+ * token, the value of any secret-named field (dpop_token, proof, poison,
11
+ * password, api_key, …), and a secret inside a URL (a query token, a
12
+ * bearer-like path segment, userinfo) never reach the buffer, the clipboard
13
+ * or storage.
12
14
  * That is the one guarantee a panel relies on — it renders what is here.
13
15
  *
14
16
  * Merged from the fleet's copies (goodearth, beesknees, cypher, excalibur,
package/dist/debugLog.js CHANGED
@@ -7,8 +7,10 @@
7
7
  * that flips the view can still be read and copied afterward.
8
8
  *
9
9
  * Every message is scrubbed on the way in: an nsec, a 64-hex key, a bearer
10
- * token, and the value of any secret-named field (dpop_token, proof, poison,
11
- * password, api_key, …) never reach the buffer, the clipboard or storage.
10
+ * token, the value of any secret-named field (dpop_token, proof, poison,
11
+ * password, api_key, …), and a secret inside a URL (a query token, a
12
+ * bearer-like path segment, userinfo) never reach the buffer, the clipboard
13
+ * or storage.
12
14
  * That is the one guarantee a panel relies on — it renders what is here.
13
15
  *
14
16
  * Merged from the fleet's copies (goodearth, beesknees, cypher, excalibur,
@@ -34,7 +36,7 @@ const SECRET_FIELD = "(?:nsec|ncryptsec|dpop_token|proof|poison|password|passphr
34
36
  // a bare one up to a delimiter. Stopping at the first quote of either kind let
35
37
  // the tail of a secret holding `'` or `\"` through.
36
38
  function fieldValue(names) {
37
- return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])(?:\\\\.|\\\\$|(?!\\2)[^\\\\])*?(?=\\2|$)|[^"'\\\\,}\\]&\\s]+)`, "gi");
39
+ return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])(?:\\\\.|\\\\$|(?!\\2)[^\\\\])*?(?=\\2|$)|(?!\\[redacted\\])[^"'\\\\,}\\]&\\s]+)`, "gi");
38
40
  }
39
41
  const FIELD_VALUE = fieldValue(SECRET_FIELD);
40
42
  // update_patron_credential / update_operator_credential carry the secret as
@@ -47,6 +49,54 @@ const BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]+/g;
47
49
  function scrubbed(_match, head, quote) {
48
50
  return `${head}${quote ?? ""}${REDACTED}`;
49
51
  }
52
+ // ── Secrets inside URLs ────────────────────────────────────────────────────
53
+ //
54
+ // A capability link carries its secret in the URL itself — Good Earth's
55
+ // calendar feed is `https://host/calendar/<32 hex>.ics` — and a link read
56
+ // back in a tool result is logged whole. Three places a URL hides a secret:
57
+ // the userinfo, a query or fragment parameter, and a bearer-like path segment.
58
+ /** Query/fragment parameter names whose value is a secret (matched whole). */
59
+ const URL_SECRET_PARAM = /([?&#;](?:token|access_token|refresh_token|id_token|auth|authorization|key|api_key|apikey|secret|client_secret|password|pass|sig|signature|code|proof|dpop_token|session|sessionid|nsec)=)[^&#;\s"'\\<>]*/gi;
60
+ // A URL up to whitespace, a quote, an escape or an angle bracket — so one
61
+ // inside JSON (or JSON inside JSON) ends at its own closing quote.
62
+ const URL_IN_TEXT = /\b(?:https?|webcal|wss?):\/\/[^\s"'\\<>]+/gi;
63
+ const USERINFO = /^([a-z]+:\/\/)[^/?#@]+@/i;
64
+ /**
65
+ * A path segment that reads as a bearer secret rather than a name: 32+ hex,
66
+ * or 24+ base64url characters holding both a letter and a digit and one
67
+ * unbroken alphanumeric run of 16+ (a slug's words are short; a token's are
68
+ * not). An npub is public and stays.
69
+ */
70
+ function isSecretSegment(seg) {
71
+ if (/^npub1/i.test(seg))
72
+ return false;
73
+ if (/^[0-9a-f]{32,}$/i.test(seg))
74
+ return true;
75
+ if (!/^[A-Za-z0-9_-]{24,}$/.test(seg))
76
+ return false;
77
+ if (!/[0-9]/.test(seg) || !/[A-Za-z]/.test(seg))
78
+ return false;
79
+ return /[A-Za-z0-9]{16,}/.test(seg);
80
+ }
81
+ function scrubUrl(url) {
82
+ const head = url.replace(USERINFO, `$1${REDACTED}@`);
83
+ const pathStart = head.indexOf("/", head.indexOf("://") + 3);
84
+ if (pathStart < 0)
85
+ return head;
86
+ const tailAt = head.slice(pathStart).search(/[?#]/);
87
+ const pathEnd = tailAt < 0 ? head.length : pathStart + tailAt;
88
+ const path = head
89
+ .slice(pathStart, pathEnd)
90
+ .split("/")
91
+ .map((seg) => {
92
+ // `<token>.ics`: judge the name, keep the extension.
93
+ const dot = seg.indexOf(".");
94
+ const name = dot < 0 ? seg : seg.slice(0, dot);
95
+ return isSecretSegment(name) ? REDACTED + (dot < 0 ? "" : seg.slice(dot)) : seg;
96
+ })
97
+ .join("/");
98
+ return head.slice(0, pathStart) + path + head.slice(pathEnd);
99
+ }
50
100
  /**
51
101
  * Scrub secrets from a log line. Deliberately over-eager: a 64-hex event id
52
102
  * is hidden along with a hex private key, because the two look alike.
@@ -55,7 +105,9 @@ export function redact(message) {
55
105
  let out = message
56
106
  .replace(NSEC, REDACTED)
57
107
  .replace(BEARER, `Bearer ${REDACTED}`)
58
- .replace(FIELD_VALUE, scrubbed);
108
+ .replace(URL_IN_TEXT, scrubUrl)
109
+ .replace(FIELD_VALUE, scrubbed)
110
+ .replace(URL_SECRET_PARAM, `$1${REDACTED}`);
59
111
  if (HAS_FIELD.test(out))
60
112
  out = out.replace(CREDENTIAL_VALUE, scrubbed);
61
113
  return out.replace(HEX_KEY, REDACTED);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lonnie VanZandt",