akanjs 3.0.0-beta.26 → 3.0.0-beta.28

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 (226) hide show
  1. package/base/baseEnv.ts +19 -5
  2. package/client/csrTypes.ts +2 -0
  3. package/client/device.ts +28 -12
  4. package/client/index.ts +1 -0
  5. package/client/native/accessibility.ts +1 -0
  6. package/client/native/app-state.ts +1 -0
  7. package/client/native/app.ts +1 -0
  8. package/client/native/appearance.ts +1 -0
  9. package/client/native/auth-session.ts +1 -0
  10. package/client/native/autostart.ts +1 -0
  11. package/client/native/badge.ts +1 -0
  12. package/client/native/biometric.ts +1 -0
  13. package/client/native/browser.ts +1 -0
  14. package/client/native/camera.ts +1 -0
  15. package/client/native/clipboard.ts +1 -0
  16. package/client/native/contacts.ts +1 -0
  17. package/client/native/device.ts +1 -0
  18. package/client/native/dialog.ts +1 -0
  19. package/client/native/dock.ts +1 -0
  20. package/client/native/file-picker.ts +1 -0
  21. package/client/native/filesystem.ts +1 -0
  22. package/client/native/geolocation.ts +1 -0
  23. package/client/native/global-shortcut.ts +1 -0
  24. package/client/native/haptics.ts +1 -0
  25. package/client/native/http.ts +1 -0
  26. package/client/native/iap.ts +1 -0
  27. package/client/native/keep-awake.ts +1 -0
  28. package/client/native/keyboard.ts +1 -0
  29. package/client/native/local-notifications.ts +1 -0
  30. package/client/native/menu.ts +1 -0
  31. package/client/native/network.ts +1 -0
  32. package/client/native/opener.ts +1 -0
  33. package/client/native/preferences.ts +1 -0
  34. package/client/native/push.ts +1 -0
  35. package/client/native/screen-orientation.ts +1 -0
  36. package/client/native/screen.ts +1 -0
  37. package/client/native/secure-storage.ts +1 -0
  38. package/client/native/share.ts +1 -0
  39. package/client/native/single-instance.ts +1 -0
  40. package/client/native/splash-screen.ts +1 -0
  41. package/client/native/sqlite.ts +1 -0
  42. package/client/native/toast.ts +1 -0
  43. package/client/native/tray.ts +1 -0
  44. package/client/native/updates.ts +1 -0
  45. package/client/native/volume.ts +1 -0
  46. package/client/native/window-state.ts +1 -0
  47. package/client/native/window.ts +1 -0
  48. package/client/native.ts +31 -1
  49. package/client/resolveServerUrl.ts +11 -0
  50. package/client/router.ts +8 -0
  51. package/common/Logger.ts +26 -2
  52. package/common/RouteLayering.ts +52 -0
  53. package/common/index.ts +1 -0
  54. package/index.ts +115 -6
  55. package/native/desktop.ts +5 -0
  56. package/package.json +11 -1
  57. package/server/akanApp.ts +23 -3
  58. package/server/akanAppHeaders.ts +4 -1
  59. package/server/akanServer.ts +27 -7
  60. package/server/index.ts +1 -0
  61. package/server/lifecycle/orphanGroup.ts +21 -0
  62. package/server/nativeFile.ts +107 -0
  63. package/server/ops/opsRoute.ts +3 -2
  64. package/server/routeTreeBuilder.ts +14 -39
  65. package/server/routing/apiRouter.ts +26 -3
  66. package/server/routing/hostAllowlist.ts +24 -0
  67. package/server/selfExec.ts +25 -0
  68. package/server/types.tsx +3 -1
  69. package/server/webRouter.ts +7 -4
  70. package/service/ipcTypes.ts +4 -0
  71. package/signal/CrossSiteGuard.ts +7 -0
  72. package/test/signalTestRuntime.ts +8 -2
  73. package/types/base/baseEnv.d.ts +1 -0
  74. package/types/client/csrTypes.d.ts +2 -0
  75. package/types/client/index.d.ts +1 -0
  76. package/types/client/native/accessibility.d.ts +1 -0
  77. package/types/client/native/app-state.d.ts +1 -0
  78. package/types/client/native/app.d.ts +1 -0
  79. package/types/client/native/appearance.d.ts +1 -0
  80. package/types/client/native/auth-session.d.ts +1 -0
  81. package/types/client/native/autostart.d.ts +1 -0
  82. package/types/client/native/badge.d.ts +1 -0
  83. package/types/client/native/biometric.d.ts +1 -0
  84. package/types/client/native/browser.d.ts +1 -0
  85. package/types/client/native/camera.d.ts +1 -0
  86. package/types/client/native/clipboard.d.ts +1 -0
  87. package/types/client/native/contacts.d.ts +1 -0
  88. package/types/client/native/device.d.ts +1 -0
  89. package/types/client/native/dialog.d.ts +1 -0
  90. package/types/client/native/dock.d.ts +1 -0
  91. package/types/client/native/file-picker.d.ts +1 -0
  92. package/types/client/native/filesystem.d.ts +1 -0
  93. package/types/client/native/geolocation.d.ts +1 -0
  94. package/types/client/native/global-shortcut.d.ts +1 -0
  95. package/types/client/native/haptics.d.ts +1 -0
  96. package/types/client/native/http.d.ts +1 -0
  97. package/types/client/native/iap.d.ts +1 -0
  98. package/types/client/native/keep-awake.d.ts +1 -0
  99. package/types/client/native/keyboard.d.ts +1 -0
  100. package/types/client/native/local-notifications.d.ts +1 -0
  101. package/types/client/native/menu.d.ts +1 -0
  102. package/types/client/native/network.d.ts +1 -0
  103. package/types/client/native/opener.d.ts +1 -0
  104. package/types/client/native/preferences.d.ts +1 -0
  105. package/types/client/native/push.d.ts +1 -0
  106. package/types/client/native/screen-orientation.d.ts +1 -0
  107. package/types/client/native/screen.d.ts +1 -0
  108. package/types/client/native/secure-storage.d.ts +1 -0
  109. package/types/client/native/share.d.ts +1 -0
  110. package/types/client/native/single-instance.d.ts +1 -0
  111. package/types/client/native/splash-screen.d.ts +1 -0
  112. package/types/client/native/sqlite.d.ts +1 -0
  113. package/types/client/native/toast.d.ts +1 -0
  114. package/types/client/native/tray.d.ts +1 -0
  115. package/types/client/native/updates.d.ts +1 -0
  116. package/types/client/native/volume.d.ts +1 -0
  117. package/types/client/native/window-state.d.ts +1 -0
  118. package/types/client/native/window.d.ts +1 -0
  119. package/types/client/native.d.ts +14 -2
  120. package/types/client/resolveServerUrl.d.ts +1 -0
  121. package/types/client/router.d.ts +3 -0
  122. package/types/common/RouteLayering.d.ts +28 -0
  123. package/types/common/index.d.ts +1 -0
  124. package/types/index.d.ts +112 -6
  125. package/types/native/desktop.d.ts +3 -0
  126. package/types/server/akanAppHeaders.d.ts +2 -0
  127. package/types/server/index.d.ts +1 -0
  128. package/types/server/lifecycle/orphanGroup.d.ts +7 -0
  129. package/types/server/nativeFile.d.ts +10 -0
  130. package/types/server/routing/apiRouter.d.ts +3 -1
  131. package/types/server/routing/hostAllowlist.d.ts +8 -0
  132. package/types/server/selfExec.d.ts +8 -0
  133. package/types/server/types.d.ts +3 -1
  134. package/types/service/ipcTypes.d.ts +16 -0
  135. package/types/signal/CrossSiteGuard.d.ts +1 -0
  136. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  137. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +57 -4
  138. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +48 -0
  139. package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +8 -0
  140. package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +8 -2
  141. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +10 -0
  142. package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +24 -0
  143. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +24 -0
  144. package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +4 -1
  145. package/types/vendor/@akanjs/native/packages/cli/src/platforms/macos.d.ts +5 -0
  146. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +38 -0
  147. package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +8 -1
  148. package/types/vendor/@akanjs/native/packages/desktop/src/grants.d.ts +11 -0
  149. package/types/vendor/@akanjs/native/packages/desktop/src/main.d.ts +11 -0
  150. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +13 -2
  151. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +27 -0
  152. package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +21 -0
  153. package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +79 -0
  154. package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +5 -0
  155. package/types/vendor/@akanjs/native/plugins/file-picker/src/index.d.ts +20 -1
  156. package/types/vendor/@akanjs/native/plugins/push/src/index.d.ts +9 -0
  157. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +28 -0
  158. package/types/vendor/@akanjs/native/plugins/volume/src/common.d.ts +10 -0
  159. package/types/vendor/@akanjs/native/plugins/volume/src/desktop.d.ts +18 -0
  160. package/types/vendor/@akanjs/native/plugins/volume/src/index.d.ts +28 -0
  161. package/types/vendor/@akanjs/native/plugins/volume/src.d.ts +1 -0
  162. package/types/webkit/csrFrameDump.d.ts +42 -0
  163. package/types/webkit/nativeNavigation.d.ts +1 -0
  164. package/types/webkit/nativePurchase.d.ts +6 -2
  165. package/types/webkit/nativeUpdates.d.ts +8 -0
  166. package/ui/Agent/Attach.tsx +6 -2
  167. package/ui/CsrImage.tsx +3 -2
  168. package/ui/System/CSR.tsx +4 -0
  169. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +5 -1
  170. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
  171. package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
  172. package/vendor/@akanjs/native/native/desktop/src/lib.rs +161 -25
  173. package/vendor/@akanjs/native/native/desktop/src/volume.rs +184 -0
  174. package/vendor/@akanjs/native/native/desktop/src/win/mod.rs +3 -1
  175. package/vendor/@akanjs/native/native/desktop/src/win/volume.rs +71 -0
  176. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
  177. package/vendor/@akanjs/native/packages/cli/src/api.ts +142 -12
  178. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +5 -3
  179. package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +10 -71
  180. package/vendor/@akanjs/native/packages/cli/src/config.ts +41 -0
  181. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -2
  182. package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +58 -0
  183. package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +2 -0
  184. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +10 -2
  185. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +38 -2
  186. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +104 -0
  187. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +85 -5
  188. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +1 -0
  189. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +6 -4
  190. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +16 -0
  191. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +44 -1
  192. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +436 -0
  193. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +2 -0
  194. package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
  195. package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +20 -0
  196. package/vendor/@akanjs/native/packages/desktop/src/grants.ts +47 -0
  197. package/vendor/@akanjs/native/packages/desktop/src/host.ts +102 -14
  198. package/vendor/@akanjs/native/packages/desktop/src/main.ts +59 -11
  199. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +38 -4
  200. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +28 -0
  201. package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +159 -0
  202. package/vendor/@akanjs/native/packages/desktop/src/server.ts +493 -0
  203. package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +14 -0
  204. package/vendor/@akanjs/native/plugins/app/native-plugin.json +2 -2
  205. package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
  206. package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
  207. package/vendor/@akanjs/native/plugins/app/src/web.ts +3 -0
  208. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +39 -2
  209. package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +36 -1
  210. package/vendor/@akanjs/native/plugins/push/android/PushPlugin.kt +9 -1
  211. package/vendor/@akanjs/native/plugins/push/ios/PushPlugin.swift +4 -0
  212. package/vendor/@akanjs/native/plugins/push/src/index.ts +10 -0
  213. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +464 -452
  214. package/vendor/@akanjs/native/plugins/volume/android/VolumePlugin.kt +107 -0
  215. package/vendor/@akanjs/native/plugins/volume/native-plugin.json +14 -0
  216. package/vendor/@akanjs/native/plugins/volume/src/common.ts +28 -0
  217. package/vendor/@akanjs/native/plugins/volume/src/desktop.ts +185 -0
  218. package/vendor/@akanjs/native/plugins/volume/src/index.ts +32 -0
  219. package/webkit/CsrRouteTable.ts +15 -35
  220. package/webkit/RenderLayer.tsx +71 -6
  221. package/webkit/bootCsr.tsx +11 -8
  222. package/webkit/csrFrameDump.ts +67 -0
  223. package/webkit/nativeNavigation.ts +17 -0
  224. package/webkit/nativePurchase.ts +12 -2
  225. package/webkit/nativeUpdates.ts +55 -0
  226. package/webkit/useCsrValues.ts +27 -0
