@tempoxyz/auth 0.0.0-pr-67.sha-53ccafe → 0.0.0-pr-71.sha-88dea78
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/Actions.d.ts +1 -0
- package/dist/Actions.js +1 -0
- package/dist/Auth.d.ts +1 -1
- package/dist/Decorator.d.ts +11 -1
- package/dist/Decorator.js +55 -24
- package/dist/TempoAccount.d.ts +7 -1
- package/dist/TempoAccount.js +1 -1
- package/dist/actions/email.d.ts +5 -0
- package/dist/actions/email.js +2 -1
- package/dist/actions/passkey.d.ts +53 -0
- package/dist/actions/passkey.js +41 -0
- package/dist/actions/tempoAccount.js +9 -3
- package/package.json +1 -1
package/dist/Actions.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { Tokens } from './Tokens.ts';
|
|
|
12
12
|
export * as device from './actions/device.ts';
|
|
13
13
|
export * as email from './actions/email.ts';
|
|
14
14
|
export * as oauth from './actions/oauth.ts';
|
|
15
|
+
export * as passkey from './actions/passkey.ts';
|
|
15
16
|
export * as provider from './actions/provider.ts';
|
|
16
17
|
export * as redirect from './actions/redirect.ts';
|
|
17
18
|
export * as tempoAccount from './actions/tempoAccount.ts';
|
package/dist/Actions.js
CHANGED
|
@@ -4,6 +4,7 @@ import { request } from './internal/request.js';
|
|
|
4
4
|
export * as device from './actions/device.js';
|
|
5
5
|
export * as email from './actions/email.js';
|
|
6
6
|
export * as oauth from './actions/oauth.js';
|
|
7
|
+
export * as passkey from './actions/passkey.js';
|
|
7
8
|
export * as provider from './actions/provider.js';
|
|
8
9
|
export * as redirect from './actions/redirect.js';
|
|
9
10
|
export * as tempoAccount from './actions/tempoAccount.js';
|
package/dist/Auth.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export declare namespace RequestError {
|
|
|
86
86
|
uri?: string | undefined;
|
|
87
87
|
};
|
|
88
88
|
/** Each refusal's stable name. `docs/errors.md` explains each one. */
|
|
89
|
-
type Reason = 'already_decided' | 'approval_pending' | 'body_too_large' | 'cross_site_request' | 'device_code_expired' | 'duplicate_limit' | 'empty_limits' | 'empty_scopes' | 'expiry_exceeds_maximum' | 'expiry_passed' | 'foreign_account' | 'foreign_origin' | 'invalid_chain' | 'invalid_code' | 'invalid_expiry' | 'invalid_field' | 'invalid_origin' | 'invalid_period' | 'invalid_public_key' | 'limit_exceeds_maximum' | 'malformed_body' | 'missing_code_challenge' | 'missing_key' | 'missing_recipients' | 'polling_too_fast' | 'rate_limited' | 'rejected' | 'request_denied' | 'scope_without_limit' | 'session_required' | 'unexpected' | 'unknown_code' | 'unknown_endpoint' | 'unsupported_call' | 'unsupported_step' | 'unsupported_token' | 'upstream_unavailable' | 'webauthn_rejected';
|
|
89
|
+
type Reason = 'already_decided' | 'approval_pending' | 'body_too_large' | 'cross_site_request' | 'device_code_expired' | 'duplicate_limit' | 'empty_limits' | 'empty_scopes' | 'expiry_exceeds_maximum' | 'expiry_passed' | 'foreign_account' | 'foreign_origin' | 'invalid_chain' | 'invalid_code' | 'invalid_expiry' | 'invalid_field' | 'invalid_origin' | 'invalid_period' | 'invalid_public_key' | 'limit_exceeds_maximum' | 'malformed_body' | 'missing_code_challenge' | 'missing_key' | 'missing_recipients' | 'polling_too_fast' | 'rate_limited' | 'rejected' | 'request_denied' | 'scope_without_limit' | 'session_required' | 'unexpected' | 'unknown_code' | 'unknown_endpoint' | 'unsupported_call' | 'unsupported_step' | 'unsupported_token' | 'upstream_unavailable' | 'webauthn_rejected' | 'webauthn_required';
|
|
90
90
|
}
|
|
91
91
|
/** A client's {@link Account} read before a sign-in set it, or after sign-out emptied it. */
|
|
92
92
|
export declare class SignedOutError extends Error {
|
package/dist/Decorator.d.ts
CHANGED
|
@@ -82,7 +82,10 @@ export declare namespace tempoAuth {
|
|
|
82
82
|
email: {
|
|
83
83
|
/** `Actions.email.sendCode`. */
|
|
84
84
|
sendCode: (options: Actions.email.sendCode.Options) => Promise<void>;
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* `Actions.email.signIn`, which sets the client's account unless it returns a step. A step
|
|
87
|
+
* that another sign-in or a sign-out overtook throws {@link SupersededError}.
|
|
88
|
+
*/
|
|
86
89
|
signIn: <const id extends number = chainId>(options: Actions.email.signIn.Options<id>) => Promise<Actions.email.signIn.ReturnType<id>>;
|
|
87
90
|
};
|
|
88
91
|
/** The instance the members call `Actions` with. */
|
|
@@ -95,6 +98,13 @@ export declare namespace tempoAuth {
|
|
|
95
98
|
/** `Actions.oauth.userInfo`. */
|
|
96
99
|
userInfo: (options: Actions.oauth.userInfo.Options) => Promise<User>;
|
|
97
100
|
};
|
|
101
|
+
passkey: {
|
|
102
|
+
/**
|
|
103
|
+
* `Actions.passkey.signIn`, which sets the client's account unless it returns a step. A step
|
|
104
|
+
* that another sign-in or a sign-out overtook throws {@link SupersededError}.
|
|
105
|
+
*/
|
|
106
|
+
signIn: <const id extends number = chainId>(options?: Actions.passkey.signIn.Options<id>) => Promise<Actions.passkey.signIn.ReturnType<id>>;
|
|
107
|
+
};
|
|
98
108
|
provider: {
|
|
99
109
|
/** `Actions.provider.list`. */
|
|
100
110
|
list: () => Promise<Actions.provider.list.ReturnType>;
|
package/dist/Decorator.js
CHANGED
|
@@ -54,42 +54,58 @@ export function tempoAuth(options) {
|
|
|
54
54
|
// sign-in or sign-out completed after it began, so a stale result never replaces a newer one,
|
|
55
55
|
// and a sign-in still pending at sign-out never signs the client back in.
|
|
56
56
|
let version = 0;
|
|
57
|
+
// The version the latest sign-out produced, while no change has followed it.
|
|
58
|
+
let signedOut;
|
|
57
59
|
/**
|
|
58
|
-
* Begins an operation that changes the client's account
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* Begins an operation that changes the client's account. `apply` applies its result: the
|
|
61
|
+
* account to sign with, or `undefined` to stop signing. `check` accepts a result that changes
|
|
62
|
+
* no account, such as a step. Applying an account, or checking a step, that a newer change
|
|
63
|
+
* overtook throws {@link SupersededError}, so the caller never holds a result the client's
|
|
64
|
+
* newer state contradicts.
|
|
62
65
|
*/
|
|
63
66
|
const begin = () => {
|
|
64
67
|
const started = version;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// A sign-in that finishes after sign-out may have set a new server session, so it ends that
|
|
69
|
+
// session again, unless a newer change already followed the sign-out.
|
|
70
|
+
const superseded = async () => {
|
|
71
|
+
if (signedOut === version)
|
|
72
|
+
await Actions.signOut(auth);
|
|
73
|
+
return new SupersededError();
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
async apply(account) {
|
|
77
|
+
if (version !== started) {
|
|
78
|
+
if (account)
|
|
79
|
+
throw await superseded();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
state.account = account;
|
|
83
|
+
version++;
|
|
84
|
+
},
|
|
85
|
+
async check() {
|
|
86
|
+
if (version !== started)
|
|
87
|
+
throw await superseded();
|
|
88
|
+
},
|
|
73
89
|
};
|
|
74
90
|
};
|
|
75
91
|
return {
|
|
76
92
|
auth: {
|
|
77
93
|
async authorize(options = {}) {
|
|
78
|
-
const
|
|
94
|
+
const change = begin();
|
|
79
95
|
const authorization = await Actions.authorize(auth, options);
|
|
80
96
|
// Without a session, the client stops signing as the user it last authorized.
|
|
81
|
-
apply(authorization?.account);
|
|
97
|
+
await change.apply(authorization?.account);
|
|
82
98
|
return authorization;
|
|
83
99
|
},
|
|
84
100
|
device: {
|
|
85
101
|
async authorize(options = {}) {
|
|
86
|
-
const
|
|
102
|
+
const change = begin();
|
|
87
103
|
const device = await Actions.device.authorize(auth, options);
|
|
88
104
|
return {
|
|
89
105
|
...device,
|
|
90
106
|
async wait() {
|
|
91
107
|
const authorization = await device.wait();
|
|
92
|
-
apply(authorization.account);
|
|
108
|
+
await change.apply(authorization.account);
|
|
93
109
|
return authorization;
|
|
94
110
|
},
|
|
95
111
|
};
|
|
@@ -98,10 +114,12 @@ export function tempoAuth(options) {
|
|
|
98
114
|
email: {
|
|
99
115
|
sendCode: (options) => Actions.email.sendCode(auth, options),
|
|
100
116
|
async signIn(options) {
|
|
101
|
-
const
|
|
117
|
+
const change = begin();
|
|
102
118
|
const result = await Actions.email.signIn(auth, options);
|
|
103
|
-
if (
|
|
104
|
-
|
|
119
|
+
if ('step' in result)
|
|
120
|
+
await change.check();
|
|
121
|
+
else
|
|
122
|
+
await change.apply(result.account);
|
|
105
123
|
return result;
|
|
106
124
|
},
|
|
107
125
|
},
|
|
@@ -111,24 +129,35 @@ export function tempoAuth(options) {
|
|
|
111
129
|
jwks: () => Actions.oauth.jwks(auth),
|
|
112
130
|
userInfo: (options) => Actions.oauth.userInfo(auth, options),
|
|
113
131
|
},
|
|
132
|
+
passkey: {
|
|
133
|
+
async signIn(options = {}) {
|
|
134
|
+
const change = begin();
|
|
135
|
+
const result = await Actions.passkey.signIn(auth, options);
|
|
136
|
+
if ('step' in result)
|
|
137
|
+
await change.check();
|
|
138
|
+
else
|
|
139
|
+
await change.apply(result.account);
|
|
140
|
+
return result;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
114
143
|
provider: {
|
|
115
144
|
list: () => Actions.provider.list(auth),
|
|
116
145
|
signIn: (options) => Actions.provider.signIn(auth, options),
|
|
117
146
|
},
|
|
118
147
|
redirect: {
|
|
119
148
|
async complete(options = {}) {
|
|
120
|
-
const
|
|
149
|
+
const change = begin();
|
|
121
150
|
const authorization = await Actions.redirect.complete(auth, options);
|
|
122
151
|
// `undefined` means the page is not a callback, which says nothing about the session.
|
|
123
152
|
if (authorization)
|
|
124
|
-
apply(authorization.account);
|
|
153
|
+
await change.apply(authorization.account);
|
|
125
154
|
return authorization;
|
|
126
155
|
},
|
|
127
156
|
createUrl: (options = {}) => Actions.redirect.createUrl(auth, options),
|
|
128
157
|
async handleCallback(options) {
|
|
129
|
-
const
|
|
158
|
+
const change = begin();
|
|
130
159
|
const authorization = await Actions.redirect.handleCallback(auth, options);
|
|
131
|
-
apply(authorization.account);
|
|
160
|
+
await change.apply(authorization.account);
|
|
132
161
|
return authorization;
|
|
133
162
|
},
|
|
134
163
|
start: (options = {}) => Actions.redirect.start(auth, options),
|
|
@@ -138,7 +167,8 @@ export function tempoAuth(options) {
|
|
|
138
167
|
// A sign-in stored before `Key.chainId` existed gains it here.
|
|
139
168
|
const key = Key.from(authorization.key);
|
|
140
169
|
const account = Key.toAccount(key, options);
|
|
141
|
-
|
|
170
|
+
state.account = account;
|
|
171
|
+
version++;
|
|
142
172
|
return { ...authorization, account, key };
|
|
143
173
|
},
|
|
144
174
|
async signOut() {
|
|
@@ -146,6 +176,7 @@ export function tempoAuth(options) {
|
|
|
146
176
|
// pending is discarded, since the user asked to leave.
|
|
147
177
|
state.account = undefined;
|
|
148
178
|
version++;
|
|
179
|
+
signedOut = version;
|
|
149
180
|
await Actions.signOut(auth);
|
|
150
181
|
},
|
|
151
182
|
tempoAccount: {
|
package/dist/TempoAccount.d.ts
CHANGED
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
export type TempoAccount = {
|
|
7
7
|
/** The account's address, the same on every Tempo chain. */
|
|
8
8
|
address: `0x${string}`;
|
|
9
|
+
/**
|
|
10
|
+
* Who holds the account's owner key: a Privy wallet, which Tempo Auth signs with (`privy`), or a
|
|
11
|
+
* passkey, which signs only on a Tempo Auth page (`webAuthn`). Treat another value as an owner
|
|
12
|
+
* that may refuse a key authorization.
|
|
13
|
+
*/
|
|
14
|
+
owner: 'privy' | 'webAuthn' | (string & {});
|
|
9
15
|
};
|
|
10
16
|
/**
|
|
11
17
|
* Instantiates a {@link TempoAccount}.
|
|
12
18
|
*
|
|
13
19
|
* @example
|
|
14
20
|
* ```ts
|
|
15
|
-
* const tempoAccount = TempoAccount.from({ address: '0x…' })
|
|
21
|
+
* const tempoAccount = TempoAccount.from({ address: '0x…', owner: 'privy' })
|
|
16
22
|
* ```
|
|
17
23
|
*/
|
|
18
24
|
export declare function from(value: TempoAccount): TempoAccount;
|
package/dist/TempoAccount.js
CHANGED
package/dist/actions/email.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export declare namespace signIn {
|
|
|
44
44
|
code: string;
|
|
45
45
|
/** The address the code was sent to. */
|
|
46
46
|
email: string;
|
|
47
|
+
/**
|
|
48
|
+
* The `link` from `Actions.passkey.signIn`'s email step, which links that wallet.tempo passkey
|
|
49
|
+
* to the user.
|
|
50
|
+
*/
|
|
51
|
+
link?: string | undefined;
|
|
47
52
|
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
48
53
|
permissions?: Permissions<chainId> | undefined;
|
|
49
54
|
};
|
package/dist/actions/email.js
CHANGED
|
@@ -23,13 +23,14 @@ export async function sendCode(auth, options) {
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
export async function signIn(auth, options) {
|
|
26
|
-
const { code, email, permissions } = options;
|
|
26
|
+
const { code, email, link, permissions } = options;
|
|
27
27
|
const key = await AccessKey.resolve(options.accessKey);
|
|
28
28
|
const body = await Api.json(await Api.post(auth, '/v1/auth/code/verify', {
|
|
29
29
|
json: {
|
|
30
30
|
authorize: request(auth, { accessKey: key.accessKey, permissions }),
|
|
31
31
|
code,
|
|
32
32
|
email,
|
|
33
|
+
...(link ? { link } : {}),
|
|
33
34
|
},
|
|
34
35
|
}));
|
|
35
36
|
if ('step' in body)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the user in with a passkey from the app's own sign-in form. The browser offers every passkey
|
|
3
|
+
* it holds for Tempo's domain, so the app needs no email first.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { Authentication } from 'ox/webauthn';
|
|
7
|
+
import type { Account } from 'viem/tempo';
|
|
8
|
+
import type { Auth } from '../Auth.ts';
|
|
9
|
+
import type { Authorization } from '../Authorization.ts';
|
|
10
|
+
import type { Permissions } from '../Permissions.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Signs the user in with a passkey, and authorizes the app in the same call. Returns the email step
|
|
13
|
+
* for a wallet.tempo passkey whose account has no email: the app signs the user in with
|
|
14
|
+
* `Actions.email.signIn`, passing the step's `link`, which links the passkey. Throws a `RequestError` with reason
|
|
15
|
+
* `webauthn_required` when a passkey owns the Tempo Account the key acts for, since a passkey signs a
|
|
16
|
+
* key authorization only on Tempo Auth's pages. `Actions.redirect.start` authorizes it there, where
|
|
17
|
+
* the user is already signed in.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const result = await Actions.passkey.signIn(auth)
|
|
22
|
+
* if ('step' in result) showEmailForm()
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function signIn<const chainId extends number = number>(auth: Auth, options?: signIn.Options<chainId>): Promise<signIn.ReturnType<chainId>>;
|
|
26
|
+
export declare namespace signIn {
|
|
27
|
+
type Options<chainId extends number = number> = {
|
|
28
|
+
/**
|
|
29
|
+
* The key to authorize, as a Viem account. Its `access` names the Tempo Account, which
|
|
30
|
+
* defaults to the user's first. Defaults to a new key.
|
|
31
|
+
*/
|
|
32
|
+
accessKey?: Account.Account | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Runs the WebAuthn ceremony in place of `navigator.credentials.get`, such as a native passkey
|
|
35
|
+
* module. Its assertion must report a web origin on the relying party's domain that the app
|
|
36
|
+
* registered, so an Android app's facet origin is refused.
|
|
37
|
+
*/
|
|
38
|
+
getFn?: Authentication.sign.Options['getFn'] | undefined;
|
|
39
|
+
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
40
|
+
permissions?: Permissions<chainId> | undefined;
|
|
41
|
+
};
|
|
42
|
+
/** The step a wallet.tempo passkey without an email needs: an emailed code, which links it. */
|
|
43
|
+
type EmailStep = {
|
|
44
|
+
/** Passed as `link` to `Actions.email.signIn`, which links the passkey to that user. */
|
|
45
|
+
link: string;
|
|
46
|
+
/** The kind of step. */
|
|
47
|
+
type: 'email';
|
|
48
|
+
};
|
|
49
|
+
/** The authorization, or the email step. */
|
|
50
|
+
type ReturnType<chainId extends number = number> = Authorization<chainId> | {
|
|
51
|
+
step: EmailStep;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the user in with a passkey from the app's own sign-in form. The browser offers every passkey
|
|
3
|
+
* it holds for Tempo's domain, so the app needs no email first.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { Authentication } from 'ox/webauthn';
|
|
7
|
+
import * as AccessKey from '../internal/accessKey.js';
|
|
8
|
+
import * as Api from '../internal/api.js';
|
|
9
|
+
import { request } from '../internal/request.js';
|
|
10
|
+
/**
|
|
11
|
+
* Signs the user in with a passkey, and authorizes the app in the same call. Returns the email step
|
|
12
|
+
* for a wallet.tempo passkey whose account has no email: the app signs the user in with
|
|
13
|
+
* `Actions.email.signIn`, passing the step's `link`, which links the passkey. Throws a `RequestError` with reason
|
|
14
|
+
* `webauthn_required` when a passkey owns the Tempo Account the key acts for, since a passkey signs a
|
|
15
|
+
* key authorization only on Tempo Auth's pages. `Actions.redirect.start` authorizes it there, where
|
|
16
|
+
* the user is already signed in.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const result = await Actions.passkey.signIn(auth)
|
|
21
|
+
* if ('step' in result) showEmailForm()
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export async function signIn(auth, options = {}) {
|
|
25
|
+
const { getFn, permissions } = options;
|
|
26
|
+
const key = await AccessKey.resolve(options.accessKey);
|
|
27
|
+
const requested = await Api.json(await Api.post(auth, '/v1/auth/passkey/options', { json: {} }));
|
|
28
|
+
const deserialized = Authentication.deserializeOptions(requested.options);
|
|
29
|
+
const assertion = Authentication.serializeResponse(await Authentication.sign(
|
|
30
|
+
// ox reads `getFn` beside request options at runtime, which its types admit only beside a challenge.
|
|
31
|
+
getFn ? { ...deserialized, getFn } : deserialized));
|
|
32
|
+
const body = await Api.json(await Api.post(auth, '/v1/auth/passkey/verify', {
|
|
33
|
+
json: {
|
|
34
|
+
assertion,
|
|
35
|
+
authorize: request(auth, { accessKey: key.accessKey, permissions }),
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
if ('step' in body)
|
|
39
|
+
return { step: body.step };
|
|
40
|
+
return Api.authorization(body.authorization, key);
|
|
41
|
+
}
|
|
@@ -12,8 +12,7 @@ import * as Api from '../internal/api.js';
|
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
14
|
export async function create(auth) {
|
|
15
|
-
|
|
16
|
-
return { address };
|
|
15
|
+
return toTempoAccount(await Api.json(await Api.post(auth, '/v1/account/tempo', { json: {} })));
|
|
17
16
|
}
|
|
18
17
|
/**
|
|
19
18
|
* Lists the signed-in user's Tempo Accounts, their default first.
|
|
@@ -27,5 +26,12 @@ export async function list(auth) {
|
|
|
27
26
|
const { tempo } = await Api.json(await (auth.fetch ?? fetch)(new URL('/v1/account', auth.url), {
|
|
28
27
|
credentials: 'include',
|
|
29
28
|
}));
|
|
30
|
-
return tempo.map(
|
|
29
|
+
return tempo.map(toTempoAccount);
|
|
30
|
+
}
|
|
31
|
+
/** Reads the account's address and the type of its owner, which the API lists first. */
|
|
32
|
+
function toTempoAccount(account) {
|
|
33
|
+
return {
|
|
34
|
+
address: account.address,
|
|
35
|
+
owner: account.config.owners[0].type,
|
|
36
|
+
};
|
|
31
37
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempoxyz/auth",
|
|
3
|
-
"version": "0.0.0-pr-
|
|
3
|
+
"version": "0.0.0-pr-71.sha-88dea78",
|
|
4
4
|
"description": "Signs users in to Tempo through Tempo Auth, and returns Viem access key accounts on their Tempo Accounts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|