#!/usr/bin/env node
'use strict';
/*
 * ═══════════════════════════════════════════════════════════════════════════
 *  SKYCLOUD — ענן פרטי וסינכרון, קובץ אחד, ללא שרת, ללא חשבון, ללא UI
 * ═══════════════════════════════════════════════════════════════════════════
 *
 *  איך משתמשים:
 *    1. זורקים את הקובץ הזה (cloud.js) לתיקייה של כל פרויקט.
 *    2. מריצים:  node cloud.js
 *       בהרצה הראשונה נוצר SYNC_ID ייחודי ונכתב אוטומטית לתוך הקובץ (למטה).
 *    3. במחשב/מקום אחר: שמים את אותו cloud.js עם אותו SYNC_ID ומריצים.
 *       הכול מסתנכרן — גם אם המקור הראשון כבוי לגמרי.
 *
 *  הטמעה בתוך קוד (ללא UI):
 *      const cloud = require('./cloud.js');
 *      await cloud.ready();                    // מחובר לרשת ומסונכרן
 *      await cloud.set('score', 123);          // שמירת נתון בענן
 *      const v = cloud.get('score');           // קריאת נתון
 *      cloud.onChange((key, value) => { ... })  // עדכון חי מכל מקום בעולם
 *
 *  מצבים מהטרמינל:
 *      node cloud.js            סינכרון חי ורציף (משאיר דולק כל עוד רוצים)
 *      node cloud.js --once     סינכרון חד-פעמי ויציאה (משוך+דחוף הכול)
 *      node cloud.js --deploy   בנייה (BUILD_COMMAND) + פרסום האתר והסינכרון
 *      node cloud.js --status   הצגת מצב, ID והכתובת הגלובלית
 *      node cloud.js --health   ניטור בריאות הרשת החי (Θ: latency/phi/score)
 *      node cloud.js --import x.zip      ייבוא מערכת שלמה מ-ZIP (פריקה+זיהוי)
 *      node cloud.js --new-id   יצירת SYNC_ID חדש (ענן חדש ריק)
 *      node cloud.js --shares 5 3        פיצול ה-ID ל-5 רסיסי-סוד (כל 3 משחזרים)
 *      node cloud.js --recover <רסיסים>  שחזור ה-ID מרסיסים
 *      node cloud.js --allow-mining      אישור מפורש לתרומת-עיבוד (אם MINING.enabled)
 *
 *  שרת אפליקציה חי על הכתובת הגלובלית (Ω) — לא סטטי:
 *      const cloud = require('./cloud.js'); await cloud.ready();
 *      cloud.route('GET', '/api/hello', async (req) => ({ msg: 'hi' }));
 *      // או גשר לשרת אמיתי (Next.js/Express): cloud.bridge('http://127.0.0.1:3000')
 *      // בדפדפן: כל fetch('/api/…') מנותב אוטומטית לעובד-שרת חי ברשת.
 *
 *  איך זה עובד בלי שרת ובלי שמשהו שלך דולק:
 *      המידע מוצפן (AES-256-GCM) ומשוגר לעשרות ממסרים ציבוריים פתוחים של
 *      רשת Nostr — רשת גלובלית מבוזרת שהיא חלק מהאינטרנט. המידע משוכפל
 *      בכמה מקומות בעולם במקביל, ונשאר שמור שם גם כשהכול אצלך כבוי.
 *      מי שמחזיק את אותו SYNC_ID (ורק הוא) יכול למשוך ולפענח את המידע,
 *      מכל מקום בעולם. תמיד מנצח העדכון האחרון (last-write-wins).
 *
 *  הכתובת הגלובלית:
 *      הכלי מפרסם עמוד סטטוס חתום לרשת, שזמין תמיד בכתובת קבועה דרך
 *      שערי רשת ציבוריים (מודפסת בהפעלה) — גם כשכל המכשירים שלך כבויים.
 * ═══════════════════════════════════════════════════════════════════════════
 */

// ─────────────────────────── הגדרות (אפשר לערוך) ───────────────────────────

// ה-ID של הענן. 'AUTO' = ייווצר לבד בהרצה הראשונה וייכתב לכאן אוטומטית.
// כדי לחבר מקום נוסף לאותו ענן — מעתיקים לשם את הקובץ עם אותו SYNC_ID.
const SYNC_ID = 'SC-372514B0C4747E61ED1115A8D5A9695C14962C59';

// אילו קבצים/תיקיות לא לסנכרן
const IGNORE = [
  'node_modules', '.git', '.svn', '.hg', '__pycache__', '.venv', 'venv',
  '.cloud-state.json', '.cloud-tmp', 'cloud.js', '.DS_Store', 'Thumbs.db',
  'dist', 'build', '.next', '.cache',
];

// ממסרים ציבוריים חופשיים של הרשת (אפשר להוסיף/להחליף)
const RELAYS = [
  'wss://relay.damus.io',
  'wss://nos.lol',
  'wss://relay.primal.net',
  'wss://relay.nostr.band',
  'wss://nostr.mom',
  'wss://offchain.pub',
  'wss://nostr.oxtr.dev',
  'wss://relay.nostr.bg',
  'wss://nostr.wine',
  'wss://relay.snort.social',
  'wss://nostr.bitcoiner.social',
  'wss://relay.nostrplebs.com',
];

// ממסרי-מילואים ("נתיבים חלופיים"): נכנסים לשימוש רק כשמנהל-המסלולים (Θ)
// מזהה שהנתיב הפעיל נחסם/מתדרדר — החלפת-מסלול דינמית (Auto-Proxy / hopping).
const RELAYS_FALLBACK = [
  'wss://relay.nostr.net',
  'wss://nostr.land',
  'wss://relay.mostr.pub',
  'wss://nostr21.com',
  'wss://relay.nostr.wirednet.jp',
  'wss://nostr.rocks',
  'wss://relay.current.fyi',
  'wss://eden.nostr.land',
];

// אתר ציבורי חי (nsite / NIP-5A): true = הפרויקט מקבל כתובת אתר אמיתית ברשת
// המבוזרת, שמציגה את התוכן ומתעדכנת בכל סינכרון — גם כשכל המכשירים כבויים.
// שים לב: במצב זה קבצי הפרויקט גלויים לצפייה בכתובת (לא מוצפנים).
// false = ענן מוצפן בלבד, בלי אתר ציבורי.
const PUBLIC_WEBSITE = true;

// ─────────── הגדרות האתר (כמו firebase.json / vercel.json — הכול כאן) ───────────

// שם האתר: 'AUTO' = שם תיקיית הפרויקט. שם בעברית מתורגם לאנגלית אוטומטית.
const SITE_NAME = 'AUTO';

// מאיזו תיקייה מגישים את האתר: 'AUTO' = זיהוי אוטומטי — אם קיימת תיקיית
// dist / build / public / www / site / web / docs עם index.html היא תוגש
// כשורש האתר; אחרת כל הפרויקט. אפשר לקבוע ידנית, למשל: 'dist'.
const SITE_DIR = 'AUTO';

// פקודת בנייה שרצה בטרמינל לפני פרסום האתר (כמו ב-Vercel/Firebase).
// למשל: 'npm run build'. ריק = בלי שלב בנייה.
const BUILD_COMMAND = '';

// דומיין משלך (אופציונלי): למשל 'www.myproject.io'. דומיין אמיתי חייב
// להירכש אצל רשם דומיינים (עלות שנתית) — אין דרך חוקית לקבל .io בחינם,
// אצל אף שירות בעולם. אחרי רכישה: מפנים רשומת CNAME מהדומיין אל כתובת
// ה-npub של האתר (מודפסת ב---status) והאתר המבוזר מוגש מהדומיין שלך.
const CUSTOM_DOMAIN = '';

// שרתי קבצים ציבוריים מבוזרים (Blossom) שמאחסנים את קבצי האתר.
// הסדר חשוב: שערי האתר פונים אליהם לפי הסדר, והראשונים מגישים
// Content-Type תקין (CSS/JS מוצגים נכון בדפדפן).
const BLOSSOM_SERVERS = [
  'https://cdn.nostrcheck.me',
  'https://nostr.download',
  'https://blossom.primal.net',
  'https://blossom.band',
];

// שערים ציבוריים שמגישים את האתר מהרשת המבוזרת (אותו אתר בכולם).
// מסודרים לפי מהירות/אמינות שנמדדו בפועל. אם רשת חוסמת אחד — נסה את הבא.
const NSITE_GATEWAYS = ['nosto.re', 'nwb.tf', 'nsite.cloud', 'nsite.run', 'nsite.lol'];

// ─────────── שרת אפליקציה חי על הכתובת הגלובלית (SkyLattice-Ω) ───────────
// הכתובת הגלובלית מגישה מעטפת סטטית (SPA) + מנוע הדפדפן. כל קריאת fetch
// לנתיב שמתחיל ב-API_PREFIX מנותבת אוטומטית דרך הרשת המבוזרת אל "צד שרת"
// חי — כל מכשיר Node שמריץ את cloud.js ורשם route()/bridge() הוא עובד-שרת.
// כך מערכת מלאה (כולל Next.js/סוכני AI) חיה בכתובת כל עוד עובד אחד פעיל
// כלשהו בעולם; כשכולם כבויים נשארת המעטפת הסטטית + הנתונים במטמון (כמו CDN).
const API_PREFIX = '/api';         // כל fetch לנתיב הזה מנותב לצד-השרת המבוזר

// גשר לשרת מקומי אמיתי (Next.js/Express/כל שפה): אם מוגדר, בקשות ה-API
// מהכתובת הגלובלית מגושרות אל השרת הזה שרץ אצלך (למשל 'http://127.0.0.1:3000').
// כך הכתובת הגלובלית מריצה שרת Node *מלא* — לא סטטי — דרך הרשת המבוזרת.
const LOCAL_BRIDGE = '';

// כרייה/תרומת-עיבוד: כבוי כברירת מחדל. הפעלה דורשת הסכמה מפורשת בזמן ריצה
// (--allow-mining) — לעולם לא רץ בשקט אצל מי שלא אישר. פרטים: ראה MiningConfig.
const MINING = {
  enabled: false,        // true = מציע תרומת-עיבוד (עדיין דורש אישור מפורש בהרצה)
  wallet: '',            // כתובת הארנק שאליה זוכפות התרומות
  coin: '',              // סמל המטבע, למשל 'XMR'
  network: '',           // הרשת/פּוּל, למשל 'monero:pool.example:3333'
  maxCpuPercent: 15,     // תקרת עומס מעבד (%) — לא ממוטט מכשירים
};

const CHUNK_SIZE = 16 * 1024;      // גודל חלק לקובץ (16KB לפני הצפנה, בטוח לכל ממסר)
const API_INLINE_MAX = 48 * 1024;  // תגובת API גדולה מזה עוברת דרך Blossom ולא inline
const KIND_DATA = 30078;           // אירוע נתונים ניתן-להחלפה (NIP-78)
const KIND_SITE = 30023;           // עמוד הסטטוס הציבורי (NIP-23)
const KIND_NSITE = 15128;          // מניפסט אתר סטטי (NIP-5A)
const KIND_BLOSSOM_LIST = 10063;   // רשימת שרתי הקבצים של המשתמש (BUD-03)
const LIVE_POLL_MS = 45_000;       // בדיקת עדכונים יזומה כגיבוי למנוי החי
const HEAL_SWEEP_MS = 300_000;     // Θ: מחזור ריפוי-עצמי (רענון שכפול + אימות שלמות)
const DEBOUNCE_MS = 900;           // השהיה אחרי שינוי קובץ לפני שיגור

// ────────────────────────────── בסיס ──────────────────────────────

const crypto = require('node:crypto');
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const path = require('node:path');
const { exec } = require('node:child_process');

const ROOT = __dirname;
const STATE_FILE = path.join(ROOT, '.cloud-state.json');

// הכלי משתמש ב-WebSocket וב-fetch המובנים של Node — נדרש Node 21 ומעלה
if (typeof WebSocket === 'undefined' || typeof fetch === 'undefined') {
  const msg = '[cloud] נדרש Node.js 21 ומעלה (WebSocket/fetch מובנים). הגרסה הנוכחית: ' + process.version;
  if (require.main === module) { console.error(msg); process.exit(1); }
  throw new Error(msg);
}

// ─────────── שם הפרויקט: זיהוי עברית/אנגלית ותרגום לאנגלית תקינה ───────────

const HEB2EN = {
  'א': 'a', 'ב': 'b', 'ג': 'g', 'ד': 'd', 'ה': 'h', 'ו': 'v', 'ז': 'z',
  'ח': 'ch', 'ט': 't', 'י': 'y', 'כ': 'k', 'ך': 'k', 'ל': 'l', 'מ': 'm',
  'ם': 'm', 'נ': 'n', 'ן': 'n', 'ס': 's', 'ע': 'a', 'פ': 'p', 'ף': 'f',
  'צ': 'ts', 'ץ': 'ts', 'ק': 'k', 'ר': 'r', 'ש': 'sh', 'ת': 't',
};

/** הופך כל שם (גם בעברית) לשם אתר אנגלי נקי, למשל "האתר שלי" -> "hatr-shly" */
function slugify(name) {
  const translit = [...name].map((c) => HEB2EN[c] ?? c).join('');
  const slug = translit
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .slice(0, 40);
  return slug || 'my-project';
}

const PROJECT_NAME = path.basename(ROOT);
const SITE_SLUG = SITE_NAME === 'AUTO' ? slugify(PROJECT_NAME) : slugify(SITE_NAME);

/** תיקיית האתר בפועל: מוגדרת ידנית, מזוהה אוטומטית, או שורש הפרויקט */
function resolveSiteDir() {
  if (SITE_DIR !== 'AUTO') {
    if (fs.existsSync(path.join(ROOT, SITE_DIR, 'index.html'))) return SITE_DIR;
    log(`אזהרה: SITE_DIR='${SITE_DIR}' לא מכיל index.html — האתר יוגש משורש הפרויקט.`);
    return null;
  }
  for (const dir of ['dist', 'build', 'public', 'www', 'site', 'web', 'docs']) {
    if (fs.existsSync(path.join(ROOT, dir, 'index.html'))) return dir;
  }
  return null;
}

/**
 * זיהוי קובץ הפעלה (.bat / .cmd) שפותח דף HTML בדפדפן.
 * אם בפרויקט יש למשל run.bat עם "start app.html" — אותו דף הופך אוטומטית
 * לדף הבית של הכתובת הגלובלית, בדיוק כמו שהוא נפתח מקומית.
 */
