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
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { AkanNativeConfig } from "./vendor/@akanjs/native/packages/cli/src/config.ts";
1
2
  import type { AkanI18nConfig } from "akanjs/common";
2
3
  import type { AkanImageConfig } from "akanjs/server";
3
4
 
@@ -27,6 +28,41 @@ export interface LibDockerConfig {
27
28
  postRuns: DockerRun[];
28
29
  }
29
30
 
31
+ /** `${process.platform}-${process.arch}` of the computer a desktop app is built on, which is the one it runs on. */
32
+ export const binPlatforms = [
33
+ "darwin-arm64",
34
+ "darwin-x64",
35
+ "linux-arm64",
36
+ "linux-x64",
37
+ "win32-arm64",
38
+ "win32-x64",
39
+ ] as const;
40
+ export type BinPlatform = (typeof binPlatforms)[number];
41
+
42
+ /** Downloaded when the desktop app is built; the file must hash to `sha256`, so plain http is as safe as https. */
43
+ export interface AkanBinUrlSource {
44
+ url: string;
45
+ sha256: string;
46
+ /** The executable inside the archive `url` names (.zip, .tar.gz, .tgz, .tar.xz, .tar.bz2, .tar). */
47
+ file?: string;
48
+ }
49
+
50
+ /** A file on the building computer, relative to the `akan.config.ts` that declares it. */
51
+ export interface AkanBinPathSource {
52
+ path: string;
53
+ /** The executable inside the archive `path` names. */
54
+ file?: string;
55
+ }
56
+
57
+ export type AkanBinSource = AkanBinUrlSource | AkanBinPathSource;
58
+
59
+ /**
60
+ * Executables a desktop app carries, by the name its code spawns and then by platform. Their folder comes first on
61
+ * the app's PATH, so the carried server's `spawn("ffmpeg")` runs the carried file; a native plugin finds it in
62
+ * `ctx.binDir`.
63
+ */
64
+ export type AkanBinConfig = Record<string, { [platform in BinPlatform]?: AkanBinSource }>;
65
+
30
66
  export interface AkanRouteDomains {
31
67
  main?: string[];
32
68
  develop?: string[];
@@ -67,8 +103,10 @@ export type MobileEnv = "local" | "debug" | "develop" | "main";
67
103
  export type MobilePermission = "camera" | "contacts" | "location" | "push" | "speech";
68
104
 
69
105
  export interface AkanMobileTargetAssets {
70
- icon?: string;
71
- splash?: string;
106
+ /** A square PNG, relative to the app folder, or it with the color behind its transparent areas. */
107
+ icon?: string | { image: string; backgroundColor?: string };
108
+ /** A PNG shown centered at launch, or the launch screen's color, image and when it hides. */
109
+ splash?: string | NonNullable<AkanNativeConfig["splash"]>;
72
110
  }
73
111
 
74
112
  export interface AkanMobileTargetDeepLinks {
@@ -106,15 +144,72 @@ export interface AkanMobileNativeConfig {
106
144
  activity?: string[];
107
145
  /** The Firebase project's google-services.json, relative to the app folder, for FCM push on Android. */
108
146
  googleServices?: string;
147
+ /** Media plays with sound without a tap first, as it does on iOS and the desktop: a signage screen. */
148
+ autoplay?: boolean;
149
+ };
150
+ /** Android's notification channel, status bar icon (relative to the app folder) and accent color. */
151
+ push?: AkanNativeConfig["push"];
152
+ /** The app's part of the iOS privacy manifest (PrivacyInfo.xcprivacy), which an App Store upload requires. */
153
+ privacy?: AkanNativeConfig["privacy"];
154
+ desktop?: {
155
+ /**
156
+ * The desktop app carries the app's server (API only, database mode single, on loopback) and its pages call
157
+ * nothing else. `build-desktop`, `start-desktop` and `publish-update` all read it, and an installed app refuses
158
+ * a release that carries a server when it has none, or none when it has one.
159
+ */
160
+ server?: boolean;
161
+ /**
162
+ * `"reload"`: a window whose page's process ends (a crash, a hang) loads it again every time, waiting
163
+ * longer after each end in a row, and the app relaunches when the webview's browser process ends — for an
164
+ * app nobody attends. `"errorPage"` (default): one reload, then an error page, and a quit for the browser.
165
+ */
166
+ recovery?: "errorPage" | "reload";
167
+ /** The main window from its first frame: borderless fullscreen, and no taskbar button (Windows, Linux). */
168
+ window?: { fullscreen?: boolean; skipTaskbar?: boolean };
169
+ /**
170
+ * Windows: `"auto"` answers getDisplayMedia() with the first screen at once, no picker or gesture — remote
171
+ * support on an unattended screen; it covers every media request, so not for an app that asks for a camera.
172
+ */
173
+ screenCapture?: "picker" | "auto";
109
174
  };
110
175
  }
111
176
 
177
+ /**
178
+ * Where an installed app looks for newer releases of itself (the native runtime's updates plugin): the whole app on a
179
+ * desktop, the web bundle on a phone. `akan publish-update` writes and signs a release with a key on this machine;
180
+ * `akan pack-update` writes an unsigned phone bundle for whoever holds the key to sign.
181
+ */
182
+ export interface AkanMobileUpdatesConfig {
183
+ /**
184
+ * A static base URL, https in a release build: `<url>/<os>-<arch>/<channel>.json` (desktop) or `<url>/<platform>/…`
185
+ * (a phone's web bundle).
186
+ */
187
+ url: string;
188
+ /** The update key's public half, the raw 32-byte Ed25519 key in base64 (`akan update-keygen` prints it). */
189
+ publicKey: string;
190
+ /**
191
+ * The manifest the app follows, e.g. a "pilot" target's "pilot". Default: the backend env the app is built for
192
+ * (`main`, `develop`, `debug`, `local`).
193
+ */
194
+ channel?: string;
195
+ /** How long a newly applied release has to call notifyReady() before it is rolled back, ms. Default 10000. */
196
+ readyTimeout?: number;
197
+ }
198
+
199
+ /**
200
+ * One bundle id, or one per platform, for an app whose store listings already carry different ids. A platform without
201
+ * its own takes `default`.
202
+ */
203
+ export type AkanMobileAppId =
204
+ | string
205
+ | { default?: string; ios?: string; android?: string; macos?: string; windows?: string; linux?: string };
206
+
112
207
  export interface AkanMobileTargetConfig {
113
208
  name: string;
114
209
  basePath?: string;
115
210
  indexPath?: string;
116
211
  appName: string;
117
- appId: string;
212
+ appId: AkanMobileAppId;
118
213
  /** Executables, archives and the Swift module; letters, digits, `.`, `_` and `-`. Default: the app's folder name. */
119
214
  fileName?: string;
120
215
  version: string;
@@ -124,16 +219,19 @@ export interface AkanMobileTargetConfig {
124
219
  deepLinks?: AkanMobileTargetDeepLinks;
125
220
  files?: AkanMobileTargetFiles;
126
221
  native?: AkanMobileNativeConfig;
222
+ /** Over mobile.updates, field by field: a pilot target names its own channel. */
223
+ updates?: Partial<AkanMobileUpdatesConfig>;
127
224
  }
128
225
 
129
226
  export interface AkanMobileConfig {
130
227
  appName: string;
131
- appId: string;
228
+ appId: AkanMobileAppId;
132
229
  fileName?: string;
133
230
  version: string;
134
231
  buildNum: number;
135
232
  files?: AkanMobileTargetFiles;
136
233
  native?: AkanMobileNativeConfig;
234
+ updates?: AkanMobileUpdatesConfig;
137
235
  targets: Record<string, AkanMobileTargetConfig>;
138
236
  }
139
237
 
@@ -228,6 +326,10 @@ export interface AppConfigResult {
228
326
  */
229
327
  syncPageLibs?: string[] | boolean;
230
328
  externalLibs: string[];
329
+ /** Dependencies whose install scripts `bun install --production` runs, in the image and in a desktop app's server. */
330
+ trustedDependencies: string[];
331
+ /** Every desktop build, run and dev session carries these, server or not; the image installs through `docker`. */
332
+ bin: AkanBinConfig;
231
333
  barrelImports: string[];
232
334
  optimizeImports: string[];
233
335
  images: AkanImageConfig;
@@ -240,6 +342,9 @@ export interface AppConfigResult {
240
342
 
241
343
  export interface LibConfigResult {
242
344
  externalLibs: string[];
345
+ trustedDependencies: string[];
346
+ /** Carried by the desktop app of every app that depends on this lib; an app's own entry of the same name wins. */
347
+ bin: AkanBinConfig;
243
348
  /** Image steps every app that mounts this lib inherits, unless that app declares a whole Dockerfile. */
244
349
  docker: LibDockerConfig;
245
350
  /** Which of this lib's own public fonts every app that mounts it must keep. */
@@ -260,12 +365,16 @@ export interface LibConfigContext {
260
365
  readonly type: "lib";
261
366
  }
262
367
 
263
- export type AppConfigInput = Omit<DeepPartial<AppConfigResult>, "docker" | "web"> & {
368
+ export type AppConfigInput = Omit<DeepPartial<AppConfigResult>, "docker" | "web" | "bin"> & {
264
369
  docker?: DockerOption;
265
370
  web?: AkanWebOption;
371
+ bin?: AkanBinConfig;
372
+ plugins?: AkanPlugin[];
373
+ };
374
+ export type LibConfigInput = Omit<DeepPartial<LibConfigResult>, "bin"> & {
375
+ bin?: AkanBinConfig;
266
376
  plugins?: AkanPlugin[];
267
377
  };
268
- export type LibConfigInput = DeepPartial<LibConfigResult> & { plugins?: AkanPlugin[] };
269
378
  export interface SubspaceDeclaration {
270
379
  /** Short name used on the command line and as the git remote suffix. */
271
380
  name: string;
@@ -0,0 +1,5 @@
1
+
2
+
3
+ export { AkanNativeError, mimeFor } from "../vendor/@akanjs/native/packages/core/src/index.ts";
4
+ export type { DesktopContext, DesktopPlugin, NativeEvent } from "../vendor/@akanjs/native/packages/desktop/src/plugin.ts";
5
+ export { createPageVeto, defineDesktopPlugin, externalOpenAllowed } from "../vendor/@akanjs/native/packages/desktop/src/plugin.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-beta.26",
3
+ "version": "3.0.0-beta.28",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -95,6 +95,16 @@
95
95
  "import": "./client/native.ts",
96
96
  "default": "./client/native.ts"
97
97
  },
98
+ "./client/native/*": {
99
+ "types": "./types/client/native/*.d.ts",
100
+ "import": "./client/native/*.ts",
101
+ "default": "./client/native/*.ts"
102
+ },
103
+ "./native/desktop": {
104
+ "types": "./types/native/desktop.d.ts",
105
+ "import": "./native/desktop.ts",
106
+ "default": "./native/desktop.ts"
107
+ },
98
108
  "./webkit": {
99
109
  "types": "./types/webkit/index.d.ts",
100
110
  "import": "./webkit/index.ts",
package/server/akanApp.ts CHANGED
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
  import { Logger } from "akanjs/common";
5
5
  import type { AkanChildRole, AkanChildStatus, AkanIpcMessage, AkanMetricsReport, AkanUpstream } from "akanjs/service";
6
6
  import { getApiPrefix, getWsPrefix, normalizeRoutePrefix, resetEnvCache } from "../base/baseEnv";
7
+ import { CrossSiteGuard } from "../signal/CrossSiteGuard";
7
8
  import { isTraceEnabled } from "../signal/trace";
8
- import { makeAkanChildProxyHeaders } from "./akanAppHeaders";
9
+ import { AKAN_CHILD_HOST, makeAkanChildProxyHeaders } from "./akanAppHeaders";
9
10
  import type { BuilderCsrReq, BuilderCsrRes, BuilderMessage, BuilderReq, BuilderRes } from "./artifact";
10
11
  import { compressResponse, encodedFileResponse } from "./contentEncoding";
11
12
  import { isPortInUseError } from "./lifecycle/portInUse";
@@ -19,6 +20,8 @@ import { RotatingLogWriter } from "./logging/rotatingLogWriter";
19
20
  import { AppInfo } from "./ops/appInfo";
20
21
  import type { OpsRoute } from "./ops/opsRoute";
21
22
  import { ProcessMetricsCollector } from "./processMetricsCollector";
23
+ import { HostAllowlist } from "./routing/hostAllowlist";
24
+ import { SelfExec } from "./selfExec";
22
25
  import { resolveStaticPath } from "./staticPath";
23
26
  import { getWebConfigFromEnv } from "./types";
24
27
 
@@ -139,6 +142,7 @@ export class AkanApp {
139
142
  #logControl: LogControlSocket | null = null;
140
143
  #logStream: LogStreamRoute | null = null;
141
144
  #ops: OpsRoute | null = null;
145
+ #hostAllowlist = HostAllowlist.fromEnv();
142
146
  static readonly #ansiPattern = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
143
147
  #gatewayMetrics: AkanMetricsReport = {};
144
148
  #proxyHopCount = 0;
@@ -149,6 +153,7 @@ export class AkanApp {
149
153
  #stopping = false;
150
154
 
151
155
  constructor(serverPathOrOptions: string | AkanAppOptions = "./server", options: AkanAppOptions = {}) {
156
+ SelfExec.adopt();
152
157
  const resolvedOptions = typeof serverPathOrOptions === "string" ? options : serverPathOrOptions;
153
158
  const serverPath = typeof serverPathOrOptions === "string" ? serverPathOrOptions : "./server";
154
159
  this.#serverPath = AkanApp.#resolveServerPath(resolvedOptions.serverPath ?? serverPath);
@@ -235,6 +240,10 @@ export class AkanApp {
235
240
  process.exit(await OpsCommand.run(process.argv.slice(3)));
236
241
  }
237
242
  if (this.#solo) return await this.#startSolo();
243
+ if (SelfExec.carried)
244
+ throw new Error(
245
+ "A desktop app's server runs in one process, and this main.ts asks for replicas (replica, solo: false or AKAN_SOLO=false), which would start with the `bun` on the user's PATH. Leave them out of the app the desktop build carries.",
246
+ );
238
247
  Logger.role = "gateway";
239
248
  await this.#prepareRuntimeDir();
240
249
  await this.#startLogHub();
@@ -579,6 +588,7 @@ export class AkanApp {
579
588
  this.#server = Bun.serve({
580
589
  idleTimeout: 0,
581
590
  port: this.#port,
591
+ hostname: process.env.AKAN_LISTEN_HOST || undefined,
582
592
  fetch: (req, server) => this.#handleFetch(req, server),
583
593
  websocket: {
584
594
  idleTimeout: 0,
@@ -595,6 +605,7 @@ export class AkanApp {
595
605
  }
596
606
 
597
607
  async #handleFetch(req: Request, server: Bun.Server<GatewayWsData>): Promise<Response | undefined> {
608
+ if (this.#hostAllowlist && !this.#hostAllowlist.allows(req)) return this.#hostAllowlist.refuse();
598
609
  const url = new URL(req.url);
599
610
  if (url.pathname === "/_akan/app/health") return Response.json(this.#getHealthStatus());
600
611
  if (url.pathname === "/_akan/app/metrics") return Response.json(this.#getMetricsStatus());
@@ -638,6 +649,12 @@ export class AkanApp {
638
649
  const upstream = child?.upstream ? (child.wsUpstream ?? this.#getChildUpstream(child.idx, child.role).ws) : null;
639
650
  if (!child || !upstream) return new Response("No websocket upstream is ready", { status: 503 });
640
651
  const url = new URL(req.url);
652
+
653
+ try {
654
+ CrossSiteGuard.assertOrigin(req, url, url.pathname === "/_akan/hmr" ? "hmr" : "websocket");
655
+ } catch {
656
+ return new Response("Forbidden", { status: 403 });
657
+ }
641
658
  const upstreamWs = new WebSocket(`ws://${upstream.host}:${upstream.port}${url.pathname}${url.search}`, {
642
659
  headers: makeAkanChildProxyHeaders(req, child.idx, server.requestIP(req)),
643
660
  } as unknown as string[]);
@@ -947,7 +964,8 @@ export class AkanApp {
947
964
  if (!location) return;
948
965
  try {
949
966
  const parsed = new URL(location);
950
- if (parsed.hostname === "akan-child") headers.set("location", `${parsed.pathname}${parsed.search}${parsed.hash}`);
967
+ if (parsed.hostname === AKAN_CHILD_HOST)
968
+ headers.set("location", `${parsed.pathname}${parsed.search}${parsed.hash}`);
951
969
  } catch {
952
970
  }
953
971
  }
@@ -1074,6 +1092,7 @@ export class AkanApp {
1074
1092
  child.upstream = message.upstream;
1075
1093
  child.wsUpstream = message.wsUpstream;
1076
1094
  child.healthPath = message.healthPath;
1095
+ if (message.crossSite) CrossSiteGuard.configure(message.crossSite);
1077
1096
  child.lastPongAtMono = performance.now();
1078
1097
  child.restartAttempts = 0;
1079
1098
 
@@ -1084,7 +1103,8 @@ export class AkanApp {
1084
1103
  this.#federationChildCache = null;
1085
1104
 
1086
1105
  const trafficChildren = [...this.#children.values()].filter((item) => item.role !== "batch");
1087
- if (child.role !== "batch" && trafficChildren.every((item) => item.ready)) {
1106
+
1107
+ if (!this.#stopping && child.role !== "batch" && trafficChildren.every((item) => item.ready)) {
1088
1108
  process.send?.({ type: "backend-ready", pid: process.pid } satisfies AkanIpcMessage);
1089
1109
  }
1090
1110
  if ([...this.#children.values()].every((item) => item.ready)) {
@@ -11,6 +11,9 @@ const HOP_BY_HOP_HEADERS = new Set([
11
11
  "upgrade",
12
12
  ]);
13
13
 
14
+ /** The Host every request the gateway hands a replica carries; a browser never sends it to a replica's port. */
15
+ export const AKAN_CHILD_HOST = "akan-child";
16
+
14
17
  export interface ProxyClientPeer {
15
18
  address: string;
16
19
  port: number;
@@ -44,6 +47,6 @@ export function makeAkanChildProxyHeaders(req: Request, childIdx: number, peer?:
44
47
  if (!headers.has("x-request-id") && process.env.AKAN_BENCH_SKIP_REQUEST_ID !== "1") {
45
48
  headers.set("x-request-id", crypto.randomUUID());
46
49
  }
47
- headers.set("host", "akan-child");
50
+ headers.set("host", AKAN_CHILD_HOST);
48
51
  return headers;
49
52
  }
@@ -19,11 +19,13 @@ import { AgentRelayAccess } from "../signal/guards";
19
19
  import { createOpenApiDocument } from "../signal/openapi";
20
20
  import { FetchSerializer } from "../signal/serializer";
21
21
  import { SignalContext } from "../signal/signalContext";
22
+ import { AKAN_CHILD_HOST } from "./akanAppHeaders";
22
23
  import type { AkanLib, AkanLibProps } from "./akanLib";
23
24
  import { BinaryPubsub } from "./binaryPubsub";
24
25
  import { DevtoolsRouter } from "./devtools";
25
26
  import { DiLifecycle } from "./di/diLifecycle";
26
27
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
28
+ import { OrphanGroup } from "./lifecycle/orphanGroup";
27
29
  import { isPortInUseError } from "./lifecycle/portInUse";
28
30
  import { resolveRuntimeDir } from "./lifecycle/runtimeDir";
29
31
  import { ShutdownManager } from "./lifecycle/shutdownManager";
@@ -42,6 +44,7 @@ import { WebProxyRunner } from "./proxy";
42
44
  import { SignalResolver } from "./resolver";
43
45
  import { type ApiRouteInputs, ApiRouter } from "./routing/apiRouter";
44
46
  import type { AppWsData } from "./routing/appWsData";
47
+ import { HostAllowlist } from "./routing/hostAllowlist";
45
48
  import { createSoloAppRoutes } from "./routing/soloAppRoutes";
46
49
  import {
47
50
  getWebConfigFromEnv,
@@ -167,6 +170,7 @@ export class AkanServer {
167
170
  #ops: OpsRoute | null | undefined;
168
171
  #lastMetrics: AkanMetricsReport = {};
169
172
  #stopping: Promise<void> | null = null;
173
+ #orphanExiting = false;
170
174
  constructor(
171
175
  name = "AkanServer",
172
176
  env: BackendEnv = {},
@@ -392,7 +396,10 @@ export class AkanServer {
392
396
  }),
393
397
  data: {},
394
398
  } as Bun.WebSocketHandler<AppWsData | HmrWsData>;
395
- const buildRoutes = (upgradeAppWs: ApiRouteInputs["upgradeAppWs"]) =>
399
+
400
+ const hostAllowlist = unix ? null : HostAllowlist.fromEnv();
401
+ const hostname = process.env.AKAN_LISTEN_HOST || undefined;
402
+ const buildRoutes = (upgradeAppWs: ApiRouteInputs["upgradeAppWs"], allowlist: HostAllowlist | null) =>
396
403
  ApiRouter.buildRoutes({
397
404
  prefix: this.prefix,
398
405
  websocketPrefix: this.websocketPrefix,
@@ -402,19 +409,25 @@ export class AkanServer {
402
409
  renderEnvRoutes,
403
410
  upgradeAppWs,
404
411
  webProxyRunner,
412
+ hostAllowlist: allowlist,
405
413
  });
406
414
  this.#server = Bun.serve({
407
415
  idleTimeout: 0,
408
- ...(unix ? { unix } : { port }),
409
- routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false),
416
+ ...(unix ? { unix } : { port, hostname }),
417
+ routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false, hostAllowlist),
410
418
  websocket: websocketHandlers,
411
419
  } as Parameters<typeof Bun.serve>[0]);
412
420
  if (unix && process.env.AKAN_CHILD_WS_PORT) {
413
421
  const preferredWsPort = Number(process.env.AKAN_CHILD_WS_PORT);
422
+
414
423
  const wsServeOptions = (port: number) => ({
415
424
  idleTimeout: 0,
416
425
  port,
417
- routes: buildRoutes((req, data) => this.#wsServer?.upgrade(req, { data }) ?? false),
426
+ hostname: "127.0.0.1",
427
+ routes: buildRoutes(
428
+ (req, data) => this.#wsServer?.upgrade(req, { data }) ?? false,
429
+ new HostAllowlist([AKAN_CHILD_HOST]),
430
+ ),
418
431
  websocket: websocketHandlers,
419
432
  });
420
433
  try {
@@ -467,6 +480,7 @@ export class AkanServer {
467
480
  upstream: unix ? { type: "unix", socketPath: unix } : { type: "tcp", host: "127.0.0.1", port: Number(port) },
468
481
  wsUpstream: typeof wsPort === "number" ? { type: "tcp", host: "127.0.0.1", port: wsPort } : undefined,
469
482
  healthPath: "/_akan/app/child-health",
483
+ crossSite: CrossSiteGuard.option(),
470
484
  } satisfies AkanIpcMessage);
471
485
  await this.#di.runSchedulerInit();
472
486
  ShutdownManager.register(this.logger, () => this.stop());
@@ -550,10 +564,16 @@ export class AkanServer {
550
564
 
551
565
  #handleParentDisconnect() {
552
566
  this.logger.warn("Parent IPC channel closed; shutting down to avoid an orphaned replica");
553
- setTimeout(() => process.exit(1), this.shutdownTimeoutMs + 1_000);
567
+ setTimeout(() => this.#exitOrphaned(1), this.shutdownTimeoutMs + 1_000);
554
568
  void this.stop()
555
- .then(() => process.exit(0))
556
- .catch(() => process.exit(1));
569
+ .then(() => this.#exitOrphaned(0))
570
+ .catch(() => this.#exitOrphaned(1));
571
+ }
572
+
573
+ #exitOrphaned(code: number) {
574
+ if (this.#orphanExiting) return;
575
+ this.#orphanExiting = true;
576
+ OrphanGroup.exit(code);
557
577
  }
558
578
 
559
579
  #handleIpcMessage(message: AkanIpcMessage) {
package/server/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./console";
7
7
  export * from "./decorators";
8
8
  export * from "./devtools";
9
9
  export type { ChangeBatch, ChangeKind } from "./hmr/wsHub";
10
+ export * from "./nativeFile";
10
11
  export * from "./oauth";
11
12
  export * from "./processMetricsCollector";
12
13
  export * from "./proxy";
@@ -0,0 +1,21 @@
1
+ import { SelfExec } from "../selfExec";
2
+
3
+ export class OrphanGroup {
4
+ static exit(code: number, { leads = SelfExec.carried && process.platform !== "win32", graceMs = 1_000 } = {}) {
5
+ if (!leads) process.exit(code);
6
+ process.removeAllListeners("SIGTERM");
7
+ process.on("SIGTERM", () => undefined);
8
+ OrphanGroup.#signal("SIGTERM");
9
+ setTimeout(() => {
10
+ OrphanGroup.#signal("SIGKILL");
11
+ process.exit(code);
12
+ }, graceMs);
13
+ }
14
+
15
+ static #signal(signal: NodeJS.Signals) {
16
+ try {
17
+ process.kill(-process.pid, signal);
18
+ } catch {
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,107 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { getEnv } from "akanjs/base";
6
+ import type { AkanIpcMessage } from "akanjs/service";
7
+
8
+ export type NativeFileMode = "read" | "write" | "folder";
9
+
10
+ type Resolved = Extract<AkanIpcMessage, { type: "file.resolved" }>;
11
+
12
+ export class NativeFile {
13
+ static timeoutMs = 5_000;
14
+ static operationMode: () => string = () => getEnv().operationMode;
15
+ static readonly #waiting = new Map<string, (answer: Resolved) => void>();
16
+ static #listener: ((message: unknown) => void) | null = null;
17
+
18
+ /** The path behind `grant`, which the user must have given for `mode`. */
19
+ static async resolve(grant: string, mode: NativeFileMode): Promise<string> {
20
+ const found = grant.startsWith("dev:") ? NativeFile.#devGrant(grant) : await NativeFile.#ask(grant);
21
+ if (found.mode !== mode) throw new Error(`The file was granted to ${found.mode}, not to ${mode}.`);
22
+ return found.path;
23
+ }
24
+
25
+ /** A path inside a folder grant; one that climbs out of the folder is refused. */
26
+ static async resolveIn(grant: string, relative: string): Promise<string> {
27
+ const folder = await NativeFile.resolve(grant, "folder");
28
+ const target = path.resolve(folder, relative);
29
+ NativeFile.#assertInside(folder, target, relative);
30
+
31
+ NativeFile.#assertInside(NativeFile.#realPath(folder), NativeFile.#realPath(target), relative);
32
+ return target;
33
+ }
34
+
35
+ static #assertInside(folder: string, target: string, relative: string) {
36
+ const inside = path.relative(folder, target);
37
+ if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside))
38
+ throw new Error(`${relative} is outside the folder the user granted.`);
39
+ }
40
+
41
+ static #realPath(target: string): string {
42
+ const rest: string[] = [];
43
+ for (let current = target; ; current = path.dirname(current)) {
44
+ try {
45
+ return path.join(realpathSync.native(current), ...rest);
46
+ } catch (error) {
47
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" || path.dirname(current) === current) throw error;
48
+ if (NativeFile.#exists(current)) throw new Error(`${target} is a link to nothing.`);
49
+ rest.unshift(path.basename(current));
50
+ }
51
+ }
52
+ }
53
+
54
+ static #exists(entry: string) {
55
+ try {
56
+ lstatSync(entry);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ static #ask(grant: string): Promise<{ path: string; mode: NativeFileMode }> {
64
+ const send = process.send?.bind(process);
65
+ if (!send) return Promise.reject(new Error("A file grant resolves only inside the desktop app that gave it."));
66
+ NativeFile.#listen();
67
+ const id = crypto.randomUUID();
68
+ return new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ NativeFile.#waiting.delete(id);
71
+ reject(new Error("The desktop app did not answer for the file grant."));
72
+ }, NativeFile.timeoutMs);
73
+ NativeFile.#waiting.set(id, (answer) => {
74
+ clearTimeout(timer);
75
+ NativeFile.#waiting.delete(id);
76
+ if (answer.path && answer.mode) resolve({ path: answer.path, mode: answer.mode });
77
+ else reject(new Error(answer.error ?? "The desktop app refused the file grant."));
78
+ });
79
+ send({ type: "file.resolve", id, grant } satisfies AkanIpcMessage);
80
+ });
81
+ }
82
+
83
+ static #listen() {
84
+ if (NativeFile.#listener) return;
85
+ NativeFile.#listener = (message) => {
86
+ const answer = message as AkanIpcMessage | null;
87
+ if (answer?.type === "file.resolved") NativeFile.#waiting.get(answer.id)?.(answer);
88
+ };
89
+ process.on("message", NativeFile.#listener);
90
+ }
91
+
92
+ static #devGrant(grant: string): { path: string; mode: NativeFileMode } {
93
+
94
+ if (NativeFile.operationMode() !== "local")
95
+ throw new Error("A dev build's file grant resolves only on akan start, in operationMode local.");
96
+ const [, mode, encoded = "", signature = ""] = grant.split(":");
97
+ if (mode !== "read" && mode !== "write" && mode !== "folder") throw new Error("The file grant is malformed.");
98
+ const key = readFileSync(
99
+ process.env.AKAN_NATIVE_DEV_GRANT_KEY || path.join(homedir(), ".akan", "native", "dev-file-grant.key"),
100
+ );
101
+ const expected = createHmac("sha256", key).update(`${mode}:${encoded}`).digest();
102
+ const given = Buffer.from(signature, "base64url");
103
+ if (given.length !== expected.length || !timingSafeEqual(given, expected))
104
+ throw new Error("The file grant was not signed by this computer's dev build.");
105
+ return { path: Buffer.from(encoded, "base64url").toString(), mode };
106
+ }
107
+ }
@@ -1,5 +1,6 @@
1
1
  import { Logger } from "akanjs/common";
2
2
  import { getEnv } from "../../base/baseEnv";
3
+ import { SelfExec } from "../selfExec";
3
4
  import type { AppDetailInfo } from "./appInfo";
4
5
  import { OpsTokenVerifier } from "./opsToken";
5
6
  import { type SnapshotJobRequest, SnapshotJobs } from "./snapshotJobs";
@@ -65,8 +66,8 @@ export class OpsRoute {
65
66
  const args = ["ops", "snapshot", "--id", request.id, "--out", dir, "--db", sources.main, "--json"];
66
67
  if (sources.solid) args.push("--solid-db", sources.solid);
67
68
  if (request.includeSolid) args.push("--include-solid");
68
- const proc = Bun.spawn([process.execPath, Bun.main, ...args], {
69
- env: process.env,
69
+ const proc = Bun.spawn(SelfExec.command(Bun.main, ...args), {
70
+ env: SelfExec.env(),
70
71
  stdout: "pipe",
71
72
  stderr: "pipe",
72
73
  });