@taprootio/site-authoring 0.12.3 → 0.12.5
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/README.md +5 -4
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/delivery-browser.js +4 -4
- package/src/delivery-check.js +65 -112
- package/src/redirects-contract.js +2 -0
- package/src/reference-help.js +10 -8
- package/src/reserved-paths.js +22 -0
- package/src/verbs/delivery-check.js +7 -0
- package/src/verbs/pages-push.js +9 -0
package/README.md
CHANGED
|
@@ -536,10 +536,11 @@ the way a visitor does and reports each dimension separately: the latest
|
|
|
536
536
|
completed deployment for the target, HTTP delivery of the authored routes and
|
|
537
537
|
the favicon, images, module preloads and site bundle they declare (status and
|
|
538
538
|
content type, no redirects followed), internal link targets, accidental
|
|
539
|
-
local-only references, and the
|
|
540
|
-
entry and capability modules
|
|
541
|
-
the
|
|
542
|
-
|
|
539
|
+
local-only references, and the pinned runtime — the version the page names,
|
|
540
|
+
its entry module and the capability modules the page declares, with a page
|
|
541
|
+
that still names the pre-pinning runtime pointer reported as unverified until it is republished. The
|
|
542
|
+
browser dimension (a fresh and a returning load
|
|
543
|
+
compared against the page's pinned entry, every document request in the browser,
|
|
543
544
|
popups included, held to the site origin before it is sent, and cache reuse
|
|
544
545
|
on the returning load observed rather than assumed)
|
|
545
546
|
runs only when Playwright resolves from the CLI's own install (install
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
package/src/cli.js
CHANGED
|
@@ -404,7 +404,7 @@ const VERBS = Object.freeze([
|
|
|
404
404
|
+ "visitor does and reports each dimension separately: the latest completed deployment for the target, HTTP "
|
|
405
405
|
+ "delivery of the authored routes (from the workspace manifest, bounded), the favicon, images, module "
|
|
406
406
|
+ "preloads and site bundle they reference, internal link targets, accidental local-only references, and the "
|
|
407
|
-
+ "
|
|
407
|
+
+ "pinned runtime (its version, entry module and capability modules), plus a browser "
|
|
408
408
|
+ "dimension that runs only when Playwright is installed and is reported as unchecked otherwise. --staging "
|
|
409
409
|
+ "resolves the acknowledged staging host and authorizes one preview session for the run (--url is refused "
|
|
410
410
|
+ "there); --production needs --url https://<published-domain>/ because a site-authoring key cannot read "
|
package/src/delivery-browser.js
CHANGED
|
@@ -4,9 +4,9 @@ import { withholdCredentialLocation } from "./staging-check.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* The optional browser dimension of delivery verification (TR00824).
|
|
6
6
|
*
|
|
7
|
-
* HTTP evidence cannot say what a visitor's browser loaded:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* HTTP evidence cannot say what a visitor's browser loaded: a returning
|
|
8
|
+
* browser may hold an older release's HTML and runtime in its cache. This
|
|
9
|
+
* probe drives a real browser at the target twice — a fresh
|
|
10
10
|
* context, then a second navigation in the same context so the browser's own
|
|
11
11
|
* cache participates — and reports which runtime entry each load evaluated,
|
|
12
12
|
* whether the declared capability elements were defined, and whether the
|
|
@@ -188,7 +188,7 @@ function judge({ observed, strayed, finalUrl }, expectedEntryUrl, capabilityCoun
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* @param options.baseUrl The delivery origin.
|
|
191
|
-
* @param options.expectedEntryUrl The runtime entry the
|
|
191
|
+
* @param options.expectedEntryUrl The runtime entry the freshly fetched page pins.
|
|
192
192
|
* @param options.capabilities Custom-element tags the home page declares.
|
|
193
193
|
* @param options.cookie Optional staging cookie, sent only to `baseUrl`'s host.
|
|
194
194
|
* @param options.importPlaywright Test seam; defaults to a dynamic import.
|
package/src/delivery-check.js
CHANGED
|
@@ -7,9 +7,9 @@ import { withholdCredentialLocation } from "./staging-check.js";
|
|
|
7
7
|
* A completed deployment job proves that Taproot wrote what it meant to
|
|
8
8
|
* write. It does not prove that a visitor receives it: the edge may still
|
|
9
9
|
* serve the previous route, an asset may 404 behind a 200 HTML error page, and
|
|
10
|
-
* the shared runtime a browser loads
|
|
11
|
-
*
|
|
12
|
-
* deployment target with explicit bounds and reports each dimension on its
|
|
10
|
+
* the shared runtime a browser loads is the version the page pins, which has
|
|
11
|
+
* to be served for that exact version. This module checks the public HTTP
|
|
12
|
+
* surface of a deployment target with explicit bounds and reports each dimension on its
|
|
13
13
|
* own: HTTP delivery is checked here, browser-observed behaviour is the
|
|
14
14
|
* optional probe in `delivery-browser.js`, and a dimension nobody checked is
|
|
15
15
|
* reported as unchecked, never as passed.
|
|
@@ -34,7 +34,6 @@ export const DELIVERY_LIMITS = Object.freeze({
|
|
|
34
34
|
retryMilliseconds: 1_000,
|
|
35
35
|
requestMilliseconds: 15_000,
|
|
36
36
|
htmlBytes: 2 * 1024 * 1024,
|
|
37
|
-
manifestBytes: 256 * 1024,
|
|
38
37
|
propagationWaitMaximumSeconds: 120,
|
|
39
38
|
failures: 40,
|
|
40
39
|
reportBytes: 24_000,
|
|
@@ -45,7 +44,6 @@ export const DELIVERY_LIMITS = Object.freeze({
|
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
const HTML_TYPES = /^text\/html\b/iu;
|
|
48
|
-
const JSON_TYPES = /^application\/(?:json|manifest\+json)\b/iu;
|
|
49
47
|
const SCRIPT_TYPES = /^(?:text|application)\/(?:javascript|ecmascript|x-javascript)\b/iu;
|
|
50
48
|
const IMAGE_TYPES = /^image\//iu;
|
|
51
49
|
const LOCAL_HOST = /^(?:localhost|127\.0\.0\.1|\[::1\]|0\.0\.0\.0|[^/]*\.(?:test|local|localhost|internal))(?::\d+)?$/iu;
|
|
@@ -171,27 +169,12 @@ function typeOf(response) {
|
|
|
171
169
|
|
|
172
170
|
function expectationFor(kind) {
|
|
173
171
|
if (kind === "route" || kind === "link") return { name: "text/html", test: HTML_TYPES };
|
|
174
|
-
if (kind === "manifest") return { name: "application/json", test: JSON_TYPES };
|
|
175
172
|
if (kind === "site-bundle" || kind === "runtime-entry" || kind === "module" || kind === "capability") {
|
|
176
173
|
return { name: "javascript", test: SCRIPT_TYPES };
|
|
177
174
|
}
|
|
178
175
|
return { name: "image/*", test: IMAGE_TYPES };
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
export function compareVersions(left, right) {
|
|
182
|
-
const parse = (value) => {
|
|
183
|
-
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(value ?? "");
|
|
184
|
-
return match ? match.slice(1).map(Number) : undefined;
|
|
185
|
-
};
|
|
186
|
-
const a = parse(left);
|
|
187
|
-
const b = parse(right);
|
|
188
|
-
if (!a || !b) return undefined;
|
|
189
|
-
for (let index = 0; index < 3; index += 1) {
|
|
190
|
-
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
191
|
-
}
|
|
192
|
-
return 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
178
|
/** The runtime bootstrap a rendered page declares, or undefined when absent. */
|
|
196
179
|
export function parseBootstrap(html) {
|
|
197
180
|
const script = tokenizeMarkup(html).find((tag) => tag.name === "script" && tag.attributes.get("id") === "taproot-runtime-bootstrap");
|
|
@@ -208,12 +191,25 @@ export function parseBootstrap(html) {
|
|
|
208
191
|
// capability this page could declare and is counted, never carried.
|
|
209
192
|
const declared = Array.isArray(parsed.runtimeCapabilities) ? parsed.runtimeCapabilities : [];
|
|
210
193
|
const runtimeCapabilities = declared.filter((value) => typeof value === "string" && CUSTOM_ELEMENT_NAME.test(value)).slice(0, 64);
|
|
194
|
+
// The page pins one runtime version: its entry and the module each declared
|
|
195
|
+
// capability resolves to. Only the declared tags are read from the map, so
|
|
196
|
+
// an oversized map costs nothing.
|
|
197
|
+
const capabilityMap = parsed.capabilities !== null && typeof parsed.capabilities === "object" && !Array.isArray(parsed.capabilities)
|
|
198
|
+
? parsed.capabilities
|
|
199
|
+
: {};
|
|
200
|
+
const capabilities = Object.fromEntries(
|
|
201
|
+
runtimeCapabilities
|
|
202
|
+
.filter((tag) => Object.hasOwn(capabilityMap, tag))
|
|
203
|
+
.map((tag) => [tag, text(capabilityMap[tag])]),
|
|
204
|
+
);
|
|
211
205
|
return {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
version: text(parsed.version).slice(0, 64),
|
|
207
|
+
entry: text(parsed.entry),
|
|
208
|
+
// A release published before runtime pinning named a mutable pointer instead.
|
|
209
|
+
legacyPointer: typeof parsed.runtimeManifestUrl === "string" && parsed.runtimeManifestUrl !== "",
|
|
215
210
|
siteBundleUrl: text(parsed.siteBundleUrl),
|
|
216
211
|
runtimeCapabilities,
|
|
212
|
+
capabilities,
|
|
217
213
|
...(declared.length > runtimeCapabilities.length ? { capabilitiesDropped: declared.length - runtimeCapabilities.length } : {}),
|
|
218
214
|
};
|
|
219
215
|
}
|
|
@@ -341,7 +337,7 @@ function bounded(value) {
|
|
|
341
337
|
* read is part of the attempt, so a server that stalls or resets after its
|
|
342
338
|
* headers is retried and recorded, never thrown.
|
|
343
339
|
*/
|
|
344
|
-
async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.
|
|
340
|
+
async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.htmlBytes } = {}) {
|
|
345
341
|
const expectation = expectationFor(kind);
|
|
346
342
|
const origin = new URL(url).origin;
|
|
347
343
|
if (!context.allowedOrigins.has(origin)) {
|
|
@@ -485,14 +481,13 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
485
481
|
}
|
|
486
482
|
}
|
|
487
483
|
|
|
488
|
-
// The runtime the page
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// admitted for the runtime probes only.
|
|
484
|
+
// The runtime the page pins: one version, the entry module that version
|
|
485
|
+
// names and the module each declared capability maps to. The bootstrap is
|
|
486
|
+
// generator-authored, so its origins are trusted here and admitted for the
|
|
487
|
+
// runtime probes only.
|
|
493
488
|
const runtime = {
|
|
494
489
|
declared: bootstrap !== undefined,
|
|
495
|
-
|
|
490
|
+
version: bootstrap?.version ?? "",
|
|
496
491
|
capabilities: bootstrap?.runtimeCapabilities ?? [],
|
|
497
492
|
...(bootstrap?.capabilitiesDropped ? { capabilitiesDropped: bootstrap.capabilitiesDropped } : {}),
|
|
498
493
|
};
|
|
@@ -510,72 +505,34 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
510
505
|
else addAsset(bundle.href, "site-bundle");
|
|
511
506
|
}
|
|
512
507
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
508
|
+
runtime.pinned = runtime.version !== "" && Boolean(bootstrap?.entry);
|
|
509
|
+
// Such a page still loads in a browser, but this check no longer reads the
|
|
510
|
+
// pointer, so its runtime is reported unverified rather than as passed or failed.
|
|
511
|
+
runtime.legacy = !runtime.pinned && Boolean(bootstrap?.legacyPointer);
|
|
512
|
+
const entryUrl = runtime.pinned ? admit(bootstrap.entry) : undefined;
|
|
513
|
+
if (entryUrl) {
|
|
514
|
+
const { row } = await probe(context, entryUrl.href, "runtime-entry");
|
|
515
|
+
runtime.entry = { url: row.url, status: row.status, ok: row.ok, ...(row.failure ? { failure: row.failure } : {}) };
|
|
516
|
+
} else {
|
|
517
|
+
runtime.entry = { url: bounded(bootstrap?.entry ?? ""), ok: false, failure: runtime.pinned ? "invalid_url" : "undeclared" };
|
|
518
518
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
let parsed;
|
|
522
|
-
if (row.ok) {
|
|
523
|
-
try {
|
|
524
|
-
parsed = JSON.parse(body);
|
|
525
|
-
} catch {
|
|
526
|
-
row.ok = false;
|
|
527
|
-
row.failure = "invalid_json";
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return { name, row, parsed, url };
|
|
531
|
-
});
|
|
519
|
+
// Declared is not resolvable: a module URL that is missing, invalid or
|
|
520
|
+
// points off an admitted origin is a failed observation, not a silent omission.
|
|
532
521
|
const capabilityUrls = [];
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (!module) return { tag, failure: "invalid_url" };
|
|
543
|
-
if (!context.allowedOrigins.has(module.origin)) return { tag, url: module.href, failure: "origin_not_allowed" };
|
|
544
|
-
return { tag, url: module.href };
|
|
545
|
-
});
|
|
546
|
-
const resolvable = outcomes.filter((outcome) => outcome.failure === undefined);
|
|
547
|
-
runtime[name] = {
|
|
548
|
-
url: bounded(row.url),
|
|
549
|
-
status: row.status,
|
|
550
|
-
ok: row.ok,
|
|
551
|
-
...(row.failure ? { failure: row.failure } : {}),
|
|
552
|
-
version: typeof parsed?.version === "string" ? bounded(parsed.version) : "",
|
|
553
|
-
majorVersion: parsed?.majorVersion === undefined ? "" : bounded(String(parsed.majorVersion)),
|
|
554
|
-
entry: typeof parsed?.entry === "string" ? bounded(parsed.entry) : "",
|
|
555
|
-
cacheControl: row.cacheControl ?? "",
|
|
556
|
-
capabilities: Object.keys(capabilities).length,
|
|
557
|
-
declaredCapabilities: declared.length,
|
|
558
|
-
resolvableCapabilities: resolvable.length,
|
|
559
|
-
};
|
|
560
|
-
if (name === "majorPointer" && row.ok) {
|
|
561
|
-
for (const outcome of outcomes) {
|
|
562
|
-
if (outcome.failure) capabilityUrls.push({ tag: outcome.tag, url: outcome.url ?? "", failure: outcome.failure });
|
|
563
|
-
else if (capabilityUrls.filter((entry) => !entry.failure).length < DELIVERY_LIMITS.capabilities) capabilityUrls.push(outcome);
|
|
564
|
-
else capabilityUrls.push({ tag: outcome.tag, url: outcome.url, unchecked: true });
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
const pointer = runtime.majorPointer;
|
|
569
|
-
if (pointer?.ok && pointer.entry) {
|
|
570
|
-
const entry = resolveReference(pointer.entry, pointer.url);
|
|
571
|
-
if (entry && context.allowedOrigins.has(entry.origin)) {
|
|
572
|
-
const { row } = await probe(context, entry.href, "runtime-entry");
|
|
573
|
-
runtime.entry = { url: row.url, status: row.status, ok: row.ok, ...(row.failure ? { failure: row.failure } : {}) };
|
|
522
|
+
let resolvable = 0;
|
|
523
|
+
// A page that does not pin a runtime declares no chunk URLs to check.
|
|
524
|
+
for (const tag of runtime.pinned ? runtime.capabilities : []) {
|
|
525
|
+
const declared = bootstrap.capabilities[tag];
|
|
526
|
+
const module = declared ? resolveReference(declared, origin) : undefined;
|
|
527
|
+
if (!module) {
|
|
528
|
+
capabilityUrls.push({ tag, url: "", failure: declared ? "invalid_url" : "undeclared" });
|
|
529
|
+
} else if (!context.allowedOrigins.has(module.origin)) {
|
|
530
|
+
capabilityUrls.push({ tag, url: module.href, failure: "origin_not_allowed" });
|
|
574
531
|
} else {
|
|
575
|
-
|
|
532
|
+
resolvable += 1;
|
|
533
|
+
const checkable = capabilityUrls.filter((entry) => !entry.failure && !entry.unchecked).length;
|
|
534
|
+
capabilityUrls.push(checkable < DELIVERY_LIMITS.capabilities ? { tag, url: module.href } : { tag, url: module.href, unchecked: true });
|
|
576
535
|
}
|
|
577
|
-
} else {
|
|
578
|
-
runtime.entry = { url: "", ok: false, failure: "pointer_unavailable" };
|
|
579
536
|
}
|
|
580
537
|
const capabilityResults = await mapBounded(capabilityUrls, async ({ tag, url, failure, unchecked }) => {
|
|
581
538
|
if (failure) return { tag: bounded(tag), url: bounded(url), kind: "capability", status: 0, ok: false, attempts: 0, failure };
|
|
@@ -592,13 +549,10 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
592
549
|
unchecked: uncheckedCapabilities,
|
|
593
550
|
items: capabilityResults.filter((row) => !row.ok && !row.unchecked).slice(0, DELIVERY_LIMITS.capabilities),
|
|
594
551
|
};
|
|
595
|
-
runtime.
|
|
596
|
-
|
|
597
|
-
&& compareVersions(pointer.version, runtime.fallback.version) === -1;
|
|
598
|
-
runtime.capabilitiesResolvable = Boolean(pointer?.ok)
|
|
599
|
-
&& pointer.resolvableCapabilities === runtime.capabilities.length
|
|
552
|
+
runtime.capabilitiesResolvable = runtime.declared
|
|
553
|
+
&& resolvable === runtime.capabilities.length
|
|
600
554
|
&& runtime.capabilityModules.failed === 0;
|
|
601
|
-
runtime.compatible = runtime.declared && runtime.
|
|
555
|
+
runtime.compatible = runtime.declared && runtime.pinned && runtime.entry.ok && runtime.capabilitiesResolvable;
|
|
602
556
|
|
|
603
557
|
const assetResults = await mapBounded(assetRows, async ({ url, kind }) => {
|
|
604
558
|
const { row } = await probe(context, url, kind);
|
|
@@ -616,18 +570,17 @@ async function observe(context, origin, sampledRoutes) {
|
|
|
616
570
|
...assetResults.filter((row) => !row.ok).map((row) => `${row.kind} ${row.url}: ${row.failure}`),
|
|
617
571
|
...linkResults.filter((row) => !row.ok).map((row) => `link ${row.path}: ${row.failure}`),
|
|
618
572
|
...(runtime.declared ? [] : ["runtime: the home page declares no runtime bootstrap"]),
|
|
619
|
-
...(runtime.declared &&
|
|
620
|
-
? [
|
|
573
|
+
...(runtime.declared && runtime.legacy
|
|
574
|
+
? ["runtime: the page names a pre-pinning runtime pointer, which is not verified here; republish the site so it pins a runtime version"]
|
|
621
575
|
: []),
|
|
622
|
-
...(runtime.declared && !runtime.
|
|
623
|
-
|
|
624
|
-
? [`runtime: fallback ${runtime.fallback.url || "(undeclared)"}: ${runtime.fallback.failure}`]
|
|
576
|
+
...(runtime.declared && !runtime.pinned && !runtime.legacy
|
|
577
|
+
? ["runtime: the page does not pin a runtime version and entry"]
|
|
625
578
|
: []),
|
|
626
|
-
...(runtime.declared && !runtime.
|
|
627
|
-
? [`runtime: ${runtime.
|
|
579
|
+
...(runtime.declared && runtime.pinned && !runtime.entry.ok
|
|
580
|
+
? [`runtime: entry ${runtime.entry.url || "(undeclared)"} (version ${runtime.version}): ${runtime.entry.failure}`]
|
|
628
581
|
: []),
|
|
629
|
-
...(runtime.
|
|
630
|
-
? [`runtime:
|
|
582
|
+
...(runtime.declared && runtime.pinned && !runtime.capabilitiesResolvable
|
|
583
|
+
? [`runtime: ${runtime.capabilityModules.failed} of ${runtime.capabilities.length} declared capability module(s) were undeclared, invalid or did not load`]
|
|
631
584
|
: []),
|
|
632
585
|
...([...localOnly].slice(0, DELIVERY_LIMITS.localReferences).map((reference) => `local-only reference: ${bounded(reference)}`)),
|
|
633
586
|
].map((value) => [...sanitizeDiagnostic(value, "")].slice(0, DELIVERY_LIMITS.urlScalars).join(""));
|
|
@@ -714,7 +667,7 @@ export async function checkDelivery({
|
|
|
714
667
|
let observed = await observe(context, origin, sampledRoutes);
|
|
715
668
|
// One bounded re-observation of everything, for edge propagation: it waits
|
|
716
669
|
// the caller's budget once and observes again from the home page down, so a
|
|
717
|
-
// recovered route, asset
|
|
670
|
+
// recovered route, asset or bootstrap is seen fresh. It never loops.
|
|
718
671
|
let waited = 0;
|
|
719
672
|
if (waitSeconds > 0 && observed.failures.length > 0) {
|
|
720
673
|
await context.sleep(waitSeconds * 1000);
|
|
@@ -724,7 +677,7 @@ export async function checkDelivery({
|
|
|
724
677
|
|
|
725
678
|
const verdict = observed.failuresTotal === 0
|
|
726
679
|
? "delivered"
|
|
727
|
-
: observed.routeRows.every((row) => row.ok) && observed.runtime.compatible
|
|
680
|
+
: observed.routeRows.every((row) => row.ok) && (observed.runtime.compatible || observed.runtime.legacy)
|
|
728
681
|
? "degraded"
|
|
729
682
|
: "failed";
|
|
730
683
|
const report = {
|
|
@@ -802,10 +755,10 @@ function withinOutputBudget(report, failuresTotal) {
|
|
|
802
755
|
const { runtime } = report;
|
|
803
756
|
report.runtime = {
|
|
804
757
|
declared: runtime.declared,
|
|
805
|
-
|
|
758
|
+
version: runtime.version,
|
|
759
|
+
pinned: runtime.pinned,
|
|
760
|
+
legacy: runtime.legacy,
|
|
806
761
|
capabilities: runtime.capabilities.length,
|
|
807
|
-
majorStreamMatches: runtime.majorStreamMatches,
|
|
808
|
-
pointerBehindFallback: runtime.pointerBehindFallback,
|
|
809
762
|
capabilitiesResolvable: runtime.capabilitiesResolvable,
|
|
810
763
|
compatible: runtime.compatible,
|
|
811
764
|
entryOk: Boolean(runtime.entry?.ok),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SiteAuthoringError } from "./errors.js";
|
|
2
|
+
import { RUNTIME_MIRROR_PATH_REASON, isRuntimeMirrorPath } from "./reserved-paths.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The redirect map's workspace contract (TR00702).
|
|
@@ -203,6 +204,7 @@ export function redirectSourceRefusalReason(path) {
|
|
|
203
204
|
if (first === RESERVED_INTEGRATION_DATA_SEGMENT) {
|
|
204
205
|
return `'/${segments[0]}' is reserved for integration data, so nothing under it can be a redirect source.`;
|
|
205
206
|
}
|
|
207
|
+
if (isRuntimeMirrorPath(decoded)) return RUNTIME_MIRROR_PATH_REASON;
|
|
206
208
|
if (GENERATED_ROOT_FILES.includes(first)) {
|
|
207
209
|
return `'/${segments[0]}' is a file the site generates, so neither it nor anything under it can be a redirect source.`;
|
|
208
210
|
}
|
package/src/reference-help.js
CHANGED
|
@@ -478,11 +478,11 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
478
478
|
"Assets are what the home page declares: favicon links, module preloads, the site bundle, and up to six "
|
|
479
479
|
+ "images. A 200 is not enough: the content type must match the kind, so an HTML error page served as an "
|
|
480
480
|
+ "image fails.",
|
|
481
|
-
"Runtime: the page's bootstrap
|
|
482
|
-
+ "
|
|
483
|
-
+ "
|
|
484
|
-
+ "
|
|
485
|
-
"Browser: a fresh and a returning (same-context, cached) load are compared against the
|
|
481
|
+
"Runtime: the page's bootstrap pins one runtime version, its entry module and the module each declared "
|
|
482
|
+
+ "capability resolves to. The entry must load as JavaScript and every declared capability must resolve and "
|
|
483
|
+
+ "load. A page that still names the pre-pinning runtime pointer is reported as unverified, so the verdict is "
|
|
484
|
+
+ "degraded, until the site is republished.",
|
|
485
|
+
"Browser: a fresh and a returning (same-context, cached) load are compared against the page's pinned entry when "
|
|
486
486
|
+ "Playwright resolves from the CLI's own install: npm install --global @taprootio/site-authoring@latest "
|
|
487
487
|
+ "playwright && npx playwright install chromium, then rerun through the installed taproot-site command (a "
|
|
488
488
|
+ "copy run through npx lives in npm's cache and cannot see a global Playwright). A DevTools "
|
|
@@ -505,8 +505,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
505
505
|
+ "not delivery), local-only references (localhost, *.test, *.local) are reported rather than fetched, a "
|
|
506
506
|
+ "redirect Location that could reflect a credential is withheld, bodies are read up to a fixed bound, "
|
|
507
507
|
+ "member-only content is not exercised, and nothing is purged, republished or rolled back.",
|
|
508
|
-
"Verdicts: delivered (no findings), degraded (every route served and the runtime is compatible
|
|
509
|
-
+ "
|
|
508
|
+
"Verdicts: delivered (no findings), degraded (every route served and the runtime is compatible or a pre-pinning "
|
|
509
|
+
+ "reference, but an asset, link or browser load did not match or the runtime could not be verified), failed (a route or the runtime did not deliver).",
|
|
510
510
|
]),
|
|
511
511
|
example: Object.freeze({
|
|
512
512
|
command: `${CLI_BINARY_NAME} delivery check --production --url https://www.example.com/ --wait 30`,
|
|
@@ -659,7 +659,9 @@ const WORKFLOW_REFERENCES = Object.freeze({
|
|
|
659
659
|
+ "no separate propagation delay; an entry a check does not see belongs to a release that is not being "
|
|
660
660
|
+ "served yet.",
|
|
661
661
|
"A source cannot name a file the site generates: '/index.html' or any page's own 'index.html', '/404.html', "
|
|
662
|
-
+ "'/sitemap.xml', '/robots.txt', '/favicon.ico', or a path under the sitemap or robots names.
|
|
662
|
+
+ "'/sitemap.xml', '/robots.txt', '/favicon.ico', or a path under the sitemap or robots names. Nothing under "
|
|
663
|
+
+ "'/taproot/<version>/' (for example '/taproot/5.0.62/manifest.json') can be a source either: the platform "
|
|
664
|
+
+ "serves the published-site runtime there before any redirect. The platform "
|
|
663
665
|
+ "already redirects every '…/index.html' address to its directory, so no entry is needed for one. A source "
|
|
664
666
|
+ "that could not be stored as a file (a dot-prefixed segment, a reserved device name, an escaped percent "
|
|
665
667
|
+ "sign) is refused too. 'validate' names the entry and the reason.",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site paths the platform reserves on every site host.
|
|
3
|
+
*
|
|
4
|
+
* The published-site Worker ships the immutable runtime as static assets at
|
|
5
|
+
* `/taproot/<semver>/...` (TR01144), and Cloudflare answers a matching request
|
|
6
|
+
* before the Worker runs, so a page or redirect authored there could never be
|
|
7
|
+
* reached. This is the one place the package owns that prefix; page push and
|
|
8
|
+
* the redirect map both ask here. The API is the authority and mirrors the rule
|
|
9
|
+
* in `PublishedOutputPaths.IsRuntimeMirrorPath`; both are pinned by the same
|
|
10
|
+
* test vectors.
|
|
11
|
+
*/
|
|
12
|
+
const RUNTIME_MIRROR_PATH =
|
|
13
|
+
/^taproot\/[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\/|$)/iu;
|
|
14
|
+
|
|
15
|
+
/** Whether a site-relative path (no leading slash) is `taproot/<semver>` or under it. */
|
|
16
|
+
export function isRuntimeMirrorPath(path) {
|
|
17
|
+
return RUNTIME_MIRROR_PATH.test(path);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RUNTIME_MIRROR_PATH_REASON =
|
|
21
|
+
"'/taproot/<version>/' is reserved for the published-site runtime, which the platform answers before any page "
|
|
22
|
+
+ "or redirect, so nothing under it can be authored.";
|
|
@@ -156,6 +156,13 @@ export async function deliveryCheck(invocation) {
|
|
|
156
156
|
let browser;
|
|
157
157
|
if (invocation.browser === false) {
|
|
158
158
|
browser = { status: "unchecked", reason: "disabled", note: "Browser verification was not requested." };
|
|
159
|
+
} else if (report.runtime.legacy) {
|
|
160
|
+
// There is no pinned entry to compare a browser load against.
|
|
161
|
+
browser = {
|
|
162
|
+
status: "unchecked",
|
|
163
|
+
reason: "legacy_runtime_reference",
|
|
164
|
+
note: "The page names a pre-pinning runtime pointer, so no entry is pinned to compare a browser load against; republish the site and rerun.",
|
|
165
|
+
};
|
|
159
166
|
} else {
|
|
160
167
|
onProgress("Browser dimension: probing with Playwright when it is installed.");
|
|
161
168
|
browser = await probeDeliveryWithBrowser({
|
package/src/verbs/pages-push.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createPage, listSitePages, PAGE_STATUS_DELETED, updatePage, withRefusalGuidance } from "../api.js";
|
|
2
2
|
import { VERB_PAGES_PUSH } from "../constants.js";
|
|
3
3
|
import { SiteAuthoringError } from "../errors.js";
|
|
4
|
+
import { RUNTIME_MIRROR_PATH_REASON, isRuntimeMirrorPath } from "../reserved-paths.js";
|
|
4
5
|
import { boundedList, openSession, successResult, warnIfExternalWritesPaused } from "../session.js";
|
|
5
6
|
import { SETTINGS_TYPE_TAPROOT_STYLES } from "../settings-catalog.js";
|
|
6
7
|
import {
|
|
@@ -484,6 +485,14 @@ export async function readWorkspacePageSource({ workspaceDir, file, manifestEntr
|
|
|
484
485
|
);
|
|
485
486
|
}
|
|
486
487
|
|
|
488
|
+
if (isRuntimeMirrorPath(pagePath)) {
|
|
489
|
+
throw documentError(
|
|
490
|
+
"pages.path_unsupported",
|
|
491
|
+
`'${file}' declares page path '${pagePath}': ${RUNTIME_MIRROR_PATH_REASON}`,
|
|
492
|
+
file,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
487
496
|
return {
|
|
488
497
|
file,
|
|
489
498
|
pagePath,
|