@tokenoftrust/storefront-runner 1.3.2
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/LICENSE +58 -0
- package/README.md +42 -0
- package/apps/storefront/astro.config.mjs +66 -0
- package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
- package/apps/storefront/env.d.ts +162 -0
- package/apps/storefront/migrations/0000_baseline.sql +54 -0
- package/apps/storefront/migrations/0001_products_fts.sql +9 -0
- package/apps/storefront/migrations/README.md +54 -0
- package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/_journal.json +20 -0
- package/apps/storefront/package.json +42 -0
- package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
- package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
- package/apps/storefront/public/brand/northwind-og.svg +13 -0
- package/apps/storefront/public/favicon.svg +5 -0
- package/apps/storefront/public/js/dashboard-team.js +147 -0
- package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
- package/apps/storefront/public/shared/commerce-chrome.css +369 -0
- package/apps/storefront/src/components/Badge.astro +34 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
- package/apps/storefront/src/components/CollectionCard.astro +56 -0
- package/apps/storefront/src/components/EmptyState.astro +55 -0
- package/apps/storefront/src/components/Img.astro +68 -0
- package/apps/storefront/src/components/ProductCard.astro +228 -0
- package/apps/storefront/src/components/Section.astro +39 -0
- package/apps/storefront/src/components/Seo.astro +70 -0
- package/apps/storefront/src/components/WidgetFrame.astro +34 -0
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
- package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
- package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
- package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
- package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
- package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
- package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
- package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
- package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
- package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
- package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
- package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
- package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
- package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
- package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
- package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
- package/apps/storefront/src/components/marketing/Faq.astro +37 -0
- package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
- package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
- package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
- package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
- package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
- package/apps/storefront/src/components/marketing/Steps.astro +36 -0
- package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
- package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
- package/apps/storefront/src/components/membership/TierCards.astro +164 -0
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
- package/apps/storefront/src/components/plp/Pagination.astro +99 -0
- package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
- package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
- package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
- package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
- package/apps/storefront/src/config/devTenantSeed.ts +78 -0
- package/apps/storefront/src/config/devTenants.ts +16 -0
- package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
- package/apps/storefront/src/config/resolver.ts +102 -0
- package/apps/storefront/src/config/tenants.ts +87 -0
- package/apps/storefront/src/layouts/Layout.astro +368 -0
- package/apps/storefront/src/lib/admin/README.md +122 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
- package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
- package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
- package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
- package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
- package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
- package/apps/storefront/src/lib/admin/envelope.ts +10 -0
- package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
- package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
- package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
- package/apps/storefront/src/lib/admin/seed.ts +54 -0
- package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
- package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
- package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
- package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
- package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
- package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
- package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
- package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
- package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
- package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
- package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
- package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
- package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
- package/apps/storefront/src/lib/analytics/index.ts +47 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
- package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
- package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
- package/apps/storefront/src/lib/auth/route.ts +146 -0
- package/apps/storefront/src/lib/auth/session.ts +203 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
- package/apps/storefront/src/lib/basePath.ts +41 -0
- package/apps/storefront/src/lib/cfImage.ts +55 -0
- package/apps/storefront/src/lib/chrome/model.ts +244 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
- package/apps/storefront/src/lib/content-edit/client.ts +230 -0
- package/apps/storefront/src/lib/content-edit/index.ts +14 -0
- package/apps/storefront/src/lib/content-edit/route.ts +25 -0
- package/apps/storefront/src/lib/content-edit/types.ts +19 -0
- package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
- package/apps/storefront/src/lib/d1/catalog.ts +366 -0
- package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
- package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
- package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
- package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
- package/apps/storefront/src/lib/demoGate.ts +113 -0
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
- package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
- package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
- package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
- package/apps/storefront/src/lib/diag/redact.ts +20 -0
- package/apps/storefront/src/lib/edgeCache.ts +128 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
- package/apps/storefront/src/lib/env.ts +60 -0
- package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
- package/apps/storefront/src/lib/format.ts +38 -0
- package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
- package/apps/storefront/src/lib/imageAttrs.ts +55 -0
- package/apps/storefront/src/lib/jsonld.ts +166 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/membership/model.ts +97 -0
- package/apps/storefront/src/lib/memoryKv.ts +32 -0
- package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
- package/apps/storefront/src/lib/placeholder.ts +99 -0
- package/apps/storefront/src/lib/previewReload.ts +125 -0
- package/apps/storefront/src/lib/priceBuckets.ts +33 -0
- package/apps/storefront/src/lib/rawChrome.ts +505 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
- package/apps/storefront/src/lib/search/index.ts +74 -0
- package/apps/storefront/src/lib/sections.ts +129 -0
- package/apps/storefront/src/lib/securityHeaders.ts +31 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
- package/apps/storefront/src/lib/subscription/model.ts +68 -0
- package/apps/storefront/src/lib/tenantRouting.ts +91 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
- package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
- package/apps/storefront/src/lib/tot/query.ts +249 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +142 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +455 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/admin.astro +1675 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
- package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
- package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
- package/apps/storefront/src/pages/api/auth/send.ts +85 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
- package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
- package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
- package/apps/storefront/src/pages/api/csp-report.ts +25 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
- package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
- package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
- package/apps/storefront/src/pages/api/dev/file.ts +57 -0
- package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
- package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
- package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
- package/apps/storefront/src/pages/api/dev/write.ts +116 -0
- package/apps/storefront/src/pages/api/newsletter.ts +104 -0
- package/apps/storefront/src/pages/api/request-access.ts +172 -0
- package/apps/storefront/src/pages/auth/login.astro +149 -0
- package/apps/storefront/src/pages/auth/magic.astro +105 -0
- package/apps/storefront/src/pages/capabilities.astro +292 -0
- package/apps/storefront/src/pages/collections/[handle].astro +177 -0
- package/apps/storefront/src/pages/collections/index.astro +59 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
- package/apps/storefront/src/pages/dashboard/index.astro +160 -0
- package/apps/storefront/src/pages/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +402 -0
- package/apps/storefront/src/pages/robots.txt.ts +24 -0
- package/apps/storefront/src/pages/saved.astro +35 -0
- package/apps/storefront/src/pages/search.astro +150 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
- package/apps/storefront/src/pages/style-guide/index.astro +98 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +292 -0
- package/apps/storefront/src/themes/reference.ts +68 -0
- package/apps/storefront/src/themes/schema.ts +111 -0
- package/apps/storefront/src/themes/tenants/index.ts +51 -0
- package/apps/storefront/tsconfig.json +16 -0
- package/apps/storefront/vitest.config.ts +22 -0
- package/package.json +26 -0
- package/packages/public-runtime/LICENSE +58 -0
- package/packages/public-runtime/package.json +22 -0
- package/packages/public-runtime/src/binary-path.ts +68 -0
- package/packages/public-runtime/src/capabilities.ts +89 -0
- package/packages/public-runtime/src/catalog-api.ts +81 -0
- package/packages/public-runtime/src/catalog-d1.ts +192 -0
- package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
- package/packages/public-runtime/src/checkout-style.ts +375 -0
- package/packages/public-runtime/src/csp.ts +109 -0
- package/packages/public-runtime/src/customization-preview.ts +197 -0
- package/packages/public-runtime/src/customization-reconcile.ts +63 -0
- package/packages/public-runtime/src/customization-runtime.ts +157 -0
- package/packages/public-runtime/src/customization-scripts.ts +64 -0
- package/packages/public-runtime/src/customization-versioning.ts +95 -0
- package/packages/public-runtime/src/foxy-orders.ts +136 -0
- package/packages/public-runtime/src/foxy.ts +196 -0
- package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
- package/packages/public-runtime/src/index.ts +34 -0
- package/packages/public-runtime/src/product.ts +159 -0
- package/packages/public-runtime/src/search.ts +45 -0
- package/packages/public-runtime/src/tenant-assets.ts +444 -0
- package/packages/public-runtime/src/tenant.ts +384 -0
- package/packages/public-runtime/tsconfig.json +8 -0
- package/packages/public-runtime/vitest.config.ts +8 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build/copy-tenant-assets.mjs +69 -0
- package/scripts/dev/ai-edit.mjs +183 -0
- package/scripts/dev/checkout-watch.mjs +116 -0
- package/scripts/dev/file-browser.mjs +159 -0
- package/scripts/dev/file-writer.mjs +141 -0
- package/scripts/dev/git-status.mjs +171 -0
- package/scripts/dev/locate-handler.mjs +163 -0
- package/scripts/dev/locate.mjs +112 -0
- package/scripts/dev/port-check.mjs +48 -0
- package/scripts/dev/safe-path.mjs +109 -0
- package/scripts/dev/shot.mjs +46 -0
- package/scripts/dev/tenant-source-root.mjs +76 -0
- package/scripts/dev/unified-diff.mjs +81 -0
- package/scripts/tot-dev.mjs +376 -0
- package/tenants/README.md +14 -0
- package/tenants/home/content/chrome.json +7 -0
- package/tenants/home/content/home.html +58 -0
- package/tenants/home/content/home.json +11 -0
- package/tenants/home/public/js/storefront.js +210 -0
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +212 -0
- package/tenants/home/theme.json +6 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline verification of tot's identity assertion (`id_token`) — the consuming
|
|
3
|
+
* half of the decoupled-authZ architecture (Phase 3).
|
|
4
|
+
*
|
|
5
|
+
* tot signs a short-lived ES256 assertion ONCE at sign-in with its PRIVATE key
|
|
6
|
+
* (`/api/internal/access-code/verify` → `id_token`). This module verifies it
|
|
7
|
+
* OFFLINE using tot's published JWKS (fetched once, cached, key-selected by
|
|
8
|
+
* `kid`) and derives the storefront session from the VERIFIED claims — no shared
|
|
9
|
+
* secret, no per-request callback to tot. Removing tot's network reachability
|
|
10
|
+
* after sign-in must not break authorization; the token's own `exp` bounds
|
|
11
|
+
* staleness.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors the Phase-2 launchpad kernel's lightweight offline verifier
|
|
14
|
+
* (`launchpad src/modules/authz/jwks-verifier.ts` + `identity-claims.ts`) and the
|
|
15
|
+
* tot-internal verify path proven in
|
|
16
|
+
* `tot20 services/InternalAuthService/identityTokenService.jest.js`.
|
|
17
|
+
*
|
|
18
|
+
* PROTOTYPE SCOPE: this replicates ONLY the JWKS offline-verify + a coarse
|
|
19
|
+
* claims→session derivation. The full Cedar PDP (`can()` policy engine, staff
|
|
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
|
|
27
|
+
* cross-version 4↔6 is wire-compatible by construction).
|
|
28
|
+
*/
|
|
29
|
+
import {
|
|
30
|
+
jwtVerify,
|
|
31
|
+
createRemoteJWKSet,
|
|
32
|
+
createLocalJWKSet,
|
|
33
|
+
decodeProtectedHeader,
|
|
34
|
+
type JWTVerifyGetKey,
|
|
35
|
+
type JSONWebKeySet,
|
|
36
|
+
} from "jose";
|
|
37
|
+
import { readEnv } from "@/lib/env";
|
|
38
|
+
|
|
39
|
+
/** tot's issuer — consumers pin/allowlist this (`auth.jwt.issuer`). */
|
|
40
|
+
export const TOT_ISSUER = "tokenoftrust-internal-ops";
|
|
41
|
+
|
|
42
|
+
/** The only signing algorithm tot uses. Consumers MUST reject anything else. */
|
|
43
|
+
export const TOT_ALG = "ES256";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* JWKS path (host is per-env app config — §6b). Contract also serves
|
|
47
|
+
* `/api/internal/auth/jwks`; the well-known path is the canonical one.
|
|
48
|
+
*/
|
|
49
|
+
export const JWKS_PATH = "/api/.well-known/jwks.json";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Coarse tenant role, collapsed by tot from its per-tenant capability
|
|
53
|
+
* (`developer`/`inviter`/none → `member`). Org standing only; finer granularity
|
|
54
|
+
* is the storefront's own job to derive locally (Phase-4 PDP work).
|
|
55
|
+
*/
|
|
56
|
+
export type CoarseRole = "owner" | "admin" | "member";
|
|
57
|
+
|
|
58
|
+
/** A single tenant membership carried in the assertion. */
|
|
59
|
+
export interface TenantMembership {
|
|
60
|
+
resource: string;
|
|
61
|
+
role: CoarseRole;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The verified identity assertion payload (tot-authoritative + registered
|
|
66
|
+
* claims). Everything here is an INPUT to the storefront's policy, not a
|
|
67
|
+
* decision. Mirrors the launchpad kernel's `IdentityClaims`.
|
|
68
|
+
*/
|
|
69
|
+
export interface IdentityClaims {
|
|
70
|
+
// Registered claims
|
|
71
|
+
iss: string;
|
|
72
|
+
sub: string;
|
|
73
|
+
aud: string;
|
|
74
|
+
iat: number;
|
|
75
|
+
exp: number;
|
|
76
|
+
jti?: string;
|
|
77
|
+
// tot-authoritative claims
|
|
78
|
+
email: string;
|
|
79
|
+
/** Baseline verification signal today (email control proven at sign-in). */
|
|
80
|
+
email_verified: boolean;
|
|
81
|
+
tenants: TenantMembership[];
|
|
82
|
+
/**
|
|
83
|
+
* ToT-staff cross-domain scope (present for ToT staff). CARRIED as a principal
|
|
84
|
+
* attribute; the coarse gate grants NOTHING from it — staff must never elevate
|
|
85
|
+
* app privileges (enforced by the Phase-4 PDP staff sandbox).
|
|
86
|
+
*/
|
|
87
|
+
staff: string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Coarse-role collapse guard: anything tot didn't send as owner/admin → member. */
|
|
91
|
+
function coerceRole(role: unknown): CoarseRole {
|
|
92
|
+
return role === "owner" || role === "admin" ? role : "member";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Normalize a jose JWT payload into typed {@link IdentityClaims}, coercing the
|
|
97
|
+
* loosely-typed pieces and filling contract defaults (`staff: []`). Throws if a
|
|
98
|
+
* required tot-authoritative claim is missing or malformed. Mirrors the kernel's
|
|
99
|
+
* `toIdentityClaims`.
|
|
100
|
+
*/
|
|
101
|
+
export function toIdentityClaims(payload: Record<string, unknown>): IdentityClaims {
|
|
102
|
+
const email = payload.email;
|
|
103
|
+
if (typeof email !== "string" || !email) {
|
|
104
|
+
throw new Error("identity assertion missing `email` claim");
|
|
105
|
+
}
|
|
106
|
+
const tenantsRaw = Array.isArray(payload.tenants) ? payload.tenants : [];
|
|
107
|
+
const tenants: TenantMembership[] = tenantsRaw.map((t): TenantMembership => {
|
|
108
|
+
const entry = t as { resource?: unknown; role?: unknown };
|
|
109
|
+
if (typeof entry.resource !== "string") {
|
|
110
|
+
throw new Error("identity assertion `tenants[].resource` must be a string");
|
|
111
|
+
}
|
|
112
|
+
return { resource: entry.resource, role: coerceRole(entry.role) };
|
|
113
|
+
});
|
|
114
|
+
const staff = Array.isArray(payload.staff)
|
|
115
|
+
? payload.staff.filter((s): s is string => typeof s === "string")
|
|
116
|
+
: [];
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
iss: String(payload.iss ?? ""),
|
|
120
|
+
sub: String(payload.sub ?? email),
|
|
121
|
+
aud: String(payload.aud ?? ""),
|
|
122
|
+
iat: Number(payload.iat ?? 0),
|
|
123
|
+
exp: Number(payload.exp ?? 0),
|
|
124
|
+
jti: typeof payload.jti === "string" ? payload.jti : undefined,
|
|
125
|
+
email: email.toLowerCase(),
|
|
126
|
+
email_verified: payload.email_verified === true,
|
|
127
|
+
tenants,
|
|
128
|
+
staff,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Derive tot's JWKS URL from the configured tot API base (§6b: path fixed, host app-config). */
|
|
133
|
+
export function deriveJwksUri(baseUrl: string): string {
|
|
134
|
+
return `${baseUrl.replace(/\/+$/, "")}${JWKS_PATH}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IdentityVerifierConfig {
|
|
138
|
+
/**
|
|
139
|
+
* This host's tenant appDomain / `resource` — enforced as the `aud` claim (tot
|
|
140
|
+
* sets `aud` per-assertion to the target app). REQUIRED: a token minted for
|
|
141
|
+
* another tenant must be rejected.
|
|
142
|
+
*/
|
|
143
|
+
audience: string;
|
|
144
|
+
/** Allowlisted issuer. Defaults to tot's. */
|
|
145
|
+
issuer?: string;
|
|
146
|
+
/**
|
|
147
|
+
* tot's JWKS endpoint, e.g. `https://qa.tokenoftrust.com/api/.well-known/jwks.json`.
|
|
148
|
+
* Provide in production. jose caches the fetch, selects the key by `kid`, and
|
|
149
|
+
* re-fetches once on an unknown `kid` before failing closed.
|
|
150
|
+
*/
|
|
151
|
+
jwksUri?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Pre-resolved JWKS (test / air-gapped path). The storefront holds ONLY the
|
|
154
|
+
* published public material — no private key, no secret. Exactly one of
|
|
155
|
+
* {@link jwksUri} or {@link jwks} must be supplied.
|
|
156
|
+
*/
|
|
157
|
+
jwks?: JSONWebKeySet;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verifies tot identity assertions offline against the published JWKS. One
|
|
162
|
+
* instance per (audience, JWKS host); its `audience` is fixed. Never calls tot
|
|
163
|
+
* per request — the JWKS fetch is cached inside jose's key resolver, so once the
|
|
164
|
+
* key material is in-process, verification survives tot being unreachable.
|
|
165
|
+
*/
|
|
166
|
+
export class IdentityAssertionVerifier {
|
|
167
|
+
private readonly audience: string;
|
|
168
|
+
private readonly issuer: string;
|
|
169
|
+
private readonly keys: JWTVerifyGetKey;
|
|
170
|
+
|
|
171
|
+
constructor(config: IdentityVerifierConfig) {
|
|
172
|
+
if (!config.audience) {
|
|
173
|
+
throw new Error("IdentityAssertionVerifier requires an `audience` (this host's resource)");
|
|
174
|
+
}
|
|
175
|
+
if (!!config.jwksUri === !!config.jwks) {
|
|
176
|
+
throw new Error("IdentityAssertionVerifier requires exactly one of `jwksUri` or `jwks`");
|
|
177
|
+
}
|
|
178
|
+
this.audience = config.audience;
|
|
179
|
+
this.issuer = config.issuer ?? TOT_ISSUER;
|
|
180
|
+
this.keys = config.jwks
|
|
181
|
+
? createLocalJWKSet(config.jwks)
|
|
182
|
+
: createRemoteJWKSet(new URL(config.jwksUri as string));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Verify a compact JWS and return typed claims. Rejects (throws) on: any
|
|
187
|
+
* `alg` ≠ ES256 (blocks `alg:none` and every HS*), bad signature, wrong/absent
|
|
188
|
+
* `aud`, unlisted `iss`, or expired/not-yet-valid `exp`/`iat`.
|
|
189
|
+
*/
|
|
190
|
+
async verify(token: string): Promise<IdentityClaims> {
|
|
191
|
+
// Fail fast + loud on the algorithm before touching keys. jose's `algorithms`
|
|
192
|
+
// allowlist below is the authoritative guard; this makes the reason explicit.
|
|
193
|
+
const header = decodeProtectedHeader(token);
|
|
194
|
+
if (header.alg !== TOT_ALG) {
|
|
195
|
+
throw new Error(`unexpected JWT alg ${String(header.alg)}; only ${TOT_ALG} is accepted`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { payload } = await jwtVerify(token, this.keys, {
|
|
199
|
+
issuer: this.issuer,
|
|
200
|
+
audience: this.audience,
|
|
201
|
+
algorithms: [TOT_ALG],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return toIdentityClaims(payload as Record<string, unknown>);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --- Verifier cache -----------------------------------------------------------
|
|
209
|
+
//
|
|
210
|
+
// In-memory, module-scoped cache keyed by (jwksUri | audience). Good enough for
|
|
211
|
+
// the prototype: each Worker isolate builds its verifier once and reuses jose's
|
|
212
|
+
// cached JWKS across requests. FOLLOW-UP (multi-instance): promote the JWKS cache
|
|
213
|
+
// to KV or a Durable Object so isolates share fetched key material and honour a
|
|
214
|
+
// single rotation signal. Tracked with the Phase-4 kernel adoption.
|
|
215
|
+
const verifierCache = new Map<string, IdentityAssertionVerifier>();
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get (or build + cache) the verifier for a resource, deriving the JWKS URL from
|
|
219
|
+
* the configured tot API base (`TOT_CORE_URL` — the same base `totAccessClient`
|
|
220
|
+
* posts the access-code endpoints to). Returns `null` when tot's base URL is not
|
|
221
|
+
* configured (routes fail closed). Pass `overrides.jwks`/`overrides.jwksUri` in
|
|
222
|
+
* tests to inject key material without a network fetch.
|
|
223
|
+
*/
|
|
224
|
+
export async function getIdentityVerifier(
|
|
225
|
+
resource: string,
|
|
226
|
+
overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string } = {},
|
|
227
|
+
): Promise<IdentityAssertionVerifier | null> {
|
|
228
|
+
if (overrides.jwks) {
|
|
229
|
+
// Test / air-gapped: never cached (keys are per-test) and never networked.
|
|
230
|
+
return new IdentityAssertionVerifier({ audience: resource, jwks: overrides.jwks });
|
|
231
|
+
}
|
|
232
|
+
const jwksUri =
|
|
233
|
+
overrides.jwksUri ??
|
|
234
|
+
deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
|
|
235
|
+
if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
|
|
236
|
+
|
|
237
|
+
const cacheKey = `${jwksUri}|${resource}`;
|
|
238
|
+
let v = verifierCache.get(cacheKey);
|
|
239
|
+
if (!v) {
|
|
240
|
+
v = new IdentityAssertionVerifier({ audience: resource, jwksUri });
|
|
241
|
+
verifierCache.set(cacheKey, v);
|
|
242
|
+
}
|
|
243
|
+
return v;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get (or build + cache) the FEDERATED verifier — the one for multi-tenant
|
|
248
|
+
* sign-in. Its audience is the STOREFRONT'S OWN app identity (`STOREFRONT_APP_DOMAIN`),
|
|
249
|
+
* because tot20's federated `id_token` sets `aud` = the calling app (storefront),
|
|
250
|
+
* NOT a tenant. The token then carries ALL of the email's memberships in
|
|
251
|
+
* `tenants[]`. Returns null when the tot base URL (`TOT_CORE_URL`) or the
|
|
252
|
+
* storefront app identity (`STOREFRONT_APP_DOMAIN`) is unconfigured — the route
|
|
253
|
+
* fails closed. Pass overrides in tests to inject key material + audience.
|
|
254
|
+
*/
|
|
255
|
+
export async function getFederatedVerifier(
|
|
256
|
+
overrides: { jwksUri?: string; jwks?: JSONWebKeySet; baseUrl?: string; audience?: string } = {},
|
|
257
|
+
): Promise<IdentityAssertionVerifier | null> {
|
|
258
|
+
const audience = overrides.audience ?? (await readEnv("STOREFRONT_APP_DOMAIN")) ?? "";
|
|
259
|
+
if (!audience) return null; // storefront's own app identity must be configured
|
|
260
|
+
if (overrides.jwks) {
|
|
261
|
+
return new IdentityAssertionVerifier({ audience, jwks: overrides.jwks });
|
|
262
|
+
}
|
|
263
|
+
const jwksUri =
|
|
264
|
+
overrides.jwksUri ??
|
|
265
|
+
deriveJwksUri(overrides.baseUrl ?? (await readEnv("TOT_CORE_URL")) ?? "");
|
|
266
|
+
if (jwksUri === JWKS_PATH) return null; // empty base → unconfigured
|
|
267
|
+
const cacheKey = `${jwksUri}|${audience}`;
|
|
268
|
+
let v = verifierCache.get(cacheKey);
|
|
269
|
+
if (!v) {
|
|
270
|
+
v = new IdentityAssertionVerifier({ audience, jwksUri });
|
|
271
|
+
verifierCache.set(cacheKey, v);
|
|
272
|
+
}
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- Session derivation from verified claims ---------------------------------
|
|
277
|
+
|
|
278
|
+
/** The session fields derived from the VERIFIED token — the Phase-3 source of truth. */
|
|
279
|
+
export interface DerivedSessionFields {
|
|
280
|
+
email: string;
|
|
281
|
+
resource: string;
|
|
282
|
+
/** Coarse standing on THIS resource, surfaced as the display capability. */
|
|
283
|
+
capability: CoarseRole | "none";
|
|
284
|
+
/** Display roles derived from the coarse claim (not tot's raw app roles). */
|
|
285
|
+
roles: string[];
|
|
286
|
+
/** Full multi-tenant memberships carried in the token (policy input). */
|
|
287
|
+
tenants: TenantMembership[];
|
|
288
|
+
/** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
|
|
289
|
+
staff: string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Derive the storefront session from VERIFIED claims for a given resource — the
|
|
294
|
+
* Phase-3 replacement for trusting tot's flat `{roles, capability}`. Picks the
|
|
295
|
+
* membership matching `resource` and collapses its coarse role to the display
|
|
296
|
+
* capability. Throws if the verified token carries no membership for `resource`
|
|
297
|
+
* (a token for another tenant must not open this gate — belt-and-braces on top
|
|
298
|
+
* of the `aud` check).
|
|
299
|
+
*/
|
|
300
|
+
export function sessionFromClaims(claims: IdentityClaims, resource: string): DerivedSessionFields {
|
|
301
|
+
const membership = claims.tenants.find((t) => t.resource === resource);
|
|
302
|
+
if (!membership) {
|
|
303
|
+
throw new Error(`verified assertion carries no membership for resource "${resource}"`);
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
email: claims.email,
|
|
307
|
+
resource,
|
|
308
|
+
capability: membership.role,
|
|
309
|
+
roles: [membership.role],
|
|
310
|
+
tenants: claims.tenants,
|
|
311
|
+
staff: claims.staff,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The multi-tenant fields derived from a verified FEDERATED assertion. */
|
|
316
|
+
export interface FederatedSessionFields {
|
|
317
|
+
email: string;
|
|
318
|
+
/** The verified `aud` — the storefront app identity this console session is for. */
|
|
319
|
+
resource: string;
|
|
320
|
+
/** EVERY tenant membership the token carries (the dashboard picker's input). */
|
|
321
|
+
tenants: TenantMembership[];
|
|
322
|
+
/** ToT-staff scope carried in the token (sandboxed; grants nothing here). */
|
|
323
|
+
staff: string[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Derive the storefront CONSOLE session from a verified FEDERATED assertion. Unlike
|
|
328
|
+
* {@link sessionFromClaims}, this does NOT require (or scope to) a single tenant:
|
|
329
|
+
* it binds ALL of `claims.tenants` so a user with N memberships yields an N-vendor
|
|
330
|
+
* session. `resource` is the verified `aud` (the storefront app identity), not a
|
|
331
|
+
* tenant — it exists only to satisfy the session record shape; the dashboard reads
|
|
332
|
+
* `tenants[]`, and the tenant-scoped preview gate never matches it.
|
|
333
|
+
*/
|
|
334
|
+
export function sessionFromFederatedClaims(claims: IdentityClaims): FederatedSessionFields {
|
|
335
|
+
return {
|
|
336
|
+
email: claims.email,
|
|
337
|
+
resource: claims.aud,
|
|
338
|
+
tenants: claims.tenants,
|
|
339
|
+
staff: claims.staff,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login-gate decision (pure) — the "Sign in with ToT" enforcement mode for a
|
|
3
|
+
* gated demo host, the eventual replacement for the interim Basic-Auth password
|
|
4
|
+
* (see `@/lib/demoGate`). Same `DEMO_GATE_HOSTS` list, switched by `DEMO_GATE_MODE`:
|
|
5
|
+
*
|
|
6
|
+
* DEMO_GATE_MODE = "password" → interim Basic-Auth gate (evaluateDemoGate)
|
|
7
|
+
* DEMO_GATE_MODE = "login" → this: redirect to /auth/login unless a valid,
|
|
8
|
+
* resource-bound tot_session cookie is present.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE (no KV, no cookies, no env I/O): the middleware reads the
|
|
11
|
+
* session (KV) and passes the resolved record in. Keeping the decision pure makes
|
|
12
|
+
* the exemption + redirect logic unit-testable and keeps the two gate modes from
|
|
13
|
+
* drifting apart. The auth surface itself (/auth/*, /api/auth/*) is always exempt
|
|
14
|
+
* so a visitor can actually sign in.
|
|
15
|
+
*/
|
|
16
|
+
import { hostInGateList } from "@/lib/demoGate";
|
|
17
|
+
import { viewerCanAccess } from "@/lib/dashboard/tenantSelection";
|
|
18
|
+
import type { SessionRecord } from "./session.js";
|
|
19
|
+
|
|
20
|
+
export type GateMode = "password" | "login";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The platform's own "self" home tenant (tenants.ts `HOME_TENANT`, registry
|
|
24
|
+
* default). It is NEVER login-gated — `storefront.tokenoftrust.store` renders its
|
|
25
|
+
* public home so people can browse + sign up. Every OTHER tenant on a gated host
|
|
26
|
+
* (path-prefix or per-tenant subdomain) is gated behind sign-in.
|
|
27
|
+
*/
|
|
28
|
+
export const PUBLIC_HOME_TENANT_ID = "home";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether a (federated) viewer session admits the gated HOST — i.e. the signed-in
|
|
32
|
+
* user holds a membership for the host's tenant (`hostResource`, the host's
|
|
33
|
+
* appDomain). This UNIFIES the preview-host gate onto the single federated session:
|
|
34
|
+
* federated sign-in mints a multi-tenant session whose `aud`/`resource` is the
|
|
35
|
+
* storefront app (not a tenant), so the old resource-scoped session match never
|
|
36
|
+
* fit; membership in `tenants[]` is the right test. Deny-closed: no session, or a
|
|
37
|
+
* session without a membership for `hostResource`, is not admitted. (Backward-
|
|
38
|
+
* compatible with a legacy resource-scoped session, whose `tenants[]` still lists
|
|
39
|
+
* its own resource.)
|
|
40
|
+
*/
|
|
41
|
+
export function sessionAdmitsHost(
|
|
42
|
+
record: Pick<SessionRecord, "tenants" | "staffSelection" | "staff"> | null,
|
|
43
|
+
hostResource: string,
|
|
44
|
+
nowSeconds: number = Math.floor(Date.now() / 1000),
|
|
45
|
+
): boolean {
|
|
46
|
+
if (!record) return false;
|
|
47
|
+
// ToT STAFF get in everywhere: any session carrying a verified `staff` scope is
|
|
48
|
+
// admitted to EVERY gated demo host with just a login — no per-vendor selection
|
|
49
|
+
// needed. This is a GATE-level admission for demo BROWSING only; dashboard-action
|
|
50
|
+
// authorization (viewerCanAccess) stays membership/selection-bound + audited.
|
|
51
|
+
if (Array.isArray(record.staff) && record.staff.length > 0) return true;
|
|
52
|
+
// Otherwise: a membership OR a live ToT-staff selection for this exact host admits
|
|
53
|
+
// — the staff-selection path is distinct from tenants[] (never a synthesized member).
|
|
54
|
+
return viewerCanAccess(record.tenants, hostResource, {
|
|
55
|
+
selection: record.staffSelection,
|
|
56
|
+
nowSeconds,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse `DEMO_GATE_MODE` (default "password"). */
|
|
61
|
+
export function parseGateMode(raw: string | null | undefined): GateMode {
|
|
62
|
+
return (raw ?? "").trim().toLowerCase() === "login" ? "login" : "password";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Paths that MUST stay reachable without a session, or the visitor could never
|
|
67
|
+
* authenticate: the login page and the send/verify endpoints. Everything else on
|
|
68
|
+
* a login-gated host requires a session.
|
|
69
|
+
*/
|
|
70
|
+
export function isAuthPath(pathname: string): boolean {
|
|
71
|
+
return pathname === "/auth/login" || pathname.startsWith("/api/auth/");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Genuinely-public API paths that must answer without a session even on a
|
|
76
|
+
* login-gated host — the CLI version-support policy is fetched by an
|
|
77
|
+
* unauthenticated `tot` install (src/pages/api/cli/policy.ts). Kept exact (not a
|
|
78
|
+
* broad `/api/cli/` prefix) so a future authed CLI endpoint isn't accidentally
|
|
79
|
+
* exposed.
|
|
80
|
+
*/
|
|
81
|
+
export function isPublicPath(pathname: string): boolean {
|
|
82
|
+
return pathname === "/api/cli/policy";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type LoginGateDecision =
|
|
86
|
+
| { action: "pass" }
|
|
87
|
+
| { action: "redirect"; location: string };
|
|
88
|
+
|
|
89
|
+
export interface LoginGateInput {
|
|
90
|
+
host: string;
|
|
91
|
+
pathname: string;
|
|
92
|
+
search: string;
|
|
93
|
+
mode: GateMode;
|
|
94
|
+
/** The gated-host list (`DEMO_GATE_HOSTS`). */
|
|
95
|
+
gatedHosts: string | null | undefined;
|
|
96
|
+
/** True when a valid, resource-bound session was resolved for this request. */
|
|
97
|
+
hasValidSession: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* True when the resolved tenant is the platform's own public home (self) —
|
|
100
|
+
* exempt from the gate so it renders publicly. See {@link PUBLIC_HOME_TENANT_ID}.
|
|
101
|
+
*/
|
|
102
|
+
tenantExempt?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decide what the login gate should do. Returns `pass` when the host isn't
|
|
107
|
+
* login-gated, the path is an auth path, or a valid session exists; otherwise a
|
|
108
|
+
* `redirect` to /auth/login carrying the intended destination as `next`.
|
|
109
|
+
*/
|
|
110
|
+
export function evaluateLoginGate(input: LoginGateInput): LoginGateDecision {
|
|
111
|
+
if (input.mode !== "login") return { action: "pass" };
|
|
112
|
+
if (!hostInGateList(input.host, input.gatedHosts)) return { action: "pass" };
|
|
113
|
+
if (input.tenantExempt) return { action: "pass" };
|
|
114
|
+
if (isAuthPath(input.pathname)) return { action: "pass" };
|
|
115
|
+
if (isPublicPath(input.pathname)) return { action: "pass" };
|
|
116
|
+
if (input.hasValidSession) return { action: "pass" };
|
|
117
|
+
const next = input.pathname + (input.search ?? "");
|
|
118
|
+
return { action: "redirect", location: `/auth/login?next=${encodeURIComponent(next)}` };
|
|
119
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the `/auth/*` + `/api/auth/*` sign-in surface. Kept out of
|
|
3
|
+
* the pure session/client modules so those stay transport- and Astro-free and
|
|
4
|
+
* unit-test against the in-memory impls.
|
|
5
|
+
*
|
|
6
|
+
* The flow (no-JS-friendly, all on the tenant's own branded host):
|
|
7
|
+
* GET /auth/login → email card (step 1) / code card (step 2)
|
|
8
|
+
* POST /api/auth/send {email} → tot20 sendAccessCode → 303 back to step 2
|
|
9
|
+
* POST /api/auth/verify{email,code}→ tot20 verifyAccessCode → session cookie → 303 next
|
|
10
|
+
*
|
|
11
|
+
* The gate ("login" mode) redirects unauthenticated viewers of a gated host to
|
|
12
|
+
* /auth/login and lets a valid `tot_session` cookie back through. The `resource`
|
|
13
|
+
* (the tot20 appDomain the grant is checked against) comes from the resolved
|
|
14
|
+
* tenant — never from request input.
|
|
15
|
+
*/
|
|
16
|
+
import type { APIContext } from "astro";
|
|
17
|
+
import { readKv } from "@/lib/env";
|
|
18
|
+
import { createTotAccessClient, type TotAccessClient } from "./totAccessClient.js";
|
|
19
|
+
import {
|
|
20
|
+
KvSessionStore,
|
|
21
|
+
readSession,
|
|
22
|
+
type SessionRecord,
|
|
23
|
+
type SessionStore,
|
|
24
|
+
SESSION_COOKIE,
|
|
25
|
+
} from "./session.js";
|
|
26
|
+
import {
|
|
27
|
+
getIdentityVerifier as buildIdentityVerifier,
|
|
28
|
+
type IdentityAssertionVerifier,
|
|
29
|
+
} from "./identityToken.js";
|
|
30
|
+
|
|
31
|
+
/** Permissive email shape — tot20 is the real authority on eligibility. */
|
|
32
|
+
export const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The tot20 `resource` (appDomain) the email's roles are checked against for this
|
|
36
|
+
* host. Derived from the resolved tenant, never from input.
|
|
37
|
+
*
|
|
38
|
+
* INTEGRATION NOTE: which tenant field equals tot20's appDomain for a store is
|
|
39
|
+
* confirmed at integration (task #4). We use `appDomain` (the tenant's app
|
|
40
|
+
* identity) and allow a per-tenant override via `commerce`/config later.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveResource(context: APIContext): string | null {
|
|
43
|
+
const t = context.locals.tenant;
|
|
44
|
+
if (!t) return null;
|
|
45
|
+
return t.appDomain || t.tenant_id || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The access client from env, or null when the integration isn't configured. */
|
|
49
|
+
export async function getAccessClient(): Promise<TotAccessClient | null> {
|
|
50
|
+
try {
|
|
51
|
+
return await createTotAccessClient();
|
|
52
|
+
} catch {
|
|
53
|
+
return null; // unconfigured → routes return 503
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** KV-backed session store, or null in runtimes without the SESSION binding (dev). */
|
|
58
|
+
export async function getSessionStore(): Promise<SessionStore | null> {
|
|
59
|
+
const kv = await readKv("SESSION");
|
|
60
|
+
return kv ? new KvSessionStore(kv) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the signed-in viewer's FULL session (email + all tenant memberships), or
|
|
65
|
+
* null when there's no valid session. Unlike the gate's read, this is NOT scoped
|
|
66
|
+
* to a single host `resource` — the vendor dashboard needs every membership in
|
|
67
|
+
* `tenants[]` to route (single → in, multiple → picker) and to authorize actions
|
|
68
|
+
* against the SELECTED vendor. Returns null in runtimes without the SESSION KV.
|
|
69
|
+
*/
|
|
70
|
+
export async function readViewerSession(
|
|
71
|
+
context: { cookies: APIContext["cookies"] },
|
|
72
|
+
): Promise<SessionRecord | null> {
|
|
73
|
+
const store = await getSessionStore();
|
|
74
|
+
if (!store) return null;
|
|
75
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
76
|
+
return readSession(store, sid, Math.floor(Date.now() / 1000));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The session store + the current request's opaque session id, for callers that
|
|
81
|
+
* need to MUTATE the stored record (e.g. stamping a staff vendor selection). Null
|
|
82
|
+
* when there's no SESSION KV (dev) or no session cookie. Kept alongside
|
|
83
|
+
* {@link readViewerSession} so mutation and read share the same store/cookie seam.
|
|
84
|
+
*/
|
|
85
|
+
export async function getSessionMutationHandle(
|
|
86
|
+
context: { cookies: APIContext["cookies"] },
|
|
87
|
+
): Promise<{ store: SessionStore; id: string } | null> {
|
|
88
|
+
const store = await getSessionStore();
|
|
89
|
+
if (!store) return null;
|
|
90
|
+
const id = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
91
|
+
if (!id) return null;
|
|
92
|
+
return { store, id };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The offline `id_token` verifier for this host's `resource`, or null when the
|
|
97
|
+
* tot JWKS host (`TOT_CORE_URL`) isn't configured. Derives the JWKS URL from the
|
|
98
|
+
* same tot API base the access-code client posts to; jose caches the fetch so
|
|
99
|
+
* verification survives tot being unreachable after sign-in (Phase-3 property).
|
|
100
|
+
*/
|
|
101
|
+
export async function getVerifier(resource: string): Promise<IdentityAssertionVerifier | null> {
|
|
102
|
+
return buildIdentityVerifier(resource);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { getVerifier as getIdentityVerifier };
|
|
106
|
+
|
|
107
|
+
/** Set the session cookie (HttpOnly/Secure/SameSite=Lax) for `maxAgeSeconds`. */
|
|
108
|
+
export function setSessionCookie(context: APIContext, id: string, maxAgeSeconds: number): void {
|
|
109
|
+
context.cookies.set(SESSION_COOKIE, id, {
|
|
110
|
+
httpOnly: true,
|
|
111
|
+
secure: true,
|
|
112
|
+
sameSite: "lax",
|
|
113
|
+
path: "/",
|
|
114
|
+
maxAge: maxAgeSeconds,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Clear the session cookie (logout). */
|
|
119
|
+
export function clearSessionCookie(context: APIContext): void {
|
|
120
|
+
context.cookies.delete(SESSION_COOKIE, { path: "/" });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Sanitize a `next` redirect target to a same-origin PATH only (starts with a
|
|
125
|
+
* single "/", not "//" or a scheme) — closes the open-redirect hole. Falls back
|
|
126
|
+
* to "/".
|
|
127
|
+
*/
|
|
128
|
+
export function safeNext(next: string | null | undefined): string {
|
|
129
|
+
if (!next) return "/";
|
|
130
|
+
if (!next.startsWith("/") || next.startsWith("//")) return "/";
|
|
131
|
+
return next;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Redirect back to the login page carrying step-2 state (email/error), no-JS-safe. */
|
|
135
|
+
export function loginRedirect(
|
|
136
|
+
context: APIContext,
|
|
137
|
+
params: { sent?: boolean; email?: string; error?: string; next?: string },
|
|
138
|
+
): Response {
|
|
139
|
+
const q = new URLSearchParams();
|
|
140
|
+
if (params.sent) q.set("sent", "1");
|
|
141
|
+
if (params.email) q.set("email", params.email);
|
|
142
|
+
if (params.error) q.set("error", params.error);
|
|
143
|
+
if (params.next && params.next !== "/") q.set("next", params.next);
|
|
144
|
+
const qs = q.toString();
|
|
145
|
+
return context.redirect(`/auth/login${qs ? `?${qs}` : ""}`, 303);
|
|
146
|
+
}
|