#!/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 --new-id   ×™×¦×™×¨×ª SYNC_ID ×—×“×© (×¢× ×Ÿ ×—×“×© ×¨×™×§)
 *
 *  ××™×š ×–×” ×¢×•×‘×“ ×‘×œ×™ ×©×¨×ª ×•×‘×œ×™ ×©×ž×©×”×• ×©×œ×š ×“×•×œ×§:
 *      ×”×ž×™×“×¢ ×ž×•×¦×¤×Ÿ (AES-256-GCM) ×•×ž×©×•×’×¨ ×œ×¢×©×¨×•×ª ×ž×ž×¡×¨×™× ×¦×™×‘×•×¨×™×™× ×¤×ª×•×—×™× ×©×œ
 *      ×¨×©×ª Nostr â€” ×¨×©×ª ×’×œ×•×‘×œ×™×ª ×ž×‘×•×–×¨×ª ×©×”×™× ×—×œ×§ ×ž×”××™× ×˜×¨× ×˜. ×”×ž×™×“×¢ ×ž×©×•×›×¤×œ
 *      ×‘×›×ž×” ×ž×§×•×ž×•×ª ×‘×¢×•×œ× ×‘×ž×§×‘×™×œ, ×•× ×©××¨ ×©×ž×•×¨ ×©× ×’× ×›×©×”×›×•×œ ××¦×œ×š ×›×‘×•×™.
 *      ×ž×™ ×©×ž×—×–×™×§ ××ª ××•×ª×• SYNC_ID (×•×¨×§ ×”×•×) ×™×›×•×œ ×œ×ž×©×•×š ×•×œ×¤×¢× ×— ××ª ×”×ž×™×“×¢,
 *      ×ž×›×œ ×ž×§×•× ×‘×¢×•×œ×. ×ª×ž×™×“ ×ž× ×¦×— ×”×¢×“×›×•×Ÿ ×”××—×¨×•×Ÿ (last-write-wins).
 *
 *  ×”×›×ª×•×‘×ª ×”×’×œ×•×‘×œ×™×ª:
 *      ×”×›×œ×™ ×ž×¤×¨×¡× ×¢×ž×•×“ ×¡×˜×˜×•×¡ ×—×ª×•× ×œ×¨×©×ª, ×©×–×ž×™×Ÿ ×ª×ž×™×“ ×‘×›×ª×•×‘×ª ×§×‘×•×¢×” ×“×¨×š
 *      ×©×¢×¨×™ ×¨×©×ª ×¦×™×‘×•×¨×™×™× (×ž×•×“×¤×¡×ª ×‘×”×¤×¢×œ×”) â€” ×’× ×›×©×›×œ ×”×ž×›×©×™×¨×™× ×©×œ×š ×›×‘×•×™×™×.
 * â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
 */

// â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ ×”×’×“×¨×•×ª (××¤×©×¨ ×œ×¢×¨×•×š) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

// ×”-ID ×©×œ ×”×¢× ×Ÿ. 'AUTO' = ×™×™×•×•×¦×¨ ×œ×‘×“ ×‘×”×¨×¦×” ×”×¨××©×•× ×” ×•×™×™×›×ª×‘ ×œ×›××Ÿ ××•×˜×•×ž×˜×™×ª.
// ×›×“×™ ×œ×—×‘×¨ ×ž×§×•× × ×•×¡×£ ×œ××•×ª×• ×¢× ×Ÿ â€” ×ž×¢×ª×™×§×™× ×œ×©× ××ª ×”×§×•×‘×¥ ×¢× ××•×ª×• SYNC_ID.
const SYNC_ID = 'SC-088443EA598695967DFE51BB903FD5FC4BE9EF63';

// ××™×œ×• ×§×‘×¦×™×/×ª×™×§×™×•×ª ×œ× ×œ×¡× ×›×¨×Ÿ
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',
];

// ××ª×¨ ×¦×™×‘×•×¨×™ ×—×™ (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'];

const CHUNK_SIZE = 16 * 1024;      // ×’×•×“×œ ×—×œ×§ ×œ×§×•×‘×¥ (16KB ×œ×¤× ×™ ×”×¦×¤× ×”, ×‘×˜×•×— ×œ×›×œ ×ž×ž×¡×¨)
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 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);

// â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 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))]);
}

// â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 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),
  };
}

const encrypt = (buf) => {
  const iv = crypto.randomBytes(12);
  const c = crypto.createCipheriv('aes-256-gcm', keys.encKey, iv);
  const enc = Buffer.concat([c.update(buf), c.final()]);
  return Buffer.concat([iv, c.getAuthTag(), enc]).toString('base64');
};
const decrypt = (b64) => {
  const raw = Buffer.from(b64, 'base64');
  const d = crypto.createDecipheriv('aes-256-gcm', keys.encKey, raw.subarray(0, 12));
  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);

// â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ ×—×™×‘×•×¨ ×œ×ž×ž×¡×¨×™× â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

