@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.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 +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -58,3 +58,15 @@ export async function readR2(binding: string): Promise<R2Bucket | undefined> {
|
|
|
58
58
|
const w = await getWorkerEnv();
|
|
59
59
|
return (w?.[binding] as R2Bucket | undefined) ?? undefined;
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cloudflare Queue producer binding (e.g. WEBHOOK_DELIVERY_QUEUE), if present.
|
|
64
|
+
* Untyped (`unknown`) deliberately — callers cast to their own minimal
|
|
65
|
+
* queue-shape interface (see `webhooks/cloudflareQueueDispatcher.ts`'s
|
|
66
|
+
* `CloudflareQueueSender`) rather than this module taking a types dep on a
|
|
67
|
+
* specific binding's shape.
|
|
68
|
+
*/
|
|
69
|
+
export async function readQueue(binding: string): Promise<unknown | undefined> {
|
|
70
|
+
const w = await getWorkerEnv();
|
|
71
|
+
return w?.[binding] ?? undefined;
|
|
72
|
+
}
|
|
@@ -9,8 +9,13 @@ export function organizationLd(tenant: TenantConfig, base: string) {
|
|
|
9
9
|
// Logo sourced from the migrated brand assets (og image / logo), so the
|
|
10
10
|
// Organization schema reflects the real merchant brand, not a generic favicon.
|
|
11
11
|
const brand = tenant.theme_tokens?.brand;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Absolutize the logo: schema.org wants an absolute URL, and tenant brand
|
|
13
|
+
// assets are stored as site-relative paths (e.g. /tenants/<id>/brand/og.svg).
|
|
14
|
+
// new URL() leaves an already-absolute value untouched.
|
|
15
|
+
const logo = new URL(
|
|
16
|
+
brand?.ogImage ?? brand?.logoLight ?? "/favicon.svg",
|
|
17
|
+
base + "/",
|
|
18
|
+
).toString();
|
|
14
19
|
return {
|
|
15
20
|
"@context": "https://schema.org",
|
|
16
21
|
"@type": "Organization",
|
|
@@ -97,10 +102,11 @@ export function productLd(
|
|
|
97
102
|
url,
|
|
98
103
|
}));
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
105
|
+
// NO aggregateRating/review here — those are emitted ONLY from real sourced
|
|
106
|
+
// numbers by reviewLd(), which callers merge over this Product graph. Emitting
|
|
107
|
+
// a rating from a bare `reviews.rating` metafield (with a placeholder count)
|
|
108
|
+
// fabricates rich-result markup for stores with no real reviews (e.g. PVC),
|
|
109
|
+
// which violates the review-markup policy (docs/architecture/review-markup-policy.md).
|
|
104
110
|
return {
|
|
105
111
|
"@context": "https://schema.org",
|
|
106
112
|
"@type": "Product",
|
|
@@ -121,17 +127,6 @@ export function productLd(
|
|
|
121
127
|
: "https://schema.org/OutOfStock",
|
|
122
128
|
offers,
|
|
123
129
|
},
|
|
124
|
-
...(ratingMf
|
|
125
|
-
? {
|
|
126
|
-
aggregateRating: {
|
|
127
|
-
"@type": "AggregateRating",
|
|
128
|
-
ratingValue: ratingMf.value,
|
|
129
|
-
bestRating: "5",
|
|
130
|
-
// ASSUMPTION: review count placeholder; real capture is Phase 2.
|
|
131
|
-
ratingCount: 24,
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
: {}),
|
|
135
130
|
};
|
|
136
131
|
}
|
|
137
132
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopper membership eligibility — the request-scoped seam that resolves whether
|
|
3
|
+
* the current shopper is a member, and which tier (F10).
|
|
4
|
+
*
|
|
5
|
+
* Phase 1 storefronts have NO shopper-login/account source (member accounts are
|
|
6
|
+
* a Phase-2 managed-checkout capability), so this resolves anonymous
|
|
7
|
+
* (`{ isMember: false }`) by default. The pricing engine in
|
|
8
|
+
* `@tot/public-runtime` is real and unit-tested regardless; member pricing only
|
|
9
|
+
* RENDERS once a real eligibility signal flows through here.
|
|
10
|
+
*
|
|
11
|
+
* Production wires the signal from the resolved account claim (e.g. a
|
|
12
|
+
* member-tier cookie/JWT set by the Phase-2 login), so the whole storefront gets
|
|
13
|
+
* member pricing by threading one function — no per-page auth logic.
|
|
14
|
+
*/
|
|
15
|
+
import type { MemberContext } from "@tot/public-runtime";
|
|
16
|
+
import { ANONYMOUS_MEMBER } from "@tot/public-runtime";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The minimal membership signal a request carries. Phase 1 never populates it;
|
|
20
|
+
* production derives it from the shopper's resolved account (a member-tier
|
|
21
|
+
* claim). Kept tiny + framework-free so it is trivially testable.
|
|
22
|
+
*/
|
|
23
|
+
export interface MembershipSignal {
|
|
24
|
+
/** The shopper's membership tier id, if they are a signed-in member. */
|
|
25
|
+
memberTierId?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the shopper's {@link MemberContext} from an optional membership signal.
|
|
30
|
+
* No signal (Phase 1, or an anonymous shopper) → non-member.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveMemberContext(signal?: MembershipSignal | null): MemberContext {
|
|
33
|
+
if (signal?.memberTierId) {
|
|
34
|
+
return { isMember: true, tierId: signal.memberTierId };
|
|
35
|
+
}
|
|
36
|
+
return ANONYMOUS_MEMBER;
|
|
37
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitoring contract (spec §F16 — monitors, alerts, operator runbooks).
|
|
3
|
+
*
|
|
4
|
+
* This is the CHECKED-IN source of truth for "what do we watch, at what
|
|
5
|
+
* threshold, at what severity, and which runbook fires when it trips." It is
|
|
6
|
+
* deliberately data, not wiring: Phase 1 ships the *contract* so operators can
|
|
7
|
+
* see coverage and reviewers can diff it; the actual alert providers (Cloudflare
|
|
8
|
+
* Health Checks / Workers metrics / Logpush alerts / a RUM sink) are provisioned
|
|
9
|
+
* out-of-band and each monitor records whether it is `in-code` today or still
|
|
10
|
+
* `needs-external` / `needs-secret`.
|
|
11
|
+
*
|
|
12
|
+
* Signal sources are named against real code where it exists in this repo
|
|
13
|
+
* (`/health` → lib/health.ts, CSP → pages/api/csp-report.ts + CSP_REPORT_PATH,
|
|
14
|
+
* commerce events → lib/analytics). Signals owned by sibling units are referenced
|
|
15
|
+
* by CONTRACT NAME, not imported — Core Web Vitals come from F14's RUM beacon,
|
|
16
|
+
* and age-verification / restricted-state denials come from F12's compliance
|
|
17
|
+
* evidence signals. This file does not edit or depend on those units; when they
|
|
18
|
+
* land, point the corresponding monitor's `observe` at the real sink.
|
|
19
|
+
*
|
|
20
|
+
* The companion operator doc renders this for humans and lists the external
|
|
21
|
+
* wiring/secrets checklist: docs/epics/sample-store-migration/monitoring-and-runbooks.md.
|
|
22
|
+
* RUNBOOK.md §6–§8 hold the incident runbooks these monitors dispatch to.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Alert severity, in escalation order.
|
|
27
|
+
* - `page` — wake someone now; launch-critical, customer-blocking.
|
|
28
|
+
* - `critical` — same-business-hours; degraded but not fully down, or a
|
|
29
|
+
* compliance/legal risk that must be resolved before more sales.
|
|
30
|
+
* - `warning` — investigate; a spike or trend that predicts a `critical`.
|
|
31
|
+
* - `info` — record only; useful for post-incident context, no alert.
|
|
32
|
+
*/
|
|
33
|
+
export type Severity = "page" | "critical" | "warning" | "info";
|
|
34
|
+
|
|
35
|
+
/** What each monitor watches, grouped so a dashboard can lane them. */
|
|
36
|
+
export type MonitorCategory =
|
|
37
|
+
| "availability"
|
|
38
|
+
| "commerce"
|
|
39
|
+
| "compliance"
|
|
40
|
+
| "catalog"
|
|
41
|
+
| "performance"
|
|
42
|
+
| "security";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Provisioning status of a monitor — the honest "is this actually alerting?"
|
|
46
|
+
* flag so a green manifest never implies coverage we don't have.
|
|
47
|
+
* - `in-code` — the signal is emitted by code in THIS repo; only a log
|
|
48
|
+
* or metric alert rule needs attaching.
|
|
49
|
+
* - `needs-external` — depends on an external provider/probe/sink that must be
|
|
50
|
+
* configured (Cloudflare Health Check, Logpush alert, RUM).
|
|
51
|
+
* - `needs-secret` — additionally blocked on a credential/token being set.
|
|
52
|
+
*/
|
|
53
|
+
export type WiringStatus = "in-code" | "needs-external" | "needs-secret";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stable ids for the six incident runbooks (RUNBOOK.md §7). Every monitor names
|
|
57
|
+
* the runbook an operator opens when it fires, so detection → response is a
|
|
58
|
+
* lookup, not archaeology.
|
|
59
|
+
*/
|
|
60
|
+
export type RunbookId =
|
|
61
|
+
| "checkout-down"
|
|
62
|
+
| "catalog-stale"
|
|
63
|
+
| "bad-price"
|
|
64
|
+
| "compliance-provider-down"
|
|
65
|
+
| "tax-shipping-outage"
|
|
66
|
+
| "bad-content-policy";
|
|
67
|
+
|
|
68
|
+
export interface Monitor {
|
|
69
|
+
/** Stable kebab-case id; also the alert name. Never renamed once shipped. */
|
|
70
|
+
id: string;
|
|
71
|
+
/** One-line human title for dashboards/alerts. */
|
|
72
|
+
title: string;
|
|
73
|
+
category: MonitorCategory;
|
|
74
|
+
/** Where the signal comes from and how a monitor reads it. */
|
|
75
|
+
signal: {
|
|
76
|
+
/** The emitter — code symbol/path in this repo, or a named sibling contract. */
|
|
77
|
+
source: string;
|
|
78
|
+
/** How the alert observes it: probe, Worker metric, log filter, RUM, webhook. */
|
|
79
|
+
observe: string;
|
|
80
|
+
};
|
|
81
|
+
/** The alarm condition in plain, checkable terms (thresholds are launch defaults). */
|
|
82
|
+
threshold: string;
|
|
83
|
+
severity: Severity;
|
|
84
|
+
/** Which incident runbook to run when this fires. */
|
|
85
|
+
runbook: RunbookId;
|
|
86
|
+
wiring: WiringStatus;
|
|
87
|
+
/** External dependency, secret name, or sibling-unit signal reference. */
|
|
88
|
+
notes?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The ten launch-critical monitors from spec §F16. Order is presentation order.
|
|
93
|
+
* Thresholds are the go-live defaults; tune against the 24h post-cutover soak
|
|
94
|
+
* (T10) and record any change here — this file is the audit trail.
|
|
95
|
+
*/
|
|
96
|
+
export const MONITORS: readonly Monitor[] = [
|
|
97
|
+
{
|
|
98
|
+
id: "uptime",
|
|
99
|
+
title: "Storefront uptime / liveness",
|
|
100
|
+
category: "availability",
|
|
101
|
+
signal: {
|
|
102
|
+
source: "GET /health (lib/health.ts — answered before tenant/gate/cache)",
|
|
103
|
+
observe: "external synthetic probe hits /health across regions; expects 200 {status:'ok'}",
|
|
104
|
+
},
|
|
105
|
+
threshold: "2 consecutive probe failures (or >1% of probes failing over 5m)",
|
|
106
|
+
severity: "page",
|
|
107
|
+
runbook: "checkout-down",
|
|
108
|
+
wiring: "needs-external",
|
|
109
|
+
notes:
|
|
110
|
+
"Wire a Cloudflare Health Check (or external uptime monitor) per production host. " +
|
|
111
|
+
"/health also returns the live commit .sha — use it to confirm which build is up during an incident.",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "page-errors",
|
|
115
|
+
title: "Page / Worker errors (5xx + unhandled exceptions)",
|
|
116
|
+
category: "availability",
|
|
117
|
+
signal: {
|
|
118
|
+
source: "Cloudflare Worker exceptions + non-2xx responses from middleware/index.ts render path",
|
|
119
|
+
observe: "Workers 'Errors' metric + Logpush; alert on error-rate over the request baseline",
|
|
120
|
+
},
|
|
121
|
+
threshold: "5xx rate > 1% over 5m, or any sustained unhandled-exception stream",
|
|
122
|
+
severity: "critical",
|
|
123
|
+
runbook: "bad-content-policy",
|
|
124
|
+
wiring: "needs-external",
|
|
125
|
+
notes:
|
|
126
|
+
"A spike isolated to one route usually points at a bad publish/content artifact (see bad-content-policy); " +
|
|
127
|
+
"a broad spike is an availability event (checkout-down).",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "checkout-failures",
|
|
131
|
+
title: "Checkout failures (add-to-cart / checkout handoff)",
|
|
132
|
+
category: "commerce",
|
|
133
|
+
signal: {
|
|
134
|
+
source:
|
|
135
|
+
"Signed add-to-cart + cross-origin checkout POST (F11; lib/checkoutCommerce.ts, checkoutCspAdditions)",
|
|
136
|
+
observe:
|
|
137
|
+
"Worker logs on the signed add-to-cart path + checkout-host success beacon; alert on failure ratio",
|
|
138
|
+
},
|
|
139
|
+
threshold: "checkout-start error ratio > 2% over 10m, or add-to-cart 5xx > 1% over 5m",
|
|
140
|
+
severity: "page",
|
|
141
|
+
runbook: "checkout-down",
|
|
142
|
+
wiring: "needs-external",
|
|
143
|
+
notes:
|
|
144
|
+
"Checkout completes on the store's cross-origin cart/checkout host, so full funnel coverage needs a beacon " +
|
|
145
|
+
"from that host too. Storefront-side we can only see the signed handoff; a CSP formAction/frameSrc block " +
|
|
146
|
+
"also surfaces here as csp-violations.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "age-verification-failures",
|
|
150
|
+
title: "Age-verification failures",
|
|
151
|
+
category: "compliance",
|
|
152
|
+
signal: {
|
|
153
|
+
source:
|
|
154
|
+
"F12 compliance 'age-verification' evidence signal (AgeGate / IsolatedAgeGate → ToT verification)",
|
|
155
|
+
observe: "F12 evidence signal + ToT verification provider errors; alert on error/timeout rate, not on user-declines",
|
|
156
|
+
},
|
|
157
|
+
threshold: "verification provider error/timeout rate > 5% over 10m (distinct from legitimate fail-age outcomes)",
|
|
158
|
+
severity: "critical",
|
|
159
|
+
runbook: "compliance-provider-down",
|
|
160
|
+
wiring: "needs-external",
|
|
161
|
+
notes:
|
|
162
|
+
"Reference F12's signal by name; do NOT read the compliance internals here. Distinguish PROVIDER failure " +
|
|
163
|
+
"(page/critical — we can't legally sell) from a shopper genuinely failing age (expected, not an alert). " +
|
|
164
|
+
"Pre-launch config-coherence gate is in-repo: GET /api/evidence.json (assembleEvidenceReport → age_gate).",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "restricted-state-denials",
|
|
168
|
+
title: "Restricted-state / shipping denials (anomaly)",
|
|
169
|
+
category: "compliance",
|
|
170
|
+
signal: {
|
|
171
|
+
source:
|
|
172
|
+
"F12 compliance 'restricted-state denial' signal (StateEligibilityNotice / ShippingRestrictionNotice / complianceFloor)",
|
|
173
|
+
observe: "F12 evidence signal counted per state; alert on ANOMALOUS spike vs baseline, not on absolute count",
|
|
174
|
+
},
|
|
175
|
+
threshold: "denial rate for a state jumps >3x its 7-day baseline over 1h (mis-configured rule, not normal traffic)",
|
|
176
|
+
severity: "warning",
|
|
177
|
+
runbook: "bad-content-policy",
|
|
178
|
+
wiring: "needs-external",
|
|
179
|
+
notes:
|
|
180
|
+
"Denials are EXPECTED (that's the control working). This monitor catches a mis-configured restriction " +
|
|
181
|
+
"rule that suddenly blocks a legal state — a spike, not a level. Reference F12's signal by name. " +
|
|
182
|
+
"Pre-launch config-coherence gate is in-repo: GET /api/evidence.json (jurisdiction/excise signals).",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "catalog-sync-failures",
|
|
186
|
+
title: "Catalog sync failures",
|
|
187
|
+
category: "catalog",
|
|
188
|
+
signal: {
|
|
189
|
+
source: "Migration incremental sync (packages/migration) + F17 catalog graduation job",
|
|
190
|
+
observe: "sync-job exit status + migration report (errors/skipped); alert on failed or missed run",
|
|
191
|
+
},
|
|
192
|
+
threshold: "any sync job fails, OR no successful sync within the expected cadence window (default 1h)",
|
|
193
|
+
severity: "warning",
|
|
194
|
+
runbook: "catalog-stale",
|
|
195
|
+
wiring: "needs-external",
|
|
196
|
+
notes:
|
|
197
|
+
"Escalate to critical once a store is on a live source (F17): a failed sync means the catalog is drifting " +
|
|
198
|
+
"from the merchant source. Depends on F17 defining the launch cadence.",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "inventory-mismatch",
|
|
202
|
+
title: "Inventory mismatch (oversell risk)",
|
|
203
|
+
category: "catalog",
|
|
204
|
+
signal: {
|
|
205
|
+
source: "F17 inventory-freshness mismatch alarm (storefront-shown vs source-of-truth stock)",
|
|
206
|
+
observe: "reconciliation check comparing served inventory to the merchant/hosted source",
|
|
207
|
+
},
|
|
208
|
+
threshold: "any product served in-stock while source says out-of-stock, or staleness > sync cadence + grace",
|
|
209
|
+
severity: "critical",
|
|
210
|
+
runbook: "catalog-stale",
|
|
211
|
+
wiring: "needs-external",
|
|
212
|
+
notes:
|
|
213
|
+
"Overselling regulated goods is a launch-blocker. This alarm is the one F16 defines but F17 FEEDS with real " +
|
|
214
|
+
"data — until F17 lands it is a contract only.",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: "payment-failures",
|
|
218
|
+
title: "Payment failures",
|
|
219
|
+
category: "commerce",
|
|
220
|
+
signal: {
|
|
221
|
+
source: "Payment provider on the checkout host (external to the storefront Worker)",
|
|
222
|
+
observe: "payment provider dashboard/webhooks; alert on declined/error rate vs baseline",
|
|
223
|
+
},
|
|
224
|
+
threshold: "payment error rate (non-decline gateway errors) > 3% over 15m",
|
|
225
|
+
severity: "page",
|
|
226
|
+
runbook: "checkout-down",
|
|
227
|
+
wiring: "needs-external",
|
|
228
|
+
notes:
|
|
229
|
+
"Owned by the checkout/payment provider, not this Worker. Separate genuine card declines (expected) from " +
|
|
230
|
+
"gateway/integration errors (alert). Coordinate the provider webhook wiring with F11/F17 owners.",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "csp-violations",
|
|
234
|
+
title: "CSP violations",
|
|
235
|
+
category: "security",
|
|
236
|
+
signal: {
|
|
237
|
+
source: "POST /api/csp-report (CSP_REPORT_PATH / CSP_REPORT_GROUP) — logs '[csp-report]'",
|
|
238
|
+
observe: "log-based alert on '[csp-report]' volume; CSP is ENFORCED in prod (middleware sets Content-Security-Policy)",
|
|
239
|
+
},
|
|
240
|
+
threshold: "sustained new-violation stream (> baseline) — esp. on checkout/widget origins after a publish",
|
|
241
|
+
severity: "warning",
|
|
242
|
+
runbook: "bad-content-policy",
|
|
243
|
+
wiring: "in-code",
|
|
244
|
+
notes:
|
|
245
|
+
"Emitter already ships (pages/api/csp-report.ts). Only a log-volume alert rule needs attaching. A spike after " +
|
|
246
|
+
"a tenant publish means a script/style/frame origin needs adding to the allowlist — or a blocked injection.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "core-web-vitals",
|
|
250
|
+
title: "Core Web Vitals (LCP / INP / CLS)",
|
|
251
|
+
category: "performance",
|
|
252
|
+
signal: {
|
|
253
|
+
source: "F14 RUM beacon (web-vitals: LCP, INP, CLS) — Real User Monitoring",
|
|
254
|
+
observe: "RUM sink p75 per metric per key template (home, PLP, PDP, checkout entry)",
|
|
255
|
+
},
|
|
256
|
+
threshold: "p75 regresses past 'good': LCP > 2.5s, INP > 200ms, CLS > 0.1 (sustained over 1h)",
|
|
257
|
+
severity: "warning",
|
|
258
|
+
runbook: "bad-content-policy",
|
|
259
|
+
wiring: "needs-external",
|
|
260
|
+
notes:
|
|
261
|
+
"Reference F14's RUM signal by name; do NOT edit F14's analytics files. Cloudflare Web Analytics " +
|
|
262
|
+
"(PUBLIC_CF_ANALYTICS_TOKEN, injected in Layout.astro) gives field CWV today; F14's beacon gives per-template detail.",
|
|
263
|
+
},
|
|
264
|
+
] as const;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The monitors §F16 requires by name. The test asserts MONITORS covers exactly
|
|
268
|
+
* these — so dropping or misnaming one fails CI, and adding a new monitor is a
|
|
269
|
+
* deliberate edit here.
|
|
270
|
+
*/
|
|
271
|
+
export const REQUIRED_MONITOR_IDS = [
|
|
272
|
+
"uptime",
|
|
273
|
+
"page-errors",
|
|
274
|
+
"checkout-failures",
|
|
275
|
+
"age-verification-failures",
|
|
276
|
+
"restricted-state-denials",
|
|
277
|
+
"catalog-sync-failures",
|
|
278
|
+
"inventory-mismatch",
|
|
279
|
+
"payment-failures",
|
|
280
|
+
"csp-violations",
|
|
281
|
+
"core-web-vitals",
|
|
282
|
+
] as const;
|
|
283
|
+
|
|
284
|
+
/** The six incident runbooks in RUNBOOK.md §7 that monitors dispatch to. */
|
|
285
|
+
export const RUNBOOK_IDS: readonly RunbookId[] = [
|
|
286
|
+
"checkout-down",
|
|
287
|
+
"catalog-stale",
|
|
288
|
+
"bad-price",
|
|
289
|
+
"compliance-provider-down",
|
|
290
|
+
"tax-shipping-outage",
|
|
291
|
+
"bad-content-policy",
|
|
292
|
+
] as const;
|
|
293
|
+
|
|
294
|
+
/** Lookup a monitor by id (undefined if absent). */
|
|
295
|
+
export function getMonitor(id: string): Monitor | undefined {
|
|
296
|
+
return MONITORS.find((m) => m.id === id);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Monitors still needing external wiring or a secret before they alert. */
|
|
300
|
+
export function monitorsNeedingWiring(): readonly Monitor[] {
|
|
301
|
+
return MONITORS.filter((m) => m.wiring !== "in-code");
|
|
302
|
+
}
|
|
@@ -6,7 +6,16 @@ function maskPart(part: string): string {
|
|
|
6
6
|
return `${part.slice(0, 1)}...${part.slice(-1)}`;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Display-only email hint for URLs/log-safe UX. Never returns the full address:
|
|
11
|
+
* the LOCAL part — the personal identifier — is always masked. The DOMAIN is shown
|
|
12
|
+
* in full for recognition value ("that's my work address"): a corporate domain is
|
|
13
|
+
* org-level, not personal data on its own, and the masked local part keeps this from
|
|
14
|
+
* being a clean, greppable address in a URL/log. Kept within the current GDPR posture
|
|
15
|
+
* (see the mask-level decision in auth/magic + the signed-in cockpit, which shows the
|
|
16
|
+
* FULL address from the validated session — never from a URL). Masks with `.` (not `*`)
|
|
17
|
+
* so the hint round-trips back through normalizeEmailHint's SAFE_HINT_RE.
|
|
18
|
+
*/
|
|
10
19
|
export function redactEmailForHint(email: string): string | null {
|
|
11
20
|
const raw = String(email || "").trim().toLowerCase();
|
|
12
21
|
const at = raw.indexOf("@");
|
|
@@ -15,10 +24,9 @@ export function redactEmailForHint(email: string): string | null {
|
|
|
15
24
|
const domain = raw.slice(at + 1);
|
|
16
25
|
const parts = domain.split(".").filter(Boolean);
|
|
17
26
|
if (parts.length < 2) return null;
|
|
18
|
-
const suffix = parts.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return `${maskPart(local)}@${maskPart(registrable)}.${suffix}`;
|
|
27
|
+
const suffix = parts[parts.length - 1];
|
|
28
|
+
if (!suffix || !local) return null;
|
|
29
|
+
return `${maskPart(local)}@${domain}`;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
/**
|