@driftengine/package 3.61.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 (137) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +408 -0
  4. package/bin/drift-package.mjs +82 -0
  5. package/dist/android.d.ts +18 -0
  6. package/dist/android.js +207 -0
  7. package/dist/bootstrap.d.ts +17 -0
  8. package/dist/bootstrap.js +326 -0
  9. package/dist/build.d.ts +11 -0
  10. package/dist/build.js +248 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/flags.d.ts +57 -0
  14. package/dist/flags.js +122 -0
  15. package/dist/identity.d.ts +51 -0
  16. package/dist/identity.js +75 -0
  17. package/dist/index.d.ts +19 -0
  18. package/dist/index.js +24 -0
  19. package/dist/init.d.ts +24 -0
  20. package/dist/init.js +45 -0
  21. package/dist/ios.d.ts +27 -0
  22. package/dist/ios.js +152 -0
  23. package/dist/macCrossSign.d.ts +34 -0
  24. package/dist/macCrossSign.js +298 -0
  25. package/dist/main/consoleForward.d.ts +43 -0
  26. package/dist/main/consoleForward.js +63 -0
  27. package/dist/main/csp.d.ts +4 -0
  28. package/dist/main/csp.js +55 -0
  29. package/dist/main/entry.d.ts +1 -0
  30. package/dist/main/entry.js +56 -0
  31. package/dist/main/ipc.d.ts +28 -0
  32. package/dist/main/ipc.js +284 -0
  33. package/dist/main/links.d.ts +1 -0
  34. package/dist/main/links.js +29 -0
  35. package/dist/main/menu.d.ts +17 -0
  36. package/dist/main/menu.js +49 -0
  37. package/dist/main/serve.d.ts +21 -0
  38. package/dist/main/serve.js +69 -0
  39. package/dist/main/shortcuts.d.ts +28 -0
  40. package/dist/main/shortcuts.js +26 -0
  41. package/dist/main/splash.d.ts +14 -0
  42. package/dist/main/splash.js +41 -0
  43. package/dist/main/splashTiming.d.ts +36 -0
  44. package/dist/main/splashTiming.js +24 -0
  45. package/dist/main/window.d.ts +19 -0
  46. package/dist/main/window.js +297 -0
  47. package/dist/manifest.d.ts +61 -0
  48. package/dist/manifest.js +91 -0
  49. package/dist/mobile/assetCheck.d.ts +1 -0
  50. package/dist/mobile/assetCheck.js +28 -0
  51. package/dist/mobile/bridgeShim.d.ts +83 -0
  52. package/dist/mobile/bridgeShim.js +208 -0
  53. package/dist/mobile/iosShim.d.ts +42 -0
  54. package/dist/mobile/iosShim.js +104 -0
  55. package/dist/mobile/report.d.ts +27 -0
  56. package/dist/mobile/report.js +122 -0
  57. package/dist/mobile/versionCode.d.ts +12 -0
  58. package/dist/mobile/versionCode.js +17 -0
  59. package/dist/outDir.d.ts +30 -0
  60. package/dist/outDir.js +32 -0
  61. package/dist/preload/bridge.d.ts +77 -0
  62. package/dist/preload/bridge.js +2 -0
  63. package/dist/preload/entry.d.ts +1 -0
  64. package/dist/preload/entry.js +77 -0
  65. package/dist/renderer/fileStore.d.ts +23 -0
  66. package/dist/renderer/fileStore.js +33 -0
  67. package/dist/renderer/host.d.ts +45 -0
  68. package/dist/renderer/host.js +30 -0
  69. package/dist/renderer/nativeHost.d.ts +83 -0
  70. package/dist/renderer/nativeHost.js +125 -0
  71. package/dist/resources.d.ts +71 -0
  72. package/dist/resources.js +83 -0
  73. package/dist/runtime.d.ts +38 -0
  74. package/dist/runtime.js +80 -0
  75. package/dist/runtimeVersion.d.ts +10 -0
  76. package/dist/runtimeVersion.js +18 -0
  77. package/dist/signing.d.ts +49 -0
  78. package/dist/signing.js +135 -0
  79. package/dist/stage.d.ts +22 -0
  80. package/dist/stage.js +138 -0
  81. package/dist/steam/cloudStore.d.ts +30 -0
  82. package/dist/steam/cloudStore.js +54 -0
  83. package/dist/steam/depot.d.ts +29 -0
  84. package/dist/steam/depot.js +39 -0
  85. package/dist/steam/main.d.ts +22 -0
  86. package/dist/steam/main.js +41 -0
  87. package/dist/templates/buildWindows.d.ts +16 -0
  88. package/dist/templates/buildWindows.js +161 -0
  89. package/dist/templates/packageWrapper.d.ts +9 -0
  90. package/dist/templates/packageWrapper.js +80 -0
  91. package/dist/toolchain.d.ts +101 -0
  92. package/dist/toolchain.js +186 -0
  93. package/package.json +73 -0
  94. package/src/android.ts +256 -0
  95. package/src/bootstrap.ts +389 -0
  96. package/src/build.ts +285 -0
  97. package/src/cli.ts +367 -0
  98. package/src/flags.ts +171 -0
  99. package/src/identity.ts +87 -0
  100. package/src/index.ts +31 -0
  101. package/src/init.ts +61 -0
  102. package/src/ios.ts +185 -0
  103. package/src/macCrossSign.ts +338 -0
  104. package/src/main/consoleForward.ts +93 -0
  105. package/src/main/csp.ts +57 -0
  106. package/src/main/entry.ts +63 -0
  107. package/src/main/ipc.ts +311 -0
  108. package/src/main/links.ts +29 -0
  109. package/src/main/menu.ts +53 -0
  110. package/src/main/serve.ts +70 -0
  111. package/src/main/shortcuts.ts +55 -0
  112. package/src/main/splash.ts +42 -0
  113. package/src/main/splashTiming.ts +51 -0
  114. package/src/main/window.ts +316 -0
  115. package/src/manifest.ts +157 -0
  116. package/src/mobile/assetCheck.ts +25 -0
  117. package/src/mobile/bridgeShim.ts +292 -0
  118. package/src/mobile/iosShim.ts +148 -0
  119. package/src/mobile/report.ts +121 -0
  120. package/src/mobile/versionCode.ts +17 -0
  121. package/src/outDir.ts +37 -0
  122. package/src/preload/bridge.ts +73 -0
  123. package/src/preload/entry.ts +91 -0
  124. package/src/renderer/fileStore.ts +38 -0
  125. package/src/renderer/host.ts +96 -0
  126. package/src/renderer/nativeHost.ts +146 -0
  127. package/src/resources.ts +150 -0
  128. package/src/runtime.ts +87 -0
  129. package/src/runtimeVersion.ts +16 -0
  130. package/src/signing.ts +206 -0
  131. package/src/stage.ts +180 -0
  132. package/src/steam/cloudStore.ts +73 -0
  133. package/src/steam/depot.ts +69 -0
  134. package/src/steam/main.ts +69 -0
  135. package/src/templates/buildWindows.ts +163 -0
  136. package/src/templates/packageWrapper.ts +82 -0
  137. package/src/toolchain.ts +210 -0
