@tempoxyz/auth 0.0.0-pr-22.sha-291bbcb → 0.0.0-pr-25.sha-5404a24

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.
Files changed (45) hide show
  1. package/README.md +72 -46
  2. package/dist/Actions.d.ts +18 -4
  3. package/dist/Actions.js +8 -12
  4. package/dist/Auth.d.ts +11 -91
  5. package/dist/Auth.js +29 -0
  6. package/dist/Authorization.d.ts +25 -0
  7. package/dist/Authorization.js +11 -0
  8. package/dist/Key.d.ts +40 -8
  9. package/dist/Key.js +41 -5
  10. package/dist/Pending.d.ts +26 -0
  11. package/dist/Pending.js +11 -0
  12. package/dist/Permissions.d.ts +25 -0
  13. package/dist/Permissions.js +15 -0
  14. package/dist/Provider.d.ts +15 -0
  15. package/dist/Provider.js +11 -0
  16. package/dist/Step.d.ts +20 -0
  17. package/dist/Step.js +11 -0
  18. package/dist/Storage.d.ts +26 -0
  19. package/dist/Storage.js +15 -0
  20. package/dist/TempoAccount.d.ts +18 -0
  21. package/dist/TempoAccount.js +11 -0
  22. package/dist/Tokens.d.ts +24 -0
  23. package/dist/Tokens.js +11 -0
  24. package/dist/User.d.ts +20 -0
  25. package/dist/User.js +11 -0
  26. package/dist/actions/device.d.ts +12 -4
  27. package/dist/actions/device.js +9 -7
  28. package/dist/actions/email.d.ts +17 -4
  29. package/dist/actions/email.js +6 -10
  30. package/dist/actions/provider.d.ts +2 -1
  31. package/dist/actions/redirect.d.ts +32 -12
  32. package/dist/actions/redirect.js +32 -23
  33. package/dist/actions/tempoAccount.d.ts +4 -3
  34. package/dist/actions/tempoAccount.js +2 -2
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +9 -0
  37. package/dist/internal/accessKey.d.ts +15 -0
  38. package/dist/internal/accessKey.js +26 -0
  39. package/dist/internal/api.d.ts +10 -8
  40. package/dist/internal/api.js +10 -6
  41. package/dist/internal/indexedDb.d.ts +8 -0
  42. package/dist/internal/indexedDb.js +31 -0
  43. package/dist/internal/request.d.ts +8 -10
  44. package/dist/internal/request.js +12 -10
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -10,6 +10,7 @@ import { Addresses, createClient, Expiry, Scopes } from 'viem/tempo'
10
10
  import { tempoTestnet } from 'viem/tempo/chains'
11
11
 
12
12
  const auth = Auth.create({
13
+ chainId: tempoTestnet.id,
13
14
  clientId: 'client_…',
14
15
  url: 'https://auth.tempo.xyz',
15
16
  })
@@ -19,8 +20,7 @@ const client = createClient({
19
20
  })
20
21
 
