@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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One publication request plan: normalized mount resolution, canonical route,
|
|
3
|
+
* content lookup path, and independently selected immutable version.
|
|
4
|
+
*
|
|
5
|
+
* The route-tier resolver owns normalization and longest-prefix matching. This
|
|
6
|
+
* module consumes that answer without decoding or canonicalizing the request a
|
|
7
|
+
* second time, so request routing, preview, static materialization, and canonical
|
|
8
|
+
* URL generation cannot disagree about which publication space owns a path.
|
|
9
|
+
*/
|
|
10
|
+
import type { ContractViolation } from "./integration/result.js";
|
|
11
|
+
import {
|
|
12
|
+
resolvePublicationChannelVersionV1,
|
|
13
|
+
validatePublicationVersionStateV1,
|
|
14
|
+
type PublicationChannelPointerV1,
|
|
15
|
+
type PublicationChannelV1,
|
|
16
|
+
type PublicationVersionStateV1,
|
|
17
|
+
type PublicationVersionV1,
|
|
18
|
+
} from "./publication-version.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveRouteTierV1,
|
|
21
|
+
type ResolvedRouteTierV1,
|
|
22
|
+
type RouteTierIndexV1,
|
|
23
|
+
} from "./route-tier.js";
|
|
24
|
+
import type { PublicationSpaceManifestV1 } from "./publication-space.js";
|
|
25
|
+
|
|
26
|
+
export const PUBLICATION_ROUTE_PLAN_VERSION = 1 as const;
|
|
27
|
+
|
|
28
|
+
export type PublicationRouteFailureCodeV1 =
|
|
29
|
+
| "publication_route_unresolved"
|
|
30
|
+
| "publication_route_authority_mismatch"
|
|
31
|
+
| "publication_state_malformed"
|
|
32
|
+
| "publication_channel_unselected"
|
|
33
|
+
| "publication_version_provenance_mismatch"
|
|
34
|
+
| "publication_renderer_contract_mismatch";
|
|
35
|
+
|
|
36
|
+
export interface PublicationRouteRenderPlanV1 {
|
|
37
|
+
version: typeof PUBLICATION_ROUTE_PLAN_VERSION;
|
|
38
|
+
action: "render";
|
|
39
|
+
route: ResolvedRouteTierV1;
|
|
40
|
+
manifest: PublicationSpaceManifestV1;
|
|
41
|
+
/** Normalized route as requested, with the matched mount still attached. */
|
|
42
|
+
requestPath: string;
|
|
43
|
+
/** Route relative to the selected publication source. Always absolute. */
|
|
44
|
+
contentPath: string;
|
|
45
|
+
/** Canonical tenant-relative route used by links and Astro dispatch. */
|
|
46
|
+
canonicalPath: string;
|
|
47
|
+
assetNamespace: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PublicationRouteRedirectPlanV1 {
|
|
51
|
+
version: typeof PUBLICATION_ROUTE_PLAN_VERSION;
|
|
52
|
+
action: "redirect-308";
|
|
53
|
+
route: ResolvedRouteTierV1;
|
|
54
|
+
manifest: PublicationSpaceManifestV1;
|
|
55
|
+
requestPath: string;
|
|
56
|
+
/** Canonical tenant-relative redirect target, including the unmatched suffix. */
|
|
57
|
+
locationPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PublicationRouteFailedPlanV1 {
|
|
61
|
+
version: typeof PUBLICATION_ROUTE_PLAN_VERSION;
|
|
62
|
+
action: "fail-closed";
|
|
63
|
+
route: ResolvedRouteTierV1;
|
|
64
|
+
code: "publication_route_unresolved";
|
|
65
|
+
violations: readonly ContractViolation[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type PublicationRoutePlanV1 =
|
|
69
|
+
| PublicationRouteRenderPlanV1
|
|
70
|
+
| PublicationRouteRedirectPlanV1
|
|
71
|
+
| PublicationRouteFailedPlanV1;
|
|
72
|
+
|
|
73
|
+
export interface SelectedPublicationRouteV1 extends PublicationRouteRenderPlanV1 {
|
|
74
|
+
channel: PublicationChannelV1;
|
|
75
|
+
pointer: PublicationChannelPointerV1;
|
|
76
|
+
publicationVersion: PublicationVersionV1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type PublicationRouteSelectionV1 =
|
|
80
|
+
| { ok: true; value: SelectedPublicationRouteV1 }
|
|
81
|
+
| { ok: false; code: PublicationRouteFailureCodeV1; plan: PublicationRoutePlanV1 };
|
|
82
|
+
|
|
83
|
+
function suffixAfterMount(path: string, mount: string): string {
|
|
84
|
+
if (mount === "/") return path === "/" ? "" : path;
|
|
85
|
+
return path === mount ? "" : path.slice(mount.length);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function appendSuffix(mount: string, suffix: string): string {
|
|
89
|
+
if (!suffix) return mount;
|
|
90
|
+
if (mount === "/") return suffix;
|
|
91
|
+
return `${mount}${suffix}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Resolve one request through the already-compiled tenant mount authority. */
|
|
95
|
+
export function resolvePublicationRoutePlanV1(
|
|
96
|
+
index: RouteTierIndexV1,
|
|
97
|
+
requestPath: string,
|
|
98
|
+
): PublicationRoutePlanV1 {
|
|
99
|
+
const route = resolveRouteTierV1(index, requestPath);
|
|
100
|
+
const mount = route.mount;
|
|
101
|
+
const spaceId = route.space?.spaceId;
|
|
102
|
+
const manifest = spaceId ? index.spaces.get(spaceId) : undefined;
|
|
103
|
+
if (route.origin !== "publication-space" || !mount || !manifest) {
|
|
104
|
+
return {
|
|
105
|
+
version: PUBLICATION_ROUTE_PLAN_VERSION,
|
|
106
|
+
action: "fail-closed",
|
|
107
|
+
route,
|
|
108
|
+
code: "publication_route_unresolved",
|
|
109
|
+
violations: route.violations,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const suffix = suffixAfterMount(route.path, mount.mount);
|
|
114
|
+
if (mount.behavior === "redirect-308") {
|
|
115
|
+
if (!mount.redirectTo) {
|
|
116
|
+
return {
|
|
117
|
+
version: PUBLICATION_ROUTE_PLAN_VERSION,
|
|
118
|
+
action: "fail-closed",
|
|
119
|
+
route,
|
|
120
|
+
code: "publication_route_unresolved",
|
|
121
|
+
violations: [{
|
|
122
|
+
path: "mount.redirectTo",
|
|
123
|
+
code: "publication_redirect_target_required",
|
|
124
|
+
message: "A redirect publication mount must have a canonical target.",
|
|
125
|
+
}],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
version: PUBLICATION_ROUTE_PLAN_VERSION,
|
|
130
|
+
action: "redirect-308",
|
|
131
|
+
route,
|
|
132
|
+
manifest,
|
|
133
|
+
requestPath: route.path,
|
|
134
|
+
locationPath: appendSuffix(mount.redirectTo, suffix),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
version: PUBLICATION_ROUTE_PLAN_VERSION,
|
|
140
|
+
action: "render",
|
|
141
|
+
route,
|
|
142
|
+
manifest,
|
|
143
|
+
requestPath: route.path,
|
|
144
|
+
contentPath: suffix || "/",
|
|
145
|
+
canonicalPath: appendSuffix(mount.canonicalMount, suffix),
|
|
146
|
+
assetNamespace: manifest.assets.namespace,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Prove a carried plan still describes the current registry and request. This
|
|
152
|
+
* check must run before pointer or artifact I/O whenever a plan crosses a seam.
|
|
153
|
+
*/
|
|
154
|
+
export function publicationRoutePlanIsCurrentV1(
|
|
155
|
+
index: RouteTierIndexV1,
|
|
156
|
+
requestPath: string,
|
|
157
|
+
plan: PublicationRoutePlanV1,
|
|
158
|
+
): boolean {
|
|
159
|
+
return JSON.stringify(resolvePublicationRoutePlanV1(index, requestPath)) === JSON.stringify(plan);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Bind a render plan to exactly one channel pointer and immutable version.
|
|
164
|
+
* Any malformed/missing/mismatched state fails closed; there is no root or
|
|
165
|
+
* tenant-wide fallback after a publication space has been selected.
|
|
166
|
+
*/
|
|
167
|
+
export function selectPublicationRouteVersionV1(
|
|
168
|
+
index: RouteTierIndexV1,
|
|
169
|
+
requestPath: string,
|
|
170
|
+
plan: PublicationRoutePlanV1,
|
|
171
|
+
stateRaw: PublicationVersionStateV1,
|
|
172
|
+
channel: PublicationChannelV1,
|
|
173
|
+
): PublicationRouteSelectionV1 {
|
|
174
|
+
if (!publicationRoutePlanIsCurrentV1(index, requestPath, plan)) {
|
|
175
|
+
return { ok: false, code: "publication_route_authority_mismatch", plan };
|
|
176
|
+
}
|
|
177
|
+
if (plan.action !== "render") {
|
|
178
|
+
return { ok: false, code: "publication_route_unresolved", plan };
|
|
179
|
+
}
|
|
180
|
+
const checked = validatePublicationVersionStateV1(stateRaw);
|
|
181
|
+
if (!checked.ok) return { ok: false, code: "publication_state_malformed", plan };
|
|
182
|
+
const selected = resolvePublicationChannelVersionV1(
|
|
183
|
+
checked.value,
|
|
184
|
+
plan.manifest.tenant,
|
|
185
|
+
plan.manifest.id,
|
|
186
|
+
channel,
|
|
187
|
+
);
|
|
188
|
+
if (!selected) return { ok: false, code: "publication_channel_unselected", plan };
|
|
189
|
+
const { pointer, version } = selected;
|
|
190
|
+
if (
|
|
191
|
+
version.source.forge !== plan.manifest.source.forge ||
|
|
192
|
+
version.source.repo !== plan.manifest.source.repo ||
|
|
193
|
+
version.source.commitSha !== pointer.sourceSha
|
|
194
|
+
) {
|
|
195
|
+
return { ok: false, code: "publication_version_provenance_mismatch", plan };
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
version.renderer.contractVersion !== plan.manifest.renderer.contractVersion ||
|
|
199
|
+
version.renderer.themeContractVersion !== plan.manifest.renderer.themeContractVersion
|
|
200
|
+
) {
|
|
201
|
+
return { ok: false, code: "publication_renderer_contract_mismatch", plan };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
value: {
|
|
206
|
+
...plan,
|
|
207
|
+
channel,
|
|
208
|
+
pointer,
|
|
209
|
+
publicationVersion: version,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Build an absolute canonical URL without carrying query or fragment input. */
|
|
215
|
+
export function publicationCanonicalUrlV1(
|
|
216
|
+
canonicalOrigin: string,
|
|
217
|
+
plan: Pick<PublicationRouteRenderPlanV1, "canonicalPath">,
|
|
218
|
+
): string {
|
|
219
|
+
const origin = new URL(canonicalOrigin);
|
|
220
|
+
return new URL(plan.canonicalPath, `${origin.protocol}//${origin.host}`).toString();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Canonicalize a tenant-relative link through the same mount authority as a
|
|
225
|
+
* request. Queries/fragments are opaque suffixes; only the path is resolved.
|
|
226
|
+
* External, protocol-relative, reserved, and malformed routes fail closed.
|
|
227
|
+
*/
|
|
228
|
+
export function canonicalizePublicationLinkV1(
|
|
229
|
+
index: RouteTierIndexV1,
|
|
230
|
+
href: string,
|
|
231
|
+
): string | null {
|
|
232
|
+
if (!href.startsWith("/") || href.startsWith("//")) return null;
|
|
233
|
+
const query = href.indexOf("?");
|
|
234
|
+
const fragment = href.indexOf("#");
|
|
235
|
+
const delimiter = query === -1
|
|
236
|
+
? fragment
|
|
237
|
+
: fragment === -1
|
|
238
|
+
? query
|
|
239
|
+
: Math.min(query, fragment);
|
|
240
|
+
const path = delimiter === -1 ? href : href.slice(0, delimiter);
|
|
241
|
+
const suffix = delimiter === -1 ? "" : href.slice(delimiter);
|
|
242
|
+
const plan = resolvePublicationRoutePlanV1(index, path);
|
|
243
|
+
if (plan.action === "fail-closed") return null;
|
|
244
|
+
const canonicalPath = plan.action === "render" ? plan.canonicalPath : plan.locationPath;
|
|
245
|
+
return `${canonicalPath}${suffix}`;
|
|
246
|
+
}
|
|
@@ -390,6 +390,21 @@ export function serializePublicationTransitionRequestV1(value: PublicationTransi
|
|
|
390
390
|
return serializeValidated(value, validatePublicationTransitionRequestV1, "PublicationTransitionRequestV1");
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
export function parsePublicationVersionStateV1Json(json: string): ContractResult<PublicationVersionStateV1> {
|
|
394
|
+
return parseJson(json, (raw) => validatePublicationVersionStateV1(raw as PublicationVersionStateV1));
|
|
395
|
+
}
|
|
396
|
+
export function serializePublicationVersionStateV1(value: PublicationVersionStateV1): string {
|
|
397
|
+
return serializeValidated(
|
|
398
|
+
value,
|
|
399
|
+
(raw) => validatePublicationVersionStateV1(raw as PublicationVersionStateV1),
|
|
400
|
+
"PublicationVersionStateV1",
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
/** Public read projection for one tenant's exact publication pointer state. */
|
|
404
|
+
export function publicationVersionStateKey(tenant: string): string {
|
|
405
|
+
return `cust:publication-state:${tenant}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
393
408
|
export function publicationVersionKey(tenant: string, spaceId: string, versionId: string): string {
|
|
394
409
|
return `cust:publication-version:${tenant}:${spaceId}:${versionId}`;
|
|
395
410
|
}
|