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,93 @@
1
+ import { basenameWithoutExtension, classifyLanguage, deriveHash, deriveToken, isGeneratedCode, isResourceFile, walkProject, writeIfChanged } from "./common.js";
2
+ import { extractDeclaredIdentifiers, generateIdentifierMappings, transformCode } from "./code.js";
3
+ import { applyResourceRenames, perturbImageHashes, planResourceRenames, recolorImages } from "./resources.js";
4
+ import { obfuscateDirectories, remapUuids, renameProjectConfig } from "./project.js";
5
+ import { filterSensitiveWords, recolorSourceColors } from "./content.js";
6
+ import { readFileSync } from "node:fs";
7
+ // src/obfuscate.ts
8
+ /**
9
+ * Deterministic in-place obfuscation of an owned project directory. Pass order is fixed so
10
+ * reference-bearing renames happen before string/code transforms consume final names:
11
+ * project config -> resource renames -> resource payloads -> content -> code.
12
+ */
13
+ function applyObfuscation(root, config, seed, framework) {
14
+ const changed = /* @__PURE__ */ new Set();
15
+ const transforms = {};
16
+ let unsupported = 0;
17
+ const record = (name, outcome) => {
18
+ transforms[name] = outcome.changed;
19
+ unsupported += outcome.unsupported ?? 0;
20
+ for (const relative of outcome.changedPaths) changed.add(relative);
21
+ };
22
+ let walked = walkProject(root);
23
+ if (config.project?.name) record("projectName", renameProjectConfig(walked.files, framework, config.project.name));
24
+ if (config.project?.directories.length) {
25
+ record("projectDirectories", obfuscateDirectories(root, walked.files, config.project.directories, seed));
26
+ walked = walkProject(root);
27
+ }
28
+ if (config.project?.uuids) record("projectUuids", remapUuids(walked.files, seed));
29
+ if (config.resources?.rename) {
30
+ const mappings = { ...config.resources.rename.mappings };
31
+ if (config.resources.rename.generated) {
32
+ const selected = [...new Set(walked.files.filter((entry) => isResourceFile(entry.relative)).map((entry) => basenameWithoutExtension(entry.relative)))].map((stem) => ({
33
+ stem,
34
+ score: deriveHash(seed, "resource-select", stem)
35
+ })).sort((a, b) => a.score < b.score ? -1 : a.score > b.score ? 1 : 0).slice(0, config.resources.rename.generated);
36
+ for (const { stem } of selected) mappings[stem] = deriveToken(seed, "resource", stem);
37
+ }
38
+ const plan = planResourceRenames(walked.files, mappings);
39
+ record("resourceRename", applyResourceRenames(root, walked.files, plan));
40
+ walked = walkProject(root);
41
+ }
42
+ if (config.resources?.hashPerturb) record("resourceHash", perturbImageHashes(walked.files, config.resources.hashPerturb.bytes, seed));
43
+ if (config.resources?.images) record("imageRecolor", recolorImages(walked.files, config.resources.images, seed));
44
+ if (config.content?.sensitiveWords) record("sensitiveWords", filterSensitiveWords(walked.files, config.content.sensitiveWords));
45
+ if (config.content?.controlColors || config.content?.fontColors) record("sourceColors", recolorSourceColors(walked.files, config.content.controlColors, config.content.fontColors));
46
+ if (config.code) {
47
+ let generatedMappings = {};
48
+ if (config.code.renameIdentifiers?.generated) {
49
+ const declared = [];
50
+ for (const entry of walked.files) {
51
+ if (isGeneratedCode(entry.relative)) continue;
52
+ const language = classifyLanguage(entry.relative);
53
+ if (language) declared.push(...extractDeclaredIdentifiers(readFileSync(entry.absolute, "utf8"), language));
54
+ }
55
+ generatedMappings = generateIdentifierMappings(declared, config.code.renameIdentifiers.generated, seed);
56
+ }
57
+ const hasGenerated = Object.keys(generatedMappings).length > 0;
58
+ let count = 0;
59
+ for (const entry of walked.files) {
60
+ const language = classifyLanguage(entry.relative);
61
+ if (!language) continue;
62
+ const options = hasGenerated && config.code.renameIdentifiers && !isGeneratedCode(entry.relative) ? {
63
+ ...config.code,
64
+ renameIdentifiers: {
65
+ ...config.code.renameIdentifiers,
66
+ mappings: {
67
+ ...generatedMappings,
68
+ ...config.code.renameIdentifiers.mappings
69
+ }
70
+ }
71
+ } : config.code;
72
+ const before = readFileSync(entry.absolute, "utf8");
73
+ const next = transformCode(before, language, options, seed, entry.relative);
74
+ if (next !== before) {
75
+ writeIfChanged(entry, next);
76
+ count += 1;
77
+ changed.add(entry.relative);
78
+ }
79
+ }
80
+ transforms.code = count;
81
+ }
82
+ return {
83
+ changedFiles: changed.size,
84
+ transforms,
85
+ skipped: {
86
+ generated: walked.skipped,
87
+ unsupported
88
+ }
89
+ };
90
+ }
91
+ export { applyObfuscation };
92
+
93
+ //# sourceMappingURL=obfuscate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obfuscate.js","names":[],"sources":["../src/obfuscate.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport type { Framework, TransformOutcome } from './common.js';\nimport { basenameWithoutExtension, classifyLanguage, deriveHash, deriveToken, isGeneratedCode, isResourceFile, walkProject, writeIfChanged } from './common.js';\nimport type { ObfuscatorConfig } from './config.js';\nimport { extractDeclaredIdentifiers, generateIdentifierMappings, transformCode } from './code.js';\nimport { applyResourceRenames, perturbImageHashes, planResourceRenames, recolorImages } from './resources.js';\nimport { obfuscateDirectories, remapUuids, renameProjectConfig } from './project.js';\nimport { filterSensitiveWords, recolorSourceColors } from './content.js';\n\nexport interface ObfuscationReport {\n /** Distinct owned files or directories whose content, path or binary payload changed. */\n changedFiles: number;\n /** Per-feature counts of changed files (or renamed entries). */\n transforms: Record<string, number>;\n skipped: { generated: number; unsupported: number };\n}\n\n/**\n * Deterministic in-place obfuscation of an owned project directory. Pass order is fixed so\n * reference-bearing renames happen before string/code transforms consume final names:\n * project config -> resource renames -> resource payloads -> content -> code.\n */\nexport function applyObfuscation(root: string, config: ObfuscatorConfig, seed: string, framework: Framework): ObfuscationReport {\n const changed = new Set<string>();\n const transforms: Record<string, number> = {};\n let unsupported = 0;\n const record = (name: string, outcome: TransformOutcome): void => {\n transforms[name] = outcome.changed;\n unsupported += outcome.unsupported ?? 0;\n for (const relative of outcome.changedPaths) changed.add(relative);\n };\n\n let walked = walkProject(root);\n\n // 1. Project configuration transforms.\n if (config.project?.name) record('projectName', renameProjectConfig(walked.files, framework, config.project.name));\n if (config.project?.directories.length) {\n record('projectDirectories', obfuscateDirectories(root, walked.files, config.project.directories, seed));\n walked = walkProject(root);\n }\n if (config.project?.uuids) record('projectUuids', remapUuids(walked.files, seed));\n\n // 2. Resource transforms (path changes before content transforms).\n if (config.resources?.rename) {\n const mappings = { ...config.resources.rename.mappings };\n if (config.resources.rename.generated) {\n const stems = [...new Set(walked.files.filter(entry => isResourceFile(entry.relative)).map(entry => basenameWithoutExtension(entry.relative)))];\n const selected = stems\n .map(stem => ({ stem, score: deriveHash(seed, 'resource-select', stem) }))\n .sort((a, b) => a.score < b.score ? -1 : a.score > b.score ? 1 : 0)\n .slice(0, config.resources.rename.generated);\n for (const { stem } of selected) mappings[stem] = deriveToken(seed, 'resource', stem);\n }\n const plan = planResourceRenames(walked.files, mappings);\n record('resourceRename', applyResourceRenames(root, walked.files, plan));\n walked = walkProject(root);\n }\n if (config.resources?.hashPerturb) record('resourceHash', perturbImageHashes(walked.files, config.resources.hashPerturb.bytes, seed));\n if (config.resources?.images) record('imageRecolor', recolorImages(walked.files, config.resources.images, seed));\n\n // 3. Content transforms.\n if (config.content?.sensitiveWords) record('sensitiveWords', filterSensitiveWords(walked.files, config.content.sensitiveWords));\n if (config.content?.controlColors || config.content?.fontColors) {\n record('sourceColors', recolorSourceColors(walked.files, config.content.controlColors, config.content.fontColors));\n }\n\n // 4. Code transforms.\n if (config.code) {\n // Seed-generated renames need a whole-project pass first: collect declared names, pick a\n // seeded selection, then rename every reference consistently (except generated code files,\n // whose serializers must keep referencing the original names).\n let generatedMappings: Record<string, string> = {};\n if (config.code.renameIdentifiers?.generated) {\n const declared: string[] = [];\n for (const entry of walked.files) {\n if (isGeneratedCode(entry.relative)) continue;\n const language = classifyLanguage(entry.relative);\n if (language) declared.push(...extractDeclaredIdentifiers(readFileSync(entry.absolute, 'utf8'), language));\n }\n generatedMappings = generateIdentifierMappings(declared, config.code.renameIdentifiers.generated, seed);\n }\n const hasGenerated = Object.keys(generatedMappings).length > 0;\n let count = 0;\n for (const entry of walked.files) {\n const language = classifyLanguage(entry.relative);\n if (!language) continue;\n const options = hasGenerated && config.code.renameIdentifiers && !isGeneratedCode(entry.relative)\n ? { ...config.code, renameIdentifiers: { ...config.code.renameIdentifiers, mappings: { ...generatedMappings, ...config.code.renameIdentifiers.mappings } } }\n : config.code;\n const before = readFileSync(entry.absolute, 'utf8');\n const next = transformCode(before, language, options, seed, entry.relative);\n if (next !== before) {\n writeIfChanged(entry, next);\n count += 1;\n changed.add(entry.relative);\n }\n }\n transforms.code = count;\n }\n\n return { changedFiles: changed.size, transforms, skipped: { generated: walked.skipped, unsupported } };\n}\n"],"mappings":"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"}
@@ -0,0 +1,283 @@
1
+ import { deflateSync, inflateSync } from "node:zlib";
2
+ // src/png.ts
3
+ const PNG_SIGNATURE = Buffer.from([
4
+ 137,
5
+ 80,
6
+ 78,
7
+ 71,
8
+ 13,
9
+ 10,
10
+ 26,
11
+ 10
12
+ ]);
13
+ const CRC_TABLE = /* @__PURE__ */ new Uint32Array(256);
14
+ for (let n = 0; n < 256; n += 1) {
15
+ let value = n;
16
+ for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 3988292384 ^ value >>> 1 : value >>> 1;
17
+ CRC_TABLE[n] = value >>> 0;
18
+ }
19
+ function crc32(data) {
20
+ let crc = 4294967295;
21
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
22
+ return (crc ^ 4294967295) >>> 0;
23
+ }
24
+ /** Split a PNG byte stream into chunks without validating payload CRCs. */
25
+ function parseChunks(buffer) {
26
+ if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) return void 0;
27
+ const chunks = [];
28
+ let offset = 8;
29
+ while (offset + 12 <= buffer.length) {
30
+ const length = buffer.readUInt32BE(offset);
31
+ if (offset + 12 + length > buffer.length) return void 0;
32
+ const type = buffer.toString("latin1", offset + 4, offset + 8);
33
+ const data = buffer.subarray(offset + 8, offset + 8 + length);
34
+ chunks.push({
35
+ type,
36
+ data
37
+ });
38
+ offset += 12 + length;
39
+ if (type === "IEND") break;
40
+ }
41
+ return chunks;
42
+ }
43
+ function buildChunk(type, data) {
44
+ const header = Buffer.alloc(8);
45
+ header.writeUInt32BE(data.length, 0);
46
+ header.write(type, 4, "latin1");
47
+ const crc = Buffer.alloc(4);
48
+ crc.writeUInt32BE(crc32(Buffer.concat([header.subarray(4), data])), 0);
49
+ return Buffer.concat([
50
+ header,
51
+ data,
52
+ crc
53
+ ]);
54
+ }
55
+ /** Paeth predictor, as specified for PNG filter type 4. */
56
+ function paeth(a, b, c) {
57
+ const p = a + b - c;
58
+ const pa = Math.abs(p - a);
59
+ const pb = Math.abs(p - b);
60
+ const pc = Math.abs(p - c);
61
+ if (pa <= pb && pa <= pc) return a;
62
+ if (pb <= pc) return b;
63
+ return c;
64
+ }
65
+ const CHANNELS = {
66
+ 0: 1,
67
+ 2: 3,
68
+ 3: 1,
69
+ 4: 2,
70
+ 6: 4
71
+ };
72
+ /** Undo scanline filters, returning the unfiltered sample bytes. */
73
+ function unfilter(raw, width, height, channels, bitDepth) {
74
+ const bitsPerPixel = channels * bitDepth;
75
+ const bytesPerRow = Math.ceil(width * bitsPerPixel / 8);
76
+ const bpp = Math.max(1, Math.floor(bitsPerPixel / 8));
77
+ const out = Buffer.alloc(bytesPerRow * height);
78
+ const previous = Buffer.alloc(bytesPerRow);
79
+ let source = 0;
80
+ for (let y = 0; y < height; y += 1) {
81
+ const filter = raw[source];
82
+ source += 1;
83
+ if (filter > 4) return Buffer.alloc(0);
84
+ const row = raw.subarray(source, source + bytesPerRow);
85
+ if (row.length !== bytesPerRow) return Buffer.alloc(0);
86
+ source += bytesPerRow;
87
+ const target = out.subarray(y * bytesPerRow, (y + 1) * bytesPerRow);
88
+ for (let x = 0; x < bytesPerRow; x += 1) {
89
+ const a = x >= bpp ? target[x - bpp] : 0;
90
+ const b = previous[x];
91
+ const c = x >= bpp ? previous[x - bpp] : 0;
92
+ let value = row[x];
93
+ if (filter === 1) value = value + a & 255;
94
+ else if (filter === 2) value = value + b & 255;
95
+ else if (filter === 3) value = value + (a + b >> 1) & 255;
96
+ else if (filter === 4) value = value + paeth(a, b, c) & 255;
97
+ target[x] = value;
98
+ }
99
+ previous.set(target);
100
+ }
101
+ return out;
102
+ }
103
+ /** Expand unfiltered samples into RGBA 8-bit pixels. Returns undefined for invalid input. */
104
+ function expand(raw, width, height, bitDepth, colorType, palette, transparent) {
105
+ const channels = CHANNELS[colorType];
106
+ if (channels === void 0 || colorType === 3 && (!palette || palette.length % 3 !== 0)) return void 0;
107
+ const bitsPerPixel = channels * bitDepth;
108
+ const bytesPerRow = Math.ceil(width * bitsPerPixel / 8);
109
+ if (raw.length < bytesPerRow * height) return void 0;
110
+ const pixels = new Uint8Array(width * height * 4);
111
+ const scale = bitDepth < 8 ? 255 / ((1 << bitDepth) - 1) : 1;
112
+ for (let y = 0; y < height; y += 1) {
113
+ const rowOffset = y * bytesPerRow;
114
+ for (let x = 0; x < width; x += 1) {
115
+ const sampleAt = (sample) => {
116
+ if (bitDepth === 8) return raw[rowOffset + sample];
117
+ if (bitDepth === 16) return raw[rowOffset + sample * 2];
118
+ const bit = sample * bitDepth;
119
+ return raw[rowOffset + (bit >> 3)] >> 8 - bitDepth - (bit & 7) & (1 << bitDepth) - 1;
120
+ };
121
+ const sampleOffset = x * channels;
122
+ const out = (y * width + x) * 4;
123
+ let r = 0, g = 0, b = 0, a = 255;
124
+ if (colorType === 0) {
125
+ const gray = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);
126
+ r = g = b = gray;
127
+ if (transparent && transparent.length >= 2 && transparent[0] === gray) a = 0;
128
+ } else if (colorType === 2) {
129
+ r = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);
130
+ g = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);
131
+ b = bitDepth === 16 ? sampleAt(sampleOffset + 2) : Math.round(sampleAt(sampleOffset + 2) * scale);
132
+ if (transparent && transparent.length >= 6 && transparent[0] === r && transparent[2] === g && transparent[4] === b) a = 0;
133
+ } else if (colorType === 3) {
134
+ const index = sampleAt(sampleOffset);
135
+ const entry = index * 3;
136
+ if (!palette || entry + 2 >= palette.length) return void 0;
137
+ r = palette[entry];
138
+ g = palette[entry + 1];
139
+ b = palette[entry + 2];
140
+ if (transparent && index < transparent.length) a = transparent[index];
141
+ } else if (colorType === 4) {
142
+ r = g = b = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);
143
+ a = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);
144
+ } else {
145
+ r = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);
146
+ g = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);
147
+ b = bitDepth === 16 ? sampleAt(sampleOffset + 2) : Math.round(sampleAt(sampleOffset + 2) * scale);
148
+ a = bitDepth === 16 ? sampleAt(sampleOffset + 3) : Math.round(sampleAt(sampleOffset + 3) * scale);
149
+ }
150
+ pixels[out] = r;
151
+ pixels[out + 1] = g;
152
+ pixels[out + 2] = b;
153
+ pixels[out + 3] = a;
154
+ }
155
+ }
156
+ return pixels;
157
+ }
158
+ /**
159
+ * Decode an owned PNG into RGBA pixels. Returns undefined for non-PNG input,
160
+ * interlaced images, reserved filters, truncated data or invalid palette references.
161
+ */
162
+ function parsePNG(buffer) {
163
+ const chunks = parseChunks(buffer);
164
+ if (!chunks) return void 0;
165
+ const ihdr = chunks.find((value) => value.type === "IHDR");
166
+ if (!ihdr || ihdr.data.length !== 13) return void 0;
167
+ const width = ihdr.data.readUInt32BE(0);
168
+ const height = ihdr.data.readUInt32BE(4);
169
+ const bitDepth = ihdr.data[8];
170
+ const colorType = ihdr.data[9];
171
+ if (ihdr.data[10] !== 0 || ihdr.data[11] !== 0) return void 0;
172
+ if (ihdr.data[12] !== 0) return void 0;
173
+ if (![
174
+ 1,
175
+ 2,
176
+ 4,
177
+ 8,
178
+ 16
179
+ ].includes(bitDepth) || !(colorType in CHANNELS)) return void 0;
180
+ if (width === 0 || height === 0) return void 0;
181
+ const palette = chunks.find((value) => value.type === "PLTE")?.data;
182
+ const transparent = chunks.find((value) => value.type === "tRNS")?.data;
183
+ const idat = Buffer.concat(chunks.filter((value) => value.type === "IDAT").map((value) => value.data));
184
+ if (idat.length === 0) return void 0;
185
+ let raw;
186
+ try {
187
+ raw = inflateSync(idat);
188
+ } catch {
189
+ return;
190
+ }
191
+ const channels = CHANNELS[colorType];
192
+ const expected = Math.ceil(width * channels * bitDepth / 8) * height + height;
193
+ if (raw.length < expected) return void 0;
194
+ const samples = unfilter(raw, width, height, channels, bitDepth);
195
+ if (samples.length === 0) return void 0;
196
+ const pixels = expand(samples, width, height, bitDepth, colorType, palette, transparent);
197
+ return pixels ? {
198
+ width,
199
+ height,
200
+ pixels
201
+ } : void 0;
202
+ }
203
+ /** Raw scanline stream for RGBA 8-bit pixels, filter type 0 (None) on every row. */
204
+ function buildRaw(image) {
205
+ const bytesPerRow = image.width * 4;
206
+ const raw = Buffer.alloc((bytesPerRow + 1) * image.height);
207
+ for (let y = 0; y < image.height; y += 1) {
208
+ raw[y * (bytesPerRow + 1)] = 0;
209
+ raw.set(image.pixels.subarray(y * bytesPerRow, (y + 1) * bytesPerRow), y * (bytesPerRow + 1) + 1);
210
+ }
211
+ return raw;
212
+ }
213
+ /**
214
+ * Splice recolorable pixels back into an owned PNG: IHDR becomes 8-bit RGBA,
215
+ * PLTE/tRNS are dropped, every IDAT chunk is replaced by one rebuilt IDAT, and all
216
+ * other chunks (text, gamma, time, …) keep their original order. Returns undefined
217
+ * when the input is not a parseable PNG.
218
+ */
219
+ function encodePNG(original, image) {
220
+ const chunks = parseChunks(original);
221
+ if (!chunks || !chunks.some((value) => value.type === "IHDR")) return void 0;
222
+ const ihdr = Buffer.alloc(13);
223
+ ihdr.writeUInt32BE(image.width, 0);
224
+ ihdr.writeUInt32BE(image.height, 4);
225
+ ihdr[8] = 8;
226
+ ihdr[9] = 6;
227
+ const idat = deflateSync(buildRaw(image));
228
+ const parts = [PNG_SIGNATURE];
229
+ let emitted = false;
230
+ for (const { type, data } of chunks) {
231
+ if (type === "IEND" || type === "PLTE" || type === "tRNS") continue;
232
+ if (type === "IHDR") {
233
+ parts.push(buildChunk("IHDR", ihdr));
234
+ continue;
235
+ }
236
+ if (type === "IDAT") {
237
+ if (!emitted) {
238
+ parts.push(buildChunk("IDAT", idat));
239
+ emitted = true;
240
+ }
241
+ continue;
242
+ }
243
+ parts.push(buildChunk(type, data));
244
+ }
245
+ if (!emitted) parts.push(buildChunk("IDAT", idat));
246
+ parts.push(buildChunk("IEND", Buffer.alloc(0)));
247
+ return Buffer.concat(parts);
248
+ }
249
+ /** Build a fresh minimal 8-bit RGBA PNG (used by fixtures and standalone tests). */
250
+ function buildPNG(image) {
251
+ const ihdr = Buffer.alloc(13);
252
+ ihdr.writeUInt32BE(image.width, 0);
253
+ ihdr.writeUInt32BE(image.height, 4);
254
+ ihdr[8] = 8;
255
+ ihdr[9] = 6;
256
+ const idat = deflateSync(buildRaw(image));
257
+ return Buffer.concat([
258
+ PNG_SIGNATURE,
259
+ buildChunk("IHDR", ihdr),
260
+ buildChunk("IDAT", idat),
261
+ buildChunk("IEND", Buffer.alloc(0))
262
+ ]);
263
+ }
264
+ /**
265
+ * Insert an ancillary chunk directly after IHDR. PNG decoders ignore unknown ancillary
266
+ * chunk types, so a tEXt-style chunk changes the file hash without touching pixels.
267
+ * Returns undefined when the input is not a parseable PNG.
268
+ */
269
+ function insertPngChunk(buffer, type, data) {
270
+ const chunks = parseChunks(buffer);
271
+ if (!chunks) return void 0;
272
+ const ihdrIndex = chunks.findIndex((value) => value.type === "IHDR");
273
+ if (ihdrIndex === -1) return void 0;
274
+ const parts = [PNG_SIGNATURE];
275
+ chunks.forEach((value, index) => {
276
+ parts.push(buildChunk(value.type, value.data));
277
+ if (index === ihdrIndex) parts.push(buildChunk(type, data));
278
+ });
279
+ return Buffer.concat(parts);
280
+ }
281
+ export { buildPNG, encodePNG, insertPngChunk, parsePNG };
282
+
283
+ //# sourceMappingURL=png.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"png.js","names":[],"sources":["../src/png.ts"],"sourcesContent":["import { deflateSync, inflateSync } from 'node:zlib';\n\n/**\n * Minimal deterministic PNG engine for the obfuscator: decode owned PNGs to RGBA,\n * splice recolorable pixels back into the original chunk stream (ancillary chunks and\n * chunk order survive), and insert benign ancillary chunks to perturb file hashes.\n * Only 8-bit-per-sample, non-interlaced re-encodes are produced; decoding additionally\n * accepts 1/2/4/16-bit samples, palette (PLTE+tRNS) and gray(+alpha) source images.\n * Unsupported inputs (interlaced, truncated, foreign signatures) yield `undefined` so\n * callers can report them as unsupported instead of corrupting files.\n */\n\nexport interface PngImage {\n width: number;\n height: number;\n /** RGBA, 4 bytes per pixel, row-major. */\n pixels: Uint8Array;\n}\n\nconst PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n\ninterface PngChunk {\n type: string;\n data: Buffer;\n}\n\n// CRC-32 (IEEE 802.3, reflected, polynomial 0xEDB88320).\nconst CRC_TABLE = new Uint32Array(256);\nfor (let n = 0; n < 256; n += 1) {\n let value = n;\n for (let bit = 0; bit < 8; bit += 1) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;\n CRC_TABLE[n] = value >>> 0;\n}\n\nfunction crc32(data: Uint8Array): number {\n let crc = 0xffffffff;\n for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 0xff]! ^ (crc >>> 8);\n return (crc ^ 0xffffffff) >>> 0;\n}\n\n/** Split a PNG byte stream into chunks without validating payload CRCs. */\nfunction parseChunks(buffer: Buffer): PngChunk[] | undefined {\n if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) return undefined;\n const chunks: PngChunk[] = [];\n let offset = 8;\n while (offset + 12 <= buffer.length) {\n const length = buffer.readUInt32BE(offset);\n if (offset + 12 + length > buffer.length) return undefined;\n const type = buffer.toString('latin1', offset + 4, offset + 8);\n const data = buffer.subarray(offset + 8, offset + 8 + length);\n chunks.push({ type, data });\n offset += 12 + length;\n if (type === 'IEND') break;\n }\n return chunks;\n}\n\nfunction buildChunk(type: string, data: Buffer): Buffer {\n const header = Buffer.alloc(8);\n header.writeUInt32BE(data.length, 0);\n header.write(type, 4, 'latin1');\n const crc = Buffer.alloc(4);\n crc.writeUInt32BE(crc32(Buffer.concat([header.subarray(4), data])), 0);\n return Buffer.concat([header, data, crc]);\n}\n\n/** Paeth predictor, as specified for PNG filter type 4. */\nfunction paeth(a: number, b: number, c: number): number {\n const p = a + b - c;\n const pa = Math.abs(p - a);\n const pb = Math.abs(p - b);\n const pc = Math.abs(p - c);\n if (pa <= pb && pa <= pc) return a;\n if (pb <= pc) return b;\n return c;\n}\n\nconst CHANNELS: Readonly<Record<number, number>> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };\n\n/** Undo scanline filters, returning the unfiltered sample bytes. */\nfunction unfilter(raw: Buffer, width: number, height: number, channels: number, bitDepth: number): Buffer {\n const bitsPerPixel = channels * bitDepth;\n const bytesPerRow = Math.ceil((width * bitsPerPixel) / 8);\n const bpp = Math.max(1, Math.floor(bitsPerPixel / 8));\n const out = Buffer.alloc(bytesPerRow * height);\n const previous = Buffer.alloc(bytesPerRow);\n let source = 0;\n for (let y = 0; y < height; y += 1) {\n const filter = raw[source]!;\n source += 1;\n if (filter > 4) return Buffer.alloc(0); // reserved filter: undecodable\n const row = raw.subarray(source, source + bytesPerRow);\n if (row.length !== bytesPerRow) return Buffer.alloc(0);\n source += bytesPerRow;\n const target = out.subarray(y * bytesPerRow, (y + 1) * bytesPerRow);\n for (let x = 0; x < bytesPerRow; x += 1) {\n const a = x >= bpp ? target[x - bpp]! : 0;\n const b = previous[x]!;\n const c = x >= bpp ? previous[x - bpp]! : 0;\n let value = row[x]!;\n if (filter === 1) value = (value + a) & 0xff;\n else if (filter === 2) value = (value + b) & 0xff;\n else if (filter === 3) value = (value + ((a + b) >> 1)) & 0xff;\n else if (filter === 4) value = (value + paeth(a, b, c)) & 0xff;\n target[x] = value;\n }\n previous.set(target);\n }\n return out;\n}\n\n/** Expand unfiltered samples into RGBA 8-bit pixels. Returns undefined for invalid input. */\nfunction expand(\n raw: Buffer, width: number, height: number, bitDepth: number, colorType: number,\n palette: Buffer | undefined, transparent: Buffer | undefined,\n): Uint8Array | undefined {\n const channels = CHANNELS[colorType];\n if (channels === undefined || (colorType === 3 && (!palette || palette.length % 3 !== 0))) return undefined;\n const bitsPerPixel = channels * bitDepth;\n const bytesPerRow = Math.ceil((width * bitsPerPixel) / 8);\n if (raw.length < bytesPerRow * height) return undefined;\n const pixels = new Uint8Array(width * height * 4);\n const scale = bitDepth < 8 ? 255 / ((1 << bitDepth) - 1) : 1;\n\n for (let y = 0; y < height; y += 1) {\n const rowOffset = y * bytesPerRow;\n for (let x = 0; x < width; x += 1) {\n const sampleAt = (sample: number): number => {\n if (bitDepth === 8) return raw[rowOffset + sample]!;\n if (bitDepth === 16) return raw[rowOffset + sample * 2]!; // high byte\n const bit = sample * bitDepth;\n const byte = raw[rowOffset + (bit >> 3)]!;\n return (byte >> (8 - bitDepth - (bit & 7))) & ((1 << bitDepth) - 1);\n };\n const sampleOffset = x * channels;\n const out = (y * width + x) * 4;\n let r = 0, g = 0, b = 0, a = 255;\n if (colorType === 0) {\n const gray = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);\n r = g = b = gray;\n if (transparent && transparent.length >= 2 && transparent[0] === gray) a = 0;\n } else if (colorType === 2) {\n r = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);\n g = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);\n b = bitDepth === 16 ? sampleAt(sampleOffset + 2) : Math.round(sampleAt(sampleOffset + 2) * scale);\n if (transparent && transparent.length >= 6 && transparent[0] === r && transparent[2] === g && transparent[4] === b) a = 0;\n } else if (colorType === 3) {\n const index = sampleAt(sampleOffset);\n const entry = index * 3;\n if (!palette || entry + 2 >= palette.length) return undefined;\n r = palette[entry]!;\n g = palette[entry + 1]!;\n b = palette[entry + 2]!;\n if (transparent && index < transparent.length) a = transparent[index]!;\n } else if (colorType === 4) {\n const gray = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);\n r = g = b = gray;\n a = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);\n } else {\n r = bitDepth === 16 ? sampleAt(sampleOffset) : Math.round(sampleAt(sampleOffset) * scale);\n g = bitDepth === 16 ? sampleAt(sampleOffset + 1) : Math.round(sampleAt(sampleOffset + 1) * scale);\n b = bitDepth === 16 ? sampleAt(sampleOffset + 2) : Math.round(sampleAt(sampleOffset + 2) * scale);\n a = bitDepth === 16 ? sampleAt(sampleOffset + 3) : Math.round(sampleAt(sampleOffset + 3) * scale);\n }\n pixels[out] = r;\n pixels[out + 1] = g;\n pixels[out + 2] = b;\n pixels[out + 3] = a;\n }\n }\n return pixels;\n}\n\n/**\n * Decode an owned PNG into RGBA pixels. Returns undefined for non-PNG input,\n * interlaced images, reserved filters, truncated data or invalid palette references.\n */\nexport function parsePNG(buffer: Buffer): PngImage | undefined {\n const chunks = parseChunks(buffer);\n if (!chunks) return undefined;\n const ihdr = chunks.find(value => value.type === 'IHDR');\n if (!ihdr || ihdr.data.length !== 13) return undefined;\n const width = ihdr.data.readUInt32BE(0);\n const height = ihdr.data.readUInt32BE(4);\n const bitDepth = ihdr.data[8]!;\n const colorType = ihdr.data[9]!;\n if (ihdr.data[10] !== 0 || ihdr.data[11] !== 0) return undefined; // foreign compression/filter\n if (ihdr.data[12] !== 0) return undefined; // interlaced\n if (![1, 2, 4, 8, 16].includes(bitDepth) || !(colorType in CHANNELS)) return undefined;\n if (width === 0 || height === 0) return undefined;\n const palette = chunks.find(value => value.type === 'PLTE')?.data;\n const transparent = chunks.find(value => value.type === 'tRNS')?.data;\n const idat = Buffer.concat(chunks.filter(value => value.type === 'IDAT').map(value => value.data));\n if (idat.length === 0) return undefined;\n let raw: Buffer;\n try { raw = inflateSync(idat); } catch { return undefined; }\n const channels = CHANNELS[colorType]!;\n const expected = Math.ceil((width * channels * bitDepth) / 8) * height + height; // + filter byte per row\n if (raw.length < expected) return undefined;\n const samples = unfilter(raw, width, height, channels, bitDepth);\n if (samples.length === 0) return undefined;\n const pixels = expand(samples, width, height, bitDepth, colorType, palette, transparent);\n return pixels ? { width, height, pixels } : undefined;\n}\n\n/** Raw scanline stream for RGBA 8-bit pixels, filter type 0 (None) on every row. */\nfunction buildRaw(image: PngImage): Buffer {\n const bytesPerRow = image.width * 4;\n const raw = Buffer.alloc((bytesPerRow + 1) * image.height);\n for (let y = 0; y < image.height; y += 1) {\n raw[y * (bytesPerRow + 1)] = 0;\n raw.set(image.pixels.subarray(y * bytesPerRow, (y + 1) * bytesPerRow), y * (bytesPerRow + 1) + 1);\n }\n return raw;\n}\n\n/**\n * Splice recolorable pixels back into an owned PNG: IHDR becomes 8-bit RGBA,\n * PLTE/tRNS are dropped, every IDAT chunk is replaced by one rebuilt IDAT, and all\n * other chunks (text, gamma, time, …) keep their original order. Returns undefined\n * when the input is not a parseable PNG.\n */\nexport function encodePNG(original: Buffer, image: PngImage): Buffer | undefined {\n const chunks = parseChunks(original);\n if (!chunks || !chunks.some(value => value.type === 'IHDR')) return undefined;\n const ihdr = Buffer.alloc(13);\n ihdr.writeUInt32BE(image.width, 0);\n ihdr.writeUInt32BE(image.height, 4);\n ihdr[8] = 8; // bit depth\n ihdr[9] = 6; // color type RGBA\n const idat = deflateSync(buildRaw(image));\n const parts: Buffer[] = [PNG_SIGNATURE];\n let emitted = false;\n for (const { type, data } of chunks) {\n if (type === 'IEND' || type === 'PLTE' || type === 'tRNS') continue;\n if (type === 'IHDR') { parts.push(buildChunk('IHDR', ihdr)); continue; }\n if (type === 'IDAT') {\n if (!emitted) { parts.push(buildChunk('IDAT', idat)); emitted = true; }\n continue;\n }\n parts.push(buildChunk(type, data));\n }\n if (!emitted) parts.push(buildChunk('IDAT', idat));\n parts.push(buildChunk('IEND', Buffer.alloc(0)));\n return Buffer.concat(parts);\n}\n\n/** Build a fresh minimal 8-bit RGBA PNG (used by fixtures and standalone tests). */\nexport function buildPNG(image: PngImage): Buffer {\n const ihdr = Buffer.alloc(13);\n ihdr.writeUInt32BE(image.width, 0);\n ihdr.writeUInt32BE(image.height, 4);\n ihdr[8] = 8;\n ihdr[9] = 6;\n const idat = deflateSync(buildRaw(image));\n return Buffer.concat([\n PNG_SIGNATURE, buildChunk('IHDR', ihdr), buildChunk('IDAT', idat), buildChunk('IEND', Buffer.alloc(0)),\n ]);\n}\n\n/**\n * Insert an ancillary chunk directly after IHDR. PNG decoders ignore unknown ancillary\n * chunk types, so a tEXt-style chunk changes the file hash without touching pixels.\n * Returns undefined when the input is not a parseable PNG.\n */\nexport function insertPngChunk(buffer: Buffer, type: string, data: Buffer): Buffer | undefined {\n const chunks = parseChunks(buffer);\n if (!chunks) return undefined;\n const ihdrIndex = chunks.findIndex(value => value.type === 'IHDR');\n if (ihdrIndex === -1) return undefined;\n const parts: Buffer[] = [PNG_SIGNATURE];\n chunks.forEach((value, index) => {\n parts.push(buildChunk(value.type, value.data));\n if (index === ihdrIndex) parts.push(buildChunk(type, data));\n });\n return Buffer.concat(parts);\n}\n"],"mappings":"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"}
@@ -0,0 +1,159 @@
1
+ import { deriveToken, deriveUuid, isReferenceCarrier, replaceWholeWords, writeIfChanged } from "./common.js";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ // src/project.ts
5
+ /**
6
+ * Deterministic project-level transforms: display-name scrubbing in framework config
7
+ * files, seeded directory renames with reference rewrites, and UUID remapping.
8
+ */
9
+ /** Framework configuration files where the app display name legitimately lives. */
10
+ const CONFIG_FILES = {
11
+ flutter: ["pubspec.yaml"],
12
+ "android-java": [
13
+ "AndroidManifest.xml",
14
+ "strings.xml",
15
+ "build.gradle",
16
+ "build.gradle.kts",
17
+ "settings.gradle",
18
+ "settings.gradle.kts",
19
+ "gradle.properties"
20
+ ],
21
+ swift: ["Info.plist", "project.pbxproj"],
22
+ "react-native": ["app.json", "package.json"],
23
+ expo: [
24
+ "app.json",
25
+ "app.config.json",
26
+ "app.config.js",
27
+ "package.json"
28
+ ],
29
+ "uni-app": ["manifest.json", "pages.json"],
30
+ mygo: []
31
+ };
32
+ const XCCONFIG_SUFFIX = ".xcconfig";
33
+ function isConfigFile(relative, framework) {
34
+ const base = relative.slice(relative.lastIndexOf("/") + 1);
35
+ if (framework === "swift" && base.endsWith(XCCONFIG_SUFFIX)) return true;
36
+ return CONFIG_FILES[framework].includes(base);
37
+ }
38
+ /**
39
+ * Replace the app's display name (`from` -> `to`, whole-word) inside framework config
40
+ * files only. Code identifiers and resource payloads are left to code/content transforms.
41
+ */
42
+ function renameProjectConfig(files, framework, name) {
43
+ const changedPaths = [];
44
+ let changed = 0;
45
+ for (const entry of files) {
46
+ if (!isConfigFile(entry.relative, framework)) continue;
47
+ const before = readFileSync(entry.absolute, "utf8");
48
+ const result = replaceWholeWords(before, { [name.from]: name.to });
49
+ if (result.count === 0) continue;
50
+ if (writeIfChanged(entry, result.text)) {
51
+ changed += 1;
52
+ changedPaths.push(entry.relative);
53
+ }
54
+ }
55
+ return {
56
+ changed,
57
+ changedPaths
58
+ };
59
+ }
60
+ /**
61
+ * Rename the named directories to seed-derived tokens and rewrite path references in
62
+ * text/code files, keeping the project buildable. Empty source directories remain on
63
+ * disk (their contents moved); collisions leave the colliding file at its old path.
64
+ */
65
+ function obfuscateDirectories(root, files, directories, seed) {
66
+ const wanted = new Set(directories.filter((name) => name.length > 0));
67
+ if (!wanted.size) return {
68
+ changed: 0,
69
+ changedPaths: []
70
+ };
71
+ const mapping = /* @__PURE__ */ new Map();
72
+ for (const name of wanted) mapping.set(name, deriveToken(seed, "directory", name));
73
+ const pathRenames = /* @__PURE__ */ new Map();
74
+ const occupied = new Set(files.map((entry) => entry.relative));
75
+ const applied = /* @__PURE__ */ new Map();
76
+ for (const entry of files) {
77
+ const segments = entry.relative.split("/");
78
+ let renamed = false;
79
+ const next = segments.map((segment) => {
80
+ const mapped = mapping.get(segment);
81
+ if (mapped !== void 0) {
82
+ renamed = true;
83
+ return mapped;
84
+ }
85
+ return segment;
86
+ });
87
+ if (!renamed) continue;
88
+ const target = next.join("/");
89
+ if (occupied.has(target)) continue;
90
+ pathRenames.set(entry.relative, target);
91
+ occupied.add(target);
92
+ for (const [segment, mapped] of segments.map((segment, index) => [segment, next[index]])) if (mapped !== segment) applied.set(segment, mapped);
93
+ }
94
+ const changedPaths = [];
95
+ let changed = 0;
96
+ if (applied.size) {
97
+ const words = {};
98
+ for (const [from, to] of applied) words[from] = to;
99
+ for (const entry of files) {
100
+ if (!isReferenceCarrier(entry.relative)) continue;
101
+ const before = readFileSync(entry.absolute, "utf8");
102
+ const result = replaceWholeWords(before, words);
103
+ if (result.count === 0) continue;
104
+ if (writeIfChanged(entry, result.text)) {
105
+ changed += 1;
106
+ changedPaths.push(entry.relative);
107
+ }
108
+ }
109
+ }
110
+ for (const [from, to] of pathRenames) {
111
+ const source = join(root, from);
112
+ const target = join(root, to);
113
+ if (!existsSync(source)) continue;
114
+ mkdirSync(dirname(target), { recursive: true });
115
+ renameSync(source, target);
116
+ changed += 1;
117
+ changedPaths.push(to);
118
+ }
119
+ return {
120
+ changed,
121
+ changedPaths
122
+ };
123
+ }
124
+ const UUID_PATTERN = /(?<![0-9A-Fa-f])[0-9A-F]{24}(?![0-9A-Fa-f])/g;
125
+ /**
126
+ * Remap every 24-char uppercase-hex identifier (Xcode UUID shape) in text/code files.
127
+ * The same original value always maps to the same seed-derived value, preserving the
128
+ * internal reference graph of project.pbxproj and similar files.
129
+ */
130
+ function remapUuids(files, seed) {
131
+ const changedPaths = [];
132
+ let changed = 0;
133
+ for (const entry of files) {
134
+ if (!isReferenceCarrier(entry.relative)) continue;
135
+ const before = readFileSync(entry.absolute, "utf8");
136
+ if (!UUID_PATTERN.test(before)) continue;
137
+ UUID_PATTERN.lastIndex = 0;
138
+ const replacements = /* @__PURE__ */ new Map();
139
+ const next = before.replace(UUID_PATTERN, (original) => {
140
+ let mapped = replacements.get(original);
141
+ if (mapped === void 0) {
142
+ mapped = deriveUuid(seed, original);
143
+ replacements.set(original, mapped);
144
+ }
145
+ return mapped;
146
+ });
147
+ if (writeIfChanged(entry, next)) {
148
+ changed += 1;
149
+ changedPaths.push(entry.relative);
150
+ }
151
+ }
152
+ return {
153
+ changed,
154
+ changedPaths
155
+ };
156
+ }
157
+ export { obfuscateDirectories, remapUuids, renameProjectConfig };
158
+
159
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","names":[],"sources":["../src/project.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport type { Framework, TransformOutcome, WalkEntry } from './common.js';\nimport { deriveToken, deriveUuid, isReferenceCarrier, replaceWholeWords, writeIfChanged } from './common.js';\n\n/**\n * Deterministic project-level transforms: display-name scrubbing in framework config\n * files, seeded directory renames with reference rewrites, and UUID remapping.\n */\n\n/** Framework configuration files where the app display name legitimately lives. */\nconst CONFIG_FILES: Readonly<Record<Framework, readonly string[]>> = {\n flutter: ['pubspec.yaml'],\n 'android-java': ['AndroidManifest.xml', 'strings.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle.properties'],\n swift: ['Info.plist', 'project.pbxproj'],\n 'react-native': ['app.json', 'package.json'],\n expo: ['app.json', 'app.config.json', 'app.config.js', 'package.json'],\n 'uni-app': ['manifest.json', 'pages.json'],\n mygo: [],\n};\n\nconst XCCONFIG_SUFFIX = '.xcconfig';\n\nfunction isConfigFile(relative: string, framework: Framework): boolean {\n const base = relative.slice(relative.lastIndexOf('/') + 1);\n if (framework === 'swift' && base.endsWith(XCCONFIG_SUFFIX)) return true;\n return CONFIG_FILES[framework].includes(base);\n}\n\n/**\n * Replace the app's display name (`from` -> `to`, whole-word) inside framework config\n * files only. Code identifiers and resource payloads are left to code/content transforms.\n */\nexport function renameProjectConfig(files: WalkEntry[], framework: Framework, name: { from: string; to: string }): TransformOutcome {\n const changedPaths: string[] = [];\n let changed = 0;\n for (const entry of files) {\n if (!isConfigFile(entry.relative, framework)) continue;\n const before = readFileSync(entry.absolute, 'utf8');\n const result = replaceWholeWords(before, { [name.from]: name.to });\n if (result.count === 0) continue;\n if (writeIfChanged(entry, result.text)) {\n changed += 1;\n changedPaths.push(entry.relative);\n }\n }\n return { changed, changedPaths };\n}\n\n/**\n * Rename the named directories to seed-derived tokens and rewrite path references in\n * text/code files, keeping the project buildable. Empty source directories remain on\n * disk (their contents moved); collisions leave the colliding file at its old path.\n */\nexport function obfuscateDirectories(root: string, files: WalkEntry[], directories: string[], seed: string): TransformOutcome {\n const wanted = new Set(directories.filter(name => name.length > 0));\n if (!wanted.size) return { changed: 0, changedPaths: [] };\n const mapping = new Map<string, string>();\n for (const name of wanted) mapping.set(name, deriveToken(seed, 'directory', name));\n\n const pathRenames = new Map<string, string>();\n const occupied = new Set(files.map(entry => entry.relative));\n const applied = new Map<string, string>();\n for (const entry of files) {\n const segments = entry.relative.split('/');\n let renamed = false;\n const next = segments.map(segment => {\n const mapped = mapping.get(segment);\n if (mapped !== undefined) { renamed = true; return mapped; }\n return segment;\n });\n if (!renamed) continue;\n const target = next.join('/');\n if (occupied.has(target)) continue; // collision: keep the original path\n pathRenames.set(entry.relative, target);\n occupied.add(target);\n for (const [segment, mapped] of segments.map((segment, index) => [segment, next[index]!] as const)) {\n if (mapped !== segment) applied.set(segment, mapped);\n }\n }\n\n const changedPaths: string[] = [];\n let changed = 0;\n if (applied.size) {\n const words: Record<string, string> = {};\n for (const [from, to] of applied) words[from] = to;\n for (const entry of files) {\n if (!isReferenceCarrier(entry.relative)) continue;\n const before = readFileSync(entry.absolute, 'utf8');\n const result = replaceWholeWords(before, words);\n if (result.count === 0) continue;\n if (writeIfChanged(entry, result.text)) {\n changed += 1;\n changedPaths.push(entry.relative);\n }\n }\n }\n for (const [from, to] of pathRenames) {\n const source = join(root, from);\n const target = join(root, to);\n if (!existsSync(source)) continue;\n mkdirSync(dirname(target), { recursive: true });\n renameSync(source, target);\n changed += 1;\n changedPaths.push(to);\n }\n return { changed, changedPaths };\n}\n\nconst UUID_PATTERN = /(?<![0-9A-Fa-f])[0-9A-F]{24}(?![0-9A-Fa-f])/g;\n\n/**\n * Remap every 24-char uppercase-hex identifier (Xcode UUID shape) in text/code files.\n * The same original value always maps to the same seed-derived value, preserving the\n * internal reference graph of project.pbxproj and similar files.\n */\nexport function remapUuids(files: WalkEntry[], seed: string): TransformOutcome {\n const changedPaths: string[] = [];\n let changed = 0;\n for (const entry of files) {\n if (!isReferenceCarrier(entry.relative)) continue;\n const before = readFileSync(entry.absolute, 'utf8');\n if (!UUID_PATTERN.test(before)) continue;\n UUID_PATTERN.lastIndex = 0;\n const replacements = new Map<string, string>();\n const next = before.replace(UUID_PATTERN, original => {\n let mapped = replacements.get(original);\n if (mapped === undefined) {\n mapped = deriveUuid(seed, original);\n replacements.set(original, mapped);\n }\n return mapped;\n });\n if (writeIfChanged(entry, next)) {\n changed += 1;\n changedPaths.push(entry.relative);\n }\n }\n return { changed, changedPaths };\n}\n"],"mappings":"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"}