@tokenoftrust/storefront-runner 2.0.18 → 2.0.19

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.
@@ -0,0 +1,187 @@
1
+ import {
2
+ publicationArtifactBundleKeyV1,
3
+ publicationArtifactFileBytesV1,
4
+ publicationArtifactKeyV1,
5
+ validatePublicationArtifactBundleV1,
6
+ type PublicationArtifactBundleV1,
7
+ type PublicationArtifactFileV1,
8
+ } from "./publication-artifact.js";
9
+ import {
10
+ selectPublicationRouteVersionV1,
11
+ type PublicationRouteFailureCodeV1,
12
+ type PublicationRoutePlanV1,
13
+ type SelectedPublicationRouteV1,
14
+ } from "./publication-routing.js";
15
+ import type {
16
+ PublicationChannelV1,
17
+ PublicationVersionStateV1,
18
+ } from "./publication-version.js";
19
+ import type { RouteTierIndexV1 } from "./route-tier.js";
20
+
21
+ export interface PublicationStoredObjectV1 {
22
+ text(): Promise<string>;
23
+ arrayBuffer(): Promise<ArrayBuffer>;
24
+ }
25
+
26
+ /** Provider-neutral object-store read surface; R2 and test stores both satisfy it. */
27
+ export interface PublicationArtifactBucketV1 {
28
+ get(key: string): Promise<PublicationStoredObjectV1 | null>;
29
+ }
30
+
31
+ export type PublicationArtifactOpenFailureCodeV1 =
32
+ | PublicationRouteFailureCodeV1
33
+ | "publication_artifact_marker_missing"
34
+ | "publication_artifact_marker_malformed"
35
+ | "publication_artifact_file_missing"
36
+ | "publication_artifact_file_mismatch"
37
+ | "publication_artifact_source_unavailable";
38
+
39
+ export interface OpenPublicationArtifactV1 {
40
+ selection: SelectedPublicationRouteV1;
41
+ bundle: PublicationArtifactBundleV1;
42
+ /** Read a UTF-8 artifact relative to the selected space's content/ root. */
43
+ readContent(relativePath: string): string | null;
44
+ /** List paths relative to the selected space's content/ root. */
45
+ listContentPaths(prefix?: string): string[];
46
+ /** Read bytes relative to this space's declared asset namespace. */
47
+ readAsset(relativePath: string): Uint8Array | null;
48
+ }
49
+
50
+ export type OpenPublicationArtifactResultV1 =
51
+ | { ok: true; value: OpenPublicationArtifactV1 }
52
+ | { ok: false; code: PublicationArtifactOpenFailureCodeV1 };
53
+
54
+ function safeRelativePath(path: string, allowEmpty = false): boolean {
55
+ if (path === "") return allowEmpty;
56
+ return !path.startsWith("/") && !path.includes("\\") && !path.includes("\0") &&
57
+ path.split("/").every((part) => !!part && part !== "." && part !== "..");
58
+ }
59
+
60
+ function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
61
+ return left.byteLength === right.byteLength &&
62
+ left.every((value, index) => value === right[index]);
63
+ }
64
+
65
+ /**
66
+ * Open exactly one selected publication version and validate every physical
67
+ * object before rendering from it. A route/state/provenance/object failure is
68
+ * terminal for this source; callers must not fall through to another space.
69
+ */
70
+ export async function openPublicationArtifactV1(input: {
71
+ index: RouteTierIndexV1;
72
+ requestPath: string;
73
+ plan: PublicationRoutePlanV1;
74
+ state: PublicationVersionStateV1;
75
+ channel: PublicationChannelV1;
76
+ bucket: PublicationArtifactBucketV1;
77
+ }): Promise<OpenPublicationArtifactResultV1> {
78
+ const selected = selectPublicationRouteVersionV1(
79
+ input.index,
80
+ input.requestPath,
81
+ input.plan,
82
+ input.state,
83
+ input.channel,
84
+ );
85
+ if (!selected.ok) return { ok: false, code: selected.code };
86
+ return openSelectedPublicationArtifactV1(selected.value, input.bucket);
87
+ }
88
+
89
+ async function openSelectedPublicationArtifactV1(
90
+ selection: SelectedPublicationRouteV1,
91
+ bucket: PublicationArtifactBucketV1,
92
+ ): Promise<OpenPublicationArtifactResultV1> {
93
+ const { manifest, publicationVersion } = selection;
94
+ let marker: PublicationStoredObjectV1 | null;
95
+ try {
96
+ marker = await bucket.get(publicationArtifactBundleKeyV1(
97
+ manifest.tenant,
98
+ manifest.id,
99
+ publicationVersion.versionId,
100
+ ));
101
+ } catch {
102
+ return { ok: false, code: "publication_artifact_source_unavailable" };
103
+ }
104
+ if (!marker) return { ok: false, code: "publication_artifact_marker_missing" };
105
+
106
+ let markerValue: unknown;
107
+ try {
108
+ markerValue = JSON.parse(await marker.text());
109
+ } catch {
110
+ return { ok: false, code: "publication_artifact_marker_malformed" };
111
+ }
112
+ const checked = await validatePublicationArtifactBundleV1(markerValue, {
113
+ tenant: manifest.tenant,
114
+ spaceId: manifest.id,
115
+ versionId: publicationVersion.versionId,
116
+ sourceSha: publicationVersion.source.commitSha,
117
+ artifactDigest: publicationVersion.artifactDigest,
118
+ forge: manifest.source.forge,
119
+ repo: manifest.source.repo,
120
+ visibility: manifest.visibility,
121
+ assetNamespace: manifest.assets.namespace,
122
+ assetAccess: manifest.assets.access,
123
+ });
124
+ if (!checked.ok) return { ok: false, code: "publication_artifact_marker_malformed" };
125
+
126
+ const physical = new Map<string, Uint8Array>();
127
+ for (const file of checked.value.files) {
128
+ const expected = publicationArtifactFileBytesV1(file);
129
+ if (!expected) return { ok: false, code: "publication_artifact_marker_malformed" };
130
+ let stored: PublicationStoredObjectV1 | null;
131
+ try {
132
+ stored = await bucket.get(publicationArtifactKeyV1(
133
+ manifest.tenant,
134
+ manifest.id,
135
+ publicationVersion.versionId,
136
+ file.lane,
137
+ file.path,
138
+ ));
139
+ } catch {
140
+ return { ok: false, code: "publication_artifact_source_unavailable" };
141
+ }
142
+ if (!stored) return { ok: false, code: "publication_artifact_file_missing" };
143
+ let actual: Uint8Array;
144
+ try {
145
+ actual = new Uint8Array(await stored.arrayBuffer());
146
+ } catch {
147
+ return { ok: false, code: "publication_artifact_file_missing" };
148
+ }
149
+ if (!equalBytes(actual, expected)) {
150
+ return { ok: false, code: "publication_artifact_file_mismatch" };
151
+ }
152
+ physical.set(`${file.lane}\0${file.path}`, actual);
153
+ }
154
+
155
+ const contentFiles = checked.value.files.filter(
156
+ (file): file is PublicationArtifactFileV1 & { encoding: "utf8" } =>
157
+ file.lane === "content" && file.encoding === "utf8" && file.path.startsWith("content/"),
158
+ );
159
+ const assetLane = manifest.assets.access === "public" ? "public-asset" : "authorized-asset";
160
+ const assetPrefix = `${manifest.assets.namespace}/`;
161
+ return {
162
+ ok: true,
163
+ value: {
164
+ selection,
165
+ bundle: checked.value,
166
+ readContent(relativePath) {
167
+ if (!safeRelativePath(relativePath)) return null;
168
+ const path = `content/${relativePath}`;
169
+ const file = contentFiles.find((candidate) => candidate.path === path);
170
+ if (!file) return null;
171
+ const bytes = physical.get(`content\0${path}`);
172
+ return bytes ? new TextDecoder().decode(bytes) : null;
173
+ },
174
+ listContentPaths(prefix = "") {
175
+ if (!safeRelativePath(prefix.replace(/\/$/, ""), true)) return [];
176
+ return contentFiles
177
+ .map((file) => file.path.slice("content/".length))
178
+ .filter((path) => path.startsWith(prefix))
179
+ .sort();
180
+ },
181
+ readAsset(relativePath) {
182
+ if (!safeRelativePath(relativePath)) return null;
183
+ return physical.get(`${assetLane}\0${assetPrefix}${relativePath}`) ?? null;
184
+ },
185
+ },
186
+ };
187
+ }
@@ -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
  }