This commit is contained in:
2025-09-14 15:35:41 +02:00
parent 9c895533b6
commit 3f90e5bc15
17 changed files with 530 additions and 29 deletions

View File

@@ -0,0 +1,23 @@
import freeflowuniverse.herolib.hero.heroserver
import freeflowuniverse.herolib.schemas.openrpc
fn testsuite_begin() {
// a clean start
// os.rm('./db')! //TODO: was giving issues
}
fn test_heroserver_new() {
// Create server
mut server := heroserver.new_server(port: 8080)!
// Register handlers
spec := openrpc.from_file('./openrpc.json')!
handler := openrpc.new_handler(spec)
server.handler_registry.register('comments', handler, spec)
// Start server
go server.start()
assert true
}

View File

@@ -0,0 +1,32 @@
{
"openrpc": "1.2.6",
"info": {
"title": "Comment Service",
"description": "A simple service for managing comments.",
"version": "1.0.0"
},
"methods": [
{
"name": "add_comment",
"summary": "Add a new comment",
"params": [
{
"name": "text",
"description": "The content of the comment.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "comment_id",
"description": "The ID of the newly created comment.",
"schema": {
"type": "string"
}
}
}
],
"components": {}
}

View File

@@ -1,27 +0,0 @@
lets make an openrpc server
over unixsocker
on /tmp/heromodels
put code in lib/hero/heromodels/openrpc
do example for comment.v
make struct called RPCServer
put as methods
- comment_get(args CommentGetArgs)[]Comment! //chose the params well is @[params] struct CommentGetArgs always with id… in this case maybe author. …
- walk over the hset with data, find the one we are looking for based on the args
- comment_set(obj Comment)!
- comment_delete(id…)
- comment_list() ![]u32
- discover()!string //returns a full openrpc spec
make one .v file per type of object now comment_…
we will then do for the other objects too
also generate the openrpc spec based on the methods we have and the objects we return

View File

@@ -0,0 +1,34 @@
implement lib/hero/heroserver
see aiprompts/v_core/veb for how servers are done and also see examples/hero/herorpc/herorpc_example.vsh how we start the server in an example
and the implementation of the example of how webserver is see lib/schemas/openrpc/controller_http.v
specs for the heroserver
- a factory (without globals) creates a server, based on chosen port
- the server does basic authentication and has
- register method: pubkey
- authreq: pubkey, it returns a unique key (hashed md5 of pubkey + something random). (is a request for authentication)
- auth: the user who wants access signs the unique key from authreq with , and sends the signature to the server, who then knows for sure I know this user, we return as sessionkey
- all consequent requests need to use this sessionkey, so the server knows who is doing the requests
- the server serves the openrpc api behind api/$handlertype/... the $handlertype is per handler type, so we can register more than 1 openrpc hander
- the server serves an html endpoint for doc/$handlertype/
for the doc (html endpoint)
- use bootstrap from cdn
- https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css
- https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js
- https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js
- create an html template in lib/hero/heroserver/template/doc.md (template for v language)
- the template uses the openrpc spec obj comes from lib/schemas/openrpc and lib/schemas/jsonrpc for the schema's
- so first fo the spec decode to the object from schemas/openrpc then use this obj to populate the template
- in the template make a header 1 for each rootobject e.g. calendar, then dense show the methods with directly in the method a dense representation of the params and return
- each object e.g. pub fn (self Comment) description(methodname string) string { has a description and pub fn (self Comment) example(methodname string) (string, string) wich returns description per method, if not filled in then its for the full rootobject, and also example, make sure to use those in the template for the documentation
- the template is markdown, we will have to use a .md to .html conversion (best to do in browser itself) and get .md from the webserver directly and convert
- the purpose is to have a very nice documentation done per object so we know what the object does, and how to use it
make clear instructions what code needs to be written and which steps are needed
we are in architecture mode

112
lib/hero/heroserver/auth.v Normal file
View File

@@ -0,0 +1,112 @@
module heroserver
import crypto.md5
import crypto.ed25519
import rand
import time
pub struct AuthManager {
mut:
registered_keys map[string]string // pubkey -> user_id
pending_auths map[string]AuthChallenge // challenge -> challenge_data
active_sessions map[string]Session // session_key -> session_data
}
pub struct AuthChallenge {
pub:
pubkey string
challenge string
created_at i64
expires_at i64
}
pub struct Session {
pub:
user_id string
pubkey string
created_at i64
expires_at i64
}
pub fn new_auth_manager() &AuthManager {
return &AuthManager{}
}
// Register public key
pub fn (mut am AuthManager) register_pubkey(pubkey string) !string {
// Validate pubkey format
if pubkey.len != 64 { // ed25519 pubkey length
return error('Invalid public key format')
}
user_id := md5.hexhash(pubkey + time.now().unix().str())
am.registered_keys[pubkey] = user_id
return user_id
}
// Generate authentication challenge
pub fn (mut am AuthManager) create_auth_challenge(pubkey string) !string {
// Check if pubkey is registered
if pubkey !in am.registered_keys {
return error('Public key not registered')
}
// Generate unique challenge
random_data := rand.string(32)
challenge := md5.hexhash(pubkey + random_data + time.now().unix().str())
now := time.now().unix()
am.pending_auths[challenge] = AuthChallenge{
pubkey: pubkey
challenge: challenge
created_at: now
expires_at: now + 300 // 5 minutes
}
return challenge
}
// Verify signature and create session
pub fn (mut am AuthManager) verify_and_create_session(challenge string, signature string) !string {
// Get challenge data
auth_challenge := am.pending_auths[challenge] or {
return error('Invalid or expired challenge')
}
// Check expiration
if time.now().unix() > auth_challenge.expires_at {
am.pending_auths.delete(challenge)
return error('Challenge expired')
}
// Verify signature
pubkey_bytes := auth_challenge.pubkey.bytes()
challenge_bytes := challenge.bytes()
signature_bytes := signature.bytes()
ed25519.verify(pubkey_bytes, challenge_bytes, signature_bytes) or {
return error('Invalid signature')
}
// Create session
session_key := md5.hexhash(auth_challenge.pubkey + time.now().unix().str() + rand.string(16))
now := time.now().unix()
am.active_sessions[session_key] = Session{
user_id: am.registered_keys[auth_challenge.pubkey]
pubkey: auth_challenge.pubkey
created_at: now
expires_at: now + 3600 // 1 hour
}
// Clean up challenge
am.pending_auths.delete(challenge)
return session_key
}
// Validate session
pub fn (am AuthManager) validate_session(session_key string) bool {
session := am.active_sessions[session_key] or { return false }
return time.now().unix() < session.expires_at
}

92
lib/hero/heroserver/doc.v Normal file
View File

@@ -0,0 +1,92 @@
module heroserver
import freeflowuniverse.herolib.schemas.openrpc
import os
import freeflowuniverse.herolib.schemas.jsonschema
// Generate HTML documentation for handler type
pub fn (s HeroServer) generate_documentation(handler_type string, handler openrpc.Handler) !string {
spec := s.handler_registry.get_spec(handler_type) or {
return error('No spec found for handler type: ${handler_type}')
}
// Load and process template
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc.md')
template_content := os.read_file(template_path) or {
return error('Failed to read documentation template: ${err}')
}
// Process template with spec data
doc_content := process_doc_template(template_content, spec, handler_type)
// Return HTML with Bootstrap and markdown processing
return generate_html_wrapper(doc_content, handler_type)
}
// Process the markdown template with OpenRPC spec data
fn process_doc_template(template string, spec openrpc.OpenRPC, handler_type string) string {
mut content := template
// Replace template variables
content = content.replace('@{handler_type}', handler_type)
content = content.replace('@{spec.info.title}', spec.info.title)
content = content.replace('@{spec.info.description}', spec.info.description)
content = content.replace('@{spec.info.version}', spec.info.version)
// Generate methods documentation
mut methods_doc := ''
for method in spec.methods {
methods_doc += generate_method_doc(method)
}
content = content.replace('@{methods}', methods_doc)
return content
}
// Generate documentation for a single method
fn generate_method_doc(method openrpc.Method) string {
mut doc := '## ${method.name}\n\n'
if method.description.len > 0 {
doc += '${method.description}\n\n'
}
// Parameters
if method.params.len > 0 {
doc += '### Parameters\n\n'
for param in method.params {
// Handle both ContentDescriptor and Reference
if param is openrpc.ContentDescriptor {
if param.schema is jsonschema.Schema {
schema := param.schema as jsonschema.Schema
doc += '- **${param.name}** (${schema.typ}): ${param.description}\n'
}
}
}
doc += '\n'
}
// Result
if method.result is openrpc.ContentDescriptor {
result := method.result as openrpc.ContentDescriptor
doc += '### Returns\n\n'
doc += '${result.description}\n\n'
}
// Examples (would need to be added to OpenRPC spec or handled differently)
doc += '### Example\n\n'
doc += '```json\n'
doc += '// Request example would go here\n'
doc += '```\n\n'
return doc
}
// Generate HTML wrapper with Bootstrap
fn generate_html_wrapper(markdown_content string, handler_type string) string {
template_path := os.join_path(@VMODROOT, 'lib/hero/heroserver/templates/doc_wrapper.html')
mut template_content := os.read_file(template_path) or { return 'Template not found' }
template_content = template_content.replace('@{handler_type}', handler_type)
template_content = template_content.replace('@{markdown_content}', markdown_content)
return template_content
}

View File

@@ -0,0 +1,19 @@
module heroserver
@[params]
pub struct ServerConfig {
pub:
port int = 8080
host string = 'localhost'
}
// Factory function to create new server instance
pub fn new_server(config ServerConfig) !&HeroServer {
mut server := &HeroServer{
config: config
auth_manager: new_auth_manager()
handler_registry: new_handler_registry()
}
return server
}

View File

@@ -0,0 +1,34 @@
module heroserver
import freeflowuniverse.herolib.schemas.openrpc
pub struct HandlerRegistry {
mut:
handlers map[string]openrpc.Handler
specs map[string]openrpc.OpenRPC
}
pub fn new_handler_registry() &HandlerRegistry {
return &HandlerRegistry{}
}
// Register OpenRPC handler with type name
pub fn (mut hr HandlerRegistry) register(handler_type string, handler openrpc.Handler, spec openrpc.OpenRPC) {
hr.handlers[handler_type] = handler
hr.specs[handler_type] = spec
}
// Get handler by type
pub fn (hr HandlerRegistry) get(handler_type string) ?openrpc.Handler {
return hr.handlers[handler_type]
}
// Get OpenRPC spec by type
pub fn (hr HandlerRegistry) get_spec(handler_type string) ?openrpc.OpenRPC {
return hr.specs[handler_type]
}
// List all registered handler types
pub fn (hr HandlerRegistry) list_types() []string {
return hr.handlers.keys()
}

View File

@@ -0,0 +1,80 @@
module heroserver
import veb
import freeflowuniverse.herolib.schemas.jsonrpc
pub struct HeroServer {
pub mut:
config ServerConfig
auth_manager &AuthManager
handler_registry &HandlerRegistry
}
pub struct Context {
veb.Context
}
// Start the server
pub fn (mut s HeroServer) start() ! {
veb.run[HeroServer, Context](mut s, s.config.port)
}
// Authentication endpoints
@['/register'; post]
pub fn (mut s HeroServer) register(mut ctx Context) veb.Result {
// Implementation for pubkey registration
return ctx.text('not implemented')
}
@['/authreq'; post]
pub fn (mut s HeroServer) authreq(mut ctx Context) veb.Result {
// Implementation for authentication request
return ctx.text('not implemented')
}
@['/auth'; post]
pub fn (mut s HeroServer) auth(mut ctx Context) veb.Result {
// Implementation for authentication verification
return ctx.text('not implemented')
}
// API endpoints
@['/api/:handler_type'; post]
pub fn (mut s HeroServer) api(mut ctx Context) veb.Result {
handler_type := ctx.params['handler_type'] or {
return ctx.request_error('handler_type not found in params')
}
// Validate session
session_key := ctx.req.header.get('Authorization') or { '' }
if !s.auth_manager.validate_session(session_key) {
return ctx.request_error('Invalid session')
}
// Get handler and process request
mut handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
request := jsonrpc.decode_request(ctx.req.data) or {
return ctx.request_error('Invalid JSON-RPC request')
}
response := handler.handle(request) or { return ctx.server_error('Handler error') }
return ctx.json(response)
}
// Documentation endpoints
@['/doc/:handler_type'; get]
pub fn (mut s HeroServer) doc(mut ctx Context) veb.Result {
handler_type := ctx.params['handler_type'] or {
return ctx.request_error('handler_type not found in params')
}
handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
doc_html := s.generate_documentation(handler_type, handler) or {
return ctx.server_error('Documentation generation failed')
}
return ctx.html(doc_html)
}

View File

@@ -0,0 +1,30 @@
# @{handler_type} API Documentation
@{spec.info.description}
**Version:** @{spec.info.version}
## Overview
This documentation provides details about the @{handler_type} API endpoints and their usage.
@{methods}
## Authentication
All API requests require a valid session key obtained through the authentication flow:
1. **Register**: Submit your public key to register
2. **Request Challenge**: Get an authentication challenge
3. **Authenticate**: Sign the challenge and submit for session key
4. **Use Session**: Include session key in subsequent API requests
## Error Handling
The API uses standard JSON-RPC 2.0 error codes:
- `-32700`: Parse error
- `-32600`: Invalid Request
- `-32601`: Method not found
- `-32602`: Invalid params
- `-32603`: Internal error

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@{handler_type} API Documentation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100;
padding: 48px 0 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
.main {
margin-left: 240px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="sidebar-sticky pt-3">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>@{handler_type} API</span>
</h6>
<ul class="nav flex-column" id="toc">
<!-- Table of contents will be generated by JavaScript -->
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4 main">
<div id="content">
<!-- Markdown content will be rendered here -->
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Render markdown content
const markdownContent = `@{markdown_content}`;
document.getElementById('content').innerHTML = marked.parse(markdownContent);
// Generate table of contents
const headers = document.querySelectorAll('#content h1, #content h2, #content h3');
const toc = document.getElementById('toc');
headers.forEach((header, index) => {
const id = 'header-' + index;
header.id = id;
const li = document.createElement('li');
li.className = 'nav-item';
const a = document.createElement('a');
a.className = 'nav-link';
a.href = '#' + id;
a.textContent = header.textContent;
a.style.paddingLeft = (header.tagName === 'H2' ? '20px' : header.tagName === 'H3' ? '40px' : '10px');
li.appendChild(a);
toc.appendChild(li);
});
</script>
</body>
</html>

View File

@@ -1,2 +0,0 @@
this will be the main server which acts as gateway to heromodels and other rpc backend services.