@brandlab-digital/conformance 2.1.0 → 3.0.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.
- package/dist/analytics-e2e.d.ts +52 -0
- package/dist/analytics-e2e.d.ts.map +1 -0
- package/dist/analytics-e2e.js +189 -0
- package/dist/analytics-e2e.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/contact-e2e.d.ts +43 -0
- package/dist/contact-e2e.d.ts.map +1 -0
- package/dist/contact-e2e.js +193 -0
- package/dist/contact-e2e.js.map +1 -0
- package/dist/current-release.js +7 -7
- package/dist/current-release.js.map +1 -1
- package/dist/e2e-support.d.ts +42 -0
- package/dist/e2e-support.d.ts.map +1 -0
- package/dist/e2e-support.js +126 -0
- package/dist/e2e-support.js.map +1 -0
- package/dist/engineering.d.ts +14 -0
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +446 -50
- package/dist/engineering.js.map +1 -1
- package/dist/estate.d.ts +105 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +391 -0
- package/dist/estate.js.map +1 -0
- package/dist/families-e2e.d.ts +45 -0
- package/dist/families-e2e.d.ts.map +1 -0
- package/dist/families-e2e.js +248 -0
- package/dist/families-e2e.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +138 -0
- package/dist/glob.js.map +1 -0
- package/dist/knip.d.ts +47 -0
- package/dist/knip.d.ts.map +1 -0
- package/dist/knip.js +84 -0
- package/dist/knip.js.map +1 -0
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +163 -147
- package/dist/project.js.map +1 -1
- package/dist/public-copy.d.ts +36 -0
- package/dist/public-copy.d.ts.map +1 -0
- package/dist/public-copy.js +517 -0
- package/dist/public-copy.js.map +1 -0
- package/dist/rule-registry.d.ts +292 -0
- package/dist/rule-registry.d.ts.map +1 -0
- package/dist/rule-registry.js +68 -0
- package/dist/rule-registry.js.map +1 -0
- package/dist/search-certification.d.ts +4 -0
- package/dist/search-certification.d.ts.map +1 -1
- package/dist/search-certification.js +17 -3
- package/dist/search-certification.js.map +1 -1
- package/dist/source-files.d.ts +54 -0
- package/dist/source-files.d.ts.map +1 -0
- package/dist/source-files.js +323 -0
- package/dist/source-files.js.map +1 -0
- package/dist/static-rules.d.ts +45 -0
- package/dist/static-rules.d.ts.map +1 -0
- package/dist/static-rules.js +762 -0
- package/dist/static-rules.js.map +1 -0
- package/dist/verification-policy.d.ts +13 -4
- package/dist/verification-policy.d.ts.map +1 -1
- package/dist/verification-policy.js +31 -12
- package/dist/verification-policy.js.map +1 -1
- package/package.json +37 -1
package/dist/knip.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knip.js","sourceRoot":"","sources":["../src/knip.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared Knip preset for Brandlab Next.js projects:\n *\n * // knip.ts\n * import { brandlabKnip } from '@brandlab-digital/conformance/knip'\n * export default brandlabKnip({ mdx: true })\n *\n * It keeps Knip's own Next.js, Playwright, Vitest and ESLint plugin entries\n * (overriding them would replace the maintained defaults) and adds only what\n * those plugins cannot see in a Brandlab project: production entries Next loads\n * outside the app graph, CSS-only dependencies and, when enabled, MDX articles.\n * Knip calls the returned function with its command-line mode.\n */\n\nexport type BrandlabKnipCompiler = (text: string, filename: string) => string;\n\n/** The subset of Knip's configuration the preset produces; it satisfies `KnipConfig`. */\nexport interface BrandlabKnipConfig {\n entry: string[];\n ignoreExportsUsedInFile: boolean;\n compilers: Record<string, true | BrandlabKnipCompiler>;\n project?: string[];\n ignoreDependencies?: string[];\n}\n\n/** The parsed Knip arguments Knip passes to a function configuration. */\nexport interface BrandlabKnipMode {\n production?: boolean;\n strict?: boolean;\n}\n\n/** A Knip function configuration: Knip calls it with its parsed arguments. */\nexport type BrandlabKnipConfigFunction = (mode?: BrandlabKnipMode) => BrandlabKnipConfig;\n\nexport interface BrandlabKnipOptions {\n /** The project renders long-form MDX articles from src/content/. */\n mdx?: boolean;\n /**\n * Additional real entry files that no plugin can discover. Suffix a pattern\n * with `!` when it is part of the production graph. Never list whole source\n * directories to silence findings.\n */\n entry?: readonly string[];\n}\n\nconst PRODUCTION_ENTRIES = [\n // Knip's production mode excludes next.config by design, so the redirect table it\n // imports (the fixed 3.0 owner) would look unused although Next applies it.\n 'src/data/redirects.ts!',\n // Read statically by brandlab-verify; it is part of the project, not dead code.\n 'brandlab.config.ts!',\n];\n\nconst SOURCE_EXTENSIONS = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'mts', 'cts'];\n\nexport function brandlabKnip(options: BrandlabKnipOptions = {}): BrandlabKnipConfigFunction {\n const entry = [...PRODUCTION_ENTRIES];\n const compilers: BrandlabKnipConfig['compilers'] = { css: compileCssImports };\n if (options.mdx) {\n // Articles are loaded by slug through dynamic imports that Knip cannot follow.\n entry.push('src/content/**/*.{md,mdx}!');\n // Knip's MDX compiler exposes each article's imports to the module graph.\n compilers.mdx = true;\n }\n for (const pattern of options.entry ?? []) {\n if (typeof pattern !== 'string' || !pattern.trim()) {\n throw new TypeError('brandlabKnip entry patterns must be non-empty strings.');\n }\n if (/^(?:src\\/?)?\\*\\*(?:\\/\\*)?!?$/.test(pattern.trim())) {\n throw new Error(`brandlabKnip entry \"${pattern}\" marks all source as used; list the real entry files instead.`);\n }\n entry.push(pattern.trim());\n }\n const extensions = `{${[...SOURCE_EXTENSIONS, ...Object.keys(compilers), ...(options.mdx ? ['md'] : [])].join(',')}}`;\n const config: BrandlabKnipConfig = {\n entry: [...new Set(entry)],\n // Exports that are also consumed inside their own module are not dead code.\n ignoreExportsUsedInFile: true,\n compilers,\n };\n // Otherwise no ignoreDependencies: optional peers of the shared packages\n // (eslint-config-next for configs.next) already count as used, and anything else\n // must stay visible.\n return (mode = {}) => {\n const copy = { ...config, entry: [...config.entry], compilers: { ...config.compilers } };\n if (mode.production !== true && mode.strict !== true) return copy;\n return {\n ...copy,\n // Knip's default project marks every file as production (**/*!), so production mode\n // reported test tooling (vitest.setup.ts, tests/ helpers) as unused files. The\n // application lives in src/; root production files are entries (brandlab.config.ts)\n // or belong to a plugin (next.config.ts, mdx-components.tsx).\n project: [`src/**/*.${extensions}!`],\n // Knip's production mode never reads next.config (not even as an entry), where the\n // MDX wrapper imports @next/mdx; the default graph still checks that import.\n ...(options.mdx ? { ignoreDependencies: ['@next/mdx'] } : {}),\n };\n };\n}\n\nconst CSS_COMMENT = /\\/\\*[\\s\\S]*?\\*\\//g;\nconst CSS_DIRECTIVE =\n /@(?:import|config|plugin|reference)\\s+(?:url\\(\\s*)?(?:\"([^\"]+)\"|'([^']+)'|([^'\"\\s;)]+))\\s*\\)?[^;]*;/g;\n\n/**\n * Expose CSS-only dependencies (`@import \"tailwindcss\"`, `@plugin`, `@config`,\n * `@reference`, local `@import`) to Knip as module imports. Remote URLs and\n * data URIs are not dependencies and are skipped.\n */\nexport function compileCssImports(text: string): string {\n const imports: string[] = [];\n for (const match of text.replace(CSS_COMMENT, '').matchAll(CSS_DIRECTIVE)) {\n const specifier = match[1] ?? match[2] ?? match[3];\n if (!specifier || /^(?:[a-z][a-z0-9+.-]*:|\\/\\/|#)/i.test(specifier)) continue;\n imports.push(`import ${JSON.stringify(specifier)};`);\n }\n return imports.join('\\n');\n}\n"]}
|
package/dist/project.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type PackedPreflight } from "./packed-preflight.js";
|
|
2
2
|
import { type BrandlabContractVersions, type BrandlabReleaseManifest, type BrandlabRuntimeReleaseContract } from "./release-manifest.js";
|
|
3
3
|
import { type BrandlabOperationalFinding, type BrandlabVerificationEvidence, type BrandlabVerificationResult } from "./verification-policy.js";
|
|
4
|
-
export {
|
|
4
|
+
export { BRANDLAB_STATIC_RULES, matchesBrandlabStaticRule, type BrandlabStaticRule, } from "./rule-registry.js";
|
|
5
|
+
export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, checkForRule, type BrandlabRuleEvaluation, type BrandlabVerificationCheckId, type BrandlabVerificationCheckResult, type BrandlabVerificationEvidence, type BrandlabVerificationEvidenceKind, type BrandlabVerificationFamilyId, type BrandlabVerificationFamilyResult, type BrandlabVerificationResult, type BrandlabVerificationStatus, } from "./verification-policy.js";
|
|
5
6
|
export declare const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v2";
|
|
6
7
|
export declare const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "current-only-v2";
|
|
7
8
|
export type BrandlabConformanceMode = "local" | "ci" | "preflight";
|
|
@@ -61,5 +62,7 @@ interface ProjectPackageJsonLike {
|
|
|
61
62
|
export declare function evaluateBrandlabProject(options: EvaluateBrandlabProjectOptions): BrandlabConformanceResult;
|
|
62
63
|
export declare function shouldFailBrandlabConformance(result: BrandlabConformanceResult): boolean;
|
|
63
64
|
export declare function detectInstalledBrandlabRelease(packageJson: ProjectPackageJsonLike | null): string | null;
|
|
65
|
+
/** Variable names documented in an env example file (`KEY=`, `export KEY=`); values are ignored. */
|
|
66
|
+
export declare function parseEnvironmentKeys(text: string): Set<string>;
|
|
64
67
|
export type { BrandlabContractVersions };
|
|
65
68
|
//# sourceMappingURL=project.d.ts.map
|
package/dist/project.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"AAGA,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACpF,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EAIpC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
|
package/dist/project.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import { join,
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { BRANDLAB_CURRENT_RUNTIME_RELEASE } from "./current-release.js";
|
|
4
4
|
import { verifyPackedPreflight } from "./packed-preflight.js";
|
|
5
5
|
import { BRANDLAB_SHARED_PACKAGE_NAMES, compareBrandlabSemver, digestBrandlabAuthority, isBrandlabSemver, projectBrandlabRuntimeReleaseContract, validateBrandlabReleaseManifest, validateBrandlabRuntimeReleaseContract, } from "./release-manifest.js";
|
|
6
|
+
import { BRANDLAB_SOURCE_ROOT_DIRECTORIES, collectBrandlabSource, lineAt, readOptionalText, stripCommentsPreservingLayout, } from "./source-files.js";
|
|
7
|
+
import { findContactContractRecipients, inspectStaticRules, recordEvaluation, staticFinding, } from "./static-rules.js";
|
|
6
8
|
import { applyBrandlabVerificationPolicy, BRANDLAB_VERIFICATION_POLICY_VERSION, } from "./verification-policy.js";
|
|
7
|
-
export {
|
|
9
|
+
export { BRANDLAB_STATIC_RULES, matchesBrandlabStaticRule, } from "./rule-registry.js";
|
|
10
|
+
export { BRANDLAB_VERIFICATION_CHECKS, BRANDLAB_VERIFICATION_FAMILIES, BRANDLAB_VERIFICATION_POLICY_VERSION, checkForRule, } from "./verification-policy.js";
|
|
8
11
|
export const BRANDLAB_CONFORMANCE_SCHEMA_VERSION = "brandlab.conformance.v2";
|
|
9
12
|
export const BRANDLAB_CONFORMANCE_EVALUATOR_VERSION = "current-only-v2";
|
|
10
13
|
const RUNTIME_PACKAGE_FIELDS = [
|
|
@@ -13,12 +16,11 @@ const RUNTIME_PACKAGE_FIELDS = [
|
|
|
13
16
|
["react-dom", "reactDom"],
|
|
14
17
|
["typescript", "typescript"],
|
|
15
18
|
];
|
|
16
|
-
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
|
|
17
19
|
const SOURCE_RULES = [
|
|
18
20
|
{
|
|
19
21
|
ruleId: "brandlab-source-retired-analytics-event",
|
|
20
22
|
surface: "analytics",
|
|
21
|
-
pattern: /\b(?:contact_form_submission|phone_call|phone_calls|form_start|cta_click)\b/,
|
|
23
|
+
pattern: /\b(?:contact_form_submission|phone_call|phone_calls|form_start|form_submit|generate_lead|cta_click)\b/,
|
|
22
24
|
expected: "Only the current Brandlab analytics event contract is used.",
|
|
23
25
|
remediation: "Use contact_form for accepted forms and phone_link_click for telephone-link diagnostics.",
|
|
24
26
|
},
|
|
@@ -106,6 +108,22 @@ const SOURCE_RULES = [
|
|
|
106
108
|
expected: "The simplified contact runtime has no project-level state-service requirement.",
|
|
107
109
|
remediation: "Remove the retired contact state variables and use the shared stateless contact flow.",
|
|
108
110
|
},
|
|
111
|
+
{
|
|
112
|
+
ruleId: "brandlab-analytics-retired-phone-api",
|
|
113
|
+
surface: "analytics",
|
|
114
|
+
pattern: /\btrackPhoneCall\b/,
|
|
115
|
+
expected: "Telephone taps are reported only as phone_link_click by the shared instrumentation.",
|
|
116
|
+
actual: "The retired trackPhoneCall API is still referenced.",
|
|
117
|
+
remediation: "Remove trackPhoneCall; installBrandlabInstrumentation reports one phone_link_click per tap.",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
ruleId: "brandlab-analytics-web-vitals",
|
|
121
|
+
surface: "analytics",
|
|
122
|
+
pattern: /\b(?:useReportWebVitals|reportWebVitals|reportBrandlabWebVital|createBrandlabWebVitalsReporter|CoreWebVitalsReporter)\b|['"](?:next\/web-vitals|web-vitals|web_vital)['"]|\bwebVitals\s*:/,
|
|
123
|
+
expected: "No web-vitals reporter; speed evidence comes from Search Console.",
|
|
124
|
+
actual: "A web-vitals reporter, web_vital event or webVitals flag is still wired.",
|
|
125
|
+
remediation: "Remove the web-vitals reporter and the webVitals flag; web_vital events are not sent in 3.0.",
|
|
126
|
+
},
|
|
109
127
|
];
|
|
110
128
|
export function evaluateBrandlabProject(options) {
|
|
111
129
|
const root = resolve(options.root);
|
|
@@ -133,8 +151,11 @@ export function evaluateBrandlabProject(options) {
|
|
|
133
151
|
? options.releaseManifest
|
|
134
152
|
: null;
|
|
135
153
|
const findings = [];
|
|
154
|
+
const evaluations = [];
|
|
155
|
+
const source = collectBrandlabSource(root);
|
|
136
156
|
inspectRelease({
|
|
137
157
|
findings,
|
|
158
|
+
evaluations,
|
|
138
159
|
packageJson,
|
|
139
160
|
packageLock,
|
|
140
161
|
installedRelease,
|
|
@@ -143,12 +164,20 @@ export function evaluateBrandlabProject(options) {
|
|
|
143
164
|
attestation,
|
|
144
165
|
packedPreflight,
|
|
145
166
|
});
|
|
146
|
-
if (manifest?.runtime.nextVersionPolicy === "exact")
|
|
147
|
-
inspectImageRuntime(root, findings);
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
if (manifest?.runtime.nextVersionPolicy === "exact") {
|
|
168
|
+
inspectImageRuntime(root, findings, evaluations);
|
|
169
|
+
}
|
|
170
|
+
inspectProjectConfig(root, source, findings, evaluations);
|
|
171
|
+
inspectProjectSource(source, findings, evaluations);
|
|
172
|
+
inspectStaticRules({
|
|
173
|
+
root,
|
|
174
|
+
source,
|
|
175
|
+
config: readOptionalText(join(root, "brandlab.config.ts")),
|
|
176
|
+
findings,
|
|
177
|
+
evaluations,
|
|
178
|
+
});
|
|
150
179
|
const sortedFindings = findings.sort(compareFindings);
|
|
151
|
-
const policyResult = applyBrandlabVerificationPolicy(sortedFindings, options.verificationEvidence);
|
|
180
|
+
const policyResult = applyBrandlabVerificationPolicy(sortedFindings, options.verificationEvidence, evaluations);
|
|
152
181
|
const operationalFindings = policyResult.operationalFindings.sort(compareFindings);
|
|
153
182
|
const authorityDigest = digestBrandlabAuthority({
|
|
154
183
|
release,
|
|
@@ -202,23 +231,28 @@ export function detectInstalledBrandlabRelease(packageJson) {
|
|
|
202
231
|
return new Set(versions).size === 1 ? versions[0] : null;
|
|
203
232
|
}
|
|
204
233
|
function inspectRelease(options) {
|
|
234
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-package-json-missing");
|
|
205
235
|
if (!options.packageJson) {
|
|
206
236
|
options.findings.push(releaseFinding("brandlab-release-package-json-missing", "A readable package.json", "package.json is missing or invalid.", "Restore a valid package.json."));
|
|
207
237
|
return;
|
|
208
238
|
}
|
|
239
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-installed-version-unresolved");
|
|
209
240
|
if (!options.installedRelease) {
|
|
210
241
|
options.findings.push(releaseFinding("brandlab-release-installed-version-unresolved", "All four Brandlab packages pinned to one exact release", "The installed Brandlab release could not be resolved.", "Pin all four packages to one exact supported version."));
|
|
211
242
|
return;
|
|
212
243
|
}
|
|
244
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-unsupported");
|
|
213
245
|
if (!options.manifest) {
|
|
214
246
|
options.findings.push(releaseFinding("brandlab-release-unsupported", "The release selected by the active Brandlab channel", `${options.installedRelease} is not supported by the active platform.`, "Upgrade the project to the current candidate before further work."));
|
|
215
247
|
return;
|
|
216
248
|
}
|
|
249
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-declared-installed-mismatch");
|
|
217
250
|
if (options.detectedRelease !== options.installedRelease) {
|
|
218
251
|
options.findings.push(releaseFinding("brandlab-release-declared-installed-mismatch", options.installedRelease, options.detectedRelease ?? "unresolved", "Restore one exact package version across all four Brandlab packages.", "package.json"));
|
|
219
252
|
}
|
|
220
253
|
inspectBrandlabPackages({
|
|
221
254
|
findings: options.findings,
|
|
255
|
+
evaluations: options.evaluations,
|
|
222
256
|
packageJson: options.packageJson,
|
|
223
257
|
packageLock: options.packageLock,
|
|
224
258
|
installedRelease: options.installedRelease,
|
|
@@ -227,16 +261,19 @@ function inspectRelease(options) {
|
|
|
227
261
|
});
|
|
228
262
|
inspectRuntime({
|
|
229
263
|
findings: options.findings,
|
|
264
|
+
evaluations: options.evaluations,
|
|
230
265
|
packageJson: options.packageJson,
|
|
231
266
|
packageLock: options.packageLock,
|
|
232
267
|
manifest: options.manifest,
|
|
233
268
|
});
|
|
234
269
|
}
|
|
235
270
|
function inspectBrandlabPackages(options) {
|
|
271
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-package-lock");
|
|
236
272
|
if (options.packageLock.state !== "valid") {
|
|
237
273
|
options.findings.push(releaseFinding(`brandlab-release-package-lock-${options.packageLock.state}`, "A valid npm lockfile v2 or v3", `package-lock.json state is ${options.packageLock.state}.`, "Regenerate the lockfile from the exact package declarations.", "package-lock.json"));
|
|
238
274
|
return;
|
|
239
275
|
}
|
|
276
|
+
recordEvaluation(options.evaluations, "release", "brandlab-release-package-pin-mismatch", "brandlab-release-package-resolution-mismatch", ...(options.attestation ? ["brandlab-release-package-integrity-mismatch"] : []));
|
|
240
277
|
for (const packageName of BRANDLAB_SHARED_PACKAGE_NAMES) {
|
|
241
278
|
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
242
279
|
const locked = lockedRootSpec(options.packageLock, packageName);
|
|
@@ -258,12 +295,17 @@ function inspectBrandlabPackages(options) {
|
|
|
258
295
|
: `version=${resolved.version ?? "missing"}; linked=${String(resolved.linked)}; integrity=${resolved.integrity ? "present" : "missing"}.`, "Run a clean npm install from the exact published release.", "package-lock.json"));
|
|
259
296
|
}
|
|
260
297
|
const expectedIntegrity = options.attestation?.packages[packageName].registry.integrity ?? null;
|
|
261
|
-
if (
|
|
298
|
+
if (options.attestation && !packed && !expectedIntegrity) {
|
|
299
|
+
// A bound manifest without registry integrity cannot prove the lockfile.
|
|
300
|
+
options.findings.push(releaseFinding("brandlab-release-package-integrity-mismatch", `${packageName} integrity matching the immutable manifest`, "The release manifest records no registry integrity for this package.", "Bind the verified release manifest recorded after registry publication.", "package-lock.json"));
|
|
301
|
+
}
|
|
302
|
+
else if (expectedIntegrity && resolved?.integrity !== expectedIntegrity) {
|
|
262
303
|
options.findings.push(releaseFinding("brandlab-release-package-integrity-mismatch", `${packageName} integrity matching the immutable manifest`, "The lockfile integrity differs from the release attestation.", "Regenerate the lockfile from the verified registry package.", "package-lock.json"));
|
|
263
304
|
}
|
|
264
305
|
}
|
|
265
306
|
}
|
|
266
307
|
function inspectRuntime(options) {
|
|
308
|
+
recordEvaluation(options.evaluations, "runtime", ...RUNTIME_PACKAGE_FIELDS.map(([packageName]) => `brandlab-runtime-${slugify(packageName)}`), "brandlab-runtime-react-pair", "brandlab-runtime-node", "brandlab-runtime-package-manager", ...(options.manifest.runtime.sharp ? ["brandlab-runtime-sharp"] : []));
|
|
267
309
|
for (const [packageName, runtimeField] of RUNTIME_PACKAGE_FIELDS) {
|
|
268
310
|
const expected = options.manifest.runtime[runtimeField];
|
|
269
311
|
const declared = declaredPackageSpec(options.packageJson, packageName);
|
|
@@ -276,6 +318,14 @@ function inspectRuntime(options) {
|
|
|
276
318
|
options.findings.push(runtimeFinding(`brandlab-runtime-${slugify(packageName)}`, runtimeExpectation(packageName, expected, options.manifest.runtime.nextVersionPolicy), `package.json=${declared ?? "missing"}; lockfile=${locked ?? "missing"}; resolved=${resolved?.version ?? "missing"}.`, `Use the runtime versions permitted by the ${options.manifest.releaseVersion} contract; an exact mitigation pin requires a reviewed policy change before upgrading.`, "package.json"));
|
|
277
319
|
}
|
|
278
320
|
}
|
|
321
|
+
// React and React DOM are one runtime: a newer patch of one alone is a mismatch.
|
|
322
|
+
const declaredReact = declaredPackageSpec(options.packageJson, "react");
|
|
323
|
+
const declaredReactDom = declaredPackageSpec(options.packageJson, "react-dom");
|
|
324
|
+
const lockedReact = options.packageLock.resolvedPackages.get("react")?.version ?? null;
|
|
325
|
+
const lockedReactDom = options.packageLock.resolvedPackages.get("react-dom")?.version ?? null;
|
|
326
|
+
if (!declaredReact || declaredReact !== declaredReactDom || lockedReact !== lockedReactDom) {
|
|
327
|
+
options.findings.push(runtimeFinding("brandlab-runtime-react-pair", "react and react-dom declared and locked at the same exact version", `package.json react=${declaredReact ?? "missing"}, react-dom=${declaredReactDom ?? "missing"}; lockfile react=${lockedReact ?? "missing"}, react-dom=${lockedReactDom ?? "missing"}.`, "Pin react and react-dom to one exact version and regenerate the lockfile.", "package.json"));
|
|
328
|
+
}
|
|
279
329
|
const node = options.packageJson.engines?.node ?? null;
|
|
280
330
|
const lockNode = options.packageLock.rootEngines.node ?? null;
|
|
281
331
|
if (node !== options.manifest.runtime.node || lockNode !== node) {
|
|
@@ -294,20 +344,22 @@ function inspectRuntime(options) {
|
|
|
294
344
|
}
|
|
295
345
|
}
|
|
296
346
|
}
|
|
297
|
-
function inspectImageRuntime(root, findings) {
|
|
347
|
+
function inspectImageRuntime(root, findings, evaluations) {
|
|
348
|
+
recordEvaluation(evaluations, "runtime", "brandlab-runtime-image-initialisation");
|
|
298
349
|
const filename = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js"]
|
|
299
350
|
.find((name) => existsSync(join(root, name)));
|
|
300
351
|
const source = filename ? stripCommentsPreservingLayout(readOptionalText(join(root, filename)) ?? "") : "";
|
|
301
352
|
// A direct side-effect import executes the shared guard before Next starts.
|
|
302
353
|
// This source check is not runtime certification; packed/browser checks prove that separately.
|
|
303
|
-
if (
|
|
304
|
-
|
|
354
|
+
if (/^[ \t]*import[ \t]*['"]@brandlab-digital\/site-next\/image-runtime['"]/m.test(source) ||
|
|
355
|
+
/^[ \t]*require\([ \t]*['"]@brandlab-digital\/site-next\/image-runtime['"][ \t]*\)/m.test(source))
|
|
305
356
|
return;
|
|
306
357
|
findings.push(runtimeFinding("brandlab-runtime-image-initialisation", "The shared image-runtime side-effect import in next.config", "The startup decoder exclusion could not be established from configuration.", "Import @brandlab-digital/site-next/image-runtime at the top of next.config; verify the packed runtime and affected images.", filename ?? "next.config.ts"));
|
|
307
358
|
}
|
|
308
|
-
function inspectProjectConfig(root, findings) {
|
|
359
|
+
function inspectProjectConfig(root, projectSource, findings, evaluations) {
|
|
309
360
|
const configPath = join(root, "brandlab.config.ts");
|
|
310
361
|
const source = readOptionalText(configPath);
|
|
362
|
+
recordEvaluation(evaluations, "routes", "brandlab-config-missing", "brandlab-config-invalid");
|
|
311
363
|
if (!source) {
|
|
312
364
|
findings.push(staticFinding({
|
|
313
365
|
ruleId: "brandlab-config-missing",
|
|
@@ -319,31 +371,33 @@ function inspectProjectConfig(root, findings) {
|
|
|
319
371
|
}));
|
|
320
372
|
return;
|
|
321
373
|
}
|
|
374
|
+
// 3.0 removed the profile field; a leftover one is reported as a retired API.
|
|
322
375
|
if (!/\bdefineBrandlabProject\s*\(\s*\{/.test(source) ||
|
|
323
|
-
!/\bprojectId\s*:\s*['"][a-z0-9][a-z0-9-]*['"]/.test(source)
|
|
324
|
-
!/\bprofile\s*:\s*['"]next-(?:local-service|root-level)['"]/.test(source)) {
|
|
376
|
+
!/\bprojectId\s*:\s*['"][a-z0-9][a-z0-9-]*['"]/.test(source)) {
|
|
325
377
|
findings.push(staticFinding({
|
|
326
378
|
ruleId: "brandlab-config-invalid",
|
|
327
379
|
surface: "routes",
|
|
328
380
|
path: "brandlab.config.ts",
|
|
329
|
-
expected: "A literal defineBrandlabProject configuration with projectId
|
|
381
|
+
expected: "A literal defineBrandlabProject configuration with projectId",
|
|
330
382
|
actual: "The minimum project identity contract could not be verified.",
|
|
331
383
|
remediation: "Keep the configuration literal and limited to project variation.",
|
|
332
384
|
}));
|
|
333
385
|
}
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
requireEnvKey(findings,
|
|
386
|
+
const envKeys = parseEnvironmentKeys(readOptionalText(join(root, ".env.example")) ?? "");
|
|
387
|
+
const contracts = findContactContractRecipients(projectSource);
|
|
388
|
+
if (/\bcontact\s*:\s*\{/.test(source) || contracts.length > 0) {
|
|
389
|
+
requireEnvKey(findings, evaluations, envKeys, "RESEND_API_KEY", "contact");
|
|
390
|
+
inspectContactRecipients(contracts, envKeys, findings, evaluations);
|
|
338
391
|
}
|
|
339
392
|
const reviewsConfig = source.match(/\breviews\s*:\s*\{([^{}]*)\}/)?.[1] ?? "";
|
|
340
393
|
const usesLiveGooglePlacesReviews = /\bprovider\s*:\s*['"]google-places['"]/.test(reviewsConfig) &&
|
|
341
394
|
/\bmode\s*:\s*['"](?:summary|full)['"]/.test(reviewsConfig);
|
|
342
395
|
if (usesLiveGooglePlacesReviews) {
|
|
343
|
-
requireEnvKey(findings,
|
|
396
|
+
requireEnvKey(findings, evaluations, envKeys, "GOOGLE_MAPS_PLATFORM_API_KEY", "reviews");
|
|
344
397
|
}
|
|
398
|
+
recordEvaluation(evaluations, "environment", "brandlab-environment-retired-contact-state");
|
|
345
399
|
for (const retired of ["CONTACT_CONTEXT_SECRET", "KV_REST_API_URL", "KV_REST_API_TOKEN"]) {
|
|
346
|
-
if (
|
|
400
|
+
if (envKeys.has(retired)) {
|
|
347
401
|
findings.push(staticFinding({
|
|
348
402
|
ruleId: "brandlab-environment-retired-contact-state",
|
|
349
403
|
surface: "environment",
|
|
@@ -355,11 +409,74 @@ function inspectProjectConfig(root, findings) {
|
|
|
355
409
|
}
|
|
356
410
|
}
|
|
357
411
|
}
|
|
358
|
-
|
|
359
|
-
|
|
412
|
+
/** The environment variable key rule site-core applies to delivery.recipientEnv. */
|
|
413
|
+
const ENV_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
414
|
+
/** Variable names documented in an env example file (`KEY=`, `export KEY=`); values are ignored. */
|
|
415
|
+
export function parseEnvironmentKeys(text) {
|
|
416
|
+
const keys = new Set();
|
|
417
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
418
|
+
const line = rawLine.trim();
|
|
419
|
+
if (!line || line.startsWith("#"))
|
|
420
|
+
continue;
|
|
421
|
+
const assignment = line.startsWith("export ") ? line.slice("export ".length).trimStart() : line;
|
|
422
|
+
const separator = assignment.indexOf("=");
|
|
423
|
+
if (separator <= 0)
|
|
424
|
+
continue;
|
|
425
|
+
const key = assignment.slice(0, separator).trimEnd();
|
|
426
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
427
|
+
keys.add(key);
|
|
428
|
+
}
|
|
429
|
+
return keys;
|
|
430
|
+
}
|
|
431
|
+
/** The recipient variable is whatever the contact contract declares, never an assumed name. */
|
|
432
|
+
function inspectContactRecipients(contracts, envKeys, findings, evaluations) {
|
|
433
|
+
recordEvaluation(evaluations, "contact", "brandlab-contact-recipient-env");
|
|
434
|
+
if (contracts.length === 0) {
|
|
435
|
+
findings.push(staticFinding({
|
|
436
|
+
ruleId: "brandlab-contact-recipient-env",
|
|
437
|
+
surface: "contact",
|
|
438
|
+
path: "src/config/contact.ts",
|
|
439
|
+
expected: "One defineContactContract contract declaring delivery.recipientEnv",
|
|
440
|
+
actual: "The contact capability is enabled but no contact contract was found.",
|
|
441
|
+
remediation: "Declare the contact contract with defineContactContract in src/config/contact.ts.",
|
|
442
|
+
}));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
for (const contract of contracts.filter(({ recipientEnv }) => !recipientEnv)) {
|
|
446
|
+
findings.push(staticFinding({
|
|
447
|
+
ruleId: "brandlab-contact-recipient-env",
|
|
448
|
+
surface: "contact",
|
|
449
|
+
path: contract.path,
|
|
450
|
+
line: contract.line,
|
|
451
|
+
expected: "delivery.recipientEnv declared as a literal environment variable key",
|
|
452
|
+
actual: "The recipient environment variable could not be read from the contract.",
|
|
453
|
+
remediation: "Write delivery.recipientEnv as a string literal such as 'CONTACT_EMAIL'.",
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
for (const contract of contracts) {
|
|
457
|
+
if (!contract.recipientEnv || ENV_KEY_PATTERN.test(contract.recipientEnv))
|
|
458
|
+
continue;
|
|
459
|
+
findings.push(staticFinding({
|
|
460
|
+
ruleId: "brandlab-contact-recipient-env",
|
|
461
|
+
surface: "contact",
|
|
462
|
+
path: contract.path,
|
|
463
|
+
line: contract.line,
|
|
464
|
+
expected: "delivery.recipientEnv is an environment variable key (A-Z, 0-9 and _, starting with a letter)",
|
|
465
|
+
actual: `delivery.recipientEnv ${JSON.stringify(contract.recipientEnv.slice(0, 64))} is not an environment variable key.`,
|
|
466
|
+
remediation: "Name the recipient variable in upper case, for example 'CONTACT_EMAIL'.",
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
const keys = [...new Set(contracts.flatMap(({ recipientEnv }) => recipientEnv && ENV_KEY_PATTERN.test(recipientEnv) ? [recipientEnv] : []))];
|
|
470
|
+
for (const key of keys.sort())
|
|
471
|
+
requireEnvKey(findings, evaluations, envKeys, key, "contact");
|
|
472
|
+
}
|
|
473
|
+
function requireEnvKey(findings, evaluations, envKeys, key, capability) {
|
|
474
|
+
const ruleId = `brandlab-environment-${capability}-${slugify(key)}`;
|
|
475
|
+
recordEvaluation(evaluations, "environment", ruleId);
|
|
476
|
+
if (envKeys.has(key))
|
|
360
477
|
return;
|
|
361
478
|
findings.push(staticFinding({
|
|
362
|
-
ruleId
|
|
479
|
+
ruleId,
|
|
363
480
|
surface: "environment",
|
|
364
481
|
path: ".env.example",
|
|
365
482
|
expected: `${key} documented for the enabled ${capability} capability`,
|
|
@@ -367,112 +484,38 @@ function requireEnvKey(findings, envSource, key, capability) {
|
|
|
367
484
|
remediation: `Document the non-secret ${key} variable name without adding a credential value.`,
|
|
368
485
|
}));
|
|
369
486
|
}
|
|
370
|
-
function inspectProjectSource(
|
|
371
|
-
|
|
372
|
-
if (
|
|
487
|
+
function inspectProjectSource(source, findings, evaluations) {
|
|
488
|
+
recordEvaluation(evaluations, "source", "brandlab-source-root-missing");
|
|
489
|
+
if (source.directoryRoots.length === 0) {
|
|
490
|
+
findings.push(staticFinding({
|
|
491
|
+
ruleId: "brandlab-source-root-missing",
|
|
492
|
+
surface: "source",
|
|
493
|
+
expected: `Project source under a recognised root (${BRANDLAB_SOURCE_ROOT_DIRECTORIES.map((name) => `${name}/`).join(", ")})`,
|
|
494
|
+
actual: "No recognised source root was found, so no source rule could run.",
|
|
495
|
+
remediation: "Keep application source under src/ (or a recognised legacy root) and rerun verification.",
|
|
496
|
+
}));
|
|
497
|
+
}
|
|
498
|
+
if (source.files.length === 0)
|
|
373
499
|
return;
|
|
374
|
-
for (const
|
|
375
|
-
|
|
376
|
-
|
|
500
|
+
for (const rule of SOURCE_RULES)
|
|
501
|
+
recordEvaluation(evaluations, rule.surface, rule.ruleId);
|
|
502
|
+
for (const file of source.files) {
|
|
377
503
|
for (const rule of SOURCE_RULES) {
|
|
378
|
-
const match =
|
|
504
|
+
const match = file.executable.match(rule.pattern);
|
|
379
505
|
if (!match || match.index === undefined)
|
|
380
506
|
continue;
|
|
381
507
|
findings.push(staticFinding({
|
|
382
508
|
ruleId: rule.ruleId,
|
|
383
509
|
surface: rule.surface,
|
|
384
|
-
path:
|
|
385
|
-
line: lineAt(
|
|
510
|
+
path: file.path,
|
|
511
|
+
line: lineAt(file.text, match.index),
|
|
386
512
|
expected: rule.expected,
|
|
387
|
-
actual: "A project-owned implementation was detected.",
|
|
513
|
+
actual: rule.actual ?? "A project-owned implementation was detected.",
|
|
388
514
|
remediation: rule.remediation,
|
|
389
515
|
}));
|
|
390
516
|
}
|
|
391
517
|
}
|
|
392
518
|
}
|
|
393
|
-
function stripCommentsPreservingLayout(source) {
|
|
394
|
-
const output = [...source];
|
|
395
|
-
let state = "code";
|
|
396
|
-
let escaped = false;
|
|
397
|
-
for (let index = 0; index < output.length; index += 1) {
|
|
398
|
-
const current = output[index];
|
|
399
|
-
const next = output[index + 1];
|
|
400
|
-
if (state === "line") {
|
|
401
|
-
if (current === "\n" || current === "\r") {
|
|
402
|
-
state = "code";
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
output[index] = " ";
|
|
406
|
-
}
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
if (state === "block") {
|
|
410
|
-
if (current === "*" && next === "/") {
|
|
411
|
-
output[index] = " ";
|
|
412
|
-
output[index + 1] = " ";
|
|
413
|
-
index += 1;
|
|
414
|
-
state = "code";
|
|
415
|
-
}
|
|
416
|
-
else if (current !== "\n" && current !== "\r") {
|
|
417
|
-
output[index] = " ";
|
|
418
|
-
}
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (state !== "code") {
|
|
422
|
-
if (escaped) {
|
|
423
|
-
escaped = false;
|
|
424
|
-
}
|
|
425
|
-
else if (current === "\\") {
|
|
426
|
-
escaped = true;
|
|
427
|
-
}
|
|
428
|
-
else if ((state === "single" && current === "'") ||
|
|
429
|
-
(state === "double" && current === '"') ||
|
|
430
|
-
(state === "template" && current === "`")) {
|
|
431
|
-
state = "code";
|
|
432
|
-
}
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
if (current === "'") {
|
|
436
|
-
state = "single";
|
|
437
|
-
}
|
|
438
|
-
else if (current === '"') {
|
|
439
|
-
state = "double";
|
|
440
|
-
}
|
|
441
|
-
else if (current === "`") {
|
|
442
|
-
state = "template";
|
|
443
|
-
}
|
|
444
|
-
else if (current === "/" && next === "/") {
|
|
445
|
-
output[index] = " ";
|
|
446
|
-
output[index + 1] = " ";
|
|
447
|
-
index += 1;
|
|
448
|
-
state = "line";
|
|
449
|
-
}
|
|
450
|
-
else if (current === "/" && next === "*") {
|
|
451
|
-
output[index] = " ";
|
|
452
|
-
output[index + 1] = " ";
|
|
453
|
-
index += 1;
|
|
454
|
-
state = "block";
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return output.join("");
|
|
458
|
-
}
|
|
459
|
-
function sourceFiles(root) {
|
|
460
|
-
const files = [];
|
|
461
|
-
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
462
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
463
|
-
continue;
|
|
464
|
-
const path = join(root, entry.name);
|
|
465
|
-
if (entry.isDirectory()) {
|
|
466
|
-
files.push(...sourceFiles(path));
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
if (SOURCE_EXTENSIONS.has(extension(entry.name)) &&
|
|
470
|
-
!/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
471
|
-
files.push(path);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
return files.sort();
|
|
475
|
-
}
|
|
476
519
|
function readPackageJson(root) {
|
|
477
520
|
const source = readOptionalText(join(root, "package.json"));
|
|
478
521
|
if (!source)
|
|
@@ -641,28 +684,11 @@ function runtimeFinding(ruleId, expected, actual, remediation, path) {
|
|
|
641
684
|
remediation,
|
|
642
685
|
};
|
|
643
686
|
}
|
|
644
|
-
function staticFinding(options) {
|
|
645
|
-
return {
|
|
646
|
-
ruleId: options.ruleId,
|
|
647
|
-
surface: options.surface,
|
|
648
|
-
severity: "blocker",
|
|
649
|
-
contract: { name: "project", version: "current-only-v2" },
|
|
650
|
-
evidenceKind: "static",
|
|
651
|
-
...(options.path ? { path: options.path } : {}),
|
|
652
|
-
...(options.line ? { line: options.line } : {}),
|
|
653
|
-
expected: options.expected,
|
|
654
|
-
actual: options.actual,
|
|
655
|
-
remediation: options.remediation,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
687
|
function compareFindings(left, right) {
|
|
659
688
|
return (left.ruleId.localeCompare(right.ruleId) ||
|
|
660
689
|
(left.path ?? "").localeCompare(right.path ?? "") ||
|
|
661
690
|
(left.line ?? 0) - (right.line ?? 0));
|
|
662
691
|
}
|
|
663
|
-
function readOptionalText(path) {
|
|
664
|
-
return existsSync(path) ? readFileSync(path, "utf8") : null;
|
|
665
|
-
}
|
|
666
692
|
function stringRecord(value) {
|
|
667
693
|
if (!isRecord(value))
|
|
668
694
|
return {};
|
|
@@ -671,17 +697,7 @@ function stringRecord(value) {
|
|
|
671
697
|
function isRecord(value) {
|
|
672
698
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
673
699
|
}
|
|
674
|
-
function extension(name) {
|
|
675
|
-
const index = name.lastIndexOf(".");
|
|
676
|
-
return index === -1 ? "" : name.slice(index);
|
|
677
|
-
}
|
|
678
|
-
function lineAt(source, index) {
|
|
679
|
-
return source.slice(0, index).split("\n").length;
|
|
680
|
-
}
|
|
681
700
|
function slugify(value) {
|
|
682
701
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
683
702
|
}
|
|
684
|
-
function normalizePath(value) {
|
|
685
|
-
return value.split(/[/\\]/).join("/");
|
|
686
|
-
}
|
|
687
703
|
//# sourceMappingURL=project.js.map
|