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.
Files changed (213) hide show
  1. package/base/baseEnv.ts +19 -5
  2. package/client/device.ts +22 -6
  3. package/client/index.ts +1 -0
  4. package/client/native/accessibility.ts +1 -0
  5. package/client/native/app-state.ts +1 -0
  6. package/client/native/app.ts +1 -0
  7. package/client/native/appearance.ts +1 -0
  8. package/client/native/auth-session.ts +1 -0
  9. package/client/native/autostart.ts +1 -0
  10. package/client/native/badge.ts +1 -0
  11. package/client/native/biometric.ts +1 -0
  12. package/client/native/browser.ts +1 -0
  13. package/client/native/camera.ts +1 -0
  14. package/client/native/clipboard.ts +1 -0
  15. package/client/native/contacts.ts +1 -0
  16. package/client/native/device.ts +1 -0
  17. package/client/native/dialog.ts +1 -0
  18. package/client/native/dock.ts +1 -0
  19. package/client/native/file-picker.ts +1 -0
  20. package/client/native/filesystem.ts +1 -0
  21. package/client/native/geolocation.ts +1 -0
  22. package/client/native/global-shortcut.ts +1 -0
  23. package/client/native/haptics.ts +1 -0
  24. package/client/native/http.ts +1 -0
  25. package/client/native/iap.ts +1 -0
  26. package/client/native/keep-awake.ts +1 -0
  27. package/client/native/keyboard.ts +1 -0
  28. package/client/native/local-notifications.ts +1 -0
  29. package/client/native/menu.ts +1 -0
  30. package/client/native/network.ts +1 -0
  31. package/client/native/opener.ts +1 -0
  32. package/client/native/preferences.ts +1 -0
  33. package/client/native/push.ts +1 -0
  34. package/client/native/screen-orientation.ts +1 -0
  35. package/client/native/screen.ts +1 -0
  36. package/client/native/secure-storage.ts +1 -0
  37. package/client/native/share.ts +1 -0
  38. package/client/native/single-instance.ts +1 -0
  39. package/client/native/splash-screen.ts +1 -0
  40. package/client/native/sqlite.ts +1 -0
  41. package/client/native/toast.ts +1 -0
  42. package/client/native/tray.ts +1 -0
  43. package/client/native/updates.ts +1 -0
  44. package/client/native/volume.ts +1 -0
  45. package/client/native/window-state.ts +1 -0
  46. package/client/native/window.ts +1 -0
  47. package/client/native.ts +31 -1
  48. package/client/resolveServerUrl.ts +11 -0
  49. package/client/router.ts +8 -0
  50. package/index.ts +115 -6
  51. package/native/desktop.ts +5 -0
  52. package/package.json +11 -1
  53. package/server/akanApp.ts +23 -3
  54. package/server/akanAppHeaders.ts +4 -1
  55. package/server/akanServer.ts +27 -7
  56. package/server/index.ts +1 -0
  57. package/server/lifecycle/orphanGroup.ts +21 -0
  58. package/server/nativeFile.ts +107 -0
  59. package/server/ops/opsRoute.ts +3 -2
  60. package/server/routing/apiRouter.ts +26 -3
  61. package/server/routing/hostAllowlist.ts +24 -0
  62. package/server/selfExec.ts +25 -0
  63. package/server/types.tsx +3 -1
  64. package/server/webRouter.ts +7 -4
  65. package/service/ipcTypes.ts +4 -0
  66. package/signal/CrossSiteGuard.ts +7 -0
  67. package/test/signalTestRuntime.ts +8 -2
  68. package/types/base/baseEnv.d.ts +1 -0
  69. package/types/client/index.d.ts +1 -0
  70. package/types/client/native/accessibility.d.ts +1 -0
  71. package/types/client/native/app-state.d.ts +1 -0
  72. package/types/client/native/app.d.ts +1 -0
  73. package/types/client/native/appearance.d.ts +1 -0
  74. package/types/client/native/auth-session.d.ts +1 -0
  75. package/types/client/native/autostart.d.ts +1 -0
  76. package/types/client/native/badge.d.ts +1 -0
  77. package/types/client/native/biometric.d.ts +1 -0
  78. package/types/client/native/browser.d.ts +1 -0
  79. package/types/client/native/camera.d.ts +1 -0
  80. package/types/client/native/clipboard.d.ts +1 -0
  81. package/types/client/native/contacts.d.ts +1 -0
  82. package/types/client/native/device.d.ts +1 -0
  83. package/types/client/native/dialog.d.ts +1 -0
  84. package/types/client/native/dock.d.ts +1 -0
  85. package/types/client/native/file-picker.d.ts +1 -0
  86. package/types/client/native/filesystem.d.ts +1 -0
  87. package/types/client/native/geolocation.d.ts +1 -0
  88. package/types/client/native/global-shortcut.d.ts +1 -0
  89. package/types/client/native/haptics.d.ts +1 -0
  90. package/types/client/native/http.d.ts +1 -0
  91. package/types/client/native/iap.d.ts +1 -0
  92. package/types/client/native/keep-awake.d.ts +1 -0
  93. package/types/client/native/keyboard.d.ts +1 -0
  94. package/types/client/native/local-notifications.d.ts +1 -0
  95. package/types/client/native/menu.d.ts +1 -0
  96. package/types/client/native/network.d.ts +1 -0
  97. package/types/client/native/opener.d.ts +1 -0
  98. package/types/client/native/preferences.d.ts +1 -0
  99. package/types/client/native/push.d.ts +1 -0
  100. package/types/client/native/screen-orientation.d.ts +1 -0
  101. package/types/client/native/screen.d.ts +1 -0
  102. package/types/client/native/secure-storage.d.ts +1 -0
  103. package/types/client/native/share.d.ts +1 -0
  104. package/types/client/native/single-instance.d.ts +1 -0
  105. package/types/client/native/splash-screen.d.ts +1 -0
  106. package/types/client/native/sqlite.d.ts +1 -0
  107. package/types/client/native/toast.d.ts +1 -0
  108. package/types/client/native/tray.d.ts +1 -0
  109. package/types/client/native/updates.d.ts +1 -0
  110. package/types/client/native/volume.d.ts +1 -0
  111. package/types/client/native/window-state.d.ts +1 -0
  112. package/types/client/native/window.d.ts +1 -0
  113. package/types/client/native.d.ts +14 -2
  114. package/types/client/resolveServerUrl.d.ts +1 -0
  115. package/types/client/router.d.ts +3 -0
  116. package/types/index.d.ts +112 -6
  117. package/types/native/desktop.d.ts +3 -0
  118. package/types/server/akanAppHeaders.d.ts +2 -0
  119. package/types/server/index.d.ts +1 -0
  120. package/types/server/lifecycle/orphanGroup.d.ts +7 -0
  121. package/types/server/nativeFile.d.ts +10 -0
  122. package/types/server/routing/apiRouter.d.ts +3 -1
  123. package/types/server/routing/hostAllowlist.d.ts +8 -0
  124. package/types/server/selfExec.d.ts +8 -0
  125. package/types/server/types.d.ts +3 -1
  126. package/types/service/ipcTypes.d.ts +16 -0
  127. package/types/signal/CrossSiteGuard.d.ts +1 -0
  128. package/types/ui/recipe/buttonRecipe.d.ts +2 -2
  129. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +57 -4
  130. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +48 -0
  131. package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +8 -0
  132. package/types/vendor/@akanjs/native/packages/cli/src/lib/prepare.d.ts +8 -2
  133. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +10 -0
  134. package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +24 -0
  135. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +24 -0
  136. package/types/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.d.ts +4 -1
  137. package/types/vendor/@akanjs/native/packages/cli/src/platforms/macos.d.ts +5 -0
  138. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +38 -0
  139. package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +8 -1
  140. package/types/vendor/@akanjs/native/packages/desktop/src/grants.d.ts +11 -0
  141. package/types/vendor/@akanjs/native/packages/desktop/src/main.d.ts +11 -0
  142. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +13 -2
  143. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +27 -0
  144. package/types/vendor/@akanjs/native/packages/desktop/src/relaunch.d.ts +21 -0
  145. package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +79 -0
  146. package/types/vendor/@akanjs/native/plugins/app/src/index.d.ts +5 -0
  147. package/types/vendor/@akanjs/native/plugins/file-picker/src/index.d.ts +20 -1
  148. package/types/vendor/@akanjs/native/plugins/push/src/index.d.ts +9 -0
  149. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +28 -0
  150. package/types/vendor/@akanjs/native/plugins/volume/src/common.d.ts +10 -0
  151. package/types/vendor/@akanjs/native/plugins/volume/src/desktop.d.ts +18 -0
  152. package/types/vendor/@akanjs/native/plugins/volume/src/index.d.ts +28 -0
  153. package/types/vendor/@akanjs/native/plugins/volume/src.d.ts +1 -0
  154. package/types/webkit/csrFrameDump.d.ts +42 -0
  155. package/types/webkit/nativeNavigation.d.ts +1 -0
  156. package/types/webkit/nativePurchase.d.ts +6 -2
  157. package/types/webkit/nativeUpdates.d.ts +8 -0
  158. package/ui/Agent/Attach.tsx +6 -2
  159. package/ui/CsrImage.tsx +3 -2
  160. package/ui/System/CSR.tsx +4 -0
  161. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -0
  162. package/vendor/@akanjs/native/native/desktop/Cargo.toml +1 -1
  163. package/vendor/@akanjs/native/native/desktop/src/lib.rs +161 -25
  164. package/vendor/@akanjs/native/native/desktop/src/volume.rs +184 -0
  165. package/vendor/@akanjs/native/native/desktop/src/win/mod.rs +3 -1
  166. package/vendor/@akanjs/native/native/desktop/src/win/volume.rs +71 -0
  167. package/vendor/@akanjs/native/packages/cli/src/api.ts +142 -12
  168. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +5 -3
  169. package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +10 -71
  170. package/vendor/@akanjs/native/packages/cli/src/config.ts +41 -0
  171. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +9 -2
  172. package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +58 -0
  173. package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +2 -0
  174. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +10 -2
  175. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +38 -2
  176. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +104 -0
  177. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +85 -5
  178. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +1 -0
  179. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +6 -4
  180. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +16 -0
  181. package/vendor/@akanjs/native/packages/cli/src/platforms/macos.ts +44 -1
  182. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +436 -0
  183. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +2 -0
  184. package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +20 -0
  185. package/vendor/@akanjs/native/packages/desktop/src/grants.ts +47 -0
  186. package/vendor/@akanjs/native/packages/desktop/src/host.ts +97 -11
  187. package/vendor/@akanjs/native/packages/desktop/src/main.ts +59 -11
  188. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +38 -4
  189. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +28 -0
  190. package/vendor/@akanjs/native/packages/desktop/src/relaunch.ts +159 -0
  191. package/vendor/@akanjs/native/packages/desktop/src/server.ts +493 -0
  192. package/vendor/@akanjs/native/plugins/app/android/AppPlugin.kt +14 -0
  193. package/vendor/@akanjs/native/plugins/app/native-plugin.json +2 -2
  194. package/vendor/@akanjs/native/plugins/app/src/desktop.ts +5 -0
  195. package/vendor/@akanjs/native/plugins/app/src/index.ts +6 -1
  196. package/vendor/@akanjs/native/plugins/app/src/web.ts +3 -0
  197. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +39 -2
  198. package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +36 -1
  199. package/vendor/@akanjs/native/plugins/push/android/PushPlugin.kt +9 -1
  200. package/vendor/@akanjs/native/plugins/push/ios/PushPlugin.swift +4 -0
  201. package/vendor/@akanjs/native/plugins/push/src/index.ts +10 -0
  202. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +464 -452
  203. package/vendor/@akanjs/native/plugins/volume/android/VolumePlugin.kt +107 -0
  204. package/vendor/@akanjs/native/plugins/volume/native-plugin.json +14 -0
  205. package/vendor/@akanjs/native/plugins/volume/src/common.ts +28 -0
  206. package/vendor/@akanjs/native/plugins/volume/src/desktop.ts +185 -0
  207. package/vendor/@akanjs/native/plugins/volume/src/index.ts +32 -0
  208. package/webkit/RenderLayer.tsx +71 -6
  209. package/webkit/csrFrameDump.ts +67 -0
  210. package/webkit/nativeNavigation.ts +17 -0
  211. package/webkit/nativePurchase.ts +12 -2
  212. package/webkit/nativeUpdates.ts +55 -0
  213. 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 === "akan-child") headers.set("location", `${parsed.pathname}${parsed.search}${parsed.hash}`);