@@ -0,0 +1,436 @@
1
+
2
+
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { exec } from "../lib/exec.ts";
6
+ import { CliError, log, ToolchainError } from "../lib/log.ts";
7
+ import type { BuildContext } from "../lib/prepare.ts";
8
+ import { akanNativeHome } from "../lib/toolchains.ts";
9
+ import { hostArch } from "../lib/updates.ts";
10
+ import { windowsVersion } from "./windows.ts";
11
+
12
+ /** Microsoft's permanent link to the Evergreen Bootstrapper (MicrosoftEdgeWebview2Setup.exe). */
13
+ export const WEBVIEW2_BOOTSTRAPPER_URL = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
14
+ /** The WebView2 Runtime's EdgeUpdate client id; its `pv` value is the installed version. */
15
+ const WEBVIEW2_CLIENT = "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
16
+ /** MAX_PATH less its NUL: what a PC without long paths enabled (the Windows default) can open or unpack. */
17
+ export const WINDOWS_MAX_PATH = 259;
18
+ /** `C:\Users\` + a 20-character user name + `\AppData\Local\Programs\`. */
19
+ const INSTALL_ROOT = "C:\\Users\\".length + 20 + "\\AppData\\Local\\Programs\\".length;
20
+ /** `.update-<version>-<sequence>\`: an update unpacks next to the app, under the app's name again, before the swap. */
21
+ const STAGING = ".update-".length + 24 + 1;
22
+
23
+ export interface InstallerInput {
24
+ name: string;
25
+ id: string;
26
+ exe: string;
27
+ version: string;
28
+ build: number;
29
+ /** The built app folder, copied whole. */
30
+ folder: string;
31
+ /** Its top-level files and folders: what an update or uninstall removes. */
32
+ entries: { name: string; dir: boolean }[];
33
+ icon?: string;
34
+ bootstrapper: string;
35
+ stopScript: string;
36
+ outFile: string;
37
+ }
38
+
39
+ /** `$` is NSIS's variable sigil; the names that reach a script cannot hold a quote (config.ts app.name). */
40
+ const nsis = (value: string) => value.replace(/\$/g, "$$$$");
41
+
42
+ const removal = (entries: InstallerInput["entries"]) =>
43
+ entries
44
+ .map(({ name, dir }) => (dir ? ` RMDir /r "$INSTDIR\\${nsis(name)}"` : ` Delete "$INSTDIR\\${nsis(name)}"`))
45
+ .join("\n");
46
+
47
+ const moveIn = (entries: InstallerInput["entries"]) =>
48
+ entries.map(({ name }) => ` Rename "$INSTDIR.setup-new\\${nsis(name)}" "$INSTDIR\\${nsis(name)}"`).join("\n");
49
+
50
+ export function installerScript(input: InstallerInput): string {
51
+ const version = windowsVersion(input.version, input.build);
52
+ const icon = input.icon ? `!define MUI_ICON "${nsis(input.icon)}"\n!define MUI_UNICON "${nsis(input.icon)}"\n` : "";
53
+ return `Unicode true
54
+ ManifestDPIAware true
55
+ ManifestLongPathAware true
56
+ SetCompressor /SOLID lzma
57
+ RequestExecutionLevel user
58
+
59
+ !include "MUI2.nsh"
60
+ !include "FileFunc.nsh"
61
+ !include "LogicLib.nsh"
62
+
63
+ !define APP_NAME "${nsis(input.name)}"
64
+ !define EXE "${nsis(input.exe)}"
65
+ !define APP_ID "${nsis(input.id)}"
66
+ !define UNINSTALL_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${input.id}"
67
+ !define RUN_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Run"
68
+ !define STARTUP_APPROVED_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run"
69
+ !define WEBVIEW2_KEY "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\${WEBVIEW2_CLIENT}"
70
+
71
+ Name "\${APP_NAME}"
72
+ OutFile "${nsis(input.outFile)}"
73
+ InstallDir "$LOCALAPPDATA\\Programs\\\${APP_NAME}"
74
+ BrandingText "\${APP_NAME} ${nsis(input.version)}"
75
+ VIProductVersion "${version}"
76
+ VIAddVersionKey "ProductName" "\${APP_NAME}"
77
+ VIAddVersionKey "ProductVersion" "${nsis(input.version)}"
78
+ VIAddVersionKey "FileVersion" "${version}"
79
+ VIAddVersionKey "FileDescription" "\${APP_NAME} Setup"
80
+
81
+ ${icon}!define MUI_FINISHPAGE_RUN "$INSTDIR\\\${EXE}"
82
+ !insertmacro MUI_PAGE_INSTFILES
83
+ !insertmacro MUI_PAGE_FINISH
84
+ !insertmacro MUI_UNPAGE_CONFIRM
85
+ !insertmacro MUI_UNPAGE_INSTFILES
86
+ !insertmacro MUI_LANGUAGE "English"
87
+ !insertmacro MUI_LANGUAGE "Korean"
88
+
89
+ Var AppStopped
90
+ Var RunAfter
91
+
92
+ Function RunApp
93
+ \${If} \${FileExists} "$INSTDIR\\\${EXE}"
94
+ SetOutPath "$LOCALAPPDATA"
95
+ Exec '"$INSTDIR\\\${EXE}"'
96
+ \${EndIf}
97
+ FunctionEnd
98
+
99
+ !macro Fail MESSAGE
100
+ DetailPrint "\${MESSAGE}"
101
+ \${IfNot} \${Silent}
102
+ MessageBox MB_ICONSTOP|MB_OK "\${MESSAGE}"
103
+ \${EndIf}
104
+ \${If} $AppStopped == 1
105
+ \${AndIf} $RunAfter == 1
106
+ Call RunApp
107
+ \${EndIf}
108
+ SetErrorLevel 2
109
+ Abort
110
+ !macroend
111
+
112
+ !macro RemoveEntries
113
+ ${removal(input.entries)}
114
+ !macroend
115
+
116
+ !macro RemoveApp
117
+ \${If} \${FileExists} "$INSTDIR\\\${EXE}"
118
+ RMDir /r "$INSTDIR"
119
+ \${Else}
120
+ !insertmacro RemoveEntries
121
+ \${EndIf}
122
+ !macroend
123
+
124
+ ; Windows refuses to rename a folder while a file in it is open: a stopped process's handles, and an antivirus
125
+ ; scan of files just written, close a moment later. $R2 is "done" once the rename went through.
126
+ !macro RenameRetry FROM TO
127
+ StrCpy $R2 0
128
+ \${Do}
129
+ ClearErrors
130
+ Rename "\${FROM}" "\${TO}"
131
+ \${IfNot} \${Errors}
132
+ StrCpy $R2 "done"
133
+ \${ExitDo}
134
+ \${EndIf}
135
+ IntOp $R2 $R2 + 1
136
+ Sleep 250
137
+ \${LoopWhile} $R2 < 40
138
+ !macroend
139
+
140
+ !macro RemoveSiblings SUFFIX
141
+ FindFirst $R0 $R1 "$INSTDIR.\${SUFFIX}*"
142
+ \${DoWhile} $R1 != ""
143
+ RMDir /r "$INSTDIR\\..\\$R1"
144
+ FindNext $R0 $R1
145
+ \${Loop}
146
+ FindClose $R0
147
+ !macroend
148
+
149
+ Function WebView2Version
150
+ SetRegView 64
151
+ ReadRegStr $0 HKLM "\${WEBVIEW2_KEY}" "pv"
152
+ SetRegView 32
153
+ \${If} $0 == ""
154
+ \${OrIf} $0 == "0.0.0.0"
155
+ ReadRegStr $0 HKLM "\${WEBVIEW2_KEY}" "pv"
156
+ \${EndIf}
157
+ \${If} $0 == ""
158
+ \${OrIf} $0 == "0.0.0.0"
159
+ ReadRegStr $0 HKCU "\${WEBVIEW2_KEY}" "pv"
160
+ \${EndIf}
161
+ \${If} $0 == "0.0.0.0"
162
+ StrCpy $0 ""
163
+ \${EndIf}
164
+ FunctionEnd
165
+
166
+ Function EnsureWebView2
167
+ Call WebView2Version
168
+ \${If} $0 != ""
169
+ DetailPrint "WebView2 Runtime $0"
170
+ Return
171
+ \${EndIf}
172
+ DetailPrint "Installing the WebView2 Runtime"
173
+ File "/oname=$PLUGINSDIR\\MicrosoftEdgeWebview2Setup.exe" "${nsis(input.bootstrapper)}"
174
+ ExecWait '"$PLUGINSDIR\\MicrosoftEdgeWebview2Setup.exe" /silent /install' $1
175
+ DetailPrint "WebView2 Runtime setup exited with $1"
176
+ Call WebView2Version
177
+ \${If} $0 != ""
178
+ DetailPrint "WebView2 Runtime $0"
179
+ Return
180
+ \${EndIf}
181
+ \${If} \${Silent}
182
+ !insertmacro Fail "The WebView2 Runtime could not be installed (setup exited with $1), and \${APP_NAME} does not start without it."
183
+ \${EndIf}
184
+ MessageBox MB_ICONEXCLAMATION|MB_OKCANCEL "The WebView2 Runtime could not be installed (setup exited with $1), and \${APP_NAME} does not start without it. Install \${APP_NAME} anyway?" IDOK webview2_anyway
185
+ SetErrorLevel 2
186
+ Abort
187
+ webview2_anyway:
188
+ DetailPrint "Installing without the WebView2 Runtime"
189
+ FunctionEnd
190
+
191
+ Section "Install"
192
+ \${If} \${Silent}
193
+ \${GetParameters} $0
194
+ ClearErrors
195
+ \${GetOptions} $0 "/RUN" $1
196
+ \${IfNot} \${Errors}
197
+ StrCpy $RunAfter 1
198
+ \${EndIf}
199
+ \${EndIf}
200
+ ; A setup cut off between its two renames left the app only in .setup-old.
201
+ \${DirState} "$INSTDIR" $0
202
+ \${If} $0 == -1
203
+ \${AndIf} \${FileExists} "$INSTDIR.setup-old\\\${EXE}"
204
+ Rename "$INSTDIR.setup-old" "$INSTDIR"
205
+ \${EndIf}
206
+ RMDir /r "$INSTDIR.setup-new"
207
+ \${If} \${FileExists} "$INSTDIR\\\${EXE}"
208
+ \${OrIfNot} \${FileExists} "$INSTDIR.setup-old\\\${EXE}"
209
+ RMDir /r "$INSTDIR.setup-old"
210
+ \${EndIf}
211
+ \${DirState} "$INSTDIR" $0
212
+ \${If} $0 == 1
213
+ \${AndIfNot} \${FileExists} "$INSTDIR\\\${EXE}"
214
+ \${AndIf} "$INSTDIR" != "$LOCALAPPDATA\\Programs\\\${APP_NAME}"
215
+ !insertmacro Fail "$INSTDIR holds other files. Install into an empty folder, or where \${APP_NAME} is installed."
216
+ \${EndIf}
217
+ InitPluginsDir
218
+ Call EnsureWebView2
219
+ SetOutPath "$INSTDIR.setup-new"
220
+ ClearErrors
221
+ File /r "${nsis(input.folder)}\\*"
222
+ \${If} \${Errors}
223
+ SetOutPath "$LOCALAPPDATA"
224
+ RMDir /r "$INSTDIR.setup-new"
225
+ !insertmacro Fail "Some files could not be written beside $INSTDIR. Windows refuses a path over 260 characters unless long paths are enabled."
226
+ \${EndIf}
227
+ ; SetOutPath is also the installer's working folder, which Windows does not let anyone rename.
228
+ SetOutPath "$LOCALAPPDATA"
229
+ File "/oname=$PLUGINSDIR\\stop-app.ps1" "${nsis(input.stopScript)}"
230
+ nsExec::Exec 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\\stop-app.ps1" "$INSTDIR"'
231
+ Pop $0
232
+ StrCpy $AppStopped 1
233
+ \${If} \${FileExists} "$INSTDIR\\\${EXE}"
234
+ !insertmacro RenameRetry "$INSTDIR" "$INSTDIR.setup-old"
235
+ \${If} $R2 != "done"
236
+ RMDir /r "$INSTDIR.setup-new"
237
+ !insertmacro Fail "$INSTDIR is in use and could not be replaced; \${APP_NAME} is left as it was."
238
+ \${EndIf}
239
+ !insertmacro RenameRetry "$INSTDIR.setup-new" "$INSTDIR"
240
+ \${If} $R2 != "done"
241
+ !insertmacro RenameRetry "$INSTDIR.setup-old" "$INSTDIR"
242
+ RMDir /r "$INSTDIR.setup-new"
243
+ !insertmacro Fail "The new files could not take the place of $INSTDIR; \${APP_NAME} is left as it was."
244
+ \${EndIf}
245
+ RMDir /r "$INSTDIR.setup-old"
246
+ \${Else}
247
+ !insertmacro RemoveEntries
248
+ RMDir "$INSTDIR"
249
+ \${DirState} "$INSTDIR" $0
250
+ \${If} $0 == 1
251
+ ClearErrors
252
+ ${moveIn(input.entries)}
253
+ \${If} \${Errors}
254
+ !insertmacro Fail "Some files could not be moved into $INSTDIR."
255
+ \${EndIf}
256
+ RMDir /r "$INSTDIR.setup-new"
257
+ \${Else}
258
+ !insertmacro RenameRetry "$INSTDIR.setup-new" "$INSTDIR"
259
+ \${If} $R2 != "done"
260
+ RMDir /r "$INSTDIR.setup-new"
261
+ !insertmacro Fail "$INSTDIR could not be made."
262
+ \${EndIf}
263
+ \${EndIf}
264
+ \${EndIf}
265
+ ClearErrors
266
+ WriteUninstaller "$INSTDIR.uninstall.exe"
267
+ \${If} \${Errors}
268
+ !insertmacro Fail "$INSTDIR.uninstall.exe could not be written: install into a folder under one this user may write to."
269
+ \${EndIf}
270
+ CreateShortcut "$SMPROGRAMS\\\${APP_NAME}.lnk" "$INSTDIR\\\${EXE}"
271
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayName" "\${APP_NAME}"
272
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayVersion" "${nsis(input.version)}"
273
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\\\${EXE}"
274
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
275
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "UninstallString" '"$INSTDIR.uninstall.exe"'
276
+ WriteRegStr HKCU "\${UNINSTALL_KEY}" "QuietUninstallString" '"$INSTDIR.uninstall.exe" /S'
277
+ WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "NoModify" 1
278
+ WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "NoRepair" 1
279
+ \${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
280
+ WriteRegDWORD HKCU "\${UNINSTALL_KEY}" "EstimatedSize" $0
281
+ \${If} $RunAfter == 1
282
+ Call RunApp
283
+ \${EndIf}
284
+ SectionEnd
285
+
286
+ Function un.onInit
287
+ ReadRegStr $INSTDIR HKCU "\${UNINSTALL_KEY}" "InstallLocation"
288
+ \${If} $INSTDIR == ""
289
+ \${IfNot} \${Silent}
290
+ MessageBox MB_ICONSTOP|MB_OK "\${APP_NAME} is not installed for this user."
291
+ \${EndIf}
292
+ SetErrorLevel 2
293
+ Abort
294
+ \${EndIf}
295
+ FunctionEnd
296
+
297
+ Section "Uninstall"
298
+ InitPluginsDir
299
+ File "/oname=$PLUGINSDIR\\stop-app.ps1" "${nsis(input.stopScript)}"
300
+ nsExec::Exec 'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\\stop-app.ps1" "$INSTDIR"'
301
+ Pop $0
302
+ Delete "$SMPROGRAMS\\\${APP_NAME}.lnk"
303
+ RMDir /r "$INSTDIR.previous"
304
+ !insertmacro RemoveSiblings "update-"
305
+ !insertmacro RemoveSiblings "failed-"
306
+ !insertmacro RemoveSiblings "setup-"
307
+ !insertmacro RemoveApp
308
+ RMDir "$INSTDIR"
309
+ Delete "$INSTDIR.uninstall.exe"
310
+ DeleteRegKey HKCU "\${UNINSTALL_KEY}"
311
+ DeleteRegValue HKCU "\${RUN_KEY}" "\${APP_ID}"
312
+ DeleteRegValue HKCU "\${STARTUP_APPROVED_KEY}" "\${APP_ID}"
313
+ RMDir /r "$LOCALAPPDATA\\\${APP_ID}\\akan-native-updates"
314
+ Delete "$LOCALAPPDATA\\\${APP_ID}\\akan-native-relaunch.json"
315
+ RMDir "$LOCALAPPDATA\\\${APP_ID}"
316
+ SectionEnd
317
+ `;
318
+ }
319
+
320
+ /**
321
+ * Stops every process running from the install folder, and waits (up to 10 s) until they are gone. Through
322
+ * WMI: the installer is a 32-bit program, so its PowerShell is too, and a 32-bit process cannot read a
323
+ * 64-bit one's path (Get-Process's Path comes back empty).
324
+ */
325
+ export const STOP_SCRIPT = `param([string]$Dir)
326
+ $prefix = [IO.Path]::GetFullPath($Dir).TrimEnd('\\') + '\\'
327
+ $running = { Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase) } }
328
+ & $running | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
329
+ for ($i = 0; $i -lt 50 -and (& $running); $i++) { Start-Sleep -Milliseconds 200 }
330
+ `;
331
+
332
+ /** The longest path under `folder`, relative and with Windows separators. Links are not followed. */
333
+ export function deepestPath(folder: string): string {
334
+ let deepest = "";
335
+ const walk = (dir: string, prefix: string) => {
336
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
337
+ const path = `${prefix}${entry.name}`;
338
+ if (path.length > deepest.length) deepest = path;
339
+ if (entry.isDirectory()) walk(join(dir, entry.name), `${path}\\`);
340
+ }
341
+ };
342
+ walk(folder, "");
343
+ return deepest;
344
+ }
345
+
346
+ /** How long `relative` gets on a typical PC while an update unpacks, the longest it ever is: root, app, staging, app. */
347
+ export function installedPathLength(appName: string, relative: string): number {
348
+ return INSTALL_ROOT + appName.length + STAGING + appName.length + 1 + relative.length;
349
+ }
350
+
351
+ /** makensis: AKAN_NATIVE_MAKENSIS, PATH, then where the NSIS installer puts it. */
352
+ export function findMakensis(env: Record<string, string | undefined> = process.env): string | null {
353
+ if (env.AKAN_NATIVE_MAKENSIS) return env.AKAN_NATIVE_MAKENSIS;
354
+ const onPath = Bun.which("makensis");
355
+ if (onPath) return onPath;
356
+ for (const base of [env["ProgramFiles(x86)"], env.ProgramFiles]) {
357
+ const candidate = base ? join(base, "NSIS", "makensis.exe") : null;
358
+ if (candidate && existsSync(candidate)) return candidate;
359
+ }
360
+ return null;
361
+ }
362
+
363
+ /** The Evergreen Bootstrapper, downloaded once into the akan-native cache and checked. */
364
+ async function webview2Bootstrapper(): Promise<string> {
365
+ const file = join(akanNativeHome(), "cache", "webview2", "MicrosoftEdgeWebview2Setup.exe");
366
+ if (!existsSync(file)) {
367
+ log.info(`downloading the WebView2 Evergreen Bootstrapper (${WEBVIEW2_BOOTSTRAPPER_URL})`);
368
+ const response = await fetch(WEBVIEW2_BOOTSTRAPPER_URL, { redirect: "follow" });
369
+ if (!response.ok) throw new CliError(`the WebView2 bootstrapper download answered ${response.status}`);
370
+ const bytes = new Uint8Array(await response.arrayBuffer());
371
+ if (bytes.length < 100_000 || bytes[0] !== 0x4d || bytes[1] !== 0x5a)
372
+ throw new CliError("the WebView2 bootstrapper download is not a Windows program");
373
+ mkdirSync(join(file, ".."), { recursive: true });
374
+ writeFileSync(file, bytes);
375
+ }
376
+ if (process.platform === "win32") {
377
+ const signer = await exec(
378
+ [
379
+ "powershell.exe",
380
+ "-NoProfile",
381
+ "-NonInteractive",
382
+ "-Command",
383
+ "$s = Get-AuthenticodeSignature -LiteralPath $env:AKAN_NATIVE_WEBVIEW2_SETUP; if ($s.Status -eq 'Valid') { $s.SignerCertificate.Subject }",
384
+ ],
385
+ { echo: false, env: { AKAN_NATIVE_WEBVIEW2_SETUP: file } },
386
+ );
387
+ if (!signer.stdout.includes("O=Microsoft Corporation"))
388
+ throw new CliError(`${file} is not signed by Microsoft; delete it to download it again`);
389
+ }
390
+ return file;
391
+ }
392
+
393
+ /** Writes the setup program next to the app folder and answers its path. */
394
+ export async function buildWindowsInstaller(ctx: BuildContext, folder: string): Promise<string> {
395
+ const makensis = findMakensis();
396
+ if (!makensis)
397
+ throw new ToolchainError(
398
+ "NSIS (makensis) builds the installer: winget install NSIS.NSIS on Windows (or set AKAN_NATIVE_MAKENSIS)",
399
+ );
400
+ const { config } = ctx.project;
401
+ const gen = join(ctx.outDir, "gen");
402
+ mkdirSync(gen, { recursive: true });
403
+ const stopScript = join(gen, "stop-app.ps1");
404
+ writeFileSync(stopScript, STOP_SCRIPT);
405
+ const icon = join(gen, "app.ico");
406
+ const outFile = join(ctx.outDir, `${config.app.fileName}-${config.app.version}-${hostArch()}-setup.exe`);
407
+ const script = join(gen, "installer.nsi");
408
+ const deepest = deepestPath(folder);
409
+ const longest = installedPathLength(config.app.name, deepest);
410
+ if (longest > WINDOWS_MAX_PATH - 20)
411
+ log.warn(
412
+ `${deepest} reaches about ${longest} characters under %LOCALAPPDATA%\\Programs while an update unpacks: Windows refuses paths over ${WINDOWS_MAX_PATH} unless long paths are enabled, so the installer or an update fails on such a PC. Shorten app.name or that path.`,
413
+ );
414
+ writeFileSync(
415
+ script,
416
+ `${installerScript({
417
+ name: config.app.name,
418
+ id: config.app.id,
419
+ exe: `${config.app.fileName}.exe`,
420
+ version: config.app.version,
421
+ build: config.app.build,
422
+ folder,
423
+ entries: readdirSync(folder).map((name) => ({ name, dir: statSync(join(folder, name)).isDirectory() })),
424
+ ...(existsSync(icon) ? { icon } : {}),
425
+ bootstrapper: await webview2Bootstrapper(),
426
+ stopScript,
427
+ outFile,
428
+ })}`,
429
+ );
430
+ log.step("installer: makensis");
431
+ const result = await exec([makensis, "/V2", script], { echo: false });
432
+ if (result.code !== 0 || !existsSync(outFile))
433
+ throw new CliError(`makensis failed:\n${(result.stdout + result.stderr).trim().split("\n").slice(-20).join("\n")}`);
434
+ log.info(`installer ${outFile} (${Math.round(readFileSync(outFile).length / 1024 / 1024)} MB)`);
435
+ return outFile;
436
+ }
@@ -17,6 +17,7 @@ import {
17
17
  runExecutable,
18
18
  writeDesktopResources,
19
19
  } from "./desktop.ts";
