--- /dev/null
+{
+ "id": "contacts",
+ "name": "Contacts",
+ "port": 3001,
+ "db": true,
+ "migrate": "npm run migrate"
+}
--- /dev/null
+{
+ "name": "crimata-contacts",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "tsx watch src/index.ts",
+ "build": "tsc",
+ "start": "node dist/index.js",
+ "migrate": "node -r tsx/cjs src/migrate.ts"
+ },
+ "dependencies": {
+ "express": "^4.18.0",
+ "pg": "^8.11.0"
+ },
+ "devDependencies": {
+ "@types/express": "^4.17.0",
+ "@types/node": "^20.0.0",
+ "@types/pg": "^8.11.0",
+ "tsx": "^4.7.0",
+ "typescript": "^5.4.0"
+ }
+}
--- /dev/null
+import { Pool } from "pg"
+
+export const pool = new Pool({
+ connectionString: process.env.DATABASE_URL,
+})
--- /dev/null
+import express from "express"
+import { router } from "./routes"
+
+const app = express()
+app.use(express.json())
+app.use("/contacts", router)
+
+const port = Number(process.env.PORT ?? 3001)
+app.listen(port, () => console.log(`contacts app listening on :${port}`))
--- /dev/null
+import { pool } from "./db"
+
+async function migrate() {
+ await pool.query(`
+ CREATE TABLE IF NOT EXISTS contacts (
+ id SERIAL PRIMARY KEY,
+ name TEXT NOT NULL,
+ domain TEXT NOT NULL UNIQUE,
+ avatar_url TEXT,
+ notes TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+ )
+ `)
+ console.log("contacts: migration complete")
+ await pool.end()
+}
+
+migrate().catch((err) => {
+ console.error("contacts: migration failed", err)
+ process.exit(1)
+})
--- /dev/null
+import { Router } from "express"
+import { pool } from "./db"
+import type { CreateContactBody, UpdateContactBody } from "./types"
+
+export const router = Router()
+
+// List all contacts
+router.get("/", async (req, res) => {
+ const result = await pool.query(
+ "SELECT * FROM contacts ORDER BY name ASC"
+ )
+ res.json(result.rows)
+})
+
+// Get a single contact
+router.get("/:id", async (req, res) => {
+ const result = await pool.query(
+ "SELECT * FROM contacts WHERE id = $1",
+ [req.params.id]
+ )
+ if (result.rowCount === 0) return res.status(404).json({ error: "Not found" })
+ res.json(result.rows[0])
+})
+
+// Create a contact
+router.post("/", async (req, res) => {
+ const { name, domain, avatar_url, notes } = req.body as CreateContactBody
+ if (!name || !domain) {
+ return res.status(400).json({ error: "name and domain are required" })
+ }
+ const result = await pool.query(
+ `INSERT INTO contacts (name, domain, avatar_url, notes)
+ VALUES ($1, $2, $3, $4)
+ RETURNING *`,
+ [name, domain, avatar_url ?? null, notes ?? null]
+ )
+ res.status(201).json(result.rows[0])
+})
+
+// Update a contact
+router.patch("/:id", async (req, res) => {
+ const { name, domain, avatar_url, notes } = req.body as UpdateContactBody
+ const result = await pool.query(
+ `UPDATE contacts
+ SET name = COALESCE($1, name),
+ domain = COALESCE($2, domain),
+ avatar_url = COALESCE($3, avatar_url),
+ notes = COALESCE($4, notes)
+ WHERE id = $5
+ RETURNING *`,
+ [name ?? null, domain ?? null, avatar_url ?? null, notes ?? null, req.params.id]
+ )
+ if (result.rowCount === 0) return res.status(404).json({ error: "Not found" })
+ res.json(result.rows[0])
+})
+
+// Delete a contact
+router.delete("/:id", async (req, res) => {
+ await pool.query("DELETE FROM contacts WHERE id = $1", [req.params.id])
+ res.status(204).send()
+})
--- /dev/null
+export interface Contact {
+ id: number
+ name: string
+ domain: string
+ avatar_url: string | null
+ notes: string | null
+ created_at: string
+}
+
+export interface CreateContactBody {
+ name: string
+ domain: string
+ avatar_url?: string
+ notes?: string
+}
+
+export interface UpdateContactBody {
+ name?: string
+ domain?: string
+ avatar_url?: string
+ notes?: string
+}
--- /dev/null
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "commonjs",
+ "lib": ["ES2022"],
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
--- /dev/null
+CC = gcc
+CFLAGS = -Wall -Wextra -O2
+LIBS = -lpam -lmicrohttpd
+SRC = src/main.c src/pam_auth.c
+OUT = crimata-auth
+
+.PHONY: all clean
+
+all:
+ $(CC) $(CFLAGS) $(SRC) $(LIBS) -o $(OUT)
+
+clean:
+ rm -f $(OUT)
--- /dev/null
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <microhttpd.h>
+#include "pam_auth.h"
+
+#define PORT 7700
+#define MAX_BODY 4096
+
+/* ── JSON helpers ─────────────────────────────────────────────────────────── */
+
+static const char *json_get(const char *json, const char *key, char *out, size_t out_len)
+{
+ char search[128];
+ snprintf(search, sizeof(search), "\"%s\"", key);
+ const char *pos = strstr(json, search);
+ if (!pos) return NULL;
+
+ pos += strlen(search);
+ while (*pos == ' ' || *pos == ':' || *pos == ' ') pos++;
+ if (*pos != '"') return NULL;
+ pos++;
+
+ size_t i = 0;
+ while (*pos && *pos != '"' && i < out_len - 1)
+ out[i++] = *pos++;
+ out[i] = '\0';
+ return out;
+}
+
+static enum MHD_Result send_json(struct MHD_Connection *conn,
+ unsigned int status,
+ const char *body)
+{
+ struct MHD_Response *resp = MHD_create_response_from_buffer(
+ strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
+ MHD_add_response_header(resp, "Content-Type", "application/json");
+ enum MHD_Result ret = MHD_queue_response(conn, status, resp);
+ MHD_destroy_response(resp);
+ return ret;
+}
+
+/* ── Request context ──────────────────────────────────────────────────────── */
+
+typedef struct {
+ char body[MAX_BODY];
+ size_t body_len;
+} request_ctx_t;
+
+/* ── Route handlers ───────────────────────────────────────────────────────── */
+
+static enum MHD_Result handle_health(struct MHD_Connection *conn)
+{
+ return send_json(conn, MHD_HTTP_OK, "{\"status\":\"ok\"}");
+}
+
+static enum MHD_Result handle_auth(struct MHD_Connection *conn, const char *body)
+{
+ char username[256] = {0};
+ char password[256] = {0};
+
+ if (!json_get(body, "username", username, sizeof(username)) ||
+ !json_get(body, "password", password, sizeof(password))) {
+ return send_json(conn, MHD_HTTP_BAD_REQUEST,
+ "{\"success\":false,\"error\":\"username and password required\"}");
+ }
+
+ int result = authenticate(username, password);
+ if (result == 0) {
+ return send_json(conn, MHD_HTTP_OK, "{\"success\":true}");
+ }
+ return send_json(conn, MHD_HTTP_UNAUTHORIZED,
+ "{\"success\":false,\"error\":\"invalid credentials\"}");
+}
+
+/* ── Main handler ─────────────────────────────────────────────────────────── */
+
+static enum MHD_Result handler(void *cls,
+ struct MHD_Connection *conn,
+ const char *url,
+ const char *method,
+ const char *version,
+ const char *upload_data,
+ size_t *upload_data_size,
+ void **con_cls)
+{
+ (void)cls; (void)version;
+
+ /* Health check */
+ if (strcmp(url, "/health") == 0 && strcmp(method, "GET") == 0)
+ return handle_health(conn);
+
+ /* Auth — collect body first */
+ if (strcmp(url, "/auth") == 0 && strcmp(method, "POST") == 0) {
+ if (!*con_cls) {
+ request_ctx_t *ctx = calloc(1, sizeof(request_ctx_t));
+ if (!ctx) return MHD_NO;
+ *con_cls = ctx;
+ return MHD_YES;
+ }
+
+ request_ctx_t *ctx = *con_cls;
+
+ if (*upload_data_size > 0) {
+ size_t remaining = MAX_BODY - ctx->body_len - 1;
+ size_t to_copy = *upload_data_size < remaining ? *upload_data_size : remaining;
+ memcpy(ctx->body + ctx->body_len, upload_data, to_copy);
+ ctx->body_len += to_copy;
+ *upload_data_size = 0;
+ return MHD_YES;
+ }
+
+ return handle_auth(conn, ctx->body);
+ }
+
+ return send_json(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"not found\"}");
+}
+
+static void request_completed(void *cls, struct MHD_Connection *conn,
+ void **con_cls, enum MHD_RequestTerminationCode toe)
+{
+ (void)cls; (void)conn; (void)toe;
+ if (*con_cls) { free(*con_cls); *con_cls = NULL; }
+}
+
+/* ── Entry point ──────────────────────────────────────────────────────────── */
+
+int main(void)
+{
+ struct MHD_Daemon *daemon = MHD_start_daemon(
+ MHD_USE_INTERNAL_POLLING_THREAD,
+ PORT,
+ NULL, NULL,
+ &handler, NULL,
+ MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL,
+ MHD_OPTION_END
+ );
+
+ if (!daemon) {
+ fprintf(stderr, "failed to start daemon on port %d\n", PORT);
+ return 1;
+ }
+
+ printf("crimata-auth listening on :%d\n", PORT);
+ getchar(); /* block until killed */
+
+ MHD_stop_daemon(daemon);
+ return 0;
+}
--- /dev/null
+#include <stdlib.h>
+#include <string.h>
+#include <security/pam_appl.h>
+#include "pam_auth.h"
+
+typedef struct {
+ const char *password;
+} pam_credentials_t;
+
+static int pam_conversation(int num_msg, const struct pam_message **msg,
+ struct pam_response **resp, void *appdata_ptr)
+{
+ pam_credentials_t *creds = (pam_credentials_t *)appdata_ptr;
+
+ *resp = calloc(num_msg, sizeof(struct pam_response));
+ if (!*resp) return PAM_BUF_ERR;
+
+ for (int i = 0; i < num_msg; i++) {
+ if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF ||
+ msg[i]->msg_style == PAM_PROMPT_ECHO_ON) {
+ (*resp)[i].resp = strdup(creds->password);
+ if (!(*resp)[i].resp) {
+ free(*resp);
+ return PAM_BUF_ERR;
+ }
+ }
+ }
+
+ return PAM_SUCCESS;
+}
+
+int authenticate(const char *username, const char *password)
+{
+ pam_credentials_t creds = { .password = password };
+ struct pam_conv conv = { pam_conversation, &creds };
+ pam_handle_t *pamh = NULL;
+ int result;
+
+ result = pam_start("login", username, &conv, &pamh);
+ if (result != PAM_SUCCESS) goto done;
+
+ result = pam_authenticate(pamh, PAM_SILENT);
+ if (result != PAM_SUCCESS) goto done;
+
+ result = pam_acct_mgmt(pamh, PAM_SILENT);
+
+done:
+ pam_end(pamh, result);
+ return (result == PAM_SUCCESS) ? 0 : -1;
+}
--- /dev/null
+#ifndef PAM_AUTH_H
+#define PAM_AUTH_H
+
+/*
+ * authenticate - verify username/password against PAM.
+ * Returns 0 on success, -1 on failure.
+ */
+int authenticate(const char *username, const char *password);
+
+#endif
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Crimata</title>
+ <link rel="stylesheet" href="styles/canvas.css" />
+ <link rel="stylesheet" href="styles/window.css" />
+ <link rel="stylesheet" href="styles/dock.css" />
+ <link rel="stylesheet" href="styles/cursor.css" />
+ <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
+</head>
+<body x-data="os" @keydown.window="handleKeydown">
+
+ <!-- Infinite canvas -->
+ <div id="canvas"
+ @mousedown="startPan"
+ @mousemove="pan"
+ @mouseup="endPan"
+ @mouseleave="endPan"
+ :style="`transform: translate(${offset.x}px, ${offset.y}px)`"
+ >
+ <!-- Windows render here -->
+ <template x-for="win in windows" :key="win.id">
+ <div class="window"
+ :style="`left:${win.x}px; top:${win.y}px; width:${win.w}px; height:${win.h}px; z-index:${win.z}`"
+ @mousedown.stop="focusWindow(win)"
+ >
+ <div class="window-titlebar"
+ @mousedown.stop="startDrag($event, win)"
+ >
+ <span class="window-title" x-text="win.title"></span>
+ <button class="window-close" @click.stop="closeWindow(win)">✕</button>
+ </div>
+ <div class="window-content">
+ <iframe :src="win.url" frameborder="0"></iframe>
+ </div>
+ </div>
+ </template>
+ </div>
+
+ <!-- Cursor input overlay -->
+ <div id="cursor-input" x-show="inputMode" @click.stop>
+ <input
+ id="cursor-input-field"
+ type="text"
+ x-model="cursorQuery"
+ @keydown.enter="submitQuery"
+ @keydown.escape="exitInputMode"
+ placeholder="Type a command..."
+ x-ref="cursorInput"
+ />
+ </div>
+
+ <!-- Dock -->
+ <div id="dock">
+ <template x-for="app in installedApps" :key="app.id">
+ <div class="dock-icon" @click="openApp(app)" :title="app.name">
+ <span x-text="app.icon"></span>
+ </div>
+ </template>
+ </div>
+
+ <script src="js/os.js"></script>
+</body>
+</html>
--- /dev/null
+document.addEventListener('alpine:init', () => {
+ Alpine.data('os', () => ({
+
+ // ── Canvas pan state ──────────────────────────────────────────────────
+ offset: { x: 0, y: 0 },
+ isPanning: false,
+ panStart: { x: 0, y: 0 },
+
+ startPan(e) {
+ if (e.target !== document.getElementById('canvas') &&
+ !e.target.closest('#canvas') ||
+ e.target.closest('.window')) return
+ this.isPanning = true
+ this.panStart = { x: e.clientX - this.offset.x, y: e.clientY - this.offset.y }
+ document.getElementById('canvas').classList.add('panning')
+ },
+
+ pan(e) {
+ if (!this.isPanning) return
+ this.offset = { x: e.clientX - this.panStart.x, y: e.clientY - this.panStart.y }
+ },
+
+ endPan() {
+ this.isPanning = false
+ document.getElementById('canvas').classList.remove('panning')
+ },
+
+ // ── Windows ───────────────────────────────────────────────────────────
+ windows: [],
+ zCounter: 100,
+
+ openApp(app) {
+ // Bring to front if already open
+ const existing = this.windows.find(w => w.appId === app.id)
+ if (existing) { this.focusWindow(existing); return }
+
+ this.windows.push({
+ id: Date.now(),
+ appId: app.id,
+ title: app.name,
+ url: app.url,
+ x: 120 + Math.random() * 200,
+ y: 80 + Math.random() * 100,
+ w: 900,
+ h: 600,
+ z: ++this.zCounter,
+ })
+ },
+
+ focusWindow(win) {
+ win.z = ++this.zCounter
+ },
+
+ closeWindow(win) {
+ this.windows = this.windows.filter(w => w.id !== win.id)
+ },
+
+ // ── Window drag ───────────────────────────────────────────────────────
+ dragging: null,
+ dragStart: { x: 0, y: 0 },
+
+ startDrag(e, win) {
+ this.focusWindow(win)
+ this.dragging = win
+ this.dragStart = { x: e.clientX - win.x, y: e.clientY - win.y }
+
+ const onMove = (e) => {
+ if (!this.dragging) return
+ this.dragging.x = e.clientX - this.dragStart.x
+ this.dragging.y = e.clientY - this.dragStart.y
+ }
+ const onUp = () => {
+ this.dragging = null
+ window.removeEventListener('mousemove', onMove)
+ window.removeEventListener('mouseup', onUp)
+ }
+ window.addEventListener('mousemove', onMove)
+ window.addEventListener('mouseup', onUp)
+ },
+
+ // ── Cursor input mode ─────────────────────────────────────────────────
+ inputMode: false,
+ cursorQuery: '',
+
+ handleKeydown(e) {
+ // ⌘+Space or Ctrl+Space toggles input mode
+ if ((e.metaKey || e.ctrlKey) && e.code === 'Space') {
+ e.preventDefault()
+ this.inputMode ? this.exitInputMode() : this.enterInputMode()
+ return
+ }
+ },
+
+ enterInputMode() {
+ this.inputMode = true
+ this.cursorQuery = ''
+ this.$nextTick(() => this.$refs.cursorInput?.focus())
+ },
+
+ exitInputMode() {
+ this.inputMode = false
+ this.cursorQuery = ''
+ },
+
+ submitQuery() {
+ const query = this.cursorQuery.trim().toLowerCase()
+
+ // Built-in commands
+ if (query === 'login') {
+ this.openApp({ id: 'login', name: 'Login', url: '/login', icon: '🔐' })
+ this.exitInputMode()
+ return
+ }
+
+ // Check if it matches an installed app name
+ const app = this.installedApps.find(a => a.name.toLowerCase() === query)
+ if (app) { this.openApp(app); this.exitInputMode(); return }
+
+ // Otherwise — send to LLM (placeholder)
+ console.log('LLM query:', query)
+ this.exitInputMode()
+ },
+
+ // ── Installed apps ────────────────────────────────────────────────────
+ // Loaded from crimata.json manifests on the server
+ installedApps: [
+ { id: 'bio', name: 'Bio', icon: '🪪', url: '/apps/bio' },
+ { id: 'contacts', name: 'Contacts', icon: '👥', url: '/apps/contacts' },
+ { id: 'blog', name: 'Blog', icon: '📝', url: '/apps/blog' },
+ ],
+
+ // ── Init ──────────────────────────────────────────────────────────────
+ init() {
+ // Open bio full-screen on load
+ const bio = this.installedApps.find(a => a.id === 'bio')
+ if (bio) this.openApp(bio)
+ }
+
+ }))
+})
--- /dev/null
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+html, body {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: #1a1a1a;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+}
+
+#canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ will-change: transform;
+ cursor: grab;
+}
+
+#canvas.panning {
+ cursor: grabbing;
+}
--- /dev/null
+#cursor-input {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 99999;
+}
+
+#cursor-input-field {
+ width: 480px;
+ padding: 14px 20px;
+ font-size: 1.1rem;
+ border: none;
+ border-radius: 12px;
+ background: rgba(255,255,255,0.95);
+ backdrop-filter: blur(20px);
+ box-shadow: 0 24px 80px rgba(0,0,0,0.4);
+ outline: none;
+ color: #111;
+}
+
+#cursor-input-field::placeholder {
+ color: #aaa;
+}
--- /dev/null
+#dock {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ gap: 12px;
+ background: rgba(255,255,255,0.15);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ padding: 10px 16px;
+ border-radius: 20px;
+ border: 1px solid rgba(255,255,255,0.2);
+ z-index: 9999;
+}
+
+.dock-icon {
+ width: 52px;
+ height: 52px;
+ border-radius: 12px;
+ background: rgba(255,255,255,0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.6rem;
+ cursor: pointer;
+ transition: transform 0.15s, background 0.15s;
+ user-select: none;
+}
+
+.dock-icon:hover {
+ transform: translateY(-8px) scale(1.1);
+ background: rgba(255,255,255,0.35);
+}
--- /dev/null
+.window {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 24px 80px rgba(0,0,0,0.5);
+ overflow: hidden;
+ min-width: 300px;
+ min-height: 200px;
+}
+
+.window-titlebar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ height: 40px;
+ background: #f5f5f5;
+ border-bottom: 1px solid #e0e0e0;
+ cursor: move;
+ user-select: none;
+ flex-shrink: 0;
+}
+
+.window-title {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: #333;
+}
+
+.window-close {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 0.8rem;
+ color: #999;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.15s, color 0.15s;
+}
+
+.window-close:hover {
+ background: #ff5f57;
+ color: #fff;
+}
+
+.window-content {
+ flex: 1;
+ overflow: hidden;
+}
+
+.window-content iframe {
+ width: 100%;
+ height: 100%;
+ border: none;
+}