diff --git a/.env.dev.enc b/.env.dev.enc index d5038b4..83e3e72 100644 --- a/.env.dev.enc +++ b/.env.dev.enc @@ -1,37 +1,38 @@ -#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 +#ENC[AES256_GCM,data:ntFeHnoneyqh,iv:6bEe6mRrFzzN6Mlv4+3uLFhrS3WSlErWCCJeYQG9Flc=,tag:qF/pmfD+RFqOa6sJaQItlA==,type:comment] +DATABASE_URL=ENC[AES256_GCM,data:yitp0cv4lTy0wAaoNrMwXOhSV4fD+ncoF3MC1aiVkHLwWq8vLd7B290TJGW6N35DI9QTfcU=,iv:NOZKLexdSGmFdjcJ4+uxNcZut5M6VUkmWayJtzEHvxo=,tag:CLOimPP1F8A4ZQaerrTu1Q==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:mhTxrXXYPpWtO5gfyr5R3Q9xFJkhYoFBmpzuKcn7Fmn123rhF+M97f4drl2OGg8APDw2tH6IrYzH2BheiKBU1w==,iv:AaYNjEnj0lbFZ9F7G8LogpG4+hAqgDknbY4rsDiqlmY=,tag:3F35iD37vfZ0T24L1LVagQ==,type:str] +PORT=ENC[AES256_GCM,data:BWEV2Q==,iv:e1bQoxLHNcNauCoFq0H5zxUeOzItzgIxiLwGLLMLBdw=,tag:vDoVUXNQhfwHbqNrl7r5Cg==,type:str] +APP_REPLICAS=ENC[AES256_GCM,data:Pg==,iv:06Qg11o7Qd6tu/dt9wFfM/GJ1O95I9DQ9Cki4PTlR60=,tag:Gsv0y3yNgl524TnAxJ981g==,type:str] +#ENC[AES256_GCM,data:dxU1,iv:6yQ291T1UqQW9cfytgmqtRaxG11C/rDXomM33/SRrUc=,tag:ktqdJZ6BYHaT1BiYmMNIQw==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:66EWnXzfu0nl7nWVddRl,iv:8VtcKQzzcdHSRV/9csZUIiDqMj5XPTl8zG4bbyTnD8E=,tag:VQPdb9YIHWh3ku+w7CNt2w==,type:str] +S3_PUBLIC_BUCKET=ENC[AES256_GCM,data:bbf1oP2KPCI5PAHGlRDz,iv:x8k4eXdS52neeumARbOfuGdWJNSoCF3j1Uipv9s6qfY=,tag:+5m1l1dVFFe8FrqlV/uNAQ==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:UyvOlJ2xAem2zJDCEFO1HbT5Nxhlw01MEoxF/nEWN2Mdh3VZgpKEV5bHTP1XGJQ/ih2r9mvCpXS+A8MaeYHFqdE=,iv:SsixoqAZNbAJDSH7Tu9BQaIY8Yde6C7R8SYpKy4xg4s=,tag:PNKfE+qHszXirDW9fSwTMg==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:t/8qyC+NzM4/SMeJQKGULDLWiKmepcmEshEPJlTjgLY=,iv:vC3N/jPdncOAQpNQYwhxlA2bodI8TehN3CBgfsX9ACA=,tag:qGYuzV6FZuRgpew8mCXn+g==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:09Uwb5itGddGd1pHAAoKL1GsIH7Po8R9U2RQL2vKJa7IsiMbwCuzgO/q9/9h1HIq3HUtIy8+AG29jiN+V85Kew==,iv:KRH2Cbni7gO+gt8QD/hvJK54+cP0Li0erfmZQeW3KXU=,tag:+GMSsWKJzmyEBem1F0Jq7g==,type:str] +ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:fJVrXxU=,iv:3lmeHpaUt/GBRqvVdZi0VDsYfJMS9RQ9wPDYOJwRhI8=,tag:cFm6capCSYtE/IReue//jA==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:6mSe55uy5NaMTc+Klh5PuI//8jXKF6wLWHBq0OXMJ7o=,iv:hwM/6E8RmizHkBDsDUTKO/NWbLoCBPs1EkgQDlmevSs=,tag:4R7cNUWRNYMT9J/z6H6lDQ==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:I06Vgi3s1YafOw8jXPBCINH6ZDKXi00pvTbXpi84YqQqDkTzTyvZXd6lWJPLJF4xHNLvbSE=,iv:nTtrPS/0XxkMePaEuYkdwN2AyIbd2wgT98BuJtzsTJM=,tag:1T5yXhEVrrXCgtFtzd/gEQ==,type:str] +DEPLOY_HOST=ENC[AES256_GCM,data:tnAKp6qTRBHVmG4C8w==,iv:x511Hzj1zR/C/T2d5gWR1hUZEfmVNkOpnRIdMU2uzm0=,tag:PKr579RV2Bk/lubZFYfscQ==,type:str] +#ENC[AES256_GCM,data:5BHAbzc4DGICBRuObJZ0QJm7bCY=,iv:cE9FQwz7g6qc6nWY9cFJDw2asGreZNCbtqU+M5bH/EY=,tag:wKWP1+1fDcZHZNmojJcIRQ==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:O7jfOARSwwaaFb8poCd+udHz4433SvkSoZcvzIDB+Yk2V3w4pA==,iv:aahdHLMr0mP/uS5z5Vu1UOVs7F2PyGPTu+Fv8pzQIL4=,tag:vCZTlnda1r8s3CrkbcVXaQ==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:ebyjMZseMc68aDc=,iv:kbR9qC71m7upgLccBE6hC0Zh3fXzoeCJIzqIwqCmF3s=,tag:b2F8+neU8s3Zk/EVus2Xkg==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:Dn7pT/o4zZFEp4WLJWauV1CMI70/AT5OB0jiZ08PO14qao4GRDRpdfx1QlyFtfNsBDIeL65XuJf3a1zgJ63Mhg==,iv:vLZolBHeP0JpkAoVmcz/JVdH74ZD6kCdicaH2MGQ+6A=,tag:7Z4QZSWM2iddUwQS775etw==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:84aaRI8uX/Y0Gj7duE9FIGzay2uA/VfJFaUbJnOvK9YJc1kRCz+FSO2YXNRGx4NuX78Fo0PRIrtWrjSc223Pzw==,iv:rcdFUbvR/+BeH6dSMp0oB6Jk85Ev5l+n4Jx1ZoBaTKM=,tag:iD/uZSzUFON6dot3cN0wFw==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:duneNCpe3i9EAN7wjoajhkWlCu3Wztb2gmyfuSYGm0lCVkoRaR8U5Q==,iv:CTwsn6BreaNzY5/t+aWyyFtwVOH36uT41rOqX+GMvcI=,tag:D8nC31C3Xj7lHlbmB4F5Pw==,type:str] +APP_URL=ENC[AES256_GCM,data:PXB13rqBei3b2LdxHowL818YVD9Q5JzK,iv:LTg8cBwYxvERqyFWXjKWH9FpBX0SdEMa4l+bM/1hTXM=,tag:kNpHHWkvAqOsEBa/tYPO0g==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXMG9vZDYydERBN1ZvK0I5\nd1JOTlBNdDk1U2djbktmajgyRldKWmRrZUNjCjVWY1RNMGFaU2lpbDVQVWltUWY5\nbUc0Mnk1ck1ZQ2NwNzVST0E1MGlXS0EKLS0tIENZRjZRWG8rUS9ZY3FMSTdYZWlh\nVDg1R2syQzhMaGlUWGNhVk05L05pYXMKncjsk23TWMbmad2H0lQyDhmW4ThJBIUU\nWbVyCZMO9DbENi/EP9aiI7usOBP1+Ww6Elmqcqw1IzcS/c7fGnTjog==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6a0s4S1JuMU95T3hlbjRw\najFxc1RTNUwxWkRuak1zdnVtYnJPVC84RTNFCkwvc3lYWTdJdUdFSmZRQ3dZOVNG\nc09QRjNUancxdEdOVVpUWXcxVUZVRU0KLS0tIE0rUjRmN2NPakNyNEVSZlZnSTJS\nSjZvQmFIN052ZFNKTkk4N1ZRRUVjT1EKyEl62jSWqc4p3vUAHLLhb0NT5c0HDk4l\n0p3w4bmWXHufrt/RJpqrZXzAldGz3nn6BxvEuZVVav0r/EmK39VhQg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6ZnphTElNUk1vdnVGNkg4\nWndib0NQR1E0NmhYK3hPMUY2dENncnUvQ0NZCkdmQWJwaVVDK2Y1TnN3cldMOVFm\nWEh4QlIzN1crMnhzVmV6VWxYS3NyOWcKLS0tIGg4cXU1dnJWdGNVTXoxSEhVaFo0\nOEdORDgvK0IzS3QrVGUrTzZaYXEwUlUKTCs6UCrJF6psL6z5KbulkjGe7kui48BN\ntWlK9TVFgyFOLpRxu1+k96H7w/7be6iGS2UPolx6k44TfSb5VcVmNQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCMFE3UVZ2djdEMjV6SXly\nNnY3cGhxL24vdDFqdmhoQ0RhYjV4MmNuVWpvCkJWbnEzS0R0R2VoL3NaMGtiQjR5\nUHJJeG85TnV4eDdpSFN3bG5qZGt3MHcKLS0tIGg5WGtMVjBpeW53bndpc3BEQURQ\nM3ZzM0hSMGVBYVN2cFRlaXo2cm5SZ00KDfmdqtWRyB+oHorjZNfjbiN7jYWTbTga\nIRV+MASJ/IxEhpi43WS1J+7zuAEcPKluJKv8W4WnK2mNmScmvFpaFg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4SklYNUd4bFZHMzdSM0Jj\nT3JnSnJncjczb2RxOXZpbUxtTnVSelY1aWpFClNXZmttZUFaZWthalgxUTFxTnJk\nS1NjMkRncDNxMHFVKzVvL0JnMi85d1EKLS0tIC91VFNHY1grK29BUlRFaWkvTUpp\naDdnbll2RW1oVVNwR04vakJ0R0F1cEkK8uG4gZXKffhiqASjHaGVYsfvtLRo+VQE\nixx9tU+0Q49Okzh9vKMsmUL3O2FJsoXWKvQKBSpK0NOKx+qq0z9EgQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBldDQzYnlxVkZ4WURJWElm\nSHc3dmN6ZkF4MTRLWm1EUC9zOW9uODVWRTFnCmt2T2JxRE9iaG1IVGw3alpIVEdF\ndUJnNWVKRUdmOUZBZzFZaU5aMlNDMncKLS0tIGh1aVZnb0w0YWo0ZWZidGw1MTdo\ncUxtTU9aM0psWkhzbTRKTzZwclRLRlEKLenke0xP0HJNkyydqXAexbbiSAbEZi4U\nrOy1nwqE/EvUaUIX20tRs/UGq0ZJsJkzgOuqD3fa6ZjJaayxFh7RBw==\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_lastmodified=2026-08-06T18:36:19Z +sops_mac=ENC[AES256_GCM,data:Lvftzer4+EdUvF8tUKhm+AqXG9d5Z9E38k5enbqJKoIPLPaKC01wRgHPwW4xBDJqnwqeIvOfhgaixWlklwjNzd3zfClX7wAl5IEAGutir9yZnl3aT6jneL6wXF4soZtYoPr56D4d5rycc8A0Y2ImowxBC1Sn4gZd73iGILlIpQc=,iv:qJi7kisW5/CGWyOj0cwBkJtiadeXBteYdImD0teUfdU=,tag:44JcHJx0Ai+rMcXhRhY3zQ==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.env.local.enc b/.env.local.enc index 44f6b68..975352d 100644 --- a/.env.local.enc +++ b/.env.local.enc @@ -1,38 +1,39 @@ -#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 +#ENC[AES256_GCM,data:49vPC3ejk6KW,iv:/8DJO87b9DmxyGGE3YEdtkuSBgamtfrj78UUwR+rIYg=,tag:oPs5/UBTY2t4i0LXr9Awzg==,type:comment] +NODE_ENV=ENC[AES256_GCM,data:5cCNfFJSNlddkdQ=,iv:bGnAmGCA3G8gDhHY19am0DKWoU6KdSybsNqfT2sfTbE=,tag:0yBDaJ/wE4wpIZoSCPgPDg==,type:str] +PORT=ENC[AES256_GCM,data:5dgzWw==,iv:U4YpZa4CqLgUOjcJWG7kshc4KFz1UfYOPIWfVb6TskM=,tag:vA5nfYr5ilf4GvzR20vC5A==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:AA9Bw7d+JuiXhRUgeglhffwtJrHYfj1VCv8dAiIAAiRcZ5l4H9IseZCdwpkK1HAVSXmQCmQ=,iv:yssOT+JBz5jOtSxmzQw1aIrVHCvsI+/j1dN8041TPcw=,tag:GfLz9+oD23z5gTIWLv0dmw==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:lr56mYmFH1TtEtqHATUzYLWzmqRPylM3TrpqXBjO4v7gRcobA3p3Asjv47TkvfpChURnCeTrB0c552Ue4TeqtQ==,iv:XdE23plTLUqvFwuSYRgf1XKBcKTRi3iWZrMqUSsWz0g=,tag:6FX+Eh9E8sU4f9Dc27jEHw==,type:str] +#ENC[AES256_GCM,data:IEu179YO951HSIYzjlc6TeStpw==,iv:i/5zqvMv2uLlNNKJqsWnP/iEthC5Ec5Xry4/NboK7Kw=,tag:ypkVagiqkQ8KU3+amaRKgA==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:Ue3XV054QBAUD+tlNzCF,iv:87tQ2skRacvhPtz27kMn2OKhnEOeD5Xhjnr1hm3c9v0=,tag:nGuWJQscbFzEkD5h0RpeUg==,type:str] +S3_PUBLIC_BUCKET=ENC[AES256_GCM,data:EM8riKv9mrTHQzwxf6IV,iv:7rodyVXj6tDY8q2hqDWO7IHCM2oezNczXKAYd4wtr3A=,tag:e09QO2uhX3DQeZqdbX3jOw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:sU2BhYR9d9f66hkf/W0wQo/00v6KEmZ1rHNSGh+Eb+xTQo4J1ts2hNm0+P+lOqex/XgTMnw1/5Ik992Vx5pfWYU=,iv:RAV3b3nRRj4BQIyZKcCI86mi7etuW4s9ekydKBgMHZA=,tag:gowf9g1eikXZ5vj97rFW0g==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:TLLepLD+gvgOcfGtmK+3nAe5+2bOSs0nePlb2kumHdA=,iv:0L+LZIeAzzYfKS4kG5KWrAQqgBoC/FLeic3m8lx5kls=,tag:JXk+2nAJT0o5exiyKg6OCw==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:8GR4v7rLL+D30JkbazuxrDq0J+DzU1lutFGwn5VJmsMSSIgnB13QtNSh2yXNHIx6P2vVzUK7+4y3hMX8hCRoVw==,iv:miUA9Cakcs4n9o/Nq8I5msO4bVFBiPVw+abONUNS50U=,tag:5dZMMAkeMTWS3/m8d3bUzA==,type:str] +#ENC[AES256_GCM,data:IKY1FUjvrQ==,iv:o3wmybvpWNy4ck3n308cf65Ymgsl4NZVcjC0BIPl774=,tag:Z5geKTBEDtwTsMgblbxYHw==,type:comment] +BACKUP_S3_PREFIX=ENC[AES256_GCM,data:BRtyCGLVhRD1,iv:vSLjzAwYdwTsrj+dmcZNUCKt4bzrsJAr/y6YgJQFfyM=,tag:o0YrfPie6RmapWoFOEpmzw==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:oUwbof7bTJF9R6hXVaepVZwoRcM04jsuifiuTHXYZcA=,iv:zS/HpnIa03NYsAKMNFKTIfmhLwqVctioF8u0lxePbXo=,tag:7RvBUgfKfiQs/ghdiQlwOg==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:yyn1DorN2jB1ZzQNooVFuMbAmlgpAeHVOD5CileKoJfGe5J76bGaxNcJJFLoJYHvztRiLV0=,iv:d1UYqCoiN0ficU848qNSHuCtCc+zg5IQcJg33Miq5GE=,tag:YVrJDhPoEDvD5Em5b7yobA==,type:str] +#ENC[AES256_GCM,data:HazdGoFon9cRJlRpBhNshZxXb/o=,iv:lnrde4ZwuoU9TALA7sqZ4PG8fF7nwmfuy5IGR73YkaM=,tag:Qis8L+tI68YB+bzwVxwVAg==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:y7BOYQeMzgc3hrpQaRR2SbrUMBHY,iv:mqxpxl7uxhENYdtAq9oEZweF8hVXwHOe4CIPaxGrjEA=,tag:Fl6fUouzdPsa+A7P/mNbww==,type:str] +OIDC_ISSUER_INTERNAL_URL=ENC[AES256_GCM,data:ewz0UA8h+Qm8ZX/fm6zRQ35L8M+a4vxbsn21Qm/hxHY=,iv:2yaS8I97rNO1IzBY1TbVew0k66/2IaxC77OojbzRGPo=,tag:e9IRiOAZTkafwzYGnU9F/w==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:e1JtQRqun/OB5qo=,iv:u8ngNpfwPuoBUmEP7LDD1X37Zefceesh1sZhvDHGjyk=,tag:4MxrWpUNtXWC27DYJf/p5A==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:Ndmt6Z7Pyb+lJP32wCGxsB/xhLNr992btFZCNC3NmOk6GgjiRozN0ToMUAN2kyMwWarpvF3qytHZgmjUmWxECA==,iv:PSbDpOTRjCQ6JZhLog0twdZkQFPzS3hUYZlsO0AoqRU=,tag:sE2KiiWrBMJqgGpJUK9llw==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:rb5uBSRQm+36OyDhtxeCRk1k+MZ8oDmLzWjtJ2h8Q5I=,iv:LsAlcjQL6bo6WPnOmXU79dj13dW+KUDHi1r9nz76yoo=,tag:/psnnv5wtV3uSOd9rtDxdw==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:/Ql8BIA4N7LlesbiFvAS0XARhwZV,iv:XXA0C6gZ9l17StxsU0VSmGoi7d06T9K7PW88EJp/tnA=,tag:2+fWnnAHhi9vvRMHFCgKPQ==,type:str] +APP_URL=ENC[AES256_GCM,data:W8YCjhg2qwH6iR0Gao66tK9wXZp+,iv:6teKauGRn8tSB4yGF3dmQj0wwf/8h2gI6XPQY69z83c=,tag:BJg0GDPP3NXVixLnzTKYhQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0K25jb0h1VlBCZnV6eWJk\nSWpSbWFhVjF5M0xHTm1FODdualhiMDB2OFZRCmhseUR2ZkxxL0dkdUtUeXBTVE02\naWFqY3pKblVFNGN6S09GcWFnSnZxa2MKLS0tIFpISjlqdFN4R2EwWitOMHI0ZnVw\nZS9MSEZKL2Fvd2xKTlQza3hWMFYwYzQKyYv3gzjWQLjvh83DSWecC6X7YIYSJa/B\nkyuw6KhRAwPJNtRkDML6SWh6JMnG15cJO9uP65gi+PO6XjWmWbUvvg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrYjdqMUlyZkVGL1dnTWEy\nSEtZZVFRRjlnc2F6UGlKMERKTHdOVTI0Z2xnCmpRQnR1MjZvS0s0WkppQkpyV3dj\nbkJGUmYxU1NBRjVrUjFCclNnS3IwbVUKLS0tIGxqd0wvRVEvQ1JlcjJOWDlZd3RN\nUkFJdFFOTmVOcnBEcUFXcGNDT2NBN1EK15cvctDmsGLnUPElLqNt5t4Fd/ygBzJh\niCazrSUpIA8RDiBxUFn3qc//dXhLVACyasPaD/cAsMI1YnKCBLIbjg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzaGIvR0lQTUhGejdDUDdO\nazJpRlZ6Z2IvWXFTa3I5TVNDVm0zSkNTVmlNCmFGUk5DWFVqYVpEMkpBWEFScnVa\ncXR2YUJJZE5xa1A0RU1pYVRGT1lNVlEKLS0tIFdYRkRUVnAzSXVERko5bHVJVFVN\nMkFGbElWeXlXQWxpRTlUcEY3SFd2ZDQKUnfXknBPhlVYJgS30nge4kag3yJniuSB\nWUO4HvMDxkj8mXKsKGed2ny8cYkCZsqXOdxaWWGtk5GsUe/v9dUw/g==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLQm9VZENUZVg1bkpHQ200\nVjUvOWg3N0M0aTBFSTdrdG9EMjJwTTFvank4Ci9qaHB6OVphYksxR04xTWJiekJM\nYWRNWkpnTEpmR0NzczRHVk0wR1dkMDgKLS0tIFlZVFhuZ2U0MUlWOTYvbndFeHZF\nWTM1RUFoNzdaVmlXck9YWjhrV0tybjAKzVao/TJ3ZzAtxNfptKP0myEKRE9Tf8r5\n2HtfWp7su0l6/kHrUdlKf9PKe8k9ebuxEvSjreB/tj+BdwMZkWrg4w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSZWRxM0lnekxWcEFJRGM4\nT2lkWlpTcVNJa1ZHRVJsbHl4QjVjWWxXQW1VCnNGZ3I2YUwxMnlYMXAzeEw3K1FN\namREblh2Mjc4SkIwTFRZVGsvSEhmV1EKLS0tIGN0UTdwdy9xR0I5MDFPa1dPeFoz\nYkhLeUkzQjJYNDNnTzdKNFpjM09LQzgKKRE/yB21pgXUe+kv0pw7UfjEs/RHDsAb\nhsPcj09HpAhauFzQxkIqFyOagshZ7c/OhAbfPwmZ2ycm5mLW8WOSRQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOdktDT21TWXBWdmZaNEdn\nOVFiNFQrTmIxVlBFMlJIVlZqRHo3NC9FSENnCjBqQWI3alpwbFFHYmJkZW1JaWoz\ndXdGSFo4R0RoK0g5Q2Z0QjNrYXBsVmMKLS0tIHdxNEJHTFZ5dk8zYUJRQWJVWFRJ\nWHllTnQ0MHhmdzZLNFVCMnRMS2Q4OG8KSR+VznPpiTPuM1PCgQqnHyJcgxwf69We\nrmcyR3jNbIgOv/re1ZhvtfQg2vJhF4fz7QvTyQWFZbIniFgCLKEcJw==\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_lastmodified=2026-08-06T18:36:16Z +sops_mac=ENC[AES256_GCM,data:AaWybTpzzvC1IMHF7eM/kczSMCY+O66Tj0gxuSOQRho93zcrEl0CCNfsq7Ptk/g8Bl+TIVZywgAIiIHKXzKKliQQaKcn2ONtFI2/tFU9HCCjbod3EtD6rP5Uw5BtOhaozUVyQlYNmr/AquZb3KoWsE6Ci0NRmJ7ldh5pSiEfNbA=,iv:sZbxN6kiHyPaqi2SlzZqZewEYuEd8egCSFtuQO2RP/0=,tag:e8saJo/8cdvk5bdlTFLg9Q==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.env.prod.enc b/.env.prod.enc index f15b48a..526930a 100644 --- a/.env.prod.enc +++ b/.env.prod.enc @@ -1,39 +1,40 @@ -#ENC[AES256_GCM,data:S8+pOKm0n6fv,iv:Ofyq3a4veKPXx26RQZVW6b7CHCAlXVG4TEtV4n5Mzbg=,tag:/FhWujfCR9RnVj5RAX9gMw==,type:comment] -DATABASE_URL=ENC[AES256_GCM,data:f5RM3Mst6QmIsxkrTkcGgXnlKmU0jZ7HqJLKBv6KndEIsskb9IInjRtIigf7rqUhMuNhptk=,iv:4TPHaEgz432GU25U2PA/1nn4xeUQ7bI61JaXyYMOkFs=,tag:qdH0XYdA06PB87RWkg5+Aw==,type:str] -SESSION_SECRET=ENC[AES256_GCM,data:iwXShRI3Or1bRBQey5j7tMuuLQ9dLljuy5OWQwUu9QmBju01IJg5IT4oCvPbi644MVuExju66lKCy+hNNZccvA==,iv:zANsccp/ZagPSijWJZBMxN0+U5yRUux6NS3ZEiqr9Kk=,tag:6FmgbCYrgRSSmiyCiIu2/A==,type:str] -PORT=ENC[AES256_GCM,data:HHnfQA==,iv:llrvyA53+jK/BdENE2MT5gIkVb4pNc6NJDpM6BV8u6A=,tag:4iEqVKRzO5Oxa0l9/hEdpQ==,type:str] -DB_PORT=ENC[AES256_GCM,data:yjI2tg==,iv:fDGMrdVK9LZzddr5LHylw9gQq3l9SSA51ICUgcj/iqw=,tag:bcWnoZLWkqeO6V53BZqwqA==,type:str] -#ENC[AES256_GCM,data:mHfJ,iv:L+EkG4/YstUnuczK7HKV0pMnKU08ncQi97k579iy7UM=,tag:78dmBG51U/oxF3bo/4laMw==,type:comment] -S3_BUCKET=ENC[AES256_GCM,data:ef9IkzGV6Q1Ny6kaFNmR,iv:SNrsSJLMO62okvsiZ7s8P5H/gJ/8r8yVnNEgwEIf2rI=,tag:c9PFjheRAXktBRhYk7lufw==,type:str] -S3_ENDPOINT=ENC[AES256_GCM,data:QDu4j4xd+KBm775hWMbcQ0JilFBWZmlzSb9mxKjRRlxUt7HkXTbivq5KIaxnDfRkN84MTRYbpW403kCmkNc5bh0=,iv:Pl+sc2SlcMIFC/WUY6tp0r1EtSOIWYHzwwY0YDEGnSQ=,tag:YCKrm4zEr+nn6/pzWClLpw==,type:str] -S3_ACCESS_KEY=ENC[AES256_GCM,data:mAKcwU5nMknU8++i08aQebMZLf0PW21czINjZu+UQWo=,iv:UQH1Zsasnu5P59Duq5XZ8rK3Y284/3L151/3euJgoiw=,tag:mvcFs5vTARleK2hNyTm1zw==,type:str] -S3_SECRET_KEY=ENC[AES256_GCM,data:cds5OTwpbbusri3Rg87h+TanLSZDpPQtzjXah+X3fN9O10RhfT9x+kuJUT1j+zw1T4+RvWJj2aNm4wd5P/s6jw==,iv:Arm379g8dD6U5jXwsUev6sC2VnmGUtP31Yo4KBsADdQ=,tag:DRpqMS7qJq5Kk85dYIDhVg==,type:str] -CF_ACCOUNT_ID=ENC[AES256_GCM,data:koW8qp8qzO82pOuIq9qr52MY/mGzkAcWlHmuqhW6kYg=,iv:ckPVXqUOHsZ12YKPSU3uTQf7SdyZOmOSNmFIpekn2+8=,tag:ab3AXVtkRWRCE2r1F38E+w==,type:str] -CF_API_TOKEN=ENC[AES256_GCM,data:TKtGFVsjY0zgAJr3YOBU/J03hQeQntgcads0913gl6zzAemt4l4S3KEFP5CCvLxKYulQH+c=,iv:4whkUwezLTNSfGjJl8ssBCbxof8sy5MNDMW+h2Dbdik=,tag:C0vNKRewsiS9BxOnNNCDZw==,type:str] -ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:QljFPTk=,iv:btCzWNeI6g9fG9Y9r/X1CVbsmpqxe1NGvdQ83R1P62c=,tag:bUh27EQEOYzbp9t+PkthwQ==,type:str] -#ENC[AES256_GCM,data:ztbFfuvIovhCrHZNeNmdKIGc,iv:7poazGgshLa2r8klGzOFnjuVir9pTk23SnrA72IXDCQ=,tag:3rrW2fw1sxaod24qeYwnoA==,type:comment] -UNDERLAY_UPSTREAM_API_KEY=ENC[AES256_GCM,data:IR0GBgJ7NYDd/YzOGfdI8+gNsRTz8VDUKDtmHPa+YEtRwak=,iv:ZPGqIQ2qZ63lQVOYXksi6JPHAzWMruIE9zhiKFk84U8=,tag:CWvyDWtVPE0+2ad6r+i4ew==,type:str] -DEPLOY_HOST=ENC[AES256_GCM,data:Jq/twyOLJkhLWEhKIw==,iv:FyvQuEn2EapO84HwiTyqw3Hni1JKH9jp7Iewv026WAo=,tag:EFwlbfQsdxSI+NkRX8vkfA==,type:str] -#ENC[AES256_GCM,data:AGIuuu2YmR4=,iv:jusIPaza8z6OU9A1uSnjbzBItJGjZ7C8OVDGgqastMc=,tag:ZQS3YzagM0TsBMVLjps9sw==,type:comment] -OIDC_ISSUER_URL=ENC[AES256_GCM,data:qs78OhH3O6f4xt5OJsCaZU49I5vlbal2pqrvNdTGP3x0,iv:OQWQtV1Kfg+PC7TDYHfaT1DRIDEoeCkczKRTpzqdaFo=,tag:2obkOqeIJx1S+lYH5/L3AQ==,type:str] -OIDC_CLIENT_ID=ENC[AES256_GCM,data:usZJpTUQ1pqencw=,iv:fSmcWnrPWNo0yuq0mPJzn6PsW8t84PuHcv3n/d8sAQw=,tag:QJFs7qyfiuB8BDd38Tzxiw==,type:str] -OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:G64r6sSyuRGZUF+303mSTJNA7G+7Lv3Ocv7FXAo8n+JxHbona94f9KkSvYHizMDlYoZj1My0EyprIIrwNW5X4w==,iv:e17k6cbv+b/uQ+MS2dxfXAOOgGs3mfFvCgSQDXR/l1E=,tag:OAyNIqqsNJSFuKyg2Wf+ng==,type:str] -AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:G/cejd1WGizQSJgcqWzKStSfC//DwdeJwjoxqOPkiGiB/WLm48ABMatL7IrUb58LQcSDxB0kj+OR4HzgWUS80g==,iv:fRjcuiMzMtoIoNrYVRPGcCD9/a0iTXgArP3sacA2oLs=,tag:2xkI9b5+ADR6C5p4khGGNg==,type:str] -OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:v8FqeH1elnxtioLGq0l7sCwhYsL2K50iHJsxkdV01b2h9uuc,iv:CttJeyrQgsoVNGqo8dPB1Ufq8CXwYbLr5LJkpJinc9o=,tag:uOkC680Z/U4IZwLgbVUiiw==,type:str] -APP_URL=ENC[AES256_GCM,data:wsh5L+WzyEyMFqANYO7fdlI7GLIWxVgn,iv:WyGkMY0Ek2f/q828QnVuJ+cn5e+dQDXDToTl6T9014Q=,tag:un6EcFA06GUu4XOsgBKovg==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqVlRiNk1OMldiUUtXYTNL\nTTYvVlVWMnJ2U1RWQVVSZXVOZ2Q5STNZRVRRCklJcFVidjZTTnNkUmh4c2owMFp5\nT3Fzc1ZxOExhMmRaWWg5Q0M4RVV2ek0KLS0tIE9UOW1UZCtnKzVLUUVIL3I3cDl5\nNmZtdnZMWmRUQlIva3FvNDhPYjVzWXMKNJVkrIplKazLydTWC0ssPps8R03jH6/2\nq4d5Pg1RAd/A8rDlAUCsgs+qIt8B1fD6uJN6x5QJBFeDHTQMBcBYLw==\n-----END AGE ENCRYPTED FILE-----\n +#ENC[AES256_GCM,data:9qa1KuKiYD7U,iv:UqV2i7HST6zpAjJxQDzWm8UbWzKeorpuFttLXoXgb4c=,tag:Pv/k3E3RI2h4lhnR6nrQAA==,type:comment] +DATABASE_URL=ENC[AES256_GCM,data:7vHL3XEyMHe7XuBl0Z03rm1zrZ5EZlO4j3obU7C4tjtpmQ/MfCPAh6vjvxm6sLe5aa8ZU6o=,iv:HTAvI2hUOkN7samvkmwxlE5yb4mzThC009XLwUqXDSI=,tag:9bvWLdpaOv3mEwBozY+6GA==,type:str] +SESSION_SECRET=ENC[AES256_GCM,data:8HYFdnVDcovsyAZujsj1dcZZJVLBv6HCjvJDl4WKAsBRh8rrWrHFJPDg+yym73m/K7cMr9OKmpv/2V217RrNaA==,iv:cxQYjsj//G8LhxrtSQnljM53ZJmi7QJCHj3NCQeeIPw=,tag:NV0o4sycJ+nj4gfUrCHJvA==,type:str] +PORT=ENC[AES256_GCM,data:fxYpZQ==,iv:x75rD2WzGKxejsMHcUdD+2dia5ksDIEZkiKDAFL0GOc=,tag:4LSso1miQ04wbbR6u0wNyg==,type:str] +DB_PORT=ENC[AES256_GCM,data:5x306Q==,iv:zyHZprHn8hoPMbRv64Wr8sHQ1V0SHkOfvuLMAtsJPAo=,tag:IzIvQ237DRV4HtahJS363Q==,type:str] +#ENC[AES256_GCM,data:JwNT,iv:dfFywgmorhTiiKU968s5QpV/uW1EuM0mJem1G2+85ZY=,tag:Nzb12PaOrj9YvFDx2aJAMg==,type:comment] +S3_BUCKET=ENC[AES256_GCM,data:SgRYVkth81/ZbNrqS27j,iv:Jdu+izNwkxgd1ZDXvaQshzxHu5x6WhMczXdjB9yUK5g=,tag:pyiXBxD2GBd9Dyzkhk+AIw==,type:str] +S3_PUBLIC_BUCKET=ENC[AES256_GCM,data:DGMzAY4bo6Rkqpr22Bcb,iv:JE9pcJDOqLtPIbya+Y2dvMRirbFrk06jjbMqstFYIqM=,tag:YTkh40UlywMk2iGhTl9PQw==,type:str] +S3_ENDPOINT=ENC[AES256_GCM,data:x6K1CumUaFq9STOp98NMcNd+0hZrjGMyx0aeOplaKMGVX3zqcwTKiPJ2GYqEPzYZ0DWJP0iXH9g233RBdMz22bw=,iv:SCMBnuWE0vsDKC1fux/QFGId0ax2/1IBjDTSDvkWl/0=,tag:v9l9FO9TaK8SyvWhGgEFHA==,type:str] +S3_ACCESS_KEY=ENC[AES256_GCM,data:msZND64ld4kRKEeSSDh5z7ZwYk6IVvgqZRMxsRaM4LA=,iv:arMkF7TJdHX9rUzzgee11RyKcV3ybomAImgv4jedKIw=,tag:pSiCF1b64F9ZYhm6c40SRw==,type:str] +S3_SECRET_KEY=ENC[AES256_GCM,data:hVRl//guU0nzVnFlrfooWNYpkBjUJPCfHjnPy4Pb1QWWwWUQ5QVzWsYNapxv45q2u3EYMi3J1zzAWDUrUlMvAg==,iv:DSeIj/o6WUcTkLnglSKyunpKXx9BUqxBvcDBEwGFEtc=,tag:mSh+J2lD252FORCWiIDBuA==,type:str] +CF_ACCOUNT_ID=ENC[AES256_GCM,data:SN768ztp5brseMZOXVfBOKdThQqFFDHLOHuvl1FVmGc=,iv:qJAc4lnsh/KRuevwI3lh6TFsGVjQpSKzS1hXUF8fFo0=,tag:PX2t59eiswsoTCux3X+GBg==,type:str] +CF_API_TOKEN=ENC[AES256_GCM,data:UaUT4ebDfRMWyPeuh/Oe3jIvbCmFfnHfIQbVVm/o4s1/BGZOm9gwknxG2Gp/IfHjwzJ/cDE=,iv:PU/l/DaONZ63y0MH3PR86kiToDOv2dlgev2mfnEB1v0=,tag:FDXwiIdI33kSmAsAttScXQ==,type:str] +ARK_DEFAULT_NAAN=ENC[AES256_GCM,data:0fTt1U8=,iv:X4SnAVEgf6NUPx5CrXkK+9CQTwKvyHr5XtsMOZGcjKo=,tag:aOb0/offTYZTdmIEoOj4zw==,type:str] +#ENC[AES256_GCM,data:zQ6KhFvV3hhE0hXItBtJB+AZ,iv:aRC7IR+wzoYYQI39XH3bmqweQhVZ6ot3+AnVx2gaiEM=,tag:V0dhyhOXm8RifOl1RBjf0w==,type:comment] +UNDERLAY_UPSTREAM_API_KEY=ENC[AES256_GCM,data:i8Bi6HFcOgAno6PsJP/SOR4NOtI7nv87GdXgPuwZXLSYRHg=,iv:PgrOGka/FdWhQUVmo0fZ9pvRM3AU+f/YcBC3Iy+Qkbs=,tag:QXrKUfOWrEhv2puE2mgskQ==,type:str] +DEPLOY_HOST=ENC[AES256_GCM,data:02VJcC+gVdbrPOYNOQ==,iv:jQRTVTBTrUfEHdEEbwlz93emDIN7CGzxfdmSvypl3HY=,tag:DdXO1N2QeOcYKide84aMNQ==,type:str] +#ENC[AES256_GCM,data:3sANAbR0vBI=,iv:aeqaDJ0OUgzOyyfGMzoTNOEFXkA44EAN1xWWVmg39q4=,tag:OwoOGqe1fBm0c7dPdU2n8g==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:dY/Wi3cFUTEWlABIXwD9PcbrR1sL6jE+zL/OiEQZqOxu,iv:6pHrqU7/eeVL+2BYoInq8TAanulOkjmVqR/Dx4d2s7E=,tag:MUrdAaiziKytT5hFA3B/Gg==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:kJbgyxHasmsuumQ=,iv:z7Bh6Cx8Bk06BNsjSX+P/4vmXXzmvUKX+KakJSscgQM=,tag:wyCOyi7CbHePOKXtAG+KxA==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:mvrfW+KjuNoser8NkB2x7ldjyCawqxaJsTOD0PLrL86no5HphtWVZVZlIqpZsTti01fHFbx5BwfvgJsZMGONAA==,iv:4vCsN64fQdPJXF1uTPlLiZeqVlL2zroAggCAAC+N1dY=,tag:VC0LUu05MDtQBW0/v0d5ZQ==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:jeaf8zHlMcBFGUV2meW4JIKiBFYcVIst8s690ODDOWVk9KB058aa60vzykBOTbmfyB3k5JWXbNyFwWtsK7Oh+A==,iv:9VD6fu+nOO6guoTsY8yYlLbPK62CvlDKTDIX39ul6O4=,tag:ysFb4K4aWi/gcImXsG5jLw==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:6SQnbPdkdKqf9JTrnSyvSODVIYdKze59vZ/IJm1RPCclsmlQ,iv:X1Dkr81oPTF8/z26Wjb29OQVSPDrrsL3KspXknP6COE=,tag:B8DeG+jxJTYE9wD0bdnhKA==,type:str] +APP_URL=ENC[AES256_GCM,data:Ysfvvk/zBRCT3nbh9tq+q+cv3N+SYbBM,iv:t5Fn6kdFFfF4d/LzsRN6Iw0BTl1eKIEzCC+GE6f3ryU=,tag:JVcrimdydSJLErnKJnWACQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3S3k3T2RmMHFBS1orTkE3\naXZxU3V3dDZVbzZxQTE2VkNtZnFwaE1LMlhFClZVTkV0blZEdUtqRFUzVWV6QXlx\naVYwRU1ZQVV1dC9GZnpjTHVJRVZLMXcKLS0tIGF0N0FaQmpOYjk1ZzV1dnZ6RG1o\nd3l6bmlmVkh3NjBKTnBCNnB3anE5YVkKlnRPYOSsLaCzlmJL11m3jcbfw6fCuEuC\nMZzpkAl4pzrUU2Ul+857yyd84mEet8b19haOB+HD0H+Vi2PXVCVxnQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFaDFodHBlQ3ZjNkNCMXhF\nZUpUK0U4blFkaWlqaWdCVjVuQkFKTFVpUmhBCko2VUJuL1V0dmF6V2Q3cU9OUnVN\nRXZ6K0RWR243SzMyY2FQSHVUQUpDbTQKLS0tIEVkeWI4eW9xRFdPWS9yTS9BaTdk\nR1RvNDg1RVNPdUc1eTFKTDZPQi82cHcKati6YkfUKx2kpE2apG2DcnuB2l4gPNaO\nIVue06wdFOgvRNvtck+n9zpeDgKYfaZaKoZhMZDNZbDjMPhDkvstBw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwbDR1eXlnQk5oL1ljNDVH\nU2l6TURMdmFGVXc3WjRiS2YzR3NZell5dVUwClIxaUI2NC9ZNDVVbnZmVTdHTlNv\nWkl4ZWZIeVpvYm12VEZISWtLbWNOV1UKLS0tIHQ3MGk2dWVWVlN0QjFNWDdIeHBh\ncm1DdDJheS9NWVFTa3pLdTJVS2kzLzQKc4r9Dy0Z6UubblmU0oyplNRkIBQbx1Ec\n+8oRqP0IqZjBrKF/GAjS27lqCiSdzGAucPdAsbauxcYkC/6CU2eaIQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1ysddqggsx3h8zkv7xn3z26sjak5pqms6pyqhnky9ukrvpk7es5jsayz8w7 -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1cFBlelcraTltVUtXOUFT\neWVKSVM1VFVNcVhDRzFTYTVaT2hVUkFYNFFFCjlMcmpER1lWNzVjV1hvMVdSZllR\nTlE2bjdFRHJKRlVOSzlqd0g4b2ZMbFUKLS0tIEIxbHBvcmRzakJZeFdlclVPVXg1\nK0ZsSEVJOUppVGdSWFN0cEJ2OGovK3cKfE+WKTkgY9ZZmATseYZVYJOKne7A9MOD\nl4pCyVOpJPM3kg5z9FpwuGRf6aB/ZgxQtDnAPPybP6Zi67BH+qKqyg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWMm5xdkZSL2cvTnZhQlNX\nRS85N3YrNnQycHlTMTVodjRPQVNmWHJSMENzCnI1d0N4akwvSXFZNFFuYmNqRE1v\nMDBpTUlQWXFEclBwZTlGR0pjeno4RmMKLS0tIEovVktTVWZEMWdjYk5TUkMrK0tQ\naE9RbnJucCtXR3RaQ1JmdEU0cVQ1VFEK+YNWUaTGAVz+gLJvFH8kBQCOz9gUDLYO\nZcRgb9awvYbzTXZrmKlswZiWdSAfYgcpWa24ZytIpKRFN30N3FAIDA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIem1WSExFU3luUEhrRm43\ndE9XSk0rc3oydzBsd1VySmVmM2NvVmZRSkg0CnFWRU52bGZza1RiSGp5VWNSaFB5\nZmZXWEhpYU1sc0RwaERZNkd0dlB1UFEKLS0tIEFscWJydGtBTUVwSHlKd3l3RHdi\nV3Z0VU13RlhmK2JDWDBXd0VicGFEK1UKlych1jW1KWZo5iPEpKZNgiL9xcat+Gyp\nZicZWf+BoSY1X0S4TadpTWvuvy1IIleaNBuSOgvd9CPlOKkCVq6poA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1eEFKaWM5cmF6OGtYb0hU\ncEthRnh5SkxsMkp1eGkwazZ4VzdVM2s5YVZ3CkJMbFhRZll1WGhDYThIMzhVTDFQ\nWFF3bldURGw2S0dtMlJsOFFLWHFPcFEKLS0tIC9nK1ArUTVJWFd0amtjd0sySllW\naEFzcVhZTlVvMEhVemZyTFJvYU4wVFUKH+EncA34lV7KDuLcqm5ag1isTp9CHLOu\nrlthXZ/4VkHqaaZpDUfyeDf1CAmWOAKLVBGe+JACMJ6rd/bLfBf6Hw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1qn0x93jhqjpqwvx5tgxnrwq5e3vuzur9whrkdnrvapd58esm45rqfkuxqh -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvV2oxUDNhZ0V2QnN1cmda\nV1dIemN2Wmw0Yy9HV2FGWlE1Uy84SzFpdEFZCkZ5OXBjQjVtVTNHZDI3N3hEZVdu\nQXBERUwzRXNNK2ZLSzQ2VzBQSXpsVlkKLS0tIEhONFlhUndudUxaTHUydVAyL0cz\nM2ZmbnNyelpZeCtWVGdDNTdMODV2cFkK9I+P52xf14oAqvkkAYCAAbAgGjpCcBN8\nvCndQ2Gq0jZImw1AukEZZMW7IRz2a5QNw999gscePD4OZYwp4IS+TA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmbnphMXVGZlFXcU1pTy9o\nYjVpVVp5MGZ4Z2xid0t2elNmRmQ3T29yOGhvCll5bU5oV0lZRTErSDQrYldFd2Rv\nUE9DMXFYS0dVbnl2Zld3OFRkYUtsY2MKLS0tIHpTOGNoY3VORk9YVmNYeHNkSWpl\nb0RydGxzdEJqVXI4K0ZhWWhlQ2NNbHMKCKUQprYNUTfSPtqmuuaom2wFZqVFvI4d\ne7SVxblts0M2qoLakDYtyqzEtdR8iVx0VAoMpiicFc996Wq/I976RQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1h86dek80u5t677tsparz395uk3zvz4yuj9m5t2v2nsdfsvyjmafsra5yt7 -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWi9rTnRDai9ubXFXM1Vp\nem44VThic01NbmV1MDd1MUJEYUlHY3VyM0VrCm5hWVo2bjByUUhBbFExM3Q4MG9P\nSUU3YjJlNWVUMnNYSTFHTTZPTlBQRlkKLS0tIHU3WWFZUW5lZVd4TWorUnB3RHNM\nTnMvQlNWaHREa200bHEvVU1PT1o2ZFEK4+wus0Y5pJCnqya+oeGrM4VwsCcrw9F0\n7qmk+O365K0j5qqp8zUNXNjIE4VIzr0YFie9lNmhasEQvTWg9nKXJg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEYldyT3NReFgzRHNucHBB\ncGROV0lqdUk0RFNQQzFhcXovQ09VWmhVRm5zClErbkZyem5EWkJzVFpTeGRaS0ZG\nekkyQWc0cFYxemtTT2ZsY3V6cHJkQkEKLS0tIE9ySnhBZVc1VEZhRUQ0ejIvR0Zv\nVW5ZTHVnRDZCcTFNMnpkWnF5MnRSZmMKx2uu4yYR+13L11cf6zhjg0P2aNBlhY8K\nnJdDfAbs48pcuV52e9gTO5rpJBVQzKJLNH9Cpju8BS54Pt2BBeZIKg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_lastmodified=2026-06-12T05:21:09Z -sops_mac=ENC[AES256_GCM,data:ie6zgLHwB1ALOseXsEvtIZfXlOy7VHSrRmHOpsysb+xei4RvdCGiMydkmeHw5BrcucWDQ4apRzaDq8kQybUKVpHLovsJeAomet2glrgdpZfY1KN6ygzzz9cZeFHsbLDEKgKTkQtczXjiW8u/lgv53NjPqdfNJkbMCycN0FBIfgA=,iv:bspmUkIJAyfAR1CVTclbjE4kaBleskFlI/bGFKO8hEo=,tag:E62niXY4HSWHLzXUuHFgvw==,type:str] +sops_lastmodified=2026-08-06T18:36:22Z +sops_mac=ENC[AES256_GCM,data:iqbPQFjO1v2790cSQ+3nUW8srSezQ/hmOXOLZaJIat6zI0il8Bs3d73yxYg+pFTbbVV0U0a3e8xmTeE29/IScRS5P1nr++Q/9xWSxGRSIGYPldGU9C8iFeoTfZjo9Ynt0WwQxA3MWr8GXweC6i1AbHiMFvJHwXsN5HJCeBpEWyE=,iv:VJSBT1HAmBfnLlN4n/jYRciTcCDlQA/LM/gMNzUO+ko=,tag:wFzG5y9ft5ZrEutnF1LPsw==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/.github/workflows/deploy-mirror.yml b/.github/workflows/deploy-mirror.yml index 2abf120..b65ae20 100644 --- a/.github/workflows/deploy-mirror.yml +++ b/.github/workflows/deploy-mirror.yml @@ -166,6 +166,7 @@ jobs: S3_ENDPOINT="${S3_ENDPOINT:-}" S3_REGION="${S3_REGION:-auto}" \ S3_ACCESS_KEY="${S3_ACCESS_KEY:-}" S3_SECRET_KEY="${S3_SECRET_KEY:-}" \ S3_BUCKET="${S3_BUCKET:-underlay}" \ + S3_PUBLIC_BUCKET="${S3_PUBLIC_BUCKET:-underlaypublic}" \ UNDERLAY_UPSTREAM_API_KEY="${UNDERLAY_UPSTREAM_API_KEY:-}" \ SESSION_SECRET="$(openssl rand -hex 32)" \ docker stack deploy -c docker-compose.yml \ diff --git a/.gitignore b/.gitignore index fe0ad2f..1390d50 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ Thumbs.db # Logs *.log npm-debug.log* + +# pnpm store cache (created by in-container installs) +.pnpm-store/ diff --git a/README.md b/README.md index f7208cf..b28cdc8 100644 --- a/README.md +++ b/README.md @@ -233,20 +233,30 @@ the same version hash as the simple one. | `GET .../versions/:semver/diff?from=...` | Diff between two versions | | `POST /api/records/batch` | Fetch records by hash (JSONL stream) | | `GET /api/records/:hash/provenance` | Find all collections containing a record | -| `POST .../fork` | Fork a collection (copies manifest, not data) | +| `POST .../fork` | Fork a collection (copies manifest, not data; 403 for a non-member if the source holds private content) | | `GET /api/schemas` | Search schemas across all collections | ## Privacy -Privacy is part of the protocol, not just a hosted-instance feature. It operates at three levels: +Privacy is part of the protocol, not just a hosted-instance feature. Four layers compose — a reader sees content only if it passes all four: +- **Private collections**: `collections.public = false` — the whole collection 404s for non-members, and nothing below is evaluated. ARK identifiers into a private collection refuse to resolve. - **Private types**: `"private": true` on a schema root hides all records of that type from public readers. - **Private fields**: `"private": true` on a schema property strips that field from public responses. -- **Private records**: `"private": true` on a record when pushing hides that specific record. +- **Private records**: `"private": true` on a **manifest entry** when pushing (not on the record body — a `private` key there is ignored) hides that specific record. -The `private` flag is not part of the record hash — a record's content identity doesn't change when you change who can see it. Each version has two hashes: a **private hash** (all content, used by owners for integrity) and a **public hash** (excludes private types, fields, and records, verifiable by anyone). +Record-level privacy is stored **per version**, on that version's reference to the record (`version_records.private`), never on the globally deduplicated record object — so two collections holding byte-identical content can disagree about privacy. Two consequences follow: **privacy is re-declared on every push (omitting the flag means public, not "unchanged")**, and **redaction is forward-only** — older versions are immutable and keep serving the record, and a file stays downloadable while any older version references it publicly. -Privacy filtering is implemented in `src/lib/core/privacy.ts` (pure functions) and enforced at the API layer in `src/api/versions.ts`. +The `private` flag is not part of the record hash — a record's content identity doesn't change when you change who can see it. Each version has two hashes: a **private hash** (all content, used by owners for integrity) and a **public hash** (excludes private types, fields, and records, verifiable by anyone). A version is identified by **both**: a push is a duplicate only when both match, which is what makes a privacy-only re-push a legitimate new version rather than a "no changes" conflict. + +Privacy filtering is implemented in `src/lib/core/privacy.ts` (pure functions) and enforced at the API layer in every module that returns record bodies, hashes, schemas, file lists, diffs, or counts: `versions.ts`, `collections.ts` (export, browse, fork), `records.ts`, `query.ts`, `files.ts`, `schemas.ts`, and `ark.ts`. + +### Access control + +- **API keys** carry `metadata.scope`. Because that metadata is client-supplied, self-service creation is clamped at `write` — requesting `admin` yields `write`, and admin keys are minted server-side only. +- **Collection-scoped keys** (share links, agent links) carry `metadata.collectionIds` and are confined to those collections; account- and org-level endpoints refuse them outright. +- **`?token=` capability URLs** authenticate `GET`/`HEAD` only, so a link prefetch can never drive a mutation. Share-link clients calling a POST send the token as a `Bearer` header. +- **Files** are never served directly from storage: every read is access-checked and answered with a short-lived presigned URL. ## Schema System @@ -304,9 +314,9 @@ Schemas can be labeled post-hoc with human-readable names or URIs (e.g. `schema. - **Hetzner** - Single box (8 vCPU, 16GB RAM) running Docker Swarm - **Caddy** - Host-level reverse proxy, TLS via `tls internal` (Cloudflare Full mode) - **Cloudflare** - DNS + CDN + DDoS protection -- **R2** - Object storage (zero egress fees), single bucket with prefixes: - - `files/` - Content-addressed immutable uploads - - `_backups/` - Compressed Postgres dumps +- **R2** - Object storage (zero egress fees), two buckets: + - `S3_BUCKET` (**private** — public access disabled, no custom domain): `files/` content-addressed immutable uploads, `_backups/` compressed Postgres dumps. Every read is a short-lived presigned URL minted after an access check; there is no stable public file URL. + - `S3_PUBLIC_BUCKET` (world-readable, fronted by `ASSETS_BASE_URL`): avatars and other inherently-public static assets. ### Stacks @@ -371,14 +381,16 @@ Supporting files live in `selfhost/` (Caddyfile, Postgres init script). See [/do ### S3 storage -| Variable | Description | -| ----------------- | --------------------------------------------------------------------------------------------------- | -| `S3_BUCKET` | S3 bucket name | -| `S3_REGION` | S3 region (`auto` for R2) | -| `S3_ENDPOINT` | S3 endpoint URL | -| `S3_ACCESS_KEY` | S3 access key | -| `S3_SECRET_KEY` | S3 secret key | -| `ASSETS_BASE_URL` | Public base URL for uploaded assets like avatars (optional, default: `https://assets.underlay.org`) | +| Variable | Description | +| ------------------------ | --------------------------------------------------------------------------------------------------- | +| `S3_BUCKET` | Private bucket for collection files and backups. Public access must be OFF | +| `S3_PUBLIC_BUCKET` | World-readable bucket for avatars and similar assets (default: `underlaypublic`) | +| `S3_PRESIGN_TTL_SECONDS` | Lifetime of presigned file-download URLs, in seconds (default: `300`) | +| `S3_REGION` | S3 region (`auto` for R2) | +| `S3_ENDPOINT` | S3 endpoint URL | +| `S3_ACCESS_KEY` | S3 access key | +| `S3_SECRET_KEY` | S3 secret key | +| `ASSETS_BASE_URL` | Public base URL for uploaded assets like avatars (optional, default: `https://assets.underlay.org`) | ### Auth (KF Auth OIDC) diff --git a/docker-compose.withauth.yml b/docker-compose.withauth.yml index 701dd51..22cd682 100644 --- a/docker-compose.withauth.yml +++ b/docker-compose.withauth.yml @@ -72,6 +72,7 @@ services: "AUTH_INTERNAL_API_KEY=$$INTERNAL_KEY" \ "DATABASE_URL=postgres://kfauth:kfauth@postgres:5432/app" \ "S3_BUCKET=underlay" \ + "S3_PUBLIC_BUCKET=underlaypublic" \ "S3_REGION=us-east-1" \ "S3_ENDPOINT=http://minio:9000" \ "S3_ACCESS_KEY=$$MINIO_ACCESS" \ @@ -153,8 +154,15 @@ services: - | set -a && . /config/.env.minio && set +a mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" + # Two buckets by design: + # underlay — PRIVATE. Collection files; every read goes through the + # API, which access-checks and then hands out a + # short-lived presigned URL. Never make this public. + # underlaypublic — world-readable assets (org avatars) served directly. mc mb --ignore-existing local/underlay - echo "Bucket ready." + mc mb --ignore-existing local/underlaypublic + mc anonymous set download local/underlaypublic + echo "Buckets ready." # --- Auth server (kf-auth) --- auth: diff --git a/package.json b/package.json index 3b98463..111e70a 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.750.0", + "@aws-sdk/s3-request-presigner": "^3.1104.0", "@better-auth/api-key": "^1.6.11", "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..aa0e02b --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,36 @@ +# @underlay/cli + +Source lives in `src/cli`; this package just bundles it (`pnpm --filter @underlay/cli build`). + +Currently **unpublished and not built** — runnable only from the repo via `pnpm cli`. + +## ⚠️ Review record privacy before publishing this for the first time + +The CLI predates per-version record privacy (`version_records.private`, added 2026-08) and +**cannot express it**. Publishing as-is would put a privacy-blind client in users' hands. + +Two concrete gaps, both must be closed first: + +1. **`src/cli/commands/push.ts` — manifest entries omit `private`.** The manifest is built as + `{ id, type, hash }`. The server takes each push's manifest as the authoritative statement of + which records are private, so a push that omits the flag marks every record public. Re-pushing + a collection that has private records would **silently publish them** in the new version. +2. **`src/cli/commands/add.ts` — `private` is dropped at ingest.** It parses only + `{ id, type, data }` and stores the canonical hashed object, which by design excludes privacy + (privacy is contextual, not part of the content hash). So the flag is lost before `push` could + ever send it. The local store needs somewhere to carry it — e.g. a per-record privacy set in + the version manifest (`src/cli/lib/store.ts`, `VersionManifest`), kept outside the hash. + +Also settle the server-side semantics this depends on before shipping: a manifest entry's +`private` is `z.boolean().optional()`, but ingest currently collapses it (`r.private ?? false`), +so **omitted means public**. If that becomes "omitted means inherit from the base version", the +CLI's obligations change. See `planning/reference-privacy-model.md`. + +## Also check before publishing + +- **The npm name `@underlay/cli` is already taken** by a 2023 package from the earlier Underlay + project ("CLI utility for downloading datasets specified in underlay.yaml", maintainers + `octref`, `joelg@mit.edu`, author "Knowledge Futures Group"). Publishing needs that account, a + version above `0.0.1`, or a different name. +- The CLI is several releases stale against the API — verify it against the current negotiate + protocol (chunked manifest, async commit) before shipping. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4837b1c..2963872 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@aws-sdk/client-s3': specifier: ^3.750.0 version: 3.1045.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1104.0 + version: 3.1104.0 '@better-auth/api-key': specifier: ^1.6.11 version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(better-auth@1.6.11(better-sqlite3@12.9.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.9.0)(kysely@0.28.17)(postgres@3.4.9)(sql.js@1.14.1))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vitest@4.1.6(@types/node@25.6.2)(happy-dom@20.9.0)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@6.4.2(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.9.0)))) @@ -146,10 +149,10 @@ importers: version: 17.0.4 oxfmt: specifier: latest - version: 0.50.0 + version: 0.62.0 oxlint: specifier: latest - version: 1.63.0 + version: 1.77.0 simple-git-hooks: specifier: ^2.13.1 version: 2.13.1 @@ -220,6 +223,10 @@ packages: resolution: {integrity: sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==} engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.977.6': + resolution: {integrity: sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/crc64-nvme@3.972.7': resolution: {integrity: sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==} engines: {node: '>=20.0.0'} @@ -304,10 +311,18 @@ packages: resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} engines: {node: '>=20.0.0'} + '@aws-sdk/s3-request-presigner@3.1104.0': + resolution: {integrity: sha512-udKZWozVoQUmOxzSqraSm1bPOlAPziRjMFpeX3UosDCWzMsqQvpl1/YMpwSuGvQ28W+tyE0Kl3JDziKPY/Lutg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.25': resolution: {integrity: sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==} engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.43': + resolution: {integrity: sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1041.0': resolution: {integrity: sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==} engines: {node: '>=20.0.0'} @@ -316,6 +331,10 @@ packages: resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-arn-parser@3.972.3': resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} engines: {node: '>=20.0.0'} @@ -344,10 +363,18 @@ packages: resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==} engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.37': + resolution: {integrity: sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} + '@aws/lambda-invoke-store@0.3.0': + resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1104,246 +1131,246 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} - '@oxfmt/binding-android-arm-eabi@0.50.0': - resolution: {integrity: sha512-ICXQVKrDvsWUtfx6EiVJxfWrajKTwTfRV8vz2XiMkxZeuCKJLgD4YAj6dE3BWvpqDlkVkie4VSTAtMUWO9LDXg==} + '@oxfmt/binding-android-arm-eabi@0.62.0': + resolution: {integrity: sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.50.0': - resolution: {integrity: sha512-quwjLQFkuW6OwLHeDeIXsTzOmipQFQbqsYN9HLk2B5I01IlAQZHP1UiLIg0O7pP+dUgPD2AD7SCYA3gs6NH5/g==} + '@oxfmt/binding-android-arm64@0.62.0': + resolution: {integrity: sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.50.0': - resolution: {integrity: sha512-ikU5umElcMi78/TNI334wtjr5WZ5F4nWa1aIDseAKKGL0W3ygxeYKkrIJ0fggWa8MOon66BmG3xCqmX1m9YAOw==} + '@oxfmt/binding-darwin-arm64@0.62.0': + resolution: {integrity: sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.50.0': - resolution: {integrity: sha512-WT4MOYG4mv9IXrH0m60vHsJh+rRMPSOKTQmwDpwmgQ+DuW/i5dU4pqc0HDO5uclO5vjz5IFX5z/taW86LSVe/g==} + '@oxfmt/binding-darwin-x64@0.62.0': + resolution: {integrity: sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.50.0': - resolution: {integrity: sha512-gH0rycVXqV4juWkvLs2uPMtTyppDc7qEUVzXAxnQ7FpcSZNXqKowUgtjH8q67ngj416r8+4NnAlyR/D35zwwhQ==} + '@oxfmt/binding-freebsd-x64@0.62.0': + resolution: {integrity: sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.50.0': - resolution: {integrity: sha512-wL/k+o0hiTeRvi/gPzeC1L/yTHTXIeHDKWU09s2zTBmv7ma59wTm+fADNSGYxhJQDxyavQbwTf1QpW3Zj924tQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': + resolution: {integrity: sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.50.0': - resolution: {integrity: sha512-Y59FKqoUM3Gf00E395b4ixfWyJGwO2GzaZawF5MZoVWcb3f6CkWUXyao0jyOvoIxDMzMybcVRuXyG7ih/Nxweg==} + '@oxfmt/binding-linux-arm-musleabihf@0.62.0': + resolution: {integrity: sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.50.0': - resolution: {integrity: sha512-OvXbfTjMignXWyJXg/NOFsiy996vFe8wb9tkxJaUq8ylq0XrzJg3ttavC5Tcmm6F8/GUs2r3XFJWWu9q/27uYw==} + '@oxfmt/binding-linux-arm64-gnu@0.62.0': + resolution: {integrity: sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.50.0': - resolution: {integrity: sha512-rqmvHZm7vMa3NLYa0khwkhReCmp9tqKnF23TFZ7S5cYJLvIE4b0k8famWE7kO897/DXznJe675n5SohFBggbxA==} + '@oxfmt/binding-linux-arm64-musl@0.62.0': + resolution: {integrity: sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.50.0': - resolution: {integrity: sha512-49bAdYbMSde42tzPDtuHnBWzOgmoS0PT9THCjvMnDVYMQYiHzPc2Mv5rkpBHVQOXM+PHfafJlxgK0anXSWBVvw==} + '@oxfmt/binding-linux-ppc64-gnu@0.62.0': + resolution: {integrity: sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.50.0': - resolution: {integrity: sha512-VFT25/6kckkIM62KeWB2bi+xCEmC/zC+DcMaIpEfaio8ulkGDLSiTz11TyK0eqgTl3x5OklYEGDWohvAgOr8Bw==} + '@oxfmt/binding-linux-riscv64-gnu@0.62.0': + resolution: {integrity: sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.50.0': - resolution: {integrity: sha512-BBJMuNy6jjkXjUUINF5UTQqb/nvjmtJad43Gp7bab0AAURAdthhJvduR7rHpWInpWYiaMzYsdrmURNcrmpxdZA==} + '@oxfmt/binding-linux-riscv64-musl@0.62.0': + resolution: {integrity: sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.50.0': - resolution: {integrity: sha512-Xd4y+yjAYHKmryXhyUUwbyRD01iKfcvI74iE01L6p4F8SwjhZQXDshK+T8PcrPZLiFqH263P5xqJk94amjkjzQ==} + '@oxfmt/binding-linux-s390x-gnu@0.62.0': + resolution: {integrity: sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.50.0': - resolution: {integrity: sha512-Qp96rYJru7l++7mk4R+eh8qq9GFfFAMdmoN6VGoRHI8AA1XMnUIzH4u+zOcKZZwY+irHdsaBldDearwB4nOH7A==} + '@oxfmt/binding-linux-x64-gnu@0.62.0': + resolution: {integrity: sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.50.0': - resolution: {integrity: sha512-5XLGp+yd5w2Key5LMqJO+X3XVsJKgeeUKljy32+MBF/J/JZ5m8WHl6dI5eOQOr3ixopxPiXIyDAxn3slI3UXiQ==} + '@oxfmt/binding-linux-x64-musl@0.62.0': + resolution: {integrity: sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.50.0': - resolution: {integrity: sha512-QAxwzh7+GHugCD7WuERolVs8TKQwXNIAZXAHHTecbKVc9oWBkWzOiLauQuezXS57tVcof5zhi1IjZ8tOV0htTg==} + '@oxfmt/binding-openharmony-arm64@0.62.0': + resolution: {integrity: sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.50.0': - resolution: {integrity: sha512-3nKN/kqClm9iCFWTwtJ9UpR5SGyExp5l3nw6uIiBt+3XitQtszin+vjHrL7JHfDksZ7Svigdaow2zqz/IKCfqw==} + '@oxfmt/binding-win32-arm64-msvc@0.62.0': + resolution: {integrity: sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.50.0': - resolution: {integrity: sha512-3r6XZ8+X6qlLbXaPW2NygfiAWSpKbkE36pAVzS83mY+cYY+pSMalJ+qnCgkr92tr+Iqv988XKQ1CpARTg9ITbQ==} + '@oxfmt/binding-win32-ia32-msvc@0.62.0': + resolution: {integrity: sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.50.0': - resolution: {integrity: sha512-BSE8D8KsvquMG9vU+Qt4qGuoOcZ36rxU5S6ZkHNguj+MlWkXWCBETnno3yJ9CfWvfCrbmieaN9LK6hdcdHNZ/w==} + '@oxfmt/binding-win32-x64-msvc@0.62.0': + resolution: {integrity: sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.63.0': - resolution: {integrity: sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg==} + '@oxlint/binding-android-arm-eabi@1.77.0': + resolution: {integrity: sha512-E06sKWS6PiI6HRxS1wyQg22HvApt01hI7fV+T3wUk3OSbaaP4a3hYGY/MIQDmASqCiRjBdpRQYkgMkqH82cWmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.63.0': - resolution: {integrity: sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A==} + '@oxlint/binding-android-arm64@1.77.0': + resolution: {integrity: sha512-NvsKz0KZxTp9cYWPLf+FXaSZwB3oO3peAjtukpOMBgse2vhQSoIIVqeO1yR0lEo/UcdZIDL18uq+kL0LzQ0ytA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.63.0': - resolution: {integrity: sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g==} + '@oxlint/binding-darwin-arm64@1.77.0': + resolution: {integrity: sha512-bgjTn6nW4bQCFBvSvuHCpDD+sONvmpo4lGI4PxzMt1quBA+xYxhczk6RiCn3GZ9gY8uhaBbwhj9MdKGfu6T9DA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.63.0': - resolution: {integrity: sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw==} + '@oxlint/binding-darwin-x64@1.77.0': + resolution: {integrity: sha512-aotaIttH1R6j1Rwhx0M0htgeZyGtVQqYNTVEYMN/UcgHPquGA6kmk9OyuDc3a2GKUQBC+3C3GVQCcrRPMYqAFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.63.0': - resolution: {integrity: sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA==} + '@oxlint/binding-freebsd-x64@1.77.0': + resolution: {integrity: sha512-nNx/wta7ksRAdYvq+l4AWjXkLxEXHALhENxjj2cYbQAIR4ybaA5L+hCbE63HOmft5czQ6ks+hb8vmEAnn7YGPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.63.0': - resolution: {integrity: sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w==} + '@oxlint/binding-linux-arm-gnueabihf@1.77.0': + resolution: {integrity: sha512-tMLLjM7xXtzXisVCzkOTXNCy9bZVId2wteNwjohlFDR/jY6WagpEDA1c1wu4xRc20Hojaxj+V6DSR7gbKxijWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.63.0': - resolution: {integrity: sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg==} + '@oxlint/binding-linux-arm-musleabihf@1.77.0': + resolution: {integrity: sha512-MiAFDFaqR0tmHTAyo0YDcZ5hyLREdYw/RQhc2R3cbT+8O3tB+zqPM2th9TTQ+Uo3jn/embS+DO+HyX9ztCPkOQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.63.0': - resolution: {integrity: sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ==} + '@oxlint/binding-linux-arm64-gnu@1.77.0': + resolution: {integrity: sha512-/xqQ3B16i1T4cyt/9Mn+4CpzhUXoBXp7kVpIwzOXNFLj5JmK1bIjsbSnX296Gg8A/o7oDtKWikFgBx0SLwztkw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.63.0': - resolution: {integrity: sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g==} + '@oxlint/binding-linux-arm64-musl@1.77.0': + resolution: {integrity: sha512-LSbwuRKiNCenPDcbARqAZ5RfBy7gmj7vOvfJRLeCDU3gFtSxWbhv/+VTlaUqzUhNj1gFLHB8h7ALnxa/Az6z6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.63.0': - resolution: {integrity: sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ==} + '@oxlint/binding-linux-ppc64-gnu@1.77.0': + resolution: {integrity: sha512-QWdcH31mXEUe5Nq1s0CfCpceaKjIo9uZtwDjAuL681g1axf+5x8xrg/eXWaw//4NCxYZ4V4e5Hu5tvdR+pTBlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.63.0': - resolution: {integrity: sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA==} + '@oxlint/binding-linux-riscv64-gnu@1.77.0': + resolution: {integrity: sha512-GnOfYgJxbcElOiPZaDFDl406ONddwvOWk2jvAAAEjwAl4GofNoHF+/HHUIBYa6bFCArlcGPi0XjC4cU1pkgF/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.63.0': - resolution: {integrity: sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ==} + '@oxlint/binding-linux-riscv64-musl@1.77.0': + resolution: {integrity: sha512-AyEMTUCf0xY+hHF+IxqXFQIX0yQOIR8ykpY0lJNOw9xYqOzUX8dyZfRvlG0RfXwuQn2eonf/8NrMmDSZJjdqsA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.63.0': - resolution: {integrity: sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg==} + '@oxlint/binding-linux-s390x-gnu@1.77.0': + resolution: {integrity: sha512-sPLzEcNvxd/oyVQ5oZo92CiHkFkpBeRop13E/P3TPY+hZfXHKCOWKI70TE2RYwMKFJDc20EMjH16L7NZICtKTw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.63.0': - resolution: {integrity: sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA==} + '@oxlint/binding-linux-x64-gnu@1.77.0': + resolution: {integrity: sha512-1Oh2ssH2L7lwyvkdSqaMUfsGfwU2Wfvew+obBUYjRVqhpBcUpwnsPSEr1IzVi9XqkuY10geiLsNKecqaZC34Dw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.63.0': - resolution: {integrity: sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A==} + '@oxlint/binding-linux-x64-musl@1.77.0': + resolution: {integrity: sha512-0j/2wRgNGO+Qj/M1uu/p57h/hFTTWWcfie0ufkbabeus2s5+/QqkCflnMOwLLN5m2GsNeWp4xdl4cPa4n7QCOQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.63.0': - resolution: {integrity: sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w==} + '@oxlint/binding-openharmony-arm64@1.77.0': + resolution: {integrity: sha512-BJ/j54qS0usEnyDkLYURMj2iiD9h5Cyy+ppzeMSXBGRXaGRNWnj1Mw14NqWMR5E/PzdgB30OOCCzLzbRoduafw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.63.0': - resolution: {integrity: sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg==} + '@oxlint/binding-win32-arm64-msvc@1.77.0': + resolution: {integrity: sha512-Yh8w+g2Lpx7StrvtYkoz9JJvXjB9wxgFChFNb85nrXm/wj/XTwGWS1hve9+900HL7llrntYB3YP+y32E3tRqzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.63.0': - resolution: {integrity: sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w==} + '@oxlint/binding-win32-ia32-msvc@1.77.0': + resolution: {integrity: sha512-zja5b7+6a7UsRFgAQSrnax5vrzliEyNPLCjfXONu/vTWswaIVZGFajJZptaeRvPE4LghtFdAzVFlexTm7MVTGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.63.0': - resolution: {integrity: sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA==} + '@oxlint/binding-win32-x64-msvc@1.77.0': + resolution: {integrity: sha512-+teyvPDZ2RjUvo+SuCqS/UhaJl1QtdW5fWT5NJTV61V5MIuIS90Db9LixmtEGvXixyttiK62P96MSu3UlpviBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1531,6 +1558,10 @@ packages: resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} engines: {node: '>=18.0.0'} + '@smithy/core@3.31.1': + resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.14': resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} engines: {node: '>=18.0.0'} @@ -1643,6 +1674,10 @@ packages: resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.6.12': + resolution: {integrity: sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.12.13': resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} engines: {node: '>=18.0.0'} @@ -1651,6 +1686,10 @@ packages: resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.14': resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} engines: {node: '>=18.0.0'} @@ -2671,25 +2710,31 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - oxfmt@0.50.0: - resolution: {integrity: sha512-owwjTnhfM5aCOJhYeqDvk7iM504OeYFZpdRU7cxx7xtZMo4uVpjlryTUon+Cf76CugsvnqA32e6rC73pr1hXaw==} + oxfmt@0.62.0: + resolution: {integrity: sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: svelte: ^5.0.0 + vite-plus: '*' peerDependenciesMeta: svelte: optional: true + vite-plus: + optional: true - oxlint@1.63.0: - resolution: {integrity: sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg==} + oxlint@1.77.0: + resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.22.1' + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: optional: true + vite-plus: + optional: true parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -3301,6 +3346,17 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@aws-sdk/core@3.977.6': + dependencies: + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.37 + '@aws/lambda-invoke-store': 0.3.0 + '@smithy/core': 3.31.1 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + bowser: 2.14.1 + tslib: 2.8.1 + '@aws-sdk/crc64-nvme@3.972.7': dependencies: '@smithy/types': 4.14.1 @@ -3557,6 +3613,15 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/s3-request-presigner@3.1104.0': + dependencies: + '@aws-sdk/core': 3.977.6 + '@aws-sdk/signature-v4-multi-region': 3.996.43 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.25': dependencies: '@aws-sdk/middleware-sdk-s3': 3.972.37 @@ -3566,6 +3631,13 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/signature-v4-multi-region@3.996.43': + dependencies: + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.12 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1041.0': dependencies: '@aws-sdk/core': 3.974.8 @@ -3583,6 +3655,11 @@ snapshots: '@smithy/types': 4.14.1 tslib: 2.8.1 + '@aws-sdk/types@3.974.2': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws-sdk/util-arn-parser@3.972.3': dependencies: tslib: 2.8.1 @@ -3622,8 +3699,15 @@ snapshots: fast-xml-parser: 5.7.2 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.37': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.2.4': {} + '@aws/lambda-invoke-store@0.3.0': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4167,118 +4251,118 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} - '@oxfmt/binding-android-arm-eabi@0.50.0': + '@oxfmt/binding-android-arm-eabi@0.62.0': optional: true - '@oxfmt/binding-android-arm64@0.50.0': + '@oxfmt/binding-android-arm64@0.62.0': optional: true - '@oxfmt/binding-darwin-arm64@0.50.0': + '@oxfmt/binding-darwin-arm64@0.62.0': optional: true - '@oxfmt/binding-darwin-x64@0.50.0': + '@oxfmt/binding-darwin-x64@0.62.0': optional: true - '@oxfmt/binding-freebsd-x64@0.50.0': + '@oxfmt/binding-freebsd-x64@0.62.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.50.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.50.0': + '@oxfmt/binding-linux-arm-musleabihf@0.62.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.50.0': + '@oxfmt/binding-linux-arm64-gnu@0.62.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.50.0': + '@oxfmt/binding-linux-arm64-musl@0.62.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.50.0': + '@oxfmt/binding-linux-ppc64-gnu@0.62.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.50.0': + '@oxfmt/binding-linux-riscv64-gnu@0.62.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.50.0': + '@oxfmt/binding-linux-riscv64-musl@0.62.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.50.0': + '@oxfmt/binding-linux-s390x-gnu@0.62.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.50.0': + '@oxfmt/binding-linux-x64-gnu@0.62.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.50.0': + '@oxfmt/binding-linux-x64-musl@0.62.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.50.0': + '@oxfmt/binding-openharmony-arm64@0.62.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.50.0': + '@oxfmt/binding-win32-arm64-msvc@0.62.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.50.0': + '@oxfmt/binding-win32-ia32-msvc@0.62.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.50.0': + '@oxfmt/binding-win32-x64-msvc@0.62.0': optional: true - '@oxlint/binding-android-arm-eabi@1.63.0': + '@oxlint/binding-android-arm-eabi@1.77.0': optional: true - '@oxlint/binding-android-arm64@1.63.0': + '@oxlint/binding-android-arm64@1.77.0': optional: true - '@oxlint/binding-darwin-arm64@1.63.0': + '@oxlint/binding-darwin-arm64@1.77.0': optional: true - '@oxlint/binding-darwin-x64@1.63.0': + '@oxlint/binding-darwin-x64@1.77.0': optional: true - '@oxlint/binding-freebsd-x64@1.63.0': + '@oxlint/binding-freebsd-x64@1.77.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.63.0': + '@oxlint/binding-linux-arm-gnueabihf@1.77.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.63.0': + '@oxlint/binding-linux-arm-musleabihf@1.77.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.63.0': + '@oxlint/binding-linux-arm64-gnu@1.77.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.63.0': + '@oxlint/binding-linux-arm64-musl@1.77.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.63.0': + '@oxlint/binding-linux-ppc64-gnu@1.77.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.63.0': + '@oxlint/binding-linux-riscv64-gnu@1.77.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.63.0': + '@oxlint/binding-linux-riscv64-musl@1.77.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.63.0': + '@oxlint/binding-linux-s390x-gnu@1.77.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.63.0': + '@oxlint/binding-linux-x64-gnu@1.77.0': optional: true - '@oxlint/binding-linux-x64-musl@1.63.0': + '@oxlint/binding-linux-x64-musl@1.77.0': optional: true - '@oxlint/binding-openharmony-arm64@1.63.0': + '@oxlint/binding-openharmony-arm64@1.77.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.63.0': + '@oxlint/binding-win32-arm64-msvc@1.77.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.63.0': + '@oxlint/binding-win32-ia32-msvc@1.77.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.63.0': + '@oxlint/binding-win32-x64-msvc@1.77.0': optional: true '@rolldown/pluginutils@1.0.0-rc.3': {} @@ -4416,6 +4500,11 @@ snapshots: '@smithy/uuid': 1.1.2 tslib: 2.8.1 + '@smithy/core@3.31.1': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.14': dependencies: '@smithy/node-config-provider': 4.3.14 @@ -4598,6 +4687,12 @@ snapshots: '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 + '@smithy/signature-v4@5.6.12': + dependencies: + '@smithy/core': 3.31.1 + '@smithy/types': 4.16.1 + tslib: 2.8.1 + '@smithy/smithy-client@4.12.13': dependencies: '@smithy/core': 3.23.17 @@ -4612,6 +4707,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + '@smithy/url-parser@4.2.14': dependencies: '@smithy/querystring-parser': 4.2.14 @@ -5542,51 +5641,51 @@ snapshots: dependencies: mimic-function: 5.0.1 - oxfmt@0.50.0: + oxfmt@0.62.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.50.0 - '@oxfmt/binding-android-arm64': 0.50.0 - '@oxfmt/binding-darwin-arm64': 0.50.0 - '@oxfmt/binding-darwin-x64': 0.50.0 - '@oxfmt/binding-freebsd-x64': 0.50.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.50.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.50.0 - '@oxfmt/binding-linux-arm64-gnu': 0.50.0 - '@oxfmt/binding-linux-arm64-musl': 0.50.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.50.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.50.0 - '@oxfmt/binding-linux-riscv64-musl': 0.50.0 - '@oxfmt/binding-linux-s390x-gnu': 0.50.0 - '@oxfmt/binding-linux-x64-gnu': 0.50.0 - '@oxfmt/binding-linux-x64-musl': 0.50.0 - '@oxfmt/binding-openharmony-arm64': 0.50.0 - '@oxfmt/binding-win32-arm64-msvc': 0.50.0 - '@oxfmt/binding-win32-ia32-msvc': 0.50.0 - '@oxfmt/binding-win32-x64-msvc': 0.50.0 - - oxlint@1.63.0: + '@oxfmt/binding-android-arm-eabi': 0.62.0 + '@oxfmt/binding-android-arm64': 0.62.0 + '@oxfmt/binding-darwin-arm64': 0.62.0 + '@oxfmt/binding-darwin-x64': 0.62.0 + '@oxfmt/binding-freebsd-x64': 0.62.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.62.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.62.0 + '@oxfmt/binding-linux-arm64-gnu': 0.62.0 + '@oxfmt/binding-linux-arm64-musl': 0.62.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.62.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.62.0 + '@oxfmt/binding-linux-riscv64-musl': 0.62.0 + '@oxfmt/binding-linux-s390x-gnu': 0.62.0 + '@oxfmt/binding-linux-x64-gnu': 0.62.0 + '@oxfmt/binding-linux-x64-musl': 0.62.0 + '@oxfmt/binding-openharmony-arm64': 0.62.0 + '@oxfmt/binding-win32-arm64-msvc': 0.62.0 + '@oxfmt/binding-win32-ia32-msvc': 0.62.0 + '@oxfmt/binding-win32-x64-msvc': 0.62.0 + + oxlint@1.77.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.63.0 - '@oxlint/binding-android-arm64': 1.63.0 - '@oxlint/binding-darwin-arm64': 1.63.0 - '@oxlint/binding-darwin-x64': 1.63.0 - '@oxlint/binding-freebsd-x64': 1.63.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.63.0 - '@oxlint/binding-linux-arm-musleabihf': 1.63.0 - '@oxlint/binding-linux-arm64-gnu': 1.63.0 - '@oxlint/binding-linux-arm64-musl': 1.63.0 - '@oxlint/binding-linux-ppc64-gnu': 1.63.0 - '@oxlint/binding-linux-riscv64-gnu': 1.63.0 - '@oxlint/binding-linux-riscv64-musl': 1.63.0 - '@oxlint/binding-linux-s390x-gnu': 1.63.0 - '@oxlint/binding-linux-x64-gnu': 1.63.0 - '@oxlint/binding-linux-x64-musl': 1.63.0 - '@oxlint/binding-openharmony-arm64': 1.63.0 - '@oxlint/binding-win32-arm64-msvc': 1.63.0 - '@oxlint/binding-win32-ia32-msvc': 1.63.0 - '@oxlint/binding-win32-x64-msvc': 1.63.0 + '@oxlint/binding-android-arm-eabi': 1.77.0 + '@oxlint/binding-android-arm64': 1.77.0 + '@oxlint/binding-darwin-arm64': 1.77.0 + '@oxlint/binding-darwin-x64': 1.77.0 + '@oxlint/binding-freebsd-x64': 1.77.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.77.0 + '@oxlint/binding-linux-arm-musleabihf': 1.77.0 + '@oxlint/binding-linux-arm64-gnu': 1.77.0 + '@oxlint/binding-linux-arm64-musl': 1.77.0 + '@oxlint/binding-linux-ppc64-gnu': 1.77.0 + '@oxlint/binding-linux-riscv64-gnu': 1.77.0 + '@oxlint/binding-linux-riscv64-musl': 1.77.0 + '@oxlint/binding-linux-s390x-gnu': 1.77.0 + '@oxlint/binding-linux-x64-gnu': 1.77.0 + '@oxlint/binding-linux-x64-musl': 1.77.0 + '@oxlint/binding-openharmony-arm64': 1.77.0 + '@oxlint/binding-win32-arm64-msvc': 1.77.0 + '@oxlint/binding-win32-ia32-msvc': 1.77.0 + '@oxlint/binding-win32-x64-msvc': 1.77.0 parse5@8.0.1: dependencies: diff --git a/public/llms.txt b/public/llms.txt index 8309cc1..32a2b13 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -76,8 +76,9 @@ GET /api/collections/:owner/:slug/versions/:semver/records.ndjson → ALL record GET /api/collections/:owner/:slug/versions/:semver/records → records for a version (?type=TypeName&limit=100&after=recordId) GET /api/collections/:owner/:slug/versions/:semver/manifest → manifest: record ids/types/hashes + file hashes + schema hashes (?since=v1.0.0 for delta) GET /api/collections/:owner/:slug/versions/:semver/files → list files for a version (hash, size, content type) -GET /api/collections/:owner/:slug/files/:hash → download a file by hash -HEAD /api/collections/:owner/:slug/files/:hash → check if a file exists (returns Content-Length, Content-Type) +GET /api/collections/:owner/:slug/files/:hash → download a file: access-checked, then 302-redirects to a short-lived presigned URL (follow it, e.g. curl -L). Public files are anonymous; private needs a session or a Bearer share/agent token. This API path is the durable locator — the redirect target is ephemeral, never persist it. +HEAD /api/collections/:owner/:slug/files/:hash → check if a file exists/is accessible (returns Content-Length, Content-Type) +POST /api/collections/:owner/:slug/files/presign → presign many files at once: body {"hashes":[...]} → {hash: presignedUrl | null}. One round trip for a page full of files. ### Records (global, content-addressed) GET /api/records/:hash/provenance → find all collections/versions containing this record hash @@ -110,7 +111,7 @@ UI feature rather than a documented API; on a large collection use records.ndjso which hydrates a collection into a queryable database built for the purpose. ### Fork -POST /api/collections/:owner/:slug/fork → fork collection into caller's org (requires write auth) +POST /api/collections/:owner/:slug/fork → fork collection into caller's org (requires write auth; a collection-scoped key is refused). 403 if you are NOT a member of the source org and the source's latest version holds any private record, private type, or private field — a fork copies the full record bodies, and there is no redacted-fork path. Body: { "targetOrg": "my-org", "slug": "optional-new-slug" } Creates a new collection under targetOrg with the source's latest version. Records, schemas, and files are referenced (not copied) — zero additional storage. @@ -234,7 +235,8 @@ hashing, or the server will reject the records. Field reference: - base_version: the semver string of the version you diffed against (e.g. "v1.2.0"). null for first push. Used for optimistic locking. - schemas: per-type JSON Schema map. Required on every push. -- manifest: array of {id, type, hash} for every record in the new version. Capped at 500,000 entries — above that, upload it in chunks instead (see 4a-chunked below). Omit when using manifest_expected. +- manifest: array of {id, type, hash, private?} for every record in the new version. Capped at 500,000 entries — above that, upload it in chunks instead (see 4a-chunked below). Omit when using manifest_expected. +- private (per manifest entry): optional boolean. true hides that record from non-owners in THIS version. OMITTING IT MEANS PUBLIC — it is not inherited from the base version and must be re-sent on every push. Applies identically to inline manifests and JSONL manifest chunks. See "Private Records" below. - manifest_expected: number of distinct record hashes you will upload in chunks. Mutually exclusive with manifest; sending both returns 400. - files: array of file hashes (SHA-256 hex strings) referenced by records. - metadata: optional JSON object for version metadata (description, readme, license, etc.). Merged with previous version's metadata. @@ -494,9 +496,14 @@ Guarantees you can rely on: Verify completeness yourself. A stream that dies halfway cannot report an error: the 200 status and headers were already sent. Count the lines and compare against -X-Underlay-Record-Count (or the version's recordCount). If they differ, resume with +X-Underlay-Record-Count. If they differ, resume with ?after= rather than starting over. +X-Underlay-Record-Count is the count for THIS request — privacy-filtered for your access +level and scoped to ?type= if you passed one — so the comparison is exact for every caller. +Do NOT compare against the version's `recordCount`: that is the full total and includes +private records and private types you may not be receiving. + That resume behaviour makes this strictly better than paging for bulk reads: the same recovery from a dropped connection, at a fraction of the requests. @@ -672,8 +679,14 @@ GET /api/accounts/:owner/collections → list collections for an o ## Privacy & Visibility -Underlay supports fine-grained privacy at three levels: types, fields, and individual records. -Private data is stored alongside public data in the same version but is only visible to the collection owner. +Underlay supports privacy at four levels that compose — a reader sees content only if it passes all of them: + 1. Collection (collections.public) — a private collection 404s entirely; nothing below is evaluated. + 2. Type — "private": true on the type's schema. Per-version. + 3. Field — "private": true on the property. Per-version. + 4. Record — "private": true on the negotiate MANIFEST ENTRY. Per-version. +Levels 2-4 are bound to the version, so the same content can be public in one collection's version +and hidden in another's. Private data is stored alongside public data in the same version and is +visible only to members of the owning organization. ### Private Types Mark an entire type as private in its schema. All records of that type are hidden from public readers. @@ -707,22 +720,56 @@ Mark individual fields as private within a type's schema. The type itself is vis Public readers see Author records with only "name". The owner sees all fields. ### Private Records -Mark individual records as private in the negotiate manifest. The type and schema are visible, but that specific record is hidden. +Mark individual records as private on their MANIFEST ENTRY in the negotiate body. The type and +schema stay visible; that specific record is hidden from non-owners. "manifest": [ {"id": "article-1", "type": "Article", "hash": ""}, {"id": "article-2", "type": "Article", "hash": "", "private": true} ] -article-2 is only visible to the collection owner. Public readers see article-1 only. +article-2 is only visible to members of the owning org. Public readers see article-1 only. + +Two rules you must design around: + +1. PRIVACY IS PER-VERSION AND MUST BE RE-DECLARED ON EVERY PUSH. The flag is stored on the + (version, record) edge, not on the record itself. Omitting `private` on a manifest entry + means PUBLIC — it does NOT inherit the previous version's value. A push that sends a full + manifest without the flags publishes everything it omits. (Dropping the flag is therefore + also how you deliberately un-hide a record.) Read the current flags back from + GET .../versions/:semver/manifest, which echoes `private: true` on the entries that have it. +2. REDACTION IS FORWARD-ONLY. Marking a record private in v2 hides it in v2 only. Versions are + immutable, so v1 still serves that record at /versions/v1.0.0/records. There is no + retroactive purge. Files are looser still: file access resolves across ALL ready versions, + so a file referenced publicly in v1 stays downloadable after the referencing record is + redacted in v2. + +`private` belongs ONLY on the manifest entry. A `private` key on a record BODY sent in step 4b is +parsed and silently ignored — no error, and the record ships public. + +The same content can be private in one collection and public in another: the flag lives on the +version edge, not on the globally deduplicated record body. ### How it works -- Public hash: computed from public content only (excludes private types, records, fields, and metadata) +- Public hash: the digest of the public projection — private types omitted, private records + omitted, private fields stripped from the records that remain. Version metadata and the file + list are NOT filtered: both digests are computed over the same metadata and the same files. +- Version identity is BOTH digests. A push is a duplicate (409 "No changes detected") only if + `hash` AND `public_hash` match an existing version. Privacy is deliberately not folded into + `hash`, so `hash` stays independently verifiable from the content; privacy moves `public_hash` + instead. That is why re-pushing byte-identical content with a record newly marked private is a + legitimate new version — it is what makes redaction-in-place possible. Converse: flagging a + record private whose TYPE is already private changes neither digest and is correctly rejected + as a duplicate, because the flag has no observable effect. +- A privacy-only push is a PATCH bump (schema change → major, record-set change → minor, + everything else → patch). - Public record hash: a record of a type with private fields is listed in public manifests under the hash of its filtered projection ({"id", "type", "data"} with private fields stripped). Record endpoints resolve either address; hashing the document you receive always reproduces the address you requested. -- Private hash: computed from all content including metadata (used by the owner for integrity verification) +- Private hash: the full digest over ALL schema and record hashes, plus files and metadata. + Served only to org members; everyone else receives public_hash in the `hash` field. Both are + prefixed — "private:<64hex>" and "public:<64hex>" — so the two forms are never confusable. - Schema filtering: the schema returned to public readers omits private types and private fields - Record filtering: queries by non-owners automatically exclude private records and strip private fields diff --git a/server.ts b/server.ts index 950faf4..fc39e0c 100644 --- a/server.ts +++ b/server.ts @@ -6,6 +6,7 @@ import { getRequestListener, serve } from '@hono/node-server' import { serveStatic } from '@hono/node-server/serve-static' import { Scalar } from '@scalar/hono-api-reference' import { Hono } from 'hono' +import type { MiddlewareHandler } from 'hono' import { createOpenApiDocument } from 'hono-zod-openapi' import { compress } from 'hono/compress' import { cors } from 'hono/cors' @@ -151,9 +152,11 @@ app.use('/api/admin/mirror/*', async (c, next) => { ) }) -// Steward admin: requires kfRole === 'admin' -// Steward admin: requires kfRole === 'admin' -app.use('/api/admin/explore-*', async (c, next) => { +// Steward admin: requires kfRole === 'admin'. +// NOTE: a wildcard like '/api/admin/explore-*' does NOT match in Hono (wildcards +// are segment-level), which previously left these routes ungated. Register the +// guard on the exact paths instead. +const requireSteward: MiddlewareHandler = 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) @@ -161,7 +164,9 @@ app.use('/api/admin/explore-*', async (c, next) => { return c.json({ error: 'Forbidden', statusCode: 403 }, 403) } return next() -}) +} +app.use('/api/admin/explore-tags', requireSteward) +app.use('/api/admin/explore-collections', requireSteward) // --- ARK resolution middleware --- app.use('/:arkpath{ark:.*}', arkMiddleware) @@ -189,6 +194,8 @@ app.on(['GET', 'POST'], '/api/auth/*', async (c) => { const url = new URL(c.req.url) const isCallback = url.pathname.includes('/callback/') if (isCallback) { + // Never log the raw URL (carries the OAuth `code`/`state`) or cookie/set-cookie + // values (carry the session token) — anyone with log access could replay them. console.log('[auth callback] incoming:', { method: c.req.method, path: url.pathname, @@ -196,16 +203,14 @@ app.on(['GET', 'POST'], '/api/auth/*', async (c) => { 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)), + hasLocation: !!res.headers.get('location'), + setCookieCount: res.headers.getSetCookie?.()?.length ?? 0, }) } return res @@ -232,16 +237,12 @@ app.get('/login', async (c, next) => { }), ) const body = await authRes.json() - console.log('[login] auth sign-in response:', { status: authRes.status, body }) + console.log('[login] auth sign-in response:', { status: authRes.status, hasUrl: !!body.url }) if (body.url) { const redirect = new Response(null, { status: 302, headers: { Location: body.url } }) 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 } } diff --git a/src/api/accounts.ts b/src/api/accounts.ts index 70d94d6..59e2dc4 100644 --- a/src/api/accounts.ts +++ b/src/api/accounts.ts @@ -4,13 +4,13 @@ import { openApi } from 'hono-zod-openapi' import { z } from 'zod' import { db, schema } from '../db/client.server.js' -import { deleteS3Objects, listS3Objects, uploadToS3 } from '../lib/s3.js' +import { deletePublicAssets, listPublicAssets, uploadPublicAsset } from '../lib/s3.js' import type { AuthEnv } from './auth.server.js' -import { requireAuth } from './auth.server.js' +import { requireAuth, requireUnscopedKey } from './auth.server.js' const ASSETS_BASE_URL = process.env.ASSETS_BASE_URL ?? 'https://assets.underlay.org' -const RESERVED_SLUGS = new Set([ +export const RESERVED_SLUGS = new Set([ 'explore', 'docs', 'connect', @@ -75,6 +75,7 @@ const app = new Hono() .get( '/me', requireAuth(), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Get current user profile', @@ -116,6 +117,7 @@ const app = new Hono() .get( '/available-kf-orgs', requireAuth(), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'List KF orgs available to link', @@ -199,7 +201,8 @@ const app = new Hono() ) .patch( '/me', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Update own profile', @@ -256,7 +259,8 @@ const app = new Hono() ) .post( '/:slug/avatar', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Upload organization avatar', @@ -297,7 +301,7 @@ const app = new Hono() const ext = file.type.split('/')[1] === 'jpeg' ? 'jpg' : file.type.split('/')[1] const key = `avatars/${org.id}/${Date.now()}.${ext}` - await uploadToS3(key, buffer, file.type) + await uploadPublicAsset(key, buffer, file.type) await db .update(schema.organization) @@ -309,7 +313,8 @@ const app = new Hono() ) .delete( '/me', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Delete own account', @@ -334,8 +339,8 @@ const app = new Hono() } try { - const avatarKeys = await listS3Objects(`avatars/${defaultOrg.id}/`) - if (avatarKeys.length > 0) await deleteS3Objects(avatarKeys) + const avatarKeys = await listPublicAssets(`avatars/${defaultOrg.id}/`) + if (avatarKeys.length > 0) await deletePublicAssets(avatarKeys) } catch (err) { // Non-fatal — orphaned avatars are harmless console.error(`[accounts] Failed to delete avatars for org ${defaultOrg.id}:`, err) @@ -354,7 +359,8 @@ const app = new Hono() ) .patch( '/:slug', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Update an organization by slug', @@ -395,6 +401,21 @@ const app = new Hono() } } + // kfOrgId links this org to a Knowledge Futures org and is trusted by the + // service-to-service summary endpoint, so a caller may only set one they + // are actually entitled to — otherwise any owner could claim another + // institution's identity. + if (kfOrgId !== undefined && kfOrgId !== null && kfOrgId !== '') { + const { resolveUserKfOrgs } = await import('../lib/auth-internal.server.js') + const allowed = await resolveUserKfOrgs(userId, db, schema) + if (!allowed.some((o: { id: string }) => o.id === kfOrgId)) { + return c.json( + { error: 'You are not a member of that KF organization', statusCode: 403 }, + 403, + ) + } + } + const updates: Record = {} if (slug !== undefined) updates.slug = slug if (displayName !== undefined) updates.name = displayName @@ -412,7 +433,8 @@ const app = new Hono() ) .delete( '/:slug', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Accounts'], summary: 'Delete an organization by slug', @@ -434,8 +456,8 @@ const app = new Hono() } try { - const avatarKeys = await listS3Objects(`avatars/${org.id}/`) - if (avatarKeys.length > 0) await deleteS3Objects(avatarKeys) + const avatarKeys = await listPublicAssets(`avatars/${org.id}/`) + if (avatarKeys.length > 0) await deletePublicAssets(avatarKeys) } catch (err) { // Non-fatal — orphaned avatars are harmless console.error(`[accounts] Failed to delete avatars for org ${org.id}:`, err) diff --git a/src/api/agent.ts b/src/api/agent.ts index 2707c22..accb21c 100644 --- a/src/api/agent.ts +++ b/src/api/agent.ts @@ -3,7 +3,11 @@ import type { Context } from 'hono' import { db, schema } from '../db/client.server.js' import { auth } from '../lib/auth.js' -import { getLatestReadyVersion, loadVersionSchemas } from '../lib/version-helpers.server.js' +import { + getLatestReadyVersion, + hasOrgAccess, + loadVersionSchemas, +} from '../lib/version-helpers.server.js' const DEFAULT_SCHEMA_SLUG = 'update' const DEFAULT_SCHEMA = { @@ -62,6 +66,7 @@ export async function agentPage(c: Context) { id: schema.collections.id, slug: schema.collections.slug, name: schema.collections.name, + organizationId: schema.collections.organizationId, ownerSlug: schema.organization.slug, ownerName: schema.organization.name, }) @@ -74,6 +79,21 @@ export async function agentPage(c: Context) { return c.html('

