From 0ef28b6cfe8e7a91123def20f74d2e566440d2ea Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 14 Sep 2025 18:01:29 +0300 Subject: [PATCH 1/3] fix: Correct AGEClient method receivers and error syntax - Change AGEClient method receivers from immutable to mutable - Remove unnecessary `!` error propagation operators --- examples/hero/crypt/hero_crypt_example.vsh | 4 ++-- lib/hero/crypt/age.v | 26 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/hero/crypt/hero_crypt_example.vsh b/examples/hero/crypt/hero_crypt_example.vsh index 797f110e..47957534 100755 --- a/examples/hero/crypt/hero_crypt_example.vsh +++ b/examples/hero/crypt/hero_crypt_example.vsh @@ -38,7 +38,7 @@ signing_keypair := age_client.generate_signing_keypair() or { 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}') return } @@ -47,7 +47,7 @@ verified := age_client.verify(signing_keypair.verify_key, message, signed.signat println('Error verifying signature: ${err}') return } -!println('Message: ${message}') +println('Message: ${message}') println('Signature: ${signed.signature}') println('Signature valid: ${verified}') diff --git a/lib/hero/crypt/age.v b/lib/hero/crypt/age.v index 5b8159ef..0245e220 100644 --- a/lib/hero/crypt/age.v +++ b/lib/hero/crypt/age.v @@ -5,7 +5,7 @@ import freeflowuniverse.herolib.core.redisclient // Stateless AGE operations // 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'])! if response.len < 2 { @@ -19,7 +19,7 @@ pub fn (client &AGEClient) generate_keypair() !KeyPair { } // 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'])! if response.len < 2 { @@ -33,7 +33,7 @@ pub fn (client &AGEClient) generate_signing_keypair() !SigningKeyPair { } // 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])! 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) -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])! } // 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])! return SignatureResult{ @@ -56,7 +56,7 @@ pub fn (client &AGEClient) sign(sign_key string, message string) !SignatureResul } // 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])! return result == 1 } @@ -64,7 +64,7 @@ pub fn (client &AGEClient) verify(verify_key string, message string, signature s // Key-managed AGE operations // 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])! if response.len < 2 { @@ -78,7 +78,7 @@ pub fn (client &AGEClient) create_named_keypair(name string) !KeyPair { } // 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])! if response.len < 2 { @@ -92,7 +92,7 @@ pub fn (client &AGEClient) create_named_signing_keypair(name string) !SigningKey } // 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])! 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 -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])! } // 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])! return SignatureResult{ @@ -115,13 +115,13 @@ 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 -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])! return result == 1 } // 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'])! mut keys := []string{} From 92c8a3b955b2ff22e9d69334a37affa675fa152f Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 14 Sep 2025 18:15:23 +0300 Subject: [PATCH 2/3] fix: improve Redis response parsing and error handling - Add error handling for non-array and error responses - Introduce `strget()` for safer string conversion from RValue - Update AGE client to use `strget()` for key retrieval - Change AGE verify methods to expect a string response - Handle multiple response types when listing AGE keys --- lib/core/redisclient/redisclient_send.v | 11 ++++++ lib/data/resp/resp_model.v | 12 +++++-- lib/hero/crypt/age.v | 45 ++++++++++++++++++------- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/lib/core/redisclient/redisclient_send.v b/lib/core/redisclient/redisclient_send.v index 3e63738c..77748642 100644 --- a/lib/core/redisclient/redisclient_send.v +++ b/lib/core/redisclient/redisclient_send.v @@ -83,5 +83,16 @@ pub fn (mut r Redis) send_expect_list(items []string) ![]resp.RValue { } r.write_cmds(items)! 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) } diff --git a/lib/data/resp/resp_model.v b/lib/data/resp/resp_model.v index 236e2355..8a5cf633 100644 --- a/lib/data/resp/resp_model.v +++ b/lib/data/resp/resp_model.v @@ -103,7 +103,7 @@ pub fn (v RValue) u32() u32 { pub fn (v RValue) strget() string { match v { RInt { - return v.str() + return v.value.str() } // RArray{ // return v.str() @@ -112,10 +112,16 @@ pub fn (v RValue) strget() string { return v.value.bytestr() } RString { - return v.str() + return v.value + } + RError { + return v.value + } + RNil { + return '' } else { - panic('could not find type') + panic('could not find type: ${v.type_name()}') } } } diff --git a/lib/hero/crypt/age.v b/lib/hero/crypt/age.v index 0245e220..c51e0849 100644 --- a/lib/hero/crypt/age.v +++ b/lib/hero/crypt/age.v @@ -1,6 +1,7 @@ module crypt import freeflowuniverse.herolib.core.redisclient +import freeflowuniverse.herolib.data.resp // Stateless AGE operations @@ -13,8 +14,8 @@ pub fn (mut client AGEClient) generate_keypair() !KeyPair { } return KeyPair{ - recipient: response[0].str() - identity: response[1].str() + recipient: response[0].strget() + identity: response[1].strget() } } @@ -27,8 +28,8 @@ pub fn (mut client AGEClient) generate_signing_keypair() !SigningKeyPair { } return SigningKeyPair{ - verify_key: response[0].str() - sign_key: response[1].str() + verify_key: response[0].strget() + sign_key: response[1].strget() } } @@ -57,8 +58,8 @@ pub fn (mut client AGEClient) sign(sign_key string, message string) !SignatureRe // verify verifies a signature with the verification key 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])! - return result == 1 + result := client.redis.send_expect_str(['AGE', 'VERIFY', verify_key, message, signature])! + return result == '1' } // Key-managed AGE operations @@ -72,8 +73,8 @@ pub fn (mut client AGEClient) create_named_keypair(name string) !KeyPair { } return KeyPair{ - recipient: response[0].str() - identity: response[1].str() + recipient: response[0].strget() + identity: response[1].strget() } } @@ -86,8 +87,8 @@ pub fn (mut client AGEClient) create_named_signing_keypair(name string) !Signing } return SigningKeyPair{ - verify_key: response[0].str() - sign_key: response[1].str() + verify_key: response[0].strget() + sign_key: response[1].strget() } } @@ -116,8 +117,8 @@ pub fn (mut client AGEClient) sign_with_named_key(key_name string, message strin // verify_with_named_key verifies a signature using a named verification key 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])! - return result == 1 + result := client.redis.send_expect_str(['AGE', 'VERIFYNAME', key_name, message, signature])! + return result == '1' } // list_keys lists all stored AGE keys @@ -126,7 +127,25 @@ pub fn (mut client AGEClient) list_keys() ![]string { mut keys := []string{} 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 From ad906b5894557d7d5b22707fc0d9d99f8b19942b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Sun, 14 Sep 2025 18:24:30 +0300 Subject: [PATCH 3/3] refactor: Simplify handler signatures and add server runner - Pass URL params as direct arguments to handlers - Use `ctx.get_custom_header` to retrieve session key - Add a runnable script to start the heroserver - Clean up formatting in documentation and code - Remove unused redisclient import --- examples/hero/heroserver/heroserver.vsh | 6 +++++ lib/hero/crypt/age.v | 1 - lib/hero/heroserver/ai_instructions_specs.md | 28 +++++++++----------- lib/hero/heroserver/factory.v | 18 ++++++------- lib/hero/heroserver/server.v | 14 +++------- 5 files changed, 30 insertions(+), 37 deletions(-) create mode 100755 examples/hero/heroserver/heroserver.vsh diff --git a/examples/hero/heroserver/heroserver.vsh b/examples/hero/heroserver/heroserver.vsh new file mode 100755 index 00000000..d7432388 --- /dev/null +++ b/examples/hero/heroserver/heroserver.vsh @@ -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()! diff --git a/lib/hero/crypt/age.v b/lib/hero/crypt/age.v index c51e0849..d98764eb 100644 --- a/lib/hero/crypt/age.v +++ b/lib/hero/crypt/age.v @@ -1,6 +1,5 @@ module crypt -import freeflowuniverse.herolib.core.redisclient import freeflowuniverse.herolib.data.resp // Stateless AGE operations diff --git a/lib/hero/heroserver/ai_instructions_specs.md b/lib/hero/heroserver/ai_instructions_specs.md index ef060614..a848a2d4 100644 --- a/lib/hero/heroserver/ai_instructions_specs.md +++ b/lib/hero/heroserver/ai_instructions_specs.md @@ -6,29 +6,27 @@ and the implementation of the example of how webserver is see lib/schemas/openrp 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 +- 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 + - 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 - diff --git a/lib/hero/heroserver/factory.v b/lib/hero/heroserver/factory.v index 5980591d..078c8e2e 100644 --- a/lib/hero/heroserver/factory.v +++ b/lib/hero/heroserver/factory.v @@ -1,19 +1,17 @@ module heroserver - @[params] pub struct ServerConfig { pub: - port int = 8080 - host string = 'localhost' + 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 -} \ No newline at end of file + return &HeroServer{ + config: config + auth_manager: new_auth_manager() + handler_registry: new_handler_registry() + } +} diff --git a/lib/hero/heroserver/server.v b/lib/hero/heroserver/server.v index 96b776e0..f400c147 100644 --- a/lib/hero/heroserver/server.v +++ b/lib/hero/heroserver/server.v @@ -40,13 +40,9 @@ pub fn (mut s HeroServer) auth(mut ctx Context) veb.Result { // 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') - } - +pub fn (mut s HeroServer) api(mut ctx Context, handler_type string) veb.Result { // 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) { return ctx.request_error('Invalid session') } @@ -65,11 +61,7 @@ pub fn (mut s HeroServer) api(mut ctx Context) veb.Result { // 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') - } - +pub fn (mut s HeroServer) doc(mut ctx Context, handler_type string) veb.Result { handler := s.handler_registry.get(handler_type) or { return ctx.not_found() } doc_html := s.generate_documentation(handler_type, handler) or {