Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew Gundersen 2026-02-25 15:35:31 -05:00
commit cbea265775
20 changed files with 730 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

BIN
apps/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,7 @@
{
"id": "contacts",
"name": "Contacts",
"port": 3001,
"db": true,
"migrate": "npm run migrate"
}

View file

@ -0,0 +1,22 @@
{
"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"
}
}

5
apps/contacts/src/db.ts Normal file
View file

@ -0,0 +1,5 @@
import { Pool } from "pg"
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})

View file

@ -0,0 +1,9 @@
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}`))

View file

@ -0,0 +1,21 @@
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)
})

View file

@ -0,0 +1,61 @@
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()
})

View file

@ -0,0 +1,22 @@
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
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}

13
auth/Makefile Normal file
View file

@ -0,0 +1,13 @@
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)

149
auth/src/main.c Normal file
View file

@ -0,0 +1,149 @@
#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;
}

50
auth/src/pam_auth.c Normal file
View file

@ -0,0 +1,50 @@
#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;
}

10
auth/src/pam_auth.h Normal file
View file

@ -0,0 +1,10 @@
#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

66
ui/index.html Normal file
View file

@ -0,0 +1,66 @@
<!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>

140
ui/js/os.js Normal file
View file

@ -0,0 +1,140 @@
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)
}
}))
})

23
ui/styles/canvas.css Normal file
View file

@ -0,0 +1,23 @@
*, *::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;
}

24
ui/styles/cursor.css Normal file
View file

@ -0,0 +1,24 @@
#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;
}

34
ui/styles/dock.css Normal file
View file

@ -0,0 +1,34 @@
#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);
}

61
ui/styles/window.css Normal file
View file

@ -0,0 +1,61 @@
.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;
}