diff mrjunejune/main.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents ee04e4e69fed
children 056790c4fb0d
line wrap: on
line diff
--- a/mrjunejune/main.c	Thu Aug 06 11:31:30 2026 -0700
+++ b/mrjunejune/main.c	Fri Aug 07 07:34:12 2026 -0700
@@ -4,11 +4,21 @@
 #include "deita/deita.h"
 #include "mrjunejune/latex_renderer.h"
 #include "mrjunejune/conversation_api.h"
+#include "mrjunejune/auth_api.h"
+#include "mrjunejune/admin_api.h"
+#include "mrjunejune/template_renderer.h"
+#include "auth/auth_crypto.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
 #include <time.h>
 #include <sys/stat.h>
 #include <stdarg.h>
 #include <stdatomic.h>
 #include <pthread.h>
+#include <arpa/inet.h>
+#include <openssl/crypto.h>
 
 // UUID + /tmp/ + format (max 4)
 #define TMP_FILE_LENGTH 47
@@ -45,38 +55,128 @@
 static int  g_s3_url_expires = 3600;
 static S3_Config g_s3_config = {0};
 static Deita_Connection *g_db_connection = NULL;
+/* S3 credentials — never logged; zero after use in init. */
+static char g_s3_access_key[128] = {0};
+static char g_s3_secret_key[128] = {0};
+
+/* Auth configuration */
+static char    g_auth_cookie_secret[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2 + 1] = {0};
+static char    g_auth_bootstrap_username[64] = {0};
+static char    g_auth_bootstrap_password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE] = {0};
+static char    g_auth_trusted_proxy[AUTH_CRYPTO_IP_MAX_BYTES] = {0};
+static int64   g_auth_session_idle_ttl = AUTH_API_SESSION_IDLE_TTL_DEFAULT;
+static int64   g_auth_session_abs_ttl  = AUTH_API_SESSION_ABS_TTL_DEFAULT;
+static int64   g_auth_guest_ttl        = AUTH_API_GUEST_TTL_DEFAULT;
+static boolean g_auth_dev_insecure     = FALSE;
+static char    g_server_host[128]      = {0};   /* SERVER_HOST; empty → 0.0.0.0 */
+
+/* Guest inference / quota configuration */
+#define G_GUEST_DAILY_TURNS_DEFAULT         10
+#define G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT 20000
+#define G_GUEST_REQUEST_OUTPUT_TOKENS_MIN   1
+#define G_GUEST_DAILY_TURNS_MIN             1
+#define G_GUEST_DAILY_TURNS_MAX             10000
+#define G_GUEST_DAILY_OUTPUT_TOKENS_MIN     1
+#define G_GUEST_DAILY_OUTPUT_TOKENS_MAX     1000000
+#define G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT 2048
+static boolean g_guest_inference_enabled     = FALSE;
+static int64   g_guest_daily_turns           = G_GUEST_DAILY_TURNS_DEFAULT;
+static int64   g_guest_daily_output_tokens   = G_GUEST_DAILY_OUTPUT_TOKENS_DEFAULT;
+static int64   g_guest_request_output_tokens = G_GUEST_REQUEST_OUTPUT_TOKENS_DEFAULT;
+
+/*
+ * Strict full-string integer parse: returns FALSE if value is empty,
+ * has trailing non-digit characters, or overflows a long.
+ */
+static boolean config__parse_int64_strict(const char *value, int64 *out)
+{
+  if (!value || value[0] == '\0')
+    return FALSE;
+  char *end = NULL;
+  long v = strtol(value, &end, 10);
+  if (end == value || *end != '\0')
+    return FALSE;
+  *out = (int64)v;
+  return TRUE;
+}
+
+/*
+ * Decode a lowercase or uppercase hex string into raw bytes.
+ * Returns the number of decoded bytes, or 0 on any error.
+ * hex_len must be even; each pair of hex chars produces one byte.
+ */
+static size_t config__hex_decode(const char *hex, uint8 *out, size_t out_capacity)
+{
+  if (!hex || !out) return 0;
+  size_t hex_len = strlen(hex);
+  if (hex_len == 0 || hex_len % 2 != 0) return 0;
+  size_t byte_count = hex_len / 2;
+  if (byte_count > out_capacity) return 0;
+  for (size_t i = 0; i < byte_count; i++)
+  {
+    int h, l;
+    char hi = hex[i * 2];
+    char lo = hex[i * 2 + 1];
+    if      (hi >= '0' && hi <= '9') h = hi - '0';
+    else if (hi >= 'a' && hi <= 'f') h = hi - 'a' + 10;
+    else if (hi >= 'A' && hi <= 'F') h = hi - 'A' + 10;
+    else return 0;
+    if      (lo >= '0' && lo <= '9') l = lo - '0';
+    else if (lo >= 'a' && lo <= 'f') l = lo - 'a' + 10;
+    else if (lo >= 'A' && lo <= 'F') l = lo - 'A' + 10;
+    else return 0;
+    out[i] = (uint8)((h << 4) | l);
+  }
+  return byte_count;
+}
 
 static void load_config(const char *config_path)
 {
   FILE *f = fopen(config_path, "r");
+  char workspace_config_path[1024] = {0};
+  if (!f)
+  {
+    const char *workspace = getenv("BUILD_WORKSPACE_DIRECTORY");
+    if (workspace && workspace[0] != '\0')
+    {
+      int written = snprintf(
+          workspace_config_path,
+          sizeof(workspace_config_path),
+          "%s/%s",
+          workspace,
+          config_path);
+      if (written > 0 && (size_t)written < sizeof(workspace_config_path))
+        f = fopen(workspace_config_path, "r");
+    }
+  }
   if (!f)
   {
     printf("[CONFIG] Warning: Could not open %s, using defaults\n", config_path);
-    return;
   }
-
-  char line[512];
-  while (fgets(line, sizeof(line), f))
+  else
   {
-    // Skip comments and empty lines
-    if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
+    char line[512];
+    while (fgets(line, sizeof(line), f))
+    {
+      // Skip comments and empty lines
+      if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') continue;
 
-    char *eq = strchr(line, '=');
-    if (!eq) continue;
+      char *eq = strchr(line, '=');
+      if (!eq) continue;
 
-    *eq = '\0';
-    char *key = line;
-    char *value = eq + 1;
+      *eq = '\0';
+      char *key = line;
+      char *value = eq + 1;
 
-    // Trim newline from value
-    size_t vlen = strlen(value);
-    while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r'))
-      value[--vlen] = '\0';
+      // Trim newline from value
+      size_t vlen = strlen(value);
+      while (vlen > 0 && (value[vlen-1] == '\n' || value[vlen-1] == '\r'))
+        value[--vlen] = '\0';
 
-    if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0)
-    {
-      strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1);
-    }
+      if (strcmp(key, "UPLOAD_AUTH_TOKEN") == 0)
+      {
+        strncpy(g_upload_auth_token, value, sizeof(g_upload_auth_token) - 1);
+      }
     else if (strcmp(key, "S3_REGION") == 0)
     {
       strncpy(g_s3_region, value, sizeof(g_s3_region) - 1);
@@ -87,7 +187,13 @@
     }
     else if (strcmp(key, "S3_URL_EXPIRES") == 0)
     {
-      g_s3_url_expires = atoi(value);
+      int64 v;
+      if (!config__parse_int64_strict(value, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n");
+        exit(1);
+      }
+      g_s3_url_expires = (int)v;
     }
     else if (strcmp(key, "S3_CLOUDFRONT_URL") == 0)
     {
@@ -97,14 +203,262 @@
     {
       strncpy(g_db_path, value, sizeof(g_db_path) - 1);
     }
+    else if (strcmp(key, "AWS_MRJUNEJUNE_ACCESS_KEY") == 0)
+    {
+      strncpy(g_s3_access_key, value, sizeof(g_s3_access_key) - 1);
+    }
+    else if (strcmp(key, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY") == 0)
+    {
+      strncpy(g_s3_secret_key, value, sizeof(g_s3_secret_key) - 1);
+    }
+    else if (strcmp(key, "AUTH_COOKIE_SECRET") == 0)
+    {
+      /* Never log this value */
+      strncpy(g_auth_cookie_secret, value, sizeof(g_auth_cookie_secret) - 1);
+    }
+    else if (strcmp(key, "AUTH_BOOTSTRAP_USERNAME") == 0)
+    {
+      strncpy(g_auth_bootstrap_username, value,
+              sizeof(g_auth_bootstrap_username) - 1);
+    }
+    else if (strcmp(key, "AUTH_BOOTSTRAP_PASSWORD_HASH") == 0)
+    {
+      strncpy(g_auth_bootstrap_password_hash, value,
+              sizeof(g_auth_bootstrap_password_hash) - 1);
+    }
+    else if (strcmp(key, "AUTH_TRUSTED_PROXY") == 0)
+    {
+      strncpy(g_auth_trusted_proxy, value, sizeof(g_auth_trusted_proxy) - 1);
+    }
+    else if (strcmp(key, "AUTH_SESSION_IDLE_TTL") == 0)
+    {
+      int64 v;
+      if (!config__parse_int64_strict(value, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_session_idle_ttl = v;
+    }
+    else if (strcmp(key, "AUTH_SESSION_ABS_TTL") == 0)
+    {
+      int64 v;
+      if (!config__parse_int64_strict(value, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_session_abs_ttl = v;
+    }
+    else if (strcmp(key, "AUTH_GUEST_TTL") == 0)
+    {
+      int64 v;
+      if (!config__parse_int64_strict(value, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_guest_ttl = v;
+    }
+      else if (strcmp(key, "AUTH_DEV_INSECURE_COOKIE") == 0)
+      {
+        g_auth_dev_insecure =
+            (strcmp(value, "1") == 0 || strcmp(value, "true") == 0);
+      }
+      else if (strcmp(key, "SERVER_HOST") == 0)
+      {
+        strncpy(g_server_host, value, sizeof(g_server_host) - 1);
+      }
+      else if (strcmp(key, "AUTH_GUEST_DAILY_TURNS") == 0)
+      {
+        int64 v;
+        if (!config__parse_int64_strict(value, &v) ||
+            v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX)
+        {
+          printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n",
+                 G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX);
+          exit(1);
+        }
+        g_guest_daily_turns = v;
+      }
+      else if (strcmp(key, "AUTH_GUEST_DAILY_OUTPUT_TOKENS") == 0)
+      {
+        int64 v;
+        if (!config__parse_int64_strict(value, &v) ||
+            v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN ||
+            v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX)
+        {
+          printf("[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n",
+                 G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX);
+          exit(1);
+        }
+        g_guest_daily_output_tokens = v;
+      }
+      else if (strcmp(key, "AUTH_GUEST_REQUEST_OUTPUT_TOKENS") == 0)
+      {
+        int64 v;
+        if (!config__parse_int64_strict(value, &v) ||
+            v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN)
+        {
+          printf("[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n",
+                 G_GUEST_REQUEST_OUTPUT_TOKENS_MIN);
+          exit(1);
+        }
+        g_guest_request_output_tokens = v;
+      }
+    }
+    fclose(f);
   }
-  fclose(f);
 
   printf("[CONFIG] Loaded: token=%s..., region=%s, bucket=%s, expires=%d, cloudfront=%s, db=%s\n",
          g_upload_auth_token[0] ? "***" : "(empty)",
          g_s3_region, g_s3_bucket, g_s3_url_expires,
          g_s3_cloudfront_url[0] ? g_s3_cloudfront_url : "(none)",
          g_db_path);
+  printf("[CONFIG] Auth: secret=%s, bootstrap_user=%s, trusted_proxy=%s\n",
+         g_auth_cookie_secret[0] ? "(set)" : "(not set)",
+         g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : "(none)",
+         g_auth_trusted_proxy[0] ? "(set)" : "(not set)");
+
+  const char *database_path_override = getenv("DB_PATH");
+  const char *test_tmpdir = getenv("TEST_TMPDIR");
+  if (database_path_override && database_path_override[0] != '\0')
+  {
+    strncpy(g_db_path, database_path_override, sizeof(g_db_path) - 1);
+  }
+  else if (test_tmpdir && test_tmpdir[0] != '\0')
+  {
+    snprintf(g_db_path, sizeof(g_db_path), "%s/mrjunejune.db", test_tmpdir);
+  }
+
+  /* Environment overrides: env vars always take precedence over file.
+   * Values are never logged. */
+  {
+    const char *env;
+
+    /* S3 / server config env overrides */
+    if ((env = getenv("UPLOAD_AUTH_TOKEN")) && env[0] != '\0')
+      strncpy(g_upload_auth_token, env, sizeof(g_upload_auth_token) - 1);
+    if ((env = getenv("S3_REGION")) && env[0] != '\0')
+      strncpy(g_s3_region, env, sizeof(g_s3_region) - 1);
+    if ((env = getenv("S3_BUCKET")) && env[0] != '\0')
+      strncpy(g_s3_bucket, env, sizeof(g_s3_bucket) - 1);
+    if ((env = getenv("S3_CLOUDFRONT_URL")) && env[0] != '\0')
+      strncpy(g_s3_cloudfront_url, env, sizeof(g_s3_cloudfront_url) - 1);
+    if ((env = getenv("S3_URL_EXPIRES")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: S3_URL_EXPIRES must be a positive integer\n");
+        exit(1);
+      }
+      g_s3_url_expires = (int)v;
+    }
+    if ((env = getenv("MRJUNEJUNE_DB_PATH")) && env[0] != '\0')
+      strncpy(g_db_path, env, sizeof(g_db_path) - 1);
+    if ((env = getenv("AWS_MRJUNEJUNE_ACCESS_KEY")) && env[0] != '\0')
+      strncpy(g_s3_access_key, env, sizeof(g_s3_access_key) - 1);
+    if ((env = getenv("AWS_MRJUNEJUNE_SECRET_ACCESS_KEY")) && env[0] != '\0')
+      strncpy(g_s3_secret_key, env, sizeof(g_s3_secret_key) - 1);
+
+    /* Auth env overrides */
+    if ((env = getenv("AUTH_COOKIE_SECRET")) && env[0] != '\0')
+      strncpy(g_auth_cookie_secret, env, sizeof(g_auth_cookie_secret) - 1);
+    if ((env = getenv("AUTH_BOOTSTRAP_USERNAME")) && env[0] != '\0')
+      strncpy(g_auth_bootstrap_username, env,
+              sizeof(g_auth_bootstrap_username) - 1);
+    if ((env = getenv("AUTH_BOOTSTRAP_PASSWORD_HASH")) && env[0] != '\0')
+      strncpy(g_auth_bootstrap_password_hash, env,
+              sizeof(g_auth_bootstrap_password_hash) - 1);
+    if ((env = getenv("AUTH_TRUSTED_PROXY")) && env[0] != '\0')
+      strncpy(g_auth_trusted_proxy, env, sizeof(g_auth_trusted_proxy) - 1);
+    if ((env = getenv("AUTH_SESSION_IDLE_TTL")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_IDLE_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_session_idle_ttl = v;
+    }
+    if ((env = getenv("AUTH_SESSION_ABS_TTL")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_SESSION_ABS_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_session_abs_ttl = v;
+    }
+    if ((env = getenv("AUTH_GUEST_TTL")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) || v <= 0)
+      {
+        fprintf(stderr, "[CONFIG] ERROR: AUTH_GUEST_TTL must be a positive integer\n");
+        exit(1);
+      }
+      g_auth_guest_ttl = v;
+    }
+    if ((env = getenv("AUTH_DEV_INSECURE_COOKIE")) && env[0] != '\0')
+      g_auth_dev_insecure =
+          (strcmp(env, "1") == 0 || strcmp(env, "true") == 0);
+    if ((env = getenv("SERVER_HOST")) && env[0] != '\0')
+      strncpy(g_server_host, env, sizeof(g_server_host) - 1);
+
+    /* Guest inference enable: runtime-only, boolean parsed strictly. */
+    if ((env = getenv("MRJUNEJUNE_ALLOW_GUEST_INFERENCE")) && env[0] != '\0')
+      g_guest_inference_enabled =
+          (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0);
+    else if ((env = getenv("MRJUNEJUNE_ALLOW_ANONYMOUS_INFERENCE")) && env[0] != '\0')
+      g_guest_inference_enabled =
+          (strcmp(env, "1") == 0 || strcasecmp(env, "true") == 0);
+
+    /* Guest quota env overrides: strict full-string integer, fail on malformed. */
+    if ((env = getenv("AUTH_GUEST_DAILY_TURNS")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) ||
+          v < G_GUEST_DAILY_TURNS_MIN || v > G_GUEST_DAILY_TURNS_MAX)
+      {
+        fprintf(stderr,
+                "[CONFIG] ERROR: AUTH_GUEST_DAILY_TURNS must be %d..%d\n",
+                G_GUEST_DAILY_TURNS_MIN, G_GUEST_DAILY_TURNS_MAX);
+        exit(1);
+      }
+      g_guest_daily_turns = v;
+    }
+    if ((env = getenv("AUTH_GUEST_DAILY_OUTPUT_TOKENS")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) ||
+          v < G_GUEST_DAILY_OUTPUT_TOKENS_MIN ||
+          v > G_GUEST_DAILY_OUTPUT_TOKENS_MAX)
+      {
+        fprintf(stderr,
+                "[CONFIG] ERROR: AUTH_GUEST_DAILY_OUTPUT_TOKENS must be %d..%d\n",
+                G_GUEST_DAILY_OUTPUT_TOKENS_MIN, G_GUEST_DAILY_OUTPUT_TOKENS_MAX);
+        exit(1);
+      }
+      g_guest_daily_output_tokens = v;
+    }
+    if ((env = getenv("AUTH_GUEST_REQUEST_OUTPUT_TOKENS")) && env[0] != '\0')
+    {
+      int64 v;
+      if (!config__parse_int64_strict(env, &v) ||
+          v < G_GUEST_REQUEST_OUTPUT_TOKENS_MIN)
+      {
+        fprintf(stderr,
+                "[CONFIG] ERROR: AUTH_GUEST_REQUEST_OUTPUT_TOKENS must be >= %d\n",
+                G_GUEST_REQUEST_OUTPUT_TOKENS_MIN);
+        exit(1);
+      }
+      g_guest_request_output_tokens = v;
+    }
+  }
 }
 
 static void init_database(void)
@@ -197,88 +551,43 @@
   Seobeo_Web_Server_Stop();
 }
 
-void Seobeo_Render_Html(
-    char *final_body,
-    char *template,
-    Dowa_Arena *arena
-)
+static Seobeo_Request_Entry *html_render_error(Dowa_Arena *arena, const char *msg)
 {
-  size_t current_offset = 0;
-  char *cursor = template;
-
-  int32 token_len = 2;
-
-  while (1)
-  {
-    char *start_tag = strstr(cursor, "{{");
-    if (!start_tag) break;
-
-    char *end_tag = strstr(start_tag, "}}");
-    if (!end_tag) break;
-
-    Seobeo_Log(SEOBEO_INFO, "[Curr] Life\n");
-
-    size_t leading_len = start_tag - cursor;
-    memcpy(final_body + current_offset, cursor, leading_len);
-    current_offset += leading_len;
-
-    size_t name_len = end_tag - (start_tag + token_len);
-    char *include_name = Dowa_Arena_Allocate(arena, name_len + 1);
-    memcpy(include_name, start_tag + token_len, name_len);
-    include_name[name_len] = '\0';
-
-    size_t sub_file_size = 0;
-    char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
-    Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading include: '%s' -> %s (size=%zu)\n",
-              include_name, sub_content ? "OK" : "FAILED", sub_file_size);
-    if (sub_content)
-    {
-      memcpy(final_body + current_offset, sub_content, sub_file_size);
-      current_offset += sub_file_size;
-      free(sub_content);
-    }
-
-    cursor = end_tag + 2;
-  }
-  strcpy(final_body + current_offset, cursor);
+  Seobeo_Request_Entry *resp = NULL;
+  Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
+  Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena);
+  Dowa_HashMap_Push_Arena(resp, "body", (char *)msg, arena);
+  return resp;
 }
 
-void Seobeo_Render_Html_FilePath(
-    char *final_body,
-    char *path,
-    Dowa_Arena *arena
-) {
-  Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Loading main template: '%s'\n", path);
-  size_t html_size = 0;
-  char *template = Seobeo_Web_LoadFile(path, &html_size);
-  Seobeo_Log(SEOBEO_DEBUG, "[TEMPLATE] Main template loaded: %s (size=%zu)\n", template ? "OK" : "FAILED", html_size);
-  if (!template) return;
-  Seobeo_Render_Html(final_body, template, arena);
-}
+#define HTML_PAGE_CAP (128 * 1024)
 
 Seobeo_Request_Entry* GetHomePage(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
-  Seobeo_Request_Entry *resp = NULL; 
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/index.html", arena);
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
 
 Seobeo_Request_Entry* GetResume(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
-  Seobeo_Request_Entry *resp = NULL; 
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/resume/index.html", arena);
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/resume/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
 
 Seobeo_Request_Entry* GetTools(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
-  Seobeo_Request_Entry *resp = NULL; 
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/tools/index.html", arena);
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -287,8 +596,9 @@
 Seobeo_Request_Entry* GetMDToHTML(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/tools/markdown_to_html/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/markdown_to_html/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -296,8 +606,9 @@
 Seobeo_Request_Entry* GetFileConverter(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/tools/file_converter/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/file_converter/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -305,8 +616,9 @@
 Seobeo_Request_Entry* GetHlsPlayer(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/tools/hls_player/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/hls_player/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -314,12 +626,14 @@
 Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/tools/latex_editor/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
 
+
 static Seobeo_Request_Entry *LatexErrorResponse(
     Dowa_Arena *arena,
     int status,
@@ -831,8 +1145,9 @@
 Seobeo_Request_Entry *RenderBlogList(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/blog/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/blog/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -847,8 +1162,15 @@
   char *blog_id = ((Seobeo_Request_Entry*)blog_id_kv)->value;
   snprintf(file_path, 1024, "/blog/%s/index.html", blog_id);
 
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, file_path, arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, file_path, arena))
+  {
+    Seobeo_Request_Entry *err = NULL;
+    Dowa_HashMap_Push_Arena(err, "status", "404", arena);
+    Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena);
+    Dowa_HashMap_Push_Arena(err, "body", "Not found", arena);
+    return err;
+  }
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -870,8 +1192,9 @@
 Seobeo_Request_Entry *GetTalk(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/talk/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/talk/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -879,17 +1202,46 @@
 Seobeo_Request_Entry *GetJrpg(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/jrpg/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/jrpg/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena);
+  return resp;
+}
+
+Seobeo_Request_Entry *GetLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+  (void)req;
+  Seobeo_Request_Entry *resp = NULL;
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/login/index.html", arena))
+  {
+    Seobeo_Request_Entry *err = NULL;
+    Dowa_HashMap_Push_Arena(err, "status", "500", arena);
+    Dowa_HashMap_Push_Arena(err, "content-type", "text/plain; charset=utf-8", arena);
+    Dowa_HashMap_Push_Arena(err, "cache-control", "no-store", arena);
+    Dowa_HashMap_Push_Arena(err, "body", "Internal Server Error", arena);
+    return err;
+  }
+  Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+  Dowa_HashMap_Push_Arena(
+      resp, "content-type", "text/html; charset=utf-8", arena);
+  Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
+  Dowa_HashMap_Push_Arena(resp, "pragma", "no-cache", arena);
+  Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena);
+  Dowa_HashMap_Push_Arena(resp, "referrer-policy", "no-referrer", arena);
+  Dowa_HashMap_Push_Arena(
+      resp, "content-security-policy", "frame-ancestors 'none'", arena);
   return resp;
 }
 
 Seobeo_Request_Entry *GetNotesLogin(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/notes/login.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/login.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -897,8 +1249,9 @@
 Seobeo_Request_Entry *GetNotes(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -906,9 +1259,9 @@
 Seobeo_Request_Entry *GetNoteById(Seobeo_Request_Entry *req, Dowa_Arena *arena)
 {
   Seobeo_Request_Entry *resp = NULL;
-  char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
-  // Same template - JavaScript handles the note_id from URL
-  Seobeo_Render_Html_FilePath(final_body, "/notes/index.html", arena);
+  char *final_body = Dowa_Arena_Allocate(arena, HTML_PAGE_CAP);
+  if (!final_body || !Mjj_Template_Render_File(final_body, HTML_PAGE_CAP, "/notes/index.html", arena))
+    return html_render_error(arena, "Internal Server Error");
   Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
   return resp;
 }
@@ -2003,52 +2356,19 @@
   signal(SIGINT, handle_sigint);
   signal(SIGTERM, handle_sigint);
 
-  // Load server config
+  // Load the ignored runtime config when present; environment overrides follow.
   load_config("mrjunejune/.config");
-  const char *database_override = getenv("MRJUNEJUNE_DB_PATH");
-  if (database_override && database_override[0] != '\0')
-  {
-    snprintf(g_db_path, sizeof(g_db_path), "%s", database_override);
-  }
-
-  // Load S3 credentials from .env
-  FILE *env_file = fopen(".env", "r");
-  static char s3_access_key[128] = {0};
-  static char s3_secret_key[128] = {0};
 
-  if (env_file)
-  {
-    char line[512];
-    while (fgets(line, sizeof(line), env_file))
-    {
-      if (strncmp(line, "AWS_MRJUNEJUNE_ACCESS_KEY=", 26) == 0)
-      {
-        char *val = line + 26;
-        size_t len = strlen(val);
-        while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
-        strncpy(s3_access_key, val, sizeof(s3_access_key) - 1);
-      }
-      else if (strncmp(line, "AWS_MRJUNEJUNE_SECRET_ACCESS_KEY=", 33) == 0)
-      {
-        char *val = line + 33;
-        size_t len = strlen(val);
-        while (len > 0 && (val[len-1] == '\n' || val[len-1] == '\r')) val[--len] = '\0';
-        strncpy(s3_secret_key, val, sizeof(s3_secret_key) - 1);
-      }
-    }
-    fclose(env_file);
-  }
-
-  // Initialize S3 config
-  g_s3_config.access_key_id = s3_access_key;
-  g_s3_config.secret_access_key = s3_secret_key;
+  // Initialize S3 config using global credentials populated by load_config
+  g_s3_config.access_key_id = g_s3_access_key;
+  g_s3_config.secret_access_key = g_s3_secret_key;
   g_s3_config.region = g_s3_region;
   g_s3_config.bucket = g_s3_bucket;
   g_s3_config.endpoint = NULL;
   g_s3_config.use_path_style = FALSE;
 
   printf("[S3] Configured: region=%s, bucket=%s, key=%s...\n",
-         g_s3_region, g_s3_bucket, s3_access_key[0] ? "***" : "(missing)");
+         g_s3_region, g_s3_bucket, g_s3_access_key[0] ? "***" : "(missing)");
 
   // Show current working directory
   char cwd[1024];
@@ -2060,8 +2380,188 @@
 
   // Initialize database
   init_database();
-  if (!Conversation_API_Init(g_db_path))
-    Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n");
+  {
+    /* Validate per-request token cap against daily limit now that both
+     * are finalised (env overrides run inside load_config). */
+    if (g_guest_request_output_tokens > g_guest_daily_output_tokens)
+      g_guest_request_output_tokens = g_guest_daily_output_tokens;
+
+    Conversation_API_Guest_Policy policy = {
+      .guest_inference_enabled = g_guest_inference_enabled,
+      .daily_turns             = g_guest_daily_turns,
+      .daily_output_tokens     = g_guest_daily_output_tokens,
+      .request_output_tokens   = g_guest_request_output_tokens,
+    };
+    if (!Conversation_API_Init(g_db_path, &policy))
+      Seobeo_Log(SEOBEO_ERROR, "[CONVERSATION] Store unavailable\n");
+  }
+
+  /* Validate and decode AUTH_COOKIE_SECRET: hex string → raw bytes. */
+  uint8  cookie_secret_bytes[AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES];
+  size_t cookie_secret_byte_len = 0;
+  {
+    size_t hex_len = strlen(g_auth_cookie_secret);
+    boolean bad_len = (
+        hex_len < (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2) ||
+        hex_len > (size_t)(AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2) ||
+        hex_len % 2 != 0);
+    if (bad_len)
+    {
+      OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret));
+      fprintf(stderr,
+              "[AUTH] AUTH_COOKIE_SECRET must be %d–%d hex chars "
+              "(%d–%d random bytes). "
+              "Generate with: openssl rand -hex 32\n",
+              AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES * 2,
+              AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES * 2,
+              AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES,
+              AUTH_CRYPTO_COOKIE_SECRET_MAX_BYTES);
+      exit(1);
+    }
+    cookie_secret_byte_len = config__hex_decode(
+        g_auth_cookie_secret, cookie_secret_bytes, sizeof(cookie_secret_bytes));
+    OPENSSL_cleanse(g_auth_cookie_secret, sizeof(g_auth_cookie_secret));
+    if (cookie_secret_byte_len < (size_t)AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_COOKIE_SECRET contains non-hex characters or "
+              "is too short.\n");
+      exit(1);
+    }
+  }
+
+  /* Bootstrap pairing: both username+hash must be set, or both absent. */
+  {
+    boolean has_user = g_auth_bootstrap_username[0] != '\0';
+    boolean has_hash = g_auth_bootstrap_password_hash[0] != '\0';
+    if (has_user != has_hash)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_BOOTSTRAP_USERNAME and AUTH_BOOTSTRAP_PASSWORD_HASH "
+              "must both be set or both absent.\n");
+      exit(1);
+    }
+    if (has_hash &&
+        Auth_Crypto_Password_Hash_Validate(g_auth_bootstrap_password_hash) != AUTH_CRYPTO_OK)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_BOOTSTRAP_PASSWORD_HASH has unrecognized format "
+              "(expected zenbu-scrypt$v=1$...).\n");
+      exit(1);
+    }
+  }
+
+  /* TTL validation: all positive, idle <= abs, all within 1 min – 1 year. */
+#define CONFIG_TTL_MIN_SECS 60
+#define CONFIG_TTL_MAX_SECS 31536000
+  {
+    int64 idle  = g_auth_session_idle_ttl;
+    int64 abst  = g_auth_session_abs_ttl;
+    int64 guest = g_auth_guest_ttl;
+    if (idle <= 0 || abst <= 0 || guest <= 0 ||
+        idle < CONFIG_TTL_MIN_SECS || idle > CONFIG_TTL_MAX_SECS ||
+        abst < CONFIG_TTL_MIN_SECS || abst > CONFIG_TTL_MAX_SECS ||
+        guest < CONFIG_TTL_MIN_SECS || guest > CONFIG_TTL_MAX_SECS)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] TTL values must be %d–%d seconds.\n",
+              CONFIG_TTL_MIN_SECS, CONFIG_TTL_MAX_SECS);
+      exit(1);
+    }
+    if (idle > abst)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_SESSION_IDLE_TTL must not exceed "
+              "AUTH_SESSION_ABS_TTL.\n");
+      exit(1);
+    }
+  }
+
+  /* Trusted proxy: validate and canonicalize via inet_pton/inet_ntop. */
+  if (g_auth_trusted_proxy[0] != '\0')
+  {
+    struct in_addr  addr4;
+    struct in6_addr addr6;
+    char canonical[AUTH_CRYPTO_IP_MAX_BYTES];
+    canonical[0] = '\0';
+    if (inet_pton(AF_INET, g_auth_trusted_proxy, &addr4) == 1)
+    {
+      if (!inet_ntop(AF_INET, &addr4, canonical, sizeof(canonical)))
+      {
+        OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+        fprintf(stderr,
+                "[AUTH] AUTH_TRUSTED_PROXY: IPv4 canonicalization failed.\n");
+        exit(1);
+      }
+    }
+    else if (inet_pton(AF_INET6, g_auth_trusted_proxy, &addr6) == 1)
+    {
+      if (!inet_ntop(AF_INET6, &addr6, canonical, sizeof(canonical)))
+      {
+        OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+        fprintf(stderr,
+                "[AUTH] AUTH_TRUSTED_PROXY: IPv6 canonicalization failed.\n");
+        exit(1);
+      }
+    }
+    else
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_TRUSTED_PROXY must be a valid IPv4 or IPv6 "
+              "address (e.g. 192.168.1.1 or ::1).\n");
+      exit(1);
+    }
+    strncpy(g_auth_trusted_proxy, canonical, sizeof(g_auth_trusted_proxy) - 1);
+    g_auth_trusted_proxy[sizeof(g_auth_trusted_proxy) - 1] = '\0';
+  }
+
+  /*
+   * Loopback enforcement: AUTH_DEV_INSECURE_COOKIE=true is only permitted
+   * when SERVER_HOST is explicitly a loopback address.  Production/edge
+   * deployments must use Secure cookies.
+   */
+  {
+    boolean is_loopback = (
+        strcmp(g_server_host, "127.0.0.1") == 0 ||
+        strcmp(g_server_host, "::1")        == 0 ||
+        strcmp(g_server_host, "localhost")   == 0);
+    if (g_auth_dev_insecure && !is_loopback)
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] AUTH_DEV_INSECURE_COOKIE=true requires SERVER_HOST "
+              "to be a loopback address (127.0.0.1, ::1, or localhost). "
+              "Set SERVER_HOST=127.0.0.1 for local development.\n");
+      exit(1);
+    }
+  }
+
+  /* Initialize auth module — fail closed: no auth means no server. */
+  {
+    if (!Auth_API_Init(
+            g_db_path,
+            cookie_secret_bytes, cookie_secret_byte_len,
+            g_auth_bootstrap_username[0] ? g_auth_bootstrap_username : NULL,
+            g_auth_bootstrap_password_hash[0] ? g_auth_bootstrap_password_hash : NULL,
+            g_auth_trusted_proxy[0] ? g_auth_trusted_proxy : NULL,
+            g_auth_session_idle_ttl,
+            g_auth_session_abs_ttl,
+            g_auth_guest_ttl,
+            g_auth_dev_insecure))
+    {
+      OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+      fprintf(stderr,
+              "[AUTH] Auth init failed — refusing to start server.\n");
+      exit(1);
+    }
+    OPENSSL_cleanse(cookie_secret_bytes, sizeof(cookie_secret_bytes));
+  }
   const char *sidecar_path = getenv("MRJUNEJUNE_INFERENCE_SIDECAR_PATH");
   const char *copilot_cli_path = getenv("MRJUNEJUNE_COPILOT_CLI_PATH");
   if (sidecar_path && copilot_cli_path)
