feat: initial release of Protocol Droid v0.2.0

A tool for authoring, sharing, and curating social protocols.

Features:
- Protocol library with search, tag filtering, and sort by date/relevance
- Protocol authoring with structured fields (title, description, steps, outcome, practice, source, tags)
- Protocol forking with provenance tracking
- Collection creation with searchable protocol picker and ordering
- User accounts with roles (admin, member, viewer)
- YAML import/export for portability
- Self-hostable on LAMP/Cloudron, works in subdirectories
- Responsive design with hamburger menu on mobile
- About page

Security:
- CSRF protection via Origin/Referer validation
- Session regeneration on login/register
- Secure session cookie params (HttpOnly, SameSite, Secure)
- Visibility enforcement on private/unlisted items
- YAML object injection hardening
- Login rate limiting
- Path traversal protection
- Input validation and length clamping
- Vote value constraining

Stack: PHP 8.x + MySQL/MariaDB, vanilla JS frontend, no external dependencies.

Hippocratic License (HL3-CORE).
This commit is contained in:
Protocolbot
2026-07-06 13:18:08 -06:00
commit 02794e565e
23 changed files with 3346 additions and 0 deletions
Executable
+81
View File
@@ -0,0 +1,81 @@
-- Protocol Droid — MySQL Schema
-- Works with MySQL 5.7+ / MariaDB 10.3+
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) UNIQUE NOT NULL,
display_name VARCHAR(128),
bio TEXT,
password_hash VARCHAR(255), -- bcrypt; NULL if SSO-only
sso_provider VARCHAR(64), -- 'oidc', 'github', 'google', etc.
sso_id VARCHAR(255), -- external identity id
role ENUM('admin','member','viewer') DEFAULT 'member',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS protocols (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(128) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
source VARCHAR(255), -- free text or @username
source_url VARCHAR(500),
tags VARCHAR(500), -- JSON array string
forked_from VARCHAR(128), -- slug of parent protocol
image VARCHAR(500),
steps TEXT, -- JSON array of {headline, description}
outcome TEXT,
practice TEXT,
author_id INT REFERENCES users(id),
visibility ENUM('public','private','unlisted') DEFAULT 'public',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_visibility (visibility),
INDEX idx_author (author_id),
INDEX idx_forked (forked_from)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS collections (
id INT AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(128) UNIQUE NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
source VARCHAR(255),
image VARCHAR(500),
author_id INT REFERENCES users(id),
visibility ENUM('public','private','unlisted') DEFAULT 'public',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_visibility (visibility),
INDEX idx_author (author_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS collection_items (
collection_id INT NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
item_type ENUM('protocol','collection') NOT NULL,
item_slug VARCHAR(128) NOT NULL,
position INT DEFAULT 0,
PRIMARY KEY (collection_id, item_type, item_slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS votes (
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
protocol_slug VARCHAR(128) NOT NULL,
value INT DEFAULT 1, -- +1 upvote, -1 downvote
comment TEXT, -- optional evaluation text
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, protocol_slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS config (
`key` VARCHAR(64) PRIMARY KEY,
`value` TEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Default config entries
INSERT INTO config (`key`, `value`) VALUES
('site_name', 'Protocol Droid'),
('site_tagline', 'A commons of social protocols'),
('registration_enabled', 'true'),
('require_approval', 'false')
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);