BREAKING CHANGE: Remove hard delete option from deleteVerb() for consistent API - Add complete metadata namespace architecture with O(1) soft delete performance - Implement periodic cleanup system for old soft-deleted items - Add restore methods for both nouns and verbs - Require metadata contracts for all augmentations - Eliminate namespace collisions with clean separation (_brainy, _augmentations, _audit) - Optimize index performance using flattened dot-notation for O(1) lookups - Add comprehensive augmentation safety system with type-safe access control - Maintain full backward compatibility for existing data - Add enterprise-grade cleanup with configurable age thresholds and batch processing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
204 lines
No EOL
5.9 KiB
TypeScript
204 lines
No EOL
5.9 KiB
TypeScript
/**
|
|
* Runtime enforcement of metadata contracts
|
|
* Ensures augmentations only access declared fields
|
|
*/
|
|
|
|
import { BrainyAugmentation, MetadataAccess } from './brainyAugmentation.js'
|
|
|
|
export class MetadataEnforcer {
|
|
/**
|
|
* Enforce metadata access based on augmentation contract
|
|
* Returns a wrapped metadata object that enforces the contract
|
|
*/
|
|
static enforce(
|
|
augmentation: BrainyAugmentation,
|
|
metadata: any,
|
|
operation: 'read' | 'write' = 'write'
|
|
): any {
|
|
// Handle simple contracts
|
|
if (augmentation.metadata === 'none') {
|
|
// No access at all
|
|
if (operation === 'read') return null
|
|
throw new Error(`Augmentation '${augmentation.name}' has metadata='none' - cannot access metadata`)
|
|
}
|
|
|
|
if (augmentation.metadata === 'readonly') {
|
|
if (operation === 'read') {
|
|
// Return frozen deep clone for read-only access
|
|
return deepFreeze(deepClone(metadata))
|
|
}
|
|
throw new Error(`Augmentation '${augmentation.name}' has metadata='readonly' - cannot write`)
|
|
}
|
|
|
|
// Handle specific field access
|
|
const access = augmentation.metadata as MetadataAccess
|
|
|
|
if (operation === 'read') {
|
|
// For reads, filter to allowed fields
|
|
if (access.reads === '*') {
|
|
return deepClone(metadata) // Can read everything
|
|
}
|
|
|
|
if (!access.reads) {
|
|
return {} // No read access
|
|
}
|
|
|
|
// Filter to specific fields
|
|
const filtered: any = {}
|
|
for (const field of access.reads) {
|
|
if (field.includes('.')) {
|
|
// Handle nested fields like '_brainy.deleted'
|
|
const parts = field.split('.')
|
|
let source = metadata
|
|
let target = filtered
|
|
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i]
|
|
if (!source[part]) break
|
|
if (!target[part]) target[part] = {}
|
|
source = source[part]
|
|
target = target[part]
|
|
}
|
|
|
|
const lastPart = parts[parts.length - 1]
|
|
if (source && lastPart in source) {
|
|
target[lastPart] = source[lastPart]
|
|
}
|
|
} else {
|
|
// Simple field
|
|
if (field in metadata) {
|
|
filtered[field] = metadata[field]
|
|
}
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// For writes, create a proxy that validates
|
|
return new Proxy(metadata, {
|
|
set(target, prop, value) {
|
|
const field = String(prop)
|
|
|
|
// Check if write is allowed
|
|
if (access.writes === '*') {
|
|
// Can write anything
|
|
target[prop] = value
|
|
return true
|
|
}
|
|
|
|
if (!access.writes || !access.writes.includes(field)) {
|
|
throw new Error(
|
|
`Augmentation '${augmentation.name}' cannot write to field '${field}'. ` +
|
|
`Allowed writes: ${access.writes?.join(', ') || 'none'}`
|
|
)
|
|
}
|
|
|
|
// Check namespace if specified
|
|
if (access.namespace && !field.startsWith(access.namespace)) {
|
|
console.warn(
|
|
`Augmentation '${augmentation.name}' writing outside its namespace. ` +
|
|
`Expected: ${access.namespace}.*, got: ${field}`
|
|
)
|
|
}
|
|
|
|
target[prop] = value
|
|
return true
|
|
},
|
|
|
|
deleteProperty(target, prop) {
|
|
const field = String(prop)
|
|
|
|
// Deletion counts as a write
|
|
if (access.writes === '*' || access.writes?.includes(field)) {
|
|
delete target[prop]
|
|
return true
|
|
}
|
|
|
|
throw new Error(
|
|
`Augmentation '${augmentation.name}' cannot delete field '${field}'`
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Validate that an augmentation's actual behavior matches its contract
|
|
* Used in testing to verify contracts are accurate
|
|
*/
|
|
static async validateContract(
|
|
augmentation: BrainyAugmentation,
|
|
testMetadata: any = { test: 'data', _brainy: { deleted: false } }
|
|
): Promise<{ valid: boolean; violations: string[] }> {
|
|
const violations: string[] = []
|
|
|
|
// Test read access
|
|
try {
|
|
const readable = this.enforce(augmentation, testMetadata, 'read')
|
|
|
|
if (augmentation.metadata === 'none' && readable !== null) {
|
|
violations.push(`Contract says 'none' but got readable metadata`)
|
|
}
|
|
} catch (error) {
|
|
violations.push(`Read enforcement error: ${error}`)
|
|
}
|
|
|
|
// Test write access
|
|
try {
|
|
const writable = this.enforce(augmentation, testMetadata, 'write')
|
|
|
|
if (augmentation.metadata === 'none') {
|
|
violations.push(`Contract says 'none' but got writable metadata`)
|
|
}
|
|
|
|
if (augmentation.metadata === 'readonly') {
|
|
// Try to write - should fail
|
|
try {
|
|
writable.testWrite = 'value'
|
|
violations.push(`Contract says 'readonly' but write succeeded`)
|
|
} catch {
|
|
// Expected to fail
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Expected for 'none' and 'readonly' on write
|
|
if (augmentation.metadata !== 'none' && augmentation.metadata !== 'readonly') {
|
|
violations.push(`Write enforcement error: ${error}`)
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: violations.length === 0,
|
|
violations
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
function deepClone(obj: any): any {
|
|
if (obj === null || typeof obj !== 'object') return obj
|
|
if (obj instanceof Date) return new Date(obj)
|
|
if (obj instanceof Array) return obj.map(item => deepClone(item))
|
|
|
|
const cloned: any = {}
|
|
for (const key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
cloned[key] = deepClone(obj[key])
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
function deepFreeze(obj: any): any {
|
|
Object.freeze(obj)
|
|
|
|
Object.getOwnPropertyNames(obj).forEach(prop => {
|
|
if (obj[prop] !== null &&
|
|
(typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
|
|
!Object.isFrozen(obj[prop])) {
|
|
deepFreeze(obj[prop])
|
|
}
|
|
})
|
|
|
|
return obj
|
|
} |