@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.
- package/README.md +72 -46
- package/dist/Actions.d.ts +18 -4
- package/dist/Actions.js +8 -12
- package/dist/Auth.d.ts +11 -91
- package/dist/Auth.js +29 -0
- package/dist/Authorization.d.ts +25 -0
- package/dist/Authorization.js +11 -0
- package/dist/Key.d.ts +40 -8
- package/dist/Key.js +41 -5
- package/dist/Pending.d.ts +26 -0
- package/dist/Pending.js +11 -0
- package/dist/Permissions.d.ts +25 -0
- package/dist/Permissions.js +15 -0
- package/dist/Provider.d.ts +15 -0
- package/dist/Provider.js +11 -0
- package/dist/Step.d.ts +20 -0
- package/dist/Step.js +11 -0
- package/dist/Storage.d.ts +26 -0
- package/dist/Storage.js +15 -0
- package/dist/TempoAccount.d.ts +18 -0
- package/dist/TempoAccount.js +11 -0
- package/dist/Tokens.d.ts +24 -0
- package/dist/Tokens.js +11 -0
- package/dist/User.d.ts +20 -0
- package/dist/User.js +11 -0
- package/dist/actions/device.d.ts +12 -4
- package/dist/actions/device.js +9 -7
- package/dist/actions/email.d.ts +17 -4
- package/dist/actions/email.js +6 -10
- package/dist/actions/provider.d.ts +2 -1
- package/dist/actions/redirect.d.ts +32 -12
- package/dist/actions/redirect.js +32 -23
- package/dist/actions/tempoAccount.d.ts +4 -3
- package/dist/actions/tempoAccount.js +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/internal/accessKey.d.ts +15 -0
- package/dist/internal/accessKey.js +26 -0
- package/dist/internal/api.d.ts +10 -8
- package/dist/internal/api.js +10 -6
- package/dist/internal/indexedDb.d.ts +8 -0
- package/dist/internal/indexedDb.js +31 -0
- package/dist/internal/request.d.ts +8 -10
- package/dist/internal/request.js +12 -10
- 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
|
-
|
|
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 `
|
|
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
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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, {
|
|
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
|
|
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
|
|
234
|
-
|
|
|
235
|
-
| `authorize(auth, {
|
|
236
|
-
| `refresh(auth, { refreshToken })`
|
|
237
|
-
| `signOut(auth)`
|
|
238
|
-
| `device.authorize(auth, {
|
|
239
|
-
| `email.sendCode(auth, { email })`
|
|
240
|
-
| `email.signIn(auth, {
|
|
241
|
-
| `provider.list(auth)`
|
|
242
|
-
| `provider.signIn(auth, { provider } \| { url })`
|
|
243
|
-
| `redirect.complete(auth)`
|
|
244
|
-
| `redirect.createUrl(auth, {
|
|
245
|
-
| `redirect.handleCallback(auth, { pending, url })`
|
|
246
|
-
| `redirect.start(auth, {
|
|
247
|
-
| `tempoAccount.create(auth)`
|
|
248
|
-
| `tempoAccount.list(auth)`
|
|
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
|
|
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
|
|
265
|
-
|
|
|
266
|
-
| `toAccount(key)` | The key as a viem access key account
|
|
267
|
-
| `expired(key)`
|
|
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
|
-
|
|
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, {
|
|
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
|
-
/**
|
|
22
|
-
|
|
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, {
|
|
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
|
|
27
|
+
const key = await AccessKey.resolve(options.accessKey);
|
|
33
28
|
const response = await Api.post(auth, '/api/authorize', {
|
|
34
|
-
json: request(auth, {
|
|
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
|
-
/**
|
|
41
|
-
|
|
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;
|
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
|
|
3
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
}
|