@decantr/verifier 1.0.5 → 1.1.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/LICENSE +21 -0
- package/README.md +8 -2
- package/dist/index.d.ts +66 -1
- package/dist/index.js +229 -31
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
- package/schema/project-health-report.v1.json +257 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Decantr AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -16,7 +16,8 @@ npm install @decantr/verifier
|
|
|
16
16
|
- `auditProject()` for project-level Decantr audits
|
|
17
17
|
- `auditBuiltDist()` for built-output runtime verification against emitted HTML, assets, and route hints
|
|
18
18
|
- `critiqueFile()` for file-level review against compiled review-pack contracts
|
|
19
|
-
- schema-backed report types for project audits, file critiques, and showcase verification
|
|
19
|
+
- schema-backed report types for project audits, Project Health, file critiques, and showcase verification
|
|
20
|
+
- `ProjectHealthReport`, `ProjectHealthFinding`, and `ProjectHealthRemediation` types for the CLI's end-user health surface
|
|
20
21
|
- published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
|
|
21
22
|
- project audits include runtime evidence when a built `dist/` output is present:
|
|
22
23
|
- root document
|
|
@@ -30,16 +31,21 @@ npm install @decantr/verifier
|
|
|
30
31
|
## Example
|
|
31
32
|
|
|
32
33
|
```ts
|
|
33
|
-
import { auditProject, critiqueFile } from '@decantr/verifier';
|
|
34
|
+
import { auditProject, critiqueFile, type ProjectHealthReport } from '@decantr/verifier';
|
|
34
35
|
|
|
35
36
|
const audit = await auditProject(process.cwd());
|
|
36
37
|
const critique = await critiqueFile('./src/pages/overview.tsx', process.cwd());
|
|
38
|
+
|
|
39
|
+
function isBlocking(report: ProjectHealthReport) {
|
|
40
|
+
return report.status === 'error';
|
|
41
|
+
}
|
|
37
42
|
```
|
|
38
43
|
|
|
39
44
|
## Schema Exports
|
|
40
45
|
|
|
41
46
|
- `@decantr/verifier/schema/verification-report.common.v1.json`
|
|
42
47
|
- `@decantr/verifier/schema/project-audit-report.v1.json`
|
|
48
|
+
- `@decantr/verifier/schema/project-health-report.v1.json`
|
|
43
49
|
- `@decantr/verifier/schema/file-critique-report.v1.json`
|
|
44
50
|
- `@decantr/verifier/schema/showcase-shortlist-report.v1.json`
|
|
45
51
|
|
package/dist/index.d.ts
CHANGED
|
@@ -118,10 +118,13 @@ declare function listKnownInteractions(): string[];
|
|
|
118
118
|
declare const VERIFICATION_SCHEMA_URLS: {
|
|
119
119
|
readonly common: "https://decantr.ai/schemas/verification-report.common.v1.json";
|
|
120
120
|
readonly projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json";
|
|
121
|
+
readonly projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json";
|
|
121
122
|
readonly fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json";
|
|
122
123
|
readonly showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json";
|
|
123
124
|
};
|
|
124
125
|
type VerificationSeverity = 'error' | 'warn' | 'info';
|
|
126
|
+
type ProjectHealthStatus = 'healthy' | 'warning' | 'error';
|
|
127
|
+
type ProjectHealthFindingSource = 'audit' | 'check' | 'brownfield' | 'runtime' | 'pack' | 'interaction';
|
|
125
128
|
interface VerificationFinding {
|
|
126
129
|
id: string;
|
|
127
130
|
category: string;
|
|
@@ -183,6 +186,68 @@ interface ProjectAuditReport {
|
|
|
183
186
|
pageCount: number;
|
|
184
187
|
};
|
|
185
188
|
}
|
|
189
|
+
interface ProjectHealthRemediation {
|
|
190
|
+
summary: string;
|
|
191
|
+
prompt: string;
|
|
192
|
+
commands: string[];
|
|
193
|
+
}
|
|
194
|
+
interface ProjectHealthFinding {
|
|
195
|
+
id: string;
|
|
196
|
+
source: ProjectHealthFindingSource;
|
|
197
|
+
category: string;
|
|
198
|
+
severity: VerificationSeverity;
|
|
199
|
+
message: string;
|
|
200
|
+
evidence: string[];
|
|
201
|
+
target?: string;
|
|
202
|
+
file?: string;
|
|
203
|
+
rule?: string;
|
|
204
|
+
suggestedFix?: string;
|
|
205
|
+
remediation: ProjectHealthRemediation;
|
|
206
|
+
}
|
|
207
|
+
interface ProjectHealthRouteSummary {
|
|
208
|
+
declared: string[];
|
|
209
|
+
runtimeChecked: string[];
|
|
210
|
+
runtimeMatched: number;
|
|
211
|
+
runtimeCoverageOk: boolean | null;
|
|
212
|
+
issues: string[];
|
|
213
|
+
}
|
|
214
|
+
interface ProjectHealthPackSummary {
|
|
215
|
+
manifestPresent: boolean;
|
|
216
|
+
reviewPackPresent: boolean;
|
|
217
|
+
scaffoldPackPresent: boolean;
|
|
218
|
+
sectionPackCount: number;
|
|
219
|
+
pagePackCount: number;
|
|
220
|
+
mutationPackCount: number;
|
|
221
|
+
generatedAt: string | null;
|
|
222
|
+
}
|
|
223
|
+
interface ProjectHealthReport {
|
|
224
|
+
$schema: string;
|
|
225
|
+
generatedAt: string;
|
|
226
|
+
projectRoot: string;
|
|
227
|
+
status: ProjectHealthStatus;
|
|
228
|
+
score: number;
|
|
229
|
+
summary: {
|
|
230
|
+
errorCount: number;
|
|
231
|
+
warnCount: number;
|
|
232
|
+
infoCount: number;
|
|
233
|
+
findingCount: number;
|
|
234
|
+
workflowMode: string | null;
|
|
235
|
+
adoptionMode: string | null;
|
|
236
|
+
essenceVersion: string | null;
|
|
237
|
+
pageCount: number;
|
|
238
|
+
runtimeAuditChecked: boolean;
|
|
239
|
+
runtimePassed: boolean | null;
|
|
240
|
+
packManifestPresent: boolean;
|
|
241
|
+
reviewPackPresent: boolean;
|
|
242
|
+
};
|
|
243
|
+
routes: ProjectHealthRouteSummary;
|
|
244
|
+
packs: ProjectHealthPackSummary;
|
|
245
|
+
ci: {
|
|
246
|
+
recommendedCommand: string;
|
|
247
|
+
failOn: 'error' | 'warn' | 'none';
|
|
248
|
+
};
|
|
249
|
+
findings: ProjectHealthFinding[];
|
|
250
|
+
}
|
|
186
251
|
interface FileCritiqueReport {
|
|
187
252
|
$schema: string;
|
|
188
253
|
file: string;
|
|
@@ -297,4 +362,4 @@ declare function auditProject(projectRoot: string): Promise<ProjectAuditReport>;
|
|
|
297
362
|
declare function critiqueSource({ filePath, code, reviewPack, packManifest, treatmentsCss, }: CritiqueSourceInput): FileCritiqueReport;
|
|
298
363
|
declare function critiqueFile(filePath: string, projectRoot: string): Promise<FileCritiqueReport>;
|
|
299
364
|
|
|
300
|
-
export { type BuiltDistAuditOptions, type CritiqueSourceInput, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
|
|
365
|
+
export { type BuiltDistAuditOptions, type CritiqueSourceInput, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type ProjectHealthFinding, type ProjectHealthFindingSource, type ProjectHealthPackSummary, type ProjectHealthRemediation, type ProjectHealthReport, type ProjectHealthRouteSummary, type ProjectHealthStatus, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
|
package/dist/index.js
CHANGED
|
@@ -610,6 +610,7 @@ function listKnownInteractions() {
|
|
|
610
610
|
var VERIFICATION_SCHEMA_URLS = {
|
|
611
611
|
common: "https://decantr.ai/schemas/verification-report.common.v1.json",
|
|
612
612
|
projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
|
|
613
|
+
projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
|
|
613
614
|
fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
|
|
614
615
|
showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
|
|
615
616
|
};
|
|
@@ -3312,8 +3313,30 @@ async function auditProject(projectRoot) {
|
|
|
3312
3313
|
};
|
|
3313
3314
|
}
|
|
3314
3315
|
function buildDecoratorInventory(treatmentsCss) {
|
|
3315
|
-
const
|
|
3316
|
-
|
|
3316
|
+
const decoratorNames = /* @__PURE__ */ new Set();
|
|
3317
|
+
for (let index = treatmentsCss.indexOf("."); index !== -1; index = treatmentsCss.indexOf(".", index + 1)) {
|
|
3318
|
+
let cursor = index + 1;
|
|
3319
|
+
while (cursor < treatmentsCss.length && isCssClassNameChar(treatmentsCss[cursor])) {
|
|
3320
|
+
cursor += 1;
|
|
3321
|
+
}
|
|
3322
|
+
if (cursor === index + 1) continue;
|
|
3323
|
+
const name = treatmentsCss.slice(index + 1, cursor);
|
|
3324
|
+
let whitespaceCursor = cursor;
|
|
3325
|
+
while (whitespaceCursor < treatmentsCss.length && whitespaceCursor - cursor <= 64 && isCssWhitespace(treatmentsCss[whitespaceCursor])) {
|
|
3326
|
+
whitespaceCursor += 1;
|
|
3327
|
+
}
|
|
3328
|
+
if (treatmentsCss[whitespaceCursor] === "{") {
|
|
3329
|
+
decoratorNames.add(name);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
return [...decoratorNames].filter((name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name));
|
|
3333
|
+
}
|
|
3334
|
+
function isCssClassNameChar(char) {
|
|
3335
|
+
const codePoint = char.charCodeAt(0);
|
|
3336
|
+
return char === "_" || char === "-" || codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 90 || codePoint >= 97 && codePoint <= 122;
|
|
3337
|
+
}
|
|
3338
|
+
function isCssWhitespace(char) {
|
|
3339
|
+
return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
|
|
3317
3340
|
}
|
|
3318
3341
|
function resolveFocusAreas(reviewPack) {
|
|
3319
3342
|
return reviewPack?.data.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
|
|
@@ -3323,9 +3346,49 @@ function resolveSeverityFromChecks(reviewPack, fallback, checkIds) {
|
|
|
3323
3346
|
return match?.severity ?? fallback;
|
|
3324
3347
|
}
|
|
3325
3348
|
function findHardcodedColors(code) {
|
|
3326
|
-
const matches =
|
|
3349
|
+
const matches = [
|
|
3350
|
+
...findHexColors(code),
|
|
3351
|
+
...findColorFunctionCalls(code, "rgb("),
|
|
3352
|
+
...findColorFunctionCalls(code, "rgba("),
|
|
3353
|
+
...findColorFunctionCalls(code, "hsl("),
|
|
3354
|
+
...findColorFunctionCalls(code, "hsla(")
|
|
3355
|
+
];
|
|
3327
3356
|
return [...new Set(matches)];
|
|
3328
3357
|
}
|
|
3358
|
+
function findHexColors(code) {
|
|
3359
|
+
const matches = [];
|
|
3360
|
+
for (let index = 0; index < code.length; index += 1) {
|
|
3361
|
+
if (code[index] !== "#") continue;
|
|
3362
|
+
let cursor = index + 1;
|
|
3363
|
+
while (cursor < code.length && cursor - index <= 8 && isHexDigit(code[cursor])) {
|
|
3364
|
+
cursor += 1;
|
|
3365
|
+
}
|
|
3366
|
+
const length = cursor - index - 1;
|
|
3367
|
+
if (length >= 3 && length <= 8 && !isHexDigit(code[cursor] ?? "")) {
|
|
3368
|
+
matches.push(code.slice(index, cursor));
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
return matches;
|
|
3372
|
+
}
|
|
3373
|
+
function findColorFunctionCalls(code, functionName) {
|
|
3374
|
+
const matches = [];
|
|
3375
|
+
const lowerCode = code.toLowerCase();
|
|
3376
|
+
for (let index = lowerCode.indexOf(functionName); index !== -1; index = lowerCode.indexOf(functionName, index + functionName.length)) {
|
|
3377
|
+
let cursor = index + functionName.length;
|
|
3378
|
+
const maxCursor = Math.min(code.length, cursor + 256);
|
|
3379
|
+
while (cursor < maxCursor && code[cursor] !== ")" && code[cursor] !== "\n" && code[cursor] !== "\r") {
|
|
3380
|
+
cursor += 1;
|
|
3381
|
+
}
|
|
3382
|
+
if (code[cursor] === ")") {
|
|
3383
|
+
matches.push(code.slice(index, cursor + 1));
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return matches;
|
|
3387
|
+
}
|
|
3388
|
+
function isHexDigit(char) {
|
|
3389
|
+
const codePoint = char.charCodeAt(0);
|
|
3390
|
+
return codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 70 || codePoint >= 97 && codePoint <= 102;
|
|
3391
|
+
}
|
|
3329
3392
|
function findUtilityFrameworkSignals(code) {
|
|
3330
3393
|
const matches = code.match(
|
|
3331
3394
|
/\b(?:bg|text|border|shadow|rounded|px|py|mx|my|gap|grid-cols|col-span|row-span|sm|md|lg|xl|hover):[-\w/.[\]]+/g
|
|
@@ -4789,18 +4852,10 @@ function countAuthCallbackStateValidationSignals(code) {
|
|
|
4789
4852
|
if (/\b(?:validateState|verifyState|assertState|compareState|checkState)\s*\(/i.test(code)) {
|
|
4790
4853
|
count += 1;
|
|
4791
4854
|
}
|
|
4792
|
-
if (
|
|
4793
|
-
code
|
|
4794
|
-
) && /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
|
|
4795
|
-
code
|
|
4796
|
-
)) {
|
|
4855
|
+
if (countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["getItem"]) > 0 && hasStateComparison(code)) {
|
|
4797
4856
|
count += 1;
|
|
4798
4857
|
}
|
|
4799
|
-
if (
|
|
4800
|
-
code
|
|
4801
|
-
) && /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
|
|
4802
|
-
code
|
|
4803
|
-
)) {
|
|
4858
|
+
if (countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["get", "getAll"]) > 0 && hasStateComparison(code)) {
|
|
4804
4859
|
count += 1;
|
|
4805
4860
|
}
|
|
4806
4861
|
if (/\b(?:state|providerState|callbackState|returnedState)\b\s*(?:===|==|!==|!=)\s*\b(?:expectedState|storedState|sessionState|oauthState|csrfState)\b/i.test(
|
|
@@ -4812,21 +4867,142 @@ function countAuthCallbackStateValidationSignals(code) {
|
|
|
4812
4867
|
}
|
|
4813
4868
|
return count;
|
|
4814
4869
|
}
|
|
4870
|
+
function hasStateComparison(code) {
|
|
4871
|
+
return /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
|
|
4872
|
+
code
|
|
4873
|
+
);
|
|
4874
|
+
}
|
|
4875
|
+
function countStateLikeMemberCalls(code, receivers, methods) {
|
|
4876
|
+
const lowerCode = code.toLowerCase();
|
|
4877
|
+
let count = 0;
|
|
4878
|
+
for (const receiver of receivers) {
|
|
4879
|
+
const receiverLower = receiver.toLowerCase();
|
|
4880
|
+
for (let index = lowerCode.indexOf(receiverLower); index !== -1; index = lowerCode.indexOf(receiverLower, index + receiverLower.length)) {
|
|
4881
|
+
let cursor = index + receiverLower.length;
|
|
4882
|
+
cursor = skipInlineWhitespace(lowerCode, cursor);
|
|
4883
|
+
if (lowerCode[cursor] !== ".") continue;
|
|
4884
|
+
cursor = skipInlineWhitespace(lowerCode, cursor + 1);
|
|
4885
|
+
for (const method of methods) {
|
|
4886
|
+
const methodLower = method.toLowerCase();
|
|
4887
|
+
if (!lowerCode.startsWith(methodLower, cursor)) continue;
|
|
4888
|
+
const afterMethod = skipInlineWhitespace(lowerCode, cursor + methodLower.length);
|
|
4889
|
+
if (lowerCode[afterMethod] !== "(") continue;
|
|
4890
|
+
if (callHasStateLikeStringArgument(code, afterMethod)) {
|
|
4891
|
+
count += 1;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
return count;
|
|
4897
|
+
}
|
|
4898
|
+
function countStateLikeFunctionCalls(code, names) {
|
|
4899
|
+
const lowerCode = code.toLowerCase();
|
|
4900
|
+
let count = 0;
|
|
4901
|
+
for (const name of names) {
|
|
4902
|
+
const nameLower = name.toLowerCase();
|
|
4903
|
+
for (let index = lowerCode.indexOf(nameLower); index !== -1; index = lowerCode.indexOf(nameLower, index + nameLower.length)) {
|
|
4904
|
+
const afterName = skipInlineWhitespace(lowerCode, index + nameLower.length);
|
|
4905
|
+
if (lowerCode[afterName] === "(" && callHasStateLikeStringArgument(code, afterName)) {
|
|
4906
|
+
count += 1;
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
return count;
|
|
4911
|
+
}
|
|
4912
|
+
function skipInlineWhitespace(value, index) {
|
|
4913
|
+
let cursor = index;
|
|
4914
|
+
while (cursor < value.length) {
|
|
4915
|
+
const char = value[cursor];
|
|
4916
|
+
if (char !== " " && char !== " " && char !== "\n" && char !== "\r") break;
|
|
4917
|
+
cursor += 1;
|
|
4918
|
+
}
|
|
4919
|
+
return cursor;
|
|
4920
|
+
}
|
|
4921
|
+
function callHasStateLikeStringArgument(code, openParenIndex) {
|
|
4922
|
+
const closeParenIndex = findCallCloseParen(code, openParenIndex, 240);
|
|
4923
|
+
if (closeParenIndex === -1) return false;
|
|
4924
|
+
return containsStateLikeStringLiteral(code.slice(openParenIndex + 1, closeParenIndex));
|
|
4925
|
+
}
|
|
4926
|
+
function findCallCloseParen(code, openParenIndex, maxLength) {
|
|
4927
|
+
const maxIndex = Math.min(code.length, openParenIndex + maxLength);
|
|
4928
|
+
let quote = null;
|
|
4929
|
+
let escaped = false;
|
|
4930
|
+
for (let index = openParenIndex + 1; index < maxIndex; index += 1) {
|
|
4931
|
+
const char = code[index];
|
|
4932
|
+
if (quote) {
|
|
4933
|
+
if (escaped) {
|
|
4934
|
+
escaped = false;
|
|
4935
|
+
} else if (char === "\\") {
|
|
4936
|
+
escaped = true;
|
|
4937
|
+
} else if (char === quote) {
|
|
4938
|
+
quote = null;
|
|
4939
|
+
}
|
|
4940
|
+
continue;
|
|
4941
|
+
}
|
|
4942
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
4943
|
+
quote = char;
|
|
4944
|
+
} else if (char === ")") {
|
|
4945
|
+
return index;
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
return -1;
|
|
4949
|
+
}
|
|
4950
|
+
function containsStateLikeStringLiteral(value) {
|
|
4951
|
+
let quote = null;
|
|
4952
|
+
let literal = "";
|
|
4953
|
+
let escaped = false;
|
|
4954
|
+
for (const char of value) {
|
|
4955
|
+
if (!quote) {
|
|
4956
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
4957
|
+
quote = char;
|
|
4958
|
+
literal = "";
|
|
4959
|
+
}
|
|
4960
|
+
continue;
|
|
4961
|
+
}
|
|
4962
|
+
if (escaped) {
|
|
4963
|
+
literal += char;
|
|
4964
|
+
escaped = false;
|
|
4965
|
+
} else if (char === "\\") {
|
|
4966
|
+
escaped = true;
|
|
4967
|
+
} else if (char === quote) {
|
|
4968
|
+
const lowerLiteral = literal.toLowerCase();
|
|
4969
|
+
if (lowerLiteral.includes("state") || lowerLiteral.includes("csrf")) return true;
|
|
4970
|
+
quote = null;
|
|
4971
|
+
} else {
|
|
4972
|
+
literal += char;
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
return false;
|
|
4976
|
+
}
|
|
4815
4977
|
function countAuthCallbackStateStorageSignals(code) {
|
|
4816
|
-
|
|
4817
|
-
/\b(?:sessionStorage|localStorage)\.getItem\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi,
|
|
4818
|
-
/\b(?:cookies?|cookieStore)\.(?:get|getAll)\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi
|
|
4819
|
-
];
|
|
4820
|
-
return patterns.reduce((total, pattern) => total + (code.match(pattern)?.length ?? 0), 0);
|
|
4978
|
+
return countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["getItem"]) + countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["get", "getAll"]);
|
|
4821
4979
|
}
|
|
4822
4980
|
function countAuthCallbackStateStorageClearSignals(code) {
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4981
|
+
return countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["removeItem"]) + countStateLikeFunctionCalls(code, ["deleteCookie", "clearCookie", "removeCookie"]) + countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["delete", "remove"]) + countDocumentCookieStateClears(code);
|
|
4982
|
+
}
|
|
4983
|
+
function countDocumentCookieStateClears(code) {
|
|
4984
|
+
const lowerCode = code.toLowerCase();
|
|
4985
|
+
let count = 0;
|
|
4986
|
+
for (let index = lowerCode.indexOf("document.cookie"); index !== -1; index = lowerCode.indexOf("document.cookie", index + "document.cookie".length)) {
|
|
4987
|
+
const assignmentIndex = lowerCode.indexOf("=", index + "document.cookie".length);
|
|
4988
|
+
if (assignmentIndex === -1 || assignmentIndex - index > 48) continue;
|
|
4989
|
+
const endIndex = findCookieAssignmentEnd(lowerCode, assignmentIndex + 1);
|
|
4990
|
+
const assignment = lowerCode.slice(assignmentIndex + 1, endIndex);
|
|
4991
|
+
const mentionsStateToken = assignment.includes("state") || assignment.includes("csrf");
|
|
4992
|
+
const expiresImmediately = assignment.includes("max-age=0") || assignment.includes("max-age = 0") || assignment.includes("thu, 01 jan 1970");
|
|
4993
|
+
if (mentionsStateToken && expiresImmediately) {
|
|
4994
|
+
count += 1;
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
return count;
|
|
4998
|
+
}
|
|
4999
|
+
function findCookieAssignmentEnd(code, startIndex) {
|
|
5000
|
+
const maxIndex = Math.min(code.length, startIndex + 320);
|
|
5001
|
+
for (let index = startIndex; index < maxIndex; index += 1) {
|
|
5002
|
+
const char = code[index];
|
|
5003
|
+
if (char === "\n" || char === "\r") return index;
|
|
5004
|
+
}
|
|
5005
|
+
return maxIndex;
|
|
4830
5006
|
}
|
|
4831
5007
|
function countAuthCallbackUrlScrubSignals(code) {
|
|
4832
5008
|
const patterns = [
|
|
@@ -8731,12 +8907,34 @@ function countHardcodedSecretSignals(code) {
|
|
|
8731
8907
|
return patterns.reduce((count, pattern) => count + (code.match(pattern)?.length ?? 0), 0);
|
|
8732
8908
|
}
|
|
8733
8909
|
function countClientSecretEnvReferenceSignals(code) {
|
|
8734
|
-
const
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8910
|
+
const useClientCount = /^\s*['"]use client['"]/m.test(code) ? countSensitiveEnvReferences(code, ["process.env."]) : 0;
|
|
8911
|
+
return countSensitiveEnvReferences(code, ["import.meta.env.", "process.env.NEXT_PUBLIC_"]) + useClientCount;
|
|
8912
|
+
}
|
|
8913
|
+
function countSensitiveEnvReferences(code, prefixes) {
|
|
8914
|
+
let count = 0;
|
|
8915
|
+
for (const prefix of prefixes) {
|
|
8916
|
+
for (let index = code.indexOf(prefix); index !== -1; index = code.indexOf(prefix, index + prefix.length)) {
|
|
8917
|
+
const envName = readEnvIdentifier(code, index + prefix.length);
|
|
8918
|
+
if (envName && isSensitiveEnvName(envName)) {
|
|
8919
|
+
count += 1;
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
return count;
|
|
8924
|
+
}
|
|
8925
|
+
function readEnvIdentifier(code, startIndex) {
|
|
8926
|
+
let cursor = startIndex;
|
|
8927
|
+
while (cursor < code.length && isEnvIdentifierChar(code[cursor])) {
|
|
8928
|
+
cursor += 1;
|
|
8929
|
+
}
|
|
8930
|
+
return code.slice(startIndex, cursor);
|
|
8931
|
+
}
|
|
8932
|
+
function isEnvIdentifierChar(char) {
|
|
8933
|
+
const codePoint = char.charCodeAt(0);
|
|
8934
|
+
return char === "_" || codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 90;
|
|
8935
|
+
}
|
|
8936
|
+
function isSensitiveEnvName(envName) {
|
|
8937
|
+
return envName.includes("SERVICE_ROLE") || envName.includes("SECRET") || envName.includes("PRIVATE_KEY");
|
|
8740
8938
|
}
|
|
8741
8939
|
function countLocalhostEndpointSignals(code) {
|
|
8742
8940
|
return code.match(
|