This commit is contained in:
2026-01-22 02:00:16 +00:00
parent 57cbf12a5a
commit 43f5021ccb
10 changed files with 1205 additions and 50 deletions

View File

@@ -0,0 +1,375 @@
# Code Review Findings Report - MetaBuilder Codebase
**Date**: 2026-01-22
**Reviewer**: Automated Code Review (Claude Opus 4.5)
**Scope**: Critical C++ components (Game Engine, DBAL, HTTP Daemon)
**Completion Promise**: All issues fixed, good logging all round
---
## Executive Summary
| Severity | Count | Status |
|----------|-------|--------|
| CRITICAL | 1 | Requires immediate fix |
| HIGH | 3 | Requires fix before production |
| MEDIUM | 2 | Should be addressed |
| LOW | 4 | Informational/improvements |
---
## CRITICAL Issues
### [CRIT-001] Plain-text Password Comparison in verify_credential.hpp
**File**: `dbal/production/src/entities/credential/crud/verify_credential.hpp:18`
**Category**: SECURITY
**Impact**: Authentication bypass, credential theft
**Description**:
The credential verification function compares passwords directly without hashing:
```cpp
if (!credential || credential->passwordHash != password) {
return Error::unauthorized("Invalid credentials");
}
```
**Issue**: Despite the field being named `passwordHash`, it's compared directly to the input `password`, suggesting either:
1. Passwords are stored in plain text (catastrophic security issue)
2. The caller is expected to hash before calling, but the API name doesn't indicate this
**Required Fix**:
```cpp
// Use constant-time comparison with proper hashing
#include <openssl/sha.h>
#include <cstring>
inline bool secureCompare(const std::string& a, const std::string& b) {
if (a.size() != b.size()) return false;
volatile int result = 0;
for (size_t i = 0; i < a.size(); ++i) {
result |= static_cast<unsigned char>(a[i]) ^ static_cast<unsigned char>(b[i]);
}
return result == 0;
}
inline Result<bool> verify(InMemoryStore& store, const std::string& username, const std::string& password) {
if (username.empty() || password.empty()) {
return Error::validationError("username and password are required");
}
auto* credential = helpers::getCredential(store, username);
if (!credential) {
// Perform dummy hash to prevent timing attacks
computeHash(password, "dummy_salt");
return Error::unauthorized("Invalid credentials");
}
// Hash the input password and compare
const std::string inputHash = computeHash(password, credential->salt);
if (!secureCompare(inputHash, credential->passwordHash)) {
return Error::unauthorized("Invalid credentials");
}
return Result<bool>(true);
}
```
---
## HIGH Issues
### [HIGH-001] Missing Path Traversal Prevention
**File**: `dbal/production/src/daemon/server/validation_internal/validate_request_path.hpp`
**Category**: SECURITY
**Impact**: Directory traversal attacks
**Description**:
The path validation checks for null bytes and length but does NOT check for path traversal sequences:
```cpp
inline bool validate_request_path(const std::string& path, HttpResponse& error_response) {
// Check for null bytes in path (CVE pattern)
if (path.find('\0') != std::string::npos) { ... }
// Validate path length
if (path.length() > MAX_PATH_LENGTH) { ... }
return true; // MISSING: Path traversal check!
}
```
**Required Fix**:
```cpp
inline bool validate_request_path(const std::string& path, HttpResponse& error_response) {
// Existing checks...
// Check for path traversal sequences
if (path.find("..") != std::string::npos) {
error_response.status_code = 400;
error_response.status_text = "Bad Request";
error_response.body = R"({"error":"Path traversal detected"})";
return false;
}
// Check for encoded traversal attempts
if (path.find("%2e%2e") != std::string::npos ||
path.find("%2E%2E") != std::string::npos ||
path.find("..%2f") != std::string::npos ||
path.find("..%5c") != std::string::npos) {
error_response.status_code = 400;
error_response.status_text = "Bad Request";
error_response.body = R"({"error":"Encoded path traversal detected"})";
return false;
}
return true;
}
```
---
### [HIGH-002] Username Validation Minimum Length Not Enforced
**File**: `dbal/production/src/validation/entity/user_validation.hpp:25-28`
**Category**: SECURITY/VALIDATION
**Impact**: Potential for weak usernames (single character)
**Description**:
```cpp
inline bool isValidUsername(const std::string& username) {
if (username.empty() || username.length() > 50) return false; // No minimum length!
static const std::regex username_pattern(R"([a-zA-Z0-9_-]+)");
return std::regex_match(username, username_pattern);
}
```
**Required Fix**:
```cpp
inline bool isValidUsername(const std::string& username) {
if (username.length() < 3 || username.length() > 50) return false; // Enforce 3-50 chars
static const std::regex username_pattern(R"([a-zA-Z0-9_-]+)");
return std::regex_match(username, username_pattern);
}
```
---
### [HIGH-003] Credential Password Validation Too Weak
**File**: `dbal/production/src/validation/entity/credential_validation.hpp:11-18`
**Category**: SECURITY
**Impact**: Allows passwords that are only whitespace or single characters
**Description**:
```cpp
inline bool isValidCredentialPassword(const std::string& hash) {
if (hash.empty()) {
return false;
}
return std::any_of(hash.begin(), hash.end(), [](unsigned char c) {
return !std::isspace(c); // Only checks for at least one non-whitespace
});
}
```
**Required Fix**:
```cpp
inline bool isValidCredentialPassword(const std::string& password) {
// Minimum length
if (password.length() < 8) {
return false;
}
// Maximum length (prevent DoS on hashing)
if (password.length() > 128) {
return false;
}
// At least one non-whitespace character
return std::any_of(password.begin(), password.end(), [](unsigned char c) {
return !std::isspace(c);
});
}
```
---
## MEDIUM Issues
### [MED-001] Regex Pattern Compilation on Every Call
**File**: `dbal/production/src/validation/entity/user_validation.hpp:18`
**Category**: PERFORMANCE
**Impact**: Unnecessary regex compilation overhead
**Description**:
While `static` is used for regex patterns, the `std::regex_match` call is still relatively expensive for a hot validation path.
**Recommended**: Consider caching compiled regex at module level or using a simpler character-class validation.
---
### [MED-002] Missing HTTP Method Validation
**File**: `dbal/production/src/daemon/server/parsing/parse_request_line.hpp:23-39`
**Category**: SECURITY
**Impact**: Potential for unexpected HTTP method handling
**Description**:
The request line parser accepts any string as the HTTP method without validating against known methods.
**Recommended Fix**:
```cpp
inline bool is_valid_http_method(const std::string& method) {
static const std::unordered_set<std::string> valid_methods = {
"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"
};
return valid_methods.count(method) > 0;
}
inline bool parse_request_line(...) {
// ... existing parsing ...
if (!is_valid_http_method(request.method)) {
error_response.status_code = 405;
error_response.status_text = "Method Not Allowed";
error_response.body = R"({"error":"Unknown HTTP method"})";
return false;
}
return true;
}
```
---
## LOW Issues (Informational)
### [LOW-001] Excellent Security Limits Configuration
**File**: `dbal/production/src/daemon/http/server/security_limits.hpp`
**Status**: GOOD
**Note**: Well-configured security limits:
- 64KB max request size (prevents buffer overflow)
- 100 max headers (prevents header bomb)
- 8KB max header size
- 2KB max path length
- 10MB max body size
- 1000 max concurrent connections
### [LOW-002] Excellent Null Byte and CRLF Injection Prevention
**File**: `dbal/production/src/daemon/server/validation_internal/validate_header.hpp`
**Status**: GOOD
**Note**: Proper validation for:
- Null byte injection in headers and paths
- CRLF injection (HTTP Response Splitting) prevention
- Header count and size limits
### [LOW-003] SQL Injection Prevention Confirmed
**File**: `dbal/production/src/adapters/sql/sql_adapter.hpp`
**Status**: GOOD
**Note**: Proper parameterized queries using `placeholder(N)` pattern with dialect-aware syntax.
### [LOW-004] Excellent Game Engine Initialization Order
**File**: `gameengine/src/services/impl/graphics/bgfx_graphics_backend.cpp:612-622`
**Status**: GOOD - CRITICAL FIX ALREADY APPLIED
**Note**: The "prime frame" pattern ensures bgfx render thread synchronization before resource creation:
```cpp
if (logger_) {
logger_->Trace("BgfxGraphicsBackend", "Initialize",
"Priming bgfx with initial frame before resource creation");
}
const uint32_t frameNumber = bgfx::frame();
frameCount_ = frameNumber + 1;
```
---
## Logging Quality Assessment
### Game Engine Logging: EXCELLENT
- Comprehensive trace logging throughout initialization
- Error messages include context (renderer type, platform info)
- Fallback renderer logging with recommendations
- Shader compilation logging with binary analysis
### DBAL Logging: NEEDS IMPROVEMENT
- Missing structured logging in credential operations
- No audit trail for security-sensitive operations
- HTTP daemon could log more connection metadata
### Recommended Logging Improvements:
```cpp
// In verify_credential.hpp
inline Result<bool> verify(InMemoryStore& store, const std::string& username, const std::string& password, ILogger* logger = nullptr) {
if (logger) {
logger->Info("Credential verification attempt for user: " + username);
}
// ... verification logic ...
if (!credential) {
if (logger) {
logger->Warn("Credential verification failed: user not found - " + username);
}
return Error::unauthorized("Invalid credentials");
}
// ... hash comparison ...
if (logger) {
logger->Info("Credential verification succeeded for user: " + username);
}
return Result<bool>(true);
}
```
---
## Summary of Required Actions
### Immediate (Before Next Release)
1. **[CRIT-001]** Fix plain-text password comparison - implement proper hashing
2. **[HIGH-001]** Add path traversal prevention to `validate_request_path.hpp`
3. **[HIGH-002]** Add minimum length (3 chars) to username validation
4. **[HIGH-003]** Strengthen password validation requirements
### Near-Term (Within 2 Weeks)
5. **[MED-001]** Optimize regex usage in hot paths
6. **[MED-002]** Add HTTP method whitelist validation
7. Add structured logging to DBAL credential operations
8. Add audit trail for security-sensitive operations
### Verification Steps
After fixes are applied:
1. Run `npm run test:e2e` to verify no regressions
2. Run static analysis: `clang-tidy` on modified files
3. Run security tests for path traversal and credential operations
4. Verify logging output for credential operations
---
## Files Reviewed
| File | Status | Issues Found |
|------|--------|--------------|
| `gameengine/CMakeLists.txt` | ✅ GOOD | None |
| `gameengine/src/app/service_based_app.cpp` | ✅ GOOD | None |
| `gameengine/src/services/impl/graphics/bgfx_graphics_backend.cpp` | ✅ GOOD | Excellent init order fix |
| `gameengine/src/services/impl/graphics/bgfx_shader_compiler.cpp` | ✅ GOOD | Integer uniform fix applied |
| `gameengine/src/services/impl/shader/shader_pipeline_validator.cpp` | ✅ GOOD | None |
| `dbal/production/src/daemon/http/server/security_limits.hpp` | ✅ GOOD | None |
| `dbal/production/src/daemon/server/validation_internal/validate_request_path.hpp` | ⚠️ HIGH | Missing path traversal check |
| `dbal/production/src/daemon/server/validation_internal/validate_header.hpp` | ✅ GOOD | None |
| `dbal/production/src/entities/credential/crud/verify_credential.hpp` | 🔴 CRIT | Plain-text password comparison |
| `dbal/production/src/entities/credential/crud/set_credential.hpp` | ✅ GOOD | Uses validation |
| `dbal/production/src/validation/entity/user_validation.hpp` | ⚠️ HIGH | Missing min length |
| `dbal/production/src/validation/entity/credential_validation.hpp` | ⚠️ HIGH | Weak validation |
| `dbal/production/src/adapters/sql/sql_adapter.hpp` | ✅ GOOD | Proper parameterization |
| `dbal/production/src/daemon/server/parsing/parse_request_line.hpp` | ⚠️ MED | No method whitelist |
---
**Report Generated**: 2026-01-22
**Review Status**: In Progress - Fixes Required

