1681-engine 0.5.9 → 0.5.10

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 (140) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +3 -0
  3. package/dist/1681-engine.js +501 -438
  4. package/dist/ambient/life/Birds.js +73 -0
  5. package/dist/ambient/life/Butterflies.js +43 -0
  6. package/dist/ambient/life/ChimneySmoke.js +50 -0
  7. package/dist/ambient/life/Drift.js +75 -0
  8. package/dist/ambient/life/Fireflies.js +52 -0
  9. package/dist/ambient/life/lifeMotion.js +49 -0
  10. package/dist/assets/AssetLoader.js +115 -70
  11. package/dist/assets/Prepainter.js +61 -0
  12. package/dist/assets/workerPainter.js +39 -0
  13. package/dist/cjs/electron/electronLike.d.ts +55 -0
  14. package/dist/cjs/electron/electronLike.js +7 -0
  15. package/dist/cjs/electron/index.d.ts +14 -0
  16. package/dist/cjs/electron/index.js +20 -0
  17. package/dist/cjs/electron/lockdown.d.ts +33 -0
  18. package/dist/cjs/electron/lockdown.js +69 -0
  19. package/dist/cjs/electron/lockdownRules.d.ts +28 -0
  20. package/dist/cjs/electron/lockdownRules.js +61 -0
  21. package/dist/cjs/electron/sessionStore.d.ts +40 -0
  22. package/dist/cjs/electron/sessionStore.js +77 -0
  23. package/dist/cjs/electron/shellId.d.ts +22 -0
  24. package/dist/cjs/electron/shellId.js +31 -0
  25. package/dist/cjs/electron/webFiles.d.ts +13 -0
  26. package/dist/cjs/electron/webFiles.js +52 -0
  27. package/dist/cjs/electron/webPatcher.d.ts +80 -0
  28. package/dist/cjs/electron/webPatcher.js +216 -0
  29. package/dist/cjs/electron/webPointers.d.ts +19 -0
  30. package/dist/cjs/electron/webPointers.js +35 -0
  31. package/dist/cjs/package.json +1 -0
  32. package/dist/cjs/updates/index.d.ts +8 -0
  33. package/dist/cjs/updates/index.js +21 -0
  34. package/dist/cjs/updates/manifest.d.ts +73 -0
  35. package/dist/cjs/updates/manifest.js +67 -0
  36. package/dist/cjs/updates/pickUpdate.d.ts +42 -0
  37. package/dist/cjs/updates/pickUpdate.js +52 -0
  38. package/dist/cjs/updates/webPlan.d.ts +20 -0
  39. package/dist/cjs/updates/webPlan.js +34 -0
  40. package/dist/electron/lockdown.js +31 -0
  41. package/dist/electron/lockdownRules.js +42 -0
  42. package/dist/electron/sessionStore.js +48 -0
  43. package/dist/electron/shellId.js +18 -0
  44. package/dist/electron/webFiles.js +36 -0
  45. package/dist/electron/webPatcher.js +147 -0
  46. package/dist/electron/webPointers.js +25 -0
  47. package/dist/electron.js +19 -0
  48. package/dist/input/haptics.js +50 -0
  49. package/dist/net/ApiClient.js +96 -0
  50. package/dist/pixel/lifeArt.js +83 -0
  51. package/dist/pixel.js +63 -58
  52. package/dist/react/ConfirmButton.js +24 -0
  53. package/dist/react/baseCss.js +1 -0
  54. package/dist/react/labels.js +7 -6
  55. package/dist/react/useJsonSelector.js +13 -0
  56. package/dist/react.js +73 -68
  57. package/dist/tilemap/TilemapView.js +57 -32
  58. package/dist/tilemap/autotile.js +103 -0
  59. package/dist/tilemap/collision.js +29 -18
  60. package/dist/tilemap/ldtk.js +21 -0
  61. package/dist/tilemap/ldtkParse.js +191 -0
  62. package/dist/types/ambient/life/Birds.d.ts +50 -0
  63. package/dist/types/ambient/life/Birds.d.ts.map +1 -0
  64. package/dist/types/ambient/life/Butterflies.d.ts +32 -0
  65. package/dist/types/ambient/life/Butterflies.d.ts.map +1 -0
  66. package/dist/types/ambient/life/ChimneySmoke.d.ts +37 -0
  67. package/dist/types/ambient/life/ChimneySmoke.d.ts.map +1 -0
  68. package/dist/types/ambient/life/Drift.d.ts +58 -0
  69. package/dist/types/ambient/life/Drift.d.ts.map +1 -0
  70. package/dist/types/ambient/life/Fireflies.d.ts +43 -0
  71. package/dist/types/ambient/life/Fireflies.d.ts.map +1 -0
  72. package/dist/types/ambient/life/lifeMotion.d.ts +63 -0
  73. package/dist/types/ambient/life/lifeMotion.d.ts.map +1 -0
  74. package/dist/types/assets/AssetLoader.d.ts +20 -0
  75. package/dist/types/assets/AssetLoader.d.ts.map +1 -1
  76. package/dist/types/assets/Prepainter.d.ts +64 -0
  77. package/dist/types/assets/Prepainter.d.ts.map +1 -0
  78. package/dist/types/assets/workerPainter.d.ts +21 -0
  79. package/dist/types/assets/workerPainter.d.ts.map +1 -0
  80. package/dist/types/electron/electronLike.d.ts +56 -0
  81. package/dist/types/electron/electronLike.d.ts.map +1 -0
  82. package/dist/types/electron/index.d.ts +15 -0
  83. package/dist/types/electron/index.d.ts.map +1 -0
  84. package/dist/types/electron/lockdown.d.ts +34 -0
  85. package/dist/types/electron/lockdown.d.ts.map +1 -0
  86. package/dist/types/electron/lockdownRules.d.ts +29 -0
  87. package/dist/types/electron/lockdownRules.d.ts.map +1 -0
  88. package/dist/types/electron/sessionStore.d.ts +41 -0
  89. package/dist/types/electron/sessionStore.d.ts.map +1 -0
  90. package/dist/types/electron/shellId.d.ts +23 -0
  91. package/dist/types/electron/shellId.d.ts.map +1 -0
  92. package/dist/types/electron/webFiles.d.ts +14 -0
  93. package/dist/types/electron/webFiles.d.ts.map +1 -0
  94. package/dist/types/electron/webPatcher.d.ts +81 -0
  95. package/dist/types/electron/webPatcher.d.ts.map +1 -0
  96. package/dist/types/electron/webPointers.d.ts +20 -0
  97. package/dist/types/electron/webPointers.d.ts.map +1 -0
  98. package/dist/types/index.d.ts +15 -2
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/input/haptics.d.ts +42 -0
  101. package/dist/types/input/haptics.d.ts.map +1 -0
  102. package/dist/types/net/ApiClient.d.ts +74 -0
  103. package/dist/types/net/ApiClient.d.ts.map +1 -0
  104. package/dist/types/pixel/index.d.ts +1 -0
  105. package/dist/types/pixel/index.d.ts.map +1 -1
  106. package/dist/types/pixel/lifeArt.d.ts +63 -0
  107. package/dist/types/pixel/lifeArt.d.ts.map +1 -0
  108. package/dist/types/react/ConfirmButton.d.ts +26 -0
  109. package/dist/types/react/ConfirmButton.d.ts.map +1 -0
  110. package/dist/types/react/baseCss.d.ts +1 -1
  111. package/dist/types/react/baseCss.d.ts.map +1 -1
  112. package/dist/types/react/index.d.ts +2 -0
  113. package/dist/types/react/index.d.ts.map +1 -1
  114. package/dist/types/react/labels.d.ts +2 -0
  115. package/dist/types/react/labels.d.ts.map +1 -1
  116. package/dist/types/react/useJsonSelector.d.ts +13 -0
  117. package/dist/types/react/useJsonSelector.d.ts.map +1 -0
  118. package/dist/types/tilemap/TilemapView.d.ts +17 -0
  119. package/dist/types/tilemap/TilemapView.d.ts.map +1 -1
  120. package/dist/types/tilemap/autotile.d.ts +95 -0
  121. package/dist/types/tilemap/autotile.d.ts.map +1 -0
  122. package/dist/types/tilemap/collision.d.ts +8 -0
  123. package/dist/types/tilemap/collision.d.ts.map +1 -1
  124. package/dist/types/tilemap/ldtk.d.ts +97 -0
  125. package/dist/types/tilemap/ldtk.d.ts.map +1 -0
  126. package/dist/types/tilemap/ldtkParse.d.ts +26 -0
  127. package/dist/types/tilemap/ldtkParse.d.ts.map +1 -0
  128. package/dist/types/updates/index.d.ts +9 -0
  129. package/dist/types/updates/index.d.ts.map +1 -0
  130. package/dist/types/updates/manifest.d.ts +74 -0
  131. package/dist/types/updates/manifest.d.ts.map +1 -0
  132. package/dist/types/updates/pickUpdate.d.ts +43 -0
  133. package/dist/types/updates/pickUpdate.d.ts.map +1 -0
  134. package/dist/types/updates/webPlan.d.ts +21 -0
  135. package/dist/types/updates/webPlan.d.ts.map +1 -0
  136. package/dist/updates/manifest.js +35 -0
  137. package/dist/updates/pickUpdate.js +27 -0
  138. package/dist/updates/webPlan.js +14 -0
  139. package/dist/updates.js +15 -0
  140. package/package.json +31 -3
