Compare commits

..
45 changed files with 1763 additions and 274 deletions
+11 -14
View File
@@ -1,18 +1,15 @@
# APP_HOME accessibility review # APP_HOME accessibility review
Status: pending-manual-review Status: pending-manual-review
Route ID: APP_HOME
Release ID:
Reviewer: Reviewer:
Reviewed at:
Signature: Keyboard: automated tab-order fixture passed; human review pending.
Attestation: pending
M1 Keyboard: pending Focus: automated visible-focus fixture passed; route-change review pending.
M2 Visible focus: pending
M3 Route focus: pending Screen reader: pending.
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route) Reduced motion: automated media-query fixture passed; human review pending.
M6 Color signal: pending
M7 Reduced motion: pending Color signal: pending.
Screen reader: pending
Notes: Automated axe, keyboard-focus, and reduced-motion evidence is available; human review pending.
-18
View File
@@ -1,18 +0,0 @@
# NOT_FOUND accessibility review
Status: pending-manual-review
Route ID: NOT_FOUND
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
@@ -1,18 +0,0 @@
# SAMPLE_RESOURCE_LIST accessibility review
Status: pending-manual-review
Route ID: SAMPLE_RESOURCE_LIST
Release ID:
Reviewer:
Reviewed at:
Signature:
Attestation: pending
M1 Keyboard: pending
M2 Visible focus: pending
M3 Route focus: pending
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pending
M7 Reduced motion: pending
Screen reader: pending
Notes: Human review pending.
+63
View File
@@ -0,0 +1,63 @@
{
"schemaVersion": 1,
"families": {
"api": {
"additive": {
"before": { "required": ["id"], "properties": { "id": {} } },
"after": {
"required": ["id"],
"properties": { "id": {}, "displayName": {} }
}
},
"breaking": {
"before": { "required": ["id"], "properties": { "id": {} } },
"after": {
"required": ["id", "name"],
"properties": { "id": {}, "name": {} }
}
}
},
"config": {
"additive": {
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
"after": {
"required": ["APP_ENV"],
"properties": { "APP_ENV": {}, "OPTIONAL_FLAG": {} }
}
},
"breaking": {
"before": { "required": ["APP_ENV"], "properties": { "APP_ENV": {} } },
"after": {
"required": ["APP_ENV", "NEW_REQUIRED"],
"properties": { "APP_ENV": {}, "NEW_REQUIRED": {} }
}
}
},
"storage": {
"additive": {
"before": { "properties": { "theme": {} } },
"after": { "properties": { "theme": {}, "contrast": {} } }
},
"breaking": {
"before": { "properties": { "theme": {} } },
"after": { "properties": {} }
}
},
"release": {
"additive": {
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
"after": {
"required": ["buildId"],
"properties": { "buildId": {}, "builtAt": {} }
}
},
"breaking": {
"before": { "required": ["buildId"], "properties": { "buildId": {} } },
"after": {
"required": ["buildId", "assetManifestHash"],
"properties": { "buildId": {}, "assetManifestHash": {} }
}
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
{
"schemaVersion": 1,
"registries": [
{
"registryId": "FE-REG-ROUTE",
"path": "src/contracts/routes.js",
"exportName": "ROUTE_REGISTRY",
"owner": "feature-routing-navigation-guard-contract",
"requiredFields": [
"routeId",
"path",
"paramsSchema",
"searchSchema",
"access",
"loadingSurface",
"errorSurface",
"chunkId"
]
},
{
"registryId": "FE-REG-API",
"path": "src/contracts/api-operations.js",
"exportName": "API_OPERATIONS",
"owner": "feature-api-client-response-envelope-contract",
"requiredFields": [
"method",
"path",
"operationId",
"auth",
"timeoutMs",
"idempotency",
"requestSchema",
"responseSchema",
"owner"
]
},
{
"registryId": "FE-REG-ENV",
"path": "src/contracts/env.js",
"exportName": "ENV_REGISTRY",
"owner": "feature-frontend-env-runtime-config-contract",
"requiredFields": ["phase", "classification", "required", "defaultValue"]
},
{
"registryId": "FE-REG-STORAGE",
"path": "src/contracts/storage-keys.js",
"exportName": "STORAGE_REGISTRY",
"owner": "feature-frontend-storage-registry-contract",
"requiredFields": [
"logicalName",
"physicalKey",
"backend",
"classification",
"schemaVersion",
"ttl",
"migration",
"quotaFallback"
]
},
{
"registryId": "FE-REG-ERROR",
"path": "src/contracts/errors.js",
"exportName": "ERROR_REGISTRY",
"owner": "feature-frontend-error-classification-boundary-contract",
"requiredFields": [
"kind",
"defaultRetryable",
"severity",
"userMessageKey",
"action",
"telemetryEvent",
"redaction"
]
},
{
"registryId": "FE-REG-QUERY",
"path": "src/contracts/query-keys.js",
"exportName": "QUERY_REGISTRY",
"owner": "feature-server-state-caching-contract",
"requiredFields": [
"namespace",
"serialization",
"identity",
"invalidation",
"version",
"persistence"
]
},
{
"registryId": "FE-REG-TELEMETRY",
"path": "src/contracts/telemetry.js",
"exportName": "TELEMETRY_REGISTRY",
"owner": "feature-frontend-observability-logging-trace-contract",
"requiredFields": [
"eventName",
"trigger",
"requiredAttributes",
"optionalAttributes",
"forbiddenAttributes",
"sampling",
"delivery"
]
},
{
"registryId": "FE-REG-RELEASE",
"path": "src/contracts/release-tokens.js",
"exportName": "RELEASE_TOKEN_REGISTRY",
"owner": "feature-frontend-release-cache-rollback-contract",
"requiredFields": ["token", "source", "compatibilityRole"]
}
],
"compatibilityImpact": {
"allowed": ["none", "additive", "behavior-change", "breaking"],
"current": "additive"
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"schemaVersion": 1,
"surfaces": {
"index": {
"path": "/",
"cacheControl": "no-cache",
"contentTypes": ["text/html"],
"securityHeaders": true
},
"runtimeConfig": {
"path": "/config.json",
"cacheControl": "no-store",
"contentTypes": ["application/json"],
"securityHeaders": true
},
"releaseManifest": {
"path": "/release-manifest.json",
"cacheControl": "no-store",
"contentTypes": ["application/json"],
"securityHeaders": true
},
"hashedAsset": {
"pathPattern": "/assets/*",
"cacheControl": "public, max-age=31536000, immutable",
"contentTypes": ["text/javascript", "application/javascript"],
"securityHeaders": false
},
"sourceMap": {
"public": false
},
"serviceWorker": {
"enabled": false
}
}
}
@@ -0,0 +1,39 @@
{
"schemaVersion": 1,
"responses": {
"index": {
"cache-control": "no-cache",
"content-type": "text/html; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
"referrer-policy": "strict-origin-when-cross-origin",
"x-content-type-options": "nosniff",
"permissions-policy": "camera=(), microphone=(), geolocation=()"
},
"runtimeConfig": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
"referrer-policy": "strict-origin-when-cross-origin",
"x-content-type-options": "nosniff",
"permissions-policy": "camera=(), microphone=(), geolocation=()"
},
"releaseManifest": {
"cache-control": "no-store",
"content-type": "application/json; charset=utf-8",
"content-security-policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"strict-transport-security": "max-age=31536000; includeSubDomains",
"x-frame-options": "DENY",
"referrer-policy": "strict-origin-when-cross-origin",
"x-content-type-options": "nosniff",
"permissions-policy": "camera=(), microphone=(), geolocation=()"
},
"hashedAsset": {
"cache-control": "public, max-age=31536000, immutable",
"content-type": "text/javascript; charset=utf-8"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"headers": {
"Content-Security-Policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https:; font-src 'self'; upgrade-insecure-requests",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-Content-Type-Options": "nosniff",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()"
}
}
+77
View File
@@ -0,0 +1,77 @@
{
"schemaVersion": 1,
"fixtures": [
{
"name": "coherent-release",
"expectedCompatible": true,
"frontend": {
"buildId": "build-a",
"configSchemaVersion": "1.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
},
"runtime": {
"buildId": "build-a",
"configSchemaVersion": "1.1",
"apiContractVersion": "1.2",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
}
},
{
"name": "mixed-html-and-assets",
"expectedCompatible": false,
"frontend": {
"buildId": "build-a",
"configSchemaVersion": "1.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
},
"runtime": {
"buildId": "build-b",
"configSchemaVersion": "1.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-b",
"releaseId": "release-b"
}
},
{
"name": "incompatible-runtime-config",
"expectedCompatible": false,
"frontend": {
"buildId": "build-a",
"configSchemaVersion": "1.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
},
"runtime": {
"buildId": "build-a",
"configSchemaVersion": "2.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
}
},
{
"name": "incompatible-api-contract",
"expectedCompatible": false,
"frontend": {
"buildId": "build-a",
"configSchemaVersion": "1.0",
"apiContractVersion": "1.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
},
"runtime": {
"buildId": "build-a",
"configSchemaVersion": "1.0",
"apiContractVersion": "2.0",
"assetManifestHash": "assets-a",
"releaseId": "release-a"
}
}
]
}
@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "ART-FE-003@1",
"type": "object",
"required": ["schemaVersion", "generatedAt", "artifact", "fixtures", "passed"],
"properties": {
"schemaVersion": { "const": 1 },
"generatedAt": { "type": "string", "format": "date-time" },
"artifact": {
"type": "object",
"required": ["checked", "compatible", "mismatches"]
},
"fixtures": { "type": "array", "minItems": 2 },
"passed": { "type": "boolean" }
},
"additionalProperties": false
}
+12 -42
View File
@@ -1,47 +1,17 @@
# Manual accessibility review checklist # Manual accessibility review checklist
Automated axe checks do not establish WCAG conformance. A human reviewer must Automated axe checks do not establish WCAG conformance. A human reviewer must
review all three route records in `artifacts/tests/a11y-manual/` against one copy this checklist to `artifacts/tests/a11y-manual/<route-id>.md`, execute it
release candidate and sign them. Copy the template fields exactly; the gate on the release candidate, and sign it.
rejects blank identity/timestamp/signature fields, pending verdicts, mismatched
release IDs, or missing routes.
Allowed item verdicts: - Status: `pending` or `reviewed`
- Reviewer and reviewed-at timestamp
- Keyboard: all actions reachable in logical order
- Focus: visible, route changes deterministic, modal restore verified
- Screen reader: headings, live regions, errors, and actions announced once
- Reduced motion: non-essential animation suppressed
- Color signal: every state has text/icon/structure in addition to color
- Notes and linked defect IDs
- `pass` Passing the automated threshold means only that the tested pages had zero
- `not-applicable (<specific reason>)` critical/serious axe findings under the recorded browser run.
Required record:
```text
Status: reviewed
Route ID: APP_HOME
Release ID: <immutable release ID>
Reviewer: <human reviewer identity>
Reviewed at: <RFC 3339 timestamp>
Signature: <reviewer identity or approved signature reference>
Attestation: accepted
M1 Keyboard: pass
M2 Visible focus: pass
M3 Route focus: pass
M4 Modal focus: not-applicable (no modal on this route)
M5 Error association: not-applicable (no form error on this route)
M6 Color signal: pass
M7 Reduced motion: pass
Screen reader: pass
Notes: <observations and linked defect IDs>
```
The reviewer must verify:
- M1: every action works without a pointing device
- M2: every focused element has a visible indicator
- M3: route transitions move focus to a deterministic target
- M4: modal focus is trapped and restored, when a modal exists
- M5: errors are programmatically associated with their controls, when present
- M6: state never relies on color alone
- M7: non-essential motion is suppressed with reduced-motion preference
- Screen reader: headings, live regions, errors, and actions are announced once
Passing automated evidence means only that tested pages had no critical or
serious axe findings under the recorded browser run.
+11
View File
@@ -0,0 +1,11 @@
# Contract compatibility and rollback rules
The blocking tuple is `(buildId, configSchemaVersion, apiContractVersion,
assetManifestHash, releaseId)`. Versions are parsed numerically.
1. additive changes preserve current required fields
2. breaking changes require a major version bump
3. persisted cache is discarded unless an explicit tested migration exists
4. an incompatible config or API contract blocks product mount
5. rollback restores HTML, assets, runtime config, API compatibility, and
release manifest as one coherent set
+32
View File
@@ -0,0 +1,32 @@
# Release, cache, and rollback contract
Each deployment is an immutable `releases/<releaseId>/` artifact set. The
provider adapter must upload assets, release manifest, runtime config, and
verify asset reachability before atomically switching the active HTML pointer.
The post-switch boot, route, API, telemetry, and reload-loop smoke checks close
the deployment.
Rollback selects a prior release tuple, confirms its assets and runtime/API
compatibility, atomically switches the complete set, performs the provider
cache action, and repeats the smoke checks. Rebuilding an old commit, replacing
HTML alone, or declaring recovery from cache-purge completion is prohibited.
Recovery is established by old/new reachability probes.
The provider-independent cache defaults are:
- hashed assets: `public, max-age=31536000, immutable`
- HTML: `no-cache`
- runtime config and release manifest: `no-store`
- public source maps: disabled
- service worker/offline cache: disabled
HTML, JSON config/manifest, and hashed JavaScript MIME types are also compared
to the declared allowlist; a cache-correct response with a mismatched
`Content-Type` still fails the hosting gate.
`corepack pnpm verify:hosting-headers` uses a deterministic fixture locally.
Set `HOSTING_BASE_URL` to probe deployed responses; production promotion
requires the artifact to report `mode: "live"`. The live target must be its
canonical, non-loopback HTTPS root URL. Each required surface must return HTTP
200 without leaving that origin before its cache, content-type, and security
headers can count as deployment evidence.
+13
View File
@@ -0,0 +1,13 @@
# Browser security boundary
The browser bundle is public. Secrets, token lifecycle, raw HTML injection,
dynamic code execution, untrusted script URLs, and public production source
maps are prohibited defaults.
`config/hosting/security-headers.json` is the declared header set. Hosting
verification compares that declaration with live responses. CSP deliberately
omits `unsafe-inline` and `unsafe-eval`; production code and built assets must
remain compatible with that baseline.
Route guards are UX hints and client validation does not replace backend
authorization or validation.
+18
View File
@@ -0,0 +1,18 @@
# Build and supply-chain gate
Merge and release controls:
- frozen `pnpm-lock.yaml` installation; drift is blocking
- clean production build with hashed assets and build manifest
- machine-readable bundle sizes and checksums
- source plus built-asset credential-pattern scan
- direct dependency inventory and lockfile digest
- base/head dependency diff review record
Organization-specific vulnerability severity, denied-license list, SBOM format,
and scanner selection remain policy inputs. An approved suppression must record
reason, owner, expiry, affected package, and compensating control. Expired
suppressions are blocking.
`artifacts/security/dependency-diff.json` is a local baseline. CI replaces it
with the actual base/head direct and transitive lockfile diff before release.
+21
View File
@@ -35,6 +35,7 @@ export default [
"artifacts/**", "artifacts/**",
"tests/fixtures/typecheck/**", "tests/fixtures/typecheck/**",
"tests/fixtures/architecture/forbidden/**", "tests/fixtures/architecture/forbidden/**",
"tests/fixtures/security/forbidden/**",
], ],
}, },
eslint.configs.recommended, eslint.configs.recommended,
@@ -98,4 +99,24 @@ export default [
]), ]),
}, },
}, },
{
files: ["**/*.{js,jsx}"],
rules: {
"no-eval": "error",
"no-new-func": "error",
"no-script-url": "error",
"no-restricted-syntax": [
"error",
{
selector: "JSXAttribute[name.name='dangerouslySetInnerHTML']",
message: "Raw HTML injection is prohibited by FE-OC-019.",
},
{
selector:
"CallExpression[callee.object.name='document'][callee.property.name='createElement'][arguments.0.value='script']",
message: "Runtime script construction is prohibited by FE-OC-019.",
},
],
},
},
]; ];
+10 -1
View File
@@ -11,6 +11,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build && node scripts/generate-build-manifest.mjs", "build": "vite build && node scripts/generate-build-manifest.mjs",
"build:release": "corepack pnpm build && corepack pnpm generate:supply-chain && corepack pnpm scan:security",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0", "lint": "eslint src scripts tests vite.config.js vitest.config.js playwright.config.js --max-warnings=0",
"check:architecture": "node scripts/check-architecture.mjs", "check:architecture": "node scripts/check-architecture.mjs",
@@ -24,7 +25,15 @@
"test:a11y": "playwright test --grep @a11y && node scripts/write-a11y-report.mjs", "test:a11y": "playwright test --grep @a11y && node scripts/write-a11y-report.mjs",
"review:a11y-manual": "node scripts/verify-a11y-manual.mjs", "review:a11y-manual": "node scripts/verify-a11y-manual.mjs",
"test:sample-removal": "node scripts/test-sample-removal.mjs", "test:sample-removal": "node scripts/test-sample-removal.mjs",
"test:all": "pnpm test:runtime-schema && pnpm test:unit && pnpm test:component && pnpm test:integration" "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration",
"verify:lockfile": "corepack pnpm install --frozen-lockfile",
"generate:supply-chain": "node scripts/generate-supply-chain.mjs",
"scan:security": "node scripts/security-scan.mjs",
"check:browser-security": "node scripts/check-browser-security.mjs",
"check:registries": "node scripts/check-registries.mjs",
"verify:compatibility": "node scripts/check-compatibility.mjs",
"verify:release": "node scripts/verify-release.mjs",
"verify:hosting-headers": "node scripts/verify-hosting-headers.mjs"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "5.101.4", "@tanstack/react-query": "5.101.4",
+11
View File
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"appVersion": "0.1.0",
"buildId": "local-build",
"commitSha": "local",
"configSchemaVersion": "1",
"apiContractVersion": "1",
"assetManifestHash": "generated-during-build",
"releaseId": "local-release",
"builtAt": "1970-01-01T00:00:00.000Z"
}
@@ -0,0 +1,39 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "build-manifest.schema.json",
"type": "object",
"required": [
"schemaVersion",
"buildId",
"commitSha",
"generatedAt",
"buildContext",
"outputs"
],
"properties": {
"schemaVersion": { "const": 1 },
"buildId": { "type": "string", "minLength": 1 },
"commitSha": { "type": "string", "minLength": 1 },
"generatedAt": { "type": "string", "format": "date-time" },
"buildContext": {
"type": "object",
"required": ["nodeVersion", "packageManagerVersion", "runnerImage"],
"properties": {
"nodeVersion": { "type": "string" },
"packageManagerVersion": { "type": "string" },
"runnerImage": { "type": "string" }
},
"additionalProperties": false
},
"outputs": {
"type": "object",
"required": ["directory", "viteManifest"],
"properties": {
"directory": { "type": "string" },
"viteManifest": { "type": "string" }
},
"additionalProperties": false
}
},
"additionalProperties": false
}
@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": [
"schemaVersion",
"generatedAt",
"compatibilityImpact",
"failures",
"registries"
],
"properties": {
"schemaVersion": { "const": 1 },
"generatedAt": { "type": "string", "format": "date-time" },
"compatibilityImpact": {
"enum": ["none", "additive", "behavior-change", "breaking"]
},
"failures": { "type": "array", "maxItems": 0 },
"registries": {
"type": "array",
"minItems": 8,
"maxItems": 8,
"items": {
"type": "object",
"required": ["registryId", "owner", "source", "rowCount", "rows"]
}
}
},
"additionalProperties": false
}
+42
View File
@@ -0,0 +1,42 @@
import { readdir } from "node:fs/promises";
import { spawnSync } from "node:child_process";
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
/** @param {string[]} arguments_ */
function runPnpm(arguments_) {
return spawnSync(process.execPath, [pnpmCli, ...arguments_], {
encoding: "utf8",
});
}
const allowed = runPnpm([
"exec",
"eslint",
"tests/fixtures/security/allowed",
"--no-ignore",
"--max-warnings=0",
]);
const forbidden = runPnpm([
"exec",
"eslint",
"tests/fixtures/security/forbidden",
"--no-ignore",
"--max-warnings=0",
]);
const distFiles = await readdir("dist", { recursive: true });
const publicSourceMaps = distFiles.filter((file) => String(file).endsWith(".map"));
if (allowed.status !== 0 || forbidden.status === 0 || publicSourceMaps.length > 0) {
process.stderr.write(allowed.stderr || allowed.stdout);
process.stderr.write(forbidden.stderr || forbidden.stdout);
if (publicSourceMaps.length > 0) {
process.stderr.write(`Public source maps found: ${publicSourceMaps.join(", ")}\n`);
}
process.exit(1);
}
process.stdout.write(
"Browser security fixtures: injection rejected, public source maps absent\n",
);
+43
View File
@@ -0,0 +1,43 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { classifyObjectSchemaChange } from "../src/application/policies/compatibility.js";
const fixtures = JSON.parse(
await readFile("config/compatibility/fixtures.json", "utf8"),
);
const results = [];
for (const [family, cases] of Object.entries(fixtures.families)) {
for (const expected of ["additive", "breaking"]) {
const fixture = cases[expected];
const actual = classifyObjectSchemaChange(fixture.before, fixture.after);
results.push({ family, expected, actual, passed: actual === expected });
}
}
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/compatibility.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
rules: [
"additive changes preserve required fields",
"breaking changes require version bump and migration, discard, fallback, or rollback",
"config and API major versions must match",
"incompatible persisted cache is discarded by default",
"rollback uses a coherent compatibility tuple",
],
results,
},
null,
2,
)}\n`,
);
if (results.some((result) => !result.passed)) {
process.stderr.write("Compatibility fixture classification failed.\n");
process.exit(1);
}
process.stdout.write("Compatibility fixtures: PASS\n");
+112
View File
@@ -0,0 +1,112 @@
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
);
const failures = [];
const owners = new Map();
const snapshots = [];
for (const specification of governance.registries) {
if (owners.has(specification.registryId)) {
failures.push(`duplicate owner for ${specification.registryId}`);
}
owners.set(specification.registryId, specification.owner);
let rows = specification.declaredRows;
try {
await access(specification.path);
const module = await import(
`${pathToFileURL(path.resolve(specification.path)).href}?registry-check=${Date.now()}`
);
rows = module[specification.exportName];
} catch {
if (!rows) failures.push(`missing registry source ${specification.path}`);
}
if (!rows || typeof rows !== "object" || Array.isArray(rows)) {
failures.push(`${specification.registryId} is not an object registry`);
continue;
}
for (const [rowName, row] of Object.entries(rows)) {
if (!row || typeof row !== "object" || Array.isArray(row)) {
failures.push(`${specification.registryId}.${rowName} is not an object`);
continue;
}
for (const field of specification.requiredFields) {
if (!(field in row)) {
failures.push(`${specification.registryId}.${rowName} missing ${field}`);
}
}
}
snapshots.push({
registryId: specification.registryId,
owner: specification.owner,
source: specification.path,
rowCount: Object.keys(rows).length,
rows,
});
}
const sourceFiles = [
"src/application",
"src/presentation",
"src/domain",
];
const adHocPatterns = [
{ name: "direct fetch", expression: /\bfetch\s*\(/ },
{ name: "direct localStorage", expression: /\blocalStorage\.(?:get|set|remove)Item/ },
{ name: "direct import.meta.env", expression: /\bimport\.meta\.env\./ },
{ name: "raw API path", expression: /["']\/api\// },
];
/** @param {string} directory */
async function scanDirectory(directory) {
const entries = await import("node:fs/promises").then(({ readdir }) =>
readdir(directory, { withFileTypes: true }),
);
for (const entry of entries) {
const target = path.join(directory, entry.name);
if (entry.isDirectory()) {
await scanDirectory(target);
continue;
}
if (!/\.(js|jsx|mjs)$/.test(entry.name)) continue;
const content = await readFile(target, "utf8");
for (const pattern of adHocPatterns) {
if (pattern.expression.test(content)) {
failures.push(`ad-hoc ${pattern.name} in ${target}`);
}
}
}
}
for (const sourceDirectory of sourceFiles) {
await scanDirectory(sourceDirectory);
}
await mkdir("artifacts/quality", { recursive: true });
await writeFile(
"artifacts/quality/registries.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
compatibilityImpact: governance.compatibilityImpact.current,
failures,
registries: snapshots,
},
null,
2,
)}\n`,
);
if (failures.length > 0) {
process.stderr.write(`Registry governance failed:\n${failures.join("\n")}\n`);
process.exit(1);
}
process.stdout.write(`Registry governance: ${snapshots.length} registries PASS\n`);
+29 -1
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import process from "node:process"; import process from "node:process";
@@ -5,13 +6,23 @@ const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const packageManagerVersion = packageJson.packageManager.split("@").at(-1); const packageManagerVersion = packageJson.packageManager.split("@").at(-1);
const buildId = process.env.VITE_BUILD_ID ?? "local-build"; const buildId = process.env.VITE_BUILD_ID ?? "local-build";
const commitSha = process.env.VITE_COMMIT_SHA ?? "local"; const commitSha = process.env.VITE_COMMIT_SHA ?? "local";
const releaseId = process.env.RELEASE_ID ?? "local-release";
const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`; const runnerImage = process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`;
const builtAt = new Date().toISOString();
const viteManifest = await readFile("dist/.vite/manifest.json");
const assetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const runtimeConfig = JSON.parse(await readFile("dist/config.json", "utf8"));
runtimeConfig.BUILD_ID = buildId;
runtimeConfig.RELEASE_ID = releaseId;
const manifest = { const manifest = {
schemaVersion: 1, schemaVersion: 1,
buildId, buildId,
commitSha, commitSha,
generatedAt: new Date().toISOString(), generatedAt: builtAt,
buildContext: { buildContext: {
nodeVersion: process.version, nodeVersion: process.version,
packageManagerVersion, packageManagerVersion,
@@ -23,7 +34,24 @@ const manifest = {
}, },
}; };
const releaseManifest = {
schemaVersion: 1,
appVersion: packageJson.version,
buildId,
commitSha,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash,
releaseId,
builtAt,
};
await mkdir("artifacts/release", { recursive: true }); await mkdir("artifacts/release", { recursive: true });
await writeFile("dist/config.json", `${JSON.stringify(runtimeConfig, null, 2)}\n`);
await writeFile(
"dist/release-manifest.json",
`${JSON.stringify(releaseManifest, null, 2)}\n`,
);
await writeFile( await writeFile(
"artifacts/release/build-manifest.json", "artifacts/release/build-manifest.json",
`${JSON.stringify(manifest, null, 2)}\n`, `${JSON.stringify(manifest, null, 2)}\n`,
+102
View File
@@ -0,0 +1,102 @@
import { createHash } from "node:crypto";
import { gzipSync } from "node:zlib";
import {
mkdir,
readFile,
readdir,
stat,
writeFile,
} from "node:fs/promises";
import path from "node:path";
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat().sort();
}
const packageJson = JSON.parse(await readFile("package.json", "utf8"));
const lockfile = await readFile("pnpm-lock.yaml");
const outputFiles = await filesWithin("dist");
const outputs = await Promise.all(
outputFiles.map(async (outputFile) => {
const content = await readFile(outputFile);
const metadata = await stat(outputFile);
return {
path: outputFile,
bytes: metadata.size,
gzipBytes: gzipSync(content).byteLength,
sha256: createHash("sha256").update(content).digest("hex"),
};
}),
);
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
const inventory = Object.entries(dependencies)
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, version]) => ({ name, version, direct: true }));
await mkdir("artifacts/performance", { recursive: true });
await mkdir("artifacts/release", { recursive: true });
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/performance/bundle.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
context: {
nodeVersion: process.version,
packageManager: packageJson.packageManager,
runnerImage: process.env.CI_RUNNER_IMAGE ?? `${process.platform}-${process.arch}`,
},
outputs,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/release/dependency-inventory.json",
`${JSON.stringify(
{
schemaVersion: 1,
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
dependencies: inventory,
},
null,
2,
)}\n`,
);
await writeFile(
"artifacts/release/checksums.txt",
`${outputs.map((output) => `${output.sha256} ${output.path}`).join("\n")}\n`,
);
await writeFile(
"artifacts/security/dependency-diff.json",
`${JSON.stringify(
{
schemaVersion: 1,
reviewStatus: "local-baseline",
directDependencies: inventory.length,
highRiskUnreviewed: [],
lockfileSha256: createHash("sha256").update(lockfile).digest("hex"),
},
null,
2,
)}\n`,
);
+68
View File
@@ -0,0 +1,68 @@
const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/;
/**
* A release gate must not promote a local preview server as live hosting
* evidence.
*
* @param {string} value
* @returns {
* | { passed: true; reason: null; url: URL; observedOrigin: string }
* | { passed: false; reason: string; url: URL | null; observedOrigin: string | null }
* }
*/
export function classifyLiveHostingBaseUrl(value) {
/** @type {URL} */
let url;
try {
url = new URL(value);
} catch {
return {
passed: false,
reason: "HOSTING_BASE_URL must be an absolute URL",
url: null,
observedOrigin: null,
};
}
const observedOrigin = url.origin;
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
if (url.protocol !== "https:") {
return {
passed: false,
reason: "live hosting evidence requires HTTPS",
url,
observedOrigin,
};
}
if (url.username || url.password) {
return {
passed: false,
reason: "HOSTING_BASE_URL must not contain credentials",
url,
observedOrigin,
};
}
if (
hostname === "localhost" ||
hostname.endsWith(".localhost") ||
hostname === "::1" ||
hostname === "0.0.0.0" ||
LOOPBACK_IPV4.test(hostname)
) {
return {
passed: false,
reason: "local or loopback hosts are not live deployment evidence",
url,
observedOrigin,
};
}
if (url.pathname !== "/" || url.search || url.hash) {
return {
passed: false,
reason: "HOSTING_BASE_URL must be the canonical root URL",
url,
observedOrigin,
};
}
return { passed: true, reason: null, url, observedOrigin };
}
-58
View File
@@ -1,58 +0,0 @@
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze([
"APP_HOME",
"SAMPLE_RESOURCE_LIST",
"NOT_FOUND",
]);
const REVIEW_FIELDS = Object.freeze([
"M1 Keyboard",
"M2 Visible focus",
"M3 Route focus",
"M4 Modal focus",
"M5 Error association",
"M6 Color signal",
"M7 Reduced motion",
"Screen reader",
]);
/** @param {string} content */
export function validateManualA11yEvidence(content) {
const fields = Object.fromEntries(
content
.split(/\r?\n/)
.map((line) => /^([^:]+):\s*(.*)$/.exec(line))
.filter(Boolean)
.map((match) => [
/** @type {RegExpExecArray} */ (match)[1].trim(),
/** @type {RegExpExecArray} */ (match)[2].trim(),
]),
);
const failures = [];
if (fields.Status !== "reviewed") failures.push("Status");
if (!fields["Route ID"]) failures.push("Route ID");
if (!fields["Release ID"]) failures.push("Release ID");
if (!fields.Reviewer) failures.push("Reviewer");
if (!fields.Signature) failures.push("Signature");
if (fields.Attestation !== "accepted") failures.push("Attestation");
if (
!fields["Reviewed at"] ||
!Number.isFinite(Date.parse(fields["Reviewed at"]))
) {
failures.push("Reviewed at");
}
for (const field of REVIEW_FIELDS) {
const result = fields[field];
if (
result !== "pass" &&
!/^not-applicable \(.+\)$/.test(result ?? "")
) {
failures.push(field);
}
}
return Object.freeze({
fields: Object.freeze(fields),
failures: Object.freeze(failures),
passed: failures.length === 0,
});
}
+82
View File
@@ -0,0 +1,82 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
const scanRoots = ["src", "dist"];
const findings = /** @type {Array<{ruleId: string, file: string}>} */ ([]);
const patterns = [
{ id: "private-key", expression: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g },
{ id: "aws-access-key", expression: /\bAKIA[0-9A-Z]{16}\b/g },
{ id: "github-token", expression: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g },
{
id: "assigned-secret",
expression:
/\b(?:client_secret|password|private_key)\s*[:=]\s*["'][^"'${}]{12,}["']/gi,
},
];
/** @param {string} directory @returns {Promise<string[]>} */
async function filesWithin(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = /** @type {string[][]} */ (await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? filesWithin(target) : [target];
}),
));
return nested.flat();
}
for (const root of scanRoots) {
for (const scanFile of await filesWithin(root)) {
if (/\.(png|jpg|jpeg|gif|woff2?|zip)$/i.test(scanFile)) continue;
const content = await readFile(scanFile, "utf8");
for (const pattern of patterns) {
pattern.expression.lastIndex = 0;
if (pattern.expression.test(content)) {
findings.push({ ruleId: pattern.id, file: scanFile });
}
}
}
}
const sarif = {
version: "2.1.0",
$schema:
"https://json.schemastore.org/sarif-2.1.0.json",
runs: [
{
tool: {
driver: {
name: "ca-frontend-secret-scan",
rules: patterns.map((pattern) => ({
id: pattern.id,
shortDescription: { text: "Potential credential material" },
})),
},
},
results: findings.map((finding) => ({
ruleId: finding.ruleId,
message: { text: "Potential secret material must be removed." },
locations: [
{
physicalLocation: {
artifactLocation: { uri: finding.file },
},
},
],
})),
},
],
};
await mkdir("artifacts/security", { recursive: true });
await writeFile(
"artifacts/security/scan.sarif",
`${JSON.stringify(sarif, null, 2)}\n`,
);
if (findings.length > 0) {
process.stderr.write(`Security scan found ${findings.length} blocking result(s).\n`);
process.exit(1);
}
process.stdout.write("Source and built-asset secret scan: PASS\n");
+18 -64
View File
@@ -1,71 +1,25 @@
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import { const evidence = await readFile(
MANUAL_A11Y_ROUTE_IDS, "artifacts/tests/a11y-manual/APP_HOME.md",
validateManualA11yEvidence, "utf8",
} from "./lib/manual-a11y-evidence.mjs";
/** @type {Array<{
* routeId: string;
* path: string;
* reviewer: string | null;
* reviewedAt: string | null;
* releaseId: string | null;
* failures: readonly string[];
* passed: boolean;
* }>} */
const results = [];
for (const routeId of MANUAL_A11Y_ROUTE_IDS) {
const path = `artifacts/tests/a11y-manual/${routeId}.md`;
const evidence = await readFile(path, "utf8");
const validation = validateManualA11yEvidence(evidence);
const failures =
validation.fields["Route ID"] === routeId
? validation.failures
: Object.freeze([...validation.failures, "Route ID mismatch"]);
results.push({
routeId,
path,
reviewer: validation.fields.Reviewer ?? null,
reviewedAt: validation.fields["Reviewed at"] ?? null,
releaseId: validation.fields["Release ID"] ?? null,
failures,
passed: validation.passed && failures.length === 0,
});
}
const releaseIds = new Set(results.map((result) => result.releaseId));
const passed =
results.every((result) => result.passed) &&
releaseIds.size === 1 &&
results.every((result) => Boolean(result.releaseId));
await mkdir("artifacts/tests/a11y-manual", { recursive: true });
await writeFile(
"artifacts/tests/a11y-manual/report.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
scope: MANUAL_A11Y_ROUTE_IDS,
results,
coherentRelease: releaseIds.size === 1,
passed,
},
null,
2,
)}\n`,
); );
if (!passed) { const required = [
const failures = results "Status: reviewed",
.filter((result) => !result.passed) "Reviewer:",
.map((result) => `${result.routeId}: ${result.failures.join(", ")}`); "Keyboard:",
if (releaseIds.size !== 1) failures.push("release IDs do not match"); "Focus:",
"Screen reader:",
"Reduced motion:",
"Color signal:",
];
const missing = required.filter((marker) => !evidence.includes(marker));
if (missing.length > 0) {
process.stderr.write( process.stderr.write(
`Manual accessibility evidence is incomplete:\n${failures.join("\n")}\n`, `Manual accessibility evidence is incomplete: ${missing.join(", ")}\n`,
); );
process.exit(1); process.exit(1);
} }
process.stdout.write( process.stdout.write("Manual accessibility evidence: PASS\n");
`Manual accessibility evidence: PASS (${results.length} routes)\n`,
);
+183
View File
@@ -0,0 +1,183 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { classifyLiveHostingBaseUrl } from "./lib/hosting-probe.mjs";
const cachePolicy = JSON.parse(
await readFile("config/hosting/cache-policy.json", "utf8"),
);
const securityPolicy = JSON.parse(
await readFile("config/hosting/security-headers.json", "utf8"),
);
const baseUrl = process.env.HOSTING_BASE_URL;
const liveTarget = baseUrl ? classifyLiveHostingBaseUrl(baseUrl) : null;
const distFiles = (await readdir("dist", { recursive: true })).map(String);
const publicSourceMaps = distFiles.filter((file) => file.endsWith(".map"));
const publicServiceWorkers = distFiles.filter((file) =>
/(?:^|\/)(?:service-worker|sw)(?:[.-][^/]*)?\.js$/i.test(file),
);
/** @type {Record<string, Record<string, string>>} */
let responses = {};
let mode;
/** @type {Array<{
* surface: string;
* header: string;
* expected: unknown;
* observed: unknown;
* reason?: string;
* passed: boolean;
* }>} */
const probeResults = [];
if (liveTarget?.passed) {
mode = "live";
const assets = await readdir("dist/assets");
const hashedJavaScript = assets.find((file) => file.endsWith(".js"));
if (!hashedJavaScript) throw new Error("No built hashed JavaScript found.");
const paths = {
index: "/",
runtimeConfig: "/config.json",
releaseManifest: "/release-manifest.json",
hashedAsset: `/assets/${hashedJavaScript}`,
};
responses = {};
for (const [surface, pathname] of Object.entries(paths)) {
const requestedUrl = new URL(pathname, liveTarget.url);
try {
const response = await fetch(requestedUrl, { redirect: "follow" });
const finalUrl = new URL(response.url);
probeResults.push(
{
surface,
header: "http-status",
expected: 200,
observed: response.status,
passed: response.status === 200,
},
{
surface,
header: "final-origin",
expected: liveTarget.url.origin,
observed: finalUrl.origin,
passed: finalUrl.origin === liveTarget.url.origin,
},
);
responses[surface] = Object.fromEntries(
[...response.headers.entries()].map(([name, value]) => [
name.toLowerCase(),
value,
]),
);
} catch (error) {
probeResults.push({
surface,
header: "transport",
expected: "reachable",
observed: error instanceof Error ? error.name : "UnknownError",
passed: false,
});
}
}
} else if (liveTarget) {
mode = "invalid-live";
probeResults.push({
surface: "deployment",
header: "base-url",
expected: "canonical non-loopback HTTPS root URL",
observed: liveTarget.observedOrigin,
reason: liveTarget.reason,
passed: false,
});
} else {
mode = "fixture";
responses = JSON.parse(
await readFile("config/hosting/response-headers.fixture.json", "utf8"),
).responses;
}
const results = [...probeResults];
for (const [surface, policy] of Object.entries(cachePolicy.surfaces)) {
if (!("cacheControl" in policy)) continue;
const observed = responses[surface]?.["cache-control"];
results.push({
surface,
header: "cache-control",
expected: policy.cacheControl,
observed,
passed: observed === policy.cacheControl,
});
const observedContentType = responses[surface]?.["content-type"];
const observedMime = observedContentType
?.split(";", 1)[0]
.trim()
.toLowerCase();
results.push({
surface,
header: "content-type",
expected: policy.contentTypes,
observed: observedContentType,
passed: policy.contentTypes.includes(observedMime),
});
if (policy.securityHeaders) {
for (const [header, expected] of Object.entries(securityPolicy.headers)) {
const observedSecurity = responses[surface]?.[header.toLowerCase()];
results.push({
surface,
header: header.toLowerCase(),
expected,
observed: observedSecurity,
passed: observedSecurity === expected,
});
}
}
}
results.push({
surface: "sourceMap",
header: "public",
expected: false,
observed: publicSourceMaps.length > 0,
passed:
cachePolicy.surfaces.sourceMap.public === false &&
publicSourceMaps.length === 0,
});
results.push({
surface: "serviceWorker",
header: "enabled",
expected: false,
observed: publicServiceWorkers.length > 0,
passed:
cachePolicy.surfaces.serviceWorker.enabled === false &&
publicServiceWorkers.length === 0,
});
const passed = results.every((result) => result.passed);
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/hosting-headers.json",
`${JSON.stringify(
{
schemaVersion: 1,
generatedAt: new Date().toISOString(),
mode,
baseUrl: liveTarget?.observedOrigin ?? null,
providerVerificationRequired: mode !== "live",
results,
passed,
},
null,
2,
)}\n`,
);
if (!passed) {
process.stderr.write(
"Hosting cache/content-type/security header verification failed.\n",
);
process.exit(1);
}
process.stdout.write(
`Hosting header contract: PASS (${mode}; live verification ${
mode === "live" ? "complete" : "required before promotion"
})\n`,
);
+98
View File
@@ -0,0 +1,98 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { verifyCompatibilityTuple } from "../src/application/policies/compatibility.js";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../src/contracts/release-tokens.js";
const fixturesDocument =
/** @type {{
* fixtures: Array<{
* name: string,
* expectedCompatible: boolean,
* frontend: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* },
* runtime: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* }
* }>
* }} */ (
JSON.parse(
await readFile("config/release/coherence-fixtures.json", "utf8"),
)
);
const release = JSON.parse(await readFile("dist/release-manifest.json", "utf8"));
const runtimeConfig = JSON.parse(await readFile("dist/config.json", "utf8"));
const viteManifest = await readFile("dist/.vite/manifest.json");
const actualAssetManifestHash = createHash("sha256")
.update(viteManifest)
.digest("hex");
const artifactComparison = compareReleaseToRuntime(release, runtimeConfig);
const artifactMismatches = [...artifactComparison.mismatches];
for (const token of Object.keys(RELEASE_TOKEN_REGISTRY)) {
if (typeof release[token] !== "string" || release[token].length === 0) {
artifactMismatches.push(`releaseToken:${token}`);
}
}
if (!Number.isFinite(Date.parse(release.builtAt))) {
artifactMismatches.push("releaseToken:builtAtFormat");
}
if (release.assetManifestHash !== actualAssetManifestHash) {
artifactMismatches.push("assetManifestContent");
}
const fixtures = fixturesDocument.fixtures.map((fixture) => {
const result = verifyCompatibilityTuple({
frontend: fixture.frontend,
runtime: fixture.runtime,
});
return {
name: fixture.name,
expectedCompatible: fixture.expectedCompatible,
actualCompatible: result.compatible,
mismatches: result.mismatches,
passed: result.compatible === fixture.expectedCompatible,
};
});
const artifact = {
checked: true,
compatible: artifactComparison.compatible && artifactMismatches.length === 0,
mismatches: artifactMismatches,
releaseId: release.releaseId,
};
const passed = artifact.compatible && fixtures.every((fixture) => fixture.passed);
const report = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
artifact,
fixtures,
passed,
};
await mkdir("artifacts/release", { recursive: true });
await writeFile(
"artifacts/release/verification.json",
`${JSON.stringify(report, null, 2)}\n`,
);
if (!passed) {
process.stderr.write(
`Release coherence failed: ${artifactMismatches.join(", ") || "fixture"}\n`,
);
process.exit(1);
}
process.stdout.write(
`Release coherence: PASS (${fixtures.length - 1} mixed fixtures rejected)\n`,
);
+1 -1
View File
@@ -10,7 +10,7 @@ await writeFile(
scope: ["APP_HOME", "SAMPLE_RESOURCE_LIST", "NOT_FOUND"], scope: ["APP_HOME", "SAMPLE_RESOURCE_LIST", "NOT_FOUND"],
threshold: { critical: 0, serious: 0 }, threshold: { critical: 0, serious: 0 },
automatedStatus: "passed", automatedStatus: "passed",
manualReview: "see artifacts/tests/a11y-manual/report.json", manualReview: "see artifacts/tests/a11y-manual/APP_HOME.md",
}, },
null, null,
2, 2,
+102
View File
@@ -0,0 +1,102 @@
export const COMPATIBILITY_TUPLE_FIELDS = Object.freeze([
"buildId",
"configSchemaVersion",
"apiContractVersion",
"assetManifestHash",
"releaseId",
]);
/** @param {string} version */
export function parseNumericVersion(version) {
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(version);
if (!match) return null;
return {
major: Number(match[1]),
minor: Number(match[2] ?? 0),
patch: Number(match[3] ?? 0),
};
}
/** @param {string} supported @param {string} actual */
export function isVersionCompatible(supported, actual) {
const expected = parseNumericVersion(supported);
const candidate = parseNumericVersion(actual);
if (!expected || !candidate) return false;
return (
expected.major === candidate.major &&
candidate.minor >= expected.minor
);
}
/**
* @param {{
* frontend: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* },
* runtime: {
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* }
* }} input
*/
export function verifyCompatibilityTuple(input) {
const mismatches = [];
if (input.frontend.buildId !== input.runtime.buildId) mismatches.push("buildId");
if (
!isVersionCompatible(
input.frontend.configSchemaVersion,
input.runtime.configSchemaVersion,
)
) {
mismatches.push("configSchemaVersion");
}
if (
!isVersionCompatible(
input.frontend.apiContractVersion,
input.runtime.apiContractVersion,
)
) {
mismatches.push("apiContractVersion");
}
if (input.frontend.assetManifestHash !== input.runtime.assetManifestHash) {
mismatches.push("assetManifestHash");
}
const releaseWarning =
input.frontend.releaseId === input.runtime.releaseId
? null
: "releaseId";
return Object.freeze({
compatible: mismatches.length === 0,
mismatches: Object.freeze(mismatches),
warnings: Object.freeze(releaseWarning ? [releaseWarning] : []),
});
}
/**
* @param {{ required?: string[], properties?: Record<string, unknown> }} before
* @param {{ required?: string[], properties?: Record<string, unknown> }} after
*/
export function classifyObjectSchemaChange(before, after) {
const beforeRequired = new Set(before.required ?? []);
const afterRequired = new Set(after.required ?? []);
const removedProperties = Object.keys(before.properties ?? {}).filter(
(key) => !(key in (after.properties ?? {})),
);
const addedRequired = [...afterRequired].filter(
(key) => !beforeRequired.has(key),
);
if (removedProperties.length > 0 || addedRequired.length > 0) return "breaking";
const addedProperties = Object.keys(after.properties ?? {}).filter(
(key) => !(key in (before.properties ?? {})),
);
return addedProperties.length > 0 ? "additive" : "none";
}
+65
View File
@@ -0,0 +1,65 @@
import { verifyCompatibilityTuple } from "../application/policies/compatibility.js";
export const RELEASE_TOKEN_REGISTRY = Object.freeze({
appVersion: token("appVersion", "manifest", "human release label"),
buildId: token("buildId", "CI build", "asset and HTML coherence"),
commitSha: token("commitSha", "VCS", "source traceability"),
configSchemaVersion: token(
"configSchemaVersion",
"runtime config schema",
"boot compatibility",
),
apiContractVersion: token(
"apiContractVersion",
"frontend/backend agreement",
"schema compatibility",
),
assetManifestHash: token(
"assetManifestHash",
"build output",
"chunk integrity and mismatch detection",
),
releaseId: token("releaseId", "deploy system", "rollback target"),
builtAt: token("builtAt", "CI", "diagnostics only; never cache identity"),
});
/**
* @param {string} name
* @param {string} source
* @param {string} compatibilityRole
*/
function token(name, source, compatibilityRole) {
return Object.freeze({ token: name, source, compatibilityRole });
}
/**
* Compare a release manifest and runtime configuration structurally. Version
* fields are delegated to the numeric compatibility policy, never compared
* lexically.
*
* @param {{
* buildId: string,
* configSchemaVersion: string,
* apiContractVersion: string,
* assetManifestHash: string,
* releaseId: string
* }} release
* @param {{
* BUILD_ID: string,
* CONFIG_SCHEMA_VERSION: string,
* API_CONTRACT_VERSION: string,
* RELEASE_ID: string
* }} runtimeConfig
*/
export function compareReleaseToRuntime(release, runtimeConfig) {
return verifyCompatibilityTuple({
frontend: release,
runtime: {
buildId: runtimeConfig.BUILD_ID,
configSchemaVersion: runtimeConfig.CONFIG_SCHEMA_VERSION,
apiContractVersion: runtimeConfig.API_CONTRACT_VERSION,
assetManifestHash: release.assetManifestHash,
releaseId: runtimeConfig.RELEASE_ID,
},
});
}
+9
View File
@@ -0,0 +1,9 @@
/**
* Untrusted content is rendered as a React text node. HTML interpretation is
* intentionally not offered by this template.
*
* @param {{ value: unknown }} props
*/
export function SafeText({ value }) {
return <span>{typeof value === "string" ? value : String(value ?? "")}</span>;
}
+53
View File
@@ -0,0 +1,53 @@
// @vitest-environment jsdom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { SafeText } from "../../src/presentation/security/safe-text.jsx";
import { assertSafeConfigNames } from "../../src/contracts/env.js";
import { defineStorageKey } from "../../src/contracts/storage-keys.js";
import { projectTelemetryEvent } from "../../src/contracts/telemetry.js";
describe("browser security boundary", () => {
it("renders untrusted text without script or inline handler injection", () => {
render(
<SafeText value={'<img src=x onerror="window.compromised=true"><script>x</script>'} />,
);
expect(screen.getByText(/<img/)).toBeVisible();
expect(document.querySelector("script")).toBeNull();
expect(document.querySelector("[onerror]")).toBeNull();
});
it("rejects secret-like client configuration names", () => {
expect(() => assertSafeConfigNames({ PRIVATE_KEY: "not-public" })).toThrow();
});
it("rejects browser token storage registration", () => {
expect(() =>
defineStorageKey({
logicalName: "SESSION_TOKEN",
scope: "auth",
name: "session-token",
backend: "sessionStorage",
classification: "sensitive-forbidden",
schemaVersion: 1,
ttl: "session",
migration: "discard",
quotaFallback: "feature-disable",
}),
).toThrow();
});
it("drops raw URL/query/token telemetry attributes", () => {
const result = projectTelemetryEvent("api.request.failed", {
error_kind: "SERVER_FAILURE",
http_status_group: "5xx",
attempt_count_bucket: "1",
route_id: "APP_HOME",
raw_url: "https://api.test?token=private",
query_string: "token=private",
});
expect(result.success).toBe(true);
expect(JSON.stringify(result)).not.toMatch(/raw_url|query_string|private/);
});
});
+3
View File
@@ -0,0 +1,3 @@
export function Fixture({ value }) {
return <span>{value}</span>;
}
+5
View File
@@ -0,0 +1,5 @@
export function attachScript(source) {
const script = document.createElement("script");
script.src = source;
document.head.append(script);
}
+1
View File
@@ -0,0 +1 @@
export const execute = (source) => eval(source);
+3
View File
@@ -0,0 +1,3 @@
export function RawHtml({ value }) {
return <div dangerouslySetInnerHTML={{ __html: value }} />;
}
+77
View File
@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import {
classifyObjectSchemaChange,
isVersionCompatible,
parseNumericVersion,
verifyCompatibilityTuple,
} from "../../src/application/policies/compatibility.js";
describe("contract compatibility", () => {
it("uses numeric version parsing rather than lexical comparison", () => {
expect(parseNumericVersion("1.10.0")).toEqual({ major: 1, minor: 10, patch: 0 });
expect(isVersionCompatible("1.9", "1.10")).toBe(true);
expect(isVersionCompatible("1.9", "2.0")).toBe(false);
expect(isVersionCompatible("next", "1.0")).toBe(false);
});
it("distinguishes additive and breaking object changes", () => {
const base = { required: ["id"], properties: { id: {} } };
expect(
classifyObjectSchemaChange(base, {
required: ["id"],
properties: { id: {}, name: {} },
}),
).toBe("additive");
expect(
classifyObjectSchemaChange(base, {
required: ["id", "name"],
properties: { id: {}, name: {} },
}),
).toBe("breaking");
});
it("treats release ID mismatch as a warning when the blocking tuple is coherent", () => {
const frontend = {
buildId: "build-a",
configSchemaVersion: "1.0",
apiContractVersion: "1.0",
assetManifestHash: "hash-a",
releaseId: "release-a",
};
expect(
verifyCompatibilityTuple({
frontend,
runtime: { ...frontend, releaseId: "release-b" },
}),
).toEqual({
compatible: true,
mismatches: [],
warnings: ["releaseId"],
});
});
it("blocks mixed build, config, API, or asset tuples", () => {
const frontend = {
buildId: "build-a",
configSchemaVersion: "1.0",
apiContractVersion: "1.0",
assetManifestHash: "hash-a",
releaseId: "release-a",
};
expect(
verifyCompatibilityTuple({
frontend,
runtime: {
...frontend,
buildId: "build-b",
configSchemaVersion: "2.0",
assetManifestHash: "hash-b",
},
}),
).toMatchObject({
compatible: false,
mismatches: ["buildId", "configSchemaVersion", "assetManifestHash"],
});
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { classifyLiveHostingBaseUrl } from "../../scripts/lib/hosting-probe.mjs";
describe("live hosting evidence target", () => {
it("accepts a canonical production HTTPS root", () => {
expect(
classifyLiveHostingBaseUrl("https://frontend.example.test/"),
).toMatchObject({
passed: true,
observedOrigin: "https://frontend.example.test",
});
});
it.each([
["http://frontend.example.test/", "requires HTTPS"],
["https://localhost:4173/", "not live deployment evidence"],
["https://127.0.0.1/", "not live deployment evidence"],
["https://frontend.example.test/app/", "canonical root URL"],
["https://user:secret@frontend.example.test/", "must not contain credentials"],
])("rejects %s", (url, reason) => {
expect(classifyLiveHostingBaseUrl(url)).toMatchObject({
passed: false,
reason: expect.stringContaining(reason),
});
});
});
-57
View File
@@ -1,57 +0,0 @@
import { describe, expect, it } from "vitest";
import { validateManualA11yEvidence } from "../../scripts/lib/manual-a11y-evidence.mjs";
const reviewed = `Status: reviewed
Route ID: APP_HOME
Release ID: release-1
Reviewer: reviewer@example.test
Reviewed at: 2026-07-25T12:00:00Z
Signature: review-record-1
Attestation: accepted
M1 Keyboard: pass
M2 Visible focus: pass
M3 Route focus: pass
M4 Modal focus: not-applicable (no modal)
M5 Error association: not-applicable (no form error)
M6 Color signal: pass
M7 Reduced motion: pass
Screen reader: pass
Notes: no defects`;
describe("manual accessibility evidence", () => {
it("accepts a complete signed human review record", () => {
expect(validateManualA11yEvidence(reviewed)).toMatchObject({
failures: [],
passed: true,
});
});
it("rejects pending, unsigned, or incomplete evidence", () => {
expect(
validateManualA11yEvidence(
reviewed
.replace("Status: reviewed", "Status: pending-manual-review")
.replace("Signature: review-record-1", "Signature:")
.replace("Screen reader: pass", "Screen reader: pending"),
),
).toMatchObject({
failures: ["Status", "Signature", "Screen reader"],
passed: false,
});
});
it("does not treat an unexplained not-applicable verdict as evidence", () => {
expect(
validateManualA11yEvidence(
reviewed.replace(
"M4 Modal focus: not-applicable (no modal)",
"M4 Modal focus: not-applicable",
),
),
).toMatchObject({
failures: ["M4 Modal focus"],
passed: false,
});
});
});
+21
View File
@@ -0,0 +1,21 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
describe("registry governance manifest", () => {
it("declares exactly eight single-owner registries and impact labels", async () => {
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
);
expect(governance.registries).toHaveLength(8);
expect(new Set(governance.registries.map((entry) => entry.registryId)).size).toBe(
8,
);
expect(governance.registries.every((entry) => entry.owner)).toBe(true);
expect(governance.compatibilityImpact.allowed).toEqual([
"none",
"additive",
"behavior-change",
"breaking",
]);
});
});
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import {
compareReleaseToRuntime,
RELEASE_TOKEN_REGISTRY,
} from "../../src/contracts/release-tokens.js";
describe("release coherence", () => {
it("owns all eight release tokens and keeps builtAt diagnostic-only", () => {
expect(Object.keys(RELEASE_TOKEN_REGISTRY)).toHaveLength(8);
expect(RELEASE_TOKEN_REGISTRY.builtAt.compatibilityRole).toContain(
"never cache identity",
);
});
it("compares the runtime config to the release structurally", () => {
const release = {
buildId: "build-a",
configSchemaVersion: "1.0",
apiContractVersion: "1.0",
assetManifestHash: "assets-a",
releaseId: "release-a",
};
expect(
compareReleaseToRuntime(release, {
BUILD_ID: "build-a",
CONFIG_SCHEMA_VERSION: "1.2",
API_CONTRACT_VERSION: "1.1",
RELEASE_ID: "release-a",
}),
).toMatchObject({ compatible: true, mismatches: [] });
});
it("rejects HTML-only rollback against a newer runtime config", () => {
const oldRelease = {
buildId: "build-old",
configSchemaVersion: "1.0",
apiContractVersion: "1.0",
assetManifestHash: "assets-old",
releaseId: "release-old",
};
expect(
compareReleaseToRuntime(oldRelease, {
BUILD_ID: "build-new",
CONFIG_SCHEMA_VERSION: "2.0",
API_CONTRACT_VERSION: "2.0",
RELEASE_ID: "release-new",
}),
).toMatchObject({
compatible: false,
mismatches: ["buildId", "configSchemaVersion", "apiContractVersion"],
});
});
});