@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type AccessKey, type Auth, type Authorization, type Pending } from '../Auth.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Finishes a {@link start} when the page loads with Tempo Auth's callback, and returns `undefined`
|
|
4
|
+
* otherwise. The code is redeemed once, however often the page calls this.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const authorization =
|
|
9
|
+
* (await Actions.redirect.complete(auth)) ??
|
|
10
|
+
* (await Actions.authorize(auth, { accessKey }))
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare function complete(auth: Auth): Promise<complete.ReturnType>;
|
|
14
|
+
export declare namespace complete {
|
|
15
|
+
/** The authorization, or `undefined` when the page is not a redirect's callback. */
|
|
16
|
+
type ReturnType = Authorization | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns the URL that signs the user in on Tempo Auth's pages and redirects back with a code,
|
|
20
|
+
* and the {@link Pending} state to keep until then, such as in a sealed cookie.
|
|
21
|
+
* {@link handleCallback} redeems the code for the tokens and the key authorization.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
26
|
+
* accessKey,
|
|
27
|
+
* redirectUri: 'https://app.example/auth/callback',
|
|
28
|
+
* })
|
|
29
|
+
* await seal(c, 'oauth', pending)
|
|
30
|
+
* return c.redirect(url)
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function createUrl(auth: Auth, options: createUrl.Options): Promise<createUrl.ReturnType>;
|
|
34
|
+
export declare namespace createUrl {
|
|
35
|
+
type Options = {
|
|
36
|
+
/** What the app's key may do. Tempo Auth's pages show it, and the user approves it. */
|
|
37
|
+
accessKey: AccessKey;
|
|
38
|
+
/** Where Tempo Auth redirects back to. Defaults to the instance's `redirectUri`. */
|
|
39
|
+
redirectUri?: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
type ReturnType = {
|
|
42
|
+
/** What to keep until the callback, such as in a sealed cookie. */
|
|
43
|
+
pending: Pending;
|
|
44
|
+
/** Where to send the browser. */
|
|
45
|
+
url: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Finishes a sign-in redirect: checks its state, and redeems the code with the PKCE verifier (and
|
|
50
|
+
* the client secret, for a backend) for the tokens and the key authorization.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* const authorization = await Actions.redirect.handleCallback(auth, {
|
|
55
|
+
* pending: await unseal(c, 'oauth'),
|
|
56
|
+
* url: c.req.url,
|
|
57
|
+
* })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function handleCallback(auth: Auth, options: handleCallback.Options): Promise<Authorization>;
|
|
61
|
+
export declare namespace handleCallback {
|
|
62
|
+
type Options = {
|
|
63
|
+
/** What `createUrl` returned to keep. */
|
|
64
|
+
pending: Pending;
|
|
65
|
+
/** The callback URL Tempo Auth redirected to. */
|
|
66
|
+
url: string | URL;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sends the browser to Tempo Auth's pages to sign in, keeping the redirect's state in `storage`.
|
|
71
|
+
* {@link complete} finishes it when the browser returns.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* await Actions.redirect.start(auth, { accessKey })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export declare function start(auth: Auth, options: start.Options): Promise<void>;
|
|
79
|
+
export declare namespace start {
|
|
80
|
+
type Options = {
|
|
81
|
+
/** What the app's key may do. Tempo Auth's pages show it, and the user approves it. */
|
|
82
|
+
accessKey: AccessKey;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signs the user in on Tempo Auth's pages, which redirect back with a code. `start` and `complete`
|
|
3
|
+
* keep the redirect's state in `storage` for a browser app. `createUrl` and `handleCallback` hand
|
|
4
|
+
* it to the app to keep, for a backend or a native app.
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { Base64, Bytes, Hash } from 'ox';
|
|
8
|
+
import { Secp256k1 } from 'viem/tempo';
|
|
9
|
+
import { RequestError, } from '../Auth.js';
|
|
10
|
+
import * as Api from '../internal/api.js';
|
|
11
|
+
import { request } from '../internal/request.js';
|
|
12
|
+
/**
|
|
13
|
+
* Finishes a {@link start} when the page loads with Tempo Auth's callback, and returns `undefined`
|
|
14
|
+
* otherwise. The code is redeemed once, however often the page calls this.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const authorization =
|
|
19
|
+
* (await Actions.redirect.complete(auth)) ??
|
|
20
|
+
* (await Actions.authorize(auth, { accessKey }))
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function complete(auth) {
|
|
24
|
+
completed ??= (async () => {
|
|
25
|
+
const stored = storage(auth).getItem(storageKey);
|
|
26
|
+
storage(auth).removeItem(storageKey);
|
|
27
|
+
const url = new URL(window.location.href);
|
|
28
|
+
if (!url.searchParams.has('code'))
|
|
29
|
+
return undefined;
|
|
30
|
+
// The app keeps its own query and fragment, without the callback's parameters. A code this page
|
|
31
|
+
// did not request is dropped too, since it redeems for whoever crafted its request.
|
|
32
|
+
const clean = new URL(url);
|
|
33
|
+
for (const name of ['code', 'state'])
|
|
34
|
+
clean.searchParams.delete(name);
|
|
35
|
+
window.history.replaceState(null, '', clean.pathname + clean.search + clean.hash);
|
|
36
|
+
if (!stored)
|
|
37
|
+
return undefined;
|
|
38
|
+
return handleCallback(auth, { pending: JSON.parse(stored), url });
|
|
39
|
+
})();
|
|
40
|
+
return completed;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns the URL that signs the user in on Tempo Auth's pages and redirects back with a code,
|
|
44
|
+
* and the {@link Pending} state to keep until then, such as in a sealed cookie.
|
|
45
|
+
* {@link handleCallback} redeems the code for the tokens and the key authorization.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const { pending, url } = await Actions.redirect.createUrl(auth, {
|
|
50
|
+
* accessKey,
|
|
51
|
+
* redirectUri: 'https://app.example/auth/callback',
|
|
52
|
+
* })
|
|
53
|
+
* await seal(c, 'oauth', pending)
|
|
54
|
+
* return c.redirect(url)
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export async function createUrl(auth, options) {
|
|
58
|
+
const redirectUri = options.redirectUri ?? auth.redirectUri ?? `${window.location.origin}/`;
|
|
59
|
+
const pending = {
|
|
60
|
+
privateKey: Secp256k1.randomPrivateKey(),
|
|
61
|
+
redirectUri,
|
|
62
|
+
// `Bytes.random` needs only `crypto.getRandomValues`, which React Native polyfills.
|
|
63
|
+
state: Base64.fromBytes(Bytes.random(16), base64url),
|
|
64
|
+
verifier: Base64.fromBytes(Bytes.random(32), base64url),
|
|
65
|
+
};
|
|
66
|
+
const { client_id, keyAuthorization, scope } = request(auth, {
|
|
67
|
+
accessKey: options.accessKey,
|
|
68
|
+
privateKey: pending.privateKey,
|
|
69
|
+
});
|
|
70
|
+
const url = new URL('/authorize', auth.url);
|
|
71
|
+
url.search = new URLSearchParams({
|
|
72
|
+
client_id,
|
|
73
|
+
code_challenge: Base64.fromBytes(Hash.sha256(Bytes.fromString(pending.verifier)), base64url),
|
|
74
|
+
key_authorization: Base64.fromString(JSON.stringify(keyAuthorization), base64url),
|
|
75
|
+
redirect_uri: redirectUri,
|
|
76
|
+
scope,
|
|
77
|
+
state: pending.state,
|
|
78
|
+
}).toString();
|
|
79
|
+
return { pending, url: url.href };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finishes a sign-in redirect: checks its state, and redeems the code with the PKCE verifier (and
|
|
83
|
+
* the client secret, for a backend) for the tokens and the key authorization.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const authorization = await Actions.redirect.handleCallback(auth, {
|
|
88
|
+
* pending: await unseal(c, 'oauth'),
|
|
89
|
+
* url: c.req.url,
|
|
90
|
+
* })
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export async function handleCallback(auth, options) {
|
|
94
|
+
const { pending } = options;
|
|
95
|
+
const params = new URL(options.url).searchParams;
|
|
96
|
+
const code = params.get('code');
|
|
97
|
+
if (!code || params.get('state') !== pending.state)
|
|
98
|
+
throw new RequestError({
|
|
99
|
+
message: 'Sign-in failed. Start again.',
|
|
100
|
+
status: 400,
|
|
101
|
+
});
|
|
102
|
+
const body = await Api.token(auth, {
|
|
103
|
+
code,
|
|
104
|
+
code_verifier: pending.verifier,
|
|
105
|
+
grant_type: 'authorization_code',
|
|
106
|
+
redirect_uri: pending.redirectUri,
|
|
107
|
+
});
|
|
108
|
+
if (!body.account || !body.key_authorization)
|
|
109
|
+
throw new RequestError({
|
|
110
|
+
message: 'Sign-in failed. Start again.',
|
|
111
|
+
status: 400,
|
|
112
|
+
});
|
|
113
|
+
return Api.authorization({
|
|
114
|
+
account: body.account,
|
|
115
|
+
keyAuthorization: body.key_authorization,
|
|
116
|
+
tokens: body,
|
|
117
|
+
}, pending);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Sends the browser to Tempo Auth's pages to sign in, keeping the redirect's state in `storage`.
|
|
121
|
+
* {@link complete} finishes it when the browser returns.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* await Actions.redirect.start(auth, { accessKey })
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export async function start(auth, options) {
|
|
129
|
+
const { pending, url } = await createUrl(auth, options);
|
|
130
|
+
storage(auth).setItem(storageKey, JSON.stringify(pending));
|
|
131
|
+
window.location.assign(url);
|
|
132
|
+
}
|
|
133
|
+
/** Unpadded URL-safe Base64, as PKCE and `key_authorization` use. */
|
|
134
|
+
const base64url = { pad: false, url: true };
|
|
135
|
+
/** The redirect this page load returns from, so its code is redeemed once. */
|
|
136
|
+
let completed;
|
|
137
|
+
/** Where a redirect keeps its state in `storage`. */
|
|
138
|
+
const storageKey = 'tempo-auth:redirect';
|
|
139
|
+
/** Where a redirect keeps its state: the instance's `storage`, or the page's session storage. */
|
|
140
|
+
function storage(auth) {
|
|
141
|
+
return auth.storage ?? sessionStorage;
|
|
142
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages the signed-in user's Tempo Accounts.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import type { Auth, TempoAccount } from '../Auth.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Creates another Tempo Account for the signed-in user, and returns it. Pass its `address` as an
|
|
8
|
+
* access key's `account` to authorize keys on it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const tempoAccount = await Actions.tempoAccount.create(auth)
|
|
13
|
+
* await Actions.authorize(auth, {
|
|
14
|
+
* accessKey: { ...accessKey, account: tempoAccount.address },
|
|
15
|
+
* })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function create(auth: Auth): Promise<create.ReturnType>;
|
|
19
|
+
export declare namespace create {
|
|
20
|
+
/** The new account. */
|
|
21
|
+
type ReturnType = TempoAccount;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Lists the signed-in user's Tempo Accounts, their default first.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const [account, ...others] = await Actions.tempoAccount.list(auth)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function list(auth: Auth): Promise<list.ReturnType>;
|
|
32
|
+
export declare namespace list {
|
|
33
|
+
/** The user's accounts, their default first. */
|
|
34
|
+
type ReturnType = readonly TempoAccount[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Api from '../internal/api.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates another Tempo Account for the signed-in user, and returns it. Pass its `address` as an
|
|
4
|
+
* access key's `account` to authorize keys on it.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const tempoAccount = await Actions.tempoAccount.create(auth)
|
|
9
|
+
* await Actions.authorize(auth, {
|
|
10
|
+
* accessKey: { ...accessKey, account: tempoAccount.address },
|
|
11
|
+
* })
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export async function create(auth) {
|
|
15
|
+
const { address } = await Api.json(await Api.post(auth, '/api/account/tempo', { json: {} }));
|
|
16
|
+
return { address };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Lists the signed-in user's Tempo Accounts, their default first.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const [account, ...others] = await Actions.tempoAccount.list(auth)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export async function list(auth) {
|
|
27
|
+
const { tempo } = await Api.json(await (auth.fetch ?? fetch)(new URL('/api/account', auth.url), {
|
|
28
|
+
credentials: 'include',
|
|
29
|
+
}));
|
|
30
|
+
return tempo.map(({ address }) => ({ address }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls Tempo Auth's API, and reads its responses into what the actions return.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { type Hex } from 'ox';
|
|
6
|
+
import type { KeyAuthorization } from 'ox/tempo';
|
|
7
|
+
import { type Auth, type Authorization, type Tokens } from '../Auth.ts';
|
|
8
|
+
import * as Key from '../Key.ts';
|
|
9
|
+
/** Builds an authorization from what Tempo Auth returned and the key the app generated. */
|
|
10
|
+
export declare function authorization(body: Headless, options: {
|
|
11
|
+
privateKey: Hex.Hex;
|
|
12
|
+
}): Authorization;
|
|
13
|
+
/** What the headless API returns for an authorization. */
|
|
14
|
+
export type Headless = {
|
|
15
|
+
account: Key.Key['account'];
|
|
16
|
+
keyAuthorization: KeyAuthorization.Rpc;
|
|
17
|
+
tokens: TokenResponse | Tokens;
|
|
18
|
+
};
|
|
19
|
+
/** Reads a JSON response, and throws a {@link RequestError} for a refused request. */
|
|
20
|
+
export declare function json<value = unknown>(response: Response): Promise<value>;
|
|
21
|
+
/** Posts JSON to Tempo Auth's API with the session cookie. */
|
|
22
|
+
export declare function post(auth: Auth, path: string, options: {
|
|
23
|
+
json?: unknown;
|
|
24
|
+
}): Promise<Response>;
|
|
25
|
+
/**
|
|
26
|
+
* Calls Tempo Auth's OAuth token endpoint, adding the client and any secret. It forwards standard
|
|
27
|
+
* OAuth form posts to WorkOS.
|
|
28
|
+
*/
|
|
29
|
+
export declare function token(auth: Auth, body: Record<string, string>): Promise<TokenResponse>;
|
|
30
|
+
/** What Tempo Auth's token endpoint returns. Redeeming a redirect's code adds its key authorization. */
|
|
31
|
+
export type TokenResponse = {
|
|
32
|
+
access_token: string;
|
|
33
|
+
account?: Key.Key['account'] | undefined;
|
|
34
|
+
expires_in: number;
|
|
35
|
+
id_token?: string | undefined;
|
|
36
|
+
key_authorization?: KeyAuthorization.Rpc | undefined;
|
|
37
|
+
refresh_token?: string | undefined;
|
|
38
|
+
};
|
|
39
|
+
/** Reads a token response into `Tokens`. */
|
|
40
|
+
export declare function tokens(response: TokenResponse): Tokens;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls Tempo Auth's API, and reads its responses into what the actions return.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import { Base64 } from 'ox';
|
|
6
|
+
import { RequestError, } from '../Auth.js';
|
|
7
|
+
import * as Key from '../Key.js';
|
|
8
|
+
/** Builds an authorization from what Tempo Auth returned and the key the app generated. */
|
|
9
|
+
export function authorization(body, options) {
|
|
10
|
+
const key = {
|
|
11
|
+
account: body.account,
|
|
12
|
+
keyAuthorization: body.keyAuthorization,
|
|
13
|
+
privateKey: options.privateKey,
|
|
14
|
+
};
|
|
15
|
+
const tokens_ = 'accessToken' in body.tokens ? body.tokens : tokens(body.tokens);
|
|
16
|
+
const claims = JSON.parse(Base64.toString(tokens_.idToken.split('.')[1]));
|
|
17
|
+
return {
|
|
18
|
+
...tokens_,
|
|
19
|
+
account: Key.toAccount(key),
|
|
20
|
+
key,
|
|
21
|
+
user: { email: claims.email, id: claims.sub },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Reads a JSON response, and throws a {@link RequestError} for a refused request. */
|
|
25
|
+
export async function json(response) {
|
|
26
|
+
// Some refusals, such as the CSRF check's, answer plain text.
|
|
27
|
+
const body = (await response.json().catch(() => ({})));
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new RequestError({
|
|
30
|
+
message: body.error ?? 'Something went wrong.',
|
|
31
|
+
status: response.status,
|
|
32
|
+
});
|
|
33
|
+
return body;
|
|
34
|
+
}
|
|
35
|
+
/** Posts JSON to Tempo Auth's API with the session cookie. */
|
|
36
|
+
export function post(auth, path, options) {
|
|
37
|
+
return (auth.fetch ?? fetch)(new URL(path, auth.url), {
|
|
38
|
+
...(options.json === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: {
|
|
41
|
+
body: JSON.stringify(options.json),
|
|
42
|
+
headers: { 'content-type': 'application/json' },
|
|
43
|
+
}),
|
|
44
|
+
credentials: 'include',
|
|
45
|
+
method: 'POST',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Calls Tempo Auth's OAuth token endpoint, adding the client and any secret. It forwards standard
|
|
50
|
+
* OAuth form posts to WorkOS.
|
|
51
|
+
*/
|
|
52
|
+
export async function token(auth, body) {
|
|
53
|
+
return json(await (auth.fetch ?? fetch)(new URL('/oauth2/token', auth.url), {
|
|
54
|
+
body: new URLSearchParams({
|
|
55
|
+
...body,
|
|
56
|
+
client_id: auth.clientId,
|
|
57
|
+
...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}),
|
|
58
|
+
}),
|
|
59
|
+
method: 'POST',
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/** Reads a token response into `Tokens`. */
|
|
63
|
+
export function tokens(response) {
|
|
64
|
+
return {
|
|
65
|
+
accessToken: response.access_token,
|
|
66
|
+
expiresAt: Date.now() + response.expires_in * 1000,
|
|
67
|
+
idToken: response.id_token,
|
|
68
|
+
refreshToken: response.refresh_token,
|
|
69
|
+
};
|
|
70
|
+
}
|
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-22.sha-291bbcb",
|
|
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": {
|