@tokenoftrust/storefront-runner 2.0.17 → 2.0.18

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.
Files changed (86) hide show
  1. package/apps/storefront/env.d.ts +18 -0
  2. package/apps/storefront/migrations-apps/0012_test_destinations.sql +29 -0
  3. package/apps/storefront/src/components/blog/ReactionBar.astro +1 -1
  4. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +365 -49
  5. package/apps/storefront/src/components/islands/checkoutVerificationFlow.ts +251 -0
  6. package/apps/storefront/src/config/compliance/rulesets.ts +18 -7
  7. package/apps/storefront/src/config/devTenantSeed.ts +24 -1
  8. package/apps/storefront/src/config/evidenceTenantRoster.ts +25 -0
  9. package/apps/storefront/src/config/publicationSpaceConfig.ts +25 -0
  10. package/apps/storefront/src/config/resolver.ts +5 -0
  11. package/apps/storefront/src/config/tenantAnalyticsFiles.ts +43 -0
  12. package/apps/storefront/src/config/tenants.ts +11 -0
  13. package/apps/storefront/src/layouts/Layout.astro +5 -1
  14. package/apps/storefront/src/lib/activity/killSwitch.ts +1 -1
  15. package/apps/storefront/src/lib/admin/consentSettingsClient.ts +113 -0
  16. package/apps/storefront/src/lib/apps/consentConfigRead.ts +24 -0
  17. package/apps/storefront/src/lib/apps/gtmMigrationDraft.ts +103 -0
  18. package/apps/storefront/src/lib/auth/loginGate.ts +25 -0
  19. package/apps/storefront/src/lib/checkoutCommerce.ts +1 -1
  20. package/apps/storefront/src/lib/compliance/enforcement.ts +25 -3
  21. package/apps/storefront/src/lib/d1/schema-apps.ts +44 -0
  22. package/apps/storefront/src/lib/env.ts +13 -111
  23. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +198 -0
  24. package/apps/storefront/src/lib/monitoring/manifest.ts +1 -1
  25. package/apps/storefront/src/lib/rawMarketingHtml.ts +9 -1
  26. package/apps/storefront/src/lib/runtimeEnv.ts +124 -0
  27. package/apps/storefront/src/lib/verification/totVerificationClient.ts +485 -0
  28. package/apps/storefront/src/middleware/candidatePin.ts +35 -0
  29. package/apps/storefront/src/middleware/controlPlane.ts +270 -0
  30. package/apps/storefront/src/middleware/index.ts +100 -796
  31. package/apps/storefront/src/middleware/publicRender.ts +43 -0
  32. package/apps/storefront/src/middleware/renderChain.ts +685 -0
  33. package/apps/storefront/src/middleware/routeBranch.ts +160 -0
  34. package/apps/storefront/src/middleware/staticBundle.ts +109 -0
  35. package/apps/storefront/src/pages/[...slug].astro +1 -0
  36. package/apps/storefront/src/pages/api/compliance/preflight.ts +179 -2
  37. package/apps/storefront/src/pages/index.astro +1 -0
  38. package/apps/storefront/vitest.config.ts +5 -0
  39. package/package.json +1 -1
  40. package/packages/public-runtime/package.json +1 -0
  41. package/packages/public-runtime/schemas/managed-analytics/v1/deliver-test-event-request.schema.json +122 -0
  42. package/packages/public-runtime/schemas/managed-analytics/v1/get-test-destination-delivery-request.schema.json +79 -0
  43. package/packages/public-runtime/schemas/managed-analytics/v1/register-test-destination-request.schema.json +93 -0
  44. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination-delivery.schema.json +106 -0
  45. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination.schema.json +77 -0
  46. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +195 -0
  47. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +80 -0
  48. package/packages/public-runtime/schemas/publication-space/v1/ref.schema.json +20 -0
  49. package/packages/public-runtime/schemas/publication-version/v1/pointer.schema.json +28 -0
  50. package/packages/public-runtime/schemas/publication-version/v1/transition-request.schema.json +44 -0
  51. package/packages/public-runtime/schemas/publication-version/v1/transition.schema.json +47 -0
  52. package/packages/public-runtime/schemas/publication-version/v1/version.schema.json +58 -0
  53. package/packages/public-runtime/src/activity/catalog.ts +1 -1
  54. package/packages/public-runtime/src/candidate-index.ts +6 -0
  55. package/packages/public-runtime/src/customization-reconcile.ts +8 -0
  56. package/packages/public-runtime/src/html-cache-policy.ts +57 -0
  57. package/packages/public-runtime/src/index.ts +7 -0
  58. package/packages/public-runtime/src/integration/consent-broker.ts +115 -0
  59. package/packages/public-runtime/src/integration/interim-bridge.ts +111 -27
  60. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +65 -0
  61. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +81 -0
  62. package/packages/public-runtime/src/integration/render-phases.ts +8 -3
  63. package/packages/public-runtime/src/publication-artifact.ts +203 -0
  64. package/packages/public-runtime/src/publication-space.ts +853 -0
  65. package/packages/public-runtime/src/publication-version.ts +587 -0
  66. package/packages/public-runtime/src/route-tier.ts +257 -0
  67. package/packages/public-runtime/src/static-bundle.ts +18 -5
  68. package/packages/public-runtime/src/tenant-assets.ts +85 -17
  69. package/packages/public-runtime/src/tenant.ts +70 -0
  70. package/packages/public-runtime/tests/fixtures/publication-spaces/internal.json +32 -0
  71. package/packages/public-runtime/tests/fixtures/publication-spaces/mounted-public.json +38 -0
  72. package/packages/public-runtime/tests/fixtures/publication-spaces/normalized-mounts.json +39 -0
  73. package/packages/public-runtime/tests/fixtures/publication-spaces/ref.json +5 -0
  74. package/packages/public-runtime/tests/fixtures/publication-spaces/root-public.json +31 -0
  75. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v1.json +20 -0
  76. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v2.json +20 -0
  77. package/packages/public-runtime/tests/fixtures/publication-versions/internal-v1.json +20 -0
  78. package/packages/public-runtime/tests/fixtures/publication-versions/live-pointer.json +11 -0
  79. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition-request.json +14 -0
  80. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition.json +17 -0
  81. package/packages/public-runtime/tests/fixtures/publication-versions/root-v1.json +20 -0
  82. package/packages/public-runtime/tests/fixtures/publication-versions/root-v2.json +20 -0
  83. package/scripts/dev/ai-edit.mjs +16 -3
  84. package/scripts/dev/port-check.mjs +18 -3
  85. package/scripts/dev/publish.mjs +26 -3
  86. package/scripts/tenant/validate.mjs +14 -1
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": 1,
3
+ "id": "articles",
4
+ "tenant": "fixture.example",
5
+ "source": {
6
+ "forge": "fixture",
7
+ "repo": "fixture-publications",
8
+ "defaultBranch": "main"
9
+ },
10
+ "routes": {
11
+ "canonicalMount": "/blog",
12
+ "aliases": [
13
+ {
14
+ "from": "/the-build",
15
+ "behavior": "redirect-308",
16
+ "to": "/blog"
17
+ }
18
+ ]
19
+ },
20
+ "visibility": "public",
21
+ "authorization": {
22
+ "policy": "public"
23
+ },
24
+ "discovery": {
25
+ "sitemap": true,
26
+ "feeds": true,
27
+ "publicSearch": true,
28
+ "llms": true
29
+ },
30
+ "assets": {
31
+ "namespace": "articles",
32
+ "access": "public"
33
+ },
34
+ "renderer": {
35
+ "contractVersion": "storefront-v1",
36
+ "themeContractVersion": "theme-v1"
37
+ }
38
+ }
@@ -0,0 +1,39 @@
1
+ [
2
+ {
3
+ "version": 1,
4
+ "tenant": "fixture.example",
5
+ "spaceId": "knowledge",
6
+ "kind": "canonical",
7
+ "mount": "/knowledge",
8
+ "canonicalMount": "/knowledge",
9
+ "behavior": "render"
10
+ },
11
+ {
12
+ "version": 1,
13
+ "tenant": "fixture.example",
14
+ "spaceId": "articles",
15
+ "kind": "alias",
16
+ "mount": "/the-build",
17
+ "canonicalMount": "/blog",
18
+ "behavior": "redirect-308",
19
+ "redirectTo": "/blog"
20
+ },
21
+ {
22
+ "version": 1,
23
+ "tenant": "fixture.example",
24
+ "spaceId": "articles",
25
+ "kind": "canonical",
26
+ "mount": "/blog",
27
+ "canonicalMount": "/blog",
28
+ "behavior": "render"
29
+ },
30
+ {
31
+ "version": 1,
32
+ "tenant": "fixture.example",
33
+ "spaceId": "root",
34
+ "kind": "canonical",
35
+ "mount": "/",
36
+ "canonicalMount": "/",
37
+ "behavior": "render"
38
+ }
39
+ ]
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": 1,
3
+ "tenant": "fixture.example",
4
+ "spaceId": "articles"
5
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "version": 1,
3
+ "id": "root",
4
+ "tenant": "fixture.example",
5
+ "source": {
6
+ "forge": "fixture",
7
+ "repo": "fixture.example",
8
+ "defaultBranch": "main"
9
+ },
10
+ "routes": {
11
+ "canonicalMount": "/"
12
+ },
13
+ "visibility": "public",
14
+ "authorization": {
15
+ "policy": "public"
16
+ },
17
+ "discovery": {
18
+ "sitemap": true,
19
+ "feeds": true,
20
+ "publicSearch": true,
21
+ "llms": true
22
+ },
23
+ "assets": {
24
+ "namespace": "root",
25
+ "access": "public"
26
+ },
27
+ "renderer": {
28
+ "contractVersion": "storefront-v1",
29
+ "themeContractVersion": "theme-v1"
30
+ }
31
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_articles-version-0001",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "articles",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-publications",
9
+ "commitSha": "3333333333333333333333333333333333333333"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
16
+ "createdAt": "2026-09-01T12:02:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-articles-0001"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_articles-version-0002",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "articles",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-publications",
9
+ "commitSha": "4444444444444444444444444444444444444444"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
16
+ "createdAt": "2026-09-01T12:03:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-articles-0002"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_knowledge-version-0001",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "knowledge",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture-private-publications",
9
+ "commitSha": "5555555555555555555555555555555555555555"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
16
+ "createdAt": "2026-09-01T12:04:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-knowledge-0001"
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "tenant": "fixture.example",
4
+ "spaceId": "articles",
5
+ "channel": "live",
6
+ "revision": 1,
7
+ "versionId": "pv_articles-version-0001",
8
+ "sourceSha": "3333333333333333333333333333333333333333",
9
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
10
+ "updatedAt": "2026-09-01T12:05:00.000Z"
11
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "transitionId": "activate-articles-0001",
4
+ "operation": "activate",
5
+ "tenant": "fixture.example",
6
+ "spaceId": "articles",
7
+ "channel": "live",
8
+ "targetVersionId": "pv_articles-version-0001",
9
+ "expectedSourceSha": "3333333333333333333333333333333333333333",
10
+ "expectedArtifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
11
+ "expectedRevision": 0,
12
+ "principal": "publisher.fixture",
13
+ "occurredAt": "2026-09-01T12:05:00.000Z"
14
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "transitionId": "activate-articles-0001",
4
+ "operation": "activate",
5
+ "tenant": "fixture.example",
6
+ "spaceId": "articles",
7
+ "channel": "live",
8
+ "previous": null,
9
+ "next": {
10
+ "revision": 1,
11
+ "versionId": "pv_articles-version-0001",
12
+ "sourceSha": "3333333333333333333333333333333333333333",
13
+ "artifactDigest": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
14
+ },
15
+ "principal": "publisher.fixture",
16
+ "occurredAt": "2026-09-01T12:05:00.000Z"
17
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_root-version-0001",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "root",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture.example",
9
+ "commitSha": "1111111111111111111111111111111111111111"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
16
+ "createdAt": "2026-09-01T12:00:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-root-0001"
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "versionId": "pv_root-version-0002",
4
+ "tenant": "fixture.example",
5
+ "spaceId": "root",
6
+ "source": {
7
+ "forge": "fixture",
8
+ "repo": "fixture.example",
9
+ "commitSha": "2222222222222222222222222222222222222222"
10
+ },
11
+ "renderer": {
12
+ "contractVersion": "storefront-v1",
13
+ "themeContractVersion": "theme-v1"
14
+ },
15
+ "artifactDigest": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
16
+ "createdAt": "2026-09-01T12:01:00.000Z",
17
+ "metadata": {
18
+ "buildId": "build-root-0002"
19
+ }
20
+ }
@@ -25,6 +25,7 @@ import { createMcpClient } from "../../packages/cli/src/mcp.mjs";
25
25
  import { resolveDeveloperSession, AuthUnavailableError } from "../../packages/cli/src/auth.mjs";
