@tempoxyz/auth 0.0.0-pr-34.sha-cbbdadc → 0.0.0-pr-36.sha-2b0babd
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 +34 -35
- package/dist/Actions.d.ts +4 -4
- package/dist/Actions.js +2 -2
- package/dist/Auth.d.ts +22 -2
- package/dist/Auth.js +22 -4
- package/dist/Permissions.d.ts +13 -6
- package/dist/Permissions.js +2 -2
- package/dist/actions/device.d.ts +3 -3
- package/dist/actions/device.js +7 -6
- package/dist/actions/email.d.ts +3 -3
- package/dist/actions/email.js +1 -1
- package/dist/actions/redirect.d.ts +8 -9
- package/dist/actions/redirect.js +18 -8
- package/dist/internal/api.d.ts +14 -1
- package/dist/internal/api.js +15 -4
- package/dist/internal/request.d.ts +2 -2
- package/dist/internal/request.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,12 @@ Every sign-in returns the same `Authorization`. Send transactions with its `acco
|
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { Actions, Auth } from '@tempoxyz/auth'
|
|
9
|
-
import {
|
|
10
|
-
import { tempoTestnet } from 'viem/tempo/chains'
|
|
9
|
+
import { createClient, Expiry, Scopes } from 'viem/tempo'
|
|
10
|
+
import { tempo, tempoTestnet } 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
|
|
11
15
|
|
|
12
16
|
const auth = Auth.create({
|
|
13
17
|
chainId: tempoTestnet.id,
|
|
@@ -16,14 +20,14 @@ const auth = Auth.create({
|
|
|
16
20
|
})
|
|
17
21
|
const client = createClient({
|
|
18
22
|
chain: tempoTestnet,
|
|
19
|
-
feeToken:
|
|
23
|
+
feeToken: ousd,
|
|
20
24
|
})
|
|
21
25
|
|
|
22
26
|
const authorization = await Actions.authorize(auth, {
|
|
23
27
|
permissions: {
|
|
24
28
|
expiry: Expiry.days(1),
|
|
25
|
-
limits: [{ limit: 10_000_000n, token:
|
|
26
|
-
scopes: [Scopes.tip20(
|
|
29
|
+
limits: [{ limit: 10_000_000n, token: ousd }],
|
|
30
|
+
scopes: [Scopes.tip20(ousd).transfer()],
|
|
27
31
|
},
|
|
28
32
|
})
|
|
29
33
|
|
|
@@ -31,7 +35,7 @@ await client.token.transferSync({
|
|
|
31
35
|
account: authorization!.account,
|
|
32
36
|
amount: 10_000n,
|
|
33
37
|
to: '0x…',
|
|
34
|
-
token:
|
|
38
|
+
token: ousd,
|
|
35
39
|
})
|
|
36
40
|
```
|
|
37
41
|
|
|
@@ -50,14 +54,14 @@ The CLI device grant needs neither. It signs in as Tempo Auth's own `tempo-cli`
|
|
|
50
54
|
|
|
51
55
|
## Access keys
|
|
52
56
|
|
|
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:
|
|
57
|
+
Each sign-in creates a key for the app and asks Tempo Auth to authorize it on the user's Tempo Account. The optional `permissions` option says what the key may do, with viem's `Expiry` and `Scopes` helpers. Tempo Auth fills every field it leaves out with the headless ceiling:
|
|
54
58
|
|
|
55
|
-
| Field | Meaning
|
|
56
|
-
| --------- |
|
|
57
|
-
| `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()]
|
|
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 |
|
|
61
65
|
|
|
62
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 gets a secp256k1 key, which it stores as a string.
|
|
63
67
|
|
|
@@ -65,10 +69,10 @@ To authorize a key the app already holds, pass it as `accessKey`, a viem account
|
|
|
65
69
|
|
|
66
70
|
```ts
|
|
67
71
|
const accessKey = Account.fromWebCryptoP256(keyPair, { access: tempoAccount })
|
|
68
|
-
await Actions.authorize(auth, { accessKey
|
|
72
|
+
await Actions.authorize(auth, { accessKey })
|
|
69
73
|
```
|
|
70
74
|
|
|
71
|
-
Flows without a Tempo Auth page (`authorize`, `email.signIn`) must stay within Tempo Auth's ceiling
|
|
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.
|
|
72
76
|
|
|
73
77
|
The returned `account` attaches the key's authorization to its first transaction, which registers the key on chain.
|
|
74
78
|
|
|
@@ -80,11 +84,11 @@ For a browser app that renders its own email form. The page calls Tempo Auth wit
|
|
|
80
84
|
|
|
81
85
|
```ts
|
|
82
86
|
// A user already signed in to Tempo Auth, from any app, needs no form.
|
|
83
|
-
const authorization = await Actions.authorize(auth
|
|
87
|
+
const authorization = await Actions.authorize(auth)
|
|
84
88
|
|
|
85
89
|
// Otherwise, sign in with an emailed code.
|
|
86
90
|
await Actions.email.sendCode(auth, { email })
|
|
87
|
-
const result = await Actions.email.signIn(auth, { code, email
|
|
91
|
+
const result = await Actions.email.signIn(auth, { code, email })
|
|
88
92
|
|
|
89
93
|
// An address that requires SSO continues at its connection, and returns to this page.
|
|
90
94
|
if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
@@ -102,7 +106,7 @@ Actions.provider.signIn(auth, { provider: 'google' })
|
|
|
102
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.
|
|
103
107
|
|
|
104
108
|
```ts
|
|
105
|
-
await Actions.redirect.start(auth
|
|
109
|
+
await Actions.redirect.start(auth)
|
|
106
110
|
|
|
107
111
|
// On the page the browser returns to, and on every load:
|
|
108
112
|
const authorization = await Actions.redirect.complete(auth)
|
|
@@ -125,12 +129,10 @@ const auth = Auth.create({
|
|
|
125
129
|
|
|
126
130
|
// Your own sign-in UI, as in a browser.
|
|
127
131
|
await Actions.email.sendCode(auth, { email })
|
|
128
|
-
const result = await Actions.email.signIn(auth, { code, email
|
|
132
|
+
const result = await Actions.email.signIn(auth, { code, email })
|
|
129
133
|
|
|
130
134
|
// Or Tempo Auth's pages, which also offer Google and SSO.
|
|
131
|
-
const { pending, url } = await Actions.redirect.createUrl(auth
|
|
132
|
-
permissions,
|
|
133
|
-
})
|
|
135
|
+
const { pending, url } = await Actions.redirect.createUrl(auth)
|
|
134
136
|
const session = await WebBrowser.openAuthSessionAsync(url, auth.redirectUri)
|
|
135
137
|
if (session.type === 'success')
|
|
136
138
|
authorization = await Actions.redirect.handleCallback(auth, {
|
|
@@ -150,7 +152,6 @@ const auth = Auth.create({ clientId, clientSecret, url })
|
|
|
150
152
|
|
|
151
153
|
// GET /auth/start
|
|
152
154
|
const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
153
|
-
permissions,
|
|
154
155
|
redirectUri: 'https://app.example/auth/callback',
|
|
155
156
|
})
|
|
156
157
|
// Keep `pending` in an encrypted, HttpOnly cookie, then redirect to `url`.
|
|
@@ -169,7 +170,6 @@ For a command-line tool. The user approves a code in any browser, and the CLI wa
|
|
|
169
170
|
```ts
|
|
170
171
|
const device = await Actions.device.authorize(auth, {
|
|
171
172
|
device: os.hostname(),
|
|
172
|
-
permissions,
|
|
173
173
|
})
|
|
174
174
|
console.log(
|
|
175
175
|
`Open ${device.verificationUriComplete} and confirm ${device.userCode}.`,
|
|
@@ -189,7 +189,6 @@ await Actions.authorize(auth, {
|
|
|
189
189
|
accessKey: Account.fromWebCryptoP256(keyPair, {
|
|
190
190
|
access: tempoAccount.address,
|
|
191
191
|
}),
|
|
192
|
-
permissions,
|
|
193
192
|
})
|
|
194
193
|
```
|
|
195
194
|
|
|
@@ -216,14 +215,14 @@ Refresh tokens rotate, so save the new tokens over the old ones after each refre
|
|
|
216
215
|
|
|
217
216
|
## Errors
|
|
218
217
|
|
|
219
|
-
A request Tempo Auth refuses throws `Auth.RequestError`,
|
|
218
|
+
A request Tempo Auth refuses throws `Auth.RequestError`. It carries the OAuth error code as `code`, and `message` says what went wrong and how to fix it. `reason`, `field`, and `meta` let an app branch on the refusal and read the values it compared. [Errors](../../docs/errors.md) lists every `reason`.
|
|
220
219
|
|
|
221
220
|
```ts
|
|
222
221
|
try {
|
|
223
|
-
await Actions.
|
|
222
|
+
await Actions.authorize(auth, { permissions })
|
|
224
223
|
} catch (error) {
|
|
225
|
-
if (error instanceof Auth.RequestError && error.
|
|
226
|
-
|
|
224
|
+
if (error instanceof Auth.RequestError && error.reason === 'limit_exceeds_maximum')
|
|
225
|
+
showMaximum(error.meta?.maximum) // in base units
|
|
227
226
|
}
|
|
228
227
|
```
|
|
229
228
|
|
|
@@ -244,22 +243,22 @@ try {
|
|
|
244
243
|
|
|
245
244
|
### `Actions`
|
|
246
245
|
|
|
247
|
-
Each sign-in flow has its own namespace. The actions every flow shares sit at the top level. Every action that
|
|
246
|
+
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`.
|
|
248
247
|
|
|
249
248
|
| Action | Returns |
|
|
250
249
|
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
251
|
-
| `authorize(auth
|
|
250
|
+
| `authorize(auth)` | An `Authorization` for a user already signed in, or `undefined` |
|
|
252
251
|
| `refresh(auth, { refreshToken })` | New `Tokens` |
|
|
253
252
|
| `signOut(auth)` | Nothing. The user's Tempo Auth session ends |
|
|
254
|
-
| `device.authorize(auth, { device
|
|
253
|
+
| `device.authorize(auth, { device })` | The code to show and a `wait()` that resolves with an `Authorization` |
|
|
255
254
|
| `email.sendCode(auth, { email })` | Nothing. Tempo Auth emails a code |
|
|
256
|
-
| `email.signIn(auth, { code, email
|
|
255
|
+
| `email.signIn(auth, { code, email })` | An `Authorization`, or the `step` the address requires first |
|
|
257
256
|
| `provider.list(auth)` | The OAuth providers Tempo Auth offers, as `{ id, name }` |
|
|
258
257
|
| `provider.signIn(auth, { provider } \| { url })` | Nothing. The browser leaves for the provider or SSO connection |
|
|
259
258
|
| `redirect.complete(auth, { accessKey })` | The `Authorization` when the page is a redirect's callback, or `undefined` |
|
|
260
|
-
| `redirect.createUrl(auth, {
|
|
259
|
+
| `redirect.createUrl(auth, { redirectUri })` | The `url` to send the browser to, and the `pending` state to keep |
|
|
261
260
|
| `redirect.handleCallback(auth, { pending, url })` | The `Authorization` for a redirect's callback |
|
|
262
|
-
| `redirect.start(auth
|
|
261
|
+
| `redirect.start(auth)` | Nothing. The browser leaves for Tempo Auth's pages |
|
|
263
262
|
| `tempoAccount.create(auth)` | Another Tempo Account for the signed-in user |
|
|
264
263
|
| `tempoAccount.list(auth)` | The signed-in user's Tempo Accounts, the default first |
|
|
265
264
|
|
package/dist/Actions.d.ts
CHANGED
|
@@ -20,11 +20,11 @@ export * as tempoAccount from './actions/tempoAccount.ts';
|
|
|
20
20
|
*
|
|
21
21
|
* @example
|
|
22
22
|
* ```ts
|
|
23
|
-
* const authorization = await Actions.authorize(auth
|
|
23
|
+
* const authorization = await Actions.authorize(auth)
|
|
24
24
|
* if (!authorization) showSignIn()
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
export declare function authorize(auth: Auth, options
|
|
27
|
+
export declare function authorize(auth: Auth, options?: authorize.Options): Promise<authorize.ReturnType>;
|
|
28
28
|
export declare namespace authorize {
|
|
29
29
|
type Options = {
|
|
30
30
|
/**
|
|
@@ -32,8 +32,8 @@ export declare namespace authorize {
|
|
|
32
32
|
* defaults to the user's first. Defaults to a new key.
|
|
33
33
|
*/
|
|
34
34
|
accessKey?: Account.Account | undefined;
|
|
35
|
-
/** What the key may do. It must fit the headless ceiling. */
|
|
36
|
-
permissions
|
|
35
|
+
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
36
|
+
permissions?: Permissions | undefined;
|
|
37
37
|
};
|
|
38
38
|
/** The authorization, or `undefined` without a session. */
|
|
39
39
|
type ReturnType = Authorization | undefined;
|
package/dist/Actions.js
CHANGED
|
@@ -13,11 +13,11 @@ export * as tempoAccount from './actions/tempoAccount.js';
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```ts
|
|
16
|
-
* const authorization = await Actions.authorize(auth
|
|
16
|
+
* const authorization = await Actions.authorize(auth)
|
|
17
17
|
* if (!authorization) showSignIn()
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export async function authorize(auth, options) {
|
|
20
|
+
export async function authorize(auth, options = {}) {
|
|
21
21
|
// Checking the session first answers a visitor without one with no failed request.
|
|
22
22
|
const session = await Api.json(await (auth.fetch ?? fetch)(new URL('/api/auth/session', auth.url), {
|
|
23
23
|
credentials: 'include',
|
package/dist/Auth.d.ts
CHANGED
|
@@ -24,12 +24,32 @@ export type Auth = {
|
|
|
24
24
|
/** A request Tempo Auth refused, with its status and message. */
|
|
25
25
|
export declare class RequestError extends Error {
|
|
26
26
|
readonly name = "Auth.RequestError";
|
|
27
|
+
/** The OAuth error code, such as `invalid_authorization_details`. */
|
|
28
|
+
readonly code: string;
|
|
29
|
+
/** The request field at fault, as a dotted path. */
|
|
30
|
+
readonly field: string | undefined;
|
|
31
|
+
/** The values the refusal compared, such as `limit` and `maximum`. Base units are decimal strings. */
|
|
32
|
+
readonly meta: Readonly<Record<string, number | string>> | undefined;
|
|
33
|
+
/** A stable name for the refusal, to branch on. */
|
|
34
|
+
readonly reason: RequestError.Reason | undefined;
|
|
27
35
|
/** The HTTP status Tempo Auth answered. */
|
|
28
36
|
readonly status: number;
|
|
29
|
-
|
|
37
|
+
/** The page that explains the error. */
|
|
38
|
+
readonly uri: string | undefined;
|
|
39
|
+
constructor(options: RequestError.Options);
|
|
40
|
+
}
|
|
41
|
+
export declare namespace RequestError {
|
|
42
|
+
type Options = {
|
|
43
|
+
code: string;
|
|
44
|
+
field?: string | undefined;
|
|
30
45
|
message: string;
|
|
46
|
+
meta?: Readonly<Record<string, number | string>> | undefined;
|
|
47
|
+
reason?: Reason | undefined;
|
|
31
48
|
status: number;
|
|
32
|
-
|
|
49
|
+
uri?: string | undefined;
|
|
50
|
+
};
|
|
51
|
+
/** Each refusal's stable name. `docs/errors.md` explains each one. */
|
|
52
|
+
type Reason = 'already_decided' | 'approval_pending' | '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' | 'rejected' | 'request_denied' | 'scope_without_limit' | 'session_required' | 'unexpected' | 'unknown_code' | 'unknown_endpoint' | 'unsupported_call' | 'unsupported_step' | 'unsupported_token';
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
35
55
|
* Creates a Tempo Auth instance.
|
package/dist/Auth.js
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* @example
|
|
7
7
|
* ```ts
|
|
8
8
|
* import { Actions, Auth } from '@tempoxyz/auth'
|
|
9
|
-
* import {
|
|
10
|
-
* import { tempoTestnet } from 'viem/tempo/chains'
|
|
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
|
|
11
14
|
*
|
|
12
15
|
* const auth = Auth.create({
|
|
13
16
|
* chainId: tempoTestnet.id,
|
|
@@ -18,8 +21,8 @@
|
|
|
18
21
|
* const authorization = await Actions.authorize(auth, {
|
|
19
22
|
* permissions: {
|
|
20
23
|
* expiry: Expiry.days(1),
|
|
21
|
-
* limits: [{ limit: 10_000_000n, token:
|
|
22
|
-
* scopes: [Scopes.tip20(
|
|
24
|
+
* limits: [{ limit: 10_000_000n, token: ousd }],
|
|
25
|
+
* scopes: [Scopes.tip20(ousd).transfer()],
|
|
23
26
|
* },
|
|
24
27
|
* })
|
|
25
28
|
* ```
|
|
@@ -29,11 +32,26 @@ import { tempo } from 'viem/tempo/chains';
|
|
|
29
32
|
/** A request Tempo Auth refused, with its status and message. */
|
|
30
33
|
export class RequestError extends Error {
|
|
31
34
|
name = 'Auth.RequestError';
|
|
35
|
+
/** The OAuth error code, such as `invalid_authorization_details`. */
|
|
36
|
+
code;
|
|
37
|
+
/** The request field at fault, as a dotted path. */
|
|
38
|
+
field;
|
|
39
|
+
/** The values the refusal compared, such as `limit` and `maximum`. Base units are decimal strings. */
|
|
40
|
+
meta;
|
|
41
|
+
/** A stable name for the refusal, to branch on. */
|
|
42
|
+
reason;
|
|
32
43
|
/** The HTTP status Tempo Auth answered. */
|
|
33
44
|
status;
|
|
45
|
+
/** The page that explains the error. */
|
|
46
|
+
uri;
|
|
34
47
|
constructor(options) {
|
|
35
48
|
super(options.message);
|
|
49
|
+
this.code = options.code;
|
|
50
|
+
this.field = options.field;
|
|
51
|
+
this.meta = options.meta;
|
|
52
|
+
this.reason = options.reason;
|
|
36
53
|
this.status = options.status;
|
|
54
|
+
this.uri = options.uri;
|
|
37
55
|
}
|
|
38
56
|
}
|
|
39
57
|
/**
|
package/dist/Permissions.d.ts
CHANGED
|
@@ -3,12 +3,19 @@
|
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
5
|
import type { KeyAuthorization } from 'ox/tempo';
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
export type Permissions = {
|
|
8
11
|
/** The chain the key acts on, such as `tempoTestnet.id`. Defaults to the instance's `chainId`. */
|
|
9
12
|
chainId?: number | undefined;
|
|
10
|
-
/** Unix seconds. viem's `Expiry` builds one, such as `Expiry.days(1)`. */
|
|
11
|
-
expiry
|
|
13
|
+
/** Unix seconds. viem's `Expiry` builds one, such as `Expiry.days(1)`. Defaults to a year from now. */
|
|
14
|
+
expiry?: number | undefined;
|
|
15
|
+
/** How much of each token the key may spend. Defaults to about $1,000 of each token it calls, or of OUSD. */
|
|
16
|
+
limits?: KeyAuthorization.KeyAuthorization['limits'];
|
|
17
|
+
/** The calls the key may make. Defaults to transfers of each token it limits. */
|
|
18
|
+
scopes?: KeyAuthorization.KeyAuthorization['scopes'];
|
|
12
19
|
};
|
|
13
20
|
/**
|
|
14
21
|
* Instantiates a {@link Permissions}.
|
|
@@ -17,8 +24,8 @@ export type Permissions = Pick<KeyAuthorization.KeyAuthorization, 'limits' | 'sc
|
|
|
17
24
|
* ```ts
|
|
18
25
|
* const permissions = Permissions.from({
|
|
19
26
|
* expiry: Expiry.days(1),
|
|
20
|
-
* limits: [{ limit: 10_000_000n, token:
|
|
21
|
-
* scopes: [Scopes.tip20(
|
|
27
|
+
* limits: [{ limit: 10_000_000n, token: ousd }],
|
|
28
|
+
* scopes: [Scopes.tip20(ousd).transfer()],
|
|
22
29
|
* })
|
|
23
30
|
* ```
|
|
24
31
|
*/
|
package/dist/Permissions.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* ```ts
|
|
6
6
|
* const permissions = Permissions.from({
|
|
7
7
|
* expiry: Expiry.days(1),
|
|
8
|
-
* limits: [{ limit: 10_000_000n, token:
|
|
9
|
-
* scopes: [Scopes.tip20(
|
|
8
|
+
* limits: [{ limit: 10_000_000n, token: ousd }],
|
|
9
|
+
* scopes: [Scopes.tip20(ousd).transfer()],
|
|
10
10
|
* })
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
package/dist/actions/device.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { Permissions } from '../Permissions.ts';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const device = await Actions.device.authorize(auth, { device: Os.hostname()
|
|
11
|
+
* const device = await Actions.device.authorize(auth, { device: Os.hostname() })
|
|
12
12
|
* console.log(`Open ${device.verificationUriComplete}`)
|
|
13
13
|
* const authorization = await device.wait()
|
|
14
14
|
* ```
|
|
@@ -23,8 +23,8 @@ export declare namespace authorize {
|
|
|
23
23
|
accessKey?: Account.Account | undefined;
|
|
24
24
|
/** The name the approval page shows, such as the machine's hostname. */
|
|
25
25
|
device: string;
|
|
26
|
-
/** What the CLI's key may do. The approval page shows it beside the device. */
|
|
27
|
-
permissions
|
|
26
|
+
/** 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;
|
|
28
28
|
};
|
|
29
29
|
type ReturnType = {
|
|
30
30
|
/** Seconds until the code expires. */
|
package/dist/actions/device.js
CHANGED
|
@@ -8,7 +8,7 @@ import { request } from '../internal/request.js';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* const device = await Actions.device.authorize(auth, { device: Os.hostname()
|
|
11
|
+
* const device = await Actions.device.authorize(auth, { device: Os.hostname() })
|
|
12
12
|
* console.log(`Open ${device.verificationUriComplete}`)
|
|
13
13
|
* const authorization = await device.wait()
|
|
14
14
|
* ```
|
|
@@ -44,12 +44,13 @@ export async function authorize(auth, options) {
|
|
|
44
44
|
if (body.error === 'slow_down')
|
|
45
45
|
interval += 5;
|
|
46
46
|
else if (body.error !== 'authorization_pending')
|
|
47
|
-
throw
|
|
48
|
-
message: `Sign-in failed: ${body.error}.`,
|
|
49
|
-
status: response.status,
|
|
50
|
-
});
|
|
47
|
+
throw Api.error(body, { status: response.status });
|
|
51
48
|
}
|
|
52
|
-
throw new RequestError({
|
|
49
|
+
throw new RequestError({
|
|
50
|
+
code: 'expired_token',
|
|
51
|
+
message: 'Device code expired. Request a new code.',
|
|
52
|
+
status: 400,
|
|
53
|
+
});
|
|
53
54
|
}
|
|
54
55
|
return {
|
|
55
56
|
expiresIn: code.expires_in,
|
package/dist/actions/email.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare namespace sendCode {
|
|
|
28
28
|
*
|
|
29
29
|
* @example
|
|
30
30
|
* ```ts
|
|
31
|
-
* const result = await Actions.email.signIn(auth, { code, email
|
|
31
|
+
* const result = await Actions.email.signIn(auth, { code, email })
|
|
32
32
|
* if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
@@ -44,8 +44,8 @@ export declare namespace signIn {
|
|
|
44
44
|
code: string;
|
|
45
45
|
/** The address the code was sent to. */
|
|
46
46
|
email: string;
|
|
47
|
-
/** What the key may do. It must fit the headless ceiling. */
|
|
48
|
-
permissions
|
|
47
|
+
/** What the key may do. It must fit the headless ceiling, which fills what it leaves out. */
|
|
48
|
+
permissions?: Permissions | undefined;
|
|
49
49
|
};
|
|
50
50
|
/** The authorization, or the step the address requires first. */
|
|
51
51
|
type ReturnType = Authorization | {
|
package/dist/actions/email.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function sendCode(auth, options) {
|
|
|
18
18
|
*
|
|
19
19
|
* @example
|
|
20
20
|
* ```ts
|
|
21
|
-
* const result = await Actions.email.signIn(auth, { code, email
|
|
21
|
+
* const result = await Actions.email.signIn(auth, { code, email })
|
|
22
22
|
* if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
@@ -11,7 +11,7 @@ import type { Permissions } from '../Permissions.ts';
|
|
|
11
11
|
* ```ts
|
|
12
12
|
* const authorization =
|
|
13
13
|
* (await Actions.redirect.complete(auth)) ??
|
|
14
|
-
* (await Actions.authorize(auth
|
|
14
|
+
* (await Actions.authorize(auth))
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export declare function complete(auth: Auth, options?: complete.Options): Promise<complete.ReturnType>;
|
|
@@ -31,14 +31,13 @@ export declare namespace complete {
|
|
|
31
31
|
* @example
|
|
32
32
|
* ```ts
|
|
33
33
|
* const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
34
|
-
* permissions,
|
|
35
34
|
* redirectUri: 'https://app.example/auth/callback',
|
|
36
35
|
* })
|
|
37
36
|
* await seal(c, 'oauth', pending)
|
|
38
37
|
* return c.redirect(url)
|
|
39
38
|
* ```
|
|
40
39
|
*/
|
|
41
|
-
export declare function createUrl(auth: Auth, options
|
|
40
|
+
export declare function createUrl(auth: Auth, options?: createUrl.Options): Promise<createUrl.ReturnType>;
|
|
42
41
|
export declare namespace createUrl {
|
|
43
42
|
type Options = {
|
|
44
43
|
/**
|
|
@@ -46,8 +45,8 @@ export declare namespace createUrl {
|
|
|
46
45
|
* defaults to the user's first. Defaults to a new key, whose material `pending` keeps.
|
|
47
46
|
*/
|
|
48
47
|
accessKey?: Account.Account | undefined;
|
|
49
|
-
/** What the key may do. Tempo Auth's pages show it, and the
|
|
50
|
-
permissions
|
|
48
|
+
/** What the key may do. Tempo Auth's pages show it, and the headless ceiling fills what it leaves out. */
|
|
49
|
+
permissions?: Permissions | undefined;
|
|
51
50
|
/** Where Tempo Auth redirects back to. Defaults to the instance's `redirectUri`. */
|
|
52
51
|
redirectUri?: string | undefined;
|
|
53
52
|
};
|
|
@@ -87,10 +86,10 @@ export declare namespace handleCallback {
|
|
|
87
86
|
*
|
|
88
87
|
* @example
|
|
89
88
|
* ```ts
|
|
90
|
-
* await Actions.redirect.start(auth
|
|
89
|
+
* await Actions.redirect.start(auth)
|
|
91
90
|
* ```
|
|
92
91
|
*/
|
|
93
|
-
export declare function start(auth: Auth, options
|
|
92
|
+
export declare function start(auth: Auth, options?: start.Options): Promise<void>;
|
|
94
93
|
export declare namespace start {
|
|
95
94
|
type Options = {
|
|
96
95
|
/**
|
|
@@ -98,7 +97,7 @@ export declare namespace start {
|
|
|
98
97
|
* key, which `storage` keeps until the browser returns.
|
|
99
98
|
*/
|
|
100
99
|
accessKey?: Account.Account | undefined;
|
|
101
|
-
/** What the key may do. Tempo Auth's pages show it, and the
|
|
102
|
-
permissions
|
|
100
|
+
/** What the key may do. Tempo Auth's pages show it, and the headless ceiling fills what it leaves out. */
|
|
101
|
+
permissions?: Permissions | undefined;
|
|
103
102
|
};
|
|
104
103
|
}
|
package/dist/actions/redirect.js
CHANGED
|
@@ -19,7 +19,7 @@ import * as Pending from '../Pending.js';
|
|
|
19
19
|
* ```ts
|
|
20
20
|
* const authorization =
|
|
21
21
|
* (await Actions.redirect.complete(auth)) ??
|
|
22
|
-
* (await Actions.authorize(auth
|
|
22
|
+
* (await Actions.authorize(auth))
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
export function complete(auth, options = {}) {
|
|
@@ -53,14 +53,13 @@ export function complete(auth, options = {}) {
|
|
|
53
53
|
* @example
|
|
54
54
|
* ```ts
|
|
55
55
|
* const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
56
|
-
* permissions,
|
|
57
56
|
* redirectUri: 'https://app.example/auth/callback',
|
|
58
57
|
* })
|
|
59
58
|
* await seal(c, 'oauth', pending)
|
|
60
59
|
* return c.redirect(url)
|
|
61
60
|
* ```
|
|
62
61
|
*/
|
|
63
|
-
export async function createUrl(auth, options) {
|
|
62
|
+
export async function createUrl(auth, options = {}) {
|
|
64
63
|
const redirectUri = options.redirectUri ?? auth.redirectUri ?? `${window.location.origin}/`;
|
|
65
64
|
const key = await AccessKey.resolve(options.accessKey);
|
|
66
65
|
const pending = Pending.from({
|
|
@@ -101,9 +100,19 @@ export async function handleCallback(auth, options) {
|
|
|
101
100
|
const { redirectUri, state, verifier, ...material } = options.pending;
|
|
102
101
|
const params = new URL(options.url).searchParams;
|
|
103
102
|
const code = params.get('code');
|
|
104
|
-
if (!code
|
|
103
|
+
if (!code)
|
|
105
104
|
throw new RequestError({
|
|
106
|
-
|
|
105
|
+
code: params.get('error') ?? 'invalid_request',
|
|
106
|
+
field: 'code',
|
|
107
|
+
message: params.get('error_description') ??
|
|
108
|
+
'Missing `code` in the callback URL. Start sign-in again.',
|
|
109
|
+
status: 400,
|
|
110
|
+
});
|
|
111
|
+
if (params.get('state') !== state)
|
|
112
|
+
throw new RequestError({
|
|
113
|
+
code: 'invalid_request',
|
|
114
|
+
field: 'state',
|
|
115
|
+
message: 'Invalid `state` in the callback URL. Start sign-in again from this app.',
|
|
107
116
|
status: 400,
|
|
108
117
|
});
|
|
109
118
|
const body = await Api.token(auth, {
|
|
@@ -114,7 +123,8 @@ export async function handleCallback(auth, options) {
|
|
|
114
123
|
});
|
|
115
124
|
if (!body.account || !body.key_authorization)
|
|
116
125
|
throw new RequestError({
|
|
117
|
-
|
|
126
|
+
code: 'invalid_grant',
|
|
127
|
+
message: 'Missing key authorization in the token response. The code was already redeemed, or did not request a key. Start sign-in again.',
|
|
118
128
|
status: 400,
|
|
119
129
|
});
|
|
120
130
|
return Api.authorization({
|
|
@@ -129,10 +139,10 @@ export async function handleCallback(auth, options) {
|
|
|
129
139
|
*
|
|
130
140
|
* @example
|
|
131
141
|
* ```ts
|
|
132
|
-
* await Actions.redirect.start(auth
|
|
142
|
+
* await Actions.redirect.start(auth)
|
|
133
143
|
* ```
|
|
134
144
|
*/
|
|
135
|
-
export async function start(auth, options) {
|
|
145
|
+
export async function start(auth, options = {}) {
|
|
136
146
|
const { pending, url } = await createUrl(auth, options);
|
|
137
147
|
await storage(auth).setItem(storageKey, pending);
|
|
138
148
|
window.location.assign(url);
|
package/dist/internal/api.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { KeyAuthorization } from 'ox/tempo';
|
|
2
2
|
import type { Account } from 'viem/tempo';
|
|
3
|
-
import { type Auth } from '../Auth.ts';
|
|
3
|
+
import { type Auth, RequestError } from '../Auth.ts';
|
|
4
4
|
import type { Authorization } from '../Authorization.ts';
|
|
5
5
|
import * as Key from '../Key.ts';
|
|
6
6
|
import type { Tokens } from '../Tokens.ts';
|
|
@@ -20,6 +20,19 @@ export type Headless = {
|
|
|
20
20
|
};
|
|
21
21
|
/** Reads a JSON response, and throws a {@link RequestError} for a refused request. */
|
|
22
22
|
export declare function json<value = unknown>(response: Response): Promise<value>;
|
|
23
|
+
/** An OAuth error response, with Tempo Auth's extension members. */
|
|
24
|
+
export type ErrorBody = {
|
|
25
|
+
error: string;
|
|
26
|
+
error_description?: string | undefined;
|
|
27
|
+
error_uri?: string | undefined;
|
|
28
|
+
field?: string | undefined;
|
|
29
|
+
meta?: Readonly<Record<string, number | string>> | undefined;
|
|
30
|
+
reason?: RequestError.Reason | undefined;
|
|
31
|
+
};
|
|
32
|
+
/** A {@link RequestError} from an OAuth error response, or from a refusal with no body. */
|
|
33
|
+
export declare function error(body: Partial<ErrorBody>, options: {
|
|
34
|
+
status: number;
|
|
35
|
+
}): RequestError;
|
|
23
36
|
/** Posts JSON to Tempo Auth's API with the session cookie. */
|
|
24
37
|
export declare function post(auth: Auth, path: string, options: {
|
|
25
38
|
json?: unknown;
|
package/dist/internal/api.js
CHANGED
|
@@ -30,12 +30,23 @@ export async function json(response) {
|
|
|
30
30
|
// Some refusals, such as the CSRF check's, answer plain text.
|
|
31
31
|
const body = (await response.json().catch(() => ({})));
|
|
32
32
|
if (!response.ok)
|
|
33
|
-
throw
|
|
34
|
-
message: body.error ?? 'Something went wrong.',
|
|
35
|
-
status: response.status,
|
|
36
|
-
});
|
|
33
|
+
throw error(body, { status: response.status });
|
|
37
34
|
return body;
|
|
38
35
|
}
|
|
36
|
+
/** A {@link RequestError} from an OAuth error response, or from a refusal with no body. */
|
|
37
|
+
export function error(body, options) {
|
|
38
|
+
return new RequestError({
|
|
39
|
+
code: body.error ?? 'server_error',
|
|
40
|
+
field: body.field,
|
|
41
|
+
message: body.error_description ??
|
|
42
|
+
body.error ??
|
|
43
|
+
`Request failed with status ${options.status}.`,
|
|
44
|
+
meta: body.meta,
|
|
45
|
+
reason: body.reason,
|
|
46
|
+
status: options.status,
|
|
47
|
+
uri: body.error_uri,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
39
50
|
/** Posts JSON to Tempo Auth's API with the session cookie. */
|
|
40
51
|
export function post(auth, path, options) {
|
|
41
52
|
return (auth.fetch ?? fetch)(new URL(path, auth.url), {
|
|
@@ -7,14 +7,14 @@ import type { Permissions } from '../Permissions.ts';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function request(auth: Auth, options: {
|
|
9
9
|
accessKey: Account.Account;
|
|
10
|
-
permissions
|
|
10
|
+
permissions?: Permissions | undefined;
|
|
11
11
|
}): {
|
|
12
12
|
client_id: string;
|
|
13
13
|
keyAuthorization: {
|
|
14
14
|
account?: `0x${string}`;
|
|
15
15
|
address: `0x${string}`;
|
|
16
16
|
chainId: `0x${string}`;
|
|
17
|
-
expiry: number;
|
|
17
|
+
expiry: number | undefined;
|
|
18
18
|
keyType: "p256" | "secp256k1" | "webAuthn";
|
|
19
19
|
limits: {
|
|
20
20
|
limit: `0x${string}`;
|
package/dist/internal/request.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Hex } from 'ox';
|
|
|
9
9
|
* key account names its Tempo Account through `access`, and a plain key leaves it to Tempo Auth.
|
|
10
10
|
*/
|
|
11
11
|
export function request(auth, options) {
|
|
12
|
-
const { accessKey, permissions } = options;
|
|
12
|
+
const { accessKey, permissions = {} } = options;
|
|
13
13
|
return {
|
|
14
14
|
client_id: auth.clientId,
|
|
15
15
|
keyAuthorization: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempoxyz/auth",
|
|
3
|
-
"version": "0.0.0-pr-
|
|
3
|
+
"version": "0.0.0-pr-36.sha-2b0babd",
|
|
4
4
|
"description": "Signs users in to Tempo through Tempo Auth, and returns viem access key accounts on their Tempo Accounts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|