chainlesschain 0.162.11 → 0.162.13
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 +30 -25
- package/package.json +4 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-ebtJGjAG.js → AIOps-Bq_zxhCr.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-CypkRN-G.js → ActionButton-CaevDm9t.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-B2JMlIng.js → Analytics-B0gOmwPw.js} +1 -1
- package/src/assets/web-panel/assets/{AppLayout-B8QQ4pk7.js → AppLayout-DWhZiV0Q.js} +2 -2
- package/src/assets/web-panel/assets/{Audit-BoYaAyFa.js → Audit-ZuZJBCxo.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BfackGZ5.js → Backup-CX7jhH5l.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C06FUpDz.js → BaseInput-CVLx7HVq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BWxMkBYZ.js → Chat-C6yL5tRD.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-XJMvS3PV.js → Checkbox-BePhbqVq.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CzR462RK.js → Codegen-B5E4x1Lm.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BQHpLNCA.js → Col-CWhNU6A7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BWRRbJYd.js → Community-mSEAuJhp.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BunoKIy9.js → Compact-DSudHzX3.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CtJfZctm.js → Compliance-CxJLYjyn.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-ER5-_bod.js → Cowork-C-trppQj.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-C80jYBw1.js → Cron-_Ij4v5fY.js} +1 -1
- package/src/assets/web-panel/assets/{Crosschain-fEMlCNsL.js → Crosschain-eLHXzp5T.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BZpctKmU.js → DID-BP04AUUB.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-RQhZmLi4.js → Dashboard-CzEeQv62.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CrpwS84l.js → Dropdown-C_SGOB22.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BEQyZtdR.js → Federation-BgaP4BOv.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DCwvl6Vh.js → FormItemContext-BxGLLt9r.js} +1 -1
- package/src/assets/web-panel/assets/{Git-6FihOxMK.js → Git-Bt_uM_Gw.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DlBLHSlJ.js → Governance-N2-0RG_o.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-DdSokzV0.js → Inference-eS3g-CzP.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-bg8GBHMr.js → KnowledgeGraph-CUuvNVah.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DdFYdLQ-.js → Logs-wPbwEt2r.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DjnlAeYF.js → Marketplace-DH91kTwo.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Czs41YUh.js → McpTools-D-GyyBrI.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CX0b3c8D.js → Memory-BOtUy-tw.js} +1 -1
- package/src/assets/web-panel/assets/{MobileBridge-BoFGb9Mm.js → MobileBridge-DIP__XQd.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B8qQ9H-0.js → MobileProjects-1nqr1UsU.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CRF1NLae.js → Mtc-CCE0x7h2.js} +1 -1
- package/src/assets/web-panel/assets/{MtcAudit-CdCm70cJ.js → MtcAudit-BBkz0XUO.js} +1 -1
- package/src/assets/web-panel/assets/{Multisig-yoZlpq2Y.js → Multisig-CIKSJvTY.js} +2 -2
- package/src/assets/web-panel/assets/{NLProgramming-hFCgqDxJ.js → NLProgramming-BDkgeFcq.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DC8pnxs-.js → Notes-ONiUxfN1.js} +1 -1
- package/src/assets/web-panel/assets/{NotificationSettings-DDVg5Nc8.js → NotificationSettings-CGcyKEIe.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-0YCtAFMS.js → Organization-BZtMYBJt.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DhPLoAdz.js → Overflow-C4LfFZAI.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-x9ZzjLwk.js → OverrideContext-C_4H9tGA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BWpuJhkD.js → P2P-D71Cpk-m.js} +1 -1
- package/src/assets/web-panel/assets/{Permissions-B4IrizO9.js → Permissions-DRGYF29v.js} +1 -1
- package/src/assets/web-panel/assets/PersonalDataHub-CfRYoIua.js +1 -0
- package/src/assets/web-panel/assets/PersonalDataHub-Dvaa8niQ.css +1 -0
- package/src/assets/web-panel/assets/{Pipeline-M65jR6sq.js → Pipeline-BOyp0_Qo.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BeO8zLup.js → Privacy-a_AcphvF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Ck_ZjrVZ.js → ProjectInit-CFu1grYt.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectSettings-ijn-97s0.js → ProjectSettings-p54Eivhh.js} +1 -1
- package/src/assets/web-panel/assets/{Projects-BsNBemeh.js → Projects-DkB88XAu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CI7UxKVO.js → Providers-EK7f8DEd.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-dE2M1KOB.js → QuickAsk-CpO0w3iP.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-B30EgbKS.js → Recommend-BUJVQdv9.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CV6n7wMx.js → Reputation-kU2fOuZt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DSaoTjlN.js → Row-oGWRbk6g.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-_NgBmHaC.js → RssFeed-saC_46Yo.js} +1 -1
- package/src/assets/web-panel/assets/{Search-B341ooTV.js → Search-CjtRqFUu.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CqCQD8hf.js → Security-BX0NBVfQ.js} +1 -1
- package/src/assets/web-panel/assets/{Services-Cju_95rB.js → Services-Bgxsnei_.js} +1 -1
- package/src/assets/web-panel/assets/{Skeleton-kh_uW22l.js → Skeleton-CwBb3k2a.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BVRPgciI.js → Skills-CZCMYH6Z.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-y-vKFYkI.js → Sla-Djy1uHnZ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BXJm9zyo.js → SpeechSettings-CGUI_Uyh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BmHZR-Kv.js → SyncSettings-DK2CKHRD.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-C7zmYW9f.js → Tasks-BKiOzeIO.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DVEG7FdA.js → Templates-CnQpleXj.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-CpvjzPCo.js → Tenant-DwKz0cjm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D_Wpp2iE.js → Terminal-A7t_wsR8.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-QBrjdNqi.js → Tokens-BqYY9l44.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BhR_VEvQ.js → Trigger-BI4bXFmi.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-C0xhM2lC.js → Trust-yMynKTRG.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BnyP-W3-.js → UkeySign-Br4IScM6.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C5Y8MyEK.js → VideoEditing-CWcThGsP.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzCPCQNF.js → Wallet-CZcAtjxj.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-6X5bLtHU.js → WebAuthn-BnTZFMA0.js} +3 -3
- package/src/assets/web-panel/assets/{WorkflowEditor-ekS27G9f.js → WorkflowEditor-N7gGz3_n.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BikodUwh.js → chat-D175ZIO0.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-CjFH_Jop.js → collapseMotion-DfnRZex1.js} +1 -1
- package/src/assets/web-panel/assets/{colors-8yIg5K7E.js → colors-LKhZyttv.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-MLWo5-GY.js → compact-item-CsJSebxT.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-nir7ccDv.js → createContext-BJ_CPYFC.js} +1 -1
- package/src/assets/web-panel/assets/{echarts-Bq-n0MtJ.js → echarts-Dj_pBaVI.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-DxUIHW2P.js → hasIn-CzD3IqH8.js} +1 -1
- package/src/assets/web-panel/assets/{icons-CLQTHa5-.js → icons-BOPtEWK4.js} +4 -4
- package/src/assets/web-panel/assets/{index-vVrIg9Jk.js → index-6qPbrYF7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCGf6IJj.js → index-B7UYymse.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSjWvxVr.js → index-BDSZDDb2.js} +4 -4
- package/src/assets/web-panel/assets/{index-BURKtxBq.js → index-BEDFHKO3.js} +1 -1
- package/src/assets/web-panel/assets/{index-q3Lr2UzW.js → index-BMvdoiFr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgQtoOHc.js → index-BNVLVzN5.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bn5VWKW1.js → index-BSNibAqz.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SFekeAy.js → index-BV-__mlC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfeuuE7v.js → index-BXH9ujMW.js} +1 -1
- package/src/assets/web-panel/assets/{index-h05fIj9Q.js → index-BZluCuTH.js} +1 -1
- package/src/assets/web-panel/assets/{index-9Y0IyfeM.js → index-BhiZDGg7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkMtxzcM.js → index-BjOrt4vw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOIryna2.js → index-BmJdof_c.js} +2 -2
- package/src/assets/web-panel/assets/{index-TB5vrA0Z.js → index-BsirlkJ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgHPrMXP.js → index-BvF2tC6C.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSIaRmzU.js → index-C2HBKw07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkSN2Ki_.js → index-CKjBAdm0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLiexKJ2.js → index-CRGNuUIM.js} +1 -1
- package/src/assets/web-panel/assets/{index-CeX-HLIi.js → index-CTIkCKav.js} +1 -1
- package/src/assets/web-panel/assets/index-CY1mQA2I.js +1 -0
- package/src/assets/web-panel/assets/{index-78olN7S9.js → index-CyHdYUeZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcyG-9vV.js → index-D401L3yx.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_W1kVtY.js → index-D5IZCkZG.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bpa9senE.js → index-D7ZcBI5s.js} +1 -1
- package/src/assets/web-panel/assets/{index-dgZZAsxo.js → index-D8kltMTW.js} +1 -1
- package/src/assets/web-panel/assets/index-D9nXHfUB.js +1 -0
- package/src/assets/web-panel/assets/{index-DkIon-Gv.js → index-DJVkBmSc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-UB9bYd.js → index-DM3uBEWD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dx5xZmzt.js → index-DOO73rHE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DVLJ1iGu.js → index-DXp1jVsK.js} +1 -1
- package/src/assets/web-panel/assets/{index-S8mYImvf.js → index-Dd7dICwB.js} +1 -1
- package/src/assets/web-panel/assets/{index-SUYLhwZI.js → index-Dm-3kvtD.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_Xi08tu.js → index-DnPt5OdD.js} +1 -1
- package/src/assets/web-panel/assets/{index-hu-wjfWv.js → index-E7t1hAnk.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cp-YnzHN.js → index-JTX9A7w0.js} +1 -1
- package/src/assets/web-panel/assets/{index-8qrwsaKy.js → index-Kn-Of5ew.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dl-O2OkQ.js → index-R1cFADfk.js} +1 -1
- package/src/assets/web-panel/assets/{index-yfNusVbo.js → index-RIO4JKMP.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDmc4cig.js → index-uTEVWPYA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-xUjF_bq0.js → initDefaultProps-CBW0okek.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B019-Q6h.js → motion-DGAffQ0Z.js} +1 -1
- package/src/assets/web-panel/assets/{move-D2XYj_gA.js → move-DFJ0-5IW.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AIzzlguv.js → omit-AvrDghg1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CRkEQaLs.js → pickAttrs-D7csw9i1.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-s4kAStH6.js → placementArrow-hZ6Lg6kG.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BCsWrTkb.js → responsiveObserve-DLLx5VvS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-B_Hggtvv.js → slide-BaRIT3ev.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DnNf15VW.js → statusUtils-Cdjyuhrz.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CSQdy9SQ.js → styleChecker-CbrNybTt.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-B8gAhiRC.js +1 -0
- package/src/assets/web-panel/assets/{useFs-D78PlgeG.js → useFs-BZPy4ICP.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-O7QXt4P5.js → useMergedState-WwedrFR0.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-0J6m8UWN.js → useRefs-Cdq8EWeF.js} +1 -1
- package/src/assets/web-panel/assets/{useState-CSzR8F8O.js → useState-DGS1NOyn.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-M5lGV-wr.js → vendor-DhFY8mDK.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BTMmpsWu.js → vnode-6Y0NDMVv.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CwOTbvKc.js → zoom-DTbMGsSH.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/commands/__tests__/hub-aichat.test.js +277 -0
- package/src/commands/__tests__/hub-wechat.test.js +243 -0
- package/src/commands/hub.js +881 -0
- package/src/commands/sync-providers.js +436 -0
- package/src/gateways/ws/personal-data-hub-protocol.js +68 -0
- package/src/index.js +6 -0
- package/src/lib/__tests__/personal-data-hub-aichat-wizard.test.js +209 -0
- package/src/lib/__tests__/sync-credentials.test.js +265 -0
- package/src/lib/__tests__/sync-engine-cli.test.js +293 -0
- package/src/lib/personal-data-hub-aichat-wizard.js +242 -0
- package/src/lib/personal-data-hub-wiring.js +189 -0
- package/src/lib/sync-cli-db.js +194 -0
- package/src/lib/sync-credentials.js +225 -0
- package/src/lib/sync-engine-cli.js +406 -0
- package/src/lib/sync-oss-client.js +273 -0
- package/src/lib/sync-webdav-client.js +194 -0
- package/src/assets/web-panel/assets/PersonalDataHub-BK7I0Rsb.css +0 -1
- package/src/assets/web-panel/assets/PersonalDataHub-ZbziiUr6.js +0 -1
- package/src/assets/web-panel/assets/index-BeA3spHc.js +0 -1
- package/src/assets/web-panel/assets/index-DoLRjAoc.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-Bt-T27Pf.js +0 -1
|
@@ -30,6 +30,8 @@ import { mkdirSync } from "node:fs";
|
|
|
30
30
|
// won't let us name-import a CJS module unless it ships a separate ESM
|
|
31
31
|
// shim, which we don't).
|
|
32
32
|
import hub from "@chainlesschain/personal-data-hub";
|
|
33
|
+
import wechatAdapterModule from "@chainlesschain/personal-data-hub/adapters/wechat";
|
|
34
|
+
const { bootstrapWechatAdapter, probeWeChatEnv } = wechatAdapterModule;
|
|
33
35
|
const {
|
|
34
36
|
LocalVault,
|
|
35
37
|
AdapterRegistry,
|
|
@@ -51,6 +53,18 @@ const {
|
|
|
51
53
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
52
54
|
import { getElectronUserDataDir } from "./paths.js";
|
|
53
55
|
|
|
56
|
+
import {
|
|
57
|
+
getAIChatWizard,
|
|
58
|
+
createAccountsStore as createAIChatAccountsStore,
|
|
59
|
+
createVendorAdapterBridge as createAIChatVendorAdapterBridge,
|
|
60
|
+
} from "./personal-data-hub-aichat-wizard.js";
|
|
61
|
+
|
|
62
|
+
async function loadAIChatHealthChecker() {
|
|
63
|
+
const mod =
|
|
64
|
+
await import("@chainlesschain/personal-data-hub/adapters/ai-chat-history/health-checker");
|
|
65
|
+
return (mod.default || mod).createAIChatHealthChecker;
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
// ─── Lazy ESM imports of cli KG / BM25 ───────────────────────────────────
|
|
55
69
|
|
|
56
70
|
let _kgMod = null;
|
|
@@ -211,6 +225,24 @@ async function initHub() {
|
|
|
211
225
|
}
|
|
212
226
|
}
|
|
213
227
|
|
|
228
|
+
// Phase 10.3.5 — AIChat HealthChecker (mirror of desktop wiring). cc ui
|
|
229
|
+
// typically only runs while the user has the web-shell open, but the
|
|
230
|
+
// periodic loop still validates persisted cookies so list-aichat-accounts
|
|
231
|
+
// can surface lastHealth reliably.
|
|
232
|
+
const aichatAccountsStore = createAIChatAccountsStore({ hubDir });
|
|
233
|
+
const aichatVendorAdapter = createAIChatVendorAdapterBridge();
|
|
234
|
+
const createAIChatHealthChecker = await loadAIChatHealthChecker();
|
|
235
|
+
const aichatHealthChecker = createAIChatHealthChecker({
|
|
236
|
+
accountsStore: aichatAccountsStore,
|
|
237
|
+
vendorAdapter: aichatVendorAdapter,
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
aichatHealthChecker.start();
|
|
241
|
+
} catch (_err) {
|
|
242
|
+
// Continue boot even if checker fails to schedule
|
|
243
|
+
}
|
|
244
|
+
const aichatWizard = getAIChatWizard({ hubDir });
|
|
245
|
+
|
|
214
246
|
return {
|
|
215
247
|
vault,
|
|
216
248
|
registry,
|
|
@@ -223,11 +255,50 @@ async function initHub() {
|
|
|
223
255
|
emailAccountsPath,
|
|
224
256
|
alipayAccountsPath,
|
|
225
257
|
entityResolver,
|
|
258
|
+
aichatAccountsStore,
|
|
259
|
+
aichatWizard,
|
|
260
|
+
aichatHealthChecker,
|
|
226
261
|
analysisSkillNames: ANALYSIS_SKILL_NAMES,
|
|
227
262
|
async runSkill(name, options = {}) {
|
|
228
263
|
return await runAnalysisSkill({ vault, llm }, name, options);
|
|
229
264
|
},
|
|
230
265
|
bm25: _bm25,
|
|
266
|
+
|
|
267
|
+
/** Phase 10.3.5 — see desktop wiring for full doc. */
|
|
268
|
+
async listAIChatAccounts() {
|
|
269
|
+
const entries = await aichatAccountsStore.list();
|
|
270
|
+
return (entries || []).map((e) => ({
|
|
271
|
+
vendor: e.vendor,
|
|
272
|
+
displayName: e.displayName || e.vendor,
|
|
273
|
+
registeredAt: e.registeredAt || null,
|
|
274
|
+
userId: e.userId || null,
|
|
275
|
+
lastSyncAt: e.lastSyncAt || null,
|
|
276
|
+
lastHealth: e.lastHealth || null,
|
|
277
|
+
cookieSpecVersion: e.cookieSpecVersion || null,
|
|
278
|
+
cookieNames: Object.keys(e.cookies || {}),
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
async unregisterAIChatVendor(vendor) {
|
|
283
|
+
if (!vendor || typeof vendor !== "string") {
|
|
284
|
+
return { ok: false, reason: "VENDOR_REQUIRED" };
|
|
285
|
+
}
|
|
286
|
+
const before = await aichatAccountsStore.get(vendor);
|
|
287
|
+
if (!before) {
|
|
288
|
+
return { ok: false, reason: "NOT_REGISTERED", vendor };
|
|
289
|
+
}
|
|
290
|
+
await aichatAccountsStore.delete(vendor);
|
|
291
|
+
try {
|
|
292
|
+
await aichatWizard.rotateLoginPartition({ vendor });
|
|
293
|
+
} catch (_err) {
|
|
294
|
+
// best-effort
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, vendor };
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
async runAIChatHealthCheckOnce() {
|
|
300
|
+
return await aichatHealthChecker.runOnce();
|
|
301
|
+
},
|
|
231
302
|
registerMockAdapter(opts = {}) {
|
|
232
303
|
if (registry.has(opts.name || "mock"))
|
|
233
304
|
return registry.get(opts.name || "mock");
|
|
@@ -358,9 +429,122 @@ async function initHub() {
|
|
|
358
429
|
zipPassword,
|
|
359
430
|
});
|
|
360
431
|
},
|
|
432
|
+
|
|
433
|
+
// ─── Phase 12.6.8 — WeChat env-probe + register / unregister / list ──
|
|
434
|
+
//
|
|
435
|
+
// cc ui mirror of the desktop wiring. Same JSON file layout
|
|
436
|
+
// (wechat-accounts.json under hubDir, mode 0o600) so the two shells
|
|
437
|
+
// share registrations when run side-by-side on the same machine.
|
|
438
|
+
|
|
439
|
+
async probeWechatEnv(opts = {}) {
|
|
440
|
+
return await probeWeChatEnv({ exec: opts.exec });
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
async registerWechatAdapter(opts = {}) {
|
|
444
|
+
if (!opts || !opts.account || !opts.account.uin) {
|
|
445
|
+
return { ok: false, reason: "UIN_REQUIRED", message: "opts.account.uin required" };
|
|
446
|
+
}
|
|
447
|
+
let r;
|
|
448
|
+
try {
|
|
449
|
+
r = await bootstrapWechatAdapter({
|
|
450
|
+
account: opts.account,
|
|
451
|
+
dbPath: opts.dbPath || null,
|
|
452
|
+
wechatDataPath: opts.wechatDataPath || null,
|
|
453
|
+
fridaOpts: opts.fridaOpts || null,
|
|
454
|
+
keyProviderOverride: opts.keyProviderOverride || null,
|
|
455
|
+
exec: opts.exec,
|
|
456
|
+
_probe: opts._probe,
|
|
457
|
+
});
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
reason: "BOOTSTRAP_THREW",
|
|
462
|
+
message: err && err.message ? err.message : String(err),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (!r.ok) return r;
|
|
466
|
+
|
|
467
|
+
if (registry.has(r.adapter.name)) registry.unregister(r.adapter.name);
|
|
468
|
+
registry.register(r.adapter);
|
|
469
|
+
|
|
470
|
+
const wechatAccountsPath = join(hubDir, "wechat-accounts.json");
|
|
471
|
+
const accounts = loadWechatAccounts(wechatAccountsPath);
|
|
472
|
+
const next = accounts.filter(
|
|
473
|
+
(c) => !(c.account && c.account.uin === opts.account.uin),
|
|
474
|
+
);
|
|
475
|
+
next.push({
|
|
476
|
+
account: { uin: opts.account.uin },
|
|
477
|
+
dbPath: opts.dbPath || null,
|
|
478
|
+
wechatDataPath: opts.wechatDataPath || null,
|
|
479
|
+
chosenKeyProvider: r.keyProvider && r.keyProvider.name ? r.keyProvider.name : null,
|
|
480
|
+
registeredAt: Date.now(),
|
|
481
|
+
lastSyncAt: null,
|
|
482
|
+
});
|
|
483
|
+
saveWechatAccounts(wechatAccountsPath, next);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
ok: true,
|
|
487
|
+
name: r.adapter.name,
|
|
488
|
+
version: r.adapter.version,
|
|
489
|
+
capabilities: r.adapter.capabilities,
|
|
490
|
+
sensitivity: r.adapter.dataDisclosure.sensitivity,
|
|
491
|
+
chosenKeyProvider: r.keyProvider && r.keyProvider.name,
|
|
492
|
+
probe: r.probe,
|
|
493
|
+
registeredAt: next[next.length - 1].registeredAt,
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
async unregisterWechatAdapter(uin) {
|
|
498
|
+
if (!uin || typeof uin !== "string") {
|
|
499
|
+
return { ok: false, reason: "UIN_REQUIRED" };
|
|
500
|
+
}
|
|
501
|
+
const wechatAccountsPath = join(hubDir, "wechat-accounts.json");
|
|
502
|
+
const accounts = loadWechatAccounts(wechatAccountsPath);
|
|
503
|
+
const target = accounts.find(
|
|
504
|
+
(c) => c.account && c.account.uin === uin,
|
|
505
|
+
);
|
|
506
|
+
const next = accounts.filter(
|
|
507
|
+
(c) => !(c.account && c.account.uin === uin),
|
|
508
|
+
);
|
|
509
|
+
saveWechatAccounts(wechatAccountsPath, next);
|
|
510
|
+
if (target && registry.has("wechat")) registry.unregister("wechat");
|
|
511
|
+
return { ok: true, removed: !!target, uin };
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
listWechatAccounts() {
|
|
515
|
+
const wechatAccountsPath = join(hubDir, "wechat-accounts.json");
|
|
516
|
+
return loadWechatAccounts(wechatAccountsPath).map((row) => ({
|
|
517
|
+
uin: row.account ? row.account.uin : null,
|
|
518
|
+
dbPath: row.dbPath || null,
|
|
519
|
+
hasWechatDataPath: !!row.wechatDataPath,
|
|
520
|
+
chosenKeyProvider: row.chosenKeyProvider || null,
|
|
521
|
+
registeredAt: row.registeredAt || null,
|
|
522
|
+
lastSyncAt: row.lastSyncAt || null,
|
|
523
|
+
}));
|
|
524
|
+
},
|
|
361
525
|
};
|
|
362
526
|
}
|
|
363
527
|
|
|
528
|
+
// ─── Phase 12.6.8 — WeChat account persistence helpers (cli mirror) ──────
|
|
529
|
+
|
|
530
|
+
function loadWechatAccounts(filePath) {
|
|
531
|
+
try {
|
|
532
|
+
if (!existsSync(filePath)) return [];
|
|
533
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
534
|
+
const parsed = JSON.parse(raw);
|
|
535
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
536
|
+
} catch (_err) {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function saveWechatAccounts(filePath, accounts) {
|
|
542
|
+
writeFileSync(filePath, JSON.stringify(accounts, null, 2), {
|
|
543
|
+
encoding: "utf-8",
|
|
544
|
+
mode: 0o600,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
364
548
|
// ─── Email account persistence (Phase 5.6) ───────────────────────────────
|
|
365
549
|
|
|
366
550
|
function loadEmailAccounts(filePath) {
|
|
@@ -418,6 +602,11 @@ export async function getHub() {
|
|
|
418
602
|
}
|
|
419
603
|
|
|
420
604
|
export function close() {
|
|
605
|
+
if (_hub && _hub.aichatHealthChecker) {
|
|
606
|
+
try {
|
|
607
|
+
_hub.aichatHealthChecker.stop();
|
|
608
|
+
} catch (_e) {}
|
|
609
|
+
}
|
|
421
610
|
if (_hub && _hub.vault) {
|
|
422
611
|
try {
|
|
423
612
|
_hub.vault.close();
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI sync vault — better-sqlite3 wrapper exposing dbManager.run/all/get
|
|
3
|
+
* interface matching desktop's engine deps signature.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3c follow-up Phase 2: cc sync run requires SQLite store for cursors,
|
|
6
|
+
* tombstones, and knowledge_items. CLI vault lives at
|
|
7
|
+
* `~/.chainlesschain/cli-vault.db` and is plain SQLite (no SQLCipher) —
|
|
8
|
+
* encryption is at the OS file-permission layer; users wanting strong
|
|
9
|
+
* crypto should use desktop with hardware U-Key.
|
|
10
|
+
*
|
|
11
|
+
* Schema mirrors desktop `database-schema.js` for the 3 sync-relevant
|
|
12
|
+
* tables (auto-initialized on first open):
|
|
13
|
+
* - knowledge_items user notes (source of truth for sync)
|
|
14
|
+
* - sync_external_provider_cursor per-provider state
|
|
15
|
+
* - sync_external_tombstones per-provider delete queue
|
|
16
|
+
* - trg_sync_ext_tombstone_on_delete trigger (auto-fan tombstone on delete)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
"use strict";
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
|
|
25
|
+
function _ccDir() {
|
|
26
|
+
return (
|
|
27
|
+
process.env.CHAINLESSCHAIN_HOME ||
|
|
28
|
+
path.join(os.homedir(), ".chainlesschain")
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _vaultPath() {
|
|
33
|
+
return path.join(_ccDir(), "cli-vault.db");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CLI_VAULT_SCHEMA = `
|
|
37
|
+
CREATE TABLE IF NOT EXISTS knowledge_items (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
title TEXT NOT NULL,
|
|
40
|
+
type TEXT NOT NULL DEFAULT 'note',
|
|
41
|
+
content TEXT,
|
|
42
|
+
tags TEXT,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
updated_at INTEGER NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS sync_external_provider_cursor (
|
|
48
|
+
provider_id TEXT NOT NULL,
|
|
49
|
+
account_key TEXT NOT NULL DEFAULT '',
|
|
50
|
+
last_sync_at INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
last_item_id TEXT,
|
|
52
|
+
remote_etag_map TEXT NOT NULL DEFAULT '{}',
|
|
53
|
+
remote_filename_map TEXT NOT NULL DEFAULT '{}',
|
|
54
|
+
last_run_status TEXT,
|
|
55
|
+
last_run_error TEXT,
|
|
56
|
+
last_run_duration_ms INTEGER,
|
|
57
|
+
items_pushed INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
items_skipped INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
items_deleted INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
|
|
61
|
+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000),
|
|
62
|
+
PRIMARY KEY (provider_id, account_key)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS sync_external_tombstones (
|
|
66
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
provider_id TEXT NOT NULL,
|
|
68
|
+
account_key TEXT NOT NULL DEFAULT '',
|
|
69
|
+
item_id TEXT NOT NULL,
|
|
70
|
+
resource_type TEXT,
|
|
71
|
+
deleted_at INTEGER NOT NULL,
|
|
72
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
last_error TEXT,
|
|
74
|
+
UNIQUE(provider_id, account_key, item_id)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
CREATE TRIGGER IF NOT EXISTS trg_sync_ext_tombstone_on_delete
|
|
78
|
+
AFTER DELETE ON knowledge_items
|
|
79
|
+
FOR EACH ROW
|
|
80
|
+
BEGIN
|
|
81
|
+
INSERT OR IGNORE INTO sync_external_tombstones
|
|
82
|
+
(provider_id, account_key, item_id, resource_type, deleted_at)
|
|
83
|
+
SELECT c.provider_id, c.account_key, OLD.id, 'KNOWLEDGE_ITEM',
|
|
84
|
+
(strftime('%s','now') * 1000)
|
|
85
|
+
FROM sync_external_provider_cursor c;
|
|
86
|
+
END;
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Wraps better-sqlite3 Database with the {run, all, get} surface that
|
|
91
|
+
* the desktop sync engine's deps.dbManager expects.
|
|
92
|
+
*/
|
|
93
|
+
class CliVaultDbManager {
|
|
94
|
+
constructor(db) {
|
|
95
|
+
this.db = db;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Match desktop dbManager.run(sql, params=[]) */
|
|
99
|
+
run(sql, params = []) {
|
|
100
|
+
this.db.prepare(sql).run(...params);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Match desktop dbManager.get(sql, params=[]) → row or undefined */
|
|
104
|
+
get(sql, params = []) {
|
|
105
|
+
const row = this.db.prepare(sql).get(...params);
|
|
106
|
+
return row ?? undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Match desktop dbManager.all(sql, params=[]) → rows */
|
|
110
|
+
all(sql, params = []) {
|
|
111
|
+
return this.db.prepare(sql).all(...params);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
close() {
|
|
115
|
+
if (this.db && this.db.open) this.db.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Open (or create) the CLI vault. Auto-initializes schema on first open.
|
|
121
|
+
*
|
|
122
|
+
* @returns {Promise<{dbManager: CliVaultDbManager, vaultPath: string}>}
|
|
123
|
+
*/
|
|
124
|
+
async function openCliVault() {
|
|
125
|
+
const dir = _ccDir();
|
|
126
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
127
|
+
|
|
128
|
+
// Lazy-import better-sqlite3 — heavy native dep, defer until run-time
|
|
129
|
+
// (so the CLI doesn't even pay the require cost for non-sync commands).
|
|
130
|
+
let Database;
|
|
131
|
+
try {
|
|
132
|
+
Database = (await import("better-sqlite3")).default;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const e = new Error(
|
|
135
|
+
"CLI vault requires better-sqlite3 npm package. " +
|
|
136
|
+
"Run `npm install -g chainlesschain` to fetch the prebuilt binary, " +
|
|
137
|
+
"or `cd packages/cli && npm install` for the dev workspace. " +
|
|
138
|
+
"Underlying error: " +
|
|
139
|
+
(err?.message || String(err)),
|
|
140
|
+
);
|
|
141
|
+
e.code = "BETTER_SQLITE3_MISSING";
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const vp = _vaultPath();
|
|
146
|
+
const db = new Database(vp);
|
|
147
|
+
db.exec(CLI_VAULT_SCHEMA);
|
|
148
|
+
return { dbManager: new CliVaultDbManager(db), vaultPath: vp };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let _ccDirOverride = null;
|
|
152
|
+
function _setCcDirForTest(dir) {
|
|
153
|
+
_ccDirOverride = dir;
|
|
154
|
+
}
|
|
155
|
+
function _resetCcDirForTest() {
|
|
156
|
+
_ccDirOverride = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Re-wrap _ccDir/_vaultPath to honor override (test seam)
|
|
160
|
+
const _originalCcDir = _ccDir;
|
|
161
|
+
function _ccDirEffective() {
|
|
162
|
+
return _ccDirOverride ?? _originalCcDir();
|
|
163
|
+
}
|
|
164
|
+
function _vaultPathEffective() {
|
|
165
|
+
return path.join(_ccDirEffective(), "cli-vault.db");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function openCliVaultT() {
|
|
169
|
+
const dir = _ccDirEffective();
|
|
170
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
171
|
+
let Database;
|
|
172
|
+
try {
|
|
173
|
+
Database = (await import("better-sqlite3")).default;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
const e = new Error(
|
|
176
|
+
"CLI vault requires better-sqlite3 npm package: " + (err?.message || err),
|
|
177
|
+
);
|
|
178
|
+
e.code = "BETTER_SQLITE3_MISSING";
|
|
179
|
+
throw e;
|
|
180
|
+
}
|
|
181
|
+
const vp = _vaultPathEffective();
|
|
182
|
+
const db = new Database(vp);
|
|
183
|
+
db.exec(CLI_VAULT_SCHEMA);
|
|
184
|
+
return { dbManager: new CliVaultDbManager(db), vaultPath: vp };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {
|
|
188
|
+
openCliVaultT as openCliVault,
|
|
189
|
+
CliVaultDbManager,
|
|
190
|
+
CLI_VAULT_SCHEMA,
|
|
191
|
+
_setCcDirForTest,
|
|
192
|
+
_resetCcDirForTest,
|
|
193
|
+
_vaultPathEffective as _vaultPath,
|
|
194
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI sync provider credentials store — Phase 3c follow-up.
|
|
3
|
+
*
|
|
4
|
+
* Electron 的 `safeStorage` 在 CLI 没法用,所以这里搭一个 file-based equivalent:
|
|
5
|
+
*
|
|
6
|
+
* - Master key 自动生成 (32 字节 random) 落 `~/.chainlesschain/sync-credentials.key`
|
|
7
|
+
* 文件 mode 0600 (Unix) / NTFS ACL (Win — fs.chmodSync 容错)
|
|
8
|
+
* - 凭据 JSON 编 AES-256-GCM (iv 12B + auth tag 16B)
|
|
9
|
+
* 落 `~/.chainlesschain/sync-credentials.enc`
|
|
10
|
+
* - SENSITIVE_FIELDS 镜像 desktop secure-config-storage.js 用于 sanitize
|
|
11
|
+
*
|
|
12
|
+
* 与 desktop sync-credentials.js 同 surface:
|
|
13
|
+
* getCredentials / setCredentials / clearCredentials / hasCredentials /
|
|
14
|
+
* getCredentialsSanitized / ALLOWED_PROVIDER_IDS
|
|
15
|
+
*
|
|
16
|
+
* 威胁模型:root/admin 能读 master key 文件即破,是 CLI 工具合理 baseline
|
|
17
|
+
* (同 git ~/.netrc / ~/.aws/credentials)。OS keyring 强加密留 v0.2。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import crypto from "node:crypto";
|
|
26
|
+
|
|
27
|
+
const SENSITIVE_FIELDS = ["sync.webdav.password", "sync.oss.secretAccessKey"];
|
|
28
|
+
const ALLOWED_PROVIDER_IDS = ["webdav", "oss"];
|
|
29
|
+
const MASK = "********";
|
|
30
|
+
const AES_ALG = "aes-256-gcm";
|
|
31
|
+
const IV_LEN = 12;
|
|
32
|
+
const TAG_LEN = 16;
|
|
33
|
+
const KEY_LEN = 32;
|
|
34
|
+
|
|
35
|
+
let _ccDirOverride = null;
|
|
36
|
+
|
|
37
|
+
function _ccDir() {
|
|
38
|
+
if (_ccDirOverride) return _ccDirOverride;
|
|
39
|
+
return (
|
|
40
|
+
process.env.CHAINLESSCHAIN_HOME ||
|
|
41
|
+
path.join(os.homedir(), ".chainlesschain")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _keyPath() {
|
|
46
|
+
return path.join(_ccDir(), "sync-credentials.key");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function _vaultPath() {
|
|
50
|
+
return path.join(_ccDir(), "sync-credentials.enc");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _ensureDir() {
|
|
54
|
+
const dir = _ccDir();
|
|
55
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _loadOrCreateMasterKey() {
|
|
59
|
+
_ensureDir();
|
|
60
|
+
const kp = _keyPath();
|
|
61
|
+
if (fs.existsSync(kp)) {
|
|
62
|
+
const buf = fs.readFileSync(kp);
|
|
63
|
+
if (buf.length !== KEY_LEN) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`sync-credentials: master key file ${kp} has wrong length ` +
|
|
66
|
+
`(${buf.length} bytes, expected ${KEY_LEN}). Delete + regenerate manually if intentional.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return buf;
|
|
70
|
+
}
|
|
71
|
+
const key = crypto.randomBytes(KEY_LEN);
|
|
72
|
+
fs.writeFileSync(kp, key);
|
|
73
|
+
try {
|
|
74
|
+
fs.chmodSync(kp, 0o600);
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
/* non-fatal: NTFS / older fs */
|
|
77
|
+
}
|
|
78
|
+
return key;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _encryptBlob(plainJson) {
|
|
82
|
+
const key = _loadOrCreateMasterKey();
|
|
83
|
+
const iv = crypto.randomBytes(IV_LEN);
|
|
84
|
+
const cipher = crypto.createCipheriv(AES_ALG, key, iv);
|
|
85
|
+
const enc = Buffer.concat([
|
|
86
|
+
cipher.update(plainJson, "utf-8"),
|
|
87
|
+
cipher.final(),
|
|
88
|
+
]);
|
|
89
|
+
const tag = cipher.getAuthTag();
|
|
90
|
+
// file layout: [iv (12)] [tag (16)] [ciphertext]
|
|
91
|
+
return Buffer.concat([iv, tag, enc]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _decryptBlob(buf) {
|
|
95
|
+
if (!Buffer.isBuffer(buf) || buf.length < IV_LEN + TAG_LEN + 1) {
|
|
96
|
+
throw new Error("sync-credentials: enc file too small or invalid");
|
|
97
|
+
}
|
|
98
|
+
const key = _loadOrCreateMasterKey();
|
|
99
|
+
const iv = buf.subarray(0, IV_LEN);
|
|
100
|
+
const tag = buf.subarray(IV_LEN, IV_LEN + TAG_LEN);
|
|
101
|
+
const enc = buf.subarray(IV_LEN + TAG_LEN);
|
|
102
|
+
const decipher = crypto.createDecipheriv(AES_ALG, key, iv);
|
|
103
|
+
decipher.setAuthTag(tag);
|
|
104
|
+
return Buffer.concat([decipher.update(enc), decipher.final()]).toString(
|
|
105
|
+
"utf-8",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function loadAll() {
|
|
110
|
+
const vp = _vaultPath();
|
|
111
|
+
if (!fs.existsSync(vp)) return {};
|
|
112
|
+
const buf = fs.readFileSync(vp);
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(_decryptBlob(buf));
|
|
115
|
+
} catch (err) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`sync-credentials: decrypt failed (${err?.message}). ` +
|
|
118
|
+
`Vault may be corrupted or master key changed. ` +
|
|
119
|
+
`Remove ${vp} + reconfigure to recover.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function saveAll(all) {
|
|
125
|
+
_ensureDir();
|
|
126
|
+
fs.writeFileSync(_vaultPath(), _encryptBlob(JSON.stringify(all)));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertProviderId(providerId) {
|
|
131
|
+
if (!ALLOWED_PROVIDER_IDS.includes(providerId)) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`sync-credentials: unknown provider id '${providerId}' ` +
|
|
134
|
+
`(allowed: ${ALLOWED_PROVIDER_IDS.join(", ")})`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function sanitize(all) {
|
|
140
|
+
if (!all || typeof all !== "object") return all;
|
|
141
|
+
const clone = JSON.parse(JSON.stringify(all));
|
|
142
|
+
for (const dotPath of SENSITIVE_FIELDS) {
|
|
143
|
+
const parts = dotPath.split(".");
|
|
144
|
+
let cur = clone;
|
|
145
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
146
|
+
if (cur && typeof cur === "object" && parts[i] in cur) {
|
|
147
|
+
cur = cur[parts[i]];
|
|
148
|
+
} else {
|
|
149
|
+
cur = null;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const last = parts[parts.length - 1];
|
|
154
|
+
if (
|
|
155
|
+
cur &&
|
|
156
|
+
typeof cur === "object" &&
|
|
157
|
+
cur[last] != null &&
|
|
158
|
+
cur[last] !== ""
|
|
159
|
+
) {
|
|
160
|
+
cur[last] = MASK;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return clone;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getCredentials(providerId) {
|
|
167
|
+
assertProviderId(providerId);
|
|
168
|
+
return loadAll()?.sync?.[providerId] ?? {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getCredentialsSanitized(providerId) {
|
|
172
|
+
assertProviderId(providerId);
|
|
173
|
+
return sanitize(loadAll())?.sync?.[providerId] ?? {};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasCredentials(providerId) {
|
|
177
|
+
return Object.values(getCredentials(providerId)).some(
|
|
178
|
+
(v) => v !== null && v !== undefined && v !== "",
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setCredentials(providerId, creds) {
|
|
183
|
+
assertProviderId(providerId);
|
|
184
|
+
if (!creds || typeof creds !== "object") {
|
|
185
|
+
throw new Error("sync-credentials: creds must be an object");
|
|
186
|
+
}
|
|
187
|
+
const all = loadAll();
|
|
188
|
+
if (!all.sync || typeof all.sync !== "object") all.sync = {};
|
|
189
|
+
all.sync[providerId] = { ...creds };
|
|
190
|
+
return saveAll(all);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function clearCredentials(providerId) {
|
|
194
|
+
assertProviderId(providerId);
|
|
195
|
+
const all = loadAll();
|
|
196
|
+
if (all?.sync?.[providerId]) {
|
|
197
|
+
delete all.sync[providerId];
|
|
198
|
+
return saveAll(all);
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Test seam: override the resolved chainlesschain dir without env leak. */
|
|
204
|
+
function _setCcDirForTest(dir) {
|
|
205
|
+
_ccDirOverride = dir;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _resetCcDirForTest() {
|
|
209
|
+
_ccDirOverride = null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export {
|
|
213
|
+
ALLOWED_PROVIDER_IDS,
|
|
214
|
+
SENSITIVE_FIELDS,
|
|
215
|
+
MASK,
|
|
216
|
+
getCredentials,
|
|
217
|
+
getCredentialsSanitized,
|
|
218
|
+
hasCredentials,
|
|
219
|
+
setCredentials,
|
|
220
|
+
clearCredentials,
|
|
221
|
+
_setCcDirForTest,
|
|
222
|
+
_resetCcDirForTest,
|
|
223
|
+
_keyPath,
|
|
224
|
+
_vaultPath,
|
|
225
|
+
};
|