akanjs 3.0.2 → 3.1.0-rc.0

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 (378) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/base/baseEnv.ts +0 -4
  3. package/base/primitiveRegistry.ts +2 -2
  4. package/client/cookie.ts +5 -1
  5. package/client/csrLocation.ts +122 -0
  6. package/client/csrTypes.ts +32 -4
  7. package/client/device.ts +2 -1
  8. package/client/frameConfig.ts +78 -7
  9. package/client/index.ts +3 -0
  10. package/client/makePageProto.tsx +3 -2
  11. package/client/native/external-view.ts +1 -0
  12. package/client/route/RootLayoutDefinition.ts +7 -1
  13. package/client/route/RouteHead.ts +108 -0
  14. package/client/route/index.ts +1 -0
  15. package/client/router.ts +36 -23
  16. package/client/rscNavigation.ts +8 -2
  17. package/client/viewportMeta.ts +60 -0
  18. package/client/webManifest.ts +18 -0
  19. package/common/Logger.ts +7 -2
  20. package/common/PipelineNames.ts +31 -0
  21. package/common/PipelinePattern.ts +70 -0
  22. package/common/PipelinePlanSchema.ts +490 -0
  23. package/common/backoff.ts +14 -0
  24. package/common/index.ts +39 -0
  25. package/common/pipelinePlan.ts +259 -0
  26. package/common/routeConvention.ts +1 -0
  27. package/common/types.ts +17 -0
  28. package/constant/constantRegistry.ts +26 -6
  29. package/dictionary/base.dictionary.ts +8 -0
  30. package/dictionary/trans.ts +9 -1
  31. package/document/into.ts +5 -0
  32. package/fetch/client/fetchClient.ts +135 -31
  33. package/fetch/client/httpClient.ts +45 -0
  34. package/fetch/client/wsClient.ts +125 -16
  35. package/fetch/fetchType/buildFetch.type.ts +1 -0
  36. package/index.ts +243 -10
  37. package/package.json +1 -5
  38. package/server/akanApp.ts +5 -2
  39. package/server/akanServer.ts +44 -8
  40. package/server/artifact/builderRpc.ts +8 -3
  41. package/server/artifact/ipcTypes.ts +5 -2
  42. package/server/di/diLifecycle.ts +39 -3
  43. package/server/di/utils.ts +28 -1
  44. package/server/ed25519Key.ts +48 -0
  45. package/server/index.ts +1 -0
  46. package/server/machine/index.ts +2 -0
  47. package/server/machine/machineAccount.ts +27 -0
  48. package/server/machine/machineToken.ts +151 -0
  49. package/server/mcp/McpApps.ts +256 -0
  50. package/server/mcp/McpDispatcher.ts +31 -7
  51. package/server/mcp/McpRouter.ts +209 -46
  52. package/server/ops/opsToken.ts +3 -10
  53. package/server/resolver/database.resolver.ts +3 -0
  54. package/server/resolver/signal.resolver.ts +40 -3
  55. package/server/routeElementComposer.tsx +78 -2
  56. package/server/routeState.ts +6 -0
  57. package/server/routeTreeBuilder.ts +26 -9
  58. package/server/routing/apiRouter.ts +6 -3
  59. package/server/routing/roomReplay.ts +88 -0
  60. package/server/rscClient.tsx +53 -8
  61. package/server/rscWorker.tsx +141 -27
  62. package/server/rscWorkerHost.ts +18 -5
  63. package/server/sitemap.ts +14 -0
  64. package/server/ssrFromRscRenderer.tsx +12 -6
  65. package/server/ssrTypes.ts +3 -1
  66. package/server/systemPageDocument.tsx +38 -30
  67. package/server/types.tsx +2 -0
  68. package/server/webRouter.ts +74 -5
  69. package/service/injectInfo.ts +9 -1
  70. package/service/ipcTypes.ts +2 -1
  71. package/service/predefinedAdaptor/database.adaptor.ts +1 -146
  72. package/service/predefinedAdaptor/queue.adaptor.ts +44 -6
  73. package/service/predefinedAdaptor/queueBackoff.ts +12 -0
  74. package/service/predefinedAdaptor/solidQueue.adaptor.ts +17 -13
  75. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +44 -2
  76. package/service/predefinedAdaptor/sql/dialect/postgres.ts +3 -0
  77. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +5 -2
  78. package/service/predefinedAdaptor/sql/search/fts5.ts +1 -1
  79. package/service/predefinedAdaptor/sql/types.ts +8 -6
  80. package/service/predefinedAdaptor/sql/values.ts +0 -23
  81. package/service/serve.ts +5 -1
  82. package/signal/agent.signal.ts +1 -1
  83. package/signal/base.signal.ts +2 -2
  84. package/signal/endpointIdempotency.ts +53 -0
  85. package/signal/internalArg.ts +14 -0
  86. package/signal/mcp/McpDocument.ts +38 -1
  87. package/signal/mcp/mcpProtocol.ts +18 -0
  88. package/signal/signalContext.ts +58 -30
  89. package/signal/signalRegistry.ts +14 -4
  90. package/signal/types.ts +18 -3
  91. package/store/agent/StoreSurfaceSource.ts +1 -1
  92. package/store/agentic/StToolBuilder.ts +3 -0
  93. package/test/conformance.ts +9 -33
  94. package/types/base/baseEnv.d.ts +0 -4
  95. package/types/base/primitiveRegistry.d.ts +1 -1
  96. package/types/client/csrLocation.d.ts +32 -0
  97. package/types/client/csrTypes.d.ts +40 -4
  98. package/types/client/frameConfig.d.ts +14 -2
  99. package/types/client/index.d.ts +3 -0
  100. package/types/client/native/external-view.d.ts +1 -0
  101. package/types/client/route/RootLayoutDefinition.d.ts +2 -1
  102. package/types/client/route/RouteHead.d.ts +13 -0
  103. package/types/client/route/index.d.ts +1 -0
  104. package/types/client/router.d.ts +6 -0
  105. package/types/client/rscNavigation.d.ts +4 -0
  106. package/types/client/viewportMeta.d.ts +18 -0
  107. package/types/client/webManifest.d.ts +4 -0
  108. package/types/common/PipelineNames.d.ts +6 -0
  109. package/types/common/PipelinePattern.d.ts +14 -0
  110. package/types/common/PipelinePlanSchema.d.ts +9 -0
  111. package/types/common/backoff.d.ts +10 -0
  112. package/types/common/index.d.ts +5 -0
  113. package/types/common/pipelinePlan.d.ts +224 -0
  114. package/types/common/types.d.ts +16 -0
  115. package/types/constant/constantRegistry.d.ts +5 -2
  116. package/types/dictionary/base.dictionary.d.ts +1 -1
  117. package/types/dictionary/dictionary.d.ts +8 -8
  118. package/types/document/into.d.ts +9 -0
  119. package/types/fetch/client/fetchClient.d.ts +31 -4
  120. package/types/fetch/client/httpClient.d.ts +8 -0
  121. package/types/fetch/client/wsClient.d.ts +26 -2
  122. package/types/fetch/fetchType/buildFetch.type.d.ts +1 -0
  123. package/types/index.d.ts +230 -9
  124. package/types/server/akanServer.d.ts +16 -1
  125. package/types/server/artifact/builderRpc.d.ts +7 -2
  126. package/types/server/artifact/ipcTypes.d.ts +5 -1
  127. package/types/server/di/utils.d.ts +1 -0
  128. package/types/server/ed25519Key.d.ts +13 -0
  129. package/types/server/index.d.ts +1 -0
  130. package/types/server/machine/index.d.ts +2 -0
  131. package/types/server/machine/machineAccount.d.ts +5 -0
  132. package/types/server/machine/machineToken.d.ts +29 -0
  133. package/types/server/machine.d.ts +1 -0
  134. package/types/server/mcp/McpApps.d.ts +50 -0
  135. package/types/server/mcp/McpDispatcher.d.ts +4 -1
  136. package/types/server/mcp/McpRouter.d.ts +4 -0
  137. package/types/server/resolver/signal.resolver.d.ts +2 -1
  138. package/types/server/routeElementComposer.d.ts +24 -4
  139. package/types/server/routeState.d.ts +4 -1
  140. package/types/server/routing/roomReplay.d.ts +24 -0
  141. package/types/server/rscWorkerHost.d.ts +5 -2
  142. package/types/server/sitemap.d.ts +2 -0
  143. package/types/server/ssrFromRscRenderer.d.ts +3 -3
  144. package/types/server/ssrTypes.d.ts +4 -1
  145. package/types/server/systemPageDocument.d.ts +2 -0
  146. package/types/server/types.d.ts +4 -1
  147. package/types/server/webRouter.d.ts +3 -1
  148. package/types/service/ipcTypes.d.ts +2 -0
  149. package/types/service/predefinedAdaptor/database.adaptor.d.ts +1 -24
  150. package/types/service/predefinedAdaptor/queue.adaptor.d.ts +10 -3
  151. package/types/service/predefinedAdaptor/queueBackoff.d.ts +4 -0
  152. package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +3 -3
  153. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -1
  154. package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
  155. package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
  156. package/types/service/predefinedAdaptor/sql/search/fts5.d.ts +1 -1
  157. package/types/service/predefinedAdaptor/sql/types.d.ts +9 -5
  158. package/types/service/predefinedAdaptor/sql/values.d.ts +0 -3
  159. package/types/service/serve.d.ts +3 -0
  160. package/types/signal/endpointIdempotency.d.ts +7 -0
  161. package/types/signal/internalArg.d.ts +10 -0
  162. package/types/signal/mcp/McpDocument.d.ts +8 -0
  163. package/types/signal/mcp/mcpProtocol.d.ts +17 -0
  164. package/types/signal/signalContext.d.ts +5 -3
  165. package/types/signal/signalRegistry.d.ts +4 -4
  166. package/types/signal/types.d.ts +21 -1
  167. package/types/store/agentic/StToolBuilder.d.ts +2 -0
  168. package/types/test/conformance.d.ts +7 -10
  169. package/types/ui/Agent/Chat.d.ts +5 -1
  170. package/types/ui/Agent/EmbedApproval.d.ts +1 -0
  171. package/types/ui/Agent/index.d.ts +3 -0
  172. package/types/ui/Agent/useKeyboardFrame.d.ts +2 -0
  173. package/types/ui/Agent/useScreenActions.d.ts +18 -0
  174. package/types/ui/BottomSheet.d.ts +1 -0
  175. package/types/ui/Constant/Doc.d.ts +11 -17
  176. package/types/ui/Constant/index.d.ts +7 -2
  177. package/types/ui/Constant/schemaDoc.d.ts +11 -0
  178. package/types/ui/Dialog/Modal.d.ts +6 -1
  179. package/types/ui/Dialog/index.d.ts +1 -1
  180. package/types/ui/Dialog/modalSurface.d.ts +12 -0
  181. package/types/ui/Layout/BottomTab.d.ts +11 -2
  182. package/types/ui/Layout/FrameSlotFallback.d.ts +9 -0
  183. package/types/ui/Layout/Navbar.d.ts +4 -1
  184. package/types/ui/Layout/TopInset.d.ts +1 -1
  185. package/types/ui/Layout/index.d.ts +3 -3
  186. package/types/ui/Link/Action.d.ts +5 -1
  187. package/types/ui/Link/Anchor.d.ts +5 -3
  188. package/types/ui/Link/activePath.d.ts +13 -0
  189. package/types/ui/Link/index.d.ts +1 -1
  190. package/types/ui/Modal.d.ts +6 -1
  191. package/types/ui/Model/EditModal.d.ts +6 -1
  192. package/types/ui/Model/New.d.ts +6 -1
  193. package/types/ui/Model/ViewEditModal.d.ts +6 -1
  194. package/types/ui/Model/ViewModal.d.ts +6 -1
  195. package/types/ui/Model/newFormOrigin.d.ts +7 -0
  196. package/types/ui/Portal.d.ts +3 -1
  197. package/types/ui/Reference/origin.d.ts +12 -0
  198. package/types/ui/Signal/Doc.d.ts +23 -18
  199. package/types/ui/Signal/Listener.d.ts +2 -6
  200. package/types/ui/Signal/Object.d.ts +6 -10
  201. package/types/ui/Signal/Response.d.ts +3 -6
  202. package/types/ui/Signal/RestApi.d.ts +11 -15
  203. package/types/ui/Signal/WebSocket.d.ts +11 -21
  204. package/types/ui/Signal/endpointEntries.d.ts +8 -0
  205. package/types/ui/Signal/index.d.ts +35 -12
  206. package/types/ui/Signal/signalText.d.ts +178 -0
  207. package/types/ui/System/CSR.d.ts +2 -2
  208. package/types/ui/System/Client.d.ts +4 -3
  209. package/types/ui/System/Common.d.ts +27 -6
  210. package/types/ui/System/SSR.d.ts +6 -6
  211. package/types/ui/Tooltip.d.ts +4 -2
  212. package/types/ui/index.d.ts +2 -1
  213. package/types/ui/overlayLayer.d.ts +6 -0
  214. package/types/ui/overlayPosition.d.ts +17 -0
  215. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +14 -3
  216. package/types/vendor/@akanjs/native/packages/cli/src/commands/update.d.ts +1 -1
  217. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +30 -5
  218. package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +8 -0
  219. package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +5 -0
  220. package/types/vendor/@akanjs/native/packages/cli/src/lib/distribution.d.ts +22 -0
  221. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +4 -0
  222. package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +16 -10
  223. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +51 -7
  224. package/types/vendor/@akanjs/native/packages/cli/src/lib/vectors.d.ts +1 -1
  225. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +7 -0
  226. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows.d.ts +5 -0
  227. package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +36 -0
  228. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +2 -0
  229. package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +11 -0
  230. package/types/vendor/@akanjs/native/plugins/external-view/src/desktop.d.ts +17 -0
  231. package/types/vendor/@akanjs/native/plugins/external-view/src/index.d.ts +132 -0
  232. package/types/vendor/@akanjs/native/plugins/external-view/src.d.ts +1 -0
  233. package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +8 -0
  234. package/types/vendor/@akanjs/native/plugins/window/src/desktop.d.ts +4 -1
  235. package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +49 -0
  236. package/types/vendor/use-agentic/types.d.ts +2 -0
  237. package/types/webkit/CsrStack.d.ts +1 -1
  238. package/types/webkit/McpAppAgent.d.ts +14 -0
  239. package/types/webkit/McpAppApprovals.d.ts +8 -0
  240. package/types/webkit/McpAppBridge.d.ts +101 -0
  241. package/types/webkit/McpAppView.d.ts +21 -0
  242. package/types/webkit/index.d.ts +1 -0
  243. package/types/webkit/useLocation.d.ts +1 -1
  244. package/types/webkit/useVisualViewportFrame.d.ts +13 -0
  245. package/ui/Agent/Chat.tsx +29 -10
  246. package/ui/Agent/Composer.tsx +2 -1
  247. package/ui/Agent/EmbedApproval.tsx +17 -0
  248. package/ui/Agent/RichInput.tsx +1 -1
  249. package/ui/Agent/index.ts +2 -0
  250. package/ui/Agent/useKeyboardFrame.ts +10 -0
  251. package/ui/Agent/useScreenActions.ts +45 -0
  252. package/ui/BottomSheet.tsx +82 -53
  253. package/ui/Constant/Doc.tsx +113 -60
  254. package/ui/Constant/index.ts +2 -2
  255. package/ui/Constant/schemaDoc.ts +86 -26
  256. package/ui/Dialog/LegacyModal.tsx +53 -52
  257. package/ui/Dialog/Modal.tsx +93 -49
  258. package/ui/Dialog/modalSurface.ts +61 -0
  259. package/ui/DragAction.tsx +1 -1
  260. package/ui/DraggableList.tsx +1 -1
  261. package/ui/Dropdown.tsx +4 -6
  262. package/ui/KeyboardAvoiding.tsx +1 -1
  263. package/ui/Layout/BottomInset.tsx +20 -14
  264. package/ui/Layout/BottomTab.tsx +42 -16
  265. package/ui/Layout/FrameSlotFallback.tsx +59 -0
  266. package/ui/Layout/Navbar.tsx +37 -11
  267. package/ui/Layout/Sider.tsx +1 -1
  268. package/ui/Layout/TopInset.tsx +15 -14
  269. package/ui/Link/Action.tsx +15 -5
  270. package/ui/Link/Anchor.tsx +25 -13
  271. package/ui/Link/activePath.ts +25 -0
  272. package/ui/Link/index.tsx +1 -1
  273. package/ui/Load/Pagination.tsx +1 -1
  274. package/ui/Load/Units.tsx +1 -1
  275. package/ui/Loading/ProgressBar.tsx +1 -1
  276. package/ui/Modal.tsx +9 -0
  277. package/ui/Model/EditModal.tsx +16 -1
  278. package/ui/Model/New.tsx +25 -2
  279. package/ui/Model/NewWrapper_Client.tsx +2 -0
  280. package/ui/Model/ViewEditModal.tsx +9 -0
  281. package/ui/Model/ViewModal.tsx +16 -1
  282. package/ui/Model/newFormOrigin.ts +45 -0
  283. package/ui/Popconfirm.tsx +5 -7
  284. package/ui/Portal.tsx +4 -7
  285. package/ui/Reference/origin.ts +31 -0
  286. package/ui/ScreenNavigator.tsx +1 -1
  287. package/ui/Select.tsx +2 -7
  288. package/ui/Signal/Arg.tsx +2 -2
  289. package/ui/Signal/Doc.tsx +125 -63
  290. package/ui/Signal/Endpoint.tsx +11 -10
  291. package/ui/Signal/Listener.tsx +7 -10
  292. package/ui/Signal/Object.tsx +7 -14
  293. package/ui/Signal/Response.tsx +24 -24
  294. package/ui/Signal/RestApi.tsx +63 -52
  295. package/ui/Signal/WebSocket.tsx +65 -72
  296. package/ui/Signal/endpointEntries.ts +25 -0
  297. package/ui/Signal/index.ts +23 -6
  298. package/ui/Signal/signalText.ts +59 -0
  299. package/ui/System/CSR.tsx +34 -8
  300. package/ui/System/Client.tsx +36 -5
  301. package/ui/System/Common.tsx +76 -32
  302. package/ui/System/SSR.tsx +73 -31
  303. package/ui/Toast.tsx +1 -1
  304. package/ui/Tooltip.tsx +84 -30
  305. package/ui/index.ts +11 -1
  306. package/ui/overlayLayer.ts +25 -2
  307. package/ui/overlayPosition.ts +90 -0
  308. package/ui/recipe/inputRecipe.ts +8 -1
  309. package/ui/styles.css +152 -0
  310. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +22 -2
  311. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +37 -0
  312. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdateReports.kt +169 -0
  313. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdates.kt +110 -38
  314. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +20 -0
  315. package/vendor/@akanjs/native/native/desktop/Cargo.lock +16 -16
  316. package/vendor/@akanjs/native/native/desktop/src/external.rs +651 -0
  317. package/vendor/@akanjs/native/native/desktop/src/json.rs +23 -0
  318. package/vendor/@akanjs/native/native/desktop/src/lib.rs +228 -5
  319. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeApp.swift +2 -1
  320. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +47 -1
  321. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdateReports.swift +145 -0
  322. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdates.swift +112 -43
  323. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +18 -0
  324. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +24 -2
  325. package/vendor/@akanjs/native/packages/cli/src/api.ts +22 -6
  326. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +24 -8
  327. package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +11 -9
  328. package/vendor/@akanjs/native/packages/cli/src/config.ts +35 -7
  329. package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +2 -0
  330. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +6 -4
  331. package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +11 -1
  332. package/vendor/@akanjs/native/packages/cli/src/lib/distribution.ts +73 -0
  333. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +15 -3
  334. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +70 -23
  335. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +159 -23
  336. package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +2 -0
  337. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +9 -4
  338. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +1 -1
  339. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +4 -2
  340. package/vendor/@akanjs/native/packages/cli/src/platforms/ios.ts +9 -4
  341. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +25 -11
  342. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +14 -3
  343. package/vendor/@akanjs/native/packages/core/src/kernel.ts +49 -0
  344. package/vendor/@akanjs/native/packages/core/vectors/updates.json +31 -0
  345. package/vendor/@akanjs/native/packages/desktop/src/main.ts +3 -0
  346. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +2 -0
  347. package/vendor/@akanjs/native/packages/desktop/src/server.ts +70 -6
  348. package/vendor/@akanjs/native/plugins/external-view/native-plugin.json +23 -0
  349. package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +321 -0
  350. package/vendor/@akanjs/native/plugins/external-view/src/index.ts +106 -0
  351. package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +133 -41
  352. package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +101 -36
  353. package/vendor/@akanjs/native/plugins/updates/native-plugin.json +4 -2
  354. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +15 -1
  355. package/vendor/@akanjs/native/plugins/updates/src/index.ts +7 -1
  356. package/vendor/@akanjs/native/plugins/window/native-plugin.json +4 -0
  357. package/vendor/@akanjs/native/plugins/window/src/desktop.ts +68 -1
  358. package/vendor/@akanjs/native/plugins/window/src/index.ts +47 -0
  359. package/vendor/use-agentic/ToolRunner.ts +8 -1
  360. package/vendor/use-agentic/types.ts +2 -0
  361. package/webkit/CsrRouteTable.ts +1 -0
  362. package/webkit/CsrStack.ts +9 -2
  363. package/webkit/McpAppAgent.ts +138 -0
  364. package/webkit/McpAppApprovals.ts +56 -0
  365. package/webkit/McpAppBridge.ts +291 -0
  366. package/webkit/McpAppView.ts +258 -0
  367. package/webkit/bootCsr.tsx +54 -5
  368. package/webkit/index.ts +1 -0
  369. package/webkit/useCsrValues.ts +16 -10
  370. package/webkit/useFrameRuntime.ts +10 -1
  371. package/webkit/useLocation.ts +4 -3
  372. package/webkit/useVisualViewportFrame.tsx +52 -0
  373. package/service/predefinedAdaptor/sql/driver/libsql.ts +0 -45
  374. package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +0 -20
  375. package/types/ui/Agent/useKeyboardInset.d.ts +0 -1
  376. package/types/ui/ServerPortal.d.ts +0 -15
  377. package/ui/Agent/useKeyboardInset.ts +0 -19
  378. package/ui/ServerPortal.tsx +0 -49
@@ -3,10 +3,14 @@ package com.akanjs.plugins.updates
3
3
  import android.os.Handler
4
4
  import android.os.Looper
5
5
  import com.akanjs.runtime.AkanNativeErrorCode
6
+ import com.akanjs.runtime.AkanNativeKernel
6
7
  import com.akanjs.runtime.AkanNativePluginContext
7
8
  import com.akanjs.runtime.AkanNativeReply
8
9
  import com.akanjs.runtime.AkanNativeUpdateConfig
10
+ import com.akanjs.runtime.AkanNativeUpdateDecision
9
11
  import com.akanjs.runtime.AkanNativeUpdateEntry
12
+ import com.akanjs.runtime.AkanNativeUpdateEnvelope
13
+ import com.akanjs.runtime.AkanNativeUpdateReports
10
14
  import com.akanjs.runtime.AkanNativeUpdates
11
15
  import com.akanjs.runtime.AkanNativeVoidReply
12
16
  import java.io.File
@@ -14,13 +18,13 @@ import java.io.IOException
14
18
  import java.net.HttpURLConnection
15
19
  import java.net.URL
16
20
  import java.security.MessageDigest
17
- import java.util.Base64
18
21
  import org.json.JSONObject
19
22
 
20
23
  /**
21
- * Web bundle updates (UP-2), the Android twin of ios/UpdatesPlugin.swift: the manifest
22
- * `<url>/android/<channel>.json` is signed as a whole (`.sig`: base64 Ed25519 over its bytes, the
23
- * platform's on API 33+, AkanNativeEd25519 in plain Kotlin below), files are content-addressed (`<url>/android/files/<sha256>`)
24
+ * Web bundle updates (UP-2, protocol v2), the Android twin of ios/UpdatesPlugin.swift: the envelope
25
+ * `<url>/android/<channel>/<nativeApi>.json` carries the manifest's bytes, the keyId of the key of `updates.publicKeys`
26
+ * that signed them and the Ed25519 signature (the platform's on API 33+, AkanNativeEd25519 in plain Kotlin below);
27
+ * revocations and the sequence rule are AkanNativeKernel.updateDecision. Files are content-addressed (`<url>/android/files/<sha256>`)
24
28
  * and checked against their hash, and files the running bundle already has are copied. Which bundle
25
29
  * runs, and the trial / rollback rules, live in the shell (native/android/.../AkanNativeUpdates.kt).
26
30
  * Network and file work run on a worker thread; AkanNativeUpdates is only touched on the main thread.
@@ -31,19 +35,30 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
31
35
  /** The running download's call (one at a time); its cancellation stops the read loop. */
32
36
  @Volatile private var download: com.akanjs.runtime.AkanNativeCall? = null
33
37
 
34
- private class Failure(val code: String, message: String) : Exception(message)
38
+ /** [report]: the reason a failed download is reported under (AkanNativeUpdateReports), null when it is not reported. */
39
+ private class Failure(val code: String, message: String, val report: String? = null) : Exception(message)
35
40
 
36
41
  private class ReleaseFile(val path: String, val sha256: String, val size: Long)
37
42
 
38
- private class Release(val bundle: String, val sequence: Double, val version: String, val nativeApi: String, val files: List<ReleaseFile>, val manifest: ByteArray, val signature: String)
43
+ /** [envelope]: as downloaded, kept next to the bundle for the shell's check at every launch. */
44
+ private class Release(
45
+ val bundle: String, val sequence: Double, val version: String, val nativeApi: String, val files: List<ReleaseFile>,
46
+ val keyId: String, val revokeKeys: List<String>, val resetSequence: Boolean, val envelope: ByteArray,
47
+ )
39
48
 
40
49
  /** What the worker needs from the main thread, read once per call. */
41
- private class Snapshot(val config: AkanNativeUpdateConfig, val running: Double, val failed: List<String>, val pending: String?, val activeRoot: File?)
50
+ private class Snapshot(
51
+ val config: AkanNativeUpdateConfig, val running: Pair<Double, String>?, val failed: List<String>, val pending: String?,
52
+ val activeRoot: File?, val revoked: List<String>,
53
+ )
42
54
 
43
55
  private fun snapshot(config: AkanNativeUpdateConfig): Snapshot {
44
56
  val state = AkanNativeUpdates.readState()
45
57
  val active = AkanNativeUpdates.active
46
- return Snapshot(config, active?.sequence ?: config.embeddedSequence, state.failed.toList(), state.pending?.bundle, active?.let { AkanNativeUpdates.bundleDirectory(it.bundle) })
58
+ return Snapshot(
59
+ config, active?.let { it.sequence to it.keyId }, state.failed.toList(), state.pending?.bundle,
60
+ active?.let { AkanNativeUpdates.bundleDirectory(it.bundle) }, state.revokedKeys.toList(),
61
+ )
47
62
  }
48
63
 
49
64
  private fun <T> configured(reply: AkanNativeReply<T>): AkanNativeUpdateConfig? {
@@ -72,7 +87,7 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
72
87
  val config = configured(reply) ?: return
73
88
  val snap = snapshot(config)
74
89
  worker(reply) {
75
- val release = fetchRelease(config)
90
+ val release = fetchRelease(config, snap)
76
91
  val available = isNewer(release, snap)
77
92
  val size = if (available) release.files.filter { localCopy(it, snap) == null }.sumOf { it.size }.toDouble() else null
78
93
  reply.resolve(UpdatesUpdateCheck(available = available, bundle = release.bundle, version = release.version, sequence = release.sequence, downloadSize = size))
@@ -88,11 +103,11 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
88
103
  download = reply.call
89
104
  val snap = snapshot(config)
90
105
  worker(reply, done = { downloading = false; download = null }) {
91
- val release = fetchRelease(config)
106
+ val release = fetchRelease(config, snap)
92
107
  if (!isNewer(release, snap)) throw Failure(AkanNativeErrorCode.NOT_FOUND, "no newer release for this app (native API ${config.nativeApi})")
93
108
  if (snap.pending != release.bundle) {
94
109
  install(release, config, snap)
95
- main.post { AkanNativeUpdates.stage(AkanNativeUpdateEntry(release.bundle, release.sequence, release.nativeApi)) }
110
+ main.post { AkanNativeUpdates.stage(AkanNativeUpdateEntry(release.bundle, release.sequence, release.nativeApi, release.keyId)) }
96
111
  }
97
112
  reply.resolve(UpdatesDownloadResult(bundle = release.bundle))
98
113
  }
@@ -133,9 +148,30 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
133
148
  }.start()
134
149
  }
135
150
 
136
- /** Newer than what runs (and than the APK's own bundle), for this binary, not rolled back before. */
137
- private fun isNewer(release: Release, snap: Snapshot): Boolean =
138
- release.nativeApi == snap.config.nativeApi && release.sequence > maxOf(snap.running, snap.config.embeddedSequence) && release.bundle !in snap.failed
151
+ /**
152
+ * Newer than what runs (and than the APK's own bundle), not rolled back before. A manifest's revocations are
153
+ * recorded whether or not its release is newer: the key behind a bad release must stop counting either way.
154
+ */
155
+ private fun isNewer(release: Release, snap: Snapshot): Boolean {
156
+ val decision = AkanNativeKernel.updateDecision(
157
+ snap.config.ring, snap.revoked, release.keyId, release.revokeKeys, release.resetSequence,
158
+ release.sequence, snap.config.embeddedSequence, snap.running,
159
+ )
160
+ return when (decision.kind) {
161
+ AkanNativeUpdateDecision.Kind.TAKE, AkanNativeUpdateDecision.Kind.STALE -> {
162
+ if (decision.revoke.isNotEmpty()) main.post { AkanNativeUpdates.revoke(decision.revoke) }
163
+ decision.kind == AkanNativeUpdateDecision.Kind.TAKE && release.bundle !in snap.failed
164
+ }
165
+ AkanNativeUpdateDecision.Kind.UNTRUSTED, AkanNativeUpdateDecision.Kind.REVOKED -> {
166
+ reportFailed(release.bundle, release.sequence, "signature")
167
+ throw Failure(AkanNativeErrorCode.INTERNAL, "the release is signed by key ${release.keyId}, which this app does not trust")
168
+ }
169
+ else -> {
170
+ reportFailed(release.bundle, release.sequence, "manifest")
171
+ throw Failure(AkanNativeErrorCode.INTERNAL, "invalid release manifest: key ${release.keyId} may not revoke ${release.revokeKeys.joinToString(", ")}")
172
+ }
173
+ }
174
+ }
139
175
 
140
176
  private fun base(config: AkanNativeUpdateConfig) = "${config.url}/${config.platform}"
141
177
 
@@ -151,6 +187,9 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
151
187
  connection.readTimeout = 30_000
152
188
  connection.useCaches = false
153
189
  connection.setRequestProperty("Cache-Control", "no-cache")
190
+ // HttpURLConnection asks for text/html first, and a CDN that injects scripts into HTML responses
191
+ // (Cloudflare Web Analytics) changes the signed bytes.
192
+ connection.setRequestProperty("Accept", "*/*")
154
193
  val status = connection.responseCode
155
194
  if (status == 404) throw Failure(AkanNativeErrorCode.NOT_FOUND, "$url not found")
156
195
  if (status != 200) throw Failure(AkanNativeErrorCode.INTERNAL, "$url: HTTP $status")
@@ -179,31 +218,57 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
179
218
  return out.toByteArray()
180
219
  }
181
220
 
182
- private fun fetchRelease(config: AkanNativeUpdateConfig): Release {
183
- val manifestUrl = "${base(config)}/${config.channel}.json"
184
- val manifest = fetch(manifestUrl, 1L shl 20)
185
- val signatureText = String(fetch("$manifestUrl.sig", 1024)).trim()
186
- val signature = try {
187
- Base64.getDecoder().decode(signatureText)
188
- } catch (e: IllegalArgumentException) {
189
- throw Failure(AkanNativeErrorCode.INTERNAL, "the release signature is not base64")
221
+ private fun fetchRelease(config: AkanNativeUpdateConfig, snap: Snapshot): Release {
222
+ val url = AkanNativeKernel.updateManifestUrl(config.url, config.platform, config.channel, config.nativeApi)
223
+ // A 1 MiB manifest is 1.4 MiB in base64.
224
+ val data = fetch(url, 3L shl 19)
225
+ val envelope = AkanNativeUpdateEnvelope.open(data, config, snap.revoked)
226
+ if (envelope == null) {
227
+ reportRefused(data, "signature")
228
+ throw Failure(AkanNativeErrorCode.INTERNAL, "the release is not signed by a key of updates.publicKeys this app still trusts")
229
+ }
230
+ return try {
231
+ parse(envelope, data, config)
232
+ } catch (f: Failure) {
233
+ reportRefused(data, "manifest")
234
+ throw f
190
235
  }
191
- if (!AkanNativeUpdates.signed(manifest, signature, config.publicKey)) throw Failure(AkanNativeErrorCode.INTERNAL, "the release signature does not match updates.publicKey")
192
- return parse(manifest, signatureText, config)
236
+ }
237
+
238
+ private fun reportFailed(bundle: String?, sequence: Double?, reason: String) {
239
+ main.post { AkanNativeUpdateReports.record("failed", bundle, sequence, reason) }
240
+ }
241
+
242
+ /**
243
+ * The release a refused envelope names, read without trusting it and only to say which release failed: a forged
244
+ * name can at worst count against a release the server already published for this app.
245
+ */
246
+ private fun reportRefused(raw: ByteArray, reason: String) {
247
+ val manifest = try {
248
+ JSONObject(String(android.util.Base64.decode(JSONObject(String(raw)).getString("manifest"), android.util.Base64.DEFAULT)))
249
+ } catch (e: Exception) {
250
+ null
251
+ }
252
+ reportFailed(manifest?.optString("bundle"), manifest?.optDouble("sequence")?.takeIf { it.isFinite() }, reason)
193
253
  }
194
254
 
195
255
  /** Checks every field of the (signed) manifest before any of it is used. */
196
- private fun parse(data: ByteArray, signature: String, config: AkanNativeUpdateConfig): Release {
256
+ private fun parse(envelope: AkanNativeUpdateEnvelope, raw: ByteArray, config: AkanNativeUpdateConfig): Release {
197
257
  fun bad(why: String) = Failure(AkanNativeErrorCode.INTERNAL, "invalid release manifest: $why")
198
258
  val o = try {
199
- JSONObject(String(data))
259
+ JSONObject(String(envelope.manifest))
200
260
  } catch (e: Exception) {
201
261
  throw bad("not a JSON object")
202
262
  }
203
- if (o.optInt("schema") != 1 || o.optString("kind") != "web") throw bad("schema 1, kind web expected")
204
- if (o.optString("app") != config.app || o.optString("platform") != config.platform || o.optString("channel") != config.channel) {
205
- throw bad("made for another app, platform or channel")
263
+ if (o.optInt("schema") != 2 || o.optString("kind") != "web") throw bad("schema 2, kind web expected")
264
+ if (o.optString("app") != config.app || o.optString("platform") != config.platform || o.optString("channel") != config.channel ||
265
+ o.optString("nativeApi") != config.nativeApi) {
266
+ throw bad("made for another app, platform, channel or native API")
206
267
  }
268
+ val revokeList = if (o.has("revokeKeys")) o.optJSONArray("revokeKeys") ?: throw bad("revokeKeys") else null
269
+ val revokeKeys = List(revokeList?.length() ?: 0) { revokeList?.opt(it) as? String ?: throw bad("revokeKeys") }
270
+ if (revokeKeys.size > 16 || !revokeKeys.all(AkanNativeKernel::isUpdateKeyId)) throw bad("revokeKeys")
271
+ if (o.has("resetSequence") && o.opt("resetSequence") !is Boolean) throw bad("resetSequence")
207
272
  val bundle = o.optString("bundle")
208
273
  if (!AkanNativeUpdates.isBundleId(bundle)) throw bad("bundle id")
209
274
  val sequence = o.optDouble("sequence", -1.0)
@@ -219,7 +284,10 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
219
284
  ReleaseFile(path, sha, size)
220
285
  }
221
286
  if (files.sumOf { it.size } > 300_000_000 || files.none { it.path == "index.html" } || files.map { it.path }.toSet().size != files.size) throw bad("files")
222
- return Release(bundle, sequence, o.optString("version"), o.optString("nativeApi"), files, data, signature)
287
+ return Release(
288
+ bundle, sequence, o.optString("version"), o.optString("nativeApi"), files,
289
+ envelope.keyId, revokeKeys, o.optBoolean("resetSequence"), raw,
290
+ )
223
291
  }
224
292
 
225
293
  /** Relative, "/"-separated, no empty, "." or ".." segments. */
@@ -243,8 +311,23 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
243
311
  return if (data.size.toLong() == file.size && hex(data) == file.sha256) data else null
244
312
  }
245
313
 
246
- /** Downloads into bundles/<bundle>.partial, verifies each file, then renames it into place. */
314
+ /**
315
+ * Downloads into bundles/<bundle>.partial, verifies each file, then renames it into place. A failure is reported
316
+ * under its reason, and an IOException outside a download is the disk's.
317
+ */
247
318
  private fun install(release: Release, config: AkanNativeUpdateConfig, snap: Snapshot) {
319
+ try {
320
+ place(release, config, snap)
321
+ } catch (f: Failure) {
322
+ f.report?.let { reportFailed(release.bundle, release.sequence, it) }
323
+ throw f
324
+ } catch (e: IOException) {
325
+ reportFailed(release.bundle, release.sequence, "disk")
326
+ throw e
327
+ }
328
+ }
329
+
330
+ private fun place(release: Release, config: AkanNativeUpdateConfig, snap: Snapshot) {
248
331
  val target = AkanNativeUpdates.bundleDirectory(release.bundle)
249
332
  val staging = File(target.parentFile, "${release.bundle}.partial")
250
333
  staging.deleteRecursively()
@@ -262,29 +345,38 @@ class UpdatesPlugin(private val context: AkanNativePluginContext) : UpdatesPlugi
262
345
  val digest = MessageDigest.getInstance("SHA-256")
263
346
  var size = 0L
264
347
  destination.outputStream().use { out ->
265
- fetch("${base(config)}/files/${file.sha256}", file.size) { buffer, n ->
266
- digest.update(buffer, 0, n)
267
- out.write(buffer, 0, n)
268
- size += n
348
+ try {
349
+ fetch("${base(config)}/files/${file.sha256}", file.size) { buffer, n ->
350
+ digest.update(buffer, 0, n)
351
+ try {
352
+ out.write(buffer, 0, n)
353
+ } catch (e: IOException) {
354
+ throw Failure(AkanNativeErrorCode.INTERNAL, "cannot write ${file.path}: ${e.message ?: e}", "disk")
355
+ }
356
+ size += n
357
+ }
358
+ } catch (f: Failure) {
359
+ throw if (f.report != null || f.code == AkanNativeErrorCode.CANCELLED) f else Failure(f.code, f.message ?: "download failed", "download")
360
+ } catch (e: IOException) {
361
+ throw Failure(AkanNativeErrorCode.INTERNAL, "${file.path}: ${e.message ?: e}", "download")
269
362
  }
270
363
  }
271
364
  if (size != file.size || digest.digest().joinToString("") { "%02x".format(it) } != file.sha256) {
272
365
  staging.deleteRecursively()
273
- throw Failure(AkanNativeErrorCode.INTERNAL, "${file.path} does not match its hash")
366
+ throw Failure(AkanNativeErrorCode.INTERNAL, "${file.path} does not match its hash", "verify")
274
367
  }
275
368
  }
276
369
  received += file.size
277
370
  val progress = UpdatesUpdateProgress(received = received.toDouble(), total = total)
278
371
  main.post { events.progress(progress) }
279
372
  }
280
- // The shell checks the signed manifest against the files at every launch (AkanNativeUpdates.verified).
281
- File(staging, "manifest.json").writeBytes(release.manifest)
282
- File(staging, "manifest.json.sig").writeText(release.signature)
373
+ // The shell checks the envelope against the files at every launch (AkanNativeUpdates.verified).
374
+ File(staging, "manifest.signed.json").writeBytes(release.envelope)
283
375
  if (release.files.any { File(staging, it.path).length() != it.size }) {
284
376
  staging.deleteRecursively()
285
- throw Failure(AkanNativeErrorCode.INTERNAL, "the downloaded bundle is incomplete")
377
+ throw Failure(AkanNativeErrorCode.INTERNAL, "the downloaded bundle is incomplete", "verify")
286
378
  }
287
379
  target.deleteRecursively()
288
- if (!staging.renameTo(target)) throw Failure(AkanNativeErrorCode.INTERNAL, "could not move the downloaded bundle into place")
380
+ if (!staging.renameTo(target)) throw Failure(AkanNativeErrorCode.INTERNAL, "could not move the downloaded bundle into place", "disk")
289
381
  }
290
382
  }
@@ -1,9 +1,10 @@
1
1
  import CryptoKit
2
2
  import Foundation
3
3
 
4
- /// Web bundle updates (UP-2). The manifest `<url>/ios/<channel>.json` is signed as a whole
5
- /// (`.sig`: base64 Ed25519 over its bytes, key from akan-native.config.ts `updates.publicKey`), so every
6
- /// field below is trusted only after `isValidSignature`. Files are content-addressed
4
+ /// Web bundle updates (UP-2, protocol v2). The envelope `<url>/ios/<channel>/<nativeApi>.json` holds the manifest's
5
+ /// bytes, the keyId of the key that signed them (one of akan-native.config.ts `updates.publicKeys`) and the Ed25519
6
+ /// signature, so every field below is trusted only after `AkanNativeUpdateEnvelope.open`. Revocations and the
7
+ /// sequence rule are AkanNativeKernel.updateDecision, shared with Android and kernel.ts. Files are content-addressed
7
8
  /// (`<url>/ios/files/<sha256>`) and each one is checked against its hash; files the running
8
9
  /// bundle already has are copied instead of downloaded. Which bundle runs, and the trial /
9
10
  /// rollback rules, live in the shell (native/ios/Sources/AkanNativeUpdates.swift).
@@ -24,6 +25,8 @@ final class UpdatesPlugin: UpdatesPluginSpec {
24
25
  struct Failure: Error {
25
26
  let code: AkanNativeErrorCode
26
27
  let message: String
28
+ /// The reason a failed download is reported under (AkanNativeUpdateReports), nil when it is not reported.
29
+ var report: String? = nil
27
30
  }
28
31
 
29
32
  struct ReleaseFile: Sendable {
@@ -38,9 +41,11 @@ final class UpdatesPlugin: UpdatesPluginSpec {
38
41
  let version: String
39
42
  let nativeApi: String
40
43
  let files: [ReleaseFile]
41
- let manifest: Data
42
- /// The manifest's `.sig` as downloaded (base64), kept next to the bundle.
43
- let signature: String
44
+ let keyId: String
45
+ let revokeKeys: [String]
46
+ let resetSequence: Bool
47
+ /// The envelope as downloaded, kept next to the bundle for the shell's check at every launch.
48
+ let envelope: Data
44
49
  }
45
50
 
46
51
  private func configured<T>(_ reply: AkanNativeReply<T>) -> AkanNativeUpdateConfig? {
@@ -68,7 +73,7 @@ final class UpdatesPlugin: UpdatesPluginSpec {
68
73
  Task {
69
74
  do {
70
75
  let release = try await Self.fetchRelease(config)
71
- let available = isNewer(release, config)
76
+ let available = try isNewer(release, config)
72
77
  reply.resolve(UpdatesUpdateCheck(
73
78
  available: available,
74
79
  bundle: release.bundle,
@@ -94,7 +99,7 @@ final class UpdatesPlugin: UpdatesPluginSpec {
94
99
  defer { downloading = false }
95
100
  do {
96
101
  let release = try await Self.fetchRelease(config)
97
- guard isNewer(release, config) else { throw Failure(code: .notFound, message: "no newer release for this app (native API \(config.nativeApi))") }
102
+ guard try isNewer(release, config) else { throw Failure(code: .notFound, message: "no newer release for this app (native API \(config.nativeApi))") }
98
103
  if AkanNativeUpdates.readState().pending?.bundle != release.bundle { try await install(release, config) }
99
104
  reply.resolve(UpdatesDownloadResult(bundle: release.bundle))
100
105
  } catch let f as Failure {
@@ -128,16 +133,33 @@ final class UpdatesPlugin: UpdatesPluginSpec {
128
133
 
129
134
  // MARK: releases
130
135
 
131
- /// Newer than what runs (and than the app's own bundle), for this binary, not rolled back before.
132
- private func isNewer(_ release: Release, _ config: AkanNativeUpdateConfig) -> Bool {
133
- let running = AkanNativeUpdates.active?.sequence ?? config.embeddedSequence
134
- return release.nativeApi == config.nativeApi && release.sequence > max(running, config.embeddedSequence)
135
- && !AkanNativeUpdates.readState().failed.contains(release.bundle)
136
+ /// Newer than what runs (and than the app's own bundle), not rolled back before. A manifest's revocations are
137
+ /// recorded whether or not its release is newer: the key behind a bad release must stop counting either way.
138
+ private func isNewer(_ release: Release, _ config: AkanNativeUpdateConfig) throws -> Bool {
139
+ let state = AkanNativeUpdates.readState()
140
+ let decision = AkanNativeKernel.updateDecision(
141
+ ring: config.ring, revoked: state.revokedKeys, signer: release.keyId, revokeKeys: release.revokeKeys,
142
+ resetSequence: release.resetSequence, sequence: release.sequence, embedded: config.embeddedSequence,
143
+ running: AkanNativeUpdates.active.map { (sequence: $0.sequence, keyId: $0.keyId) }
144
+ )
145
+ switch decision.kind {
146
+ case .take, .stale:
147
+ if !decision.revoke.isEmpty { AkanNativeUpdates.revoke(decision.revoke) }
148
+ return decision.kind == .take && !state.failed.contains(release.bundle)
149
+ case .untrusted, .revoked:
150
+ AkanNativeUpdateReports.record("failed", bundle: release.bundle, sequence: release.sequence, reason: "signature")
151
+ throw Failure(code: .internalError, message: "the release is signed by key \(release.keyId), which this app does not trust")
152
+ case .selfRevoke, .revokeLater:
153
+ AkanNativeUpdateReports.record("failed", bundle: release.bundle, sequence: release.sequence, reason: "manifest")
154
+ throw Failure(code: .internalError, message: "invalid release manifest: key \(release.keyId) may not revoke \(release.revokeKeys.joined(separator: ", "))")
155
+ }
136
156
  }
137
157
 
138
158
  private static func request(_ url: URL) -> URLRequest {
139
159
  var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
140
160
  request.setValue("no-cache", forHTTPHeaderField: "Cache-Control")
161
+ //? Not text/html: a CDN that injects scripts into HTML responses (Cloudflare Web Analytics) would change signed bytes.
162
+ request.setValue("*/*", forHTTPHeaderField: "Accept")
141
163
  return request
142
164
  }
143
165
 
@@ -187,27 +209,45 @@ final class UpdatesPlugin: UpdatesPluginSpec {
187
209
  private static func base(_ config: AkanNativeUpdateConfig) -> String { "\(config.url)/\(config.platform)" }
188
210
 
189
211
  private static func fetchRelease(_ config: AkanNativeUpdateConfig) async throws -> Release {
190
- guard let manifestURL = URL(string: "\(base(config))/\(config.channel).json"), let sigURL = URL(string: "\(base(config))/\(config.channel).json.sig") else {
191
- throw Failure(code: .internalError, message: "bad updates.url \(config.url)")
212
+ let address = AkanNativeKernel.updateManifestURL(config.url, platform: config.platform, channel: config.channel, nativeApi: config.nativeApi)
213
+ guard let url = URL(string: address) else { throw Failure(code: .internalError, message: "bad updates.url \(config.url)") }
214
+ //? A 1 MiB manifest is 1.4 MiB in base64.
215
+ let data = try await fetch(url, limit: 3 << 19)
216
+ guard let envelope = AkanNativeUpdateEnvelope.open(data, config, revoked: AkanNativeUpdates.readState().revokedKeys) else {
217
+ reportRefused(data, "signature")
218
+ throw Failure(code: .internalError, message: "the release is not signed by a key of updates.publicKeys this app still trusts")
192
219
  }
193
- let manifest = try await fetch(manifestURL, limit: 1 << 20)
194
- let sigText = String(decoding: try await fetch(sigURL, limit: 1024), as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
195
- guard let signature = Data(base64Encoded: sigText),
196
- let key = try? Curve25519.Signing.PublicKey(rawRepresentation: config.publicKey),
197
- key.isValidSignature(signature, for: manifest) else {
198
- throw Failure(code: .internalError, message: "the release signature does not match updates.publicKey")
220
+ do {
221
+ return try parse(envelope, data, config)
222
+ } catch {
223
+ reportRefused(data, "manifest")
224
+ throw error
199
225
  }
200
- return try parse(manifest, sigText, config)
226
+ }
227
+
228
+ /// The release a refused envelope names, read without trusting it and only to say which release failed: a forged
229
+ /// name can at worst count against a release the server already published for this app.
230
+ private static func reportRefused(_ raw: Data, _ reason: String) {
231
+ let envelope = (try? JSONSerialization.jsonObject(with: raw)) as? [String: Any]
232
+ let manifest = (envelope?["manifest"] as? String).flatMap { Data(base64Encoded: $0) }
233
+ .flatMap { (try? JSONSerialization.jsonObject(with: $0)) as? [String: Any] }
234
+ AkanNativeUpdateReports.record(
235
+ "failed", bundle: manifest?["bundle"] as? String, sequence: (manifest?["sequence"] as? NSNumber)?.doubleValue, reason: reason
236
+ )
201
237
  }
202
238
 
203
239
  /// Checks every field of the (signed) manifest before any of it is used.
204
- private static func parse(_ data: Data, _ signature: String, _ config: AkanNativeUpdateConfig) throws -> Release {
240
+ private static func parse(_ envelope: AkanNativeUpdateEnvelope, _ raw: Data, _ config: AkanNativeUpdateConfig) throws -> Release {
205
241
  let bad = { (why: String) in Failure(code: .internalError, message: "invalid release manifest: \(why)") }
206
- guard let o = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw bad("not a JSON object") }
207
- guard (o["schema"] as? NSNumber)?.intValue == 1, o["kind"] as? String == "web" else { throw bad("schema 1, kind web expected") }
208
- guard o["app"] as? String == config.app, o["platform"] as? String == config.platform, o["channel"] as? String == config.channel else {
209
- throw bad("made for another app, platform or channel")
242
+ guard let o = try? JSONSerialization.jsonObject(with: envelope.manifest) as? [String: Any] else { throw bad("not a JSON object") }
243
+ guard (o["schema"] as? NSNumber)?.intValue == 2, o["kind"] as? String == "web" else { throw bad("schema 2, kind web expected") }
244
+ guard o["app"] as? String == config.app, o["platform"] as? String == config.platform, o["channel"] as? String == config.channel,
245
+ o["nativeApi"] as? String == config.nativeApi else {
246
+ throw bad("made for another app, platform, channel or native API")
210
247
  }
248
+ let revokeKeys = o["revokeKeys"] as? [String] ?? []
249
+ guard o["revokeKeys"] == nil || o["revokeKeys"] is [String], revokeKeys.count <= 16, revokeKeys.allSatisfy(AkanNativeKernel.isUpdateKeyId),
250
+ o["resetSequence"] == nil || o["resetSequence"] is Bool else { throw bad("revokeKeys or resetSequence") }
211
251
  guard let bundle = o["bundle"] as? String, AkanNativeUpdates.isBundleId(bundle) else { throw bad("bundle id") }
212
252
  guard let sequence = (o["sequence"] as? NSNumber)?.doubleValue, sequence.isFinite, sequence > 0 else { throw bad("sequence") }
213
253
  guard let nativeApi = o["nativeApi"] as? String, let list = o["files"] as? [[String: Any]], (1...5000).contains(list.count) else { throw bad("nativeApi or files") }
@@ -220,7 +260,10 @@ final class UpdatesPlugin: UpdatesPluginSpec {
220
260
  files.append(ReleaseFile(path: path, sha256: sha.lowercased(), size: size))
221
261
  }
222
262
  guard total <= 300_000_000, files.contains(where: { $0.path == "index.html" }), Set(files.map(\.path)).count == files.count else { throw bad("files") }
223
- return Release(bundle: bundle, sequence: sequence, version: o["version"] as? String ?? "", nativeApi: nativeApi, files: files, manifest: data, signature: signature)
263
+ return Release(
264
+ bundle: bundle, sequence: sequence, version: o["version"] as? String ?? "", nativeApi: nativeApi, files: files,
265
+ keyId: envelope.keyId, revokeKeys: revokeKeys, resetSequence: o["resetSequence"] as? Bool ?? false, envelope: raw
266
+ )
224
267
  }
225
268
 
226
269
  /// Relative, "/"-separated, no empty, "." or ".." segments.
@@ -247,8 +290,23 @@ final class UpdatesPlugin: UpdatesPluginSpec {
247
290
  return localHashes[key] == file.sha256 ? candidate : nil
248
291
  }
249
292
 
250
- /// Downloads into bundles/<bundle>.partial, verifies each file, then renames and stages it.
293
+ /// Downloads into bundles/<bundle>.partial, verifies each file, then renames and stages it. A failure is reported
294
+ /// under its reason, and an error that is neither a download's nor a cancellation is the disk's.
251
295
  private func install(_ release: Release, _ config: AkanNativeUpdateConfig) async throws {
296
+ do {
297
+ try await place(release, config)
298
+ } catch {
299
+ let reason = (error as? Failure).map(\.report) ?? (Self.cancelled(error) ? nil : "disk")
300
+ if let reason { AkanNativeUpdateReports.record("failed", bundle: release.bundle, sequence: release.sequence, reason: reason) }
301
+ throw error
302
+ }
303
+ }
304
+
305
+ private static func cancelled(_ error: Error) -> Bool {
306
+ error is CancellationError || (error as? URLError)?.code == .cancelled
307
+ }
308
+
309
+ private func place(_ release: Release, _ config: AkanNativeUpdateConfig) async throws {
252
310
  let fm = FileManager.default
253
311
  let target = AkanNativeUpdates.bundleDirectory(release.bundle)
254
312
  let staging = target.deletingLastPathComponent().appendingPathComponent("\(release.bundle).partial", isDirectory: true)
@@ -264,29 +322,36 @@ final class UpdatesPlugin: UpdatesPluginSpec {
264
322
  try fm.copyItem(at: local, to: destination)
265
323
  } else {
266
324
  guard let url = URL(string: "\(Self.base(config))/files/\(file.sha256)") else { throw Failure(code: .internalError, message: "bad file URL") }
267
- let downloaded = try await Self.download(url, size: file.size)
325
+ let downloaded: URL
326
+ do {
327
+ downloaded = try await Self.download(url, size: file.size)
328
+ } catch let failure as Failure {
329
+ throw Failure(code: failure.code, message: failure.message, report: "download")
330
+ } catch {
331
+ if Self.cancelled(error) { throw error }
332
+ throw Failure(code: .internalError, message: "\(file.path): \(error.localizedDescription)", report: "download")
333
+ }
268
334
  guard (try? Self.hex(file: downloaded)) == file.sha256 else {
269
335
  try? fm.removeItem(at: downloaded)
270
336
  try? fm.removeItem(at: staging)
271
- throw Failure(code: .internalError, message: "\(file.path) does not match its hash")
337
+ throw Failure(code: .internalError, message: "\(file.path) does not match its hash", report: "verify")
272
338
  }
273
339
  try fm.moveItem(at: downloaded, to: destination)
274
340
  }
275
341
  received += file.size
276
342
  events.progress(UpdatesUpdateProgress(received: Double(received), total: Double(total)))
277
343
  }
278
- // The shell checks the signed manifest against the files at every launch (AkanNativeUpdates.verified).
279
- try release.manifest.write(to: staging.appendingPathComponent("manifest.json"))
280
- try Data(release.signature.utf8).write(to: staging.appendingPathComponent("manifest.json.sig"))
344
+ // The shell checks the envelope against the files at every launch (AkanNativeUpdates.verified).
345
+ try release.envelope.write(to: staging.appendingPathComponent("manifest.signed.json"))
281
346
  let complete = release.files.allSatisfy { file in
282
347
  (try? fm.attributesOfItem(atPath: staging.appendingPathComponent(file.path).path)[.size] as? NSNumber)?.intValue == file.size
283
348
  }
284
349
  guard complete else {
285
350
  try? fm.removeItem(at: staging)
286
- throw Failure(code: .internalError, message: "the downloaded bundle is incomplete")
351
+ throw Failure(code: .internalError, message: "the downloaded bundle is incomplete", report: "verify")
287
352
  }
288
353
  if fm.fileExists(atPath: target.path) { try fm.removeItem(at: target) }
289
354
  try fm.moveItem(at: staging, to: target)
290
- AkanNativeUpdates.stage(AkanNativeUpdateEntry(bundle: release.bundle, sequence: release.sequence, nativeApi: release.nativeApi))
355
+ AkanNativeUpdates.stage(AkanNativeUpdateEntry(bundle: release.bundle, sequence: release.sequence, nativeApi: release.nativeApi, keyId: release.keyId))
291
356
  }
292
357
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "updates",
3
3
  "apiVersion": 1,
4
- "methods": ["getState", "check", "download", "apply", "notifyReady", "reset"],
4
+ "methods": ["getState", "check", "download", "apply", "notifyReady", "reset", "setChannel"],
5
5
  "events": ["progress"],
6
6
  "coalesce": ["progress"],
7
7
  "codegen": true,
@@ -10,10 +10,12 @@
10
10
  "ios": {
11
11
  "sources": ["ios/*.swift"],
12
12
  "class": "UpdatesPlugin",
13
+ "methods": ["getState", "check", "download", "apply", "notifyReady", "reset"],
13
14
  "frameworks": ["CryptoKit"]
14
15
  },
15
16
  "android": {
16
17
  "sources": ["android/*.kt"],
17
- "class": "com.akanjs.plugins.updates.UpdatesPlugin"
18
+ "class": "com.akanjs.plugins.updates.UpdatesPlugin",
19
+ "methods": ["getState", "check", "download", "apply", "notifyReady", "reset"]
18
20
  }
19
21
  }
@@ -78,6 +78,8 @@ interface State {
78
78
  rollback?: { build: number; previous: string; to: string; attempts: number };
79
79
  /** embeddedSequence of the build installed here: another one means the app was installed again. */
80
80
  installed?: number;
81
+ /** setChannel(): the manifest read over updates.json's `channel`. */
82
+ channel?: string;
81
83
  }
82
84
 
83
85
  interface Manifest {
@@ -269,7 +271,8 @@ export function createDesktopUpdates(options: DesktopUpdatesOptions = {}): Deskt
269
271
  "UNSUPPORTED",
270
272
  "updates are not configured (akan-native.config.ts `updates`, a built app)",
271
273
  );
272
- return config;
274
+ const { channel } = readState();
275
+ return channel ? { ...config, channel } : config;
273
276
  };
274
277
  /** `entry` when the running app is that release (installed by an update), not another build. */
275
278
  const ifRunning = <E extends Entry>(entry: E | undefined): E | undefined =>
@@ -848,6 +851,17 @@ export function createDesktopUpdates(options: DesktopUpdatesOptions = {}): Deskt
848
851
  if (windows) void recordInstalledVersion(c.app, installed, entry.version);
849
852
  },
850
853
 
854
+ async setChannel(args) {
855
+ need();
856
+ const channel = (args as { channel?: unknown } | undefined)?.channel ?? null;
857
+ if (channel !== null && (typeof channel !== "string" || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(channel)))
858
+ throw new AkanNativeError("INVALID_ARGS", "channel must be a manifest name (letters, digits, . _ -) or null");
859
+ const state = readState();
860
+ if (channel === null) delete state.channel;
861
+ else state.channel = channel;
862
+ writeState(state);
863
+ },
864
+
851
865
  async reset() {
852
866
  need();
853
867
  const state = readState();
@@ -67,6 +67,12 @@ export interface UpdatesApi {
67
67
  * releases; the running app stays, and a trial keeps its record.
68
68
  */
69
69
  reset(): Promise<void>;
70
+ /**
71
+ * Desktop: the manifest this app reads from now on, over the built `updates.channel` until set back with `null` —
72
+ * a server that moves this device between a pilot and a stable channel. A release older than the running one is
73
+ * never taken, so a device leaving a pilot channel stays on its pilot build until the other channel passes it.
74
+ */
75
+ setChannel(args: { channel: string | null }): Promise<void>;
70
76
  }
71
77
 
72
78
  export interface UpdatesEvents {
@@ -74,7 +80,7 @@ export interface UpdatesEvents {
74
80
  }
75
81
 
76
82
  export const updates = definePlugin<UpdatesApi, UpdatesEvents>("updates", {
77
- methods: ["getState", "check", "download", "apply", "notifyReady", "reset"],
83
+ methods: ["getState", "check", "download", "apply", "notifyReady", "reset", "setChannel"],
78
84
  events: ["progress"],
79
85
  });
80
86
 
@@ -13,6 +13,10 @@
13
13
  "restore",
14
14
  "setFullscreen",
15
15
  "setAlwaysOnTop",
16
+ "setDecorations",
17
+ "setCursorVisible",
18
+ "setMinSize",
19
+ "placeOnDisplay",
16
20
  "show",
17
21
  "hide",
18
22
  "focus",