@tokenoftrust/storefront-runner 2.0.18 → 2.0.20

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.
Files changed (46) hide show
  1. package/apps/storefront/env.d.ts +5 -0
  2. package/apps/storefront/migrations-apps/0013_publication_space_editorial.sql +64 -0
  3. package/apps/storefront/src/config/publicationSpaceRegistry.ts +19 -0
  4. package/apps/storefront/src/lib/auth/brokerAssertion.ts +18 -0
  5. package/apps/storefront/src/lib/auth/identityToken.ts +11 -1
  6. package/apps/storefront/src/lib/auth/internalContentAuthority.ts +153 -0
  7. package/apps/storefront/src/lib/auth/session.ts +3 -0
  8. package/apps/storefront/src/lib/blog/collection.ts +24 -18
  9. package/apps/storefront/src/lib/blog/provider.ts +28 -5
  10. package/apps/storefront/src/lib/blog/reactions.ts +4 -2
  11. package/apps/storefront/src/lib/blog/types.ts +1 -1
  12. package/apps/storefront/src/lib/blog/viewCounts.ts +3 -1
  13. package/apps/storefront/src/lib/d1/catalog.ts +18 -1
  14. package/apps/storefront/src/lib/d1/schema-apps.ts +93 -0
  15. package/apps/storefront/src/lib/edgeCache.ts +19 -1
  16. package/apps/storefront/src/lib/publication/contentProvider.ts +159 -0
  17. package/apps/storefront/src/lib/publication/runtimeSource.ts +194 -0
  18. package/apps/storefront/src/lib/the-build/provider.ts +2 -2
  19. package/apps/storefront/src/middleware/controlPlane.ts +24 -3
  20. package/apps/storefront/src/middleware/index.ts +125 -22
  21. package/apps/storefront/src/middleware/renderChain.ts +109 -56
  22. package/apps/storefront/src/middleware/routeBranch.ts +39 -7
  23. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -0
  24. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +1 -0
  25. package/apps/storefront/src/pages/api/auth/verify.ts +1 -0
  26. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +4 -1
  27. package/apps/storefront/src/pages/api/rum/vitals.ts +4 -1
  28. package/apps/storefront/src/pages/blog/[slug].astro +5 -4
  29. package/apps/storefront/src/pages/blog/author/[author].astro +2 -2
  30. package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
  31. package/apps/storefront/src/pages/blog/index.astro +2 -2
  32. package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
  33. package/apps/storefront/src/pages/llms.txt.ts +2 -2
  34. package/apps/storefront/src/pages/sitemap.xml.ts +8 -4
  35. package/apps/storefront/src/pages/the-build/[slug].astro +6 -3
  36. package/apps/storefront/src/pages/the-build/index.astro +2 -2
  37. package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
  38. package/package.json +1 -1
  39. package/packages/public-runtime/package.json +2 -1
  40. package/packages/public-runtime/src/blog-visibility.d.mts +8 -0
  41. package/packages/public-runtime/src/blog-visibility.mjs +32 -0
  42. package/packages/public-runtime/src/index.ts +3 -0
  43. package/packages/public-runtime/src/publication-artifact.ts +20 -3
  44. package/packages/public-runtime/src/publication-reader.ts +187 -0
  45. package/packages/public-runtime/src/publication-routing.ts +246 -0
  46. package/packages/public-runtime/src/publication-version.ts +15 -0
@@ -250,6 +250,99 @@ export const testDestinationDeliveries = sqliteTable(
250
250
  }),
251
251
  );
252
252
 
