writeonce/docs/examples/web-app/main.wo
shoney.arickathil c17a8a2558 feat(framework): auth in core — Bearer/Basic mechanism + principal slot
- http/auth.wo: auth_header (scheme split, case-insensitive, RFC 9110),
  bearer_token, basic_credentials (first-colon split, RFC 7617),
  pure-.wo base64_decode (RFC 4648, strict padding), ct_eq constant-time
  compare (no early exit, both Basic fields always compared)
- req.principal: the blessed "who is this" slot, "" until authenticated;
  Middleware.before now takes mut req so auth can write it
- BearerAuth { token, principal } and BasicAuth { user, pass, realm }
  middlewares; BasicAuth answers the WWW-Authenticate challenge; policy
  (routes/users/secrets) stays app-side on the exposed fns
- web-app dogfoods BearerAuth; its hand-rolled Auth class deleted
- probe matrix 26/26 (RFC 4648 vectors, rfc7617 pair, pass-with-colon,
  bad padding/chars/length, deny paths, challenge header) release+ASan
- gate grows 16 -> 17: wrong bearer token answers 401 over the wire
- README: auth bullet + the core CHECKLIST (done / candidate / parked
  behind 8-11 by design); story 16 + board record the landing
- all gates green: web-app 17/0, oop-e2e 89/0, deps-accept 8/0,
  log-watcher 7/0, employee 8/0, woc-test green

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 03:15:16 +02:00

109 lines
3.7 KiB
Text

-- web-app — the storefront: writeonce-framework (via [deps]) + @table
-- persistence. Every handler is a class satisfying Handler; the auth gate is
-- a Middleware; the data layer is the language's own database — no ORM, no
-- separate process, one binary.
use env
use json
use framework
use framework/http
use framework/router
-- decode target for POST /products, encode shape for every product answer
typedef ProductView = { name: Text, price: Int, stock: Int }
typedef NewOrder = { product: Text, qty: Int }
fn view_json(name: Text, price: Int, stock: Int) -> Text {
return json.encode(ProductView { name: name, price: price, stock: stock });
}
class ListProducts {
pad: Int
fn handle(req: Req) -> Resp {
let body = "[";
let first = true;
for p in from x in Product order by x.name select x {
if first == false { body = body .. ","; }
first = false;
body = body .. view_json(p.name, p.price, p.stock);
}
return ok_json(body .. "]");
}
}
class ShowProduct {
pad: Int
fn handle(req: Req) -> Resp {
let name = req.params["name"];
if name == nil { return bad_request("no name"); }
let hits = from p in Product where p.name == name take 1 select p;
if len(hits) == 0 { return not_found(); }
let p = hits[0];
return ok_json(view_json(p.name, p.price, p.stock));
}
}
class CreateProduct {
pad: Int
fn handle(req: Req) -> Resp {
let v = json.decode(req.body) as ProductView;
if v == nil { return bad_request("body must be {name, price, stock}"); }
let made = try insert Product { name: v.name, price: v.price, stock: v.stock }
catch (e) nil;
if made == nil { return conflict("product name already exists"); }
return created_json(view_json(v.name, v.price, v.stock));
}
}
class CreateOrder {
pad: Int
fn handle(req: Req) -> Resp {
let v = json.decode(req.body) as NewOrder;
if v == nil { return bad_request("body must be {product, qty}"); }
if v.qty < 1 { return bad_request("qty must be positive"); }
let hits = from p in Product where p.name == v.product take 1 select p;
if len(hits) == 0 { return not_found(); }
insert Order { product: hits[0], qty: v.qty };
return created_json("{\"ok\":true}");
}
}
class DeleteProduct {
pad: Int
fn handle(req: Req) -> Resp {
let name = req.params["name"];
if name == nil { return bad_request("no name"); }
let hits = from p in Product where p.name == name take 1 select p;
if len(hits) == 0 { return not_found(); }
let gone = try delete hits[0] catch (e) nil;
if gone == nil { return conflict("orders still reference this product"); }
return ok_json("{\"deleted\":true}");
}
}
fn main(args: multi Text) -> Int {
if len(args) < 1 {
print_err("usage: web-app <port> (WA_TOKEN and WO_DATA must be set)");
return 2;
}
let port = parse_int(args[0]);
if port == nil {
print_err("web-app: <port> must be a number");
return 2;
}
let token = env.get("WA_TOKEN");
if token == nil {
print_err("web-app: WA_TOKEN is required (the auth middleware's bearer token)");
return 2;
}
let app = App { middleware: [], routes: [] };
-- the framework's Bearer mechanism: constant-time compare, principal
-- attached to req.principal for handlers that want "who is this"
app.use_mw(Mw { m: BearerAuth { token: token, principal: "api" } });
app.get("/products", ListProducts { pad: 0 });
app.get("/products/:name", ShowProduct { pad: 0 });
app.post("/products", CreateProduct { pad: 0 });
app.post("/orders", CreateOrder { pad: 0 });
app.delete_("/products/:name", DeleteProduct { pad: 0 });
return app.serve("127.0.0.1", port);
}