diff --git a/.env.prod.enc b/.env.prod.enc index 2454674..c0e320c 100644 --- a/.env.prod.enc +++ b/.env.prod.enc @@ -1,38 +1,38 @@ -#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 +#ENC[AES256_GCM,data:O+nchjkaQ9wK,iv:c0fbl/PM6wnYbj+HDT4d8mOuHCq9VngfpJsxQqYrmt4=,tag:mGQWYUJh0wZFcUcWS57f+Q==,type:comment] +DATABASE_URL=ENC[AES256_GCM,data:nC9pqB4zDScCNkW/QdgTcvF19uhmuZm8WtrmQ0AtGO2vHaevdDx8EUAAuLF5hQWmGfW6LFQ=,iv:xo5W+yttEazn8AckWFnc9IoALdQ8uRM6TS/GFLrVNiQ=,tag:sjmfGbyejGCZDGxOxz+oBg==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:AfAiynfkB7KFNpcs13IwUULNR4uJNvSA1sRcimBdXxe+k7rcozMk83fD/lMgNLclv8wv2qQ3EIKs/+Ar6UlB3g==,iv:CUtywxHxGTANbpPsoebYLWoWaUMnuuHThxxQdUnEaEY=,tag:Kpm2rU+4BmKCy6h05bPq5A==,type:str] +PORT=ENC[AES256_GCM,data:EVfPrw==,iv:YI4epRUkR0+1ejKcBIja+CrPu/HuCjgeg8mN4djU4+0=,tag:JVlgeXZ4x6gSOWAdjO8/lQ==,type:str] +#ENC[AES256_GCM,data:+sXw,iv:jL1yf95M/jAyYcPcJagLa7gAURDw3HdhPQ8lAmY5AN4=,tag:5D9nLPujVjks9fCqieYbZg==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:4vG/sXkvLEKbq88Sd9/K,iv:T7tyAHgcMeFP7BvW9Xbq5nRUtw4imj4+qZLWDe6QhuM=,tag:P3dlKD6FMP0MBQTUz1HwIQ==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:mbSEK3wkiUuxg3ETQtVIg8fLfODrc0QFjl1WX0x1YFHu9md1T5/Yn7ibq1QaOI1db3okrwnBTBn6tEmgU5QSWjg=,iv:4j9Q1fquwwcCeCC6kyOf7ZkfRDxanBrDnC7CVVlavrU=,tag:Tt8fsgbzn/DBaoA9Vn/IYw==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:A+prb2tXs2GMeIDYRY3+bcSdn12JoSZ77QHeXQIXKYI=,iv:7Nix4bEZh/uSRNJDiDE0oiYJFZ/tGTBSi0Nn7+ZL6NU=,tag:E1RpsL61fnXaC5C02KZbZQ==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:cyRoknQkJi5xabRdE1NovpibJJFpIb0JQsX00Jrd/2m7kRrbu056KqWdW+QXcVNdokXunbz1wTP+3dyP8DgwhQ==,iv:wXh6W76thveWGu0Iq2Fe9cxJJrMRzrpg/4cGWGC/8ro=,tag:kXn7sCPU04LYuOSWz78+rg==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:FHIDA3WRC7/t+NHQgGqULG6a5xI+A+e4pPeMz5ZvbMs=,iv:1Vk0BSWIqZGhA5og0nVGxZDFI4ylEm+XuzxbGltwpLA=,tag:kAUpABomnLzd3VKWR7/R4g==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:bB4adE2h8VyRDzv6kXpBa0IoCiRY3aMWYAMtHeWYxjZbyMuvyeGfeoX4symD05H4BCm1oDs=,iv:hVwnSPZjjIDbOnaUN8ggbWuQ7PqrxzzpSiaTospY5Qc=,tag:HwoZBW1cehaPDcHwzVKAXw==,type:str] +ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:TzSzgMI=,iv:aue3Wv7kPd7d1U745YalEjV//rTq0BtzYXzPOJPfync=,tag:sgbJapnwMSaNaxlXXmPBEA==,type:str] +#ENC[AES256_GCM,data:bm0c2TWxwFykuTIMW/2YLesr,iv:H1vQLauzzMMGndOsh2AHIhNAlH2VV+FlN0l7bOjv5SY=,tag:8hvh877Rl0zh1FeL0kDxDQ==,type:comment] +UNDERLAY_UPSTREAM_API_KEY=ENC[AES256_GCM,data:gHG8XChBFKsZwySUsIsmbwmRqPCOj5dJ4Rj0mu1i1Iuz0Cw=,iv:WCPG3HIUrraUdaLUZ/EotGbpcXTtkncWYA4KBck/QqU=,tag:CknwtSknBCNmQIrzdjQaVw==,type:str] +DEPLOY_HOST=ENC[AES256_GCM,data:LAyqgvv86wbjaZfI1w==,iv:Bo647hzSqyvJDQ+r1zWjWEWY5PQ1yXd396rIaZzLC+g=,tag:qCiRHP9gUpn3bf4AWYs/Og==,type:str] +#ENC[AES256_GCM,data:YQpq1XCNqmk=,iv:mhg6Kldq246GC5Qh0peXp7xOOa7LjCpaGauBkzIrP3A=,tag:rmMCKHxrEX3SByAnBJh+UA==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:Wi31C6exKWScoJpohcQ9FVnNIB0RwPczf6XNt+5skQaO,iv:ZFRHNXVBuNBJkrLoePKPpQB+awzWvKPrujcuF9uc0lY=,tag:hAikIZJTvfRriZ3JecVaew==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:8tjJVzF7aH796rw=,iv:od+vi7jOtMjBe8oFZqtuODNeQS7QISxMJMUd/X9yrJA=,tag:upZ0nyb1pNTYEsy1lNEOjQ==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:51ggvZnGCb568LUFmXvp15pRo52yKUwxc8VVoaM2AYUdikxsinkUvL1FsESfO9j4a6cTaxMIZUkUhQ6nNNQAXQ==,iv:8wVy9lJZhe+3u9rPf6lcDIS1JnkyNxUGt6bq17TIhyY=,tag:gtBQie5BFm2sgPlbYpBvCQ==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:Eqt1KQyxlZ4JEMN1RBdcwzogqQ6eyUEljj6FFIULjQ1wV5tE1ZgxrOKp4inyx1Mfdz6kpKhf8nH/vX3S4P9xJg==,iv:L4VBSArsYDoPmQHDi7Mo4qjItJlV8AdfTaP097y4laQ=,tag:oEuGjoPRE5BUNxmO+lpjqA==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:joXmwqCkLkvlF/2tDN0i03R1vzcseoskA+eznd9PGzKiwnyI,iv:ibZJXcxCKIm185jIg42p8sk0q2Y0GvCJi4FKfrICYK4=,tag:i6ny1ZdCRsaqNil+CX76Lg==,type:str] +APP_URL=ENC[AES256_GCM,data:0Cc7cFdS5esZGYQXPISblLRNuvIvBe1r,iv:nmjOnMgLcojCOElb7ZT97NZO4YAOWoQB2fxOzwm8Vgw=,tag:IqFinV59tu8kaTfR0Tj5DA==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOVmpOUHpjV1AvbDY0eVpZ\nV3htMzB6SU43eHRtNDlmUW92NjR0Yk5CWVdFCmxzRDNZQXFLY3hMVXQ4WkFwNWtD\nYmx0STY5QjgzeUlYL0xpcnlKRmk2UXMKLS0tIGpuQ29QaVdPSFM1bktBb3VnbHI0\naVBSeGViSWRFcHE5VHF3UVFoK2E4WGMKo0o66lKkVKBrw5QAvZRjplj4ySIdn021\nS494v7X+emlBKfDCD4XHnSfyktgitSu5KTDVaeViix+EeiXLfsDeAQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHZGI0NnpoY3VrelFrZjZw\nWStqK2VIU0NPMGs3Mk8ySU5uQS9saVFmQ0JFCnkvZjczWkQvKzdFdG5lTThyNTJr\nbGd0dzVsZlp5U0M4ZG92TUtteXFSR00KLS0tIGY0OGJteHBjN1A3MkV0aGpUS3M3\nRXZYbng1MVVITXJEQ0lpY1lJdllLa2MKHzX1gXYmdiMyid18xDUtdk8jej7DYG4W\nwbum7nH+/KmJbCS0NZDIpPkAUmDth1AZUUnFujMqtkLZ7YNChWmIAg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDSkl2eVJXV3FGMkZ2S1c4\nOGlneUdERTFCTkJpTE92WVBXWW5qR2RLTkRRCmFxTkVlMVJ3RDk3N0VQb2ZuV0Qy\nYmlvcDdNei9mdUQxcnhjdXgxYWtBem8KLS0tIFJ0WkRDM25nL04wNFpCOTVMZFpN\ncWNlTTdFNXEvQzE4b1VaY3IyUVIyTmcKtk/miVOnklKdwunwSK3teaU2zVQzFOjW\n0e5FEBfMfs4Th840y62f594v6z8/rRnYeH6vYEk4Tj0Rjr2ISIqXGQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQUNGd0daLzFsYVFiMGhk\nY1dJMHZQcjBqTTVpM29XS1NnbmdEMnUzNUZvCnh0bDFIcWtWWGdvRGhOV0RTdkY2\nTkVPTENGeUdjb0d2TjRxVXVxd04wbVUKLS0tIGdWRGFZTU5tWU1nbEd5Zkl5anVr\naUhuWTRhRURCZDYySkczSUJaRFozUkkKzkr5HK5nmEsLcCLmSEILCAlE/773tboY\nS8ZuKoHds2NmWrZGDbodwVD7kYQ9l89rQa4WTjrgFwprOayp7e/P1g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwbnFuZ2JKbFpZalRWeUQ0\nbkJKZk1IVkovOW5jLzdUSzNkRmVQeHpNeGs0ClcwcnZFWGpiQWpIT0N6bnRkQXlx\nWDVPUUJ6cDRyckNxL3J0UXAxVUdnL1kKLS0tIHFVN0pzVXVUUUREOW0ycndoUVV5\nWUJNblhoa2ZOU0VoTGFoZzZGRlZwVW8K30qpBsvJHwWeLiiNTb0Yn41mkr2GZo58\nlSn9w97P9+vsK98izcZhSEp5CSJDQEH67ltCcR1LUuGNc2ILRphX6g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYUVN1OFRMQlFuT05IbnVx\nZitFblpoenNEQ1lBSjUrbWhqVXN4TVU5SXhFClh1WnptaUNiQjVNMXdRVkVvS09m\nbkoxRkI4MHNGajRxNnpwZ1FCV3ZXb28KLS0tIHBnaTBXUUcyQlB5WWpBaDY0MVA5\na0Jjd2xpMDhOeG9EVzUxRUl5KythOUEKENX9NZo03ACFxom+yNFyf3Ywzvxc92w9\nwSodCz2+yJiTbFlInSj8bmNy2tsTI5KKfTEl2Qh58uTQXDTMCotWRg==\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_lastmodified=2026-06-12T03:40:05Z +sops_mac=ENC[AES256_GCM,data:z6RsG4cEMF0LmGGRCYgrMTYn0plJMbSitgTqOKY5xumVOagyWpzH1RF8rTknRj/91QdecWmso0fISkHnDyFO/vPYixKY54eaYMPVTlLCrUH3Ywqyd00z5KxKIQqQWtQR/z/PFP+K65Wl4i/NmP2Mx5WCGIjszT2T+SwkfxZcjJQ=,iv:c16LMsP/cf09kzz9e6WFZ3PCA3DHlGfqPze0ojqMZew=,tag:5aR1cM6kOR8X0ebv2ebHXA==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/README.md b/README.md index 4d7a94f..47de1d6 100644 --- a/README.md +++ b/README.md @@ -207,11 +207,11 @@ tools/ The protocol and the platform are documented together: -| Resource | URL | Purpose | -| ------------- | -------------------------------------------------------------- | ------------------------------------------------------------------- | -| Protocol spec | [/protocol](https://underlay.org/protocol) | Full protocol: data model, hashing, push, pull, provenance, privacy | -| User docs | [/docs](https://underlay.org/docs) | Concepts, integration guide, API reference, quickstart | -| llms.txt | [/llms.txt](https://underlay.org/llms.txt) | Machine-readable API docs for LLMs and bots | +| Resource | URL | Purpose | +| ------------- | ------------------------------------------ | ------------------------------------------------------------------- | +| Protocol spec | [/protocol](https://underlay.org/protocol) | Full protocol: data model, hashing, push, pull, provenance, privacy | +| User docs | [/docs](https://underlay.org/docs) | Concepts, integration guide, API reference, quickstart | +| llms.txt | [/llms.txt](https://underlay.org/llms.txt) | Machine-readable API docs for LLMs and bots | ### Key API endpoints @@ -329,19 +329,28 @@ Required GitHub secrets: `SSH_PRIVATE_KEY`, `SSH_USER`, `GHCR_USER`, `GHCR_TOKEN | ----------------------------- | ---------------------------------------------- | | `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 | +| `docker-compose.withauth.yml` | Self-hosted: app + KF Auth + MinIO + Caddy | ### 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 +DOMAIN=https://my-instance.com docker compose -f docker-compose.withauth.yml up -d ``` -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. +This starts Postgres, KF Auth (auth + account), MinIO (S3-compatible storage), the Underlay app, and Caddy with automatic TLS. On first boot, an init container auto-generates all secrets (session keys, OAuth client credentials, S3 credentials). -Supporting files live in `selfhost/` (Caddyfile, Postgres init script). +Optional configuration (via environment variables or `.env` file): + +- `SMTP_*` vars for email delivery (password resets, invitations) +- `GITHUB_CLIENT_ID`/`GITHUB_CLIENT_SECRET` for GitHub login +- `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` for Google login +- `ORCID_CLIENT_ID`/`ORCID_CLIENT_SECRET` for ORCID login + +To use external S3 (AWS, Cloudflare R2, etc.) instead of bundled MinIO, remove the `minio` and `minio-init` services and set `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` in the app environment. + +Supporting files live in `selfhost/` (Caddyfile, Postgres init script). See [/docs/self-host](https://underlay.org/docs/self-host) for full details. ## Environment Variables diff --git a/docker-compose.withauth.yml b/docker-compose.withauth.yml index 6a848c5..701dd51 100644 --- a/docker-compose.withauth.yml +++ b/docker-compose.withauth.yml @@ -1,10 +1,15 @@ # docker-compose.withauth.yml — Self-hosted Underlay with bundled auth. # -# Runs: Underlay app + KF Auth (auth + account) + Postgres + Caddy +# Runs: Underlay app + KF Auth (auth + account) + Postgres + MinIO (S3) + 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. +# +# To use external S3 instead of bundled MinIO, remove the minio service and set +# S3_BUCKET, S3_REGION, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY in the app +# environment block (or pass them as env vars that the init container writes +# into .env.app). name: underlay-withauth @@ -22,8 +27,11 @@ services: fi apk add --no-cache openssl AUTH_SECRET=$$(openssl rand -hex 32) + SESSION_SECRET=$$(openssl rand -hex 32) CLIENT_SECRET=$$(openssl rand -hex 32) INTERNAL_KEY=$$(openssl rand -hex 32) + MINIO_ACCESS=$$(openssl rand -hex 16) + MINIO_SECRET=$$(openssl rand -hex 32) printf '%s\n' \ "BETTER_AUTH_SECRET=$$AUTH_SECRET" \ "BETTER_AUTH_URL=$${DOMAIN:-http://localhost}/auth" \ @@ -49,19 +57,30 @@ services: "- client_id: underlay" \ " client_secret: $$CLIENT_SECRET" \ " redirect_uris:" \ - " - $${DOMAIN:-http://localhost}/auth/callback" \ + " - $${DOMAIN:-http://localhost}/api/auth/oauth2/callback/kf-auth" \ " skip_consent: true" \ " display_name: \"Underlay\"" \ " allow_sign_up: true" \ > /config/apps.withauth.yaml printf '%s\n' \ + "SESSION_SECRET=$$SESSION_SECRET" \ "OIDC_ISSUER_URL=$${DOMAIN:-http://localhost}/auth" \ "OIDC_ISSUER_INTERNAL_URL=http://auth:3000" \ + "OIDC_ACCOUNT_URL=$${DOMAIN:-http://localhost}/account" \ "OIDC_CLIENT_ID=underlay" \ "OIDC_CLIENT_SECRET=$$CLIENT_SECRET" \ "AUTH_INTERNAL_API_KEY=$$INTERNAL_KEY" \ "DATABASE_URL=postgres://kfauth:kfauth@postgres:5432/app" \ + "S3_BUCKET=underlay" \ + "S3_REGION=us-east-1" \ + "S3_ENDPOINT=http://minio:9000" \ + "S3_ACCESS_KEY=$$MINIO_ACCESS" \ + "S3_SECRET_KEY=$$MINIO_SECRET" \ > /config/.env.app + printf '%s\n' \ + "MINIO_ROOT_USER=$$MINIO_ACCESS" \ + "MINIO_ROOT_PASSWORD=$$MINIO_SECRET" \ + > /config/.env.minio echo "Init complete." volumes: - withauth-config:/config @@ -96,6 +115,47 @@ services: timeout: 5s retries: 10 + # --- MinIO (S3-compatible object storage) --- + # Remove this service if using external S3/R2 — set S3_* vars in app environment instead. + minio: + image: minio/minio:latest + depends_on: + withauth-init: + condition: service_completed_successfully + volumes: + - minio-data:/data + - withauth-config:/config:ro + entrypoint: /bin/sh + command: + - -c + - | + set -a && . /config/.env.minio && set +a + exec minio server /data --console-address ":9001" + healthcheck: + test: ['CMD', 'mc', 'ready', 'local'] + interval: 5s + timeout: 5s + retries: 10 + + # Create the default bucket on first run + minio-init: + image: minio/mc:latest + depends_on: + minio: + condition: service_healthy + withauth-init: + condition: service_completed_successfully + volumes: + - withauth-config:/config:ro + entrypoint: /bin/sh + command: + - -c + - | + set -a && . /config/.env.minio && set +a + mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" + mc mb --ignore-existing local/underlay + echo "Bucket ready." + # --- Auth server (kf-auth) --- auth: image: ghcr.io/knowledgefutures/kf-auth:latest @@ -136,13 +196,15 @@ services: condition: service_completed_successfully auth: condition: service_started + minio-init: + condition: service_completed_successfully 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" + command: sh -c "set -a && . /config/.env.app && set +a && node --import tsx/esm server.ts" # --- Caddy reverse proxy --- caddy: @@ -164,6 +226,7 @@ services: volumes: pgdata: + minio-data: withauth-config: caddy-data: caddy-config: diff --git a/public/logoLight.svg b/public/logoLight.svg new file mode 100644 index 0000000..6c64b24 --- /dev/null +++ b/public/logoLight.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/server.ts b/server.ts index 6e9760a..cf25029 100644 --- a/server.ts +++ b/server.ts @@ -98,9 +98,9 @@ app.get('/agent/:token', agentHandlers.agentPage) app.use('/api/*', authMiddleware) app.use('/api/*', rateLimitMiddleware) -// --- Mirror mode guard for admin routes --- -// Operator-only: admin-scoped API key, or a session user listed in MIRROR_ADMIN_EMAILS -app.use('/api/admin/*', async (c, next) => { +// --- Admin route guards --- +// Mirror admin: requires mirror mode + admin API key or MIRROR_ADMIN_EMAILS +app.use('/api/admin/mirror/*', async (c, next) => { const config = getMirrorConfig() if (!config.enabled) { return c.json({ error: 'Not found', statusCode: 404 }, 404) @@ -122,6 +122,18 @@ app.use('/api/admin/*', async (c, next) => { ) }) +// Steward admin: requires kfRole === 'admin' +// Steward admin: requires kfRole === 'admin' +app.use('/api/admin/explore-*', async (c, next) => { + const userId = c.get('userId') + if (!userId) return c.json({ error: 'Unauthorized', statusCode: 401 }, 401) + const sessionUser = await getSessionUser(c.req.raw) + if (sessionUser?.kfRole !== 'admin') { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } + return next() +}) + // --- ARK resolution middleware --- app.use('/ark\\:*', arkMiddleware) @@ -145,31 +157,62 @@ if (!isProd) { // --- Better-auth handler (OIDC login, sessions, API keys) --- app.on(['GET', 'POST'], '/api/auth/*', async (c) => { - return auth.handler(c.req.raw) + const url = new URL(c.req.url) + const isCallback = url.pathname.includes('/callback/') + if (isCallback) { + console.log('[auth callback] incoming:', { + method: c.req.method, + path: url.pathname, + hasCode: url.searchParams.has('code'), + hasState: url.searchParams.has('state'), + hasError: url.searchParams.has('error'), + error: url.searchParams.get('error'), + rawUrl: c.req.url, + cookieHeader: c.req.header('cookie')?.substring(0, 200), + }) + } + const res = await auth.handler(c.req.raw) + if (isCallback) { + console.log('[auth callback] response:', { + status: res.status, + location: res.headers.get('location'), + setCookies: res.headers.getSetCookie?.()?.map((s: string) => s.substring(0, 80)), + }) + } + return res }) // /login redirect — fall through to React route only when there's an error to display app.get('/login', async (c, next) => { const url = new URL(c.req.url) + const appOrigin = new URL(process.env.APP_URL ?? 'http://localhost:4100').origin if (!url.searchParams.has('error')) { - const signInUrl = new URL('/api/auth/sign-in/oauth2', url.origin) + const signInUrl = new URL('/api/auth/sign-in/oauth2', appOrigin) + const headers = new Headers({ + 'Content-Type': 'application/json', + Cookie: c.req.header('cookie') ?? '', + Origin: appOrigin, + }) + const xff = c.req.header('x-forwarded-for') + if (xff) headers.set('X-Forwarded-For', xff) const authRes = await auth.handler( new Request(signInUrl, { method: 'POST', - headers: new Headers({ - 'Content-Type': 'application/json', - Cookie: c.req.header('cookie') ?? '', - Origin: url.origin, - }), + headers, body: JSON.stringify({ providerId: 'kf-auth', callbackURL: '/dashboard' }), }), ) const body = await authRes.json() + console.log('[login] auth sign-in response:', { status: authRes.status, body }) if (body.url) { const redirect = new Response(null, { status: 302, headers: { Location: body.url } }) - for (const [key, value] of authRes.headers.entries()) { - if (key.toLowerCase() === 'set-cookie') redirect.headers.append(key, value) + for (const cookie of authRes.headers.getSetCookie()) { + redirect.headers.append('set-cookie', cookie) } + console.log( + '[login] forwarding cookies:', + authRes.headers.getSetCookie().map((s: string) => s.substring(0, 80)), + ) return redirect } } @@ -204,6 +247,68 @@ app.get('/api/admin/mirror/sync/progress', admin.mirrorSyncProgress) app.get('/api/admin/mirror/sync/active', admin.mirrorSyncActive) app.get('/api/admin/mirror/history', admin.mirrorHistory) +// Steward-only: explore featured tags +app.get('/api/admin/explore-tags', async (c) => { + const { db, schema } = await import('~/db/client.server') + const { eq } = await import('drizzle-orm') + const [row] = await db + .select({ value: schema.instanceSettings.value }) + .from(schema.instanceSettings) + .where(eq(schema.instanceSettings.key, 'explore_featured_tags')) + .limit(1) + return c.json({ tags: Array.isArray(row?.value) ? row.value : [] }) +}) + +app.put('/api/admin/explore-tags', async (c) => { + const body = await c.req.json<{ tags: string[] }>() + if (!Array.isArray(body.tags) || !body.tags.every((t: unknown) => typeof t === 'string')) { + return c.json({ error: 'tags must be an array of strings', statusCode: 422 }, 422) + } + const { db, schema } = await import('~/db/client.server') + await db + .insert(schema.instanceSettings) + .values({ key: 'explore_featured_tags', value: body.tags, updatedAt: new Date() }) + .onConflictDoUpdate({ + target: schema.instanceSettings.key, + set: { value: body.tags, updatedAt: new Date() }, + }) + return c.json({ ok: true, tags: body.tags }) +}) + +// Steward-only: explore featured collections +app.get('/api/admin/explore-collections', async (c) => { + const { db, schema } = await import('~/db/client.server') + const { eq } = await import('drizzle-orm') + const [row] = await db + .select({ value: schema.instanceSettings.value }) + .from(schema.instanceSettings) + .where(eq(schema.instanceSettings.key, 'explore_featured_collections')) + .limit(1) + return c.json({ collections: Array.isArray(row?.value) ? row.value : [] }) +}) + +app.put('/api/admin/explore-collections', async (c) => { + const body = await c.req.json<{ collections: string[] }>() + if ( + !Array.isArray(body.collections) || + !body.collections.every((s: unknown) => typeof s === 'string') + ) { + return c.json( + { error: 'collections must be an array of "owner/slug" strings', statusCode: 422 }, + 422, + ) + } + const { db, schema } = await import('~/db/client.server') + await db + .insert(schema.instanceSettings) + .values({ key: 'explore_featured_collections', value: body.collections, updatedAt: new Date() }) + .onConflictDoUpdate({ + target: schema.instanceSettings.key, + set: { value: body.collections, updatedAt: new Date() }, + }) + return c.json({ ok: true, collections: body.collections }) +}) + // Query app.get('/api/query/sqlite/:owner/:slug/:version', query.sqlite) app.get('/api/query/ddl/:owner/:slug/:version', query.ddl) diff --git a/src/App.tsx b/src/App.tsx index 8c21d97..edfd22b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import type { LoaderFunctionArgs, RouteObject } from 'react-router' import Root from '~/components/Root' +import { fetchBase } from '~/lib/fetch-base' import { buildDataRoutes } from '~/route-gen' const components = import.meta.glob<{ default: React.ComponentType }>('./routes/**/[!_]*.tsx') @@ -11,7 +12,7 @@ const dataModules = import.meta.glob<{ }>('./routes/**/*.data.ts', { eager: true }) async function rootLoader({ request }: LoaderFunctionArgs) { - const res = await fetch(new URL('/api/context', request.url), { + const res = await fetch(`${fetchBase(request.url)}/api/context`, { headers: { Cookie: request.headers.get('Cookie') ?? '' }, }) if (!res.ok) { diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 1d8adf4..70d94d6 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -123,17 +123,9 @@ const app = new Hono() }), async (c) => { const userId = c.get('userId')! - - const [acct] = await db - .select({ accountId: schema.account.accountId }) - .from(schema.account) - .where(and(eq(schema.account.userId, userId), eq(schema.account.providerId, 'kf-auth'))) - .limit(1) - - if (!acct) return c.json([]) - - const { fetchAuthOrgs } = await import('../lib/auth-internal.server.js') - return c.json(await fetchAuthOrgs(acct.accountId)) + const { resolveUserKfOrgs } = await import('../lib/auth-internal.server.js') + const orgs = await resolveUserKfOrgs(userId, db, schema) + return c.json(orgs) }, ) .get( diff --git a/src/api/collections.ts b/src/api/collections.ts index 9d4ae05..8a61444 100644 --- a/src/api/collections.ts +++ b/src/api/collections.ts @@ -26,6 +26,7 @@ const app = new Hono() async (c) => { const q = c.req.query('q') const owner = c.req.query('owner') + const tag = c.req.query('tag') const sort = c.req.query('sort') const take = Math.min(parseInt(c.req.query('limit') ?? '50', 10), 100) const skip = parseInt(c.req.query('offset') ?? '0', 10) @@ -54,7 +55,7 @@ const app = new Hono() eq(schema.collections.organizationId, schema.organization.id), ) .where(and(...conditions)) - .limit(take) + .limit(take + 200) .offset(skip) .orderBy(sort === 'name' ? schema.collections.name : desc(schema.collections.updatedAt)) @@ -98,6 +99,36 @@ const app = new Hono() } } + // Build enriched results with tags, then apply tag filter + const enriched = results.map((r) => { + const stats = statsMap.get(r.id) + const meta = stats?.metadata as Record | null | undefined + const tags = Array.isArray(meta?.tags) ? (meta.tags as string[]) : [] + return { + ...r, + description: (meta?.description as string) ?? null, + tags, + latestVersion: stats?.semver ?? null, + recordCount: stats?.recordCount ?? null, + fileCount: stats?.fileCount ?? null, + totalBytes: stats?.totalBytes ?? null, + lastPushAt: stats?.lastPushAt ?? null, + } + }) + + const filtered = tag ? enriched.filter((c) => c.tags.includes(tag)) : enriched + + // Compute tag facets from all visible collections (before tag filter, after search/owner) + const tagCounts = new Map() + for (const c of enriched) { + for (const t of c.tags) { + tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1) + } + } + const tagFacets = [...tagCounts.entries()] + .map(([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + const facetConditions = [eq(schema.collections.public, true)] if (q) { facetConditions.push(ilike(schema.collections.name, `%${q}%`)) @@ -118,21 +149,49 @@ const app = new Hono() .groupBy(schema.organization.slug, schema.organization.name) .orderBy(sql`count(*) DESC`) + // Load instance settings for explore page + const settingsRows = await db + .select({ key: schema.instanceSettings.key, value: schema.instanceSettings.value }) + .from(schema.instanceSettings) + .where( + inArray(schema.instanceSettings.key, [ + 'explore_featured_tags', + 'explore_featured_collections', + ]), + ) + const settingsMap = new Map(settingsRows.map((r) => [r.key, r.value])) + const featuredTags = Array.isArray(settingsMap.get('explore_featured_tags')) + ? (settingsMap.get('explore_featured_tags') as string[]) + : [] + const featuredSlugs = Array.isArray(settingsMap.get('explore_featured_collections')) + ? (settingsMap.get('explore_featured_collections') as string[]) + : [] + + // Apply sort to the full filtered set, then slice for pagination + if (sort === 'records') { + filtered.sort((a, b) => (b.recordCount ?? 0) - (a.recordCount ?? 0)) + } else if (sort === 'featured') { + const featuredSet = new Set(featuredSlugs) + filtered.sort((a, b) => { + const aFeat = featuredSet.has(`${a.ownerSlug}/${a.slug}`) ? 0 : 1 + const bFeat = featuredSet.has(`${b.ownerSlug}/${b.slug}`) ? 0 : 1 + if (aFeat !== bFeat) return aFeat - bFeat + return (a.name ?? '').localeCompare(b.name ?? '') + }) + } + + const page = filtered.slice(0, take) + + // Build featured collections list from the enriched set + const featuredCollections = featuredSlugs + .map((s) => enriched.find((c) => `${c.ownerSlug}/${c.slug}` === s)) + .filter(Boolean) + return c.json({ - collections: results.map((r) => { - const stats = statsMap.get(r.id) - const meta = stats?.metadata as Record | null | undefined - return { - ...r, - description: (meta?.description as string) ?? null, - latestVersion: stats?.semver ?? null, - recordCount: stats?.recordCount ?? null, - fileCount: stats?.fileCount ?? null, - totalBytes: stats?.totalBytes ?? null, - lastPushAt: stats?.lastPushAt ?? null, - } - }), - facets: { owners: ownerFacets }, + collections: page, + facets: { owners: ownerFacets, tags: tagFacets }, + featuredTags, + featuredCollections, }) }, ) @@ -145,13 +204,18 @@ const app = new Hono() summary: 'Create a collection', request: { param: z.object({ owner: z.string() }), - json: z.object({ slug: z.string(), name: z.string(), public: z.boolean().optional() }), + json: z.object({ + slug: z.string(), + name: z.string().optional(), + public: z.boolean().optional(), + }), }, responses: { 200: z.any() }, }), async (c) => { const { owner } = c.req.valid('param') - const { slug, name, public: isPublic } = c.req.valid('json') + const { slug, name: rawName, public: isPublic } = c.req.valid('json') + const name = rawName || slug // Resolve owner org const [org] = await db diff --git a/src/components/BaseLayout.tsx b/src/components/BaseLayout.tsx index d07acd4..b3004d7 100644 --- a/src/components/BaseLayout.tsx +++ b/src/components/BaseLayout.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router' +import CreateMenu from '~/components/CreateMenu' import UserMenu from '~/components/UserMenu' import { useAppContext } from '~/lib/app-context' @@ -12,7 +13,7 @@ export default function BaseLayout({ children }: { children: React.ReactNode })