@tokenoftrust/storefront-runner 1.4.1 → 1.4.2-rc.0
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.
- package/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +14 -0
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1105 -2334
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +17 -19
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +3 -3
- package/apps/storefront/src/lib/activity/deployVersion.ts +19 -26
- package/apps/storefront/src/lib/activity/ingest.ts +8 -8
- package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
- package/apps/storefront/src/lib/activity/query.ts +9 -9
- package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
- package/apps/storefront/src/lib/activity/store.ts +4 -6
- package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
- package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +96 -17
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -49
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +113 -2
- package/apps/storefront/src/pages/admin.astro +10 -1312
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +37 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/catalog.ts +52 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +1 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offline verification of tot's identity assertion (`id_token`) — the consuming
|
|
3
|
-
* half of the decoupled-authZ architecture
|
|
3
|
+
* half of the decoupled-authZ architecture.
|
|
4
4
|
*
|
|
5
5
|
* tot signs a short-lived ES256 assertion ONCE at sign-in with its PRIVATE key
|
|
6
6
|
* (`/api/internal/access-code/verify` → `id_token`). This module verifies it
|
|
@@ -10,20 +10,12 @@
|
|
|
10
10
|
* after sign-in must not break authorization; the token's own `exp` bounds
|
|
11
11
|
* staleness.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* `tot20 services/InternalAuthService/identityTokenService.jest.js`.
|
|
13
|
+
* SCOPE: this does ONLY the JWKS offline-verify + a coarse claims→session
|
|
14
|
+
* derivation. Do NOT vendor the Cedar-wasm PDP into the Worker; it is
|
|
15
|
+
* unnecessary for the current coarse gate.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* sandbox) and consuming the launchpad kernel as an installed package are
|
|
21
|
-
* Phase 4 — see branch `feat/authz-cedar-kernel`. Do NOT vendor the Cedar-wasm
|
|
22
|
-
* PDP into the Worker; it is unnecessary for the current coarse gate.
|
|
23
|
-
*
|
|
24
|
-
* Contract: /tmp/authz-token-claims-contract.md (AS-BUILT). Key points mirrored
|
|
25
|
-
* here: §2 (ES256 + jose), §3 (claim set), §5 (consumer verify algorithm),
|
|
26
|
-
* §6 (field name `id_token`), §6b (JWKS host is per-env app config; jose
|
|
17
|
+
* Contract points as-built: ES256 + jose, the claim set, the consumer verify
|
|
18
|
+
* algorithm, field name `id_token`, and JWKS host is per-env app config (jose
|
|
27
19
|
* cross-version 4↔6 is wire-compatible by construction).
|
|
28
20
|
*/
|
|
29
21
|
import {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Login-gate decision (pure) — the "Sign in with ToT" enforcement mode for a
|
|
3
|
-
* gated demo host
|
|
4
|
-
*
|
|
3
|
+
* gated demo host (see `@/lib/demoGate`). Same `DEMO_GATE_HOSTS` list, switched
|
|
4
|
+
* by `DEMO_GATE_MODE`:
|
|
5
5
|
*
|
|
6
|
-
* DEMO_GATE_MODE = "password" →
|
|
6
|
+
* DEMO_GATE_MODE = "password" → Basic-Auth gate (evaluateDemoGate)
|
|
7
7
|
* DEMO_GATE_MODE = "login" → this: redirect to /auth/login unless a valid,
|
|
8
8
|
* resource-bound tot_session cookie is present.
|
|
9
9
|
*
|
|
@@ -29,7 +29,7 @@ export const PUBLIC_HOME_TENANT_ID = "home";
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The record fields the host-admission + host-capability checks read. All optional
|
|
32
|
-
* — these readers tolerate a partial
|
|
32
|
+
* — these readers tolerate a partial record and are deny-closed on absence.
|
|
33
33
|
*/
|
|
34
34
|
type HostAdmissionRecord = Partial<
|
|
35
35
|
Pick<SessionRecord, "tenants" | "staffSelection" | "staff" | "resource" | "capability">
|
|
@@ -144,6 +144,18 @@ export function isPublicPath(pathname: string): boolean {
|
|
|
144
144
|
return pathname === "/api/cli/policy";
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The authenticated static-bundle staging upload (`/api/publish/stage-bundle`).
|
|
149
|
+
* Like the candidate-preview control plane, it governs its OWN, stricter auth (a
|
|
150
|
+
* CI `private_key_jwt` assertion — see `@/lib/publish/stageBundleUpload`), so it
|
|
151
|
+
* must reach its handler server-to-server rather than being masked behind the
|
|
152
|
+
* demo login gate's "Coming soon" holding page. Gate-exemption does NOT weaken
|
|
153
|
+
* it: an unauthenticated request still gets a 401 from the route's own check.
|
|
154
|
+
*/
|
|
155
|
+
export function isStaticBundleUploadPath(pathname: string): boolean {
|
|
156
|
+
return pathname === "/api/publish/stage-bundle";
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
/**
|
|
148
160
|
* The candidate-preview CONTROL-PLANE routes — the per-change hosted review
|
|
149
161
|
* surfaces that carry their OWN, stricter auth (the cockpit gate: signed-in +
|
|
@@ -158,20 +170,56 @@ export function isPublicPath(pathname: string): boolean {
|
|
|
158
170
|
* blocker naming the head SHA), never a misleading "Coming soon". A demo BROWSE of
|
|
159
171
|
* the tenant's own pages stays gated as before.
|
|
160
172
|
*
|
|
161
|
-
* Matched (prefix, so sub-paths are covered)
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* /preview/<tenant
|
|
165
|
-
* /preview/<
|
|
173
|
+
* Matched (prefix, so sub-paths are covered). The PR/rev aliases are tenant-first
|
|
174
|
+
* (`/<tenant>/preview/...`, the leading segment middleware-selects the tenant),
|
|
175
|
+
* matched here on the pre-strip request path:
|
|
176
|
+
* /preview/render/<tenant>/<changeId>/... the candidate content renderer
|
|
177
|
+
* /preview/change/<changeId> the per-change review dashboard
|
|
178
|
+
* /<tenant>/preview/pr/<n> the friendly PR-number alias
|
|
179
|
+
* /<tenant>/preview/rev/<sha>/... the immutable per-revision renderer
|
|
166
180
|
*/
|
|
167
181
|
export function isCandidatePreviewPath(pathname: string): boolean {
|
|
168
182
|
if (pathname.startsWith("/preview/render/")) return true;
|
|
169
183
|
if (pathname === "/preview/change" || pathname.startsWith("/preview/change/")) return true;
|
|
170
|
-
//
|
|
171
|
-
if (/^\/
|
|
172
|
-
//
|
|
184
|
+
// /<tenant>/preview/pr and /<tenant>/preview/pr/<n> — the PR-number alias.
|
|
185
|
+
if (/^\/[^/]+\/preview\/pr(?:\/|$)/.test(pathname)) return true;
|
|
186
|
+
// /<tenant>/preview/rev/<sha> and /<tenant>/preview/rev/<sha>/<path...> — the
|
|
173
187
|
// immutable per-revision alias.
|
|
174
|
-
return /^\/
|
|
188
|
+
return /^\/[^/]+\/preview\/rev(?:\/|$)/.test(pathname);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Admin surfaces, in BOTH the bare host-routed form (`/admin`, `/admin/...`) and
|
|
193
|
+
* the tenant-first path form (`/<tenant>/admin`, `/<tenant>/admin/...`, where the
|
|
194
|
+
* leading segment middleware-selects the tenant). Admin reuses the same
|
|
195
|
+
* loginGate/sessionAdmitsHost mechanism as public content — there is no separate
|
|
196
|
+
* password layer — so this predicate is what keeps the QA content bypass from ever
|
|
197
|
+
* reaching admin: /admin ALWAYS requires a real, valid session.
|
|
198
|
+
*/
|
|
199
|
+
export function isAdminPath(pathname: string): boolean {
|
|
200
|
+
if (pathname === "/admin" || pathname.startsWith("/admin/")) return true;
|
|
201
|
+
return /^\/[^/]+\/admin(?:\/|$)/.test(pathname);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Paths the QA content bypass must NEVER cover — everything privileged or
|
|
206
|
+
* state-mutating. The bypass is a convenience for browsing public tenant content
|
|
207
|
+
* (product/marketing pages) only; these fail toward requiring a real session:
|
|
208
|
+
* - admin ({@link isAdminPath}) — the hard security requirement.
|
|
209
|
+
* - the cockpit control-plane (`/cockpit`, `/<tenant>/cockpit`) and its `/dev`
|
|
210
|
+
* redirect alias.
|
|
211
|
+
* - every `/api/` route (tenant-state mutation). Genuinely-public API endpoints
|
|
212
|
+
* are already gate-exempt via {@link isPublicPath}/{@link isAuthPath}, so
|
|
213
|
+
* excluding all of `/api/` here costs nothing and fails safe.
|
|
214
|
+
*/
|
|
215
|
+
export function isBypassPrivilegedPath(pathname: string): boolean {
|
|
216
|
+
if (isAdminPath(pathname)) return true;
|
|
217
|
+
if (pathname === "/cockpit" || pathname.startsWith("/cockpit/")) return true;
|
|
218
|
+
if (/^\/[^/]+\/cockpit(?:\/|$)/.test(pathname)) return true;
|
|
219
|
+
if (pathname === "/dev" || pathname.startsWith("/dev/")) return true;
|
|
220
|
+
if (/^\/[^/]+\/dev(?:\/|$)/.test(pathname)) return true;
|
|
221
|
+
if (pathname === "/api" || pathname.startsWith("/api/")) return true;
|
|
222
|
+
return false;
|
|
175
223
|
}
|
|
176
224
|
|
|
177
225
|
export type LoginGateDecision =
|
|
@@ -192,6 +240,14 @@ export interface LoginGateInput {
|
|
|
192
240
|
* exempt from the gate so it renders publicly. See {@link PUBLIC_HOME_TENANT_ID}.
|
|
193
241
|
*/
|
|
194
242
|
tenantExempt?: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* True when the request presented the server-configured QA bypass token (see
|
|
245
|
+
* `DEMO_GATE_BYPASS_TOKEN` in middleware). Only ever set in a QA/test deploy — an
|
|
246
|
+
* unset server token leaves it false, so this is fully inert by default. Passes
|
|
247
|
+
* the gate for PUBLIC CONTENT paths only; {@link isBypassPrivilegedPath} paths
|
|
248
|
+
* (admin, cockpit, /api) are never bypassed regardless of this flag.
|
|
249
|
+
*/
|
|
250
|
+
qaBypass?: boolean;
|
|
195
251
|
}
|
|
196
252
|
|
|
197
253
|
/**
|
|
@@ -208,6 +264,12 @@ export function evaluateLoginGate(input: LoginGateInput): LoginGateDecision {
|
|
|
208
264
|
// Candidate-preview control-plane routes govern their OWN (stricter, honest) auth
|
|
209
265
|
// and must never fall to the "Coming soon" holding page for a real candidate.
|
|
210
266
|
if (isCandidatePreviewPath(input.pathname)) return { action: "pass" };
|
|
267
|
+
// The static-bundle staging upload carries its own private_key_jwt auth.
|
|
268
|
+
if (isStaticBundleUploadPath(input.pathname)) return { action: "pass" };
|
|
269
|
+
// QA content-bypass: a request presenting the server-configured token passes for
|
|
270
|
+
// public content only. NEVER for admin/cockpit/api — that guard lives here (not
|
|
271
|
+
// just in middleware) so /admin stays gated even if the flag is set upstream.
|
|
272
|
+
if (input.qaBypass && !isBypassPrivilegedPath(input.pathname)) return { action: "pass" };
|
|
211
273
|
if (input.hasValidSession) return { action: "pass" };
|
|
212
274
|
const next = input.pathname + (input.search ?? "");
|
|
213
275
|
return { action: "redirect", location: `/auth/login?next=${encodeURIComponent(next)}` };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP client-assertion MINTER + signing-key loader — the storefront half of the
|
|
3
|
-
* "MCP machine identity" contract
|
|
4
|
-
* `sf-client-assertion`).
|
|
3
|
+
* "MCP machine identity" contract.
|
|
5
4
|
*
|
|
6
5
|
* The deployed storefront Worker is a browserless, multi-tenant service with no
|
|
7
6
|
* human in the loop. It authenticates to the MCP's `client_credentials` grant
|
|
@@ -10,8 +9,7 @@
|
|
|
10
9
|
* it holds `MCP_CLIENT_ASSERTION_PRIVATE_KEY_B64`, and the MCP verifies the
|
|
11
10
|
* signature against storefront's PUBLISHED public key (fetched by `kid` from the
|
|
12
11
|
* JWKS this module derives — see `loadMcpClientAssertionJwks` + the sibling
|
|
13
|
-
* `.well-known/mcp-client-jwks.json` route
|
|
14
|
-
* `c0-client-key-resolution`).
|
|
12
|
+
* `.well-known/mcp-client-jwks.json` route).
|
|
15
13
|
*
|
|
16
14
|
* Claims (RFC 7523 §3): `iss = sub = client_id`, `aud = <mcp token endpoint>`,
|
|
17
15
|
* `exp ~= iat + 60s`, a fresh `jti` every call (one-time-use, verified server-side
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared machinery for storefront's two-path operator-session resolvers: a
|
|
3
|
+
* signed-in host-bound tenant viewer (path 1), else a Bearer operator secret
|
|
4
|
+
* (path 2), fail-closed if neither validates. The grants owner session
|
|
5
|
+
* (`../grants/session.ts`) and the Private Apps admin session
|
|
6
|
+
* (`../apps/adminSession.ts`) are both configured from these primitives, so the
|
|
7
|
+
* fail-closed ordering and the host-bound tenant derivation live in ONE audited
|
|
8
|
+
* place. Each resolver supplies only what genuinely differs — the result shape,
|
|
9
|
+
* the Bearer secret/env sourcing, the operator-capability set, and how a present
|
|
10
|
+
* viewer that lacks that capability is treated.
|
|
11
|
+
*/
|
|
12
|
+
import type { APIContext } from "astro";
|
|
13
|
+
|
|
14
|
+
/** The host-bound viewer chrome the middleware sets on `locals.viewer`. */
|
|
15
|
+
type Viewer = NonNullable<APIContext["locals"]["viewer"]>;
|
|
16
|
+
|
|
17
|
+
/** The fail-closed shape both resolvers share for a refused session. */
|
|
18
|
+
export type SessionFailure = { ok: false; status: number; message: string };
|
|
19
|
+
|
|
20
|
+
/** Pure inputs to the Bearer-fallback authorization decision (env + request, no I/O). */
|
|
21
|
+
export interface BearerOperatorInput {
|
|
22
|
+
/** The request `Authorization` header, if any. */
|
|
23
|
+
authHeader: string | null;
|
|
24
|
+
/** The configured operator gate secret (or undefined if unconfigured). */
|
|
25
|
+
gateSecret: string | undefined;
|
|
26
|
+
/** Server-configured principal for this deployment, preferred over the header. */
|
|
27
|
+
configuredPrincipal: string | undefined;
|
|
28
|
+
/** Requested-principal header — honoured ONLY once the Bearer authenticates. */
|
|
29
|
+
requestedPrincipal: string | null;
|
|
30
|
+
/** 503 message when the gate secret is unconfigured. */
|
|
31
|
+
unconfiguredMessage: string;
|
|
32
|
+
/** 400 message when no principal can be resolved. */
|
|
33
|
+
unresolvedMessage: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type BearerOperatorResult = { ok: true; principal: string } | SessionFailure;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The fail-closed Bearer-operator core, with NO I/O — unit-testable. The
|
|
40
|
+
* ordering is the security contract: unconfigured gate → 503, bad/absent Bearer
|
|
41
|
+
* → 401 (BEFORE any principal is read, so a bare caller cannot target another
|
|
42
|
+
* tenant by naming it in the requested-principal header), no resolvable
|
|
43
|
+
* principal → 400. The server-configured principal is preferred over the
|
|
44
|
+
* request header so a single-principal deploy cannot be redirected cross-tenant.
|
|
45
|
+
*/
|
|
46
|
+
export function authorizeBearerOperator(
|
|
47
|
+
input: BearerOperatorInput,
|
|
48
|
+
): BearerOperatorResult {
|
|
49
|
+
if (!input.gateSecret) {
|
|
50
|
+
return { ok: false, status: 503, message: input.unconfiguredMessage };
|
|
51
|
+
}
|
|
52
|
+
if (input.authHeader !== `Bearer ${input.gateSecret}`) {
|
|
53
|
+
return { ok: false, status: 401, message: "Unauthorized" };
|
|
54
|
+
}
|
|
55
|
+
const principal = (
|
|
56
|
+
input.configuredPrincipal ??
|
|
57
|
+
input.requestedPrincipal ??
|
|
58
|
+
""
|
|
59
|
+
).trim();
|
|
60
|
+
if (!principal) {
|
|
61
|
+
return { ok: false, status: 400, message: input.unresolvedMessage };
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, principal };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* How to treat a signed-in viewer that IS present on the resolved host but lacks
|
|
68
|
+
* an operator capability. The two resolvers deliberately differ here:
|
|
69
|
+
* - `fallThrough` — continue to the Bearer gate (grants' owner session), so a
|
|
70
|
+
* lower-capability viewer that also presents a valid operator secret is
|
|
71
|
+
* admitted via path 2 exactly as an unauthenticated headless caller is.
|
|
72
|
+
* - `failClosed` — return the supplied 403 and do NOT consult the Bearer gate
|
|
73
|
+
* (apps admin session), so an authenticated-but-insufficient viewer is never
|
|
74
|
+
* silently admitted by a secret it happens to also present.
|
|
75
|
+
*/
|
|
76
|
+
export type InsufficientCapabilityPolicy =
|
|
77
|
+
| { mode: "fallThrough" }
|
|
78
|
+
| { mode: "failClosed"; failure: SessionFailure };
|
|
79
|
+
|
|
80
|
+
export interface HostBoundSessionConfig<T> {
|
|
81
|
+
/** Capabilities that make a host-bound viewer an operator for their tenant. */
|
|
82
|
+
capabilities: ReadonlySet<string>;
|
|
83
|
+
/** How to treat a present-but-insufficient viewer (see the type). */
|
|
84
|
+
onInsufficientCapability: InsufficientCapabilityPolicy;
|
|
85
|
+
/** Build the success result from the authorized viewer + resolved host appDomain. */
|
|
86
|
+
resolveViewer: (viewer: Viewer, tenant: string) => Promise<T> | T;
|
|
87
|
+
/** The Bearer path (path 2), consulted whenever path 1 does not resolve. */
|
|
88
|
+
resolveBearer: (context: APIContext) => Promise<T | SessionFailure>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Two-path host-bound operator session: a signed-in `locals.viewer` on the
|
|
93
|
+
* middleware-resolved `locals.tenant` (path 1), else the Bearer gate (path 2).
|
|
94
|
+
* The tenant is ALWAYS the resolved host `appDomain` — never the request body.
|
|
95
|
+
* When no viewer/tenant pair is present, or a present viewer is insufficient
|
|
96
|
+
* under a `fallThrough` policy, path 2 decides.
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveHostBoundSession<T>(
|
|
99
|
+
context: APIContext,
|
|
100
|
+
config: HostBoundSessionConfig<T>,
|
|
101
|
+
): Promise<T | SessionFailure> {
|
|
102
|
+
const viewer = context.locals?.viewer;
|
|
103
|
+
const tenant = context.locals?.tenant?.appDomain;
|
|
104
|
+
if (viewer && tenant) {
|
|
105
|
+
if (config.capabilities.has(viewer.capability)) {
|
|
106
|
+
return config.resolveViewer(viewer, tenant);
|
|
107
|
+
}
|
|
108
|
+
if (config.onInsufficientCapability.mode === "failClosed") {
|
|
109
|
+
return config.onInsufficientCapability.failure;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return config.resolveBearer(context);
|
|
113
|
+
}
|
|
@@ -20,7 +20,7 @@ export const SESSION_COOKIE = "tot_session";
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* A ToT-staff viewer's ACTIVE vendor selection — the two-phase-admission marker
|
|
23
|
-
*
|
|
23
|
+
* for staff cross-vendor access. This is a DISTINCT access path from
|
|
24
24
|
* `tenants[]`: a staff user isn't a member of a bounded set of vendors, so we
|
|
25
25
|
* NEVER synthesize a `tenants[]` entry for them. Instead, after the staff viewer
|
|
26
26
|
* explicitly searches + enters a vendor and tot20's `staffVendorAccess` writes
|
|
@@ -43,25 +43,24 @@ export const DEFAULT_SESSION_TTL_SECONDS = 7 * 24 * 3600;
|
|
|
43
43
|
/**
|
|
44
44
|
* What we know about a signed-in viewer.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* `email`/`resource`/`roles`/`capability` are derived from tot's
|
|
47
47
|
* OFFLINE-VERIFIED `id_token` claims (see `@/lib/auth/identityToken`
|
|
48
48
|
* `sessionFromClaims`), not from trusting tot's flat `/verify` JSON. `capability`
|
|
49
49
|
* is the coarse standing on `resource` (owner|admin|member). `tenants`/`staff`
|
|
50
|
-
* are the multi-tenant claims carried in the verified token
|
|
51
|
-
* session was minted via the Phase-3 path; optional for backward compatibility).
|
|
50
|
+
* are the multi-tenant claims carried in the verified token.
|
|
52
51
|
*/
|
|
53
52
|
export interface SessionRecord {
|
|
54
53
|
/** Verified email (lowercased) — the principal that passed the grant check. */
|
|
55
54
|
email: string;
|
|
56
55
|
/** The resource (appDomain/tenant, e.g. "sample-store.example") this session authorizes. */
|
|
57
56
|
resource: string;
|
|
58
|
-
/** Display roles for `resource` (
|
|
57
|
+
/** Display roles for `resource` (the coarse role from verified claims). */
|
|
59
58
|
roles: string[];
|
|
60
|
-
/** Coarse capability on `resource` (owner|admin|member
|
|
59
|
+
/** Coarse capability on `resource` (owner|admin|member). */
|
|
61
60
|
capability: string;
|
|
62
|
-
/** All tenant memberships from the verified assertion (policy input).
|
|
61
|
+
/** All tenant memberships from the verified assertion (policy input). */
|
|
63
62
|
tenants?: { resource: string; role: string }[];
|
|
64
|
-
/** ToT-staff scope from the verified assertion (sandboxed; grants nothing
|
|
63
|
+
/** ToT-staff scope from the verified assertion (sandboxed; grants nothing). */
|
|
65
64
|
staff?: string[];
|
|
66
65
|
/**
|
|
67
66
|
* A staff viewer's active, explicitly-entered vendor selection (the two-phase
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { readEnv } from "@/lib/env";
|
|
30
30
|
import { diagEmail } from "@/lib/diag/redact";
|
|
31
|
+
import { FetchHttpTransport, type HttpResponseLike, type HttpTransport } from "@/lib/http/fetchTransport";
|
|
31
32
|
import { classifyStepUp, type StepUpChallenge, type StepUpObligation } from "./stepUpChallenge.js";
|
|
32
33
|
|
|
33
34
|
/** `send` is deliberately opaque — it never reveals whether the email was eligible. */
|
|
@@ -150,24 +151,13 @@ export type SmsVerifyResult =
|
|
|
150
151
|
|
|
151
152
|
// --- HTTP transport seam (injected by tests) --------------------------------
|
|
152
153
|
|
|
153
|
-
export
|
|
154
|
-
status: number;
|
|
155
|
-
json(): Promise<unknown>;
|
|
156
|
-
}
|
|
157
|
-
export interface HttpTransport {
|
|
158
|
-
post(url: string, headers: Record<string, string>, body: unknown): Promise<HttpResponseLike>;
|
|
159
|
-
}
|
|
154
|
+
export type { HttpResponseLike, HttpTransport };
|
|
160
155
|
|
|
161
|
-
/** Default transport: a JSON `fetch` POST
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
167
|
-
body: JSON.stringify(body),
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
156
|
+
/** Default transport: a JSON `fetch` POST that also sends `accept: application/json`. */
|
|
157
|
+
const defaultTransport = () =>
|
|
158
|
+
new FetchHttpTransport({
|
|
159
|
+
defaultHeaders: { "content-type": "application/json", accept: "application/json" },
|
|
160
|
+
});
|
|
171
161
|
|
|
172
162
|
export interface HttpTotAccessClientConfig {
|
|
173
163
|
/** tot20 core API base (no trailing slash), e.g. https://qa.tokenoftrust.com */
|
|
@@ -193,7 +183,7 @@ export class HttpTotAccessClient implements TotAccessClient {
|
|
|
193
183
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
194
184
|
this.apiKey = config.apiKey;
|
|
195
185
|
this.secretKey = config.secretKey;
|
|
196
|
-
this.transport = config.transport ??
|
|
186
|
+
this.transport = config.transport ?? defaultTransport();
|
|
197
187
|
}
|
|
198
188
|
|
|
199
189
|
/**
|
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
*
|
|
14
14
|
* It is idempotent and conservative: only root-absolute single-slash paths are
|
|
15
15
|
* prefixed. Protocol-relative URLs ("//cdn…"), absolute URLs ("https://…"),
|
|
16
|
-
* fragments/queries ("#x", "?x"), and already-prefixed paths pass through.
|
|
17
|
-
*
|
|
16
|
+
* fragments/queries ("#x", "?x"), and already-prefixed paths pass through. The
|
|
17
|
+
* GLOBAL static-asset namespaces are also left untouched — they are served at root
|
|
18
|
+
* under both routing modes and must NOT carry a tenant prefix:
|
|
19
|
+
* - `/tenants/…` — per-tenant static assets (logo, favicon, theme/mkt css);
|
|
20
|
+
* - `/shared/…` — platform-shipped shared assets (shared commerce-chrome css).
|
|
21
|
+
* This mirrors both `prefixTenantLinks` (lib/rawMarketingHtml.ts) and the
|
|
22
|
+
* defense-in-depth client script in Layout.astro.
|
|
18
23
|
*/
|
|
19
24
|
export function withBase(basePath: string, href: string): string {
|
|
20
25
|
if (!basePath || !href) return href;
|
|
21
26
|
// Only same-origin root-absolute paths; skip protocol-relative "//".
|
|
22
27
|
if (href.charAt(0) !== "/" || href.charAt(1) === "/") return href;
|
|
28
|
+
// Global asset namespaces are root-served under every tenant; never prefix them.
|
|
29
|
+
if (href.startsWith("/tenants/") || href.startsWith("/shared/")) return href;
|
|
23
30
|
// Already inside the prefix.
|
|
24
31
|
if (href === basePath || href.startsWith(`${basePath}/`)) return href;
|
|
25
32
|
return `${basePath}${href}`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SessionRecord } from "@/lib/auth/session.js";
|
|
2
|
+
import type { BlogAccess } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a signed-in viewer counts as a paid member. v1: authenticated ⇒ member,
|
|
6
|
+
* because there is no consumer entitlement source yet. This is the single seam a
|
|
7
|
+
* future billing/entitlement integration plugs into — keep it a named export so
|
|
8
|
+
* routes and predicates never re-implement the "is a member" decision inline.
|
|
9
|
+
*/
|
|
10
|
+
export function isConsumerMember(_viewer: SessionRecord): boolean {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Entitlement chokepoint: may this viewer read the post's body? Orthogonal to
|
|
16
|
+
* visibility (`isVisibleOnLive`) — a gated post is still published/listed; this
|
|
17
|
+
* only decides full body vs teaser. Every reader path calls this one function.
|
|
18
|
+
*/
|
|
19
|
+
export function viewerHasBlogAccess(
|
|
20
|
+
viewer: SessionRecord | null,
|
|
21
|
+
post: { access?: BlogAccess },
|
|
22
|
+
): boolean {
|
|
23
|
+
const need = post.access ?? "public";
|
|
24
|
+
if (need === "public") return true;
|
|
25
|
+
if (!viewer) return false;
|
|
26
|
+
if (need === "authenticated") return true;
|
|
27
|
+
if (need === "members") return isConsumerMember(viewer);
|
|
28
|
+
return false;
|
|
29
|
+
}
|