Merge branch 'development' of github.com:Incubaid/herolib into development

* 'development' of github.com:Incubaid/herolib:
  refactor: Simplify handler signatures and add server runner
  fix: improve Redis response parsing and error handling
  fix: Correct AGEClient method receivers and error syntax
This commit is contained in:
2025-09-15 20:28:58 +02:00
8 changed files with 97 additions and 68 deletions

View File

@@ -38,7 +38,7 @@ signing_keypair := age_client.generate_signing_keypair() or {
return return
} }
!signed := age_client.sign(signing_keypair.sign_key, message) or { signed := age_client.sign(signing_keypair.sign_key, message) or {
println('Error signing message: ${err}') println('Error signing message: ${err}')
return return
} }
@@ -47,7 +47,7 @@ verified := age_client.verify(signing_keypair.verify_key, message, signed.signat
println('Error verifying signature: ${err}') println('Error verifying signature: ${err}')
return return
} }
!println('Message: ${message}') println('Message: ${message}')
println('Signature: ${signed.signature}') println('Signature: ${signed.signature}')
println('Signature valid: ${verified}') println('Signature valid: ${verified}')

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.hero.heroserver
mut server := heroserver.new_server(port: 8080)!
server.start()!

View File

@@ -83,5 +83,16 @@ pub fn (mut r Redis) send_expect_list(items []string) ![]resp.RValue {
} }
r.write_cmds(items)! r.write_cmds(items)!
res := r.get_response()! res := r.get_response()!
// Check if we got an error response
if res is resp.RError {
return error('Redis error: ${res.value}')
}
// Check if we got an array response
if res !is resp.RArray {
return error('Expected array response but got ${res.type_name()}. Response: ${resp.get_redis_value(res)}')
}
return resp.get_redis_array(res) return resp.get_redis_array(res)
} }

View File

@@ -103,7 +103,7 @@ pub fn (v RValue) u32() u32 {
pub fn (v RValue) strget() string { pub fn (v RValue) strget() string {
match v { match v {
RInt { RInt {
return v.str() return v.value.str()
} }
// RArray{ // RArray{
// return v.str() // return v.str()
@@ -112,10 +112,16 @@ pub fn (v RValue) strget() string {
return v.value.bytestr() return v.value.bytestr()
} }
RString { RString {
return v.str() return v.value
}
RError {
return v.value
}
RNil {
return ''
} }
else { else {
panic('could not find type') panic('could not find type: ${v.type_name()}')
} }
} }
} }

View File

