unpeek 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Efe Arabacı & unpeek contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,246 @@
1
- # Temporary Holding Version
1
+ # unpeek
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Zero-config, call-graph-aware polymorphic JavaScript protection and runtime anti-tamper toolkit with <2% CPU overhead.**
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-emerald.svg)](LICENSE)
6
+ [![CPU Overhead](https://img.shields.io/badge/CPU_Overhead-%3C2%25-10b981.svg)](packages/unpeek/benchmarks/overhead.bench.ts)
7
+ [![Gzip Delta](https://img.shields.io/badge/Gzip_Delta-%3C15%25-06b6d4.svg)](packages/unpeek/benchmarks/overhead.bench.ts)
8
+ [![Interactive Playground](https://img.shields.io/badge/Playground-Live_Demo-6366f1.svg)](https://woffluon.github.io/unpeek)
9
+
10
+ English | [Türkçe](./README.tr.md) · [Live Interactive Playground](https://woffluon.github.io/unpeek) · [Agent Architecture Guide](./AGENTS.md) · [AI Skill](./skills/unpeek/SKILL.md)
11
+
12
+ ---
13
+
14
+ <baglam>
15
+
16
+ ## Why unpeek?
17
+
18
+ Shipping a modern web application in Next.js, Vite, or Webpack exposes client-side business logic, internal API endpoints, feature flags, and proprietary algorithms to anyone who opens Chrome DevTools or runs an automated AST deobfuscator (`webcrack`, `synchrony`).
19
+
20
+ Existing tools force an uncomfortable trade-off:
21
+ - **Standard minifiers (`esbuild`, `SWC`, `Terser`)** compress whitespace and shorten variable names, leaving string literals, endpoint URLs, and control flow readable after a single click on the DevTools Pretty-Print (`{}`) button.
22
+ - **Legacy AST obfuscators (`javascript-obfuscator`)** apply indiscriminate control-flow flattening, `eval` traps, and eager string-array rotation to every function in the bundle. This inflates bundle sizes by 40–300%, breaks V8 TurboFan JIT inlining inside 60fps UI rendering loops, and triggers false-positive browser freezes.
23
+
24
+ `unpeek` resolves this trade-off by combining static call-graph analysis with polymorphic compile-time encryption:
25
+
26
+ | Capability | Standard Minifiers (`Terser` / `SWC`) | Legacy AST Obfuscators | `unpeek` |
27
+ | :--- | :--- | :--- | :--- |
28
+ | **Runtime CPU Overhead** | ~0% | 15% – 85% (throttles UI loops) | **< 2.0%** (Call-Graph Hot-Path Exemption) |
29
+ | **Gzip Bundle Growth** | 0% (Baseline) | +45% – +300% | **< 15%** (O(1) Lazy String Vault) |
30
+ | **String & Endpoint Encryption** | Plaintext in bundle | Eager array rotation (high CPU) | **Polymorphic XOR + O(1) Memoized Vault** |
31
+ | **DevTools Pretty-Print (`{}`) Defense** | None | `debugger` infinite loops (easily bypassed) | **`Function.prototype.toString()` FNV-1a Integrity** |
32
+ | **Source Map Isolation** | Manual CI scripts | Manual configuration | **Automatic `.unpeek-maps/` Vault + Comment Strip** |
33
+ | **Static Deobfuscator Resistance** | None | Fixed templates (fingerprinted by `webcrack`) | **Per-Build Polymorphic AST Seed** |
34
+ | **SSR / Next.js Compatibility** | Native | Frequently breaks server hydration | **Client-Only Auto-Detection + SSR Guard** |
35
+
36
+ ### Core Architecture
37
+
38
+ 1. **Call-Graph Hot-Path Exemption (`<2%` CPU Budget)**
39
+ Before modifying a single AST node, [`detectHotPaths`](packages/unpeek/src/ast/index.ts) traverses the file's call graph. Functions containing loops (`for`, `while`, `do-while`, `for-of`), array higher-order callbacks (`.map`, `.filter`, `.reduce`, `.forEach`), `requestAnimationFrame` tickers, and React/UI render components (`useState`, `useEffect`, `createElement`) are flagged as hot paths. `unpeek` mangles identifiers and encrypts string constants across the entire bundle, while injecting opaque control-flow predicates exclusively into cold functions (such as license checks, request signers, and auth validators). V8 JIT optimization inside hot loops remains untouched.
40
+ 2. **O(1) Lazy String Vault (`<15%` Gzip Budget)**
41
+ Plaintext string literals (`minStringLength >= 4`) are encrypted at build time with a polymorphic XOR stream cipher keyed by a per-build 32-bit seed. Instead of decrypting every string on page startup, the injected vault helper decodes each string lazily on its first access and caches the plaintext result in its array slot. Subsequent reads resolve in O(1) time via direct array indexing.
42
+ 3. **Self-Defending `Function.prototype.toString()` Integrity & Passive Deterrent**
43
+ When an analyst formats a protected chunk using DevTools Pretty-Print (`{}`) or edits a branch instruction in the Sources panel, the formatted source alters `Function.prototype.toString()`. [`verifyCodeIntegrity`](packages/unpeek/src/runtime/index.ts) validates a seeded 32-bit FNV-1a checksum against the function's source representation with zero false positives on untouched minified code, triggering gradual state degradation (`deterrentMode: 'gradual'`) rather than an obvious crash point that attackers can patch out.
44
+ 4. **Automatic Source Map Vault (`.unpeek-maps/`)**
45
+ Leaving `.js.map` files in a public build directory reconstructs your original TypeScript project tree inside DevTools. [`protectDirectory`](packages/unpeek/src/cli/index.ts) automatically extracts `.map` files from your public build directory into a private `.unpeek-maps/` folder and strips `//# sourceMappingURL=` comments from every client bundle so you can upload maps to Sentry or Datadog without serving them to end users.
46
+
47
+ </baglam>
48
+
49
+ ---
50
+
51
+ <talimat>
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. One-Command Project Setup (`npx unpeek init`)
56
+
57
+ Run the initializer from your project root (Next.js, Vite, Webpack, or vanilla):
58
+
59
+ ```bash
60
+ npx unpeek init
61
+ ```
62
+
63
+ This command performs three non-destructive setup actions:
64
+ 1. Detects your framework (`nextjs`, `vite`, `webpack`, or `vanilla`) from `package.json` and build config files.
65
+ 2. Adds `"postbuild": "unpeek protect"` to your `package.json` scripts so every production build is automatically protected.
66
+ 3. Appends `.unpeek-maps/` to `.gitignore` so extracted source maps stay out of version control.
67
+
68
+ ### 2. CLI Usage (`unpeek protect`)
69
+
70
+ You can also install `unpeek` as a development dependency and run it directly in CI/CD or local builds:
71
+
72
+ ```bash
73
+ npm install -D unpeek
74
+ npx unpeek protect
75
+ ```
76
+
77
+ #### CLI Options
78
+
79
+ | Flag | Description | Default |
80
+ | :--- | :--- | :--- |
81
+ | `--dir <path>` | Explicit client output directory override (e.g., `dist/assets`, `.next/static/chunks`) | Auto-detected |
82
+ | `--seed <number>` | Deterministic 32-bit polymorphic build seed | Random per build |
83
+ | `--mode <gradual\|passive>` | Runtime anti-tamper deterrent mode | `gradual` |
84
+ | `--min-str <number>` | Minimum character length for O(1) lazy string vault encryption | `4` |
85
+ | `--no-shield` | Skip injecting the runtime integrity and DevTools telemetry preamble | Shield enabled |
86
+ | `--delete-maps` | Permanently delete `.map` files instead of moving them to `.unpeek-maps/` | Moves to `.unpeek-maps/` |
87
+
88
+ ### 3. Universal Bundler Plugin (`unpeek/unplugin`)
89
+
90
+ If you prefer in-memory chunk transformations during the bundler lifecycle rather than a post-build CLI pass, use [`unpeek/unplugin`](packages/unpeek/src/bundler/unplugin.ts):
91
+
92
+ #### Vite (`vite.config.ts`)
93
+
94
+ ```ts
95
+ import { defineConfig } from 'vite';
96
+ import unpeek from 'unpeek/unplugin';
97
+
98
+ export default defineConfig({
99
+ build: {
100
+ sourcemap: true, // Extracted into .unpeek-maps/ automatically
101
+ },
102
+ plugins: [
103
+ unpeek.vite({
104
+ deterrentMode: 'gradual',
105
+ keepPrivateMaps: true,
106
+ }),
107
+ ],
108
+ });
109
+ ```
110
+
111
+ #### Next.js (`next.config.mjs`)
112
+
113
+ Scope the plugin to client bundles (`!isServer`) so Node.js server chunks keep their native stack traces:
114
+
115
+ ```js
116
+ import unpeek from 'unpeek/unplugin';
117
+
118
+ /** @type {import('next').NextConfig} */
119
+ const nextConfig = {
120
+ webpack(config, { dev, isServer }) {
121
+ if (!dev && !isServer) {
122
+ config.plugins.push(
123
+ unpeek.webpack({
124
+ deterrentMode: 'gradual',
125
+ keepPrivateMaps: true,
126
+ })
127
+ );
128
+ }
129
+ return config;
130
+ },
131
+ };
132
+
133
+ export default nextConfig;
134
+ ```
135
+
136
+ #### Programmatic API
137
+
138
+ ```ts
139
+ import { transformJavaScript, protectDirectory } from 'unpeek';
140
+
141
+ // Transform an individual JavaScript string in memory
142
+ const result = transformJavaScript(sourceCode, {
143
+ seed: 0x7f3a91,
144
+ minStringLength: 4,
145
+ injectRuntimeShield: true,
146
+ deterrentMode: 'gradual',
147
+ });
148
+
149
+ console.log(result.hotPathReport);
150
+ // { totalFunctions: 12, hotFunctionsSkipped: ['renderLoop'], coldFunctionsProtected: ['signRequest'] }
151
+
152
+ // Or protect an entire build directory
153
+ const summary = await protectDirectory({
154
+ cwd: process.cwd(),
155
+ keepPrivateMaps: true,
156
+ privateMapsDir: '.unpeek-maps',
157
+ });
158
+ ```
159
+
160
+ ---
161
+
162
+ ## `.unpeek-maps/` Source Map Vault & Sentry Workflow
163
+
164
+ When `keepPrivateMaps` is enabled (the default), `unpeek` relocates all `.js.map` files from your public output directory into `.unpeek-maps/` while preserving their relative filenames, and strips `//# sourceMappingURL=` directives from the public `.js` chunks.
165
+
166
+ To retain full stack-trace symbolication in production error trackers:
167
+
168
+ ```bash
169
+ # 1. Build your app and run unpeek (automatically triggered if wired via postbuild)
170
+ npm run build
171
+
172
+ # 2. Upload the private source maps from .unpeek-maps/ to Sentry or Datadog
173
+ npx @sentry/cli sourcemaps upload .unpeek-maps --release="$GIT_COMMIT_SHA"
174
+
175
+ # 3. Deploy your public build folder (.next/, dist/, or build/)
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Official AI Agent Skill Installation
181
+
182
+ `unpeek` ships with an official, token-optimized AI Agent Skill ([`skills/unpeek/SKILL.md`](skills/unpeek/SKILL.md)) following Agent Skills Discovery Optimization (SDO) standards. Installing this skill teaches your coding agent (Claude Code, Cursor, Antigravity, OpenAI Codex, or Gemini CLI) how to configure `unpeek`, guard SSR chunks, and wire `.unpeek-maps/` into CI/CD pipelines.
183
+
184
+ ### Project-Scoped Installation (Shared with Team via Git)
185
+
186
+ Copy the skill into your repository's `.agents/skills/unpeek/` directory:
187
+
188
+ ```bash
189
+ mkdir -p .agents/skills/unpeek
190
+ curl -fsSL https://raw.githubusercontent.com/Woffluon/unpeek/main/skills/unpeek/SKILL.md \
191
+ -o .agents/skills/unpeek/SKILL.md
192
+ ```
193
+
194
+ ### Global Machine Installation (Antigravity / Gemini CLI / Claude Code)
195
+
196
+ ```bash
197
+ # Antigravity & Gemini CLI
198
+ mkdir -p ~/.gemini/config/skills/unpeek
199
+ curl -fsSL https://raw.githubusercontent.com/Woffluon/unpeek/main/skills/unpeek/SKILL.md \
200
+ -o ~/.gemini/config/skills/unpeek/SKILL.md
201
+
202
+ # Cross-agent standard (~/.agents/skills)
203
+ mkdir -p ~/.agents/skills/unpeek
204
+ curl -fsSL https://raw.githubusercontent.com/Woffluon/unpeek/main/skills/unpeek/SKILL.md \
205
+ -o ~/.agents/skills/unpeek/SKILL.md
206
+ ```
207
+
208
+ </talimat>
209
+
210
+ ---
211
+
212
+ <dogrulama>
213
+
214
+ ## Verification, Testing & Benchmarks
215
+
216
+ This monorepo is managed with `pnpm` and verified against strict semantic equivalence, deobfuscation resistance, and runtime overhead budgets:
217
+
218
+ ```bash
219
+ # Install workspace dependencies
220
+ pnpm install
221
+
222
+ # Run unit & reverse-engineering resistance test suites (Vitest)
223
+ pnpm test
224
+
225
+ # Run the V8 VM CPU overhead (<2%) & Gzip delta (<15%) benchmark
226
+ pnpm bench
227
+
228
+ # Run strict TypeScript typechecking across packages/unpeek and apps/web
229
+ pnpm typecheck
230
+
231
+ # Build the core package and static interactive web playground
232
+ pnpm build
233
+ ```
234
+
235
+ ### Benchmark Output Reference ([`packages/unpeek/benchmarks/overhead.bench.ts`](packages/unpeek/benchmarks/overhead.bench.ts))
236
+
237
+ - **Build / Transform Time**: `< 3000 ms`
238
+ - **Gzip Size Delta**: `< 15%` on production application bundles
239
+ - **Runtime CPU Overhead**: `< 2.00%` (interleaved multi-round V8 JIT benchmark)
240
+ - **Plaintext Leak Rate**: `0%` for string literals meeting `minStringLength`
241
+
242
+ </dogrulama>
243
+
244
+ ## License
245
+
246
+ Released under the [MIT License](./LICENSE).
package/bin/unpeek.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../dist/cli/index.js';
3
+
4
+ runCli(process.argv.slice(2)).catch((err) => {
5
+ console.error('[unpeek] fatal error:', err instanceof Error ? err.message : String(err));
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,2 @@
1
+ import * as acorn from 'acorn';
2
+ export declare function applyColdFlowGuard(ast: acorn.Program, coldFunctionNodes: WeakSet<acorn.Node>, seed?: number): number;
@@ -0,0 +1,39 @@
1
+ import * as acorn from 'acorn';
2
+ import * as walk from 'acorn-walk';
3
+ function createOpaqueGuardStatement(seed, fnIndex) {
4
+ // Deterministic polymorphic constants where ((A ^ B) & 7) === Expected
5
+ const a = ((seed >>> 0) + Math.imul(fnIndex + 1, 0x45d9f3b)) & 0xffff;
6
+ const b = (a ^ 0x1357) & 0xffff;
7
+ const wrongVal = ((a ^ b) & 7) ^ 5; // Guaranteed never equal to ((a ^ b) & 7)
8
+ const guardCode = `if (((${a} ^ ${b}) & 7) === ${wrongVal}) return;`;
9
+ const parsed = acorn.parse(guardCode, {
10
+ ecmaVersion: 'latest',
11
+ allowReturnOutsideFunction: true,
12
+ });
13
+ return parsed.body[0];
14
+ }
15
+ export function applyColdFlowGuard(ast, coldFunctionNodes, seed = 0x9e3779b9) {
16
+ let guardedCount = 0;
17
+ function maybeInjectGuard(node) {
18
+ if (!coldFunctionNodes.has(node))
19
+ return;
20
+ if (!node.body || node.body.type !== 'BlockStatement')
21
+ return;
22
+ guardedCount++;
23
+ const guardStmt = createOpaqueGuardStatement(seed, guardedCount);
24
+ // Insert after any leading 'use strict' directive inside the function body
25
+ let insertIdx = 0;
26
+ while (insertIdx < node.body.body.length &&
27
+ node.body.body[insertIdx].type === 'ExpressionStatement' &&
28
+ node.body.body[insertIdx].directive) {
29
+ insertIdx++;
30
+ }
31
+ node.body.body.splice(insertIdx, 0, guardStmt);
32
+ }
33
+ walk.simple(ast, {
34
+ FunctionDeclaration: maybeInjectGuard,
35
+ FunctionExpression: maybeInjectGuard,
36
+ ArrowFunctionExpression: maybeInjectGuard,
37
+ });
38
+ return guardedCount;
39
+ }
@@ -0,0 +1,10 @@
1
+ import * as acorn from 'acorn';
2
+ import type { HotPathReport } from '../types.js';
3
+ export interface HotPathAnalysis {
4
+ report: HotPathReport;
5
+ hotFunctionNodes: WeakSet<acorn.Node>;
6
+ coldFunctionNodes: WeakSet<acorn.Node>;
7
+ }
8
+ export declare function parseJavaScriptAst(code: string): acorn.Program | null;
9
+ export declare function analyzeAstHotPaths(ast: acorn.Program): HotPathAnalysis;
10
+ export declare function detectHotPaths(code: string): HotPathReport;
@@ -0,0 +1,235 @@
1
+ import * as acorn from 'acorn';
2
+ import * as walk from 'acorn-walk';
3
+ const LOOP_NODE_TYPES = new Set([
4
+ 'ForStatement',
5
+ 'ForInStatement',
6
+ 'ForOfStatement',
7
+ 'WhileStatement',
8
+ 'DoWhileStatement',
9
+ ]);
10
+ const ARRAY_HOF_METHODS = new Set([
11
+ 'map',
12
+ 'filter',
13
+ 'reduce',
14
+ 'forEach',
15
+ 'flatMap',
16
+ 'find',
17
+ 'some',
18
+ 'every',
19
+ ]);
20
+ const UI_HOOK_OR_RENDER_CALLS = new Set([
21
+ 'useState',
22
+ 'useEffect',
23
+ 'useMemo',
24
+ 'useCallback',
25
+ 'useRef',
26
+ 'useLayoutEffect',
27
+ 'jsx',
28
+ 'jsxs',
29
+ '_jsx',
30
+ '_jsxs',
31
+ 'createElement',
32
+ 'requestAnimationFrame',
33
+ ]);
34
+ const FUNCTION_NODE_TYPES = new Set([
35
+ 'FunctionDeclaration',
36
+ 'FunctionExpression',
37
+ 'ArrowFunctionExpression',
38
+ ]);
39
+ export function parseJavaScriptAst(code) {
40
+ try {
41
+ return acorn.parse(code, {
42
+ ecmaVersion: 'latest',
43
+ sourceType: 'module',
44
+ allowReturnOutsideFunction: true,
45
+ });
46
+ }
47
+ catch {
48
+ try {
49
+ return acorn.parse(code, {
50
+ ecmaVersion: 'latest',
51
+ sourceType: 'script',
52
+ allowReturnOutsideFunction: true,
53
+ });
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ }
60
+ function getFunctionLabel(node, ancestors) {
61
+ if (node.id && typeof node.id.name === 'string') {
62
+ return node.id.name;
63
+ }
64
+ const parent = ancestors[ancestors.length - 2];
65
+ if (parent) {
66
+ if (parent.type === 'VariableDeclarator' && parent.id?.type === 'Identifier') {
67
+ return parent.id.name;
68
+ }
69
+ if (parent.type === 'Property' && parent.key?.type === 'Identifier') {
70
+ return parent.key.name;
71
+ }
72
+ if (parent.type === 'MethodDefinition' && parent.key?.type === 'Identifier') {
73
+ return parent.key.name;
74
+ }
75
+ if (parent.type === 'AssignmentExpression' &&
76
+ parent.left?.type === 'Identifier') {
77
+ return parent.left.name;
78
+ }
79
+ }
80
+ return `anonymous_${node.start}`;
81
+ }
82
+ function isPascalCaseComponent(name) {
83
+ return /^[A-Z][a-zA-Z0-9]*$/.test(name);
84
+ }
85
+ export function analyzeAstHotPaths(ast) {
86
+ const hotFunctionNodes = new WeakSet();
87
+ const coldFunctionNodes = new WeakSet();
88
+ const allFunctions = [];
89
+ const fnByName = new Map();
90
+ const callerToCallees = new Map();
91
+ walk.ancestor(ast, {
92
+ FunctionDeclaration(node, _state, ancestors) {
93
+ const name = getFunctionLabel(node, ancestors);
94
+ allFunctions.push({ node, name });
95
+ fnByName.set(name, node);
96
+ if (isPascalCaseComponent(name)) {
97
+ hotFunctionNodes.add(node);
98
+ }
99
+ },
100
+ FunctionExpression(node, _state, ancestors) {
101
+ const name = getFunctionLabel(node, ancestors);
102
+ allFunctions.push({ node, name });
103
+ fnByName.set(name, node);
104
+ if (isPascalCaseComponent(name)) {
105
+ hotFunctionNodes.add(node);
106
+ }
107
+ },
108
+ ArrowFunctionExpression(node, _state, ancestors) {
109
+ const name = getFunctionLabel(node, ancestors);
110
+ allFunctions.push({ node, name });
111
+ fnByName.set(name, node);
112
+ if (isPascalCaseComponent(name)) {
113
+ hotFunctionNodes.add(node);
114
+ }
115
+ },
116
+ CallExpression(node, _state, ancestors) {
117
+ const callee = node.callee;
118
+ let calleeName = '';
119
+ let memberProp = '';
120
+ if (callee?.type === 'Identifier') {
121
+ calleeName = callee.name;
122
+ for (let i = ancestors.length - 1; i >= 0; i--) {
123
+ if (FUNCTION_NODE_TYPES.has(ancestors[i].type)) {
124
+ let set = callerToCallees.get(ancestors[i]);
125
+ if (!set) {
126
+ set = new Set();
127
+ callerToCallees.set(ancestors[i], set);
128
+ }
129
+ set.add(calleeName);
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ else if (callee?.type === 'MemberExpression' &&
135
+ !callee.computed &&
136
+ callee.property?.type === 'Identifier') {
137
+ memberProp = callee.property.name;
138
+ }
139
+ // Check if arguments are hot callbacks (.map, .filter, .reduce, .forEach, .flatMap, requestAnimationFrame)
140
+ if (ARRAY_HOF_METHODS.has(memberProp) ||
141
+ calleeName === 'requestAnimationFrame' ||
142
+ memberProp === 'requestAnimationFrame') {
143
+ for (const arg of node.arguments || []) {
144
+ if (FUNCTION_NODE_TYPES.has(arg.type)) {
145
+ hotFunctionNodes.add(arg);
146
+ }
147
+ }
148
+ }
149
+ // Check if enclosing function calls React hooks / createElement / jsx
150
+ if (UI_HOOK_OR_RENDER_CALLS.has(calleeName) ||
151
+ UI_HOOK_OR_RENDER_CALLS.has(memberProp)) {
152
+ for (let i = ancestors.length - 1; i >= 0; i--) {
153
+ if (FUNCTION_NODE_TYPES.has(ancestors[i].type)) {
154
+ hotFunctionNodes.add(ancestors[i]);
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ },
160
+ });
161
+ // Mark functions containing any loop statement as hot
162
+ walk.ancestor(ast, {
163
+ ForStatement(_node, _state, ancestors) {
164
+ markEnclosingFunctionHot(ancestors, hotFunctionNodes);
165
+ },
166
+ ForInStatement(_node, _state, ancestors) {
167
+ markEnclosingFunctionHot(ancestors, hotFunctionNodes);
168
+ },
169
+ ForOfStatement(_node, _state, ancestors) {
170
+ markEnclosingFunctionHot(ancestors, hotFunctionNodes);
171
+ },
172
+ WhileStatement(_node, _state, ancestors) {
173
+ markEnclosingFunctionHot(ancestors, hotFunctionNodes);
174
+ },
175
+ DoWhileStatement(_node, _state, ancestors) {
176
+ markEnclosingFunctionHot(ancestors, hotFunctionNodes);
177
+ },
178
+ });
179
+ // Propagate hotness bottom-up along the static call graph (caller -> hot callee)
180
+ let changed = true;
181
+ while (changed) {
182
+ changed = false;
183
+ for (const [callerNode, calleeNames] of callerToCallees.entries()) {
184
+ if (hotFunctionNodes.has(callerNode))
185
+ continue;
186
+ for (const cName of calleeNames) {
187
+ const targetNode = fnByName.get(cName);
188
+ if (targetNode && hotFunctionNodes.has(targetNode)) {
189
+ hotFunctionNodes.add(callerNode);
190
+ changed = true;
191
+ break;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ const hotFunctionsSkipped = [];
197
+ const coldFunctionsProtected = [];
198
+ for (const fn of allFunctions) {
199
+ if (hotFunctionNodes.has(fn.node)) {
200
+ hotFunctionsSkipped.push(fn.name);
201
+ }
202
+ else {
203
+ coldFunctionNodes.add(fn.node);
204
+ coldFunctionsProtected.push(fn.name);
205
+ }
206
+ }
207
+ return {
208
+ report: {
209
+ totalFunctions: allFunctions.length,
210
+ hotFunctionsSkipped,
211
+ coldFunctionsProtected,
212
+ },
213
+ hotFunctionNodes,
214
+ coldFunctionNodes,
215
+ };
216
+ }
217
+ function markEnclosingFunctionHot(ancestors, hotSet) {
218
+ for (let i = ancestors.length - 1; i >= 0; i--) {
219
+ if (FUNCTION_NODE_TYPES.has(ancestors[i].type)) {
220
+ hotSet.add(ancestors[i]);
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ export function detectHotPaths(code) {
226
+ const ast = parseJavaScriptAst(code);
227
+ if (!ast) {
228
+ return {
229
+ totalFunctions: 0,
230
+ hotFunctionsSkipped: [],
231
+ coldFunctionsProtected: [],
232
+ };
233
+ }
234
+ return analyzeAstHotPaths(ast).report;
235
+ }
@@ -0,0 +1,4 @@
1
+ import type { TransformOptions, TransformResult } from '../types.js';
2
+ export { detectHotPaths } from './hot-path-detector.js';
3
+ export { computeFnv1aHash } from './string-vault.js';
4
+ export declare function transformJavaScript(code: string, options?: TransformOptions): TransformResult;
@@ -0,0 +1,55 @@
1
+ import { generate } from 'astring';
2
+ import { analyzeAstHotPaths, parseJavaScriptAst, } from './hot-path-detector.js';
3
+ import { applyStringVault, computeFnv1aHash } from './string-vault.js';
4
+ import { applyPolymorphicMangler } from './polymorphic-mangler.js';
5
+ import { applyColdFlowGuard } from './cold-flow-guard.js';
6
+ export { detectHotPaths } from './hot-path-detector.js';
7
+ export { computeFnv1aHash } from './string-vault.js';
8
+ export function transformJavaScript(code, options = {}) {
9
+ const seed = options.seed ?? 0x9e3779b9;
10
+ const minStringLength = options.minStringLength ?? 4;
11
+ if (!code || !code.trim()) {
12
+ return {
13
+ code,
14
+ encryptedStringsCount: 0,
15
+ mangledSymbolsCount: 0,
16
+ hotPathReport: {
17
+ totalFunctions: 0,
18
+ hotFunctionsSkipped: [],
19
+ coldFunctionsProtected: [],
20
+ },
21
+ integrityToken: computeFnv1aHash('', seed),
22
+ };
23
+ }
24
+ const ast = parseJavaScriptAst(code);
25
+ if (!ast) {
26
+ return {
27
+ code,
28
+ encryptedStringsCount: 0,
29
+ mangledSymbolsCount: 0,
30
+ hotPathReport: {
31
+ totalFunctions: 0,
32
+ hotFunctionsSkipped: [],
33
+ coldFunctionsProtected: [],
34
+ },
35
+ integrityToken: computeFnv1aHash(code, seed),
36
+ };
37
+ }
38
+ // 1. Detect hot paths (loops, array HOF callbacks, rAF, UI components)
39
+ const { report: hotPathReport, coldFunctionNodes } = analyzeAstHotPaths(ast);
40
+ // 2. Inject polymorphic opaque guards only on cold functions (<2% runtime CPU budget)
41
+ applyColdFlowGuard(ast, coldFunctionNodes, seed);
42
+ // 3. Mangle local parameters & function-scoped variables safely
43
+ const mangledSymbolsCount = applyPolymorphicMangler(ast, seed);
44
+ // 4. Encrypt eligible string literals into an O(1) lazy inline-cached vault
45
+ const { encryptedStringsCount, integrityToken } = applyStringVault(ast, seed, minStringLength);
46
+ // 5. Generate compact single-line JavaScript output
47
+ const outputCode = generate(ast, { indent: '', lineEnd: '' });
48
+ return {
49
+ code: outputCode,
50
+ encryptedStringsCount,
51
+ mangledSymbolsCount,
52
+ hotPathReport,
53
+ integrityToken,
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ import * as acorn from 'acorn';
2
+ export declare function applyPolymorphicMangler(ast: acorn.Program, seed?: number): number;