commit 85454f9737dc84bdeeb2e4ab541ef974d560367c Author: The Howling Whispers Date: Tue Jun 30 16:57:28 2026 +0200 Initial commit: Character sandbox with React+Express frontend/backend, SvelteKit foundation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a422975 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +build/ +.svelte-kit/ +data/ +*.db +.env +.env.local +.vite/ +dist/ +.tmp/ +*.log +.DS_Store diff --git a/client b/client new file mode 160000 index 0000000..b9e538e --- /dev/null +++ b/client @@ -0,0 +1 @@ +Subproject commit b9e538e1914933ac4eedbdf75328737e7df70caf diff --git a/package.json b/package.json new file mode 100644 index 0000000..efb7998 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "character-sandbox-monorepo", + "version": "1.0.0", + "description": "A monorepo for the advanced character sandbox application.", + "main": "index.js", + "scripts": { + "start": "concurrently \"npm run start --prefix client\" \"npm run start --prefix server\"", + "install-all": "npm install --prefix client && npm install --prefix server", + "dev": "concurrently \"npm run dev --prefix client\" \"npm run dev --prefix server\"" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "concurrently": "^8.2.2" + }, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^4.3.0", + "chalk": "^4.1.2", + "cliui": "^8.0.1", + "color-convert": "^2.0.1", + "color-name": "^1.1.4", + "date-fns": "^2.30.0", + "emoji-regex": "^8.0.0", + "escalade": "^3.2.0", + "get-caller-file": "^2.0.5", + "has-flag": "^4.0.0", + "is-fullwidth-code-point": "^3.0.0", + "lodash": "^4.18.1", + "require-directory": "^2.1.1", + "rxjs": "^7.8.2", + "shell-quote": "^1.9.0", + "spawn-command": "^0.0.2", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "tslib": "^2.8.1", + "wrap-ansi": "^7.0.0", + "y18n": "^5.0.8", + "yargs": "^17.7.3", + "yargs-parser": "^21.1.1" + } +} diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..68143a7 --- /dev/null +++ b/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Wrapper for the Sandbox SvelteKit process. +set -e +DIR="/var/www/sandbox" +PORT="${PORT:-2027}" +HOST="${HOST:-127.0.0.1}" +ORIGIN="${ORIGIN:-https://sandbox.thehowlingwhispers.com}" +export PORT HOST ORIGIN + +if [ -f "$DIR/.env" ]; then + set -a + # shellcheck disable=SC1090,SC1091 + source "$DIR/.env" + set +a +fi + +exec node "$DIR/build/index.js" diff --git a/scripts/netcup-dns.sh b/scripts/netcup-dns.sh new file mode 100755 index 0000000..6bfc0b7 --- /dev/null +++ b/scripts/netcup-dns.sh @@ -0,0 +1,268 @@ +#!/bin/bash +# /var/www/rp/scripts/netcup-dns.sh +# Reusable Netcup DNS tool for thehowlingwhispers.com (and any Netcup-managed zone). +# +# Credentials: /root/.netcup-creds (chmod 600, NETCUP_CUSTOMER / NETCUP_API_KEY / NETCUP_API_PASSWORD) +# Audit log: /var/log/netcup-dns.log +# +# Usage: +# netcup-dns.sh list # list all records +# netcup-dns.sh add # add a record (idempotent) +# netcup-dns.sh delete # delete a record +# netcup-dns.sh --dry-run add ... # preview without API calls +# netcup-dns.sh --yes add ... # skip confirmation prompt +# netcup-dns.sh --json list ... # raw JSON output +# +# Examples: +# netcup-dns.sh list thehowlingwhispers.com +# netcup-dns.sh add thehowlingwhispers.com rp A 159.195.194.180 +# netcup-dns.sh add thehowlingwhispers.com sandbox CNAME rp.thehowlingwhispers.com +# netcup-dns.sh add thehowlingwhispers.com @ TXT "v=spf1 -all" +# netcup-dns.sh --dry-run add thehowlingwhispers.com blog A 159.195.194.180 +# netcup-dns.sh --json list thehowlingwhispers.com + +set -euo pipefail + +# ─── config ──────────────────────────────────────────────────── +CREDS_FILE="${NETCUP_CREDS_FILE:-/root/.netcup-creds}" +LOG_FILE="${NETCUP_DNS_LOG:-/var/log/netcup-dns.log}" +API_BASE="https://ccp.netcup.net/api/v1" +JQ_FORMAT='@json' # not used; kept for clarity + +# ─── colors ─────────────────────────────────────────────────── +if [ -t 1 ]; then + C_RED=$'\e[31m'; C_GRN=$'\e[32m'; C_YEL=$'\e[33m'; C_DIM=$'\e[2m'; C_RST=$'\e[0m' +else + C_RED=''; C_GRN=''; C_YEL=''; C_DIM=''; C_RST='' +fi +ok() { printf '%s✓%s %s\n' "$C_GRN" "$C_RST" "$*"; } +warn() { printf '%s!%s %s\n' "$C_YEL" "$C_RST" "$*" >&2; } +err() { printf '%s✗%s %s\n' "$C_RED" "$C_RST" "$*" >&2; } + +# ─── flag parsing ───────────────────────────────────────────── +DRY_RUN=false +ASSUME_YES=false +JSON_OUT=false +SUBCMD="" +ARGS=() + +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --yes|-y) ASSUME_YES=true; shift ;; + --json) JSON_OUT=true; shift ;; + --help|-h) + sed -n '2,28p' "$0" + exit 0 + ;; + list|add|delete) + SUBCMD="$1"; shift; ARGS=("$@"); break ;; + *) + err "Unknown flag or command: $1"; exit 2 ;; + esac +done + +# ─── log helper ─────────────────────────────────────────────── +log() { + printf '%s [%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" "$2" >> "$LOG_FILE" +} + +# ─── load creds (chmod 600 enforced) ──────────────────────── +if [ ! -f "$CREDS_FILE" ]; then + err "Credentials file not found: $CREDS_FILE" + err "Create it with NETCUP_CUSTOMER / NETCUP_API_KEY / NETCUP_API_PASSWORD (chmod 600)" + exit 1 +fi +PERMS=$(stat -c '%a' "$CREDS_FILE" 2>/dev/null || echo "") +if [ "$PERMS" != "600" ] && [ "$PERMS" != "400" ]; then + err "Credentials file must be chmod 600 or 400 (currently: $PERMS). Refusing to run." + exit 1 +fi +# shellcheck disable=SC1090 +source "$CREDS_FILE" + +if [ -z "${NETCUP_CUSTOMER:-}" ] || [ -z "${NETCUP_API_KEY:-}" ] || [ -z "${NETCUP_API_PASSWORD:-}" ]; then + err "Missing one of NETCUP_CUSTOMER / NETCUP_API_KEY / NETCUP_API_PASSWORD in $CREDS_FILE" + exit 1 +fi + +# ─── API helpers ───────────────────────────────────────────── +# Netcup requires X-Api-Key + X-Customer-Number headers, plus +# X-Session-Id for authenticated calls. Login returns sessionId. +SESSION_ID="" +SESSION_LOGGED_IN_AT=0 +SESSION_TTL=240 # refresh session every 4 minutes + +login() { + local now + now=$(date +%s) + if [ -n "$SESSION_ID" ] && [ $((now - SESSION_LOGGED_IN_AT)) -lt $SESSION_TTL ]; then + return 0 + fi + local response http_code body + response=$(curl -sS -w '\n%{http_code}' -X POST \ + -H "X-Api-Key: ${NETCUP_API_KEY}" \ + -H "X-Customer-Number: ${NETCUP_CUSTOMER}" \ + -H "Content-Type: application/json" \ + "${API_BASE}/login") + http_code=$(printf '%s' "$response" | tail -n1) + body=$(printf '%s' "$response" | sed '$d') + if [ "$http_code" != "200" ]; then + err "Login failed (HTTP $http_code): $body" + log "ERROR" "login failed: HTTP $http_code" + exit 1 + fi + SESSION_ID=$(printf '%s' "$body" | jq -r '.sessionId') + SESSION_LOGGED_IN_AT=$now + log "INFO" "login OK (session refreshed)" +} + +api() { + local method=$1; shift + local path=$1; shift + local body=${1:-} + login + local args=(-sS -w '\n%{http_code}' -X "$method" -H "X-Session-Id: $SESSION_ID" -H "Content-Type: application/json") + if [ -n "$body" ]; then + args+=(-d "$body") + fi + curl "${args[@]}" "${API_BASE}${path}" +} + +# ─── subdomain ────────────────────────────────────────────── +require_domain() { + if [ -z "${1:-}" ]; then err "Domain required"; exit 1; fi + echo "$1" +} +require_arg() { + if [ -z "${1:-}" ]; then err "$2 required"; exit 1; fi + echo "$1" +} + +# ─── subcommands ───────────────────────────────────────────── +cmd_list() { + local domain + domain=$(require_domain "${ARGS[0]:-}") + local response http_code body + response=$(api GET "/dns/${domain}") + http_code=$(printf '%s' "$response" | tail -n1) + body=$(printf '%s' "$response" | sed '$d') + if [ "$http_code" != "200" ]; then + err "List failed (HTTP $http_code): $body" + log "ERROR" "list $domain failed: HTTP $http_code" + exit 1 + fi + if $JSON_OUT; then + printf '%s\n' "$body" + else + printf '%s%-8s %-30s %-25s %s%s\n' "$C_DIM" "TYPE" "HOST" "VALUE" "TTL" "$C_RST" + printf '%s\n' "$body" | jq -r '.[] | [.type, .hostname, (.value|tostring|.[0:25]), .ttl] | @tsv' | \ + while IFS=$'\t' read -r t h v ttl; do + printf '%-8s %-30s %-25s %s\n' "$t" "$h" "$v" "$ttl" + done + fi + log "INFO" "list $domain (HTTP $http_code, $(printf '%s' "$body" | jq 'length') records)" +} + +cmd_add() { + local domain host type value + domain=$(require_domain "${ARGS[0]:-}") + host=$(require_arg "${ARGS[1]:-}" "host") + type=$(require_arg "${ARGS[2]:-}" "type") + value=$(require_arg "${ARGS[3]:-}" "value") + + # Idempotency check: list first + local list_body + list_body=$(api GET "/dns/${domain}" | sed '$d') + local existing + existing=$(printf '%s' "$list_body" | jq -r --arg h "$host" --arg t "$type" --arg v "$value" \ + '.[] | select(.hostname == $h and .type == $t and (.value|tostring) == $v) | .id') + if [ -n "$existing" ] && [ "$existing" != "null" ]; then + ok "Already exists: $domain $host $type $value (recordId=$existing) — no-op" + log "INFO" "add $domain $host $type $value — no-op (already exists, id=$existing)" + return 0 + fi + + warn "About to add: $domain $host $type $value" + if ! $ASSUME_YES && [ -t 0 ]; then + read -r -p "Continue? [y/N] " reply + case "$reply" in + y|Y|yes|YES) ;; + *) err "Aborted."; exit 1 ;; + esac + fi + + if $DRY_RUN; then + ok "[dry-run] would POST $host $type $value to $domain" + log "INFO" "add $domain $host $type $value — DRY RUN" + return 0 + fi + + # Netcup payload format + local payload + payload=$(jq -n --arg h "$host" --arg t "$type" --arg v "$value" \ + '{hostname:$h, type:$t, value:$v, ttl:3600}') + + local response http_code body + response=$(api POST "/dns/${domain}" "$payload") + http_code=$(printf '%s' "$response" | tail -n1) + body=$(printf '%s' "$response" | sed '$d') + if [ "$http_code" != "200" ] && [ "$http_code" != "201" ]; then + err "Add failed (HTTP $http_code): $body" + log "ERROR" "add $domain $host $type $value failed: HTTP $http_code" + exit 1 + fi + local new_id + new_id=$(printf '%s' "$body" | jq -r '.id // .recordId // empty') + ok "Added: $domain $host $type $value (recordId=$new_id)" + log "INFO" "add $domain $host $type $value OK (id=$new_id)" +} + +cmd_delete() { + local domain record_id + domain=$(require_domain "${ARGS[0]:-}") + record_id=$(require_arg "${ARGS[1]:-}" "recordId") + + warn "About to delete: $domain recordId=$record_id" + if ! $ASSUME_YES && [ -t 0 ]; then + read -r -p "Continue? [y/N] " reply + case "$reply" in + y|Y|yes|YES) ;; + *) err "Aborted."; exit 1 ;; + esac + fi + + if $DRY_RUN; then + ok "[dry-run] would DELETE recordId=$record_id from $domain" + log "INFO" "delete $domain $record_id — DRY RUN" + return 0 + fi + + local response http_code body + response=$(api DELETE "/dns/${domain}/${record_id}") + http_code=$(printf '%s' "$response" | tail -n1) + body=$(printf '%s' "$response" | sed '$d') + if [ "$http_code" != "200" ] && [ "$http_code" != "204" ]; then + err "Delete failed (HTTP $http_code): $body" + log "ERROR" "delete $domain $record_id failed: HTTP $http_code" + exit 1 + fi + ok "Deleted: $domain recordId=$record_id" + log "INFO" "delete $domain $record_id OK" +} + +# ─── dispatch ──────────────────────────────────────────────── +case "$SUBCMD" in + list) cmd_list ;; + add) cmd_add ;; + delete) cmd_delete ;; + "") + err "Subcommand required (list|add|delete). Try --help." + sed -n '2,28p' "$0" >&2 + exit 2 + ;; + *) + err "Unknown subcommand: $SUBCMD" + exit 2 + ;; +esac diff --git a/server/ai/trainer.js b/server/ai/trainer.js new file mode 100644 index 0000000..32adccd --- /dev/null +++ b/server/ai/trainer.js @@ -0,0 +1,196 @@ +import db from '../db.js'; + +const LLM_HOST = process.env.LLM_HOST || null; +const LLM_MODEL = process.env.LLM_MODEL || 'qwen7b.Q4_K_M.gguf'; + +async function queryLLM(messages) { + if (!LLM_HOST) return null; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 180000); + try { + const response = await fetch(`${LLM_HOST}/v1/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ model: LLM_MODEL, messages, max_tokens: 500, temperature: 0.8, stream: false }), + signal: controller.signal, + }); + clearTimeout(timeout); + if (!response.ok) return null; + const data = await response.json(); + return data.choices?.[0]?.message?.content || null; + } catch (e) { + clearTimeout(timeout); + return null; + } +} + +const DEFAULT_KNOWLEDGE = { + character_archetypes: [ + { + name: 'The Hero', + traits: ['brave', 'selfless', 'determined', 'courageous'], + backstory_template: 'A {adjective} individual driven by {motivation} to {goal}.', + needs_priority: { Food: 3, Energy: 4, Intimate: 1 } + }, + { + name: 'The Sage', + traits: ['wise', 'patient', 'introspective', 'knowledgeable'], + backstory_template: 'Through years of study and experience, this {adjective} soul has gained {knowledge}.', + needs_priority: { Food: 1, Energy: 2, Intimate: 1 } + }, + { + name: 'The Rogue', + traits: ['cunning', 'adaptive', 'independent', 'resourceful'], + backstory_template: 'Living on the edge, this {adjective} character uses their {skill} to survive.', + needs_priority: { Food: 4, Energy: 3, Intimate: 3 } + }, + { + name: 'The Caregiver', + traits: ['nurturing', 'selfless', 'compassionate', 'protective'], + backstory_template: 'Driven by {motivation}, this {adjective} soul puts others before themselves.', + needs_priority: { Food: 2, Energy: 3, Intimate: 2 } + }, + { + name: 'The Wild One', + traits: ['untamed', 'instinctual', 'free-spirited', 'primal'], + backstory_template: 'Born of {origin}, this {adjective} being follows the call of the wild.', + needs_priority: { Food: 5, Energy: 5, Bladder: 4, Bowel: 4, Hormones: 5, Intimate: 5 } + } + ], + name_generators: { + fantasy: ['Kaelen', 'Lyra', 'Thorn', 'Elara', 'Bryn', 'Zephyr', 'Mira', 'Orion', 'Sable', 'Finn'], + modern: ['Alex', 'Jordan', 'Riley', 'Sam', 'Taylor', 'Morgan', 'Casey', 'Avery', 'Quinn', 'Drew'], + gothic: ['Vladimir', 'Isabella', 'Mortimer', 'Ophelia', 'Caspian', 'Seraphina', 'Damien', 'Raven', 'Lucian', 'Vesper'], + cyberpunk: ['Neon', 'Pixel', 'Cipher', 'Blade', 'Vex', 'Synthia', 'Zero', 'Echo', 'Byte', 'Nyx'] + }, + motivations: ['justice', 'knowledge', 'power', 'love', 'survival', 'redemption', 'freedom', 'discovery', 'revenge', 'balance'], + origins: ['the ancient forests', 'a forgotten kingdom', 'the stars above', 'the depths of the sea', 'a laboratory', 'the void between worlds', 'a nomadic tribe', 'an order of knights'] +}; + +function getRandomItem(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function generateName(style) { + const generators = DEFAULT_KNOWLEDGE.name_generators; + const names = generators[style] || generators.fantasy; + return getRandomItem(names); +} + +function generateArchetype() { + const archetypes = DEFAULT_KNOWLEDGE.character_archetypes; + const archetype = getRandomItem(archetypes); + const adjective = getRandomItem(archetype.traits); + const motivation = getRandomItem(DEFAULT_KNOWLEDGE.motivations); + const origin = getRandomItem(DEFAULT_KNOWLEDGE.origins); + const goal = getRandomItem(['save their people', 'find the truth', 'protect the innocent', 'gain ultimate power', 'achieve inner peace', 'survive the coming storm']); + const knowledge = getRandomItem(['ancient wisdom', 'forgotten secrets', 'the art of diplomacy', 'alchemical mastery', 'technological prowess']); + + const backstory = archetype.backstory_template + .replace('{adjective}', adjective) + .replace('{motivation}', motivation) + .replace('{goal}', goal) + .replace('{knowledge}', knowledge) + .replace('{origin}', origin) + .replace('{skill}', `${motivation} and ${adjective}ness`); + + return { + archetype: archetype.name, + traits: archetype.traits, + backstory, + needs_priority: archetype.needs_priority, + motivation, + origin + }; +} + +function getTrainingData(userId) { + const data = db.prepare('SELECT prompt, response, category FROM ai_training_data WHERE user_id = ? ORDER BY created_at DESC LIMIT 50').all(userId); + return data; +} + +function generateCharacterSuggestion(prompt, userId) { + const userTraining = getTrainingData(userId); + const archetype = generateArchetype(); + const nameStyle = prompt.toLowerCase().includes('fantasy') ? 'fantasy' : + prompt.toLowerCase().includes('cyber') ? 'cyberpunk' : + prompt.toLowerCase().includes('goth') ? 'gothic' : + prompt.toLowerCase().includes('modern') ? 'modern' : 'fantasy'; + const name = generateName(nameStyle); + + const defaultNeeds = [ + { name: 'Food', enabled: true, initial_value: 80, min_value: 0, max_value: 100, decay_rate: 1.5, priority: 3 }, + { name: 'Energy', enabled: true, initial_value: 90, min_value: 0, max_value: 100, decay_rate: 2, priority: 4 }, + { name: 'Bladder', enabled: true, initial_value: 30, min_value: 0, max_value: 100, decay_rate: 0.8, priority: 2 }, + { name: 'Bowel', enabled: false, initial_value: 20, min_value: 0, max_value: 100, decay_rate: 0.5, priority: 1 }, + { name: 'Hormones', enabled: true, initial_value: 40, min_value: 0, max_value: 100, decay_rate: 0.3, priority: 1 }, + { name: 'Intimate', enabled: true, initial_value: 30, min_value: 0, max_value: 100, decay_rate: 0.4, priority: 2 } + ]; + + const needsWithPriority = defaultNeeds.map(n => ({ + ...n, + priority: archetype.needs_priority[n.name] || n.priority + })); + + const response = { + name, + description: `A ${nameStyle.toLowerCase()} character embodying the ${archetype.archetype.toLowerCase()} archetype.`, + personality_traits: archetype.traits, + backstory: archetype.backstory, + suggested_needs: needsWithPriority, + suggested_ui_elements: needsWithPriority + .filter(n => n.enabled) + .map(n => ({ + need_name: n.name, + element_type: n.name === 'Energy' ? 'gauge' : 'progress_bar', + config: { color: getColorForNeed(n.name), label: n.name, animation: 'smooth' } + })), + reasoning: `Based on your request, I drew inspiration from the ${archetype.archetype.toLowerCase()} archetype. ${archetype.motivation} drives this character forward. I prioritized needs that match their instincts.` + }; + + if (userTraining.length > 0) { + const recentTraining = userTraining[0]; + response.training_influence = `Drawing from your past preferences: "${recentTraining.prompt}" -> "${recentTraining.response}"`; + } + + return response; +} + +function getColorForNeed(needName) { + const colors = { + Food: '#e74c3c', + Energy: '#f39c12', + Bladder: '#3498db', + Bowel: '#8e44ad', + Hormones: '#e91e63', + Intimate: '#ff6b6b' + }; + return colors[needName] || '#95a5a6'; +} + +async function processTrainingPrompt(prompt, userId) { + try { + const messages = [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: `Create a character based on this request: "${prompt}". Return ONLY valid JSON.` } + ]; + const ollamaResponse = await queryLLM(messages); + if (ollamaResponse) { + const cleaned = ollamaResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim(); + const parsed = JSON.parse(cleaned); + const valid = validateSuggestion(parsed); + if (valid) return parsed; + } + } catch { + // Fallback to rule-based + } + return generateCharacterSuggestion(prompt, userId); +} + +function validateSuggestion(s) { + if (!s.name || !s.description || !Array.isArray(s.personality_traits)) return false; + if (!Array.isArray(s.suggested_needs) || s.suggested_needs.length === 0) return false; + return true; +} + +export { generateCharacterSuggestion, processTrainingPrompt, DEFAULT_KNOWLEDGE }; diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..bbb3d6e --- /dev/null +++ b/server/db.js @@ -0,0 +1,103 @@ +import Database from 'better-sqlite3'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import bcrypt from 'bcrypt'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const db = new Database(path.join(__dirname, 'sandbox.db')); + +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS characters ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT DEFAULT '', + personality_traits TEXT DEFAULT '[]', + backstory TEXT DEFAULT '', + avatar_url TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS character_needs ( + id TEXT PRIMARY KEY, + character_id TEXT NOT NULL, + name TEXT NOT NULL, + enabled INTEGER DEFAULT 1, + initial_value REAL DEFAULT 50, + min_value REAL DEFAULT 0, + max_value REAL DEFAULT 100, + decay_rate REAL DEFAULT 1, + priority INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS character_ui_elements ( + id TEXT PRIMARY KEY, + character_id TEXT NOT NULL, + need_id TEXT, + element_type TEXT NOT NULL, + config TEXT DEFAULT '{}', + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE, + FOREIGN KEY (need_id) REFERENCES character_needs(id) ON DELETE SET NULL + ); + + CREATE TABLE IF NOT EXISTS character_brain_rules ( + id TEXT PRIMARY KEY, + character_id TEXT NOT NULL, + condition TEXT NOT NULL, + action TEXT NOT NULL, + priority INTEGER DEFAULT 0, + enabled INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS lorebooks ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS fragments ( + id TEXT PRIMARY KEY, + lorebook_id TEXT NOT NULL, + title TEXT NOT NULL, + content TEXT DEFAULT '', + tags TEXT DEFAULT '[]', + linked_characters TEXT DEFAULT '[]', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (lorebook_id) REFERENCES lorebooks(id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS ai_training_data ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + prompt TEXT NOT NULL, + response TEXT NOT NULL, + category TEXT DEFAULT 'general', + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); +`); + +export default db; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..f97d575 --- /dev/null +++ b/server/index.js @@ -0,0 +1,27 @@ +import express from 'express'; +import cors from 'cors'; +import authRoutes from './routes/auth.js'; +import characterRoutes from './routes/characters.js'; +import lorebookRoutes from './routes/lorebooks.js'; +import fragmentRoutes from './routes/fragments.js'; +import aiRoutes from './routes/ai.js'; + +const app = express(); +const PORT = process.env.PORT || 3001; + +app.use(cors()); +app.use(express.json()); + +app.use('/api/auth', authRoutes); +app.use('/api/characters', characterRoutes); +app.use('/api/lorebooks', lorebookRoutes); +app.use('/api/fragments', fragmentRoutes); +app.use('/api/ai', aiRoutes); + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +app.listen(PORT, () => { + console.log(`Character Sandbox API running on http://localhost:${PORT}`); +}); diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..c700573 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,26 @@ +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'sandbox-secret-key-change-in-production'; + +export function generateToken(userId) { + return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' }); +} + +export function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ error: 'Authentication required' }); + } + + jwt.verify(token, JWT_SECRET, (err, decoded) => { + if (err) { + return res.status(403).json({ error: 'Invalid or expired token' }); + } + req.userId = decoded.userId; + next(); + }); +} + +export { JWT_SECRET }; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..0cb3581 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2102 @@ +{ + "name": "character-sandbox-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "character-sandbox-server", + "version": "1.0.0", + "dependencies": { + "bcrypt": "^5.1.1", + "better-sqlite3": "^12.11.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3", + "uuid": "^9.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", + "integrity": "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.15.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.15.0.tgz", + "integrity": "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..c16563c --- /dev/null +++ b/server/package.json @@ -0,0 +1,20 @@ +{ + "name": "character-sandbox-server", + "version": "1.0.0", + "description": "Backend for the character sandbox", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "better-sqlite3": "^12.11.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3", + "uuid": "^9.0.0" + } +} diff --git a/server/routes/ai.js b/server/routes/ai.js new file mode 100644 index 0000000..7db6677 --- /dev/null +++ b/server/routes/ai.js @@ -0,0 +1,55 @@ +import { Router } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import db from '../db.js'; +import { authenticateToken } from '../middleware/auth.js'; +import { processTrainingPrompt, generateCharacterSuggestion, DEFAULT_KNOWLEDGE } from '../ai/trainer.js'; + +const router = Router(); + +router.post('/suggest', authenticateToken, async (req, res) => { + const { prompt } = req.body; + if (!prompt) return res.status(400).json({ error: 'Prompt is required' }); + + const suggestion = await processTrainingPrompt(prompt, req.userId); + res.json({ suggestion }); +}); + +router.post('/generate-name', authenticateToken, (req, res) => { + const { style } = req.body; + const name = generateCharacterSuggestion('generate name', req.userId).name; + res.json({ name }); +}); + +router.post('/train', authenticateToken, (req, res) => { + const { prompt, response, category } = req.body; + if (!prompt || !response) return res.status(400).json({ error: 'Prompt and response are required' }); + + const id = uuidv4(); + db.prepare('INSERT INTO ai_training_data (id, user_id, prompt, response, category) VALUES (?, ?, ?, ?, ?)') + .run(id, req.userId, prompt, response, category || 'general'); + + res.status(201).json({ message: 'Training data added', id }); +}); + +router.get('/training-data', authenticateToken, (req, res) => { + const data = db.prepare('SELECT id, prompt, response, category, created_at FROM ai_training_data WHERE user_id = ? ORDER BY created_at DESC').all(req.userId); + res.json({ training_data: data }); +}); + +router.delete('/training-data/:id', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM ai_training_data WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!existing) return res.status(404).json({ error: 'Training data not found' }); + db.prepare('DELETE FROM ai_training_data WHERE id = ?').run(req.params.id); + res.json({ message: 'Training data deleted' }); +}); + +router.get('/knowledge', authenticateToken, (req, res) => { + const archetypes = DEFAULT_KNOWLEDGE.character_archetypes.map(a => ({ + name: a.name, + traits: a.traits, + needs_priority: a.needs_priority + })); + res.json({ archetypes, name_styles: Object.keys(DEFAULT_KNOWLEDGE.name_generators) }); +}); + +export default router; diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..28ea142 --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,49 @@ +import { Router } from 'express'; +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; +import db from '../db.js'; +import { generateToken, authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +router.post('/register', (req, res) => { + const { username, email, password } = req.body; + if (!username || !email || !password) { + return res.status(400).json({ error: 'Username, email, and password are required' }); + } + + const existing = db.prepare('SELECT id FROM users WHERE username = ? OR email = ?').get(username, email); + if (existing) { + return res.status(409).json({ error: 'Username or email already exists' }); + } + + const id = uuidv4(); + const passwordHash = bcrypt.hashSync(password, 10); + db.prepare('INSERT INTO users (id, username, email, password_hash) VALUES (?, ?, ?, ?)').run(id, username, email, passwordHash); + + const token = generateToken(id); + res.status(201).json({ token, user: { id, username, email } }); +}); + +router.post('/login', (req, res) => { + const { username, password } = req.body; + if (!username || !password) { + return res.status(400).json({ error: 'Username and password are required' }); + } + + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); + if (!user || !bcrypt.compareSync(password, user.password_hash)) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + const token = generateToken(user.id); + res.json({ token, user: { id: user.id, username: user.username, email: user.email } }); +}); + +router.get('/me', authenticateToken, (req, res) => { + const user = db.prepare('SELECT id, username, email, created_at FROM users WHERE id = ?').get(req.userId); + if (!user) return res.status(404).json({ error: 'User not found' }); + res.json({ user }); +}); + +export default router; diff --git a/server/routes/characters.js b/server/routes/characters.js new file mode 100644 index 0000000..29beee6 --- /dev/null +++ b/server/routes/characters.js @@ -0,0 +1,238 @@ +import { Router } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import db from '../db.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +router.get('/', authenticateToken, (req, res) => { + const characters = db.prepare('SELECT * FROM characters WHERE user_id = ? ORDER BY updated_at DESC').all(req.userId); + res.json({ characters }); +}); + +router.post('/', authenticateToken, (req, res) => { + const { name, description, personality_traits, backstory } = req.body; + if (!name) return res.status(400).json({ error: 'Name is required' }); + + const id = uuidv4(); + const traits = JSON.stringify(personality_traits || []); + db.prepare('INSERT INTO characters (id, user_id, name, description, personality_traits, backstory) VALUES (?, ?, ?, ?, ?, ?)') + .run(id, req.userId, name, description || '', traits, backstory || ''); + + const character = db.prepare('SELECT * FROM characters WHERE id = ?').get(id); + res.status(201).json({ character }); +}); + +router.get('/:id', authenticateToken, (req, res) => { + const character = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!character) return res.status(404).json({ error: 'Character not found' }); + + const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ?').all(req.params.id); + const uiElements = db.prepare('SELECT * FROM character_ui_elements WHERE character_id = ?').all(req.params.id); + const brainRules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ?').all(req.params.id); + + res.json({ character, needs, ui_elements: uiElements, brain_rules: brainRules }); +}); + +router.put('/:id', authenticateToken, (req, res) => { + const { name, description, personality_traits, backstory, avatar_url } = req.body; + const existing = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!existing) return res.status(404).json({ error: 'Character not found' }); + + db.prepare('UPDATE characters SET name = ?, description = ?, personality_traits = ?, backstory = ?, avatar_url = ?, updated_at = datetime(\'now\') WHERE id = ?') + .run( + name || existing.name, + description !== undefined ? description : existing.description, + personality_traits ? JSON.stringify(personality_traits) : existing.personality_traits, + backstory !== undefined ? backstory : existing.backstory, + avatar_url !== undefined ? avatar_url : existing.avatar_url, + req.params.id + ); + + const character = db.prepare('SELECT * FROM characters WHERE id = ?').get(req.params.id); + res.json({ character }); +}); + +router.delete('/:id', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM characters WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!existing) return res.status(404).json({ error: 'Character not found' }); + + db.prepare('DELETE FROM characters WHERE id = ?').run(req.params.id); + res.json({ message: 'Character deleted' }); +}); + +// --- Needs --- + +router.get('/:id/needs', authenticateToken, (req, res) => { + const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ?').all(req.params.id); + res.json({ needs }); +}); + +router.post('/:id/needs', authenticateToken, (req, res) => { + const { name, enabled, initial_value, min_value, max_value, decay_rate, priority } = req.body; + if (!name) return res.status(400).json({ error: 'Need name is required' }); + + const needId = uuidv4(); + db.prepare('INSERT INTO character_needs (id, character_id, name, enabled, initial_value, min_value, max_value, decay_rate, priority) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)') + .run(needId, req.params.id, name, enabled !== false ? 1 : 0, initial_value || 50, min_value || 0, max_value || 100, decay_rate || 1, priority || 0); + + const need = db.prepare('SELECT * FROM character_needs WHERE id = ?').get(needId); + res.status(201).json({ need }); +}); + +router.put('/:id/needs/:needId', authenticateToken, (req, res) => { + const { name, enabled, initial_value, min_value, max_value, decay_rate, priority } = req.body; + const existing = db.prepare('SELECT * FROM character_needs WHERE id = ? AND character_id = ?').get(req.params.needId, req.params.id); + if (!existing) return res.status(404).json({ error: 'Need not found' }); + + db.prepare('UPDATE character_needs SET name = ?, enabled = ?, initial_value = ?, min_value = ?, max_value = ?, decay_rate = ?, priority = ? WHERE id = ?') + .run(name || existing.name, enabled !== undefined ? (enabled ? 1 : 0) : existing.enabled, initial_value ?? existing.initial_value, min_value ?? existing.min_value, max_value ?? existing.max_value, decay_rate ?? existing.decay_rate, priority ?? existing.priority, req.params.needId); + + const need = db.prepare('SELECT * FROM character_needs WHERE id = ?').get(req.params.needId); + res.json({ need }); +}); + +router.delete('/:id/needs/:needId', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM character_needs WHERE id = ? AND character_id = ?').get(req.params.needId, req.params.id); + if (!existing) return res.status(404).json({ error: 'Need not found' }); + db.prepare('DELETE FROM character_needs WHERE id = ?').run(req.params.needId); + res.json({ message: 'Need deleted' }); +}); + +// --- UI Elements --- + +router.get('/:id/ui-elements', authenticateToken, (req, res) => { + const elements = db.prepare('SELECT * FROM character_ui_elements WHERE character_id = ?').all(req.params.id); + res.json({ ui_elements: elements }); +}); + +router.post('/:id/ui-elements', authenticateToken, (req, res) => { + const { need_id, element_type, config } = req.body; + if (!element_type) return res.status(400).json({ error: 'Element type is required' }); + + const elementId = uuidv4(); + db.prepare('INSERT INTO character_ui_elements (id, character_id, need_id, element_type, config) VALUES (?, ?, ?, ?, ?)') + .run(elementId, req.params.id, need_id || null, element_type, JSON.stringify(config || {})); + + const element = db.prepare('SELECT * FROM character_ui_elements WHERE id = ?').get(elementId); + res.status(201).json({ ui_element: element }); +}); + +router.put('/:id/ui-elements/:elementId', authenticateToken, (req, res) => { + const { need_id, element_type, config } = req.body; + const existing = db.prepare('SELECT * FROM character_ui_elements WHERE id = ? AND character_id = ?').get(req.params.elementId, req.params.id); + if (!existing) return res.status(404).json({ error: 'UI element not found' }); + + db.prepare('UPDATE character_ui_elements SET need_id = ?, element_type = ?, config = ? WHERE id = ?') + .run(need_id !== undefined ? need_id : existing.need_id, element_type || existing.element_type, config ? JSON.stringify(config) : existing.config, req.params.elementId); + + const element = db.prepare('SELECT * FROM character_ui_elements WHERE id = ?').get(req.params.elementId); + res.json({ ui_element: element }); +}); + +router.delete('/:id/ui-elements/:elementId', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM character_ui_elements WHERE id = ? AND character_id = ?').get(req.params.elementId, req.params.id); + if (!existing) return res.status(404).json({ error: 'UI element not found' }); + db.prepare('DELETE FROM character_ui_elements WHERE id = ?').run(req.params.elementId); + res.json({ message: 'UI element deleted' }); +}); + +// --- Brain Rules --- + +router.get('/:id/brain-rules', authenticateToken, (req, res) => { + const rules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ?').all(req.params.id); + res.json({ brain_rules: rules }); +}); + +router.post('/:id/brain-rules', authenticateToken, (req, res) => { + const { condition, action, priority, enabled } = req.body; + if (!condition || !action) return res.status(400).json({ error: 'Condition and action are required' }); + + const ruleId = uuidv4(); + db.prepare('INSERT INTO character_brain_rules (id, character_id, condition, action, priority, enabled) VALUES (?, ?, ?, ?, ?, ?)') + .run(ruleId, req.params.id, JSON.stringify(condition), JSON.stringify(action), priority || 0, enabled !== false ? 1 : 0); + + const rule = db.prepare('SELECT * FROM character_brain_rules WHERE id = ?').get(ruleId); + res.status(201).json({ brain_rule: rule }); +}); + +router.put('/:id/brain-rules/:ruleId', authenticateToken, (req, res) => { + const { condition, action, priority, enabled } = req.body; + const existing = db.prepare('SELECT * FROM character_brain_rules WHERE id = ? AND character_id = ?').get(req.params.ruleId, req.params.id); + if (!existing) return res.status(404).json({ error: 'Brain rule not found' }); + + db.prepare('UPDATE character_brain_rules SET condition = ?, action = ?, priority = ?, enabled = ? WHERE id = ?') + .run(condition ? JSON.stringify(condition) : existing.condition, action ? JSON.stringify(action) : existing.action, priority ?? existing.priority, enabled !== undefined ? (enabled ? 1 : 0) : existing.enabled, req.params.ruleId); + + const rule = db.prepare('SELECT * FROM character_brain_rules WHERE id = ?').get(req.params.ruleId); + res.json({ brain_rule: rule }); +}); + +router.delete('/:id/brain-rules/:ruleId', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM character_brain_rules WHERE id = ? AND character_id = ?').get(req.params.ruleId, req.params.id); + if (!existing) return res.status(404).json({ error: 'Brain rule not found' }); + db.prepare('DELETE FROM character_brain_rules WHERE id = ?').run(req.params.ruleId); + res.json({ message: 'Brain rule deleted' }); +}); + +// --- Simulation --- + +router.post('/:id/simulate', authenticateToken, (req, res) => { + const { steps = 10, events = [] } = req.body; + const needs = db.prepare('SELECT * FROM character_needs WHERE character_id = ? AND enabled = 1').all(req.params.id); + const rules = db.prepare('SELECT * FROM character_brain_rules WHERE character_id = ? AND enabled = 1 ORDER BY priority DESC').all(req.params.id); + + const simulation = []; + let currentValues = {}; + needs.forEach(n => { currentValues[n.name] = { ...n }; }); + + for (let step = 0; step < steps; step++) { + for (const need of needs) { + if (currentValues[need.name]) { + let newVal = currentValues[need.name].current_value || need.initial_value; + newVal -= need.decay_rate; + newVal = Math.max(need.min_value, Math.min(need.max_value, newVal)); + currentValues[need.name].current_value = newVal; + } + } + + for (const event of events) { + for (const need of needs) { + if (event[need.name]) { + let val = currentValues[need.name].current_value || need.initial_value; + val += event[need.name]; + val = Math.max(need.min_value, Math.min(need.max_value, val)); + currentValues[need.name].current_value = val; + } + } + } + + const triggeredRules = []; + for (const rule of rules) { + const cond = JSON.parse(rule.condition); + const needVal = currentValues[cond.need]?.current_value ?? need.initial_value; + if (cond.operator === 'lt' && needVal < cond.value) { + triggeredRules.push(rule); + } else if (cond.operator === 'gt' && needVal > cond.value) { + triggeredRules.push(rule); + } else if (cond.operator === 'eq' && needVal === cond.value) { + triggeredRules.push(rule); + } else if (cond.operator === 'lte' && needVal <= cond.value) { + triggeredRules.push(rule); + } else if (cond.operator === 'gte' && needVal >= cond.value) { + triggeredRules.push(rule); + } + } + + const snapshot = {}; + for (const need of needs) { + snapshot[need.name] = Math.round((currentValues[need.name].current_value || need.initial_value) * 100) / 100; + } + + simulation.push({ step, values: snapshot, triggered_rules: triggeredRules.map(r => ({ id: r.id, action: JSON.parse(r.action) })) }); + } + + res.json({ simulation, final_values: simulation[simulation.length - 1]?.values }); +}); + +export default router; diff --git a/server/routes/fragments.js b/server/routes/fragments.js new file mode 100644 index 0000000..58b88ce --- /dev/null +++ b/server/routes/fragments.js @@ -0,0 +1,84 @@ +import { Router } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import db from '../db.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +router.get('/search', authenticateToken, (req, res) => { + const { q, tag } = req.query; + let query = `SELECT f.*, l.name as lorebook_name FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id WHERE l.user_id = ?`; + const params = [req.userId]; + + if (q) { + query += ` AND (f.title LIKE ? OR f.content LIKE ?)`; + params.push(`%${q}%`, `%${q}%`); + } + if (tag) { + query += ` AND f.tags LIKE ?`; + params.push(`%"${tag}"%`); + } + + query += ` ORDER BY f.updated_at DESC LIMIT 50`; + const fragments = db.prepare(query).all(...params); + res.json({ fragments }); +}); + +router.get('/:id', authenticateToken, (req, res) => { + const fragment = db.prepare(` + SELECT f.*, l.name as lorebook_name, l.user_id + FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id + WHERE f.id = ? AND l.user_id = ? + `).get(req.params.id, req.userId); + if (!fragment) return res.status(404).json({ error: 'Fragment not found' }); + res.json({ fragment }); +}); + +router.post('/lorebook/:lorebookId', authenticateToken, (req, res) => { + const { title, content, tags, linked_characters } = req.body; + if (!title) return res.status(400).json({ error: 'Title is required' }); + + const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.lorebookId, req.userId); + if (!lorebook) return res.status(404).json({ error: 'Lorebook not found' }); + + const id = uuidv4(); + db.prepare('INSERT INTO fragments (id, lorebook_id, title, content, tags, linked_characters) VALUES (?, ?, ?, ?, ?, ?)') + .run(id, req.params.lorebookId, title, content || '', JSON.stringify(tags || []), JSON.stringify(linked_characters || [])); + + const fragment = db.prepare('SELECT * FROM fragments WHERE id = ?').get(id); + res.status(201).json({ fragment }); +}); + +router.put('/:id', authenticateToken, (req, res) => { + const { title, content, tags, linked_characters } = req.body; + const fragment = db.prepare(` + SELECT f.* FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id + WHERE f.id = ? AND l.user_id = ? + `).get(req.params.id, req.userId); + if (!fragment) return res.status(404).json({ error: 'Fragment not found' }); + + db.prepare('UPDATE fragments SET title = ?, content = ?, tags = ?, linked_characters = ?, updated_at = datetime(\'now\') WHERE id = ?') + .run( + title || fragment.title, + content !== undefined ? content : fragment.content, + tags ? JSON.stringify(tags) : fragment.tags, + linked_characters ? JSON.stringify(linked_characters) : fragment.linked_characters, + req.params.id + ); + + const updated = db.prepare('SELECT * FROM fragments WHERE id = ?').get(req.params.id); + res.json({ fragment: updated }); +}); + +router.delete('/:id', authenticateToken, (req, res) => { + const fragment = db.prepare(` + SELECT f.* FROM fragments f JOIN lorebooks l ON f.lorebook_id = l.id + WHERE f.id = ? AND l.user_id = ? + `).get(req.params.id, req.userId); + if (!fragment) return res.status(404).json({ error: 'Fragment not found' }); + + db.prepare('DELETE FROM fragments WHERE id = ?').run(req.params.id); + res.json({ message: 'Fragment deleted' }); +}); + +export default router; diff --git a/server/routes/lorebooks.js b/server/routes/lorebooks.js new file mode 100644 index 0000000..c28d236 --- /dev/null +++ b/server/routes/lorebooks.js @@ -0,0 +1,51 @@ +import { Router } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import db from '../db.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +router.get('/', authenticateToken, (req, res) => { + const lorebooks = db.prepare('SELECT * FROM lorebooks WHERE user_id = ? ORDER BY updated_at DESC').all(req.userId); + res.json({ lorebooks }); +}); + +router.post('/', authenticateToken, (req, res) => { + const { name, description } = req.body; + if (!name) return res.status(400).json({ error: 'Name is required' }); + + const id = uuidv4(); + db.prepare('INSERT INTO lorebooks (id, user_id, name, description) VALUES (?, ?, ?, ?)').run(id, req.userId, name, description || ''); + const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ?').get(id); + res.status(201).json({ lorebook }); +}); + +router.get('/:id', authenticateToken, (req, res) => { + const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!lorebook) return res.status(404).json({ error: 'Lorebook not found' }); + + const fragments = db.prepare('SELECT * FROM fragments WHERE lorebook_id = ? ORDER BY created_at DESC').all(req.params.id); + res.json({ lorebook, fragments }); +}); + +router.put('/:id', authenticateToken, (req, res) => { + const { name, description } = req.body; + const existing = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!existing) return res.status(404).json({ error: 'Lorebook not found' }); + + db.prepare('UPDATE lorebooks SET name = ?, description = ?, updated_at = datetime(\'now\') WHERE id = ?') + .run(name || existing.name, description !== undefined ? description : existing.description, req.params.id); + + const lorebook = db.prepare('SELECT * FROM lorebooks WHERE id = ?').get(req.params.id); + res.json({ lorebook }); +}); + +router.delete('/:id', authenticateToken, (req, res) => { + const existing = db.prepare('SELECT * FROM lorebooks WHERE id = ? AND user_id = ?').get(req.params.id, req.userId); + if (!existing) return res.status(404).json({ error: 'Lorebook not found' }); + + db.prepare('DELETE FROM lorebooks WHERE id = ?').run(req.params.id); + res.json({ message: 'Lorebook deleted' }); +}); + +export default router; diff --git a/server/sandbox.db-shm b/server/sandbox.db-shm new file mode 100644 index 0000000..c07e0df Binary files /dev/null and b/server/sandbox.db-shm differ diff --git a/server/sandbox.db-wal b/server/sandbox.db-wal new file mode 100644 index 0000000..ea5a6b4 Binary files /dev/null and b/server/sandbox.db-wal differ diff --git a/server/serve-frontend.js b/server/serve-frontend.js new file mode 100644 index 0000000..f139804 --- /dev/null +++ b/server/serve-frontend.js @@ -0,0 +1,17 @@ +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const app = express(); +const PORT = process.env.PORT || 3000; + +app.use(express.static(path.join(__dirname, '..', 'client', 'build'))); + +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '..', 'client', 'build', 'index.html')); +}); + +app.listen(PORT, () => { + console.log(`Frontend serving on http://localhost:${PORT}`); +}); diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..7ae4cf6 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +/// + +declare namespace App { + interface Locals { + user: { + id: string; + username: string; + global_name: string | null; + avatar: string | null; + display_name: string | null; + } | null; + } +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..c780481 --- /dev/null +++ b/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + + sandbox — character workbench + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..c45a78b --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,43 @@ +import { redirect, type Handle } from '@sveltejs/kit'; +import { getSessionUser, SESSION_COOKIE } from '$lib/server/auth'; + +const PUBLIC_PATHS = new Set([ + '/login', + '/auth/discord', + '/auth/discord/callback', + '/auth/local', + '/api/auth/me', + '/api/auth/local-enabled', + '/api/health' +]); + +const PUBLIC_PREFIXES = ['/auth/discord/callback', '/_app/', '/favicon']; + +function isPublic(pathname: string): boolean { + if (PUBLIC_PATHS.has(pathname)) return true; + return PUBLIC_PREFIXES.some((p) => pathname === p || pathname.startsWith(p)); +} + +export const handle: Handle = async ({ event, resolve }) => { + const { url, cookies } = event; + const cookie = cookies.get(SESSION_COOKIE); + const user = getSessionUser(cookie); + + // Expose the user to all routes via event.locals + (event.locals as any).user = user; + + if (!user && !isPublic(url.pathname)) { + // API routes: 401 JSON + if (url.pathname.startsWith('/api/')) { + return new Response(JSON.stringify({ error: 'authentication required' }), { + status: 401, + headers: { 'content-type': 'application/json' } + }); + } + // Page routes: redirect to /login + const next = url.pathname + url.search; + throw redirect(302, `/login?next=${encodeURIComponent(next)}`); + } + + return resolve(event); +}; diff --git a/src/lib/server/allowlist.ts b/src/lib/server/allowlist.ts new file mode 100644 index 0000000..8f349e5 --- /dev/null +++ b/src/lib/server/allowlist.ts @@ -0,0 +1,8 @@ +// /var/www/sandbox/src/lib/server/allowlist.ts +// Allowlist controls who can use the sandbox. +// Currently allows everyone. Can be narrowed later. + +export function isUserAllowed(userId: string): boolean { + // TODO: implement allowlist table or markdown file if needed + return true; +} diff --git a/src/lib/server/auth/discord.ts b/src/lib/server/auth/discord.ts new file mode 100644 index 0000000..73dbf8e --- /dev/null +++ b/src/lib/server/auth/discord.ts @@ -0,0 +1,74 @@ +// /var/www/sandbox/src/lib/server/auth/discord.ts +// Discord OAuth2 flow. Uses $env/dynamic/private for the env vars. +// Returns the user object on success, or null on any failure. + +const DISCORD_API = 'https://discord.com/api'; + +interface DiscordTokenResponse { + access_token: string; + token_type: string; + expires_in: number; + refresh_token: string; + scope: string; +} + +interface DiscordUser { + id: string; + username: string; + global_name: string | null; + avatar: string | null; + email: string | null; + verified: boolean; +} + +function requireEnv(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`Missing env: ${name}`); + return v; +} + +function getRedirectUri(): string { + return requireEnv('DISCORD_REDIRECT_URI'); +} + +export function buildAuthorizeUrl(redirectAfter: string): string { + const state = redirectAfter; + const params = new URLSearchParams({ + client_id: requireEnv('DISCORD_CLIENT_ID'), + redirect_uri: getRedirectUri(), + response_type: 'code', + scope: 'identify', + state + }); + return `${DISCORD_API}/oauth2/authorize?${params.toString()}`; +} + +export async function exchangeCodeForUser(code: string, state: string): Promise<{ user: DiscordUser; redirect: string }> { + const tokenRes = await fetch(`${DISCORD_API}/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: requireEnv('DISCORD_CLIENT_ID'), + client_secret: requireEnv('DISCORD_CLIENT_SECRET'), + grant_type: 'authorization_code', + code, + redirect_uri: getRedirectUri() + }).toString() + }); + if (!tokenRes.ok) { + const text = await tokenRes.text(); + throw new Error(`Discord token exchange failed: ${tokenRes.status} ${text}`); + } + const token = (await tokenRes.json()) as DiscordTokenResponse; + + const userRes = await fetch(`${DISCORD_API}/users/@me`, { + headers: { Authorization: `Bearer ${token.access_token}` } + }); + if (!userRes.ok) { + const text = await userRes.text(); + throw new Error(`Discord user fetch failed: ${userRes.status} ${text}`); + } + const user = (await userRes.json()) as DiscordUser; + + return { user, redirect: state }; +} diff --git a/src/lib/server/auth/index.ts b/src/lib/server/auth/index.ts new file mode 100644 index 0000000..1adbada --- /dev/null +++ b/src/lib/server/auth/index.ts @@ -0,0 +1,24 @@ +// /var/www/sandbox/src/lib/server/auth/index.ts +// Re-exports for the auth surface. +export { + getSessionUser, + createSession, + destroySession, + upsertUser, + SESSION_COOKIE, + SESSION_TTL_DURATION, + type SessionUser +} from './session'; + +export { + isLocalLoginEnabled, + verifyLocalPassword, + type LocalUser +} from './local'; + +export { + buildAuthorizeUrl, + exchangeCodeForUser +} from './discord'; + +export { isUserAllowed } from '$lib/server/allowlist'; diff --git a/src/lib/server/auth/local.ts b/src/lib/server/auth/local.ts new file mode 100644 index 0000000..65e3213 --- /dev/null +++ b/src/lib/server/auth/local.ts @@ -0,0 +1,38 @@ +// /var/www/sandbox/src/lib/server/auth/local.ts +// Local password login — fallback for when Discord OAuth is unavailable. + +import { timingSafeEqual } from 'node:crypto'; +import { env } from '$env/dynamic/private'; + +const LOCAL_USER_ID = 'local-admin'; + +export interface LocalUser { + id: string; + username: string; + global_name: string | null; + avatar: string | null; + display_name: string | null; +} + +export function isLocalLoginEnabled(): boolean { + return !!env.LOCAL_ADMIN_PASSWORD && env.LOCAL_ADMIN_PASSWORD.length > 0; +} + +export function verifyLocalPassword(submitted: string): LocalUser | null { + if (!isLocalLoginEnabled()) return null; + const expected = env.LOCAL_ADMIN_PASSWORD!; + if (submitted.length !== expected.length) { + timingSafeEqual(Buffer.from(submitted), Buffer.from(submitted)); + return null; + } + const ok = timingSafeEqual(Buffer.from(submitted), Buffer.from(expected)); + if (!ok) return null; + + return { + id: LOCAL_USER_ID, + username: 'local-admin', + global_name: 'Local Admin', + avatar: null, + display_name: null + }; +} diff --git a/src/lib/server/auth/session.ts b/src/lib/server/auth/session.ts new file mode 100644 index 0000000..561cc14 --- /dev/null +++ b/src/lib/server/auth/session.ts @@ -0,0 +1,96 @@ +// /var/www/sandbox/src/lib/server/auth/session.ts +// Signed cookie sessions. SESSION_SECRET must be set in .env (32+ chars). + +import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; +import { db } from '../db'; + +const SESSION_TTL_DAYS = 30; +const SESSION_TTL_MS = SESSION_TTL_DAYS * 24 * 60 * 60 * 1000; +export const SESSION_COOKIE = 'thw_sandbox_session'; +export const SESSION_TTL_DURATION = SESSION_TTL_DAYS * 24 * 60 * 60; + +function getSessionSecret(): string { + const s = process.env.SESSION_SECRET; + if (!s || s.length < 32) { + throw new Error('SESSION_SECRET must be set in .env (32+ chars)'); + } + return s; +} + +function sign(value: string): string { + return createHmac('sha256', getSessionSecret()).update(value).digest('hex'); +} + +function makeCookieValue(sessionId: string): string { + return `${sessionId}.${sign(sessionId)}`; +} + +function verifyCookieValue(cookieValue: string): string | null { + const dotIdx = cookieValue.lastIndexOf('.'); + if (dotIdx < 1) return null; + const sessionId = cookieValue.slice(0, dotIdx); + const providedSig = cookieValue.slice(dotIdx + 1); + const expectedSig = sign(sessionId); + if (providedSig.length !== expectedSig.length) return null; + try { + if (!timingSafeEqual(Buffer.from(providedSig, 'hex'), Buffer.from(expectedSig, 'hex'))) { + return null; + } + } catch { + return null; + } + return sessionId; +} + +export interface SessionUser { + id: string; + username: string; + global_name: string | null; + avatar: string | null; + display_name: string | null; +} + +export function upsertUser(user: SessionUser): void { + db.prepare( + `INSERT INTO users (id, username, global_name, avatar, display_name) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + username = excluded.username, + global_name = excluded.global_name, + avatar = excluded.avatar, + display_name = excluded.display_name` + ).run(user.id, user.username, user.global_name, user.avatar, user.display_name); +} + +export function createSession(userId: string): { id: string; cookie: string; expiresAt: Date } { + const id = randomBytes(32).toString('hex'); + const expiresAt = new Date(Date.now() + SESSION_TTL_MS); + db.prepare('INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)').run( + id, userId, expiresAt.toISOString() + ); + return { id, cookie: makeCookieValue(id), expiresAt }; +} + +export function destroySession(sessionId: string): void { + db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId); +} + +export function getSessionUser(cookieValue: string | undefined | null): SessionUser | null { + if (!cookieValue) return null; + const sessionId = verifyCookieValue(cookieValue); + if (!sessionId) return null; + const row = db.prepare( + `SELECT u.id, u.username, u.global_name, u.avatar, u.display_name, s.expires_at + FROM sessions s + JOIN users u ON u.id = s.user_id + WHERE s.id = ? AND s.expires_at > datetime('now')` + ).get(sessionId) as (SessionUser & { expires_at: string }) | undefined; + if (!row) return null; + return { + id: row.id, + username: row.username, + global_name: row.global_name, + avatar: row.avatar, + display_name: row.display_name + }; +} diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..2e1ac76 --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,50 @@ +// /var/www/sandbox/src/lib/server/db/index.ts +// Minimal DB for the sandbox character workbench. Per-user isolated. +// 3 tables: users, sessions, characters. + +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; + +const DB_PATH = process.env.DATABASE_PATH || './data/sandbox.db'; +mkdirSync(dirname(DB_PATH), { recursive: true }); + +export const db = new Database(DB_PATH); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + global_name TEXT, + avatar TEXT, + display_name TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + expires_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id); + + CREATE TABLE IF NOT EXISTS characters ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + personality TEXT, + scenario TEXT, + first_mes TEXT, + mes_example TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_characters_user ON characters(user_id); + CREATE INDEX IF NOT EXISTS idx_characters_updated ON characters(updated_at DESC); +`); diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..431be50 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,8 @@ +// /var/www/sandbox/src/routes/+layout.server.ts +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + user: (locals as any).user ?? null + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..cfee190 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,85 @@ + + + + sandbox — character workbench + + +
+ + | + character workbench +
+ {#if user} + {user.display_name || user.username || user.global_name || user.id} +
+ +
+ {/if} +
+ +
+ +
+ + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..d7ec4da --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,391 @@ + + + + sandbox — character workbench + + +
+ + + + +
+ {#if !selected} +
+

select a character to edit

+
+ {:else} +
+

+ {(selected as any)._new ? 'new character' : selected.name || 'untitled'} +

+
+ + + +
+
+ + {#if errorMsg} +

! {errorMsg}

+ {/if} + +
+ {#each [ + { key: 'name', label: 'Name', big: false }, + { key: 'description', label: 'Description', big: true }, + { key: 'personality', label: 'Personality', big: true }, + { key: 'scenario', label: 'Scenario', big: true }, + { key: 'first_mes', label: 'First Message', big: true }, + { key: 'mes_example', label: 'Message Example', big: true } + ] as field} + {@const val = (selected as any)[field.key] ?? ''} +
+ + {#if field.big} + + {:else} + { (selected as any)[field.key] = (e.target as HTMLInputElement).value; }} + /> + {/if} +
+ {/each} +
+ {/if} +
+
+ + diff --git a/src/routes/api/auth/local-enabled/+server.ts b/src/routes/api/auth/local-enabled/+server.ts new file mode 100644 index 0000000..c4b299a --- /dev/null +++ b/src/routes/api/auth/local-enabled/+server.ts @@ -0,0 +1,6 @@ +import { json, type RequestHandler } from '@sveltejs/kit'; +import { isLocalLoginEnabled } from '$lib/server/auth/local'; + +export const GET: RequestHandler = async () => { + return json({ enabled: isLocalLoginEnabled() }); +}; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts new file mode 100644 index 0000000..0ec2bf1 --- /dev/null +++ b/src/routes/api/auth/logout/+server.ts @@ -0,0 +1,14 @@ +// /var/www/sandbox/src/routes/api/auth/logout/+server.ts +import { redirect, type RequestHandler } from '@sveltejs/kit'; + +export const POST: RequestHandler = async ({ cookies }) => { + const { destroySession, SESSION_COOKIE } = await import('$lib/server/auth'); + const cookie = cookies.get(SESSION_COOKIE); + if (cookie) { + const dotIdx = cookie.lastIndexOf('.'); + const sessionId = dotIdx > 0 ? cookie.slice(0, dotIdx) : cookie; + destroySession(sessionId); + } + cookies.delete(SESSION_COOKIE, { path: '/' }); + throw redirect(302, '/login'); +}; diff --git a/src/routes/api/auth/me/+server.ts b/src/routes/api/auth/me/+server.ts new file mode 100644 index 0000000..035da68 --- /dev/null +++ b/src/routes/api/auth/me/+server.ts @@ -0,0 +1,10 @@ +// /var/www/sandbox/src/routes/api/auth/me/+server.ts +import { json, type RequestHandler } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ locals }) => { + const user = (locals as any).user; + if (!user) { + return json({ user: null }); + } + return json({ user }); +}; diff --git a/src/routes/api/characters/+server.ts b/src/routes/api/characters/+server.ts new file mode 100644 index 0000000..d37f5c7 --- /dev/null +++ b/src/routes/api/characters/+server.ts @@ -0,0 +1,53 @@ +// /var/www/sandbox/src/routes/api/characters/+server.ts +import { json, error, type RequestHandler } from '@sveltejs/kit'; +import { randomBytes } from 'node:crypto'; + +function getDb() { + return import('$lib/server/db').then((m) => m.db); +} + +// GET /api/characters — list user's characters +export const GET: RequestHandler = async ({ locals }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + const db = await getDb(); + const rows = db.prepare( + 'SELECT id, name, created_at, updated_at FROM characters WHERE user_id = ? ORDER BY updated_at DESC' + ).all(user.id); + return json({ characters: rows }); +}; + +// POST /api/characters — create a new character +export const POST: RequestHandler = async ({ locals, request }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + + let body: Record; + try { + body = await request.json(); + } catch { + throw error(400, 'invalid json'); + } + + const name = typeof body.name === 'string' && body.name.trim() ? body.name.trim() : null; + if (!name) throw error(400, 'name is required'); + + const id = randomBytes(16).toString('hex'); + const db = await getDb(); + db.prepare( + `INSERT INTO characters (id, user_id, name, description, personality, scenario, first_mes, mes_example) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, + user.id, + name, + typeof body.description === 'string' ? body.description : null, + typeof body.personality === 'string' ? body.personality : null, + typeof body.scenario === 'string' ? body.scenario : null, + typeof body.first_mes === 'string' ? body.first_mes : null, + typeof body.mes_example === 'string' ? body.mes_example : null + ); + + const created = db.prepare('SELECT * FROM characters WHERE id = ?').get(id); + return json({ character: created }, { status: 201 }); +}; diff --git a/src/routes/api/characters/[id]/+server.ts b/src/routes/api/characters/[id]/+server.ts new file mode 100644 index 0000000..25de870 --- /dev/null +++ b/src/routes/api/characters/[id]/+server.ts @@ -0,0 +1,77 @@ +// /var/www/sandbox/src/routes/api/characters/[id]/+server.ts +import { json, error, type RequestHandler } from '@sveltejs/kit'; + +function getDb() { + return import('$lib/server/db').then((m) => m.db); +} + +function ownRow(db: any, id: string, userId: string) { + const row = db.prepare('SELECT * FROM characters WHERE id = ?').get(id) as any; + if (!row || row.user_id !== userId) return null; + return row; +} + +// PUT /api/characters/:id — update fields +export const PUT: RequestHandler = async ({ locals, params, request }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + + const db = await getDb(); + const row = ownRow(db, params.id, user.id); + if (!row) throw error(404, 'not found'); + + let body: Record; + try { + body = await request.json(); + } catch { + throw error(400, 'invalid json'); + } + + const FIELDS = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example'] as const; + const updates: string[] = []; + const vals: unknown[] = []; + + for (const f of FIELDS) { + if (body[f] !== undefined && typeof body[f] === 'string') { + updates.push(`${f} = ?`); + vals.push(body[f]!.trim()); + } + } + + if (updates.length === 0) { + return json({ character: row }); + } + + updates.push('updated_at = datetime(\'now\')'); + vals.push(params.id); + + db.prepare(`UPDATE characters SET ${updates.join(', ')} WHERE id = ?`).run(...vals); + + const updated = db.prepare('SELECT * FROM characters WHERE id = ?').get(params.id); + return json({ character: updated }); +}; + +// DELETE /api/characters/:id +export const DELETE: RequestHandler = async ({ locals, params }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + + const db = await getDb(); + const row = ownRow(db, params.id, user.id); + if (!row) throw error(404, 'not found'); + + db.prepare('DELETE FROM characters WHERE id = ?').run(params.id); + return json({ ok: true }); +}; + +// GET /api/characters/:id — return character for editing +export const GET: RequestHandler = async ({ locals, params }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + + const db = await getDb(); + const row = ownRow(db, params.id, user.id); + if (!row) throw error(404, 'not found'); + + return json({ character: row }); +}; diff --git a/src/routes/api/characters/[id]/json/+server.ts b/src/routes/api/characters/[id]/json/+server.ts new file mode 100644 index 0000000..35fda34 --- /dev/null +++ b/src/routes/api/characters/[id]/json/+server.ts @@ -0,0 +1,39 @@ +// /var/www/sandbox/src/routes/api/characters/[id]/json/+server.ts +import { json, error, type RequestHandler } from '@sveltejs/kit'; + +function getDb() { + return import('$lib/server/db').then((m) => m.db); +} + +export const GET: RequestHandler = async ({ locals, params }) => { + const user = (locals as any).user; + if (!user) throw error(401, 'auth required'); + + const db = await getDb(); + const row = db.prepare('SELECT * FROM characters WHERE id = ?').get(params.id) as any; + if (!row || row.user_id !== user.id) throw error(404, 'not found'); + + const card = { + name: row.name, + description: row.description || '', + personality: row.personality || '', + scenario: row.scenario || '', + first_mes: row.first_mes || '', + mes_example: row.mes_example || '', + creator_notes: 'Exported from TheHowlingWhispers Character Workbench', + system_prompt: '', + post_history_instructions: '', + tags: [], + creator: user.display_name || user.username, + character_version: '1.0', + extras: { + sandbox_exported_at: new Date().toISOString() + } + }; + + return json(card, { + headers: { + 'Content-Disposition': `attachment; filename="${row.name.replace(/[^a-zA-Z0-9_-]/g, '_')}.json"` + } + }); +}; diff --git a/src/routes/api/health/+server.ts b/src/routes/api/health/+server.ts new file mode 100644 index 0000000..afe5366 --- /dev/null +++ b/src/routes/api/health/+server.ts @@ -0,0 +1,6 @@ +// /var/www/sandbox/src/routes/api/health/+server.ts +import { json, type RequestHandler } from '@sveltejs/kit'; + +export const GET: RequestHandler = async () => { + return json({ status: 'ok' }); +}; diff --git a/src/routes/auth/discord/+server.ts b/src/routes/auth/discord/+server.ts new file mode 100644 index 0000000..1f10e2e --- /dev/null +++ b/src/routes/auth/discord/+server.ts @@ -0,0 +1,10 @@ +// /var/www/sandbox/src/routes/auth/discord/+server.ts +// Initiates the Discord OAuth flow. +import { redirect, type RequestHandler } from '@sveltejs/kit'; + +export const GET: RequestHandler = async ({ url }) => { + const next = url.searchParams.get('next'); + const safeNext = next && next.startsWith('/') && !next.startsWith('//') ? next : '/'; + const { buildAuthorizeUrl } = await import('$lib/server/auth'); + throw redirect(302, buildAuthorizeUrl(safeNext)); +}; diff --git a/src/routes/auth/discord/callback/+server.ts b/src/routes/auth/discord/callback/+server.ts new file mode 100644 index 0000000..5496e1d --- /dev/null +++ b/src/routes/auth/discord/callback/+server.ts @@ -0,0 +1,53 @@ +// /var/www/sandbox/src/routes/auth/discord/callback/+server.ts +// Handles the Discord OAuth callback. +import { redirect, type RequestHandler } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; + +export const GET: RequestHandler = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const error = url.searchParams.get('error'); + + if (error) { + throw redirect(302, `/login?error=${encodeURIComponent(error)}`); + } + if (!code || !state) { + throw redirect(302, '/login?error=missing_code_or_state'); + } + + const { exchangeCodeForUser, isUserAllowed, createSession, upsertUser, SESSION_COOKIE, SESSION_TTL_DURATION } = await import('$lib/server/auth'); + + let user, redirectTo; + try { + const result = await exchangeCodeForUser(code, state); + user = result.user; + redirectTo = result.redirect; + } catch (e) { + const msg = e instanceof Error ? e.message : 'unknown error'; + throw redirect(302, `/login?error=${encodeURIComponent(msg.slice(0, 200))}`); + } + + if (!isUserAllowed(user.id)) { + throw redirect(302, `/login?error=not_on_allowlist`); + } + + upsertUser({ + id: user.id, + username: user.username, + global_name: user.global_name, + avatar: user.avatar, + display_name: null + }); + const session = createSession(user.id); + + const isHttps = env.ORIGIN?.startsWith('https://') ?? false; + cookies.set(SESSION_COOKIE, session.cookie, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: isHttps, + maxAge: SESSION_TTL_DURATION + }); + + throw redirect(302, redirectTo); +}; diff --git a/src/routes/auth/local/+server.ts b/src/routes/auth/local/+server.ts new file mode 100644 index 0000000..7896843 --- /dev/null +++ b/src/routes/auth/local/+server.ts @@ -0,0 +1,43 @@ +// /var/www/sandbox/src/routes/auth/local/+server.ts +// Local password login. +import { redirect, type RequestHandler } from '@sveltejs/kit'; +import { env } from '$env/dynamic/private'; + +export const POST: RequestHandler = async ({ request, cookies, url }) => { + const { isLocalLoginEnabled, verifyLocalPassword } = await import('$lib/server/auth/local'); + const { createSession, upsertUser, SESSION_COOKIE, SESSION_TTL_DURATION } = await import('$lib/server/auth'); + + if (!isLocalLoginEnabled()) { + throw redirect(302, '/login?error=local_login_disabled'); + } + + const data = await request.formData(); + const password = (data.get('password') ?? '').toString(); + + const user = verifyLocalPassword(password); + if (!user) { + throw redirect(302, '/login?error=local_login_invalid'); + } + + upsertUser({ + id: user.id, + username: user.username, + global_name: user.global_name, + avatar: user.avatar, + display_name: user.display_name + }); + const session = createSession(user.id); + + const isHttps = env.ORIGIN?.startsWith('https://') ?? false; + cookies.set(SESSION_COOKIE, session.cookie, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: isHttps, + maxAge: SESSION_TTL_DURATION + }); + + const next = url.searchParams.get('next'); + const safeNext = next && next.startsWith('/') && !next.startsWith('//') ? next : '/'; + throw redirect(302, safeNext); +}; diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte new file mode 100644 index 0000000..b53174c --- /dev/null +++ b/src/routes/login/+page.svelte @@ -0,0 +1,150 @@ + + + + Sign in — sandbox + + +
+
+

sandbox / sign in

+

Access required

+ + {#if errorMessage} +

! {errorMessage}

+ {/if} + + + Continue with Discord + + + {#if localLoginEnabled} +
— or —
+ +
+ + + +
+ {/if} +
+
+ + diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..a7a82ed Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..b21357c Binary files /dev/null and b/static/favicon.png differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..7d31d86 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..511cc14 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,16 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + out: 'build', + precompress: false + }), + csrf: { checkOrigin: false } + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b584b27 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + port: 2027, + host: '127.0.0.1', + strictPort: true + } +});