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
@@ -7,6 +7,7 @@ import type { AkanNativeConfig } from "../config.ts";
7
7
  import { manifestPermissionProblems, type PermissionSet, resolveAcl } from "./acl.ts";
8
8
  import { unknownConfigKeys } from "./configkeys.ts";
9
9
  import { validateCsp, validateExternalSchemes } from "./csp.ts";
10
+ import { type DesktopServerConfig, validateDesktopServer } from "./desktop-server.ts";
10
11
  import { type EnvConfig, validateEnvConfig } from "./env.ts";
11
12
  import { CliError } from "./log.ts";
12
13
  import { type PermissionsConfig, validatePermissions } from "./permissions.ts";
@@ -26,7 +27,15 @@ export interface ResolvedConfig extends Omit<AkanNativeConfig, "icon" | "splash"
26
27
  usageDescriptions: Record<string, string>;
27
28
  permissions: PermissionsConfig;
28
29
  deepLinks: { schemes: string[]; domains: { host: string; pathPrefixes: string[] }[] };
29
- desktop: { quitOnLastWindowClosed: boolean };
30
+ desktop: {
31
+ quitOnLastWindowClosed: boolean;
32
+ recovery: "errorPage" | "reload";
33
+ window: { fullscreen: boolean; skipTaskbar: boolean };
34
+ screenCapture: "picker" | "auto";
35
+ server?: DesktopServerConfig;
36
+ /** Absolute. */
37
+ bin?: string;
38
+ };
30
39
  updates: UpdatesConfig | null;
31
40
  /** Absolute image path. */
32
41
  icon: { image: string; backgroundColor?: string } | null;
@@ -186,6 +195,23 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
186
195
  if (raw.desktop?.quitOnLastWindowClosed !== undefined && typeof raw.desktop.quitOnLastWindowClosed !== "boolean") {
187
196
  problems.push("desktop.quitOnLastWindowClosed must be a boolean");
188
197
  }
198
+ const recovery = raw.desktop?.recovery;
199
+ if (recovery !== undefined && recovery !== "errorPage" && recovery !== "reload")
200
+ problems.push(`desktop.recovery must be "errorPage" or "reload" (got ${JSON.stringify(recovery)})`);
201
+ for (const key of ["fullscreen", "skipTaskbar"] as const) {
202
+ const value = raw.desktop?.window?.[key];
203
+ if (value !== undefined && typeof value !== "boolean") problems.push(`desktop.window.${key} must be a boolean`);
204
+ }
205
+ const screenCapture = raw.desktop?.screenCapture;
206
+ if (screenCapture !== undefined && screenCapture !== "picker" && screenCapture !== "auto")
207
+ problems.push(`desktop.screenCapture must be "picker" or "auto" (got ${JSON.stringify(screenCapture)})`);
208
+ if (raw.android?.autoplay !== undefined && typeof raw.android.autoplay !== "boolean")
209
+ problems.push("android.autoplay must be a boolean");
210
+ const bin = raw.desktop?.bin;
211
+ const binDir = typeof bin === "string" && bin ? resolve(appDir, bin) : null;
212
+ if (bin !== undefined && !(binDir && existsSync(binDir) && statSync(binDir).isDirectory()))
213
+ problems.push(`desktop.bin must be a folder (got ${JSON.stringify(bin)})`);
214
+ const desktopServer = validateDesktopServer(raw.desktop, appDir, problems);
189
215
  if (raw.keyboard?.resize !== undefined && raw.keyboard.resize !== "resize" && raw.keyboard.resize !== "none") {
190
216
  problems.push(`keyboard.resize must be "resize" or "none" (got ${JSON.stringify(raw.keyboard.resize)})`);
191
217
  }
@@ -245,7 +271,17 @@ export function projectFromConfig(raw: AkanNativeConfig, appDir: string, options
245
271
  typeof d === "string" ? { host: d, pathPrefixes: [] } : { host: d.host, pathPrefixes: d.pathPrefixes ?? [] },
246
272
  ),
