cloak-cli 0.1.2__tar.gz → 0.2.0__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.
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/CHANGELOG.md +8 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/PKG-INFO +2 -2
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/README.md +1 -1
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/__init__.py +1 -1
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/cli.py +1 -1
- cloak_cli-0.2.0/src/cloak/obfuscate/js_transformer.py +175 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/obfuscate/runner.py +22 -3
- cloak_cli-0.2.0/tests/test_obfuscate_js.py +201 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/.cloakpolicy.example +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/.github/workflows/ci.yml +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/.github/workflows/release.yml +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/.gitignore +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/CONTRIBUTING.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/LICENSE +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/AGENT_INTEGRATION.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/BUILD_PLAN.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/COMPETITOR_RESEARCH.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/PROMPTS.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/quotecraft.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/quotecraft.redacted.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/result_prompt1.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/docs/research/result_prompt2.md +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/pyproject.toml +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/__main__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/context/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/context/generator.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/context/js_redactor.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/filesystem.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/obfuscate/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/obfuscate/manifest.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/obfuscate/transformer.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/policy.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/policy_init.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/scan/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/src/cloak/scan/scanner.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/__init__.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/test_cli.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/test_context.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/test_context_js.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/test_obfuscate.py +0 -0
- {cloak_cli-0.1.2 → cloak_cli-0.2.0}/tests/test_policy_init.py +0 -0
|
@@ -6,6 +6,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-05-08
|
|
10
|
+
|
|
11
|
+
Feature matrix complete: all three headline commands now work for both Python and JS/TS.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Phase 5: `cloak obfuscate` now handles JavaScript and TypeScript files, dispatched by extension. Per-file rename of module-level identifiers starting with `_` (function declarations, class declarations, generator functions, `const`/`let`/`var` simple bindings) using tree-sitter for parsing and byte-splice for output. Skips dunder-like names (`__version`), all-underscore names, and anything matching `policy.public_api` (with trailing-`*` wildcard support). Property access (`obj._foo`), shorthand object properties, and destructuring patterns are deliberately not renamed in v1 — they would silently change object shapes and risk runtime breakage. Limitations are caught by `--verify` (cross-file imports, local-variable shadowing of module-level `_names`).
|
|
15
|
+
- `cloak obfuscate` against a mixed Python+JS repo now transforms both languages in one pass with one manifest, one rename map, and one verify command.
|
|
16
|
+
|
|
9
17
|
## [0.1.2] - 2026-05-08
|
|
10
18
|
|
|
11
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloak-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Local CLI for safer LLM workflows: redact code before pasting, generate verified obfuscated copies, enforce policy from your repo.
|
|
5
5
|
Project-URL: Homepage, https://github.com/newtophilly/cloak
|
|
6
6
|
Project-URL: Repository, https://github.com/newtophilly/cloak
|
|
@@ -385,7 +385,7 @@ CLOAK is designed to be called as a subprocess from other developer tools and AI
|
|
|
385
385
|
| 3 | `cloak context` for Python | ✅ Done |
|
|
386
386
|
| 3.5 | `cloak context` JS/TS via tree-sitter | ✅ Done |
|
|
387
387
|
| 4 | `cloak obfuscate` Python with `--verify` | ✅ Done (v1) |
|
|
388
|
-
| 5 | `cloak obfuscate` JS/TS
|
|
388
|
+
| 5 | `cloak obfuscate` JS/TS via tree-sitter | ✅ Done (v1) |
|
|
389
389
|
| 6 | `cloak eval` (LLM-prompt-based regression harness) | ⏳ |
|
|
390
390
|
|
|
391
391
|
## Contributing
|
|
@@ -144,7 +144,7 @@ CLOAK is designed to be called as a subprocess from other developer tools and AI
|
|
|
144
144
|
| 3 | `cloak context` for Python | ✅ Done |
|
|
145
145
|
| 3.5 | `cloak context` JS/TS via tree-sitter | ✅ Done |
|
|
146
146
|
| 4 | `cloak obfuscate` Python with `--verify` | ✅ Done (v1) |
|
|
147
|
-
| 5 | `cloak obfuscate` JS/TS
|
|
147
|
+
| 5 | `cloak obfuscate` JS/TS via tree-sitter | ✅ Done (v1) |
|
|
148
148
|
| 6 | `cloak eval` (LLM-prompt-based regression harness) | ⏳ |
|
|
149
149
|
|
|
150
150
|
## Contributing
|
|
@@ -247,7 +247,7 @@ def _emit_obfuscate_terminal(policy: Policy, result: ObfuscateResult) -> None:
|
|
|
247
247
|
"[bold green]✓ Obfuscated[/bold green]",
|
|
248
248
|
f"output: {result.output_dir}",
|
|
249
249
|
f"manifest: {result.manifest_path}",
|
|
250
|
-
f"transformed: {result.files_transformed}
|
|
250
|
+
f"transformed: {result.files_transformed} source files",
|
|
251
251
|
f"copied: {result.files_copied} other files",
|
|
252
252
|
f"renames: {len(result.rename_map)} module-private identifiers",
|
|
253
253
|
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Phase 5 — JS/TS obfuscation transformer.
|
|
2
|
+
|
|
3
|
+
Per-file rename of module-level identifiers that start with `_` (and aren't dunder-like or
|
|
4
|
+
all underscores). Same shape as the Python obfuscator: collect candidate names, build a
|
|
5
|
+
deterministic rename map (`_a000`, `_a001`, ...), then byte-splice every reference of those
|
|
6
|
+
names in the file with their new identifier.
|
|
7
|
+
|
|
8
|
+
Tree-sitter is used as a parser only — we don't unparse. Edits happen at the byte level so
|
|
9
|
+
all formatting and comments outside renamed identifiers survive.
|
|
10
|
+
|
|
11
|
+
v1 limitations (deferred — caught by `--verify`):
|
|
12
|
+
- Cross-file rename. If `import { _helper } from './lib'`, the import keeps `_helper` but
|
|
13
|
+
the target file renames it; tests will fail and the user knows immediately.
|
|
14
|
+
- Local-variable shadowing. If a function body defines `const _helper = ...` shadowing a
|
|
15
|
+
module-level `_helper`, both get renamed; tests will catch logic bugs that arise.
|
|
16
|
+
- Shorthand property in object literals (`{ _helper }`) and destructuring patterns
|
|
17
|
+
(`const { _helper } = ...`) are NOT renamed (would change object shape silently). This
|
|
18
|
+
means a module-level `_helper` referenced via shorthand goes un-renamed there.
|
|
19
|
+
- Class methods are NOT renamed (could be public API).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
|
|
24
|
+
from tree_sitter import Node, Parser
|
|
25
|
+
|
|
26
|
+
from cloak.context.js_redactor import JsLikeKind, _get_language
|
|
27
|
+
from cloak.policy import Policy
|
|
28
|
+
|
|
29
|
+
_RENAMEABLE_IDENTIFIER_TYPES = {"identifier", "type_identifier"}
|
|
30
|
+
|
|
31
|
+
_DEFINITION_NODE_TYPES = {
|
|
32
|
+
"function_declaration",
|
|
33
|
+
"generator_function_declaration",
|
|
34
|
+
"class_declaration",
|
|
35
|
+
"lexical_declaration",
|
|
36
|
+
"variable_declaration",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class JsTransformResult:
|
|
42
|
+
"""Result of transforming a single JS/TS file."""
|
|
43
|
+
|
|
44
|
+
source_text: str
|
|
45
|
+
output_text: str
|
|
46
|
+
rename_map: dict[str, str] = field(default_factory=dict)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transform_js_like_source(
|
|
50
|
+
source: str,
|
|
51
|
+
kind: JsLikeKind,
|
|
52
|
+
policy: Policy,
|
|
53
|
+
) -> JsTransformResult:
|
|
54
|
+
"""Apply v1 obfuscation to a JS/TS source string. Returns transformed text + rename map."""
|
|
55
|
+
src_bytes = source.encode("utf-8")
|
|
56
|
+
parser = Parser(_get_language(kind))
|
|
57
|
+
tree = parser.parse(src_bytes)
|
|
58
|
+
|
|
59
|
+
names = _collect_top_level_private_names(tree.root_node, src_bytes, policy)
|
|
60
|
+
rename_map = _build_rename_map(names)
|
|
61
|
+
|
|
62
|
+
if not rename_map:
|
|
63
|
+
return JsTransformResult(source_text=source, output_text=source)
|
|
64
|
+
|
|
65
|
+
edits: list[tuple[int, int, bytes]] = []
|
|
66
|
+
_collect_rename_edits(tree.root_node, src_bytes, rename_map, edits)
|
|
67
|
+
|
|
68
|
+
if not edits:
|
|
69
|
+
return JsTransformResult(source_text=source, output_text=source, rename_map=rename_map)
|
|
70
|
+
|
|
71
|
+
edits.sort(key=lambda e: e[0], reverse=True)
|
|
72
|
+
out = bytearray(src_bytes)
|
|
73
|
+
for start, end, replacement in edits:
|
|
74
|
+
out[start:end] = replacement
|
|
75
|
+
return JsTransformResult(
|
|
76
|
+
source_text=source,
|
|
77
|
+
output_text=out.decode("utf-8", errors="replace"),
|
|
78
|
+
rename_map=rename_map,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _collect_top_level_private_names(root_node: Node, src: bytes, policy: Policy) -> list[str]:
|
|
83
|
+
"""Find top-level definitions whose name starts with `_`. Skips dunders, all-underscore."""
|
|
84
|
+
names: list[str] = []
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
public_api = set(policy.public_api)
|
|
87
|
+
|
|
88
|
+
for child in root_node.children:
|
|
89
|
+
# `export ...` wraps a real declaration — peek through one level.
|
|
90
|
+
target = child
|
|
91
|
+
if child.type == "export_statement":
|
|
92
|
+
for inner in child.children:
|
|
93
|
+
if inner.type in _DEFINITION_NODE_TYPES:
|
|
94
|
+
target = inner
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
for name in _extract_definition_names(target, src):
|
|
98
|
+
if _should_rename(name, public_api) and name not in seen:
|
|
99
|
+
seen.add(name)
|
|
100
|
+
names.append(name)
|
|
101
|
+
|
|
102
|
+
return names
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _extract_definition_names(node: Node, src: bytes) -> list[str]:
|
|
106
|
+
"""Return the symbol names this top-level definition introduces."""
|
|
107
|
+
if node.type in {
|
|
108
|
+
"function_declaration",
|
|
109
|
+
"generator_function_declaration",
|
|
110
|
+
"class_declaration",
|
|
111
|
+
}:
|
|
112
|
+
name_node = node.child_by_field_name("name")
|
|
113
|
+
if name_node is not None:
|
|
114
|
+
return [_text(name_node, src)]
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
if node.type in {"lexical_declaration", "variable_declaration"}:
|
|
118
|
+
result: list[str] = []
|
|
119
|
+
for declarator in node.children:
|
|
120
|
+
if declarator.type != "variable_declarator":
|
|
121
|
+
continue
|
|
122
|
+
name_node = declarator.child_by_field_name("name")
|
|
123
|
+
# Only handle the simple `const _foo = ...` case. Destructuring patterns
|
|
124
|
+
# (object_pattern, array_pattern) are skipped — too risky to rename in v1.
|
|
125
|
+
if name_node is not None and name_node.type == "identifier":
|
|
126
|
+
result.append(_text(name_node, src))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _should_rename(name: str, public_api: set[str]) -> bool:
|
|
133
|
+
if not name.startswith("_"):
|
|
134
|
+
return False
|
|
135
|
+
if name.startswith("__"):
|
|
136
|
+
# Dunder-like; conservative skip (e.g. `__dirname` in CommonJS).
|
|
137
|
+
return False
|
|
138
|
+
if all(c == "_" for c in name):
|
|
139
|
+
return False
|
|
140
|
+
if name in public_api:
|
|
141
|
+
return False
|
|
142
|
+
for pattern in public_api:
|
|
143
|
+
if pattern.endswith("*") and name.startswith(pattern.rstrip("*")):
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_rename_map(names: list[str]) -> dict[str, str]:
|
|
149
|
+
return {name: f"_a{i:03d}" for i, name in enumerate(names)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _collect_rename_edits(
|
|
153
|
+
node: Node,
|
|
154
|
+
src: bytes,
|
|
155
|
+
rename_map: dict[str, str],
|
|
156
|
+
edits: list[tuple[int, int, bytes]],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Walk the tree; replace `identifier`/`type_identifier` nodes whose text is in the map.
|
|
159
|
+
|
|
160
|
+
Critically: we do NOT touch `property_identifier` (member access — `.foo` is a different
|
|
161
|
+
scope), `shorthand_property_identifier` (would silently change object shape), or
|
|
162
|
+
destructuring identifiers — see v1 limitations in module docstring.
|
|
163
|
+
"""
|
|
164
|
+
if node.type in _RENAMEABLE_IDENTIFIER_TYPES:
|
|
165
|
+
text = _text(node, src)
|
|
166
|
+
if text in rename_map:
|
|
167
|
+
edits.append((node.start_byte, node.end_byte, rename_map[text].encode("utf-8")))
|
|
168
|
+
return # leaf
|
|
169
|
+
|
|
170
|
+
for child in node.children:
|
|
171
|
+
_collect_rename_edits(child, src, rename_map, edits)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _text(node: Node, src: bytes) -> str:
|
|
175
|
+
return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
@@ -17,7 +17,9 @@ from dataclasses import dataclass, field
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
|
|
19
19
|
from cloak import __version__
|
|
20
|
+
from cloak.context.js_redactor import language_for_extension
|
|
20
21
|
from cloak.filesystem import walk_repo
|
|
22
|
+
from cloak.obfuscate.js_transformer import transform_js_like_source
|
|
21
23
|
from cloak.obfuscate.manifest import Manifest
|
|
22
24
|
from cloak.obfuscate.transformer import transform_python_source
|
|
23
25
|
from cloak.policy import Policy
|
|
@@ -81,6 +83,8 @@ def run_obfuscate(
|
|
|
81
83
|
|
|
82
84
|
source_hashes[str(rel)] = _sha256_file(src_file)
|
|
83
85
|
|
|
86
|
+
js_like_kind = language_for_extension(src_file.suffix)
|
|
87
|
+
|
|
84
88
|
if src_file.suffix == ".py":
|
|
85
89
|
try:
|
|
86
90
|
source_text = src_file.read_text(encoding="utf-8")
|
|
@@ -91,7 +95,7 @@ def run_obfuscate(
|
|
|
91
95
|
continue
|
|
92
96
|
|
|
93
97
|
try:
|
|
94
|
-
|
|
98
|
+
py_result = transform_python_source(source_text, policy, file_label=str(rel))
|
|
95
99
|
except SyntaxError:
|
|
96
100
|
# Don't break on un-parseable files. Copy them through unchanged.
|
|
97
101
|
shutil.copy2(src_file, out_file)
|
|
@@ -99,9 +103,24 @@ def run_obfuscate(
|
|
|
99
103
|
files_copied += 1
|
|
100
104
|
continue
|
|
101
105
|
|
|
102
|
-
out_file.write_text(
|
|
106
|
+
out_file.write_text(py_result.output_text, encoding="utf-8")
|
|
107
|
+
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
108
|
+
for orig, new in py_result.rename_map.items():
|
|
109
|
+
rename_map_global[f"{rel}:{orig}"] = new
|
|
110
|
+
files_transformed += 1
|
|
111
|
+
elif js_like_kind is not None:
|
|
112
|
+
try:
|
|
113
|
+
source_text = src_file.read_text(encoding="utf-8")
|
|
114
|
+
except (UnicodeDecodeError, OSError):
|
|
115
|
+
shutil.copy2(src_file, out_file)
|
|
116
|
+
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
117
|
+
files_copied += 1
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
js_result = transform_js_like_source(source_text, js_like_kind, policy)
|
|
121
|
+
out_file.write_text(js_result.output_text, encoding="utf-8")
|
|
103
122
|
output_hashes[str(rel)] = _sha256_file(out_file)
|
|
104
|
-
for orig, new in
|
|
123
|
+
for orig, new in js_result.rename_map.items():
|
|
105
124
|
rename_map_global[f"{rel}:{orig}"] = new
|
|
106
125
|
files_transformed += 1
|
|
107
126
|
else:
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Tests for `cloak obfuscate` JS/TS support (Phase 5)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from cloak.cli import app
|
|
9
|
+
from cloak.obfuscate.js_transformer import transform_js_like_source
|
|
10
|
+
from cloak.obfuscate.runner import run_obfuscate
|
|
11
|
+
from cloak.policy import Policy
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------- transformer unit tests ----------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_renames_module_level_function() -> None:
|
|
20
|
+
src = (
|
|
21
|
+
"function _helper(x) {\n"
|
|
22
|
+
" return x + 1;\n"
|
|
23
|
+
"}\n"
|
|
24
|
+
"\n"
|
|
25
|
+
"export function publicFn(x) {\n"
|
|
26
|
+
" return _helper(x);\n"
|
|
27
|
+
"}\n"
|
|
28
|
+
)
|
|
29
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
30
|
+
assert "_helper" not in result.output_text
|
|
31
|
+
assert "_a000" in result.output_text
|
|
32
|
+
# Public name preserved
|
|
33
|
+
assert "publicFn" in result.output_text
|
|
34
|
+
# Reference inside publicFn correctly rewritten
|
|
35
|
+
assert "_a000(x)" in result.output_text
|
|
36
|
+
assert result.rename_map == {"_helper": "_a000"}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_renames_module_level_const() -> None:
|
|
40
|
+
src = "const _internal = 42;\nexport const value = _internal * 2;\n"
|
|
41
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
42
|
+
assert "_internal" not in result.output_text
|
|
43
|
+
assert "_a000" in result.output_text
|
|
44
|
+
assert "value = _a000 * 2" in result.output_text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_renames_module_level_class() -> None:
|
|
48
|
+
src = (
|
|
49
|
+
"class _Internal {\n"
|
|
50
|
+
" constructor() { this.x = 1; }\n"
|
|
51
|
+
"}\n"
|
|
52
|
+
"\n"
|
|
53
|
+
"export function make() {\n"
|
|
54
|
+
" return new _Internal();\n"
|
|
55
|
+
"}\n"
|
|
56
|
+
)
|
|
57
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
58
|
+
assert "_Internal" not in result.output_text
|
|
59
|
+
assert "_a000" in result.output_text
|
|
60
|
+
assert "new _a000()" in result.output_text
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_does_not_rename_dunder_like_names() -> None:
|
|
64
|
+
src = "const __version = '1.0';\nconst _foo = 1;\n"
|
|
65
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
66
|
+
assert "__version" in result.output_text # preserved
|
|
67
|
+
assert "_foo" not in result.output_text # renamed
|
|
68
|
+
assert "_a000" in result.output_text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_respects_public_api_in_policy() -> None:
|
|
72
|
+
src = "export function _publicHelper(x) { return x; }\n"
|
|
73
|
+
policy = Policy(public_api=["_publicHelper"])
|
|
74
|
+
result = transform_js_like_source(src, "javascript", policy)
|
|
75
|
+
assert "_publicHelper" in result.output_text
|
|
76
|
+
assert not result.rename_map
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_handles_export_statement_with_lexical_declaration() -> None:
|
|
80
|
+
src = "export const _privateUtil = (x) => x * 2;\n"
|
|
81
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
82
|
+
# The name should still be renamed even though it's wrapped in `export`.
|
|
83
|
+
assert "_privateUtil" not in result.output_text
|
|
84
|
+
assert "_a000" in result.output_text
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_preserves_property_access_with_same_name() -> None:
|
|
88
|
+
"""obj._helper is a member access — different scope; we don't rename it."""
|
|
89
|
+
src = (
|
|
90
|
+
"function _helper(x) { return x * 2; }\n"
|
|
91
|
+
"const obj = { _helper: 'something else' };\n"
|
|
92
|
+
"export function go() {\n"
|
|
93
|
+
" return _helper(obj._helper);\n"
|
|
94
|
+
"}\n"
|
|
95
|
+
)
|
|
96
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
97
|
+
# The bare `_helper` reference (function call) should be renamed.
|
|
98
|
+
# The property access `obj._helper` should NOT be renamed (different scope).
|
|
99
|
+
assert "obj._helper" in result.output_text
|
|
100
|
+
# The standalone calls should be renamed.
|
|
101
|
+
assert "_a000(obj._helper)" in result.output_text
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_typescript_renames_top_level_types() -> None:
|
|
105
|
+
"""TypeScript: rename top-level _-prefixed type identifiers as well."""
|
|
106
|
+
src = (
|
|
107
|
+
"type _Internal = { x: number };\nexport function make(): _Internal { return { x: 1 }; }\n"
|
|
108
|
+
)
|
|
109
|
+
result = transform_js_like_source(src, "typescript", Policy())
|
|
110
|
+
# type_alias_declaration: tree-sitter-typescript should expose this; for v1, our
|
|
111
|
+
# _DEFINITION_NODE_TYPES doesn't include it, so the rename won't happen. This is
|
|
112
|
+
# documented as a limitation. The point of this test: confirm we don't crash.
|
|
113
|
+
assert result.output_text # non-empty
|
|
114
|
+
# The visible behavior may or may not rename; either is acceptable for v1.
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_returns_unchanged_when_no_private_names() -> None:
|
|
118
|
+
src = "export function pub(x) { return x; }\n"
|
|
119
|
+
result = transform_js_like_source(src, "javascript", Policy())
|
|
120
|
+
assert result.output_text == src
|
|
121
|
+
assert not result.rename_map
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_handles_invalid_source_gracefully() -> None:
|
|
125
|
+
src = "function broken( {{{"
|
|
126
|
+
# Should not raise.
|
|
127
|
+
transform_js_like_source(src, "javascript", Policy())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------- pipeline integration ----------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _make_js_repo(tmp_path: Path) -> Path:
|
|
134
|
+
"""Sample JS repo: lib + matching test that imports the public function."""
|
|
135
|
+
repo = tmp_path / "src_repo"
|
|
136
|
+
repo.mkdir()
|
|
137
|
+
(repo / "lib.js").write_text(
|
|
138
|
+
"function _double(x) {\n"
|
|
139
|
+
" return x * 2;\n"
|
|
140
|
+
"}\n"
|
|
141
|
+
"\n"
|
|
142
|
+
"export function publicFn(x) {\n"
|
|
143
|
+
" return _double(x);\n"
|
|
144
|
+
"}\n"
|
|
145
|
+
)
|
|
146
|
+
(repo / "package.json").write_text('{"type":"module"}\n')
|
|
147
|
+
return repo
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_runner_handles_mixed_python_and_js(tmp_path: Path) -> None:
|
|
151
|
+
repo = tmp_path / "repo"
|
|
152
|
+
repo.mkdir()
|
|
153
|
+
(repo / "lib.py").write_text(
|
|
154
|
+
"def _helper(x):\n return x + 1\n\ndef pub(x):\n return _helper(x)\n"
|
|
155
|
+
)
|
|
156
|
+
(repo / "lib.js").write_text(
|
|
157
|
+
"function _helper(x) { return x + 1; }\nexport function pub(x) { return _helper(x); }\n"
|
|
158
|
+
)
|
|
159
|
+
out = tmp_path / "obfuscated"
|
|
160
|
+
|
|
161
|
+
result = run_obfuscate(repo, out, Policy())
|
|
162
|
+
assert result.files_transformed == 2 # both py and js were transformed
|
|
163
|
+
py_text = (out / "lib.py").read_text()
|
|
164
|
+
js_text = (out / "lib.js").read_text()
|
|
165
|
+
assert "_helper" not in py_text
|
|
166
|
+
assert "_helper" not in js_text
|
|
167
|
+
assert "_a000" in py_text
|
|
168
|
+
assert "_a000" in js_text
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_runner_writes_manifest_with_js_renames(tmp_path: Path) -> None:
|
|
172
|
+
repo = _make_js_repo(tmp_path)
|
|
173
|
+
out = tmp_path / "obfuscated"
|
|
174
|
+
run_obfuscate(repo, out, Policy())
|
|
175
|
+
|
|
176
|
+
manifest = json.loads((out / "cloak-manifest.json").read_text())
|
|
177
|
+
assert "lib.js:_double" in manifest["rename_map"]
|
|
178
|
+
assert manifest["rename_map"]["lib.js:_double"] == "_a000"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ---------- CLI integration ----------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_cli_obfuscate_against_js_repo(tmp_path: Path) -> None:
|
|
185
|
+
repo = _make_js_repo(tmp_path)
|
|
186
|
+
out = tmp_path / "obf"
|
|
187
|
+
result = runner.invoke(app, ["obfuscate", str(repo), "--out", str(out)])
|
|
188
|
+
assert result.exit_code == 0, result.stdout
|
|
189
|
+
obfuscated = (out / "lib.js").read_text()
|
|
190
|
+
assert "_double" not in obfuscated
|
|
191
|
+
assert "_a000" in obfuscated
|
|
192
|
+
assert "publicFn" in obfuscated # public preserved
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_cli_obfuscate_panel_shows_renames(tmp_path: Path) -> None:
|
|
196
|
+
repo = _make_js_repo(tmp_path)
|
|
197
|
+
out = tmp_path / "obf"
|
|
198
|
+
result = runner.invoke(app, ["obfuscate", str(repo), "--out", str(out)])
|
|
199
|
+
assert result.exit_code == 0
|
|
200
|
+
assert "Obfuscated" in result.stdout
|
|
201
|
+
assert "1 module-private" in result.stdout or "renames:" in result.stdout
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|