srclawyer 0.5.0 → 0.6.1
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/README.md +28 -6
- package/dist/cli.js +45 -3
- package/dist/cloud/protectedLogicClient.d.ts +3 -0
- package/dist/cloud/protectedLogicClient.js +62 -20
- package/dist/cloud/resolveProtectedLogic.d.ts +1 -1
- package/dist/cloud/resolveProtectedLogic.js +15 -1
- package/dist/commands/init.d.ts +16 -1
- package/dist/commands/init.js +107 -15
- package/dist/commands/scan.js +19 -4
- package/dist/config/config.d.ts +12 -1
- package/dist/config/config.js +12 -1
- package/dist/engine/discoverFiles.d.ts +1 -0
- package/dist/engine/discoverFiles.js +16 -2
- package/dist/engine/languageDetection.js +37 -7
- package/dist/engine/pythonAstUtils.d.ts +2 -0
- package/dist/engine/pythonAstUtils.js +44 -0
- package/dist/engine/scanEngine.js +42 -5
- package/dist/engine/types.d.ts +9 -0
- package/dist/grammars/LICENSE +21 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/policy/generatePolicy.d.ts +1 -1
- package/dist/policy/generatePolicy.js +127 -3
- package/dist/policy/legalNotices.d.ts +5 -0
- package/dist/policy/legalNotices.js +29 -0
- package/dist/report/formatText.js +1 -1
- package/dist/rules/dataCategoryLexicon.js +9 -3
- package/dist/rules/frameworkRules.js +89 -12
- package/dist/rules/pythonFrameworkRules.d.ts +3 -0
- package/dist/rules/pythonFrameworkRules.js +136 -0
- package/dist/rules/pythonSdkRules.d.ts +3 -0
- package/dist/rules/pythonSdkRules.js +73 -0
- package/dist/rules/safeFieldNames.js +14 -0
- package/dist/rules/zodPreFilter.d.ts +21 -1
- package/dist/rules/zodPreFilter.js +104 -13
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Static analysis engine that derives privacy policy documentation directly from source code, and keeps it in sync as the code changes.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> **Beta.** The CLI (`init`, `scan`), the Layer 1 static analysis engine, and markdown privacy policy generation all work today. JavaScript/TypeScript (Express, Next.js) is the most tested path; Python (Flask) is newer, with FastAPI and Django explicitly detected and flagged as unsupported rather than silently skipped — see [Supported languages and frameworks](#supported-languages-and-frameworks) and [Known gaps](#known-gaps) below. LLM-assisted reasoning for ambiguous findings (Layer 2) is available now under the bring-your-own-key tier, documented below as one optional step in `init` — a managed (hosted) tier is planned but not yet available.
|
|
6
6
|
>
|
|
7
|
-
> Scanning is manual-only (`srclawyer scan`, run on demand) — this is the free/solo tier's permanent behavior, not a placeholder. CI/CD
|
|
7
|
+
> Scanning is manual-only (`srclawyer scan`, run on demand) — this is the free/solo tier's permanent behavior, not a placeholder. Automatic CI/CD updating (a scan that runs itself on every PR, or on a schedule) isn't available yet; it's scoped for a future paid/team tier. See `PRODUCT_SPEC.md` Section 8.5 for the plan.
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
10
10
|
|
|
@@ -63,18 +63,34 @@ If none of this resolves it, `npx srclawyer scan` (see [Usage](#usage) above) si
|
|
|
63
63
|
|
|
64
64
|
## What `scan` does today
|
|
65
65
|
|
|
66
|
-
- **Known SDK integrations** (import/require detection): Stripe, SendGrid, Twilio, Google Analytics, Mixpanel, Auth0, Firebase, Cognito — each mapped to the data categories it typically processes (`src/rules/processorProfiles.ts`).
|
|
67
|
-
- **Request-body field collection** in Express (`req.body.x`, destructured `req.body`) and Next.js (both `pages/api` and the app router's `await request.json()`), classified against a field-name lexicon (`src/rules/dataCategoryLexicon.ts`).
|
|
66
|
+
- **Known SDK integrations** (import/require detection): Stripe, SendGrid, Twilio, Google Analytics, Mixpanel, Auth0, Firebase, Cognito for JS/TS; Stripe, SendGrid, Twilio, Mixpanel, Auth0, Firebase for Python — each mapped to the data categories it typically processes (`src/rules/processorProfiles.ts`).
|
|
67
|
+
- **Request-body field collection** in Express (`req.body.x`, destructured `req.body`) and Next.js (both `pages/api` and the app router's `await request.json()`), and in **Flask** (`request.form`/`.json`/`.args`, both subscript and `.get(...)` forms), classified against a field-name lexicon (`src/rules/dataCategoryLexicon.ts`). In both languages, a value assigned to a local variable earlier in the *same function* (`const body = req.body` / `data = request.get_json()`) is tracked one mechanical hop, so a later `body.email` / `data['email']` is still caught — not just the direct, unaliased form.
|
|
68
68
|
- **OpenAPI/Swagger schemas** (`components.schemas.*.properties`) and **GraphQL SDL** type/input fields.
|
|
69
69
|
- **HTML forms and inline scripts** (`src/rules/htmlRules.ts`): native `<input>` elements classified by `type` (password/email/tel are unambiguous native signals) or by `id`/`name` against the lexicon; inline `<script>` blocks are extracted and run through the same SDK/framework/web-API rules as any other file.
|
|
70
70
|
- **Browser platform APIs** (`src/rules/webApiRules.ts`): `getUserMedia` (split into microphone and camera access), geolocation, and `localStorage`/`sessionStorage` writes classified by key name.
|
|
71
71
|
- Fields that can't be confidently classified (generic names like `data`, `payload`) are surfaced with `requiresReview: true` rather than silently guessed.
|
|
72
|
-
- **A field-name allowlist** (`src/rules/safeFieldNames.ts`) suppresses findings entirely for exact-match structural fields (`id`, `createdAt`, `status`,
|
|
73
|
-
- **Unsupported language/framework detection** (`src/engine/languageDetection.ts`) runs at the start of every scan: it checks for Django (`manage.py`), Rails (`Gemfile` + `app/controllers`), gRPC (`.proto` files), and falls back to a file-count ratio for
|
|
72
|
+
- **A field-name allowlist** (`src/rules/safeFieldNames.ts`) suppresses findings entirely for exact-match structural and pagination/sort fields (`id`, `createdAt`, `status`, `per_page`, `cursor`, `sort_by`, etc.) so "needs review" stays a signal, not noise from routine boilerplate. Anything not on the allowlist keeps the existing classify-or-flag behavior above.
|
|
73
|
+
- **Unsupported language/framework detection** (`src/engine/languageDetection.ts`) runs at the start of every scan: it checks for Django (`manage.py`), FastAPI (`from fastapi import` / `import fastapi`), Rails (`Gemfile` + `app/controllers`), gRPC (`.proto` files), and falls back to a file-count ratio for Ruby/Go/Java/PHP/C# vs JS/TS/HTML/Python. When it fires, a bordered warning appears at the top of the CLI output *and* at the top of the generated `PRIVACY_POLICY.md` (not just a log line easy to miss), the warning is included in `--json` output, and the process exits with code 1 so CI can't silently treat a scan of an unsupported codebase as a pass.
|
|
74
74
|
- **Generates a markdown privacy policy** (`src/policy/generatePolicy.ts`) in the same command: findings are grouped by data category, each clause cites the file:line that justified it, third-party processors get their own section, and anything flagged `requiresReview` is excluded from the body and listed separately under "Items Requiring Manual Review" instead of being guessed. The mandatory disclaimer (`src/policy/disclaimer.ts`) is always appended, unconditionally.
|
|
75
75
|
|
|
76
76
|
Every finding traces back to a file and line (`src/engine/types.ts#Finding`).
|
|
77
77
|
|
|
78
|
+
### Supported languages and frameworks
|
|
79
|
+
|
|
80
|
+
| Language | Frameworks | Status |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| JavaScript / TypeScript | Express, Next.js (pages + app router) | Supported |
|
|
83
|
+
| Python | Flask | Supported (beta — see "Known gaps" below) |
|
|
84
|
+
| Python | FastAPI, Django | Explicitly detected and flagged as unsupported, never silently skipped |
|
|
85
|
+
| Ruby, Go, Java, PHP, C#, gRPC | — | Explicitly detected and flagged as unsupported |
|
|
86
|
+
|
|
87
|
+
### Known gaps
|
|
88
|
+
|
|
89
|
+
- **Cross-function aliasing isn't tracked, in either language.** The same-function alias tracking described above follows exactly one assignment hop within the function it occurs in. If that value is passed on — as a function parameter, through a closure, via a module-level variable — a field read from it afterward is not detected. Example: a Flask handler reads `data = request.get_json()` and calls `User.from_dict(data)`; a field read as `data['password']` *inside* `from_dict` is not caught, only reads inside the original handler function are.
|
|
90
|
+
- **Data sent to a third party via a raw HTTP call isn't attributed to that processor.** SDK-integration detection (above) is anchored to known package imports. A call like Python's `requests.post("https://api.some-provider.com/...", json={...})` or JS's `fetch("https://api.some-provider.com/...")` — bypassing a dedicated SDK entirely — is invisible to this detection today, in both languages, regardless of how sensitive the data it sends is.
|
|
91
|
+
- **Python support is based on one real-world corpus repository so far** (a Flask app; see `test-fixtures/golden-corpus/`), not the broader validation JS detection has had. Expect rougher edges on Flask codebases with unusual patterns.
|
|
92
|
+
- **OpenAPI/Swagger scanning reports every schema under `components.schemas`, including response-only ones** — a schema that's only ever returned by the server (never submitted by a client) gets identical treatment to a real request-input schema, since nothing currently checks whether a schema is actually reachable from an operation's `requestBody` or `parameters`. On a large, schema-heavy spec this can mean a long "Items Requiring Manual Review" list that includes fields the product never collects, only ever returns. Separately, a schema defined **inline** inside a `requestBody` (not as a named, `$ref`'d `components.schemas` entry) isn't read at all today, in either direction — only named component schemas are scanned.
|
|
93
|
+
|
|
78
94
|
## Network calls
|
|
79
95
|
|
|
80
96
|
Detection itself is fully local and offline — no code leaves your machine for pattern matching. Once detection completes, `scan` makes one batched network call to grade the confidence of what it found (field names and derived yes/no signals only, never source code), and, for scans that use Zod schemas as request validators, one additional call sending a small redacted code fragment. This runs on every scan regardless of tier and is unrelated to the separate, optional LLM-assisted (Layer 2) reasoning described above. See `PRODUCT_SPEC.md`'s "Static Analysis Depth" section for the precise wording.
|
|
@@ -91,6 +107,12 @@ Detection itself is fully local and offline — no code leaves your machine for
|
|
|
91
107
|
|
|
92
108
|
`npm test` never touches the network. A separate, real-world accuracy check lives in `test-fixtures/golden-corpus/` — see its own README for what it measures and why it's not part of the default test run.
|
|
93
109
|
|
|
110
|
+
## Legal
|
|
111
|
+
|
|
112
|
+
SrcLawyer generates a *starting draft*, not a substitute for legal review — every generated policy carries that disclaimer (`src/policy/disclaimer.ts`), and `scan` repeats a short reminder of it at the end of its own output.
|
|
113
|
+
|
|
114
|
+
A dedicated privacy policy and terms of service **for SrcLawyer itself** (the tool, not the policy it writes for your project) don't exist yet. Until they do, this README is the authoritative reference for what this tool sends over the network and to whom — see [Network calls](#network-calls) above — and for how it's licensed — see [License](#license) below.
|
|
115
|
+
|
|
94
116
|
## License
|
|
95
117
|
|
|
96
118
|
[Business Source License 1.1](LICENSE) (BUSL-1.1) — source-available, not OSI-approved open source.
|
package/dist/cli.js
CHANGED
|
@@ -16,9 +16,51 @@ program
|
|
|
16
16
|
.version(version);
|
|
17
17
|
program
|
|
18
18
|
.command("init")
|
|
19
|
-
.description("One-time setup: jurisdiction, markets, industry")
|
|
20
|
-
.
|
|
21
|
-
|
|
19
|
+
.description("One-time setup: jurisdiction, markets, industry, and optional policy details (company info, data practices, regional rights)")
|
|
20
|
+
.option("--policy-details", "ask the optional policy-detail questions (company info, data practices, regional rights)")
|
|
21
|
+
.option("--no-policy-details", "skip all optional policy-detail questions, leaving them unanswered")
|
|
22
|
+
.option("--collects-childrens-data", "declare that this product knowingly collects data from children under 13/16")
|
|
23
|
+
.option("--no-collects-childrens-data", "declare that this product does not knowingly collect data from children under 13/16")
|
|
24
|
+
.option("--company-name <name>", "company or product name")
|
|
25
|
+
.option("--site-url <url>", "site or app URL")
|
|
26
|
+
.option("--privacy-contact-email <email>", "privacy contact email address")
|
|
27
|
+
.option("--eu-uk-users", "declare that this product has users in the EU/UK")
|
|
28
|
+
.option("--no-eu-uk-users", "declare that this product has no users in the EU/UK")
|
|
29
|
+
.option("--california-users", "declare that this product has users in California")
|
|
30
|
+
.option("--no-california-users", "declare that this product has no users in California")
|
|
31
|
+
.option("--sells-or-shares-data", "declare that personal data is sold or shared with third parties")
|
|
32
|
+
.option("--no-sells-or-shares-data", "declare that personal data is not sold or shared with third parties")
|
|
33
|
+
.option("--uses-data-for-advertising", "declare that personal data is used for advertising")
|
|
34
|
+
.option("--no-uses-data-for-advertising", "declare that personal data is not used for advertising")
|
|
35
|
+
.option("--data-retention-policy <text>", "how long personal data is retained")
|
|
36
|
+
.option("--account-deletion-instructions <text>", "how a user can delete their account or request data deletion")
|
|
37
|
+
.option("--minimum-age <number>", "minimum age required to use this product")
|
|
38
|
+
.option("--sends-data-to-ai-services", "declare that user data is sent to third-party AI services")
|
|
39
|
+
.option("--no-sends-data-to-ai-services", "declare that user data is not sent to third-party AI services")
|
|
40
|
+
.action(async (opts) => {
|
|
41
|
+
let minimumAge;
|
|
42
|
+
if (opts.minimumAge !== undefined) {
|
|
43
|
+
const parsed = Number(opts.minimumAge);
|
|
44
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
45
|
+
program.error(`error: --minimum-age must be a whole, non-negative number (got "${opts.minimumAge}")`);
|
|
46
|
+
}
|
|
47
|
+
minimumAge = parsed;
|
|
48
|
+
}
|
|
49
|
+
await runInit(process.cwd(), {
|
|
50
|
+
policyDetails: opts.policyDetails,
|
|
51
|
+
collectsChildrensData: opts.collectsChildrensData,
|
|
52
|
+
companyName: opts.companyName,
|
|
53
|
+
siteUrl: opts.siteUrl,
|
|
54
|
+
privacyContactEmail: opts.privacyContactEmail,
|
|
55
|
+
euUkUsers: opts.euUkUsers,
|
|
56
|
+
californiaUsers: opts.californiaUsers,
|
|
57
|
+
sellsOrSharesData: opts.sellsOrSharesData,
|
|
58
|
+
usesDataForAdvertising: opts.usesDataForAdvertising,
|
|
59
|
+
dataRetentionPolicy: opts.dataRetentionPolicy,
|
|
60
|
+
accountDeletionInstructions: opts.accountDeletionInstructions,
|
|
61
|
+
minimumAge,
|
|
62
|
+
sendsDataToAiServices: opts.sendsDataToAiServices,
|
|
63
|
+
});
|
|
22
64
|
});
|
|
23
65
|
program
|
|
24
66
|
.command("scan")
|
|
@@ -3,6 +3,9 @@ export interface ProtectedLogicClientOptions {
|
|
|
3
3
|
baseUrl: string;
|
|
4
4
|
apiKey: string;
|
|
5
5
|
batchSize?: number;
|
|
6
|
+
/** Max total JSON-serialized bytes of items per batch, in addition to batchSize -- whichever limit
|
|
7
|
+
* is hit first ends the batch. */
|
|
8
|
+
batchMaxBytes?: number;
|
|
6
9
|
timeoutMs?: number;
|
|
7
10
|
}
|
|
8
11
|
export interface ScoreFieldsItem {
|
|
@@ -3,42 +3,83 @@
|
|
|
3
3
|
* these calls are bound by). Mirrors resolveAmbiguousFindings.ts's established pattern deliberately:
|
|
4
4
|
* batch, try/catch per batch, never throw out of this module — a failure here degrades the scan
|
|
5
5
|
* (safe local fallback + a scan-level warning), it never crashes it.
|
|
6
|
+
*
|
|
7
|
+
* Batching is by total request-body bytes as well as item count: confirmed via wrangler tail that the
|
|
8
|
+
* Worker's own CPU time limit is hit intermittently on large detect-zod-schema batches (large
|
|
9
|
+
* code fragments mean more server-side parsing work per batch, not just more JSON to transfer). Count
|
|
10
|
+
* alone (the original design) doesn't bound that — 25 large fragments can still be a very large batch.
|
|
6
11
|
*/
|
|
7
12
|
const DEFAULT_BATCH_SIZE = 25;
|
|
13
|
+
const DEFAULT_BATCH_MAX_BYTES = 50_000;
|
|
8
14
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
9
|
-
|
|
15
|
+
const RETRY_BACKOFF_MS = 500;
|
|
16
|
+
/**
|
|
17
|
+
* Greedily fills batches up to maxCount items AND maxBytes of JSON-serialized item size, whichever
|
|
18
|
+
* comes first. A single item already at or over maxBytes (shouldn't happen for zod candidates — see
|
|
19
|
+
* zodPreFilter.ts's own per-fragment cap, enforced upstream — but score-fields items have no such cap)
|
|
20
|
+
* still gets its own one-item batch rather than being dropped, so this never silently discards data;
|
|
21
|
+
* only the per-fragment cap upstream does that, and it always warns when it does.
|
|
22
|
+
*/
|
|
23
|
+
function chunkByCountAndBytes(items, maxCount, maxBytes) {
|
|
10
24
|
const batches = [];
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
let current = [];
|
|
26
|
+
let currentBytes = 0;
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const itemBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
29
|
+
const wouldExceed = current.length > 0 && (current.length >= maxCount || currentBytes + itemBytes > maxBytes);
|
|
30
|
+
if (wouldExceed) {
|
|
31
|
+
batches.push(current);
|
|
32
|
+
current = [];
|
|
33
|
+
currentBytes = 0;
|
|
34
|
+
}
|
|
35
|
+
current.push(item);
|
|
36
|
+
currentBytes += itemBytes;
|
|
37
|
+
}
|
|
38
|
+
if (current.length > 0)
|
|
39
|
+
batches.push(current);
|
|
13
40
|
return batches;
|
|
14
41
|
}
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
15
45
|
async function postJson(url, apiKey, body, timeoutMs) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
const MAX_ATTEMPTS = 2; // one retry, on a 5xx only -- a 4xx (bad request, auth) won't succeed on retry
|
|
47
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
50
|
+
let status;
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "content-type": "application/json", "x-api-key": apiKey },
|
|
55
|
+
body: JSON.stringify(body),
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
if (response.ok)
|
|
59
|
+
return (await response.json());
|
|
60
|
+
status = response.status;
|
|
27
61
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
const isLastAttempt = attempt === MAX_ATTEMPTS;
|
|
66
|
+
if (status < 500 || isLastAttempt) {
|
|
67
|
+
throw new Error(`protected-logic request failed (${status})`);
|
|
68
|
+
}
|
|
69
|
+
await sleep(RETRY_BACKOFF_MS);
|
|
32
70
|
}
|
|
71
|
+
// Unreachable: the loop above always either returns or throws.
|
|
72
|
+
throw new Error("protected-logic request failed (unreachable)");
|
|
33
73
|
}
|
|
34
74
|
export async function scoreFields(items, options) {
|
|
35
75
|
const results = new Map();
|
|
36
76
|
if (items.length === 0)
|
|
37
77
|
return { results, error: null };
|
|
38
78
|
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
79
|
+
const batchMaxBytes = options.batchMaxBytes ?? DEFAULT_BATCH_MAX_BYTES;
|
|
39
80
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
40
81
|
let error = null;
|
|
41
|
-
for (const batch of
|
|
82
|
+
for (const batch of chunkByCountAndBytes(items, batchSize, batchMaxBytes)) {
|
|
42
83
|
try {
|
|
43
84
|
const response = await postJson(`${options.baseUrl}/v1/score-fields`, options.apiKey, { fields: batch }, timeoutMs);
|
|
44
85
|
for (const result of response.results)
|
|
@@ -55,9 +96,10 @@ export async function detectZodSchemas(items, options) {
|
|
|
55
96
|
if (items.length === 0)
|
|
56
97
|
return { results, error: null };
|
|
57
98
|
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
99
|
+
const batchMaxBytes = options.batchMaxBytes ?? DEFAULT_BATCH_MAX_BYTES;
|
|
58
100
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
59
101
|
let error = null;
|
|
60
|
-
for (const batch of
|
|
102
|
+
for (const batch of chunkByCountAndBytes(items, batchSize, batchMaxBytes)) {
|
|
61
103
|
try {
|
|
62
104
|
const response = await postJson(`${options.baseUrl}/v1/detect-zod-schema`, options.apiKey, { candidates: batch }, timeoutMs);
|
|
63
105
|
for (const result of response.results)
|
|
@@ -11,4 +11,4 @@ export interface ResolveProtectedLogicResult {
|
|
|
11
11
|
* same way resolveAmbiguousFindings.ts is layered on top of it for Layer 2 — not called from inside
|
|
12
12
|
* scan() itself, so scan()'s own purity/auditability claim stays true.
|
|
13
13
|
*/
|
|
14
|
-
export declare function resolveProtectedLogic(findings: Finding[], pendingZodCandidates: ZodCandidate[], options: ProtectedLogicClientOptions): Promise<ResolveProtectedLogicResult>;
|
|
14
|
+
export declare function resolveProtectedLogic(findings: Finding[], pendingZodCandidates: ZodCandidate[], options: ProtectedLogicClientOptions, oversizedZodFiles?: string[]): Promise<ResolveProtectedLogicResult>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyFieldName, isAmbiguousFieldName } from "../rules/dataCategoryLexicon.js";
|
|
2
|
+
import { isSafeFieldName } from "../rules/safeFieldNames.js";
|
|
2
3
|
import { countConfidentSiblings } from "../rules/confidenceFactors.js";
|
|
3
4
|
import { detectZodSchemas, scoreFields } from "./protectedLogicClient.js";
|
|
4
5
|
/**
|
|
@@ -7,9 +8,20 @@ import { detectZodSchemas, scoreFields } from "./protectedLogicClient.js";
|
|
|
7
8
|
* same way resolveAmbiguousFindings.ts is layered on top of it for Layer 2 — not called from inside
|
|
8
9
|
* scan() itself, so scan()'s own purity/auditability claim stays true.
|
|
9
10
|
*/
|
|
10
|
-
export async function resolveProtectedLogic(findings, pendingZodCandidates, options) {
|
|
11
|
+
export async function resolveProtectedLogic(findings, pendingZodCandidates, options, oversizedZodFiles = []) {
|
|
11
12
|
let workingFindings = findings;
|
|
12
13
|
const warnings = [];
|
|
14
|
+
if (oversizedZodFiles.length > 0) {
|
|
15
|
+
// Kept to a single summarized line -- this list is meant to be rare now that the hard ceiling is
|
|
16
|
+
// only for pathological cases (see MAX_FRAGMENT_BYTES), but a long list here would still be an
|
|
17
|
+
// unreadable wall of text. The full, untruncated list is always in ScanResult.oversizedZodFiles
|
|
18
|
+
// (surfaced in --json output), so nothing here is actually lost, just not all printed inline.
|
|
19
|
+
const PREVIEW_COUNT = 3;
|
|
20
|
+
const preview = oversizedZodFiles.slice(0, PREVIEW_COUNT).join(", ");
|
|
21
|
+
const remaining = oversizedZodFiles.length - PREVIEW_COUNT;
|
|
22
|
+
const suffix = remaining > 0 ? `, and ${remaining} more (full list in --json output)` : "";
|
|
23
|
+
warnings.push(`Zod-schema detection skipped for ${oversizedZodFiles.length} file(s) because the extracted fragment was too large to send safely: ${preview}${suffix}.`);
|
|
24
|
+
}
|
|
13
25
|
if (pendingZodCandidates.length > 0) {
|
|
14
26
|
const byId = new Map(pendingZodCandidates.map((c) => [c.id, c]));
|
|
15
27
|
const { results, error } = await detectZodSchemas(pendingZodCandidates.map((c) => ({ id: c.id, codeFragment: c.codeFragment })), options);
|
|
@@ -22,6 +34,8 @@ export async function resolveProtectedLogic(findings, pendingZodCandidates, opti
|
|
|
22
34
|
continue;
|
|
23
35
|
const allNames = result.fields.map((f) => f.name);
|
|
24
36
|
for (const field of result.fields) {
|
|
37
|
+
if (isSafeFieldName(field.name))
|
|
38
|
+
continue;
|
|
25
39
|
const sourceLine = candidate.fragmentLineToSourceLine[field.line - 1] ?? field.line;
|
|
26
40
|
const lexiconCategory = classifyFieldName(field.name);
|
|
27
41
|
const ambiguousName = isAmbiguousFieldName(field.name);
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface InitFlags {
|
|
2
|
+
policyDetails?: boolean;
|
|
3
|
+
collectsChildrensData?: boolean;
|
|
4
|
+
companyName?: string;
|
|
5
|
+
siteUrl?: string;
|
|
6
|
+
privacyContactEmail?: string;
|
|
7
|
+
euUkUsers?: boolean;
|
|
8
|
+
californiaUsers?: boolean;
|
|
9
|
+
sellsOrSharesData?: boolean;
|
|
10
|
+
usesDataForAdvertising?: boolean;
|
|
11
|
+
dataRetentionPolicy?: string;
|
|
12
|
+
accountDeletionInstructions?: string;
|
|
13
|
+
minimumAge?: number;
|
|
14
|
+
sendsDataToAiServices?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function runInit(root: string, flags?: InitFlags): Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { CONFIG_FILENAME, DEFAULT_CONFIG, configPath, hasUnmigratedLegacyConfig, legacyConfigMessage, writeConfig, } from "../config/config.js";
|
|
5
|
+
import { NETWORK_CALLS_NOTICE } from "../policy/legalNotices.js";
|
|
5
6
|
function guessIndustry(root) {
|
|
6
7
|
const pkgPath = join(root, "package.json");
|
|
7
8
|
if (!existsSync(pkgPath))
|
|
@@ -23,7 +24,62 @@ async function ask(lines, question, fallback) {
|
|
|
23
24
|
const answer = done ? "" : value.trim();
|
|
24
25
|
return answer.length > 0 ? answer : fallback;
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
async function askOptional(lines, question) {
|
|
28
|
+
process.stdout.write(`${question} (optional, press Enter to skip): `);
|
|
29
|
+
const { value, done } = await lines.next();
|
|
30
|
+
const answer = done ? "" : value.trim();
|
|
31
|
+
return answer.length > 0 ? answer : null;
|
|
32
|
+
}
|
|
33
|
+
// suggested, when not null, is shown as the bracketed default and adopted on a blank answer -- used
|
|
34
|
+
// to prefill EU/UK and California questions from targetMarkets without ever forcing an answer. When
|
|
35
|
+
// suggested is null (no signal), blank means genuinely unanswered, same as every other optional field.
|
|
36
|
+
async function askYesNoOptional(lines, question, suggested = null) {
|
|
37
|
+
const suffix = suggested === null ? " (y/n, optional, press Enter to skip)" : ` [${suggested ? "y" : "n"}]`;
|
|
38
|
+
process.stdout.write(`${question}${suffix}: `);
|
|
39
|
+
const { value, done } = await lines.next();
|
|
40
|
+
const answer = done ? "" : value.trim().toLowerCase();
|
|
41
|
+
if (answer.length === 0)
|
|
42
|
+
return suggested;
|
|
43
|
+
return /^y(es)?$/.test(answer);
|
|
44
|
+
}
|
|
45
|
+
async function askOptionalAge(lines, question) {
|
|
46
|
+
const raw = await askOptional(lines, question);
|
|
47
|
+
if (raw === null)
|
|
48
|
+
return null;
|
|
49
|
+
const parsed = Number(raw);
|
|
50
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
51
|
+
console.log(` (Could not read "${raw}" as a whole, non-negative number -- skipping. You can set this later by editing ${CONFIG_FILENAME}.)`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
// Generic resolution order for every new optional field: an explicit flag always wins outright;
|
|
57
|
+
// otherwise the field is only asked if the policy-details gate resolved to yes; otherwise it's null.
|
|
58
|
+
async function resolveField(flagValue, gateYes, askFn) {
|
|
59
|
+
if (flagValue !== undefined)
|
|
60
|
+
return flagValue;
|
|
61
|
+
if (!gateYes)
|
|
62
|
+
return null;
|
|
63
|
+
return askFn();
|
|
64
|
+
}
|
|
65
|
+
const EU_UK_TOKENS = new Set(["eu", "uk", "gb", "eea", "european union", "united kingdom"]);
|
|
66
|
+
const CALIFORNIA_TOKENS = new Set(["ca", "california", "us-ca"]);
|
|
67
|
+
function targetMarketsSuggestEuUk(markets) {
|
|
68
|
+
return markets.some((m) => EU_UK_TOKENS.has(m.trim().toLowerCase()));
|
|
69
|
+
}
|
|
70
|
+
function targetMarketsSuggestCalifornia(markets) {
|
|
71
|
+
return markets.some((m) => CALIFORNIA_TOKENS.has(m.trim().toLowerCase()));
|
|
72
|
+
}
|
|
73
|
+
// Only warns in the one direction that's a real contradiction: target markets signal a region, but
|
|
74
|
+
// the final answer explicitly denies it. The reverse (no signal, but the user says yes) isn't a
|
|
75
|
+
// contradiction -- targetMarkets is free text and was never meant to be an exhaustive jurisdiction
|
|
76
|
+
// list. Never blocks or overrides -- the explicit answer always stands, this only warns.
|
|
77
|
+
function warnIfContradictsTargetMarkets(label, finalValue, suggestedFromMarkets) {
|
|
78
|
+
if (suggestedFromMarkets && finalValue === false) {
|
|
79
|
+
console.log(`\nWarning: your target markets suggest ${label} users, but you indicated none -- the generated policy will omit ${label}-specific disclosures. Re-run init or edit ${CONFIG_FILENAME} if this is incorrect.\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function runInit(root, flags = {}) {
|
|
27
83
|
const existingPath = configPath(root);
|
|
28
84
|
if (existsSync(existingPath)) {
|
|
29
85
|
console.log(`${CONFIG_FILENAME} already exists. Delete it first if you want to redo setup.`);
|
|
@@ -36,36 +92,72 @@ export async function runInit(root) {
|
|
|
36
92
|
const guessedIndustry = guessIndustry(root);
|
|
37
93
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
38
94
|
const lines = rl[Symbol.asyncIterator]();
|
|
95
|
+
// Printed once, here, not re-shown on every scan -- informational only, never blocks: nothing below
|
|
96
|
+
// waits on an acknowledgment, so this is safe in non-interactive contexts (CI, scripted installs).
|
|
97
|
+
console.log(`${NETWORK_CALLS_NOTICE}\n`);
|
|
39
98
|
console.log("SrcLawyer setup — answered once, reused on every scan.\n");
|
|
40
99
|
const entityLocation = await ask(lines, "Where is your legal entity located? (country)", DEFAULT_CONFIG.entityLocation ?? "");
|
|
41
100
|
const targetMarketsRaw = await ask(lines, "Which regions/markets do you serve? (comma-separated, e.g. US, EU)", "");
|
|
101
|
+
const targetMarkets = targetMarketsRaw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
42
102
|
const industry = await ask(lines, "What industry/vertical is this product in?", guessedIndustry ?? "");
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
103
|
+
const gateYes = flags.policyDetails !== undefined
|
|
104
|
+
? flags.policyDetails
|
|
105
|
+
: /^y(es)?$/i.test((await ask(lines, "Add privacy policy details now? (company info, data practices, regional rights, etc.)", "y")).trim());
|
|
106
|
+
// Deliberately requires an explicit y/n -- a bare Enter, empty stdin, or a declined gate must leave
|
|
107
|
+
// this null (unanswered, rendered as a placeholder), never silently "no". Unlike askOptional's other
|
|
108
|
+
// callers, there is no safe default here: "no" is itself a real, consequential legal declaration, not
|
|
109
|
+
// a neutral fallback. Layer 2's high-stakes override (scan.ts) coerces null to false at its one call
|
|
110
|
+
// site, so leaving this unanswered never weakens that safety check -- see childrensDataOverride.test.ts.
|
|
111
|
+
const collectsChildrensData = await resolveField(flags.collectsChildrensData, gateYes, () => askYesNoOptional(lines, "Does this product knowingly collect data from children under 13/16?"));
|
|
112
|
+
const minimumAge = await resolveField(flags.minimumAge, gateYes, () => askOptionalAge(lines, "Minimum age required to use this product"));
|
|
113
|
+
const companyName = await resolveField(flags.companyName, gateYes, () => askOptional(lines, "Company or product name"));
|
|
114
|
+
const siteUrl = await resolveField(flags.siteUrl, gateYes, () => askOptional(lines, "Site or app URL"));
|
|
115
|
+
const privacyContactEmail = await resolveField(flags.privacyContactEmail, gateYes, () => askOptional(lines, "Privacy contact email"));
|
|
116
|
+
const euUkSuggested = targetMarketsSuggestEuUk(targetMarkets);
|
|
117
|
+
const hasEuUkUsers = await resolveField(flags.euUkUsers, gateYes, () => askYesNoOptional(lines, "Do you have users in the EU/UK?", euUkSuggested ? true : null));
|
|
118
|
+
warnIfContradictsTargetMarkets("EU/UK", hasEuUkUsers, euUkSuggested);
|
|
119
|
+
const californiaSuggested = targetMarketsSuggestCalifornia(targetMarkets);
|
|
120
|
+
const hasCaliforniaUsers = await resolveField(flags.californiaUsers, gateYes, () => askYesNoOptional(lines, "Do you have users in California?", californiaSuggested ? true : null));
|
|
121
|
+
warnIfContradictsTargetMarkets("California", hasCaliforniaUsers, californiaSuggested);
|
|
122
|
+
const sellsOrSharesData = await resolveField(flags.sellsOrSharesData, gateYes, () => askYesNoOptional(lines, "Does this product sell or share personal data with third parties?"));
|
|
123
|
+
const usesDataForAdvertising = await resolveField(flags.usesDataForAdvertising, gateYes, () => askYesNoOptional(lines, "Does this product use personal data for advertising?"));
|
|
124
|
+
const dataRetentionPolicy = await resolveField(flags.dataRetentionPolicy, gateYes, () => askOptional(lines, "How long is personal data retained?"));
|
|
125
|
+
const accountDeletionInstructions = await resolveField(flags.accountDeletionInstructions, gateYes, () => askOptional(lines, "How can a user delete their account or request data deletion?"));
|
|
126
|
+
// Deliberately about the SCANNED PRODUCT's own behavior (does it send its end-users' data to AI
|
|
127
|
+
// services as part of its own functionality) -- not about SrcLawyer's own, separately-documented
|
|
128
|
+
// BYOK network calls described in NETWORK_CALLS_NOTICE above.
|
|
129
|
+
const sendsDataToAiServices = await resolveField(flags.sendsDataToAiServices, gateYes, () => askYesNoOptional(lines, "Does this product send user data to third-party AI services (e.g. OpenAI, Anthropic) as part of its own functionality?"));
|
|
130
|
+
// "managed" is deliberately not offered here -- it's a real SrcLawyerTier value and config.ts/
|
|
131
|
+
// resolveProviderForTier.ts both still support it fully (a hand-edited config with tier: managed
|
|
132
|
+
// still gets a clear, accurate error at scan time), but there's no advertised path to it from this
|
|
133
|
+
// prompt, so a tester reading the choices here can't select a dead end during setup.
|
|
134
|
+
const enableByokRaw = await ask(lines, "Enable optional AI-assisted review with your own Anthropic key? (advanced)", "n");
|
|
135
|
+
const tier = /^y(es)?$/i.test(enableByokRaw.trim()) ? "byok" : "free";
|
|
46
136
|
let apiKeyEnvVar = null;
|
|
47
137
|
if (tier === "byok") {
|
|
48
138
|
apiKeyEnvVar = await ask(lines, "Which environment variable holds your Anthropic API key?", "ANTHROPIC_API_KEY");
|
|
49
139
|
console.log(`\nNote: only the name "${apiKeyEnvVar}" is written to ${CONFIG_FILENAME} — the key itself is never written to disk. It's read from your environment at scan time.`);
|
|
50
140
|
}
|
|
51
|
-
else if (tier === "managed") {
|
|
52
|
-
console.log("\nHeads up: the managed tier's hosted backend isn't available yet. Layer 1 (static analysis) will still run fine; AI-assisted resolution will report a clear error until it ships.");
|
|
53
|
-
}
|
|
54
141
|
rl.close();
|
|
55
142
|
const config = {
|
|
56
143
|
entityLocation: entityLocation || null,
|
|
57
|
-
targetMarkets
|
|
144
|
+
targetMarkets,
|
|
58
145
|
industry: industry || null,
|
|
59
|
-
collectsChildrensData
|
|
146
|
+
collectsChildrensData,
|
|
60
147
|
tier,
|
|
61
148
|
...(tier === "byok" ? { llm: { provider: "anthropic", apiKeyEnvVar } } : {}),
|
|
149
|
+
companyName,
|
|
150
|
+
siteUrl,
|
|
151
|
+
privacyContactEmail,
|
|
152
|
+
hasEuUkUsers,
|
|
153
|
+
hasCaliforniaUsers,
|
|
154
|
+
sellsOrSharesData,
|
|
155
|
+
usesDataForAdvertising,
|
|
156
|
+
dataRetentionPolicy,
|
|
157
|
+
accountDeletionInstructions,
|
|
158
|
+
minimumAge,
|
|
159
|
+
sendsDataToAiServices,
|
|
62
160
|
};
|
|
63
161
|
writeConfig(root, config);
|
|
64
162
|
console.log(`\nWrote ${CONFIG_FILENAME}. Run "srclawyer scan" to analyze this codebase.`);
|
|
65
163
|
}
|
|
66
|
-
function normalizeTier(raw) {
|
|
67
|
-
const normalized = raw.trim().toLowerCase();
|
|
68
|
-
if (normalized === "byok" || normalized === "managed")
|
|
69
|
-
return normalized;
|
|
70
|
-
return "free";
|
|
71
|
-
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -8,6 +8,8 @@ import { resolveProviderForTier } from "../llm/resolveProviderForTier.js";
|
|
|
8
8
|
import { resolveAmbiguousFindings } from "../llm/resolveAmbiguousFindings.js";
|
|
9
9
|
import { resolveProtectedLogic } from "../cloud/resolveProtectedLogic.js";
|
|
10
10
|
import { resolveProtectedLogicEndpoint } from "../cloud/config.js";
|
|
11
|
+
import { MAX_FRAGMENT_BYTES } from "../rules/zodPreFilter.js";
|
|
12
|
+
import { SCAN_DISCLAIMER_REMINDER, byokNetworkNotice } from "../policy/legalNotices.js";
|
|
11
13
|
export async function runScan(root, options) {
|
|
12
14
|
const config = loadConfig(root);
|
|
13
15
|
let legacyConfigWarning = null;
|
|
@@ -22,15 +24,18 @@ export async function runScan(root, options) {
|
|
|
22
24
|
}
|
|
23
25
|
const result = await scan(root);
|
|
24
26
|
let findings = result.findings;
|
|
27
|
+
if (result.maxZodFragmentBytes > 0) {
|
|
28
|
+
console.error(`Largest Zod-schema fragment this scan: ${result.maxZodFragmentBytes} bytes (hard ceiling: ${MAX_FRAGMENT_BYTES} bytes).`);
|
|
29
|
+
}
|
|
25
30
|
// Not tier-gated, not behind --no-llm: confidence scoring and Zod-schema detection are Layer 1
|
|
26
31
|
// capabilities now, needed on every real scan regardless of which LLM tier (if any) is configured.
|
|
27
32
|
// A failure here degrades honestly (findings keep their safe local fallback) rather than blocking
|
|
28
33
|
// the scan — see resolveProtectedLogic's own per-call error handling.
|
|
29
|
-
const hasScorableFindings = findings.some((f) => f.confidenceFactors) || result.pendingZodCandidates.length > 0;
|
|
34
|
+
const hasScorableFindings = findings.some((f) => f.confidenceFactors) || result.pendingZodCandidates.length > 0 || result.oversizedZodFiles.length > 0;
|
|
30
35
|
let protectedLogicWarning = null;
|
|
31
36
|
if (hasScorableFindings) {
|
|
32
37
|
const endpoint = resolveProtectedLogicEndpoint();
|
|
33
|
-
const resolved = await resolveProtectedLogic(findings, result.pendingZodCandidates, endpoint);
|
|
38
|
+
const resolved = await resolveProtectedLogic(findings, result.pendingZodCandidates, endpoint, result.oversizedZodFiles);
|
|
34
39
|
findings = resolved.findings;
|
|
35
40
|
protectedLogicWarning = resolved.warning;
|
|
36
41
|
if (protectedLogicWarning)
|
|
@@ -46,9 +51,16 @@ export async function runScan(root, options) {
|
|
|
46
51
|
else if (provider) {
|
|
47
52
|
const ambiguous = findings.filter((f) => f.requiresReview);
|
|
48
53
|
if (ambiguous.length > 0) {
|
|
54
|
+
if (config.tier === "byok" && config.llm) {
|
|
55
|
+
console.log(byokNetworkNotice(config.llm.apiKeyEnvVar));
|
|
56
|
+
}
|
|
49
57
|
console.log(`Sending ${ambiguous.length} ambiguous item(s) to ${provider.name}...`);
|
|
50
58
|
const resolved = await resolveAmbiguousFindings(ambiguous, provider, {
|
|
51
|
-
|
|
59
|
+
// Preserves today's exact override behavior bit-for-bit: a missing/legacy key already
|
|
60
|
+
// behaved as false before collectsChildrensData became nullable. Only the generated
|
|
61
|
+
// document (generatePolicy.ts) distinguishes "explicitly no" from "never answered" -- this
|
|
62
|
+
// safety-critical override is unchanged.
|
|
63
|
+
collectsChildrensData: config.collectsChildrensData ?? false,
|
|
52
64
|
});
|
|
53
65
|
const resolvedById = new Map(resolved.map((f) => [f.id, f]));
|
|
54
66
|
findings = findings.map((f) => resolvedById.get(f.id) ?? f);
|
|
@@ -58,7 +70,7 @@ export async function runScan(root, options) {
|
|
|
58
70
|
const ambiguousCount = findings.filter((f) => f.requiresReview).length;
|
|
59
71
|
const finalResult = { ...result, findings, ambiguousCount };
|
|
60
72
|
const policyPath = resolve(root, options.out);
|
|
61
|
-
writeFileSync(policyPath, generatePolicyMarkdown(finalResult, config, llmError));
|
|
73
|
+
writeFileSync(policyPath, generatePolicyMarkdown(finalResult, config, llmError, protectedLogicWarning));
|
|
62
74
|
if (result.unsupportedStackWarning || llmError || protectedLogicWarning || legacyConfigWarning) {
|
|
63
75
|
process.exitCode = 1;
|
|
64
76
|
}
|
|
@@ -69,5 +81,8 @@ export async function runScan(root, options) {
|
|
|
69
81
|
console.log(formatText(finalResult));
|
|
70
82
|
if (protectedLogicWarning)
|
|
71
83
|
console.log(`\n${protectedLogicWarning}`);
|
|
84
|
+
if (legacyConfigWarning)
|
|
85
|
+
console.log(`\n${legacyConfigWarning}`);
|
|
72
86
|
console.log(`\nPrivacy policy written to ${join(options.out)}`);
|
|
87
|
+
console.log(`\n${SCAN_DISCLAIMER_REMINDER}`);
|
|
73
88
|
}
|
package/dist/config/config.d.ts
CHANGED
|
@@ -10,9 +10,20 @@ export interface SrcLawyerConfig {
|
|
|
10
10
|
entityLocation: string | null;
|
|
11
11
|
targetMarkets: string[];
|
|
12
12
|
industry: string | null;
|
|
13
|
-
collectsChildrensData: boolean;
|
|
13
|
+
collectsChildrensData: boolean | null;
|
|
14
14
|
tier?: SrcLawyerTier;
|
|
15
15
|
llm?: SrcLawyerLlmConfig;
|
|
16
|
+
companyName?: string | null;
|
|
17
|
+
siteUrl?: string | null;
|
|
18
|
+
privacyContactEmail?: string | null;
|
|
19
|
+
hasEuUkUsers?: boolean | null;
|
|
20
|
+
hasCaliforniaUsers?: boolean | null;
|
|
21
|
+
sellsOrSharesData?: boolean | null;
|
|
22
|
+
usesDataForAdvertising?: boolean | null;
|
|
23
|
+
dataRetentionPolicy?: string | null;
|
|
24
|
+
accountDeletionInstructions?: string | null;
|
|
25
|
+
minimumAge?: number | null;
|
|
26
|
+
sendsDataToAiServices?: boolean | null;
|
|
16
27
|
}
|
|
17
28
|
export declare const DEFAULT_CONFIG: SrcLawyerConfig;
|
|
18
29
|
export declare function configPath(root: string): string;
|
package/dist/config/config.js
CHANGED
|
@@ -8,8 +8,19 @@ export const DEFAULT_CONFIG = {
|
|
|
8
8
|
entityLocation: null,
|
|
9
9
|
targetMarkets: [],
|
|
10
10
|
industry: null,
|
|
11
|
-
collectsChildrensData:
|
|
11
|
+
collectsChildrensData: null,
|
|
12
12
|
tier: "free",
|
|
13
|
+
companyName: null,
|
|
14
|
+
siteUrl: null,
|
|
15
|
+
privacyContactEmail: null,
|
|
16
|
+
hasEuUkUsers: null,
|
|
17
|
+
hasCaliforniaUsers: null,
|
|
18
|
+
sellsOrSharesData: null,
|
|
19
|
+
usesDataForAdvertising: null,
|
|
20
|
+
dataRetentionPolicy: null,
|
|
21
|
+
accountDeletionInstructions: null,
|
|
22
|
+
minimumAge: null,
|
|
23
|
+
sendsDataToAiServices: null,
|
|
13
24
|
};
|
|
14
25
|
export function configPath(root) {
|
|
15
26
|
return join(root, CONFIG_FILENAME);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fg from "fast-glob";
|
|
2
2
|
const CODE_PATTERNS = ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
|
|
3
|
+
const PYTHON_PATTERNS = ["**/*.py"];
|
|
3
4
|
const HTML_PATTERNS = ["**/*.html", "**/*.htm"];
|
|
4
5
|
const SCHEMA_PATTERNS = ["**/*.graphql", "**/*.gql", "**/openapi.yaml", "**/openapi.yml", "**/openapi.json", "**/swagger.yaml", "**/swagger.yml", "**/swagger.json"];
|
|
5
6
|
const MANIFEST_PATTERNS = ["**/package.json", "**/requirements.txt"];
|
|
@@ -14,12 +15,25 @@ const IGNORE = [
|
|
|
14
15
|
"**/*.spec.*",
|
|
15
16
|
"**/*.d.ts",
|
|
16
17
|
];
|
|
18
|
+
// Separate from IGNORE: node_modules has no Python analogue, and excluding a venv/site-packages
|
|
19
|
+
// directory matters even more here, since it's a full copy of every installed pip package's own
|
|
20
|
+
// source -- the Python equivalent of node_modules, and just as irrelevant/misleading to scan.
|
|
21
|
+
const PYTHON_IGNORE = [
|
|
22
|
+
...IGNORE,
|
|
23
|
+
"**/venv/**",
|
|
24
|
+
"**/.venv/**",
|
|
25
|
+
"**/__pycache__/**",
|
|
26
|
+
"**/site-packages/**",
|
|
27
|
+
"**/test_*.py",
|
|
28
|
+
"**/*_test.py",
|
|
29
|
+
];
|
|
17
30
|
export async function discoverFiles(root) {
|
|
18
|
-
const [codeFiles, htmlFiles, schemaFiles, manifestFiles] = await Promise.all([
|
|
31
|
+
const [codeFiles, pythonFiles, htmlFiles, schemaFiles, manifestFiles] = await Promise.all([
|
|
19
32
|
fg(CODE_PATTERNS, { cwd: root, absolute: true, ignore: IGNORE, dot: false }),
|
|
33
|
+
fg(PYTHON_PATTERNS, { cwd: root, absolute: true, ignore: PYTHON_IGNORE, dot: false }),
|
|
20
34
|
fg(HTML_PATTERNS, { cwd: root, absolute: true, ignore: IGNORE, dot: false }),
|
|
21
35
|
fg(SCHEMA_PATTERNS, { cwd: root, absolute: true, ignore: ["**/node_modules/**"], dot: false }),
|
|
22
36
|
fg(MANIFEST_PATTERNS, { cwd: root, absolute: true, ignore: ["**/node_modules/**"], dot: false }),
|
|
23
37
|
]);
|
|
24
|
-
return { codeFiles, htmlFiles, schemaFiles, manifestFiles };
|
|
38
|
+
return { codeFiles, pythonFiles, htmlFiles, schemaFiles, manifestFiles };
|
|
25
39
|
}
|