@taprootio/site-authoring 0.12.4 → 0.12.6

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 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 declared runtime — the major pointer's stream,
540
- entry and capability modules, and whether the pointer lags the fallback copy
541
- the site shipped with. The browser dimension (a fresh and a returning load
542
- compared against the pointer's entry, every document request in the browser,
539
+ local-only references, and the pinned runtime — the version the page names,
540
+ its entry module and the capability modules the page declares (a page that
541
+ pins no runtime version and entry fails the runtime check). 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.4",
3
+ "version": "0.12.6",
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
- + "declared runtime (major pointer, fallback copy, entry module and capability modules), plus a browser "
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 "
@@ -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: the shared runtime
8
- * comes from a mutable major pointer that a returning browser may hold in its
9
- * cache. This probe drives a real browser at the target twice — a fresh
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 major pointer names right now.
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.
@@ -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 comes from a mutable major pointer that
11
- * can lag or be cached. This module checks the public HTTP surface of a
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,23 @@ 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
- runtimeMajorVersion: text(parsed.runtimeMajorVersion).slice(0, 16),
213
- runtimeManifestUrl: text(parsed.runtimeManifestUrl),
214
- fallbackRuntimeManifestUrl: text(parsed.fallbackRuntimeManifestUrl),
206
+ version: text(parsed.version).slice(0, 64),
207
+ entry: text(parsed.entry),
215
208
  siteBundleUrl: text(parsed.siteBundleUrl),
216
209
  runtimeCapabilities,
210
+ capabilities,
217
211
  ...(declared.length > runtimeCapabilities.length ? { capabilitiesDropped: declared.length - runtimeCapabilities.length } : {}),
218
212
  };
219
213
  }