class RelayPool {
  constructor(urls) {
    this.urls = 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.closed = false;
  }

  connectAll() {
    for (const url of this.urls) this._connect(url);
  }

  _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) => {
      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) { p.oks.add(url); if (p.oks.size >= p.target) p.resolve(p.oks.size); }
      } else if (msg[0] === 'EVENT') {
        // ×”×¡×™× ×•×Ÿ ×œ×¤×™ ×ž×—×‘×¨ × ×¢×©×” ×‘×¤×™×œ×˜×¨×™× ×©×œ ×›×œ ×ž× ×•×™ (×›×“×™ ×œ××¤×©×¨ ×’× × ×ª×•× ×™ ×ž×‘×§×¨×™×)
        const sub = this.subs.get(msg[1]);
        if (sub && msg[2]) 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(),
        resolve: (count) => { clearTimeout(timer); this.pendingOk.delete(ev.id); resolve(count); },
        target,
      };
      const timer = setTimeout(() => {
        this.pendingOk.delete(ev.id);
        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 ws of this.sockets.values()) { 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 P16 = PROJECT.slice(0, 16);
const PRE = { kv: 'scweb:' + P16 + ':', usr: 'scusr:' + P16 + ':', ud: 'scud:' + P16 + ':', job: 'scjob:' + P16 + ':', res: 'scres:' + 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))]);
}

/* â”€â”€â”€ ×ž××’×¨×™ × ×ª×•× ×™× ×—×™×™× â”€â”€â”€ */
const sockets = new Set();
const kv = {};        /* × ×ª×•× ×™× ×ž×©×•×ª×¤×™× (×¦×™×‘×•×¨×™) */
const usersReg = {};  /* name -> {pub, ts} â€” ×”×¨×©×ž×” ×ž×•×§×“×ž×ª ×ž× ×¦×—×ª */
const userData = {};  /* pub16:key -> {iv, ct, ts} â€” ×ž×•×¦×¤×Ÿ ×¤×¨-×ž×©×ª×ž×© */
const results = {};   /* jobId -> ×ª×•×¦××ª ×—×™×©×•×‘ */
const resWaiters = {};
let handlers = null;
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);
  }
}

