mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
stuff
This commit is contained in:
375
docs/CODE_REVIEW_FINDINGS.md
Normal file
375
docs/CODE_REVIEW_FINDINGS.md
Normal 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
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
9
mojo/examples/snake/conanfile.txt
Normal file
9
mojo/examples/snake/conanfile.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
[requires]
|
||||
sdl/3.2.20
|
||||
|
||||
[generators]
|
||||
CMakeDeps
|
||||
CMakeToolchain
|
||||
|
||||
[options]
|
||||
sdl/*:shared=True
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user