@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 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 { Addresses, createClient, Expiry, Scopes } from 'viem/tempo'
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: Addresses.pathUsd,
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: Addresses.pathUsd }],
26
- scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
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: Addresses.pathUsd,
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` | 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()]` |
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, permissions })
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, 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.
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, { permissions })
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, permissions })
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, { permissions })
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, permissions })
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`, with its `status` and `message`.
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.email.signIn(auth, { code, email, permissions })
222
+ await Actions.authorize(auth, { permissions })
224
223
  } catch (error) {
225
- if (error instanceof Auth.RequestError && error.status === 401)
226
- showWrongCode()
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 takes `permissions` also takes an optional `accessKey`.
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, { permissions })` | An `Authorization` for a user already signed in, or `undefined` |
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, permissions })` | The code to show and a `wait()` that resolves with an `Authorization` |
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, permissions })` | An `Authorization`, or the `step` the address requires first |
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, { permissions, redirectUri })` | The `url` to send the browser to, and the `pending` state to keep |
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, { permissions })` | Nothing. The browser leaves for Tempo Auth's pages |
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, { permissions })
23
+ * const authorization = await Actions.authorize(auth)
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(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: 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, { permissions })
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
- constructor(options: {
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 { Addresses, Expiry, Scopes } from 'viem/tempo'
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: Addresses.pathUsd }],
22
- * scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
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
  /**
@@ -3,12 +3,19 @@
3
3
  * @module
4
4
  */
5
5
  import type { KeyAuthorization } from 'ox/tempo';
6
- /** What an app's access key may do, as viem's `accessKey.authorize` takes it. */
7
- export type Permissions = Pick<KeyAuthorization.KeyAuthorization, 'limits' | 'scopes'> & {
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: number;
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: Addresses.pathUsd }],
21
- * scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
27
+ * limits: [{ limit: 10_000_000n, token: ousd }],
28
+ * scopes: [Scopes.tip20(ousd).transfer()],
22
29
  * })
23
30
  * ```
24
31
  */
@@ -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: Addresses.pathUsd }],
9
- * scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
8
+ * limits: [{ limit: 10_000_000n, token: ousd }],
9
+ * scopes: [Scopes.tip20(ousd).transfer()],
10
10
  * })
11
11
  * ```
12
12
  */
@@ -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(), permissions })
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: 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. */
@@ -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(), permissions })
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 new RequestError({
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({ message: 'The code expired.', status: 400 });
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,
@@ -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, permissions })
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: 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 | {
@@ -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, permissions })
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, { permissions }))
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: createUrl.Options): Promise<createUrl.ReturnType>;
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 user approves it. */
50
- permissions: 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, { permissions })
89
+ * await Actions.redirect.start(auth)
91
90
  * ```
92
91
  */
93
- export declare function start(auth: Auth, options: start.Options): Promise<void>;
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 user approves it. */
102
- permissions: 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
  }
@@ -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, { permissions }))
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 || params.get('state') !== state)
103
+ if (!code)
105
104
  throw new RequestError({
106
- message: 'Sign-in failed. Start again.',
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
- message: 'Sign-in failed. Start again.',
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, { permissions })
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);
@@ -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;
@@ -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 new RequestError({
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: 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}`;
@@ -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-34.sha-cbbdadc",
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": {