@salesforce/storefront-next-dev 1.4.0-alpha.1 → 1.4.1-alpha.0

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 (136) hide show
  1. package/README.md +13 -2
  2. package/dist/assets.js +25 -0
  3. package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
  4. package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
  5. package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
  6. package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
  7. package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
  8. package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
  9. package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
  10. package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
  11. package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
  12. package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
  13. package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
  14. package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
  15. package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
  16. package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
  17. package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
  18. package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
  19. package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
  20. package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
  21. package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
  22. package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
  23. package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
  24. package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
  25. package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
  26. package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
  27. package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
  28. package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
  29. package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
  30. package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
  31. package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
  32. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
  33. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
  34. package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
  35. package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
  36. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
  37. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
  38. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
  39. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
  40. package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
  41. package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
  42. package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
  43. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
  44. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
  45. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
  46. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
  47. package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
  48. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
  49. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
  50. package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
  51. package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
  52. package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
  53. package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
  54. package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
  55. package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
  56. package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
  57. package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
  58. package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
  59. package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
  60. package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
  61. package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
  62. package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
  63. package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
  64. package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
  65. package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
  66. package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
  67. package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
  68. package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
  69. package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
  70. package/dist/audit/instructions/evaluate-rule.md +42 -0
  71. package/dist/audit/schemas/audit-result.schema.json +1237 -0
  72. package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
  73. package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
  74. package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
  75. package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
  76. package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
  77. package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
  78. package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
  79. package/dist/audit/ui/assets/index-BKCWlO9b.css +1 -0
  80. package/dist/audit/ui/assets/index-CUPt8e4A.js +44 -0
  81. package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
  82. package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
  83. package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
  84. package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
  85. package/dist/audit/ui/einstein.png +0 -0
  86. package/dist/audit/ui/index.html +13 -0
  87. package/dist/audit.js +1896 -0
  88. package/dist/cartridge-services/index.d.ts +5 -0
  89. package/dist/cartridge-services/index.d.ts.map +1 -1
  90. package/dist/cartridge-services/index.js +94 -7
  91. package/dist/cartridge-services/index.js.map +1 -1
  92. package/dist/catalog.js +89 -0
  93. package/dist/commands/audit/catalog.js +27 -0
  94. package/dist/commands/audit/finalize.js +42 -0
  95. package/dist/commands/audit/prepare.js +84 -0
  96. package/dist/commands/audit/progress.js +51 -0
  97. package/dist/commands/audit/serve.js +63 -0
  98. package/dist/commands/config/aggregate-extensions.js +1 -0
  99. package/dist/commands/create-bundle.js +2 -1
  100. package/dist/commands/create-instructions.js +1 -0
  101. package/dist/commands/create-storefront.js +1 -0
  102. package/dist/commands/deploy-cartridge.js +11 -3
  103. package/dist/commands/dev.js +2 -0
  104. package/dist/commands/extensions/create.js +1 -0
  105. package/dist/commands/extensions/install.js +1 -0
  106. package/dist/commands/extensions/list.js +1 -0
  107. package/dist/commands/extensions/remove.js +1 -0
  108. package/dist/commands/generate-cartridge.js +49 -5
  109. package/dist/commands/locales/aggregate-extensions.js +1 -0
  110. package/dist/commands/prepare-local.js +1 -0
  111. package/dist/commands/preview.js +2 -0
  112. package/dist/commands/push.js +3 -1
  113. package/dist/commands/validate-cartridge.js +13 -4
  114. package/dist/config.js +13 -2
  115. package/dist/document-size.js +231 -0
  116. package/dist/events.js +688 -0
  117. package/dist/flags2.js +34 -0
  118. package/dist/generate-cartridge.js +41 -6
  119. package/dist/har.js +205 -0
  120. package/dist/images.js +146 -0
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/live.js +277 -0
  123. package/dist/logger.js +1 -4
  124. package/dist/measured-run.js +91 -0
  125. package/dist/metric-budget.js +125 -0
  126. package/dist/mrt/ssr.mjs +19 -19
  127. package/dist/mrt/ssr.mjs.map +1 -1
  128. package/dist/mrt/streamingHandler.mjs +26 -26
  129. package/dist/mrt/streamingHandler.mjs.map +1 -1
  130. package/dist/package.js +5 -0
  131. package/dist/sanitize.js +185 -0
  132. package/dist/schema.js +121 -0
  133. package/dist/server.js +8 -60
  134. package/dist/server2.js +185 -0
  135. package/dist/ts-import.js +57 -0
  136. package/package.json +21 -4
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: performance/ttfb-budget
3
+ title: Time to First Byte (TTFB)
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.summary]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's server response time from its representative mobile cold-cache run is 800 ms or less. The representative run is the run selected by median LCP. TTFB isolates the time from sending the document request until its first response byte, without counting DNS, TCP, TLS, or redirect time.
11
+ ## Inspect
12
+ The rule script reads each page's `summary.json` and takes the mobile cold-cache cell's `medianRun.ttfb`, which Playhouse derives from Lighthouse's `server-response-time` audit. A valid run must have both a positive LCP and a positive TTFB. Playhouse can write 0 for either metric when it could not measure it, so those runs are left out. When any run is invalid, the script picks the median again from the valid runs the way Playhouse does (the run closest to the median LCP, ties to the earlier run) and reads that run's `runs[].row.ttfb`.
13
+ ## Evaluate
14
+ Pass when every measured page's representative-run TTFB is at or under 800 ms; fail otherwise. A failure is medium severity, or high above 1,800 ms. A page without a mobile cold-cache cell, or without any measured run, is skipped. If no page can be measured, the rule is not decided.
15
+ ## Evidence
16
+ One `summary.json` pointer per page, `/cells/<i>/medianRun/ttfb` (or `/runs/<j>/row/ttfb` when runs were left out), with the measured value, the measured min–max spread when several runs exist, and how many runs were not measured. The verdict stays limited to the stable server-response measurement; captured network timing and response headers can be inspected separately when diagnosing a failure.
17
+ ## Remediation
18
+ Use application and platform traces, plus existing response timing and cache headers when available, to locate the delay. Parallelize independent server work and reduce blocking SCAPI or CMS calls. Do not prescribe shared HTML caching without confirming with the merchant that the response does not vary by shopper. Re-run the same Playhouse capture to confirm the mobile cold-cache representative-run TTFB is at or under 800 ms.
19
+ ## Examples
20
+ Compliant: home representative-run TTFB 480 ms. Problematic: PLP representative-run TTFB 1,250 ms.
@@ -0,0 +1,17 @@
1
+ # Security
2
+
3
+ ## Purpose and boundaries
4
+
5
+ Trace trust boundaries and deployed response evidence. Combine final response headers with Playhouse diagnostics where runtime behavior matters. Never persist secret values; cite only redacted key names and locations.
6
+
7
+ ## Investigation sequence
8
+
9
+ Map client/server boundaries, protected routes, sinks, and configuration before evaluating each rule. For CSP, identify the final document policy first, then correlate console, Inspector Issue, and blocked-request evidence across representative cells.
10
+
11
+ ## Evidence and redaction
12
+
13
+ Redact credentials and personal data before writing any artifact. Playhouse console and diagnostic text can contain arbitrary application data; cite stable reason codes and sanitized URLs instead of copying full messages.
14
+
15
+ ## Severity and blind spots
16
+
17
+ Dependency and infrastructure controls need current external evidence. A CSP diagnostic is not automatically a failure: the policy may be correctly blocking an optional or malicious request.
@@ -0,0 +1,37 @@
1
+ import { a as harRuleDataSchema, f as tokens, i as harRule, o as header } from "../../../../../har.js";
2
+
3
+ //#region src/audit/catalog/categories/security/rules/browser-hardening-headers.ts
4
+ const dataSchema = harRuleDataSchema;
5
+ const REFERRER_POLICIES = [
6
+ "no-referrer",
7
+ "same-origin",
8
+ "strict-origin",
9
+ "strict-origin-when-cross-origin"
10
+ ];
11
+ var browser_hardening_headers_default = harRule("HTML page responses", ({ document }) => {
12
+ const { entry } = document;
13
+ const headers = {
14
+ "x-content-type-options": header(entry, "x-content-type-options"),
15
+ "x-frame-options": header(entry, "x-frame-options"),
16
+ "referrer-policy": header(entry, "referrer-policy"),
17
+ "permissions-policy": header(entry, "permissions-policy")
18
+ };
19
+ const reasons = [];
20
+ if (headers["x-content-type-options"]?.trim().toLowerCase() !== "nosniff") reasons.push("X-Content-Type-Options is not nosniff.");
21
+ if (!/(?:^|[;,])\s*frame-ancestors\b/i.test(header(entry, "content-security-policy") ?? "") && !headers["x-frame-options"]) reasons.push("Neither CSP frame-ancestors nor X-Frame-Options is set.");
22
+ const referrer = tokens(headers["referrer-policy"]).at(-1);
23
+ if (!referrer) reasons.push("Referrer-Policy is missing.");
24
+ else if (!REFERRER_POLICIES.includes(referrer)) reasons.push(`Referrer-Policy ${referrer} discloses more than the baseline allows.`);
25
+ if (!headers["permissions-policy"]) reasons.push("Permissions-Policy is missing.");
26
+ return {
27
+ responses: 1,
28
+ failures: reasons.length ? [{
29
+ entry: document,
30
+ reasons,
31
+ headers
32
+ }] : []
33
+ };
34
+ }, "send nosniff, block framing by other sites, limit the referrer and set a Permissions-Policy");
35
+
36
+ //#endregion
37
+ export { dataSchema, browser_hardening_headers_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: security/browser-hardening-headers
3
+ title: Basic Security Headers
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's HTML response sends four standard security headers: `X-Content-Type-Options: nosniff`, so browsers don't run a file as a different type than declared; CSP `frame-ancestors` or `X-Frame-Options`, so other sites can't show the page in a hidden frame and trick clicks (clickjacking); a `Referrer-Policy` that doesn't send full page URLs to other sites; and a `Permissions-Policy` that limits browser features such as the camera or location.
11
+ ## Inspect
12
+ The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. It reads `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` and CSP `frame-ancestors`. If no page's document can be matched, the rule is not decided.
13
+ ## Evaluate
14
+ Fail when `X-Content-Type-Options` is not `nosniff`; neither CSP `frame-ancestors` nor `X-Frame-Options` is set; `Referrer-Policy` is missing or its effective (last) value is not `no-referrer`, `same-origin`, `strict-origin` or `strict-origin-when-cross-origin`; or `Permissions-Policy` is missing. Pass otherwise.
15
+ ## Evidence
16
+ The HAR entry of each failing document, with the reasons and the four header values.
17
+ ## Remediation
18
+ Restore the Storefront Next security-header defaults at the response owner or public edge (Managed Runtime, CDN or application), keeping only documented integration-specific exceptions, then re-run Playhouse and confirm the headers on the final document response.
19
+ ## Examples
20
+ Compliant: `nosniff`, `strict-origin-when-cross-origin`, CSP `frame-ancestors 'self'` and a `Permissions-Policy`. Problematic: no `frame-ancestors` and no `X-Frame-Options`.
@@ -0,0 +1,40 @@
1
+ import { a as harRuleDataSchema, i as harRule, o as header } from "../../../../../har.js";
2
+
3
+ //#region src/audit/catalog/categories/security/rules/content-security-policy.ts
4
+ const dataSchema = harRuleDataSchema;
5
+ /** Each comma-separated policy as directive → lowercased sources; every enforced policy applies. */
6
+ function policies(value) {
7
+ return (value ?? "").split(",").map((policy) => new Map(policy.split(";").flatMap((directive) => {
8
+ const [name, ...sources] = directive.trim().toLowerCase().split(/\s+/);
9
+ return name ? [[name, sources]] : [];
10
+ }))).filter((policy) => policy.size);
11
+ }
12
+ var content_security_policy_default = harRule("HTML page responses", ({ document }) => {
13
+ const { entry } = document;
14
+ const headers = {
15
+ "content-security-policy": header(entry, "content-security-policy"),
16
+ "content-security-policy-report-only": header(entry, "content-security-policy-report-only")
17
+ };
18
+ const enforced = policies(headers["content-security-policy"]);
19
+ const reasons = [];
20
+ if (!enforced.length) reasons.push(headers["content-security-policy-report-only"] ? "Content-Security-Policy is report-only." : "Content-Security-Policy is missing.");
21
+ else {
22
+ if (enforced.some((policy) => [...policy.values()].some((sources) => sources.includes("'unsafe-eval'")))) reasons.push("CSP allows 'unsafe-eval'.");
23
+ if (!enforced.some((policy) => {
24
+ const sources = policy.get("script-src") ?? policy.get("default-src");
25
+ return sources !== void 0 && !sources.includes("*");
26
+ })) reasons.push("CSP does not restrict scripts: script-src (or default-src without script-src) is missing or allows *.");
27
+ for (const directive of ["frame-ancestors", "form-action"]) if (!enforced.some((policy) => policy.has(directive))) reasons.push(`CSP ${directive} is missing.`);
28
+ }
29
+ return {
30
+ responses: 1,
31
+ failures: reasons.length ? [{
32
+ entry: document,
33
+ reasons,
34
+ headers
35
+ }] : []
36
+ };
37
+ }, "enforce a Content Security Policy that restricts scripts, framing and form targets and doesn't allow 'unsafe-eval'");
38
+
39
+ //#endregion
40
+ export { dataSchema, content_security_policy_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: security/content-security-policy
3
+ title: Content Security Policy (CSP)
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's HTML response sends an enforced `Content-Security-Policy` header (report-only isn't enough) that restricts where scripts can come from, doesn't allow `'unsafe-eval'`, and sets `frame-ancestors` and `form-action`. A CSP limits what an injected script can do.
11
+ ## Inspect
12
+ The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. It parses `Content-Security-Policy` (every enforced policy) and notes `Content-Security-Policy-Report-Only`. If no page's document can be matched, the rule is not decided.
13
+ ## Evaluate
14
+ Fail when there is no enforcing CSP (report-only alone fails); any directive allows `'unsafe-eval'`; no policy restricts scripts (`script-src`, or `default-src` when `script-src` is absent, is missing or contains `*`); or `frame-ancestors` or `form-action` is missing. `'wasm-unsafe-eval'` is not `'unsafe-eval'`. Pass otherwise.
15
+ ## Evidence
16
+ The HAR entry of each failing document, with the reasons and the CSP header values.
17
+ ## Remediation
18
+ Start from the Storefront Next CSP defaults, keep nonce/hash-based script protection, and extend only the directives audited integrations need. Remove `'unsafe-eval'`, add `frame-ancestors` and `form-action`, and use report-only only as a temporary rollout step before enforcing. Re-run Playhouse and confirm the header on the final document response.
19
+ ## Examples
20
+ Compliant: `script-src 'self' 'strict-dynamic' 'nonce-…'; frame-ancestors 'self'; form-action 'self'`. Problematic: only `Content-Security-Policy-Report-Only`, or `script-src * 'unsafe-eval'`.
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: security/csp-runtime-compatibility
3
+ title: Site Features Blocked by the CSP
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.diagnostics, playhouse.network-har, playhouse.document, source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks what the Content Security Policy blocked while the pages loaded, from the browser console and DevTools issues, and whether any of it is something the storefront needs: its own scripts, checkout or account features, or an approved integration. Blocked probes, browser extensions and one-off messages are ignored.
11
+ ## Inspect
12
+ For every representative Playhouse cell, inspect diagnostics `sourceStatus`, normalized `contentSecurityPolicyIssue` counts (from CDP `ContentSecurityPolicyIssue`) and `contentSecurityPolicyViolationType:*` reason codes, CSP-related warning/error entries, and network failures with a CSP blocked reason. Correlate sanitized hosts, resource types, and source locations to first-party code or an owned integration, then confirm the relevant deployed directive in the HAR and its source configuration. Identify whether the update belongs in Storefront Next CSP configuration, a tag manager/container, customer edge configuration, or vendor code.
13
+
14
+ ## Evaluate
15
+ Create a finding only when repeated evidence shows CSP blocks a required first-party resource, checkout/account behavior, or an approved integration. Probes, malicious attempts, browser extensions, and isolated unexplained messages don't fail the rule. A blocked optional tracker that needs ownership review is a `low` finding that must name the affected integration or host, resource type, likely owner surface, and safe next action. Report-only violations are rollout evidence, not proof of enforcement. Use `not-verified` only when diagnostics sources are missing or invalid.
16
+
17
+ ## Evidence
18
+ Cite page, form factor, cache state, diagnostics source status, issue type/count, stable reason codes, sanitized blocked host/path, resource type, likely integration owner, and relevant configuration or source location. Quote only the minimum sanitized console fragment needed; console text can contain arbitrary customer or application data.
19
+
20
+ ## Remediation
21
+ State where the change belongs. For an approved blocked resource, prefer the exact HTTPS origin in the narrow resource directive over broadening policy globally. For tag-manager integrations, identify the container/tag owner and update or remove the owning tag before changing application CSP; if ownership is unresolved, keep it a `low` finding that asks for that decision rather than proposing an allowlist. For code that requires `eval`, remove, replace, or obtain a CSP-compatible vendor implementation; never recommend `unsafe-eval` as the shortcut. Repeat the affected journey and confirm required functionality works without introducing a high-risk CSP exception.
22
+
23
+ ## Examples
24
+ Compliant: diagnostics were captured and required storefront resources have no CSP failures; no change is required. Problematic: every PDP capture blocks a required first-party product script; cite the source/configuration owner and narrow directive. Ambiguous: an image analytics pixel is blocked; record the exact host and `Image` resource type, ask the tag owner whether it is required, then either remove the tag or allow that origin only in `img-src`. A vendor script attempts `eval`; recommend removing or replacing that integration, never enabling `unsafe-eval`.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: security/dependency-vulnerabilities
3
+ title: Known Vulnerabilities in Dependencies
4
+ severity: high
5
+ appliesTo: [project]
6
+ inputs: [source.manifests, dependency.advisories]
7
+ ---
8
+ ## Intent
9
+ Checks the production npm dependencies against a current security advisory scan from the package manager, and reports advisories that affect a version the storefront ships and code it actually uses. Without a current scan, the rule is left for manual review.
10
+ ## Inspect
11
+ Identify the production dependency graph and obtain a current advisory scan from the ecosystem package manager or approved scanner; do not infer safety from lockfile text alone.
12
+ ## Evaluate
13
+ Report a finding only when a current advisory maps to a shipped version and reachable production use, or use `not-verified` when no current scan is available.
14
+ ## Evidence
15
+ Cite package, installed version, advisory identifier, scanner timestamp, dependency path, and reachability assessment; never include registry credentials.
16
+ ## Remediation
17
+ Upgrade, replace, remove, or document a time-bound mitigation, then rerun the same scanner.
18
+ ## Examples
19
+ Compliant: current scan with no applicable production advisories. Problematic: reachable critical advisory. Ambiguous: stale scan or development-only dependency.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: security/dynamic-code-execution
3
+ title: eval and new Function With Untrusted Input
4
+ severity: high
5
+ appliesTo: [project]
6
+ inputs: [source.code]
7
+ ---
8
+ ## Intent
9
+ Checks every use of `eval` and `new Function` for input an attacker could influence, such as URL parameters, CMS content or API responses. Running that input as code would let an attacker run their own JavaScript on the site.
10
+ ## Inspect
11
+ Trace eval and Function constructor inputs across trust boundaries.
12
+ ## Evaluate
13
+ Finding requires dynamic or untrusted input; constant tooling code may be not-applicable.
14
+ ## Evidence
15
+ Cite source, transformations, and sink.
16
+ ## Remediation
17
+ Replace string execution with data-driven logic.
18
+ ## Examples
19
+ Compliant: no dynamic execution. Problematic: CMS value reaches eval. Ambiguous: compile-time constant unresolved.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: security/hardcoded-credentials
3
+ title: Credentials in Source Code
4
+ severity: critical
5
+ appliesTo: [project]
6
+ inputs: [source.code]
7
+ ---
8
+ ## Intent
9
+ Searches the project's source for real credentials written into code or config, such as API keys, passwords, client secrets and private keys, ignoring test fixtures and examples. Anyone with access to the repository can read them.
10
+ ## Inspect
11
+ Search high-confidence credential formats and assignments while excluding fixtures.
12
+ ## Evaluate
13
+ Finding requires a real credential-like secret in shipping source.
14
+ ## Evidence
15
+ Cite credential type and location only; replace value with [redacted].
16
+ ## Remediation
17
+ Remove, rotate, and load from a server-side secret manager.
18
+ ## Examples
19
+ Compliant: environment lookup. Problematic: private key literal. Ambiguous: documented placeholder.
@@ -0,0 +1,46 @@
1
+ import "../../../../../har.js";
2
+ import { a as measurePage } from "../../../../../document-size.js";
3
+ import { z } from "zod";
4
+
5
+ //#region src/audit/catalog/categories/security/rules/html-config-secrets.ts
6
+ const dataSchema = z.object({
7
+ pages: z.array(z.object({
8
+ page: z.string(),
9
+ url: z.string(),
10
+ appConfig: z.array(z.string()),
11
+ pageData: z.array(z.string()),
12
+ other: z.array(z.string())
13
+ }).strict()),
14
+ skipped: z.array(z.object({
15
+ page: z.string(),
16
+ reason: z.string()
17
+ }).strict())
18
+ }).strict();
19
+ /** Lists the inline scripts archived from each page's HTML for the agent to review; secrets can't be recognized by a script, so this never decides. */
20
+ const listInlineScripts = async ({ pages, playhouse }) => {
21
+ const data = {
22
+ pages: [],
23
+ skipped: []
24
+ };
25
+ for (const page of pages) {
26
+ const size = await measurePage(playhouse, page.label);
27
+ if ("reason" in size) data.skipped.push({
28
+ page: page.label,
29
+ reason: size.reason
30
+ });
31
+ else if (size.markupBytes === null) data.skipped.push({
32
+ page: page.label,
33
+ reason: "Playhouse archived no inline scripts for this page."
34
+ });
35
+ else data.pages.push({
36
+ page: page.label,
37
+ url: size.url,
38
+ ...size.scripts
39
+ });
40
+ }
41
+ return { data };
42
+ };
43
+ var html_config_secrets_default = listInlineScripts;
44
+
45
+ //#endregion
46
+ export { dataSchema, html_config_secrets_default as default };
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: security/html-config-secrets
3
+ title: Secrets in the Page's Config and Data
4
+ severity: critical
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.scripts, playhouse.main-document, source.code, project.config]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks the config and data the deployed site writes into each page's HTML for secrets, such as third-party API keys, client secrets, access tokens and passwords. Anything in the HTML can be read by every visitor, so a secret there must be treated as leaked. Unlike `security/public-client-secrets`, which reads the project's source, this checks what the live site actually sends, which includes values set as environment variables on Managed Runtime. Keys that are meant to be public, such as the Commerce API client id or a publishable payment key, are allowed.
11
+ ## Inspect
12
+ The rule script lists the inline scripts Playhouse archived from each page's HTML (`data` file, paths relative to the page's `archiveDir`): `appConfig` (`window.__APP_CONFIG__`), `pageData` (React Router loader data) and `other` (every other inline script, such as analytics and tag snippets). Read all of them; secrets can have any property name and any format, so don't rely on names like `apiKey` or `secret` alone. Look at every value that looks like a credential: long random strings, tokens with known prefixes (`sk_`, `ghp_`, `xoxb-`, `AKIA`, `eyJ` for JWTs), URLs with credentials or tokens in them, private keys, and values under names like `key`, `token`, `secret`, `password`, `auth` or `credentials`. If a page has no archived scripts, `curl -sL <page url>` and read its inline scripts instead.
13
+
14
+ For each candidate, decide whether it is public by design. Public: the Commerce API (SLAS) public client id, organization id, short code and site id; analytics and tag manager ids; payment publishable or client keys (`pk_live_`, Adyen client keys); reCAPTCHA site keys; search-only keys (Algolia search keys); error-reporting DSNs; map and other browser keys meant to be restricted by referrer. Secret: SLAS private client secrets, Commerce API or OCAPI client passwords, payment secret keys (`sk_live_`), cloud credentials, webhook signing secrets, admin or write API keys, access or refresh tokens issued to the server, private keys and passwords. When the same vendor issues both, check the vendor's key type or format. Then find where the value comes from in the project (config schema, `config.server.ts`, environment variable names, loaders) so the finding can say what to move.
15
+ ## Evaluate
16
+ Report a `finding` when a page's HTML contains a secret. Use the rule's severity for credentials that grant write, admin or account access, and lower it for a key that only grants read access to public data. Keys you can't classify with confidence are `info` findings that ask the customer to confirm the key type. Pass when every candidate is public by design, and cite what was reviewed. The same config usually appears on every page; cite it once and say so.
17
+ ## Evidence
18
+ For each secret: the archived script that contains it (`playhouse`) with a `value` naming its location in the config or data (for example `engagement.adapters.reviews.apiKey`) and the key type, never the secret itself; and `source` evidence for where the project puts it into browser config or loader data. Replace the value with `[redacted]` everywhere, including in the summary and remediation.
19
+ ## Remediation
20
+ Keep the secret on the server: read it in server-only code (loaders, actions or server middleware) instead of browser config, or call the third party through a server route. Then rotate the leaked secret, since anyone who loaded the page may have copied it.
21
+ ## Examples
22
+ Compliant: `__APP_CONFIG__` with the SLAS public client id and a Stripe publishable key. Problematic: `__APP_CONFIG__` with a reviews vendor's write API key, or loader data with a server access token.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: security/protected-route-authorization
3
+ title: Server-Side Access Checks for Shopper Data
4
+ severity: critical
5
+ appliesTo: [account, checkout]
6
+ inputs: [source.code, manual.authorization]
7
+ ---
8
+ ## Intent
9
+ Checks that the loaders and actions behind account, order, profile and checkout pages verify on the server that the shopper is signed in and allowed to see that specific record. Hiding a page in the UI isn't protection: without a server check, anyone can request another shopper's data directly.
10
+ ## Inspect
11
+ Trace account, order, profile, and checkout loaders/actions through session and object authorization.
12
+ ## Evaluate
13
+ Finding requires missing or bypassable server enforcement; UI guards do not count.
14
+ ## Evidence
15
+ Cite route, server handler, authorization path, and reproduction where safe.
16
+ ## Remediation
17
+ Enforce authentication and object-level authorization before data access.
18
+ ## Examples
19
+ Compliant: loader rejects unauthorized user. Problematic: direct loader exposes orders. Ambiguous: custom guard body unavailable.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: security/public-client-secrets
3
+ title: Secrets Exposed to the Browser
4
+ severity: critical
5
+ appliesTo: [project]
6
+ inputs: [source.code, project.config]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks the configuration sent to the browser (`PUBLIC__` variables and other client-side config) for secret keys, while allowing values meant to be public, such as site ids and publishable keys. Every visitor can read anything in client config.
11
+ ## Inspect
12
+ Inspect PUBLIC__ and client-exposed config for secret-shaped keys/values while recognizing intentionally public site and publishable keys.
13
+ ## Evaluate
14
+ Finding requires a credential with secret capability exposed to the client.
15
+ ## Evidence
16
+ Cite only key name and source location with value replaced by [redacted].
17
+ ## Remediation
18
+ Move server-side and rotate the exposed credential.
19
+ ## Examples
20
+ Compliant: referrer-restricted public site key. Problematic: client secret under PUBLIC__. Ambiguous: high-entropy non-secret identifier.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: security/raw-html-sinks
3
+ title: Unsanitized HTML (XSS)
4
+ severity: high
5
+ appliesTo: [project]
6
+ inputs: [source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Traces every `dangerouslySetInnerHTML` and `innerHTML` back to where its content comes from, such as the CMS, shoppers or APIs, and checks that untrusted content is sanitized first. Rendering unsanitized HTML lets injected scripts run on the site (cross-site scripting).
11
+ ## Inspect
12
+ Trace dangerouslySetInnerHTML and innerHTML values backward through CMS/user inputs, transformations, sanitizers, and trust boundaries.
13
+ ## Evaluate
14
+ Finding requires an untrusted or inadequately sanitized flow; sink presence alone, with every flow trusted or sanitized, is a pass.
15
+ ## Evidence
16
+ Cite source, transformations, sanitizer policy, and sink lines without sensitive content.
17
+ ## Remediation
18
+ Render structured data or apply a maintained context-appropriate sanitizer.
19
+ ## Examples
20
+ Compliant: compile-time constant markup. Problematic: unsanitized CMS HTML. Ambiguous: sink with unresolved upstream source.
@@ -0,0 +1,183 @@
1
+ import { c as matchDocument, s as headerValues } from "../../../../../har.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/audit/catalog/categories/security/rules/strict-transport-security.ts
5
+ const documentResultSchema = z.object({
6
+ page: z.string(),
7
+ cell: z.string(),
8
+ harArtifact: z.string(),
9
+ networkArtifact: z.string(),
10
+ finalUrl: z.string().nullable(),
11
+ statusCode: z.number().nullable(),
12
+ header: z.string().nullable(),
13
+ maxAgeSeconds: z.number().nullable(),
14
+ includeSubDomains: z.boolean(),
15
+ result: z.enum([
16
+ "pass",
17
+ "fail",
18
+ "not-evaluated"
19
+ ]),
20
+ reasons: z.array(z.string()),
21
+ evidence: z.array(z.object({
22
+ artifact: z.string(),
23
+ pointer: z.string(),
24
+ value: z.union([
25
+ z.string(),
26
+ z.number(),
27
+ z.boolean()
28
+ ]).optional()
29
+ }).strict())
30
+ }).strict();
31
+ const dataSchema = z.object({ documents: z.array(documentResultSchema) }).strict();
32
+ const MIN_MAX_AGE_SECONDS = 15552e3;
33
+ function isDevelopmentUrl(value) {
34
+ try {
35
+ const url = new URL(value);
36
+ return url.protocol !== "https:" || [
37
+ "localhost",
38
+ "127.0.0.1",
39
+ "::1"
40
+ ].includes(url.hostname);
41
+ } catch {
42
+ return true;
43
+ }
44
+ }
45
+ function parseHeader(value) {
46
+ if (value === null) return {
47
+ maxAgeSeconds: null,
48
+ includeSubDomains: false
49
+ };
50
+ const directives = value.split(";").map((item) => item.trim()).filter(Boolean);
51
+ const match = directives.find((item) => /^max-age\s*=/i.test(item))?.match(/^max-age\s*=\s*"?(\d+)"?$/i);
52
+ return {
53
+ maxAgeSeconds: match ? Number(match[1]) : null,
54
+ includeSubDomains: directives.some((item) => /^includesubdomains$/i.test(item))
55
+ };
56
+ }
57
+ const prepare = async ({ pages, playhouse }) => {
58
+ const documents = [];
59
+ for (const page of pages) {
60
+ const harArtifacts = (await playhouse.artifactNames(page.label)).filter((name) => /^network-.+\.har$/.test(name));
61
+ if (!harArtifacts.length) {
62
+ documents.push({
63
+ page: page.label,
64
+ cell: "unknown",
65
+ harArtifact: "network-{cell}.har",
66
+ networkArtifact: "network-{cell}.json",
67
+ finalUrl: null,
68
+ statusCode: null,
69
+ header: null,
70
+ maxAgeSeconds: null,
71
+ includeSubDomains: false,
72
+ result: "not-evaluated",
73
+ reasons: ["No representative HAR artifact was available."],
74
+ evidence: []
75
+ });
76
+ continue;
77
+ }
78
+ for (const harArtifact of harArtifacts) {
79
+ const cell = /^network-(.+)\.har$/.exec(harArtifact)?.[1];
80
+ if (!cell) continue;
81
+ const networkArtifact = `network-${cell}.json`;
82
+ const match = await matchDocument(playhouse, page.label, cell);
83
+ const { finalUrl } = match;
84
+ if (finalUrl && isDevelopmentUrl(finalUrl)) {
85
+ documents.push({
86
+ page: page.label,
87
+ cell,
88
+ harArtifact,
89
+ networkArtifact,
90
+ finalUrl,
91
+ statusCode: null,
92
+ header: null,
93
+ maxAgeSeconds: null,
94
+ includeSubDomains: false,
95
+ result: "not-evaluated",
96
+ reasons: ["The final document is not an eligible public HTTPS URL."],
97
+ evidence: []
98
+ });
99
+ continue;
100
+ }
101
+ if ("reason" in match) {
102
+ documents.push({
103
+ page: page.label,
104
+ cell,
105
+ harArtifact,
106
+ networkArtifact,
107
+ finalUrl,
108
+ statusCode: null,
109
+ header: null,
110
+ maxAgeSeconds: null,
111
+ includeSubDomains: false,
112
+ result: "not-evaluated",
113
+ reasons: [match.reason],
114
+ evidence: []
115
+ });
116
+ continue;
117
+ }
118
+ const matched = match.document;
119
+ const hstsHeaders = headerValues(matched.entry, "strict-transport-security");
120
+ const header = hstsHeaders.length === 1 ? hstsHeaders[0].value : null;
121
+ const parsed = parseHeader(header);
122
+ const reasons = [];
123
+ if (hstsHeaders.length === 0) reasons.push("Strict-Transport-Security is missing.");
124
+ else if (hstsHeaders.length > 1) reasons.push("Multiple Strict-Transport-Security headers make the effective policy ambiguous.");
125
+ if (hstsHeaders.length === 1 && parsed.maxAgeSeconds === null) reasons.push("max-age is missing or invalid.");
126
+ else if (parsed.maxAgeSeconds !== null && parsed.maxAgeSeconds < MIN_MAX_AGE_SECONDS) reasons.push(`max-age is below ${MIN_MAX_AGE_SECONDS} seconds.`);
127
+ if (hstsHeaders.length === 1 && !parsed.includeSubDomains) reasons.push("includeSubDomains is missing.");
128
+ const result = hstsHeaders.length > 1 ? "not-evaluated" : reasons.length ? "fail" : "pass";
129
+ const evidence = [{
130
+ artifact: harArtifact,
131
+ pointer: `/log/entries/${matched.index}/request/url`,
132
+ value: match.finalUrl
133
+ }, ...header !== null ? [{
134
+ artifact: harArtifact,
135
+ pointer: `/log/entries/${matched.index}/response/headers/${hstsHeaders[0].index}/value`,
136
+ value: header
137
+ }] : []];
138
+ documents.push({
139
+ page: page.label,
140
+ cell,
141
+ harArtifact,
142
+ networkArtifact,
143
+ finalUrl: match.finalUrl,
144
+ statusCode: matched.entry.response.status,
145
+ header,
146
+ maxAgeSeconds: parsed.maxAgeSeconds,
147
+ includeSubDomains: parsed.includeSubDomains,
148
+ result,
149
+ reasons,
150
+ evidence
151
+ });
152
+ }
153
+ }
154
+ const failed = documents.filter((item) => item.result === "fail");
155
+ const unresolved = documents.filter((item) => item.result === "not-evaluated");
156
+ const passed = documents.filter((item) => item.result === "pass");
157
+ const evidenceFor = (items) => items.flatMap((item) => item.evidence.map((evidence) => ({
158
+ type: "playhouse",
159
+ page: item.page,
160
+ ...evidence
161
+ })));
162
+ if (failed.length) return {
163
+ data: { documents },
164
+ decision: {
165
+ status: "fail",
166
+ summary: `${failed.length} HTML page response${failed.length === 1 ? "" : "s"} failed the HSTS baseline (max-age of at least 15552000 seconds with includeSubDomains).`,
167
+ evidence: evidenceFor(failed)
168
+ }
169
+ };
170
+ if (passed.length && !unresolved.length) return {
171
+ data: { documents },
172
+ decision: {
173
+ status: "pass",
174
+ summary: `${passed.length} HTML page response${passed.length === 1 ? "" : "s"} sent Strict-Transport-Security with a max-age of at least 15552000 seconds and includeSubDomains.`,
175
+ evidence: evidenceFor(passed)
176
+ }
177
+ };
178
+ return { data: { documents } };
179
+ };
180
+ var strict_transport_security_default = prepare;
181
+
182
+ //#endregion
183
+ export { dataSchema, strict_transport_security_default as default };
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: security/strict-transport-security
3
+ title: HTTPS-Only Header (HSTS)
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har, playhouse.document, source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's HTML response sends `Strict-Transport-Security` with a `max-age` of at least 180 days (15552000 seconds) and `includeSubDomains`. The header tells browsers to always use HTTPS for the site, so an attacker can't intercept a first plain-HTTP request.
11
+ ## Inspect
12
+ Match the final HTTPS document response in a representative sanitized HAR and inspect `Strict-Transport-Security`. Inspect edge or Storefront Next configuration only to explain the deployed value.
13
+
14
+ ## Evaluate
15
+ Create a finding when HSTS is missing, `max-age` is invalid or below 15552000 seconds, or `includeSubDomains` is absent from the expected production baseline. Do not evaluate localhost or plain-HTTP development URLs. Use `not-verified` when the final HTTPS document cannot be matched.
16
+
17
+ ## Evidence
18
+ Cite page, sanitized final URL, HTTPS status, and the HSTS directives needed for the conclusion. Do not cite unrelated headers or unredacted URL parameters.
19
+
20
+ ## Remediation
21
+ Restore the Storefront Next or public-edge HSTS baseline and verify the deployed response. Confirm organizational ownership before adding `preload`.
22
+
23
+ ## Examples
24
+ Compliant: `max-age=15552000; includeSubDomains`. Problematic: HSTS is absent on production HTTPS. Ambiguous: the audit target is localhost; mark not applicable rather than passing it.
@@ -0,0 +1,17 @@
1
+ # SEO
2
+
3
+ ## Purpose and boundaries
4
+
5
+ Inspect delivered main-document evidence together with route metadata source. A helper's presence is not proof of correct output on every page.
6
+
7
+ ## Investigation sequence
8
+
9
+ Compare home, PLP, and PDP delivered metadata, then trace each value to source.
10
+
11
+ ## Evidence and redaction
12
+
13
+ Cite delivered element/value and source line when available.
14
+
15
+ ## Severity and blind spots
16
+
17
+ Crawler behavior and production indexing controls may vary by deployment.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: seo/canonical-url
3
+ title: Canonical URL
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.main-document, source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page declares a canonical URL (`<link rel="canonical">`) pointing to its own clean URL, and that variants such as query strings, locales and filtered category pages point to the right one. Search engines use it to merge duplicate URLs into one result.
11
+ ## Inspect
12
+ Compare each delivered canonical with the final URL and inspect handling of query, locale, PLP, and PDP variants.
13
+ ## Evaluate
14
+ Finding requires missing, malformed, cross-page, or incorrect canonical output on a captured page.
15
+ ## Evidence
16
+ Cite page URL, delivered canonical value, and generation source.
17
+ ## Remediation
18
+ Emit one absolute self-consistent canonical for the clean page URL.
19
+ ## Examples
20
+ Compliant: PDP canonical drops tracking parameters. Problematic: every page canonicalizes to home. Ambiguous: helper exists but output was not captured.