@tempoxyz/auth 0.0.0-pr-71.sha-838619f → 0.0.0-pr-75.sha-dfc7d2c

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 CHANGED
@@ -35,6 +35,12 @@ export declare namespace authorize {
35
35
  * defaults to the user's first. Defaults to a new key.
36
36
  */
37
37
  accessKey?: Account.Account | undefined;
38
+ /**
39
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
40
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
41
+ * `tempo-auth:sign` scope may send one.
42
+ */
43
+ challenge?: string | undefined;
38
44
  /** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
39
45
  permissions?: Permissions<chainId> | undefined;
40
46
  };
package/dist/Actions.js CHANGED
@@ -30,6 +30,7 @@ export async function authorize(auth, options = {}) {
30
30
  const response = await Api.post(auth, '/v1/authorize', {
31
31
  json: request(auth, {
32
32
  accessKey: key.accessKey,
33
+ challenge: options.challenge,
33
34
  permissions: options.permissions,
34
35
  }),
35
36
  });
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. It must keep
55
- * objects as they are, since a browser key is a `CryptoKey`. Defaults to IndexedDB.
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`. */
@@ -86,7 +87,7 @@ export declare namespace RequestError {
86
87
  uri?: string | undefined;
87
88
  };
88
89
  /** 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' | 'webauthn_required';
90
+ type Reason = 'already_decided' | 'approval_pending' | 'body_too_large' | 'challenge_not_permitted' | '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
91
  }
91
92
  /** A client's {@link Account} read before a sign-in set it, or after sign-out emptied it. */
92
93
  export declare class SignedOutError extends Error {
@@ -21,6 +21,12 @@ export declare namespace authorize {
21
21
  * defaults to the user's first. Defaults to a new key.
22
22
  */
23
23
  accessKey?: Account.Account | undefined;
24
+ /**
25
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
26
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
27
+ * `tempo-auth:sign` scope may send one.
28
+ */
29
+ challenge?: string | undefined;
24
30
  /**
25
31
  * The name the approval page shows. Defaults to the machine's hostname in Node, Bun, and Deno,
26
32
  * and to `Unknown device` elsewhere.
@@ -17,6 +17,9 @@ export async function authorize(auth, options = {}) {
17
17
  const key = await AccessKey.resolve(options.accessKey);
18
18
  const code = await Api.json(await Api.post(auth, '/v1/device/authorize', {
19
19
  json: {
20
+ ...(options.challenge === undefined
21
+ ? {}
22
+ : { challenge: options.challenge }),
20
23
  device: options.device ?? hostname(),
21
24
  key_authorization: request(auth, {
22
25
  accessKey: key.accessKey,
@@ -40,13 +40,21 @@ export declare namespace signIn {
40
40
  * defaults to the user's first. Defaults to a new key.
41
41
  */
42
42
  accessKey?: Account.Account | undefined;
43
+ /**
44
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
45
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
46
+ * `tempo-auth:sign` scope may send one.
47
+ */
48
+ challenge?: string | undefined;
43
49
  /** The 6-digit code the user was emailed. */
44
50
  code: string;
45
51
  /** The address the code was sent to. */
46
52
  email: string;
47
53
  /**
48
54
  * The `link` from `Actions.passkey.signIn`'s email step, which links that wallet.tempo passkey
49
- * 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`, `challenge`, and `permissions`.
57
+ * Another `accessKey` replaces the key that sign-in generated.
50
58
  */
51
59
  link?: string | undefined;
52
60
  /** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
@@ -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.
@@ -23,16 +24,28 @@ export async function sendCode(auth, options) {
23
24
  * ```
24
25
  */
25
26
  export async function signIn(auth, options) {
26
- const { code, email, link, permissions } = options;
27
- const key = await AccessKey.resolve(options.accessKey);
27
+ const { challenge, code, email, link, permissions } = options;
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. A key the app supplies replaces it.
30
+ const kept = link && !options.accessKey ? 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
- authorize: request(auth, { accessKey: key.accessKey, permissions }),
34
+ authorize: request(auth, {
35
+ accessKey: key.accessKey,
36
+ challenge,
37
+ permissions,
38
+ }),
31
39
  code,
32
40
  email,
33
41
  ...(link ? { link } : {}),
34
42
  },
35
43
  }));