function resolveBatEntry() {
  let names = [];
  try { names = fs.readdirSync(ROOT); } catch {}
  for (const name of names.filter((n) => /\.(bat|cmd)$/i.test(n))) {
    let text = '';
    try { text = fs.readFileSync(path.join(ROOT, name), 'utf8'); } catch { continue; }
    for (const rawLine of text.split(/\r?\n/)) {
      const line = rawLine.trim();
      if (!line || /^(rem\b|::)/i.test(line)) continue;
      // תומך גם בנתיבים עם רווחים בתוך מרכאות: start "" "my page.html"
      const m = line.match(/"([^"]+\.html?)"|'([^']+\.html?)'|(\S+\.html?)\b/i);
      if (!m) continue;
      const raw = m[1] ?? m[2] ?? m[3];
      if (/^https?:\/\//i.test(raw)) continue; // כתובת אינטרנט/localhost — לא קובץ מקומי
      const rel = raw.replace(/\\/g, '/').replace(/^\.\//, '');
      if (fs.existsSync(path.join(ROOT, rel))) return { bat: name, html: rel };
    }
  }
  return null;
}
const sha256 = (...bufs) => {
  const h = crypto.createHash('sha256');
  for (const b of bufs) h.update(b);
  return h.digest();
};
const hex = (b) => Buffer.from(b).toString('hex');
const unhex = (s) => Buffer.from(s, 'hex');
const now = () => Math.floor(Date.now() / 1000);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const log = (...a) => console.log('[cloud]', ...a);

// ─────────── Θ: שעון לוגי היברידי (Hybrid Logical Clock) ───────────
// זמן פיזי + מונה מונוטוני. מבטיח סדר אירועים עקבי (last-write-wins) גם
// כששעוני המכשירים סוטים זה מזה — הערך תמיד עולה, ותמיד בר-השוואה מול
// חותמות Date.now() רגילות (כולל גרסאות ישנות של המנוע). זהו תיקון-עצמי
// מתמטי לסטיית שעונים: אין צורך בשרת זמן מרכזי.
let _hlcLast = 0;
function hlcNow() {
  const phys = Date.now();
  _hlcLast = phys > _hlcLast ? phys : _hlcLast + 1;
  return _hlcLast;
}
/** קליטת חותמת מרוחקת — מקדם את השעון המקומי מעבר לכל זמן שנצפה */
function hlcObserve(remoteTs) {
  if (typeof remoteTs === 'number' && remoteTs > _hlcLast) _hlcLast = remoteTs;
}

// ─────────── פורק ZIP טהור (ללא תלות) — לייבוא מערכת שלמה ───────────
// קורא את ספריית המרכז (Central Directory) של ארכיון PKZIP ומחלץ כל קובץ:
// שיטת אחסון 0 (stored) או 8 (deflate → zlib.inflateRawSync). כך אפשר לזרוק
// zip של מערכת שלמה, המנוע יפרוק אותה, יזהה בנייה/הפעלה ויגיש לכתובת.
const zlib = require('node:zlib');
function unzip(buf) {
  const files = [];
  // איתור End Of Central Directory (EOCD): חתימה 0x06054b50 מהסוף
  let eocd = -1;
  for (let i = buf.length - 22; i >= 0 && i >= buf.length - 22 - 65536; i--) {
    if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
  }
  if (eocd < 0) throw new Error('ZIP לא תקין: לא נמצא EOCD');
  const total = buf.readUInt16LE(eocd + 10);
  let ptr = buf.readUInt32LE(eocd + 16); // היסט ספריית המרכז
  for (let n = 0; n < total; n++) {
    if (buf.readUInt32LE(ptr) !== 0x02014b50) break;
    const method = buf.readUInt16LE(ptr + 10);
    const compSize = buf.readUInt32LE(ptr + 20);
    const nameLen = buf.readUInt16LE(ptr + 28);
    const extraLen = buf.readUInt16LE(ptr + 30);
    const commentLen = buf.readUInt16LE(ptr + 32);
    const localOff = buf.readUInt32LE(ptr + 42);
    const name = buf.toString('utf8', ptr + 46, ptr + 46 + nameLen);
    ptr += 46 + nameLen + extraLen + commentLen;
    if (name.endsWith('/')) continue; // תיקייה
    // כותרת מקומית — אורך שם/extra יכול להיות שונה מהמרכז
    const lNameLen = buf.readUInt16LE(localOff + 26);
    const lExtraLen = buf.readUInt16LE(localOff + 28);
    const dataStart = localOff + 30 + lNameLen + lExtraLen;
    const raw = buf.subarray(dataStart, dataStart + compSize);
    let data;
    if (method === 0) data = Buffer.from(raw);
    else if (method === 8) data = zlib.inflateRawSync(raw);
    else throw new Error(`ZIP: שיטת דחיסה לא נתמכת (${method}) עבור ${name}`);
    files.push({ name: name.replace(/\\/g, '/'), data });
  }
  return files;
}

// ─────────── שכבה מטמורפית: טביעת-רגל שונה בכל הרצה (Polymorphism) ───────────
// כל הרצה מקבלת "מלח" אקראי שמעצב אחרת את צורת התעבורה והזיכרון: ריפוד
// אקראי לאירועים, ערבוב סדר החיבור לממסרים, וריצוד תזמון. התוצאה: אין שתי
// הרצות עם אותה חתימת-רשת/זיכרון — קשה מאוד לטבוע-אצבע, לסנן או לחסום.
const RUN_SALT = crypto.randomBytes(8).toString('hex');
const metaPad = () => crypto.randomBytes(3 + Math.floor(Math.random() * 29)).toString('base64');
const metaJitter = (base = 0, spread = 220) => base + Math.floor(Math.random() * spread);
function shuffle(arr) {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
  return a;
}
/** חתימת ההרצה — משתנה בכל הפעלה (לניטור/הוכחת פולימורפיזם) */
const runFingerprint = () => hex(sha256(Buffer.from(RUN_SALT + ':' + process.pid))).slice(0, 16);

// ─────────── חלוקה א-סימטרית משוקללת לפי עומס (Θ) ───────────
// מפזר עבודה/חלקים בין יעדים ביחס לציון-הבריאות שלהם: יעד בריא יותר מקבל
// יותר, עמוס/חשוד מקבל פחות — ולא סיבוב שווה עיוור. דגימה משוקללת אקראית.
function pickWeighted(scored, k) {
  const pool = scored.filter((s) => s.score > 0).map((s) => ({ ...s }));
  const out = [];
  k = Math.min(k, pool.length);
  for (let n = 0; n < k; n++) {
    let total = pool.reduce((t, s) => t + s.score, 0);
    if (total <= 0) break;
    let r = Math.random() * total, idx = 0;
    for (; idx < pool.length; idx++) { r -= pool[idx].score; if (r <= 0) break; }
    out.push(pool[Math.min(idx, pool.length - 1)].id);
    pool.splice(Math.min(idx, pool.length - 1), 1);
  }
  return out;
}

// ─────────────────── secp256k1 + Schnorr (BIP-340), ללא תלות ───────────────────

const P = 2n ** 256n - 2n ** 32n - 977n;
const N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
const G = {
  x: 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798n,
  y: 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8n,
};

const mod = (a, m = P) => ((a % m) + m) % m;
const powmod = (b, e, m) => {
  let r = 1n; b = mod(b, m);
  while (e > 0n) { if (e & 1n) r = (r * b) % m; b = (b * b) % m; e >>= 1n; }
  return r;
};
const inv = (a, m = P) => powmod(mod(a, m), m - 2n, m);

function ptDouble(a) {
  if (!a) return null;
  const lam = mod(3n * a.x * a.x * inv(2n * a.y));
  const x = mod(lam * lam - 2n * a.x);
  return { x, y: mod(lam * (a.x - x) - a.y) };
}
function ptAdd(a, b) {
  if (!a) return b;
  if (!b) return a;
  if (a.x === b.x) return a.y === b.y ? ptDouble(a) : null;
  const lam = mod((b.y - a.y) * inv(b.x - a.x));
  const x = mod(lam * lam - a.x - b.x);
  return { x, y: mod(lam * (a.x - x) - a.y) };
}
function ptMul(pt, k) {
  let r = null, a = pt;
  while (k > 0n) { if (k & 1n) r = ptAdd(r, a); a = ptDouble(a); k >>= 1n; }
  return r;
}

const big = (buf) => BigInt('0x' + hex(buf));
const b32 = (n) => unhex(n.toString(16).padStart(64, '0'));
const taggedHash = (tag, ...data) => {
  const th = sha256(Buffer.from(tag));
  return sha256(th, th, ...data);
};
const xorBuf = (a, b) => Buffer.from(a.map((v, i) => v ^ b[i]));

/** חתימת Schnorr לפי BIP-340 — זה מה שהרשת דורשת ומאמתת */
function schnorrSign(msg32, privBig) {
  let d = privBig;
  const Pub = ptMul(G, d);
  if (Pub.y % 2n !== 0n) d = N - d;
  const px = b32(Pub.x);
  const t = xorBuf(b32(d), taggedHash('BIP0340/aux', crypto.randomBytes(32)));
  const k0 = mod(big(taggedHash('BIP0340/nonce', t, px, msg32)), N);
  if (k0 === 0n) throw new Error('bad nonce');
  const R = ptMul(G, k0);
  const k = R.y % 2n === 0n ? k0 : N - k0;
  const rx = b32(R.x);
  const e = mod(big(taggedHash('BIP0340/challenge', rx, px, msg32)), N);
  return Buffer.concat([rx, b32(mod(k + e * d, N))]);
}

// ─────────── SkyLattice-Σ3: אימות Schnorr מלא — אפס אמון בממסרים ───────────
// כל אירוע שמגיע מהרשת מאומת מתמטית (sG = R + eP לפי BIP-340) לפני קבלה.
// ממסר זדוני לא יכול לזייף, לשנות או להחדיר נתונים בשם אף מחבר.

/** שחזור נקודה על העקומה מקואורדינטת X (עם Y זוגי) — BIP-340 lift_x */
function liftX(x) {
  if (x >= P) return null;
  const c = mod(x * x * x + 7n);
  const y = powmod(c, (P + 1n) / 4n, P);
  if ((y * y) % P !== c) return null;
  return { x, y: y % 2n === 0n ? y : P - y };
}

/** אימות חתימת Schnorr (BIP-340) */
function schnorrVerify(sig, msg32, pubHex) {
  if (sig.length !== 64) return false;
  const r = big(sig.subarray(0, 32));
  const s = big(sig.subarray(32));
  if (r >= P || s >= N) return false;
  const Ppt = liftX(big(unhex(pubHex)));
  if (!Ppt) return false;
  const e = mod(big(taggedHash('BIP0340/challenge', sig.subarray(0, 32), unhex(pubHex), msg32)), N);
  const R = ptAdd(ptMul(G, s), ptMul(Ppt, N - e));
  return !!R && R.y % 2n === 0n && R.x === r;
}

/** אימות אירוע שלם: תקינות ה-ID (hash) + החתימה. עם מטמון נגד אימות כפול. */
const verifiedIds = new Set();
let rejectedEvents = 0;
function verifyEvent(ev) {
  try {
    if (!ev || typeof ev.id !== 'string' || typeof ev.sig !== 'string' || typeof ev.pubkey !== 'string') return false;
    // בדיקת ה-hash רצה תמיד (זולה) — מונעת replay של תוכן זר תחת id מאומת;
    // המטמון חוסך רק את אימות החתימה (היקר) עבור אותו id.
    const calc = hex(sha256(Buffer.from(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content]))));
    if (calc !== ev.id) return false;
    if (verifiedIds.has(ev.id)) return true;
    if (!schnorrVerify(unhex(ev.sig), unhex(ev.id), ev.pubkey)) return false;
    if (verifiedIds.size > 5000) verifiedIds.clear();
    verifiedIds.add(ev.id);
    return true;
  } catch { return false; }
}

// ─────────────────────── bech32 (לכתובת הגלובלית, NIP-19) ───────────────────────

const B32C = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
function bech32Polymod(values) {
  const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
  let chk = 1;
  for (const v of values) {
    const b = chk >> 25;
    chk = ((chk & 0x1ffffff) << 5) ^ v;
    for (let i = 0; i < 5; i++) if ((b >> i) & 1) chk ^= GEN[i];
  }
  return chk;
}
function bech32Encode(hrp, data5) {
  const hrpExp = [...[...hrp].map((c) => c.charCodeAt(0) >> 5), 0, ...[...hrp].map((c) => c.charCodeAt(0) & 31)];
  const pm = bech32Polymod([...hrpExp, ...data5, 0, 0, 0, 0, 0, 0]) ^ 1;
  const chk = Array.from({ length: 6 }, (_, i) => (pm >> (5 * (5 - i))) & 31);
  return hrp + '1' + [...data5, ...chk].map((d) => B32C[d]).join('');
}
function to5bit(bytes) {
  const out = []; let acc = 0, bits = 0;
  for (const b of bytes) {
    acc = (acc << 8) | b; bits += 8;
    while (bits >= 5) { bits -= 5; out.push((acc >> bits) & 31); }
  }
  if (bits > 0) out.push((acc << (5 - bits)) & 31);
  return out;
}
/** יצירת כתובת naddr קבועה לעמוד הסטטוס */
function makeNaddr(dTag, pubkeyHex, kind, relay) {
  const tlv = [];
  const push = (type, buf) => { tlv.push(type, buf.length, ...buf); };
  push(0, Buffer.from(dTag, 'utf8'));
  push(1, Buffer.from(relay, 'ascii'));
  push(2, unhex(pubkeyHex));
  const kb = Buffer.alloc(4); kb.writeUInt32BE(kind); push(3, kb);
  return bech32Encode('naddr', to5bit(Buffer.from(tlv)));
}

// ────────────────────────── זהות והצפנה מתוך ה-SYNC_ID ──────────────────────────

let syncId = SYNC_ID;

/** בהרצה ראשונה: יצירת ID וכתיבתו לתוך הקובץ הזה עצמו */
function ensureId() {
  if (syncId && syncId !== 'AUTO') return;
  syncId = 'SC-' + hex(crypto.randomBytes(20)).toUpperCase();
  try {
    const self = fs.readFileSync(__filename, 'utf8');
    const updated = self.replace(`const SYNC_ID = 'AUTO';`, `const SYNC_ID = '${syncId}';`);
    if (updated === self) throw new Error('SYNC_ID marker not found');
    fs.writeFileSync(__filename, updated, 'utf8');
    log('נוצר SYNC_ID חדש ונשמר בתוך הקובץ:');
    log('  ' + syncId);
    log('כדי לסנכרן ממקום אחר — העתק את cloud.js הזה (עם ה-ID שבפנים) לשם.');
  } catch (err) {
    log('אזהרה: לא ניתן לכתוב את ה-ID לקובץ (' + err.message + '). ה-ID לריצה זו: ' + syncId);
  }
}

let keys = null;
function deriveKeys() {
  const seed = sha256(Buffer.from('skycloud:v1:' + syncId));
  let priv = mod(big(seed), N - 1n) + 1n;
  const pub = ptMul(G, priv);
  keys = {
    priv,
    pubkey: hex(b32(pub.x)),
    encKey: sha256(Buffer.from('skycloud:enc:' + syncId)),
    ns: hex(sha256(Buffer.from('skycloud:ns:' + syncId))).slice(0, 12),
  };
}

// SkyLattice-Σ2: הצפנה כבולת-כתובת (Address-Bound AEAD).
// הכתובת הרשתית של כל פיסת מידע (ה-d-tag שלה) נכרכת קריפטוגרפית לתוך
// אימות ההצפנה (AAD של AES-256-GCM). התוצאה: גם עם ciphertext תקין לחלוטין,
// אי אפשר להזיז/להחליף בלוק בין קבצים, מיקומים או מפתחות — הפענוח ייכשל.
const encrypt = (buf, aad) => {
  const iv = crypto.randomBytes(12);
  const c = crypto.createCipheriv('aes-256-gcm', keys.encKey, iv);
  if (aad) c.setAAD(Buffer.from('sc-sigma2:' + aad));
  const enc = Buffer.concat([c.update(buf), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), enc]).toString('base64');
};
const decrypt = (b64, aad) => {
  const raw = Buffer.from(b64, 'base64');
  const d = crypto.createDecipheriv('aes-256-gcm', keys.encKey, raw.subarray(0, 12));
  if (aad) d.setAAD(Buffer.from('sc-sigma2:' + aad));
  d.setAuthTag(raw.subarray(12, 28));
  return Buffer.concat([d.update(raw.subarray(28)), d.final()]);
};

// ────────────────────────────── אירועי רשת ──────────────────────────────

function signEvent(kind, tags, content) {
  const ev = { pubkey: keys.pubkey, created_at: now(), kind, tags, content };
  ev.id = hex(sha256(Buffer.from(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content]))));
  ev.sig = hex(schnorrSign(unhex(ev.id), keys.priv));
  return ev;
}

const makeEvent = (kind, dTag, content, extraTags = []) => signEvent(kind, [['d', dTag], ...extraTags], content);

// ────────────────────────────── חיבור לממסרים ──────────────────────────────

// ─────────── Θ: מד-בריאות ממסרים (EWMA + Phi-Accrual) ───────────
// לכל ממסר נמדד זמן-תגובה (ממוצע נע מעריכי) ורצף הצלחות/כשלים. גלאי
// הכשל φ (phi-accrual, כמו ב-Cassandra/Akka) מחשב "רמת חשד" רציפה לפי
// התפלגות מרווחי-ההגעה — לא בוליאני "חי/מת" אלא הסתברות מתמטית. המנוע
// בוחר אוטומטית לשגר קודם לממסרים הבריאים ביותר ומדלג על חשודים.
class RelayHealth {
  constructor() { this.ewma = 400; this.ok = 1; this.fail = 0; this.last = Date.now(); this.iv = [700]; }
  observe() { // הגעת הודעה כלשהי — מזין את גלאי ה-φ
    const t = Date.now(), gap = t - this.last; this.last = t;
    if (gap > 0 && gap < 120_000) { this.iv.push(gap); if (this.iv.length > 40) this.iv.shift(); }
  }
  latency(ms) { this.ewma = 0.7 * this.ewma + 0.3 * ms; this.ok++; }
  miss() { this.fail++; }
  phi() { // רמת חשד: ככל שעבר זמן רב יותר מהצפוי, φ גדל
    const mean = this.iv.reduce((s, x) => s + x, 0) / this.iv.length || 700;
    const t = Date.now() - this.last;
    return (t / mean) / Math.LN10; // התפלגות מעריכית: φ = -log10(e^{-t/μ})
  }
  score() { // גבוה = בריא: יחס הצלחה גבוה, זמן-תגובה נמוך, חשד נמוך
    const rate = this.ok / (this.ok + this.fail);
    return (rate * 1000) / (1 + this.ewma / 100) / (1 + Math.max(0, this.phi() - 1));
  }
}

class RelayPool {
  constructor(urls, fallback = []) {
    this.urls = urls;
    this.fallback = fallback;      // נתיבים חלופיים ל-hopping (Auto-Proxy)
    this.active = new Set(urls);   // מסלולים פעילים כרגע
    this.sockets = new Map();      // url -> WebSocket
    this.pendingOk = new Map();    // eventId -> {resolve, oks:Set}
    this.subs = new Map();         // subId -> {onEvent, eose:Set, onEose}
    this.health = new Map();       // url -> RelayHealth (Θ)
    this.hops = 0;                 // כמה פעמים הוחלף מסלול
    this.closed = false;
  }

  _h(url) { let h = this.health.get(url); if (!h) { h = new RelayHealth(); this.health.set(url, h); } return h; }

  /** החלטת החלפת-מסלול: פונקציה טהורה, ניתנת לבדיקה */
  _hopDecision(report, connected) {
    if (connected < 3) return true;                       // מעט מדי חיבורים
    const suspect = report.filter((r) => r.phi > 6).length;
    return report.length > 0 && suspect / report.length >= 0.5; // רוב הנתיב מתדרדר
  }

  /** מנתח מסלולים בזמן אמת ומחליף לנתיבים חלופיים בעת חסימה/הידרדרות */
  autoHop() {
    if (this.closed) return;
    const report = this.healthReport();
    if (!this._hopDecision(report, this.sockets.size)) return;
    const spare = this.fallback.filter((u) => !this.active.has(u));
    if (!spare.length) return;
    for (const url of shuffle(spare).slice(0, 4)) { this.active.add(url); this._connect(url); }
    this.hops++;
    log(`Θ: החלפת-מסלול דינמית (#${this.hops}) — נוספו נתיבים חלופיים בעקבות הידרדרות/חסימה.`);
  }

  /** סדר הממסרים לפי בריאות (הבריא ביותר ראשון) — לשיגור מועדף */
  healthyUrls() {
    return [...this.sockets.keys()].sort((a, b) => this._h(b).score() - this._h(a).score());
  }

  /** תמונת בריאות הרשת — לניטור עצמי */
  healthReport() {
    return [...this.sockets.keys()].map((u) => {
      const h = this._h(u);
      return { relay: u, latency: Math.round(h.ewma), phi: +h.phi().toFixed(1), score: Math.round(h.score()) };
    }).sort((a, b) => b.score - a.score);
  }

  connectAll() {
    // ערבוב סדר החיבור + ריצוד תזמון — טביעת-רגל מטמורפית בכל הרצה
    shuffle(this.urls).forEach((url, i) => setTimeout(() => this._connect(url), metaJitter(i * 15, 120)));
  }

  _connect(url, attempt = 0) {
    if (this.closed) return;
    let ws;
    try { ws = new WebSocket(url); } catch { return; }
    ws.onopen = () => {
      this.sockets.set(url, ws);
      // רישום מנויים קיימים גם על חיבור חדש
      for (const [subId, sub] of this.subs) {
        try { ws.send(JSON.stringify(['REQ', subId, ...sub.filters])); } catch {}
      }
    };
    ws.onmessage = (m) => {
      this._h(url).observe(); // Θ: הזנת גלאי ה-φ בכל הודעה נכנסת
      let msg;
      try { msg = JSON.parse(m.data); } catch { return; }
      if (msg[0] === 'OK') {
        const p = this.pendingOk.get(msg[1]);
        if (p && msg[2] === true) {
          if (!p.oks.has(url)) this._h(url).latency(Date.now() - (p.sentAt ?? Date.now())); // Θ: זמן-תגובה
          p.oks.add(url);
          if (p.oks.size >= p.target) p.resolve(p.oks.size);
        }
      } else if (msg[0] === 'EVENT') {
        // Σ3: כל אירוע מאומת מתמטית (hash + Schnorr) לפני שנכנס למערכת
        const sub = this.subs.get(msg[1]);
        if (sub && msg[2]) {
          if (!verifyEvent(msg[2])) {
            if (++rejectedEvents % 10 === 1) log(`Σ3: נדחה אירוע עם חתימה/זהות לא תקינה מ-${url}`);
            return;
          }
          sub.onEvent(msg[2], url);
        }
      } else if (msg[0] === 'EOSE') {
        const sub = this.subs.get(msg[1]);
        if (sub) { sub.eose.add(url); sub.onEose?.(sub.eose.size); }
      }
    };
    ws.onclose = () => {
      this.sockets.delete(url);
      if (!this.closed) setTimeout(() => this._connect(url, attempt + 1), Math.min(30_000, 1000 * 2 ** Math.min(attempt, 5)));
    };
    ws.onerror = () => { try { ws.close(); } catch {} };
  }

  async waitForConnections(min = 3, timeoutMs = 10_000) {
    const t0 = Date.now();
    while (this.sockets.size < min && Date.now() - t0 < timeoutMs) await sleep(150);
    return this.sockets.size;
  }