253
+ // --- Publication-space editorial control -----------------------------------
254
+ // Immutable reviewed versions plus one CAS pointer/audit chain per space. The
255
+ // API always derives tenant+space from its authenticated route; the composite
256
+ // keys make cross-tenant aliases impossible in persistence.
257
+ export const publicationSpaceVersions = sqliteTable(
258
+ "publication_space_versions",
259
+ {
260
+ tenantId: text("tenant_id").notNull(),
261
+ spaceId: text("space_id").notNull(),
262
+ versionId: text("version_id").notNull(),
263
+ record: text("record").notNull(),
264
+ },
265
+ (t) => ({ pk: primaryKey({ columns: [t.tenantId, t.spaceId, t.versionId] }) }),
266
+ );
267
+
268
+ export const publicationSpaceReviews = sqliteTable(
269
+ "publication_space_reviews",
270
+ {
271
+ tenantId: text("tenant_id").notNull(),
272
+ spaceId: text("space_id").notNull(),
273
+ versionId: text("version_id").notNull(),
274
+ record: text("record").notNull(),
275
+ },
276
+ (t) => ({
277
+ pk: primaryKey({ columns: [t.tenantId, t.spaceId, t.versionId] }),
278
+ idxTenant: index("idx_publication_space_reviews_tenant").on(t.tenantId, t.spaceId),
279
+ }),
280
+ );
281
+
282
+ export const publicationSpacePointers = sqliteTable(
283
+ "publication_space_pointers",
284
+ {
285
+ tenantId: text("tenant_id").notNull(),
286
+ spaceId: text("space_id").notNull(),
287
+ channel: text("channel").notNull(),
288
+ revision: integer("revision").notNull(),
289
+ record: text("record").notNull(),
290
+ },
291
+ (t) => ({ pk: primaryKey({ columns: [t.tenantId, t.spaceId, t.channel] }) }),
292
+ );
293
+
294
+ export const publicationSpaceTransitions = sqliteTable(
295
+ "publication_space_transitions",
296
+ {
297
+ tenantId: text("tenant_id").notNull(),
298
+ spaceId: text("space_id").notNull(),
299
+ transitionId: text("transition_id").notNull(),
300
+ channel: text("channel").notNull(),
301
+ revision: integer("revision").notNull(),
302
+ record: text("record").notNull(),
303
+ },
304
+ (t) => ({
305
+ pk: primaryKey({ columns: [t.tenantId, t.spaceId, t.transitionId] }),
306
+ uqRevision: uniqueIndex("uq_publication_space_transition_revision").on(
307
+ t.tenantId,
308
+ t.spaceId,
309
+ t.channel,
310
+ t.revision,
311
+ ),
312
+ idxTenant: index("idx_publication_space_transitions_tenant").on(
313
+ t.tenantId,
314
+ t.spaceId,
315
+ t.channel,
316
+ ),
317
+ }),
318
+ );
319
+
320
+ export const publicationSpaceProjections = sqliteTable(
321
+ "publication_space_projections",
322
+ {
323
+ tenantId: text("tenant_id").notNull(),
324
+ spaceId: text("space_id").notNull(),
325
+ channel: text("channel").notNull(),
326
+ revision: integer("revision").notNull(),
327
+ transitionId: text("transition_id").notNull(),
328
+ operation: text("operation").notNull(),
329
+ provider: text("provider").notNull(),
330
+ status: text("status").notNull().default("pending"),
331
+ attempts: integer("attempts").notNull().default(0),
332
+ lastError: text("last_error"),
333
+ record: text("record").notNull(),
334
+ },
335
+ (t) => ({
336
+ pk: primaryKey({ columns: [t.tenantId, t.spaceId, t.channel, t.revision] }),
337
+ uqTransition: uniqueIndex("uq_publication_space_projection_transition").on(
338
+ t.tenantId, t.spaceId, t.transitionId,
339
+ ),
340
+ idxPending: index("idx_publication_space_projections_pending").on(
341
+ t.tenantId, t.status, t.revision,
342
+ ),
343
+ }),
344
+ );
345
+
253
346
  // --- Webhook event spine ----------------------------------------------------
254
347
  // Immutable event log. `id` is the semantic event id we mint — distinct from
255
348
  // any per-delivery wire envelope id (see `webhookDeliveries.deliveryId` below).