44
+ // A refusal throws above, which keeps the key for a retry with a new code. Tempo Auth already took
45
+ // the parked request, so a failed removal keeps the stale key, which the next email step replaces,
46
+ // rather than discard the authorization.
47
+ if (kept && link)
48
+ await Link.release(auth, { link }).catch(() => undefined);
36
49
  if ('step' in body)
37
50
  return {
38
51
  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. The browser offers every passkey
3
- * it holds for Tempo's domain, so the app needs no email first.
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. By default the browser offers every passkey it holds for Tempo's domain, so the
4
+ * app needs no email first. A non-empty `credentialId` narrows that to the passkeys it names.
4
5
  * @module
5
6
  */
6
7
  import { Authentication } from 'ox/webauthn';
@@ -9,12 +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. 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.
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
18
+ * `Actions.email.signIn`, passing the step's `link`, which links the passkey, and the same
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
@@ -30,6 +36,19 @@ export declare namespace signIn {
30
36
  * defaults to the user's first. Defaults to a new key.
31
37
  */
32
38
  accessKey?: Account.Account | undefined;
39
+ /**
40
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
41
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
42
+ * `tempo-auth:sign` scope may send one.
43
+ */
44
+ challenge?: string | undefined;
45
+ /**
46
+ * The credential ID, or up to 16 IDs, the browser offers. Each is unpadded base64url, as
47
+ * `PublicKeyCredential.id` reports it, of 1 to 1,023 bytes. A padded or otherwise non-canonical
48
+ * encoding throws a `RequestError` with reason `invalid_field`. Defaults to every passkey the
49
+ * browser holds for Tempo's domain, as an empty list does.
50
+ */
51
+ credentialId?: readonly string[] | string | undefined;
33
52
  /**
34
53
  * Runs the WebAuthn ceremony in place of `navigator.credentials.get`, such as a native passkey
35
54
  * module. Its assertion must report a web origin on the relying party's domain that the app
@@ -38,6 +57,8 @@ export declare namespace signIn {
38
57
  * `Actions.redirect.createUrl` in the system browser, where the user signs in again.
39
58
  */
40
59
  getFn?: Authentication.sign.Options['getFn'] | undefined;
60
+ /** How the browser mediates the ceremony, such as `conditional` for passkey autofill. Defaults to the browser's own mediation. */
61
+ mediation?: 'conditional' | 'optional' | 'required' | 'silent' | undefined;
41
62
  /** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
42
63
  permissions?: Permissions<chainId> | undefined;
43
64
  };
@@ -45,6 +66,11 @@ export declare namespace signIn {
45
66
  type EmailStep = {
46
67
  /** Passed as `link` to `Actions.email.signIn`, which links the passkey to that user. */
47
68
  link: string;
69
+ /**
70
+ * Whether the assertion already signed the key, which `Actions.email.signIn` returns with
71
+ * `link`. Otherwise that call authorizes the key itself.
72
+ */
73
+ signed: boolean;
48
74
  /** The kind of step. */
49
75
  type: 'email';
50
76
  };
@@ -1,19 +1,26 @@
1
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.
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. By default the browser offers every passkey it holds for Tempo's domain, so the
4
+ * app needs no email first. A non-empty `credentialId` narrows that to the passkeys it names.
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. 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.
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
18
+ * `Actions.email.signIn`, passing the step's `link`, which links the passkey, and the same
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.
17
24
  *
18
25
  * @example
19
26
  * ```ts
@@ -22,20 +29,36 @@ import { request } from '../internal/request.js';
22
29
  * ```
23
30
  */
24
31
  export async function signIn(auth, options = {}) {
25
- const { getFn, permissions } = options;
32
+ const { challenge, credentialId, getFn, mediation, permissions } = options;
26
33
  const key = await AccessKey.resolve(options.accessKey);
27
- const requested = await Api.json(await Api.post(auth, '/v1/auth/passkey/options', { json: {} }));
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 === undefined ? {} : { credentialId }),
46
+ ...(mediation === undefined ? {} : { mediation }),
47
+ },
48
+ }));
28
49
  const deserialized = Authentication.deserializeOptions(requested.options);