  /** שיגור אירוע לכל הממסרים; מסתיים כש-minOks ממסרים אישרו קליטה */
  publish(ev, timeoutMs = 12_000, minOks = 1) {
    return new Promise((resolve, reject) => {
      const target = Math.min(minOks, Math.max(1, this.sockets.size));
      const entry = {
        oks: new Set(),
        sentAt: Date.now(),
        resolve: (count) => { clearTimeout(timer); this.pendingOk.delete(ev.id); resolve(count); },
        target,
      };
      const timer = setTimeout(() => {
        this.pendingOk.delete(ev.id);
        // Θ: ממסרים שלא אישרו נספרים ככשל — מוריד את ציון הבריאות שלהם
        for (const u of this.sockets.keys()) if (!entry.oks.has(u)) this._h(u).miss();
        entry.oks.size > 0 ? resolve(entry.oks.size) : reject(new Error('no relay accepted event'));
      }, timeoutMs);
      this.pendingOk.set(ev.id, entry);
      const frame = JSON.stringify(['EVENT', ev]);
      let sent = 0;
      // Θ: משגרים קודם לבריאים ביותר (אך לכולם — לחוסן)
      for (const url of this.healthyUrls()) { const ws = this.sockets.get(url); try { ws.send(frame); sent++; } catch {} }
      if (sent === 0) { clearTimeout(timer); this.pendingOk.delete(ev.id); reject(new Error('no open relay connections')); }
    });
  }

  /** משיכה חד-פעמית: מחזיר את האירועים העדכניים ביותר לכל d-tag */
  query(filters, timeoutMs = 9_000) {
    return new Promise((resolve) => {
      const subId = 'q' + hex(crypto.randomBytes(4));
      const latest = new Map(); // dTag -> event
      const finish = () => { this.subs.delete(subId); this._closeSub(subId); resolve([...latest.values()]); };
      const timer = setTimeout(finish, timeoutMs);
      let target = Math.max(1, Math.floor(this.sockets.size / 2));
      this.subs.set(subId, {
        filters,
        eose: new Set(),
        onEvent: (ev) => {
          const d = ev.tags.find((t) => t[0] === 'd')?.[1] ?? '';
          const cur = latest.get(d);
          if (!cur || ev.created_at > cur.created_at) latest.set(d, ev);
        },
        onEose: (count) => { if (count >= target) { clearTimeout(timer); setTimeout(finish, 400); } },
      });
      const frame = JSON.stringify(['REQ', subId, ...filters]);
      for (const ws of this.sockets.values()) { try { ws.send(frame); } catch {} }
    });
  }

  /** מנוי חי — קבלת עדכונים בזמן אמת */
  subscribe(filters, onEvent) {
    const subId = 's' + hex(crypto.randomBytes(4));
    const seen = new Set();
    this.subs.set(subId, {
      filters,
      eose: new Set(),
      onEvent: (ev) => { if (!seen.has(ev.id)) { seen.add(ev.id); onEvent(ev); } },
    });
    const frame = JSON.stringify(['REQ', subId, ...filters]);
    for (const ws of this.sockets.values()) { try { ws.send(frame); } catch {} }
    return subId;
  }

  _closeSub(subId) {
    const frame = JSON.stringify(['CLOSE', subId]);
    for (const ws of this.sockets.values()) { try { ws.send(frame); } catch {} }
  }

  close() {
    this.closed = true;
    for (const ws of this.sockets.values()) { try { ws.close(); } catch {} }
    this.sockets.clear();
  }
}

// ────────────────────────────── מצב מקומי ──────────────────────────────

function loadState() {
  try { return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch { return { files: {}, kv: {} }; }
}
function saveState(state) {
  try { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } catch (err) { log('אזהרה: שמירת מצב נכשלה:', err.message); }
}

async function scanLocal() {
  const files = new Map(); // relPath -> {hash, size, mtime}
  async function walk(dir) {
    let entries;
    try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; }
    for (const e of entries) {
      if (IGNORE.includes(e.name) || e.name.startsWith('.cloud-')) continue;
      const full = path.join(dir, e.name);
      const rel = path.relative(ROOT, full).replace(/\\/g, '/');
      if (e.isDirectory()) await walk(full);
      else if (e.isFile()) {
        try {
          const st = await fsp.stat(full);
          const data = await fsp.readFile(full);
          files.set(rel, { hash: hex(sha256(data)), size: st.size, mtime: Math.floor(st.mtimeMs) });
        } catch {}
      }
    }
  }
  await walk(ROOT);
  return files;
}

// ────────────────────────────── מנוע הסינכרון ──────────────────────────────

const npub = () => bech32Encode('npub', to5bit(unhex(keys.pubkey)));

/** הכתובת הגלובלית הקבועה של הפרויקט — עובדת גם כשכל המכשירים כבויים */
function globalAddress() {
  if (CUSTOM_DOMAIN) return `https://${CUSTOM_DOMAIN}`;
  if (PUBLIC_WEBSITE) return `https://${npub()}.${NSITE_GATEWAYS[0]}`;
  const naddr = makeNaddr(dSite(), keys.pubkey, KIND_SITE, 'wss://relay.damus.io');
  return 'https://yakihonne.com/article/' + naddr;
}

/** כתובות מראה — אותו אתר מוגש מכמה שערים בלתי-תלויים */
function mirrorAddresses() {
  return NSITE_GATEWAYS.map((g) => `https://${npub()}.${g}`);
}

