@tempoxyz/auth 0.0.0-pr-63.sha-3c03e9f → 0.0.0-pr-66.sha-480b2a0
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/README.md +22 -263
- package/dist/Actions.d.ts +5 -5
- package/dist/Auth.d.ts +70 -4
- package/dist/Auth.js +61 -35
- package/dist/Authorization.d.ts +8 -5
- package/dist/Decorator.d.ts +111 -0
- package/dist/Decorator.js +98 -0
- package/dist/Key.d.ts +17 -4
- package/dist/Key.js +8 -3
- package/dist/Permissions.d.ts +8 -7
- package/dist/Permissions.js +1 -1
- package/dist/actions/device.d.ts +12 -9
- package/dist/actions/device.js +12 -3
- package/dist/actions/email.d.ts +5 -5
- package/dist/actions/redirect.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal/accessKey.d.ts +1 -1
- package/dist/internal/account.d.ts +13 -0
- package/dist/internal/account.js +2 -0
- package/dist/internal/api.d.ts +4 -3
- package/dist/internal/api.js +2 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
# @tempoxyz/auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Signs users in to Tempo through [Tempo Auth](../../docs/README.md), and returns their tokens for Tempo APIs and a [Viem](https://viem.sh) access key account on their Tempo Account.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import {
|
|
8
|
+
import { Auth, tempoAuth } from '@tempoxyz/auth'
|
|
9
9
|
import { createClient, Expiry, Scopes } from 'viem/tempo'
|
|
10
|
-
import { tempo, tempoTestnet } from 'viem/tempo/chains'
|
|
11
10
|
import * as Tokens from 'viem/tokens'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const client = createClient({ account: Auth.account() }).extend(
|
|
13
|
+
tempoAuth({ clientId: 'client_…' }),
|
|
14
|
+
)
|
|
15
|
+
const ousd = Tokens.ousd(client.chain.id).address
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
clientId: 'client_…',
|
|
19
|
-
url: 'https://auth.tempo.xyz',
|
|
20
|
-
})
|
|
21
|
-
const client = createClient({
|
|
22
|
-
chain: tempoTestnet,
|
|
23
|
-
feeToken: ousd,
|
|
24
|
-
})
|
|
17
|
+
// 1. Email the user a sign-in code.
|
|
18
|
+
await client.auth.email.sendCode({ email: 'user@example.com' })
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
// 2. Sign in with the code, and authorize an access key, which becomes the client's account.
|
|
21
|
+
const result = await client.auth.email.signIn({
|
|
22
|
+
code: '123456',
|
|
23
|
+
email: 'user@example.com',
|
|
27
24
|
permissions: {
|
|
28
25
|
expiry: Expiry.days(1),
|
|
29
26
|
limits: [{ limit: 10_000_000n, token: ousd }],
|
|
@@ -31,264 +28,26 @@ const authorization = await Actions.authorize(auth, {
|
|
|
31
28
|
},
|
|
32
29
|
})
|
|
33
30
|
|
|
31
|
+
if ('step' in result) throw new Error('This address signs in through SSO.')
|
|
32
|
+
|
|
33
|
+
// 3. Send a transaction with the access key.
|
|
34
34
|
await client.token.transferSync({
|
|
35
|
-
account: authorization!.account,
|
|
36
35
|
amount: 10_000n,
|
|
37
36
|
to: '0x…',
|
|
38
37
|
token: ousd,
|
|
39
38
|
})
|
|
40
39
|
```
|
|
41
40
|
|
|
41
|
+
Every sign-in returns the same `Authorization` and sets the client's account. The client sends with the access key, and the `accessToken` calls Tempo APIs. Each action is also available without the extension, as `Actions`.
|
|
42
|
+
|
|
42
43
|
## Install
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
46
|
pnpm add @tempoxyz/auth viem
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
- **Register the app.** Create a WorkOS Connect app in the Tempo Issuer environment, and register every redirect URI it uses. Its `client_id` is the `clientId` that `Auth.create` takes. An app with a backend also receives a client secret.
|
|
51
|
-
- **Allow the app's origin.** Tempo Auth allows the origins of every first-party Connect app's `http(s)` redirect URIs, within about three minutes of a change. A mobile app has no origin, and registers its custom scheme as its redirect URI instead.
|
|
52
|
-
|
|
53
|
-
The CLI device grant needs neither. It signs in as Tempo Auth's own `tempo-cli` app.
|
|
54
|
-
|
|
55
|
-
## Access keys
|
|
56
|
-
|
|
57
|
-
Each sign-in creates a key for the app and requests that Tempo Auth authorize it on the user's Tempo Account. The optional `permissions` option specifies what the key may do, with viem's `Expiry` and `Scopes` helpers. Tempo Auth fills every field it leaves out with the headless ceiling:
|
|
58
|
-
|
|
59
|
-
| Field | Meaning |
|
|
60
|
-
| --------- | ----------------------------------------------------------------------------------------------------------- |
|
|
61
|
-
| `chainId` | The chain the key acts on. Defaults to the instance's `chainId` |
|
|
62
|
-
| `expiry` | When the key expires, in Unix seconds, such as `Expiry.days(1)`. Defaults to a year |
|
|
63
|
-
| `limits` | One-time spending limits per token, such as `[{ limit: 10_000_000n, token }]`. Defaults to about $1,000 of each token the `scopes` call, or of OUSD |
|
|
64
|
-
| `scopes` | The calls the key may make, such as `[Scopes.tip20(token).transfer()]`. Defaults to transfers of each token the key limits |
|
|
65
|
-
|
|
66
|
-
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 uses a secp256k1 key, which it stores as a string.
|
|
67
|
-
|
|
68
|
-
To authorize a key the app already holds, pass it as `accessKey`, a viem account. The library never reads its private key. Its `access` names the Tempo Account, which defaults to the user's first:
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
const accessKey = Account.fromWebCryptoP256(keyPair, { access: tempoAccount })
|
|
72
|
-
await Actions.authorize(auth, { accessKey })
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Flows without a Tempo Auth page (`authorize`, `email.signIn`) must stay within Tempo Auth's ceiling: about $1,000 per token, for at most a year. [Permissions](../../docs/permissions.md) explains the defaults and the bounds on each flow. 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.
|
|
76
|
-
|
|
77
|
-
The returned `account` attaches the key's authorization to its first transaction, which registers the key on chain.
|
|
78
|
-
|
|
79
|
-
## Flows
|
|
80
|
-
|
|
81
|
-
### Your own sign-in UI
|
|
82
|
-
|
|
83
|
-
For a browser app that renders its own email form. The page calls Tempo Auth with the user's Tempo Auth session, so it works across every app on the same site.
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
// A user already signed in to Tempo Auth, from any app, needs no form.
|
|
87
|
-
const authorization = await Actions.authorize(auth)
|
|
88
|
-
|
|
89
|
-
// Otherwise, sign in with an emailed code.
|
|
90
|
-
await Actions.email.sendCode(auth, { email })
|
|
91
|
-
const result = await Actions.email.signIn(auth, { code, email })
|
|
92
|
-
|
|
93
|
-
// An address that requires SSO continues at its connection, and returns to this page.
|
|
94
|
-
if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
To offer OAuth providers such as Google, list the ones Tempo Auth enables and send the browser to one. It returns to the current page signed in, where `authorize` finishes.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const providers = await Actions.provider.list(auth) // [{ id: 'google', name: 'Google' }]
|
|
101
|
-
Actions.provider.signIn(auth, { provider: 'google' })
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Tempo Auth's pages
|
|
105
|
-
|
|
106
|
-
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.
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
await Actions.redirect.start(auth)
|
|
110
|
-
|
|
111
|
-
// On the page the browser returns to, and on every load:
|
|
112
|
-
const authorization = await Actions.redirect.complete(auth)
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`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.
|
|
116
|
-
|
|
117
|
-
### A mobile app
|
|
118
|
-
|
|
119
|
-
For a React Native or Expo app. It calls the same actions as a browser app, with no `Origin`, and opens Tempo Auth's pages in the system browser, which returns to the app's custom scheme. Import `react-native-get-random-values` first, since keys need `crypto.getRandomValues`.
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
import * as WebBrowser from 'expo-web-browser'
|
|
123
|
-
|
|
124
|
-
const auth = Auth.create({
|
|
125
|
-
clientId: 'client_…',
|
|
126
|
-
redirectUri: 'my-app://callback',
|
|
127
|
-
url: 'https://auth.tempo.xyz',
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
// Your own sign-in UI, as in a browser.
|
|
131
|
-
await Actions.email.sendCode(auth, { email })
|
|
132
|
-
const result = await Actions.email.signIn(auth, { code, email })
|
|
133
|
-
|
|
134
|
-
// Or Tempo Auth's pages, which also offer Google and SSO.
|
|
135
|
-
const { pending, url } = await Actions.redirect.createUrl(auth)
|
|
136
|
-
const session = await WebBrowser.openAuthSessionAsync(url, auth.redirectUri)
|
|
137
|
-
if (session.type === 'success')
|
|
138
|
-
authorization = await Actions.redirect.handleCallback(auth, {
|
|
139
|
-
pending,
|
|
140
|
-
url: session.url,
|
|
141
|
-
})
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Keep the authorization in the device's secure store, as [Keep a sign-in](#keep-a-sign-in) describes.
|
|
145
|
-
|
|
146
|
-
### A backend
|
|
147
|
-
|
|
148
|
-
For an app whose server holds a client secret. The server builds the URL, keeps the pending state until the callback, and redeems the code itself.
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
const auth = Auth.create({ clientId, clientSecret, url })
|
|
152
|
-
|
|
153
|
-
// GET /auth/start
|
|
154
|
-
const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
155
|
-
redirectUri: 'https://app.example/auth/callback',
|
|
156
|
-
})
|
|
157
|
-
// Keep `pending` in an encrypted, HttpOnly cookie, then redirect to `url`.
|
|
158
|
-
|
|
159
|
-
// GET /auth/callback
|
|
160
|
-
const authorization = await Actions.redirect.handleCallback(auth, {
|
|
161
|
-
pending,
|
|
162
|
-
url: request.url,
|
|
163
|
-
})
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### A CLI
|
|
167
|
-
|
|
168
|
-
For a command-line tool. The user approves a code in any browser, and the CLI waits.
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
const device = await Actions.device.authorize(auth, {
|
|
172
|
-
device: os.hostname(),
|
|
173
|
-
})
|
|
174
|
-
console.log(
|
|
175
|
-
`Open ${device.verificationUriComplete} and confirm ${device.userCode}.`,
|
|
176
|
-
)
|
|
177
|
-
const authorization = await device.wait()
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## More Tempo Accounts
|
|
181
|
-
|
|
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:
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
const tempoAccount = await Actions.tempoAccount.create(auth)
|
|
186
|
-
const accounts = await Actions.tempoAccount.list(auth) // the default first
|
|
187
|
-
|
|
188
|
-
await Actions.authorize(auth, {
|
|
189
|
-
accessKey: Account.fromWebCryptoP256(keyPair, {
|
|
190
|
-
access: tempoAccount.address,
|
|
191
|
-
}),
|
|
192
|
-
})
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Keep a sign-in
|
|
196
|
-
|
|
197
|
-
`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`.
|
|
198
|
-
|
|
199
|
-
- **Browser:** the key is a `CryptoKey` pair, which IndexedDB stores without exporting it.
|
|
200
|
-
- **Elsewhere:** the key is a secp256k1 private key, as JSON. Store it as securely as a session.
|
|
201
|
-
- **Supplied key:** the key holds no material. Pass the `accessKey` again, as `Key.toAccount(saved.key, { accessKey })`.
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { Actions, Key } from '@tempoxyz/auth'
|
|
205
|
-
|
|
206
|
-
if (Key.expired(saved.key)) signInAgain()
|
|
207
|
-
|
|
208
|
-
const account = Key.toAccount(saved.key)
|
|
209
|
-
const tokens = await Actions.refresh(auth, {
|
|
210
|
-
refreshToken: saved.refreshToken,
|
|
211
|
-
})
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Refresh tokens rotate, so save the new tokens over the old ones after each refresh, such as `{ ...saved, ...tokens }`.
|
|
215
|
-
|
|
216
|
-
## Errors
|
|
217
|
-
|
|
218
|
-
A request Tempo Auth refuses throws `Auth.RequestError`. It carries the OAuth error code as `code`, and `message` states the fault and its fix. `reason`, `field`, and `meta` let an app branch on the refusal and read the values it compared. [Errors](../../docs/errors.md) lists every `reason`.
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
try {
|
|
222
|
-
await Actions.authorize(auth, { permissions })
|
|
223
|
-
} catch (error) {
|
|
224
|
-
if (error instanceof Auth.RequestError && error.reason === 'limit_exceeds_maximum')
|
|
225
|
-
showMaximum(error.meta?.maximum) // in base units
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Reference
|
|
230
|
-
|
|
231
|
-
### `Auth.create(options)`
|
|
232
|
-
|
|
233
|
-
| Option | Meaning |
|
|
234
|
-
| -------------- | ----------------------------------------------------------------------------------------------- |
|
|
235
|
-
| `clientId` | The app's Connect client ID. Required |
|
|
236
|
-
| `chainId` | The chain the app's keys act on, such as `tempoTestnet.id`. Defaults to Tempo mainnet |
|
|
237
|
-
| `url` | Where Tempo Auth runs, such as `https://auth.tempo.xyz`. Required |
|
|
238
|
-
| `clientSecret` | The app's client secret. Only a backend holds one |
|
|
239
|
-
| `redirectUri` | Where Tempo Auth redirects back to. Defaults to the page's origin |
|
|
240
|
-
| `scope` | Scopes beyond `openid` and `email`, which every instance includes. Defaults to `offline_access` |
|
|
241
|
-
| `storage` | Where `redirect.start` keeps its state, without serializing it. Defaults to IndexedDB |
|
|
242
|
-
| `fetch` | Calls Tempo Auth. Defaults to the global `fetch` |
|
|
243
|
-
|
|
244
|
-
The actions call version 1 of Tempo Auth's JSON API, under `/v1`. [API versions](../../docs/versioning.md) describes versions and compatibility.
|
|
245
|
-
|
|
246
|
-
### `Actions`
|
|
247
|
-
|
|
248
|
-
Each sign-in flow has its own namespace. The actions every flow shares sit at the top level. Every action that creates a key also takes optional `accessKey` and `permissions`.
|
|
249
|
-
|
|
250
|
-
| Action | Returns |
|
|
251
|
-
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
252
|
-
| `authorize(auth)` | An `Authorization` for a user already signed in, or `undefined` |
|
|
253
|
-
| `refresh(auth, { refreshToken })` | New `Tokens` |
|
|
254
|
-
| `signOut(auth)` | Nothing. The user's Tempo Auth session ends |
|
|
255
|
-
| `device.authorize(auth, { device })` | The code to show and a `wait()` that resolves with an `Authorization` |
|
|
256
|
-
| `email.sendCode(auth, { email })` | Nothing. Tempo Auth emails a code |
|
|
257
|
-
| `email.signIn(auth, { code, email })` | An `Authorization`, or the `step` the address requires first |
|
|
258
|
-
| `provider.list(auth)` | The OAuth providers Tempo Auth offers, as `{ id, name }` |
|
|
259
|
-
| `provider.signIn(auth, { provider } \| { url })` | Nothing. The browser leaves for the provider or SSO connection |
|
|
260
|
-
| `redirect.complete(auth, { accessKey })` | The `Authorization` when the page is a redirect's callback, or `undefined` |
|
|
261
|
-
| `redirect.createUrl(auth, { redirectUri })` | The `url` to send the browser to, and the `pending` state to keep |
|
|
262
|
-
| `redirect.handleCallback(auth, { pending, url })` | The `Authorization` for a redirect's callback |
|
|
263
|
-
| `redirect.start(auth)` | Nothing. The browser leaves for Tempo Auth's pages |
|
|
264
|
-
| `tempoAccount.create(auth)` | Another Tempo Account for the signed-in user |
|
|
265
|
-
| `tempoAccount.list(auth)` | The signed-in user's Tempo Accounts, the default first |
|
|
266
|
-
|
|
267
|
-
### `Authorization`
|
|
268
|
-
|
|
269
|
-
| Field | Meaning |
|
|
270
|
-
| -------------- | -------------------------------------------------------------------------------- |
|
|
271
|
-
| `account` | The user's Tempo Account, as a viem account that signs with the app's access key |
|
|
272
|
-
| `key` | The same key as data, for `Key.toAccount` |
|
|
273
|
-
| `accessToken` | The Bearer token Tempo APIs accept |
|
|
274
|
-
| `expiresAt` | When the access token expires, in milliseconds |
|
|
275
|
-
| `idToken` | Optional: the OIDC ID token, which names the user |
|
|
276
|
-
| `refreshToken` | Optional: exchanges for new tokens through `refresh`. It rotates each time |
|
|
277
|
-
| `user` | The user's `email` and `id`, the `sub` of every token |
|
|
278
|
-
|
|
279
|
-
### Types
|
|
280
|
-
|
|
281
|
-
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:
|
|
282
|
-
|
|
283
|
-
```ts
|
|
284
|
-
import { Permissions } from '@tempoxyz/auth'
|
|
285
|
-
|
|
286
|
-
const permissions = Permissions.from({ expiry: Expiry.days(1), limits, scopes })
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### `Key`
|
|
49
|
+
## Documentation
|
|
290
50
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
| `expired(key)` | Whether the key's authorization has expired |
|
|
51
|
+
- [Guides](docs/guides/README.md): each sign-in flow, access keys, sessions, errors, and Viem.
|
|
52
|
+
- [API reference](docs/api/README.md): every action, function, and type.
|
|
53
|
+
- [Tempo Auth](../../docs/README.md): the service the package calls, over HTTP.
|
package/dist/Actions.d.ts
CHANGED
|
@@ -24,19 +24,19 @@ export * as tempoAccount from './actions/tempoAccount.ts';
|
|
|
24
24
|
* if (!authorization) showSignIn()
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
export declare function authorize(auth: Auth, options?: authorize.Options): Promise<authorize.ReturnType
|
|
27
|
+
export declare function authorize<const chainId extends number = number>(auth: Auth, options?: authorize.Options<chainId>): Promise<authorize.ReturnType<chainId>>;
|
|
28
28
|
export declare namespace authorize {
|
|
29
|
-
type Options = {
|
|
29
|
+
type Options<chainId extends number = number> = {
|
|
30
30
|
/**
|
|
31
|
-
* The key to authorize, as a
|
|
31
|
+
* The key to authorize, as a Viem account. Its `access` names the Tempo Account, which
|
|
32
32
|
* defaults to the user's first. Defaults to a new key.
|
|
33
33
|
*/
|
|
34
34
|
accessKey?: Account.Account | undefined;
|
|
35
35
|
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
36
|
-
permissions?: Permissions | undefined;
|
|
36
|
+
permissions?: Permissions<chainId> | undefined;
|
|
37
37
|
};
|
|
38
38
|
/** The authorization, or `undefined` without a session. */
|
|
39
|
-
type ReturnType = Authorization | undefined;
|
|
39
|
+
type ReturnType<chainId extends number = number> = Authorization<chainId> | undefined;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Exchanges a refresh token for new tokens. Refresh tokens rotate, so store the new one.
|
package/dist/Auth.d.ts
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
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 { Expiry, Scopes } from 'viem/tempo'
|
|
10
|
+
* import { tempo } from 'viem/tempo/chains'
|
|
11
|
+
* import * as Tokens from 'viem/tokens'
|
|
12
|
+
*
|
|
13
|
+
* // OUSD has its mainnet address on every Tempo chain.
|
|
14
|
+
* const ousd = Tokens.ousd(tempo.id).address
|
|
15
|
+
*
|
|
16
|
+
* const auth = Auth.create({ clientId: 'client_…', dev: true })
|
|
17
|
+
*
|
|
18
|
+
* const authorization = await Actions.authorize(auth, {
|
|
19
|
+
* permissions: {
|
|
20
|
+
* expiry: Expiry.days(1),
|
|
21
|
+
* limits: [{ limit: 10_000_000n, token: ousd }],
|
|
22
|
+
* scopes: [Scopes.tip20(ousd).transfer()],
|
|
23
|
+
* },
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
import type { Account as ViemAccount } from 'viem/tempo';
|
|
29
|
+
import * as AccountState from './internal/account.ts';
|
|
1
30
|
import type { Storage } from './Storage.ts';
|
|
31
|
+
/**
|
|
32
|
+
* The account of a client extended with `tempoAuth`: an access key account that each sign-in sets
|
|
33
|
+
* and sign-out empties. Reading it while signed out throws {@link SignedOutError}.
|
|
34
|
+
*/
|
|
35
|
+
export type Account = ViemAccount.AccessKeyAccount & {
|
|
36
|
+
/** What the account signs as, which only the `tempoAuth` extension reads. */
|
|
37
|
+
readonly [AccountState.key]: AccountState.State;
|
|
38
|
+
};
|
|
2
39
|
/** A Tempo Auth instance, which every action takes. */
|
|
3
40
|
export type Auth = {
|
|
4
41
|
/** The chain the app's keys act on, unless an action's `permissions` names another. */
|
|
@@ -49,25 +86,54 @@ export declare namespace RequestError {
|
|
|
49
86
|
uri?: string | undefined;
|
|
50
87
|
};
|
|
51
88
|
/** Each refusal's stable name. `docs/errors.md` explains each one. */
|
|
52
|
-
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_public_key' | 'limit_exceeds_maximum' | 'malformed_body' | 'missing_code_challenge' | 'missing_key' | 'missing_recipients' | 'periodic_limit' | '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'
|
|
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_public_key' | 'limit_exceeds_maximum' | 'malformed_body' | 'missing_code_challenge' | 'missing_key' | 'missing_recipients' | 'periodic_limit' | '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';
|
|
90
|
+
}
|
|
91
|
+
/** A client's {@link Account} read before a sign-in set it, or after sign-out emptied it. */
|
|
92
|
+
export declare class SignedOutError extends Error {
|
|
93
|
+
readonly name = "Auth.SignedOutError";
|
|
94
|
+
constructor();
|
|
53
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates the account of a client extended with `tempoAuth`. It holds no key until a sign-in sets
|
|
98
|
+
* it, and reading it before then throws {@link SignedOutError}, so a transaction fails before it
|
|
99
|
+
* reaches the node. Each client needs its own, since the account signs as one user.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const client = createClient({
|
|
104
|
+
* account: Auth.account(),
|
|
105
|
+
* chain: tempoTestnet,
|
|
106
|
+
* }).extend(tempoAuth({ clientId: 'client_…' }))
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export declare function account(): Account;
|
|
54
110
|
/**
|
|
55
111
|
* Creates a Tempo Auth instance.
|
|
56
112
|
*
|
|
57
113
|
* @example
|
|
58
114
|
* ```ts
|
|
59
|
-
* const auth = Auth.create({ clientId: 'client_…'
|
|
115
|
+
* const auth = Auth.create({ clientId: 'client_…' })
|
|
60
116
|
* ```
|
|
61
117
|
*/
|
|
62
118
|
export declare function create(options: create.Options): Auth;
|
|
63
119
|
export declare namespace create {
|
|
64
|
-
type Options = Omit<Auth, 'chainId' | 'scope'> & {
|
|
65
|
-
/**
|
|
120
|
+
type Options = Omit<Auth, 'chainId' | 'scope' | 'url'> & {
|
|
121
|
+
/**
|
|
122
|
+
* The chain the app's keys act on, such as `client.chain.id`. Defaults to Tempo mainnet, or to
|
|
123
|
+
* Tempo testnet with `dev`.
|
|
124
|
+
*/
|
|
66
125
|
chainId?: number | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* Whether to use Tempo Auth's development deployment, `https://auth.tehq.dev`, with keys on
|
|
128
|
+
* Tempo testnet unless `chainId` names another chain. Defaults to `false`.
|
|
129
|
+
*/
|
|
130
|
+
dev?: boolean | undefined;
|
|
67
131
|
/**
|
|
68
132
|
* Scopes the app's tokens carry beyond `openid` and `email`, which every instance includes.
|
|
69
133
|
* Defaults to `offline_access`, for a refresh token.
|
|
70
134
|
*/
|
|
71
135
|
scope?: string | undefined;
|
|
136
|
+
/** Where Tempo Auth runs. Defaults to `https://auth.tempo.xyz`, or `https://auth.tehq.dev` with `dev`. */
|
|
137
|
+
url?: string | undefined;
|
|
72
138
|
};
|
|
73
139
|
}
|
package/dist/Auth.js
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
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 { Expiry, Scopes } from 'viem/tempo'
|
|
10
|
-
* import { tempo, tempoTestnet } from 'viem/tempo/chains'
|
|
11
|
-
* import * as Tokens from 'viem/tokens'
|
|
12
|
-
*
|
|
13
|
-
* const ousd = Tokens.ousd(tempo.id).address
|
|
14
|
-
*
|
|
15
|
-
* const auth = Auth.create({
|
|
16
|
-
* chainId: tempoTestnet.id,
|
|
17
|
-
* clientId: 'client_…',
|
|
18
|
-
* url: 'https://auth.tempo.xyz',
|
|
19
|
-
* })
|
|
20
|
-
*
|
|
21
|
-
* const authorization = await Actions.authorize(auth, {
|
|
22
|
-
* permissions: {
|
|
23
|
-
* expiry: Expiry.days(1),
|
|
24
|
-
* limits: [{ limit: 10_000_000n, token: ousd }],
|
|
25
|
-
* scopes: [Scopes.tip20(ousd).transfer()],
|
|
26
|
-
* },
|
|
27
|
-
* })
|
|
28
|
-
* ```
|
|
29
|
-
* @module
|
|
30
|
-
*/
|
|
31
|
-
import { tempo } from 'viem/tempo/chains';
|
|
1
|
+
import { tempo, tempoTestnet } from 'viem/tempo/chains';
|
|
2
|
+
import * as AccountState from './internal/account.js';
|
|
32
3
|
/** A request Tempo Auth refused, with its status and message. */
|
|
33
4
|
export class RequestError extends Error {
|
|
34
5
|
name = 'Auth.RequestError';
|
|
@@ -54,20 +25,75 @@ export class RequestError extends Error {
|
|
|
54
25
|
this.uri = options.uri;
|
|
55
26
|
}
|
|
56
27
|
}
|
|
28
|
+
/** A client's {@link Account} read before a sign-in set it, or after sign-out emptied it. */
|
|
29
|
+
export class SignedOutError extends Error {
|
|
30
|
+
name = 'Auth.SignedOutError';
|
|
31
|
+
constructor() {
|
|
32
|
+
super("The client's account is signed out. Sign in through `client.auth` before sending.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates the account of a client extended with `tempoAuth`. It holds no key until a sign-in sets
|
|
37
|
+
* it, and reading it before then throws {@link SignedOutError}, so a transaction fails before it
|
|
38
|
+
* reaches the node. Each client needs its own, since the account signs as one user.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const client = createClient({
|
|
43
|
+
* account: Auth.account(),
|
|
44
|
+
* chain: tempoTestnet,
|
|
45
|
+
* }).extend(tempoAuth({ clientId: 'client_…' }))
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function account() {
|
|
49
|
+
const state = { account: undefined };
|
|
50
|
+
// Viem reads a client's account only when an action runs, through property reads, `in` checks,
|
|
51
|
+
// and spreads, so each trap reads through to the account the latest sign-in set.
|
|
52
|
+
const proxy = new Proxy({}, {
|
|
53
|
+
get(_, key) {
|
|
54
|
+
if (key === AccountState.key)
|
|
55
|
+
return state;
|
|
56
|
+
if (state.account)
|
|
57
|
+
return Reflect.get(state.account, key);
|
|
58
|
+
// Promise resolution, serialization, and inspection probe these, which must not throw.
|
|
59
|
+
if (typeof key === 'symbol' || key === 'then' || key === 'toJSON')
|
|
60
|
+
return undefined;
|
|
61
|
+
throw new SignedOutError();
|
|
62
|
+
},
|
|
63
|
+
getOwnPropertyDescriptor(_, key) {
|
|
64
|
+
if (!state.account)
|
|
65
|
+
return undefined;
|
|
66
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(state.account, key);
|
|
67
|
+
// A proxy may report a property its empty target lacks only as configurable.
|
|
68
|
+
return descriptor && { ...descriptor, configurable: true };
|
|
69
|
+
},
|
|
70
|
+
has: (_, key) => key === AccountState.key ||
|
|
71
|
+
(state.account !== undefined && Reflect.has(state.account, key)),
|
|
72
|
+
ownKeys: () => (state.account ? Reflect.ownKeys(state.account) : []),
|
|
73
|
+
});
|
|
74
|
+
return proxy;
|
|
75
|
+
}
|
|
57
76
|
/**
|
|
58
77
|
* Creates a Tempo Auth instance.
|
|
59
78
|
*
|
|
60
79
|
* @example
|
|
61
80
|
* ```ts
|
|
62
|
-
* const auth = Auth.create({ clientId: 'client_…'
|
|
81
|
+
* const auth = Auth.create({ clientId: 'client_…' })
|
|
63
82
|
* ```
|
|
64
83
|
*/
|
|
65
84
|
export function create(options) {
|
|
85
|
+
const { chainId, dev, scope = 'offline_access', url, ...rest } = options;
|
|
66
86
|
// Every authorization reads the user from the ID token, which needs `openid` and `email`.
|
|
67
|
-
const scopes =
|
|
87
|
+
const scopes = scope.split(' ').filter(Boolean);
|
|
68
88
|
return {
|
|
69
|
-
...
|
|
70
|
-
chainId:
|
|
89
|
+
...rest,
|
|
90
|
+
chainId: chainId ?? (dev ? tempoTestnet.id : tempo.id),
|
|
71
91
|
scope: [...new Set(['openid', 'email', ...scopes])].join(' '),
|
|
92
|
+
url: url ?? (dev ? urls.dev : urls.production),
|
|
72
93
|
};
|
|
73
94
|
}
|
|
95
|
+
/** Where Tempo Auth runs unless `create` receives a `url`. */
|
|
96
|
+
const urls = {
|
|
97
|
+
dev: 'https://auth.tehq.dev',
|
|
98
|
+
production: 'https://auth.tempo.xyz',
|
|
99
|
+
};
|
package/dist/Authorization.d.ts
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
import type * as Key from './Key.ts';
|
|
6
6
|
import type { Tokens } from './Tokens.ts';
|
|
7
7
|
import type { User } from './User.ts';
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* What every sign-in returns: the app's tokens for Tempo APIs, with its access key and the user.
|
|
10
|
+
* `chainId` is the key's chain, as a literal type when the sign-in's `permissions` named one.
|
|
11
|
+
*/
|
|
12
|
+
export type Authorization<chainId extends number = number> = Tokens & {
|
|
13
|
+
/** The user's Tempo Account, as a Viem account that signs with the app's access key. */
|
|
11
14
|
account: ReturnType<typeof Key.toAccount>;
|
|
12
15
|
/** The same key as data, for apps that store it and rebuild it with `Key.toAccount`. */
|
|
13
|
-
key: Key.Key
|
|
16
|
+
key: Key.Key<chainId>;
|
|
14
17
|
/** The signed-in user, from the ID token. */
|
|
15
18
|
user: User;
|
|
16
19
|
};
|
|
@@ -22,4 +25,4 @@ export type Authorization = Tokens & {
|
|
|
22
25
|
* const authorization = Authorization.from({ ...saved, account: Key.toAccount(saved.key) })
|
|
23
26
|
* ```
|
|
24
27
|
*/
|
|
25
|
-
export declare function from(value: Authorization): Authorization
|
|
28
|
+
export declare function from<chainId extends number = number>(value: Authorization<chainId>): Authorization<chainId>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tempoAuth`, a Viem client extension that adds Tempo Auth's actions as `client.auth`. It reads the
|
|
3
|
+
* chain from the client, and sets the client's {@link Auth.Account} at each sign-in, so the client
|
|
4
|
+
* sends with the signed-in user's access key.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { Auth, tempoAuth } from '@tempoxyz/auth'
|
|
9
|
+
* import { createClient } from 'viem/tempo'
|
|
10
|
+
* import { tempoTestnet } from 'viem/tempo/chains'
|
|
11
|
+
*
|
|
12
|
+
* const client = createClient({
|
|
13
|
+
* account: Auth.account(),
|
|
14
|
+
* chain: tempoTestnet,
|
|
15
|
+
* }).extend(tempoAuth({ clientId: 'client_…' }))
|
|
16
|
+
*
|
|
17
|
+
* await client.auth.email.signIn({ code, email })
|
|
18
|
+
* await client.token.transferSync({ amount, to, token })
|
|
19
|
+
* ```
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type { Chain, Client, Transport } from 'viem';
|
|
23
|
+
import * as Actions from './Actions.ts';
|
|
24
|
+
import * as Auth from './Auth.ts';
|
|
25
|
+
import type { Authorization } from './Authorization.ts';
|
|
26
|
+
import * as Key from './Key.ts';
|
|
27
|
+
/** A client created without `account: Auth.account()`, which the extension sets at sign-in. */
|
|
28
|
+
export declare class AccountRequiredError extends Error {
|
|
29
|
+
readonly name = "Decorator.AccountRequiredError";
|
|
30
|
+
constructor();
|
|
31
|
+
}
|
|
32
|
+
/** A client created without a chain, which the extension reads the instance's `chainId` from. */
|
|
33
|
+
export declare class ChainRequiredError extends Error {
|
|
34
|
+
readonly name = "Decorator.ChainRequiredError";
|
|
35
|
+
constructor();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns a Viem client extension that adds Tempo Auth's actions as `client.auth`. Each member calls
|
|
39
|
+
* the action of the same name in `Actions` with one instance, whose `chainId` is the client's chain.
|
|
40
|
+
* Each sign-in sets the client's account to the authorization's, and `signOut` empties it. Throws
|
|
41
|
+
* {@link ChainRequiredError} for a client without a chain, and {@link AccountRequiredError} for one
|
|
42
|
+
* whose account is not an `Auth.account()`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const client = createClient({
|
|
47
|
+
* account: Auth.account(),
|
|
48
|
+
* chain: tempoTestnet,
|
|
49
|
+
* }).extend(tempoAuth({ clientId: 'client_…' }))
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function tempoAuth(options: tempoAuth.Options): <transport extends Transport, chain extends Chain>(client: Client<transport, chain, Auth.Account>) => tempoAuth.Decorator<chain['id']>;
|
|
53
|
+
export declare namespace tempoAuth {
|
|
54
|
+
/** `Auth.create`'s options, without `chainId`, which the extension reads from the client. */
|
|
55
|
+
type Options = Omit<Auth.create.Options, 'chainId'>;
|
|
56
|
+
/**
|
|
57
|
+
* What the extension adds to a client on the chain `chainId`. Each member's `permissions.chainId`
|
|
58
|
+
* defaults to `chainId`, and an override names another chain.
|
|
59
|
+
*/
|
|
60
|
+
type Decorator<chainId extends number> = {
|
|
61
|
+
/** Tempo Auth's actions, with the extension's instance. */
|
|
62
|
+
auth: {
|
|
63
|
+
/** `Actions.authorize`, which sets the client's account. */
|
|
64
|
+
authorize: <const id extends number = chainId>(options?: Actions.authorize.Options<id>) => Promise<Actions.authorize.ReturnType<id>>;
|
|
65
|
+
device: {
|
|
66
|
+
/** `Actions.device.authorize`, whose `wait` sets the client's account. */
|
|
67
|
+
authorize: <const id extends number = chainId>(options?: Actions.device.authorize.Options<id>) => Promise<Actions.device.authorize.ReturnType<id>>;
|
|
68
|
+
};
|
|
69
|
+
email: {
|
|
70
|
+
/** `Actions.email.sendCode`. */
|
|
71
|
+
sendCode: (options: Actions.email.sendCode.Options) => Promise<void>;
|
|
72
|
+
/** `Actions.email.signIn`, which sets the client's account unless it returns a step. */
|
|
73
|
+
signIn: <const id extends number = chainId>(options: Actions.email.signIn.Options<id>) => Promise<Actions.email.signIn.ReturnType<id>>;
|
|
74
|
+
};
|
|
75
|
+
/** The instance the members call `Actions` with. */
|
|
76
|
+
instance: Auth.Auth;
|
|
77
|
+
provider: {
|
|
78
|
+
/** `Actions.provider.list`. */
|
|
79
|
+
list: () => Promise<Actions.provider.list.ReturnType>;
|
|
80
|
+
/** `Actions.provider.signIn`. */
|
|
81
|
+
signIn: (options: Actions.provider.signIn.Options) => void;
|
|
82
|
+
};
|
|
83
|
+
redirect: {
|
|
84
|
+
/** `Actions.redirect.complete`, which sets the client's account. */
|
|
85
|
+
complete: (options?: Actions.redirect.complete.Options) => Promise<Actions.redirect.complete.ReturnType>;
|
|
86
|
+
/** `Actions.redirect.createUrl`. */
|
|
87
|
+
createUrl: (options?: Actions.redirect.createUrl.Options) => Promise<Actions.redirect.createUrl.ReturnType>;
|
|
88
|
+
/** `Actions.redirect.handleCallback`, which sets the client's account. */
|
|
89
|
+
handleCallback: (options: Actions.redirect.handleCallback.Options) => Promise<Authorization>;
|
|
90
|
+
/** `Actions.redirect.start`. */
|
|
91
|
+
start: (options?: Actions.redirect.start.Options) => Promise<void>;
|
|
92
|
+
};
|
|
93
|
+
/** `Actions.refresh`. */
|
|
94
|
+
refresh: (options: Actions.refresh.Options) => ReturnType<typeof Actions.refresh>;
|
|
95
|
+
/**
|
|
96
|
+
* Rebuilds a stored authorization's account with `Key.toAccount`, sets the client's account to
|
|
97
|
+
* it, and returns the authorization. Throws `Key.AccessKeyRequiredError` for a key that carries
|
|
98
|
+
* no material without the `accessKey` the app supplied.
|
|
99
|
+
*/
|
|
100
|
+
restore: <id extends number = chainId>(authorization: Omit<Authorization<id>, 'account'>, options?: Key.toAccount.Options) => Authorization<id>;
|
|
101
|
+
/** `Actions.signOut`, which empties the client's account, even when the request fails. */
|
|
102
|
+
signOut: () => Promise<void>;
|
|
103
|
+
tempoAccount: {
|
|
104
|
+
/** `Actions.tempoAccount.create`. */
|
|
105
|
+
create: () => Promise<Actions.tempoAccount.create.ReturnType>;
|
|
106
|
+
/** `Actions.tempoAccount.list`. */
|
|
107
|
+
list: () => Promise<Actions.tempoAccount.list.ReturnType>;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as Actions from './Actions.js';
|
|
2
|
+
import * as Auth from './Auth.js';
|
|
3
|
+
import * as AccountState from './internal/account.js';
|
|
4
|
+
import * as Key from './Key.js';
|
|
5
|
+
/** A client created without `account: Auth.account()`, which the extension sets at sign-in. */
|
|
6
|
+
export class AccountRequiredError extends Error {
|
|
7
|
+
name = 'Decorator.AccountRequiredError';
|
|
8
|
+
constructor() {
|
|
9
|
+
super('The client has no Tempo Auth account. Create the client with `account: Auth.account()`.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** A client created without a chain, which the extension reads the instance's `chainId` from. */
|
|
13
|
+
export class ChainRequiredError extends Error {
|
|
14
|
+
name = 'Decorator.ChainRequiredError';
|
|
15
|
+
constructor() {
|
|
16
|
+
super('The client has no chain. Create the client with a Tempo `chain`.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns a Viem client extension that adds Tempo Auth's actions as `client.auth`. Each member calls
|
|
21
|
+
* the action of the same name in `Actions` with one instance, whose `chainId` is the client's chain.
|
|
22
|
+
* Each sign-in sets the client's account to the authorization's, and `signOut` empties it. Throws
|
|
23
|
+
* {@link ChainRequiredError} for a client without a chain, and {@link AccountRequiredError} for one
|
|
24
|
+
* whose account is not an `Auth.account()`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const client = createClient({
|
|
29
|
+
* account: Auth.account(),
|
|
30
|
+
* chain: tempoTestnet,
|
|
31
|
+
* }).extend(tempoAuth({ clientId: 'client_…' }))
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function tempoAuth(options) {
|
|
35
|
+
return (client) => {
|
|
36
|
+
if (!client.chain)
|
|
37
|
+
throw new ChainRequiredError();
|
|
38
|
+
const state = client.account?.[AccountState.key];
|
|
39
|
+
if (!state)
|
|
40
|
+
throw new AccountRequiredError();
|
|
41
|
+
const auth = Auth.create({ ...options, chainId: client.chain.id });
|
|
42
|
+
/** Sets the client's account to the authorization's, and returns the authorization. */
|
|
43
|
+
const set = (authorization) => {
|
|
44
|
+
if (authorization)
|
|
45
|
+
state.account = authorization.account;
|
|
46
|
+
return authorization;
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
auth: {
|
|
50
|
+
authorize: async (options = {}) => set(await Actions.authorize(auth, options)),
|
|
51
|
+
device: {
|
|
52
|
+
async authorize(options = {}) {
|
|
53
|
+
const device = await Actions.device.authorize(auth, options);
|
|
54
|
+
return { ...device, wait: async () => set(await device.wait()) };
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
email: {
|
|
58
|
+
sendCode: (options) => Actions.email.sendCode(auth, options),
|
|
59
|
+
async signIn(options) {
|
|
60
|
+
const result = await Actions.email.signIn(auth, options);
|
|
61
|
+
if (!('step' in result))
|
|
62
|
+
set(result);
|
|
63
|
+
return result;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
instance: auth,
|
|
67
|
+
provider: {
|
|
68
|
+
list: () => Actions.provider.list(auth),
|
|
69
|
+
signIn: (options) => Actions.provider.signIn(auth, options),
|
|
70
|
+
},
|
|
71
|
+
redirect: {
|
|
72
|
+
complete: async (options = {}) => set(await Actions.redirect.complete(auth, options)),
|
|
73
|
+
createUrl: (options = {}) => Actions.redirect.createUrl(auth, options),
|
|
74
|
+
handleCallback: async (options) => set(await Actions.redirect.handleCallback(auth, options)),
|
|
75
|
+
start: (options = {}) => Actions.redirect.start(auth, options),
|
|
76
|
+
},
|
|
77
|
+
refresh: (options) => Actions.refresh(auth, options),
|
|
78
|
+
restore: (authorization, options = {}) => set({
|
|
79
|
+
...authorization,
|
|
80
|
+
account: Key.toAccount(authorization.key, options),
|
|
81
|
+
}),
|
|
82
|
+
async signOut() {
|
|
83
|
+
// The client stops signing even when the request fails, since the user asked to leave.
|
|
84
|
+
try {
|
|
85
|
+
await Actions.signOut(auth);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
state.account = undefined;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
tempoAccount: {
|
|
92
|
+
create: () => Actions.tempoAccount.create(auth),
|
|
93
|
+
list: () => Actions.tempoAccount.list(auth),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/Key.d.ts
CHANGED
|
@@ -12,9 +12,11 @@ import { Account } from 'viem/tempo';
|
|
|
12
12
|
* keeps without exporting. Elsewhere it is a secp256k1 private key, as JSON for a cookie or a file.
|
|
13
13
|
* A key the app supplied as `accessKey` carries no material.
|
|
14
14
|
*/
|
|
15
|
-
export type Key = {
|
|
15
|
+
export type Key<chainId extends number = number> = {
|
|
16
16
|
/** The Tempo Account the key acts for. */
|
|
17
17
|
account: Address.Address;
|
|
18
|
+
/** The chain the key acts on, from its authorization. */
|
|
19
|
+
chainId: chainId;
|
|
18
20
|
/** The signed authorization, in the node's RPC format. */
|
|
19
21
|
keyAuthorization: KeyAuthorization.Rpc;
|
|
20
22
|
} & Material;
|
|
@@ -41,16 +43,27 @@ export declare class AccessKeyRequiredError extends Error {
|
|
|
41
43
|
*/
|
|
42
44
|
export declare function expired(key: Key): boolean;
|
|
43
45
|
/**
|
|
44
|
-
* Instantiates a {@link Key}.
|
|
46
|
+
* Instantiates a {@link Key}. Fills `chainId` from the key authorization when the value leaves it
|
|
47
|
+
* out, as a key stored before `chainId` existed does.
|
|
45
48
|
*
|
|
46
49
|
* @example
|
|
47
50
|
* ```ts
|
|
48
51
|
* const key = Key.from(JSON.parse(saved).key)
|
|
49
52
|
* ```
|
|
50
53
|
*/
|
|
51
|
-
export declare function from(value:
|
|
54
|
+
export declare function from<chainId extends number = number>(value: from.Value<chainId>): Key<chainId>;
|
|
55
|
+
export declare namespace from {
|
|
56
|
+
type Value<chainId extends number = number> = {
|
|
57
|
+
/** The Tempo Account the key acts for. */
|
|
58
|
+
account: Address.Address;
|
|
59
|
+
/** The chain the key acts on. Defaults to the key authorization's. */
|
|
60
|
+
chainId?: chainId | undefined;
|
|
61
|
+
/** The signed authorization, in the node's RPC format. */
|
|
62
|
+
keyAuthorization: KeyAuthorization.Rpc;
|
|
63
|
+
} & Material;
|
|
64
|
+
}
|
|
52
65
|
/**
|
|
53
|
-
* Returns the key as a
|
|
66
|
+
* Returns the key as a Viem access key account acting for its Tempo Account. Viem attaches the
|
|
54
67
|
* authorization to transactions until the chain registers the key. A key the app supplied signs
|
|
55
68
|
* with that `accessKey`. Throws {@link AccessKeyRequiredError} when neither holds key material.
|
|
56
69
|
*
|
package/dist/Key.js
CHANGED
|
@@ -26,7 +26,8 @@ export function expired(key) {
|
|
|
26
26
|
return Boolean(expiry) && expiry * 1000 <= Date.now();
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Instantiates a {@link Key}.
|
|
29
|
+
* Instantiates a {@link Key}. Fills `chainId` from the key authorization when the value leaves it
|
|
30
|
+
* out, as a key stored before `chainId` existed does.
|
|
30
31
|
*
|
|
31
32
|
* @example
|
|
32
33
|
* ```ts
|
|
@@ -34,10 +35,14 @@ export function expired(key) {
|
|
|
34
35
|
* ```
|
|
35
36
|
*/
|
|
36
37
|
export function from(value) {
|
|
37
|
-
|
|
38
|
+
if (value.chainId !== undefined)
|
|
39
|
+
return { ...value, chainId: value.chainId };
|
|
40
|
+
// The node signs the chain ID the request named, so it is the request's literal.
|
|
41
|
+
const chainId = Number(KeyAuthorization.fromRpc(value.keyAuthorization).chainId);
|
|
42
|
+
return { ...value, chainId };
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
40
|
-
* Returns the key as a
|
|
45
|
+
* Returns the key as a Viem access key account acting for its Tempo Account. Viem attaches the
|
|
41
46
|
* authorization to transactions until the chain registers the key. A key the app supplied signs
|
|
42
47
|
* with that `accessKey`. Throws {@link AccessKeyRequiredError} when neither holds key material.
|
|
43
48
|
*
|
package/dist/Permissions.d.ts
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { KeyAuthorization } from 'ox/tempo';
|
|
6
6
|
/**
|
|
7
|
-
* What an app's access key may do, as
|
|
8
|
-
* it leaves out with the headless ceiling.
|
|
7
|
+
* What an app's access key may do, as Viem's `accessKey.authorize` takes it. Tempo Auth fills what
|
|
8
|
+
* it leaves out with the headless ceiling. `chainId` carries the chain's ID as a literal type, which
|
|
9
|
+
* the authorization's `key.chainId` keeps.
|
|
9
10
|
*/
|
|
10
|
-
export type Permissions = {
|
|
11
|
+
export type Permissions<chainId extends number = number> = {
|
|
11
12
|
/** The chain the key acts on, such as `tempoTestnet.id`. Defaults to the instance's `chainId`. */
|
|
12
|
-
chainId?:
|
|
13
|
-
/** Unix seconds.
|
|
13
|
+
chainId?: chainId | undefined;
|
|
14
|
+
/** Unix seconds. Viem's `Expiry` builds one, such as `Expiry.days(1)`. Defaults to a year from now. */
|
|
14
15
|
expiry?: number | undefined;
|
|
15
16
|
/** How much of each token the key may spend. Defaults to about $1,000 of each token it calls, or of OUSD. */
|
|
16
17
|
limits?: KeyAuthorization.KeyAuthorization['limits'];
|
|
@@ -18,7 +19,7 @@ export type Permissions = {
|
|
|
18
19
|
scopes?: KeyAuthorization.KeyAuthorization['scopes'];
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
|
-
* Instantiates a {@link Permissions}.
|
|
22
|
+
* Instantiates a {@link Permissions}, keeping `chainId` as a literal type.
|
|
22
23
|
*
|
|
23
24
|
* @example
|
|
24
25
|
* ```ts
|
|
@@ -29,4 +30,4 @@ export type Permissions = {
|
|
|
29
30
|
* })
|
|
30
31
|
* ```
|
|
31
32
|
*/
|
|
32
|
-
export declare function from(value: Permissions): Permissions
|
|
33
|
+
export declare function from<const chainId extends number = number>(value: Permissions<chainId>): Permissions<chainId>;
|
package/dist/Permissions.js
CHANGED
package/dist/actions/device.d.ts
CHANGED
|
@@ -8,25 +8,28 @@ import type { Permissions } from '../Permissions.ts';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const device = await Actions.device.authorize(auth
|
|
11
|
+
* const device = await Actions.device.authorize(auth)
|
|
12
12
|
* console.log(`Open ${device.verificationUriComplete}`)
|
|
13
13
|
* const authorization = await device.wait()
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
export declare function authorize(auth: Auth, options
|
|
16
|
+
export declare function authorize<const chainId extends number = number>(auth: Auth, options?: authorize.Options<chainId>): Promise<authorize.ReturnType<chainId>>;
|
|
17
17
|
export declare namespace authorize {
|
|
18
|
-
type Options = {
|
|
18
|
+
type Options<chainId extends number = number> = {
|
|
19
19
|
/**
|
|
20
|
-
* The key to authorize, as a
|
|
20
|
+
* The key to authorize, as a Viem account. Its `access` names the Tempo Account, which
|
|
21
21
|
* defaults to the user's first. Defaults to a new key.
|
|
22
22
|
*/
|
|
23
23
|
accessKey?: Account.Account | undefined;
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The name the approval page shows. Defaults to the machine's hostname in Node, Bun, and Deno,
|
|
26
|
+
* and to `Unknown device` elsewhere.
|
|
27
|
+
*/
|
|
28
|
+
device?: string | undefined;
|
|
26
29
|
/** What the CLI's key may do. The approval page shows it beside the device, and the headless ceiling fills what it leaves out. */
|
|
27
|
-
permissions?: Permissions | undefined;
|
|
30
|
+
permissions?: Permissions<chainId> | undefined;
|
|
28
31
|
};
|
|
29
|
-
type ReturnType = {
|
|
32
|
+
type ReturnType<chainId extends number = number> = {
|
|
30
33
|
/** Seconds until the code expires. */
|
|
31
34
|
expiresIn: number;
|
|
32
35
|
/** The code the user confirms in the browser. */
|
|
@@ -36,6 +39,6 @@ export declare namespace authorize {
|
|
|
36
39
|
/** Where the user approves the code, with it already filled in. */
|
|
37
40
|
verificationUriComplete: string;
|
|
38
41
|
/** Resolves with the authorization once the user approves. */
|
|
39
|
-
wait: () => Promise<Authorization
|
|
42
|
+
wait: () => Promise<Authorization<chainId>>;
|
|
40
43
|
};
|
|
41
44
|
}
|
package/dist/actions/device.js
CHANGED
|
@@ -8,16 +8,16 @@ import { request } from '../internal/request.js';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const device = await Actions.device.authorize(auth
|
|
11
|
+
* const device = await Actions.device.authorize(auth)
|
|
12
12
|
* console.log(`Open ${device.verificationUriComplete}`)
|
|
13
13
|
* const authorization = await device.wait()
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
|
-
export async function authorize(auth, options) {
|
|
16
|
+
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
|
-
device: options.device,
|
|
20
|
+
device: options.device ?? hostname(),
|
|
21
21
|
key_authorization: request(auth, {
|
|
22
22
|
accessKey: key.accessKey,
|
|
23
23
|
permissions: options.permissions,
|
|
@@ -60,3 +60,12 @@ export async function authorize(auth, options) {
|
|
|
60
60
|
wait,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The machine's hostname, within the 64 characters Tempo Auth accepts. `getBuiltinModule` loads
|
|
65
|
+
* `node:os` without an import, which browser and React Native bundlers would fail to resolve.
|
|
66
|
+
*/
|
|
67
|
+
function hostname() {
|
|
68
|
+
const process = Reflect.get(globalThis, 'process');
|
|
69
|
+
const name = process?.getBuiltinModule?.('node:os')?.hostname().trim();
|
|
70
|
+
return name ? name.slice(0, 64) : 'Unknown device';
|
|
71
|
+
}
|
package/dist/actions/email.d.ts
CHANGED
|
@@ -32,11 +32,11 @@ export declare namespace sendCode {
|
|
|
32
32
|
* if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
|
-
export declare function signIn(auth: Auth, options: signIn.Options): Promise<signIn.ReturnType
|
|
35
|
+
export declare function signIn<const chainId extends number = number>(auth: Auth, options: signIn.Options<chainId>): Promise<signIn.ReturnType<chainId>>;
|
|
36
36
|
export declare namespace signIn {
|
|
37
|
-
type Options = {
|
|
37
|
+
type Options<chainId extends number = number> = {
|
|
38
38
|
/**
|
|
39
|
-
* The key to authorize, as a
|
|
39
|
+
* The key to authorize, as a Viem account. Its `access` names the Tempo Account, which
|
|
40
40
|
* defaults to the user's first. Defaults to a new key.
|
|
41
41
|
*/
|
|
42
42
|
accessKey?: Account.Account | undefined;
|
|
@@ -45,10 +45,10 @@ export declare namespace signIn {
|
|
|
45
45
|
/** The address the code was sent to. */
|
|
46
46
|
email: string;
|
|
47
47
|
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
48
|
-
permissions?: Permissions | undefined;
|
|
48
|
+
permissions?: Permissions<chainId> | undefined;
|
|
49
49
|
};
|
|
50
50
|
/** The authorization, or the step the address requires first. */
|
|
51
|
-
type ReturnType = Authorization | {
|
|
51
|
+
type ReturnType<chainId extends number = number> = Authorization<chainId> | {
|
|
52
52
|
step: Step;
|
|
53
53
|
};
|
|
54
54
|
}
|
|
@@ -41,7 +41,7 @@ export declare function createUrl(auth: Auth, options?: createUrl.Options): Prom
|
|
|
41
41
|
export declare namespace createUrl {
|
|
42
42
|
type Options = {
|
|
43
43
|
/**
|
|
44
|
-
* The key to authorize, as a
|
|
44
|
+
* The key to authorize, as a Viem account. Its `access` names the Tempo Account, which
|
|
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;
|
|
@@ -93,7 +93,7 @@ export declare function start(auth: Auth, options?: start.Options): Promise<void
|
|
|
93
93
|
export declare namespace start {
|
|
94
94
|
type Options = {
|
|
95
95
|
/**
|
|
96
|
-
* The key to authorize, as a
|
|
96
|
+
* The key to authorize, as a Viem account. Pass it to {@link complete} too. Defaults to a new
|
|
97
97
|
* key, which `storage` keeps until the browser returns.
|
|
98
98
|
*/
|
|
99
99
|
accessKey?: Account.Account | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * as Actions from './Actions.ts';
|
|
2
2
|
export * as Auth from './Auth.ts';
|
|
3
3
|
export * as Authorization from './Authorization.ts';
|
|
4
|
+
export * as Decorator from './Decorator.ts';
|
|
5
|
+
export { tempoAuth } from './Decorator.ts';
|
|
4
6
|
export * as Key from './Key.ts';
|
|
5
7
|
export * as Pending from './Pending.ts';
|
|
6
8
|
export * as Permissions from './Permissions.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export * as Actions from './Actions.js';
|
|
3
3
|
export * as Auth from './Auth.js';
|
|
4
4
|
export * as Authorization from './Authorization.js';
|
|
5
|
+
export * as Decorator from './Decorator.js';
|
|
6
|
+
export { tempoAuth } from './Decorator.js';
|
|
5
7
|
export * as Key from './Key.js';
|
|
6
8
|
export * as Pending from './Pending.js';
|
|
7
9
|
export * as Permissions from './Permissions.js';
|
|
@@ -7,7 +7,7 @@ import type * as Key from '../Key.ts';
|
|
|
7
7
|
export declare function resolve(accessKey: Account.Account | undefined): Promise<resolve.ReturnType>;
|
|
8
8
|
export declare namespace resolve {
|
|
9
9
|
type ReturnType = {
|
|
10
|
-
/** The key, as a
|
|
10
|
+
/** The key, as a Viem account that signs with it. */
|
|
11
11
|
accessKey: Account.Account;
|
|
12
12
|
/** What the authorization keeps to rebuild the key. Empty for a key the app supplied. */
|
|
13
13
|
material: Key.Material;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state behind `Auth.account()`: the access key account a sign-in set, which the account reads
|
|
3
|
+
* through to. The `tempoAuth` extension reads and writes it through {@link key}.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { Account } from 'viem/tempo';
|
|
7
|
+
/** The property that holds an `Auth.account()`'s state. Other accounts lack it. */
|
|
8
|
+
export declare const key: unique symbol;
|
|
9
|
+
/** What an `Auth.account()` signs as. */
|
|
10
|
+
export type State = {
|
|
11
|
+
/** The signed-in access key account, or `undefined` while signed out. */
|
|
12
|
+
account: Account.AccessKeyAccount | undefined;
|
|
13
|
+
};
|
package/dist/internal/api.d.ts
CHANGED
|
@@ -6,12 +6,13 @@ import * as Key from '../Key.ts';
|
|
|
6
6
|
import type { Tokens } from '../Tokens.ts';
|
|
7
7
|
/**
|
|
8
8
|
* Builds an authorization from what Tempo Auth returned and the sign-in's key: the material the
|
|
9
|
-
* library generated, or the `accessKey` the app supplied.
|
|
9
|
+
* library generated, or the `accessKey` the app supplied. `chainId` is the chain the request named,
|
|
10
|
+
* which the key authorization repeats.
|
|
10
11
|
*/
|
|
11
|
-
export declare function authorization(body: Headless, options: {
|
|
12
|
+
export declare function authorization<chainId extends number = number>(body: Headless, options: {
|
|
12
13
|
accessKey?: Account.Account | undefined;
|
|
13
14
|
material: Key.Material;
|
|
14
|
-
}): Authorization
|
|
15
|
+
}): Authorization<chainId>;
|
|
15
16
|
/** What the headless API returns for an authorization. */
|
|
16
17
|
export type Headless = {
|
|
17
18
|
account: Key.Key['account'];
|
package/dist/internal/api.js
CHANGED
|
@@ -7,7 +7,8 @@ import { RequestError } from '../Auth.js';
|
|
|
7
7
|
import * as Key from '../Key.js';
|
|
8
8
|
/**
|
|
9
9
|
* Builds an authorization from what Tempo Auth returned and the sign-in's key: the material the
|
|
10
|
-
* library generated, or the `accessKey` the app supplied.
|
|
10
|
+
* library generated, or the `accessKey` the app supplied. `chainId` is the chain the request named,
|
|
11
|
+
* which the key authorization repeats.
|
|
11
12
|
*/
|
|
12
13
|
export function authorization(body, options) {
|
|
13
14
|
const { accessKey, material } = options;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempoxyz/auth",
|
|
3
|
-
"version": "0.0.0-pr-
|
|
4
|
-
"description": "Signs users in to Tempo through Tempo Auth, and returns
|
|
3
|
+
"version": "0.0.0-pr-66.sha-480b2a0",
|
|
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": {
|
|
7
7
|
".": {
|