@de-otio/chaoskb-client 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/dist/cli/agent-registry/config-merger.d.ts +28 -0
  2. package/dist/cli/agent-registry/config-merger.d.ts.map +1 -0
  3. package/dist/cli/agent-registry/config-merger.js +90 -0
  4. package/dist/cli/agent-registry/config-merger.js.map +1 -0
  5. package/dist/cli/agent-registry/detector.d.ts +7 -0
  6. package/dist/cli/agent-registry/detector.d.ts.map +1 -0
  7. package/dist/cli/agent-registry/detector.js +100 -0
  8. package/dist/cli/agent-registry/detector.js.map +1 -0
  9. package/dist/cli/agent-registry/index.d.ts +26 -0
  10. package/dist/cli/agent-registry/index.d.ts.map +1 -0
  11. package/dist/cli/agent-registry/index.js +77 -0
  12. package/dist/cli/agent-registry/index.js.map +1 -0
  13. package/dist/cli/agent-registry/path-validator.d.ts +11 -0
  14. package/dist/cli/agent-registry/path-validator.d.ts.map +1 -0
  15. package/dist/cli/agent-registry/path-validator.js +69 -0
  16. package/dist/cli/agent-registry/path-validator.js.map +1 -0
  17. package/dist/cli/agent-registry/registry.json +108 -0
  18. package/dist/cli/agent-registry/types.d.ts +29 -0
  19. package/dist/cli/agent-registry/types.d.ts.map +1 -0
  20. package/dist/cli/agent-registry/types.js +2 -0
  21. package/dist/cli/agent-registry/types.js.map +1 -0
  22. package/dist/cli/bootstrap-lock.d.ts +7 -0
  23. package/dist/cli/bootstrap-lock.d.ts.map +1 -0
  24. package/dist/cli/bootstrap-lock.js +62 -0
  25. package/dist/cli/bootstrap-lock.js.map +1 -0
  26. package/dist/cli/bootstrap.d.ts +23 -0
  27. package/dist/cli/bootstrap.d.ts.map +1 -0
  28. package/dist/cli/bootstrap.js +438 -0
  29. package/dist/cli/bootstrap.js.map +1 -0
  30. package/dist/cli/commands/config.d.ts +13 -0
  31. package/dist/cli/commands/config.d.ts.map +1 -0
  32. package/dist/cli/commands/config.js +244 -0
  33. package/dist/cli/commands/config.js.map +1 -0
  34. package/dist/cli/commands/devices.d.ts +21 -0
  35. package/dist/cli/commands/devices.d.ts.map +1 -0
  36. package/dist/cli/commands/devices.js +229 -0
  37. package/dist/cli/commands/devices.js.map +1 -0
  38. package/dist/cli/commands/export.d.ts +12 -0
  39. package/dist/cli/commands/export.d.ts.map +1 -0
  40. package/dist/cli/commands/export.js +183 -0
  41. package/dist/cli/commands/export.js.map +1 -0
  42. package/dist/cli/commands/import.d.ts +26 -0
  43. package/dist/cli/commands/import.d.ts.map +1 -0
  44. package/dist/cli/commands/import.js +311 -0
  45. package/dist/cli/commands/import.js.map +1 -0
  46. package/dist/cli/commands/kb.d.ts +39 -0
  47. package/dist/cli/commands/kb.d.ts.map +1 -0
  48. package/dist/cli/commands/kb.js +138 -0
  49. package/dist/cli/commands/kb.js.map +1 -0
  50. package/dist/cli/commands/project.d.ts +6 -0
  51. package/dist/cli/commands/project.d.ts.map +1 -0
  52. package/dist/cli/commands/project.js +115 -0
  53. package/dist/cli/commands/project.js.map +1 -0
  54. package/dist/cli/commands/projects.d.ts +33 -0
  55. package/dist/cli/commands/projects.d.ts.map +1 -0
  56. package/dist/cli/commands/projects.js +189 -0
  57. package/dist/cli/commands/projects.js.map +1 -0
  58. package/dist/cli/commands/register.d.ts +8 -0
  59. package/dist/cli/commands/register.d.ts.map +1 -0
  60. package/dist/cli/commands/register.js +146 -0
  61. package/dist/cli/commands/register.js.map +1 -0
  62. package/dist/cli/commands/rotate-key.d.ts +16 -0
  63. package/dist/cli/commands/rotate-key.d.ts.map +1 -0
  64. package/dist/cli/commands/rotate-key.js +197 -0
  65. package/dist/cli/commands/rotate-key.js.map +1 -0
  66. package/dist/cli/commands/setup-sync.d.ts +2 -0
  67. package/dist/cli/commands/setup-sync.d.ts.map +1 -0
  68. package/dist/cli/commands/setup-sync.js +165 -0
  69. package/dist/cli/commands/setup-sync.js.map +1 -0
  70. package/dist/cli/commands/setup.d.ts +12 -0
  71. package/dist/cli/commands/setup.d.ts.map +1 -0
  72. package/dist/cli/commands/setup.js +39 -0
  73. package/dist/cli/commands/setup.js.map +1 -0
  74. package/dist/cli/commands/status.d.ts +5 -0
  75. package/dist/cli/commands/status.d.ts.map +1 -0
  76. package/dist/cli/commands/status.js +96 -0
  77. package/dist/cli/commands/status.js.map +1 -0
  78. package/dist/cli/commands/uninstall.d.ts +4 -0
  79. package/dist/cli/commands/uninstall.d.ts.map +1 -0
  80. package/dist/cli/commands/uninstall.js +85 -0
  81. package/dist/cli/commands/uninstall.js.map +1 -0
  82. package/dist/cli/commands/unregister.d.ts +2 -0
  83. package/dist/cli/commands/unregister.d.ts.map +1 -0
  84. package/dist/cli/commands/unregister.js +46 -0
  85. package/dist/cli/commands/unregister.js.map +1 -0
  86. package/dist/cli/device-metadata.d.ts +15 -0
  87. package/dist/cli/device-metadata.d.ts.map +1 -0
  88. package/dist/cli/device-metadata.js +58 -0
  89. package/dist/cli/device-metadata.js.map +1 -0
  90. package/dist/cli/github.d.ts +38 -0
  91. package/dist/cli/github.d.ts.map +1 -0
  92. package/dist/cli/github.js +159 -0
  93. package/dist/cli/github.js.map +1 -0
  94. package/dist/cli/guide-hashes.json +13 -0
  95. package/dist/cli/index.d.ts +3 -0
  96. package/dist/cli/index.d.ts.map +1 -0
  97. package/dist/cli/index.js +226 -0
  98. package/dist/cli/index.js.map +1 -0
  99. package/dist/cli/mcp-server.d.ts +205 -0
  100. package/dist/cli/mcp-server.d.ts.map +1 -0
  101. package/dist/cli/mcp-server.js +366 -0
  102. package/dist/cli/mcp-server.js.map +1 -0
  103. package/dist/cli/tools/kb-delete.d.ts +10 -0
  104. package/dist/cli/tools/kb-delete.d.ts.map +1 -0
  105. package/dist/cli/tools/kb-delete.js +28 -0
  106. package/dist/cli/tools/kb-delete.js.map +1 -0
  107. package/dist/cli/tools/kb-ingest.d.ts +13 -0
  108. package/dist/cli/tools/kb-ingest.d.ts.map +1 -0
  109. package/dist/cli/tools/kb-ingest.js +72 -0
  110. package/dist/cli/tools/kb-ingest.js.map +1 -0
  111. package/dist/cli/tools/kb-list.d.ts +20 -0
  112. package/dist/cli/tools/kb-list.d.ts.map +1 -0
  113. package/dist/cli/tools/kb-list.js +24 -0
  114. package/dist/cli/tools/kb-list.js.map +1 -0
  115. package/dist/cli/tools/kb-query-shared.d.ts +27 -0
  116. package/dist/cli/tools/kb-query-shared.d.ts.map +1 -0
  117. package/dist/cli/tools/kb-query-shared.js +28 -0
  118. package/dist/cli/tools/kb-query-shared.js.map +1 -0
  119. package/dist/cli/tools/kb-query.d.ts +20 -0
  120. package/dist/cli/tools/kb-query.d.ts.map +1 -0
  121. package/dist/cli/tools/kb-query.js +109 -0
  122. package/dist/cli/tools/kb-query.js.map +1 -0
  123. package/dist/cli/tools/kb-summary.d.ts +29 -0
  124. package/dist/cli/tools/kb-summary.d.ts.map +1 -0
  125. package/dist/cli/tools/kb-summary.js +89 -0
  126. package/dist/cli/tools/kb-summary.js.map +1 -0
  127. package/dist/cli/tools/kb-sync-status.d.ts +7 -0
  128. package/dist/cli/tools/kb-sync-status.d.ts.map +1 -0
  129. package/dist/cli/tools/kb-sync-status.js +48 -0
  130. package/dist/cli/tools/kb-sync-status.js.map +1 -0
  131. package/dist/crypto/aad.d.ts +8 -0
  132. package/dist/crypto/aad.d.ts.map +1 -0
  133. package/dist/crypto/aad.js +11 -0
  134. package/dist/crypto/aad.js.map +1 -0
  135. package/dist/crypto/aead.d.ts +21 -0
  136. package/dist/crypto/aead.d.ts.map +1 -0
  137. package/dist/crypto/aead.js +43 -0
  138. package/dist/crypto/aead.js.map +1 -0
  139. package/dist/crypto/argon2.d.ts +11 -0
  140. package/dist/crypto/argon2.d.ts.map +1 -0
  141. package/dist/crypto/argon2.js +33 -0
  142. package/dist/crypto/argon2.js.map +1 -0
  143. package/dist/crypto/blob-id.d.ts +6 -0
  144. package/dist/crypto/blob-id.d.ts.map +1 -0
  145. package/dist/crypto/blob-id.js +33 -0
  146. package/dist/crypto/blob-id.js.map +1 -0
  147. package/dist/crypto/canonical-json.d.ts +6 -0
  148. package/dist/crypto/canonical-json.d.ts.map +1 -0
  149. package/dist/crypto/canonical-json.js +88 -0
  150. package/dist/crypto/canonical-json.js.map +1 -0
  151. package/dist/crypto/commitment.d.ts +12 -0
  152. package/dist/crypto/commitment.d.ts.map +1 -0
  153. package/dist/crypto/commitment.js +37 -0
  154. package/dist/crypto/commitment.js.map +1 -0
  155. package/dist/crypto/encryption-service.d.ts +19 -0
  156. package/dist/crypto/encryption-service.d.ts.map +1 -0
  157. package/dist/crypto/encryption-service.js +38 -0
  158. package/dist/crypto/encryption-service.js.map +1 -0
  159. package/dist/crypto/envelope-cbor.d.ts +37 -0
  160. package/dist/crypto/envelope-cbor.d.ts.map +1 -0
  161. package/dist/crypto/envelope-cbor.js +124 -0
  162. package/dist/crypto/envelope-cbor.js.map +1 -0
  163. package/dist/crypto/envelope.d.ts +34 -0
  164. package/dist/crypto/envelope.d.ts.map +1 -0
  165. package/dist/crypto/envelope.js +160 -0
  166. package/dist/crypto/envelope.js.map +1 -0
  167. package/dist/crypto/hkdf.d.ts +16 -0
  168. package/dist/crypto/hkdf.d.ts.map +1 -0
  169. package/dist/crypto/hkdf.js +33 -0
  170. package/dist/crypto/hkdf.js.map +1 -0
  171. package/dist/crypto/index.d.ts +15 -0
  172. package/dist/crypto/index.d.ts.map +1 -0
  173. package/dist/crypto/index.js +15 -0
  174. package/dist/crypto/index.js.map +1 -0
  175. package/dist/crypto/invite.d.ts +31 -0
  176. package/dist/crypto/invite.d.ts.map +1 -0
  177. package/dist/crypto/invite.js +137 -0
  178. package/dist/crypto/invite.js.map +1 -0
  179. package/dist/crypto/keyring.d.ts +37 -0
  180. package/dist/crypto/keyring.d.ts.map +1 -0
  181. package/dist/crypto/keyring.js +219 -0
  182. package/dist/crypto/keyring.js.map +1 -0
  183. package/dist/crypto/known-keys.d.ts +34 -0
  184. package/dist/crypto/known-keys.d.ts.map +1 -0
  185. package/dist/crypto/known-keys.js +106 -0
  186. package/dist/crypto/known-keys.js.map +1 -0
  187. package/dist/crypto/project-keys.d.ts +26 -0
  188. package/dist/crypto/project-keys.d.ts.map +1 -0
  189. package/dist/crypto/project-keys.js +69 -0
  190. package/dist/crypto/project-keys.js.map +1 -0
  191. package/dist/crypto/secure-buffer.d.ts +31 -0
  192. package/dist/crypto/secure-buffer.d.ts.map +1 -0
  193. package/dist/crypto/secure-buffer.js +61 -0
  194. package/dist/crypto/secure-buffer.js.map +1 -0
  195. package/dist/crypto/ssh-agent.d.ts +16 -0
  196. package/dist/crypto/ssh-agent.d.ts.map +1 -0
  197. package/dist/crypto/ssh-agent.js +225 -0
  198. package/dist/crypto/ssh-agent.js.map +1 -0
  199. package/dist/crypto/ssh-keys.d.ts +19 -0
  200. package/dist/crypto/ssh-keys.d.ts.map +1 -0
  201. package/dist/crypto/ssh-keys.js +121 -0
  202. package/dist/crypto/ssh-keys.js.map +1 -0
  203. package/dist/crypto/tiers/enhanced.d.ts +25 -0
  204. package/dist/crypto/tiers/enhanced.d.ts.map +1 -0
  205. package/dist/crypto/tiers/enhanced.js +56 -0
  206. package/dist/crypto/tiers/enhanced.js.map +1 -0
  207. package/dist/crypto/tiers/maximum.d.ts +19 -0
  208. package/dist/crypto/tiers/maximum.d.ts.map +1 -0
  209. package/dist/crypto/tiers/maximum.js +25 -0
  210. package/dist/crypto/tiers/maximum.js.map +1 -0
  211. package/dist/crypto/tiers/standard.d.ts +27 -0
  212. package/dist/crypto/tiers/standard.d.ts.map +1 -0
  213. package/dist/crypto/tiers/standard.js +147 -0
  214. package/dist/crypto/tiers/standard.js.map +1 -0
  215. package/dist/crypto/types.d.ts +169 -0
  216. package/dist/crypto/types.d.ts.map +1 -0
  217. package/dist/crypto/types.js +11 -0
  218. package/dist/crypto/types.js.map +1 -0
  219. package/dist/pipeline/chunker.d.ts +27 -0
  220. package/dist/pipeline/chunker.d.ts.map +1 -0
  221. package/dist/pipeline/chunker.js +96 -0
  222. package/dist/pipeline/chunker.js.map +1 -0
  223. package/dist/pipeline/content-pipeline.d.ts +24 -0
  224. package/dist/pipeline/content-pipeline.d.ts.map +1 -0
  225. package/dist/pipeline/content-pipeline.js +49 -0
  226. package/dist/pipeline/content-pipeline.js.map +1 -0
  227. package/dist/pipeline/embedder.d.ts +49 -0
  228. package/dist/pipeline/embedder.d.ts.map +1 -0
  229. package/dist/pipeline/embedder.js +195 -0
  230. package/dist/pipeline/embedder.js.map +1 -0
  231. package/dist/pipeline/extract.d.ts +17 -0
  232. package/dist/pipeline/extract.d.ts.map +1 -0
  233. package/dist/pipeline/extract.js +70 -0
  234. package/dist/pipeline/extract.js.map +1 -0
  235. package/dist/pipeline/fetch.d.ts +26 -0
  236. package/dist/pipeline/fetch.d.ts.map +1 -0
  237. package/dist/pipeline/fetch.js +91 -0
  238. package/dist/pipeline/fetch.js.map +1 -0
  239. package/dist/pipeline/index.d.ts +10 -0
  240. package/dist/pipeline/index.d.ts.map +1 -0
  241. package/dist/pipeline/index.js +10 -0
  242. package/dist/pipeline/index.js.map +1 -0
  243. package/dist/pipeline/model-manager.d.ts +57 -0
  244. package/dist/pipeline/model-manager.d.ts.map +1 -0
  245. package/dist/pipeline/model-manager.js +234 -0
  246. package/dist/pipeline/model-manager.js.map +1 -0
  247. package/dist/pipeline/search.d.ts +37 -0
  248. package/dist/pipeline/search.d.ts.map +1 -0
  249. package/dist/pipeline/search.js +65 -0
  250. package/dist/pipeline/search.js.map +1 -0
  251. package/dist/pipeline/tokenizer.d.ts +29 -0
  252. package/dist/pipeline/tokenizer.d.ts.map +1 -0
  253. package/dist/pipeline/tokenizer.js +54 -0
  254. package/dist/pipeline/tokenizer.js.map +1 -0
  255. package/dist/pipeline/types.d.ts +86 -0
  256. package/dist/pipeline/types.d.ts.map +1 -0
  257. package/dist/pipeline/types.js +2 -0
  258. package/dist/pipeline/types.js.map +1 -0
  259. package/dist/pipeline/wordpiece-tokenizer.d.ts +60 -0
  260. package/dist/pipeline/wordpiece-tokenizer.d.ts.map +1 -0
  261. package/dist/pipeline/wordpiece-tokenizer.js +251 -0
  262. package/dist/pipeline/wordpiece-tokenizer.js.map +1 -0
  263. package/dist/storage/chunk-repo.d.ts +29 -0
  264. package/dist/storage/chunk-repo.d.ts.map +1 -0
  265. package/dist/storage/chunk-repo.js +115 -0
  266. package/dist/storage/chunk-repo.js.map +1 -0
  267. package/dist/storage/database-manager.d.ts +17 -0
  268. package/dist/storage/database-manager.d.ts.map +1 -0
  269. package/dist/storage/database-manager.js +100 -0
  270. package/dist/storage/database-manager.js.map +1 -0
  271. package/dist/storage/database.d.ts +10 -0
  272. package/dist/storage/database.d.ts.map +1 -0
  273. package/dist/storage/database.js +34 -0
  274. package/dist/storage/database.js.map +1 -0
  275. package/dist/storage/embedding-index.d.ts +22 -0
  276. package/dist/storage/embedding-index.d.ts.map +1 -0
  277. package/dist/storage/embedding-index.js +78 -0
  278. package/dist/storage/embedding-index.js.map +1 -0
  279. package/dist/storage/index.d.ts +10 -0
  280. package/dist/storage/index.d.ts.map +1 -0
  281. package/dist/storage/index.js +10 -0
  282. package/dist/storage/index.js.map +1 -0
  283. package/dist/storage/kb-database.d.ts +11 -0
  284. package/dist/storage/kb-database.d.ts.map +1 -0
  285. package/dist/storage/kb-database.js +24 -0
  286. package/dist/storage/kb-database.js.map +1 -0
  287. package/dist/storage/schema.d.ts +6 -0
  288. package/dist/storage/schema.d.ts.map +1 -0
  289. package/dist/storage/schema.js +122 -0
  290. package/dist/storage/schema.js.map +1 -0
  291. package/dist/storage/source-repo.d.ts +20 -0
  292. package/dist/storage/source-repo.d.ts.map +1 -0
  293. package/dist/storage/source-repo.js +120 -0
  294. package/dist/storage/source-repo.js.map +1 -0
  295. package/dist/storage/sync-status-repo.d.ts +15 -0
  296. package/dist/storage/sync-status-repo.d.ts.map +1 -0
  297. package/dist/storage/sync-status-repo.js +40 -0
  298. package/dist/storage/sync-status-repo.js.map +1 -0
  299. package/dist/storage/types.d.ts +139 -0
  300. package/dist/storage/types.d.ts.map +1 -0
  301. package/dist/storage/types.js +9 -0
  302. package/dist/storage/types.js.map +1 -0
  303. package/dist/sync/canary.d.ts +14 -0
  304. package/dist/sync/canary.d.ts.map +1 -0
  305. package/dist/sync/canary.js +53 -0
  306. package/dist/sync/canary.js.map +1 -0
  307. package/dist/sync/full-sync.d.ts +16 -0
  308. package/dist/sync/full-sync.d.ts.map +1 -0
  309. package/dist/sync/full-sync.js +91 -0
  310. package/dist/sync/full-sync.js.map +1 -0
  311. package/dist/sync/http-client.d.ts +28 -0
  312. package/dist/sync/http-client.d.ts.map +1 -0
  313. package/dist/sync/http-client.js +90 -0
  314. package/dist/sync/http-client.js.map +1 -0
  315. package/dist/sync/incremental-sync.d.ts +17 -0
  316. package/dist/sync/incremental-sync.d.ts.map +1 -0
  317. package/dist/sync/incremental-sync.js +155 -0
  318. package/dist/sync/incremental-sync.js.map +1 -0
  319. package/dist/sync/index.d.ts +12 -0
  320. package/dist/sync/index.d.ts.map +1 -0
  321. package/dist/sync/index.js +12 -0
  322. package/dist/sync/index.js.map +1 -0
  323. package/dist/sync/quota.d.ts +17 -0
  324. package/dist/sync/quota.d.ts.map +1 -0
  325. package/dist/sync/quota.js +48 -0
  326. package/dist/sync/quota.js.map +1 -0
  327. package/dist/sync/sequence.d.ts +21 -0
  328. package/dist/sync/sequence.d.ts.map +1 -0
  329. package/dist/sync/sequence.js +49 -0
  330. package/dist/sync/sequence.js.map +1 -0
  331. package/dist/sync/ssh-signer.d.ts +59 -0
  332. package/dist/sync/ssh-signer.d.ts.map +1 -0
  333. package/dist/sync/ssh-signer.js +241 -0
  334. package/dist/sync/ssh-signer.js.map +1 -0
  335. package/dist/sync/sync-service.d.ts +48 -0
  336. package/dist/sync/sync-service.d.ts.map +1 -0
  337. package/dist/sync/sync-service.js +116 -0
  338. package/dist/sync/sync-service.js.map +1 -0
  339. package/dist/sync/types.d.ts +106 -0
  340. package/dist/sync/types.d.ts.map +1 -0
  341. package/dist/sync/types.js +2 -0
  342. package/dist/sync/types.js.map +1 -0
  343. package/dist/sync/upload-queue.d.ts +40 -0
  344. package/dist/sync/upload-queue.d.ts.map +1 -0
  345. package/dist/sync/upload-queue.js +148 -0
  346. package/dist/sync/upload-queue.js.map +1 -0
  347. package/dist/sync/verification.d.ts +17 -0
  348. package/dist/sync/verification.d.ts.map +1 -0
  349. package/dist/sync/verification.js +25 -0
  350. package/dist/sync/verification.js.map +1 -0
  351. package/dist/vitest.config.d.ts +3 -0
  352. package/dist/vitest.config.d.ts.map +1 -0
  353. package/dist/vitest.config.js +16 -0
  354. package/dist/vitest.config.js.map +1 -0
  355. package/package.json +68 -0