/** הוראות DNS מדויקות לחיבור דומיין אמיתי משלך (למשל www.myproject.io) */
function printDomainSetup() {
  const host = CUSTOM_DOMAIN.replace(/^https?:\/\//, '');
  const label = host.split('.')[0];
  // היעד חייב להיות שער שתומך בדומיין מותאם + תעודת TLS אוטומטית.
  // nsite.lol (מימוש הייחוס של nsite-gateway) מתעד תמיכה ב-CNAME + on-demand TLS.
  const target = `${npub()}.nsite.lol`;
  log('─────────────────────────────────────────────');
  log(`חיבור הדומיין ${host} לאתר המבוזר — הגדרה חד-פעמית ברשם הדומיינים:`);
  log(`  סוג רשומה: CNAME`);
  log(`  שם/Host:   ${label}`);
  log(`  יעד/Value: ${target}`);
  log('אחרי שהרשומה מתעדכנת (עד כמה שעות), האתר יוגש מ:');
  log(`  https://${host}  — עם HTTPS אמיתי, שם קצר, נראה כאתר רגיל.`);
  log('הערה: השער צריך לתמוך בדומיינים מותאמים + TLS אוטומטי (כמו nsite.lol).');
  log('─────────────────────────────────────────────');
}

/**
 * מנוע SkyLattice לדפדפן — מתפרסם אוטומטית כ-/cloud-web.js בכל אתר.
 * הופך את הכתובת הגלובלית למערכת שלמה בלי שום שרת:
 *
 *   נתונים חיים:   SkyCloud.save / load / watch / onChange
 *   משתמשים:       SkyCloud.register / login / logout / user  (כמו Firebase Auth,
 *                  אבל הזהות נגזרת מתמטית — PBKDF2 310K איטרציות -> Schnorr+AES,
 *                  אין מסד משתמשים בשום מקום, אי אפשר לגנוב מה שלא מאוחסן)
 *   אחסון פרטי:    SkyCloud.userSet / userGet  (מוצפן AES-256-GCM לכל משתמש)
 *   קבצים:         SkyCloud.upload(file)  — העלאה מהדפדפן לאחסון המבוזר, URL קבוע
 *   חוות שרתים:    SkyCloud.serve({task: fn}) / SkyCloud.run(task, payload)
 *                  כל טאב פתוח וכל מכשיר Node הם עובדי-חישוב; בחירת המבצע
 *                  דטרמיניסטית (Rendezvous Hashing) — בלי מתאם מרכזי.
 */
function buildWebSdk() {
  return `/* SkyCloud / SkyLattice Web SDK (auto-generated) — decentralized data, users, files, compute. No server. */
(() => {
'use strict';
if (window.SkyCloud) return;
const PROJECT = '${keys.pubkey}';
const RELAYS = ${JSON.stringify(RELAYS.slice(0, 8))};
const KIND = ${KIND_DATA};
const BLOSSOM = ${JSON.stringify(BLOSSOM_SERVERS)};
const API_PREFIX = ${JSON.stringify(API_PREFIX)};
const P16 = PROJECT.slice(0, 16);
const PRE = { kv: 'scweb:' + P16 + ':', usr: 'scusr:' + P16 + ':', ud: 'scud:' + P16 + ':', job: 'scjob:' + P16 + ':', res: 'scres:' + P16 + ':', dm: 'scdm:' + P16 + ':', api: 'scapi:' + P16 + ':', apx: 'scapx:' + P16 + ':', snp: 'scsnp:' + P16 + ':' };
const DPREFIX = PRE.kv;

/* secp256k1 + Schnorr (BIP-340) in pure JS */
const Pf = 2n ** 256n - 2n ** 32n - 977n;
const Nn = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141');
const G = {
  x: BigInt('0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'),
  y: BigInt('0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8'),
};
const mod = (a, m) => ((a % m) + m) % m;
const powmod = (b, e, m) => { let r = 1n; b = mod(b, m); while (e > 0n) { if (e & 1n) r = (r * b) % m; b = (b * b) % m; e >>= 1n; } return r; };
const inv = (a) => powmod(mod(a, Pf), Pf - 2n, Pf);
const ptDouble = (a) => { if (!a) return null; const l = mod(3n * a.x * a.x * inv(2n * a.y), Pf); const x = mod(l * l - 2n * a.x, Pf); return { x, y: mod(l * (a.x - x) - a.y, Pf) }; };
const ptAdd = (a, b) => { if (!a) return b; if (!b) return a; if (a.x === b.x) return a.y === b.y ? ptDouble(a) : null; const l = mod((b.y - a.y) * inv(mod(b.x - a.x, Pf)), Pf); const x = mod(l * l - a.x - b.x, Pf); return { x, y: mod(l * (a.x - x) - a.y, Pf) }; };
const ptMul = (pt, k) => { let r = null, a = pt; while (k > 0n) { if (k & 1n) r = ptAdd(r, a); a = ptDouble(a); k >>= 1n; } return r; };

const enc = new TextEncoder();
const dec = new TextDecoder();
const concatB = (arrs) => { const t = arrs.reduce((s, a) => s + a.length, 0); const out = new Uint8Array(t); let o = 0; for (const a of arrs) { out.set(a, o); o += a.length; } return out; };
const sha256 = async (...chunks) => new Uint8Array(await crypto.subtle.digest('SHA-256', concatB(chunks)));
const hex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, '0')).join('');
const unhex = (s) => new Uint8Array(s.match(/../g).map((h) => parseInt(h, 16)));
const big = (u8) => BigInt('0x' + hex(u8));
const b32 = (n) => unhex(n.toString(16).padStart(64, '0'));
const taggedHash = async (tag, ...data) => { const th = await sha256(enc.encode(tag)); return sha256(th, th, ...data); };
const xorB = (a, b) => a.map((v, i) => v ^ b[i]);
const b64 = (u8) => { let s = ''; for (let i = 0; i < u8.length; i += 8192) s += String.fromCharCode.apply(null, u8.subarray(i, i + 8192)); return btoa(s); };
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));

async function schnorrSign(msg32, dIn) {
  let d = dIn;
  const Pub = ptMul(G, d);
  if (Pub.y % 2n !== 0n) d = Nn - d;
  const px = b32(Pub.x);
  const aux = crypto.getRandomValues(new Uint8Array(32));
  const t = xorB(b32(d), await taggedHash('BIP0340/aux', aux));
  const k0 = mod(big(await taggedHash('BIP0340/nonce', t, px, msg32)), Nn);
  const R = ptMul(G, k0);
  const k = R.y % 2n === 0n ? k0 : Nn - k0;
  const rx = b32(R.x);
  const e = mod(big(await taggedHash('BIP0340/challenge', rx, px, msg32)), Nn);
  return concatB([rx, b32(mod(k + e * d, Nn))]);
}

/* Σ3: אימות Schnorr מלא של כל אירוע נכנס — אפס אמון בממסרים */
function liftX(x) {
  if (x >= Pf) return null;
  const c = mod(x * x * x + 7n, Pf);
  const y = powmod(c, (Pf + 1n) / 4n, Pf);
  if ((y * y) % Pf !== c) return null;
  return { x, y: y % 2n === 0n ? y : Pf - y };
}
const verified = new Set();
async function verifyEvent(ev) {
  try {
    if (!ev || typeof ev.id !== 'string' || typeof ev.sig !== 'string' || typeof ev.pubkey !== 'string') return false;
    /* בדיקת ה-hash רצה תמיד — מונעת replay של תוכן זר תחת id מאומת */
    const calc = hex(await sha256(enc.encode(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content]))));
    if (calc !== ev.id) return false;
    if (verified.has(ev.id)) return true;
    const sig = unhex(ev.sig);
    if (sig.length !== 64) return false;
    const r = big(sig.slice(0, 32));
    const s = big(sig.slice(32));
    if (r >= Pf || s >= Nn) return false;
    const Ppt = liftX(big(unhex(ev.pubkey)));
    if (!Ppt) return false;
    const e = mod(big(await taggedHash('BIP0340/challenge', sig.slice(0, 32), unhex(ev.pubkey), unhex(ev.id))), Nn);
    const R = ptAdd(ptMul(G, s), ptMul(Ppt, Nn - e));
    if (!R || R.y % 2n !== 0n || R.x !== r) return false;
    if (verified.size > 4000) verified.clear();
    verified.add(ev.id);
    return true;
  } catch { return false; }
}

/* ─── מאגרי נתונים חיים ─── */
const sockets = new Set();
const kv = {};        /* נתונים משותפים (ציבורי) */
const usersReg = {};  /* name -> {pub, ts} — הרשמה מוקדמת מנצחת */
const userData = {};  /* pub16:key -> {iv, ct, ts} — מוצפן פר-משתמש */
const results = {};   /* jobId -> תוצאת חישוב */
const resWaiters = {};
const apiDone = {};   /* Ω: reqId -> תגובת API */
const apiWaiters = {};
const snaps = {};     /* Ω: snapHash -> תמונת-API סטטית (נענית גם ללא עובד חי) */
const apiSeen = new Set();
let apiRoutes = null; /* Ω: נתיבי-שרת שהטאב הזה מגיש (אם הופעל serveApi) */
let handlers = null;
const dms = [];       /* הודעות מוצפנות מקצה-לקצה (Ψ) שפוענחו */
const rawDms = [];    /* מעטפות גולמיות — נסרקות שוב אחרי login */
const dmSeen = new Set();
const dmWatchers = [];
const globalWatchers = [];
const keyWatchers = {};
let readyResolve;
const ready = new Promise((r) => { readyResolve = r; });

/* ─── זהות: מבקר אנונימי כברירת מחדל; משתמש רשום אחרי login ─── */
const storeKey = 'skycloud_visitor_' + PROJECT.slice(0, 8);
const sessKey = 'skycloud_user_' + PROJECT.slice(0, 8);
let seedHex = null;
try { seedHex = localStorage.getItem(storeKey); } catch {}
if (!seedHex || seedHex.length !== 64) {
  seedHex = hex(crypto.getRandomValues(new Uint8Array(32)));
  try { localStorage.setItem(storeKey, seedHex); } catch {}
}
const visitorPriv = mod(big(unhex(seedHex)), Nn - 1n) + 1n;
const visitorPub = hex(b32(ptMul(G, visitorPriv).x));
let cur = { priv: visitorPriv, pub: visitorPub, aes: null, name: null };

async function makeEvent(tags, content, kind) {
  const ev = { pubkey: cur.pub, created_at: Math.floor(Date.now() / 1000), kind: kind || KIND, tags, content };
  const idBytes = await sha256(enc.encode(JSON.stringify([0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content])));
  ev.id = hex(idBytes);
  ev.sig = hex(await schnorrSign(idBytes, cur.priv));
  return ev;
}
function publish(ev) {
  const frame = JSON.stringify(['EVENT', ev]);
  let sent = 0;
  for (const ws of sockets) { try { ws.send(frame); sent++; } catch {} }
  if (!sent) throw new Error('SkyCloud: no relay connection');
  return ev.id;
}

/* ─── מיזוג אירועים מהרשת: נתונים / משתמשים / אחסון פרטי / חישוב ─── */
function merge(ev, notify) {
  const d = (ev.tags.find((t) => t[0] === 'd') || [])[1] || '';
  let entry;
  try { entry = JSON.parse(ev.content); } catch { return; }
  if (!entry || typeof entry.ts !== 'number') return;
  if (d.indexOf(PRE.kv) === 0) {
    const key = d.slice(PRE.kv.length);
    const c = kv[key];
    if (c && c.ts >= entry.ts) return;
    kv[key] = entry;
    if (!notify) return;
    for (const fn of globalWatchers) { try { fn(key, entry.v, ev.pubkey); } catch {} }
    for (const fn of keyWatchers[key] || []) { try { fn(entry.v, ev.pubkey); } catch {} }
  } else if (d.indexOf(PRE.usr) === 0) {
    const nm = String(entry.name || '').toLowerCase();
    const c = usersReg[nm];
    /* ההרשמה המוקדמת ביותר מנצחת — אי אפשר להשתלט על שם תפוס */
    if (!c || entry.ts < c.ts) usersReg[nm] = { pub: ev.pubkey, ts: entry.ts };
  } else if (d.indexOf(PRE.ud) === 0) {
    const k = d.slice(PRE.ud.length);
    const c = userData[k];
    if (!c || entry.ts > c.ts) userData[k] = entry;
  } else if (d.indexOf(PRE.res) === 0) {
    const id = d.slice(PRE.res.length);
    if (!results[id] || entry.ts > results[id].ts) results[id] = entry;
    const w = resWaiters[id];
    if (w) { delete resWaiters[id]; w(results[id]); }
  } else if (d.indexOf(PRE.job) === 0) {
    const id = d.slice(PRE.job.length);
    if (notify && handlers && !results[id] && Date.now() - entry.ts < 60000) maybeRun(id, entry);
  } else if (d.indexOf(PRE.apx) === 0) {
    /* Ω: תגובת API חזרה מעובד-שרת מבוזר */
    const id = d.slice(PRE.apx.length);
    if (!apiDone[id]) { apiDone[id] = entry; const w = apiWaiters[id]; if (w) { delete apiWaiters[id]; w(entry); } }
  } else if (d.indexOf(PRE.snp) === 0) {
    /* Ω: תמונת-API סטטית — נשמרת לשירות ללא-מפעיל (LWW לפי ts) */
    const h = d.slice(PRE.snp.length);
    if (!snaps[h] || entry.ts > snaps[h].ts) snaps[h] = entry;
  } else if (d.indexOf(PRE.api) === 0) {
    /* Ω: בקשת API — אם הטאב הזה מגיש נתיבים, הוא עובד-שרת בעצמו */
    if (notify && apiRoutes && entry.from !== cur.pub && Date.now() - entry.ts < 30000) serveApiReq(d.slice(PRE.api.length), entry);
  } else if (d.indexOf(PRE.dm) === 0) {
    rawDms.push({ ev, d, env: entry });
    handleDm(ev, d, entry, notify);
  }
}

/* ─── Ψ: "ערבוב הצבעים" — סוד משותף ECDH: (dA·PB).x == (dB·PA).x ─── */
async function ecdhKey(pubHex) {
  const Ppt = liftX(big(unhex(pubHex)));
  if (!Ppt) throw new Error('SkyCloud: bad recipient id');
  const S = ptMul(Ppt, cur.priv);
  const raw = await sha256(enc.encode('skylattice-psi:v1:'), b32(S.x));
  return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
}
async function handleDm(ev, d, env, notify) {
  try {
    if (typeof env.ct !== 'string' || typeof env.to !== 'string') return;
    const mine = env.to === cur.pub;
    const fromMe = ev.pubkey === cur.pub;
    if (!mine && !fromMe) return; /* לא מיועד לזהות הנוכחית */
    const seenKey = d + ':' + cur.pub;
    if (dmSeen.has(seenKey)) return;
    const key = await ecdhKey(mine ? ev.pubkey : env.to);
    const raw = unb64(env.ct);
    const pt = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv: raw.slice(0, 12), additionalData: enc.encode('sc-psi:' + d) },
      key, raw.slice(12));
    const payload = JSON.parse(dec.decode(new Uint8Array(pt)));
    dmSeen.add(seenKey);
    const msg = { from: ev.pubkey, to: env.to, value: payload.v, ts: payload.ts };
    dms.push(msg);
    dms.sort((a, b) => a.ts - b.ts);
    if (notify) for (const fn of dmWatchers) { try { fn(msg); } catch {} }
  } catch {} /* פענוח נכשל = לא שלנו / חבלה */
}

/* בחירת מבצע דטרמיניסטית (Rendezvous Hashing): לכל עובד ציון sha256(worker:job);
   ציון נמוך רץ מוקדם יותר, והראשון שמפרסם תוצאה עוצר את כל השאר — בלי מתאם מרכזי */
async function maybeRun(id, job) {
  const fn = handlers[job.task];
  if (!fn) return;
  const rank = Number(big(await sha256(enc.encode(cur.pub + ':' + id))) % 1500n);
  await new Promise((r) => setTimeout(r, 450 + rank));
  if (results[id]) return; /* עובד אחר (מכשיר Node או טאב) כבר ענה */
  let res;
  try { res = { ok: true, value: await fn(job.payload, job) }; }
  catch (e) { res = { ok: false, error: String((e && e.message) || e) }; }
  res.ts = Date.now();
  res.worker = cur.pub;
  results[id] = res;
  try { publish(await makeEvent([['d', PRE.res + id], ['p', PROJECT]], JSON.stringify(res))); } catch {}
}

/* Ω: hash-נתיב לתמונה סטטית — זהה לצד ה-Node (sha256('snap:'+path), 24 hex) */
async function snapHash(path) { return hex(await sha256(enc.encode('snap:' + path))).slice(0, 24); }

/* Ω: התאמת נתיב מבין הנתיבים שהטאב הזה מגיש (serveApi) */
function matchApiRoute(method, pathname) {
  for (const r of apiRoutes) {
    if (r.method !== method && r.method !== 'ALL') continue;
    if (r.path instanceof RegExp) { const m = pathname.match(r.path); if (m) return { r, params: m.groups || {} }; }
    else if (r.path === pathname) return { r, params: {} };
  }
  return null;
}

/* Ω: הטאב הזה עונה כעובד-שרת — בחירת מבצע דטרמיניסטית (Rendezvous) */
async function serveApiReq(id, req) {
  if (apiSeen.has(id)) return;
  const rank = Number(big(await sha256(enc.encode(cur.pub + ':' + id))) % 700n);
  await new Promise((r) => setTimeout(r, 150 + rank));
  if (apiSeen.has(id)) return;
  apiSeen.add(id); if (apiSeen.size > 3000) apiSeen.clear();
  const pathname = String(req.path || '').split('?')[0];
  const hit = matchApiRoute(req.method, pathname);
  let res;
  if (!hit) res = { status: 404, headers: { 'content-type': 'application/json' }, body: b64(enc.encode(JSON.stringify({ error: 'no route', path: req.path }))) };
  else {
    try {
      const bodyBytes = req.body ? unb64(req.body) : new Uint8Array(0);
      const out = await hit.r.fn({ method: req.method, path: req.path, headers: req.headers || {}, params: hit.params, body: bodyBytes, json: () => JSON.parse(dec.decode(bodyBytes) || 'null'), text: () => dec.decode(bodyBytes) });
      let status = 200, headers = { 'content-type': 'application/json' }, payload = out;
      if (out && typeof out === 'object' && ('status' in out || 'body' in out || 'headers' in out)) { status = out.status || 200; headers = out.headers || headers; payload = out.body; }
      let bytes;
      if (payload instanceof Uint8Array) bytes = payload;
      else if (typeof payload === 'string') bytes = enc.encode(payload);
      else { bytes = enc.encode(JSON.stringify(payload == null ? null : payload)); headers['content-type'] = headers['content-type'] || 'application/json'; }
      res = { status, headers, body: b64(bytes) };
    } catch (e) { res = { status: 500, headers: { 'content-type': 'application/json' }, body: b64(enc.encode(JSON.stringify({ error: String((e && e.message) || e) }))) }; }
  }
  res.ts = Date.now(); res.worker = cur.pub;
  try { publish(await makeEvent([['d', PRE.apx + id], ['p', PROJECT]], JSON.stringify(res))); } catch {}
}

/* ─── משתמשים: הזהות נגזרת מתמטית משם+סיסמה, לא מאוחסנת בשום מקום ─── */
async function deriveUser(name, pass) {
  const nm = String(name || '').trim().toLowerCase();
  if (!nm || !pass) throw new Error('SkyCloud: username and password required');
  const km = await crypto.subtle.importKey('raw', enc.encode(String(pass)), 'PBKDF2', false, ['deriveBits']);
  const bits = new Uint8Array(await crypto.subtle.deriveBits(
    { name: 'PBKDF2', hash: 'SHA-256', salt: enc.encode('skylattice:u1:' + PROJECT + ':' + nm), iterations: 310000 },
    km, 512));
  const priv = mod(big(bits.slice(0, 32)), Nn - 1n) + 1n;
  const pub = hex(b32(ptMul(G, priv).x));
  return { nm, bits, pub };
}
async function activate(nm, bits) {
  const priv = mod(big(bits.slice(0, 32)), Nn - 1n) + 1n;
  const pub = hex(b32(ptMul(G, priv).x));
  const aes = await crypto.subtle.importKey('raw', bits.slice(32), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
  cur = { priv, pub, aes, name: nm };
  try { localStorage.setItem(sessKey, JSON.stringify({ n: nm, b: hex(bits) })); } catch {}
  /* Ψ: סריקה מחדש של מעטפות מוצפנות — הודעות שממתינות לזהות הזאת */
  for (const r of rawDms) handleDm(r.ev, r.d, r.env, true);
  return { name: nm, id: pub };
}
function toVisitor() {
  cur = { priv: visitorPriv, pub: visitorPub, aes: null, name: null };
  try { localStorage.removeItem(sessKey); } catch {}
}
/* שחזור התחברות מביקור קודם */
try {
  const s = JSON.parse(localStorage.getItem(sessKey) || 'null');
  if (s && s.n && s.b && s.b.length === 128) activate(s.n, unhex(s.b));
} catch {}

/* ─── העלאת קבצים מהדפדפן לאחסון המבוזר (BUD-02, חתום) ─── */
async function upload(data, type) {
  let bytes;
  if (typeof Blob !== 'undefined' && data instanceof Blob) { type = type || data.type; bytes = new Uint8Array(await data.arrayBuffer()); }
  else if (typeof data === 'string') { bytes = enc.encode(data); type = type || 'text/plain'; }
  else if (data instanceof Uint8Array) bytes = data;
  else bytes = new Uint8Array(data);
  type = type || 'application/octet-stream';
  const hash = hex(await sha256(bytes));
  const auth = await makeEvent([['t', 'upload'], ['x', hash], ['expiration', String(Math.floor(Date.now() / 1000) + 600)]], 'upload blob', 24242);
  const hdr = 'Nostr ' + btoa(JSON.stringify(auth));
  const urls = [];
  await Promise.allSettled(BLOSSOM.map(async (s) => {
    try {
      const head = await fetch(s + '/' + hash, { method: 'HEAD' });
      if (!head.ok) {
        const r = await fetch(s + '/upload', { method: 'PUT', headers: { Authorization: hdr, 'Content-Type': type }, body: bytes });
        if (!r.ok) return;
      }
      urls.push(s + '/' + hash);
    } catch {}
  }));
  if (!urls.length) throw new Error('SkyCloud: upload failed on all storage servers');
  return { hash, url: urls[0], urls, size: bytes.length, type };
}

/* ─── חיבור לממסרי הרשת ─── */
function connect(url) {
  let ws;
  try { ws = new WebSocket(url); } catch { return; }
  ws.onopen = () => {
    sockets.add(ws);
    ws.send(JSON.stringify(['REQ', 'scw', { kinds: [KIND], '#p': [PROJECT] }]));
  };
  ws.onmessage = async (m) => {
    let msg;
    try { msg = JSON.parse(m.data); } catch { return; }
    if (msg[0] === 'EVENT' && msg[2]) {
      if (await verifyEvent(msg[2])) merge(msg[2], true); /* Σ3: זיוף נדחה מתמטית */
    } else if (msg[0] === 'EOSE') setTimeout(readyResolve, 300); /* מרווח לאימותים אסינכרוניים שבדרך */
  };
  ws.onclose = () => { sockets.delete(ws); setTimeout(() => connect(url), 3000 + Math.random() * 4000); };
  ws.onerror = () => { try { ws.close(); } catch {} };
}
RELAYS.forEach(connect);
setTimeout(() => readyResolve(), 8000); /* גם בלי EOSE — לא נתקעים */

/* ─── Ω: שרת אפליקציה חי — כל fetch('/api/…') מנותב לרשת המבוזרת ─── */
async function apiCall(path, init) {
  await ready;
  init = init || {};
  const method = (init.method || 'GET').toUpperCase();
  let bodyB64 = '';
  if (init.body != null) {
    const b = typeof init.body === 'string' ? enc.encode(init.body)
      : (init.body instanceof Uint8Array ? init.body : enc.encode(JSON.stringify(init.body)));
    bodyB64 = b64(b);
  }
  const id = hex(crypto.getRandomValues(new Uint8Array(8)));
  const sh = await snapHash(path);
  const req = { method, path, headers: init.headers || {}, body: bodyB64, ts: Date.now(), from: cur.pub };
  publish(await makeEvent([['d', PRE.api + id], ['p', PROJECT]], JSON.stringify(req)));
  const res = await new Promise((resolve, reject) => {
    let done = false;
    const finish = (r) => { if (!done) { done = true; clearTimeout(hard); clearTimeout(fb); delete apiWaiters[id]; resolve(r); } };
    /* מנצח: עובד-שרת חי אם ענה מהר; אחרת נופלים לתמונה הסטטית מהרשת (ללא-מפעיל) */
    const hard = setTimeout(() => { if (done) return; done = true; delete apiWaiters[id]; if (snaps[sh]) resolve(snaps[sh]); else reject(new Error('SkyCloud: no server peer / snapshot for ' + path)); }, init.timeoutMs || 25000);
    const fb = setTimeout(() => { if (!done && (method === 'GET' || method === 'HEAD') && snaps[sh]) finish(snaps[sh]); }, init.staticFallbackMs || 2200);
    if (apiDone[id]) return finish(apiDone[id]);
    apiWaiters[id] = finish;
  });
  let bodyBytes;
  if (res.blob) bodyBytes = new Uint8Array(await (await fetch(res.blob)).arrayBuffer());
  else bodyBytes = res.body ? unb64(res.body) : new Uint8Array(0);
  return new Response(bodyBytes, { status: res.status || 200, headers: res.headers || {} });
}
/* מחליף את fetch לנתיבי API באותו origin — מערכות קיימות "פשוט עובדות" */
if (typeof window !== 'undefined' && window.fetch && !window.__skyFetch) {
  window.__skyFetch = window.fetch.bind(window);
  window.fetch = (input, init) => {
    try {
      const url = typeof input === 'string' ? input : (input && input.url) || '';
      const u = new URL(url, location.href);
      if (u.origin === location.origin && u.pathname.indexOf(API_PREFIX) === 0) {
        return apiCall(u.pathname + u.search, Object.assign({ method: (input && input.method) || 'GET' }, init));
      }
    } catch {}
    return window.__skyFetch(input, init);
  };
}

window.SkyCloud = {
  ready,
  visitor: visitorPub,
  get user() { return cur.name ? { name: cur.name, id: cur.pub } : null; },

  /* נתונים חיים משותפים */
  load: (key) => (kv[key] || {}).v,
  all: () => { const o = {}; for (const k in kv) o[k] = kv[k].v; return o; },
  save: async (key, value) => {
    const entry = { v: value, ts: Date.now() };
    merge({ tags: [['d', PRE.kv + key]], content: JSON.stringify(entry), pubkey: cur.pub }, true);
    return publish(await makeEvent([['d', PRE.kv + key], ['p', PROJECT]], JSON.stringify(entry)));
  },
  watch: (key, fn) => { (keyWatchers[key] = keyWatchers[key] || []).push(fn); },
  onChange: (fn) => { globalWatchers.push(fn); },

  /* משתמשים — כמו Firebase Auth, בלי שרת ובלי מסד סיסמאות */
  register: async (name, pass) => {
    await ready;
    const u = await deriveUser(name, pass);
    const taken = usersReg[u.nm];
    if (taken && taken.pub !== u.pub) throw new Error('SkyCloud: username already taken');
    const out = await activate(u.nm, u.bits);
    publish(await makeEvent([['d', PRE.usr + cur.pub], ['p', PROJECT]], JSON.stringify({ name: u.nm, ts: Date.now() })));
    return out;
  },
  login: async (name, pass) => {
    await ready;
    const u = await deriveUser(name, pass);
    const reg = usersReg[u.nm];
    if (!reg) throw new Error('SkyCloud: unknown username (or not yet synced)');
    if (reg.pub !== u.pub) throw new Error('SkyCloud: wrong password');
    return activate(u.nm, u.bits);
  },
  logout: () => { toVisitor(); },
  usersCount: () => Object.keys(usersReg).length,

  /* אחסון פרטי מוצפן לכל משתמש (AES-256-GCM, מפתח נגזר מהסיסמה) */
  userSet: async (key, value) => {
    if (!cur.aes) throw new Error('SkyCloud: login required');
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const ct = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cur.aes, enc.encode(JSON.stringify(value))));
    const entry = { iv: hex(iv), ct: b64(ct), ts: Date.now() };
    userData[cur.pub.slice(0, 16) + ':' + key] = entry;
    return publish(await makeEvent([['d', PRE.ud + cur.pub.slice(0, 16) + ':' + key], ['p', PROJECT]], JSON.stringify(entry)));
  },
  userGet: async (key) => {
    if (!cur.aes) throw new Error('SkyCloud: login required');
    const e = userData[cur.pub.slice(0, 16) + ':' + key];
    if (!e) return undefined;
    try {
      const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: unhex(e.iv) }, cur.aes, unb64(e.ct));
      return JSON.parse(dec.decode(pt));
    } catch { return undefined; }
  },

  /* קבצים — העלאה מהדפדפן, URL קבוע באחסון המבוזר */
  upload,

  /* הודעות מוצפנות מקצה-לקצה (Ψ) — אף אחד בדרך לא יכול לקרוא */
  owner: PROJECT,
  sendTo: async (recipientId, value) => {
    await ready;
    const d = PRE.dm + hex(crypto.getRandomValues(new Uint8Array(9)));
    const key = await ecdhKey(recipientId);
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const ct = new Uint8Array(await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv, additionalData: enc.encode('sc-psi:' + d) },
      key, enc.encode(JSON.stringify({ v: value, ts: Date.now() }))));
    const body = new Uint8Array(12 + ct.length);
    body.set(iv, 0); body.set(ct, 12);
    return publish(await makeEvent([['d', d], ['p', PROJECT]], JSON.stringify({ to: recipientId, ct: b64(body), ts: Date.now() })));
  },
  onMessage: (fn) => { dmWatchers.push(fn); },
  messages: () => dms.slice(),
  messagesWith: (id) => dms.filter((m) => m.from === id || m.to === id),

  /* שרת אפליקציה חי (Ω): קריאת API מפורשת דרך הרשת המבוזרת */
  api: apiCall,

  /* חוות השרתים המבוזרת — כל טאב וכל מכשיר הם עובדי-חישוב */
  run: async (task, payload, timeoutMs) => {
    await ready;
    const id = hex(crypto.getRandomValues(new Uint8Array(8)));
    publish(await makeEvent([['d', PRE.job + id], ['p', PROJECT]], JSON.stringify({ task, payload, ts: Date.now(), from: cur.pub })));
    return new Promise((resolve, reject) => {
      const t = setTimeout(() => { delete resWaiters[id]; reject(new Error('SkyCloud: compute timeout for ' + task)); }, timeoutMs || 20000);
      const settle = (r) => { clearTimeout(t); r.ok ? resolve(r.value) : reject(new Error(r.error)); };
      if (results[id]) return settle(results[id]); /* התוצאה כבר הגיעה — לא נתקעים */
      resWaiters[id] = settle;
    });
  },
  serve: (h) => { handlers = Object.assign(handlers || {}, h); },

  /* Ω: הטאב הזה הופך לעובד-שרת — כל מבקר מריץ את ה-backend, ללא מפעיל ייעודי.
     defs: { 'GET /api/ping': fn, 'POST /api/echo': fn }  (fn מקבל req ומחזיר ערך/‏{status,headers,body}) */
  serveApi: (defs) => {
    apiRoutes = apiRoutes || [];
    for (const k in defs) {
      const sp = k.trim().split(/\s+/);
      const method = sp.length > 1 ? sp[0].toUpperCase() : 'GET';
      const path = sp.length > 1 ? sp[1] : sp[0];
      apiRoutes.push({ method, path, fn: defs[k] });
    }
    return window.SkyCloud;
  },
  /* פרסום תמונת-API סטטית מהדפדפן — נענית מהרשת גם ללא אף עובד חי */
  snapshot: async (path, value) => {
    await ready;
    let status = 200, headers = { 'content-type': 'application/json' }, payload = value;
    if (value && typeof value === 'object' && ('status' in value || 'body' in value || 'headers' in value)) { status = value.status || 200; headers = value.headers || headers; payload = value.body; }
    let bytes;
    if (payload instanceof Uint8Array) bytes = payload;
    else if (typeof payload === 'string') bytes = enc.encode(payload);
    else { bytes = enc.encode(JSON.stringify(payload == null ? null : payload)); }
    const entry = { status, headers, body: b64(bytes), ts: Date.now(), snap: true };
    const h = await snapHash(path);
    snaps[h] = entry;
    return publish(await makeEvent([['d', PRE.snp + h], ['p', PROJECT]], JSON.stringify(entry)));
  },
};
})();
`;
}

// ─────────────── Blossom — אחסון קבצי האתר ברשת המבוזרת (BUD-01/02) ───────────────

const MIME = {
  html: 'text/html', htm: 'text/html', css: 'text/css', js: 'text/javascript',
  mjs: 'text/javascript', json: 'application/json', txt: 'text/plain',
  md: 'text/markdown', xml: 'application/xml', svg: 'image/svg+xml',
  png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
  webp: 'image/webp', ico: 'image/x-icon', pdf: 'application/pdf',
  mp3: 'audio/mpeg', mp4: 'video/mp4', woff: 'font/woff', woff2: 'font/woff2',
};
const mimeOf = (p) => MIME[p.split('.').pop()?.toLowerCase()] ?? 'application/octet-stream';

/** אישור חתום להעלאה (kind 24242) — כך שרתי Blossom מזהים את הבעלים */
function blossomAuth(action, hash) {
  const ev = signEvent(24242, [
    ['t', action],
    ['x', hash],
    ['expiration', String(now() + 600)],
  ], action + ' blob');
  return 'Nostr ' + Buffer.from(JSON.stringify(ev)).toString('base64');
}

/** הרצת פקודת הבנייה של הפרויקט בטרמינל (כמו Vercel/Firebase) */
let buildRunning = null;
let buildQuietUntil = 0; // חלון שבו צופה הקבצים מתעלם משינויים שהבנייה עצמה יצרה
const isBuildQuiet = () => Date.now() < buildQuietUntil;
function runBuild() {
  if (!BUILD_COMMAND) return Promise.resolve();
  if (buildRunning) return buildRunning;
  log(`מריץ בנייה: ${BUILD_COMMAND}`);
  buildQuietUntil = Date.now() + 600_000; // ננעל עד שהבנייה מסתיימת (+מרווח)
  buildRunning = new Promise((resolve) => {
    exec(BUILD_COMMAND, { cwd: ROOT, timeout: 300_000, windowsHide: true }, (err, stdout, stderr) => {
      buildRunning = null;
      buildQuietUntil = Date.now() + 5_000;
      if (err) {
        log('אזהרה: הבנייה נכשלה:', (stderr || err.message).trim().split('\n').slice(-3).join(' | '));
      } else {
        log('הבנייה הושלמה.');
      }
      resolve();
    });
  });
  return buildRunning;
}

/** בדיקה אם הקובץ כבר קיים בשרת (לפי hash) */
async function blossomHas(server, hash) {
  try {
    const res = await fetch(`${server}/${hash}`, { method: 'HEAD', signal: AbortSignal.timeout(8000) });
    return res.ok;
  } catch { return false; }
}

/** העלאת קובץ לכמה שרתי Blossom במקביל; מצליח אם לפחות אחד קלט */
async function blossomUpload(data, contentType) {
  const hash = hex(sha256(data));
  const results = await Promise.allSettled(BLOSSOM_SERVERS.map(async (server) => {
    if (await blossomHas(server, hash)) return server;
    const res = await fetch(`${server}/upload`, {
      method: 'PUT',
      headers: { Authorization: blossomAuth('upload', hash), 'Content-Type': contentType },
      body: data,
      signal: AbortSignal.timeout(45_000),
    });
    if (!res.ok) throw new Error(`${server} -> HTTP ${res.status}`);
    return server;
  }));
  const okServers = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
  if (okServers.length === 0) {
    const reasons = results.map((r) => r.reason?.message).filter(Boolean).join(' | ');
    throw new Error('אף שרת אחסון לא קיבל את הקובץ: ' + reasons);
  }
  return { hash, servers: okServers };
}

// ═══════════ SkyLattice-Δ: שכבת הריפוי העצמי — אלגברה מעל שדה גלואה GF(2^8) ═══════════
// Δ1: קידוד מחיקה Reed-Solomon עם מטריצת Cauchy — כל קובץ מקבל בלוקי-זוגיות
//     מתמטיים; חלקים שאובדים ברשת משוחזרים מהמתמטיקה עצמה. תכונת המפתח של
//     מטריצת Cauchy: כל תת-מטריצה ריבועית שלה הפיכה, ולכן כל צירוף של k בלוקים
//     (מקוריים או זוגיות) מספיק מתמטית לשחזור מלא — מובטח, לא הסתברותי.
// Δ2: פיצול סוד של שמיר — ה-SYNC_ID מפוצל ל-N רסיסים בעזרת פולינום אקראי
//     מעל GF(2^8); כל K רסיסים משחזרים (אינטרפולציית לגראנז'), K-1 לא חושפים דבר.

const GF_EXP = new Uint8Array(512);
const GF_LOG = new Uint8Array(256);
(() => {
  let x = 1;
  for (let i = 0; i < 255; i++) {
    GF_EXP[i] = x; GF_LOG[x] = i;
    x <<= 1; if (x & 0x100) x ^= 0x11d; // הפולינום הפרימיטיבי של AES/QR
  }
  for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];
})();
const gfMul = (a, b) => (a && b) ? GF_EXP[GF_LOG[a] + GF_LOG[b]] : 0;
const gfInv = (a) => GF_EXP[255 - GF_LOG[a]];
const gfDiv = (a, b) => a ? GF_EXP[(GF_LOG[a] - GF_LOG[b] + 255) % 255] : 0;