967
+ if (parsed.hostname === AKAN_CHILD_HOST)
968
+ headers.set("location", `${parsed.pathname}${parsed.search}${parsed.hash}`);
951
969
  } catch {
952
970
  }
953
971
  }
@@ -1074,6 +1092,7 @@ export class AkanApp {
1074
1092
  child.upstream = message.upstream;
1075
1093
  child.wsUpstream = message.wsUpstream;
1076
1094
  child.healthPath = message.healthPath;
1095
+ if (message.crossSite) CrossSiteGuard.configure(message.crossSite);
1077
1096
  child.lastPongAtMono = performance.now();
1078
1097
  child.restartAttempts = 0;
1079
1098
 
@@ -1084,7 +1103,8 @@ export class AkanApp {
1084
1103
  this.#federationChildCache = null;
1085
1104
 
1086
1105
  const trafficChildren = [...this.#children.values()].filter((item) => item.role !== "batch");
1087
- if (child.role !== "batch" && trafficChildren.every((item) => item.ready)) {
1106
+
1107
+ if (!this.#stopping && child.role !== "batch" && trafficChildren.every((item) => item.ready)) {
1088
1108
  process.send?.({ type: "backend-ready", pid: process.pid } satisfies AkanIpcMessage);
1089
1109
  }
1090
1110
  if ([...this.#children.values()].every((item) => item.ready)) {
@@ -11,6 +11,9 @@ const HOP_BY_HOP_HEADERS = new Set([
11
11
  "upgrade",
12
12
  ]);
13
13
 
14
+ /** The Host every request the gateway hands a replica carries; a browser never sends it to a replica's port. */
15
+ export const AKAN_CHILD_HOST = "akan-child";
16
+
14
17
  export interface ProxyClientPeer {
15
18
  address: string;
16
19
  port: number;
@@ -44,6 +47,6 @@ export function makeAkanChildProxyHeaders(req: Request, childIdx: number, peer?:
44
47
  if (!headers.has("x-request-id") && process.env.AKAN_BENCH_SKIP_REQUEST_ID !== "1") {
45
48
  headers.set("x-request-id", crypto.randomUUID());
46
49
  }
47
- headers.set("host", "akan-child");
50
+ headers.set("host", AKAN_CHILD_HOST);
48
51
  return headers;
49
52
  }
@@ -19,11 +19,13 @@ import { AgentRelayAccess } from "../signal/guards";
19
19
  import { createOpenApiDocument } from "../signal/openapi";
20
20
  import { FetchSerializer } from "../signal/serializer";
21
21
  import { SignalContext } from "../signal/signalContext";
22
+ import { AKAN_CHILD_HOST } from "./akanAppHeaders";
22
23
  import type { AkanLib, AkanLibProps } from "./akanLib";
23
24
  import { BinaryPubsub } from "./binaryPubsub";
24
25
  import { DevtoolsRouter } from "./devtools";
25
26
  import { DiLifecycle } from "./di/diLifecycle";
26
27
  import type { HmrWsData, HmrWsHub } from "./hmr/wsHub";
28
+ import { OrphanGroup } from "./lifecycle/orphanGroup";
27
29
  import { isPortInUseError } from "./lifecycle/portInUse";
28
30
  import { resolveRuntimeDir } from "./lifecycle/runtimeDir";
29
31
  import { ShutdownManager } from "./lifecycle/shutdownManager";
@@ -42,6 +44,7 @@ import { WebProxyRunner } from "./proxy";
42
44
  import { SignalResolver } from "./resolver";
43
45
  import { type ApiRouteInputs, ApiRouter } from "./routing/apiRouter";
44
46
  import type { AppWsData } from "./routing/appWsData";
47
+ import { HostAllowlist } from "./routing/hostAllowlist";
45
48
  import { createSoloAppRoutes } from "./routing/soloAppRoutes";
46
49
  import {
47
50
  getWebConfigFromEnv,
@@ -167,6 +170,7 @@ export class AkanServer {
167
170
  #ops: OpsRoute | null | undefined;
168
171
  #lastMetrics: AkanMetricsReport = {};
169
172
  #stopping: Promise<void> | null = null;
173
+ #orphanExiting = false;
170
174
  constructor(
171
175
  name = "AkanServer",
172
176
  env: BackendEnv = {},
@@ -392,7 +396,10 @@ export class AkanServer {
392
396
  }),
393
397
  data: {},
394
398
  } as Bun.WebSocketHandler<AppWsData | HmrWsData>;
395
- const buildRoutes = (upgradeAppWs: ApiRouteInputs["upgradeAppWs"]) =>
399
+
400
+ const hostAllowlist = unix ? null : HostAllowlist.fromEnv();
401
+ const hostname = process.env.AKAN_LISTEN_HOST || undefined;
402
+ const buildRoutes = (upgradeAppWs: ApiRouteInputs["upgradeAppWs"], allowlist: HostAllowlist | null) =>
396
403
  ApiRouter.buildRoutes({
397
404
  prefix: this.prefix,
398
405
  websocketPrefix: this.websocketPrefix,
@@ -402,19 +409,25 @@ export class AkanServer {
402
409
  renderEnvRoutes,
403
410
  upgradeAppWs,
404
411
  webProxyRunner,
412
+ hostAllowlist: allowlist,
405
413
  });
406
414
  this.#server = Bun.serve({
407
415
  idleTimeout: 0,
408
- ...(unix ? { unix } : { port }),
409
- routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false),
416
+ ...(unix ? { unix } : { port, hostname }),
417
+ routes: buildRoutes((req, data) => this.#server?.upgrade(req, { data }) ?? false, hostAllowlist),
410
418
  websocket: websocketHandlers,
411
419
  } as Parameters<typeof Bun.serve>[0]);
412
420
  if (unix && process.env.AKAN_CHILD_WS_PORT) {
413
421
  const preferredWsPort = Number(process.env.AKAN_CHILD_WS_PORT);
422
+
414
423
  const wsServeOptions = (port: number) => ({
415
424
  idleTimeout: 0,
416
425
  port,
417
- routes: buildRoutes((req, data) => this.#wsServer?.upgrade(req, { data }) ?? false),
426
+ hostname: "127.0.0.1",
427
+ routes: buildRoutes(
428
+ (req, data) => this.#wsServer?.upgrade(req, { data }) ?? false,
429
+ new HostAllowlist([AKAN_CHILD_HOST]),
430
+ ),
418
431
  websocket: websocketHandlers,
419
432
  });
420
433
  try {
@@ -467,6 +480,7 @@ export class AkanServer {
467
480
  upstream: unix ? { type: "unix", socketPath: unix } : { type: "tcp", host: "127.0.0.1", port: Number(port) },
468
481
  wsUpstream: typeof wsPort === "number" ? { type: "tcp", host: "127.0.0.1", port: wsPort } : undefined,
469
482
  healthPath: "/_akan/app/child-health",
483
+ crossSite: CrossSiteGuard.option(),
470
484
  } satisfies AkanIpcMessage);
471
485
  await this.#di.runSchedulerInit();
472
486
  ShutdownManager.register(this.logger, () => this.stop());
@@ -550,10 +564,16 @@ export class AkanServer {
550
564
 
551
565
  #handleParentDisconnect() {
552
566
  this.logger.warn("Parent IPC channel closed; shutting down to avoid an orphaned replica");
553
- setTimeout(() => process.exit(1), this.shutdownTimeoutMs + 1_000);
567
+ setTimeout(() => this.#exitOrphaned(1), this.shutdownTimeoutMs + 1_000);
554
568
  void this.stop()
555
- .then(() => process.exit(0))
556
- .catch(() => process.exit(1));
569
+ .then(() => this.#exitOrphaned(0))
570
+ .catch(() => this.#exitOrphaned(1));
571
+ }
572
+
573
+ #exitOrphaned(code: number) {
574
+ if (this.#orphanExiting) return;
575
+ this.#orphanExiting = true;
576
+ OrphanGroup.exit(code);
557
577
  }
558
578
 
559
579
  #handleIpcMessage(message: AkanIpcMessage) {
package/server/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./console";
7
7
  export * from "./decorators";
8
8
  export * from "./devtools";
9
9
  export type { ChangeBatch, ChangeKind } from "./hmr/wsHub";
10
+ export * from "./nativeFile";
10
11
  export * from "./oauth";
11
12
  export * from "./processMetricsCollector";
12
13
  export * from "./proxy";
@@ -0,0 +1,21 @@
1
+ import { SelfExec } from "../selfExec";
2
+
3
+ export class OrphanGroup {
4
+ static exit(code: number, { leads = SelfExec.carried && process.platform !== "win32", graceMs = 1_000 } = {}) {
5
+ if (!leads) process.exit(code);
6
+ process.removeAllListeners("SIGTERM");
7
+ process.on("SIGTERM", () => undefined);
8
+ OrphanGroup.#signal("SIGTERM");
9
+ setTimeout(() => {
10
+ OrphanGroup.#signal("SIGKILL");
11
+ process.exit(code);
12
+ }, graceMs);
13
+ }
14
+
15
+ static #signal(signal: NodeJS.Signals) {
16
+ try {
17
+ process.kill(-process.pid, signal);
18
+ } catch {
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,107 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { getEnv } from "akanjs/base";
6
+ import type { AkanIpcMessage } from "akanjs/service";
7
+
8
+ export type NativeFileMode = "read" | "write" | "folder";
9
+
10
+ type Resolved = Extract<AkanIpcMessage, { type: "file.resolved" }>;
11
+
12
+ export class NativeFile {
13
+ static timeoutMs = 5_000;
14
+ static operationMode: () => string = () => getEnv().operationMode;
15
+ static readonly #waiting = new Map<string, (answer: Resolved) => void>();
16
+ static #listener: ((message: unknown) => void) | null = null;
17
+
18
+ /** The path behind `grant`, which the user must have given for `mode`. */
19
+ static async resolve(grant: string, mode: NativeFileMode): Promise<string> {
20
+ const found = grant.startsWith("dev:") ? NativeFile.#devGrant(grant) : await NativeFile.#ask(grant);
21
+ if (found.mode !== mode) throw new Error(`The file was granted to ${found.mode}, not to ${mode}.`);
22
+ return found.path;
23
+ }
24
+
25
+ /** A path inside a folder grant; one that climbs out of the folder is refused. */
26
+ static async resolveIn(grant: string, relative: string): Promise<string> {
27
+ const folder = await NativeFile.resolve(grant, "folder");
28
+ const target = path.resolve(folder, relative);
29
+ NativeFile.#assertInside(folder, target, relative);
30
+
31
+ NativeFile.#assertInside(NativeFile.#realPath(folder), NativeFile.#realPath(target), relative);
32
+ return target;
33
+ }
34
+
35
+ static #assertInside(folder: string, target: string, relative: string) {
36
+ const inside = path.relative(folder, target);
37
+ if (inside === ".." || inside.startsWith(`..${path.sep}`) || path.isAbsolute(inside))
38
+ throw new Error(`${relative} is outside the folder the user granted.`);
39
+ }
40
+
41
+ static #realPath(target: string): string {
42
+ const rest: string[] = [];
43
+ for (let current = target; ; current = path.dirname(current)) {
44
+ try {
45
+ return path.join(realpathSync.native(current), ...rest);
46
+ } catch (error) {
47
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT" || path.dirname(current) === current) throw error;
48
+ if (NativeFile.#exists(current)) throw new Error(`${target} is a link to nothing.`);
49
+ rest.unshift(path.basename(current));
50
+ }
51
+ }
52
+ }
53
+
54
+ static #exists(entry: string) {
55
+ try {
56
+ lstatSync(entry);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ static #ask(grant: string): Promise<{ path: string; mode: NativeFileMode }> {
64
+ const send = process.send?.bind(process);
65
+ if (!send) return Promise.reject(new Error("A file grant resolves only inside the desktop app that gave it."));
66
+ NativeFile.#listen();
67
+ const id = crypto.randomUUID();
68
+ return new Promise((resolve, reject) => {
69
+ const timer = setTimeout(() => {
70
+ NativeFile.#waiting.delete(id);
71
+ reject(new Error("The desktop app did not answer for the file grant."));
72
+ }, NativeFile.timeoutMs);
73
+ NativeFile.#waiting.set(id, (answer) => {
74
+ clearTimeout(timer);
75
+ NativeFile.#waiting.delete(id);
76
+ if (answer.path && answer.mode) resolve({ path: answer.path, mode: answer.mode });
77
+ else reject(new Error(answer.error ?? "The desktop app refused the file grant."));
78
+ });
79
+ send({ type: "file.resolve", id, grant } satisfies AkanIpcMessage);
80
+ });
81
+ }
82
+
83
+ static #listen() {
84
+ if (NativeFile.#listener) return;
85
+ NativeFile.#listener = (message) => {
86
+ const answer = message as AkanIpcMessage | null;
87
+ if (answer?.type === "file.resolved") NativeFile.#waiting.get(answer.id)?.(answer);
88
+ };
89
+ process.on("message", NativeFile.#listener);
90
+ }
91
+
92
+ static #devGrant(grant: string): { path: string; mode: NativeFileMode } {
93
+
94
+ if (NativeFile.operationMode() !== "local")
95
+ throw new Error("A dev build's file grant resolves only on akan start, in operationMode local.");
96
+ const [, mode, encoded = "", signature = ""] = grant.split(":");
97
+ if (mode !== "read" && mode !== "write" && mode !== "folder") throw new Error("The file grant is malformed.");
98
+ const key = readFileSync(
99
+ process.env.AKAN_NATIVE_DEV_GRANT_KEY || path.join(homedir(), ".akan", "native", "dev-file-grant.key"),
100
+ );
101
+ const expected = createHmac("sha256", key).update(`${mode}:${encoded}`).digest();
102
+ const given = Buffer.from(signature, "base64url");
103
+ if (given.length !== expected.length || !timingSafeEqual(given, expected))
104
+ throw new Error("The file grant was not signed by this computer's dev build.");
105
+ return { path: Buffer.from(encoded, "base64url").toString(), mode };
106
+ }
107
+ }
@@ -1,5 +1,6 @@
1
1
  import { Logger } from "akanjs/common";
2
2
  import { getEnv } from "../../base/baseEnv";
3
+ import { SelfExec } from "../selfExec";
3
4
  import type { AppDetailInfo } from "./appInfo";
4
5
  import { OpsTokenVerifier } from "./opsToken";
5
6
  import { type SnapshotJobRequest, SnapshotJobs } from "./snapshotJobs";
@@ -65,8 +66,8 @@ export class OpsRoute {
65
66
  const args = ["ops", "snapshot", "--id", request.id, "--out", dir, "--db", sources.main, "--json"];
66
67
  if (sources.solid) args.push("--solid-db", sources.solid);
67
68
  if (request.includeSolid) args.push("--include-solid");
68
- const proc = Bun.spawn([process.execPath, Bun.main, ...args], {
69
- env: process.env,
69
+ const proc = Bun.spawn(SelfExec.command(Bun.main, ...args), {
70
+ env: SelfExec.env(),
70
71
  stdout: "pipe",
71
72
  stderr: "pipe",
72
73
  });
@@ -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
- return webProxyRunner
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" ? wrap(handler as RouteHandler) : handler,
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
- appId: string;
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[];
@@ -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.appId}`],
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) => association.domains.length > 0 && (association.androidSha256CertFingerprints?.length ?? 0) > 0,
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.appId}${suffix}`,
1193
+ package_name: `${association.androidAppId}${suffix}`,
1191
1194
  sha256_cert_fingerprints: association.androidSha256CertFingerprints,
1192
1195
  },
1193
1196
  })),
@@ -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) => {
@@ -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: BackendEnv;
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, ...targetModule.env },
108
+ env: { ...env, ...testingEnv },
103
109
  fetch: targetModule.fetch,
104
110
  libs: [...dependencyModules.map((mod) => mod.lib), targetModule.lib],
105
111
  };
@@ -77,3 +77,4 @@ export declare const getWsPrefix: () => string;
77
77
  export declare const resetEnvCache: () => void;
78
78
  /** Cached after the first call. */
79
79
  export declare const getEnv: () => ClientEnv;
80
+ export declare const getServerOrigin: () => string;
@@ -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";