skill added

This commit is contained in:
2026-08-28 14:08:51 +02:00
parent 1316e49456
commit 2743dfb4c0
3 changed files with 322 additions and 5 deletions
+16
View File
@@ -214,6 +214,13 @@
(regexp-match? #px"^/api/(?:auth|agent)(?:/|$)" (regexp-match? #px"^/api/(?:auth|agent)(?:/|$)"
(request-path request))) (request-path request)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Add one HTTP header to an existing response.
; pre : Value is a response and extra-header is an HTTP header.
; post : The original response remains unchanged.
; result : A response with the same body and metadata and the additional
; header prepended to its header list.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (response-add-header value extra-header) (define (response-add-header value extra-header)
(response (response-code value) (response (response-code value)
(response-message value) (response-message value)
@@ -222,6 +229,15 @@
(cons extra-header (response-headers value)) (cons extra-header (response-headers value))
(response-output value))) (response-output value)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Dispatch an API request and renew an eligible browser cookie.
; pre : Current-player and current-auth are initialized and request targets
; an API route.
; post : The selected handler has run. A due browser-session renewal is
; recorded and returned as Set-Cookie; agent requests never renew it.
; result : The HTTP response produced by the API handler, optionally extended
; with the renewed session cookie.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (dispatch-api request) (define (dispatch-api request)
(define value (api-dispatch request)) (define value (api-dispatch request))
(define renewed-cookie (define renewed-cookie
+83 -5
View File
@@ -26,6 +26,13 @@
(username [last-seen #:mutable] [last-cookie-renewal #:mutable]) (username [last-seen #:mutable] [last-cookie-renewal #:mutable])
#:transparent) #:transparent)
(struct failures ([attempts #:mutable] [started #:mutable]) #:transparent) (struct failures ([attempts #:mutable] [started #:mutable]) #:transparent)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Hold configured users, trusted proxies, and volatile auth state.
; pre : Constructor fields contain normalized and parsed internal values.
; post : Creating or recognizing a value does not change external state.
; result : auth-manager? recognizes values used by the authentication API.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(struct auth-manager (struct auth-manager
(users trusted-proxies session-seconds sessions failed lock) (users trusted-proxies session-seconds sessions failed lock)
#:transparent) #:transparent)
@@ -45,6 +52,12 @@
(define failure-window-seconds 300) (define failure-window-seconds 300)
(define maximum-failures 5) (define maximum-failures 5)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Create an Argon2id password hash for configuration storage.
; pre : Password is a string containing at least twelve characters.
; post : No module state is changed.
; result : A salted Argon2id hash encoded as a string.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-password-hash password) (define (make-password-hash password)
(unless (and (string? password) (unless (and (string? password)
(>= (string-length password) 12)) (>= (string-length password) 12))
@@ -56,6 +69,12 @@
(string->bytes/utf-8 password) (string->bytes/utf-8 password)
password-parameters)) password-parameters))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Verify a password against an encoded Argon2id hash.
; pre : Password and encoded are arbitrary values.
; post : No module state is changed.
; result : #t only when both values are strings and the password matches.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (password-hash-valid? password encoded) (define (password-hash-valid? password encoded)
(and (string? password) (and (string? password)
(string? encoded) (string? encoded)
@@ -135,6 +154,12 @@
(string-trim (last (string-split forwarded ","))) (string-trim (last (string-split forwarded ",")))
peer)) peer))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Report whether browser authentication is configured.
; pre : Manager is an auth-manager.
; post : Manager remains unchanged.
; result : #t when at least one configured user can log in, otherwise #f.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-enabled? manager) (define (auth-enabled? manager)
(positive? (hash-count (auth-manager-users manager)))) (positive? (hash-count (auth-manager-users manager))))
@@ -150,6 +175,14 @@
(auth-manager-session-seconds manager)) (auth-manager-session-seconds manager))
(hash-remove! (auth-manager-sessions manager) token))))) (hash-remove! (auth-manager-sessions manager) token)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Resolve the browser user represented by a request cookie.
; pre : Manager is an auth-manager and request is an HTTP request.
; post : Expired sessions are removed and a valid session's last-seen time
; is updated.
; result : "anonymous" when authentication is disabled, the normalized
; username for a valid session, or #f when login is required.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-request-user manager request) (define (auth-request-user manager request)
(cond (cond
((not (auth-enabled? manager)) "anonymous") ((not (auth-enabled? manager)) "anonymous")
@@ -187,7 +220,16 @@
address address
(failures 1 now)))) (failures 1 now))))
;; Returns a new token, #f for invalid credentials, or 'rate-limited. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Authenticate credentials and start a browser session.
; pre : Manager is an auth-manager, request is an HTTP request, and
; username and password are strings.
; post : A valid login creates a new session; a failed login updates the
; rate-limit state for the effective client address.
; result : A new opaque token, #f for invalid credentials, or 'rate-limited.
; internals: Unknown users follow the same Argon2id verification path as known
; users to reduce username-dependent timing differences.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-login! manager request username password) (define (auth-login! manager request username password)
(define address (request-address manager request)) (define address (request-address manager request))
(define now (current-seconds)) (define now (current-seconds))
@@ -217,6 +259,12 @@
(record-failure! manager address now) (record-failure! manager address now)
#f))))))) #f)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : End the browser session named by the request cookie.
; pre : Manager is an auth-manager and request is an HTTP request.
; post : The matching server-side session is removed when it exists.
; result : Void.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-logout! manager request) (define (auth-logout! manager request)
(let ((token (request-session-token request))) (let ((token (request-session-token request)))
(when token (when token
@@ -225,6 +273,13 @@
(lambda () (lambda ()
(hash-remove! (auth-manager-sessions manager) token)))))) (hash-remove! (auth-manager-sessions manager) token))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Encode an authenticated session token as a browser cookie.
; pre : Manager is an auth-manager and token is a session token string.
; post : Manager remains unchanged.
; result : A Secure, HttpOnly, SameSite=Strict Set-Cookie value whose Max-Age
; equals the configured session lifetime.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-session-cookie manager token) (define (auth-session-cookie manager token)
(string->bytes/utf-8 (string->bytes/utf-8
(format (format
@@ -233,9 +288,17 @@
token token
(auth-manager-session-seconds manager)))) (auth-manager-session-seconds manager))))
;; Return a refreshed cookie at most once per half session lifetime. The ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; server-side inactivity timer is updated on every authenticated request, but ; goal : Renew an actively used browser cookie at a bounded frequency.
;; limiting Set-Cookie avoids rewriting it for every one-second player poll. ; pre : Manager is an auth-manager and request is an HTTP request.
; post : Expired sessions are removed. When renewal is due, the session's
; last-cookie-renewal time is advanced.
; result : A fresh Set-Cookie value after half the configured lifetime has
; elapsed, otherwise #f.
; internals: The server idle timer moves on every authenticated request, while
; this half-life threshold prevents the one-second player poll from
; returning Set-Cookie every second.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-renewal-cookie manager request) (define (auth-renewal-cookie manager request)
(and (auth-enabled? manager) (and (auth-enabled? manager)
(let ((token (request-session-token request)) (let ((token (request-session-token request))
@@ -243,7 +306,7 @@
(and token (and token
(call-with-semaphore (call-with-semaphore
(auth-manager-lock manager) (auth-manager-lock manager)
(lambda () (λ ()
(prune-sessions! manager now) (prune-sessions! manager now)
(define value (define value
(hash-ref (auth-manager-sessions manager) token #f)) (hash-ref (auth-manager-sessions manager) token #f))
@@ -257,12 +320,27 @@
(set-session-last-cookie-renewal! value now) (set-session-last-cookie-renewal! value now)
(auth-session-cookie manager token))))))))) (auth-session-cookie manager token)))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Encode deletion of the browser session cookie.
; pre : None.
; post : No module state is changed.
; result : A Secure, HttpOnly, SameSite=Strict Set-Cookie value with Max-Age 0.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (auth-expired-cookie) (define (auth-expired-cookie)
(string->bytes/utf-8 (string->bytes/utf-8
(format (format
"~a=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Strict" "~a=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Strict"
session-cookie-name))) session-cookie-name)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : Create the authentication and session manager used by the server.
; pre : User-pairs contains username and Argon2id-hash pairs, trusted proxy
; values are IP addresses or CIDR networks, and session-seconds is a
; positive exact integer.
; post : No external state is changed; session and rate-limit tables start
; empty.
; result : A new auth-manager with normalized usernames and parsed networks.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (make-auth-manager user-pairs (define (make-auth-manager user-pairs
#:trusted-proxies #:trusted-proxies
[trusted-proxy-values '("127.0.0.0/8" "::1/128")] [trusted-proxy-values '("127.0.0.0/8" "::1/128")]
+223
View File
@@ -0,0 +1,223 @@
---
name: racket-programmeer-skill
description: Hiermee wordt mijn voorkeur racket programmeerstijl aangegeven.
---
---
name: racket-programmeerstijl
description: Gebruik deze skill wanneer je Racket-code voor Hans schrijft, wijzigt, refactort of beoordeelt. Pas de bestaande, eenvoudige en procedurele programmeerstijl toe; voorkom over-engineering en onnodige abstracties. Gebruik deze skill niet voor algemene uitleg over Racket waarbij geen code voor zijn projecten wordt gemaakt of aangepast.
---
# Racket-programmeerstijl
Gebruik deze stijl wanneer je Racket-code voor Hans schrijft of aanpast.
## Uitgangspunt
Het *allerbelangrijkste* uitgangspunt is dat je de programmerstijl van aangeleverde code volgt.
Als je een zip met een package aangeleverd krijgt via de prompt dan volg je de programmeerstijl die je in de aangeleverde code vindt.
Wanneer bestaande broncode beschikbaar is, heeft de stijl van die broncode voorrang. Sluit daar zo nauw mogelijk op aan.
Schrijf eenvoudige, directe en goed leesbare Racket-code.
Kies de kleinste oplossing die het huidige probleem netjes oplost.
Bouw geen abstraheringslaag voor mogelijk toekomstig gebruik.
En maak geen helpers die alleen maar in de weg staan.
## Structuur
- Houd modules klein en doelgericht.
- Splits functionaliteit alleen af naar een private module wanneer die een duidelijk eigen doel heeft.
- Gebruik voor duidelijke secties bij voorkeur commentaar in deze vorm:
```racket
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Supporting functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
```
of, wanneer dat beter bij de module past:
```racket
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Internal state / functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
```
Voor publieke functies:
```racket
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Provided functions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
```
- Houd `provide` en `require` eenvoudig en overzichtelijk.
- Voeg geen extra framework, wrapperlaag of generieke infrastructuur toe zonder concrete noodzaak.
## pre/postcondities
Geëxporteerde functies/procedures/classes of functies/procedures/classes die daarvoor duidelijk in
aanmerking komen, d.w.z. die die provided zijn of naar verwachting zullen worden, moeten gedocumenteerd worden.
Zowel in een module scribble als in de code zelf. In het engels.
In de code zelf: minimaal:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; goal : <doel>
; pre : <preconditie(s)>
; post : <postconditie(s)>
; [result:] <resultaat en onder welke conditie>
Over het algemeen wil je de internals van een functie weten. Hoe werkt het en waarom werkt het zo.
; [internals:]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Als een functie/procedure/class overduidelijk in aanmerking komt voor 'provide' en hij staat er nog niet in.
Verzamel dan de lijst en vraag of je ze moet toevoegen.
## Procedures en control flow
- Geef de voorkeur aan gewone procedures met een direct leesbare control flow.
- Gebruik `let`, `if`, `when`, `unless`, `begin` en `cond` expliciet wanneer dat de code duidelijker maakt.
- Maak niet voor iedere kleine stap een aparte helperprocedure.
- Introduceer geen hogere-orde of functionele constructies alleen omdat dat compacter kan.
- Gebruik recursie of een named `let` wanneer dat de meest directe oplossing is.
- Gebruik bij `cond` bij voorkeur deze vorm:
```racket
(cond
([condition] korte body)
([other-condition]
langere body))
(else ... alleen als het nodig is)
```
## Mate van abstractie vs leesbaarheid
Liever concreet dan abstract.
Geef expliciete, goed leesbare constructies de voorkeur boven compacte abstracte idiomen.
Bijvoorbeeld combinaties als (filter values (list (and condition 'symbol) ...)).
Schrijf dan liever expliciet (filter (lambda (x) x) (list (if condition 'symbol #f) ...)).
Vermijd vooral het stapelen van meerdere impliciete idiomen wanneer dat de leesbaarheid vermindert.
## Gebruik lambda.
Geef de voorkeur aan λ boven lambda.
## Waarden en state
- Gebruik `#f` als normale waarde voor "niet gevonden", "niet beschikbaar" of "nog niet geïnitialiseerd" wanneer dat natuurlijk past.
- Expliciete vergelijkingen zoals `(eq? value #f)` zijn prima wanneer dat de bedoeling duidelijk maakt.
- Houd state eenvoudig. Een gewone modulevariabele zoals `cached-git-exe` is prima wanneer daarvoor geen zwaarder mechanisme nodig is.
- Gebruik geen parameters, structs, classes of objectlagen wanneer een gewone variabele of procedure voldoende is.
## Publieke API
- Gebruik `define/contract` voor publieke procedures wanneer een contract nuttige documentatie en controle geeft.
- Houd publieke procedures klein en voorspelbaar.
- Verander een bestaande publieke API niet zonder noodzaak.
- Voeg geen extra publieke functies toe voor hypothetische toekomstige behoeften.
## Fouten en interactie
- Geef duidelijke en concrete foutmeldingen.
- Los eenvoudige interactieve invoer lokaal en procedureel op.
- Maak foutafhandeling niet generieker dan nodig.
- Als een externe executable of voorziening ontbreekt, meld precies wat ontbreekt en wat de gebruiker kan doen.
## Configuratie
- Bewaar lokale configuratie in een kleine, afzonderlijke private module wanneer dat de hoofdmodule eenvoudiger maakt.
- Gebruik bestaande projectvoorzieningen, zoals `simple-ini`, rechtstreeks in plaats van er een extra abstractielaag omheen te bouwen.
- Dupliceer geen configuratie die al door een extern programma zelf wordt beheerd.
## Naamgeving en leesbaarheid
- Kies concrete, korte namen die passen bij de bestaande code.
- Gebruik Engels voor identifiers en technische namen wanneer de bestaande code dat doet.
- Schrijf comments alleen wanneer ze iets toevoegen dat niet al vanzelf uit de code blijkt.
- Geef de voorkeur aan een paar duidelijke regels boven een compacte maar moeilijker leesbare expressie.
## Vermijd
Vermijd zonder concrete noodzaak:
- over-engineering;
- generieke wrappers;
- extra abstraheringslagen;
- dynamische `require`-constructies;
- classes wanneer procedures volstaan;
- structs wanneer een eenvoudige waarde volstaat;
- configuratie-objecten of dependency-injectionpatronen;
- veel kleine helperprocedures die de control flow versnipperen;
- refactors die alleen bedoeld zijn om code "slimmer" of abstracter te maken.
## Werkwijze bij aanpassen van bestaande code
1. Lees eerst de omliggende module(s).
2. Neem naamgeving, inspringing, control-flow-stijl en module-indeling over.
3. Wijzig alleen wat voor de gevraagde stap nodig is.
4. Houd bestaande werkende code intact als er geen reden is die te veranderen.
5. Voeg geen volgende architectuurstappen alvast toe.
6. Controleer of de oplossing eenvoudiger is dan het probleem; zo niet, vereenvoudig.
## Referentiestijl
Deze vorm is representatief:
```racket
(define cached-value #f)
(define/contract (get-value)
(-> (or/c path? #f))
(if (eq? cached-value #f)
(let ((value (find-value)))
(set! cached-value value)
value)
cached-value))
```
Een wat langere maar direct leesbare implementatie heeft de voorkeur boven een kortere oplossing met meerdere nieuwe abstracties.
# Schrijven van testgevallen voor modules/packages
Een test die alleen werkt vanuit de development directory, op het development-OS of met de lokale shell/environment is geen geldige package-test.
## Racket Package Index / build-service tests
Behandel de Racket Package Index/build service als een aparte, strikte
en onbekende testomgeving.
Bij packagecode en tests gelden daarom altijd de volgende regels:
- Maak nooit aannames over `current-directory` of de directory van waaruit
code of tests worden uitgevoerd. Bepaal testdata en paden expliciet en
relocatable, bijvoorbeeld met runtime paths en tijdelijke directories.
- Maak nooit impliciete aannames over het besturingssysteem. Vermijd
OS-specifieke paden, shells, executables en gedrag, of handel verschillen
expliciet per platform af.
- Maak tests onafhankelijk van lokale environment state. Benodigde
environment variables moeten expliciet en bij voorkeur geïsoleerd worden
ingesteld.
- Een succesvolle test moet stil en ondubbelzinnig succesvol zijn.
Laat geen verwachte foutmeldingen naar de echte stdout/stderr lekken,
omdat `raco test --drdr` en de Package Index dergelijke output als een
mogelijke test failure kunnen classificeren.
- Verwachte foutoutput moet worden gecaptureerd en geassert.
- Tests moeten hun eigen tijdelijke state en testbestanden aanmaken en
mogen geen bestanden, processen, environment changes of andere state
achterlaten.
- Test packagewijzigingen waar mogelijk ook in een omgeving die lijkt op:
`raco setup --check-pkg-deps` en
`raco test --drdr --package <package>`.