@@ -0,0 +1,61 @@
1
+ function d({ w: o, h: t, pixels: e }) {
2
+ if (typeof document > "u") return null;
3
+ const s = document.createElement("canvas");
4
+ s.width = o, s.height = t;
5
+ const n = s.getContext("2d");
6
+ return n ? (n.putImageData(new ImageData(new Uint8ClampedArray(e), o, t), 0, 0), s) : null;
7
+ }
8
+ class h {
9
+ constructor(t) {
10
+ this.options = t, this.key = t.key ?? ((e) => e);
11
+ }
12
+ options;
13
+ pending = /* @__PURE__ */ new Set();
14
+ /** Ids whose painting is held as a texture: the current one and its neighbours. */
15
+ held = /* @__PURE__ */ new Set();
16
+ key;
17
+ destroyed = !1;
18
+ /**
19
+ * Stands on `current`: its texture (painted by the caller on arrival, or ahead) and its neighbours' are kept, every
20
+ * other one this prepainter held is freed, and the neighbours not yet painted are started.
21
+ */
22
+ enter(t, e) {
23
+ const s = this.options.id, n = s(t), r = /* @__PURE__ */ new Set([n, ...e.map(s)]);
24
+ for (const i of this.held)
25
+ r.has(i) || (this.options.textures.remove(this.key(i)), this.held.delete(i));
26
+ this.held.add(n);
27
+ for (const i of e) this.prepare(i);
28
+ }
29
+ /** Whether `id` is being painted right now (so a caller may wait for it a moment rather than paint it again). */
30
+ painting(t) {
31
+ return this.pending.has(t);
32
+ }
33
+ /** Ids held as textures now. */
34
+ get holding() {
35
+ return this.held;
36
+ }
37
+ /** Starts painting `item` unless it is painted or under way. */
38
+ prepare(t) {
39
+ const e = this.options.id(t), s = this.options.textures.exists(this.key(e));
40
+ s && this.held.add(e);
41
+ const n = this.options.painter;
42
+ s || this.pending.has(e) || !n || this.destroyed || (this.pending.add(e), n.paint(t, e).then(
43
+ (r) => this.receive(e, r),
44
+ () => this.pending.delete(e)
45
+ ));
46
+ }
47
+ /** Stops painting; textures already made are left to the registry. */
48
+ destroy() {
49
+ this.destroyed = !0, this.options.painter?.dispose?.(), this.pending.clear();
50
+ }
51
+ receive(t, e) {
52
+ if (this.pending.delete(t), this.destroyed) return;
53
+ const s = this.key(t);
54
+ if (this.options.textures.exists(s)) return;
55
+ const n = (this.options.canvas ?? d)(e);
56
+ n && (this.options.textures.addCanvas(s, n), this.held.add(t), this.options.onPainted?.(t));
57
+ }
58
+ }
59
+ export {
60
+ h as Prepainter
61
+ };
@@ -0,0 +1,39 @@
1
+ function w(i) {
2
+ let n;
3
+ const o = /* @__PURE__ */ new Map(), c = (r) => {
4
+ n?.terminate(), n = null;
5
+ for (const t of o.values()) for (const e of t) e.reject(new Error(r));
6
+ o.clear();
7
+ }, f = () => {
8
+ if (n !== void 0) return n;
9
+ try {
10
+ const r = i.create();
11
+ r.onmessage = (t) => {
12
+ const e = t.data, s = o.get(e.id);
13
+ if (s) {
14
+ o.delete(e.id);
15
+ for (const a of s)
16
+ "error" in e ? a.reject(new Error(e.error)) : a.resolve(e);
17
+ }
18
+ }, r.onerror = () => c("the painter worker failed"), n = r;
19
+ } catch {
20
+ n = null;
21
+ }
22
+ return n;
23
+ };
24
+ return {
25
+ paint(r, t) {
26
+ const e = f();
27
+ return e ? new Promise((s, a) => {
28
+ const l = o.get(t);
29
+ l ? l.push({ resolve: s, reject: a }) : (o.set(t, [{ resolve: s, reject: a }]), e.postMessage(i.message ? i.message(r, t) : { id: t, item: r }));
30
+ }) : Promise.reject(new Error("no painter worker"));
31
+ },
32
+ dispose() {
33
+ c("disposed");
34
+ }
35
+ };
36
+ }
37
+ export {
38
+ w as workerPainter
39
+ };
@@ -0,0 +1,55 @@
1
+ /** `electron.safeStorage`. */
2
+ export interface SafeStorageLike {
3
+ isEncryptionAvailable(): boolean;
4
+ /** Linux only: `basic_text` means a hard-coded key, which is no encryption at all. */
5
+ getSelectedStorageBackend?(): string;
6
+ encryptString(plainText: string): Uint8Array;
7
+ decryptString(encrypted: Buffer): string;
8
+ }
9
+ /** `electron.ipcMain`. */
10
+ export interface IpcMainLike {
11
+ handle(channel: string, listener: (event: any, ...args: any[]) => unknown): void;
12
+ removeHandler?(channel: string): void;
13
+ }
14
+ /** `electron.shell`. */
15
+ export interface ShellLike {
16
+ openExternal(url: string): Promise<void>;
17
+ }
18
+ /** What `before-input-event` hands a listener (`Electron.Input`). */
19
+ export interface InputLike {
20
+ type: string;
21
+ key: string;
22
+ control: boolean;
23
+ meta: boolean;
24
+ shift: boolean;
25
+ alt: boolean;
26
+ }
27
+ /** An event a listener may cancel. */
28
+ export interface PreventableLike {
29
+ preventDefault(): void;
30
+ }
31
+ /** `electron.Session`, for the permission handlers. */
32
+ export interface SessionLike {
33
+ setPermissionRequestHandler(handler: ((webContents: any, permission: any, callback: (granted: boolean) => void, details: any) => void) | null): void;
34
+ setPermissionCheckHandler(handler: ((webContents: any, permission: any, requestingOrigin: string, details: any) => boolean) | null): void;
35
+ }
36
+ /** `electron.WebContents`. */
37
+ export interface WebContentsLike {
38
+ setWindowOpenHandler(handler: (details: {
39
+ url: string;
40
+ }) => any): void;
41
+ on(event: any, listener: (...args: any[]) => void): unknown;
42
+ toggleDevTools(): void;
43
+ session?: SessionLike;
44
+ }
45
+ /** `electron.BrowserWindow`, as far as a lockdown needs it. */
46
+ export interface WindowLike {
47
+ webContents: WebContentsLike;
48
+ }
49
+ /** `electron.app`, as far as the debugger refusal needs it. */
50
+ export interface AppLike {
51
+ isPackaged: boolean;
52
+ commandLine: {
53
+ hasSwitch(name: string): boolean;
54
+ };
55
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /*
3
+ * The few parts of Electron's main-process API this module touches, written as structural types so the engine never
4
+ * imports or depends on `electron`: a game passes its own `app`, `safeStorage`, `ipcMain`, `shell` and windows, and
5
+ * a test passes fakes. Each is a subset of Electron's own type, so Electron's objects fit them as they are.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `1681-engine/electron`: the main-process side of a desktop build on Electron — file-by-file web patching from a
3
+ * release store, the shell's identity, an OS-encrypted session store and a window lockdown. Electron is an optional
4
+ * peer: nothing here imports it, the game hands in its own `app`, `safeStorage`, `ipcMain`, `shell` and windows. Runs
5
+ * in the main process only (it uses Node's `fs` and `crypto`); the pure rules it shares with browsers and phones are
6
+ * `1681-engine/updates`. Importable from ESM (`import`) and from a CommonJS main process (`require`).
7
+ */
8
+ export type { SafeStorageLike, IpcMainLike, ShellLike, InputLike, PreventableLike, SessionLike, WebContentsLike, WindowLike, AppLike } from './electronLike';
9
+ export { shellId, type ShellIdOptions } from './shellId';
10
+ export { WebPatcher, type WebPatcherOptions } from './webPatcher';
11
+ export type { WebPointer, PointerName } from './webPointers';
12
+ export { SessionStore, registerSessionStore, type SessionStoreOptions } from './sessionStore';
13
+ export { isAllowedExternal, isAllowedNavigation, isDevToolsKey, debuggerRequested, DEBUG_SWITCHES, type ExternalPolicy } from './lockdownRules';
14
+ export { lockDownWindow, denyPermissions, refuseDebugLaunch, type LockdownOptions } from './lockdown';
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refuseDebugLaunch = exports.denyPermissions = exports.lockDownWindow = exports.DEBUG_SWITCHES = exports.debuggerRequested = exports.isDevToolsKey = exports.isAllowedNavigation = exports.isAllowedExternal = exports.registerSessionStore = exports.SessionStore = exports.WebPatcher = exports.shellId = void 0;
4
+ var shellId_1 = require("./shellId");
5
+ Object.defineProperty(exports, "shellId", { enumerable: true, get: function () { return shellId_1.shellId; } });
6
+ var webPatcher_1 = require("./webPatcher");
7
+ Object.defineProperty(exports, "WebPatcher", { enumerable: true, get: function () { return webPatcher_1.WebPatcher; } });
8
+ var sessionStore_1 = require("./sessionStore");
9
+ Object.defineProperty(exports, "SessionStore", { enumerable: true, get: function () { return sessionStore_1.SessionStore; } });
10
+ Object.defineProperty(exports, "registerSessionStore", { enumerable: true, get: function () { return sessionStore_1.registerSessionStore; } });
11
+ var lockdownRules_1 = require("./lockdownRules");
12
+ Object.defineProperty(exports, "isAllowedExternal", { enumerable: true, get: function () { return lockdownRules_1.isAllowedExternal; } });
13
+ Object.defineProperty(exports, "isAllowedNavigation", { enumerable: true, get: function () { return lockdownRules_1.isAllowedNavigation; } });
14
+ Object.defineProperty(exports, "isDevToolsKey", { enumerable: true, get: function () { return lockdownRules_1.isDevToolsKey; } });
15
+ Object.defineProperty(exports, "debuggerRequested", { enumerable: true, get: function () { return lockdownRules_1.debuggerRequested; } });
16
+ Object.defineProperty(exports, "DEBUG_SWITCHES", { enumerable: true, get: function () { return lockdownRules_1.DEBUG_SWITCHES; } });
17
+ var lockdown_1 = require("./lockdown");
18
+ Object.defineProperty(exports, "lockDownWindow", { enumerable: true, get: function () { return lockdown_1.lockDownWindow; } });
19
+ Object.defineProperty(exports, "denyPermissions", { enumerable: true, get: function () { return lockdown_1.denyPermissions; } });
20
+ Object.defineProperty(exports, "refuseDebugLaunch", { enumerable: true, get: function () { return lockdown_1.refuseDebugLaunch; } });
@@ -0,0 +1,33 @@
1
+ import type { AppLike, SessionLike, ShellLike, WindowLike } from './electronLike';
2
+ import { type ExternalPolicy } from './lockdownRules';
3
+ /** How {@link lockDownWindow} treats a window. */
4
+ export interface LockdownOptions {
5
+ /** `electron.shell`, which opens an allowed link in the system browser. */
6
+ shell: ShellLike;
7
+ /** The links that may leave for the system browser. */
8
+ external: ExternalPolicy;
9
+ /** `app.isPackaged`: a packaged build refuses the DevTools keys. */
10
+ packaged: boolean;
11
+ /** A dev run's page origin, allowed for navigation; leave unset in a packaged build whatever the environment says. */
12
+ devServerUrl?: string;
13
+ /** In a dev run, this key toggles DevTools. Default `F12`; null for none. */
14
+ devToolsKey?: string | null;
15
+ /** Where refusals are reported. Default `console.log`. */
16
+ log?: (...args: unknown[]) => void;
17
+ }
18
+ /**
19
+ * Keeps a window on the game: a new window is never opened (an allowed link goes to the system browser instead, any
20
+ * other is refused), the window never navigates or redirects away from the game's own page, and a packaged build
21
+ * refuses the DevTools keys (set `webPreferences.devTools: !app.isPackaged` too: the keys are only the visible way in).
22
+ */
23
+ export declare function lockDownWindow(win: WindowLike, o: LockdownOptions): void;
24
+ /**
25
+ * Answers every permission request and check on `session` (camera, microphone, notifications, geolocation…) with no,
26
+ * but those in `allow`. Chromium's default in Electron is yes to almost everything, which a game never needs.
27
+ */
28
+ export declare function denyPermissions(session: SessionLike, allow?: readonly string[]): void;
29
+ /**
30
+ * A packaged app started with a debugger switch quits before it opens a window. Call it first thing in the main
31
+ * process; `exit` defaults to `process.exit` — not `app.exit`, so nothing after the call may run.
32
+ */
33
+ export declare function refuseDebugLaunch(app: AppLike, argv?: readonly string[], exit?: (code: number) => void): boolean;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lockDownWindow = lockDownWindow;
4
+ exports.denyPermissions = denyPermissions;
5
+ exports.refuseDebugLaunch = refuseDebugLaunch;
6
+ const lockdownRules_1 = require("./lockdownRules");
7
+ /**
8
+ * Keeps a window on the game: a new window is never opened (an allowed link goes to the system browser instead, any
9
+ * other is refused), the window never navigates or redirects away from the game's own page, and a packaged build
10
+ * refuses the DevTools keys (set `webPreferences.devTools: !app.isPackaged` too: the keys are only the visible way in).
11
+ */
12
+ function lockDownWindow(win, o) {
13
+ const log = o.log ?? ((...a) => console.log(...a));
14
+ const wc = win.webContents;
15
+ const openOut = (url, what) => {
16
+ if ((0, lockdownRules_1.isAllowedExternal)(url, o.external))
17
+ void o.shell.openExternal(url).catch((err) => log('could not open', url, err));
18
+ else
19
+ log(`refused ${what}:`, url);
20
+ };
21
+ wc.setWindowOpenHandler(({ url }) => {
22
+ openOut(url, 'external link');
23
+ return { action: 'deny' };
24
+ });
25
+ wc.on('will-navigate', (event, url) => {
26
+ if ((0, lockdownRules_1.isAllowedNavigation)(url, o.devServerUrl))
27
+ return;
28
+ event.preventDefault();
29
+ openOut(url, 'navigation');
30
+ });
31
+ wc.on('will-redirect', (event, url) => {
32
+ if (!(0, lockdownRules_1.isAllowedNavigation)(url, o.devServerUrl))
33
+ event.preventDefault();
34
+ });
35
+ const devKey = o.devToolsKey === undefined ? 'F12' : o.devToolsKey;
36
+ wc.on('before-input-event', (event, input) => {
37
+ if (o.packaged) {
38
+ if ((0, lockdownRules_1.isDevToolsKey)(input))
39
+ event.preventDefault();
40
+ return;
41
+ }
42
+ if (devKey && input.type === 'keyDown' && input.key === devKey) {
43
+ wc.toggleDevTools();
44
+ event.preventDefault();
45
+ }
46
+ });
47
+ }
48
+ /**
49
+ * Answers every permission request and check on `session` (camera, microphone, notifications, geolocation…) with no,
50
+ * but those in `allow`. Chromium's default in Electron is yes to almost everything, which a game never needs.
51
+ */
52
+ function denyPermissions(session, allow = []) {
53
+ const allowed = new Set(allow);
54
+ session.setPermissionRequestHandler((_wc, permission, callback) => callback(allowed.has(permission)));
55
+ session.setPermissionCheckHandler((_wc, permission) => allowed.has(permission));
56
+ }
57
+ /**
58
+ * A packaged app started with a debugger switch quits before it opens a window. Call it first thing in the main
59
+ * process; `exit` defaults to `process.exit` — not `app.exit`, so nothing after the call may run.
60
+ */
61
+ function refuseDebugLaunch(app, argv = process.argv, exit = (c) => process.exit(c)) {
62
+ if (!app.isPackaged)
63
+ return false;
64
+ if (!(0, lockdownRules_1.debuggerRequested)(argv, (s) => app.commandLine.hasSwitch(s)))
65
+ return false;
66
+ console.error('[main] refusing to run with a debugger attached');
67
+ exit(1);
68
+ return true;
69
+ }
@@ -0,0 +1,28 @@
1
+ import type { InputLike } from './electronLike';
2
+ /**
3
+ * The links a locked-down window may open in the system browser: https only, no credentials in the URL, to one of
4
+ * `hosts` or a subdomain of it — or, for a host where anyone can publish a page (a code host), only under one of its
5
+ * `paths`.
6
+ */
7
+ export interface ExternalPolicy {
8
+ /** Hosts allowed whole, each with its subdomains (`example.com` allows `www.example.com`). */
9
+ hosts: readonly string[];
10
+ /** Hosts allowed only under a path prefix (exact host, prefix compared case-insensitively), e.g. one owner's repositories. */
11
+ paths?: readonly {
12
+ host: string;
13
+ prefix: string;
14
+ }[];
15
+ }
16
+ /** Whether `url` is a link {@link ExternalPolicy} lets out to the system browser. */
17
+ export declare function isAllowedExternal(url: string, policy: ExternalPolicy): boolean;
18
+ /**
19
+ * The game's own page: the bundled `file://` build, or the dev server's origin in a dev run. Anything else followed
20
+ * inside the window would replace the game with a web page that has the shell's preload API.
21
+ */
22
+ export declare function isAllowedNavigation(url: string, devServerUrl?: string): boolean;
23
+ /** The keys that open DevTools (F12, Ctrl/Cmd+Shift/Alt+I/J/C), refused in a packaged build. */
24
+ export declare function isDevToolsKey(input: InputLike): boolean;
25
+ /** Chromium and Node switches that attach a debugger to a running app. */
26
+ export declare const DEBUG_SWITCHES: readonly string[];
27
+ /** Whether the command line (`argv`, or a switch Chromium parsed: `hasSwitch`) asks for a debugger. */
28
+ export declare function debuggerRequested(argv: readonly string[], hasSwitch?: (name: string) => boolean): boolean;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEBUG_SWITCHES = void 0;
4
+ exports.isAllowedExternal = isAllowedExternal;
5
+ exports.isAllowedNavigation = isAllowedNavigation;
6
+ exports.isDevToolsKey = isDevToolsKey;
7
+ exports.debuggerRequested = debuggerRequested;
8
+ function parse(url) {
9
+ try {
10
+ return new URL(url);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ /** Whether `url` is a link {@link ExternalPolicy} lets out to the system browser. */
17
+ function isAllowedExternal(url, policy) {
18
+ const u = parse(url);
19
+ if (!u || u.protocol !== 'https:' || u.username || u.password)
20
+ return false;
21
+ const host = u.hostname.toLowerCase();
22
+ const scoped = (policy.paths ?? []).filter((p) => p.host.toLowerCase() === host);
23
+ if (scoped.length)
24
+ return scoped.some((p) => u.pathname.toLowerCase().startsWith(p.prefix.toLowerCase()));
25
+ return policy.hosts.some((h) => {
26
+ const want = h.toLowerCase();
27
+ return host === want || host.endsWith(`.${want}`);
28
+ });
29
+ }
30
+ /**
31
+ * The game's own page: the bundled `file://` build, or the dev server's origin in a dev run. Anything else followed
32
+ * inside the window would replace the game with a web page that has the shell's preload API.
33
+ */
34
+ function isAllowedNavigation(url, devServerUrl) {
35
+ const u = parse(url);
36
+ if (!u)
37
+ return false;
38
+ if (devServerUrl) {
39
+ const dev = parse(devServerUrl);
40
+ return !!dev && u.origin === dev.origin;
41
+ }
42
+ return u.protocol === 'file:';
43
+ }
44
+ /** The keys that open DevTools (F12, Ctrl/Cmd+Shift/Alt+I/J/C), refused in a packaged build. */
45
+ function isDevToolsKey(input) {
46
+ if (input.type !== 'keyDown')
47
+ return false;
48
+ if (input.key === 'F12')
49
+ return true;
50
+ const key = input.key.toLowerCase();
51
+ const mod = input.control || input.meta;
52
+ return mod && (input.shift || input.alt) && (key === 'i' || key === 'j' || key === 'c');
53
+ }
54
+ /** Chromium and Node switches that attach a debugger to a running app. */
55
+ exports.DEBUG_SWITCHES = ['--inspect', '--remote-debugging-port', '--remote-debugging-pipe', '--remote-debugging-address'];
56
+ const DEBUG_SWITCH_NAMES = ['remote-debugging-port', 'remote-debugging-pipe', 'inspect', 'inspect-brk'];
57
+ /** Whether the command line (`argv`, or a switch Chromium parsed: `hasSwitch`) asks for a debugger. */
58
+ function debuggerRequested(argv, hasSwitch = () => false) {
59
+ const flagged = argv.some((arg) => exports.DEBUG_SWITCHES.some((s) => arg === s || arg.startsWith(`${s}=`) || arg.startsWith(`${s}-`)));
60
+ return flagged || DEBUG_SWITCH_NAMES.some((s) => hasSwitch(s));
61
+ }
@@ -0,0 +1,40 @@
1
+ import type { IpcMainLike, SafeStorageLike } from './electronLike';
2
+ /** Where and how a {@link SessionStore} keeps its one secret. */
3
+ export interface SessionStoreOptions {
4
+ /** `electron.safeStorage`. */
5
+ safeStorage: SafeStorageLike;
6
+ /** The folder, e.g. `app.getPath('userData')` — a second app (a test build) with its own user data keeps its own. */
7
+ dir: string;
8
+ /** Default `session.bin`. */
9
+ fileName?: string;
10
+ /** Longest value accepted, in characters. Default 4096: a saved login is a token and a name, never a blob. */
11
+ maxLength?: number;
12
+ /** Default `process.platform`. */
13
+ platform?: string;
14
+ }
15
+ /**
16
+ * One small secret (a refresh token and username, as a JSON string) kept for "stay logged in", encrypted by the OS —
17
+ * `safeStorage`: DPAPI for the Windows user, the Keychain on the Mac. Where the OS offers no real encryption (Linux
18
+ * without a keyring: `basic_text`, a key written into Chromium) nothing is written and the player logs in again after
19
+ * closing; a token on disk in the clear is worse than that. Never `localStorage`: any script in the page could read it.
20
+ */
21
+ export declare class SessionStore {
22
+ private readonly o;
23
+ private readonly file;
24
+ private readonly maxLength;
25
+ private readonly platform;
26
+ constructor(o: SessionStoreOptions);
27
+ /** Whether the OS can encrypt for real here. */
28
+ canEncrypt(): boolean;
29
+ /** The saved value, or null when there is none, it cannot be decrypted (another OS user, a reinstall) or nothing may be. */
30
+ get(): Promise<string | null>;
31
+ /** Saves `value`; false when it is not a string, too long, or the OS cannot encrypt it. */
32
+ set(value: unknown): Promise<boolean>;
33
+ clear(): Promise<void>;
34
+ }
35
+ /**
36
+ * Answers a renderer's `<prefix>:get`, `<prefix>:set` (one string) and `<prefix>:clear` from `store`, so the page only
37
+ * ever sees the plain value, and only its own. Expose them in the preload with `ipcRenderer.invoke`. Returns a function
38
+ * that removes the handlers.
39
+ */
40
+ export declare function registerSessionStore(ipcMain: IpcMainLike, store: SessionStore, prefix?: string): () => void;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionStore = void 0;
4
+ exports.registerSessionStore = registerSessionStore;
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ /**
8
+ * One small secret (a refresh token and username, as a JSON string) kept for "stay logged in", encrypted by the OS —
9
+ * `safeStorage`: DPAPI for the Windows user, the Keychain on the Mac. Where the OS offers no real encryption (Linux
10
+ * without a keyring: `basic_text`, a key written into Chromium) nothing is written and the player logs in again after
11
+ * closing; a token on disk in the clear is worse than that. Never `localStorage`: any script in the page could read it.
12
+ */
13
+ class SessionStore {
14
+ o;
15
+ file;
16
+ maxLength;
17
+ platform;
18
+ constructor(o) {
19
+ this.o = o;
20
+ this.file = (0, node_path_1.join)(o.dir, o.fileName ?? 'session.bin');
21
+ this.maxLength = o.maxLength ?? 4096;
22
+ this.platform = o.platform ?? process.platform;
23
+ }
24
+ /** Whether the OS can encrypt for real here. */
25
+ canEncrypt() {
26
+ const s = this.o.safeStorage;
27
+ if (!s.isEncryptionAvailable())
28
+ return false;
29
+ return this.platform !== 'linux' || s.getSelectedStorageBackend?.() !== 'basic_text';
30
+ }
31
+ /** The saved value, or null when there is none, it cannot be decrypted (another OS user, a reinstall) or nothing may be. */
32
+ async get() {
33
+ if (!this.canEncrypt())
34
+ return null;
35
+ try {
36
+ return this.o.safeStorage.decryptString(await node_fs_1.promises.readFile(this.file));
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** Saves `value`; false when it is not a string, too long, or the OS cannot encrypt it. */
43
+ async set(value) {
44
+ if (typeof value !== 'string' || value.length > this.maxLength || !this.canEncrypt())
45
+ return false;
46
+ // written beside it and renamed over it, so a crash or a second write mid-way never leaves half a file
47
+ const temp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
48
+ try {
49
+ await node_fs_1.promises.mkdir(this.o.dir, { recursive: true });
50
+ await node_fs_1.promises.writeFile(temp, this.o.safeStorage.encryptString(value));
51
+ await node_fs_1.promises.rename(temp, this.file);
52
+ }
53
+ catch (err) {
54
+ await node_fs_1.promises.rm(temp, { force: true });
55
+ throw err;
56
+ }
57
+ return true;
58
+ }
59
+ async clear() {
60
+ await node_fs_1.promises.rm(this.file, { force: true });
61
+ }
62
+ }
63
+ exports.SessionStore = SessionStore;
64
+ /**
65
+ * Answers a renderer's `<prefix>:get`, `<prefix>:set` (one string) and `<prefix>:clear` from `store`, so the page only
66
+ * ever sees the plain value, and only its own. Expose them in the preload with `ipcRenderer.invoke`. Returns a function
67
+ * that removes the handlers.
68
+ */
69
+ function registerSessionStore(ipcMain, store, prefix = 'session') {
70
+ ipcMain.handle(`${prefix}:get`, () => store.get());
71
+ ipcMain.handle(`${prefix}:set`, (_e, value) => store.set(value));
72
+ ipcMain.handle(`${prefix}:clear`, () => store.clear());
73
+ return () => {
74
+ for (const c of ['get', 'set', 'clear'])
75
+ ipcMain.removeHandler?.(`${prefix}:${c}`);
76
+ };
77
+ }
@@ -0,0 +1,22 @@
1
+ /** What goes into a shell's identity besides the runtime's version. */
2
+ export interface ShellIdOptions {
3
+ /** Folders whose files (non-recursive) are hashed, in this order. */
4
+ dirs: string[];
5
+ /** Which files of each folder count; default the compiled main-process code, `*.cjs`. */
6
+ match?: (fileName: string) => boolean;
7
+ /** More text to fold in (an engine or framework version the main process bundles from elsewhere). */
8
+ extra?: string[];
9
+ /** Hex characters kept; default 16. */
10
+ length?: number;
11
+ }
12
+ /**
13
+ * A desktop shell's identity: a hash of the main process's own code and the Electron version it runs on. A web patch
14
+ * is taken only by a shell with the same identity, because the web talks to the shell through its preload script and
15
+ * may need what a newer shell adds; anything else comes with an installer. The publish script reckons it the same way
16
+ * off the build it uploads, so nobody has to remember to raise a number when the shell changes.
17
+ *
18
+ * `shellId(version, dir)` hashes `dir`'s `*.cjs`; pass {@link ShellIdOptions} for more folders or other files. With one
19
+ * folder, the default match and no `extra`, both forms give the same hash. Code the shell takes from `node_modules`
20
+ * (this module included) is not in `dir`: name its version in `extra`, or a change there would not change the identity.
21
+ */
22
+ export declare function shellId(electronVersion: string, dirOrOptions: string | ShellIdOptions): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shellId = shellId;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
7
+ /**
8
+ * A desktop shell's identity: a hash of the main process's own code and the Electron version it runs on. A web patch
9
+ * is taken only by a shell with the same identity, because the web talks to the shell through its preload script and
10
+ * may need what a newer shell adds; anything else comes with an installer. The publish script reckons it the same way
11
+ * off the build it uploads, so nobody has to remember to raise a number when the shell changes.
12
+ *
13
+ * `shellId(version, dir)` hashes `dir`'s `*.cjs`; pass {@link ShellIdOptions} for more folders or other files. With one
14
+ * folder, the default match and no `extra`, both forms give the same hash. Code the shell takes from `node_modules`
15
+ * (this module included) is not in `dir`: name its version in `extra`, or a change there would not change the identity.
16
+ */
17
+ function shellId(electronVersion, dirOrOptions) {
18
+ const o = typeof dirOrOptions === 'string' ? { dirs: [dirOrOptions] } : dirOrOptions;
19
+ const match = o.match ?? ((f) => f.endsWith('.cjs'));
20
+ const h = (0, node_crypto_1.createHash)('sha256');
21
+ h.update(`electron ${electronVersion}\n`);
22
+ for (const dir of o.dirs) {
23
+ for (const name of (0, node_fs_1.readdirSync)(dir).filter(match).sort()) {
24
+ h.update(`${name}\n`);
25
+ h.update((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, name)));
26
+ }
27
+ }
28
+ for (const e of o.extra ?? [])
29
+ h.update(`extra ${e}\n`);
30
+ return h.digest('hex').slice(0, o.length ?? 16);
31
+ }
@@ -0,0 +1,13 @@
1
+ /** Lower-case hex sha256. */
2
+ export declare const sha256: (data: Uint8Array) => string;
3
+ /** Every file under `dir`, as forward-slash paths relative to it. */
4
+ export declare function listFiles(dir: string, base?: string): Promise<string[]>;
5
+ /** Every file of the web in `dir`, path → sha256. */
6
+ export declare function hashWeb(dir: string): Promise<Map<string, string>>;
7
+ /** Writes `data` at the manifest path `path` under `dir`, making its folders. */
8
+ export declare function putFile(dir: string, path: string, data: Uint8Array): Promise<void>;
9
+ /**
10
+ * `fetch` that gives up after `ms` and never takes a cached answer: a store behind a CDN otherwise hands back
11
+ * yesterday's `latest.json`, and a stalled connection would hold an update check open for good.
12
+ */
13
+ export declare function fetchWithTimeout(fetcher: typeof fetch, url: string, ms: number): Promise<Response>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sha256 = void 0;
4
+ exports.listFiles = listFiles;
5
+ exports.hashWeb = hashWeb;
6
+ exports.putFile = putFile;
7
+ exports.fetchWithTimeout = fetchWithTimeout;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const promises_1 = require("node:fs/promises");
10
+ const node_path_1 = require("node:path");
11
+ /** Lower-case hex sha256. */
12
+ const sha256 = (data) => (0, node_crypto_1.createHash)('sha256').update(data).digest('hex');
13
+ exports.sha256 = sha256;
14
+ /** Every file under `dir`, as forward-slash paths relative to it. */
15
+ async function listFiles(dir, base = '') {
16
+ const out = [];
17
+ for (const e of await (0, promises_1.readdir)((0, node_path_1.join)(dir, base), { withFileTypes: true })) {
18
+ const rel = base ? `${base}/${e.name}` : e.name;
19
+ if (e.isDirectory())
20
+ out.push(...(await listFiles(dir, rel)));
21
+ else
22
+ out.push(rel);
23
+ }
24
+ return out;
25
+ }
26
+ /** Every file of the web in `dir`, path → sha256. */
27
+ async function hashWeb(dir) {
28
+ const have = new Map();
29
+ for (const path of await listFiles(dir))
30
+ have.set(path, (0, exports.sha256)(await (0, promises_1.readFile)((0, node_path_1.join)(dir, ...path.split('/')))));
31
+ return have;
32
+ }
33
+ /** Writes `data` at the manifest path `path` under `dir`, making its folders. */
34
+ async function putFile(dir, path, data) {
35
+ const file = (0, node_path_1.join)(dir, ...path.split('/'));
36
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(file), { recursive: true });
37
+ await (0, promises_1.writeFile)(file, data);
38
+ }
39
+ /**
40
+ * `fetch` that gives up after `ms` and never takes a cached answer: a store behind a CDN otherwise hands back
41
+ * yesterday's `latest.json`, and a stalled connection would hold an update check open for good.
42
+ */
43
+ async function fetchWithTimeout(fetcher, url, ms) {
44
+ const ctrl = new AbortController();
45
+ const timer = setTimeout(() => ctrl.abort(), ms);
46
+ try {
47
+ return await fetcher(url, { signal: ctrl.signal, cache: 'no-store' });
48
+ }
49
+ finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }