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.
- package/CHANGELOG.md +26 -0
- package/base/baseEnv.ts +0 -4
- package/base/primitiveRegistry.ts +2 -2
- package/client/cookie.ts +5 -1
- package/client/csrLocation.ts +122 -0
- package/client/csrTypes.ts +32 -4
- package/client/device.ts +2 -1
- package/client/frameConfig.ts +78 -7
- package/client/index.ts +3 -0
- package/client/makePageProto.tsx +3 -2
- package/client/native/external-view.ts +1 -0
- package/client/route/RootLayoutDefinition.ts +7 -1
- package/client/route/RouteHead.ts +108 -0
- package/client/route/index.ts +1 -0
- package/client/router.ts +36 -23
- package/client/rscNavigation.ts +8 -2
- package/client/viewportMeta.ts +60 -0
- package/client/webManifest.ts +18 -0
- package/common/Logger.ts +7 -2
- package/common/PipelineNames.ts +31 -0
- package/common/PipelinePattern.ts +70 -0
- package/common/PipelinePlanSchema.ts +490 -0
- package/common/backoff.ts +14 -0
- package/common/index.ts +39 -0
- package/common/pipelinePlan.ts +259 -0
- package/common/routeConvention.ts +1 -0
- package/common/types.ts +17 -0
- package/constant/constantRegistry.ts +26 -6
- package/dictionary/base.dictionary.ts +8 -0
- package/dictionary/trans.ts +9 -1
- package/document/into.ts +5 -0
- package/fetch/client/fetchClient.ts +135 -31
- package/fetch/client/httpClient.ts +45 -0
- package/fetch/client/wsClient.ts +125 -16
- package/fetch/fetchType/buildFetch.type.ts +1 -0
- package/index.ts +243 -10
- package/package.json +1 -5
- package/server/akanApp.ts +5 -2
- package/server/akanServer.ts +44 -8
- package/server/artifact/builderRpc.ts +8 -3
- package/server/artifact/ipcTypes.ts +5 -2
- package/server/di/diLifecycle.ts +39 -3
- package/server/di/utils.ts +28 -1
- package/server/ed25519Key.ts +48 -0
- package/server/index.ts +1 -0
- package/server/machine/index.ts +2 -0
- package/server/machine/machineAccount.ts +27 -0
- package/server/machine/machineToken.ts +151 -0
- package/server/mcp/McpApps.ts +256 -0
- package/server/mcp/McpDispatcher.ts +31 -7
- package/server/mcp/McpRouter.ts +209 -46
- package/server/ops/opsToken.ts +3 -10
- package/server/resolver/database.resolver.ts +3 -0
- package/server/resolver/signal.resolver.ts +40 -3
- package/server/routeElementComposer.tsx +78 -2
- package/server/routeState.ts +6 -0
- package/server/routeTreeBuilder.ts +26 -9
- package/server/routing/apiRouter.ts +6 -3
- package/server/routing/roomReplay.ts +88 -0
- package/server/rscClient.tsx +53 -8
- package/server/rscWorker.tsx +141 -27
- package/server/rscWorkerHost.ts +18 -5
- package/server/sitemap.ts +14 -0
- package/server/ssrFromRscRenderer.tsx +12 -6
- package/server/ssrTypes.ts +3 -1
- package/server/systemPageDocument.tsx +38 -30
- package/server/types.tsx +2 -0
- package/server/webRouter.ts +74 -5
- package/service/injectInfo.ts +9 -1
- package/service/ipcTypes.ts +2 -1
- package/service/predefinedAdaptor/database.adaptor.ts +1 -146
- package/service/predefinedAdaptor/queue.adaptor.ts +44 -6
- package/service/predefinedAdaptor/queueBackoff.ts +12 -0
- package/service/predefinedAdaptor/solidQueue.adaptor.ts +17 -13
- package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +44 -2
- package/service/predefinedAdaptor/sql/dialect/postgres.ts +3 -0
- package/service/predefinedAdaptor/sql/dialect/sqlite.ts +5 -2
- package/service/predefinedAdaptor/sql/search/fts5.ts +1 -1
- package/service/predefinedAdaptor/sql/types.ts +8 -6
- package/service/predefinedAdaptor/sql/values.ts +0 -23
- package/service/serve.ts +5 -1
- package/signal/agent.signal.ts +1 -1
- package/signal/base.signal.ts +2 -2
- package/signal/endpointIdempotency.ts +53 -0
- package/signal/internalArg.ts +14 -0
- package/signal/mcp/McpDocument.ts +38 -1
- package/signal/mcp/mcpProtocol.ts +18 -0
- package/signal/signalContext.ts +58 -30
- package/signal/signalRegistry.ts +14 -4
- package/signal/types.ts +18 -3
- package/store/agent/StoreSurfaceSource.ts +1 -1
- package/store/agentic/StToolBuilder.ts +3 -0
- package/test/conformance.ts +9 -33
- package/types/base/baseEnv.d.ts +0 -4
- package/types/base/primitiveRegistry.d.ts +1 -1
- package/types/client/csrLocation.d.ts +32 -0
- package/types/client/csrTypes.d.ts +40 -4
- package/types/client/frameConfig.d.ts +14 -2
- package/types/client/index.d.ts +3 -0
- package/types/client/native/external-view.d.ts +1 -0
- package/types/client/route/RootLayoutDefinition.d.ts +2 -1
- package/types/client/route/RouteHead.d.ts +13 -0
- package/types/client/route/index.d.ts +1 -0
- package/types/client/router.d.ts +6 -0
- package/types/client/rscNavigation.d.ts +4 -0
- package/types/client/viewportMeta.d.ts +18 -0
- package/types/client/webManifest.d.ts +4 -0
- package/types/common/PipelineNames.d.ts +6 -0
- package/types/common/PipelinePattern.d.ts +14 -0
- package/types/common/PipelinePlanSchema.d.ts +9 -0
- package/types/common/backoff.d.ts +10 -0
- package/types/common/index.d.ts +5 -0
- package/types/common/pipelinePlan.d.ts +224 -0
- package/types/common/types.d.ts +16 -0
- package/types/constant/constantRegistry.d.ts +5 -2
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictionary.d.ts +8 -8
- package/types/document/into.d.ts +9 -0
- package/types/fetch/client/fetchClient.d.ts +31 -4
- package/types/fetch/client/httpClient.d.ts +8 -0
- package/types/fetch/client/wsClient.d.ts +26 -2
- package/types/fetch/fetchType/buildFetch.type.d.ts +1 -0
- package/types/index.d.ts +230 -9
- package/types/server/akanServer.d.ts +16 -1
- package/types/server/artifact/builderRpc.d.ts +7 -2
- package/types/server/artifact/ipcTypes.d.ts +5 -1
- package/types/server/di/utils.d.ts +1 -0
- package/types/server/ed25519Key.d.ts +13 -0
- package/types/server/index.d.ts +1 -0
- package/types/server/machine/index.d.ts +2 -0
- package/types/server/machine/machineAccount.d.ts +5 -0
- package/types/server/machine/machineToken.d.ts +29 -0
- package/types/server/machine.d.ts +1 -0
- package/types/server/mcp/McpApps.d.ts +50 -0
- package/types/server/mcp/McpDispatcher.d.ts +4 -1
- package/types/server/mcp/McpRouter.d.ts +4 -0
- package/types/server/resolver/signal.resolver.d.ts +2 -1
- package/types/server/routeElementComposer.d.ts +24 -4
- package/types/server/routeState.d.ts +4 -1
- package/types/server/routing/roomReplay.d.ts +24 -0
- package/types/server/rscWorkerHost.d.ts +5 -2
- package/types/server/sitemap.d.ts +2 -0
- package/types/server/ssrFromRscRenderer.d.ts +3 -3
- package/types/server/ssrTypes.d.ts +4 -1
- package/types/server/systemPageDocument.d.ts +2 -0
- package/types/server/types.d.ts +4 -1
- package/types/server/webRouter.d.ts +3 -1
- package/types/service/ipcTypes.d.ts +2 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +1 -24
- package/types/service/predefinedAdaptor/queue.adaptor.d.ts +10 -3
- package/types/service/predefinedAdaptor/queueBackoff.d.ts +4 -0
- package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +3 -3
- package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -1
- package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
- package/types/service/predefinedAdaptor/sql/search/fts5.d.ts +1 -1
- package/types/service/predefinedAdaptor/sql/types.d.ts +9 -5
- package/types/service/predefinedAdaptor/sql/values.d.ts +0 -3
- package/types/service/serve.d.ts +3 -0
- package/types/signal/endpointIdempotency.d.ts +7 -0
- package/types/signal/internalArg.d.ts +10 -0
- package/types/signal/mcp/McpDocument.d.ts +8 -0
- package/types/signal/mcp/mcpProtocol.d.ts +17 -0
- package/types/signal/signalContext.d.ts +5 -3
- package/types/signal/signalRegistry.d.ts +4 -4
- package/types/signal/types.d.ts +21 -1
- package/types/store/agentic/StToolBuilder.d.ts +2 -0
- package/types/test/conformance.d.ts +7 -10
- package/types/ui/Agent/Chat.d.ts +5 -1
- package/types/ui/Agent/EmbedApproval.d.ts +1 -0
- package/types/ui/Agent/index.d.ts +3 -0
- package/types/ui/Agent/useKeyboardFrame.d.ts +2 -0
- package/types/ui/Agent/useScreenActions.d.ts +18 -0
- package/types/ui/BottomSheet.d.ts +1 -0
- package/types/ui/Constant/Doc.d.ts +11 -17
- package/types/ui/Constant/index.d.ts +7 -2
- package/types/ui/Constant/schemaDoc.d.ts +11 -0
- package/types/ui/Dialog/Modal.d.ts +6 -1
- package/types/ui/Dialog/index.d.ts +1 -1
- package/types/ui/Dialog/modalSurface.d.ts +12 -0
- package/types/ui/Layout/BottomTab.d.ts +11 -2
- package/types/ui/Layout/FrameSlotFallback.d.ts +9 -0
- package/types/ui/Layout/Navbar.d.ts +4 -1
- package/types/ui/Layout/TopInset.d.ts +1 -1
- package/types/ui/Layout/index.d.ts +3 -3
- package/types/ui/Link/Action.d.ts +5 -1
- package/types/ui/Link/Anchor.d.ts +5 -3
- package/types/ui/Link/activePath.d.ts +13 -0
- package/types/ui/Link/index.d.ts +1 -1
- package/types/ui/Modal.d.ts +6 -1
- package/types/ui/Model/EditModal.d.ts +6 -1
- package/types/ui/Model/New.d.ts +6 -1
- package/types/ui/Model/ViewEditModal.d.ts +6 -1
- package/types/ui/Model/ViewModal.d.ts +6 -1
- package/types/ui/Model/newFormOrigin.d.ts +7 -0
- package/types/ui/Portal.d.ts +3 -1
- package/types/ui/Reference/origin.d.ts +12 -0
- package/types/ui/Signal/Doc.d.ts +23 -18
- package/types/ui/Signal/Listener.d.ts +2 -6
- package/types/ui/Signal/Object.d.ts +6 -10
- package/types/ui/Signal/Response.d.ts +3 -6
- package/types/ui/Signal/RestApi.d.ts +11 -15
- package/types/ui/Signal/WebSocket.d.ts +11 -21
- package/types/ui/Signal/endpointEntries.d.ts +8 -0
- package/types/ui/Signal/index.d.ts +35 -12
- package/types/ui/Signal/signalText.d.ts +178 -0
- package/types/ui/System/CSR.d.ts +2 -2
- package/types/ui/System/Client.d.ts +4 -3
- package/types/ui/System/Common.d.ts +27 -6
- package/types/ui/System/SSR.d.ts +6 -6
- package/types/ui/Tooltip.d.ts +4 -2
- package/types/ui/index.d.ts +2 -1
- package/types/ui/overlayLayer.d.ts +6 -0
- package/types/ui/overlayPosition.d.ts +17 -0
- package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +14 -3
- package/types/vendor/@akanjs/native/packages/cli/src/commands/update.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +30 -5
- package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +8 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/distribution.d.ts +22 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +4 -0
- package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +16 -10
- package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +51 -7
- package/types/vendor/@akanjs/native/packages/cli/src/lib/vectors.d.ts +1 -1
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +7 -0
- package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows.d.ts +5 -0
- package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +36 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +2 -0
- package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +11 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/desktop.d.ts +17 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src/index.d.ts +132 -0
- package/types/vendor/@akanjs/native/plugins/external-view/src.d.ts +1 -0
- package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +8 -0
- package/types/vendor/@akanjs/native/plugins/window/src/desktop.d.ts +4 -1
- package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +49 -0
- package/types/vendor/use-agentic/types.d.ts +2 -0
- package/types/webkit/CsrStack.d.ts +1 -1
- package/types/webkit/McpAppAgent.d.ts +14 -0
- package/types/webkit/McpAppApprovals.d.ts +8 -0
- package/types/webkit/McpAppBridge.d.ts +101 -0
- package/types/webkit/McpAppView.d.ts +21 -0
- package/types/webkit/index.d.ts +1 -0
- package/types/webkit/useLocation.d.ts +1 -1
- package/types/webkit/useVisualViewportFrame.d.ts +13 -0
- package/ui/Agent/Chat.tsx +29 -10
- package/ui/Agent/Composer.tsx +2 -1
- package/ui/Agent/EmbedApproval.tsx +17 -0
- package/ui/Agent/RichInput.tsx +1 -1
- package/ui/Agent/index.ts +2 -0
- package/ui/Agent/useKeyboardFrame.ts +10 -0
- package/ui/Agent/useScreenActions.ts +45 -0
- package/ui/BottomSheet.tsx +82 -53
- package/ui/Constant/Doc.tsx +113 -60
- package/ui/Constant/index.ts +2 -2
- package/ui/Constant/schemaDoc.ts +86 -26
- package/ui/Dialog/LegacyModal.tsx +53 -52
- package/ui/Dialog/Modal.tsx +93 -49
- package/ui/Dialog/modalSurface.ts +61 -0
- package/ui/DragAction.tsx +1 -1
- package/ui/DraggableList.tsx +1 -1
- package/ui/Dropdown.tsx +4 -6
- package/ui/KeyboardAvoiding.tsx +1 -1
- package/ui/Layout/BottomInset.tsx +20 -14
- package/ui/Layout/BottomTab.tsx +42 -16
- package/ui/Layout/FrameSlotFallback.tsx +59 -0
- package/ui/Layout/Navbar.tsx +37 -11
- package/ui/Layout/Sider.tsx +1 -1
- package/ui/Layout/TopInset.tsx +15 -14
- package/ui/Link/Action.tsx +15 -5
- package/ui/Link/Anchor.tsx +25 -13
- package/ui/Link/activePath.ts +25 -0
- package/ui/Link/index.tsx +1 -1
- package/ui/Load/Pagination.tsx +1 -1
- package/ui/Load/Units.tsx +1 -1
- package/ui/Loading/ProgressBar.tsx +1 -1
- package/ui/Modal.tsx +9 -0
- package/ui/Model/EditModal.tsx +16 -1
- package/ui/Model/New.tsx +25 -2
- package/ui/Model/NewWrapper_Client.tsx +2 -0
- package/ui/Model/ViewEditModal.tsx +9 -0
- package/ui/Model/ViewModal.tsx +16 -1
- package/ui/Model/newFormOrigin.ts +45 -0
- package/ui/Popconfirm.tsx +5 -7
- package/ui/Portal.tsx +4 -7
- package/ui/Reference/origin.ts +31 -0
- package/ui/ScreenNavigator.tsx +1 -1
- package/ui/Select.tsx +2 -7
- package/ui/Signal/Arg.tsx +2 -2
- package/ui/Signal/Doc.tsx +125 -63
- package/ui/Signal/Endpoint.tsx +11 -10
- package/ui/Signal/Listener.tsx +7 -10
- package/ui/Signal/Object.tsx +7 -14
- package/ui/Signal/Response.tsx +24 -24
- package/ui/Signal/RestApi.tsx +63 -52
- package/ui/Signal/WebSocket.tsx +65 -72
- package/ui/Signal/endpointEntries.ts +25 -0
- package/ui/Signal/index.ts +23 -6
- package/ui/Signal/signalText.ts +59 -0
- package/ui/System/CSR.tsx +34 -8
- package/ui/System/Client.tsx +36 -5
- package/ui/System/Common.tsx +76 -32
- package/ui/System/SSR.tsx +73 -31
- package/ui/Toast.tsx +1 -1
- package/ui/Tooltip.tsx +84 -30
- package/ui/index.ts +11 -1
- package/ui/overlayLayer.ts +25 -2
- package/ui/overlayPosition.ts +90 -0
- package/ui/recipe/inputRecipe.ts +8 -1
- package/ui/styles.css +152 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +22 -2
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +37 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdateReports.kt +169 -0
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdates.kt +110 -38
- package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +20 -0
- package/vendor/@akanjs/native/native/desktop/Cargo.lock +16 -16
- package/vendor/@akanjs/native/native/desktop/src/external.rs +651 -0
- package/vendor/@akanjs/native/native/desktop/src/json.rs +23 -0
- package/vendor/@akanjs/native/native/desktop/src/lib.rs +228 -5
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeApp.swift +2 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +47 -1
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdateReports.swift +145 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdates.swift +112 -43
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +18 -0
- package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +24 -2
- package/vendor/@akanjs/native/packages/cli/src/api.ts +22 -6
- package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +24 -8
- package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +11 -9
- package/vendor/@akanjs/native/packages/cli/src/config.ts +35 -7
- package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +6 -4
- package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +11 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/distribution.ts +73 -0
- package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +15 -3
- package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +70 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +159 -23
- package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +2 -0
- package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +1 -1
- package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +4 -2
- package/vendor/@akanjs/native/packages/cli/src/platforms/ios.ts +9 -4
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +25 -11
- package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +14 -3
- package/vendor/@akanjs/native/packages/core/src/kernel.ts +49 -0
- package/vendor/@akanjs/native/packages/core/vectors/updates.json +31 -0
- package/vendor/@akanjs/native/packages/desktop/src/main.ts +3 -0
- package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +2 -0
- package/vendor/@akanjs/native/packages/desktop/src/server.ts +70 -6
- package/vendor/@akanjs/native/plugins/external-view/native-plugin.json +23 -0
- package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +321 -0
- package/vendor/@akanjs/native/plugins/external-view/src/index.ts +106 -0
- package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +133 -41
- package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +101 -36
- package/vendor/@akanjs/native/plugins/updates/native-plugin.json +4 -2
- package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +15 -1
- package/vendor/@akanjs/native/plugins/updates/src/index.ts +7 -1
- package/vendor/@akanjs/native/plugins/window/native-plugin.json +4 -0
- package/vendor/@akanjs/native/plugins/window/src/desktop.ts +68 -1
- package/vendor/@akanjs/native/plugins/window/src/index.ts +47 -0
- package/vendor/use-agentic/ToolRunner.ts +8 -1
- package/vendor/use-agentic/types.ts +2 -0
- package/webkit/CsrRouteTable.ts +1 -0
- package/webkit/CsrStack.ts +9 -2
- package/webkit/McpAppAgent.ts +138 -0
- package/webkit/McpAppApprovals.ts +56 -0
- package/webkit/McpAppBridge.ts +291 -0
- package/webkit/McpAppView.ts +258 -0
- package/webkit/bootCsr.tsx +54 -5
- package/webkit/index.ts +1 -0
- package/webkit/useCsrValues.ts +16 -10
- package/webkit/useFrameRuntime.ts +10 -1
- package/webkit/useLocation.ts +4 -3
- package/webkit/useVisualViewportFrame.tsx +52 -0
- package/service/predefinedAdaptor/sql/driver/libsql.ts +0 -45
- package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +0 -20
- package/types/ui/Agent/useKeyboardInset.d.ts +0 -1
- package/types/ui/ServerPortal.d.ts +0 -15
- package/ui/Agent/useKeyboardInset.ts +0 -19
- 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
|
|
22
|
-
* `<url>/android/<channel>.json`
|
|
23
|
-
* platform's on API 33+, AkanNativeEd25519 in plain Kotlin below)
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
val
|
|
186
|
-
val
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
throw Failure(AkanNativeErrorCode.INTERNAL, "the release
|
|
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
|
-
|
|
192
|
-
|
|
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(
|
|
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(
|
|
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") !=
|
|
204
|
-
if (o.optString("app") != config.app || o.optString("platform") != config.platform || o.optString("channel") != config.channel
|
|
205
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
281
|
-
File(staging, "manifest.json").writeBytes(release.
|
|
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
|
|
5
|
-
///
|
|
6
|
-
/// field below is trusted only after `
|
|
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
|
|
42
|
-
|
|
43
|
-
let
|
|
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),
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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(_
|
|
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:
|
|
207
|
-
guard (o["schema"] as? NSNumber)?.intValue ==
|
|
208
|
-
guard o["app"] as? String == config.app, o["platform"] as? String == config.platform, o["channel"] as? String == config.channel
|
|
209
|
-
|
|
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(
|
|
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
|
|
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
|
|
279
|
-
try release.
|
|
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
|
-
|
|
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
|
|