@tempoxyz/id 0.0.1 → 0.0.2
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/dist/app/server/.vite/manifest.json +65 -65
- package/dist/app/server/assets/Grant-D_tun0mF.js.map +1 -1
- package/dist/app/server/assets/authorize-DXYo-K2n-B-6vqhvc.js +56 -0
- package/dist/app/server/assets/authorize-DXYo-K2n-B-6vqhvc.js.map +1 -0
- package/dist/app/server/assets/core-FSS9-r35.js +56 -0
- package/dist/app/server/assets/core-FSS9-r35.js.map +1 -0
- package/dist/app/server/assets/default-u_dwuiYb-Bby14ucv.js +2 -0
- package/dist/app/server/assets/{default-u_dwuiYb-DOrMMcg9.js.map → default-u_dwuiYb-Bby14ucv.js.map} +1 -1
- package/dist/app/server/assets/{dist-CoH0hD1s.js → dist-B9KiBJYw.js} +2 -2
- package/dist/app/server/assets/{dist-CoH0hD1s.js.map → dist-B9KiBJYw.js.map} +1 -1
- package/dist/app/server/assets/{dist-DcICI3fd.js → dist-CU1NXSiz.js} +2 -2
- package/dist/app/server/assets/{dist-DcICI3fd.js.map → dist-CU1NXSiz.js.map} +1 -1
- package/dist/app/server/assets/dist-CetH4JSY.js +1 -0
- package/dist/app/server/assets/{effect-QlVUlMFu-mXPkuQTb.js → effect-QlVUlMFu-Cm-eTtaO.js} +2 -2
- package/dist/app/server/assets/{effect-QlVUlMFu-mXPkuQTb.js.map → effect-QlVUlMFu-Cm-eTtaO.js.map} +1 -1
- package/dist/app/server/assets/factory-g3i_Ffi5.js +5 -0
- package/dist/app/server/assets/{factory-dvoOlKer.js.map → factory-g3i_Ffi5.js.map} +1 -1
- package/dist/app/server/assets/get-id-field-BpOAoqqI.js +6 -0
- package/dist/app/server/assets/get-id-field-BpOAoqqI.js.map +1 -0
- package/dist/app/server/assets/{index-CLddUTqr-CC33CMvS.js → index-CLddUTqr-CmgkbWi9.js} +2 -2
- package/dist/app/server/assets/{index-CLddUTqr-CC33CMvS.js.map → index-CLddUTqr-CmgkbWi9.js.map} +1 -1
- package/dist/app/server/assets/{index-DZEfthgZ-Ec2MMQ_e.js → index-DZEfthgZ-BnPmps9O.js} +2 -2
- package/dist/app/server/assets/{index-DZEfthgZ-Ec2MMQ_e.js.map → index-DZEfthgZ-BnPmps9O.js.map} +1 -1
- package/dist/app/server/assets/kysely-adapter-B8VoPArC.js +1 -0
- package/dist/app/server/assets/{schema-check-oLqvHR8y.js → schema-check-Bgwl1v0_.js} +2 -2
- package/dist/app/server/assets/{schema-check-oLqvHR8y.js.map → schema-check-Bgwl1v0_.js.map} +1 -1
- package/dist/app/server/assets/{sury-CWZTCd75-CDaOS2oM.js → sury-CWZTCd75-Bd_PzqCV.js} +2 -2
- package/dist/app/server/assets/{sury-CWZTCd75-CDaOS2oM.js.map → sury-CWZTCd75-Bd_PzqCV.js.map} +1 -1
- package/dist/app/server/assets/{valibot--1zFm7rT-CIKoH3MS.js → valibot--1zFm7rT-Dd2aEhWG.js} +2 -2
- package/dist/app/server/assets/{valibot--1zFm7rT-CIKoH3MS.js.map → valibot--1zFm7rT-Dd2aEhWG.js.map} +1 -1
- package/dist/app/server/assets/{valibot-D_HTw1Gn-BdPyLkXt.js → valibot-D_HTw1Gn-C1l3va6F.js} +2 -2
- package/dist/app/server/assets/{valibot-D_HTw1Gn-BdPyLkXt.js.map → valibot-D_HTw1Gn-C1l3va6F.js.map} +1 -1
- package/dist/app/server/assets/{zod-Bwrt9trS-BezI5Gi1.js → zod-Bwrt9trS-Dry2w0in.js} +2 -2
- package/dist/app/server/assets/{zod-Bwrt9trS-BezI5Gi1.js.map → zod-Bwrt9trS-Dry2w0in.js.map} +1 -1
- package/dist/app/server/assets/{zod-DSgpEGAE-DZ7M_Q33.js → zod-DSgpEGAE-BZMP86UA.js} +2 -2
- package/dist/app/server/assets/{zod-DSgpEGAE-DZ7M_Q33.js.map → zod-DSgpEGAE-BZMP86UA.js.map} +1 -1
- package/dist/app/server/index.js +35 -35
- package/dist/app/server/index.js.map +1 -1
- package/dist/client/Auth.d.ts +1 -1
- package/dist/client/Auth.d.ts.map +1 -1
- package/dist/client/Auth.js.map +1 -1
- package/dist/client/Decorator.d.ts +3 -1
- package/dist/client/Decorator.d.ts.map +1 -1
- package/dist/client/Decorator.js +1 -0
- package/dist/client/Decorator.js.map +1 -1
- package/dist/client/actions/passkey.d.ts +63 -2
- package/dist/client/actions/passkey.d.ts.map +1 -1
- package/dist/client/actions/passkey.js +121 -3
- package/dist/client/actions/passkey.js.map +1 -1
- package/dist/internal/Apps.d.ts +14 -0
- package/dist/internal/Apps.d.ts.map +1 -1
- package/dist/internal/Apps.js +14 -2
- package/dist/internal/Apps.js.map +1 -1
- package/dist/internal/Auth.d.ts +7 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +2 -1
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Authorize.d.ts.map +1 -1
- package/dist/internal/Authorize.js +13 -13
- package/dist/internal/Authorize.js.map +1 -1
- package/dist/internal/ErrorResponse.d.ts +5 -0
- package/dist/internal/ErrorResponse.d.ts.map +1 -1
- package/dist/internal/ErrorResponse.js +8 -0
- package/dist/internal/ErrorResponse.js.map +1 -1
- package/dist/internal/KeyAuthorization.d.ts.map +1 -1
- package/dist/internal/KeyAuthorization.js +28 -28
- package/dist/internal/KeyAuthorization.js.map +1 -1
- package/dist/internal/Owner.d.ts +16 -0
- package/dist/internal/Owner.d.ts.map +1 -1
- package/dist/internal/Owner.js.map +1 -1
- package/dist/internal/Passkey.d.ts +66 -4
- package/dist/internal/Passkey.d.ts.map +1 -1
- package/dist/internal/Passkey.js +66 -30
- package/dist/internal/Passkey.js.map +1 -1
- package/dist/internal/Privy.d.ts +27 -0
- package/dist/internal/Privy.d.ts.map +1 -1
- package/dist/internal/Privy.js +82 -0
- package/dist/internal/Privy.js.map +1 -1
- package/dist/internal/TempoAccount.d.ts +57 -0
- package/dist/internal/TempoAccount.d.ts.map +1 -1
- package/dist/internal/TempoAccount.js +118 -5
- package/dist/internal/TempoAccount.js.map +1 -1
- package/dist/internal/owners/Privy.d.ts.map +1 -1
- package/dist/internal/owners/Privy.js +11 -2
- package/dist/internal/owners/Privy.js.map +1 -1
- package/package.json +1 -1
- package/src/api/account.ts +140 -103
- package/src/api/authenticate.ts +48 -51
- package/src/api/authorize.ts +35 -36
- package/src/api/device.ts +20 -23
- package/src/api/health.ts +1 -2
- package/src/api/lib/AccountStep.ts +75 -0
- package/src/api/lib/Auth.ts +6 -0
- package/src/api/lib/Cors.ts +3 -0
- package/src/api/lib/KeySigner.ts +1 -1
- package/src/api/oauth.ts +23 -22
- package/src/client/Auth.ts +1 -0
- package/src/client/Decorator.ts +6 -1
- package/src/client/actions/passkey.ts +197 -3
- package/src/internal/Apps.ts +26 -2
- package/src/internal/Auth.ts +4 -0
- package/src/internal/Authorize.ts +13 -15
- package/src/internal/ErrorResponse.ts +10 -0
- package/src/internal/KeyAuthorization.ts +28 -31
- package/src/internal/OpenApi.ts +35 -0
- package/src/internal/Owner.ts +16 -0
- package/src/internal/Passkey.ts +99 -34
- package/src/internal/Privy.ts +128 -0
- package/src/internal/TempoAccount.ts +221 -7
- package/src/internal/owners/Privy.ts +19 -6
- package/dist/app/server/assets/authorize-DXYo-K2n-rmQQ42x1.js +0 -56
- package/dist/app/server/assets/authorize-DXYo-K2n-rmQQ42x1.js.map +0 -1
- package/dist/app/server/assets/core-BBrZXfrB.js +0 -56
- package/dist/app/server/assets/core-BBrZXfrB.js.map +0 -1
- package/dist/app/server/assets/default-u_dwuiYb-DOrMMcg9.js +0 -2
- package/dist/app/server/assets/dist-CR6FW-yS.js +0 -1
- package/dist/app/server/assets/factory-dvoOlKer.js +0 -5
- package/dist/app/server/assets/get-id-field-OwLpfjj2.js +0 -6
- package/dist/app/server/assets/get-id-field-OwLpfjj2.js.map +0 -1
- package/dist/app/server/assets/kysely-adapter-B8tQwTcu.js +0 -1
package/src/api/oauth.ts
CHANGED
|
@@ -75,7 +75,7 @@ namespace schema {
|
|
|
75
75
|
.optional()
|
|
76
76
|
.meta({
|
|
77
77
|
description:
|
|
78
|
-
"A message, such as a sign-in challenge
|
|
78
|
+
"A message, such as a sign-in challenge, that the key authorization's `witness` binds (TIP-1053). Requires `key_authorization` and the `tempo-id:sign` scope.",
|
|
79
79
|
examples: ['Sign in to app.example.com\nNonce: 7f3a9c2e5b1d4f60'],
|
|
80
80
|
}),
|
|
81
81
|
client_id: z.string().meta({
|
|
@@ -95,7 +95,7 @@ namespace schema {
|
|
|
95
95
|
.optional()
|
|
96
96
|
.meta({
|
|
97
97
|
description:
|
|
98
|
-
"A
|
|
98
|
+
"A `KeyAuthorizationRequest` for the app's own key, as base64url JSON. After approval, `/oauth2/token` returns the signed key with the tokens.",
|
|
99
99
|
examples: [
|
|
100
100
|
'eyJhZGRyZXNzIjoiMHg4MWMwZjIyYmQxNzNjMTE1ZDE4ZWVlYTBmNmEzOGU5MzVlY2Y1MmZiIn0',
|
|
101
101
|
],
|
|
@@ -145,7 +145,7 @@ namespace schema {
|
|
|
145
145
|
}),
|
|
146
146
|
url: z.string().meta({
|
|
147
147
|
description:
|
|
148
|
-
'
|
|
148
|
+
'The URL to send the browser to next: the sign-in page, the approval page, or the redirect URI with a code.',
|
|
149
149
|
examples: [
|
|
150
150
|
'https://app.example/callback?code=SplxlOBeZQQYbYS6WxSbIA',
|
|
151
151
|
],
|
|
@@ -310,7 +310,7 @@ namespace schema {
|
|
|
310
310
|
}),
|
|
311
311
|
key_authorization: KeyAuthorization.schema.rpc.optional().meta({
|
|
312
312
|
description:
|
|
313
|
-
'The key authorization
|
|
313
|
+
'The signed key authorization, on the first redemption of a code whose request carried `key_authorization`. Absent otherwise.',
|
|
314
314
|
}),
|
|
315
315
|
refresh_token: z
|
|
316
316
|
.string()
|
|
@@ -404,7 +404,7 @@ namespace schema {
|
|
|
404
404
|
}),
|
|
405
405
|
key_authorization: authorize.query.shape.key_authorization.meta({
|
|
406
406
|
description:
|
|
407
|
-
"A
|
|
407
|
+
"A `KeyAuthorizationRequest` for the app's own key, as base64url JSON, from a public first-party app only. After approval, `/oauth2/token` returns the signed key with the tokens.",
|
|
408
408
|
examples: [
|
|
409
409
|
'eyJhZGRyZXNzIjoiMHg4MWMwZjIyYmQxNzNjMTE1ZDE4ZWVlYTBmNmEzOGU5MzVlY2Y1MmZiIn0',
|
|
410
410
|
],
|
|
@@ -585,7 +585,7 @@ namespace schema {
|
|
|
585
585
|
.optional()
|
|
586
586
|
.meta({
|
|
587
587
|
description:
|
|
588
|
-
'The sealed parameters of a form POST that
|
|
588
|
+
'The sealed parameters of a form POST that had no session cookie, from its 303 redirect.',
|
|
589
589
|
examples: ['a3f1c2…'],
|
|
590
590
|
}),
|
|
591
591
|
id_token_hint: z
|
|
@@ -593,7 +593,7 @@ namespace schema {
|
|
|
593
593
|
.optional()
|
|
594
594
|
.meta({
|
|
595
595
|
description:
|
|
596
|
-
'An ID token that Tempo ID issued to the app,
|
|
596
|
+
'An ID token that Tempo ID issued to the app, which names the session to end. It may be expired, and is required without `continuation`.',
|
|
597
597
|
examples: ['eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.e30.c2lnbmF0dXJl'],
|
|
598
598
|
}),
|
|
599
599
|
post_logout_redirect_uri: z
|
|
@@ -617,8 +617,7 @@ namespace schema {
|
|
|
617
617
|
|
|
618
618
|
export const response = z.object({
|
|
619
619
|
signed_out: z.literal(true).meta({
|
|
620
|
-
description:
|
|
621
|
-
'The browser holds no session that the hint names, since it ended or never held one.',
|
|
620
|
+
description: 'The browser has no session that the hint names.',
|
|
622
621
|
}),
|
|
623
622
|
})
|
|
624
623
|
}
|
|
@@ -703,7 +702,7 @@ const introspectRoute = {
|
|
|
703
702
|
/** The description and responses of `GET` and `POST /oauth2/end-session` in the OpenAPI document. */
|
|
704
703
|
const endSessionRoute = {
|
|
705
704
|
description:
|
|
706
|
-
"Signs the browser out of the session that
|
|
705
|
+
"Signs the browser out of the session that `id_token_hint` names, revokes its tokens, and redirects to the app's post-logout redirect URI (OpenID Connect RP-Initiated Logout).",
|
|
707
706
|
responses: {
|
|
708
707
|
302: {
|
|
709
708
|
description:
|
|
@@ -713,7 +712,7 @@ const endSessionRoute = {
|
|
|
713
712
|
errors: [...OpenApi.validate.errors, ...Logout.check.errors],
|
|
714
713
|
success: {
|
|
715
714
|
description:
|
|
716
|
-
'The browser is signed out
|
|
715
|
+
'The browser is signed out, for a request without `post_logout_redirect_uri`.',
|
|
717
716
|
schema: schema.endSession.response,
|
|
718
717
|
},
|
|
719
718
|
}),
|
|
@@ -824,7 +823,7 @@ export const oauth = new Hono<Env>()
|
|
|
824
823
|
.get(
|
|
825
824
|
'/.well-known/openid-configuration',
|
|
826
825
|
OpenApi.describeRoute({
|
|
827
|
-
description: "
|
|
826
|
+
description: "Returns the issuer's OpenID Connect discovery document.",
|
|
828
827
|
responses: OpenApi.responses({
|
|
829
828
|
success: {
|
|
830
829
|
description: 'The discovery document.',
|
|
@@ -838,7 +837,8 @@ export const oauth = new Hono<Env>()
|
|
|
838
837
|
.get(
|
|
839
838
|
'/.well-known/oauth-authorization-server',
|
|
840
839
|
OpenApi.describeRoute({
|
|
841
|
-
description:
|
|
840
|
+
description:
|
|
841
|
+
"Returns the issuer's authorization server metadata (RFC 8414).",
|
|
842
842
|
responses: OpenApi.responses({
|
|
843
843
|
success: {
|
|
844
844
|
description: 'The metadata.',
|
|
@@ -853,7 +853,7 @@ export const oauth = new Hono<Env>()
|
|
|
853
853
|
'/oauth2/authorize',
|
|
854
854
|
OpenApi.describeRoute({
|
|
855
855
|
description:
|
|
856
|
-
'
|
|
856
|
+
'Redirect authorization, step 1 of 2: starts the authorization in the browser. Tempo ID shows the sign-in, account, and approval pages as needed, then redirects to `redirect_uri` with a code.',
|
|
857
857
|
responses: {
|
|
858
858
|
200: {
|
|
859
859
|
content: {
|
|
@@ -863,7 +863,7 @@ export const oauth = new Hono<Env>()
|
|
|
863
863
|
'text/html': { schema: { type: 'string' } },
|
|
864
864
|
},
|
|
865
865
|
description:
|
|
866
|
-
|
|
866
|
+
'The next step as JSON, for a request from a script. A browser navigation receives the page of the next step.',
|
|
867
867
|
},
|
|
868
868
|
302: {
|
|
869
869
|
description:
|
|
@@ -904,7 +904,7 @@ export const oauth = new Hono<Env>()
|
|
|
904
904
|
'/oauth2/token',
|
|
905
905
|
OpenApi.describeRoute({
|
|
906
906
|
description:
|
|
907
|
-
'
|
|
907
|
+
'Exchanges an authorization code, refresh token, device code, or client credentials for tokens. It is the last step of the redirect authorization and of the device grant.',
|
|
908
908
|
responses: {
|
|
909
909
|
200: {
|
|
910
910
|
content: {
|
|
@@ -995,7 +995,7 @@ export const oauth = new Hono<Env>()
|
|
|
995
995
|
'/device/code',
|
|
996
996
|
OpenApi.describeRoute({
|
|
997
997
|
description:
|
|
998
|
-
'
|
|
998
|
+
'Device grant, step 1 of 4: returns a device code and a user code (RFC 8628). The user enters the user code on the device page, and the device polls `/oauth2/token`.',
|
|
999
999
|
responses: {
|
|
1000
1000
|
200: {
|
|
1001
1001
|
content: {
|
|
@@ -1125,7 +1125,7 @@ export const oauth = new Hono<Env>()
|
|
|
1125
1125
|
'/oauth2/introspect',
|
|
1126
1126
|
OpenApi.describeRoute({
|
|
1127
1127
|
description:
|
|
1128
|
-
'
|
|
1128
|
+
'Returns whether a token is active, and its claims (RFC 7662). The app authenticates with its client credentials.',
|
|
1129
1129
|
...introspectRoute,
|
|
1130
1130
|
}),
|
|
1131
1131
|
OpenApi.input('form', schema.introspect.form),
|
|
@@ -1135,7 +1135,7 @@ export const oauth = new Hono<Env>()
|
|
|
1135
1135
|
'/oauth2/revoke',
|
|
1136
1136
|
OpenApi.describeRoute({
|
|
1137
1137
|
description:
|
|
1138
|
-
'Revokes an access or refresh token (RFC 7009). The app authenticates with its client credentials
|
|
1138
|
+
'Revokes an access or refresh token (RFC 7009). The app authenticates with its client credentials.',
|
|
1139
1139
|
responses: {
|
|
1140
1140
|
200: {
|
|
1141
1141
|
content: {
|
|
@@ -1160,7 +1160,8 @@ export const oauth = new Hono<Env>()
|
|
|
1160
1160
|
.get(
|
|
1161
1161
|
'/oauth2/userinfo',
|
|
1162
1162
|
OpenApi.describeRoute({
|
|
1163
|
-
description:
|
|
1163
|
+
description:
|
|
1164
|
+
"Returns the claims of the access token's user (OIDC Core §5.3).",
|
|
1164
1165
|
responses: {
|
|
1165
1166
|
200: {
|
|
1166
1167
|
content: {
|
|
@@ -1192,7 +1193,7 @@ export const oauth = new Hono<Env>()
|
|
|
1192
1193
|
...OpenApi.errors(BodyLimit.errors),
|
|
1193
1194
|
303: {
|
|
1194
1195
|
description:
|
|
1195
|
-
'
|
|
1196
|
+
'Redirects a form POST without the session cookie to `GET /oauth2/end-session`, which the browser sends with the cookie.',
|
|
1196
1197
|
},
|
|
1197
1198
|
},
|
|
1198
1199
|
}),
|
|
@@ -1203,7 +1204,7 @@ export const oauth = new Hono<Env>()
|
|
|
1203
1204
|
'/oauth2/jwks',
|
|
1204
1205
|
OpenApi.describeRoute({
|
|
1205
1206
|
description:
|
|
1206
|
-
"
|
|
1207
|
+
"Returns the issuer's signing keys. A verifier may cache the response for 60 seconds.",
|
|
1207
1208
|
responses: OpenApi.responses({
|
|
1208
1209
|
success: {
|
|
1209
1210
|
description: 'The JSON Web Key Set.',
|
package/src/client/Auth.ts
CHANGED
|
@@ -158,6 +158,7 @@ export declare namespace RequestError {
|
|
|
158
158
|
| 'passkey_limit'
|
|
159
159
|
| 'passkey_link_expired'
|
|
160
160
|
| 'passkey_not_permitted'
|
|
161
|
+
| 'passkey_register_not_permitted'
|
|
161
162
|
| 'post_logout_redirect_not_permitted'
|
|
162
163
|
| 'rate_limited'
|
|
163
164
|
| 'reauthentication_required'
|
package/src/client/Decorator.ts
CHANGED
|
@@ -228,6 +228,7 @@ export function tempoId(options: tempoId.Options) {
|
|
|
228
228
|
passkeySignIn(() => prepared.signIn(signInOptions)),
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
|
+
register: (options = {}) => Actions.passkey.register(auth, options),
|
|
231
232
|
signIn: (options = {}) =>
|
|
232
233
|
passkeySignIn(() => Actions.passkey.signIn(auth, options)),
|
|
233
234
|
},
|
|
@@ -360,7 +361,7 @@ export declare namespace tempoId {
|
|
|
360
361
|
/** `Actions.oauth.userInfo`. */
|
|
361
362
|
userInfo: (options: Actions.oauth.userInfo.Options) => Promise<User>
|
|
362
363
|
}
|
|
363
|
-
/** Actions for sign-in with a passkey. */
|
|
364
|
+
/** Actions for sign-in with a passkey, and the registration of one. */
|
|
364
365
|
passkey: {
|
|
365
366
|
/** `Actions.passkey.available`. */
|
|
366
367
|
available: () => Promise<boolean>
|
|
@@ -374,6 +375,10 @@ export declare namespace tempoId {
|
|
|
374
375
|
>(
|
|
375
376
|
options?: Actions.passkey.prepare.Options<id, key>,
|
|
376
377
|
) => Actions.passkey.prepare.ReturnType<id, key>
|
|
378
|
+
/** `Actions.passkey.register`, which leaves the client's account unchanged. */
|
|
379
|
+
register: (
|
|
380
|
+
options?: Actions.passkey.register.Options,
|
|
381
|
+
) => Promise<Actions.passkey.register.ReturnType>
|
|
377
382
|
/**
|
|
378
383
|
* `Actions.passkey.signIn`, which sets the client's account as `auth.email.signIn` does,
|
|
379
384
|
* unless it returns a step.
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Signs the user in with a passkey from the app's own sign-in form, and authorizes the app's key.
|
|
3
3
|
* The handle of {@link prepare} requests the sign-in's options before the user acts, so its sign-in
|
|
4
|
-
* starts the ceremony without a request.
|
|
4
|
+
* starts the ceremony without a request. {@link register} adds a passkey for the signed-in user
|
|
5
|
+
* from the app's page.
|
|
5
6
|
* @module
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Authentication,
|
|
10
|
+
Credential,
|
|
11
|
+
Registration,
|
|
12
|
+
type Types,
|
|
13
|
+
} from 'ox/webauthn'
|
|
8
14
|
import type { Account } from 'viem/tempo'
|
|
9
|
-
import { type Auth, ChallengeWithoutKeyError } from '../Auth.js'
|
|
15
|
+
import { type Auth, ChallengeWithoutKeyError, RequestError } from '../Auth.js'
|
|
10
16
|
import type { Authorization } from '../Authorization.js'
|
|
17
|
+
import type * as Key from '../Key.js'
|
|
11
18
|
import type * as AccessKey from '../internal/accessKey.js'
|
|
12
19
|
import * as Api from '../internal/api.js'
|
|
13
20
|
import * as Headless from '../internal/headless.js'
|
|
@@ -160,6 +167,143 @@ export declare namespace prepare {
|
|
|
160
167
|
type SignInOptions = Pick<signIn.Options, 'getFn'>
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Registers a new passkey for the signed-in user from a first-party app's page, and returns its
|
|
172
|
+
* credential ID and the address of the Tempo Account that it owns. The passkey belongs to the
|
|
173
|
+
* `default` realm's relying party, which must cover the page and Tempo ID. It runs two ceremonies:
|
|
174
|
+
* the authenticator creates the passkey, then the new passkey signs a proof challenge, so call it
|
|
175
|
+
* from a click.
|
|
176
|
+
*
|
|
177
|
+
* With `key`, the key of a sign-in on a cloud wallet's configurable account, the passkey joins that
|
|
178
|
+
* account as a second owner on the key's chain, through an update that the cloud wallet signs. The
|
|
179
|
+
* key keeps working. On a chain whose accounts are not configurable, `key` sets nothing, and the
|
|
180
|
+
* passkey owns an account of its own. `account` and `chain` name the account and chain instead.
|
|
181
|
+
*
|
|
182
|
+
* Requires a sign-in within the last 10 minutes, or throws `reauthentication_required`. A user who
|
|
183
|
+
* holds 10 passkeys throws `passkey_limit`, and a page that the relying party does not cover throws
|
|
184
|
+
* `passkey_not_permitted`.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* const { authorization } = await Actions.email.signIn(auth, { code, email })
|
|
189
|
+
* const { address, id } = await Actions.passkey.register(auth, {
|
|
190
|
+
* key: authorization.key,
|
|
191
|
+
* })
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export async function register(
|
|
195
|
+
auth: Auth,
|
|
196
|
+
options: register.Options = {},
|
|
197
|
+
): Promise<register.ReturnType> {
|
|
198
|
+
const { createFn, getFn, key } = options
|
|
199
|
+
// The key's chain resolves beside the options request, so it adds no round trip. Only a chain
|
|
200
|
+
// whose accounts are configurable takes an owner, so on any other the passkey owns its own account.
|
|
201
|
+
const joined =
|
|
202
|
+
key && (options.account === undefined || options.chain === undefined)
|
|
203
|
+
? configurableChain(auth, { chainId: key.chainId }).then((name) =>
|
|
204
|
+
name ? { account: key.account, chain: name } : undefined,
|
|
205
|
+
)
|
|
206
|
+
: undefined
|
|
207
|
+
// A registration that fails before the verify request reads no chain, so its failure stays silent.
|
|
208
|
+
joined?.catch(() => {})
|
|
209
|
+
const requested = await Api.json<{
|
|
210
|
+
options: Types.CredentialCreationOptions<true>
|
|
211
|
+
}>(await Api.post(auth, '/v1/account/passkey/options', { json: {} }))
|
|
212
|
+
const deserialized = Registration.deserializeOptions(requested.options)
|
|
213
|
+
const credential = Credential.serialize(
|
|
214
|
+
await Registration.create(
|
|
215
|
+
// ox reads `createFn` beside creation options at runtime, which its types admit only beside flat options.
|
|
216
|
+
createFn ? ({ ...deserialized, createFn } as never) : deserialized,
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
const created = {
|
|
221
|
+
credentialId: credential.id,
|
|
222
|
+
rpId: requested.options.publicKey!.rp.id!,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The attestation proves no private key, so the new passkey signs a proof challenge before
|
|
226
|
+
// Tempo ID stores it. A failure before the proof leaves the passkey unstored, so the
|
|
227
|
+
// authenticator drops it.
|
|
228
|
+
const assertion = await (async () => {
|
|
229
|
+
try {
|
|
230
|
+
const { step } = await Api.json<{
|
|
231
|
+
step: { options: Types.CredentialRequestOptions<true> }
|
|
232
|
+
}>(await Api.post(auth, '/v1/account/passkey', { json: { credential } }))
|
|
233
|
+
const proof = Authentication.deserializeOptions(step.options)
|
|
234
|
+
return Authentication.serializeResponse(
|
|
235
|
+
await Authentication.sign(
|
|
236
|
+
// ox reads `getFn` beside request options at runtime, which its types admit only beside a challenge.
|
|
237
|
+
getFn ? ({ ...proof, getFn } as never) : proof,
|
|
238
|
+
),
|
|
239
|
+
)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
forget(created)
|
|
242
|
+
throw error
|
|
243
|
+
}
|
|
244
|
+
})()
|
|
245
|
+
|
|
246
|
+
// A failed chain lookup comes before the verify request, so Tempo ID stored nothing.
|
|
247
|
+
const target = await joined?.catch((error: unknown) => {
|
|
248
|
+
forget(created)
|
|
249
|
+
throw error
|
|
250
|
+
})
|
|
251
|
+
const account = options.account ?? target?.account
|
|
252
|
+
const chain = options.chain ?? target?.chain
|
|
253
|
+
try {
|
|
254
|
+
const registered = await Api.json<register.ReturnType>(
|
|
255
|
+
await Api.post(auth, '/v1/account/passkey/verify', {
|
|
256
|
+
json: {
|
|
257
|
+
...(account ? { account } : {}),
|
|
258
|
+
assertion,
|
|
259
|
+
...(chain ? { chain } : {}),
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
return { address: registered.address, id: registered.id }
|
|
264
|
+
} catch (error) {
|
|
265
|
+
// Any failure but a refusal can follow a stored passkey, so the authenticator keeps it.
|
|
266
|
+
if (refused(error)) forget(created)
|
|
267
|
+
throw error
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export declare namespace register {
|
|
272
|
+
type Options = {
|
|
273
|
+
/**
|
|
274
|
+
* A configurable Tempo Account of the user's cloud wallet, which the passkey joins as a second
|
|
275
|
+
* owner on `chain`. Defaults to `key.account` on a chain whose accounts are configurable, else
|
|
276
|
+
* none, and the passkey owns an account of its own.
|
|
277
|
+
*/
|
|
278
|
+
account?: `0x${string}` | undefined
|
|
279
|
+
/**
|
|
280
|
+
* The chain whose accounts are configurable, by its name in `GET /v1/chains`, such as
|
|
281
|
+
* `testnet`. It selects where `account` is, and the chain of the address that the call returns.
|
|
282
|
+
* Defaults to the chain of `key` when its accounts are configurable, else the first such chain
|
|
283
|
+
* for `account`, and the passkey's own address otherwise.
|
|
284
|
+
*/
|
|
285
|
+
chain?: string | undefined
|
|
286
|
+
/** Runs the creation ceremony in place of `navigator.credentials.create`. */
|
|
287
|
+
createFn?: Registration.create.Options['createFn'] | undefined
|
|
288
|
+
/** Runs the proof ceremony in place of `navigator.credentials.get`. */
|
|
289
|
+
getFn?: Authentication.sign.Options['getFn'] | undefined
|
|
290
|
+
/**
|
|
291
|
+
* The key of a sign-in on the cloud wallet's account, such as `authorization.key`. On a chain
|
|
292
|
+
* whose accounts are configurable, it sets the default `account` and `chain`. Registration does
|
|
293
|
+
* not use the key otherwise.
|
|
294
|
+
*/
|
|
295
|
+
key?: Pick<Key.Key, 'account' | 'chainId'> | undefined
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The new passkey and the account that it owns. */
|
|
299
|
+
type ReturnType = {
|
|
300
|
+
/** The address of the Tempo Account that the passkey owns: `account`, or its own on `chain`. */
|
|
301
|
+
address: `0x${string}`
|
|
302
|
+
/** The credential ID of the passkey, base64url. */
|
|
303
|
+
id: string
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
163
307
|
/**
|
|
164
308
|
* Signs the user in with a passkey, and authorizes a first-party app with the same assertion. The
|
|
165
309
|
* key must fit the headless ceiling. A user without a Tempo Account receives one.
|
|
@@ -243,6 +387,56 @@ export declare namespace signIn {
|
|
|
243
387
|
}
|
|
244
388
|
}
|
|
245
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Returns the name of the chain with `chainId` in `GET /v1/chains` whose accounts are configurable,
|
|
392
|
+
* or `undefined` when the deployment flags no such chain.
|
|
393
|
+
*/
|
|
394
|
+
async function configurableChain(auth: Auth, options: { chainId: number }) {
|
|
395
|
+
const { chains } = await Api.json<{
|
|
396
|
+
chains: readonly { chainId: number; configurable: boolean; name: string }[]
|
|
397
|
+
}>(await (auth.fetch ?? fetch)(new URL('/v1/chains', auth.url)))
|
|
398
|
+
return chains.find(
|
|
399
|
+
(entry) => entry.chainId === options.chainId && entry.configurable,
|
|
400
|
+
)?.name
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Signals the browser to drop the passkey `credentialId`, which Tempo ID did not store, where the
|
|
405
|
+
* browser supports the signal. The signal is best effort: a passkey that stays signs in nowhere.
|
|
406
|
+
*/
|
|
407
|
+
function forget(options: { credentialId: string; rpId: string }) {
|
|
408
|
+
if (typeof PublicKeyCredential === 'undefined') return
|
|
409
|
+
const signal = (
|
|
410
|
+
PublicKeyCredential as {
|
|
411
|
+
signalUnknownCredential?: (options: {
|
|
412
|
+
credentialId: string
|
|
413
|
+
rpId: string
|
|
414
|
+
}) => Promise<void>
|
|
415
|
+
}
|
|
416
|
+
).signalUnknownCredential
|
|
417
|
+
// The registration's result does not depend on the signal, so its failure is ignored.
|
|
418
|
+
void signal
|
|
419
|
+
?.call(PublicKeyCredential, {
|
|
420
|
+
credentialId: options.credentialId,
|
|
421
|
+
rpId: options.rpId,
|
|
422
|
+
})
|
|
423
|
+
.catch(() => {})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Returns whether Tempo ID refused a request with a 4xx and a `reason`, so it stored nothing. An
|
|
428
|
+
* owner change can return `config_conflict` or `rate_limited` after it stores the passkey.
|
|
429
|
+
*/
|
|
430
|
+
function refused(error: unknown) {
|
|
431
|
+
return (
|
|
432
|
+
error instanceof RequestError &&
|
|
433
|
+
error.reason !== undefined &&
|
|
434
|
+
error.reason !== 'config_conflict' &&
|
|
435
|
+
error.reason !== 'rate_limited' &&
|
|
436
|
+
error.status < 500
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
246
440
|
/** A passkey sign-in's options and the key that they authorize, which one sign-in uses. */
|
|
247
441
|
type Prepared = {
|
|
248
442
|
/** The authorization request that the options carry, which the passkey step repeats. */
|
package/src/internal/Apps.ts
CHANGED
|
@@ -359,7 +359,7 @@ const seeded = new Set<string>()
|
|
|
359
359
|
|
|
360
360
|
/** Returns the Better Auth plugins that serve OAuth apps: the JWT plugin and the OAuth provider. */
|
|
361
361
|
export function plugins(options: plugins.Options) {
|
|
362
|
-
const { accountStep, baseURL, cache, pendingKey } = options
|
|
362
|
+
const { accounts, accountStep, baseURL, cache, pendingKey } = options
|
|
363
363
|
const fetch = options.metadataFetch ?? fetchMetadata()
|
|
364
364
|
return [
|
|
365
365
|
// `iss` is the configured origin, never the request's host. No client reads the session's JWT
|
|
@@ -395,9 +395,22 @@ export function plugins(options: plugins.Options) {
|
|
|
395
395
|
// Every app may request the one resource.
|
|
396
396
|
enforcePerClientResources: false,
|
|
397
397
|
// A third-party app names its metadata document's URL or its origin as its client ID, and
|
|
398
|
-
// registers no row.
|
|
398
|
+
// registers no row. A registered app's access token names the user's accounts in the app's
|
|
399
|
+
// realms, read again on each grant, so a refresh lists an account that the user created since.
|
|
400
|
+
// A third-party app receives none, as `GET /v1/account` lists them only to registered apps.
|
|
399
401
|
extensions: [
|
|
400
402
|
{
|
|
403
|
+
claims: {
|
|
404
|
+
accessToken: async (input) => ({
|
|
405
|
+
tempo_accounts:
|
|
406
|
+
accounts && input.user && !thirdParty(input.client.clientId)
|
|
407
|
+
? await accounts({
|
|
408
|
+
clientId: input.client.clientId,
|
|
409
|
+
userId: input.user.id,
|
|
410
|
+
})
|
|
411
|
+
: [],
|
|
412
|
+
}),
|
|
413
|
+
},
|
|
401
414
|
clientDiscovery: [discovery({ fetch }), originDiscovery({ fetch })],
|
|
402
415
|
},
|
|
403
416
|
],
|
|
@@ -553,6 +566,17 @@ export function plugins(options: plugins.Options) {
|
|
|
553
566
|
|
|
554
567
|
export declare namespace plugins {
|
|
555
568
|
type Options = {
|
|
569
|
+
/**
|
|
570
|
+
* Returns every address of the user's Tempo Accounts in the app's realms, which each access
|
|
571
|
+
* token of a registered app names as `tempo_accounts`. A third-party app's tokens, and every
|
|
572
|
+
* token without this option, name an empty array.
|
|
573
|
+
*/
|
|
574
|
+
accounts?:
|
|
575
|
+
| ((options: {
|
|
576
|
+
clientId: string
|
|
577
|
+
userId: string
|
|
578
|
+
}) => Promise<readonly string[]>)
|
|
579
|
+
| undefined
|
|
556
580
|
/** Returns whether the app's authorization needs `/choose-account`. Absent, none does. */
|
|
557
581
|
accountStep?:
|
|
558
582
|
| ((options: {
|
package/src/internal/Auth.ts
CHANGED
|
@@ -109,6 +109,7 @@ export const sessionCookie = 'tempo-id-session'
|
|
|
109
109
|
/** Returns the Better Auth options for `options`. */
|
|
110
110
|
export function options(options: options.Options) {
|
|
111
111
|
const {
|
|
112
|
+
accounts,
|
|
112
113
|
accountStep,
|
|
113
114
|
baseURL,
|
|
114
115
|
cache,
|
|
@@ -191,6 +192,7 @@ export function options(options: options.Options) {
|
|
|
191
192
|
}),
|
|
192
193
|
Passkey.plugin(),
|
|
193
194
|
...Apps.plugins({
|
|
195
|
+
accounts,
|
|
194
196
|
accountStep,
|
|
195
197
|
baseURL,
|
|
196
198
|
cache,
|
|
@@ -240,6 +242,8 @@ export function options(options: options.Options) {
|
|
|
240
242
|
|
|
241
243
|
export declare namespace options {
|
|
242
244
|
type Options = {
|
|
245
|
+
/** Lists the addresses of the user's accounts that each access token names. */
|
|
246
|
+
accounts?: Apps.plugins.Options['accounts']
|
|
243
247
|
/** Decides whether an authorization needs the account step. */
|
|
244
248
|
accountStep?: Apps.plugins.Options['accountStep']
|
|
245
249
|
/** Tempo ID's origin, such as `https://id.tempo.xyz`. */
|
|
@@ -43,7 +43,7 @@ export namespace schema {
|
|
|
43
43
|
.object({
|
|
44
44
|
access_token: z.string().meta({
|
|
45
45
|
description:
|
|
46
|
-
"The access token, a JWT that
|
|
46
|
+
"The access token, a JWT that APIs verify against Tempo ID's JSON Web Key Set.",
|
|
47
47
|
examples: ['eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.c2lnbmF0dXJl'],
|
|
48
48
|
}),
|
|
49
49
|
expires_in: z.number().meta({
|
|
@@ -55,7 +55,7 @@ export namespace schema {
|
|
|
55
55
|
.optional()
|
|
56
56
|
.meta({
|
|
57
57
|
description:
|
|
58
|
-
'The OIDC ID token, a JWT about the user. Absent
|
|
58
|
+
'The OIDC ID token, a JWT about the user. Absent without the `openid` scope.',
|
|
59
59
|
examples: ['eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJhcHAifQ.c2lnbmF0dXJl'],
|
|
60
60
|
}),
|
|
61
61
|
refresh_token: z
|
|
@@ -63,7 +63,7 @@ export namespace schema {
|
|
|
63
63
|
.optional()
|
|
64
64
|
.meta({
|
|
65
65
|
description:
|
|
66
|
-
'The refresh token,
|
|
66
|
+
'The refresh token, for new tokens at `/oauth2/token`. Absent without the `offline_access` scope.',
|
|
67
67
|
examples: ['yAjhKk7QnRZ3vD9pLm2Xc8Ws5Tf1Ub4N'],
|
|
68
68
|
}),
|
|
69
69
|
token_type: z.string().meta({
|
|
@@ -81,7 +81,7 @@ export namespace schema {
|
|
|
81
81
|
.optional()
|
|
82
82
|
.meta({
|
|
83
83
|
description:
|
|
84
|
-
"A message, such as a sign-in challenge
|
|
84
|
+
"A message, such as a sign-in challenge, that the key authorization's `witness` binds (TIP-1053). Requires `key_authorization` and the `tempo-id:sign` scope.",
|
|
85
85
|
examples: ['Sign in to app.example.com\nNonce: 7f3a9c2e5b1d4f60'],
|
|
86
86
|
})
|
|
87
87
|
|
|
@@ -103,7 +103,7 @@ export namespace schema {
|
|
|
103
103
|
.optional()
|
|
104
104
|
.meta({
|
|
105
105
|
description:
|
|
106
|
-
"
|
|
106
|
+
"An S256 PKCE challenge (RFC 7636). With it, the response holds a code for the app's backend, and without it, tokens.",
|
|
107
107
|
examples: ['E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'],
|
|
108
108
|
}),
|
|
109
109
|
key_authorization: KeyAuthorization.schema.request,
|
|
@@ -115,7 +115,7 @@ export namespace schema {
|
|
|
115
115
|
})
|
|
116
116
|
.meta({
|
|
117
117
|
description:
|
|
118
|
-
'A registered redirect URI of
|
|
118
|
+
'A registered redirect URI of the app, on the origin of the page.',
|
|
119
119
|
examples: ['https://app.example.com/callback'],
|
|
120
120
|
}),
|
|
121
121
|
scope: z
|
|
@@ -132,7 +132,7 @@ export namespace schema {
|
|
|
132
132
|
.optional()
|
|
133
133
|
.meta({
|
|
134
134
|
description:
|
|
135
|
-
"Whether
|
|
135
|
+
"Whether to sign the key before a pending upgrade of a wallet.tempo passkey's account, in place of `upgrade_required`. The key then stops working after the upgrade, unless the upgrade registers it.",
|
|
136
136
|
examples: [true],
|
|
137
137
|
}),
|
|
138
138
|
state: z
|
|
@@ -141,8 +141,7 @@ export namespace schema {
|
|
|
141
141
|
.max(256)
|
|
142
142
|
.optional()
|
|
143
143
|
.meta({
|
|
144
|
-
description:
|
|
145
|
-
'An opaque value that the response returns unchanged, to bind it to the request (RFC 6749 §4.1.1).',
|
|
144
|
+
description: 'An opaque value that the response returns unchanged.',
|
|
146
145
|
examples: ['af0ifjsldkj'],
|
|
147
146
|
}),
|
|
148
147
|
})
|
|
@@ -193,7 +192,7 @@ export namespace schema {
|
|
|
193
192
|
.optional()
|
|
194
193
|
.meta({
|
|
195
194
|
description:
|
|
196
|
-
"The authorization code,
|
|
195
|
+
"The authorization code, if the request sent a `code_challenge`. The app's backend redeems it at `/oauth2/token`.",
|
|
197
196
|
examples: ['Vb7mQ2xLkP9sTzR4'],
|
|
198
197
|
}),
|
|
199
198
|
key_authorization: KeyAuthorization.schema.rpc.optional(),
|
|
@@ -201,21 +200,20 @@ export namespace schema {
|
|
|
201
200
|
.string()
|
|
202
201
|
.optional()
|
|
203
202
|
.meta({
|
|
204
|
-
description:
|
|
205
|
-
"The request's `state`, unchanged. Absent when the request sent none.",
|
|
203
|
+
description: "The request's `state`, unchanged.",
|
|
206
204
|
examples: ['af0ifjsldkj'],
|
|
207
205
|
}),
|
|
208
206
|
tokens: tokens.optional().meta({
|
|
209
|
-
description: 'The tokens,
|
|
207
|
+
description: 'The tokens, if the request sent no `code_challenge`.',
|
|
210
208
|
}),
|
|
211
209
|
upgrade_available: z.literal(true).optional().meta({
|
|
212
210
|
description:
|
|
213
|
-
|
|
211
|
+
'Present if `skip_upgrade` signed the key before a pending upgrade. Send the key to `POST /v1/account/upgrade` to keep it working after the upgrade.',
|
|
214
212
|
}),
|
|
215
213
|
})
|
|
216
214
|
.meta({
|
|
217
215
|
description:
|
|
218
|
-
'An authorization: a code or tokens, with
|
|
216
|
+
'An authorization: a code or tokens, with any signed key authorization.',
|
|
219
217
|
id: 'Authorization',
|
|
220
218
|
})
|
|
221
219
|
}
|
|
@@ -183,6 +183,12 @@ export const reasons = {
|
|
|
183
183
|
error: 'unauthorized_client',
|
|
184
184
|
status: 403,
|
|
185
185
|
},
|
|
186
|
+
passkey_register_not_permitted: {
|
|
187
|
+
description:
|
|
188
|
+
"A passkey registration comes from an app's page, and no app at the page's origin holds the `tempo-id:sign` scope.",
|
|
189
|
+
error: 'unauthorized_client',
|
|
190
|
+
status: 403,
|
|
191
|
+
},
|
|
186
192
|
not_configurable: {
|
|
187
193
|
description:
|
|
188
194
|
'The account is not configurable on the chain, so no owner can join it, or no passkey owns it, so it cannot upgrade.',
|
|
@@ -638,6 +644,10 @@ export namespace schema {
|
|
|
638
644
|
error_description:
|
|
639
645
|
'Passkey not permitted: the app client_01JZ8Q6W3N1R5B4V7C2X9K0M3T holds no passkeys. Sign in with an emailed code.',
|
|
640
646
|
},
|
|
647
|
+
passkey_register_not_permitted: {
|
|
648
|
+
error_description:
|
|
649
|
+
'Passkey registration not permitted: no app at https://app.example.com holds the `tempo-id:sign` scope. Register the passkey on Tempo ID, or grant the app the scope.',
|
|
650
|
+
},
|
|
641
651
|
not_configurable: {
|
|
642
652
|
error_description:
|
|
643
653
|
'Not configurable: 0x8C3b5d0e2A4f6B1c9D7e3F5a0B2c4D6e8F1a3B5c is not a configurable account yet. Upgrade it with its passkey, then add the owner.',
|