chainlesschain 0.162.12 → 0.162.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +29 -24
  2. package/package.json +5 -2
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-C3TDNq29.js → AIOps-D34d_Nh1.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-C9fE18pE.js → ActionButton-Br7HxCnl.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-wnZF602C.js → Analytics-bVKq79Xd.js} +1 -1
  7. package/src/assets/web-panel/assets/{AppLayout-BjgTMK7O.js → AppLayout-CWSLIbAz.js} +2 -2
  8. package/src/assets/web-panel/assets/{Audit-BBL0BW5_.js → Audit-Cmnu1qqa.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-BKLqYCWU.js → Backup-Rok20-TL.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-BGSzMCZs.js → BaseInput-BJzs_ZtT.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CQWzZWEY.js → Chat-CSYapbcq.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-BkTri12Q.js → Checkbox-BEa7Sr7e.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-BH1m09EO.js → Codegen-C9M4e7ne.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-BXnBuqIa.js → Col-DU9NoUIi.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-C_Nr4XCx.js → Community-DA9uz_jP.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-Du6GwLrj.js → Compact-3_bEraVw.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-66M0oi1Q.js → Compliance-BtF8jWUQ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-DQrkZRNd.js → Cowork-BqvA7oaM.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cron-CwdIFH_v.js → Cron-CxZy7Mzg.js} +1 -1
  20. package/src/assets/web-panel/assets/{Crosschain-DqlcrQ9L.js → Crosschain-1DB-XRGu.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-OmPLKf7L.js → DID-B6Ezp1pt.js} +1 -1
  22. package/src/assets/web-panel/assets/{Dashboard-D_dampTL.js → Dashboard-QDJ6VVsn.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-CA1W7jAn.js → Dropdown-CovsWjxG.js} +1 -1
  24. package/src/assets/web-panel/assets/{Federation-Chlk9a7s.js → Federation-DbRxS4Y4.js} +1 -1
  25. package/src/assets/web-panel/assets/{FormItemContext-t0UqYFLq.js → FormItemContext-9E9dNGtx.js} +1 -1
  26. package/src/assets/web-panel/assets/{Git-CEq0raYm.js → Git-CqEpyxRZ.js} +2 -2
  27. package/src/assets/web-panel/assets/{Governance-C06CX7Ge.js → Governance-On47KtGq.js} +1 -1
  28. package/src/assets/web-panel/assets/{Inference-6VIFHxIP.js → Inference-RZcjcyaq.js} +1 -1
  29. package/src/assets/web-panel/assets/{KnowledgeGraph-BCJPjMBQ.js → KnowledgeGraph-C-1rRAM9.js} +1 -1
  30. package/src/assets/web-panel/assets/{Logs-BBpOYFct.js → Logs-BuunmG_r.js} +1 -1
  31. package/src/assets/web-panel/assets/{Marketplace-BFH6jMWt.js → Marketplace-CromymyA.js} +1 -1
  32. package/src/assets/web-panel/assets/{McpTools-uCFvRqGs.js → McpTools-5XlFExh7.js} +1 -1
  33. package/src/assets/web-panel/assets/{Memory-B0Kux_KT.js → Memory-DjnUT7YM.js} +1 -1
  34. package/src/assets/web-panel/assets/{MobileBridge-DHow2jiK.js → MobileBridge-BrYIgLg6.js} +1 -1
  35. package/src/assets/web-panel/assets/{MobileProjects-BFo9YQZp.js → MobileProjects-CL5V3fTm.js} +1 -1
  36. package/src/assets/web-panel/assets/{Mtc-riOh1G_F.js → Mtc-CHYJq6zK.js} +1 -1
  37. package/src/assets/web-panel/assets/{MtcAudit-Bm-hE2SP.js → MtcAudit-BZxUO0qt.js} +1 -1
  38. package/src/assets/web-panel/assets/{Multisig-DfUQxh5a.js → Multisig-FZTmJgW1.js} +2 -2
  39. package/src/assets/web-panel/assets/{NLProgramming-DuNvLBEq.js → NLProgramming-C9Mhefph.js} +1 -1
  40. package/src/assets/web-panel/assets/{Notes-DB20wd3c.js → Notes-W7usj-Ar.js} +1 -1
  41. package/src/assets/web-panel/assets/{NotificationSettings-CB-GkOWR.js → NotificationSettings-PBuYv_Bh.js} +1 -1
  42. package/src/assets/web-panel/assets/{Organization-3bU7PZuG.js → Organization-CuYCE-rF.js} +4 -4
  43. package/src/assets/web-panel/assets/{Overflow-BGCPP_0Y.js → Overflow-Dojx-kzE.js} +1 -1
  44. package/src/assets/web-panel/assets/{OverrideContext-x9ZzjLwk.js → OverrideContext-C_4H9tGA.js} +1 -1
  45. package/src/assets/web-panel/assets/{P2P-BHgAe1oC.js → P2P-BgIaSrLX.js} +1 -1
  46. package/src/assets/web-panel/assets/{Permissions-BuOD4xwc.js → Permissions-Byj2dkF_.js} +1 -1
  47. package/src/assets/web-panel/assets/PersonalDataHub-CMOOI13-.js +1 -0
  48. package/src/assets/web-panel/assets/PersonalDataHub-Dvaa8niQ.css +1 -0
  49. package/src/assets/web-panel/assets/{Pipeline-DBS5U4LB.js → Pipeline-CWwEOF09.js} +1 -1
  50. package/src/assets/web-panel/assets/{Privacy-UNjIc5El.js → Privacy-VT7gldcN.js} +1 -1
  51. package/src/assets/web-panel/assets/{ProjectInit-CicqCJGy.js → ProjectInit-7UH3c3p7.js} +1 -1
  52. package/src/assets/web-panel/assets/{ProjectSettings-CIxAbt4Y.js → ProjectSettings-DqLp-72a.js} +1 -1
  53. package/src/assets/web-panel/assets/{Projects-BJycZScO.js → Projects-B_54eDhH.js} +1 -1
  54. package/src/assets/web-panel/assets/{Providers-DxXvprme.js → Providers-BIrNfNpc.js} +1 -1
  55. package/src/assets/web-panel/assets/{QuickAsk-rrqjU8_Y.js → QuickAsk-BbYPwCso.js} +1 -1
  56. package/src/assets/web-panel/assets/{Recommend-BEwHMhI7.js → Recommend-BF4qBssF.js} +1 -1
  57. package/src/assets/web-panel/assets/{Reputation-DoVKCCMn.js → Reputation-DPEzlC2V.js} +1 -1
  58. package/src/assets/web-panel/assets/{Row-F5XcDhHr.js → Row-DjHxhH1L.js} +1 -1
  59. package/src/assets/web-panel/assets/{RssFeed-cZrRG7k8.js → RssFeed-D0_j678P.js} +1 -1
  60. package/src/assets/web-panel/assets/{Search-B9ctZjqx.js → Search-DctfGehu.js} +1 -1
  61. package/src/assets/web-panel/assets/{Security-Z62hl1mc.js → Security-BFHggeYM.js} +1 -1
  62. package/src/assets/web-panel/assets/{Services-CQf5XqgZ.js → Services-CmrFMukV.js} +1 -1
  63. package/src/assets/web-panel/assets/{Skeleton-DuCKw2Eh.js → Skeleton-DR4vn_nS.js} +1 -1
  64. package/src/assets/web-panel/assets/{Skills-qVkhva0s.js → Skills-DlXG2yyV.js} +1 -1
  65. package/src/assets/web-panel/assets/{Sla-BQbatr7s.js → Sla-4PPGL3SE.js} +1 -1
  66. package/src/assets/web-panel/assets/{SpeechSettings-DLFBzAgD.js → SpeechSettings-D9EhJOqm.js} +1 -1
  67. package/src/assets/web-panel/assets/{SyncSettings-CrzETZMW.js → SyncSettings-Dasmbi0p.js} +1 -1
  68. package/src/assets/web-panel/assets/{Tasks-D_EQ1nJ7.js → Tasks-vilEiuPA.js} +1 -1
  69. package/src/assets/web-panel/assets/{Templates-D4y-dGRc.js → Templates-Ca9Rvktn.js} +1 -1
  70. package/src/assets/web-panel/assets/{Tenant-2XI0jkPn.js → Tenant-CEZb9gfK.js} +1 -1
  71. package/src/assets/web-panel/assets/{Terminal-fUi5V2Z9.js → Terminal-DanCBdbD.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tokens-BuUNB2mg.js → Tokens-SPkClW2d.js} +1 -1
  73. package/src/assets/web-panel/assets/{Trigger-7DqLLuej.js → Trigger-B645yL7g.js} +1 -1
  74. package/src/assets/web-panel/assets/{Trust-CeACvTYx.js → Trust-D9sM_Ig0.js} +1 -1
  75. package/src/assets/web-panel/assets/{UkeySign-mDP9EXHq.js → UkeySign-B_Nr2K-u.js} +1 -1
  76. package/src/assets/web-panel/assets/{VideoEditing-veWlKclv.js → VideoEditing-U01Lea8j.js} +1 -1
  77. package/src/assets/web-panel/assets/{Wallet-Cd2Hheb8.js → Wallet-6xBySVV8.js} +1 -1
  78. package/src/assets/web-panel/assets/{WebAuthn-DyL7ZiHX.js → WebAuthn-DbgMoBu6.js} +3 -3
  79. package/src/assets/web-panel/assets/{WorkflowEditor-C7-7LJH9.js → WorkflowEditor-Bz-Y6IR2.js} +1 -1
  80. package/src/assets/web-panel/assets/{chat-DXomZMuo.js → chat-BC_O9hag.js} +1 -1
  81. package/src/assets/web-panel/assets/{collapseMotion-CjFH_Jop.js → collapseMotion-DfnRZex1.js} +1 -1
  82. package/src/assets/web-panel/assets/{colors-DlU92QNs.js → colors-ChlOGOvr.js} +1 -1
  83. package/src/assets/web-panel/assets/{compact-item-sBiTL8mX.js → compact-item-BSbAYGGF.js} +1 -1
  84. package/src/assets/web-panel/assets/{createContext-DZXEnzum.js → createContext-CFcZly5M.js} +1 -1
  85. package/src/assets/web-panel/assets/{echarts-Bq-n0MtJ.js → echarts-Dj_pBaVI.js} +1 -1
  86. package/src/assets/web-panel/assets/{hasIn-CpCHBZ2M.js → hasIn-BomYwwYE.js} +1 -1
  87. package/src/assets/web-panel/assets/{icons-CLQTHa5-.js → icons-BOPtEWK4.js} +4 -4
  88. package/src/assets/web-panel/assets/{index-B0Qbxr57.js → index-5Ewm6KZA.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-CjXSvceY.js → index-B6LJHQoE.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-CD3iljXs.js → index-BEJ6YiLI.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BK2AFy44.js → index-BGUbtM3R.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-Di1_EQ-X.js → index-BHGsFwYW.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-B23tuoo9.js → index-BHi69MHF.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DUlPMzoM.js → index-BI1jAWcc.js} +1 -1
  95. package/src/assets/web-panel/assets/index-BIRYt1of.js +1 -0
  96. package/src/assets/web-panel/assets/{index-B3k9UPHc.js → index-BYDvb1pi.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-CwbWZubA.js → index-BZGdjNLA.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-ThrAiEF9.js → index-BwFykZ5U.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CUe5t5Aa.js → index-ByZQNO0A.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Dn8m1d1f.js → index-C0rr1X9W.js} +2 -2
  101. package/src/assets/web-panel/assets/{index-C6SDf50u.js → index-CBhoZhCO.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-ClN_JuFa.js → index-CCRSz2cR.js} +1 -1
  103. package/src/assets/web-panel/assets/index-CZfySmWX.js +1 -0
  104. package/src/assets/web-panel/assets/{index-Dq5Rn5VS.js → index-Cj47XwJQ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-ChahjdYE.js → index-Cmzh8gKL.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-Dyg6ikIL.js → index-CnxlKTDK.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-Dj9Nvz6S.js → index-CoF95pYK.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-TwQZkVGh.js → index-Ctx97mH-.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-s8tvk-fF.js → index-D0vX9jQA.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-X48zYgZ6.js → index-DNkth8dM.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DygNvCeR.js → index-DRp5_Xns.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-D_MzScPM.js → index-DW-Ji07y.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BqnhEJls.js → index-DXgE2VW6.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CDyzZ8_O.js → index-D_0B3CiU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C59FgSkU.js → index-Dbf5YmDX.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-_zyXBoS7.js → index-DsNQ2hqI.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CJ7XYa5K.js → index-EY733h9z.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-ibFHnqHz.js → index-QD_n54XT.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CivbS-57.js → index-T5Y_9IPv.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-C52udT0_.js → index-b8GbH2Yi.js} +4 -4
  121. package/src/assets/web-panel/assets/{index-XI6772AD.js → index-gUACAWbM.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-F9cBucYf.js → index-onW325hZ.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-DKe9jmKG.js → index-ozVPr1gj.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-D_IgY63-.js → index-slYX2rCE.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B_-RETt0.js → index-t9u2bHpH.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DeGnHcp5.js → index-za1GUJBG.js} +1 -1
  127. package/src/assets/web-panel/assets/{initDefaultProps-DEi92ZnZ.js → initDefaultProps-DnadEaxu.js} +1 -1
  128. package/src/assets/web-panel/assets/{motion-BtYKzpOc.js → motion-CC_Na0Tl.js} +1 -1
  129. package/src/assets/web-panel/assets/{move-Cb3A1-v-.js → move-C2d9Mkk9.js} +1 -1
  130. package/src/assets/web-panel/assets/{omit-B6qPDdOf.js → omit-QvpKbF8p.js} +1 -1
  131. package/src/assets/web-panel/assets/{pickAttrs-DDyeQMUc.js → pickAttrs-Dm8r3X1_.js} +1 -1
  132. package/src/assets/web-panel/assets/{placementArrow-BPV6VO47.js → placementArrow-DaqaVfoX.js} +1 -1
  133. package/src/assets/web-panel/assets/{responsiveObserve-DJ1ra4dT.js → responsiveObserve-Iida9fIn.js} +1 -1
  134. package/src/assets/web-panel/assets/{slide-D6v8tHvB.js → slide-YqHexXQD.js} +1 -1
  135. package/src/assets/web-panel/assets/{statusUtils-DulKcQLZ.js → statusUtils-BGKLoeEt.js} +1 -1
  136. package/src/assets/web-panel/assets/{styleChecker-Bne7zwMt.js → styleChecker-aI-gsQO8.js} +1 -1
  137. package/src/assets/web-panel/assets/useFlexGapSupport-BiOsz4rc.js +1 -0
  138. package/src/assets/web-panel/assets/{useFs-CR-iLa4Z.js → useFs-CZy7Zo2X.js} +1 -1
  139. package/src/assets/web-panel/assets/{useMergedState-O7QXt4P5.js → useMergedState-WwedrFR0.js} +1 -1
  140. package/src/assets/web-panel/assets/{useRefs-0J6m8UWN.js → useRefs-Cdq8EWeF.js} +1 -1
  141. package/src/assets/web-panel/assets/{useState-CSzR8F8O.js → useState-DGS1NOyn.js} +1 -1
  142. package/src/assets/web-panel/assets/{vendor-M5lGV-wr.js → vendor-DhFY8mDK.js} +1 -1
  143. package/src/assets/web-panel/assets/{vnode-yL9axxBy.js → vnode-B6WqjmE4.js} +1 -1
  144. package/src/assets/web-panel/assets/{zoom-B-VCMXSD.js → zoom-DTeTrJ2z.js} +1 -1
  145. package/src/assets/web-panel/index.html +3 -3
  146. package/src/commands/__tests__/android.test.js +260 -0
  147. package/src/commands/__tests__/hub-aichat.test.js +277 -0
  148. package/src/commands/__tests__/hub-wechat.test.js +243 -0
  149. package/src/commands/android.js +284 -0
  150. package/src/commands/hub.js +457 -0
  151. package/src/commands/sync-providers.js +436 -0
  152. package/src/gateways/ws/personal-data-hub-protocol.js +88 -0
  153. package/src/index.js +6 -0
  154. package/src/lib/__tests__/personal-data-hub-aichat-wizard.test.js +209 -0
  155. package/src/lib/__tests__/sync-credentials.test.js +265 -0
  156. package/src/lib/__tests__/sync-engine-cli.test.js +293 -0
  157. package/src/lib/cc-android-bridge.js +162 -0
  158. package/src/lib/personal-data-hub-aichat-wizard.js +242 -0
  159. package/src/lib/personal-data-hub-wiring.js +258 -13
  160. package/src/lib/sync-cli-db.js +194 -0
  161. package/src/lib/sync-credentials.js +225 -0
  162. package/src/lib/sync-engine-cli.js +406 -0
  163. package/src/lib/sync-oss-client.js +273 -0
  164. package/src/lib/sync-webdav-client.js +194 -0
  165. package/src/lib/web-ui-server.js +2 -1
  166. package/src/assets/web-panel/assets/PersonalDataHub--WA-aZAJ.js +0 -1
  167. package/src/assets/web-panel/assets/PersonalDataHub-BK7I0Rsb.css +0 -1
  168. package/src/assets/web-panel/assets/index-CcRX6BlT.js +0 -1
  169. package/src/assets/web-panel/assets/index-z6h6tqP3.js +0 -1
  170. package/src/assets/web-panel/assets/useFlexGapSupport-C1miTomM.js +0 -1
