apphx 1.0.0 → 2.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.
Files changed (126) hide show
  1. package/README.md +52 -138
  2. package/benchmarks/peer-context-worker.mjs +186 -0
  3. package/benchmarks/peer-context.mjs +150 -0
  4. package/dist/apple-types.js.map +1 -1
  5. package/dist/cli.js +373 -121
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.js +42 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/dependencies.js +455 -16
  10. package/dist/dependencies.js.map +1 -1
  11. package/dist/export-options.js +44 -2
  12. package/dist/export-options.js.map +1 -1
  13. package/dist/flutter.js +13 -25
  14. package/dist/flutter.js.map +1 -1
  15. package/dist/go-dependencies.js +389 -0
  16. package/dist/go-dependencies.js.map +1 -0
  17. package/dist/inbox.js +7 -8
  18. package/dist/inbox.js.map +1 -1
  19. package/dist/integrations.js +29 -29
  20. package/dist/integrations.js.map +1 -1
  21. package/dist/isolation.js +190 -0
  22. package/dist/isolation.js.map +1 -0
  23. package/dist/macos.js +57 -0
  24. package/dist/macos.js.map +1 -0
  25. package/dist/mygo-build.js +55 -0
  26. package/dist/mygo-build.js.map +1 -0
  27. package/dist/policy.js +35 -0
  28. package/dist/policy.js.map +1 -0
  29. package/dist/preflight.js +1 -1
  30. package/dist/process.js +3 -3
  31. package/dist/project-profiles.js +163 -0
  32. package/dist/project-profiles.js.map +1 -0
  33. package/dist/proof.js +33 -0
  34. package/dist/proof.js.map +1 -0
  35. package/dist/remote.js +1 -1
  36. package/dist/runner.js +269 -287
  37. package/dist/runner.js.map +1 -1
  38. package/dist/scheduler.js +0 -3
  39. package/dist/scheduler.js.map +1 -1
  40. package/dist/schemas.js +156 -44
  41. package/dist/schemas.js.map +1 -1
  42. package/dist/store.js +166 -44
  43. package/dist/store.js.map +1 -1
  44. package/dist/vendor/obfuscator/dist/cli.js +72 -0
  45. package/dist/vendor/obfuscator/dist/cli.js.map +1 -0
  46. package/dist/vendor/obfuscator/dist/code.js +585 -0
  47. package/dist/vendor/obfuscator/dist/code.js.map +1 -0
  48. package/dist/vendor/obfuscator/dist/common.js +262 -0
  49. package/dist/vendor/obfuscator/dist/common.js.map +1 -0
  50. package/dist/vendor/obfuscator/dist/config.js +123 -0
  51. package/dist/vendor/obfuscator/dist/config.js.map +1 -0
  52. package/dist/vendor/obfuscator/dist/content.js +89 -0
  53. package/dist/vendor/obfuscator/dist/content.js.map +1 -0
  54. package/dist/vendor/obfuscator/dist/obfuscate.js +93 -0
  55. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -0
  56. package/dist/vendor/obfuscator/dist/png.js +283 -0
  57. package/dist/vendor/obfuscator/dist/png.js.map +1 -0
  58. package/dist/vendor/obfuscator/dist/project.js +159 -0
  59. package/dist/vendor/obfuscator/dist/project.js.map +1 -0
  60. package/dist/vendor/obfuscator/dist/resources.js +335 -0
  61. package/dist/vendor/obfuscator/dist/resources.js.map +1 -0
  62. package/dist/vendor/obfuscator/package.json +22 -0
  63. package/dist/workspace.js +113 -0
  64. package/dist/workspace.js.map +1 -0
  65. package/evals/control.mjs +73 -0
  66. package/evals/expectations.mjs +37 -0
  67. package/evals/host.mjs +51 -0
  68. package/evals/oracle.mjs +80 -0
  69. package/evals/policy.mjs +268 -0
  70. package/examples/android-java.factory.json +18 -0
  71. package/examples/android-java.json +16 -0
  72. package/examples/existing-project.factory.json +18 -0
  73. package/examples/existing-project.json +17 -0
  74. package/examples/expo.eas.json +17 -0
  75. package/examples/expo.factory.json +18 -0
  76. package/examples/expo.json +16 -0
  77. package/examples/factory.json +0 -2
  78. package/examples/flutter.factory.json +13 -0
  79. package/examples/flutter.json +16 -0
  80. package/examples/luggage.json +5 -3
  81. package/examples/mygo-web.factory.json +18 -0
  82. package/examples/mygo-web.json +16 -0
  83. package/examples/mygo.factory.json +18 -0
  84. package/examples/mygo.json +16 -0
  85. package/examples/react-native.factory.json +18 -0
  86. package/examples/react-native.json +16 -0
  87. package/examples/remote.json +1 -1
  88. package/examples/runtime/android-java/app/build.gradle +7 -0
  89. package/examples/runtime/android-java/app/src/main/AndroidManifest.xml +8 -0
  90. package/examples/runtime/android-java/app/src/main/java/dev/apphx/runtime/MainActivity.java +44 -0
  91. package/examples/runtime/android-java/sdk-build.mjs +29 -0
  92. package/examples/runtime/android-java/settings.gradle +2 -0
  93. package/examples/runtime/android-ui.mjs +70 -0
  94. package/examples/runtime/mygo-native/go.mod +13 -0
  95. package/examples/runtime/mygo-native/go.sum +10 -0
  96. package/examples/runtime/mygo-native/main.go +173 -0
  97. package/examples/runtime/mygo-native/main_test.go +14 -0
  98. package/examples/runtime/mygo-native/mygo.json +1 -0
  99. package/examples/runtime/mygo-web/bun.lock +15 -0
  100. package/examples/runtime/mygo-web/go.mod +9 -0
  101. package/examples/runtime/mygo-web/go.sum +8 -0
  102. package/examples/runtime/mygo-web/index.html +1 -0
  103. package/examples/runtime/mygo-web/main.go +60 -0
  104. package/examples/runtime/mygo-web/mygo.config.ts +10 -0
  105. package/examples/runtime/mygo-web/package.json +1 -0
  106. package/examples/runtime/mygo-web/src/main.ts +23 -0
  107. package/examples/runtime/mygo-web/src/mygo.ts +14 -0
  108. package/examples/swift.factory.json +18 -0
  109. package/examples/swift.json +16 -0
  110. package/examples/uni-app.factory.json +18 -0
  111. package/examples/uni-app.json +16 -0
  112. package/package.json +12 -5
  113. package/scripts/bundle-obfuscator.mjs +19 -0
  114. package/scripts/runtime-smoke.mjs +209 -0
  115. package/skills/apphx/SKILL.md +180 -0
  116. package/skills/apphx/resources/google-play-research-result.json +6 -0
  117. package/skills/apphx/resources/local-research-result.json +6 -0
  118. package/skills/{apple-app-factory → apphx}/resources/requirement.json +5 -8
  119. package/skills/apphx/resources/research-result.json +12 -0
  120. package/skills/apphx/resources/review-result.json +7 -0
  121. package/dist/apple.js +0 -900
  122. package/dist/apple.js.map +0 -1
  123. package/skills/apple-app-factory/SKILL.md +0 -103
  124. package/skills/apple-app-factory/resources/research-result.json +0 -21
  125. package/skills/apple-app-factory/resources/review-result.json +0 -7
  126. /package/skills/{apple-app-factory → apphx}/LICENSE +0 -0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import { FRAMEWORKS, deriveHash } from "./common.js";