/** שורת מטריצת Cauchy עבור בלוק זוגיות j מול k בלוקי מקור: 1/(x_j XOR y_i) */
const cauchyRow = (j, k) => {
  const row = new Uint8Array(k);
  for (let i = 0; i < k; i++) row[i] = gfInv((200 + j) ^ i); // x=200..255, y=0..199 — זרים תמיד
  return row;
};

/** Δ1: חישוב m בלוקי זוגיות מ-k בלוקי מקור (כולם באורך זהה) */
function rsEncode(shards, m) {
  const k = shards.length;
  const len = shards[0].length;
  const parity = [];
  for (let j = 0; j < m; j++) {
    const row = cauchyRow(j, k);
    const p = Buffer.alloc(len);
    for (let i = 0; i < k; i++) {
      const c = row[i];
      if (!c) continue;
      const s = shards[i];
      for (let b = 0; b < len; b++) p[b] ^= gfMul(c, s[b]);
    }
    parity.push(p);
  }
  return parity;
}

/**
 * Δ1: שחזור בלוקי מקור חסרים מכל צירוף של k בלוקים זמינים.
 * shards: מערך באורך k+m (מקור ואז זוגיות), null = אבד. מחזיר את k המקוריים או null.
 */
function rsDecode(k, shards, len) {
  const present = [];
  for (let i = 0; i < shards.length && present.length < k; i++) {
    if (shards[i]) present.push(i);
  }
  if (present.length < k) return null; // אבדו יותר בלוקים מכמות הזוגיות — בלתי אפשרי מתמטית
  if (present.every((i) => i < k)) return shards.slice(0, k); // כל המקור קיים

  // בניית מערכת משוואות: שורת יחידה לכל בלוק מקור קיים, שורת Cauchy לכל זוגיות
  const rows = present.map((i) => {
    if (i < k) { const r = new Uint8Array(k); r[i] = 1; return r; }
    return Uint8Array.from(cauchyRow(i - k, k));
  });
  const vals = present.map((i) => Buffer.from(shards[i]));

  // חילוץ גאוס מעל GF(2^8) — חיבור הוא XOR, כפל דרך טבלאות לוגריתם
  for (let col = 0; col < k; col++) {
    let piv = -1;
    for (let r = col; r < k; r++) if (rows[r][col]) { piv = r; break; }
    if (piv < 0) return null;
    [rows[col], rows[piv]] = [rows[piv], rows[col]];
    [vals[col], vals[piv]] = [vals[piv], vals[col]];
    const inv = gfInv(rows[col][col]);
    for (let c = 0; c < k; c++) rows[col][c] = gfMul(rows[col][c], inv);
    for (let b = 0; b < len; b++) vals[col][b] = gfMul(vals[col][b], inv);
    for (let r = 0; r < k; r++) {
      if (r === col || !rows[r][col]) continue;
      const f = rows[r][col];
      for (let c = 0; c < k; c++) rows[r][c] ^= gfMul(f, rows[col][c]);
      for (let b = 0; b < len; b++) vals[r][b] ^= gfMul(f, vals[col][b]);
    }
  }
  return vals;
}

/** Δ2: פיצול סוד ל-n רסיסים, כל k משחזרים (פולינום אקראי מדרגה k-1 לכל בית) */
function shamirSplit(secret, n, k) {
  if (k < 2 || n < k || n > 255) throw new Error('נדרש: 2 <= k <= n <= 255');
  const shares = Array.from({ length: n }, (_, i) => ({ x: i + 1, y: Buffer.alloc(secret.length) }));
  for (let p = 0; p < secret.length; p++) {
    const coef = [secret[p], ...crypto.randomBytes(k - 1)];
    for (const sh of shares) {
      let y = 0, xp = 1;
      for (let c = 0; c < k; c++) { y ^= gfMul(coef[c], xp); xp = gfMul(xp, sh.x); }
      sh.y[p] = y;
    }
  }
  return shares.map((sh) => `SCS${sh.x}-${sh.y.toString('hex').toUpperCase()}`);
}

/** Δ2: שחזור הסוד מרסיסים (אינטרפולציית לגראנז' בנקודה 0) */
function shamirJoin(shareStrings) {
  const shares = shareStrings.map((s) => {
    const m = String(s).trim().match(/^SCS(\d+)-([0-9A-Fa-f]+)$/);
    if (!m) throw new Error('רסיס לא תקין: ' + s);
    return { x: Number(m[1]), y: Buffer.from(m[2], 'hex') };
  });
  const len = shares[0].y.length;
  const secret = Buffer.alloc(len);
  for (let p = 0; p < len; p++) {
    let acc = 0;
    for (let j = 0; j < shares.length; j++) {
      let num = 1, den = 1;
      for (let l = 0; l < shares.length; l++) {
        if (l === j) continue;
        num = gfMul(num, shares[l].x);
        den = gfMul(den, shares[l].x ^ shares[j].x);
      }
      acc ^= gfMul(shares[j].y[p], gfDiv(num, den));
    }
    secret[p] = acc;
  }
  return secret.toString('utf8');
}

const RS_MAX_DATA = 200; // מגבלת בלוקי מקור לקובץ (מטריצת Cauchy ב-GF(2^8))
const rsParityCount = (k) => Math.max(1, Math.ceil(k / 4)); // ‎25%‎ זוגיות, מינימום 1

// ═══════════ SkyLattice-Ψ: ערוצים מוצפנים מקצה-לקצה בין זהויות (ECDH) ═══════════
// "ערבוב הצבעים" של הקריפטוגרפיה: כל צד מערבב את המפתח הסודי שלו עם המפתח
// הציבורי של הצד השני — ושניהם מגיעים לאותו סוד משותף: (dA·PB).x == (dB·PA).x
// מתבונן מהצד (כולל הממסרים) רואה רק את שני ה"צבעים הציבוריים" ולא יכול
// לשחזר את התערובת. מעל הסוד: AES-256-GCM עם כריכת-כתובת (Σ2).

/** גזירת מפתח משותף בין זהות פרטית לציבורית — סימטרי משני הכיוונים */
function ecdhKey(privBig, pubHex) {
  const Ppt = liftX(big(unhex(pubHex)));
  if (!Ppt) throw new Error('Ψ: מפתח ציבורי לא תקין');
  const S = ptMul(Ppt, privBig);
  if (!S) throw new Error('Ψ: נקודת אינסוף');
  return sha256(Buffer.from('skylattice-psi:v1:'), b32(S.x));
}

// פורמט תיל תואם-WebCrypto (iv || ct || tag) — זהה בדפדפן וב-Node
function psiSeal(buf, key32, aad) {
  const iv = crypto.randomBytes(12);
  const c = crypto.createCipheriv('aes-256-gcm', key32, iv);
  c.setAAD(Buffer.from('sc-psi:' + aad));
  const ct = Buffer.concat([c.update(buf), c.final()]);
  return Buffer.concat([iv, ct, c.getAuthTag()]).toString('base64');
}
function psiOpen(b64, key32, aad) {
  const raw = Buffer.from(b64, 'base64');
  const d = crypto.createDecipheriv('aes-256-gcm', key32, raw.subarray(0, 12));
  d.setAAD(Buffer.from('sc-psi:' + aad));
  d.setAuthTag(raw.subarray(raw.length - 16));
  return Buffer.concat([d.update(raw.subarray(12, raw.length - 16)), d.final()]);
}

const DEVICE = hex(crypto.randomBytes(6));
const dManifest = () => `sc:${keys.ns}:manifest`;
const dKv = () => `sc:${keys.ns}:kv`;
const dSite = () => `sc:${keys.ns}:site`;
const dChunk = (pathHash, i) => `sc:${keys.ns}:f:${pathHash}:${i}`;
const dWeb = (key) => `scweb:${keys.pubkey.slice(0, 16)}:${key}`;
const dJob = (id) => `scjob:${keys.pubkey.slice(0, 16)}:${id}`;
const dRes = (id) => `scres:${keys.pubkey.slice(0, 16)}:${id}`;
const dApiReq = (id) => `scapi:${keys.pubkey.slice(0, 16)}:${id}`; // Ω: בקשת HTTP מבוזרת
const dApiRes = (id) => `scapx:${keys.pubkey.slice(0, 16)}:${id}`; // Ω: תגובת HTTP מבוזרת
const dSnap = (p) => `scsnp:${keys.pubkey.slice(0, 16)}:${hex(sha256(Buffer.from('snap:' + p))).slice(0, 24)}`; // Ω: תמונת-API סטטית
const pathHash = (rel) => hex(sha256(Buffer.from('p:' + rel))).slice(0, 20);

class Cloud {
  constructor() {
    this.pool = new RelayPool(RELAYS, RELAYS_FALLBACK);
    this.state = loadState();
    this.manifest = { files: {}, updatedAt: 0, device: DEVICE };
    this.kv = {};            // key -> {v, ts} — פרטי ומוצפן
    this.kvListeners = [];
    this.webkv = {};         // key -> {v, ts} — ציבורי, משותף עם האתר
    this.webListeners = [];
    this.jobHandlers = {};   // task -> fn — רשת החישוב (SkyLattice)
    this.jobResults = {};    // jobId -> result — מניעת ביצוע כפול
    this.resWaiters = {};    // jobId -> resolve — בקשות שממתינות לתשובה
    this.routes = [];        // Ω: [{method, path/RegExp, fn}] — שרת האפליקציה
    this.apiWaiters = {};    // Ω: reqId -> resolve — בקשות API שממתינות לתשובה
    this.apiDone = {};       // Ω: reqId -> response — מניעת מענה/עיבוד כפול
    this.apiSeen = new Set();// Ω: reqId שכבר טופלו כאן
    this.dms = [];           // הודעות מוצפנות מקצה-לקצה (Ψ)
    this.dmSeen = new Set();
    this.dmListeners = [];
    this.readyPromise = null;
    this.syncing = Promise.resolve();
    this.pendingScan = null;
    this.watcher = null;
  }

  ready() {
    if (!this.readyPromise) this.readyPromise = this._start();
    return this.readyPromise;
  }

  async _start() {
    ensureId();
    deriveKeys();
    this.pool.connectAll();
    // מנסים להתחבר לכמה שיותר ממסרים — כך העדכון מתפזר רחב ברשת
    const n = await this.pool.waitForConnections(6, 15_000);
    if (n === 0) throw new Error('אין חיבור לאף ממסר — בדוק חיבור אינטרנט');
    log(`מחובר ל-${n} ממסרים ברשת | SYNC_ID: ${syncId.slice(0, 14)}…`);
    await this._pullRemote();
    await this._reconcile();
    // גם אם לא היו שינויים בקבצים — לוודא שהאתר החי קיים ומעודכן ברשת
    if (!this.publishedThisRun) {
      await this._publishSite().catch(() => {});
      if (PUBLIC_WEBSITE) await this._publishWebsite().catch((err) => log('אזהרה: עדכון האתר נכשל:', err.message));
    }
    log('סינכרון ראשוני הושלם.');
    log(`כתובת האתר החי של "${PROJECT_NAME}" [${SITE_SLUG}] (זמינה תמיד, גם כשהכול כבוי):`);
    log('  ' + this.address());
    if (PUBLIC_WEBSITE) log('כתובות מראה (אם רשת חוסמת אחת — נסה אחרת): ' + mirrorAddresses().slice(1).join('  ,  '));
    if (CUSTOM_DOMAIN) printDomainSetup();
  }

  address() { return globalAddress(); }

  /** משיכת המניפסט וה-KV העדכניים ביותר מהרשת */
  async _pullRemote() {
    // המשיכה הראשונה שקטה (טעינת מצב); משיכות הסקר התקופתי כן מדווחות —
    // כך עדכונים/הודעות/עבודות שהמנוי החי פספס עדיין מפעילים מאזינים.
    const notify = this.firstPullDone === true;
    const webPull = this.pool.query([{ kinds: [KIND_DATA], '#p': [keys.pubkey] }])
      .then((evs) => { for (const ev of evs) this._mergeWebEvent(ev, notify); })
      .catch(() => {});
    const events = await this.pool.query([{ kinds: [KIND_DATA], authors: [keys.pubkey], '#d': [dManifest(), dKv()] }]);
    await webPull;
    for (const ev of events) {
      const d = ev.tags.find((t) => t[0] === 'd')?.[1];
      try {
        const payload = JSON.parse(decrypt(ev.content, d).toString('utf8'));
        if (d === dManifest() && (payload.updatedAt ?? 0) >= (this.manifest.updatedAt ?? 0)) {
          if (this._isRollback(payload)) continue;
          this._acceptManifest(payload);
        } else if (d === dKv()) {
          this._mergeKv(payload, false);
        }
      } catch (err) {
        log('אזהרה: אירוע רשת לא ניתן לפענוח (ייתכן מ-ID אחר):', err.message);
      }
    }
    this.firstPullDone = true;
  }

  // ─────────── SkyLattice-Σ1: שרשרת התחייבויות (Hash-Chain Commitments) ───────────
  // כל מניפסט נושא מונה רץ (seq) ואת ה-hash של המניפסט הקודם (prev) —
  // היסטוריה משורשרת חסינת-זיוף. ממסר שמנסה להגיש גרסה ישנה (rollback) נחשף.

  _isRollback(payload) {
    const knownSeq = this.state.chain?.seq ?? 0;
    if ((payload.seq ?? 0) + 3 < knownSeq) {
      log(`Σ1: נחסם ניסיון rollback — התקבל מניפסט seq=${payload.seq ?? 0} מול שרשרת מקומית seq=${knownSeq}.`);
      return true;
    }
    return false;
  }

  _acceptManifest(payload) {
    this.manifest = payload;
    if ((payload.seq ?? 0) > (this.state.chain?.seq ?? 0)) {
      this.state.chain = { seq: payload.seq ?? 0, hash: hex(sha256(Buffer.from(JSON.stringify(payload)))) };
    }
  }

  /** השוואת מקומי מול ענן — מי עדכני יותר מנצח, לשני הכיוונים */
  async _reconcile() {
    const local = await scanLocal();
    const remote = this.manifest.files ?? {};
    const toUpload = [];
    const toDownload = [];
    const toDeleteLocal = [];
    let manifestChanged = false;

    for (const [rel, info] of local) {
      const r = remote[rel];
      const known = this.state.files[rel];
      if (r?.deleted && r.ts > info.mtime) { toDeleteLocal.push(rel); continue; }
      if (!r || r.deleted) { toUpload.push([rel, info]); continue; }
      if (r.hash === info.hash) {
        this.state.files[rel] = { hash: info.hash, ts: r.ts };
        continue;
      }
      // תוכן שונה — העדכון האחרון מנצח
      const localChanged = !known || known.hash !== info.hash;
      const localTs = localChanged ? info.mtime : (known.ts ?? 0);
      if (localTs > r.ts) toUpload.push([rel, info]);
      else toDownload.push([rel, r]);
    }
    for (const [rel, r] of Object.entries(remote)) {
      if (r.deleted || local.has(rel)) continue;
      const known = this.state.files[rel];
      if (known && known.ts >= r.ts) {
        // הקובץ נמחק מקומית אחרי שסונכרן — לשדר מחיקה לענן
        remote[rel] = { deleted: true, ts: now() * 1000 };
        manifestChanged = true;
        delete this.state.files[rel];
      } else {
        toDownload.push([rel, r]);
      }
    }

    for (const rel of toDeleteLocal) {
      try { await fsp.unlink(path.join(ROOT, rel)); delete this.state.files[rel]; log('נמחק (סונכרן):', rel); } catch {}
    }
    for (const [rel, r] of toDownload) await this._downloadFile(rel, r);
    for (const [rel, info] of toUpload) { await this._uploadFile(rel, info); manifestChanged = true; }
    if (manifestChanged || toUpload.length) await this._publishManifest();
    saveState(this.state);
  }

