@tempoxyz/auth 0.0.0-pr-73.sha-45751db → 0.0.0-pr-75.sha-0f45efb
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/Auth.d.ts +3 -2
- package/dist/actions/email.d.ts +2 -1
- package/dist/actions/email.js +8 -1
- package/dist/actions/passkey.d.ts +22 -8
- package/dist/actions/passkey.js +37 -19
- package/dist/actions/redirect.js +4 -10
- package/dist/internal/accessKey.d.ts +5 -0
- package/dist/internal/accessKey.js +11 -0
- package/dist/internal/link.d.ts +31 -0
- package/dist/internal/link.js +22 -0
- package/dist/internal/store.d.ts +11 -0
- package/dist/internal/store.js +14 -0
- package/package.json +1 -1
package/dist/Auth.d.ts
CHANGED
|
@@ -51,8 +51,9 @@ export type Auth = {
|
|
|
51
51
|
/** The scopes the app's tokens carry. */
|
|
52
52
|
scope: string;
|
|
53
53
|
/**
|
|
54
|
-
* Where `Actions.redirect.start` keeps its state until Tempo Auth redirects back
|
|
55
|
-
*
|
|
54
|
+
* Where `Actions.redirect.start` keeps its state until Tempo Auth redirects back, and
|
|
55
|
+
* `Actions.passkey.signIn` keeps a new key its email step signed. It must keep objects as they
|
|
56
|
+
* are, since a browser key is a `CryptoKey`. Defaults to IndexedDB.
|
|
56
57
|
*/
|
|
57
58
|
storage?: Storage | undefined;
|
|
58
59
|
/** Where Tempo Auth runs, such as `https://auth.tempo.xyz`. */
|
package/dist/actions/email.d.ts
CHANGED
|
@@ -52,7 +52,8 @@ export declare namespace signIn {
|
|
|
52
52
|
email: string;
|
|
53
53
|
/**
|
|
54
54
|
* The `link` from `Actions.passkey.signIn`'s email step, which links that wallet.tempo passkey
|
|
55
|
-
* to the user.
|
|
55
|
+
* to the user. When the step is `signed`, the call returns the key that passkey sign-in
|
|
56
|
+
* authorized with its assertion, given the same `accessKey` if the app supplied one.
|
|
56
57
|
*/
|
|
57
58
|
link?: string | undefined;
|
|
58
59
|
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
package/dist/actions/email.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as AccessKey from '../internal/accessKey.js';
|
|
2
2
|
import * as Api from '../internal/api.js';
|
|
3
|
+
import * as Link from '../internal/link.js';
|
|
3
4
|
import { request } from '../internal/request.js';
|
|
4
5
|
/**
|
|
5
6
|
* Emails the user a sign-in code. The response is the same for every address.
|
|
@@ -24,7 +25,10 @@ export async function sendCode(auth, options) {
|
|
|
24
25
|
*/
|
|
25
26
|
export async function signIn(auth, options) {
|
|
26
27
|
const { challenge, code, email, link, permissions } = options;
|
|
27
|
-
|
|
28
|
+
// A one-tap passkey sign-in's email step already signed the key it generated, which Tempo Auth
|
|
29
|
+
// returns once this request names it again.
|
|
30
|
+
const kept = link ? await Link.get(auth, { link }) : undefined;
|
|
31
|
+
const key = kept ?? (await AccessKey.resolve(options.accessKey));
|
|
28
32
|
const body = await Api.json(await Api.post(auth, '/v1/auth/code/verify', {
|
|
29
33
|
json: {
|
|
30
34
|
authorize: request(auth, {
|
|
@@ -37,6 +41,9 @@ export async function signIn(auth, options) {
|
|
|
37
41
|
...(link ? { link } : {}),
|
|
38
42
|
},
|
|
39
43
|
}));
|
|
44
|
+
// A refusal throws above, which keeps the key for a retry with a new code.
|
|
45
|
+
if (kept)
|
|
46
|
+
await Link.release(auth);
|
|
40
47
|
if ('step' in body)
|
|
41
48
|
return {
|
|
42
49
|
step: { ...body.step, url: new URL(body.step.url, auth.url).href },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signs the user in with a passkey from the app's own sign-in form
|
|
3
|
-
* it holds for Tempo's domain, so the app needs no
|
|
2
|
+
* Signs the user in with a passkey from the app's own sign-in form, and signs the app's key with the
|
|
3
|
+
* same assertion. The browser offers every passkey it holds for Tempo's domain, so the app needs no
|
|
4
|
+
* email first.
|
|
4
5
|
* @module
|
|
5
6
|
*/
|
|
6
7
|
import { Authentication } from 'ox/webauthn';
|
|
@@ -9,13 +10,17 @@ import type { Auth } from '../Auth.ts';
|
|
|
9
10
|
import type { Authorization } from '../Authorization.ts';
|
|
10
11
|
import type { Permissions } from '../Permissions.ts';
|
|
11
12
|
/**
|
|
12
|
-
* Signs the user in with a passkey, and authorizes the app in the same call.
|
|
13
|
-
*
|
|
13
|
+
* Signs the user in with a passkey, and authorizes the app in the same call. On a page, the
|
|
14
|
+
* passkey's one assertion also signs the app's key when that passkey owns the Tempo Account, since
|
|
15
|
+
* the ceremony's challenge is the key's digest. A native app, which sends no `Origin`, signs in
|
|
16
|
+
* with a random challenge and authorizes as a headless sign-in does. Returns the email step for a
|
|
17
|
+
* wallet.tempo passkey whose account has no email: the app signs the user in with
|
|
14
18
|
* `Actions.email.signIn`, passing the step's `link`, which links the passkey, and the same
|
|
15
|
-
* `accessKey`, `challenge`, and `permissions
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
19
|
+
* `accessKey`, `challenge`, and `permissions`. A `signed` step's assertion already signed the key,
|
|
20
|
+
* which that call returns without another tap, so a new key waits in the instance's `storage`.
|
|
21
|
+
* Throws a `RequestError` with reason `webauthn_required` when another passkey owns
|
|
22
|
+
* the account, which signs only on Tempo Auth's pages, where `Actions.redirect.start` authorizes it
|
|
23
|
+
* for the signed-in user.
|
|
19
24
|
*
|
|
20
25
|
* @example
|
|
21
26
|
* ```ts
|
|
@@ -37,12 +42,16 @@ export declare namespace signIn {
|
|
|
37
42
|
* `tempo-auth:sign` scope may send one.
|
|
38
43
|
*/
|
|
39
44
|
challenge?: string | undefined;
|
|
45
|
+
/** The credential ID, or IDs, the browser offers. Defaults to every passkey it holds for Tempo's domain. */
|
|
46
|
+
credentialId?: string | readonly string[] | undefined;
|
|
40
47
|
/**
|
|
41
48
|
* Runs the WebAuthn ceremony in place of `navigator.credentials.get`, such as a native passkey
|
|
42
49
|
* module. Its assertion must report a web origin on the relying party's domain that the app
|
|
43
50
|
* registered, so an Android app's facet origin is refused.
|
|
44
51
|
*/
|
|
45
52
|
getFn?: Authentication.sign.Options['getFn'] | undefined;
|
|
53
|
+
/** How the browser mediates the ceremony, such as `conditional` for passkey autofill. */
|
|
54
|
+
mediation?: 'conditional' | 'optional' | 'required' | 'silent' | undefined;
|
|
46
55
|
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
47
56
|
permissions?: Permissions<chainId> | undefined;
|
|
48
57
|
};
|
|
@@ -50,6 +59,11 @@ export declare namespace signIn {
|
|
|
50
59
|
type EmailStep = {
|
|
51
60
|
/** Passed as `link` to `Actions.email.signIn`, which links the passkey to that user. */
|
|
52
61
|
link: string;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the assertion already signed the key, which `Actions.email.signIn` returns with
|
|
64
|
+
* `link`. Otherwise that call authorizes the key itself.
|
|
65
|
+
*/
|
|
66
|
+
signed: boolean;
|
|
53
67
|
/** The kind of step. */
|
|
54
68
|
type: 'email';
|
|
55
69
|
};
|
package/dist/actions/passkey.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signs the user in with a passkey from the app's own sign-in form
|
|
3
|
-
* it holds for Tempo's domain, so the app needs no
|
|
2
|
+
* Signs the user in with a passkey from the app's own sign-in form, and signs the app's key with the
|
|
3
|
+
* same assertion. The browser offers every passkey it holds for Tempo's domain, so the app needs no
|
|
4
|
+
* email first.
|
|
4
5
|
* @module
|
|
5
6
|
*/
|
|
6
7
|
import { Authentication } from 'ox/webauthn';
|
|
7
8
|
import * as AccessKey from '../internal/accessKey.js';
|
|
8
9
|
import * as Api from '../internal/api.js';
|
|
10
|
+
import * as Link from '../internal/link.js';
|
|
9
11
|
import { request } from '../internal/request.js';
|
|
10
12
|
/**
|
|
11
|
-
* Signs the user in with a passkey, and authorizes the app in the same call.
|
|
12
|
-
*
|
|
13
|
+
* Signs the user in with a passkey, and authorizes the app in the same call. On a page, the
|
|
14
|
+
* passkey's one assertion also signs the app's key when that passkey owns the Tempo Account, since
|
|
15
|
+
* the ceremony's challenge is the key's digest. A native app, which sends no `Origin`, signs in
|
|
16
|
+
* with a random challenge and authorizes as a headless sign-in does. Returns the email step for a
|
|
17
|
+
* wallet.tempo passkey whose account has no email: the app signs the user in with
|
|
13
18
|
* `Actions.email.signIn`, passing the step's `link`, which links the passkey, and the same
|
|
14
|
-
* `accessKey`, `challenge`, and `permissions
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the
|
|
19
|
+
* `accessKey`, `challenge`, and `permissions`. A `signed` step's assertion already signed the key,
|
|
20
|
+
* which that call returns without another tap, so a new key waits in the instance's `storage`.
|
|
21
|
+
* Throws a `RequestError` with reason `webauthn_required` when another passkey owns
|
|
22
|
+
* the account, which signs only on Tempo Auth's pages, where `Actions.redirect.start` authorizes it
|
|
23
|
+
* for the signed-in user.
|
|
18
24
|
*
|
|
19
25
|
* @example
|
|
20
26
|
* ```ts
|
|
@@ -23,24 +29,36 @@ import { request } from '../internal/request.js';
|
|
|
23
29
|
* ```
|
|
24
30
|
*/
|
|
25
31
|
export async function signIn(auth, options = {}) {
|
|
26
|
-
const { challenge, getFn, permissions } = options;
|
|
32
|
+
const { challenge, credentialId, getFn, mediation, permissions } = options;
|
|
27
33
|
const key = await AccessKey.resolve(options.accessKey);
|
|
28
|
-
const
|
|
34
|
+
const authorize = request(auth, {
|
|
35
|
+
accessKey: key.accessKey,
|
|
36
|
+
challenge,
|
|
37
|
+
permissions,
|
|
38
|
+
});
|
|
39
|
+
// A page sends the request with the options, so the challenge is its key's digest. A native app
|
|
40
|
+
// sends no `Origin` to bind one-tap to, so it sends the request with the assertion instead.
|
|
41
|
+
const native = !/^https?:/i.test(authorize.redirect_uri);
|
|
42
|
+
const requested = await Api.json(await Api.post(auth, '/v1/auth/passkey/options', {
|
|
43
|
+
json: {
|
|
44
|
+
...(native ? {} : { authorize }),
|
|
45
|
+
...(credentialId ? { credentialId } : {}),
|
|
46
|
+
...(mediation ? { mediation } : {}),
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
29
49
|
const deserialized = Authentication.deserializeOptions(requested.options);
|
|
30
50
|
const assertion = Authentication.serializeResponse(await Authentication.sign(
|
|
31
51
|
// ox reads `getFn` beside request options at runtime, which its types admit only beside a challenge.
|
|
32
52
|
getFn ? { ...deserialized, getFn } : deserialized));
|
|
33
53
|
const body = await Api.json(await Api.post(auth, '/v1/auth/passkey/verify', {
|
|
34
|
-
json: {
|
|
35
|
-
assertion,
|
|
36
|
-
authorize: request(auth, {
|
|
37
|
-
accessKey: key.accessKey,
|
|
38
|
-
challenge,
|
|
39
|
-
permissions,
|
|
40
|
-
}),
|
|
41
|
-
},
|
|
54
|
+
json: native ? { assertion, authorize } : { assertion },
|
|
42
55
|
}));
|
|
43
|
-
if ('step' in body)
|
|
56
|
+
if ('step' in body) {
|
|
57
|
+
// The emailed-code sign-in that sends the link returns this key's authorization, so a key the
|
|
58
|
+
// library generated waits for it in `storage`.
|
|
59
|
+
if (body.step.signed && !options.accessKey)
|
|
60
|
+
await Link.keep(auth, { link: body.step.link, material: key.material });
|
|
44
61
|
return { step: body.step };
|
|
62
|
+
}
|
|
45
63
|
return Api.authorization(body.authorization, key);
|
|
46
64
|
}
|
package/dist/actions/redirect.js
CHANGED
|
@@ -8,8 +8,8 @@ import { Base64, Bytes, Hash } from 'ox';
|
|
|
8
8
|
import { RequestError } from '../Auth.js';
|
|
9
9
|
import * as AccessKey from '../internal/accessKey.js';
|
|
10
10
|
import * as Api from '../internal/api.js';
|
|
11
|
-
import * as IndexedDb from '../internal/indexedDb.js';
|
|
12
11
|
import { request } from '../internal/request.js';
|
|
12
|
+
import * as Store from '../internal/store.js';
|
|
13
13
|
import * as Pending from '../Pending.js';
|
|
14
14
|
/**
|
|
15
15
|
* Finishes a {@link start} when the page loads with Tempo Auth's callback, and returns `undefined`
|
|
@@ -24,8 +24,8 @@ import * as Pending from '../Pending.js';
|
|
|
24
24
|
*/
|
|
25
25
|
export function complete(auth, options = {}) {
|
|
26
26
|
completed ??= (async () => {
|
|
27
|
-
const stored = (await
|
|
28
|
-
await
|
|
27
|
+
const stored = (await Store.get(auth).getItem(storageKey));
|
|
28
|
+
await Store.get(auth).removeItem(storageKey);
|
|
29
29
|
const url = new URL(window.location.href);
|
|
30
30
|
if (!url.searchParams.has('code'))
|
|
31
31
|
return undefined;
|
|
@@ -148,18 +148,12 @@ export async function handleCallback(auth, options) {
|
|
|
148
148
|
*/
|
|
149
149
|
export async function start(auth, options = {}) {
|
|
150
150
|
const { pending, url } = await createUrl(auth, options);
|
|
151
|
-
await
|
|
151
|
+
await Store.get(auth).setItem(storageKey, pending);
|
|
152
152
|
window.location.assign(url);
|
|
153
153
|
}
|
|
154
154
|
/** Unpadded URL-safe Base64, as PKCE and `key_authorization` use. */
|
|
155
155
|
const base64url = { pad: false, url: true };
|
|
156
156
|
/** The redirect this page load returns from, so its code is redeemed once. */
|
|
157
157
|
let completed;
|
|
158
|
-
/** The default `storage`, opened on first use. */
|
|
159
|
-
let indexedDb;
|
|
160
158
|
/** Where a redirect keeps its state in `storage`. */
|
|
161
159
|
const storageKey = 'tempo-auth:redirect';
|
|
162
|
-
/** Where a redirect keeps its state: the instance's `storage`, or IndexedDB. */
|
|
163
|
-
function storage(auth) {
|
|
164
|
-
return auth.storage ?? (indexedDb ??= IndexedDb.create());
|
|
165
|
-
}
|
|
@@ -5,6 +5,11 @@ import type * as Key from '../Key.ts';
|
|
|
5
5
|
* a `CryptoKey` across page loads. Elsewhere a CLI or native app must store the key as a string.
|
|
6
6
|
*/
|
|
7
7
|
export declare function resolve(accessKey: Account.Account | undefined): Promise<resolve.ReturnType>;
|
|
8
|
+
/**
|
|
9
|
+
* Rebuilds a key that {@link resolve} generated from its material. Returns `undefined` for the
|
|
10
|
+
* empty material of a key the app supplied.
|
|
11
|
+
*/
|
|
12
|
+
export declare function restore(material: Key.Material): resolve.ReturnType | undefined;
|
|
8
13
|
export declare namespace resolve {
|
|
9
14
|
type ReturnType = {
|
|
10
15
|
/** The key, as a Viem account that signs with it. */
|
|
@@ -24,3 +24,14 @@ export async function resolve(accessKey) {
|
|
|
24
24
|
material: { privateKey },
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Rebuilds a key that {@link resolve} generated from its material. Returns `undefined` for the
|
|
29
|
+
* empty material of a key the app supplied.
|
|
30
|
+
*/
|
|
31
|
+
export function restore(material) {
|
|
32
|
+
if (material.keyPair)
|
|
33
|
+
return { accessKey: Account.fromWebCryptoP256(material.keyPair), material };
|
|
34
|
+
if (material.privateKey)
|
|
35
|
+
return { accessKey: Account.fromSecp256k1(material.privateKey), material };
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a new key that a one-tap passkey sign-in signed, until the emailed-code sign-in that sends
|
|
3
|
+
* its email step's `link` returns the key's authorization.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { Auth } from '../Auth.ts';
|
|
7
|
+
import type * as Key from '../Key.ts';
|
|
8
|
+
import * as AccessKey from './accessKey.ts';
|
|
9
|
+
/** Keeps the key's material for `link`. Keeps nothing outside a browser without `storage`. */
|
|
10
|
+
export declare function keep(auth: Auth, options: keep.Options): Promise<void>;
|
|
11
|
+
export declare namespace keep {
|
|
12
|
+
type Options = {
|
|
13
|
+
/** The email step's `link`. */
|
|
14
|
+
link: string;
|
|
15
|
+
/** The key `AccessKey.resolve` generated. */
|
|
16
|
+
material: Key.Material;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns the key kept for `link`, or `undefined` when none is. Leaves it in place, so a refused
|
|
21
|
+
* code retries with the same key, until {@link release} removes it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function get(auth: Auth, options: get.Options): Promise<AccessKey.resolve.ReturnType | undefined>;
|
|
24
|
+
export declare namespace get {
|
|
25
|
+
type Options = {
|
|
26
|
+
/** The `link` the emailed-code sign-in sends. */
|
|
27
|
+
link: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Removes the kept key, once the emailed-code sign-in that used it finished. */
|
|
31
|
+
export declare function release(auth: Auth): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as AccessKey from './accessKey.js';
|
|
2
|
+
import * as Store from './store.js';
|
|
3
|
+
/** Where the key waits in `storage`. A later email step replaces it. */
|
|
4
|
+
const storageKey = 'tempo-auth:passkey-link';
|
|
5
|
+
/** Keeps the key's material for `link`. Keeps nothing outside a browser without `storage`. */
|
|
6
|
+
export async function keep(auth, options) {
|
|
7
|
+
await Store.get(auth)?.setItem(storageKey, options);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns the key kept for `link`, or `undefined` when none is. Leaves it in place, so a refused
|
|
11
|
+
* code retries with the same key, until {@link release} removes it.
|
|
12
|
+
*/
|
|
13
|
+
export async function get(auth, options) {
|
|
14
|
+
const kept = (await Store.get(auth)?.getItem(storageKey));
|
|
15
|
+
if (kept?.link !== options.link)
|
|
16
|
+
return undefined;
|
|
17
|
+
return AccessKey.restore(kept.material);
|
|
18
|
+
}
|
|
19
|
+
/** Removes the kept key, once the emailed-code sign-in that used it finished. */
|
|
20
|
+
export async function release(auth) {
|
|
21
|
+
await Store.get(auth)?.removeItem(storageKey);
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves where actions keep state across page loads: the instance's `storage`, or IndexedDB.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import type { Auth } from '../Auth.ts';
|
|
6
|
+
import type { Storage } from '../Storage.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Returns the instance's `storage`, or the page's IndexedDB. Returns `undefined` outside a browser
|
|
9
|
+
* when the instance names no `storage`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function get(auth: Auth): Storage | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as IndexedDb from './indexedDb.js';
|
|
2
|
+
/** The default `storage`, opened on first use. */
|
|
3
|
+
let indexedDb;
|
|
4
|
+
/**
|
|
5
|
+
* Returns the instance's `storage`, or the page's IndexedDB. Returns `undefined` outside a browser
|
|
6
|
+
* when the instance names no `storage`.
|
|
7
|
+
*/
|
|
8
|
+
export function get(auth) {
|
|
9
|
+
if (auth.storage)
|
|
10
|
+
return auth.storage;
|
|
11
|
+
if (typeof indexedDB === 'undefined')
|
|
12
|
+
return undefined;
|
|
13
|
+
return (indexedDb ??= IndexedDb.create());
|
|
14
|
+
}
|
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-75.sha-0f45efb",
|
|
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": {
|