Rewrite jwt-pitbul and boko-framework with modern Bux constructs

- jwt-pitbul: algebraic enums, Array<String>, methods on JwtAlg,
  string interpolation, structured Result/Option error handling.
- boko-framework: StringMap<String> for headers/query/path params,
  Request/Response methods, StringBuilder, for loops, algebraic enums.
- Update READMEs and bump versions to 0.2.0.
This commit is contained in:
2026-06-16 10:12:52 +03:00
parent aaeb01e518
commit 9c632ce389
7 changed files with 570 additions and 458 deletions
+19 -14
View File
@@ -4,12 +4,14 @@
Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic. Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic.
This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap<String>` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation.
## Quick Start ## Quick Start
```bash ```bash
cd apps/boko-framework cd apps/boko-framework
../../buxc build ../../buxc build
./boko-framework ./build/boko-framework
``` ```
Open `http://localhost:8080` — you'll see the demo landing page. Open `http://localhost:8080` — you'll see the demo landing page.
@@ -20,7 +22,7 @@ Boko follows a **single-dispatch-function** pattern. You define one function —
```bux ```bux
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound, import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
Path_Match, Request_GetQuery, Request_GetPathParam, Path_Match,
App, App_New, App_Run}; App, App_New, App_Run};
func Boko_Router(req: Request) -> Response { func Boko_Router(req: Request) -> Response {
@@ -36,14 +38,14 @@ func Boko_Router(req: Request) -> Response {
// Route: GET /users/{id} — path parameter // Route: GET /users/{id} — path parameter
if Path_Match("/users/{id}", req.path, &req) { if Path_Match("/users/{id}", req.path, &req) {
let id: String = Request_GetPathParam(&req, "id"); let id: String = req.GetPathParam("id");
// Build JSON response with id // Build JSON response with id
return Response_Json(...); return Response_Json(...);
} }
// Route: GET /search?q=... — query parameter // Route: GET /search?q=... — query parameter
if String_Eq(req.path, "/search") { if String_Eq(req.path, "/search") {
let q: String = Request_GetQuery(&req, "q"); let q: String = req.GetQuery("q");
return Response_Json(...); return Response_Json(...);
} }
@@ -63,16 +65,19 @@ func Main() -> int {
| Field | Type | Description | | Field | Type | Description |
|-------|------|-------------| |-------|------|-------------|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE | | `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
| `path` | `String` | Request path (without query string) | | `path` | `String` | Request path (without query string) |
| `body` | `String` | Request body (for POST/PUT) | | `body` | `String` | Request body (for POST/PUT) |
| `headerCount` | `int` | Number of headers | | `headers` | `StringMap<String>` | Parsed request headers |
| `query` | `StringMap<String>` | Parsed query parameters |
| `pathParams` | `StringMap<String>` | Extracted path parameters |
| Function | Returns | Description | | Method | Returns | Description |
|----------|---------|-------------| |--------|---------|-------------|
| `Request_GetHeader(req, name)` | `String` | Get header value by name | | `req.GetHeader(name)` | `String` | Get header value by name |
| `Request_GetQuery(req, name)` | `String` | Get query parameter by name | | `req.GetQuery(name)` | `String` | Get query parameter by name |
| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter | | `req.HasQuery(name)` | `bool` | Check if query parameter exists |
| `req.GetPathParam(name)` | `String` | Get extracted path parameter |
### Response ### Response
@@ -96,12 +101,12 @@ func Main() -> int {
// Extracts: id=42, postId=7 // Extracts: id=42, postId=7
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) { if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
let userId: String = Request_GetPathParam(&req, "id"); // "42" let userId: String = req.GetPathParam("id"); // "42"
let postId: String = Request_GetPathParam(&req, "postId"); // "7" let postId: String = req.GetPathParam("postId"); // "7"
} }
``` ```
The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`. The function returns `true` if the pattern matches and populates `req.pathParams`.
### App ### App
+1 -1
View File
@@ -1,6 +1,6 @@
[Package] [Package]
Name = "boko-framework" Name = "boko-framework"
Version = "0.1.0" Version = "0.2.0"
Type = "bin" Type = "bin"
Description = "Boko — async web framework for Bux, inspired by FastAPI" Description = "Boko — async web framework for Bux, inspired by FastAPI"
+199 -231
View File
@@ -1,28 +1,22 @@
// ============================================================================= // =============================================================================
// Boko — Async Web Framework for Bux (inspired by FastAPI) // Boko — Async Web Framework for Bux (inspired by FastAPI)
// Rewritten with modern Bux: methods on structs, generic StringMap, for-in
// loops, algebraic enums, StringBuilder, and string interpolation.
// ============================================================================= // =============================================================================
module Boko { module Boko {
import Std::Io::{PrintLine, Print, PrintInt}; import Std::Io::{PrintLine, Print, PrintInt};
import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError};
import Std::String::{String_Len, String_Eq, String_StartsWith, String_Contains, String_Offset}; import Std::String::{
import Std::Task::{Task_Join, TaskHandle}; String_Len, String_Eq, String_IsNull,
String_StartsWith, String_Contains,
String_Find, String_Offset, String_Slice,
String_SplitCount, String_SplitPart,
StringBuilder, StringBuilder_New, StringBuilder_Append,
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
};
import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has};
extern func bux_alloc(size: uint) -> *void;
extern func bux_strlen(s: String) -> uint;
extern func bux_strcmp(a: String, b: String) -> int;
extern func bux_strstr(haystack: String, needle: String) -> String;
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
extern func bux_str_split_count(s: String, delim: String) -> uint;
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
extern func bux_sb_new(initial_cap: uint) -> *void;
extern func bux_sb_append(sb: *void, s: String);
extern func bux_sb_append_int(sb: *void, n: int64);
extern func bux_sb_append_char(sb: *void, c: char8);
extern func bux_sb_build(sb: *void) -> String;
extern func bux_sb_free(sb: *void);
extern func bux_base64url_encode(data: String, len: int) -> String;
extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String;
// ============================================================================= // =============================================================================
// HTTP Methods // HTTP Methods
@@ -37,6 +31,28 @@ enum HttpVerb {
OPTIONS, OPTIONS,
} }
func HttpVerb_MethodName(verb: HttpVerb) -> String {
if verb.tag == HttpVerb_GET { return "GET"; }
if verb.tag == HttpVerb_POST { return "POST"; }
if verb.tag == HttpVerb_PUT { return "PUT"; }
if verb.tag == HttpVerb_DELETE { return "DELETE"; }
if verb.tag == HttpVerb_PATCH { return "PATCH"; }
if verb.tag == HttpVerb_HEAD { return "HEAD"; }
if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; }
return "?";
}
func HttpVerb_Parse(methodStr: String) -> HttpVerb {
if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; }
if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; }
if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; }
if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; }
if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; }
if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; }
if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; }
return HttpVerb { tag: HttpVerb_GET };
}
// ============================================================================= // =============================================================================
// Request — parsed incoming HTTP request // Request — parsed incoming HTTP request
// ============================================================================= // =============================================================================
@@ -44,15 +60,36 @@ struct Request {
method: HttpVerb, method: HttpVerb,
path: String, path: String,
body: String, body: String,
headerKeys: *String, headers: StringMap<String>,
headerValues: *String, query: StringMap<String>,
headerCount: int, pathParams: StringMap<String>,
queryKeys: *String, }
queryValues: *String,
queryCount: int, extend Request {
pathParamKeys: *String, func GetHeader(self: Request, name: String) -> String {
pathParamValues: *String, if StringMap_Has<String>(&self.headers, name) {
pathParamCount: int, return StringMap_Get<String>(&self.headers, name);
}
return "";
}
func GetQuery(self: Request, name: String) -> String {
if StringMap_Has<String>(&self.query, name) {
return StringMap_Get<String>(&self.query, name);
}
return "";
}
func HasQuery(self: Request, name: String) -> bool {
return StringMap_Has<String>(&self.query, name);
}
func GetPathParam(self: Request, name: String) -> String {
if StringMap_Has<String>(&self.pathParams, name) {
return StringMap_Get<String>(&self.pathParams, name);
}
return "";
}
} }
// ============================================================================= // =============================================================================
@@ -65,63 +102,9 @@ struct Response {
extraHeaders: String, extraHeaders: String,
} }
// ============================================================================= // --- Constructors ---
// App — server instance
// =============================================================================
struct App {
port: int,
threadCount: int,
serverName: String,
}
// =============================================================================
// Request helpers
// =============================================================================
func Request_GetHeader(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.headerCount {
if bux_strcmp(req.headerKeys[i], name) == 0 {
return req.headerValues[i];
}
i = i + 1;
}
return "";
}
func Request_GetQuery(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.queryCount {
if bux_strcmp(req.queryKeys[i], name) == 0 {
return req.queryValues[i];
}
i = i + 1;
}
return "";
}
func Request_GetPathParam(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.pathParamCount {
if bux_strcmp(req.pathParamKeys[i], name) == 0 {
return req.pathParamValues[i];
}
i = i + 1;
}
return "";
}
// =============================================================================
// Response constructors
// =============================================================================
func Response_New(status: int, contentType: String, body: String) -> Response { func Response_New(status: int, contentType: String, body: String) -> Response {
var resp: Response; return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
resp.statusCode = status;
resp.contentType = contentType;
resp.body = body;
resp.extraHeaders = "";
return resp;
} }
func Response_Ok(body: String) -> Response { func Response_Ok(body: String) -> Response {
@@ -141,118 +124,143 @@ func Response_Text(text: String) -> Response {
} }
func Response_Redirect(url: String) -> Response { func Response_Redirect(url: String) -> Response {
var resp: Response; let sb: StringBuilder = StringBuilder_New();
resp.statusCode = 302; StringBuilder_Append(&sb, "Location: ");
resp.contentType = ""; StringBuilder_Append(&sb, url);
resp.body = ""; StringBuilder_Append(&sb, "\r\n");
let sb: *void = bux_sb_new(256); let headers: String = StringBuilder_Build(&sb);
bux_sb_append(sb, "Location: "); StringBuilder_Free(&sb);
bux_sb_append(sb, url); return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers };
bux_sb_append(sb, "\r\n");
resp.extraHeaders = bux_sb_build(sb);
bux_sb_free(sb);
return resp;
} }
func Response_NotFound() -> Response { func Response_NotFound() -> Response {
return Response_New(404, "application/json; charset=utf-8", return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
"{\"error\":\"not_found\"}");
} }
func Response_Error(status: int, message: String) -> Response { func Response_Error(status: int, message: String) -> Response {
return Response_New(status, "application/json; charset=utf-8", message); let sb: StringBuilder = StringBuilder_New();
StringBuilder_Append(&sb, "{\"error\":\"");
StringBuilder_Append(&sb, message);
StringBuilder_Append(&sb, "\"}");
let body: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return Response_New(status, "application/json; charset=utf-8", body);
} }
func Response_NoContent() -> Response { func Response_NoContent() -> Response {
return Response_New(204, "", ""); return Response_New(204, "", "");
} }
// --- Instance helpers ---
extend Response {
func WithHeader(self: Response, key: String, value: String) -> Response {
let sb: StringBuilder = StringBuilder_New();
StringBuilder_Append(&sb, self.extraHeaders);
StringBuilder_Append(&sb, key);
StringBuilder_Append(&sb, ": ");
StringBuilder_Append(&sb, value);
StringBuilder_Append(&sb, "\r\n");
self.extraHeaders = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return self;
}
}
// =============================================================================
// Status phrase lookup
// =============================================================================
func StatusPhrase(code: int) -> String {
if code == 200 { return "OK"; }
if code == 201 { return "Created"; }
if code == 204 { return "No Content"; }
if code == 301 { return "Moved Permanently"; }
if code == 302 { return "Found"; }
if code == 400 { return "Bad Request"; }
if code == 401 { return "Unauthorized"; }
if code == 403 { return "Forbidden"; }
if code == 404 { return "Not Found"; }
if code == 500 { return "Internal Server Error"; }
if code == 502 { return "Bad Gateway"; }
if code == 503 { return "Service Unavailable"; }
return "OK";
}
// ============================================================================= // =============================================================================
// Build HTTP response string from Response struct // Build HTTP response string from Response struct
// ============================================================================= // =============================================================================
func Response_Build(resp: Response) -> String { func Response_Build(resp: Response) -> String {
let sb: *void = bux_sb_new(2048); let sb: StringBuilder = StringBuilder_New();
// Status line // Status line
bux_sb_append(sb, "HTTP/1.1 "); StringBuilder_Append(&sb, "HTTP/1.1 ");
bux_sb_append_int(sb, resp.statusCode as int64); StringBuilder_AppendInt(&sb, resp.statusCode as int64);
bux_sb_append(sb, " "); StringBuilder_Append(&sb, " ");
if resp.statusCode == 200 { bux_sb_append(sb, "OK"); } StringBuilder_Append(&sb, StatusPhrase(resp.statusCode));
else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); } StringBuilder_Append(&sb, "\r\n");
else if resp.statusCode == 204 { bux_sb_append(sb, "No Content"); }
else if resp.statusCode == 302 { bux_sb_append(sb, "Found"); }
else if resp.statusCode == 400 { bux_sb_append(sb, "Bad Request"); }
else if resp.statusCode == 404 { bux_sb_append(sb, "Not Found"); }
else if resp.statusCode == 500 { bux_sb_append(sb, "Internal Server Error"); }
else { bux_sb_append(sb, "OK"); }
bux_sb_append(sb, "\r\n");
// Server // Server
bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n"); StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n");
// Extra headers (redirects, CORS, etc.) // Extra headers
if bux_strlen(resp.extraHeaders) > 0 { if String_Len(resp.extraHeaders) > 0 {
bux_sb_append(sb, resp.extraHeaders); StringBuilder_Append(&sb, resp.extraHeaders);
} }
// Content-Type // Content-Type
if bux_strlen(resp.contentType) > 0 { if String_Len(resp.contentType) > 0 {
bux_sb_append(sb, "Content-Type: "); StringBuilder_Append(&sb, "Content-Type: ");
bux_sb_append(sb, resp.contentType); StringBuilder_Append(&sb, resp.contentType);
bux_sb_append(sb, "\r\n"); StringBuilder_Append(&sb, "\r\n");
} }
// Content-Length // Content-Length
let bodyLen: uint = bux_strlen(resp.body); let bodyLen: uint = String_Len(resp.body);
bux_sb_append(sb, "Content-Length: "); StringBuilder_Append(&sb, "Content-Length: ");
bux_sb_append_int(sb, bodyLen as int64); StringBuilder_AppendInt(&sb, bodyLen as int64);
bux_sb_append(sb, "\r\n"); StringBuilder_Append(&sb, "\r\n");
// Connection close // Connection close
bux_sb_append(sb, "Connection: close\r\n"); StringBuilder_Append(&sb, "Connection: close\r\n");
StringBuilder_Append(&sb, "\r\n");
bux_sb_append(sb, "\r\n");
if bodyLen > 0 { if bodyLen > 0 {
bux_sb_append(sb, resp.body); StringBuilder_Append(&sb, resp.body);
} }
let result: String = bux_sb_build(sb); let result: String = StringBuilder_Build(&sb);
bux_sb_free(sb); StringBuilder_Free(&sb);
return result; return result;
} }
// ============================================================================= // =============================================================================
// Parse query string: ?key=val&key2=val2 → key-value arrays // Parse query string: ?key=val&key2=val2 → generic StringMap
// ============================================================================= // =============================================================================
func Query_Parse(req: *Request, queryString: String) { func Query_Parse(queryString: String) -> StringMap<String> {
if bux_strlen(queryString) == 0 { return; } let map: StringMap<String> = StringMap_New<String>(8);
let pairCount: uint = bux_str_split_count(queryString, "&"); if String_Len(queryString) == 0 {
let cap: int = pairCount as int + 1; return map;
req.queryKeys = bux_alloc(cap as uint * 8) as *String; }
req.queryValues = bux_alloc(cap as uint * 8) as *String;
req.queryCount = 0;
let pairCount: uint = String_SplitCount(queryString, "&");
var i: uint = 0; var i: uint = 0;
while i < pairCount { while i < pairCount {
let pair: String = bux_str_split_part(queryString, "&", i); let pair: String = String_SplitPart(queryString, "&", i);
let eqPos: String = bux_strstr(pair, "="); let eqPos: String = String_Find(pair, "=");
var key: String = pair; var key: String = pair;
var value: String = ""; var value: String = "";
if String_Len(eqPos) > 0 { if !String_IsNull(eqPos) {
let keyLen: uint = String_Offset(eqPos, pair); let keyLen: uint = String_Offset(eqPos, pair);
key = bux_str_slice(pair, 0, keyLen); key = String_Slice(pair, 0, keyLen);
let valStart: uint = keyLen + 1; let valStart: uint = keyLen + 1;
let pairLen: uint = bux_strlen(pair); let pairLen: uint = String_Len(pair);
if valStart < pairLen { if valStart < pairLen {
value = bux_str_slice(pair, valStart, pairLen - valStart); value = String_Slice(pair, valStart, pairLen - valStart);
} }
} }
req.queryKeys[req.queryCount] = key; StringMap_Set<String>(&map, key, value);
req.queryValues[req.queryCount] = value;
req.queryCount = req.queryCount + 1;
i = i + 1; i = i + 1;
} }
return map;
} }
// ============================================================================= // =============================================================================
@@ -263,50 +271,45 @@ func Request_Parse(raw: String) -> Request {
req.method = HttpVerb { tag: HttpVerb_GET }; req.method = HttpVerb { tag: HttpVerb_GET };
req.path = "/"; req.path = "/";
req.body = ""; req.body = "";
req.headerCount = 0; req.headers = StringMap_New<String>(8);
req.queryCount = 0; req.query = StringMap_New<String>(4);
req.pathParamCount = 0; req.pathParams = StringMap_New<String>(4);
if String_Len(raw) == 0 { return req; } if String_Len(raw) == 0 { return req; }
let rawLen: uint = bux_strlen(raw);
if rawLen == 0 { return req; }
// Find header/body boundary // Find header/body boundary
let boundary: String = bux_strstr(raw, "\r\n\r\n"); let boundary: String = String_Find(raw, "\r\n\r\n");
let rawLen: uint = String_Len(raw);
var headerLen: uint = rawLen; var headerLen: uint = rawLen;
if String_Len(boundary) > 0 { if !String_IsNull(boundary) {
headerLen = String_Offset(boundary, raw); headerLen = String_Offset(boundary, raw);
let bodyStart: uint = headerLen + 4; let bodyStart: uint = headerLen + 4;
if bodyStart < rawLen { if bodyStart < rawLen {
req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart); req.body = String_Slice(raw, bodyStart, rawLen - bodyStart);
} }
} }
if headerLen == 0 { return req; } if headerLen == 0 { return req; }
let headerBlock: String = bux_str_slice(raw, 0, headerLen); let headerBlock: String = String_Slice(raw, 0, headerLen);
let lineCount: uint = bux_str_split_count(headerBlock, "\r\n"); let lineCount: uint = String_SplitCount(headerBlock, "\r\n");
if lineCount == 0 { return req; } if lineCount == 0 { return req; }
// Parse request line // Parse request line
let requestLine: String = bux_str_split_part(headerBlock, "\r\n", 0); let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0);
if bux_strlen(requestLine) > 0 { if String_Len(requestLine) > 0 {
let methodStr: String = bux_str_split_part(requestLine, " ", 0); let methodStr: String = String_SplitPart(requestLine, " ", 0);
if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; } req.method = HttpVerb_Parse(methodStr);
else if String_Eq(methodStr, "POST") { req.method = HttpVerb { tag: HttpVerb_POST }; }
else if String_Eq(methodStr, "PUT") { req.method = HttpVerb { tag: HttpVerb_PUT }; }
else if String_Eq(methodStr, "DELETE") { req.method = HttpVerb { tag: HttpVerb_DELETE }; }
let fullPath: String = bux_str_split_part(requestLine, " ", 1); let fullPath: String = String_SplitPart(requestLine, " ", 1);
// Split path and query string let qmark: String = String_Find(fullPath, "?");
let qmark: String = bux_strstr(fullPath, "?"); if !String_IsNull(qmark) {
if String_Len(qmark) > 0 {
let pathLen: uint = String_Offset(qmark, fullPath); let pathLen: uint = String_Offset(qmark, fullPath);
req.path = bux_str_slice(fullPath, 0, pathLen); req.path = String_Slice(fullPath, 0, pathLen);
let qsStart: uint = pathLen + 1; let qsStart: uint = pathLen + 1;
let fullLen: uint = bux_strlen(fullPath); let fullLen: uint = String_Len(fullPath);
if qsStart < fullLen { if qsStart < fullLen {
let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart); let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart);
Query_Parse(&req, qs); req.query = Query_Parse(qs);
} }
} else { } else {
req.path = fullPath; req.path = fullPath;
@@ -314,26 +317,20 @@ func Request_Parse(raw: String) -> Request {
} }
// Parse headers // Parse headers
let cap: int = lineCount as int + 1;
req.headerKeys = bux_alloc(cap as uint * 8) as *String;
req.headerValues = bux_alloc(cap as uint * 8) as *String;
var i: uint = 1; var i: uint = 1;
while i < lineCount { while i < lineCount {
let line: String = bux_str_split_part(headerBlock, "\r\n", i); let line: String = String_SplitPart(headerBlock, "\r\n", i);
let colonPos: String = bux_strstr(line, ": "); let colonPos: String = String_Find(line, ": ");
if String_Len(colonPos) > 0 { if !String_IsNull(colonPos) {
let keyLen: uint = String_Offset(colonPos, line); let keyLen: uint = String_Offset(colonPos, line);
let key: String = bux_str_slice(line, 0, keyLen); let key: String = String_Slice(line, 0, keyLen);
let valStart: uint = keyLen + 2; let valStart: uint = keyLen + 2;
let lineLen: uint = bux_strlen(line); let lineLen: uint = String_Len(line);
var value: String = ""; var value: String = "";
if valStart < lineLen { if valStart < lineLen {
value = bux_str_slice(line, valStart, lineLen - valStart); value = String_Slice(line, valStart, lineLen - valStart);
} }
req.headerKeys[req.headerCount] = key; StringMap_Set<String>(&req.headers, key, value);
req.headerValues[req.headerCount] = value;
req.headerCount = req.headerCount + 1;
} }
i = i + 1; i = i + 1;
} }
@@ -345,48 +342,23 @@ func Request_Parse(raw: String) -> Request {
// Path pattern matching: /users/{id} against /users/42 → extracts id=42 // Path pattern matching: /users/{id} against /users/42 → extracts id=42
// ============================================================================= // =============================================================================
func Path_Match(pattern: String, path: String, req: *Request) -> bool { func Path_Match(pattern: String, path: String, req: *Request) -> bool {
// Split both by "/" let patParts: uint = String_SplitCount(pattern, "/");
let patParts: uint = bux_str_split_count(pattern, "/"); let pathParts: uint = String_SplitCount(path, "/");
let pathParts: uint = bux_str_split_count(path, "/");
if patParts != pathParts { return false; } if patParts != pathParts { return false; }
// Count params to allocate req.pathParams = StringMap_New<String>(8);
var paramCount: int = 0;
var pi: uint = 0;
while pi < patParts {
let patPart: String = bux_str_split_part(pattern, "/", pi);
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
paramCount = paramCount + 1;
}
pi = pi + 1;
}
let cap: int = paramCount + 1;
req.pathParamKeys = bux_alloc(cap as uint * 8) as *String;
req.pathParamValues = bux_alloc(cap as uint * 8) as *String;
req.pathParamCount = 0;
// Match and extract
var i: uint = 0; var i: uint = 0;
while i < patParts { while i < patParts {
let patPart: String = bux_str_split_part(pattern, "/", i); let patPart: String = String_SplitPart(pattern, "/", i);
let pathPart: String = bux_str_split_part(path, "/", i); let pathPart: String = String_SplitPart(path, "/", i);
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
// Extract param name from {name} let nameLen: uint = String_Len(patPart) - 2;
let braceOpen: uint = 1; // skip "{" let name: String = String_Slice(patPart, 1, nameLen);
let braceClose: String = bux_strstr(patPart, "}"); StringMap_Set<String>(&req.pathParams, name, pathPart);
var nameLen: uint = 0;
if String_Len(braceClose) > 0 {
nameLen = String_Offset(braceClose, patPart) - 1;
}
let name: String = bux_str_slice(patPart, braceOpen, nameLen);
req.pathParamKeys[req.pathParamCount] = name;
req.pathParamValues[req.pathParamCount] = pathPart;
req.pathParamCount = req.pathParamCount + 1;
} else { } else {
// Literal match
if !String_Eq(patPart, pathPart) { return false; } if !String_Eq(patPart, pathPart) { return false; }
} }
i = i + 1; i = i + 1;
@@ -396,14 +368,16 @@ func Path_Match(pattern: String, path: String, req: *Request) -> bool {
} }
// ============================================================================= // =============================================================================
// App constructor // App — server instance
// ============================================================================= // =============================================================================
struct App {
port: int,
threadCount: int,
serverName: String,
}
func App_New(port: int, threadCount: int) -> App { func App_New(port: int, threadCount: int) -> App {
var app: App; return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" };
app.port = port;
app.threadCount = threadCount;
app.serverName = "Boko/0.1.0 (Bux)";
return app;
} }
// ============================================================================= // =============================================================================
@@ -417,19 +391,13 @@ func Boko_Router(req: Request) -> Response;
func App_HandleConnection(clientFd: int) { func App_HandleConnection(clientFd: int) {
let raw: String = Net_Recv(clientFd, 8192); let raw: String = Net_Recv(clientFd, 8192);
if String_Len(raw) == 0 { return; } if String_Len(raw) == 0 { return; }
if bux_strlen(raw) == 0 { return; }
let req: Request = Request_Parse(raw); let req: Request = Request_Parse(raw);
// Call user-defined dispatch (forward declaration — user must implement)
let resp: Response = Boko_Router(req); let resp: Response = Boko_Router(req);
// Log // Log
if req.method.tag == HttpVerb_GET { Print("GET "); } Print(HttpVerb_MethodName(req.method));
else if req.method.tag == HttpVerb_POST { Print("POST "); } Print(" ");
else if req.method.tag == HttpVerb_PUT { Print("PUT "); }
else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); }
else { Print("? "); }
Print(req.path); Print(req.path);
Print(" → "); Print(" → ");
PrintInt(resp.statusCode); PrintInt(resp.statusCode);
@@ -457,9 +425,9 @@ func App_Worker(serverFd: int) {
// ============================================================================= // =============================================================================
func App_Run(app: *App) { func App_Run(app: *App) {
PrintLine("╔══════════════════════════════════════╗"); PrintLine("╔══════════════════════════════════════╗");
PrintLine("║ Boko Framework v0.1.0 ║"); PrintLine("║ Boko Framework v0.2.0 ║");
PrintLine("║ Async web framework for Bux ║"); PrintLine("║ Async web framework for Bux ║");
PrintLine("╚══════════════════════════════════════╝"); PrintLine("╚══════════════════════════════════════════════════════╝");
PrintLine(""); PrintLine("");
let fd: int = Net_Create(); let fd: int = Net_Create();
+35 -43
View File
@@ -1,25 +1,22 @@
// ============================================================================= // =============================================================================
// Boko Example App — demonstrates the framework API // Boko Example App — demonstrates the modern framework API
// ============================================================================= // =============================================================================
module Main { module Main {
import Std::Io::{PrintLine, Print}; import Std::Io::{PrintLine, Print};
import Std::String::{String_Eq}; import Std::String::{
String_Eq, String_Len,
StringBuilder, StringBuilder_New, StringBuilder_Append,
StringBuilder_Build, StringBuilder_Free
};
import Boko::{ import Boko::{
App, App_New, App_Run, App, App_New, App_Run,
Request, Response, Request, Response,
Request_GetQuery, Request_GetPathParam,
Response_Html, Response_Json, Response_NotFound, Response_Redirect, Response_Html, Response_Json, Response_NotFound, Response_Redirect,
Path_Match, Path_Match,
HttpVerb HttpVerb
}; };
extern func bux_strlen(s: String) -> uint;
extern func bux_sb_new(initial_cap: uint) -> *void;
extern func bux_sb_append(sb: *void, s: String);
extern func bux_sb_build(sb: *void) -> String;
extern func bux_sb_free(sb: *void);
// ============================================================================= // =============================================================================
// Boko_Router — user-defined dispatch (called by the framework) // Boko_Router — user-defined dispatch (called by the framework)
// ============================================================================= // =============================================================================
@@ -31,7 +28,7 @@ func Boko_Router(req: Request) -> Response {
// --- GET /api/health --- // --- GET /api/health ---
if String_Eq(req.path, "/api/health") { if String_Eq(req.path, "/api/health") {
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}"); return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
} }
// --- GET /api/info --- // --- GET /api/info ---
@@ -41,43 +38,38 @@ func Boko_Router(req: Request) -> Response {
// --- GET /hello?name=World --- // --- GET /hello?name=World ---
if String_Eq(req.path, "/hello") { if String_Eq(req.path, "/hello") {
let name: String = Request_GetQuery(&req, "name"); var name: String = req.GetQuery("name");
if bux_strlen(name) == 0 { name = "World"; } if String_Len(name) == 0 { name = "World"; }
let sb: *void = bux_sb_new(256); let html: String = f"<h1>Hello, {name}!</h1>";
bux_sb_append(sb, "<h1>Hello, ");
bux_sb_append(sb, name);
bux_sb_append(sb, "!</h1>");
let html: String = bux_sb_build(sb);
bux_sb_free(sb);
return Response_Html(html); return Response_Html(html);
} }
// --- GET /users/{id} --- // --- GET /users/{id} ---
if Path_Match("/users/{id}", req.path, &req) { if Path_Match("/users/{id}", req.path, &req) {
let id: String = Request_GetPathParam(&req, "id"); let id: String = req.GetPathParam("id");
let sb: *void = bux_sb_new(128); let sb: StringBuilder = StringBuilder_New();
bux_sb_append(sb, "{\"id\":"); StringBuilder_Append(&sb, "{\"id\":");
bux_sb_append(sb, id); StringBuilder_Append(&sb, id);
bux_sb_append(sb, ",\"name\":\"User "); StringBuilder_Append(&sb, ",\"name\":\"User ");
bux_sb_append(sb, id); StringBuilder_Append(&sb, id);
bux_sb_append(sb, "\"}"); StringBuilder_Append(&sb, "\"}");
let json: String = bux_sb_build(sb); let json: String = StringBuilder_Build(&sb);
bux_sb_free(sb); StringBuilder_Free(&sb);
return Response_Json(json); return Response_Json(json);
} }
// --- GET /posts/{postId}/comments/{commentId} --- // --- GET /posts/{postId}/comments/{commentId} ---
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) { if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
let pid: String = Request_GetPathParam(&req, "postId"); let pid: String = req.GetPathParam("postId");
let cid: String = Request_GetPathParam(&req, "commentId"); let cid: String = req.GetPathParam("commentId");
let sb: *void = bux_sb_new(128); let sb: StringBuilder = StringBuilder_New();
bux_sb_append(sb, "{\"postId\":"); StringBuilder_Append(&sb, "{\"postId\":");
bux_sb_append(sb, pid); StringBuilder_Append(&sb, pid);
bux_sb_append(sb, ",\"commentId\":"); StringBuilder_Append(&sb, ",\"commentId\":");
bux_sb_append(sb, cid); StringBuilder_Append(&sb, cid);
bux_sb_append(sb, ",\"text\":\"Great post!\"}"); StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}");
let json: String = bux_sb_build(sb); let json: String = StringBuilder_Build(&sb);
bux_sb_free(sb); StringBuilder_Free(&sb);
return Response_Json(json); return Response_Json(json);
} }
@@ -88,12 +80,12 @@ func Boko_Router(req: Request) -> Response {
// --- POST /api/echo --- // --- POST /api/echo ---
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST { if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
let sb: *void = bux_sb_new(256); let sb: StringBuilder = StringBuilder_New();
bux_sb_append(sb, "{\"echo\":"); StringBuilder_Append(&sb, "{\"echo\":");
bux_sb_append(sb, req.body); StringBuilder_Append(&sb, req.body);
bux_sb_append(sb, "}"); StringBuilder_Append(&sb, "}");
let json: String = bux_sb_build(sb); let json: String = StringBuilder_Build(&sb);
bux_sb_free(sb); StringBuilder_Free(&sb);
return Response_Json(json); return Response_Json(json);
} }
+3 -1
View File
@@ -4,12 +4,14 @@
A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL. A standalone command-line utility for working with JSON Web Tokens (RFC 7519). Built on `Std::Crypto`, backed by OpenSSL.
This version (0.2.0) is rewritten with modern Bux: algebraic enums, generic `Array<String>`, methods on `JwtAlg`, string interpolation, `for` loops, and structured `Result`/`Option`-style error handling.
## Quick Start ## Quick Start
```bash ```bash
cd apps/jwt-pitbul cd apps/jwt-pitbul
../../buxc build ../../buxc build
./jwt-pitbul ./build/jwt-pitbul
``` ```
## Commands ## Commands
+1 -1
View File
@@ -1,6 +1,6 @@
[Package] [Package]
Name = "jwt-pitbul" Name = "jwt-pitbul"
Version = "0.1.0" Version = "0.2.0"
Type = "bin" Type = "bin"
Description = "JWT CLI tool — encode, decode, verify, and key generation" Description = "JWT CLI tool — encode, decode, verify, and key generation"
+312 -167
View File
@@ -1,11 +1,17 @@
// ============================================================================= // =============================================================================
// jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen) // jwt-pitbul — JWT CLI Tool (encode, decode, verify, keygen)
// Built with Bux + Std::Crypto // Rewritten with modern Bux: algebraic enums, methods, string interpolation,
// for-in loops, generic arrays, and structured error handling.
// ============================================================================= // =============================================================================
module Main { module Main {
import Std::Io::{PrintLine, Print, PrintInt}; import Std::Io::{PrintLine, Print, PrintInt};
import Std::String::{String_Len, String_Eq, String_Contains}; import Std::String::{
String_Len, String_Eq,
String_SplitCount, String_SplitPart,
StringBuilder, StringBuilder_New, StringBuilder_Append,
StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free
};
import Std::Crypto::Jwt::{ import Std::Crypto::Jwt::{
JwtAlg, JwtAlg,
Jwt_MakeHeader, Jwt_MakeHeader,
@@ -22,24 +28,191 @@ extern func bux_argv(index: int) -> String;
extern func bux_alloc(size: uint) -> *void; extern func bux_alloc(size: uint) -> *void;
extern func bux_read_file(path: String) -> String; extern func bux_read_file(path: String) -> String;
extern func bux_file_exists(path: String) -> int; extern func bux_file_exists(path: String) -> int;
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
extern func bux_str_split_count(s: String, delim: String) -> uint;
extern func bux_base64_encode(data: String, len: int) -> String; extern func bux_base64_encode(data: String, len: int) -> String;
// =============================================================================
// Constants
// =============================================================================
const AppName: String = "jwt-pitbul";
const Version: String = "0.2.0";
// =============================================================================
// Algebraic enums for optionals and results
// =============================================================================
enum AlgOption {
Some(JwtAlg),
None,
}
func AlgOption_MakeSome(value: JwtAlg) -> AlgOption {
let o: AlgOption = AlgOption { tag: AlgOption_Some };
o.data.Some_0 = value;
return o;
}
func AlgOption_MakeNone() -> AlgOption {
return AlgOption { tag: AlgOption_None };
}
enum CmdOption {
Some(Cmd),
None,
}
func CmdOption_MakeSome(value: Cmd) -> CmdOption {
let o: CmdOption = CmdOption { tag: CmdOption_Some };
o.data.Some_0 = value;
return o;
}
func CmdOption_MakeNone() -> CmdOption {
return CmdOption { tag: CmdOption_None };
}
enum KeyTypeOption {
Some(KeyType),
None,
}
func KeyTypeOption_MakeSome(value: KeyType) -> KeyTypeOption {
let o: KeyTypeOption = KeyTypeOption { tag: KeyTypeOption_Some };
o.data.Some_0 = value;
return o;
}
func KeyTypeOption_MakeNone() -> KeyTypeOption {
return KeyTypeOption { tag: KeyTypeOption_None };
}
enum KeyResult {
Ok(String),
Err(String),
}
func KeyResult_MakeOk(value: String) -> KeyResult {
let r: KeyResult = KeyResult { tag: KeyResult_Ok };
r.data.Ok_0 = value;
return r;
}
func KeyResult_MakeErr(msg: String) -> KeyResult {
let r: KeyResult = KeyResult { tag: KeyResult_Err };
r.data.Err_0 = msg;
return r;
}
enum ExitResult {
Ok(int),
Err(String),
}
func ExitResult_MakeOk(value: int) -> ExitResult {
let r: ExitResult = ExitResult { tag: ExitResult_Ok };
r.data.Ok_0 = value;
return r;
}
func ExitResult_MakeErr(msg: String) -> ExitResult {
let r: ExitResult = ExitResult { tag: ExitResult_Err };
r.data.Err_0 = msg;
return r;
}
// =============================================================================
// Commands
// =============================================================================
enum Cmd {
Sign,
Verify,
Decode,
Keygen,
Help,
}
func ParseCmd(name: String) -> CmdOption {
if String_Eq(name, "sign") { return CmdOption_MakeSome(Cmd { tag: Cmd_Sign }); }
if String_Eq(name, "verify") { return CmdOption_MakeSome(Cmd { tag: Cmd_Verify }); }
if String_Eq(name, "decode") { return CmdOption_MakeSome(Cmd { tag: Cmd_Decode }); }
if String_Eq(name, "keygen") { return CmdOption_MakeSome(Cmd { tag: Cmd_Keygen }); }
if String_Eq(name, "help") || String_Eq(name, "--help") || String_Eq(name, "-h") {
return CmdOption_MakeSome(Cmd { tag: Cmd_Help });
}
return CmdOption_MakeNone();
}
// =============================================================================
// Key type generation
// =============================================================================
enum KeyType {
Rsa,
Ecdsa,
Ed25519,
}
func ParseKeyType(name: String) -> KeyTypeOption {
if String_Eq(name, "rsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Rsa }); }
if String_Eq(name, "ecdsa") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ecdsa }); }
if String_Eq(name, "ed25519") { return KeyTypeOption_MakeSome(KeyType { tag: KeyType_Ed25519 }); }
return KeyTypeOption_MakeNone();
}
// =============================================================================
// Extend JwtAlg with parsing / introspection methods
// =============================================================================
extend JwtAlg {
func Name(self: JwtAlg) -> String {
match self {
JwtAlg::HS256 => "HS256",
JwtAlg::HS384 => "HS384",
JwtAlg::HS512 => "HS512",
JwtAlg::RS256 => "RS256",
JwtAlg::RS384 => "RS384",
JwtAlg::RS512 => "RS512",
JwtAlg::ES256 => "ES256",
JwtAlg::ES384 => "ES384",
JwtAlg::EdDSA => "EdDSA",
_ => "HS256",
}
}
func IsHmac(self: JwtAlg) -> bool {
return self.tag == JwtAlg_HS256 || self.tag == JwtAlg_HS384 || self.tag == JwtAlg_HS512;
}
func NeedsPemFile(self: JwtAlg) -> bool {
return self.tag == JwtAlg_RS256 || self.tag == JwtAlg_RS384 || self.tag == JwtAlg_RS512 ||
self.tag == JwtAlg_ES256 || self.tag == JwtAlg_ES384;
}
}
func ParseAlg(name: String) -> AlgOption {
if String_Eq(name, "HS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS256 }); }
if String_Eq(name, "HS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS384 }); }
if String_Eq(name, "HS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_HS512 }); }
if String_Eq(name, "RS256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS256 }); }
if String_Eq(name, "RS384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS384 }); }
if String_Eq(name, "RS512") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_RS512 }); }
if String_Eq(name, "ES256") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES256 }); }
if String_Eq(name, "ES384") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_ES384 }); }
if String_Eq(name, "EdDSA") { return AlgOption_MakeSome(JwtAlg { tag: JwtAlg_EdDSA }); }
return AlgOption_MakeNone();
}
// ============================================================================= // =============================================================================
// Help / Usage // Help / Usage
// ============================================================================= // =============================================================================
func PrintUsage() { func PrintUsage() {
PrintLine("╔══════════════════════════════════════════════════════╗"); PrintLine("╔══════════════════════════════════════════════════════╗");
PrintLine("║ jwt-pitbul — JWT CLI Tool v0.1.0 ║"); PrintLine(f"║ {AppName} — JWT CLI Tool v{Version} ║");
PrintLine("║ Sign, verify, decode JSON Web Tokens ║"); PrintLine("║ Sign, verify, decode JSON Web Tokens ║");
PrintLine("╚══════════════════════════════════════════════════════╝"); PrintLine("╚══════════════════════════════════════════════════════╝");
PrintLine(""); PrintLine("");
PrintLine("Usage:"); PrintLine("Usage:");
PrintLine(" jwt-pitbul sign <alg> <key> <claims>"); PrintLine(f" {AppName} sign <alg> <key> <claims>");
PrintLine(" jwt-pitbul verify <token> <alg> <key>"); PrintLine(f" {AppName} verify <token> <alg> <key>");
PrintLine(" jwt-pitbul decode <token>"); PrintLine(f" {AppName} decode <token>");
PrintLine(" jwt-pitbul keygen <type>"); PrintLine(f" {AppName} keygen <type>");
PrintLine(""); PrintLine("");
PrintLine("Commands:"); PrintLine("Commands:");
PrintLine(" sign Create a signed JWT from claims JSON"); PrintLine(" sign Create a signed JWT from claims JSON");
@@ -60,130 +233,109 @@ func PrintUsage() {
PrintLine(" EdDSA: base64-encoded 32-byte raw key"); PrintLine(" EdDSA: base64-encoded 32-byte raw key");
PrintLine(""); PrintLine("");
PrintLine("Key generation:"); PrintLine("Key generation:");
PrintLine(" jwt-pitbul keygen rsa # RSA 2048-bit (PEM)"); PrintLine(f" {AppName} keygen rsa # RSA 2048-bit (PEM)");
PrintLine(" jwt-pitbul keygen ecdsa # ECDSA P-256 (PEM)"); PrintLine(f" {AppName} keygen ecdsa # ECDSA P-256 (PEM)");
PrintLine(" jwt-pitbul keygen ed25519 # Ed25519 (raw base64)"); PrintLine(f" {AppName} keygen ed25519 # Ed25519 (raw base64)");
PrintLine(""); PrintLine("");
PrintLine("Examples:"); PrintLine("Examples:");
PrintLine(" jwt-pitbul sign HS256 'my-secret' '{\"sub\":\"123\"}'"); Print(f" {AppName} sign HS256 'my-secret' ");
PrintLine(" jwt-pitbul verify eyJh... HS256 'my-secret'"); PrintLine("'{\"sub\":\"123\"}'");
PrintLine(" jwt-pitbul decode eyJh..."); PrintLine(f" {AppName} verify eyJh... HS256 'my-secret'");
PrintLine(" jwt-pitbul keygen ed25519"); PrintLine(f" {AppName} decode eyJh...");
PrintLine(f" {AppName} keygen ed25519");
} }
// ============================================================================= // =============================================================================
// Algorithm name → JwtAlg enum // Key resolution — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
// ============================================================================= // =============================================================================
func ParseAlg(name: String) -> JwtAlg { func ResolveKey(alg: JwtAlg, keyArg: String) -> KeyResult {
if String_Eq(name, "HS256") { return JwtAlg { tag: JwtAlg_HS256 }; } if !alg.NeedsPemFile() {
if String_Eq(name, "HS384") { return JwtAlg { tag: JwtAlg_HS384 }; } return KeyResult_MakeOk(keyArg);
if String_Eq(name, "HS512") { return JwtAlg { tag: JwtAlg_HS512 }; }
if String_Eq(name, "RS256") { return JwtAlg { tag: JwtAlg_RS256 }; }
if String_Eq(name, "RS384") { return JwtAlg { tag: JwtAlg_RS384 }; }
if String_Eq(name, "RS512") { return JwtAlg { tag: JwtAlg_RS512 }; }
if String_Eq(name, "ES256") { return JwtAlg { tag: JwtAlg_ES256 }; }
if String_Eq(name, "ES384") { return JwtAlg { tag: JwtAlg_ES384 }; }
if String_Eq(name, "EdDSA") { return JwtAlg { tag: JwtAlg_EdDSA }; }
return JwtAlg { tag: JwtAlg_HS256 };
}
// =============================================================================
// Resolve key — for RSA/ECDSA, read PEM file; for HMAC/EdDSA, pass through
// =============================================================================
func ResolveKey(alg: JwtAlg, keyArg: String) -> String {
// RSA and ECDSA: key is a path to PEM file
if alg.tag == JwtAlg_RS256 || alg.tag == JwtAlg_RS384 || alg.tag == JwtAlg_RS512 ||
alg.tag == JwtAlg_ES256 || alg.tag == JwtAlg_ES384 {
if bux_file_exists(keyArg) != 0 {
let pem: String = bux_read_file(keyArg);
if String_Len(pem) > 0 && String_Len(pem) > 0 {
return pem;
}
Print("ERROR: could not read PEM file: ");
PrintLine(keyArg);
return "";
}
Print("ERROR: PEM file not found: ");
PrintLine(keyArg);
return "";
} }
// HMAC and EdDSA: key is used directly if bux_file_exists(keyArg) == 0 {
return keyArg; return KeyResult_MakeErr(f"ERROR: PEM file not found: {keyArg}");
} }
// ============================================================================= let pem: String = bux_read_file(keyArg);
// Check if algorithm is symmetric (HMAC) or asymmetric if String_Len(pem) == 0 {
// ============================================================================= return KeyResult_MakeErr(f"ERROR: could not read PEM file: {keyArg}");
func IsSymmetric(alg: JwtAlg) -> bool { }
return alg.tag == JwtAlg_HS256 || alg.tag == JwtAlg_HS384 || alg.tag == JwtAlg_HS512; return KeyResult_MakeOk(pem);
} }
// ============================================================================= // =============================================================================
// Command: sign // Command: sign
// ============================================================================= // =============================================================================
func CmdSign(algName: String, keyArg: String, claimsJson: String) -> int { func CmdSign(algName: String, keyArg: String, claimsJson: String) -> ExitResult {
let alg: JwtAlg = ParseAlg(algName); let algOpt: AlgOption = ParseAlg(algName);
let key: String = ResolveKey(alg, keyArg); if algOpt.tag != AlgOption_Some {
if String_Len(key) == 0 { return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
return 1;
} }
let alg: JwtAlg = algOpt.data.Some_0;
let keyRes: KeyResult = ResolveKey(alg, keyArg);
if keyRes.tag != KeyResult_Ok {
return ExitResult_MakeErr(keyRes.data.Err_0);
}
let key: String = keyRes.data.Ok_0;
let header: String = Jwt_MakeHeader(alg); let header: String = Jwt_MakeHeader(alg);
let token: String = Jwt_Encode(header, claimsJson, alg, key); let token: String = Jwt_Encode(header, claimsJson, alg, key);
if String_Len(token) == 0 { if String_Len(token) == 0 {
PrintLine("ERROR: signing failed"); return ExitResult_MakeErr("ERROR: signing failed");
return 1;
} }
PrintLine(token); PrintLine(token);
return 0; return ExitResult_MakeOk(0);
} }
// ============================================================================= // =============================================================================
// Command: verify // Command: verify
// ============================================================================= // =============================================================================
func CmdVerify(token: String, algName: String, keyArg: String) -> int { func CmdVerify(token: String, algName: String, keyArg: String) -> ExitResult {
let alg: JwtAlg = ParseAlg(algName); let algOpt: AlgOption = ParseAlg(algName);
let key: String = ResolveKey(alg, keyArg); if algOpt.tag != AlgOption_Some {
if String_Len(key) == 0 { return ExitResult_MakeErr(f"ERROR: unknown algorithm '{algName}'");
return 1;
} }
let alg: JwtAlg = algOpt.data.Some_0;
let keyRes: KeyResult = ResolveKey(alg, keyArg);
if keyRes.tag != KeyResult_Ok {
return ExitResult_MakeErr(keyRes.data.Err_0);
}
let key: String = keyRes.data.Ok_0;
var header: String = ""; var header: String = "";
var payload: String = ""; var payload: String = "";
let ok: bool = Jwt_Decode(token, alg, key, &header, &payload); if !Jwt_Decode(token, alg, key, &header, &payload) {
return ExitResult_MakeErr("✗ Signature INVALID (or malformed token)");
if ok {
PrintLine("✓ Signature valid");
PrintLine("");
PrintLine("Header:");
PrintLine(header);
PrintLine("");
PrintLine("Payload:");
PrintLine(payload);
return 0;
} }
PrintLine(" Signature INVALID (or malformed token)"); PrintLine(" Signature valid");
return 1; PrintLine("");
PrintLine("Header:");
PrintLine(header);
PrintLine("");
PrintLine("Payload:");
PrintLine(payload);
return ExitResult_MakeOk(0);
} }
// ============================================================================= // =============================================================================
// Command: decode (no verification) // Command: decode (no verification)
// ============================================================================= // =============================================================================
func CmdDecode(token: String) -> int { func CmdDecode(token: String) -> ExitResult {
let partCount: uint = bux_str_split_count(token, "."); let partCount: uint = String_SplitCount(token, ".");
if partCount != 3 { if partCount != 3 {
PrintLine("ERROR: not a valid JWT (expected 3 parts)"); return ExitResult_MakeErr("ERROR: not a valid JWT (expected 3 parts)");
return 1;
} }
let headerB64: String = bux_str_split_part(token, ".", 0); let headerB64: String = String_SplitPart(token, ".", 0);
let payloadB64: String = bux_str_split_part(token, ".", 1); let payloadB64: String = String_SplitPart(token, ".", 1);
let sigB64: String = bux_str_split_part(token, ".", 2); let sigB64: String = String_SplitPart(token, ".", 2);
let headerJson: String = Base64URL_Decode(headerB64); let headerJson: String = Base64URL_Decode(headerB64);
let payloadJson: String = Base64URL_Decode(payloadB64); let payloadJson: String = Base64URL_Decode(payloadB64);
@@ -199,52 +351,84 @@ func CmdDecode(token: String) -> int {
Print("Signature (base64url): "); Print("Signature (base64url): ");
PrintLine(sigB64); PrintLine(sigB64);
return 0; return ExitResult_MakeOk(0);
} }
// ============================================================================= // =============================================================================
// Command: keygen // Command: keygen
// ============================================================================= // =============================================================================
func CmdKeygen(keyType: String) -> int { func CmdKeygen(keyType: String) -> ExitResult {
if String_Eq(keyType, "ed25519") { let ktOpt: KeyTypeOption = ParseKeyType(keyType);
if ktOpt.tag != KeyTypeOption_Some {
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'. Use: rsa, ecdsa, ed25519");
}
let kt: KeyType = ktOpt.data.Some_0;
if kt.tag == KeyType_Ed25519 {
let pubBuf: *void = bux_alloc(32); let pubBuf: *void = bux_alloc(32);
let priv: *void = bux_alloc(32); let priv: *void = bux_alloc(32);
if !Ed25519_Keypair(pubBuf, priv) { if !Ed25519_Keypair(pubBuf, priv) {
PrintLine("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)"); return ExitResult_MakeErr("ERROR: Ed25519 key generation failed (OpenSSL 1.1.1+ required)");
return 1;
} }
let pubB64: String = bux_base64_encode(pubBuf as String, 32); let pubB64: String = bux_base64_encode(pubBuf as String, 32);
let privB64: String = bux_base64_encode(priv as String, 32); let privB64: String = bux_base64_encode(priv as String, 32);
PrintLine("Ed25519 keypair (base64):"); PrintLine("Ed25519 keypair (base64):");
Print(" Public: "); PrintLine(f" Public: {pubB64}");
PrintLine(pubB64); PrintLine(f" Private: {privB64}");
Print(" Private: "); return ExitResult_MakeOk(0);
PrintLine(privB64);
return 0;
} }
if kt.tag == KeyType_Rsa {
if String_Eq(keyType, "rsa") {
PrintLine("RSA key generation requires OpenSSL CLI:"); PrintLine("RSA key generation requires OpenSSL CLI:");
PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048"); PrintLine(" openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048");
PrintLine(" openssl rsa -in private.pem -pubout -out public.pem"); PrintLine(" openssl rsa -in private.pem -pubout -out public.pem");
PrintLine(""); PrintLine("");
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
return 0; return ExitResult_MakeOk(0);
} }
if kt.tag == KeyType_Ecdsa {
if String_Eq(keyType, "ecdsa") {
PrintLine("ECDSA key generation requires OpenSSL CLI:"); PrintLine("ECDSA key generation requires OpenSSL CLI:");
PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem"); PrintLine(" openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem");
PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem"); PrintLine(" openssl ec -in ec_private.pem -pubout -out ec_public.pem");
PrintLine(""); PrintLine("");
PrintLine("(Key generation from within Bux requires PEM write support — coming soon)"); PrintLine("(Key generation from within Bux requires PEM write support — coming soon)");
return 0; return ExitResult_MakeOk(0);
} }
return ExitResult_MakeErr(f"ERROR: unknown key type '{keyType}'");
}
Print("ERROR: unknown key type '"); // =============================================================================
Print(keyType); // Dispatch a parsed command to its handler
PrintLine("'. Use: rsa, ecdsa, ed25519"); // =============================================================================
return 1; func RunCommand(cmd: Cmd, args: Array<String>) -> ExitResult {
if cmd.tag == Cmd_Help {
PrintUsage();
return ExitResult_MakeOk(0);
}
if cmd.tag == Cmd_Sign {
if args.len < 5 {
return ExitResult_MakeErr("ERROR: 'sign' requires: <alg> <key> <claims_json>");
}
return CmdSign(args[2], args[3], args[4]);
}
if cmd.tag == Cmd_Verify {
if args.len < 5 {
return ExitResult_MakeErr("ERROR: 'verify' requires: <token> <alg> <key>");
}
return CmdVerify(args[2], args[3], args[4]);
}
if cmd.tag == Cmd_Decode {
if args.len < 3 {
return ExitResult_MakeErr("ERROR: 'decode' requires: <token>");
}
return CmdDecode(args[2]);
}
if cmd.tag == Cmd_Keygen {
if args.len < 3 {
return ExitResult_MakeErr("ERROR: 'keygen' requires: <type>");
}
return CmdKeygen(args[2]);
}
return ExitResult_MakeErr("ERROR: unhandled command");
} }
// ============================================================================= // =============================================================================
@@ -253,71 +437,32 @@ func CmdKeygen(keyType: String) -> int {
func Main() -> int { func Main() -> int {
let argc: int = bux_argc(); let argc: int = bux_argc();
if argc < 2 { // Collect CLI arguments into a generic Array<String>
PrintUsage(); let args: Array<String> = Array_New<String>(argc as uint);
return 0; for i in 0..argc {
Array_Push<String>(&args, bux_argv(i));
} }
// Collect arguments if args.len < 2 {
let args: *String = bux_alloc(argc as uint * 8) as *String; PrintUsage();
var i: int = 0; return 0;
while i < argc {
args[i] = bux_argv(i);
i = i + 1;
} }
let command: String = args[1]; let command: String = args[1];
let cmdOpt: CmdOption = ParseCmd(command);
if String_Eq(command, "help") || String_Eq(command, "--help") || String_Eq(command, "-h") { if cmdOpt.tag != CmdOption_Some {
PrintUsage(); PrintLine(f"ERROR: unknown command '{command}'");
return 0; PrintLine(f"Run '{AppName} help' for usage.");
return 1;
} }
let cmd: Cmd = cmdOpt.data.Some_0;
// --- sign <alg> <key> <claims> --- let result: ExitResult = RunCommand(cmd, args);
if String_Eq(command, "sign") { if result.tag == ExitResult_Err {
if argc < 5 { PrintLine(result.data.Err_0);
PrintLine("ERROR: 'sign' requires: <alg> <key> <claims_json>"); return 1;
PrintLine(" jwt-pitbul sign HS256 'secret' '{\"sub\":\"123\"}'");
return 1;
}
return CmdSign(args[2], args[3], args[4]);
} }
return result.data.Ok_0;
// --- verify <token> <alg> <key> ---
if String_Eq(command, "verify") {
if argc < 5 {
PrintLine("ERROR: 'verify' requires: <token> <alg> <key>");
PrintLine(" jwt-pitbul verify eyJh... HS256 'secret'");
return 1;
}
return CmdVerify(args[2], args[3], args[4]);
}
// --- decode <token> ---
if String_Eq(command, "decode") {
if argc < 3 {
PrintLine("ERROR: 'decode' requires: <token>");
PrintLine(" jwt-pitbul decode eyJh...");
return 1;
}
return CmdDecode(args[2]);
}
// --- keygen <type> ---
if String_Eq(command, "keygen") {
if argc < 3 {
PrintLine("ERROR: 'keygen' requires: <type>");
PrintLine(" jwt-pitbul keygen ed25519");
return 1;
}
return CmdKeygen(args[3]);
}
Print("ERROR: unknown command '");
Print(command);
PrintLine("'");
PrintLine("Run 'jwt-pitbul help' for usage.");
return 1;
} }
} // module Main } // module Main