20
+ import { buildWindowsInstaller } from "./windows-installer.ts";
20
21
 
21
22
  /** Characters left for the app id under C:\Users\<name>\AppData\Local\ before WebView2's own paths reach MAX_PATH. */
22
23
  const WEBVIEW2_ID_BUDGET = 40;
@@ -70,6 +71,7 @@ export async function buildWindows(ctx: BuildContext): Promise<string> {
70
71
  const resources = resourcesOf("windows", dir);
71
72
  writeDesktopResources(ctx, resources, "windows");
72
73
  if (art) writeFileSync(join(resources, "icon.rgba"), windowIcon(art.master));
74
+ if (ctx.windows?.installer) ctx.artifacts.push({ kind: "installer", path: await buildWindowsInstaller(ctx, dir) });
73
75
  return dir;
74
76
  }
75
77
 
@@ -93,7 +93,7 @@ export function runtime(): Runtime {
93
93
  };
94
94
  if (rt.transport) w.addEventListener?.("pageshow", (e) => e.persisted && w.location?.reload());
95
95
 
96
- if (boot.dev && rt.transport && boot.platform !== "android") forwardConsole(rt);
96
+ if (boot.dev && rt.transport) forwardConsole(boot.platform);
97
97
  }
98
98
  return g.__runtime as Runtime;