247
273
  },
248
- desktop: { quitOnLastWindowClosed: raw.desktop?.quitOnLastWindowClosed ?? true },
274
+ desktop: {
275
+ quitOnLastWindowClosed: raw.desktop?.quitOnLastWindowClosed ?? true,
276
+ recovery: raw.desktop?.recovery === "reload" ? "reload" : "errorPage",
277
+ screenCapture: raw.desktop?.screenCapture === "auto" ? "auto" : "picker",
278
+ window: {
279
+ fullscreen: raw.desktop?.window?.fullscreen === true,
280
+ skipTaskbar: raw.desktop?.window?.skipTaskbar === true,
281
+ },
282
+ ...(desktopServer ? { server: desktopServer } : {}),
283
+ ...(binDir ? { bin: binDir } : {}),
284
+ },
249
285
  updates,
250
286
  icon: icon ? { ...icon, image: resolve(appDir, icon.image) } : null,
251
287
  splash: {
@@ -0,0 +1,104 @@
1
+
2
+
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { publishAppUpdate } from "../platforms/desktop-update.ts";
6
+ import { BUNDLE_FILE, readBundleInfo } from "./compat.ts";
7
+ import { CliError, log } from "./log.ts";
8
+ import type { BuildContext } from "./prepare.ts";
9
+ import {
10
+ assertChannel,
11
+ hostArch,
12
+ signingKey,
13
+ signManifest,
14
+ type UpdateManifest,
15
+ updateKeyPath,
16
+ webManifest,
17
+ writeWebBundle,
18
+ } from "./updates.ts";
19
+
20
+ export type ReleasePlatform = "macos" | "windows" | "linux" | "ios" | "android";
21
+
22
+ export interface PublishedRelease {
23
+ /** `<out>/<os>-<arch>` for a desktop app, `<out>/<platform>` for a phone's web bundle. */
24
+ dir: string;
25
+ manifest: UpdateManifest;
26
+ }
27
+
28
+ /**
29
+ * The publish time in seconds, and past the release `<channel>.json` in `dir` already names: apps take only a larger
30
+ * sequence, so a clock behind the last publisher's would make this release look older to every one of them.
31
+ */
32
+ export function nextSequence(dir: string, channel: string, now = Math.floor(Date.now() / 1000)): number {
33
+ const path = join(dir, `${channel}.json`);
34
+ if (!existsSync(path)) return now;
35
+ const previous = (JSON.parse(readFileSync(path, "utf8")) as Partial<UpdateManifest>).sequence;
36
+ if (typeof previous !== "number" || previous < now) return now;
37
+ if (previous > now)
38
+ log.warn(
39
+ `the ${channel} release already here has sequence ${previous}, later than this computer's clock (${now}): publishing as ${previous + 1}. Check the clock of the computer that publishes.`,
40
+ );
41
+ return previous + 1;
42
+ }
43
+
44
+ /** Refuses before the build a release that could not be signed: no key on this machine, or another app's. */
45
+ export function assertSigningKey(config: { app: { id: string }; updates?: { publicKey: string } | null }): void {
46
+ if (!config.updates)
47
+ throw new CliError("akan-native.config.ts has no updates: { url, publicKey } (run `akan-native update keygen`)");
48
+ signingKey(updateKeyPath(config.app.id), config.updates.publicKey);
49
+ }
50
+
51
+ /**
52
+ * `<channel>.json` and its `.sig` land together, the signature first: an app that reads a manifest beside another
53
+ * one's signature refuses every release until the next publish.
54
+ */
55
+ function writeSigned(dir: string, channel: string, bytes: Uint8Array, signature: string): void {
56
+ const json = join(dir, `${channel}.json`);
57
+ writeFileSync(`${json}.tmp`, bytes);
58
+ writeFileSync(`${json}.sig.tmp`, signature);
59
+ renameSync(`${json}.sig.tmp`, `${json}.sig`);
60
+ renameSync(`${json}.tmp`, json);
61
+ }
62
+
63
+ /** Writes the release of `artifact` (the app a release build made) and its signed `<channel>.json` under `out`. */
64
+ export async function publishRelease(
65
+ ctx: BuildContext,
66
+ platform: ReleasePlatform,
67
+ artifact: string,
68
+ out: string,
69
+ channel: string,
70
+ ): Promise<PublishedRelease> {
71
+ const { config } = ctx.project;
72
+ if (!config.updates)
73
+ throw new CliError("akan-native.config.ts has no updates: { url, publicKey } (run `akan-native update keygen`)");
74
+ const desktop = platform === "macos" || platform === "windows" || platform === "linux" ? platform : null;
75
+
76
+ const dir = join(out, desktop ? `${desktop}-${hostArch()}` : platform);
77
+ assertChannel(channel);
78
+ assertSigningKey(config);
79
+ const keyPath = updateKeyPath(config.app.id);
80
+ const sequence = nextSequence(dir, channel);
81
+
82
+ let manifest: UpdateManifest;
83
+ let superseded: string | null = null;
84
+ if (desktop) {
85
+ ({ manifest, superseded } = await publishAppUpdate(ctx, desktop, artifact, dir, channel, sequence));
86
+ } else {
87
+ const info = readBundleInfo(join(ctx.outDir, BUNDLE_FILE));
88
+ manifest = webManifest({
89
+ app: config.app,
90
+ platform,
91
+ channel,
92
+ nativeApi: info.nativeApi.hash,
93
+ sequence,
94
+ bundle: `${sequence}-${info.web.hash.slice(0, 8)}`,
95
+ files: writeWebBundle(dir, ctx.webDir, ctx.html, ctx.env),
96
+ });
97
+ }
98
+ const bytes = new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`);
99
+ const signature = `${signManifest(bytes, keyPath, config.updates.publicKey)}\n`;
100
+ mkdirSync(dir, { recursive: true });
101
+ writeSigned(dir, channel, bytes, signature);
102
+ if (superseded) rmSync(superseded, { force: true });
103
+ return { dir, manifest };
104
+ }
@@ -1,9 +1,10 @@
1
1
 
2
2
 
3
3
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
4
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
4
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
5
+ import { dirname, join, relative } from "node:path";
6
6
  import type { Platform } from "../../../core/src/index.ts";
7
+ import { runtimeVersion } from "./boot.ts";
7
8
  import { CliError } from "./log.ts";
8
9
  import { akanNativeHome } from "./toolchains.ts";
9
10
 
@@ -59,6 +60,77 @@ export interface UpdateManifest {
59
60
  patches?: { from: string; url: string; sha256: string; size: number }[];
60
61
  }
61
62
 
63
+ /** Every file of a web bundle as the app serves it: index.html with the init script and CSP, public/, env.runtime.json. */
64
+ export function webFiles(
65
+ webDir: string,
66
+ html: string,
67
+ env: Record<string, string>,
68
+ ): { path: string; data: Uint8Array }[] {
69
+ const files: { path: string; data: Uint8Array }[] = [];
70
+ const walk = (dir: string) => {
71
+ for (const name of readdirSync(dir).sort()) {
72
+ const path = join(dir, name);
73
+ if (statSync(path).isDirectory()) walk(path);
74
+ else {
75
+ const rel = relative(webDir, path);
76
+ files.push({ path: rel, data: rel === "index.html" ? new TextEncoder().encode(html) : readFileSync(path) });
77
+ }
78
+ }
79
+ };
80
+ walk(webDir);
81
+ files.push({ path: "env.runtime.json", data: new TextEncoder().encode(JSON.stringify(env, null, 2)) });
82
+ return files;
83
+ }
84
+
85
+ /** Writes a web bundle's files under `<out>/files/<sha256>` (content-addressed, so releases share them) and lists them. */
86
+ export function writeWebBundle(out: string, webDir: string, html: string, env: Record<string, string>): UpdateFile[] {
87
+ mkdirSync(join(out, "files"), { recursive: true });
88
+ return webFiles(webDir, html, env).map(({ path, data }) => {
89
+ const hash = sha256(data);
90
+ const target = join(out, "files", hash);
91
+ if (!existsSync(target)) writeFileSync(target, data);
92
+ return { path, sha256: hash, size: data.length };
93
+ });
94
+ }
95
+
96
+ /** A web bundle's manifest (UP-2), keys in the order `update publish` has always written them. */
97
+ export function webManifest(release: {
98
+ app: { id: string; version: string };
99
+ platform: Platform;
100
+ channel: string;
101
+ nativeApi: string;
102
+ sequence: number;
103
+ bundle: string;
104
+ files: UpdateFile[];
105
+ }): UpdateManifest {
106
+ return {
107
+ schema: 1,
108
+ kind: "web",
109
+ app: release.app.id,
110
+ platform: release.platform,
111
+ channel: release.channel,
112
+ nativeApi: release.nativeApi,
113
+ sequence: release.sequence,
114
+ bundle: release.bundle,
115
+ runtimeVersion: runtimeVersion(),
116
+ version: release.app.version,
117
+ files: release.files,
118
+ };
119
+ }
120
+
121
+ /** A channel is a file name in the release folder and a string an app's config compares: lowercase, no separators. */
122
+ const CHANNEL_NAME = /^[a-z0-9][a-z0-9._-]{0,40}$/;
123
+ /** `<channel>.json` of these would overwrite what an unsigned phone update (packUpdate) writes to the same folder. */
124
+ const RESERVED_CHANNELS = ["bundle", "manifest.template", "compat"];
125
+
126
+ /** Refuses a channel no app could be configured for: it would be written but never taken. */
127
+ export function assertChannel(channel: string): void {
128
+ if (!CHANNEL_NAME.test(channel))
129
+ throw new CliError(`channel "${channel}" is not a short lowercase name like "production" or "pilot"`);
130
+ if (RESERVED_CHANNELS.includes(channel))
131
+ throw new CliError(`channel "${channel}" would overwrite ${channel}.json of an unsigned update: name another`);
132
+ }
133
+
62
134
  export function validateUpdates(raw: unknown, problems: string[]): UpdatesConfig | null {
63
135
  if (raw === undefined) return null;
64
136
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
@@ -70,7 +142,10 @@ export function validateUpdates(raw: unknown, problems: string[]): UpdatesConfig
70
142
  problems.push("updates.url must be an http(s) URL");
71
143
  if (typeof r.publicKey !== "string" || Buffer.from(r.publicKey, "base64").length !== 32)
72
144
  problems.push("updates.publicKey must be a base64 Ed25519 public key (akan-native update keygen)");
73
- if (r.channel !== undefined && (typeof r.channel !== "string" || !/^[a-z0-9][a-z0-9._-]{0,40}$/.test(r.channel)))
145
+ if (
146
+ r.channel !== undefined &&
147
+ (typeof r.channel !== "string" || !CHANNEL_NAME.test(r.channel) || RESERVED_CHANNELS.includes(r.channel))
148
+ )
74
149
  problems.push('updates.channel must be a short name like "production"');
75
150
  if (r.readyTimeout !== undefined && (typeof r.readyTimeout !== "number" || r.readyTimeout < 1000))
76
151
  problems.push("updates.readyTimeout must be a number of milliseconds >= 1000");
@@ -112,11 +187,16 @@ function publicKeyOf(privateKey: ReturnType<typeof createPrivateKey>): string {
112
187
  return der.subarray(der.length - 32).toString("base64");
113
188
  }
114
189
 
115
- export function signManifest(bytes: Uint8Array, keyPath: string, expectedPublicKey: string): string {
190
+ /** The key at `keyPath`, refused unless it is the one whose public half the apps were built with. */
191
+ export function signingKey(keyPath: string, expectedPublicKey: string) {
116
192
  const key = readPrivateKey(keyPath);
117
193
  if (publicKeyOf(key) !== expectedPublicKey)
118
194
  throw new CliError(`the key at ${keyPath} does not match updates.publicKey in akan-native.config.ts`);
119
- return sign(null, bytes, key).toString("base64");
195
+ return key;
196
+ }
197
+
198
+ export function signManifest(bytes: Uint8Array, keyPath: string, expectedPublicKey: string): string {
199
+ return sign(null, bytes, signingKey(keyPath, expectedPublicKey)).toString("base64");
120
200
  }
121
201
 
122
202
  /** Verifies like the apps do: raw public key, base64 signature over the exact manifest bytes. */
@@ -503,6 +503,7 @@ export async function buildAndroid(ctx: BuildContext): Promise<string> {
503
503
 
504
504
  externalSchemes: config.security?.shell?.externalSchemes ?? [],
505
505
  minWebViewVersion: config.android?.minWebViewVersion ?? MIN_WEBVIEW,
506
+ autoplay: config.android?.autoplay === true,
506
507
 
507
508
  keyboardResize: config.keyboard?.resize ?? "resize",
508
509
 
@@ -1,6 +1,6 @@
1
1
 
2
2
 
3
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { createDelta } from "../../../desktop/src/delta.ts";
6
6
  import { runtimeVersion } from "../lib/boot.ts";
@@ -23,7 +23,7 @@ export async function publishAppUpdate(
23
23
  out: string,
24
24
  channel: string,
25
25
  sequence: number,
26
- ): Promise<UpdateManifest> {
26
+ ): Promise<{ manifest: UpdateManifest; superseded: string | null }> {
27
27
  const { config } = ctx.project;
28
28
  const dir = join(out, "app");
29
29
  mkdirSync(dir, { recursive: true });
@@ -36,6 +36,7 @@ export async function publishAppUpdate(
36
36
  writeFileSync(join(dir, `${tarSha}.tar.gz`), gz);
37
37
 
38
38
  const patches: NonNullable<UpdateManifest["patches"]> = [];
39
+ let superseded: string | null = null;
39
40
  const previousPath = join(out, `${channel}.json`);
40
41
  if (existsSync(previousPath)) {
41
42
  const previous = JSON.parse(readFileSync(previousPath, "utf8")) as UpdateManifest;
@@ -54,10 +55,10 @@ export async function publishAppUpdate(
54
55
  );
55
56
  }
56
57
 
57
- if (from && from !== tarSha) rmSync(base, { force: true });
58
+ if (from && from !== tarSha) superseded = base;
58
59
  }
59
60
 
60
- return {
61
+ const manifest: UpdateManifest = {
61
62
  schema: 1,
62
63
  kind: "app",
63
64
  app: config.app.id,
@@ -75,4 +76,5 @@ export async function publishAppUpdate(
75
76
  archive: { sha256: tarSha, url: `app/${tarSha}.tar.gz`, size: gz.length, gzSha256: sha256(gz) },
76
77
  patches,
77
78
  };
79
+ return { manifest, superseded };
78
80
  }
@@ -208,6 +208,18 @@ export function writeDesktopResources(
208
208
  updatesResource(config.updates, config.app, os, boot.nativeApi, ctx.dev),
209
209
  );
210
210
  writeFileSync(join(resources, "env.runtime.json"), JSON.stringify(ctx.env, null, 2));
211
+ const server = config.desktop.server;
212
+ if (server) {
213
+ if (!ctx.project.plugins.some((p) => p.manifest.id === "single-instance"))
214
+ log.warn(
215
+ "desktop.server without the single-instance plugin: every launch starts another server on the same data",
216
+ );
217
+ cpSync(server.dir, join(resources, "server"), { recursive: true, verbatimSymlinks: true });
218
+ writeFileSync(join(resources, "server.json"), JSON.stringify({ entry: server.entry, env: server.env }));
219
+ writeFileSync(join(resources, "server.bunfig.toml"), "");
220
+ }
221
+ if (config.desktop.bin)
222
+ cpSync(config.desktop.bin, join(resources, "bin"), { recursive: true, verbatimSymlinks: true });
211
223
  writeFileSync(
212
224
  join(resources, "shell.json"),
213
225
  JSON.stringify({
@@ -216,6 +228,10 @@ export function writeDesktopResources(
216
228
  backgroundColorDark: config.shell.backgroundColorDark,
217
229
  devtools: ctx.dev,
218
230
  quitOnLastWindowClosed: config.desktop.quitOnLastWindowClosed,
231
+ recovery: config.desktop.recovery,
232
+ screenCapture: config.desktop.screenCapture,
233
+ fullscreen: config.desktop.window.fullscreen,
234
+ skipTaskbar: config.desktop.window.skipTaskbar,
219
235
 
220
236
  deepLinks: config.deepLinks.schemes,
221
237
 
@@ -1,6 +1,17 @@
1
1
 
2
2
 
3
- import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import {
4
+ closeSync,
5
+ cpSync,
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readdirSync,
11
+ readSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
4
15
  import { join } from "node:path";
5
16
  import type { ParsedArgs } from "../lib/args.ts";
6
17
  import { execOrThrow } from "../lib/exec.ts";
@@ -78,6 +89,8 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
78
89
  log.step(`sign: ${identity ? identity.name : "ad-hoc"}`);
79
90
 
80
91
  await execOrThrow(["xattr", "-cr", appPath], { echo: false });
92
+ for (const addon of carriedNativeCode(resources))
93
+ await execOrThrow(["codesign", "--force", "--sign", sign, addon], { echo: false });
81
94
  await execOrThrow(["codesign", "--force", "--sign", sign, join(contents, "Frameworks", NATIVE_LIB.macos)], {
82
95
  echo: false,
83
96
  });
@@ -87,6 +100,36 @@ export async function buildMacos(ctx: BuildContext): Promise<string> {
87
100
  return appPath;
88
101
  }
89
102
 
103
+ /**
104
+ * Mach-O files the app carries in its server and its `bin`, whatever their names (native addons, executables, a
105
+ * package's own executable): nested code the bundle's signature does not sign.
106
+ */
107
+ export function carriedNativeCode(resources: string): string[] {
108
+ return ["server", "bin"].flatMap((folder) => {
109
+ const dir = join(resources, folder);
110
+ if (!existsSync(dir)) return [];
111
+ return (readdirSync(dir, { recursive: true }) as string[])
112
+ .filter((file) => !/\.(c?js|mjs|ts|json|map|md|txt|html|css)$/i.test(file))
113
+ .map((file) => join(dir, file))
114
+ .filter(isMachO);
115
+ });
116
+ }
117
+
118
+ function isMachO(file: string): boolean {
119
+ if (!lstatSync(file).isFile()) return false;
120
+ const head = Buffer.alloc(8);
121
+ const fd = openSync(file, "r");
122
+ try {
123
+ if (readSync(fd, head, 0, 8, 0) < 8) return false;
124
+ } finally {
125
+ closeSync(fd);
126
+ }
127
+ const magic = head.readUInt32BE(0);
128
+ if (magic === 0xfeedface || magic === 0xfeedfacf || magic === 0xcefaedfe || magic === 0xcffaedfe) return true;
129
+
130
+ return (magic === 0xcafebabe || magic === 0xcafebabf) && head.readUInt32BE(4) < 45;
131
+ }
132
+
90
133
  export async function launchMacos(ctx: BuildContext, appPath: string, opts: LaunchOptions): Promise<Launched> {
91
134
 
92
135
  return launchExecutable(join(appPath, "Contents", "MacOS", ctx.project.config.app.fileName), opts);