@tokenoftrust/storefront-runner 4.11.5 → 4.11.7

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,10 @@
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. Never the URL, query, secret or thrown text.
18
+ *
15
19
  * A 401 or 404 from a seam route is misconfiguration, never a business answer: tot-mcp's
16
20
  * routes answer 200 for every absent/unknown case, so 401 means the secret or URL is wrong
17
21
  * and 404 means the path is not served. Both log LOUDLY and come back as `misconfigured`
@@ -75,11 +79,23 @@ function misconfigurationReason(status: 401 | 404, urlEnv: string): string {
75
79
  : "tot-mcp does not serve this path; storefront and tot-mcp disagree on the service-seam contract";
76
80
  }
77
81
 
82
+ /** Error classes a transport failure may name in a log line; anything else reads `Error`. */
83
+ const TRANSPORT_ERROR_CLASSES = new Set(["TimeoutError", "AbortError", "TypeError", "SyntaxError"]);
84
+
85
+ /** The class of a thrown transport failure, from a fixed list. Never its message. */
86
+ export function transportErrorClass(thrown: unknown): string {
87
+ return thrown instanceof Error && TRANSPORT_ERROR_CLASSES.has(thrown.name) ? thrown.name : "Error";
88
+ }
89
+
78
90
  export async function callMcpService(req: McpServiceRequest): Promise<McpServiceOutcome> {
79
91
  const urlEnv = AUTHORITY_ENV[req.authority ?? "binding"];
80
92
  const authorityUrl = await readEnv(urlEnv);
81
93
  const secret = await readEnv("MCP_SERVICE_SHARED_SECRET");
82
- if (!authorityUrl || !secret) return { kind: "not-configured" };
94
+ if (!authorityUrl || !secret) {
95
+ const missing = [!authorityUrl && urlEnv, !secret && "MCP_SERVICE_SHARED_SECRET"].filter(Boolean).join(" and ");
96
+ console.warn(`[mcp-service-seam] ${req.method} ${req.path} not-configured: ${missing} unset`);
97
+ return { kind: "not-configured" };
98
+ }
83
99
 
84
100
  let origin: string | null;
85
101
  if (req.authority === "grant") {
@@ -91,7 +107,10 @@ export async function callMcpService(req: McpServiceRequest): Promise<McpService
91
107
  origin = null;
92
108
  }
93
109
  }
94
- if (!origin) return { kind: "bad-url" };
110
+ if (!origin) {
111
+ console.error(`[mcp-service-seam] ${req.method} ${req.path} bad-url: ${urlEnv} is not a usable origin`);
112
+ return { kind: "bad-url" };
113
+ }
95
114
  let url = `${origin}${req.path}`;
96
115
  if (req.query) url += `?${new URLSearchParams(req.query).toString()}`;
97
116
 
@@ -105,7 +124,8 @@ export async function callMcpService(req: McpServiceRequest): Promise<McpService
105
124
  if (!req.transport.post) throw new Error("transport has no post()");
106
125
  res = await req.transport.post(url, headers, req.body);
107
126
  }
108
- } catch {
127
+ } catch (thrown) {
128
+ console.error(`[mcp-service-seam] ${req.method} ${req.path} transport-error: ${transportErrorClass(thrown)} (${urlEnv})`);
109
129
  return { kind: "transport-error", message: MCP_SERVICE_TRANSPORT_FAILURE };
110
130
  }
111
131
 
@@ -86,6 +86,22 @@ export function workerOrderSourceDeps(
86
86
  };
87
87
  }
88
88
 
