wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
@@ -1,5 +1,579 @@
1
- import{createReadStream as D,createWriteStream as z,statSync as H,existsSync as l,copyFileSync as T,unlinkSync as N,readFileSync as W,writeFileSync as G,renameSync as v}from"fs";import{createGzip as q,gunzipSync as Y}from"zlib";import{pipeline as P}from"stream/promises";import{createCipheriv as F,createDecipheriv as J,randomBytes as O,scryptSync as K,createHash as x,createHmac as A}from"crypto";import{join as C}from"path";import{hostname as V,platform as X,arch as Q,cpus as Z}from"os";const tt="3.2.0",et=1,L="aes-256-gcm",E=16,j=32,R=16,B=32,nt=10;function M(s,t){const o=O(E),e=O(j),c=K(t,e,B),n=F(L,c,o),r=Buffer.concat([n.update(s),n.final()]),a=n.getAuthTag();return Buffer.concat([o,e,a,r])}function _(s,t){const o=E+j+R;if(s.length<o)throw new Error("Encrypted data too short \u2014 corrupted or not a Wyrm backup");let e=0;const c=s.subarray(e,e+=E),n=s.subarray(e,e+=j),r=s.subarray(e,e+=R),a=s.subarray(e),i=K(t,n,B),u=J(L,i,c);u.setAuthTag(r);try{return Buffer.concat([u.update(a),u.final()])}catch{throw new Error("Decryption failed \u2014 wrong password or corrupted backup")}}class st{accessKeyId;secretAccessKey;region;endpoint;bucket;host;constructor(t){this.accessKeyId=t.accessKeyId,this.secretAccessKey=t.secretAccessKey,this.region=t.region??"auto",this.endpoint=t.endpoint.replace(/\/$/,""),this.bucket=t.bucket;const o=new URL(this.endpoint);this.host=o.host}async putObject(t,o,e="application/octet-stream"){const c=`/${this.bucket}/${I(t)}`,n={"Content-Type":e,"Content-Length":o.length.toString()},r=this.signRequest("PUT",c,n,o),a=await fetch(`${this.endpoint}${c}`,{method:"PUT",headers:r,body:o});if(!a.ok){const i=await a.text();throw new Error(`S3 PUT failed (${a.status}): ${i}`)}}async getObject(t){const o=`/${this.bucket}/${I(t)}`,e={},c=this.signRequest("GET",o,e),n=await fetch(`${this.endpoint}${o}`,{method:"GET",headers:c});if(!n.ok){const a=await n.text();throw new Error(`S3 GET failed (${n.status}): ${a}`)}const r=await n.arrayBuffer();return Buffer.from(r)}async listObjects(t){const o=[];let e;do{const c=new URLSearchParams({"list-type":"2",prefix:t});e&&c.set("continuation-token",e);const n=`/${this.bucket}?${c.toString()}`,r={},a=this.signRequest("GET",n,r),i=await fetch(`${this.endpoint}${n}`,{method:"GET",headers:a});if(!i.ok){const h=await i.text();throw new Error(`S3 LIST failed (${i.status}): ${h}`)}const u=await i.text();o.push(...ht(u)),u.match(/<IsTruncated>(.*?)<\/IsTruncated>/)?.[1]==="true"?e=u.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/)?.[1]:e=void 0}while(e);return o}async deleteObject(t){const o=`/${this.bucket}/${I(t)}`,e={},c=this.signRequest("DELETE",o,e),n=await fetch(`${this.endpoint}${o}`,{method:"DELETE",headers:c});if(!n.ok){const r=await n.text();throw new Error(`S3 DELETE failed (${n.status}): ${r}`)}}signRequest(t,o,e,c){const n=new Date,r=ot(n),a=ct(n),[i,u]=it(o);e.host=this.host,e["x-amz-date"]=a,e["x-amz-content-sha256"]=k(c??Buffer.alloc(0));const d=Object.keys(e).map(w=>w.toLowerCase()).sort(),m=d.map(w=>`${w}:${e[Object.keys(e).find(U=>U.toLowerCase()===w)].trim()}`).join(`
2
- `)+`
3
- `,h=d.join(";"),p=[t,i,ut(u),m,h,e["x-amz-content-sha256"]].join(`
4
- `),f=`${r}/${this.region}/s3/aws4_request`,y=["AWS4-HMAC-SHA256",a,f,k(Buffer.from(p,"utf-8"))].join(`
5
- `),$=at(this.secretAccessKey,r,this.region,"s3"),g=rt($,y);return e.Authorization=`AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${f}, SignedHeaders=${h}, Signature=${g}`,e}}function ot(s){return s.toISOString().replace(/[-:T]/g,"").slice(0,8)}function ct(s){return s.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}/,"")}function k(s){return x("sha256").update(s).digest("hex")}function b(s,t){return A("sha256",s).update(t,"utf-8").digest()}function rt(s,t){return A("sha256",s).update(t,"utf-8").digest("hex")}function at(s,t,o,e){let c=b(`AWS4${s}`,t);return c=b(c,o),c=b(c,e),c=b(c,"aws4_request"),c}function I(s){return s.split("/").map(t=>encodeURIComponent(t)).join("/")}function it(s){const t=s.indexOf("?");return t===-1?[s,""]:[s.slice(0,t),s.slice(t+1)]}function ut(s){return s?s.split("&").map(t=>{const[o,e]=t.split("=");return`${encodeURIComponent(decodeURIComponent(o))}=${encodeURIComponent(decodeURIComponent(e??""))}`}).sort().join("&"):""}function ht(s){const t=[],o=/<Contents>([\s\S]*?)<\/Contents>/g;let e;for(;(e=o.exec(s))!==null;){const c=e[1],n=c.match(/<Key>(.*?)<\/Key>/)?.[1]??"",r=parseInt(c.match(/<Size>(.*?)<\/Size>/)?.[1]??"0",10),a=c.match(/<LastModified>(.*?)<\/LastModified>/)?.[1]??"";t.push({key:n,size:r,lastModified:a})}return t}class wt{config=null;encryptionKey=null;s3=null;constructor(){}configure(t,o){if(!t.endpoint||!t.bucket||!t.accessKeyId||!t.secretAccessKey)throw new Error("Cloud config requires endpoint, bucket, accessKeyId, and secretAccessKey");this.config={...t,region:t.region??"auto"},this.encryptionKey=o??null,this.s3=new st(this.config)}isConfigured(){return this.config!==null&&this.s3!==null}async backup(t){if(this.assertConfigured(),!l(t))throw new Error(`Database not found: ${t}`);const o=H(t),e=this.getMachineId(),c=new Date,n=dt(c),r=`backups/${e}/${n}.wyrm.bak`,a=`backups/${e}/${n}.wyrm.meta.json`;if(this.encryptionKey===null&&process.env.WYRM_ALLOW_PLAINTEXT_BACKUP!=="1")throw new Error("Refusing to upload an unencrypted full-DB backup to cloud storage. Set WYRM_ENCRYPTION_KEY to encrypt the snapshot, or WYRM_ALLOW_PLAINTEXT_BACKUP=1 to override (NOT recommended \u2014 uploads all private rows in cleartext).");const i=C(t,".."),u=C(i,`.wyrm-backup-${Date.now()}.tmp`),d=C(i,`.wyrm-backup-${Date.now()}.gz.tmp`);try{T(t,u);const m=`${t}-wal`,h=`${t}-shm`;l(m)&&T(m,`${u}-wal`),l(h)&&T(h,`${u}-shm`),await P(D(u),q({level:9}),z(d));let p=Buffer.from(W(d));const f=p.length,y=this.encryptionKey!==null;y&&(p=M(p,this.encryptionKey));const $=k(p);await this.s3.putObject(r,p,"application/x-wyrm-backup");const g={version:et,wyrm_version:tt,timestamp:c.toISOString(),db_size:o.size,compressed_size:f,encrypted:y,machine_id:e,checksum:$};return await this.s3.putObject(a,Buffer.from(JSON.stringify(g,null,2),"utf-8"),"application/json"),{key:r,metadata:g}}finally{S(u),S(d),S(`${u}-wal`),S(`${u}-shm`)}}async listBackups(){this.assertConfigured();const o=`backups/${this.getMachineId()}/`,c=(await this.s3.listObjects(o)).filter(r=>r.key.endsWith(".wyrm.meta.json")),n=[];for(const r of c)try{const a=await this.s3.getObject(r.key),i=JSON.parse(a.toString("utf-8"));n.push(i)}catch{}return n.sort((r,a)=>new Date(a.timestamp).getTime()-new Date(r.timestamp).getTime()),n}async listBackupsWithKeys(){this.assertConfigured();const o=`backups/${this.getMachineId()}/`,c=(await this.s3.listObjects(o)).filter(r=>r.key.endsWith(".wyrm.meta.json")),n=[];for(const r of c)try{const a=await this.s3.getObject(r.key),i=JSON.parse(a.toString("utf-8")),u=r.key.replace(/\.wyrm\.meta\.json$/,".wyrm.bak");n.push({key:u,metadata:i})}catch{}return n.sort((r,a)=>new Date(a.metadata.timestamp).getTime()-new Date(r.metadata.timestamp).getTime()),n}async restore(t,o){this.assertConfigured();const e=t.replace(/\.wyrm\.bak$/,".wyrm.meta.json");let c;try{const i=await this.s3.getObject(e);c=JSON.parse(i.toString("utf-8"))}catch{throw new Error(`Backup metadata not found for key: ${t}`)}let n=await this.s3.getObject(t);const r=k(n);if(r!==c.checksum)throw new Error(`Checksum mismatch \u2014 backup may be corrupted. Expected: ${c.checksum}, Got: ${r}`);if(c.encrypted){if(!this.encryptionKey)throw new Error("Backup is encrypted but no encryption key is configured");n=_(n,this.encryptionKey)}const a=Y(n);if(l(o)){const i=`${o}.bak`;v(o,i)}return G(o,a),{restored:!0,size:a.length}}async pruneBackups(t=nt){this.assertConfigured();const e=`backups/${this.getMachineId()}/`,n=(await this.s3.listObjects(e)).filter(i=>i.key.endsWith(".wyrm.bak")).sort((i,u)=>u.lastModified.localeCompare(i.lastModified));if(n.length<=t)return{deleted:0};const r=n.slice(t);let a=0;for(const i of r){const u=i.key.replace(/\.wyrm\.bak$/,".wyrm.meta.json");try{await this.s3.deleteObject(i.key),a++}catch{}try{await this.s3.deleteObject(u)}catch{}}return{deleted:a}}getMachineId(){const t=[V(),X(),Q(),Z()[0]?.model??"unknown-cpu"].join("|");return x("sha256").update(t).digest("hex").slice(0,16)}assertConfigured(){if(!this.isConfigured())throw new Error("Cloud backup not configured. Call configure() with R2 credentials first.")}}function dt(s){const t=s.getUTCFullYear(),o=String(s.getUTCMonth()+1).padStart(2,"0"),e=String(s.getUTCDate()).padStart(2,"0"),c=String(s.getUTCHours()).padStart(2,"0"),n=String(s.getUTCMinutes()).padStart(2,"0"),r=String(s.getUTCSeconds()).padStart(2,"0");return`${t}-${o}-${e}_${c}${n}${r}`}function S(s){try{l(s)&&N(s)}catch{}}export{wt as WyrmCloudBackup,_ as decryptBuffer,M as encryptBuffer};
1
+ /**
2
+ * Wyrm Cloud Backup — Encrypted backup/restore to Cloudflare R2
3
+ *
4
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
5
+ * @license Proprietary (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
6
+ * @module cloud-backup
7
+ * @version 3.2.0
8
+ */
9
+ import { createReadStream, createWriteStream, statSync, existsSync, copyFileSync, unlinkSync, readFileSync, writeFileSync, renameSync } from 'fs';
10
+ import { createGzip, gunzipSync } from 'zlib';
11
+ import { pipeline } from 'stream/promises';
12
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, createHash, createHmac } from 'crypto';
13
+ import { join } from 'path';
14
+ import { hostname, platform, arch, cpus } from 'os';
15
+ // ==================== CONSTANTS ====================
16
+ const WYRM_VERSION = '3.2.0';
17
+ const BACKUP_FORMAT_VERSION = 1;
18
+ const ALGORITHM = 'aes-256-gcm';
19
+ const IV_LENGTH = 16;
20
+ const SALT_LENGTH = 32;
21
+ const TAG_LENGTH = 16;
22
+ const KEY_LENGTH = 32;
23
+ const DEFAULT_KEEP_COUNT = 10;
24
+ // ==================== BUFFER ENCRYPTION ====================
25
+ /**
26
+ * Encrypt a buffer with AES-256-GCM using a password.
27
+ * Output format: [16-byte IV][32-byte salt][16-byte auth tag][encrypted data]
28
+ */
29
+ function encryptBuffer(data, password) {
30
+ const iv = randomBytes(IV_LENGTH);
31
+ const salt = randomBytes(SALT_LENGTH);
32
+ const key = scryptSync(password, salt, KEY_LENGTH);
33
+ const cipher = createCipheriv(ALGORITHM, key, iv);
34
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
35
+ const tag = cipher.getAuthTag();
36
+ // Header: IV + salt + auth tag + ciphertext
37
+ return Buffer.concat([iv, salt, tag, encrypted]);
38
+ }
39
+ /**
40
+ * Decrypt a buffer encrypted by encryptBuffer.
41
+ * Parses the [IV][salt][tag][data] header, derives key, decrypts.
42
+ */
43
+ function decryptBuffer(encrypted, password) {
44
+ const headerSize = IV_LENGTH + SALT_LENGTH + TAG_LENGTH;
45
+ if (encrypted.length < headerSize) {
46
+ throw new Error('Encrypted data too short — corrupted or not a Wyrm backup');
47
+ }
48
+ let offset = 0;
49
+ const iv = encrypted.subarray(offset, offset += IV_LENGTH);
50
+ const salt = encrypted.subarray(offset, offset += SALT_LENGTH);
51
+ const tag = encrypted.subarray(offset, offset += TAG_LENGTH);
52
+ const data = encrypted.subarray(offset);
53
+ const key = scryptSync(password, salt, KEY_LENGTH);
54
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
55
+ decipher.setAuthTag(tag);
56
+ try {
57
+ return Buffer.concat([decipher.update(data), decipher.final()]);
58
+ }
59
+ catch {
60
+ throw new Error('Decryption failed — wrong password or corrupted backup');
61
+ }
62
+ }
63
+ // ==================== MINIMAL S3-COMPATIBLE CLIENT ====================
64
+ /**
65
+ * Minimal S3-compatible client for Cloudflare R2.
66
+ * Uses AWS Signature V4 signing with Node.js built-ins only — zero dependencies.
67
+ */
68
+ class S3Client {
69
+ accessKeyId;
70
+ secretAccessKey;
71
+ region;
72
+ endpoint;
73
+ bucket;
74
+ host;
75
+ constructor(config) {
76
+ this.accessKeyId = config.accessKeyId;
77
+ this.secretAccessKey = config.secretAccessKey;
78
+ this.region = config.region ?? 'auto';
79
+ this.endpoint = config.endpoint.replace(/\/$/, '');
80
+ this.bucket = config.bucket;
81
+ // Extract host from endpoint
82
+ const url = new URL(this.endpoint);
83
+ this.host = url.host;
84
+ }
85
+ /** PUT an object to the bucket */
86
+ async putObject(key, body, contentType = 'application/octet-stream') {
87
+ const path = `/${this.bucket}/${encodeS3Key(key)}`;
88
+ const headers = {
89
+ 'Content-Type': contentType,
90
+ 'Content-Length': body.length.toString(),
91
+ };
92
+ const signed = this.signRequest('PUT', path, headers, body);
93
+ const res = await fetch(`${this.endpoint}${path}`, {
94
+ method: 'PUT',
95
+ headers: signed,
96
+ body: body,
97
+ });
98
+ if (!res.ok) {
99
+ const text = await res.text();
100
+ throw new Error(`S3 PUT failed (${res.status}): ${text}`);
101
+ }
102
+ }
103
+ /** GET an object from the bucket */
104
+ async getObject(key) {
105
+ const path = `/${this.bucket}/${encodeS3Key(key)}`;
106
+ const headers = {};
107
+ const signed = this.signRequest('GET', path, headers);
108
+ const res = await fetch(`${this.endpoint}${path}`, {
109
+ method: 'GET',
110
+ headers: signed,
111
+ });
112
+ if (!res.ok) {
113
+ const text = await res.text();
114
+ throw new Error(`S3 GET failed (${res.status}): ${text}`);
115
+ }
116
+ const arrayBuf = await res.arrayBuffer();
117
+ return Buffer.from(arrayBuf);
118
+ }
119
+ /** LIST objects with a given prefix */
120
+ async listObjects(prefix) {
121
+ const results = [];
122
+ let continuationToken;
123
+ do {
124
+ const params = new URLSearchParams({
125
+ 'list-type': '2',
126
+ prefix,
127
+ });
128
+ if (continuationToken) {
129
+ params.set('continuation-token', continuationToken);
130
+ }
131
+ const path = `/${this.bucket}?${params.toString()}`;
132
+ const headers = {};
133
+ const signed = this.signRequest('GET', path, headers);
134
+ const res = await fetch(`${this.endpoint}${path}`, {
135
+ method: 'GET',
136
+ headers: signed,
137
+ });
138
+ if (!res.ok) {
139
+ const text = await res.text();
140
+ throw new Error(`S3 LIST failed (${res.status}): ${text}`);
141
+ }
142
+ const xml = await res.text();
143
+ results.push(...parseListXml(xml));
144
+ // Check for truncation / pagination
145
+ const truncatedMatch = xml.match(/<IsTruncated>(.*?)<\/IsTruncated>/);
146
+ const isTruncated = truncatedMatch?.[1] === 'true';
147
+ if (isTruncated) {
148
+ const tokenMatch = xml.match(/<NextContinuationToken>(.*?)<\/NextContinuationToken>/);
149
+ continuationToken = tokenMatch?.[1];
150
+ }
151
+ else {
152
+ continuationToken = undefined;
153
+ }
154
+ } while (continuationToken);
155
+ return results;
156
+ }
157
+ /** DELETE an object from the bucket */
158
+ async deleteObject(key) {
159
+ const path = `/${this.bucket}/${encodeS3Key(key)}`;
160
+ const headers = {};
161
+ const signed = this.signRequest('DELETE', path, headers);
162
+ const res = await fetch(`${this.endpoint}${path}`, {
163
+ method: 'DELETE',
164
+ headers: signed,
165
+ });
166
+ if (!res.ok) {
167
+ const text = await res.text();
168
+ throw new Error(`S3 DELETE failed (${res.status}): ${text}`);
169
+ }
170
+ }
171
+ /**
172
+ * Generate AWS Signature V4 headers.
173
+ * Implements the full signing flow:
174
+ * 1. Canonical request
175
+ * 2. String to sign
176
+ * 3. Signing key derivation
177
+ * 4. Authorization header
178
+ */
179
+ signRequest(method, path, headers, body) {
180
+ const now = new Date();
181
+ const dateStamp = toDateStamp(now); // YYYYMMDD
182
+ const amzDate = toAmzDate(now); // YYYYMMDD'T'HHMMSS'Z'
183
+ // Split path and query string
184
+ const [canonicalUri, queryString] = splitPathQuery(path);
185
+ // Required headers
186
+ headers['host'] = this.host;
187
+ headers['x-amz-date'] = amzDate;
188
+ headers['x-amz-content-sha256'] = sha256Hex(body ?? Buffer.alloc(0));
189
+ // 1. Canonical request
190
+ const signedHeaderKeys = Object.keys(headers)
191
+ .map(k => k.toLowerCase())
192
+ .sort();
193
+ const canonicalHeaders = signedHeaderKeys
194
+ .map(k => `${k}:${headers[Object.keys(headers).find(h => h.toLowerCase() === k)].trim()}`)
195
+ .join('\n') + '\n';
196
+ const signedHeaders = signedHeaderKeys.join(';');
197
+ const canonicalRequest = [
198
+ method,
199
+ canonicalUri,
200
+ normalizeQueryString(queryString),
201
+ canonicalHeaders,
202
+ signedHeaders,
203
+ headers['x-amz-content-sha256'],
204
+ ].join('\n');
205
+ // 2. String to sign
206
+ const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
207
+ const stringToSign = [
208
+ 'AWS4-HMAC-SHA256',
209
+ amzDate,
210
+ credentialScope,
211
+ sha256Hex(Buffer.from(canonicalRequest, 'utf-8')),
212
+ ].join('\n');
213
+ // 3. Signing key
214
+ const signingKey = deriveSigningKey(this.secretAccessKey, dateStamp, this.region, 's3');
215
+ // 4. Signature
216
+ const signature = hmacHex(signingKey, stringToSign);
217
+ // 5. Authorization header
218
+ headers['Authorization'] =
219
+ `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, ` +
220
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
221
+ return headers;
222
+ }
223
+ }
224
+ // ==================== S3 SIGNING HELPERS ====================
225
+ function toDateStamp(date) {
226
+ return date.toISOString().replace(/[-:T]/g, '').slice(0, 8);
227
+ }
228
+ function toAmzDate(date) {
229
+ return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
230
+ }
231
+ function sha256Hex(data) {
232
+ return createHash('sha256').update(data).digest('hex');
233
+ }
234
+ function hmac(key, data) {
235
+ return createHmac('sha256', key).update(data, 'utf-8').digest();
236
+ }
237
+ function hmacHex(key, data) {
238
+ return createHmac('sha256', key).update(data, 'utf-8').digest('hex');
239
+ }
240
+ function deriveSigningKey(secret, dateStamp, region, service) {
241
+ let key = hmac(`AWS4${secret}`, dateStamp);
242
+ key = hmac(key, region);
243
+ key = hmac(key, service);
244
+ key = hmac(key, 'aws4_request');
245
+ return key;
246
+ }
247
+ /**
248
+ * Encode an S3 key — URI-encode each path segment individually,
249
+ * preserving `/` separators.
250
+ */
251
+ function encodeS3Key(key) {
252
+ return key
253
+ .split('/')
254
+ .map(segment => encodeURIComponent(segment))
255
+ .join('/');
256
+ }
257
+ /**
258
+ * Split a path into canonical URI and query string.
259
+ * e.g. "/bucket?list-type=2&prefix=x" → ["/bucket", "list-type=2&prefix=x"]
260
+ */
261
+ function splitPathQuery(path) {
262
+ const idx = path.indexOf('?');
263
+ if (idx === -1)
264
+ return [path, ''];
265
+ return [path.slice(0, idx), path.slice(idx + 1)];
266
+ }
267
+ /**
268
+ * Normalize a query string for canonical request:
269
+ * sort by parameter name, then by value.
270
+ */
271
+ function normalizeQueryString(qs) {
272
+ if (!qs)
273
+ return '';
274
+ return qs
275
+ .split('&')
276
+ .map(pair => {
277
+ const [k, v] = pair.split('=');
278
+ return `${encodeURIComponent(decodeURIComponent(k))}=${encodeURIComponent(decodeURIComponent(v ?? ''))}`;
279
+ })
280
+ .sort()
281
+ .join('&');
282
+ }
283
+ /**
284
+ * Parse the XML response from S3 ListObjectsV2.
285
+ * Extracts <Key>, <Size>, <LastModified> from each <Contents> block.
286
+ */
287
+ function parseListXml(xml) {
288
+ const results = [];
289
+ const contentsRegex = /<Contents>([\s\S]*?)<\/Contents>/g;
290
+ let match;
291
+ while ((match = contentsRegex.exec(xml)) !== null) {
292
+ const block = match[1];
293
+ const key = block.match(/<Key>(.*?)<\/Key>/)?.[1] ?? '';
294
+ const size = parseInt(block.match(/<Size>(.*?)<\/Size>/)?.[1] ?? '0', 10);
295
+ const lastModified = block.match(/<LastModified>(.*?)<\/LastModified>/)?.[1] ?? '';
296
+ results.push({ key, size, lastModified });
297
+ }
298
+ return results;
299
+ }
300
+ // ==================== WYRM CLOUD BACKUP ====================
301
+ export class WyrmCloudBackup {
302
+ config = null;
303
+ encryptionKey = null;
304
+ s3 = null;
305
+ constructor() { }
306
+ /** Configure cloud storage credentials and optional encryption key */
307
+ configure(config, encryptionKey) {
308
+ if (!config.endpoint || !config.bucket || !config.accessKeyId || !config.secretAccessKey) {
309
+ throw new Error('Cloud config requires endpoint, bucket, accessKeyId, and secretAccessKey');
310
+ }
311
+ this.config = {
312
+ ...config,
313
+ region: config.region ?? 'auto',
314
+ };
315
+ this.encryptionKey = encryptionKey ?? null;
316
+ this.s3 = new S3Client(this.config);
317
+ }
318
+ /** Check if cloud backup is configured */
319
+ isConfigured() {
320
+ return this.config !== null && this.s3 !== null;
321
+ }
322
+ /**
323
+ * Backup: SQLite DB → gzip → AES-256-GCM encrypt → upload to R2
324
+ *
325
+ * Pipeline:
326
+ * 1. Copy SQLite file to a temp snapshot (safe point-in-time copy)
327
+ * 2. Gzip compress
328
+ * 3. If encryption key set: AES-256-GCM encrypt the gzipped blob
329
+ * 4. Upload encrypted/compressed blob to R2
330
+ * 5. Upload metadata JSON alongside
331
+ * 6. Clean up temp files
332
+ *
333
+ * Key format: backups/{machine_id}/{YYYY-MM-DD_HHmmss}.wyrm.bak
334
+ */
335
+ async backup(dbPath) {
336
+ this.assertConfigured();
337
+ if (!existsSync(dbPath)) {
338
+ throw new Error(`Database not found: ${dbPath}`);
339
+ }
340
+ const dbStat = statSync(dbPath);
341
+ const machineId = this.getMachineId();
342
+ const now = new Date();
343
+ const timestamp = formatTimestamp(now);
344
+ const backupKey = `backups/${machineId}/${timestamp}.wyrm.bak`;
345
+ const metadataKey = `backups/${machineId}/${timestamp}.wyrm.meta.json`;
346
+ // [sec 6.3.1] Default-deny plaintext egress: refuse to upload a full-DB
347
+ // snapshot to cloud storage unless it will be encrypted. Fail fast before
348
+ // any temp file is written. Override only via an explicit, loud opt-in.
349
+ if (this.encryptionKey === null && process.env.WYRM_ALLOW_PLAINTEXT_BACKUP !== '1') {
350
+ throw new Error('Refusing to upload an unencrypted full-DB backup to cloud storage. Set '
351
+ + 'WYRM_ENCRYPTION_KEY to encrypt the snapshot, or WYRM_ALLOW_PLAINTEXT_BACKUP=1 '
352
+ + 'to override (NOT recommended — uploads all private rows in cleartext).');
353
+ }
354
+ // Temp files in the same directory as the DB to avoid cross-device issues
355
+ const dbDir = join(dbPath, '..');
356
+ const tempCopy = join(dbDir, `.wyrm-backup-${Date.now()}.tmp`);
357
+ const tempGz = join(dbDir, `.wyrm-backup-${Date.now()}.gz.tmp`);
358
+ try {
359
+ // 1. Safe snapshot — copy the database file
360
+ copyFileSync(dbPath, tempCopy);
361
+ // Also copy WAL/SHM if they exist (for consistency)
362
+ const walPath = `${dbPath}-wal`;
363
+ const shmPath = `${dbPath}-shm`;
364
+ if (existsSync(walPath))
365
+ copyFileSync(walPath, `${tempCopy}-wal`);
366
+ if (existsSync(shmPath))
367
+ copyFileSync(shmPath, `${tempCopy}-shm`);
368
+ // 2. Gzip compress
369
+ await pipeline(createReadStream(tempCopy), createGzip({ level: 9 }), createWriteStream(tempGz));
370
+ let uploadPayload = Buffer.from(readFileSync(tempGz));
371
+ const compressedSize = uploadPayload.length;
372
+ // 3. Encrypt if key is set
373
+ const isEncrypted = this.encryptionKey !== null;
374
+ if (isEncrypted) {
375
+ uploadPayload = encryptBuffer(uploadPayload, this.encryptionKey);
376
+ }
377
+ // Compute checksum of final payload
378
+ const checksum = sha256Hex(uploadPayload);
379
+ // 4. Upload blob
380
+ await this.s3.putObject(backupKey, uploadPayload, 'application/x-wyrm-backup');
381
+ // 5. Build and upload metadata
382
+ const metadata = {
383
+ version: BACKUP_FORMAT_VERSION,
384
+ wyrm_version: WYRM_VERSION,
385
+ timestamp: now.toISOString(),
386
+ db_size: dbStat.size,
387
+ compressed_size: compressedSize,
388
+ encrypted: isEncrypted,
389
+ machine_id: machineId,
390
+ checksum,
391
+ };
392
+ await this.s3.putObject(metadataKey, Buffer.from(JSON.stringify(metadata, null, 2), 'utf-8'), 'application/json');
393
+ return { key: backupKey, metadata };
394
+ }
395
+ finally {
396
+ // 6. Clean up temp files
397
+ safeUnlink(tempCopy);
398
+ safeUnlink(tempGz);
399
+ safeUnlink(`${tempCopy}-wal`);
400
+ safeUnlink(`${tempCopy}-shm`);
401
+ }
402
+ }
403
+ /**
404
+ * List available backups for this machine, sorted newest-first.
405
+ * Fetches metadata JSON for each backup.
406
+ */
407
+ async listBackups() {
408
+ this.assertConfigured();
409
+ const machineId = this.getMachineId();
410
+ const prefix = `backups/${machineId}/`;
411
+ const objects = await this.s3.listObjects(prefix);
412
+ // Find metadata files
413
+ const metaObjects = objects.filter(o => o.key.endsWith('.wyrm.meta.json'));
414
+ const metadataList = [];
415
+ for (const obj of metaObjects) {
416
+ try {
417
+ const buf = await this.s3.getObject(obj.key);
418
+ const meta = JSON.parse(buf.toString('utf-8'));
419
+ metadataList.push(meta);
420
+ }
421
+ catch {
422
+ // Skip unreadable metadata
423
+ }
424
+ }
425
+ // Sort newest first
426
+ metadataList.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
427
+ return metadataList;
428
+ }
429
+ /**
430
+ * List backups paired with their storage key (the .wyrm.bak blob key).
431
+ * Convenience for callers (e.g. the cloud-sync daemon) that need to call
432
+ * `restore()` without reconstructing the timestamp-format key themselves.
433
+ */
434
+ async listBackupsWithKeys() {
435
+ this.assertConfigured();
436
+ const machineId = this.getMachineId();
437
+ const prefix = `backups/${machineId}/`;
438
+ const objects = await this.s3.listObjects(prefix);
439
+ const metaObjects = objects.filter(o => o.key.endsWith('.wyrm.meta.json'));
440
+ const results = [];
441
+ for (const obj of metaObjects) {
442
+ try {
443
+ const buf = await this.s3.getObject(obj.key);
444
+ const meta = JSON.parse(buf.toString('utf-8'));
445
+ const blobKey = obj.key.replace(/\.wyrm\.meta\.json$/, '.wyrm.bak');
446
+ results.push({ key: blobKey, metadata: meta });
447
+ }
448
+ catch {
449
+ // Skip unreadable
450
+ }
451
+ }
452
+ results.sort((a, b) => new Date(b.metadata.timestamp).getTime() - new Date(a.metadata.timestamp).getTime());
453
+ return results;
454
+ }
455
+ /**
456
+ * Restore: Download from R2 → decrypt → gunzip → write to target path.
457
+ *
458
+ * If the target file already exists, it is renamed to `{target}.bak` before overwriting.
459
+ * Verifies checksum of the downloaded blob against metadata.
460
+ */
461
+ async restore(backupKey, targetPath) {
462
+ this.assertConfigured();
463
+ // Derive metadata key from backup key
464
+ const metadataKey = backupKey.replace(/\.wyrm\.bak$/, '.wyrm.meta.json');
465
+ // Download metadata
466
+ let metadata;
467
+ try {
468
+ const metaBuf = await this.s3.getObject(metadataKey);
469
+ metadata = JSON.parse(metaBuf.toString('utf-8'));
470
+ }
471
+ catch {
472
+ throw new Error(`Backup metadata not found for key: ${backupKey}`);
473
+ }
474
+ // 1. Download encrypted blob
475
+ let blob = await this.s3.getObject(backupKey);
476
+ // 5. Verify checksum
477
+ const actualChecksum = sha256Hex(blob);
478
+ if (actualChecksum !== metadata.checksum) {
479
+ throw new Error(`Checksum mismatch — backup may be corrupted. ` +
480
+ `Expected: ${metadata.checksum}, Got: ${actualChecksum}`);
481
+ }
482
+ // 2. Decrypt if encrypted
483
+ if (metadata.encrypted) {
484
+ if (!this.encryptionKey) {
485
+ throw new Error('Backup is encrypted but no encryption key is configured');
486
+ }
487
+ blob = decryptBuffer(blob, this.encryptionKey);
488
+ }
489
+ // 3. Gunzip decompress
490
+ const decompressed = gunzipSync(blob);
491
+ // 4. Write to targetPath (back up existing file first)
492
+ if (existsSync(targetPath)) {
493
+ const bakPath = `${targetPath}.bak`;
494
+ renameSync(targetPath, bakPath);
495
+ }
496
+ writeFileSync(targetPath, decompressed);
497
+ return { restored: true, size: decompressed.length };
498
+ }
499
+ /**
500
+ * Delete old backups, keeping the most recent `keepCount`.
501
+ * Deletes both the .wyrm.bak blob and its .wyrm.meta.json sidecar.
502
+ */
503
+ async pruneBackups(keepCount = DEFAULT_KEEP_COUNT) {
504
+ this.assertConfigured();
505
+ const machineId = this.getMachineId();
506
+ const prefix = `backups/${machineId}/`;
507
+ const objects = await this.s3.listObjects(prefix);
508
+ // Pair backup blobs with their metadata by timestamp stem
509
+ const backupBlobs = objects
510
+ .filter(o => o.key.endsWith('.wyrm.bak'))
511
+ .sort((a, b) => b.lastModified.localeCompare(a.lastModified)); // newest first
512
+ if (backupBlobs.length <= keepCount) {
513
+ return { deleted: 0 };
514
+ }
515
+ const toDelete = backupBlobs.slice(keepCount);
516
+ let deleted = 0;
517
+ for (const obj of toDelete) {
518
+ const metaKey = obj.key.replace(/\.wyrm\.bak$/, '.wyrm.meta.json');
519
+ try {
520
+ await this.s3.deleteObject(obj.key);
521
+ deleted++;
522
+ }
523
+ catch {
524
+ // Best-effort deletion
525
+ }
526
+ try {
527
+ await this.s3.deleteObject(metaKey);
528
+ }
529
+ catch {
530
+ // Metadata may not exist
531
+ }
532
+ }
533
+ return { deleted };
534
+ }
535
+ /**
536
+ * Get a deterministic machine fingerprint for device tracking.
537
+ * Hash of: hostname + platform + arch + first CPU model.
538
+ * Stable across reboots, unique per machine.
539
+ */
540
+ getMachineId() {
541
+ const parts = [
542
+ hostname(),
543
+ platform(),
544
+ arch(),
545
+ cpus()[0]?.model ?? 'unknown-cpu',
546
+ ].join('|');
547
+ return createHash('sha256').update(parts).digest('hex').slice(0, 16);
548
+ }
549
+ /** Throw if not configured */
550
+ assertConfigured() {
551
+ if (!this.isConfigured()) {
552
+ throw new Error('Cloud backup not configured. Call configure() with R2 credentials first.');
553
+ }
554
+ }
555
+ }
556
+ // ==================== HELPERS ====================
557
+ /** Format a Date as YYYY-MM-DD_HHmmss */
558
+ function formatTimestamp(date) {
559
+ const y = date.getUTCFullYear();
560
+ const mo = String(date.getUTCMonth() + 1).padStart(2, '0');
561
+ const d = String(date.getUTCDate()).padStart(2, '0');
562
+ const h = String(date.getUTCHours()).padStart(2, '0');
563
+ const mi = String(date.getUTCMinutes()).padStart(2, '0');
564
+ const s = String(date.getUTCSeconds()).padStart(2, '0');
565
+ return `${y}-${mo}-${d}_${h}${mi}${s}`;
566
+ }
567
+ /** Unlink a file if it exists, swallowing errors */
568
+ function safeUnlink(path) {
569
+ try {
570
+ if (existsSync(path))
571
+ unlinkSync(path);
572
+ }
573
+ catch {
574
+ // best-effort cleanup
575
+ }
576
+ }
577
+ // ==================== EXPORTS ====================
578
+ export { encryptBuffer, decryptBuffer };
579
+ //# sourceMappingURL=cloud-backup.js.map