@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.1
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/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +20 -0
- package/apps/storefront/package.json +12 -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 +3562 -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/activity/alerts.ts +428 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +127 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +105 -0
- package/apps/storefront/src/lib/activity/store.ts +122 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- 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 +219 -0
- package/apps/storefront/src/lib/auth/route.ts +60 -5
- 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 +267 -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/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/dev/vcBinding.ts +80 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +63 -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 +369 -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/middleware/index.ts +45 -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/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +183 -9
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +86 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +125 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +79 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +80 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +98 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +126 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +70 -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 +183 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +162 -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 +74 -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/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -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 +202 -45
- 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 +470 -69
- 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/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +501 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +324 -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 +12 -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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared emit helper for UI-ORIGINATED operational activity (card D6).
|
|
3
|
+
*
|
|
4
|
+
* The UI cohort's entry point onto the D0 actor×action telemetry contract. It sits
|
|
5
|
+
* ON TOP of D1's server emitter ({@link recordActivity}) — every admin/cockpit action
|
|
6
|
+
* handler already runs SERVER-SIDE in the same Worker, so a UI action reaches
|
|
7
|
+
* telemetry through the same in-process `recordActivity()` call D1 uses (one
|
|
8
|
+
* structured Workers-Logs line), NOT a client-side `fetch` to the D4 ingest endpoint.
|
|
9
|
+
* There is no browser-only signal to capture: an admin/cockpit "Publish / Connect /
|
|
10
|
+
* Invite / Approve / Rollback" click is a server-rendered form/JSON POST to an API
|
|
11
|
+
* route, so the route handler that already does the work is exactly where the emit
|
|
12
|
+
* belongs (zero extra HTTP round-trip, no new auth/trust boundary). The D4 HMAC
|
|
13
|
+
* ingest path exists for emitters that CAN'T call `recordActivity()` directly (the
|
|
14
|
+
* out-of-process CLI cohort, D3) — it is not this cohort's path.
|
|
15
|
+
*
|
|
16
|
+
* WHAT THIS ADDS over calling `recordActivity()` raw: it stamps `source: "ui"` and,
|
|
17
|
+
* critically, resolves the ACTOR KIND from the authenticated session so the timeline
|
|
18
|
+
* (D5) can attribute an action to the right cohort:
|
|
19
|
+
*
|
|
20
|
+
* • "admin" — a ToT-STAFF viewer (the session carries `staff[]` scope). ToT
|
|
21
|
+
* operators acting through the admin shell / staff vendor selection.
|
|
22
|
+
* • "merchant" — a store OWNER acting on THEIR OWN store (a signed-in viewer whose
|
|
23
|
+
* host-bound capability is owner/admin). The self-serve cohort.
|
|
24
|
+
* • "dev" — a signed-in DEVELOPER viewer who is neither staff nor owner (e.g. a
|
|
25
|
+
* ship-on-behalf developer invited onto someone else's store).
|
|
26
|
+
* • "agent" — a HEADLESS, capability-driven caller with no human viewer session
|
|
27
|
+
* (the Bearer/CLI operator seam) — automation acting on the surface.
|
|
28
|
+
*
|
|
29
|
+
* The classifier is a PURE function ({@link classifyActorKind}) so the cohort mapping
|
|
30
|
+
* is unit-tested without a live request. The opaque `actor.id` is the salted,
|
|
31
|
+
* non-reversible hash of the acting identity (never raw PII in the core zone) — the
|
|
32
|
+
* SAME {@link hashActorId} + `ACTIVITY_ACTOR_SALT` D1 established, so one person stays
|
|
33
|
+
* correlatable across the CLI, server, and UI cohorts.
|
|
34
|
+
*
|
|
35
|
+
* USAGE (mirrors D1's reject.ts — resolve the actor ONCE after auth, emit at each
|
|
36
|
+
* exit point, zero change to the handler's control flow):
|
|
37
|
+
*
|
|
38
|
+
* const actor = await resolveUiActor(context, { owner: auth.owner });
|
|
39
|
+
* // …do the work…
|
|
40
|
+
* recordUiActivity(actor, { action: "change.ship", outcome: { status: "succeeded" }, scope });
|
|
41
|
+
*/
|
|
42
|
+
import type { APIContext } from "astro";
|
|
43
|
+
import type {
|
|
44
|
+
ActivityActor,
|
|
45
|
+
ActorKind,
|
|
46
|
+
ActivityOutcome,
|
|
47
|
+
ActivityScope,
|
|
48
|
+
ActivityPayload,
|
|
49
|
+
} from "@tot/public-runtime";
|
|
50
|
+
import type { ActionKey } from "@tot/public-runtime";
|
|
51
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
52
|
+
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
53
|
+
import { readEnv } from "@/lib/env";
|
|
54
|
+
import { hashActorId, recordActivity } from "./recordActivity";
|
|
55
|
+
|
|
56
|
+
/** The host-bound capabilities that mark a viewer as the store OWNER (merchant). */
|
|
57
|
+
const OWNER_CAPABILITIES = new Set(["owner", "admin"]);
|
|
58
|
+
|
|
59
|
+
/** The minimal viewer chrome the middleware stamps on `locals.viewer`. */
|
|
60
|
+
interface ViewerLike {
|
|
61
|
+
email?: string;
|
|
62
|
+
capability?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Classify the acting cohort from the authenticated session — PURE so the mapping is
|
|
67
|
+
* testable without a request. Order matters and is fail-safe:
|
|
68
|
+
*
|
|
69
|
+
* 1. no signed-in viewer → "agent" (headless Bearer/CLI operator — capability-
|
|
70
|
+
* driven automation; there is no human identity on this request).
|
|
71
|
+
* 2. ToT-staff session → "admin" (checked BEFORE ownership: a staff viewer who
|
|
72
|
+
* entered a vendor via a live selection carries an owner-ish host capability, but
|
|
73
|
+
* they are ToT staff, not the merchant).
|
|
74
|
+
* 3. owner/admin capability on this host → "merchant" (the store owner on their own store).
|
|
75
|
+
* 4. otherwise → "dev" (a signed-in developer without ownership, e.g.
|
|
76
|
+
* ship-on-behalf).
|
|
77
|
+
*/
|
|
78
|
+
export function classifyActorKind(
|
|
79
|
+
viewer: ViewerLike | null | undefined,
|
|
80
|
+
session: { staff?: string[] } | null | undefined,
|
|
81
|
+
): ActorKind {
|
|
82
|
+
if (!viewer?.email) return "agent";
|
|
83
|
+
if (isStaffSession(session)) return "admin";
|
|
84
|
+
if (viewer.capability && OWNER_CAPABILITIES.has(viewer.capability)) return "merchant";
|
|
85
|
+
return "dev";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ResolveUiActorInput {
|
|
89
|
+
/**
|
|
90
|
+
* Fallback acting identity for the HEADLESS path (no signed-in viewer) — typically
|
|
91
|
+
* the resolved tenant owner (`auth.owner`). Only used to seed the opaque id when
|
|
92
|
+
* there is no viewer email; never stored raw (always hashed).
|
|
93
|
+
*/
|
|
94
|
+
owner?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the {@link ActivityActor} for a UI-originated action from the request's
|
|
99
|
+
* authenticated session. Reads the full viewer session ONCE (for the staff scope the
|
|
100
|
+
* host-bound `locals.viewer` chrome doesn't carry) and computes the opaque, salted
|
|
101
|
+
* actor id. Best-effort: any read fault degrades to a coarse non-PII actor rather
|
|
102
|
+
* than throwing — telemetry must never break the action it describes. Resolve this
|
|
103
|
+
* ONCE per request (after auth) and reuse the result across every emit exit point.
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveUiActor(
|
|
106
|
+
context: APIContext,
|
|
107
|
+
input: ResolveUiActorInput = {},
|
|
108
|
+
): Promise<ActivityActor> {
|
|
109
|
+
// The host-bound `locals.viewer` chrome omits `staff[]`, so read the full session
|
|
110
|
+
// to tell a ToT-staff (admin) actor from a store owner (merchant). Best-effort.
|
|
111
|
+
const session = await readViewerSession(context).catch(() => null);
|
|
112
|
+
// Prefer the host-scoped `locals.viewer` (set by middleware for tenant-gated
|
|
113
|
+
// surfaces — the ship/grants routes); fall back to the raw session's
|
|
114
|
+
// email/capability for a route that never went through that host gate (e.g. the
|
|
115
|
+
// /dev cockpit's rendezvous-approval endpoint) but still has a real signed-in
|
|
116
|
+
// viewer. Both describe the SAME signed-in-or-not shape.
|
|
117
|
+
const viewer: ViewerLike | undefined =
|
|
118
|
+
(context.locals?.viewer as ViewerLike | undefined) ??
|
|
119
|
+
(session ? { email: session.email, capability: session.capability } : undefined);
|
|
120
|
+
const kind = classifyActorKind(viewer, session);
|
|
121
|
+
const identifier = viewer?.email ?? input.owner ?? "system";
|
|
122
|
+
const id = await hashActorId(identifier, await readEnv("ACTIVITY_ACTOR_SALT"));
|
|
123
|
+
return { kind, id };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Emit ONE UI-originated activity event with a PRE-RESOLVED actor. A thin, SYNC
|
|
128
|
+
* wrapper over D1's {@link recordActivity} that stamps `source: "ui"` — so a handler
|
|
129
|
+
* calls it at each exit point (succeeded / failed / refused) exactly as D1's server
|
|
130
|
+
* emitters do, with no control-flow change. Best-effort + never throws (inherited
|
|
131
|
+
* from `recordActivity`).
|
|
132
|
+
*/
|
|
133
|
+
export function recordUiActivity(
|
|
134
|
+
actor: ActivityActor,
|
|
135
|
+
input: {
|
|
136
|
+
action: ActionKey;
|
|
137
|
+
outcome: ActivityOutcome;
|
|
138
|
+
scope?: ActivityScope;
|
|
139
|
+
payload?: ActivityPayload;
|
|
140
|
+
},
|
|
141
|
+
): void {
|
|
142
|
+
recordActivity({
|
|
143
|
+
action: input.action,
|
|
144
|
+
actor,
|
|
145
|
+
source: "ui",
|
|
146
|
+
outcome: input.outcome,
|
|
147
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
148
|
+
...(input.payload ? { payload: input.payload } : {}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worker_commit — build provenance for the running Worker (card D2).
|
|
3
|
+
*
|
|
4
|
+
* The commit SHA THIS Worker build was cut from, made available REPO-WIDE at
|
|
5
|
+
* runtime so every server-emitted ActivityEvent can carry it (see event.ts
|
|
6
|
+
* `ActivityScope.workerCommit`) and D5 can later derive **deploy skew** — "is the
|
|
7
|
+
* build serving this request == the pushed umbrella tip, or is a stale worker
|
|
8
|
+
* still live?".
|
|
9
|
+
*
|
|
10
|
+
* HOW IT GETS HERE. Workers have no filesystem/git at runtime, so the SHA is
|
|
11
|
+
* baked in at BUILD time: astro.config.mjs's Vite `define` replaces the
|
|
12
|
+
* `__GIT_SHA__` token with a string literal of the build's commit (CI-provided
|
|
13
|
+
* `GIT_SHA`/`WORKERS_CI_COMMIT_SHA`/`GITHUB_SHA`, else local `git rev-parse`,
|
|
14
|
+
* else "unknown"). This is the SAME source /health already reports (lib/health.ts)
|
|
15
|
+
* — we read it through the identical `typeof` guard so both agree by construction,
|
|
16
|
+
* and so the value is safe under vitest (where the define isn't applied → "unknown").
|
|
17
|
+
*
|
|
18
|
+
* This is the single accessor the whole repo threads through: D1's recordActivity()
|
|
19
|
+
* spreads serverActivityScope() into every server event; D2's deploy.version_observed
|
|
20
|
+
* emitter (deployVersion.ts) reads workerCommit() directly.
|
|
21
|
+
*/
|
|
22
|
+
import type { ActivityScope } from "@tot/public-runtime";
|
|
23
|
+
|
|
24
|
+
/** Sentinel when the build had no git context (also the vitest value). */
|
|
25
|
+
export const UNKNOWN_COMMIT = "unknown";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The commit SHA this Worker build was cut from, or "unknown". Never throws; the
|
|
29
|
+
* `typeof` guard keeps it safe under vitest (the `__GIT_SHA__` define is only
|
|
30
|
+
* applied by the Astro/Vite build). Optionally overridable for tests.
|
|
31
|
+
*/
|
|
32
|
+
export function workerCommit(override?: string): string {
|
|
33
|
+
if (override != null) return override;
|
|
34
|
+
return typeof __GIT_SHA__ !== "undefined" ? __GIT_SHA__ : UNKNOWN_COMMIT;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** First 7 chars of the build commit for eyeballing against `git log`, or "unknown". */
|
|
38
|
+
export function workerCommitShort(override?: string): string {
|
|
39
|
+
const sha = workerCommit(override);
|
|
40
|
+
return sha === UNKNOWN_COMMIT ? UNKNOWN_COMMIT : sha.slice(0, 7);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* True when the build has a real git commit (not the "unknown" sentinel). D5's
|
|
45
|
+
* skew derivation only compares against the umbrella tip when this holds — an
|
|
46
|
+
* "unknown" build can't be meaningfully skew-checked.
|
|
47
|
+
*/
|
|
48
|
+
export function hasKnownWorkerCommit(override?: string): boolean {
|
|
49
|
+
return workerCommit(override) !== UNKNOWN_COMMIT;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build the CORE scope for a SERVER-sourced ActivityEvent with `worker_commit`
|
|
54
|
+
* stamped in. This is the seam D1's recordActivity() spreads into every server
|
|
55
|
+
* event so provenance is present WITHOUT each call site remembering it:
|
|
56
|
+
*
|
|
57
|
+
* createActivityEvent({ ..., source: "server", scope: serverActivityScope({ tenantId }) })
|
|
58
|
+
*
|
|
59
|
+
* Pass the event's other scope fields (tenantId/changeId/traceId) as `extra`;
|
|
60
|
+
* workerCommit always wins for the `workerCommit` slot. Omits the field entirely
|
|
61
|
+
* when the build commit is "unknown" so a downstream skew query never mistakes the
|
|
62
|
+
* sentinel for a real deployed SHA.
|
|
63
|
+
*/
|
|
64
|
+
export function serverActivityScope(extra?: ActivityScope): ActivityScope {
|
|
65
|
+
const sha = workerCommit();
|
|
66
|
+
return {
|
|
67
|
+
...extra,
|
|
68
|
+
...(sha !== UNKNOWN_COMMIT ? { workerCommit: sha } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "F14 performance budgets manifest — the checked-in, tool-neutral source of truth for the storefront go-live framework's perf gate. Consumed by (1) the lab budget-assertion harness (perf/assert-budgets.ts), (2) the Lighthouse budgets exporter (toLighthouseBudgets), and (3) the field RUM regression-alert contract (rumAlert.ts). Edit HERE — everything else derives from it. Shape + validation: src/lib/analytics/perfBudgets.ts (parsePerfBudgets).",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"webVitals": {
|
|
5
|
+
"$comment": "Core Web Vitals thresholds, evaluated at the 75th percentile (p75) of the field distribution — the same percentile Google's CrUX / the RUM alert contract use.",
|
|
6
|
+
"lcpMs": 2500,
|
|
7
|
+
"inpMs": 200,
|
|
8
|
+
"cls": 0.1
|
|
9
|
+
},
|
|
10
|
+
"resourceBudgets": {
|
|
11
|
+
"$comment": "Per-page transfer budgets in kibibytes (KiB), compressed-over-the-wire. Defaults; a route may tighten or (rarely) loosen them via its own resourceBudgets.",
|
|
12
|
+
"jsKib": 180,
|
|
13
|
+
"cssKib": 90,
|
|
14
|
+
"imageKib": 900,
|
|
15
|
+
"totalKib": 1300
|
|
16
|
+
},
|
|
17
|
+
"thirdPartyScriptAllowlist": [
|
|
18
|
+
"static.cloudflareinsights.com",
|
|
19
|
+
"cdn.foxycart.com"
|
|
20
|
+
],
|
|
21
|
+
"routes": [
|
|
22
|
+
{
|
|
23
|
+
"id": "home",
|
|
24
|
+
"label": "Home",
|
|
25
|
+
"samplePath": "/",
|
|
26
|
+
"match": ["="]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "plp",
|
|
30
|
+
"label": "Product listing",
|
|
31
|
+
"samplePath": "/collections/all",
|
|
32
|
+
"match": ["/collections"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "pdp",
|
|
36
|
+
"label": "Product detail",
|
|
37
|
+
"samplePath": "/products/",
|
|
38
|
+
"match": ["/products/"],
|
|
39
|
+
"resourceBudgets": {
|
|
40
|
+
"$comment": "PDP carries the largest hero + gallery imagery.",
|
|
41
|
+
"imageKib": 1100,
|
|
42
|
+
"totalKib": 1500
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "search",
|
|
47
|
+
"label": "Search results",
|
|
48
|
+
"samplePath": "/search?q=cap",
|
|
49
|
+
"match": ["/search"]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "account",
|
|
53
|
+
"label": "Account / dashboard",
|
|
54
|
+
"samplePath": "/dashboard",
|
|
55
|
+
"match": ["/dashboard", "/account", "/saved"]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "checkout",
|
|
59
|
+
"label": "Checkout entry",
|
|
60
|
+
"samplePath": "/cart",
|
|
61
|
+
"match": ["/cart", "/checkout"],
|
|
62
|
+
"resourceBudgets": {
|
|
63
|
+
"$comment": "The managed-checkout loader (cdn.foxycart.com) ships its own JS bundle; give this route headroom the on-site pages do not need.",
|
|
64
|
+
"jsKib": 320,
|
|
65
|
+
"totalKib": 1600
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F14 — Lighthouse result → budget measurement (the lab hook, pure half).
|
|
3
|
+
*
|
|
4
|
+
* The budget gate (`perfBudgets.ts`) evaluates a tool-neutral `RouteMeasurement`.
|
|
5
|
+
* This module is the one adapter that turns a Lighthouse result object (LHR JSON,
|
|
6
|
+
* as `lighthouse --output=json` / LHCI emit) into that shape, so a live
|
|
7
|
+
* Lighthouse run in preview validation feeds the SAME gate the unit tests do.
|
|
8
|
+
*
|
|
9
|
+
* Kept pure (LHR in → measurement out, no fs/network) so `lighthouseReport.test.ts`
|
|
10
|
+
* can prove the extraction against a fixture LHR without running Lighthouse; the
|
|
11
|
+
* thin CLI (`perf/assert-budgets.ts`) does the fs + Lighthouse invocation.
|
|
12
|
+
*
|
|
13
|
+
* We read only the stable, documented LHR fields:
|
|
14
|
+
* - `audits['largest-contentful-paint'].numericValue` (ms) → webVitals.lcpMs
|
|
15
|
+
* - `audits['cumulative-layout-shift'].numericValue` (unitless)→ webVitals.cls
|
|
16
|
+
* - `audits['resource-summary'].details.items[]` (bytes) → resource KiB
|
|
17
|
+
* - `audits['network-requests'].details.items[]` (url) → third-party origins
|
|
18
|
+
* INP is intentionally absent — it has no lab value; it is enforced from field
|
|
19
|
+
* RUM (`rumAlert.ts`), matching `toLighthouseBudgets`' note.
|
|
20
|
+
*
|
|
21
|
+
* Third-party origins are taken from `network-requests` (real request URLs →
|
|
22
|
+
* hostnames), NOT `third-party-summary` — the latter reports entity DISPLAY
|
|
23
|
+
* names ("Google Fonts"), which would never match the manifest's hostname
|
|
24
|
+
* allowlist. We keep hostnames that loaded a Script and are cross-origin to the
|
|
25
|
+
* page, so the gate's allowlist check compares like-for-like.
|
|
26
|
+
*/
|
|
27
|
+
import type { RouteId, RouteMeasurement } from "./perfBudgets.js";
|
|
28
|
+
|
|
29
|
+
/** The (loose) slice of a Lighthouse result we read. */
|
|
30
|
+
export interface LighthouseResult {
|
|
31
|
+
audits?: Record<
|
|
32
|
+
string,
|
|
33
|
+
{
|
|
34
|
+
numericValue?: number;
|
|
35
|
+
details?: {
|
|
36
|
+
items?: Array<Record<string, unknown>>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
>;
|
|
40
|
+
finalUrl?: string;
|
|
41
|
+
requestedUrl?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hostnameOf = (url: string | undefined): string | undefined => {
|
|
45
|
+
if (!url) return undefined;
|
|
46
|
+
try {
|
|
47
|
+
return new URL(url).hostname.toLowerCase();
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const BYTES_PER_KIB = 1024;
|
|
54
|
+
const toKib = (bytes: unknown): number | undefined =>
|
|
55
|
+
typeof bytes === "number" && Number.isFinite(bytes)
|
|
56
|
+
? Math.round((bytes / BYTES_PER_KIB) * 10) / 10
|
|
57
|
+
: undefined;
|
|
58
|
+
|
|
59
|
+
/** Map a Lighthouse `resource-summary` resourceType onto our budget keys. */
|
|
60
|
+
const RESOURCE_TYPE_KEY: Record<string, "jsKib" | "cssKib" | "imageKib" | "totalKib"> = {
|
|
61
|
+
script: "jsKib",
|
|
62
|
+
stylesheet: "cssKib",
|
|
63
|
+
image: "imageKib",
|
|
64
|
+
total: "totalKib",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract a `RouteMeasurement` for `routeId` from a Lighthouse result. Missing
|
|
69
|
+
* audits simply yield missing dimensions — the gate only checks what was
|
|
70
|
+
* measured — so a partial/failed audit under-reports rather than crashing.
|
|
71
|
+
*/
|
|
72
|
+
export function lhrToMeasurement(lhr: LighthouseResult, routeId: RouteId): RouteMeasurement {
|
|
73
|
+
const audits = lhr.audits ?? {};
|
|
74
|
+
|
|
75
|
+
const resources: NonNullable<RouteMeasurement["resources"]> = {};
|
|
76
|
+
const summaryItems = audits["resource-summary"]?.details?.items ?? [];
|
|
77
|
+
for (const item of summaryItems) {
|
|
78
|
+
const type = typeof item["resourceType"] === "string" ? (item["resourceType"] as string) : "";
|
|
79
|
+
const key = RESOURCE_TYPE_KEY[type];
|
|
80
|
+
const kib = toKib(item["transferSize"]);
|
|
81
|
+
if (key && kib !== undefined) resources[key] = kib;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const webVitals: NonNullable<RouteMeasurement["webVitals"]> = {};
|
|
85
|
+
const lcp = audits["largest-contentful-paint"]?.numericValue;
|
|
86
|
+
if (typeof lcp === "number") webVitals.lcpMs = Math.round(lcp);
|
|
87
|
+
const cls = audits["cumulative-layout-shift"]?.numericValue;
|
|
88
|
+
if (typeof cls === "number") webVitals.cls = Math.round(cls * 1e4) / 1e4;
|
|
89
|
+
|
|
90
|
+
// Third-party SCRIPT hostnames, cross-origin to the page, from real requests.
|
|
91
|
+
const pageHost = hostnameOf(lhr.finalUrl ?? lhr.requestedUrl);
|
|
92
|
+
const netItems = audits["network-requests"]?.details?.items ?? [];
|
|
93
|
+
const thirdPartyOrigins: string[] = [];
|
|
94
|
+
for (const item of netItems) {
|
|
95
|
+
const type = typeof item["resourceType"] === "string" ? (item["resourceType"] as string) : "";
|
|
96
|
+
if (type.toLowerCase() !== "script") continue;
|
|
97
|
+
const host = hostnameOf(typeof item["url"] === "string" ? (item["url"] as string) : undefined);
|
|
98
|
+
if (!host || host === pageHost) continue;
|
|
99
|
+
if (!thirdPartyOrigins.includes(host)) thirdPartyOrigins.push(host);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
routeId,
|
|
104
|
+
...(lhr.finalUrl || lhr.requestedUrl ? { url: lhr.finalUrl ?? lhr.requestedUrl } : {}),
|
|
105
|
+
...(Object.keys(resources).length ? { resources } : {}),
|
|
106
|
+
...(Object.keys(webVitals).length ? { webVitals } : {}),
|
|
107
|
+
...(thirdPartyOrigins.length ? { thirdPartyOrigins } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|