3
+ import { ObfuscatorConfigSchema } from "./config.js";
4
+ import { applyObfuscation } from "./obfuscate.js";
5
+ import { parseArgs } from "node:util";
6
+ import { readFileSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ // src/cli.ts
9
+ const help = `obfuscator --project <dir> [--config <config.json>] [--framework <stack>] [--seed <text>]
10
+ Deterministically obfuscates an owned app project in place.
11
+ --project <dir> Existing project directory to transform (required)
12
+ --config <file> Obfuscator options as JSON; defaults to all features off
13
+ --framework <stack> flutter|android-java|swift|react-native|expo|uni-app|mygo (required unless set in config)
14
+ --seed <text> Determinism seed; defaults to config.seed, then a hash of the config file
15
+ --version Print the package version
16
+ Exit codes: 0 transformed; 1 transform failure; 2 configuration or usage error.
17
+ Report is printed to stdout as JSON: { changedFiles, transforms, skipped }`;
18
+ function usageError(message) {
19
+ console.error(JSON.stringify({ error: message }));
20
+ console.error(help);
21
+ process.exitCode = 2;
22
+ throw new Error(message);
23
+ }
24
+ async function main() {
25
+ const { values } = parseArgs({
26
+ allowPositionals: false,
27
+ options: {
28
+ project: { type: "string" },
29
+ config: { type: "string" },
30
+ framework: { type: "string" },
31
+ seed: { type: "string" },
32
+ version: {
33
+ type: "boolean",
34
+ short: "v"
35
+ },
36
+ help: {
37
+ type: "boolean",
38
+ short: "h"
39
+ }
40
+ }
41
+ });
42
+ if (values.version) {
43
+ const metadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
44
+ console.log(metadata.version);
45
+ return;
46
+ }
47
+ if (values.help || !values.project) {
48
+ console.log(help);
49
+ if (!values.project) process.exitCode = 2;
50
+ return;
51
+ }
52
+ const configPath = values.config ? resolve(values.config) : void 0;
53
+ const raw = configPath ? readFileSync(configPath, "utf8") : "{}";
54
+ const parsed = ObfuscatorConfigSchema.safeParse(JSON.parse(raw));
55
+ if (!parsed.success) usageError(`Invalid obfuscator configuration: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`);
56
+ const config = parsed.data;
57
+ const frameworkValue = values.framework ?? config.framework;
58
+ if (!frameworkValue || !FRAMEWORKS.includes(frameworkValue)) usageError(`--framework must be one of ${FRAMEWORKS.join("|")}`);
59
+ const framework = frameworkValue;
60
+ if (values.seed !== void 0 && values.seed.trim().length === 0) usageError("--seed must be a non-empty string");
61
+ const seed = values.seed ?? config.seed ?? deriveHash(JSON.stringify(config)).slice(0, 16);
62
+ const report = applyObfuscation(resolve(values.project), config, seed, framework);
63
+ console.log(JSON.stringify(report, null, 2));
64
+ }
65
+ main().catch((error) => {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ console.error(JSON.stringify({ error: message }));
68
+ process.exitCode = process.exitCode === 2 ? 2 : 1;
69
+ });
70
+ export {};
71
+
72
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from 'node:util';\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { deriveHash, FRAMEWORKS } from './common.js';\nimport type { Framework } from './common.js';\nimport { ObfuscatorConfigSchema } from './config.js';\nimport { applyObfuscation } from './obfuscate.js';\n\nconst help = `obfuscator --project <dir> [--config <config.json>] [--framework <stack>] [--seed <text>]\n Deterministically obfuscates an owned app project in place.\n --project <dir> Existing project directory to transform (required)\n --config <file> Obfuscator options as JSON; defaults to all features off\n --framework <stack> flutter|android-java|swift|react-native|expo|uni-app|mygo (required unless set in config)\n --seed <text> Determinism seed; defaults to config.seed, then a hash of the config file\n --version Print the package version\nExit codes: 0 transformed; 1 transform failure; 2 configuration or usage error.\nReport is printed to stdout as JSON: { changedFiles, transforms, skipped }`;\n\nfunction usageError(message: string): never {\n console.error(JSON.stringify({ error: message }));\n console.error(help);\n process.exitCode = 2;\n throw new Error(message);\n}\n\nasync function main(): Promise<void> {\n const { values } = parseArgs({\n allowPositionals: false,\n options: {\n project: { type: 'string' },\n config: { type: 'string' },\n framework: { type: 'string' },\n seed: { type: 'string' },\n version: { type: 'boolean', short: 'v' },\n help: { type: 'boolean', short: 'h' },\n },\n });\n if (values.version) {\n const metadata = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string };\n console.log(metadata.version);\n return;\n }\n if (values.help || !values.project) { console.log(help); if (!values.project) process.exitCode = 2; return; }\n const configPath = values.config ? resolve(values.config) : undefined;\n const raw = configPath ? readFileSync(configPath, 'utf8') : '{}';\n const parsed = ObfuscatorConfigSchema.safeParse(JSON.parse(raw));\n if (!parsed.success) usageError(`Invalid obfuscator configuration: ${parsed.error.issues.map(issue => `${issue.path.join('.') || '<root>'}: ${issue.message}`).join('; ')}`);\n const config = parsed.data;\n const frameworkValue = values.framework ?? config.framework;\n if (!frameworkValue || !FRAMEWORKS.includes(frameworkValue as Framework)) usageError(`--framework must be one of ${FRAMEWORKS.join('|')}`);\n const framework = frameworkValue as Framework;\n // The config schema enforces non-empty seeds; keep the CLI flag path consistent with it.\n if (values.seed !== undefined && values.seed.trim().length === 0) usageError('--seed must be a non-empty string');\n const seed = values.seed ?? config.seed ?? deriveHash(JSON.stringify(config)).slice(0, 16);\n const report = applyObfuscation(resolve(values.project), config, seed, framework);\n console.log(JSON.stringify(report, null, 2));\n}\n\nmain().catch(error => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(JSON.stringify({ error: message }));\n process.exitCode = process.exitCode === 2 ? 2 : 1;\n});\n"],"mappings":";;;;;;;;AASA,MAAM,OAAO;;;;;;;;;AAUb,SAAS,WAAW,SAAwB;CAC1C,QAAQ,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC,CAAC;CAChD,QAAQ,MAAM,IAAI;CAClB,QAAQ,WAAW;CACnB,MAAM,IAAI,MAAM,OAAO;AACzB;AAEA,eAAe,OAAsB;CACnC,MAAM,EAAE,WAAW,UAAU;EAC3B,kBAAkB;EAClB,SAAS;GACP,SAAS,EAAE,MAAM,SAAS;GAC1B,QAAQ,EAAE,MAAM,SAAS;GACzB,WAAW,EAAE,MAAM,SAAS;GAC5B,MAAM,EAAE,MAAM,SAAS;GACvB,SAAS;IAAE,MAAM;IAAW,OAAO;GAAI;GACvC,MAAM;IAAE,MAAM;IAAW,OAAO;GAAI;EACtC;CACF,CAAC;CACD,IAAI,OAAO,SAAS;EAClB,MAAM,WAAW,KAAK,MAAM,aAAa,IAAI,IAAI,mBAAmB,YAAY,GAAG,GAAG,MAAM,CAAC;EAC7F,QAAQ,IAAI,SAAS,OAAO;EAC5B;CACF;CACA,IAAI,OAAO,QAAQ,CAAC,OAAO,SAAS;EAAE,QAAQ,IAAI,IAAI;EAAG,IAAI,CAAC,OAAO,SAAS,QAAQ,WAAW;EAAG;CAAQ;CAC5G,MAAM,aAAa,OAAO,SAAS,QAAQ,OAAO,MAAM,IAAI,KAAA;CAC5D,MAAM,MAAM,aAAa,aAAa,YAAY,MAAM,IAAI;CAC5D,MAAM,SAAS,uBAAuB,UAAU,KAAK,MAAM,GAAG,CAAC;CAC/D,IAAI,CAAC,OAAO,SAAS,WAAW,qCAAqC,OAAO,MAAM,OAAO,KAAI,UAAS,GAAG,MAAM,KAAK,KAAK,GAAG,KAAK,SAAS,IAAI,MAAM,SAAS,CAAC,CAAC,KAAK,IAAI,GAAG;CAC3K,MAAM,SAAS,OAAO;CACtB,MAAM,iBAAiB,OAAO,aAAa,OAAO;CAClD,IAAI,CAAC,kBAAkB,CAAC,WAAW,SAAS,cAA2B,GAAG,WAAW,8BAA8B,WAAW,KAAK,GAAG,GAAG;CACzI,MAAM,YAAY;CAElB,IAAI,OAAO,SAAS,KAAA,KAAa,OAAO,KAAK,KAAK,CAAC,CAAC,WAAW,GAAG,WAAW,mCAAmC;CAChH,MAAM,OAAO,OAAO,QAAQ,OAAO,QAAQ,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE;CACzF,MAAM,SAAS,iBAAiB,QAAQ,OAAO,OAAO,GAAG,QAAQ,MAAM,SAAS;CAChF,QAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAC7C;AAEA,KAAK,CAAC,CAAC,OAAM,UAAS;CACpB,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACrE,QAAQ,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC,CAAC;CAChD,QAAQ,WAAW,QAAQ,aAAa,IAAI,IAAI;AAClD,CAAC"}
@@ -0,0 +1,585 @@
1
+ import { deriveHash, deriveToken, replaceWholeWords, seededInt } from "./common.js";
2
+ // src/code.ts
3
+ /** Control characters are illegal inside real identifiers/expressions, so they cannot collide with code. */
4
+ const SEPARATOR_CANDIDATES = [
5
+ "",
6
+ "",
7
+ "",
8
+ "",
9
+ "",
10
+ "\x07",
11
+ "\b",
12
+ "",
13
+ ""
14
+ ];
15
+ /** Pick two placeholder characters that do not occur anywhere in the content. */
16
+ function pickSeparators(content) {
17
+ let first;
18
+ let second;
19
+ for (const candidate of SEPARATOR_CANDIDATES) {
20
+ if (content.includes(candidate)) continue;
21
+ if (first === void 0) {
22
+ first = candidate;
23
+ continue;
24
+ }
25
+ second = candidate;
26
+ break;
27
+ }
28
+ if (first === void 0 || second === void 0) return {
29
+ stringSep: "",
30
+ commentSep: ""
31
+ };
32
+ return {
33
+ stringSep: first,
34
+ commentSep: second
35
+ };
36
+ }
37
+ /** Index of the first '\n' or '\r' at or after `start` (content length when absent). */
38
+ function lineEnd(content, start) {
39
+ const nextLf = content.indexOf("\n", start);
40
+ const nextCr = content.indexOf("\r", start);
41
+ if (nextLf === -1) return nextCr === -1 ? content.length : nextCr;
42
+ if (nextCr === -1) return nextLf;
43
+ return Math.min(nextLf, nextCr);
44
+ }
45
+ /** Scan a quoted literal body. Raw literals have no escapes; single-line literals end at a newline. */
46
+ function scanQuoted(content, start, quote, isRaw, multiline) {
47
+ let index = start;
48
+ while (index < content.length) {
49
+ const char = content[index];
50
+ if (!multiline && (char === "\n" || char === "\r")) return index;
51
+ if (!isRaw && char === "\\") {
52
+ index += 2;
53
+ continue;
54
+ }
55
+ if (char === quote) return index + 1;
56
+ index += 1;
57
+ }
58
+ return content.length;
59
+ }
60
+ /** Scan a triple-quoted literal body: closes only at the full delimiter, never at a lone quote. */
61
+ function scanTripleQuoted(content, start, quote, isRaw) {
62
+ const delimiter = quote.repeat(3);
63
+ let index = start;
64
+ while (index < content.length) {
65
+ if (!isRaw && content[index] === "\\") {
66
+ index += 2;
67
+ continue;
68
+ }
69
+ if (content.startsWith(delimiter, index)) return index + 3;
70
+ index += 1;
71
+ }
72
+ return content.length;
73
+ }
74
+ /**
75
+ * Mask every string literal and comment with inert placeholders, left to right, so later
76
+ * replacement passes can never touch literal/comment text. The scanner recognizes, per language:
77
+ * - comments: `//` to end of line, `/* ... *\/` blocks (non-nesting);
78
+ * - strings: 'single' and "double" everywhere; JavaScript backtick templates with `${}`;
79
+ * Dart raw `r'...'`/`r"..."` and triple-quoted `'''...'''`/`"""..."""` (with `$` interpolation);
80
+ * Kotlin `"""..."""`; Swift raw `#"..."#` (and deeper `##`/`###` nesting).
81
+ * A backslash skips the next character inside a normal string; a newline before the closing
82
+ * quote ends a single-line string. The `#!` shebang on line 1 of javascript files is treated
83
+ * as a comment so transforms leave it alone.
84
+ */
85
+ function maskContent(content, language) {
86
+ const { stringSep, commentSep } = pickSeparators(content);
87
+ const parts = [];
88
+ const strings = [];
89
+ const stringKinds = [];
90
+ const comments = [];
91
+ const commentKinds = [];
92
+ const javascript = language === "javascript";
93
+ let index = 0;
94
+ while (index < content.length) {
95
+ if (javascript && index === 0 && content.startsWith("#!")) {
96
+ const end = lineEnd(content, index);
97
+ parts.push({
98
+ text: content.slice(index, end),
99
+ isPlaceholder: true,
100
+ placeholderIndex: comments.length,
101
+ placeholderKind: "comment"
102
+ });
103
+ comments.push(content.slice(index, end));
104
+ commentKinds.push(true);
105
+ index = end;
106
+ continue;
107
+ }
108
+ if (content.startsWith("//", index)) {
109
+ const end = lineEnd(content, index);
110
+ parts.push({
111
+ text: content.slice(index, end),
112
+ isPlaceholder: true,
113
+ placeholderIndex: comments.length,
114
+ placeholderKind: "comment"
115
+ });
116
+ comments.push(content.slice(index, end));
117
+ commentKinds.push(false);
118
+ index = end;
119
+ continue;
120
+ }
121
+ if (content.startsWith("/*", index)) {
122
+ const close = content.indexOf("*/", index + 2);
123
+ const end = close === -1 ? content.length : close + 2;
124
+ parts.push({
125
+ text: content.slice(index, end),
126
+ isPlaceholder: true,
127
+ placeholderIndex: comments.length,
128
+ placeholderKind: "comment"
129
+ });
130
+ comments.push(content.slice(index, end));
131
+ commentKinds.push(false);
132
+ index = end;
133
+ continue;
134
+ }
135
+ if (language === "dart" && (content.startsWith("r'''", index) || content.startsWith("r\"\"\"", index))) {
136
+ const quote = content[index + 1];
137
+ const end = scanTripleQuoted(content, index + 4, quote, true);
138
+ const text = content.slice(index, end);
139
+ parts.push({
140
+ text,
141
+ isPlaceholder: true,
142
+ placeholderIndex: strings.length,
143
+ placeholderKind: "string"
144
+ });
145
+ strings.push(text);
146
+ stringKinds.push("other");
147
+ index = end;
148
+ continue;
149
+ }
150
+ if (language === "dart" && (content.startsWith("r'", index) || content.startsWith("r\"", index))) {
151
+ const quote = content[index + 1];
152
+ const end = scanQuoted(content, index + 2, quote, true, false);
153
+ const text = content.slice(index, end);
154
+ parts.push({
155
+ text,
156
+ isPlaceholder: true,
157
+ placeholderIndex: strings.length,
158
+ placeholderKind: "string"
159
+ });
160
+ strings.push(text);
161
+ stringKinds.push("other");
162
+ index = end;
163
+ continue;
164
+ }
165
+ if ((language === "dart" || language === "kotlin") && content.startsWith("\"\"\"", index)) {
166
+ const end = scanTripleQuoted(content, index + 3, "\"", false);
167
+ const text = content.slice(index, end);
168
+ parts.push({
169
+ text,
170
+ isPlaceholder: true,
171
+ placeholderIndex: strings.length,
172
+ placeholderKind: "string"
173
+ });
174
+ strings.push(text);
175
+ stringKinds.push("other");
176
+ index = end;
177
+ continue;
178
+ }
179
+ if (language === "dart" && content.startsWith("'''", index)) {
180
+ const end = scanTripleQuoted(content, index + 3, "'", false);
181
+ const text = content.slice(index, end);
182
+ parts.push({
183
+ text,
184
+ isPlaceholder: true,
185
+ placeholderIndex: strings.length,
186
+ placeholderKind: "string"
187
+ });
188
+ strings.push(text);
189
+ stringKinds.push("other");
190
+ index = end;
191
+ continue;
192
+ }
193
+ if (javascript && content[index] === "`") {
194
+ const end = scanQuoted(content, index + 1, "`", false, true);
195
+ const text = content.slice(index, end);
196
+ parts.push({
197
+ text,
198
+ isPlaceholder: true,
199
+ placeholderIndex: strings.length,
200
+ placeholderKind: "string"
201
+ });
202
+ strings.push(text);
203
+ stringKinds.push("other");
204
+ index = end;
205
+ continue;
206
+ }
207
+ if (language === "swift" && content[index] === "#") {
208
+ let hashEnd = index;
209
+ while (content[hashEnd] === "#") hashEnd += 1;
210
+ if (content[hashEnd] === "\"") {
211
+ let cursor = hashEnd + 1;
212
+ let end = -1;
213
+ while (cursor < content.length) {
214
+ const close = content.indexOf("\"", cursor);
215
+ if (close === -1) {
216
+ cursor = content.length;
217
+ break;
218
+ }
219
+ let closeEnd = close + 1;
220
+ while (content[closeEnd] === "#") closeEnd += 1;
221
+ if (closeEnd - close - 1 === hashEnd - index) {
222
+ end = closeEnd;
223
+ break;
224
+ }
225
+ cursor = close + 1;
226
+ }
227
+ if (end === -1) end = content.length;
228
+ const text = content.slice(index, end);
229
+ parts.push({
230
+ text,
231
+ isPlaceholder: true,
232
+ placeholderIndex: strings.length,
233
+ placeholderKind: "string"
234
+ });
235
+ strings.push(text);
236
+ stringKinds.push("other");
237
+ index = end;
238
+ continue;
239
+ }
240
+ }
241
+ const char = content[index];
242
+ if (char === "'" || char === "\"") {
243
+ const end = scanQuoted(content, index + 1, char, false, false);
244
+ const text = content.slice(index, end);
245
+ parts.push({
246
+ text,
247
+ isPlaceholder: true,
248
+ placeholderIndex: strings.length,
249
+ placeholderKind: "string"
250
+ });
251
+ strings.push(text);
252
+ stringKinds.push(char === "'" ? "single" : "double");
253
+ index = end;
254
+ continue;
255
+ }
256
+ const end = indexOfAnyFrom(content, index + 1, [
257
+ "'",
258
+ "\"",
259
+ "/",
260
+ "`",
261
+ "#",
262
+ "r"
263
+ ]);
264
+ parts.push({
265
+ text: content.slice(index, end),
266
+ isPlaceholder: false,
267
+ placeholderIndex: -1,
268
+ placeholderKind: "string"
269
+ });
270
+ index = end;
271
+ }
272
+ return {
273
+ masked: parts.map((part) => part.isPlaceholder ? part.placeholderKind === "string" ? `${stringSep}${part.placeholderIndex}${stringSep}` : `${commentSep}${part.placeholderIndex}${commentSep}` : part.text).join(""),
274
+ strings,
275
+ stringKinds,
276
+ comments,
277
+ commentKinds,
278
+ stringSep,
279
+ commentSep
280
+ };
281
+ }
282
+ /** First index of any search character at or after `start` (content length when absent). */
283
+ function indexOfAnyFrom(content, start, chars) {
284
+ let best = content.length;
285
+ for (const char of chars) {
286
+ const found = content.indexOf(char, start);
287
+ if (found !== -1 && found < best) best = found;
288
+ }
289
+ return best;
290
+ }
291
+ /** Split masked text into alternating code and placeholder parts. */
292
+ function splitMasked(masked) {
293
+ const separatorPattern = new RegExp(`${escapeRegExpChar(masked.stringSep)}(\\d+)${escapeRegExpChar(masked.stringSep)}|${escapeRegExpChar(masked.commentSep)}(\\d+)${escapeRegExpChar(masked.commentSep)}`, "g");
294
+ const parts = [];
295
+ let last = 0;
296
+ for (const match of masked.masked.matchAll(separatorPattern)) {
297
+ if (match.index > last) parts.push({
298
+ text: masked.masked.slice(last, match.index),
299
+ isPlaceholder: false,
300
+ placeholderIndex: -1,
301
+ placeholderKind: "string"
302
+ });
303
+ const commentGroup = match[2];
304
+ const isComment = commentGroup !== void 0;
305
+ parts.push({
306
+ text: match[0],
307
+ isPlaceholder: true,
308
+ placeholderIndex: Number(isComment ? commentGroup : match[1]),
309
+ placeholderKind: isComment ? "comment" : "string"
310
+ });
311
+ last = match.index + match[0].length;
312
+ }
313
+ if (last < masked.masked.length) parts.push({
314
+ text: masked.masked.slice(last),
315
+ isPlaceholder: false,
316
+ placeholderIndex: -1,
317
+ placeholderKind: "string"
318
+ });
319
+ return parts;
320
+ }
321
+ /** Escape a single separator character for interpolation into a RegExp source. */
322
+ function escapeRegExpChar(char) {
323
+ return /[.*+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
324
+ }
325
+ /** Restore placeholders from `rebuiltMasked`, substituting `rebuildString` output for each string. */
326
+ function unmask(rebuiltMasked, masked, rebuildString) {
327
+ const stringPattern = new RegExp(`${escapeRegExpChar(masked.stringSep)}(\\d+)${escapeRegExpChar(masked.stringSep)}`, "g");
328
+ return rebuiltMasked.replace(stringPattern, (_whole, digits) => rebuildString(Number(digits)));
329
+ }
330
+ /**
331
+ * Remove comments while keeping every newline they contained, so line numbering stays stable.
332
+ * The `#!` shebang on line 1 of javascript files is preserved verbatim.
333
+ */
334
+ function stripComments(content, language) {
335
+ const masked = maskContent(content, language);
336
+ return unmask(splitMasked(masked).map((part) => {
337
+ if (!part.isPlaceholder) return part.text;
338
+ if (part.placeholderKind === "string") return `${masked.stringSep}${part.placeholderIndex}${masked.stringSep}`;
339
+ const original = masked.comments[part.placeholderIndex];
340
+ if (masked.commentKinds[part.placeholderIndex]) return original;
341
+ return original.replace(/[^\r\n]/g, "");
342
+ }).join(""), masked, (index) => masked.strings[index]);
343
+ }
344
+ /** Whether a quoted literal is eligible for runtime-decode encryption. */
345
+ function isEligible(content, quote, excludes) {
346
+ const body = content.slice(1, -1);
347
+ if (body.length === 0) return false;
348
+ if (body.includes("%") || body.includes("$")) return false;
349
+ if (body.startsWith("./") || body.startsWith("../") || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(body)) return false;
350
+ for (const exclude of excludes) if (new RegExp(exclude).test(body)) return false;
351
+ let index = 0;
352
+ while (index < body.length) {
353
+ if (body[index] === "\\") {
354
+ const next = body[index + 1];
355
+ if (next !== "\\" && next !== quote && next !== "n" && next !== "t" && next !== "r") return false;
356
+ index += 2;
357
+ continue;
358
+ }
359
+ if (body[index] === quote) return false;
360
+ index += 1;
361
+ }
362
+ return true;
363
+ }
364
+ /** UTF-16 code units for dart/javascript decode expressions (surrogate pairs for astral code points). */
365
+ function utf16Units(content) {
366
+ const units = [];
367
+ for (const char of content) {
368
+ const codePoint = char.codePointAt(0);
369
+ if (codePoint > 65535) {
370
+ const offset = codePoint - 65536;
371
+ units.push(55296 + (offset >> 10), 56320 + (offset & 1023));
372
+ } else units.push(codePoint);
373
+ }
374
+ return units;
375
+ }
376
+ /** Runtime-decode expression replacing the literal, per language. */
377
+ function encodeString(content, language) {
378
+ if (language === "dart") return `String.fromCharCodes(const [${utf16Units(content).join(", ")}])`;
379
+ if (language === "javascript") return `String.fromCharCode(${utf16Units(content).join(", ")})`;
380
+ if (language === "java") return `new String(new byte[]{${Array.from(Buffer.from(content, "utf8")).join(",")}}, java.nio.charset.StandardCharsets.UTF_8)`;
381
+ if (language === "kotlin") return `String(byteArrayOf(${Array.from(Buffer.from(content, "utf8")).join(",")}), Charsets.UTF_8)`;
382
+ if (language === "swift") return `String(bytes: [${Array.from(Buffer.from(content, "utf8")).map((byte) => `UInt8(${byte})`).join(", ")}], encoding: .utf8)!`;
383
+ return `string([]byte{${Array.from(Buffer.from(content, "utf8")).join(",")}})`;
384
+ }
385
+ /**
386
+ * Replace each eligible string literal with a deterministic runtime-decode expression.
387
+ * Eligible literals are non-empty, single/double-quoted (templates, raw and triple-quoted
388
+ * literals are never touched), contain no `%`, no `$`, no path/URL prefix, no disallowed
389
+ * backslash escapes, and match none of the `excludes` regexes.
390
+ */
391
+ function encryptStringLiterals(content, language, excludes, seed) {
392
+ const masked = maskContent(content, language);
393
+ const encrypted = /* @__PURE__ */ new Map();
394
+ masked.strings.forEach((literal, index) => {
395
+ const kind = masked.stringKinds[index];
396
+ if (kind === "single" || kind === "double") {
397
+ if (isEligible(literal, kind === "single" ? "'" : "\"", excludes)) encrypted.set(index, encodeString(literal.slice(1, -1), language));
398
+ }
399
+ });
400
+ return unmask(masked.masked, masked, (index) => encrypted.get(index) ?? masked.strings[index]);
401
+ }
402
+ /**
403
+ * Rename identifiers in one combined pass: whole-word `mappings` (longest key first) run before
404
+ * `patterns` (regular expressions whose replacements may use $1-style group references). Both
405
+ * operate only on code; string literals and comments are masked and restored untouched.
406
+ */
407
+ function renameIdentifiers(content, language, mappings = {}, patterns = []) {
408
+ const masked = maskContent(content, language);
409
+ const parts = splitMasked(masked);
410
+ let rebuilt = "";
411
+ for (const part of parts) if (!part.isPlaceholder) {
412
+ let text = replaceWholeWords(part.text, mappings).text;
413
+ for (const pattern of patterns) text = text.replace(new RegExp(pattern.match, "g"), pattern.replacement);
414
+ rebuilt += text;
415
+ } else if (part.placeholderKind === "string") rebuilt += `${masked.stringSep}${part.placeholderIndex}${masked.stringSep}`;
416
+ else rebuilt += `${masked.commentSep}${part.placeholderIndex}${masked.commentSep}`;
417
+ return unmask(rebuilt, masked, (index) => masked.strings[index]);
418
+ }
419
+ /**
420
+ * Whole-word replacement of every `keywords` mapping. Applies inside code and inside string
421
+ * literals, but never inside comments.
422
+ */
423
+ function obfuscateKeywords(content, language, keywords) {
424
+ const masked = maskContent(content, language);
425
+ const replaced = replaceWholeWords(masked.masked, keywords).text;
426
+ return unmask(replaced, masked, (index) => masked.strings[index]);
427
+ }
428
+ const JUNK_FORMATS = {
429
+ dart: (name, value) => `final ${name} = ${value};`,
430
+ java: (name, value) => `class ${name.charAt(0).toUpperCase()}${name.slice(1)} { static { int j = ${value}; } }`,
431
+ kotlin: (name, value) => `val ${name} = ${value}`,
432
+ swift: (name, value) => `private enum ${name.charAt(0).toUpperCase()}${name.slice(1)} { static let j = ${value} }`,
433
+ javascript: (name, value) => `const ${name} = ${value}; void ${name};`,
434
+ go: (name, value) => `var ${name} = ${value}`
435
+ };
436
+ /**
437
+ * Conservative per-language declaration scanners for seed-generated renames. Every pattern
438
+ * anchors at a line start so indented methods (framework callbacks like Flutter's `build`)
439
+ * are never mistaken for declarations; statement call sites such as `return foo(` are
440
+ * filtered out afterwards.
441
+ */
442
+ const TYPE_DECLARATION = /(?:^|\n)(?:(?:abstract|base|final|interface|sealed|open|data|public|private|protected|internal|static)\s+)*(?:class|interface|enum|struct|object|union|mixin|extension|protocol|record)\s+([A-Za-z_$][\w$]*)/g;
443
+ const TOP_LEVEL_FUNCTION = /(?:^|\n)(?:[A-Za-z_$][\w$<>.,?\[\]]*\s+)+([A-Za-z_$][\w$]*)\s*\(/g;
444
+ const STATEMENT_PREFIXES = {
445
+ return: true,
446
+ throw: true,
447
+ case: true,
448
+ yield: true,
449
+ await: true,
450
+ else: true,
451
+ do: true,
452
+ goto: true
453
+ };
454
+ /** Names that must never be generated as rename targets (entrypoints, keywords, common false positives). */
455
+ const NEVER_RENAME = {
456
+ main: true,
457
+ init: true,
458
+ on: true,
459
+ get: true,
460
+ set: true,
461
+ build: true,
462
+ new: true,
463
+ if: true,
464
+ else: true,
465
+ for: true,
466
+ while: true,
467
+ do: true,
468
+ switch: true,
469
+ return: true,
470
+ class: true,
471
+ struct: true,
472
+ enum: true,
473
+ func: true,
474
+ fun: true,
475
+ function: true,
476
+ void: true,
477
+ var: true,
478
+ val: true,
479
+ let: true,
480
+ const: true,
481
+ final: true,
482
+ static: true,
483
+ import: true,
484
+ export: true,
485
+ package: true,
486
+ type: true,
487
+ case: true,
488
+ default: true,
489
+ break: true,
490
+ continue: true,
491
+ try: true,
492
+ catch: true,
493
+ throw: true,
494
+ async: true,
495
+ await: true,
496
+ in: true,
497
+ is: true,
498
+ as: true,
499
+ of: true,
500
+ to: true,
501
+ from: true,
502
+ this: true,
503
+ super: true,
504
+ null: true,
505
+ true: true,
506
+ false: true
507
+ };
508
+ const DECLARATION_PATTERNS = {
509
+ dart: [TYPE_DECLARATION, TOP_LEVEL_FUNCTION],
510
+ java: [TYPE_DECLARATION],
511
+ kotlin: [TYPE_DECLARATION, /(?:^|\n)fun\s+(?:<[^>\n]*>\s*)?([A-Za-z_$][\w$]*)\s*\(/g],
512
+ swift: [TYPE_DECLARATION, /(?:^|\n)func\s+([A-Za-z_$][\w$]*)\s*\(/g],
513
+ javascript: [
514
+ TYPE_DECLARATION,
515
+ /(?:^|\n)(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g,
516
+ /(?:^|\n)const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\(|function)/g
517
+ ],
518
+ go: [/(?:^|\n)type\s+([A-Za-z_]\w*)\s+(?:struct|interface)\b/g, /(?:^|\n)func\s+([A-Za-z_]\w*)\s*\(/g]
519
+ };
520
+ /** Extract declared type/function names worth renaming; methods and statement sites stay untouched. */
521
+ function extractDeclaredIdentifiers(content, language) {
522
+ const names = [];
523
+ for (const pattern of DECLARATION_PATTERNS[language]) {
524
+ pattern.lastIndex = 0;
525
+ for (const match of content.matchAll(pattern)) {
526
+ const name = match[1];
527
+ if (name in NEVER_RENAME) continue;
528
+ if (pattern === TOP_LEVEL_FUNCTION) {
529
+ const lineStart = content.lastIndexOf("\n", match.index) + 1;
530
+ const prefix = content.slice(lineStart, match.index).trim().split(/\s+/).pop();
531
+ if (prefix && prefix in STATEMENT_PREFIXES) continue;
532
+ }
533
+ names.push(name);
534
+ }
535
+ }
536
+ return names;
537
+ }
538
+ /**
539
+ * Deterministically pick `count` of the declared names (seeded hash ordering) and map each
540
+ * to a seed-derived token. Identical (declared, count, seed) always yields the same mapping;
541
+ * a different seed yields a different selection and different tokens.
542
+ */
543
+ function generateIdentifierMappings(declared, count, seed) {
544
+ const selected = [...new Set(declared)].filter((name) => !(name in NEVER_RENAME)).map((name) => ({
545
+ name,
546
+ score: deriveHash(seed, "identifier-select", name)
547
+ })).sort((a, b) => a.score < b.score ? -1 : a.score > b.score ? 1 : 0).slice(0, Math.max(0, count));
548
+ const mappings = {};
549
+ for (const { name } of selected) mappings[name] = deriveToken(seed, "identifier", name);
550
+ return mappings;
551
+ }
552
+ /**
553
+ * Append `lines` deterministic junk statements at end of file, ensuring exactly one trailing
554
+ * newline before them. Names and values derive from the seed and the project-relative file
555
+ * path, so two files never share junk names. Non-positive `lines` returns the input unchanged.
556
+ */
557
+ function insertJunkCode(content, language, lines, seed, filePath) {
558
+ if (content.length === 0 || lines <= 0) return content;
559
+ const base = content.replace(/[\t \r\n]+$/, "");
560
+ const statements = [];
561
+ for (let i = 0; i < lines; i += 1) {
562
+ const name = deriveToken(seed, filePath, "junk", `${i}`);
563
+ const value = seededInt(seed, `${filePath}:junk:${i}`, 0, 2147483647);
564
+ statements.push(JUNK_FORMATS[language](name, value));
565
+ }
566
+ return `${base}\n${statements.join("\n")}\n`;
567
+ }
568
+ /**
569
+ * Apply the code transforms in fixed order — stripComments, encryptStringLiterals,
570
+ * renameIdentifiers, obfuscateKeywords, insertJunkCode — each only when enabled in `options`.
571
+ * Deterministic: identical arguments yield byte-identical output. Idempotence is not a
572
+ * contract; the orchestrator restores pristine sources between runs.
573
+ */
574
+ function transformCode(content, language, options, seed, filePath) {
575
+ let result = content;
576
+ if (options.removeComments === true) result = stripComments(result, language);
577
+ if (options.encryptStrings) result = encryptStringLiterals(result, language, options.encryptStrings.excludes, seed);
578
+ if (options.renameIdentifiers) result = renameIdentifiers(result, language, options.renameIdentifiers.mappings, options.renameIdentifiers.patterns);
579
+ if (options.keywords && Object.keys(options.keywords).length > 0) result = obfuscateKeywords(result, language, options.keywords);
580
+ if (options.junkCode) result = insertJunkCode(result, language, options.junkCode.lines, seed, filePath);
581
+ return result;
582
+ }
583
+ export { encryptStringLiterals, extractDeclaredIdentifiers, generateIdentifierMappings, insertJunkCode, obfuscateKeywords, renameIdentifiers, stripComments, transformCode };
584
+
585
+ //# sourceMappingURL=code.js.map