29
50
  const assertion = Authentication.serializeResponse(await Authentication.sign(
30
51
  // ox reads `getFn` beside request options at runtime, which its types admit only beside a challenge.
31
52
  getFn ? { ...deserialized, getFn } : deserialized));
32
53
  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
- },
54
+ json: native ? { assertion, authorize } : { assertion },
37
55
  }));
38
- 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 });
39
61
  return { step: body.step };
62
+ }
40
63
  return Api.authorization(body.authorization, key);
41
64
  }
@@ -45,6 +45,12 @@ export declare namespace createUrl {
45
45
  * defaults to the user's first. Defaults to a new key, whose material `pending` keeps.
46
46
  */
47
47
  accessKey?: Account.Account | undefined;
48
+ /**
49
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
50
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
51
+ * `tempo-auth:sign` scope may send one.
52
+ */
53
+ challenge?: string | undefined;
48
54
  /** What the key may do. Tempo Auth's pages show it, and the headless ceiling fills what it leaves out. */
49
55
  permissions?: Permissions | undefined;
50
56
  /** Where Tempo Auth redirects back to. Defaults to the instance's `redirectUri`. */
@@ -97,6 +103,12 @@ export declare namespace start {
97
103
  * key, which `storage` keeps until the browser returns.
98
104
  */
99
105
  accessKey?: Account.Account | undefined;
106
+ /**
107
+ * A message the owner's signature binds, such as a sign-in challenge the app's backend issued.
108
+ * Tempo Auth binds its EIP-191 hash as the key authorization's `witness`. Only an app with the
109
+ * `tempo-auth:sign` scope may send one.
110
+ */
111
+ challenge?: string | undefined;
100
112
  /** What the key may do. Tempo Auth's pages show it, and the headless ceiling fills what it leaves out. */
101
113
  permissions?: Permissions | undefined;
102
114
  };
@@ -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 storage(auth).getItem(storageKey));
28
- await storage(auth).removeItem(storageKey);
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;
@@ -75,6 +75,9 @@ export async function createUrl(auth, options = {}) {
75
75
  });
76
76
  const url = new URL('/authorize', auth.url);