  async _downloadFile(rel, r) {
    try {
      const ph = pathHash(rel);
      const k = r.chunks;
      const m = r.par ?? 0;
      const dTags = Array.from({ length: k + m }, (_, i) => dChunk(ph, i));
      const parts = new Array(k + m).fill(null);
      for (let i = 0; i < dTags.length; i += 25) {
        const events = await this.pool.query([{ kinds: [KIND_DATA], authors: [keys.pubkey], '#d': dTags.slice(i, i + 25) }]);
        for (const ev of events) {
          // אירוע פגום אחד לא מפיל את שאר החלקים
          try {
            const d = ev.tags.find((t) => t[0] === 'd')?.[1] ?? '';
            const idx = Number(d.split(':').pop());
            // Σ2: הפענוח כבול לכתובת (d) — בלוק שהוזז/הוחלף ייכשל כאן מתמטית
            const payload = JSON.parse(decrypt(ev.content, d).toString('utf8'));
            if (payload.h === r.hash && Number.isInteger(idx)) parts[idx] = Buffer.from(payload.data, 'base64');
          } catch {}
        }
      }
      let dataParts = parts.slice(0, k);
      // Δ1: חלקים חסרים? משוחזרים מתמטית מבלוקי הזוגיות (Reed-Solomon/Cauchy)
      if (dataParts.some((p) => p === null) && m > 0) {
        const missing = dataParts.filter((p) => p === null).length;
        const shards = parts.map((p, i) => {
          if (!p) return null;
          if (i < k && p.length < CHUNK_SIZE) { const s = Buffer.alloc(CHUNK_SIZE); p.copy(s); return s; }
          return p;
        });
        const recovered = rsDecode(k, shards, CHUNK_SIZE);
        if (recovered) {
          const lastLen = r.size - (k - 1) * CHUNK_SIZE;
          dataParts = recovered.map((s, i) => s.subarray(0, i === k - 1 ? lastLen : CHUNK_SIZE));
          log(`Δ1: ${missing} חלקים חסרים של ${rel} שוחזרו מתמטית מבלוקי הזוגיות.`);
        }
      }
      if (dataParts.some((p) => p === null)) { log('אזהרה: חלקים חסרים ברשת עבור', rel, '- ינוסה שוב בסבב הבא'); return; }
      const data = Buffer.concat(dataParts);
      if (hex(sha256(data)) !== r.hash) { log('אזהרה: אימות תוכן נכשל עבור', rel); return; }
      const full = path.join(ROOT, rel);
      await fsp.mkdir(path.dirname(full), { recursive: true });
      this._suppress(rel);
      await fsp.writeFile(full, data);
      this.state.files[rel] = { hash: r.hash, ts: r.ts };
      log('הורד מהענן:', rel, `(${data.length.toLocaleString()} bytes)`);
    } catch (err) {
      log('שגיאה בהורדת', rel + ':', err.message);
    }
  }

  async _uploadFile(rel, info) {
    try {
      const data = await fsp.readFile(path.join(ROOT, rel));
      const hash = hex(sha256(data));
      const ph = pathHash(rel);
      const chunks = Math.max(1, Math.ceil(data.length / CHUNK_SIZE));
      for (let i = 0; i < chunks; i++) {
        const slice = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
        const dTag = dChunk(ph, i);
        const content = encrypt(Buffer.from(JSON.stringify({ h: hash, i, data: slice.toString('base64') })), dTag);
        await this.pool.publish(makeEvent(KIND_DATA, dTag, content));
        if (chunks > 3) await sleep(120); // עדינות מול הממסרים בקבצים גדולים
      }
      // Δ1: בלוקי זוגיות Reed-Solomon — הקובץ ישרוד גם אם חלקים יאבדו ברשת
      let par = 0;
      if (chunks <= RS_MAX_DATA) {
        par = rsParityCount(chunks);
        const shards = [];
        for (let i = 0; i < chunks; i++) {
          const s = Buffer.alloc(CHUNK_SIZE);
          data.copy(s, 0, i * CHUNK_SIZE, Math.min((i + 1) * CHUNK_SIZE, data.length));
          shards.push(s);
        }
        const parity = rsEncode(shards, par);
        for (let j = 0; j < par; j++) {
          const dTag = dChunk(ph, chunks + j);
          const content = encrypt(Buffer.from(JSON.stringify({ h: hash, i: chunks + j, par: 1, data: parity[j].toString('base64') })), dTag);
          await this.pool.publish(makeEvent(KIND_DATA, dTag, content));
          if (par > 3) await sleep(120);
        }
      }
      const ts = info.mtime;
      this.manifest.files[rel] = { hash, size: data.length, mtime: info.mtime, chunks, par, ts };
      this.state.files[rel] = { hash, ts };
      log('הועלה לענן:', rel, `(${data.length.toLocaleString()} bytes, ${chunks} חלקים + ${par} זוגיות)`);
    } catch (err) {
      log('שגיאה בהעלאת', rel + ':', err.message);
    }
  }

  async _publishManifest() {
    this.publishedThisRun = true;
    this.manifest.updatedAt = hlcNow();
    this.manifest.device = DEVICE;
    // Σ1: קישור לשרשרת — מונה רץ + hash המניפסט הקודם
    this.manifest.seq = (this.state.chain?.seq ?? this.manifest.seq ?? 0) + 1;
    this.manifest.prev = this.state.chain?.hash ?? null;
    const json = JSON.stringify(this.manifest);
    this.state.chain = { seq: this.manifest.seq, hash: hex(sha256(Buffer.from(json))) };
    const content = encrypt(Buffer.from(json), dManifest());
    await this.pool.publish(makeEvent(KIND_DATA, dManifest(), content));
    await this._publishSite().catch(() => {});
    if (PUBLIC_WEBSITE) await this._publishWebsite().catch((err) => log('אזהרה: עדכון האתר נכשל:', err.message));
  }

  /**
   * פרסום האתר החי (nsite / NIP-5A): קבצי הפרויקט מועלים לשרתי אחסון
   * מבוזרים, ומניפסט חתום ברשת ממפה כל נתיב לקובץ. שערים ציבוריים מגישים
   * את זה ככתובת חיה — בלי ששום דבר שלך ירוץ ברקע.
   */
  async _publishWebsite() {
    this.state.blobs ??= {};
    // ב---deploy מאמתים מחדש את כל הקבצים מול שרתי האחסון (HEAD זול; מעלה רק חסרים)
    if (this.forceVerifyBlobs) { this.state.blobs = {}; this.forceVerifyBlobs = false; }
    await runBuild();

    // מה מגישים: תיקיית אתר ייעודית (dist/public/... או SITE_DIR) — או כל הפרויקט
    const siteDir = resolveSiteDir();
    let siteFiles; // [webPath, diskPath, size, mtime]
    if (siteDir) {
      siteFiles = [];
      const base = path.join(ROOT, siteDir);
      const walk = async (dir) => {
        let entries = [];
        try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch {}
        for (const e of entries) {
          if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
          const full = path.join(dir, e.name);
          if (e.isDirectory()) await walk(full);
          else if (e.isFile()) {
            const st = await fsp.stat(full).catch(() => null);
            if (st) siteFiles.push(['/' + path.relative(base, full).replace(/\\/g, '/'), full, st.size, Math.floor(st.mtimeMs)]);
          }
        }
      };
      await walk(base);
      if (!this.siteDirAnnounced) { this.siteDirAnnounced = true; log(`האתר מוגש מתוך "${siteDir}/" (${siteFiles.length} קבצים).`); }
    } else {
      siteFiles = Object.entries(this.manifest.files ?? {})
        .filter(([, f]) => !f.deleted)
        .map(([rel, f]) => ['/' + rel, path.join(ROOT, rel), f.size, f.mtime]);
    }

    const paths = [];   // [webPath, sha256]
    const fileMeta = []; // לרשימת הקבצים בדף הבית האוטומטי
    let uploaded = 0;
    let failed = 0;

    for (const [webPath, diskPath, size, mtime] of siteFiles) {
      try {
        const data = await fsp.readFile(diskPath);
        const hash = hex(sha256(data));
        fileMeta.push([webPath.slice(1), { size, mtime }]);
        if (this.state.blobs[hash]) { paths.push([webPath, hash]); continue; }
        await blossomUpload(data, mimeOf(webPath));
        this.state.blobs[hash] = true;
        paths.push([webPath, hash]);
        uploaded++;
      } catch (err) {
        failed++;
        log('אזהרה: העלאת', webPath, 'לאתר נכשלה:', err.message);
      }
    }
    const files = fileMeta;

    // קבצים שנכשלו — ניסיון חוזר אוטומטי בעוד דקה, עד שהאתר שלם
    if (failed > 0) {
      clearTimeout(this.siteRetryTimer);
      this.siteRetryTimer = setTimeout(() => {
        log(`ניסיון חוזר להעלאת ${failed} קבצים לאתר…`);
        this._publishWebsite().catch((err) => log('אזהרה: עדכון האתר נכשל:', err.message));
      }, 60_000);
      this.siteRetryTimer.unref?.();
    }

    // אם ה-index.html של הפרויקט יושב בתת-תיקייה (public/, www/, dist וכו') —
    // התיקייה ההיא ממופה גם לשורש הכתובת, כך שהאתר האמיתי מוצג ולא רשימת קבצים
    if (!paths.some(([p]) => p === '/index.html')) {
      const candidates = paths
        .filter(([p]) => p.endsWith('/index.html'))
        .map(([p]) => p.slice(0, -'index.html'.length));
      const preferred = ['/public/', '/www/', '/site/', '/web/', '/docs/'].find((d) => candidates.includes(d))
        ?? candidates.sort((a, b) => (a.split('/').length - b.split('/').length) || a.localeCompare(b))[0];
      if (preferred) {
        for (const [p, h] of [...paths]) {
          if (!p.startsWith(preferred)) continue;
          const rootPath = '/' + p.slice(preferred.length);
          if (!paths.some(([existing]) => existing === rootPath)) paths.push([rootPath, h]);
        }
        log(`האתר מוגש מהתיקייה "${preferred.slice(1, -1)}" בשורש הכתובת.`);
      }
    }

    // קובץ הפעלה (.bat/.cmd) שפותח דף HTML בדפדפן — אותו דף הופך לדף הבית
    // של הכתובת הגלובלית, כך שהאתר ברשת מתנהג בדיוק כמו ההפעלה המקומית
    if (!paths.some(([p]) => p === '/index.html')) {
      const batEntry = resolveBatEntry();
      const entry = batEntry && paths.find(([p]) => p === '/' + batEntry.html);
      if (entry) {
        const baseDir = entry[0].slice(0, entry[0].lastIndexOf('/') + 1);
        // כל תיקיית דף הכניסה ממופה לשורש — קישורים יחסיים (css/js/תמונות) ממשיכים לעבוד
        if (baseDir !== '/') {
          for (const [p, h] of [...paths]) {
            if (!p.startsWith(baseDir)) continue;
            const rootPath = '/' + p.slice(baseDir.length);
            if (!paths.some(([existing]) => existing === rootPath)) paths.push([rootPath, h]);
          }
        }
        if (!paths.some(([p]) => p === '/index.html')) paths.push(['/index.html', entry[1]]);
        if (!this.batAnnounced) {
          this.batAnnounced = true;
          log(`זוהה קובץ הפעלה "${batEntry.bat}" — הדף "${batEntry.html}" הוגדר כדף הבית של הכתובת הגלובלית.`);
        }
      }
    }

    // מנוע הסינכרון לדפדפן — זמין לכל דף באתר בנתיב /cloud-web.js
    if (!paths.some(([p]) => p === '/cloud-web.js')) {
      try {
        const sdk = Buffer.from(buildWebSdk(), 'utf8');
        const { hash } = await blossomUpload(sdk, 'text/javascript');
        this.state.blobs[hash] = true;
        paths.push(['/cloud-web.js', hash]);
      } catch (err) {
        log('אזהרה: פרסום מנוע הדפדפן נכשל:', err.message);
      }
    }

    // אם עדיין אין index.html — נוצר דף בית אוטומטי שמציג את הפרויקט ומצבו החי
    if (!paths.some(([p]) => p === '/index.html')) {
      try {
        const html = Buffer.from(this._buildIndexHtml(files), 'utf8');
        const { hash } = await blossomUpload(html, 'text/html');
        this.state.blobs[hash] = true;
        paths.push(['/index.html', hash]);
      } catch (err) {
        log('אזהרה: יצירת דף הבית נכשלה:', err.message);
      }
    }

    if (paths.length === 0) return;
    const aggregate = hex(sha256(Buffer.from(
      paths.map(([p, h]) => `${h} ${p}\n`).sort().join(''),
    )));
    const manifestEv = signEvent(KIND_NSITE, [
      ...paths.map(([p, h]) => ['path', p, h]),
      ['x', aggregate, 'aggregate'],
      ...BLOSSOM_SERVERS.map((s) => ['server', s]),
      ['title', SITE_SLUG],
      ['description', `Live decentralized site "${SITE_SLUG}" (${PROJECT_NAME}) — synced by SkyCloud`],
      ['z', metaPad()], // ריפוד מטמורפי — טביעת-רגל שונה בכל הרצה
    ], '');
    // מניפסט האתר חייב להגיע לכמה שיותר ממסרים — זה מה שהשערים קוראים
    await this.pool.publish(manifestEv, 15_000, 6);
    saveState(this.state);
    if (uploaded > 0 || !this.siteAnnounced) {
      this.siteAnnounced = true;
      log(`האתר החי עודכן ברשת המבוזרת (${paths.length} נתיבים${uploaded ? ', ' + uploaded + ' קבצים חדשים הועלו' : ''}).`);
    }
  }