@@ -0,0 +1,298 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { open, readdir, rm } from 'node:fs/promises';
3
+ import { basename, dirname, join } from 'node:path';
4
+ /**
5
+ * The macOS code signature, applied from a machine that is not a Mac.
6
+ *
7
+ * **Why this exists at all.** electron-builder's own refusal to build a Mac target fires on
8
+ * Windows and nowhere else, so a `.app` packs perfectly well on Linux — and then
9
+ * `isSignAllowed()` returns false off darwin, one warning is logged, and the artifact is finished
10
+ * and unsigned. Apple Silicon's loader refuses an unsigned binary outright, which is not
11
+ * Gatekeeper and which no user gesture works around, so that artifact is one that looks complete
12
+ * and cannot start. `rcodesign` is the one implementation of Apple's signature format that runs
13
+ * anywhere, and this module is the step that puts a signature back on.
14
+ *
15
+ * **Everything here is checked rather than assumed, because nothing on this side can launch the
16
+ * result.** A Mac is the only thing that can prove the app runs; what a Linux host *can* prove is
17
+ * that every binary in the bundle carries a signature, that every nested bundle was sealed, and
18
+ * that the slice inside is the architecture that was asked for. Those are the three ways this step
19
+ * fails silently, and each one below is a refusal rather than a warning.
20
+ */
21
+ /**
22
+ * The line rcodesign prints when a nested bundle's main executable cannot be resolved.
23
+ *
24
+ * **Measured rather than guessed, and it is not cosmetic.** A stock Electron distribution ships
25
+ * its helper apps with no `CFBundleExecutable` key; Apple's `codesign` infers the executable from
26
+ * the bundle's own name and rcodesign does not. When it cannot resolve one, it signs the Mach-O
27
+ * inside and then leaves that bundle out of the outer app's `CodeResources` — so the app's
28
+ * signature does not cover its own helpers, and nothing about the build says so.
29
+ *
30
+ * electron-builder rewrites every helper plist with a `CFBundleExecutable` before this runs, which
31
+ * is why a real build does not hit this. That is a fact about a dependency's current behaviour,
32
+ * not a guarantee, so it is checked here instead of trusted.
33
+ */
34
+ const UNRESOLVED_NESTED = 'could not find main executable of presumed nested bundle';
35
+ /**
36
+ * Ad-hoc sign a `.app` in place, then refuse it if the result is not what was asked for.
37
+ *
38
+ * Ad-hoc is the only mode offered: a Developer ID would need a certificate this host cannot use,
39
+ * and `planSigning` refuses that combination before a build gets this far.
40
+ */
41
+ export async function crossSignMacApp(options) {
42
+ /* No identity argument at all is rcodesign's spelling of an ad-hoc signature, and a bundle path
43
+ is signed recursively: nested bundles and loose Mach-O binaries included. */
44
+ const output = await run(options.rcodesign, ['sign', options.app]);
45
+ if (output.includes(UNRESOLVED_NESTED)) {
46
+ const bundles = output
47
+ .split('\n')
48
+ .filter((line) => line.includes(UNRESOLVED_NESTED))
49
+ .map((line) => line.slice(line.indexOf(UNRESOLVED_NESTED) + UNRESOLVED_NESTED.length + 2))
50
+ .join(', ');
51
+ throw new Error(`rcodesign could not resolve the main executable of ${bundles}, so the app's signature ` +
52
+ "does not cover it. That is a bundle whose Info.plist has no CFBundleExecutable: Apple's " +
53
+ 'codesign infers one from the bundle name and rcodesign does not. The artifact would ' +
54
+ 'look signed and fail validation on a Mac.');
55
+ }
56
+ await assertEverySealed(options.app);
57
+ await assertArch(options.app, options.arch);
58
+ }
59
+ /**
60
+ * The `.zip` a cross-built Mac artifact ships as, archived here rather than by electron-builder.
61
+ *
62
+ * **electron-builder cannot produce this one correctly off a Mac, and says so in its own source**:
63
+ * its archiver picks 7-Zip everywhere except macOS, guarded by a comment reading *"7zip
64
+ * dereferences symlinks, corrupting .framework structure and breaking codesign"*. The guard is
65
+ * written as `process.platform === "darwin"`, so on Linux it takes the 7-Zip path regardless.
66
+ *
67
+ * **A `.framework` is symlinks by construction.** `Electron Framework` points into
68
+ * `Versions/Current`, which points at `Versions/A`. Dereferenced, the archive carries the
69
+ * framework's 192 MB payload twice and no links at all, and macOS rejects that layout as
70
+ * unrecognised — the signature that was correct on disk no longer validates against it. Measured
71
+ * on this repository before this function existed: a 404 MB archive with zero symlink entries,
72
+ * against 353 MB on disk.
73
+ *
74
+ * `zip -y` stores a symlink as a symlink, `-r` recurses, and Unix permission bits are kept, which
75
+ * every executable inside the bundle depends on.
76
+ */
77
+ export async function zipMacApp(app, into) {
78
+ /* `zip` updates an existing archive in place rather than replacing it, so a rebuild would
79
+ otherwise leave files from the previous one in the artifact. */
80
+ await rm(into, { force: true });
81
+ try {
82
+ await run('zip', ['-y', '-r', '-q', into, basename(app)], { cwd: dirname(app) });
83
+ }
84
+ catch (cause) {
85
+ throw new Error(`could not archive the app with \`zip\`: ${String(cause)}. This build needs it because ` +
86
+ "electron-builder's own zip dereferences symlinks off a Mac, which corrupts every " +
87
+ 'framework in the bundle. Install it (`apt install zip`) and build again.');
88
+ }
89
+ const symlinks = await countSymlinkEntries(into);
90
+ if (symlinks === 0) {
91
+ throw new Error(`${into} contains no symlinks, and a macOS app bundle is built out of them — every ` +
92
+ 'framework links its top level into `Versions/Current`. An archive without them holds ' +
93
+ 'each framework twice and is rejected by macOS as an unrecognised bundle format. That ' +
94
+ 'is what `zip` without `-y` produces.');
95
+ }
96
+ console.log(`[drift-package] archived ${into} with ${String(symlinks)} symlinks preserved`);
97
+ }
98
+ /* The three fields of the zip format this needs, from APPNOTE.TXT. */
99
+ const EOCD_SIGNATURE = 0x06054b50;
100
+ const CENTRAL_SIGNATURE = 0x02014b50;
101
+ /** `S_IFLNK`, in the Unix mode that lives in the high half of the external attributes field. */
102
+ const S_IFMT = 0xf000;
103
+ const S_IFLNK = 0xa000;
104
+ /**
105
+ * How many entries in an archive are symlinks, read from its central directory.
106
+ *
107
+ * **The archive is inspected rather than the tool trusted**, because the failure this guards
108
+ * against is silent by nature: an archiver that dereferences links produces a larger file that
109
+ * unpacks into something that looks complete. Reading the central directory is exact, needs no
110
+ * `zipinfo` on the host, and answers the one question that matters.
111
+ */
112
+ async function countSymlinkEntries(zip) {
113
+ const handle = await open(zip, 'r');
114
+ try {
115
+ const { size } = await handle.stat();
116
+ /* The end-of-central-directory record is last, after a comment of up to 64 KiB. */
117
+ const tailLength = Math.min(size, 0x10000 + 22);
118
+ const tail = Buffer.alloc(tailLength);
119
+ await handle.read(tail, 0, tailLength, size - tailLength);
120
+ let eocd = -1;
121
+ for (let at = tail.length - 22; at >= 0; at -= 1) {
122
+ if (tail.readUInt32LE(at) === EOCD_SIGNATURE) {
123
+ eocd = at;
124
+ break;
125
+ }
126
+ }
127
+ if (eocd < 0)
128
+ throw new Error(`${zip} has no end-of-central-directory record`);
129
+ const entries = tail.readUInt16LE(eocd + 10);
130
+ const directoryLength = tail.readUInt32LE(eocd + 12);
131
+ const directoryStart = tail.readUInt32LE(eocd + 16);
132
+ const directory = Buffer.alloc(directoryLength);
133
+ await handle.read(directory, 0, directoryLength, directoryStart);
134
+ let at = 0;
135
+ let symlinks = 0;
136
+ for (let index = 0; index < entries && at + 46 <= directory.length; index += 1) {
137
+ if (directory.readUInt32LE(at) !== CENTRAL_SIGNATURE)
138
+ break;
139
+ /* The high half of the external attributes is the Unix mode, for an archive written on
140
+ Unix — which is the only kind this function is ever pointed at. */
141
+ const mode = directory.readUInt32LE(at + 38) >>> 16;
142
+ if ((mode & S_IFMT) === S_IFLNK)
143
+ symlinks += 1;
144
+ at +=
145
+ 46 +
146
+ directory.readUInt16LE(at + 28) +
147
+ directory.readUInt16LE(at + 30) +
148
+ directory.readUInt16LE(at + 32);
149
+ }
150
+ return symlinks;
151
+ }
152
+ finally {
153
+ await handle.close();
154
+ }
155
+ }
156
+ /**
157
+ * Every bundle in the artifact carries its own signature.
158
+ *
159
+ * A signed bundle has a `_CodeSignature/CodeResources` beside its contents — the outer app under
160
+ * `Contents/`, a framework under `Versions/A/`. Checking the directory rather than parsing the
161
+ * seal is deliberate: this catches the failure that actually happens, which is a bundle rcodesign
162
+ * walked past, and it does not pretend to be the validation only a Mac can do.
163
+ */
164
+ async function assertEverySealed(app) {
165
+ const missing = [];
166
+ if (!(await exists(join(app, 'Contents', '_CodeSignature', 'CodeResources')))) {
167
+ missing.push('the app itself');
168
+ }
169
+ const frameworks = join(app, 'Contents', 'Frameworks');
170
+ for (const entry of await list(frameworks)) {
171
+ const nested = join(frameworks, entry);
172
+ /* A `.app` seals under `Contents/`; a `.framework` under the version it points `Current` at,
173
+ which is `A` for everything Electron ships. */
174
+ const seal = entry.endsWith('.framework')
175
+ ? join(nested, 'Versions', 'A', '_CodeSignature', 'CodeResources')
176
+ : join(nested, 'Contents', '_CodeSignature', 'CodeResources');
177
+ if (entry.endsWith('.app') || entry.endsWith('.framework')) {
178
+ if (!(await exists(seal)))
179
+ missing.push(entry);
180
+ }
181
+ }
182
+ if (missing.length > 0) {
183
+ throw new Error(`these parts of the app were not signed: ${missing.join(', ')}. An unsigned binary inside ` +
184
+ 'a signed app is refused by the loader at the moment it is needed, which for a helper is ' +
185
+ 'the first window rather than the launch.');
186
+ }
187
+ }
188
+ /**
189
+ * The artifact carries the slice it was asked for.
190
+ *
191
+ * **The failure this prevents is a build that produced the wrong architecture and said nothing.**
192
+ * electron-builder picks the Electron distribution from the target's arch, so a `mac-x64` asked
193
+ * for should never contain arm64 — but a cache with the wrong download in it, or a default that
194
+ * inherited the host's arch, both end as an app that refuses to start on the machine it was made
195
+ * for, with no error that names the cause.
196
+ */
197
+ async function assertArch(app, expected) {
198
+ const executable = await mainExecutable(app);
199
+ const found = await machoArch(executable);
200
+ if (found === null) {
201
+ throw new Error(`${executable} is not a Mach-O binary; the app did not pack correctly`);
202
+ }
203
+ if (found !== expected) {
204
+ throw new Error(`this build asked for ${expected} and the binary inside is ${found}. That artifact does ` +
205
+ 'not run on the machine it was built for, and nothing about it says so until it is opened.');
206
+ }
207
+ }
208
+ /** The one file under `Contents/MacOS`, which is the app's entry point whatever it is named. */
209
+ async function mainExecutable(app) {
210
+ const dir = join(app, 'Contents', 'MacOS');
211
+ const [entry] = await list(dir);
212
+ if (entry === undefined)
213
+ throw new Error(`${dir} is empty; the app did not pack correctly`);
214
+ return join(dir, entry);
215
+ }
216
+ /* Mach-O's own constants, from `mach-o/loader.h` and `mach/machine.h`. */
217
+ const MACHO_64_LE = 0xfeedfacf;
218
+ const FAT_BE = 0xcafebabe;
219
+ const CPU_X86_64 = 0x01000007;
220
+ const CPU_ARM64 = 0x0100000c;
221
+ /**
222
+ * Which architecture a Mach-O binary holds, read from its header.
223
+ *
224
+ * Sixteen bytes of file rather than a tool: `file` and `lipo` both answer this and neither is on
225
+ * a Linux host by default, and the header is four fields that have not moved in twenty years.
226
+ * A universal binary answers with what it contains only when that is unambiguous — one slice.
227
+ */
228
+ async function machoArch(path) {
229
+ const handle = await open(path, 'r');
230
+ try {
231
+ const header = Buffer.alloc(8);
232
+ await handle.read(header, 0, 8, 0);
233
+ if (header.readUInt32LE(0) === MACHO_64_LE)
234
+ return cpuName(header.readUInt32LE(4));
235
+ if (header.readUInt32BE(0) === FAT_BE) {
236
+ /* A fat header is a count and then 20-byte entries whose first field is the cpu type. */
237
+ const count = header.readUInt32BE(4);
238
+ const names = new Set();
239
+ for (let index = 0; index < count; index += 1) {
240
+ const entry = Buffer.alloc(4);
241
+ await handle.read(entry, 0, 4, 8 + index * 20);
242
+ const name = cpuName(entry.readUInt32BE(0));
243
+ if (name !== null)
244
+ names.add(name);
245
+ }
246
+ return names.size === 1 ? [...names][0] : null;
247
+ }
248
+ return null;
249
+ }
250
+ finally {
251
+ await handle.close();
252
+ }
253
+ }
254
+ function cpuName(cpu) {
255
+ if (cpu === CPU_ARM64)
256
+ return 'arm64';
257
+ if (cpu === CPU_X86_64)
258
+ return 'x64';
259
+ return null;
260
+ }
261
+ async function list(dir) {
262
+ try {
263
+ return await readdir(dir);
264
+ }
265
+ catch {
266
+ return [];
267
+ }
268
+ }
269
+ async function exists(path) {
270
+ try {
271
+ await (await import('node:fs/promises')).access(path);
272
+ return true;
273
+ }
274
+ catch {
275
+ return false;
276
+ }
277
+ }
278
+ /**
279
+ * Run a command and return everything it said, on both streams.
280
+ *
281
+ * rcodesign narrates to stderr and the warning this module refuses on arrives there, so the two
282
+ * are merged rather than one being watched — a check that read only stdout would pass every time.
283
+ */
284
+ async function run(command, args, options = {}) {
285
+ return new Promise((resolve, reject) => {
286
+ const child = spawn(command, [...args], {
287
+ cwd: options.cwd,
288
+ stdio: ['ignore', 'pipe', 'pipe'],
289
+ });
290
+ let output = '';
291
+ child.stdout.on('data', (chunk) => (output += chunk.toString()));
292
+ child.stderr.on('data', (chunk) => (output += chunk.toString()));
293
+ child.on('error', reject);
294
+ child.on('close', (code) => code === 0
295
+ ? resolve(output)
296
+ : reject(new Error(`${command} exited with ${String(code)}:\n${output}`)));
297
+ });
298
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Reading a renderer's console line off an event whose shape has changed, and deciding whether it
3
+ * is worth forwarding.
4
+ *
5
+ * **Pure, and tested, because the failure of getting it wrong is silence.** A packaged game has no
6
+ * developer tools and no console a player can open, so this forwarding is the only way anything the
7
+ * renderer says reaches a bug report. Reading the wrong argument does not throw and does not warn:
8
+ * it compares `undefined` against a string for every message, forwards none of them, and is
9
+ * indistinguishable from a game that never logged.
10
+ *
11
+ * The two shapes, both of which are `console-message` on `WebContents`:
12
+ *
13
+ * - **Electron 35 and newer.** One object first, carrying `message` and a string `level` of
14
+ * `debug`, `info`, `warning` or `error`. The old positional arguments follow it, deprecated.
15
+ * - **Before that, fully positional**: `(event, level: 0..3, message, line, sourceId)`, where the
16
+ * event itself carries neither. **Its own typings say otherwise** — `electron.d.ts` at 33.4.11
17
+ * declares `(event, messageDetails)` with the details as an object — and reading them is what
18
+ * produced a first version of this module that handled two object shapes and forwarded nothing
19
+ * on the runtime it was written to rescue. The positional form below is what a probe against
20
+ * that binary actually received. The object-second shape is still accepted, because it is what
21
+ * the typings promise and costs one branch to keep.
22
+ */
23
+ export type ConsoleLevel = 'debug' | 'verbose' | 'info' | 'warning' | 'error';
24
+ export interface ConsoleLine {
25
+ readonly level: ConsoleLevel;
26
+ readonly message: string;
27
+ }
28
+ /**
29
+ * The line a listener was handed, whichever shape it came in, or `null` when neither carried one.
30
+ *
31
+ * The first argument is preferred when it carries a level, which is the newer shape; the second is
32
+ * read otherwise. The third is the positional message the newer shape also passes, used only when
33
+ * an object carried a level without a message.
34
+ */
35
+ export declare function consoleLine(first: unknown, second: unknown, third: unknown): ConsoleLine | null;
36
+ /**
37
+ * Whether a line at this level is forwarded.
38
+ *
39
+ * Errors in both kinds of build: an error nobody can quote is an error nobody can fix. Warnings in
40
+ * development only — they are where a framework's advice and a game's own diagnostics live, and a
41
+ * shipped game printing them to a terminal nobody is watching buries the line that matters.
42
+ */
43
+ export declare function forwardsConsole(level: ConsoleLevel, development: boolean): boolean;
@@ -0,0 +1,63 @@
1
+ /** 0 to 3, as the older event numbers them. */
2
+ const BY_NUMBER = ['verbose', 'info', 'warning', 'error'];
3
+ const NAMED = ['debug', 'verbose', 'info', 'warning', 'error'];
4
+ function levelOf(value) {
5
+ if (typeof value === 'string' && NAMED.includes(value))
6
+ return value;
7
+ if (typeof value === 'number' && Number.isInteger(value))
8
+ return BY_NUMBER[value] ?? null;
9
+ return null;
10
+ }
11
+ function fieldsOf(value) {
12
+ if (value === null || typeof value !== 'object')
13
+ return null;
14
+ const record = value;
15
+ return { level: record.level, message: record.message };
16
+ }
17
+ /**
18
+ * The line a listener was handed, whichever shape it came in, or `null` when neither carried one.
19
+ *
20
+ * The first argument is preferred when it carries a level, which is the newer shape; the second is
21
+ * read otherwise. The third is the positional message the newer shape also passes, used only when
22
+ * an object carried a level without a message.
23
+ */
24
+ export function consoleLine(first, second, third) {
25
+ /*
26
+ * The positional form first, because it is unambiguous: a number where an object would be is the
27
+ * older event, and no newer one puts a level there without also putting one on its first
28
+ * argument — which the loop below would have taken anyway.
29
+ */
30
+ if (typeof second === 'number' &&
31
+ typeof third === 'string' &&
32
+ fieldsOf(first)?.level === undefined) {
33
+ const level = levelOf(second);
34
+ if (level !== null)
35
+ return { level, message: third };
36
+ }
37
+ for (const candidate of [fieldsOf(first), fieldsOf(second)]) {
38
+ if (candidate === null)
39
+ continue;
40
+ const level = levelOf(candidate.level);
41
+ if (level === null)
42
+ continue;
43
+ const message = typeof candidate.message === 'string'
44
+ ? candidate.message
45
+ : typeof third === 'string'
46
+ ? third
47
+ : '';
48
+ return { level, message };
49
+ }
50
+ return null;
51
+ }
52
+ /**
53
+ * Whether a line at this level is forwarded.
54
+ *
55
+ * Errors in both kinds of build: an error nobody can quote is an error nobody can fix. Warnings in
56
+ * development only — they are where a framework's advice and a game's own diagnostics live, and a
57
+ * shipped game printing them to a terminal nobody is watching buries the line that matters.
58
+ */
59
+ export function forwardsConsole(level, development) {
60
+ if (level === 'error')
61
+ return true;
62
+ return development && level === 'warning';
63
+ }
@@ -0,0 +1,4 @@
1
+ /** The header value, as one string. */
2
+ export declare const CSP: string;
3
+ /** The source list for one directive, or an empty array when the policy does not carry it. */
4
+ export declare function sourcesFor(directive: string): readonly string[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * What the game is allowed to load, and it is deliberately not strict about very much.
3
+ *
4
+ * **The one thing it forbids is a script from somewhere else.** That is the attack this is for: a
5
+ * renderer that fetches a script over `http:` is a renderer whose network can hand it code, and it
6
+ * runs beside a bridge to the filesystem. Everything a game legitimately does — inline module
7
+ * preloads, a WebAssembly module, a blob worker, a texture from a data URL, a socket to its own
8
+ * server — is allowed, because a policy that breaks an ordinary bundle is a policy somebody
9
+ * switches off.
10
+ *
11
+ * **What would make this wrong** is a game that loads code from a CDN, which this refuses. That is
12
+ * the intended refusal and the answer is to bundle it.
13
+ *
14
+ * **Its own module, and tested, because a directive missing one scheme is invisible.** This lived
15
+ * as a constant in `window.ts`, which is Electron lifecycle and is not unit-tested — and it shipped
16
+ * allowing `wss:` without `ws:`, so a packaged game could not reach a relay on its own network. The
17
+ * shape of that failure is what makes it worth a test rather than a careful read: Chromium blocks
18
+ * the connection before a packet leaves, so the relay logs nothing and the game reports only that
19
+ * it did not connect. The two together point at the network. Seeing the actual line took a remote
20
+ * debugging session against the packaged binary, and a packaged build has no developer tools.
21
+ */
22
+ const DIRECTIVES = [
23
+ "default-src 'self' drift: blob: data:",
24
+ "script-src 'self' drift: blob: 'unsafe-inline' 'wasm-unsafe-eval'",
25
+ "style-src 'self' drift: 'unsafe-inline'",
26
+ "img-src 'self' drift: blob: data: https:",
27
+ "media-src 'self' drift: blob: data: https:",
28
+ "font-src 'self' drift: data: https:",
29
+ "worker-src 'self' drift: blob:",
30
+ /*
31
+ * **`ws:` beside `wss:`, because a relay on a local network cannot hold a certificate.**
32
+ *
33
+ * `ws://192.168.1.20:8787` is the ordinary case for a desktop game with its own server, and no
34
+ * certificate authority issues for a private address — so the secure scheme is not an
35
+ * alternative there, it is a refusal. A public relay needs `wss:` regardless and is unaffected.
36
+ *
37
+ * **What this widens, stated rather than waved past.** Reaching an arbitrary host was already
38
+ * permitted: `https:` and `wss:` are both open, so a compromised asset could already talk to a
39
+ * server of its choosing. What `ws:` adds is plaintext, which is the developer's own choice about
40
+ * their own traffic, and sockets to services on the machine itself, which `http:` being absent
41
+ * still denies to everything else. Neither is the attack `script-src` is here for.
42
+ *
43
+ * **A manifest field was the other option and is not taken.** `drift.package.json` says nothing
44
+ * about the policy today, and the first thing it said should not be a security surface a consumer
45
+ * has to get right — particularly when the value it would carry is the one this line now has.
46
+ */
47
+ "connect-src 'self' drift: blob: data: https: wss: ws:",
48
+ ];
49
+ /** The header value, as one string. */
50
+ export const CSP = DIRECTIVES.join('; ');
51
+ /** The source list for one directive, or an empty array when the policy does not carry it. */
52
+ export function sourcesFor(directive) {
53
+ const found = DIRECTIVES.find((entry) => entry.startsWith(`${directive} `));
54
+ return found === undefined ? [] : found.slice(directive.length + 1).split(' ');
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { app } from 'electron';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { parseManifest } from '../manifest.js';
5
+ import { startApp } from './window.js';
6
+ /**
7
+ * What the packaged application actually starts.
8
+ *
9
+ * The layout beside this file is fixed by the builder and is worth stating once, because three
10
+ * files depend on it agreeing:
11
+ *
12
+ * main.cjs this, bundled
13
+ * preload.cjs the bridge, bundled
14
+ * drift.package.json the manifest, copied verbatim from the consumer's project
15
+ * app/ the game's built web bundle, with index.html at its root
16
+ *
17
+ * **The target is derived rather than baked in.** A build produces one artifact per target and
18
+ * each runs on exactly one platform, so asking the process it is running in cannot be wrong —
19
+ * where a value written at build time can be, and would be discovered as the wrong Chromium flags
20
+ * on somebody else's machine.
21
+ */
22
+ function hostTarget() {
23
+ if (process.platform === 'win32')
24
+ return 'win-x64';
25
+ if (process.platform === 'darwin')
26
+ return process.arch === 'x64' ? 'mac-x64' : 'mac-arm64';
27
+ return 'linux-x64';
28
+ }
29
+ const here = __dirname;
30
+ const manifest = parseManifest(JSON.parse(readFileSync(join(here, 'drift.package.json'), 'utf8')));
31
+ /**
32
+ * Whether this is a development run, which decides what a person can open.
33
+ *
34
+ * **Absent means production, and that direction is the whole point.** A shipped artifact whose
35
+ * marker file was lost must not fall back to exposing developer tools; a development run whose
36
+ * marker is missing merely loses a convenience.
37
+ */
38
+ function isDevelopment() {
39
+ try {
40
+ const raw = JSON.parse(readFileSync(join(here, 'build.json'), 'utf8'));
41
+ return raw.development === true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ startApp(manifest, join(here, 'app'), hostTarget(), { development: isDevelopment() }).catch((cause) => {
48
+ /*
49
+ * A failure here is a failure to open a window at all, and there is nothing on screen to show
50
+ * it on. The console is what a player can be asked to copy, and a non-zero exit is what a Steam
51
+ * launch reports.
52
+ */
53
+ console.error('[drift-package] the application could not start:', cause);
54
+ process.exitCode = 1;
55
+ app.quit();
56
+ });
@@ -0,0 +1,28 @@
1
+ import { BrowserWindow } from 'electron';
2
+ import type { SteamRuntime } from '../steam/main.ts';
3
+ /**
4
+ * What the game starts with, read from wherever this build's saves live.
5
+ *
6
+ * Steam Cloud first where a store answered, because a player who moved machines expects their
7
+ * settings to have travelled — and falling back to the local file when the cloud has nothing is
8
+ * what makes the first launch after enabling Steam keep the saves it already had.
9
+ */
10
+ export declare function readStoreSnapshot(steam?: SteamRuntime | null): Record<string, string>;
11
+ /** What the window half needs back from here, so quitting is one decision in one place. */
12
+ export interface QuitProtocol {
13
+ /** True once the game has said it wants to be told before the window closes. */
14
+ wantsNotice(): boolean;
15
+ /** True when the close should proceed rather than be deferred. */
16
+ mayClose(): boolean;
17
+ /** Called from the window's `close` handler when it defers, to arm the grace timer. */
18
+ deferClose(window: BrowserWindow): void;
19
+ }
20
+ export declare function installIpc(snapshot: Record<string, string>, windowOf: () => BrowserWindow | null, steam?: SteamRuntime | null): QuitProtocol;
21
+ /**
22
+ * The refresh rate of the display this window is on, which the browser implementation of
23
+ * `DisplayControl` has to answer null for.
24
+ *
25
+ * Null rather than a plausible 60 where the platform withholds it, matching the engine's rule:
26
+ * something will divide by this.
27
+ */
28
+ export declare function refreshHzFor(window: BrowserWindow | null): number | null;