@tokenoftrust/storefront-runner 4.11.6 → 4.11.8

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.
@@ -143,12 +143,22 @@ export async function resolveNamedSubjectGrant(
143
143
  });
144
144
  if (outcome.kind !== "ok") return false;
145
145
  const payload = (await outcome.json()) as Record<string, unknown> | null;
146
- return payload?.capability === capability;
146
+ if (payload?.capability === capability) return true;
147
+ console.warn(`[mcp-service-seam] named grant ${capability} not held: ${namedGrantRefusal(payload)}`);
148
+ return false;
147
149
  } catch {
150
+ console.warn(`[mcp-service-seam] named grant ${capability} not held: unparseable answer`);
148
151
  return false;
149
152
  }
150
153
  }
151
154
 
155
+ /** Why a 2xx named-grant answer did not grant, in the seam's own fixed vocabulary. Never the subject. */
156
+ function namedGrantRefusal(payload: Record<string, unknown> | null): string {
157
+ const reason = typeof payload?.reason === "string" && /^[a-z_-]{1,40}$/.test(payload.reason) ? payload.reason : null;
158
+ if (payload?.capability === null) return reason ? `capability null, reason ${reason}` : "capability null (no grant)";
159
+ return "unexpected capability";
160
+ }
161
+
152
162
  /**
153
163
  * Resolve the viewer's LIVE ship capability over `appDomain`. Returns the granted
154
164
  * `Capability` — `"ship-on-behalf"` ONLY on an explicit `200 { capability:
@@ -12,6 +12,13 @@
12
12
  * A thrown transport failure comes back as one fixed diagnostic, never the thrown value: a
13
13
  * fetch error can carry the request URL (and so its subject query) to every downstream sink.
14
14
  *
15
+ * Every outcome but `ok` logs one `[mcp-service-seam]` line naming the method, the contract path
16
+ * and a fixed reason: the env name for `not-configured` / `bad-url`, the error class (from a fixed
17
+ * list) for `transport-error`, the status otherwise. A `TypeError` also carries its message and its
18
+ * cause's message, scrubbed of URLs, email addresses and the secret, because fetch's wording is
19
+ * what separates a lost connection from one refused before connecting or a TLS trust failure.
20
+ * Never the URL, query or secret, and no other thrown value's text.
21
+ *
15
22
  * A 401 or 404 from a seam route is misconfiguration, never a business answer: tot-mcp's
16
23
  * routes answer 200 for every absent/unknown case, so 401 means the secret or URL is wrong
17
24
  * and 404 means the path is not served. Both log LOUDLY and come back as `misconfigured`
@@ -75,11 +82,50 @@ function misconfigurationReason(status: 401 | 404, urlEnv: string): string {
75
82
  : "tot-mcp does not serve this path; storefront and tot-mcp disagree on the service-seam contract";
76
83
  }
77
84
 
85
+ /** Error classes a transport failure may name in a log line; anything else reads `Error`. */
86
+ const TRANSPORT_ERROR_CLASSES = new Set(["TimeoutError", "AbortError", "TypeError", "SyntaxError"]);
87
+
88
+ /** The class of a thrown transport failure, from a fixed list. Never its message. */
89
+ export function transportErrorClass(thrown: unknown): string {
90
+ return thrown instanceof Error && TRANSPORT_ERROR_CLASSES.has(thrown.name) ? thrown.name : "Error";
91
+ }
92
+
93
+ /** A thrown text with every URL, email address and the given secret removed, whitespace folded, capped. */
94
+ function scrubbed(text: string, secret: string): string {
95
+ let out = text;
96
+ if (secret) out = out.split(secret).join("<secret>");
97
+ return out
98
+ .replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"<>)]+/gi, "<url>")
99
+ .replace(/[^\s@'"<>(),;:]+@[^\s@'"<>(),;:]+\.[a-z]{2,}/gi, "<email>")
100
+ .replace(/\s+/g, " ")
101
+ .trim()
102
+ .slice(0, 160);
103
+ }
104
+
105
+ /**
106
+ * For a `TypeError` (how a Workers fetch reports a connection it could not make: lost, refused
107
+ * before connecting, TLS trust), its message and its cause's message, scrubbed of URLs, email
108
+ * addresses and the service secret. Those words are what tell the network causes apart. Any other
109
+ * thrown value yields nothing: its text is not a fetch's own wording.
110
+ */
111
+ export function transportErrorDetail(thrown: unknown, secret: string): string {
112
+ if (!(thrown instanceof TypeError)) return "";
113
+ const cause = (thrown as { cause?: unknown }).cause;
114
+ const causeText = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : "";
115
+ const parts = [`message "${scrubbed(thrown.message, secret)}"`];
116
+ if (causeText) parts.push(`cause "${scrubbed(causeText, secret)}"`);
117
+ return `: ${parts.join(", ")}`;
118
+ }
119
+
78
120
  export async function callMcpService(req: McpServiceRequest): Promise<McpServiceOutcome> {
79
121
  const urlEnv = AUTHORITY_ENV[req.authority ?? "binding"];
80
122
  const authorityUrl = await readEnv(urlEnv);
81
123
  const secret = await readEnv("MCP_SERVICE_SHARED_SECRET");
82
- if (!authorityUrl || !secret) return { kind: "not-configured" };
124
+ if (!authorityUrl || !secret) {
125
+ const missing = [!authorityUrl && urlEnv, !secret && "MCP_SERVICE_SHARED_SECRET"].filter(Boolean).join(" and ");
126
+ console.warn(`[mcp-service-seam] ${req.method} ${req.path} not-configured: ${missing} unset`);
127
+ return { kind: "not-configured" };
128
+ }
83
129
 
84
130
  let origin: string | null;
85
131
  if (req.authority === "grant") {
@@ -91,7 +137,10 @@ export async function callMcpService(req: McpServiceRequest): Promise<McpService
91
137
  origin = null;
92
138
  }
93
139
  }
94
- if (!origin) return { kind: "bad-url" };
140
+ if (!origin) {
141
+ console.error(`[mcp-service-seam] ${req.method} ${req.path} bad-url: ${urlEnv} is not a usable origin`);
142
+ return { kind: "bad-url" };
143
+ }
95
144
  let url = `${origin}${req.path}`;
96
145
  if (req.query) url += `?${new URLSearchParams(req.query).toString()}`;
97
146
 
@@ -105,7 +154,10 @@ export async function callMcpService(req: McpServiceRequest): Promise<McpService
105
154
  if (!req.transport.post) throw new Error("transport has no post()");
106
155
  res = await req.transport.post(url, headers, req.body);
107
156
  }
108
- } catch {
157
+ } catch (thrown) {
158
+ console.error(
159
+ `[mcp-service-seam] ${req.method} ${req.path} transport-error: ${transportErrorClass(thrown)} (${urlEnv})${transportErrorDetail(thrown, secret)}`,
160
+ );
109
161
  return { kind: "transport-error", message: MCP_SERVICE_TRANSPORT_FAILURE };
110
162
  }
111
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.11.6",
3
+ "version": "4.11.8",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",