mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-30 06:07:38 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a172c73ab0 | |||
| ef28959c4d | |||
| 7c737afc5a | |||
| a70e3e02d0 | |||
| cec8eb5c0f | |||
| 06fa3198f2 | |||
| 52d3dca1fa | |||
| 873e8925d4 |
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||||
github.com/getsentry/sentry-go v0.46.2
|
github.com/getsentry/sentry-go v0.46.2
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
@@ -61,7 +62,6 @@ require (
|
|||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v28.5.1+incompatible // indirect
|
github.com/docker/docker v28.5.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
@@ -144,12 +144,12 @@ require (
|
|||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect
|
|
||||||
golang.org/x/mod v0.36.0 // indirect
|
golang.org/x/mod v0.36.0 // indirect
|
||||||
golang.org/x/net v0.54.0 // indirect
|
golang.org/x/net v0.54.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
golang.org/x/text v0.37.0 // indirect
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
golang.org/x/tools v0.45.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 // indirect
|
||||||
google.golang.org/grpc v1.81.1 // indirect
|
google.golang.org/grpc v1.81.1 // indirect
|
||||||
|
|||||||
@@ -5,43 +5,35 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h
|
|||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
|
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf h1:TqhNAT4zKbTdLa62d2HDBFdvgSbIGB3eJE8HqhgiL9I=
|
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
|
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
@@ -68,14 +60,13 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||||
|
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||||
@@ -94,12 +85,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
|
||||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
|
||||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
|
||||||
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
|
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
|
||||||
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
@@ -115,16 +102,13 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||||
@@ -137,8 +121,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
|
|
||||||
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
|
||||||
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
|
||||||
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
@@ -152,8 +134,6 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
|||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
@@ -164,8 +144,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
|
|||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
|
||||||
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
|
||||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
@@ -183,10 +161,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
|
|||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
|
||||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -200,21 +178,13 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ
|
|||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
github.com/mark3labs/mcp-go v0.46.0 h1:8KRibF4wcKejbLsHxCA/QBVUr5fQ9nwz/n8lGqmaALo=
|
|
||||||
github.com/mark3labs/mcp-go v0.46.0/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag=
|
|
||||||
github.com/mark3labs/mcp-go v0.54.0 h1:PZhQvd+5xrT43cUoiaKn/hDcvLUhcLc1twSEKYPTcTA=
|
github.com/mark3labs/mcp-go v0.54.0 h1:PZhQvd+5xrT43cUoiaKn/hDcvLUhcLc1twSEKYPTcTA=
|
||||||
github.com/mark3labs/mcp-go v0.54.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
|
github.com/mark3labs/mcp-go v0.54.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||||
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
|
||||||
github.com/microsoft/go-mssqldb v1.9.5 h1:orwya0X/5bsL1o+KasupTkk2eNTNFkTQG0BEe/HxCn0=
|
|
||||||
github.com/microsoft/go-mssqldb v1.9.5/go.mod h1:VCP2a0KEZZtGLRHd1PsLavLFYy/3xX2yJUPycv3Sr2Q=
|
|
||||||
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
|
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
|
||||||
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
|
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
@@ -237,20 +207,14 @@ github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArw
|
|||||||
github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc=
|
github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc=
|
||||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
|
||||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
|
||||||
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
|
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
|
||||||
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
|
||||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
|
||||||
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
|
||||||
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
|
||||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
|
||||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
|
||||||
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
|
||||||
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
|
||||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
@@ -261,8 +225,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
|
||||||
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||||
@@ -281,29 +243,20 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h
|
|||||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
|
||||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
|
||||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
|
||||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
|
||||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
|
||||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
|
||||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||||
@@ -344,8 +297,6 @@ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSW
|
|||||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
|
||||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
@@ -364,22 +315,10 @@ github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYm
|
|||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ=
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16 h1:rKv0cKPNBviXadB/+2Y/UedA/c1JnwGzUWZkdN5FdSQ=
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
github.com/uptrace/bun/dialect/mssqldialect v1.2.16/go.mod h1:J5U7tGKWDsx2Q7MwDZF2417jCdpD6yD/ZMFJcCR80bk=
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.17 h1:xEUH4WamuY9rXT9d8wHVZanhmLJCrc4s4v7frDH/PMc=
|
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.17/go.mod h1:i1NRx/5cz1nivwtV7FEb/gP3CIbRTj4AQC9/Q0lNVno=
|
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ=
|
|
||||||
github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk=
|
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16 h1:KFNZ0LxAyczKNfK/IJWMyaleO6eI9/Z5tUv3DE1NVL4=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.16/go.mod h1:IJdMeV4sLfh0LDUZl7TIxLI0LipF1vwTK3hBC7p5qLo=
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.17 h1:DFmhOollvbYHvooxoS8ZIbiGC0wXIzstKeFUmWs+TP4=
|
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.17/go.mod h1:ej8ZDsvLETvyELlRDfUtIoA57sWnATv1GhOEVsuVG/k=
|
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
|
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
|
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16 h1:6wVAiYLj1pMibRthGwy4wDLa3D5AQo32Y8rvwPd8CQ0=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.16/go.mod h1:Z7+5qK8CGZkDQiPMu+LSdVuDuR1I5jcwtkB1Pi3F82E=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.17 h1:ZipEoNr+wQJQleGy2poKSSoaQDavzc+nXTDp3ZzkA0E=
|
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.17/go.mod h1:phXmrxxeYqUhMU09FgazbfNxq9LlArdqjZqHc1ILy9U=
|
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
|
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18/go.mod h1:1MVOS/Ncy4FZbkJcgUFH6OqYoQinYNjkEwsmNQEXz2A=
|
|
||||||
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
|
||||||
github.com/uptrace/bun/driver/sqliteshim v1.2.16/go.mod h1:iKdJ06P3XS+pwKcONjSIK07bbhksH3lWsw3mpfr0+bY=
|
github.com/uptrace/bun/driver/sqliteshim v1.2.16/go.mod h1:iKdJ06P3XS+pwKcONjSIK07bbhksH3lWsw3mpfr0+bY=
|
||||||
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
|
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
|
||||||
@@ -403,47 +342,30 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
|
||||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
|
||||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
|
||||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
@@ -452,12 +374,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
|||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
|
||||||
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
|
||||||
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
|
||||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
@@ -474,22 +392,14 @@ golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOM
|
|||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
|
||||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
|
||||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
|
||||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
|
||||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -509,12 +419,8 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
|||||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
|
||||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
|
||||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -524,8 +430,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -551,8 +455,6 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
@@ -570,9 +472,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
|||||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
@@ -587,12 +488,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
|||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
|
||||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -601,24 +498,16 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
|
||||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||||
|
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94 h1:DddG61lE5LkX6144z22i0gma9BMBs5aZ9B8lZLobxyw=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:1dCETSCY2YKZNXQE3h4fun3TYwF5p8jejRKZgfWAgAY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94 h1:eZCjr/aAF8c5ccm5pb6T4EXgIei5MlAAPWPJk+5ArfY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260519071638-aa98bba5eb94/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
@@ -644,37 +533,28 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
|||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
|
||||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
|
||||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.67.4 h1:zZGmCMUVPORtKv95c2ReQN5VDjvkoRm9GWPTEPuvlWg=
|
|
||||||
modernc.org/libc v1.67.4/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
|
|
||||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||||
|
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74=
|
|
||||||
modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
|
||||||
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
|
||||||
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
|||||||
@@ -90,6 +90,30 @@ type SortOption struct {
|
|||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrimaryKeySortColumn is a sentinel SortOption.Column value that gets
|
||||||
|
// resolved to a model's actual primary key column name at query time.
|
||||||
|
// This lets a single global default sort (e.g. set once via
|
||||||
|
// Handler.SetDefaultSort) work across models with different primary keys,
|
||||||
|
// e.g. common.SortOption{Column: common.PrimaryKeySortColumn, Direction: "asc"}.
|
||||||
|
const PrimaryKeySortColumn = "$pk"
|
||||||
|
|
||||||
|
// ResolveSortColumns returns a copy of sort with any PrimaryKeySortColumn
|
||||||
|
// entries replaced by pkName. If pkName is empty, matching entries are
|
||||||
|
// dropped since there is no column to sort by.
|
||||||
|
func ResolveSortColumns(sort []SortOption, pkName string) []SortOption {
|
||||||
|
resolved := make([]SortOption, 0, len(sort))
|
||||||
|
for _, s := range sort {
|
||||||
|
if s.Column == PrimaryKeySortColumn {
|
||||||
|
if pkName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.Column = pkName
|
||||||
|
}
|
||||||
|
resolved = append(resolved, s)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
type CustomOperator struct {
|
type CustomOperator struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SQL string `json:"sql"`
|
SQL string `json:"sql"`
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveSortColumns(t *testing.T) {
|
||||||
|
sort := []SortOption{
|
||||||
|
{Column: PrimaryKeySortColumn, Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ResolveSortColumns(sort, "user_id")
|
||||||
|
want := []SortOption{
|
||||||
|
{Column: "user_id", Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ResolveSortColumns() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original slice must not be mutated
|
||||||
|
if sort[0].Column != PrimaryKeySortColumn {
|
||||||
|
t.Errorf("ResolveSortColumns() mutated input slice: %v", sort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSortColumnsDesc(t *testing.T) {
|
||||||
|
sort := []SortOption{
|
||||||
|
{Column: PrimaryKeySortColumn, Direction: "desc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ResolveSortColumns(sort, "id")
|
||||||
|
want := []SortOption{{Column: "id", Direction: "desc"}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ResolveSortColumns() = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSortColumnsEmptyPK(t *testing.T) {
|
||||||
|
sort := []SortOption{
|
||||||
|
{Column: PrimaryKeySortColumn, Direction: "asc"},
|
||||||
|
{Column: "name", Direction: "desc"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := ResolveSortColumns(sort, "")
|
||||||
|
want := []SortOption{{Column: "name", Direction: "desc"}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("ResolveSortColumns() with empty pk = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSortColumnsNil(t *testing.T) {
|
||||||
|
if got := ResolveSortColumns(nil, "id"); len(got) != 0 {
|
||||||
|
t.Errorf("ResolveSortColumns(nil) = %v, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,7 +197,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
hookCtx.Tx = tx
|
hookCtx.Tx = tx
|
||||||
|
|
||||||
// Execute BeforeQueryList hook (inside transaction)
|
// Execute BeforeQueryList hook (inside transaction)
|
||||||
if err := h.hooks.Execute(BeforeQueryList, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeQueryList, hookCtx); err != nil {
|
||||||
logger.Error("BeforeQueryList hook failed: %v", err)
|
logger.Error("BeforeQueryList hook failed: %v", err)
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return err
|
return err
|
||||||
@@ -261,7 +261,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
|
|||||||
|
|
||||||
// Execute BeforeSQLExec hook
|
// Execute BeforeSQLExec hook
|
||||||
hookCtx.SQLQuery = sqlquery
|
hookCtx.SQLQuery = sqlquery
|
||||||
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
|
||||||
logger.Error("BeforeSQLExec hook failed: %v", err)
|
logger.Error("BeforeSQLExec hook failed: %v", err)
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return err
|
return err
|
||||||
@@ -563,7 +563,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
hookCtx.Tx = tx
|
hookCtx.Tx = tx
|
||||||
|
|
||||||
// Execute BeforeQuery hook (inside transaction)
|
// Execute BeforeQuery hook (inside transaction)
|
||||||
if err := h.hooks.Execute(BeforeQuery, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeQuery, hookCtx); err != nil {
|
||||||
logger.Error("BeforeQuery hook failed: %v", err)
|
logger.Error("BeforeQuery hook failed: %v", err)
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return err
|
return err
|
||||||
@@ -582,7 +582,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
|
|||||||
sqlquery = hookCtx.SQLQuery
|
sqlquery = hookCtx.SQLQuery
|
||||||
|
|
||||||
// Execute BeforeSQLExec hook
|
// Execute BeforeSQLExec hook
|
||||||
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
|
||||||
logger.Error("BeforeSQLExec hook failed: %v", err)
|
logger.Error("BeforeSQLExec hook failed: %v", err)
|
||||||
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ const (
|
|||||||
|
|
||||||
// Response hooks (before response is sent)
|
// Response hooks (before response is sent)
|
||||||
BeforeResponse HookType = "before_response"
|
BeforeResponse HookType = "before_response"
|
||||||
|
|
||||||
|
// BeforeOp fires immediately before every SQL operation (query, query list, SQL exec).
|
||||||
|
// It fires at each individual SQL-operation hook point, so it runs once per statement executed.
|
||||||
|
BeforeOp HookType = "before_op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookContext contains all the data available to a hook
|
// HookContext contains all the data available to a hook
|
||||||
@@ -130,6 +134,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
||||||
|
// (BeforeQuery, BeforeQueryList, or BeforeSQLExec). BeforeOp always runs first so it can
|
||||||
|
// observe/veto every SQL operation regardless of type.
|
||||||
|
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
|
||||||
|
if err := r.Execute(BeforeOp, ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.Execute(hookType, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear removes all hooks for the specified type
|
// Clear removes all hooks for the specified type
|
||||||
func (r *HookRegistry) Clear(hookType HookType) {
|
func (r *HookRegistry) Clear(hookType HookType) {
|
||||||
delete(r.hooks, hookType)
|
delete(r.hooks, hookType)
|
||||||
|
|||||||
+28
-4
@@ -313,7 +313,7 @@ func (h *Handler) handleRequest(client *Client, msg *Message) {
|
|||||||
// handleRead processes a read operation
|
// handleRead processes a read operation
|
||||||
func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
|
||||||
logger.Error("[MQTTSpec] BeforeRead hook failed: %v", err)
|
logger.Error("[MQTTSpec] BeforeRead hook failed: %v", err)
|
||||||
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
||||||
return
|
return
|
||||||
@@ -356,7 +356,7 @@ func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext)
|
|||||||
// handleCreate processes a create operation
|
// handleCreate processes a create operation
|
||||||
func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
logger.Error("[MQTTSpec] BeforeCreate hook failed: %v", err)
|
logger.Error("[MQTTSpec] BeforeCreate hook failed: %v", err)
|
||||||
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
||||||
return
|
return
|
||||||
@@ -390,7 +390,7 @@ func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContex
|
|||||||
// handleUpdate processes an update operation
|
// handleUpdate processes an update operation
|
||||||
func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
logger.Error("[MQTTSpec] BeforeUpdate hook failed: %v", err)
|
logger.Error("[MQTTSpec] BeforeUpdate hook failed: %v", err)
|
||||||
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
||||||
return
|
return
|
||||||
@@ -424,7 +424,7 @@ func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContex
|
|||||||
// handleDelete processes a delete operation
|
// handleDelete processes a delete operation
|
||||||
func (h *Handler) handleDelete(client *Client, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleDelete(client *Client, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("[MQTTSpec] BeforeDelete hook failed: %v", err)
|
logger.Error("[MQTTSpec] BeforeDelete hook failed: %v", err)
|
||||||
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
|
||||||
return
|
return
|
||||||
@@ -686,6 +686,18 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
|
||||||
|
if hookCtx.Metadata == nil {
|
||||||
|
hookCtx.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
hookCtx.Metadata["query"] = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
return nil, fmt.Errorf("BeforeScan hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
|
||||||
|
query = modifiedQuery
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
if err := query.ScanModel(hookCtx.Context); err != nil {
|
if err := query.ScanModel(hookCtx.Context); err != nil {
|
||||||
return nil, fmt.Errorf("failed to read record: %w", err)
|
return nil, fmt.Errorf("failed to read record: %w", err)
|
||||||
@@ -738,6 +750,18 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
|
||||||
|
if hookCtx.Metadata == nil {
|
||||||
|
hookCtx.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
hookCtx.Metadata["query"] = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("BeforeScan hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
|
||||||
|
query = modifiedQuery
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
if err := query.ScanModel(hookCtx.Context); err != nil {
|
if err := query.ScanModel(hookCtx.Context); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to read records: %w", err)
|
return nil, nil, fmt.Errorf("failed to read records: %w", err)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const (
|
|||||||
AfterUpdate = websocketspec.AfterUpdate
|
AfterUpdate = websocketspec.AfterUpdate
|
||||||
BeforeDelete = websocketspec.BeforeDelete
|
BeforeDelete = websocketspec.BeforeDelete
|
||||||
AfterDelete = websocketspec.AfterDelete
|
AfterDelete = websocketspec.AfterDelete
|
||||||
|
BeforeScan = websocketspec.BeforeScan
|
||||||
|
|
||||||
// Subscription hooks
|
// Subscription hooks
|
||||||
BeforeSubscribe = websocketspec.BeforeSubscribe
|
BeforeSubscribe = websocketspec.BeforeSubscribe
|
||||||
@@ -46,6 +47,9 @@ const (
|
|||||||
AfterConnect = websocketspec.AfterConnect
|
AfterConnect = websocketspec.AfterConnect
|
||||||
BeforeDisconnect = websocketspec.BeforeDisconnect
|
BeforeDisconnect = websocketspec.BeforeDisconnect
|
||||||
AfterDisconnect = websocketspec.AfterDisconnect
|
AfterDisconnect = websocketspec.AfterDisconnect
|
||||||
|
|
||||||
|
// BeforeOp fires immediately before every SQL operation (read, create, update, delete)
|
||||||
|
BeforeOp = websocketspec.BeforeOp
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHookRegistry creates a new hook registry
|
// NewHookRegistry creates a new hook registry
|
||||||
|
|||||||
@@ -27,25 +27,31 @@ func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList
|
|||||||
return security.LoadSecurityRules(secCtx, securityList)
|
return security.LoadSecurityRules(secCtx, securityList)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 2: AfterRead - Apply column-level security (masking)
|
// Hook 2: BeforeScan - Apply row-level security filters
|
||||||
|
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
|
||||||
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
return security.ApplyRowSecurity(secCtx, securityList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 3: AfterRead - Apply column-level security (masking)
|
||||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.ApplyColumnSecurity(secCtx, securityList)
|
return security.ApplyColumnSecurity(secCtx, securityList)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 3 (Optional): Audit logging
|
// Hook 4 (Optional): Audit logging
|
||||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.LogDataAccess(secCtx)
|
return security.LogDataAccess(secCtx)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 4: BeforeUpdate - enforce CanUpdate rule from context/registry
|
// Hook 5: BeforeUpdate - enforce CanUpdate rule from context/registry
|
||||||
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.CheckModelUpdateAllowed(secCtx)
|
return security.CheckModelUpdateAllowed(secCtx)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 5: BeforeDelete - enforce CanDelete rule from context/registry
|
// Hook 6: BeforeDelete - enforce CanDelete rule from context/registry
|
||||||
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.CheckModelDeleteAllowed(secCtx)
|
return security.CheckModelDeleteAllowed(secCtx)
|
||||||
|
|||||||
+81
-24
@@ -30,6 +30,7 @@ type Handler struct {
|
|||||||
hooks *HookRegistry
|
hooks *HookRegistry
|
||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
|
defaultSort map[string][]common.SortOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -56,6 +57,32 @@ func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
|||||||
h.fallbackHandler = fallback
|
h.fallbackHandler = fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDefaultSort sets the sort order applied to list ("read") results when a
|
||||||
|
// request does not specify its own sort. Pass an empty schema and name to set
|
||||||
|
// a global default used by any model without a more specific default.
|
||||||
|
func (h *Handler) SetDefaultSort(schema, name string, sort ...common.SortOption) {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
h.defaultSort = make(map[string][]common.SortOption)
|
||||||
|
}
|
||||||
|
h.defaultSort[defaultSortKey(schema, name)] = sort
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultSort returns the configured default sort for a model, falling
|
||||||
|
// back to the global default (registered with an empty schema and name).
|
||||||
|
func (h *Handler) getDefaultSort(schema, name string) []common.SortOption {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sort := h.defaultSort[defaultSortKey(schema, name)]; len(sort) > 0 {
|
||||||
|
return sort
|
||||||
|
}
|
||||||
|
return h.defaultSort[defaultSortKey("", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSortKey(schema, name string) string {
|
||||||
|
return fmt.Sprintf("%s.%s", schema, name)
|
||||||
|
}
|
||||||
|
|
||||||
// GetDatabase returns the underlying database connection
|
// GetDatabase returns the underlying database connection
|
||||||
// Implements common.SpecHandler interface
|
// Implements common.SpecHandler interface
|
||||||
func (h *Handler) GetDatabase() common.Database {
|
func (h *Handler) GetDatabase() common.Database {
|
||||||
@@ -288,7 +315,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
|
||||||
statusCode, errCode, errMsg = http.StatusInternalServerError, "hook_error", "BeforeRead hook failed"
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "hook_error", "BeforeRead hook failed"
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -345,6 +372,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Where(customOp.SQL)
|
query = query.Where(customOp.SQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the configured default sort when the request didn't specify one
|
||||||
|
if len(options.Sort) == 0 {
|
||||||
|
options.Sort = common.ResolveSortColumns(h.getDefaultSort(schema, entity), reflection.GetPrimaryKeyName(model))
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
for _, sort := range options.Sort {
|
for _, sort := range options.Sort {
|
||||||
direction := "ASC"
|
direction := "ASC"
|
||||||
@@ -538,6 +570,16 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
pkName := reflection.GetPrimaryKeyName(singleResult)
|
pkName := reflection.GetPrimaryKeyName(singleResult)
|
||||||
|
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query chain so hooks (e.g. row-level security) can modify it
|
||||||
|
hookCtx.Query = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
logger.Error("BeforeScan hook failed: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
query = hookCtx.Query
|
||||||
|
|
||||||
if err := query.Scan(ctx, singleResult); err != nil {
|
if err := query.Scan(ctx, singleResult); err != nil {
|
||||||
logger.Error("Error querying record: %v", err)
|
logger.Error("Error querying record: %v", err)
|
||||||
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
|
||||||
@@ -546,6 +588,16 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
result = singleResult
|
result = singleResult
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Querying multiple records")
|
logger.Debug("Querying multiple records")
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query chain so hooks (e.g. row-level security) can modify it
|
||||||
|
hookCtx.Query = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
logger.Error("BeforeScan hook failed: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
query = hookCtx.Query
|
||||||
|
|
||||||
// Use the modelPtr already created and set on the query
|
// Use the modelPtr already created and set on the query
|
||||||
if err := query.Scan(ctx, modelPtr); err != nil {
|
if err := query.Scan(ctx, modelPtr); err != nil {
|
||||||
logger.Error("Error querying records: %v", err)
|
logger.Error("Error querying records: %v", err)
|
||||||
@@ -635,7 +687,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -682,7 +734,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -764,7 +816,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -811,7 +863,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -895,7 +947,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -948,7 +1000,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
@@ -1056,6 +1108,12 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
// Get the primary key name
|
// Get the primary key name
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
|
|
||||||
|
if targetID == nil {
|
||||||
|
logger.Error("Update request for %s.%s has no ID (not in URL, request ID, or data)", schema, entity)
|
||||||
|
h.sendError(w, http.StatusBadRequest, "missing_id", "No ID provided for update", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap in transaction to ensure BeforeUpdate hook is inside transaction
|
// Wrap in transaction to ensure BeforeUpdate hook is inside transaction
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
// Execute BeforeUpdate hooks inside transaction, before any queries run.
|
// Execute BeforeUpdate hooks inside transaction, before any queries run.
|
||||||
@@ -1075,7 +1133,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1088,21 +1146,20 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
existingRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
|
selectQuery := tx.NewSelect().Model(existingRecord).Column(reflection.GetSQLModelColumns(model)...)
|
||||||
|
|
||||||
// Apply conditions to select
|
// Apply conditions to select, based on the resolved target ID
|
||||||
if urlID != "" {
|
// (URL ID, request ID, or the "id" field embedded in the data payload).
|
||||||
logger.Debug("Updating by URL ID: %s", urlID)
|
switch id := targetID.(type) {
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
|
||||||
} else if reqID != nil {
|
|
||||||
switch id := reqID.(type) {
|
|
||||||
case string:
|
case string:
|
||||||
logger.Debug("Updating by request ID: %s", id)
|
logger.Debug("Updating by ID: %s", id)
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
case []string:
|
case []string:
|
||||||
if len(id) > 0 {
|
if len(id) > 0 {
|
||||||
logger.Debug("Updating by multiple IDs: %v", id)
|
logger.Debug("Updating by multiple IDs: %v", id)
|
||||||
selectQuery = selectQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
}
|
default:
|
||||||
|
logger.Debug("Updating by ID: %v", id)
|
||||||
|
selectQuery = selectQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := selectQuery.ScanModel(ctx); err != nil {
|
if err := selectQuery.ScanModel(ctx); err != nil {
|
||||||
@@ -1141,16 +1198,16 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
// Build update query with merged data
|
// Build update query with merged data
|
||||||
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
||||||
|
|
||||||
// Apply conditions
|
// Apply conditions, using the same target ID resolved for the select above
|
||||||
if urlID != "" {
|
switch id := targetID.(type) {
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), urlID)
|
|
||||||
} else if reqID != nil {
|
|
||||||
switch id := reqID.(type) {
|
|
||||||
case string:
|
case string:
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
case []string:
|
case []string:
|
||||||
|
if len(id) > 0 {
|
||||||
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
query = query.Where(fmt.Sprintf("%s IN (?)", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := query.Exec(ctx)
|
result, err := query.Exec(ctx)
|
||||||
@@ -1297,7 +1354,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
|
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1453,7 +1510,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
|
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1556,7 +1613,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
Tx: h.db,
|
Tx: h.db,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("BeforeDelete hook failed: %v", err)
|
logger.Error("BeforeDelete hook failed: %v", err)
|
||||||
h.sendError(w, http.StatusForbidden, "delete_forbidden", "Delete operation not allowed", err)
|
h.sendError(w, http.StatusForbidden, "delete_forbidden", "Delete operation not allowed", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -41,6 +41,36 @@ func TestSetFallbackHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultSort(t *testing.T) {
|
||||||
|
handler := NewHandler(nil, nil)
|
||||||
|
|
||||||
|
// No default configured yet
|
||||||
|
if got := handler.getDefaultSort("public", "users"); got != nil {
|
||||||
|
t.Errorf("Expected no default sort, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global default
|
||||||
|
global := []common.SortOption{{Column: "created_at", Direction: "desc"}}
|
||||||
|
handler.SetDefaultSort("", "", global...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v, got %v", global, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-model default overrides the global default
|
||||||
|
perModel := []common.SortOption{{Column: "name", Direction: "asc"}}
|
||||||
|
handler.SetDefaultSort("public", "users", perModel...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, perModel) {
|
||||||
|
t.Errorf("Expected per-model default sort %v, got %v", perModel, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other models still fall back to the global default
|
||||||
|
if got := handler.getDefaultSort("public", "orders"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v for unrelated model, got %v", global, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDatabase(t *testing.T) {
|
func TestGetDatabase(t *testing.T) {
|
||||||
handler := NewHandler(nil, nil)
|
handler := NewHandler(nil, nil)
|
||||||
db := handler.GetDatabase()
|
db := handler.GetDatabase()
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ const (
|
|||||||
|
|
||||||
// Scan/Execute operation hooks (for query building)
|
// Scan/Execute operation hooks (for query building)
|
||||||
BeforeScan HookType = "before_scan"
|
BeforeScan HookType = "before_scan"
|
||||||
|
|
||||||
|
// BeforeOp fires immediately before every SQL operation (read, create, update, delete, scan).
|
||||||
|
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
|
||||||
|
// individual SQL-operation hook point, so it runs once per statement executed.
|
||||||
|
BeforeOp HookType = "before_op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookContext contains all the data available to a hook
|
// HookContext contains all the data available to a hook
|
||||||
@@ -128,6 +133,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
||||||
|
// (BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete, or BeforeScan). BeforeOp always
|
||||||
|
// runs first so it can observe/veto every SQL operation regardless of type.
|
||||||
|
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
|
||||||
|
if err := r.Execute(BeforeOp, ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.Execute(hookType, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear removes all hooks for the specified type
|
// Clear removes all hooks for the specified type
|
||||||
func (r *HookRegistry) Clear(hookType HookType) {
|
func (r *HookRegistry) Clear(hookType HookType) {
|
||||||
delete(r.hooks, hookType)
|
delete(r.hooks, hookType)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package restheadspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetDefaultSort(t *testing.T) {
|
||||||
|
handler := NewHandler(nil, nil)
|
||||||
|
|
||||||
|
// No default configured yet
|
||||||
|
if got := handler.getDefaultSort("public", "users"); got != nil {
|
||||||
|
t.Errorf("Expected no default sort, got %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global default
|
||||||
|
global := []common.SortOption{{Column: "created_at", Direction: "desc"}}
|
||||||
|
handler.SetDefaultSort("", "", global...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v, got %v", global, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-model default overrides the global default
|
||||||
|
perModel := []common.SortOption{{Column: "name", Direction: "asc"}}
|
||||||
|
handler.SetDefaultSort("public", "users", perModel...)
|
||||||
|
|
||||||
|
if got := handler.getDefaultSort("public", "users"); !reflect.DeepEqual(got, perModel) {
|
||||||
|
t.Errorf("Expected per-model default sort %v, got %v", perModel, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other models still fall back to the global default
|
||||||
|
if got := handler.getDefaultSort("public", "orders"); !reflect.DeepEqual(got, global) {
|
||||||
|
t.Errorf("Expected global default sort %v for unrelated model, got %v", global, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
-12
@@ -32,6 +32,7 @@ type Handler struct {
|
|||||||
nestedProcessor *common.NestedCUDProcessor
|
nestedProcessor *common.NestedCUDProcessor
|
||||||
fallbackHandler FallbackHandler
|
fallbackHandler FallbackHandler
|
||||||
openAPIGenerator func() (string, error)
|
openAPIGenerator func() (string, error)
|
||||||
|
defaultSort map[string][]common.SortOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new API handler with database and registry abstractions
|
// NewHandler creates a new API handler with database and registry abstractions
|
||||||
@@ -64,6 +65,33 @@ func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
|||||||
h.fallbackHandler = fallback
|
h.fallbackHandler = fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDefaultSort sets the sort order applied to list ("read") results when a
|
||||||
|
// request does not specify its own sort (no x-sort header / sort query param).
|
||||||
|
// Pass an empty schema and name to set a global default used by any model
|
||||||
|
// without a more specific default.
|
||||||
|
func (h *Handler) SetDefaultSort(schema, name string, sort ...common.SortOption) {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
h.defaultSort = make(map[string][]common.SortOption)
|
||||||
|
}
|
||||||
|
h.defaultSort[defaultSortKey(schema, name)] = sort
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultSort returns the configured default sort for a model, falling
|
||||||
|
// back to the global default (registered with an empty schema and name).
|
||||||
|
func (h *Handler) getDefaultSort(schema, name string) []common.SortOption {
|
||||||
|
if h.defaultSort == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if sort := h.defaultSort[defaultSortKey(schema, name)]; len(sort) > 0 {
|
||||||
|
return sort
|
||||||
|
}
|
||||||
|
return h.defaultSort[defaultSortKey("", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSortKey(schema, name string) string {
|
||||||
|
return fmt.Sprintf("%s.%s", schema, name)
|
||||||
|
}
|
||||||
|
|
||||||
// handlePanic is a helper function to handle panics with stack traces
|
// handlePanic is a helper function to handle panics with stack traces
|
||||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||||
stack := debug.Stack()
|
stack := debug.Stack()
|
||||||
@@ -372,7 +400,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
hookCtx.Tx = tx
|
hookCtx.Tx = tx
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
|
||||||
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -645,6 +673,11 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
query = query.Where(fmt.Sprintf("%s.%s = ?", common.QuoteIdent(tableAlias), common.QuoteIdent(pkName)), id)
|
query = query.Where(fmt.Sprintf("%s.%s = ?", common.QuoteIdent(tableAlias), common.QuoteIdent(pkName)), id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the configured default sort when the request didn't specify one
|
||||||
|
if len(options.Sort) == 0 {
|
||||||
|
options.Sort = common.ResolveSortColumns(h.getDefaultSort(schema, entity), reflection.GetPrimaryKeyName(model))
|
||||||
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
tableAlias := reflection.ExtractTableNameOnly(tableName)
|
tableAlias := reflection.ExtractTableNameOnly(tableName)
|
||||||
for _, sort := range options.Sort {
|
for _, sort := range options.Sort {
|
||||||
@@ -792,7 +825,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
|
|
||||||
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
||||||
hookCtx.Query = query
|
hookCtx.Query = query
|
||||||
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
logger.Error("BeforeScan hook failed: %v", err)
|
logger.Error("BeforeScan hook failed: %v", err)
|
||||||
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
||||||
return err
|
return err
|
||||||
@@ -1184,7 +1217,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
results := make([]interface{}, 0)
|
results := make([]interface{}, 0)
|
||||||
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
hookCtx.Tx = tx
|
hookCtx.Tx = tx
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1269,7 +1302,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
Query: query,
|
Query: query,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
if err := h.hooks.Execute(BeforeScan, itemHookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, itemHookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeScan hook failed for item %d: %w", i, err)
|
return fmt.Errorf("BeforeScan hook failed for item %d: %w", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1423,7 +1456,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Writer: w,
|
Writer: w,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
return fmt.Errorf("BeforeUpdate hook failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1500,7 +1533,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
||||||
hookCtx.Query = query
|
hookCtx.Query = query
|
||||||
hookCtx.Tx = tx
|
hookCtx.Tx = tx
|
||||||
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
return fmt.Errorf("BeforeScan hook failed: %w", err)
|
return fmt.Errorf("BeforeScan hook failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1544,7 +1577,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
// pooled connection rather than the now-dead tx.
|
// pooled connection rather than the now-dead tx.
|
||||||
hookCtx.Tx = h.db
|
hookCtx.Tx = h.db
|
||||||
hookCtx.Query = selectQuery
|
hookCtx.Query = selectQuery
|
||||||
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
logger.Error("BeforeScan hook failed: %v", err)
|
logger.Error("BeforeScan hook failed: %v", err)
|
||||||
h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
|
||||||
return
|
return
|
||||||
@@ -1619,7 +1652,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("BeforeDelete hook failed for ID %s: %v", itemID, err)
|
logger.Error("BeforeDelete hook failed for ID %s: %v", itemID, err)
|
||||||
return fmt.Errorf("delete not allowed for ID %s: %w", itemID, err)
|
return fmt.Errorf("delete not allowed for ID %s: %w", itemID, err)
|
||||||
}
|
}
|
||||||
@@ -1693,7 +1726,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
|
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
|
||||||
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
|
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
|
||||||
}
|
}
|
||||||
@@ -1751,7 +1784,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
|
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
|
||||||
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
|
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
|
||||||
}
|
}
|
||||||
@@ -1836,7 +1869,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
Data: recordToDelete,
|
Data: recordToDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("BeforeDelete hook failed: %v", err)
|
logger.Error("BeforeDelete hook failed: %v", err)
|
||||||
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return
|
return
|
||||||
@@ -1847,7 +1880,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
|
|||||||
|
|
||||||
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
||||||
hookCtx.Query = query
|
hookCtx.Query = query
|
||||||
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
logger.Error("BeforeScan hook failed: %v", err)
|
logger.Error("BeforeScan hook failed: %v", err)
|
||||||
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ const (
|
|||||||
|
|
||||||
// Scan/Execute operation hooks
|
// Scan/Execute operation hooks
|
||||||
BeforeScan HookType = "before_scan"
|
BeforeScan HookType = "before_scan"
|
||||||
|
|
||||||
|
// BeforeOp fires immediately before every SQL operation (read, create, update, delete, scan).
|
||||||
|
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
|
||||||
|
// individual SQL-operation hook point, so it runs once per statement executed.
|
||||||
|
BeforeOp HookType = "before_op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookContext contains all the data available to a hook
|
// HookContext contains all the data available to a hook
|
||||||
@@ -137,6 +142,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
||||||
|
// (BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete, or BeforeScan). BeforeOp always
|
||||||
|
// runs first so it can observe/veto every SQL operation regardless of type.
|
||||||
|
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
|
||||||
|
if err := r.Execute(BeforeOp, ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.Execute(hookType, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear removes all hooks for the specified type
|
// Clear removes all hooks for the specified type
|
||||||
func (r *HookRegistry) Clear(hookType HookType) {
|
func (r *HookRegistry) Clear(hookType HookType) {
|
||||||
delete(r.hooks, hookType)
|
delete(r.hooks, hookType)
|
||||||
|
|||||||
@@ -1597,6 +1597,8 @@ CREATE TABLE IF NOT EXISTS oauth_clients (
|
|||||||
client_name VARCHAR(255),
|
client_name VARCHAR(255),
|
||||||
grant_types TEXT[] DEFAULT ARRAY['authorization_code'],
|
grant_types TEXT[] DEFAULT ARRAY['authorization_code'],
|
||||||
allowed_scopes TEXT[] DEFAULT ARRAY['openid','profile','email'],
|
allowed_scopes TEXT[] DEFAULT ARRAY['openid','profile','email'],
|
||||||
|
client_secret_hash TEXT, -- sha256 hex of the confidential-client secret; NULL for public clients
|
||||||
|
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
|
||||||
is_active BOOLEAN DEFAULT true,
|
is_active BOOLEAN DEFAULT true,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -1634,13 +1636,15 @@ DECLARE
|
|||||||
BEGIN
|
BEGIN
|
||||||
v_client_id := p_data->>'client_id';
|
v_client_id := p_data->>'client_id';
|
||||||
|
|
||||||
INSERT INTO oauth_clients (client_id, redirect_uris, client_name, grant_types, allowed_scopes)
|
INSERT INTO oauth_clients (client_id, redirect_uris, client_name, grant_types, allowed_scopes, client_secret_hash, token_endpoint_auth_method)
|
||||||
VALUES (
|
VALUES (
|
||||||
v_client_id,
|
v_client_id,
|
||||||
ARRAY(SELECT jsonb_array_elements_text(p_data->'redirect_uris')),
|
ARRAY(SELECT jsonb_array_elements_text(p_data->'redirect_uris')),
|
||||||
p_data->>'client_name',
|
p_data->>'client_name',
|
||||||
COALESCE(ARRAY(SELECT jsonb_array_elements_text(p_data->'grant_types')), ARRAY['authorization_code']),
|
COALESCE(ARRAY(SELECT jsonb_array_elements_text(p_data->'grant_types')), ARRAY['authorization_code']),
|
||||||
COALESCE(ARRAY(SELECT jsonb_array_elements_text(p_data->'allowed_scopes')), ARRAY['openid','profile','email'])
|
COALESCE(ARRAY(SELECT jsonb_array_elements_text(p_data->'allowed_scopes')), ARRAY['openid','profile','email']),
|
||||||
|
NULLIF(p_data->>'client_secret_hash', ''),
|
||||||
|
COALESCE(NULLIF(p_data->>'token_endpoint_auth_method', ''), 'none')
|
||||||
)
|
)
|
||||||
RETURNING to_jsonb(oauth_clients.*) INTO v_row;
|
RETURNING to_jsonb(oauth_clients.*) INTO v_row;
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ CREATE TABLE IF NOT EXISTS oauth_clients (
|
|||||||
client_name VARCHAR(255),
|
client_name VARCHAR(255),
|
||||||
grant_types TEXT, -- JSON-encoded []string
|
grant_types TEXT, -- JSON-encoded []string
|
||||||
allowed_scopes TEXT, -- JSON-encoded []string
|
allowed_scopes TEXT, -- JSON-encoded []string
|
||||||
|
client_secret_hash TEXT, -- sha256 hex of the confidential-client secret; NULL for public clients
|
||||||
|
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
|
||||||
is_active BOOLEAN DEFAULT 1,
|
is_active BOOLEAN DEFAULT 1,
|
||||||
created_at TIMESTAMP
|
created_at TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|||||||
+399
-10
@@ -3,8 +3,11 @@ package security
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -12,6 +15,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OAuthServerConfig configures the MCP-standard OAuth2 authorization server.
|
// OAuthServerConfig configures the MCP-standard OAuth2 authorization server.
|
||||||
@@ -44,6 +50,15 @@ type OAuthServerConfig struct {
|
|||||||
|
|
||||||
// AuthCodeTTL is the auth code lifetime. Defaults to 2 minutes.
|
// AuthCodeTTL is the auth code lifetime. Defaults to 2 minutes.
|
||||||
AuthCodeTTL time.Duration
|
AuthCodeTTL time.Duration
|
||||||
|
|
||||||
|
// ResourceIdentifier is this server's protected-resource identifier, advertised in
|
||||||
|
// RFC 9728 metadata. Defaults to Issuer.
|
||||||
|
ResourceIdentifier string
|
||||||
|
|
||||||
|
// SigningKey signs id_tokens (RS256) and is exposed via the JWKS endpoint. If nil, an
|
||||||
|
// RSA-2048 key is generated in memory when the server starts. Supply a persistent key
|
||||||
|
// for multi-instance deployments so id_tokens remain verifiable across restarts/instances.
|
||||||
|
SigningKey *rsa.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// oauthClient is a dynamically registered OAuth2 client (RFC 7591).
|
// oauthClient is a dynamically registered OAuth2 client (RFC 7591).
|
||||||
@@ -53,6 +68,14 @@ type oauthClient struct {
|
|||||||
ClientName string `json:"client_name,omitempty"`
|
ClientName string `json:"client_name,omitempty"`
|
||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
AllowedScopes []string `json:"allowed_scopes,omitempty"`
|
AllowedScopes []string `json:"allowed_scopes,omitempty"`
|
||||||
|
ClientSecretHash string `json:"client_secret_hash,omitempty"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// isConfidential reports whether the client has a registered secret and must
|
||||||
|
// authenticate itself at the token endpoint.
|
||||||
|
func (c *oauthClient) isConfidential() bool {
|
||||||
|
return c.ClientSecretHash != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// pendingAuth tracks an in-progress authorization code exchange.
|
// pendingAuth tracks an in-progress authorization code exchange.
|
||||||
@@ -85,13 +108,25 @@ type externalProvider struct {
|
|||||||
// The server exposes these RFC-compliant endpoints:
|
// The server exposes these RFC-compliant endpoints:
|
||||||
//
|
//
|
||||||
// GET /.well-known/oauth-authorization-server RFC 8414 — server metadata discovery
|
// GET /.well-known/oauth-authorization-server RFC 8414 — server metadata discovery
|
||||||
|
// GET /.well-known/openid-configuration OIDC discovery (superset of the above)
|
||||||
|
// GET /.well-known/oauth-protected-resource RFC 9728 — protected resource metadata
|
||||||
// POST /oauth/register RFC 7591 — dynamic client registration
|
// POST /oauth/register RFC 7591 — dynamic client registration
|
||||||
// GET /oauth/authorize OAuth 2.1 + PKCE — start authorization
|
// GET /oauth/authorize OAuth 2.1 + PKCE — start authorization
|
||||||
// POST /oauth/authorize Direct login form submission
|
// POST /oauth/authorize Direct login form submission
|
||||||
// POST /oauth/token Token exchange and refresh
|
// POST /oauth/token Token exchange: authorization_code,
|
||||||
|
// refresh_token, client_credentials (RFC 6749 §4.4)
|
||||||
// POST /oauth/revoke RFC 7009 — token revocation
|
// POST /oauth/revoke RFC 7009 — token revocation
|
||||||
// POST /oauth/introspect RFC 7662 — token introspection
|
// POST /oauth/introspect RFC 7662 — token introspection
|
||||||
|
// GET /oauth/userinfo OIDC UserInfo endpoint
|
||||||
|
// GET /oauth/jwks.json JWKS — id_token verification keys
|
||||||
// GET {ProviderCallbackPath} Internal — external provider callback
|
// GET {ProviderCallbackPath} Internal — external provider callback
|
||||||
|
//
|
||||||
|
// Confidential clients (registered with token_endpoint_auth_method other than "none", or
|
||||||
|
// any grant_types including client_credentials) authenticate at /oauth/token via
|
||||||
|
// client_secret_basic or client_secret_post. Public clients keep relying on PKCE alone.
|
||||||
|
//
|
||||||
|
// When the granted scope includes "openid", authorization_code and refresh_token responses
|
||||||
|
// include an RS256-signed id_token (see OAuthServerConfig.SigningKey).
|
||||||
type OAuthServer struct {
|
type OAuthServer struct {
|
||||||
cfg OAuthServerConfig
|
cfg OAuthServerConfig
|
||||||
auth *DatabaseAuthenticator // nil = only external providers
|
auth *DatabaseAuthenticator // nil = only external providers
|
||||||
@@ -102,6 +137,9 @@ type OAuthServer struct {
|
|||||||
pending map[string]*pendingAuth // provider_state → pending (external flow)
|
pending map[string]*pendingAuth // provider_state → pending (external flow)
|
||||||
codes map[string]*pendingAuth // auth_code → pending (post-auth)
|
codes map[string]*pendingAuth // auth_code → pending (post-auth)
|
||||||
|
|
||||||
|
signingKey *rsa.PrivateKey
|
||||||
|
signingKeyID string
|
||||||
|
|
||||||
done chan struct{} // closed by Close() to stop background goroutines
|
done chan struct{} // closed by Close() to stop background goroutines
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +168,33 @@ func NewOAuthServer(cfg OAuthServerConfig, auth *DatabaseAuthenticator) *OAuthSe
|
|||||||
}
|
}
|
||||||
// Normalize issuer: remove trailing slash to ensure consistent endpoint URL construction.
|
// Normalize issuer: remove trailing slash to ensure consistent endpoint URL construction.
|
||||||
cfg.Issuer = strings.TrimSuffix(cfg.Issuer, "/")
|
cfg.Issuer = strings.TrimSuffix(cfg.Issuer, "/")
|
||||||
|
if cfg.ResourceIdentifier == "" {
|
||||||
|
cfg.ResourceIdentifier = cfg.Issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
signingKey := cfg.SigningKey
|
||||||
|
if signingKey == nil {
|
||||||
|
var err error
|
||||||
|
signingKey, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
// Signing keys are only required for id_token issuance (OIDC "openid" scope);
|
||||||
|
// leaving signingKey nil degrades gracefully by omitting id_token/JWKS support.
|
||||||
|
signingKey = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s := &OAuthServer{
|
s := &OAuthServer{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
auth: auth,
|
auth: auth,
|
||||||
clients: make(map[string]*oauthClient),
|
clients: make(map[string]*oauthClient),
|
||||||
pending: make(map[string]*pendingAuth),
|
pending: make(map[string]*pendingAuth),
|
||||||
codes: make(map[string]*pendingAuth),
|
codes: make(map[string]*pendingAuth),
|
||||||
|
signingKey: signingKey,
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
if signingKey != nil {
|
||||||
|
s.signingKeyID = rsaKeyID(&signingKey.PublicKey)
|
||||||
|
}
|
||||||
go s.cleanupExpired()
|
go s.cleanupExpired()
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
@@ -178,11 +235,15 @@ func (s *OAuthServer) ProviderCallbackPath() string {
|
|||||||
func (s *OAuthServer) HTTPHandler() http.Handler {
|
func (s *OAuthServer) HTTPHandler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/.well-known/oauth-authorization-server", s.metadataHandler)
|
mux.HandleFunc("/.well-known/oauth-authorization-server", s.metadataHandler)
|
||||||
|
mux.HandleFunc("/.well-known/openid-configuration", s.openIDConfigurationHandler)
|
||||||
|
mux.HandleFunc("/.well-known/oauth-protected-resource", s.protectedResourceHandler)
|
||||||
mux.HandleFunc("/oauth/register", s.registerHandler)
|
mux.HandleFunc("/oauth/register", s.registerHandler)
|
||||||
mux.HandleFunc("/oauth/authorize", s.authorizeHandler)
|
mux.HandleFunc("/oauth/authorize", s.authorizeHandler)
|
||||||
mux.HandleFunc("/oauth/token", s.tokenHandler)
|
mux.HandleFunc("/oauth/token", s.tokenHandler)
|
||||||
mux.HandleFunc("/oauth/revoke", s.revokeHandler)
|
mux.HandleFunc("/oauth/revoke", s.revokeHandler)
|
||||||
mux.HandleFunc("/oauth/introspect", s.introspectHandler)
|
mux.HandleFunc("/oauth/introspect", s.introspectHandler)
|
||||||
|
mux.HandleFunc("/oauth/userinfo", s.userinfoHandler)
|
||||||
|
mux.HandleFunc("/oauth/jwks.json", s.jwksHandler)
|
||||||
mux.HandleFunc(s.cfg.ProviderCallbackPath, s.providerCallbackHandler)
|
mux.HandleFunc(s.cfg.ProviderCallbackPath, s.providerCallbackHandler)
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
@@ -217,25 +278,127 @@ func (s *OAuthServer) cleanupExpired() {
|
|||||||
// RFC 8414 — Server metadata
|
// RFC 8414 — Server metadata
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
func (s *OAuthServer) metadataHandler(w http.ResponseWriter, r *http.Request) {
|
// serverMetadata builds the fields shared by RFC 8414 authorization-server
|
||||||
|
// metadata and OIDC discovery metadata.
|
||||||
|
func (s *OAuthServer) serverMetadata() map[string]interface{} {
|
||||||
issuer := s.cfg.Issuer
|
issuer := s.cfg.Issuer
|
||||||
meta := map[string]interface{}{
|
grantTypes := []string{"authorization_code", "refresh_token"}
|
||||||
|
if s.auth != nil {
|
||||||
|
grantTypes = append(grantTypes, "client_credentials")
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
"issuer": issuer,
|
"issuer": issuer,
|
||||||
"authorization_endpoint": issuer + "/oauth/authorize",
|
"authorization_endpoint": issuer + "/oauth/authorize",
|
||||||
"token_endpoint": issuer + "/oauth/token",
|
"token_endpoint": issuer + "/oauth/token",
|
||||||
"registration_endpoint": issuer + "/oauth/register",
|
"registration_endpoint": issuer + "/oauth/register",
|
||||||
"revocation_endpoint": issuer + "/oauth/revoke",
|
"revocation_endpoint": issuer + "/oauth/revoke",
|
||||||
"introspection_endpoint": issuer + "/oauth/introspect",
|
"introspection_endpoint": issuer + "/oauth/introspect",
|
||||||
|
"userinfo_endpoint": issuer + "/oauth/userinfo",
|
||||||
|
"jwks_uri": issuer + "/oauth/jwks.json",
|
||||||
"scopes_supported": s.cfg.DefaultScopes,
|
"scopes_supported": s.cfg.DefaultScopes,
|
||||||
"response_types_supported": []string{"code"},
|
"response_types_supported": []string{"code"},
|
||||||
"grant_types_supported": []string{"authorization_code", "refresh_token"},
|
"grant_types_supported": grantTypes,
|
||||||
"code_challenge_methods_supported": []string{"S256"},
|
"code_challenge_methods_supported": []string{"S256"},
|
||||||
"token_endpoint_auth_methods_supported": []string{"none"},
|
"token_endpoint_auth_methods_supported": []string{"none", "client_secret_basic", "client_secret_post"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OAuthServer) metadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(s.serverMetadata()) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// OIDC discovery — GET /.well-known/openid-configuration
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *OAuthServer) openIDConfigurationHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
meta := s.serverMetadata()
|
||||||
|
meta["subject_types_supported"] = []string{"public"}
|
||||||
|
meta["id_token_signing_alg_values_supported"] = []string{"RS256"}
|
||||||
|
meta["claims_supported"] = []string{"sub", "iss", "aud", "exp", "iat", "email", "preferred_username"}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(meta) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// RFC 9728 — Protected Resource Metadata
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *OAuthServer) protectedResourceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
meta := map[string]interface{}{
|
||||||
|
"resource": s.cfg.ResourceIdentifier,
|
||||||
|
"authorization_servers": []string{s.cfg.Issuer},
|
||||||
|
"scopes_supported": s.cfg.DefaultScopes,
|
||||||
|
"bearer_methods_supported": []string{"header"},
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(meta) //nolint:errcheck
|
json.NewEncoder(w).Encode(meta) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// JWKS — GET /oauth/jwks.json
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *OAuthServer) jwksHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if s.signingKey == nil {
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{"keys": []interface{}{}}) //nolint:errcheck
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pub := s.signingKey.PublicKey
|
||||||
|
jwk := map[string]interface{}{
|
||||||
|
"kty": "RSA",
|
||||||
|
"use": "sig",
|
||||||
|
"alg": "RS256",
|
||||||
|
"kid": s.signingKeyID,
|
||||||
|
"n": base64.RawURLEncoding.EncodeToString(pub.N.Bytes()),
|
||||||
|
"e": base64.RawURLEncoding.EncodeToString(bigEndianBytes(pub.E)),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{"keys": []interface{}{jwk}}) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Userinfo — GET/POST /oauth/userinfo
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *OAuthServer) userinfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
auth := r.Header.Get("Authorization")
|
||||||
|
token := strings.TrimPrefix(auth, "Bearer ")
|
||||||
|
if token == "" || token == auth {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
|
||||||
|
writeOAuthError(w, "invalid_token", "missing bearer token", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authToUse := s.auth
|
||||||
|
if authToUse == nil {
|
||||||
|
s.mu.RLock()
|
||||||
|
if len(s.providers) > 0 {
|
||||||
|
authToUse = s.providers[0].auth
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
}
|
||||||
|
if authToUse == nil {
|
||||||
|
writeOAuthError(w, "invalid_token", "no authenticator configured", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := authToUse.OAuthIntrospectToken(r.Context(), token)
|
||||||
|
if err != nil || !info.Active {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
|
||||||
|
writeOAuthError(w, "invalid_token", "token is inactive or invalid", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{ //nolint:errcheck
|
||||||
|
"sub": info.Sub,
|
||||||
|
"preferred_username": info.Username,
|
||||||
|
"email": info.Email,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// RFC 7591 — Dynamic client registration
|
// RFC 7591 — Dynamic client registration
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -250,6 +413,7 @@ func (s *OAuthServer) registerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ClientName string `json:"client_name"`
|
ClientName string `json:"client_name"`
|
||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
AllowedScopes []string `json:"allowed_scopes"`
|
AllowedScopes []string `json:"allowed_scopes"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
writeOAuthError(w, "invalid_request", "malformed JSON", http.StatusBadRequest)
|
writeOAuthError(w, "invalid_request", "malformed JSON", http.StatusBadRequest)
|
||||||
@@ -272,12 +436,37 @@ func (s *OAuthServer) registerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// client_credentials is a machine-to-machine grant and requires a confidential
|
||||||
|
// client (RFC 6749 §4.4), so it always forces secret issuance regardless of the
|
||||||
|
// requested auth method.
|
||||||
|
authMethod := req.TokenEndpointAuthMethod
|
||||||
|
if authMethod == "" {
|
||||||
|
authMethod = "none"
|
||||||
|
}
|
||||||
|
if oauthSliceContains(grantTypes, "client_credentials") && authMethod == "none" {
|
||||||
|
authMethod = "client_secret_basic"
|
||||||
|
}
|
||||||
|
|
||||||
|
var plaintextSecret string
|
||||||
|
var secretHash string
|
||||||
|
if authMethod != "none" {
|
||||||
|
plaintextSecret, err = randomOAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
secretHash = hashClientSecret(plaintextSecret)
|
||||||
|
}
|
||||||
|
|
||||||
client := &oauthClient{
|
client := &oauthClient{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
RedirectURIs: req.RedirectURIs,
|
RedirectURIs: req.RedirectURIs,
|
||||||
ClientName: req.ClientName,
|
ClientName: req.ClientName,
|
||||||
GrantTypes: grantTypes,
|
GrantTypes: grantTypes,
|
||||||
AllowedScopes: allowedScopes,
|
AllowedScopes: allowedScopes,
|
||||||
|
ClientSecretHash: secretHash,
|
||||||
|
TokenEndpointAuthMethod: authMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.cfg.PersistClients && s.auth != nil {
|
if s.cfg.PersistClients && s.auth != nil {
|
||||||
@@ -287,6 +476,8 @@ func (s *OAuthServer) registerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ClientName: client.ClientName,
|
ClientName: client.ClientName,
|
||||||
GrantTypes: client.GrantTypes,
|
GrantTypes: client.GrantTypes,
|
||||||
AllowedScopes: client.AllowedScopes,
|
AllowedScopes: client.AllowedScopes,
|
||||||
|
ClientSecretHash: client.ClientSecretHash,
|
||||||
|
TokenEndpointAuthMethod: client.TokenEndpointAuthMethod,
|
||||||
}
|
}
|
||||||
if _, err := s.auth.OAuthRegisterClient(r.Context(), dbClient); err != nil {
|
if _, err := s.auth.OAuthRegisterClient(r.Context(), dbClient); err != nil {
|
||||||
http.Error(w, "server error", http.StatusInternalServerError)
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
@@ -298,9 +489,25 @@ func (s *OAuthServer) registerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.clients[clientID] = client
|
s.clients[clientID] = client
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
// RFC 7591 registration response: the plaintext secret is returned exactly once here
|
||||||
|
// and never persisted or served again — only its hash (client.ClientSecretHash) is
|
||||||
|
// stored, and that hash is deliberately excluded from this response.
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"client_id": client.ClientID,
|
||||||
|
"redirect_uris": client.RedirectURIs,
|
||||||
|
"client_name": client.ClientName,
|
||||||
|
"grant_types": client.GrantTypes,
|
||||||
|
"allowed_scopes": client.AllowedScopes,
|
||||||
|
"token_endpoint_auth_method": client.TokenEndpointAuthMethod,
|
||||||
|
}
|
||||||
|
if plaintextSecret != "" {
|
||||||
|
resp["client_secret"] = plaintextSecret
|
||||||
|
resp["client_secret_expires_at"] = 0
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
json.NewEncoder(w).Encode(client) //nolint:errcheck
|
json.NewEncoder(w).Encode(resp) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -577,6 +784,8 @@ func (s *OAuthServer) tokenHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleAuthCodeGrant(w, r)
|
s.handleAuthCodeGrant(w, r)
|
||||||
case "refresh_token":
|
case "refresh_token":
|
||||||
s.handleRefreshGrant(w, r)
|
s.handleRefreshGrant(w, r)
|
||||||
|
case "client_credentials":
|
||||||
|
s.handleClientCredentialsGrant(w, r)
|
||||||
default:
|
default:
|
||||||
writeOAuthError(w, "unsupported_grant_type", "", http.StatusBadRequest)
|
writeOAuthError(w, "unsupported_grant_type", "", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
@@ -593,6 +802,15 @@ func (s *OAuthServer) handleAuthCodeGrant(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confidential clients (those registered with a client_secret) must authenticate;
|
||||||
|
// public clients keep relying on PKCE alone, unchanged from prior behavior.
|
||||||
|
if client, ok := s.lookupOrFetchClient(r.Context(), clientID); ok && client.isConfidential() {
|
||||||
|
if _, err := s.authenticateClient(r); err != nil {
|
||||||
|
writeOAuthError(w, "invalid_client", err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var sessionToken string
|
var sessionToken string
|
||||||
var refreshToken string
|
var refreshToken string
|
||||||
var scopes []string
|
var scopes []string
|
||||||
@@ -647,12 +865,13 @@ func (s *OAuthServer) handleAuthCodeGrant(w http.ResponseWriter, r *http.Request
|
|||||||
scopes = pending.Scopes
|
scopes = pending.Scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
s.writeOAuthToken(w, sessionToken, refreshToken, scopes)
|
s.writeOAuthToken(w, r, sessionToken, refreshToken, clientID, scopes, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request) {
|
func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request) {
|
||||||
refreshToken := r.FormValue("refresh_token")
|
refreshToken := r.FormValue("refresh_token")
|
||||||
providerName := r.FormValue("provider")
|
providerName := r.FormValue("provider")
|
||||||
|
clientID := r.FormValue("client_id")
|
||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
writeOAuthError(w, "invalid_request", "refresh_token required", http.StatusBadRequest)
|
writeOAuthError(w, "invalid_request", "refresh_token required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -666,7 +885,7 @@ func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request)
|
|||||||
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
s.writeOAuthToken(w, r, loginResp.Token, loginResp.RefreshToken, clientID, nil, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,13 +895,86 @@ func (s *OAuthServer) handleRefreshGrant(w http.ResponseWriter, r *http.Request)
|
|||||||
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
writeOAuthError(w, "invalid_grant", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.writeOAuthToken(w, loginResp.Token, loginResp.RefreshToken, nil)
|
s.writeOAuthToken(w, r, loginResp.Token, loginResp.RefreshToken, clientID, nil, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeOAuthError(w, "invalid_grant", "no provider available for refresh", http.StatusBadRequest)
|
writeOAuthError(w, "invalid_grant", "no provider available for refresh", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// RFC 6749 §4.4 — Client credentials grant
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func (s *OAuthServer) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.auth == nil {
|
||||||
|
writeOAuthError(w, "unsupported_grant_type", "client_credentials requires a local user store", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := s.authenticateClient(r)
|
||||||
|
if err != nil {
|
||||||
|
w.Header().Set("WWW-Authenticate", `Basic realm="oauth"`)
|
||||||
|
writeOAuthError(w, "invalid_client", err.Error(), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !oauthSliceContains(client.GrantTypes, "client_credentials") {
|
||||||
|
writeOAuthError(w, "unauthorized_client", "client is not authorized for client_credentials", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requested := strings.Fields(r.FormValue("scope"))
|
||||||
|
effectiveScopes := client.AllowedScopes
|
||||||
|
if len(requested) > 0 {
|
||||||
|
effectiveScopes = nil
|
||||||
|
for _, sc := range requested {
|
||||||
|
if oauthSliceContains(client.AllowedScopes, sc) {
|
||||||
|
effectiveScopes = append(effectiveScopes, sc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(effectiveScopes) == 0 {
|
||||||
|
writeOAuthError(w, "invalid_scope", "no requested scope is allowed for this client", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// client_credentials tokens have no end user, but the rest of the stack (RLS-scoping
|
||||||
|
// hooks, introspection) expects every access token to resolve to a user_sessions row
|
||||||
|
// with a user_id. Represent the client as a deterministic synthetic "service account"
|
||||||
|
// user so the existing get-or-create/create-session/introspection pipeline handles it
|
||||||
|
// unchanged — no new tables or code paths required.
|
||||||
|
userCtx := &UserContext{
|
||||||
|
UserName: "client:" + client.ClientID,
|
||||||
|
Email: "oauth-client-" + client.ClientID + "@service.internal",
|
||||||
|
RemoteID: client.ClientID,
|
||||||
|
Roles: effectiveScopes,
|
||||||
|
}
|
||||||
|
userID, err := s.auth.oauth2GetOrCreateUser(r.Context(), userCtx, "oauth2_client")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionToken, err := randomOAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiresAt := time.Now().Add(s.cfg.AccessTokenTTL)
|
||||||
|
err = s.auth.oauth2CreateSession(r.Context(), sessionToken, userID, &oauth2.Token{
|
||||||
|
AccessToken: sessionToken,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
}, expiresAt, "oauth2_client")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No refresh token per RFC 6749 §4.4.3, and no id_token — client_credentials has no
|
||||||
|
// end-user subject to represent in OIDC terms.
|
||||||
|
s.writeOAuthToken(w, r, sessionToken, "", client.ClientID, effectiveScopes, false)
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// RFC 7009 — Token revocation
|
// RFC 7009 — Token revocation
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
@@ -879,7 +1171,10 @@ func oauthSliceContains(slice []string, s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *OAuthServer) writeOAuthToken(w http.ResponseWriter, accessToken, refreshToken string, scopes []string) {
|
// writeOAuthToken writes the token response. When issueIDToken is true and the granted
|
||||||
|
// scopes include "openid", an RS256-signed id_token is included (OIDC); client_credentials
|
||||||
|
// responses always pass issueIDToken=false since that grant has no end-user subject.
|
||||||
|
func (s *OAuthServer) writeOAuthToken(w http.ResponseWriter, r *http.Request, accessToken, refreshToken, clientID string, scopes []string, issueIDToken bool) {
|
||||||
expiresIn := int64(s.cfg.AccessTokenTTL.Seconds())
|
expiresIn := int64(s.cfg.AccessTokenTTL.Seconds())
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"access_token": accessToken,
|
"access_token": accessToken,
|
||||||
@@ -892,12 +1187,106 @@ func (s *OAuthServer) writeOAuthToken(w http.ResponseWriter, accessToken, refres
|
|||||||
if len(scopes) > 0 {
|
if len(scopes) > 0 {
|
||||||
resp["scope"] = strings.Join(scopes, " ")
|
resp["scope"] = strings.Join(scopes, " ")
|
||||||
}
|
}
|
||||||
|
if issueIDToken && oauthSliceContains(scopes, "openid") {
|
||||||
|
if idToken, err := s.buildIDToken(r.Context(), accessToken, clientID, scopes); err == nil {
|
||||||
|
resp["id_token"] = idToken
|
||||||
|
}
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
w.Header().Set("Pragma", "no-cache")
|
w.Header().Set("Pragma", "no-cache")
|
||||||
json.NewEncoder(w).Encode(resp) //nolint:errcheck
|
json.NewEncoder(w).Encode(resp) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildIDToken issues an OIDC id_token for the just-issued access token by reusing the
|
||||||
|
// existing introspection pipeline to resolve the subject's claims.
|
||||||
|
func (s *OAuthServer) buildIDToken(ctx context.Context, accessToken, clientID string, scopes []string) (string, error) {
|
||||||
|
if s.signingKey == nil {
|
||||||
|
return "", fmt.Errorf("no signing key configured")
|
||||||
|
}
|
||||||
|
authToUse := s.auth
|
||||||
|
if authToUse == nil {
|
||||||
|
s.mu.RLock()
|
||||||
|
if len(s.providers) > 0 {
|
||||||
|
authToUse = s.providers[0].auth
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
}
|
||||||
|
if authToUse == nil {
|
||||||
|
return "", fmt.Errorf("no authenticator configured")
|
||||||
|
}
|
||||||
|
info, err := authToUse.OAuthIntrospectToken(ctx, accessToken)
|
||||||
|
if err != nil || !info.Active {
|
||||||
|
return "", fmt.Errorf("token not active")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"iss": s.cfg.Issuer,
|
||||||
|
"sub": info.Sub,
|
||||||
|
"aud": clientID,
|
||||||
|
"exp": now.Add(s.cfg.AccessTokenTTL).Unix(),
|
||||||
|
"iat": now.Unix(),
|
||||||
|
}
|
||||||
|
if oauthSliceContains(scopes, "profile") && info.Username != "" {
|
||||||
|
claims["preferred_username"] = info.Username
|
||||||
|
}
|
||||||
|
if oauthSliceContains(scopes, "email") && info.Email != "" {
|
||||||
|
claims["email"] = info.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
token.Header["kid"] = s.signingKeyID
|
||||||
|
return token.SignedString(s.signingKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateClient validates client_secret_basic (Authorization: Basic) or
|
||||||
|
// client_secret_post (client_id/client_secret form fields) credentials against a
|
||||||
|
// registered confidential client's stored secret hash.
|
||||||
|
func (s *OAuthServer) authenticateClient(r *http.Request) (*oauthClient, error) {
|
||||||
|
clientID, clientSecret, ok := r.BasicAuth()
|
||||||
|
if !ok {
|
||||||
|
clientID = r.FormValue("client_id")
|
||||||
|
clientSecret = r.FormValue("client_secret")
|
||||||
|
}
|
||||||
|
if clientID == "" || clientSecret == "" {
|
||||||
|
return nil, fmt.Errorf("client authentication required")
|
||||||
|
}
|
||||||
|
client, ok := s.lookupOrFetchClient(r.Context(), clientID)
|
||||||
|
if !ok || !client.isConfidential() {
|
||||||
|
return nil, fmt.Errorf("invalid client credentials")
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare([]byte(hashClientSecret(clientSecret)), []byte(client.ClientSecretHash)) != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid client credentials")
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashClientSecret(secret string) string {
|
||||||
|
sum := sha256.Sum256([]byte(secret))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// rsaKeyID derives a stable JWKS "kid" from an RSA public key's modulus.
|
||||||
|
func rsaKeyID(pub *rsa.PublicKey) string {
|
||||||
|
sum := sha256.Sum256(pub.N.Bytes())
|
||||||
|
return base64.RawURLEncoding.EncodeToString(sum[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
// bigEndianBytes encodes a small positive int (e.g. an RSA public exponent) as
|
||||||
|
// minimal big-endian bytes for JWK "e" encoding.
|
||||||
|
func bigEndianBytes(n int) []byte {
|
||||||
|
if n == 0 {
|
||||||
|
return []byte{0}
|
||||||
|
}
|
||||||
|
var b []byte
|
||||||
|
for n > 0 {
|
||||||
|
b = append([]byte{byte(n & 0xff)}, b...)
|
||||||
|
n >>= 8
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func writeOAuthError(w http.ResponseWriter, errCode, description string, status int) {
|
func writeOAuthError(w http.ResponseWriter, errCode, description string, status int) {
|
||||||
resp := map[string]string{"error": errCode}
|
resp := map[string]string{"error": errCode}
|
||||||
if description != "" {
|
if description != "" {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ type OAuthServerClient struct {
|
|||||||
ClientName string `json:"client_name,omitempty"`
|
ClientName string `json:"client_name,omitempty"`
|
||||||
GrantTypes []string `json:"grant_types"`
|
GrantTypes []string `json:"grant_types"`
|
||||||
AllowedScopes []string `json:"allowed_scopes,omitempty"`
|
AllowedScopes []string `json:"allowed_scopes,omitempty"`
|
||||||
|
ClientSecretHash string `json:"client_secret_hash,omitempty"`
|
||||||
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthCode is a short-lived authorization code.
|
// OAuthCode is a short-lived authorization code.
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ import (
|
|||||||
// Array columns (redirect_uris, grant_types, allowed_scopes, scopes) are
|
// Array columns (redirect_uris, grant_types, allowed_scopes, scopes) are
|
||||||
// JSON-encoded TEXT instead of native Postgres arrays.
|
// JSON-encoded TEXT instead of native Postgres arrays.
|
||||||
|
|
||||||
|
// nullIfEmpty converts an empty string to a SQL NULL so optional TEXT columns
|
||||||
|
// (e.g. client_secret_hash for public clients) stay unset rather than "".
|
||||||
|
func nullIfEmpty(s string) any {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
func (a *DatabaseAuthenticator) oauthRegisterClientDirect(ctx context.Context, client *OAuthServerClient) (*OAuthServerClient, error) {
|
func (a *DatabaseAuthenticator) oauthRegisterClientDirect(ctx context.Context, client *OAuthServerClient) (*OAuthServerClient, error) {
|
||||||
grantTypes := client.GrantTypes
|
grantTypes := client.GrantTypes
|
||||||
if len(grantTypes) == 0 {
|
if len(grantTypes) == 0 {
|
||||||
@@ -38,11 +47,16 @@ func (a *DatabaseAuthenticator) oauthRegisterClientDirect(ctx context.Context, c
|
|||||||
return nil, fmt.Errorf("failed to marshal allowed_scopes: %w", err)
|
return nil, fmt.Errorf("failed to marshal allowed_scopes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authMethod := client.TokenEndpointAuthMethod
|
||||||
|
if authMethod == "" {
|
||||||
|
authMethod = "none"
|
||||||
|
}
|
||||||
|
|
||||||
err = a.runDBOpWithReconnect(func(db *sql.DB) error {
|
err = a.runDBOpWithReconnect(func(db *sql.DB) error {
|
||||||
query := rewritePlaceholders(db, fmt.Sprintf(
|
query := rewritePlaceholders(db, fmt.Sprintf(
|
||||||
`INSERT INTO %s (client_id, redirect_uris, client_name, grant_types, allowed_scopes, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
`INSERT INTO %s (client_id, redirect_uris, client_name, grant_types, allowed_scopes, client_secret_hash, token_endpoint_auth_method, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
a.tableNames.OAuthClients))
|
a.tableNames.OAuthClients))
|
||||||
_, err := db.ExecContext(ctx, query, client.ClientID, string(redirectURIsJSON), client.ClientName, string(grantTypesJSON), string(allowedScopesJSON), true, time.Now())
|
_, err := db.ExecContext(ctx, query, client.ClientID, string(redirectURIsJSON), client.ClientName, string(grantTypesJSON), string(allowedScopesJSON), nullIfEmpty(client.ClientSecretHash), authMethod, true, time.Now())
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,18 +69,20 @@ func (a *DatabaseAuthenticator) oauthRegisterClientDirect(ctx context.Context, c
|
|||||||
ClientName: client.ClientName,
|
ClientName: client.ClientName,
|
||||||
GrantTypes: grantTypes,
|
GrantTypes: grantTypes,
|
||||||
AllowedScopes: allowedScopes,
|
AllowedScopes: allowedScopes,
|
||||||
|
ClientSecretHash: client.ClientSecretHash,
|
||||||
|
TokenEndpointAuthMethod: authMethod,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *DatabaseAuthenticator) oauthGetClientDirect(ctx context.Context, clientID string) (*OAuthServerClient, error) {
|
func (a *DatabaseAuthenticator) oauthGetClientDirect(ctx context.Context, clientID string) (*OAuthServerClient, error) {
|
||||||
var redirectURIsJSON, grantTypesJSON, allowedScopesJSON sql.NullString
|
var redirectURIsJSON, grantTypesJSON, allowedScopesJSON sql.NullString
|
||||||
var clientName sql.NullString
|
var clientName, clientSecretHash, authMethod sql.NullString
|
||||||
|
|
||||||
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
||||||
query := rewritePlaceholders(db, fmt.Sprintf(
|
query := rewritePlaceholders(db, fmt.Sprintf(
|
||||||
`SELECT redirect_uris, client_name, grant_types, allowed_scopes FROM %s WHERE client_id = ? AND is_active = ?`,
|
`SELECT redirect_uris, client_name, grant_types, allowed_scopes, client_secret_hash, token_endpoint_auth_method FROM %s WHERE client_id = ? AND is_active = ?`,
|
||||||
a.tableNames.OAuthClients))
|
a.tableNames.OAuthClients))
|
||||||
return db.QueryRowContext(ctx, query, clientID, true).Scan(&redirectURIsJSON, &clientName, &grantTypesJSON, &allowedScopesJSON)
|
return db.QueryRowContext(ctx, query, clientID, true).Scan(&redirectURIsJSON, &clientName, &grantTypesJSON, &allowedScopesJSON, &clientSecretHash, &authMethod)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -75,7 +91,12 @@ func (a *DatabaseAuthenticator) oauthGetClientDirect(ctx context.Context, client
|
|||||||
return nil, fmt.Errorf("failed to get client: %w", err)
|
return nil, fmt.Errorf("failed to get client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
result := &OAuthServerClient{ClientID: clientID, ClientName: clientName.String}
|
result := &OAuthServerClient{
|
||||||
|
ClientID: clientID,
|
||||||
|
ClientName: clientName.String,
|
||||||
|
ClientSecretHash: clientSecretHash.String,
|
||||||
|
TokenEndpointAuthMethod: authMethod.String,
|
||||||
|
}
|
||||||
if redirectURIsJSON.Valid {
|
if redirectURIsJSON.Valid {
|
||||||
_ = json.Unmarshal([]byte(redirectURIsJSON.String), &result.RedirectURIs)
|
_ = json.Unmarshal([]byte(redirectURIsJSON.String), &result.RedirectURIs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,483 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestOAuthServer(t *testing.T) (*OAuthServer, *DatabaseAuthenticator) {
|
||||||
|
t.Helper()
|
||||||
|
db := newDirectTestDB(t)
|
||||||
|
auth := NewDatabaseAuthenticatorWithOptions(db, DatabaseAuthenticatorOptions{QueryMode: ModeDirect})
|
||||||
|
srv := NewOAuthServer(OAuthServerConfig{Issuer: "https://auth.example.com", PersistCodes: true}, auth)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
return srv, auth
|
||||||
|
}
|
||||||
|
|
||||||
|
// s256Challenge computes the PKCE S256 code_challenge for a given verifier,
|
||||||
|
// matching validatePKCESHA256 in oauth_server.go.
|
||||||
|
func s256Challenge(verifier string) string {
|
||||||
|
h := sha256.Sum256([]byte(verifier))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func doJSON(t *testing.T, mux http.Handler, method, path string, body map[string]interface{}) (*httptest.ResponseRecorder, map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
var reqBody *strings.Reader
|
||||||
|
if body != nil {
|
||||||
|
b, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal body: %v", err)
|
||||||
|
}
|
||||||
|
reqBody = strings.NewReader(string(b))
|
||||||
|
} else {
|
||||||
|
reqBody = strings.NewReader("")
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(method, path, reqBody)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if rec.Body.Len() > 0 {
|
||||||
|
_ = json.Unmarshal(rec.Body.Bytes(), &parsed)
|
||||||
|
}
|
||||||
|
return rec, parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func doForm(t *testing.T, mux http.Handler, path string, form url.Values, basicUser, basicPass string) (*httptest.ResponseRecorder, map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
if basicUser != "" {
|
||||||
|
req.SetBasicAuth(basicUser, basicPass)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if rec.Body.Len() > 0 {
|
||||||
|
_ = json.Unmarshal(rec.Body.Bytes(), &parsed)
|
||||||
|
}
|
||||||
|
return rec, parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func doGet(mux http.Handler, path, bearer string) (*httptest.ResponseRecorder, map[string]interface{}) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
if bearer != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+bearer)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
mux.ServeHTTP(rec, req)
|
||||||
|
var parsed map[string]interface{}
|
||||||
|
if rec.Body.Len() > 0 {
|
||||||
|
_ = json.Unmarshal(rec.Body.Bytes(), &parsed)
|
||||||
|
}
|
||||||
|
return rec, parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_RegisterConfidentialClient_IssuesSecretOnce(t *testing.T) {
|
||||||
|
srv, _ := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
rec, resp := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
"grant_types": []string{"client_credentials"},
|
||||||
|
})
|
||||||
|
if rec.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("register status = %d, body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
secret, _ := resp["client_secret"].(string)
|
||||||
|
if secret == "" {
|
||||||
|
t.Fatal("expected client_secret to be present in registration response")
|
||||||
|
}
|
||||||
|
if _, ok := resp["client_secret_hash"]; ok {
|
||||||
|
t.Error("client_secret_hash must never be returned in the registration response")
|
||||||
|
}
|
||||||
|
if resp["token_endpoint_auth_method"] != "client_secret_basic" {
|
||||||
|
t.Errorf("token_endpoint_auth_method = %v, want client_secret_basic", resp["token_endpoint_auth_method"])
|
||||||
|
}
|
||||||
|
if resp["client_id"] == "" || resp["client_id"] == nil {
|
||||||
|
t.Fatal("expected non-empty client_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetching the client back (e.g. via a later authorize/token call) must never leak the hash.
|
||||||
|
clientID := resp["client_id"].(string)
|
||||||
|
fetched, ok := srv.lookupOrFetchClient(context.Background(), clientID)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected client to be found")
|
||||||
|
}
|
||||||
|
if fetched.ClientSecretHash == "" {
|
||||||
|
t.Error("expected ClientSecretHash to be stored internally")
|
||||||
|
}
|
||||||
|
if fetched.ClientSecretHash == secret {
|
||||||
|
t.Error("stored hash must not equal the plaintext secret")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public registration (no client_credentials) should stay a public client with no secret.
|
||||||
|
recPublic, respPublic := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
})
|
||||||
|
if recPublic.Code != http.StatusCreated {
|
||||||
|
t.Fatalf("public register status = %d", recPublic.Code)
|
||||||
|
}
|
||||||
|
if _, ok := respPublic["client_secret"]; ok {
|
||||||
|
t.Error("public client registration should not receive a client_secret")
|
||||||
|
}
|
||||||
|
if respPublic["token_endpoint_auth_method"] != "none" {
|
||||||
|
t.Errorf("public client token_endpoint_auth_method = %v, want none", respPublic["token_endpoint_auth_method"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_ClientCredentialsGrant(t *testing.T) {
|
||||||
|
srv, _ := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
_, reg := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
"grant_types": []string{"client_credentials"},
|
||||||
|
"allowed_scopes": []string{"read", "write"},
|
||||||
|
})
|
||||||
|
clientID := reg["client_id"].(string)
|
||||||
|
clientSecret := reg["client_secret"].(string)
|
||||||
|
|
||||||
|
t.Run("valid credentials", func(t *testing.T) {
|
||||||
|
rec, resp := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
"scope": {"read"},
|
||||||
|
}, clientID, clientSecret)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("token status = %d, body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if resp["access_token"] == "" || resp["access_token"] == nil {
|
||||||
|
t.Error("expected non-empty access_token")
|
||||||
|
}
|
||||||
|
if _, ok := resp["refresh_token"]; ok {
|
||||||
|
t.Error("client_credentials must not issue a refresh_token (RFC 6749 §4.4.3)")
|
||||||
|
}
|
||||||
|
if resp["scope"] != "read" {
|
||||||
|
t.Errorf("scope = %v, want read", resp["scope"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("scope not allowed for client", func(t *testing.T) {
|
||||||
|
rec, resp := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
"scope": {"admin"},
|
||||||
|
}, clientID, clientSecret)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want 400", rec.Code)
|
||||||
|
}
|
||||||
|
if resp["error"] != "invalid_scope" {
|
||||||
|
t.Errorf("error = %v, want invalid_scope", resp["error"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong secret", func(t *testing.T) {
|
||||||
|
rec, resp := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
}, clientID, "wrong-secret")
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status = %d, want 401", rec.Code)
|
||||||
|
}
|
||||||
|
if resp["error"] != "invalid_client" {
|
||||||
|
t.Errorf("error = %v, want invalid_client", resp["error"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("public client cannot use client_credentials", func(t *testing.T) {
|
||||||
|
_, pubReg := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
})
|
||||||
|
pubClientID := pubReg["client_id"].(string)
|
||||||
|
|
||||||
|
rec, _ := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"client_credentials"},
|
||||||
|
"client_id": {pubClientID},
|
||||||
|
}, "", "")
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status = %d, want 401 for public client attempting client_credentials", rec.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_ConfidentialClient_AuthCodeGrantRequiresSecret(t *testing.T) {
|
||||||
|
srv, auth := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
regResp, err := auth.Register(context.Background(), RegisterRequest{
|
||||||
|
Username: "nadia", Password: "p", Email: "nadia@example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Register() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, reg := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
"token_endpoint_auth_method": "client_secret_basic",
|
||||||
|
})
|
||||||
|
clientID := reg["client_id"].(string)
|
||||||
|
clientSecret := reg["client_secret"].(string)
|
||||||
|
|
||||||
|
verifier := "verifier-nadia-1234567890"
|
||||||
|
challenge := s256Challenge(verifier)
|
||||||
|
if err := auth.OAuthSaveCode(context.Background(), &OAuthCode{
|
||||||
|
Code: "code-no-auth",
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: "https://app.example.com/callback",
|
||||||
|
CodeChallenge: challenge,
|
||||||
|
SessionToken: regResp.Token,
|
||||||
|
Scopes: []string{"profile"},
|
||||||
|
ExpiresAt: futureTime(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("OAuthSaveCode() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No client credentials supplied -> must be rejected for a confidential client.
|
||||||
|
rec, resp := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"authorization_code"},
|
||||||
|
"code": {"code-no-auth"},
|
||||||
|
"redirect_uri": {"https://app.example.com/callback"},
|
||||||
|
"client_id": {clientID},
|
||||||
|
"code_verifier": {verifier},
|
||||||
|
}, "", "")
|
||||||
|
if rec.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status = %d, want 401 without client auth, body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if resp["error"] != "invalid_client" {
|
||||||
|
t.Errorf("error = %v, want invalid_client", resp["error"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same code, now with correct client credentials -> succeeds.
|
||||||
|
if err := auth.OAuthSaveCode(context.Background(), &OAuthCode{
|
||||||
|
Code: "code-with-auth",
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: "https://app.example.com/callback",
|
||||||
|
CodeChallenge: challenge,
|
||||||
|
SessionToken: regResp.Token,
|
||||||
|
Scopes: []string{"profile"},
|
||||||
|
ExpiresAt: futureTime(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("OAuthSaveCode() error = %v", err)
|
||||||
|
}
|
||||||
|
rec2, _ := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"authorization_code"},
|
||||||
|
"code": {"code-with-auth"},
|
||||||
|
"redirect_uri": {"https://app.example.com/callback"},
|
||||||
|
"client_id": {clientID},
|
||||||
|
"code_verifier": {verifier},
|
||||||
|
}, clientID, clientSecret)
|
||||||
|
if rec2.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200 with client auth, body=%s", rec2.Code, rec2.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_PublicClient_AuthCodeGrantUnaffected(t *testing.T) {
|
||||||
|
srv, auth := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
regResp, err := auth.Register(context.Background(), RegisterRequest{
|
||||||
|
Username: "oscar", Password: "p", Email: "oscar@example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Register() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, reg := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
})
|
||||||
|
clientID := reg["client_id"].(string)
|
||||||
|
if _, ok := reg["client_secret"]; ok {
|
||||||
|
t.Fatal("expected no client_secret for a default (public) registration")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := "verifier-oscar-1234567890"
|
||||||
|
challenge := s256Challenge(verifier)
|
||||||
|
if err := auth.OAuthSaveCode(context.Background(), &OAuthCode{
|
||||||
|
Code: "public-code",
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: "https://app.example.com/callback",
|
||||||
|
CodeChallenge: challenge,
|
||||||
|
SessionToken: regResp.Token,
|
||||||
|
Scopes: []string{"profile"},
|
||||||
|
ExpiresAt: futureTime(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("OAuthSaveCode() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No credentials needed for a public client — PKCE alone is sufficient, unchanged.
|
||||||
|
rec, _ := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"authorization_code"},
|
||||||
|
"code": {"public-code"},
|
||||||
|
"redirect_uri": {"https://app.example.com/callback"},
|
||||||
|
"client_id": {clientID},
|
||||||
|
"code_verifier": {verifier},
|
||||||
|
}, "", "")
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200 for public client without credentials, body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_ProtectedResourceMetadata(t *testing.T) {
|
||||||
|
srv, _ := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
rec, resp := doGet(mux, "/.well-known/oauth-protected-resource", "")
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d", rec.Code)
|
||||||
|
}
|
||||||
|
if resp["resource"] != "https://auth.example.com" {
|
||||||
|
t.Errorf("resource = %v", resp["resource"])
|
||||||
|
}
|
||||||
|
servers, ok := resp["authorization_servers"].([]interface{})
|
||||||
|
if !ok || len(servers) != 1 || servers[0] != "https://auth.example.com" {
|
||||||
|
t.Errorf("authorization_servers = %v", resp["authorization_servers"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_OIDCDiscoveryAndIDToken(t *testing.T) {
|
||||||
|
srv, auth := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
// Discovery document.
|
||||||
|
rec, disc := doGet(mux, "/.well-known/openid-configuration", "")
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("discovery status = %d", rec.Code)
|
||||||
|
}
|
||||||
|
if disc["jwks_uri"] != "https://auth.example.com/oauth/jwks.json" {
|
||||||
|
t.Errorf("jwks_uri = %v", disc["jwks_uri"])
|
||||||
|
}
|
||||||
|
grantTypes, _ := disc["grant_types_supported"].([]interface{})
|
||||||
|
found := false
|
||||||
|
for _, g := range grantTypes {
|
||||||
|
if g == "client_credentials" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("expected client_credentials in grant_types_supported, got %v", grantTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWKS.
|
||||||
|
rec, _ = doGet(mux, "/oauth/jwks.json", "")
|
||||||
|
var jwks struct {
|
||||||
|
Keys []struct {
|
||||||
|
Kid string `json:"kid"`
|
||||||
|
N string `json:"n"`
|
||||||
|
E string `json:"e"`
|
||||||
|
} `json:"keys"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(rec.Body.Bytes(), &jwks)
|
||||||
|
if len(jwks.Keys) != 1 {
|
||||||
|
t.Fatalf("expected 1 JWKS key, got %d", len(jwks.Keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-to-end authorization_code flow with scope=openid, verifying the id_token
|
||||||
|
// signature against the published JWKS key.
|
||||||
|
regResp, err := auth.Register(context.Background(), RegisterRequest{
|
||||||
|
Username: "olivia", Password: "p", Email: "olivia@example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Register() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, clientReg := doJSON(t, mux, http.MethodPost, "/oauth/register", map[string]interface{}{
|
||||||
|
"redirect_uris": []string{"https://app.example.com/callback"},
|
||||||
|
})
|
||||||
|
clientID := clientReg["client_id"].(string)
|
||||||
|
|
||||||
|
verifier := "test-code-verifier-1234567890"
|
||||||
|
challenge := s256Challenge(verifier)
|
||||||
|
if err := auth.OAuthSaveCode(context.Background(), &OAuthCode{
|
||||||
|
Code: "oidc-code",
|
||||||
|
ClientID: clientID,
|
||||||
|
RedirectURI: "https://app.example.com/callback",
|
||||||
|
CodeChallenge: challenge,
|
||||||
|
SessionToken: regResp.Token,
|
||||||
|
Scopes: []string{"openid", "profile", "email"},
|
||||||
|
ExpiresAt: futureTime(),
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("OAuthSaveCode() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokRec, tokenResp := doForm(t, mux, "/oauth/token", url.Values{
|
||||||
|
"grant_type": {"authorization_code"},
|
||||||
|
"code": {"oidc-code"},
|
||||||
|
"redirect_uri": {"https://app.example.com/callback"},
|
||||||
|
"client_id": {clientID},
|
||||||
|
"code_verifier": {verifier},
|
||||||
|
}, "", "")
|
||||||
|
if tokRec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("token exchange status = %d, body = %s", tokRec.Code, tokRec.Body.String())
|
||||||
|
}
|
||||||
|
idTokenStr, _ := tokenResp["id_token"].(string)
|
||||||
|
if idTokenStr == "" {
|
||||||
|
t.Fatal("expected id_token in response for scope containing openid")
|
||||||
|
}
|
||||||
|
|
||||||
|
nBytes, err := base64.RawURLEncoding.DecodeString(jwks.Keys[0].N)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode n: %v", err)
|
||||||
|
}
|
||||||
|
eBytes, err := base64.RawURLEncoding.DecodeString(jwks.Keys[0].E)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode e: %v", err)
|
||||||
|
}
|
||||||
|
pub := &rsa.PublicKey{N: new(big.Int).SetBytes(nBytes), E: int(new(big.Int).SetBytes(eBytes).Int64())}
|
||||||
|
|
||||||
|
parsed, err := jwt.Parse(idTokenStr, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return pub, nil
|
||||||
|
}, jwt.WithValidMethods([]string{"RS256"}))
|
||||||
|
if err != nil || !parsed.Valid {
|
||||||
|
t.Fatalf("id_token did not verify against JWKS key: %v", err)
|
||||||
|
}
|
||||||
|
claims := parsed.Claims.(jwt.MapClaims)
|
||||||
|
if claims["iss"] != "https://auth.example.com" {
|
||||||
|
t.Errorf("iss claim = %v", claims["iss"])
|
||||||
|
}
|
||||||
|
if claims["aud"] != clientID {
|
||||||
|
t.Errorf("aud claim = %v, want %v", claims["aud"], clientID)
|
||||||
|
}
|
||||||
|
if claims["preferred_username"] != "olivia" {
|
||||||
|
t.Errorf("preferred_username claim = %v", claims["preferred_username"])
|
||||||
|
}
|
||||||
|
if claims["email"] != "olivia@example.com" {
|
||||||
|
t.Errorf("email claim = %v", claims["email"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOAuthServer_Userinfo(t *testing.T) {
|
||||||
|
srv, auth := newTestOAuthServer(t)
|
||||||
|
mux := srv.HTTPHandler()
|
||||||
|
|
||||||
|
regResp, err := auth.Register(context.Background(), RegisterRequest{
|
||||||
|
Username: "pete", Password: "p", Email: "pete@example.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Register() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rec, info := doGet(mux, "/oauth/userinfo", regResp.Token)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("userinfo status = %d, body = %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if info["preferred_username"] != "pete" {
|
||||||
|
t.Errorf("preferred_username = %v", info["preferred_username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
rec2, _ := doGet(mux, "/oauth/userinfo", "not-a-real-token")
|
||||||
|
if rec2.Code != http.StatusUnauthorized {
|
||||||
|
t.Fatalf("status = %d, want 401 for invalid token", rec2.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -221,7 +221,7 @@ func (h *Handler) handleRequest(conn *Connection, msg *Message) {
|
|||||||
// handleRead processes a read operation
|
// handleRead processes a read operation
|
||||||
func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
|
||||||
logger.Error("[WebSocketSpec] BeforeRead hook failed: %v", err)
|
logger.Error("[WebSocketSpec] BeforeRead hook failed: %v", err)
|
||||||
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
||||||
_ = conn.SendJSON(errResp)
|
_ = conn.SendJSON(errResp)
|
||||||
@@ -273,7 +273,7 @@ func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContex
|
|||||||
// handleCreate processes a create operation
|
// handleCreate processes a create operation
|
||||||
func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
|
||||||
logger.Error("[WebSocketSpec] BeforeCreate hook failed: %v", err)
|
logger.Error("[WebSocketSpec] BeforeCreate hook failed: %v", err)
|
||||||
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
||||||
_ = conn.SendJSON(errResp)
|
_ = conn.SendJSON(errResp)
|
||||||
@@ -311,7 +311,7 @@ func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookCont
|
|||||||
// handleUpdate processes an update operation
|
// handleUpdate processes an update operation
|
||||||
func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
|
||||||
logger.Error("[WebSocketSpec] BeforeUpdate hook failed: %v", err)
|
logger.Error("[WebSocketSpec] BeforeUpdate hook failed: %v", err)
|
||||||
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
||||||
_ = conn.SendJSON(errResp)
|
_ = conn.SendJSON(errResp)
|
||||||
@@ -349,7 +349,7 @@ func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookCont
|
|||||||
// handleDelete processes a delete operation
|
// handleDelete processes a delete operation
|
||||||
func (h *Handler) handleDelete(conn *Connection, msg *Message, hookCtx *HookContext) {
|
func (h *Handler) handleDelete(conn *Connection, msg *Message, hookCtx *HookContext) {
|
||||||
// Execute before hook
|
// Execute before hook
|
||||||
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil {
|
if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
|
||||||
logger.Error("[WebSocketSpec] BeforeDelete hook failed: %v", err)
|
logger.Error("[WebSocketSpec] BeforeDelete hook failed: %v", err)
|
||||||
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
|
||||||
_ = conn.SendJSON(errResp)
|
_ = conn.SendJSON(errResp)
|
||||||
@@ -574,6 +574,18 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
|
||||||
|
if hookCtx.Metadata == nil {
|
||||||
|
hookCtx.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
hookCtx.Metadata["query"] = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
return nil, fmt.Errorf("BeforeScan hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
|
||||||
|
query = modifiedQuery
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
if err := query.ScanModel(hookCtx.Context); err != nil {
|
if err := query.ScanModel(hookCtx.Context); err != nil {
|
||||||
return nil, fmt.Errorf("failed to read record: %w", err)
|
return nil, fmt.Errorf("failed to read record: %w", err)
|
||||||
@@ -624,6 +636,18 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
|
||||||
|
if hookCtx.Metadata == nil {
|
||||||
|
hookCtx.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
hookCtx.Metadata["query"] = query
|
||||||
|
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("BeforeScan hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
|
||||||
|
query = modifiedQuery
|
||||||
|
}
|
||||||
|
|
||||||
// Execute query
|
// Execute query
|
||||||
if err := query.ScanModel(hookCtx.Context); err != nil {
|
if err := query.ScanModel(hookCtx.Context); err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to read records: %w", err)
|
return nil, nil, fmt.Errorf("failed to read records: %w", err)
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ const (
|
|||||||
// AfterDelete is called after a delete operation
|
// AfterDelete is called after a delete operation
|
||||||
AfterDelete HookType = "after_delete"
|
AfterDelete HookType = "after_delete"
|
||||||
|
|
||||||
|
// BeforeScan is called right before a read query is executed against the database,
|
||||||
|
// after all filters/sort/pagination have been applied. Use this for row-level
|
||||||
|
// security that needs to modify the query (stored in HookContext.Metadata["query"]).
|
||||||
|
BeforeScan HookType = "before_scan"
|
||||||
|
|
||||||
// BeforeSubscribe is called before creating a subscription
|
// BeforeSubscribe is called before creating a subscription
|
||||||
BeforeSubscribe HookType = "before_subscribe"
|
BeforeSubscribe HookType = "before_subscribe"
|
||||||
// AfterSubscribe is called after creating a subscription
|
// AfterSubscribe is called after creating a subscription
|
||||||
@@ -54,6 +59,11 @@ const (
|
|||||||
BeforeDisconnect HookType = "before_disconnect"
|
BeforeDisconnect HookType = "before_disconnect"
|
||||||
// AfterDisconnect is called after a connection is closed
|
// AfterDisconnect is called after a connection is closed
|
||||||
AfterDisconnect HookType = "after_disconnect"
|
AfterDisconnect HookType = "after_disconnect"
|
||||||
|
|
||||||
|
// BeforeOp fires immediately before every SQL operation (read, create, update, delete).
|
||||||
|
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
|
||||||
|
// individual SQL-operation hook point, so it runs once per statement executed.
|
||||||
|
BeforeOp HookType = "before_op"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookContext contains context information for hook execution
|
// HookContext contains context information for hook execution
|
||||||
@@ -197,6 +207,16 @@ func (hr *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
|
||||||
|
// (BeforeRead, BeforeCreate, BeforeUpdate, or BeforeDelete). BeforeOp always runs first
|
||||||
|
// so it can observe/veto every SQL operation regardless of type.
|
||||||
|
func (hr *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
|
||||||
|
if err := hr.Execute(BeforeOp, ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return hr.Execute(hookType, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// HasHooks checks if any hooks are registered for a hook type
|
// HasHooks checks if any hooks are registered for a hook type
|
||||||
func (hr *HookRegistry) HasHooks(hookType HookType) bool {
|
func (hr *HookRegistry) HasHooks(hookType HookType) bool {
|
||||||
hooks, exists := hr.hooks[hookType]
|
hooks, exists := hr.hooks[hookType]
|
||||||
|
|||||||
@@ -27,25 +27,31 @@ func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList
|
|||||||
return security.LoadSecurityRules(secCtx, securityList)
|
return security.LoadSecurityRules(secCtx, securityList)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 2: AfterRead - Apply column-level security (masking)
|
// Hook 2: BeforeScan - Apply row-level security filters
|
||||||
|
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
|
||||||
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
return security.ApplyRowSecurity(secCtx, securityList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 3: AfterRead - Apply column-level security (masking)
|
||||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.ApplyColumnSecurity(secCtx, securityList)
|
return security.ApplyColumnSecurity(secCtx, securityList)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 3 (Optional): Audit logging
|
// Hook 4 (Optional): Audit logging
|
||||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.LogDataAccess(secCtx)
|
return security.LogDataAccess(secCtx)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 4: BeforeUpdate - enforce CanUpdate rule from context/registry
|
// Hook 5: BeforeUpdate - enforce CanUpdate rule from context/registry
|
||||||
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.CheckModelUpdateAllowed(secCtx)
|
return security.CheckModelUpdateAllowed(secCtx)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hook 5: BeforeDelete - enforce CanDelete rule from context/registry
|
// Hook 6: BeforeDelete - enforce CanDelete rule from context/registry
|
||||||
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
return security.CheckModelDeleteAllowed(secCtx)
|
return security.CheckModelDeleteAllowed(secCtx)
|
||||||
|
|||||||
Reference in New Issue
Block a user