@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.
- package/apps/storefront/env.d.ts +5 -0
- package/apps/storefront/migrations-apps/0013_publication_space_editorial.sql +64 -0
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +19 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +18 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +11 -1
- package/apps/storefront/src/lib/auth/internalContentAuthority.ts +153 -0
- package/apps/storefront/src/lib/auth/session.ts +3 -0
- package/apps/storefront/src/lib/blog/collection.ts +24 -18
- package/apps/storefront/src/lib/blog/provider.ts +28 -5
- package/apps/storefront/src/lib/blog/reactions.ts +4 -2
- package/apps/storefront/src/lib/blog/types.ts +1 -1
- package/apps/storefront/src/lib/blog/viewCounts.ts +3 -1
- package/apps/storefront/src/lib/d1/catalog.ts +18 -1
- package/apps/storefront/src/lib/d1/schema-apps.ts +93 -0
- package/apps/storefront/src/lib/edgeCache.ts +19 -1
- package/apps/storefront/src/lib/publication/contentProvider.ts +159 -0
- package/apps/storefront/src/lib/publication/runtimeSource.ts +194 -0
- package/apps/storefront/src/lib/the-build/provider.ts +2 -2
- package/apps/storefront/src/middleware/controlPlane.ts +24 -3
- package/apps/storefront/src/middleware/index.ts +125 -22
- package/apps/storefront/src/middleware/renderChain.ts +109 -56
- package/apps/storefront/src/middleware/routeBranch.ts +39 -7
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +1 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +1 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +4 -1
- package/apps/storefront/src/pages/api/rum/vitals.ts +4 -1
- package/apps/storefront/src/pages/blog/[slug].astro +5 -4
- package/apps/storefront/src/pages/blog/author/[author].astro +2 -2
- package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
- package/apps/storefront/src/pages/blog/index.astro +2 -2
- package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
- package/apps/storefront/src/pages/llms.txt.ts +2 -2
- package/apps/storefront/src/pages/sitemap.xml.ts +8 -4
- package/apps/storefront/src/pages/the-build/[slug].astro +6 -3
- package/apps/storefront/src/pages/the-build/index.astro +2 -2
- package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
- package/package.json +1 -1
- package/packages/public-runtime/package.json +2 -1
- package/packages/public-runtime/src/blog-visibility.d.mts +8 -0
- package/packages/public-runtime/src/blog-visibility.mjs +32 -0
- package/packages/public-runtime/src/index.ts +3 -0
- package/packages/public-runtime/src/publication-artifact.ts +20 -3
- package/packages/public-runtime/src/publication-reader.ts +187 -0
- package/packages/public-runtime/src/publication-routing.ts +246 -0
- 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 {
|
|
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
|
|