@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,3001 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lays out every Tempo ID flow as a canvas of artboards: where each flow enters, the screens it
|
|
3
|
+
* shows, the request on each arrow, and where it exits. Each artboard renders its page's screen
|
|
4
|
+
* from `src/app/screens/` with sample props, so the canvas changes with the pages. An app's own
|
|
5
|
+
* screens, in the headless flows, render with the third-party tokens, so they read apart from
|
|
6
|
+
* Tempo ID's.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
10
|
+
import type { FormEvent, ReactNode } from 'react'
|
|
11
|
+
import ChevronRight from '~icons/lucide/chevron-right'
|
|
12
|
+
import { CodeInput } from '../components/CodeInput.js'
|
|
13
|
+
import { Account } from '../screens/Account.js'
|
|
14
|
+
import { Approval, InvalidRequest } from '../screens/Approval.js'
|
|
15
|
+
import { ChooseAccount } from '../screens/ChooseAccount.js'
|
|
16
|
+
import { Deposit } from '../screens/Deposit.js'
|
|
17
|
+
import { DeviceApproval } from '../screens/DeviceApproval.js'
|
|
18
|
+
import { SignIn } from '../screens/SignIn.js'
|
|
19
|
+
import { style, variants, vars } from '../zyzz.config.js'
|
|
20
|
+
|
|
21
|
+
/** One box in a flow: an entry point, a Tempo ID screen, or an external step such as a provider. */
|
|
22
|
+
type Node = {
|
|
23
|
+
/** A secondary line, such as the request that enters the flow. */
|
|
24
|
+
detail?: string | undefined
|
|
25
|
+
/** The request or event on the arrow that leads into this node. Absent on the first node. */
|
|
26
|
+
edge?: string | undefined
|
|
27
|
+
/** The screen's or the step's name. */
|
|
28
|
+
name: string
|
|
29
|
+
/** Who renders the screen: Tempo ID when absent, or `app` for an app's own interface. */
|
|
30
|
+
owner?: 'app' | undefined
|
|
31
|
+
/** The screen, for an artboard. An external step has none. */
|
|
32
|
+
screen?: ReactNode | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One path through Tempo ID, from its entry point to its exit. */
|
|
36
|
+
type Flow = {
|
|
37
|
+
/** What the flow does, and when it applies, with code spans between backticks. */
|
|
38
|
+
description: string
|
|
39
|
+
/** The entry point, each screen or external step, and the exit, in order. */
|
|
40
|
+
nodes: readonly Node[]
|
|
41
|
+
/** The flow's name. */
|
|
42
|
+
title: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A titled group of related flows. */
|
|
46
|
+
type Section = {
|
|
47
|
+
/** The flows, in reading order. */
|
|
48
|
+
flows: readonly Flow[]
|
|
49
|
+
/** The group's name. */
|
|
50
|
+
title: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Route = createFileRoute('/_/flows')({
|
|
54
|
+
// The route splitter drops the style namespace, so the component stays in this chunk.
|
|
55
|
+
codeSplitGroupings: [],
|
|
56
|
+
component: Page,
|
|
57
|
+
// The canvas renders in the browser alone, since its sample balances format in the browser's
|
|
58
|
+
// locale and its sample expiries read the clock.
|
|
59
|
+
ssr: false,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
/** Renders the canvas. */
|
|
63
|
+
function Page(): ReactNode {
|
|
64
|
+
return (
|
|
65
|
+
<main {...styles.canvas()}>
|
|
66
|
+
<header {...styles.intro()}>
|
|
67
|
+
<h1 {...styles.title()}>Flows</h1>
|
|
68
|
+
<p {...styles.lede()}>
|
|
69
|
+
Every path through Tempo ID, from the entry point on the left to the
|
|
70
|
+
exit on the right. Each arrow names the request or event that leads to
|
|
71
|
+
the next step. Scroll to pan.
|
|
72
|
+
</p>
|
|
73
|
+
</header>
|
|
74
|
+
{sections().map((section) => (
|
|
75
|
+
<section {...styles.section()} key={section.title}>
|
|
76
|
+
<h2 {...styles.sectionTitle()}>{section.title}</h2>
|
|
77
|
+
{section.flows.map((flow) => (
|
|
78
|
+
<article {...styles.flow()} key={flow.title}>
|
|
79
|
+
<header {...styles.flowHeader()}>
|
|
80
|
+
<h3 {...styles.flowTitle()}>{flow.title}</h3>
|
|
81
|
+
<p {...styles.flowDescription()}>
|
|
82
|
+
<Description text={flow.description} />
|
|
83
|
+
</p>
|
|
84
|
+
</header>
|
|
85
|
+
<ol {...styles.row()}>
|
|
86
|
+
{flow.nodes.map((node, index) => (
|
|
87
|
+
<li {...styles.step()} key={`${index}:${node.name}`}>
|
|
88
|
+
{index > 0 ? <Arrow label={node.edge} /> : null}
|
|
89
|
+
{node.screen ? (
|
|
90
|
+
<figure {...styles.artboard()}>
|
|
91
|
+
<figcaption {...styles.frameLabel()}>
|
|
92
|
+
{node.owner === 'app'
|
|
93
|
+
? `App · ${node.name}`
|
|
94
|
+
: node.name}
|
|
95
|
+
</figcaption>
|
|
96
|
+
{node.owner === 'app' ? (
|
|
97
|
+
<div {...vars({ set: 'thirdParty' })}>
|
|
98
|
+
<div {...styles.frame()}>{node.screen}</div>
|
|
99
|
+
</div>
|
|
100
|
+
) : (
|
|
101
|
+
<div {...styles.frame()}>{node.screen}</div>
|
|
102
|
+
)}
|
|
103
|
+
</figure>
|
|
104
|
+
) : (
|
|
105
|
+
<External
|
|
106
|
+
detail={node.detail}
|
|
107
|
+
kind={(() => {
|
|
108
|
+
if (index === 0) return 'Entry'
|
|
109
|
+
if (index === flow.nodes.length - 1) return 'Exit'
|
|
110
|
+
return 'External'
|
|
111
|
+
})()}
|
|
112
|
+
name={node.name}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
</li>
|
|
116
|
+
))}
|
|
117
|
+
</ol>
|
|
118
|
+
</article>
|
|
119
|
+
))}
|
|
120
|
+
</section>
|
|
121
|
+
))}
|
|
122
|
+
</main>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Renders `text`, with each span between backticks as code. */
|
|
127
|
+
function Description(props: Description.Props): ReactNode {
|
|
128
|
+
// Splitting on the backticks puts each code span at an odd index.
|
|
129
|
+
return props.text.split('`').map((part, index) =>
|
|
130
|
+
index % 2 ? (
|
|
131
|
+
<code {...styles.code()} key={index}>
|
|
132
|
+
{part}
|
|
133
|
+
</code>
|
|
134
|
+
) : (
|
|
135
|
+
part
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
declare namespace Description {
|
|
141
|
+
type Props = {
|
|
142
|
+
/** The flow's description, with code spans between backticks. */
|
|
143
|
+
text: string
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A labeled arrow between two nodes. */
|
|
148
|
+
function Arrow(props: Arrow.Props): ReactNode {
|
|
149
|
+
return (
|
|
150
|
+
<span {...styles.arrow()} aria-label={props.label ?? 'Then'} role="img">
|
|
151
|
+
<span {...styles.edge()}>{props.label}</span>
|
|
152
|
+
<span {...styles.shaft()}>
|
|
153
|
+
<span {...styles.line()} />
|
|
154
|
+
<ChevronRight {...styles.head()} aria-hidden />
|
|
155
|
+
</span>
|
|
156
|
+
</span>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
declare namespace Arrow {
|
|
161
|
+
type Props = {
|
|
162
|
+
/** The request or event that the arrow stands for. */
|
|
163
|
+
label?: string | undefined
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A step outside Tempo ID's pages, such as the app that starts a flow or a provider's page. */
|
|
168
|
+
function External(props: External.Props): ReactNode {
|
|
169
|
+
return (
|
|
170
|
+
<div {...styles.external({ kind: props.kind })}>
|
|
171
|
+
<span {...styles.kind()}>{props.kind}</span>
|
|
172
|
+
<span {...styles.externalName()}>{props.name}</span>
|
|
173
|
+
{props.detail ? <code {...styles.detail()}>{props.detail}</code> : null}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
declare namespace External {
|
|
179
|
+
type Props = {
|
|
180
|
+
/** A secondary line, such as the request that enters the flow. */
|
|
181
|
+
detail?: string | undefined
|
|
182
|
+
/** Where the step sits in its flow. */
|
|
183
|
+
kind: 'Entry' | 'Exit' | 'External'
|
|
184
|
+
/** The step's name. */
|
|
185
|
+
name: string
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A sample app's own screen, which a headless app builds on Tempo ID's API. It draws its own
|
|
191
|
+
* controls, styled after a food-delivery app, so it reads apart from Tempo ID's pages.
|
|
192
|
+
*/
|
|
193
|
+
function AppScreen(props: AppScreen.Props): ReactNode {
|
|
194
|
+
return (
|
|
195
|
+
<div {...styles.app()}>
|
|
196
|
+
<header {...styles.appHeader()}>
|
|
197
|
+
<h4 {...styles.appTitle()}>{props.title}</h4>
|
|
198
|
+
{props.description ? (
|
|
199
|
+
<p {...styles.appDescription()}>{props.description}</p>
|
|
200
|
+
) : null}
|
|
201
|
+
</header>
|
|
202
|
+
{props.children}
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
declare namespace AppScreen {
|
|
208
|
+
type Props = {
|
|
209
|
+
/** The screen's controls. */
|
|
210
|
+
children?: ReactNode | undefined
|
|
211
|
+
/** The line below the title. */
|
|
212
|
+
description?: string | undefined
|
|
213
|
+
/** The screen's heading. */
|
|
214
|
+
title: string
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** A pill button in the sample app's style. */
|
|
219
|
+
function AppButton(props: AppButton.Props): ReactNode {
|
|
220
|
+
return (
|
|
221
|
+
<button {...styles.appButton({ tone: props.tone })} type="button">
|
|
222
|
+
{props.children}
|
|
223
|
+
</button>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
declare namespace AppButton {
|
|
228
|
+
type Props = {
|
|
229
|
+
/** The button's label. */
|
|
230
|
+
children: ReactNode
|
|
231
|
+
/** The button's colors: the brand red, Google's blue, or a quiet gray. */
|
|
232
|
+
tone: 'brand' | 'google' | 'quiet'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The sample app's labeled email field. */
|
|
237
|
+
function AppEmailField(): ReactNode {
|
|
238
|
+
return (
|
|
239
|
+
<label {...styles.appField()}>
|
|
240
|
+
<span {...styles.appLabel()}>Email</span>
|
|
241
|
+
<input {...styles.appInput()} placeholder="Required" readOnly />
|
|
242
|
+
</label>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The sample app's sign-in, which offers Google, a passkey, and an email. */
|
|
247
|
+
function AppSignIn(): ReactNode {
|
|
248
|
+
return (
|
|
249
|
+
<AppScreen title="Sign in or sign up">
|
|
250
|
+
<AppButton tone="google">Continue with Google</AppButton>
|
|
251
|
+
<AppButton tone="quiet">Continue with passkey</AppButton>
|
|
252
|
+
<p {...styles.appDivider()}>or continue with email</p>
|
|
253
|
+
<AppEmailField />
|
|
254
|
+
<AppButton tone="brand">Continue</AppButton>
|
|
255
|
+
</AppScreen>
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The sample app's email step, which an unlinked passkey needs before its code. */
|
|
260
|
+
function AppEmail(): ReactNode {
|
|
261
|
+
return (
|
|
262
|
+
<AppScreen
|
|
263
|
+
description="Sign in with your email to link this passkey."
|
|
264
|
+
title="Add your email"
|
|
265
|
+
>
|
|
266
|
+
<AppEmailField />
|
|
267
|
+
<AppButton tone="brand">Continue</AppButton>
|
|
268
|
+
</AppScreen>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The sample app's code step. */
|
|
273
|
+
function AppCode(): ReactNode {
|
|
274
|
+
return (
|
|
275
|
+
<AppScreen
|
|
276
|
+
description={`Enter the 6-digit code sent to ${email}.`}
|
|
277
|
+
title="Check your email"
|
|
278
|
+
>
|
|
279
|
+
<CodeInput aria-label="Code" length={6} readOnly />
|
|
280
|
+
<AppButton tone="brand">Verify</AppButton>
|
|
281
|
+
</AppScreen>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The sample app once it holds the authorization. */
|
|
286
|
+
function AppSignedIn(): ReactNode {
|
|
287
|
+
return (
|
|
288
|
+
<AppScreen description={email} title="You're signed in">
|
|
289
|
+
<p {...styles.appAddress()}>{accounts[0].address}</p>
|
|
290
|
+
<AppButton tone="quiet">Sign out</AppButton>
|
|
291
|
+
</AppScreen>
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** A handler that the canvas's screens call, which does nothing. */
|
|
296
|
+
function noop() {}
|
|
297
|
+
|
|
298
|
+
/** A form handler that the canvas's screens call, which keeps the form from submitting. */
|
|
299
|
+
function prevent(event: FormEvent<HTMLFormElement>) {
|
|
300
|
+
event.preventDefault()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The sample user's Tempo Accounts: one owned by a Privy wallet, and one by a passkey. */
|
|
304
|
+
const accounts = [
|
|
305
|
+
{
|
|
306
|
+
address: '0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9f3c',
|
|
307
|
+
balance: { amount: '1250.5', currency: 'USD' },
|
|
308
|
+
owner: 'privy',
|
|
309
|
+
realm: null,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
address: '0x7c4d2e9a1b3f5c7d9e0a2b4c6d8e0f1a3b5c21ab',
|
|
313
|
+
balance: { amount: '42', currency: 'USD' },
|
|
314
|
+
owner: 'webAuthn',
|
|
315
|
+
realm: null,
|
|
316
|
+
},
|
|
317
|
+
] as const satisfies readonly ChooseAccount.Account[]
|
|
318
|
+
|
|
319
|
+
/** The sample address that the code steps name. */
|
|
320
|
+
const email = 'user@example.com'
|
|
321
|
+
|
|
322
|
+
/** The sample refusal of an upstream service, which the failure flows show. */
|
|
323
|
+
const unavailable = 'An upstream service is unavailable. Retry later.'
|
|
324
|
+
|
|
325
|
+
/** The key that the samples request: 100 USD a day for a week, in pathUSD. */
|
|
326
|
+
function key(): Approval.Key {
|
|
327
|
+
return {
|
|
328
|
+
expiry: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60,
|
|
329
|
+
limits: [
|
|
330
|
+
{
|
|
331
|
+
amount: '100',
|
|
332
|
+
currency: 'USD',
|
|
333
|
+
period: 24 * 60 * 60,
|
|
334
|
+
tokens: [{ amount: '100', symbol: 'pathUSD' }],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The props that `Approval` takes for a key, before a decision. */
|
|
341
|
+
function approval(): Approval.Props {
|
|
342
|
+
return {
|
|
343
|
+
app: 'Tempo Wallet',
|
|
344
|
+
approving: false,
|
|
345
|
+
denying: false,
|
|
346
|
+
destination: 'wallet.tempo.xyz',
|
|
347
|
+
email,
|
|
348
|
+
heading: 'h4',
|
|
349
|
+
keyAuthorization: key(),
|
|
350
|
+
onApprove: noop,
|
|
351
|
+
onDeny: noop,
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** The request that the sign-in page shows and grants: the app and its key. */
|
|
356
|
+
function signInRequest(): NonNullable<SignIn.Props['request']> {
|
|
357
|
+
return { app: 'Tempo Wallet', key: key() }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The entry of an app's redirect with a key, which a signed-out user signs in for. */
|
|
361
|
+
const keyedEntry = {
|
|
362
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
363
|
+
name: 'Web app',
|
|
364
|
+
} as const satisfies Node
|
|
365
|
+
|
|
366
|
+
/** The props that `Account` takes once the user is signed in. */
|
|
367
|
+
function account(): Account.Props {
|
|
368
|
+
return {
|
|
369
|
+
accounts: [accounts[0]],
|
|
370
|
+
email,
|
|
371
|
+
heading: 'h4',
|
|
372
|
+
onSignOut: noop,
|
|
373
|
+
signingOut: false,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The props that `ChooseAccount` takes with nothing chosen. */
|
|
378
|
+
function chooseAccount(): ChooseAccount.Props {
|
|
379
|
+
return { accounts, app: 'Tempo Wallet', heading: 'h4', onChoose: noop }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** The props that `DeviceApproval` takes before anything is submitted. */
|
|
383
|
+
function deviceApproval(): DeviceApproval.Props {
|
|
384
|
+
return {
|
|
385
|
+
approving: false,
|
|
386
|
+
code: '',
|
|
387
|
+
denying: false,
|
|
388
|
+
fetching: false,
|
|
389
|
+
heading: 'h4',
|
|
390
|
+
invalid: false,
|
|
391
|
+
onApprove: noop,
|
|
392
|
+
onDeny: noop,
|
|
393
|
+
onSubmitCode: prevent,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** A sample request of `tempo-id login`, with its key, as `GET /v1/device/requests/:code` returns it. */
|
|
398
|
+
function deviceRequest(): DeviceApproval.Request {
|
|
399
|
+
return {
|
|
400
|
+
app: { name: 'Tempo CLI' },
|
|
401
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
402
|
+
device: 'ada-macbook',
|
|
403
|
+
ip: '203.0.113.7',
|
|
404
|
+
key_authorization: key(),
|
|
405
|
+
location: 'San Francisco, US',
|
|
406
|
+
user_code: 'BDWP-HQPK',
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The entry of `tempo-id login`, which requests a code with a new access key. */
|
|
411
|
+
const loginEntry = {
|
|
412
|
+
detail: 'POST /v1/device/authorize with key_authorization',
|
|
413
|
+
name: 'tempo-id login',
|
|
414
|
+
} as const satisfies Node
|
|
415
|
+
|
|
416
|
+
/** The exit of `tempo-id login`: the poll that receives the tokens and the signed key. */
|
|
417
|
+
const loginExit = {
|
|
418
|
+
detail:
|
|
419
|
+
'Tokens and key_authorization, stored in ~/.tempo/id/credentials.json. A replaced session’s refresh token goes to POST /oauth2/revoke.',
|
|
420
|
+
edge: 'POST /v1/device/token: authorization_pending, then tokens',
|
|
421
|
+
name: 'tempo-id login',
|
|
422
|
+
} as const satisfies Node
|
|
423
|
+
|
|
424
|
+
/** The props that `SignIn` takes before anything is submitted, with every sign-in method. */
|
|
425
|
+
function signIn(): SignIn.Props {
|
|
426
|
+
return {
|
|
427
|
+
heading: 'h4',
|
|
428
|
+
invalid: false,
|
|
429
|
+
onChangeEmail: noop,
|
|
430
|
+
onGoogle: noop,
|
|
431
|
+
onPasskey: noop,
|
|
432
|
+
onResend: noop,
|
|
433
|
+
onStartOver: noop,
|
|
434
|
+
onSubmitCode: prevent,
|
|
435
|
+
onSubmitEmail: prevent,
|
|
436
|
+
resend: 'idle',
|
|
437
|
+
sending: false,
|
|
438
|
+
verifying: false,
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** The entry of an app's redirect: the browser opens `/authorize`, which `/oauth2/authorize` signs. */
|
|
443
|
+
const redirectEntry = {
|
|
444
|
+
detail: 'GET /authorize?client_id&redirect_uri&code_challenge&state',
|
|
445
|
+
name: 'Web app',
|
|
446
|
+
} as const satisfies Node
|
|
447
|
+
|
|
448
|
+
/** The requests of a headless app's page load that select its own sign-in: no session, and the providers. */
|
|
449
|
+
const appSignInEdge = 'GET /v1/auth/session: no user, GET /v1/auth/providers'
|
|
450
|
+
|
|
451
|
+
/** The exit of an app's redirect: the app redeems the code at the token endpoint. */
|
|
452
|
+
const tokenExit = {
|
|
453
|
+
detail: 'Access, ID, and refresh tokens',
|
|
454
|
+
edge: 'POST /oauth2/token with code_verifier',
|
|
455
|
+
name: 'Web app',
|
|
456
|
+
} as const satisfies Node
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Every flow, grouped by how it starts. Update it in the same change as any flow it shows. The page
|
|
460
|
+
* calls it on each render, so sample dates and expiries read the clock then.
|
|
461
|
+
*/
|
|
462
|
+
function sections(): readonly Section[] {
|
|
463
|
+
return [
|
|
464
|
+
{
|
|
465
|
+
flows: [
|
|
466
|
+
{
|
|
467
|
+
description:
|
|
468
|
+
'A visitor opens Tempo ID without a session and signs in with an emailed code. An accepted code shows green for 500 ms before the sign-in continues.',
|
|
469
|
+
nodes: [
|
|
470
|
+
{ detail: 'GET /', name: 'Browser opens Tempo ID' },
|
|
471
|
+
{
|
|
472
|
+
edge: '302 /login',
|
|
473
|
+
name: 'Sign in',
|
|
474
|
+
screen: <SignIn {...signIn()} />,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
edge: 'POST /v1/auth/code',
|
|
478
|
+
name: 'Check your email',
|
|
479
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
edge: 'POST /v1/auth/code/verify: 200',
|
|
483
|
+
name: 'Check your email (accepted)',
|
|
484
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
edge: 'after 500 ms, redirect: null',
|
|
488
|
+
name: 'Your account',
|
|
489
|
+
screen: <Account {...account()} />,
|
|
490
|
+
},
|
|
491
|
+
],
|
|
492
|
+
title: 'Email code',
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
description:
|
|
496
|
+
'A passkey of the realm signs the user in with one prompt, from the button or from the email field’s autofill.',
|
|
497
|
+
nodes: [
|
|
498
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
499
|
+
{ name: 'Sign in', screen: <SignIn {...signIn()} /> },
|
|
500
|
+
{
|
|
501
|
+
detail: 'POST /v1/auth/passkey/options',
|
|
502
|
+
edge: 'Sign in with a passkey, or autofill',
|
|
503
|
+
name: 'Authenticator prompt',
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
edge: 'POST /v1/auth/passkey/verify',
|
|
507
|
+
name: 'Your account',
|
|
508
|
+
screen: <Account {...account()} />,
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
title: 'Passkey',
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
description:
|
|
515
|
+
'A wallet.tempo passkey without an email verifies one with a code. The passkey signs again with the code, which links it to the user.',
|
|
516
|
+
nodes: [
|
|
517
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
518
|
+
{ name: 'Sign in', screen: <SignIn {...signIn()} /> },
|
|
519
|
+
{
|
|
520
|
+
detail: 'POST /v1/auth/passkey/options',
|
|
521
|
+
edge: 'Sign in with a passkey',
|
|
522
|
+
name: 'Authenticator prompt',
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
edge: 'POST /v1/auth/passkey/verify: step email',
|
|
526
|
+
name: 'Sign in (link passkey)',
|
|
527
|
+
screen: <SignIn {...signIn()} linking />,
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
edge: 'POST /v1/auth/code',
|
|
531
|
+
name: 'Check your email (link passkey)',
|
|
532
|
+
screen: <SignIn {...signIn()} email={email} linking />,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
detail: 'POST /v1/auth/passkey/options with link',
|
|
536
|
+
edge: 'Code complete',
|
|
537
|
+
name: 'Authenticator prompt',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
edge: 'POST /v1/auth/code/verify with link and assertion: 200',
|
|
541
|
+
name: 'Check your email (link accepted)',
|
|
542
|
+
screen: <SignIn {...signIn()} email={email} linking verified />,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
edge: 'after 500 ms, redirect: null',
|
|
546
|
+
name: 'Your account',
|
|
547
|
+
screen: <Account {...account()} />,
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
title: 'Passkey without an email',
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
description:
|
|
554
|
+
'Start over on the email step drops the passkey’s link and a pending code request, so the passkey stays unlinked.',
|
|
555
|
+
nodes: [
|
|
556
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
557
|
+
{
|
|
558
|
+
edge: 'Sign in with a passkey, POST /v1/auth/passkey/verify: step email',
|
|
559
|
+
name: 'Sign in (link passkey)',
|
|
560
|
+
screen: <SignIn {...signIn()} linking />,
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
edge: 'Start over',
|
|
564
|
+
name: 'Sign in',
|
|
565
|
+
screen: <SignIn {...signIn()} />,
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
title: 'Passkey link, started over',
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
description:
|
|
572
|
+
'Google signs the user in, and its callback returns to the sign-in page, which sends a signed-in user on.',
|
|
573
|
+
nodes: [
|
|
574
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
575
|
+
{ name: 'Sign in', screen: <SignIn {...signIn()} /> },
|
|
576
|
+
{
|
|
577
|
+
edge: 'GET /v1/auth/oauth/google?return_to=/login',
|
|
578
|
+
name: 'Google',
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
edge: 'GET /callback/google, 302 /login, 302 /',
|
|
582
|
+
name: 'Your account',
|
|
583
|
+
screen: <Account {...account()} />,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
title: 'Google',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
description:
|
|
590
|
+
'A Google sign-in that fails returns to the sign-in page with `error=access_denied`, whatever the cause.',
|
|
591
|
+
nodes: [
|
|
592
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
593
|
+
{ name: 'Sign in', screen: <SignIn {...signIn()} /> },
|
|
594
|
+
{
|
|
595
|
+
edge: 'GET /v1/auth/oauth/google?return_to=/login',
|
|
596
|
+
name: 'Google',
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
edge: 'GET /callback/google, 302 /v1/auth/oauth/google/failed, 302 /login?error=access_denied',
|
|
600
|
+
name: 'Sign in (Google failed)',
|
|
601
|
+
screen: (
|
|
602
|
+
<SignIn
|
|
603
|
+
{...signIn()}
|
|
604
|
+
error="Sign-in with Google failed. Try again, or continue with email."
|
|
605
|
+
/>
|
|
606
|
+
),
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
title: 'Google, failed',
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
description:
|
|
613
|
+
'A deployment without Google, and realms without a passkey relying party at Tempo ID’s origin, leave only the email field.',
|
|
614
|
+
nodes: [
|
|
615
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
616
|
+
{
|
|
617
|
+
edge: '200, google: false, passkeys: false',
|
|
618
|
+
name: 'Sign in (email only)',
|
|
619
|
+
screen: (
|
|
620
|
+
<SignIn
|
|
621
|
+
{...signIn()}
|
|
622
|
+
onGoogle={undefined}
|
|
623
|
+
onPasskey={undefined}
|
|
624
|
+
/>
|
|
625
|
+
),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
title: 'Email only',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
description:
|
|
632
|
+
'Each sign-in request holds its controls, shows a spinner, and announces its progress until it settles. One sign-in runs at a time.',
|
|
633
|
+
nodes: [
|
|
634
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
635
|
+
{
|
|
636
|
+
edge: 'Continue, POST /v1/auth/code',
|
|
637
|
+
name: 'Sign in (sending)',
|
|
638
|
+
screen: <SignIn {...signIn()} sending />,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
edge: 'Or: Sign in with a passkey',
|
|
642
|
+
name: 'Sign in (passkey pending)',
|
|
643
|
+
screen: <SignIn {...signIn()} passkeyPending />,
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
edge: 'Code complete, POST /v1/auth/code/verify',
|
|
647
|
+
name: 'Check your email (verifying)',
|
|
648
|
+
screen: <SignIn {...signIn()} email={email} verifying />,
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
edge: 'Or: Resend code, POST /v1/auth/code',
|
|
652
|
+
name: 'Check your email (resending)',
|
|
653
|
+
screen: <SignIn {...signIn()} email={email} resend="pending" />,
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
title: 'Sign-in in progress',
|
|
657
|
+
},
|
|
658
|
+
],
|
|
659
|
+
title: 'Sign-in',
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
flows: [
|
|
663
|
+
{
|
|
664
|
+
description:
|
|
665
|
+
'The account page lists the user’s Tempo Accounts in the `default` realm, oldest first, each with its balance and a deposit link. A lone wallet shows its balance large, without a list.',
|
|
666
|
+
nodes: [
|
|
667
|
+
{ detail: 'GET /', name: 'Browser opens Tempo ID' },
|
|
668
|
+
{
|
|
669
|
+
edge: '200, one wallet',
|
|
670
|
+
name: 'Your account (one wallet)',
|
|
671
|
+
screen: <Account {...account()} />,
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
edge: 'Or: several wallets',
|
|
675
|
+
name: 'Your account (several wallets)',
|
|
676
|
+
screen: <Account {...account()} accounts={accounts} />,
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
edge: 'Or: a balance read fails',
|
|
680
|
+
name: 'Your account (balance unavailable)',
|
|
681
|
+
screen: (
|
|
682
|
+
<Account
|
|
683
|
+
{...account()}
|
|
684
|
+
accounts={[accounts[0], { ...accounts[1], balance: null }]}
|
|
685
|
+
/>
|
|
686
|
+
),
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
edge: 'Or: no wallets',
|
|
690
|
+
name: 'Your account (no wallets)',
|
|
691
|
+
screen: <Account {...account()} accounts={[]} />,
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
edge: 'Or: the accounts cannot be read',
|
|
695
|
+
name: 'Your account (unavailable)',
|
|
696
|
+
screen: (
|
|
697
|
+
<Account
|
|
698
|
+
{...account()}
|
|
699
|
+
accountError="Your wallets failed to load. Reload the page to try again."
|
|
700
|
+
accounts={null}
|
|
701
|
+
/>
|
|
702
|
+
),
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
title: 'Account page',
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
description:
|
|
709
|
+
'The account page and the deposit page send a visitor without a session to sign-in, which then opens the account page. The deposit page leaves the address out of the sign-in query.',
|
|
710
|
+
nodes: [
|
|
711
|
+
{ detail: 'GET / or GET /deposit?address', name: 'Browser' },
|
|
712
|
+
{
|
|
713
|
+
edge: '302 /login, no session',
|
|
714
|
+
name: 'Sign in',
|
|
715
|
+
screen: <SignIn {...signIn()} />,
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
edge: 'POST /v1/auth/code',
|
|
719
|
+
name: 'Check your email',
|
|
720
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
edge: 'POST /v1/auth/code/verify: 200',
|
|
724
|
+
name: 'Check your email (accepted)',
|
|
725
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
edge: 'after 500 ms, redirect: null, GET /',
|
|
729
|
+
name: 'Your account',
|
|
730
|
+
screen: <Account {...account()} />,
|
|
731
|
+
},
|
|
732
|
+
],
|
|
733
|
+
title: 'Account page, signed out',
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
description:
|
|
737
|
+
'Deposit lists the networks and cash methods for one of the user’s wallets. Only Tempo is available, and it shows the wallet’s address as a QR code and as text. Back on Receive funds returns to the deposit methods.',
|
|
738
|
+
nodes: [
|
|
739
|
+
{ detail: 'GET /', name: 'Browser opens Tempo ID' },
|
|
740
|
+
{
|
|
741
|
+
edge: '200, one wallet',
|
|
742
|
+
name: 'Your account',
|
|
743
|
+
screen: <Account {...account()} />,
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
edge: 'Deposit, GET /deposit?address',
|
|
747
|
+
name: 'Deposit',
|
|
748
|
+
screen: <Deposit address={accounts[0].address} heading="h4" />,
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
edge: 'Tempo, GET /deposit?address&method=tempo',
|
|
752
|
+
name: 'Receive funds',
|
|
753
|
+
screen: (
|
|
754
|
+
<Deposit
|
|
755
|
+
address={accounts[0].address}
|
|
756
|
+
heading="h4"
|
|
757
|
+
method="tempo"
|
|
758
|
+
/>
|
|
759
|
+
),
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
edge: 'Back, GET /deposit?address',
|
|
763
|
+
name: 'Deposit',
|
|
764
|
+
screen: <Deposit address={accounts[0].address} heading="h4" />,
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
edge: 'Back, GET /',
|
|
768
|
+
name: 'Your account',
|
|
769
|
+
screen: <Account {...account()} />,
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
title: 'Deposit',
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
description: 'Done on Receive funds opens the account page at once.',
|
|
776
|
+
nodes: [
|
|
777
|
+
{ detail: 'GET /deposit?address&method=tempo', name: 'Browser' },
|
|
778
|
+
{
|
|
779
|
+
edge: '200 deposit page',
|
|
780
|
+
name: 'Receive funds',
|
|
781
|
+
screen: (
|
|
782
|
+
<Deposit
|
|
783
|
+
address={accounts[0].address}
|
|
784
|
+
heading="h4"
|
|
785
|
+
method="tempo"
|
|
786
|
+
/>
|
|
787
|
+
),
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
edge: 'Done, GET /',
|
|
791
|
+
name: 'Your account',
|
|
792
|
+
screen: <Account {...account()} />,
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
title: 'Deposit, Done',
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
description:
|
|
799
|
+
'An address that is not one of the user’s wallets in the `default` realm shows no address. A spent rate limit or a failed account read shows its own reason in the same place.',
|
|
800
|
+
nodes: [
|
|
801
|
+
{
|
|
802
|
+
detail: 'GET /deposit?address (not the user’s)',
|
|
803
|
+
name: 'Browser',
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
edge: '200, address: null',
|
|
807
|
+
name: 'Deposit (wallet not found)',
|
|
808
|
+
screen: (
|
|
809
|
+
<Deposit
|
|
810
|
+
error="This wallet was not found. Go back to your account, then choose Deposit."
|
|
811
|
+
heading="h4"
|
|
812
|
+
/>
|
|
813
|
+
),
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
edge: 'Or: the account rate limit is spent',
|
|
817
|
+
name: 'Deposit (rate limited)',
|
|
818
|
+
screen: (
|
|
819
|
+
<Deposit
|
|
820
|
+
error="Too many requests. Retry after 60 seconds."
|
|
821
|
+
heading="h4"
|
|
822
|
+
/>
|
|
823
|
+
),
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
edge: 'Or: the accounts cannot be read',
|
|
827
|
+
name: 'Deposit (unavailable)',
|
|
828
|
+
screen: (
|
|
829
|
+
<Deposit
|
|
830
|
+
error="Your wallets failed to load. Reload the page to try again."
|
|
831
|
+
heading="h4"
|
|
832
|
+
/>
|
|
833
|
+
),
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
title: 'Wallet not found',
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
title: 'Account and deposit',
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
flows: [
|
|
843
|
+
{
|
|
844
|
+
description:
|
|
845
|
+
'A web app redirects a signed-out user. Better Auth signs the request, and the sign-in continues it.',
|
|
846
|
+
nodes: [
|
|
847
|
+
redirectEntry,
|
|
848
|
+
{
|
|
849
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
850
|
+
name: 'Sign in',
|
|
851
|
+
screen: <SignIn {...signIn()} />,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
edge: 'POST /v1/auth/code',
|
|
855
|
+
name: 'Check your email',
|
|
856
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200',
|
|
860
|
+
name: 'Check your email (accepted)',
|
|
861
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
detail: 'GET redirect_uri?code&state',
|
|
865
|
+
edge: 'after 500 ms, redirect',
|
|
866
|
+
name: 'Web app callback',
|
|
867
|
+
},
|
|
868
|
+
tokenExit,
|
|
869
|
+
],
|
|
870
|
+
title: 'Redirect, signed out',
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
description:
|
|
874
|
+
'A signed-out user’s request with a key shows the app and the key on the sign-in page. The first sign-in of the request grants the key, so the browser returns to the app without the approval page.',
|
|
875
|
+
nodes: [
|
|
876
|
+
keyedEntry,
|
|
877
|
+
{
|
|
878
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig, GET /login: request',
|
|
879
|
+
name: 'Sign in (key)',
|
|
880
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
edge: 'POST /v1/auth/code',
|
|
884
|
+
name: 'Check your email',
|
|
885
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, grants the key',
|
|
889
|
+
name: 'Check your email (accepted)',
|
|
890
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
detail: 'GET redirect_uri?code&state',
|
|
894
|
+
edge: 'after 500 ms, redirect',
|
|
895
|
+
name: 'Web app callback',
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
...tokenExit,
|
|
899
|
+
detail: 'Tokens, account, and key_authorization',
|
|
900
|
+
},
|
|
901
|
+
],
|
|
902
|
+
title: 'Sign-in that grants a key',
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
description:
|
|
906
|
+
'On the sign-in page, the passkey’s challenge is the digest of the request’s key. One tap signs the user in and signs the key, for an account that this passkey owns, or that Privy or secp256k1 owns.',
|
|
907
|
+
nodes: [
|
|
908
|
+
keyedEntry,
|
|
909
|
+
{
|
|
910
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
911
|
+
name: 'Sign in (key)',
|
|
912
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
detail:
|
|
916
|
+
'POST /v1/auth/passkey/options with oauth_query: the key’s digest',
|
|
917
|
+
edge: 'Sign in with a passkey',
|
|
918
|
+
name: 'Authenticator prompt',
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
detail: 'GET redirect_uri?code&state',
|
|
922
|
+
edge: 'POST /v1/auth/passkey/verify with oauth_query, grants the key',
|
|
923
|
+
name: 'Web app callback',
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
...tokenExit,
|
|
927
|
+
detail: 'Tokens, account, and key_authorization',
|
|
928
|
+
},
|
|
929
|
+
],
|
|
930
|
+
title: 'Sign-in that grants a key, passkey one-tap',
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
description:
|
|
934
|
+
'The sign-in page starts Google with `sec-fetch-site: same-origin`, so the start sets the `__Host-tempo-id-grant` cookie for the request. The callback grants the key only when the cookie matches the request, and clears it.',
|
|
935
|
+
nodes: [
|
|
936
|
+
keyedEntry,
|
|
937
|
+
{
|
|
938
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
939
|
+
name: 'Sign in (key)',
|
|
940
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
edge: 'GET /v1/auth/oauth/google?oauth_query&return_to, same-origin: sets __Host-tempo-id-grant',
|
|
944
|
+
name: 'Google',
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
detail: 'GET redirect_uri?code&state',
|
|
948
|
+
edge: 'GET /callback/google, the cookie matches: grants the key, 302',
|
|
949
|
+
name: 'Web app callback',
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
...tokenExit,
|
|
953
|
+
detail: 'Tokens, account, and key_authorization',
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
title: 'Sign-in that grants a key, Google',
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
description:
|
|
960
|
+
'Any site can send a browser to the Google start. Without `sec-fetch-site: same-origin`, the start clears the grant cookie. The callback then keeps the redirect to the approval page, which shows the key.',
|
|
961
|
+
nodes: [
|
|
962
|
+
{
|
|
963
|
+
detail: 'GET /v1/auth/oauth/google?oauth_query&return_to',
|
|
964
|
+
name: 'Another site',
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
edge: 'cross-site: clears __Host-tempo-id-grant, 302',
|
|
968
|
+
name: 'Google',
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
edge: 'GET /callback/google, no grant cookie, 302 /authorize?…&sig',
|
|
972
|
+
name: 'Authorize this app',
|
|
973
|
+
screen: <Approval {...approval()} />,
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
detail: 'GET redirect_uri?code&state',
|
|
977
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
978
|
+
name: 'Web app callback',
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
...tokenExit,
|
|
982
|
+
detail: 'Tokens, account, and key_authorization',
|
|
983
|
+
},
|
|
984
|
+
],
|
|
985
|
+
title: 'Google start from another site',
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
description:
|
|
989
|
+
'The account step does not run when the account reads are spent. The sign-in page shows no account, so a user with several accounts gets no grant, and the approval page opens. That page holds Approve until a reload after a minute.',
|
|
990
|
+
nodes: [
|
|
991
|
+
keyedEntry,
|
|
992
|
+
{
|
|
993
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
994
|
+
name: 'Sign in (key)',
|
|
995
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
edge: 'POST /v1/auth/code',
|
|
999
|
+
name: 'Check your email',
|
|
1000
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: ACCOUNT_LIMIT spent, no account step, several accounts: no grant',
|
|
1004
|
+
name: 'Check your email (accepted)',
|
|
1005
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
edge: 'after 500 ms, redirect /authorize?…&sig',
|
|
1009
|
+
name: 'Authorize this app (unavailable)',
|
|
1010
|
+
screen: (
|
|
1011
|
+
<Approval
|
|
1012
|
+
{...approval()}
|
|
1013
|
+
keyAuthorization={{ ...key(), refusal: 'unavailable' }}
|
|
1014
|
+
/>
|
|
1015
|
+
),
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
edge: 'Reload after a minute',
|
|
1019
|
+
name: 'Authorize this app',
|
|
1020
|
+
screen: <Approval {...approval()} />,
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
detail: 'GET redirect_uri?code&state',
|
|
1024
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1025
|
+
name: 'Web app callback',
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
...tokenExit,
|
|
1029
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1030
|
+
},
|
|
1031
|
+
],
|
|
1032
|
+
title: 'Several accounts, none chosen',
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
description:
|
|
1036
|
+
'A held grant covers only the request that the sign-in spent. A request with another `scope`, or with a `challenge`, takes no held grant on the account step, so the approval page opens.',
|
|
1037
|
+
nodes: [
|
|
1038
|
+
{
|
|
1039
|
+
detail: 'GET /choose-account?…&sig, another scope or a challenge',
|
|
1040
|
+
name: 'Browser',
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
edge: '200 account step',
|
|
1044
|
+
name: 'Choose a wallet',
|
|
1045
|
+
screen: <ChooseAccount {...chooseAccount()} />,
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
edge: 'Choose, POST /v1/authorize/account: another scope, no held grant, redirect /authorize?…&sig',
|
|
1049
|
+
name: 'Authorize this app (another scope)',
|
|
1050
|
+
screen: <Approval {...approval()} />,
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
edge: 'Or: a challenge, which takes no grant, redirect /authorize?…&sig',
|
|
1054
|
+
name: 'Authorize this app (challenge)',
|
|
1055
|
+
screen: (
|
|
1056
|
+
<Approval
|
|
1057
|
+
{...approval()}
|
|
1058
|
+
keyAuthorization={{
|
|
1059
|
+
...key(),
|
|
1060
|
+
challenge: 'Sign in to api.example.com\nNonce: 8f2a91',
|
|
1061
|
+
}}
|
|
1062
|
+
/>
|
|
1063
|
+
),
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
detail: 'GET redirect_uri?code&state',
|
|
1067
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1068
|
+
name: 'Web app callback',
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
...tokenExit,
|
|
1072
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
title: 'Account step for another request',
|
|
1076
|
+
},
|
|
1077
|
+
{
|
|
1078
|
+
description:
|
|
1079
|
+
'A user with more than one account in the app’s realms chooses one. The sign-in holds its grant for the session for 10 minutes, and the choice takes it.',
|
|
1080
|
+
nodes: [
|
|
1081
|
+
keyedEntry,
|
|
1082
|
+
{
|
|
1083
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
1084
|
+
name: 'Sign in (key)',
|
|
1085
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
edge: 'POST /v1/auth/code',
|
|
1089
|
+
name: 'Check your email',
|
|
1090
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, holds the grant',
|
|
1094
|
+
name: 'Check your email (accepted)',
|
|
1095
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
edge: 'after 500 ms, redirect /choose-account?…&sig',
|
|
1099
|
+
name: 'Choose a wallet',
|
|
1100
|
+
screen: <ChooseAccount {...chooseAccount()} />,
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
edge: 'Choose, POST /v1/authorize/account',
|
|
1104
|
+
name: 'Choose a wallet (selecting)',
|
|
1105
|
+
screen: (
|
|
1106
|
+
<ChooseAccount
|
|
1107
|
+
{...chooseAccount()}
|
|
1108
|
+
selecting={accounts[0].address}
|
|
1109
|
+
/>
|
|
1110
|
+
),
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
detail: 'GET redirect_uri?code&state',
|
|
1114
|
+
edge: 'takes the held grant, redirect',
|
|
1115
|
+
name: 'Web app callback',
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
...tokenExit,
|
|
1119
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
title: 'Sign-in that grants a key, account step',
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
description:
|
|
1126
|
+
'A sign-in that cannot grant the key keeps the redirect to the approval page, which shows the request again. Another passkey may own the account, or the request may be a replay. Approval may refuse the key, or the rate limit may be spent, which also refuses Approve until it resets. An upstream service may also fail.',
|
|
1127
|
+
nodes: [
|
|
1128
|
+
keyedEntry,
|
|
1129
|
+
{
|
|
1130
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
1131
|
+
name: 'Sign in (key)',
|
|
1132
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
edge: 'Sign-in with oauth_query: the grant fails, redirect /authorize?…&sig',
|
|
1136
|
+
name: 'Authorize this app',
|
|
1137
|
+
screen: <Approval {...approval()} />,
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1141
|
+
name: 'Authorize this app (approving)',
|
|
1142
|
+
screen: <Approval {...approval()} approving />,
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
detail: 'GET redirect_uri?code&state',
|
|
1146
|
+
edge: 'redirect, or the passkey step first',
|
|
1147
|
+
name: 'Web app callback',
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
...tokenExit,
|
|
1151
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1152
|
+
},
|
|
1153
|
+
],
|
|
1154
|
+
title: 'Sign-in that keeps the approval page',
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
description:
|
|
1158
|
+
'The account step’s signed query expires 10 minutes after the sign-in, just before its held grant. A later choice is refused with `authorization_expired`, so the user starts again in the app.',
|
|
1159
|
+
nodes: [
|
|
1160
|
+
keyedEntry,
|
|
1161
|
+
{
|
|
1162
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig',
|
|
1163
|
+
name: 'Sign in (key)',
|
|
1164
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
edge: 'POST /v1/auth/code',
|
|
1168
|
+
name: 'Check your email',
|
|
1169
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, holds the grant',
|
|
1173
|
+
name: 'Check your email (accepted)',
|
|
1174
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
edge: 'after 500 ms, redirect /choose-account?…&sig',
|
|
1178
|
+
name: 'Choose a wallet',
|
|
1179
|
+
screen: <ChooseAccount {...chooseAccount()} />,
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
edge: 'After 10 minutes, Choose, POST /v1/authorize/account: authorization_expired',
|
|
1183
|
+
name: 'Choose a wallet (request expired)',
|
|
1184
|
+
screen: (
|
|
1185
|
+
<ChooseAccount
|
|
1186
|
+
{...chooseAccount()}
|
|
1187
|
+
error="Expired authorization request: the request from the app is no longer valid. Return to the app, and start again."
|
|
1188
|
+
/>
|
|
1189
|
+
),
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
detail: 'GET /authorize?…&key_authorization, a new request',
|
|
1193
|
+
edge: 'Return to the app, and start again',
|
|
1194
|
+
name: 'Web app',
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
title: 'Account step after the request expires',
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
description:
|
|
1201
|
+
'A request with a `challenge` signs a message that only the approval page shows. So the sign-in page shows no key, and the sign-in grants nothing.',
|
|
1202
|
+
nodes: [
|
|
1203
|
+
{
|
|
1204
|
+
...keyedEntry,
|
|
1205
|
+
detail: 'GET /authorize?…&key_authorization&challenge',
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
edge: '302 /oauth2/authorize, 302 /login?…&sig, GET /login: request: null',
|
|
1209
|
+
name: 'Sign in',
|
|
1210
|
+
screen: <SignIn {...signIn()} />,
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
edge: 'POST /v1/auth/code',
|
|
1214
|
+
name: 'Check your email',
|
|
1215
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200',
|
|
1219
|
+
name: 'Check your email (accepted)',
|
|
1220
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
edge: 'after 500 ms, redirect /authorize?…&sig',
|
|
1224
|
+
name: 'Authorize this app (challenge)',
|
|
1225
|
+
screen: (
|
|
1226
|
+
<Approval
|
|
1227
|
+
{...approval()}
|
|
1228
|
+
keyAuthorization={{
|
|
1229
|
+
...key(),
|
|
1230
|
+
challenge: 'Sign in to api.example.com\nNonce: 8f2a91',
|
|
1231
|
+
}}
|
|
1232
|
+
/>
|
|
1233
|
+
),
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
detail: 'GET redirect_uri?code&state',
|
|
1237
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1238
|
+
name: 'Web app callback',
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
...tokenExit,
|
|
1242
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1243
|
+
},
|
|
1244
|
+
],
|
|
1245
|
+
title: 'Sign-in with a challenge',
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
description:
|
|
1249
|
+
'A signed-in user returns to an app that skips consent at once. Tempo ID shows no screen.',
|
|
1250
|
+
nodes: [
|
|
1251
|
+
redirectEntry,
|
|
1252
|
+
{
|
|
1253
|
+
detail: 'GET redirect_uri?code&state',
|
|
1254
|
+
edge: '302 /oauth2/authorize, 302, no screen',
|
|
1255
|
+
name: 'Web app callback',
|
|
1256
|
+
},
|
|
1257
|
+
tokenExit,
|
|
1258
|
+
],
|
|
1259
|
+
title: 'Redirect, signed in',
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
description:
|
|
1263
|
+
'A request with `prompt=login` asks a signed-in user to sign in again. The sign-in page shows the request, and its key, despite the session.',
|
|
1264
|
+
nodes: [
|
|
1265
|
+
{
|
|
1266
|
+
...keyedEntry,
|
|
1267
|
+
detail: 'GET /authorize?…&key_authorization&prompt=login',
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
edge: '302 /oauth2/authorize, 302 /login?…&prompt=login consent&sig',
|
|
1271
|
+
name: 'Sign in (key)',
|
|
1272
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
edge: 'POST /v1/auth/code',
|
|
1276
|
+
name: 'Check your email',
|
|
1277
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, grants the key',
|
|
1281
|
+
name: 'Check your email (accepted)',
|
|
1282
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
detail: 'GET redirect_uri?code&state',
|
|
1286
|
+
edge: 'after 500 ms, redirect',
|
|
1287
|
+
name: 'Web app callback',
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
...tokenExit,
|
|
1291
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1292
|
+
},
|
|
1293
|
+
],
|
|
1294
|
+
title: 'Redirect with prompt=login',
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
description:
|
|
1298
|
+
'A signed-in user approves an app that does not skip consent, once for the scopes it requests. The request carries no key, so the page shows none.',
|
|
1299
|
+
nodes: [
|
|
1300
|
+
redirectEntry,
|
|
1301
|
+
{
|
|
1302
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig',
|
|
1303
|
+
name: 'Authorize this app (no key)',
|
|
1304
|
+
screen: <Approval {...approval()} keyAuthorization={undefined} />,
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
detail: 'GET redirect_uri?code&state',
|
|
1308
|
+
edge: 'POST /v1/authorize/consent, accept',
|
|
1309
|
+
name: 'Web app callback',
|
|
1310
|
+
},
|
|
1311
|
+
tokenExit,
|
|
1312
|
+
],
|
|
1313
|
+
title: 'Approval without a key',
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
description:
|
|
1317
|
+
'A signed-in user’s request with a key opens the approval page. A Privy or secp256k1 owner signs the key on the server when the user approves.',
|
|
1318
|
+
nodes: [
|
|
1319
|
+
{
|
|
1320
|
+
...redirectEntry,
|
|
1321
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig',
|
|
1325
|
+
name: 'Authorize this app',
|
|
1326
|
+
screen: <Approval {...approval()} />,
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1330
|
+
name: 'Authorize this app (approving)',
|
|
1331
|
+
screen: <Approval {...approval()} approving />,
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
detail: 'GET redirect_uri?code&state',
|
|
1335
|
+
edge: 'redirect',
|
|
1336
|
+
name: 'Web app callback',
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
...tokenExit,
|
|
1340
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1341
|
+
},
|
|
1342
|
+
],
|
|
1343
|
+
title: 'Approval of a key',
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
description:
|
|
1347
|
+
'A passkey owns the account, so the approval returns the passkey step. The passkey signs the key’s digest, and the page approves again.',
|
|
1348
|
+
nodes: [
|
|
1349
|
+
{
|
|
1350
|
+
...redirectEntry,
|
|
1351
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig',
|
|
1355
|
+
name: 'Authorize this app',
|
|
1356
|
+
screen: <Approval {...approval()} />,
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
1360
|
+
name: 'Authorize this app (approving)',
|
|
1361
|
+
screen: <Approval {...approval()} approving />,
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
detail: 'Signs the key authorization, over the approving page',
|
|
1365
|
+
edge: 'step passkey',
|
|
1366
|
+
name: 'Authenticator prompt',
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
detail: 'GET redirect_uri?code&state',
|
|
1370
|
+
edge: 'POST /v1/authorize/consent with passkey.assertion',
|
|
1371
|
+
name: 'Web app callback',
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
...tokenExit,
|
|
1375
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
title: 'Approval of a key for a passkey account',
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
description:
|
|
1382
|
+
'An app with the `tempo-id:sign` scope sends a challenge, and the approval page shows the message that the key signs.',
|
|
1383
|
+
nodes: [
|
|
1384
|
+
{
|
|
1385
|
+
detail: 'GET /authorize?…&key_authorization&challenge',
|
|
1386
|
+
name: 'Native app',
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig',
|
|
1390
|
+
name: 'Authorize this app (challenge)',
|
|
1391
|
+
screen: (
|
|
1392
|
+
<Approval
|
|
1393
|
+
{...approval()}
|
|
1394
|
+
app="Tempo Mobile"
|
|
1395
|
+
destination="tempo"
|
|
1396
|
+
keyAuthorization={{
|
|
1397
|
+
...key(),
|
|
1398
|
+
challenge: 'Sign in to api.example.com\nNonce: 8f2a91',
|
|
1399
|
+
}}
|
|
1400
|
+
/>
|
|
1401
|
+
),
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
detail: 'tempo:/callback?code&state',
|
|
1405
|
+
edge: 'POST /v1/authorize/consent, accept',
|
|
1406
|
+
name: 'Native app callback',
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
...tokenExit,
|
|
1410
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1411
|
+
name: 'Native app',
|
|
1412
|
+
},
|
|
1413
|
+
],
|
|
1414
|
+
title: 'Approval with a challenge',
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
description:
|
|
1418
|
+
'A user with more than one Tempo Account in the app’s realms chooses one. Each row shows the account’s balance on the chain of the request’s key, or Unavailable when the read fails. The choice continues the authorization to the approval page.',
|
|
1419
|
+
nodes: [
|
|
1420
|
+
{
|
|
1421
|
+
...redirectEntry,
|
|
1422
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
edge: '302 /oauth2/authorize, 302 /choose-account?…&sig',
|
|
1426
|
+
name: 'Choose a wallet',
|
|
1427
|
+
screen: <ChooseAccount {...chooseAccount()} />,
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
edge: 'Choose, POST /v1/authorize/account',
|
|
1431
|
+
name: 'Choose a wallet (selecting)',
|
|
1432
|
+
screen: (
|
|
1433
|
+
<ChooseAccount
|
|
1434
|
+
{...chooseAccount()}
|
|
1435
|
+
selecting={accounts[0].address}
|
|
1436
|
+
/>
|
|
1437
|
+
),
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
edge: 'redirect /authorize?…&sig',
|
|
1441
|
+
name: 'Authorize this app',
|
|
1442
|
+
screen: <Approval {...approval()} />,
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
detail: 'GET redirect_uri?code&state',
|
|
1446
|
+
edge: 'POST /v1/authorize/consent, accept',
|
|
1447
|
+
name: 'Web app callback',
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
...tokenExit,
|
|
1451
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1452
|
+
},
|
|
1453
|
+
],
|
|
1454
|
+
title: 'Account step',
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
description:
|
|
1458
|
+
'An app in several realms, such as web-shared, lists accounts from each. Each row then names its realm, apart from the `default` realm.',
|
|
1459
|
+
nodes: [
|
|
1460
|
+
{
|
|
1461
|
+
...redirectEntry,
|
|
1462
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1463
|
+
name: 'Web app in two realms',
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
edge: '302 /oauth2/authorize, 302 /choose-account?…&sig',
|
|
1467
|
+
name: 'Choose a wallet (two realms)',
|
|
1468
|
+
screen: (
|
|
1469
|
+
<ChooseAccount
|
|
1470
|
+
{...chooseAccount()}
|
|
1471
|
+
accounts={[
|
|
1472
|
+
{ ...accounts[0], realm: 'Tempo Cash' },
|
|
1473
|
+
accounts[1],
|
|
1474
|
+
]}
|
|
1475
|
+
/>
|
|
1476
|
+
),
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
edge: 'Choose, POST /v1/authorize/account',
|
|
1480
|
+
name: 'Choose a wallet (two realms, selecting)',
|
|
1481
|
+
screen: (
|
|
1482
|
+
<ChooseAccount
|
|
1483
|
+
{...chooseAccount()}
|
|
1484
|
+
accounts={[
|
|
1485
|
+
{ ...accounts[0], realm: 'Tempo Cash' },
|
|
1486
|
+
accounts[1],
|
|
1487
|
+
]}
|
|
1488
|
+
selecting={accounts[0].address}
|
|
1489
|
+
/>
|
|
1490
|
+
),
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
edge: 'redirect /authorize?…&sig',
|
|
1494
|
+
name: 'Authorize this app',
|
|
1495
|
+
screen: <Approval {...approval()} />,
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
detail: 'GET redirect_uri?code&state',
|
|
1499
|
+
edge: 'POST /v1/authorize/consent, accept',
|
|
1500
|
+
name: 'Web app callback',
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
...tokenExit,
|
|
1504
|
+
detail: 'Tokens, account, and key_authorization',
|
|
1505
|
+
},
|
|
1506
|
+
],
|
|
1507
|
+
title: 'Account step across realms',
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
description:
|
|
1511
|
+
'Denying returns the browser to the app with `error=access_denied`. The app receives no code.',
|
|
1512
|
+
nodes: [
|
|
1513
|
+
{
|
|
1514
|
+
...redirectEntry,
|
|
1515
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig',
|
|
1519
|
+
name: 'Authorize this app',
|
|
1520
|
+
screen: <Approval {...approval()} />,
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
edge: 'Deny',
|
|
1524
|
+
name: 'Authorize this app (denying)',
|
|
1525
|
+
screen: <Approval {...approval()} denying />,
|
|
1526
|
+
},
|
|
1527
|
+
{
|
|
1528
|
+
detail: 'GET redirect_uri?error=access_denied&state',
|
|
1529
|
+
edge: 'POST /v1/authorize/consent, accept: false',
|
|
1530
|
+
name: 'Web app callback',
|
|
1531
|
+
},
|
|
1532
|
+
],
|
|
1533
|
+
title: 'Denied approval',
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
description:
|
|
1537
|
+
'Approval cannot sign some keys, so the page holds Approve and states why. Deny still returns to the app.',
|
|
1538
|
+
nodes: [
|
|
1539
|
+
{
|
|
1540
|
+
...redirectEntry,
|
|
1541
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
edge: '302 /oauth2/authorize, 302 /authorize?…&sig: no account, and no Privy app to create one',
|
|
1545
|
+
name: 'Authorize this app (no account)',
|
|
1546
|
+
screen: (
|
|
1547
|
+
<Approval
|
|
1548
|
+
{...approval()}
|
|
1549
|
+
keyAuthorization={{ ...key(), refusal: 'no_account' }}
|
|
1550
|
+
/>
|
|
1551
|
+
),
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
edge: 'Or: a passkey whose relying party is not Tempo ID’s, or POST /v1/authorize/consent: webauthn_required',
|
|
1555
|
+
name: 'Authorize this app (passkey)',
|
|
1556
|
+
screen: (
|
|
1557
|
+
<Approval
|
|
1558
|
+
{...approval()}
|
|
1559
|
+
keyAuthorization={{ ...key(), refusal: 'passkey' }}
|
|
1560
|
+
/>
|
|
1561
|
+
),
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
edge: 'Or: the accounts cannot be read',
|
|
1565
|
+
name: 'Authorize this app (unavailable)',
|
|
1566
|
+
screen: (
|
|
1567
|
+
<Approval
|
|
1568
|
+
{...approval()}
|
|
1569
|
+
keyAuthorization={{ ...key(), refusal: 'unavailable' }}
|
|
1570
|
+
/>
|
|
1571
|
+
),
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
detail: 'GET redirect_uri?error=access_denied&state',
|
|
1575
|
+
edge: 'Deny, POST /v1/authorize/consent, accept: false',
|
|
1576
|
+
name: 'Web app callback',
|
|
1577
|
+
},
|
|
1578
|
+
],
|
|
1579
|
+
title: 'Key that approval cannot sign',
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
description:
|
|
1583
|
+
'A key that breaks a rule returns to the app with the refusal, as `/oauth2/authorize` returns one before sign-in. The `error` names the rule, such as `invalid_authorization_details` for an expired key, or `unauthorized_client` for a challenge without the `tempo-id:sign` scope.',
|
|
1584
|
+
nodes: [
|
|
1585
|
+
{
|
|
1586
|
+
...redirectEntry,
|
|
1587
|
+
detail: 'GET /authorize?…&key_authorization',
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
detail: 'GET redirect_uri?error&error_description',
|
|
1591
|
+
edge: '302 /oauth2/authorize, key refused',
|
|
1592
|
+
name: 'Web app callback',
|
|
1593
|
+
},
|
|
1594
|
+
],
|
|
1595
|
+
title: 'Refused key',
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
description:
|
|
1599
|
+
'A changed or expired query, or a request without a known app, shows an invalid request, on the approval page and on the account step.',
|
|
1600
|
+
nodes: [
|
|
1601
|
+
{ detail: 'GET /authorize?…&sig (changed)', name: 'Browser' },
|
|
1602
|
+
{
|
|
1603
|
+
edge: '200, request: null',
|
|
1604
|
+
name: 'This sign-in request is invalid',
|
|
1605
|
+
screen: <InvalidRequest heading="h4" />,
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
edge: 'Or: GET /choose-account?…&sig (changed), 200, request: null',
|
|
1609
|
+
name: 'This sign-in request is invalid (account step)',
|
|
1610
|
+
screen: <InvalidRequest heading="h4" />,
|
|
1611
|
+
},
|
|
1612
|
+
],
|
|
1613
|
+
title: 'Invalid request',
|
|
1614
|
+
},
|
|
1615
|
+
{
|
|
1616
|
+
description:
|
|
1617
|
+
'A backend runs the redirect flow and redeems the code with its client secret, so tokens never reach the browser. By default the request carries a key. The seeded backend uses only the `default` realm. Without a Privy app or secp256k1 owners there, as in the test harness, no account can sign the key. So the approval page holds Approve, and Deny signs the user in again without a key.',
|
|
1618
|
+
nodes: [
|
|
1619
|
+
{
|
|
1620
|
+
detail: 'GET /auth/start on the backend, with a key',
|
|
1621
|
+
name: 'Backend playground',
|
|
1622
|
+
},
|
|
1623
|
+
{
|
|
1624
|
+
edge: '302 /authorize with state and code_challenge, 302 /oauth2/authorize, 302 /login?…&sig',
|
|
1625
|
+
name: 'Sign in (key)',
|
|
1626
|
+
screen: (
|
|
1627
|
+
<SignIn
|
|
1628
|
+
{...signIn()}
|
|
1629
|
+
request={{ ...signInRequest(), app: 'Backend playground' }}
|
|
1630
|
+
/>
|
|
1631
|
+
),
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
edge: 'POST /v1/auth/code',
|
|
1635
|
+
name: 'Check your email',
|
|
1636
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, no account to sign',
|
|
1640
|
+
name: 'Check your email (accepted)',
|
|
1641
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
edge: 'after 500 ms, redirect /authorize?…&sig',
|
|
1645
|
+
name: 'Authorize this app (no account)',
|
|
1646
|
+
screen: (
|
|
1647
|
+
<Approval
|
|
1648
|
+
{...approval()}
|
|
1649
|
+
app="Backend playground"
|
|
1650
|
+
destination="backend.example.com"
|
|
1651
|
+
keyAuthorization={{ ...key(), refusal: 'no_account' }}
|
|
1652
|
+
/>
|
|
1653
|
+
),
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
detail: 'GET /auth/callback?error=access_denied&state',
|
|
1657
|
+
edge: 'Deny, POST /v1/authorize/consent, accept: false',
|
|
1658
|
+
name: 'Backend callback',
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
detail: 'GET /auth/callback?code&state',
|
|
1662
|
+
edge: '302 /auth/start?key=false, 302 /authorize, 302 /oauth2/authorize, 302, no screen',
|
|
1663
|
+
name: 'Backend callback',
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
detail: 'Tokens stay on the server, without a key',
|
|
1667
|
+
edge: 'POST /oauth2/token with client_secret',
|
|
1668
|
+
name: 'Backend session',
|
|
1669
|
+
},
|
|
1670
|
+
],
|
|
1671
|
+
title: 'Backend app',
|
|
1672
|
+
},
|
|
1673
|
+
],
|
|
1674
|
+
title: 'App authorization',
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
flows: [
|
|
1678
|
+
{
|
|
1679
|
+
description:
|
|
1680
|
+
"The app's own pages send the code, verify it, and request the authorization. Tempo ID shows no screen.",
|
|
1681
|
+
nodes: [
|
|
1682
|
+
{ detail: 'Web playground', name: "App's sign-in" },
|
|
1683
|
+
{
|
|
1684
|
+
edge: appSignInEdge,
|
|
1685
|
+
name: 'Sign in',
|
|
1686
|
+
owner: 'app',
|
|
1687
|
+
screen: <AppSignIn />,
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
edge: 'POST /v1/auth/code',
|
|
1691
|
+
name: 'Check your email',
|
|
1692
|
+
owner: 'app',
|
|
1693
|
+
screen: <AppCode />,
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
edge: 'POST /v1/auth/code/verify, POST /v1/account, POST /v1/authorize',
|
|
1697
|
+
name: 'Signed in',
|
|
1698
|
+
owner: 'app',
|
|
1699
|
+
screen: <AppSignedIn />,
|
|
1700
|
+
},
|
|
1701
|
+
],
|
|
1702
|
+
title: 'Headless email code',
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
description:
|
|
1706
|
+
'A signed-in user skips the code. The app requests the authorization at once.',
|
|
1707
|
+
nodes: [
|
|
1708
|
+
{ detail: 'Session cookie set', name: "App's page" },
|
|
1709
|
+
{
|
|
1710
|
+
edge: 'GET /v1/auth/session, POST /v1/authorize',
|
|
1711
|
+
name: 'Signed in',
|
|
1712
|
+
owner: 'app',
|
|
1713
|
+
screen: <AppSignedIn />,
|
|
1714
|
+
},
|
|
1715
|
+
],
|
|
1716
|
+
title: 'Headless, signed in',
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
description:
|
|
1720
|
+
"The app's page runs the passkey ceremony with its client ID, which selects the app's realms.",
|
|
1721
|
+
nodes: [
|
|
1722
|
+
{ detail: 'Web playground', name: "App's sign-in" },
|
|
1723
|
+
{
|
|
1724
|
+
edge: appSignInEdge,
|
|
1725
|
+
name: 'Sign in',
|
|
1726
|
+
owner: 'app',
|
|
1727
|
+
screen: <AppSignIn />,
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
detail: 'POST /v1/auth/passkey/options with client_id',
|
|
1731
|
+
edge: 'Continue with passkey',
|
|
1732
|
+
name: 'Authenticator prompt',
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
edge: 'POST /v1/auth/passkey/verify, POST /v1/account, POST /v1/authorize',
|
|
1736
|
+
name: 'Signed in',
|
|
1737
|
+
owner: 'app',
|
|
1738
|
+
screen: <AppSignedIn />,
|
|
1739
|
+
},
|
|
1740
|
+
],
|
|
1741
|
+
title: 'Headless passkey',
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
description:
|
|
1745
|
+
"A wallet.tempo passkey without an email verifies one through the app's own code step. The passkey signs again with the code, which links it. If that passkey owns the account, the app continues through the headless passkey fallback below.",
|
|
1746
|
+
nodes: [
|
|
1747
|
+
{ detail: 'Web playground', name: "App's sign-in" },
|
|
1748
|
+
{
|
|
1749
|
+
edge: appSignInEdge,
|
|
1750
|
+
name: 'Sign in',
|
|
1751
|
+
owner: 'app',
|
|
1752
|
+
screen: <AppSignIn />,
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
detail: 'POST /v1/auth/passkey/options with client_id',
|
|
1756
|
+
edge: 'Continue with passkey',
|
|
1757
|
+
name: 'Authenticator prompt',
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
edge: 'POST /v1/auth/passkey/verify: step email',
|
|
1761
|
+
name: 'Add your email',
|
|
1762
|
+
owner: 'app',
|
|
1763
|
+
screen: <AppEmail />,
|
|
1764
|
+
},
|
|
1765
|
+
{
|
|
1766
|
+
edge: 'POST /v1/auth/code',
|
|
1767
|
+
name: 'Check your email',
|
|
1768
|
+
owner: 'app',
|
|
1769
|
+
screen: <AppCode />,
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
detail: 'POST /v1/auth/passkey/options with link',
|
|
1773
|
+
edge: 'Code complete',
|
|
1774
|
+
name: 'Authenticator prompt',
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
edge: 'POST /v1/auth/code/verify with link and assertion, POST /v1/account, POST /v1/authorize',
|
|
1778
|
+
name: 'Signed in',
|
|
1779
|
+
owner: 'app',
|
|
1780
|
+
screen: <AppSignedIn />,
|
|
1781
|
+
},
|
|
1782
|
+
],
|
|
1783
|
+
title: 'Headless passkey without an email',
|
|
1784
|
+
},
|
|
1785
|
+
{
|
|
1786
|
+
description:
|
|
1787
|
+
'Google returns the browser to the app with a session. The app then requests the authorization. A user without a Tempo Account is refused with `rejected`, so the app reads the accounts, creates the first one if none exists, and requests the authorization again.',
|
|
1788
|
+
nodes: [
|
|
1789
|
+
{ detail: 'Web playground', name: "App's sign-in" },
|
|
1790
|
+
{
|
|
1791
|
+
edge: appSignInEdge,
|
|
1792
|
+
name: 'Sign in',
|
|
1793
|
+
owner: 'app',
|
|
1794
|
+
screen: <AppSignIn />,
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
edge: 'GET /v1/auth/oauth/google?client_id&return_to',
|
|
1798
|
+
name: 'Google',
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
edge: 'GET /callback/google, 302 return_to, GET /v1/auth/session, POST /v1/authorize: rejected, GET /v1/account, POST /v1/account, GET /v1/auth/session, POST /v1/authorize',
|
|
1802
|
+
name: 'Signed in',
|
|
1803
|
+
owner: 'app',
|
|
1804
|
+
screen: <AppSignedIn />,
|
|
1805
|
+
},
|
|
1806
|
+
],
|
|
1807
|
+
title: 'Headless Google',
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
description:
|
|
1811
|
+
'Approval does not sign a key of a passkey that owns the account, so `POST /v1/authorize` refuses it with `webauthn_required`. The app signs in through a redirect without a key, which skips the approval page, since the playgrounds skip consent.',
|
|
1812
|
+
nodes: [
|
|
1813
|
+
{
|
|
1814
|
+
detail: 'POST /v1/authorize: webauthn_required',
|
|
1815
|
+
name: "App's sign-in",
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
detail: 'GET redirect_uri?code&state',
|
|
1819
|
+
edge: 'GET /authorize without key_authorization, 302 /oauth2/authorize, 302, no screen',
|
|
1820
|
+
name: 'Web app callback',
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
edge: 'POST /oauth2/token with code_verifier',
|
|
1824
|
+
name: 'Signed in',
|
|
1825
|
+
owner: 'app',
|
|
1826
|
+
screen: <AppSignedIn />,
|
|
1827
|
+
},
|
|
1828
|
+
],
|
|
1829
|
+
title: 'Headless passkey fallback',
|
|
1830
|
+
},
|
|
1831
|
+
],
|
|
1832
|
+
title: 'Headless apps',
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
flows: [
|
|
1836
|
+
{
|
|
1837
|
+
description:
|
|
1838
|
+
"`tempo-id login` requests a code with a new access key, and prints `verification_uri_complete`. Sign-in returns to the device page with the code filled in. The CLI's next poll receives the tokens and the signed key, and stores them.",
|
|
1839
|
+
nodes: [
|
|
1840
|
+
loginEntry,
|
|
1841
|
+
{
|
|
1842
|
+
detail: 'GET /device?code',
|
|
1843
|
+
edge: 'Shows verification_uri_complete',
|
|
1844
|
+
name: 'Browser',
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
edge: '302 /login?return_to=/device?code',
|
|
1848
|
+
name: 'Sign in',
|
|
1849
|
+
screen: <SignIn {...signIn()} />,
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
edge: 'POST /v1/auth/code',
|
|
1853
|
+
name: 'Check your email',
|
|
1854
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
edge: 'POST /v1/auth/code/verify: 200',
|
|
1858
|
+
name: 'Check your email (accepted)',
|
|
1859
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
edge: 'after 500 ms, /device?code',
|
|
1863
|
+
name: 'Authorize a device',
|
|
1864
|
+
screen: (
|
|
1865
|
+
<DeviceApproval
|
|
1866
|
+
{...deviceApproval()}
|
|
1867
|
+
request={deviceRequest()}
|
|
1868
|
+
/>
|
|
1869
|
+
),
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
edge: 'POST /v1/device/approve',
|
|
1873
|
+
name: 'Authorization successful',
|
|
1874
|
+
screen: (
|
|
1875
|
+
<DeviceApproval
|
|
1876
|
+
{...deviceApproval()}
|
|
1877
|
+
request={deviceRequest()}
|
|
1878
|
+
status="approved"
|
|
1879
|
+
/>
|
|
1880
|
+
),
|
|
1881
|
+
},
|
|
1882
|
+
loginExit,
|
|
1883
|
+
],
|
|
1884
|
+
title: 'Device code, signed out',
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
description:
|
|
1888
|
+
'A user who opens the bare `verification_uri` types the code that the terminal shows.',
|
|
1889
|
+
nodes: [
|
|
1890
|
+
loginEntry,
|
|
1891
|
+
{
|
|
1892
|
+
detail: 'GET /device',
|
|
1893
|
+
edge: 'Shows user_code and verification_uri',
|
|
1894
|
+
name: 'Browser',
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
edge: '200 device page',
|
|
1898
|
+
name: 'Authorize a device (enter a code)',
|
|
1899
|
+
screen: <DeviceApproval {...deviceApproval()} />,
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
edge: 'Continue, GET /device?code',
|
|
1903
|
+
name: 'Authorize a device',
|
|
1904
|
+
screen: (
|
|
1905
|
+
<DeviceApproval
|
|
1906
|
+
{...deviceApproval()}
|
|
1907
|
+
request={deviceRequest()}
|
|
1908
|
+
/>
|
|
1909
|
+
),
|
|
1910
|
+
},
|
|
1911
|
+
{
|
|
1912
|
+
edge: 'POST /v1/device/approve',
|
|
1913
|
+
name: 'Authorization successful',
|
|
1914
|
+
screen: (
|
|
1915
|
+
<DeviceApproval
|
|
1916
|
+
{...deviceApproval()}
|
|
1917
|
+
request={deviceRequest()}
|
|
1918
|
+
status="approved"
|
|
1919
|
+
/>
|
|
1920
|
+
),
|
|
1921
|
+
},
|
|
1922
|
+
loginExit,
|
|
1923
|
+
],
|
|
1924
|
+
title: 'Device code, typed',
|
|
1925
|
+
},
|
|
1926
|
+
{
|
|
1927
|
+
description:
|
|
1928
|
+
'A device client that reads discovery uses the standard endpoints of RFC 8628. The browser takes the same steps as for `POST /v1/device/authorize`.',
|
|
1929
|
+
nodes: [
|
|
1930
|
+
{ detail: 'POST /device/code', name: 'Device client' },
|
|
1931
|
+
{
|
|
1932
|
+
detail: 'GET /device?user_code',
|
|
1933
|
+
edge: 'Shows verification_uri_complete',
|
|
1934
|
+
name: 'Browser',
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
edge: '302 /login?return_to=/device?user_code',
|
|
1938
|
+
name: 'Sign in',
|
|
1939
|
+
screen: <SignIn {...signIn()} />,
|
|
1940
|
+
},
|
|
1941
|
+
{
|
|
1942
|
+
edge: 'POST /v1/auth/code',
|
|
1943
|
+
name: 'Check your email',
|
|
1944
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
edge: 'POST /v1/auth/code/verify: 200',
|
|
1948
|
+
name: 'Check your email (accepted)',
|
|
1949
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
edge: 'after 500 ms, /device?code',
|
|
1953
|
+
name: 'Authorize a device',
|
|
1954
|
+
screen: (
|
|
1955
|
+
<DeviceApproval
|
|
1956
|
+
{...deviceApproval()}
|
|
1957
|
+
request={{
|
|
1958
|
+
...deviceRequest(),
|
|
1959
|
+
device: undefined,
|
|
1960
|
+
key_authorization: undefined,
|
|
1961
|
+
}}
|
|
1962
|
+
/>
|
|
1963
|
+
),
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
edge: 'Approve, POST /v1/device/approve',
|
|
1967
|
+
name: 'Authorization successful',
|
|
1968
|
+
screen: (
|
|
1969
|
+
<DeviceApproval
|
|
1970
|
+
{...deviceApproval()}
|
|
1971
|
+
request={{
|
|
1972
|
+
...deviceRequest(),
|
|
1973
|
+
device: undefined,
|
|
1974
|
+
key_authorization: undefined,
|
|
1975
|
+
}}
|
|
1976
|
+
status="approved"
|
|
1977
|
+
/>
|
|
1978
|
+
),
|
|
1979
|
+
},
|
|
1980
|
+
{
|
|
1981
|
+
detail:
|
|
1982
|
+
'POST /oauth2/token with grant_type=urn:ietf:params:oauth:grant-type:device_code',
|
|
1983
|
+
edge: 'Next poll: tokens',
|
|
1984
|
+
name: 'Device client',
|
|
1985
|
+
},
|
|
1986
|
+
],
|
|
1987
|
+
title: 'Device code, standard endpoints',
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
description:
|
|
1991
|
+
"Denying ends the request. The CLI's next poll is refused.",
|
|
1992
|
+
nodes: [
|
|
1993
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
1994
|
+
{
|
|
1995
|
+
edge: '200 device page',
|
|
1996
|
+
name: 'Authorize a device',
|
|
1997
|
+
screen: (
|
|
1998
|
+
<DeviceApproval
|
|
1999
|
+
{...deviceApproval()}
|
|
2000
|
+
request={deviceRequest()}
|
|
2001
|
+
/>
|
|
2002
|
+
),
|
|
2003
|
+
},
|
|
2004
|
+
{
|
|
2005
|
+
edge: 'POST /v1/device/deny',
|
|
2006
|
+
name: 'Denied',
|
|
2007
|
+
screen: (
|
|
2008
|
+
<DeviceApproval
|
|
2009
|
+
{...deviceApproval()}
|
|
2010
|
+
request={deviceRequest()}
|
|
2011
|
+
status="denied"
|
|
2012
|
+
/>
|
|
2013
|
+
),
|
|
2014
|
+
},
|
|
2015
|
+
{
|
|
2016
|
+
detail: 'Exits with not_signed_in',
|
|
2017
|
+
edge: 'POST /v1/device/token: request_denied',
|
|
2018
|
+
name: 'tempo-id login',
|
|
2019
|
+
},
|
|
2020
|
+
],
|
|
2021
|
+
title: 'Device code, denied',
|
|
2022
|
+
},
|
|
2023
|
+
{
|
|
2024
|
+
description:
|
|
2025
|
+
'A passkey owns the account, so the approval returns the passkey step. The passkey signs the key’s digest in this browser, and the page approves again.',
|
|
2026
|
+
nodes: [
|
|
2027
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2028
|
+
{
|
|
2029
|
+
edge: '200 device page',
|
|
2030
|
+
name: 'Authorize a device',
|
|
2031
|
+
screen: (
|
|
2032
|
+
<DeviceApproval
|
|
2033
|
+
{...deviceApproval()}
|
|
2034
|
+
request={deviceRequest()}
|
|
2035
|
+
/>
|
|
2036
|
+
),
|
|
2037
|
+
},
|
|
2038
|
+
{
|
|
2039
|
+
edge: 'Approve, POST /v1/device/approve',
|
|
2040
|
+
name: 'Authorize a device (approving)',
|
|
2041
|
+
screen: (
|
|
2042
|
+
<DeviceApproval
|
|
2043
|
+
{...deviceApproval()}
|
|
2044
|
+
approving
|
|
2045
|
+
request={deviceRequest()}
|
|
2046
|
+
/>
|
|
2047
|
+
),
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
detail: 'Signs the key authorization, over the approving page',
|
|
2051
|
+
edge: 'step passkey',
|
|
2052
|
+
name: 'Authenticator prompt',
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
edge: 'POST /v1/device/approve with passkey.assertion',
|
|
2056
|
+
name: 'Authorization successful',
|
|
2057
|
+
screen: (
|
|
2058
|
+
<DeviceApproval
|
|
2059
|
+
{...deviceApproval()}
|
|
2060
|
+
request={deviceRequest()}
|
|
2061
|
+
status="approved"
|
|
2062
|
+
/>
|
|
2063
|
+
),
|
|
2064
|
+
},
|
|
2065
|
+
loginExit,
|
|
2066
|
+
],
|
|
2067
|
+
title: 'Device approval for a passkey account',
|
|
2068
|
+
},
|
|
2069
|
+
{
|
|
2070
|
+
description:
|
|
2071
|
+
'Approval cannot sign some keys, so the page holds Approve and states why. Deny still ends the request.',
|
|
2072
|
+
nodes: [
|
|
2073
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2074
|
+
{
|
|
2075
|
+
edge: 'The key names an account that the user does not hold',
|
|
2076
|
+
name: 'Authorize a device (foreign account)',
|
|
2077
|
+
screen: (
|
|
2078
|
+
<DeviceApproval
|
|
2079
|
+
{...deviceApproval()}
|
|
2080
|
+
request={{
|
|
2081
|
+
...deviceRequest(),
|
|
2082
|
+
key_authorization: { ...key(), refusal: 'foreign_account' },
|
|
2083
|
+
}}
|
|
2084
|
+
/>
|
|
2085
|
+
),
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
edge: 'Or: no account, and no Privy app to create one',
|
|
2089
|
+
name: 'Authorize a device (no account)',
|
|
2090
|
+
screen: (
|
|
2091
|
+
<DeviceApproval
|
|
2092
|
+
{...deviceApproval()}
|
|
2093
|
+
request={{
|
|
2094
|
+
...deviceRequest(),
|
|
2095
|
+
key_authorization: { ...key(), refusal: 'no_account' },
|
|
2096
|
+
}}
|
|
2097
|
+
/>
|
|
2098
|
+
),
|
|
2099
|
+
},
|
|
2100
|
+
{
|
|
2101
|
+
edge: 'Or: a passkey whose relying party is not Tempo ID’s, or POST /v1/device/approve: webauthn_required',
|
|
2102
|
+
name: 'Authorize a device (passkey)',
|
|
2103
|
+
screen: (
|
|
2104
|
+
<DeviceApproval
|
|
2105
|
+
{...deviceApproval()}
|
|
2106
|
+
request={{
|
|
2107
|
+
...deviceRequest(),
|
|
2108
|
+
key_authorization: { ...key(), refusal: 'passkey' },
|
|
2109
|
+
}}
|
|
2110
|
+
/>
|
|
2111
|
+
),
|
|
2112
|
+
},
|
|
2113
|
+
{
|
|
2114
|
+
edge: 'Or: the accounts cannot be read',
|
|
2115
|
+
name: 'Authorize a device (unavailable)',
|
|
2116
|
+
screen: (
|
|
2117
|
+
<DeviceApproval
|
|
2118
|
+
{...deviceApproval()}
|
|
2119
|
+
request={{
|
|
2120
|
+
...deviceRequest(),
|
|
2121
|
+
key_authorization: { ...key(), refusal: 'unavailable' },
|
|
2122
|
+
}}
|
|
2123
|
+
/>
|
|
2124
|
+
),
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
detail: 'POST /v1/device/token: request_denied',
|
|
2128
|
+
edge: 'Deny, POST /v1/device/deny',
|
|
2129
|
+
name: 'tempo-id login',
|
|
2130
|
+
},
|
|
2131
|
+
],
|
|
2132
|
+
title: 'Device key that approval cannot sign',
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
description:
|
|
2136
|
+
'A CLI with the `tempo-id:sign` scope sends a challenge, and the device page shows the message that the key signs.',
|
|
2137
|
+
nodes: [
|
|
2138
|
+
{
|
|
2139
|
+
detail:
|
|
2140
|
+
'POST /v1/device/authorize with key_authorization and challenge',
|
|
2141
|
+
name: 'CLI',
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
edge: 'GET /device?code, 200 device page',
|
|
2145
|
+
name: 'Authorize a device (challenge)',
|
|
2146
|
+
screen: (
|
|
2147
|
+
<DeviceApproval
|
|
2148
|
+
{...deviceApproval()}
|
|
2149
|
+
request={{
|
|
2150
|
+
...deviceRequest(),
|
|
2151
|
+
challenge: 'Sign in to api.example.com\nNonce: 8f2a91',
|
|
2152
|
+
}}
|
|
2153
|
+
/>
|
|
2154
|
+
),
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
edge: 'Approve, POST /v1/device/approve, or the passkey step first',
|
|
2158
|
+
name: 'Authorization successful',
|
|
2159
|
+
screen: (
|
|
2160
|
+
<DeviceApproval
|
|
2161
|
+
{...deviceApproval()}
|
|
2162
|
+
request={deviceRequest()}
|
|
2163
|
+
status="approved"
|
|
2164
|
+
/>
|
|
2165
|
+
),
|
|
2166
|
+
},
|
|
2167
|
+
{
|
|
2168
|
+
detail:
|
|
2169
|
+
'Tokens, and key_authorization bound to the challenge’s hash',
|
|
2170
|
+
edge: 'POST /v1/device/token: authorization_pending, then tokens',
|
|
2171
|
+
name: 'CLI',
|
|
2172
|
+
},
|
|
2173
|
+
],
|
|
2174
|
+
title: 'Device code with a challenge',
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
description:
|
|
2178
|
+
'A code that is already approved or denied shows its outcome again, without the decision buttons.',
|
|
2179
|
+
nodes: [
|
|
2180
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2181
|
+
{
|
|
2182
|
+
edge: '200, status: approved',
|
|
2183
|
+
name: 'Authorization successful',
|
|
2184
|
+
screen: (
|
|
2185
|
+
<DeviceApproval
|
|
2186
|
+
{...deviceApproval()}
|
|
2187
|
+
request={deviceRequest()}
|
|
2188
|
+
status="approved"
|
|
2189
|
+
/>
|
|
2190
|
+
),
|
|
2191
|
+
},
|
|
2192
|
+
],
|
|
2193
|
+
title: 'Decided device code',
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
description:
|
|
2197
|
+
'A code that no one approves before `expires_in` ends the login. The CLI stores nothing, and the user runs `tempo-id login` again.',
|
|
2198
|
+
nodes: [
|
|
2199
|
+
loginEntry,
|
|
2200
|
+
{
|
|
2201
|
+
detail: 'Exits with not_signed_in',
|
|
2202
|
+
edge: 'POST /v1/device/token: authorization_pending, then device_code_expired',
|
|
2203
|
+
name: 'tempo-id login',
|
|
2204
|
+
},
|
|
2205
|
+
],
|
|
2206
|
+
title: 'tempo-id login, expired',
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
description:
|
|
2210
|
+
'The CLI reads the stored session, refreshes its access token when it expires within 30 seconds, and reads the user.',
|
|
2211
|
+
nodes: [
|
|
2212
|
+
{ detail: '~/.tempo/id/credentials.json', name: 'tempo-id whoami' },
|
|
2213
|
+
{
|
|
2214
|
+
detail: 'Email and Tempo Account',
|
|
2215
|
+
edge: 'POST /oauth2/token with refresh_token within 30 s of expiry, GET /oauth2/userinfo',
|
|
2216
|
+
name: 'tempo-id whoami',
|
|
2217
|
+
},
|
|
2218
|
+
],
|
|
2219
|
+
title: 'tempo-id whoami',
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
description:
|
|
2223
|
+
'Each device request holds the code field or the decision buttons, and shows a spinner, until it settles.',
|
|
2224
|
+
nodes: [
|
|
2225
|
+
{ detail: 'GET /device', name: 'Browser' },
|
|
2226
|
+
{
|
|
2227
|
+
edge: 'Continue, GET /device?code',
|
|
2228
|
+
name: 'Authorize a device (fetching)',
|
|
2229
|
+
screen: (
|
|
2230
|
+
<DeviceApproval
|
|
2231
|
+
{...deviceApproval()}
|
|
2232
|
+
code="BDWP-HQPK"
|
|
2233
|
+
fetching
|
|
2234
|
+
/>
|
|
2235
|
+
),
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
edge: 'Approve, POST /v1/device/approve',
|
|
2239
|
+
name: 'Authorize a device (approving)',
|
|
2240
|
+
screen: (
|
|
2241
|
+
<DeviceApproval
|
|
2242
|
+
{...deviceApproval()}
|
|
2243
|
+
approving
|
|
2244
|
+
request={deviceRequest()}
|
|
2245
|
+
/>
|
|
2246
|
+
),
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
edge: 'Or: Deny, POST /v1/device/deny',
|
|
2250
|
+
name: 'Authorize a device (denying)',
|
|
2251
|
+
screen: (
|
|
2252
|
+
<DeviceApproval
|
|
2253
|
+
{...deviceApproval()}
|
|
2254
|
+
denying
|
|
2255
|
+
request={deviceRequest()}
|
|
2256
|
+
/>
|
|
2257
|
+
),
|
|
2258
|
+
},
|
|
2259
|
+
],
|
|
2260
|
+
title: 'Device code in progress',
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
description:
|
|
2264
|
+
'A device app without a display name could be anyone’s, so the request omits `app`, and the page flags an unverified app.',
|
|
2265
|
+
nodes: [
|
|
2266
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2267
|
+
{
|
|
2268
|
+
edge: '200 device page, request without app',
|
|
2269
|
+
name: 'Authorize a device (unverified app)',
|
|
2270
|
+
screen: (
|
|
2271
|
+
<DeviceApproval
|
|
2272
|
+
{...deviceApproval()}
|
|
2273
|
+
request={{ ...deviceRequest(), app: undefined }}
|
|
2274
|
+
/>
|
|
2275
|
+
),
|
|
2276
|
+
},
|
|
2277
|
+
],
|
|
2278
|
+
title: 'Unverified device app',
|
|
2279
|
+
},
|
|
2280
|
+
{
|
|
2281
|
+
description:
|
|
2282
|
+
'A code that matches no pending request, or one that another user holds, keeps the code form.',
|
|
2283
|
+
nodes: [
|
|
2284
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2285
|
+
{
|
|
2286
|
+
edge: 'unknown_code',
|
|
2287
|
+
name: 'Authorize a device (unknown code)',
|
|
2288
|
+
screen: (
|
|
2289
|
+
<DeviceApproval
|
|
2290
|
+
{...deviceApproval()}
|
|
2291
|
+
code="BDWP-HQPX"
|
|
2292
|
+
error="Invalid or expired code. Check the code in the terminal."
|
|
2293
|
+
invalid
|
|
2294
|
+
/>
|
|
2295
|
+
),
|
|
2296
|
+
},
|
|
2297
|
+
],
|
|
2298
|
+
title: 'Unknown device code',
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
description:
|
|
2302
|
+
'Each user’s code lookups share one rate limit, so the page refuses a lookup over it and keeps the code form.',
|
|
2303
|
+
nodes: [
|
|
2304
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2305
|
+
{
|
|
2306
|
+
edge: 'Lookup over the rate limit',
|
|
2307
|
+
name: 'Authorize a device (rate limited)',
|
|
2308
|
+
screen: (
|
|
2309
|
+
<DeviceApproval
|
|
2310
|
+
{...deviceApproval()}
|
|
2311
|
+
code="BDWP-HQPK"
|
|
2312
|
+
error="Too many requests. Retry after 60 seconds."
|
|
2313
|
+
invalid
|
|
2314
|
+
/>
|
|
2315
|
+
),
|
|
2316
|
+
},
|
|
2317
|
+
],
|
|
2318
|
+
title: 'Device code lookup rate limited',
|
|
2319
|
+
},
|
|
2320
|
+
],
|
|
2321
|
+
title: 'Device grant',
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
flows: [
|
|
2325
|
+
{
|
|
2326
|
+
description:
|
|
2327
|
+
'Sign-out ends the session and revokes its refresh tokens, then opens the sign-in page.',
|
|
2328
|
+
nodes: [
|
|
2329
|
+
{ detail: 'GET /', name: 'Browser opens Tempo ID' },
|
|
2330
|
+
{
|
|
2331
|
+
name: 'Your account',
|
|
2332
|
+
screen: <Account {...account()} />,
|
|
2333
|
+
},
|
|
2334
|
+
{
|
|
2335
|
+
edge: 'Sign out, POST /v1/auth/logout',
|
|
2336
|
+
name: 'Your account (signing out)',
|
|
2337
|
+
screen: <Account {...account()} signingOut />,
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
edge: '200, then /login',
|
|
2341
|
+
name: 'Sign in',
|
|
2342
|
+
screen: <SignIn {...signIn()} />,
|
|
2343
|
+
},
|
|
2344
|
+
],
|
|
2345
|
+
title: 'Sign-out',
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
description:
|
|
2349
|
+
'An app signs the user out with its ID token. Tempo ID ends the session that the hint names, if this browser holds it, and redirects to the app.',
|
|
2350
|
+
nodes: [
|
|
2351
|
+
{
|
|
2352
|
+
detail:
|
|
2353
|
+
'GET /oauth2/end-session?id_token_hint&post_logout_redirect_uri&state',
|
|
2354
|
+
name: 'Web app',
|
|
2355
|
+
},
|
|
2356
|
+
{
|
|
2357
|
+
detail: 'GET post_logout_redirect_uri?state',
|
|
2358
|
+
edge: '302, no screen',
|
|
2359
|
+
name: 'Web app',
|
|
2360
|
+
},
|
|
2361
|
+
],
|
|
2362
|
+
title: 'RP-initiated logout',
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
description:
|
|
2366
|
+
'A cross-site form POST carries no `SameSite=Lax` session cookie, so Tempo ID seals the parameters and continues as a GET, which carries the cookie.',
|
|
2367
|
+
nodes: [
|
|
2368
|
+
{
|
|
2369
|
+
detail:
|
|
2370
|
+
'POST /oauth2/end-session with id_token_hint, post_logout_redirect_uri, and state',
|
|
2371
|
+
name: 'Web app',
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
detail: 'GET /oauth2/end-session?continuation',
|
|
2375
|
+
edge: '303, no session cookie',
|
|
2376
|
+
name: 'Browser',
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
detail: 'GET post_logout_redirect_uri?state',
|
|
2380
|
+
edge: '302, no screen',
|
|
2381
|
+
name: 'Web app',
|
|
2382
|
+
},
|
|
2383
|
+
],
|
|
2384
|
+
title: 'RP-initiated logout, form POST',
|
|
2385
|
+
},
|
|
2386
|
+
{
|
|
2387
|
+
description:
|
|
2388
|
+
"An app's own Sign out ends the Tempo ID session. The app forgets the sign-in even if the request fails, and shows the error.",
|
|
2389
|
+
nodes: [
|
|
2390
|
+
{ detail: 'Web playground', name: "App's page" },
|
|
2391
|
+
{
|
|
2392
|
+
edge: 'GET /v1/auth/session, POST /v1/authorize',
|
|
2393
|
+
name: 'Signed in',
|
|
2394
|
+
owner: 'app',
|
|
2395
|
+
screen: <AppSignedIn />,
|
|
2396
|
+
},
|
|
2397
|
+
{
|
|
2398
|
+
edge: 'Sign out, POST /v1/auth/logout',
|
|
2399
|
+
name: 'Sign in',
|
|
2400
|
+
owner: 'app',
|
|
2401
|
+
screen: <AppSignIn />,
|
|
2402
|
+
},
|
|
2403
|
+
],
|
|
2404
|
+
title: 'Headless sign-out',
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
description:
|
|
2408
|
+
'The CLI revokes the stored refresh token at the deployment that issued it, and deletes the session even if the revocation fails.',
|
|
2409
|
+
nodes: [
|
|
2410
|
+
{ detail: '~/.tempo/id/credentials.json', name: 'tempo-id logout' },
|
|
2411
|
+
{
|
|
2412
|
+
detail: 'Session deleted',
|
|
2413
|
+
edge: 'POST /oauth2/revoke with the refresh token',
|
|
2414
|
+
name: 'tempo-id logout',
|
|
2415
|
+
},
|
|
2416
|
+
],
|
|
2417
|
+
title: 'tempo-id logout',
|
|
2418
|
+
},
|
|
2419
|
+
],
|
|
2420
|
+
title: 'Sign-out',
|
|
2421
|
+
},
|
|
2422
|
+
{
|
|
2423
|
+
flows: [
|
|
2424
|
+
{
|
|
2425
|
+
description:
|
|
2426
|
+
'A refused send keeps the first step, with the refusal below the email field, which it marks invalid. The user can try again or pick another method.',
|
|
2427
|
+
nodes: [
|
|
2428
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2429
|
+
{
|
|
2430
|
+
edge: 'POST /v1/auth/code: 503',
|
|
2431
|
+
name: 'Sign in (send failed)',
|
|
2432
|
+
screen: <SignIn {...signIn()} error={unavailable} invalid />,
|
|
2433
|
+
},
|
|
2434
|
+
],
|
|
2435
|
+
title: 'Send failed',
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
description:
|
|
2439
|
+
'A canceled prompt or a refused passkey keeps the first step, with the error below the email field. The email field is not marked invalid, since no address was sent.',
|
|
2440
|
+
nodes: [
|
|
2441
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2442
|
+
{
|
|
2443
|
+
edge: 'Canceled prompt, or POST /v1/auth/passkey/verify refused',
|
|
2444
|
+
name: 'Sign in (passkey failed)',
|
|
2445
|
+
screen: (
|
|
2446
|
+
<SignIn
|
|
2447
|
+
{...signIn()}
|
|
2448
|
+
error="Passkey sign-in was canceled. Try again, or continue with email."
|
|
2449
|
+
/>
|
|
2450
|
+
),
|
|
2451
|
+
},
|
|
2452
|
+
],
|
|
2453
|
+
title: 'Passkey failed',
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
description:
|
|
2457
|
+
'A refused code clears the field and marks it invalid, so the next code can be typed at once.',
|
|
2458
|
+
nodes: [
|
|
2459
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2460
|
+
{
|
|
2461
|
+
edge: 'POST /v1/auth/code/verify: 401',
|
|
2462
|
+
name: 'Check your email (wrong code)',
|
|
2463
|
+
screen: (
|
|
2464
|
+
<SignIn
|
|
2465
|
+
{...signIn()}
|
|
2466
|
+
email={email}
|
|
2467
|
+
error="Invalid or expired code. Request a new one."
|
|
2468
|
+
invalid
|
|
2469
|
+
/>
|
|
2470
|
+
),
|
|
2471
|
+
},
|
|
2472
|
+
],
|
|
2473
|
+
title: 'Wrong code',
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
description:
|
|
2477
|
+
'A passkey that does not sign again, or whose assertion is refused, drops the link. The code field clears and takes focus, so the next code signs in with email only.',
|
|
2478
|
+
nodes: [
|
|
2479
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2480
|
+
{
|
|
2481
|
+
edge: 'Canceled prompt, or POST /v1/auth/code/verify with link: webauthn_rejected',
|
|
2482
|
+
name: 'Check your email (link failed)',
|
|
2483
|
+
screen: (
|
|
2484
|
+
<SignIn
|
|
2485
|
+
{...signIn()}
|
|
2486
|
+
email={email}
|
|
2487
|
+
error="Your passkey did not confirm the sign-in. Enter the code again to sign in with email only."
|
|
2488
|
+
invalid
|
|
2489
|
+
/>
|
|
2490
|
+
),
|
|
2491
|
+
},
|
|
2492
|
+
],
|
|
2493
|
+
title: 'Passkey link failed',
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
description:
|
|
2497
|
+
'A resent code shows Code sent on the button, and a hidden status announces the send. The code field keeps its value.',
|
|
2498
|
+
nodes: [
|
|
2499
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2500
|
+
{
|
|
2501
|
+
edge: 'Resend code, POST /v1/auth/code',
|
|
2502
|
+
name: 'Check your email (resent)',
|
|
2503
|
+
screen: <SignIn {...signIn()} email={email} resend="sent" />,
|
|
2504
|
+
},
|
|
2505
|
+
],
|
|
2506
|
+
title: 'Resent code',
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
description:
|
|
2510
|
+
'A refused resend keeps the code step and its code, with the refusal below the code field. The code stays valid, so the field is not marked invalid.',
|
|
2511
|
+
nodes: [
|
|
2512
|
+
{ detail: 'GET /login', name: 'Browser opens sign-in' },
|
|
2513
|
+
{
|
|
2514
|
+
edge: 'Resend code, POST /v1/auth/code: 429',
|
|
2515
|
+
name: 'Check your email (resend refused)',
|
|
2516
|
+
screen: (
|
|
2517
|
+
<SignIn
|
|
2518
|
+
{...signIn()}
|
|
2519
|
+
email={email}
|
|
2520
|
+
error="Too many requests. Retry after 60 seconds."
|
|
2521
|
+
/>
|
|
2522
|
+
),
|
|
2523
|
+
},
|
|
2524
|
+
],
|
|
2525
|
+
title: 'Resend refused',
|
|
2526
|
+
},
|
|
2527
|
+
{
|
|
2528
|
+
description:
|
|
2529
|
+
'A failed selection keeps the account step, so the user can choose again. The page does not reload, so an ended session or an expired request shows its refusal in the same place.',
|
|
2530
|
+
nodes: [
|
|
2531
|
+
{ detail: 'GET /choose-account?…&sig', name: 'Browser' },
|
|
2532
|
+
{
|
|
2533
|
+
edge: 'POST /v1/authorize/account: 503',
|
|
2534
|
+
name: 'Choose a wallet (failed)',
|
|
2535
|
+
screen: (
|
|
2536
|
+
<ChooseAccount {...chooseAccount()} error={unavailable} />
|
|
2537
|
+
),
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
edge: 'Or: POST /v1/authorize/account: session_required',
|
|
2541
|
+
name: 'Choose a wallet (session ended)',
|
|
2542
|
+
screen: (
|
|
2543
|
+
<ChooseAccount {...chooseAccount()} error="Sign in first." />
|
|
2544
|
+
),
|
|
2545
|
+
},
|
|
2546
|
+
{
|
|
2547
|
+
edge: 'Or: POST /v1/authorize/account: authorization_expired',
|
|
2548
|
+
name: 'Choose a wallet (request expired)',
|
|
2549
|
+
screen: (
|
|
2550
|
+
<ChooseAccount
|
|
2551
|
+
{...chooseAccount()}
|
|
2552
|
+
error="Expired authorization request: the request from the app is no longer valid. Return to the app, and start again."
|
|
2553
|
+
/>
|
|
2554
|
+
),
|
|
2555
|
+
},
|
|
2556
|
+
],
|
|
2557
|
+
title: 'Account selection failed',
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
description:
|
|
2561
|
+
'A failed decision keeps the approval page, with the refusal below the buttons.',
|
|
2562
|
+
nodes: [
|
|
2563
|
+
{ detail: 'GET /authorize?…&sig', name: 'Browser' },
|
|
2564
|
+
{
|
|
2565
|
+
edge: 'POST /v1/authorize/consent: 503',
|
|
2566
|
+
name: 'Authorize this app (failed)',
|
|
2567
|
+
screen: <Approval {...approval()} error={unavailable} />,
|
|
2568
|
+
},
|
|
2569
|
+
],
|
|
2570
|
+
title: 'Approval failed',
|
|
2571
|
+
},
|
|
2572
|
+
{
|
|
2573
|
+
description:
|
|
2574
|
+
'A spent authorization rate limit refuses Approve, as it refuses the grant at sign-in. The approval page stays until the limit resets.',
|
|
2575
|
+
nodes: [
|
|
2576
|
+
{ detail: 'GET /authorize?…&sig', name: 'Browser' },
|
|
2577
|
+
{
|
|
2578
|
+
edge: 'Approve, POST /v1/authorize/consent: 429 rate_limited',
|
|
2579
|
+
name: 'Authorize this app (rate limited)',
|
|
2580
|
+
screen: (
|
|
2581
|
+
<Approval
|
|
2582
|
+
{...approval()}
|
|
2583
|
+
error="Too many requests. Retry after 60 seconds."
|
|
2584
|
+
/>
|
|
2585
|
+
),
|
|
2586
|
+
},
|
|
2587
|
+
],
|
|
2588
|
+
title: 'Approval rate limited',
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
description:
|
|
2592
|
+
'A canceled prompt or a refused assertion keeps the approval page, and Approve starts a new passkey step.',
|
|
2593
|
+
nodes: [
|
|
2594
|
+
{ detail: 'GET /authorize?…&sig', name: 'Browser' },
|
|
2595
|
+
{
|
|
2596
|
+
edge: 'Canceled prompt, or POST /v1/authorize/consent with passkey.assertion: webauthn_rejected',
|
|
2597
|
+
name: 'Authorize this app (passkey failed)',
|
|
2598
|
+
screen: (
|
|
2599
|
+
<Approval
|
|
2600
|
+
{...approval()}
|
|
2601
|
+
error="The passkey request was canceled. Approve again, or deny the request."
|
|
2602
|
+
/>
|
|
2603
|
+
),
|
|
2604
|
+
},
|
|
2605
|
+
],
|
|
2606
|
+
title: 'Passkey approval failed',
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
description:
|
|
2610
|
+
'A session that ends before the decision reloads the page, and the loader sends the browser to sign-in with the signed query. A sign-in already spent this request, so the next sign-in opens the approval page again.',
|
|
2611
|
+
nodes: [
|
|
2612
|
+
{ detail: 'GET /authorize?…&sig', name: 'Browser' },
|
|
2613
|
+
{
|
|
2614
|
+
edge: '200 approval page',
|
|
2615
|
+
name: 'Authorize this app',
|
|
2616
|
+
screen: <Approval {...approval()} />,
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
edge: 'Approve, POST /v1/authorize/consent: session_required, reload, 302 /login?…&sig',
|
|
2620
|
+
name: 'Sign in (key)',
|
|
2621
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
2622
|
+
},
|
|
2623
|
+
{
|
|
2624
|
+
edge: 'POST /v1/auth/code',
|
|
2625
|
+
name: 'Check your email',
|
|
2626
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
2627
|
+
},
|
|
2628
|
+
{
|
|
2629
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200',
|
|
2630
|
+
name: 'Check your email (accepted)',
|
|
2631
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
2632
|
+
},
|
|
2633
|
+
{
|
|
2634
|
+
edge: 'after 500 ms, redirect /authorize?…&sig',
|
|
2635
|
+
name: 'Authorize this app',
|
|
2636
|
+
screen: <Approval {...approval()} />,
|
|
2637
|
+
},
|
|
2638
|
+
{
|
|
2639
|
+
detail: 'GET redirect_uri?code&state',
|
|
2640
|
+
edge: 'Approve, POST /v1/authorize/consent',
|
|
2641
|
+
name: 'Web app callback',
|
|
2642
|
+
},
|
|
2643
|
+
{
|
|
2644
|
+
...tokenExit,
|
|
2645
|
+
detail: 'Tokens, account, and key_authorization',
|
|
2646
|
+
},
|
|
2647
|
+
],
|
|
2648
|
+
title: 'Approval, session expired',
|
|
2649
|
+
},
|
|
2650
|
+
{
|
|
2651
|
+
description:
|
|
2652
|
+
'A user who was signed in when the request arrived reached the approval page without a sign-in, so the request is unspent. The sign-in after the reload grants the key and returns to the app at once.',
|
|
2653
|
+
nodes: [
|
|
2654
|
+
{ detail: 'GET /authorize?…&sig', name: 'Browser' },
|
|
2655
|
+
{
|
|
2656
|
+
edge: '200 approval page',
|
|
2657
|
+
name: 'Authorize this app',
|
|
2658
|
+
screen: <Approval {...approval()} />,
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
edge: 'Approve, POST /v1/authorize/consent: session_required, reload, 302 /login?…&sig',
|
|
2662
|
+
name: 'Sign in (key)',
|
|
2663
|
+
screen: <SignIn {...signIn()} request={signInRequest()} />,
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
edge: 'POST /v1/auth/code',
|
|
2667
|
+
name: 'Check your email',
|
|
2668
|
+
screen: <SignIn {...signIn()} email={email} />,
|
|
2669
|
+
},
|
|
2670
|
+
{
|
|
2671
|
+
edge: 'POST /v1/auth/code/verify with oauth_query: 200, grants the key',
|
|
2672
|
+
name: 'Check your email (accepted)',
|
|
2673
|
+
screen: <SignIn {...signIn()} email={email} verified />,
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
detail: 'GET redirect_uri?code&state',
|
|
2677
|
+
edge: 'after 500 ms, redirect',
|
|
2678
|
+
name: 'Web app callback',
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
...tokenExit,
|
|
2682
|
+
detail: 'Tokens, account, and key_authorization',
|
|
2683
|
+
},
|
|
2684
|
+
],
|
|
2685
|
+
title: 'Approval, session expired, unspent request',
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
description:
|
|
2689
|
+
'A failed decision keeps the device page with the request, and the refusal below the buttons.',
|
|
2690
|
+
nodes: [
|
|
2691
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2692
|
+
{
|
|
2693
|
+
edge: 'POST /v1/device/approve: 503',
|
|
2694
|
+
name: 'Authorize a device (failed)',
|
|
2695
|
+
screen: (
|
|
2696
|
+
<DeviceApproval
|
|
2697
|
+
{...deviceApproval()}
|
|
2698
|
+
error={unavailable}
|
|
2699
|
+
request={deviceRequest()}
|
|
2700
|
+
/>
|
|
2701
|
+
),
|
|
2702
|
+
},
|
|
2703
|
+
],
|
|
2704
|
+
title: 'Device decision failed',
|
|
2705
|
+
},
|
|
2706
|
+
{
|
|
2707
|
+
description:
|
|
2708
|
+
'A session that ends before the decision reloads the page, and sign-in returns to the same code. The request is bound to its user, so another user’s sign-in shows the unknown code.',
|
|
2709
|
+
nodes: [
|
|
2710
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2711
|
+
{
|
|
2712
|
+
edge: '200 device page',
|
|
2713
|
+
name: 'Authorize a device',
|
|
2714
|
+
screen: (
|
|
2715
|
+
<DeviceApproval
|
|
2716
|
+
{...deviceApproval()}
|
|
2717
|
+
request={deviceRequest()}
|
|
2718
|
+
/>
|
|
2719
|
+
),
|
|
2720
|
+
},
|
|
2721
|
+
{
|
|
2722
|
+
edge: 'Approve, POST /v1/device/approve: session_required, reload, 302 /login?return_to=/device?code',
|
|
2723
|
+
name: 'Sign in',
|
|
2724
|
+
screen: <SignIn {...signIn()} />,
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
edge: 'Sign-in, /device?code',
|
|
2728
|
+
name: 'Authorize a device',
|
|
2729
|
+
screen: (
|
|
2730
|
+
<DeviceApproval
|
|
2731
|
+
{...deviceApproval()}
|
|
2732
|
+
request={deviceRequest()}
|
|
2733
|
+
/>
|
|
2734
|
+
),
|
|
2735
|
+
},
|
|
2736
|
+
],
|
|
2737
|
+
title: 'Device decision, session expired',
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
description:
|
|
2741
|
+
'A canceled prompt or a refused assertion keeps the device page, and Approve starts a new passkey step.',
|
|
2742
|
+
nodes: [
|
|
2743
|
+
{ detail: 'GET /device?code', name: 'Browser' },
|
|
2744
|
+
{
|
|
2745
|
+
edge: 'Canceled prompt, or POST /v1/device/approve with passkey.assertion: webauthn_rejected',
|
|
2746
|
+
name: 'Authorize a device (passkey failed)',
|
|
2747
|
+
screen: (
|
|
2748
|
+
<DeviceApproval
|
|
2749
|
+
{...deviceApproval()}
|
|
2750
|
+
error="The passkey request was canceled. Approve again, or deny the request."
|
|
2751
|
+
request={deviceRequest()}
|
|
2752
|
+
/>
|
|
2753
|
+
),
|
|
2754
|
+
},
|
|
2755
|
+
],
|
|
2756
|
+
title: 'Device passkey approval failed',
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
description:
|
|
2760
|
+
'A failed sign-out keeps the account page, with the refusal above the button.',
|
|
2761
|
+
nodes: [
|
|
2762
|
+
{ detail: 'GET /', name: 'Browser opens Tempo ID' },
|
|
2763
|
+
{
|
|
2764
|
+
edge: 'POST /v1/auth/logout: 503',
|
|
2765
|
+
name: 'Your account (sign-out failed)',
|
|
2766
|
+
screen: <Account {...account()} error={unavailable} />,
|
|
2767
|
+
},
|
|
2768
|
+
],
|
|
2769
|
+
title: 'Sign-out failed',
|
|
2770
|
+
},
|
|
2771
|
+
],
|
|
2772
|
+
title: 'Request failures',
|
|
2773
|
+
},
|
|
2774
|
+
]
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
namespace styles {
|
|
2778
|
+
export const app = style({ display: 'grid', gap: 3 })
|
|
2779
|
+
|
|
2780
|
+
export const appAddress = style({
|
|
2781
|
+
color: 'gray.900',
|
|
2782
|
+
margin: 0,
|
|
2783
|
+
overflowWrap: 'anywhere',
|
|
2784
|
+
typography: 'copy.13.mono',
|
|
2785
|
+
})
|
|
2786
|
+
|
|
2787
|
+
export const appButton = variants({
|
|
2788
|
+
base: {
|
|
2789
|
+
alignItems: 'center',
|
|
2790
|
+
border: 'none',
|
|
2791
|
+
borderRadius: 'full',
|
|
2792
|
+
cursor: 'pointer',
|
|
2793
|
+
display: 'flex',
|
|
2794
|
+
gap: 3,
|
|
2795
|
+
height: 12,
|
|
2796
|
+
justifyContent: 'center',
|
|
2797
|
+
paddingInline: 6,
|
|
2798
|
+
typography: 'button.16',
|
|
2799
|
+
width: 'full',
|
|
2800
|
+
},
|
|
2801
|
+
variants: {
|
|
2802
|
+
tone: {
|
|
2803
|
+
brand: { backgroundColor: 'red.700', color: 'white' },
|
|
2804
|
+
google: { backgroundColor: 'blue.700', color: 'white' },
|
|
2805
|
+
quiet: { backgroundColor: 'gray.100', color: 'foreground' },
|
|
2806
|
+
},
|
|
2807
|
+
},
|
|
2808
|
+
})
|
|
2809
|
+
|
|
2810
|
+
export const appDescription = style({
|
|
2811
|
+
color: 'gray.900',
|
|
2812
|
+
typography: 'copy.16',
|
|
2813
|
+
})
|
|
2814
|
+
|
|
2815
|
+
// A rule on each side of the label, as the sample app's divider draws it.
|
|
2816
|
+
export const appDivider = style({
|
|
2817
|
+
alignItems: 'center',
|
|
2818
|
+
color: 'gray.900',
|
|
2819
|
+
display: 'flex',
|
|
2820
|
+
gap: 4,
|
|
2821
|
+
margin: 0,
|
|
2822
|
+
typography: 'copy.14',
|
|
2823
|
+
whiteSpace: 'nowrap',
|
|
2824
|
+
'&::before, &::after': {
|
|
2825
|
+
backgroundColor: 'gray.400',
|
|
2826
|
+
content: '""',
|
|
2827
|
+
flexGrow: 1,
|
|
2828
|
+
height: 'px',
|
|
2829
|
+
},
|
|
2830
|
+
})
|
|
2831
|
+
|
|
2832
|
+
export const appField = style({ display: 'grid', gap: 2 })
|
|
2833
|
+
|
|
2834
|
+
export const appHeader = style({ display: 'grid', gap: 2, marginBottom: 2 })
|
|
2835
|
+
|
|
2836
|
+
export const appInput = style({
|
|
2837
|
+
backgroundColor: 'gray.100',
|
|
2838
|
+
border: 'none',
|
|
2839
|
+
borderRadius: 'md',
|
|
2840
|
+
color: 'foreground',
|
|
2841
|
+
height: 12,
|
|
2842
|
+
paddingInline: 4,
|
|
2843
|
+
typography: 'copy.16',
|
|
2844
|
+
width: 'full',
|
|
2845
|
+
})
|
|
2846
|
+
|
|
2847
|
+
export const appLabel = style({ typography: 'label.16' })
|
|
2848
|
+
|
|
2849
|
+
export const appTitle = style({ typography: 'heading.32' })
|
|
2850
|
+
|
|
2851
|
+
export const arrow = style({
|
|
2852
|
+
alignItems: 'center',
|
|
2853
|
+
display: 'grid',
|
|
2854
|
+
flexShrink: 0,
|
|
2855
|
+
gap: 2,
|
|
2856
|
+
justifyItems: 'center',
|
|
2857
|
+
paddingInline: 3,
|
|
2858
|
+
width: 48,
|
|
2859
|
+
})
|
|
2860
|
+
|
|
2861
|
+
export const artboard = style({ display: 'grid', gap: 2, margin: 0 })
|
|
2862
|
+
|
|
2863
|
+
export const canvas = style({
|
|
2864
|
+
backgroundColor: 'gray.100',
|
|
2865
|
+
display: 'grid',
|
|
2866
|
+
gap: 16,
|
|
2867
|
+
minHeight: 'screen',
|
|
2868
|
+
// The canvas grows to its widest flow, so the page scrolls to pan.
|
|
2869
|
+
minWidth: 'full',
|
|
2870
|
+
padding: 4,
|
|
2871
|
+
width: 'max',
|
|
2872
|
+
'@media (min-width: 48rem)': { padding: 12 },
|
|
2873
|
+
})
|
|
2874
|
+
|
|
2875
|
+
export const code = style({ typography: 'copy.13.mono' })
|
|
2876
|
+
|
|
2877
|
+
export const detail = style({
|
|
2878
|
+
color: 'gray.900',
|
|
2879
|
+
overflowWrap: 'anywhere',
|
|
2880
|
+
typography: 'copy.13.mono',
|
|
2881
|
+
})
|
|
2882
|
+
|
|
2883
|
+
export const edge = style({
|
|
2884
|
+
color: 'gray.900',
|
|
2885
|
+
overflowWrap: 'anywhere',
|
|
2886
|
+
textAlign: 'center',
|
|
2887
|
+
typography: 'copy.13.mono',
|
|
2888
|
+
})
|
|
2889
|
+
|
|
2890
|
+
export const external = variants({
|
|
2891
|
+
base: {
|
|
2892
|
+
backgroundColor: 'background.surface',
|
|
2893
|
+
border: '1px dashed',
|
|
2894
|
+
borderColor: 'gray.500',
|
|
2895
|
+
borderRadius: 'lg',
|
|
2896
|
+
display: 'grid',
|
|
2897
|
+
flexShrink: 0,
|
|
2898
|
+
gap: 1,
|
|
2899
|
+
padding: 4,
|
|
2900
|
+
width: 56,
|
|
2901
|
+
},
|
|
2902
|
+
variants: {
|
|
2903
|
+
kind: {
|
|
2904
|
+
Entry: { borderColor: 'blue.700', borderStyle: 'solid' },
|
|
2905
|
+
Exit: { borderColor: 'green.700', borderStyle: 'solid' },
|
|
2906
|
+
External: {},
|
|
2907
|
+
},
|
|
2908
|
+
},
|
|
2909
|
+
})
|
|
2910
|
+
|
|
2911
|
+
export const externalName = style({ typography: 'label.14' })
|
|
2912
|
+
|
|
2913
|
+
export const flow = style({ display: 'grid', gap: 4 })
|
|
2914
|
+
|
|
2915
|
+
export const flowDescription = style({
|
|
2916
|
+
color: 'gray.900',
|
|
2917
|
+
typography: 'copy.14',
|
|
2918
|
+
})
|
|
2919
|
+
|
|
2920
|
+
// The flow's text stays at the canvas's left edge while the row scrolls.
|
|
2921
|
+
export const flowHeader = style({
|
|
2922
|
+
display: 'grid',
|
|
2923
|
+
gap: 1,
|
|
2924
|
+
left: 4,
|
|
2925
|
+
maxWidth: 'readable',
|
|
2926
|
+
position: 'sticky',
|
|
2927
|
+
'@media (min-width: 48rem)': { left: 12 },
|
|
2928
|
+
})
|
|
2929
|
+
|
|
2930
|
+
export const flowTitle = style({ typography: 'heading.16' })
|
|
2931
|
+
|
|
2932
|
+
export const frame = style({
|
|
2933
|
+
backgroundColor: 'background.primary',
|
|
2934
|
+
border: '1px solid',
|
|
2935
|
+
borderColor: 'grayAlpha.400',
|
|
2936
|
+
borderRadius: 'lg',
|
|
2937
|
+
boxShadow: 'md',
|
|
2938
|
+
paddingBlock: 10,
|
|
2939
|
+
paddingInline: 6,
|
|
2940
|
+
width: 96,
|
|
2941
|
+
})
|
|
2942
|
+
|
|
2943
|
+
export const frameLabel = style({ color: 'gray.900', typography: 'label.12' })
|
|
2944
|
+
|
|
2945
|
+
export const head = style({
|
|
2946
|
+
color: 'gray.600',
|
|
2947
|
+
flexShrink: 0,
|
|
2948
|
+
height: 4,
|
|
2949
|
+
width: 4,
|
|
2950
|
+
})
|
|
2951
|
+
|
|
2952
|
+
export const intro = style({
|
|
2953
|
+
display: 'grid',
|
|
2954
|
+
gap: 2,
|
|
2955
|
+
left: 4,
|
|
2956
|
+
maxWidth: 'readable',
|
|
2957
|
+
position: 'sticky',
|
|
2958
|
+
'@media (min-width: 48rem)': { left: 12 },
|
|
2959
|
+
})
|
|
2960
|
+
|
|
2961
|
+
export const kind = style({
|
|
2962
|
+
color: 'gray.900',
|
|
2963
|
+
textTransform: 'uppercase',
|
|
2964
|
+
typography: 'label.12',
|
|
2965
|
+
})
|
|
2966
|
+
|
|
2967
|
+
export const lede = style({ color: 'gray.900', typography: 'copy.16' })
|
|
2968
|
+
|
|
2969
|
+
export const line = style({
|
|
2970
|
+
backgroundColor: 'gray.600',
|
|
2971
|
+
flexGrow: 1,
|
|
2972
|
+
height: 'px',
|
|
2973
|
+
})
|
|
2974
|
+
|
|
2975
|
+
export const row = style({
|
|
2976
|
+
alignItems: 'center',
|
|
2977
|
+
display: 'flex',
|
|
2978
|
+
listStyle: 'none',
|
|
2979
|
+
margin: 0,
|
|
2980
|
+
padding: 0,
|
|
2981
|
+
})
|
|
2982
|
+
|
|
2983
|
+
export const section = style({ display: 'grid', gap: 10 })
|
|
2984
|
+
|
|
2985
|
+
export const sectionTitle = style({
|
|
2986
|
+
borderBottom: '1px solid',
|
|
2987
|
+
borderBottomColor: 'grayAlpha.400',
|
|
2988
|
+
paddingBottom: 2,
|
|
2989
|
+
typography: 'heading.20',
|
|
2990
|
+
})
|
|
2991
|
+
|
|
2992
|
+
export const shaft = style({
|
|
2993
|
+
alignItems: 'center',
|
|
2994
|
+
display: 'flex',
|
|
2995
|
+
width: 'full',
|
|
2996
|
+
})
|
|
2997
|
+
|
|
2998
|
+
export const step = style({ alignItems: 'center', display: 'flex' })
|
|
2999
|
+
|
|
3000
|
+
export const title = style({ typography: 'heading.32' })
|
|
3001
|
+
}
|