@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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* SHELL CONTRACT — every AdminXTab.astro component mounted below must preserve:
|
|
14
14
|
* - A `<section id="{tab}" data-admin-panel data-agent-scope="{tab}">` root,
|
|
15
15
|
* where {tab} is one of: orders, products, subscriptions, customers, fulfillment,
|
|
16
|
-
* reporting, ai-workflows, settings. The shell's hash router shows/hides
|
|
16
|
+
* reporting, ai-workflows, publish, settings. The shell's hash router shows/hides
|
|
17
17
|
* panels by matching `html[data-admin-tab]` to this id, and the AI Ask
|
|
18
18
|
* envelope reads `data-agent-scope` to label its active context.
|
|
19
19
|
* - No tab-switching chrome of its own — `#{tab}` deep links and the shell's
|
|
@@ -37,9 +37,11 @@ import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
|
|
|
37
37
|
import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
|
|
38
38
|
import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
|
|
39
39
|
import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
|
|
40
|
+
import AdminPublishTab from "../components/admin/AdminPublishTab.astro";
|
|
40
41
|
import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
|
|
41
42
|
import { readViewerSession } from "@/lib/auth/route";
|
|
42
43
|
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
44
|
+
import { resolveAdminEntry } from "@/lib/auth/adminEntry";
|
|
43
45
|
|
|
44
46
|
const { tenant, basePath, cspNonce } = Astro.locals;
|
|
45
47
|
const adminPath = `${basePath || ""}/admin`;
|
|
@@ -49,6 +51,19 @@ const viewerEmail = (Astro.locals.viewer?.email ?? viewerSession?.email ?? "").t
|
|
|
49
51
|
const viewerDomain = viewerEmail.split("@").at(-1) ?? "";
|
|
50
52
|
const canViewInternalGuide = viewerDomain === "tokenoftrust.com";
|
|
51
53
|
|
|
54
|
+
// dg6 — admin session ENTRY resolved SERVER-SIDE against the resolved tenant's
|
|
55
|
+
// appDomain (never client input, never a bearer secret): an authenticated
|
|
56
|
+
// owner/team member for THIS tenant reaches the admin context; a non-member /
|
|
57
|
+
// anonymous visitor was already held at the sign-in gate (middleware). Consumes
|
|
58
|
+
// u10's owner + ship-capability model; the go-live tabs (AdminPublishTab) read
|
|
59
|
+
// the same host-bound `Astro.locals.viewer.capability`, so the shell just stamps
|
|
60
|
+
// the authoritative principal for chrome + tests — it does not re-gate.
|
|
61
|
+
const adminEntry = resolveAdminEntry({
|
|
62
|
+
record: viewerSession,
|
|
63
|
+
hostResource: tenant.appDomain,
|
|
64
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
// Internal admin-app tenant mode (baseline doc §Admin App As Tenant,
|
|
53
68
|
// tenant-envelope-contract): every mounted tab receives an explicit
|
|
54
69
|
// { adminAppTenant, targetTenant } envelope. Only a real ToT-staff session
|
|
@@ -72,6 +87,11 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
72
87
|
data-admin-target-tenant={tenantEnvelope.targetTenant}
|
|
73
88
|
data-admin-operator={tenantEnvelope.operator ?? ""}
|
|
74
89
|
data-admin-is-staff={isStaff ? "true" : "false"}
|
|
90
|
+
data-admin-viewer-authenticated={viewerSession ? "true" : "false"}
|
|
91
|
+
data-admin-entry-principal={adminEntry.principal}
|
|
92
|
+
data-admin-is-owner={adminEntry.isOwnerViewer ? "true" : "false"}
|
|
93
|
+
data-admin-can-ship={adminEntry.shipCapabilities.shipOnBehalf ? "true" : "false"}
|
|
94
|
+
data-admin-can-apex-cutover={adminEntry.shipCapabilities.apexCutover ? "true" : "false"}
|
|
75
95
|
>
|
|
76
96
|
<head>
|
|
77
97
|
<meta charset="utf-8" />
|
|
@@ -80,10 +100,12 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
80
100
|
<title>{tenantLabel} Admin</title>
|
|
81
101
|
<script is:inline nonce={cspNonce}>
|
|
82
102
|
(() => {
|
|
83
|
-
const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
103
|
+
const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
|
|
84
104
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
85
105
|
const defaultTab = canViewInternalGuide ? "orders" : "settings";
|
|
86
|
-
|
|
106
|
+
// A deep link may carry a query suffix in the hash (e.g. #publish?pr=42);
|
|
107
|
+
// the tab id is only the part before "?".
|
|
108
|
+
const hash = window.location.hash.slice(1).split("?")[0];
|
|
87
109
|
const activeTab = adminTabs.includes(hash) ? hash : defaultTab;
|
|
88
110
|
const mode = canViewInternalGuide && hash && !adminTabs.includes(hash) ? "guide" : "work-area";
|
|
89
111
|
|
|
@@ -232,6 +254,100 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
232
254
|
background: rgba(255, 255, 255, 0.06);
|
|
233
255
|
}
|
|
234
256
|
|
|
257
|
+
/* Cross-surface hop to the developer cockpit — same card language as the
|
|
258
|
+
sidebar's other blocks, kept visually quieter than the nav tabs. */
|
|
259
|
+
.cockpit-link {
|
|
260
|
+
display: flex;
|
|
261
|
+
align-items: center;
|
|
262
|
+
gap: 8px;
|
|
263
|
+
padding: 10px 12px;
|
|
264
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
265
|
+
border-radius: var(--admin-radius);
|
|
266
|
+
background: rgba(255, 255, 255, 0.04);
|
|
267
|
+
color: rgba(238, 247, 245, 0.85);
|
|
268
|
+
font-size: 13px;
|
|
269
|
+
text-decoration: none;
|
|
270
|
+
}
|
|
271
|
+
.cockpit-link:hover {
|
|
272
|
+
background: rgba(255, 255, 255, 0.1);
|
|
273
|
+
color: #fff;
|
|
274
|
+
}
|
|
275
|
+
.cockpit-link svg {
|
|
276
|
+
width: 15px;
|
|
277
|
+
height: 15px;
|
|
278
|
+
stroke: currentColor;
|
|
279
|
+
fill: none;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Signed-in-as chip (identity visibility — Trello 13075 postscript: an
|
|
283
|
+
owner staring at a read-only Publish tab could not tell WHO the shell
|
|
284
|
+
thought they were). Mirrors .store's card language; the standing line
|
|
285
|
+
states the resolved principal FOR THIS STORE, not a global role. */
|
|
286
|
+
.account {
|
|
287
|
+
display: grid;
|
|
288
|
+
grid-template-columns: auto 1fr;
|
|
289
|
+
gap: 10px;
|
|
290
|
+
align-items: center;
|
|
291
|
+
padding: 12px;
|
|
292
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
293
|
+
border-radius: var(--admin-radius);
|
|
294
|
+
background: rgba(255, 255, 255, 0.06);
|
|
295
|
+
/* Pinned to the sidebar's lower-left (the conventional account slot) —
|
|
296
|
+
the sidebar is a flex column, so this soaks the remaining space. */
|
|
297
|
+
margin-top: auto;
|
|
298
|
+
}
|
|
299
|
+
.account .account-avatar {
|
|
300
|
+
display: grid;
|
|
301
|
+
place-items: center;
|
|
302
|
+
width: 32px;
|
|
303
|
+
height: 32px;
|
|
304
|
+
border-radius: 50%;
|
|
305
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
306
|
+
background: rgba(255, 255, 255, 0.08);
|
|
307
|
+
}
|
|
308
|
+
.account .account-avatar svg {
|
|
309
|
+
width: 16px;
|
|
310
|
+
height: 16px;
|
|
311
|
+
stroke: currentColor;
|
|
312
|
+
fill: none;
|
|
313
|
+
stroke-width: 1.8;
|
|
314
|
+
}
|
|
315
|
+
.account .account-meta {
|
|
316
|
+
display: grid;
|
|
317
|
+
gap: 2px;
|
|
318
|
+
min-width: 0;
|
|
319
|
+
}
|
|
320
|
+
.account .account-email {
|
|
321
|
+
font-weight: 640;
|
|
322
|
+
font-size: 13px;
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
text-overflow: ellipsis;
|
|
325
|
+
white-space: nowrap;
|
|
326
|
+
}
|
|
327
|
+
.account .account-standing {
|
|
328
|
+
color: rgba(238, 247, 245, 0.7);
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
}
|
|
331
|
+
.account .account-standing[data-standing="none"] {
|
|
332
|
+
color: rgba(255, 196, 120, 0.9);
|
|
333
|
+
}
|
|
334
|
+
.account a {
|
|
335
|
+
color: inherit;
|
|
336
|
+
}
|
|
337
|
+
.account .account-signout button {
|
|
338
|
+
margin-top: 2px;
|
|
339
|
+
padding: 0;
|
|
340
|
+
border: 0;
|
|
341
|
+
background: none;
|
|
342
|
+
color: rgba(238, 247, 245, 0.6);
|
|
343
|
+
font-size: 11px;
|
|
344
|
+
text-decoration: underline;
|
|
345
|
+
cursor: pointer;
|
|
346
|
+
}
|
|
347
|
+
.account .account-signout button:hover {
|
|
348
|
+
color: rgba(238, 247, 245, 0.9);
|
|
349
|
+
}
|
|
350
|
+
|
|
235
351
|
.tenant-picker {
|
|
236
352
|
display: grid;
|
|
237
353
|
gap: 4px;
|
|
@@ -508,6 +624,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
508
624
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
|
|
509
625
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
|
|
510
626
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="ai-workflows"] #ai-workflows,
|
|
627
|
+
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="publish"] #publish,
|
|
511
628
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="settings"] #settings {
|
|
512
629
|
display: grid;
|
|
513
630
|
}
|
|
@@ -784,6 +901,13 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
784
901
|
background: #fff6e7;
|
|
785
902
|
}
|
|
786
903
|
|
|
904
|
+
/* A JS-toggled `.risk-alert` (e.g. the degraded-forge strip) must stay hidden
|
|
905
|
+
when `hidden` is set — the base display:flex above would otherwise win over
|
|
906
|
+
the UA [hidden] rule and keep it visible. */
|
|
907
|
+
.risk-alert[hidden] {
|
|
908
|
+
display: none;
|
|
909
|
+
}
|
|
910
|
+
|
|
787
911
|
.risk-alert svg {
|
|
788
912
|
color: var(--admin-amber);
|
|
789
913
|
}
|
|
@@ -1068,6 +1192,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1068
1192
|
<symbol id="i-orders" viewBox="0 0 24 24"><path d="M7 4h10a2 2 0 0 1 2 2v14l-3-2-2 2-2-2-2 2-3 2V6a2 2 0 0 1 2-2Z"></path><path d="M9 9h6M9 13h6"></path></symbol>
|
|
1069
1193
|
<symbol id="i-box" viewBox="0 0 24 24"><path d="m3 7 9-4 9 4-9 4-9-4Z"></path><path d="M3 7v10l9 4 9-4V7"></path><path d="M12 11v10"></path></symbol>
|
|
1070
1194
|
<symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"></path><circle cx="9.5" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.9"></path><path d="M16 3.1a4 4 0 0 1 0 7.8"></path></symbol>
|
|
1195
|
+
<symbol id="i-user" viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></symbol>
|
|
1071
1196
|
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 20 6v6c0 5-3.4 8.4-8 9-4.6-.6-8-4-8-9V6l8-3Z"></path><path d="m8.8 12 2.2 2.2L15.8 9"></path></symbol>
|
|
1072
1197
|
<symbol id="i-chart" viewBox="0 0 24 24"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M8 16v-5"></path><path d="M12 16V8"></path><path d="M16 16v-3"></path></symbol>
|
|
1073
1198
|
<symbol id="i-workflows" viewBox="0 0 24 24"><path d="M6 5h6a4 4 0 0 1 4 4v1"></path><path d="m19 7-3 3-3-3"></path><path d="M18 19h-6a4 4 0 0 1-4-4v-1"></path><path d="m5 17 3-3 3 3"></path><circle cx="6" cy="5" r="2"></circle><circle cx="18" cy="19" r="2"></circle></symbol>
|
|
@@ -1133,6 +1258,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1133
1258
|
<a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
|
|
1134
1259
|
<a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
|
|
1135
1260
|
<a href="#ai-workflows" data-admin-tab-link><svg><use href="#i-workflows"></use></svg><span>AI Workflows</span></a>
|
|
1261
|
+
<a href="#publish" data-admin-tab-link><svg><use href="#i-export"></use></svg><span>Publish</span></a>
|
|
1136
1262
|
<a href="#settings" data-admin-tab-link><svg><use href="#i-gear"></use></svg><span>Settings</span></a>
|
|
1137
1263
|
</nav>
|
|
1138
1264
|
|
|
@@ -1141,6 +1267,44 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1141
1267
|
<div class="health-row"><span>Payments</span><strong>OK</strong></div>
|
|
1142
1268
|
<div class="health-row"><span>Risk checks</span><strong>OK</strong></div>
|
|
1143
1269
|
</div>
|
|
1270
|
+
|
|
1271
|
+
<!-- Cross-surface hop: the developer cockpit is a SIBLING surface (dev
|
|
1272
|
+
setup + connection status), not an admin tab — link, don't embed.
|
|
1273
|
+
basePath-aware so it stays inside the tenant prefix. -->
|
|
1274
|
+
<a class="cockpit-link" href={`${basePath || ""}/cockpit`}>
|
|
1275
|
+
<svg><use href="#i-gear"></use></svg><span>Developer cockpit</span>
|
|
1276
|
+
</a>
|
|
1277
|
+
|
|
1278
|
+
<div class="account" data-admin-account>
|
|
1279
|
+
<span class="account-avatar" aria-hidden="true"><svg><use href="#i-user"></use></svg></span>
|
|
1280
|
+
<span class="account-meta">
|
|
1281
|
+
{viewerEmail ? (
|
|
1282
|
+
<>
|
|
1283
|
+
<span class="account-email" title={viewerEmail}>{viewerEmail}</span>
|
|
1284
|
+
<span
|
|
1285
|
+
class="account-standing"
|
|
1286
|
+
data-standing={adminEntry.isOwnerViewer ? "owner" : isStaff ? "staff" : "none"}
|
|
1287
|
+
>
|
|
1288
|
+
{adminEntry.isOwnerViewer
|
|
1289
|
+
? `owner/admin of ${tenantEnvelope.targetTenant}`
|
|
1290
|
+
: isStaff
|
|
1291
|
+
? `ToT staff · not an owner of ${tenantEnvelope.targetTenant}`
|
|
1292
|
+
: `signed in · no owner/admin standing for ${tenantEnvelope.targetTenant}`}
|
|
1293
|
+
</span>
|
|
1294
|
+
<form class="account-signout" method="post" action="/api/auth/logout">
|
|
1295
|
+
<button type="submit">Sign out</button>
|
|
1296
|
+
</form>
|
|
1297
|
+
</>
|
|
1298
|
+
) : (
|
|
1299
|
+
<>
|
|
1300
|
+
<span class="account-email">Not signed in</span>
|
|
1301
|
+
<span class="account-standing" data-standing="none">
|
|
1302
|
+
<a href="/auth/login">sign in</a> as an owner/admin of {tenantEnvelope.targetTenant} to publish
|
|
1303
|
+
</span>
|
|
1304
|
+
</>
|
|
1305
|
+
)}
|
|
1306
|
+
</span>
|
|
1307
|
+
</div>
|
|
1144
1308
|
</aside>
|
|
1145
1309
|
|
|
1146
1310
|
<main class="main">
|
|
@@ -1159,6 +1323,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1159
1323
|
<a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1160
1324
|
<a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
1161
1325
|
<a class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
|
|
1326
|
+
<a class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
|
|
1162
1327
|
<a class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
|
|
1163
1328
|
</div>
|
|
1164
1329
|
|
|
@@ -1206,6 +1371,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1206
1371
|
<a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1207
1372
|
<a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
1208
1373
|
<a id="admin-tab-ai-workflows" class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
|
|
1374
|
+
<a id="admin-tab-publish" class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
|
|
1209
1375
|
<a id="admin-tab-settings" class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
|
|
1210
1376
|
</div>
|
|
1211
1377
|
|
|
@@ -1331,6 +1497,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1331
1497
|
<AdminFulfillmentTab envelope={tenantEnvelope} />
|
|
1332
1498
|
<AdminReportingTab envelope={tenantEnvelope} />
|
|
1333
1499
|
<AdminAIWorkflowsTab envelope={tenantEnvelope} />
|
|
1500
|
+
<AdminPublishTab envelope={tenantEnvelope} />
|
|
1334
1501
|
<AdminSettingsTab envelope={tenantEnvelope} />
|
|
1335
1502
|
|
|
1336
1503
|
{canViewInternalGuide && (
|
|
@@ -1447,7 +1614,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1447
1614
|
|
|
1448
1615
|
<script is:inline nonce={cspNonce}>
|
|
1449
1616
|
(() => {
|
|
1450
|
-
const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
1617
|
+
const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
|
|
1451
1618
|
const tabSet = new Set(tabIds);
|
|
1452
1619
|
const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
|
|
1453
1620
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
@@ -1460,16 +1627,20 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1460
1627
|
fulfillment: "Fulfillment",
|
|
1461
1628
|
reporting: "Reporting",
|
|
1462
1629
|
"ai-workflows": "AI Workflows",
|
|
1630
|
+
publish: "Publish",
|
|
1463
1631
|
settings: "Settings",
|
|
1464
1632
|
};
|
|
1465
1633
|
|
|
1466
1634
|
const panelIdFromHash = () => {
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1635
|
+
const rawHash = window.location.hash.slice(1);
|
|
1636
|
+
// The tab id is the part before any "?" query suffix (#publish?pr=42);
|
|
1637
|
+
// the raw hash is still used for internal-guide anchor resolution.
|
|
1638
|
+
const hash = rawHash.split("?")[0];
|
|
1639
|
+
if (!rawHash) return defaultTab;
|
|
1469
1640
|
if (tabSet.has(hash)) return hash;
|
|
1470
1641
|
if (!canViewInternalGuide) return defaultTab;
|
|
1471
1642
|
|
|
1472
|
-
const target =
|
|
1643
|
+
const target = rawHash ? document.getElementById(rawHash) : null;
|
|
1473
1644
|
const panel = target?.closest?.("[data-admin-panel]");
|
|
1474
1645
|
if (panel?.id && tabSet.has(panel.id)) return panel.id;
|
|
1475
1646
|
|
|
@@ -1599,8 +1770,11 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1599
1770
|
|
|
1600
1771
|
if (panelId) {
|
|
1601
1772
|
setActiveTab(panelId);
|
|
1602
|
-
|
|
1603
|
-
|
|
1773
|
+
// Strip any query suffix before deciding whether the hash names a
|
|
1774
|
+
// real tab — a valid deep link like #publish?pr=42 must not be
|
|
1775
|
+
// normalized away to #settings for merchant viewers.
|
|
1776
|
+
const hashTab = window.location.hash.slice(1).split("?")[0];
|
|
1777
|
+
if (!canViewInternalGuide && !tabSet.has(hashTab)) {
|
|
1604
1778
|
history.replaceState(null, "", "#settings");
|
|
1605
1779
|
}
|
|
1606
1780
|
if (scroll) requestAnimationFrame(() => scrollToHashTarget(panelId));
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/activity — the ingest endpoint of the operational activity telemetry
|
|
3
|
+
* spine (card D4). Accepts one `ActivityEvent` (or a batch array) as JSON from
|
|
4
|
+
* the emitter cohorts — the CLI (D3) and the UI (D6), both posting the wire shape
|
|
5
|
+
* of `packages/public-runtime/src/activity/event.ts` — authenticates the caller,
|
|
6
|
+
* re-redacts server-side, and fans each accepted event out to the sink(s) its
|
|
7
|
+
* catalog entry declares. See DECISIONS.md §8 for the store decision.
|
|
8
|
+
*
|
|
9
|
+
* AUTH — HMAC-SHA256 service-auth (`src/lib/activity/ingestAuth.ts`). The caller
|
|
10
|
+
* presents the authenticated tenant in `x-tot-activity-tenant` and a hex
|
|
11
|
+
* signature over `<tenant>\n<rawBody>` in `x-tot-activity-signature`, keyed by
|
|
12
|
+
* the `ACTIVITY_INGEST_SECRET` Worker secret. Binding the tenant into the signed
|
|
13
|
+
* material is what makes the tenant scope trustworthy: the route writes every
|
|
14
|
+
* row under THIS verified tenant and discards any body-supplied `scope.tenantId`
|
|
15
|
+
* (multi-tenant isolation). An unset secret is a 503 (seam not configured — the
|
|
16
|
+
* same convention as the MCP webhook receivers), not an auth failure.
|
|
17
|
+
*
|
|
18
|
+
* DEFENSE IN DEPTH — every event is rebuilt through `createActivityEvent()`
|
|
19
|
+
* (`ingestEvents`), re-applying the default-deny allowlist + canary scan even
|
|
20
|
+
* though emitters already redacted. A value-canary trip is self-telemetried as
|
|
21
|
+
* `system.ingest.rejected` so a leak attempt is visible (D6/D7 alert on it).
|
|
22
|
+
*
|
|
23
|
+
* OFF THE CRITICAL PATH — auth + parse + validate + redact are cheap synchronous
|
|
24
|
+
* CPU; the sink WRITES (D1 + Analytics Engine) run past the response via
|
|
25
|
+
* `runAfterResponse` (Workers `waitUntil`). The response is a 202 with per-event
|
|
26
|
+
* accepted/rejected counts the instant validation is done — a slow D1 write never
|
|
27
|
+
* holds the emitter's request open. JSON bodies are exempt from Astro's CSRF
|
|
28
|
+
* origin check, so the server-to-server callers work.
|
|
29
|
+
*/
|
|
30
|
+
import type { APIContext } from "astro";
|
|
31
|
+
import { readD1, readEnv, readAnalytics, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
|
|
32
|
+
import { fromD1 } from "@/lib/d1/catalog";
|
|
33
|
+
import { verifyIngestAuth } from "@/lib/activity/ingestAuth";
|
|
34
|
+
import { ingestEvents, type IngestRejection } from "@/lib/activity/ingest";
|
|
35
|
+
import { fanOutActivity, type AnalyticsEngineLike, type ActivitySinks } from "@/lib/activity/store";
|
|
36
|
+
import { isActivityDisabled } from "@/lib/activity/killSwitch";
|
|
37
|
+
import { createActivityEvent } from "@tot/public-runtime";
|
|
38
|
+
|
|
39
|
+
export const prerender = false;
|
|
40
|
+
|
|
41
|
+
/** Cap the batch so a single POST can't fan out unbounded work past the response. */
|
|
42
|
+
const MAX_BATCH = 200;
|
|
43
|
+
|
|
44
|
+
function json(body: unknown, status: number): Response {
|
|
45
|
+
return new Response(JSON.stringify(body), {
|
|
46
|
+
status,
|
|
47
|
+
// Write-path ack — never cacheable. Mirrors the house convention.
|
|
48
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
53
|
+
// Kill switch (D8): telemetry off ⇒ accept-and-drop. Fail-open toward the EMITTER —
|
|
54
|
+
// a 202 (never a 4xx/5xx) so a CLI/UI poster never errors, retries, or is delayed by
|
|
55
|
+
// the switch; nothing is validated, stored, or fanned out.
|
|
56
|
+
if (isActivityDisabled()) {
|
|
57
|
+
return json({ ok: true, disabled: true, accepted: 0, rejected: [] }, 202);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const secret = await readEnv("ACTIVITY_INGEST_SECRET");
|
|
61
|
+
if (!secret) {
|
|
62
|
+
// Seam not configured (emitters don't post without the shared ingest secret).
|
|
63
|
+
return new Response("activity ingest not configured", { status: 503 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read the RAW body once: it is both the auth material and the JSON payload.
|
|
67
|
+
const rawBody = await context.request.text();
|
|
68
|
+
|
|
69
|
+
const auth = await verifyIngestAuth(context.request.headers, rawBody, secret);
|
|
70
|
+
if (!auth.ok) return new Response(auth.reason, { status: auth.status });
|
|
71
|
+
|
|
72
|
+
let parsed: unknown;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(rawBody);
|
|
75
|
+
} catch {
|
|
76
|
+
return json({ ok: false, reason: "invalid JSON body" }, 400);
|
|
77
|
+
}
|
|
78
|
+
const events = Array.isArray(parsed) ? parsed : [parsed];
|
|
79
|
+
if (events.length === 0) return json({ ok: true, accepted: 0, rejected: [] }, 202);
|
|
80
|
+
if (events.length > MAX_BATCH) {
|
|
81
|
+
return json({ ok: false, reason: `batch too large (max ${MAX_BATCH})` }, 413);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate + re-redact under the AUTHENTICATED tenant (cheap, synchronous).
|
|
85
|
+
const { accepted, rejected } = ingestEvents(events, auth.tenantId);
|
|
86
|
+
|
|
87
|
+
// Resolve the sink bindings (absent in dev/until provisioned ⇒ that sink is
|
|
88
|
+
// skipped, never an error).
|
|
89
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
90
|
+
const analytics = (await readAnalytics("ACTIVITY_ANALYTICS")) as AnalyticsEngineLike | undefined;
|
|
91
|
+
const sinks: ActivitySinks = {
|
|
92
|
+
...(d1 ? { db: fromD1(d1) } : {}),
|
|
93
|
+
...(analytics ? { analytics } : {}),
|
|
94
|
+
ingestedAt: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Self-telemetry: a value-canary trip during server-side redaction means an
|
|
98
|
+
// emitter leaked (or tried to leak) a secret/PII into the payload. Emit a
|
|
99
|
+
// `system.ingest.rejected` event (itself routed to logs+analytics) so D6/D7 can
|
|
100
|
+
// alert — the fact of the trip, the action, never the offending value. Rejected
|
|
101
|
+
// events get the same self-telemetry so a spoof/unknown-action attempt is
|
|
102
|
+
// visible. Built here (synchronously) but fanned out off-path below.
|
|
103
|
+
const selfTelemetry = [
|
|
104
|
+
...accepted
|
|
105
|
+
.filter((a) => a.canaryTripped)
|
|
106
|
+
.map((a) =>
|
|
107
|
+
createActivityEvent({
|
|
108
|
+
action: "system.ingest.rejected",
|
|
109
|
+
actor: { kind: "system", id: "activity-ingest" },
|
|
110
|
+
source: "server",
|
|
111
|
+
outcome: { status: "refused", errorClass: "canary_tripped" },
|
|
112
|
+
scope: { tenantId: auth.tenantId },
|
|
113
|
+
payload: { args: { reason: "canary_tripped", attemptedAction: a.event.action } },
|
|
114
|
+
}).event,
|
|
115
|
+
),
|
|
116
|
+
...rejected.map((r: IngestRejection) =>
|
|
117
|
+
createActivityEvent({
|
|
118
|
+
action: "system.ingest.rejected",
|
|
119
|
+
actor: { kind: "system", id: "activity-ingest" },
|
|
120
|
+
source: "server",
|
|
121
|
+
outcome: { status: "refused", errorClass: r.reason },
|
|
122
|
+
scope: { tenantId: auth.tenantId },
|
|
123
|
+
payload: { args: { reason: r.reason, ...(r.attemptedAction ? { attemptedAction: r.attemptedAction } : {}) } },
|
|
124
|
+
}).event,
|
|
125
|
+
),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Fan every accepted event + the self-telemetry out to its sinks, PAST the
|
|
129
|
+
// response. Best-effort per sink (never throws). The adapter's per-request
|
|
130
|
+
// ExecutionContext lives at `locals.runtime.ctx`; runAfterResponse floats the
|
|
131
|
+
// promise in dev/Node when it is absent.
|
|
132
|
+
const cfContext = (context.locals as { runtime?: { ctx?: CfExecutionContextLike } })?.runtime?.ctx ?? null;
|
|
133
|
+
const work = Promise.all([...accepted.map((a) => a.event), ...selfTelemetry].map((e) => fanOutActivity(e, sinks)));
|
|
134
|
+
runAfterResponse(work, cfContext);
|
|
135
|
+
|
|
136
|
+
return json({ ok: true, accepted: accepted.length, rejected }, 202);
|
|
137
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/admin/activity-alerts` — the polled/on-demand surface for card D7's
|
|
3
|
+
* operational-activity alert conditions.
|
|
4
|
+
*
|
|
5
|
+
* On-demand by design (see `lib/activity/alerts.ts` header): D7 deliberately does
|
|
6
|
+
* NOT add a cron/scheduled runner (no established precedent for one in this repo).
|
|
7
|
+
* This route runs `checkActivityAlerts()` fresh on every call, so it doubles as:
|
|
8
|
+
* (a) the data source for the `/admin/ops-timeline` alerts banner (that page
|
|
9
|
+
* calls the same function directly, in-process — this route is for OTHER
|
|
10
|
+
* callers), and
|
|
11
|
+
* (b) a lightweight endpoint an external uptime/monitor tool can poll on an
|
|
12
|
+
* interval, turning "on-demand" into "effectively continuous" without this
|
|
13
|
+
* repo standing up its own scheduler.
|
|
14
|
+
*
|
|
15
|
+
* ACCESS CONTROL mirrors `/admin/ops-timeline` exactly (same owner/admin-of-tenant
|
|
16
|
+
* or ToT-staff gate via `resolveAdminEntry`/`resolveAdminTenantMode`) rather than a
|
|
17
|
+
* new service-secret seam — this repo's other machine-to-machine surfaces
|
|
18
|
+
* (candidate-reconcile, the activity ingest route) authenticate a SPECIFIC known
|
|
19
|
+
* caller (MCP, an emitter) via a shared secret; there is no such caller for
|
|
20
|
+
* "a monitor polls for alerts" yet, and inventing one is the "new notification
|
|
21
|
+
* infra" the card scopes OUT. An admin/staff session (the same one that already
|
|
22
|
+
* views ops-timeline) is the correct, already-available caller today.
|
|
23
|
+
*
|
|
24
|
+
* TENANT ISOLATION — scoped to `Astro.locals.tenant.tenant_id` exactly like
|
|
25
|
+
* ops-timeline; never a URL/query value.
|
|
26
|
+
*/
|
|
27
|
+
import type { APIContext } from "astro";
|
|
28
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
29
|
+
import { resolveAdminEntry } from "@/lib/auth/adminEntry";
|
|
30
|
+
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
31
|
+
import { readD1 } from "@/lib/env";
|
|
32
|
+
import { fromD1 } from "@/lib/d1/catalog";
|
|
33
|
+
import { checkActivityAlerts } from "@/lib/activity/alerts";
|
|
34
|
+
|
|
35
|
+
export const prerender = false;
|
|
36
|
+
|
|
37
|
+
const JSON_HEADERS = {
|
|
38
|
+
"content-type": "application/json; charset=utf-8",
|
|
39
|
+
"cache-control": "no-store",
|
|
40
|
+
"x-robots-tag": "noindex, nofollow",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function json(body: unknown, status = 200): Response {
|
|
44
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
48
|
+
const { tenant } = context.locals;
|
|
49
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
50
|
+
|
|
51
|
+
const viewerSession = await readViewerSession(context);
|
|
52
|
+
if (!viewerSession) return json({ ok: false, reason: "sign_in_required" }, 401);
|
|
53
|
+
|
|
54
|
+
const adminEntry = resolveAdminEntry({
|
|
55
|
+
record: viewerSession,
|
|
56
|
+
hostResource: tenant.appDomain,
|
|
57
|
+
nowSeconds,
|
|
58
|
+
});
|
|
59
|
+
const { isStaff } = resolveAdminTenantMode({
|
|
60
|
+
session: viewerSession,
|
|
61
|
+
adminAppTenant: tenant.appDomain,
|
|
62
|
+
requestedTarget: null,
|
|
63
|
+
nowSeconds,
|
|
64
|
+
});
|
|
65
|
+
const authorized = adminEntry.isOwnerViewer || isStaff;
|
|
66
|
+
if (!authorized) return json({ ok: false, reason: "not_authorized" }, 403);
|
|
67
|
+
|
|
68
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
69
|
+
if (!d1) return json({ ok: true, dbAvailable: false, alerts: [] });
|
|
70
|
+
|
|
71
|
+
const alerts = await checkActivityAlerts(fromD1(d1), tenant.tenant_id);
|
|
72
|
+
return json({ ok: true, dbAvailable: true, alerts });
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/apps/admin/credentials/rotate — rotate an install's client
|
|
3
|
+
* secret, keeping the same `client_id` (PrivateApps epic, D2 Chunk C; D8
|
|
4
|
+
* console-session auth added). The new secret is returned ONCE here and never
|
|
5
|
+
* again — the same one-time-secret guarantee `install()` gives (see
|
|
6
|
+
* `registryService.ts`'s `InstallResult` doc).
|
|
7
|
+
*
|
|
8
|
+
* `registryService.rotateCredentials()` takes a bare `installId` with no
|
|
9
|
+
* tenant check of its own (Chunk A assumption #1) — so this route resolves
|
|
10
|
+
* the install via `requireOwnedInstall` BEFORE calling it, rejecting a
|
|
11
|
+
* foreign-tenant `installId` with the SAME 404 a truly-missing one gets (no
|
|
12
|
+
* cross-tenant existence oracle). Mutating: `requireManageRole` gates a
|
|
13
|
+
* console `developer` role out — the host-bound/Bearer path is exempt (no
|
|
14
|
+
* `role`).
|
|
15
|
+
*/
|
|
16
|
+
import type { APIContext } from "astro";
|
|
17
|
+
import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
|
|
18
|
+
import {
|
|
19
|
+
getRegistryService,
|
|
20
|
+
registryErrorResponse,
|
|
21
|
+
requireOwnedInstall,
|
|
22
|
+
requireManageRole,
|
|
23
|
+
parseInstallId,
|
|
24
|
+
json,
|
|
25
|
+
} from "@/lib/apps/adminService.js";
|
|
26
|
+
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
27
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
28
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
29
|
+
|
|
30
|
+
export const prerender = false;
|
|
31
|
+
|
|
32
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
33
|
+
const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
|
|
34
|
+
const auth = await resolveOperatorSession(context, requestedTenant);
|
|
35
|
+
if (!auth.ok) return json({ error: auth.message }, auth.status);
|
|
36
|
+
const manageDenied = requireManageRole(auth);
|
|
37
|
+
if (manageDenied) return manageDenied;
|
|
38
|
+
|
|
39
|
+
let body: unknown;
|
|
40
|
+
try {
|
|
41
|
+
body = await context.request.json();
|
|
42
|
+
} catch {
|
|
43
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const installId = parseInstallId(body);
|
|
47
|
+
if (!installId) return json({ error: "installId is required" }, 400);
|
|
48
|
+
|
|
49
|
+
const registry = await getRegistryService();
|
|
50
|
+
if (registry instanceof Response) return registry;
|
|
51
|
+
|
|
52
|
+
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
53
|
+
if (owned instanceof Response) return owned;
|
|
54
|
+
|
|
55
|
+
// D10: acting cohort (merchant/admin), emitted source "server". SECURITY: the
|
|
56
|
+
// rotated secret (`issued.secret`) is NEVER placed in the payload — the allowlist
|
|
57
|
+
// names only the non-sensitive app handle (appId/env, from the owned install) +
|
|
58
|
+
// rotation time.
|
|
59
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const issued = await registry.rotateCredentials(installId, auth.actor);
|
|
63
|
+
recordActivity({
|
|
64
|
+
action: "app.credential.rotated",
|
|
65
|
+
actor: activityActor,
|
|
66
|
+
source: "server",
|
|
67
|
+
outcome: { status: "succeeded" },
|
|
68
|
+
scope: { tenantId: auth.tenantId },
|
|
69
|
+
payload: { args: { appId: owned.appId, env: owned.env, rotatedAt: new Date().toISOString() } },
|
|
70
|
+
});
|
|
71
|
+
return json({ credential: { clientId: issued.clientId, secret: issued.secret } });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err instanceof RegistryError) {
|
|
74
|
+
recordActivity({
|
|
75
|
+
action: "app.credential.rotated",
|
|
76
|
+
actor: activityActor,
|
|
77
|
+
source: "server",
|
|
78
|
+
outcome: { status: "failed", errorClass: err.code },
|
|
79
|
+
scope: { tenantId: auth.tenantId },
|
|
80
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
81
|
+
});
|
|
82
|
+
return registryErrorResponse(err);
|
|
83
|
+
}
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/apps/admin/health — the console's per-install health rollup for
|
|
3
|
+
* the resolved operator tenant (PrivateApps epic, D8 Chunk C). Read-only:
|
|
4
|
+
* any membership role admits (mirrors `list.ts`'s `?tenant=`-as-candidate
|
|
5
|
+
* scoping — the tenant actually queried is always `auth.tenantId`, never the
|
|
6
|
+
* raw param).
|
|
7
|
+
*
|
|
8
|
+
* Folds `registryService.listByTenant` with `deliveryStore.getInstallHealth`
|
|
9
|
+
* per install through `healthAggregate.ts`'s `buildInstallHealthView` — that
|
|
10
|
+
* view is what data-minimizes `webhookEndpoint` down to `hasWebhookEndpoint`,
|
|
11
|
+
* so this route never even holds the raw endpoint value to leak.
|
|
12
|
+
*/
|
|
13
|
+
import type { APIContext } from "astro";
|
|
14
|
+
import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
|
|
15
|
+
import { getRegistryService, json } from "@/lib/apps/adminService.js";
|
|
16
|
+
import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
|
|
17
|
+
import { buildInstallHealthView, type AppInstallHealthView } from "@/lib/apps/healthAggregate.js";
|
|
18
|
+
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
19
|
+
import { readD1 } from "@/lib/env";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
24
|
+
const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
|
|
25
|
+
const auth = await resolveOperatorSession(context, requestedTenant);
|
|
26
|
+
if (!auth.ok) return json({ error: auth.message }, auth.status);
|
|
27
|
+
|
|
28
|
+
const registry = await getRegistryService();
|
|
29
|
+
if (registry instanceof Response) return registry;
|
|
30
|
+
|
|
31
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
32
|
+
if (!d1) return json({ error: "webhook store not configured" }, 503);
|
|
33
|
+
const deliveryStore = new WebhookDeliveryStore(fromD1(d1));
|
|
34
|
+
|
|
35
|
+
const installs = await registry.listByTenant(auth.tenantId);
|
|
36
|
+
const apps: AppInstallHealthView[] = await Promise.all(
|
|
37
|
+
installs.map(async (install) => {
|
|
38
|
+
const health = await deliveryStore.getInstallHealth(install.installId);
|
|
39
|
+
return buildInstallHealthView(install, health);
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return json({ apps });
|
|
44
|
+
}
|