99
99
  }
@@ -102,7 +102,7 @@ export function runtime(): Runtime {
102
102
  * Dev builds on native hosts: mirror console output and uncaught errors to the host,
103
103
  * which prints them next to its own logs (WV-3).
104
104
  */
105
- function forwardConsole(_rt: Runtime): void {
105
+ function forwardConsole(platform: AkanNativeBoot["platform"]): void {
106
106
 
107
107
  const g = akanNativeGlobal() as { __consolePatched?: boolean };
108
108
  if (g.__consolePatched) return;
@@ -110,13 +110,32 @@ function forwardConsole(_rt: Runtime): void {
110
110
  const c = globalThis.console as unknown as Record<string, (...args: unknown[]) => void>;
111
111
  const format = (value: unknown): string => {
112
112
  if (typeof value === "string") return value;
113
- if (value instanceof Error) return value.stack ?? `${value.name}: ${value.message}`;
113
+
114
+ if (value instanceof Error) {
115
+ const head = `${value.name}: ${value.message}`;
116
+ return value.stack?.startsWith(head) ? value.stack : `${head}${value.stack ? `\n${value.stack}` : ""}`;
117
+ }
114
118
  try {
115
119
  return JSON.stringify(value) ?? String(value);
116
120
  } catch {
117
121
  return String(value);
118
122
  }
119
123
  };
124
+
125
+ const render = (args: unknown[]): string => {
126
+ const [first, ...rest] = args;
127
+ if (typeof first !== "string" || !/%[sdifoOc]/.test(first)) return args.map(format).join(" ");
128
+ const text = first.replace(/%([sdifoOc%])/g, (match, spec: string) => {
129
+ if (spec === "%") return "%";
130
+ if (rest.length === 0) return match;
131
+ const value = rest.shift();
132
+ if (spec === "c") return "";
133
+ if (spec === "d" || spec === "i") return String(Number.parseInt(String(value), 10));
134
+ if (spec === "f") return String(Number(value));
135
+ return format(value);
136
+ });
137
+ return [text, ...rest.map(format)].join(" ");
138
+ };
120
139
  const send = (level: string, args: unknown[]) => {
121
140
  const rt = akanNativeGlobal().__runtime as Runtime | undefined;
122
141
  if (!rt?.transport || !rt.boot.dev) return;
@@ -125,10 +144,12 @@ function forwardConsole(_rt: Runtime): void {
125
144
  id: ++rt.nextId,
126
145
  plugin: CONSOLE_PLUGIN,
127
146
  method: level,
128
- args: { message: args.map(format).join(" ") },
147
+ args: { message: render(args).replace(/\n+$/, "") },
129
148
  };
130
149
  rt.transport.send(request).catch(() => {});
131
150
  };
151
+
152
+ if (platform === "android") send("attach", []);
132
153
  for (const level of ["log", "info", "warn", "error", "debug"]) {
133
154
  const original = c[level]!.bind(globalThis.console);
134
155
  c[level] = (...args: unknown[]) => {
@@ -137,7 +158,7 @@ function forwardConsole(_rt: Runtime): void {
137
158
  };
138
159
  }
139
160
  const w = globalThis as { addEventListener?: (type: string, fn: (e: any) => void) => void };
140
- w.addEventListener?.("error", (e) => send("error", [`uncaught: ${e.error?.stack ?? e.message}`]));
161
+ w.addEventListener?.("error", (e) => send("error", ["uncaught:", e.error ?? e.message]));
141
162
  w.addEventListener?.("unhandledrejection", (e) => send("error", ["unhandled rejection:", e.reason]));
142
163
  }
143
164
 
@@ -22,6 +22,7 @@ import type { Lifecycle } from "./lifecycle.ts";
22
22
  import type {
23
23
  DesktopContext,
24
24
  DesktopPlugin,
25
+ DesktopServerStatus,
25
26
  DocumentInfo,
26
27
  DocumentScope,
27
28
  EmitTarget,
@@ -40,7 +41,14 @@ export const SETUP_WAIT_MS = 5000;
40
41
 
41
42
  export interface HostServices {
42
43
  app: AppInfo;
44
+ dev?: boolean;
43
45
  appDataDir: string;
46
+ /** paths.ts appLocalDataDir; appDataDir when not given. */
47
+ appLocalDataDir?: string;
48
+ /** desktop.bin in the app's resources, or null. */
49
+ binDir?: string | null;
50
+ /** The carried server (resources/server.json), or null. */
51
+ server?: DesktopServerStatus | null;
44
52
  /** Sends an event (numbered for its document) to a window's page. */
45
53
  emit(window: number, message: BridgeEvent): void;
46
54
  /**
@@ -203,6 +211,7 @@ export function createDispatcher(
203
211
  ): Dispatcher {
204
212
  const byId = new Map(plugins.map((p) => [p.id, p]));
205
213
  let dataDirReady = false;
214
+ let localDataDirReady = false;
206
215
  const launch: Launch = { window: {} };
207
216
  let launching = true;
208
217
  const duringLaunch = (plugin: DesktopPlugin, what: string, apply: () => void) => {
@@ -215,6 +224,7 @@ export function createDispatcher(
215
224
  if (!ctx) {
216
225
  ctx = {
217
226
  app: services.app,
227
+ dev: services.dev === true,
218
228
  get appDataDir() {
219
229
  if (!dataDirReady) {
220
230
  mkdirSync(services.appDataDir, { recursive: true });
@@ -222,6 +232,16 @@ export function createDispatcher(
222
232
  }
223
233
  return services.appDataDir;
224
234
  },
235
+ get appLocalDataDir() {
236
+ const dir = services.appLocalDataDir ?? services.appDataDir;
237
+ if (!localDataDirReady) {
238
+ mkdirSync(dir, { recursive: true });
239
+ localDataDirReady = true;
240
+ }
241
+ return dir;
242
+ },
243
+ binDir: services.binDir ?? null,
244
+ server: services.server ?? null,
225
245
  emit: (event, data, target) => deliver(plugin.id, event, data, target),
226
246
  registerFile: (path, mime) => services.registerFile(path, mime),
227
247
  shell: (op, args) =>
@@ -0,0 +1,47 @@
1
+
2
+
3
+ import { createHmac, randomBytes } from "node:crypto";
4
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+
8
+ export type GrantMode = "read" | "write" | "folder";
9
+
10
+ export interface FileGrant {
11
+ path: string;
12
+ mode: GrantMode;
13
+ }
14
+
15
+ const grants = new Map<string, FileGrant>();
16
+
17
+ export function devGrantKeyFile(home = homedir()): string {
18
+ return process.env.AKAN_NATIVE_DEV_GRANT_KEY || join(home, ".akan", "native", "dev-file-grant.key");
19
+ }
20
+
21
+ export function grantFile(path: string, mode: GrantMode, { dev = false }: { dev?: boolean } = {}): string {
22
+ if (dev) {
23
+ const body = `${mode}:${Buffer.from(path).toString("base64url")}`;
24
+ return `dev:${body}:${createHmac("sha256", devGrantKey()).update(body).digest("base64url")}`;
25
+ }
26
+ const id = randomBytes(18).toString("base64url");
27
+ grants.set(id, { path, mode });
28
+ return id;
29
+ }
30
+
31
+ export function resolveGrant(id: unknown): FileGrant | null {
32
+ return typeof id === "string" ? (grants.get(id) ?? null) : null;
33
+ }
34
+
35
+ export function devGrantKey(file = devGrantKeyFile()): Buffer {
36
+ try {
37
+ return readFileSync(file);
38
+ } catch {
39
+ mkdirSync(dirname(file), { recursive: true });
40
+ try {
41
+ writeFileSync(file, randomBytes(32), { mode: 0o600, flag: "wx" });
42
+ chmodSync(file, 0o600);
43
+ } catch {
44
+ }
45
+ return readFileSync(file);
46
+ }
47
+ }