Part 2 of 5: Building a Zero-Trust Architecture that AI tools can't break
Welcome back to our AI security series! In Part 1, we saw how AI-generated code can create critical security vulnerabilities. Today, we're going to fix that with a revolutionary approach: Decorator Contract Programming.
๐ฏ The Core Problem
AI tools generate code that works, but they don't understand:
- Layer responsibilities (where should auth checks go?)
- Cross-cutting concerns (logging, validation, rate limiting)
- Business rules (who can access what?)
- Security boundaries (trust nothing, verify everything)
Traditional solutions fail because they expect AI to be smart about security. Spoiler alert: It's not.
๐ก The Breakthrough: Contracts as Code
Instead of hoping AI gets security right, we make security violations impossible to deploy.
The secret? Declarative contracts that tell the system exactly what must be true before, during, and after each function call.
@contract({
requires: [auth('user'), validates(schema), owns('userId')],
ensures: [auditLog('action'), returns(outputSchema)]
})
async function dangerousFunction(input, context) {
// Even if AI writes terrible code here,
// the contract catches violations
}
๐๏ธ Zero-Trust Layer Architecture
Here's the architecture that changes everything:
โโโโโโโโโโโโโโโโโโโ
โ Presentation โ โ Basic auth (login status, session)
โโโโโโโโโโโโโโโโโโโค
โ Action โ โ Permissions (roles, rate limits, input validation)
โโโโโโโโโโโโโโโโโโโค
โ Business โ โ Ownership (resource access, business rules)
โโโโโโโโโโโโโโโโโโโค
โ Data โ โ Final defense (query-level security, audit)
โโโโโโโโโโโโโโโโโโโ
Key insight: Even if AI misses security in one layer, the other layers catch it.
๐ง Building Your First Contract
Let's start simple. Here's how to create a basic contract system:
Step 1: Define Contract Types
type ContractCondition = (input: any, context: any) => boolean | Promise<boolean>;
type ContractValidator = (input: unknown) => any;
interface ContractOptions {
requires?: Array<ContractCondition | ContractValidator>;
ensures?: Array<(output: any, input: any, context: any) => boolean>;
layer?: 'presentation' | 'action' | 'business' | 'data';
}
Step 2: Create the Contract Decorator
function contract(options: ContractOptions) {
return function(target: any, propertyName: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const [input, context] = args;
try {
// Pre-conditions (requires)
let validatedInput = input;
for (const condition of options.requires || []) {
if (typeof condition === 'function' && condition.length === 1) {
// Validator - transforms input
validatedInput = condition(validatedInput);
} else {
// Condition check
await condition(validatedInput, context);
}
}
// Execute original method
const result = await originalMethod.call(this, validatedInput, context);
// Post-conditions (ensures)
for (const condition of options.ensures || []) {
await condition(result, validatedInput, context);
}
return result;
} catch (error) {
throw new ContractViolationError(
`${target.constructor.name}.${propertyName}`,
options.layer || 'unknown',
error
);
}
};
};
}
Step 3: Build Contract Conditions
// Authentication check
function auth(requiredRole?: string) {
return async (input: any, context: AuthContext): Promise<boolean> => {
if (!context.user) {
throw new ContractError('AUTHENTICATION_REQUIRED', 'User must be logged in');
}
if (!context.session || new Date(context.session.expiresAt) < new Date()) {
throw new ContractError('SESSION_EXPIRED', 'Session has expired');
}
if (requiredRole && !context.user.roles.includes(requiredRole)) {
throw new ContractError('INSUFFICIENT_ROLE',
`Required role: ${requiredRole}`);
}
return true;
};
}
// Ownership validation
function owns(resourceIdField: string) {
return async (input: any, context: AuthContext): Promise<boolean> => {
const resourceId = input[resourceIdField];
if (!resourceId) {
throw new ContractError('MISSING_RESOURCE_ID',
`Field ${resourceIdField} is required`);
}
// Admin users can access all resources
if (context.user.roles.includes('admin')) {
return true;
}
// Check resource ownership
const resource = await getResourceById(resourceId);
if (!resource || resource.userId !== context.user.id) {
throw new ContractError('OWNERSHIP_DENIED',
`User ${context.user.id} does not own resource ${resourceId}`);
}
return true;
};
}
// Input validation
function validates(schema: z.ZodSchema) {
return (input: unknown): any => {
try {
return schema.parse(input);
} catch (error) {
if (error instanceof z.ZodError) {
const messages = error.issues.map(issue =>
`${issue.path.join('.')}: ${issue.message}`
).join(', ');
throw new ContractError('VALIDATION_FAILED',
`Input validation failed: ${messages}`);
}
throw error;
}
};
}
Step 4: Handle Contract Violations
class ContractError extends Error {
constructor(public type: string, message: string) {
super(message);
this.name = 'ContractError';
}
}
class ContractViolationError extends Error {
constructor(
public contractName: string,
public layer: string,
public originalError: any
) {
super(`Contract violation in ${layer}.${contractName}: ${originalError.message}`);
this.name = 'ContractViolationError';
}
getAppropriateResponse() {
switch (this.layer) {
case 'presentation':
return { redirect: '/login', error: 'Authentication required' };
case 'action':
return { success: false, error: this.originalError.message };
case 'business':
return { success: false, error: 'Permission denied' };
case 'data':
return { success: false, error: 'Operation failed' };
default:
return { success: false, error: 'An error occurred' };
}
}
}
๐ช See It In Action
Now let's transform that dangerous AI-generated function:
Before (AI-generated, vulnerable):
export async function updateUserProfile(formData: FormData) {
const userId = formData.get('userId') as string;
const email = formData.get('email') as string;
const name = formData.get('name') as string;
const updatedUser = await db.user.update({
where: { id: userId },
data: { email, name }
});
return { success: true, user: updatedUser };
}
After (contract-protected):
const userUpdateSchema = z.object({
userId: z.string().uuid(),
email: z.string().email().optional(),
name: z.string().min(1).max(100).optional()
});
@contract({
requires: [
auth('user'), // Must be authenticated
validates(userUpdateSchema), // Input validation
owns('userId') // Must own the profile
],
ensures: [
auditLog('profile_update') // Log the action
],
layer: 'action'
})
export async function updateUserProfile(
input: UserUpdateInput,
context: AuthContext
): Promise<User> {
return userService.updateUser(input, context);
}
๐ The Magic
What happens when someone tries to hack this?
-
No auth token? โ Contract throws
AUTHENTICATION_REQUIRED
-
Invalid email format? โ Contract throws
VALIDATION_FAILED
-
Trying to update someone else's profile? โ Contract throws
OWNERSHIP_DENIED
- Everything succeeds? โ Action is automatically logged for audit
The AI doesn't need to know about security. The contract handles everything.
๐ฏ Why This Works With AI
AI tools love this approach because:
- Declarative: Clear requirements, no ambiguity
- Copy-pasteable: Perfect patterns to replicate
- Composable: Mix and match contract pieces
- Fail-safe: Violations are caught automatically
Instead of teaching AI complex security concepts, we give it simple, repeatable patterns.
๐ฎ Coming Up
In Part 3, we'll build a complete Next.js user management system using these contracts. You'll see how to:
- Structure your layers properly
- Handle different types of operations
- Integrate with Next.js Server Actions
- Make it production-ready
Questions or ideas? Drop them in the comments! I love seeing how you adapt these patterns.
Don't miss Part 3: We're building a real-world system that AI tools can contribute to safely.
Top comments (0)