auto(0129): agents_dashboard — secret_store_cpp_infra + CMakeLists register #4
@@ -0,0 +1,44 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JWTGenerate codifica un JWT firmado con HMAC-SHA256 (alg: HS256).
|
||||
// Si claims.IssuedAt viene en cero se setea a time.Now().Unix().
|
||||
// Retorna el token en formato "header.payload.signature" con los tres segmentos
|
||||
// codificados en base64url sin padding.
|
||||
func JWTGenerate(claims JWTClaims, secret string) (string, error) {
|
||||
if secret == "" {
|
||||
return "", errors.New("jwt_generate: secret vacio")
|
||||
}
|
||||
if claims.IssuedAt == 0 {
|
||||
claims.IssuedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
header := map[string]string{"alg": "HS256", "typ": "JWT"}
|
||||
headerJSON, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payloadJSON, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
enc := base64.RawURLEncoding
|
||||
headerPart := enc.EncodeToString(headerJSON)
|
||||
payloadPart := enc.EncodeToString(payloadJSON)
|
||||
signingInput := headerPart + "." + payloadPart
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(signingInput))
|
||||
sigPart := enc.EncodeToString(mac.Sum(nil))
|
||||
|
||||
return signingInput + "." + sigPart, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: jwt_generate
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func JWTGenerate(claims JWTClaims, secret string) (string, error)"
|
||||
description: "Codifica y firma un JWT con HMAC-SHA256 (HS256). Retorna el token en formato header.payload.signature. Setea IssuedAt automaticamente si viene en cero."
|
||||
tags: [jwt, auth, token, hmac, sign, infra]
|
||||
uses_functions: []
|
||||
uses_types: [JWTClaims_go_infra]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [crypto/hmac, crypto/sha256, encoding/base64, encoding/json, errors, time]
|
||||
params:
|
||||
- name: claims
|
||||
desc: "claims del JWT (sub, iss, aud, exp, iat, custom). Si IssuedAt es 0 se rellena con time.Now()"
|
||||
- name: secret
|
||||
desc: "clave HMAC para firmar. No debe estar vacia. Obtenerla de env var o pass_get, nunca hardcoded"
|
||||
output: "token JWT firmado en formato base64url header.payload.signature"
|
||||
tested: true
|
||||
tests: ["genera token valido con claims completas", "setea IssuedAt si viene en cero", "error si secret vacio"]
|
||||
test_file_path: "functions/infra/jwt_generate_test.go"
|
||||
file_path: "functions/infra/jwt_generate.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
claims := JWTClaims{
|
||||
Subject: "user-123",
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
||||
Custom: map[string]any{"role": "admin"},
|
||||
}
|
||||
token, err := JWTGenerate(claims, os.Getenv("JWT_SECRET"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Authorization", "Bearer " + token)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — usa `time.Now()` para el claim `iat` cuando no viene fijado. Implementa HS256 sin libreria externa (solo stdlib crypto/hmac + crypto/sha256). Solo soporta HS256: para RS256/ES256 se crearia una funcion separada. El secret debe tener al menos 256 bits de entropia (32+ bytes aleatorios) para resistencia real. NO apto para escenarios multi-servicio donde se necesita clave publica/privada — usa RS256 en ese caso.
|
||||
@@ -0,0 +1,54 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJWTGenerate_ReturnsThreeSegments(t *testing.T) {
|
||||
claims := JWTClaims{
|
||||
Subject: "user-1",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
token, err := JWTGenerate(claims, "test-secret-0123456789abcdef")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTGenerate error: %v", err)
|
||||
}
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
t.Fatalf("esperados 3 segmentos, got %d en %q", len(parts), token)
|
||||
}
|
||||
for i, p := range parts {
|
||||
if p == "" {
|
||||
t.Fatalf("segmento %d vacio", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTGenerate_FillsIssuedAtWhenZero(t *testing.T) {
|
||||
claims := JWTClaims{
|
||||
Subject: "user-1",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
before := time.Now().Unix()
|
||||
token, err := JWTGenerate(claims, "s")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTGenerate error: %v", err)
|
||||
}
|
||||
after := time.Now().Unix()
|
||||
parsed, err := JWTValidate(token, "s")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTValidate error: %v", err)
|
||||
}
|
||||
if parsed.IssuedAt < before || parsed.IssuedAt > after {
|
||||
t.Fatalf("IssuedAt %d fuera del rango [%d,%d]", parsed.IssuedAt, before, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTGenerate_ErrorsOnEmptySecret(t *testing.T) {
|
||||
_, err := JWTGenerate(JWTClaims{Subject: "x"}, "")
|
||||
if err == nil {
|
||||
t.Fatal("esperaba error con secret vacio")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JWTValidate verifica la firma HMAC-SHA256 de un JWT y decodifica sus claims.
|
||||
// Rechaza tokens mal formados, con firma invalida o expirados (exp < time.Now()).
|
||||
// Retorna las claims si todo es valido.
|
||||
func JWTValidate(token string, secret string) (JWTClaims, error) {
|
||||
var zero JWTClaims
|
||||
if secret == "" {
|
||||
return zero, errors.New("jwt_validate: secret vacio")
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return zero, errors.New("jwt_validate: token malformado (se esperaban 3 segmentos)")
|
||||
}
|
||||
|
||||
enc := base64.RawURLEncoding
|
||||
signingInput := parts[0] + "." + parts[1]
|
||||
|
||||
// Verificar firma
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(signingInput))
|
||||
expectedSig := mac.Sum(nil)
|
||||
|
||||
gotSig, err := enc.DecodeString(parts[2])
|
||||
if err != nil {
|
||||
return zero, errors.New("jwt_validate: firma mal codificada")
|
||||
}
|
||||
if !hmac.Equal(expectedSig, gotSig) {
|
||||
return zero, errors.New("jwt_validate: firma invalida")
|
||||
}
|
||||
|
||||
// Decodificar header y confirmar alg
|
||||
headerBytes, err := enc.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return zero, errors.New("jwt_validate: header mal codificado")
|
||||
}
|
||||
var header map[string]string
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return zero, errors.New("jwt_validate: header no es JSON valido")
|
||||
}
|
||||
if alg, _ := header["alg"]; alg != "HS256" {
|
||||
return zero, errors.New("jwt_validate: algoritmo no soportado (solo HS256)")
|
||||
}
|
||||
|
||||
// Decodificar claims
|
||||
payloadBytes, err := enc.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return zero, errors.New("jwt_validate: payload mal codificado")
|
||||
}
|
||||
var claims JWTClaims
|
||||
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
|
||||
return zero, errors.New("jwt_validate: payload no es JSON valido")
|
||||
}
|
||||
|
||||
// Validar expiracion si esta presente
|
||||
if claims.ExpiresAt > 0 && time.Now().Unix() >= claims.ExpiresAt {
|
||||
return zero, errors.New("jwt_validate: token expirado")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: jwt_validate
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func JWTValidate(token string, secret string) (JWTClaims, error)"
|
||||
description: "Verifica la firma HMAC-SHA256 de un JWT y decodifica sus claims. Rechaza tokens mal formados, con firma invalida o expirados."
|
||||
tags: [jwt, auth, token, hmac, verify, infra]
|
||||
uses_functions: []
|
||||
uses_types: [JWTClaims_go_infra]
|
||||
returns: [JWTClaims_go_infra]
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [crypto/hmac, crypto/sha256, encoding/base64, encoding/json, errors, strings, time]
|
||||
params:
|
||||
- name: token
|
||||
desc: "JWT string en formato header.payload.signature (base64url, sin padding)"
|
||||
- name: secret
|
||||
desc: "clave HMAC usada para firmar el token. Debe coincidir con la usada en JWTGenerate"
|
||||
output: "claims decodificadas si el token es valido; error si firma invalida, expirado o malformado"
|
||||
tested: true
|
||||
tests: ["valida token generado por JWTGenerate", "rechaza firma invalida", "rechaza token expirado", "rechaza token malformado", "rechaza algoritmo distinto de HS256"]
|
||||
test_file_path: "functions/infra/jwt_validate_test.go"
|
||||
file_path: "functions/infra/jwt_validate.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
auth := r.Header.Get("Authorization")
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
claims, err := JWTValidate(token, os.Getenv("JWT_SECRET"))
|
||||
if err != nil {
|
||||
HTTPErrorResponse(w, HTTPError{Status: 401, Code: "invalid_token", Message: err.Error()})
|
||||
return
|
||||
}
|
||||
userID := claims.Subject
|
||||
role, _ := claims.Custom["role"].(string)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — usa `time.Now()` para comparar contra `exp`. Usa `hmac.Equal` para comparacion constant-time de firmas (mitiga timing attacks). Solo acepta alg=HS256 en el header, otros algoritmos se rechazan explicitamente para evitar el ataque "alg=none". Si `exp` es 0 (no fijado) no se valida expiracion — es responsabilidad del caller asegurar que sus tokens siempre tengan exp fijado. Errores descriptivos con prefijo `jwt_validate:` para facilitar debugging; en respuestas HTTP conviene mapear todos a un mensaje generico "token invalido" para no filtrar informacion.
|
||||
@@ -0,0 +1,83 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestJWTValidate_ValidatesGeneratedToken(t *testing.T) {
|
||||
claims := JWTClaims{
|
||||
Subject: "user-42",
|
||||
Issuer: "tester",
|
||||
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
||||
Custom: map[string]any{"role": "admin"},
|
||||
}
|
||||
token, err := JWTGenerate(claims, "super-secret")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTGenerate error: %v", err)
|
||||
}
|
||||
got, err := JWTValidate(token, "super-secret")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTValidate error: %v", err)
|
||||
}
|
||||
if got.Subject != "user-42" {
|
||||
t.Errorf("Subject = %q, esperado user-42", got.Subject)
|
||||
}
|
||||
if got.Issuer != "tester" {
|
||||
t.Errorf("Issuer = %q, esperado tester", got.Issuer)
|
||||
}
|
||||
if got.Custom["role"] != "admin" {
|
||||
t.Errorf("Custom[role] = %v, esperado admin", got.Custom["role"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTValidate_RejectsInvalidSignature(t *testing.T) {
|
||||
token, err := JWTGenerate(JWTClaims{Subject: "x", ExpiresAt: time.Now().Add(time.Hour).Unix()}, "secret-a")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTGenerate error: %v", err)
|
||||
}
|
||||
if _, err := JWTValidate(token, "secret-b"); err == nil {
|
||||
t.Fatal("esperaba error con secret distinto")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTValidate_RejectsExpiredToken(t *testing.T) {
|
||||
token, err := JWTGenerate(JWTClaims{
|
||||
Subject: "x",
|
||||
ExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
|
||||
}, "s")
|
||||
if err != nil {
|
||||
t.Fatalf("JWTGenerate error: %v", err)
|
||||
}
|
||||
if _, err := JWTValidate(token, "s"); err == nil {
|
||||
t.Fatal("esperaba error con token expirado")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTValidate_RejectsMalformedToken(t *testing.T) {
|
||||
cases := []string{
|
||||
"",
|
||||
"not-a-token",
|
||||
"one.two",
|
||||
"one.two.three.four",
|
||||
}
|
||||
for _, tok := range cases {
|
||||
if _, err := JWTValidate(tok, "s"); err == nil {
|
||||
t.Errorf("esperaba error con token %q", tok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTValidate_RejectsOtherAlgorithms(t *testing.T) {
|
||||
// Token con alg=none no es aceptado aunque la firma sea vacia
|
||||
// Construimos manualmente: header con alg=none
|
||||
// "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0" = {"alg":"none","typ":"JWT"}
|
||||
token := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ4In0."
|
||||
if _, err := JWTValidate(token, "s"); err == nil {
|
||||
t.Fatal("esperaba error con alg=none")
|
||||
}
|
||||
if !strings.Contains(token, ".") {
|
||||
t.Fatal("token debe tener puntos")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package infra
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// PasswordHash hashea un password con bcrypt.
|
||||
// cost controla el trabajo computacional (4 = minimo, 14 = muy lento). Valor 0 usa default 12.
|
||||
func PasswordHash(password string, cost int) (string, error) {
|
||||
if cost <= 0 {
|
||||
cost = 12
|
||||
}
|
||||
b, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: password_hash
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func PasswordHash(password string, cost int) (string, error)"
|
||||
description: "Hashea un password con bcrypt. Cost por defecto es 12 (si se pasa 0). El hash resultante incluye el salt y el cost embebidos."
|
||||
tags: [password, hash, bcrypt, auth, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [golang.org/x/crypto/bcrypt]
|
||||
params:
|
||||
- name: password
|
||||
desc: "password en texto plano a hashear"
|
||||
- name: cost
|
||||
desc: "coste bcrypt entre 4 y 14. 0 usa el default 12 (buen balance velocidad/seguridad en 2025)"
|
||||
output: "hash bcrypt en formato $2a$... apto para guardar en BD y verificar con PasswordVerify"
|
||||
tested: true
|
||||
tests: ["hashea password con cost default", "hashea password con cost custom", "hashes distintos para mismo password (salt diferente)"]
|
||||
test_file_path: "functions/infra/password_hash_test.go"
|
||||
file_path: "functions/infra/password_hash.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
hash, err := PasswordHash(inputPassword, 12)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.Exec("INSERT INTO users (email, password_hash) VALUES (?, ?)", email, hash)
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — bcrypt usa entropia del OS para generar salt aleatorio en cada invocacion. El hash producido incluye el salt y el cost embebidos en el string (`$2a$12$salt...hash`), por lo que PasswordVerify no necesita el cost como parametro aparte. Cost 12 = ~250ms/hash en hardware moderno (2025): suficiente para bloquear ataques por fuerza bruta sin ser insoportable en el login. Para proteccion extra en servidores con mucho CPU disponible se puede subir a 14.
|
||||
@@ -0,0 +1,34 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPasswordHash_DefaultCost(t *testing.T) {
|
||||
hash, err := PasswordHash("hunter2", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("PasswordHash error: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(hash, "$2") {
|
||||
t.Errorf("hash no tiene prefijo bcrypt: %q", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordHash_CustomCost(t *testing.T) {
|
||||
hash, err := PasswordHash("password", 4) // 4 = minimum, rapido para tests
|
||||
if err != nil {
|
||||
t.Fatalf("PasswordHash error: %v", err)
|
||||
}
|
||||
if hash == "" {
|
||||
t.Fatal("hash vacio")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordHash_DifferentSalts(t *testing.T) {
|
||||
h1, _ := PasswordHash("same-password", 4)
|
||||
h2, _ := PasswordHash("same-password", 4)
|
||||
if h1 == h2 {
|
||||
t.Fatal("los hashes deben diferir por salt distinto")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package infra
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
// PasswordVerify compara un password en texto plano contra un hash bcrypt.
|
||||
// Retorna nil si hacen match, error si no.
|
||||
func PasswordVerify(password string, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: password_verify
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func PasswordVerify(password string, hash string) error"
|
||||
description: "Verifica un password en texto plano contra un hash bcrypt. Retorna nil si hacen match, error si no coinciden o si el hash es invalido."
|
||||
tags: [password, verify, bcrypt, auth, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: [golang.org/x/crypto/bcrypt]
|
||||
params:
|
||||
- name: password
|
||||
desc: "password en texto plano a verificar"
|
||||
- name: hash
|
||||
desc: "hash bcrypt obtenido previamente de PasswordHash (guardado en BD)"
|
||||
output: "nil si el password coincide con el hash; error si no coincide o hash invalido"
|
||||
tested: true
|
||||
tests: ["verifica password correcto", "rechaza password incorrecto", "rechaza hash malformado"]
|
||||
test_file_path: "functions/infra/password_verify_test.go"
|
||||
file_path: "functions/infra/password_verify.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
row := db.QueryRow("SELECT password_hash FROM users WHERE email = ?", email)
|
||||
var stored string
|
||||
if err := row.Scan(&stored); err != nil {
|
||||
HTTPErrorResponse(w, HTTPError{Status: 401, Code: "invalid_credentials", Message: "email o password incorrectos"})
|
||||
return
|
||||
}
|
||||
if err := PasswordVerify(input, stored); err != nil {
|
||||
HTTPErrorResponse(w, HTTPError{Status: 401, Code: "invalid_credentials", Message: "email o password incorrectos"})
|
||||
return
|
||||
}
|
||||
// OK, emitir token
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Impura — bcrypt.CompareHashAndPassword es constant-time internamente (mitiga timing attacks). En respuestas HTTP al usuario NO distinguir entre "email no existe" y "password incorrecto": ambos casos deben retornar el mismo mensaje generico para no filtrar existencia de cuentas. El error real se puede loguear internamente con log_info/log_warn sin problema.
|
||||
@@ -0,0 +1,23 @@
|
||||
package infra
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPasswordVerify_CorrectPassword(t *testing.T) {
|
||||
hash, _ := PasswordHash("correct-horse-battery-staple", 4)
|
||||
if err := PasswordVerify("correct-horse-battery-staple", hash); err != nil {
|
||||
t.Fatalf("esperaba nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordVerify_WrongPassword(t *testing.T) {
|
||||
hash, _ := PasswordHash("secret", 4)
|
||||
if err := PasswordVerify("wrong", hash); err == nil {
|
||||
t.Fatal("esperaba error con password incorrecto")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordVerify_InvalidHash(t *testing.T) {
|
||||
if err := PasswordVerify("x", "not-a-bcrypt-hash"); err == nil {
|
||||
t.Fatal("esperaba error con hash invalido")
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/marcboeker/go-duckdb v1.8.5
|
||||
github.com/mattn/go-sqlite3 v1.14.37
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
nhooyr.io/websocket v1.8.17
|
||||
|
||||
@@ -155,6 +155,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c h1:KL/ZBHXgKGVmuZBZ01Lt57yE5ws8ZPSkkihmEyq7FXc=
|
||||
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
|
||||
Reference in New Issue
Block a user