- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
219 lines
6.6 KiB
JavaScript
219 lines
6.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Credential & Attribution Validator
|
|
*
|
|
* Prevents unauthorized changes to:
|
|
* - Copyright attribution
|
|
* - GitHub repository URLs
|
|
* - Contact emails
|
|
* - Domain names
|
|
* - API keys/credentials
|
|
* - Legal entity names
|
|
*
|
|
* Architectural enforcement to prevent Claude from arbitrarily changing
|
|
* important credentials without human confirmation.
|
|
*
|
|
* Created: 2025-10-18
|
|
* Incident: Footer component overwrote copyright from "John G Stroh" to "Tractatus AI Safety Framework"
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// IMMUTABLE CREDENTIALS - These values MUST NOT be changed without human approval
|
|
const PROTECTED_VALUES = {
|
|
copyright_holder: 'John G Stroh',
|
|
github_org: 'AgenticGovernance',
|
|
github_repo: 'tractatus-framework',
|
|
primary_email: 'hello@agenticgovernance.digital',
|
|
support_email: 'support@agenticgovernance.digital',
|
|
domain: 'agenticgovernance.digital',
|
|
license: 'Apache License 2.0',
|
|
};
|
|
|
|
// File patterns that commonly contain credentials
|
|
const CREDENTIAL_FILES = [
|
|
'public/js/components/footer.js',
|
|
'LICENSE',
|
|
'package.json',
|
|
'public/**/*.html',
|
|
'docs/**/*.md',
|
|
];
|
|
|
|
/**
|
|
* Validate that protected values haven't been arbitrarily changed
|
|
*/
|
|
function validateCredentials(filePath, fileContent) {
|
|
const violations = [];
|
|
|
|
// Check for incorrect copyright attribution
|
|
const copyrightRegex = /©.*?(\d{4})\s+([^.\n<]+)/g;
|
|
let match;
|
|
while ((match = copyrightRegex.exec(fileContent)) !== null) {
|
|
const holder = match[2].trim();
|
|
|
|
// Allow "John G Stroh" or "John G. Stroh" (with period)
|
|
if (!holder.includes('John G') && !holder.includes(PROTECTED_VALUES.copyright_holder)) {
|
|
violations.push({
|
|
type: 'COPYRIGHT_MISMATCH',
|
|
line: getLineNumber(fileContent, match.index),
|
|
found: holder,
|
|
expected: PROTECTED_VALUES.copyright_holder,
|
|
message: `Copyright holder must be "${PROTECTED_VALUES.copyright_holder}" (from LICENSE file)`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for incorrect GitHub URLs
|
|
const githubRegex = /github\.com\/([^\/\s"']+)\/([^\/\s"'<]+)/g;
|
|
while ((match = githubRegex.exec(fileContent)) !== null) {
|
|
const org = match[1];
|
|
const repo = match[2];
|
|
|
|
// Check if it's supposed to be our repo but has wrong values
|
|
if ((org !== PROTECTED_VALUES.github_org || repo !== PROTECTED_VALUES.github_repo) &&
|
|
(org.includes('tractatus') || repo.includes('tractatus'))) {
|
|
violations.push({
|
|
type: 'GITHUB_URL_INCORRECT',
|
|
line: getLineNumber(fileContent, match.index),
|
|
found: `${org}/${repo}`,
|
|
expected: `${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}`,
|
|
message: `GitHub repository must be "${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}"`
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for placeholder GitHub URLs
|
|
if (fileContent.includes('github.com/yourusername') ||
|
|
fileContent.includes('github.com/your-org')) {
|
|
violations.push({
|
|
type: 'GITHUB_PLACEHOLDER',
|
|
message: 'GitHub URL contains placeholder - must use actual repository URL',
|
|
expected: `https://github.com/${PROTECTED_VALUES.github_org}/${PROTECTED_VALUES.github_repo}`
|
|
});
|
|
}
|
|
|
|
// Check for incorrect domain references
|
|
const domainRegex = /https?:\/\/([a-z0-9-]+\.)+[a-z]{2,}/gi;
|
|
while ((match = domainRegex.exec(fileContent)) !== null) {
|
|
const url = match[0];
|
|
// Skip CDNs, external services, etc.
|
|
if (url.includes('tractatus') && !url.includes(PROTECTED_VALUES.domain)) {
|
|
violations.push({
|
|
type: 'DOMAIN_INCORRECT',
|
|
line: getLineNumber(fileContent, match.index),
|
|
found: url,
|
|
expected: `https://${PROTECTED_VALUES.domain}`,
|
|
message: `Domain must be "${PROTECTED_VALUES.domain}"`
|
|
});
|
|
}
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
/**
|
|
* Get line number from character index
|
|
*/
|
|
function getLineNumber(content, index) {
|
|
return content.substring(0, index).split('\n').length;
|
|
}
|
|
|
|
/**
|
|
* Main validation function
|
|
*/
|
|
function validate(filePath, operation) {
|
|
// Only validate files that might contain credentials
|
|
const shouldValidate = CREDENTIAL_FILES.some(pattern => {
|
|
if (pattern.includes('*')) {
|
|
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
return regex.test(filePath);
|
|
}
|
|
return filePath.includes(pattern);
|
|
});
|
|
|
|
if (!shouldValidate) {
|
|
return { valid: true };
|
|
}
|
|
|
|
// Read file content
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(filePath, 'utf-8');
|
|
} catch (error) {
|
|
// File doesn't exist yet (being created)
|
|
return { valid: true };
|
|
}
|
|
|
|
// Validate credentials
|
|
const violations = validateCredentials(filePath, content);
|
|
|
|
if (violations.length > 0) {
|
|
return {
|
|
valid: false,
|
|
violations,
|
|
message: formatViolations(filePath, violations)
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
/**
|
|
* Format violations for human-readable output
|
|
*/
|
|
function formatViolations(filePath, violations) {
|
|
let message = `\n❌ CREDENTIAL VIOLATION in ${filePath}\n\n`;
|
|
message += `PROTECTED CREDENTIALS ENFORCEMENT:\n`;
|
|
message += `The following values are IMMUTABLE and require human approval to change:\n\n`;
|
|
|
|
for (const violation of violations) {
|
|
message += ` ⚠️ ${violation.type}:\n`;
|
|
if (violation.line) {
|
|
message += ` Line: ${violation.line}\n`;
|
|
}
|
|
if (violation.found) {
|
|
message += ` Found: "${violation.found}"\n`;
|
|
}
|
|
message += ` Expected: "${violation.expected}"\n`;
|
|
message += ` Reason: ${violation.message}\n\n`;
|
|
}
|
|
|
|
message += `\n`;
|
|
message += `RESOLUTION:\n`;
|
|
message += `1. This change requires EXPLICIT human approval\n`;
|
|
message += `2. Verify the change is intentional and authorized\n`;
|
|
message += `3. If correcting an error, update to the protected values above\n`;
|
|
message += `4. If legitimately changing credentials, update PROTECTED_VALUES in:\n`;
|
|
message += ` scripts/hook-validators/validate-credentials.js\n`;
|
|
message += `\n`;
|
|
message += `Protected values are defined in LICENSE and package.json.\n`;
|
|
message += `Copyright holder: John G Stroh (LICENSE:189)\n`;
|
|
message += `GitHub repository: AgenticGovernance/tractatus-framework\n`;
|
|
|
|
return message;
|
|
}
|
|
|
|
// Export for use as module
|
|
module.exports = { validate, validateCredentials, PROTECTED_VALUES };
|
|
|
|
// CLI usage
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length < 2) {
|
|
console.error('Usage: validate-credentials.js <file-path> <operation>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const [filePath, operation] = args;
|
|
const result = validate(filePath, operation);
|
|
|
|
if (!result.valid) {
|
|
console.error(result.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|