...
This commit is contained in:
23
examples/hero/heroserver/heroserver_test.v
Normal file
23
examples/hero/heroserver/heroserver_test.v
Normal 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
|
||||
}
|
||||
32
examples/hero/heroserver/openrpc.json
Normal file
32
examples/hero/heroserver/openrpc.json
Normal 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": {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
34
lib/hero/heroserver/ai_instructions_specs.md
Normal file
34
lib/hero/heroserver/ai_instructions_specs.md
Normal 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
112
lib/hero/heroserver/auth.v
Normal 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
92
lib/hero/heroserver/doc.v
Normal 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
|
||||
}
|
||||
19
lib/hero/heroserver/factory.v
Normal file
19
lib/hero/heroserver/factory.v
Normal 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
|
||||
}
|
||||
34
lib/hero/heroserver/handlers.v
Normal file
34
lib/hero/heroserver/handlers.v
Normal 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()
|
||||
}
|
||||
80
lib/hero/heroserver/server.v
Normal file
80
lib/hero/heroserver/server.v
Normal 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)
|
||||
}
|
||||
30
lib/hero/heroserver/templates/doc.md
Normal file
30
lib/hero/heroserver/templates/doc.md
Normal 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
|
||||
74
lib/hero/heroserver/templates/doc_wrapper.html
Normal file
74
lib/hero/heroserver/templates/doc_wrapper.html
Normal 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>
|
||||
@@ -1,2 +0,0 @@
|
||||
this will be the main server which acts as gateway to heromodels and other rpc backend services.
|
||||
|
||||
Reference in New Issue
Block a user