Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions bundles/campaigns/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "campaigns",
"name": "Campaigns",
"version": "1.0.2",
"version": "1.0.3",
"description": "Social media campaign manager — draft, schedule, and publish posts across platforms (starting with Reddit)",
"type": "mcp-server",
"author": "Crow",
Expand All @@ -22,9 +22,12 @@
"env_vars": [
{
"name": "CROW_CAMPAIGNS_ENCRYPTION_KEY",
"description": "AES-256-GCM encryption key for Reddit credentials (generate with: openssl rand -base64 32)",
"description": "Key that encrypts your saved Reddit credentials (AES-256-GCM). Crow generates it at install and saves it in Settings → Passwords (this machine only); a reinstall takes it back from the keychain. Without it, re-enter the Reddit credentials.",
"required": true,
"secret": true
"secret": true,
"generate": "secret",
"keychain": true,
"keychain_label": "data encryption key"
}
],
"notes": "12 MCP tools for social media campaign management. Reddit support via snoowrap. Credentials encrypted at rest with AES-256-GCM. Dashboard panel with approval workflow."
Expand Down
2 changes: 1 addition & 1 deletion bundles/campaigns/panel/campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default {
}

if (action === "save_creds") {
const ENCRYPTION_KEY = process.env.CROW_CAMPAIGNS_ENCRYPTION_KEY;
const ENCRYPTION_KEY = (await import(pathToFileURL(join(serverDir, "key.js")).href)).campaignsKey();
if (!ENCRYPTION_KEY) {
return res.redirectAfterPost("/dashboard/campaigns?view=setup&error=CROW_CAMPAIGNS_ENCRYPTION_KEY+not+set");
}
Expand Down
3 changes: 2 additions & 1 deletion bundles/campaigns/panel/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { createDbClient } = await import(pathToFileURL(join(serverDir, "db.js")).
const { encrypt, decrypt } = await import(pathToFileURL(join(serverDir, "crypto.js")).href);
const { createRedditClient, testAuth } = await import(pathToFileURL(join(serverDir, "reddit-client.js")).href);

const ENCRYPTION_KEY = process.env.CROW_CAMPAIGNS_ENCRYPTION_KEY;
const { campaignsKey } = await import(pathToFileURL(join(serverDir, "key.js")).href);

/**
* @param {Function} authMiddleware - Dashboard auth middleware
Expand Down Expand Up @@ -228,6 +228,7 @@ export default function campaignsRouter(authMiddleware) {
// --- Credentials: save (encrypt + validate) ---
router.post("/api/credentials", async (req, res) => {
try {
const ENCRYPTION_KEY = campaignsKey();
if (!ENCRYPTION_KEY) return res.status(500).json({ error: "CROW_CAMPAIGNS_ENCRYPTION_KEY not set" });

const { platform, username, client_id, client_secret, password } = req.body;
Expand Down
20 changes: 20 additions & 0 deletions bundles/campaigns/server/key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* The campaigns credentials key (CROW_CAMPAIGNS_ENCRYPTION_KEY), read when needed.
*
* The MCP server gets it in its own env: the gateway env overlaid by this instance's
* campaigns entry in mcp-addons.json (proxy.js). The dashboard panel runs in the gateway,
* so it reads them in that SAME order — the entry first — and both always use one key. A
* generated key reaches only the entry (mode 600). Never logged.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";

export function campaignsKey(env = process.env) {
let fromEntry = "";
try {
const file = join(env.CROW_HOME || join(homedir(), ".crow"), "mcp-addons.json");
fromEntry = JSON.parse(readFileSync(file, "utf8"))?.campaigns?.env?.CROW_CAMPAIGNS_ENCRYPTION_KEY || "";
} catch { /* no entry */ }
return fromEntry || env.CROW_CAMPAIGNS_ENCRYPTION_KEY || "";
}
14 changes: 7 additions & 7 deletions bundles/mastodon/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "mastodon",
"name": "Mastodon",
"version": "1.0.3",
"version": "1.0.4",
"description": "The flagship ActivityPub server — federated microblogging at scale. Heaviest of the small-AP bundles. Hosts a public (or invite-only) Mastodon instance with the full v1/v2 API surface, web UI, and Sidekiq background job processing. Media is kept on local disk.",
"type": "bundle",
"author": "Crow",
Expand Down Expand Up @@ -33,14 +33,14 @@
"env_vars": [
{ "name": "MASTODON_LOCAL_DOMAIN", "description": "Public domain that appears in handles (@user@<this>). IMMUTABLE after first boot — changing breaks all federation identities.", "required": true },
{ "name": "MASTODON_WEB_DOMAIN", "description": "Domain where the web UI lives (if using apex-delegation this differs from LOCAL_DOMAIN; otherwise same).", "required": false },
{ "name": "MASTODON_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_SECRET_KEY_BASE", "description": "Rails secret_key_base. Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_OTP_SECRET", "description": "2FA OTP secret. Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; saved in Settings → Passwords (this machine only), so a database restored from a backup still opens after a reinstall.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "database password" },
{ "name": "MASTODON_SECRET_KEY_BASE", "description": "Rails secret_key_base. Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "session signing key (secret_key_base)" },
{ "name": "MASTODON_OTP_SECRET", "description": "2FA OTP secret. Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "two-factor secret" },
{ "name": "MASTODON_VAPID_PRIVATE_KEY", "description": "Web Push VAPID private key (P-256). Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "vapid_private_key" },
{ "name": "MASTODON_VAPID_PUBLIC_KEY", "description": "Web Push VAPID public key, derived from the private key. Generated by Crow at install.", "required": true, "generate": "vapid_public_key", "pair": "MASTODON_VAPID_PRIVATE_KEY" },
{ "name": "MASTODON_AR_ENCRYPTION_DETERMINISTIC_KEY", "description": "Active Record encryption deterministic key (Mastodon 4.3+ refuses to boot without it). Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_AR_ENCRYPTION_KEY_DERIVATION_SALT", "description": "Active Record encryption key-derivation salt (Mastodon 4.3+). Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_AR_ENCRYPTION_PRIMARY_KEY", "description": "Active Record encryption primary key (Mastodon 4.3+). Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "MASTODON_AR_ENCRYPTION_DETERMINISTIC_KEY", "description": "Active Record encryption deterministic key (Mastodon 4.3+ refuses to boot without it). Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "data encryption key (deterministic)" },
{ "name": "MASTODON_AR_ENCRYPTION_KEY_DERIVATION_SALT", "description": "Active Record encryption key-derivation salt (Mastodon 4.3+). Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "data encryption key-derivation salt" },
{ "name": "MASTODON_AR_ENCRYPTION_PRIMARY_KEY", "description": "Active Record encryption primary key (Mastodon 4.3+). Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "data encryption key (primary)" },
{ "name": "MASTODON_ACCESS_TOKEN", "description": "Admin access token (Settings → Development → New Application, grant read/write/admin:read/admin:write).", "required": false, "secret": true },
{ "name": "MASTODON_URL", "description": "Internal URL the MCP server uses to reach Mastodon's web container (default http://mastodon-web:3000 over crow-federation).", "default": "http://mastodon-web:3000", "required": false },
{ "name": "MASTODON_MEDIA_RETENTION_DAYS", "description": "Remote media cache retention in days (default 14; 7 on Pi-class hosts).", "default": "14", "required": false },
Expand Down
6 changes: 3 additions & 3 deletions bundles/peertube/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "peertube",
"name": "PeerTube",
"version": "1.0.3",
"version": "1.0.4",
"description": "Federated video platform over ActivityPub — YouTube-alternative on the fediverse. Upload and transcode video, federate channels, stream via WebTorrent or HLS. Heaviest bundle in the federated line — video is kept on local disk, so plan for disk space and keep the transcoding/retention policy tight.",
"type": "bundle",
"author": "Crow",
Expand Down Expand Up @@ -32,8 +32,8 @@
},
"env_vars": [
{ "name": "PEERTUBE_WEBSERVER_HOSTNAME", "description": "Public domain (subdomain; IMMUTABLE after first federation). Appears in video URLs, channel URLs, every ActivityPub actor.", "required": true },
{ "name": "PEERTUBE_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "PEERTUBE_SECRET", "description": "Server secret that signs federation requests. IMMUTABLE: generated once by Crow at install, kept across reinstalls, never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "PEERTUBE_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; saved in Settings → Passwords (this machine only), so a database restored from a backup still opens after a reinstall.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "database password" },
{ "name": "PEERTUBE_SECRET", "description": "Server secret that signs federation requests. IMMUTABLE: generated once by Crow at install, kept across reinstalls, saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "server secret" },
{ "name": "PEERTUBE_ACCESS_TOKEN", "description": "OAuth bearer token (Administration → Users → your account → Personal Access Tokens, OR POST /api/v1/users/token with username+password + client_id+client_secret from /api/v1/oauth-clients/local).", "required": false, "secret": true },
{ "name": "PEERTUBE_URL", "description": "Internal URL the MCP server uses to reach PeerTube (default http://peertube:9000 over crow-federation).", "default": "http://peertube:9000", "required": false },
{ "name": "PEERTUBE_ADMIN_EMAIL", "description": "Admin email — where takedown notices, abuse reports, and federation alerts go.", "required": false },
Expand Down
6 changes: 3 additions & 3 deletions bundles/pixelfed/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "pixelfed",
"name": "Pixelfed",
"version": "1.0.3",
"version": "1.0.4",
"description": "Federated photo-sharing server over ActivityPub — Instagram-alternative on the fediverse. Publish photos/stories/collections; remote Mastodon/GoToSocial/Funkwhale followers see your posts in their timelines. Media is kept on local disk.",
"type": "bundle",
"author": "Crow",
Expand Down Expand Up @@ -32,8 +32,8 @@
},
"env_vars": [
{ "name": "PIXELFED_HOSTNAME", "description": "Public domain (subdomain; path-mounts break ActivityPub).", "required": true },
{ "name": "PIXELFED_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "secret" },
{ "name": "PIXELFED_APP_KEY", "description": "Laravel application key (base64: + 32 random bytes). Generated by Crow at install; never shown.", "required": true, "secret": true, "generate": "laravel_key" },
{ "name": "PIXELFED_DB_PASSWORD", "description": "Password for the bundled Postgres role. Generated by Crow at install; saved in Settings → Passwords (this machine only), so a database restored from a backup still opens after a reinstall.", "required": true, "secret": true, "generate": "secret", "keychain": true, "keychain_label": "database password" },
{ "name": "PIXELFED_APP_KEY", "description": "Laravel application key (base64: + 32 random bytes). Generated by Crow at install; saved in Settings → Passwords (this machine only): export the keychain to keep a copy, or data encrypted with it cannot be recovered on another machine.", "required": true, "secret": true, "generate": "laravel_key", "keychain": true, "keychain_label": "data encryption key (APP_KEY)" },
{ "name": "PIXELFED_ACCESS_TOKEN", "description": "OAuth2 Personal Access Token (Settings → Development → New Application).", "required": false, "secret": true },
{ "name": "PIXELFED_URL", "description": "Internal URL the MCP server uses to reach Pixelfed (default http://pixelfed:80 over crow-federation).", "default": "http://pixelfed:80", "required": false },
{ "name": "PIXELFED_OPEN_REGISTRATION", "description": "Allow new user signups (true/false). Default false — opening registration without moderation tooling invites abuse.", "default": "false", "required": false },
Expand Down
5 changes: 2 additions & 3 deletions bundles/tax/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Crow Tax — Environment Variables

# Required: Passphrase for encrypting PII (SSN, names) at rest
# Set a passphrase of your own (keep a copy: without it saved returns cannot be read).
CROW_TAX_ENCRYPTION_KEY=
# CROW_TAX_ENCRYPTION_KEY is generated by Crow at install and saved in
# Settings → Passwords; it is not set here.

# Optional: Path to tax database (defaults to Crow's main DB)
# CROW_TAX_DB_PATH=/path/to/tax.db
Expand Down
36 changes: 28 additions & 8 deletions bundles/tax/engine/ingest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Returns extracted data with confidence scores.
*/

import { readFileSync } from "node:fs";
import { readFileSync, statSync } from "node:fs";
import { extname } from "node:path";
import { extractW2 } from "./w2-reader.js";
import { extract1099 } from "./1099-reader.js";
Expand Down Expand Up @@ -37,19 +37,39 @@ const EXTRACTORS = {
* @param {string} documentType - Type of document (w2, 1099-sa, 1098-t, etc.)
* @returns {Promise<{ data: object, confidence: object, warnings: string[], raw_text: string }>}
*/
export async function ingestDocument(filePath, documentType) {
const MAX_DOCUMENT_BYTES = 25 * 1024 * 1024;

export async function ingestDocument(filePath, documentType, { keys = null } = {}) {
// Checked BEFORE reading: the path comes from the AI (crow_tax_ingest_document), so a
// device, FIFO or huge file must never be read.
const ext = extname(filePath).toLowerCase();
if (ext !== ".pdf" && ext !== ".enc") throw new Error(`Only PDF files are supported. Got: ${ext}`);
const st = statSync(filePath);
if (!st.isFile()) throw new Error("Not a regular file");
if (st.size > MAX_DOCUMENT_BYTES) throw new Error("Document is larger than 25 MB");
const buf = readFileSync(filePath);
// A document the dashboard stored encrypted at rest (.enc): open it with the tax key.
const { isEncryptedDocument, decryptDocument } = await import("../../server/documents-crypto.js");
if (isEncryptedDocument(buf)) {
const k = keys || [process.env.CROW_TAX_ENCRYPTION_KEY, process.env.CROW_TAX_ENCRYPTION_KEY_PREVIOUS];
return ingestPdfBuffer((await decryptDocument(buf, k)).plain, documentType, filePath);
}
if (ext !== ".pdf") throw new Error(`Only PDF files are supported. Got: ${ext}`);
return ingestPdfBuffer(buf, documentType, filePath);
}

/**
* Ingest a PDF already in memory (the dashboard upload: the file on disk is stored
* encrypted, so the plaintext is never written out). `label` names it in errors.
*/
export async function ingestPdfBuffer(pdfBuffer, documentType, label = "the uploaded document") {
const filePath = label;
const type = documentType.toLowerCase();
const extractor = EXTRACTORS[type];
if (!extractor) {
throw new Error(`Unsupported document type: ${documentType}. Supported: ${Object.keys(EXTRACTORS).join(", ")}`);
}

const ext = extname(filePath).toLowerCase();
if (ext !== ".pdf") {
throw new Error(`Only PDF files are supported. Got: ${ext}`);
}

const pdfBuffer = readFileSync(filePath);

// Pipeline: try form fields first, then text extraction, then OCR
let result;
Expand Down
12 changes: 8 additions & 4 deletions bundles/tax/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "tax",
"name": "Tax Filing Assistant",
"version": "2.0.3",
"version": "2.1.0",
"description": "Prepare federal income taxes — PDF document ingestion, calculation, form generation, and guided filing. Federal returns only (no state). Supports 1040, Schedules 1/C/D/SE, Form 8889, Form 8863, and common credits. 2024 + 2025 tax years.",
"type": "mcp-server",
"author": "Crow",
Expand All @@ -28,10 +28,14 @@
},
{
"name": "CROW_TAX_ENCRYPTION_KEY",
"description": "Passphrase that encrypts personal data at rest (SSN, names). Choose your own and keep a copy: without it saved returns can't be read. Changing it keeps the key it replaces in CROW_TAX_ENCRYPTION_KEY_PREVIOUS, and the tax server moves saved returns to the new key when it next starts. Restart it between two changes.",
"description": "Key that encrypts your saved returns and uploaded documents at rest. Crow generates it at install and saves it in Settings → Passwords (this machine only). Export the keychain to keep a copy: without the key, saved returns and documents can't be read on another machine. A reinstall takes it back from the keychain.",
"required": true,
"secret": true,
"previous_key": "CROW_TAX_ENCRYPTION_KEY_PREVIOUS"
"generate": "secret",
"keychain": true,
"keychain_label": "data encryption key",
"previous_key": "CROW_TAX_ENCRYPTION_KEY_PREVIOUS",
"replace_values_sha256": ["113e3beef46e2a2ab77b0c3ea62ef37d823b82eab4f713a44af32bcdd2655365"]
},
{
"name": "CROW_TAX_ENCRYPTION_KEY_PREVIOUS",
Expand All @@ -41,5 +45,5 @@
}
],
"webUI": null,
"notes": "Federal returns only — no state income tax support. Supports MFJ, Single, HoH filing. 2025 tax year. Generates filled IRS PDFs ready to mail and step-by-step guided filing instructions. PII is encrypted at rest. Not a substitute for professional tax advice."
"notes": "Federal returns only — no state income tax support. Supports MFJ, Single, HoH filing. 2025 tax year. Generates filled IRS PDFs ready to mail and step-by-step guided filing instructions. Saved returns and uploaded documents are encrypted at rest with a key Crow generates and keeps in Settings → Passwords; the figures read from documents (shown on the Documents tab) are stored unencrypted in Crow's database. Documents uploaded before tax 2.1.0 are encrypted when the dashboard next starts; plain copies may remain in earlier backups or on the disk until overwritten. Not a substitute for professional tax advice."
}
Loading
Loading