View File

@@ -310,17 +310,8 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::ILogger>());
registry_.RegisterService<services::IFrameWorkflowService, services::impl::FrameWorkflowService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::IAudioService>(),
registry_.GetService<services::IInputService>(),
registry_.GetService<services::IMeshService>(),
registry_.GetService<services::IPhysicsService>(),
registry_.GetService<services::ISceneService>(),
registry_.GetService<services::IRenderCoordinatorService>(),
registry_.GetService<services::IValidationTourService>(),
registry_.GetService<services::ISoundboardStateService>());
// NOTE: FrameWorkflowService registration moved below after all dependencies are registered
// (IPhysicsService, ISceneService, IRenderCoordinatorService)
// Physics bridge services
registry_.RegisterService<services::IPhysicsBridgeService, services::impl::PhysicsBridgeService>(
@@ -376,6 +367,19 @@ void ServiceBasedApp::RegisterServices() {
registry_.GetService<services::ISceneService>(),
registry_.GetService<services::IValidationTourService>());
// Frame workflow service (registered after all dependencies: physics, scene, render coordinator)
registry_.RegisterService<services::IFrameWorkflowService, services::impl::FrameWorkflowService>(
registry_.GetService<services::ILogger>(),
registry_.GetService<services::IConfigService>(),
registry_.GetService<services::IAudioService>(),
registry_.GetService<services::IInputService>(),
registry_.GetService<services::IMeshService>(),
registry_.GetService<services::IPhysicsService>(),
registry_.GetService<services::ISceneService>(),
registry_.GetService<services::IRenderCoordinatorService>(),
registry_.GetService<services::IValidationTourService>(),
registry_.GetService<services::ISoundboardStateService>());
// Application loop service
registry_.RegisterService<services::IApplicationLoopService, services::impl::ApplicationLoopService>(
registry_.GetService<services::ILogger>(),

View File

@@ -3,6 +3,7 @@
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
@@ -473,8 +474,10 @@ bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
"falling back to temp-file compilation for " + label);
}
// Fallback to temp-file + pipelineCompiler_/executable flow
std::string tempInputPath = "/tmp/" + label + (isVertex ? ".vert.glsl" : ".frag.glsl");
std::string tempOutputPath = "/tmp/" + label + (isVertex ? ".vert.bin" : ".frag.bin");
// Use std::filesystem::temp_directory_path() for cross-platform compatibility
const std::filesystem::path tempDir = std::filesystem::temp_directory_path();
const std::string tempInputPath = (tempDir / (label + (isVertex ? ".vert.glsl" : ".frag.glsl"))).string();
const std::string tempOutputPath = (tempDir / (label + (isVertex ? ".vert.bin" : ".frag.bin"))).string();
{
std::ofstream ofs(tempInputPath);
ofs << source;
@@ -485,10 +488,15 @@ bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
std::vector<std::string> args = {"--type", isVertex ? "vertex" : "fragment", "--profile", "spirv"};
ok = pipelineCompiler_->Compile(tempInputPath, tempOutputPath, args);
} else {
std::string cmd = "./src/bgfx_tools/shaderc/shaderc -f " + tempInputPath + " -o " + tempOutputPath;
if (logger_) logger_->Trace("BgfxShaderCompiler", "CompileShaderCmd", cmd);
int rc = std::system(cmd.c_str());
ok = (rc == 0);
// Security: Avoid std::system() which is vulnerable to command injection.
// Pipeline compiler service must be available for fallback compilation.
if (logger_) {
logger_->Error("BgfxShaderCompiler: Pipeline compiler service not available for " + label +
" - cannot compile shader without in-memory compilation support");
}
// Cleanup temp input file before returning
std::filesystem::remove(tempInputPath);
return BGFX_INVALID_HANDLE;
}
if (!ok) {
@@ -514,9 +522,9 @@ bgfx::ShaderHandle BgfxShaderCompiler::CompileShader(
if (logger_) logger_->Error("BgfxShaderCompiler: Failed to read compiled shader data");
return BGFX_INVALID_HANDLE;
}
// cleanup temp files
remove(tempInputPath.c_str());
remove(tempOutputPath.c_str());
// cleanup temp files using std::filesystem for cross-platform compatibility
std::filesystem::remove(tempInputPath);
std::filesystem::remove(tempOutputPath);
}
if (!shaderInfo.has_value()) {

View File

@@ -8,8 +8,18 @@ std::vector<ShaderPipelineValidator::AttributeInfo>
ShaderPipelineValidator::ExtractShaderInputs(const std::string& glslSource) const {
std::vector<AttributeInfo> inputs;
// Match: layout (location = N) in type name; (handles compact syntax too)
std::regex pattern(R"(layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+(\w+)\s+(\w+)\s*;)");
// Improved pattern to handle:
// - Optional qualifiers (flat, smooth, noperspective)
// - Multiple layout params separated by commas
// - Array types like vec3[N]
// - Whitespace variations
// Pattern: layout([...] location = N [...]) [qualifier] in type name [array]?;
std::regex pattern(
R"(layout\s*\([^)]*location\s*=\s*(\d+)[^)]*\)\s*)" // layout(... location=N ...)
R"((?:flat\s+|smooth\s+|noperspective\s+)?)" // optional interpolation qualifier
R"(in\s+(\w+)\s+(\w+))" // in type name
R"((?:\s*\[\s*\d+\s*\])?\s*;)" // optional array index, semicolon
);
std::sregex_iterator begin(glslSource.begin(), glslSource.end(), pattern);
std::sregex_iterator end;
@@ -21,8 +31,6 @@ ShaderPipelineValidator::ExtractShaderInputs(const std::string& glslSource) cons
size_t size = GetGlslTypeSize(type);
inputs.emplace_back(location, type, name, size);
// Trace logging disabled to avoid API mismatch
}
return inputs;
@@ -32,8 +40,18 @@ std::vector<ShaderPipelineValidator::AttributeInfo>
ShaderPipelineValidator::ExtractShaderOutputs(const std::string& glslSource) const {
std::vector<AttributeInfo> outputs;
// Match: layout (location = N) out type name; (handles compact syntax too)
std::regex pattern(R"(layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*out\s+(\w+)\s+(\w+)\s*;)");
// Improved pattern to handle:
// - Optional qualifiers (flat, smooth, noperspective)
// - Multiple layout params separated by commas
// - Array types like vec4[N]
// - Whitespace variations
// Pattern: layout([...] location = N [...]) [qualifier] out type name [array]?;
std::regex pattern(
R"(layout\s*\([^)]*location\s*=\s*(\d+)[^)]*\)\s*)" // layout(... location=N ...)
R"((?:flat\s+|smooth\s+|noperspective\s+)?)" // optional interpolation qualifier
R"(out\s+(\w+)\s+(\w+))" // out type name
R"((?:\s*\[\s*\d+\s*\])?\s*;)" // optional array index, semicolon
);
std::sregex_iterator begin(glslSource.begin(), glslSource.end(), pattern);
std::sregex_iterator end;
@@ -180,8 +198,13 @@ ShaderPipelineValidator::ValidateSpirvRequirements(
ValidationResult result;
// Check that all in/out variables have layout(location=N)
std::regex inOutPattern(R"(\b(in|out)\s+\w+\s+\w+\s*;)");
std::regex layoutPattern(R"(layout\s*\()");
// Improved patterns to handle qualifiers and array syntax
std::regex inOutPattern(
R"(\b(?:flat\s+|smooth\s+|noperspective\s+)?)" // optional interpolation
R"((in|out)\s+\w+\s+\w+)" // in/out type name
R"((?:\s*\[\s*\d+\s*\])?\s*;)" // optional array, semicolon
);
std::regex layoutPattern(R"(layout\s*\([^)]*location\s*=)");
size_t lineNum = 1;
std::istringstream stream(glslSource);

View File

@@ -0,0 +1,9 @@
[requires]
sdl/3.2.20
[generators]
CMakeDeps
CMakeToolchain
[options]
sdl/*:shared=True

View File

@@ -9,8 +9,8 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy application files
COPY backend/app.py backend/auth.py backend/config_db.py backend/auth_sqlalchemy.py backend/config_db_sqlalchemy.py backend/models.py backend/rocksdb_store.py ./
# Copy schema from parent directory
COPY schema.json /app/schema.json
# Copy schema to where app.py expects it (parent of /app = /)
COPY schema.json /schema.json
# Create data directory
RUN mkdir -p /data/blobs /data/meta

View File

@@ -15,6 +15,9 @@ COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
# Set backend URL for Next.js rewrites (Docker internal network)
ARG BACKEND_URL=http://backend:5000
ENV BACKEND_URL=${BACKEND_URL}
# Ensure public directory exists (Next.js may not create it if no static assets)
RUN mkdir -p public

View File

@@ -6,6 +6,20 @@ import styles from './page.module.scss';
import { getOperationLabel, getOperationDescription, getOperationCategory, getCategoryColor } from '../../utils/operations';
import { getApiUrl } from '../../utils/api';
// Helper to safely parse JSON - handles both already-parsed objects and JSON strings
function safeParseJson(value, fallback = []) {
if (value === null || value === undefined) return fallback;
if (typeof value === 'object') return value; // Already parsed
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
return fallback;
}
export default function AdminPage() {
const router = useRouter();
const [user, setUser] = useState(null);
@@ -189,7 +203,7 @@ export default function AdminPage() {
<>
<div style={{ marginBottom: '16px' }}>
<strong>Protocols:</strong>{' '}
{JSON.parse(config.capabilities.protocols || '[]').map((p, i) => (
{safeParseJson(config.capabilities.protocols).map((p, i) => (
<span key={i} className={`${styles.badge} ${styles['badge--primary']}`}>
{p}
</span>
@@ -197,7 +211,7 @@ export default function AdminPage() {
</div>
<div style={{ marginBottom: '16px' }}>
<strong>Storage:</strong>{' '}
{JSON.parse(config.capabilities.storage || '[]').map((s, i) => (
{safeParseJson(config.capabilities.storage).map((s, i) => (
<span key={i} className={`${styles.badge} ${styles['badge--primary']}`}>
{s}
</span>
@@ -205,7 +219,7 @@ export default function AdminPage() {
</div>
<div>
<strong>Features:</strong>{' '}
{JSON.parse(config.capabilities.features || '[]').map((f, i) => (
{safeParseJson(config.capabilities.features).map((f, i) => (
<span key={i} className={`${styles.badge} ${styles['badge--success']}`}>
{f}
</span>
@@ -265,7 +279,7 @@ export default function AdminPage() {
<td><strong>{field.name}</strong></td>
<td>{field.type}</td>
<td>{field.optional ? '✓' : '✗'}</td>
<td>{JSON.parse(field.normalizations || '[]').join(', ') || 'none'}</td>
<td>{safeParseJson(field.normalizations).join(', ') || 'none'}</td>
</tr>
))}
</tbody>
@@ -411,7 +425,7 @@ export default function AdminPage() {
<div className={styles.section__content}>
{config.api_routes && config.api_routes.length > 0 ? (
config.api_routes.map((route, i) => {
const pipeline = JSON.parse(route.pipeline || '[]');
const pipeline = safeParseJson(route.pipeline);
const isExpanded = expandedRoute === i;
return (
@@ -426,7 +440,7 @@ export default function AdminPage() {
{' '}
<code>{route.path}</code>
{' • '}
{JSON.parse(route.tags || '[]').map((tag, j) => (
{safeParseJson(route.tags).map((tag, j) => (
<span key={j} className={`${styles.badge} ${styles['badge--success']}`}>
{tag}
</span>
@@ -568,7 +582,7 @@ export default function AdminPage() {
{config.auth_scopes.map((scope, i) => (
<tr key={i}>
<td><strong>{scope.name}</strong></td>
<td>{JSON.parse(scope.actions || '[]').join(', ')}</td>
<td>{safeParseJson(scope.actions).join(', ')}</td>
<td>
<button className={`${styles.button} ${styles['button--secondary']} ${styles['button--small']}`}>
Edit
@@ -616,8 +630,8 @@ export default function AdminPage() {
</div>
<div className={styles.codeBlock}>
<pre>{JSON.stringify({
conditions: JSON.parse(policy.conditions || '{}'),
requirements: JSON.parse(policy.requirements || '{}')
conditions: safeParseJson(policy.conditions, {}),
requirements: safeParseJson(policy.requirements, {})
}, null, 2)}</pre>
</div>
</div>

View File

@@ -33,7 +33,14 @@ export function getApiUrl() {
// For client-side, try to infer from current location
if (typeof window !== 'undefined') {
const { protocol, hostname, port } = window.location;
// If running on localhost with a custom port (e.g., 3003 from Docker),
// use relative URLs to go through Next.js rewrites/proxy
if ((hostname === 'localhost' || hostname === '127.0.0.1') && port && port !== '3000') {
// Docker compose or similar setup - use Next.js proxy
return '';
}
// If running on a deployed domain (not localhost), try intelligent defaults
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
// Pattern 1: Frontend on custom port (e.g., :3000) - try backend on :5000
@@ -42,14 +49,14 @@ export function getApiUrl() {
if (port && port !== '80' && port !== '443') {
return `${protocol}//${hostname}:5000`;
}
// Pattern 2: Same origin with Next.js rewrites
// Return empty string to use relative URLs, which will be handled by Next.js rewrites
// This works when backend routes are proxied through Next.js
return '';
}
// For localhost development, backend is typically on port 5000
// For localhost development on port 3000, backend is typically on port 5000
return 'http://localhost:5000';
}

View File

@@ -1,11 +1,11 @@
{
"schema_version": "1.0",
"schema_version": "1.1",
"type_id": "acme.declarative_repo_type",
"description": "Mega schema: declarative repository type with closed-world ops, routes, storage, indexing, caching, replication, GC, invariants, and static validation.",
"description": "Declarative package repository supporting generic artifacts, Docker/OCI images, Conan packages, and OS packages with dependencies, signing, and metadata.",
"capabilities": {
"protocols": ["http"],
"protocols": ["http", "oci"],
"storage": ["blob", "kv", "index"],
"features": ["proxy", "virtual", "replication", "gc", "audit", "immutability"]
"features": ["proxy", "virtual", "replication", "gc", "audit", "immutability", "signing", "dependencies", "oci"]
},
"entities": {
@@ -28,6 +28,70 @@
{ "field": "digest", "regex": "^(sha256:)?[a-f0-9]{64}$", "when_present": true }
]
},
"manifest": {
"description": "OCI/Docker manifest for container images",
"fields": {
"namespace": { "type": "string", "normalize": ["trim", "lower"] },
"name": { "type": "string", "normalize": ["trim", "lower", "replace:_:-"] },
"reference": { "type": "string", "normalize": ["trim"] },
"media_type": { "type": "string" },
"digest": { "type": "string", "normalize": ["trim", "lower"] }
},
"primary_key": ["namespace", "name", "reference"],
"constraints": [
{ "field": "namespace", "regex": "^[a-z0-9][a-z0-9._-]{0,127}$" },
{ "field": "name", "regex": "^[a-z0-9][a-z0-9._/-]{0,255}$" },
{ "field": "reference", "regex": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$|^sha256:[a-f0-9]{64}$" },
{ "field": "media_type", "regex": "^application/vnd\\.(oci|docker)\\..*$", "when_present": true },
{ "field": "digest", "regex": "^sha256:[a-f0-9]{64}$", "when_present": true }
]
},
"layer": {
"description": "OCI/Docker image layer blob",
"fields": {
"digest": { "type": "string", "normalize": ["trim", "lower"] },
"media_type": { "type": "string" },
"size": { "type": "integer" }
},
"primary_key": ["digest"],
"constraints": [
{ "field": "digest", "regex": "^sha256:[a-f0-9]{64}$" }
]
},
"dependency": {
"description": "Package dependency relationship",
"fields": {
"namespace": { "type": "string", "normalize": ["trim", "lower"] },
"name": { "type": "string", "normalize": ["trim", "lower"] },
"version": { "type": "string", "normalize": ["trim"] },
"variant": { "type": "string", "optional": true, "normalize": ["trim", "lower"] },
"dep_namespace": { "type": "string", "normalize": ["trim", "lower"] },
"dep_name": { "type": "string", "normalize": ["trim", "lower"] },
"dep_version_spec": { "type": "string" },
"dep_kind": { "type": "string", "optional": true }
},
"primary_key": ["namespace", "name", "version", "variant", "dep_namespace", "dep_name"],
"constraints": [
{ "field": "dep_kind", "regex": "^(build|runtime|test|dev|optional)$", "when_present": true }
]
},
"signature": {
"description": "Cryptographic signature for an artifact",
"fields": {
"namespace": { "type": "string", "normalize": ["trim", "lower"] },
"name": { "type": "string", "normalize": ["trim", "lower"] },
"version": { "type": "string", "normalize": ["trim"] },
"variant": { "type": "string", "optional": true, "normalize": ["trim", "lower"] },
"key_id": { "type": "string" },
"algo": { "type": "string" },
"signature": { "type": "string" }
},
"primary_key": ["namespace", "name", "version", "variant", "key_id"],
"constraints": [
{ "field": "algo", "regex": "^(ed25519|rsa-sha256|ecdsa-p256)$" },
{ "field": "key_id", "regex": "^[a-zA-Z0-9:_-]{1,128}$" }
]
},
"versioning": {
"scheme": "semver",
"ordering": "semver",
@@ -63,12 +127,32 @@
"artifact_meta": {
"store": "meta",
"key_template": "artifact/{namespace}/{name}/{version}/{variant?}",
"schema": "ArtifactMetaV1"
"schema": "ArtifactMetaV2"
},
"tag_map": {
"store": "meta",
"key_template": "tag/{namespace}/{name}/{tag}",
"schema": "TagMapV1"
},
"manifest": {
"store": "meta",
"key_template": "manifest/{namespace}/{name}/{reference}",
"schema": "ManifestV1"
},
"manifest_by_digest": {
"store": "meta",
"key_template": "manifest_digest/{namespace}/{name}/{digest}",
"schema": "ManifestV1"
},
"dependency_graph": {
"store": "meta",
"key_template": "deps/{namespace}/{name}/{version}/{variant?}",
"schema": "DependencyGraphV1"
},
"signature": {
"store": "meta",
"key_template": "sig/{namespace}/{name}/{version}/{variant?}/{key_id}",
"schema": "SignatureV1"
}
},
"schemas": {
@@ -94,6 +178,93 @@
}
}
},
"ArtifactMetaV2": {
"type": "object",
"required": ["namespace", "name", "version", "blob_digest", "created_at"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"version": { "type": "string" },
"variant": { "type": "string" },
"blob_digest": { "type": "string" },
"blob_size": { "type": "integer" },
"created_at": { "type": "string" },
"created_by": { "type": "string" },
"labels": { "type": "object" },
"integrity": {
"type": "object",
"properties": {
"algo": { "type": "string" },
"digest": { "type": "string" }
}
},
"package_info": {
"type": "object",
"properties": {
"description": { "type": "string" },
"license": { "type": "string" },
"homepage": { "type": "string" },
"repository": { "type": "string" },
"authors": { "type": "array", "items": { "type": "string" } },
"keywords": { "type": "array", "items": { "type": "string" } },
"package_type": { "type": "string", "enum": ["generic", "conan", "docker", "os", "npm", "pip"] }
}
},
"dependencies": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "version_spec"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"version_spec": { "type": "string" },
"kind": { "type": "string", "enum": ["build", "runtime", "test", "dev", "optional"] }
}
}
},
"signatures": {
"type": "array",
"items": {
"type": "object",
"required": ["key_id", "algo", "signature"],
"properties": {
"key_id": { "type": "string" },
"algo": { "type": "string" },
"signature": { "type": "string" },
"signed_at": { "type": "string" }
}
}
}
}
},
"ManifestV1": {
"type": "object",
"description": "OCI/Docker image manifest",
"required": ["namespace", "name", "reference", "media_type", "config_digest", "layers"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"reference": { "type": "string" },
"media_type": { "type": "string" },
"config_digest": { "type": "string" },
"config_size": { "type": "integer" },
"layers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"digest": { "type": "string" },
"size": { "type": "integer" },
"media_type": { "type": "string" }
}
}
},
"annotations": { "type": "object" },
"created_at": { "type": "string" },
"total_size": { "type": "integer" }
}
},
"TagMapV1": {
"type": "object",
"required": ["namespace", "name", "tag", "target_key", "updated_at"],
@@ -105,6 +276,32 @@
"updated_at": { "type": "string" },
"updated_by": { "type": "string" }
}
},
"DependencyGraphV1": {
"type": "object",
"description": "Pre-computed dependency graph for a package",
"required": ["namespace", "name", "version"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"version": { "type": "string" },
"variant": { "type": "string" },
"direct_deps": { "type": "array", "items": { "type": "string" } },
"transitive_deps": { "type": "array", "items": { "type": "string" } },
"computed_at": { "type": "string" }
}
},
"SignatureV1": {
"type": "object",
"description": "Cryptographic signature for a package artifact",
"required": ["key_id", "algo", "signature"],
"properties": {
"key_id": { "type": "string" },
"algo": { "type": "string" },
"signature": { "type": "string" },
"signed_at": { "type": "string" },
"signed_by": { "type": "string" }
}
}
}
},
@@ -223,7 +420,13 @@
"respond.redirect",
"respond.error",
"time.now_iso8601",
"string.format"
"string.format",
"sig.verify",
"sig.sign",
"oci.parse_manifest",
"oci.validate_manifest",
"deps.resolve",
"deps.graph"
],
"limits": {
"max_pipeline_ops": 128,
@@ -676,6 +879,476 @@
}
}
]
},
{
"id": "oci_push_blob",
"method": "PUT",
"path": "/v2/{namespace}/{name}/blobs/uploads/{upload_id}",
"tags": ["write_path", "oci"],
"description": "Upload a blob (layer or config) for OCI/Docker images",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["write"] } },
{ "op": "parse.path", "args": { "entity": "layer" } },
{ "op": "parse.query", "args": { "params": ["digest"] } },
{ "op": "txn.begin", "args": { "isolation": "serializable" } },
{
"op": "blob.put",
"args": {
"store": "primary",
"from": "request.body",
"out": "computed_digest",
"out_size": "blob_size"
}
},
{
"op": "blob.verify_digest",
"args": { "digest": "{digest}", "computed": "$computed_digest" }
},
{ "op": "txn.commit", "args": {} },
{
"op": "respond.json",
"args": {
"status": 201,
"headers": {
"Location": "/v2/{namespace}/{name}/blobs/{digest}",
"Docker-Content-Digest": "{digest}"
},
"body": {}
}
}
]
},
{
"id": "oci_get_blob",
"method": "GET",
"path": "/v2/{namespace}/{name}/blobs/{digest}",
"tags": ["public", "oci"],
"description": "Fetch a blob by digest for OCI/Docker images",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["read"] } },
{ "op": "parse.path", "args": { "entity": "layer" } },
{
"op": "blob.get",
"args": {
"store": "primary",
"digest": "{digest}",
"out": "blob"
}
},
{
"op": "respond.error",
"args": {
"when": { "is_null": "$blob" },
"status": 404,
"code": "BLOB_UNKNOWN",
"message": "Blob not found"
}
},
{
"op": "respond.bytes",
"args": {
"status": 200,
"headers": {
"Content-Type": "application/octet-stream",
"Docker-Content-Digest": "{digest}"
},
"body": "$blob"
}
}
]
},
{
"id": "oci_push_manifest",
"method": "PUT",
"path": "/v2/{namespace}/{name}/manifests/{reference}",
"tags": ["write_path", "oci"],
"description": "Push an OCI/Docker image manifest",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["write"] } },
{ "op": "parse.path", "args": { "entity": "manifest" } },
{ "op": "parse.json", "args": { "out": "manifest_body" } },
{ "op": "oci.parse_manifest", "args": { "manifest": "$manifest_body", "out": "parsed" } },
{ "op": "oci.validate_manifest", "args": { "manifest": "$parsed" } },
{ "op": "txn.begin", "args": { "isolation": "serializable" } },
{
"op": "blob.put",
"args": {
"store": "primary",
"from": "request.body",
"out": "manifest_digest",
"out_size": "manifest_size"
}
},
{ "op": "time.now_iso8601", "args": { "out": "now" } },
{
"op": "kv.put",
"args": {
"doc": "manifest",
"key": "manifest/{namespace}/{name}/{reference}",
"value": {
"namespace": "{namespace}",
"name": "{name}",
"reference": "{reference}",
"media_type": "$parsed.media_type",
"config_digest": "$parsed.config.digest",
"layers": "$parsed.layers",
"created_at": "$now",
"total_size": "$parsed.total_size"
}
}
},
{
"op": "kv.put",
"args": {
"doc": "manifest_by_digest",
"key": "manifest_digest/{namespace}/{name}/$manifest_digest",
"value": {
"namespace": "{namespace}",
"name": "{name}",
"reference": "{reference}",
"media_type": "$parsed.media_type",
"config_digest": "$parsed.config.digest",
"layers": "$parsed.layers",
"created_at": "$now"
}
}
},
{
"op": "emit.event",
"args": {
"type": "manifest.pushed",
"payload": {
"namespace": "{namespace}",
"name": "{name}",
"reference": "{reference}",
"digest": "$manifest_digest",
"at": "$now",
"by": "{principal.sub}"
}
}
},
{ "op": "txn.commit", "args": {} },
{
"op": "respond.json",
"args": {
"status": 201,
"headers": {
"Location": "/v2/{namespace}/{name}/manifests/$manifest_digest",
"Docker-Content-Digest": "$manifest_digest"
},
"body": {}
}
}
]
},
{
"id": "oci_get_manifest",
"method": "GET",
"path": "/v2/{namespace}/{name}/manifests/{reference}",
"tags": ["public", "oci"],
"description": "Fetch an OCI/Docker image manifest by tag or digest",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["read"] } },
{ "op": "parse.path", "args": { "entity": "manifest" } },
{
"op": "kv.get",
"args": {
"doc": "manifest",
"key": "manifest/{namespace}/{name}/{reference}",
"out": "manifest_meta"
}
},
{
"op": "respond.error",
"args": {
"when": { "is_null": "$manifest_meta" },
"status": 404,
"code": "MANIFEST_UNKNOWN",
"message": "Manifest not found"
}
},
{
"op": "blob.get",
"args": {
"store": "primary",
"digest": "$manifest_meta.config_digest",
"out": "manifest_blob"
}
},
{
"op": "respond.bytes",
"args": {
"status": 200,
"headers": {
"Content-Type": "$manifest_meta.media_type",
"Docker-Content-Digest": "$manifest_meta.config_digest"
},
"body": "$manifest_blob"
}
}
]
},
{
"id": "publish_with_dependencies",
"method": "PUT",
"path": "/v1/{namespace}/{name}/{version}/{variant}/package",
"tags": ["write_path"],
"description": "Publish a package with metadata and dependencies",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["write"] } },
{ "op": "parse.path", "args": { "entity": "artifact" } },
{ "op": "normalize.entity", "args": { "entity": "artifact" } },
{ "op": "validate.entity", "args": { "entity": "artifact" } },
{ "op": "parse.json", "args": { "out": "pkg" } },
{
"op": "validate.json_schema",
"args": {
"schema": {
"type": "object",
"required": ["blob_digest"],
"properties": {
"blob_digest": { "type": "string" },
"description": { "type": "string" },
"license": { "type": "string" },
"homepage": { "type": "string" },
"authors": { "type": "array", "items": { "type": "string" } },
"keywords": { "type": "array", "items": { "type": "string" } },
"package_type": { "type": "string" },
"dependencies": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "version_spec"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"version_spec": { "type": "string" },
"kind": { "type": "string" }
}
}
}
}
},
"value": "$pkg"
}
},
{ "op": "txn.begin", "args": { "isolation": "serializable" } },
{ "op": "time.now_iso8601", "args": { "out": "now" } },
{
"op": "kv.cas_put",
"args": {
"doc": "artifact_meta",
"key": "artifact/{namespace}/{name}/{version}/{variant}",
"if_absent": true,
"value": {
"namespace": "{namespace}",
"name": "{name}",
"version": "{version}",
"variant": "{variant}",
"blob_digest": "$pkg.blob_digest",
"created_at": "$now",
"created_by": "{principal.sub}",
"package_info": {
"description": "$pkg.description",
"license": "$pkg.license",
"homepage": "$pkg.homepage",
"authors": "$pkg.authors",
"keywords": "$pkg.keywords",
"package_type": "$pkg.package_type"
},
"dependencies": "$pkg.dependencies"
}
}
},
{
"op": "index.upsert",
"args": {
"index": "artifact_versions",
"key": { "namespace": "{namespace}", "name": "{name}" },
"value": {
"namespace": "{namespace}",
"name": "{name}",
"version": "{version}",
"variant": "{variant}",
"blob_digest": "$pkg.blob_digest"
}
}
},
{
"op": "emit.event",
"args": {
"type": "artifact.published",
"payload": {
"namespace": "{namespace}",
"name": "{name}",
"version": "{version}",
"variant": "{variant}",
"blob_digest": "$pkg.blob_digest",
"at": "$now",
"by": "{principal.sub}",
"has_dependencies": true
}
}
},
{ "op": "txn.commit", "args": {} },
{
"op": "respond.json",
"args": {
"status": 201,
"body": { "ok": true, "digest": "$pkg.blob_digest" }
}
}
]
},
{
"id": "get_dependencies",
"method": "GET",
"path": "/v1/{namespace}/{name}/{version}/{variant}/dependencies",
"tags": ["public"],
"description": "Get dependencies for a package",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["read"] } },
{ "op": "parse.path", "args": { "entity": "artifact" } },
{ "op": "normalize.entity", "args": { "entity": "artifact" } },
{
"op": "kv.get",
"args": {
"doc": "artifact_meta",
"key": "artifact/{namespace}/{name}/{version}/{variant}",
"out": "meta"
}
},
{
"op": "respond.error",
"args": {
"when": { "is_null": "$meta" },
"status": 404,
"code": "NOT_FOUND",
"message": "Package not found"
}
},
{
"op": "respond.json",
"args": {
"status": 200,
"body": {
"namespace": "{namespace}",
"name": "{name}",
"version": "{version}",
"variant": "{variant}",
"dependencies": "$meta.dependencies"
}
}
}
]
},
{
"id": "add_signature",
"method": "POST",
"path": "/v1/{namespace}/{name}/{version}/{variant}/signatures",
"tags": ["write_path"],
"description": "Add a cryptographic signature to a package",
"pipeline": [
{ "op": "auth.require_scopes", "args": { "scopes": ["write"] } },
{ "op": "parse.path", "args": { "entity": "artifact" } },
{ "op": "normalize.entity", "args": { "entity": "artifact" } },
{ "op": "parse.json", "args": { "out": "sig_data" } },
{
"op": "validate.json_schema",
"args": {
"schema": {
"type": "object",
"required": ["key_id", "algo", "signature"],
"properties": {
"key_id": { "type": "string" },
"algo": { "type": "string", "enum": ["ed25519", "rsa-sha256", "ecdsa-p256"] },
"signature": { "type": "string" }
}
},
"value": "$sig_data"
}
},
{
"op": "kv.get",
"args": {
"doc": "artifact_meta",
"key": "artifact/{namespace}/{name}/{version}/{variant}",
"out": "meta"
}
},
{
"op": "respond.error",
"args": {
"when": { "is_null": "$meta" },
"status": 404,
"code": "NOT_FOUND",
"message": "Package not found"
}
},
{
"op": "sig.verify",
"args": {
"key_id": "$sig_data.key_id",
"algo": "$sig_data.algo",
"signature": "$sig_data.signature",
"data_digest": "$meta.blob_digest"
}
},
{ "op": "txn.begin", "args": { "isolation": "serializable" } },
{ "op": "time.now_iso8601", "args": { "out": "now" } },
{
"op": "kv.put",
"args": {
"doc": "signature",
"key": "sig/{namespace}/{name}/{version}/{variant}/$sig_data.key_id",
"value": {
"key_id": "$sig_data.key_id",
"algo": "$sig_data.algo",
"signature": "$sig_data.signature",
"signed_at": "$now",
"signed_by": "{principal.sub}"
}
}
},
{
"op": "emit.event",
"args": {
"type": "signature.added",
"payload": {
"namespace": "{namespace}",
"name": "{name}",
"version": "{version}",
"variant": "{variant}",
"key_id": "$sig_data.key_id",
"at": "$now",
"by": "{principal.sub}"
}
}
},
{ "op": "txn.commit", "args": {} },
{ "op": "respond.json", "args": { "status": 201, "body": { "ok": true } } }
]
}
]
},
@@ -709,6 +1382,41 @@
"name": "tag.updated",
"durable": true,
"schema": { "type": "object" }
},
{
"name": "manifest.pushed",
"durable": true,
"description": "Emitted when an OCI/Docker manifest is pushed",
"schema": {
"type": "object",
"required": ["namespace", "name", "reference", "digest", "at"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"reference": { "type": "string" },
"digest": { "type": "string" },
"at": { "type": "string" },
"by": { "type": "string" }
}
}
},
{
"name": "signature.added",
"durable": true,
"description": "Emitted when a signature is added to a package",
"schema": {
"type": "object",
"required": ["namespace", "name", "version", "key_id", "at"],
"properties": {
"namespace": { "type": "string" },
"name": { "type": "string" },
"version": { "type": "string" },
"variant": { "type": "string" },
"key_id": { "type": "string" },
"at": { "type": "string" },
"by": { "type": "string" }
}
}
}
]
},
@@ -839,7 +1547,11 @@
"features": {
"mutable_tags": true,
"allow_overwrite_artifacts": false,
"proxy_enabled": true
"proxy_enabled": true,
"gc_enabled": true,
"signing_enabled": true,
"oci_enabled": true,
"dependencies_enabled": true
},
"validation": {