89
+ /**
90
+ * The stage a tenant's orders are recorded under, shared by every reader of an order (the Orders
91
+ * tab, the order routes and the contact vault routes): a shared-store tenant's ledger and sealed
92
+ * contacts carry this Worker's `COMMERCE_STAGE`; an own-store tenant's carry the routed environment's.
93
+ */
94
+ export async function resolveOrderStage(
95
+ input: { tenantId: string; customizationEnv: CustomizationEnv | undefined },
96
+ deps: Pick<OrderSourceDeps, "admitSharedStoreTenant" | "readEnv">,
97
+ ): Promise<{ checkout: "shared-test-store" | "own-store"; stage: CommerceStage | null }> {
98
+ if ((await deps.admitSharedStoreTenant(input.tenantId)).ok) {
99
+ const stage = await deps.readEnv("COMMERCE_STAGE");
100
+ return { checkout: "shared-test-store", stage: stage === "preview" || stage === "publication" ? stage : null };
101
+ }
102
+ return { checkout: "own-store", stage: commerceStageForEnv(input.customizationEnv) };
103
+ }
104
+
89
105
  export async function resolveOrderSource(
90
106
  input: { tenantId: string; customizationEnv: CustomizationEnv | undefined },
91
107
  deps: OrderSourceDeps,
@@ -93,19 +109,16 @@ export async function resolveOrderSource(
93
109
  const tenantId = input.tenantId.trim();
94
110
  if (!tenantId) return { kind: "not-connected", checkout: null, gap: "no-tenant" };
95
111
 
96
- if ((await deps.admitSharedStoreTenant(tenantId)).ok) {
97
- const checkout = "shared-test-store";
98
- const stage = await deps.readEnv("COMMERCE_STAGE");
99
- if (stage !== "preview" && stage !== "publication") return { kind: "not-connected", checkout, gap: "stage-unknown" };
112
+ const { checkout, stage } = await resolveOrderStage({ tenantId, customizationEnv: input.customizationEnv }, deps);
113
+ if (!stage) return { kind: "not-connected", checkout, gap: "stage-unknown" };
114
+
115
+ if (checkout === "shared-test-store") {
100
116
  const db = await deps.readLedgerDb();
101
117
  if (!db) return { kind: "not-connected", checkout, gap: "ledger-unavailable" };
102
118
  const book = ledgerOrderBook(new ScrappyOrderLedger(db), { tenantId, stage });
103
119
  return { kind: "ledger", checkout, scope: { tenantId, stage }, client: book.client, status: book.status };
104
120
  }
105
121
 
106
- const checkout = "own-store";
107
- const stage = commerceStageForEnv(input.customizationEnv);
108
- if (!stage) return { kind: "not-connected", checkout, gap: "stage-unknown" };
109
122
  const bearer = await deps.readEnv("FOXY_ORDER_API_KEY");
110
123
  if (!bearer) return { kind: "not-connected", checkout, gap: "partner-key-missing" };
111
124
  return {
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * The admission every order-contact admin route runs before it touches the vault: the owner gate,
3
3
  * then the owner-or-staff split (contactReaderAuthority), then — for staff — a reason, a ticket and a
4
- * LIVE owner-consented customer-contact-read grant. The tenant comes from the routed host and the
5
- * stage from the routed customization environment, never from the request.
4
+ * LIVE owner-consented customer-contact-read grant. The tenant comes from the routed host, never
5
+ * from the request. An order contact's stage is the one its order is recorded under
6
+ * (`resolveOrderStage`, shared with the Orders tab, with the shared-store admission its route
7
+ * supplies); a message's is the routed customization environment's.
6
8
  */
7
9
  import type { APIContext } from "astro";
8
10
  import { requireAdminApiEntry } from "@/lib/access/auth/adminApiGuard";
9
11
  import { readViewerSession } from "@/lib/access/auth/route";
10
12
  import { contactReaderAuthority, resolveContactReadGrant } from "@/lib/access/auth/contactReaderAuthority";
11
- import { readD1 } from "@/lib/publication/shared/env";
13
+ import { readD1, readEnv } from "@/lib/publication/shared/env";
12
14
  import { fromD1, type Queryable } from "@/lib/catalog/d1/catalog.js";
13
15
  import { commerceStageForEnv } from "@/lib/fulfillment/fulfillmentService";
16
+ import { resolveOrderStage, type OrderSourceDeps } from "@/lib/orders/admin/orderSource";
14
17
  import type { KmsClient } from "@/lib/customers/contactVault/kms.js";
15
18
  import { loadVaultKms } from "./kmsEnv.js";
16
19
  import { type ContactActor, type VaultStage } from "@/lib/customers/contactVault/contactAudit.js";
@@ -54,6 +57,7 @@ export type ContactRoute =
54
57
  export async function openContactRoute(
55
58
  context: APIContext,
56
59
  expect: "owner" | "staff",
60
+ record: "message" | Pick<OrderSourceDeps, "admitSharedStoreTenant">,
57
61
  body?: unknown,
58
62
  ): Promise<ContactRoute> {
59
63
  const refuse = (error: string, status: number): ContactRoute => ({ ok: false, response: contactJson({ error }, status) });
@@ -79,7 +83,13 @@ export async function openContactRoute(
79
83
  actor = { actorId: authority.actorId, role: "staff", ...reason };
80
84
  }
81
85
 
82
- const stage = commerceStageForEnv(context.locals.customizationEnv);
86
+ const customizationEnv = context.locals.customizationEnv;
87
+ const stage = record === "message"
88
+ ? commerceStageForEnv(customizationEnv)
89
+ : (await resolveOrderStage({ tenantId: host, customizationEnv }, {
90
+ admitSharedStoreTenant: record.admitSharedStoreTenant,
91
+ readEnv: async (name) => (await readEnv(name)) ?? undefined,
92
+ })).stage;
83
93
  if (!stage) return refuse("stage-unknown", 409);
84
94
  const d1 = await readD1("STOREFRONT_APPS_DB");
85
95
  if (!d1) return refuse("unavailable", 503);
@@ -10,6 +10,7 @@ import { routedApiParam } from "@/lib/publication/shared/routedApiParam";
10
10
  */
11
11
  import type { APIContext } from "astro";
12
12
  import { isOpaqueId } from "@/lib/fulfillment/fulfillmentService";
13
+ import { admitSharedStoreTenant } from "@/lib/checkout/commerce/sharedStoreAdmission";
13
14
  import { contactJson, openContactRoute } from "@/lib/orders/vault/contactRoutes";
14
15
  import { eraseOrderContact, readEraseGroups, readOrderContact, type Group } from "@/lib/orders/vault/orderContactVault";
15
16
 
@@ -18,7 +19,7 @@ export const prerender = false;
18
19
  export async function GET(context: APIContext): Promise<Response> {
19
20
  const orderId = routedApiParam(context, "fulfillment-address");
20
21
  if (!orderId || !isOpaqueId(orderId)) return contactJson({ error: "not-found" }, 404);
21
- const route = await openContactRoute(context, "owner");
22
+ const route = await openContactRoute(context, "owner", { admitSharedStoreTenant });
22
23
  if (!route.ok) return route.response;
23
24
  const read = await readOrderContact(route.db, route.kms, {
24
25
  tenantId: route.tenantId, stage: route.stage, orderId, actor: route.actor, auditId: crypto.randomUUID(), now: new Date(),
@@ -48,7 +49,7 @@ async function eraseGroups(request: Request): Promise<Group[] | undefined | null
48
49
  export async function DELETE(context: APIContext): Promise<Response> {
49
50
  const orderId = routedApiParam(context, "fulfillment-address");
50
51
  if (!orderId || !isOpaqueId(orderId)) return contactJson({ error: "not-found" }, 404);
51
- const route = await openContactRoute(context, "owner");
52
+ const route = await openContactRoute(context, "owner", { admitSharedStoreTenant });
52
53
  if (!route.ok) return route.response;
53
54
  const groups = await eraseGroups(context.request);
54
55
  if (groups === null) return contactJson({ error: "malformed" }, 400);
@@ -6,6 +6,7 @@
6
6
  * Staff are refused 403 support-read-required. Nothing here logs the body.
7
7
  */
8
8
  import type { APIContext } from "astro";
9
+ import { admitSharedStoreTenant } from "@/lib/checkout/commerce/sharedStoreAdmission";
9
10
  import { contactJson, openContactRoute, readRequestJson } from "@/lib/orders/vault/contactRoutes";
10
11
  import { readSearchQuery, searchOrderContacts } from "@/lib/orders/vault/orderContactVault";
11
12
 
@@ -13,7 +14,7 @@ export const prerender = false;
13
14
 
14
15
  export async function POST(context: APIContext): Promise<Response> {
15
16
  if (new URL(context.request.url).search !== "") return contactJson({ error: "malformed" }, 400);
16
- const route = await openContactRoute(context, "owner");
17
+ const route = await openContactRoute(context, "owner", { admitSharedStoreTenant });
17
18
  if (!route.ok) return route.response;
18
19
  const query = readSearchQuery(await readRequestJson(context.request));
19
20
  if (!query) return contactJson({ error: "malformed" }, 400);
@@ -5,13 +5,14 @@
5
5
  * expiries and rule ids. Owner only; staff are refused 403 support-read-required.
6
6
  */
7
7
  import type { APIContext } from "astro";
8
+ import { admitSharedStoreTenant } from "@/lib/checkout/commerce/sharedStoreAdmission";
8
9
  import { contactJson, openContactRoute } from "@/lib/orders/vault/contactRoutes";
9
10
  import { listOrderContacts } from "@/lib/orders/vault/orderContactVault";
10
11
 
11
12
  export const prerender = false;
12
13
 
13
14
  export async function GET(context: APIContext): Promise<Response> {
14
- const route = await openContactRoute(context, "owner");
15
+ const route = await openContactRoute(context, "owner", { admitSharedStoreTenant });
15
16
  if (!route.ok) return route.response;
16
17
  const listed = await listOrderContacts(route.db, route.kms, {
17
18
  tenantId: route.tenantId, stage: route.stage, actor: route.actor, auditId: crypto.randomUUID(), now: new Date(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "4.11.5",
3
+ "version": "4.11.7",
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",