@@ -341,7 +335,7 @@ function bounded(value) {
341
335
  * read is part of the attempt, so a server that stalls or resets after its
342
336
  * headers is retried and recorded, never thrown.
343
337
  */
344
- async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.manifestBytes } = {}) {
338
+ async function probe(context, url, kind, { readBody = false, maximumBytes = DELIVERY_LIMITS.htmlBytes } = {}) {
345
339
  const expectation = expectationFor(kind);
346
340
  const origin = new URL(url).origin;
347
341
  if (!context.allowedOrigins.has(origin)) {
@@ -485,14 +479,13 @@ async function observe(context, origin, sampledRoutes) {
485
479
  }
486
480
  }
487
481
 
488
- // The runtime the page declares: the mutable major pointer a browser
489
- // resolves, the immutable fallback copy shipped with the site, the entry
490
- // module the pointer names and the capability modules it maps. The
491
- // bootstrap is generator-authored, so its origins are trusted here and
492
- // admitted for the runtime probes only.
482
+ // The runtime the page pins: one version, the entry module that version
483
+ // names and the module each declared capability maps to. The bootstrap is
484
+ // generator-authored, so its origins are trusted here and admitted for the
485
+ // runtime probes only.
493
486
  const runtime = {
494
487
  declared: bootstrap !== undefined,
495
- expectedMajorVersion: bootstrap?.runtimeMajorVersion ?? "",
488
+ version: bootstrap?.version ?? "",
496
489
  capabilities: bootstrap?.runtimeCapabilities ?? [],
497
490
  ...(bootstrap?.capabilitiesDropped ? { capabilitiesDropped: bootstrap.capabilitiesDropped } : {}),
498
491
  };
@@ -510,72 +503,31 @@ async function observe(context, origin, sampledRoutes) {
510
503
  else addAsset(bundle.href, "site-bundle");
511
504
  }
512
505
  }
513
- const manifests = [];
514
- for (const [name, value] of [["majorPointer", bootstrap?.runtimeManifestUrl], ["fallback", bootstrap?.fallbackRuntimeManifestUrl]]) {
515
- const url = admit(value);
516
- if (url) manifests.push({ name, url: url.href });
517
- else runtime[name] = { url: "", ok: false, failure: "undeclared" };
506
+ runtime.pinned = runtime.version !== "" && Boolean(bootstrap?.entry);
507
+ const entryUrl = runtime.pinned ? admit(bootstrap.entry) : undefined;
508
+ if (entryUrl) {
509
+ const { row } = await probe(context, entryUrl.href, "runtime-entry");
510
+ runtime.entry = { url: row.url, status: row.status, ok: row.ok, ...(row.failure ? { failure: row.failure } : {}) };
511
+ } else {
512
+ runtime.entry = { url: bounded(bootstrap?.entry ?? ""), ok: false, failure: runtime.pinned ? "invalid_url" : "undeclared" };
518
513
  }
519
- const manifestResults = await mapBounded(manifests, async ({ name, url }) => {
520
- const { row, body } = await probe(context, url, "manifest", { readBody: true });
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
- });
514
+ // Declared is not resolvable: a module URL that is missing, invalid or
515
+ // points off an admitted origin is a failed observation, not a silent omission.
532
516
  const capabilityUrls = [];
533
- for (const { name, row, parsed, url } of manifestResults) {
534
- const capabilities = parsed?.capabilities && typeof parsed.capabilities === "object" && !Array.isArray(parsed.capabilities)
535
- ? parsed.capabilities
536
- : {};
537
- const declared = runtime.capabilities.filter((tag) => typeof capabilities[tag] === "string" && capabilities[tag] !== "");
538
- // Declared is not resolvable: a module URL that is invalid or points off
539
- // the runtime's origin is a failed observation, not a silent omission.
540
- const outcomes = declared.map((tag) => {
541
- const module = resolveReference(capabilities[tag], url);
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 } : {}) };
517
+ let resolvable = 0;
518
+ // A page that does not pin a runtime declares no chunk URLs to check.
519
+ for (const tag of runtime.pinned ? runtime.capabilities : []) {
520
+ const declared = bootstrap.capabilities[tag];
521
+ const module = declared ? resolveReference(declared, origin) : undefined;
522
+ if (!module) {
523
+ capabilityUrls.push({ tag, url: "", failure: declared ? "invalid_url" : "undeclared" });
524
+ } else if (!context.allowedOrigins.has(module.origin)) {
525
+ capabilityUrls.push({ tag, url: module.href, failure: "origin_not_allowed" });
574
526
  } else {
575
- runtime.entry = { url: bounded(pointer.entry), ok: false, failure: "origin_not_allowed" };
527
+ resolvable += 1;
528
+ const checkable = capabilityUrls.filter((entry) => !entry.failure && !entry.unchecked).length;
529
+ capabilityUrls.push(checkable < DELIVERY_LIMITS.capabilities ? { tag, url: module.href } : { tag, url: module.href, unchecked: true });
576
530
  }
577
- } else {
578
- runtime.entry = { url: "", ok: false, failure: "pointer_unavailable" };
579
531
  }
580
532
  const capabilityResults = await mapBounded(capabilityUrls, async ({ tag, url, failure, unchecked }) => {
581
533
  if (failure) return { tag: bounded(tag), url: bounded(url), kind: "capability", status: 0, ok: false, attempts: 0, failure };
@@ -592,13 +544,10 @@ async function observe(context, origin, sampledRoutes) {
592
544
  unchecked: uncheckedCapabilities,
593
545
  items: capabilityResults.filter((row) => !row.ok && !row.unchecked).slice(0, DELIVERY_LIMITS.capabilities),
594
546
  };
595
- runtime.majorStreamMatches = Boolean(pointer?.ok) && pointer.majorVersion === runtime.expectedMajorVersion;
596
- runtime.pointerBehindFallback = Boolean(pointer?.ok && runtime.fallback?.ok)
597
- && compareVersions(pointer.version, runtime.fallback.version) === -1;
598
- runtime.capabilitiesResolvable = Boolean(pointer?.ok)
599
- && pointer.resolvableCapabilities === runtime.capabilities.length
547
+ runtime.capabilitiesResolvable = runtime.declared
548
+ && resolvable === runtime.capabilities.length
600
549
  && runtime.capabilityModules.failed === 0;
601
- runtime.compatible = runtime.declared && runtime.majorStreamMatches && runtime.entry.ok && runtime.capabilitiesResolvable;
550
+ runtime.compatible = runtime.declared && runtime.pinned && runtime.entry.ok && runtime.capabilitiesResolvable;
602
551
 
603
552
  const assetResults = await mapBounded(assetRows, async ({ url, kind }) => {
604
553
  const { row } = await probe(context, url, kind);
@@ -616,18 +565,14 @@ async function observe(context, origin, sampledRoutes) {
616
565
  ...assetResults.filter((row) => !row.ok).map((row) => `${row.kind} ${row.url}: ${row.failure}`),
617
566
  ...linkResults.filter((row) => !row.ok).map((row) => `link ${row.path}: ${row.failure}`),
618
567
  ...(runtime.declared ? [] : ["runtime: the home page declares no runtime bootstrap"]),
619
- ...(runtime.declared && !runtime.majorStreamMatches
620
- ? [`runtime: the major pointer serves major ${pointer?.majorVersion || "unknown"}, the page expects ${runtime.expectedMajorVersion}`]
621
- : []),
622
- ...(runtime.declared && !runtime.entry.ok ? [`runtime: entry ${runtime.entry.url || "(undeclared)"}: ${runtime.entry.failure}`] : []),
623
- ...(runtime.declared && runtime.fallback && !runtime.fallback.ok
624
- ? [`runtime: fallback ${runtime.fallback.url || "(undeclared)"}: ${runtime.fallback.failure}`]
568
+ ...(runtime.declared && !runtime.pinned
569
+ ? ["runtime: the page does not pin a runtime version and entry"]
625
570
  : []),
626
- ...(runtime.declared && !runtime.capabilitiesResolvable
627
- ? [`runtime: ${runtime.capabilities.length - (pointer?.declaredCapabilities ?? 0)} declared capability module(s) are missing from the major pointer and ${runtime.capabilityModules.failed} were invalid or did not load`]
571
+ ...(runtime.declared && runtime.pinned && !runtime.entry.ok
572
+ ? [`runtime: entry ${runtime.entry.url || "(undeclared)"} (version ${runtime.version}): ${runtime.entry.failure}`]
628
573
  : []),
629
- ...(runtime.pointerBehindFallback
630
- ? [`runtime: the major pointer (${pointer.version}) is behind the fallback runtime this site shipped with (${runtime.fallback.version}); returning browsers may be on an older runtime until it is promoted`]
574
+ ...(runtime.declared && runtime.pinned && !runtime.capabilitiesResolvable
575
+ ? [`runtime: ${runtime.capabilityModules.failed} of ${runtime.capabilities.length} declared capability module(s) were undeclared, invalid or did not load`]
631
576
  : []),
632
577
  ...([...localOnly].slice(0, DELIVERY_LIMITS.localReferences).map((reference) => `local-only reference: ${bounded(reference)}`)),
633
578
  ].map((value) => [...sanitizeDiagnostic(value, "")].slice(0, DELIVERY_LIMITS.urlScalars).join(""));
@@ -714,7 +659,7 @@ export async function checkDelivery({
714
659
  let observed = await observe(context, origin, sampledRoutes);
715
660
  // One bounded re-observation of everything, for edge propagation: it waits
716
661
  // the caller's budget once and observes again from the home page down, so a
717
- // recovered route, asset, pointer or bootstrap is seen fresh. It never loops.
662
+ // recovered route, asset or bootstrap is seen fresh. It never loops.
718
663
  let waited = 0;
719
664
  if (waitSeconds > 0 && observed.failures.length > 0) {
720
665
  await context.sleep(waitSeconds * 1000);
@@ -802,10 +747,9 @@ function withinOutputBudget(report, failuresTotal) {
802
747
  const { runtime } = report;
803
748
  report.runtime = {
804
749
  declared: runtime.declared,
805
- expectedMajorVersion: runtime.expectedMajorVersion,
750
+ version: runtime.version,
751
+ pinned: runtime.pinned,
806
752
  capabilities: runtime.capabilities.length,
807
- majorStreamMatches: runtime.majorStreamMatches,
808
- pointerBehindFallback: runtime.pointerBehindFallback,
809
753
  capabilitiesResolvable: runtime.capabilitiesResolvable,
810
754
  compatible: runtime.compatible,
811
755
  entryOk: Boolean(runtime.entry?.ok),
@@ -478,11 +478,10 @@ 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 declares the major stream, the mutable major pointer, the immutable fallback "
482
- + "copy and the capability modules. The pointer's major must match, its entry must load as JavaScript, every "
483
- + "declared capability must be resolvable, and a pointer behind the fallback's version is reported as a "
484
- + "stale runtime pointer that returning browsers may still be on.",
485
- "Browser: a fresh and a returning (same-context, cached) load are compared against the pointer's entry when "
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 whose bootstrap does not pin a runtime version and entry fails the runtime check.",
484
+ "Browser: a fresh and a returning (same-context, cached) load are compared against the page's pinned entry when "
486
485
  + "Playwright resolves from the CLI's own install: npm install --global @taprootio/site-authoring@latest "
487
486
  + "playwright && npx playwright install chromium, then rerun through the installed taproot-site command (a "
488
487
  + "copy run through npx lives in npm's cache and cannot see a global Playwright). A DevTools "
@@ -505,8 +504,8 @@ const WORKFLOW_REFERENCES = Object.freeze({
505
504
  + "not delivery), local-only references (localhost, *.test, *.local) are reported rather than fetched, a "
506
505
  + "redirect Location that could reflect a credential is withheld, bodies are read up to a fixed bound, "
507
506
  + "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, but an asset, "
509
- + "link, pointer or browser load did not match), failed (a route or the runtime did not deliver).",
507
+ "Verdicts: delivered (no findings), degraded (every route served and the runtime is compatible, "
508
+ + "but an asset, link or browser load did not match), failed (a route or the runtime did not deliver).",
510
509
  ]),
511
510
  example: Object.freeze({
512
511
  command: `${CLI_BINARY_NAME} delivery check --production --url https://www.example.com/ --wait 30`,