/* ×‘×—×™×¨×ª ×ž×‘×¦×¢ ×“×˜×¨×ž×™× ×™×¡×˜×™×ª (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 {}
}

/* â”€â”€â”€ ×ž×©×ª×ž×©×™×: ×”×–×”×•×ª × ×’×–×¨×ª ×ž×ª×ž×˜×™×ª ×ž×©×+×¡×™×¡×ž×”, ×œ× ×ž××•×—×¡× ×ª ×‘×©×•× ×ž×§×•× â”€â”€â”€ */
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 {}
  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 = (m) => {
    let msg;
    try { msg = JSON.parse(m.data); } catch { return; }
    if (msg[0] === 'EVENT' && msg[2]) merge(msg[2], true);
    else if (msg[0] === 'EOSE') readyResolve();
  };
  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 â€” ×œ× × ×ª×§×¢×™× */

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,

  /* ×—×•×•×ª ×”×©×¨×ª×™× ×”×ž×‘×•×–×¨×ª â€” ×›×œ ×˜××‘ ×•×›×œ ×ž×›×©×™×¨ ×”× ×¢×•×‘×“×™-×—×™×©×•×‘ */
  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);
      resWaiters[id] = (r) => { clearTimeout(t); r.ok ? resolve(r.value) : reject(new Error(r.error)); };
    });
  },
  serve: (h) => { handlers = Object.assign(handlers || {}, h); },
};
})();
`;
}

// â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ 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 };
}

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 pathHash = (rel) => hex(sha256(Buffer.from('p:' + rel))).slice(0, 20);

class Cloud {
  constructor() {
    this.pool = new RelayPool(RELAYS);
    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.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() {
    // × ×ª×•× ×™ webkv â€” ×ž×›×œ ×”×ž×—×‘×¨×™× (×’× ×ž×‘×§×¨×™ ×”××ª×¨), ×ž×¡×•×ž× ×™× ×‘-p ×©×œ ×”×¤×¨×•×™×§×˜.
    // ×¨×¥ ×‘×ž×§×‘×™×œ ×œ×ž×©×™×›×ª ×”×ž× ×™×¤×¡×˜, ×•×©× ×™×”× ×ž×¡×ª×™×™×ž×™× ×œ×¤× ×™ ×©-ready() ×ž×¡×ª×™×™×.
    const webPull = this.pool.query([{ kinds: [KIND_DATA], '#p': [keys.pubkey] }])
      .then((evs) => { for (const ev of evs) this._mergeWebEvent(ev, false); })
      .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).toString('utf8'));
        if (d === dManifest() && (payload.updatedAt ?? 0) >= (this.manifest.updatedAt ?? 0)) {
          this.manifest = payload;
        } else if (d === dKv()) {
          this._mergeKv(payload, false);
        }
      } catch (err) {
        log('××–×”×¨×”: ××™×¨×•×¢ ×¨×©×ª ×œ× × ×™×ª×Ÿ ×œ×¤×¢× ×•×— (×™×™×ª×›×Ÿ ×ž-ID ××—×¨):', err.message);
      }
    }
  }

  /** ×”×©×•×•××ª ×ž×§×•×ž×™ ×ž×•×œ ×¢× ×Ÿ â€” ×ž×™ ×¢×“×›× ×™ ×™×•×ª×¨ ×ž× ×¦×—, ×œ×©× ×™ ×”×›×™×•×•× ×™× */
  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 dTags = Array.from({ length: r.chunks }, (_, i) => dChunk(ph, i));
      const parts = new Array(r.chunks).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());
            const payload = JSON.parse(decrypt(ev.content).toString('utf8'));
            if (payload.h === r.hash && Number.isInteger(idx)) parts[idx] = Buffer.from(payload.data, 'base64');
          } catch {}
        }
      }
      if (parts.some((p) => p === null)) { log('××–×”×¨×”: ×—×œ×§×™× ×—×¡×¨×™× ×‘×¨×©×ª ×¢×‘×•×¨', rel, '- ×™× ×•×¡×” ×©×•×‘ ×‘×¡×‘×‘ ×”×‘×'); return; }
      const data = Buffer.concat(parts);
      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 content = encrypt(Buffer.from(JSON.stringify({ h: hash, i, data: slice.toString('base64') })));
        await this.pool.publish(makeEvent(KIND_DATA, dChunk(ph, i), content));
        if (chunks > 3) await sleep(120); // ×¢×“×™× ×•×ª ×ž×•×œ ×”×ž×ž×¡×¨×™× ×‘×§×‘×¦×™× ×’×“×•×œ×™×
      }
      const ts = info.mtime;
      this.manifest.files[rel] = { hash, size: data.length, mtime: info.mtime, chunks, ts };
      this.state.files[rel] = { hash, ts };
      log('×”×•×¢×œ×” ×œ×¢× ×Ÿ:', rel, `(${data.length.toLocaleString()} bytes, ${chunks} ×—×œ×§×™×)`);
    } catch (err) {
      log('×©×’×™××” ×‘×”×¢×œ××ª', rel + ':', err.message);
    }
  }

  async _publishManifest() {
    this.publishedThisRun = true;
    this.manifest.updatedAt = Date.now();
    this.manifest.device = DEVICE;
    const content = encrypt(Buffer.from(JSON.stringify(this.manifest)));
    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`],
    ], '');
    // ×ž× ×™×¤×¡×˜ ×”××ª×¨ ×—×™×™×‘ ×œ×”×’×™×¢ ×œ×›×ž×” ×©×™×•×ª×¨ ×ž×ž×¡×¨×™× â€” ×–×” ×ž×” ×©×”×©×¢×¨×™× ×§×•×¨××™×
    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 ?? {})) {
      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: Date.now() };
    const content = encrypt(Buffer.from(JSON.stringify(this.kv)));
    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;
    }
    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;
      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: Date.now() };
    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);
    log(`×¢×•×‘×“-×—×™×©×•×‘ ×¤×¢×™×œ: ${Object.keys(this.jobHandlers).join(', ')}`);
  }

  /** ×©×™×’×•×¨ ×¢×‘×•×“×ª ×—×™×©×•×‘ ×œ×¨×©×ª â€” ×›×œ ×¢×•×‘×“ ×–×ž×™×Ÿ (Node ××• ×“×¤×“×¤×Ÿ) ×™×¢× ×” */
  async run(task, payload, timeoutMs = 20_000) {
    await this.ready();
    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);
      this.resWaiters[id] = (res) => {
        clearTimeout(timer);
        res.ok ? resolve(res.value) : reject(new Error(res.error));
      };
    });
  }

  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); }
  }

  /** ×”×¢×œ××ª ×§×•×‘×¥ ×¦×™×‘×•×¨×™ ×œ×¨×©×ª ×”××—×¡×•×Ÿ ×”×ž×‘×•×–×¨×ª â€” ×ž×§×‘×œ 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 };
  }

  // â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ ×ž×¦×‘ ×—×™: ×ž× ×•×™ + ×¦×¤×™×™×” ×‘×§×‘×¦×™× â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

  _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).toString('utf8'));
          if (d === dKv()) { this._mergeKv(payload); return; }
          if (d === dManifest() && payload.device !== DEVICE && payload.updatedAt > this.manifest.updatedAt) {
            this.manifest = 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);

    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);
    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.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();
  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),
    address: () => { ensureId(); deriveKeys(); return cloud.address(); },
    stop: () => cloud.stop(),
    get id() { ensureId(); return syncId; },
  };
}

if (process.env.SDK_DUMP) { ensureId(); deriveKeys(); fs.writeFileSync('sdk-out.js', buildWebSdk()); }