21
22
  const authorization = await Actions.authorize(auth, {
22
- accessKey: {
23
- chainId: tempoTestnet.id,
23
+ permissions: {
24
24
  expiry: Expiry.days(1),
25
25
  limits: [{ limit: 10_000_000n, token: Addresses.pathUsd }],
26
26
  scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
@@ -50,15 +50,23 @@ The CLI device grant needs neither. It signs in as Tempo Auth's own `tempo-cli`
50
50
 
51
51
  ## Access keys
52
52
 
53
- Each sign-in creates a key for the app and asks Tempo Auth to authorize it on the user's Tempo Account. The `accessKey` option says what the key may do, with viem's `Expiry` and `Scopes` helpers:
53
+ Each sign-in creates a key for the app and asks Tempo Auth to authorize it on the user's Tempo Account. The `permissions` option says what the key may do, with viem's `Expiry` and `Scopes` helpers:
54
54
 
55
- | Field | Meaning |
56
- | --------- | --------------------------------------------------------------------------------------------- |
57
- | `account` | Optional: which of the user's Tempo Accounts, from `tempoAccount.list`. Defaults to the first |
58
- | `chainId` | The chain the key acts on, such as `tempoTestnet.id` |
59
- | `expiry` | When the key expires, in Unix seconds, such as `Expiry.days(1)` |
60
- | `limits` | One-time spending limits per token, such as `[{ limit: 10_000_000n, token }]` |
61
- | `scopes` | The calls the key may make, such as `[Scopes.tip20(token).transfer()]` |
55
+ | Field | Meaning |
56
+ | --------- | ----------------------------------------------------------------------------- |
57
+ | `chainId` | Optional: the chain the key acts on. Defaults to the instance's `chainId` |
58
+ | `expiry` | When the key expires, in Unix seconds, such as `Expiry.days(1)` |
59
+ | `limits` | One-time spending limits per token, such as `[{ limit: 10_000_000n, token }]` |
60
+ | `scopes` | The calls the key may make, such as `[Scopes.tip20(token).transfer()]` |
61
+
62
+ In a browser, the key is a non-extractable WebCrypto P256 key, so no script can read its private key. A CLI, a native app, or a backend gets a secp256k1 key, which it stores as a string.
63
+
64
+ To authorize a key the app already holds, pass it as `accessKey`, a viem account. The library never sees its private key. Its `access` names the Tempo Account, which defaults to the user's first:
65
+
66
+ ```ts
67
+ const accessKey = Account.fromWebCryptoP256(keyPair, { access: tempoAccount })
68
+ await Actions.authorize(auth, { accessKey, permissions })
69
+ ```
62
70
 
63
71
  Flows without a Tempo Auth page (`authorize`, `email.signIn`) must stay within Tempo Auth's ceiling, about $1,000 per token. Flows with a page (`redirect.start`, `redirect.createUrl`, `device.authorize`) show the limits to the user, who approves them, so they may go beyond it.
64
72
 
@@ -72,11 +80,11 @@ For a browser app that renders its own email form. The page calls Tempo Auth wit
72
80
 
73
81
  ```ts
74
82
  // A user already signed in to Tempo Auth, from any app, needs no form.
75
- const authorization = await Actions.authorize(auth, { accessKey })
83
+ const authorization = await Actions.authorize(auth, { permissions })
76
84
 
77
85
  // Otherwise, sign in with an emailed code.
78
86
  await Actions.email.sendCode(auth, { email })
79
- const result = await Actions.email.signIn(auth, { accessKey, code, email })
87
+ const result = await Actions.email.signIn(auth, { code, email, permissions })
80
88
 
81
89
  // An address that requires SSO continues at its connection, and returns to this page.
82
90
  if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
@@ -94,13 +102,13 @@ Actions.provider.signIn(auth, { provider: 'google' })
94
102
  For a browser app without its own sign-in UI. The browser leaves for Tempo Auth's sign-in page, which shows the key's limits, and returns with a code.
95
103
 
96
104
  ```ts
97
- await Actions.redirect.start(auth, { accessKey })
105
+ await Actions.redirect.start(auth, { permissions })
98
106
 
99
107
  // On the page the browser returns to, and on every load:
100
108
  const authorization = await Actions.redirect.complete(auth)
101
109
  ```
102
110
 
103
- `redirect.start` keeps its state in session storage until the browser returns. Pass `storage` to `Auth.create` to keep it elsewhere.
111
+ `redirect.start` keeps its state, with the new key, in IndexedDB until the browser returns. Pass `storage` to `Auth.create` to keep it elsewhere. An app that supplies `accessKey` passes it to `redirect.complete` too.
104
112
 
105
113
  ### A mobile app
106
114
 
@@ -117,11 +125,11 @@ const auth = Auth.create({
117
125
 
118
126
  // Your own sign-in UI, as in a browser.
119
127
  await Actions.email.sendCode(auth, { email })
120
- const result = await Actions.email.signIn(auth, { accessKey, code, email })
128
+ const result = await Actions.email.signIn(auth, { code, email, permissions })
121
129
 
122
130
  // Or Tempo Auth's pages, which also offer Google and SSO.
123
131
  const { pending, url } = await Actions.redirect.createUrl(auth, {
124
- accessKey,
132
+ permissions,
125
133
  })
126
134
  const session = await WebBrowser.openAuthSessionAsync(url, auth.redirectUri)
127
135
  if (session.type === 'success')
@@ -142,7 +150,7 @@ const auth = Auth.create({ clientId, clientSecret, url })
142
150
 
143
151
  // GET /auth/start
144
152
  const { pending, url } = await Actions.redirect.createUrl(auth, {
145
- accessKey,
153
+ permissions,
146
154
  redirectUri: 'https://app.example/auth/callback',
147
155
  })
148
156
  // Keep `pending` in an encrypted, HttpOnly cookie, then redirect to `url`.
@@ -160,8 +168,8 @@ For a command-line tool. The user approves a code in any browser, and the CLI wa
160
168
 
161
169
  ```ts
162
170
  const device = await Actions.device.authorize(auth, {
163
- accessKey,
164
171
  device: os.hostname(),
172
+ permissions,
165
173
  })
166
174
  console.log(
167
175
  `Open ${device.verificationUriComplete} and confirm ${device.userCode}.`,
@@ -171,20 +179,27 @@ const authorization = await device.wait()
171
179
 
172
180
  ## More Tempo Accounts
173
181
 
174
- A user starts with one Tempo Account and can have more. Add one, then name it in an access key's `account`:
182
+ A user starts with one Tempo Account and can have more. Add one, then name it in the `access` of the key the app authorizes:
175
183
 
176
184
  ```ts
177
185
  const tempoAccount = await Actions.tempoAccount.create(auth)
178
186
  const accounts = await Actions.tempoAccount.list(auth) // the default first
179
187
 
180
188
  await Actions.authorize(auth, {
181
- accessKey: { ...accessKey, account: tempoAccount.address },
189
+ accessKey: Account.fromWebCryptoP256(keyPair, {
190
+ access: tempoAccount.address,
191
+ }),
192
+ permissions,
182
193
  })
183
194
  ```
184
195
 
185
196
  ## Keep a sign-in
186
197
 
187
- `Authorization.key` is the app's key as JSON: the Tempo Account, the signed authorization, and the private key. Store it with the tokens, as securely as a session, and rebuild the account later with `Key.toAccount`.
198
+ `Authorization.key` is the app's key as data: the Tempo Account, the signed authorization, and the key the library created. Store it with the tokens, and rebuild the account later with `Key.toAccount`.
199
+
200
+ - **Browser:** the key is a `CryptoKey` pair, which IndexedDB stores without exporting it.
201
+ - **Elsewhere:** the key is a secp256k1 private key, as JSON. Store it as securely as a session.
202
+ - **Supplied key:** the key holds no material. Pass the `accessKey` again, as `Key.toAccount(saved.key, { accessKey })`.
188
203
 
189
204
  ```ts
190
205
  import { Actions, Key } from '@tempoxyz/auth'
@@ -205,7 +220,7 @@ A request Tempo Auth refuses throws `Auth.RequestError`, with its `status` and `
205
220
 
206
221
  ```ts
207
222
  try {
208
- await Actions.email.signIn(auth, { accessKey, code, email })
223
+ await Actions.email.signIn(auth, { code, email, permissions })
209
224
  } catch (error) {
210
225
  if (error instanceof Auth.RequestError && error.status === 401)
211
226
  showWrongCode()
@@ -219,49 +234,60 @@ try {
219
234
  | Option | Meaning |
220
235
  | -------------- | ----------------------------------------------------------------------------------------------- |
221
236
  | `clientId` | The app's Connect client ID. Required |
237
+ | `chainId` | The chain the app's keys act on, such as `tempoTestnet.id`. Defaults to Tempo mainnet |
222
238
  | `url` | Where Tempo Auth runs, such as `https://auth.tempo.xyz`. Required |
223
239
  | `clientSecret` | The app's client secret. Only a backend holds one |
224
240
  | `redirectUri` | Where Tempo Auth redirects back to. Defaults to the page's origin |
225
241
  | `scope` | Scopes beyond `openid` and `email`, which every instance includes. Defaults to `offline_access` |
226
- | `storage` | Where `redirect.start` keeps its state. Defaults to session storage |
242
+ | `storage` | Where `redirect.start` keeps its state, without serializing it. Defaults to IndexedDB |
227
243
  | `fetch` | Calls Tempo Auth. Defaults to the global `fetch` |
228
244
 
229
245
  ### `Actions`
230
246
 
231
- Each sign-in flow has its own namespace. The actions every flow shares sit at the top level.
232
-
233
- | Action | Returns |
234
- | ------------------------------------------------------ | -------------------------------------------------------------------------- |
235
- | `authorize(auth, { accessKey })` | An `Authorization` for a user already signed in, or `undefined` |
236
- | `refresh(auth, { refreshToken })` | New `Tokens` |
237
- | `signOut(auth)` | Nothing. The user's Tempo Auth session ends |
238
- | `device.authorize(auth, { accessKey, device })` | The code to show and a `wait()` that resolves with an `Authorization` |
239
- | `email.sendCode(auth, { email })` | Nothing. Tempo Auth emails a code |
240
- | `email.signIn(auth, { accessKey, code, email })` | An `Authorization`, or the `step` the address requires first |
241
- | `provider.list(auth)` | The OAuth providers Tempo Auth offers, as `{ id, name }` |
242
- | `provider.signIn(auth, { provider } \| { url })` | Nothing. The browser leaves for the provider or SSO connection |
243
- | `redirect.complete(auth)` | The `Authorization` when the page is a redirect's callback, or `undefined` |
244
- | `redirect.createUrl(auth, { accessKey, redirectUri })` | The `url` to send the browser to, and the `pending` state to keep |
245
- | `redirect.handleCallback(auth, { pending, url })` | The `Authorization` for a redirect's callback |
246
- | `redirect.start(auth, { accessKey })` | Nothing. The browser leaves for Tempo Auth's pages |
247
- | `tempoAccount.create(auth)` | Another Tempo Account for the signed-in user |
248
- | `tempoAccount.list(auth)` | The signed-in user's Tempo Accounts, the default first |
247
+ Each sign-in flow has its own namespace. The actions every flow shares sit at the top level. Every action that takes `permissions` also takes an optional `accessKey`.
248
+
249
+ | Action | Returns |
250
+ | -------------------------------------------------------- | -------------------------------------------------------------------------- |
251
+ | `authorize(auth, { permissions })` | An `Authorization` for a user already signed in, or `undefined` |
252
+ | `refresh(auth, { refreshToken })` | New `Tokens` |
253
+ | `signOut(auth)` | Nothing. The user's Tempo Auth session ends |
254
+ | `device.authorize(auth, { device, permissions })` | The code to show and a `wait()` that resolves with an `Authorization` |
255
+ | `email.sendCode(auth, { email })` | Nothing. Tempo Auth emails a code |
256
+ | `email.signIn(auth, { code, email, permissions })` | An `Authorization`, or the `step` the address requires first |
257
+ | `provider.list(auth)` | The OAuth providers Tempo Auth offers, as `{ id, name }` |
258
+ | `provider.signIn(auth, { provider } \| { url })` | Nothing. The browser leaves for the provider or SSO connection |
259
+ | `redirect.complete(auth, { accessKey })` | The `Authorization` when the page is a redirect's callback, or `undefined` |
260
+ | `redirect.createUrl(auth, { permissions, redirectUri })` | The `url` to send the browser to, and the `pending` state to keep |
261
+ | `redirect.handleCallback(auth, { pending, url })` | The `Authorization` for a redirect's callback |
262
+ | `redirect.start(auth, { permissions })` | Nothing. The browser leaves for Tempo Auth's pages |
263
+ | `tempoAccount.create(auth)` | Another Tempo Account for the signed-in user |
264
+ | `tempoAccount.list(auth)` | The signed-in user's Tempo Accounts, the default first |
249
265
 
250
266
  ### `Authorization`
251
267
 
252
268
  | Field | Meaning |
253
269
  | -------------- | -------------------------------------------------------------------------------- |
254
270
  | `account` | The user's Tempo Account, as a viem account that signs with the app's access key |
255
- | `key` | The same key as JSON, for `Key.toAccount` |
271
+ | `key` | The same key as data, for `Key.toAccount` |
256
272
  | `accessToken` | The Bearer token Tempo APIs accept |
257
273
  | `expiresAt` | When the access token expires, in milliseconds |
258
274
  | `idToken` | Optional: the OIDC ID token, which names the user |
259
275
  | `refreshToken` | Optional: exchanges for new tokens through `refresh`. It rotates each time |
260
276
  | `user` | The user's `email` and `id`, the `sub` of every token |
261
277
 
278
+ ### Types
279
+
280
+ Each type an action takes or returns lives in its own module, exported beside `Actions`: `Authorization`, `Key`, `Pending`, `Permissions`, `Provider`, `Step`, `Storage`, `TempoAccount`, `Tokens`, and `User`. Each module's `from` builds a value of its type:
281
+
282
+ ```ts
283
+ import { Permissions } from '@tempoxyz/auth'
284
+
285
+ const permissions = Permissions.from({ expiry: Expiry.days(1), limits, scopes })
286
+ ```
287
+
262
288
  ### `Key`
263
289
 
264
- | Function | Returns |
265
- | ---------------- | ------------------------------------------- |
266
- | `toAccount(key)` | The key as a viem access key account |
267
- | `expired(key)` | Whether the key's authorization has expired |
290
+ | Function | Returns |
291
+ | ------------------------------- | ------------------------------------------------------------------------------- |
292
+ | `toAccount(key, { accessKey })` | The key as a viem access key account. `accessKey` is for a key the app supplied |
293
+ | `expired(key)` | Whether the key's authorization has expired |
package/dist/Actions.d.ts CHANGED
@@ -1,4 +1,13 @@
1
- import type { AccessKey, Auth, Authorization, Tokens } from './Auth.ts';
1
+ /**
2
+ * Tempo Auth's actions. Each takes an instance from `Auth.create`. Each sign-in flow has its own
3
+ * namespace, and the actions every flow shares sit at the top level.
4
+ * @module
5
+ */
6
+ import type { Account } from 'viem/tempo';
7
+ import type { Auth } from './Auth.ts';
8
+ import type { Authorization } from './Authorization.ts';
9
+ import type { Permissions } from './Permissions.ts';
10
+ import type { Tokens } from './Tokens.ts';
2
11
  export * as device from './actions/device.ts';
3
12
  export * as email from './actions/email.ts';
4
13
  export * as provider from './actions/provider.ts';
@@ -11,15 +20,20 @@ export * as tempoAccount from './actions/tempoAccount.ts';
11
20
  *
12
21
  * @example
13
22
  * ```ts
14
- * const authorization = await Actions.authorize(auth, { accessKey })
23
+ * const authorization = await Actions.authorize(auth, { permissions })
15
24
  * if (!authorization) showSignIn()
16
25
  * ```
17
26
  */
18
27
  export declare function authorize(auth: Auth, options: authorize.Options): Promise<authorize.ReturnType>;
19
28
  export declare namespace authorize {
20
29
  type Options = {
21
- /** What the app's key may do. It must fit the headless ceiling. */
22
- accessKey: AccessKey;
30
+ /**
31
+ * The key to authorize, as a viem account. Its `access` names the Tempo Account, which
32
+ * defaults to the user's first. Defaults to a new key.
33
+ */
34
+ accessKey?: Account.Account | undefined;
35
+ /** What the key may do. It must fit the headless ceiling. */
36
+ permissions: Permissions;
23
37
  };
24
38
  /** The authorization, or `undefined` without a session. */
25
39
  type ReturnType = Authorization | undefined;
package/dist/Actions.js CHANGED
@@ -1,9 +1,4 @@
1
- /**
2
- * Tempo Auth's actions. Each takes an instance from `Auth.create`. Each sign-in flow has its own
3
- * namespace, and the actions every flow shares sit at the top level.
4
- * @module
5
- */
6
- import { Secp256k1 } from 'viem/tempo';
1
+ import * as AccessKey from './internal/accessKey.js';
7
2
  import * as Api from './internal/api.js';
8
3
  import { request } from './internal/request.js';
9
4
  export * as device from './actions/device.js';
@@ -18,7 +13,7 @@ export * as tempoAccount from './actions/tempoAccount.js';
18
13
  *
19
14
  * @example
20
15
  * ```ts
21
- * const authorization = await Actions.authorize(auth, { accessKey })
16
+ * const authorization = await Actions.authorize(auth, { permissions })
22
17
  * if (!authorization) showSignIn()
23
18
  * ```
24
19
  */
@@ -29,15 +24,16 @@ export async function authorize(auth, options) {
29
24
  }));
30
25
  if (!session.user)
31
26
  return undefined;
32
- const privateKey = Secp256k1.randomPrivateKey();
27
+ const key = await AccessKey.resolve(options.accessKey);
33
28
  const response = await Api.post(auth, '/api/authorize', {
34
- json: request(auth, { ...options, privateKey }),
29
+ json: request(auth, {
30
+ accessKey: key.accessKey,
31
+ permissions: options.permissions,
32
+ }),
35
33
  });
36
34
  if (response.status === 401)
37
35
  return undefined;
38
- return Api.authorization(await Api.json(response), {
39
- privateKey,
40
- });
36
+ return Api.authorization(await Api.json(response), key);
41
37
  }
42
38
  /**
43
39
  * Exchanges a refresh token for new tokens. Refresh tokens rotate, so store the new one.
package/dist/Auth.d.ts CHANGED
@@ -1,32 +1,8 @@
1
- /**
2
- * A Tempo Auth instance, and the types its actions take and return. Create one with
3
- * {@link create}, then pass it to each action in `Actions`. Every sign-in returns an
4
- * {@link Authorization}: the user's tokens and a viem access key account.
5
- *
6
- * @example
7
- * ```ts
8
- * import { Actions, Auth } from '@tempoxyz/auth'
9
- * import { Addresses, Expiry, Scopes } from 'viem/tempo'
10
- * import { tempoTestnet } from 'viem/tempo/chains'
11
- *
12
- * const auth = Auth.create({ clientId: 'client_…', url: 'https://auth.tempo.xyz' })
13
- *
14
- * const authorization = await Actions.authorize(auth, {
15
- * accessKey: {
16
- * chainId: tempoTestnet.id,
17
- * expiry: Expiry.days(1),
18
- * limits: [{ limit: 10_000_000n, token: Addresses.pathUsd }],
19
- * scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
20
- * },
21
- * })
22
- * ```
23
- * @module
24
- */
25
- import type { Hex } from 'ox';
26
- import type { KeyAuthorization } from 'ox/tempo';
27
- import type * as Key from './Key.ts';
1
+ import type { Storage } from './Storage.ts';
28
2
  /** A Tempo Auth instance, which every action takes. */
29
3
  export type Auth = {
4
+ /** The chain the app's keys act on, unless an action's `permissions` names another. */
5
+ chainId: number;
30
6
  /** The app's Connect client ID. */
31
7
  clientId: string;
32
8
  /** The app's client secret. Only a backend holds one. */
@@ -37,72 +13,14 @@ export type Auth = {
37
13
  redirectUri?: string | undefined;
38
14
  /** The scopes the app's tokens carry. */
39
15
  scope: string;
40
- /** Where a redirect sign-in keeps its state until Tempo Auth redirects back. */
41
- storage?: Pick<Storage, 'getItem' | 'removeItem' | 'setItem'> | undefined;
16
+ /**
17
+ * Where `Actions.redirect.start` keeps its state until Tempo Auth redirects back. It must keep
18
+ * objects as they are, since a browser key is a `CryptoKey`. Defaults to IndexedDB.
19
+ */
20
+ storage?: Storage | undefined;
42
21
  /** Where Tempo Auth runs, such as `https://auth.tempo.xyz`. */
43
22
  url: string;
44
23
  };
45
- /** What an app's access key may do, as viem's `accessKey.signAuthorization` takes it. */
46
- export type AccessKey = Pick<KeyAuthorization.KeyAuthorization, 'limits' | 'scopes'> & {
47
- /** The user's Tempo Account the key acts for, from `Actions.tempoAccount.list`. Defaults to their first. */
48
- account?: `0x${string}` | undefined;
49
- /** The chain the key acts on, such as `tempoTestnet.id`. */
50
- chainId: number;
51
- /** Unix seconds. viem's `Expiry` builds one, such as `Expiry.days(1)`. */
52
- expiry: number;
53
- };
54
- /** One of the user's Tempo Accounts. */
55
- export type TempoAccount = {
56
- /** The account's address, the same on every Tempo chain. */
57
- address: `0x${string}`;
58
- };
59
- /** What every sign-in returns: the app's tokens for Tempo APIs, with its access key and the user. */
60
- export type Authorization = Tokens & {
61
- /** The user's Tempo Account, as a viem account that signs with the app's access key. */
62
- account: ReturnType<typeof Key.toAccount>;
63
- /** The same key as JSON, for apps that store it and rebuild it with `Key.toAccount`. */
64
- key: Key.Key;
65
- /** The signed-in user, from the ID token. */
66
- user: User;
67
- };
68
- /** The app's tokens for Tempo APIs. */
69
- export type Tokens = {
70
- /** The Bearer token Tempo APIs accept. */
71
- accessToken: string;
72
- /** When the access token expires, in milliseconds. */
73
- expiresAt: number;
74
- /** The OIDC ID token, which names the user. A refresh may omit it. */
75
- idToken?: string | undefined;
76
- /** Exchanges for new tokens through `Actions.refresh`. It rotates on every refresh. */
77
- refreshToken?: string | undefined;
78
- };
79
- /** The signed-in user. */
80
- export type User = {
81
- /** The user's verified email address. */
82
- email: string;
83
- /** The user's ID, the `sub` of every token. */
84
- id: string;
85
- };
86
- /** An OAuth provider's short name, as `Actions.provider.list` lists them. */
87
- export type Provider = 'apple' | 'bitbucket' | 'github' | 'gitlab' | 'google' | 'intuit' | 'linkedin' | 'microsoft' | 'salesforce' | 'slack' | 'xero';
88
- /** A step a sign-in needs before it finishes, such as the SSO connection an address requires. */
89
- export type Step = {
90
- /** The kind of step. */
91
- type: 'sso';
92
- /** Where `Actions.provider.signIn` sends the browser to take the step. */
93
- url: string;
94
- };
95
- /** A sign-in redirect in progress: what the app keeps until Tempo Auth redirects back. */
96
- export type Pending = {
97
- /** The app's new access key, which the callback authorizes. */
98
- privateKey: Hex.Hex;
99
- /** Where Tempo Auth redirects back to, which the code exchange repeats. */
100
- redirectUri: string;
101
- /** The value the callback must echo, so a forged callback is refused. */
102
- state: string;
103
- /** The PKCE verifier that redeems the code. */
104
- verifier: string;
105
- };
106
24
  /** A request Tempo Auth refused, with its status and message. */
107
25
  export declare class RequestError extends Error {
108
26
  readonly name = "Auth.RequestError";
@@ -123,7 +41,9 @@ export declare class RequestError extends Error {
123
41
  */
124
42
  export declare function create(options: create.Options): Auth;
125
43
  export declare namespace create {
126
- type Options = Omit<Auth, 'scope'> & {
44
+ type Options = Omit<Auth, 'chainId' | 'scope'> & {
45
+ /** The chain the app's keys act on, such as `tempoTestnet.id`. Defaults to Tempo mainnet. */
46
+ chainId?: number | undefined;
127
47
  /**
128
48
  * Scopes the app's tokens carry beyond `openid` and `email`, which every instance includes.
129
49
  * Defaults to `offline_access`, for a refresh token.
package/dist/Auth.js CHANGED
@@ -1,3 +1,31 @@
1
+ /**
2
+ * A Tempo Auth instance, which every action takes. Create one with {@link create}, then pass it
3
+ * to each action in `Actions`. Every sign-in returns an `Authorization`: the user's tokens and a
4
+ * viem access key account.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { Actions, Auth } from '@tempoxyz/auth'
9
+ * import { Addresses, Expiry, Scopes } from 'viem/tempo'
10
+ * import { tempoTestnet } from 'viem/tempo/chains'
11
+ *
12
+ * const auth = Auth.create({
13
+ * chainId: tempoTestnet.id,
14
+ * clientId: 'client_…',
15
+ * url: 'https://auth.tempo.xyz',
16
+ * })
17
+ *
18
+ * const authorization = await Actions.authorize(auth, {
19
+ * permissions: {
20
+ * expiry: Expiry.days(1),
21
+ * limits: [{ limit: 10_000_000n, token: Addresses.pathUsd }],
22
+ * scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
23
+ * },
24
+ * })
25
+ * ```
26
+ * @module
27
+ */
28
+ import { tempo } from 'viem/tempo/chains';
1
29
  /** A request Tempo Auth refused, with its status and message. */
2
30
  export class RequestError extends Error {
3
31
  name = 'Auth.RequestError';
@@ -21,6 +49,7 @@ export function create(options) {
21
49
  const scopes = (options.scope ?? 'offline_access').split(' ').filter(Boolean);
22
50
  return {
23
51
  ...options,
52
+ chainId: options.chainId ?? tempo.id,
24
53
  scope: [...new Set(['openid', 'email', ...scopes])].join(' '),
25
54
  };
26
55
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What every sign-in returns: the app's tokens for Tempo APIs, its access key, and the user.
3
+ * @module
4
+ */
5
+ import type * as Key from './Key.ts';
6
+ import type { Tokens } from './Tokens.ts';
7
+ import type { User } from './User.ts';
8
+ /** What every sign-in returns: the app's tokens for Tempo APIs, with its access key and the user. */
9
+ export type Authorization = Tokens & {
10
+ /** The user's Tempo Account, as a viem account that signs with the app's access key. */
11
+ account: ReturnType<typeof Key.toAccount>;
12
+ /** The same key as data, for apps that store it and rebuild it with `Key.toAccount`. */
13
+ key: Key.Key;
14
+ /** The signed-in user, from the ID token. */
15
+ user: User;
16
+ };
17
+ /**
18
+ * Instantiates a {@link Authorization}.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const authorization = Authorization.from({ ...saved, account: Key.toAccount(saved.key) })
23
+ * ```
24
+ */
25
+ export declare function from(value: Authorization): Authorization;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Instantiates a {@link Authorization}.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const authorization = Authorization.from({ ...saved, account: Key.toAccount(saved.key) })
7
+ * ```
8
+ */
9
+ export function from(value) {
10
+ return value;
11
+ }
package/dist/Key.d.ts CHANGED
@@ -1,20 +1,36 @@
1
1
  /**
2
- * An app's access key on a user's Tempo Account, as the app stores it: the key, the account it
3
- * acts for, and the authorization Tempo Auth signed for it.
2
+ * An app's access key on a user's Tempo Account, as the app stores it: the account it acts for,
3
+ * the authorization Tempo Auth signed for it, and the key material the library generated.
4
4
  * @module
5
5
  */
6
- import type { Address, Hex } from 'ox';
6
+ import { type Address, type Hex, type WebCryptoP256 } from 'ox';
7
7
  import { KeyAuthorization } from 'ox/tempo';
8
+ import type { OneOf } from 'viem';
8
9
  import { Account } from 'viem/tempo';
9
- /** A stored access key. It is JSON, so servers and CLIs can keep it in a cookie or a file. */
10
+ /**
11
+ * A stored access key. A browser key is a non-extractable WebCrypto key pair, which IndexedDB
12
+ * keeps without exporting. Elsewhere it is a secp256k1 private key, as JSON for a cookie or a file.
13
+ * A key the app supplied as `accessKey` carries no material.
14
+ */
10
15
  export type Key = {
11
16
  /** The Tempo Account the key acts for. */
12
17
  account: Address.Address;
13
18
  /** The signed authorization, in the node's RPC format. */
14
19
  keyAuthorization: KeyAuthorization.Rpc;
15
- /** The access key's private key. Keep it as secret as a session. */
20
+ } & Material;
21
+ /** The key material the library generated for a sign-in. */
22
+ export type Material = OneOf<{
23
+ /** A non-extractable WebCrypto P256 key pair, in a browser. */
24
+ keyPair: WebCryptoP256.createKeyPair.ReturnType;
25
+ } | {
26
+ /** A secp256k1 private key, outside a browser. Keep it as secret as a session. */
16
27
  privateKey: Hex.Hex;
17
- };
28
+ } | {}>;
29
+ /** A key the app supplied as `accessKey`, which `toAccount` needs to rebuild its account. */
30
+ export declare class AccessKeyRequiredError extends Error {
31
+ readonly name = "Key.AccessKeyRequiredError";
32
+ constructor();
33
+ }
18
34
  /**
19
35
  * Whether the key's authorization has expired, so a new sign-in must authorize a new key.
20
36
  *
@@ -24,9 +40,19 @@ export type Key = {
24
40
  * ```
25
41
  */
26
42
  export declare function expired(key: Key): boolean;
43
+ /**
44
+ * Instantiates a {@link Key}.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const key = Key.from(JSON.parse(saved).key)
49
+ * ```
50
+ */
51
+ export declare function from(value: Key): Key;
27
52
  /**
28
53
  * Returns the key as a viem access key account acting for its Tempo Account. viem attaches the
29
- * authorization to transactions until the chain registers the key.
54
+ * authorization to transactions until the chain registers the key. A key the app supplied signs
55
+ * with that `accessKey`. Throws {@link AccessKeyRequiredError} when neither holds key material.
30
56
  *
31
57
  * @example
32
58
  * ```ts
@@ -36,4 +62,10 @@ export declare function expired(key: Key): boolean;
36
62
  * await client.token.transferSync({ account, amount, to, token })
37
63
  * ```
38
64
  */
39
- export declare function toAccount(key: Key): Account.AccessKeyAccount;
65
+ export declare function toAccount(key: Key, options?: toAccount.Options): Account.AccessKeyAccount;
66
+ export declare namespace toAccount {
67
+ type Options = {
68
+ /** The key the app supplied to the sign-in, for a key that carries no material. */
69
+ accessKey?: Account.Account | undefined;
70
+ };
71
+ }