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.
- package/base/baseEnv.ts +19 -5
- package/client/csrTypes.ts +2 -0
- package/client/device.ts +28 -12
- package/client/index.ts +1 -0
- package/client/native/accessibility.ts +1 -0
- package/client/native/app-state.ts +1 -0
- package/client/native/app.ts +1 -0
- package/client/native/appearance.ts +1 -0
- package/client/native/auth-session.ts +1 -0
- package/client/native/autostart.ts +1 -0
- package/client/native/badge.ts +1 -0
- package/client/native/biometric.ts +1 -0
- package/client/native/browser.ts +1 -0
- package/client/native/camera.ts +1 -0
- package/client/native/clipboard.ts +1 -0
- package/client/native/contacts.ts +1 -0
- package/client/native/device.ts +1 -0
- package/client/native/dialog.ts +1 -0
- package/client/native/dock.ts +1 -0
- package/client/native/file-picker.ts +1 -0
- package/client/native/filesystem.ts +1 -0
- package/client/native/geolocation.ts +1 -0
- package/client/native/global-shortcut.ts +1 -0
- package/client/native/haptics.ts +1 -0
- package/client/native/http.ts +1 -0
- package/client/native/iap.ts +1 -0
- package/client/native/keep-awake.ts +1 -0
- package/client/native/keyboard.ts +1 -0
- package/client/native/local-notifications.ts +1 -0
- package/client/native/menu.ts +1 -0
- package/client/native/network.ts +1 -0
- package/client/native/opener.ts +1 -0
- package/client/native/preferences.ts +1 -0
- package/client/native/push.ts +1 -0
- package/client/native/screen-orientation.ts +1 -0
- package/client/native/screen.ts +1 -0
- package/client/native/secure-storage.ts +1 -0
- package/client/native/share.ts +1 -0
- package/client/native/single-instance.ts +1 -0
- package/client/native/splash-screen.ts +1 -0
- package/client/native/sqlite.ts +1 -0
- package/client/native/toast.ts +1 -0
- package/client/native/tray.ts +1 -0
- package/client/native/updates.ts +1 -0
- package/client/native/volume.ts +1 -0
- package/client/native/window-state.ts +1 -0
- package/client/native/window.ts +1 -0
- package/client/native.ts +31 -1
- package/client/resolveServerUrl.ts +11 -0
- package/client/router.ts +8 -0
- package/common/Logger.ts +26 -2
- package/common/RouteLayering.ts +52 -0
- package/common/index.ts +1 -0
- package/index.ts +115 -6
- package/native/desktop.ts +5 -0
- package/package.json +11 -1
- package/server/akanApp.ts +23 -3
- package/server/akanAppHeaders.ts +4 -1
- package/server/akanServer.ts +27 -7
- package/server/index.ts +1 -0
- package/server/lifecycle/orphanGroup.ts +21 -0
- package/server/nativeFile.ts +107 -0
- package/server/ops/opsRoute.ts +3 -2
- package/server/routeTreeBuilder.ts +14 -39
- package/server/routing/apiRouter.ts +26 -3
- package/server/routing/hostAllowlist.ts +24 -0
- package/server/selfExec.ts +25 -0
- package/server/types.tsx +3 -1
- package/server/webRouter.ts +7 -4
- package/service/ipcTypes.ts +4 -0
- package/signal/CrossSiteGuard.ts +7 -0
- package/test/signalTestRuntime.ts +8 -2
- package/types/base/baseEnv.d.ts +1 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/client/index.d.ts +1 -0
- package/types/client/native/accessibility.d.ts +1 -0
- package/types/client/native/app-state.d.ts +1 -0
- package/types/client/native/app.d.ts +1 -0
- package/types/client/native/appearance.d.ts +1 -0
- package/types/client/native/auth-session.d.ts +1 -0
- package/types/client/native/autostart.d.ts +1 -0
- package/types/client/native/badge.d.ts +1 -0
- package/types/client/native/biometric.d.ts +1 -0
- package/types/client/native/browser.d.ts +1 -0
- package/types/client/native/camera.d.ts +1 -0
- package/types/client/native/clipboard.d.ts +1 -0
- package/types/client/native/contacts.d.ts +1 -0
- package/types/client/native/device.d.ts +1 -0
- package/types/client/native/dialog.d.ts +1 -0
- package/types/client/native/dock.d.ts +1 -0
- package/types/client/native/file-picker.d.ts +1 -0
- package/types/client/native/filesystem.d.ts +1 -0
- package/types/client/native/geolocation.d.ts +1 -0
- package/types/client/native/global-shortcut.d.ts +1 -0
- package/types/client/native/haptics.d.ts +1 -0
- package/types/client/native/http.d.ts +1 -0
- package/types/client/native/iap.d.ts +1 -0
- package/types/client/native/keep-awake.d.ts +1 -0
- package/types/client/native/keyboard.d.ts +1 -0
- package/types/client/native/local-notifications.d.ts +1 -0
- package/types/client/native/menu.d.ts +1 -0
- package/types/client/native/network.d.ts +1 -0
- package/types/client/native/opener.d.ts +1 -0
- package/types/client/native/preferences.d.ts +1 -0
- package/types/client/native/push.d.ts +1 -0
- package/types/client/native/screen-orientation.d.ts +1 -0
- package/types/client/native/screen.d.ts +1 -0
- package/types/client/native/secure-storage.d.ts +1 -0
- package/types/client/native/share.d.ts +1 -0
- package/types/client/native/single-instance.d.ts +1 -0
- package/types/client/native/splash-screen.d.ts +1 -0
- package/types/client/native/sqlite.d.ts +1 -0
- package/types/client/native/toast.d.ts +1 -0
- package/types/client/native/tray.d.ts +1 -0
- package/types/client/native/updates.d.ts +1 -0
- package/types/client/native/volume.d.ts +1 -0
- package/types/client/native/window-state.d.ts +1 -0
- package/types/client/native/window.d.ts +1 -0
- package/types/client/native.d.ts +14 -2
- package/types/client/resolveServerUrl.d.ts +1 -0
- package/types/client/router.d.ts +3 -0
- package/types/common/RouteLayering.d.ts +28 -0
- package/types/common/index.d.ts +1 -0
- package/types/index.d.ts +112 -6
- package/types/native/desktop.d.ts +3 -0
- package/types/server/akanAppHeaders.d.ts +2 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/lifecycle/orphanGroup.d.ts +7 -0
- package/types/server/nativeFile.d.ts +10 -0
- package/types/server/routing/apiRouter.d.ts +3 -1
- package/types/server/routing/hostAllowlist.d.ts +8 -0
- package/types/server/selfExec.d.ts +8 -0
- package/types/server/types.d.ts +3 -1
- package/types/service/ipcTypes.d.ts +16 -0
- package/types/signal/CrossSiteGuard.d.ts +1 -0
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +57 -4
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +48 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +8 -2
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +10 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +24 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +4 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/macos.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +38 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +8 -1
- package/types/vendor/@akanjs/native/packages/desktop/src/grants.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/main.d.ts +11 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +13 -2
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +27 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +21 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +79 -0
- package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +5 -0
- package/types/vendor/@akanjs/native/plugins/file-picker/src/index.d.ts +20 -1
- package/types/vendor/@akanjs/native/plugins/push/src/index.d.ts +9 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +28 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/common.d.ts +10 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/desktop.d.ts +18 -0
- package/types/vendor/@akanjs/native/plugins/volume/src/index.d.ts +28 -0
- package/types/vendor/@akanjs/native/plugins/volume/src.d.ts +1 -0
- package/types/webkit/csrFrameDump.d.ts +42 -0
- package/types/webkit/nativeNavigation.d.ts +1 -0
- package/types/webkit/nativePurchase.d.ts +6 -2
- package/types/webkit/nativeUpdates.d.ts +8 -0
- package/ui/Agent/Attach.tsx +6 -2
- package/ui/CsrImage.tsx +3 -2
- package/ui/System/CSR.tsx +4 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +5 -1
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeBridge.kt +19 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +161 -25
- package/vendor/@akanjs/native/native/desktop/src/volume.rs +184 -0
- package/vendor/@akanjs/native/native/desktop/src/win/mod.rs +3 -1
- package/vendor/@akanjs/native/native/desktop/src/win/volume.rs +71 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeShell.swift +12 -2
- package/vendor/@akanjs/native/packages/cli/src/api.ts +142 -12
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +5 -3
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +10 -71
- package/vendor/@akanjs/native/packages/cli/src/config.ts +41 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +58 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +10 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +38 -2
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +104 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +85 -5
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +1 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +6 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +16 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +44 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +436 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +2 -0
- package/vendor/@akanjs/native/packages/core/src/runtime.ts +26 -5
- package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +20 -0
- package/vendor/@akanjs/native/packages/desktop/src/grants.ts +47 -0
- package/vendor/@akanjs/native/packages/desktop/src/host.ts +102 -14
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +59 -11
- package/vendor/@akanjs/native/packages/desktop/src/paths.ts +38 -4
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +28 -0
- package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +159 -0
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +493 -0
- package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +14 -0
- package/vendor/@akanjs/native/plugins/app/native-plugin.json +2 -2
- package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
- package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
- package/vendor/@akanjs/native/plugins/app/src/web.ts +3 -0
- package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +39 -2
- package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +36 -1
- package/vendor/@akanjs/native/plugins/push/android/PushPlugin.kt +9 -1
- package/vendor/@akanjs/native/plugins/push/ios/PushPlugin.swift +4 -0
- package/vendor/@akanjs/native/plugins/push/src/index.ts +10 -0
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +464 -452
- package/vendor/@akanjs/native/plugins/volume/android/VolumePlugin.kt +107 -0
- package/vendor/@akanjs/native/plugins/volume/native-plugin.json +14 -0
- package/vendor/@akanjs/native/plugins/volume/src/common.ts +28 -0
- package/vendor/@akanjs/native/plugins/volume/src/desktop.ts +185 -0
- package/vendor/@akanjs/native/plugins/volume/src/index.ts +32 -0
- package/webkit/CsrRouteTable.ts +15 -35
- package/webkit/RenderLayer.tsx +71 -6
- package/webkit/bootCsr.tsx +11 -8
- package/webkit/csrFrameDump.ts +67 -0
- package/webkit/nativeNavigation.ts +17 -0
- package/webkit/nativePurchase.ts +12 -2
- package/webkit/nativeUpdates.ts +55 -0
- 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: {
|
|
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: {
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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,
|
|
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)
|
|
58
|
+
if (from && from !== tarSha) superseded = base;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
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 {
|
|
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);
|