@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
|
|
30
|
-
* app-binding WRITE (`recordDeveloperAppBinding`) targets, so
|
|
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
|
-
|
|
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
|
|
114
|
-
* `200 { capability: <capability> }`; unconfigured
|
|
115
|
-
* any
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
|
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 (!
|
|
82
|
+
if (!authorityUrl || !secret) return { kind: "not-configured" };
|
|
53
83
|
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
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
|
|
73
|
-
return { kind: "transport-error", message:
|
|
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
|
+
"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",
|