@@ -2085,6 +2585,8 @@
   }
 
   Seobeo_Router_Init();
+  Auth_API_Register_Routes();
+  Admin_API_Register_Routes();
   Conversation_API_Register_Routes();
 
   Seobeo_Router_Register("GET", "/", GetHomePage);
@@ -2137,6 +2639,9 @@
   Seobeo_Router_Register("GET", "/jrpg", GetJrpg);
   Seobeo_Router_Register("GET", "/jrpg/index.html", GetRedirectJrpg);
 
+  // -- Login --/
+  Seobeo_Router_Register("GET", "/login", GetLogin);
+
   // -- Notes --/
   Seobeo_Router_Register("GET", "/notes", GetNotes);
   Seobeo_Router_Register("GET", "/notes/", GetNotes);
@@ -2152,8 +2657,19 @@
   const char *server_port = getenv("MRJUNEJUNE_PORT");
   if (!server_port || server_port[0] == '\0')
     server_port = "6969";
-  Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
+  const char *server_bind = g_server_host[0] ? g_server_host : "0.0.0.0";
+  Mjj_Template_Renderer_Init("mrjunejune/src");
+  int server_result = Seobeo_Web_Server_Start_On(
+      server_bind, "mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
   Seobeo_Worker_Pool_Destroy(g_media_worker_pool);
   g_media_worker_pool = NULL;
   Conversation_API_Destroy();
+  Auth_API_Destroy();
+  if (server_result != 0)
+  {
+    fprintf(stderr, "[STARTUP] Server bind/listen failed (host=%s port=%s)\n",
+            server_bind, server_port);
+    return 1;
+  }
+  return 0;
 }