77
77
  url.search = new URLSearchParams({
78
+ ...(options.challenge === undefined
79
+ ? {}
80
+ : { challenge: options.challenge }),
78
81
  client_id,
79
82
  code_challenge: Base64.fromBytes(Hash.sha256(Bytes.fromString(pending.verifier)), base64url),
80
83
  key_authorization: Base64.fromString(JSON.stringify(key_authorization), base64url),
@@ -145,18 +148,12 @@ export async function handleCallback(auth, options) {
145
148
  */
146
149
  export async function start(auth, options = {}) {
147
150
  const { pending, url } = await createUrl(auth, options);
148
- await storage(auth).setItem(storageKey, pending);
151
+ await Store.get(auth).setItem(storageKey, pending);
149
152
  window.location.assign(url);
150
153
  }
151
154
  /** Unpadded URL-safe Base64, as PKCE and `key_authorization` use. */
152
155
  const base64url = { pad: false, url: true };
153
156
  /** The redirect this page load returns from, so its code is redeemed once. */
154
157
  let completed;
155
- /** The default `storage`, opened on first use. */
156
- let indexedDb;
157
158
  /** Where a redirect keeps its state in `storage`. */
158
159
  const storageKey = 'tempo-auth:redirect';
159
- /** Where a redirect keeps its state: the instance's `storage`, or IndexedDB. */
160
- function storage(auth) {
161
- return auth.storage ?? (indexedDb ??= IndexedDb.create());
162
- }
@@ -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,38 @@
1
+ import type { Auth } from '../Auth.ts';
2
+ import type * as Key from '../Key.ts';
3
+ import * as AccessKey from './accessKey.ts';
4
+ /**
5
+ * Keeps the key's material for `link`. Keeps nothing outside a browser without `storage`. Only the
6
+ * link's digest is stored, since the link links the passkey for whoever sends it with a code.
7
+ */
8
+ export declare function keep(auth: Auth, options: keep.Options): Promise<void>;
9
+ export declare namespace keep {
10
+ type Options = {
11
+ /** The email step's `link`. */
12
+ link: string;
13
+ /** The key `AccessKey.resolve` generated. */
14
+ material: Key.Material;
15
+ };
16
+ }
17
+ /**
18
+ * Returns the key kept for `link`, or `undefined` when none is. Leaves it in place, so a refused
19
+ * code retries with the same key, until {@link release} removes it.
20
+ */
21
+ export declare function get(auth: Auth, options: get.Options): Promise<AccessKey.resolve.ReturnType | undefined>;
22
+ export declare namespace get {
23
+ type Options = {
24
+ /** The `link` the emailed-code sign-in sends. */
25
+ link: string;
26
+ };
27
+ }
28
+ /**
29
+ * Removes the key kept for `link`, once the emailed-code sign-in that used it finished. A key a newer
30
+ * email step kept stays, so a sign-in that finishes late cannot remove it.
31
+ */
32
+ export declare function release(auth: Auth, options: release.Options): Promise<void>;
33
+ export declare namespace release {
34
+ type Options = {
35
+ /** The `link` the finished sign-in sent. */
36
+ link: string;
37
+ };
38
+ }
@@ -0,0 +1,44 @@
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 { Bytes, Hash } from 'ox';
7
+ import * as AccessKey from './accessKey.js';
8
+ import * as Store from './store.js';
9
+ /** Where the key waits in `storage`. A later email step replaces it. */
10
+ const storageKey = 'tempo-auth:passkey-link';
11
+ /**
12
+ * Keeps the key's material for `link`. Keeps nothing outside a browser without `storage`. Only the
13
+ * link's digest is stored, since the link links the passkey for whoever sends it with a code.
14
+ */
15
+ export async function keep(auth, options) {
16
+ const kept = {
17
+ digest: digest(options.link),
18
+ material: options.material,
19
+ };
20
+ await Store.get(auth)?.setItem(storageKey, kept);
21
+ }
22
+ /**
23
+ * Returns the key kept for `link`, or `undefined` when none is. Leaves it in place, so a refused
24
+ * code retries with the same key, until {@link release} removes it.
25
+ */
26
+ export async function get(auth, options) {
27
+ const kept = (await Store.get(auth)?.getItem(storageKey));
28
+ if (kept?.digest !== digest(options.link))
29
+ return undefined;
30
+ return AccessKey.restore(kept.material);
31
+ }
32
+ /**
33
+ * Removes the key kept for `link`, once the emailed-code sign-in that used it finished. A key a newer
34
+ * email step kept stays, so a sign-in that finishes late cannot remove it.
35
+ */
36
+ export async function release(auth, options) {
37
+ const store = Store.get(auth);
38
+ const kept = (await store?.getItem(storageKey));
39
+ if (kept?.digest === digest(options.link))
40
+ await store?.removeItem(storageKey);
41
+ }
42
+ function digest(link) {
43
+ return Hash.sha256(Bytes.fromString(link), { as: 'Hex' });
44
+ }
@@ -7,8 +7,11 @@ import type { Permissions } from '../Permissions.ts';
7
7
  */
8
8
  export declare function request(auth: Auth, options: {
9
9
  accessKey: Account.Account;
10
+ /** A message the key's witness binds. */
11
+ challenge?: string | undefined;
10
12
  permissions?: Permissions | undefined;
11
13
  }): {
14
+ challenge?: string;
12
15
  client_id: string;
13
16
  key_authorization: {
14
17
  account?: `0x${string}`;
@@ -9,8 +9,9 @@ import { Hex } from 'ox';
9
9
  * key account names its Tempo Account through `access`, and a plain key leaves it to Tempo Auth.
10
10
  */
11
11
  export function request(auth, options) {
12
- const { accessKey, permissions = {} } = options;
12
+ const { accessKey, challenge, permissions = {} } = options;
13
13
  return {
14
+ ...(challenge === undefined ? {} : { challenge }),
14
15
  client_id: auth.clientId,
15
16
  key_authorization: {
16
17
  ...(accessKey.accessKeyAddress ? { account: accessKey.address } : {}),
@@ -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-71.sha-838619f",
3
+ "version": "0.0.0-pr-75.sha-dfc7d2c",
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": {