akanjs 3.0.0-beta.27 → 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/device.ts +22 -6
- 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/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/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/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/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 +2 -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/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/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 +97 -11
- 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/RenderLayer.tsx +71 -6
- 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/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
|
});
|
|
@@ -8,6 +8,7 @@ import { copyBunRequestFields, type WebProxyRunner } from "../proxy";
|
|
|
8
8
|
import { SignalResolver } from "../resolver";
|
|
9
9
|
import type { HttpRoutes, SignalRouteOptions, WebsocketRoutes } from "../types";
|
|
10
10
|
import { AppWsData } from "./appWsData";
|
|
11
|
+
import type { HostAllowlist } from "./hostAllowlist";
|
|
11
12
|
|
|
12
13
|
export interface HmrStateSource {
|
|
13
14
|
readonly state: {
|
|
@@ -34,6 +35,7 @@ export interface ApiRouteInputs {
|
|
|
34
35
|
renderEnvRoutes: HttpRoutes;
|
|
35
36
|
upgradeAppWs: (req: Request, data: AppWsData) => boolean;
|
|
36
37
|
webProxyRunner?: WebProxyRunner | null;
|
|
38
|
+
hostAllowlist?: HostAllowlist | null;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
type RouteValue = NonNullHttpRoutes[keyof NonNullHttpRoutes];
|
|
@@ -62,6 +64,7 @@ export class ApiRouter {
|
|
|
62
64
|
renderEnvRoutes,
|
|
63
65
|
upgradeAppWs,
|
|
64
66
|
webProxyRunner,
|
|
67
|
+
hostAllowlist,
|
|
65
68
|
}: ApiRouteInputs): NonNullHttpRoutes {
|
|
66
69
|
const endpointEntries = Object.entries(routes ?? {}).map(
|
|
67
70
|
([p, handler]) =>
|
|
@@ -76,6 +79,12 @@ export class ApiRouter {
|
|
|
76
79
|
const endpointPaths = new Set([...endpointEntries.map(([path]) => path), ...builtinEntries.map(([path]) => path)]);
|
|
77
80
|
const routeTable = {
|
|
78
81
|
[`${prefix}${websocketPrefix}` as "/api/ws"]: (req) => {
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
CrossSiteGuard.assertOrigin(req, new URL(req.url), "websocket");
|
|
85
|
+
} catch {
|
|
86
|
+
return new Response("Forbidden", { status: 403 });
|
|
87
|
+
}
|
|
79
88
|
const upgraded = upgradeAppWs(req, AppWsData.fromRequest(req));
|
|
80
89
|
if (upgraded) return;
|
|
81
90
|
return new Response("Failed to upgrade to WebSocket", { status: 500 });
|
|
@@ -84,9 +93,10 @@ export class ApiRouter {
|
|
|
84
93
|
...Object.fromEntries(builtinEntries),
|
|
85
94
|
...(renderEnvRoutes ?? {}),
|
|
86
95
|
} as NonNullHttpRoutes;
|
|
87
|
-
|
|
96
|
+
const served = webProxyRunner
|
|
88
97
|
? ApiRouter.#wrapRoutesWithWebProxy(routeTable, webProxyRunner, prefix, endpointPaths)
|
|
89
98
|
: routeTable;
|
|
99
|
+
return hostAllowlist ? ApiRouter.#guardHosts(served, hostAllowlist) : served;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
static buildWebsocketHandlers({
|
|
@@ -214,6 +224,15 @@ export class ApiRouter {
|
|
|
214
224
|
) as NonNullHttpRoutes;
|
|
215
225
|
}
|
|
216
226
|
|
|
227
|
+
static #guardHosts(routes: NonNullHttpRoutes, allowlist: HostAllowlist): NonNullHttpRoutes {
|
|
228
|
+
return Object.fromEntries(
|
|
229
|
+
Object.entries(routes).map(([path, route]) => [
|
|
230
|
+
path,
|
|
231
|
+
ApiRouter.#mapRoute(route, (handler) => (req) => (allowlist.allows(req) ? handler(req) : allowlist.refuse())),
|
|
232
|
+
]),
|
|
233
|
+
) as NonNullHttpRoutes;
|
|
234
|
+
}
|
|
235
|
+
|
|
217
236
|
static #isApiRoute(path: string, apiPrefix: string): boolean {
|
|
218
237
|
const normalized = apiPrefix.replace(/\/$/, "");
|
|
219
238
|
return path === normalized || path.startsWith(`${normalized}/`);
|
|
@@ -242,12 +261,16 @@ export class ApiRouter {
|
|
|
242
261
|
|
|
243
262
|
static #mapRoute(route: RouteValue, wrap: (handler: RouteHandler) => RouteHandler): RouteValue {
|
|
244
263
|
if (typeof route === "function") return wrap(route as RouteHandler) as RouteValue;
|
|
245
|
-
if (route instanceof Response) return wrap(() => route) as RouteValue;
|
|
264
|
+
if (route instanceof Response) return wrap(() => route.clone()) as RouteValue;
|
|
246
265
|
if (!route || typeof route !== "object") return route;
|
|
247
266
|
return Object.fromEntries(
|
|
248
267
|
Object.entries(route).map(([method, handler]) => [
|
|
249
268
|
method,
|
|
250
|
-
typeof handler === "function"
|
|
269
|
+
typeof handler === "function"
|
|
270
|
+
? wrap(handler as RouteHandler)
|
|
271
|
+
: handler instanceof Response
|
|
272
|
+
? wrap(() => handler.clone())
|
|
273
|
+
: handler,
|
|
251
274
|
]),
|
|
252
275
|
) as RouteValue;
|
|
253
276
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export class HostAllowlist {
|
|
4
|
+
readonly #hosts: Set<string>;
|
|
5
|
+
|
|
6
|
+
constructor(hosts: Iterable<string>) {
|
|
7
|
+
this.#hosts = new Set([...hosts].map((host) => host.trim().toLowerCase()).filter(Boolean));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** `AKAN_ALLOWED_HOSTS`: comma-separated `host:port` entries; unset or blank serves every Host. */
|
|
11
|
+
static fromEnv(env: Record<string, string | undefined> = process.env): HostAllowlist | null {
|
|
12
|
+
const allowlist = new HostAllowlist((env.AKAN_ALLOWED_HOSTS ?? "").split(","));
|
|
13
|
+
return allowlist.#hosts.size ? allowlist : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
allows(req: Request): boolean {
|
|
17
|
+
const host = req.headers.get("host")?.trim().toLowerCase();
|
|
18
|
+
return !!host && this.#hosts.has(host);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
refuse(): Response {
|
|
22
|
+
return new Response("Forbidden", { status: 403 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export class SelfExec {
|
|
4
|
+
static #beBun: string | undefined;
|
|
5
|
+
|
|
6
|
+
static adopt() {
|
|
7
|
+
const value = process.env.BUN_BE_BUN;
|
|
8
|
+
if (value === undefined) return;
|
|
9
|
+
SelfExec.#beBun = value;
|
|
10
|
+
delete process.env.BUN_BE_BUN;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A desktop app's carried server: only its shell starts the app executable as Bun. */
|
|
14
|
+
static get carried(): boolean {
|
|
15
|
+
return SelfExec.#beBun !== undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static env(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
19
|
+
return SelfExec.#beBun === undefined ? env : { ...env, BUN_BE_BUN: SelfExec.#beBun };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static command(script: string, ...args: string[]): string[] {
|
|
23
|
+
return [process.execPath, ...process.execArgv, script, ...args];
|
|
24
|
+
}
|
|
25
|
+
}
|
package/server/types.tsx
CHANGED
|
@@ -125,7 +125,9 @@ export const getWebConfigFromEnv = (): AkanWebConfig => {
|
|
|
125
125
|
|
|
126
126
|
export interface MobileDeepLinkAssociation {
|
|
127
127
|
targetName: string;
|
|
128
|
-
|
|
128
|
+
/** Null when the target names no id for the platform; that platform then gets no association. */
|
|
129
|
+
iosAppId: string | null;
|
|
130
|
+
androidAppId: string | null;
|
|
129
131
|
domains: string[];
|
|
130
132
|
iosTeamId?: string;
|
|
131
133
|
androidSha256CertFingerprints?: string[];
|
package/server/webRouter.ts
CHANGED
|
@@ -1168,9 +1168,9 @@ export class WebRouter {
|
|
|
1168
1168
|
const associations = artifact.deepLinkAssociations ?? [];
|
|
1169
1169
|
if (pathname === APPLE_APP_SITE_ASSOCIATION_PATH) {
|
|
1170
1170
|
const details = associations
|
|
1171
|
-
.filter((association) => association.domains.length > 0 && association.iosTeamId)
|
|
1171
|
+
.filter((association) => association.domains.length > 0 && association.iosTeamId && association.iosAppId)
|
|
1172
1172
|
.map((association) => ({
|
|
1173
|
-
appIDs: [`${association.iosTeamId}.${association.
|
|
1173
|
+
appIDs: [`${association.iosTeamId}.${association.iosAppId}`],
|
|
1174
1174
|
components: [{ "/": "/*" }],
|
|
1175
1175
|
}));
|
|
1176
1176
|
if (details.length === 0) return null;
|
|
@@ -1180,14 +1180,17 @@ export class WebRouter {
|
|
|
1180
1180
|
const packageSuffixes = process.env.AKAN_PUBLIC_ENV === "main" ? [""] : ["", ".debug"];
|
|
1181
1181
|
const assetLinks = associations
|
|
1182
1182
|
.filter(
|
|
1183
|
-
(association) =>
|
|
1183
|
+
(association) =>
|
|
1184
|
+
association.domains.length > 0 &&
|
|
1185
|
+
!!association.androidAppId &&
|
|
1186
|
+
(association.androidSha256CertFingerprints?.length ?? 0) > 0,
|
|
1184
1187
|
)
|
|
1185
1188
|
.flatMap((association) =>
|
|
1186
1189
|
packageSuffixes.map((suffix) => ({
|
|
1187
1190
|
relation: ["delegate_permission/common.handle_all_urls"],
|
|
1188
1191
|
target: {
|
|
1189
1192
|
namespace: "android_app",
|
|
1190
|
-
package_name: `${association.
|
|
1193
|
+
package_name: `${association.androidAppId}${suffix}`,
|
|
1191
1194
|
sha256_cert_fingerprints: association.androidSha256CertFingerprints,
|
|
1192
1195
|
},
|
|
1193
1196
|
})),
|
package/service/ipcTypes.ts
CHANGED
|
@@ -143,6 +143,7 @@ export type AkanIpcMessage =
|
|
|
143
143
|
/** Actual websocket upstream the child bound; may differ from the preferred port when it was in use. */
|
|
144
144
|
wsUpstream?: Extract<AkanUpstream, { type: "tcp" }>;
|
|
145
145
|
healthPath?: string;
|
|
146
|
+
crossSite?: { allowedOrigins: string[]; enabled: boolean };
|
|
146
147
|
}
|
|
147
148
|
| { type: "backend-ready"; pid: number }
|
|
148
149
|
| { type: "pubsub.publish"; roomId: string; data: object | object[]; origin?: string }
|
|
@@ -163,6 +164,9 @@ export type AkanIpcMessage =
|
|
|
163
164
|
/** Hub → child: the lowest severity any subscriber wants; `null` tells the child to stop forwarding. */
|
|
164
165
|
| { type: "log.level"; minSev: number | null }
|
|
165
166
|
| { type: "shutdown"; signal?: string }
|
|
167
|
+
/** Desktop server → the shell that started it: the path behind a file picker grant (forServer). */
|
|
168
|
+
| { type: "file.resolve"; id: string; grant: string }
|
|
169
|
+
| { type: "file.resolved"; id: string; path?: string; mode?: "read" | "write" | "folder"; error?: string }
|
|
166
170
|
| { type: "error"; message: string; stack?: string; pid?: number };
|
|
167
171
|
|
|
168
172
|
export const sendAkanIpc = (message: AkanIpcMessage) => {
|
package/signal/CrossSiteGuard.ts
CHANGED
|
@@ -14,16 +14,23 @@ export class CrossSiteGuard {
|
|
|
14
14
|
* Android and Windows; both WebViews send exactly that as `Origin`. */
|
|
15
15
|
static readonly nativeOrigins = ["app://localhost", "https://app.localhost"] as const;
|
|
16
16
|
static #enabled = true;
|
|
17
|
+
static #allowedOrigins: string[] = [];
|
|
17
18
|
static #allowed = new Set<string>(CrossSiteGuard.nativeOrigins);
|
|
18
19
|
|
|
19
20
|
/** Applied at boot from the mounting app's `option.ts`; the native shells stay allowed unless disabled. */
|
|
20
21
|
static configure({ allowedOrigins = [], enabled = true }: CrossSiteOption) {
|
|
21
22
|
CrossSiteGuard.#enabled = enabled;
|
|
23
|
+
CrossSiteGuard.#allowedOrigins = [...allowedOrigins];
|
|
22
24
|
CrossSiteGuard.#allowed = new Set([...CrossSiteGuard.nativeOrigins, ...allowedOrigins]);
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
static option(): Required<CrossSiteOption> {
|
|
28
|
+
return { allowedOrigins: [...CrossSiteGuard.#allowedOrigins], enabled: CrossSiteGuard.#enabled };
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
static reset() {
|
|
26
32
|
CrossSiteGuard.#enabled = true;
|
|
33
|
+
CrossSiteGuard.#allowedOrigins = [];
|
|
27
34
|
CrossSiteGuard.#allowed = new Set(CrossSiteGuard.nativeOrigins);
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -20,7 +20,7 @@ export interface SignalTestContext<Fetch = FetchProxy> {
|
|
|
20
20
|
export type SignalTestOptions = Pick<TestServerOptions, "storage" | "workerId" | "port" | "serverMode">;
|
|
21
21
|
|
|
22
22
|
type SignalServerModule = {
|
|
23
|
-
env
|
|
23
|
+
env?: BackendEnv;
|
|
24
24
|
fetch: FetchProxy;
|
|
25
25
|
lib: AkanLib;
|
|
26
26
|
};
|
|
@@ -68,6 +68,11 @@ const importServerModule = async (type: "app" | "lib", name: string): Promise<Si
|
|
|
68
68
|
|
|
69
69
|
const importLibModule = async (name: string): Promise<SignalServerModule> => await import(`@libs/${name}/server`);
|
|
70
70
|
|
|
71
|
+
const importTestingEnv = async (type: "app" | "lib", name: string, targetModule: SignalServerModule) =>
|
|
72
|
+
type === "app"
|
|
73
|
+
? ((await import(`@apps/${name}/env/env.server.testing`)) as { env?: BackendEnv }).env
|
|
74
|
+
: targetModule.env;
|
|
75
|
+
|
|
71
76
|
export const setupSignalTestTarget = async <Fetch = FetchProxy>(
|
|
72
77
|
{
|
|
73
78
|
type = process.env.AKAN_TEST_TARGET_TYPE as "app" | "lib" | undefined,
|
|
@@ -96,10 +101,11 @@ export const setupSignalTestTarget = async <Fetch = FetchProxy>(
|
|
|
96
101
|
Promise.all((libNames ?? []).filter((libName) => libName !== name).map(importLibModule)),
|
|
97
102
|
importServerModule(type, name),
|
|
98
103
|
]);
|
|
104
|
+
const testingEnv = await importTestingEnv(type, name, targetModule);
|
|
99
105
|
const target: SignalTestTarget = {
|
|
100
106
|
type,
|
|
101
107
|
name,
|
|
102
|
-
env: { ...env, ...
|
|
108
|
+
env: { ...env, ...testingEnv },
|
|
103
109
|
fetch: targetModule.fetch,
|
|
104
110
|
libs: [...dependencyModules.map((mod) => mod.lib), targetModule.lib],
|
|
105
111
|
};
|
package/types/base/baseEnv.d.ts
CHANGED
package/types/client/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./device.d.ts";
|
|
|
8
8
|
export * from "./frameConfig.d.ts";
|
|
9
9
|
export * from "./locale.d.ts";
|
|
10
10
|
export * from "./makePageProto.d.ts";
|
|
11
|
+
export * from "./resolveServerUrl.d.ts";
|
|
11
12
|
export * from "./route.d.ts";
|
|
12
13
|
export * from "./router.d.ts";
|
|
13
14
|
export * from "./rscNavigation.d.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../vendor/@akanjs/native/plugins/accessibility/src/index.d.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../vendor/@akanjs/native/plugins/app-state/src/index.d.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../vendor/@akanjs/native/plugins/app/src/index.d.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../../vendor/@akanjs/native/plugins/appearance/src/index.d.ts";
|