diff --git a/.gitignore b/.gitignore index 48735d7ba..717ce9e8c 100755 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ yarn-error.log* .env.* !.env.enc !.env.dev.enc +!.env.local.enc !infra/.env.test @@ -71,3 +72,5 @@ tsconfig.tsbuildinfo infra/pgdata/ tmp/ + +planning/ \ No newline at end of file diff --git a/client/containers/CommunityCreate/CommunityCreate.tsx b/client/containers/CommunityCreate/CommunityCreate.tsx index 79609a920..b5cba2f5f 100644 --- a/client/containers/CommunityCreate/CommunityCreate.tsx +++ b/client/containers/CommunityCreate/CommunityCreate.tsx @@ -82,10 +82,19 @@ const CommunityCreatedView = ({ subdomain, hubName }: { subdomain: string; hubNa ); }; +type KFOrg = { + id: string; + name: string; + slug: string; + type: 'personal' | 'shared'; + role: string; +}; + type Props = { hubData?: Hub | null; templates?: CommunityTemplate[]; hubCommunities?: { id: string; title: string; subdomain: string; avatar?: string | null }[]; + kfOrgs?: KFOrg[]; }; const HubBrandedHeader = ({ hub }: { hub: Hub }) => { @@ -109,7 +118,7 @@ const HubBrandedHeader = ({ hub }: { hub: Hub }) => { }; const CommunityCreate = (props: Props) => { - const { hubData, templates = [], hubCommunities = [] } = props; + const { hubData, templates = [], hubCommunities = [], kfOrgs = [] } = props; const { loginData, locationData } = usePageContext(); const altchaRef = useRef(null); const hubSlug = hubData?.slug || locationData?.query?.hub || null; @@ -126,6 +135,12 @@ const CommunityCreate = (props: Props) => { const [selectedTemplateId, setSelectedTemplateId] = useState(null); const [cloneCommunityId, setCloneCommunityId] = useState(null); + // KF org picker: default to personal org, or first available + const personalOrg = kfOrgs.find((o) => o.type === 'personal'); + const [selectedKfOrgId, setSelectedKfOrgId] = useState( + personalOrg?.id ?? kfOrgs[0]?.id ?? null, + ); + const hasHub = !!hubData; const hubAccentDark = hubData?.accentColorDark || '#2D2E2F'; @@ -163,6 +178,7 @@ const CommunityCreate = (props: Props) => { ...(selectedTemplateId === CLONE_MARKER && cloneCommunityId ? { cloneCommunityId } : {}), + ...(selectedKfOrgId ? { kfOrgId: selectedKfOrgId } : {}), }); setCreateIsLoading(false); setIsCreated(true); @@ -308,6 +324,27 @@ const CommunityCreate = (props: Props) => { onChange={onDescriptionChange} helperText={`${description.length}/280 characters`} /> + {kfOrgs.length > 1 && ( + +
+ +
+
+ )} {selectedTemplateId ? ( { + + ); diff --git a/client/containers/DashboardSettings/CommunitySettings/TransferOwnership.tsx b/client/containers/DashboardSettings/CommunitySettings/TransferOwnership.tsx new file mode 100644 index 000000000..cfb270367 --- /dev/null +++ b/client/containers/DashboardSettings/CommunitySettings/TransferOwnership.tsx @@ -0,0 +1,154 @@ +import React, { useCallback, useEffect, useState } from 'react'; + +import { Button, Callout, Classes } from '@blueprintjs/core'; + +import { apiFetch } from 'client/utils/apiFetch'; +import { SettingsSection } from 'components'; + +type KFOrg = { + id: string; + name: string; + slug: string; + type: 'personal' | 'shared'; + role: string; +}; + +type Props = { + communityData: { + id: string; + title: string; + kfOrgId: string | null; + }; +}; + +const TransferOwnership = (props: Props) => { + const { communityData } = props; + const [orgs, setOrgs] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedOrgId, setSelectedOrgId] = useState(null); + const [isTransferring, setIsTransferring] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const loadOrgs = useCallback(async () => { + try { + const data = await apiFetch.get('/api/kf/my-orgs'); + const fetchedOrgs: KFOrg[] = data.orgs ?? []; + setOrgs(fetchedOrgs); + // Default to current org if set, otherwise first org + if (communityData.kfOrgId && fetchedOrgs.some((o) => o.id === communityData.kfOrgId)) { + setSelectedOrgId(communityData.kfOrgId); + } else if (fetchedOrgs.length > 0) { + setSelectedOrgId(fetchedOrgs[0].id); + } + } catch { + setError('Failed to load organizations'); + } finally { + setLoading(false); + } + }, [communityData.kfOrgId]); + + useEffect(() => { + loadOrgs(); + }, [loadOrgs]); + + const selectedOrg = orgs.find((o) => o.id === selectedOrgId); + const isCurrentOrg = selectedOrgId === communityData.kfOrgId; + + const handleTransfer = async () => { + if (!selectedOrgId || isCurrentOrg) return; + setIsTransferring(true); + setError(null); + setSuccess(null); + try { + await apiFetch.post('/api/kf/transfer-community', { + communityId: communityData.id, + kfOrgId: selectedOrgId, + }); + setSuccess( + `Community transferred to ${selectedOrg?.name ?? 'the selected organization'}.`, + ); + // Update the local state so the button disables + communityData.kfOrgId = selectedOrgId; + } catch (err: any) { + setError(err?.error || err?.message || 'Failed to transfer community'); + } finally { + setIsTransferring(false); + } + }; + + if (loading) { + return ( + +

Loading organizations...

+
+ ); + } + + // Need at least 2 orgs to have somewhere to transfer to + if (orgs.length < 2) { + return null; + } + + const currentOrg = orgs.find((o) => o.id === communityData.kfOrgId); + + return ( + +

+ Transfer this community to a different KF Account. The target account will become + the billing owner of this community. +

+ + {currentOrg && ( +

+ Currently owned by: {currentOrg.name} + {currentOrg.type === 'personal' ? ' (Personal)' : ''} +

+ )} + + {error && ( + + {error} + + )} + + {success && ( + + {success} + + )} + +
+
+
+ +
+
+
+
+ ); +}; + +export default TransferOwnership; diff --git a/infra/.env.dev.enc b/infra/.env.dev.enc index 6db5bf1c2..11bb2fb24 100644 --- a/infra/.env.dev.enc +++ b/infra/.env.dev.enc @@ -1,53 +1,60 @@ -AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:YwdwuazMHhri6N2Lxs4mNbm5hwRSJm3yop5LxmzaB/6f36IbuVcPL9bZEpUhqSbbza0Uw1FYAzimLF5PMfor2Q==,iv:CImEY++dwcJjJ6shRdnhEgwFUwvBRvWL5XejfKZJVB4=,tag:wlbsUyJlvVCPpkDWwuaB8g==,type:str] -ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:m3h3mTBUsQQVKP9vqfHzgB3yLPNSuAMX8von2pALFWLl11sw5oeuyetx0oznddZCQjE596h7WAo0KN6sQJslmw==,iv:Mcojr/cgUKz4DQuNESe/1tDnlVeUaz42EhxxCOKOdiE=,tag:hsPq+qm9WqczCBR5zVFXlA==,type:str] -AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:V2EPX34kkLSzVkapq/AXTANt0RA=,iv:Vd2Hi6LxL7FXu4NdkQPvQUQHhJJecqay4IN955eTxEM=,tag:fpW+1Z1AkfqbvhasRmKD2Q==,type:str] -AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:9vHS7UaaLe4qzIMv4+/TVj/iNiU=,iv:RhGRvIoWNWsh00Eo95b14gKBUCWhL/qK+kaWWXYKIG0=,tag:PSse94gE1uwo/s+v8WojWw==,type:str] -AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:15It93JTjRwIeF+K3iC9C3fbb7oec44it5ZaQ6SZNS3BJoQG705sSQ==,iv:ur3MbUSWqnh7z9O5Ow1UbcVHLM3sQgRVucz7KDjvMz4=,tag:yWPCfshCsQRtB+7cOO8INg==,type:str] -AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:nLVLV6QiLG6DLhG/+Gqo5DmprPCNlubKUXw+1v9DXHD3AI/qnDJ4pA==,iv:meLdta+HFrOZuC7T1FHC7yAvK1ZPJqqDQ3gRigEPnvA=,tag:Gvl7KH+IwN5sDoLFl0MuKw==,type:str] -BACKUPS_SECRET=ENC[AES256_GCM,data:5ey56vyaaaFDg0yAau+rdD6DIp2ybUbsd19KkP04icjMWtYq3odwPgBYYt4=,iv:rYABukoHrmO2Q7XIMryVADsDf1li/R0R98Ct9rDtpfU=,tag:Va1tZW37BUqEQXl5Odx6cQ==,type:str] -CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:JKYGVdcx+fzXXIP/ir32J6sDbPXsUQJCs0cH8QWFOAMkHd0pORIWHtRoAbNCc+asWZKh6ts=,iv:8DIZcel5/3crIrEdmh4Acvme+K6xdXDT7J/VPqJ+IpY=,tag:3ptWLOiCtOvckryH6RaWDw==,type:str] -CLOUDFLARE_CUSTOM_HOSTNAME_API_TOKEN=ENC[AES256_GCM,data:ZqLdXUQ0kgruGjEJuF2tZ8UoqzGW5yVATq94JZy6zokILEQ9claOL06u/Kug2uXmfknUjys=,iv:rO0avhy+tZfwdOazdkNFzU0Dlno6c5M+Vhx8EUlA4rI=,tag:U3kr2EzLaJeaT/cPS72JwA==,type:str] -CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:qyoMHUDQLyTQxilvK//0sjA/UMlqC1AHE9laplkY7dc=,iv:xvM3bWtP73b+qre2Yn2ECQjle4Wz/wS/CZ3NGmehxEk=,tag:Quf9aeUIIQi7OzQwBChLEA==,type:str] -CONTENT_SEARCH_TERMS=ENC[AES256_GCM,data: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,iv:ToxQVOKx3ZP+oYt0596Xa3JacoF/9qB3q4TjiUycp/g=,tag:2uQZ/E/ZytYDWidA7ATlXg==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:zaYEq9/FoXQqIjXAH1Xm2KZhTs7hxCrxaT6geoxQEJoEhgX9pVxTzlC4zbI=,iv:Zfo6DSvryrww6u+QPhAiisQsMeFKNwlDPeQE+EyHSEo=,tag:Gm1j50yWe9nD1enIV2NAxw==,type:str] -DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:O61IgJI1xGiPCxmRPDkMsMlgB9ZAxZC2Yxohb7W72DCZ3A==,iv:FaFM0GoDNfz+6lM0pvyUAM8CqrDaapcm6K2kKtEX3TU=,tag:bc59T0LvR+cxoX/ayDsF1A==,type:str] -DOI_LOGIN_ID=ENC[AES256_GCM,data:/N76Zjh9,iv:wSJ/F6ycYlxoQ0dDJK4zEECrUw7F1uRzQIpaut7XLy0=,tag:O/CHgRWTPWs/MEZf1ADiNg==,type:str] -DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:KPJsTBKbjm7Lh65xeQN4MeynW2k=,iv:2aHPo98X4HSuFWvcbqwFxFLHRekvsxxLCQ081ZNtvg0=,tag:W0f5abzYvJkya/GG96zT4A==,type:str] -DOI_SUBMISSION_URL=ENC[AES256_GCM,data:c/BcNuFqQPbYVi++NUbkbdNV2JYLaFkcmBtzn8EVN+A/HWjc8BlaOOs=,iv:phyuU35fFCr1VLNgKS+0NxoTg7BQoYhOo2MEI9rb5T4=,tag:uEe04YOPrHWTcz6jniLztA==,type:str] -FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:ttl2E8BgOJ9ij+/p2b3XjmHzDGtuSyo18R9QxfQtCug=,iv:baGXWnNtKe5YuKwTYvEx4+tJFAoyohpcEjr+yN714g0=,tag:ZHJj9haQ/gU6lw+GK31tWQ==,type:str] -FASTLY_SERVICE_ID=ENC[AES256_GCM,data:1cqxB+w/8LxrCzkvUVJydCNVv4xSSA==,iv:4fjl+V4U5fAT1h+90lOx3hw1NIuZyO7m/i+2wbZCu+U=,tag:Jkfs38M72erSxnA31qPEDw==,type:str] -FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data:8qYpAvSy0P/11YMqPHqhu8iOIKQfEqUlgaviycItwusyjXRZtSxNS3T5JMPNhUIUc+zlUIjBVdjHj0V0lVfpvlhbh8+243DO0p9rkqePAtjTIFvzWowm+iC8RqYE4mV/U/Hj/QeGLzusZ4ounBqsEYFMbpedbh5/5ZZu+aRGcl72WPsyPC08BQVC32fNMnsvrA+2wjhqI9fJqrbmELkEIC6FHMVeUGO7QL25FBbWJQbb7j7hK6TFF/Oq36w+n7FEV1J3OSl8yCRzpTViWLRXR0xHuS8BZDfnWrc2+avbfgwxrZm7kHvqah7av/BBDvpIAUeGhkmo4w8Jt4v/1EpDy/YI3zslaxVIL7nCePMr//dPkfGDGm8BivGkkN5Wya/7NUx8Ia8Tytw5EmcWSEPTvljXYFFSNPX/A1X2m2b+rtgSv/NxRp03k3RJeRqSG0t8SX0TpC7jWKgf47PhbwCaQKB8aQG0hrhwosWYxuROZlyGXGHdAhTG16gSBb9uVhCbnjimoLUXDvGvOB2HweBiyhvjETlK0Nw8qQQ94ifpJgRVsN1oU8mvoMHwDsQyUpCBYWklPEMjqEYMQ5K2N5NNbpW9DQ/5YPJg/K0gDFVBaE/Y4ydB4NvQMRMQmisRr+5NdWuDYiz2MGPFGXvHah3pF9zZkcwCftCDQCLuyKYgjbDgye20DqsRCsW4n47/LjjVYXE5azSoj7/19lp5gC9rEN1feSYQmTei+FA8HEW7Yp0Vx1yP71rV+KkRVBHOyQGcyGPV4NFIxOz/97daf2Di7Mnna0L/yEyyl1OG/GwSILiT/pc+A/u6hB18hXQziQYN/TFeeJE2ZNDXbVOjEs85UWh0eZytOrSsZPHPLivjz6OFBMmIS7DxQj+oPRgQmohNNqrvakj34DNPAYx5vmtUtGW/U6Yt7eO/QplCo5H5wG8AdSzM7XbFW4HlVH8iOjz9RI79F8Sn3Ltulxlyy4SFGiIYwWfA5c9reOIY+5AJrzA/Ep4DXFQfc3aLN0Jg/7KH/9v3ttgkx5NZS4A8xI8PGC+Ye1vXw942sueV0LQ4XqRcRlugpfllQdQ3/k4jsPZq7U8rc4m0Q+btdIHASFwFzx5twxbdDY2QjFHWihHKmOijEd31maPsTPwlhJo+wNMHkJpJLJH/b91962ZbyuAG/SxLfclM2OvSpw0c02YgMCMt/3T9rUpEDlqG+CuSW1wEGvQZa9/WgQ4btvyITZhBD4Wpnj1x53ZOF6MW0M4CIkk66H5Ha7SONKE5YcA27o/ZyT02jD9VgTVDO+rF0HgdKDageNJD9FlCohEERGJKjOvb3CkJ0FeC6ZyXhmLWBxGo7VrNSbGhDUHgwAGBtrxTo+AhkjEhgyvCQdp/7gnUw4a2+HoZqPr/pTHi47YqJyxZQplbt/Fk5WVdyL7QLblnWSRQFvWT1ixozUPNmolnywCV+TZhL4Mx18X1KhUQxk6hxK7uGjsiQj+lxksW5hwbqPdYWDLxl4F8suF7MRQpeXbkM+jlpQ06Vw4TZOgXp1c9vq6DbX0DjnkeiZ2XSzJAWmIY6lx0uIGCY3fY1TSvzoZq7mgeOsz41YT3zQcVGiA8mvP1cST2mr6AKOgb3hKNKFE8Ch+MTRZmCIUJ/0oduMm7aSIJPuN5W2lTxWFs/FoSdJkAiPBW2BBruHCaZwRbOwGDfijIa/7PRpJjGpCuYVGrlI8UWWqmE7r07hFygtfROHPRTpSnMD/ej8FfBqyZOiliDjDZVG/YhsChLPDhn9qkx2cyLXEp2F/Tjtx2bA6u5HSGRFFnD0KfkGX1Bt4n7l892aP0Rc2OzrcmAZBhrnJ+MlMpd5Z+2jW2SF3+a2g3KhUkXGDEM8JnU9i6wV4Ob0dtXAQW4FY3XdNQTJLg3C0GDtWBLj6l8ltgLzYntjMNoPcs7IXWGNxWg2aw7iO2YioICBYrPzqJ1+3aSqA6HmhdG/Co2CZKbo8P8IXBq6TyKFGbEHnZt9tHAPwW2uVYRXdnmKUwmA79rQjzxBeNv3irsEbWhnRdf4Gx+rNLHL0VDg3Y2I9GFHL1j6Ha+nXGlIqDKySvMpBZwdqqNu86DOTd483k+KbVJJ+3GrpqbdeAYUzpwKk7rqXa3iEf6TFCkhMj2VRZ1nmI7Ysm1l4cqmb7GxGu8xC2JR4tJDrFl1mSz9/KmevoPhNpjS2XVqx+AMJR5DAHeOHyrDzWfVxIxz/zwtGoWDEnWMIcKoIN6BwTpXa5JgsZuV0FwDPmAF8Bt4cKE9rv7M4M8Fx21c0UVmUrFCfqOBfjfiCVHSJ42g7xanHWoewj870XJaO/ZG+xAM80KDWF3CcGcvtAxTgf0eIylzhM+nEs2YeBV84KqioNmp6Rv8DHWELBOR89GTT74eMAr4mgtXPe5c/Y/HKBfd5kSsDbRdZqE+dGixaqTPJWP5xzxtg6dj8E8vX2QfAji6786FADcJGRj7X2O9POjJxH9S+YJe4cY8fIp2UmoTiO4Cl8GYHzirzXY+mIDsJUV9a7b9706tHP8U7Ke6IJBtjq1Y8ihx/4JMsMw0ONbL+otlHU5g/74qecMxLgRwRItJfVKa+qS6a5c6qh8zzzH14vexmVNJpNZ23FZNa3ZJvyMVrN3M3PvSTs7tLmW28lvyh9wH9JZ0Cg94OPEsCfKu1RFs6mtGV3d6wPGc1WqgJrAZWd6aZu0Dgbh6UKQg46WE/y6pw9iuIagts1bY5huoxt8NRUUmFBhWyVuT/42t63sz5VBH/ELRWw2+Col5PmRNlbrhRsOVx9W5IetoHlXvhX5MHNAFsm2bygOvdLv4HeOqiOZXtJwvrxjtx7LsDHmxy2PVWd/8g2ksmb7ow7g3XiDo/zlYqiqvzKX63QVeW9Ly9Si9EHaJ69Nfo19lcPChRrS37U1/6Qwek1m9UHiiwhoz6fBx/FzqyuWoyiqF+CdFRUfMUBOKpYphmzWBs/m3ZZUQZfaDgNSwBN5CXt8hqX1HijGrbgQQBicYpA8z3LnMOxNGFj//bmixKc8oafEqnX39uupM+Wep4qgeHOgkm/Nmwjpal+prYwhgJ55M0jnjtFuH3Coo8mT3Ei5jGbmzI1onGca4HhCzaRgFL7IVNyJlpkXZs4Id9OJvutoQ6P0jRDO+Yfl0uaQrHMCzzaxuzLvG7t3YkfkUKFwQerdvv4fYMLP04yp/vrcOe4ETtQe09rKpPJQyhUUGEZ19r++Bnull1c4KfGftQVDlXo0hGiz6VMYTx9KTRi9Mm0OtlZgTr1lkTzg5nVIPEUb6yito0/VQTPA9kDMOgTU5py3upjFBNLnVOJy7eevF8YTODLvALUg1iJSzznp/JDBkyOORyG23fCu4b9kFoYCs4/6ikOlhtNr+rukyQxBwy3zlEKa8NHYDpU21e/kt0K8nYaNueCaIAW9q7t2qOriGKdH4sEibxVQZ54yA4NO+uVV3WFunSlDkpPq5EDIdTTr7vxH23jxx5owLFpuarRuuTLtHIEiCIUvMVUv9mO3bw/LhWsrKhYKBx8Gq85abTBlTTgJ0ubqLAHTK4XAwJa+twN5vjfrmmbmhLPsOdmzzRTwILPKVOeCuENKsc2TLSGHGt4br9RJFYUr0r71kbxLZ+l43WM+z88cAMAENS9CCwEO4b5oJdTgm9tMEzkUKap3Rk6bZZWkr9FYn0bXd/5a6EWIc25vinrYPM25dmjm9LVERHjb2iFHnw6ap5jNksHBa0bCedOcde8eh5aa5yDLV5fzXDHrQBXFDpmlmjAr653LaRIR7Uhl1rUmM0SFUaqVKF55sTv5OUd5XuAONirQzU6JXeHepRqV39VmIzETRDx7LHBR/yhQwzZukKAkwFu7YYz8IWV4HZBD6N5Jbtd1LkNJvgB48zu6qrS1zIH9Kh7N/E8UQn3QyDw6t0+Z4idBNJ31Cbp3JzLI+7Q+iDCo/n6uJG/B5FtXZbclXk7rn4nzwT3C9dxkmPBr6PZgMlzVMAyem/D3hL/ptRrFYMX0dDsiRJBulS3VM80O/GidXvuA/xJ91zedbuk3+mtT7FKZ5IAXJfbdkLUMuzH1X/KTIQi3hskIDH5F3wtnmg4Tmn/l4IpSDhPRS83SpV7jHQwQOd1qUxU+eHwG5tnf6UYGaFjxzk=,iv:308cafiJL9MAG0WrbbF4pWkw/8Pj/1FBMIRvCoFFjDY=,tag:OqsPaBf57YWIxpWH9/lraA==,type:str] -IS_DUQDUQ=ENC[AES256_GCM,data:Ik/xiQ==,iv:lXXGaZyiKVQuiL2NcPDzr/necCT3JAAtVNANJ1Yhpi4=,tag:akOgfXrGkwfp/8BVaCZ53g==,type:str] -JWT_SIGNING_SECRET=ENC[AES256_GCM,data:mzWTmqVKGdMCnV6AMXaTXZOU9ZySaziFpLtRhcwgKGmmIUjb9r9uL+VaptpkMj7F53O61aSiJahGAQ4fmVstzVPr8LV8dm9d1rzkBBtKOy2AV3uNtB85yzuoAwgs5cFzdTIAqrUlYq7kEFiqdZA9Q3OKwdlhihLyTVy/Jpp6nDyxfQzhBGtV0babZz28b/pX0uwWMTkxentuRWPXaogw/fvv+T+U74TrxliB1yNSZgH4AUDs1yrt2St8Kr3N/hobnwl87a6YpsH5QlFZxjw6L8aWuUXXHX2YhGVuuyyrokBaHb/yUs6bnzDsu43LBit/J9QTl1NwhgTK7+jO5azaKZBVDYQmi7OI7BiGdtFSlAr/mLrz+86nVa3mRQJxdTdoEwS7U/noKkkbXYvSKn3jXol6ZauivSYDC5/FVVcluikaHPYhZzD37gSu1TmDEyaTjKDrxKhf6wMUo41Q7BBWjYUF4dIYZkMp16Yoj8vbAkvKDmyfel7qh95ZNaztsQ+isgeduygnWl77cjQQXfQXjNG3JZD+pXQtBgRDaZRgQy2sXshx624ZLxrCxc93SA+ysEgdDxabT8WcStPC1Mf0NyOGckpJ5WshNbv9oLK+tAZYArzhIQHlXPsPEdIXSjEsuCLnRgQXCmRnIwH4dQg/+OycGdCutNBoE2xzCzLwFtpmQCfC6svIVkaFhDxBIQrYvU9JB8xOPKwGt+VmsVSPC5BAXXspk9YgAd1OBK2jwK5rC4WZyN42OG6yecVflJrXBw4vUN/qHtmVGsb6cQ1kqqrwJMPVK7XLrheFqxWbh5Qo5Sd0iiDfeU0QMLbqjZ7PM6QBctZYO7FxOoO3oKakYLJHsuIsydVjmuUAQU2XTkWDA3goD7e4IQcTk/dt96pchQ89slvr7X6GtKZ8xXv/W8fVS556sWgtPGj3CsBgnbKHK54eM562VjX7momuTeyyPvMqnh/2cDH35D2sqmlSAVsF5ZMaqCCvSk3sxgtG47Nrgwr0JfjMKj31qP/tlKrRiOPRCyU4vQj+PNpL+YN6aN7IaTJSJEuZHzMxU3oYFZJi0oaIRKZWGz7mz0AVvjBNBsCWUWr8lNeWBiapqg3dfeyEhr7WC7A1wJM2+uxtTWJtn1QouOh8QwfhoheJ5hFdJJsM3fW/i6hzm42FPeB7YqkuxjSx/kIFzlfg0P/V6Wrq0A/OO4oreXbcXAHes8U9RaZhal/0OBXDvlF2zpnvnAX8LaDU59vPtJ4pD+F9b6g9fwdnv47/FeBJEnyguSYPSKLyB7XbmICPn6r6ACdstj6QxWc6TdTQfxNlMHy/hZ5lUrsYflt7cm4E6FESD0VHHAGzl1O6spg7cYFnEiN/aA==,iv:CgCL+WKalNud/pqASKfp7nmx+0TkUi68OAHtILql8mE=,tag:1pS2XW4Lp7KW7uzgQPh3+w==,type:str] -MAILCHIMP_API_KEY=ENC[AES256_GCM,data:xGUlZ+DQO5MYWoNzIKzU+60AOfikW1xZ0yPYNRP/jxElzxWf,iv:QcGbONp8pHdWTwCZDcA2K/Kcv1d8q8JdxoEB9oGWY50=,tag:BEMdL4hqukE4FEH8fU4VkA==,type:str] -MAILGUN_API_KEY=ENC[AES256_GCM,data:HWakd4g1OjIDcxbRd0KqAD6eBXPIWK8/T51m2ldipfpprNjo,iv:TTGZYCS/l9xs1n4+MED7AGc9w0oiXdn9sq9Jn8j2usY=,tag:+hbpxadiNfoqjR7MXyn+6A==,type:str] -NODE_ENV=ENC[AES256_GCM,data:doZX128C2L0Okw==,iv:xWVM/BmZjaYZAW8p9ZnFBn+Zu8vkqZgY7Dowz4FOVY8=,tag:VtA8xKVXjV/ub/9A2uP4AQ==,type:str] -S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:TTi/idrf7erJ2MP0Ly5WKiM5QI8=,iv:TZartUZmgMyiiq3pgsdzjAlLawNAUlJmdYbrDGCLGOI=,tag:7KfnXeYBbQOsWonYH2633Q==,type:str] -S3_BACKUP_BUCKET=ENC[AES256_GCM,data:BGLP2jKBGO+oYas=,iv:2bQ0SVZbBTQfGMF4Bp5LEcOCtB+c9Ea7pX/RbhcZeq8=,tag:giBYs8Ou2iYKciiv6HQU4w==,type:str] -S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:iRWyMNJIXCLnNF+PkbN08jGaUrcgvdkF01pwiaQP8cf41J8=,iv:9wSXMPJWDfhB1t+7yKYmC9qS7RZ/BnPvYqoA5sRl0nw=,tag:SYXmtg2zwVzd1n0FP1Hw6g==,type:str] -S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:evC1NR7vP8pplfT6aoTrhm+z6NzIW6gJFk+3MXTpvvaBIeV2ZRSh0A==,iv:V0TMCtGnkYLr5k19a3QPyUOAyAvwbe1Wunful0RMd80=,tag:S7pMGtvS6x4dyRMF3spKgQ==,type:str] -SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:sh7B89Ys9jaTqSERyLWvgdutKyFQiXGCmC++cCbPonBTF1Zel4vx/7D/3yyVgB1fD9hnHE+f+vaR9PucgkBaRlaV6h+L55X6jGsy5LKBGKZ1F2SenuuYNy3apZzRjTeWvqamM2IyxscD7bYZE49WeFjDdI/TIadh2h8d0Wj0ioO85nTKaDu/59rBqCcHcxwERwZqdjwLgtEGQhNap4w7n2N4lSKj3riYQzODaMQumFZDuRn7fuNiGBGGhQ==,iv:IHUsQRxm8+dF6p30VUsBK79DrPM3ROCdHkNK14Jb4Us=,tag:lSvhTbS2qiKRB40sWabvnQ==,type:str] -SENTRY_ORG=ENC[AES256_GCM,data:NLcL,iv:T9ACrxbQ+t1F38Iinem4fP7x52Whb1DK3UBGomMvc5Q=,tag:ysro3OF2u/biNgQkJ88RwA==,type:str] -SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:6Nw=,iv:AW/nNSkLutTFL1LCvLpHg/B8at1QG9uT7Vgc2h6PnL0=,tag:Falok1cxL1rtzfdQM7+RCA==,type:str] -SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:ARPx73gaS6aPcu54iBL9iLoqD56bmZHxJd21T8jq7Q86kt0cDQzmXCooOfnySMAZyGu6Dr/3rQAPDos5err2C4e/Vo5Cq9edEHZg7ZlX1A==,iv:LaIjGlLb3du7SIiEaOVs9QH8EmHV0khZD9dirOk1qJo=,tag:aempM2mdF/dIzGOT3ZiStg==,type:str] -SMTP_HOST=ENC[AES256_GCM,data:GXxeWN1vPBoJ3rBcZhLcOBpjrXm6TC0Pw6wqOBTkxmId7g==,iv:9mI4C0+LF29B3LnkcT57N2QZ6nefJdFb/FJjm3v7BNs=,tag:Vjg8oswBxEERkuLgIHOrJA==,type:str] -SMTP_PASS=ENC[AES256_GCM,data:MGIc/933R7Snd6zRCqVpRXGFUpURbHRB85NDM10Ogrzu3BLP2myEz+jCy6c=,iv:DfEFgvs/A+GZhgHtos6Uh2IJ0bco0gXhZFxf1XrjoiM=,tag:zcgatj2yVIxBsilAWkLo+Q==,type:str] -SMTP_USER=ENC[AES256_GCM,data:pDD/xGWqvm01AyFJCKWlJJ6Y+cU=,iv:zPV32VpVrwofsuz8jd7qC/W89v4lhxDp4xaaddHRq4g=,tag:HxyXmJP9ujO57ZBf244lfQ==,type:str] -ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:8M7oq5CzSxu6aB6K7fC8CWEfdNY=,iv:W2yDnL7dA74ALw2ORhxar1mZaTE/Ds34ni/B8GpIJYA=,tag:7in8tKyOxLb/tYWT8Od0WQ==,type:str] -ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:mnLljrjAzBnNKaFImzs000XvJe8=,iv:85pHkDq/R6gSajKQJ0A6x71e/WUySvSjLpWnxp8oNig=,tag:qIP1AQQqUgTVlTT3DQzt2w==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxM1RhaGJ2M0lVZVhIWDdG\ncjVCMEYwTWYvdjMzM0RqSVJTM2daKytuUVFZCmhIcEVSSFg4dHNRYkdXbG5Wenk0\nTVdscmFXZDdSNnd2SG1wdjVTS0dkREkKLS0tIEtHV0JOSHE3eHZXWmlMQTVYdU1K\nNE5nUjJaYk5KRTBRZXUyU2NhZmFOUDQKucEo4hRAKMOUTY1g4M8HjvThhqdWrTHU\nFrMJ2q25jtSNHtB0uNq1fMmV9V5UqbEABWRYKBbgDBucxMp9F6+gqw==\n-----END AGE ENCRYPTED FILE-----\n +AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:QTHQKsrGXVcC6n0Df+FPitm4spVEFmor8tBW1Us4KecRHT/CS/c4liG/zhmxg608o1BImvYUoNXOnAUzx0c7rA==,iv:UyRQD8CY/wKgBvfVwT9A6KME2OS8qr/DaFByVw2hy40=,tag:tEAiJg8C3RQFzVbl/kof0A==,type:str] +ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:pEQuEORnajXVGVKBeGIybmceHBr94su8UjdRdxVDs/w5P2lA+AOkljqmf58pFp4QYljvb8WM0K0Qucu6BX6nsg==,iv:5JKeF6ZlnlL9QKtSZQvSA8owri690+LfqdjO8qH9h0k=,tag:hYnfaZdz2U1NZrOyGbAmRg==,type:str] +AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:Zh6nZj9vyGdL7YYyjZnV1Hu2fsk=,iv:vIN6uPJMx0+BDGcHe3tazIOwZdzeqYSznyxDWgPE7yU=,tag:AkTJteLfaO55PmCpFdDHGQ==,type:str] +AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:AukGF5oUW76exk8tlVSCK8C4EbY=,iv:uYhLhWI5MmLLYX+0tE7b6JLSUmVgfV2/M/Rc2dbJeO4=,tag:a0vAFyRyyb926Oq1UMIGaQ==,type:str] +AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:uM/KzblBwrJX9ixFEk3thYP2x/c2pn+GHU0SfkbvbNUo6bJQKPHUtw==,iv:Kl9+6SMhzqd0/FepOET/5wOlmFHG0NSl40hZ+xpc2cw=,tag:ce4G5Wj8IhLtqGH9j4yOgA==,type:str] +AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:3yhIGaVfcSx82Iu570lGQTj7wg6/U4eKLJBX3fWllFUXPQ7YTaTSxg==,iv:Zw789TR9JYE8Zub372k1+nUUHHp5898+leNGmUUadxQ=,tag:OdHy7ixCn9HgAcyOotFf1A==,type:str] +BACKUPS_SECRET=ENC[AES256_GCM,data:vW/KL6d2JrTLEqMyWdewdG5arNFi/k4XItSJIpKtLCubWtVPA7Vx+ndKafk=,iv:A08RonZOgfK5Rv+jFCrPulB5GLFtPWQhzWEo7qVI0H8=,tag:YRMdEGEXtHwbXYHOxGb2IQ==,type:str] +CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:2ycCWIKjallqDhI3tI7Y3DkJD64i6PpwTUkb+mTcFiho5W53mCjqX7LHoBQ/KM6//aHN7f8=,iv:14R0jm2nxMCd1pfRDah2n1b807opRio8/1oMqG6CBXc=,tag:uOH4SQRvgkW5wumxqQ1RfA==,type:str] +CLOUDFLARE_CUSTOM_HOSTNAME_API_TOKEN=ENC[AES256_GCM,data:aQjJLeWn2p0K5aMlGQezyhhnuvp8zIKCSdEzQRPq5y3vq5cRdrSijdLNl5PyFbyse3OdKW0=,iv:cY4t9ygDSouRuebY5sFQnOsFmMHD+STqiI5A7DOxpQw=,tag:CBEsbEuvR5sTy6iSboIfXQ==,type:str] +CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:G8HYYAD4o/9v2ewmQHLoyi7Gs/IC+rRfVbtrO7WG9lo=,iv:UU5q1PeCYI685vP0M0mPnfvUg1jEaJF29+n65YMGrms=,tag:PzupiZcYg+S60keNcbZscw==,type:str] +CONTENT_SEARCH_TERMS=ENC[AES256_GCM,data:boLscLRlauGMqji0t2Xk6DknS+o5zy+Ah1Drg+f66OWLm3YCd8Y88nYfUMcv7L/8/r3XvdDXbpWoE0ebgR/JxM8pyXiaJdudsAxgX0956pp/mZB/bZVZGd101zFPUEIFbiLAncHZxHDojsyxmFaPcqfaJnQ98RXlV8OvrkvfxRLUhCILoPFpNfhDkZY0IzHUSc5oF2rZcP7LLIJR9enpsdWzVNS5XtKFVnTQgpU+1NewO6MmSq7H34A4fohMcmegt/sdi99xpw1ASs+31jaPcBr0hnpivk2crHKlGdvt3FESRb2SFgcxS2w+ALBji7Sq9MuS3aRhPOn60klg7mQuzy5yiOVDf83t8lkRunSDvJxyWKuZ1NZOo+/N2mlcZQeEkQuYEjnxeWIji8fCj9ICqhIgMxfsEosnZoR1VFOKrdz1rDPSvYdSS8WLbVnsUp+MuOEWnak0JIXYyAOJ0pLVPE/cxQu1sDyEHOfHQyiPwtzbWc0j6451iISfDDMgz0FK5Q8/AjhqdjMKE+vfyO4pKIzpg8U+aJKppAxgEi1AfJepOfO35OjI2Fb/cAVOr0rv2Vuk7bODM0XaNowuHQLOpgDbGvKR5QJKJK5fHFRBWHKQLrff0YgVBMwZC3b5DkNhSOofC3e6FeZcPeQrPVVtQPlFMTUYY8d0rBO0rdXbeY4Sz43jZXTCAmoWGNp5jEecC4e0yqTARgLnARu26YgmVnAmY2/Xz9rCFRxktPkMroJuyG4PYXCGfEQ+1OlF4cWXL0WIStAHctbWlTvlQIr1p4fcFxVirlz9yYoDC7WsEyaM8BJfmqkJsif6P1pvB8j4CA1HSsUQpRSzaDxKavZc6aBm2xaplpOApsPm0kASrbKT0q6mRk7tvXXIuZuh6MtUnXVXqwfFIvbm1LRDwesm5WX1Imy0Kw7le3ptY3DSA7lKEyfrgCPRqHD+SXXio8ze4P183IiBJ88W41MEvS/ggHpoiYifGCPCzKjMpFE7Yokd96qBV7TqAmHe0iuXiCcUZ/2Fg2BHoQnRA2Fe+PyklNCfa09OtOdBg/cIAJPlryixlQMoIogjiSPQhtB/TA+WK2bkXAE7Ohojx0NiqrSAxCVsyxyXqT3bNJ3WMNZT0hTM1n5mxX2t2hMd1y8FxVmYqZgApzMwtymvYYQGBC6rBgNoaupcZ4LRb7o7EmbYZs7NAEdshx/2lLea0SnUPJI5iX//lbd42U9IH6Dx39Nbw8a0ZlK4CZTpWIzlxCdo5NK3un3vEuL+qA/X0aDRp4mXly4hvUglpStPv49aISfZkc3PggvlZ/kDUrymUFrQmIyfq6D8/Oi5XBdCK81yJuhpjHFzPMeFzUNm6f21gnXO7ozZD56RArRGg6xgsUubJoRZQbrIR7nZkgr/0S0D9N534Oa5Iho5bT81pCWjvvfcK5LZZkMfqOrmk0nG8KkK2rQjkZmTZksmIlkU11VDu5pPavkH+GctrLgui2agzNe6FiRURvZqrpkCfBqQ1+GgftqRuGsgN0hXk9Ddn5m/b83VrXR9/qo/amSCgMR/F7qXTlYwN/zc0zJ4YVKxhHXSbpDX/0QFzpJMBO+uz/0eYhRYTB6P9wA+65QOIApSTBmObVVU9HtJhyjOjf+ZHQg2uuo+BKRu6yEGvILZ57GL1vAiEQoXgFh3JNadzHHgMs+cmeyzXWdVfXQTjNQ4qwNFA1WDmUxc9/D0aFWHCaGpu4GbMYwXiOuU3P0rLlBabdNg1CikJvFl6+blGhX1gfYnLztng43nrAoPezBhphqbgU1fZFWadaaknwJHQuanWQSHuyQYI06oG7PRnL7iAet4x/ZZ7/t/t8qSOQ8XEpWsCeGYCYWHnXk/oLKPDqKZVYxFV/CiTmYpgnjZdDqtLb75ydGRL0KTOzhkGP0MtETlIhf+diJpjB0LfGkDoN4Lhd0/jdbxiFy1p9XbRQdybMGhspxuIMLR+Qf3+OyTmmywgbphfN1PPghykHRpkvLlZg79ahtl4Hr3ysZIv3dVTHrEXQxePhQizeMhDro1U7ecjPhPvp/51X68fiytS6Vax/Kpx0UvxFwTBsdTRt8L5EPgmd3FcHzJw7KLrFB22x2Yf1Me/af0Jd4B//ZYYltuHyEVwRjK3m05PDED5wsMubcLBDRjyMFRb10UUayav/KFUwydg+H1U+665kAaBMh2j2docbks4a/W9CwIAIuU/Ln1u6Rj0/ZSAL+ZD+o4luz+flHgc988g7dKsPW6xCDiI1BHjdFxHpRh+mHTez8JJ3b3dDoSQ5q/TSlOmdH353r/zveVP62o8WKzcOSaboxMseUzKr4jgO/+mC7WRqbhbSCotWRopBBpbgUKGCeGpiI8wpStZz5CPWA0f95BG0EkVmh2+fKwo2vH2H+3hmNfcS+xiTnoaP0PdzpV0BAIqLbczFMhMNGUKaQags/UyX9hsommmGAK0VRRPiaWBRO7Q9oqZFLwbNJfA8jhRPPNNm22QgLeLw3DvMQTOtv8EIM2Aj2k3SUGmzQmxeZtmXUWSw+rTp9+AF8vzxXDSUBg4sqC/qOvVKTl9cBrO7EKK2rV57Cec0yGWrHXxDKnxpDGyb9DzrWoJXwiTJNeduBv6tyhtCk7O2x6awjVUIodQ+yrdglcFDyJvS6i/4uaEZNbAp22m70VKcI7EXBVYDEVxAc0jiVJASzCSsckMTO6BnPlyioGnyceG6YsJ2yeRMh5ZWjaIM4F1j/BhJFEJtK470Q5GilGYbLLC6B3vzEsoF3pE284OQdjsp8Qt3hMZWw3OFdC9zgBaylbnZBLBEvm9vZJnQSs3Sq12iB1c82LeVCNBVRaMawWZVrGhh+cZnuvrRNkdS5Pz06B/iu7j22axvseLaMJYDAKHAPJxO88ILXgu5pJOJ901mXHrqyys1WAozr3UvnaBz/IBgw3O9hXkm5mxRCZMS4C+2WrmmmlqmTMUtqZ0seVZWUUaSiksZq2eJ2kP886KLM7RjZd1A5O3W3JN6xwBNdSKJaNvhvWX4MQQ+zj1HgHEyxXeJcmi5tZ668fggnsLeEErfkaBhR4ApuJM0E31DU6okTTEZq7ma6yd1HTh2CqnoiuipIC2hOOhpmxXmMLRa/IgByU71pGkM7YUB4yOaJ+KEGvvQf4C+oFsBc5WIj8eSK9pl3hanGD0NCTUxKI7rQCYIer2MJTfxbSqAuGS4YffP5ipET6uVZ8tIb4iY4HvdnDq4u403Zyh/5iCJMVv7Nz9pfAoqG71gpVSmX+zdPj259R65uBF7GXg4Vy+gl/QjYFxgNYSqPhmDghLsYEogyHbpQ8/vW2vfpJDSm2VPRZ2apnHtNcsDuZZ7Aarh2VcLqbIZHHShnZwwzBy860jfpnoGswRBKY/N7VKOB3h3bmSD5uLDOmI1padLTX8+8vUDLClkt5fXydRPFJNDu3YkVP4YJd3ioYW92RZBWhtosRp4sM7rXA2yX16UeP2EmlWFtXzYgBNP+5TBn9P3OtqlS544b0tBgUQqNCjq3wmfSu1ZtoDHhnlLmaLSxP/iLOoam8RbzXj/1UNhJ1NZzNWhX/TyH7rpDxdcK3So/lsas8YEhn0VpbMt7QuFfKWdB/Rzr63Jy0+BUt6uUVnynnbwmFuNkb5hwJ9zUrYZ15PkvWWPuIgCZgkw6OOX2fyVJEMSzwdeyNCxcCJnhNS+uT8sKTCdqFng6XT3zIfyG9JNfA1dfPF0Zw6boBWGGE2r9Ebu0IfPeOSfwYJow1mjYyAw7GyFSMSApPHAcDV3GjIpv6DAbzzAI1LKN1lO6eKlgrTL3fh4xZ1ftGrMYyI5viPFttn9teflde/DOoGPdWJwhZ5/spwu38bM6w9r0UOFDpkB32fjEw9HtZvFnGmpzUGKyFvxiCRZbhqEnTESsUozX5SJaJt7mhgnWib/ULEASRCFBod0PDosqu2OGznQt2/sOtgqNWkV0IbvPVBbbr6gbIxLiKnsKubE34/h6+aFBQMfTK0IYVrN1KhpIYjqwgmorJhbEUWt/vMZDTNoDj7ebRXpO7kuDp7cC+MggI2QmYhwEMsNY=,iv:mRgRYJVuUOeYwrCZbjZIVZzjNuWpTsLkNYpl0j/wZNE=,tag:kOtCOlJB3gKyzaZsUo1Y9A==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:oEBTHXU0LJ4AmF9ProfQDon5aGG4vDQK5JIC/o7OENh6Od9I/zRMaxOvyPM=,iv:EeYD+Z829jA7mGKL0HUDoMBbMTpxp2IaDm+Z+Ik98K0=,tag:NtmCFVY4OCmc73dKlWvtHQ==,type:str] +DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:4OWiB4JXhX2j/yk8YF//3yYLZpG2RC34yUJUeBi9ilNySQ==,iv:2TPp76R7IK2LxrFPTv1BbYIYEEf9oyZbY9fODp8negY=,tag:ei3JiVgIS44q06fvPIPioQ==,type:str] +DOI_LOGIN_ID=ENC[AES256_GCM,data:dBNBTJAj,iv:+L9bHQ0qpE30YIvkHhLy0F94xk4WltCQ0kiN1tm5MH8=,tag:42lv4Qc61GMca9SU22yPaQ==,type:str] +DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:pYL86VNuswtQyo5VTmDSBO8FWbo=,iv:eyDVpD/d9R/YqUDiAN823aRtWJSEIx71aZ5vzpSxMTo=,tag:3dWa2DwrxdeYfYxL4G84hA==,type:str] +DOI_SUBMISSION_URL=ENC[AES256_GCM,data:tUIkPyvU2TE3cWaGgDf+AFgzFgDibsgUUV2hNkdDvJxGYsAecNaq34o=,iv:SoWYYFQaI78/4KdwSu3Q70vYsTawiwv3rHDCPAN+J0g=,tag:u+4BZU0agtSkFHBPF3G76Q==,type:str] +FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:w24gixo2dL6Nly9Numyq79W1FkC7gog5QxIwb7r1iJw=,iv:NJXaDOiofZwIHtuFaFbAc1S/A7wsZURUt5DD89lvJIc=,tag:C4hoW3wSrcEFXrapr5UVQg==,type:str] +FASTLY_SERVICE_ID=ENC[AES256_GCM,data:km/VXm/CCP2VUJFkdrh/G/tr+A8VbA==,iv:x9vrwMvPg4eXUQB3MuzZ9Ra5jD3y4hJYHGl2vTWmyys=,tag:rXPz7yyBL1i7g3BNe14xbg==,type:str] +FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:t/EvVAQw2gbV7qwOZO+MlmKqCETbC7gqRsXrKxDvWUk=,tag:g68huzvs+cmz/jNKj786IQ==,type:str] +IS_DUQDUQ=ENC[AES256_GCM,data:+obHZg==,iv:L92l1JcMlAf3eBgrmn5ktSkRWAu80OsM25pWAwbCGz4=,tag:mgSQTM2cjEyoG5tVwqQQIw==,type:str] +JWT_SIGNING_SECRET=ENC[AES256_GCM,data:dkLpNzbugrM6XUEjI9PbmXqVDMB41fbL0BRCZ5Wh1/cnXqpAZg9X1+ql1qIAWl4vUdTT0Qla5hBvQS9AcTl2VrCsu3opJgZ9KfooMnO6fgeIcKn2jolWWOXgafKZ9OauXHUCCAyhZK2MHz7kY2NcaJHjqdE+gzec0BW4s+T+44iKeX9lWCueGD0Z0pomAfJCHVh5TOppxNase4rth48Fpl0b+jbIpA9puXcFSHV4xfPPnDlo3zeO012HdTf28QWOT7hwnw5aTBDKyqFoUNoJ/wONtzYpkMoiuYAWJT8iamrCBSJP5yuQfnQHHBxlx8DVqN2sB598XkvvBRJTjL8c4qU/9sKQK+SeX8OiJ6b7oIr6tnBuHGtmHy/atJqCTlABv083gLA1Yvsl7oI+3XZkqWcuT1JmsJzM5IhfrrcgY66qVAfQtBwd7SLB/sX9286n62FCLu+sy+otl4Tc4l/Cbm9Eslp+LV+XD/XvixqSnrH1tVE6WZv+t0LM6GA6UALm9C0kP+WFqu91/q1hK5WYXTFpeOCy8t1u+ORQUABQf5HtJMxgsk9kDyKO8IlQlLs63szsa3kERJbQyy3UEKJ80Uywhkgz6MDF3U1QbJEfGxgq7wInl4nGFLe9Dso61ULzUHOZD+03JcwS1cVpHXJO+2FsLs+diiV16XqNxUu26YFu/cbCc2/0mbrJoFF7KnrW2e8s2nzdBsWh0qj0A1mNWmhEC7HkmRaqkajhGR5Mqgp94P4/Aoz/BIJoiCXOk+LF8GpodrghGSVDsJBGD6/2om385LE218tCnmShzI8IBT8oRh7IXnTKmZX5Pi1dxD3GG6wjtFL5ecxNRlVPZITCEbj2sIN+HaweqP8rzgufrO+Qbn6uySR5zjAsvTZt1QRsn7m9nLFHy5UzZbrpIxt4x00x14oUpTBnDhakm5OYRkc7Sk4o40mO1ibSMdXBCRMiAs9Wwlw6DfeBxF2zbbNGX3T6A8TsNuZLhQIPhol0KKNNhrIeozqi7w2gmJjTlJQVttg0fin8KFI9cwKlhcuFQknO3bzB5v7RxZEg6PdWQvS3L3f9uuISwgFS8sV8IloXIlmJwqDxE/g8bhRysdj1XHLxvSxd4VNex323H7Uw1h+6U2Pqufa4JDbcOUl8a58DJE69xNs+9a0vKhI6sghAe8fq682BNM0kiV5etoT4AHwr61VVnT43okoUFWBfYRwnWhwn0kDZ2N/Tc6zIF26FRXqKRS/WKNI4KBN51tVRCX73Y9BReSEMAHwz64A4wnvn32orT2ICejTkauSawFGOcBB80kws3zMCcHkcueu+5aZL4687N9h2UpAu1S+JEwE1HGRTb1PP9SHE+/dr3+yVig==,iv:bepSFYamCtmRhGMhoMUmEvKcwGX8KG6bCPlU0fE6dsU=,tag:9by0PjYZ9GZIpja/XPzHYg==,type:str] +MAILCHIMP_API_KEY=ENC[AES256_GCM,data:OSemiKy5JLCj8q3cB6MqPbDe+BJv46zR3Pjh59l4qTTt4/9W,iv:FQ1aBZDU26e9pVwiHjoxit4h88EWYSFF0YS3/jE6edY=,tag:Eg9BksR/m7a6UCDFxnSfCg==,type:str] +MAILGUN_API_KEY=ENC[AES256_GCM,data:vmeClID/tuJeMKhAEBqGuUlKVmoRZk4XH80faSFBSkfoJqX3,iv:ueEV3T3tD1DEgti4MXQLgEohKzgqMwHKK5jCMt+xlJg=,tag:o8++Leq8zn0yESoS2w+GzQ==,type:str] +NODE_ENV=ENC[AES256_GCM,data:5sqC5U2w3gs9bA==,iv:fmfMUpFPGfIt0X6ZAbm4j4oH2pCiRYy7OG66eb7C7EU=,tag:E9fgASNvznjqbzjKMn3VWw==,type:str] +S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:D2H1silMP93AZI/N+rIR2iSkE9E=,iv:lmMAB7dcHhFyS/nxjCBtBJrrH3NIrmg+GiO/ba7NmjI=,tag:QQ2KHknsdIisnhgKDrL4Xw==,type:str] +S3_BACKUP_BUCKET=ENC[AES256_GCM,data:l+WkrTk4NsH/Rfc=,iv:fkdyvBn//ULtid63U/CKTXgpx70bk46QMQ4sOBZzdnw=,tag:eOkieRCMvwymWm+uRnpTaw==,type:str] +S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:yOdkAwO+XStgzkwo8Nt5RSDU96JexzeU2UwMpORgzNLQXm4=,iv:kw2k3EedL5bjVbCL6CEHY/F0aSNg0lHAKUaCb3EzGyc=,tag:sKwXq+SD3+4O/a4rs3PgIw==,type:str] +S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:/kFd13/ExfcwFX4/zcQSQ0j5E/9M0avGQluhl6I00lXS+eJz/qVg8A==,iv:kyJaxXyk9cAXFNeATBjMqC0yEvI3mGwD49a+/q9tUPY=,tag:pYuabKR84BpiYYPyGhoc8w==,type:str] +SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:kXbKKC5rK0JNyp4YXOk1IObQEsDjdbg7lt5l/lKQzfxHi982e25Ux5kTY59xp2SnDyNNVQZaFa5qhQGKG5fQBKDL6NNir81f9hdIimLaPr0f/Z7zw8Lx2uZoOXwf0gSRIp3E8tnNj0aj009miOp9zfd/gHUeSwPXdF2bKxu6SMbSOXoK7G89w8gTLtuKdcoptqIGYAWlUYOKmYiDY7XewRp0vil1skaom6Y+6k+t3rbr3L0a9Q+Cc3pxDQ==,iv:ZiX210v2BthDmlh0wXLydYbj0GzBY/fb7VtahLt46+k=,tag:+2DVZo9oxBFojHtEbFXUIg==,type:str] +SENTRY_ORG=ENC[AES256_GCM,data:9rhL,iv:qWrrpZLPsBNiV85XtjY0MCj4nY3jlptAFmjxiWAXQeA=,tag:7gh4OpcrMGWeRyeLsZ8ztw==,type:str] +SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:azQ=,iv:aqInEL2yeXtDwZPL4oovJhvvKDS1VohqCfKxJhijWcg=,tag:u2xR+O3Gh4Sl+/SBqJzwUQ==,type:str] +SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:XzGfApMoNtWvpoUef/NlC4P6ms9FMCRnMHNs95ZBk89ht1sDOlBCdtRmt2/Kzv4SQA4oTDRA4J357AGZKP8L/UeWswBHB1QeFnz9GvZE3w==,iv:XNrtFa0gGgGcFcyMBxKDF/8tJpRSdJ5Zqi+l3m6M8TQ=,tag:zJrK07YBxmc0X4bfAUoDhQ==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:GJLUyrkiEl4zPyTj6ZxNCKPZaA1aIhMOlR4vJ7fohSrYMw==,iv:aRD1It5hgjVSIMEbtXOJs8/pUaXHsQ063Zfko7Z6+C8=,tag:jl1t5KiG3ftueSdHyQzkwA==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:+pnmEDvbXpNLpnroMv5FD2Y4otwjTk3k2JX82+ve2xIs9dzKMDkVPXkXcqo=,iv:cF/29JYKF+IwGRAWZzLuDt0+ZxJ3K/eegMr8XHG4GDY=,tag:/932F/VShBTzjBR03TPMbw==,type:str] +SMTP_USER=ENC[AES256_GCM,data:FyH5lsmBv5KleZlUM060W11YqeM=,iv:3xr6bPbG4pIW6Jc/pnuP2RAtl94JBYUJ2QdwOsE0mnM=,tag:zOkkt4HVcKjiVQNfp2IqyA==,type:str] +ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:TbcRcUKMSbnh4uZa6eMMzwTGVbc=,iv:zP1yYWFNh2W4d7+IP8v9ut+qTmgZ2LNNa7YzALXKOzE=,tag:LRKOtZYEvuelCYU7m6/kXQ==,type:str] +ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:AuqTkMUytxv42YlsARXnGr5lkXE=,iv:xZdJ26k0WSfwGdkxsLLrpy8vYzYQRPM3edhksMSGOUo=,tag:v8YJYUqGIinUGRr4ITA5cQ==,type:str] +#ENC[AES256_GCM,data:5UNnOoaCSXve3FQuWEeMQTU/bvg=,iv:TvRWMmfiSIgZu1E/H271RZg7t5ZXZuS00ZWUZNZuHU8=,tag:Wy+4V2xZWRQ/p8YRR9fBUw==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:KD3dcG0IBxI65gS9zn0zU2OyqH7XDLaUjF97NBtg65XQSP9LmA==,iv:Rgr2YOSprfARpIclEz1vnvv9d5n02ICtXILisV98qSc=,tag:jIh/cOn4gxP+u/9GsGLgDQ==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:d6wnR+55BHfe,iv:y2qsuRl8+74N6CreatAlEnmezfTRPvOuRdaLF2H3Vy8=,tag:wpsl8Oo9oPJD0KkerDwsCA==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:h7rW7LyWb92196d7KvJ7gQLMJAnTwNw9qV1mmnyU2qwUXottErv463JD/87Roc2NT/VPVrs5lNcTGEMF1uikaQ==,iv:Q4/I1SPV41YqYoH2LSpcThgb89jcj08oebatpmKzOBU=,tag:ef8yNKjrJCpgyklDhY2h2g==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:oDMLpeLW5AjUPjzQmwb59NA6ULq8YdLraJLeuKR5WK6yBTZt4OKImnqQnmYXCN59O5YCrqmNFVJDci18N9BQYQ==,iv:dGYxwjgQn5iomGFldVMWnJpfxLtyQkwazDO+fBVK2X0=,tag:OZjvP9v87af9Ubphi1s2WQ==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:7Fj4JuBQ130186MMlgURKoCGijYwDASqAjISd+hnoQBajNuxXuLOBg==,iv:0O50QSvdldSESMoBNZyagGz8QYUkip7QqiZkcwuAku8=,tag:WZSTC+XX85vPKmWD4i5jSg==,type:str] +APP_URL=ENC[AES256_GCM,data:/x8aPCCcsslp6gD7MwA5IC9WO8tAHg==,iv:MIBAmZCLD21QI4CsDWkyTIuSfsY4BhlFRRQ+lI/7/Zo=,tag:UNR22KrCQdSpMsWWPzXI+Q==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjSmZoSHREN3NVMXdFNzRv\nRmllOGNYUExHRGFXamR3dE9yb3ZJYUsyaWo4CmtQYjBmL0ZpSkdtL0ZKUS9ya1gz\nN21keVZTeTNFbnFzbXhZM2N6Z2xXTk0KLS0tIHV4bklFRWdvMVBIdkpyd1VxYnpL\nNVB0SUZ3aTJvYnlzRUpIYURLRlM1aWcK6Vpqr6F+Fa2jO9ndX5OeXoe8Mgg9KBPa\nt196Mw3nG5v+/9m4iH24/Aip7rR2USnuQ2NWZ18NqdBgb2cstLOWaQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPck9EQUNKMm1nQjhaSFBk\nVElzU3FJRWZXbHl2M2VBSW5ZaWJtcDM1Zkd3CmJQd0Q4TFh4M2NrU1Q3dlBrc1Yw\nQ1dBZXdOYmJtTUpGYk40K3k0ZjFyVUEKLS0tIHFGS1cxTzNza1lHbHZyeW83aEhZ\nZ2JhYzRVNkRQUFpSTDhHOFJBdnhpQUkK4RTqWukZ5TW8vluCWDbfVt7Ft3RctxSs\nzSe6OWr0/9+geSyTS3LDdYnwI881c4utCkLT38iRbDNWrHB+wevhyg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnaXZUcHR2eWJFZWpTdGYr\nUVo1RG9tNzlaU0s1RVNwbGhtQnBocWpmQ1dJCms0NWpGa0dBZVN3QTFOSGg2SDVT\nN3VoNmtuMzU2bktBSlg1R3BVMlJTaDQKLS0tIEdDSTFOdzFPaG1HdW54R3hsdTBF\ndzNtd0IwRlF2YlkwdXZlSnQ5ZFQzRFkK7eC7apakezOG8qrZGIy6JgY2onMw2oGO\nB0Q++/YzEfDOt7sipS6/IgWynq2LjmQweJcsRo+P4x8LiKhxmuKkiQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtbXpUUXJUL2tyV0lzeWlv\ndDd4VW5SVUM2alpxSC9ySDZmTitJVEhlWUI0CnYxanpwaHpGc3RJaU5yaHdlZGxI\nL1d6S3FzR3h3UnFvRVUxZCtrTk45ZmcKLS0tIFZwTDZtNi9qMjRqTWxQYjQ1TzAv\nS1dWeGZHQ1pWcnFTZW9UWHJ3YXBSazgKtiUFAtwXghA0PDm45wWKNY+ZyaDPfC+5\nWFUsd7CSnTnKrFvnIJpJKvxoP2a40dDv6D1/FgIr/2FgOuTP7N92NA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHbDFXUGRMWFdJVkZpSm1y\nOE00U1FRbEtCVnhWbXN6N3FQVkxvNWF5NWdNCkdaZ2E3d1VkamFMQjRHUzhGTGJB\nTjQva1VpdUdBbi9mRWdHN3pxYyt6bE0KLS0tIEdFVFJodC9tWWNJOW45WGdDeXFv\nS3h5cHlnNVlNU25oSUlOTlBsZFlSUk0KbztjhSLmznefAXgvroXIj9g5SaNXdjnH\nS8wrDSFixtT0o6ZOC+R0qg3Ny9txbztq8cvY3DsqrQjw5xca4AhEXA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4NWRLUkN6dGk2UFhmMU9h\nTXJuZ3M5U2luV1lJcy9mWktaZGVlMkRMVERzCkRaeG9HbXpPOGYvWUhJTDl2Smc2\nWW51WmV6bFFiNVF3WTBUTVZ1azQ3ZFEKLS0tIFQvaDd0RFZOV3FJVi95MDYrMzBV\nNUk0aWxBRkNDVkduYVhXTGFabWVEdmcKGTZOOe4yUfGjZS3uIe6NZSEjj/u2bpkb\nd4Bd259G2uVrG5RkK7AptKM1QHVsolAwF+MsdlopAux1sQJe75ZpKA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2cWlKczAxbXJYQ29FNVlO\ncDd2bEY1Sm1DbnZrOWl2cm82c0FWYitEOEZNCmVlRjFUdGRIV05mVkNIUlo2S0t3\ncFFsRXp1Wk1KaThNSTc5Qmk1akViMTgKLS0tIGxyU0pDd2NrVE1neUlnSE9KcVZT\nWEp1SFNQVjlRR3IxTXJzU2wyZUNaOWsKlBCNRxI/V6f8p6SUkh/FnzvZw0WSeRa0\nuSOH65C34wBw7F/fWqRDSnjrbNp4iGH1GPNUh7ogS4ZkodY2VQqohA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsb1FEdjNNQmk0S24rRzFn\nR1VjNXk4QzJ3K3lBY3dMNUQwU3ozUzk2aEZvCm56cmdjVEw0V2N0ejFLdk1sWHZz\nZlNUcEcxbGIzUDd6azhTaXcwYzRBUm8KLS0tIHBCWTgwUVRKZUFrWUpIdGh0NWV4\nditXdkFzQmUzRnJOMjBqNW1oMlhBekkKMmDJYcIz3iU6q34K9Ni/NGsyr6piekCJ\nIElWygu7hkVO3lzzZFyMCAZLpG/Y0I9xqcA7ovbkxOtabhoHNU/5qg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKTWxPaFJkTi9EZkVsdXov\nZC9WSVJPejhaNGg2RU10eHZ2dXBscWxQNXd3CnZxYitoeXhkVXhmOEJDSG90Y2dM\nbFBmNlJJMlJ5dm0ySXpyTjZvMGFqTWMKLS0tIGhWQkY0Q2RwR3lpRFJWYzNid2N0\nbWNYK2Nlc0E4ZklORGI0eHpUU2xSeU0KnbN1ppQhOJPVVjJ3p5Rk0jemcRQq+RgM\nBKJLfupw9yz2fsYIOn2bRU84JMGswDZd9KD/9hOKLQrES7V0UxlDZw==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwcmxSVVRqamVmdW9vckxx\nZ1hpWmJyeHNWK3hzTnpkNXN1N1VHakV6TWhFCmlabXdXUkZzeWRpL3N3TjdBZEdw\nZHJVNm9CMDg3emdRQmZSSUYwbEdScjgKLS0tIEFmblNNelQ1Q0d2dmxOdGNjTmxr\nSnAyUGo3Ui9oTUxac0krZmFSdndDMlEKpd4ME/6Lt0muiNUsq1AY+7NFbvUd5Rez\n7XAP+16L/nQsqCX2CE3w+m+ezlL3Xqd10RbTgxhG+wQWtON6NoRbRw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWNmhYYmVnay9MNHJlOFlR\nQU1KU1UzeGpobCs0RkZEcERJM0R2eHVzVEVjCmhTeDNPUmN0VVhnNWNNN2pLMXpT\neGZFdzFHSmY2c1FRVmdtbXY3Z2FkbWcKLS0tIFRweFVad09KNDd5QjJPbkEvdjBo\nRDAxTWQyWkNVb2hoRGFTRWRaZzF1SkUKYICTvgSDU5Zvh5Xwkj6PYYO4+l4NXd4M\nn3FMQ/HnpwPQB5YTDCNGW64RPgg7DizKQYhwJfvouN8jppNgLS1S3w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_lastmodified=2026-04-23T17:35:43Z -sops_mac=ENC[AES256_GCM,data:bLZQR9QReCmUAYgBkxVBdZmw+3im+oo5G+lxghn68KX5vRczkf2tS2cyFOhirXD6PvvHKbsQpZlB8bHoF3uD2bsuEzdPpbtawYlUJIrh46uK9b5MbzfzqNUmKV7i7m4AHaOYxyly3PebRvUzbgoX/A/R8fYvCNz/vemaBWZzc/A=,iv:MGRNy+Q7PZcs9NpWS1XeBdOOBvUbErkEeJWCesGa904=,tag:wQhGaa70yAn4eV8Z9BgbJg==,type:str] +sops_lastmodified=2026-05-20T17:17:38Z +sops_mac=ENC[AES256_GCM,data:E57DxTP+vuC9BlaVev4qkxHeHmcFLjTMjuUzvI1UjVdBSQW8ySig/ys2kRbol9I8yOri9IbHlD+Ii2XUIN7z6KhLVIBHoN6462fJU6uJOs8ic72b1lk2XRBFImhzhuvydx7L/MBfXt6+V/1G/ACq+3g0YuTVkMu2oFiR34rwcK4=,iv:APIaikdSLA5HBJPE5DZDQpbdgD26SX5gdUpIRqM0Tlc=,tag:SQR1QHHm3tU06vi4cBaY4Q==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/infra/.env.enc b/infra/.env.enc index da0ec5e11..c56371cfd 100644 --- a/infra/.env.enc +++ b/infra/.env.enc @@ -1,54 +1,61 @@ -AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:DtnOR9FslQd7A3/tQ4uc5/bZyreygrvCvmh8FFr4BnuAMni2c+xvDJWoGMiOrJXfM8SJXiGXwHmTWnpK4wWYFg==,iv:3mByOY1GW19guprXzyWhlQXupfMeV4aG6ZcnOjnjPDQ=,tag:5vKZpj/lvNuUYzADD6SxPA==,type:str] -ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:FB8caHEBJ68RH+XLuknJB8xAs7NAjoo3m3vLVmthJW4ohY/MOPkWGUJ3ZwE4ye8+MOIj3+FyQpI3XLI9ff0PS3fccHO+jVqRLEUra5eVrwF82xA3Avb6FQ5Fip1tsMvWA0R/iZAMfLz91q+HyB1f1vaIhDfl40OD2tCjMpAHsLo=,iv:FJLTcsH3OZVCTxhrHqXbmaX1GtxfgQHE5wBNToLKCkU=,tag:P1JccTUqSnndn4AMalThNA==,type:str] -AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:5DgOmZNNIoyIRtkIIUYNFO40/ig=,iv:r/JKLOwcirzGDg4GRFzSEqR6Eh8XCMUaMKuNiGOMr0o=,tag:P/65wVGehJnhlw/1g1GKQw==,type:str] -AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:8/ugchv+eUBwP1W4/fcDSup1bbM=,iv:wArSDC7pDcT47iXm2UFP/mIDNm/j3aA8ez6ehVLWrTE=,tag:+CNMCN1BeiDA9SEtQ/URbQ==,type:str] -AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:s9I+3HkZHk70A2DAVe9L6IP4wat8vcTeYkrJIbCvUTRpXXmAwyp/vg==,iv:G6nr/qcSDNiDPVUNVcKTaEHbZ4Thauia7QLC2qvwtq4=,tag:GZW7NMJbjbPEM/lk2DFweQ==,type:str] -AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:WgARPvtgbrOXn4uTXirt3tvKHdviODYPqAuFsdhNukIjtmtIczuAyA==,iv:FdzgdfVbQLvyp2esIVFbQVGuDO2McUMtldkTtgOWn4w=,tag:26YH4loI7deTZjzp/FVNqg==,type:str] -BACKUPS_SECRET=ENC[AES256_GCM,data:WhcQu4Cq8vjwo48kyeHf0RtHWuAJz3NsAG+NqAABb+UssEjxehHC6gkNqs4=,iv:SBGFAnR8fJmVZKCQItftj2/YjrW69xMbCJhsx6PQbSk=,tag:I8q/W9PCWMbqh1h62kNyqw==,type:str] -BLOCKLIST_IP_ADDRESSES=ENC[AES256_GCM,data:zJaScOpFk0Phag2OCBuwgWQir3qE,iv:2ZuGe4Xp+3RsNyZwUpCa7TqQkT8MLP3kRd3WVjc9m6g=,tag:aUIY+usy7StJhmP9mLuGGA==,type:str] -CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:7rkvVhpGE3J/+N14kyE0O/W/N3puG/ev5fBY0+i1JVPV0ye14beK14kBf/7MbkPN7pdidXs=,iv:rYEcc2loeZjL73BBJ3NTd6JYcsDdvakF4RNX9IjE5a8=,tag:ZhgfUXHXJ+1adfMNjWsdig==,type:str] -CLOUDFLARE_CUSTOM_HOSTNAME_API_TOKEN=ENC[AES256_GCM,data:+tPheMpR6NEbXOyniti787QD8YfCdgcootm10KK5by9FWfJbIqu3ZU9cHY62YyiXk2GHdJ4=,iv:mhV0zJcJRfWSRDleiq845W5SefdKyme9eSYIOBFRfO8=,tag:3Mf0f/ucG6brqC6TlGNBYw==,type:str] -CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:PCwdYZmy9QBOufSq5MgGPJLMTpdB05PUp+vt7VvcGGM=,iv:60KmrslEOnPuZ6UQB+RHiWUu8GW2B2lY7EJ5Fim7Jew=,tag:UXPANQeMulE8U10NQrp1zw==,type:str] -CONTENT_SEARCH_TERMS=ENC[AES256_GCM,data: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,iv:tGRkx84G7TMc6ma+twDJcV7cy8sXnWvPR9aURBsb+JQ=,tag:CTTS/6DO+KiEkGaWhmYDsw==,type:str] -DATABASE_URL=ENC[AES256_GCM,data:Vrt+du4j1aUaPtM23+wzJi3m5H5TOYsuTW5WBcB+/UyGywoMnK83rpgH2Tk=,iv:O6HtqO5wdgds+fkorxukESc05bs99L1JJrpEVW/g8l8=,tag:X8OnCdlGG/Lt4JTwH5+1yw==,type:str] -DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:8JocdzaYksxJpCbT/jZcSpEhk4Bt+OKQFgyYx0I=,iv:RccjvInQf4RyWjKbgeNjXrdn13ZkajosW48DKd8BTek=,tag:uhA8lt852nPKqGmtwGDIyw==,type:str] -DOI_LOGIN_ID=ENC[AES256_GCM,data:Su2Lh2Fk,iv:JtVU5RxcRuyxxfAE1H7xMby0IBwGWfBap1OTWduvrms=,tag:yvUfepFL7Rzg+zISDFBPwg==,type:str] -DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:uOwpgd0WtlMLApZHV7BKz62H6GA=,iv:E581+Gq/x/0NW2u6qznPNSXgM7cHdJxTbWtJQ+Ge6pc=,tag:BYvYA24ScYJ0Coxl74oDuA==,type:str] -DOI_SUBMISSION_URL=ENC[AES256_GCM,data:cbaJvId/YXeI+HoERODrBQNY9bVO7A5GF5A63X1skuCcBJdJk6atEA==,iv:J2+VZQmU0Lb1Ft5AHHQIyX0dYmHrC7Ix2H6Qjq/ZnnI=,tag:IK6TG3FSNRZDFicrG3fBVA==,type:str] -FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:YESGL6a3zJDvogpCPGX3V3nSLVAHmF4hDOwh8q8pryY=,iv:UnJ/l8+/mCQl3a+SudqLatl6QhanANZeW9FG8S8YEog=,tag:ysiYgJu4BA8mmrWOmIJ9+Q==,type:str] -FASTLY_SERVICE_ID=ENC[AES256_GCM,data:LTeTy86QFKpjks8KfLOup7SyAGWgBA==,iv:NVqPYIKAVmH+gb+87O7BeOrso7IzB1INm9hpoMxaNUY=,tag:JIFqm6/mFajwPV90OypBBA==,type:str] -FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:EaGxt4jGDMmdpqnvegtbzaR4IJXVNsKX4Xy31MTKw0o=,tag:rpP4/i89TvHpYV6JaSFP1g==,type:str] -JWT_SIGNING_SECRET=ENC[AES256_GCM,data:72wuDCGgAvZJ9NUq8p2IoS0n95dWWB3NCdr0osTN0p4hv9SKy+NTw33tjpnKKfTm1aGxvmKGwWrbfG5o0YDcwRq2+kHRSjECkZbn9GqilRdE/D+D7ajhKTXkei46AzvvQnf7cx6nYC1JKARJJyhzL1avnw/4H+NwnjWd6ItfDpAbyLQwuCE8Oi1VwJnVSfsR7nX2mjxLGTVbj3iuagO2aYWCm+njQ2vqy/mHHOjT4K87AUVc5IuSbVoV6vijYNXB3W5WOJzKmWRPVeIBnxNqynhkzapv1Br3UweZFXdQXBgy/uCxPi0Q6J8jNX5G43M5uy05g9bloqjm2hLmpv4/2MQTin4TjxnxWgtRnUFMRbKSrgzbKQ+sFj3gKQ+GGYU2uXS72M4Y4uN2FhaDAa+5ueUajFm/O5D2IJ8UFtGjGkzJrIlNW6YS+aal54dAIgUbrt9T9T5Xq6w1BSYdoT7a0NHqebb8NymF+scZhzLNutkjTQEabe0Fy8tKqCwmCsli7VPq4iPpLFbw5qJTDfTMzHsDen/+qvvasED3nyI7T7zM6gmM82XwmLJlc6V6mfxwbvVF1XxZRyZTOt313Y+vst+esn8tNwQ4hW1FRzILZKLjc3tZU60bvdAQwoxmCoJr0LUVVU+c85lMFAaDH+GTUfUfDkP52UWJ1wLAoLdKNAw=,iv:VxBtQST4kqbwwq9f0zuPtE4qqbDmk3a3+Wwy1cRAdCY=,tag:OJjcpbN6cpyP3VtU01usAQ==,type:str] -MAILCHIMP_API_KEY=ENC[AES256_GCM,data:St9p+GYB2JZqsbqpvFWsXqiS5F3UoKU30iX0ibhUntaotKJi,iv:oaooK5kVhd7MDtBOMkwUlG9Wwdrfq3HGUDDkNeOYXbM=,tag:xVkwLzI9H2ExRNo1gG6nsw==,type:str] -NEW_ACCOUNT_LINK_COMMENT_WINDOW_MINUTES=ENC[AES256_GCM,data:hS0=,iv:FNEBLfxNbUZQrSxyjKh6k1NLfOX6Lm4GG7+WFFAO4kU=,tag:u7QT3mZorvBmi8UXY+OQ8Q==,type:str] -NODE_ENV=ENC[AES256_GCM,data:jzwrtdbmFoGvBA==,iv:RYD2R56sdOxsACNhO8++X0423xEwSThqVOwaZ2YaWJ4=,tag:msVdFqbaTcp2QGqahAo8Dw==,type:str] -PUBPUB_PRODUCTION=ENC[AES256_GCM,data:zwYrwg==,iv:dbVTCcunNCDAUXHBKmfJwOet5n9epF2FCfNnTABQQBw=,tag:C5paBnweU6l+4Bwe3iOirg==,type:str] -S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:/YiCUr7V0+qyfFTPiZpaxRKbKyY=,iv:jaPF/enQZ55tKsnel8wrpUmUQiKe+Z9LOzIkNxHnUQI=,tag:m2pn7+NzHw41DVPvlGwcow==,type:str] -S3_BACKUP_BUCKET=ENC[AES256_GCM,data:uVJrmRqJ2z8VCEM=,iv:t2X/7DOumesRdQhxoTZBc1Za0zq6+F3oreYKEpBQPAs=,tag:5egKbV7idgAwNpZ35HkcMQ==,type:str] -S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:y0LEJqDVugX9/tmL0XTfyvFSeVV0cSJ2Pg3mnscUPRGafdg=,iv:ot0c3oac4lWf3gCOBbxMW71OJys9DCnH0D3efNNsluA=,tag:yZIsie890ijP5m/oORU5Iw==,type:str] -S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:HuAFX/fUCahjpSHzR6IzBQE1oJcX33go8iZMRX36QsQIfq5j7IHz3Q==,iv:bsUzuNX2L4pl4EiWG7Wl7+f8l/zHPflvKcQO2AtggV8=,tag:PAKwAxX4wiFWsaG92cx4Xw==,type:str] -SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:CdWM7W9oNKxmncyYtYgj3zpKZp/oB5z2SJ+XUWvHarY1vrX1vc0t523p9aqAfIzgqyZp5lrmifHV4Yhhhc/gLydoNrLF3vHjr5nIDFY9JANq0fJ6gUtb4aGCKIty/Sga57o8bpcjbPdM6CZNf50UH219eYU5XyPXuuiGgFQYPGYAOa1rbAjvYsorUAD49uIcW5VGDjtXh8k1jRS3nf0X2yWHVM0eGUemFfO3Y//JCpEpjSy+Fl7L/K1LvQ==,iv:aOHFgmBCFTw5NODpPBhs0CePTrlImvDJoitQdKO0/w8=,tag:x9YiGhxTnKSAX7015tXalg==,type:str] -SENTRY_ORG=ENC[AES256_GCM,data:xkvy,iv:se+o068ibuiqYQNxMbfX7DclUH4MfNNYOARVbPT3WuI=,tag:Lcqnk7QLtW+4bu03qD7ukg==,type:str] -SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:4cc=,iv:S2baOXfar5SmWjhpRPTltK2i4AKqqSUlyUtgFXknlkg=,tag:fL2wFoUarK2VRisolgAgSg==,type:str] -SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:dUi645vseIRPgegbfUfrzNIbFeLEGgZBfEV6uw1OY1uw079g7xZBJgdQnMcDvka8lR0jjMi3tphCg37ec+5uGJbcH6KOq6EVz5qZn88ElA==,iv:YIHNnDbZKdFCuLtEeX0/y8W8zC3FBJeP21gjIbt0nL4=,tag:o+T/3JleZFR6cukFo0CtuA==,type:str] -SMTP_HOST=ENC[AES256_GCM,data:J9FFCJd95Fvf4xAvHzBnlPGIXyobkjlkBveM4a1q+ZYYAQ==,iv:LLSfinAh4ADrxWIkW2daOkfsZ/GfE+p9QOVAnnNVsBw=,tag:9uMbu+eM+8Gujwxjst5Xiw==,type:str] -SMTP_PASS=ENC[AES256_GCM,data:/Hd40twt30Iy1tlb1ErKjaZzHYz5L9ntwUPW39EHNEbjN5NUBkBCdRXU2RU=,iv:L9VrPDHh6+LiTt62ue1rD13g5Y80NlbBM4h+SpiRsv0=,tag:G3XMaLf7rXtGLQMZ12UXzA==,type:str] -SMTP_USER=ENC[AES256_GCM,data:/Y3R6n6CMYyLoChd0izt1QtO+38=,iv:4rqRcCCUrTWUCmu7PzLOPHRl0xnpbmBNxQC9rUgbCwk=,tag:pBkePy9loHDK6pS5cRI1zA==,type:str] -ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:kK3Bw1Oge9rvLkduxjFw+EVNQAU=,iv:LX8THgcf/k7ohcfGLOQ5n+y+kPn2KdmXyqjtt6Z/moM=,tag:xXtAxiZBJnxWMx/WKentrQ==,type:str] -ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:4G7g0Pehiur5Qr9jKJNCaBVYzGQ=,iv:8blu6Qbv6QBN2KeNc2j0In4PMU5rhlxWe8aOZWkyiaE=,tag:VKGf8qbTFIOfoc9g8QB6OQ==,type:str] -sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4aXdwVk1pZXNwWFJWamdi\nekpjazVUZ2tpY1VwVHM0cGU0NHhVNXByeER3CmZaamVYQWI4UWtYcVBXMnFIQWpi\neS9keVA4OG5rS1FibHJ3Q2ZIQldJYTAKLS0tIDBEclR3dUdpQnF3NHJndi9XRUhW\nb0dvRUlURnZaMmVXRDRDc3ZrSkNkR2sKNaEr5SQdjKj7tA5hnI5LlxjKxBXLfHJ+\nnHSg1I2kP4gQ/fthum9YGY9pDWy4FGZ6sTJ3dBaoYj6SY1xVdphIxw==\n-----END AGE ENCRYPTED FILE-----\n +AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:Rb8p/Q7y8T8bj+QsSUoREgvQQ4juEJkkQJOZ4HCEG5cETtMnX2PeKEmGJnvCLDGQxcmcTNY8FOfpoQNQn+c+BQ==,iv:RYVAAggyGs7E3NCFFeYpCMdvv2iGT7Fk+vFUYxkhwTg=,tag:hvs14F7Y8dnBIWf3+Hp/Rw==,type:str] +ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:HHd9xcbgKNhtIFHRXNakSn6eszeI3hfPLfjdgX8CUbP283Q8g/W8naU3FMG43j6CxuAoZXoimo9ua9l2ClqEaL89oYp8UkPf9ySphTDIrwzWIYelEmFDUxv2kxaTIwnyziBSXBsM9Za93IxdU3WBiIBPU/xl89tBywzPTXVVmqk=,iv:0Iv8rAc4NGxYWgQeeIcvUtaIC/e2EigMLIlIpCHw/wU=,tag:+9Ufxrh7cHIHuBZxY8T/EA==,type:str] +AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:afGBZUwTv7JPJxrU8O6IZ0Ff8MA=,iv:D67ERuD+In+ViOquixuLNomlAzxtvFXD3Kg8z7b0Dfo=,tag:imReGZGqRoJC1+8FwSP87Q==,type:str] +AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:PPIXUkRfD/K5pMg73BH0Lr7v2uA=,iv:IOkV8xlarjgPwZaA7MailwtUeNOqY8L6+Gzzjt4jxHg=,tag:XSiuboy5dImloSvVVU0n3g==,type:str] +AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:R4WFN09++pHl96degrNkldpTdXwkW0CzaHugWlrLg4Tst9jhumc88A==,iv:Hp4lbrJ8qjf66FL255nmoYeKKRnV56Oeuwrs4GUrkqQ=,tag:IIJTWj4v8/w6cWKh9lmjzA==,type:str] +AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:SxZyLHf3uqevdyvYbqwzCv3MR9lQ2E41dkcIFuJ7bQvHvj7BvPu7eA==,iv:Pb52mdAjkXdc3Yv2oAqYHMgAoXJmhZtwb9dsUAtm/xQ=,tag:FNuwxcuGBSOKbyXoc4tJUw==,type:str] +BACKUPS_SECRET=ENC[AES256_GCM,data:BQuPEsZJ4nDaPBx6zI1SyE3zI24NqdUj2vOWJJA/83x1osWZ/uIA/CA0E3Q=,iv:isGQZGhJ3qrZzZ/A/3uxDfu3P6OZspJFuh6wsmzZGGo=,tag:dj1S4E6RpXdvo+iKKmJneg==,type:str] +BLOCKLIST_IP_ADDRESSES=ENC[AES256_GCM,data:vOJNtI4XoIt54mUO2G0Gr7Imlf/F,iv:uGte2NDGJ8FDzQr04HEbyy16eL9N3iQzAuEs083woGk=,tag:8QmjriYUgfVv1ea4NGUk4g==,type:str] +CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:6XTa179pf+WJuQezPfS18iTZa/0otUnG+fGAuVFUedloyFbT5DmHxYWLPJ1pDa7NUpLni08=,iv:LIpI+a2iEELKIE3jolG22toVBsouivpMT8Flp1gxhc4=,tag:TZ1TnmN3gT/Sp5Fu2y6LQQ==,type:str] +CLOUDFLARE_CUSTOM_HOSTNAME_API_TOKEN=ENC[AES256_GCM,data:BT+IafsvZ3vHtB0678VsR1IBsIuaYPsfBKIPwT3xb4wQ8TiTIEE0KRFYSoDsxu11Wg0q0/A=,iv:p3GAMzADfVd7pAffkrP9sgWcKaMUhTePXSeUmE9TSUw=,tag:dwNJIvRS2oTA1LY4z4+V4w==,type:str] +CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:jq/0WRN6Mk1xGwaDRZe9XWiySh+F+ZyjHfDJvg9sxHQ=,iv:ICJiWpiiXTqGv4VVodwXUvaAsQ4KXVGNXEKSTwAUTRU=,tag:TT2pXOQXZh+YKrzfow/Fpw==,type:str] +CONTENT_SEARCH_TERMS=ENC[AES256_GCM,data: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,iv:wJYo1JlO/T1r0V5xV08PWUW/ABKaGWtwliI9v0y9x/4=,tag:6U1yQelpvoBVwczyFGsnaA==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:kpCVMaY3Ha+ybplD9VPvCeRQEt61yoSu0Dn6JR5Dq8PtU1rdA1Ewk0YiCT4=,iv:K5yVw8w7+UsG09xXZTHC4Q0Z0VylRQBVr0h/QuuS7TA=,tag:UnEm3S6LO4/V1NB6mT6biQ==,type:str] +DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:AWe1swwvbchmZR74yfzheNNAeTx0iZhzUS5OoBM=,iv:vm1oWKva8CoTahrJRLEtQpKlcURrScVhQIHOeVSGt7I=,tag:YCmRyYujj1dwBD9KaS6DnQ==,type:str] +DOI_LOGIN_ID=ENC[AES256_GCM,data:ztqYO35m,iv:oXWNxzAPIqaQ9WYrbbi5ESNhT/9s8jakr75wEfP7Aw4=,tag:m8ufweCmoNhbcb0iS5RTkw==,type:str] +DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:uLERMaUTWMrpkiHmRhLW4RT729Q=,iv:THMJshfMlF232SRnYgdz5IcWC7q6h1vB7RpB8+Cnyxk=,tag:DS3eDHZVORVrKE26L7hq3Q==,type:str] +DOI_SUBMISSION_URL=ENC[AES256_GCM,data:oi98KqaBFe3pSAzSH+Rc0GYJR3SA6nicj3wx04J+vx0D/490jPNRfw==,iv:lNFpyU++R77YzGUM9elVCJ+j51G3J8SgOmEBJVtbKX0=,tag:xWZqnH4/b5wYPwoXsfmlXw==,type:str] +FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:1j93DIsNHb9HJ1DTHMCy+JX95cUIj6rJnAcWXsIEiXw=,iv:PfTYmWxl7lSY6JLhHbnFeJKKVMgFuMVzjum8NbWOiw0=,tag:uN4MGUe+KehHFsSVfwsJ0g==,type:str] +FASTLY_SERVICE_ID=ENC[AES256_GCM,data:cnhHu1xfiGQAZpTbdlmo5F3w0bsiAQ==,iv:8FFbwbJnkgR9vh8NU7sKMTaTKMgCpfjlQqgN4enSu7U=,tag:R+I+XANrlDx8bRo0rJU3jA==,type:str] +FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:wDDidSsOKgcynBnC7bxgADxJT4C61/OmGb2XtLFbIOo=,tag:Pizy5E49y96EYTI1VQp1zQ==,type:str] +JWT_SIGNING_SECRET=ENC[AES256_GCM,data:BGlvfkuWh0iHW/Hj96cZSY66xiK+oChbHzBTpXPLbhcwUYkDOjIF/bjwJZeiVfGwSmfen7ERi6ZjasI6vChiJMCVqdmjShInlaunOEUB0f759kdtqtN6jOKp9ZnMcQ5MlAE3N3XVPhNS0VuBY6F4zwcMeGLDXbZXc7HViUjKtJd+8OttLGgmE7MMjJ5+/ajn42JaGYD9Q6Bb7x4ibb4J2BFRO16m4iH0KQXEoWb20ikkh4H4EyC0h6BgQ49G5hnJIukkziASwDt1dN6NNuRU7RQSkPYi20x35tyDx2u/GxHAEZxdL9S5BHQV7PW4OyP7Kw9pOmVSFmrIiHIO3V1Hf2O2+Tm2b1qtxkZagMmXjMOyurtAoKlfXLIpEdXumSWZivFdnkfyXrGp8cvlajY1fTG87Q8n/C78AJ/Z8oiwN05IursUlHbSE2xWmoY8IEoLvphfZSZ2aZoeBUbe0+sjnK8Utkd2vInwhZUaPo+78jmAfHtcscIMzzAdZV5u7qkelkhDWOuo9ORKSbZ+MqcuSSNekUqIiA9UE3dnkYLgVtM/6B08nU9i4HkzXFyHh0AqmOMqdSBjXXSdAMPV+AjadNUGdyE33lTzPQun6VfiUNuOTt1NCD4IjS/fklvdzKuw+xkIZ/VhGEIr+FNxJJUnT6gDHKkAXLVotXYXwBJfiWU=,iv:10/6CnMbjNucMtV686xPWHAkFVzzl/SutwG5qafOtO0=,tag:UHFHQ7swB7kLS9XkknSKKA==,type:str] +MAILCHIMP_API_KEY=ENC[AES256_GCM,data:DsCvDCEZ3fHGc6W4XMFdN1OHLi1zC9+fwAToBOOs7PFO2z2V,iv:8XOGBn/W0OyhbLO2dYre0ukgk9xX40u3M/vLttz3BFY=,tag:7YNavzArdtVh8LoTKKf2ew==,type:str] +NEW_ACCOUNT_LINK_COMMENT_WINDOW_MINUTES=ENC[AES256_GCM,data:V4k=,iv:aqYkd18RYpa7kuuzbpAmSk9Vcr47BW8kIZm9gKEwEXU=,tag:RIsIsobpnql49/0WX7ymVQ==,type:str] +NODE_ENV=ENC[AES256_GCM,data:KwOcuP7/y4p1EA==,iv:T989Sim7c2uyqTH8uL6yeAPajoYJahzFemZcSyI/Sss=,tag:mkChCafe8w2VZy5a52tRhQ==,type:str] +PUBPUB_PRODUCTION=ENC[AES256_GCM,data:XjBuCw==,iv:nSFjSKPbf4Uu6hME/jDQxce3cYH1pJO8DJQMF1NyWKU=,tag:NmH5qyOtM96tX5EDIR1cTg==,type:str] +S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:/QG2ZvWm5+yRLQ9jk2WoiAjJMI4=,iv:8zSs4ZHAVpf/ZHkkugQqrpCQeF8aKUCOwEeE/Xoknlk=,tag:JIMyV0NLIZy26dtgrTK66w==,type:str] +S3_BACKUP_BUCKET=ENC[AES256_GCM,data:ahjpBwf2y41SpCM=,iv:fUODBbghyloP8U8PbKACtr55E7cm1UmgC7CbpTHs8l4=,tag:LfgUFA5rGG4zqw9JMb/UTQ==,type:str] +S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:ovFYPVj43nPuTmg/UtA1JDtyUxToGLuGFMZEVc/lbEmfeqI=,iv:rQ+oqx7YraJkJMBB39GNaHAQtI9jUAeehv0BdmWBUts=,tag:oyW7YFacdo9br6m27u5Awg==,type:str] +S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:3Bj6Gffv8F9xrpfh6WkEXmDIsCMSASObbHR20oP2tUZfw15q4ev1fw==,iv:kDmAFX4YXr9pqtRJoqtPlucXZGl5mWso4FpzuGo+ZhQ=,tag:Jdw9gj6IXNAdolEBSvhT3g==,type:str] +SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:PUaTTaw3pyiUbuCm0WDBoNKwt8rUv2gKKzX0sROPDCLfXxgwaCWTV6nZwjxyaryIpsfpL2x0zfvKH/AjnvLc0AFbBO9sU+cCrq3yuQFCuK2T4olrOitaZGbigHpiEtCPM/laCRS5vxGhbENynWM33mz+jHUEsKNFcQOyKzb5U6SibDS5Cs1WeFoPGObslsfR9FinmMpehf0bvHgneEur0kPw7iHdtpDIJf1/1+o9YcRqJwvaaUx+ZVXYVA==,iv:gd5qwJM6wMUOxwXcDZMPUPXbfSRRr29ZMEZX+SxStFo=,tag:/v6LTnbX71Rf6inniPCCbA==,type:str] +SENTRY_ORG=ENC[AES256_GCM,data:5mlX,iv:HYvoEOkVMOTw5I4/5n25G/4VJGp3cgU7TKYFpvbTsG8=,tag:6a9akdj+cQUhusG2gxi5bA==,type:str] +SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:qSw=,iv:i+u6W2yUFvEZhgJwWoMj6sQTE3InQrm781t7b3f10gI=,tag:hEwRJ6hP+66kskzxsf/sgQ==,type:str] +SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:wTLkeHcl5sRBZwS0cN76m7wzK08jMgaGbENvxqLOLDajwV6QcI1s86CGey3HHpef4iUX22ZKuFZ9Q8FyS4x/XwQi1cckvzjznqF45N6BXQ==,iv:yr8bcynfv7ASsta0iyB0I3ex2Yej+lcUE1XdmGkrD18=,tag:h5NFEQ0tWsfCra7v+wQXcQ==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:BUyE1bVMOJEJOtgHqkHU9hxxMcM4BN6se/ErfQvZjvTYkw==,iv:JaISCEPObi0iArR0dr9dqSg6AxIFJbFGGYRewntPFxo=,tag:Lr2xuIvVz9MIwVXvb01wNw==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:jDTmSuleP6u35yLS+u6R7Pm3jgOM1sKTBajDZ9QWZ5pBUN5LdI/X8Kly3vA=,iv:q7OLmKSfYT1ggmF5vL3QijvwoWBzLbsZ9AsZBXxDWhs=,tag:CtAOweypxrewqTLbxEIowg==,type:str] +SMTP_USER=ENC[AES256_GCM,data:pr660T3ADIaK06QaP3WHaD19nj0=,iv:fNEeFbvdPa+QYyXpGGJNkUCZmBvtuU/j2c0w1HLlTqQ=,tag:u83hapQheqPzh06iELenQg==,type:str] +ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:TnNAGrCWHBPFJn/bdvXBG+dcTkI=,iv:xvQtGH/6zaRL4LgtAnO4zloLBh8SxnHByasRo6PTuec=,tag:8Bg3x/oVwLEroJCY2aA+ww==,type:str] +ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:ujfa/HTCpDoosNoa6TxDF6UQM3E=,iv:EdC4fX9W8XdObL35eNRzcT2UgUNmRfH3NAeLgqytAUI=,tag:GErhJOFLqVhia2G3rdJ0PA==,type:str] +#ENC[AES256_GCM,data:tUzJpG36v1MCZVwtDivOa2dPI9E=,iv:XAaV6kSZUxcAj8W3R8tjiAu+ZIKyFaGiOReyBg1x4A8=,tag:TZQErGhvNehnHw8G76Y0VA==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:7FcCegV2iNhxPWuZcXLt33MpOjxKoGZQTzT3lQhjpEsn,iv:/OQgpmtPttmd3Zm9TFUi0U1xaQOp2Jh0hrjp1g8cHsA=,tag:S2aWuK0BSN4f5wupxd9Mig==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:rRPEdmmq8tyb,iv:K32MKM7A8mj7zJ1rnKzAYTVaAjl5gEB0gtrdMtCSYYU=,tag:pH7S/4tulQjantSnBr4SfQ==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:QaSFOmYu68bG0zAjSstcaZmh8jgH36eRKiu450gCFsJomt5+Z7egA8UNqRTvLf6i7FQ+gJvBDUvqSzEpZNuuXQ==,iv:JsTveBXK/CH/gD6YaL4IQ3vyJGujIXmRH4ydmrvj87Q=,tag:qJVn8dPnmgIEQTPpCMPchw==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:S3S2Gq8xnsvlfKnoaOEaZ7Akpn9cZQtazJGTay0k9Baq7uqZRSy+IWjoB2CiYLr/CAtESwcHweY2FcslltJrmg==,iv:6PGhhLcsORNTJH/xFzbZrAh5mx2UNR7bB85vhOYPEp0=,tag:sc0VggJh77D4GtMyKfmIIw==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:tmhgeJpCj3kX52riHjncNk7nVNBUw1ic9EzSOlyW9HEOr83t,iv:k5+OtNyPHhgBF0XXpaCVS8q5HoJhYw5QeV4q/Fgu0ss=,tag:kyUUke7FJ13UQsJSxG4zrA==,type:str] +APP_URL=ENC[AES256_GCM,data:qNnlTBK+bpluTRPCL3gHGgekA5FtUw==,iv:FMAbRxulpga3W+A/bmSZbrmtaY8Cgmqu4gJMEqSFlps=,tag:DnFx1Y9Rhyho80fTQof3qw==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyOW8xclhZZldKbnN6VGtR\nQjIvUFNrdXBRTTQ5RUtiNklsaUFOWkZwd0ZRCjNnbjFIZnZPeG1oT3c0Tm5CaG1O\nRXJPY1pSS1JET3BqaGprME4zTVhPVFUKLS0tIHcwemZWbFpTdlFuSHBBZFgvSHdV\nMkFFNkI0ZGFKVEZET3h1bW51OXJwRHMK0LpTnZaAak+N3h58yiSJMzY/Xw44iC6t\n5okiCdXSTB/fl8VJAtaNwqRS93dLwdzRAPO94KitKtYdeQlE2s4tHA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr -sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBEMGM0a2RRUEgyTmR2TUdl\nVzZwSFNVMFJhcTQyNW96alV4bGsybktaRGxRClQ2dS9xcFFRWi9aTEZCYW5PM3lo\nYzJxaElGdnZrZHRLVUlnWmdIbk1KQU0KLS0tIFgyRHVzOVZGaFAyL2theGtyNFlU\nU1lWTmRiM09pMVdSdkdybUhZMGpvU1UK5twbjHrtZwk3QTYv5JfgsAgrcHDytYQ+\nM1yk6QHt0loScJ9Vpatyw/StPCYS7Avpfk9p6LZOIQ5oRzYLAVmZVg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpWFZKYWg0bDJ6QmJMaXp0\nZ2tnbDA1aFFycmh5TDJMV0pzam5HTlR1LzNRCnZ0cEQ4ZGRNMm5NRFVJRWV1NGNo\nc3dOLzBVK1RRbWY5dS8wL240RFphRFUKLS0tIGIxMTV4VGdaOVRhWlBNOUlmbGln\nbFIzdUc0NEF0Ymc3N1RzakZBSDN2ckUKkAdNqWbWaIS5SK1Wd4nMq2eGTttfGd0i\nlgGytU6HINzF9yU7OAeK73Q6WWunHHy9DUYoD3dl8tedbxN7FlDxsA==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj -sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzZ2NnSU5IN0NMVXpGdGJ3\nVFYzd1p0alExOGtuM29EVW5zT0Fpendac0FJCkxZaWo1UlhjN1I1bGMxeWFKWWR2\nWUh2YzZVQWUzN3BkY0toeTZaRE5LUlkKLS0tIDdGdjdZdXNtWTlFV0NGRjVvZWo1\nejFRNjFCMm1uY01qbVJpUUVPbk1ZTTQKx2WXbhYsAHWNZQJhzlrMuBqTEoDKBprO\nmRKwWuJ/Eb7gVvXot3xhPAHws6ZFcidf9LUhaUCkPHz94hDZmzP9Tg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2SXJDTUJoS0NqWVZ6cnRB\nYk9YejhWclVwTkxSSGJSelJJeDNrYnB3eUhnCmprYnRlTElpajA3TEFCTlk5YTdQ\nZWdTTkpyUVUyVVZuNnQycmtGZmhaVGMKLS0tIElCeGcxb2NqdEhwSytsWGtPSVU2\nTFJxS0VsWFFBbm5reFBQRkZaOTVneUEKuMJjoNAQtQipFP9Kod0ejlO1SX9PpWB2\ncR7Q3VcIUf7t5u+pnpJ7e56lHHcaAfiOUQFsShvjjXBLwXTeFu3IJg==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk -sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUemtOQUFudVdzdHJZZHMz\nYXp1R3lBeElVZGdlUHF2QWhSYVVvTlkyYXdJCjlOUU51eXIwWU1aaG9UWFRkWHV0\nZDk2eUtpc0ZyVmEyUzFHZTFnczlIaU0KLS0tIGpwT1VRUWdRdmtpYnJXUU9MUDds\nbW8wZmc4YU1Sc1V2VDlkMGFNNUM5LzQKq0UU5uv9ypGc01/JO+kWGJu7SQNGDuL6\n1ldZXGG6h4hMYSFsA7blvnhcGWdJAlU2rhNN144hQjRNAj3JlewpUA==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJUzVqRGJUUllLbFJGSlRC\najM4L2lqclcyVWpYcElVbGlsTm9yaXE0a1hBCmFRcDRXV3Y5RHNvL1VtV1Y4UG5P\nUDdYV08zV1gwS3I3QU9aUlhyV3hITzQKLS0tIEpLZzBPV0V1VVJtRkVjd2lmdjFB\nSjdKWHJQakF1K3ZRZzFNanBkSFNhNkUKTq/DTismWQDjyf+L8fxq2VOiNUCLXAse\nfujiuMVcrYIKXP8yR2tAyylsdDzto/g4e5RzX93/GsMnlvWrPfPZJQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h -sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwZ1BzUEVzWTNYT3JZbHZ0\nQ2owYXZJeDk0LzUrbldnUGVWRjVScGZ3NjIwClVuVmRsUTRUMGZJQjlESlRVK3dT\nUjE3d1YxaVB6ZFBMZnk3dE5WNzRRMUkKLS0tIFFKUStlSmdRWVdEWEhCZkZSbFg4\ndGVqUlo0OEFSbmVaRFBQWENIcWdKNmcK898+HdJV0lVa0/Jrf06XKZtHuPHQQwxb\nu6aDPQ/V6N886gpFtJyzB9WaeCMyFKRWRD48tTPWISZVmebnL7XCGw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpRXdaTFRLa2F3Wkg2T25o\na3V2aU4zZ0RRYVFoYjI2OCtTWURNcWtYTEhNCkNIcW9Zcm9GR2JOUTZ4UlBUSXV2\nenJNVExldnFBeEZZbzMrRVZuaWRFQ1UKLS0tIEJUMS9QbkRMazJpRE40bjBJbDlP\nZjk3SkhTQ1J5RFk0WDJKMS9tUENrRVkK1d9PJZF74p9GdFrmSLazCXBjgXRwNZ9G\n0QDbPasLQze8o9s55irb8vTAI+yQsdJnupHR8qDxj0AAsSRyl6Nt9w==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx -sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoNHlCUlpRSXA3UWoyK0sv\nalUrK0tpakVxakowTzZ1Y2VHTmZmbFJKS1YwCnB3bmN1ZEQ4ZzVpdXlkUmNWNkdu\nVEwrelhHYkgxZ2xLTm0veUs5Mm1tTEUKLS0tIHpjZElvMCtDVUpFTFA0L1hyeFdH\nWlR1RnFpT05ZV1ZwWEx4ejExRVpCdG8K4KZGgNsB+QmZjK9C7eUJuJlPXHq08v5V\nxD0IyEasHXorMqTUDHvwa18AU8n4BaT+MCTRoYTN2h//G47vMx+ruQ==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjK0o4TEtERVNlNCsrTG91\nSkg4WktLa0xMc1hUbVBtTS84ZGhZQ3NRWFdFCnlQRmVjdklya3lEamdESjQ4VWpM\neG9kaGpyVVJadjY5cUwwc1NpUi9lSU0KLS0tIFRERTlEM3FQYlM0WjhsZ2dNekhm\nRlBPb2p6MWV0b2dudmNxUVgvcldQNFEKvogefgMFzKyL6VjrbNUWxTMDKl0+CeAR\nbf+3TNzTxurazzgqNRgUheXZ9mv4YKo5Yu1MNgI6Rm0JJwfZryogsQ==\n-----END AGE ENCRYPTED FILE-----\n sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy -sops_lastmodified=2026-04-23T17:35:46Z -sops_mac=ENC[AES256_GCM,data:LMDQ4dAjsRljCyGxwY+Za6piTZRBo8+p3/HuVybwkGpm0v84VV4ksWcjJWnQ9Ii0hKzRO72k5byLBEJNWeUC/+Mqz/yUYn5lMDy7I+pJV0fy68qmlkJxJWM1I7pna/0BJWG4INjA8HT5IFyov7vGzbOFhG+3GvlUk5pvLu8N2YA=,iv:jooWNJe6n6qiHUNRn4P3FuhEvuPhUvqVTujggXp13XU=,tag:+Tj00rUBm/UDvcvFcroiVg==,type:str] +sops_lastmodified=2026-05-20T17:17:46Z +sops_mac=ENC[AES256_GCM,data:aysQdMNX85UzCFkaOdAJWm6mV8XSpCA2f86VMGLcuMa0AoCsAf9DClVQh78OI/Ut9mDOPqKr7/UBSUJSVveP5x08vCvXaRTyX+BbSobD1Qfbbx7d2Z66XC7FyEv7dVz+P5LBCSQWBpTGASP9DqWlmen6uTpYJB8oqQc6Bw+84tM=,iv:+RR0Kk8gVnwvtqKh9y/6O92Ymmc2ksvJZnzyFaXaFvQ=,tag:6B2VEwUw/c5cMEaF0TQ4ag==,type:str] sops_unencrypted_suffix=_unencrypted sops_version=3.11.0 diff --git a/infra/.env.local.enc b/infra/.env.local.enc new file mode 100644 index 000000000..7c15703ed --- /dev/null +++ b/infra/.env.local.enc @@ -0,0 +1,61 @@ +AES_ENCRYPTION_KEY=ENC[AES256_GCM,data:vIrqh66O0vIjl5yZufSBw4N0iFFa3aa8Ge4/7fsQaFu7FIaNJngDW9rY0Kssx3MeMTeTSRsQUYDKgEeuguUwWQ==,iv:U1HAoyGkJvJzsxWCvDZC+oasHj85wBFLO/KVQ/L3sEg=,tag:qkbB7uUlIYBLPtpq/oQ5wQ==,type:str] +ALTCHA_HMAC_KEY=ENC[AES256_GCM,data:4OfsN3DoUJW/S/IjG2u1CD6pVVCuimRUC1RLeWV3I+x2XLFVNVFMabRpSdWYi7A0D7Hemk6LDph3NXwrSu/yzg==,iv:ig3JqrQJ1k8QM8TEttythWE2eAbD7UAfSz+D7EWgePE=,tag:vHpOGbKI+a9Uyw8BAoboJg==,type:str] +AWS_ACCESS_KEY_ID=ENC[AES256_GCM,data:go7ZUa8BGf7XHwS9hg3ASw4bRt4=,iv:xY1SlZedf3zJPI5APuWkVAlElVVKY2AoLh19GjNSq78=,tag:mOakCQGa2m51we4/HId5VQ==,type:str] +AWS_BACKUP_ACCESS_KEY_ID=ENC[AES256_GCM,data:ux+kgdwZ89ob5NrOjX8gg6IFWEk=,iv:zEsH0RZF1oWkb8SAct1T6lLp9B0iPDs3IimN4h1wp0w=,tag:V/abHX/etKgXkq32VKbHWg==,type:str] +AWS_BACKUP_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:YTTl7jZpgj4zrkKBFa9TFQmUs2s5EBtUPrVmSJwhCYDAPEgG5cMvaQ==,iv:k2FkkDGsRO4ZBaIi8jC1KWHk9EWv1KjZ7i1INOXTmIw=,tag:1yeyZq+tmarD9Rh1ygwPrg==,type:str] +AWS_SECRET_ACCESS_KEY=ENC[AES256_GCM,data:vQF57pHMG3xzLmyMmH0SSSaN/GHHB6e1Ys3GW6FEkK6pKMffwQVvOA==,iv:MIHd9fQG3jP3J67yGscANL249U/ZsGRoJ8zKvH1CXp0=,tag:LibPhBr9liC75tjinj1IbQ==,type:str] +BACKUPS_SECRET=ENC[AES256_GCM,data:zhX/bJBMNrEWGMvu9Cc4wRG4OIVjFU22kl1t0ryU0e5bmRx6D4u1hy9lAgA=,iv:LAyXrFZqxyYWKaz7bxTnF57V5TPFRqZTY10I+xyrEos=,tag:pgePnfjo+XxOHoVxYzlsdg==,type:str] +CLOUDFLARE_ANALYTICS_API_TOKEN=ENC[AES256_GCM,data:DyW7eeMeX1wJ/1+0FzeN2w/Wzg1kKAZ2JBCbcSwIKdmeSYZGTHCJ7tZRDOPqoz/DRabkDAo=,iv:O39bayrEyYSIiwWKdDo4R1gz2/WC+99QMgySV/CnM2g=,tag:q+yPo2RGjQDzQ6zvwkIl2w==,type:str] +CLOUDFLARE_CUSTOM_HOSTNAME_API_TOKEN=ENC[AES256_GCM,data:cLR9rTfrs6JRgRLBz96yMYt64YB0ASDU+4QissBpdHwrBuS8LqWkUDOl97x+IUL8yh/XHQI=,iv:SemyA3qERi1lQMrk3S5zMvWJn+R+zX/ELfRjlulHLhA=,tag:6Qz+l73w3KSVqFSDnWFtaQ==,type:str] +CLOUDFLARE_ZONE_TAG=ENC[AES256_GCM,data:azYYUIr+jjnc08bnQQwzIVc4HgxmVwG10rLEdMhJJro=,iv:s+de5cZF4/N1QrSK3bYXvr8Wo4AQka08cMzK5W+D8vE=,tag:TdlZqgBc+Y7hdabmK1g7uw==,type:str] +CONTENT_SEARCH_TERMS=ENC[AES256_GCM,data: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,iv:VOce1t4w1aH/6j78p8eOeN9ijamYZ0pgFWEkvkQBHz4=,tag:Kvc7d6lV55muFVebnbOaQg==,type:str] +DATABASE_URL=ENC[AES256_GCM,data:M3oNL5nTiB1vUt4lxnHVkuekQVgYcORiVZI7uHPepq2cIZFKjuf2uBfHfCA=,iv:R8Oy6X06lY0sP91yZMmS2sd6MyUzIX+5P9t0LZ22bIY=,tag:qzxOtwy4Kdab8jcWf+XA5g==,type:str] +DATACITE_DEPOSIT_URL=ENC[AES256_GCM,data:unPKZvOkD9NW5i/qu/+CJOeDw3A7/nVoV3T7Qw++059Fiw==,iv:cO9mI1M3yZ+6WD/898AU4JP2w9JAiNf6Dfuku57xygA=,tag:Osnh7hZ5O5/Vhn3vw7FKSw==,type:str] +DOI_LOGIN_ID=ENC[AES256_GCM,data:692fnKaD,iv:YoJ43peJggSsAhXtInv3AEuHX5uCxqUR2AM0NYNu9Cc=,tag:MJD9KJPtr7MDO0IP5QuoCw==,type:str] +DOI_LOGIN_PASSWORD=ENC[AES256_GCM,data:/zZsrJALUEVltMS/XK5n1gwn4KQ=,iv:lpRBEBlQvvg7lGuddPmqgqMCVgHaacQK3ToNwnlLzOw=,tag:7Ra1Fi3Tplq+AbC3PusNKw==,type:str] +DOI_SUBMISSION_URL=ENC[AES256_GCM,data:afGWmXnlK2v3ovsNWC6LXyyTN2SZkGxyRtmIrCY+PhWdmnq/kgeehGc=,iv:H1bZ4D3DvEWrFPNtYQjD9/ES4qd0UxD+VJ5pJkqm9iM=,tag:0+IB/7bifExa0Y0M9aD5Aw==,type:str] +FASTLY_PURGE_TOKEN=ENC[AES256_GCM,data:+o4L7IcjDsJFmSvU7TQX5hFoDHXuviZi+yiLoOw76sE=,iv:wJTcoR4hs+IS7DmsfFsPy1FV2wNZHugpbkPQ0PlA8MQ=,tag:/gpfMK/9vaDCq31uD5YKxw==,type:str] +FASTLY_SERVICE_ID=ENC[AES256_GCM,data:N6oV1j+eV5/hjTO9G5Cic7kWGZJ1cw==,iv:ZLYJpjSrx5r9hYgf3g6OXAzuPKxwoaIrmWwnvkGSQcs=,tag:jrI6eQuEKniYerPGPYn9Rg==,type:str] +FIREBASE_SERVICE_ACCOUNT_BASE64=ENC[AES256_GCM,data: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,iv:Us+CnNTBLETpGwtnPs/2KROsUGbPpknnu2kb8xtlMNk=,tag:SfX/hQIc8lSgvVZP3Qzsmw==,type:str] +IS_DUQDUQ=ENC[AES256_GCM,data:U1kWzw==,iv:HX2tO/dJrRHNVTG7WEhDzSJ1TgdRAijUHpDfUEAZYHY=,tag:+O5bbrsOODLwaapINpvJwg==,type:str] +JWT_SIGNING_SECRET=ENC[AES256_GCM,data:rzMTJ6WaNqnTKjbgE8ympwqq54uRSGhAB6XfI9kPVoCqZTgV1dROMSdgWKzPg8sb8kiAMEZW3hZXpfoMRvwi62tIBIHb2wRI5hkrkGGWEXI26pQaOpWQsHDQ/9Z6wBQnhX0TZ/vR7c2/9t1X89K86y25nE3WuI4ZJNrMD619xw4wcSkk6DycDV/KwfMitW5MmsP+YMBHtawMQcPvo6rk6N1vvroyry6pGlfXcUSaNrcYm078SnZTRP1kp2kXhybCbGvZo/aCBbvujQcBtGOd6GoZu5KbNSfhmWmEK6ox8C5uUy9QAUqfb+4k0+6WWtivN5ZXaGZNkQqUI9kyxwtq8V/8aGZ0BCsWvUocOHvUdufL/VoJhpEtgSB3TRv3OqEVTtmm7c3587ML55KP86E0kcjpiVLymVXVfDnaAyq5uhKLmea4l8j8mkpzkVWltTji3HWVWnhLEZDMRQa4xobfHyalOQOA0Hi6UW2i9PLfiYuHNO/86qi7+boChT2eAlgX9L68vC1sNXc4mTtcDtnuuLNKzb7UrJDlvG07VRidXvXmBoKjwgoOYfd/8p2PXhOklk92OhTR1ElG47y6/a69O6/YhSq0W3IoUzAuvtO6TN6BtjV7ONrOM4/HPN76nEKcaxh/DFnoacn6Ocx7Es4BoZ++L5oFSB+swTcA13BLPIXF95+D2kP7icvjOjKxazxJkgV3rsfa3+Vmk3clnynnPgU5puzgZ/mkrvz+elbpfe83DYFfYdMm28vt38uOyjBU0prytY1rMPrZRhRW8wOYj18FgKMNVOQqu1z8HOzqcJN3E151wpgoM5VhTYnJ1CUjpOcz46g+4NfZD0mY6kRuRJHh1ylojom4IOixcomB+l4GVB/jqoAyp420aclnkqc6pSue2qE29RcjiR0Gpu7VpGKd1aAKbGrfUCas7pxv8/WmPJ5oT8JoAn90KSSauAJ1ld7yJY6sd9v05EPJ4q/pY4ql5t0sK5LPHdhbbvd99r/VY9EmJ+/rDOePvyiTm3K7L4xsCwKi3XilmDDnJP5VsY/53gqlCziK4MqHaGt5bzhutqQZjt4Cmo/RBetydKQvkLHkEDiLaxwajb6JFLH7FNt412aKDepbItF901jYyN5QEDmaXQC4gywLp1BKQIcL+dfHi1uN7XZVaVSYY5F8SZ45a6DVTqas7fQpopDgSl4EHkbRVFFp0qh0RATlI0RT2OfWfxA6DSlrCqu9n8T31yZU90V8y2vqDU0A2WuizIueeXg2aorprU70ogRLaDG114J9uMJ7xyEpnkb+kz3mJGaqZfntUnNcEZ8kLeiA8qKk++9HkqXdZV9W/9QLSfxzqAFYpaTVipmmHTvFucsRzA==,iv:Oe95yrYzqKonTZC9YO5k7QXBmx3Sa4GEpyiFJzovgHw=,tag:UATqruNo2htBSIxywSIVAw==,type:str] +MAILCHIMP_API_KEY=ENC[AES256_GCM,data:6NmLHYMAm31e/Oa2EIxG3s09fKrTT39nverHS0ZrbHd0ijLH,iv:AiikD3j+u1oaOty7J8fwYsWW5H/6K1+03XEgKqz4KcI=,tag:0BJWnxeCdHG0Lmrha0eTUw==,type:str] +MAILGUN_API_KEY=ENC[AES256_GCM,data:2sN86sd16WtohlMDkjQ0yKuvGgr4j+xIuiY8ULyTA4Jcs/p/,iv:hlTl+AR6O9xtnnykGLTAuI+q3AFMr8k6aJ3Qa2FtlcM=,tag:yMsW1h//6ZlKiifXhK/Y3g==,type:str] +NODE_ENV=ENC[AES256_GCM,data:W2zO+aQ3AQedyw==,iv:V15cRHUEIHVy0Y/nVJutv05OKqjwGD9VmAG6Mt6+xe8=,tag:jPT6EgvFks3NGtKQZq8HlA==,type:str] +S3_BACKUP_ACCESS_KEY=ENC[AES256_GCM,data:IMO8So3hBgfBlqCV1m6FZvmo3uY=,iv:14Bq4IQE7I6LP4XeZdu4Nq8MKkBm7/uaL47gFqIVQ5s=,tag:V7mJq7liFavpyetLuokMjA==,type:str] +S3_BACKUP_BUCKET=ENC[AES256_GCM,data:qW5FvcAneutiqXg=,iv:Czx/nxOW+usXbKREDD3fXACZ4agR5CfOlvRca4hqai0=,tag:PNF6sbkagOETqfbZFQgC4g==,type:str] +S3_BACKUP_ENDPOINT=ENC[AES256_GCM,data:qbqH8J8cJghBiNG+Rrb5CB0ltCxW1kp2I3MARAZB9Gt0bbo=,iv:aPsyr2nrMneu1ng9dEs/AlRktyXaj36SKIIGzTE+KHM=,tag:OpiH7Iparyn5X16/W33AdA==,type:str] +S3_BACKUP_SECRET_KEY=ENC[AES256_GCM,data:S3gFLILtjNIW06pT8CZOJI9VRBmNQn7FiHp/qPUSRstOZmoMRMb9sg==,iv:eJJ4g/afVUiJgGgFl31JvOs4Aw7iCD0dqyD59V4M6wA=,tag:keLFkQoMt5I6xePQDcElbQ==,type:str] +SENTRY_AUTH_TOKEN=ENC[AES256_GCM,data:k+7bpMGRHSjjfkn4um4nj7h4Zz7ha6RZpFj2+tI8HTyoJRsmMWYwYoE7JM1tnrrkLJPX5v8Z8Gx0IyYpYlpdtQohGGzEqmV9FG0weLI+WzCOMMi4j4fHzQRLC8W/QCJYvUhY1FcSw0qTtJXkGqSwrJKMs2XSkwN8SgG6PQGhnoqQKFBG88SQWhVszkPc/X+qVslGykcMPURnUihTyIFf54UW5kLrCVpmZ3Wk/gmuKQ9YNQNaaV+EMWR5CQ==,iv:Crd3/4Ish05R3nKDd0SxW6u26rVr9SHHTGfsIS0Kd7E=,tag:HGSAd2KTUtjSyq1lBjsLyQ==,type:str] +SENTRY_ORG=ENC[AES256_GCM,data:zAGf,iv:sMPG/+O2lap1vfx+jIHRvPQ3/bGdCywABnZfvJpBAPI=,tag:DyLuuy2svsQlNz9yG7Hhcg==,type:str] +SEQUELIZE_MAX_CONNECTIONS=ENC[AES256_GCM,data:C5o=,iv:te0B+QUWnIOwnBhKq9lRpUhkzpwqm9ykOm5VKaXqEKg=,tag:6IzPC6t+VxVKt8f6vl0dig==,type:str] +SLACK_WEBHOOK_URL=ENC[AES256_GCM,data:PaZb6Tnx/EuLix8o1NlLXSdpXeHoRHTxaz70Nkw3vgomscx9dT45UOUqMI0Mz8sRcW29YMqr4G1LbLO10MOwlkgJpECuKsdvbG1B/uTa3Q==,iv:Qak+46OGRxwtk7br6EAuRHn3c2FxBiODugoT37htNjU=,tag:Jsfs+rGRWgA903N3EguHaw==,type:str] +SMTP_HOST=ENC[AES256_GCM,data:/HGDBiGkHHBfuS8zmvfZqq3bFnZ1z/xiag9vlPjT2KrwCw==,iv:iwQCsrrs69jwQW0RoHXr3GT7WSU7rGlUSe4b+MLY6uQ=,tag:BTy3FvSKRhdy54Bwk4Gypw==,type:str] +SMTP_PASS=ENC[AES256_GCM,data:PzXVffQ4YzY13mI4DZTpdaTivRKKXPoNCOKmrfUVeYwhXuWjOeRPr6xyOZ4=,iv:L3THYQEciHtKVSLeyzcUHoDjEGvUEeXQutLnNkbIxTU=,tag:lK4vvOxLg/Z1yUBJnjWrdQ==,type:str] +SMTP_USER=ENC[AES256_GCM,data:b5mxkDDZn8cHzzxqg3L9yiou4q0=,iv:e4wWIYD6HpV38wAjiEr5OtD49xcP9+hAhT9fREHJ/bY=,tag:FD62PoZxRxk/UIiP6PRFtA==,type:str] +ZOTERO_CLIENT_KEY=ENC[AES256_GCM,data:GQ8X1rsoxzkizLXvo0dlr6AEHY0=,iv:a8fIPhKBBqFiWmYqzzK4P3nsQyghUAVjTziZV8lEfqw=,tag:O5Mu5eKl61vehsKtOERvMg==,type:str] +ZOTERO_CLIENT_SECRET=ENC[AES256_GCM,data:tP27hLtMVBtvNkp7OZwnFgnCMgk=,iv:dCfCJ14DzqvGIun3w+WkBTw4ZtXLffxdSH2+w/gkpVY=,tag:0tdEyObg+GeW7JKyGZrgGA==,type:str] +#ENC[AES256_GCM,data:qkhNcd2cpoR7xna7lSMHVYyRkwU=,iv:xuTXXggrRr1SIxoL4g8g7aHNq3Zn/eMZDPGzpoWJSPg=,tag:YrHS3JWvk6AMAaKP7T6d+g==,type:comment] +OIDC_ISSUER_URL=ENC[AES256_GCM,data:Y6ZZZg/KO46M4xN4Itrk+/RKVqch,iv:QzhlB7rzUYQ0qvDQWfpO4g3cAFr1CZv/kVWh8hkTZEo=,tag:jrqQwc6qnAFicPpCocAoDQ==,type:str] +OIDC_ISSUER_INTERNAL_URL=ENC[AES256_GCM,data:GLZtsu+CeDo3bLg+qpnPSr8Vfsxt8Q7SMkkK0HBOgR0=,iv:nLZwEdRR+Jbleut3ydLcK6dkeeXGQr19VKeOWCCMRU4=,tag:n7U5+a1PqTaB/821Tl2HHQ==,type:str] +OIDC_CLIENT_ID=ENC[AES256_GCM,data:u/QSy8R47Bkh,iv:M5UDl6iM57cWfNfbS7c77o13il4PoKmEBhughUA4b/o=,tag:4ejq5eBXuPv4pjeZslgejQ==,type:str] +OIDC_CLIENT_SECRET=ENC[AES256_GCM,data:1owOMDVHC3wU5lISzIUvKwyHvyHkLKg36MRmctWRI78iFK7wI6tKebaQmIOwwlLdvjoVP0wk6nr8vFZHYexIDg==,iv:aN4v5vsdkH8qA9YwsQQXvqDXszqBeUkAv9OQixcqqUA=,tag:wPcjLdEeEivqaX4RgbMM+A==,type:str] +AUTH_INTERNAL_API_KEY=ENC[AES256_GCM,data:VcT4eqKsqoLmYWqJU+YCQxNYjangyvFfslBePwROz8Y=,iv:GyJxeKs6ZO3ZXggu3m9Pc6GnBlXjoCU33cBb3SPrcc4=,tag:6IziFPglbwIPXmURwTeyXQ==,type:str] +OIDC_ACCOUNT_URL=ENC[AES256_GCM,data:AfoxH2PbtJMT1YiWgVhu+zH8PeGn,iv:+n5wB8teZAQ1abEKZOpJL2/yQCW22GHglYBWMZDKju0=,tag:+EgWhvD22GWPQrE19VMCOQ==,type:str] +APP_URL=ENC[AES256_GCM,data:nzoHDC1p4LOJiT162qcS6ejrj2Zm,iv:206a8lmEpzWDQhI+5ASqwWg/a9KAs3MnWiTwjQ3YLCA=,tag:H3i1tj0xeF6rAxtR2sIVtQ==,type:str] +sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxWXZQalUyb2JaRFZzY2lW\nMTZzN2JxdWdZa3Z6NE9mNjZhVFBXaEdIUjJzClFlTjF4bDFudHIvMHdWQVVyWDNF\nY3Zvc29JUHpRaDluazlDTVROeTBiS0EKLS0tIGRLRFpkVDBJYmhUdkhpSFBSSWdC\naGFHNk1yZUxhRUtGZkt6UFYrUGc2Nk0K8MU6bWI+2I1BFuxrgJSA0yuHzIES0XaF\nSpGQXVQ9pSuMzXVc7FuiEBEt6iQN/rfMhgeNSlDH3/6P/s7btfakpg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_0__map_recipient=age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr +sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBVkFkbERML3JjK3VQcVdK\nT3JUMUdWbG1ldm5NTFRISjVSR1RlUEpMUFZvClR5K1BuREMxRmZoVVBPYTRSbm1R\nYmphWTlXdk1FMER5TkdnMEhiR3Jya00KLS0tIDhJMW1HcDBBcm94M3M5VWFVOVNs\nNHY0dFhZT1oxckNoQzR1amx4QndWYVUKgAaoyraxVST7kBjDgtJOvZfaHmP9X785\noOic81S8FsZjmDjE5d6VoxsHp62pjDkUGzyqiSGIjAjKczXiLo7ewg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_1__map_recipient=age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj +sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSdDdnM1gwR29XSVZoOGF3\nd3Y1RUhpdzllWTlZUXNDTUsyVm9HWWE3MWxJCnhTKzNJY2tKWDdyWXhzR2Mzc3c5\nVURyUE1hZjRranU3clhkVUk0ZVJhbHcKLS0tIGZXMzArS01FYTRRSU9NVmVXU0ND\nZlh3R1JaeXVQcG5ZQ0Y0SUp5NGJrVXMKe/Oo71jhPQNm3owxPHz9Xec9JbqiEjnV\nay9cV5nMINhEPfK3e/R94KbzJbKzbmPDidQPBuMunJ7b91GetspTKw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_2__map_recipient=age1slx6e48k7fre0ddyu7dtm2wwcqaywn5ke2mkngym3rpazxwvvuyq9qjknk +sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUVG8wZmgvcDBHNUhOUVRl\nVzRha1JRNjhxVkFCclE3YnBPT1liSmlPcHpjCk1ySDNZcEtUVEhBQ2RJNEZWaGRF\nK2VIS3UzWG82cEhJV0w2eit3b3RvVmMKLS0tIDFMUFZZZzlWZ2FrWVBlRE9ucUdI\nczB4RXliZ254ZHdPbTdlMklySnE1TU0KQKS4xxMWocaTI/7mJkoKLzxaBRq9pTKt\nzwnN4ywFYcyWuNa/qs/ZO3H3uUTZC4DAHnKz5thcTUfTrT7tc9Y8lw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_3__map_recipient=age1vfhyk6wmt993dezz5wjf6n3ynkd6xptv2dr0qdl6kmttev9lh5dsfjfs3h +sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWL211K3h3S1NPZm1nb1BG\nRWYza2R3OWRLMGFRNmtMRFRYSjdlUVZOc3lnCll4bGxpNVN2UFZ4ZEJnbUpxSW9D\nNFU5UTZhZVk3UlVSemVvbHZVVW16TU0KLS0tIGtnTldpM0VzcmVnMk9jOEV2c1lY\naHNOR0RYTTBNR2ozRmFod2RrWS9JdVEKgjsrRHjM/07zkVOHeTrpE49/32LOYSAQ\nw4YO0oD9G+Riv0hB+bR6K/XmvJDlfu/QOkW1PgkimIDQ7ZEnl1iiPg==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_4__map_recipient=age1jwuvzyghfer7rx3qtqa4vs0gxyff0sg6pqgmqvp5zlhnpmvrkdlsdha4kx +sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsVkxWRnlxRjVqZ05kSDIv\nMlFSelhjMVJRVHpWeDlKQzQrSGxWenJmdW1rCjJiUWJZTW1yZkVUb3M1aHYwaUVI\nK0NmSGwrQ1dodzFWcXozaUFFWURuWEkKLS0tIFdteWYzQ284aWY3M2lSaDZSWVBG\ncWtxY3I5VkMwYmxYT3h1dm1yYXQyakkKW8obLgS85L7S3sSz4SNmBab34n2HPpMP\nUfwSIEBK50BwZxNikL/exYQq8N537xX/CbLR/9Z5kXjSg+aciI0cuw==\n-----END AGE ENCRYPTED FILE-----\n +sops_age__list_5__map_recipient=age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy +sops_lastmodified=2026-05-20T17:17:29Z +sops_mac=ENC[AES256_GCM,data:uCaCv8548cz57JodHdPc7UUqGpkELBQIoaJpGYivXnLFWz42C4xHBsWB/jJ5cEdSz5JrLxIJdslIpZt+hhAe7PKry+YInIficoVfU9MIzo3FQMJxkrED9wgpNmn3NWWa/S0U00z/qp6KNVmk2J7Bqt3JB6MU/y50ApVyJ7rwtvY=,iv:W0mAYXsBM4M40mTmFrz6kRjU7DE9gBaMA7TCx3JskFQ=,tag:jM1+R5/N6ZTjR7xil3hcSQ==,type:str] +sops_unencrypted_suffix=_unencrypted +sops_version=3.11.0 diff --git a/infra/.sops.yaml b/infra/.sops.yaml index 86ab8b2af..a03f7aa73 100644 --- a/infra/.sops.yaml +++ b/infra/.sops.yaml @@ -1,5 +1,5 @@ creation_rules: - - path_regex: \.env(\.dev)?(\.enc)?$ + - path_regex: \.env(\.dev|\.local)?(\.enc)?$ age: - age1wravpjmed26772xfjhawmnsnc4933htapg6y5xseqml0jdv8z9hqemzhcr - age1vhftscteyrwphx0jpp0yl60xxrjs77jq05mkzv88js7ckc926vcqepp2cj diff --git a/infra/docker-compose.dev.yml b/infra/docker-compose.dev.yml index 6d069a00c..c3511a261 100644 --- a/infra/docker-compose.dev.yml +++ b/infra/docker-compose.dev.yml @@ -7,7 +7,7 @@ services: dockerfile: Dockerfile target: dev env_file: - - .env.dev + - .env.local # healthcheck: # test: # - CMD-SHELL @@ -38,7 +38,7 @@ services: dockerfile: Dockerfile target: dev env_file: - - .env.dev + - .env.local environment: NODE_ENV: development CLOUDAMQP_URL: amqp://appuser:apppassword@rabbitmq:5672/appvhost diff --git a/package.json b/package.json index 508b3afbe..363c55ff0 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,10 @@ "continue": "concurrently --kill-others \"pnpm run api-dev\" \"pnpm run build-dev\"", "secrets:encrypt": "bash scripts/confirm-encrypt.sh && cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.enc .env", "secrets:encrypt:dev": "bash scripts/confirm-encrypt.sh dev && cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.dev.enc .env.dev", + "secrets:encrypt:local": "bash scripts/confirm-encrypt.sh local && cd infra && sops -e --input-type dotenv --output-type dotenv --output .env.local.enc .env.local", "secrets:decrypt": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env .env.enc", "secrets:decrypt:dev": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env.dev .env.dev.enc", + "secrets:decrypt:local": "cd infra && sops -d --input-type dotenv --output-type dotenv --output .env.local .env.local.enc", "start": "pnpm run build-dev-once && pnpm run continue", "storybook": "start-storybook -p 9001 -c .storybook -s ./", "test": "pnpm run lint && pnpm run test-no-lint", diff --git a/scripts/confirm-encrypt.sh b/scripts/confirm-encrypt.sh index c347f5813..689669209 100755 --- a/scripts/confirm-encrypt.sh +++ b/scripts/confirm-encrypt.sh @@ -9,6 +9,10 @@ if [ "${1:-}" = "dev" ]; then ENC_FILE="infra/.env.dev.enc" PLAIN_FILE="infra/.env.dev" LABEL="dev" +elif [ "${1:-}" = "local" ]; then + ENC_FILE="infra/.env.local.enc" + PLAIN_FILE="infra/.env.local" + LABEL="local" else ENC_FILE="infra/.env.enc" PLAIN_FILE="infra/.env" diff --git a/server/community/model.ts b/server/community/model.ts index ac2d30609..83e562e97 100644 --- a/server/community/model.ts +++ b/server/community/model.ts @@ -18,6 +18,7 @@ import { DefaultScope, ForeignKey, HasMany, + Index, Is, IsLowercase, Length, @@ -241,6 +242,11 @@ export class Community extends Model< @Column(DataType.UUID) declare templateId: string | null; + /** KF Auth organization that owns this community (for billing/ownership) */ + @Index + @Column(DataType.TEXT) + declare kfOrgId: string | null; + @BelongsTo(() => CommunityTemplate, { as: 'template', foreignKey: 'templateId', diff --git a/server/community/queries.ts b/server/community/queries.ts index b1ec5c8f6..2f7d3fac0 100644 --- a/server/community/queries.ts +++ b/server/community/queries.ts @@ -88,6 +88,7 @@ export const createCommunity = async ( accentColorDark: inputValues.accentColorDark ?? '#000000', navigation: [{ type: 'page', id: homePageId }], hideCreatePubButton: true, + ...(inputValues.kfOrgId ? { kfOrgId: inputValues.kfOrgId } : {}), }, { actorId: userData.id }, ); diff --git a/server/kf/api.ts b/server/kf/api.ts new file mode 100644 index 000000000..fa6ed2a4b --- /dev/null +++ b/server/kf/api.ts @@ -0,0 +1,1187 @@ +/** + * KF Auth integration routes for PubPub. + * + * OIDC login/callback: +docker service logs auth_auth --tail 50 2>&1 | grep -i "error\|invalid\|authorize\|token" * GET /auth/login — redirect to KF Auth + * GET /auth/callback — handle OIDC callback, create session + * GET /auth/session-set — establish session on custom domains (via encrypted token) + * POST /auth/logout — clear session + redirect to KF Auth logout + * + * Internal service-to-service endpoints (AUTH_INTERNAL_API_KEY): + * POST /api/kf/profile-sync — receive profile updates from KF Auth + * GET /api/kf/branding — return community branding for login page + * GET /api/kf/summary — return community list for a KF org + * GET /api/kf/billing/usage — return usage stats for billing (placeholder) + * + * Session-authenticated endpoints: + * GET /api/kf/my-orgs — return current user's KF Account memberships + * POST /api/kf/transfer-community — transfer community ownership to a different KF Account + */ + +import { timingSafeEqual } from 'crypto'; +import { Router } from 'express'; +import { Op } from 'sequelize'; +import { promisify } from 'util'; + +import { Collection, Community, Member, Pub, PubAttribution, Release, User } from 'server/models'; +import { sequelize } from 'server/sequelize'; +import { getHashedUserId } from 'utils/caching/getHashedUserId'; +import { ensureUserIsCommunityAdmin } from 'utils/ensureUserIsCommunityAdmin'; +import { isDevelopment, isDuqDuq, isProd } from 'utils/environment'; +import { slugifyString } from 'utils/strings'; + +import { + buildAuthorizeUrl, + decryptPayload, + encryptPayload, + exchangeCode, + fetchUserInfo, + fetchUserOrgs, + generateCodeVerifier, + OIDC_ISSUER_URL, +} from './auth'; + +// ── Helpers ────────────────────────────────────────────────────────── + +const AUTH_INTERNAL_API_KEY = process.env.AUTH_INTERNAL_API_KEY; + +function requireInternalKey(req: any, res: any, next: () => void): void { + if (!AUTH_INTERNAL_API_KEY) { + res.status(500).json({ error: 'AUTH_INTERNAL_API_KEY not configured' }); + return; + } + const auth = req.headers.authorization; + const expected = `Bearer ${AUTH_INTERNAL_API_KEY}`; + // Use timing-safe comparison to prevent timing attacks on the API key + if ( + !auth || + auth.length !== expected.length || + !timingSafeEqual(Buffer.from(auth), Buffer.from(expected)) + ) { + res.status(401).json({ error: 'Unauthorized' }); + return; + } + next(); +} + +/** + * Derive the community hostname the user came from. + * Needed because the OIDC callback always hits the main domain. + * Note: PubPub's hostname middleware rewrites duqduq.org → pubpub.org + * for community resolution, so we reverse that here. + */ +function getCommunityHost(req: any): string { + const host: string = req.headers.communityhostname || req.hostname; + if (isDuqDuq() && host.includes('pubpub.org')) { + return host.replace('pubpub.org', 'duqduq.org'); + } + return host; +} + +/** + * Returns true if the host is a platform subdomain (*.pubpub.org or *.duqduq.org) + * where the shared session cookie works across subdomains. + */ +function isPlatformSubdomain(host: string): boolean { + return host.endsWith('.pubpub.org') || host.endsWith('.duqduq.org'); +} + +// ── Router ─────────────────────────────────────────────────────────── + +export const router = Router(); + +// ─── OIDC login ────────────────────────────────────────────────────── + +router.get('/auth/login', async (req: any, res: any) => { + const communityHost = getCommunityHost(req); + const rawReturn = req.query.return_to || '/'; + // Validate return_to is a safe relative path (prevent open redirect) + const returnTo = + typeof rawReturn === 'string' && rawReturn.startsWith('/') && !rawReturn.startsWith('//') + ? rawReturn + : '/'; + + // Generate verifier first, then encrypt it with routing info into state. + // This avoids cookies/session for OIDC state, so it works across + // domains (custom domains → callback on www.duqduq.org). + const codeVerifier = generateCodeVerifier(); + const stateToken = encryptPayload({ v: codeVerifier, h: communityHost, r: returnTo }); + + // Pass the community hostname as context for per-community branding. + // The branding endpoint resolves hostnames → slugs. + const { url } = await buildAuthorizeUrl(stateToken, codeVerifier, communityHost); + + return res.redirect(url); +}); + +// ─── OIDC callback ─────────────────────────────────────────────────── + +router.get('/auth/callback', async (req: any, res: any) => { + try { + const { code, state, error } = req.query; + + if (error) { + console.error('KF Auth error:', error, req.query.error_description); + return res.status(400).send('Authentication failed. Please try again.'); + } + + if (!code || !state) { + return res.status(400).send('Missing authentication parameters.'); + } + + // Decrypt state → {v: codeVerifier, h: host, r: returnTo} + const stateData = decryptPayload<{ v: string; h: string; r: string }>(state); + if (!stateData || !stateData.v) { + return res.status(400).send('Invalid or expired authentication state.'); + } + + const { v: codeVerifier, h: host, r: rawReturn } = stateData; + const returnTo = + typeof rawReturn === 'string' && + rawReturn.startsWith('/') && + !rawReturn.startsWith('//') + ? rawReturn + : '/'; + + // Exchange authorization code for tokens + const tokens = await exchangeCode(code, codeVerifier); + + // Fetch user info from KF Auth + const userInfo = await fetchUserInfo(tokens.access_token); + const kfUserId = userInfo.sub; + + // Look up PubPub user by ID, or auto-create on first login + let user = await User.findOne({ where: { id: kfUserId } }); + + if (!user) { + const firstName = (userInfo.given_name || userInfo.name || 'New').trim(); + const lastName = (userInfo.family_name || 'User').trim(); + const fullName = `${firstName} ${lastName}`; + const initials = `${firstName[0] || '?'}${lastName[0] || '?'}`; + const baseSlug = slugifyString(fullName) || 'user'; + const existingSlugCount = await User.count({ + where: { slug: { [Op.like]: `${baseSlug}%` } }, + }); + const slug = existingSlugCount ? `${baseSlug}-${existingSlugCount + 1}` : baseSlug; + + // Use KF Auth email if available and not already taken + let email = `${kfUserId}@placeholder.invalid`; + if (userInfo.email) { + const emailTaken = await User.findOne({ + where: { email: userInfo.email.toLowerCase() }, + }); + if (!emailTaken) { + email = userInfo.email.toLowerCase(); + } + } + + user = await User.create({ + id: kfUserId, + slug, + firstName, + lastName, + fullName, + initials, + email, + avatar: userInfo.picture || null, + hash: '', + salt: '', + } as any); + console.log(`Auto-created PubPub user ${user.id} (${user.slug}) from KF Auth`); + } + + const protocol = isDevelopment() ? 'http' : 'https'; + + // For custom domains, we can't set a session here (different domain). + // Create a one-time encrypted token and redirect to session-set on the origin. + if (host && !isPlatformSubdomain(host)) { + const sessionToken = encryptPayload({ + u: user.id, + r: returnTo, + exp: Date.now() + 60_000, // 60 seconds + }); + const sessionSetUrl = `${protocol}://${host}/auth/session-set?token=${encodeURIComponent(sessionToken)}`; + return res.redirect(sessionSetUrl); + } + + // For platform subdomains: create session directly (shared cookie on .pubpub.org / .duqduq.org) + const logIn = promisify(req.logIn.bind(req)); + await logIn(user); + + const hashedUserId = getHashedUserId(user); + res.cookie('pp-lic', `pp-li-${hashedUserId}`, { + ...(isProd() && { domain: '.pubpub.org' }), + ...(isDuqDuq() && { domain: '.duqduq.org' }), + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + if (host && host !== req.hostname) { + return res.redirect(`${protocol}://${host}${returnTo}`); + } + return res.redirect(returnTo); + } catch (err: any) { + console.error('OIDC callback error:', err); + const detail = isDuqDuq() ? ` (${err?.message || err})` : ''; + return res.status(500).send(`Login failed. Please try again.${detail}`); + } +}); + +// ─── Session transfer for custom domains ───────────────────────────── + +router.get('/auth/session-set', async (req: any, res: any) => { + try { + const { token } = req.query; + if (!token) { + return res.status(400).send('Missing session token.'); + } + + const data = decryptPayload<{ u: string; r: string; exp: number }>(token); + if (!data || !data.u) { + return res.status(400).send('Invalid session token.'); + } + + if (Date.now() > data.exp) { + return res.status(400).send('Session token expired. Please log in again.'); + } + + const user = await User.findOne({ where: { id: data.u } }); + if (!user) { + return res.status(400).send('User not found.'); + } + + // Create Passport session on this domain + const logIn = promisify(req.logIn.bind(req)); + await logIn(user); + + // Set the CDN cache cookie on this domain + const hashedUserId = getHashedUserId(user); + res.cookie('pp-lic', `pp-li-${hashedUserId}`, { + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + const returnTo = data.r || '/'; + return res.redirect(returnTo); + } catch (err) { + console.error('Session-set error:', err); + return res.status(500).send('Failed to establish session. Please try again.'); + } +}); + +// ─── Logout ────────────────────────────────────────────────────────── + +router.post('/auth/logout', (req: any, res: any) => { + // Clear local session + req.logout(() => { + // Set pp-lic to logged-out state + res.cookie('pp-lic', 'pp-lo', { + ...(isProd() && + req.hostname.indexOf('pubpub.org') > -1 && { + domain: '.pubpub.org', + }), + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + // Redirect to KF Auth's logout endpoint so the SSO session is also cleared + const returnUrl = `${process.env.APP_URL || 'http://localhost:9876'}/`; + return res.redirect( + `${OIDC_ISSUER_URL}/api/auth/sign-out?callbackURL=${encodeURIComponent(returnUrl)}`, + ); + }); +}); + +// ─── Profile sync (webhook from KF Auth) ───────────────────────────── + +router.post('/api/kf/profile-sync', requireInternalKey, async (req: any, res: any) => { + try { + const { userId, givenName, familyName, displayName, email, image } = req.body; + + if (!userId) { + return res.status(400).json({ error: 'userId is required' }); + } + + const user = await User.findOne({ where: { id: userId } }); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } + + const updates: Record = {}; + if (displayName !== undefined) updates.fullName = displayName; + if (givenName !== undefined) updates.firstName = givenName; + if (familyName !== undefined) updates.lastName = familyName; + if (email !== undefined) updates.email = email.toLowerCase(); + if (image !== undefined) updates.avatar = image; + + // Recalculate initials when name changes + if (givenName !== undefined || familyName !== undefined || displayName !== undefined) { + const first = givenName ?? user.firstName ?? ''; + const last = familyName ?? user.lastName ?? ''; + if (first || last) { + updates.initials = `${first.charAt(0)}${last.charAt(0)}`.toUpperCase(); + } + } + + if (Object.keys(updates).length > 0) { + await user.update(updates); + } + + return res.status(200).json({ ok: true }); + } catch (err) { + console.error('Profile sync error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Context listing (for KF Auth playground) ─────────────────────── + +router.get('/api/kf/contexts', requireInternalKey, async (req: any, res: any) => { + try { + const communities = await Community.findAll({ + attributes: ['subdomain', 'title', 'avatar'], + order: [['title', 'ASC']], + limit: 200, + }); + + return res.json( + communities.map((c: any) => ({ + slug: c.subdomain, + title: c.title, + avatar: c.avatar || null, + })), + ); + } catch (err) { + console.error('Contexts listing error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Branding API (for KF Auth login page) ─────────────────────────── + +router.get('/api/kf/branding', requireInternalKey, async (req: any, res: any) => { + try { + const { subdomain, context } = req.query; + let identifier = context || subdomain; + + if (!identifier) { + return res.status(400).json({ error: 'subdomain or context param required' }); + } + + // If the identifier looks like a platform hostname, extract the subdomain slug. + // e.g. "mycommunity.duqduq.org" → "mycommunity", "mycommunity.pubpub.org" → "mycommunity" + const platformMatch = identifier.match(/^([^.]+)\.(pubpub\.org|duqduq\.org)$/); + if (platformMatch && platformMatch[1] !== 'www') { + identifier = platformMatch[1]; + } + + // Try by subdomain slug first + let community = await Community.findOne({ + where: { subdomain: identifier }, + attributes: [ + 'title', + 'avatar', + 'headerLogo', + 'accentColorLight', + 'accentColorDark', + 'subdomain', + ], + }); + + // If not found and identifier looks like a hostname, try as custom domain + if (!community && identifier.includes('.')) { + community = await Community.findOne({ + where: { domain: identifier }, + attributes: [ + 'title', + 'avatar', + 'headerLogo', + 'accentColorLight', + 'accentColorDark', + 'subdomain', + ], + }); + } + + if (!community) { + return res.status(404).json({ error: 'Community not found' }); + } + + return res.json({ + // Fields expected by kf-auth's loadAppContext() + display_name: community.title, + logo_url: community.avatar || community.headerLogo || null, + brand_color: community.accentColorDark || null, + background_color: community.accentColorLight || null, + // Extra fields for backwards compat / debugging + subdomain: community.subdomain, + }); + } catch (err) { + console.error('Branding API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Summary API (for KF Account roster / Hub) ────────────────────── + +router.get('/api/kf/summary', requireInternalKey, async (req: any, res: any) => { + try { + const { kf_org_id } = req.query; + if (!kf_org_id) { + return res.status(400).json({ error: 'kf_org_id is required' }); + } + + const communities = await Community.findAll({ + where: { kfOrgId: kf_org_id }, + attributes: ['id', 'title', 'subdomain', 'domain', 'avatar'], + }); + + const accounts = await Promise.all( + communities.map(async (community: any) => { + const [pubCount, memberCount] = await Promise.all([ + Pub.count({ where: { communityId: community.id } }), + Member.count({ + where: { communityId: community.id }, + }), + ]); + + const host = community.domain || `${community.subdomain}.pubpub.org`; + const protocol = isProd() ? 'https' : 'http'; + + return { + id: community.id, + slug: community.subdomain, + type: 'community', + name: community.title, + url: `${protocol}://${host}`, + avatar: community.avatar || null, + stats: { pubs: pubCount, members: memberCount }, + collections: [], + }; + }), + ); + + return res.json({ accounts }); + } catch (err) { + console.error('Summary API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Billing usage API (placeholder) ───────────────────────────────── + +router.get('/api/kf/billing/usage', requireInternalKey, async (req: any, res: any) => { + try { + const { kf_org_id } = req.query; + if (!kf_org_id) { + return res.status(400).json({ error: 'kf_org_id is required' }); + } + + const communityCount = await Community.count({ + where: { kfOrgId: kf_org_id }, + }); + + // Placeholder — just return community count for now + return res.json({ + kf_org_id, + line_items: [{ key: 'communities', quantity: communityCount }], + }); + } catch (err) { + console.error('Billing usage API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── User's KF orgs (session-authenticated) ───────────────────────── + +router.get('/api/kf/my-orgs', async (req: any, res: any) => { + if (!req.user?.id) { + return res.status(401).json({ error: 'Not authenticated' }); + } + try { + const orgs = await fetchUserOrgs(req.user.id); + return res.json({ orgs }); + } catch (err) { + console.error('Failed to fetch KF orgs:', err); + return res.status(500).json({ error: 'Failed to fetch organizations' }); + } +}); + +// ─── Transfer community ownership ─────────────────────────────────── + +router.post('/api/kf/transfer-community', async (req: any, res: any) => { + if (!req.user?.id) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + const { communityId, kfOrgId } = req.body; + if (!communityId || !kfOrgId) { + return res.status(400).json({ error: 'communityId and kfOrgId are required' }); + } + + try { + // Verify the user is an admin of this community + await ensureUserIsCommunityAdmin({ ...req, id: communityId }); + } catch { + return res.status(403).json({ error: 'You must be an admin of this community' }); + } + + try { + // Verify the user belongs to the target org + const userOrgs = await fetchUserOrgs(req.user.id); + const targetOrg = userOrgs.find((o) => o.id === kfOrgId); + if (!targetOrg) { + return res + .status(403) + .json({ error: 'You are not a member of the target organization' }); + } + + // Update the community's kfOrgId + const [updatedCount] = await Community.update({ kfOrgId }, { where: { id: communityId } }); + + if (updatedCount === 0) { + return res.status(404).json({ error: 'Community not found' }); + } + + return res.json({ success: true, kfOrgId }); + } catch (err) { + console.error('Transfer community error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Community detail (for Hubs dashboard) ─────────────────────────── + +router.get('/api/kf/community/:id/detail', requireInternalKey, async (req: any, res: any) => { + try { + const communityId = req.params.id; + // Optional date range params for analytics + const startDate = req.query.startDate || null; + const endDate = req.query.endDate || null; + // Determine analytics date range + const analyticsStart = + startDate || new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); + const analyticsEnd = endDate || new Date().toISOString().slice(0, 10); + const pubsMonthsBack = startDate + ? Math.max( + Math.ceil( + (Date.now() - new Date(startDate).getTime()) / (30 * 24 * 60 * 60 * 1000), + ), + 3, + ) || 24 + : 24; + + const community = await Community.findByPk(communityId, { + attributes: [ + 'id', + 'title', + 'subdomain', + 'domain', + 'avatar', + 'accentColorDark', + 'accentColorLight', + 'headerLogo', + 'heroLogo', + 'description', + 'heroBackgroundImage', + 'heroImage', + ], + }); + + if (!community) { + return res.status(404).json({ error: 'Community not found' }); + } + + const protocol = isProd() ? 'https' : 'http'; + const host = (community as any).domain || `${(community as any).subdomain}.pubpub.org`; + + // Run queries in parallel + const hasReleaseInclude = { + model: Release, + as: 'releases', + attributes: [], + required: true, + where: {}, + }; + + const [ + pubCount, + memberCount, + collectionCount, + releaseCount, + members, + recentPubRows, + pubsByMonthRows, + topAuthorsRaw, + collectionsRaw, + ] = await Promise.all([ + Pub.count({ where: { communityId }, include: [hasReleaseInclude] }), + Member.count({ where: { communityId } }), + Collection.count({ where: { communityId } }), + sequelize + .query( + `SELECT COUNT(*)::int AS count FROM "Releases" r INNER JOIN "Pubs" p ON r."pubId" = p.id WHERE p."communityId" = :communityId`, + { replacements: { communityId }, type: 'SELECT' as any }, + ) + .then((rows: any) => rows[0]?.count ?? 0), + // Members with user details + Member.findAll({ + where: { communityId }, + attributes: ['id', 'userId', 'permissions', 'isOwner', 'createdAt'], + include: [ + { + model: User, + as: 'user', + attributes: ['fullName', 'avatar', 'slug'], + }, + ], + order: [['createdAt', 'ASC']], + limit: 500, + }), + // Recent pubs (released only) + Pub.findAll({ + where: { communityId }, + attributes: [ + 'id', + 'title', + 'slug', + 'description', + 'avatar', + 'customPublishedAt', + 'createdAt', + ], + include: [ + hasReleaseInclude, + { + model: PubAttribution, + as: 'attributions', + attributes: ['name', 'avatar', 'order', 'isAuthor'], + where: { isAuthor: true }, + required: false, + include: [ + { model: User, as: 'user', attributes: ['fullName', 'avatar', 'slug'] }, + ], + }, + ], + order: [['createdAt', 'DESC']], + limit: 500, + }), + // Pubs by month + sequelize.query( + `SELECT + to_char(date_trunc('month', p."createdAt"), 'YYYY-MM') AS month, + COUNT(*)::int AS count + FROM "Pubs" p + INNER JOIN "Releases" r ON r."pubId" = p.id + WHERE p."communityId" = :communityId + AND p."createdAt" >= :pubsCutoff + GROUP BY 1 ORDER BY 1`, + { + replacements: { + communityId, + pubsCutoff: new Date( + Date.now() - pubsMonthsBack * 30 * 24 * 60 * 60 * 1000, + ).toISOString(), + }, + type: 'SELECT' as any, + }, + ), + // Top authors + PubAttribution.findAll({ + attributes: ['userId', 'name', 'avatar'], + where: { isAuthor: true }, + include: [ + { + model: Pub, + as: 'pub', + attributes: [], + where: { communityId }, + required: true, + include: [hasReleaseInclude], + }, + { + model: User, + as: 'user', + attributes: ['fullName', 'avatar', 'slug'], + required: false, + }, + ], + }), + // Collections with pub counts + sequelize.query( + `SELECT + c."id", c."title", c."slug", c."kind", + COUNT(cp."pubId")::int AS "pubCount" + FROM "Collections" c + LEFT JOIN "CollectionPubs" cp ON cp."collectionId" = c."id" + WHERE c."communityId" = :communityId + GROUP BY c."id", c."title", c."slug", c."kind" + ORDER BY "pubCount" DESC`, + { replacements: { communityId }, type: 'SELECT' as any }, + ), + ]); + + // Format members + const memberList = members.map((m: any) => { + const mj = m.toJSON(); + return { + id: mj.id, + userId: mj.userId, + name: mj.user?.fullName ?? 'Unknown', + avatar: mj.user?.avatar ?? null, + slug: mj.user?.slug ?? null, + role: mj.isOwner ? 'owner' : (mj.permissions ?? 'view'), + createdAt: mj.createdAt, + }; + }); + + // Format recent pubs + const recentPubs = recentPubRows.map((p: any) => { + const pj = p.toJSON(); + const authors = (pj.attributions || []) + .sort((a: any, b: any) => (a.order || 0) - (b.order || 0)) + .map((attr: any) => ({ + name: attr.user?.fullName || attr.name || 'Unknown', + avatar: attr.user?.avatar || null, + slug: attr.user?.slug || null, + })); + return { + id: pj.id, + title: pj.title, + slug: pj.slug, + description: pj.description, + avatar: pj.avatar, + publishedAt: pj.customPublishedAt || pj.createdAt, + authors, + }; + }); + + // Aggregate top authors + const authorMap = new Map< + string, + { name: string; avatar: string | null; slug: string | null; count: number } + >(); + for (const attr of topAuthorsRaw) { + const a = (attr as any).toJSON(); + const key = a.userId || `name:${a.name}`; + const existing = authorMap.get(key); + if (existing) { + existing.count++; + } else { + authorMap.set(key, { + name: a.user?.fullName || a.name || 'Unknown', + avatar: a.user?.avatar || a.avatar || null, + slug: a.user?.slug || null, + count: 1, + }); + } + } + const topAuthors = [...authorMap.values()].sort((a, b) => b.count - a.count); + + // Try to get analytics (daily views for selected range) from matview + let dailyViews: Array<{ date: string; views: number }> = []; + try { + dailyViews = (await sequelize.query( + `SELECT + date::text, + page_views::int AS views + FROM analytics_daily_summary + WHERE "communityId" = :communityId + AND date >= :analyticsStart::date + AND date <= :analyticsEnd::date + ORDER BY date`, + { + replacements: { communityId, analyticsStart, analyticsEnd }, + type: 'SELECT' as any, + }, + )) as any; + } catch { + // Matview may not exist in dev — that's fine + } + + // Total views/downloads from the selected range + let totalPageViews = 0; + let totalDownloads = 0; + try { + const [totals] = (await sequelize.query( + `SELECT + COALESCE(SUM(page_views), 0)::int AS views, + COALESCE(SUM(downloads), 0)::int AS downloads + FROM analytics_daily_summary + WHERE "communityId" = :communityId + AND date >= :analyticsStart::date + AND date <= :analyticsEnd::date`, + { + replacements: { communityId, analyticsStart, analyticsEnd }, + type: 'SELECT' as any, + }, + )) as any[]; + totalPageViews = totals?.views ?? 0; + totalDownloads = totals?.downloads ?? 0; + } catch { + // Matview may not exist + } + + return res.json({ + community: { + id: (community as any).id, + title: (community as any).title, + subdomain: (community as any).subdomain, + domain: (community as any).domain, + url: `${protocol}://${host}`, + avatar: (community as any).avatar, + headerLogo: (community as any).headerLogo, + heroLogo: (community as any).heroLogo, + description: (community as any).description, + accentColorDark: (community as any).accentColorDark, + accentColorLight: (community as any).accentColorLight, + heroBackgroundImage: (community as any).heroBackgroundImage, + heroImage: (community as any).heroImage, + }, + stats: { + pubs: pubCount, + members: memberCount, + collections: collectionCount, + releases: releaseCount, + totalPageViews, + totalDownloads, + }, + members: memberList, + recentPubs, + topAuthors, + pubsByMonth: pubsByMonthRows, + dailyViews, + collections: (collectionsRaw as any[]).map((c: any) => ({ + id: c.id, + title: c.title, + slug: c.slug, + kind: c.kind ?? 'tag', + pubCount: c.pubCount, + })), + }); + } catch (err) { + console.error('Community detail API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Suggested Communities (domain-based discovery) ────────────────── + +router.get('/api/kf/suggested-communities', requireInternalKey, async (req: any, res: any) => { + try { + const domainsParam = req.query.domains as string; + const excludeIds = (req.query.excludeIds as string) || ''; + if (!domainsParam) return res.json([]); + + const domains = domainsParam + .split(',') + .map((d: string) => d.trim().toLowerCase()) + .filter(Boolean); + if (domains.length === 0) return res.json([]); + + const excludeList = excludeIds.split(',').filter(Boolean); + + // Build domain match clause for User.email + const domainClauses: string[] = []; + const replacements: Record = {}; + domains.forEach((d, i) => { + domainClauses.push( + `(LOWER(SUBSTRING("Users"."email" FROM '@(.+)$')) = :dom${i} OR LOWER(SUBSTRING("Users"."email" FROM '@(.+)$')) LIKE :domLike${i})`, + ); + replacements[`dom${i}`] = d; + replacements[`domLike${i}`] = `%.${d}`; + }); + const domainWhere = domainClauses.join(' OR '); + + // Find communities with managers matching the domains + const managersQuery = ` + SELECT "Members"."communityId", COUNT(DISTINCT "Members"."userId")::int AS "managerCount" + FROM "Members" + INNER JOIN "Users" ON "Users"."id" = "Members"."userId" + WHERE "Members"."communityId" IS NOT NULL + AND "Members"."permissions" IN ('manage', 'admin') + AND (${domainWhere}) + GROUP BY "Members"."communityId" + `; + + // Find communities with authors matching the domains + const authorsQuery = ` + SELECT "Pubs"."communityId", COUNT(DISTINCT "PubAttributions"."userId")::int AS "authorCount" + FROM "PubAttributions" + INNER JOIN "Pubs" ON "Pubs"."id" = "PubAttributions"."pubId" + INNER JOIN "Users" ON "Users"."id" = "PubAttributions"."userId" + WHERE "PubAttributions"."isAuthor" = true + AND "PubAttributions"."userId" IS NOT NULL + AND (${domainWhere}) + GROUP BY "Pubs"."communityId" + `; + + const [managerRows, authorRows] = await Promise.all([ + sequelize.query(managersQuery, { replacements, type: 'SELECT' as any }) as any, + sequelize.query(authorsQuery, { replacements, type: 'SELECT' as any }) as any, + ]); + + // Merge counts + const communityMap = new Map(); + for (const row of managerRows) { + communityMap.set(row.communityId, { managerCount: row.managerCount, authorCount: 0 }); + } + for (const row of authorRows) { + const existing = communityMap.get(row.communityId) || { + managerCount: 0, + authorCount: 0, + }; + existing.authorCount = row.authorCount; + communityMap.set(row.communityId, existing); + } + + if (communityMap.size === 0) return res.json([]); + + // Exclude already-added communities + for (const id of excludeList) communityMap.delete(id); + if (communityMap.size === 0) return res.json([]); + + const communityIds = [...communityMap.keys()]; + const idPlaceholders = communityIds.map((_, i) => `:cid${i}`).join(', '); + const idReplacements: Record = {}; + communityIds.forEach((id, i) => { + idReplacements[`cid${i}`] = id; + }); + + const communityRows = (await sequelize.query( + `SELECT c."id", c."title", c."subdomain", c."domain", c."description", c."heroLogo", c."accentColorDark", c."accentColorLight", c."createdAt", + (SELECT COUNT(*)::int FROM "Pubs" p INNER JOIN "Releases" r ON r."pubId" = p."id" WHERE p."communityId" = c."id") AS "pubCount" + FROM "Communities" c + WHERE c."id" IN (${idPlaceholders}) + ORDER BY c."title" ASC`, + { replacements: idReplacements, type: 'SELECT' as any }, + )) as any[]; + + const results = communityRows.map((c: any) => { + const counts = communityMap.get(c.id) || { managerCount: 0, authorCount: 0 }; + return { + id: c.id, + title: c.title, + subdomain: c.subdomain, + domain: c.domain, + description: c.description, + heroLogo: c.heroLogo, + accentColorDark: c.accentColorDark, + accentColorLight: c.accentColorLight, + createdAt: c.createdAt, + pubCount: c.pubCount ?? 0, + managerCount: counts.managerCount, + authorCount: counts.authorCount, + }; + }); + + return res.json(results); + } catch (err) { + console.error('Suggested communities API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Suggested Pubs (full-text search discovery) ───────────────────── + +router.get('/api/kf/suggested-pubs', requireInternalKey, async (req: any, res: any) => { + try { + const termsParam = req.query.terms as string; + const excludeCommunityIds = (req.query.excludeCommunityIds as string) || ''; + const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 200); + if (!termsParam) return res.json([]); + + const terms = termsParam + .split(',') + .map((t: string) => t.trim()) + .filter(Boolean); + if (terms.length === 0) return res.json([]); + + const excludeList = excludeCommunityIds.split(',').filter(Boolean); + + // Build tsquery from terms — use adjacency operator (<->) for exact phrase matching + // e.g. "Mellon Foundation" → "mellon <-> foundation", single words get prefix match + const tsQuery = terms + .map((t) => { + const words = t + .trim() + .toLowerCase() + .replace(/[^\w\s]/g, ' ') + .replace(/\s+/g, ' ') + .trim() + .split(/\s+/) + .filter(Boolean); + if (words.length === 0) return null; + if (words.length === 1) return `${words[0]}:*`; + return `(${words.join(' <-> ')})`; + }) + .filter(Boolean) + .join(' | '); + + if (!tsQuery) return res.json([]); + + let excludeClause = ''; + const replacements: Record = { tsQuery, limit }; + if (excludeList.length > 0) { + const excludePlaceholders = excludeList.map((_, i) => `:excl${i}`).join(', '); + excludeList.forEach((id, i) => { + replacements[`excl${i}`] = id; + }); + excludeClause = `AND p."communityId" NOT IN (${excludePlaceholders})`; + } + + const rows = (await sequelize.query( + `SELECT + p."id", + p."title", + p."slug", + p."description", + p."avatar", + p."customPublishedAt", + p."communityId", + c."title" AS "communityTitle", + c."subdomain" AS "communitySubdomain", + c."domain" AS "communityDomain", + ts_rank(p."searchVector", to_tsquery('english', :tsQuery)) AS "rank", + CASE WHEN p."description" IS NOT NULL AND p."description" != '' + THEN ts_headline('english', p."description", to_tsquery('english', :tsQuery), 'StartSel=,StopSel=,MaxWords=60,MinWords=20,MaxFragments=2,FragmentDelimiter= … ') + ELSE NULL + END AS "snippet", + ( + SELECT string_agg(COALESCE(u2."fullName", pa2."name"), ', ' ORDER BY pa2."order" ASC) + FROM "PubAttributions" pa2 + LEFT JOIN "Users" u2 ON u2."id" = pa2."userId" + WHERE pa2."pubId" = p."id" AND pa2."isAuthor" = true + ) AS "byline" + FROM "Pubs" p + INNER JOIN "Communities" c ON c."id" = p."communityId" + INNER JOIN "Releases" r ON r."pubId" = p."id" + WHERE p."searchVector" @@ to_tsquery('english', :tsQuery) + ${excludeClause} + GROUP BY p."id", c."id" + ORDER BY "rank" DESC + LIMIT :limit`, + { replacements, type: 'SELECT' as any }, + )) as any[]; + + return res.json( + rows.map((r: any) => ({ + id: r.id, + title: r.title, + slug: r.slug, + description: r.description, + avatar: r.avatar, + communityId: r.communityId, + communityTitle: r.communityTitle, + communitySubdomain: r.communitySubdomain, + communityDomain: r.communityDomain, + byline: r.byline ?? null, + snippet: r.snippet ?? null, + publishedAt: r.customPublishedAt ?? null, + rank: parseFloat(r.rank), + })), + ); + } catch (err) { + console.error('Suggested pubs API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); + +// ─── Graph data (cross-community people network) ──────────────────── + +router.get('/api/kf/graph-data', requireInternalKey, async (req: any, res: any) => { + try { + const communityIdsParam = req.query.communityIds as string; + if (!communityIdsParam) return res.json({ nodes: [], links: [] }); + + const communityIds = communityIdsParam.split(',').filter(Boolean); + if (communityIds.length === 0) return res.json({ nodes: [], links: [] }); + + const idPlaceholders = communityIds.map((_: string, i: number) => `:cid${i}`).join(', '); + const replacements: Record = {}; + communityIds.forEach((id: string, i: number) => { + replacements[`cid${i}`] = id; + }); + + // Get communities + const communities = (await sequelize.query( + `SELECT "id", "title", "subdomain", "accentColorDark" + FROM "Communities" + WHERE "id" IN (${idPlaceholders})`, + { replacements, type: 'SELECT' as any }, + )) as any[]; + + // Get people who appear in multiple communities (managers + authors) + const peopleQuery = ` + SELECT + u."id" AS "userId", + u."fullName" AS "name", + u."avatar", + array_agg(DISTINCT sub."communityId") AS "communityIds", + array_agg(DISTINCT sub."role") AS "roles" + FROM ( + SELECT m."userId", m."communityId", 'member' AS "role" + FROM "Members" m + WHERE m."communityId" IN (${idPlaceholders}) + AND m."userId" IS NOT NULL + UNION ALL + SELECT pa."userId", p."communityId", 'author' AS "role" + FROM "PubAttributions" pa + INNER JOIN "Pubs" p ON p."id" = pa."pubId" + WHERE p."communityId" IN (${idPlaceholders}) + AND pa."userId" IS NOT NULL + AND pa."isAuthor" = true + ) sub + INNER JOIN "Users" u ON u."id" = sub."userId" + GROUP BY u."id", u."fullName", u."avatar" + HAVING COUNT(DISTINCT sub."communityId") >= 2 + ORDER BY COUNT(DISTINCT sub."communityId") DESC + LIMIT 200 + `; + + const people = (await sequelize.query(peopleQuery, { + replacements, + type: 'SELECT' as any, + })) as any[]; + + // Build graph nodes and links + type GraphNode = { + id: string; + label: string; + type: 'community' | 'person'; + color?: string; + avatar?: string; + }; + type GraphLink = { source: string; target: string; roles: string[] }; + + const nodes: GraphNode[] = [ + ...communities.map((c: any) => ({ + id: c.id, + label: c.title, + type: 'community' as const, + color: c.accentColorDark ?? '#5c7080', + })), + ...people.map((p: any) => ({ + id: p.userId, + label: p.name ?? 'Anonymous', + type: 'person' as const, + avatar: p.avatar, + })), + ]; + + const links: GraphLink[] = []; + for (const p of people) { + for (const cid of p.communityIds) { + if (communityIds.includes(cid)) { + links.push({ + source: p.userId, + target: cid, + roles: p.roles ?? [], + }); + } + } + } + + return res.json({ nodes, links, communities: communities.length, people: people.length }); + } catch (err) { + console.error('Graph data API error:', err); + return res.status(500).json({ error: 'Internal error' }); + } +}); diff --git a/server/kf/auth.ts b/server/kf/auth.ts new file mode 100644 index 000000000..59be35e74 --- /dev/null +++ b/server/kf/auth.ts @@ -0,0 +1,24 @@ +/** + * Legacy re-export shim for PubPub's OIDC client. + * New code should import from './oidc.server.js' directly. + */ + +export { + APP_URL, + buildAuthorizeUrl, + decryptPayload, + encryptPayload, + exchangeCode, + extractOrgs, + fetchUserInfo, + fetchUserOrgs, + generateCodeChallenge, + generateCodeVerifier, + initOidc, + OIDC_CLIENT_ID, + OIDC_ISSUER_URL, + type OIDCOrg as KFOrg, + type OIDCUserInfo as KFUserInfo, + REDIRECT_URI, + type TokenResponse, +} from './oidc.server.js'; diff --git a/server/kf/oidc.server.ts b/server/kf/oidc.server.ts new file mode 100644 index 000000000..734e47250 --- /dev/null +++ b/server/kf/oidc.server.ts @@ -0,0 +1,280 @@ +/** + * Generic OIDC client with auto-discovery (PubPub edition). + * + * Reads endpoints from the provider's .well-known/openid-configuration. + * Works with any standards-compliant OIDC provider (KF Auth, Keycloak, Auth0, etc.). + * + * Env vars: + * OIDC_ISSUER_URL — browser-facing issuer URL + * OIDC_ISSUER_INTERNAL_URL — server-to-server URL for Docker (falls back to OIDC_ISSUER_URL) + * OIDC_CLIENT_ID — OAuth client ID + * OIDC_CLIENT_SECRET — OAuth client secret + * OIDC_ORGS_CLAIM — custom claim key for org memberships (default: https://knowledgefutures.org/orgs) + */ + +import * as crypto from 'node:crypto'; + +// --- Config (with backward-compat fallbacks) --- + +const OIDC_ISSUER_URL = process.env.OIDC_ISSUER_URL ?? 'http://localhost:3000'; + +const OIDC_ISSUER_INTERNAL_URL = process.env.OIDC_ISSUER_INTERNAL_URL ?? OIDC_ISSUER_URL; + +const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID ?? 'kf_pubpub'; + +const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET ?? ''; + +const OIDC_ORGS_CLAIM = process.env.OIDC_ORGS_CLAIM ?? 'https://knowledgefutures.org/orgs'; + +const APP_URL = process.env.APP_URL ?? 'http://localhost:9876'; +const REDIRECT_URI = `${APP_URL}/auth/callback`; + +// --- OIDC Discovery --- + +interface OIDCDiscovery { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + jwks_uri?: string; +} + +let discoveryCache: OIDCDiscovery | null = null; +let discoveryPromise: Promise | null = null; + +async function discover(): Promise { + if (discoveryCache) return discoveryCache; + if (discoveryPromise) return discoveryPromise; + + discoveryPromise = (async () => { + const url = `${OIDC_ISSUER_INTERNAL_URL}/.well-known/openid-configuration`; + const res = await fetch(url); + if (!res.ok) { + throw new Error( + `OIDC discovery failed: ${res.status} from ${url}. ` + + `Ensure OIDC_ISSUER_URL points to a valid OIDC provider.`, + ); + } + const config = (await res.json()) as OIDCDiscovery; + discoveryCache = config; + return config; + })().catch((err) => { + // Clear the cached promise so subsequent calls can retry + discoveryPromise = null; + throw err; + }); + + return discoveryPromise; +} + +/** Pre-warm OIDC discovery cache. Non-fatal — discovery will be retried on demand. */ +export async function initOidc(): Promise { + await discover(); +} + +/** + * Rewrite a discovered endpoint URL to use the internal host. + * Discovery may return URLs with the public host (BETTER_AUTH_URL), + * but server-to-server calls must use OIDC_ISSUER_INTERNAL_URL. + */ +function internalEndpoint(discoveredUrl: string): string { + const url = new URL(discoveredUrl); + const base = new URL(OIDC_ISSUER_INTERNAL_URL); + url.protocol = base.protocol; + url.host = base.host; + return url.toString(); +} + +// --- Symmetric encryption (AES-256-GCM) --- + +/** Derive a 32-byte key from the client secret for AES-256-GCM. */ +function deriveKey(): Buffer { + return crypto.createHash('sha256').update(OIDC_CLIENT_SECRET).digest(); +} + +/** Encrypt a JSON-serializable object → base64url token. */ +export function encryptPayload(data: object): string { + const key = deriveKey(); + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); + const plaintext = JSON.stringify(data); + const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + return Buffer.concat([iv, tag, encrypted]).toString('base64url'); +} + +/** Decrypt a base64url token → parsed object, or null on failure. */ +export function decryptPayload(token: string): T | null { + try { + const key = deriveKey(); + const buf = Buffer.from(token, 'base64url'); + if (buf.length < 29) return null; + const iv = buf.subarray(0, 12); + const tag = buf.subarray(12, 28); + const ciphertext = buf.subarray(28); + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(tag); + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]); + return JSON.parse(decrypted.toString('utf8')) as T; + } catch { + return null; + } +} + +// --- PKCE helpers --- + +export function generateCodeVerifier(): string { + return crypto.randomBytes(32).toString('base64url'); +} + +export function generateCodeChallenge(verifier: string): string { + return crypto.createHash('sha256').update(verifier).digest('base64url'); +} + +// --- OIDC Flows --- + +/** + * Build the URL to redirect the user to for authentication. + * Uses the discovered authorization_endpoint (browser-facing). + */ +export async function buildAuthorizeUrl( + state: string, + existingVerifier?: string, + context?: string, +): Promise<{ url: string; codeVerifier: string }> { + const config = await discover(); + const codeVerifier = existingVerifier ?? generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + + // Use browser-facing URL for authorize endpoint + const authorizeUrl = new URL(config.authorization_endpoint); + const browserBase = new URL(OIDC_ISSUER_URL); + authorizeUrl.protocol = browserBase.protocol; + authorizeUrl.host = browserBase.host; + + const params = new URLSearchParams({ + client_id: OIDC_CLIENT_ID, + redirect_uri: REDIRECT_URI, + response_type: 'code', + scope: 'openid profile email', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + ...(context && { context }), + }); + + return { url: `${authorizeUrl.toString()}?${params}`, codeVerifier }; +} + +export interface TokenResponse { + access_token: string; + token_type: string; + expires_in: number; + id_token?: string; + refresh_token?: string; +} + +/** + * Exchange an authorization code for tokens (server-to-server). + */ +export async function exchangeCode(code: string, codeVerifier: string): Promise { + const config = await discover(); + const body = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: REDIRECT_URI, + client_id: OIDC_CLIENT_ID, + client_secret: OIDC_CLIENT_SECRET, + code_verifier: codeVerifier, + }); + + const res = await fetch(internalEndpoint(config.token_endpoint), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Token exchange failed: ${res.status} ${text}`); + } + + return res.json() as Promise; +} + +// --- UserInfo --- + +export interface OIDCOrg { + id: string; + name: string; + slug: string; + type: 'personal' | 'shared'; + role: string; +} + +export interface OIDCUserInfo { + sub: string; + name?: string; + email?: string; + picture?: string; + given_name?: string; + family_name?: string; + [key: string]: unknown; +} + +export async function fetchUserInfo(accessToken: string): Promise { + const config = await discover(); + const res = await fetch(internalEndpoint(config.userinfo_endpoint), { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!res.ok) { + throw new Error(`UserInfo failed: ${res.status}`); + } + + return res.json() as Promise; +} + +/** Extract org memberships from the userinfo response. */ +export function extractOrgs(userInfo: OIDCUserInfo): OIDCOrg[] { + const orgs = userInfo[OIDC_ORGS_CLAIM]; + if (Array.isArray(orgs)) return orgs as OIDCOrg[]; + return []; +} + +// --- Internal API (optional, for KF Auth specific features) --- + +const AUTH_INTERNAL_API_URL = process.env.AUTH_INTERNAL_API_URL ?? OIDC_ISSUER_INTERNAL_URL; + +const AUTH_INTERNAL_API_KEY = process.env.AUTH_INTERNAL_API_KEY ?? ''; + +/** Whether the internal API is configured and available. */ +export const hasInternalApi = Boolean(AUTH_INTERNAL_API_KEY); + +/** + * Fetch a user's orgs from the auth provider's internal API. + * Returns empty array if internal API is not configured. + */ +export async function fetchUserOrgs(userId: string): Promise { + if (!AUTH_INTERNAL_API_KEY) return []; + + const res = await fetch(`${AUTH_INTERNAL_API_URL}/api/internal/users/${userId}/orgs`, { + headers: { Authorization: `Bearer ${AUTH_INTERNAL_API_KEY}` }, + }); + + if (!res.ok) return []; + const data = (await res.json()) as { orgs?: OIDCOrg[] }; + return data.orgs ?? []; +} + +// --- Exports --- + +export { + OIDC_ISSUER_URL, + OIDC_ISSUER_INTERNAL_URL, + OIDC_CLIENT_ID, + OIDC_CLIENT_SECRET, + OIDC_ORGS_CLAIM, + APP_URL, + REDIRECT_URI, +}; diff --git a/server/routes/communityCreate.tsx b/server/routes/communityCreate.tsx index ab3653f50..cf2d9ba8f 100644 --- a/server/routes/communityCreate.tsx +++ b/server/routes/communityCreate.tsx @@ -10,6 +10,7 @@ import { getHubWithCommunities, isUserHubManager, } from 'server/hub/queries'; +import { fetchUserOrgs } from 'server/kf/auth'; import { handleErrors } from 'server/utils/errors'; import { getInitialData } from 'server/utils/initData'; import { hostIsValid } from 'server/utils/routes'; @@ -27,8 +28,9 @@ router.get('/community/create', (req, res, next) => { return Promise.all([ getInitialData(req), hubSlug ? getHubBySlug(hubSlug) : Promise.resolve(null), + req.user?.id ? fetchUserOrgs(req.user.id) : Promise.resolve([]), ]) - .then(async ([initialData, hubData]) => { + .then(async ([initialData, hubData, kfOrgs]) => { const templates = hubData ? await getActiveTemplatesForHub(hubData.id) : []; // Fetch hub communities for the clone-from-community picker @@ -81,7 +83,7 @@ router.get('/community/create', (req, res, next) => { { + const returnTo = req.query.redirect || req.query.return_to || '/'; + return res.redirect(`/auth/login?return_to=${encodeURIComponent(String(returnTo))}`); +}); diff --git a/server/routes/passwordReset.kf.tsx b/server/routes/passwordReset.kf.tsx new file mode 100644 index 000000000..c0be4dd17 --- /dev/null +++ b/server/routes/passwordReset.kf.tsx @@ -0,0 +1,17 @@ +/** + * Phase C: Password reset redirect. + * + * Password management now happens through KF Auth. + * Redirect both the request-reset page and the reset-with-hash page. + */ + +import { Router } from 'express'; + +import { OIDC_ISSUER_URL } from 'server/kf/auth'; + +export const router = Router(); + +router.get(['/password-reset', '/password-reset/:resetHash/:slug'], (req, res) => { + // Old reset links won't work; redirect to KF Auth's password reset flow + return res.redirect(`${OIDC_ISSUER_URL}/forgot-password`); +}); diff --git a/server/routes/signup.kf.tsx b/server/routes/signup.kf.tsx new file mode 100644 index 000000000..d20ce31bf --- /dev/null +++ b/server/routes/signup.kf.tsx @@ -0,0 +1,29 @@ +/** + * Phase C: Signup page redirect. + * + * Instead of rendering the PubPub signup page, redirect to KF Auth's + * sign-up flow. KF Auth handles account creation now. + */ + +import { Router } from 'express'; + +import { APP_URL, OIDC_CLIENT_ID, OIDC_ISSUER_URL } from 'server/kf/auth'; + +export const router = Router(); + +router.get('/signup', (req, res) => { + // Redirect to KF Auth's sign-up page, passing the PubPub client_id + // so KF Auth shows PubPub-branded signup and redirects back after. + const params = new URLSearchParams({ + client_id: OIDC_CLIENT_ID, + redirect_uri: `${APP_URL}/auth/callback`, + }); + return res.redirect(`${OIDC_ISSUER_URL}/sign-up?${params}`); +}); + +// Also redirect the /user/create/:hash route (email verification step) +// These links in old verification emails won't work after migration; +// users who click them should be directed to sign up fresh via KF Auth. +router.get('/user/create/:hash', (req, res) => { + return res.redirect(`${OIDC_ISSUER_URL}/sign-up`); +}); diff --git a/server/server.ts b/server/server.ts index 1cd9c2fa2..a201775a9 100755 --- a/server/server.ts +++ b/server/server.ts @@ -339,6 +339,12 @@ app.use(appRouter); /* ------------ */ const port = env.PORT; export const startServer = async () => { + // Pre-warm OIDC discovery (non-fatal — will retry on first auth request) + const { initOidc } = await import('./kf/oidc.server.js'); + await initOidc().catch((err) => { + console.warn('[OIDC] Discovery failed at startup (will retry on demand):', err.message); + }); + await sequelizeSyncPromise; return app.listen( port, diff --git a/tools/migrations/2026_05_15_addKfOrgIdToCommunities.js b/tools/migrations/2026_05_15_addKfOrgIdToCommunities.js new file mode 100644 index 000000000..6b2e95e71 --- /dev/null +++ b/tools/migrations/2026_05_15_addKfOrgIdToCommunities.js @@ -0,0 +1,17 @@ +export const up = async ({ Sequelize, sequelize }) => { + await sequelize.queryInterface.addColumn('Communities', 'kfOrgId', { + type: Sequelize.TEXT, + allowNull: true, + }); + await sequelize.queryInterface.addIndex('Communities', ['kfOrgId'], { + name: 'communities_kf_org_id_idx', + }); +}; + +export const down = async ({ sequelize }) => { + await sequelize.queryInterface.removeIndex( + 'Communities', + 'communities_kf_org_id_idx', + ); + await sequelize.queryInterface.removeColumn('Communities', 'kfOrgId'); +}; diff --git a/tools/migrations/2026_06_15_makeKfOrgIdNotNull.js b/tools/migrations/2026_06_15_makeKfOrgIdNotNull.js new file mode 100644 index 000000000..f9ec0a880 --- /dev/null +++ b/tools/migrations/2026_06_15_makeKfOrgIdNotNull.js @@ -0,0 +1,33 @@ +/** + * Phase D cleanup: Make kfOrgId NOT NULL on Communities. + * + * Run this ONLY after confirming all communities have been assigned + * a kfOrgId value (from the seed script + new community creation). + */ + +export const up = async ({ Sequelize, sequelize }) => { + // First verify there are no NULL values + const [results] = await sequelize.query( + `SELECT count(*) as count FROM "Communities" WHERE "kfOrgId" IS NULL`, + ); + const nullCount = parseInt(results[0].count, 10); + + if (nullCount > 0) { + throw new Error( + `Cannot make kfOrgId NOT NULL: ${nullCount} communities still have NULL kfOrgId. ` + + `Assign ownership first, then re-run this migration.`, + ); + } + + await sequelize.queryInterface.changeColumn('Communities', 'kfOrgId', { + type: Sequelize.TEXT, + allowNull: false, + }); +}; + +export const down = async ({ Sequelize, sequelize }) => { + await sequelize.queryInterface.changeColumn('Communities', 'kfOrgId', { + type: Sequelize.TEXT, + allowNull: true, + }); +}; diff --git a/tools/migrations/2026_06_15_removePasswordColumns.js b/tools/migrations/2026_06_15_removePasswordColumns.js new file mode 100644 index 000000000..2e9d602c8 --- /dev/null +++ b/tools/migrations/2026_06_15_removePasswordColumns.js @@ -0,0 +1,35 @@ +/** + * Phase D cleanup: Remove password-related columns from Users table. + * + * After the 30-day transition period, all users authenticate via KF Auth. + * These columns are no longer needed in PubPub's database. + * + * Run this ONLY after confirming all old sessions have expired and + * the OIDC login flow is working reliably. + */ + +export const up = async ({ Sequelize, sequelize }) => { + const qi = sequelize.queryInterface; + + // Remove password-related columns + await qi.removeColumn('Users', 'hash'); + await qi.removeColumn('Users', 'salt'); + await qi.removeColumn('Users', 'passwordDigest'); + await qi.removeColumn('Users', 'sha3hashedPassword'); + await qi.removeColumn('Users', 'resetHash'); + await qi.removeColumn('Users', 'resetHashExpiration'); +}; + +export const down = async ({ Sequelize, sequelize }) => { + const qi = sequelize.queryInterface; + + // Restore password-related columns (data is gone though) + await qi.addColumn('Users', 'hash', { type: Sequelize.TEXT }); + await qi.addColumn('Users', 'salt', { type: Sequelize.TEXT }); + await qi.addColumn('Users', 'passwordDigest', { type: Sequelize.TEXT }); + await qi.addColumn('Users', 'sha3hashedPassword', { type: Sequelize.TEXT }); + await qi.addColumn('Users', 'resetHash', { type: Sequelize.TEXT }); + await qi.addColumn('Users', 'resetHashExpiration', { + type: Sequelize.DATE, + }); +}; diff --git a/utils/api/schemas/community.ts b/utils/api/schemas/community.ts index 9cc73c780..528b0b879 100644 --- a/utils/api/schemas/community.ts +++ b/utils/api/schemas/community.ts @@ -98,6 +98,7 @@ export const communitySchema = baseSchema.extend({ spamTagId: z.string().uuid().nullable(), scopeSummaryId: z.string().uuid().nullable(), templateId: z.string().uuid().nullable(), + kfOrgId: z.string().nullable(), accentTextColor: z.string(), analyticsSettings: analyticsSettingsSchema, }) satisfies z.ZodType; @@ -125,6 +126,7 @@ export const communityCreateSchema = communitySchema altcha: z.string().optional(), _honeypot: z.string().optional(), templateId: z.string().uuid().nullish(), + kfOrgId: z.string().nullish(), }); export const communityUpdateSchema = communitySchema