26
26
  import { applyUnifiedDiff } from "./unified-diff.mjs";
27
27
  import { tenantDirSegments } from "../lib/tenant-dirs.mjs";
28
+ import { recordDiagnostic } from "../../packages/cli/src/diagnostics.mjs";
28
29
 
29
30
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
30
31
 
@@ -83,7 +84,7 @@ async function resolveClient(mcpBaseUrl) {
83
84
  * not-configured all come back as `{ ok:false, status, message }`).
84
85
  */
85
86
  export async function handleAiEdit({ repoRoot, tenantId, prompt, mcpBaseUrl }, deps = {}) {
86
- const { resolveClientImpl = resolveClient } = deps;
87
+ const { resolveClientImpl = resolveClient, env = process.env } = deps;
87
88
  const paths = tenantPaths(repoRoot, tenantId);
88
89
  const files = [
89
90
  { path: "content/home.html", content: readFileIfPresent(paths.home) },
@@ -98,14 +99,26 @@ export async function handleAiEdit({ repoRoot, tenantId, prompt, mcpBaseUrl }, d
98
99
  try {
99
100
  client = await resolveClientImpl(mcpBaseUrl);
100
101
  } catch (e) {
101
- return { ok: false, status: "unauthenticated", message: e.message };
102
+ const diagnostic = recordDiagnostic(e, {
103
+ command: "dev-ai-edit", operation: "session_initialize", required: true,
104
+ }, env);
105
+ return {
106
+ ok: false, status: "unauthenticated", message: e.message,
107
+ diagnosticId: diagnostic.diagnosticId,
108
+ };
102
109
  }
103
110
 
104
111
  let result;
105
112
  try {
106
113
  result = await client.callTool("content_edit", { prompt, files });
107
114
  } catch (e) {
108
- return { ok: false, status: "error", message: e.message };
115
+ const diagnostic = recordDiagnostic(e, {
116
+ command: "dev-ai-edit", operation: "content_edit", required: true,
117
+ }, env);
118
+ return {
119
+ ok: false, status: "error", message: e.message,
120
+ diagnosticId: diagnostic.diagnosticId,
121
+ };
109
122
  }
110
123
  if (result?.status && result.status !== "ok") {
111
124
  return { ok: false, status: result.status, message: result.message || "content_edit refused the request." };
@@ -18,19 +18,34 @@ import net from "node:net";
18
18
  * @param {number} port
19
19
  * @returns {Promise<boolean>}
20
20
  */
21
- export function isPortFree(port) {
21
+ /**
22
+ * @param {number} port
23
+ * @param {string | undefined} [host]
24
+ * @param {boolean} [unsupportedIsFree]
25
+ */
26
+ function canBind(port, host = undefined, unsupportedIsFree = false) {
22
27
  return new Promise((resolve) => {
23
28
  const srv = net.createServer();
24
- srv.once("error", () => resolve(false));
29
+ srv.once("error", (error) => {
30
+ const code = /** @type {NodeJS.ErrnoException} */ (error).code;
31
+ const unsupported = code === "EAFNOSUPPORT" || code === "EADDRNOTAVAIL";
32
+ resolve(unsupportedIsFree && unsupported);
33
+ });
25
34
  srv.once("listening", () => srv.close(() => resolve(true)));
26
35
  try {
27
- srv.listen(port);
36
+ srv.listen({ port, ...(host ? { host } : {}) });
28
37
  } catch {
29
38
  resolve(false);
30
39
  }
31
40
  });
32
41
  }
33
42
 
43
+ export async function isPortFree(port) {
44
+ if (!(await canBind(port))) return false;
45
+ if (!(await canBind(port, "127.0.0.1"))) return false;
46
+ return canBind(port, "::1", true);
47
+ }
48
+
34
49
  /**
35
50
  * The first free port at or after `preferred` (scanning upward). Lets `tot dev`
36
51
  * boot on a busy default port by moving to the next open one and printing it —
@@ -32,6 +32,7 @@ import { matchesWriteAllowlist } from "./safe-path.mjs";
32
32
  import { resolveTenantSourceRoot } from "./tenant-source-root.mjs";
33
33
  import { createMcpClient } from "../../packages/cli/src/mcp.mjs";
34
34
  import { resolveDeveloperSession, AuthUnavailableError } from "../../packages/cli/src/auth.mjs";
35
+ import { recordDiagnostic } from "../../packages/cli/src/diagnostics.mjs";
35
36
 
36
37
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
37
38
 
@@ -90,13 +91,26 @@ export async function publishFile(env, sourceRoot, tenantId, relPath, deps = {})
90
91
  session = await (deps.resolveSession || resolveDeveloperSession)(client, env);
91
92
  await client.initialize();
92
93
  } catch (e) {
94
+ const diagnostic = recordDiagnostic(e, {
95
+ command: "dev-publish", operation: "session_initialize", required: true,
96
+ }, env);
93
97
  if (e instanceof AuthUnavailableError) {
94
98
  return {
95
99
  status: 503,
96
- json: { ok: false, status: "not_signed_in", message: `${e.message}${e.hint ? ` — ${e.hint}` : ""}` },
100
+ json: {
101
+ ok: false, status: "not_signed_in",
102
+ message: `${e.message}${e.hint ? ` — ${e.hint}` : ""}`,
103
+ diagnosticId: diagnostic.diagnosticId,
104
+ },
97
105
  };
98
106
  }
99
- return { status: 502, json: { ok: false, status: "error", message: String(e?.message || e) } };
107
+ return {
108
+ status: 502,
109
+ json: {
110
+ ok: false, status: "error", message: String(e?.message || e),
111
+ diagnosticId: diagnostic.diagnosticId,
112
+ },
113
+ };
100
114
  }
101
115
 
102
116
  const repo = `${tenantId}-main`;
@@ -127,7 +141,16 @@ export async function publishFile(env, sourceRoot, tenantId, relPath, deps = {})
127
141
  },
128
142
  };
129
143
  } catch (e) {
130
- return { status: 502, json: { ok: false, status: "error", message: String(e?.message || e) } };
144
+ const diagnostic = recordDiagnostic(e, {
145
+ command: "dev-publish", operation: "candidate_open", required: true,
146
+ }, env);
147
+ return {
148
+ status: 502,
149
+ json: {
150
+ ok: false, status: "error", message: String(e?.message || e),
151
+ diagnosticId: diagnostic.diagnosticId,
152
+ },
153
+ };
131
154
  }
132
155
  }
133
156
 
@@ -738,7 +738,12 @@ export function discoverTenantIds() {
738
738
 
739
739
  // --- link / asset extraction -------------------------------------------------
740
740
  const HREF_RE = /\bhref\s*=\s*"([^"]*)"/gi;
741
- const SRC_RE = /\b(?:src|srcset)\s*=\s*"([^"]*)"/gi;
741
+ const SRC_RE = /\bsrc\s*=\s*"([^"]*)"/gi;
742
+ // `srcset` carries a comma-separated list of `url descriptor` pairs (e.g.
743
+ // "a.webp 480w, b.webp 800w"), not a single URL — each candidate must be split
744
+ // off its descriptor before being checked as an asset. Mirrors
745
+ // scripts/publish/lib/asset-reachability.mjs's srcset handling.
746
+ const SRCSET_RE = /\bsrcset\s*=\s*"([^"]*)"/gi;
742
747
  const STYLE_OPEN_WITH_ATTRS_RE = /<style\s+[^>]*>/i;
743
748
  /** An in-page skip link: an anchor to a #main-ish target, or one carrying a skip class. */
744
749
  const SKIP_LINK_RE = /<a\b[^>]*(?:class="[^"]*\bskip[-\w]*\b[^"]*"|href="#(?:main|content|main-content)\b")/i;
@@ -981,6 +986,14 @@ export function validateTenant(tenantDir, opts = {}) {
981
986
  const f = checkAsset(m[1].trim(), r, publicDir, opts.tenantId || config?.tenant);
982
987
  if (f) findings.push(f);
983
988
  }
989
+ for (const m of html.matchAll(SRCSET_RE)) {
990
+ for (const candidate of m[1].split(",")) {
991
+ const url = candidate.trim().split(/\s+/)[0];
992
+ if (!url) continue;
993
+ const f = checkAsset(url, r, publicDir, opts.tenantId || config?.tenant);
994
+ if (f) findings.push(f);
995
+ }
996
+ }
984
997
  }
985
998
 
986
999
  // 5. git conflict markers — advisory (never blocks), but LOUD: a half-resolved