@senguoyun/dsh-arkme 0.1.16 → 0.1.22
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/README.md +9 -0
- package/assets/desktop_call/arkme-video-linear.svg +5 -0
- package/assets/desktop_call/bundle.js +104 -5
- package/assets/desktop_call/call-demo-peer.png +0 -0
- package/assets/desktop_call/call-demo-self.png +0 -0
- package/assets/desktop_call/index.html +4 -2
- package/assets/desktop_call/manifest.json +1 -1
- package/assets/windows/arkme-credential-helper.exe +0 -0
- package/assets/windows/manifest.json +9 -0
- package/cordis.patch.yml +2 -0
- package/docs/consumer-plugin-contract.md +3 -1
- package/docs/plugin-release.md +23 -0
- package/lib/client.js +48592 -11863
- package/lib/client.js.map +1 -1
- package/lib/extension-profile-restart-helper.js +1 -1
- package/lib/index.js +776 -462
- package/lib/persistent-extension.js +1 -1
- package/lib/{persistent-runtime-D_hNgTX3.js → persistent-runtime-CNdmWCSF.js} +3754 -596
- package/lib/plugin-updater-helper-Bg28PYR7.js +2 -0
- package/lib/plugin-updater-helper-CEnlAUhI.js +731 -0
- package/lib/plugin-updater-helper.js +5 -2
- package/lib/sdk.js +58 -0
- package/lib/types/arkme-service.d.ts +43 -1
- package/lib/types/arkme-service.d.ts.map +1 -1
- package/lib/types/chat-realtime.d.ts +9 -0
- package/lib/types/chat-realtime.d.ts.map +1 -1
- package/lib/types/client/ArkmeAvatar.d.ts +5 -0
- package/lib/types/client/ArkmeAvatar.d.ts.map +1 -1
- package/lib/types/client/ArkmeCalendarSurface.d.ts +3 -1
- package/lib/types/client/ArkmeCalendarSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeCallSurface.d.ts +5 -0
- package/lib/types/client/ArkmeCallSurface.d.ts.map +1 -0
- package/lib/types/client/ArkmeChatMemberActions.d.ts +10 -2
- package/lib/types/client/ArkmeChatMemberActions.d.ts.map +1 -1
- package/lib/types/client/ArkmeComposerToolButton.d.ts +6 -0
- package/lib/types/client/ArkmeComposerToolButton.d.ts.map +1 -0
- package/lib/types/client/ArkmeComposerToolIcon.d.ts +3 -0
- package/lib/types/client/ArkmeComposerToolIcon.d.ts.map +1 -0
- package/lib/types/client/ArkmeDshAgentInputMarker.d.ts +10 -0
- package/lib/types/client/ArkmeDshAgentInputMarker.d.ts.map +1 -0
- package/lib/types/client/ArkmeEmojiPicker.d.ts +7 -0
- package/lib/types/client/ArkmeEmojiPicker.d.ts.map +1 -0
- package/lib/types/client/ArkmeFooterDropdown.d.ts.map +1 -1
- package/lib/types/client/ArkmeGroupChatControls.d.ts +1 -0
- package/lib/types/client/ArkmeGroupChatControls.d.ts.map +1 -1
- package/lib/types/client/ArkmeLogin.d.ts +2 -0
- package/lib/types/client/ArkmeLogin.d.ts.map +1 -1
- package/lib/types/client/ArkmeMentionTextarea.d.ts +6 -2
- package/lib/types/client/ArkmeMentionTextarea.d.ts.map +1 -1
- package/lib/types/client/ArkmeOutgoingCallHost.d.ts.map +1 -1
- package/lib/types/client/ArkmePersistentShell.d.ts +4 -3
- package/lib/types/client/ArkmePersistentShell.d.ts.map +1 -1
- package/lib/types/client/ArkmePluginUpdateDialog.d.ts +11 -0
- package/lib/types/client/ArkmePluginUpdateDialog.d.ts.map +1 -0
- package/lib/types/client/ArkmeProductNavigation.d.ts.map +1 -1
- package/lib/types/client/ArkmeRecordingSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeRichComposerInput.d.ts +27 -0
- package/lib/types/client/ArkmeRichComposerInput.d.ts.map +1 -0
- package/lib/types/client/ArkmeRichContent.d.ts +4 -0
- package/lib/types/client/ArkmeRichContent.d.ts.map +1 -1
- package/lib/types/client/ArkmeSearchSurface.d.ts +5 -0
- package/lib/types/client/ArkmeSearchSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSettingsSurface.d.ts +17 -10
- package/lib/types/client/ArkmeSettingsSurface.d.ts.map +1 -1
- package/lib/types/client/ArkmeSidebar.d.ts +17 -3
- package/lib/types/client/ArkmeSidebar.d.ts.map +1 -1
- package/lib/types/client/ArkmeSourceBreadcrumb.d.ts +1 -1
- package/lib/types/client/ArkmeSourceBreadcrumb.d.ts.map +1 -1
- package/lib/types/client/ArkmeStartupAuthGate.d.ts +6 -2
- package/lib/types/client/ArkmeStartupAuthGate.d.ts.map +1 -1
- package/lib/types/client/ArkmeTopicDirectoryPopover.d.ts.map +1 -1
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts +47 -0
- package/lib/types/client/ArkmeUpdateSurfaces.d.ts.map +1 -0
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts +11 -2
- package/lib/types/client/ArkmeVirtualWorkspace.d.ts.map +1 -1
- package/lib/types/client/ArkmeWorldEmptyNotice.d.ts +7 -0
- package/lib/types/client/ArkmeWorldEmptyNotice.d.ts.map +1 -0
- package/lib/types/client/ArkmeWorldSurface.d.ts +9 -4
- package/lib/types/client/ArkmeWorldSurface.d.ts.map +1 -1
- package/lib/types/client/DeepSeekHarnessSurface.d.ts +9 -2
- package/lib/types/client/DeepSeekHarnessSurface.d.ts.map +1 -1
- package/lib/types/client/api.d.ts +1 -1
- package/lib/types/client/api.d.ts.map +1 -1
- package/lib/types/client/arkme-auth-flow.d.ts +4 -2
- package/lib/types/client/arkme-auth-flow.d.ts.map +1 -1
- package/lib/types/client/arkme-emoji-assets.d.ts +2 -0
- package/lib/types/client/arkme-emoji-assets.d.ts.map +1 -0
- package/lib/types/client/arkme-emoji.d.ts +23 -0
- package/lib/types/client/arkme-emoji.d.ts.map +1 -1
- package/lib/types/client/arkme-login-errors.d.ts +6 -0
- package/lib/types/client/arkme-login-errors.d.ts.map +1 -0
- package/lib/types/client/arkme-login-locales.d.ts +85 -0
- package/lib/types/client/arkme-login-locales.d.ts.map +1 -0
- package/lib/types/client/arkme-theme.d.ts +1 -0
- package/lib/types/client/arkme-theme.d.ts.map +1 -1
- package/lib/types/client/arko-conversation-preview-sync.d.ts +32 -0
- package/lib/types/client/arko-conversation-preview-sync.d.ts.map +1 -0
- package/lib/types/client/auth-store.d.ts +1 -0
- package/lib/types/client/auth-store.d.ts.map +1 -1
- package/lib/types/client/chat-directory-store.d.ts +15 -1
- package/lib/types/client/chat-directory-store.d.ts.map +1 -1
- package/lib/types/client/composer-draft-store.d.ts +14 -0
- package/lib/types/client/composer-draft-store.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/outgoing-call-bridge.d.ts +3 -0
- package/lib/types/client/outgoing-call-bridge.d.ts.map +1 -1
- package/lib/types/client/outgoing-call-runtime.d.ts +13 -1
- package/lib/types/client/outgoing-call-runtime.d.ts.map +1 -1
- package/lib/types/client/outgoing-call-ui-controller.d.ts +9 -0
- package/lib/types/client/outgoing-call-ui-controller.d.ts.map +1 -1
- package/lib/types/client/plugin-update-store.d.ts.map +1 -1
- package/lib/types/client/realtime-client-events.d.ts +3 -0
- package/lib/types/client/realtime-client-events.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/AlphabeticalContactList.d.ts +20 -0
- package/lib/types/client/redesign/contacts/AlphabeticalContactList.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts +13 -0
- package/lib/types/client/redesign/contacts/CollapsibleDirectorySection.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts +35 -0
- package/lib/types/client/redesign/contacts/ContactDirectorySurface.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts +96 -0
- package/lib/types/client/redesign/contacts/ContactProfileDetail.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/ContactWorldList.d.ts +44 -0
- package/lib/types/client/redesign/contacts/ContactWorldList.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts +13 -0
- package/lib/types/client/redesign/contacts/DirectoryDetailPane.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/SpeakerChoicePanel.d.ts +21 -0
- package/lib/types/client/redesign/contacts/SpeakerChoicePanel.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerAudioList.d.ts +15 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerAudioList.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerDetail.d.ts +27 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerDetail.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerVisuals.d.ts +9 -0
- package/lib/types/client/redesign/contacts/UnmarkedSpeakerVisuals.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/contact-directory-state.d.ts +66 -0
- package/lib/types/client/redesign/contacts/contact-directory-state.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts +45 -0
- package/lib/types/client/redesign/contacts/contacts-tab-store.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/directory-workspace-controller.d.ts +58 -0
- package/lib/types/client/redesign/contacts/directory-workspace-controller.d.ts.map +1 -0
- package/lib/types/client/redesign/contacts/useSingleAudioPlayback.d.ts +16 -0
- package/lib/types/client/redesign/contacts/useSingleAudioPlayback.d.ts.map +1 -0
- package/lib/types/client/source-list.d.ts +2 -0
- package/lib/types/client/source-list.d.ts.map +1 -1
- package/lib/types/client/ui-controller.d.ts +4 -3
- package/lib/types/client/ui-controller.d.ts.map +1 -1
- package/lib/types/client/update-ui-controller.d.ts +16 -0
- package/lib/types/client/update-ui-controller.d.ts.map +1 -0
- package/lib/types/client/world-voiceprint-expectation-copy.d.ts +1 -6
- package/lib/types/client/world-voiceprint-expectation-copy.d.ts.map +1 -1
- package/lib/types/contact-directory-presentation.d.ts +19 -0
- package/lib/types/contact-directory-presentation.d.ts.map +1 -0
- package/lib/types/dsh-agent-input-source.d.ts +11 -0
- package/lib/types/dsh-agent-input-source.d.ts.map +1 -0
- package/lib/types/dsh-agent-input-sync.d.ts +14 -0
- package/lib/types/dsh-agent-input-sync.d.ts.map +1 -0
- package/lib/types/extensions/bundle-materializer.d.ts.map +1 -1
- package/lib/types/extensions/client-owner.d.ts +9 -0
- package/lib/types/extensions/client-owner.d.ts.map +1 -0
- package/lib/types/extensions/manager.d.ts +17 -1
- package/lib/types/extensions/manager.d.ts.map +1 -1
- package/lib/types/extensions/persistent-bundle.d.ts +9 -0
- package/lib/types/extensions/persistent-bundle.d.ts.map +1 -1
- package/lib/types/extensions/persistent-client-bundle.d.ts +3 -0
- package/lib/types/extensions/persistent-client-bundle.d.ts.map +1 -1
- package/lib/types/extensions/profile-installer.d.ts +1 -0
- package/lib/types/extensions/profile-installer.d.ts.map +1 -1
- package/lib/types/host-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +4 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/keychain-store.d.ts +2 -4
- package/lib/types/keychain-store.d.ts.map +1 -1
- package/lib/types/outgoing-call-assets.d.ts.map +1 -1
- package/lib/types/outgoing-call-broker.d.ts.map +1 -1
- package/lib/types/plugin-update-install-receipt.d.ts +9 -0
- package/lib/types/plugin-update-install-receipt.d.ts.map +1 -0
- package/lib/types/plugin-update-install-state.d.ts +1 -0
- package/lib/types/plugin-update-install-state.d.ts.map +1 -1
- package/lib/types/plugin-update-policy.d.ts +2 -0
- package/lib/types/plugin-update-policy.d.ts.map +1 -0
- package/lib/types/plugin-update.d.ts.map +1 -1
- package/lib/types/plugin-updater-helper-cli.d.ts +2 -0
- package/lib/types/plugin-updater-helper-cli.d.ts.map +1 -0
- package/lib/types/plugin-updater-helper.d.ts +8 -0
- package/lib/types/plugin-updater-helper.d.ts.map +1 -1
- package/lib/types/request-coordinator.d.ts +1 -1
- package/lib/types/request-coordinator.d.ts.map +1 -1
- package/lib/types/sdk/index.d.ts +18 -2
- package/lib/types/sdk/index.d.ts.map +1 -1
- package/lib/types/services/arko-service.d.ts.map +1 -1
- package/lib/types/services/auth-service.d.ts.map +1 -1
- package/lib/types/services/bot-service.d.ts +18 -2
- package/lib/types/services/bot-service.d.ts.map +1 -1
- package/lib/types/services/call-history-service.d.ts +28 -0
- package/lib/types/services/call-history-service.d.ts.map +1 -0
- package/lib/types/services/chat-realtime-service.d.ts +1 -0
- package/lib/types/services/chat-realtime-service.d.ts.map +1 -1
- package/lib/types/services/chat-service.d.ts +14 -1
- package/lib/types/services/chat-service.d.ts.map +1 -1
- package/lib/types/services/contact-directory-service.d.ts +46 -0
- package/lib/types/services/contact-directory-service.d.ts.map +1 -0
- package/lib/types/services/group-service.d.ts.map +1 -1
- package/lib/types/services/media-service.d.ts +13 -0
- package/lib/types/services/media-service.d.ts.map +1 -1
- package/lib/types/services/outgoing-call-service.d.ts.map +1 -1
- package/lib/types/services/profile-service.d.ts +2 -0
- package/lib/types/services/profile-service.d.ts.map +1 -1
- package/lib/types/services/record-service.d.ts +2 -0
- package/lib/types/services/record-service.d.ts.map +1 -1
- package/lib/types/services/search-service.d.ts.map +1 -1
- package/lib/types/services/service.d.ts +6 -0
- package/lib/types/services/service.d.ts.map +1 -1
- package/lib/types/services/source-service.d.ts +16 -0
- package/lib/types/services/source-service.d.ts.map +1 -1
- package/lib/types/services/unmarked-speaker-service.d.ts +56 -0
- package/lib/types/services/unmarked-speaker-service.d.ts.map +1 -0
- package/lib/types/services/world-service.d.ts +18 -2
- package/lib/types/services/world-service.d.ts.map +1 -1
- package/lib/types/tools/business/calls/detail.d.ts +2 -0
- package/lib/types/tools/business/calls/detail.d.ts.map +1 -0
- package/lib/types/tools/business/calls/history.d.ts +2 -0
- package/lib/types/tools/business/calls/history.d.ts.map +1 -0
- package/lib/types/tools/business/calls/index.d.ts +2 -0
- package/lib/types/tools/business/calls/index.d.ts.map +1 -0
- package/lib/types/tools/business/calls/retry-summary.d.ts +2 -0
- package/lib/types/tools/business/calls/retry-summary.d.ts.map +1 -0
- package/lib/types/tools/business/contacts/index.d.ts +2 -1
- package/lib/types/tools/business/contacts/index.d.ts.map +1 -1
- package/lib/types/tools/business/groups/index.d.ts +1 -1
- package/lib/types/tools/business/groups/index.d.ts.map +1 -1
- package/lib/types/tools/business/groups/rename.d.ts +2 -0
- package/lib/types/tools/business/groups/rename.d.ts.map +1 -0
- package/lib/types/tools/business/index.d.ts.map +1 -1
- package/lib/types/tools/business/world/index.d.ts +1 -0
- package/lib/types/tools/business/world/index.d.ts.map +1 -1
- package/lib/types/tools/business/world/open-private-chat.d.ts +2 -0
- package/lib/types/tools/business/world/open-private-chat.d.ts.map +1 -0
- package/lib/types/tools/ports/call-history.d.ts +7 -0
- package/lib/types/tools/ports/call-history.d.ts.map +1 -0
- package/lib/types/tools/ports/contacts.d.ts +4 -1
- package/lib/types/tools/ports/contacts.d.ts.map +1 -1
- package/lib/types/tools/ports/groups.d.ts +2 -1
- package/lib/types/tools/ports/groups.d.ts.map +1 -1
- package/lib/types/tools/ports/index.d.ts +3 -2
- package/lib/types/tools/ports/index.d.ts.map +1 -1
- package/lib/types/tools/ports/world.d.ts +2 -1
- package/lib/types/tools/ports/world.d.ts.map +1 -1
- package/lib/types/tools/prompts/business.d.ts.map +1 -1
- package/lib/types/tools/registry/registrar.d.ts.map +1 -1
- package/lib/types/tools/system/plugin-contract.d.ts.map +1 -1
- package/lib/types/types.d.ts +239 -4
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/windows-credential-helper.d.ts +29 -0
- package/lib/types/windows-credential-helper.d.ts.map +1 -0
- package/lib/types/world-voiceprint-copy.d.ts +18 -0
- package/lib/types/world-voiceprint-copy.d.ts.map +1 -0
- package/package.json +14 -6
- package/lib/plugin-updater-helper-BEYBTOld.js +0 -346
- package/lib/types/client/ArkmeAccountMenu.d.ts +0 -4
- package/lib/types/client/ArkmeAccountMenu.d.ts.map +0 -1
- package/lib/types/client/ArkmeSettingsRow.d.ts +0 -5
- package/lib/types/client/ArkmeSettingsRow.d.ts.map +0 -1
- package/lib/types/client/redesign/ArkmeRootFrame.d.ts +0 -19
- package/lib/types/client/redesign/ArkmeRootFrame.d.ts.map +0 -1
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { a as securePrivateFile, n as prepareProfilePackageManager, r as securePrivateDirectory } from "./profile-package-manager-DwuQu8ZL.js";
|
|
2
|
+
import { closeSync, existsSync, openSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname, isAbsolute, join, posix } from "node:path";
|
|
5
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { chmod as chmod$1, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import semver from "semver";
|
|
8
|
+
import { gunzipSync } from "node:zlib";
|
|
9
|
+
//#region src/plugin-update-artifact.ts
|
|
10
|
+
const ARKME_PLUGIN_PACKAGE_NAME = "@senguoyun/dsh-arkme";
|
|
11
|
+
const PLUGIN_UPDATE_API_PATH = "/api/public/v1/arkme/plugin-update/latest";
|
|
12
|
+
const MAX_PLUGIN_ARTIFACT_BYTES = 100 * 1024 * 1024;
|
|
13
|
+
const MAX_TAR_FILES = 4096;
|
|
14
|
+
const TAR_BLOCK_BYTES = 512;
|
|
15
|
+
var PluginUpdateArtifactError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
retryable;
|
|
18
|
+
constructor(code, message, retryable = false) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.retryable = retryable;
|
|
22
|
+
this.name = "PluginUpdateArtifactError";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function stringValue(value, maxLength) {
|
|
26
|
+
if (typeof value !== "string") return void 0;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (trimmed === "" || trimmed.length > maxLength) return void 0;
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
function assertNoControls(value, label, allowedControls = "") {
|
|
32
|
+
for (const character of value) if (character < " " && !allowedControls.includes(character) || character === "") throw new PluginUpdateArtifactError("plugin-update-manifest-control", `${label} 包含非法控制字符`);
|
|
33
|
+
}
|
|
34
|
+
function validatePluginUpdateServiceOrigin(raw) {
|
|
35
|
+
const url = new URL(raw);
|
|
36
|
+
if (url.hostname.toLowerCase() === "registry.npmjs.org") throw new Error("dsh-arkme: update service must not use the npm registry");
|
|
37
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error("dsh-arkme: update service base URL must be an HTTPS origin without credentials or path");
|
|
38
|
+
return url.origin;
|
|
39
|
+
}
|
|
40
|
+
function validatePluginUpdateArtifactOrigin(raw) {
|
|
41
|
+
const url = new URL(raw);
|
|
42
|
+
if (url.hostname.toLowerCase() === "registry.npmjs.org") throw new Error("dsh-arkme: update artifacts must not use the npm registry");
|
|
43
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error("dsh-arkme: update artifact base URL must be an HTTPS origin without credentials or path");
|
|
44
|
+
return url.origin;
|
|
45
|
+
}
|
|
46
|
+
function validateArtifactUrl(raw, expectedOrigin) {
|
|
47
|
+
const url = new URL(raw);
|
|
48
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hostname === "" || url.hash !== "" || url.hostname.toLowerCase() === "registry.npmjs.org") throw new PluginUpdateArtifactError("plugin-update-artifact-url-invalid", "插件更新制品 URL 必须是非 npm 的 HTTPS 下载地址");
|
|
49
|
+
if (url.pathname === "" || url.pathname === "/" || url.pathname.includes("\\") || url.pathname.split("/").includes("..") || url.pathname.endsWith("/")) throw new PluginUpdateArtifactError("plugin-update-artifact-url-invalid", "插件更新制品 URL 路径无效");
|
|
50
|
+
if (expectedOrigin !== void 0 && url.origin !== validatePluginUpdateArtifactOrigin(expectedOrigin)) throw new PluginUpdateArtifactError("plugin-update-artifact-origin-invalid", "插件更新制品 URL 不属于受信下载源");
|
|
51
|
+
return url.toString();
|
|
52
|
+
}
|
|
53
|
+
function parsePluginUpdateManifest(value, options) {
|
|
54
|
+
validatePluginUpdateServiceOrigin(options.updateServiceOrigin);
|
|
55
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new PluginUpdateArtifactError("plugin-update-response-invalid", "更新服务返回格式无效", true);
|
|
56
|
+
const source = value;
|
|
57
|
+
const version = stringValue(source.version, 128);
|
|
58
|
+
const artifactUrl = stringValue(source.downloadUrl, 4096);
|
|
59
|
+
if (version === void 0 || semver.valid(version) === null || artifactUrl === void 0) throw new PluginUpdateArtifactError("plugin-update-response-invalid", "更新服务返回格式无效", true);
|
|
60
|
+
assertNoControls(version, "version");
|
|
61
|
+
const releaseNotes = stringValue(source.releaseNotes, 300);
|
|
62
|
+
if (releaseNotes !== void 0) assertNoControls(releaseNotes, "releaseNotes", " \n\r");
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
packageName: ARKME_PLUGIN_PACKAGE_NAME,
|
|
66
|
+
version,
|
|
67
|
+
artifactUrl: validateArtifactUrl(artifactUrl, options.artifactOrigin),
|
|
68
|
+
artifactSize: MAX_PLUGIN_ARTIFACT_BYTES,
|
|
69
|
+
notice: {
|
|
70
|
+
level: "normal",
|
|
71
|
+
title: "插件更新",
|
|
72
|
+
summary: releaseNotes ?? "发现新版本"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function pluginUpdateEndpointUrl(updateServiceOrigin, input) {
|
|
77
|
+
const url = new URL(PLUGIN_UPDATE_API_PATH, validatePluginUpdateServiceOrigin(updateServiceOrigin));
|
|
78
|
+
url.searchParams.set("app_version", input.appVersion ?? "");
|
|
79
|
+
url.searchParams.set("dsh_version", input.dshVersion ?? "");
|
|
80
|
+
url.searchParams.set("current_version", input.currentVersion);
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
function parseOctal(buffer, offset, length) {
|
|
84
|
+
const value = buffer.subarray(offset, offset + length).toString("ascii").replace(/\0.*$/, "").trim();
|
|
85
|
+
if (!/^[0-7]*$/.test(value)) throw new PluginUpdateArtifactError("plugin-update-tgz-invalid", "插件 tgz tar 数值字段无效");
|
|
86
|
+
return value === "" ? 0 : Number.parseInt(value, 8);
|
|
87
|
+
}
|
|
88
|
+
function tarPath(header) {
|
|
89
|
+
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
|
90
|
+
const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
|
|
91
|
+
return prefix === "" ? name : `${prefix}/${name}`;
|
|
92
|
+
}
|
|
93
|
+
function assertTarHeaderChecksum(header) {
|
|
94
|
+
const expected = parseOctal(header, 148, 8);
|
|
95
|
+
let actual = 0;
|
|
96
|
+
for (let index = 0; index < header.byteLength; index += 1) actual += index >= 148 && index < 156 ? 32 : header[index] ?? 0;
|
|
97
|
+
if (expected !== actual) throw new PluginUpdateArtifactError("plugin-update-tgz-checksum-invalid", "插件 tgz tar 头校验失败");
|
|
98
|
+
}
|
|
99
|
+
function validateTarPath(path) {
|
|
100
|
+
const clean = posix.normalize(path);
|
|
101
|
+
if (!path.startsWith("package/") || clean !== path || path.includes("\\") || path.includes("\0") || path.startsWith("/") || path.split("/").includes("..") || /^[A-Za-z]:/.test(path)) throw new PluginUpdateArtifactError("plugin-update-tgz-path-invalid", `插件 tgz 包含非法路径:${path}`);
|
|
102
|
+
for (const character of path) if (character < " " || character === "") throw new PluginUpdateArtifactError("plugin-update-tgz-path-invalid", "插件 tgz 路径包含控制字符");
|
|
103
|
+
}
|
|
104
|
+
function readTar(tar) {
|
|
105
|
+
const files = /* @__PURE__ */ new Map();
|
|
106
|
+
let offset = 0;
|
|
107
|
+
let total = 0;
|
|
108
|
+
while (offset + TAR_BLOCK_BYTES <= tar.byteLength) {
|
|
109
|
+
const header = tar.subarray(offset, offset + TAR_BLOCK_BYTES);
|
|
110
|
+
if (header.every((byte) => byte === 0)) {
|
|
111
|
+
if (!tar.subarray(offset).every((byte) => byte === 0)) throw new PluginUpdateArtifactError("plugin-update-tgz-trailing-data", "插件 tgz tar 结束块后包含额外数据");
|
|
112
|
+
return files;
|
|
113
|
+
}
|
|
114
|
+
assertTarHeaderChecksum(header);
|
|
115
|
+
const path = tarPath(header);
|
|
116
|
+
validateTarPath(path);
|
|
117
|
+
if (files.has(path)) throw new PluginUpdateArtifactError("plugin-update-tgz-duplicate-path", `插件 tgz 包含重复路径:${path}`);
|
|
118
|
+
const type = header[156];
|
|
119
|
+
if (type !== "0".charCodeAt(0) && type !== 0) throw new PluginUpdateArtifactError("plugin-update-tgz-entry-type", "插件 tgz 只允许普通文件");
|
|
120
|
+
const size = parseOctal(header, 124, 12);
|
|
121
|
+
total += size;
|
|
122
|
+
if (total > MAX_PLUGIN_ARTIFACT_BYTES) throw new PluginUpdateArtifactError("plugin-update-tgz-too-large", "插件 tgz 解包总量超过限制");
|
|
123
|
+
const start = offset + TAR_BLOCK_BYTES;
|
|
124
|
+
const end = start + size;
|
|
125
|
+
if (end > tar.byteLength) throw new PluginUpdateArtifactError("plugin-update-tgz-truncated", "插件 tgz 内容不完整");
|
|
126
|
+
files.set(path, Buffer.from(tar.subarray(start, end)));
|
|
127
|
+
if (files.size > MAX_TAR_FILES) throw new PluginUpdateArtifactError("plugin-update-tgz-file-count", "插件 tgz 文件数量超限");
|
|
128
|
+
offset = start + Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
|
|
129
|
+
}
|
|
130
|
+
throw new PluginUpdateArtifactError("plugin-update-tgz-end-missing", "插件 tgz 缺少 tar 结束块");
|
|
131
|
+
}
|
|
132
|
+
function readPackageJSON(bytes) {
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
135
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("not object");
|
|
136
|
+
return value;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new PluginUpdateArtifactError("plugin-update-package-json-invalid", `插件 package.json 无效:${String(error)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function normalizePackagePath(path) {
|
|
142
|
+
return `package/${posix.normalize(path.replace(/^\.\//, ""))}`;
|
|
143
|
+
}
|
|
144
|
+
function inspectPluginPackageTgz(bytes) {
|
|
145
|
+
if (bytes.byteLength <= 0 || bytes.byteLength > MAX_PLUGIN_ARTIFACT_BYTES) throw new PluginUpdateArtifactError("plugin-update-artifact-size-invalid", "插件制品大小无效");
|
|
146
|
+
let tar;
|
|
147
|
+
try {
|
|
148
|
+
tar = gunzipSync(bytes, { maxOutputLength: 109052928 });
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new PluginUpdateArtifactError("plugin-update-gzip-invalid", `插件 tgz 无法安全解压:${String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
const files = readTar(tar);
|
|
153
|
+
const packageJson = files.get("package/package.json");
|
|
154
|
+
if (packageJson === void 0 || !files.has("package/lib/index.js") || !files.has("package/lib/client.js")) throw new PluginUpdateArtifactError("plugin-update-required-file-missing", "插件 tgz 缺少必要入口文件");
|
|
155
|
+
const manifest = readPackageJSON(packageJson);
|
|
156
|
+
if (manifest.name !== "@senguoyun/dsh-arkme" || typeof manifest.version !== "string" || semver.valid(manifest.version) === null) throw new PluginUpdateArtifactError("plugin-update-package-identity-invalid", "插件 tgz package identity 无效");
|
|
157
|
+
const dsh = manifest.dsh;
|
|
158
|
+
const patch = dsh !== null && typeof dsh === "object" && !Array.isArray(dsh) ? dsh.bundle?.patch : void 0;
|
|
159
|
+
if (typeof patch !== "string" || patch.trim() === "" || !files.has(normalizePackagePath(patch))) throw new PluginUpdateArtifactError("plugin-update-bundle-patch-missing", "插件 tgz 缺少 dsh.bundle.patch");
|
|
160
|
+
return {
|
|
161
|
+
packageName: ARKME_PLUGIN_PACKAGE_NAME,
|
|
162
|
+
version: manifest.version,
|
|
163
|
+
files
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function cachedPluginArtifactPath(cacheDirectory, version) {
|
|
167
|
+
return join(cacheDirectory, version, `dsh-arkme-${version}.tgz`);
|
|
168
|
+
}
|
|
169
|
+
function existingCachedPluginArtifactPath(cacheDirectory, version) {
|
|
170
|
+
const path = cachedPluginArtifactPath(cacheDirectory, version);
|
|
171
|
+
return existsSync(path) ? path : void 0;
|
|
172
|
+
}
|
|
173
|
+
async function readResponseBytes(response, maximumSize) {
|
|
174
|
+
const declared = Number(response.headers.get("content-length"));
|
|
175
|
+
if (Number.isFinite(declared) && declared > maximumSize) throw new PluginUpdateArtifactError("plugin-update-artifact-too-large", "插件制品响应超过最大限制", true);
|
|
176
|
+
if (response.body === null) {
|
|
177
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
178
|
+
if (bytes.byteLength > maximumSize) throw new PluginUpdateArtifactError("plugin-update-artifact-too-large", "插件制品响应超过最大限制", true);
|
|
179
|
+
return bytes;
|
|
180
|
+
}
|
|
181
|
+
const reader = response.body.getReader();
|
|
182
|
+
const chunks = [];
|
|
183
|
+
let total = 0;
|
|
184
|
+
while (true) {
|
|
185
|
+
const result = await reader.read();
|
|
186
|
+
if (result.done) break;
|
|
187
|
+
total += result.value.byteLength;
|
|
188
|
+
if (total > maximumSize || total > MAX_PLUGIN_ARTIFACT_BYTES) {
|
|
189
|
+
await reader.cancel();
|
|
190
|
+
throw new PluginUpdateArtifactError("plugin-update-artifact-too-large", "插件制品响应超过声明大小", true);
|
|
191
|
+
}
|
|
192
|
+
chunks.push(Buffer.from(result.value));
|
|
193
|
+
}
|
|
194
|
+
return Buffer.concat(chunks);
|
|
195
|
+
}
|
|
196
|
+
async function fetchPluginArtifactBytes(manifest, options) {
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs);
|
|
199
|
+
timeout.unref?.();
|
|
200
|
+
try {
|
|
201
|
+
const response = await options.fetchImpl(manifest.artifactUrl, {
|
|
202
|
+
method: "GET",
|
|
203
|
+
headers: { Accept: "application/octet-stream" },
|
|
204
|
+
redirect: "error",
|
|
205
|
+
signal: controller.signal
|
|
206
|
+
});
|
|
207
|
+
if (!response.ok) throw new PluginUpdateArtifactError("plugin-update-artifact-http-error", `插件制品下载返回 HTTP ${String(response.status)}`, response.status >= 500 || response.status === 429);
|
|
208
|
+
return await readResponseBytes(response, manifest.artifactSize);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof PluginUpdateArtifactError) throw error;
|
|
211
|
+
throw new PluginUpdateArtifactError(controller.signal.aborted ? "plugin-update-artifact-timeout" : "plugin-update-artifact-network-error", controller.signal.aborted ? "插件制品下载超时" : "无法下载插件制品", true);
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function downloadAndCachePluginArtifact(manifest, options) {
|
|
217
|
+
validateArtifactUrl(manifest.artifactUrl, options.artifactOrigin);
|
|
218
|
+
const cachedPath = cachedPluginArtifactPath(options.cacheDirectory, manifest.version);
|
|
219
|
+
if (existsSync(cachedPath)) {
|
|
220
|
+
try {
|
|
221
|
+
if (inspectPluginPackageTgz(readFileSync(cachedPath)).version === manifest.version) return cachedPath;
|
|
222
|
+
} catch {}
|
|
223
|
+
await unlink(cachedPath).catch(() => void 0);
|
|
224
|
+
}
|
|
225
|
+
const bytes = await fetchPluginArtifactBytes(manifest, {
|
|
226
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch.bind(globalThis),
|
|
227
|
+
requestTimeoutMs: options.requestTimeoutMs ?? 3e4
|
|
228
|
+
});
|
|
229
|
+
const inspected = inspectPluginPackageTgz(bytes);
|
|
230
|
+
if (inspected.packageName !== "@senguoyun/dsh-arkme" || inspected.version !== manifest.version) throw new PluginUpdateArtifactError("plugin-update-package-identity-invalid", "插件制品包名或版本不匹配");
|
|
231
|
+
const targetDirectory = join(options.cacheDirectory, manifest.version);
|
|
232
|
+
const temporaryDirectory = join(options.cacheDirectory, ".tmp");
|
|
233
|
+
await mkdir(targetDirectory, {
|
|
234
|
+
recursive: true,
|
|
235
|
+
mode: 448
|
|
236
|
+
});
|
|
237
|
+
await chmod$1(targetDirectory, 448);
|
|
238
|
+
await mkdir(temporaryDirectory, {
|
|
239
|
+
recursive: true,
|
|
240
|
+
mode: 448
|
|
241
|
+
});
|
|
242
|
+
await chmod$1(temporaryDirectory, 448);
|
|
243
|
+
const temporaryPath = join(temporaryDirectory, `${process.pid}.${randomUUID()}.tgz`);
|
|
244
|
+
await writeFile(temporaryPath, bytes, { mode: 384 });
|
|
245
|
+
await chmod$1(temporaryPath, 384);
|
|
246
|
+
try {
|
|
247
|
+
await rename(temporaryPath, cachedPath);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await unlink(temporaryPath).catch(() => void 0);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
await chmod$1(cachedPath, 384);
|
|
253
|
+
return cachedPath;
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/plugin-update-install-state.ts
|
|
257
|
+
const PHASES = /* @__PURE__ */ new Set([
|
|
258
|
+
"idle",
|
|
259
|
+
"preparing",
|
|
260
|
+
"downloading",
|
|
261
|
+
"verifying",
|
|
262
|
+
"installing",
|
|
263
|
+
"restarting",
|
|
264
|
+
"succeeded",
|
|
265
|
+
"failed",
|
|
266
|
+
"rolled-back"
|
|
267
|
+
]);
|
|
268
|
+
function boundedString(value, maxLength) {
|
|
269
|
+
if (typeof value !== "string") return "";
|
|
270
|
+
const normalized = value.trim();
|
|
271
|
+
return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength);
|
|
272
|
+
}
|
|
273
|
+
function parsePluginUpdateInstallSnapshot(value) {
|
|
274
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
275
|
+
const source = value;
|
|
276
|
+
const phase = source.phase;
|
|
277
|
+
const jobId = boundedString(source.jobId, 128);
|
|
278
|
+
const previousVersion = boundedString(source.previousVersion, 128);
|
|
279
|
+
const targetVersion = boundedString(source.targetVersion, 128);
|
|
280
|
+
const targetArtifactPath = boundedString(source.targetArtifactPath, 4096);
|
|
281
|
+
const targetArtifactSha512 = boundedString(source.targetArtifactSha512, 128);
|
|
282
|
+
const appVersion = boundedString(source.appVersion, 128);
|
|
283
|
+
const dshVersion = boundedString(source.dshVersion, 128);
|
|
284
|
+
const message = boundedString(source.message, 500);
|
|
285
|
+
const updatedAtMillis = source.updatedAtMillis;
|
|
286
|
+
if (source.schemaVersion !== 1 || !PHASES.has(phase) || jobId === "" || previousVersion === "" || targetVersion === "" || typeof updatedAtMillis !== "number" || !Number.isFinite(updatedAtMillis) || updatedAtMillis <= 0) return void 0;
|
|
287
|
+
return {
|
|
288
|
+
schemaVersion: 1,
|
|
289
|
+
jobId,
|
|
290
|
+
phase,
|
|
291
|
+
previousVersion,
|
|
292
|
+
targetVersion,
|
|
293
|
+
...targetArtifactPath === "" ? {} : { targetArtifactPath },
|
|
294
|
+
...!/^[a-f0-9]{128}$/.test(targetArtifactSha512) ? {} : { targetArtifactSha512 },
|
|
295
|
+
...appVersion === "" ? {} : { appVersion },
|
|
296
|
+
...dshVersion === "" ? {} : { dshVersion },
|
|
297
|
+
message,
|
|
298
|
+
updatedAtMillis
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
var PluginUpdateInstallStateStore = class {
|
|
302
|
+
path;
|
|
303
|
+
constructor(directory) {
|
|
304
|
+
this.path = join(directory, "plugin-update-install-state.json");
|
|
305
|
+
}
|
|
306
|
+
async read() {
|
|
307
|
+
try {
|
|
308
|
+
return parsePluginUpdateInstallSnapshot(JSON.parse(await readFile(this.path, "utf8")));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error.code === "ENOENT") return void 0;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async write(snapshot) {
|
|
315
|
+
const directory = dirname(this.path);
|
|
316
|
+
await mkdir(directory, {
|
|
317
|
+
recursive: true,
|
|
318
|
+
mode: 448
|
|
319
|
+
});
|
|
320
|
+
await securePrivateDirectory(directory);
|
|
321
|
+
const temporary = `${this.path}.${process.pid}.${randomUUID()}.tmp`;
|
|
322
|
+
await writeFile(temporary, `${JSON.stringify(snapshot, void 0, 2)}\n`, { mode: 384 });
|
|
323
|
+
await securePrivateFile(temporary);
|
|
324
|
+
await rename(temporary, this.path);
|
|
325
|
+
await securePrivateFile(this.path);
|
|
326
|
+
}
|
|
327
|
+
async clear() {
|
|
328
|
+
await unlink(this.path).catch((error) => {
|
|
329
|
+
if (error.code !== "ENOENT") throw error;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/plugin-update-install-receipt.ts
|
|
335
|
+
async function writePluginUpdateInstallReceipt(input) {
|
|
336
|
+
if (input.targetArtifactSha512 === void 0 || input.appVersion === void 0 || input.dshVersion === void 0) throw new Error("plugin update install receipt metadata is incomplete");
|
|
337
|
+
if (!/^[a-f0-9]{128}$/.test(input.targetArtifactSha512)) throw new Error("plugin update install receipt digest is invalid");
|
|
338
|
+
const directory = dirname(input.targetArtifactPath);
|
|
339
|
+
const receiptPath = join(directory, "plugin-update-install-receipt.json");
|
|
340
|
+
await securePrivateDirectory(directory);
|
|
341
|
+
const temporaryPath = `${receiptPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
342
|
+
await writeFile(temporaryPath, `${JSON.stringify({
|
|
343
|
+
schemaVersion: 1,
|
|
344
|
+
packageName: ARKME_PLUGIN_PACKAGE_NAME,
|
|
345
|
+
targetVersion: input.targetVersion,
|
|
346
|
+
targetArtifactPath: input.targetArtifactPath,
|
|
347
|
+
targetArtifactSha512: input.targetArtifactSha512,
|
|
348
|
+
appVersion: input.appVersion,
|
|
349
|
+
dshVersion: input.dshVersion,
|
|
350
|
+
installedAtMillis: Date.now()
|
|
351
|
+
}, void 0, 2)}\n`, { mode: 384 });
|
|
352
|
+
await chmod$1(temporaryPath, 384);
|
|
353
|
+
try {
|
|
354
|
+
await rename(temporaryPath, receiptPath);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
await unlink(temporaryPath).catch(() => void 0);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
await securePrivateFile(receiptPath);
|
|
360
|
+
}
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region src/plugin-updater-helper.ts
|
|
363
|
+
const PARENT_EXIT_TIMEOUT_MS = 2e4;
|
|
364
|
+
const HEALTH_TIMEOUT_MS = 45e3;
|
|
365
|
+
function nonEmptyString(value, maxLength = 4096) {
|
|
366
|
+
if (typeof value !== "string") return void 0;
|
|
367
|
+
const normalized = value.trim();
|
|
368
|
+
return normalized !== "" && normalized.length <= maxLength ? normalized : void 0;
|
|
369
|
+
}
|
|
370
|
+
function stringArray(value, options = {}) {
|
|
371
|
+
if (!Array.isArray(value)) return void 0;
|
|
372
|
+
const result = value.map((item) => nonEmptyString(item));
|
|
373
|
+
if (result.some((item) => item === void 0) || !options.allowEmpty && result.length === 0) return void 0;
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
function parsePluginUpdaterPlan(value) {
|
|
377
|
+
if (value === null || typeof value !== "object") throw new Error("updater plan must be an object");
|
|
378
|
+
const source = value;
|
|
379
|
+
const jobId = nonEmptyString(source.jobId, 128);
|
|
380
|
+
const execPath = nonEmptyString(source.execPath);
|
|
381
|
+
const execArgv = stringArray(source.execArgv, { allowEmpty: true });
|
|
382
|
+
const dshBinPath = nonEmptyString(source.dshBinPath);
|
|
383
|
+
const dshHome = nonEmptyString(source.dshHome);
|
|
384
|
+
const profileName = nonEmptyString(source.profileName, 64);
|
|
385
|
+
const previousVersion = nonEmptyString(source.previousVersion, 128);
|
|
386
|
+
const previousSpec = nonEmptyString(source.previousSpec, 4096);
|
|
387
|
+
const targetVersion = nonEmptyString(source.targetVersion, 128);
|
|
388
|
+
const targetArtifactPath = nonEmptyString(source.targetArtifactPath, 4096);
|
|
389
|
+
const targetArtifactSha512 = nonEmptyString(source.targetArtifactSha512, 128);
|
|
390
|
+
const appVersion = nonEmptyString(source.appVersion, 128);
|
|
391
|
+
const dshVersion = nonEmptyString(source.dshVersion, 128);
|
|
392
|
+
const previousArtifactPath = nonEmptyString(source.previousArtifactPath, 4096);
|
|
393
|
+
const stateDirectory = nonEmptyString(source.stateDirectory);
|
|
394
|
+
const healthUrl = nonEmptyString(source.healthUrl);
|
|
395
|
+
const logPath = nonEmptyString(source.logPath);
|
|
396
|
+
const restartArgv = stringArray(source.restartArgv);
|
|
397
|
+
if (source.schemaVersion !== 1 || jobId === void 0 || execPath === void 0 || execArgv === void 0 || dshBinPath === void 0 || dshHome === void 0 || profileName === void 0 || previousVersion === void 0 || previousSpec === void 0 || targetVersion === void 0 || targetArtifactPath === void 0 || stateDirectory === void 0 || healthUrl === void 0 || logPath === void 0 || restartArgv === void 0 || source.parentPid === void 0 || typeof source.parentPid !== "number" || !Number.isSafeInteger(source.parentPid) || source.parentPid <= 0) throw new Error("updater plan is incomplete");
|
|
398
|
+
if (!isAbsolute(targetArtifactPath) || !existsSync(targetArtifactPath) || previousArtifactPath !== void 0 && (!isAbsolute(previousArtifactPath) || !existsSync(previousArtifactPath))) throw new Error("updater plan artifact paths must be existing absolute files");
|
|
399
|
+
if (targetArtifactSha512 !== void 0 && !/^[a-f0-9]{128}$/.test(targetArtifactSha512)) throw new Error("updater plan artifact digest is invalid");
|
|
400
|
+
const url = new URL(healthUrl);
|
|
401
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname)) throw new Error("updater health URL must be loopback HTTP");
|
|
402
|
+
return {
|
|
403
|
+
schemaVersion: 1,
|
|
404
|
+
jobId,
|
|
405
|
+
parentPid: source.parentPid,
|
|
406
|
+
execPath,
|
|
407
|
+
execArgv,
|
|
408
|
+
dshBinPath,
|
|
409
|
+
restartArgv,
|
|
410
|
+
dshHome,
|
|
411
|
+
profileName,
|
|
412
|
+
previousVersion,
|
|
413
|
+
previousSpec,
|
|
414
|
+
targetVersion,
|
|
415
|
+
targetArtifactPath,
|
|
416
|
+
...targetArtifactSha512 === void 0 ? {} : { targetArtifactSha512 },
|
|
417
|
+
...appVersion === void 0 ? {} : { appVersion },
|
|
418
|
+
...dshVersion === void 0 ? {} : { dshVersion },
|
|
419
|
+
...previousArtifactPath === void 0 ? {} : { previousArtifactPath },
|
|
420
|
+
stateDirectory,
|
|
421
|
+
healthUrl: url.toString(),
|
|
422
|
+
logPath
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function processAlive(pid) {
|
|
426
|
+
try {
|
|
427
|
+
process.kill(pid, 0);
|
|
428
|
+
return true;
|
|
429
|
+
} catch (error) {
|
|
430
|
+
return error.code !== "ESRCH";
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
434
|
+
const deadline = Date.now() + timeoutMs;
|
|
435
|
+
while (processAlive(pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 200));
|
|
436
|
+
return !processAlive(pid);
|
|
437
|
+
}
|
|
438
|
+
function isLocalPackageSpec(spec) {
|
|
439
|
+
return /^(?:link:|file:)/.test(spec);
|
|
440
|
+
}
|
|
441
|
+
function buildTargetInstallArgs(plan) {
|
|
442
|
+
return [
|
|
443
|
+
...plan.execArgv,
|
|
444
|
+
plan.dshBinPath,
|
|
445
|
+
"plugin",
|
|
446
|
+
"--profile",
|
|
447
|
+
plan.profileName,
|
|
448
|
+
"add",
|
|
449
|
+
`file:${plan.targetArtifactPath}`
|
|
450
|
+
];
|
|
451
|
+
}
|
|
452
|
+
function buildTargetRemoveArgs(plan) {
|
|
453
|
+
return [
|
|
454
|
+
...plan.execArgv,
|
|
455
|
+
plan.dshBinPath,
|
|
456
|
+
"plugin",
|
|
457
|
+
"--profile",
|
|
458
|
+
plan.profileName,
|
|
459
|
+
"remove",
|
|
460
|
+
ARKME_PLUGIN_PACKAGE_NAME
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
function assertTargetArtifactIntegrity(plan) {
|
|
464
|
+
if (plan.targetArtifactSha512 === void 0) return;
|
|
465
|
+
if (createHash("sha512").update(readFileSync(plan.targetArtifactPath)).digest("hex") !== plan.targetArtifactSha512) throw new Error("updater target artifact digest mismatch");
|
|
466
|
+
}
|
|
467
|
+
function readDshPackageVersion(dshBinPath) {
|
|
468
|
+
try {
|
|
469
|
+
let resolvedBinPath = dshBinPath;
|
|
470
|
+
try {
|
|
471
|
+
resolvedBinPath = realpathSync(dshBinPath);
|
|
472
|
+
} catch {}
|
|
473
|
+
const manifest = JSON.parse(readFileSync(join(dirname(resolvedBinPath), "..", "package.json"), "utf8"));
|
|
474
|
+
return typeof manifest.version === "string" && semver.valid(manifest.version) !== null ? manifest.version : void 0;
|
|
475
|
+
} catch {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function completeReceiptMetadata(plan, environment = process.env) {
|
|
480
|
+
const targetArtifactSha512 = plan.targetArtifactSha512 ?? createHash("sha512").update(readFileSync(plan.targetArtifactPath)).digest("hex");
|
|
481
|
+
const appVersion = plan.appVersion ?? nonEmptyString(environment.ARKME_APP_VERSION, 128);
|
|
482
|
+
const dshVersion = plan.dshVersion ?? readDshPackageVersion(plan.dshBinPath);
|
|
483
|
+
if (appVersion === void 0 || dshVersion === void 0) throw new Error("updater plan cannot derive receipt compatibility metadata");
|
|
484
|
+
return {
|
|
485
|
+
...plan,
|
|
486
|
+
targetArtifactSha512,
|
|
487
|
+
appVersion,
|
|
488
|
+
dshVersion
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
function runTargetInstall(plan) {
|
|
492
|
+
return spawnSync(plan.execPath, buildTargetInstallArgs(plan), {
|
|
493
|
+
env: {
|
|
494
|
+
...process.env,
|
|
495
|
+
DSH_HOME: plan.dshHome
|
|
496
|
+
},
|
|
497
|
+
stdio: "inherit",
|
|
498
|
+
shell: false
|
|
499
|
+
}).status === 0;
|
|
500
|
+
}
|
|
501
|
+
function runTargetRemove(plan) {
|
|
502
|
+
return spawnSync(plan.execPath, buildTargetRemoveArgs(plan), {
|
|
503
|
+
env: {
|
|
504
|
+
...process.env,
|
|
505
|
+
DSH_HOME: plan.dshHome
|
|
506
|
+
},
|
|
507
|
+
stdio: "inherit",
|
|
508
|
+
shell: false
|
|
509
|
+
}).status === 0;
|
|
510
|
+
}
|
|
511
|
+
function runRollbackInstall(plan) {
|
|
512
|
+
const fallbackSpec = plan.previousArtifactPath === void 0 ? plan.previousSpec : `file:${plan.previousArtifactPath}`;
|
|
513
|
+
if (plan.previousArtifactPath === void 0 && !isLocalPackageSpec(fallbackSpec)) return false;
|
|
514
|
+
spawnSync(plan.execPath, buildTargetRemoveArgs(plan), {
|
|
515
|
+
env: {
|
|
516
|
+
...process.env,
|
|
517
|
+
DSH_HOME: plan.dshHome
|
|
518
|
+
},
|
|
519
|
+
stdio: "inherit",
|
|
520
|
+
shell: false
|
|
521
|
+
});
|
|
522
|
+
const args = [
|
|
523
|
+
...plan.execArgv,
|
|
524
|
+
plan.dshBinPath,
|
|
525
|
+
"plugin",
|
|
526
|
+
"--profile",
|
|
527
|
+
plan.profileName,
|
|
528
|
+
"add",
|
|
529
|
+
fallbackSpec
|
|
530
|
+
];
|
|
531
|
+
return spawnSync(plan.execPath, args, {
|
|
532
|
+
env: {
|
|
533
|
+
...process.env,
|
|
534
|
+
DSH_HOME: plan.dshHome
|
|
535
|
+
},
|
|
536
|
+
stdio: "inherit",
|
|
537
|
+
shell: false
|
|
538
|
+
}).status === 0;
|
|
539
|
+
}
|
|
540
|
+
function installedProfileVersion(plan) {
|
|
541
|
+
try {
|
|
542
|
+
return JSON.parse(readFileSync(join(plan.dshHome, "profiles", plan.profileName, "node_modules", "@senguoyun", "dsh-arkme", "package.json"), "utf8")).version ?? "";
|
|
543
|
+
} catch {
|
|
544
|
+
return "";
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function restartDsh(plan) {
|
|
548
|
+
const child = spawn(plan.execPath, plan.restartArgv, {
|
|
549
|
+
detached: true,
|
|
550
|
+
env: {
|
|
551
|
+
...process.env,
|
|
552
|
+
DSH_HOME: plan.dshHome
|
|
553
|
+
},
|
|
554
|
+
stdio: "inherit",
|
|
555
|
+
shell: false
|
|
556
|
+
});
|
|
557
|
+
child.unref();
|
|
558
|
+
return child;
|
|
559
|
+
}
|
|
560
|
+
async function waitForHealthy(plan, expectedVersion) {
|
|
561
|
+
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
|
|
562
|
+
while (Date.now() < deadline) {
|
|
563
|
+
const installedVersion = installedProfileVersion(plan);
|
|
564
|
+
try {
|
|
565
|
+
const response = await fetch(plan.healthUrl, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: {
|
|
568
|
+
"Content-Type": "application/json",
|
|
569
|
+
Origin: new URL(plan.healthUrl).origin
|
|
570
|
+
},
|
|
571
|
+
body: JSON.stringify({ operation: "plugin.update.status" }),
|
|
572
|
+
signal: AbortSignal.timeout(2e3)
|
|
573
|
+
});
|
|
574
|
+
if (response.ok) {
|
|
575
|
+
const body = await response.json();
|
|
576
|
+
if (body.ok === true && body.value?.installedVersion === expectedVersion) return true;
|
|
577
|
+
}
|
|
578
|
+
} catch {}
|
|
579
|
+
if (installedVersion === expectedVersion) try {
|
|
580
|
+
if ((await fetch(new URL("/", plan.healthUrl), { signal: AbortSignal.timeout(2e3) })).ok) return true;
|
|
581
|
+
} catch {}
|
|
582
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
function managedOperations(overrides) {
|
|
587
|
+
return {
|
|
588
|
+
runRollbackInstall,
|
|
589
|
+
waitForHealthy,
|
|
590
|
+
writeInstallReceipt: writePluginUpdateInstallReceipt,
|
|
591
|
+
...overrides
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function planWithReplacementHealthUrl(plan, replacementUrl) {
|
|
595
|
+
const replacement = new URL(replacementUrl);
|
|
596
|
+
if (replacement.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(replacement.hostname)) throw new Error("managed plugin update replacement URL must be loopback HTTP");
|
|
597
|
+
const health = new URL(plan.healthUrl);
|
|
598
|
+
health.protocol = replacement.protocol;
|
|
599
|
+
health.hostname = replacement.hostname;
|
|
600
|
+
health.port = replacement.port;
|
|
601
|
+
return {
|
|
602
|
+
...plan,
|
|
603
|
+
healthUrl: health.toString()
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
async function writePhase(store, plan, phase, message) {
|
|
607
|
+
const snapshot = {
|
|
608
|
+
schemaVersion: 1,
|
|
609
|
+
jobId: plan.jobId,
|
|
610
|
+
phase,
|
|
611
|
+
previousVersion: plan.previousVersion,
|
|
612
|
+
targetVersion: plan.targetVersion,
|
|
613
|
+
...plan.targetArtifactSha512 === void 0 ? {} : {
|
|
614
|
+
targetArtifactPath: plan.targetArtifactPath,
|
|
615
|
+
targetArtifactSha512: plan.targetArtifactSha512
|
|
616
|
+
},
|
|
617
|
+
...plan.appVersion === void 0 ? {} : { appVersion: plan.appVersion },
|
|
618
|
+
...plan.dshVersion === void 0 ? {} : { dshVersion: plan.dshVersion },
|
|
619
|
+
message,
|
|
620
|
+
updatedAtMillis: Date.now()
|
|
621
|
+
};
|
|
622
|
+
await store.write(snapshot);
|
|
623
|
+
return snapshot;
|
|
624
|
+
}
|
|
625
|
+
async function rollbackAndRestart(plan, store, rolledBackMessage) {
|
|
626
|
+
if (!runRollbackInstall(plan)) {
|
|
627
|
+
await writePhase(store, plan, "failed", "更新失败,旧版本恢复也失败;请使用更新命令手动修复。");
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
restartDsh(plan);
|
|
631
|
+
const healthy = await waitForHealthy(plan, plan.previousVersion);
|
|
632
|
+
await writePhase(store, plan, healthy ? "rolled-back" : "failed", healthy ? rolledBackMessage : "已恢复旧版本文件,但 DSH 未能自动启动。");
|
|
633
|
+
}
|
|
634
|
+
async function runPluginUpdater(planPath) {
|
|
635
|
+
const plan = completeReceiptMetadata(parsePluginUpdaterPlan(JSON.parse(await readFile(planPath, "utf8"))));
|
|
636
|
+
await unlink(planPath).catch(() => void 0);
|
|
637
|
+
const store = new PluginUpdateInstallStateStore(plan.stateDirectory);
|
|
638
|
+
if (!await waitForProcessExit(plan.parentPid, PARENT_EXIT_TIMEOUT_MS)) {
|
|
639
|
+
await writePhase(store, plan, "failed", "旧 DSH 进程未能退出,更新已取消。");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
prepareProfilePackageManager(plan.dshHome, plan.profileName);
|
|
644
|
+
} catch (error) {
|
|
645
|
+
restartDsh(plan);
|
|
646
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
647
|
+
await writePhase(store, plan, "failed", await waitForHealthy(plan, plan.previousVersion) ? `Profile pnpm 不可用,更新已取消并重新启动旧版本:${detail}` : `Profile pnpm 不可用,更新已取消,但旧版本未能自动启动:${detail}`);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
assertTargetArtifactIntegrity(plan);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
restartDsh(plan);
|
|
654
|
+
const healthy = await waitForHealthy(plan, plan.previousVersion);
|
|
655
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
656
|
+
await writePhase(store, plan, "failed", healthy ? `更新包完整性校验失败,已重新启动旧版本:${detail}` : `更新包完整性校验失败,且旧版本未能自动启动:${detail}`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
await writePhase(store, plan, "installing", `正在安装 ${plan.targetVersion}…`);
|
|
660
|
+
if (!runTargetRemove(plan)) {
|
|
661
|
+
await writePhase(store, plan, "failed", "旧版本清理失败,正在恢复旧版本…");
|
|
662
|
+
await rollbackAndRestart(plan, store, "旧版本清理失败,已自动恢复旧版本。");
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (!runTargetInstall(plan)) {
|
|
666
|
+
await writePhase(store, plan, "failed", "新版本安装失败,正在恢复旧版本…");
|
|
667
|
+
await rollbackAndRestart(plan, store, "新版本安装失败,已自动恢复旧版本。");
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const installedVersion = installedProfileVersion(plan);
|
|
671
|
+
if (semver.valid(installedVersion) === null || semver.lt(installedVersion, plan.targetVersion)) {
|
|
672
|
+
await writePhase(store, plan, "failed", "未安装预期插件版本,正在恢复旧版本…");
|
|
673
|
+
await rollbackAndRestart(plan, store, "插件版本不符合预期,已自动恢复旧版本。");
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
plan.targetVersion = installedVersion;
|
|
677
|
+
await writePhase(store, plan, "restarting", "安装完成,正在重启 DSH…");
|
|
678
|
+
const child = restartDsh(plan);
|
|
679
|
+
if (await waitForHealthy(plan, plan.targetVersion)) {
|
|
680
|
+
try {
|
|
681
|
+
assertTargetArtifactIntegrity(plan);
|
|
682
|
+
await writePluginUpdateInstallReceipt(plan);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
child.kill("SIGTERM");
|
|
685
|
+
if (child.pid !== void 0) await waitForProcessExit(child.pid, 1e4);
|
|
686
|
+
await writePhase(store, plan, "failed", `成功回执持久化失败,正在恢复旧版本:${error instanceof Error ? error.message : String(error)}`).catch(() => void 0);
|
|
687
|
+
await rollbackAndRestart(plan, store, "成功回执持久化失败,已自动恢复旧版本。");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await writePhase(store, plan, "succeeded", `已更新到 ${plan.targetVersion}。`);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
child.kill("SIGTERM");
|
|
694
|
+
if (child.pid !== void 0) await waitForProcessExit(child.pid, 1e4);
|
|
695
|
+
await writePhase(store, plan, "failed", "新版本健康检查失败,正在恢复旧版本…");
|
|
696
|
+
await rollbackAndRestart(plan, store, "新版本启动失败,已自动恢复旧版本。");
|
|
697
|
+
}
|
|
698
|
+
async function finalizeManagedPluginUpdate(planPath, replacementUrl, overrides = {}) {
|
|
699
|
+
const plan = completeReceiptMetadata(planWithReplacementHealthUrl(parsePluginUpdaterPlan(JSON.parse(await readFile(planPath, "utf8"))), replacementUrl));
|
|
700
|
+
const store = new PluginUpdateInstallStateStore(plan.stateDirectory);
|
|
701
|
+
const ops = managedOperations(overrides);
|
|
702
|
+
assertTargetArtifactIntegrity(plan);
|
|
703
|
+
if (!await ops.waitForHealthy(plan, plan.targetVersion)) throw new Error("managed plugin update did not become healthy");
|
|
704
|
+
assertTargetArtifactIntegrity(plan);
|
|
705
|
+
await ops.writeInstallReceipt(plan);
|
|
706
|
+
await writePhase(store, plan, "succeeded", `已更新到 ${plan.targetVersion}。`);
|
|
707
|
+
await unlink(planPath);
|
|
708
|
+
}
|
|
709
|
+
async function rollbackManagedPluginUpdate(planPath, overrides = {}) {
|
|
710
|
+
const plan = parsePluginUpdaterPlan(JSON.parse(await readFile(planPath, "utf8")));
|
|
711
|
+
const store = new PluginUpdateInstallStateStore(plan.stateDirectory);
|
|
712
|
+
if (!managedOperations(overrides).runRollbackInstall(plan)) {
|
|
713
|
+
await writePhase(store, plan, "failed", "更新失败,旧版本恢复也失败;请使用更新命令手动修复。");
|
|
714
|
+
throw new Error("managed plugin update rollback failed");
|
|
715
|
+
}
|
|
716
|
+
await writePhase(store, plan, "rolled-back", "已恢复旧版本文件,正在由 Arkme 重启 DSH…");
|
|
717
|
+
await unlink(planPath);
|
|
718
|
+
}
|
|
719
|
+
async function runPluginUpdaterCli(planPath) {
|
|
720
|
+
if (planPath === void 0) throw new Error("updater plan path is required");
|
|
721
|
+
const plan = parsePluginUpdaterPlan(JSON.parse(await readFile(planPath, "utf8")));
|
|
722
|
+
const logFd = openSync(plan.logPath, "a", 384);
|
|
723
|
+
try {
|
|
724
|
+
closeSync(logFd);
|
|
725
|
+
await runPluginUpdater(planPath);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
await writePhase(new PluginUpdateInstallStateStore(plan.stateDirectory), plan, "failed", error instanceof Error ? error.message : String(error));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
731
|
+
export { validatePluginUpdateServiceOrigin as _, parsePluginUpdaterPlan as a, runPluginUpdaterCli as c, PluginUpdateArtifactError as d, downloadAndCachePluginArtifact as f, validatePluginUpdateArtifactOrigin as g, pluginUpdateEndpointUrl as h, finalizeManagedPluginUpdate as i, PluginUpdateInstallStateStore as l, parsePluginUpdateManifest as m, buildTargetInstallArgs as n, rollbackManagedPluginUpdate as o, existingCachedPluginArtifactPath as p, buildTargetRemoveArgs as r, runPluginUpdater as s, assertTargetArtifactIntegrity as t, ARKME_PLUGIN_PACKAGE_NAME as u };
|