Collection not found

', 404) } + // The key's `collectionIds` metadata is set by whoever created the key, so a + // valid key does NOT by itself prove a relationship to this collection. This + // page discloses the schema and sample records, so require the key's owner to + // actually be a member of the owning org — otherwise anyone could mint a key + // pointed at a collection UUID they don't own and read it here. + if (!(await hasOrgAccess(keyInfo.userId, coll.organizationId))) { + return c.html( + `Invalid token +

Invalid or expired token

+

This agent link is no longer valid. Ask the collection owner for a new link.

+`, + 404, + ) + } + const latest = await getLatestReadyVersion(coll.id) const versionMeta = (latest?.metadata as Record) ?? null const description = (versionMeta?.description as string) ?? '' @@ -128,7 +148,10 @@ export async function agentPage(c: Context) { base_version: latest?.semver ?? null, message: 'Added update from conversation', schemas: Object.fromEntries(displaySchemas.map((s) => [s.slug, s.schema])), - manifest: [{ id: 'record-1', type: exampleType, hash: '' }], + manifest: [ + { id: 'record-1', type: exampleType, hash: '' }, + { id: 'record-2', type: exampleType, hash: '', private: true }, + ], files: [], }, null, @@ -179,6 +202,8 @@ ${description ? `Description${escapeHtml(description)}Records${latest.recordCount.toLocaleString()}` : ''} ${latest ? `Files${latest.fileCount.toLocaleString()}` : ''} API key${escapeHtml(token)} +Key expires1 hour after this link was generated. Start promptly; if requests begin returning 401, ask the collection owner for a new link. +Key scopeWrite access to this collection only. Requests against any other collection return 403, and account or organization endpoints are refused outright. Collection URL${escapeHtml(origin)}/${escapeHtml(collPath)} @@ -228,11 +253,20 @@ Content-Type: application/json - +
base_versionThe semver of the version you’re building on (e.g. ${latest ? escapeHtml(latest.semver) : 'null'}). Use null for the first push.
schemasRequired. A map of type name → JSON Schema for every type in this version.
manifestRequired (unless uploading it in chunks, see below). Array of {id, type, hash} for every record in the new version. The hash is SHA-256 of the canonical JSON (see llms.txt for the exact algorithm). Capped at 500,000 entries.
manifestRequired (unless uploading it in chunks, see below). Array of {id, type, hash, private?} for every record in the new version. The hash is SHA-256 of the canonical JSON (see llms.txt for the exact algorithm). Capped at 500,000 entries. private: true hides that record from everyone who is not a member of the owning organization.
filesArray of file hashes referenced by records. Empty array if none.
messageOptional commit message describing this update.
+
+Record privacy is declared here, per push, and is not inherited. +private belongs on the manifest entry above — not on the record body — and it is stored per version. +Omitting it means public. It does not mean “leave it as it was”: if you are re-pushing a collection that already +has private records, you must include "private": true on each of them again, or this push will publish them. +Conversely, dropping the flag from a record is how you un-hide it. +Redaction is forward-only — hiding a record in a new version does not change older versions, which remain immutable and still serve it. +
+

Response

${escapeHtml(JSON.stringify({ session_id: '', needed_records: [''], needed_files: [], total_records: 1, already_have_records: 0 }, null, 2))}
@@ -244,9 +278,15 @@ Content-Type: application/x-ndjson

Record format

${escapeHtml(recordExample)}
+

Exactly three keys: id, type, data. A private key here is silently ignored — +record privacy is declared on the manifest entry in Step 1, not on the record body. The hash you sent in the manifest must be the +SHA-256 of the canonical JSON of these three keys.

Step 3: Commit

-

Finalize the version. The server validates all records against schemas, computes the version hash, and creates the new immutable version.

+

Finalize the version. The server validates all records against schemas, computes both version digests — hash over the full +content and public_hash over the public projection — and creates the new immutable version. A push is rejected as a duplicate +(409) only if both digests match an existing version, so re-pushing identical content with different private flags is a +real new version rather than a no-op.

POST ${escapeHtml(origin)}/api/collections/${escapeHtml(collPath)}/versions/negotiate/<session_id>/commit
 Authorization: Bearer ${escapeHtml(token)}
diff --git a/src/api/ark.ts b/src/api/ark.ts index 0579a54..e53e4f7 100644 --- a/src/api/ark.ts +++ b/src/api/ark.ts @@ -59,6 +59,7 @@ export async function resolve(c: Context) { customUrl: schema.arkCollections.customUrl, collectionSlug: schema.collections.slug, collectionName: schema.collections.name, + collectionPublic: schema.collections.public, ownerSlug: schema.organization.slug, ownerName: schema.organization.name, ownerNaan: schema.organization.arkNaan, @@ -72,6 +73,11 @@ export async function resolve(c: Context) { if (!collRow || !collRow.enabled) return c.json({ type: 'not_found' }, 404) + // ARK resolution is anonymous (the middleware forwards no credentials), so a + // private collection must not resolve — that would expose its name, owner, + // version metadata, and non-private record bodies to anyone with the ARK. + if (!collRow.collectionPublic) return c.json({ type: 'not_found' }, 404) + // Verify the shoulder belongs to the collection's owner if (shoulderRow.organizationId !== collRow.collectionOrgId) { return c.json({ type: 'not_found' }, 404) @@ -166,7 +172,7 @@ export async function resolve(c: Context) { if (!versionRow) return c.json({ type: 'not_found' }, 404) const [recordRow] = await db - .select({ data: schema.recordObjects.data, private: schema.recordObjects.private }) + .select({ data: schema.recordObjects.data, private: schema.versionRecords.private }) .from(schema.versionRecords) .innerJoin( schema.recordObjects, @@ -328,6 +334,10 @@ export async function getArk(c: Context) { if (!coll) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) // Must be owner/member + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(coll.id)) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } const hasAccess = await checkCollectionAccess(coll.organizationId, c.get('userId')!) if (!hasAccess) return c.json({ error: 'Forbidden', statusCode: 403 }, 403) @@ -372,6 +382,10 @@ export async function updateArk(c: Context) { .limit(1) if (!coll) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(coll.id)) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } const hasAccess = await checkCollectionAccess(coll.organizationId, c.get('userId')!) if (!hasAccess) return c.json({ error: 'Forbidden', statusCode: 403 }, 403) @@ -420,6 +434,10 @@ export async function getArkRecordTypes(c: Context) { .limit(1) if (!coll) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(coll.id)) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } const hasAccess = await checkCollectionAccess(coll.organizationId, c.get('userId')!) if (!hasAccess) return c.json({ error: 'Forbidden', statusCode: 403 }, 403) @@ -449,6 +467,10 @@ export async function updateArkRecordTypes(c: Context) { .limit(1) if (!coll) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(coll.id)) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } const hasAccess = await checkCollectionAccess(coll.organizationId, c.get('userId')!) if (!hasAccess) return c.json({ error: 'Forbidden', statusCode: 403 }, 403) diff --git a/src/api/auth.server.ts b/src/api/auth.server.ts index d0fba32..4c1268b 100644 --- a/src/api/auth.server.ts +++ b/src/api/auth.server.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto' -import type { MiddlewareHandler } from 'hono' +import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { auth } from '../lib/auth.js' @@ -22,6 +22,51 @@ export type AuthEnv = { const publicPaths = new Set(['/api/health', '/api/query/generate-sql']) +/** + * POST endpoints that are semantically READS — they use POST only because the + * request carries a list too large for a query string, and they perform no + * mutation. These must be reachable anonymously, exactly like the GET they + * stand in for, or public data becomes unreadable in bulk. + * + * Note this only waives the "all non-GET requests need a userId" gate; each + * handler still runs its own per-collection access check. The `?token=` query + * param remains GET/HEAD-only, so these cannot be driven by a capability URL + * via link prefetch — a share-link caller sends the token as a Bearer header. + */ +const READ_ONLY_POST_PATHS = [/^\/api\/collections\/[^/]+\/[^/]+\/files\/presign$/] +const isReadOnlyPost = (path: string) => READ_ONLY_POST_PATHS.some((re) => re.test(path)) + +/** Verify an API key and load its identity/scope into the request context. */ +async function applyApiKey( + c: Context, + key: string, +): Promise<'ok' | 'invalid' | 'rate-limited'> { + try { + const result = await auth.api.verifyApiKey({ body: { key } }) + if (result?.valid && result.key) { + c.set('userId', (result.key as any).userId ?? (result.key as any).referenceId) + const perms = (result.key.permissions as Record) ?? {} + if (perms['collections']?.includes('admin')) { + c.set('apiKeyScope', 'admin') + } else if (perms['collections']?.includes('write')) { + c.set('apiKeyScope', 'write') + } else { + c.set('apiKeyScope', 'read') + } + const meta = (result.key as any).metadata as Record | null + if (meta?.collectionIds?.length) { + c.set('apiKeyCollectionIds', meta.collectionIds) + } + return 'ok' + } + } catch (err: any) { + if (err?.status === 'TOO_MANY_REQUESTS' || err?.statusCode === 429) { + return 'rate-limited' + } + } + return 'invalid' +} + const internalToken = process.env.INTERNAL_API_TOKEN ?? '' const authInternalApiKey = process.env.AUTH_INTERNAL_API_KEY ?? '' @@ -47,30 +92,29 @@ export const authMiddleware = createMiddleware(async (c, next) => { // API key auth via Bearer token (better-auth apiKey plugin) if (authorization?.startsWith('Bearer ')) { const key = authorization.slice(7) - try { - const result = await auth.api.verifyApiKey({ body: { key } }) - if (result?.valid && result.key) { - c.set('userId', (result.key as any).userId ?? (result.key as any).referenceId) - const perms = (result.key.permissions as Record) ?? {} - if (perms['collections']?.includes('admin')) { - c.set('apiKeyScope', 'admin') - } else if (perms['collections']?.includes('write')) { - c.set('apiKeyScope', 'write') - } else { - c.set('apiKeyScope', 'read') - } - const meta = (result.key as any).metadata as Record | null - if (meta?.collectionIds?.length) { - c.set('apiKeyCollectionIds', meta.collectionIds) - } - return next() - } - } catch (err: any) { - if (err?.status === 'TOO_MANY_REQUESTS' || err?.statusCode === 429) { + const outcome = await applyApiKey(c, key) + if (outcome === 'rate-limited') { + return c.json({ error: 'Rate limit exceeded', statusCode: 429 }, 429) + } + if (outcome === 'invalid') { + return c.json({ error: 'Invalid API key', statusCode: 401 }, 401) + } + return next() + } + + // API key in the query string (?token=...) — capability URLs (read-only share + // links, export downloads) authenticate plain browser GETs that can't set + // headers. An invalid or expired token falls through to anonymous access + // rather than 401, so the page still renders whatever is public. + if (c.req.method === 'GET' || c.req.method === 'HEAD') { + const queryToken = new URL(c.req.url).searchParams.get('token') + if (queryToken) { + const outcome = await applyApiKey(c, queryToken) + if (outcome === 'rate-limited') { return c.json({ error: 'Rate limit exceeded', statusCode: 429 }, 429) } + if (outcome === 'ok') return next() } - return c.json({ error: 'Invalid API key', statusCode: 401 }, 401) } // Session cookie auth (better-auth managed) @@ -91,10 +135,11 @@ export const authMiddleware = createMiddleware(async (c, next) => { // Public GETs are allowed without auth if (c.req.method === 'GET') return next() - // All writes require auth, except public paths + // All writes require auth, except public paths and read-only POSTs if (!c.get('userId')) { const path = new URL(c.req.url).pathname if (publicPaths.has(path)) return next() + if (c.req.method === 'POST' && isReadOnlyPost(path)) return next() return c.json({ error: 'Authentication required', statusCode: 401 }, 401) } @@ -115,3 +160,43 @@ export function requireAuth(scope?: 'read' | 'write' | 'admin'): MiddlewareHandl return next() } } + +/** + * The caller's user id ONLY when they are a full principal — a session or an + * API key that is not restricted to specific collections. A collection-scoped + * key (share/agent link) carries its creator's user id, so anything that grants + * access from mere org membership must treat such a key as anonymous outside its + * scope; use this in place of `c.get('userId')` for cross-collection/aggregate + * reads (collection listings, global schema search, record-by-hash lookups). + * Per-collection endpoints keep using `c.get('userId')` + `apiKeyCollectionIds`. + */ +export function fullPrincipalUserId(c: { + get: { + (key: 'userId'): string | undefined + (key: 'apiKeyCollectionIds'): string[] | undefined + } +}): string | undefined { + return c.get('apiKeyCollectionIds') ? undefined : c.get('userId') +} + +/** + * Reject collection-scoped API keys. Account- and organization-level resources + * are never within the scope of a key minted for a single collection, so a + * share/agent link must not be able to reach them even though it carries the + * creator's identity. + */ +export function requireUnscopedKey(): MiddlewareHandler { + return async (c, next) => { + if (c.get('apiKeyCollectionIds')) { + return c.json( + { + error: + 'This API key is scoped to specific collections and cannot access account or organization resources', + statusCode: 403, + }, + 403, + ) + } + return next() + } +} diff --git a/src/api/collections.ts b/src/api/collections.ts index a4473da..0117200 100644 --- a/src/api/collections.ts +++ b/src/api/collections.ts @@ -10,9 +10,18 @@ import { z } from 'zod' import { db, schema } from '../db/client.server.js' import { buildArkUrl, collectionToArkId, DEFAULT_NAAN, getOrMintShoulder } from '../lib/ark.js' import { downloadFromS3 } from '../lib/s3.js' -import { getLatestReadyVersion, getOrgRole, hasOrgAccess } from '../lib/version-helpers.server.js' +import { + filterRecordData, + filterTypeSchema, + getLatestReadyVersion, + getOrgRole, + getPrivateFields, + getPrivateTypes, + hasOrgAccess, + loadVersionSchemas, +} from '../lib/version-helpers.server.js' import { type AuthEnv } from './auth.server.js' -import { requireAuth } from './auth.server.js' +import { fullPrincipalUserId, requireAuth, requireUnscopedKey } from './auth.server.js' // Export streams in bounded parts, so this is no longer a memory limit — it is a // guard against handing someone a multi-GB tarball from a single GET. Bulk reads @@ -44,7 +53,10 @@ const app = new Hono() // Visibility scope. Public collections by default; with ?mine=true, every // collection owned by an org the caller belongs to — private ones included, // since org membership is what grants access elsewhere (hasOrgAccess). - if (mine && !c.get('userId')) { + // A collection-scoped key (share/agent link) is not "you" for this + // listing, so it does not unlock the creator's private collections. + const listingUserId = fullPrincipalUserId(c) + if (mine && !listingUserId) { return c.json( { error: 'Unauthorized — mine=true requires a session', statusCode: 401 }, 401, @@ -56,7 +68,7 @@ const app = new Hono() db .select({ id: schema.member.organizationId }) .from(schema.member) - .where(eq(schema.member.userId, c.get('userId')!)), + .where(eq(schema.member.userId, listingUserId!)), ) : eq(schema.collections.public, true) @@ -231,6 +243,7 @@ const app = new Hono() .post( '/accounts/:owner/collections', requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Collections'], summary: 'Create a collection', @@ -356,9 +369,14 @@ const app = new Hono() return c.json({ error: 'Collection not found', statusCode: 404 }, 404) } + // A collection-scoped API key (share/agent link) only grants access to + // the collections it is scoped to. + const scopedCollections = c.get('apiKeyCollectionIds') + const keyScopeOk = !scopedCollections || scopedCollections.includes(result.id) + const userId = c.get('userId') let hasAccess = false - if (userId) { + if (userId && keyScopeOk) { const [membership] = await db .select() .from(schema.member) @@ -594,7 +612,8 @@ const app = new Hono() // Transfer collection to another org .post( '/collections/:owner/:slug/transfer', - requireAuth(), + requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Collections'], summary: 'Transfer a collection to another org', @@ -726,9 +745,10 @@ const app = new Hono() if (!org) return c.json([]) - // Check if the requester is an org member + // Check if the requester is an org member. A collection-scoped key does + // not count — it must not enumerate the org's private collections. let hasFullAccess = false - const userId = c.get('userId') + const userId = fullPrincipalUserId(c) if (userId) { const [membership] = await db .select() @@ -793,11 +813,14 @@ const app = new Hono() return c.json({ error: 'Collection not found', statusCode: 404 }, 404) } - if (!collection.public) { - const userId = c.get('userId') - if (!userId || !(await hasOrgAccess(userId, collection.organizationId))) { - return c.json({ error: 'Collection not found', statusCode: 404 }, 404) - } + // A member of the owning org (and not scoped out by a collection-scoped + // key) gets the full export; everyone else gets the privacy-filtered view. + const scopedCollections = c.get('apiKeyCollectionIds') + const keyScopeOk = !scopedCollections || scopedCollections.includes(collection.id) + const ownerAccess = + keyScopeOk && (await hasOrgAccess(c.get('userId'), collection.organizationId)) + if (!collection.public && !ownerAccess) { + return c.json({ error: 'Collection not found', statusCode: 404 }, 404) } // Resolve version (latest if not specified) @@ -865,7 +888,25 @@ const app = new Hono() .innerJoin(schema.schemas, eq(schema.versionSchemas.schemaId, schema.schemas.id)) .where(eq(schema.versionSchemas.versionId, version.id)) - const schemasMap = Object.fromEntries(versionSchemaEntries.map((e) => [e.slug, e.schemaBody])) + // Private types are dropped and private fields stripped for non-owners. + const privateTypes = new Set( + versionSchemaEntries + .filter((e) => (e.schemaBody as any)?.private === true) + .map((e) => e.slug), + ) + const privateFieldsByType = new Map>() + for (const e of versionSchemaEntries) { + privateFieldsByType.set(e.slug, getPrivateFields(e.schemaBody as Record)) + } + + const schemasMap = Object.fromEntries( + versionSchemaEntries + .filter((e) => ownerAccess || !privateTypes.has(e.slug)) + .map((e) => [ + e.slug, + ownerAccess ? e.schemaBody : filterTypeSchema(e.schemaBody as Record), + ]), + ) // Build manifest.json (packed last, so it can report any files that // failed to download) @@ -879,8 +920,13 @@ const app = new Hono() }, version: { semver: version.semver, - hash: version.hash, + // Non-owners get the public version digest, never the private one. + hash: ownerAccess ? version.hash : (version.publicHash ?? version.hash), message: version.message, + // For non-owners these are overwritten below with what the archive + // actually contains; the version row's totals count private content + // that was filtered out, so reporting them verbatim would both + // contradict the tarball and disclose how much is hidden. recordCount: version.recordCount, fileCount: version.fileCount, totalBytes: version.totalBytes, @@ -915,7 +961,30 @@ const app = new Hono() // one part regardless of collection size. const RECORDS_PER_PART = 25_000 + // For non-owners, only files referenced by the (privacy-filtered) records + // that actually ship may be included — a file attached solely to a private + // record or private field must not leak. + let emittedRecordCount = 0 + const referencedFileHashes = new Set() + // Walks nested objects and arrays: `$file` refs are not restricted to the + // top level (nothing in schema validation forbids nesting), and a missed + // ref would silently drop a legitimately-public file from the archive. + const collectFileRefs = (value: unknown) => { + if (!value || typeof value !== 'object') return + const ref = (value as { $file?: unknown }).$file + if (typeof ref === 'string') { + referencedFileHashes.add(ref.replace('sha256:', '')) + return + } + for (const child of Object.values(value as Record)) { + collectFileRefs(child) + } + } + for (const { type } of types) { + // Non-owners never see private types. + if (!ownerAccess && privateTypes.has(type)) continue + const privateFields = privateFieldsByType.get(type) ?? new Set() let batchCursor: string | null = null let batchHasMore = true let partIndex = 0 @@ -940,6 +1009,10 @@ const app = new Hono() eq(schema.versionRecords.versionId, version.id), eq(schema.versionRecords.type, type), ] + // Non-owners never see records flagged private (per-version flag). + if (!ownerAccess) { + conditions.push(eq(schema.versionRecords.private, false)) + } if (batchCursor) { conditions.push(sql`${schema.versionRecords.recordId} > ${batchCursor}`) } @@ -962,7 +1035,13 @@ const app = new Hono() const page = batchHasMore ? batch.slice(0, 5000) : batch if (page.length > 0) batchCursor = page[page.length - 1]!.recordId for (const r of page) { - pending.push(JSON.stringify({ id: r.recordId, type: r.type, data: r.data })) + const data = + !ownerAccess && privateFields.size > 0 + ? filterRecordData(r.data, privateFields) + : r.data + if (!ownerAccess) collectFileRefs(data) + emittedRecordCount++ + pending.push(JSON.stringify({ id: r.recordId, type: r.type, data })) } // Emit whenever a part fills, so `pending` never grows past one part. if (pending.length >= RECORDS_PER_PART) flushPart(false) @@ -971,16 +1050,29 @@ const app = new Hono() } // Add files + let emittedFileCount = 0 + let emittedFileBytes = 0 for (const file of versionFiles) { + if (!ownerAccess && !referencedFileHashes.has(file.hash)) continue try { const fileBuffer = await downloadFromS3(file.storageKey) pack.entry({ name: `files/${file.hash}`, size: fileBuffer.length }, fileBuffer) + emittedFileCount++ + emittedFileBytes += fileBuffer.length } catch (err) { console.error(`[export] Failed to download file ${file.hash} (${file.storageKey}):`, err) manifest.files_missing.push(file.hash) } } + // Report what the archive actually contains, so a non-owner's manifest + // matches its payload instead of the owner's (larger) totals. + if (!ownerAccess) { + manifest.version.recordCount = emittedRecordCount + manifest.version.fileCount = emittedFileCount + manifest.version.totalBytes = emittedFileBytes + } + const manifestBuf = Buffer.from(JSON.stringify(manifest, null, 2)) pack.entry({ name: 'manifest.json', size: manifestBuf.length }, manifestBuf) @@ -1012,6 +1104,7 @@ const app = new Hono() .post( '/collections/:owner/:slug/fork', requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Collections'], summary: "Fork a collection into the caller's org", @@ -1101,6 +1194,53 @@ const app = new Hono() return c.json({ error: 'Source collection has no versions', statusCode: 422 }, 422) } + // A fork copies `version_records` rows verbatim, and those point at the + // FULL `record_objects` bodies. The forker owns the copy, so they get + // owner-level access to it — which would hand a non-member every piece of + // private content in a public collection: private records, private types, + // and private field values inside otherwise-public records. + // + // Serving a redacted fork would mean materializing the filtered record + // bodies (only their hashes exist today, as `public_record_hash`), which + // this endpoint does not do. So for a caller who cannot already see the + // source in full, refuse rather than leak. The three conditions below are + // exactly "nothing is filtered for a non-owner of the source". + const sourceOwnerAccess = await hasOrgAccess(c.get('userId'), source.organizationId) + if (!sourceOwnerAccess) { + const [privateRow] = await db + .select({ n: sql`count(*)::int` }) + .from(schema.versionRecords) + .where( + and( + eq(schema.versionRecords.versionId, latestVersion.id), + eq(schema.versionRecords.private, true), + ), + ) + const [strippedRow] = await db + .select({ n: sql`count(*)::int` }) + .from(schema.versionRecords) + .where( + and( + eq(schema.versionRecords.versionId, latestVersion.id), + sql`${schema.versionRecords.publicRecordHash} IS NOT NULL`, + ), + ) + const hasPrivateType = getPrivateTypes(await loadVersionSchemas(latestVersion.id)).size > 0 + + if ((privateRow?.n ?? 0) > 0 || (strippedRow?.n ?? 0) > 0 || hasPrivateType) { + return c.json( + { + error: + 'This collection contains private content, so it cannot be forked by a ' + + 'non-member. Forking copies the full record bodies, which would expose ' + + 'private records, private types, or private fields.', + statusCode: 403, + }, + 403, + ) + } + } + // Create forked collection + version in a transaction const newCollectionId = uuidv4() await db.transaction(async (tx) => { @@ -1138,8 +1278,8 @@ const app = new Hono() // Copy the record set server-side. A fork of a multi-million-record // collection has no reason to round-trip every row through the app. await tx.execute(sql` - INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type) - SELECT ${newVersion!.id}, record_hash, public_record_hash, record_id, type + INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type, private) + SELECT ${newVersion!.id}, record_hash, public_record_hash, record_id, type, private FROM version_records WHERE version_id = ${latestVersion.id} `) diff --git a/src/api/files.ts b/src/api/files.ts index f605d93..c01ea31 100644 --- a/src/api/files.ts +++ b/src/api/files.ts @@ -1,27 +1,38 @@ import { createHash } from 'node:crypto' -import { and, eq, sql } from 'drizzle-orm' +import { and, eq, inArray, sql } from 'drizzle-orm' import { Hono } from 'hono' import { openApi } from 'hono-zod-openapi' import { z } from 'zod' import { db, schema } from '../db/client.server.js' -import { getS3ObjectMeta, uploadToS3 } from '../lib/s3.js' +import { getPresignedFileUrl, getS3ObjectMeta, uploadToS3 } from '../lib/s3.js' +import { hasOrgAccess } from '../lib/version-helpers.server.js' import { type AuthEnv } from './auth.server.js' import { requireAuth } from './auth.server.js' const MAX_UPLOAD_BYTES = parseInt(process.env.MAX_FILE_UPLOAD_BYTES ?? '', 10) || 100 * 1024 * 1024 // 100 MB +// Content types that render/execute inline in a browser are coerced to an +// inert type on storage, so an uploaded object can never act as a page even if +// it is later served without a forced download. +const UNSAFE_MIME_RE = /^(text\/html|application\/xhtml|image\/svg|text\/xml|application\/xml)/i +function safeMimeType(mime: string): string { + return UNSAFE_MIME_RE.test(mime.trim()) ? 'application/octet-stream' : mime +} + async function isFilePubliclyAccessible( owner: string, slug: string, fileHash: string, userId: string | undefined, + apiKeyCollectionIds?: string[], ): Promise { const [collection] = await db .select({ id: schema.collections.id, organizationId: schema.collections.organizationId, + public: schema.collections.public, }) .from(schema.collections) .innerJoin(schema.organization, eq(schema.collections.organizationId, schema.organization.id)) @@ -30,7 +41,27 @@ async function isFilePubliclyAccessible( if (!collection) return false - if (userId != null) { + // A collection-scoped API key (share/agent link) only counts for the + // collections it is scoped to. + const keyScopeOk = !apiKeyCollectionIds || apiKeyCollectionIds.includes(collection.id) + + // The file must actually belong to THIS collection (in any of its versions). + // Without this, the owner/slug in the path is decorative: a member could fetch + // any file in the system by requesting it under a collection they belong to. + const [belongs] = await db + .select({ fileHash: schema.versionFiles.fileHash }) + .from(schema.versionFiles) + .innerJoin(schema.versions, eq(schema.versionFiles.versionId, schema.versions.id)) + .where( + and( + eq(schema.versions.collectionId, collection.id), + eq(schema.versionFiles.fileHash, fileHash), + ), + ) + .limit(1) + if (!belongs) return false + + if (userId != null && keyScopeOk) { const [membership] = await db .select() .from(schema.member) @@ -44,85 +75,89 @@ async function isFilePubliclyAccessible( if (membership) return true } - const [latest] = await db - .select({ id: schema.versions.id }) - .from(schema.versions) - .where( - and(eq(schema.versions.collectionId, collection.id), eq(schema.versions.status, 'ready')), - ) - .orderBy( - sql`${schema.versions.major} desc, ${schema.versions.minor} desc, ${schema.versions.patch} desc`, - ) - .limit(1) - - if (!latest) return false - - const [vf] = await db - .select({ fileHash: schema.versionFiles.fileHash }) - .from(schema.versionFiles) - .where( - and(eq(schema.versionFiles.versionId, latest.id), eq(schema.versionFiles.fileHash, fileHash)), - ) - .limit(1) + // Non-members get files only from PUBLIC collections. Without this a file in a + // private collection was downloadable by anyone who knew its hash. + if (!collection.public) return false - if (!vf) return false - - const schemaEntries = await db + // OR across every ready version: a file is accessible if it is referenced via + // a non-private field of a non-private record of a non-private type in ANY + // ready version of this (public) collection. Files are content-addressed and + // immutable, and the URL carries no version, so "published publicly in any + // accessible version ⇒ public" is the correct resolution. + const candidates = await db .select({ - slug: schema.versionSchemas.slug, - schemaBody: schema.schemas.schema, + versionId: schema.versionRecords.versionId, + type: schema.recordObjects.type, + data: schema.recordObjects.data, }) - .from(schema.versionSchemas) - .innerJoin(schema.schemas, eq(schema.versionSchemas.schemaId, schema.schemas.id)) - .where(eq(schema.versionSchemas.versionId, latest.id)) - - const privateTypes = new Set() - const typeSchemaMap = new Map>() - for (const entry of schemaEntries) { - const body = entry.schemaBody as Record - typeSchemaMap.set(entry.slug, body) - if (body?.private === true) privateTypes.add(entry.slug) - } - - const records = await db - .select({ type: schema.recordObjects.type, data: schema.recordObjects.data }) .from(schema.versionRecords) + .innerJoin(schema.versions, eq(schema.versionRecords.versionId, schema.versions.id)) .innerJoin( schema.recordObjects, eq(schema.versionRecords.recordHash, schema.recordObjects.hash), ) .where( and( - eq(schema.versionRecords.versionId, latest.id), - eq(schema.recordObjects.private, false), + eq(schema.versions.collectionId, collection.id), + eq(schema.versions.status, 'ready'), + eq(schema.versionRecords.private, false), sql`${schema.recordObjects.data}::text LIKE ${'%' + fileHash + '%'}`, ), ) - .limit(10) + .limit(50) + + if (candidates.length === 0) return false + + // Type/field privacy is per-version; load each candidate version's schema once. + const schemaCache = new Map< + number, + { privateTypes: Set; typeSchemas: Map> } + >() + const loadVersionPrivacy = async (versionId: number) => { + const cached = schemaCache.get(versionId) + if (cached) return cached + const entries = await db + .select({ slug: schema.versionSchemas.slug, schemaBody: schema.schemas.schema }) + .from(schema.versionSchemas) + .innerJoin(schema.schemas, eq(schema.versionSchemas.schemaId, schema.schemas.id)) + .where(eq(schema.versionSchemas.versionId, versionId)) + const privateTypes = new Set() + const typeSchemas = new Map>() + for (const e of entries) { + const body = e.schemaBody as Record + typeSchemas.set(e.slug, body) + if (body?.private === true) privateTypes.add(e.slug) + } + const value = { privateTypes, typeSchemas } + schemaCache.set(versionId, value) + return value + } - for (const rec of records) { + for (const rec of candidates) { + const { privateTypes, typeSchemas } = await loadVersionPrivacy(rec.versionId) if (privateTypes.has(rec.type)) continue - const typeSchema = typeSchemaMap.get(rec.type) - const typeProps = typeSchema?.properties as Record | undefined - if (!typeProps) return true - + const typeProps = typeSchemas.get(rec.type)?.properties as Record | undefined const privateFields = new Set() - for (const [fieldName, fieldDef] of Object.entries(typeProps)) { - if ((fieldDef as any)?.private === true) privateFields.add(fieldName) + if (typeProps) { + for (const [fieldName, fieldDef] of Object.entries(typeProps)) { + if ((fieldDef as any)?.private === true) privateFields.add(fieldName) + } + } + + // `$file` refs may be nested inside objects/arrays, so search recursively + // within each non-private top-level field (field privacy is top-level only). + const containsRef = (value: unknown): boolean => { + if (!value || typeof value !== 'object') return false + const ref = (value as { $file?: unknown }).$file + if (typeof ref === 'string') return ref === `sha256:${fileHash}` + return Object.values(value as Record).some(containsRef) } const data = rec.data as Record for (const [key, val] of Object.entries(data)) { if (privateFields.has(key)) continue - if ( - val && - typeof val === 'object' && - '$file' in val && - (val as { $file: string }).$file === `sha256:${fileHash}` - ) { - return true - } + if (containsRef(val)) return true } } @@ -152,7 +187,13 @@ const app = new Hono() return c.body(null, 404) } - const accessible = await isFilePubliclyAccessible(owner, slug, cleanHash, c.get('userId')) + const accessible = await isFilePubliclyAccessible( + owner, + slug, + cleanHash, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!accessible) { return c.body(null, 404) } @@ -183,13 +224,131 @@ const app = new Hono() return c.json({ error: 'File not found', statusCode: 404 }, 404) } - const accessible = await isFilePubliclyAccessible(owner, slug, cleanHash, c.get('userId')) + const accessible = await isFilePubliclyAccessible( + owner, + slug, + cleanHash, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!accessible) { return c.json({ error: 'File not found', statusCode: 404 }, 404) } - const cdnUrl = `https://assets.underlay.org/files/${cleanHash.slice(0, 2)}/${cleanHash.slice(2, 4)}/${cleanHash}` - return c.redirect(cdnUrl) + // Files live in a private bucket; mint a short-lived presigned URL now that + // the access check has passed, so the object is never reachable by hash + // alone from the storage origin. + const signedUrl = await getPresignedFileUrl(file.storageKey) + return c.redirect(signedUrl) + }, + ) + // Collection-agnostic download (used by the global record-provenance page): + // presign only if the file is accessible through at least one collection the + // caller may read. + .get( + '/files/:hash', + openApi({ + tags: ['Files'], + summary: 'Download a file by hash (collection-agnostic)', + request: { param: z.object({ hash: z.string() }) }, + responses: { 302: z.any(), 404: z.object({ error: z.string() }) }, + }), + async (c) => { + const cleanHash = c.req.param('hash').replace('sha256:', '') + + const [file] = await db + .select() + .from(schema.files) + .where(eq(schema.files.hash, cleanHash)) + .limit(1) + if (!file) return c.json({ error: 'File not found', statusCode: 404 }, 404) + + // Candidate collections that contain this file; the per-collection check + // enforces public-vs-private and the non-private-reference requirement. + const candidates = await db + .selectDistinct({ owner: schema.organization.slug, slug: schema.collections.slug }) + .from(schema.versionFiles) + .innerJoin(schema.versions, eq(schema.versionFiles.versionId, schema.versions.id)) + .innerJoin(schema.collections, eq(schema.versions.collectionId, schema.collections.id)) + .innerJoin( + schema.organization, + eq(schema.collections.organizationId, schema.organization.id), + ) + .where(eq(schema.versionFiles.fileHash, cleanHash)) + .limit(50) + + let accessible = false + for (const cand of candidates) { + if ( + await isFilePubliclyAccessible( + cand.owner, + cand.slug, + cleanHash, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) + ) { + accessible = true + break + } + } + if (!accessible) return c.json({ error: 'File not found', statusCode: 404 }, 404) + + const signedUrl = await getPresignedFileUrl(file.storageKey) + return c.redirect(signedUrl) + }, + ) + // Bulk presign: resolve the (caller, owner, slug) context once and check each + // hash, so a page referencing many files makes one request instead of N. Same + // access model as the single GET; returns a presigned URL per accessible hash + // and null otherwise. (Share-link callers pass their token as a Bearer header; + // the query-param token is GET/HEAD-only and does not authenticate this POST.) + .post( + '/:owner/:slug/files/presign', + openApi({ + tags: ['Files'], + summary: 'Presign a batch of files by hash', + request: { + param: z.object({ owner: z.string(), slug: z.string() }), + json: z.object({ hashes: z.array(z.string()).max(500) }), + }, + responses: { 200: z.any() }, + }), + async (c) => { + const { owner, slug } = c.req.valid('param') + const { hashes } = c.req.valid('json') + const userId = c.get('userId') + const scoped = c.get('apiKeyCollectionIds') + + // Keys in the response are echoed back EXACTLY as the caller sent them + // (`sha256:`-prefixed or not), so a client can look up what it asked for. + const requested = [...new Set(hashes)] + const cleanOf = new Map(requested.map((h) => [h, h.replace('sha256:', '')])) + const clean = [...new Set(cleanOf.values())] + + const fileRows = clean.length + ? await db + .select({ hash: schema.files.hash, storageKey: schema.files.storageKey }) + .from(schema.files) + .where(inArray(schema.files.hash, clean)) + : [] + const storageByHash = new Map(fileRows.map((f) => [f.hash, f.storageKey])) + + // Resolve access once per distinct hash, even if requested in both forms. + const urlByClean = new Map() + for (const h of clean) { + const storageKey = storageByHash.get(h) + if (!storageKey) { + urlByClean.set(h, null) + continue + } + const ok = await isFilePubliclyAccessible(owner, slug, h, userId, scoped) + urlByClean.set(h, ok ? await getPresignedFileUrl(storageKey) : null) + } + + const result: Record = {} + for (const h of requested) result[h] = urlByClean.get(cleanOf.get(h)!) ?? null + return c.json(result) }, ) .put( @@ -202,9 +361,32 @@ const app = new Hono() responses: { 200: z.any(), 201: z.any() }, }), async (c) => { - const { hash } = c.req.valid('param') + const { owner, slug, hash } = c.req.valid('param') const cleanHash = hash.replace('sha256:', '') + // Files are content-addressed and shared, but a write must still be tied to + // a collection the caller can actually push to — otherwise any write-scoped + // credential (including an agent link) could upload into global storage. + const [uploadCollection] = await db + .select({ id: schema.collections.id, organizationId: schema.collections.organizationId }) + .from(schema.collections) + .innerJoin( + schema.organization, + eq(schema.collections.organizationId, schema.organization.id), + ) + .where(and(eq(schema.organization.slug, owner), eq(schema.collections.slug, slug))) + .limit(1) + if (!uploadCollection) { + return c.json({ error: 'Collection not found', statusCode: 404 }, 404) + } + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(uploadCollection.id)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + if (!(await hasOrgAccess(c.get('userId'), uploadCollection.organizationId))) { + return c.json({ error: 'Forbidden', statusCode: 403 }, 403) + } + const [existing] = await db .select() .from(schema.files) @@ -283,13 +465,14 @@ const app = new Hono() } const storageKey = `files/${cleanHash.slice(0, 2)}/${cleanHash.slice(2, 4)}/${cleanHash}` + const storedMimeType = safeMimeType(mimeType) - await uploadToS3(storageKey, buffer, mimeType) + await uploadToS3(storageKey, buffer, storedMimeType) await db.insert(schema.files).values({ hash: cleanHash, size: buffer.length, - mimeType, + mimeType: storedMimeType, storageKey, }) diff --git a/src/api/kf-summary.ts b/src/api/kf-summary.ts index 8b46398..190b385 100644 --- a/src/api/kf-summary.ts +++ b/src/api/kf-summary.ts @@ -1,8 +1,17 @@ +import crypto from 'node:crypto' + import { and, eq, sql } from 'drizzle-orm' import type { Context } from 'hono' import { db, schema } from '../db/client.server.js' +function timingSafeEquals(a: string, b: string): boolean { + const ab = Buffer.from(a) + const bb = Buffer.from(b) + if (ab.length !== bb.length) return false + return crypto.timingSafeEqual(ab, bb) +} + /** * GET /api/kf/summary?kf_org_id=xxx * @@ -19,7 +28,7 @@ export async function summary(c: Context) { // Verify internal API key const authHeader = c.req.header('Authorization') const expectedKey = process.env.AUTH_INTERNAL_API_KEY - if (!expectedKey || authHeader !== `Bearer ${expectedKey}`) { + if (!expectedKey || !authHeader || !timingSafeEquals(authHeader, `Bearer ${expectedKey}`)) { return c.json({ error: 'Unauthorized', statusCode: 401 }, 401) } diff --git a/src/api/negotiate.ts b/src/api/negotiate.ts index fecb13b..4588eef 100644 --- a/src/api/negotiate.ts +++ b/src/api/negotiate.ts @@ -357,6 +357,13 @@ app.post( if (sessionRow.userId !== c.get('userId')) { return c.json({ error: 'Not authorized', statusCode: 403 }, 403) } + { + // A collection-scoped key must not act on a session for another collection. + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(sessionRow.collectionId)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + } if (sessionRow.manifestExpected === null) { return c.json( { @@ -456,6 +463,13 @@ app.get( if (session.userId !== c.get('userId')) { return c.json({ error: 'Not authorized', statusCode: 403 }, 403) } + { + // A collection-scoped key must not act on a session for another collection. + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(session.collectionId)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + } const [manifestCounts] = await db .select({ @@ -513,6 +527,13 @@ app.post( if (sessionRow.userId !== c.get('userId')) { return c.json({ error: 'Not authorized', statusCode: 403 }, 403) } + { + // A collection-scoped key must not act on a session for another collection. + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(sessionRow.collectionId)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + } const rawBody = await c.req.text() const lines = rawBody @@ -647,7 +668,6 @@ app.post( recordId: string type: string data: unknown - private: boolean size: number }[] = [] @@ -671,7 +691,6 @@ app.post( recordId: rec.id, type: rec.type, data, - private: rec.private ?? false, size: Buffer.byteLength(canonical, 'utf-8'), }) } @@ -992,8 +1011,12 @@ app.post( // `<> ALL` over an empty array is TRUE, so the type clause needs no special // case when nothing is private — which lets the same predicate be written // through drizzle and through the raw driver used for the digest cursors. + // Public-view membership is a pure function of THIS version's authored + // inputs: the push's own per-record private flag (m.private) and its + // private types. It deliberately does NOT consult a global record flag — + // that is what made public_hash depend on other collections' push history. const privateTypeList = [...privateTypes] - const publicRows = sql`NOT m.private AND NOT ro.private + const publicRows = sql`NOT m.private AND ro.type <> ALL(${sql.param(privateTypeList)}::text[])` const validationErrors: { recordId: string; type: string; errors: string[] }[] = [] @@ -1025,7 +1048,7 @@ app.post( let cursor: string | null = null for (;;) { const batch = (await db.execute(sql` - SELECT m.hash, ro.record_id AS "recordId", ro.type, ro.data, ro.private, ro.size + SELECT m.hash, ro.record_id AS "recordId", ro.type, ro.data, ro.size FROM negotiate_session_manifest m INNER JOIN record_objects ro ON ro.hash = m.hash WHERE m.session_id = ${sessionId} AND (${needsValidation}) @@ -1037,7 +1060,6 @@ app.post( recordId: string type: string data: unknown - private: boolean size: number }[] @@ -1052,7 +1074,6 @@ app.post( recordId: string type: string data: unknown - private: boolean size: number }[] = [] const rehashed: { hash: string; finalHash: string }[] = [] @@ -1104,7 +1125,6 @@ app.post( recordId: rec.recordId, type: rec.type, data, - private: rec.private, size: Buffer.byteLength(result.canonical, 'utf-8'), }) rehashed.push({ hash: rec.hash, finalHash: result.hash }) @@ -1130,7 +1150,6 @@ app.post( recordId: r.recordId, type: r.type, data: r.data as any, - private: r.private, size: r.size, })), ) @@ -1345,7 +1364,7 @@ app.post( FROM negotiate_session_manifest m INNER JOIN record_objects ro ON ro.hash = coalesce(m.final_hash, m.hash) WHERE m.session_id = ${sessionId} - AND NOT m.private AND NOT ro.private + AND NOT m.private AND ro.type <> ALL(${privateTypeList}::text[]) ORDER BY coalesce(m.public_hash, m.final_hash, m.hash) COLLATE "C" `.cursor(CURSOR_CHUNK, (rows) => { @@ -1362,7 +1381,17 @@ app.post( metadataChanged, ) - // Check for duplicate + // Check for duplicate. + // + // A version is identified by BOTH digests. `hash` covers records, schemas, + // files and metadata but NOT record-level privacy, so comparing it alone + // would reject a push that changes only which records are private — + // i.e. redaction-in-place, the workflow per-version privacy exists to + // support: identical content re-pushed with `private: true` would 409 as + // "no changes". `publicHash` is exactly the digest that privacy moves, so + // requiring both to match makes a privacy-only change a real new version. + // Versions written before public_hash existed store NULL; for those, fall + // back to matching on `hash` alone so their duplicate behaviour is unchanged. const [existingHash] = await db .select({ semver: schema.versions.semver }) .from(schema.versions) @@ -1370,6 +1399,7 @@ app.post( and( eq(schema.versions.collectionId, session.collectionId), eq(schema.versions.hash, versionHash), + sql`(${schema.versions.publicHash} IS NULL OR ${schema.versions.publicHash} = ${publicHash})`, eq(schema.versions.status, 'ready'), ), ) @@ -1461,11 +1491,11 @@ app.post( const hi = bound.hi await db.execute(sql` - INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type) + INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type, private) SELECT ${versionId!}, ro.hash, nullif(coalesce(m.public_hash, m.final_hash, m.hash), coalesce(m.final_hash, m.hash)), - ro.record_id, ro.type + ro.record_id, ro.type, m.private FROM negotiate_session_manifest m INNER JOIN record_objects ro ON ro.hash = coalesce(m.final_hash, m.hash) WHERE m.session_id = ${sessionId} @@ -1625,6 +1655,13 @@ app.delete( if (session.userId !== c.get('userId')) { return c.json({ error: 'Not authorized', statusCode: 403 }, 403) } + { + // A collection-scoped key must not act on a session for another collection. + const scopedCollections = c.get('apiKeyCollectionIds') + if (scopedCollections && !scopedCollections.includes(session.collectionId)) { + return c.json({ error: 'API key is not scoped to this collection', statusCode: 403 }, 403) + } + } await expireSession(sessionId) diff --git a/src/api/organizations.ts b/src/api/organizations.ts index c948e5d..9803d6d 100644 --- a/src/api/organizations.ts +++ b/src/api/organizations.ts @@ -5,14 +5,16 @@ import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' import { db, schema } from '../db/client.server.js' +import { RESERVED_SLUGS } from './accounts.js' import { type AuthEnv } from './auth.server.js' -import { requireAuth } from './auth.server.js' +import { requireAuth, requireUnscopedKey } from './auth.server.js' const SLUG_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ const app = new Hono().post( '/', requireAuth('write'), + requireUnscopedKey(), openApi({ tags: ['Organizations'], summary: 'Create an organization', @@ -31,6 +33,9 @@ const app = new Hono().post( if (!slug || slug.length < 2 || slug.length > 64 || !SLUG_RE.test(slug)) { return c.json({ error: 'Invalid slug', statusCode: 422 }, 422) } + if (RESERVED_SLUGS.has(slug)) { + return c.json({ error: 'That slug is reserved', statusCode: 422 }, 422) + } const [existing] = await db .select({ id: schema.organization.id }) diff --git a/src/api/query.ts b/src/api/query.ts index 6e16ee6..753046f 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -12,7 +12,7 @@ import { parseSemver, type SchemaEntry, } from '../lib/version-helpers.server.js' -import { type AuthEnv } from './auth.server.js' +import { type AuthEnv, fullPrincipalUserId } from './auth.server.js' // In-memory LRU cache: key = `${collectionId}:${semver}`, value = { buffer, expiresAt } const sqliteCache = new Map< @@ -77,6 +77,7 @@ async function getOrBuildSqlite( slug: string, versionSemver: string, userId: string | undefined, + apiKeyCollectionIds?: string[], ) { const { semver: normalizedSemver } = parseSemver(versionSemver) @@ -95,8 +96,10 @@ async function getOrBuildSqlite( if (!collection) return null // Access: private collections are only visible to org members; non-members of - // public collections get a privacy-filtered build - const ownerAccess = await hasOrgAccess(userId, collection.organizationId) + // public collections get a privacy-filtered build. A collection-scoped API + // key (share/agent link) only counts for the collections it is scoped to. + const keyScopeOk = !apiKeyCollectionIds || apiKeyCollectionIds.includes(collection.id) + const ownerAccess = keyScopeOk && (await hasOrgAccess(userId, collection.organizationId)) if (!collection.public && !ownerAccess) return null // Resolve version @@ -158,10 +161,11 @@ async function getOrBuildSqlite( } } - // Load records (excluding private types and private records for non-owners) + // Load records (excluding private types and private records for non-owners). + // Record-level privacy is the per-version version_records.private flag. const recordConditions = [eq(schema.versionRecords.versionId, version.id)] if (!ownerAccess) { - recordConditions.push(eq(schema.recordObjects.private, false)) + recordConditions.push(eq(schema.versionRecords.private, false)) for (const pt of privateTypes) { recordConditions.push(ne(schema.recordObjects.type, pt)) } @@ -254,7 +258,13 @@ export async function sqlite(c: Context) { const versionSemver = c.req.param('version')! const { semver } = parseSemver(versionSemver) - const result = await getOrBuildSqlite(owner, slug, versionSemver, c.get('userId')) + const result = await getOrBuildSqlite( + owner, + slug, + versionSemver, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!result) return c.json({ error: 'Collection or version not found', statusCode: 404 }, 404) if ('tooLarge' in result) return tooLargeResponse(c, result.recordCount) @@ -274,7 +284,13 @@ export async function ddl(c: Context) { const slug = c.req.param('slug')! const versionSemver = c.req.param('version')! - const result = await getOrBuildSqlite(owner, slug, versionSemver, c.get('userId')) + const result = await getOrBuildSqlite( + owner, + slug, + versionSemver, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!result) return c.json({ error: 'Collection or version not found', statusCode: 404 }, 404) if ('tooLarge' in result) return tooLargeResponse(c, result.recordCount) @@ -284,10 +300,16 @@ export async function ddl(c: Context) { // POST /query/generate-sql — LLM-powered SQL generation from natural language export async function generateSql(c: Context) { // Public endpoint that spends Cloudflare AI credits — rate-limit per user/IP - const ip = - c.req.header('cf-connecting-ip') ?? - c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ?? - 'unknown' + // Prefer cf-connecting-ip (unforgeable behind Cloudflare); the RIGHTMOST + // X-Forwarded-For hop is the proxy-observed address, unlike the client-set + // leftmost one. + const xffParts = + c.req + .header('x-forwarded-for') + ?.split(',') + .map((s) => s.trim()) + .filter(Boolean) ?? [] + const ip = c.req.header('cf-connecting-ip') ?? xffParts[xffParts.length - 1] ?? 'unknown' const rateKey = c.get('userId') ?? `ip:${ip}` if (!checkRateLimit(rateKey)) { return c.json({ error: 'Rate limit exceeded — try again in a minute', statusCode: 429 }, 429) @@ -318,7 +340,13 @@ export async function generateSql(c: Context) { if (collectionRefs.length === 1) { const ref = collectionRefs[0] - const result = await getOrBuildSqlite(ref.owner, ref.slug, ref.version, c.get('userId')) + const result = await getOrBuildSqlite( + ref.owner, + ref.slug, + ref.version, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!result) return c.json( { error: `Collection ${ref.owner}/${ref.slug} v${ref.version} not found`, statusCode: 404 }, @@ -330,7 +358,13 @@ export async function generateSql(c: Context) { } else { const parts: string[] = [] for (const ref of collectionRefs) { - const result = await getOrBuildSqlite(ref.owner, ref.slug, ref.version, c.get('userId')) + const result = await getOrBuildSqlite( + ref.owner, + ref.slug, + ref.version, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!result) return c.json( { error: `Collection ${ref.owner}/${ref.slug} v${ref.version} not found` }, @@ -464,7 +498,8 @@ export async function searchCollections(c: Context) { if (!q || q.trim().length < 2) return c.json([]) const term = `%${q.trim()}%` - const userId = c.get('userId') + // A collection-scoped key must not surface the creator's private collections. + const userId = fullPrincipalUserId(c) // Build accessible org IDs (user's own + orgs they belong to) let accessibleAccountIds: string[] = [] diff --git a/src/api/rate-limit.server.ts b/src/api/rate-limit.server.ts index ac16b24..7f2d02e 100644 --- a/src/api/rate-limit.server.ts +++ b/src/api/rate-limit.server.ts @@ -13,6 +13,28 @@ interface Bucket { const buckets = new Map() +/** + * Best-effort client IP for anonymous rate limiting. The LEFTMOST + * `X-Forwarded-For` entry is set by the client and trivially spoofable (rotate + * it to defeat the limit), so prefer `cf-connecting-ip` — which Cloudflare + * overwrites at the edge and a client cannot forge when the origin is only + * reachable through it — and otherwise use the RIGHTMOST hop, the address the + * trusted reverse proxy actually observed. + */ +function clientIp(c: { req: { header: (name: string) => string | undefined } }): string { + const cf = c.req.header('cf-connecting-ip') + if (cf) return cf.trim() + const xff = c.req.header('x-forwarded-for') + if (xff) { + const parts = xff + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + if (parts.length > 0) return parts[parts.length - 1]! + } + return 'unknown' +} + setInterval(() => { const now = Date.now() for (const [key, bucket] of buckets) { @@ -22,9 +44,7 @@ setInterval(() => { export const rateLimitMiddleware = createMiddleware(async (c, next) => { const userId = c.get('userId') - const key = userId - ? `user:${userId}` - : `ip:${c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'}` + const key = userId ? `user:${userId}` : `ip:${clientIp(c)}` const limit = userId ? AUTH_LIMIT : ANON_LIMIT const now = Date.now() diff --git a/src/api/records.ts b/src/api/records.ts index c561a0b..56b0d6c 100644 --- a/src/api/records.ts +++ b/src/api/records.ts @@ -6,7 +6,7 @@ import { z } from 'zod' import { db, schema } from '../db/client.server.js' import { filterRecordData, getPrivateFields } from '../lib/core/index.js' -import { type AuthEnv } from './auth.server.js' +import { type AuthEnv, fullPrincipalUserId } from './auth.server.js' const BatchRequest = z.object({ hashes: z.array(z.string()).min(1).max(10000), @@ -104,10 +104,7 @@ async function resolveRecordAccess( .from(schema.versionRecords) .innerJoin( schema.recordObjects, - and( - eq(schema.versionRecords.recordHash, schema.recordObjects.hash), - eq(schema.recordObjects.private, false), - ), + eq(schema.versionRecords.recordHash, schema.recordObjects.hash), ) .innerJoin(schema.versions, eq(schema.versionRecords.versionId, schema.versions.id)) .innerJoin( @@ -125,7 +122,15 @@ async function resolveRecordAccess( ), ) .innerJoin(schema.schemas, eq(schema.versionSchemas.schemaId, schema.schemas.id)) - .where(inArray(schema.versionRecords.recordHash, remaining)) + // Record-level privacy is per-version: a hash is publicly readable if it + // appears NOT-private in some public collection's version (under a + // non-private type). This is the correct global-lookup OR semantics. + .where( + and( + inArray(schema.versionRecords.recordHash, remaining), + eq(schema.versionRecords.private, false), + ), + ) .groupBy(schema.versionRecords.recordHash, schema.schemas.schema) for (const row of publicRows) { @@ -186,7 +191,9 @@ const app = new Hono() if (!record) return c.json({ error: 'Record not found', statusCode: 404 }, 404) - const accessEntry = (await resolveRecordAccess([recordHash], c.get('userId'))).get(recordHash) + const accessEntry = (await resolveRecordAccess([recordHash], fullPrincipalUserId(c))).get( + recordHash, + ) if (!accessEntry) return c.json({ error: 'Record not found', statusCode: 404 }, 404) const references = await db @@ -249,7 +256,8 @@ const app = new Hono() }), async (c) => { const { hashes } = c.req.valid('json') - const userId = c.get('userId') + // A collection-scoped key is treated anonymously for global hash lookups. + const userId = fullPrincipalUserId(c) const CHUNK = 500 c.header('Content-Type', 'application/x-ndjson') @@ -272,7 +280,6 @@ const app = new Hono() recordId: schema.recordObjects.recordId, type: schema.recordObjects.type, data: schema.recordObjects.data, - private: schema.recordObjects.private, }) .from(schema.recordObjects) .where(inArray(schema.recordObjects.hash, readable)) @@ -291,7 +298,6 @@ const app = new Hono() id: direct.recordId, type: direct.type, data, - private: direct.private, hash: direct.hash, }) + '\n', ) @@ -309,7 +315,6 @@ const app = new Hono() id: row.recordId, type: row.type, data: filterRecordData(row.data, pub.privateFields), - private: row.private, hash: requested, }) + '\n', ) diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 5fad27d..e63ed2a 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -4,9 +4,9 @@ import { openApi } from 'hono-zod-openapi' import { z } from 'zod' import { db, schema } from '../db/client.server.js' -import { hasOrgAccess } from '../lib/version-helpers.server.js' +import { filterTypeSchema, hashSchema, hasOrgAccess } from '../lib/version-helpers.server.js' import type { AuthEnv } from './auth.server.js' -import { requireAuth } from './auth.server.js' +import { fullPrincipalUserId, requireAuth } from './auth.server.js' async function getUsageCount(schemaId: string): Promise { const [result] = await db @@ -61,7 +61,7 @@ const app = new Hono() const pageLimit = Math.min(parseInt(limit ?? '50', 10), 100) const pageOffset = parseInt(offset ?? '0', 10) - const visible = visibleSchemaCondition(c.get('userId')) + const visible = visibleSchemaCondition(fullPrincipalUserId(c)) if (schema_hash) { const [row] = await db @@ -242,7 +242,7 @@ const app = new Hono() const [row] = await db .select() .from(schema.schemas) - .where(and(eq(schema.schemas.id, id), visibleSchemaCondition(c.get('userId')))) + .where(and(eq(schema.schemas.id, id), visibleSchemaCondition(fullPrincipalUserId(c)))) .limit(1) if (!row) return c.json({ error: 'Schema not found', statusCode: 404 }, 404) @@ -311,7 +311,11 @@ const app = new Hono() if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) - if (!collection.public && !(await hasOrgAccess(c.get('userId'), collection.organizationId))) { + const scopedCollections = c.get('apiKeyCollectionIds') + const keyScopeOk = !scopedCollections || scopedCollections.includes(collection.id) + const ownerAccess = + keyScopeOk && (await hasOrgAccess(c.get('userId'), collection.organizationId)) + if (!collection.public && !ownerAccess) { return c.json({ error: 'Collection not found', statusCode: 404 }, 404) } @@ -367,22 +371,30 @@ const app = new Hono() return c.json({ version: version.semver, semver: version.semver, - schemas: entries.map((e) => { - const labels = labelsMap.get(e.schemaId) ?? [] - const body = - raw === 'true' + // Non-owners never see private types, and private field definitions are + // stripped (with the schema hash recomputed over the filtered body). + schemas: entries + .filter((e) => ownerAccess || (e.schemaBody as any)?.private !== true) + .map((e) => { + const labels = labelsMap.get(e.schemaId) ?? [] + const filteredBody = ownerAccess ? e.schemaBody - : labels.length > 0 - ? { ...(e.schemaBody as object), 'x-underlay-labels': labels } - : e.schemaBody - - return { - slug: e.slug, - schemaId: e.schemaId, - schemaHash: e.schemaHash, - schema: body, - } - }), + : filterTypeSchema(e.schemaBody as Record) + const schemaHash = ownerAccess ? e.schemaHash : hashSchema(filteredBody) + const body = + raw === 'true' + ? filteredBody + : labels.length > 0 + ? { ...(filteredBody as object), 'x-underlay-labels': labels } + : filteredBody + + return { + slug: e.slug, + schemaId: e.schemaId, + schemaHash, + schema: body, + } + }), }) }, ) diff --git a/src/api/versions.ts b/src/api/versions.ts index a5ab483..9961947 100644 --- a/src/api/versions.ts +++ b/src/api/versions.ts @@ -29,6 +29,28 @@ import { type AuthEnv, requireAuth } from './auth.server.js' const MAX_METADATA_BYTES = 64 * 1024 +/** + * Strip owner-only data from a version row before returning it to a non-owner: + * private-type entries in `typeCounts` (which would disclose the existence and + * exact size of private types) and the internal provenance fields. + */ +function sanitizeVersionForPublic( + version: Record, + privateTypes: Set, +): Record { + const out: Record = { ...version } + const tc = version.typeCounts as Record | null | undefined + if (tc) { + out.typeCounts = Object.fromEntries( + Object.entries(tc).filter(([type]) => !privateTypes.has(type)), + ) + } + delete out.pushedBy + delete out.actorId + delete out.signature + return out +} + // Offset pagination is O(offset): Postgres must produce and discard every // skipped row. Past a few thousand rows on a large version it exceeds the // statement timeout. Reject deep offsets with a clear 400 and steer clients to @@ -184,7 +206,12 @@ const app = new Hono() const limit = c.req.query('limit') const offset = c.req.query('offset') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const ownerAccess = collection.ownerAccess @@ -213,13 +240,15 @@ const app = new Hono() .limit(Math.min(parseInt(limit ?? '50', 10), 100)) .offset(parseInt(offset ?? '0', 10)) + // `actorId` is owner-only here, matching latest/:n (sanitizeVersionForPublic). + // Without this the list endpoint leaked it while the detail endpoints hid it. return c.json( rows.map((row) => ({ semver: row.semver, hash: ownerAccess ? row.hash : (row.publicHash ?? row.hash), message: row.message, appId: row.appId, - actorId: row.actorId, + ...(ownerAccess ? { actorId: row.actorId } : {}), recordCount: row.recordCount, fileCount: row.fileCount, totalBytes: row.totalBytes, @@ -242,7 +271,12 @@ const app = new Hono() }), async (c) => { const { owner, slug } = c.req.valid('param') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const version = await getLatestReadyVersion(collection.id) @@ -257,8 +291,12 @@ const app = new Hono() ? Object.fromEntries(schemaEntries.map((e) => [e.slug, e.schema])) : filterSchemasForPublic(schemaEntries) + const versionView = ownerAccess + ? version + : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)) + return c.json({ - ...version, + ...versionView, hash: ownerAccess ? version.hash : (version.publicHash ?? version.hash), schemas: schemasMap, ark: arkInfo @@ -278,7 +316,12 @@ const app = new Hono() }), async (c) => { const { owner, slug, n } = c.req.valid('param') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver } = parseSemver(n) @@ -304,8 +347,12 @@ const app = new Hono() ? Object.fromEntries(schemaEntries.map((e) => [e.slug, e.schema])) : filterSchemasForPublic(schemaEntries) + const versionView = ownerAccess + ? version + : sanitizeVersionForPublic(version, getPrivateTypes(schemaEntries)) + return c.json({ - ...version, + ...versionView, hash: ownerAccess ? version.hash : (version.publicHash ?? version.hash), schemas: schemasMap, ark: arkInfo @@ -332,7 +379,12 @@ const app = new Hono() // client that sends ?cursor= isn't silently reset to offset 0. const after = c.req.query('after') ?? c.req.query('cursor') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver } = parseSemver(n) @@ -379,8 +431,8 @@ const app = new Hono() conditions.push(sql`${schema.versionRecords.type} != ${pt}`) } } - // Exclude record-level private records - conditions.push(eq(schema.recordObjects.private, false)) + // Exclude record-level private records (per-version flag). + conditions.push(eq(schema.versionRecords.private, false)) } const pageLimit = Math.min(parseInt(limit ?? '100', 10), MAX_RECORDS_LIMIT) @@ -530,7 +582,12 @@ const app = new Hono() const type = c.req.query('type') const after = c.req.query('after') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver } = parseSemver(n) @@ -573,6 +630,31 @@ const app = new Hono() const client = db.$client const privateTypeList = [...privateTypes] + // The count a caller can actually verify against. `version.recordCount` is + // the version's full total, so for a non-owner reading a collection with + // private records or types the stream is legitimately shorter — and the + // documented "count the lines, resume if they differ" check would never + // terminate. Count what THIS caller will receive instead. One indexed + // count is negligible next to streaming the rows. + let streamedRecordCount = version.recordCount + if (!ownerAccess || type) { + const countConditions = [eq(schema.versionRecords.versionId, version.id)] + if (type) countConditions.push(eq(schema.versionRecords.type, type)) + if (!ownerAccess) { + countConditions.push(eq(schema.versionRecords.private, false)) + if (privateTypeList.length > 0) { + countConditions.push( + sql`${schema.versionRecords.type} <> ALL(${sql.param(privateTypeList)}::text[])`, + ) + } + } + const [countRow] = await db + .select({ n: sql`count(*)::int` }) + .from(schema.versionRecords) + .where(and(...countConditions)) + streamedRecordCount = countRow?.n ?? 0 + } + // Hono's compress() middleware deliberately skips this response: its // compressible-type list covers application/json and +json suffixes but // not application/x-ndjson, and it bails on anything already marked @@ -632,7 +714,7 @@ const app = new Hono() WHERE vr.version_id = ${version.id} ${type ? client`AND vr.type = ${type}` : client``} ${keyset} - ${ownerAccess ? client`` : client`AND ro.private = false AND vr.type <> ALL(${privateTypeList}::text[])`} + ${ownerAccess ? client`` : client`AND vr.private = false AND vr.type <> ALL(${privateTypeList}::text[])`} ORDER BY vr.record_id, vr.record_hash LIMIT ${BATCH} ` @@ -669,8 +751,10 @@ const app = new Hono() const headers: Record = { 'Content-Type': 'application/x-ndjson', - // Lets a client verify completeness without a second request. - 'X-Underlay-Record-Count': String(version.recordCount), + // Lets a client verify completeness without a second request. This is the + // count for THIS request (privacy-filtered, and `?type=`-scoped), so + // comparing it against the lines received is an exact check for everyone. + 'X-Underlay-Record-Count': String(streamedRecordCount), } if (acceptsGzip) headers['Content-Encoding'] = 'gzip' @@ -697,7 +781,12 @@ const app = new Hono() }), async (c) => { const { owner, slug, n } = c.req.valid('param') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver } = parseSemver(n) @@ -715,6 +804,18 @@ const app = new Hono() if (!version) return c.json({ error: 'Version not found', statusCode: 404 }, 404) + const ownerAccess = collection.ownerAccess + + // For non-owners, private types/fields must not appear in the references, + // and files reachable only through private content must not be listed. + let privateTypes = new Set() + const privateFieldsByType = new Map>() + if (!ownerAccess) { + const schemaEntries = await loadVersionSchemas(version.id) + privateTypes = getPrivateTypes(schemaEntries) + for (const e of schemaEntries) privateFieldsByType.set(e.slug, getPrivateFields(e.schema)) + } + const fileRows = await db .select({ hash: schema.versionFiles.fileHash, @@ -726,7 +827,13 @@ const app = new Hono() .innerJoin(schema.files, eq(schema.versionFiles.fileHash, schema.files.hash)) .where(eq(schema.versionFiles.versionId, version.id)) - // Only load records that contain $file references (DB-level filter) + // Only load records that contain $file references (DB-level filter). + // Non-owners never see records flagged private. + const refConditions = [ + eq(schema.versionRecords.versionId, version.id), + sql`${schema.recordObjects.data}::text LIKE '%"$file"%'`, + ] + if (!ownerAccess) refConditions.push(eq(schema.versionRecords.private, false)) const fileRefRecords = await db .select({ recordId: schema.recordObjects.recordId, @@ -738,17 +845,17 @@ const app = new Hono() schema.recordObjects, eq(schema.versionRecords.recordHash, schema.recordObjects.hash), ) - .where( - and( - eq(schema.versionRecords.versionId, version.id), - sql`${schema.recordObjects.data}::text LIKE '%"$file"%'`, - ), - ) + .where(and(...refConditions)) const fileRefs = new Map() for (const rec of fileRefRecords) { + if (!ownerAccess && privateTypes.has(rec.type)) continue + const privateFields = ownerAccess + ? undefined + : (privateFieldsByType.get(rec.type) ?? new Set()) const data = rec.data as Record for (const [field, val] of Object.entries(data)) { + if (privateFields?.has(field)) continue if (val && typeof val === 'object' && '$file' in (val as any)) { const hash = ((val as any).$file as string).replace('sha256:', '') if (!fileRefs.has(hash)) fileRefs.set(hash, []) @@ -757,8 +864,11 @@ const app = new Hono() } } + // Non-owners only see files still reachable through a non-private reference. + const visibleFileRows = ownerAccess ? fileRows : fileRows.filter((f) => fileRefs.has(f.hash)) + return c.json( - fileRows.map((f) => ({ + visibleFileRows.map((f) => ({ ...f, references: fileRefs.get(f.hash) ?? [], })), @@ -777,7 +887,12 @@ const app = new Hono() async (c) => { const { owner, slug, n } = c.req.valid('param') const sinceParam = c.req.query('since') - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver } = parseSemver(n) @@ -810,19 +925,19 @@ const app = new Hono() // content-address (hash of the filtered record), so readers can verify // what they actually receive. // - // Type and record id come off version_records, so only the record-level - // `private` flag still needs record_objects — the owner path joins - // nothing at all. + // Record-level privacy is the per-version `version_records.private` flag and + // type/id/public-hash are all on version_records too, so these queries never + // join record_objects at all. const ownerAccess = collection.ownerAccess const privateTypes = ownerAccess ? new Set() : getPrivateTypes(schemaEntries) - const privacyJoin = ownerAccess - ? sql`` - : sql`INNER JOIN record_objects ro ON ro.hash = vr.record_hash` + // `NOT IN (${array})` must NOT be used here: drizzle renders an embedded + // array as a row constructor — `NOT IN (($1,$2))` — which Postgres rejects + // with `operator does not exist: text <> record`. Use `<> ALL(array)`. + const typeExclusion = (types: Set) => + types.size > 0 ? sql`AND vr.type <> ALL(${sql.param([...types])}::text[])` : sql`` const privacyWhere = ownerAccess ? sql`` - : privateTypes.size > 0 - ? sql`AND ro.private = false AND vr.type NOT IN (${[...privateTypes]})` - : sql`AND ro.private = false` + : sql`AND vr.private = false ${typeExclusion(privateTypes)}` const servedHash = ownerAccess ? sql`vr.record_hash` : sql`coalesce(vr.public_record_hash, vr.record_hash)` @@ -864,37 +979,71 @@ const app = new Hono() type DeltaRow = { id: string; type: string; hash: string; recordHash: string } type UpdatedRow = DeltaRow & { previousHash: string | null } + // The `removed` list iterates the `since` version and `previousHash` + // reads the since record, so both must judge privacy against the SINCE + // version's own private-type set (which can differ from the target's). + const sincePrivateTypes = ownerAccess + ? new Set() + : getPrivateTypes(await loadVersionSchemas(sinceId)) + + // Non-owners get the previous version's PUBLIC served hash, and only when + // that prior record was itself public — otherwise `previousHash` is NULL. + // Never serve `s.record_hash` for a private prior record; that would leak + // the private content's digest. + const sinceTypeGuard = + sincePrivateTypes.size > 0 + ? sql`AND s.type <> ALL(${sql.param([...sincePrivateTypes])}::text[])` + : sql`` const previousServedHash = ownerAccess ? sql`(SELECT s.record_hash FROM version_records s WHERE s.version_id = ${sinceId} AND s.record_id = vr.record_id LIMIT 1)` - : sql`(SELECT coalesce(s.public_record_hash, s.record_hash) FROM version_records s + : sql`(SELECT CASE WHEN s.private = false ${sinceTypeGuard} + THEN coalesce(s.public_record_hash, s.record_hash) END + FROM version_records s WHERE s.version_id = ${sinceId} AND s.record_id = vr.record_id LIMIT 1)` - /** One delta list. `presence` selects the anti- or semi-join. */ + // Visibility of a row for the CALLER, evaluated against the version that + // row belongs to (private-type sets differ between versions). Applied to + // BOTH the iterated rows and the presence subquery, so that a record + // whose privacy flipped with unchanged content still shows up: becoming + // private reads as `removed`, becoming public reads as `added`. Without + // it such a record is in neither list and delta-following mirrors keep + // serving content the full manifest no longer includes. + const visible = (alias: string, types: Set) => + ownerAccess + ? sql`` + : types.size > 0 + ? sql`AND ${sql.raw(alias)}.private = false AND ${sql.raw(alias)}.type <> ALL(${sql.param([...types])}::text[])` + : sql`AND ${sql.raw(alias)}.private = false` + const deltaQuery = ( - versionId: number, + selfId: number, + selfTypes: Set, otherId: number, + otherTypes: Set, presence: 'absent' | 'changed', cursor: ListCursor, extraColumn = sql``, ) => sql` SELECT vr.record_id AS id, vr.type, ${servedHash} AS hash, vr.record_hash AS "recordHash"${extraColumn} - FROM version_records vr ${privacyJoin} - WHERE vr.version_id = ${versionId} + FROM version_records vr + WHERE vr.version_id = ${selfId} ${ presence === 'absent' ? sql`AND NOT EXISTS ( SELECT 1 FROM version_records s WHERE s.version_id = ${otherId} AND s.record_id = vr.record_id + ${visible('s', otherTypes)} )` : sql`AND EXISTS ( SELECT 1 FROM version_records s WHERE s.version_id = ${otherId} AND s.record_id = vr.record_id AND s.record_hash <> vr.record_hash + ${visible('s', otherTypes)} )` } - ${privacyWhere} ${afterCursor('vr', cursor)} + ${visible('vr', selfTypes)} ${afterCursor('vr', cursor)} ORDER BY vr.record_id, vr.record_hash LIMIT ${limit + 1} ` @@ -908,13 +1057,28 @@ const app = new Hono() ? Promise.resolve([] as T[]) : (tx.execute(query) as unknown as Promise) return Promise.all([ - run(at.added, deltaQuery(targetId, sinceId, 'absent', at.added)), - run(at.removed, deltaQuery(sinceId, targetId, 'absent', at.removed)), + run( + at.added, + deltaQuery(targetId, privateTypes, sinceId, sincePrivateTypes, 'absent', at.added), + ), + run( + at.removed, + deltaQuery( + sinceId, + sincePrivateTypes, + targetId, + privateTypes, + 'absent', + at.removed, + ), + ), run( at.updated, deltaQuery( targetId, + privateTypes, sinceId, + sincePrivateTypes, 'changed', at.updated, sql`, ${previousServedHash} AS "previousHash"`, @@ -957,25 +1121,39 @@ const app = new Hono() // hash, which for public readers is a coalesce() expression and therefore // an unindexed sort of the whole version. const at = decodeDeltaCursor(cursor) + // `private` is echoed back so a manifest round-trip is lossless. Privacy is + // authored per push and omitting it on a re-push means PUBLIC, so a client + // that reads a manifest, edits it and pushes it back would silently + // de-privatize every record if it could not read the flag. Only `true` is + // emitted (a non-owner's rows are all public, so they see none). const recordRows = (await withStatementTimeout(DELTA_STATEMENT_TIMEOUT_MS, async (tx) => tx.execute(sql` SELECT vr.record_id AS id, vr.type, ${servedHash} AS hash, - vr.record_hash AS "recordHash" - FROM version_records vr ${privacyJoin} + vr.record_hash AS "recordHash", vr.private + FROM version_records vr WHERE vr.version_id = ${version.id} ${privacyWhere} ${afterCursor('vr', at.added)} ORDER BY vr.record_id, vr.record_hash LIMIT ${limit + 1} `), - )) as unknown as { id: string; type: string; hash: string; recordHash: string }[] + )) as unknown as { + id: string + type: string + hash: string + recordHash: string + private: boolean + }[] const { page, next, hasMore } = paginate(recordRows, limit) + const records = page.map(({ private: isPrivate, ...rest }) => + isPrivate ? { ...rest, private: true } : rest, + ) return c.json({ semver: version.semver, hash: manifestHash, schemas: schemasOut, - records: page, + records, files: fileHashes.map((f) => f.hash), pagination: { limit, @@ -1000,7 +1178,12 @@ const app = new Hono() const diffLimit = Math.min(parseInt(c.req.query('limit') ?? '500', 10), MAX_DIFF_LIMIT) const diffCursor = decodeDeltaCursor(c.req.query('cursor')) - const collection = await resolveAccessibleCollection(owner, slug, c.get('userId')) + const collection = await resolveAccessibleCollection( + owner, + slug, + c.get('userId'), + c.get('apiKeyCollectionIds'), + ) if (!collection) return c.json({ error: 'Collection not found', statusCode: 404 }, 404) const { semver: targetSemver } = parseSemver(n) @@ -1043,45 +1226,61 @@ const app = new Hono() const fromId = fromVersion?.id // Privacy filtering for non-owners: hide private types and private records. - // record_objects is joined for the body (and the record-level `private` - // flag); the set operations themselves run on version_records alone. + // record_objects is joined only for the record body (ro.data); record-level + // privacy is the per-version `version_records.private` flag. const targetSchemas = await loadVersionSchemas(targetVersion.id) const ownerAccess = collection.ownerAccess const privateTypes = ownerAccess ? new Set() : getPrivateTypes(targetSchemas) - const privacyWhere = ownerAccess - ? sql`` - : privateTypes.size > 0 - ? sql`AND ro.private = false AND vr.type NOT IN (${[...privateTypes]})` - : sql`AND ro.private = false` + const fromPrivateTypes = + ownerAccess || !fromId + ? new Set() + : getPrivateTypes(await loadVersionSchemas(fromId)) + + // Caller-visibility of a row, judged against its OWN version's private-type + // set. Applied to the iterated rows and to the presence subquery alike, so a + // record whose privacy flipped with unchanged content is reported (redacted + // ⇒ `removed`, un-redacted ⇒ `added`) instead of vanishing from the diff. + // `<> ALL(array)`, never `NOT IN (${array})` — drizzle renders an embedded + // array as a row constructor, which Postgres rejects. + const visible = (alias: string, types: Set) => + ownerAccess + ? sql`` + : types.size > 0 + ? sql`AND ${sql.raw(alias)}.private = false AND ${sql.raw(alias)}.type <> ALL(${sql.param([...types])}::text[])` + : sql`AND ${sql.raw(alias)}.private = false` type DiffRow = { id: string; type: string; data: unknown; recordHash: string } - /** One side of the diff: rows of `versionId` absent from / changed in `otherId`. */ + /** One side of the diff: rows of `selfId` absent from / changed in `otherId`. */ const diffQuery = ( - versionId: number, + selfId: number, + selfTypes: Set, otherId: number | undefined, + otherTypes: Set, mode: 'absent' | 'changed' | 'all', cursor: ListCursor, ) => sql` SELECT vr.record_id AS id, vr.type, ro.data, vr.record_hash AS "recordHash" FROM version_records vr INNER JOIN record_objects ro ON ro.hash = vr.record_hash - WHERE vr.version_id = ${versionId} + WHERE vr.version_id = ${selfId} ${ mode === 'absent' ? sql`AND NOT EXISTS ( SELECT 1 FROM version_records s WHERE s.version_id = ${otherId!} AND s.record_id = vr.record_id + ${visible('s', otherTypes)} )` : mode === 'changed' ? sql`AND EXISTS ( SELECT 1 FROM version_records s WHERE s.version_id = ${otherId!} AND s.record_id = vr.record_id AND s.record_hash <> vr.record_hash + ${visible('s', otherTypes)} )` : sql`` } - ${privacyWhere} ${afterCursor('vr', cursor)} + ${visible('vr', selfTypes)} ${afterCursor('vr', cursor)} ORDER BY vr.record_id, vr.record_hash LIMIT ${diffLimit + 1} ` @@ -1098,15 +1297,40 @@ const app = new Hono() return Promise.all([ run( diffCursor.added, - diffQuery(targetId, fromId, fromId ? 'absent' : 'all', diffCursor.added), + diffQuery( + targetId, + privateTypes, + fromId, + fromPrivateTypes, + fromId ? 'absent' : 'all', + diffCursor.added, + ), ), run( diffCursor.removed, - fromId ? diffQuery(fromId, targetId, 'absent', diffCursor.removed) : null, + fromId + ? diffQuery( + fromId, + fromPrivateTypes, + targetId, + privateTypes, + 'absent', + diffCursor.removed, + ) + : null, ), run( diffCursor.updated, - fromId ? diffQuery(targetId, fromId, 'changed', diffCursor.updated) : null, + fromId + ? diffQuery( + targetId, + privateTypes, + fromId, + fromPrivateTypes, + 'changed', + diffCursor.updated, + ) + : null, ), ]) }, @@ -1292,9 +1516,8 @@ const app = new Hono() await client` SELECT coalesce(vr.public_record_hash, vr.record_hash) AS h FROM version_records vr - INNER JOIN record_objects ro ON ro.hash = vr.record_hash WHERE vr.version_id = ${latest.id} - AND NOT ro.private + AND NOT vr.private AND vr.type <> ALL(${[...privateTypes]}::text[]) ORDER BY coalesce(vr.public_record_hash, vr.record_hash) COLLATE "C" `.cursor(CURSOR_CHUNK, (rows) => { @@ -1342,10 +1565,13 @@ const app = new Hono() } // Copy the record set server-side. The schema set is unchanged, so every - // column — including the public content-addresses — carries over as-is. + // column — including the public content-addresses and the per-version + // `private` flag — carries over as-is. `private` MUST be copied: it + // defaults to false, so omitting it would silently de-privatize every + // private record in the new (and now latest) version. await tx.execute(sql` - INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type) - SELECT ${version!.id}, record_hash, public_record_hash, record_id, type + INSERT INTO version_records (version_id, record_hash, public_record_hash, record_id, type, private) + SELECT ${version!.id}, record_hash, public_record_hash, record_id, type, private FROM version_records WHERE version_id = ${latest.id} `) diff --git a/src/cli/commands/push.ts b/src/cli/commands/push.ts index 86bc676..656fd59 100644 --- a/src/cli/commands/push.ts +++ b/src/cli/commands/push.ts @@ -60,6 +60,13 @@ export async function push(remoteName: string = 'origin'): Promise { } // 3. Build manifest + // + // ⚠️ Record privacy is NOT carried here. The server treats each push's manifest + // as the authoritative statement of which records are private, so omitting the + // flag publishes every record: re-pushing a collection that has private records + // would silently expose them in the new version. `add` also drops `private` + // before this point, so there is nothing to send yet. Both must be fixed before + // this CLI is published — see packages/cli/README.md. const manifest = version.records .map((hash) => { const obj = readObject(root, hash) diff --git a/src/db/migrations/0011_wealthy_hardball.sql b/src/db/migrations/0011_wealthy_hardball.sql new file mode 100644 index 0000000..47755ce --- /dev/null +++ b/src/db/migrations/0011_wealthy_hardball.sql @@ -0,0 +1,10 @@ +ALTER TABLE "version_records" ADD COLUMN "private" boolean DEFAULT false NOT NULL;--> statement-breakpoint +-- Backfill the new per-version record-privacy flag from the (about-to-be-dropped) +-- global record_objects.private, so existing versions keep their historical +-- record-level privacy. Only rows that were actually private are touched, so the +-- cost is proportional to the number of private records — near zero in practice — +-- not to the size of version_records. +UPDATE "version_records" vr SET "private" = true +FROM "record_objects" ro +WHERE ro."hash" = vr."record_hash" AND ro."private" = true;--> statement-breakpoint +ALTER TABLE "record_objects" DROP COLUMN "private"; diff --git a/src/db/migrations/0012_sloppy_nightcrawler.sql b/src/db/migrations/0012_sloppy_nightcrawler.sql new file mode 100644 index 0000000..e128718 --- /dev/null +++ b/src/db/migrations/0012_sloppy_nightcrawler.sql @@ -0,0 +1,2 @@ +ALTER TABLE "versions" DROP CONSTRAINT "versions_collection_id_hash_unique";--> statement-breakpoint +ALTER TABLE "versions" ADD CONSTRAINT "versions_collection_id_hash_public_hash_unique" UNIQUE("collection_id","hash","public_hash"); \ No newline at end of file diff --git a/src/db/migrations/meta/0011_snapshot.json b/src/db/migrations/meta/0011_snapshot.json new file mode 100644 index 0000000..6a27f20 --- /dev/null +++ b/src/db/migrations/meta/0011_snapshot.json @@ -0,0 +1,2746 @@ +{ + "id": "a6a02887-ae8c-482b-8ffa-573761b54e12", + "prevId": "e6efbada-0ca0-4c16-9234-2dc933096a1e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_collections": { + "name": "ark_collections", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ark_id": { + "name": "ark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_url": { + "name": "custom_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_collections_collection_id_collections_id_fk": { + "name": "ark_collections_collection_id_collections_id_fk", + "tableFrom": "ark_collections", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_collections_ark_id_unique": { + "name": "ark_collections_ark_id_unique", + "nullsNotDistinct": false, + "columns": ["ark_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_record_types": { + "name": "ark_record_types", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url_field": { + "name": "redirect_url_field", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ark_record_types_collection_id_collections_id_fk": { + "name": "ark_record_types_collection_id_collections_id_fk", + "tableFrom": "ark_record_types", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ark_record_types_collection_id_record_type_pk": { + "name": "ark_record_types_collection_id_record_type_pk", + "columns": ["collection_id", "record_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_shoulders": { + "name": "ark_shoulders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shoulder": { + "name": "shoulder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_shoulders_organization_id_organization_id_fk": { + "name": "ark_shoulders_organization_id_organization_id_fk", + "tableFrom": "ark_shoulders", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_shoulders_organization_id_unique": { + "name": "ark_shoulders_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["organization_id"] + }, + "ark_shoulders_shoulder_unique": { + "name": "ark_shoulders_shoulder_unique", + "nullsNotDistinct": false, + "columns": ["shoulder"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_webhooks": { + "name": "collection_webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bump_filter": { + "name": "bump_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{major,minor,patch}'::text[]" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_delivery_at": { + "name": "last_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collection_webhooks_collection_id_idx": { + "name": "collection_webhooks_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_webhooks_collection_id_collections_id_fk": { + "name": "collection_webhooks_collection_id_collections_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_webhooks_created_by_user_id_fk": { + "name": "collection_webhooks_created_by_user_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collections_organization_id_idx": { + "name": "collections_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_organization_id_organization_id_fk": { + "name": "collections_organization_id_organization_id_fk", + "tableFrom": "collections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collections_forked_from_collections_id_fk": { + "name": "collections_forked_from_collections_id_fk", + "tableFrom": "collections", + "tableTo": "collections", + "columnsFrom": ["forked_from"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "collections_organization_id_slug_unique": { + "name": "collections_organization_id_slug_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_session_manifest": { + "name": "negotiate_session_manifest", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needed": { + "name": "needed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "submitted": { + "name": "submitted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "final_hash": { + "name": "final_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "nsm_session_needed_idx": { + "name": "nsm_session_needed_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "needed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "negotiate_session_manifest_session_id_negotiate_sessions_id_fk": { + "name": "negotiate_session_manifest_session_id_negotiate_sessions_id_fk", + "tableFrom": "negotiate_session_manifest", + "tableTo": "negotiate_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "negotiate_session_manifest_session_id_hash_pk": { + "name": "negotiate_session_manifest_session_id_hash_pk", + "columns": ["session_id", "hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_sessions": { + "name": "negotiate_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schemas": { + "name": "schemas", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_hashes": { + "name": "file_hashes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "needed_files": { + "name": "needed_files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "strip_unknown_fields": { + "name": "strip_unknown_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_expected": { + "name": "manifest_expected", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finalize_started_at": { + "name": "finalize_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "negotiate_sessions_collection_id_collections_id_fk": { + "name": "negotiate_sessions_collection_id_collections_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "negotiate_sessions_user_id_user_id_fk": { + "name": "negotiate_sessions_user_id_user_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ark_naan": { + "name": "ark_naan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kf_org_id": { + "name": "kf_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_comments": { + "name": "page_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quote_context": { + "name": "quote_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_comments_page_anchor_idx": { + "name": "page_comments_page_anchor_idx", + "columns": [ + { + "expression": "page", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comments_user_id_idx": { + "name": "page_comments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comments_user_id_user_id_fk": { + "name": "page_comments_user_id_user_id_fk", + "tableFrom": "page_comments", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.record_objects": { + "name": "record_objects", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "record_objects_record_id_idx": { + "name": "record_objects_record_id_idx", + "columns": [ + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schema_labels": { + "name": "schema_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_labels_label_idx": { + "name": "schema_labels_label_idx", + "columns": [ + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_labels_schema_id_schemas_id_fk": { + "name": "schema_labels_schema_id_schemas_id_fk", + "tableFrom": "schema_labels", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schema_labels_schema_id_label_unique": { + "name": "schema_labels_schema_id_label_unique", + "nullsNotDistinct": false, + "columns": ["schema_id", "label"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schemas_schema_hash_unique": { + "name": "schemas_schema_hash_unique", + "nullsNotDistinct": false, + "columns": ["schema_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_runs": { + "name": "sync_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "collections_synced": { + "name": "collections_synced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_created": { + "name": "collections_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_failed": { + "name": "collections_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "versions_pulled": { + "name": "versions_pulled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_downloaded": { + "name": "files_downloaded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_skipped": { + "name": "files_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_files": { + "name": "version_files", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_files_file_hash_idx": { + "name": "version_files_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_files_version_id_versions_id_fk": { + "name": "version_files_version_id_versions_id_fk", + "tableFrom": "version_files", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_files_file_hash_files_hash_fk": { + "name": "version_files_file_hash_files_hash_fk", + "tableFrom": "version_files", + "tableTo": "files", + "columnsFrom": ["file_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_files_version_id_file_hash_pk": { + "name": "version_files_version_id_file_hash_pk", + "columns": ["version_id", "file_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_records": { + "name": "version_records", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "record_hash": { + "name": "record_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_record_hash": { + "name": "public_record_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_records_record_hash_idx": { + "name": "version_records_record_hash_idx", + "columns": [ + { + "expression": "record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_public_record_hash_idx": { + "name": "version_records_public_record_hash_idx", + "columns": [ + { + "expression": "public_record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "public_record_hash IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_record_idx": { + "name": "version_records_version_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_type_record_idx": { + "name": "version_records_version_type_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_records_version_id_versions_id_fk": { + "name": "version_records_version_id_versions_id_fk", + "tableFrom": "version_records", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_records_record_hash_record_objects_hash_fk": { + "name": "version_records_record_hash_record_objects_hash_fk", + "tableFrom": "version_records", + "tableTo": "record_objects", + "columnsFrom": ["record_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_records_version_id_record_hash_pk": { + "name": "version_records_version_id_record_hash_pk", + "columns": ["version_id", "record_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_schemas": { + "name": "version_schemas", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_schemas_schema_id_idx": { + "name": "version_schemas_schema_id_idx", + "columns": [ + { + "expression": "schema_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_schemas_version_id_versions_id_fk": { + "name": "version_schemas_version_id_versions_id_fk", + "tableFrom": "version_schemas", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_schemas_schema_id_schemas_id_fk": { + "name": "version_schemas_schema_id_schemas_id_fk", + "tableFrom": "version_schemas", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_schemas_version_id_slug_pk": { + "name": "version_schemas_version_id_slug_pk", + "columns": ["version_id", "slug"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.versions": { + "name": "versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pushed_by": { + "name": "pushed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_counts": { + "name": "type_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "versions_ordering_idx": { + "name": "versions_ordering_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "versions_collection_id_collections_id_fk": { + "name": "versions_collection_id_collections_id_fk", + "tableFrom": "versions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "versions_pushed_by_user_id_fk": { + "name": "versions_pushed_by_user_id_fk", + "tableFrom": "versions", + "tableTo": "user", + "columnsFrom": ["pushed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "versions_collection_id_semver_unique": { + "name": "versions_collection_id_semver_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "semver"] + }, + "versions_collection_id_hash_unique": { + "name": "versions_collection_id_hash_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bump_type": { + "name": "bump_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'version.created'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_webhook_id_idx": { + "name": "webhook_deliveries_webhook_id_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_collection_id_idx": { + "name": "webhook_deliveries_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_created_at_idx": { + "name": "webhook_deliveries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_sweep_idx": { + "name": "webhook_deliveries_sweep_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_collection_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_collection_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collection_webhooks", + "columnsFrom": ["webhook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_collection_id_collections_id_fk": { + "name": "webhook_deliveries_collection_id_collections_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_version_id_versions_id_fk": { + "name": "webhook_deliveries_version_id_versions_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/0012_snapshot.json b/src/db/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..81ee5bf --- /dev/null +++ b/src/db/migrations/meta/0012_snapshot.json @@ -0,0 +1,2746 @@ +{ + "id": "50b02777-a57b-4c16-ad39-92a2162f818f", + "prevId": "a6a02887-ae8c-482b-8ffa-573761b54e12", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_collections": { + "name": "ark_collections", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ark_id": { + "name": "ark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_url": { + "name": "custom_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_collections_collection_id_collections_id_fk": { + "name": "ark_collections_collection_id_collections_id_fk", + "tableFrom": "ark_collections", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_collections_ark_id_unique": { + "name": "ark_collections_ark_id_unique", + "nullsNotDistinct": false, + "columns": ["ark_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_record_types": { + "name": "ark_record_types", + "schema": "", + "columns": { + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_type": { + "name": "record_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url_field": { + "name": "redirect_url_field", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "ark_record_types_collection_id_collections_id_fk": { + "name": "ark_record_types_collection_id_collections_id_fk", + "tableFrom": "ark_record_types", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ark_record_types_collection_id_record_type_pk": { + "name": "ark_record_types_collection_id_record_type_pk", + "columns": ["collection_id", "record_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ark_shoulders": { + "name": "ark_shoulders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shoulder": { + "name": "shoulder", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ark_shoulders_organization_id_organization_id_fk": { + "name": "ark_shoulders_organization_id_organization_id_fk", + "tableFrom": "ark_shoulders", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ark_shoulders_organization_id_unique": { + "name": "ark_shoulders_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["organization_id"] + }, + "ark_shoulders_shoulder_unique": { + "name": "ark_shoulders_shoulder_unique", + "nullsNotDistinct": false, + "columns": ["shoulder"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collection_webhooks": { + "name": "collection_webhooks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bump_filter": { + "name": "bump_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{major,minor,patch}'::text[]" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_delivery_at": { + "name": "last_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "collection_webhooks_collection_id_idx": { + "name": "collection_webhooks_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collection_webhooks_collection_id_collections_id_fk": { + "name": "collection_webhooks_collection_id_collections_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collection_webhooks_created_by_user_id_fk": { + "name": "collection_webhooks_created_by_user_id_fk", + "tableFrom": "collection_webhooks", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.collections": { + "name": "collections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "forked_from": { + "name": "forked_from", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "collections_organization_id_idx": { + "name": "collections_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "collections_organization_id_organization_id_fk": { + "name": "collections_organization_id_organization_id_fk", + "tableFrom": "collections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "collections_forked_from_collections_id_fk": { + "name": "collections_forked_from_collections_id_fk", + "tableFrom": "collections", + "tableTo": "collections", + "columnsFrom": ["forked_from"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "collections_organization_id_slug_unique": { + "name": "collections_organization_id_slug_unique", + "nullsNotDistinct": false, + "columns": ["organization_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.files": { + "name": "files", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_session_manifest": { + "name": "negotiate_session_manifest", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "needed": { + "name": "needed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "submitted": { + "name": "submitted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "final_hash": { + "name": "final_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "nsm_session_needed_idx": { + "name": "nsm_session_needed_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "needed", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "negotiate_session_manifest_session_id_negotiate_sessions_id_fk": { + "name": "negotiate_session_manifest_session_id_negotiate_sessions_id_fk", + "tableFrom": "negotiate_session_manifest", + "tableTo": "negotiate_sessions", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "negotiate_session_manifest_session_id_hash_pk": { + "name": "negotiate_session_manifest_session_id_hash_pk", + "columns": ["session_id", "hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.negotiate_sessions": { + "name": "negotiate_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schemas": { + "name": "schemas", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "file_hashes": { + "name": "file_hashes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "needed_files": { + "name": "needed_files", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "strip_unknown_fields": { + "name": "strip_unknown_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "manifest_expected": { + "name": "manifest_expected", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "finalize_started_at": { + "name": "finalize_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "negotiate_sessions_collection_id_collections_id_fk": { + "name": "negotiate_sessions_collection_id_collections_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "negotiate_sessions_user_id_user_id_fk": { + "name": "negotiate_sessions_user_id_user_id_fk", + "tableFrom": "negotiate_sessions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ark_naan": { + "name": "ark_naan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kf_org_id": { + "name": "kf_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_comments": { + "name": "page_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "page": { + "name": "page", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quote": { + "name": "quote", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quote_context": { + "name": "quote_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "page_comments_page_anchor_idx": { + "name": "page_comments_page_anchor_idx", + "columns": [ + { + "expression": "page", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "anchor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "page_comments_user_id_idx": { + "name": "page_comments_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_comments_user_id_user_id_fk": { + "name": "page_comments_user_id_user_id_fk", + "tableFrom": "page_comments", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.record_objects": { + "name": "record_objects", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "record_objects_record_id_idx": { + "name": "record_objects_record_id_idx", + "columns": [ + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schema_labels": { + "name": "schema_labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "schema_labels_label_idx": { + "name": "schema_labels_label_idx", + "columns": [ + { + "expression": "label", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schema_labels_schema_id_schemas_id_fk": { + "name": "schema_labels_schema_id_schemas_id_fk", + "tableFrom": "schema_labels", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schema_labels_schema_id_label_unique": { + "name": "schema_labels_schema_id_label_unique", + "nullsNotDistinct": false, + "columns": ["schema_id", "label"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schemas": { + "name": "schemas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "schema_hash": { + "name": "schema_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "schemas_schema_hash_unique": { + "name": "schemas_schema_hash_unique", + "nullsNotDistinct": false, + "columns": ["schema_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_runs": { + "name": "sync_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "collections_synced": { + "name": "collections_synced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_created": { + "name": "collections_created", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "collections_failed": { + "name": "collections_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "versions_pulled": { + "name": "versions_pulled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_downloaded": { + "name": "files_downloaded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "files_skipped": { + "name": "files_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_files": { + "name": "version_files", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "file_hash": { + "name": "file_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_files_file_hash_idx": { + "name": "version_files_file_hash_idx", + "columns": [ + { + "expression": "file_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_files_version_id_versions_id_fk": { + "name": "version_files_version_id_versions_id_fk", + "tableFrom": "version_files", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_files_file_hash_files_hash_fk": { + "name": "version_files_file_hash_files_hash_fk", + "tableFrom": "version_files", + "tableTo": "files", + "columnsFrom": ["file_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_files_version_id_file_hash_pk": { + "name": "version_files_version_id_file_hash_pk", + "columns": ["version_id", "file_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_records": { + "name": "version_records", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "record_hash": { + "name": "record_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_record_hash": { + "name": "public_record_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "record_id": { + "name": "record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_records_record_hash_idx": { + "name": "version_records_record_hash_idx", + "columns": [ + { + "expression": "record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_public_record_hash_idx": { + "name": "version_records_public_record_hash_idx", + "columns": [ + { + "expression": "public_record_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "public_record_hash IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_record_idx": { + "name": "version_records_version_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_records_version_type_record_idx": { + "name": "version_records_version_type_record_idx", + "columns": [ + { + "expression": "version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_records_version_id_versions_id_fk": { + "name": "version_records_version_id_versions_id_fk", + "tableFrom": "version_records", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_records_record_hash_record_objects_hash_fk": { + "name": "version_records_record_hash_record_objects_hash_fk", + "tableFrom": "version_records", + "tableTo": "record_objects", + "columnsFrom": ["record_hash"], + "columnsTo": ["hash"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_records_version_id_record_hash_pk": { + "name": "version_records_version_id_record_hash_pk", + "columns": ["version_id", "record_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.version_schemas": { + "name": "version_schemas", + "schema": "", + "columns": { + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_id": { + "name": "schema_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "version_schemas_schema_id_idx": { + "name": "version_schemas_schema_id_idx", + "columns": [ + { + "expression": "schema_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "version_schemas_version_id_versions_id_fk": { + "name": "version_schemas_version_id_versions_id_fk", + "tableFrom": "version_schemas", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "version_schemas_schema_id_schemas_id_fk": { + "name": "version_schemas_schema_id_schemas_id_fk", + "tableFrom": "version_schemas", + "tableTo": "schemas", + "columnsFrom": ["schema_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "version_schemas_version_id_slug_pk": { + "name": "version_schemas_version_id_slug_pk", + "columns": ["version_id", "slug"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.versions": { + "name": "versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "major": { + "name": "major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minor": { + "name": "minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "patch": { + "name": "patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_hash": { + "name": "public_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_semver": { + "name": "base_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pushed_by": { + "name": "pushed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature": { + "name": "signature", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type_counts": { + "name": "type_counts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "total_bytes": { + "name": "total_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "versions_ordering_idx": { + "name": "versions_ordering_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "versions_collection_id_collections_id_fk": { + "name": "versions_collection_id_collections_id_fk", + "tableFrom": "versions", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "versions_pushed_by_user_id_fk": { + "name": "versions_pushed_by_user_id_fk", + "tableFrom": "versions", + "tableTo": "user", + "columnsFrom": ["pushed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "versions_collection_id_semver_unique": { + "name": "versions_collection_id_semver_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "semver"] + }, + "versions_collection_id_hash_public_hash_unique": { + "name": "versions_collection_id_hash_public_hash_unique", + "nullsNotDistinct": false, + "columns": ["collection_id", "hash", "public_hash"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "webhook_id": { + "name": "webhook_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "semver": { + "name": "semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bump_type": { + "name": "bump_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'version.created'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_webhook_id_idx": { + "name": "webhook_deliveries_webhook_id_idx", + "columns": [ + { + "expression": "webhook_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_collection_id_idx": { + "name": "webhook_deliveries_collection_id_idx", + "columns": [ + { + "expression": "collection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_created_at_idx": { + "name": "webhook_deliveries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_deliveries_sweep_idx": { + "name": "webhook_deliveries_sweep_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_deliveries_webhook_id_collection_webhooks_id_fk": { + "name": "webhook_deliveries_webhook_id_collection_webhooks_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collection_webhooks", + "columnsFrom": ["webhook_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_collection_id_collections_id_fk": { + "name": "webhook_deliveries_collection_id_collections_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "collections", + "columnsFrom": ["collection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deliveries_version_id_versions_id_fk": { + "name": "webhook_deliveries_version_id_versions_id_fk", + "tableFrom": "webhook_deliveries", + "tableTo": "versions", + "columnsFrom": ["version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 417ed07..04ded63 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -78,6 +78,20 @@ "when": 1785533520514, "tag": "0010_unique_nemesis", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1786030057109, + "tag": "0011_wealthy_hardball", + "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1786031983588, + "tag": "0012_sloppy_nightcrawler", + "breakpoints": true } ] } diff --git a/src/db/schema.ts b/src/db/schema.ts index 2c0c4e0..76800d8 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -250,13 +250,23 @@ export const versions = pgTable( }, (t) => [ unique().on(t.collectionId, t.semver), - unique().on(t.collectionId, t.hash), + // A version is identified by BOTH digests. `hash` covers content, schemas, + // files and metadata but not record-level privacy, so keying uniqueness on it + // alone would make redaction-in-place impossible: re-pushing identical + // content with a record newly marked private produces the same `hash` and a + // different `public_hash`, and must be allowed to become a new version. + unique().on(t.collectionId, t.hash, t.publicHash), index('versions_ordering_idx').on(t.collectionId, t.major, t.minor, t.patch), ], ) // --- Records (globally deduplicated, content-addressed) --- - +// +// PURE CONTENT. A record object is byte-identical content shared across every +// collection that pushes it, so it carries no privacy: privacy is contextual +// (a per-version property — see version_records.private), never intrinsic to the +// bytes. A `private` flag once lived here and was the source of cross-collection +// privacy coupling; it has been removed. export const recordObjects = pgTable( 'record_objects', { @@ -264,7 +274,6 @@ export const recordObjects = pgTable( recordId: text('record_id').notNull(), type: text('type').notNull(), data: jsonb('data').notNull(), - private: boolean('private').default(false).notNull(), size: integer('size').notNull(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), }, @@ -285,6 +294,14 @@ export const versionRecords = pgTable( // equals record_hash (i.e. the type has no private fields), which is the // common case — only private-field bindings pay the storage cost. publicRecordHash: text('public_record_hash'), + // Record-level privacy is a per-VERSION property (this collection's version + // declares this record private), NOT a property of the globally-shared, + // content-addressed record object. Set once at commit from the push's own + // intent (negotiate_session_manifest.private). This is the SOLE record-level + // privacy signal — the content object carries none — which is what lets two + // collections hold byte-identical content at different privacy levels and + // makes public_hash a pure function of the authored version. + private: boolean('private').default(false).notNull(), // Denormalized from record_objects. Records are immutable and // content-addressed, so these can never drift from the row they were copied // from. Carrying them here is what lets record listing, `?type=` filtering diff --git a/src/db/seed.ts b/src/db/seed.ts index 2f89002..5d98cb5 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -60,7 +60,6 @@ async function insertRecords( recordId: r.recordId, type: r.type, data: r.data as any, - private: false, size: Buffer.byteLength(canonical, 'utf8'), } }) diff --git a/src/db/seedKfCollections.ts b/src/db/seedKfCollections.ts index f43135c..2bf6d12 100644 --- a/src/db/seedKfCollections.ts +++ b/src/db/seedKfCollections.ts @@ -79,7 +79,6 @@ async function insertRecords( recordId: r.recordId, type: r.type, data: r.data as any, - private: false, size: Buffer.byteLength(canonical, 'utf8'), } }) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 126f628..3ddf3ea 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -56,22 +56,42 @@ export const auth = betterAuth({ bio: { type: 'string', required: false, input: true }, website: { type: 'string', required: false, input: true }, avatarUrl: { type: 'string', required: false, input: true }, - arkNaan: { type: 'string', required: false, input: true }, + // Not client-settable: arkNaan is set via the validated ARK endpoint, + // and isDefault is server-controlled (the signup hook marks the + // personal org) — otherwise a caller could claim another + // institution's NAAN or hijack default-org selection. + arkNaan: { type: 'string', required: false, input: false }, + // kfOrgId stays user-selectable (the new-org UI offers a picker) but + // is validated against the caller's entitled KF orgs in the hook below. kfOrgId: { type: 'string', required: false, input: true }, - isDefault: { type: 'boolean', required: false, input: true, defaultValue: false }, + isDefault: { type: 'boolean', required: false, input: false, defaultValue: false }, }, }, }, organizationHooks: { beforeCreateOrganization: async ({ organization: orgData, user }) => { - if (orgData.kfOrgId) return + // A client-supplied kfOrgId must be one the caller actually belongs to + // — otherwise the org would spoof its way into another institution's KF + // dashboard (kf-summary trusts organization.kfOrgId). Validate it; on + // any mismatch, fall through to the server-resolved default. + if (orgData.kfOrgId) { + try { + const { resolveUserKfOrgs } = await import('./auth-internal.server.js') + const allowed = await resolveUserKfOrgs(user.id, db, schema) + if (allowed.some((o: { id: string }) => o.id === orgData.kfOrgId)) return + } catch (err) { + console.error('[org hook] failed to validate kfOrgId:', err) + } + } try { const { resolveDefaultKfOrgId } = await import('./auth-internal.server.js') const kfOrgId = await resolveDefaultKfOrgId(user.id, db, schema) - if (kfOrgId) return { data: { kfOrgId } } + return { data: { kfOrgId: kfOrgId ?? undefined } } } catch (err) { console.error('[org hook] failed to resolve kfOrgId:', err) } + // No valid kfOrgId — ensure the unvalidated client value is not persisted. + return { data: { kfOrgId: undefined } } }, }, }), @@ -90,9 +110,16 @@ export const auth = betterAuth({ }, permissions: { defaultPermissions: async (_referenceId, ctx) => { - const scope = ctx.body?.metadata?.scope ?? 'read' - if (scope === 'admin') return { collections: ['admin', 'write', 'read'] } - if (scope === 'write') return { collections: ['write', 'read'] } + // `metadata` is client-controlled, so a caller must NOT be able to + // grant themselves 'admin' by passing metadata.scope='admin'. Admin + // keys are minted out-of-band (server-side), never through this + // self-service create endpoint. + // + // 'admin' is CLAMPED DOWN to write rather than ignored: falling + // through to the read default would hand someone who asked for more + // access strictly less than 'write', silently breaking their pushes. + const scope = ctx.body?.metadata?.scope + if (scope === 'write' || scope === 'admin') return { collections: ['write', 'read'] } return { collections: ['read'] } }, }, diff --git a/src/lib/mirror-sync.ts b/src/lib/mirror-sync.ts index 5d614b6..48aa004 100644 --- a/src/lib/mirror-sync.ts +++ b/src/lib/mirror-sync.ts @@ -539,7 +539,6 @@ async function pullVersion( recordId: r.id, type: r.type, data: r.data as any, - private: false, size: Buffer.byteLength(canonical, 'utf8'), } }) diff --git a/src/lib/s3.ts b/src/lib/s3.ts index 7e26e88..9c71af6 100644 --- a/src/lib/s3.ts +++ b/src/lib/s3.ts @@ -8,6 +8,7 @@ import { PutObjectCommand, S3Client, } from '@aws-sdk/client-s3' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' // Works with AWS S3, Cloudflare R2, MinIO, or any S3-compatible service. // For R2: S3_ENDPOINT=https://.r2.cloudflarestorage.com @@ -22,7 +23,16 @@ const s3 = new S3Client({ }, }) +// Two buckets: +// - `bucket` (private): content-addressed record files. Public access is +// DISABLED on this bucket; reads go through short-lived presigned URLs minted +// only after the API's access check passes. +// - `publicBucket`: world-readable assets (avatars, etc.) served directly. const bucket = process.env.S3_BUCKET ?? 'underlay' +const publicBucket = process.env.S3_PUBLIC_BUCKET ?? 'underlaypublic' + +/** Seconds a presigned file URL stays valid. Short — it is minted per request. */ +const PRESIGN_TTL_SECONDS = Number(process.env.S3_PRESIGN_TTL_SECONDS ?? '300') export async function uploadToS3( key: string, @@ -39,6 +49,52 @@ export async function uploadToS3( ) } +/** + * Mint a short-lived presigned GET URL for a private file object. The download + * is forced to `attachment` so a file stored with an active content type + * (e.g. text/html, image/svg+xml) cannot execute as a page in the browser. + */ +export async function getPresignedFileUrl(storageKey: string, filename?: string): Promise { + const disposition = filename + ? `attachment; filename="${filename.replace(/["\\]/g, '')}"` + : 'attachment' + return getSignedUrl( + // The presigner and client-s3 ship their own copies of the S3Client type; + // they are structurally identical but nominally distinct, so cast here. + s3 as unknown as Parameters[0], + new GetObjectCommand({ + Bucket: bucket, + Key: storageKey, + ResponseContentDisposition: disposition, + }), + { expiresIn: PRESIGN_TTL_SECONDS }, + ) +} + +/** Upload a world-readable asset (avatars, etc.) to the public bucket. */ +export async function uploadPublicAsset( + key: string, + body: Buffer | Readable, + contentType?: string, +): Promise { + await s3.send( + new PutObjectCommand({ + Bucket: publicBucket, + Key: key, + Body: body, + ContentType: contentType, + }), + ) +} + +export async function listPublicAssets(prefix: string): Promise { + return listObjects(publicBucket, prefix) +} + +export async function deletePublicAssets(keys: string[]): Promise { + return deleteObjects(publicBucket, keys) +} + export async function downloadFromS3(key: string): Promise { const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })) const stream = res.Body as Readable @@ -70,14 +126,14 @@ export async function getS3ObjectMeta( } } -export async function listS3Objects(prefix: string): Promise { +async function listObjects(targetBucket: string, prefix: string): Promise { const keys: string[] = [] let continuationToken: string | undefined do { const res = await s3.send( new ListObjectsV2Command({ - Bucket: bucket, + Bucket: targetBucket, Prefix: prefix, ContinuationToken: continuationToken, }), @@ -91,12 +147,20 @@ export async function listS3Objects(prefix: string): Promise { return keys } -export async function deleteS3Objects(keys: string[]): Promise { +async function deleteObjects(targetBucket: string, keys: string[]): Promise { if (keys.length === 0) return await s3.send( new DeleteObjectsCommand({ - Bucket: bucket, + Bucket: targetBucket, Delete: { Objects: keys.map((Key) => ({ Key })) }, }), ) } + +export async function listS3Objects(prefix: string): Promise { + return listObjects(bucket, prefix) +} + +export async function deleteS3Objects(keys: string[]): Promise { + return deleteObjects(bucket, keys) +} diff --git a/src/lib/share-token.tsx b/src/lib/share-token.tsx new file mode 100644 index 0000000..26aa187 --- /dev/null +++ b/src/lib/share-token.tsx @@ -0,0 +1,48 @@ +import { Link, useSearchParams } from 'react-router' + +/** + * Read-only share links carry a collection-scoped API key in the page URL + * (?token=ul_...). These helpers keep that token attached as the viewer + * navigates between collection pages, and forward it to API requests. + */ + +/** The share token from the current page URL, or null. */ +export function useShareToken(): string | null { + const [searchParams] = useSearchParams() + return searchParams.get('token') +} + +/** Append a share token to an internal path, preserving existing query params. */ +export function withToken(to: string, token: string | null): string { + if (!token) return to + const sep = to.includes('?') ? '&' : '?' + return `${to}${sep}token=${encodeURIComponent(token)}` +} + +/** + * Drop-in replacement for react-router's Link that carries the current share + * token across page clicks. Use for links between pages of the same collection + * so a shared-link viewer keeps their access as they navigate. + */ +export function TokenLink({ to, ...props }: React.ComponentProps) { + const token = useShareToken() + return +} + +/** Extract the share token from a loader's request URL, or null. */ +export function shareTokenFromRequest(requestUrl: string): string | null { + return new URL(requestUrl).searchParams.get('token') +} + +/** + * Loader helper: builds API URLs that forward the page's share token as a + * ?token= query param (the API's auth middleware accepts it on GETs). + */ +export function apiUrlBuilder(request: Request, base: string): (path: string) => URL { + const token = shareTokenFromRequest(request.url) + return (path: string) => { + const url = new URL(path, base) + if (token) url.searchParams.set('token', token) + return url + } +} diff --git a/src/lib/version-helpers.server.ts b/src/lib/version-helpers.server.ts index feee1d6..cb7387f 100644 --- a/src/lib/version-helpers.server.ts +++ b/src/lib/version-helpers.server.ts @@ -63,11 +63,16 @@ export async function resolveCollection(owner: string, slug: string) { * Returns null when the collection doesn't exist OR is private and the caller * isn't an org member — indistinguishable to the caller (404 either way). * `ownerAccess` is true when the caller is a member of the owning org. + * + * When the request authenticated with a collection-scoped API key (share/agent + * links), pass `apiKeyCollectionIds` — the key's identity only counts for the + * collections it is scoped to; anything else is treated as anonymous. */ export async function resolveAccessibleCollection( owner: string, slug: string, userId: string | undefined, + apiKeyCollectionIds?: string[], ) { const [result] = await db .select({ @@ -81,7 +86,8 @@ export async function resolveAccessibleCollection( .where(and(eq(schema.organization.slug, owner), eq(schema.collections.slug, slug))) .limit(1) if (!result) return null - const ownerAccess = await hasOrgAccess(userId, result.organizationId) + const keyScopeOk = !apiKeyCollectionIds || apiKeyCollectionIds.includes(result.id) + const ownerAccess = keyScopeOk && (await hasOrgAccess(userId, result.organizationId)) if (!result.public && !ownerAccess) return null return { ...result, ownerAccess } } diff --git a/src/routes/[owner]/[collection]/diff.data.ts b/src/routes/[owner]/[collection]/diff.data.ts index 00022b8..c6b384a 100644 --- a/src/routes/[owner]/[collection]/diff.data.ts +++ b/src/routes/[owner]/[collection]/diff.data.ts @@ -1,6 +1,7 @@ import type { LoaderFunctionArgs } from 'react-router' import { fetchBase } from '~/lib/fetch-base' +import { apiUrlBuilder } from '~/lib/share-token' export const handle = { title: (params: Record) => @@ -8,15 +9,13 @@ export const handle = { } export async function loader({ params, request }: LoaderFunctionArgs) { - const base = fetchBase(request.url) + const api = apiUrlBuilder(request, fetchBase(request.url)) const headers = { Cookie: request.headers.get('Cookie') ?? '' } const prefix = `/api/collections/${params.owner}/${params.collection}` const [data, versions] = await Promise.all([ - fetch(new URL(prefix, base), { headers }).then((r) => (r.ok ? r.json() : null)), - fetch(new URL(`${prefix}/versions?limit=100`, base), { headers }).then((r) => - r.ok ? r.json() : [], - ), + fetch(api(prefix), { headers }).then((r) => (r.ok ? r.json() : null)), + fetch(api(`${prefix}/versions?limit=100`), { headers }).then((r) => (r.ok ? r.json() : [])), ]) if (!data) throw new Response('Not Found', { status: 404 }) diff --git a/src/routes/[owner]/[collection]/diff.tsx b/src/routes/[owner]/[collection]/diff.tsx index 730d1d9..127fc43 100644 --- a/src/routes/[owner]/[collection]/diff.tsx +++ b/src/routes/[owner]/[collection]/diff.tsx @@ -3,6 +3,7 @@ import { useLoaderData, useParams, useSearchParams } from 'react-router' import BaseLayout from '~/components/BaseLayout' import { useAppContext } from '~/lib/app-context' +import { useShareToken, withToken } from '~/lib/share-token' import { CollectionNav } from '.' @@ -23,6 +24,7 @@ export default function CollectionDiffPage() { const isOwner = currentUser?.slug === owner || currentUser?.orgs?.some((o: any) => o.slug === owner) + const shareToken = useShareToken() const [diff, setDiff] = useState(null) const [diffError, setDiffError] = useState(null) @@ -39,9 +41,13 @@ export default function CollectionDiffPage() { setDiffError(null) const fromParam = fromVer ? `?from=${fromVer}` : '' - fetch(`/api/collections/${owner}/${collection}/versions/${toVer}/diff${fromParam}`, { - credentials: 'include', - }) + fetch( + withToken( + `/api/collections/${owner}/${collection}/versions/${toVer}/diff${fromParam}`, + shareToken, + ), + { credentials: 'include' }, + ) .then(async (r) => { if (r.ok) { setDiff(await r.json()) @@ -51,7 +57,7 @@ export default function CollectionDiffPage() { } }) .finally(() => setDiffLoading(false)) - }, [fromVer, toVer, owner, collection]) + }, [fromVer, toVer, owner, collection, shareToken]) function handleCompare(e: React.FormEvent) { e.preventDefault() @@ -63,6 +69,7 @@ export default function CollectionDiffPage() { setToVer(t) const params: Record = { to: t } if (f) params.from = f + if (shareToken) params.token = shareToken setSearchParams(params) } diff --git a/src/routes/[owner]/[collection]/index.data.ts b/src/routes/[owner]/[collection]/index.data.ts index c770b31..63f038a 100644 --- a/src/routes/[owner]/[collection]/index.data.ts +++ b/src/routes/[owner]/[collection]/index.data.ts @@ -1,16 +1,17 @@ import type { LoaderFunctionArgs } from 'react-router' import { fetchBase } from '~/lib/fetch-base' +import { apiUrlBuilder } from '~/lib/share-token' export const handle = { title: (params: Record) => `${params.owner}/${params.collection} · Underlay`, } export async function loader({ params, request }: LoaderFunctionArgs) { - const res = await fetch( - new URL(`/api/collections/${params.owner}/${params.collection}`, fetchBase(request.url)), - { headers: { Cookie: request.headers.get('Cookie') ?? '' } }, - ) + const api = apiUrlBuilder(request, fetchBase(request.url)) + const res = await fetch(api(`/api/collections/${params.owner}/${params.collection}`), { + headers: { Cookie: request.headers.get('Cookie') ?? '' }, + }) if (!res.ok) throw new Response('Not Found', { status: 404 }) return res.json() } diff --git a/src/routes/[owner]/[collection]/index.tsx b/src/routes/[owner]/[collection]/index.tsx index 0d19f03..dcaf53c 100644 --- a/src/routes/[owner]/[collection]/index.tsx +++ b/src/routes/[owner]/[collection]/index.tsx @@ -6,6 +6,7 @@ import { Link, useLoaderData, useParams } from 'react-router' import BaseLayout from '~/components/BaseLayout' import { useAppContext } from '~/lib/app-context' import { authClient } from '~/lib/auth-client' +import { TokenLink, useShareToken, withToken } from '~/lib/share-token' function CollectionNav({ owner, @@ -25,6 +26,7 @@ function CollectionNav({ const linkClass = 'px-3 py-2 text-sm font-medium border-b-2 -mb-px transition-colors' const activeClass = `${linkClass} border-ink text-ink` const inactiveClass = `${linkClass} border-transparent text-ink-muted hover:text-ink hover:border-rule` + const shareToken = useShareToken() return ( <> @@ -34,36 +36,44 @@ function CollectionNav({ {owner} / - + {collection} - + {isPublic !== undefined && ( {isPublic ? 'public' : 'private'} )} + {shareToken && !isOwner && ( + + shared link + + )}
- Overview - - + Versions - + {versionLabel && {versionLabel}} - Schemas - + {isOwner && (
- {data.latestVersion.semver} - + · {data.latestVersion.recordCount.toLocaleString()} records @@ -223,7 +233,7 @@ export default function CollectionPage() { timeZone: 'UTC', })} - {totalVersions} - +
)} @@ -246,7 +256,7 @@ export default function CollectionPage() { {allTypes.length > 0 && (
{allTypes.map((t: any, i: number) => ( - {t.count.toLocaleString()} records - + ))}
)} @@ -444,18 +454,26 @@ export default function CollectionPage() { {data.latestVersion && ( - Download .tar.gz - + )} - {/* Agent Share */} - {isOwner && } + {/* Share */} + {isOwner && ( + + )} {/* ARK */} {collectionArkPath && ( @@ -478,20 +496,52 @@ export default function CollectionPage() { ) } -function AgentShareSection({ +const VIEW_LINK_EXPIRES_SECONDS = 30 * 24 * 3600 + +function SharePanel({ + owner, collection, collectionId, + isPublic, }: { + owner: string collection: string collectionId: string + isPublic: boolean }) { - const [showModal, setShowModal] = useState(false) + const [modal, setModal] = useState<'view' | 'agent' | null>(null) + const [viewUrl, setViewUrl] = useState(null) const [agentUrl, setAgentUrl] = useState(null) - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState<'view' | 'agent' | null>(null) const [copied, setCopied] = useState<'link' | 'blurb' | null>(null) - const generate = useCallback(async () => { - setLoading(true) + const generateView = useCallback(async () => { + setLoading('view') + setCopied(null) + try { + const { data: keyData } = await authClient.apiKey.create({ + name: `share-${collection}`, + metadata: { + scope: 'read', + collectionIds: [collectionId], + linkShare: true, + }, + expiresIn: VIEW_LINK_EXPIRES_SECONDS, + prefix: 'ul', + } as any) + if (keyData) { + setViewUrl( + withToken(`${window.location.origin}/${owner}/${collection}`, (keyData as any).key), + ) + setModal('view') + } + } finally { + setLoading(null) + } + }, [owner, collection, collectionId]) + + const generateAgent = useCallback(async () => { + setLoading('agent') setCopied(null) try { const { data: keyData } = await authClient.apiKey.create({ @@ -505,60 +555,136 @@ function AgentShareSection({ prefix: 'ul', } as any) if (keyData) { - const url = `${window.location.origin}/agent/${(keyData as any).key}` - setAgentUrl(url) - setShowModal(true) + setAgentUrl(`${window.location.origin}/agent/${(keyData as any).key}`) + setModal('agent') } } finally { - setLoading(false) + setLoading(null) } }, [collection, collectionId]) - const copyLink = useCallback(() => { - if (!agentUrl) return - navigator.clipboard.writeText(agentUrl) - setCopied('link') + const copy = useCallback((text: string, which: 'link' | 'blurb') => { + navigator.clipboard.writeText(text) + setCopied(which) setTimeout(() => setCopied(null), 2000) - }, [agentUrl]) + }, []) - const copyBlurb = useCallback(() => { - if (!agentUrl) return - const blurb = `Will you create an update that captures this conversation. Here is a link with reference how to do that: ${agentUrl}` - navigator.clipboard.writeText(blurb) - setCopied('blurb') - setTimeout(() => setCopied(null), 2000) - }, [agentUrl]) + const agentBlurb = agentUrl + ? `Will you create an update that captures this conversation. Here is a link with reference how to do that: ${agentUrl}` + : '' return ( <>
-

- Update via Agent -

-

- Generate a temporary link that lets an AI agent push updates to this collection. -

- +

Share

+ +
+

+ {isPublic + ? 'This collection is public — anyone with its URL can view it.' + : 'Create a read-only link that lets anyone view this collection without signing in or becoming a member.'} +

+ {isPublic ? ( + + ) : ( + + )} +
+ +
+

+ Or generate a temporary link that lets an AI agent push updates to this collection. +

+ +
- {showModal && agentUrl && ( + {modal === 'view' && viewUrl && ( +
{ + if (e.target === e.currentTarget) setModal(null) + }} + > +
+
+

View-only Link

+ +
+ +

+ Anyone with this link can browse this collection — overview, versions, records, + schemas, and exports — without signing in. They cannot make changes. The link stays + attached as they click between pages. +

+ +
+ +
+ {viewUrl} +
+ +
+ +
+

+ Expires in 30 days. Revoke it anytime from Settings → API Keys. +

+ +
+
+
+ )} + + {modal === 'agent' && agentUrl && (
{ - if (e.target === e.currentTarget) setShowModal(false) + if (e.target === e.currentTarget) setModal(null) }} >

Agent Update Link