  /** דף בית אוטומטי — שם הפרויקט, הקבצים והמצב החי */
  _buildIndexHtml(files) {
    const fmtSize = (n) => n >= 1048576 ? (n / 1048576).toFixed(1) + ' MB' : n >= 1024 ? (n / 1024).toFixed(1) + ' KB' : n + ' B';
    const rows = files
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([rel, f]) => `<tr><td><a href="/${rel}">${rel}</a></td><td>${fmtSize(f.size ?? 0)}</td><td>${new Date(f.mtime).toISOString().replace('T', ' ').slice(0, 19)}</td></tr>`)
      .join('\n');
    const updated = new Date().toISOString().replace('T', ' ').slice(0, 19);
    return `<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${PROJECT_NAME}</title>
<style>
  :root { color-scheme: dark; }
  * { box-sizing: border-box; margin: 0; }
  body { font-family: system-ui, -apple-system, 'Segoe UI', sans-serif; background: #0b0e14; color: #e6e9ef; min-height: 100vh; padding: 48px 20px; }
  .wrap { max-width: 860px; margin: 0 auto; }
  .badge { display: inline-block; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; color: #7dd3a0; border: 1px solid #2a4a38; background: #10231a; border-radius: 999px; padding: 4px 12px; margin-bottom: 18px; }
  h1 { font-size: 34px; margin-bottom: 8px; }
  .sub { color: #8b93a7; margin-bottom: 34px; font-size: 15px; }
  .card { background: #121826; border: 1px solid #1f2937; border-radius: 14px; padding: 22px; margin-bottom: 18px; }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  th { text-align: right; color: #8b93a7; font-weight: 600; padding: 8px 10px; border-bottom: 1px solid #1f2937; }
  td { padding: 9px 10px; border-bottom: 1px solid #171e2c; }
  a { color: #7aa2f7; text-decoration: none; }
  a:hover { text-decoration: underline; }
  footer { color: #5c6474; font-size: 12.5px; margin-top: 26px; text-align: center; }
</style>
</head>
<body>
<div class="wrap">
  <span class="badge">אתר חי ברשת מבוזרת</span>
  <h1>${PROJECT_NAME}</h1>
  <p class="sub">עדכון אחרון: ${updated} UTC · ${files.length} קבצים · הדף מתעדכן אוטומטית בכל סינכרון</p>
  <div class="card">
    <table>
      <thead><tr><th>קובץ</th><th>גודל</th><th>עודכן</th></tr></thead>
      <tbody>${rows || '<tr><td colspan="3">(אין קבצים עדיין)</td></tr>'}</tbody>
    </table>
  </div>
  <footer>מוגש מרשת מבוזרת (Nostr + Blossom) — אין שרת מרכזי, האתר חי גם כשכל המכשירים כבויים.</footer>
</div>
</body>
</html>`;
  }

  /** פרופיל + רשימת ממסרים — כדי ששערי הרשת ידעו לאתר את העמוד */
  async _publishIdentity() {
    const profile = {
      pubkey: keys.pubkey,
      created_at: now(),
      kind: 0,
      tags: [],
      content: JSON.stringify({
        name: 'SkyCloud ' + syncId.slice(3, 11),
        about: 'Private cloud sync node. Data is end-to-end encrypted.',
      }),
    };
    profile.id = hex(sha256(Buffer.from(JSON.stringify([0, profile.pubkey, profile.created_at, profile.kind, profile.tags, profile.content]))));
    profile.sig = hex(schnorrSign(unhex(profile.id), keys.priv));
    const relayList = {
      pubkey: keys.pubkey,
      created_at: now(),
      kind: 10002,
      tags: RELAYS.slice(0, 6).map((r) => ['r', r]),
      content: '',
    };
    relayList.id = hex(sha256(Buffer.from(JSON.stringify([0, relayList.pubkey, relayList.created_at, relayList.kind, relayList.tags, relayList.content]))));
    relayList.sig = hex(schnorrSign(unhex(relayList.id), keys.priv));
    // רשימת שרתי האחסון (BUD-03) — כדי ששערי האתר ידעו מאיפה למשוך את הקבצים
    const serverList = signEvent(KIND_BLOSSOM_LIST, BLOSSOM_SERVERS.map((s) => ['server', s]), '');
    await this.pool.publish(profile).catch(() => {});
    await this.pool.publish(relayList).catch(() => {});
    await this.pool.publish(serverList).catch(() => {});
  }

  /** עמוד הסטטוס הציבורי — זמין בכתובת הגלובלית גם כשהכול כבוי */
  async _publishSite() {
    if (!this.identityPublished) {
      this.identityPublished = true;
      await this._publishIdentity().catch(() => {});
    }
    const files = Object.entries(this.manifest.files ?? {}).filter(([, f]) => !f.deleted);
    const totalSize = files.reduce((s, [, f]) => s + (f.size ?? 0), 0);
    const rows = files
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([rel, f]) => `| \`${rel}\` | ${(f.size ?? 0).toLocaleString()} B | ${new Date(f.mtime).toISOString().replace('T', ' ').slice(0, 19)} |`)
      .join('\n');
    // באנגלית בכוונה — חלק משערי הרשת לא מציגים עברית נכון
    const md = [
      `# SkyCloud Project — ${syncId.slice(0, 14)}...`,
      '',
      `**Last update:** ${new Date().toISOString().replace('T', ' ').slice(0, 19)} UTC`,
      `**Synced files:** ${files.length} | **Total size:** ${totalSize.toLocaleString()} bytes`,
      '',
      '| File | Size | Updated |',
      '|---|---|---|',
      rows || '| _(no files yet)_ | | |',
      '',
      '_File contents are end-to-end encrypted and stored across the network. This page shows live status only._',
    ].join('\n');
    const ev = makeEvent(KIND_SITE, dSite(), md, [
      ['title', 'SkyCloud Project Status'],
      ['published_at', String(now())],
    ]);
    await this.pool.publish(ev);
  }

  // ─────────────── KV — ה-API לאפליקציות (ללא UI) ───────────────

  _mergeKv(remoteKv, notify = true) {
    for (const [k, entry] of Object.entries(remoteKv ?? {})) {
      hlcObserve(entry?.ts); // Θ: מקדם את השעון הלוגי מעבר לכל זמן שנצפה
      const cur = this.kv[k];
      if (!cur || entry.ts > cur.ts) {
        this.kv[k] = entry;
        if (notify) for (const fn of this.kvListeners) { try { fn(k, entry.v); } catch {} }
      }
    }
  }

  async set(key, value) {
    await this.ready();
    this.kv[key] = { v: value, ts: hlcNow() };
    const content = encrypt(Buffer.from(JSON.stringify(this.kv)), dKv());
    await this.pool.publish(makeEvent(KIND_DATA, dKv(), content));
  }

  get(key) { return this.kv[key]?.v; }
  getAll() { return Object.fromEntries(Object.entries(this.kv).map(([k, e]) => [k, e.v])); }
  onChange(fn) { this.kvListeners.push(fn); }

  // ─────────────── webkv — נתונים חיים משותפים עם האתר הציבורי ───────────────
  // ערוץ ציבורי (לא מוצפן): גם האתר בדפדפן וגם המכשירים שלך קוראים
  // וכותבים בו — זה מה שמאפשר מערכת חיה עם משתמשים ישירות מהכתובת.

  _mergeWebEvent(ev, notify = true) {
    const d = ev.tags.find((t) => t[0] === 'd')?.[1] ?? '';
    const p16 = keys.pubkey.slice(0, 16);
    // ניתוב רשת החישוב: תוצאות עבודה ובקשות חישוב חדשות
    if (d.startsWith(`scres:${p16}:`)) {
      try {
        const res = JSON.parse(ev.content);
        if (typeof res?.ts === 'number') this._onResult(d.slice(`scres:${p16}:`.length), res);
      } catch {}
      return;
    }
    if (d.startsWith(`scjob:${p16}:`)) {
      try {
        const job = JSON.parse(ev.content);
        if (typeof job?.ts === 'number' && notify) this._onJob(d.slice(`scjob:${p16}:`.length), job).catch(() => {});
      } catch {}
      return;
    }
    // Ω: תגובת API חוזרת למבקש
    if (d.startsWith(`scapx:${p16}:`)) {
      try { const res = JSON.parse(ev.content); if (typeof res?.ts === 'number') this._onApiRes(d.slice(`scapx:${p16}:`.length), res); } catch {}
      return;
    }
    // Ω: בקשת API — עובד-שרת מריץ ומחזיר תגובה
    if (d.startsWith(`scapi:${p16}:`)) {
      try { const req = JSON.parse(ev.content); if (typeof req?.ts === 'number' && notify) this._onApiReq(d.slice(`scapi:${p16}:`.length), req).catch(() => {}); } catch {}
      return;
    }
    if (d.startsWith(`scdm:${p16}:`)) { this._onDm(ev, d, notify); return; }
    const prefix = `scweb:${p16}:`;
    if (!d.startsWith(prefix)) return;
    const key = d.slice(prefix.length);
    try {
      const entry = JSON.parse(ev.content);
      if (typeof entry?.ts !== 'number') return;
      hlcObserve(entry.ts);
      const cur = this.webkv[key];
      if (!cur || entry.ts > cur.ts) {
        this.webkv[key] = entry;
        if (notify) for (const fn of this.webListeners) { try { fn(key, entry.v, ev.pubkey); } catch {} }
      }
    } catch {}
  }

  async webSet(key, value) {
    await this.ready();
    const entry = { v: value, ts: hlcNow() };
    this.webkv[key] = entry;
    await this.pool.publish(signEvent(KIND_DATA, [['d', dWeb(key)], ['p', keys.pubkey]], JSON.stringify(entry)));
  }

  webGet(key) { return this.webkv[key]?.v; }
  webGetAll() { return Object.fromEntries(Object.entries(this.webkv).map(([k, e]) => [k, e.v])); }
  onWebChange(fn) { this.webListeners.push(fn); }

  // ─────────────── SkyLattice — רשת חישוב מבוזרת של הפרויקט ───────────────
  // כל מכשיר שמריץ serve() וכל טאב דפדפן פתוח של האתר הופכים לעובד-חישוב.
  // בקשה (run) משוגרת לרשת; העובדים בוחרים מבצע יחיד בבחירה דטרמיניסטית
  // (Rendezvous Hashing — לכל עובד ציון sha256(worker+jobId), הנמוך רץ ראשון)
  // כך אין ביצוע כפול ואין צורך במתאם מרכזי. התוצאה חתומה וחוזרת למבקש.

  /** רישום המכשיר הזה כעובד-חישוב: serve({ taskName: async (payload) => result }) */
  serve(handlers) {
    Object.assign(this.jobHandlers, handlers);
    // קבלת עבודות דורשת מנוי חי — מובטח גם אם serve נקרא לפני ready
    this.ready().then(() => this.startLive()).catch(() => {});
    log(`עובד-חישוב פעיל: ${Object.keys(this.jobHandlers).join(', ')}`);
  }

  /** שיגור עבודת חישוב לרשת — כל עובד זמין (Node או דפדפן) יענה */
  async run(task, payload, timeoutMs = 20_000) {
    await this.ready();
    this.startLive(); // קבלת התוצאה דורשת מנוי חי
    const id = hex(crypto.randomBytes(8));
    const entry = { task, payload, ts: Date.now(), from: keys.pubkey };
    await this.pool.publish(signEvent(KIND_DATA, [['d', dJob(id)], ['p', keys.pubkey]], JSON.stringify(entry)));
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        delete this.resWaiters[id];
        reject(new Error(`compute timeout: אף עובד לא ענה למשימה "${task}"`));
      }, timeoutMs);
      const settle = (res) => {
        clearTimeout(timer);
        res.ok ? resolve(res.value) : reject(new Error(res.error));
      };
      // התוצאה אולי כבר הגיעה לפני שנרשמנו (עובד מהיר במיוחד) — לא נתקעים
      if (this.jobResults[id]) return settle(this.jobResults[id]);
      this.resWaiters[id] = settle;
    });
  }

  async _onJob(id, job) {
    const fn = this.jobHandlers[job.task];
    if (!fn || this.jobResults[id]) return;
    if (Date.now() - job.ts > 60_000) return; // עבודה ישנה — לא רלוונטית
    // בחירת מבצע דטרמיניסטית: ציון לפי sha256; עובדי Node מקבלים עדיפות קלה
    const rank = Number(BigInt('0x' + hex(sha256(Buffer.from(DEVICE + ':' + id))).slice(0, 8)) % 700n);
    await sleep(150 + rank);
    if (this.jobResults[id]) return; // עובד אחר כבר ענה
    let res;
    try { res = { ok: true, value: await fn(job.payload, job) }; }
    catch (err) { res = { ok: false, error: err.message }; }
    res.ts = Date.now();
    res.worker = DEVICE;
    this.jobResults[id] = res;
    await this.pool.publish(signEvent(KIND_DATA, [['d', dRes(id)], ['p', keys.pubkey]], JSON.stringify(res))).catch(() => {});
  }

  _onResult(id, res) {
    if (!this.jobResults[id] || res.ts > this.jobResults[id].ts) this.jobResults[id] = res;
    const waiter = this.resWaiters[id];
    if (waiter) { delete this.resWaiters[id]; waiter(res); }
  }

  // ─────────── SkyLattice-Ω: שרת אפליקציה חי על הכתובת הגלובלית ───────────
  // הכתובת הגלובלית מגישה מעטפת סטטית; כאן חי "צד השרת". כל מכשיר Node
  // שרשם route()/bridge() הוא עובד-שרת. הדפדפן שולח בקשות HTTP דרך הרשת
  // המבוזרת, עובד יחיד נבחר דטרמיניסטית (Rendezvous), מריץ ומחזיר תגובה
  // חתומה. מערכת מלאה (Next.js/Express/סוכני AI) חיה בכתובת — לא סטטית.

  /** רישום נתיב שרת: route('GET', '/api/users', async (req) => ({ status, body })) */
  route(method, pathPattern, fn, opts = {}) {
    this.routes.push({ method: String(method).toUpperCase(), path: pathPattern, fn, static: !!opts.static });
    this.ready().then(() => this.startLive()).then(() => { if (opts.static && typeof pathPattern === 'string') this._snapshotRoute(this.routes[this.routes.length - 1]); }).catch(() => {});
    if (!this.httpAnnounced) { this.httpAnnounced = true; log('Ω: שרת אפליקציה פעיל — הכתובת הגלובלית מריצה צד-שרת חי דרך הרשת.'); }
    return this;
  }

  /**
   * פרסום "תמונת-API" סטטית לרשת: תשובה מחושבת-מראש שמוגשת מהממסרים
   * גם כשאין אף עובד-שרת חי בעולם — backend ללא-מפעיל לקריאות מבוססות-נתונים.
   */
  async snapshot(pathName, value, opts = {}) {
    await this.ready();
    this.startLive();
    let status = 200, headers = { 'content-type': 'application/json' }, payload = value;
    if (value && typeof value === 'object' && ('status' in value || 'body' in value || 'headers' in value)) {
      status = value.status ?? 200; headers = value.headers ?? headers; payload = value.body;
    }
    let bodyBuf;
    if (Buffer.isBuffer(payload)) bodyBuf = payload;
    else if (typeof payload === 'string') bodyBuf = Buffer.from(payload, 'utf8');
    else { bodyBuf = Buffer.from(JSON.stringify(payload ?? null), 'utf8'); headers['content-type'] ??= 'application/json'; }
    const res = { status, headers, body: bodyBuf.toString('base64'), enc: 'base64', ts: hlcNow(), snap: true };
    if (res.body.length > API_INLINE_MAX) {
      try { const { hash, servers } = await blossomUpload(bodyBuf, headers['content-type'] || 'application/octet-stream'); res.blob = `${servers[0]}/${hash}`; delete res.body; } catch {}
    }
    await this.pool.publish(signEvent(KIND_DATA, [['d', dSnap(pathName)], ['p', keys.pubkey], ['z', metaPad()]], JSON.stringify(res)), { minOks: 2 });
    return this;
  }

  /** מריץ נתיב סטטי מול בקשת-GET סינתטית ומפרסם את התוצאה כתמונת-API */
  async _snapshotRoute(r) {
    try {
      const out = await this._serveRequest({ method: 'GET', path: r.path, headers: {}, body: '' });
      const body = out.blob ? Buffer.from(await (await fetch(out.blob)).arrayBuffer()) : Buffer.from(out.body || '', 'base64');
      await this.snapshot(r.path, { status: out.status, headers: out.headers, body });
    } catch (err) { log(`Ω: כשל בצילום נתיב סטטי ${r.path}: ${err.message}`); }
  }

  /** גשר לשרת מקומי אמיתי (Next.js/Express/כל שפה) שרץ אצלך — שרת Node *מלא* */
  bridge(url) {
    this.bridgeUrl = String(url).replace(/\/$/, '');
    this.ready().then(() => this.startLive()).catch(() => {});
    log(`Ω: גשר שרת פעיל — בקשות API מהכתובת הגלובלית מגושרות אל ${this.bridgeUrl}`);
    return this;
  }

  _matchRoute(method, pathname) {
    for (const r of this.routes) {
      if (r.method !== method && r.method !== 'ALL') continue;
      if (typeof r.path === 'string') { if (r.path === pathname) return { r, params: {} }; }
      else if (r.path instanceof RegExp) { const m = pathname.match(r.path); if (m) return { r, params: m.groups ?? {} }; }
    }
    return null;
  }

  /** ביצוע בקשה מקומית: גשר → שרת אמיתי, אחרת נתיב רשום */
  async _serveRequest(req) {
    if (this.bridgeUrl) {
      const r = await fetch(this.bridgeUrl + req.path, {
        method: req.method,
        headers: req.headers || {},
        body: ['GET', 'HEAD'].includes(req.method) ? undefined : Buffer.from(req.body || '', 'base64'),
        signal: AbortSignal.timeout(25_000),
      });
      const buf = Buffer.from(await r.arrayBuffer());
      const headers = {}; r.headers.forEach((v, k) => { headers[k] = v; });
      return { status: r.status, headers, body: buf.toString('base64'), enc: 'base64' };
    }
    const pathname = req.path.split('?')[0];
    const hit = this._matchRoute(req.method, pathname);
    if (!hit) return { status: 404, headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify({ error: 'no route', path: req.path })).toString('base64'), enc: 'base64' };
    let out;
    try {
      const body = req.body ? Buffer.from(req.body, 'base64') : Buffer.alloc(0);
      out = await hit.r.fn({ method: req.method, path: req.path, headers: req.headers || {}, params: hit.params, body, json: () => JSON.parse(body.toString('utf8') || 'null'), text: () => body.toString('utf8') });
    } catch (err) {
      return { status: 500, headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify({ error: err.message })).toString('base64'), enc: 'base64' };
    }
    // נורמליזציה של תגובת המשתמש לפורמט תיל אחיד
    let status = 200, headers = { 'content-type': 'application/json' }, payload;
    if (out && typeof out === 'object' && ('status' in out || 'body' in out || 'headers' in out)) {
      status = out.status ?? 200;
      headers = out.headers ?? headers;
      payload = out.body;
    } else payload = out;
    let bodyBuf;
    if (Buffer.isBuffer(payload)) bodyBuf = payload;
    else if (typeof payload === 'string') bodyBuf = Buffer.from(payload, 'utf8');
    else { bodyBuf = Buffer.from(JSON.stringify(payload ?? null), 'utf8'); headers['content-type'] ??= 'application/json'; }
    return { status, headers, body: bodyBuf.toString('base64'), enc: 'base64' };
  }

  async _onApiReq(id, req) {
    if (!this.routes.length && !this.bridgeUrl) return; // המכשיר הזה אינו עובד-שרת
    if (this.apiSeen.has(id)) return;
    if (Date.now() - (req.ts || 0) > 30_000) return; // בקשה ישנה
    // בחירת מבצע דטרמיניסטית (Rendezvous) — עובד יחיד עונה, בלי כפילות
    const rank = Number(BigInt('0x' + hex(sha256(Buffer.from(DEVICE + ':' + id))).slice(0, 8)) % 500n);
    await sleep(60 + rank);
    if (this.apiSeen.has(id)) return;
    this.apiSeen.add(id);
    if (this.apiSeen.size > 4000) this.apiSeen.clear();
    let res;
    try { res = await this._serveRequest(req); }
    catch (err) { res = { status: 502, headers: { 'content-type': 'application/json' }, body: Buffer.from(JSON.stringify({ error: err.message })).toString('base64'), enc: 'base64' }; }
    res.ts = Date.now(); res.worker = DEVICE;
    // תגובה גדולה עוברת דרך Blossom (URL) במקום inline — כמו CDN לתגובות כבדות
    if (res.body && res.body.length > API_INLINE_MAX) {
      try {
        const { hash, servers } = await blossomUpload(Buffer.from(res.body, 'base64'), res.headers?.['content-type'] || 'application/octet-stream');
        res.blob = `${servers[0]}/${hash}`; delete res.body;
      } catch {}
    }
    await this.pool.publish(signEvent(KIND_DATA, [['d', dApiRes(id)], ['p', keys.pubkey]], JSON.stringify(res))).catch(() => {});
  }

  _onApiRes(id, res) {
    if (this.apiDone[id]) return;
    this.apiDone[id] = res;
    const w = this.apiWaiters[id];
    if (w) { delete this.apiWaiters[id]; w(res); }
  }

  /** קריאת API מצד Node (למשל לבדיקות/הטמעה): מחזירה {status, headers, body:Buffer} */
  async fetchApi(pathOrReq, init = {}) {
    await this.ready();
    this.startLive();
    const req = typeof pathOrReq === 'string'
      ? { method: (init.method || 'GET').toUpperCase(), path: pathOrReq, headers: init.headers || {}, body: init.body ? Buffer.from(init.body).toString('base64') : '', ts: Date.now(), from: keys.pubkey }
      : pathOrReq;
    const id = hex(crypto.randomBytes(8));
    await this.pool.publish(signEvent(KIND_DATA, [['d', dApiReq(id)], ['p', keys.pubkey]], JSON.stringify(req)));
    const res = await new Promise((resolve, reject) => {
      const timer = setTimeout(() => { delete this.apiWaiters[id]; reject(new Error(`Ω: אף עובד-שרת לא ענה ל-${req.path}`)); }, init.timeoutMs || 25_000);
      const settle = (r) => { clearTimeout(timer); resolve(r); };
      if (this.apiDone[id]) return settle(this.apiDone[id]);
      this.apiWaiters[id] = settle;
    });
    let body = res.body ? Buffer.from(res.body, 'base64') : Buffer.alloc(0);
    if (res.blob) body = Buffer.from(await (await fetch(res.blob)).arrayBuffer());
    return { status: res.status, headers: res.headers || {}, body };
  }

  // ─────────────── SkyLattice-Ψ: הודעות מוצפנות מקצה-לקצה ───────────────
  // המכשיר הזה (זהות הפרויקט) יכול להתכתב מוצפן עם כל משתמש/מבקר של האתר,
  // והם איתו — אף גורם בדרך (כולל הממסרים) לא יכול לפענח.

  _onDm(ev, d, notify = true) {
    try {
      const env = JSON.parse(ev.content);
      if (typeof env?.ts !== 'number' || typeof env?.ct !== 'string' || typeof env?.to !== 'string') return;
      const mine = env.to === keys.pubkey;
      const fromMe = ev.pubkey === keys.pubkey;
      if (!mine && !fromMe) return; // הודעה בין צדדים אחרים — לא ניתנת לפענוח אצלנו ממילא
      if (this.dmSeen.has(d)) return;
      const other = mine ? ev.pubkey : env.to;
      const key = ecdhKey(keys.priv, other === keys.pubkey ? keys.pubkey : other);
      const payload = JSON.parse(psiOpen(env.ct, key, d).toString('utf8'));
      this.dmSeen.add(d);
      const msg = { from: ev.pubkey, to: env.to, value: payload.v, ts: payload.ts };
      this.dms.push(msg);
      this.dms.sort((a, b) => a.ts - b.ts);
      if (notify) for (const fn of this.dmListeners) { try { fn(msg); } catch {} }
    } catch {} // פענוח נכשל = לא מיועד לנו / חבלה — נזרק בשקט
  }

  /** שליחת הודעה מוצפנת מקצה-לקצה לזהות אחרת (משתמש/מבקר/מכשיר) */
  async sendTo(recipientPub, value) {
    await this.ready();
    this.startLive();
    const d = `scdm:${keys.pubkey.slice(0, 16)}:${hex(crypto.randomBytes(9))}`;
    const key = ecdhKey(keys.priv, recipientPub);
    const ct = psiSeal(Buffer.from(JSON.stringify({ v: value, ts: Date.now() })), key, d);
    await this.pool.publish(signEvent(KIND_DATA, [['d', d], ['p', keys.pubkey]], JSON.stringify({ to: recipientPub, ct, ts: Date.now() })));
  }

  onMessage(fn) { this.dmListeners.push(fn); }
  messages() { return [...this.dms]; }

  /** העלאת קובץ ציבורי לרשת האחסון המבוזרת — מקבל URL קבוע שחי לתמיד */
  async upload(input, type) {
    await this.ready();
    let data, mime = type;
    if (Buffer.isBuffer(input)) data = input;
    else {
      const p = path.isAbsolute(String(input)) ? String(input) : path.join(ROOT, String(input));
      if (fs.existsSync(p) && fs.statSync(p).isFile()) { data = await fsp.readFile(p); mime ??= mimeOf(p); }
      else data = Buffer.from(String(input), 'utf8');
    }
    mime ??= 'application/octet-stream';
    const { hash, servers } = await blossomUpload(data, mime);
    return { hash, url: `${servers[0]}/${hash}`, urls: servers.map((s) => `${s}/${hash}`), size: data.length, type: mime };
  }

  // ─────────── ייבוא מערכת שלמה מ-ZIP → פריקה + זיהוי + הגשה ───────────
  /** פורק ZIP לתיקיית הפרויקט, מזהה תיקיית פרסום/בנייה, ומסנכרן לכתובת */
  async importZip(zipPath, opts = {}) {
    const abs = path.isAbsolute(zipPath) ? zipPath : path.join(ROOT, zipPath);
    const buf = await fsp.readFile(abs);
    const entries = unzip(buf);
    // אם כל הקבצים תחת תיקיית-שורש אחת (כמו myapp/…) — פורקים בלעדיה
    const tops = new Set(entries.map((e) => e.name.split('/')[0]));
    const strip = tops.size === 1 && entries.every((e) => e.name.includes('/'));
    const destDir = opts.dir ? path.join(ROOT, opts.dir) : ROOT;
    let written = 0;
    for (const e of entries) {
      const rel = strip ? e.name.split('/').slice(1).join('/') : e.name;
      if (!rel || /(^|\/)node_modules\//.test(rel)) continue; // תלויות יותקנו מחדש
      const full = path.join(destDir, rel);
      if (!full.startsWith(destDir)) continue; // הגנת path-traversal (Zip Slip)
      await fsp.mkdir(path.dirname(full), { recursive: true });
      await fsp.writeFile(full, e.data);
      written++;
    }
    log(`ייבוא ZIP: נפרקו ${written} קבצים${strip ? ' (הוסרה תיקיית-שורש)' : ''}.`);
    // זיהוי חכם: package.json עם build/start → הצעת שילוב מלא
    try {
      const pkg = JSON.parse(await fsp.readFile(path.join(destDir, 'package.json'), 'utf8'));
      const scripts = pkg.scripts || {};
      if (scripts.build) log(`זוהתה פקודת בנייה: "npm run build" — הגדר BUILD_COMMAND='npm run build' כדי לבנות לפני פרסום.`);
      if (scripts.start || scripts.dev) log(`זוהה שרת: הרץ אותו מקומית (npm start) והגדר LOCAL_BRIDGE='http://127.0.0.1:<port>' — הכתובת הגלובלית תריץ אותו מלא (לא סטטי).`);
    } catch {}
    const sd = resolveSiteDir();
    if (sd) log(`תיקיית הגשה שזוהתה: ${sd}/`);
    return { files: written };
  }

  // ─────────── Θ: ריפוי-עצמי — רענון שכפול + אימות שלמות אוטומטי ───────────
  // מחזורי: (1) מרענן את המניפסט והזהות ברשת כדי לשמור שכפול רחב (מונע
  // דעיכת TTL בממסרים), (2) מאמת מחדש שקבצי האתר עדיין קיימים בשרתי האחסון
  // ומעלה מחדש חסרים. תיקון-עצמי בלי מגע יד אדם, מונע-נתונים.
  async _healSweep() {
    try {
      await this._publishSite().catch(() => {});
      if (Object.keys(this.manifest.files ?? {}).length) {
        await this.pool.publish(makeEvent(KIND_DATA, dManifest(), encrypt(Buffer.from(JSON.stringify(this.manifest)), dManifest())), 12_000, 4).catch(() => {});
      }
      if (PUBLIC_WEBSITE) { this.forceVerifyBlobs = true; await this._publishWebsite().catch(() => {}); }
      // Ω: רענון תמונות-API סטטיות — נשמרות זמינות לשירות ללא-מפעיל
      for (const r of this.routes) if (r.static && typeof r.path === 'string') await this._snapshotRoute(r).catch(() => {});
      const rep = this.pool.healthReport();
      const weak = rep.filter((r) => r.phi > 6).length;
      if (weak) log(`Θ: ריפוי-עצמי — ${rep.length} ממסרים פעילים, ${weak} חשודים; שכפול המידע רועֶן ברשת.`);
    } catch (err) { log('Θ: מחזור ריפוי נכשל (ינוסה שוב):', err.message); }
  }

  /** תמונת בריאות הרשת — לניטור עצמי (Θ) */
  health() { return this.pool.healthReport(); }

  // ─────────── תרומת-עיבוד / כרייה — הסכמה מפורשת בלבד, לעולם לא בשקט ───────────
  // עקרון: הרצת עומס-מעבד אצל מי שלא אישר במפורש = "cryptojacking" (זדוני,
  // חסום ע"י דפדפנים/אנטי-וירוס ואף לא חוקי במקומות רבים). לכן המנגנון כאן
  // כבוי כברירת מחדל, דורש גם MINING.enabled=true וגם דגל --allow-mining
  // בזמן ריצה, מווסת עומס לפי maxCpuPercent, ושקוף לחלוטין (מדפיס מה שהוא עושה).
  startMining(consented) {
    if (!MINING.enabled) return;
    if (!consented) { log('כרייה: MINING.enabled=true אך לא ניתן אישור ריצה (--allow-mining). לא מופעל.'); return; }
    if (!MINING.wallet || !MINING.network) { log('כרייה: חובה להגדיר MINING.wallet ו-MINING.network. לא מופעל.'); return; }
    const duty = Math.max(1, Math.min(90, MINING.maxCpuPercent)) / 100; // מחזור-עבודה
    const sliceMs = 50, workMs = Math.round(sliceMs * duty), restMs = sliceMs - workMs;
    let hashes = 0, nonce = 0;
    const seed = Buffer.from(`skymine:${MINING.coin}:${MINING.wallet}:${keys.pubkey}`);
    log(`כרייה/תרומת-עיבוד פעילה (באישור מפורש): מטבע=${MINING.coin || '?'} רשת=${MINING.network} תקרה=${MINING.maxCpuPercent}% ארנק=${MINING.wallet.slice(0, 10)}…`);
    log('שקיפות: זהו מנוע-עבודה מווסת. לחיבור לפּוּל אמיתי ממש את onShare/network בהתאם לפרוטוקול המטבע.');
    const tick = () => {
      const end = Date.now() + workMs;
      while (Date.now() < end) { sha256(seed, b32(BigInt(nonce++))); hashes++; }
    };
    this.miningTimer = setInterval(tick, sliceMs + restMs);
    this.miningTimer.unref?.();
    this.miningStat = setInterval(() => log(`כרייה: ${(hashes / 10).toFixed(0)} hash/s (מווסת ל-${MINING.maxCpuPercent}%).`), 10_000);
    this.miningStat.unref?.();
  }

  // ─────────────── מצב חי: מנוי + צפייה בקבצים ───────────────

  _suppress(rel) {
    this._suppressed ??= new Map();
    this._suppressed.set(rel, Date.now() + 3000);
  }

  startLive() {
    if (this.liveStarted) return;
    this.liveStarted = true;
    // מנוי לעדכונים חיים ממקומות אחרים עם אותו ID
    this.pool.subscribe(
      [{ kinds: [KIND_DATA], authors: [keys.pubkey], '#d': [dManifest(), dKv()], since: now() - 5 }],
      (ev) => {
        const d = ev.tags.find((t) => t[0] === 'd')?.[1];
        try {
          const payload = JSON.parse(decrypt(ev.content, d).toString('utf8'));
          if (d === dKv()) { this._mergeKv(payload); return; }
          if (d === dManifest() && payload.device !== DEVICE && payload.updatedAt > this.manifest.updatedAt) {
            if (this._isRollback(payload)) return;
            this._acceptManifest(payload);
            log('עדכון חי התקבל מהרשת — מסנכרן…');
            this._queueSync();
          }
        } catch {}
      },
    );

    // מנוי חי לנתוני האתר (webkv) — מכל מבקר, מכל מקום בעולם
    this.pool.subscribe(
      [{ kinds: [KIND_DATA], '#p': [keys.pubkey], since: now() - 5 }],
      (ev) => this._mergeWebEvent(ev),
    );

    // צפייה בשינויים מקומיים
    try {
      this.watcher = fs.watch(ROOT, { recursive: true }, (_evt, fname) => {
        // התעלמות משינויים שנוצרו ע"י פקודת הבנייה עצמה — מונע לולאת בנייה אינסופית
        if (isBuildQuiet()) return;
        if (!fname) return this._queueSync();
        const rel = String(fname).replace(/\\/g, '/');
        const top = rel.split('/')[0];
        if (IGNORE.includes(top) || IGNORE.includes(path.basename(rel)) || rel.startsWith('.cloud-')) return;
        const until = this._suppressed?.get(rel);
        if (until && Date.now() < until) return;
        this._queueSync();
      });
    } catch (err) {
      log('אזהרה: מעקב קבצים אוטומטי לא זמין (' + err.message + ') — יבוצע סקר תקופתי בלבד.');
    }

    // סקר תקופתי כגיבוי — גם אם המנוי החי נפל
    this.pollTimer = setInterval(() => {
      this._pullRemote().then(() => this._queueSync()).catch(() => {});
    }, LIVE_POLL_MS);

    // Θ: מחזור ריפוי-עצמי — רענון שכפול ואימות שלמות אוטומטי ברשת
    this.healTimer = setInterval(() => this._healSweep(), HEAL_SWEEP_MS);
    this.healTimer.unref?.();

    // Θ: ניתוח מסלולים והחלפה דינמית — Auto-Proxy / Protocol Hopping
    this.hopTimer = setInterval(() => this.pool.autoHop(), 30_000);
    this.hopTimer.unref?.();

    log('מצב חי פעיל — כל שינוי כאן או בכל מקום אחר עם אותו ID יסתנכרן אוטומטית.');
  }

  _queueSync() {
    clearTimeout(this.pendingScan);
    this.pendingScan = setTimeout(() => {
      this.syncing = this.syncing.then(() => this._reconcile()).catch((err) => log('שגיאת סינכרון:', err.message));
    }, DEBOUNCE_MS);
  }

  async stop() {
    clearInterval(this.pollTimer);
    clearInterval(this.healTimer);
    clearInterval(this.hopTimer);
    clearInterval(this.miningTimer);
    clearInterval(this.miningStat);
    clearTimeout(this.pendingScan);
    clearTimeout(this.siteRetryTimer);
    this.watcher?.close();
    await this.syncing.catch(() => {});
    this.pool.close();
  }
}

