@tokenoftrust/storefront-runner 4.11.3 → 4.11.4

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.
@@ -26,9 +26,11 @@
26
26
  * it keeps its OWN small transport rather than importing another cluster's copy so
27
27
  * the two stay decoupled (the same reason previewStatus duplicates its transport).
28
28
  *
29
- * The MCP HOST is the ORIGIN of `MCP_APP_BINDING_URL` — the same tier the invite
30
- * app-binding WRITE (`recordDeveloperAppBinding`) targets, so this READ and that
31
- * WRITE always agree on which subject-capability store they speak to.
29
+ * The ship and contribute reads go to the ORIGIN of `MCP_APP_BINDING_URL` — the same tier
30
+ * the invite app-binding WRITE (`recordDeveloperAppBinding`) targets, so those READs and
31
+ * that WRITE always agree on which subject-capability store they speak to. A named grant
32
+ * (`resolveNamedSubjectGrant`) always goes to the code-owned grant authority,
33
+ * `MCP_GRANT_AUTHORITY_URL`, with no fallback to the app binding.
32
34
  */
33
35
  import { callMcpService } from "@/lib/developer-experience/http/mcpServiceCall";
34
36
  import type { Capability } from "./capability.js";
@@ -48,7 +50,14 @@ export class FetchShipGrantTransport implements ViewerShipGrantGetTransport {
48
50
  // every admin tab), so an unbounded hang stalls the whole document stream. A
49
51
  // timeout aborts to the documented deny-default (the resolver maps any transport/
50
52
  // timeout fault to `preview-access`) — never a render-blocking wait on a slow MCP.
51
- const res = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(2500) });
53
+ // `redirect: "error"`: a redirect would carry the service-secret header to another origin,
54
+ // so it rejects (a transport fault, so a deny) and nothing is sent to the redirect target.
55
+ const res = await fetch(url, {
56
+ method: "GET",
57
+ headers,
58
+ redirect: "error",
59
+ signal: AbortSignal.timeout(2500),
60
+ });
52
61
  return { status: res.status, json: () => res.json() };
53
62
  }
54
63
  }
@@ -110,9 +119,10 @@ async function resolveRawViewerCapability(
110
119
 
111
120
  /**
112
121
  * Whether `email` holds a LIVE owner-consented grant of exactly `capability` over `appDomain`,
113
- * asked of the same subject-capability seam with the capability named. True only on an explicit
114
- * `200 { capability: <capability> }`; unconfigured, any non-2xx, a timeout, an unparseable body or
115
- * any other answer is false. No cache: a revoke takes effect on the next call. Never throws.
122
+ * asked of the grant authority's subject-capability seam with the capability named. True only on
123
+ * an explicit `200 { capability: <capability> }`; an unconfigured or malformed grant authority,
124
+ * any non-2xx, a redirect, a timeout, an unparseable body or any other answer is false. No cache:
125
+ * a revoke takes effect on the next call. Never throws.
116
126
  */
117
127
  export async function resolveNamedSubjectGrant(
118
128
  email: string | null | undefined,
@@ -128,6 +138,7 @@ export async function resolveNamedSubjectGrant(
128
138
  method: "GET",
129
139
  path: "/internal/dev-access/subject-capability",
130
140
  query: { subject, tenant, app: "storefront", capability },
141
+ authority: "grant",
131
142
  transport: deps.transport ?? new FetchShipGrantTransport(),
132
143
  });
133
144
  if (outcome.kind !== "ok") return false;
@@ -1,8 +1,16 @@
1
1
  /**
2
2
  * The one transport for storefront → tot-mcp service calls (`/internal/*`, keyless
3
- * `x-mcp-service-secret` seam). Every caller builds its URL here from the ORIGIN of
4
- * `MCP_APP_BINDING_URL` plus a literal contract path, so the paths and capabilities a caller
5
- * sends are pinned by `contracts/tot-mcp-service-seam.json` (see `mcpServiceSeam.test.ts`).
3
+ * `x-mcp-service-secret` seam). Every caller builds its URL here from a code-owned authority
4
+ * origin plus a literal contract path, so the paths and capabilities a caller sends are pinned
5
+ * by `contracts/tot-mcp-service-seam.json` (see `mcpServiceSeam.test.ts`).
6
+ *
7
+ * The authority is chosen by the caller's code, never by a request:
8
+ * - omitted → the ORIGIN of `MCP_APP_BINDING_URL` (the registry/identity tier).
9
+ * - `"grant"` → `MCP_GRANT_AUTHORITY_URL`, which must be a bare HTTPS origin. Missing is
10
+ * `not-configured` and malformed is `bad-url`; neither falls back to the app binding.
11
+ *
12
+ * A thrown transport failure comes back as one fixed diagnostic, never the thrown value: a
13
+ * fetch error can carry the request URL (and so its subject query) to every downstream sink.
6
14
  *
7
15
  * A 401 or 404 from a seam route is misconfiguration, never a business answer: tot-mcp's
8
16
  * routes answer 200 for every absent/unknown case, so 401 means the secret or URL is wrong
@@ -37,26 +45,54 @@ export interface McpServiceRequest {
37
45
  path: string;
38
46
  query?: Record<string, string>;
39
47
  body?: unknown;
48
+ /** `"grant"` selects `MCP_GRANT_AUTHORITY_URL`; omitted selects `MCP_APP_BINDING_URL`. */
49
+ authority?: "grant";
40
50
  transport: McpServiceTransport;
41
51
  }
