@tempoxyz/id 0.0.0-pr-187.sha-eed04d3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/dist/App.d.ts +20 -0
- package/dist/App.d.ts.map +1 -0
- package/dist/App.js +26 -0
- package/dist/App.js.map +1 -0
- package/dist/app/client/.assetsignore +2 -0
- package/dist/app/client/assets/BRLA-CZvVDNYr.png +0 -0
- package/dist/app/client/assets/CHFAU-DCedc4aH.png +0 -0
- package/dist/app/client/assets/DLUSD-BnRXm3EI.png +0 -0
- package/dist/app/client/assets/PRIME-CiFp2-3e.svg +14 -0
- package/dist/app/client/assets/Pilat-Bold-C1t1eIrF.woff2 +0 -0
- package/dist/app/client/assets/Pilat-Book-CbS8ofT8.woff2 +0 -0
- package/dist/app/client/assets/Pilat-Demi-CADSbPAt.woff2 +0 -0
- package/dist/app/client/assets/Pilat-Regular-Bx8vzE7q.woff2 +0 -0
- package/dist/app/client/assets/USD1-COVcO_PB.svg +30 -0
- package/dist/app/client/assets/YLDS-CRh8Zw0g.svg +3 -0
- package/dist/app/client/assets/ccip-Cq1mMiVl.js +1 -0
- package/dist/app/client/assets/frxUSD-n9pTpinR.svg +1 -0
- package/dist/app/client/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/app/client/assets/index-B7YJVP00.css +1 -0
- package/dist/app/client/assets/index-UXTxXCLk.js +55 -0
- package/dist/app/client/assets/pathUSD-BvaO6ooS.svg +25 -0
- package/dist/app/client/assets/reset-co-CHrff.css +1 -0
- package/dist/app/client/assets/wYLDS-CaCKDrO-.svg +10 -0
- package/dist/app/db/auth.d.ts +3207 -0
- package/dist/app/db/auth.d.ts.map +1 -0
- package/dist/app/db/auth.js +330 -0
- package/dist/app/db/auth.js.map +1 -0
- package/dist/app/db/schema.d.ts +932 -0
- package/dist/app/db/schema.d.ts.map +1 -0
- package/dist/app/db/schema.js +171 -0
- package/dist/app/db/schema.js.map +1 -0
- package/dist/app/server/.vite/manifest.json +489 -0
- package/dist/app/server/assets/BRLA-CZvVDNYr.png +0 -0
- package/dist/app/server/assets/CHFAU-DCedc4aH.png +0 -0
- package/dist/app/server/assets/DLUSD-BnRXm3EI.png +0 -0
- package/dist/app/server/assets/PRIME-CiFp2-3e.svg +14 -0
- package/dist/app/server/assets/Pilat-Bold-C1t1eIrF.woff2 +0 -0
- package/dist/app/server/assets/Pilat-Book-CbS8ofT8.woff2 +0 -0
- package/dist/app/server/assets/Pilat-Demi-CADSbPAt.woff2 +0 -0
- package/dist/app/server/assets/Pilat-Regular-Bx8vzE7q.woff2 +0 -0
- package/dist/app/server/assets/USD1-COVcO_PB.svg +30 -0
- package/dist/app/server/assets/YLDS-CRh8Zw0g.svg +3 -0
- package/dist/app/server/assets/_tanstack-start-manifest_v-DjQNeRZp.js +1 -0
- package/dist/app/server/assets/arktype-aI7TBD0R-D1R3Byo5.js +2 -0
- package/dist/app/server/assets/arktype-aI7TBD0R-D1R3Byo5.js.map +1 -0
- package/dist/app/server/assets/authorize-C95F-9E1-CbESJKGU.js +31 -0
- package/dist/app/server/assets/authorize-C95F-9E1-CbESJKGU.js.map +1 -0
- package/dist/app/server/assets/bun-sqlite-dialect-DoOVo4ja-CtyBkVMb.js +2 -0
- package/dist/app/server/assets/bun-sqlite-dialect-DoOVo4ja-CtyBkVMb.js.map +1 -0
- package/dist/app/server/assets/call-UQamaCKN.js +32 -0
- package/dist/app/server/assets/call-UQamaCKN.js.map +1 -0
- package/dist/app/server/assets/ccip-CbNKFO4n.js +2 -0
- package/dist/app/server/assets/ccip-CbNKFO4n.js.map +1 -0
- package/dist/app/server/assets/compiled-query-Ds2NVcFT.js +2 -0
- package/dist/app/server/assets/compiled-query-Ds2NVcFT.js.map +1 -0
- package/dist/app/server/assets/convert-BIC8n32x.js +2 -0
- package/dist/app/server/assets/convert-BIC8n32x.js.map +1 -0
- package/dist/app/server/assets/cookie-CS900nSP.css +1 -0
- package/dist/app/server/assets/cookie-Cce9CrIP.js +10 -0
- package/dist/app/server/assets/cookie-Cce9CrIP.js.map +1 -0
- package/dist/app/server/assets/core-B1HCXUmx.js +56 -0
- package/dist/app/server/assets/core-B1HCXUmx.js.map +1 -0
- package/dist/app/server/assets/d1-sqlite-dialect-m0f01-4z-Cv-csE1u.js +2 -0
- package/dist/app/server/assets/d1-sqlite-dialect-m0f01-4z-Cv-csE1u.js.map +1 -0
- package/dist/app/server/assets/default-u_dwuiYb-BJJ9PwCi.js +2 -0
- package/dist/app/server/assets/default-u_dwuiYb-BJJ9PwCi.js.map +1 -0
- package/dist/app/server/assets/dist-BxSwugSY.js +1 -0
- package/dist/app/server/assets/dist-CZGFhmQd.js +2 -0
- package/dist/app/server/assets/dist-CZGFhmQd.js.map +1 -0
- package/dist/app/server/assets/dist-Dg3PQqiW.js +15 -0
- package/dist/app/server/assets/dist-Dg3PQqiW.js.map +1 -0
- package/dist/app/server/assets/effect-QlVUlMFu-2tSPDT2b.js +2 -0
- package/dist/app/server/assets/effect-QlVUlMFu-2tSPDT2b.js.map +1 -0
- package/dist/app/server/assets/empty-plugin-adapters-BaGcmrjC.js +2 -0
- package/dist/app/server/assets/empty-plugin-adapters-BaGcmrjC.js.map +1 -0
- package/dist/app/server/assets/factory-ipHlJXZY.js +5 -0
- package/dist/app/server/assets/factory-ipHlJXZY.js.map +1 -0
- package/dist/app/server/assets/frxUSD-n9pTpinR.svg +1 -0
- package/dist/app/server/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/dist/app/server/assets/get-id-field-B0x109VU.js +6 -0
- package/dist/app/server/assets/get-id-field-B0x109VU.js.map +1 -0
- package/dist/app/server/assets/index-CLddUTqr-xpSuLpBt.js +2 -0
- package/dist/app/server/assets/index-CLddUTqr-xpSuLpBt.js.map +1 -0
- package/dist/app/server/assets/index-DZEfthgZ-8Uq5s5Pt.js +2 -0
- package/dist/app/server/assets/index-DZEfthgZ-8Uq5s5Pt.js.map +1 -0
- package/dist/app/server/assets/kysely-adapter-MkHAI9Jk.js +1 -0
- package/dist/app/server/assets/node-sqlite-dialect-BowWLXvK.js +2 -0
- package/dist/app/server/assets/node-sqlite-dialect-BowWLXvK.js.map +1 -0
- package/dist/app/server/assets/pathUSD-BvaO6ooS.svg +25 -0
- package/dist/app/server/assets/reset-co-CHrff.css +1 -0
- package/dist/app/server/assets/rolldown-runtime-B0Z9INg1.js +1 -0
- package/dist/app/server/assets/router-CcbyBCI4.css +1 -0
- package/dist/app/server/assets/router-xjZtEeaI.js +7 -0
- package/dist/app/server/assets/router-xjZtEeaI.js.map +1 -0
- package/dist/app/server/assets/schema-check-Cf7QOZZD.js +9 -0
- package/dist/app/server/assets/schema-check-Cf7QOZZD.js.map +1 -0
- package/dist/app/server/assets/schemas-C2lCYmR5.js +2 -0
- package/dist/app/server/assets/schemas-C2lCYmR5.js.map +1 -0
- package/dist/app/server/assets/secp256k1-buM5g4AL.js +2 -0
- package/dist/app/server/assets/secp256k1-buM5g4AL.js.map +1 -0
- package/dist/app/server/assets/sqlite-adapter-C_LAV4e3.js +2 -0
- package/dist/app/server/assets/sqlite-adapter-C_LAV4e3.js.map +1 -0
- package/dist/app/server/assets/sqlite-introspector-DSCAP82F-Crx7Ny14.js +6 -0
- package/dist/app/server/assets/sqlite-introspector-DSCAP82F-Crx7Ny14.js.map +1 -0
- package/dist/app/server/assets/standard-json-Cg1N07hM.js +2 -0
- package/dist/app/server/assets/standard-json-Cg1N07hM.js.map +1 -0
- package/dist/app/server/assets/standard-json-DY77efq0.js +2 -0
- package/dist/app/server/assets/standard-json-DY77efq0.js.map +1 -0
- package/dist/app/server/assets/standard-json-ig2t0wRt.js +2 -0
- package/dist/app/server/assets/standard-json-ig2t0wRt.js.map +1 -0
- package/dist/app/server/assets/standard-json-t-LNckA6.js +2 -0
- package/dist/app/server/assets/standard-json-t-LNckA6.js.map +1 -0
- package/dist/app/server/assets/standard-openapi-CONmVRl8.js +2 -0
- package/dist/app/server/assets/standard-openapi-CONmVRl8.js.map +1 -0
- package/dist/app/server/assets/start-C63VetBp.js +2 -0
- package/dist/app/server/assets/start-C63VetBp.js.map +1 -0
- package/dist/app/server/assets/sury-CWZTCd75-xt1mWc9f.js +2 -0
- package/dist/app/server/assets/sury-CWZTCd75-xt1mWc9f.js.map +1 -0
- package/dist/app/server/assets/typebox-Dei93FPO-D_lCVdAz.js +2 -0
- package/dist/app/server/assets/typebox-Dei93FPO-D_lCVdAz.js.map +1 -0
- package/dist/app/server/assets/utils-C4ZXZIZW.js +2 -0
- package/dist/app/server/assets/utils-C4ZXZIZW.js.map +1 -0
- package/dist/app/server/assets/valibot--1zFm7rT-NSoY2xTp.js +2 -0
- package/dist/app/server/assets/valibot--1zFm7rT-NSoY2xTp.js.map +1 -0
- package/dist/app/server/assets/valibot-D_HTw1Gn-CJwWeOUE.js +2 -0
- package/dist/app/server/assets/valibot-D_HTw1Gn-CJwWeOUE.js.map +1 -0
- package/dist/app/server/assets/wYLDS-CaCKDrO-.svg +10 -0
- package/dist/app/server/assets/zod-Bwrt9trS-CV38yPnQ.js +2 -0
- package/dist/app/server/assets/zod-Bwrt9trS-CV38yPnQ.js.map +1 -0
- package/dist/app/server/assets/zod-DSgpEGAE-BvlnBE2L.js +2 -0
- package/dist/app/server/assets/zod-DSgpEGAE-BvlnBE2L.js.map +1 -0
- package/dist/app/server/index.js +574 -0
- package/dist/app/server/index.js.map +1 -0
- package/dist/app/server/wrangler.json +1 -0
- package/dist/cli/Admin.d.ts +38 -0
- package/dist/cli/Admin.d.ts.map +1 -0
- package/dist/cli/Admin.js +62 -0
- package/dist/cli/Admin.js.map +1 -0
- package/dist/cli/Apps.d.ts +9 -0
- package/dist/cli/Apps.d.ts.map +1 -0
- package/dist/cli/Apps.js +36 -0
- package/dist/cli/Apps.js.map +1 -0
- package/dist/cli/Cli.d.ts +14 -0
- package/dist/cli/Cli.d.ts.map +1 -0
- package/dist/cli/Cli.js +27 -0
- package/dist/cli/Cli.js.map +1 -0
- package/dist/cli/Project.d.ts +20 -0
- package/dist/cli/Project.d.ts.map +1 -0
- package/dist/cli/Project.js +88 -0
- package/dist/cli/Project.js.map +1 -0
- package/dist/cli/Realms.d.ts +24 -0
- package/dist/cli/Realms.d.ts.map +1 -0
- package/dist/cli/Realms.js +49 -0
- package/dist/cli/Realms.js.map +1 -0
- package/dist/cli/Session.d.ts +85 -0
- package/dist/cli/Session.d.ts.map +1 -0
- package/dist/cli/Session.js +178 -0
- package/dist/cli/Session.js.map +1 -0
- package/dist/cli/User.d.ts +29 -0
- package/dist/cli/User.d.ts.map +1 -0
- package/dist/cli/User.js +39 -0
- package/dist/cli/User.js.map +1 -0
- package/dist/cli/V1.d.ts +35 -0
- package/dist/cli/V1.d.ts.map +1 -0
- package/dist/cli/V1.js +41 -0
- package/dist/cli/V1.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +46 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/admin/apps/create.d.ts +23 -0
- package/dist/cli/commands/admin/apps/create.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/create.js +89 -0
- package/dist/cli/commands/admin/apps/create.js.map +1 -0
- package/dist/cli/commands/admin/apps/delete.d.ts +14 -0
- package/dist/cli/commands/admin/apps/delete.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/delete.js +41 -0
- package/dist/cli/commands/admin/apps/delete.js.map +1 -0
- package/dist/cli/commands/admin/apps/index.d.ts +5 -0
- package/dist/cli/commands/admin/apps/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/index.js +14 -0
- package/dist/cli/commands/admin/apps/index.js.map +1 -0
- package/dist/cli/commands/admin/apps/list.d.ts +9 -0
- package/dist/cli/commands/admin/apps/list.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/list.js +26 -0
- package/dist/cli/commands/admin/apps/list.js.map +1 -0
- package/dist/cli/commands/admin/apps/rotate-secret.d.ts +16 -0
- package/dist/cli/commands/admin/apps/rotate-secret.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/rotate-secret.js +47 -0
- package/dist/cli/commands/admin/apps/rotate-secret.js.map +1 -0
- package/dist/cli/commands/admin/apps/update.d.ts +20 -0
- package/dist/cli/commands/admin/apps/update.d.ts.map +1 -0
- package/dist/cli/commands/admin/apps/update.js +76 -0
- package/dist/cli/commands/admin/apps/update.js.map +1 -0
- package/dist/cli/commands/admin/index.d.ts +5 -0
- package/dist/cli/commands/admin/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/index.js +13 -0
- package/dist/cli/commands/admin/index.js.map +1 -0
- package/dist/cli/commands/admin/migrate.d.ts +9 -0
- package/dist/cli/commands/admin/migrate.d.ts.map +1 -0
- package/dist/cli/commands/admin/migrate.js +17 -0
- package/dist/cli/commands/admin/migrate.js.map +1 -0
- package/dist/cli/commands/admin/realms/add.d.ts +12 -0
- package/dist/cli/commands/admin/realms/add.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/add.js +42 -0
- package/dist/cli/commands/admin/realms/add.js.map +1 -0
- package/dist/cli/commands/admin/realms/index.d.ts +5 -0
- package/dist/cli/commands/admin/realms/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/index.js +13 -0
- package/dist/cli/commands/admin/realms/index.js.map +1 -0
- package/dist/cli/commands/admin/realms/list.d.ts +13 -0
- package/dist/cli/commands/admin/realms/list.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/list.js +36 -0
- package/dist/cli/commands/admin/realms/list.js.map +1 -0
- package/dist/cli/commands/admin/realms/passkeys/index.d.ts +5 -0
- package/dist/cli/commands/admin/realms/passkeys/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/passkeys/index.js +13 -0
- package/dist/cli/commands/admin/realms/passkeys/index.js.map +1 -0
- package/dist/cli/commands/admin/realms/passkeys/set.d.ts +16 -0
- package/dist/cli/commands/admin/realms/passkeys/set.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/passkeys/set.js +46 -0
- package/dist/cli/commands/admin/realms/passkeys/set.js.map +1 -0
- package/dist/cli/commands/admin/realms/privy/index.d.ts +5 -0
- package/dist/cli/commands/admin/realms/privy/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/privy/index.js +13 -0
- package/dist/cli/commands/admin/realms/privy/index.js.map +1 -0
- package/dist/cli/commands/admin/realms/privy/set.d.ts +18 -0
- package/dist/cli/commands/admin/realms/privy/set.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/privy/set.js +66 -0
- package/dist/cli/commands/admin/realms/privy/set.js.map +1 -0
- package/dist/cli/commands/admin/realms/remove.d.ts +11 -0
- package/dist/cli/commands/admin/realms/remove.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/remove.js +35 -0
- package/dist/cli/commands/admin/realms/remove.js.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/disable.d.ts +15 -0
- package/dist/cli/commands/admin/realms/secp256k1/disable.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/disable.js +42 -0
- package/dist/cli/commands/admin/realms/secp256k1/disable.js.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/enable.d.ts +15 -0
- package/dist/cli/commands/admin/realms/secp256k1/enable.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/enable.js +42 -0
- package/dist/cli/commands/admin/realms/secp256k1/enable.js.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/index.d.ts +9 -0
- package/dist/cli/commands/admin/realms/secp256k1/index.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/secp256k1/index.js +17 -0
- package/dist/cli/commands/admin/realms/secp256k1/index.js.map +1 -0
- package/dist/cli/commands/admin/realms/set.d.ts +16 -0
- package/dist/cli/commands/admin/realms/set.d.ts.map +1 -0
- package/dist/cli/commands/admin/realms/set.js +46 -0
- package/dist/cli/commands/admin/realms/set.js.map +1 -0
- package/dist/cli/commands/admin/rotate-key.d.ts +15 -0
- package/dist/cli/commands/admin/rotate-key.d.ts.map +1 -0
- package/dist/cli/commands/admin/rotate-key.js +46 -0
- package/dist/cli/commands/admin/rotate-key.js.map +1 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +34 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/login.d.ts +12 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +29 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/logout.d.ts +8 -0
- package/dist/cli/commands/logout.d.ts.map +1 -0
- package/dist/cli/commands/logout.js +19 -0
- package/dist/cli/commands/logout.js.map +1 -0
- package/dist/cli/commands/whoami.d.ts +12 -0
- package/dist/cli/commands/whoami.d.ts.map +1 -0
- package/dist/cli/commands/whoami.js +24 -0
- package/dist/cli/commands/whoami.js.map +1 -0
- package/dist/client/Actions.d.ts +101 -0
- package/dist/client/Actions.d.ts.map +1 -0
- package/dist/client/Actions.js +90 -0
- package/dist/client/Actions.js.map +1 -0
- package/dist/client/Auth.d.ts +161 -0
- package/dist/client/Auth.d.ts.map +1 -0
- package/dist/client/Auth.js +113 -0
- package/dist/client/Auth.js.map +1 -0
- package/dist/client/Authorization.d.ts +29 -0
- package/dist/client/Authorization.d.ts.map +1 -0
- package/dist/client/Authorization.js +12 -0
- package/dist/client/Authorization.js.map +1 -0
- package/dist/client/Decorator.d.ts +170 -0
- package/dist/client/Decorator.d.ts.map +1 -0
- package/dist/client/Decorator.js +227 -0
- package/dist/client/Decorator.js.map +1 -0
- package/dist/client/Key.d.ts +85 -0
- package/dist/client/Key.d.ts.map +1 -0
- package/dist/client/Key.js +83 -0
- package/dist/client/Key.js.map +1 -0
- package/dist/client/Pending.d.ts +39 -0
- package/dist/client/Pending.d.ts.map +1 -0
- package/dist/client/Pending.js +12 -0
- package/dist/client/Pending.js.map +1 -0
- package/dist/client/Permissions.d.ts +34 -0
- package/dist/client/Permissions.d.ts.map +1 -0
- package/dist/client/Permissions.js +16 -0
- package/dist/client/Permissions.js.map +1 -0
- package/dist/client/Provider.d.ts +16 -0
- package/dist/client/Provider.d.ts.map +1 -0
- package/dist/client/Provider.js +12 -0
- package/dist/client/Provider.js.map +1 -0
- package/dist/client/Step.d.ts +21 -0
- package/dist/client/Step.d.ts.map +1 -0
- package/dist/client/Step.js +16 -0
- package/dist/client/Step.js.map +1 -0
- package/dist/client/Storage.d.ts +27 -0
- package/dist/client/Storage.d.ts.map +1 -0
- package/dist/client/Storage.js +16 -0
- package/dist/client/Storage.js.map +1 -0
- package/dist/client/TempoAccount.d.ts +40 -0
- package/dist/client/TempoAccount.d.ts.map +1 -0
- package/dist/client/TempoAccount.js +16 -0
- package/dist/client/TempoAccount.js.map +1 -0
- package/dist/client/Tokens.d.ts +25 -0
- package/dist/client/Tokens.d.ts.map +1 -0
- package/dist/client/Tokens.js +12 -0
- package/dist/client/Tokens.js.map +1 -0
- package/dist/client/User.d.ts +21 -0
- package/dist/client/User.d.ts.map +1 -0
- package/dist/client/User.js +12 -0
- package/dist/client/User.js.map +1 -0
- package/dist/client/actions/account.d.ts +25 -0
- package/dist/client/actions/account.d.ts.map +1 -0
- package/dist/client/actions/account.js +34 -0
- package/dist/client/actions/account.js.map +1 -0
- package/dist/client/actions/device.d.ts +53 -0
- package/dist/client/actions/device.d.ts.map +1 -0
- package/dist/client/actions/device.js +79 -0
- package/dist/client/actions/device.js.map +1 -0
- package/dist/client/actions/email.d.ts +84 -0
- package/dist/client/actions/email.d.ts.map +1 -0
- package/dist/client/actions/email.js +63 -0
- package/dist/client/actions/email.js.map +1 -0
- package/dist/client/actions/oauth.d.ts +115 -0
- package/dist/client/actions/oauth.d.ts.map +1 -0
- package/dist/client/actions/oauth.js +105 -0
- package/dist/client/actions/oauth.js.map +1 -0
- package/dist/client/actions/passkey.d.ts +88 -0
- package/dist/client/actions/passkey.d.ts.map +1 -0
- package/dist/client/actions/passkey.js +60 -0
- package/dist/client/actions/passkey.js.map +1 -0
- package/dist/client/actions/provider.d.ts +49 -0
- package/dist/client/actions/provider.d.ts.map +1 -0
- package/dist/client/actions/provider.js +34 -0
- package/dist/client/actions/provider.js.map +1 -0
- package/dist/client/actions/realm.d.ts +24 -0
- package/dist/client/actions/realm.d.ts.map +1 -0
- package/dist/client/actions/realm.js +15 -0
- package/dist/client/actions/realm.js.map +1 -0
- package/dist/client/actions/redirect.d.ts +153 -0
- package/dist/client/actions/redirect.d.ts.map +1 -0
- package/dist/client/actions/redirect.js +236 -0
- package/dist/client/actions/redirect.js.map +1 -0
- package/dist/client/internal/accessKey.d.ts +21 -0
- package/dist/client/internal/accessKey.d.ts.map +1 -0
- package/dist/client/internal/accessKey.js +38 -0
- package/dist/client/internal/accessKey.js.map +1 -0
- package/dist/client/internal/account.d.ts +14 -0
- package/dist/client/internal/account.d.ts.map +1 -0
- package/dist/client/internal/account.js +3 -0
- package/dist/client/internal/account.js.map +1 -0
- package/dist/client/internal/api.d.ts +106 -0
- package/dist/client/internal/api.d.ts.map +1 -0
- package/dist/client/internal/api.js +129 -0
- package/dist/client/internal/api.js.map +1 -0
- package/dist/client/internal/headless.d.ts +33 -0
- package/dist/client/internal/headless.d.ts.map +1 -0
- package/dist/client/internal/headless.js +46 -0
- package/dist/client/internal/headless.js.map +1 -0
- package/dist/client/internal/indexedDb.d.ts +9 -0
- package/dist/client/internal/indexedDb.d.ts.map +1 -0
- package/dist/client/internal/indexedDb.js +32 -0
- package/dist/client/internal/indexedDb.js.map +1 -0
- package/dist/client/internal/request.d.ts +58 -0
- package/dist/client/internal/request.d.ts.map +1 -0
- package/dist/client/internal/request.js +45 -0
- package/dist/client/internal/request.js.map +1 -0
- package/dist/client/internal/store.d.ts +12 -0
- package/dist/client/internal/store.d.ts.map +1 -0
- package/dist/client/internal/store.js +15 -0
- package/dist/client/internal/store.js.map +1 -0
- package/dist/client.d.ts +20 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +20 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/Apps.d.ts +3051 -0
- package/dist/internal/Apps.d.ts.map +1 -0
- package/dist/internal/Apps.js +472 -0
- package/dist/internal/Apps.js.map +1 -0
- package/dist/internal/Auth.d.ts +4058 -0
- package/dist/internal/Auth.d.ts.map +1 -0
- package/dist/internal/Auth.js +137 -0
- package/dist/internal/Auth.js.map +1 -0
- package/dist/internal/Authorize.d.ts +532 -0
- package/dist/internal/Authorize.d.ts.map +1 -0
- package/dist/internal/Authorize.js +326 -0
- package/dist/internal/Authorize.js.map +1 -0
- package/dist/internal/Device.d.ts +371 -0
- package/dist/internal/Device.d.ts.map +1 -0
- package/dist/internal/Device.js +271 -0
- package/dist/internal/Device.js.map +1 -0
- package/dist/internal/Env.d.ts +57 -0
- package/dist/internal/Env.d.ts.map +1 -0
- package/dist/internal/Env.js +2 -0
- package/dist/internal/Env.js.map +1 -0
- package/dist/internal/ErrorResponse.d.ts +609 -0
- package/dist/internal/ErrorResponse.d.ts.map +1 -0
- package/dist/internal/ErrorResponse.js +679 -0
- package/dist/internal/ErrorResponse.js.map +1 -0
- package/dist/internal/Id.d.ts +10 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +34 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/KeyAuthorization.d.ts +1456 -0
- package/dist/internal/KeyAuthorization.d.ts.map +1 -0
- package/dist/internal/KeyAuthorization.js +1030 -0
- package/dist/internal/KeyAuthorization.js.map +1 -0
- package/dist/internal/Limits.d.ts +65 -0
- package/dist/internal/Limits.d.ts.map +1 -0
- package/dist/internal/Limits.js +35 -0
- package/dist/internal/Limits.js.map +1 -0
- package/dist/internal/Log.d.ts +49 -0
- package/dist/internal/Log.d.ts.map +1 -0
- package/dist/internal/Log.js +101 -0
- package/dist/internal/Log.js.map +1 -0
- package/dist/internal/LogLine.d.ts +33 -0
- package/dist/internal/LogLine.d.ts.map +1 -0
- package/dist/internal/LogLine.js +56 -0
- package/dist/internal/LogLine.js.map +1 -0
- package/dist/internal/Migrations.d.ts +19 -0
- package/dist/internal/Migrations.d.ts.map +1 -0
- package/dist/internal/Migrations.js +57 -0
- package/dist/internal/Migrations.js.map +1 -0
- package/dist/internal/Owner.d.ts +146 -0
- package/dist/internal/Owner.d.ts.map +1 -0
- package/dist/internal/Owner.js +65 -0
- package/dist/internal/Owner.js.map +1 -0
- package/dist/internal/Passkey.d.ts +528 -0
- package/dist/internal/Passkey.d.ts.map +1 -0
- package/dist/internal/Passkey.js +658 -0
- package/dist/internal/Passkey.js.map +1 -0
- package/dist/internal/PendingKey.d.ts +49 -0
- package/dist/internal/PendingKey.d.ts.map +1 -0
- package/dist/internal/PendingKey.js +58 -0
- package/dist/internal/PendingKey.js.map +1 -0
- package/dist/internal/Privy.d.ts +105 -0
- package/dist/internal/Privy.d.ts.map +1 -0
- package/dist/internal/Privy.js +224 -0
- package/dist/internal/Privy.js.map +1 -0
- package/dist/internal/RateLimit.d.ts +190 -0
- package/dist/internal/RateLimit.d.ts.map +1 -0
- package/dist/internal/RateLimit.js +148 -0
- package/dist/internal/RateLimit.js.map +1 -0
- package/dist/internal/Realm.d.ts +281 -0
- package/dist/internal/Realm.d.ts.map +1 -0
- package/dist/internal/Realm.js +477 -0
- package/dist/internal/Realm.js.map +1 -0
- package/dist/internal/RequestId.d.ts +16 -0
- package/dist/internal/RequestId.d.ts.map +1 -0
- package/dist/internal/RequestId.js +78 -0
- package/dist/internal/RequestId.js.map +1 -0
- package/dist/internal/SigningKey.d.ts +115 -0
- package/dist/internal/SigningKey.d.ts.map +1 -0
- package/dist/internal/SigningKey.js +197 -0
- package/dist/internal/SigningKey.js.map +1 -0
- package/dist/internal/TestInbox.d.ts +42 -0
- package/dist/internal/TestInbox.d.ts.map +1 -0
- package/dist/internal/TestInbox.js +20 -0
- package/dist/internal/TestInbox.js.map +1 -0
- package/dist/internal/Timing.d.ts +51 -0
- package/dist/internal/Timing.d.ts.map +1 -0
- package/dist/internal/Timing.js +103 -0
- package/dist/internal/Timing.js.map +1 -0
- package/dist/internal/Validation.d.ts +52 -0
- package/dist/internal/Validation.d.ts.map +1 -0
- package/dist/internal/Validation.js +22 -0
- package/dist/internal/Validation.js.map +1 -0
- package/dist/internal/Wallet.d.ts +37 -0
- package/dist/internal/Wallet.d.ts.map +1 -0
- package/dist/internal/Wallet.js +43 -0
- package/dist/internal/Wallet.js.map +1 -0
- package/dist/worker.d.ts +12 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +12 -0
- package/dist/worker.js.map +1 -0
- package/package.json +82 -0
- package/src/Api.ts +151 -0
- package/src/App.ts +56 -0
- package/src/api/account.ts +311 -0
- package/src/api/authenticate.ts +1290 -0
- package/src/api/authorize.ts +1030 -0
- package/src/api/device.ts +874 -0
- package/src/api/health.ts +72 -0
- package/src/api/lib/AccountStep.ts +335 -0
- package/src/api/lib/Auth.ts +134 -0
- package/src/api/lib/Consent.ts +339 -0
- package/src/api/lib/Cors.ts +107 -0
- package/src/api/lib/Database.ts +10 -0
- package/src/api/lib/DeviceRequest.ts +265 -0
- package/src/api/lib/FirstAccount.ts +90 -0
- package/src/api/lib/KeySigner.ts +393 -0
- package/src/api/lib/PasskeyLink.ts +162 -0
- package/src/api/lib/Session.ts +34 -0
- package/src/api/lib/SignInConsent.ts +316 -0
- package/src/api/oauth.ts +1282 -0
- package/src/api/provider.ts +441 -0
- package/src/app/components/Button.tsx +128 -0
- package/src/app/components/CodeInput.tsx +186 -0
- package/src/app/components/ErrorMessage.tsx +66 -0
- package/src/app/components/Grant.tsx +323 -0
- package/src/app/components/Input.tsx +48 -0
- package/src/app/components/Tooltip.tsx +45 -0
- package/src/app/components/icons/BRLA.png +0 -0
- package/src/app/components/icons/BRZ.svg +1 -0
- package/src/app/components/icons/CHFAU.png +0 -0
- package/src/app/components/icons/DLUSD.png +0 -0
- package/src/app/components/icons/EURAU.svg +1 -0
- package/src/app/components/icons/EURC.e.svg +1 -0
- package/src/app/components/icons/GBPA.svg +4 -0
- package/src/app/components/icons/GUSD.svg +1 -0
- package/src/app/components/icons/MACH.svg +4 -0
- package/src/app/components/icons/OUSD.svg +5 -0
- package/src/app/components/icons/PRIME.svg +14 -0
- package/src/app/components/icons/SBC.svg +1 -0
- package/src/app/components/icons/SEKAU.svg +5 -0
- package/src/app/components/icons/USD1.svg +30 -0
- package/src/app/components/icons/USDB.svg +11 -0
- package/src/app/components/icons/USDC.e.svg +1 -0
- package/src/app/components/icons/USDT0.svg +1 -0
- package/src/app/components/icons/USDY.svg +16 -0
- package/src/app/components/icons/USDe.svg +1 -0
- package/src/app/components/icons/YLDS.svg +3 -0
- package/src/app/components/icons/alphaUSD.svg +1 -0
- package/src/app/components/icons/betaUSD.svg +1 -0
- package/src/app/components/icons/cUSD.svg +1 -0
- package/src/app/components/icons/cbBTC.svg +1 -0
- package/src/app/components/icons/frxUSD.svg +1 -0
- package/src/app/components/icons/goUSD.svg +5 -0
- package/src/app/components/icons/iUSD.svg +1 -0
- package/src/app/components/icons/pathUSD.svg +25 -0
- package/src/app/components/icons/rUSD.svg +1 -0
- package/src/app/components/icons/reUSD.svg +1 -0
- package/src/app/components/icons/sUSDe.svg +1 -0
- package/src/app/components/icons/siUSD.svg +1 -0
- package/src/app/components/icons/stcUSD.svg +1 -0
- package/src/app/components/icons/syrupUSDC.svg +5 -0
- package/src/app/components/icons/thetaUSD.svg +1 -0
- package/src/app/components/icons/wYLDS.svg +10 -0
- package/src/app/components/icons/wsrUSD.svg +1 -0
- package/src/app/components/logos/OUSD.dark.svg +17 -0
- package/src/app/components/logos/OUSD.light.svg +17 -0
- package/src/app/crons/cleanup.ts +42 -0
- package/src/app/db/auth.ts +416 -0
- package/src/app/db/migrations/0000_better_auth.sql +53 -0
- package/src/app/db/migrations/0001_oauth_provider.sql +162 -0
- package/src/app/db/migrations/0002_device_code.sql +17 -0
- package/src/app/db/migrations/0003_realms.sql +27 -0
- package/src/app/db/migrations/0004_default_realm.sql +2 -0
- package/src/app/db/migrations/0005_passkeys.sql +30 -0
- package/src/app/db/migrations/0006_first_accounts.sql +9 -0
- package/src/app/db/migrations/0007_pending_keys.sql +13 -0
- package/src/app/db/migrations/0008_secp256k1_owners.sql +18 -0
- package/src/app/db/migrations/0009_device_requests.sql +8 -0
- package/src/app/db/migrations/0010_device_keys.sql +4 -0
- package/src/app/db/migrations/meta/0000_snapshot.json +362 -0
- package/src/app/db/migrations/meta/0001_snapshot.json +1487 -0
- package/src/app/db/migrations/meta/0002_snapshot.json +1603 -0
- package/src/app/db/migrations/meta/0003_snapshot.json +1760 -0
- package/src/app/db/migrations/meta/0004_snapshot.json +1760 -0
- package/src/app/db/migrations/meta/0005_snapshot.json +1911 -0
- package/src/app/db/migrations/meta/0006_snapshot.json +1967 -0
- package/src/app/db/migrations/meta/0007_snapshot.json +2047 -0
- package/src/app/db/migrations/meta/0008_snapshot.json +2157 -0
- package/src/app/db/migrations/meta/0009_snapshot.json +2207 -0
- package/src/app/db/migrations/meta/0010_snapshot.json +2231 -0
- package/src/app/db/migrations/meta/_journal.json +83 -0
- package/src/app/db/schema.ts +218 -0
- package/src/app/env.ts +49 -0
- package/src/app/lib/Client.ts +82 -0
- package/src/app/lib/Loader.ts +24 -0
- package/src/app/package.json +8 -0
- package/src/app/routeTree.gen.ts +198 -0
- package/src/app/router.tsx +48 -0
- package/src/app/routes/[_].flows.tsx +3001 -0
- package/src/app/routes/__root.tsx +91 -0
- package/src/app/routes/authorize.tsx +197 -0
- package/src/app/routes/choose-account.tsx +111 -0
- package/src/app/routes/deposit.tsx +105 -0
- package/src/app/routes/device.tsx +227 -0
- package/src/app/routes/index.tsx +115 -0
- package/src/app/routes/login.tsx +506 -0
- package/src/app/screens/Account.tsx +347 -0
- package/src/app/screens/Approval.tsx +276 -0
- package/src/app/screens/ChooseAccount.tsx +240 -0
- package/src/app/screens/Deposit.tsx +762 -0
- package/src/app/screens/DeviceApproval.tsx +480 -0
- package/src/app/screens/SignIn.tsx +362 -0
- package/src/app/screens/chains/base.svg +3 -0
- package/src/app/screens/chains/ethereum.svg +11 -0
- package/src/app/screens/chains/solana.svg +4 -0
- package/src/app/screens/chains/tempo.svg +4 -0
- package/src/app/server.ts +204 -0
- package/src/app/vite.config.ts +95 -0
- package/src/app/void.config.ts +29 -0
- package/src/app/zyzz.config.ts +274 -0
- package/src/cli/Admin.ts +73 -0
- package/src/cli/Apps.ts +29 -0
- package/src/cli/Cli.ts +42 -0
- package/src/cli/Project.ts +117 -0
- package/src/cli/Realms.ts +50 -0
- package/src/cli/Session.ts +239 -0
- package/src/cli/User.ts +45 -0
- package/src/cli/V1.ts +57 -0
- package/src/cli/bin.ts +44 -0
- package/src/cli/commands/admin/apps/create.ts +101 -0
- package/src/cli/commands/admin/apps/delete.ts +38 -0
- package/src/cli/commands/admin/apps/index.ts +15 -0
- package/src/cli/commands/admin/apps/list.ts +25 -0
- package/src/cli/commands/admin/apps/rotate-secret.ts +47 -0
- package/src/cli/commands/admin/apps/update.ts +86 -0
- package/src/cli/commands/admin/index.ts +14 -0
- package/src/cli/commands/admin/migrate.ts +16 -0
- package/src/cli/commands/admin/realms/add.ts +40 -0
- package/src/cli/commands/admin/realms/index.ts +14 -0
- package/src/cli/commands/admin/realms/list.ts +38 -0
- package/src/cli/commands/admin/realms/passkeys/index.ts +15 -0
- package/src/cli/commands/admin/realms/passkeys/set.ts +44 -0
- package/src/cli/commands/admin/realms/privy/index.ts +14 -0
- package/src/cli/commands/admin/realms/privy/set.ts +69 -0
- package/src/cli/commands/admin/realms/remove.ts +33 -0
- package/src/cli/commands/admin/realms/secp256k1/disable.ts +40 -0
- package/src/cli/commands/admin/realms/secp256k1/enable.ts +40 -0
- package/src/cli/commands/admin/realms/secp256k1/index.ts +19 -0
- package/src/cli/commands/admin/realms/set.ts +46 -0
- package/src/cli/commands/admin/rotate-key.ts +47 -0
- package/src/cli/commands/init.ts +42 -0
- package/src/cli/commands/login.ts +32 -0
- package/src/cli/commands/logout.ts +19 -0
- package/src/cli/commands/whoami.ts +23 -0
- package/src/client/Actions.ts +168 -0
- package/src/client/Auth.ts +285 -0
- package/src/client/Authorization.ts +34 -0
- package/src/client/Decorator.ts +410 -0
- package/src/client/Key.ts +140 -0
- package/src/client/Pending.ts +46 -0
- package/src/client/Permissions.ts +39 -0
- package/src/client/Provider.ts +18 -0
- package/src/client/Step.ts +24 -0
- package/src/client/Storage.ts +29 -0
- package/src/client/TempoAccount.ts +42 -0
- package/src/client/Tokens.ts +27 -0
- package/src/client/User.ts +23 -0
- package/src/client/actions/account.ts +57 -0
- package/src/client/actions/device.ts +151 -0
- package/src/client/actions/email.ts +141 -0
- package/src/client/actions/oauth.ts +182 -0
- package/src/client/actions/passkey.ts +141 -0
- package/src/client/actions/provider.ts +70 -0
- package/src/client/actions/realm.ts +36 -0
- package/src/client/actions/redirect.ts +408 -0
- package/src/client/internal/accessKey.ts +54 -0
- package/src/client/internal/account.ts +15 -0
- package/src/client/internal/api.ts +224 -0
- package/src/client/internal/headless.ts +77 -0
- package/src/client/internal/indexedDb.ts +44 -0
- package/src/client/internal/request.ts +71 -0
- package/src/client/internal/store.ts +20 -0
- package/src/client.ts +19 -0
- package/src/index.ts +6 -0
- package/src/internal/Apps.ts +718 -0
- package/src/internal/Auth.ts +203 -0
- package/src/internal/Authorize.ts +424 -0
- package/src/internal/Balance.ts +168 -0
- package/src/internal/BodyLimit.ts +36 -0
- package/src/internal/Chain.ts +16 -0
- package/src/internal/Device.ts +469 -0
- package/src/internal/Env.ts +58 -0
- package/src/internal/ErrorResponse.ts +854 -0
- package/src/internal/Id.ts +41 -0
- package/src/internal/KeyAuthorization.ts +1339 -0
- package/src/internal/Limits.ts +39 -0
- package/src/internal/Log.ts +178 -0
- package/src/internal/LogLine.ts +87 -0
- package/src/internal/Logout.ts +336 -0
- package/src/internal/Migrations.ts +86 -0
- package/src/internal/OpenApi.ts +300 -0
- package/src/internal/Owner.ts +211 -0
- package/src/internal/Passkey.ts +1052 -0
- package/src/internal/PendingKey.ts +104 -0
- package/src/internal/Privy.ts +402 -0
- package/src/internal/Provider.ts +125 -0
- package/src/internal/RateLimit.ts +259 -0
- package/src/internal/Realm.ts +768 -0
- package/src/internal/RequestId.ts +118 -0
- package/src/internal/SignInGrant.ts +168 -0
- package/src/internal/SigningKey.ts +286 -0
- package/src/internal/TempoAccount.ts +342 -0
- package/src/internal/TestInbox.ts +51 -0
- package/src/internal/Timing.ts +158 -0
- package/src/internal/Validation.ts +52 -0
- package/src/internal/Wallet.ts +62 -0
- package/src/internal/owners/Passkey.ts +72 -0
- package/src/internal/owners/Privy.ts +81 -0
- package/src/internal/owners/Secp256k1.ts +143 -0
- package/src/types/app.d.ts +19 -0
- package/src/worker.ts +11 -0
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
/** The sign-in routes under `/v1/auth`: email codes, passkeys, sign-out, and the session. @module */
|
|
2
|
+
import { verifyOAuthQueryParams } from '@better-auth/oauth-provider'
|
|
3
|
+
import { isAPIError } from 'better-auth/api'
|
|
4
|
+
import { and, eq, isNull } from 'drizzle-orm'
|
|
5
|
+
import type { Context } from 'hono'
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import { Authentication } from 'webauthx/server'
|
|
8
|
+
import { env } from 'void/env'
|
|
9
|
+
import * as z from 'zod'
|
|
10
|
+
import * as Schema from '../app/db/schema.js'
|
|
11
|
+
import * as Apps from '../internal/Apps.js'
|
|
12
|
+
import * as core_Auth from '../internal/Auth.js'
|
|
13
|
+
import * as Authorize from '../internal/Authorize.js'
|
|
14
|
+
import * as BodyLimit from '../internal/BodyLimit.js'
|
|
15
|
+
import * as Device from '../internal/Device.js'
|
|
16
|
+
import type { Env } from '../internal/Env.js'
|
|
17
|
+
import * as ErrorResponse from '../internal/ErrorResponse.js'
|
|
18
|
+
import * as KeyAuthorization from '../internal/KeyAuthorization.js'
|
|
19
|
+
import * as Log from '../internal/Log.js'
|
|
20
|
+
import * as OpenApi from '../internal/OpenApi.js'
|
|
21
|
+
import * as Passkey from '../internal/Passkey.js'
|
|
22
|
+
import * as RateLimit from '../internal/RateLimit.js'
|
|
23
|
+
import * as Realm from '../internal/Realm.js'
|
|
24
|
+
import * as Timing from '../internal/Timing.js'
|
|
25
|
+
import * as Wallet from '../internal/Wallet.js'
|
|
26
|
+
import * as AccountStep from './lib/AccountStep.js'
|
|
27
|
+
import * as Auth from './lib/Auth.js'
|
|
28
|
+
import * as Cors from './lib/Cors.js'
|
|
29
|
+
import { db } from './lib/Database.js'
|
|
30
|
+
import * as FirstAccount from './lib/FirstAccount.js'
|
|
31
|
+
import * as PasskeyLink from './lib/PasskeyLink.js'
|
|
32
|
+
import * as Session from './lib/Session.js'
|
|
33
|
+
import * as SignInConsent from './lib/SignInConsent.js'
|
|
34
|
+
|
|
35
|
+
/** A hostname label: 1 to 63 letters, digits, and hyphens, with no hyphen at either end. */
|
|
36
|
+
const hostnameLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/
|
|
37
|
+
|
|
38
|
+
namespace schema {
|
|
39
|
+
export namespace code {
|
|
40
|
+
export const json = z.object({
|
|
41
|
+
email: z
|
|
42
|
+
.string()
|
|
43
|
+
.trim()
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.pipe(
|
|
46
|
+
// A malformed or long address stops at its first failed check, so the refusal names it once.
|
|
47
|
+
z
|
|
48
|
+
.email({ abort: true })
|
|
49
|
+
.max(254, { abort: true, error: 'Invalid email address' })
|
|
50
|
+
// The email service refuses a local part over 64 characters, and a domain label that is
|
|
51
|
+
// not a hostname label: over 63 characters, or with a hyphen at either end.
|
|
52
|
+
.refine((value) => {
|
|
53
|
+
const [local = '', domain = ''] = value.split('@')
|
|
54
|
+
return (
|
|
55
|
+
local.length <= 64 &&
|
|
56
|
+
domain.split('.').every((label) => hostnameLabel.test(label))
|
|
57
|
+
)
|
|
58
|
+
}, 'Invalid email address'),
|
|
59
|
+
)
|
|
60
|
+
.meta({
|
|
61
|
+
description: "The user's email address.",
|
|
62
|
+
examples: ['ada@example.com'],
|
|
63
|
+
}),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const response = z.object({
|
|
67
|
+
sent: z.literal(true).meta({
|
|
68
|
+
description:
|
|
69
|
+
'The request is accepted. The value is the same for every address.',
|
|
70
|
+
}),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export namespace verify {
|
|
75
|
+
export const json = z
|
|
76
|
+
.object({
|
|
77
|
+
assertion: Passkey.schema.assertion.optional().meta({
|
|
78
|
+
description:
|
|
79
|
+
"The assertion of the email step's passkey, over a challenge from `POST /v1/auth/passkey/options` with `link`. Required with `link`, since only the person who holds the passkey can link it.",
|
|
80
|
+
}),
|
|
81
|
+
code: z
|
|
82
|
+
.string()
|
|
83
|
+
.trim()
|
|
84
|
+
.regex(/^\d{6}$/)
|
|
85
|
+
.meta({
|
|
86
|
+
description: 'The six-digit emailed code.',
|
|
87
|
+
examples: ['123456'],
|
|
88
|
+
}),
|
|
89
|
+
email: code.json.shape.email,
|
|
90
|
+
link: z
|
|
91
|
+
.string()
|
|
92
|
+
.max(64)
|
|
93
|
+
.optional()
|
|
94
|
+
.meta({
|
|
95
|
+
description:
|
|
96
|
+
"The email step's `link`, which links the wallet.tempo passkey that asked for it to this user, with that passkey's `assertion`. An expired link refuses the sign-in with `passkey_link_expired`, and the code stays valid.",
|
|
97
|
+
examples: ['dGhlIG5vbmNlIHRoYXQgdGhlIHN0ZXAgY2Fycmllcw'],
|
|
98
|
+
}),
|
|
99
|
+
// The bound covers an authorization request's query, and limits what an unsigned query can
|
|
100
|
+
// write, since the passkey challenge stores its client ID before the signature is checked.
|
|
101
|
+
oauth_query: z
|
|
102
|
+
.string()
|
|
103
|
+
.max(8192)
|
|
104
|
+
.optional()
|
|
105
|
+
.meta({
|
|
106
|
+
description:
|
|
107
|
+
"The sign-in page's query string when an app's authorization request sent the browser there, which the sign-in continues. Better Auth signs it. At most 8,192 characters.",
|
|
108
|
+
examples: [
|
|
109
|
+
'client_id=client_01JZ8Q6W3N1R5B4V7C2X9K0M3T&response_type=code&exp=1760000000&sig=Gx3kB0',
|
|
110
|
+
],
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
.refine((value) => !value.link || value.assertion, {
|
|
114
|
+
message: 'Required with `link`.',
|
|
115
|
+
path: ['assertion'],
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/** The user fields that the API returns. */
|
|
119
|
+
export const user = z
|
|
120
|
+
.object({
|
|
121
|
+
email: z.email().meta({
|
|
122
|
+
description: "The user's email address.",
|
|
123
|
+
examples: ['ada@example.com'],
|
|
124
|
+
}),
|
|
125
|
+
id: z.string().meta({
|
|
126
|
+
description:
|
|
127
|
+
"The user's ID, which is the `sub` of every token Tempo ID issues for the user.",
|
|
128
|
+
examples: ['user_01JZ8Q6W3N1R5B4V7C2X9K0M3T'],
|
|
129
|
+
}),
|
|
130
|
+
})
|
|
131
|
+
.meta({ description: 'The signed-in user.', id: 'User' })
|
|
132
|
+
|
|
133
|
+
export const response = z.object({
|
|
134
|
+
redirect: z
|
|
135
|
+
.url()
|
|
136
|
+
.nullable()
|
|
137
|
+
.meta({
|
|
138
|
+
description:
|
|
139
|
+
"Where the browser continues: the app's `redirect_uri` with an authorization code, or `null` for a sign-in that no app waits on.",
|
|
140
|
+
examples: [
|
|
141
|
+
'https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA',
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
user,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/** What Better Auth returns: the user, or the authorization that the sign-in continued. */
|
|
148
|
+
export const signedIn = z.union([
|
|
149
|
+
z.object({ url: z.string() }),
|
|
150
|
+
z.object({ user: z.object({ email: z.string(), id: z.string() }) }),
|
|
151
|
+
])
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export namespace passkeyOptions {
|
|
155
|
+
export const json = z.object({
|
|
156
|
+
client_id: z
|
|
157
|
+
.string()
|
|
158
|
+
.min(1)
|
|
159
|
+
.max(64)
|
|
160
|
+
.optional()
|
|
161
|
+
.meta({
|
|
162
|
+
description:
|
|
163
|
+
"The client ID of the first-party app whose page runs the ceremony. It selects the app's realms, and the request's `Origin` must be the origin of one of the app's redirect URIs. Required from an app's page.",
|
|
164
|
+
examples: ['client_01JZ8Q6W3N1R5B4V7C2X9K0M3T'],
|
|
165
|
+
}),
|
|
166
|
+
credentialId: z
|
|
167
|
+
.union([
|
|
168
|
+
Passkey.schema.credentialId,
|
|
169
|
+
z.array(Passkey.schema.credentialId).max(16),
|
|
170
|
+
])
|
|
171
|
+
.optional()
|
|
172
|
+
.meta({
|
|
173
|
+
description:
|
|
174
|
+
'The credential ID, or up to 16 IDs, that the browser offers as `allowCredentials`, base64url. Defaults to every discoverable passkey of the relying party, as an empty list does.',
|
|
175
|
+
examples: ['KEbWNCc7NgaYnUyrNeFGX9_3Y-8'],
|
|
176
|
+
}),
|
|
177
|
+
link: verify.json.shape.link.meta({
|
|
178
|
+
description:
|
|
179
|
+
"The email step's `link`. The options then name only that step's passkey, at its relying party, in place of `credentialId`. The emailed-code sign-in sends the passkey's assertion with the `link`.",
|
|
180
|
+
examples: ['dGhlIG5vbmNlIHRoYXQgdGhlIHN0ZXAgY2Fycmllcw'],
|
|
181
|
+
}),
|
|
182
|
+
mediation: z
|
|
183
|
+
.enum(['conditional', 'optional', 'required', 'silent'])
|
|
184
|
+
.optional()
|
|
185
|
+
.meta({
|
|
186
|
+
description:
|
|
187
|
+
'Returned in the options for `navigator.credentials.get()`, such as `conditional` for passkey autofill.',
|
|
188
|
+
examples: ['conditional'],
|
|
189
|
+
}),
|
|
190
|
+
oauth_query: verify.json.shape.oauth_query,
|
|
191
|
+
user_code: z
|
|
192
|
+
.string()
|
|
193
|
+
.max(32)
|
|
194
|
+
.optional()
|
|
195
|
+
.meta({
|
|
196
|
+
description:
|
|
197
|
+
'The user code of the device grant that the sign-in returns to. While the code is pending and bound to no user, it selects the realms of the app that requested it. Any other code selects the `default` realm. Ignored with `oauth_query` or `client_id`.',
|
|
198
|
+
examples: ['WDJB-MJHT'],
|
|
199
|
+
}),
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
export const response = z.object({
|
|
203
|
+
options: z
|
|
204
|
+
.object({
|
|
205
|
+
mediation: passkeyOptions.json.shape.mediation,
|
|
206
|
+
publicKey: z
|
|
207
|
+
.object({
|
|
208
|
+
allowCredentials: z
|
|
209
|
+
.array(
|
|
210
|
+
z
|
|
211
|
+
.object({
|
|
212
|
+
id: z.string().meta({
|
|
213
|
+
description: 'A credential ID, base64url.',
|
|
214
|
+
examples: ['KEbWNCc7NgaYnUyrNeFGX9_3Y-8'],
|
|
215
|
+
}),
|
|
216
|
+
type: z.literal('public-key').meta({
|
|
217
|
+
description: 'The credential type.',
|
|
218
|
+
}),
|
|
219
|
+
})
|
|
220
|
+
.meta({ description: 'A passkey the browser offers.' }),
|
|
221
|
+
)
|
|
222
|
+
.optional()
|
|
223
|
+
.meta({
|
|
224
|
+
description:
|
|
225
|
+
'The passkeys the browser offers, from `credentialId`. Absent, it offers every discoverable passkey of the relying party.',
|
|
226
|
+
}),
|
|
227
|
+
challenge: z.templateLiteral(['0x', z.string()]).meta({
|
|
228
|
+
description:
|
|
229
|
+
'The single-use challenge, as hex. It expires after 5 minutes.',
|
|
230
|
+
examples: [
|
|
231
|
+
'0xf6310589abb21116acc6e123967ed0a125b5041c43f8e157237093e07eaa354c',
|
|
232
|
+
],
|
|
233
|
+
}),
|
|
234
|
+
rpId: z.string().meta({
|
|
235
|
+
description: 'The relying party ID of the passkeys.',
|
|
236
|
+
examples: ['tempo.xyz'],
|
|
237
|
+
}),
|
|
238
|
+
userVerification: z.literal('required').meta({
|
|
239
|
+
description:
|
|
240
|
+
'Every passkey sign-in requires user verification.',
|
|
241
|
+
}),
|
|
242
|
+
})
|
|
243
|
+
.meta({
|
|
244
|
+
description:
|
|
245
|
+
'The `PublicKeyCredentialRequestOptions`, serialized with the challenge as hex.',
|
|
246
|
+
}),
|
|
247
|
+
})
|
|
248
|
+
.meta({
|
|
249
|
+
description: 'The options for `navigator.credentials.get()`.',
|
|
250
|
+
}),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export namespace passkeyVerify {
|
|
255
|
+
export const json = z.object({
|
|
256
|
+
assertion: Passkey.schema.assertion,
|
|
257
|
+
oauth_query: verify.json.shape.oauth_query,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
export const response = z.union([
|
|
261
|
+
verify.response.meta({ description: 'The signed-in user.' }),
|
|
262
|
+
z
|
|
263
|
+
.object({
|
|
264
|
+
step: z
|
|
265
|
+
.object({
|
|
266
|
+
link: z.string().meta({
|
|
267
|
+
description:
|
|
268
|
+
'Send as `link` with the emailed-code sign-in, which links the passkey to that user.',
|
|
269
|
+
examples: ['dGhlIG5vbmNlIHRoYXQgdGhlIHN0ZXAgY2Fycmllcw'],
|
|
270
|
+
}),
|
|
271
|
+
signed: z.literal(false).meta({
|
|
272
|
+
description:
|
|
273
|
+
'Whether the assertion already signed a key authorization. This route signs none.',
|
|
274
|
+
}),
|
|
275
|
+
type: z.literal('email').meta({
|
|
276
|
+
description:
|
|
277
|
+
'Sign in with an emailed code, which links the passkey to that user.',
|
|
278
|
+
}),
|
|
279
|
+
})
|
|
280
|
+
.meta({
|
|
281
|
+
description:
|
|
282
|
+
'The email step, for a wallet.tempo passkey whose account has no email.',
|
|
283
|
+
}),
|
|
284
|
+
})
|
|
285
|
+
.meta({
|
|
286
|
+
description:
|
|
287
|
+
'A passkey sign-in that continues when the user verifies an email address.',
|
|
288
|
+
}),
|
|
289
|
+
])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export namespace logout {
|
|
293
|
+
export const response = z.object({
|
|
294
|
+
signed_out: z.literal(true).meta({
|
|
295
|
+
description: 'The session is over, or there was none.',
|
|
296
|
+
}),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export namespace session {
|
|
301
|
+
export const response = z.object({
|
|
302
|
+
user: verify.user
|
|
303
|
+
.extend({
|
|
304
|
+
name: z
|
|
305
|
+
.string()
|
|
306
|
+
.nullable()
|
|
307
|
+
.meta({
|
|
308
|
+
description:
|
|
309
|
+
"The user's name, or `null` for a user who signed up without one.",
|
|
310
|
+
examples: ['Ada Lovelace'],
|
|
311
|
+
}),
|
|
312
|
+
})
|
|
313
|
+
.nullable()
|
|
314
|
+
.meta({
|
|
315
|
+
description: 'The signed-in user, or `null` without a session.',
|
|
316
|
+
id: 'SessionUser',
|
|
317
|
+
}),
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export namespace methods {
|
|
322
|
+
export const query = z.object({
|
|
323
|
+
oauth_query: verify.json.shape.oauth_query,
|
|
324
|
+
return_to: z
|
|
325
|
+
.string()
|
|
326
|
+
.max(2048)
|
|
327
|
+
.optional()
|
|
328
|
+
.meta({
|
|
329
|
+
description:
|
|
330
|
+
"The page that sign-in returns to, from the sign-in page's query. Only a path on the device page passes, and any other value reads as absent.",
|
|
331
|
+
examples: ['/device?code=WDJB-MJHT'],
|
|
332
|
+
}),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
export const response = z.object({
|
|
336
|
+
google: z.boolean().meta({
|
|
337
|
+
description:
|
|
338
|
+
'Whether Tempo ID offers Google sign-in, which the deployment configures.',
|
|
339
|
+
examples: [true],
|
|
340
|
+
}),
|
|
341
|
+
passkeys: z.boolean().meta({
|
|
342
|
+
description:
|
|
343
|
+
"Whether Tempo ID offers passkey sign-in: one of the realms of the app that `oauth_query` or the device code of `return_to` names, or the `default` realm without one, holds passkeys that sign in at Tempo ID's origin.",
|
|
344
|
+
examples: [true],
|
|
345
|
+
}),
|
|
346
|
+
request: z
|
|
347
|
+
.object({
|
|
348
|
+
app: z.string().meta({
|
|
349
|
+
description:
|
|
350
|
+
"The app's display name, or its client ID without one.",
|
|
351
|
+
examples: ['Tempo Cash'],
|
|
352
|
+
}),
|
|
353
|
+
key: KeyAuthorization.schema.description,
|
|
354
|
+
})
|
|
355
|
+
.nullable()
|
|
356
|
+
.meta({
|
|
357
|
+
description:
|
|
358
|
+
'The app and the key of the signed authorization request in `oauth_query`, which the sign-in grants, or `null` for a forged or expired query, an unknown app, a request without a key, or a key with a `challenge`, which the approval page shows instead.',
|
|
359
|
+
}),
|
|
360
|
+
return_to: z
|
|
361
|
+
.string()
|
|
362
|
+
.nullable()
|
|
363
|
+
.meta({
|
|
364
|
+
description:
|
|
365
|
+
'The page that sign-in returns to, a path on the device page, or `null` for the account.',
|
|
366
|
+
examples: ['/device?code=WDJB-MJHT'],
|
|
367
|
+
}),
|
|
368
|
+
user_code: z
|
|
369
|
+
.string()
|
|
370
|
+
.nullable()
|
|
371
|
+
.meta({
|
|
372
|
+
description:
|
|
373
|
+
'The user code of the device page that sign-in returns to, or `null` without one.',
|
|
374
|
+
examples: ['WDJBMJHT'],
|
|
375
|
+
}),
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The sign-in routes under `/v1/auth`. */
|
|
381
|
+
export const authenticate = new Hono<Env>()
|
|
382
|
+
.post(
|
|
383
|
+
'/v1/auth/code',
|
|
384
|
+
OpenApi.describeRoute({
|
|
385
|
+
description:
|
|
386
|
+
'Emails a sign-in code. The response is the same for every address, so it reveals no account.',
|
|
387
|
+
responses: OpenApi.responses({
|
|
388
|
+
errors: [
|
|
389
|
+
...BodyLimit.errors,
|
|
390
|
+
...Auth.sendCode.errors,
|
|
391
|
+
...OpenApi.validate.errors,
|
|
392
|
+
...RateLimit.check.errors,
|
|
393
|
+
ErrorResponse.CrossSiteRequestError,
|
|
394
|
+
ErrorResponse.UpstreamUnavailableError,
|
|
395
|
+
],
|
|
396
|
+
success: {
|
|
397
|
+
description:
|
|
398
|
+
'The response for every address: the email service accepted the code, or does not deliver to the address.',
|
|
399
|
+
schema: schema.code.response,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
tags: ['Authenticate'],
|
|
403
|
+
}),
|
|
404
|
+
OpenApi.validate('json', schema.code.json),
|
|
405
|
+
async (c) => {
|
|
406
|
+
const { email } = c.req.valid('json')
|
|
407
|
+
// The IP address's limit counts first, so a refused flood spends no address's sends.
|
|
408
|
+
const refused = await Timing.measure(
|
|
409
|
+
c,
|
|
410
|
+
RateLimit.check(c, {
|
|
411
|
+
limits: [
|
|
412
|
+
{ binding: 'SIGN_IN_IP_LIMIT', key: RateLimit.ip(c) },
|
|
413
|
+
{ binding: 'SIGN_IN_CODE_LIMIT', key: RateLimit.email({ email }) },
|
|
414
|
+
],
|
|
415
|
+
}),
|
|
416
|
+
'RateLimit.check',
|
|
417
|
+
)
|
|
418
|
+
if (refused) return refused
|
|
419
|
+
|
|
420
|
+
// Every address receives a code, so the response reveals no account. The route waits for the
|
|
421
|
+
// email service, so a failed send refuses the request instead of a sign-in that never arrives.
|
|
422
|
+
const code = await Timing.measure(
|
|
423
|
+
c,
|
|
424
|
+
c.var.auth.api.createVerificationOTP({
|
|
425
|
+
body: { email, type: 'sign-in' },
|
|
426
|
+
}),
|
|
427
|
+
'auth.createVerificationOTP',
|
|
428
|
+
)
|
|
429
|
+
const failure = await Timing.measure(
|
|
430
|
+
c,
|
|
431
|
+
Auth.sendCode(c, { code, email: email.toLowerCase() }),
|
|
432
|
+
'Auth.sendCode',
|
|
433
|
+
).catch((error: unknown) => error)
|
|
434
|
+
if (failure) {
|
|
435
|
+
// The refusal is the email service's, as for any upstream service, so it is a warning.
|
|
436
|
+
Log.warn(c, {
|
|
437
|
+
error: failure,
|
|
438
|
+
message: 'The email service refused a sign-in code.',
|
|
439
|
+
})
|
|
440
|
+
// A refusal of the address itself responds as a sent code, so the response is the same for
|
|
441
|
+
// every address. The email service's rate limit is one the caller can wait out. Any other
|
|
442
|
+
// refusal is an outage.
|
|
443
|
+
if (!(failure instanceof Auth.UndeliverableError))
|
|
444
|
+
return ErrorResponse.json(
|
|
445
|
+
c,
|
|
446
|
+
failure instanceof ErrorResponse.RateLimitedError
|
|
447
|
+
? failure
|
|
448
|
+
: new ErrorResponse.UpstreamUnavailableError(),
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return c.json({ sent: true as const }, 200)
|
|
453
|
+
},
|
|
454
|
+
)
|
|
455
|
+
.post(
|
|
456
|
+
'/v1/auth/code/verify',
|
|
457
|
+
OpenApi.describeRoute({
|
|
458
|
+
description:
|
|
459
|
+
'Signs in with an emailed code, and sets the session cookie. For a user with no Tempo Account in the realm of the sign-in, the first realm of the app that `oauth_query` names or the `default` realm, it creates one when the realm creates accounts. A failed creation leaves the sign-in signed in, and the next sign-in or `POST /v1/account` creates the account.',
|
|
460
|
+
responses: OpenApi.responses({
|
|
461
|
+
errors: [
|
|
462
|
+
...BodyLimit.errors,
|
|
463
|
+
...Cors.ownPage.errors,
|
|
464
|
+
...OpenApi.validate.errors,
|
|
465
|
+
...RateLimit.check.errors,
|
|
466
|
+
core_Auth.AuthorizationExpiredError,
|
|
467
|
+
core_Auth.InvalidCodeError,
|
|
468
|
+
Passkey.LinkExpiredError,
|
|
469
|
+
Passkey.WebAuthnRejectedError,
|
|
470
|
+
],
|
|
471
|
+
success: {
|
|
472
|
+
description: 'The signed-in user.',
|
|
473
|
+
schema: schema.verify.response,
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
tags: ['Authenticate'],
|
|
477
|
+
}),
|
|
478
|
+
OpenApi.validate('json', schema.verify.json),
|
|
479
|
+
async (c) => {
|
|
480
|
+
const { assertion, code, email, link, oauth_query } = c.req.valid('json')
|
|
481
|
+
const crossSite = Cors.ownPage(c, { oauth_query })
|
|
482
|
+
if (crossSite) return ErrorResponse.json(c, crossSite)
|
|
483
|
+
// Better Auth counts the guesses at each code. The IP address's limit bounds guesses across
|
|
484
|
+
// addresses.
|
|
485
|
+
const refused = await Timing.measure(
|
|
486
|
+
c,
|
|
487
|
+
RateLimit.check(c, {
|
|
488
|
+
limits: [{ binding: 'SIGN_IN_IP_LIMIT', key: RateLimit.ip(c) }],
|
|
489
|
+
}),
|
|
490
|
+
'RateLimit.check',
|
|
491
|
+
)
|
|
492
|
+
if (refused) return refused
|
|
493
|
+
|
|
494
|
+
// The email step's link is checked before the code, so an expired one refuses the sign-in and
|
|
495
|
+
// leaves the code unspent. The sign-in then never ends without the passkey it was meant to link.
|
|
496
|
+
const deferred = link
|
|
497
|
+
? await Timing.measure(
|
|
498
|
+
c,
|
|
499
|
+
PasskeyLink.claim(c, { nonce: link }),
|
|
500
|
+
'PasskeyLink.claim',
|
|
501
|
+
)
|
|
502
|
+
: undefined
|
|
503
|
+
if (link && !deferred)
|
|
504
|
+
return ErrorResponse.json(c, new Passkey.LinkExpiredError())
|
|
505
|
+
|
|
506
|
+
// The link's passkey signs again before the code is checked, so a page left at the email step
|
|
507
|
+
// links nothing for the next person, and a refusal leaves the code unspent.
|
|
508
|
+
if (deferred) {
|
|
509
|
+
const challenge = assertion
|
|
510
|
+
? await Timing.measure(
|
|
511
|
+
c,
|
|
512
|
+
Passkey.take(db, {
|
|
513
|
+
clientDataJSON: assertion.metadata.clientDataJSON,
|
|
514
|
+
}),
|
|
515
|
+
'Passkey.take',
|
|
516
|
+
)
|
|
517
|
+
: undefined
|
|
518
|
+
// Only a challenge that the options issued for this link confirms it.
|
|
519
|
+
const bound =
|
|
520
|
+
challenge && link
|
|
521
|
+
? await Timing.measure(
|
|
522
|
+
c,
|
|
523
|
+
PasskeyLink.matches({ expected: challenge.link, nonce: link }),
|
|
524
|
+
'PasskeyLink.matches',
|
|
525
|
+
)
|
|
526
|
+
: false
|
|
527
|
+
if (
|
|
528
|
+
!assertion ||
|
|
529
|
+
!challenge ||
|
|
530
|
+
!bound ||
|
|
531
|
+
challenge.clientId !== client({ oauth_query }) ||
|
|
532
|
+
challenge.origin !== Cors.appOrigin(c) ||
|
|
533
|
+
challenge.rpId !== deferred.rpId ||
|
|
534
|
+
assertion.id !== deferred.id ||
|
|
535
|
+
!Passkey.verify(assertion, {
|
|
536
|
+
challenge: challenge.challenge,
|
|
537
|
+
origin: challenge.origin ?? new URL(env.URL).origin,
|
|
538
|
+
publicKey: deferred.publicKey,
|
|
539
|
+
rpId: deferred.rpId,
|
|
540
|
+
})
|
|
541
|
+
)
|
|
542
|
+
return ErrorResponse.json(c, new Passkey.WebAuthnRejectedError())
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const result = await Timing.measure(
|
|
546
|
+
c,
|
|
547
|
+
c.var.auth.api.signInEmailOTP({
|
|
548
|
+
// A call with a request returns a `Response` unless `asResponse` is false.
|
|
549
|
+
asResponse: false,
|
|
550
|
+
body: { email, otp: code, ...(oauth_query ? { oauth_query } : {}) },
|
|
551
|
+
// Better Auth sets `accept` to continue an authorization as JSON, and a request's own headers
|
|
552
|
+
// are immutable.
|
|
553
|
+
headers: new Headers(c.req.raw.headers),
|
|
554
|
+
// Better Auth continues an authorization only for a call that carries its request.
|
|
555
|
+
request: c.req.raw,
|
|
556
|
+
returnHeaders: true,
|
|
557
|
+
}),
|
|
558
|
+
'auth.signInEmailOTP',
|
|
559
|
+
).catch((error: unknown) => {
|
|
560
|
+
// Better Auth checks the authorization request's signature before the code, so a forged or
|
|
561
|
+
// expired one leaves the code unspent.
|
|
562
|
+
if (isAPIError(error) && error.body?.error === 'invalid_signature')
|
|
563
|
+
return new core_Auth.AuthorizationExpiredError()
|
|
564
|
+
// Better Auth refuses a wrong, expired, or spent code, and a code past its guesses. Any other
|
|
565
|
+
// failure fails the request.
|
|
566
|
+
if (isAPIError(error)) return new core_Auth.InvalidCodeError()
|
|
567
|
+
throw error
|
|
568
|
+
})
|
|
569
|
+
if (result instanceof ErrorResponse.ResponseError)
|
|
570
|
+
return ErrorResponse.json(c, result)
|
|
571
|
+
|
|
572
|
+
for (const cookie of result.headers.getSetCookie())
|
|
573
|
+
c.header('set-cookie', cookie, { append: true })
|
|
574
|
+
c.header('cache-control', 'private, no-store')
|
|
575
|
+
|
|
576
|
+
// With `oauth_query`, Better Auth continues the app's authorization once the session exists, and
|
|
577
|
+
// returns its redirect in place of the user.
|
|
578
|
+
const signedIn = schema.verify.signedIn.parse(result.response)
|
|
579
|
+
const user = await (async () => {
|
|
580
|
+
if ('user' in signedIn)
|
|
581
|
+
return { email: signedIn.user.email, id: signedIn.user.id }
|
|
582
|
+
const context = await Timing.measure(
|
|
583
|
+
c,
|
|
584
|
+
c.var.auth.$context,
|
|
585
|
+
'auth.context',
|
|
586
|
+
)
|
|
587
|
+
const found = await Timing.measure(
|
|
588
|
+
c,
|
|
589
|
+
context.internalAdapter.findUserByEmail(email),
|
|
590
|
+
'db.user.findByEmail',
|
|
591
|
+
)
|
|
592
|
+
if (!found) throw new Error('The signed-in user is missing.')
|
|
593
|
+
return { email: found.user.email, id: found.user.id }
|
|
594
|
+
})()
|
|
595
|
+
|
|
596
|
+
// The email step's passkey links to the user that the code signed in.
|
|
597
|
+
if (deferred) {
|
|
598
|
+
const linked = await Timing.measure(
|
|
599
|
+
c,
|
|
600
|
+
Passkey.link(db, {
|
|
601
|
+
credential: deferred,
|
|
602
|
+
id: deferred.id,
|
|
603
|
+
rpId: deferred.rpId,
|
|
604
|
+
userId: user.id,
|
|
605
|
+
}),
|
|
606
|
+
'Passkey.link',
|
|
607
|
+
)
|
|
608
|
+
// A concurrent sign-in can link the passkey to another user first. Its row decides, so this
|
|
609
|
+
// sign-in links nothing, and the cookie stays until it expires.
|
|
610
|
+
if (linked === user.id) PasskeyLink.release(c)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// A linked passkey owns its account, so the first account follows the link. The sign-in grant
|
|
614
|
+
// below can then sign the key with it.
|
|
615
|
+
await Timing.measure(
|
|
616
|
+
c,
|
|
617
|
+
FirstAccount.create(c, {
|
|
618
|
+
clientId: client({ oauth_query }),
|
|
619
|
+
userId: user.id,
|
|
620
|
+
}),
|
|
621
|
+
'FirstAccount.create',
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
// The sign-in page showed the request's key, so the sign-in grants it.
|
|
625
|
+
const redirect =
|
|
626
|
+
'user' in signedIn
|
|
627
|
+
? null
|
|
628
|
+
: await Timing.measure(
|
|
629
|
+
c,
|
|
630
|
+
SignInConsent.afterSignIn(c, {
|
|
631
|
+
redirect: signedIn.url,
|
|
632
|
+
setCookie: result.headers.getSetCookie(),
|
|
633
|
+
}),
|
|
634
|
+
'SignInConsent.afterSignIn',
|
|
635
|
+
)
|
|
636
|
+
return c.json({ redirect, user }, 200)
|
|
637
|
+
},
|
|
638
|
+
)
|
|
639
|
+
.post(
|
|
640
|
+
'/v1/auth/passkey/options',
|
|
641
|
+
OpenApi.describeRoute({
|
|
642
|
+
description:
|
|
643
|
+
"Issues a single-use challenge, and returns request options for the passkeys of the relying party that the sign-in reaches. Without `oauth_query` or `client_id`, that is the relying party of the `default` realm. With either, it is the first of the app's realms that holds passkeys. From a first-party app's page, with `client_id`, the relying party must cover the page's origin and Tempo ID's, and only that page's assertion spends the challenge. With `link`, the options name only the email step's passkey.",
|
|
644
|
+
responses: OpenApi.responses({
|
|
645
|
+
errors: [
|
|
646
|
+
...BodyLimit.errors,
|
|
647
|
+
...Cors.ownPage.errors,
|
|
648
|
+
...OpenApi.validate.errors,
|
|
649
|
+
...Passkey.checkPage.errors,
|
|
650
|
+
...RateLimit.check.errors,
|
|
651
|
+
Passkey.LinkExpiredError,
|
|
652
|
+
Passkey.NotPermittedError,
|
|
653
|
+
],
|
|
654
|
+
success: {
|
|
655
|
+
description: 'The request options.',
|
|
656
|
+
schema: schema.passkeyOptions.response,
|
|
657
|
+
},
|
|
658
|
+
}),
|
|
659
|
+
tags: ['Authenticate'],
|
|
660
|
+
}),
|
|
661
|
+
OpenApi.validate('json', schema.passkeyOptions.json),
|
|
662
|
+
async (c) => {
|
|
663
|
+
const {
|
|
664
|
+
client_id,
|
|
665
|
+
credentialId,
|
|
666
|
+
link,
|
|
667
|
+
mediation,
|
|
668
|
+
oauth_query,
|
|
669
|
+
user_code,
|
|
670
|
+
} = c.req.valid('json')
|
|
671
|
+
const crossSite = Cors.ownPage(c, { oauth_query })
|
|
672
|
+
if (crossSite) return ErrorResponse.json(c, crossSite)
|
|
673
|
+
// Each view of the sign-in page requests options for its autofill, so the options requests
|
|
674
|
+
// count on their own key, and spend none of the address's quota for email sign-in.
|
|
675
|
+
const ip = RateLimit.ip(c)
|
|
676
|
+
const refused = await Timing.measure(
|
|
677
|
+
c,
|
|
678
|
+
RateLimit.check(c, {
|
|
679
|
+
limits: [
|
|
680
|
+
{
|
|
681
|
+
binding: 'SIGN_IN_IP_LIMIT',
|
|
682
|
+
key: ip ? `passkey-options:${ip}` : undefined,
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
}),
|
|
686
|
+
'RateLimit.check',
|
|
687
|
+
)
|
|
688
|
+
if (refused) return refused
|
|
689
|
+
|
|
690
|
+
const deferred = link
|
|
691
|
+
? await Timing.measure(
|
|
692
|
+
c,
|
|
693
|
+
PasskeyLink.claim(c, { nonce: link }),
|
|
694
|
+
'PasskeyLink.claim',
|
|
695
|
+
)
|
|
696
|
+
: undefined
|
|
697
|
+
if (link && !deferred)
|
|
698
|
+
return ErrorResponse.json(c, new Passkey.LinkExpiredError())
|
|
699
|
+
|
|
700
|
+
// A first-party app's page runs the ceremony at its own origin, which the challenge stores.
|
|
701
|
+
// Only an assertion for that origin, from a request that origin sends, spends it.
|
|
702
|
+
const page = Cors.appOrigin(c)
|
|
703
|
+
if (page || client_id) {
|
|
704
|
+
const app = client_id
|
|
705
|
+
? await Timing.measure(
|
|
706
|
+
c,
|
|
707
|
+
// A disabled app can share its origin with an enabled one, which CORS admits.
|
|
708
|
+
Apps.get(db, { clientId: client_id, enabled: true }).catch(
|
|
709
|
+
(error: unknown) => {
|
|
710
|
+
if (error instanceof Apps.NotFoundError) return undefined
|
|
711
|
+
throw error
|
|
712
|
+
},
|
|
713
|
+
),
|
|
714
|
+
'Apps.get',
|
|
715
|
+
)
|
|
716
|
+
: undefined
|
|
717
|
+
const refusal = Passkey.checkPage({
|
|
718
|
+
app,
|
|
719
|
+
clientId: client_id,
|
|
720
|
+
origin: c.req.header('origin'),
|
|
721
|
+
})
|
|
722
|
+
if (refusal) return ErrorResponse.json(c, refusal)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// The signature of `oauth_query` is checked when the session is created. Until then, its app
|
|
726
|
+
// selects only the relying party. An app's page continues no authorization, so its challenge
|
|
727
|
+
// names no app.
|
|
728
|
+
const clientId = client({ oauth_query })
|
|
729
|
+
// A device app comes from the code's own row, never from the request, and selects only the
|
|
730
|
+
// relying party, so the challenge names no app.
|
|
731
|
+
const userCode = user_code ? Device.normalize(user_code) : undefined
|
|
732
|
+
const deviceApp =
|
|
733
|
+
!clientId && !client_id && userCode
|
|
734
|
+
? await Timing.measure(
|
|
735
|
+
c,
|
|
736
|
+
Device.client(db, { userCode }),
|
|
737
|
+
'Device.client',
|
|
738
|
+
)
|
|
739
|
+
: undefined
|
|
740
|
+
const appId = clientId ?? client_id ?? deviceApp
|
|
741
|
+
const stored = appId
|
|
742
|
+
? await Timing.measure(
|
|
743
|
+
c,
|
|
744
|
+
Realm.forApp(db, { clientId: appId }),
|
|
745
|
+
'Realm.forApp',
|
|
746
|
+
)
|
|
747
|
+
: [
|
|
748
|
+
await Timing.measure(
|
|
749
|
+
c,
|
|
750
|
+
Realm.get(db, { id: Realm.defaultId }),
|
|
751
|
+
'Realm.get',
|
|
752
|
+
),
|
|
753
|
+
]
|
|
754
|
+
const realms = await Timing.measure(
|
|
755
|
+
c,
|
|
756
|
+
Realm.withDefaults(db, { origin: env.URL, realms: stored }),
|
|
757
|
+
'Realm.withDefaults',
|
|
758
|
+
)
|
|
759
|
+
const relyingParty = Passkey.relyingParty(realms, {
|
|
760
|
+
origin: env.URL,
|
|
761
|
+
page,
|
|
762
|
+
})
|
|
763
|
+
// Only a challenge of the `default` realm reads wallet.tempo, so a link's passkey signs again
|
|
764
|
+
// only for a sign-in whose realms select that realm, at the link's relying party.
|
|
765
|
+
const refusesLink =
|
|
766
|
+
deferred &&
|
|
767
|
+
(relyingParty?.realmId !== Realm.defaultId ||
|
|
768
|
+
relyingParty.rpId !== deferred.rpId)
|
|
769
|
+
if (!relyingParty || refusesLink)
|
|
770
|
+
return ErrorResponse.json(
|
|
771
|
+
c,
|
|
772
|
+
new Passkey.NotPermittedError({
|
|
773
|
+
// A device app stays unnamed, so the refusal names no app that a code reveals.
|
|
774
|
+
clientId: (clientId ?? client_id) || undefined,
|
|
775
|
+
origin: page,
|
|
776
|
+
}),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
// A challenge for a link stores the link's digest, so only that link's code sign-in takes it.
|
|
780
|
+
const linked = link
|
|
781
|
+
? await Timing.measure(
|
|
782
|
+
c,
|
|
783
|
+
PasskeyLink.digest({ nonce: link }),
|
|
784
|
+
'PasskeyLink.digest',
|
|
785
|
+
)
|
|
786
|
+
: undefined
|
|
787
|
+
// On the sign-in page, the challenge of a request with a key is the key's digest, so one
|
|
788
|
+
// passkey assertion signs in and signs the key. A link's step and an app's page sign in only.
|
|
789
|
+
const oneTap =
|
|
790
|
+
oauth_query && !link && !page
|
|
791
|
+
? await Timing.measure(
|
|
792
|
+
c,
|
|
793
|
+
SignInConsent.oneTap(c, { oauthQuery: oauth_query }),
|
|
794
|
+
'SignInConsent.oneTap',
|
|
795
|
+
)
|
|
796
|
+
: undefined
|
|
797
|
+
const challenge = await Timing.measure(
|
|
798
|
+
c,
|
|
799
|
+
Passkey.challenge(db, {
|
|
800
|
+
clientId,
|
|
801
|
+
link: linked,
|
|
802
|
+
oneTap,
|
|
803
|
+
origin: page,
|
|
804
|
+
...relyingParty,
|
|
805
|
+
}),
|
|
806
|
+
'Passkey.challenge',
|
|
807
|
+
)
|
|
808
|
+
// Without a credential ID, the browser offers every discoverable passkey of the relying party,
|
|
809
|
+
// as WebAuthn defines for an empty list. The challenge stays Tempo ID's either way.
|
|
810
|
+
const allowed = (() => {
|
|
811
|
+
if (deferred) return [deferred.id]
|
|
812
|
+
return credentialId === undefined ? [] : [credentialId].flat()
|
|
813
|
+
})()
|
|
814
|
+
const options = schema.passkeyOptions.response.shape.options.parse({
|
|
815
|
+
...Authentication.getOptions({
|
|
816
|
+
challenge,
|
|
817
|
+
...(allowed.length > 0 ? { credentialId: allowed } : {}),
|
|
818
|
+
rpId: relyingParty.rpId,
|
|
819
|
+
userVerification: 'required',
|
|
820
|
+
}).options,
|
|
821
|
+
...(mediation ? { mediation } : {}),
|
|
822
|
+
})
|
|
823
|
+
c.header('cache-control', 'private, no-store')
|
|
824
|
+
return c.json({ options }, 200)
|
|
825
|
+
},
|
|
826
|
+
)
|
|
827
|
+
.post(
|
|
828
|
+
'/v1/auth/passkey/verify',
|
|
829
|
+
OpenApi.describeRoute({
|
|
830
|
+
description:
|
|
831
|
+
'Signs in with a passkey assertion over a challenge from the options, sent from the page that requested them, and sets the session cookie. For a user with no Tempo Account in the realm of the sign-in, the first realm of the app that `oauth_query` names or the `default` realm, it creates one when the realm creates accounts. A failed creation leaves the sign-in signed in, and the next sign-in or `POST /v1/account` creates the account.',
|
|
832
|
+
responses: OpenApi.responses({
|
|
833
|
+
errors: [
|
|
834
|
+
...BodyLimit.errors,
|
|
835
|
+
...Cors.ownPage.errors,
|
|
836
|
+
...OpenApi.validate.errors,
|
|
837
|
+
...ErrorResponse.fromUpstream.errors,
|
|
838
|
+
...RateLimit.check.errors,
|
|
839
|
+
core_Auth.AuthorizationExpiredError,
|
|
840
|
+
Passkey.WebAuthnRejectedError,
|
|
841
|
+
],
|
|
842
|
+
success: {
|
|
843
|
+
description:
|
|
844
|
+
'The signed-in user, or the email step for a wallet.tempo passkey without an email.',
|
|
845
|
+
schema: schema.passkeyVerify.response,
|
|
846
|
+
},
|
|
847
|
+
}),
|
|
848
|
+
tags: ['Authenticate'],
|
|
849
|
+
}),
|
|
850
|
+
OpenApi.validate('json', schema.passkeyVerify.json),
|
|
851
|
+
async (c) => {
|
|
852
|
+
const { assertion, oauth_query } = c.req.valid('json')
|
|
853
|
+
const crossSite = Cors.ownPage(c, { oauth_query })
|
|
854
|
+
if (crossSite) return ErrorResponse.json(c, crossSite)
|
|
855
|
+
const refused = await Timing.measure(
|
|
856
|
+
c,
|
|
857
|
+
RateLimit.check(c, {
|
|
858
|
+
limits: [{ binding: 'SIGN_IN_IP_LIMIT', key: RateLimit.ip(c) }],
|
|
859
|
+
}),
|
|
860
|
+
'RateLimit.check',
|
|
861
|
+
)
|
|
862
|
+
if (refused) return refused
|
|
863
|
+
|
|
864
|
+
// The challenge is spent before any other check, so a failed attempt cannot be retried.
|
|
865
|
+
const challenge = await Timing.measure(
|
|
866
|
+
c,
|
|
867
|
+
Passkey.take(db, {
|
|
868
|
+
clientDataJSON: assertion.metadata.clientDataJSON,
|
|
869
|
+
}),
|
|
870
|
+
'Passkey.take',
|
|
871
|
+
)
|
|
872
|
+
// A challenge serves only the sign-in it was issued for: the same app, or none, from the same
|
|
873
|
+
// page. A challenge for an email step's link serves only that link's code sign-in.
|
|
874
|
+
if (
|
|
875
|
+
!challenge ||
|
|
876
|
+
challenge.link !== undefined ||
|
|
877
|
+
challenge.clientId !== client({ oauth_query }) ||
|
|
878
|
+
challenge.origin !== Cors.appOrigin(c)
|
|
879
|
+
)
|
|
880
|
+
return ErrorResponse.json(c, new Passkey.WebAuthnRejectedError())
|
|
881
|
+
|
|
882
|
+
const { id } = assertion
|
|
883
|
+
const key = await Timing.measure(
|
|
884
|
+
c,
|
|
885
|
+
Passkey.get(db, { id, rpId: challenge.rpId }),
|
|
886
|
+
'Passkey.get',
|
|
887
|
+
)
|
|
888
|
+
// A passkey that Tempo ID does not store yet may be wallet.tempo's, which shares the `default`
|
|
889
|
+
// realm's relying party, and links once it verifies.
|
|
890
|
+
const credential = await (async () => {
|
|
891
|
+
if (key || !c.env.WALLET || challenge.realmId !== Realm.defaultId)
|
|
892
|
+
return undefined
|
|
893
|
+
try {
|
|
894
|
+
return await Timing.measure(
|
|
895
|
+
c,
|
|
896
|
+
Wallet.credential(c.env.WALLET, { id }),
|
|
897
|
+
'Wallet.credential',
|
|
898
|
+
)
|
|
899
|
+
} catch (error) {
|
|
900
|
+
// Only the binding call throws, so the failure is wallet.tempo's.
|
|
901
|
+
Log.warn(c, {
|
|
902
|
+
error,
|
|
903
|
+
message: 'The wallet service failed a passkey lookup.',
|
|
904
|
+
})
|
|
905
|
+
return (
|
|
906
|
+
(error instanceof Error && ErrorResponse.fromUpstream(error)) ||
|
|
907
|
+
new ErrorResponse.UpstreamUnavailableError()
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
})()
|
|
911
|
+
if (credential instanceof ErrorResponse.ResponseError)
|
|
912
|
+
return ErrorResponse.json(c, credential)
|
|
913
|
+
const publicKey = key?.publicKey ?? credential?.publicKey
|
|
914
|
+
if (
|
|
915
|
+
!publicKey ||
|
|
916
|
+
!Passkey.verify(assertion, {
|
|
917
|
+
challenge: challenge.challenge,
|
|
918
|
+
origin: challenge.origin ?? new URL(env.URL).origin,
|
|
919
|
+
publicKey: publicKey as `0x${string}`,
|
|
920
|
+
rpId: challenge.rpId,
|
|
921
|
+
})
|
|
922
|
+
)
|
|
923
|
+
return ErrorResponse.json(c, new Passkey.WebAuthnRejectedError())
|
|
924
|
+
|
|
925
|
+
// A wallet.tempo account without an email links once the user verifies one with a code.
|
|
926
|
+
if (credential && !credential.email) {
|
|
927
|
+
const link = await Timing.measure(
|
|
928
|
+
c,
|
|
929
|
+
PasskeyLink.defer(c, { credential, id, rpId: challenge.rpId }),
|
|
930
|
+
'PasskeyLink.defer',
|
|
931
|
+
)
|
|
932
|
+
c.header('cache-control', 'private, no-store')
|
|
933
|
+
return c.json(
|
|
934
|
+
{ step: { link, signed: false, type: 'email' as const } },
|
|
935
|
+
200,
|
|
936
|
+
)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const userId = await (async () => {
|
|
940
|
+
if (key) return key.userId
|
|
941
|
+
// wallet.tempo verified the email, so its user is found or created by that address.
|
|
942
|
+
const user = await Timing.measure(
|
|
943
|
+
c,
|
|
944
|
+
emailUser(c, { email: credential!.email! }),
|
|
945
|
+
'authenticate.emailUser',
|
|
946
|
+
)
|
|
947
|
+
return Timing.measure(
|
|
948
|
+
c,
|
|
949
|
+
Passkey.link(db, {
|
|
950
|
+
credential: credential!,
|
|
951
|
+
id,
|
|
952
|
+
rpId: challenge.rpId,
|
|
953
|
+
userId: user,
|
|
954
|
+
}),
|
|
955
|
+
'Passkey.link',
|
|
956
|
+
)
|
|
957
|
+
})()
|
|
958
|
+
// Another passkey holds the address that wallet.tempo returned.
|
|
959
|
+
if (!userId)
|
|
960
|
+
return ErrorResponse.json(c, new Passkey.WebAuthnRejectedError())
|
|
961
|
+
|
|
962
|
+
const result = await Timing.measure(
|
|
963
|
+
c,
|
|
964
|
+
c.var.auth.api.signInPasskey({
|
|
965
|
+
asResponse: false,
|
|
966
|
+
body: {
|
|
967
|
+
userId,
|
|
968
|
+
...(oauth_query ? { oauth_query } : {}),
|
|
969
|
+
},
|
|
970
|
+
// Better Auth sets `accept` to continue an authorization as JSON, and a request's own headers
|
|
971
|
+
// are immutable.
|
|
972
|
+
headers: new Headers(c.req.raw.headers),
|
|
973
|
+
request: c.req.raw,
|
|
974
|
+
returnHeaders: true,
|
|
975
|
+
}),
|
|
976
|
+
'auth.signInPasskey',
|
|
977
|
+
).catch((error: unknown) => {
|
|
978
|
+
if (isAPIError(error) && error.body?.error === 'invalid_signature')
|
|
979
|
+
return new core_Auth.AuthorizationExpiredError()
|
|
980
|
+
throw error
|
|
981
|
+
})
|
|
982
|
+
if (result instanceof ErrorResponse.ResponseError)
|
|
983
|
+
return ErrorResponse.json(c, result)
|
|
984
|
+
|
|
985
|
+
for (const cookie of result.headers.getSetCookie())
|
|
986
|
+
c.header('set-cookie', cookie, { append: true })
|
|
987
|
+
c.header('cache-control', 'private, no-store')
|
|
988
|
+
|
|
989
|
+
// With `oauth_query`, Better Auth continues the app's authorization once the session exists, and
|
|
990
|
+
// returns its redirect in place of the user.
|
|
991
|
+
const signedIn = schema.verify.signedIn.parse(result.response)
|
|
992
|
+
// A wallet.tempo passkey owns its account, so only a Tempo ID passkey's user can receive one.
|
|
993
|
+
await Timing.measure(
|
|
994
|
+
c,
|
|
995
|
+
FirstAccount.create(c, { clientId: client({ oauth_query }), userId }),
|
|
996
|
+
'FirstAccount.create',
|
|
997
|
+
)
|
|
998
|
+
if ('user' in signedIn) {
|
|
999
|
+
const { user } = signedIn
|
|
1000
|
+
return c.json(
|
|
1001
|
+
{ redirect: null, user: { email: user.email, id: user.id } },
|
|
1002
|
+
200,
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
const [user] = await Timing.measure(
|
|
1006
|
+
c,
|
|
1007
|
+
db
|
|
1008
|
+
.select({ email: Schema.user.email, id: Schema.user.id })
|
|
1009
|
+
.from(Schema.user)
|
|
1010
|
+
.where(eq(Schema.user.id, userId)),
|
|
1011
|
+
'db.user.get',
|
|
1012
|
+
)
|
|
1013
|
+
if (!user) throw new Error('The signed-in user is missing.')
|
|
1014
|
+
|
|
1015
|
+
// The sign-in page showed the request's key, so the sign-in grants it. A one-tap challenge was
|
|
1016
|
+
// the key's digest, so the same assertion signs it for an account that this passkey owns.
|
|
1017
|
+
const redirect = await Timing.measure(
|
|
1018
|
+
c,
|
|
1019
|
+
SignInConsent.afterSignIn(c, {
|
|
1020
|
+
oneTap: challenge.witness
|
|
1021
|
+
? { assertion, witness: challenge.witness }
|
|
1022
|
+
: undefined,
|
|
1023
|
+
redirect: signedIn.url,
|
|
1024
|
+
setCookie: result.headers.getSetCookie(),
|
|
1025
|
+
}),
|
|
1026
|
+
'SignInConsent.afterSignIn',
|
|
1027
|
+
)
|
|
1028
|
+
return c.json({ redirect, user }, 200)
|
|
1029
|
+
},
|
|
1030
|
+
)
|
|
1031
|
+
.post(
|
|
1032
|
+
'/v1/auth/logout',
|
|
1033
|
+
OpenApi.describeRoute({
|
|
1034
|
+
description:
|
|
1035
|
+
'Signs out: ends the session, revokes the refresh tokens that apps received through it, and clears its cookie.',
|
|
1036
|
+
responses: OpenApi.responses({
|
|
1037
|
+
errors: [...BodyLimit.errors, ErrorResponse.CrossSiteRequestError],
|
|
1038
|
+
success: {
|
|
1039
|
+
description: 'The session is over.',
|
|
1040
|
+
schema: schema.logout.response,
|
|
1041
|
+
},
|
|
1042
|
+
}),
|
|
1043
|
+
tags: ['Authenticate'],
|
|
1044
|
+
}),
|
|
1045
|
+
async (c) => {
|
|
1046
|
+
const headers = c.req.raw.headers
|
|
1047
|
+
const session = await Timing.measure(
|
|
1048
|
+
c,
|
|
1049
|
+
c.var.auth.api.getSession({ headers }),
|
|
1050
|
+
'auth.getSession',
|
|
1051
|
+
)
|
|
1052
|
+
// Better Auth revokes a session's tokens when it deletes the session, except `offline_access`
|
|
1053
|
+
// refresh tokens, which every app requests. Sign-out ends the apps' access too.
|
|
1054
|
+
if (session)
|
|
1055
|
+
await Timing.measure(
|
|
1056
|
+
c,
|
|
1057
|
+
db
|
|
1058
|
+
.update(Schema.oauthRefreshToken)
|
|
1059
|
+
.set({ revoked: new Date() })
|
|
1060
|
+
.where(
|
|
1061
|
+
and(
|
|
1062
|
+
eq(Schema.oauthRefreshToken.sessionId, session.session.id),
|
|
1063
|
+
isNull(Schema.oauthRefreshToken.revoked),
|
|
1064
|
+
),
|
|
1065
|
+
),
|
|
1066
|
+
'db.oauthRefreshToken.revoke',
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
const result = await Timing.measure(
|
|
1070
|
+
c,
|
|
1071
|
+
c.var.auth.api.signOut({ headers, returnHeaders: true }),
|
|
1072
|
+
'auth.signOut',
|
|
1073
|
+
)
|
|
1074
|
+
for (const cookie of result.headers.getSetCookie())
|
|
1075
|
+
c.header('set-cookie', cookie, { append: true })
|
|
1076
|
+
// The next sign-in chooses each app's account again.
|
|
1077
|
+
c.header('set-cookie', AccountStep.clear(), { append: true })
|
|
1078
|
+
c.header('cache-control', 'private, no-store')
|
|
1079
|
+
return c.json({ signed_out: true as const }, 200)
|
|
1080
|
+
},
|
|
1081
|
+
)
|
|
1082
|
+
.get(
|
|
1083
|
+
'/v1/auth/session',
|
|
1084
|
+
OpenApi.describeRoute({
|
|
1085
|
+
description: 'Reads the signed-in user, or `null` without a session.',
|
|
1086
|
+
responses: OpenApi.responses({
|
|
1087
|
+
success: {
|
|
1088
|
+
description: 'The session.',
|
|
1089
|
+
schema: schema.session.response,
|
|
1090
|
+
},
|
|
1091
|
+
}),
|
|
1092
|
+
tags: ['Authenticate'],
|
|
1093
|
+
}),
|
|
1094
|
+
async (c) => {
|
|
1095
|
+
const session = await Timing.measure(c, Session.get(c), 'Session.get')
|
|
1096
|
+
c.header('cache-control', 'private, no-store')
|
|
1097
|
+
if (!session) return c.json({ user: null }, 200)
|
|
1098
|
+
return c.json(
|
|
1099
|
+
{
|
|
1100
|
+
user: {
|
|
1101
|
+
email: session.user.email,
|
|
1102
|
+
id: session.user.id,
|
|
1103
|
+
// Better Auth stores an empty name for a user who signed up without one.
|
|
1104
|
+
name: session.user.name || null,
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
200,
|
|
1108
|
+
)
|
|
1109
|
+
},
|
|
1110
|
+
)
|
|
1111
|
+
.get(
|
|
1112
|
+
'/v1/auth/methods',
|
|
1113
|
+
OpenApi.describeRoute({
|
|
1114
|
+
description:
|
|
1115
|
+
"Reads the sign-in methods that the sign-in page offers, the key that a signed authorization request in `oauth_query` grants, and the page that sign-in returns to. A device code in `return_to` names the app that requested it, while the code is pending and bound to no user. The read binds nothing, and counts against the IP address's sign-in limit. Without `oauth_query`, a client ID reads the `default` realm, as the other sign-in routes do.",
|
|
1116
|
+
responses: OpenApi.responses({
|
|
1117
|
+
errors: [...OpenApi.validate.errors],
|
|
1118
|
+
success: {
|
|
1119
|
+
description: 'The sign-in methods.',
|
|
1120
|
+
schema: schema.methods.response,
|
|
1121
|
+
},
|
|
1122
|
+
}),
|
|
1123
|
+
tags: ['Authenticate'],
|
|
1124
|
+
}),
|
|
1125
|
+
OpenApi.validate('query', schema.methods.query),
|
|
1126
|
+
async (c) => {
|
|
1127
|
+
const input = c.req.valid('query')
|
|
1128
|
+
// Sign-in returns only to an allowlisted path, so `return_to` never redirects to another site.
|
|
1129
|
+
const returnTo = Device.returnTo(input.return_to, { origin: env.URL })
|
|
1130
|
+
// A failed Google sign-in's `error` is not part of the signed request, so the route drops it.
|
|
1131
|
+
const query = (() => {
|
|
1132
|
+
if (!input.oauth_query) return undefined
|
|
1133
|
+
const query = new URLSearchParams(input.oauth_query)
|
|
1134
|
+
query.delete('error')
|
|
1135
|
+
return query.has('sig') ? query : undefined
|
|
1136
|
+
})()
|
|
1137
|
+
|
|
1138
|
+
// The user code comes from `return_to`, but the app comes from the code's own row, so the
|
|
1139
|
+
// visitor selects no app by itself.
|
|
1140
|
+
const userCode = returnTo
|
|
1141
|
+
? Device.normalize(
|
|
1142
|
+
new URL(returnTo, env.URL).searchParams.get('code') ?? '',
|
|
1143
|
+
)
|
|
1144
|
+
: undefined
|
|
1145
|
+
const clientId = await (async () => {
|
|
1146
|
+
if (query) return query.get('client_id') ?? undefined
|
|
1147
|
+
if (!userCode) return undefined
|
|
1148
|
+
// A live code's realms change the page, so each IP address's lookups count, and keep a guess
|
|
1149
|
+
// of a live code out of reach (RFC 8628 §5.1). Past the limit, the page shows the `default`
|
|
1150
|
+
// realm.
|
|
1151
|
+
const ip = RateLimit.ip(c)
|
|
1152
|
+
if (!ip) return undefined
|
|
1153
|
+
const outcome = await Timing.measure(
|
|
1154
|
+
c,
|
|
1155
|
+
c.env.SIGN_IN_IP_LIMIT.limit({ key: `device-lookup:${ip}` }),
|
|
1156
|
+
'ratelimit.limit',
|
|
1157
|
+
)
|
|
1158
|
+
if (!outcome.success) return undefined
|
|
1159
|
+
return await Timing.measure(
|
|
1160
|
+
c,
|
|
1161
|
+
Device.client(db, { userCode }),
|
|
1162
|
+
'Device.client',
|
|
1163
|
+
)
|
|
1164
|
+
})()
|
|
1165
|
+
const stored = clientId
|
|
1166
|
+
? await Timing.measure(
|
|
1167
|
+
c,
|
|
1168
|
+
Realm.forApp(db, { clientId }),
|
|
1169
|
+
'Realm.forApp',
|
|
1170
|
+
)
|
|
1171
|
+
: [
|
|
1172
|
+
await Timing.measure(
|
|
1173
|
+
c,
|
|
1174
|
+
Realm.get(db, { id: Realm.defaultId }),
|
|
1175
|
+
'Realm.get',
|
|
1176
|
+
),
|
|
1177
|
+
]
|
|
1178
|
+
const realms = await Timing.measure(
|
|
1179
|
+
c,
|
|
1180
|
+
Realm.withDefaults(db, { origin: env.URL, realms: stored }),
|
|
1181
|
+
'Realm.withDefaults',
|
|
1182
|
+
)
|
|
1183
|
+
const request = query
|
|
1184
|
+
? await Timing.measure(c, granted(c, { query }), 'authenticate.granted')
|
|
1185
|
+
: null
|
|
1186
|
+
return c.json(
|
|
1187
|
+
{
|
|
1188
|
+
google: Boolean(Auth.google()),
|
|
1189
|
+
passkeys: Boolean(Passkey.relyingParty(realms, { origin: env.URL })),
|
|
1190
|
+
request,
|
|
1191
|
+
return_to: returnTo ?? null,
|
|
1192
|
+
user_code: userCode || null,
|
|
1193
|
+
},
|
|
1194
|
+
200,
|
|
1195
|
+
)
|
|
1196
|
+
},
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
/** Returns the client ID of the app that `oauth_query` names, or `null` without one. */
|
|
1200
|
+
function client(options: client.Options) {
|
|
1201
|
+
if (!options.oauth_query) return null
|
|
1202
|
+
return new URLSearchParams(options.oauth_query).get('client_id')
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
declare namespace client {
|
|
1206
|
+
type Options = {
|
|
1207
|
+
/** The sign-in page's query string, as the request sends it. */
|
|
1208
|
+
oauth_query?: string | undefined
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Returns the ID of the user with `email`, which wallet.tempo verified, and creates that user when
|
|
1214
|
+
* none exists. A concurrent first sign-in can create the user first, so the read repeats.
|
|
1215
|
+
*/
|
|
1216
|
+
async function emailUser(c: Context<Env>, options: emailUser.Options) {
|
|
1217
|
+
const email = options.email.trim().toLowerCase()
|
|
1218
|
+
const context = await Timing.measure(c, c.var.auth.$context, 'auth.context')
|
|
1219
|
+
const found = await Timing.measure(
|
|
1220
|
+
c,
|
|
1221
|
+
context.internalAdapter.findUserByEmail(email),
|
|
1222
|
+
'db.user.findByEmail',
|
|
1223
|
+
)
|
|
1224
|
+
if (found) return found.user.id
|
|
1225
|
+
|
|
1226
|
+
try {
|
|
1227
|
+
const created = await Timing.measure(
|
|
1228
|
+
c,
|
|
1229
|
+
context.internalAdapter.createUser(
|
|
1230
|
+
{ email, emailVerified: true, name: '' },
|
|
1231
|
+
{ method: 'passkey' },
|
|
1232
|
+
),
|
|
1233
|
+
'db.user.create',
|
|
1234
|
+
)
|
|
1235
|
+
return created.id
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
const again = await Timing.measure(
|
|
1238
|
+
c,
|
|
1239
|
+
context.internalAdapter.findUserByEmail(email),
|
|
1240
|
+
'db.user.findByEmail',
|
|
1241
|
+
)
|
|
1242
|
+
if (again) return again.user.id
|
|
1243
|
+
throw error
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
declare namespace emailUser {
|
|
1248
|
+
type Options = {
|
|
1249
|
+
/** The email address that wallet.tempo verified. */
|
|
1250
|
+
email: string
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/**
|
|
1255
|
+
* Returns the app and the key of the signed authorization request `query`, which the sign-in grants,
|
|
1256
|
+
* or `null` for a forged or expired query, an unknown app, or a request without a key. A request
|
|
1257
|
+
* with a `challenge` shows its key on the approval page instead, beside its message.
|
|
1258
|
+
*/
|
|
1259
|
+
async function granted(c: Context<Env>, options: granted.Options) {
|
|
1260
|
+
const { query } = options
|
|
1261
|
+
if (!query.has('key_authorization') || query.has('challenge')) return null
|
|
1262
|
+
|
|
1263
|
+
const context = await Timing.measure(c, c.var.auth.$context, 'auth.context')
|
|
1264
|
+
const verified = await Timing.measure(
|
|
1265
|
+
c,
|
|
1266
|
+
verifyOAuthQueryParams(query.toString(), context.secret),
|
|
1267
|
+
'auth.verifyOAuthQueryParams',
|
|
1268
|
+
)
|
|
1269
|
+
const clientId = query.get('client_id')
|
|
1270
|
+
if (!verified || !clientId) return null
|
|
1271
|
+
|
|
1272
|
+
const app = await Timing.measure(
|
|
1273
|
+
c,
|
|
1274
|
+
Apps.get(db, { clientId }).catch((error: unknown) => {
|
|
1275
|
+
if (error instanceof Apps.NotFoundError) return undefined
|
|
1276
|
+
throw error
|
|
1277
|
+
}),
|
|
1278
|
+
'Apps.get',
|
|
1279
|
+
)
|
|
1280
|
+
const key = Authorize.fromQuery(query)
|
|
1281
|
+
if (!app || !key || key instanceof ErrorResponse.ResponseError) return null
|
|
1282
|
+
return { app: app.name ?? clientId, key: KeyAuthorization.describe(key) }
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
declare namespace granted {
|
|
1286
|
+
type Options = {
|
|
1287
|
+
/** The sign-in page's query, without a failed Google sign-in's `error`. */
|
|
1288
|
+
query: URLSearchParams
|
|
1289
|
+
}
|
|
1290
|
+
}
|