// ────────────────────────────── הפעלה ──────────────────────────────

const cloud = new Cloud();

async function main() {
  const args = process.argv.slice(2);

  if (args.includes('--new-id')) {
    const self = fs.readFileSync(__filename, 'utf8');
    const updated = self.replace(/const SYNC_ID = '[^']*';/, `const SYNC_ID = 'AUTO';`);
    fs.writeFileSync(__filename, updated, 'utf8');
    try { fs.unlinkSync(STATE_FILE); } catch {}
    log('ה-ID אופס. בהרצה הבאה ייווצר ענן חדש וריק.');
    return;
  }

  if (args[0] === '--shares') {
    ensureId();
    const n = Math.max(2, parseInt(args[1], 10) || 5);
    const k = Math.min(n, Math.max(2, parseInt(args[2], 10) || 3));
    const shares = shamirSplit(Buffer.from(syncId, 'utf8'), n, k);
    log(`Δ2: ה-SYNC_ID פוצל ל-${n} רסיסים; כל ${k} מהם משחזרים אותו במלואו:`);
    for (const s of shares) log('  ' + s);
    log('שמור כל רסיס במקום נפרד. רסיס בודד (או פחות מ-' + k + ') לא חושף שום דבר מה-ID.');
    log('שחזור: node cloud.js --recover <רסיס> <רסיס> <רסיס>');
    return;
  }

  if (args[0] === '--import') {
    if (!args[1]) { log('שימוש: node cloud.js --import <file.zip> [--dir <תיקיית-יעד>]'); return; }
    const di = args.indexOf('--dir');
    ensureId(); deriveKeys();
    await cloud.importZip(args[1], di > 0 ? { dir: args[di + 1] } : {});
    log('הייבוא הושלם. הרץ "node cloud.js --deploy" כדי לבנות ולפרסם לכתובת הגלובלית.');
    return;
  }

  if (args[0] === '--recover') {
    if (args.length < 3) { log('שימוש: node cloud.js --recover <רסיס1> <רסיס2> [...]'); return; }
    const secret = shamirJoin(args.slice(1));
    log('Δ2: הסוד המשוחזר: ' + secret);
    if (!/^SC-[0-9A-F]{40}$/.test(secret)) {
      log('אזהרה: התוצאה לא נראית כמו SYNC_ID תקין — כנראה סופקו פחות רסיסים');
      log('מהסף (K) שנקבע בפיצול, או רסיס שגוי. נסה שוב עם רסיסים נוספים.');
    }
    return;
  }

  if (args.includes('--status')) {
    ensureId();
    deriveKeys();
    log('SYNC_ID:', syncId);
    log('שם הפרויקט:', PROJECT_NAME, `[${SITE_SLUG}]`);
    log('כתובת האתר החי:', globalAddress());
    if (PUBLIC_WEBSITE) for (const m of mirrorAddresses().slice(1)) log('  מראה:', m);
    if (CUSTOM_DOMAIN) printDomainSetup();
    const sd = resolveSiteDir();
    if (sd) log('תיקיית האתר:', sd + '/');
    const bat = resolveBatEntry();
    if (bat) log(`קובץ הפעלה: ${bat.bat} → דף הבית: ${bat.html}`);
    if (BUILD_COMMAND) log('פקודת בנייה:', BUILD_COMMAND);
    const st = loadState();
    log('קבצים במעקב מקומי:', Object.keys(st.files ?? {}).length);
    return;
  }

  if (args.includes('--deploy')) cloud.forceVerifyBlobs = true;
  await cloud.ready();

  if (args.includes('--once') || args.includes('--deploy')) {
    log('סינכרון ופרסום הושלמו. המידע והאתר שמורים ברשת וימשיכו להתקיים גם כשהכול כבוי.');
    await cloud.stop();
    process.exit(0);
  }

  cloud.startLive();
  if (LOCAL_BRIDGE) cloud.bridge(LOCAL_BRIDGE); // גשר שרת מלא לכתובת הגלובלית
  if (MINING.enabled) cloud.startMining(args.includes('--allow-mining'));
  if (args.includes('--health')) setInterval(() => { for (const r of cloud.health()) log('Θ', r.relay, `lat=${r.latency}ms phi=${r.phi} score=${r.score}`); }, 15_000);
  const shutdown = async () => {
    log('סוגר… המידע כבר שמור ברשת ויהיה זמין מכל מקום.');
    await cloud.stop();
    process.exit(0);
  };
  process.on('SIGINT', shutdown);
  process.on('SIGTERM', shutdown);
}

if (require.main === module) {
  main().catch((err) => { console.error('[cloud] שגיאה קריטית:', err.message); process.exit(1); });
} else {
  // מוטמע כמודול בתוך אפליקציה — API שקט ללא UI
  module.exports = {
    ready: () => cloud.ready().then(() => cloud.startLive()).then(() => cloud),
    // ענן פרטי (מוצפן — רק מי שמחזיק את ה-SYNC_ID)
    set: (k, v) => cloud.set(k, v),
    get: (k) => cloud.get(k),
    getAll: () => cloud.getAll(),
    onChange: (fn) => cloud.onChange(fn),
    // נתונים חיים משותפים עם האתר הציבורי (webkv)
    webSet: (k, v) => cloud.webSet(k, v),
    webGet: (k) => cloud.webGet(k),
    webGetAll: () => cloud.webGetAll(),
    onWebChange: (fn) => cloud.onWebChange(fn),
    // רשת החישוב המבוזרת (SkyLattice) + העלאת קבצים ציבוריים
    serve: (handlers) => cloud.serve(handlers),
    run: (task, payload, timeoutMs) => cloud.run(task, payload, timeoutMs),
    upload: (input, type) => cloud.upload(input, type),
    // שרת אפליקציה חי על הכתובת הגלובלית (SkyLattice-Ω)
    route: (method, path, fn, opts) => cloud.route(method, path, fn, opts),
    bridge: (url) => cloud.bridge(url),
    fetchApi: (p, init) => cloud.fetchApi(p, init),
    // תמונת-API סטטית — נענית מהרשת גם ללא אף עובד-שרת חי (ללא-מפעיל)
    snapshot: (p, value, opts) => cloud.snapshot(p, value, opts),
    // ייבוא מערכת שלמה מ-ZIP + ניטור בריאות עצמי (Θ)
    importZip: (zip, opts) => cloud.importZip(zip, opts),
    health: () => cloud.health(),
    // הודעות מוצפנות מקצה-לקצה (Ψ) — התכתבות עם משתמשי האתר
    sendTo: (pub, value) => cloud.sendTo(pub, value),
    onMessage: (fn) => cloud.onMessage(fn),
    messages: () => cloud.messages(),
    address: () => { ensureId(); deriveKeys(); return cloud.address(); },
    stop: () => cloud.stop(),
    get id() { ensureId(); return syncId; },
    // הזהות הציבורית של הפרויקט — אליה משתמשי האתר שולחים הודעות (SkyCloud.owner)
    get pubkey() { ensureId(); if (!keys) deriveKeys(); return keys.pubkey; },
  };
}

// חשיפת פונקציות פנימיות לבדיקות בלבד (רץ רק כשמוגדר משתנה סביבה SC_TEST)
if (process.env.SC_TEST && module.exports) { module.exports.__test = { encrypt, decrypt, signEvent, verifyEvent, schnorrVerify, ensureId, deriveKeys, sha256, rsEncode, rsDecode, shamirSplit, shamirJoin, ecdhKey, psiSeal, psiOpen, ptMul, G, N, b32, big, hex, unhex, unzip, hlcNow, hlcObserve, RelayHealth, RelayPool, zlib, buildWebSdk, pickWeighted, shuffle, runFingerprint, metaPad, RUN_SALT }; }