42
52
 
43
- function misconfigurationReason(status: 401 | 404): string {
53
+ /** The only diagnostic a thrown transport failure produces. */
54
+ export const MCP_SERVICE_TRANSPORT_FAILURE = "tot-mcp service request failed";
55
+
56
+ const AUTHORITY_ENV = {
57
+ binding: "MCP_APP_BINDING_URL",
58
+ grant: "MCP_GRANT_AUTHORITY_URL",
59
+ } as const;
60
+
61
+ /**
62
+ * The grant authority's origin, or null unless `raw` is exactly a bare HTTPS origin (an optional
63
+ * trailing `/` allowed): no credentials, port spelling, path, query or fragment.
64
+ */
65
+ export function grantAuthorityOrigin(raw: string): string | null {
66
+ if (!URL.canParse(raw)) return null;
67
+ const parsed = new URL(raw);
68
+ if (parsed.protocol !== "https:") return null;
69
+ return raw === parsed.origin || raw === `${parsed.origin}/` ? parsed.origin : null;
70
+ }
71
+
72
+ function misconfigurationReason(status: 401 | 404, urlEnv: string): string {
44
73
  return status === 401
45
- ? "tot-mcp rejected the service secret, or MCP_APP_BINDING_URL points at a session-guarded surface"
74
+ ? `tot-mcp rejected the service secret, or ${urlEnv} points at a session-guarded surface`
46
75
  : "tot-mcp does not serve this path; storefront and tot-mcp disagree on the service-seam contract";
47
76
  }
48
77
 
49
78
  export async function callMcpService(req: McpServiceRequest): Promise<McpServiceOutcome> {
50
- const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
79
+ const urlEnv = AUTHORITY_ENV[req.authority ?? "binding"];
80
+ const authorityUrl = await readEnv(urlEnv);
51
81
  const secret = await readEnv("MCP_SERVICE_SHARED_SECRET");
52
- if (!bindingUrl || !secret) return { kind: "not-configured" };
82
+ if (!authorityUrl || !secret) return { kind: "not-configured" };
53
83
 
54
- let url: string;
55
- try {
56
- url = `${new URL(bindingUrl).origin}${req.path}`;
57
- } catch {
58
- return { kind: "bad-url" };
84
+ let origin: string | null;
85
+ if (req.authority === "grant") {
86
+ origin = grantAuthorityOrigin(authorityUrl);
87
+ } else {
88
+ try {
89
+ origin = new URL(authorityUrl).origin;
90
+ } catch {
91
+ origin = null;
92
+ }
59
93
  }
94
+ if (!origin) return { kind: "bad-url" };
95
+ let url = `${origin}${req.path}`;
60
96
  if (req.query) url += `?${new URLSearchParams(req.query).toString()}`;
61
97
 
62
98
  const headers = { "x-mcp-service-secret": secret };
@@ -69,12 +105,12 @@ export async function callMcpService(req: McpServiceRequest): Promise<McpService
69
105
  if (!req.transport.post) throw new Error("transport has no post()");
70
106
  res = await req.transport.post(url, headers, req.body);
71
107
  }
72
- } catch (cause) {
73
- return { kind: "transport-error", message: cause instanceof Error ? cause.message : String(cause) };
108
+ } catch {
109
+ return { kind: "transport-error", message: MCP_SERVICE_TRANSPORT_FAILURE };
74
110
  }
75
111
 
76
112
  if (res.status === 401 || res.status === 404) {
77
- const reason = misconfigurationReason(res.status);
113
+ const reason = misconfigurationReason(res.status, urlEnv);
78
114
  console.error(
79
115
  `[mcp-service-seam] ${req.method} ${req.path} answered ${res.status}: ${reason}`,
80
116
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.11.3",
3
+ "version": "4.11.4",
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",