@@ -1,11 +1,11 @@
module crypt module crypt
import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.resp
// Stateless AGE operations // Stateless AGE operations
// generate_keypair creates a new Age encryption key pair // generate_keypair creates a new Age encryption key pair
pub fn (client &AGEClient) generate_keypair() !KeyPair { pub fn (mut client AGEClient) generate_keypair() !KeyPair {
response := client.redis.send_expect_list(['AGE', 'GENENC'])! response := client.redis.send_expect_list(['AGE', 'GENENC'])!
if response.len < 2 { if response.len < 2 {
@@ -13,13 +13,13 @@ pub fn (client &AGEClient) generate_keypair() !KeyPair {
} }
return KeyPair{ return KeyPair{
recipient: response[0].str() recipient: response[0].strget()
identity: response[1].str() identity: response[1].strget()
} }
} }
// generate_signing_keypair creates a new Age signing key pair // generate_signing_keypair creates a new Age signing key pair
pub fn (client &AGEClient) generate_signing_keypair() !SigningKeyPair { pub fn (mut client AGEClient) generate_signing_keypair() !SigningKeyPair {
response := client.redis.send_expect_list(['AGE', 'GENSIGN'])! response := client.redis.send_expect_list(['AGE', 'GENSIGN'])!
if response.len < 2 { if response.len < 2 {
@@ -27,13 +27,13 @@ pub fn (client &AGEClient) generate_signing_keypair() !SigningKeyPair {
} }
return SigningKeyPair{ return SigningKeyPair{
verify_key: response[0].str() verify_key: response[0].strget()
sign_key: response[1].str() sign_key: response[1].strget()
} }
} }
// encrypt encrypts a message with the recipient's public key // encrypt encrypts a message with the recipient's public key
pub fn (client &AGEClient) encrypt(recipient string, message string) !EncryptionResult { pub fn (mut client AGEClient) encrypt(recipient string, message string) !EncryptionResult {
ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPT', recipient, message])! ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPT', recipient, message])!
return EncryptionResult{ return EncryptionResult{
@@ -42,12 +42,12 @@ pub fn (client &AGEClient) encrypt(recipient string, message string) !Encryption
} }
// decrypt decrypts a message with the identity (private key) // decrypt decrypts a message with the identity (private key)
pub fn (client &AGEClient) decrypt(identity string, ciphertext string) !string { pub fn (mut client AGEClient) decrypt(identity string, ciphertext string) !string {
return client.redis.send_expect_str(['AGE', 'DECRYPT', identity, ciphertext])! return client.redis.send_expect_str(['AGE', 'DECRYPT', identity, ciphertext])!
} }
// sign signs a message with the signing key // sign signs a message with the signing key
pub fn (client &AGEClient) sign(sign_key string, message string) !SignatureResult { pub fn (mut client AGEClient) sign(sign_key string, message string) !SignatureResult {
signature := client.redis.send_expect_str(['AGE', 'SIGN', sign_key, message])! signature := client.redis.send_expect_str(['AGE', 'SIGN', sign_key, message])!
return SignatureResult{ return SignatureResult{
@@ -56,15 +56,15 @@ pub fn (client &AGEClient) sign(sign_key string, message string) !SignatureResul
} }
// verify verifies a signature with the verification key // verify verifies a signature with the verification key
pub fn (client &AGEClient) verify(verify_key string, message string, signature string) !bool { pub fn (mut client AGEClient) verify(verify_key string, message string, signature string) !bool {
result := client.redis.send_expect_int(['AGE', 'VERIFY', verify_key, message, signature])! result := client.redis.send_expect_str(['AGE', 'VERIFY', verify_key, message, signature])!
return result == 1 return result == '1'
} }
// Key-managed AGE operations // Key-managed AGE operations
// create_named_keypair creates and stores a named encryption key pair // create_named_keypair creates and stores a named encryption key pair
pub fn (client &AGEClient) create_named_keypair(name string) !KeyPair { pub fn (mut client AGEClient) create_named_keypair(name string) !KeyPair {
response := client.redis.send_expect_list(['AGE', 'KEYGEN', name])! response := client.redis.send_expect_list(['AGE', 'KEYGEN', name])!
if response.len < 2 { if response.len < 2 {
@@ -72,13 +72,13 @@ pub fn (client &AGEClient) create_named_keypair(name string) !KeyPair {
} }
return KeyPair{ return KeyPair{
recipient: response[0].str() recipient: response[0].strget()
identity: response[1].str() identity: response[1].strget()
} }
} }
// create_named_signing_keypair creates and stores a named signing key pair // create_named_signing_keypair creates and stores a named signing key pair
pub fn (client &AGEClient) create_named_signing_keypair(name string) !SigningKeyPair { pub fn (mut client AGEClient) create_named_signing_keypair(name string) !SigningKeyPair {
response := client.redis.send_expect_list(['AGE', 'SIGNKEYGEN', name])! response := client.redis.send_expect_list(['AGE', 'SIGNKEYGEN', name])!
if response.len < 2 { if response.len < 2 {
@@ -86,13 +86,13 @@ pub fn (client &AGEClient) create_named_signing_keypair(name string) !SigningKey
} }
return SigningKeyPair{ return SigningKeyPair{
verify_key: response[0].str() verify_key: response[0].strget()
sign_key: response[1].str() sign_key: response[1].strget()
} }
} }
// encrypt_with_named_key encrypts a message using a named key // encrypt_with_named_key encrypts a message using a named key
pub fn (client &AGEClient) encrypt_with_named_key(key_name string, message string) !EncryptionResult { pub fn (mut client AGEClient) encrypt_with_named_key(key_name string, message string) !EncryptionResult {
ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPTNAME', key_name, message])! ciphertext := client.redis.send_expect_str(['AGE', 'ENCRYPTNAME', key_name, message])!
return EncryptionResult{ return EncryptionResult{
@@ -101,12 +101,12 @@ pub fn (client &AGEClient) encrypt_with_named_key(key_name string, message strin
} }
// decrypt_with_named_key decrypts a message using a named key // decrypt_with_named_key decrypts a message using a named key
pub fn (client &AGEClient) decrypt_with_named_key(key_name string, ciphertext string) !string { pub fn (mut client AGEClient) decrypt_with_named_key(key_name string, ciphertext string) !string {
return client.redis.send_expect_str(['AGE', 'DECRYPTNAME', key_name, ciphertext])! return client.redis.send_expect_str(['AGE', 'DECRYPTNAME', key_name, ciphertext])!
} }
// sign_with_named_key signs a message using a named signing key // sign_with_named_key signs a message using a named signing key
pub fn (client &AGEClient) sign_with_named_key(key_name string, message string) !SignatureResult { pub fn (mut client AGEClient) sign_with_named_key(key_name string, message string) !SignatureResult {
signature := client.redis.send_expect_str(['AGE', 'SIGNNAME', key_name, message])! signature := client.redis.send_expect_str(['AGE', 'SIGNNAME', key_name, message])!
return SignatureResult{ return SignatureResult{
@@ -115,18 +115,36 @@ pub fn (client &AGEClient) sign_with_named_key(key_name string, message string)
} }
// verify_with_named_key verifies a signature using a named verification key // verify_with_named_key verifies a signature using a named verification key
pub fn (client &AGEClient) verify_with_named_key(key_name string, message string, signature string) !bool { pub fn (mut client AGEClient) verify_with_named_key(key_name string, message string, signature string) !bool {
result := client.redis.send_expect_int(['AGE', 'VERIFYNAME', key_name, message, signature])! result := client.redis.send_expect_str(['AGE', 'VERIFYNAME', key_name, message, signature])!
return result == 1 return result == '1'
} }
// list_keys lists all stored AGE keys // list_keys lists all stored AGE keys
pub fn (client &AGEClient) list_keys() ![]string { pub fn (mut client AGEClient) list_keys() ![]string {
response := client.redis.send_expect_list(['AGE', 'LIST'])! response := client.redis.send_expect_list(['AGE', 'LIST'])!
mut keys := []string{} mut keys := []string{}
for i in 0 .. response.len { for i in 0 .. response.len {
keys << response[i].str() item := response[i]
// Handle different response types
match item {
resp.RString {
keys << item.value
}
resp.RBString {
keys << item.value.bytestr()
}
resp.RArray {
// If it's an array, try to get the first element as the key name
if item.values.len > 0 {
keys << item.values[0].strget()
}
}
else {
keys << item.strget()
}
}
} }
return keys return keys

View File

@@ -6,29 +6,27 @@ and the implementation of the example of how webserver is see lib/schemas/openrp
specs for the heroserver specs for the heroserver
- a factory (without globals) creates a server, based on chosen port - a factory (without globals) creates a server, based on chosen port
- the server does basic authentication and has - the server does basic authentication and has
- register method: pubkey - register method: pubkey
- authreq: pubkey, it returns a unique key (hashed md5 of pubkey + something random). (is a request for authentication) - 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 - 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 - 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 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/ - the server serves an html endpoint for doc/$handlertype/
for the doc (html endpoint) for the doc (html endpoint)
- use bootstrap from cdn - 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/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.min.js>
- https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.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) - 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 - 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 - 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 - 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 - 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 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 - 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 make clear instructions what code needs to be written and which steps are needed
we are in architecture mode we are in architecture mode

View File

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

View File

@@ -50,13 +50,9 @@ pub fn (mut s HeroServer) auth(mut ctx Context) veb.Result {
// API endpoints // API endpoints
@['/api/:handler_type'; post] @['/api/:handler_type'; post]
pub fn (mut s HeroServer) api(mut ctx Context) veb.Result { pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result {
handler_type := ctx.params['handler_type'] or {
return ctx.request_error('handler_type not found in params')
}
// Validate session // Validate session
session_key := ctx.req.header.get('Authorization') or { '' } session_key := ctx.get_custom_header('Authorization') or { '' }
if !s.auth_manager.validate_session(session_key) { if !s.auth_manager.validate_session(session_key) {
return ctx.request_error('Invalid session') return ctx.request_error('Invalid session')
} }
@@ -75,11 +71,7 @@ pub fn (mut s HeroServer) api(mut ctx Context) veb.Result {
// Documentation endpoints // Documentation endpoints
@['/doc/:handler_type'; get] @['/doc/:handler_type'; get]
pub fn (mut s HeroServer) doc(mut ctx Context) veb.Result { pub fn (mut s HeroServer) doc(mut ctx Context, handler_type string) 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() } handler := s.handler_registry.get(handler_type) or { return ctx.not_found() }
doc_html := s.generate_documentation(handler_type, handler) or { doc_html := s.generate_documentation(handler_type, handler) or {