mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
This commit bundles two unrelated changes that were swept together by an
accidental `git add -A` in another working session. Documented here so the
history reflects what's actually inside.
═══════════════════════════════════════════════════════════════════════
1. fix(mana-auth): /api/v1/auth/login mints JWT via auth.handler instead
of api.signInEmail
═══════════════════════════════════════════════════════════════════════
Previous attempt (commit 55cc75e7d) tried to fix the broken JWT mint in
/api/v1/auth/login by switching the cookie name from `mana.session_token`
to `__Secure-mana.session_token` for production. That was necessary but
not sufficient: Better Auth's session cookie value isn't just the raw
session token, it's `<token>.<HMAC>` where the HMAC is derived from the
better-auth secret. Reconstructing the cookie from auth.api.signInEmail's
JSON response only gave us the raw token, so /api/auth/token's
get-session middleware still couldn't validate it and the JWT mint kept
silently failing.
Real fix: do the sign-in via auth.handler (the HTTP path) rather than
auth.api.signInEmail (the SDK path). The handler returns a real fetch
Response with a Set-Cookie header containing the fully signed cookie
envelope. We capture that header verbatim and forward it as the cookie
on the /api/auth/token request, which now passes validation and mints
the JWT correctly.
Verified end-to-end on auth.mana.how:
$ curl -X POST https://auth.mana.how/api/v1/auth/login \
-d '{"email":"...","password":"..."}'
{
"user": {...},
"token": "<session token>",
"accessToken": "eyJhbGciOiJFZERTQSI...", ← real JWT now
"refreshToken": "<session token>"
}
Side benefits:
- Email-not-verified path is now handled by checking
signInResponse.status === 403 directly, no more catching APIError
with the comment-noted async-stream footgun.
- X-Forwarded-For is forwarded explicitly so Better Auth's rate limiter
and our security log see the real client IP.
- The leftover catch block now only handles unexpected exceptions
(network errors etc); the FORBIDDEN-checking logic in it is dead but
harmless and left in for defense in depth.
═══════════════════════════════════════════════════════════════════════
2. chore: remove the entire self-hosted Matrix stack (Synapse, Element,
Manalink, mana-matrix-bot)
═══════════════════════════════════════════════════════════════════════
The Matrix subsystem ran parallel to the main Mana product without any
load-bearing integration: the unified web app never imported matrix-js-sdk,
the chat module uses mana-sync (local-first), and mana-matrix-bot's
plugins duplicated features the unified app already ships natively.
Keeping it alive cost a Synapse + Element + matrix-web + bot container
quartet, three Cloudflare routes, an OIDC provider plugin in mana-auth,
and a steady drip of devlog/dependency churn.
Removed:
- apps/matrix (Manalink web + mobile, ~150 files)
- services/mana-matrix-bot (Go bot with ~20 plugins)
- docker/matrix configs (Synapse + Element)
- synapse/element-web/matrix-web/mana-matrix-bot services in
docker-compose.macmini.yml
- matrix.mana.how/element.mana.how/link.mana.how Cloudflare tunnel routes
- OIDC provider plugin + matrix-synapse trustedClient + matrixUserLinks
table from mana-auth (oauth_* schema definitions also removed)
- MatrixService import path in mana-media (importFromMatrix endpoint)
- Matrix notification channel in mana-notify (worker, metrics, config,
channel_type enum, MatrixOptions handler)
- Matrix entries from shared-branding (mana-apps + app-icons),
notify-client, the i18n bundle, the observatory map, the credits
app-label list, the landing footer/apps page, the prometheus + alerts
+ promtail tier mappings, and the matrix-related deploy paths in
cd-macmini.yml + ci.yml
Devlog/manascore/blueprint entries that mention Matrix are left intact
as historical record. The oauth_* + matrix_user_links Postgres tables
stay on existing prod databases — code can no longer write to them, drop
them in a follow-up migration if you want them gone for real.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
325 lines
8.3 KiB
JavaScript
Executable file
325 lines
8.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Audit Workspace Dependencies
|
|
*
|
|
* Finds SvelteKit web apps that import workspace packages (@mana/*, @project/shared, etc.)
|
|
* without declaring them in package.json. This works locally due to pnpm hoisting but breaks
|
|
* in Docker builds.
|
|
*
|
|
* Usage:
|
|
* node scripts/audit-workspace-deps.mjs # Report missing deps
|
|
* node scripts/audit-workspace-deps.mjs --fix # Auto-add missing deps and run pnpm install
|
|
*/
|
|
|
|
import { readFileSync, readdirSync, statSync, writeFileSync, existsSync } from 'fs';
|
|
import { join, relative, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { execSync } from 'child_process';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
const MONOREPO_ROOT = join(__dirname, '..');
|
|
|
|
const FIX_MODE = process.argv.includes('--fix');
|
|
|
|
// Colors
|
|
const RED = '\x1b[31m';
|
|
const GREEN = '\x1b[32m';
|
|
const YELLOW = '\x1b[33m';
|
|
const DIM = '\x1b[2m';
|
|
const RESET = '\x1b[0m';
|
|
const BOLD = '\x1b[1m';
|
|
|
|
// Directories to skip when scanning source files
|
|
const SKIP_DIRS = new Set(['node_modules', 'dist', '.svelte-kit', 'build', '.turbo', '.vercel']);
|
|
|
|
// File extensions to scan
|
|
const SCAN_EXTENSIONS = new Set(['.ts', '.svelte', '.js']);
|
|
|
|
// Regex to match workspace package imports
|
|
// Matches: @mana/*, @calendar/shared, @todo/shared, @zitare/content, etc.
|
|
const IMPORT_REGEX =
|
|
/(?:import\s+(?:[\s\S]*?\s+from\s+)?|import\s*\()\s*['"](@[a-z-]+\/[a-z-]+)(?:\/[^'"]*)?['"]/g;
|
|
|
|
// Known workspace scopes (to distinguish from npm packages like @sveltejs/kit)
|
|
const WORKSPACE_SCOPES = new Set([
|
|
'@mana',
|
|
'@calendar',
|
|
'@chat',
|
|
'@clock',
|
|
'@contacts',
|
|
'@context',
|
|
'@music',
|
|
'@nutriphi',
|
|
'@photos',
|
|
'@picture',
|
|
'@planta',
|
|
'@presi',
|
|
'@storage',
|
|
'@todo',
|
|
'@traces',
|
|
'@zitare',
|
|
'@mana-core',
|
|
]);
|
|
|
|
/**
|
|
* Build a set of all workspace package names by scanning the monorepo.
|
|
*/
|
|
function buildWorkspacePackageSet() {
|
|
const names = new Set();
|
|
const patterns = [
|
|
'packages/*/package.json',
|
|
'apps/*/packages/*/package.json',
|
|
'services/*/package.json',
|
|
];
|
|
|
|
for (const pattern of patterns) {
|
|
const parts = pattern.split('/');
|
|
let dirs = [MONOREPO_ROOT];
|
|
|
|
for (const part of parts) {
|
|
const nextDirs = [];
|
|
for (const dir of dirs) {
|
|
if (part === '*') {
|
|
try {
|
|
const entries = readdirSync(dir);
|
|
for (const entry of entries) {
|
|
const full = join(dir, entry);
|
|
try {
|
|
if (statSync(full).isDirectory()) {
|
|
nextDirs.push(full);
|
|
}
|
|
} catch {}
|
|
}
|
|
} catch {}
|
|
} else {
|
|
nextDirs.push(join(dir, part));
|
|
}
|
|
}
|
|
dirs = nextDirs;
|
|
}
|
|
|
|
for (const pkgPath of dirs) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
if (pkg.name) names.add(pkg.name);
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
/**
|
|
* Recursively collect source files from a directory.
|
|
*/
|
|
function collectSourceFiles(dir, files = []) {
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(dir);
|
|
} catch {
|
|
return files;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
if (SKIP_DIRS.has(entry)) continue;
|
|
const full = join(dir, entry);
|
|
try {
|
|
const stat = statSync(full);
|
|
if (stat.isDirectory()) {
|
|
collectSourceFiles(full, files);
|
|
} else if (stat.isFile()) {
|
|
const ext = entry.slice(entry.lastIndexOf('.'));
|
|
if (SCAN_EXTENSIONS.has(ext)) {
|
|
files.push(full);
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/**
|
|
* Extract workspace package imports from a source file.
|
|
*/
|
|
function extractImports(filePath, workspacePackages) {
|
|
const content = readFileSync(filePath, 'utf-8');
|
|
const imports = new Set();
|
|
|
|
let match;
|
|
// Reset regex lastIndex
|
|
IMPORT_REGEX.lastIndex = 0;
|
|
|
|
while ((match = IMPORT_REGEX.exec(content)) !== null) {
|
|
const pkg = match[1]; // e.g. @mana/shared-utils
|
|
const scope = pkg.split('/')[0]; // e.g. @mana
|
|
|
|
if (WORKSPACE_SCOPES.has(scope) && workspacePackages.has(pkg)) {
|
|
imports.add(pkg);
|
|
}
|
|
}
|
|
|
|
return imports;
|
|
}
|
|
|
|
/**
|
|
* Get declared dependencies from package.json (both deps and devDeps).
|
|
*/
|
|
function getDeclaredDeps(pkgJsonPath) {
|
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
const deps = new Set();
|
|
|
|
for (const name of Object.keys(pkg.dependencies || {})) {
|
|
deps.add(name);
|
|
}
|
|
for (const name of Object.keys(pkg.devDependencies || {})) {
|
|
deps.add(name);
|
|
}
|
|
|
|
return { deps, pkg };
|
|
}
|
|
|
|
/**
|
|
* Find all SvelteKit web app directories.
|
|
*/
|
|
function findWebApps() {
|
|
const appsDir = join(MONOREPO_ROOT, 'apps');
|
|
const webApps = [];
|
|
|
|
try {
|
|
const projects = readdirSync(appsDir);
|
|
for (const project of projects) {
|
|
// Try the correct path structure: apps/{project}/apps/web
|
|
const correctWebDir = join(appsDir, project, 'apps', 'web');
|
|
if (existsSync(join(correctWebDir, 'package.json'))) {
|
|
webApps.push(correctWebDir);
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
return webApps.sort();
|
|
}
|
|
|
|
// --- Main ---
|
|
|
|
console.log(`${BOLD}Workspace Dependency Audit${RESET}`);
|
|
console.log(`Scanning SvelteKit web apps for undeclared workspace imports...\n`);
|
|
|
|
const workspacePackages = buildWorkspacePackageSet();
|
|
console.log(`${DIM}Found ${workspacePackages.size} workspace packages${RESET}\n`);
|
|
|
|
const webApps = findWebApps();
|
|
let totalMissing = 0;
|
|
const fixes = []; // { webDir, filterName, missingPkgs }
|
|
|
|
for (const webDir of webApps) {
|
|
const relPath = relative(MONOREPO_ROOT, webDir);
|
|
const pkgJsonPath = join(webDir, 'package.json');
|
|
const srcDir = join(webDir, 'src');
|
|
|
|
if (!existsSync(srcDir)) {
|
|
console.log(`${DIM}- ${relPath} - no src/ directory, skipping${RESET}`);
|
|
continue;
|
|
}
|
|
|
|
const { deps, pkg } = getDeclaredDeps(pkgJsonPath);
|
|
const sourceFiles = collectSourceFiles(srcDir);
|
|
|
|
// Map: packageName -> Set of files that import it
|
|
const importedPackages = new Map();
|
|
|
|
for (const file of sourceFiles) {
|
|
const fileImports = extractImports(file, workspacePackages);
|
|
for (const imp of fileImports) {
|
|
if (!importedPackages.has(imp)) {
|
|
importedPackages.set(imp, new Set());
|
|
}
|
|
importedPackages.get(imp).add(relative(webDir, file));
|
|
}
|
|
}
|
|
|
|
// Find missing: imported but not declared
|
|
const missing = new Map();
|
|
for (const [pkgName, files] of importedPackages) {
|
|
if (!deps.has(pkgName)) {
|
|
missing.set(pkgName, files);
|
|
}
|
|
}
|
|
|
|
if (missing.size === 0) {
|
|
console.log(`${GREEN}\u2713${RESET} ${relPath} - all imports covered`);
|
|
} else {
|
|
totalMissing += missing.size;
|
|
const filterName = pkg.name || relPath;
|
|
console.log(
|
|
`${RED}\u2717${RESET} ${relPath} - ${BOLD}${missing.size}${RESET} missing dep${missing.size > 1 ? 's' : ''}:`
|
|
);
|
|
|
|
const missingPkgs = [];
|
|
for (const [pkgName, files] of missing) {
|
|
const fileList = [...files].sort().join(', ');
|
|
console.log(` ${YELLOW}${pkgName}${RESET} ${DIM}(imported in ${fileList})${RESET}`);
|
|
missingPkgs.push(pkgName);
|
|
}
|
|
console.log(` ${DIM}\u2192 pnpm add ${missingPkgs.join(' ')} --filter ${filterName}${RESET}`);
|
|
|
|
fixes.push({ webDir, pkgJsonPath, filterName, missingPkgs });
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
|
|
if (totalMissing === 0) {
|
|
console.log(`${GREEN}${BOLD}All workspace imports are properly declared!${RESET}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(
|
|
`${RED}${BOLD}Found ${totalMissing} missing dependency declaration${totalMissing > 1 ? 's' : ''} across ${fixes.length} app${fixes.length > 1 ? 's' : ''}.${RESET}`
|
|
);
|
|
|
|
if (!FIX_MODE) {
|
|
console.log(`\n${DIM}Run with --fix to automatically add missing dependencies.${RESET}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// --- Fix mode ---
|
|
console.log(`\n${BOLD}Applying fixes...${RESET}\n`);
|
|
|
|
for (const { pkgJsonPath, missingPkgs } of fixes) {
|
|
const relPkgPath = relative(MONOREPO_ROOT, pkgJsonPath);
|
|
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
|
|
if (!pkg.dependencies) {
|
|
pkg.dependencies = {};
|
|
}
|
|
|
|
for (const pkgName of missingPkgs) {
|
|
pkg.dependencies[pkgName] = 'workspace:*';
|
|
console.log(` ${GREEN}+${RESET} Added ${pkgName} to ${relPkgPath}`);
|
|
}
|
|
|
|
// Sort dependencies alphabetically
|
|
const sorted = {};
|
|
for (const key of Object.keys(pkg.dependencies).sort()) {
|
|
sorted[key] = pkg.dependencies[key];
|
|
}
|
|
pkg.dependencies = sorted;
|
|
|
|
writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, '\t') + '\n');
|
|
}
|
|
|
|
console.log(`\n${BOLD}Running pnpm install to update lockfile...${RESET}\n`);
|
|
|
|
try {
|
|
execSync('pnpm install', {
|
|
cwd: MONOREPO_ROOT,
|
|
stdio: 'inherit',
|
|
});
|
|
console.log(`\n${GREEN}${BOLD}All fixes applied successfully!${RESET}`);
|
|
} catch {
|
|
console.error(`\n${RED}pnpm install failed. You may need to run it manually.${RESET}`);
|
|
process.exit(1);
|
|
}
|