diff --git a/.env.dev.enc b/.env.dev.enc index 9162ddb..d5038b4 100644 --- a/.env.dev.enc +++ b/.env.dev.enc @@ -1,34 +1,37 @@ -#ENC[AES256_GCM,data:lmG0AqBdAGWZ,iv:MwavSYQHmVIdo7D/1H2P6IefCglKP/zO8H2Xid/Q3aA=,tag:erWfGwBLgtkurOL1QyEfqA==,type:comment] -DATABASE_URL=ENC[AES256_GCM,data:qaQbPyVtnXzaLBvXgbJmz0u8lbMucfDeWndAdohHyv0Obgaw41YP1MHHYhGu9CVwaD1gtnU=,iv:bMnTxRSRxyrk1+w4J4LlxxHTBB3kHegGDZMpFud9xJk=,tag:wf5APMrohuj9mjB3bxRLuA==,type:str] -SESSION_SECRET=ENC[AES256_GCM,data:Ndjpa8UbWLB4mERTyYRzdnHOYeA=,iv:hL3b1nEA7oRzCKwk9eysL7wJpdf2/IatYNQOUOOEH4g=,tag:27AtxAFW47IoEyeu4RTiLQ==,type:str] -PORT=ENC[AES256_GCM,data:cwe3nQ==,iv:P2g2H8uZayCkxK72zt6iEXz6gYuBjMzGSLAT9bjmDF8=,tag:bHCQjJR3+q1b9UsBeFp11g==,type:str] -APP_REPLICAS=ENC[AES256_GCM,data:kw==,iv:xziOg6i4k+7EW1/9qRLqa+81MHI3C8qpf4SBX2hTtkI=,tag:+ZaHEXP3SuQwz7RbmEJlEw==,type:str] -#ENC[AES256_GCM,data:0wTn,iv:47NbN2UqfY6m1bc236726HMblGjeeG/Zhq6OusazugY=,tag:8eVx9TY7QKVVtTfZ6vZr0A==,type:comment] -S3_BUCKET=ENC[AES256_GCM,data:vYPB9/hnBdidqjZSTSji,iv:h01LvE7PgzdlGbPDlO4hvEXOrHyUIwa0V5HLrt65shc=,tag:oeSdbohtQ3MM7hKSJGmnhg==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:NVE8DTSt7kOhNAN1jDb2OnIyQO1CUUVYJVd9DOOEOGB+8+thHPft6qLTsKUTLAMM6Hhwe/fz3KvKoEZPL1Jp5GY=,iv:zGhNHAuVT+j5FlMbOLvn1MFDhkItiF8ULmAsTYDaOGo=,tag:4KY5RC60LvE+O7e+Z2asEw==,type:str] -S3_ACCESS_KEY=ENC[AES256_GCM,data:YCQiNgmHl5Y7iOufuXH/h0MMcV9FD5s17dLNKZOVfKw=,iv:6r/l6bFkvPu0pjfv1hVzDmAUXHYKCcikyNsOLKst1Rw=,tag:xf39HHqjnCSdmGCYvRlsVg==,type:str] -S3_SECRET_KEY=ENC[AES256_GCM,data:m/oU91hR/3oMOSCq7JfXvMj1F6B4bXORol8s8NCjzeT0INjUxUopiOrjWyPczQ6Ob+UNUQqYMG9UakmppK8aJg==,iv:+g3Zp1OsklYlR0LTV/M7PONRxtfy1PNvv9NFw3c9mxg=,tag:OiZcJW9JHboPleMAaW4ZHQ==,type:str] -ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:0xHZcdQ=,iv:YTsBYn4WFyFOp5jI5KvEt++jMttMbAKG3wf4QClGq3c=,tag:qKNachtilRJuhNOMRf8D7Q==,type:str] -CF_ACCOUNT_ID=ENC[AES256_GCM,data:NJtRMOUyRcuch+gO/uDL/mm2lZfzmv2BSsrUa+/2AAo=,iv:gXK7qb5N8X/nLHTruVJA0S3SIOwk0yBS3FTmoN5BrCE=,tag:wboPv1RKrwIokNKrvmvD4w==,type:str] -CF_API_TOKEN=ENC[AES256_GCM,data:ftZnNMauVci3t2kigVJ8YzZ/Iht4tanQCEMYJM+MX/iqHli39T9TeyNomjemt4ckCNy7CEk=,iv:AoXwvL2jsF13CjKjJEIb0Gsxbfdy9pPQFc7nXkGA/+U=,tag:D2nTBOaW+9nl3Pikw96xXw==,type:str] -DEPLOY_HOST=ENC[AES256_GCM,data:hmfvyFfEthMPpl7glA==,iv:dzQE2927TQW59oxEszmOc+nprDmj9MVhCywBxzuE7yU=,tag:KKaUYhYwVfgQhEoyBy8I0w==,type:str] -#ENC[AES256_GCM,data:5EmRUT5TP04DiujwA4ybhpxmpwg=,iv:KL1SBHuUitjog44NMZM0NDljMebwx8sYPUMFIWCPp80=,tag:0uDxOgyB1yDf2y981u7Upw==,type:comment] -KF_AUTH_URL=ENC[AES256_GCM,data:E34WyjcN/ynL8nqSBE4cGrWkFgHpVpq+6quoX5IS4mKxCYPNuQ==,iv:TRqhyuD69okX6T3Zfh+DRG87X6a06S7u2+Pf4nUEwWM=,tag:SpOJTCdcjg705lDzft16EA==,type:str] -KF_AUTH_CLIENT_ID=ENC[AES256_GCM,data:iztGhF7KOBQGGiM=,iv:azzug4Y7T90ICdRwITtSFpPoX3g3fpMr5V7s1c3wrno=,tag:QIGjCq5g49IYoJhweqjCYQ==,type:str] -KF_AUTH_CLIENT_SECRET=ENC[AES256_GCM,data:nP7Nd/LsicIDeEAN6xbWGQ3jYECinkca+PMve8qUHISTKECNEGvs/BBhkSPTTbT/gb/wqZuqLMa7I9RVc7rWWQ==,iv:Ngb+Bg1bPVuHxtHY0hpF1BgKU14ECE7SG+Vl+wCB0ms=,tag:LIc8nj+gxQdlyggLMHT4/g==,type:str] -KF_INTERNAL_API_KEY=ENC[AES256_GCM,data:D4P6kATJx5yiKzvN6syctZJxDTpyw5A7k+61LkJG3FS7frqnIYop7EKc5tBoF/CdI/4tkwXYBchDxXsK89k59g==,iv:p82O44FMgnxV2okordv8Jm+JcVLB6sCERbzXRY9yJaQ=,tag:f28C3tInSKpxhrxlitWq1g==,type:str] -APP_URL=ENC[AES256_GCM,data:IPJstMk95GuHJunALmEIDZDa9QRgcsid,iv:llGmGCn+mXDPj1zB8H05z8pLGTuyecjoYeL0uyQyBfk=,tag:sM7e9S73CGC0KOQHOAFjHw==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPa0tzZ1k4eGR4NzhXWmU1\nQll0SXZUWUFsRGVNNnFUc08rZ3ozSXRDNlhBCkVKeEZYQlRzWnlUTVZZSWUyTFZn\naHV6eGhlVjR4TElUTzVOVVByYmxPZ3cKLS0tIDh1QVUxY2ZGY05pZklwUEFOVHU1\neUxzTmhnTEl5ZHM5WDhySnBTRThwYUUKtb4XQILCcsJWRDmt2ckSeey1jonQbmx7\nxq3wc07dNjXLTQOKzbqRPCRRl3ivxRSEQhP3GRPXISh9V+Pl9aUj3g==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:SkXTEkxEVNDC,iv:20MGbTzjrXXEfgrg07TlWcd65OBvSXra6LES+JszsRA=,tag:TcDi0/3N4yfU6+wvW+IPEg==,type:comment] +DATABASE_URL=ENC[AES256_GCM,data:2mlttdfAAng7kvNGJ9wiJYTHzfioSe2famC722SQOCdoNiL3+EfXVBiht+0371TuIDdoUvo=,iv:RtarTtROsZzPHNgXRmlTdAwr7I3V6XWQSnTHHj19tLE=,tag:eJrY7eohdNGHlha15J9lKg==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:D5+kHHe3QSmU/q0dw1qH0Uo3NqE=,iv:AxZbEKwAH5kVo0usT5na/0/AnENsrpVb9EuzeGExtpI=,tag:yl+D5z9e9KROagNL/lCSbA==,type:str] +PORT=ENC[AES256_GCM,data:yV9fcA==,iv:3YO4mLxBfzrTaBLXEaTVNK+o0Wk+FgRQGNYL+nEv5Io=,tag:r/PFa3C+l6lMGstSNXHvOA==,type:str] +APP_REPLICAS=ENC[AES256_GCM,data:9Q==,iv:xlV2bT7re8mXxkFl89KPVFb5TlGGrjBPf5kP+3IjC4o=,tag:eRnU05fOAeVNLlR2dDDqsA==,type:str] +#ENC[AES256_GCM,data:suOT,iv:QnvGa3MHGQSvGPKv9+uc8UnecaUn6C5vO6FTv2/AUaM=,tag:DI5owLtwNV7rjoB0EGgJgg==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:gZJT5O5p4bZiDwF9aqgj,iv:am1qKwxcVWs1/ghwCZCfcdIsqF/Fesf9stLVi0jB/3Q=,tag:3We68PqApD2kgMWp126N0g==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:6Izx/xafSWgedBJRKlpTySv8waJeCEtJ9eA4REgxHYifDLOY0IABHYmzxv7t0jT4lW5hlpy4NfboZ4d/JIqN8pc=,iv:UWoyuLt++ldO8HneIn4ztwtS2zo0wdn7umYbBihMhfw=,tag:x/LEnUgi/wF/RKPT9CzSrw==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:XTVGaelE08hdjcdTD9rYr4bge5OVU1Y2GWfCbTp9sY8=,iv:z+08ILWfAx6AXKfKk5qj0klFco0cnwn60svaIhvUVOo=,tag:AQHU9DizJ4pd2PF2GeJLrA==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:hobkMoScovS/9rAFn3/arUmjAJmKceO8MEqh6M4QoQ19a34PvQHPcAek5W3AuF1wcnsRDL581Nsrnr5QkpJSaQ==,iv:B8IjNrh0uDT7MU5zvBXKhV0vhGLmxTvP1k9GQYSogXU=,tag:qeEVvsb5B1qYm/330NiG3Q==,type:str] +ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:Y3HQHnA=,iv:7dk6VP4SAMl6+4H9QoJ5H8+8An1p9TRMA+qaGECQgS0=,tag:nBFQTPgOqePcPHJ7yWYjIw==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:ampmc7WgrIqN52sKfDbkPpnzziFNEdNbKo8kAH5Vvzo=,iv:yBnG/2aUPNtTMc9q67rlQcZ1XwW88URk1QO87evGbjE=,tag:0KieLLQJ0k59Gemua1F4gw==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:YEmE20j9XdfaZA07FYvWu7RBlkOIUS5AM1Ac//ZglMXk1xMbpTo5suV66byDHX9oc9LDDkY=,iv:fi4FamLKcnZbyALGq6xKmytTMhZRD7dLnV5vkVOcZso=,tag:AXsipKabcHBYyNSb1qA4xg==,type:str] +DEPLOY_HOST=ENC[AES256_GCM,data:H+kIyRVkmxXFEROceg==,iv:+6MTOkolYuRUM1kDfaxnAK681tmAmu2IP+Qk2V2BCmk=,tag:Y5jQBIFw1QOV4CMtMksFUQ==,type:str] +#ENC[AES256_GCM,data:D81HIiY6HaN5TdubwexjVFrfFgs=,iv:eV4CTjlponnfAN6bSCosCUJ2JTtpybDzoDjflyRkjPY=,tag:agerV1G4l7CAHO8KlDwAbA==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:11yTXIz4rEMkaEvjw1jjpd0PKylG7sIZcDzzQrRSFykGfeyMOA==,iv:MzUn9rBkRcXphqnybWbykYtYFW0t9W3VAYHUsDjQac8=,tag:pHrJrS4Mcd4oZiG9S57ADA==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:TT/TmBpVv38HN74=,iv:dn3Divc7SQ3/4/3N4N+pjaWI1hstyyMf5DihL3Nnjl4=,tag:1U7wPDmcf3kvGGNtKWduCw==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:JZjZBjUVqhgVCQFG9w6ZJUC7jHVW9vADF5D7hM3wN9h14Ix4KfviJWOu3AU5ANMIG0e2b5SQiB+IWU6HHtrP1A==,iv:rh5ytL3CnUTFlmZXD+mkxYSVUQP8YvEfWJhahJLNe9k=,tag:fQG2BE85XoMMZqtn+NItKw==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:G1oyzUx7mmv9COkDUBOlhDUJsS5yefnHX//M2C3TfiJUSkK3MsdfPfcJk/kasgVfwOr2B8vtWGUV1SKJEPACGg==,iv:2i/ChJK96Yw7yIzVdLt9hps87L7JyJVgZ+nMHfHTAt4=,tag:AlHror7WRIQSXzLoqbg8hQ==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:qaM+JUm38v6EJ9z8OnNNFJ3lpRxyr2TYBp16fnJ3opBkheFmUtLx2Q==,iv:CBSlYhECmeQGM4SgwqpiCbdMhs/6M29ibi7f3zURme8=,tag:3B3Jd50Ssghoc9+fYx3E7w==,type:str] +APP_URL=ENC[AES256_GCM,data:0Ee4N1DtAO6zd/iWLhFPjNSTsUPB8XBG,iv:7zkzsfoXzC1hg3X/zA0LuXd+dc1lifKPYy6m4wZ94vU=,tag:wUvE9eVnvCgfXY7OfKSVqA==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0NzduTnlsVnowK3kzV0FF\nN3plM2tGc0dQN1lRK2M2clRmME9oYWhrWUhnCkNTOUNvR2RGdFNkRzBvUVYwNWtr\nREJXOXVOdWUxQytSSy9RZ0RweGJuV3MKLS0tIElvWHVnRFp5N0kwWmp5WjRSNVZP\nY2c1WWV2d1Y5WkwyYS9PSFpWajl6dDQKtm7+/VTLWjgcyU03ATz5nthkf7FdW+gv\nDYg+0GFoKJ329Vdam1n5B/km9GXLFLFcdshb/7kzbM4pHz7YMRre6w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhWkUvRnQ1VFV0eGc1NTBD\nZlh3NGE0eU92S0tkQjdUenBCNDFBYW9Rc0hBCi9rRkxLaDhkNzZycktaT1NKaEFk\nUzFkblU1RkUvQVNxcmQyQXVzWjJFY2sKLS0tIDhncEowTDV3QzFaQ1RyM2RMdGNG\nb1FKKzhwRXpkOW9kREc2OEh4NmFQS28KQq7UW46I8aivy7v54ssfMlzss3uHxv2V\nsuPkfJhuWynpGZC6SOh++M7jqskLilR+mi67DjFNGhth+ehu09A/bQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmVzRzZnVqdEx1YThmYndD\nRWpwTHh2d094MVdlOVZlS2FWR1FhVXZURmhvCityR1oyL3Z0VkZjTlBrcmY1SDI2\nR1dpZXJFOC9ObXh2ZnFzV1NCeWdjK28KLS0tIGsrM3o0ODkycXdpVHo0bVFFdnZ1\nM0l5RVVXemRGN2k3bisrM2ZCaTI5cVUKat5SVPmUJh0TFMzwukfZURZHEQgYla11\nJn5KEyc0XRtWV4KAq13EZtnxy2ZX1eEneAn63tM0h3qtpKi2ioua1A==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzQ3paZ0ZNaFg2NmE5c3B1\nY0lMdTc5ZTZ5SVBPZHo3M21pbFNhQ2dFdkJNCjU5WUxtMlM4VGRZVDcrY1htQ3VB\nVDZ6aGRPWWpnMWZITjRnVEFXdmIzSU0KLS0tIHA4VlljSEp2UzV4TGs4bVg0bjFD\naS9TUTQ1NE9Ga0pMR3Q4ZUNiVWIrQWcKd+zLbd1tHj1dVq5iY1B9cVmCwXGwdaQU\np5Wyyz2oygptyUheShfTim68mTDYm2k0+Ip6l9pIrtCTP9gGtM9Meg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPN01vMWJGdTZaRjN1VytG\ndkx3bUhHVlZnZHBLMHIwb0RXSWRXVlYrd0FBCmZqbjg3Y2lKUXI0T0NXbGlDSlZU\nS3lHVDBTbnJtSCtpK0F0dXc1WUVNdzAKLS0tIDVaU2YvT25qSDArWTJBSlN4R0xN\nbnlIWUlBaE8ySUlYSlhsYjNKZ3huekUK+XvJxNKUQhb5OdbBwtmNP5xACJDtiz/V\nhlbt3tAfImxt19heQIgTIe1ahJmF5eSua+vhDhO36SyOWU+bioTizA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBscnFReUxTYklKZWZxdWc4\nK1M0WHA0bjhwMGhzN1dGYTJhaFdyVW5ob0JrCkxGTmd0UVdmNmdNa0ZBR0VKRWMv\nZEdXYzF2RXdSZFVwREthR244SGVVcEkKLS0tIGZrSW5DQnBGUG9Zb3ZPUS85SXVY\nODdPeDFXWEtDQ1RTTHZUUlZKZmxoOUEK02DjreAQVgqpgrE29yyJ72D+OmlBXC8C\nwAZEZxZBbEL0wo145N2Tnm22tHkQ7B6P2C46fymxUS4l49qF6DvLHw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqUkI1K255NTh2bjN0VW9i\ncEJLNEZTdnIzcWJBc0dNYVd6cWY2aFgycFFBCnJxMXM5THlaaDVFVmlzL0tZTjBF\nR2hnRzVuYUVNU3UrRGdNVU1oQzZlVGsKLS0tIFovMWI0c1BxOFJ0bFNIY0lHOXFM\nTDU4VGpYSzNaa0VlTU1taDdmSWJISncKX8j7oH4Tss4yq+mHi9gQET911tT5k38d\nHYzgqADuz+MY20bBpDggaC7Gl/Qksj9LpOBWC+QUf/bmHwcWNZC9IQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVWTRRNStCUy85T0J3TFlZ\nTEZIUEZVSElreTFLVUVsMVY1aU5MRlFhSVZ3CjFBUnY1TWtNbHdZM0VvR3Znalg4\naWVwdnhYOTkxR3p4Vis1K2xyeGI1UDgKLS0tIDh4bytib2JLU1pvUTU4NUJFc1U4\nR2JjS3FYNUIrS01jTmxEbE1kNlkzajQKEfSkMPUMCDMoZD/exYQDJ0OrWdfHW5K+\nXgII97m2GwW+oOrajdXhZCxgWsOZ0QnS1WP8JE4NN9vp6sxthoC72g==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBteU1aTERRK21hN2c0bUhi\nT0JXTGllYlV2Uks0RytyWUFsTExKMjkvM2lzCmF0b2FUT0RYNmxoUzBORzlVSDBx\ndkJPQmQyVzJWOTNVZDNBVHNleUtVdkUKLS0tIHNJV1RQZEpmUlliUjI1QUNBMDRw\nVkFlTnNacFc1RmsyaU1BKzVGTjVpV2MKL9IL9BgmS0Hl6bx/S+ACHais3qj2zpL9\nP43XHpbK/SbCBH/w0CoH+cj5uO4ZTk6hIZ7m0N9dLOCp7sfldX6LrA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -sops_lastmodified=2026-05-17T03:28:30Z -sops_mac=ENC[AES256_GCM,data:qhUZeGuCDydHnrSDM5AQUTct8ss+O3pKFWphuIh88YCfRMSFuZjjVDbql5SqD5etXyGSxG4+pxVUFCvyfF0DKw8a73XdnYsPM05zrnFuX1p3UOikVtajKWYCHBu/Yjx6VOOS29HP9YCCJJX3bKfpTcNpkh30YWm8IH4/zUz8yb0=,iv:m48OnT821Uetn/GSoGeteqMn2awWLU1QgdDca8SV+bE=,tag:F9NIItju1QfEEXvZElgZgA==,type:str] +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTOTRHQmlCV1FtWXVDQU9M\neVlwVGNqSlFxNzY4NUpEU0lrbXJsRTVoK2hZCkZucDJ1bVZwdTBHcHdrazVmZGNL\nQzNFQURMajlNcVZ1aFgwTDc2UE9tVDgKLS0tIEtESjN6TjdJaHcvVjNYNWp4SCtX\nZEJiTmxONXFIQzl1Q2RXVnF6OTU2NmsKES3lEZwqRSiS0tuHp70Ula7uohnwBqnc\nDYbCQZlHx7egkG780RLaVZAThO2Rr3osQqTnVoVz5CwvcUtGT2Etaw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_lastmodified=2026-05-20T17:21:40Z +sops_mac=ENC[AES256_GCM,data:zEs7C2tA8cLsullU6zSNeZzPYM/Qt38ATTgxcjRcDQ5A5NQzEaTsBrlIyn6sXvzqH1l+QDv6yMxXzxRVhZo1/zuDNgAPeyScbed1x8ThNO6yHuU+u+oduKgnwrPZGS7whEHpSOk355Bhr7POylFTGZl6SK2E7fNhzoX5T77pY5Q=,iv:PV7GU3ZQDxSHRaCSOewfNe10kEbPSTg5SyLGG4TiSWI=,tag:j2DSZia+O/FUCyCGdnCSnA==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.env.local.enc b/.env.local.enc index e2be693..44f6b68 100644 --- a/.env.local.enc +++ b/.env.local.enc @@ -1,35 +1,38 @@ -#ENC[AES256_GCM,data:EwG7RRV6tRRp,iv:szSa5gysyS7lr5/4rnulyYhX33/6v6kWNoREGFrj2JM=,tag:k9obKCeUYSEzuUnnAapqEg==,type:comment] -NODE_ENV=ENC[AES256_GCM,data:xfSbj3OvQSlkuhI=,iv:U7OzOOp3BlZGITKK7ixzN2StVi5yGwobli26ZqRWRyY=,tag:kx+KDazKRB/8T2XdHjN3kw==,type:str] -PORT=ENC[AES256_GCM,data:igm7kQ==,iv:I1WRKuavFad/4voErHMyT7HfJT2OeCjktH1zv7CvOZ8=,tag:3DAa/hCbrp2ItT976w8eRg==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:Zyccn7eQMZu6N5o4/VqB0Gafsip1V8ZPWjR4PytIIevmvMef0Br52FkhsVPXj9qD2g/Kuo0=,iv:eWo/ggSxXL3nRYrNv9bPPnu922H9F4JN6MUYt+Xp+PA=,tag:fpy8wx+0dPnnlaBfzSGQFw==,type:str] -SESSION_SECRET=ENC[AES256_GCM,data:+COS9fGKuGEMyHQBXWJMwlBVfnY=,iv:pv8tJfJhZrAgLPO8Txvf5gEUZ2m9eioxVhj6KziIopg=,tag:D8yOO+xCKUuax4B5EuqGVQ==,type:str] -#ENC[AES256_GCM,data:iqmMy/fCrzgR0yK3OpWkHWaBwg==,iv:uqznSmxMAyseEyqy7yQgrY0cT2AIG7tXbyIB7DX2/XE=,tag:DxyQyxqvHe0jTBshYfWfPw==,type:comment] -S3_BUCKET=ENC[AES256_GCM,data:qQCv9WqEOCpxZfRJvejV,iv:PX8OTqKO34d/gomzjF4pHAU3Xi3NWdXQfQmBTXOEs3E=,tag:ylKc+e6T8jWDm43O1XNBEA==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:doEPI2dGiv08oLdBfrtNSxFA2G3JAfHPeSSJclj4APVFDrqTrFGnVdxVZEsELUBsvtPBf87942fX7L4+opTqYtU=,iv:Aa2ZPZUbsuMSrAWUF/fIbZEIJqvlvjbZiYAhCUdUhIo=,tag:v4Q77lniZGDMj+ZTdMBmgg==,type:str] -S3_ACCESS_KEY=ENC[AES256_GCM,data:vwB86XE3vg4efKrDMnxFgNjMh9+9m+IqfZzcgmT6eJE=,iv:JNx8Uyw4qGA9y77wCyBuOwXj4kY+Or3mTiYflQORQHc=,tag:doZWw9vFc9WmAeYCwnXI7w==,type:str] -S3_SECRET_KEY=ENC[AES256_GCM,data:Iu0wo4qgPUdk/zpk4S0yHXJCqVH9C9S0Vw1TPbVo8z9shhl0/GmSdgKeDUIrpXuM2PGyqgPIsBf46A6ORnwGVA==,iv:ZZWajwWE0PLjEN4hPpx+Im1cisfZlUkIg4PBm3HQ5b0=,tag:8eK9TCD+q2V6j7sRFkSWOA==,type:str] -#ENC[AES256_GCM,data:wAJC60sWyg==,iv:RjxyGe9nMuoemVnBj8jH7/zXwtm7dT6LTVdsA6DvQ5w=,tag:6RaUkl8fb2nlBq1AbhGOJQ==,type:comment] -BACKUP_S3_PREFIX=ENC[AES256_GCM,data:HXINLjypKwQN,iv:0MX7Qebmyha8ZRuN/vt/3mIvApsXbtpmsDhYl/i0je8=,tag:VTXo8vAJV3ESu810/bjeDQ==,type:str] -CF_ACCOUNT_ID=ENC[AES256_GCM,data:NTsh9OYOuOpdobd0yl5u6kG8Dpr0FW6nPh3VZkRGocM=,iv:HYcfttiil7d6bnC+Lzf5aw6IyV8XoEDH/YiL9rypJT4=,tag:rwWbpxDsdN+NzVSnLsTp3g==,type:str] -CF_API_TOKEN=ENC[AES256_GCM,data:utXG/zszpYGYy9BBTl+toYzMncKdI6USIFCYQntaUQqbvdQ9NTqZIyXRmSQHG4l9r4QMPP0=,iv:vjsT4dZlICdI+wwzMOtIC/i5pOQIwgV1Mhnr1KHmMSc=,tag:RV9uy6EpzmAVxkoQktT3oQ==,type:str] -KF_AUTH_URL=ENC[AES256_GCM,data:nmJJWcAYd5rYkWB/NFJNeblQ/qoM,iv:q7HaKrlDKIfm8T4NsjBXlkr4qNbwGLaMNWk+C4fm8TY=,tag:U2yzNSb35kkZViX11P8BzA==,type:str] -KF_AUTH_INTERNAL_URL=ENC[AES256_GCM,data:Kv0ZgmnWS9l7CwkXgxV9fjVIsJnue4MfHqFz4acxL2o=,iv:HXKcVpIq8/GznRznSz7CzVoTet3tjgaw3SyqMPDDUf4=,tag:dCWZnd6g6y/3livKpUfjmQ==,type:str] -KF_ACCOUNT_URL=ENC[AES256_GCM,data:Un3dm3GYts2mKBC8V1dBdTG5R7h6,iv:QuGXX1JSV+egkGpsWdDUhVBvScUSn6ra/PW4h9Tqeq4=,tag:4ax4V1aKfA9HZW1sPSr2uw==,type:str] -KF_AUTH_CLIENT_ID=ENC[AES256_GCM,data:t8yRbfOcoNq79zE=,iv:O7qE3QStFljwyEiJoGnWAukglolacPTuHN5nrftZ9KA=,tag:Js6yNFM7XPvfz8tW1avZZw==,type:str] -KF_AUTH_CLIENT_SECRET=ENC[AES256_GCM,data:rKG4MU2+iYvutBwjZEXvekzrphn/G5rS7w7AYyjmfvolpeN2n2A/0/qsRzObfDmUK9oHSny9hMh87quULJEMEA==,iv:G98aGrbpCOcKevLemnKoUDfCznWEyk/OIJObVdljMS4=,tag:uknMJTV24+ann9+n2zzklg==,type:str] -APP_URL=ENC[AES256_GCM,data:P2FPGGTVQorCezo3vpd1RQf17mLy,iv:p47nF3xiNcciBQE7ixVYPkFLDv1HvoqPMWmVg/OhAz8=,tag:6Np2Pp1VDHauxP2g52hYXg==,type:str] -KF_INTERNAL_API_KEY=ENC[AES256_GCM,data:5W76Sb3vbv3G/GY49V8vhpZeOn9nQ4Ezgg2EfTajGVA=,iv:suuMiX8lKXKhHp+JAmT7VkESeFDgJZK7+ATw3jTsRvU=,tag:Fz1xxA3CAmgNb40fVEkllQ==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpc1J2cHdHL1ZuWHFwZGZs\nUUR0OHhHUGlKWk5PRlE3azhGZ0EwSHcwbWdJCktpVHBLSXVxUW01dmd2ZWFZQ092\nQ0VjeGZ0b1hWaG51VmhsMGNMSkZCMnMKLS0tIFUxd3FLS1NUWWFLWkdNa0RhS2Zt\nUWZGYi93N3ExWTg5elVxWnhQSXdxdWsKpvGWBahHOOTicknPKDOqgkzF0VSuYtwA\nw3SMwZzwQ00gnRLw7LrY/EDAM+KYk/C1egMEAtAPPDfyX5xAGSDIIw==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:/JOFp1qXl8zo,iv:YkvwILjKP3ukgfDU4FAXRspTcOtlriAhF1ARrSK64qo=,tag:MdTBsgsJ96NwB1OO7gil8g==,type:comment] +NODE_ENV=ENC[AES256_GCM,data:G0E4LCkWP3GpxqU=,iv:gHXVHnY7SBuDHZxoHuC+wASCd9fAUyJEnD7kxPq4I5Q=,tag:P5mzOLMi139EQNzbL2Z07w==,type:str] +PORT=ENC[AES256_GCM,data:TxnQfg==,iv:MpcCr/BfSQnR0MTu9IkqaLEHwDr9rmdN1UuFJPE4Dfo=,tag:/QQgaZEsTDRFN05YFwJFfQ==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:v7SbUAfNKKHwkZNalJQ8RarypW9TM5AS5ELaeo2Tao/q2isK7XmrcwqC3y7cSwGrlW7V670=,iv:qquaG701XM7G/zDGaKlQCW8/k8E4pVUb162DlemlxgU=,tag:XaXCPOksPgVSnUMtPORiEg==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:ZAMAQhydGYVCSmgAzx2CajZ2Ivs=,iv:l1XQIa3LusZS/yJBzY8uJAjw9ulZ1h0dhCc7Ttsk4OA=,tag:4f+r1bAI6HWHE01VEOwF+Q==,type:str] +#ENC[AES256_GCM,data:EHmPdIQNJsCHlM5mXxhlaoXtzw==,iv:zMeB/NL81FJ4wfnZimy23Cj+oVP+OrJcw1XSnADx+Qo=,tag:3ikLncPbT82dfAglLxVQtA==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:LMDoOfh9F4fConQ7B849,iv:O978wGztWUl/UGJ9C4VQdBQ0/RdJDR9w3d0JoeLlJdc=,tag:PAd2k2eGHkqfffryQ6NCVQ==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:7rA38EkALQGQ9RXsrlL/xMMC9trsHWaVzc0xv47U13p4neWympZ00wC4xaT1WfL2O7hatAFq2wOeci80UM97HKw=,iv:TWCLRN+JYI1tmDCtE9hN4T/Y2mXf+OCb28LlFTJA1aM=,tag:7ljrPXcM27XTFmTUdiceiQ==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:/jEB8GxyKHN/+3IZwPysg9EGzoF05KpyvCxEV8KM4fs=,iv:V6/u6230hz3zOlYT/Vu0WwsVB+zUmTs3BKS+sKOK5wQ=,tag:mGcJPRfWROQZOjnoUE9JJg==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:NSFIRisBJyck5DmBJ+XuBcZ6Wh62l9BvCztxXBeyc5ba8qrYhNcPMol6fYW3jFgwDO05WPfd/0Yl4vtnt4/cFQ==,iv:U/Kga1EbBjRTKp5/0rngRu029baPkWaPXgooXMPewyM=,tag:UMNf9yfkuCLORN+R8uRNVA==,type:str] +#ENC[AES256_GCM,data:EqyQ7+7nuA==,iv:/3C+1h2EoaCyKRdomS2yMeHLSXCsKlglyGSrFveX3d4=,tag:6QQ3pZD7wweChAi4LfRveQ==,type:comment] +BACKUP_S3_PREFIX=ENC[AES256_GCM,data:zwS0vQbVdzpM,iv:zYnicTlGWhyR/5lAJ/s+1T/bwuLbB74KZummZdT7EC8=,tag:FoLMGUrnH6JzouJCGGzr7g==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:eNjyfXP8R3IkBZahMBHEXHQ/BfE9wH1rX3a901esxak=,iv:5vuR2gDeO6xiCMIzAjlI8pVM1w1LcjSlcTeRCy4BO48=,tag:S5EjVdkMiak70wQQVY1uQg==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:mnmrFcUdk53aaax+uk02Ss+9HwYgSqbyxYp80ZYr4+1b/UcplqoA1wSMXj005DZQUXQ4US8=,iv:N6WrkigbAogpDfSi9trQiYYD9DurAWjr1iDRLtbu+94=,tag:7bFo/80/9xwtp7ntlOx6KA==,type:str] +#ENC[AES256_GCM,data:NDvfVG5fK9OXOlCN0ojPElRnn/I=,iv:ttkB15j3Ab6WJ/m7ML7ihRt3aMWk5AV17idnEmjgMuQ=,tag:3kr2o7KKoU4GwAh8pvOg1g==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:wJ+w0GnQACTcNWXgsaaWVCU2HF5d,iv:R6k+E0fYot0kC+daLWUROqlYAlEZGOEEKGiePHYf6bw=,tag:u6ptwpKsR7gcrFfJckqk0w==,type:str] +OIDC_ISSUER_INTERNAL_URL=ENC[AES256_GCM,data:sK2O2GgXn1G4TnBA4j8jEjlfQ4vw4YAl207U9IGQk34=,iv:XqA89gOHbIUlygddNpRu/McL6pibPlsEmdlFV9OF8PI=,tag:/bsevi7+NXjFWfz7MiKOsA==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:BpC8mloAIXnShdU=,iv:MZqmeq6UZ8iS5RX+d+suLxNGIpu+hI04QuCO/GD5glE=,tag:F/mhnLP0jsvuV69huovGkQ==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:fpSa/WfQu7bVScAfo0APRpoGWZgwBX38MAsWhkiNYsN95I6J0twDaFBlOwH8Xkhv4wFz3FbhVrQ2cyIHlmVm/A==,iv:a6NwC4Oltkytu5cLfHiTDx1k/nwucQQ0yfgdS6HYTOM=,tag:CntUqzB0zwYTc0di7iLhjQ==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:8OEi7Xe3GCL4Emt6I4Tp1ofzk1TycM3L7y5dH+yZPCE=,iv:gj4/n9eQFYbFJXWe8OzOsPUL9cf1/6GQ2nzeCzc2dGI=,tag:oz+v5bsTYynyr/7IyxHIKg==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:Mk7/kRwo+ynNnsXseSvmaQsvh8Jq,iv:vUcEbL3w6rh94+yVk+G5ZiebJYaVJtTgIckyuiCKp1c=,tag:R0iJNZY0iUjzfshAPuKDFA==,type:str] +APP_URL=ENC[AES256_GCM,data:0vzSZMY99Ywa0+HikgLknFOq9D5L,iv:fxtP8b0ihBPjqtoxsS/LU3M7UjT5WB0Md8dOeTyFIkQ=,tag:c/zR1WNBx9qWBtnKSJx5pw==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3cFhvbktEb3JZdGxMMEt1\nNDcwRXVHbC84NVdwUHlhcFdncU5xRkR6bmxJCmRBL2cwWW9ZUGRUS04ySllNT3hm\nbzFRR2xKbllPSzR1bjVJU3lSL3k2dFUKLS0tIFJpSE1Wc3B3V1kwRlpGOUY0TTFo\nVlJpakhlWDF4amZIOVF4akxzeGYya2cKaBsAXo+d3n4E46X7XCxvuNV69YGW0kH+\ngYDMnYHTj38BHmovffGB8xBByBtIH9NXBM78JabdZfm6Tbm4f0DppA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPaTdrOFo0RGpOQkp3NCt4\nUEUzS3FZTjZWc3czbUhISHZyVFVrVm9CaEhZCi85WkR2NmxObFVneDdqZFpja1NO\nZldyYXRZSjRnUmhMRnlOamVRa2VCdGsKLS0tIHpTTGJDWUFJQThiR3pERUhXY2tM\nV3huTDVYRnlabkNuQmJqWnFwT2ZWMlkKlTbID7me8QBHvBjljXvj+kl5Hmo0Oprn\nSLT6MSYP0rvuD3uu4Qj+Z6IELbAzrt6ffGjMx+xohWse5R+u8UFMzA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3SFdHN3hUZE83a0V5eHE5\nSmRiUmR3djN1ZHJxUjZOY1lTQ1pxbEpUTlg0CnhFZWpKQjZCMG5xVFZsYU1LRDBj\nV2h0U2tYa3hIa08wUlFPb3ZITVJjSFkKLS0tIFFwcGRudUxJYnpCNHg0ckNXdEhh\nK3NtN1dZQnJScmlZRVc5N1pVQVZZNWMKOhDUZQMIF4RNibiASaRv0LcynMSMsQdP\n6qd4OOp3eGOPRNRYbEJcvc4lkIM3VCtmb+/cDJK54aZN+AWTrn7OLg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZMkhPQmF5RWJvWS9UL2hD\nWmlEK2QrckhIN255Y2FpcGdkenF0eEJZSXpJCkdESlB5SEZMVW1uNkdWeVQrUHVY\nQjNtZy8wZjVwcjZWTzFLam1LaWw3b1EKLS0tIDRyM1RiTERUN0hFMWVCQnAraktu\nWW1pQ2lwZmZBN0JlYThKRTdLRE5qTDgK3LZGWZwVlmmiQ4CghkA1VEJLWaOsRCE7\nD57rKhXq5/QKjoziyXsc//PxZnJaTiJQ2xxsG8uMLDL5sCMz9NDiTg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjNUR3TDhITElMdE00MkxZ\ndG45ckdyNklDaHNnYXRWS2ZLZGRwMFhpdWl3CjcyS2lXcWJVRkhrcXlGMU13NXFz\ndHFWcHlGd05kRitsZzdsSlRwR3B4V2sKLS0tICtwSzQyVm5ITUhZR3ZCdkpIWjl6\nejk0enVaVGxwMXdRajFiRVFTbm5sNUUK21DqPtP9y4R6MEuboU8RnNhdQXY0CH68\nAU4vpnQ6lC/pIgBCeI1BCFV+x87qnxiGcaUAVI1t4KC017SFmuAxUw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYV0ZhbVVxZVcxa2ZrZVRU\nVnorTWZvc1JMc2ZhM01mbFBRUTNLeTAyUTA4CjVubkNNZ0xoUjdhVXloNDZ4clpO\neUlGVlB4QTB4TEhGdVNPaXV1UW1YUTAKLS0tIFJrOUJSSlc4RmpBWW5QK1pRdXhK\nS05OWGh6a0RrekxteTRGSnhZd0VMRUEKcYsvFySf+hAGk3zsHN1MOK2/AayQQsQI\ntWerQqnKHoKHt5Fd2WZRUMrtzyV5stTZ0WwUhMIvFnapz+sQNDSYSA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqWmdPRjlPTGZrTmFxcjMy\najMyL2c2RUVHMDNNV2wzZGF1MXcxWFphZVFRCkhlSFFvQXRuQjluWVdBMWtZM0c0\nOGpqa1hkWnFobWllc3VIeGxHOVdVbW8KLS0tIDBFZkgzVnlzZzl2RHowYnRtaUdH\nMWxnWFllcnhyWFM1djM2QnYxTHpBRWsKmJOSjUn1oNtx/i+nKQoBwdYsnA9z0JCa\nQrGhQ5YXP/cRLBaunywJLNdcWCnG1dDi5PUkEEnELHqi1nwd2Ge8ag==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnY0RGNkNoczNrTHdYbmtN\nZTNtY1RQNHVHMmNhOEJCNzZ6ZVVxUUJLeUd3Ckovdm1vdWttR2NMeWpZZnhnN0xD\ncS8yZEJjUkhQWG1ReWdTNDJOU3cvMUUKLS0tIEpRSzBOZmxRVERIQ1pWZDFiV2l5\neGxGQ1ZxVmZTUjB5OUNadENnTWNUWVUK45kOkImJ/sdznjiCBSV1BLa6Z3ZFZh/H\n4WhN0fUKLPoBIY2MtcETa0XFuIdx54p+CJuMlok6KUuMh3h8ORxsVQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvekJ1ZUxET0tvbkJTVUVF\neDYxdnpZd1pxOGhaRXcvaFp3RFdHSW10a0drCkdZZDQ3bkFsa0loYjhpenl2Q1JH\nRlNEQ0JPQW5mdkNEVzVtOVFFbEFtNW8KLS0tIDlITXViVUxSLzRDVGY2K05MQ1Nx\nYjNGSzlXc2hlbTBBMStDZjVSaDA2cnMKgIOm1HunSwCHRu3ciWG2KRjucfV+2OM3\ntwYtmVdCVIIXYxwRU1HHcjZZEtPUIS5Xbb6XyvZB8CgVBLe8L3ixEg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -sops_lastmodified=2026-05-18T03:03:32Z -sops_mac=ENC[AES256_GCM,data:QYYsO6U+2eK6GX90Y8+uy1LtPlJdiWTkj9QPZ1jgEUlZJOc79xPBai7i7TfwSbfG9Pv0aPVkcWjoIIBQkGC3Jdt1wIGMLCmlIeHU+MNH1ZcdOlmn9PI72GbMhWGF534kJFfgSxWJDrH2ow9NqOebcDESsZ/S35ajCUeyqAD5Yas=,iv:ZVbQlqQF5IU7YkfexRNQPDbXHnPlZ6z6soBpvzUVpIc=,tag:vINZd9Dznsyf0zxXdbOU5g==,type:str] +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEOWVseFNmYkprekdKNmNP\nM3FMSFVqVFI4ZkovcFZjc255bmRrODdkeTNNCjRndVdmQVU3YkxSeTE0aWJnOTRY\nUVZGTmovUEVUUW9xVDAxOW5ZVkdFVWMKLS0tIEsxOWpOWnZYbjVTMlkyR0pJcjE5\nRWdPWUI3TnV4NjYwWEhrU0ZBNUVpaGsK9UGm6eSy4SbwmkaBVoiKVXwVBIHQXRG0\nbq3T5HpOJcCViVuOzrm5mF4pPK9WyVBqpnc3zxPZIb9DfPWLa7+5Eg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_lastmodified=2026-05-20T17:21:37Z +sops_mac=ENC[AES256_GCM,data:usGnoCBtaNFyUVJK+H6PDlODzDBKeO6hagXWhLz0dS/jEIj9z3enfPniIalb6K4ZHBVaENdsErEWRz4h+u8Yc740LLfQZvPzsmY+8F5yyH6tOHYZfYxr8z5/ADmC2UW9cx+Mv/Jl0l9v32UlaMIF1YYGAa6DSz4PevdJYLyjATs=,iv:04ENRUatPgleieKadCcuHLPolVKoWe8YYkEqr6aebWM=,tag:KtkYhNqR9/b6SEgBbiqOgw==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.env.prod.enc b/.env.prod.enc index c1f0b6f..2454674 100644 --- a/.env.prod.enc +++ b/.env.prod.enc @@ -1,29 +1,38 @@ -#ENC[AES256_GCM,data:G79O2jtY5wOd,iv:PPCHTkPLT222tWp72Yf8nOpUSQ+Xtl9WElZrALB80QE=,tag:eINk1hO6VgXTgY3xulstzA==,type:comment] -DATABASE_URL=ENC[AES256_GCM,data:8a45ENrpPPPmc078uZM+szGNSus2H1nn78yxEnCsfI1MAmTVdoK5mnRfSOdnVz7s9GQEkCQ=,iv:P3k/harr4BNmXks9qOZI0G/M9al33P5r8ze8wgjhizU=,tag:u4xB+ykAJ5Sa+fAzidhaEw==,type:str] -SESSION_SECRET=ENC[AES256_GCM,data:5HFYIHfpPDZMhutvmw/B5Ja+TOlw3Vm4YpP5GqUTrRU=,iv:2cIpp0mjV70bwGSGgaZTujp2/X4FxJw8akntGT3XHGk=,tag:sRm2qujcLBKt4hFXh9vlsA==,type:str] -PORT=ENC[AES256_GCM,data:cmAkiQ==,iv:IuiEZmYBgluA/eON4EbUZfqZQxeBboZ1ucaGBB2KTB4=,tag:R65RYJlxA9dZxIE9I15/Nw==,type:str] -#ENC[AES256_GCM,data:h8th,iv:3UWBQr6vElyiPMFRCHFluuNdjsWvelZK8z+oHI3zVA0=,tag:8bPbDmEITWt+qvAKRoLEuA==,type:comment] -S3_BUCKET=ENC[AES256_GCM,data:gJwRRmJZC1i3hyjKJLFK,iv:Kzm8l5jNoaX5c+OtBaU+fgSLTrI85Lu9IHcTWjqZrnQ=,tag:cxF/kmlqTx+SmyEwZ2/4sQ==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:+qV7lAVsT5SAnxU9VLixecjcTSLgWtN11ihQzSUcYyFtP/4f8BkmdDbCbndx5LGJ5+VXZqgIY9rX9l0UiCrRhd4=,iv:EJERkOiZBN7QBDOhggdnZqS4d/mqBV0S7nMnkuBConE=,tag:NIovhkVEVmkfA1lcJpWmng==,type:str] -S3_ACCESS_KEY=ENC[AES256_GCM,data:a6ULL1I8WkBLElwYYvSKfUeqghe2lbkHRBkI24Tn3vY=,iv:EkG7Jx930v+p309GFHTdPyi/AOYsrZPAWJbxpqAD+10=,tag:jXcLsfS7nMPI8sqSHNx97Q==,type:str] -S3_SECRET_KEY=ENC[AES256_GCM,data:hzl6E8NCCumH4C7VdBbVvYIM6hDGllBd6VO3Fi30fCieuTALHfrEFbHfxS9YgvfynD2hIK+GS8CgBXGtO2OBdQ==,iv:PAqnAY2Me5UgmrLBZsZuD6N20QJZgL/bWszzCnFMA30=,tag:CFOpvlovhud3IjD5PWFY5g==,type:str] -CF_ACCOUNT_ID=ENC[AES256_GCM,data:pNRHODXwuEgxTujxDKqD4AFSlnlN5QxoksT12zf5a/8=,iv:2/HQxHDrh7DiwjHgCZCs2EKXvMtPSakfPpQGAiTFDWU=,tag:QgXB+Cwm4hJ9jyzZb7DEbw==,type:str] -CF_API_TOKEN=ENC[AES256_GCM,data:QeXoWYFuRfCK4iBvszGq45sL5LnEsRu0A2o5K0TV1OjZCYQzm3KzBrKy6lVPi0ocQZZW6YY=,iv:LeFbb6z0hrPXOF4ThTYNQ+HOYywUf0q1cyDt85d/Q38=,tag:Pvd5wQuSDOyWnuBHUlRh7g==,type:str] -ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:+1GMiZc=,iv:tFS+h5ZmvYRjn/1fbn9L9TzvIJXCgrK0L0aoMw1t3ho=,tag:FS4qiSsWZUzgZrPYXaArqw==,type:str] -#ENC[AES256_GCM,data:JMY6pl3vv+/WUg+XKcZaiCRK,iv:LYHlOqnGZJWtMFHwRUGw/2vY6dBetyyNfxIAJdgdhvw=,tag:/3mR2XZJJFfJ7AT86Gm0yA==,type:comment] -UNDERLAY_UPSTREAM_API_KEY=ENC[AES256_GCM,data:mOJ8BO5XC7lckg3a8lZIsalt+QaXufTWI+r3BpIpNjopXU8=,iv:Iv99fo5Lf5A6CoQRse4oMcH1iBhP+BQFEVsQfXr0nwQ=,tag:Mqks9E5KMOj0YDLvbdalSQ==,type:str] -DEPLOY_HOST=ENC[AES256_GCM,data:/sJzsacP03kwQBtQQA==,iv:OA/emiwAyDLtdsxutC8ydE1WeAHaI8u/1vbmugRtOYU=,tag:R+P9FwLluo3c5GHMJooufg==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzLzVaZlpNeStza0p2a3V0\nZFFXOHh2ZnNCMFU4bW1ycXZPY0N5UWxreVFvCjRSRjhGLzNTODBXOXpwWTd5T2FI\nLzhvdGRWS211WDh3KytBa3RmM1Z6SEUKLS0tIDVlWEczOFFjM1VmTnllODl2Vitj\nWnoxYzd3U3lvVFJjelhrcEp5ZFRyWmMKbaggcwEccx1k3UYOE+a+bLNPlBL4vIsx\nhRav2a5TUBc+Wp55cbfliiTQCm+lXcv6kci+YeKHQf6Q67VDeIFRVg==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:TS4j3twMFoZh,iv:67VeU7kyhgPK0TptCh60HYxLTe56pK2862zEboOr26M=,tag:4JiTyCGuL3fqU7LlqtYzkA==,type:comment] +DATABASE_URL=ENC[AES256_GCM,data:9c2V8Ezkdb4sXC+mrz6BhrtesdQ7pnA5PjrffBIRBHNfPSkZft1HrIGAEvgde25jFeXt1Ck=,iv:LSzvRX/PSto7ROQ5TXCM3G5pg/Qm2RiQnX1ZOw2Gc3Y=,tag:OkYD9v2+pZpQBNQay1Cxyg==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:W2tBSuCsF69dFbX+uYYa/5H6KKnVWYl3Bibez0btM8g=,iv:DE3NdgXvRJbd6Xvnu9ZVCmOhzbANvvw9X/DkcZAGGHw=,tag:gs+2u6AmFGISNW+3mCjSLQ==,type:str] +PORT=ENC[AES256_GCM,data:ZbjWrQ==,iv:LNymntIzDLZO3Y4+ROk0StHRoD73LP5ympAFIeihC9s=,tag:PiiFCezINAJjqOAyOFM8Rg==,type:str] +#ENC[AES256_GCM,data:T5oV,iv:rsC4Pk0F4XQ2lVXl+mKL+hrnBYQAMuWuaJPgvfleXPg=,tag:eC85ejuc/0YgfrrtIxCXSA==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:uguPyh8NpEh5UQtgeBBd,iv:trAHyJZ0JDG4d4G2mudHLcvsq3Mimlw+GnGvf4DrbmU=,tag:Cs1RVRextKulmyknzr6qYg==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:XmbQiGK/QP7SEazjUrAZWX0daHnXNWNmWqzbSzh5goFBkR62aKB9IefC8wtQ9NTySQlNv+kuisVOHQJ8/SFvVx4=,iv:AIur7LSlRhHwcPYnuasW89dt/Zr6I/qtmwVJfy3gHdE=,tag:IuTmLbTUmIw8fKm2xCKXiQ==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:3pnGOvtCorluqErK6kZtUz7cYJQaUsJgl2X7mqZDgg0=,iv:L2D4WRE45Q2L7S6WY0tffaS9KRqNTvXZLtwJMU6tHSw=,tag:ZOpEFvpihR0tcsQ4WVSDsg==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:e+wGR1nZbyDZ8J/inqOooffPYlFHxa7mS1XujmP45LZx704GIs6RLZ4v1WPkjuoq1NoGV3HPe3gk5u2vU3vz5w==,iv:6TtKzHjhQG5xCnhLFPmp/CEIlBGB6sZIEkDjeIU+OWE=,tag:O0biF8VDHRYz7YBy2OgDAg==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:OO7KrYgpLwbj/VWx+6bSCL4qo7LhjcjNPFQfl0Gg0KA=,iv:h1vVwXbxdRBbU+FbGpdZrbFarKdYzDn+aUS8fvLhyyc=,tag:y80M9a7h45UIrCFDO8h/Ow==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:iJdQyRQScMxcrcU4CEQgW7+qKWmb7s93QRcYgh291Qzf3NC59ExxnOpBU++ZV6LFWjUDdUU=,iv:xztI5eMbEk1WNw3o9zrjbi8jJ8NtAuMJl9FbWTUsaTE=,tag:1lOSYrww6hZ/WjWzSTW04Q==,type:str] +ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:8xsRwK8=,iv:tCaPuOCVI5aoxnwxTKa7OMqGFQD+W8ll76Q8Srw9gt4=,tag:C03OWlm2jOOcSrCGNn2whA==,type:str] +#ENC[AES256_GCM,data:+BWEFM6L6aD1rMG+g/Fypx73,iv:7ez6bhjdrh4suF7FnzeqbV9YaGWQjdzHwrg5C12POdA=,tag:lPwrgKv9U94oei4l9wnPsg==,type:comment] +UNDERLAY_UPSTREAM_API_KEY=ENC[AES256_GCM,data:gg8Fm3ngcXfMSqnravyzd4F7nVaOfjF0kLq6akC4c41pqoc=,iv:3tjfQSU3RoapeTpZ/26b/cRyXEET1/plU/zAAZqBUek=,tag:SchlwcyYhcStKabyhHXHsA==,type:str] +DEPLOY_HOST=ENC[AES256_GCM,data:R3wMJvEgnZLS2sUzJQ==,iv:ILujgHggz/XnjDQajRC5UPSdiKWGPqDtpG7MxbfycBI=,tag:gjmxSKYwdxKt0PZzoxE5Uw==,type:str] +#ENC[AES256_GCM,data:kYe5O3MvtkE=,iv:SsygxrvIPlPYGwdmgghhWQEmhYXfHTqjYEcqQBwdBn8=,tag:c7IPDL4NUvHDY/HvALw4Qg==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:6EoGyzM6mMjE7gkG2VHCmf7wjfoUAebL2xNX2b4AlohZ,iv:J9UCzieBnS67x1SF99LQxNXRFtXnzAsx/nKgHZAsEtg=,tag:aXKXAXOhWUiqts1j/85y1A==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:YJZQDDDbW3SCUsw=,iv:i02lGIh0O1Ni0pnQzFQwdA4yGW7DMaGBern62zEurdc=,tag:HyQbPDlIRPkodn9mMixrgw==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:CKbYqzPEHYnffkosxGMzbaUQrjmAr1mbpkEObM/COucG+nr35sJ0eZCVUgq9rh4fSXh7XX8R5MUGrjSamTZBtg==,iv:2JwORSKnfi+7apoQKkxYAxwBK0WjWLSADDowRExcRik=,tag:AYwc/V95qREMocDAo7cMSA==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:T80WuhiPSGm1FMNsvrM+4wRINOSvxhxoVMXeybBOjiNrKb4V6PxX54R3rweyqe2AJNdYlv2zaiQVYX3C3cvQ9Q==,iv:ycYFuwUGrSJGXw9gwxWQrG4Xj1DZfBW0SoUaBRopBAo=,tag:+FZQLFaWnwQSBZQUynAwxw==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:bqv5vgziTgy55XFOdtYm+qPzmFNrs7Xn4kTxCPzcHy7ikbSJ,iv:+9jMnI8mQgZm9c2+5BQCpO8fkOdDoXj/4WifPjH8LHE=,tag:1fHxiv8ST6DOYCnw2kWn3g==,type:str] +APP_URL=ENC[AES256_GCM,data:GgfRNwfNJ7O3mCanfl+EyvDT8bqH/qE=,iv:4soW9wM1vyXRMWSSU9ZuGKwwXWr0/BNQt9eU+K7Ahjc=,tag:PfFGv8qnIOufUXfORF5D3w==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGN01qQVdQTHlHcWJHbHNI\nUXVnaE1EcEUxRG12UGx4YTU3SmdBUmhuYmwwClkvaUZHelB6a1FTQ1YrbzJia2dR\nRkRaQUJJejhMeUJLMnpqUmRTTVBBbm8KLS0tIFZmNkcrTWVZUnpaMGU2N3VsOVc2\nNTYxemxzQmxxbDhDeTQybnRxd0NrQzgK6AwczPTgFWItfLq6+jc3d8AwHBJ6UfOo\nnJoEcNu1hjcXWxb3PLeEVer9ICR1VcYBZEtyOZ1dPKgb3OLOjTym7g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXMjgzNlZhOGRkOGgxTGRn\nZEhnNXYwakE2ekFGc2c4eXJ0VUZLT2p0QVVFCnQreWx6RjljK1F5L0hjeXFmZ3lX\nbGlBNHhxN2h0MUM0ZWo1eFBpQ0xNSWcKLS0tIDRsY3VkYnBETlljbE5yM3VjdXA0\neGt2R0FTam52VTVRdnlMOFNxZ2VjdHMK+L7MquhICF+TVqX4Cd7/6z8tIouB4NQR\nYs9eHJukpgK0e/8pA4LtZG5PRkyEbaWIIXH9XWvQ18+nXa+kOYiG7Q==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWEN5L1BtRloyWHNzZklN\nL05uazJLTWRGS0pXQVMvQU5kd1p6K0RDQVM0ClJBd0lsQ1N4bmp0OTMwRmpNQXAr\ndTd1ZEZqaFhoYzU2RzRYN1Y0aHNXSEkKLS0tIG1uMDZvRWF3VnN3ZXZwb1VwVGNm\nMmd6TmxxeUkxdi9CQkQ4Wm5qQVVya2MK9TRB9XCAtavdWC+rQnGd9MAfn4KenAQI\nludEN39eo296ZzAUE6WJ5/7y4U4CcpCyKBPILKdCjpeNRYQ6pmR8wg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmNVkvd3c1NEpLYzdNQnlo\nZWI1TlpEbDRjWVJaL1dMSk5VK2I1SzhxU1JnCjE0MW1BS3lOdEZOa09WSmxJdkNS\nK3ZLY3lWa0g5ZGdwQjlXSmlsS21lRDgKLS0tIFVUYWZPVVczNU1yVnJqNGNFMy9L\nUSt0QkxUM2V2QTRXdkNRV0JsZUF5ZXcKh37S7FzQ/FesSscsliDL6F5OJhLVmZpd\niuyotrYRbxLJe07FCF2qKCEAZgJ+LQSHOGXFjXyilZhaCD7jjlxuGQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCcCtkYTNvUm51TmV2YWZ1\nRitKQkw2RUJwdkltYlhvUWpNazI5TUJwcnpjCjNrTms3ZFVXUGhlaXBlamRETXBE\nN0Vud20wbU1FUXNmQVp5YnFVZncwalkKLS0tIDdPQk5KbUo4ZmRpa1AzWk9pRlFB\nNU8zN3lLRnNpQ3RMRnBXWGlOTTBWQzQKXtQDtLS0TNZiW46EsrW6tnbxXc773oMW\nH8BWDxRavEpiRyqkH1EKjEBzTA3xKfwaG3RT6d6tUY6g9PnuL0BkoA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLdUZWT1g5N0pIeU85NFl6\nWllydjNQM05BbkJMbmdGOXU4YnZ2ZnVIdUZNCnlUclhJa0RPNTJvbGhEaWJlaUxh\ndTJDMFFoYmFpK0NyTkc2ZDNBTk5jSHMKLS0tIGkwZlpnUy9HVnZvUDR6WE1TbkN4\nRWx4NnNVbE55bkpxbFJUWVZTVnBvUGsKaBWF4o4HEa+AYrhab0PlS2jbMPLy5FcI\niULVo+OXB/rF5309D3kCSAECHGhAaWTKKDQaX8L9r8PRPxiKHnbukA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvbU9wVnlZUjBDQTVQT21i\ndU01TTEvUzlPTzZGMFVwc0M1bkkvNUU0YkNvCm91NHdGWTVvU1JaOWRmQVJ3cHBo\nMXdmc0syVncvZzNKMlBCbHJDdm5ZblEKLS0tIE5lOWJvMDhyM01VbU0wbGNEc2xI\nVkZrbER1LzhWQ2swS0ZTV1FiWGx3K0EKIoezkGxEUfWDjlkeAnKH20UxsDFQJ+1b\nzdcghmt6gnzIxR3FWSCFHHKAzfi3rL8QlkW3Ro5UBoAEEDWy7nvz2w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBudkNDQTV2Ukh4T1RGMHZM\neDVUdU5FWmVhUUlQc0VTa1BuS1cxY0pqS2s0CnB0S1YrUFlpRE91akxXd0k4Sksy\nbWl0NWtaeDBWTFFNMUw1dFNUQkwrSUUKLS0tIE5NYXJBZ29qOG1QdzVMTDVXUXFq\nVXdqSlpsQjBlUjRiaHJ6NEEvTjdDVGcKrL/jghlJmI/2gXli7FBkSkHn/48CC10Q\nJd20NjPz12l0u6jRodhqJbUdO45JVoa+Jn11kGegA1+2RC/CbbLeZA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWU1drSWRUbEVYSVNUTzFy\nM1JJbSthM3VpU1Y0ejgzbjNUSEhYMENGRUNZCmowZE13TUtYclVmMS9VenBVQXp0\nd09hM0lsa01Cd0lzTDlCRDduOWd0aFEKLS0tIHI0M0xOSFhndW5XdUJJV3cwV09X\nWUx4T05INWRvU01OU3UwSUFxcC9nM00Keb2AeDtqamChWI3PAYxLDT3qZhmCf8Q9\nZi8Qe6LJ6a8C6wsFN+auPy7bIJdmVYcfoHFgsRVdyEXx9bcj1+hIGg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -sops_lastmodified=2026-05-11T15:15:44Z -sops_mac=ENC[AES256_GCM,data:gdyOZK39HYrvgEIYaPpQpdaXdaSUUPzbJoMD+GD1jUYk3Tnd6cCQzCczuV5FYaX2h4iz3L2SQkQHi0ltlmdPSUQjhdXEQgVKjJ05Z2TrjtHPnishIZJT14CxBZ/ZyvbIvZe7XZDKX13tLs89ozR9v11oswfxQx5HyrI/qekUJMQ=,iv:AaDc3U9qDEPKr0xUhUfIVCwOBxqjr7xSnUNtwv4UyVw=,tag:bjOsQwuVBS8il62Cpq0QPw==,type:str] +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSclNPdDJGeWlCSzJtQi92\nYVlaVFhGd2krM2J1VEVqVSt0REJwRnI2ZmlJClBzTCtSZFBvMWdBcU1IeXFzaThz\nMFRCazJmTFQ4WkNPRU5xU0VqOEJZbkUKLS0tIFF5eitRekJUSDBhZmhsZHpMdHpi\nK0N4L1FqazJOVUNYaUIxL0dMMm9kZjgKr3PaMpDWqnt0J3yMZxuD0hvCVDnSuX+G\nhSSLmO0gaXuiw2/m7ehzkTtcl8BlN6o8iVPfdbA09lOAl0U9vOPSaA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_lastmodified=2026-05-20T17:24:41Z +sops_mac=ENC[AES256_GCM,data:JUtXxPbs16EWlzLgiPArQMZWlIhixP9KQyBnHSW9vAbApSzop+MCAhVcBXrbBsozhn+EPej+br3r3zfAjR9gof+lSh1WBqSKkAw9sR1gMcfV8T4eOKVk7nCUZRuKVhoSHyDvAv6gJZF0LKhvwBc1TR0A4msZaecDDG5jNSaMsDw=,iv:RbmNhPgiPozA3WGpZAqk5xTMhuFtSOk2bsbollKT174=,tag:UTGqsTB8uMcMV7esNHQS3w==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.sops.yaml b/.sops.yaml index 1fb0434..47614a9 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -9,4 +9,5 @@ creation_rules: age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7, age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy, age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh, - age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 + age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7, + age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h diff --git a/README.md b/README.md index d1344e6..6a6bd70 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,23 @@ Required GitHub secrets: `SSH_PRIVATE_KEY`, `SSH_USER`, `GHCR_USER`, `GHCR_TOKEN ### Docker Compose Files -| File | Purpose | -| -------------------------- | ---------------------------------------------- | -| `docker-compose.yml` | Deployed stacks (prod & dev via Swarm) | -| `docker-compose.local.yml` | Local development (source-mounted, hot reload) | +| File | Purpose | +| ----------------------------- | ---------------------------------------------- | +| `docker-compose.yml` | Deployed stacks (prod & dev via Swarm) | +| `docker-compose.local.yml` | Local development (source-mounted, hot reload) | +| `docker-compose.withauth.yml` | Self-hosted: app + bundled KF Auth stack | + +### Self-Hosting + +Run the Underlay with a bundled auth server (no external auth provider needed): + +```bash +DOMAIN=https://my-instance.com docker compose -f docker-compose.withauth.yml up +``` + +This starts Postgres, KF Auth (auth + account), the Underlay app, and Caddy with TLS. On first boot, secrets are auto-generated. Set `SMTP_*` vars for email delivery. + +Supporting files live in `selfhost/` (Caddyfile, Postgres init script). ## Environment Variables diff --git a/docker-compose.withauth.yml b/docker-compose.withauth.yml new file mode 100644 index 0000000..6a848c5 --- /dev/null +++ b/docker-compose.withauth.yml @@ -0,0 +1,169 @@ +# docker-compose.withauth.yml — Self-hosted Underlay with bundled auth. +# +# Runs: Underlay app + KF Auth (auth + account) + Postgres + Caddy +# One command: docker compose -f docker-compose.withauth.yml up +# +# First run generates secrets automatically via the init container. +# Set DOMAIN=https://your-domain.com in your shell or .env file. + +name: underlay-withauth + +services: + # --- Init container: generates secrets + config --- + withauth-init: + image: alpine:3.20 + entrypoint: /bin/sh + command: + - -c + - | + if [ -f /config/.env.withauth ]; then + echo "Config already exists, skipping init." + exit 0 + fi + apk add --no-cache openssl + AUTH_SECRET=$$(openssl rand -hex 32) + CLIENT_SECRET=$$(openssl rand -hex 32) + INTERNAL_KEY=$$(openssl rand -hex 32) + printf '%s\n' \ + "BETTER_AUTH_SECRET=$$AUTH_SECRET" \ + "BETTER_AUTH_URL=$${DOMAIN:-http://localhost}/auth" \ + "ACCOUNT_URL=$${DOMAIN:-http://localhost}/account" \ + "DATABASE_URL=postgres://kfauth:kfauth@postgres:5432/kfauth" \ + "SMTP_HOST=$${SMTP_HOST:-localhost}" \ + "SMTP_PORT=$${SMTP_PORT:-25}" \ + "SMTP_FROM=$${SMTP_FROM:-noreply@localhost}" \ + "SMTP_USER=$${SMTP_USER:-}" \ + "SMTP_PASS=$${SMTP_PASS:-}" \ + "KF_INTERNAL_API_KEY=$$INTERNAL_KEY" \ + "BASE_PATH=/auth" \ + "APPS_REGISTRY_FILE=/config/apps.withauth.yaml" \ + "GITHUB_CLIENT_ID=$${GITHUB_CLIENT_ID:-}" \ + "GITHUB_CLIENT_SECRET=$${GITHUB_CLIENT_SECRET:-}" \ + "GOOGLE_CLIENT_ID=$${GOOGLE_CLIENT_ID:-}" \ + "GOOGLE_CLIENT_SECRET=$${GOOGLE_CLIENT_SECRET:-}" \ + "ORCID_CLIENT_ID=$${ORCID_CLIENT_ID:-}" \ + "ORCID_CLIENT_SECRET=$${ORCID_CLIENT_SECRET:-}" \ + "AUTH_SERVICE_NAME=$${AUTH_SERVICE_NAME:-Underlay Auth}" \ + > /config/.env.withauth + printf '%s\n' \ + "- client_id: underlay" \ + " client_secret: $$CLIENT_SECRET" \ + " redirect_uris:" \ + " - $${DOMAIN:-http://localhost}/auth/callback" \ + " skip_consent: true" \ + " display_name: \"Underlay\"" \ + " allow_sign_up: true" \ + > /config/apps.withauth.yaml + printf '%s\n' \ + "OIDC_ISSUER_URL=$${DOMAIN:-http://localhost}/auth" \ + "OIDC_ISSUER_INTERNAL_URL=http://auth:3000" \ + "OIDC_CLIENT_ID=underlay" \ + "OIDC_CLIENT_SECRET=$$CLIENT_SECRET" \ + "AUTH_INTERNAL_API_KEY=$$INTERNAL_KEY" \ + "DATABASE_URL=postgres://kfauth:kfauth@postgres:5432/app" \ + > /config/.env.app + echo "Init complete." + volumes: + - withauth-config:/config + environment: + - DOMAIN + - SMTP_HOST + - SMTP_PORT + - SMTP_FROM + - SMTP_USER + - SMTP_PASS + - GITHUB_CLIENT_ID + - GITHUB_CLIENT_SECRET + - GOOGLE_CLIENT_ID + - GOOGLE_CLIENT_SECRET + - ORCID_CLIENT_ID + - ORCID_CLIENT_SECRET + - AUTH_SERVICE_NAME + + # --- Shared Postgres (two databases: kfauth + app) --- + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: kfauth + POSTGRES_PASSWORD: kfauth + POSTGRES_DB: kfauth + volumes: + - pgdata:/var/lib/postgresql/data + - ./selfhost/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U kfauth'] + interval: 5s + timeout: 5s + retries: 10 + + # --- Auth server (kf-auth) --- + auth: + image: ghcr.io/knowledgefutures/kf-auth:latest + depends_on: + postgres: + condition: service_healthy + withauth-init: + condition: service_completed_successfully + environment: + NODE_ENV: production + PORT: 3000 + volumes: + - withauth-config:/config:ro + command: sh -c "set -a && . /config/.env.withauth && set +a && node dist/server.js" + + # --- Account server (kf-auth account UI) --- + account: + image: ghcr.io/knowledgefutures/kf-auth:latest + depends_on: + postgres: + condition: service_healthy + withauth-init: + condition: service_completed_successfully + environment: + NODE_ENV: production + PORT: 3001 + volumes: + - withauth-config:/config:ro + command: sh -c "set -a && . /config/.env.withauth && set +a && node dist/server-account.js" + + # --- Underlay app --- + app: + image: ghcr.io/knowledgefutures/underlay:latest + depends_on: + postgres: + condition: service_healthy + withauth-init: + condition: service_completed_successfully + auth: + condition: service_started + environment: + NODE_ENV: production + PORT: 4100 + APP_URL: ${DOMAIN:-http://localhost} + volumes: + - withauth-config:/config:ro + command: sh -c "set -a && . /config/.env.app && set +a && node dist/server.js" + + # --- Caddy reverse proxy --- + caddy: + image: caddy:2-alpine + ports: + - '80:80' + - '443:443' + volumes: + - ./selfhost/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy-data:/data + - caddy-config:/config + environment: + DOMAIN: ${DOMAIN:-localhost} + APP_PORT: '4100' + depends_on: + - auth + - account + - app + +volumes: + pgdata: + withauth-config: + caddy-data: + caddy-config: diff --git a/selfhost/Caddyfile b/selfhost/Caddyfile new file mode 100644 index 0000000..ea30e34 --- /dev/null +++ b/selfhost/Caddyfile @@ -0,0 +1,17 @@ +{ + admin off +} + +{$DOMAIN:localhost} { + handle_path /auth/* { + reverse_proxy auth:3000 + } + + handle_path /account/* { + reverse_proxy account:3001 + } + + handle { + reverse_proxy app:{$APP_PORT:4100} + } +} diff --git a/selfhost/init-db.sh b/selfhost/init-db.sh new file mode 100755 index 0000000..22c7a7b --- /dev/null +++ b/selfhost/init-db.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Creates both the auth and app databases in a single Postgres instance. +# Mounted at /docker-entrypoint-initdb.d/ — runs once on first container start. + +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE DATABASE app; + GRANT ALL PRIVILEGES ON DATABASE app TO $POSTGRES_USER; +EOSQL + +echo "Created 'app' database alongside default '${POSTGRES_DB}' database." diff --git a/server.ts b/server.ts index f575334..2848703 100644 --- a/server.ts +++ b/server.ts @@ -23,6 +23,7 @@ import * as schemas from '~/api/schemas' import * as uploads from '~/api/uploads' import * as versions from '~/api/versions' import { getMirrorConfig } from '~/lib/mirror-config' +import { initOidc } from '~/lib/oidc.server' const isProd = process.env.NODE_ENV === 'production' const app = new Hono() @@ -304,5 +305,13 @@ if (isProd) { } const port = Number(process.env.PORT) || 3000 + +// Validate OIDC provider is reachable before accepting requests +await initOidc().catch((err) => { + console.error('FATAL: OIDC discovery failed — cannot start without a valid OIDC provider.') + console.error(err.message) + process.exit(1) +}) + console.log(`Server running at http://localhost:${port}`) serve({ fetch: app.fetch, port }) diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 03a231c..589f59a 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -1059,23 +1059,16 @@ export async function acceptInvitation(c: Context) { } // Verify the logged-in user's email matches the invitation. - // Email is fetched from KF Auth since we don't store it locally. - const { getKfProfile } = await import('../lib/kf-profile-cache.server.js') + // Email is fetched from auth internal API since we don't store it locally. + const { getAuthUserWithEmail } = await import('../lib/auth-internal.server.js') const accountId = c.get('accountId')! - // Fetch email from KF Auth internal API directly (profile cache doesn't include email) - const KF_AUTH_URL = process.env.KF_AUTH_URL ?? 'http://localhost:3000' - const KF_INTERNAL_API_KEY = process.env.KF_INTERNAL_API_KEY ?? '' + // Fetch email from auth internal API let userEmail: string | null = null - try { - const res = await fetch(`${KF_AUTH_URL}/api/internal/users/${accountId}`, { - headers: { Authorization: `Bearer ${KF_INTERNAL_API_KEY}` }, - }) - if (res.ok) { - const data = (await res.json()) as { email: string } - userEmail = data.email - } - } catch {} + const authUser = await getAuthUserWithEmail(accountId) + if (authUser) { + userEmail = authUser.email + } if (!userEmail || userEmail !== invitation.email) { return c.json( diff --git a/src/api/auth.server.ts b/src/api/auth.server.ts index 2fa3a38..84b529b 100644 --- a/src/api/auth.server.ts +++ b/src/api/auth.server.ts @@ -18,7 +18,8 @@ export type AuthEnv = { const publicPaths = new Set(['/api/health', '/api/query/generate-sql']) const internalToken = process.env.INTERNAL_API_TOKEN ?? 'internal-dev-token' -const kfInternalApiKey = process.env.KF_INTERNAL_API_KEY ?? '' +const authInternalApiKey = + process.env.AUTH_INTERNAL_API_KEY ?? process.env.KF_INTERNAL_API_KEY ?? '' const sessionSecret = process.env.SESSION_SECRET ?? 'dev-secret-change-me' export const authMiddleware = createMiddleware(async (c, next) => { @@ -29,9 +30,9 @@ export const authMiddleware = createMiddleware(async (c, next) => { return next() } - // KF Auth internal API key (used by /api/kf/* endpoints) + // Auth provider internal API key (used by /api/kf/* endpoints) const auth = c.req.header('authorization') - if (kfInternalApiKey && auth === `Bearer ${kfInternalApiKey}`) { + if (authInternalApiKey && auth === `Bearer ${authInternalApiKey}`) { c.set('apiKeyScope', 'admin') return next() } diff --git a/src/api/kf-auth.ts b/src/api/kf-auth.ts index a6d367a..8dd071e 100644 --- a/src/api/kf-auth.ts +++ b/src/api/kf-auth.ts @@ -9,9 +9,10 @@ import { db, schema } from '../db/client.server.js' import { buildAuthorizeUrl, exchangeCode, + extractOrgs, fetchUserInfo, - type KFOrg, -} from '../lib/kf-auth.server.js' + type OIDCOrg, +} from '../lib/oidc.server.js' import { type AuthEnv, setSessionCookie } from './auth.server.js' const STATE_COOKIE = 'kf_oauth_state' @@ -37,7 +38,7 @@ export async function login(c: Context) { setCookie(c, STATE_COOKIE, state, cookieOpts) setCookie(c, RETURN_COOKIE, returnTo, cookieOpts) - const { url, codeVerifier } = buildAuthorizeUrl(state) + const { url, codeVerifier } = await buildAuthorizeUrl(state) setCookie(c, VERIFIER_COOKIE, codeVerifier, cookieOpts) return c.redirect(url) @@ -99,7 +100,7 @@ export async function callback(c: Context) { // User account id IS the KF Auth user id (userInfo.sub). // No profile data stored locally — fetched from KF Auth on demand. const kfUserId = userInfo.sub - const kfOrgs: KFOrg[] = userInfo['https://knowledgefutures.org/orgs'] ?? [] + const kfOrgs: OIDCOrg[] = extractOrgs(userInfo) const kfPersonalOrg = kfOrgs.find((o) => o.type === 'personal') const accountId = kfUserId diff --git a/src/api/kf-summary.ts b/src/api/kf-summary.ts index b028ddf..49b3190 100644 --- a/src/api/kf-summary.ts +++ b/src/api/kf-summary.ts @@ -19,7 +19,7 @@ export async function summary(c: Context) { // Verify internal API key const authHeader = c.req.header('Authorization') - const expectedKey = process.env.KF_INTERNAL_API_KEY + const expectedKey = process.env.AUTH_INTERNAL_API_KEY ?? process.env.KF_INTERNAL_API_KEY if (!expectedKey || authHeader !== `Bearer ${expectedKey}`) { return c.json({ error: 'Unauthorized' }, 401) } diff --git a/src/lib/auth-internal.server.ts b/src/lib/auth-internal.server.ts new file mode 100644 index 0000000..99852aa --- /dev/null +++ b/src/lib/auth-internal.server.ts @@ -0,0 +1,128 @@ +/** + * Auth provider internal API client. + * + * Optional — only active when AUTH_INTERNAL_API_URL + AUTH_INTERNAL_API_KEY are set. + * When the internal API is unavailable, apps fall back to OIDC userinfo data only. + * + * Env vars (with backward-compat fallbacks): + * AUTH_INTERNAL_API_URL — base URL for internal API (fallback: KF_AUTH_INTERNAL_URL, then OIDC_ISSUER_INTERNAL_URL) + * AUTH_INTERNAL_API_KEY — shared secret for service-to-service calls (fallback: KF_INTERNAL_API_KEY) + */ + +import { OIDC_ISSUER_INTERNAL_URL } from './oidc.server.js' + +const AUTH_INTERNAL_API_URL = + process.env.AUTH_INTERNAL_API_URL ?? + process.env.KF_AUTH_INTERNAL_URL ?? + process.env.KF_AUTH_URL ?? + OIDC_ISSUER_INTERNAL_URL + +const AUTH_INTERNAL_API_KEY = + process.env.AUTH_INTERNAL_API_KEY ?? process.env.KF_INTERNAL_API_KEY ?? '' + +/** Whether the internal API is configured and available. */ +export const hasInternalApi = Boolean(AUTH_INTERNAL_API_KEY) + +// --- Types --- + +export interface AuthOrg { + id: string + name: string + slug: string + type: 'personal' | 'shared' + role: string +} + +export interface AuthProfile { + name: string + image: string | null +} + +// --- Profile cache (in-memory, 5-minute TTL) --- + +const TTL_MS = 5 * 60 * 1000 + +interface CachedProfile { + name: string + image: string | null + fetchedAt: number +} + +const profileCache = new Map() + +/** + * Fetch a user's profile (name, image) from the auth internal API. + * Uses an in-memory cache with a 5-minute TTL. + * Returns null if internal API is not configured or request fails. + */ +export async function getAuthProfile(userId: string): Promise { + if (!hasInternalApi) return null + + const cached = profileCache.get(userId) + if (cached && Date.now() - cached.fetchedAt < TTL_MS) { + return { name: cached.name, image: cached.image } + } + + try { + const res = await fetch(`${AUTH_INTERNAL_API_URL}/api/internal/users/${userId}`, { + headers: { Authorization: `Bearer ${AUTH_INTERNAL_API_KEY}` }, + }) + if (!res.ok) return null + + const data = (await res.json()) as { + id: string + name: string + email: string + image: string | null + } + const entry: CachedProfile = { name: data.name, image: data.image, fetchedAt: Date.now() } + profileCache.set(userId, entry) + return { name: entry.name, image: entry.image } + } catch (e) { + console.error(`Failed to fetch auth profile for ${userId}:`, e) + if (cached) return { name: cached.name, image: cached.image } + return null + } +} + +/** + * Fetch all orgs a user belongs to from the auth internal API. + * Returns empty array if internal API is not configured or request fails. + */ +export async function fetchAuthOrgs(userId: string): Promise { + if (!hasInternalApi) return [] + + const res = await fetch(`${AUTH_INTERNAL_API_URL}/api/internal/users/${userId}/orgs`, { + headers: { Authorization: `Bearer ${AUTH_INTERNAL_API_KEY}` }, + }) + if (!res.ok) { + console.error(`Failed to fetch orgs for ${userId}: ${res.status}`) + return [] + } + const data = (await res.json()) as { orgs?: AuthOrg[] } | AuthOrg[] + if (Array.isArray(data)) return data + return data.orgs ?? [] +} + +/** + * Fetch a single user's full profile including email from the auth internal API. + * Used for cases where email is needed but not stored locally. + * Returns null if internal API is not configured or request fails. + */ +export async function getAuthUserWithEmail( + userId: string, +): Promise<{ id: string; name: string; email: string; image: string | null } | null> { + if (!hasInternalApi) return null + + try { + const res = await fetch(`${AUTH_INTERNAL_API_URL}/api/internal/users/${userId}`, { + headers: { Authorization: `Bearer ${AUTH_INTERNAL_API_KEY}` }, + }) + if (!res.ok) return null + return (await res.json()) as { id: string; name: string; email: string; image: string | null } + } catch { + return null + } +} + +export { AUTH_INTERNAL_API_URL, AUTH_INTERNAL_API_KEY } diff --git a/src/lib/kf-auth.server.ts b/src/lib/kf-auth.server.ts index 6db31ff..642e234 100644 --- a/src/lib/kf-auth.server.ts +++ b/src/lib/kf-auth.server.ts @@ -1,124 +1,17 @@ /** - * Lightweight OIDC client for KF Auth. - * - * Two base URLs: - * KF_AUTH_URL — browser-facing (e.g. localhost:3000) - * KF_AUTH_INTERNAL_URL — server-to-server (e.g. host.docker.internal:3000 in Docker) - * Falls back to KF_AUTH_URL when not set (production). + * Legacy re-export shim. + * New code should import from './oidc.server.js' and './auth-internal.server.js' directly. */ -import crypto from 'node:crypto' - -const KF_AUTH_URL = process.env.KF_AUTH_URL ?? 'http://localhost:3000' -const KF_AUTH_INTERNAL_URL = process.env.KF_AUTH_INTERNAL_URL ?? KF_AUTH_URL -const KF_AUTH_CLIENT_ID = process.env.KF_AUTH_CLIENT_ID ?? 'kf_underlay' -const KF_AUTH_CLIENT_SECRET = process.env.KF_AUTH_CLIENT_SECRET ?? '' -const APP_URL = process.env.APP_URL ?? 'http://localhost:4100' -const REDIRECT_URI = `${APP_URL}/auth/callback` - -// BetterAuth OIDC endpoints (well-known paths) -const AUTHORIZE_PATH = '/api/auth/oauth2/authorize' -const TOKEN_PATH = '/api/auth/oauth2/token' -const USERINFO_PATH = '/api/auth/oauth2/userinfo' - -// --- PKCE helpers --- - -/** Generate a random code_verifier (43–128 chars, URL-safe). */ -export function generateCodeVerifier(): string { - return crypto.randomBytes(32).toString('base64url') -} - -/** Derive the S256 code_challenge from a code_verifier. */ -export function generateCodeChallenge(verifier: string): string { - return crypto.createHash('sha256').update(verifier).digest('base64url') -} - -/** - * Build the URL to redirect the user to for authentication. - * Uses KF_AUTH_URL (browser-facing). - * Returns the URL and the PKCE code_verifier (must be stored server-side). - */ -export function buildAuthorizeUrl(state: string): { url: string; codeVerifier: string } { - const codeVerifier = generateCodeVerifier() - const codeChallenge = generateCodeChallenge(codeVerifier) - const params = new URLSearchParams({ - client_id: KF_AUTH_CLIENT_ID, - redirect_uri: REDIRECT_URI, - response_type: 'code', - scope: 'openid profile email', - state, - code_challenge: codeChallenge, - code_challenge_method: 'S256', - }) - return { url: `${KF_AUTH_URL}${AUTHORIZE_PATH}?${params}`, codeVerifier } -} - -interface TokenResponse { - access_token: string - token_type: string - expires_in: number - id_token?: string - refresh_token?: string -} - -/** - * Exchange an authorization code for tokens. - * Uses KF_AUTH_INTERNAL_URL (server-to-server). - */ -export async function exchangeCode(code: string, codeVerifier: string): Promise { - const body = new URLSearchParams({ - grant_type: 'authorization_code', - code, - redirect_uri: REDIRECT_URI, - client_id: KF_AUTH_CLIENT_ID, - client_secret: KF_AUTH_CLIENT_SECRET, - code_verifier: codeVerifier, - }) - - const res = await fetch(`${KF_AUTH_INTERNAL_URL}${TOKEN_PATH}`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body, - }) - - if (!res.ok) { - const text = await res.text() - throw new Error(`Token exchange failed: ${res.status} ${text}`) - } - - return res.json() as Promise -} - -export interface KFOrg { - id: string - name: string - slug: string - type: 'personal' | 'shared' - role: string -} - -export interface KFUserInfo { - sub: string - name?: string - email?: string - picture?: string - 'https://knowledgefutures.org/orgs'?: KFOrg[] -} - -/** - * Fetch user info from KF Auth using an access token. - * Uses KF_AUTH_INTERNAL_URL (server-to-server). - */ -export async function fetchUserInfo(accessToken: string): Promise { - const res = await fetch(`${KF_AUTH_INTERNAL_URL}${USERINFO_PATH}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }) - - if (!res.ok) { - throw new Error(`UserInfo failed: ${res.status}`) - } - - return res.json() as Promise -} - -export { KF_AUTH_CLIENT_ID, KF_AUTH_URL, REDIRECT_URI } +export { + buildAuthorizeUrl, + exchangeCode, + fetchUserInfo, + extractOrgs, + OIDC_ISSUER_URL as KF_AUTH_URL, + OIDC_CLIENT_ID as KF_AUTH_CLIENT_ID, + REDIRECT_URI, + type OIDCOrg as KFOrg, + type OIDCUserInfo as KFUserInfo, + initOidc, +} from './oidc.server.js' diff --git a/src/lib/kf-orgs.server.ts b/src/lib/kf-orgs.server.ts index 6eadd64..9498c9e 100644 --- a/src/lib/kf-orgs.server.ts +++ b/src/lib/kf-orgs.server.ts @@ -1,28 +1,6 @@ /** - * Fetch a user's KF org memberships on demand from KF Auth's internal API. - * Used for the org-creation dropdown (availableKfOrgs). + * Legacy re-export shim. + * New code should import from './auth-internal.server.js' directly. */ -import type { KFOrg } from './kf-auth.server.js' - -const KF_AUTH_URL = process.env.KF_AUTH_URL ?? 'http://localhost:3000' -const KF_AUTH_INTERNAL_URL = process.env.KF_AUTH_INTERNAL_URL ?? KF_AUTH_URL -const KF_INTERNAL_API_KEY = process.env.KF_INTERNAL_API_KEY ?? '' - -/** - * Fetch all KF orgs the given user belongs to. - * Calls KF Auth internal API with API key auth. - */ -export async function fetchKfOrgs(kfUserId: string): Promise { - const res = await fetch(`${KF_AUTH_INTERNAL_URL}/api/internal/users/${kfUserId}/orgs`, { - headers: { Authorization: `Bearer ${KF_INTERNAL_API_KEY}` }, - }) - if (!res.ok) { - console.error(`Failed to fetch KF orgs for ${kfUserId}: ${res.status}`) - return [] - } - const data = (await res.json()) as { orgs?: KFOrg[] } | KFOrg[] - // Handle both { orgs: [...] } and bare array shapes - if (Array.isArray(data)) return data - return data.orgs ?? [] -} +export { fetchAuthOrgs as fetchKfOrgs, type AuthOrg as KFOrg } from './auth-internal.server.js' diff --git a/src/lib/kf-profile-cache.server.ts b/src/lib/kf-profile-cache.server.ts index 1bb5dc8..c4e8041 100644 --- a/src/lib/kf-profile-cache.server.ts +++ b/src/lib/kf-profile-cache.server.ts @@ -1,57 +1,9 @@ /** - * In-memory cache for KF Auth user profile data. - * Fetches name + image from KF Auth's internal API with a 5-minute TTL. + * Legacy re-export shim. + * New code should import from './auth-internal.server.js' directly. */ -const KF_AUTH_URL = process.env.KF_AUTH_URL ?? 'http://localhost:3000' -const KF_AUTH_INTERNAL_URL = process.env.KF_AUTH_INTERNAL_URL ?? KF_AUTH_URL -const KF_INTERNAL_API_KEY = process.env.KF_INTERNAL_API_KEY ?? '' - -const TTL_MS = 5 * 60 * 1000 // 5 minutes - -interface CachedProfile { - name: string - image: string | null - fetchedAt: number -} - -const cache = new Map() - -export interface KFProfile { - name: string - image: string | null -} - -/** - * Get a KF Auth user's profile (name, image). - * Uses an in-memory cache with a 5-minute TTL. - * On miss/expiry, calls KF Auth internal API. - */ -export async function getKfProfile(userId: string): Promise { - const cached = cache.get(userId) - if (cached && Date.now() - cached.fetchedAt < TTL_MS) { - return { name: cached.name, image: cached.image } - } - - try { - const res = await fetch(`${KF_AUTH_INTERNAL_URL}/api/internal/users/${userId}`, { - headers: { Authorization: `Bearer ${KF_INTERNAL_API_KEY}` }, - }) - if (!res.ok) return null - - const data = (await res.json()) as { - id: string - name: string - email: string - image: string | null - } - const entry: CachedProfile = { name: data.name, image: data.image, fetchedAt: Date.now() } - cache.set(userId, entry) - return { name: entry.name, image: entry.image } - } catch (e) { - console.error(`Failed to fetch KF profile for ${userId}:`, e) - // Return stale cache on error if available - if (cached) return { name: cached.name, image: cached.image } - return null - } -} +export { + getAuthProfile as getKfProfile, + type AuthProfile as KFProfile, +} from './auth-internal.server.js' diff --git a/src/lib/oidc.server.ts b/src/lib/oidc.server.ts new file mode 100644 index 0000000..34c0c17 --- /dev/null +++ b/src/lib/oidc.server.ts @@ -0,0 +1,230 @@ +/** + * Generic OIDC client with auto-discovery. + * + * Reads endpoints from the provider's .well-known/openid-configuration. + * Works with any standards-compliant OIDC provider (KF Auth, Keycloak, Auth0, etc.). + * + * Env vars (new canonical names with backward-compat fallbacks): + * OIDC_ISSUER_URL — browser-facing issuer URL (fallback: KF_AUTH_URL) + * OIDC_ISSUER_INTERNAL_URL — server-to-server URL for Docker (fallback: KF_AUTH_INTERNAL_URL, then OIDC_ISSUER_URL) + * OIDC_CLIENT_ID — OAuth client ID (fallback: KF_AUTH_CLIENT_ID) + * OIDC_CLIENT_SECRET — OAuth client secret (fallback: KF_AUTH_CLIENT_SECRET) + * OIDC_ORGS_CLAIM — custom claim key for org memberships (default: https://knowledgefutures.org/orgs) + */ + +import crypto from 'node:crypto' + +// --- Config (with backward-compat fallbacks) --- + +const OIDC_ISSUER_URL = + process.env.OIDC_ISSUER_URL ?? process.env.KF_AUTH_URL ?? 'http://localhost:3000' + +const OIDC_ISSUER_INTERNAL_URL = + process.env.OIDC_ISSUER_INTERNAL_URL ?? process.env.KF_AUTH_INTERNAL_URL ?? OIDC_ISSUER_URL + +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID ?? process.env.KF_AUTH_CLIENT_ID ?? 'kf_underlay' + +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET ?? process.env.KF_AUTH_CLIENT_SECRET ?? '' + +const OIDC_ORGS_CLAIM = process.env.OIDC_ORGS_CLAIM ?? 'https://knowledgefutures.org/orgs' + +const APP_URL = process.env.APP_URL ?? 'http://localhost:4100' +const REDIRECT_URI = `${APP_URL}/auth/callback` + +// --- OIDC Discovery --- + +interface OIDCDiscovery { + issuer: string + authorization_endpoint: string + token_endpoint: string + userinfo_endpoint: string + jwks_uri?: string +} + +let discoveryCache: OIDCDiscovery | null = null +let discoveryPromise: Promise | null = null + +/** + * Fetch and cache the OIDC discovery document. + * Uses the internal URL for server-to-server fetch. + * Throws if discovery fails — app should not start without valid OIDC config. + */ +async function discover(): Promise { + if (discoveryCache) return discoveryCache + if (discoveryPromise) return discoveryPromise + + discoveryPromise = (async () => { + const url = `${OIDC_ISSUER_INTERNAL_URL}/.well-known/openid-configuration` + const res = await fetch(url) + if (!res.ok) { + throw new Error( + `OIDC discovery failed: ${res.status} from ${url}. ` + + `Ensure OIDC_ISSUER_URL or KF_AUTH_URL points to a valid OIDC provider.`, + ) + } + const config = (await res.json()) as OIDCDiscovery + discoveryCache = config + return config + })() + + return discoveryPromise +} + +/** + * Initialize OIDC — call at app startup to fail fast if provider is unreachable. + */ +export async function initOidc(): Promise { + await discover() +} + +/** + * Rewrite a discovered endpoint URL to use the internal host. + * Discovery may return URLs with the public host (BETTER_AUTH_URL), + * but server-to-server calls must use OIDC_ISSUER_INTERNAL_URL. + */ +function internalEndpoint(discoveredUrl: string): string { + const url = new URL(discoveredUrl) + const base = new URL(OIDC_ISSUER_INTERNAL_URL) + url.protocol = base.protocol + url.host = base.host + return url.toString() +} + +// --- PKCE helpers --- + +/** Generate a random code_verifier (43–128 chars, URL-safe). */ +export function generateCodeVerifier(): string { + return crypto.randomBytes(32).toString('base64url') +} + +/** Derive the S256 code_challenge from a code_verifier. */ +export function generateCodeChallenge(verifier: string): string { + return crypto.createHash('sha256').update(verifier).digest('base64url') +} + +// --- OIDC Flows --- + +/** + * Build the URL to redirect the user to for authentication. + * Uses the discovered authorization_endpoint. + * Returns the URL and the PKCE code_verifier (must be stored server-side). + */ +export async function buildAuthorizeUrl( + state: string, +): Promise<{ url: string; codeVerifier: string }> { + const config = await discover() + const codeVerifier = generateCodeVerifier() + const codeChallenge = generateCodeChallenge(codeVerifier) + + // Use the browser-facing issuer URL for authorize (user's browser navigates here) + // Discovery may return an internal URL, so construct from OIDC_ISSUER_URL + path + const authorizeUrl = new URL(config.authorization_endpoint) + // Replace host with the browser-facing URL if discovery returned an internal one + const browserBase = new URL(OIDC_ISSUER_URL) + authorizeUrl.protocol = browserBase.protocol + authorizeUrl.host = browserBase.host + + const params = new URLSearchParams({ + client_id: OIDC_CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: 'code', + scope: 'openid profile email', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }) + + return { url: `${authorizeUrl.toString()}?${params}`, codeVerifier } +} + +export interface TokenResponse { + access_token: string + token_type: string + expires_in: number + id_token?: string + refresh_token?: string +} + +/** + * Exchange an authorization code for tokens. + * Uses the discovered token_endpoint (server-to-server). + */ +export async function exchangeCode(code: string, codeVerifier: string): Promise { + const config = await discover() + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: REDIRECT_URI, + client_id: OIDC_CLIENT_ID, + client_secret: OIDC_CLIENT_SECRET, + code_verifier: codeVerifier, + }) + + const res = await fetch(internalEndpoint(config.token_endpoint), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }) + + if (!res.ok) { + const text = await res.text() + throw new Error(`Token exchange failed: ${res.status} ${text}`) + } + + return res.json() as Promise +} + +export interface OIDCOrg { + id: string + name: string + slug: string + type: 'personal' | 'shared' + role: string +} + +export interface OIDCUserInfo { + sub: string + name?: string + email?: string + picture?: string + [key: string]: unknown +} + +/** + * Fetch user info from the OIDC provider using an access token. + * Uses the discovered userinfo_endpoint (server-to-server). + */ +export async function fetchUserInfo(accessToken: string): Promise { + const config = await discover() + const res = await fetch(internalEndpoint(config.userinfo_endpoint), { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + if (!res.ok) { + throw new Error(`UserInfo failed: ${res.status}`) + } + + return res.json() as Promise +} + +/** + * Extract org memberships from the userinfo response. + * Uses the configurable OIDC_ORGS_CLAIM key. + */ +export function extractOrgs(userInfo: OIDCUserInfo): OIDCOrg[] { + const orgs = userInfo[OIDC_ORGS_CLAIM] + if (Array.isArray(orgs)) return orgs as OIDCOrg[] + return [] +} + +// --- Exports --- + +export { + OIDC_ISSUER_URL, + OIDC_ISSUER_INTERNAL_URL, + OIDC_CLIENT_ID, + OIDC_CLIENT_SECRET, + OIDC_ORGS_CLAIM, + APP_URL, + REDIRECT_URI, +} diff --git a/src/loaders.server.ts b/src/loaders.server.ts index 0b50ad8..13f2541 100644 --- a/src/loaders.server.ts +++ b/src/loaders.server.ts @@ -91,7 +91,12 @@ const loaders: Record = { }, '/logout': async () => { - return { data: { kfAuthUrl: process.env.KF_AUTH_URL ?? 'http://localhost:3000' } } + return { + data: { + kfAuthUrl: + process.env.OIDC_ISSUER_URL ?? process.env.KF_AUTH_URL ?? 'http://localhost:3000', + }, + } }, '/forgot-password': async () => { @@ -386,7 +391,8 @@ const loaders: Record = { }, } -const kfAccountUrl = process.env.KF_ACCOUNT_URL ?? 'http://localhost:3001' +const kfAccountUrl = + process.env.OIDC_ACCOUNT_URL ?? process.env.KF_ACCOUNT_URL ?? 'http://localhost:3001' export async function runLoaders( matchedRoutes: { path: string; params: Record }[],