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
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
|
-
|
|
71
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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 ===
|
|
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
|
-
|
|
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)) {
|
package/server/akanAppHeaders.ts
CHANGED
|
@@ -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",
|
|
50
|
+
headers.set("host", AKAN_CHILD_HOST);
|
|
48
51
|
return headers;
|
|
49
52
|
}
|
package/server/akanServer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
567
|
+
setTimeout(() => this.#exitOrphaned(1), this.shutdownTimeoutMs + 1_000);
|
|
554
568
|
void this.stop()
|
|
555
|
-
.then(() =>
|
|
556
|
-
.catch(() =>
|
|
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
|
+
}
|
package/server/ops/opsRoute.ts
CHANGED
|
@@ -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(
|
|
69
|
-
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
|
});
|