readystack-view-in-browser-leak-audit 1.0.2__tar.gz
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.
- readystack_view_in_browser_leak_audit-1.0.2/LICENSE.txt +15 -0
- readystack_view_in_browser_leak_audit-1.0.2/PKG-INFO +84 -0
- readystack_view_in_browser_leak_audit-1.0.2/README.md +64 -0
- readystack_view_in_browser_leak_audit-1.0.2/pyproject.toml +29 -0
- readystack_view_in_browser_leak_audit-1.0.2/setup.cfg +4 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/__init__.py +1 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/__main__.py +12 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/cli.js +161 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/engine.js +76 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/license.js +83 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/package.json +44 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/rules.json +186 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/strings.json +10 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/PKG-INFO +84 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/SOURCES.txt +17 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/dependency_links.txt +1 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/entry_points.txt +2 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/requires.txt +1 -0
- readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
View in Browser — Publish Leak Audit (HTML) — Licence
|
|
2
|
+
|
|
3
|
+
Free scope
|
|
4
|
+
Audit the HTML file you are about to open in the browser and get every leak with its line number and the fix. is free for any use, personal or commercial, for as long as you keep the
|
|
5
|
+
extension installed, and the online check at the product page is free without registration.
|
|
6
|
+
|
|
7
|
+
Paid scope
|
|
8
|
+
Sweep every HTML file in the workspace in one command and write a dated audit report you keep. is the paid part. It asks for a licence key issued at purchase (one seat, one key). The key is checked against the payment provider and
|
|
9
|
+
cached locally for 30 days so it keeps working offline.
|
|
10
|
+
|
|
11
|
+
Redistribution
|
|
12
|
+
You may not resell, re-host or bundle this extension or its rule set. The rule set and the
|
|
13
|
+
engine are provided as-is; check results are advisory and do not constitute legal advice.
|
|
14
|
+
|
|
15
|
+
(c) ReadyStack — https://getreadystack.com
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-view-in-browser-leak-audit
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: 24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resou (bundled Node.js runtime)
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: view in browser,html,preview,api key,secret,localhost,publish,static site,audit,leak
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: nodejs-wheel-binaries>=20
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# View in Browser — Publish Leak Audit (HTML)
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as the npm package `@readystack/view-in-browser-leak-audit` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resources, a leftover noindex.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
view-in-browser-leak-audit file
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Node 18+. The same 24 rules as the VS Code extension, from a terminal or CI.
|
|
36
|
+
|
|
37
|
+
## Free
|
|
38
|
+
|
|
39
|
+
- Audit the HTML file you are about to open in the browser: all 24 rules, every finding with its severity, line number and fix. The clean fixture returns 0 findings, the dirty one returns 27. No key, no watermark, no locked answers.
|
|
40
|
+
- `--rules` lists every rule
|
|
41
|
+
|
|
42
|
+
## With a licence ($29 once)
|
|
43
|
+
|
|
44
|
+
- Scope: one command sweeps every HTML file in the workspace instead of the one in front of you, and writes a dated LEAK-AUDIT.md you keep as evidence — forty pages in one pass instead of forty passes.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
@readystack/view-in-browser-leak-audit --dir ./templates --report html --out report.html
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
In February 2026 a developer reported an $82,000 Google Cloud bill after a key embedded in client-side Google Maps code was taken from the page source; their normal spend was $180 a month.
|
|
51
|
+
|
|
52
|
+
## Use from an AI agent (MCP)
|
|
53
|
+
|
|
54
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "view-in-browser-leak-audit": { "command": "npx", "args": ["-y", "@readystack/view-in-browser-leak-audit", "--mcp"] } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
61
|
+
|
|
62
|
+
## Use in CI
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- name: View in Browser — Publish Leak Audit (HTML)
|
|
66
|
+
run: npx -y @readystack/view-in-browser-leak-audit --dir . --ci
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/view-in-browser-leak-audit --dir /work --ci`)
|
|
70
|
+
|
|
71
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
72
|
+
|
|
73
|
+
[Get a licence](https://buy.polar.sh/polar_cl_CwvLDIxujtAKjVYNswBmnY71CVpJXNzshSaBQ2RDuKe)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
<!-- view in browser leak audit -->
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Install (PyPI)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
pip install readystack-view-in-browser-leak-audit
|
|
83
|
+
view-in-browser-leak-audit --help
|
|
84
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# View in Browser — Publish Leak Audit (HTML)
|
|
2
|
+
|
|
3
|
+
> Runs the same checker as the npm package `@readystack/view-in-browser-leak-audit` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resources, a leftover noindex.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
view-in-browser-leak-audit file
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node 18+. The same 24 rules as the VS Code extension, from a terminal or CI.
|
|
16
|
+
|
|
17
|
+
## Free
|
|
18
|
+
|
|
19
|
+
- Audit the HTML file you are about to open in the browser: all 24 rules, every finding with its severity, line number and fix. The clean fixture returns 0 findings, the dirty one returns 27. No key, no watermark, no locked answers.
|
|
20
|
+
- `--rules` lists every rule
|
|
21
|
+
|
|
22
|
+
## With a licence ($29 once)
|
|
23
|
+
|
|
24
|
+
- Scope: one command sweeps every HTML file in the workspace instead of the one in front of you, and writes a dated LEAK-AUDIT.md you keep as evidence — forty pages in one pass instead of forty passes.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
@readystack/view-in-browser-leak-audit --dir ./templates --report html --out report.html
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
In February 2026 a developer reported an $82,000 Google Cloud bill after a key embedded in client-side Google Maps code was taken from the page source; their normal spend was $180 a month.
|
|
31
|
+
|
|
32
|
+
## Use from an AI agent (MCP)
|
|
33
|
+
|
|
34
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{ "mcpServers": { "view-in-browser-leak-audit": { "command": "npx", "args": ["-y", "@readystack/view-in-browser-leak-audit", "--mcp"] } } }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
41
|
+
|
|
42
|
+
## Use in CI
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
- name: View in Browser — Publish Leak Audit (HTML)
|
|
46
|
+
run: npx -y @readystack/view-in-browser-leak-audit --dir . --ci
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/view-in-browser-leak-audit --dir /work --ci`)
|
|
50
|
+
|
|
51
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
52
|
+
|
|
53
|
+
[Get a licence](https://buy.polar.sh/polar_cl_CwvLDIxujtAKjVYNswBmnY71CVpJXNzshSaBQ2RDuKe)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
<!-- view in browser leak audit -->
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Install (PyPI)
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
pip install readystack-view-in-browser-leak-audit
|
|
63
|
+
view-in-browser-leak-audit --help
|
|
64
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "readystack-view-in-browser-leak-audit"
|
|
7
|
+
version = "1.0.2"
|
|
8
|
+
description = "24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resou (bundled Node.js runtime)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["nodejs-wheel-binaries>=20"]
|
|
12
|
+
license = {text = "Proprietary - see LICENSE.txt"}
|
|
13
|
+
keywords = ["view in browser", "html", "preview", "api key", "secret", "localhost", "publish", "static site", "audit", "leak"]
|
|
14
|
+
authors = [{name = "ReadyStack", email = "support@getreadystack.com"}]
|
|
15
|
+
classifiers = ["Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Software Development :: Quality Assurance", "Operating System :: OS Independent"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi"
|
|
19
|
+
Documentation = "https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi"
|
|
20
|
+
Support = "https://getreadystack.com/support"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
"view-in-browser-leak-audit" = "readystack_view_in_browser_leak_audit.__main__:main"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
"readystack_view_in_browser_leak_audit" = ["js/*"]
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.2'
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import os, sys
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main():
|
|
5
|
+
# Runs the same checker as the npm package on the Node.js runtime installed with this package (nodejs-wheel-binaries).
|
|
6
|
+
from nodejs_wheel import node
|
|
7
|
+
here = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
return node([os.path.join(here, "js", "cli.js")] + sys.argv[1:])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
sys.exit(main())
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/cli.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli.js — the same rules as the VS Code extension, run from a terminal or a container. Generated by adapters.py.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs'), path = require('path');
|
|
5
|
+
const RULES = require('./rules.json');
|
|
6
|
+
const S = require('./strings.json');
|
|
7
|
+
const lic = require('./license.js');
|
|
8
|
+
const ENGINE = require('./engine.js'); // s144 — scaffold products: the same engine the VS Code extension runs
|
|
9
|
+
const RULE_LIST = Array.isArray(ENGINE.RULES) ? ENGINE.RULES : (Array.isArray(RULES) ? RULES : []);
|
|
10
|
+
const RULE_N = ENGINE.RULE_COUNT || RULE_LIST.length;
|
|
11
|
+
function scan(text, file) {
|
|
12
|
+
const r = ENGINE.engine.check(String(text), { today: new Date().toISOString().slice(0, 10), path: file || '' });
|
|
13
|
+
return (r && r.findings || []).map((f) => ({ line: parseInt(f.line, 10) || 1, msg: String(f.msg || f.message || f.check || ''), fix: f.fix || null, sev: (String(f.sev || 'error').toLowerCase().startsWith('err') ? 'error' : 'warn') }));
|
|
14
|
+
}
|
|
15
|
+
function isText(p) { try { const b = fs.readFileSync(p); if (b.length > 2 * 1024 * 1024) return false; const s = b.subarray(0, 4096); for (const x of s) if (x === 0) return false; return true; } catch (e) { return false; } }
|
|
16
|
+
function walk(dir, exts, out) {
|
|
17
|
+
let ents = []; try { ents = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return out; }
|
|
18
|
+
for (const e of ents) {
|
|
19
|
+
if (['node_modules', '.git', '.hg', '.svn', '.venv', 'venv', '.tox', '.cache', '.next', '.nuxt', '.terraform', '__pycache__'].includes(e.name)) continue; // s158 — .github · .gitlab-ci.yml · .env · .well-known 은 본다 (옛 줄은 점으로 시작하는 것을 전부 건너뛰어 워크플로 검사기가 .github/workflows 를 못 봤다)
|
|
20
|
+
const p = path.join(dir, e.name);
|
|
21
|
+
if (e.isDirectory()) walk(p, exts, out);
|
|
22
|
+
else if ((!exts.length || exts.includes(path.extname(e.name).toLowerCase())) && isText(p)) out.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
27
|
+
function csvq(s) { return '"' + String(s == null ? '' : s).replace(/"/g, '""') + '"'; }
|
|
28
|
+
function render(rows, fmt) {
|
|
29
|
+
if (fmt === 'json') return JSON.stringify({ tool: S.name, rules: RULE_N, files: rows }, null, 1);
|
|
30
|
+
if (fmt === 'csv') { const o = ['file,line,severity,message,fix']; for (const r of rows) for (const h of r.hits) o.push([csvq(r.file), h.line, h.sev, csvq(h.msg), csvq(h.fix)].join(',')); return o.join('\n') + '\n'; }
|
|
31
|
+
if (fmt === 'html') {
|
|
32
|
+
const n = rows.reduce((a, r) => a + r.hits.length, 0);
|
|
33
|
+
let o = '<!doctype html><meta charset="utf-8"><title>' + esc(S.name) + ' report</title><style>body{font:14px system-ui;margin:24px}table{border-collapse:collapse}td,th{border:1px solid #ddd;padding:4px 8px;font-size:13px}code{font-family:ui-monospace,monospace}</style>';
|
|
34
|
+
o += '<h1>' + esc(S.name) + '</h1><p>' + rows.length + ' files · ' + n + ' findings · ' + RULE_N + ' rules</p><table><tr><th>file</th><th>line</th><th>sev</th><th>message</th><th>fix</th></tr>';
|
|
35
|
+
for (const r of rows) for (const h of r.hits) o += '<tr><td><code>' + esc(r.file) + '</code></td><td>' + h.line + '</td><td>' + esc(h.sev) + '</td><td>' + esc(h.msg) + '</td><td>' + esc(h.fix || '') + '</td></tr>';
|
|
36
|
+
return o + '</table>';
|
|
37
|
+
}
|
|
38
|
+
let o = ''; for (const r of rows) { o += r.file + '\n'; for (const h of r.hits) o += ' ' + String(h.line).padStart(5) + ' ' + h.sev.padEnd(5) + ' ' + h.msg + (h.fix ? '\n -> ' + h.fix : '') + '\n'; if (!r.hits.length) o += ' (no findings)\n'; }
|
|
39
|
+
return o;
|
|
40
|
+
}
|
|
41
|
+
function trialState() {
|
|
42
|
+
// s158 — ⚑7일 무료 없음: 새 체험은 ⛔열지 않는다 · 이미 시작된 체험(trial.json)만 끝까지 지킨다 (읽기 전용)
|
|
43
|
+
if (process.env.READYSTACK_NO_TRIAL) return { active: false };
|
|
44
|
+
try {
|
|
45
|
+
const p = path.join(path.dirname(lic.storePath()), S.bin + '.trial.json');
|
|
46
|
+
const t = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
47
|
+
return { active: !!(t && t.until) && new Date(t.until + 'T23:59:59Z').getTime() > Date.now(), until: t && t.until };
|
|
48
|
+
} catch (e) { return { active: false }; }
|
|
49
|
+
}
|
|
50
|
+
// ★s158 2026-09-23 — the free run SPEAKS about the rest of the folder (vsix: auto.js · crx: welcome tab).
|
|
51
|
+
// The answer for the named files is complete and free; the next step is shown with the user's OWN numbers:
|
|
52
|
+
// "this folder has N more matching files - M issues in K of them" + the one command that sweeps them.
|
|
53
|
+
// Never blocks, never starts a trial (s158: no free trial - the free answer on the named files is the "try it"), human seat only, 400 files / 1.5 s cap.
|
|
54
|
+
function folderHint(done) {
|
|
55
|
+
if (!process.stderr.isTTY || process.env.CI || process.env.READYSTACK_NO_HINT || !done.length) return;
|
|
56
|
+
const base = path.dirname(path.resolve(done[0]));
|
|
57
|
+
const seen = new Set(done.map((f) => path.resolve(f)));
|
|
58
|
+
const all = walk(base, S.exts || [], []).filter((f) => !seen.has(path.resolve(f)));
|
|
59
|
+
if (!all.length) return;
|
|
60
|
+
const t0 = Date.now(); let issues = 0, hitFiles = 0, scanned = 0;
|
|
61
|
+
for (const f of all.slice(0, 400)) { if (Date.now() - t0 > 1500) break; let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } scanned++; const h = scan(t, f); if (h.length) { issues += h.length; hitFiles++; } }
|
|
62
|
+
const rel = path.relative(process.cwd(), base) || '.';
|
|
63
|
+
const tail = trialState().active ? 'free during your trial' : '$' + S.price + ' once';
|
|
64
|
+
const more = all.length + ' more matching file' + (all.length > 1 ? 's' : '');
|
|
65
|
+
const found = issues ? ' - ' + (scanned < all.length ? 'at least ' : '') + issues + ' issue' + (issues > 1 ? 's' : '') + ' in ' + hitFiles + ' of them' : ' - no issues found in them';
|
|
66
|
+
process.stderr.write('\n' + (rel === '.' ? 'This folder' : rel) + ' has ' + more + found + '.\n' + (issues ? 'Sweep them all: ' + S.bin + ' --dir ' + (/\s/.test(rel) ? JSON.stringify(rel) : rel) + ' (' + tail + ')\n' : ''));
|
|
67
|
+
}
|
|
68
|
+
let _usePinged = false;
|
|
69
|
+
function pingUse() {
|
|
70
|
+
try {
|
|
71
|
+
if (_usePinged) return; _usePinged = true;
|
|
72
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return;
|
|
73
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY)) return; // human seat only (s152)
|
|
74
|
+
const https = require('https');
|
|
75
|
+
const body = JSON.stringify({ t: 'use', slug: S.bin, src: 'cli', why: 'free' });
|
|
76
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
77
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + S.bin } }, function (res) { res.resume(); });
|
|
78
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
79
|
+
req.write(body); req.end();
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
}
|
|
82
|
+
function mcpServe() {
|
|
83
|
+
process.env.READYSTACK_MCP = '1'; // s152 - 에이전트(Claude Code·Cursor)가 부른 세션은 사람 자리다 · 키 판 핑 src=mcp
|
|
84
|
+
// s144 — MCP server over stdio (newline-delimited JSON-RPC · no dependencies). Free: check_text · check_file. Licence (s158: no free trial): check_dir.
|
|
85
|
+
const rl = require('readline').createInterface({ input: process.stdin });
|
|
86
|
+
const send = (o) => process.stdout.write(JSON.stringify(o) + '\n');
|
|
87
|
+
const tools = [
|
|
88
|
+
{ name: 'check_text', description: S.name + ' - run all ' + RULE_N + ' checks on a text (free)', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'file contents' }, path: { type: 'string', description: 'optional file name for context' } }, required: ['text'] } },
|
|
89
|
+
{ name: 'check_file', description: S.name + ' - run all checks on one file by path (free)', inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } },
|
|
90
|
+
{ name: 'check_dir', description: S.name + ' - sweep a folder and return every finding (licence; $' + S.price + ' once)', inputSchema: { type: 'object', properties: { dir: { type: 'string' }, ext: { type: 'string', description: 'optional extension filter, e.g. .html' } }, required: ['dir'] } }
|
|
91
|
+
];
|
|
92
|
+
const result = (id, rows) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: render(rows, 'text') }], structuredContent: { tool: S.name, rules: RULE_N, files: rows } } });
|
|
93
|
+
const fail = (id, text) => send({ jsonrpc: '2.0', id, result: { content: [{ type: 'text', text }], isError: true } });
|
|
94
|
+
rl.on('line', async (line) => {
|
|
95
|
+
let m; try { m = JSON.parse(line); } catch (e) { return; }
|
|
96
|
+
const id = m.id, method = m.method;
|
|
97
|
+
if (method === 'initialize') return send({ jsonrpc: '2.0', id, result: { protocolVersion: '2025-06-18', capabilities: { tools: {} }, serverInfo: { name: '@readystack/' + S.bin, version: '1.0.0' } } });
|
|
98
|
+
if (method === 'notifications/initialized' || method === 'ping') { if (id !== undefined) send({ jsonrpc: '2.0', id, result: {} }); return; }
|
|
99
|
+
if (method === 'tools/list') return send({ jsonrpc: '2.0', id, result: { tools } });
|
|
100
|
+
if (method === 'tools/call') {
|
|
101
|
+
const name = (m.params || {}).name, args = (m.params || {}).arguments || {};
|
|
102
|
+
try {
|
|
103
|
+
if (name === 'check_text') return result(id, [{ file: args.path || '(text)', hits: scan(String(args.text || ''), args.path || '') }]);
|
|
104
|
+
if (name === 'check_file') return result(id, [{ file: args.path, hits: scan(fs.readFileSync(args.path, 'utf8'), args.path) }]);
|
|
105
|
+
if (name === 'check_dir') {
|
|
106
|
+
const r = await lic.ensure();
|
|
107
|
+
if (!r.ok && !trialState().active) return fail(id, S.need_key + ' Get a licence ($' + S.price + ', once): ' + lic.BUY_URL);
|
|
108
|
+
const files = walk(args.dir, args.ext ? [args.ext] : (S.exts || []), []);
|
|
109
|
+
return result(id, files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t, f) }; }));
|
|
110
|
+
}
|
|
111
|
+
return send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'unknown tool ' + name } });
|
|
112
|
+
} catch (e) { return fail(id, String(e && e.message || e)); }
|
|
113
|
+
}
|
|
114
|
+
if (id !== undefined) send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'method not found: ' + method } });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function help() {
|
|
118
|
+
return [S.name + ' - ' + S.subtitle, '', 'Usage: ' + S.bin + ' <file> [more files] check the files you name (free, every rule)',
|
|
119
|
+
' ' + S.bin + ' --dir <folder> [--ext .html] scan a whole folder (licence)',
|
|
120
|
+
' ' + S.bin + ' ... --report csv|json|html [--out file] export a report (licence)',
|
|
121
|
+
' ' + S.bin + ' ... --ci exit 1 when an error-level finding exists (licence)',
|
|
122
|
+
' ' + S.bin + ' --license <key> store your licence key (or set READYSTACK_LICENSE)',
|
|
123
|
+
' ' + S.bin + ' --rules list the ' + RULE_N + ' rules',
|
|
124
|
+
' ' + S.bin + ' --mcp run as an MCP server (stdio) for Claude Code / Cursor / Windsurf - free checks, folder sweep needs a licence', '',
|
|
125
|
+
'Free: ' + S.free, 'Licence ($' + S.price + ', once): ' + S.paid, 'Get a licence: ' + lic.BUY_URL, ''].join('\n');
|
|
126
|
+
}
|
|
127
|
+
(async function main() {
|
|
128
|
+
try { const _feed = await lic.pullFeed(); if (_feed && Array.isArray(_feed.rules)) { if (Array.isArray(ENGINE.RULES)) for (const r of _feed.rules) ENGINE.RULES.push(r); } } catch (e) {} // ★s134 구독 피드 병합 (키 있는 손님만)
|
|
129
|
+
const a = process.argv.slice(2);
|
|
130
|
+
const get = (k) => { const i = a.indexOf(k); return i >= 0 ? a[i + 1] : null; };
|
|
131
|
+
if (a.includes('--mcp')) { mcpServe(); return; }
|
|
132
|
+
if (!a.length || a.includes('--help') || a.includes('-h')) { process.stdout.write(help()); return; }
|
|
133
|
+
if (a.includes('--rules')) { process.stdout.write((RULE_LIST.length ? RULE_LIST.map((r, i) => String(i + 1).padStart(3) + ' [' + (r.sev || 'warn') + '] ' + (r.message || r.msg || r.id || '')) : Array.from({ length: RULE_N }, (_, i) => String(i + 1).padStart(3) + ' [engine] check ' + (i + 1))).join('\n') + '\n'); return; }
|
|
134
|
+
if (a.includes('--license')) { const r = await lic.ensure(get('--license')); process.stdout.write(r.ok ? 'Licence stored: ' + lic.storePath() + '\n' : 'Licence not accepted (' + r.why + '). Get one: ' + lic.BUY_URL + '\n'); process.exit(r.ok ? 0 : 2); }
|
|
135
|
+
const dir = get('--dir'), fmt = get('--report'), out = get('--out'), ci = a.includes('--ci');
|
|
136
|
+
const exts = a.includes('--ext') ? [get('--ext')] : (S.exts || []);
|
|
137
|
+
const paid = !!(dir || fmt || ci);
|
|
138
|
+
if (paid) {
|
|
139
|
+
const r = await lic.ensure();
|
|
140
|
+
if (!r.ok) {
|
|
141
|
+
const t = trialState(); // s158 — ⚑7일 무료 없음 · 이미 시작된 체험만 지킨다
|
|
142
|
+
if (t.active) process.stderr.write('Trial: the full run is free until ' + t.until + ' — after that $' + S.price + ' once. Get a licence: ' + lic.BUY_URL + '\n');
|
|
143
|
+
else {
|
|
144
|
+
// s158 — the key is asked WITH the customer's own count (endowment · open loop): how many issues this folder holds.
|
|
145
|
+
let own = '';
|
|
146
|
+
if (dir) { try { const all = walk(dir, exts, []); let n = 0, k = 0, sc = 0; const t0 = Date.now();
|
|
147
|
+
for (const f of all.slice(0, 2000)) { if (Date.now() - t0 > 4000) break; let tx = ''; try { tx = fs.readFileSync(f, 'utf8'); } catch (e) { continue; } sc++; const h = scan(tx, f); if (h.length) { n += h.length; k++; } }
|
|
148
|
+
if (n) own = dir + ': ' + (sc < all.length ? 'at least ' : '') + n + ' issue' + (n > 1 ? 's' : '') + ' in ' + k + ' of ' + all.length + ' files.\n'; } catch (e) {} }
|
|
149
|
+
process.stderr.write(own + S.need_key + '\n set READYSTACK_LICENSE=<key> or ' + S.bin + ' --license <key>\n Get a licence ($' + S.price + ' once): ' + lic.BUY_URL + '\n'); process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const files = dir ? walk(dir, exts, []) : a.filter((x, i) => !x.startsWith('--') && !['--dir', '--report', '--out', '--ext', '--license'].includes(a[i - 1]));
|
|
154
|
+
if (!files.length) { process.stderr.write('No files. ' + S.bin + ' --help\n'); process.exit(2); }
|
|
155
|
+
const rows = files.map((f) => { let t = ''; try { t = fs.readFileSync(f, 'utf8'); } catch (e) { return { file: f, hits: [], error: String(e.message) }; } return { file: f, hits: scan(t) }; });
|
|
156
|
+
const text = render(rows, fmt || 'text');
|
|
157
|
+
if (out) fs.writeFileSync(out, text); else process.stdout.write(text.endsWith('\n') ? text : text + '\n');
|
|
158
|
+
if (!paid) { try { folderHint(files); } catch (e) {} pingUse(); } // s158 — a free run ends with the user's own folder count + one anonymous 'used' count (human seat only)
|
|
159
|
+
const errors = rows.reduce((n, r) => n + r.hits.filter((h) => h.sev === 'error').length, 0);
|
|
160
|
+
if (ci && errors) process.exit(1);
|
|
161
|
+
})().catch((e) => { process.stderr.write(String(e && e.stack || e) + '\n'); process.exit(3); });
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/engine.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* View in Browser — Publish Leak Audit
|
|
2
|
+
* One brain, two homes: this exact file runs in the VS Code extension and in the free web page.
|
|
3
|
+
*/
|
|
4
|
+
(function (root) {
|
|
5
|
+
var isNode = (typeof module !== 'undefined' && module.exports);
|
|
6
|
+
var RULES = isNode ? require('./rules.json') : root.VIL_RULES;
|
|
7
|
+
|
|
8
|
+
function lineOf(text, index) {
|
|
9
|
+
var n = 1;
|
|
10
|
+
for (var i = 0; i < index && i < text.length; i++) if (text.charCodeAt(i) === 10) n++;
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function attrOf(tag, name) {
|
|
15
|
+
var m = new RegExp('\\b' + name + '\\s*=\\s*("([^"]*)"|\'([^\']*)\'|([^\\s>]+))', 'i').exec(tag);
|
|
16
|
+
if (!m) return null;
|
|
17
|
+
return m[2] !== undefined ? m[2] : (m[3] !== undefined ? m[3] : (m[4] || ''));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function scanRegex(rule, text, out) {
|
|
21
|
+
var re = new RegExp(rule.re, 'gi');
|
|
22
|
+
var m;
|
|
23
|
+
while ((m = re.exec(text)) !== null) {
|
|
24
|
+
out.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: lineOf(text, m.index) });
|
|
25
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function scanTagMissingAttr(rule, text, out) {
|
|
30
|
+
var re = new RegExp('<' + rule.tag + '\\b[^>]*>', 'gi');
|
|
31
|
+
var m;
|
|
32
|
+
while ((m = re.exec(text)) !== null) {
|
|
33
|
+
var tag = m[0];
|
|
34
|
+
var trigger = attrOf(tag, rule.when_attr);
|
|
35
|
+
if (trigger === null) continue;
|
|
36
|
+
if (rule.when_re && !new RegExp(rule.when_re, 'i').test(trigger)) continue;
|
|
37
|
+
var have = attrOf(tag, rule.require_attr);
|
|
38
|
+
var ok = rule.require_attr_re
|
|
39
|
+
? (have !== null && new RegExp(rule.require_attr_re, 'i').test(have))
|
|
40
|
+
: (have !== null && have !== '');
|
|
41
|
+
if (!ok) out.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: lineOf(text, m.index) });
|
|
42
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scanYearLtToday(rule, text, opts, out) {
|
|
47
|
+
var year = parseInt(String((opts && opts.today) || '').slice(0, 4), 10);
|
|
48
|
+
if (!year) return;
|
|
49
|
+
var re = new RegExp(rule.re, 'gi');
|
|
50
|
+
var m;
|
|
51
|
+
while ((m = re.exec(text)) !== null) {
|
|
52
|
+
if (parseInt(m[1], 10) < year) {
|
|
53
|
+
out.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: lineOf(text, m.index) });
|
|
54
|
+
}
|
|
55
|
+
if (m.index === re.lastIndex) re.lastIndex++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function check(text, opts) {
|
|
60
|
+
text = String(text == null ? '' : text);
|
|
61
|
+
opts = opts || {};
|
|
62
|
+
var out = [];
|
|
63
|
+
for (var i = 0; i < RULES.length; i++) {
|
|
64
|
+
var rule = RULES[i];
|
|
65
|
+
if (rule.kind === 'tag_missing_attr') scanTagMissingAttr(rule, text, out);
|
|
66
|
+
else if (rule.kind === 'year_lt_today') scanYearLtToday(rule, text, opts, out);
|
|
67
|
+
else scanRegex(rule, text, out);
|
|
68
|
+
}
|
|
69
|
+
out.sort(function (a, b) { return a.line - b.line || a.check.localeCompare(b.check); });
|
|
70
|
+
return { findings: out };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
|
|
74
|
+
root.VILENGINE = api;
|
|
75
|
+
if (isNode) module.exports = api;
|
|
76
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/license.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// license.js — Polar licence check for the CLI / container. Generated by adapters.py; do not edit by hand.
|
|
2
|
+
'use strict';
|
|
3
|
+
const https = require('https'), fs = require('fs'), path = require('path'), os = require('os');
|
|
4
|
+
const ORG_ID = 'a5cdf664-d8e7-4f87-8895-056717aaba17';
|
|
5
|
+
const BENEFIT_ID = 'f6649c56-c479-4e11-93d5-607ea7751ddb'; // s142 — this product's Polar benefit (s140 law: ask with benefit_id, or one key opens every product)
|
|
6
|
+
const BUY_URL = 'https://buy.polar.sh/polar_cl_CwvLDIxujtAKjVYNswBmnY71CVpJXNzshSaBQ2RDuKe';
|
|
7
|
+
const SLUG = 'view-in-browser-leak-audit';
|
|
8
|
+
const GRACE_MS = 30 * 24 * 3600 * 1000; // after a successful check, 30 days work offline
|
|
9
|
+
const RECHECK_MS = 7 * 24 * 3600 * 1000; // re-ask Polar every 7 days (refunds / cancellations)
|
|
10
|
+
function storePath() { return path.join(process.env.READYSTACK_HOME || path.join(os.homedir(), '.config', 'readystack'), SLUG + '.json'); }
|
|
11
|
+
function load() { try { return JSON.parse(fs.readFileSync(storePath(), 'utf8')); } catch (e) { return {}; } }
|
|
12
|
+
function save(o) { try { fs.mkdirSync(path.dirname(storePath()), { recursive: true }); fs.writeFileSync(storePath(), JSON.stringify(o)); } catch (e) { /* read-only home: still works for this run */ } }
|
|
13
|
+
const ALL_BENEFIT_ID = '22692551-5203-4467-b1a3-e33cdba6589d'; // s149 2026-09-17 — 팀 키(전 린터 한 키 · Polar benefit) · 상품 benefit 다음에 한 번 더 묻는다
|
|
14
|
+
function validate(key) {
|
|
15
|
+
return validate1(key, BENEFIT_ID).then(function (r) { return (r.ok || r.offline || !/^[0-9a-f-]{36}$/.test(ALL_BENEFIT_ID)) ? r : validate1(key, ALL_BENEFIT_ID); });
|
|
16
|
+
}
|
|
17
|
+
function validate1(key, ben) {
|
|
18
|
+
return new Promise(function (resolve) {
|
|
19
|
+
if (!ORG_ID) return resolve({ ok: false, offline: false });
|
|
20
|
+
const body = JSON.stringify(/^[0-9a-f-]{36}$/.test(ben) ? { key: key, organization_id: ORG_ID, benefit_id: ben } : { key: key, organization_id: ORG_ID });
|
|
21
|
+
const req = https.request({ hostname: 'api.polar.sh', path: '/v1/customer-portal/license-keys/validate', method: 'POST', timeout: 8000,
|
|
22
|
+
headers: { 'content-type': 'application/json', 'polar-version': '2026-04', 'content-length': Buffer.byteLength(body) } }, function (res) {
|
|
23
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
24
|
+
res.on('end', function () {
|
|
25
|
+
if (res.statusCode !== 200) return resolve({ ok: false, offline: false });
|
|
26
|
+
try { const j = JSON.parse(buf); resolve({ ok: j && (j.status === 'granted' || j.valid === true || !!j.id), offline: false }); }
|
|
27
|
+
catch (e) { resolve({ ok: false, offline: false }); }
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
req.on('timeout', function () { req.destroy(); resolve({ ok: false, offline: true }); });
|
|
31
|
+
req.on('error', function () { resolve({ ok: false, offline: true }); });
|
|
32
|
+
req.write(body); req.end();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// ★s151 2026-09-19 — 키 판(키가 없거나 거절된 순간)을 익명으로 센다 (슬러그·출처·이유만) · DO_NOT_TRACK=1 · READYSTACK_NO_TELEMETRY 면 안 보낸다 · 실패는 조용히 · 프로세스당 한 번.
|
|
36
|
+
let _pinged = false;
|
|
37
|
+
function pingPaywall(why) {
|
|
38
|
+
try {
|
|
39
|
+
if (_pinged) return; _pinged = true;
|
|
40
|
+
if (process.env.DO_NOT_TRACK === '1' || process.env.READYSTACK_NO_TELEMETRY || process.env.CI) return; // s151: CI(깃허브 액션 등)와 우리 빌드는 손님이 아니다
|
|
41
|
+
if (!(process.stdout.isTTY || process.stdin.isTTY || process.env.READYSTACK_MCP === '1')) return; // s152: 사람 자리(터미널·MCP 세션)에서만 센다 - 발행 1분 뒤 남의 실행기(JP · 우리 3대는 US)가 돌린 no_key 13건은 손님이 아니다
|
|
42
|
+
const body = JSON.stringify({ t: 'paywall', slug: SLUG, src: process.env.READYSTACK_MCP === '1' ? 'mcp' : 'cli', why: why || 'no_key' });
|
|
43
|
+
const req = https.request({ hostname: 'getreadystack.com', path: '/api/ev', method: 'POST', timeout: 3000,
|
|
44
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body), 'user-agent': 'readystack-cli/' + SLUG } }, function (res) { res.resume(); });
|
|
45
|
+
req.on('timeout', function () { req.destroy(); }); req.on('error', function () {});
|
|
46
|
+
req.write(body); req.end();
|
|
47
|
+
} catch (e) { /* 세는 것이 실패해도 상품은 돈다 */ }
|
|
48
|
+
}
|
|
49
|
+
async function ensure(explicitKey) {
|
|
50
|
+
const st = load();
|
|
51
|
+
const key = explicitKey || process.env.READYSTACK_LICENSE || st.key;
|
|
52
|
+
if (!key) { pingPaywall('no_key'); return { ok: false, why: 'no_key' }; } // s151
|
|
53
|
+
const age = Date.now() - (st.okAt || 0);
|
|
54
|
+
if (!explicitKey && st.key === key && age < RECHECK_MS) return { ok: true, cached: true };
|
|
55
|
+
const r = await validate(String(key).trim());
|
|
56
|
+
if (r.ok) { save({ key: String(key).trim(), okAt: Date.now() }); return { ok: true }; }
|
|
57
|
+
if (r.offline && st.key === key && age < GRACE_MS) return { ok: true, offline: true };
|
|
58
|
+
if (!r.offline) pingPaywall('invalid'); // s151
|
|
59
|
+
return { ok: false, why: r.offline ? 'offline' : 'invalid' };
|
|
60
|
+
}
|
|
61
|
+
// ★s134 — 구독 규칙 피드(층3 "바뀌면 업데이트"): 키 있는 손님만 · 7일마다 · 오프라인은 캐시. 워커 GET /api/rules/<slug>?key=
|
|
62
|
+
const FEED_URL = 'https://getreadystack.com/api/rules/';
|
|
63
|
+
function pullFeed() {
|
|
64
|
+
const st = load(); const key = process.env.READYSTACK_LICENSE || st.key; const cached = st.feed || null;
|
|
65
|
+
if (!key) return Promise.resolve(cached);
|
|
66
|
+
if (cached && (Date.now() - (st.feedAt || 0)) < RECHECK_MS) return Promise.resolve(cached);
|
|
67
|
+
return new Promise(function (resolve) {
|
|
68
|
+
let req;
|
|
69
|
+
try {
|
|
70
|
+
req = https.get(FEED_URL + encodeURIComponent(SLUG) + '?key=' + encodeURIComponent(key), { timeout: 8000, headers: { 'user-agent': 'readystack-cli' } }, function (res) {
|
|
71
|
+
let buf = ''; res.on('data', function (d) { buf += d; });
|
|
72
|
+
res.on('end', function () {
|
|
73
|
+
if (res.statusCode !== 200) return resolve(cached);
|
|
74
|
+
try { const j = JSON.parse(buf); if (!j || !Array.isArray(j.rules)) return resolve(cached); save(Object.assign(load(), { feed: j, feedAt: Date.now() })); resolve(j); }
|
|
75
|
+
catch (e) { resolve(cached); }
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
} catch (e) { return resolve(cached); }
|
|
79
|
+
req.on('timeout', function () { req.destroy(); resolve(cached); });
|
|
80
|
+
req.on('error', function () { resolve(cached); });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
module.exports = { ensure, BUY_URL, storePath, pullFeed };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@readystack/view-in-browser-leak-audit",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resou",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"view in browser",
|
|
11
|
+
"html",
|
|
12
|
+
"preview",
|
|
13
|
+
"api key",
|
|
14
|
+
"secret",
|
|
15
|
+
"localhost",
|
|
16
|
+
"publish",
|
|
17
|
+
"static site",
|
|
18
|
+
"audit",
|
|
19
|
+
"leak"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://getreadystack.com",
|
|
22
|
+
"funding": "https://buy.polar.sh/polar_cl_CwvLDIxujtAKjVYNswBmnY71CVpJXNzshSaBQ2RDuKe",
|
|
23
|
+
"bin": {
|
|
24
|
+
"view-in-browser-leak-audit": "cli.js"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"mcpName": "io.github.jmshinhwa/view-in-browser-leak-audit",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/jmshinhwa/readystack-themes.git",
|
|
33
|
+
"directory": "view-in-browser-leak-audit"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"cli.js",
|
|
37
|
+
"license.js",
|
|
38
|
+
"rules.json",
|
|
39
|
+
"strings.json",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE.txt",
|
|
42
|
+
"engine.js"
|
|
43
|
+
]
|
|
44
|
+
}
|
readystack_view_in_browser_leak_audit-1.0.2/src/readystack_view_in_browser_leak_audit/js/rules.json
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "secret_google_api_key",
|
|
4
|
+
"sev": "high",
|
|
5
|
+
"kind": "regex",
|
|
6
|
+
"re": "AIza[0-9A-Za-z_\\-]{35}",
|
|
7
|
+
"msg": "Google API key literal sits in the page source — anyone who opens View Source has it. Restrict the key by HTTP referrer or move the call server-side before you publish.",
|
|
8
|
+
"consts": {
|
|
9
|
+
"sev_order": [
|
|
10
|
+
"high",
|
|
11
|
+
"med",
|
|
12
|
+
"low"
|
|
13
|
+
],
|
|
14
|
+
"rail": "local preview -> public host",
|
|
15
|
+
"kit": "readystack"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "secret_aws_access_key",
|
|
20
|
+
"sev": "high",
|
|
21
|
+
"kind": "regex",
|
|
22
|
+
"re": "AKIA[0-9A-Z]{16}",
|
|
23
|
+
"msg": "AWS access key id in client-side HTML. Rotate it and sign requests from a backend — a browser page cannot keep this secret."
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "secret_stripe_live",
|
|
27
|
+
"sev": "high",
|
|
28
|
+
"kind": "regex",
|
|
29
|
+
"re": "sk_live_[0-9a-zA-Z]{10,}",
|
|
30
|
+
"msg": "Stripe SECRET key (sk_live_) in the page. Only pk_ publishable keys belong in a browser. Roll this key in the Stripe dashboard now."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "secret_slack_token",
|
|
34
|
+
"sev": "high",
|
|
35
|
+
"kind": "regex",
|
|
36
|
+
"re": "xox[baprs]-[0-9A-Za-z-]{10,}",
|
|
37
|
+
"msg": "Slack token in the page source. Revoke it and call Slack from a server, not from the visitor's browser."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "secret_private_key_block",
|
|
41
|
+
"sev": "high",
|
|
42
|
+
"kind": "regex",
|
|
43
|
+
"re": "-----BEGIN [A-Z ]*PRIVATE KEY-----",
|
|
44
|
+
"msg": "A PEM private key block is pasted into this HTML file. Delete it, rotate the key pair, and never ship a private key to a static host."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "secret_firebase_apikey",
|
|
48
|
+
"sev": "high",
|
|
49
|
+
"kind": "regex",
|
|
50
|
+
"re": "apiKey\\s*:\\s*[\\\"'][^\\\"']{15,}[\\\"']",
|
|
51
|
+
"msg": "Firebase-style apiKey literal in inline script. Lock the project with security rules and App Check — a published config without rules is an open database."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "secret_generic_assignment",
|
|
55
|
+
"sev": "high",
|
|
56
|
+
"kind": "regex",
|
|
57
|
+
"re": "(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|password)\\s*[:=]\\s*[\\\"'][^\\\"'\\s]{12,}[\\\"']",
|
|
58
|
+
"msg": "A credential is assigned to a literal string in this page. Move it behind a request your server makes, then rotate the value you just exposed."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "basic_auth_in_url",
|
|
62
|
+
"sev": "high",
|
|
63
|
+
"kind": "regex",
|
|
64
|
+
"re": "https?://[A-Za-z0-9._%+-]+:[^\\s\\\"'/@]+@",
|
|
65
|
+
"msg": "user:password embedded in a URL. Credentials in a URL leak through history, logs and the Referer header. Remove them before publishing."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "localhost_url",
|
|
69
|
+
"sev": "high",
|
|
70
|
+
"kind": "regex",
|
|
71
|
+
"re": "https?://(?:localhost|127\\.0\\.0\\.1)(?::\\d+)?",
|
|
72
|
+
"msg": "A localhost URL works in your preview and dies for every visitor. Point it at the public origin, or make it a relative path."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "private_lan_url",
|
|
76
|
+
"sev": "high",
|
|
77
|
+
"kind": "regex",
|
|
78
|
+
"re": "https?://(?:10\\.\\d|192\\.168\\.|172\\.(?:1[6-9]|2\\d|3[01])\\.)",
|
|
79
|
+
"msg": "A private LAN address is hard-coded here. It resolves on your machine only, and it tells strangers your internal network layout."
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "file_scheme_url",
|
|
83
|
+
"sev": "high",
|
|
84
|
+
"kind": "regex",
|
|
85
|
+
"re": "file:///",
|
|
86
|
+
"msg": "A file:/// URL only opens from your own disk. Browsers block it from a web page, so this asset will be missing once published."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "local_disk_path",
|
|
90
|
+
"sev": "high",
|
|
91
|
+
"kind": "regex",
|
|
92
|
+
"re": "(?:src|href)\\s*=\\s*[\\\"'](?:[A-Za-z]:\\\\|/Users/|/home/|/tmp/)",
|
|
93
|
+
"msg": "An absolute path from your own disk is used as src/href. It breaks on the host and it prints your username into the page."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "mixed_content_resource",
|
|
97
|
+
"sev": "med",
|
|
98
|
+
"kind": "regex",
|
|
99
|
+
"re": "(?:src|href)\\s*=\\s*[\\\"']http://(?!localhost|127\\.0\\.0\\.1)",
|
|
100
|
+
"msg": "Plain http:// resource on a page that will be served over https. Browsers block or downgrade it — switch the URL to https://."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "insecure_form_action",
|
|
104
|
+
"sev": "high",
|
|
105
|
+
"kind": "regex",
|
|
106
|
+
"re": "<form\\b[^>]*\\baction\\s*=\\s*[\\\"']http://",
|
|
107
|
+
"msg": "Form posts over plain http://. Whatever the visitor types travels in clear text. Change the action to https://."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "robots_noindex",
|
|
111
|
+
"sev": "high",
|
|
112
|
+
"kind": "regex",
|
|
113
|
+
"re": "<meta\\b[^>]*name\\s*=\\s*[\\\"']robots[\\\"'][^>]*noindex",
|
|
114
|
+
"msg": "A leftover robots noindex tag. The page will publish fine and then never appear in search — the failure nobody notices for weeks."
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"id": "cdn_unpinned_latest",
|
|
118
|
+
"sev": "med",
|
|
119
|
+
"kind": "regex",
|
|
120
|
+
"re": "src\\s*=\\s*[\\\"']https://(?:cdn\\.jsdelivr\\.net|unpkg\\.com)/[^\\\"']*@latest",
|
|
121
|
+
"msg": "Third-party script pinned to @latest. The vendor can change your live page at any moment. Pin an exact version."
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "source_map_ref",
|
|
125
|
+
"sev": "med",
|
|
126
|
+
"kind": "regex",
|
|
127
|
+
"re": "//#\\s*sourceMappingURL=",
|
|
128
|
+
"msg": "A source map reference is shipped with the page, handing readers your unminified source. Drop it from the production build."
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "commented_secret",
|
|
132
|
+
"sev": "med",
|
|
133
|
+
"kind": "regex",
|
|
134
|
+
"re": "<!--[^>]*?(?:api[_-]?key|token|password|secret)",
|
|
135
|
+
"msg": "A credential is sitting inside an HTML comment. Comments ship to the browser — commenting a secret out does not hide it."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"id": "placeholder_left",
|
|
139
|
+
"sev": "med",
|
|
140
|
+
"kind": "regex",
|
|
141
|
+
"re": "(?:YOUR_API_KEY|CHANGE_?ME|FIXME|TODO)",
|
|
142
|
+
"msg": "A placeholder or TODO marker survived into the page you are about to publish. Fill it in or delete the block."
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "console_debug_left",
|
|
146
|
+
"sev": "low",
|
|
147
|
+
"kind": "regex",
|
|
148
|
+
"re": "console\\.(?:log|debug|table|dir)\\s*\\(",
|
|
149
|
+
"msg": "Debug console output left in the page. It is noise at best and it prints your data objects to anyone with DevTools open."
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"id": "debug_flag_true",
|
|
153
|
+
"sev": "low",
|
|
154
|
+
"kind": "regex",
|
|
155
|
+
"re": "\\b(?:DEBUG|VERBOSE|IS_DEV)\\s*[:=]\\s*true\\b",
|
|
156
|
+
"msg": "A debug flag is still switched on. Set it to false so the published page does not run its development branch."
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"id": "external_script_no_sri",
|
|
160
|
+
"sev": "med",
|
|
161
|
+
"kind": "tag_missing_attr",
|
|
162
|
+
"tag": "script",
|
|
163
|
+
"when_attr": "src",
|
|
164
|
+
"when_re": "^https?://",
|
|
165
|
+
"require_attr": "integrity",
|
|
166
|
+
"msg": "External script loaded with no integrity hash. If that CDN account is ever taken over, the attacker runs code on your page. Add integrity and crossorigin."
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
"id": "blank_link_no_noopener",
|
|
170
|
+
"sev": "low",
|
|
171
|
+
"kind": "tag_missing_attr",
|
|
172
|
+
"tag": "a",
|
|
173
|
+
"when_attr": "target",
|
|
174
|
+
"when_re": "^_blank$",
|
|
175
|
+
"require_attr": "rel",
|
|
176
|
+
"require_attr_re": "noopener",
|
|
177
|
+
"msg": "target=\"_blank\" without rel=\"noopener\". The page you link to can reach back into this tab. Add rel=\"noopener\"."
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "stale_year_literal",
|
|
181
|
+
"sev": "low",
|
|
182
|
+
"kind": "year_lt_today",
|
|
183
|
+
"re": "(?:©|©|Copyright)\\s*(?:©\\s*)?(\\d{4})",
|
|
184
|
+
"msg": "The copyright year in this page is older than today. Visitors read a stale year as an abandoned site; generated markup gets this wrong constantly."
|
|
185
|
+
}
|
|
186
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "View in Browser — Publish Leak Audit (HTML)",
|
|
3
|
+
"subtitle": "24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resources, a leftover noindex.",
|
|
4
|
+
"bin": "view-in-browser-leak-audit",
|
|
5
|
+
"price": 29,
|
|
6
|
+
"free": "Audit the HTML file you are about to open in the browser: all 24 rules, every finding with its severity, line number and fix. The clean fixture returns 0 findings, the dirty one returns 27. No key, no watermark, no locked answers.",
|
|
7
|
+
"paid": "Scope: one command sweeps every HTML file in the workspace instead of the one in front of you, and writes a dated LEAK-AUDIT.md you keep as evidence — forty pages in one pass instead of forty passes.",
|
|
8
|
+
"need_key": "This option needs a licence (View in Browser — Publish Leak Audit (HTML)).",
|
|
9
|
+
"exts": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: readystack-view-in-browser-leak-audit
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: 24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resou (bundled Node.js runtime)
|
|
5
|
+
Author-email: ReadyStack <support@getreadystack.com>
|
|
6
|
+
License: Proprietary - see LICENSE.txt
|
|
7
|
+
Project-URL: Homepage, https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi
|
|
8
|
+
Project-URL: Documentation, https://getreadystack.com/tools/view-in-browser-leak-audit?ref=pypi
|
|
9
|
+
Project-URL: Support, https://getreadystack.com/support
|
|
10
|
+
Keywords: view in browser,html,preview,api key,secret,localhost,publish,static site,audit,leak
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Requires-Dist: nodejs-wheel-binaries>=20
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# View in Browser — Publish Leak Audit (HTML)
|
|
22
|
+
|
|
23
|
+
> Runs the same checker as the npm package `@readystack/view-in-browser-leak-audit` on a Node.js runtime that pip installs for you (`nodejs-wheel-binaries`) - no system Node.js needed. Your files are checked locally.
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
24 rules read the .html file you are about to open in the browser and flag every line that behaves differently once the page has a public URL: live API keys, localhost and file:// paths, http:// resources, a leftover noindex.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
view-in-browser-leak-audit file
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Node 18+. The same 24 rules as the VS Code extension, from a terminal or CI.
|
|
36
|
+
|
|
37
|
+
## Free
|
|
38
|
+
|
|
39
|
+
- Audit the HTML file you are about to open in the browser: all 24 rules, every finding with its severity, line number and fix. The clean fixture returns 0 findings, the dirty one returns 27. No key, no watermark, no locked answers.
|
|
40
|
+
- `--rules` lists every rule
|
|
41
|
+
|
|
42
|
+
## With a licence ($29 once)
|
|
43
|
+
|
|
44
|
+
- Scope: one command sweeps every HTML file in the workspace instead of the one in front of you, and writes a dated LEAK-AUDIT.md you keep as evidence — forty pages in one pass instead of forty passes.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
@readystack/view-in-browser-leak-audit --dir ./templates --report html --out report.html
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
In February 2026 a developer reported an $82,000 Google Cloud bill after a key embedded in client-side Google Maps code was taken from the page source; their normal spend was $180 a month.
|
|
51
|
+
|
|
52
|
+
## Use from an AI agent (MCP)
|
|
53
|
+
|
|
54
|
+
Claude Code · Cursor · Windsurf · any MCP client - add to your MCP config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "mcpServers": { "view-in-browser-leak-audit": { "command": "npx", "args": ["-y", "@readystack/view-in-browser-leak-audit", "--mcp"] } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Tools: `check_text` and `check_file` (free) · `check_dir` (licence). The agent gets every finding with the line number.
|
|
61
|
+
|
|
62
|
+
## Use in CI
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
- name: View in Browser — Publish Leak Audit (HTML)
|
|
66
|
+
run: npx -y @readystack/view-in-browser-leak-audit --dir . --ci
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
(container: `docker run --rm -v "$PWD:/work" getreadystack/view-in-browser-leak-audit --dir /work --ci`)
|
|
70
|
+
|
|
71
|
+
The folder sweep, reports and CI mode need one licence — one payment, no subscription. Set `READYSTACK_LICENSE=<key>` or run `--license <key>` once.
|
|
72
|
+
|
|
73
|
+
[Get a licence](https://buy.polar.sh/polar_cl_CwvLDIxujtAKjVYNswBmnY71CVpJXNzshSaBQ2RDuKe)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
<!-- view in browser leak audit -->
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Install (PyPI)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
pip install readystack-view-in-browser-leak-audit
|
|
83
|
+
view-in-browser-leak-audit --help
|
|
84
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/readystack_view_in_browser_leak_audit/__init__.py
|
|
5
|
+
src/readystack_view_in_browser_leak_audit/__main__.py
|
|
6
|
+
src/readystack_view_in_browser_leak_audit.egg-info/PKG-INFO
|
|
7
|
+
src/readystack_view_in_browser_leak_audit.egg-info/SOURCES.txt
|
|
8
|
+
src/readystack_view_in_browser_leak_audit.egg-info/dependency_links.txt
|
|
9
|
+
src/readystack_view_in_browser_leak_audit.egg-info/entry_points.txt
|
|
10
|
+
src/readystack_view_in_browser_leak_audit.egg-info/requires.txt
|
|
11
|
+
src/readystack_view_in_browser_leak_audit.egg-info/top_level.txt
|
|
12
|
+
src/readystack_view_in_browser_leak_audit/js/cli.js
|
|
13
|
+
src/readystack_view_in_browser_leak_audit/js/engine.js
|
|
14
|
+
src/readystack_view_in_browser_leak_audit/js/license.js
|
|
15
|
+
src/readystack_view_in_browser_leak_audit/js/package.json
|
|
16
|
+
src/readystack_view_in_browser_leak_audit/js/rules.json
|
|
17
|
+
src/readystack_view_in_browser_leak_audit/js/strings.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodejs-wheel-binaries>=20
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
readystack_view_in_browser_leak_audit
|