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
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// Web bundle updates (UP-2): which bundle the shell serves, and the trial / confirm / rollback
|
|
1
|
+
// Web bundle updates (UP-2, protocol v2): which bundle the shell serves, and the trial / confirm / rollback
|
|
2
2
|
// state. @akanjs/native/plugins/updates downloads and verifies bundles and calls stage/apply/confirm; the
|
|
3
3
|
// shell picks the bundle at launch (`launch`) and rolls back a trial that never confirms.
|
|
4
4
|
//
|
|
5
|
-
// Layout: Library/Application Support/akan-native-updates/{state.json, bundles/<bundle>/{…, manifest.json
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// state.json: { current?, pending?, trial?, failed: [bundle], rolledBack
|
|
10
|
-
// { bundle, sequence, nativeApi, attempts? }.
|
|
5
|
+
// Layout: Library/Application Support/akan-native-updates/{state.json, bundles/<bundle>/{…, manifest.signed.json}}
|
|
6
|
+
// (the filesystem plugin's `data` is Application Support/files). A bundle is served only while its envelope still
|
|
7
|
+
// verifies under a key of updates.publicKeys that is not revoked, and its files are all there with their sizes
|
|
8
|
+
// (checked at every launch).
|
|
9
|
+
// state.json: { current?, pending?, trial?, failed: [bundle], rolledBack?, revokedKeys: [keyId] } with entries
|
|
10
|
+
// { bundle, sequence, nativeApi, keyId, attempts? }. revokedKeys outlive reset(): a revocation is for good.
|
|
11
11
|
// - A downloaded bundle is `pending`. At the next launch (or apply()) it becomes `trial` with one
|
|
12
12
|
// attempt. notifyReady() makes the trial `current`. A trial that is launched again without
|
|
13
13
|
// having confirmed (it crashed or hung) is marked failed and the previous bundle serves.
|
|
@@ -27,22 +27,26 @@ struct AkanNativeUpdateEntry: Equatable, Sendable {
|
|
|
27
27
|
var bundle: String
|
|
28
28
|
var sequence: Double
|
|
29
29
|
var nativeApi: String
|
|
30
|
+
/// The key whose envelope the bundle came in.
|
|
31
|
+
var keyId: String
|
|
30
32
|
var attempts: Int = 0
|
|
31
33
|
|
|
32
|
-
init(bundle: String, sequence: Double, nativeApi: String, attempts: Int = 0) {
|
|
34
|
+
init(bundle: String, sequence: Double, nativeApi: String, keyId: String, attempts: Int = 0) {
|
|
33
35
|
self.bundle = bundle
|
|
34
36
|
self.sequence = sequence
|
|
35
37
|
self.nativeApi = nativeApi
|
|
38
|
+
self.keyId = keyId
|
|
36
39
|
self.attempts = attempts
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
init?(_ json: Any?) {
|
|
40
43
|
guard let o = json as? [String: Any], let bundle = o["bundle"] as? String, AkanNativeUpdates.isBundleId(bundle),
|
|
41
|
-
let sequence = (o["sequence"] as? NSNumber)?.doubleValue, let nativeApi = o["nativeApi"] as? String
|
|
42
|
-
|
|
44
|
+
let sequence = (o["sequence"] as? NSNumber)?.doubleValue, let nativeApi = o["nativeApi"] as? String,
|
|
45
|
+
let keyId = o["keyId"] as? String, AkanNativeKernel.isUpdateKeyId(keyId) else { return nil }
|
|
46
|
+
self.init(bundle: bundle, sequence: sequence, nativeApi: nativeApi, keyId: keyId, attempts: (o["attempts"] as? NSNumber)?.intValue ?? 0)
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
var json: [String: Any] { ["bundle": bundle, "sequence": sequence, "nativeApi": nativeApi, "attempts": attempts] }
|
|
49
|
+
var json: [String: Any] { ["bundle": bundle, "sequence": sequence, "nativeApi": nativeApi, "keyId": keyId, "attempts": attempts] }
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
struct AkanNativeUpdateState: Sendable {
|
|
@@ -51,8 +55,11 @@ struct AkanNativeUpdateState: Sendable {
|
|
|
51
55
|
var trial: AkanNativeUpdateEntry?
|
|
52
56
|
var failed: [String] = []
|
|
53
57
|
var rolledBack: String?
|
|
58
|
+
var revokedKeys: [String] = []
|
|
54
59
|
|
|
55
|
-
init() {
|
|
60
|
+
init(revokedKeys: [String] = []) {
|
|
61
|
+
self.revokedKeys = revokedKeys
|
|
62
|
+
}
|
|
56
63
|
|
|
57
64
|
init(_ o: [String: Any]) {
|
|
58
65
|
current = AkanNativeUpdateEntry(o["current"])
|
|
@@ -60,10 +67,11 @@ struct AkanNativeUpdateState: Sendable {
|
|
|
60
67
|
trial = AkanNativeUpdateEntry(o["trial"])
|
|
61
68
|
failed = (o["failed"] as? [String] ?? []).filter(AkanNativeUpdates.isBundleId)
|
|
62
69
|
rolledBack = o["rolledBack"] as? String
|
|
70
|
+
revokedKeys = (o["revokedKeys"] as? [String] ?? []).filter(AkanNativeKernel.isUpdateKeyId)
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
var json: [String: Any] {
|
|
66
|
-
var o: [String: Any] = ["failed": Array(failed.suffix(20))]
|
|
74
|
+
var o: [String: Any] = ["failed": Array(failed.suffix(20)), "revokedKeys": revokedKeys]
|
|
67
75
|
if let current { o["current"] = current.json }
|
|
68
76
|
if let pending { o["pending"] = pending.json }
|
|
69
77
|
if let trial { o["trial"] = trial.json }
|
|
@@ -75,33 +83,62 @@ struct AkanNativeUpdateState: Sendable {
|
|
|
75
83
|
/// updates.json from the build (akan-native.config.ts `updates`), nil when the app has no updates.
|
|
76
84
|
struct AkanNativeUpdateConfig: Sendable {
|
|
77
85
|
let app: String
|
|
86
|
+
let version: String
|
|
78
87
|
let platform: String
|
|
79
88
|
let nativeApi: String
|
|
80
89
|
let embeddedSequence: Double
|
|
81
90
|
let url: String
|
|
82
|
-
|
|
91
|
+
/// updates.publicKeys in order: the order decides which key may revoke which (AkanNativeKernel.updateDecision).
|
|
92
|
+
let keys: [(keyId: String, key: Data)]
|
|
83
93
|
let channel: String
|
|
84
94
|
let readyTimeout: TimeInterval
|
|
85
95
|
let dev: Bool
|
|
96
|
+
/// updates.report.url: where AkanNativeUpdateReports posts what became of each release; nil sends nothing.
|
|
97
|
+
let reportUrl: String?
|
|
98
|
+
|
|
99
|
+
var ring: [String] { keys.map(\.keyId) }
|
|
100
|
+
|
|
101
|
+
func key(_ keyId: String) -> Data? { keys.first { $0.keyId == keyId }?.key }
|
|
86
102
|
|
|
87
103
|
static func load() -> AkanNativeUpdateConfig? {
|
|
88
104
|
let o = AkanNativeResources.json("updates.json")
|
|
89
|
-
|
|
105
|
+
let encoded = o["publicKeys"] as? [String] ?? (o["publicKey"] as? String).map { [$0] } ?? []
|
|
106
|
+
let raw = encoded.compactMap { Data(base64Encoded: $0) }.filter { $0.count == 32 }
|
|
107
|
+
guard let app = o["app"] as? String, let url = o["url"] as? String, !raw.isEmpty, raw.count == encoded.count,
|
|
90
108
|
let nativeApi = o["nativeApi"] as? String else { return nil }
|
|
91
109
|
return AkanNativeUpdateConfig(
|
|
92
110
|
app: app,
|
|
111
|
+
version: o["version"] as? String ?? "",
|
|
93
112
|
platform: o["platform"] as? String ?? "ios",
|
|
94
113
|
nativeApi: nativeApi,
|
|
95
114
|
embeddedSequence: (o["embeddedSequence"] as? NSNumber)?.doubleValue ?? 0,
|
|
96
115
|
url: url,
|
|
97
|
-
|
|
116
|
+
keys: raw.map { (keyId: AkanNativeKernel.updateKeyId($0), key: $0) },
|
|
98
117
|
channel: o["channel"] as? String ?? "production",
|
|
99
118
|
readyTimeout: ((o["readyTimeout"] as? NSNumber)?.doubleValue ?? 10_000) / 1000,
|
|
100
|
-
dev: o["dev"] as? Bool ?? false
|
|
119
|
+
dev: o["dev"] as? Bool ?? false,
|
|
120
|
+
reportUrl: o["reportUrl"] as? String
|
|
101
121
|
)
|
|
102
122
|
}
|
|
103
123
|
}
|
|
104
124
|
|
|
125
|
+
/// A `<channel>/<nativeApi>.json` envelope, checked as far as its signature: what the manifest says is read after.
|
|
126
|
+
struct AkanNativeUpdateEnvelope {
|
|
127
|
+
let manifest: Data
|
|
128
|
+
let keyId: String
|
|
129
|
+
|
|
130
|
+
/// nil: not an envelope, or signed by a key this binary does not trust or has revoked.
|
|
131
|
+
static func open(_ data: Data, _ config: AkanNativeUpdateConfig, revoked: [String]) -> AkanNativeUpdateEnvelope? {
|
|
132
|
+
guard let o = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
133
|
+
let keyId = o["keyId"] as? String, AkanNativeKernel.isUpdateKeyId(keyId), !revoked.contains(keyId),
|
|
134
|
+
let raw = config.key(keyId), let key = try? Curve25519.Signing.PublicKey(rawRepresentation: raw),
|
|
135
|
+
let manifest = (o["manifest"] as? String).flatMap({ Data(base64Encoded: $0) }), manifest.count <= 1 << 20,
|
|
136
|
+
let signature = (o["signature"] as? String).flatMap({ Data(base64Encoded: $0) }),
|
|
137
|
+
key.isValidSignature(signature, for: manifest) else { return nil }
|
|
138
|
+
return AkanNativeUpdateEnvelope(manifest: manifest, keyId: keyId)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
105
142
|
@MainActor
|
|
106
143
|
enum AkanNativeUpdates {
|
|
107
144
|
static let config = AkanNativeUpdateConfig.load()
|
|
@@ -147,24 +184,40 @@ enum AkanNativeUpdates {
|
|
|
147
184
|
}
|
|
148
185
|
}
|
|
149
186
|
|
|
150
|
-
/// Whether a bundle may run in this binary.
|
|
151
|
-
static func usable(_ entry: AkanNativeUpdateEntry?, _ config: AkanNativeUpdateConfig) -> Bool {
|
|
187
|
+
/// Whether a bundle may run in this binary: one it still takes, whose files are intact.
|
|
188
|
+
static func usable(_ entry: AkanNativeUpdateEntry?, _ config: AkanNativeUpdateConfig, revoked: [String]) -> Bool {
|
|
152
189
|
guard let entry else { return false }
|
|
153
|
-
return entry
|
|
154
|
-
&& FileManager.default.fileExists(atPath: bundleDirectory(entry.bundle).appendingPathComponent("index.html").path)
|
|
155
|
-
&& verified(entry, config)
|
|
190
|
+
return takes(entry, config, revoked: revoked) && intact(entry, config, revoked: revoked)
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
///
|
|
193
|
+
/// Built for its native API, newer than its own bundle, and signed by a key it trusts and has not revoked: a bundle
|
|
194
|
+
/// a new binary or a revocation left behind is dropped as expected, not reported as a failure.
|
|
195
|
+
private static func takes(_ entry: AkanNativeUpdateEntry, _ config: AkanNativeUpdateConfig, revoked: [String]) -> Bool {
|
|
196
|
+
entry.nativeApi == config.nativeApi && entry.sequence > config.embeddedSequence
|
|
197
|
+
&& config.ring.contains(entry.keyId) && !revoked.contains(entry.keyId)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private static func intact(_ entry: AkanNativeUpdateEntry, _ config: AkanNativeUpdateConfig, revoked: [String]) -> Bool {
|
|
201
|
+
FileManager.default.fileExists(atPath: bundleDirectory(entry.bundle).appendingPathComponent("index.html").path)
|
|
202
|
+
&& verified(entry, config, revoked: revoked)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Drops an entry this launch cannot serve, reporting `event` (reason verify) when its files were the reason.
|
|
206
|
+
private static func drop(_ entry: AkanNativeUpdateEntry?, _ config: AkanNativeUpdateConfig, revoked: [String], _ event: String) -> AkanNativeUpdateEntry? {
|
|
207
|
+
guard let entry else { return nil }
|
|
208
|
+
guard takes(entry, config, revoked: revoked) else { return nil }
|
|
209
|
+
if intact(entry, config, revoked: revoked) { return entry }
|
|
210
|
+
AkanNativeUpdateReports.record(event, entry, reason: "verify")
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// The bundle's envelope (kept next to it by the plugin) verifies, describes this entry,
|
|
159
215
|
/// and every file it lists is there with its size. The hashes were checked at the download.
|
|
160
|
-
private static func verified(_ entry: AkanNativeUpdateEntry, _ config: AkanNativeUpdateConfig) -> Bool {
|
|
216
|
+
private static func verified(_ entry: AkanNativeUpdateEntry, _ config: AkanNativeUpdateConfig, revoked: [String]) -> Bool {
|
|
161
217
|
let dir = bundleDirectory(entry.bundle)
|
|
162
|
-
guard let
|
|
163
|
-
let
|
|
164
|
-
let
|
|
165
|
-
let key = try? Curve25519.Signing.PublicKey(rawRepresentation: config.publicKey),
|
|
166
|
-
key.isValidSignature(signature, for: manifest),
|
|
167
|
-
let o = try? JSONSerialization.jsonObject(with: manifest) as? [String: Any],
|
|
218
|
+
guard let data = try? Data(contentsOf: dir.appendingPathComponent("manifest.signed.json")),
|
|
219
|
+
let envelope = AkanNativeUpdateEnvelope.open(data, config, revoked: revoked), envelope.keyId == entry.keyId,
|
|
220
|
+
let o = try? JSONSerialization.jsonObject(with: envelope.manifest) as? [String: Any],
|
|
168
221
|
o["bundle"] as? String == entry.bundle, (o["sequence"] as? NSNumber)?.doubleValue == entry.sequence,
|
|
169
222
|
o["nativeApi"] as? String == entry.nativeApi, o["app"] as? String == config.app, o["platform"] as? String == config.platform,
|
|
170
223
|
let files = o["files"] as? [[String: Any]] else { return false }
|
|
@@ -180,15 +233,16 @@ enum AkanNativeUpdates {
|
|
|
180
233
|
static func launch() -> URL? {
|
|
181
234
|
guard let config else { return nil }
|
|
182
235
|
var state = readState()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
236
|
+
state.current = drop(state.current, config, revoked: state.revokedKeys, "rolledBack")
|
|
237
|
+
state.pending = drop(state.pending, config, revoked: state.revokedKeys, "failed")
|
|
238
|
+
state.trial = drop(state.trial, config, revoked: state.revokedKeys, "rolledBack")
|
|
186
239
|
if let trial = state.trial {
|
|
187
240
|
// Launched again without notifyReady(): the trial crashed or hung.
|
|
188
241
|
state.failed.append(trial.bundle)
|
|
189
242
|
state.rolledBack = trial.bundle
|
|
190
243
|
state.trial = nil
|
|
191
244
|
NSLog("[akan-native] updates: bundle %@ did not confirm; rolled back", trial.bundle)
|
|
245
|
+
AkanNativeUpdateReports.record("rolledBack", trial, reason: "relaunch")
|
|
192
246
|
}
|
|
193
247
|
if var pending = state.pending, !state.failed.contains(pending.bundle) {
|
|
194
248
|
pending.attempts = 1
|
|
@@ -198,8 +252,10 @@ enum AkanNativeUpdates {
|
|
|
198
252
|
if !writeState(state), state.trial != nil {
|
|
199
253
|
state.trial = nil // not on record: the pending bundle waits for a launch that can record it
|
|
200
254
|
}
|
|
255
|
+
if let trial = state.trial { AkanNativeUpdateReports.record("applied", trial) }
|
|
201
256
|
active = state.trial ?? state.current
|
|
202
257
|
onTrial = state.trial != nil
|
|
258
|
+
AkanNativeUpdateReports.start()
|
|
203
259
|
return active.map { bundleDirectory($0.bundle) }
|
|
204
260
|
}
|
|
205
261
|
|
|
@@ -208,20 +264,21 @@ enum AkanNativeUpdates {
|
|
|
208
264
|
var state = readState()
|
|
209
265
|
state.pending = entry
|
|
210
266
|
state.failed.removeAll { $0 == entry.bundle }
|
|
211
|
-
writeState(state)
|
|
267
|
+
if writeState(state) { AkanNativeUpdateReports.record("downloaded", entry) }
|
|
212
268
|
}
|
|
213
269
|
|
|
214
270
|
/// apply(): makes the pending bundle the trial now; returns its folder for the reload.
|
|
215
271
|
static func applyPending() -> URL? {
|
|
216
272
|
guard let config else { return nil }
|
|
217
273
|
var state = readState()
|
|
218
|
-
guard var pending = state.pending, usable(pending, config) else { return nil }
|
|
274
|
+
guard var pending = state.pending, usable(pending, config, revoked: state.revokedKeys) else { return nil }
|
|
219
275
|
pending.attempts = 1
|
|
220
276
|
state.trial = pending
|
|
221
277
|
state.pending = nil
|
|
222
278
|
guard writeState(state) else { return nil }
|
|
223
279
|
active = pending
|
|
224
280
|
onTrial = true
|
|
281
|
+
AkanNativeUpdateReports.record("applied", pending)
|
|
225
282
|
return bundleDirectory(pending.bundle)
|
|
226
283
|
}
|
|
227
284
|
|
|
@@ -230,34 +287,46 @@ enum AkanNativeUpdates {
|
|
|
230
287
|
guard onTrial, let trial = active else { return }
|
|
231
288
|
var state = readState()
|
|
232
289
|
guard state.trial?.bundle == trial.bundle else { return }
|
|
233
|
-
state.current = AkanNativeUpdateEntry(bundle: trial.bundle, sequence: trial.sequence, nativeApi: trial.nativeApi)
|
|
290
|
+
state.current = AkanNativeUpdateEntry(bundle: trial.bundle, sequence: trial.sequence, nativeApi: trial.nativeApi, keyId: trial.keyId)
|
|
234
291
|
state.trial = nil
|
|
235
292
|
state.rolledBack = nil
|
|
236
293
|
writeState(state)
|
|
237
294
|
onTrial = false
|
|
295
|
+
AkanNativeUpdateReports.record("confirmed", trial)
|
|
238
296
|
prune(keep: [state.current?.bundle, state.pending?.bundle].compactMap { $0 })
|
|
239
297
|
}
|
|
240
298
|
|
|
241
|
-
/// The trial did not confirm in time: back to the previous bundle. Returns its folder (nil: the app's own)
|
|
242
|
-
|
|
243
|
-
|
|
299
|
+
/// The trial did not confirm in time: back to the previous bundle. Returns its folder (nil: the app's own) and the
|
|
300
|
+
/// trial it ended, whose report waits for the renderer probe to say whether the page timed out or hung.
|
|
301
|
+
static func rollback() -> (root: URL?, trial: AkanNativeUpdateEntry?) {
|
|
302
|
+
guard onTrial, let trial = active else { return (active.map { bundleDirectory($0.bundle) }, nil) }
|
|
244
303
|
var state = readState()
|
|
245
304
|
state.trial = nil
|
|
246
305
|
state.failed.append(trial.bundle)
|
|
247
306
|
state.rolledBack = trial.bundle
|
|
248
307
|
writeState(state)
|
|
249
308
|
onTrial = false
|
|
250
|
-
active = config.flatMap { usable(state.current, $0) ? state.current : nil }
|
|
309
|
+
active = config.flatMap { usable(state.current, $0, revoked: state.revokedKeys) ? state.current : nil }
|
|
251
310
|
NSLog("[akan-native] updates: bundle %@ did not call notifyReady() in time; rolled back", trial.bundle)
|
|
252
|
-
return active.map { bundleDirectory($0.bundle) }
|
|
311
|
+
return (active.map { bundleDirectory($0.bundle) }, trial)
|
|
253
312
|
}
|
|
254
313
|
|
|
255
|
-
/// reset(): the app's own bundle from the next launch on.
|
|
314
|
+
/// reset(): the app's own bundle from the next launch on. Revocations stay.
|
|
256
315
|
static func reset() {
|
|
257
|
-
writeState(AkanNativeUpdateState())
|
|
316
|
+
writeState(AkanNativeUpdateState(revokedKeys: readState().revokedKeys))
|
|
258
317
|
prune(keep: active.map { [$0.bundle] } ?? [])
|
|
259
318
|
}
|
|
260
319
|
|
|
320
|
+
/// Keys a verified manifest revoked: no bundle they signed is served again, from the next launch on.
|
|
321
|
+
static func revoke(_ keyIds: [String]) {
|
|
322
|
+
var state = readState()
|
|
323
|
+
let added = keyIds.filter { !state.revokedKeys.contains($0) }
|
|
324
|
+
guard !added.isEmpty else { return }
|
|
325
|
+
state.revokedKeys += added
|
|
326
|
+
writeState(state)
|
|
327
|
+
NSLog("[akan-native] updates: revoked keys %@", added.joined(separator: ", "))
|
|
328
|
+
}
|
|
329
|
+
|
|
261
330
|
/// A download in progress (bundles/<bundle>.partial) is the plugin's, which removes it.
|
|
262
331
|
private static func prune(keep: [String]) {
|
|
263
332
|
let folder = directory.appendingPathComponent("bundles", isDirectory: true)
|
|
@@ -141,6 +141,24 @@ enum AkanNativeVectors {
|
|
|
141
141
|
check(valid == c[3] as? Bool, "ed25519 \(c[4])")
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
// Updates protocol v2 (AkanNativeUpdates, AkanNativeKernel.updateDecision).
|
|
145
|
+
let updates = load("updates")
|
|
146
|
+
for c in rows(updates, "keyIds") {
|
|
147
|
+
check(AkanNativeKernel.updateKeyId(Data(base64Encoded: c[0] as! String) ?? Data()) == c[1] as? String, "update keyId \(c[0])")
|
|
148
|
+
}
|
|
149
|
+
for c in rows(updates, "decisions") {
|
|
150
|
+
let running = (c[7] as? [Any]).map { (sequence: ($0[0] as! NSNumber).doubleValue, keyId: $0[1] as! String) }
|
|
151
|
+
let got = AkanNativeKernel.updateDecision(
|
|
152
|
+
ring: c[0] as! [String], revoked: c[1] as! [String], signer: c[2] as! String, revokeKeys: c[3] as! [String],
|
|
153
|
+
resetSequence: c[4] as! Bool, sequence: (c[5] as! NSNumber).doubleValue, embedded: (c[6] as! NSNumber).doubleValue, running: running
|
|
154
|
+
)
|
|
155
|
+
check(got.wire == c[8] as? String, "update decision \(c[9]): \(got.wire)")
|
|
156
|
+
}
|
|
157
|
+
for c in rows(updates, "urls") {
|
|
158
|
+
let got = AkanNativeKernel.updateManifestURL(c[0] as! String, platform: c[1] as! String, channel: c[2] as! String, nativeApi: c[3] as! String)
|
|
159
|
+
check(got == c[4] as? String, "update url \(got)")
|
|
160
|
+
}
|
|
161
|
+
|
|
144
162
|
// A missing or truncated file must not pass as fewer checks.
|
|
145
163
|
for (name, count) in AkanNativeVectorData.counts where ran[name, default: 0] != count {
|
|
146
164
|
failures.append("\(name): \(ran[name, default: 0]) of \(count) checks ran")
|
|
@@ -185,6 +185,7 @@ final class AkanNativeViewController: UIViewController, WKNavigationDelegate, WK
|
|
|
185
185
|
// MARK: web bundle updates (UP-2)
|
|
186
186
|
|
|
187
187
|
private var readyTimer: Timer?
|
|
188
|
+
private var rendererAnswered = false
|
|
188
189
|
|
|
189
190
|
/// A bundle on trial must call notifyReady() within updates.readyTimeout of loading.
|
|
190
191
|
private func startReadyTimer() {
|
|
@@ -194,12 +195,32 @@ final class AkanNativeViewController: UIViewController, WKNavigationDelegate, WK
|
|
|
194
195
|
readyTimer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
|
|
195
196
|
MainActor.assumeIsolated {
|
|
196
197
|
guard let self, AkanNativeUpdates.onTrial else { return }
|
|
197
|
-
self.
|
|
198
|
+
self.rollBackTrial()
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
#endif
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
#if AKAN_NATIVE_UPDATES
|
|
205
|
+
/// A web content process stuck in a script never runs the next load: one that does not answer is left behind with
|
|
206
|
+
/// its window, and a new window serves the bundle the rollback made active.
|
|
207
|
+
private func rollBackTrial() {
|
|
208
|
+
let (root, trial) = AkanNativeUpdates.rollback()
|
|
209
|
+
rendererAnswered = false
|
|
210
|
+
webView.evaluateJavaScript("0") { [weak self] _, _ in self?.rendererAnswered = true }
|
|
211
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
|
|
212
|
+
guard let self else { return }
|
|
213
|
+
if let trial { AkanNativeUpdateReports.record("rolledBack", trial, reason: self.rendererAnswered ? "timeout" : "hang") }
|
|
214
|
+
if !self.rendererAnswered, let scene = self.view.window?.windowScene?.delegate as? AkanNativeSceneDelegate {
|
|
215
|
+
AkanNativeLog.info("updates: the page on trial does not answer; making the window again")
|
|
216
|
+
scene.recreate()
|
|
217
|
+
} else {
|
|
218
|
+
self.serveBundle(root)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
#endif
|
|
223
|
+
|
|
203
224
|
/// Serves another web bundle (nil: the app's own) and loads it from the start.
|
|
204
225
|
func serveBundle(_ root: URL?) {
|
|
205
226
|
readyTimer?.invalidate()
|
|
@@ -296,6 +317,8 @@ final class AkanNativeViewController: UIViewController, WKNavigationDelegate, WK
|
|
|
296
317
|
/// fail or turn into a download and leave the page in place.
|
|
297
318
|
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
298
319
|
bridge.endDocument()
|
|
320
|
+
// Armed at commit, not at didFinish: a bundle stuck in a script never finishes loading.
|
|
321
|
+
startReadyTimer()
|
|
299
322
|
}
|
|
300
323
|
|
|
301
324
|
/// The window is going away (the scene disconnected, or $host.recreate): its page's document ends.
|
|
@@ -306,7 +329,6 @@ final class AkanNativeViewController: UIViewController, WKNavigationDelegate, WK
|
|
|
306
329
|
|
|
307
330
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
308
331
|
if config.splashAutoHide { hideSplash(fadeOut: 0.2) }
|
|
309
|
-
startReadyTimer()
|
|
310
332
|
}
|
|
311
333
|
|
|
312
334
|
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
|
@@ -6,6 +6,7 @@ import { type DoctorCheck, doctorChecks, hostPlatforms, runFixes } from "./comma
|
|
|
6
6
|
import type { AkanNativeConfig } from "./config.ts";
|
|
7
7
|
import { makeNativeBoot } from "./lib/boot.ts";
|
|
8
8
|
import { BUNDLE_FILE, compatProblems, readBundleInfo } from "./lib/compat.ts";
|
|
9
|
+
import { requestedDistribution } from "./lib/distribution.ts";
|
|
9
10
|
import { CancelledError, withSignal } from "./lib/exec.ts";
|
|
10
11
|
import { type HmrContext, lanAddress, resolveDevEntry, startHmrServer } from "./lib/hmr.ts";
|
|
11
12
|
import type { Device, DeviceSelector, Launched } from "./lib/launch.ts";
|
|
@@ -47,6 +48,12 @@ import { androidDevices, launchAndroid } from "./platforms/android.ts";
|
|
|
47
48
|
import { PLATFORM_TARGETS, type TargetPlatform } from "./platforms/index.ts";
|
|
48
49
|
import { iosDevices, physicalIosDevice } from "./platforms/ios.ts";
|
|
49
50
|
|
|
51
|
+
export { type UpdateDecision, type UpdateDecisionInput, updateDecision } from "../../core/src/kernel.ts";
|
|
52
|
+
/**
|
|
53
|
+
* A phone's update protocol as the apps run it, for a signer or a server that checks what it publishes or registers:
|
|
54
|
+
* the envelope's verification, the keyId of a public key, and what a verified manifest means to a binary.
|
|
55
|
+
*/
|
|
56
|
+
export { keyIdOf, openEnvelope } from "./lib/updates.ts";
|
|
50
57
|
export type {
|
|
51
58
|
AkanNativeConfig,
|
|
52
59
|
AndroidSigning,
|
|
@@ -71,10 +78,10 @@ export type {
|
|
|
71
78
|
* environment (the API itself reads no variable): pass the result as `macos`.
|
|
72
79
|
*/
|
|
73
80
|
/** The Authenticode settings the AKAN_NATIVE_WINDOWS_* variables name: pass the result as `windows.signing`. */
|
|
74
|
-
export { macosDistributionFromEnv, windowsSigningFromEnv };
|
|
81
|
+
export { macosDistributionFromEnv, requestedDistribution, windowsSigningFromEnv };
|
|
75
82
|
|
|
76
83
|
/** semver of this API. A caller checks the major before it relies on anything here. */
|
|
77
|
-
export const API_VERSION = "0.
|
|
84
|
+
export const API_VERSION = "0.11.0";
|
|
78
85
|
|
|
79
86
|
export type AkanNativeErrorCode =
|
|
80
87
|
| "CONFIG_INVALID"
|
|
@@ -293,6 +300,8 @@ export interface PublishCheckOptions extends Pick<TaskOptions, "appDir" | "confi
|
|
|
293
300
|
out?: string;
|
|
294
301
|
/** Whether the release will carry a server; the config's `desktop.server` by default. */
|
|
295
302
|
server?: boolean;
|
|
303
|
+
/** A desktop release's CPU, which names its feed folder: this computer's by default. */
|
|
304
|
+
arch?: DesktopArch;
|
|
296
305
|
}
|
|
297
306
|
|
|
298
307
|
/**
|
|
@@ -302,14 +311,18 @@ export interface PublishCheckOptions extends Pick<TaskOptions, "appDir" | "confi
|
|
|
302
311
|
*/
|
|
303
312
|
export function checkPublishUpdate(options: PublishCheckOptions): { channel: string; out: string } {
|
|
304
313
|
if (!options.config.updates)
|
|
305
|
-
throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url,
|
|
314
|
+
throw new AkanNativeError("CONFIG_INVALID", "the config has no updates: { url, publicKeys } to publish for");
|
|
306
315
|
const channel = options.channel ?? options.config.updates.channel ?? "production";
|
|
307
316
|
const out = resolve(options.out ?? resolve(options.appDir, ".akan", "native", "updates"));
|
|
308
317
|
try {
|
|
309
318
|
if (options.channel !== undefined) assertChannel(options.channel);
|
|
310
|
-
|
|
319
|
+
const { publicKey, publicKeys } = options.config.updates;
|
|
320
|
+
assertSigningKey(
|
|
321
|
+
{ app: options.config.app, updates: { publicKeys: publicKeys ?? (publicKey ? [publicKey] : []) } },
|
|
322
|
+
options.platform,
|
|
323
|
+
);
|
|
311
324
|
const server = options.server ?? !!options.config.desktop?.server;
|
|
312
|
-
assertServerOfChannel({ desktop: server ? { server } : {} }, options.platform, out, channel);
|
|
325
|
+
assertServerOfChannel({ desktop: server ? { server } : {} }, options.platform, out, channel, options.arch);
|
|
313
326
|
} catch (error) {
|
|
314
327
|
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
315
328
|
}
|
|
@@ -329,6 +342,8 @@ export function publishUpdate(
|
|
|
329
342
|
macos?: Pick<MacosBuild, "signing" | "notarize">;
|
|
330
343
|
/** Authenticode for a Windows release's files. No setup program is made. */
|
|
331
344
|
windows?: Pick<WindowsBuild, "signing">;
|
|
345
|
+
/** A desktop release's CPU, of the same OS: an x64 release from an arm64 machine. Default this computer's. */
|
|
346
|
+
arch?: DesktopArch;
|
|
332
347
|
},
|
|
333
348
|
): Promise<PublishResult> {
|
|
334
349
|
return task(options, async (warnings) => {
|
|
@@ -336,6 +351,7 @@ export function publishUpdate(
|
|
|
336
351
|
const { ctx, artifact, result } = await buildIn(options, "release", options.mode ?? "production", warnings, {
|
|
337
352
|
...(options.macos ? { macos: { ...options.macos, dmg: false } } : {}),
|
|
338
353
|
...(options.windows?.signing ? { windows: { signing: options.windows.signing } } : {}),
|
|
354
|
+
...(options.arch ? { arch: options.arch } : {}),
|
|
339
355
|
});
|
|
340
356
|
const { dir, manifest } = await publishRelease(ctx, options.platform, artifact, out, channel).catch((error) => {
|
|
341
357
|
throw toAkanNativeError(error, "CONFIG_INVALID");
|
|
@@ -366,7 +382,7 @@ export function compareBundles(shipped: string, bundle: string): { compatible: b
|
|
|
366
382
|
|
|
367
383
|
/**
|
|
368
384
|
* The app's update signing key: made once (AKAN_NATIVE_UPDATE_KEY, else ~/.akan/native/keys/<app id>.update.key),
|
|
369
|
-
* then read. `publicKey` goes into the config's `updates.
|
|
385
|
+
* then read. `publicKey` goes into the config's `updates.publicKeys`; the key file never leaves the machine.
|
|
370
386
|
*/
|
|
371
387
|
export function updateKeygen(options: { config: AkanNativeConfig }): {
|
|
372
388
|
publicKey: string;
|
|
@@ -2,16 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
import { relative } from "node:path";
|
|
4
4
|
import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
|
|
5
|
+
import { requestedDistribution } from "../lib/distribution.ts";
|
|
5
6
|
import { bold, CliError, dim, log } from "../lib/log.ts";
|
|
6
|
-
import { macosDistributionFromEnv } from "../lib/macossigning.ts";
|
|
7
7
|
import { type BuildContext, type BuildProfile, prepare } from "../lib/prepare.ts";
|
|
8
8
|
import { findAppDir, loadProject } from "../lib/project.ts";
|
|
9
9
|
import type { DesktopArch } from "../lib/updates.ts";
|
|
10
|
-
import { windowsSigningFromEnv } from "../lib/windowssigning.ts";
|
|
11
10
|
import { PLATFORM_TARGETS, TARGETS, type TargetPlatform } from "../platforms/index.ts";
|
|
12
11
|
import { physicalIosDevice } from "../platforms/ios.ts";
|
|
13
12
|
|
|
14
|
-
export const BOOLEAN_FLAGS = [
|
|
13
|
+
export const BOOLEAN_FLAGS = [
|
|
14
|
+
"skip-web-build",
|
|
15
|
+
"open",
|
|
16
|
+
"release",
|
|
17
|
+
"debug",
|
|
18
|
+
"headless",
|
|
19
|
+
"aab",
|
|
20
|
+
"installer",
|
|
21
|
+
"sign",
|
|
22
|
+
"notarize",
|
|
23
|
+
];
|
|
15
24
|
|
|
16
25
|
/** Flags of every build: the app, the env mode (.env.<mode>), the profile, the web build. */
|
|
17
26
|
export const BUILD_FLAGS = [
|
|
@@ -25,11 +34,13 @@ export const BUILD_FLAGS = [
|
|
|
25
34
|
"device",
|
|
26
35
|
"build",
|
|
27
36
|
"arch",
|
|
37
|
+
"sign",
|
|
38
|
+
"notarize",
|
|
28
39
|
];
|
|
29
40
|
/** Flags of launching a build (web server, simulators, emulators). */
|
|
30
41
|
export const LAUNCH_FLAGS = ["port", "host", "open", "headless", "device", "avd"];
|
|
31
42
|
|
|
32
|
-
export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>] [--arch <arm64|x64>]`;
|
|
43
|
+
export const BUILD_USAGE = `akan-native build <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--debug] [--skip-web-build] [--aab] [--installer] [--device] [--build <n>] [--arch <arm64|x64>] [--sign] [--notarize]`;
|
|
33
44
|
export const RUN_USAGE = `akan-native run <${TARGETS.join("|")}> [--app <dir>] [--mode <mode>] [--release] [--skip-web-build] [--port <n>] [--open] [--headless] [--device <name>] [--avd <name>] [--start <path>]`;
|
|
34
45
|
|
|
35
46
|
export interface BuildDefaults {
|
|
@@ -77,6 +88,13 @@ export async function buildFromArgs(
|
|
|
77
88
|
const device = args.flags.device;
|
|
78
89
|
const iphone = platform === "ios" && typeof device === "string" ? await physicalIosDevice(device) : null;
|
|
79
90
|
const iosDevice = platform === "ios" && (device === true || iphone !== null);
|
|
91
|
+
const distribution = requestedDistribution(platform, profile, {
|
|
92
|
+
sign: args.flags.sign === true,
|
|
93
|
+
notarize: args.flags.notarize === true,
|
|
94
|
+
});
|
|
95
|
+
if (distribution.notice) log.warn(distribution.notice);
|
|
96
|
+
else if (distribution.unused.length && (platform === "macos" || platform === "windows"))
|
|
97
|
+
log.info(dim(`${distribution.unused.join(", ")} set: this build is not signed without --sign`));
|
|
80
98
|
const ctx = await prepare(project, platform, {
|
|
81
99
|
mode: stringFlag(args, "mode") ?? defaults.mode,
|
|
82
100
|
profile,
|
|
@@ -89,16 +107,14 @@ export async function buildFromArgs(
|
|
|
89
107
|
? {
|
|
90
108
|
windows: {
|
|
91
109
|
...(args.flags.installer === true ? { installer: true } : {}),
|
|
92
|
-
|
|
93
|
-
...(profile === "release" ? { signing: windowsSigningFromEnv() } : {}),
|
|
110
|
+
...distribution.windows,
|
|
94
111
|
},
|
|
95
112
|
}
|
|
96
113
|
: {}),
|
|
97
|
-
|
|
98
114
|
...(platform === "macos"
|
|
99
115
|
? {
|
|
100
116
|
macos: {
|
|
101
|
-
...
|
|
117
|
+
...distribution.macos,
|
|
102
118
|
...(args.flags.installer === true ? { dmg: true } : {}),
|
|
103
119
|
},
|
|
104
120
|
}
|
|
@@ -4,13 +4,14 @@ import { existsSync, statSync } from "node:fs";
|
|
|
4
4
|
import { join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { checkFlags, type ParsedArgs, parseArgs, stringFlag } from "../lib/args.ts";
|
|
6
6
|
import { bold, CliError, dim, log } from "../lib/log.ts";
|
|
7
|
+
import { desktopArch } from "../lib/prepare.ts";
|
|
7
8
|
import { findAppDir, loadProject } from "../lib/project.ts";
|
|
8
9
|
import { assertServerOfChannel, assertSigningKey, publishRelease } from "../lib/publish.ts";
|
|
9
|
-
import { assertChannel, generateUpdateKey, updateKeyPath } from "../lib/updates.ts";
|
|
10
|
+
import { assertChannel, type DesktopArch, generateUpdateKey, updateKeyPath } from "../lib/updates.ts";
|
|
10
11
|
import { BOOLEAN_FLAGS, buildFromArgs } from "./build.ts";
|
|
11
12
|
|
|
12
13
|
export const UPDATE_USAGE =
|
|
13
|
-
"akan-native update keygen [--app <dir>] | akan-native update publish <ios|android|macos|windows|linux> [--app <dir>] [--mode <mode>] [--debug] [--channel <name>] [--out <dir>] | akan-native update serve [--app <dir>] [--out <dir>] [--port <n>]";
|
|
14
|
+
"akan-native update keygen [--app <dir>] | akan-native update publish <ios|android|macos|windows|linux> [--app <dir>] [--mode <mode>] [--debug] [--channel <name>] [--out <dir>] [--arch <arm64|x64>] [--sign] [--notarize] | akan-native update serve [--app <dir>] [--out <dir>] [--port <n>]";
|
|
14
15
|
|
|
15
16
|
const outDir = (args: ParsedArgs, appDir: string) =>
|
|
16
17
|
resolve(stringFlag(args, "out") ?? join(appDir, ".akan", "native", "updates"));
|
|
@@ -25,10 +26,10 @@ async function keygen(args: ParsedArgs): Promise<number> {
|
|
|
25
26
|
);
|
|
26
27
|
else log.info(`using the existing key ${path}`);
|
|
27
28
|
log.info(
|
|
28
|
-
`Add to akan-native.config.ts:\n\n updates: { url: "https://…/updates",
|
|
29
|
+
`Add to akan-native.config.ts:\n\n updates: { url: "https://…/updates", publicKeys: [${JSON.stringify(publicKey)}] },\n`,
|
|
29
30
|
);
|
|
30
|
-
if (project.config.updates && project.config.updates.publicKey
|
|
31
|
-
log.warn("akan-native.config.ts has
|
|
31
|
+
if (project.config.updates && !project.config.updates.publicKeys.includes(publicKey))
|
|
32
|
+
log.warn("akan-native.config.ts has updates.publicKeys without this key");
|
|
32
33
|
return 0;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -40,12 +41,13 @@ async function publish(args: ParsedArgs): Promise<number> {
|
|
|
40
41
|
const requested = stringFlag(args, "channel");
|
|
41
42
|
if (requested !== undefined) assertChannel(requested);
|
|
42
43
|
const project = await loadProject(findAppDir(stringFlag(args, "app") ?? process.cwd()));
|
|
43
|
-
assertSigningKey(project.config);
|
|
44
|
+
assertSigningKey(project.config, platform);
|
|
44
45
|
assertServerOfChannel(
|
|
45
46
|
project.config,
|
|
46
47
|
platform,
|
|
47
48
|
outDir(args, project.appDir),
|
|
48
49
|
requested ?? project.config.updates?.channel ?? "production",
|
|
50
|
+
desktopArch(platform, stringFlag(args, "arch") as DesktopArch | undefined),
|
|
49
51
|
);
|
|
50
52
|
const { ctx, artifact } = await buildFromArgs(
|
|
51
53
|
{ positional: [platform], flags: args.flags },
|
|
@@ -54,10 +56,10 @@ async function publish(args: ParsedArgs): Promise<number> {
|
|
|
54
56
|
);
|
|
55
57
|
const { config, appDir } = ctx.project;
|
|
56
58
|
const channel = requested ?? config.updates?.channel ?? "production";
|
|
57
|
-
const { dir, manifest } = await publishRelease(ctx, platform, artifact, outDir(args, appDir), channel);
|
|
59
|
+
const { dir, manifest, file } = await publishRelease(ctx, platform, artifact, outDir(args, appDir), channel);
|
|
58
60
|
const size = manifest.files.reduce((n, f) => n + f.size, 0);
|
|
59
61
|
log.ok(
|
|
60
|
-
`published ${bold(manifest.bundle)} to ${relative(process.cwd(),
|
|
62
|
+
`published ${bold(manifest.bundle)} to ${relative(process.cwd(), file) || file} ${dim(`(${manifest.files.length} files, ${(size / 1024).toFixed(0)} KiB${manifest.nativeApi ? `, native API ${manifest.nativeApi}` : ""})`)}`,
|
|
61
63
|
);
|
|
62
64
|
log.info(
|
|
63
65
|
dim(
|
|
@@ -97,7 +99,7 @@ export async function update(argv: string[]): Promise<number> {
|
|
|
97
99
|
|
|
98
100
|
const known = {
|
|
99
101
|
keygen: ["app"],
|
|
100
|
-
publish: ["app", "mode", "debug", "channel", "out", "skip-web-build"],
|
|
102
|
+
publish: ["app", "mode", "debug", "channel", "out", "skip-web-build", "arch", "sign", "notarize"],
|
|
101
103
|
serve: ["app", "out", "port"],
|
|
102
104
|
}[args.positional[0] as string];
|
|
103
105
|
if (known) checkFlags(args, known, UPDATE_USAGE);
|