Building Trust Through Audit-First Architecture in Smart Lock SDKs
Introduction
In security-critical systems like smart lock management, trust isn't just built through encryption and access controls—it's fundamentally established through comprehensive audit trails and defensive programming patterns. Recent developments in the UnlockOS SDK demonstrate how audit-first architecture can create unshakeable confidence in physical access control systems.
Audit-First Lookup Patterns
The cornerstone of trustworthy access control is ensuring every operation is traceable. The audit-first lookup pattern prioritizes logging before data retrieval:
interface AuditFirstLookup<T> {
performLookup(criteria: LookupCriteria): Promise<{
auditEntry: AuditLogEntry;
result: T | null;
}>;
}
// Implementation ensures audit logging happens first
async function auditFirstGuestLookup(profileId: string) {
// Audit the lookup attempt before accessing data
const auditEntry = await createAuditEntry({
action: 'GUEST_PROFILE_LOOKUP',
resourceId: profileId,
timestamp: new Date().toISOString(),
context: { source: 'identity_federation' }
});
try {
const profile = await fetchGuestProfile(profileId);
await updateAuditEntry(auditEntry.id, { status: 'SUCCESS' });
return { auditEntry, result: profile };
} catch (error) {
await updateAuditEntry(auditEntry.id, {
status: 'FAILED',
error: sanitizeError(error)
});
throw error;
}
}This pattern ensures that even failed operations leave an audit trail, crucial for security investigations and compliance.
Concurrent-Safe Identity Federation
Smart lock systems often deal with concurrent access attempts. The ensureUnlockPass helper demonstrates how to handle identity federation safely:
interface UnlockPassEnsureResult {
unlockPass: UnlockPass;
wasCreated: boolean;
auditTrail: AuditLogEntry[];
}
async function ensureUnlockPass(
guestProfile: GuestProfile,
organizationId: string
): Promise<UnlockPassEnsureResult> {
const auditTrail: AuditLogEntry[] = [];
// Use database-level constraints for concurrency safety
try {
const result = await db.transaction(async (tx) => {
// Attempt insert with ON CONFLICT handling
const insertResult = await tx
.insert(unlockPasses)
.values({
guestProfileId: guestProfile.id,
organizationId,
createdAt: new Date(),
status: 'active'
})
.onConflict([
unlockPasses.guestProfileId,
unlockPasses.organizationId
])
.doUpdate({
set: { lastAccessedAt: new Date() }
})
.returning();
// Log the operation
const auditEntry = await tx.insert(guestProfileAuditLog)
.values({
guestProfileId: guestProfile.id,
action: insertResult.length > 0 ? 'CREATED' : 'ACCESSED',
organizationId,
metadata: { concurrencySafe: true }
});
auditTrail.push(auditEntry);
return insertResult;
});
return {
unlockPass: result[0],
wasCreated: result.length > 0,
auditTrail
};
} catch (error) {
// Ensure failed attempts are also audited
await logFailedUnlockPassCreation(guestProfile.id, organizationId, error);
throw new SecurityError('Failed to ensure unlock pass', { cause: error });
}
}Defensive Authorization Checks
Every operation that can affect physical access must include robust authorization validation:
interface AuthorizationContext {
userId: string;
organizationId: string;
facilityId?: string;
requiredScopes: string[];
}
async function withAuthorizationCheck<T>(
context: AuthorizationContext,
operation: () => Promise<T>
): Promise<T> {
// Multi-layer authorization validation
const authResult = await validateAuthorization({
userId: context.userId,
organizationId: context.organizationId,
facilityId: context.facilityId,
requiredScopes: context.requiredScopes
});
if (!authResult.authorized) {
// Log unauthorized access attempt
await logSecurityEvent({
type: 'UNAUTHORIZED_ACCESS_ATTEMPT',
userId: context.userId,
organizationId: context.organizationId,
deniedScopes: authResult.missingScopes,
timestamp: new Date().toISOString()
});
throw new AuthorizationError(
`Insufficient permissions: missing ${authResult.missingScopes.join(', ')}`
);
}
// Log authorized operation start
const operationId = generateOperationId();
await logSecurityEvent({
type: 'AUTHORIZED_OPERATION_START',
operationId,
userId: context.userId,
organizationId: context.organizationId,
scopes: context.requiredScopes
});
try {
const result = await operation();
// Log successful completion
await logSecurityEvent({
type: 'AUTHORIZED_OPERATION_COMPLETE',
operationId,
status: 'SUCCESS'
});
return result;
} catch (error) {
// Log operation failure
await logSecurityEvent({
type: 'AUTHORIZED_OPERATION_FAILED',
operationId,
error: sanitizeError(error)
});
throw error;
}
}Idempotent State Management
Smart lock operations must be idempotent to handle network failures and retry scenarios safely:
interface IdempotentOperation<T> {
key: string;
operation: () => Promise<T>;
isIdempotent: (existing: T, new: T) => boolean;
}
async function performIdempotentCheckin(
membershipId: string,
facilityId: string
): Promise<CheckinResult> {
const idempotencyKey = `checkin:${membershipId}:${facilityId}`;
// Check for existing active session
const existingSession = await db
.select()
.from(checkinSessions)
.where(
and(
eq(checkinSessions.membershipId, membershipId),
eq(checkinSessions.facilityId, facilityId),
eq(checkinSessions.status, 'checked_in')
)
)
.limit(1);
if (existingSession.length > 0) {
// Idempotent response for active session
await logAuditEvent({
type: 'IDEMPOTENT_CHECKIN_RETURN',
sessionId: existingSession[0].id,
membershipId,
facilityId
});
return {
session: existingSession[0],
wasCreated: false,
idempotent: true
};
}
// Create new session with partial unique index protection
try {
const newSession = await db.transaction(async (tx) => {
const session = await tx
.insert(checkinSessions)
.values({
membershipId,
facilityId,
status: 'checked_in',
checkedInAt: new Date()
})
.returning();
// Audit successful creation
await tx.insert(auditLog).values({
entityType: 'checkin_session',
entityId: session[0].id,
action: 'CREATED',
userId: membershipId,
metadata: { idempotencyKey }
});
return session[0];
});
return {
session: newSession,
wasCreated: true,
idempotent: false
};
} catch (error) {
if (isUniqueConstraintViolation(error)) {
// Handle race condition - return existing session
const raceConditionSession = await findExistingSession(
membershipId,
facilityId
);
return {
session: raceConditionSession,
wasCreated: false,
idempotent: true
};
}
throw error;
}
}Error Recovery with Audit Integrity
Even when operations fail, the audit trail must remain intact:
async function handleKeyRevocationWithRecovery(
keyId: string,
userId: string,
reason: string
): Promise<KeyRevocationResult> {
let auditEntryId: string | null = null;
try {
// Always record revocation timestamp first
auditEntryId = await recordRevocationAttempt({
keyId,
userId,
reason,
timestamp: new Date(),
status: 'ATTEMPTED'
});
// Attempt the actual revocation
const revocationResult = await performKeyRevocation(keyId);
// Update audit entry with success
await updateAuditEntry(auditEntryId, {
status: 'SUCCESS',
completedAt: new Date(),
revocationDetails: revocationResult
});
return { success: true, auditEntryId };
} catch (error) {
// Even if revocation fails, record the timestamp
// This is crucial for security - we know a revocation was attempted
if (auditEntryId) {
await updateAuditEntry(auditEntryId, {
status: 'FAILED',
failedAt: new Date(),
error: sanitizeError(error),
// Still record entry_key_revoked_at for safety
revokedAt: new Date()
});
}
// Fail safely - assume key might be compromised
await markKeyAsSuspicious(keyId, 'REVOCATION_FAILED');
throw new SecurityError(
'Key revocation failed but marked as suspicious',
{ keyId, originalError: error }
);
}
}Summary
Building trust in security-critical systems requires more than just implementing security features—it demands an architecture that prioritizes auditability, handles failures gracefully, and maintains data integrity even under adverse conditions. The patterns demonstrated here show how audit-first design, concurrent-safe operations, comprehensive authorization checks, idempotent state management, and robust error recovery create a foundation of trust that users and auditors can rely on.
By implementing these patterns, smart lock SDKs can provide not just secure access control, but verifiable secure access control—a crucial distinction when physical safety and security are at stake.