@tokenoftrust/storefront-runner 1.4.1 → 1.4.2-rc.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 +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +14 -0
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1105 -2334
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +17 -19
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +3 -3
- package/apps/storefront/src/lib/activity/deployVersion.ts +19 -26
- package/apps/storefront/src/lib/activity/ingest.ts +8 -8
- package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
- package/apps/storefront/src/lib/activity/query.ts +9 -9
- package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
- package/apps/storefront/src/lib/activity/store.ts +4 -6
- package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
- package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +2 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +96 -17
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -49
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +113 -2
- package/apps/storefront/src/pages/admin.astro +10 -1312
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +37 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/catalog.ts +52 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +1 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
// Unlike the packages/* configs (which extend the repo's ../../tsconfig.base.json),
|
|
3
|
+
// the storefront extends Astro's own preset: it wires up .astro type resolution
|
|
4
|
+
// (.astro/types.d.ts), Astro/Vite bundler module resolution, and the Preact JSX
|
|
5
|
+
// runtime the client islands compile against. verbatimModuleSyntax is turned off
|
|
6
|
+
// (the base sets it on) because the JSX/Preact + Astro compilation model needs it.
|
|
2
7
|
"extends": "astro/tsconfigs/strict",
|
|
3
8
|
"compilerOptions": {
|
|
4
9
|
"strict": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2-rc.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
|
|
6
6
|
"packageManager": "pnpm@11.9.0",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tot/public-runtime",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
+
"description": "Public, edge-shipped storefront runtime model — renderer-safe tenant/product/catalog types, the CSP builder, the customization read/apply runtime, and the activity-telemetry contract. Node-free and open-sourceable; bundles into the Cloudflare Worker and must not import @tot/private-controlplane.",
|
|
4
5
|
"private": true,
|
|
5
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
7
|
"type": "module",
|
|
@@ -179,6 +179,26 @@ export const ACTION_CATALOG = {
|
|
|
179
179
|
argsAllow: ["changeId", "result", "evidenceStatus"], renderedAllow: [],
|
|
180
180
|
desc: "A candidate reconcile reached a terminal state.",
|
|
181
181
|
},
|
|
182
|
+
"candidate.closed": {
|
|
183
|
+
// A `pull_request` `closed` webhook drove a candidate to its TERMINAL status
|
|
184
|
+
// (candidate-reconcile.ts): `merged` (squash-merged) or `closed` (closed
|
|
185
|
+
// unmerged), so the row leaves the operator's review queue. Low volume (one
|
|
186
|
+
// per PR close), high value — the end-of-life marker for a candidate's timeline.
|
|
187
|
+
actors: ["system"], source: "server", sinks: LIFECYCLE,
|
|
188
|
+
argsAllow: ["changeId", "merged", "result", "status"], renderedAllow: [],
|
|
189
|
+
desc: "A merged/closed PR webhook transitioned a candidate to a terminal state.",
|
|
190
|
+
},
|
|
191
|
+
"prebuild.dispatched": {
|
|
192
|
+
// T1 background pre-build (docs/epics/proposed/storefront-instant-static-rollback.md
|
|
193
|
+
// Addendum §B): a successful candidate reconcile fired (or failed to fire) the
|
|
194
|
+
// materialize+validate+STAGE-ONLY repository_dispatch for a static-publish
|
|
195
|
+
// tenant (candidate-reconcile.ts). Fail-open by design — this never gates the
|
|
196
|
+
// candidate reaching `ready`; recorded so a stuck/never-dispatched prebuild is
|
|
197
|
+
// observable rather than silently absent.
|
|
198
|
+
actors: ["system"], source: "server", sinks: LIFECYCLE,
|
|
199
|
+
argsAllow: ["changeId", "headSha"], renderedAllow: [],
|
|
200
|
+
desc: "A candidate's background static-prebuild dispatch (stage-only) succeeded or failed.",
|
|
201
|
+
},
|
|
182
202
|
|
|
183
203
|
// ── Deploy provenance (source: server or cli) — card D2 ────────────────────
|
|
184
204
|
"deploy.version_observed": {
|
|
@@ -186,6 +206,11 @@ export const ACTION_CATALOG = {
|
|
|
186
206
|
argsAllow: ["version", "commit", "env", "observedFrom"], renderedAllow: [],
|
|
187
207
|
desc: "A deployed worker version/commit was observed (provenance).",
|
|
188
208
|
},
|
|
209
|
+
"publish.readiness_evidence.recorded": {
|
|
210
|
+
actors: ["system"], source: "server", sinks: PROVENANCE,
|
|
211
|
+
argsAllow: ["routes", "cfFailures", "statusMismatches", "targetHealthy"], renderedAllow: [],
|
|
212
|
+
desc: "Go-live readiness dual-run evidence was recorded for a tenant (u9 producer).",
|
|
213
|
+
},
|
|
189
214
|
|
|
190
215
|
// ── ShipLoop change lifecycle (source: ui/server) ──────────────────────────
|
|
191
216
|
// 1:1 with the admin ship routes + CHANGE_STATES (change-record.ts). Low
|
|
@@ -208,6 +233,15 @@ export const ACTION_CATALOG = {
|
|
|
208
233
|
argsAllow: ["changeId"], renderedAllow: ["reason"],
|
|
209
234
|
desc: "A change was rejected (reason required).",
|
|
210
235
|
},
|
|
236
|
+
"change.resolve": {
|
|
237
|
+
// u3 (tot-merge-conflict-resolution-ux): one-click conflict resolution via
|
|
238
|
+
// candidate_refresh. Same admission ladder as change.reject (resolveShipSession
|
|
239
|
+
// → ship-on-behalf floor), so the same actor cohort. `strategy` is a
|
|
240
|
+
// low-cardinality enum (ours|theirs|merge), safe to render.
|
|
241
|
+
actors: ["admin"], source: "ui", sinks: LIFECYCLE,
|
|
242
|
+
argsAllow: ["changeId"], renderedAllow: ["strategy"],
|
|
243
|
+
desc: "A not-mergeable change was resolved by rebuilding it from the base tip under a strategy.",
|
|
244
|
+
},
|
|
211
245
|
"change.ship": {
|
|
212
246
|
// D6: the admin/cockpit "Publish" (go-live) handler admits the store OWNER
|
|
213
247
|
// (merchant), ToT staff (admin), a ship-on-behalf developer (dev), AND the
|
|
@@ -247,6 +281,24 @@ export const ACTION_CATALOG = {
|
|
|
247
281
|
argsAllow: ["changeId"], renderedAllow: [],
|
|
248
282
|
desc: "A change was integrated into the shared preview.",
|
|
249
283
|
},
|
|
284
|
+
"change.rebuild": {
|
|
285
|
+
// u15: retry a dead aggregate rebuild without a new merge (/api/changes/rebuild).
|
|
286
|
+
// Same admission ladder as change.integrate/revert (resolveShipSession) — the
|
|
287
|
+
// route gates on the SAME seam, so the emitter cohort matches. No changeId: a
|
|
288
|
+
// rebuild retries the tenant's most recent integration attempt, not one candidate.
|
|
289
|
+
actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
|
|
290
|
+
argsAllow: [], renderedAllow: [],
|
|
291
|
+
desc: "A dead aggregate rebuild was retried without a new merge.",
|
|
292
|
+
},
|
|
293
|
+
"change.resync": {
|
|
294
|
+
// u12: advance the shared preview aggregate to an out-of-band branch tip
|
|
295
|
+
// without a candidate/<changeId> PR (/api/changes/resync). Same admission
|
|
296
|
+
// ladder as change.integrate/rebuild (resolveShipSession). No changeId: a
|
|
297
|
+
// resync pins an operator-supplied branch/sha, not one candidate.
|
|
298
|
+
actors: ["admin", "merchant", "dev", "agent"], source: "ui", sinks: LIFECYCLE,
|
|
299
|
+
argsAllow: [], renderedAllow: [],
|
|
300
|
+
desc: "The shared preview aggregate was resynced to an out-of-band branch tip.",
|
|
301
|
+
},
|
|
250
302
|
"change.transition": {
|
|
251
303
|
// Generic SDLC transition for states without a dedicated verb above
|
|
252
304
|
// (triage↔in_progress↔ready_for_review↔announced↔closed).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* D1 (edge SQLite) catalog read-model — schema + row projections (the one-store
|
|
3
|
-
* target
|
|
3
|
+
* target). The storefront reads search, faceted
|
|
4
4
|
* listing, and full products from D1; the ETL emits the rows. This is the shared
|
|
5
5
|
* contract both sides agree on.
|
|
6
6
|
*
|
|
@@ -142,6 +142,8 @@ export interface EnvResolutionInput {
|
|
|
142
142
|
/** Platform preview-signing secret (env), if configured. */
|
|
143
143
|
secret?: string | null;
|
|
144
144
|
nowSeconds: number;
|
|
145
|
+
/** Request pathname — used only to exempt the token dispenser itself (see {@link isPreviewDispatcherPath}). */
|
|
146
|
+
pathname?: string;
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export interface EnvResolution {
|
|
@@ -152,16 +154,37 @@ export interface EnvResolution {
|
|
|
152
154
|
reject?: { status: number; message: string };
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The bare tenant-preview DISPATCHER path (`/<tenant>/preview` or
|
|
159
|
+
* `/<tenant>/preview/`, e.g. `pages/preview/index.astro`, tenant-first) — the page
|
|
160
|
+
* whose ENTIRE job is to mint a fresh `__preview` token and hand it to the
|
|
161
|
+
* visitor. On a preview-prefixed host this path would otherwise hit the SAME
|
|
162
|
+
* "no token -> 401" gate `resolveCustomizationEnv` enforces for every other
|
|
163
|
+
* path, deadlocking a signed-in-but-token-less visitor: they can never reach
|
|
164
|
+
* the one page that gives them a token. Distinct from `isCandidatePreviewPath`
|
|
165
|
+
* (loginGate.ts) — that exempts several preview routes from the LOGIN wall; this
|
|
166
|
+
* exempts exactly ONE route from the TOKEN wall. `<tenant>` is required to look
|
|
167
|
+
* like a real tenant id (contains a `.`) so this never accidentally matches a
|
|
168
|
+
* sibling route. Matched on the pre-strip request path.
|
|
169
|
+
*/
|
|
170
|
+
export function isPreviewDispatcherPath(pathname: string): boolean {
|
|
171
|
+
const m = pathname.match(/^\/([^/]+)\/preview\/?$/);
|
|
172
|
+
return m != null && m[1]!.includes(".");
|
|
173
|
+
}
|
|
174
|
+
|
|
155
175
|
/**
|
|
156
176
|
* Decide which customization environment to serve. Production hosts always get
|
|
157
177
|
* `live` and never consult a token. Preview hosts are auth-gated: a valid,
|
|
158
178
|
* unexpired, tenant-matching token yields `test` (noindex); anything else is
|
|
159
|
-
* rejected.
|
|
179
|
+
* rejected. The one exception is the token-dispenser path itself (see
|
|
180
|
+
* {@link isPreviewDispatcherPath}) — it renders the dispatcher's own minimal
|
|
181
|
+
* page, never tenant content, so it's let through as `live`/no-token rather
|
|
182
|
+
* than 401ing on the very request that would otherwise fetch it a token.
|
|
160
183
|
*/
|
|
161
184
|
export async function resolveCustomizationEnv(
|
|
162
185
|
input: EnvResolutionInput,
|
|
163
186
|
): Promise<EnvResolution> {
|
|
164
|
-
if (!input.isPreviewHost) {
|
|
187
|
+
if (!input.isPreviewHost || isPreviewDispatcherPath(input.pathname ?? "")) {
|
|
165
188
|
return { env: "live", noindex: false };
|
|
166
189
|
}
|
|
167
190
|
if (!input.secret) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type ChannelName,
|
|
16
16
|
type PointerRecord,
|
|
17
17
|
type SiteVersionManifest,
|
|
18
|
+
type SiteVersionSummary,
|
|
18
19
|
} from "./customization-versioning.js";
|
|
19
20
|
import type { CustomizationEnv } from "./customization-preview.js";
|
|
20
21
|
|
|
@@ -93,6 +94,27 @@ export async function resolveChannelVersion(
|
|
|
93
94
|
return pointer?.channels?.[channel] ?? null;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The static-bundle `treeDigest` a channel currently serves (ADR 0012), or null
|
|
99
|
+
* when the tenant has no digest companion for that channel (a hosted tenant — it
|
|
100
|
+
* sets only `channels`, never `channelDigests`).
|
|
101
|
+
*
|
|
102
|
+
* This is the RECONCILED read the digest-addressed serving path
|
|
103
|
+
* (`static-bundle.ts`), the pointer verify (`createPointerVerifyTarget`), and
|
|
104
|
+
* the pointer-served "current" read (`resolveStaticLiveDigestFromPointer`) use
|
|
105
|
+
* INSTEAD of `resolveChannelVersion` for a static tenant: the versionId
|
|
106
|
+
* (`channels[ch]`, the Gitea sha) is NOT the R2 key — the treeDigest is, and it
|
|
107
|
+
* lives here. Reads the pointer fresh; never throws.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveChannelDigest(
|
|
110
|
+
get: KvGet,
|
|
111
|
+
tenantId: string,
|
|
112
|
+
channel: ChannelName,
|
|
113
|
+
): Promise<string | null> {
|
|
114
|
+
const pointer = await readPointer(get, tenantId);
|
|
115
|
+
return pointer?.channelDigests?.[channel] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
/** Read one immutable site-version manifest, or null. Never throws. */
|
|
97
119
|
export async function readSiteVersion(
|
|
98
120
|
get: KvGet,
|
|
@@ -108,12 +130,112 @@ export async function readSiteVersion(
|
|
|
108
130
|
}
|
|
109
131
|
}
|
|
110
132
|
|
|
133
|
+
/** A manifest is BUILT (servable) when it exists AND materialized ≥1 artifact. */
|
|
134
|
+
function isBuiltManifest(m: SiteVersionManifest | null): m is SiteVersionManifest {
|
|
135
|
+
return !!m && Object.keys(m.artifacts ?? {}).length > 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read a tenant's version-history index (list of summaries), NEWEST first — the
|
|
140
|
+
* runtime read-side view of the same `siteVersionIndexKey` the KV store appends
|
|
141
|
+
* to (stored oldest-first). Never throws — malformed/absent → `[]`.
|
|
142
|
+
*/
|
|
143
|
+
export async function readSiteVersionIndex(
|
|
144
|
+
get: KvGet,
|
|
145
|
+
tenantId: string,
|
|
146
|
+
): Promise<SiteVersionSummary[]> {
|
|
147
|
+
const raw = await get(siteVersionIndexKey(tenantId));
|
|
148
|
+
if (!raw) return [];
|
|
149
|
+
try {
|
|
150
|
+
return (JSON.parse(raw) as SiteVersionSummary[]).slice().reverse();
|
|
151
|
+
} catch {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The outcome of self-healing a channel pointer to a SERVABLE version. */
|
|
157
|
+
export interface ServableChannelVersion {
|
|
158
|
+
/**
|
|
159
|
+
* The versionId to actually SERVE from: the pointed version when it is built,
|
|
160
|
+
* else the newest BUILT version in history (self-heal), else `null` (nothing
|
|
161
|
+
* built at all — the caller falls back to build-time, still never a bare 404).
|
|
162
|
+
*/
|
|
163
|
+
versionId: string | null;
|
|
164
|
+
/** The versionId the channel pointer NAMES (may dangle at an unbuilt version). */
|
|
165
|
+
pointedVersionId: string | null;
|
|
166
|
+
/**
|
|
167
|
+
* True when the pointer named a version with no built manifest — so the read
|
|
168
|
+
* was self-healed onto an older built version (or found none). Signals the
|
|
169
|
+
* caller to enqueue a rebuild of the pointed version.
|
|
170
|
+
*/
|
|
171
|
+
dangling: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Optional seam so a self-healing read can enqueue a rebuild of the dangling
|
|
175
|
+
* version. Best-effort/fail-open: never awaited into the serve path, never
|
|
176
|
+
* throws through it. Absent ⇒ pure resolution (no side effect). */
|
|
177
|
+
export type EnqueueRebuild = (info: {
|
|
178
|
+
tenantId: string;
|
|
179
|
+
channel: ChannelName;
|
|
180
|
+
versionId: string;
|
|
181
|
+
}) => void;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve the SERVABLE version for a channel, self-healing a dangling pointer.
|
|
185
|
+
*
|
|
186
|
+
* A channel pointer can name a version that was never built (or whose manifest
|
|
187
|
+
* was evicted). Serving that naively empties/404s the whole surface. Instead: if
|
|
188
|
+
* the pointed version has no built manifest, fall back to the newest version in
|
|
189
|
+
* history that DOES, and flag `dangling` so the caller can enqueue a rebuild.
|
|
190
|
+
* Only kicks in when the pointed
|
|
191
|
+
* manifest is entirely absent/unbuilt — a present manifest that merely omits one
|
|
192
|
+
* path stays authoritative (unchanged behavior). Reads fresh; never throws.
|
|
193
|
+
*/
|
|
194
|
+
export async function resolveServableChannelVersion(
|
|
195
|
+
get: KvGet,
|
|
196
|
+
tenantId: string,
|
|
197
|
+
channel: ChannelName,
|
|
198
|
+
enqueueRebuild?: EnqueueRebuild,
|
|
199
|
+
): Promise<ServableChannelVersion> {
|
|
200
|
+
const pointedVersionId = await resolveChannelVersion(get, tenantId, channel);
|
|
201
|
+
if (!pointedVersionId) {
|
|
202
|
+
return { versionId: null, pointedVersionId: null, dangling: false };
|
|
203
|
+
}
|
|
204
|
+
const pointed = await readSiteVersion(get, tenantId, pointedVersionId);
|
|
205
|
+
if (isBuiltManifest(pointed)) {
|
|
206
|
+
return { versionId: pointedVersionId, pointedVersionId, dangling: false };
|
|
207
|
+
}
|
|
208
|
+
// Dangling: the pointer names an unbuilt/absent version. Self-heal onto the
|
|
209
|
+
// newest BUILT version in history so the surface keeps rendering last-good.
|
|
210
|
+
let healed: string | null = null;
|
|
211
|
+
for (const summary of await readSiteVersionIndex(get, tenantId)) {
|
|
212
|
+
if (summary.versionId === pointedVersionId) continue;
|
|
213
|
+
if (isBuiltManifest(await readSiteVersion(get, tenantId, summary.versionId))) {
|
|
214
|
+
healed = summary.versionId;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (enqueueRebuild) {
|
|
219
|
+
try {
|
|
220
|
+
enqueueRebuild({ tenantId, channel, versionId: pointedVersionId });
|
|
221
|
+
} catch {
|
|
222
|
+
/* fail-open: enqueue is best-effort, never breaks the read */
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { versionId: healed, pointedVersionId, dangling: true };
|
|
226
|
+
}
|
|
227
|
+
|
|
111
228
|
/**
|
|
112
229
|
* Resolve a single artifact's content via the VERSIONED path:
|
|
113
230
|
* channel → versionId → site-version manifest → content hash → bytes. Returns
|
|
114
231
|
* null (so the caller falls back to build-time) when the tenant has no channel
|
|
115
|
-
* pointer, the
|
|
116
|
-
*
|
|
232
|
+
* pointer, the path isn't in the snapshot, or the content is gone. Never throws.
|
|
233
|
+
*
|
|
234
|
+
* Self-healing (u3): when the channel pointer DANGLES (names an unbuilt/absent
|
|
235
|
+
* version), the artifact is read from the newest built version instead of
|
|
236
|
+
* returning null for every path — so a dangling `preview`/`live` pointer serves
|
|
237
|
+
* the last built site rather than 404ing the whole surface. See
|
|
238
|
+
* {@link resolveServableChannelVersion}.
|
|
117
239
|
*/
|
|
118
240
|
export async function readVersionedArtifact(
|
|
119
241
|
get: KvGet,
|
|
@@ -121,7 +243,7 @@ export async function readVersionedArtifact(
|
|
|
121
243
|
channel: ChannelName,
|
|
122
244
|
path: string,
|
|
123
245
|
): Promise<string | null> {
|
|
124
|
-
const versionId = await
|
|
246
|
+
const { versionId } = await resolveServableChannelVersion(get, tenantId, channel);
|
|
125
247
|
if (!versionId) return null;
|
|
126
248
|
const manifest = await readSiteVersion(get, tenantId, versionId);
|
|
127
249
|
const contentHash = manifest?.artifacts?.[path];
|
|
@@ -37,6 +37,15 @@ export type ChannelPointers = Partial<Record<ChannelName, string>>;
|
|
|
37
37
|
* - `channels`: the versioned-publish channel pointers (`live`/`preview` →
|
|
38
38
|
* a single site `versionId`). Additive + optional; when present it is
|
|
39
39
|
* authoritative for that channel and shadows the legacy selection.
|
|
40
|
+
* - `channelDigests`: (ADR 0012) the RECONCILED static-serving companion to
|
|
41
|
+
* `channels`. For a static-publish tenant, `channels[ch]` still holds the site
|
|
42
|
+
* `versionId` (the tenant Gitea commit sha — the hosted contract), while
|
|
43
|
+
* `channelDigests[ch]` holds that same publish's bundle `treeDigest` (the
|
|
44
|
+
* content-addressed R2 key prefix `versions/<digest>/` `static-bundle.ts`
|
|
45
|
+
* reads). The two are DISTINCT namespaces — the Gitea sha is NOT the treeDigest
|
|
46
|
+
* — and are written in the SAME CAS pointer write (`setChannel`), so they can
|
|
47
|
+
* never drift. Absent for a hosted tenant (which only ever sets/reads
|
|
48
|
+
* `channels`).
|
|
40
49
|
* - `rev`: optimistic-concurrency token, incremented on every successful write.
|
|
41
50
|
*/
|
|
42
51
|
export interface PointerRecord {
|
|
@@ -45,6 +54,7 @@ export interface PointerRecord {
|
|
|
45
54
|
live: Selection;
|
|
46
55
|
test: Selection;
|
|
47
56
|
channels?: ChannelPointers;
|
|
57
|
+
channelDigests?: ChannelPointers;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export function emptyPointer(tenantId: string): PointerRecord {
|
|
@@ -36,6 +36,7 @@ export * from "./raw-html-policy.js";
|
|
|
36
36
|
export * from "./review-trust-proof.js";
|
|
37
37
|
export * from "./compliance/index.js";
|
|
38
38
|
export * from "./tenant-assets.js";
|
|
39
|
+
export * from "./static-bundle.js";
|
|
39
40
|
export * from "./binary-path.js";
|
|
40
41
|
export * from "./hot-standby-readiness.js";
|
|
41
42
|
export * from "./hash.js";
|