@@ -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
+ };
@@ -0,0 +1,406 @@
1
+ /**
2
+ * CLI sync engine — Phase 3c follow-up Phase 2.
3
+ *
4
+ * Provider-agnostic sync engine consolidating the desktop-side
5
+ * sync-external-store + incremental-walker + markdown-renderer + engine
6
+ * into ONE focused CLI module. The CLI vault has only KNOWLEDGE_ITEM
7
+ * tombstones (no mobile sync ResourceTypes) so we drop the filter
8
+ * parameter and simplify.
9
+ *
10
+ * Flow (mirror desktop runWebDAVSync / runOSSSync):
11
+ * 1. ensureCursor
12
+ * 2. drain tombstones (delete then deleteFile on remote)
13
+ * 3. push loop: fetchBatch → putFile → recordPushed → advance cursor
14
+ * 4. update final cursor state
15
+ */
16
+
17
+ "use strict";
18
+
19
+ const PROGRESS_FLUSH_EVERY = 5;
20
+ const PROGRESS_FLUSH_MS = 500;
21
+
22
+ // ── markdown renderer (deterministic; mirror desktop) ──────────────
23
+
24
+ function _cleanTitle(title) {
25
+ // Note: place `-` AT END of char-class — putting it mid-class triggers
26
+ // JS "invalid range" silent fallthrough and space stops matching.
27
+ return (
28
+ String(title || "untitled")
29
+ .replace(/[\\/:*?"<>|\s-]/g, "_")
30
+ .replace(/_+/g, "_")
31
+ .replace(/^_|_$/g, "")
32
+ .slice(0, 80) || "untitled"
33
+ );
34
+ }
35
+
36
+ function generateFilename(item) {
37
+ return `${item.id}-${_cleanTitle(item.title)}.md`;
38
+ }
39
+
40
+ function generateMarkdown(item) {
41
+ const tags = item.tags ? String(item.tags) : "";
42
+ const frontMatter = [
43
+ "---",
44
+ `id: ${item.id}`,
45
+ `title: ${JSON.stringify(item.title || "untitled")}`,
46
+ `type: ${item.type || "note"}`,
47
+ `created_at: ${item.created_at}`,
48
+ `updated_at: ${item.updated_at}`,
49
+ tags ? `tags: ${tags}` : "",
50
+ "---",
51
+ "",
52
+ ]
53
+ .filter(Boolean)
54
+ .join("\n");
55
+ return frontMatter + (item.content || "") + "\n";
56
+ }
57
+
58
+ // ── store: cursor + tombstone CRUD ─────────────────────────────────
59
+
60
+ function _parseJsonField(v, fallback) {
61
+ if (v == null || v === "") return fallback;
62
+ try {
63
+ return JSON.parse(v);
64
+ } catch (_e) {
65
+ return fallback;
66
+ }
67
+ }
68
+
69
+ function getCursor(dbManager, providerId, accountKey = "") {
70
+ const row = dbManager.get(
71
+ `SELECT * FROM sync_external_provider_cursor
72
+ WHERE provider_id = ? AND account_key = ?`,
73
+ [providerId, accountKey],
74
+ );
75
+ if (!row) return undefined;
76
+ return {
77
+ providerId: row.provider_id,
78
+ accountKey: row.account_key,
79
+ lastSyncAt: row.last_sync_at,
80
+ lastItemId: row.last_item_id,
81
+ remoteEtagMap: _parseJsonField(row.remote_etag_map, {}),
82
+ remoteFilenameMap: _parseJsonField(row.remote_filename_map, {}),
83
+ lastRunStatus: row.last_run_status,
84
+ lastRunError: row.last_run_error,
85
+ lastRunDurationMs: row.last_run_duration_ms,
86
+ itemsPushed: row.items_pushed,
87
+ itemsSkipped: row.items_skipped,
88
+ itemsDeleted: row.items_deleted,
89
+ createdAt: row.created_at,
90
+ updatedAt: row.updated_at,
91
+ };
92
+ }
93
+
94
+ function ensureCursor(dbManager, providerId, accountKey = "") {
95
+ const existing = getCursor(dbManager, providerId, accountKey);
96
+ if (existing) return existing;
97
+ dbManager.run(
98
+ `INSERT OR IGNORE INTO sync_external_provider_cursor
99
+ (provider_id, account_key) VALUES (?, ?)`,
100
+ [providerId, accountKey],
101
+ );
102
+ return getCursor(dbManager, providerId, accountKey);
103
+ }
104
+
105
+ function updateCursor(dbManager, providerId, patch, accountKey = "") {
106
+ if (!patch || typeof patch !== "object") return;
107
+ const fields = [];
108
+ const params = [];
109
+ const map = {
110
+ lastSyncAt: "last_sync_at",
111
+ lastItemId: "last_item_id",
112
+ lastRunStatus: "last_run_status",
113
+ lastRunError: "last_run_error",
114
+ lastRunDurationMs: "last_run_duration_ms",
115
+ itemsPushed: "items_pushed",
116
+ itemsSkipped: "items_skipped",
117
+ itemsDeleted: "items_deleted",
118
+ };
119
+ for (const [k, col] of Object.entries(map)) {
120
+ if (k in patch) {
121
+ fields.push(`${col} = ?`);
122
+ params.push(patch[k]);
123
+ }
124
+ }
125
+ if ("remoteEtagMap" in patch) {
126
+ fields.push("remote_etag_map = ?");
127
+ params.push(JSON.stringify(patch.remoteEtagMap));
128
+ }
129
+ if ("remoteFilenameMap" in patch) {
130
+ fields.push("remote_filename_map = ?");
131
+ params.push(JSON.stringify(patch.remoteFilenameMap));
132
+ }
133
+ fields.push("updated_at = ?");
134
+ params.push(Date.now());
135
+ params.push(providerId, accountKey);
136
+ dbManager.run(
137
+ `UPDATE sync_external_provider_cursor SET ${fields.join(", ")}
138
+ WHERE provider_id = ? AND account_key = ?`,
139
+ params,
140
+ );
141
+ }
142
+
143
+ function recordPushedItem(
144
+ dbManager,
145
+ providerId,
146
+ itemId,
147
+ etag,
148
+ filename,
149
+ accountKey = "",
150
+ ) {
151
+ const cursor = getCursor(dbManager, providerId, accountKey) || {
152
+ remoteEtagMap: {},
153
+ remoteFilenameMap: {},
154
+ };
155
+ const etagMap = { ...cursor.remoteEtagMap, [itemId]: etag };
156
+ const fnMap = { ...cursor.remoteFilenameMap, [itemId]: filename };
157
+ dbManager.run(
158
+ `UPDATE sync_external_provider_cursor
159
+ SET remote_etag_map = ?, remote_filename_map = ?, updated_at = ?
160
+ WHERE provider_id = ? AND account_key = ?`,
161
+ [
162
+ JSON.stringify(etagMap),
163
+ JSON.stringify(fnMap),
164
+ Date.now(),
165
+ providerId,
166
+ accountKey,
167
+ ],
168
+ );
169
+ }
170
+
171
+ function removeFromMaps(dbManager, providerId, itemId, accountKey = "") {
172
+ const cursor = getCursor(dbManager, providerId, accountKey);
173
+ if (!cursor) return;
174
+ const etagMap = { ...cursor.remoteEtagMap };
175
+ const fnMap = { ...cursor.remoteFilenameMap };
176
+ delete etagMap[itemId];
177
+ delete fnMap[itemId];
178
+ dbManager.run(
179
+ `UPDATE sync_external_provider_cursor
180
+ SET remote_etag_map = ?, remote_filename_map = ?, updated_at = ?
181
+ WHERE provider_id = ? AND account_key = ?`,
182
+ [
183
+ JSON.stringify(etagMap),
184
+ JSON.stringify(fnMap),
185
+ Date.now(),
186
+ providerId,
187
+ accountKey,
188
+ ],
189
+ );
190
+ }
191
+
192
+ function listTombstones(dbManager, providerId, accountKey = "", limit = 1000) {
193
+ return dbManager.all(
194
+ `SELECT * FROM sync_external_tombstones
195
+ WHERE provider_id = ? AND account_key = ?
196
+ AND resource_type = 'KNOWLEDGE_ITEM'
197
+ ORDER BY deleted_at ASC LIMIT ?`,
198
+ [providerId, accountKey, limit],
199
+ );
200
+ }
201
+
202
+ function deleteTombstone(dbManager, id) {
203
+ dbManager.run(`DELETE FROM sync_external_tombstones WHERE id = ?`, [id]);
204
+ }
205
+
206
+ function markTombstoneFailed(dbManager, id, errMsg) {
207
+ dbManager.run(
208
+ `UPDATE sync_external_tombstones
209
+ SET retry_count = retry_count + 1, last_error = ?
210
+ WHERE id = ?`,
211
+ [String(errMsg || "").slice(0, 500), id],
212
+ );
213
+ }
214
+
215
+ // ── walker: incremental batch fetching ─────────────────────────────
216
+
217
+ function fetchBatch(dbManager, cursor, batchSize = 200) {
218
+ const after = cursor?.lastSyncAt || 0;
219
+ const afterId = cursor?.lastItemId || "";
220
+ return dbManager.all(
221
+ `SELECT id, title, type, content, tags, created_at, updated_at
222
+ FROM knowledge_items
223
+ WHERE updated_at > ? OR (updated_at = ? AND id > ?)
224
+ ORDER BY updated_at ASC, id ASC
225
+ LIMIT ?`,
226
+ [after, after, afterId, batchSize],
227
+ );
228
+ }
229
+
230
+ function cursorAfterItem(item) {
231
+ return { lastSyncAt: item.updated_at, lastItemId: item.id };
232
+ }
233
+
234
+ function countPending(dbManager, cursor) {
235
+ const after = cursor?.lastSyncAt || 0;
236
+ const afterId = cursor?.lastItemId || "";
237
+ const row = dbManager.get(
238
+ `SELECT COUNT(*) AS c FROM knowledge_items
239
+ WHERE updated_at > ? OR (updated_at = ? AND id > ?)`,
240
+ [after, after, afterId],
241
+ );
242
+ return row?.c || 0;
243
+ }
244
+
245
+ // ── engine: orchestration ──────────────────────────────────────────
246
+
247
+ async function runSync(deps) {
248
+ const { dbManager, client, providerId, accountKey = "", onProgress } = deps;
249
+
250
+ const t0 = Date.now();
251
+ let pushed = 0;
252
+ let skipped = 0;
253
+ let deleted = 0;
254
+ let lastError = null;
255
+ let lastFlushPushed = 0;
256
+ let lastFlushDeleted = 0;
257
+ let lastFlushAt = Date.now();
258
+ let totalPending = 0;
259
+
260
+ function maybeFlush(phase) {
261
+ const now = Date.now();
262
+ const delta = pushed - lastFlushPushed + (deleted - lastFlushDeleted);
263
+ if (
264
+ delta >= PROGRESS_FLUSH_EVERY ||
265
+ now - lastFlushAt >= PROGRESS_FLUSH_MS
266
+ ) {
267
+ onProgress?.({ phase, pushed, skipped, deleted, totalPending });
268
+ lastFlushPushed = pushed;
269
+ lastFlushDeleted = deleted;
270
+ lastFlushAt = now;
271
+ }
272
+ }
273
+
274
+ function refresh() {
275
+ return getCursor(dbManager, providerId, accountKey) || {};
276
+ }
277
+
278
+ let cursor = ensureCursor(dbManager, providerId, accountKey);
279
+ totalPending =
280
+ countPending(dbManager, cursor) +
281
+ listTombstones(dbManager, providerId, accountKey, 1000).length;
282
+ onProgress?.({
283
+ phase: "start",
284
+ pushed: 0,
285
+ skipped: 0,
286
+ deleted: 0,
287
+ totalPending,
288
+ });
289
+
290
+ // Drain tombstones
291
+ const tombs = listTombstones(dbManager, providerId, accountKey, 1000);
292
+ for (const t of tombs) {
293
+ const fn = cursor.remoteFilenameMap?.[t.item_id];
294
+ if (!fn) {
295
+ deleteTombstone(dbManager, t.id);
296
+ continue;
297
+ }
298
+ const etag = cursor.remoteEtagMap?.[t.item_id] || null;
299
+ let res;
300
+ try {
301
+ res = await client.deleteFile(fn, etag);
302
+ } catch (err) {
303
+ markTombstoneFailed(dbManager, t.id, err?.message || String(err));
304
+ lastError = err?.message || String(err);
305
+ continue;
306
+ }
307
+ if (res.ok) {
308
+ deleteTombstone(dbManager, t.id);
309
+ removeFromMaps(dbManager, providerId, t.item_id, accountKey);
310
+ deleted++;
311
+ cursor = refresh();
312
+ maybeFlush("delete");
313
+ } else if (res.conflict) {
314
+ markTombstoneFailed(dbManager, t.id, "etag mismatch");
315
+ skipped++;
316
+ } else {
317
+ markTombstoneFailed(dbManager, t.id, res.error || "unknown");
318
+ lastError = res.error || lastError;
319
+ }
320
+ }
321
+
322
+ // Push loop
323
+ pushLoop: while (true) {
324
+ const batch = fetchBatch(dbManager, cursor, 200);
325
+ if (batch.length === 0) break;
326
+ for (const item of batch) {
327
+ const filename = generateFilename(item);
328
+ const content = generateMarkdown(item);
329
+ const etag = cursor.remoteEtagMap?.[item.id] || null;
330
+ let res;
331
+ try {
332
+ res = await client.putFile(filename, content, etag);
333
+ } catch (err) {
334
+ lastError = err?.message || String(err);
335
+ break pushLoop;
336
+ }
337
+ if (res.ok) {
338
+ recordPushedItem(
339
+ dbManager,
340
+ providerId,
341
+ item.id,
342
+ res.etag,
343
+ filename,
344
+ accountKey,
345
+ );
346
+ updateCursor(dbManager, providerId, cursorAfterItem(item), accountKey);
347
+ cursor = refresh();
348
+ pushed++;
349
+ maybeFlush("push");
350
+ } else if (res.conflict) {
351
+ skipped++;
352
+ updateCursor(dbManager, providerId, cursorAfterItem(item), accountKey);
353
+ cursor = refresh();
354
+ } else {
355
+ lastError = res.error || `status ${res.status}`;
356
+ break pushLoop;
357
+ }
358
+ }
359
+ }
360
+
361
+ const durationMs = Date.now() - t0;
362
+ const status = lastError ? "failed" : skipped > 0 ? "conflict" : "success";
363
+ updateCursor(
364
+ dbManager,
365
+ providerId,
366
+ {
367
+ lastRunStatus: status,
368
+ lastRunError: lastError,
369
+ lastRunDurationMs: durationMs,
370
+ itemsPushed: pushed,
371
+ itemsSkipped: skipped,
372
+ itemsDeleted: deleted,
373
+ },
374
+ accountKey,
375
+ );
376
+ onProgress?.({ phase: status, pushed, skipped, deleted, totalPending });
377
+
378
+ return {
379
+ success: !lastError,
380
+ status,
381
+ pushed,
382
+ skipped,
383
+ deleted,
384
+ durationMs,
385
+ error: lastError || undefined,
386
+ };
387
+ }
388
+
389
+ export {
390
+ runSync,
391
+ generateFilename,
392
+ generateMarkdown,
393
+ getCursor,
394
+ ensureCursor,
395
+ updateCursor,
396
+ recordPushedItem,
397
+ removeFromMaps,
398
+ listTombstones,
399
+ deleteTombstone,
400
+ markTombstoneFailed,
401
+ fetchBatch,
402
+ cursorAfterItem,
403
+ countPending,
404
+ PROGRESS_FLUSH_EVERY,
405
+ PROGRESS_FLUSH_MS,
406
+ };