@tempoxyz/auth 0.0.0-pr-19.sha-2c8fa8e → 0.0.0-pr-22.sha-291bbcb
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 +45 -43
- package/dist/Actions.d.ts +6 -225
- package/dist/Actions.js +14 -352
- package/dist/Auth.d.ts +3 -3
- package/dist/actions/device.d.ts +33 -0
- package/dist/actions/device.js +59 -0
- package/dist/actions/email.d.ts +41 -0
- package/dist/actions/email.js +44 -0
- package/dist/actions/provider.d.ts +47 -0
- package/dist/actions/provider.js +31 -0
- package/dist/actions/redirect.d.ts +84 -0
- package/dist/actions/redirect.js +142 -0
- package/dist/actions/tempoAccount.d.ts +35 -0
- package/dist/actions/tempoAccount.js +31 -0
- package/dist/internal/api.d.ts +40 -0
- package/dist/internal/api.js +70 -0
- package/package.json +1 -1
package/dist/Actions.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tempo Auth's actions. Each takes an instance from `Auth.create`.
|
|
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.
|
|
3
4
|
* @module
|
|
4
5
|
*/
|
|
5
|
-
import { Base64, Bytes, Hash, Hex } from 'ox';
|
|
6
6
|
import { Secp256k1 } from 'viem/tempo';
|
|
7
|
-
import
|
|
8
|
-
import * as Key from './Key.js';
|
|
7
|
+
import * as Api from './internal/api.js';
|
|
9
8
|
import { request } from './internal/request.js';
|
|
9
|
+
export * as device from './actions/device.js';
|
|
10
|
+
export * as email from './actions/email.js';
|
|
11
|
+
export * as provider from './actions/provider.js';
|
|
12
|
+
export * as redirect from './actions/redirect.js';
|
|
13
|
+
export * as tempoAccount from './actions/tempoAccount.js';
|
|
10
14
|
/**
|
|
11
15
|
* Authorizes the app for a user already signed in to Tempo Auth, from any app. Returns
|
|
12
16
|
* `undefined` without a session, so the app shows sign-in. The key must fit Tempo Auth's headless
|
|
@@ -20,236 +24,20 @@ import { request } from './internal/request.js';
|
|
|
20
24
|
*/
|
|
21
25
|
export async function authorize(auth, options) {
|
|
22
26
|
// Checking the session first answers a visitor without one with no failed request.
|
|
23
|
-
const session = await json(await (auth.fetch ?? fetch)(new URL('/api/auth/session', auth.url), {
|
|
27
|
+
const session = await Api.json(await (auth.fetch ?? fetch)(new URL('/api/auth/session', auth.url), {
|
|
24
28
|
credentials: 'include',
|
|
25
29
|
}));
|
|
26
30
|
if (!session.user)
|
|
27
31
|
return undefined;
|
|
28
32
|
const privateKey = Secp256k1.randomPrivateKey();
|
|
29
|
-
const response = await post(auth, '/api/authorize', {
|
|
33
|
+
const response = await Api.post(auth, '/api/authorize', {
|
|
30
34
|
json: request(auth, { ...options, privateKey }),
|
|
31
35
|
});
|
|
32
36
|
if (response.status === 401)
|
|
33
37
|
return undefined;
|
|
34
|
-
return authorization(await json(response), {
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Starts the device grant for a CLI: the user approves `userCode` at `verificationUri` from any
|
|
38
|
-
* browser, and `wait` resolves once they do.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* ```ts
|
|
42
|
-
* const device = await Actions.authorizeDevice(auth, { accessKey, device: Os.hostname() })
|
|
43
|
-
* console.log(`Open ${device.verificationUriComplete}`)
|
|
44
|
-
* const authorization = await device.wait()
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export async function authorizeDevice(auth, options) {
|
|
48
|
-
const privateKey = Secp256k1.randomPrivateKey();
|
|
49
|
-
const code = await json(await post(auth, '/api/device/authorize', {
|
|
50
|
-
json: {
|
|
51
|
-
device: options.device,
|
|
52
|
-
keyAuthorization: request(auth, { ...options, privateKey })
|
|
53
|
-
.keyAuthorization,
|
|
54
|
-
},
|
|
55
|
-
}));
|
|
56
|
-
/** Polls until the user approves or denies the code, or it expires. */
|
|
57
|
-
async function wait() {
|
|
58
|
-
let interval = code.interval;
|
|
59
|
-
const deadline = Date.now() + code.expires_in * 1000;
|
|
60
|
-
while (Date.now() < deadline) {
|
|
61
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
62
|
-
const response = await post(auth, '/api/device/token', {
|
|
63
|
-
json: { device_code: code.device_code },
|
|
64
|
-
});
|
|
65
|
-
const body = (await response.json());
|
|
66
|
-
if ('access_token' in body)
|
|
67
|
-
return authorization({
|
|
68
|
-
account: body.account,
|
|
69
|
-
keyAuthorization: body.key_authorization,
|
|
70
|
-
tokens: tokens(body),
|
|
71
|
-
}, { privateKey });
|
|
72
|
-
// RFC 8628 asks a client told to slow down to add 5 seconds to its interval.
|
|
73
|
-
if (body.error === 'slow_down')
|
|
74
|
-
interval += 5;
|
|
75
|
-
else if (body.error !== 'authorization_pending')
|
|
76
|
-
throw new RequestError({
|
|
77
|
-
message: `Sign-in failed: ${body.error}.`,
|
|
78
|
-
status: response.status,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
throw new RequestError({ message: 'The code expired.', status: 400 });
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
expiresIn: code.expires_in,
|
|
85
|
-
userCode: code.user_code,
|
|
86
|
-
verificationUri: code.verification_uri,
|
|
87
|
-
verificationUriComplete: code.verification_uri_complete,
|
|
88
|
-
wait,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Returns the URL that signs the user in on Tempo Auth's pages and redirects back with a code,
|
|
93
|
-
* and the {@link Pending} state to keep until then, such as in a sealed cookie.
|
|
94
|
-
* {@link handleCallback} redeems the code for the tokens and the key authorization.
|
|
95
|
-
*
|
|
96
|
-
* @example
|
|
97
|
-
* ```ts
|
|
98
|
-
* const { pending, url } = await Actions.createAuthorizationUrl(auth, {
|
|
99
|
-
* accessKey,
|
|
100
|
-
* redirectUri: 'https://app.example/auth/callback',
|
|
101
|
-
* })
|
|
102
|
-
* await seal(c, 'oauth', pending)
|
|
103
|
-
* return c.redirect(url)
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
export async function createAuthorizationUrl(auth, options) {
|
|
107
|
-
const redirectUri = options.redirectUri ?? auth.redirectUri ?? `${window.location.origin}/`;
|
|
108
|
-
const pending = {
|
|
109
|
-
privateKey: Secp256k1.randomPrivateKey(),
|
|
110
|
-
redirectUri,
|
|
111
|
-
// `Bytes.random` needs only `crypto.getRandomValues`, which React Native polyfills.
|
|
112
|
-
state: Base64.fromBytes(Bytes.random(16), base64url),
|
|
113
|
-
verifier: Base64.fromBytes(Bytes.random(32), base64url),
|
|
114
|
-
};
|
|
115
|
-
const { client_id, keyAuthorization, scope } = request(auth, {
|
|
116
|
-
accessKey: options.accessKey,
|
|
117
|
-
privateKey: pending.privateKey,
|
|
118
|
-
});
|
|
119
|
-
const url = new URL('/authorize', auth.url);
|
|
120
|
-
url.search = new URLSearchParams({
|
|
121
|
-
client_id,
|
|
122
|
-
code_challenge: Base64.fromBytes(Hash.sha256(Bytes.fromString(pending.verifier)), base64url),
|
|
123
|
-
key_authorization: Base64.fromString(JSON.stringify(keyAuthorization), base64url),
|
|
124
|
-
redirect_uri: redirectUri,
|
|
125
|
-
scope,
|
|
126
|
-
state: pending.state,
|
|
127
|
-
}).toString();
|
|
128
|
-
return { pending, url: url.href };
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Creates another Tempo Account for the signed-in user, and returns it. Pass its `address` as an
|
|
132
|
-
* access key's `account` to authorize keys on it.
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* ```ts
|
|
136
|
-
* const tempoAccount = await Actions.createTempoAccount(auth)
|
|
137
|
-
* await Actions.authorize(auth, {
|
|
138
|
-
* accessKey: { ...accessKey, account: tempoAccount.address },
|
|
139
|
-
* })
|
|
140
|
-
* ```
|
|
141
|
-
*/
|
|
142
|
-
export async function createTempoAccount(auth) {
|
|
143
|
-
const { address } = await json(await post(auth, '/api/account/tempo', { json: {} }));
|
|
144
|
-
return { address };
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Lists the OAuth providers Tempo Auth offers, for an app that shows its own sign-in buttons.
|
|
148
|
-
*
|
|
149
|
-
* @example
|
|
150
|
-
* ```ts
|
|
151
|
-
* const providers = await Actions.getProviders(auth)
|
|
152
|
-
* // [{ id: 'google', name: 'Google' }]
|
|
153
|
-
* ```
|
|
154
|
-
*/
|
|
155
|
-
export async function getProviders(auth) {
|
|
156
|
-
const { providers } = await json(await (auth.fetch ?? fetch)(new URL('/api/auth/providers', auth.url)));
|
|
157
|
-
return providers;
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Lists the signed-in user's Tempo Accounts, their default first.
|
|
161
|
-
*
|
|
162
|
-
* @example
|
|
163
|
-
* ```ts
|
|
164
|
-
* const [account, ...others] = await Actions.getTempoAccounts(auth)
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
export async function getTempoAccounts(auth) {
|
|
168
|
-
const { tempo } = await json(await (auth.fetch ?? fetch)(new URL('/api/account', auth.url), {
|
|
169
|
-
credentials: 'include',
|
|
170
|
-
}));
|
|
171
|
-
return tempo.map(({ address }) => ({ address }));
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Finishes a sign-in redirect: checks its state, and redeems the code with the PKCE verifier (and
|
|
175
|
-
* the client secret, for a backend) for the tokens and the key authorization.
|
|
176
|
-
*
|
|
177
|
-
* @example
|
|
178
|
-
* ```ts
|
|
179
|
-
* const authorization = await Actions.handleCallback(auth, {
|
|
180
|
-
* pending: await unseal(c, 'oauth'),
|
|
181
|
-
* url: c.req.url,
|
|
182
|
-
* })
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
export async function handleCallback(auth, options) {
|
|
186
|
-
const { pending } = options;
|
|
187
|
-
const params = new URL(options.url).searchParams;
|
|
188
|
-
const code = params.get('code');
|
|
189
|
-
if (!code || params.get('state') !== pending.state)
|
|
190
|
-
throw new RequestError({
|
|
191
|
-
message: 'Sign-in failed. Start again.',
|
|
192
|
-
status: 400,
|
|
193
|
-
});
|
|
194
|
-
const body = await token(auth, {
|
|
195
|
-
code,
|
|
196
|
-
code_verifier: pending.verifier,
|
|
197
|
-
grant_type: 'authorization_code',
|
|
198
|
-
redirect_uri: pending.redirectUri,
|
|
38
|
+
return Api.authorization(await Api.json(response), {
|
|
39
|
+
privateKey,
|
|
199
40
|
});
|
|
200
|
-
if (!body.account || !body.key_authorization)
|
|
201
|
-
throw new RequestError({
|
|
202
|
-
message: 'Sign-in failed. Start again.',
|
|
203
|
-
status: 400,
|
|
204
|
-
});
|
|
205
|
-
return authorization({
|
|
206
|
-
account: body.account,
|
|
207
|
-
keyAuthorization: body.key_authorization,
|
|
208
|
-
tokens: body,
|
|
209
|
-
}, pending);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Finishes a {@link redirect} when the page loads with Tempo Auth's callback, and returns
|
|
213
|
-
* `undefined` otherwise. The code is redeemed once, however often the page calls this.
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* ```ts
|
|
217
|
-
* const authorization =
|
|
218
|
-
* (await Actions.handleRedirect(auth)) ??
|
|
219
|
-
* (await Actions.authorize(auth, { accessKey }))
|
|
220
|
-
* ```
|
|
221
|
-
*/
|
|
222
|
-
export function handleRedirect(auth) {
|
|
223
|
-
redirected ??= (async () => {
|
|
224
|
-
const stored = storage(auth).getItem(storageKey);
|
|
225
|
-
storage(auth).removeItem(storageKey);
|
|
226
|
-
const url = new URL(window.location.href);
|
|
227
|
-
if (!url.searchParams.has('code'))
|
|
228
|
-
return undefined;
|
|
229
|
-
// The app keeps its own query and fragment, without the callback's parameters. A code this page
|
|
230
|
-
// did not request is dropped too, since it redeems for whoever crafted its request.
|
|
231
|
-
const clean = new URL(url);
|
|
232
|
-
for (const name of ['code', 'state'])
|
|
233
|
-
clean.searchParams.delete(name);
|
|
234
|
-
window.history.replaceState(null, '', clean.pathname + clean.search + clean.hash);
|
|
235
|
-
if (!stored)
|
|
236
|
-
return undefined;
|
|
237
|
-
return handleCallback(auth, { pending: JSON.parse(stored), url });
|
|
238
|
-
})();
|
|
239
|
-
return redirected;
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Sends the browser to Tempo Auth's pages to sign in, keeping the redirect's state in `storage`.
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```ts
|
|
246
|
-
* await Actions.redirect(auth, { accessKey })
|
|
247
|
-
* ```
|
|
248
|
-
*/
|
|
249
|
-
export async function redirect(auth, options) {
|
|
250
|
-
const { pending, url } = await createAuthorizationUrl(auth, options);
|
|
251
|
-
storage(auth).setItem(storageKey, JSON.stringify(pending));
|
|
252
|
-
window.location.assign(url);
|
|
253
41
|
}
|
|
254
42
|
/**
|
|
255
43
|
* Exchanges a refresh token for new tokens. Refresh tokens rotate, so store the new one.
|
|
@@ -262,38 +50,11 @@ export async function redirect(auth, options) {
|
|
|
262
50
|
* ```
|
|
263
51
|
*/
|
|
264
52
|
export async function refresh(auth, options) {
|
|
265
|
-
return tokens(await token(auth, {
|
|
53
|
+
return Api.tokens(await Api.token(auth, {
|
|
266
54
|
grant_type: 'refresh_token',
|
|
267
55
|
refresh_token: options.refreshToken,
|
|
268
56
|
}));
|
|
269
57
|
}
|
|
270
|
-
/**
|
|
271
|
-
* Emails the user a sign-in code. The response is the same for every address.
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```ts
|
|
275
|
-
* await Actions.sendCode(auth, { email: 'user@example.com' })
|
|
276
|
-
* ```
|
|
277
|
-
*/
|
|
278
|
-
export async function sendCode(auth, options) {
|
|
279
|
-
await json(await post(auth, '/api/auth/code', { json: options }));
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Sends the browser to an OAuth provider, or to the SSO connection a {@link Step} names, and back to
|
|
283
|
-
* `returnTo` signed in to Tempo Auth. The app then calls {@link authorize}.
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* ```ts
|
|
287
|
-
* Actions.signIn(auth, { provider: 'google' })
|
|
288
|
-
* ```
|
|
289
|
-
*/
|
|
290
|
-
export function signIn(auth, options) {
|
|
291
|
-
const url = 'url' in options
|
|
292
|
-
? new URL(options.url)
|
|
293
|
-
: new URL(`/api/auth/oauth/${options.provider}`, auth.url);
|
|
294
|
-
url.searchParams.set('return_to', options.returnTo ?? window.location.href);
|
|
295
|
-
window.location.assign(url);
|
|
296
|
-
}
|
|
297
58
|
/**
|
|
298
59
|
* Ends the user's Tempo Auth session, which signs them out of every app that relies on it.
|
|
299
60
|
*
|
|
@@ -304,104 +65,5 @@ export function signIn(auth, options) {
|
|
|
304
65
|
*/
|
|
305
66
|
export async function signOut(auth) {
|
|
306
67
|
// A JSON body keeps the request out of Tempo Auth's CSRF check for cross-site form posts.
|
|
307
|
-
await json(await post(auth, '/api/auth/logout', { json: {} }));
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Signs the user in with the code they were emailed, and authorizes the app in the same call.
|
|
311
|
-
* Returns a {@link Step} when the address requires SSO, which {@link signIn} continues.
|
|
312
|
-
*
|
|
313
|
-
* @example
|
|
314
|
-
* ```ts
|
|
315
|
-
* const result = await Actions.verifyCode(auth, { accessKey, code, email })
|
|
316
|
-
* if ('step' in result) Actions.signIn(auth, { url: result.step.url })
|
|
317
|
-
* ```
|
|
318
|
-
*/
|
|
319
|
-
export async function verifyCode(auth, options) {
|
|
320
|
-
const { accessKey, code, email } = options;
|
|
321
|
-
const privateKey = Secp256k1.randomPrivateKey();
|
|
322
|
-
const body = await json(await post(auth, '/api/auth/code/verify', {
|
|
323
|
-
json: {
|
|
324
|
-
authorize: request(auth, { accessKey, privateKey }),
|
|
325
|
-
code,
|
|
326
|
-
email,
|
|
327
|
-
},
|
|
328
|
-
}));
|
|
329
|
-
if ('step' in body)
|
|
330
|
-
return {
|
|
331
|
-
step: { ...body.step, url: new URL(body.step.url, auth.url).href },
|
|
332
|
-
};
|
|
333
|
-
return authorization(body.authorization, { privateKey });
|
|
334
|
-
}
|
|
335
|
-
/** Unpadded URL-safe Base64, as PKCE and `key_authorization` use. */
|
|
336
|
-
const base64url = { pad: false, url: true };
|
|
337
|
-
/** The redirect this page load returns from, so its code is redeemed once. */
|
|
338
|
-
let redirected;
|
|
339
|
-
/** Where a redirect keeps its state in `storage`. */
|
|
340
|
-
const storageKey = 'tempo-auth:redirect';
|
|
341
|
-
/** Builds an authorization from what Tempo Auth returned and the key the app generated. */
|
|
342
|
-
function authorization(body, options) {
|
|
343
|
-
const key = {
|
|
344
|
-
account: body.account,
|
|
345
|
-
keyAuthorization: body.keyAuthorization,
|
|
346
|
-
privateKey: options.privateKey,
|
|
347
|
-
};
|
|
348
|
-
const tokens_ = 'accessToken' in body.tokens ? body.tokens : tokens(body.tokens);
|
|
349
|
-
const claims = JSON.parse(Base64.toString(tokens_.idToken.split('.')[1]));
|
|
350
|
-
return {
|
|
351
|
-
...tokens_,
|
|
352
|
-
account: Key.toAccount(key),
|
|
353
|
-
key,
|
|
354
|
-
user: { email: claims.email, id: claims.sub },
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
/** Reads a JSON response, and throws a {@link RequestError} for a refused request. */
|
|
358
|
-
async function json(response) {
|
|
359
|
-
// Some refusals, such as the CSRF check's, answer plain text.
|
|
360
|
-
const body = (await response.json().catch(() => ({})));
|
|
361
|
-
if (!response.ok)
|
|
362
|
-
throw new RequestError({
|
|
363
|
-
message: body.error ?? 'Something went wrong.',
|
|
364
|
-
status: response.status,
|
|
365
|
-
});
|
|
366
|
-
return body;
|
|
367
|
-
}
|
|
368
|
-
/** Posts JSON to Tempo Auth's API with the session cookie. */
|
|
369
|
-
function post(auth, path, options) {
|
|
370
|
-
return (auth.fetch ?? fetch)(new URL(path, auth.url), {
|
|
371
|
-
...(options.json === undefined
|
|
372
|
-
? {}
|
|
373
|
-
: {
|
|
374
|
-
body: JSON.stringify(options.json),
|
|
375
|
-
headers: { 'content-type': 'application/json' },
|
|
376
|
-
}),
|
|
377
|
-
credentials: 'include',
|
|
378
|
-
method: 'POST',
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
/** Where a redirect keeps its state: the instance's `storage`, or the page's session storage. */
|
|
382
|
-
function storage(auth) {
|
|
383
|
-
return auth.storage ?? sessionStorage;
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* Calls Tempo Auth's OAuth token endpoint, adding the client and any secret. It forwards standard
|
|
387
|
-
* OAuth form posts to WorkOS.
|
|
388
|
-
*/
|
|
389
|
-
async function token(auth, body) {
|
|
390
|
-
return json(await (auth.fetch ?? fetch)(new URL('/oauth2/token', auth.url), {
|
|
391
|
-
body: new URLSearchParams({
|
|
392
|
-
...body,
|
|
393
|
-
client_id: auth.clientId,
|
|
394
|
-
...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}),
|
|
395
|
-
}),
|
|
396
|
-
method: 'POST',
|
|
397
|
-
}));
|
|
398
|
-
}
|
|
399
|
-
/** Reads a token response into `Tokens`. */
|
|
400
|
-
function tokens(response) {
|
|
401
|
-
return {
|
|
402
|
-
accessToken: response.access_token,
|
|
403
|
-
expiresAt: Date.now() + response.expires_in * 1000,
|
|
404
|
-
idToken: response.id_token,
|
|
405
|
-
refreshToken: response.refresh_token,
|
|
406
|
-
};
|
|
68
|
+
await Api.json(await Api.post(auth, '/api/auth/logout', { json: {} }));
|
|
407
69
|
}
|
package/dist/Auth.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export type Auth = {
|
|
|
44
44
|
};
|
|
45
45
|
/** What an app's access key may do, as viem's `accessKey.signAuthorization` takes it. */
|
|
46
46
|
export type AccessKey = Pick<KeyAuthorization.KeyAuthorization, 'limits' | 'scopes'> & {
|
|
47
|
-
/** The user's Tempo Account the key acts for, from `
|
|
47
|
+
/** The user's Tempo Account the key acts for, from `Actions.tempoAccount.list`. Defaults to their first. */
|
|
48
48
|
account?: `0x${string}` | undefined;
|
|
49
49
|
/** The chain the key acts on, such as `tempoTestnet.id`. */
|
|
50
50
|
chainId: number;
|
|
@@ -83,13 +83,13 @@ export type User = {
|
|
|
83
83
|
/** The user's ID, the `sub` of every token. */
|
|
84
84
|
id: string;
|
|
85
85
|
};
|
|
86
|
-
/** An OAuth provider's short name, as `Actions.
|
|
86
|
+
/** An OAuth provider's short name, as `Actions.provider.list` lists them. */
|
|
87
87
|
export type Provider = 'apple' | 'bitbucket' | 'github' | 'gitlab' | 'google' | 'intuit' | 'linkedin' | 'microsoft' | 'salesforce' | 'slack' | 'xero';
|
|
88
88
|
/** A step a sign-in needs before it finishes, such as the SSO connection an address requires. */
|
|
89
89
|
export type Step = {
|
|
90
90
|
/** The kind of step. */
|
|
91
91
|
type: 'sso';
|
|
92
|
-
/** Where `Actions.signIn` sends the browser to take the step. */
|
|
92
|
+
/** Where `Actions.provider.signIn` sends the browser to take the step. */
|
|
93
93
|
url: string;
|
|
94
94
|
};
|
|
95
95
|
/** A sign-in redirect in progress: what the app keeps until Tempo Auth redirects back. */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type AccessKey, type Auth, type Authorization } from '../Auth.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Starts the device grant for a CLI: the user approves `userCode` at `verificationUri` from any
|
|
4
|
+
* browser, and `wait` resolves once they do.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const device = await Actions.device.authorize(auth, { accessKey, device: Os.hostname() })
|
|
9
|
+
* console.log(`Open ${device.verificationUriComplete}`)
|
|
10
|
+
* const authorization = await device.wait()
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function authorize(auth: Auth, options: authorize.Options): Promise<authorize.ReturnType>;
|
|
14
|
+
export declare namespace authorize {
|
|
15
|
+
type Options = {
|
|
16
|
+
/** What the CLI's key may do. The approval page shows it beside the device. */
|
|
17
|
+
accessKey: AccessKey;
|
|
18
|
+
/** The name the approval page shows, such as the machine's hostname. */
|
|
19
|
+
device: string;
|
|
20
|
+
};
|
|
21
|
+
type ReturnType = {
|
|
22
|
+
/** Seconds until the code expires. */
|
|
23
|
+
expiresIn: number;
|
|
24
|
+
/** The code the user confirms in the browser. */
|
|
25
|
+
userCode: string;
|
|
26
|
+
/** Where the user enters the code. */
|
|
27
|
+
verificationUri: string;
|
|
28
|
+
/** Where the user approves the code, with it already filled in. */
|
|
29
|
+
verificationUriComplete: string;
|
|
30
|
+
/** Resolves with the authorization once the user approves. */
|
|
31
|
+
wait: () => Promise<Authorization>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Secp256k1 } from 'viem/tempo';
|
|
2
|
+
import { RequestError, } from '../Auth.js';
|
|
3
|
+
import * as Api from '../internal/api.js';
|
|
4
|
+
import { request } from '../internal/request.js';
|
|
5
|
+
/**
|
|
6
|
+
* Starts the device grant for a CLI: the user approves `userCode` at `verificationUri` from any
|
|
7
|
+
* browser, and `wait` resolves once they do.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const device = await Actions.device.authorize(auth, { accessKey, device: Os.hostname() })
|
|
12
|
+
* console.log(`Open ${device.verificationUriComplete}`)
|
|
13
|
+
* const authorization = await device.wait()
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export async function authorize(auth, options) {
|
|
17
|
+
const privateKey = Secp256k1.randomPrivateKey();
|
|
18
|
+
const code = await Api.json(await Api.post(auth, '/api/device/authorize', {
|
|
19
|
+
json: {
|
|
20
|
+
device: options.device,
|
|
21
|
+
keyAuthorization: request(auth, { ...options, privateKey })
|
|
22
|
+
.keyAuthorization,
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
/** Polls until the user approves or denies the code, or it expires. */
|
|
26
|
+
async function wait() {
|
|
27
|
+
let interval = code.interval;
|
|
28
|
+
const deadline = Date.now() + code.expires_in * 1000;
|
|
29
|
+
while (Date.now() < deadline) {
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
31
|
+
const response = await Api.post(auth, '/api/device/token', {
|
|
32
|
+
json: { device_code: code.device_code },
|
|
33
|
+
});
|
|
34
|
+
const body = (await response.json());
|
|
35
|
+
if ('access_token' in body)
|
|
36
|
+
return Api.authorization({
|
|
37
|
+
account: body.account,
|
|
38
|
+
keyAuthorization: body.key_authorization,
|
|
39
|
+
tokens: Api.tokens(body),
|
|
40
|
+
}, { privateKey });
|
|
41
|
+
// RFC 8628 asks a client told to slow down to add 5 seconds to its interval.
|
|
42
|
+
if (body.error === 'slow_down')
|
|
43
|
+
interval += 5;
|
|
44
|
+
else if (body.error !== 'authorization_pending')
|
|
45
|
+
throw new RequestError({
|
|
46
|
+
message: `Sign-in failed: ${body.error}.`,
|
|
47
|
+
status: response.status,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new RequestError({ message: 'The code expired.', status: 400 });
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
expiresIn: code.expires_in,
|
|
54
|
+
userCode: code.user_code,
|
|
55
|
+
verificationUri: code.verification_uri,
|
|
56
|
+
verificationUriComplete: code.verification_uri_complete,
|
|
57
|
+
wait,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AccessKey, Auth, Authorization, Step } from '../Auth.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Emails the user a sign-in code. The response is the same for every address.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* await Actions.email.sendCode(auth, { email: 'user@example.com' })
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export declare function sendCode(auth: Auth, options: sendCode.Options): Promise<void>;
|
|
11
|
+
export declare namespace sendCode {
|
|
12
|
+
type Options = {
|
|
13
|
+
/** The user's email address. */
|
|
14
|
+
email: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Signs the user in with the code they were emailed, and authorizes the app in the same call.
|
|
19
|
+
* Returns a {@link Step} when the address requires SSO, which `Actions.provider.signIn` continues.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const result = await Actions.email.signIn(auth, { accessKey, code, email })
|
|
24
|
+
* if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function signIn(auth: Auth, options: signIn.Options): Promise<signIn.ReturnType>;
|
|
28
|
+
export declare namespace signIn {
|
|
29
|
+
type Options = {
|
|
30
|
+
/** What the app's key may do. It must fit the headless ceiling. */
|
|
31
|
+
accessKey: AccessKey;
|
|
32
|
+
/** The 6-digit code the user was emailed. */
|
|
33
|
+
code: string;
|
|
34
|
+
/** The address the code was sent to. */
|
|
35
|
+
email: string;
|
|
36
|
+
};
|
|
37
|
+
/** The authorization, or the step the address requires first. */
|
|
38
|
+
type ReturnType = Authorization | {
|
|
39
|
+
step: Step;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the user in with a code emailed to them, from the app's own sign-in form.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { Secp256k1 } from 'viem/tempo';
|
|
6
|
+
import * as Api from '../internal/api.js';
|
|
7
|
+
import { request } from '../internal/request.js';
|
|
8
|
+
/**
|
|
9
|
+
* Emails the user a sign-in code. The response is the same for every address.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* await Actions.email.sendCode(auth, { email: 'user@example.com' })
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export async function sendCode(auth, options) {
|
|
17
|
+
await Api.json(await Api.post(auth, '/api/auth/code', { json: options }));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Signs the user in with the code they were emailed, and authorizes the app in the same call.
|
|
21
|
+
* Returns a {@link Step} when the address requires SSO, which `Actions.provider.signIn` continues.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const result = await Actions.email.signIn(auth, { accessKey, code, email })
|
|
26
|
+
* if ('step' in result) Actions.provider.signIn(auth, { url: result.step.url })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function signIn(auth, options) {
|
|
30
|
+
const { accessKey, code, email } = options;
|
|
31
|
+
const privateKey = Secp256k1.randomPrivateKey();
|
|
32
|
+
const body = await Api.json(await Api.post(auth, '/api/auth/code/verify', {
|
|
33
|
+
json: {
|
|
34
|
+
authorize: request(auth, { accessKey, privateKey }),
|
|
35
|
+
code,
|
|
36
|
+
email,
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
if ('step' in body)
|
|
40
|
+
return {
|
|
41
|
+
step: { ...body.step, url: new URL(body.step.url, auth.url).href },
|
|
42
|
+
};
|
|
43
|
+
return Api.authorization(body.authorization, { privateKey });
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the user in to Tempo Auth through an OAuth provider or an SSO connection, from the app's
|
|
3
|
+
* own sign-in buttons.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { Auth, Provider } from '../Auth.ts';
|
|
7
|
+
/**
|
|
8
|
+
* Lists the OAuth providers Tempo Auth offers, for an app that shows its own sign-in buttons.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const providers = await Actions.provider.list(auth)
|
|
13
|
+
* // [{ id: 'google', name: 'Google' }]
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare function list(auth: Auth): Promise<list.ReturnType>;
|
|
17
|
+
export declare namespace list {
|
|
18
|
+
type ReturnType = readonly {
|
|
19
|
+
/** The provider's short name, which `signIn` takes. */
|
|
20
|
+
id: Provider;
|
|
21
|
+
/** The name to show, such as `Google`. */
|
|
22
|
+
name: string;
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Sends the browser to an OAuth provider, or to the SSO connection a `Step` names, and back to
|
|
27
|
+
* `returnTo` signed in to Tempo Auth. Unlike `Actions.email.signIn`, it does not authorize the
|
|
28
|
+
* app: the app calls `Actions.authorize` once the browser returns.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* Actions.provider.signIn(auth, { provider: 'google' })
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function signIn(auth: Auth, options: signIn.Options): void;
|
|
36
|
+
export declare namespace signIn {
|
|
37
|
+
type Options = ({
|
|
38
|
+
/** The OAuth provider to sign in with, from `list`. */
|
|
39
|
+
provider: Provider;
|
|
40
|
+
} | {
|
|
41
|
+
/** The URL of a `Step`, such as the SSO connection an address requires. */
|
|
42
|
+
url: string;
|
|
43
|
+
}) & {
|
|
44
|
+
/** Where to return. Defaults to the current page. It must be on an internal origin. */
|
|
45
|
+
returnTo?: string | undefined;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Api from '../internal/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Lists the OAuth providers Tempo Auth offers, for an app that shows its own sign-in buttons.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const providers = await Actions.provider.list(auth)
|
|
8
|
+
* // [{ id: 'google', name: 'Google' }]
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export async function list(auth) {
|
|
12
|
+
const { providers } = await Api.json(await (auth.fetch ?? fetch)(new URL('/api/auth/providers', auth.url)));
|
|
13
|
+
return providers;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Sends the browser to an OAuth provider, or to the SSO connection a `Step` names, and back to
|
|
17
|
+
* `returnTo` signed in to Tempo Auth. Unlike `Actions.email.signIn`, it does not authorize the
|
|
18
|
+
* app: the app calls `Actions.authorize` once the browser returns.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* Actions.provider.signIn(auth, { provider: 'google' })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function signIn(auth, options) {
|
|
26
|
+
const url = 'url' in options
|
|
27
|
+
? new URL(options.url)
|
|
28
|
+
: new URL(`/api/auth/oauth/${options.provider}`, auth.url);
|
|
29
|
+
url.searchParams.set('return_to', options.returnTo ?? window.location.href);
|
|
30
|
+
window.location.assign(url);
|
|
31
|
+
}
|