@@ -0,0 +1,155 @@
1
+ import { SyncStatus } from '../storage/types.js';
2
+ /**
3
+ * Download changes from the server since the last sync timestamp.
4
+ *
5
+ * On first sync (no lastSyncTimestamp), downloads metadata for all blobs.
6
+ * For each new/updated blob, downloads the content and stores it locally.
7
+ * For each tombstone, soft-deletes the local record if it exists.
8
+ *
9
+ * Conflict resolution strategy:
10
+ * - New remote blobs (not present locally): accept as-is
11
+ * - Remote blob updated, local is synced: accept remote update
12
+ * - Remote blob updated, local has unsynchronized changes: last-write-wins
13
+ * - Remote tombstone, local has unsynchronized changes: keep local (local_wins)
14
+ */
15
+ export async function incrementalSync(client, storage, lastSyncTimestamp) {
16
+ const errors = [];
17
+ const conflicts = [];
18
+ let newBlobs = 0;
19
+ let updatedBlobs = 0;
20
+ let deletedBlobs = 0;
21
+ // Fetch blob list from server
22
+ const listPath = lastSyncTimestamp
23
+ ? `/v1/blobs?since=${encodeURIComponent(lastSyncTimestamp)}`
24
+ : '/v1/blobs';
25
+ const listResponse = await client.get(listPath);
26
+ if (!listResponse.ok) {
27
+ return {
28
+ newBlobs: 0,
29
+ updatedBlobs: 0,
30
+ deletedBlobs: 0,
31
+ conflicts: [],
32
+ errors: [
33
+ {
34
+ message: `Failed to list blobs: HTTP ${listResponse.status}`,
35
+ code: 'LIST_FAILED',
36
+ retryable: listResponse.status >= 500,
37
+ },
38
+ ],
39
+ success: false,
40
+ };
41
+ }
42
+ const data = (await listResponse.json());
43
+ // Download each new/updated blob
44
+ for (const blobMeta of data.blobs) {
45
+ try {
46
+ // Check for conflict: blob exists locally with unsynchronized changes
47
+ const existing = storage.syncStatus.get(blobMeta.id);
48
+ if (existing && existing.status === SyncStatus.LocalOnly) {
49
+ // Conflict: local has unsynchronized changes, remote also has changes.
50
+ // Resolve with last-write-wins based on timestamp.
51
+ const localTimestamp = existing.lastAttempt ?? '';
52
+ const remoteTimestamp = blobMeta.ts;
53
+ if (localTimestamp > remoteTimestamp) {
54
+ // Local is newer — keep local version, skip remote
55
+ conflicts.push({
56
+ blobId: blobMeta.id,
57
+ resolution: 'local_wins',
58
+ reason: 'Local changes are newer than remote',
59
+ localTimestamp,
60
+ remoteTimestamp,
61
+ });
62
+ continue;
63
+ }
64
+ else {
65
+ // Remote is newer — accept remote, overwrite local
66
+ conflicts.push({
67
+ blobId: blobMeta.id,
68
+ resolution: 'remote_wins',
69
+ reason: 'Remote changes are newer than local',
70
+ localTimestamp,
71
+ remoteTimestamp,
72
+ });
73
+ }
74
+ }
75
+ if (existing && existing.status === SyncStatus.SyncFailed) {
76
+ // Previously failed sync — try again with remote version
77
+ conflicts.push({
78
+ blobId: blobMeta.id,
79
+ resolution: 'remote_wins',
80
+ reason: 'Local sync had failed, accepting remote version',
81
+ });
82
+ }
83
+ const blobResponse = await client.get(`/v1/blobs/${blobMeta.id}`);
84
+ if (!blobResponse.ok) {
85
+ errors.push({
86
+ blobId: blobMeta.id,
87
+ message: `Failed to download blob: HTTP ${blobResponse.status}`,
88
+ code: 'DOWNLOAD_FAILED',
89
+ retryable: blobResponse.status >= 500,
90
+ });
91
+ continue;
92
+ }
93
+ const blobData = new Uint8Array(await blobResponse.arrayBuffer());
94
+ if (existing) {
95
+ updatedBlobs++;
96
+ }
97
+ else {
98
+ newBlobs++;
99
+ }
100
+ // Store blob data — the caller is responsible for decryption.
101
+ // We store the raw encrypted envelope bytes as a source record.
102
+ // For now, update sync status to mark it as synced.
103
+ storage.syncStatus.set(blobMeta.id, SyncStatus.Synced);
104
+ // Store the blob data in the chunk repository as raw bytes.
105
+ // The actual deserialization into sources/chunks happens at a higher layer.
106
+ // Here we just track sync status.
107
+ void blobData; // Consumed by higher-layer processing
108
+ }
109
+ catch (error) {
110
+ errors.push({
111
+ blobId: blobMeta.id,
112
+ message: error instanceof Error ? error.message : String(error),
113
+ code: 'DOWNLOAD_ERROR',
114
+ retryable: true,
115
+ });
116
+ }
117
+ }
118
+ // Process tombstones
119
+ for (const tombstone of data.tombstones) {
120
+ try {
121
+ const existing = storage.syncStatus.get(tombstone.id);
122
+ if (existing) {
123
+ if (existing.status === SyncStatus.LocalOnly) {
124
+ // Conflict: remote deleted, but local has unsynchronized changes.
125
+ // Keep local version — user's local edits take priority over remote deletion.
126
+ conflicts.push({
127
+ blobId: tombstone.id,
128
+ resolution: 'local_wins',
129
+ reason: 'Remote deleted but local has unsynchronized changes',
130
+ });
131
+ continue;
132
+ }
133
+ storage.syncStatus.set(tombstone.id, SyncStatus.PendingDelete);
134
+ deletedBlobs++;
135
+ }
136
+ }
137
+ catch (error) {
138
+ errors.push({
139
+ blobId: tombstone.id,
140
+ message: error instanceof Error ? error.message : String(error),
141
+ code: 'TOMBSTONE_ERROR',
142
+ retryable: false,
143
+ });
144
+ }
145
+ }
146
+ return {
147
+ newBlobs,
148
+ updatedBlobs,
149
+ deletedBlobs,
150
+ conflicts,
151
+ errors,
152
+ success: errors.length === 0,
153
+ };
154
+ }
155
+ //# sourceMappingURL=incremental-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incremental-sync.js","sourceRoot":"","sources":["../../sync/incremental-sync.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAuB,EACvB,OAAkB,EAClB,iBAA0B;IAE1B,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,MAAM,SAAS,GAAmB,EAAE,CAAC;IACrC,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,8BAA8B;IAC9B,MAAM,QAAQ,GAAG,iBAAiB;QAChC,CAAC,CAAC,mBAAmB,kBAAkB,CAAC,iBAAiB,CAAC,EAAE;QAC5D,CAAC,CAAC,WAAW,CAAC;IAEhB,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;QACrB,OAAO;YACL,QAAQ,EAAE,CAAC;YACX,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,CAAC;YACf,SAAS,EAAE,EAAE;YACb,MAAM,EAAE;gBACN;oBACE,OAAO,EAAE,8BAA8B,YAAY,CAAC,MAAM,EAAE;oBAC5D,IAAI,EAAE,aAAa;oBACnB,SAAS,EAAE,YAAY,CAAC,MAAM,IAAI,GAAG;iBACtC;aACF;YACD,OAAO,EAAE,KAAK;SACf,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,YAAY,CAAC,IAAI,EAAE,CAAqB,CAAC;IAE7D,iCAAiC;IACjC,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAClC,IAAI,CAAC;YACH,sEAAsE;YACtE,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YAErD,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,CAAC,SAAS,EAAE,CAAC;gBACzD,uEAAuE;gBACvE,mDAAmD;gBACnD,MAAM,cAAc,GAAG,QAAQ,CAAC,WAAW,IAAI,EAAE,CAAC;gBAClD,MAAM,eAAe,GAAG,QAAQ,CAAC,EAAE,CAAC;gBAEpC,IAAI,cAAc,GAAG,eAAe,EAAE,CAAC;oBACrC,mDAAmD;oBACnD,SAAS,CAAC,IAAI,CAAC;wBACb,MAAM,EAAE,QAAQ,CAAC,EAAE;wBACnB,UAAU,EAAE,YAAY;wBACxB,MAAM,EAAE,qCAAqC;wBAC7C,cAAc;wBACd,eAAe;qBAChB,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;qBAAM,CAAC;oBACN,mDAAmD;oBACnD,SAAS,CAAC,IAAI,CAAC;wBACb,MAAM,EAAE,QAAQ,CAAC,EAAE;wBACnB,UAAU,EAAE,aAAa;wBACzB,MAAM,EAAE,qCAAqC;wBAC7C,cAAc;wBACd,eAAe;qBAChB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAED,IAAI,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,CAAC,UAAU,EAAE,CAAC;gBAC1D,yDAAyD;gBACzD,SAAS,CAAC,IAAI,CAAC;oBACb,MAAM,EAAE,QAAQ,CAAC,EAAE;oBACnB,UAAU,EAAE,aAAa;oBACzB,MAAM,EAAE,iDAAiD;iBAC1D,CAAC,CAAC;YACL,CAAC;YAED,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,aAAa,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC;YAClE,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;gBACrB,MAAM,CAAC,IAAI,CAAC;oBACV,MAAM,EAAE,QAAQ,CAAC,EAAE;oBACnB,OAAO,EAAE,iCAAiC,YAAY,CAAC,MAAM,EAAE;oBAC/D,IAAI,EAAE,iBAAiB;oBACvB,SAAS,EAAE,YAAY,CAAC,MAAM,IAAI,GAAG;iBACtC,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,MAAM,QAAQ,GAAG,IAAI,UAAU,CAAC,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC,CAAC;YAElE,IAAI,QAAQ,EAAE,CAAC;gBACb,YAAY,EAAE,CAAC;YACjB,CAAC;iBAAM,CAAC;gBACN,QAAQ,EAAE,CAAC;YACb,CAAC;YAED,8DAA8D;YAC9D,gEAAgE;YAChE,oDAAoD;YACpD,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;YAEvD,4DAA4D;YAC5D,4EAA4E;YAC5E,kCAAkC;YAClC,KAAK,QAAQ,CAAC,CAAC,sCAAsC;QACvD,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,QAAQ,CAAC,EAAE;gBACnB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC/D,IAAI,EAAE,gBAAgB;gBACtB,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,qBAAqB;IACrB,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACtD,IAAI,QAAQ,EAAE,CAAC;gBACb,IAAI,QAAQ,CAAC,MAAM,KAAK,UAAU,CAAC,SAAS,EAAE,CAAC;oBAC7C,kEAAkE;oBAClE,8EAA8E;oBAC9E,SAAS,CAAC,IAAI,CAAC;wBACb,MAAM,EAAE,SAAS,CAAC,EAAE;wBACpB,UAAU,EAAE,YAAY;wBACxB,MAAM,EAAE,qDAAqD;qBAC9D,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBAED,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC;gBAC/D,YAAY,EAAE,CAAC;YACjB,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC;gBACV,MAAM,EAAE,SAAS,CAAC,EAAE;gBACpB,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;gBAC/D,IAAI,EAAE,iBAAiB;gBACvB,SAAS,EAAE,KAAK;aACjB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,YAAY;QACZ,SAAS;QACT,MAAM;QACN,OAAO,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;KAC7B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ export * from './types.js';
2
+ export { SSHSigner } from './ssh-signer.js';
3
+ export { SequenceCounter } from './sequence.js';
4
+ export { SyncHttpClient, RetryableError } from './http-client.js';
5
+ export { incrementalSync } from './incremental-sync.js';
6
+ export { fullSync } from './full-sync.js';
7
+ export { UploadQueue } from './upload-queue.js';
8
+ export { verifyCanary } from './canary.js';
9
+ export { verifyBlobCount } from './verification.js';
10
+ export { parseQuotaError, getQuotaWarning } from './quota.js';
11
+ export { SyncService } from './sync-service.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,12 @@
1
+ export * from './types.js';
2
+ export { SSHSigner } from './ssh-signer.js';
3
+ export { SequenceCounter } from './sequence.js';
4
+ export { SyncHttpClient, RetryableError } from './http-client.js';
5
+ export { incrementalSync } from './incremental-sync.js';
6
+ export { fullSync } from './full-sync.js';
7
+ export { UploadQueue } from './upload-queue.js';
8
+ export { verifyCanary } from './canary.js';
9
+ export { verifyBlobCount } from './verification.js';
10
+ export { parseQuotaError, getQuotaWarning } from './quota.js';
11
+ export { SyncService } from './sync-service.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../sync/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { QuotaInfo } from './types.js';
2
+ /**
3
+ * Parse a 413 (Payload Too Large / Quota Exceeded) response into QuotaInfo.
4
+ *
5
+ * Expects the response body to contain `{ used: number, limit: number }`
6
+ * where both values are in bytes.
7
+ *
8
+ * @returns QuotaInfo if the response could be parsed, null otherwise.
9
+ */
10
+ export declare function parseQuotaError(response: Response): Promise<QuotaInfo | null>;
11
+ /**
12
+ * Get a user-facing warning string based on quota usage.
13
+ *
14
+ * @returns A warning message, or null if usage is below the warning threshold.
15
+ */
16
+ export declare function getQuotaWarning(quota: QuotaInfo): string | null;
17
+ //# sourceMappingURL=quota.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.d.ts","sourceRoot":"","sources":["../../sync/quota.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAmBnF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAc/D"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Parse a 413 (Payload Too Large / Quota Exceeded) response into QuotaInfo.
3
+ *
4
+ * Expects the response body to contain `{ used: number, limit: number }`
5
+ * where both values are in bytes.
6
+ *
7
+ * @returns QuotaInfo if the response could be parsed, null otherwise.
8
+ */
9
+ export async function parseQuotaError(response) {
10
+ if (response.status !== 413) {
11
+ return null;
12
+ }
13
+ try {
14
+ const data = (await response.json());
15
+ if (typeof data.used !== 'number' || typeof data.limit !== 'number') {
16
+ return null;
17
+ }
18
+ const percentage = data.limit > 0 ? Math.round((data.used / data.limit) * 100) : 100;
19
+ return {
20
+ used: data.used,
21
+ limit: data.limit,
22
+ percentage,
23
+ };
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ /**
30
+ * Get a user-facing warning string based on quota usage.
31
+ *
32
+ * @returns A warning message, or null if usage is below the warning threshold.
33
+ */
34
+ export function getQuotaWarning(quota) {
35
+ const usedMB = Math.round(quota.used / (1024 * 1024));
36
+ const limitMB = Math.round(quota.limit / (1024 * 1024));
37
+ if (quota.percentage >= 100) {
38
+ return 'Storage limit reached. Articles are stored locally only and not synced.';
39
+ }
40
+ if (quota.percentage >= 95) {
41
+ return `Storage is nearly full (${usedMB}MB / ${limitMB}MB). New articles will be stored locally only.`;
42
+ }
43
+ if (quota.percentage >= 80) {
44
+ return `Storage is 80% full (${usedMB}MB / ${limitMB}MB)`;
45
+ }
46
+ return null;
47
+ }
48
+ //# sourceMappingURL=quota.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quota.js","sourceRoot":"","sources":["../../sync/quota.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAkB;IACtD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAsC,CAAC;QAC1E,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACpE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QACrF,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU;SACX,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAgB;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC;IAExD,IAAI,KAAK,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;QAC5B,OAAO,yEAAyE,CAAC;IACnF,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QAC3B,OAAO,2BAA2B,MAAM,QAAQ,OAAO,gDAAgD,CAAC;IAC1G,CAAC;IACD,IAAI,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;QAC3B,OAAO,wBAAwB,MAAM,QAAQ,OAAO,KAAK,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Per-device monotonic sequence counter for replay protection.
3
+ *
4
+ * Each signed request includes a sequence number that the server tracks.
5
+ * The server rejects any request with a sequence <= the highest it has seen,
6
+ * preventing replay attacks.
7
+ *
8
+ * The counter is persisted to disk so it survives process restarts.
9
+ */
10
+ export declare class SequenceCounter {
11
+ private readonly filePath;
12
+ private current;
13
+ constructor(filePath?: string);
14
+ /** Get the next sequence number (monotonically increasing). */
15
+ next(): number;
16
+ /** Get the current sequence number without incrementing. */
17
+ peek(): number;
18
+ private load;
19
+ private persist;
20
+ }
21
+ //# sourceMappingURL=sequence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence.d.ts","sourceRoot":"","sources":["../../sync/sequence.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAS;gBAEZ,QAAQ,CAAC,EAAE,MAAM;IAK7B,+DAA+D;IAC/D,IAAI,IAAI,MAAM;IAMd,4DAA4D;IAC5D,IAAI,IAAI,MAAM;IAId,OAAO,CAAC,IAAI;IAUZ,OAAO,CAAC,OAAO;CAShB"}
@@ -0,0 +1,49 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, dirname } from 'node:path';
4
+ /**
5
+ * Per-device monotonic sequence counter for replay protection.
6
+ *
7
+ * Each signed request includes a sequence number that the server tracks.
8
+ * The server rejects any request with a sequence <= the highest it has seen,
9
+ * preventing replay attacks.
10
+ *
11
+ * The counter is persisted to disk so it survives process restarts.
12
+ */
13
+ export class SequenceCounter {
14
+ filePath;
15
+ current;
16
+ constructor(filePath) {
17
+ this.filePath = filePath ?? join(homedir(), '.chaoskb', 'sequence');
18
+ this.current = this.load();
19
+ }
20
+ /** Get the next sequence number (monotonically increasing). */
21
+ next() {
22
+ this.current += 1;
23
+ this.persist();
24
+ return this.current;
25
+ }
26
+ /** Get the current sequence number without incrementing. */
27
+ peek() {
28
+ return this.current;
29
+ }
30
+ load() {
31
+ try {
32
+ const content = readFileSync(this.filePath, 'utf-8').trim();
33
+ const value = parseInt(content, 10);
34
+ return isNaN(value) ? 0 : value;
35
+ }
36
+ catch {
37
+ return 0;
38
+ }
39
+ }
40
+ persist() {
41
+ const dir = dirname(this.filePath);
42
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
43
+ // Atomic write: write to temp file, then rename
44
+ const tmpPath = this.filePath + '.tmp';
45
+ writeFileSync(tmpPath, String(this.current), { mode: 0o600 });
46
+ renameSync(tmpPath, this.filePath);
47
+ }
48
+ }
49
+ //# sourceMappingURL=sequence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence.js","sourceRoot":"","sources":["../../sync/sequence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAe;IACT,QAAQ,CAAS;IAC1B,OAAO,CAAS;IAExB,YAAY,QAAiB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;QACpE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC7B,CAAC;IAED,+DAA+D;IAC/D,IAAI;QACF,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC;QAClB,IAAI,CAAC,OAAO,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,4DAA4D;IAC5D,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAEO,IAAI;QACV,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5D,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACpC,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC;QACX,CAAC;IACH,CAAC;IAEO,OAAO;QACb,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAEjD,gDAAgD;QAChD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC;QACvC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9D,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;CACF"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Signs HTTP requests with an SSH private key for ChaosKB-SSH authentication.
3
+ *
4
+ * Uses Ed25519 keys. Attempts ssh-agent first if SSH_AUTH_SOCK is available,
5
+ * then falls back to reading the private key file from disk.
6
+ */
7
+ export declare class SSHSigner {
8
+ private readonly keyPath;
9
+ constructor(sshKeyPath?: string);
10
+ /**
11
+ * Sign an HTTP request, returning headers for authentication.
12
+ */
13
+ signRequest(method: string, path: string, sequence: number, body?: Uint8Array): Promise<{
14
+ authorization: string;
15
+ timestamp: string;
16
+ sequence: number;
17
+ publicKey: string;
18
+ }>;
19
+ /**
20
+ * Compute SHA-256 hex digest of body bytes. Empty string if no body.
21
+ */
22
+ computeBodyHash(body?: Uint8Array): string;
23
+ /**
24
+ * Build the canonical string to be signed.
25
+ *
26
+ * Format: chaoskb-auth\nMETHOD PATH\nTIMESTAMP\nSEQUENCE\nBODY_HASH
27
+ * The sequence number prevents replay attacks — the server rejects
28
+ * any sequence <= the highest it has seen for this device.
29
+ */
30
+ buildCanonical(method: string, path: string, timestamp: string, sequence: number, bodyHash: string): string;
31
+ /**
32
+ * Read the SSH public key from the .pub file alongside the private key.
33
+ * Returns the raw public key content as a UTF-8 string.
34
+ */
35
+ private readPublicKey;
36
+ /**
37
+ * Sign canonical data using the Ed25519 private key.
38
+ *
39
+ * Attempts ssh-agent first if SSH_AUTH_SOCK is set, falling back to
40
+ * reading the key file from disk.
41
+ */
42
+ private signCanonical;
43
+ /**
44
+ * Sign using the SSH private key file on disk with Ed25519.
45
+ */
46
+ private signWithKeyFile;
47
+ /**
48
+ * Sign using ssh-agent via SSH_AUTH_SOCK.
49
+ *
50
+ * Implements the SSH agent protocol (draft-miller-ssh-agent):
51
+ * 1. Connect to the Unix domain socket at SSH_AUTH_SOCK
52
+ * 2. Send SSH_AGENTC_REQUEST_IDENTITIES to list available keys
53
+ * 3. Find an Ed25519 key matching our public key
54
+ * 4. Send SSH_AGENTC_SIGN_REQUEST with the data to sign
55
+ * 5. Parse the SSH_AGENT_SIGN_RESPONSE to extract the signature
56
+ */
57
+ private signWithAgent;
58
+ }
59
+ //# sourceMappingURL=ssh-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh-signer.d.ts","sourceRoot":"","sources":["../../sync/ssh-signer.ts"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,UAAU,CAAC,EAAE,MAAM;IAI/B;;OAEG;IACG,WAAW,CACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC;QACT,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAiBF;;OAEG;IACH,eAAe,CAAC,IAAI,CAAC,EAAE,UAAU,GAAG,MAAM;IAO1C;;;;;;OAMG;IACH,cAAc,CACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM;IAIT;;;OAGG;YACW,aAAa;IAM3B;;;;;OAKG;YACW,aAAa;IAa3B;;OAEG;YACW,eAAe;IAS7B;;;;;;;;;OASG;YACW,aAAa;CAyB5B"}
@@ -0,0 +1,241 @@
1
+ import { createHash, sign as cryptoSign } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { connect } from 'node:net';
6
+ /**
7
+ * Signs HTTP requests with an SSH private key for ChaosKB-SSH authentication.
8
+ *
9
+ * Uses Ed25519 keys. Attempts ssh-agent first if SSH_AUTH_SOCK is available,
10
+ * then falls back to reading the private key file from disk.
11
+ */
12
+ export class SSHSigner {
13
+ keyPath;
14
+ constructor(sshKeyPath) {
15
+ this.keyPath = sshKeyPath ?? join(homedir(), '.ssh', 'id_ed25519');
16
+ }
17
+ /**
18
+ * Sign an HTTP request, returning headers for authentication.
19
+ */
20
+ async signRequest(method, path, sequence, body) {
21
+ const timestamp = new Date().toISOString();
22
+ const bodyHash = this.computeBodyHash(body);
23
+ const canonical = this.buildCanonical(method, path, timestamp, sequence, bodyHash);
24
+ const publicKeyRaw = await this.readPublicKey();
25
+ const signature = await this.signCanonical(canonical);
26
+ const base64Sig = signature.toString('base64');
27
+ // Extract the base64 key blob from the public key line (ssh-ed25519 AAAA... comment)
28
+ const parts = publicKeyRaw.split(/\s+/);
29
+ const publicKey = parts.length >= 2 ? parts[1] : publicKeyRaw;
30
+ const authorization = `SSH-Signature ${base64Sig}`;
31
+ return { authorization, timestamp, sequence, publicKey };
32
+ }
33
+ /**
34
+ * Compute SHA-256 hex digest of body bytes. Empty string if no body.
35
+ */
36
+ computeBodyHash(body) {
37
+ if (!body || body.length === 0) {
38
+ return '';
39
+ }
40
+ return createHash('sha256').update(body).digest('hex');
41
+ }
42
+ /**
43
+ * Build the canonical string to be signed.
44
+ *
45
+ * Format: chaoskb-auth\nMETHOD PATH\nTIMESTAMP\nSEQUENCE\nBODY_HASH
46
+ * The sequence number prevents replay attacks — the server rejects
47
+ * any sequence <= the highest it has seen for this device.
48
+ */
49
+ buildCanonical(method, path, timestamp, sequence, bodyHash) {
50
+ return `chaoskb-auth\n${method} ${path}\n${timestamp}\n${sequence}\n${bodyHash}`;
51
+ }
52
+ /**
53
+ * Read the SSH public key from the .pub file alongside the private key.
54
+ * Returns the raw public key content as a UTF-8 string.
55
+ */
56
+ async readPublicKey() {
57
+ const pubKeyPath = this.keyPath + '.pub';
58
+ const content = await readFile(pubKeyPath, 'utf-8');
59
+ return content.trim();
60
+ }
61
+ /**
62
+ * Sign canonical data using the Ed25519 private key.
63
+ *
64
+ * Attempts ssh-agent first if SSH_AUTH_SOCK is set, falling back to
65
+ * reading the key file from disk.
66
+ */
67
+ async signCanonical(canonical) {
68
+ // Attempt ssh-agent if SSH_AUTH_SOCK is set
69
+ if (process.env.SSH_AUTH_SOCK) {
70
+ try {
71
+ return await this.signWithAgent(canonical);
72
+ }
73
+ catch {
74
+ // Fall through to file-based signing
75
+ }
76
+ }
77
+ return this.signWithKeyFile(canonical);
78
+ }
79
+ /**
80
+ * Sign using the SSH private key file on disk with Ed25519.
81
+ */
82
+ async signWithKeyFile(canonical) {
83
+ const keyData = await readFile(this.keyPath, 'utf-8');
84
+ const data = Buffer.from(canonical, 'utf-8');
85
+ return cryptoSign(undefined, data, {
86
+ key: keyData,
87
+ format: 'pem',
88
+ });
89
+ }
90
+ /**
91
+ * Sign using ssh-agent via SSH_AUTH_SOCK.
92
+ *
93
+ * Implements the SSH agent protocol (draft-miller-ssh-agent):
94
+ * 1. Connect to the Unix domain socket at SSH_AUTH_SOCK
95
+ * 2. Send SSH_AGENTC_REQUEST_IDENTITIES to list available keys
96
+ * 3. Find an Ed25519 key matching our public key
97
+ * 4. Send SSH_AGENTC_SIGN_REQUEST with the data to sign
98
+ * 5. Parse the SSH_AGENT_SIGN_RESPONSE to extract the signature
99
+ */
100
+ async signWithAgent(canonical) {
101
+ const socketPath = process.env.SSH_AUTH_SOCK;
102
+ if (!socketPath) {
103
+ throw new Error('SSH_AUTH_SOCK not set');
104
+ }
105
+ const pubKeyContent = await this.readPublicKey();
106
+ const pubKeyBlob = parseSSHPublicKey(pubKeyContent);
107
+ const socket = await connectToAgent(socketPath);
108
+ try {
109
+ // Request the agent sign our data
110
+ const data = Buffer.from(canonical, 'utf-8');
111
+ const signatureBlob = await agentSign(socket, pubKeyBlob, data);
112
+ // The signature blob from the agent is in SSH wire format:
113
+ // string signature-format (e.g., "ssh-ed25519")
114
+ // string signature-blob
115
+ // We need the raw signature bytes for our authorization header.
116
+ const sigData = parseSSHSignature(signatureBlob);
117
+ return sigData;
118
+ }
119
+ finally {
120
+ socket.destroy();
121
+ }
122
+ }
123
+ }
124
+ // --- SSH Agent Protocol Constants ---
125
+ /** SSH agent message types */
126
+ const SSH_AGENTC_SIGN_REQUEST = 13;
127
+ const SSH_AGENT_SIGN_RESPONSE = 14;
128
+ const SSH_AGENT_FAILURE = 5;
129
+ // --- SSH Agent Protocol Helpers ---
130
+ /**
131
+ * Connect to the ssh-agent Unix domain socket.
132
+ */
133
+ function connectToAgent(socketPath) {
134
+ return new Promise((resolve, reject) => {
135
+ const socket = connect(socketPath, () => resolve(socket));
136
+ socket.on('error', reject);
137
+ socket.setTimeout(5000);
138
+ socket.on('timeout', () => {
139
+ socket.destroy();
140
+ reject(new Error('ssh-agent connection timed out'));
141
+ });
142
+ });
143
+ }
144
+ /**
145
+ * Send an SSH_AGENTC_SIGN_REQUEST and read the response.
146
+ *
147
+ * Wire format:
148
+ * uint32 length
149
+ * byte SSH_AGENTC_SIGN_REQUEST (13)
150
+ * string key_blob
151
+ * string data
152
+ * uint32 flags (0 for default)
153
+ */
154
+ function agentSign(socket, keyBlob, data) {
155
+ return new Promise((resolve, reject) => {
156
+ // Build the message body
157
+ const bodyParts = [
158
+ Buffer.from([SSH_AGENTC_SIGN_REQUEST]),
159
+ sshString(keyBlob),
160
+ sshString(data),
161
+ uint32(0), // flags
162
+ ];
163
+ const body = Buffer.concat(bodyParts);
164
+ // Prepend length header
165
+ const message = Buffer.concat([uint32(body.length), body]);
166
+ socket.write(message);
167
+ // Read the response
168
+ const chunks = [];
169
+ socket.on('data', (chunk) => {
170
+ chunks.push(chunk);
171
+ // Check if we have enough data
172
+ const response = Buffer.concat(chunks);
173
+ if (response.length < 4)
174
+ return; // Need length header
175
+ const responseLen = response.readUInt32BE(0);
176
+ if (response.length < 4 + responseLen)
177
+ return; // Need full message
178
+ const msgType = response[4];
179
+ if (msgType === SSH_AGENT_FAILURE) {
180
+ reject(new Error('ssh-agent refused the signing request (key may not be loaded)'));
181
+ return;
182
+ }
183
+ if (msgType !== SSH_AGENT_SIGN_RESPONSE) {
184
+ reject(new Error(`Unexpected ssh-agent response type: ${msgType}`));
185
+ return;
186
+ }
187
+ // Parse: byte SSH_AGENT_SIGN_RESPONSE, string signature
188
+ const sigOffset = 5; // 4 (length) + 1 (type)
189
+ const sigLen = response.readUInt32BE(sigOffset);
190
+ const signatureBlob = response.subarray(sigOffset + 4, sigOffset + 4 + sigLen);
191
+ resolve(Buffer.from(signatureBlob));
192
+ });
193
+ socket.on('error', reject);
194
+ socket.on('close', () => {
195
+ reject(new Error('ssh-agent connection closed unexpectedly'));
196
+ });
197
+ });
198
+ }
199
+ /**
200
+ * Parse an SSH public key line (e.g., "ssh-ed25519 AAAA... comment")
201
+ * into the raw key blob (base64-decoded middle field).
202
+ */
203
+ function parseSSHPublicKey(pubKeyLine) {
204
+ const parts = pubKeyLine.trim().split(/\s+/);
205
+ if (parts.length < 2) {
206
+ throw new Error('Invalid SSH public key format');
207
+ }
208
+ return Buffer.from(parts[1], 'base64');
209
+ }
210
+ /**
211
+ * Parse an SSH signature blob to extract the raw signature bytes.
212
+ *
213
+ * Wire format:
214
+ * string format (e.g., "ssh-ed25519")
215
+ * string signature
216
+ */
217
+ function parseSSHSignature(blob) {
218
+ let offset = 0;
219
+ // Skip format string
220
+ const formatLen = blob.readUInt32BE(offset);
221
+ offset += 4 + formatLen;
222
+ // Read signature string
223
+ const sigLen = blob.readUInt32BE(offset);
224
+ offset += 4;
225
+ return Buffer.from(blob.subarray(offset, offset + sigLen));
226
+ }
227
+ /**
228
+ * Encode a buffer as an SSH string (uint32 length + bytes).
229
+ */
230
+ function sshString(buf) {
231
+ return Buffer.concat([uint32(buf.length), buf]);
232
+ }
233
+ /**
234
+ * Encode a number as a big-endian uint32.
235
+ */
236
+ function uint32(n) {
237
+ const buf = Buffer.alloc(4);
238
+ buf.writeUInt32BE(n);
239
+ return buf;
240
+ }
241
+ //# sourceMappingURL=ssh-signer.js.map