@@ -64,6 +64,14 @@ export function pageCacheKey(
64
64
  search: string,
65
65
  version: number,
66
66
  planRevision?: string,
67
+ publication?: {
68
+ spaceId: string;
69
+ channel: "preview" | "live";
70
+ pointerRevision: number;
71
+ versionId: string;
72
+ artifactDigest: string;
73
+ publicationTimestamp: string | null;
74
+ },
67
75
  ): Request {
68
76
  // A stored page carries the CSP header it was rendered with, INCLUDING any
69
77
  // version-scoped integration origins. Folding the resolved `planRevision` into
@@ -73,8 +81,18 @@ export function pageCacheKey(
73
81
  // integration plane resolved) the key is byte-identical to the non-integration
74
82
  // form, so a plain tenant pays nothing and its cache is never busted.
75
83
  const planSegment = planRevision ? `/pr:${planRevision}` : "";
84
+ const publicationSegment = publication
85
+ ? `/publication:${encodeURIComponent([
86
+ publication.spaceId,
87
+ publication.channel,
88
+ publication.pointerRevision,
89
+ publication.versionId,
90
+ publication.artifactDigest,
91
+ publication.publicationTimestamp ?? "unclocked",
92
+ ].join(":"))}`
93
+ : "";
76
94
  return new Request(
77
- `${CACHE_ORIGIN}/${tenantId}/v${version}${planSegment}${pathname}${search}`,
95
+ `${CACHE_ORIGIN}/${tenantId}/v${version}${planSegment}${publicationSegment}${pathname}${search}`,
78
96
  );
79
97
  }
80
98
 
@@ -0,0 +1,159 @@
1
+ import type {
2
+ OpenPublicationArtifactV1,
3
+ PublicationRouteRenderPlanV1,
4
+ } from "@tot/public-runtime";
5
+ import type { EditorialPage, HomeContent } from "@/lib/storyblok/content-model.js";
6
+ import type { ChromeConfig } from "@/lib/chrome/model.js";
7
+ import { parseChromeConfig } from "@/lib/chrome/parseConfig.js";
8
+ import { parseChromeAssignments } from "@/lib/chrome/model.js";
9
+ import { renderRawChrome } from "@/lib/rawChrome.js";
10
+ import { stampTrackingIdsAtPublish } from "@/lib/tracking/publishSafetyNet.js";
11
+ import type { ContentProvider } from "@/lib/storyblok/provider.js";
12
+
13
+ const EMPTY_CHROME_CONFIG: ChromeConfig = {
14
+ header: { variant: "primary", brand: { label: "" }, nav: [] },
15
+ footer: { variant: "default", columns: [] },
16
+ };
17
+
18
+ function parseJson<T>(raw: string | null): T | null {
19
+ if (raw === null) return null;
20
+ try {
21
+ return JSON.parse(raw) as T;
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function stripSlashes(value: string): string {
28
+ return value.replace(/^\/+|\/+$/g, "");
29
+ }
30
+
31
+ /**
32
+ * Map the canonical Astro catch-all slug back to the already-resolved source
33
+ * root. The plan, not string-prefix guessing, owns which mount is stripped.
34
+ */
35
+ function selectedPageSlug(plan: PublicationRouteRenderPlanV1, slug: string): string | null {
36
+ const requested = stripSlashes(slug);
37
+ const canonical = stripSlashes(plan.canonicalPath);
38
+ if (requested !== canonical) return null;
39
+ return stripSlashes(plan.contentPath);
40
+ }
41
+
42
+ function canonicalSlugForMount(plan: PublicationRouteRenderPlanV1, sourceSlug: string): string {
43
+ const mount = stripSlashes(plan.manifest.routes.canonicalMount);
44
+ const source = stripSlashes(sourceSlug);
45
+ return [mount, source].filter(Boolean).join("/");
46
+ }
47
+
48
+ function rebasePage(
49
+ plan: PublicationRouteRenderPlanV1,
50
+ page: EditorialPage,
51
+ canonicalSlug = canonicalSlugForMount(plan, page.slug),
52
+ ): EditorialPage {
53
+ return { ...page, slug: canonicalSlug };
54
+ }
55
+
56
+ /**
57
+ * Existing Storefront renderer adapter for one already-opened publication
58
+ * version. Selected page/blog artifacts never fall through to another source;
59
+ * a mounted space reuses only the root space's shared chrome/home services.
60
+ */
61
+ export class PublicationSpaceContentProvider implements ContentProvider {
62
+ constructor(
63
+ private readonly sharedRoot: ContentProvider,
64
+ private readonly publication: OpenPublicationArtifactV1,
65
+ ) {}
66
+
67
+ private get plan(): PublicationRouteRenderPlanV1 {
68
+ return this.publication.selection;
69
+ }
70
+
71
+ private get ownsRoot(): boolean {
72
+ return this.plan.manifest.routes.canonicalMount === "/";
73
+ }
74
+
75
+ async getHome(): Promise<HomeContent> {
76
+ if (!this.ownsRoot) return this.sharedRoot.getHome();
77
+ const home = parseJson<HomeContent>(this.publication.readContent("home.json"));
78
+ if (!home) throw new Error("selected root publication home is unavailable");
79
+ return home;
80
+ }
81
+
82
+ async getHomeHtml(): Promise<string | null> {
83
+ if (!this.ownsRoot) return this.sharedRoot.getHomeHtml();
84
+ const raw = this.publication.readContent("home.html");
85
+ return raw === null ? null : stampTrackingIdsAtPublish(raw);
86
+ }
87
+
88
+ async getPageHtml(slug: string): Promise<string | null> {
89
+ const selected = selectedPageSlug(this.plan, slug);
90
+ if (selected === null) return null;
91
+ const raw = this.publication.readContent(
92
+ selected ? `pages-html/${selected}.html` : "home.html",
93
+ );
94
+ return raw === null ? null : stampTrackingIdsAtPublish(raw);
95
+ }
96
+
97
+ async listPageHtmlSlugs(): Promise<string[]> {
98
+ return this.publication.listContentPaths("pages-html/")
99
+ .filter((path) => path.endsWith(".html"))
100
+ .map((path) => canonicalSlugForMount(
101
+ this.plan,
102
+ path.slice("pages-html/".length, -".html".length),
103
+ ));
104
+ }
105
+
106
+ async getChromeHtml(currentPath?: string): Promise<string | null> {
107
+ if (!this.ownsRoot) return this.sharedRoot.getChromeHtml(currentPath);
108
+ const html = this.publication.readContent("chrome.html");
109
+ if (html !== null) return stampTrackingIdsAtPublish(html);
110
+ const config = parseChromeConfig(parseJson(this.publication.readContent("chrome.json")));
111
+ if (!config) return null;
112
+ const assignments = parseChromeAssignments(
113
+ parseJson(this.publication.readContent("chrome-assignments.json")),
114
+ );
115
+ return renderRawChrome(config, currentPath, assignments);
116
+ }
117
+
118
+ async getChrome(): Promise<ChromeConfig> {
119
+ if (!this.ownsRoot) return this.sharedRoot.getChrome();
120
+ return parseChromeConfig(parseJson(this.publication.readContent("chrome.json"))) ??
121
+ EMPTY_CHROME_CONFIG;
122
+ }
123
+
124
+ async getPage(slug: string): Promise<EditorialPage | null> {
125
+ const selected = selectedPageSlug(this.plan, slug);
126
+ if (!selected) return null;
127
+ const page = (await this.sourcePages()).find((candidate) => candidate.slug === selected);
128
+ return page ? rebasePage(this.plan, page, stripSlashes(this.plan.canonicalPath)) : null;
129
+ }
130
+
131
+ async listPages(): Promise<EditorialPage[]> {
132
+ return (await this.sourcePages()).map((page) => rebasePage(this.plan, page));
133
+ }
134
+
135
+ private async sourcePages(): Promise<EditorialPage[]> {
136
+ return this.publication.listContentPaths("pages/")
137
+ .filter((path) => path.endsWith(".json"))
138
+ .map((path) => parseJson<EditorialPage>(this.publication.readContent(path)))
139
+ .filter((page): page is EditorialPage => page !== null);
140
+ }
141
+
142
+ async readArtifact(relative: string): Promise<string | null> {
143
+ return this.publication.readContent(relative);
144
+ }
145
+
146
+ async listArtifactPaths(prefix = ""): Promise<string[]> {
147
+ return this.publication.listContentPaths(prefix);
148
+ }
149
+ }
150
+
151
+ /** Bind an opened exact publication into the existing renderer provider chain. */
152
+ export function bindPublicationSpaceContentProviderV1(
153
+ sharedRoot: ContentProvider,
154
+ publication?: OpenPublicationArtifactV1,
155
+ ): ContentProvider {
156
+ return publication
157
+ ? new PublicationSpaceContentProvider(sharedRoot, publication)
158
+ : sharedRoot;
159
+ }
@@ -0,0 +1,194 @@
1
+ import {
2
+ exactPublicationTimestamp,
3
+ openPublicationArtifactV1,
4
+ parsePublicationVersionStateV1Json,
5
+ publicationRoutePlanIsCurrentV1,
6
+ publicationVersionStateKey,
7
+ type OpenPublicationArtifactV1,
8
+ type PublicationArtifactBucketV1,
9
+ type PublicationArtifactOpenFailureCodeV1,
10
+ type PublicationChannelV1,
11
+ type PublicationChannelPointerV1,
12
+ type PublicationRoutePlanV1,
13
+ type RouteTierIndexV1,
14
+ } from "@tot/public-runtime";
15
+
16
+ export interface PublicationStateStoreV1 {
17
+ get(key: string): Promise<string | null>;
18
+ }
19
+
20
+ export interface PublicationPageCacheScopeV1 {
21
+ spaceId: string;
22
+ channel: PublicationChannelV1;
23
+ pointerRevision: number;
24
+ versionId: string;
25
+ artifactDigest: string;
26
+ publicationTimestamp: string | null;
27
+ }
28
+
29
+ export interface PublicationRuntimeClockV1 {
30
+ version: 1;
31
+ tenant: string;
32
+ spaceId: string;
33
+ channel: PublicationChannelV1;
34
+ revision: number;
35
+ versionId: string;
36
+ sourceSha: string;
37
+ artifactDigest: string;
38
+ publicationTimestamp: string;
39
+ }
40
+
41
+ export function publicationRuntimeClockKeyV1(
42
+ tenant: string,
43
+ spaceId: string,
44
+ channel: PublicationChannelV1,
45
+ revision: number,
46
+ ): string {
47
+ return `cust:publication-clock:${tenant}:${spaceId}:${channel}:${revision}`;
48
+ }
49
+
50
+ export function publicationRuntimeClockV1(input: {
51
+ pointer: PublicationChannelPointerV1;
52
+ publicationTimestamp: string;
53
+ }): PublicationRuntimeClockV1 | null {
54
+ const publicationTimestamp = exactPublicationTimestamp(input.publicationTimestamp);
55
+ const pointer = input.pointer;
56
+ if (!publicationTimestamp || !Number.isSafeInteger(pointer.revision) || pointer.revision < 1) return null;
57
+ return {
58
+ version: 1,
59
+ tenant: pointer.tenant,
60
+ spaceId: pointer.spaceId,
61
+ channel: pointer.channel,
62
+ revision: pointer.revision,
63
+ versionId: pointer.versionId,
64
+ sourceSha: pointer.sourceSha,
65
+ artifactDigest: pointer.artifactDigest,
66
+ publicationTimestamp,
67
+ };
68
+ }
69
+
70
+ export function parsePublicationRuntimeClockV1(json: string): PublicationRuntimeClockV1 | null {
71
+ try {
72
+ const value = JSON.parse(json) as Record<string, unknown>;
73
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
74
+ Object.keys(value).sort().join("\0") !==
75
+ ["artifactDigest", "channel", "publicationTimestamp", "revision", "sourceSha", "spaceId", "tenant", "version", "versionId"].sort().join("\0") ||
76
+ value.version !== 1 || typeof value.tenant !== "string" || typeof value.spaceId !== "string" ||
77
+ (value.channel !== "live" && value.channel !== "preview") ||
78
+ !Number.isSafeInteger(value.revision) || Number(value.revision) < 1 ||
79
+ typeof value.versionId !== "string" || typeof value.sourceSha !== "string" ||
80
+ typeof value.artifactDigest !== "string" || !exactPublicationTimestamp(value.publicationTimestamp)) return null;
81
+ return value as unknown as PublicationRuntimeClockV1;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ export function publicationRuntimeClockMatchesStateV1(input: {
88
+ clock: PublicationRuntimeClockV1;
89
+ pointer: PublicationChannelPointerV1;
90
+ }): boolean {
91
+ const expected = publicationRuntimeClockV1({
92
+ pointer: input.pointer,
93
+ publicationTimestamp: input.clock.publicationTimestamp,
94
+ });
95
+ return !!expected && JSON.stringify(expected) === JSON.stringify(input.clock);
96
+ }
97
+
98
+ export type PublicationRuntimeSourceV1 =
99
+ | { status: "legacy-root" }
100
+ | {
101
+ status: "selected";
102
+ artifact: OpenPublicationArtifactV1;
103
+ cacheScope: PublicationPageCacheScopeV1;
104
+ /** Exact audited transition instant bound to this selected space revision. */
105
+ publicationTimestamp: string | null;
106
+ }
107
+ | { status: "unavailable"; code: PublicationArtifactOpenFailureCodeV1 };
108
+
109
+ /**
110
+ * Resolve one live request from the public KV state projection into its exact
111
+ * immutable artifact. A carried plan is checked against current route authority
112
+ * before state or object-store I/O. Only an unselected root keeps the legacy
113
+ * adapter; a mounted space never falls through to root content.
114
+ */
115
+ export async function resolvePublicationRuntimeSourceV1(input: {
116
+ index: RouteTierIndexV1;
117
+ requestPath: string;
118
+ plan: PublicationRoutePlanV1;
119
+ channel: PublicationChannelV1;
120
+ store?: PublicationStateStoreV1;
121
+ bucket?: PublicationArtifactBucketV1;
122
+ }): Promise<PublicationRuntimeSourceV1> {
123
+ if (input.plan.action !== "render") return { status: "legacy-root" };
124
+ if (!publicationRoutePlanIsCurrentV1(input.index, input.requestPath, input.plan)) {
125
+ return { status: "unavailable", code: "publication_route_authority_mismatch" };
126
+ }
127
+ const isRoot = input.plan.manifest.routes.canonicalMount === "/";
128
+ if (!input.store) {
129
+ return isRoot
130
+ ? { status: "legacy-root" }
131
+ : { status: "unavailable", code: "publication_channel_unselected" };
132
+ }
133
+
134
+ let stateJson: string | null;
135
+ try {
136
+ stateJson = await input.store.get(publicationVersionStateKey(input.plan.manifest.tenant));
137
+ } catch {
138
+ return { status: "unavailable", code: "publication_artifact_source_unavailable" };
139
+ }
140
+ if (stateJson === null) {
141
+ return isRoot
142
+ ? { status: "legacy-root" }
143
+ : { status: "unavailable", code: "publication_channel_unselected" };
144
+ }
145
+ const state = parsePublicationVersionStateV1Json(stateJson);
146
+ if (!state.ok) return { status: "unavailable", code: "publication_state_malformed" };
147
+ if (!input.bucket) {
148
+ return { status: "unavailable", code: "publication_artifact_source_unavailable" };
149
+ }
150
+ const opened = await openPublicationArtifactV1({
151
+ index: input.index,
152
+ requestPath: input.requestPath,
153
+ plan: input.plan,
154
+ state: state.value,
155
+ channel: input.channel,
156
+ bucket: input.bucket,
157
+ });
158
+ if (!opened.ok) return { status: "unavailable", code: opened.code };
159
+ const { selection } = opened.value;
160
+ let clock: PublicationRuntimeClockV1 | null = null;
161
+ if (selection.manifest.visibility === "public") {
162
+ try {
163
+ const clockJson = await input.store.get(publicationRuntimeClockKeyV1(
164
+ selection.pointer.tenant,
165
+ selection.pointer.spaceId,
166
+ selection.channel,
167
+ selection.pointer.revision,
168
+ ));
169
+ const candidate = clockJson ? parsePublicationRuntimeClockV1(clockJson) : null;
170
+ if (candidate && publicationRuntimeClockMatchesStateV1({
171
+ clock: candidate,
172
+ pointer: selection.pointer,
173
+ })) clock = candidate;
174
+ } catch {
175
+ // The content remains selectable, but scheduled visibility fails closed.
176
+ }
177
+ }
178
+ const publicationTimestamp = selection.manifest.visibility === "public"
179
+ ? clock?.publicationTimestamp ?? null
180
+ : exactPublicationTimestamp(selection.pointer.updatedAt);
181
+ return {
182
+ status: "selected",
183
+ artifact: opened.value,
184
+ publicationTimestamp,
185
+ cacheScope: {
186
+ spaceId: selection.manifest.id,
187
+ channel: selection.channel,
188
+ pointerRevision: selection.pointer.revision,
189
+ versionId: selection.publicationVersion.versionId,
190
+ artifactDigest: selection.publicationVersion.artifactDigest,
191
+ publicationTimestamp,
192
+ },
193
+ };
194
+ }
@@ -10,7 +10,7 @@ export const THE_BUILD: Collection = { id: "the-build", base: "/the-build" };
10
10
 
11
11
  export function listBuildPosts(
12
12
  tenantId: string,
13
- opts?: { includeUnpublished?: boolean; now?: string; content?: ContentProvider },
13
+ opts?: { includeUnpublished?: boolean; now?: string | null; content?: ContentProvider },
14
14
  ): Promise<BlogPostSummary[]> {
15
15
  return listPosts(tenantId, THE_BUILD.id, opts);
16
16
  }
@@ -18,7 +18,7 @@ export function listBuildPosts(
18
18
  export function getBuildPost(
19
19
  tenantId: string,
20
20
  slug: string,
21
- opts?: { includeUnpublished?: boolean; now?: string; content?: ContentProvider },
21
+ opts?: { includeUnpublished?: boolean; now?: string | null; content?: ContentProvider },
22
22
  ): Promise<BlogPost | null> {
23
23
  return getPost(tenantId, THE_BUILD.id, slug, opts);
24
24
  }
@@ -12,7 +12,14 @@ import { fromD1, type D1Like } from "@/lib/d1/catalog";
12
12
  import { readCurrentTenantConsentConfig } from "@/lib/apps/consentConfigRead";
13
13
  import { loadActiveManagedInstallDeclarations } from "@/lib/integration/managedInstallDeclarations";
14
14
  import { buildTenantConsentBroker, verifyPreviewToken } from "@tot/public-runtime";
15
- import type { ConsentSnapshot, IntegrationDeclarationInput, TenantConfig } from "@tot/public-runtime";
15
+ import type {
16
+ ConsentSnapshot,
17
+ IntegrationDeclarationInput,
18
+ PublicationArtifactBucketV1,
19
+ PublicationRoutePlanV1,
20
+ RouteTierIndexV1,
21
+ TenantConfig,
22
+ } from "@tot/public-runtime";
16
23
  import {
17
24
  evaluateLoginGate,
18
25
  isAuthPath,
@@ -27,6 +34,7 @@ import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
27
34
  import { gatedHoldingResponse } from "@/lib/auth/gatePage";
28
35
  import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
29
36
  import { runRenderChain } from "./renderChain.js";
37
+ import type { InternalContentGrant } from "@/lib/auth/internalContentAuthority";
30
38
 
31
39
  export interface ControlPlaneInput {
32
40
  context: APIContext;
@@ -38,11 +46,17 @@ export interface ControlPlaneInput {
38
46
  renderPath: string;
39
47
  cache: Cache | undefined;
40
48
  kv: KVNamespace | undefined;
49
+ routeTierIndex: RouteTierIndexV1;
50
+ publicationRequestPath: string;
51
+ publicationRoute: PublicationRoutePlanV1;
52
+ publicationBucket: PublicationArtifactBucketV1 | undefined;
41
53
  gateMode: GateMode;
42
54
  /** `DEMO_GATE_HOSTS`, raw. */
43
55
  demoGateHosts: string | null | undefined;
44
56
  /** True when this host is withheld behind sign-in — see `resolveMiddlewareBranchV1`. */
45
57
  hostLoginGated: boolean;
58
+ /** Exact per-request authorization for an F2-selected internal space. */
59
+ internalContentGrant?: InternalContentGrant;
46
60
  }
47
61
 
48
62
  export async function serveControlPlane(input: ControlPlaneInput): Promise<Response> {
@@ -57,7 +71,7 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
57
71
  // otherwise an unauthenticated visitor gets a "Coming soon" holding page with
58
72
  // a Sign-in button (auth paths are exempt so sign-in itself is reachable). The
59
73
  // grant `resource` is the tenant's appDomain — never input.
60
- if (input.hostLoginGated) {
74
+ if (input.hostLoginGated && !input.internalContentGrant) {
61
75
  const tenantExempt = tenant.tenant_id === PUBLIC_HOME_TENANT_ID;
62
76
  // UNIFIED on the federated session: read the FULL session (no resource filter —
63
77
  // federated sign-in's aud is the storefront app, not this tenant) and admit the
@@ -232,8 +246,14 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
232
246
  tenant,
233
247
  basePath,
234
248
  renderPath,
235
- cache,
249
+ // Internal output never receives a shared-cache handle. The render chain
250
+ // also refuses its page-cache predicate, making this a two-sided invariant.
251
+ cache: input.internalContentGrant ? undefined : cache,
236
252
  kv,
253
+ routeTierIndex: input.routeTierIndex,
254
+ publicationRequestPath: input.publicationRequestPath,
255
+ publicationRoute: input.publicationRoute,
256
+ publicationBucket: input.publicationBucket,
237
257
  managedIntegrationDeclarations,
238
258
  readTenantConsentSnapshot: () => readSavedConsentSnapshot(tenant.tenant_id),
239
259
  // The D1 catalog edge read replica, when this deploy opts into it. Deferred to
@@ -242,6 +262,7 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
242
262
  // — see `publicRender`.
243
263
  readCatalogD1: async (): Promise<D1Like | undefined> =>
244
264
  (await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined,
265
+ internalContentGrant: input.internalContentGrant,
245
266
  });
246
267
  }
247
268