diff --git a/src/lib/auth.ts b/src/lib/auth.ts index e314cad..87d5284 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1 +1,338 @@ -aW1wb3J0IHsgYmV0dGVyQXV0aCB9IGZyb20gImJldHRlci1hdXRoIjsKaW1wb3J0IHsgZHJpenpsZUFkYXB0ZXIgfSBmcm9tICJiZXR0ZXItYXV0aC9hZGFwdGVycy9kcml6emxlIjsKaW1wb3J0IHsgZ2VuZXJpY09BdXRoIH0gZnJvbSAiYmV0dGVyLWF1dGgvcGx1Z2lucyI7CmltcG9ydCB7IGdldERiLCBhdXRoUHJvdmlkZXJDb25maWcsIGVxIH0gZnJvbSAiQGdyb29tYm9vay9kYiI7CmltcG9ydCB7IGRlY3J5cHRTZWNyZXQgfSBmcm9tICJAZ3Jvb21ib29rL2RiIjsKaW1wb3J0IHsgc2VuZEVtYWlsIH0gZnJvbSAiLi4vc2VydmljZXMvZW1haWwuanMiOwoKY29uc3QgQkVUVEVSX0FVVEhfU0VDUkVUID0gcHJvY2Vzcy5lbnYuQkVUVEVSX0FVVEhfU0VDUkVUOwpjb25zdCBCRVRURVJfQVVUSF9VUkwgPSBwcm9jZXNzLmVudi5CRVRURVJfQVVUSF9VUkwgPz8gImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCI7CgovLyBBdXRoIGluc3RhbmNlIOKAlCBpbml0aWFsaXplZCBsYXppbHkgdmlhIGluaXRBdXRoKCkKLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIEB0eXBlc2NyaXB0LWVzbGludC9uby1leHBsaWNpdC1hbnkKbGV0IGF1dGhJbnN0YW5jZTogYW55ID0gbnVsbDsKbGV0IGF1dGhJbml0UHJvbWlzZTogUHJvbWlzZTx2b2lkPiB8IG51bGwgPSBudWxsOwoKLyoqIFJldHVybnMgdGhlIGN1cnJlbnQgYXV0aCBpbnN0YW5jZS4gVGhyb3dzIGlmIG5vdCB5ZXQgaW5pdGlhbGl6ZWQuICovCmV4cG9ydCBmdW5jdGlvbiBnZXRBdXRoKCkgewogIGlmICghYXV0aEluc3RhbmNlKSB7CiAgICB0aHJvdyBuZXcgRXJyb3IoCiAgICAgICJBdXRoIG5vdCBpbml0aWFsaXplZC4gQ2FsbCBpbml0QXV0aCgpIGF0IHN0YXJ0dXAgYmVmb3JlIGhhbmRsaW5nIHJlcXVlc3RzLiIKICAgICk7CiAgfQogIHJldHVybiBhdXRoSW5zdGFuY2U7Cn0KCi8qKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHJlc29sdmVzIHdoZW4gYXV0aCBpcyBpbml0aWFsaXplZC4gKi8KZXhwb3J0IGZ1bmN0aW9uIGdldEF1dGhQcm9taXNlKCkgewogIHJldHVybiBhdXRoSW5pdFByb21pc2U7Cn0KCi8qKiBSZXR1cm5zIHdoaWNoIE9BdXRoL3NvY2lhbCBwcm92aWRlcnMgYXJlIGNvbmZpZ3VyZWQgdmlhIGVudiB2YXJzLiAqLwpleHBvcnQgZnVuY3Rpb24gZ2V0QWN0aXZlUHJvdmlkZXJzKCk6IHN0cmluZ1tdIHsKICBjb25zdCBwcm92aWRlcnM6IHN0cmluZ1tdID0gW107CiAgaWYgKHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnb29nbGUiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpIHsKICAgIHByb3ZpZGVycy5wdXNoKCJnaXRodWIiKTsKICB9CiAgaWYgKHByb2Nlc3MuZW52Lk9JRENfSVNTVUVSICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52Lk9JRENfQ0xJRU5UX1NFQ1JFVCkgewogICAgcHJvdmlkZXJzLnB1c2goImF1dGhlbnRpayIpOwogIH0KICByZXR1cm4gcHJvdmlkZXJzOwp9CgovKioKICogUmUtaW5pdGlhbGl6ZXMgdGhlIEJldHRlci1BdXRoIGluc3RhbmNlIGFmdGVyIGF1dGggY29uZmlnIGNoYW5nZXMuCiAqCiAqIENsZWFycyBib3RoIGF1dGhJbnN0YW5jZSBhbmQgYXV0aEluaXRQcm9taXNlLCB0aGVuIGNhbGxzIGluaXRBdXRoKCkgdG8KICogcmUtcmVhZCBjb25maWcgZnJvbSBEQiBhbmQgYnVpbGQgYSBmcmVzaCBCZXR0ZXItQXV0aCBpbnN0YW5jZS4KICogU2Vzc2lvbnMgYXJlIERCLWJhY2tlZCBhbmQgc3Vydml2ZSB0aGUgcmUtaW5pdC4KICovCmV4cG9ydCBhc3luYyBmdW5jdGlvbiByZWluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGF1dGhJbnN0YW5jZSA9IG51bGw7CiAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsKICBhd2FpdCBpbml0QXV0aCgpOwogIGNvbnNvbGUubG9nKCJbYXV0aF0gUmUtaW5pdGlhbGl6ZWQgYXV0aCBpbnN0YW5jZSBhZnRlciBjb25maWcgY2hhbmdlIik7Cn0KCi8qKgogKiBJbml0aWFsaXplcyB0aGUgQmV0dGVyLUF1dGggaW5zdGFuY2UuCiAqCiAqIENvbmZpZyByZXNvbHV0aW9uIGNoYWluOgogKiAxLiBRdWVyeSBhdXRoX3Byb3ZpZGVyX2NvbmZpZyB0YWJsZSBmb3IgYW4gZW5hYmxlZCBwcm92aWRlcgogKiAyLiBJZiBEQiBjb25maWcgZXhpc3RzIOKGkiB1c2UgaXQgKGRlY3J5cHQgY2xpZW50U2VjcmV0KQogKiAzLiBJZiBubyBEQiBjb25maWcg4oaSIGZhbGwgYmFjayB0byBPSURDXyogZW52IHZhcnMKICogNC4gSWYgbmVpdGhlciDihpIgYXV0aCBpcyB1bmNvbmZpZ3VyZWQgKGdldEF1dGgoKSByZXR1cm5zIG51bGwsIEFVVEhfRElTQUJMRUQgaW1wbGllZCkKICoKICogSWRlbXBvdGVudCDigJQgc3Vic2VxdWVudCBjYWxscyByZXR1cm4gaW1tZWRpYXRlbHkgYWZ0ZXIgaW5pdGlhbGl6YXRpb24gY29tcGxldGVzLgogKi8KZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGluaXRBdXRoKCk6IFByb21pc2U8dm9pZD4gewogIGlmIChhdXRoSW5zdGFuY2UpIHJldHVybjsgLy8gQWxyZWFkeSBpbml0aWFsaXplZAogIGlmIChhdXRoSW5pdFByb21pc2UpIHsKICAgIGF3YWl0IGF1dGhJbml0UHJvbWlzZTsKICAgIHJldHVybjsKICB9CgogIGF1dGhJbml0UHJvbWlzZSA9IChhc3luYyAoKSA9PiB7CiAgICAvLyBHdWFyZDogcmVxdWlyZSBCRVRURVJfQVVUSF9TRUNSRVQgdW5sZXNzIGV4cGxpY2l0bHkgaW4gZGV2L2RlbW8gbW9kZQogICAgaWYgKCFCRVRURVJfQVVUSF9TRUNSRVQgJiYgcHJvY2Vzcy5lbnYuQVVUSF9ESVNBQkxFRCAhPT0gInRydWUiKSB7CiAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAiW0ZBVEFMXSBCRVRURVJfQVVUSF9TRUNSRVQgZW52aXJvbm1lbnQgdmFyaWFibGUgaXMgcmVxdWlyZWQgd2hlbiBhdXRoIGlzIGVuYWJsZWQiCiAgICAgICk7CiAgICB9CgogICAgLy8gQVVUSF9ESVNBQkxFRD10cnVlIG1lYW5zIGRldi9kZW1vIG1vZGUg4oCUIHN0aWxsIGJ1aWxkIEJldHRlci1BdXRoIHdpdGggcGxhY2Vob2xkZXIKICAgIC8vIGNvbmZpZyBzbyBhdXRoLmhhbmRsZXIgZXhpc3RzIChtaWRkbGV3YXJlIGJ5cGFzc2VzIGl0IGFueXdheSkKICAgIGlmIChwcm9jZXNzLmVudi5BVVRIX0RJU0FCTEVEID09PSAidHJ1ZSIpIHsKICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gQVVUSF9ESVNBQkxFRD10cnVlIOKAlCBidWlsZGluZyBwbGFjZWhvbGRlciBhdXRoIGluc3RhbmNlIik7CiAgICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICAgIGRhdGFiYXNlOiBkcml6emxlQWRhcHRlcihnZXREYigpLCB7IHByb3ZpZGVyOiAicGciIH0pLAogICAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVUISwKICAgICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgICAgcmF0ZUxpbWl0OiB7CiAgICAgICAgICBlbmFibGVkOiB0cnVlLAogICAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgICB3aW5kb3c6IDEwLAogICAgICAgICAgc3RvcmFnZTogIm1lbW9yeSIsCiAgICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgICAiL2dldC1zZXNzaW9uIjogZmFsc2UsCiAgICAgICAgICB9LAogICAgICAgIH0sCiAgICAgICAgcGx1Z2luczogWwogICAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgICAgY29uZmlnOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgcHJvdmlkZXJJZDogImF1dGhlbnRpayIsCiAgICAgICAgICAgICAgICBjbGllbnRJZDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGNsaWVudFNlY3JldDogInBsYWNlaG9sZGVyIiwKICAgICAgICAgICAgICAgIGRpc2NvdmVyeVVybDogdW5kZWZpbmVkLAogICAgICAgICAgICAgICAgc2NvcGVzOiBbIm9wZW5pZCIsICJwcm9maWxlIiwgImVtYWlsIl0sCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgXSwKICAgICAgICAgIH0pLAogICAgICAgIF0sCiAgICAgICAgc2Vzc2lvbjogewogICAgICAgICAgZXhwaXJlc0luOiA2MCAqIDYwICogMjQgKiA3LAogICAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsCiAgICAgICAgICBjb29raWVDYWNoZTogeyBlbmFibGVkOiBmYWxzZSB9LAogICAgICAgIH0sCiAgICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAgIC5zcGxpdCgiLCIpLm1hcCgocykgPT4gcy50cmltKCkpLmZpbHRlcihCb29sZWFuKSwKICAgICAgfSk7CiAgICAgIHJldHVybjsKICAgIH0KCiAgICAvLyBTdGVwIDE6IFRyeSB0byBsb2FkIGNvbmZpZyBmcm9tIERCLCB3aXRoIHJldHJ5LXdpdGgtYmFja29mZiBmb3IgdHJhbnNpZW50IEVDT05OUkVTRVQgKEdSTy0yNjUyKS4KICAgIC8vIEEgc2luZ2xlIGNvbm5lY3Rpb24gcmVzZXQgZHVyaW5nIGJvb3QgbXVzdCBub3QgYWJvcnQgaW5pdGlhbGl6YXRpb24uCiAgICBjb25zdCBkYiA9IGdldERiKCk7CiAgICBsZXQgZGJRdWVyeVJvd3M6ICh0eXBlb2YgYXV0aFByb3ZpZGVyQ29uZmlnLiRpbmZlclNlbGVjdClbXSA9IFtdOwogICAgbGV0IGRiQXR0ZW1wdCA9IDA7CiAgICB3aGlsZSAodHJ1ZSkgewogICAgICB0cnkgewogICAgICAgIGRiUXVlcnlSb3dzID0gYXdhaXQgZGIKICAgICAgICAgIC5zZWxlY3QoKQogICAgICAgICAgLmZyb20oYXV0aFByb3ZpZGVyQ29uZmlnKQogICAgICAgICAgLndoZXJlKGVxKGF1dGhQcm92aWRlckNvbmZpZy5lbmFibGVkLCB0cnVlKSkKICAgICAgICAgIC5saW1pdCgxKTsKICAgICAgICBicmVhazsKICAgICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgICAgZGJBdHRlbXB0Kys7CiAgICAgICAgaWYgKGRiQXR0ZW1wdCA+PSA1KSB0aHJvdyBlcnI7CiAgICAgICAgY29uc3QgZGVsYXkgPSBNYXRoLm1pbigxMDAwICogMiAqKiAoZGJBdHRlbXB0IC0gMSksIDhfMDAwKTsKICAgICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBEQiBxdWVyeSBhdHRlbXB0ICR7ZGJBdHRlbXB0fSBmYWlsZWQgKCR7ZXJyfSksIHJldHJ5aW5nIGluICR7ZGVsYXl9bXNgKTsKICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZSgocikgPT4gc2V0VGltZW91dChyLCBkZWxheSkpOwogICAgICB9CiAgICB9CiAgICBjb25zdCBbZGJDb25maWddID0gZGJRdWVyeVJvd3M7CgogICAgbGV0IHByb3ZpZGVyQ29uZmlnOiB7CiAgICAgIHByb3ZpZGVySWQ6IHN0cmluZzsKICAgICAgY2xpZW50SWQ6IHN0cmluZzsKICAgICAgY2xpZW50U2VjcmV0OiBzdHJpbmc7CiAgICAgIGlzc3VlclVybDogc3RyaW5nOwogICAgICBpbnRlcm5hbEJhc2VVcmw/OiBzdHJpbmc7CiAgICAgIHNjb3Blczogc3RyaW5nOwogICAgfTsKCiAgICBpZiAoZGJDb25maWcpIHsKICAgICAgLy8gU3RlcCAyOiBVc2UgREIgY29uZmlnIChkZWNyeXB0IGNsaWVudFNlY3JldCkKICAgICAgY29uc3QgZGVjcnlwdGVkU2VjcmV0ID0gZGVjcnlwdFNlY3JldChkYkNvbmZpZy5jbGllbnRTZWNyZXQpOwogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiBkYkNvbmZpZy5wcm92aWRlcklkLAogICAgICAgIGNsaWVudElkOiBkYkNvbmZpZy5jbGllbnRJZCwKICAgICAgICBjbGllbnRTZWNyZXQ6IGRlY3J5cHRlZFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IGRiQ29uZmlnLmlzc3VlclVybCwKICAgICAgICBpbnRlcm5hbEJhc2VVcmw6IGRiQ29uZmlnLmludGVybmFsQmFzZVVybCA/PyB1bmRlZmluZWQsCiAgICAgICAgc2NvcGVzOiBkYkNvbmZpZy5zY29wZXMsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgREIgY29uZmlnIGZvciBwcm92aWRlcjoiLCBkYkNvbmZpZy5wcm92aWRlcklkKTsKICAgIH0gZWxzZSB7CiAgICAgIC8vIFN0ZXAgMzogRmFsbCBiYWNrIHRvIGVudiB2YXJzCiAgICAgIGNvbnN0IG9pZGNJc3N1ZXIgPSBwcm9jZXNzLmVudi5PSURDX0lTU1VFUjsKICAgICAgY29uc3Qgb2lkY0NsaWVudElkID0gcHJvY2Vzcy5lbnYuT0lEQ19DTElFTlRfSUQ7CiAgICAgIGNvbnN0IG9pZGNDbGllbnRTZWNyZXQgPSBwcm9jZXNzLmVudi5PSURDX0NMSUVOVF9TRUNSRVQ7CgogICAgICBpZiAoIW9pZGNJc3N1ZXIgfHwgIW9pZGNDbGllbnRJZCB8fCAhb2lkY0NsaWVudFNlY3JldCkgewogICAgICAgIC8vIFN0ZXAgNDogTmVpdGhlciBEQiBjb25maWcgbm9yIGVudiB2YXJzIOKAlCBhdXRoIGlzIHVuY29uZmlndXJlZAogICAgICAgIGNvbnNvbGUud2FybigKICAgICAgICAgICJbYXV0aF0gTm8gYXV0aCBwcm92aWRlciBjb25maWd1cmVkLiBTZXQgdXAgYXV0aF9wcm92aWRlcl9jb25maWcgaW4gREIgb3IgT0lEQ18qIGVudiB2YXJzLiIKICAgICAgICApOwogICAgICAgIHJldHVybjsgLy8gYXV0aEluc3RhbmNlIHN0YXlzIG51bGwg4oCUIEFVVEhfRElTQUJMRUQgbW9kZQogICAgICB9CgogICAgICBwcm92aWRlckNvbmZpZyA9IHsKICAgICAgICBwcm92aWRlcklkOiAiYXV0aGVudGlrIiwKICAgICAgICBjbGllbnRJZDogb2lkY0NsaWVudElkLAogICAgICAgIGNsaWVudFNlY3JldDogb2lkY0NsaWVudFNlY3JldCwKICAgICAgICBpc3N1ZXJVcmw6IG9pZGNJc3N1ZXIsCiAgICAgICAgaW50ZXJuYWxCYXNlVXJsOiBwcm9jZXNzLmVudi5PSURDX0lOVEVSTkFMX0JBU0UsCiAgICAgICAgc2NvcGVzOiAib3BlbmlkIHByb2ZpbGUgZW1haWwgcm9sZSIsCiAgICAgIH07CiAgICAgIGNvbnNvbGUubG9nKCJbYXV0aF0gVXNpbmcgZW52IHZhciBjb25maWcgKG5vIERCIGNvbmZpZyBmb3VuZCkiKTsKICAgIH0KCiAgICBjb25zdCBoYXNHb29nbGUgPSAhIShwcm9jZXNzLmVudi5HT09HTEVfQ0xJRU5UX0lEICYmIHByb2Nlc3MuZW52LkdPT0dMRV9DTElFTlRfU0VDUkVUKTsKICAgIGNvbnN0IGhhc0dpdEh1YiA9ICEhKHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQgJiYgcHJvY2Vzcy5lbnYuR0lUSFVCX0NMSUVOVF9TRUNSRVQpOwoKICAgIGNvbnN0IGlzc3VlclVybE9iaiA9IG5ldyBVUkwocHJvdmlkZXJDb25maWcuaXNzdWVyVXJsKTsKICAgIGNvbnN0IGlzc3Vlckhvc3RuYW1lID0gaXNzdWVyVXJsT2JqLmhvc3RuYW1lOwoKICAgIGNvbnN0IGRpc2NvdmVyeVVybFN0ciA9IGAke3Byb3ZpZGVyQ29uZmlnLmlzc3VlclVybH0vLndlbGwta25vd24vb3BlbmlkLWNvbmZpZ3VyYXRpb25gOwogICAgbGV0IG9pZGNDb25maWc6IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gPSB7fTsKICAgIHRyeSB7CiAgICAgIGNvbnN0IGRpc2NvdmVyeVJlcyA9IGF3YWl0IGZldGNoKGRpc2NvdmVyeVVybFN0ciwgewogICAgICAgIHNpZ25hbDogQWJvcnRTaWduYWwudGltZW91dCg1MDAwKSwKICAgICAgfSk7CiAgICAgIGlmIChkaXNjb3ZlcnlSZXMub2spIHsKICAgICAgICBjb25zdCBkaXNjb3ZlcnkgPSBhd2FpdCBkaXNjb3ZlcnlSZXMuanNvbigpIGFzIHsKICAgICAgICAgIGF1dGhvcml6YXRpb25fZW5kcG9pbnQ/OiBzdHJpbmc7CiAgICAgICAgICB0b2tlbl9lbmRwb2ludD86IHN0cmluZzsKICAgICAgICAgIHVzZXJpbmZvX2VuZHBvaW50Pzogc3RyaW5nOwogICAgICAgIH07CiAgICAgICAgY29uc3QgcmVwbGFjZUhvc3QgPSAodXJsOiBzdHJpbmcsIG5ld0hvc3Q6IHN0cmluZykgPT4gewogICAgICAgICAgdHJ5IHsKICAgICAgICAgICAgY29uc3QgcGFyc2VkID0gbmV3IFVSTCh1cmwpOwogICAgICAgICAgICBjb25zdCBuZXdQYXJzZWQgPSBuZXcgVVJMKG5ld0hvc3QpOwogICAgICAgICAgICByZXR1cm4gYCR7bmV3UGFyc2VkLm9yaWdpbn0ke3BhcnNlZC5wYXRobmFtZX0ke3BhcnNlZC5zZWFyY2h9YDsKICAgICAgICAgIH0gY2F0Y2ggewogICAgICAgICAgICByZXR1cm4gdXJsOwogICAgICAgICAgfQogICAgICAgIH07CiAgICAgICAgY29uc3QgYXV0aHpVcmwgPSBkaXNjb3ZlcnkuYXV0aG9yaXphdGlvbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB0b2tlblVybCA9IGRpc2NvdmVyeS50b2tlbl9lbmRwb2ludDsKICAgICAgICBjb25zdCB1c2VySW5mb1VybCA9IGRpc2NvdmVyeS51c2VyaW5mb19lbmRwb2ludDsKICAgICAgICBpZiAoYXV0aHpVcmwgJiYgdG9rZW5VcmwgJiYgdXNlckluZm9VcmwpIHsKICAgICAgICAgIGNvbnN0IGF1dGh6VXJsT2JqID0gbmV3IFVSTChhdXRoelVybCk7CiAgICAgICAgICAvLyBPbmx5IHZhbGlkYXRlIGF1dGhvcml6YXRpb25VcmwgaG9zdG5hbWUgYWdhaW5zdCBpc3N1ZXIg4oCUIHRva2VuL3VzZXJpbmZvCiAgICAgICAgICAvLyBtYXkgbGVnaXRpbWF0ZWx5IHVzZSBpbnRlcm5hbCBob3N0bmFtZXMgKE9JRENfSU5URVJOQUxfQkFTRSkgZm9yIHNlcnZlci10by1zZXJ2ZXIgY2FsbHMuCiAgICAgICAgICBpZiAoYXV0aHpVcmxPYmouaG9zdG5hbWUgIT09IGlzc3Vlckhvc3RuYW1lKSB7CiAgICAgICAgICAgIHRocm93IG5ldyBFcnJvcigKICAgICAgICAgICAgICBgW0ZBVEFMXSBPSURDIGRpc2NvdmVyeSBVUkwgaG9zdG5hbWUgbWlzbWF0Y2g6IGV4cGVjdGVkICcke2lzc3Vlckhvc3RuYW1lfScgYnV0IGdvdCAnJHthdXRoelVybE9iai5ob3N0bmFtZX0nLiBUaGlzIG1heSBpbmRpY2F0ZSBhIG1hbi1pbi10aGUtbWlkZGxlIGF0dGFjay5gCiAgICAgICAgICAgICk7CiAgICAgICAgICB9CiAgICAgICAgICBvaWRjQ29uZmlnID0gewogICAgICAgICAgICBhdXRob3JpemF0aW9uVXJsOiBhdXRoelVybCwKICAgICAgICAgICAgdG9rZW5Vcmw6IHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybAogICAgICAgICAgICAgID8gcmVwbGFjZUhvc3QodG9rZW5VcmwsIHByb3ZpZGVyQ29uZmlnLmludGVybmFsQmFzZVVybCkKICAgICAgICAgICAgICA6IHRva2VuVXJsLAogICAgICAgICAgICB1c2VySW5mb1VybDogcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsCiAgICAgICAgICAgICAgPyByZXBsYWNlSG9zdCh1c2VySW5mb1VybCwgcHJvdmlkZXJDb25maWcuaW50ZXJuYWxCYXNlVXJsKQogICAgICAgICAgICAgIDogdXNlckluZm9VcmwsCiAgICAgICAgICB9OwogICAgICAgICAgY29uc29sZS5sb2coIlthdXRoXSBPSURDIGRpc2NvdmVyeSBzdWNjZXNzZnVsLCBwcm92aWRlcjoiLCBwcm92aWRlckNvbmZpZy5wcm92aWRlcklkKTsKICAgICAgICB9IGVsc2UgewogICAgICAgICAgY29uc29sZS53YXJuKCJbYXV0aF0gT0lEQyBkaXNjb3ZlcnkgbWlzc2luZyByZXF1aXJlZCBlbmRwb2ludHMsIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5Iik7CiAgICAgICAgfQogICAgICB9IGVsc2UgewogICAgICAgIGNvbnNvbGUud2FybihgW2F1dGhdIE9JREMgZGlzY292ZXJ5IGZhaWxlZCAoJHtkaXNjb3ZlcnlSZXMuc3RhdHVzfSksIHVzaW5nIGRpc2NvdmVyeVVybCBvbmx5YCk7CiAgICAgIH0KICAgIH0gY2F0Y2ggKGVycikgewogICAgICBjb25zb2xlLndhcm4oYFthdXRoXSBPSURDIGRpc2NvdmVyeSBmZXRjaCBmYWlsZWQ6ICR7ZXJyfSwgdXNpbmcgZGlzY292ZXJ5VXJsIG9ubHlgKTsKICAgIH0KCiAgICAvLyBCdWlsZCBCZXR0ZXItQXV0aCBpbnN0YW5jZSB1c2luZyByZXNvbHZlZCBjb25maWcKICAgIGF1dGhJbnN0YW5jZSA9IGJldHRlckF1dGgoewogICAgICBkYXRhYmFzZTogZHJpenpsZUFkYXB0ZXIoZGIsIHsKICAgICAgICBwcm92aWRlcjogInBnIiwKICAgICAgfSksCiAgICAgIHNlY3JldDogQkVUVEVSX0FVVEhfU0VDUkVULAogICAgICBiYXNlVVJMOiBCRVRURVJfQVVUSF9VUkwsCiAgICAgIHJhdGVMaW1pdDogewogICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgbWF4OiAxMDAsCiAgICAgICAgd2luZG93OiAxMCwKICAgICAgICBzdG9yYWdlOiAibWVtb3J5IiwKICAgICAgICBjdXN0b21SdWxlczogewogICAgICAgICAgIi9nZXQtc2Vzc2lvbiI6IGZhbHNlLAogICAgICAgIH0sCiAgICAgIH0sCiAgICAgIGFjY291bnQ6IHsKICAgICAgICBhY2NvdW50TGlua2luZzogewogICAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICAgIHRydXN0ZWRQcm92aWRlcnM6IFsiYXV0aGVudGlrIl0sCiAgICAgICAgfSwKICAgICAgICBzdG9yZVN0YXRlU3RyYXRlZ3k6ICJjb29raWUiIGFzIGNvbnN0LAogICAgICB9LAogICAgICBlbWFpbEFuZFBhc3N3b3JkOiB7CiAgICAgICAgZW5hYmxlZDogdHJ1ZSwKICAgICAgICBlbWFpbFZlcmlmaWNhdGlvbjogewogICAgICAgICAgc2VuZFZlcmlmaWNhdGlvbkVtYWlsOiBhc3luYyAoeyB1c2VyLCB1cmwgfTogeyB1c2VyOiB7IGVtYWlsOiBzdHJpbmcgfTsgdXJsOiBzdHJpbmcgfSkgPT4gewogICAgICAgICAgICBhd2FpdCBzZW5kRW1haWwoewogICAgICAgICAgICAgIHRvOiB1c2VyLmVtYWlsLAogICAgICAgICAgICAgIHN1YmplY3Q6ICJWZXJpZnkgeW91ciBHcm9vbUJvb2sgZW1haWwiLAogICAgICAgICAgICAgIHRleHQ6IGBDbGljayB0aGUgbGluayB0byB2ZXJpZnkgeW91ciBlbWFpbDogJHt1cmx9YCwKICAgICAgICAgICAgICBodG1sOiBgPHA+Q2xpY2sgdGhlIGxpbmsgdG8gdmVyaWZ5IHlvdXIgZW1haWw6PC9wPjxhIGhyZWY9IiR7dXJsfSI+JHt1cmx9PC9hPmAsCiAgICAgICAgICAgIH0pOwogICAgICAgICAgfSwKICAgICAgICB9LAogICAgICB9LAogICAgICBwbHVnaW5zOiBbCiAgICAgICAgZ2VuZXJpY09BdXRoKHsKICAgICAgICAgIGNvbmZpZzogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgcHJvdmlkZXJJZDogcHJvdmlkZXJDb25maWcucHJvdmlkZXJJZCwKICAgICAgICAgICAgICBjbGllbnRJZDogcHJvdmlkZXJDb25maWcuY2xpZW50SWQsCiAgICAgICAgICAgICAgY2xpZW50U2VjcmV0OiBwcm92aWRlckNvbmZpZy5jbGllbnRTZWNyZXQsCiAgICAgICAgICAgICAgZGlzY292ZXJ5VXJsOiBkaXNjb3ZlcnlVcmxTdHIsCiAgICAgICAgICAgICAgLi4uKE9iamVjdC5rZXlzKG9pZGNDb25maWcpLmxlbmd0aCA+IDAgPyBvaWRjQ29uZmlnIDoge30pLAogICAgICAgICAgICAgIHNjb3BlczogcHJvdmlkZXJDb25maWcuc2NvcGVzLnNwbGl0KCIgIikuZmlsdGVyKEJvb2xlYW4pLAogICAgICAgICAgICB9LAogICAgICAgICAgXSwKICAgICAgICB9KSwKICAgICAgXSwKICAgICAgc29jaWFsUHJvdmlkZXJzOiB7CiAgICAgICAgLi4uKGhhc0dvb2dsZSA/IHsKICAgICAgICAgIGdvb2dsZTogewogICAgICAgICAgICBjbGllbnRJZDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9JRCEsCiAgICAgICAgICAgIGNsaWVudFNlY3JldDogcHJvY2Vzcy5lbnYuR09PR0xFX0NMSUVOVF9TRUNSRVQhLAogICAgICAgICAgfSwKICAgICAgICB9IDoge30pLAogICAgICAgIC4uLihoYXNHaXRIdWIgPyB7CiAgICAgICAgICBnaXRodWI6IHsKICAgICAgICAgICAgY2xpZW50SWQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfSUQhLAogICAgICAgICAgICBjbGllbnRTZWNyZXQ6IHByb2Nlc3MuZW52LkdJVEhVQl9DTElFTlRfU0VDUkVUISwKICAgICAgICAgIH0sCiAgICAgICAgfSA6IHt9KSwKICAgICAgfSwKICAgICAgc2Vzc2lvbjogewogICAgICAgIGV4cGlyZXNJbjogNjAgKiA2MCAqIDI0ICogNywgLy8gNyBkYXlzCiAgICAgICAgdXBkYXRlQWdlOiA2MCAqIDYwICogMjQsIC8vIDEgZGF5CiAgICAgICAgY29va2llQ2FjaGU6IHsKICAgICAgICAgIGVuYWJsZWQ6IHRydWUsCiAgICAgICAgICBtYXhBZ2U6IDUgKiA2MCwgLy8gNSBtaW51dGVzCiAgICAgICAgfSwKICAgICAgfSwKICAgICAgdHJ1c3RlZE9yaWdpbnM6IChwcm9jZXNzLmVudi5DT1JTX09SSUdJTiA/PyAiaHR0cDovL2xvY2FsaG9zdDo1MTczIikKICAgICAgICAuc3BsaXQoIiwiKS5tYXAoKHMpID0+IHMudHJpbSgpKS5maWx0ZXIoQm9vbGVhbiksCiAgICB9KTsKICB9KSgpOwoKICB0cnkgewogICAgYXdhaXQgYXV0aEluaXRQcm9taXNlOwogIH0gY2F0Y2ggKGVycikgewogICAgYXV0aEluaXRQcm9taXNlID0gbnVsbDsgLy8gYWxsb3cgcmV0cnkgb24gbmV4dCBjYWxsCiAgICB0aHJvdyBlcnI7CiAgfQp9Cg== \ No newline at end of file +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { genericOAuth } from "better-auth/plugins"; +import { getDb, authProviderConfig, eq } from "@groombook/db"; +import { decryptSecret } from "@groombook/db"; +import { sendEmail } from "../services/email.js"; + +const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET; +const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000"; + +// Auth instance — initialized lazily via initAuth() +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let authInstance: any = null; +let authInitPromise: Promise | null = null; + +/** Returns the current auth instance. Throws if not yet initialized. */ +export function getAuth() { + if (!authInstance) { + throw new Error( + "Auth not initialized. Call initAuth() at startup before handling requests." + ); + } + return authInstance; +} + +/** Returns a promise that resolves when auth is initialized. */ +export function getAuthPromise() { + return authInitPromise; +} + +/** Returns which OAuth/social providers are configured via env vars. */ +export function getActiveProviders(): string[] { + const providers: string[] = []; + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + providers.push("google"); + } + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + providers.push("github"); + } + if (process.env.OIDC_ISSUER && process.env.OIDC_CLIENT_ID && process.env.OIDC_CLIENT_SECRET) { + providers.push("authentik"); + } + return providers; +} + +/** + * Re-initializes the Better-Auth instance after auth config changes. + * + * Clears both authInstance and authInitPromise, then calls initAuth() to + * re-read config from DB and build a fresh Better-Auth instance. + * Sessions are DB-backed and survive the re-init. + */ +export async function reinitAuth(): Promise { + authInstance = null; + authInitPromise = null; + await initAuth(); + console.log("[auth] Re-initialized auth instance after config change"); +} + +/** + * Initializes the Better-Auth instance. + * + * Config resolution chain: + * 1. Query auth_provider_config table for an enabled provider + * 2. If DB config exists → use it (decrypt clientSecret) + * 3. If no DB config → fall back to OIDC_* env vars + * 4. If neither → auth is unconfigured (getAuth() returns null, AUTH_DISABLED implied) + * + * Idempotent — subsequent calls return immediately after initialization completes. + */ +export async function initAuth(): Promise { + if (authInstance) return; // Already initialized + if (authInitPromise) { + await authInitPromise; + return; + } + + authInitPromise = (async () => { + // Guard: require BETTER_AUTH_SECRET unless explicitly in dev/demo mode + if (!BETTER_AUTH_SECRET && process.env.AUTH_DISABLED !== "true") { + throw new Error( + "[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled" + ); + } + + // AUTH_DISABLED=true means dev/demo mode — still build Better-Auth with placeholder + // config so auth.handler exists (middleware bypasses it anyway) + if (process.env.AUTH_DISABLED === "true") { + console.warn("[auth] AUTH_DISABLED=true — building placeholder auth instance"); + authInstance = betterAuth({ + database: drizzleAdapter(getDb(), { provider: "pg" }), + secret: BETTER_AUTH_SECRET!, + baseURL: BETTER_AUTH_URL, + rateLimit: { + enabled: true, + max: 100, + window: 10, + storage: "memory", + customRules: { + "/get-session": false, + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: "authentik", + clientId: "placeholder", + clientSecret: "placeholder", + discoveryUrl: undefined, + scopes: ["openid", "profile", "email"], + }, + ], + }), + ], + session: { + expiresIn: 60 * 60 * 24 * 7, + updateAge: 60 * 60 * 24, + cookieCache: { enabled: false }, + }, + trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") + .split(",").map((s) => s.trim()).filter(Boolean), + }); + return; + } + + // Step 1: Try to load config from DB, with retry-with-backoff for transient ECONNRESET (GRO-2652). + // A single connection reset during boot must not abort initialization. + const db = getDb(); + let dbQueryRows: (typeof authProviderConfig.$inferSelect)[] = []; + let dbAttempt = 0; + while (true) { + try { + dbQueryRows = await db + .select() + .from(authProviderConfig) + .where(eq(authProviderConfig.enabled, true)) + .limit(1); + break; + } catch (err) { + dbAttempt++; + if (dbAttempt >= 5) throw err; + const delay = Math.min(1000 * 2 ** (dbAttempt - 1), 8_000); + console.warn(`[auth] DB query attempt ${dbAttempt} failed (${err}), retrying in ${delay}ms`); + await new Promise((r) => setTimeout(r, delay)); + } + } + const [dbConfig] = dbQueryRows; + + let providerConfig: { + providerId: string; + clientId: string; + clientSecret: string; + issuerUrl: string; + internalBaseUrl?: string; + scopes: string; + }; + + if (dbConfig) { + // Step 2: Use DB config (decrypt clientSecret) + const decryptedSecret = decryptSecret(dbConfig.clientSecret); + providerConfig = { + providerId: dbConfig.providerId, + clientId: dbConfig.clientId, + clientSecret: decryptedSecret, + issuerUrl: dbConfig.issuerUrl, + internalBaseUrl: dbConfig.internalBaseUrl ?? undefined, + scopes: dbConfig.scopes, + }; + console.log("[auth] Using DB config for provider:", dbConfig.providerId); + } else { + // Step 3: Fall back to env vars + const oidcIssuer = process.env.OIDC_ISSUER; + const oidcClientId = process.env.OIDC_CLIENT_ID; + const oidcClientSecret = process.env.OIDC_CLIENT_SECRET; + + if (!oidcIssuer || !oidcClientId || !oidcClientSecret) { + // Step 4: Neither DB config nor env vars — auth is unconfigured + console.warn( + "[auth] No auth provider configured. Set up auth_provider_config in DB or OIDC_* env vars." + ); + return; // authInstance stays null — AUTH_DISABLED mode + } + + providerConfig = { + providerId: "authentik", + clientId: oidcClientId, + clientSecret: oidcClientSecret, + issuerUrl: oidcIssuer, + internalBaseUrl: process.env.OIDC_INTERNAL_BASE, + scopes: "openid profile email role", + }; + console.log("[auth] Using env var config (no DB config found)"); + } + + const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); + const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); + + const issuerUrlObj = new URL(providerConfig.issuerUrl); + const issuerHostname = issuerUrlObj.hostname; + + const discoveryUrlStr = `${providerConfig.issuerUrl}/.well-known/openid-configuration`; + let oidcConfig: Record = {}; + try { + const discoveryRes = await fetch(discoveryUrlStr, { + signal: AbortSignal.timeout(5000), + }); + if (discoveryRes.ok) { + const discovery = await discoveryRes.json() as { + authorization_endpoint?: string; + token_endpoint?: string; + userinfo_endpoint?: string; + }; + const replaceHost = (url: string, newHost: string) => { + try { + const parsed = new URL(url); + const newParsed = new URL(newHost); + return `${newParsed.origin}${parsed.pathname}${parsed.search}`; + } catch { + return url; + } + }; + const authzUrl = discovery.authorization_endpoint; + const tokenUrl = discovery.token_endpoint; + const userInfoUrl = discovery.userinfo_endpoint; + if (authzUrl && tokenUrl && userInfoUrl) { + const authzUrlObj = new URL(authzUrl); + // Only validate authorizationUrl hostname against issuer — token/userinfo + // may legitimately use internal hostnames (OIDC_INTERNAL_BASE) for server-to-server calls. + if (authzUrlObj.hostname !== issuerHostname) { + throw new Error( + `[FATAL] OIDC discovery URL hostname mismatch: expected '${issuerHostname}' but got '${authzUrlObj.hostname}'. This may indicate a man-in-the-middle attack.` + ); + } + oidcConfig = { + authorizationUrl: authzUrl, + tokenUrl: providerConfig.internalBaseUrl + ? replaceHost(tokenUrl, providerConfig.internalBaseUrl) + : tokenUrl, + userInfoUrl: providerConfig.internalBaseUrl + ? replaceHost(userInfoUrl, providerConfig.internalBaseUrl) + : userInfoUrl, + }; + console.log("[auth] OIDC discovery successful, provider:", providerConfig.providerId); + } else { + console.warn("[auth] OIDC discovery missing required endpoints, using discoveryUrl only"); + } + } else { + console.warn(`[auth] OIDC discovery failed (${discoveryRes.status}), using discoveryUrl only`); + } + } catch (err) { + console.warn(`[auth] OIDC discovery fetch failed: ${err}, using discoveryUrl only`); + } + + // Build Better-Auth instance using resolved config + authInstance = betterAuth({ + database: drizzleAdapter(db, { + provider: "pg", + }), + secret: BETTER_AUTH_SECRET, + baseURL: BETTER_AUTH_URL, + rateLimit: { + enabled: true, + max: 100, + window: 10, + storage: "memory", + customRules: { + "/get-session": false, + }, + }, + account: { + accountLinking: { + enabled: true, + trustedProviders: ["authentik"], + }, + storeStateStrategy: "cookie" as const, + }, + emailAndPassword: { + enabled: true, + emailVerification: { + sendVerificationEmail: async ({ user, url }: { user: { email: string }; url: string }) => { + await sendEmail({ + to: user.email, + subject: "Verify your GroomBook email", + text: `Click the link to verify your email: ${url}`, + html: `

Click the link to verify your email:

${url}`, + }); + }, + }, + }, + plugins: [ + genericOAuth({ + config: [ + { + providerId: providerConfig.providerId, + clientId: providerConfig.clientId, + clientSecret: providerConfig.clientSecret, + discoveryUrl: discoveryUrlStr, + ...(Object.keys(oidcConfig).length > 0 ? oidcConfig : {}), + scopes: providerConfig.scopes.split(" ").filter(Boolean), + }, + ], + }), + ], + socialProviders: { + ...(hasGoogle ? { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + } : {}), + ...(hasGitHub ? { + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + }, + } : {}), + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24, // 1 day + cookieCache: { + enabled: true, + maxAge: 5 * 60, // 5 minutes + }, + }, + trustedOrigins: (process.env.CORS_ORIGIN ?? "http://localhost:5173") + .split(",").map((s) => s.trim()).filter(Boolean), + }); + })(); + + try { + await authInitPromise; + } catch (err) { + authInitPromise = null; // allow retry on next call + throw err; + } +}