@tokenoftrust/storefront-runner 2.0.10 → 2.0.12

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.
@@ -144,6 +144,39 @@ export function isPublicPath(pathname: string): boolean {
144
144
  return pathname === "/api/cli/policy";
145
145
  }
146
146
 
147
+ /**
148
+ * The CLI viewer-session mint (`/api/dev/cli-session`) — how an invited developer
149
+ * turns their own `tot login` MCP token into a storefront session so `tot accept`
150
+ * can reach the ship surface. Like the static-bundle upload it governs its OWN,
151
+ * stricter auth and must reach its handler rather than being masked behind the
152
+ * "Coming soon" holding page.
153
+ *
154
+ * Gate-exemption does NOT weaken it, on three independent grounds:
155
+ * - It authenticates ITSELF: no `Authorization: Bearer <token>` → 401
156
+ * `missing_bearer`. It reads no cookie, so it carries zero ambient authority.
157
+ * - It resolves WHO that opaque token is only by asking the MCP over the
158
+ * service-secret seam, fail-closed (unconfigured seam, transport fault, non-2xx,
159
+ * unparseable body, or absent email all → 401).
160
+ * - The session it mints is capability `member`, roles `[]` — byte-for-byte the
161
+ * one `/api/auth/magic-exchange` produces from the same `reissueSubjectToken` →
162
+ * verify → `createSession` path, and that route is ALREADY exempt via
163
+ * {@link isAuthPath}. So this exposes no reachable surface that is not.
164
+ *
165
+ * It also grants no admission: a developer holding no membership for this host gets
166
+ * a session that STILL fails `sessionAdmitsHost`, so the gate stays closed to them.
167
+ *
168
+ * Matched in BOTH addressing forms, like {@link isCandidatePreviewPath} — the gate
169
+ * sees the PRE-STRIP request path, so a tenant reached by path prefix arrives as
170
+ * `/<tenant>/api/dev/cli-session`. The CLI addresses tenants exactly that way (the
171
+ * shared storefront origin serves every tenant under a path prefix, which is the only
172
+ * form that works for a tenant whose apex has not cut over), so an exact-match-only
173
+ * exemption would never fire for the one caller this route exists to serve.
174
+ */
175
+ export function isDevCliSessionPath(pathname: string): boolean {
176
+ if (pathname === "/api/dev/cli-session") return true;
177
+ return /^\/[^/]+\/api\/dev\/cli-session$/.test(pathname);
178
+ }
179
+
147
180
  /**
148
181
  * The authenticated static-bundle staging upload (`/api/publish/stage-bundle`).
149
182
  * Like the candidate-preview control plane, it governs its OWN, stricter auth (a
@@ -266,6 +299,8 @@ export function evaluateLoginGate(input: LoginGateInput): LoginGateDecision {
266
299
  if (isCandidatePreviewPath(input.pathname)) return { action: "pass" };
267
300
  // The static-bundle staging upload carries its own private_key_jwt auth.
268
301
  if (isStaticBundleUploadPath(input.pathname)) return { action: "pass" };
302
+ // The CLI viewer-session mint carries its own bearer auth — see isDevCliSessionPath.
303
+ if (isDevCliSessionPath(input.pathname)) return { action: "pass" };
269
304
  // QA content-bypass: a request presenting the server-configured token passes for
270
305
  // public content only. NEVER for admin/cockpit/api — that guard lives here (not
271
306
  // just in middleware) so /admin stays gated even if the flag is set upstream.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The "Delegate ship-on-behalf" gate on /dashboard/<appDomain>/team, as a PURE
3
+ * decision — who sees the control, and what everyone else is told instead.
4
+ *
5
+ * The gate itself is unchanged and stays OWNER-ONLY: `/api/grants/principal`
6
+ * refuses anything but a real owner (mirroring tot20's `ownsTenant`), so showing
7
+ * the control to an admin would only ever produce a 403. What changes here is
8
+ * that every OTHER viewer now gets an explicit sentence naming their actual role,
9
+ * instead of the section vanishing with no explanation — a hidden control is
10
+ * indistinguishable from a broken one, and that ambiguity has already cost a
11
+ * live debugging session.
12
+ *
13
+ * Why staff get a REFUSAL and not a bypass: `ship-on-behalf` is a grant-required
14
+ * operation under `guidance://access-authority` — it is not required to keep the
15
+ * store's service running, so it runs on owner consent even when an admin or ToT
16
+ * staff performs it. Staff capture that consent and issue the grant with their
17
+ * own attestation on the record; they never self-serve it from a staff boolean.
18
+ * Do NOT "fix" the staff state by widening {@link resolveDelegateGate} to admit
19
+ * `mode: "staff"` — that is the confused-deputy hole the model exists to close.
20
+ *
21
+ * PURE (no I/O, no Astro) so all four states are unit-testable without rendering
22
+ * the page, matching `@/lib/dashboard/tenantSelection` and `@/lib/auth/loginGate`.
23
+ */
24
+ import { membershipFor, staffAdmits } from "@/lib/dashboard/tenantSelection";
25
+ import type { StaffAdmission, TenantMembership } from "@/lib/dashboard/tenantSelection";
26
+
27
+ /**
28
+ * What the delegate section renders for this viewer.
29
+ *
30
+ * - `owner` — a real `tenants[]` membership with coarse role `owner`. The only
31
+ * state that renders the control.
32
+ * - `member` — a real membership that is not owner (`admin`, `member`, …).
33
+ * - `staff` — no real membership; admitted by a LIVE ToT-staff vendor selection.
34
+ * `role` is the selection's capability, not a membership role.
35
+ * - `none` — neither. Unreachable from team.astro (its `membershipFor` guard
36
+ * redirects to /dashboard first), but represented so the function is total and
37
+ * no input can ever produce a silently blank section.
38
+ */
39
+ export type DelegateGate =
40
+ | { mode: "owner" }
41
+ | { mode: "member"; role: string; explanation: string }
42
+ | { mode: "staff"; role: string; explanation: string }
43
+ | { mode: "none"; role: null; explanation: string };
44
+
45
+ /**
46
+ * Resolve the delegate gate for a viewer.
47
+ *
48
+ * Order matters and mirrors {@link membershipFor}: a REAL membership always wins
49
+ * over a staff selection, so a ToT staffer who is also a genuine owner of this
50
+ * store is treated as the owner they are. Only a viewer with no membership at all
51
+ * falls through to the staff branch.
52
+ *
53
+ * The role string is surfaced verbatim in the explanation on purpose. The coarse
54
+ * role reaching `tenants[]` is the end of a five-hop chain (tot20 `appSpec.users`
55
+ * → `collapseCapability` → `coarseRole` → the signed id_token → the storefront's
56
+ * `coerceRole`, which defaults anything unrecognised to `member`). Naming the
57
+ * value the page actually received turns "why is this hidden?" into a one-glance
58
+ * answer instead of a trace through both repos.
59
+ */
60
+ export function resolveDelegateGate(
61
+ tenants: readonly TenantMembership[] | undefined,
62
+ resource: string,
63
+ staff?: StaffAdmission,
64
+ ): DelegateGate {
65
+ const target = (resource ?? "").trim();
66
+ const membership = membershipFor(tenants, target);
67
+ if (membership) {
68
+ if (membership.role === "owner") return { mode: "owner" };
69
+ return {
70
+ mode: "member",
71
+ role: membership.role,
72
+ explanation:
73
+ `Only the store owner can delegate ship-on-behalf. Your role on ${target} is ` +
74
+ `“${membership.role}”. Ask an owner to set up the delegation, or to make you one.`,
75
+ };
76
+ }
77
+
78
+ if (staffAdmits(staff, target)) {
79
+ const role = staff!.selection!.capability;
80
+ return {
81
+ mode: "staff",
82
+ role,
83
+ explanation:
84
+ `You’re here through Token of Trust staff access (“${role}”), not as a member of ` +
85
+ `${target}. Delegating ship-on-behalf is the owner’s own control, so it isn’t ` +
86
+ `offered here — capture the owner’s consent and issue the grant on their behalf instead.`,
87
+ };
88
+ }
89
+
90
+ return {
91
+ mode: "none",
92
+ role: null,
93
+ explanation:
94
+ `You don’t hold a role on ${target}, so ship-on-behalf delegation isn’t available here.`,
95
+ };
96
+ }
@@ -65,6 +65,8 @@ export interface CatalogSyncState {
65
65
  source: SyncStateSourceMode;
66
66
  fidelity: SyncStateFidelity;
67
67
  lastSyncAt: string | null;
68
+ /** True when the most recent sync loaded with record errors; absent on a pre-hsby-B3 catalog. */
69
+ lastSyncHadErrors?: boolean;
68
70
  products: SyncStateEntity;
69
71
  collections: SyncStateEntity;
70
72
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
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",