@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apex domain-connect state + dispatch (handoff 2026-08-05-domain-connect-ux).
|
|
3
|
+
*
|
|
4
|
+
* The admin Publish tab's "Domain" section drives the apex DNS cutover
|
|
5
|
+
* (tokenoftrust.com → CloudFront) and its rollback as PRODUCT actions, not a
|
|
6
|
+
* terminal runbook. The Worker holds no AWS credentials: it fires the SAME
|
|
7
|
+
* `repository_dispatch` seam www-publish uses (`event_type=www-domain`,
|
|
8
|
+
* `client_payload.action: connect | rollback`) and the CI workflow (OIDC role)
|
|
9
|
+
* runs the existing scripts. This module owns:
|
|
10
|
+
*
|
|
11
|
+
* - the per-tenant domain-cutover STATE record (KV) — current state
|
|
12
|
+
* (`pantheon` | `cloudfront`), prior-DNS-records capture pointer, and the
|
|
13
|
+
* last/in-flight run;
|
|
14
|
+
* - the PURE state transitions (`beginDomainRun` / `completeDomainRun`) so
|
|
15
|
+
* the refusal + completion policy is unit-testable without I/O;
|
|
16
|
+
* - the dispatch transport (`createDomainDispatch`) mirroring
|
|
17
|
+
* `createRepositoryDispatchSink`'s contract (throw on non-2xx).
|
|
18
|
+
*
|
|
19
|
+
* STATUS-BACK CHOICE (handoff asks to pick one and say why): CI posts a
|
|
20
|
+
* completion CALLBACK to the Worker (`POST /api/domain/callback`, HMAC over the
|
|
21
|
+
* raw body — the same webhook trust shape as `/api/preview/reconcile`), rather
|
|
22
|
+
* than the Worker polling GitHub run status. Why: the dispatch token is a
|
|
23
|
+
* write-only fine-grained credential (actions:write) and the Worker should not
|
|
24
|
+
* grow a GitHub READ credential + polling loop when the reconcile webhook trust
|
|
25
|
+
* model already exists and CI knows exactly when it finished.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Where the apex DNS points right now. `pantheon` is the pre-cutover host. */
|
|
29
|
+
export type DomainPlatformState = "pantheon" | "cloudfront";
|
|
30
|
+
|
|
31
|
+
export type DomainAction = "connect" | "rollback";
|
|
32
|
+
|
|
33
|
+
/** CI callbacks are terminal-only (succeeded|failed); "dispatched" is the in-flight state. */
|
|
34
|
+
export type DomainRunStatus = "dispatched" | "succeeded" | "failed";
|
|
35
|
+
|
|
36
|
+
/** One connect/rollback run — audit fields included (who/when, accept.ts-style). */
|
|
37
|
+
export interface DomainRun {
|
|
38
|
+
/** The dispatch correlation id — `dispatchId` on the wire (both directions). */
|
|
39
|
+
runId: string;
|
|
40
|
+
action: DomainAction;
|
|
41
|
+
/** Dry-run rehearsal (cutover-dns.mjs --dry-run in CI) — never moves state. */
|
|
42
|
+
rehearsal: boolean;
|
|
43
|
+
status: DomainRunStatus;
|
|
44
|
+
/** Audit: the acting operator (viewer email / owner), never from the body. */
|
|
45
|
+
actor: string;
|
|
46
|
+
startedAt: string;
|
|
47
|
+
finishedAt: string | null;
|
|
48
|
+
/** CI run link once known (from the completion callback). */
|
|
49
|
+
runUrl: string | null;
|
|
50
|
+
/** Legible failure detail, or null. */
|
|
51
|
+
error: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The per-tenant domain-cutover state record (KV, `www-domain:state:<tenantId>`). */
|
|
55
|
+
export interface DomainState {
|
|
56
|
+
state: DomainPlatformState;
|
|
57
|
+
/** When the current `state` took effect (ISO), or null if never cutover. */
|
|
58
|
+
since: string | null;
|
|
59
|
+
/**
|
|
60
|
+
* S3 key of the captured prior DNS records powering rollback
|
|
61
|
+
* (`meta/dns-prior-records.<ts>.json`) + when they were captured. Written by
|
|
62
|
+
* CI (which does the capture) via the completion callback.
|
|
63
|
+
*/
|
|
64
|
+
priorRecordsKey: string | null;
|
|
65
|
+
priorCapturedAt: string | null;
|
|
66
|
+
lastRun: DomainRun | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** KV key for a tenant's domain-cutover state. */
|
|
70
|
+
export function domainStateKey(tenantId: string): string {
|
|
71
|
+
return `www-domain:state:${tenantId}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The starting state for a tenant that has never been cutover. */
|
|
75
|
+
export function initialDomainState(): DomainState {
|
|
76
|
+
return {
|
|
77
|
+
state: "pantheon",
|
|
78
|
+
since: null,
|
|
79
|
+
priorRecordsKey: null,
|
|
80
|
+
priorCapturedAt: null,
|
|
81
|
+
lastRun: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Minimal KV surface (matches the `CandidateKv` shape used across the repo). */
|
|
86
|
+
export interface DomainStateKv {
|
|
87
|
+
get(key: string): Promise<string | null>;
|
|
88
|
+
put(key: string, value: string): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read a tenant's domain state; a missing/corrupt record is the initial state. */
|
|
92
|
+
export async function readDomainState(
|
|
93
|
+
kv: DomainStateKv,
|
|
94
|
+
tenantId: string,
|
|
95
|
+
): Promise<DomainState> {
|
|
96
|
+
const raw = await kv.get(domainStateKey(tenantId));
|
|
97
|
+
if (!raw) return initialDomainState();
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(raw) as DomainState;
|
|
100
|
+
if (parsed.state !== "pantheon" && parsed.state !== "cloudfront") {
|
|
101
|
+
return initialDomainState();
|
|
102
|
+
}
|
|
103
|
+
return parsed;
|
|
104
|
+
} catch {
|
|
105
|
+
return initialDomainState();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function writeDomainState(
|
|
110
|
+
kv: DomainStateKv,
|
|
111
|
+
tenantId: string,
|
|
112
|
+
state: DomainState,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
await kv.put(domainStateKey(tenantId), JSON.stringify(state));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export type DomainTransitionResult =
|
|
118
|
+
| { ok: true; state: DomainState }
|
|
119
|
+
| { ok: false; status: number; error: string };
|
|
120
|
+
|
|
121
|
+
/** True when the state's last run is still in flight (awaiting the CI callback). */
|
|
122
|
+
export function runInFlight(state: DomainState): boolean {
|
|
123
|
+
return state.lastRun?.status === "dispatched";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* PURE: begin a connect/rollback run. Refuses (409) while a run is in flight —
|
|
128
|
+
* two concurrent DNS mutations is exactly the race this state exists to prevent.
|
|
129
|
+
* Also refuses (409) a non-rehearsal connect when already on `cloudfront`, and a
|
|
130
|
+
* rollback when there is no captured prior-records pointer to restore from
|
|
131
|
+
* (nothing to roll back TO — CI would have nothing to apply).
|
|
132
|
+
*/
|
|
133
|
+
export function beginDomainRun(
|
|
134
|
+
state: DomainState,
|
|
135
|
+
run: {
|
|
136
|
+
runId: string;
|
|
137
|
+
action: DomainAction;
|
|
138
|
+
rehearsal: boolean;
|
|
139
|
+
actor: string;
|
|
140
|
+
at: string;
|
|
141
|
+
},
|
|
142
|
+
): DomainTransitionResult {
|
|
143
|
+
if (runInFlight(state)) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
status: 409,
|
|
147
|
+
error: `a ${state.lastRun?.action} run is already in flight (${state.lastRun?.runId})`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (run.action === "connect" && !run.rehearsal && state.state === "cloudfront") {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
status: 409,
|
|
154
|
+
error: "domain is already connected (state: cloudfront)",
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (run.action === "rollback" && !run.rehearsal && !state.priorRecordsKey) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
status: 409,
|
|
161
|
+
error: "no captured prior DNS records to roll back to",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
state: {
|
|
167
|
+
...state,
|
|
168
|
+
lastRun: {
|
|
169
|
+
runId: run.runId,
|
|
170
|
+
action: run.action,
|
|
171
|
+
rehearsal: run.rehearsal,
|
|
172
|
+
status: "dispatched",
|
|
173
|
+
actor: run.actor,
|
|
174
|
+
startedAt: run.at,
|
|
175
|
+
finishedAt: null,
|
|
176
|
+
runUrl: null,
|
|
177
|
+
error: null,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The CI completion callback (wire contract with the www-domain workflow,
|
|
185
|
+
* branch issue/www-publish-auto-path). Terminal-only: succeeded | failed.
|
|
186
|
+
* Correlated on `dispatchId` — the id WE minted at dispatch time and sent in
|
|
187
|
+
* `client_payload.dispatchId`; the workflow's `runId`/`runUrl` identify the GH
|
|
188
|
+
* Actions run and are recorded/surfaced, never used for correlation.
|
|
189
|
+
*/
|
|
190
|
+
export interface DomainRunCallback {
|
|
191
|
+
/** Round-trips from `client_payload.dispatchId` — the correlation key. */
|
|
192
|
+
dispatchId: string;
|
|
193
|
+
status: "succeeded" | "failed";
|
|
194
|
+
/** CI-reported resulting platform state ("unknown" → infer from the action). */
|
|
195
|
+
state?: "cloudfront" | "pantheon" | "unknown" | null;
|
|
196
|
+
/** GH Actions run link — surfaced in the UI status. */
|
|
197
|
+
runUrl?: string | null;
|
|
198
|
+
error?: string | null;
|
|
199
|
+
/** On a successful connect: where CI persisted the prior-records capture. */
|
|
200
|
+
priorKey?: string | null;
|
|
201
|
+
completedAt?: string | null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* PURE: apply a CI completion callback. A callback for an unknown `dispatchId`
|
|
206
|
+
* is refused (409) — it belongs to a run this state never dispatched (or one
|
|
207
|
+
* that was superseded), and applying it would corrupt the record.
|
|
208
|
+
*
|
|
209
|
+
* On a SUCCESSFUL non-rehearsal (non-dryRun) run the platform state flips to
|
|
210
|
+
* the CI-reported `state` when concrete, else inferred from the action:
|
|
211
|
+
* connect → `cloudfront` (+ prior-records pointer from `priorKey`; never
|
|
212
|
+
* overwrite an existing pointer with null — the FIRST capture is
|
|
213
|
+
* the true pre-platform state)
|
|
214
|
+
* rollback → `pantheon`
|
|
215
|
+
* Rehearsal (wire: dryRun) runs never move state — only the run outcome is
|
|
216
|
+
* recorded.
|
|
217
|
+
*/
|
|
218
|
+
export function completeDomainRun(
|
|
219
|
+
state: DomainState,
|
|
220
|
+
cb: DomainRunCallback,
|
|
221
|
+
at: string,
|
|
222
|
+
): DomainTransitionResult {
|
|
223
|
+
const run = state.lastRun;
|
|
224
|
+
if (!run || run.runId !== cb.dispatchId) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
status: 409,
|
|
228
|
+
error: `callback for unknown dispatch "${cb.dispatchId}"`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (run.status === "succeeded" || run.status === "failed") {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
status: 409,
|
|
235
|
+
error: `dispatch "${cb.dispatchId}" already completed (${run.status})`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const updatedRun: DomainRun = {
|
|
240
|
+
...run,
|
|
241
|
+
status: cb.status,
|
|
242
|
+
runUrl: cb.runUrl ?? run.runUrl,
|
|
243
|
+
error: cb.status === "failed" ? (cb.error ?? "run failed") : null,
|
|
244
|
+
finishedAt: cb.completedAt ?? at,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
let next: DomainState = { ...state, lastRun: updatedRun };
|
|
248
|
+
if (cb.status === "succeeded" && !run.rehearsal) {
|
|
249
|
+
const reported =
|
|
250
|
+
cb.state === "cloudfront" || cb.state === "pantheon" ? cb.state : null;
|
|
251
|
+
next = {
|
|
252
|
+
...next,
|
|
253
|
+
state: reported ?? (run.action === "connect" ? "cloudfront" : "pantheon"),
|
|
254
|
+
since: updatedRun.finishedAt,
|
|
255
|
+
};
|
|
256
|
+
if (run.action === "connect") {
|
|
257
|
+
next = {
|
|
258
|
+
...next,
|
|
259
|
+
// Keep the FIRST capture if the callback carries none — never regress
|
|
260
|
+
// the pointer to null once a real capture exists.
|
|
261
|
+
priorRecordsKey: cb.priorKey ?? state.priorRecordsKey,
|
|
262
|
+
priorCapturedAt: cb.priorKey
|
|
263
|
+
? (cb.completedAt ?? at)
|
|
264
|
+
: state.priorCapturedAt,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return { ok: true, state: next };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Config for the `www-domain` repository_dispatch (env-sourced, never tenant config). */
|
|
272
|
+
export interface DomainDispatchConfig {
|
|
273
|
+
apiBase?: string;
|
|
274
|
+
owner: string;
|
|
275
|
+
repo: string;
|
|
276
|
+
/** `repository_dispatch` event type; the workflow shim routes on it. */
|
|
277
|
+
eventType: string;
|
|
278
|
+
/** Bearer token with actions:write — never logged. */
|
|
279
|
+
token: string;
|
|
280
|
+
fetchImpl?: typeof fetch;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The `client_payload` wire contract shared with the www-domain CI workflow
|
|
285
|
+
* (branch issue/www-publish-auto-path). The CI shim GATES on the presence/shape
|
|
286
|
+
* of `tenantId`, `action`, `dispatchId`, `callbackUrl`, and `dryRun`; the
|
|
287
|
+
* remaining fields are ADDITIONAL keys (audit-only, tolerated by the shim).
|
|
288
|
+
*/
|
|
289
|
+
export interface DomainDispatchPayload {
|
|
290
|
+
tenantId: string;
|
|
291
|
+
action: DomainAction;
|
|
292
|
+
/** Correlation id — round-trips back as the callback's `dispatchId`. */
|
|
293
|
+
dispatchId: string;
|
|
294
|
+
/** Absolute plain-https URL of /api/domain/callback (CI gates: https, no query/userinfo). */
|
|
295
|
+
callbackUrl: string;
|
|
296
|
+
/** The rehearsal flag on the wire. */
|
|
297
|
+
dryRun: boolean;
|
|
298
|
+
// --- additional (non-gated) audit keys ---
|
|
299
|
+
appDomain: string;
|
|
300
|
+
/** Audit only — CI echoes it back; authorization happened Worker-side. */
|
|
301
|
+
actor: string;
|
|
302
|
+
dispatchedAt: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Fire the `www-domain` repository_dispatch. Mirrors
|
|
307
|
+
* `createRepositoryDispatchSink` (publish-sink.ts): a non-2xx response or a
|
|
308
|
+
* transport error THROWS, so the caller never records a dispatched run that was
|
|
309
|
+
* never actually dispatched.
|
|
310
|
+
*/
|
|
311
|
+
export function createDomainDispatch(config: DomainDispatchConfig) {
|
|
312
|
+
const apiBase = (config.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
|
|
313
|
+
const doFetch = config.fetchImpl ?? fetch;
|
|
314
|
+
return {
|
|
315
|
+
async dispatch(payload: DomainDispatchPayload): Promise<void> {
|
|
316
|
+
const url = `${apiBase}/repos/${config.owner}/${config.repo}/dispatches`;
|
|
317
|
+
let res: Response;
|
|
318
|
+
try {
|
|
319
|
+
res = await doFetch(url, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: {
|
|
322
|
+
authorization: `Bearer ${config.token}`,
|
|
323
|
+
accept: "application/vnd.github+json",
|
|
324
|
+
"content-type": "application/json",
|
|
325
|
+
"user-agent": "tot-storefront-domain-dispatch",
|
|
326
|
+
},
|
|
327
|
+
body: JSON.stringify({
|
|
328
|
+
event_type: config.eventType,
|
|
329
|
+
client_payload: payload,
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
} catch (e) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`domain dispatch to ${config.owner}/${config.repo} failed: ${(e as Error).message}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (!res.ok) {
|
|
338
|
+
let detail = "";
|
|
339
|
+
try {
|
|
340
|
+
detail = (await res.text()).slice(0, 500);
|
|
341
|
+
} catch {
|
|
342
|
+
/* keep the HTTP status */
|
|
343
|
+
}
|
|
344
|
+
throw new Error(
|
|
345
|
+
`domain dispatch to ${config.owner}/${config.repo} returned HTTP ${res.status}` +
|
|
346
|
+
(detail ? `: ${detail}` : ""),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|