@tempoxyz/example-auth-cli 0.0.0-pr-5.sha-414467e → 0.0.0-pr-6.sha-b989a0e
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/dist/Session.js +46 -0
- package/dist/auth.config.js +11 -0
- package/dist/commands/fund.js +5 -6
- package/dist/commands/login.js +23 -54
- package/dist/commands/logout.js +3 -4
- package/dist/commands/send.js +5 -6
- package/dist/commands/whoami.js +12 -8
- package/package.json +3 -2
- package/dist/Credentials.js +0 -81
- package/dist/TempoAccount.js +0 -39
package/dist/Session.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's saved sign-in: the person, their tokens, and the CLI's access key, in a file only the
|
|
3
|
+
* current user can read. `TEMPO_AUTH_CREDENTIALS` moves the file.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import * as Fs from 'node:fs/promises';
|
|
7
|
+
import * as Os from 'node:os';
|
|
8
|
+
import * as Path from 'node:path';
|
|
9
|
+
import { Actions, Key } from '@tempoxyz/auth';
|
|
10
|
+
import { auth } from './auth.config.js';
|
|
11
|
+
/** Where the sign-in is saved. */
|
|
12
|
+
const file = process.env.TEMPO_AUTH_CREDENTIALS ??
|
|
13
|
+
Path.join(Os.homedir(), '.config', 'tempo-auth', 'credentials.json');
|
|
14
|
+
/**
|
|
15
|
+
* Returns the saved sign-in, refreshing its access token when it has expired, or `undefined`.
|
|
16
|
+
* A sign-in saved by an older CLI, or whose key has expired, counts as signed out, so `login`
|
|
17
|
+
* authorizes a new key.
|
|
18
|
+
*/
|
|
19
|
+
export async function get() {
|
|
20
|
+
const saved = await Fs.readFile(file, 'utf8')
|
|
21
|
+
.then((text) => JSON.parse(text))
|
|
22
|
+
.catch(() => undefined);
|
|
23
|
+
// Earlier CLIs saved no `key`, or kept the tokens at the top level, so those count as signed out.
|
|
24
|
+
if (!saved?.key?.keyAuthorization ||
|
|
25
|
+
!saved.tokens?.accessToken ||
|
|
26
|
+
Key.expired(saved.key))
|
|
27
|
+
return undefined;
|
|
28
|
+
if (saved.tokens.expiresAt - 30_000 > Date.now())
|
|
29
|
+
return saved;
|
|
30
|
+
const tokens = await Actions.refresh(auth, {
|
|
31
|
+
refreshToken: saved.tokens.refreshToken,
|
|
32
|
+
}).catch(() => undefined);
|
|
33
|
+
if (!tokens)
|
|
34
|
+
return undefined;
|
|
35
|
+
return save({ ...saved, tokens });
|
|
36
|
+
}
|
|
37
|
+
/** Forgets the saved sign-in. */
|
|
38
|
+
export async function remove() {
|
|
39
|
+
await Fs.rm(file, { force: true });
|
|
40
|
+
}
|
|
41
|
+
/** Saves a sign-in, readable only by the current user. */
|
|
42
|
+
export async function save(session) {
|
|
43
|
+
await Fs.mkdir(Path.dirname(file), { recursive: true });
|
|
44
|
+
await Fs.writeFile(file, JSON.stringify(session), { mode: 0o600 });
|
|
45
|
+
return session;
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tempo Auth instance the CLI signs in through, configured from the environment.
|
|
3
|
+
* `TEMPO_AUTH_URL` points it at a deployment, such as a pull request's preview.
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { Auth } from '@tempoxyz/auth';
|
|
7
|
+
export const auth = Auth.create({
|
|
8
|
+
// The `tempo-cli` Connect app in the staging environment, which refreshes with its client ID alone.
|
|
9
|
+
clientId: process.env.CLI_CLIENT_ID ?? 'client_01M4N1K6HAT9QWVT2V59NNY48J',
|
|
10
|
+
url: process.env.TEMPO_AUTH_URL ?? 'http://localhost:5173',
|
|
11
|
+
});
|
package/dist/commands/fund.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/** Funds the person's Tempo Account from the Tempo testnet faucet, including pathUSD for fees. @module */
|
|
2
2
|
import { Cli } from 'incur';
|
|
3
|
-
import * as
|
|
3
|
+
import * as Session from '../Session.js';
|
|
4
4
|
import { client } from '../viem.config.js';
|
|
5
5
|
export default Cli.command({
|
|
6
6
|
description: 'Fund your Tempo Account with testnet tokens',
|
|
7
|
-
env: Credentials.env,
|
|
8
7
|
async run(c) {
|
|
9
|
-
const
|
|
10
|
-
if (!
|
|
8
|
+
const session = await Session.get();
|
|
9
|
+
if (!session)
|
|
11
10
|
return c.error({
|
|
12
11
|
code: 'SIGNED_OUT',
|
|
13
12
|
message: 'Not signed in. Run `login`.',
|
|
14
13
|
});
|
|
15
|
-
await client.faucet.fundSync({ account:
|
|
16
|
-
return { account:
|
|
14
|
+
await client.faucet.fundSync({ account: session.key.account });
|
|
15
|
+
return { account: session.key.account, funded: true };
|
|
17
16
|
},
|
|
18
17
|
});
|
package/dist/commands/login.js
CHANGED
|
@@ -4,65 +4,34 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import * as Os from 'node:os';
|
|
7
|
+
import { Actions } from '@tempoxyz/auth';
|
|
7
8
|
import { Cli } from 'incur';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import * as
|
|
9
|
+
import { Addresses, Expiry, Scopes } from 'viem/tempo';
|
|
10
|
+
import { auth } from '../auth.config.js';
|
|
11
|
+
import * as Session from '../Session.js';
|
|
12
|
+
import { client } from '../viem.config.js';
|
|
11
13
|
export default Cli.command({
|
|
12
14
|
description: 'Sign in by approving a code in any browser',
|
|
13
|
-
env: Credentials.env,
|
|
14
15
|
async *run(c) {
|
|
15
16
|
// The approval page shows the key's limits beside the device.
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
25
|
-
if (!authorized.ok)
|
|
26
|
-
return c.error({ code: 'AUTHORIZE', message: 'Could not start sign-in.' });
|
|
27
|
-
const code = (await authorized.json());
|
|
28
|
-
yield `Open ${code.verification_uri_complete}`;
|
|
29
|
-
yield `and confirm the code ${code.user_code}.`;
|
|
30
|
-
let interval = code.interval;
|
|
31
|
-
const deadline = Date.now() + code.expires_in * 1000;
|
|
32
|
-
while (Date.now() < deadline) {
|
|
33
|
-
await new Promise((resolve) => setTimeout(resolve, interval * 1000));
|
|
34
|
-
const response = await fetch(new URL('/api/device/token', c.env.TEMPO_AUTH_URL), {
|
|
35
|
-
body: JSON.stringify({ device_code: code.device_code }),
|
|
36
|
-
headers: { 'content-type': 'application/json' },
|
|
37
|
-
method: 'POST',
|
|
38
|
-
});
|
|
39
|
-
const body = (await response.json());
|
|
40
|
-
if ('access_token' in body) {
|
|
41
|
-
const credentials = await Credentials.save(c.env, {
|
|
42
|
-
key: {
|
|
43
|
-
account: body.account,
|
|
44
|
-
keyAuthorization: body.key_authorization,
|
|
45
|
-
privateKey,
|
|
46
|
-
},
|
|
47
|
-
tokens: body,
|
|
48
|
-
});
|
|
49
|
-
const email = await Credentials.email(c.env, { credentials });
|
|
50
|
-
// A streaming command prints what it yields, not what it returns.
|
|
51
|
-
yield `Signed in as ${email}.`;
|
|
52
|
-
return c.ok({ email });
|
|
53
|
-
}
|
|
54
|
-
// RFC 8628 asks a client told to slow down to add 5 seconds to its interval.
|
|
55
|
-
if (body.error === 'slow_down')
|
|
56
|
-
interval += 5;
|
|
57
|
-
else if (body.error !== 'authorization_pending')
|
|
58
|
-
return c.error({
|
|
59
|
-
code: 'DENIED',
|
|
60
|
-
message: `Sign-in failed: ${body.error}.`,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
return c.error({
|
|
64
|
-
code: 'EXPIRED',
|
|
65
|
-
message: 'The code expired. Sign in again.',
|
|
17
|
+
const device = await Actions.authorizeDevice(auth, {
|
|
18
|
+
accessKey: {
|
|
19
|
+
chainId: client.chain.id,
|
|
20
|
+
expiry: Expiry.days(1),
|
|
21
|
+
limits: [{ limit: 10000000n, token: Addresses.pathUsd }],
|
|
22
|
+
scopes: [Scopes.tip20(Addresses.pathUsd).transfer()],
|
|
23
|
+
},
|
|
24
|
+
device: Os.hostname(),
|
|
66
25
|
});
|
|
26
|
+
yield `Open ${device.verificationUriComplete}`;
|
|
27
|
+
yield `and confirm the code ${device.userCode}.`;
|
|
28
|
+
const authorization = await device.wait().catch((error) => error);
|
|
29
|
+
if (authorization instanceof Error)
|
|
30
|
+
return c.error({ code: 'DENIED', message: authorization.message });
|
|
31
|
+
const { key, tokens, user } = authorization;
|
|
32
|
+
await Session.save({ key, tokens, user });
|
|
33
|
+
// A streaming command prints what it yields, not what it returns.
|
|
34
|
+
yield `Signed in as ${user.email}.`;
|
|
35
|
+
return c.ok({ email: user.email });
|
|
67
36
|
},
|
|
68
37
|
});
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/** Forgets the saved tokens. @module */
|
|
2
2
|
import { Cli } from 'incur';
|
|
3
|
-
import * as
|
|
3
|
+
import * as Session from '../Session.js';
|
|
4
4
|
export default Cli.command({
|
|
5
5
|
description: 'Sign out on this machine',
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
await Credentials.remove(c.env);
|
|
6
|
+
async run() {
|
|
7
|
+
await Session.remove();
|
|
9
8
|
return { signedOut: true };
|
|
10
9
|
},
|
|
11
10
|
});
|
package/dist/commands/send.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Sends pathUSD from the person's Tempo Account with the CLI's access key. @module */
|
|
2
|
+
import { Key } from '@tempoxyz/auth';
|
|
2
3
|
import { Cli, z } from 'incur';
|
|
3
4
|
import { Value } from 'ox';
|
|
4
5
|
import { Addresses } from 'viem/tempo';
|
|
5
|
-
import * as
|
|
6
|
-
import * as TempoAccount from '../TempoAccount.js';
|
|
6
|
+
import * as Session from '../Session.js';
|
|
7
7
|
import { client } from '../viem.config.js';
|
|
8
8
|
export default Cli.command({
|
|
9
9
|
args: z.object({
|
|
@@ -17,16 +17,15 @@ export default Cli.command({
|
|
|
17
17
|
.describe('Amount of pathUSD, such as 0.01'),
|
|
18
18
|
}),
|
|
19
19
|
description: 'Send pathUSD from your Tempo Account',
|
|
20
|
-
env: Credentials.env,
|
|
21
20
|
async run(c) {
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
21
|
+
const session = await Session.get();
|
|
22
|
+
if (!session)
|
|
24
23
|
return c.error({
|
|
25
24
|
code: 'SIGNED_OUT',
|
|
26
25
|
message: 'Not signed in. Run `login`.',
|
|
27
26
|
});
|
|
28
27
|
const { receipt } = await client.token.transferSync({
|
|
29
|
-
account:
|
|
28
|
+
account: Key.toAccount(session.key),
|
|
30
29
|
amount: Value.from(c.args.amount, 6),
|
|
31
30
|
to: c.args.to,
|
|
32
31
|
token: Addresses.pathUsd,
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/** Prints the signed-in person's email and Tempo Account, refreshing the saved tokens when needed. @module */
|
|
2
2
|
import { Cli } from 'incur';
|
|
3
|
-
import
|
|
3
|
+
import { auth } from '../auth.config.js';
|
|
4
|
+
import * as Session from '../Session.js';
|
|
4
5
|
export default Cli.command({
|
|
5
6
|
description: 'Show who is signed in',
|
|
6
|
-
env: Credentials.env,
|
|
7
7
|
async run(c) {
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
8
|
+
const session = await Session.get();
|
|
9
|
+
if (!session)
|
|
10
10
|
return c.error({
|
|
11
11
|
code: 'SIGNED_OUT',
|
|
12
12
|
message: 'Not signed in. Run `login`.',
|
|
13
13
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
14
|
+
// Tempo Auth's userinfo endpoint answers for the access token, as any Tempo API would.
|
|
15
|
+
const response = await fetch(new URL('/oauth2/userinfo', auth.url), {
|
|
16
|
+
headers: { authorization: `Bearer ${session.tokens.accessToken}` },
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
throw new Error(`userinfo answered ${response.status}.`);
|
|
20
|
+
const { email } = (await response.json());
|
|
21
|
+
return { account: session.key.account, email };
|
|
18
22
|
},
|
|
19
23
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempoxyz/example-auth-cli",
|
|
3
|
-
"version": "0.0.0-pr-
|
|
3
|
+
"version": "0.0.0-pr-6.sha-b989a0e",
|
|
4
4
|
"description": "Signs in to Tempo through Tempo Auth's device grant.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"incur": "0.5.1",
|
|
17
17
|
"ox": "0.14.56",
|
|
18
|
-
"viem": "https://pkg.pr.new/viem@0872b53"
|
|
18
|
+
"viem": "https://pkg.pr.new/viem@0872b53",
|
|
19
|
+
"@tempoxyz/auth": "0.0.0-pr-6.sha-b989a0e"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@types/node": "26.6.4",
|
package/dist/Credentials.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The tokens and access key the CLI keeps between runs, in a file only the current user can read.
|
|
3
|
-
* Access tokens expire after minutes, so `active` refreshes them, and refresh tokens rotate on
|
|
4
|
-
* every refresh.
|
|
5
|
-
* @module
|
|
6
|
-
*/
|
|
7
|
-
import * as Fs from 'node:fs/promises';
|
|
8
|
-
import * as Os from 'node:os';
|
|
9
|
-
import * as Path from 'node:path';
|
|
10
|
-
import { z } from 'incur';
|
|
11
|
-
import { KeyAuthorization } from 'ox/tempo';
|
|
12
|
-
/** Where Tempo Auth runs, the CLI's Connect app, and where tokens live. */
|
|
13
|
-
export const env = z.object({
|
|
14
|
-
TEMPO_AUTH_CREDENTIALS: z
|
|
15
|
-
.string()
|
|
16
|
-
.default(Path.join(Os.homedir(), '.config', 'tempo-auth', 'credentials.json')),
|
|
17
|
-
TEMPO_AUTH_URL: z.string().default('http://localhost:5173'),
|
|
18
|
-
// The `tempo-cli` Connect app in the staging environment, which refreshes with its client ID alone.
|
|
19
|
-
CLI_CLIENT_ID: z.string().default('client_01M4N1K6HAT9QWVT2V59NNY48J'),
|
|
20
|
-
});
|
|
21
|
-
/**
|
|
22
|
-
* Returns the saved tokens, refreshing the access token when it has expired, or `undefined`.
|
|
23
|
-
* Credentials without an access key, from an older CLI, or whose key has expired count as signed
|
|
24
|
-
* out, so `login` authorizes a new key.
|
|
25
|
-
*/
|
|
26
|
-
export async function active(env) {
|
|
27
|
-
const saved = await Fs.readFile(env.TEMPO_AUTH_CREDENTIALS, 'utf8')
|
|
28
|
-
// An older CLI saved no `key`, which this check catches at runtime.
|
|
29
|
-
.then((text) => JSON.parse(text))
|
|
30
|
-
.catch(() => undefined);
|
|
31
|
-
if (!saved?.key || expired(saved.key))
|
|
32
|
-
return undefined;
|
|
33
|
-
if (saved.expiresAt - 30_000 > Date.now())
|
|
34
|
-
return saved;
|
|
35
|
-
const response = await fetch(new URL('/oauth2/token', env.TEMPO_AUTH_URL), {
|
|
36
|
-
body: new URLSearchParams({
|
|
37
|
-
client_id: env.CLI_CLIENT_ID,
|
|
38
|
-
grant_type: 'refresh_token',
|
|
39
|
-
refresh_token: saved.refreshToken,
|
|
40
|
-
}),
|
|
41
|
-
method: 'POST',
|
|
42
|
-
});
|
|
43
|
-
if (!response.ok)
|
|
44
|
-
return undefined;
|
|
45
|
-
return save(env, {
|
|
46
|
-
key: saved.key,
|
|
47
|
-
tokens: (await response.json()),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
/** Reads the signed-in person's email from Tempo Auth's userinfo endpoint. */
|
|
51
|
-
export async function email(env, options) {
|
|
52
|
-
const response = await fetch(new URL('/oauth2/userinfo', env.TEMPO_AUTH_URL), {
|
|
53
|
-
headers: { authorization: `Bearer ${options.credentials.accessToken}` },
|
|
54
|
-
});
|
|
55
|
-
if (!response.ok)
|
|
56
|
-
throw new Error(`userinfo answered ${response.status}.`);
|
|
57
|
-
return (await response.json()).email;
|
|
58
|
-
}
|
|
59
|
-
/** Whether the key's authorization has expired. */
|
|
60
|
-
function expired(key) {
|
|
61
|
-
const { expiry } = KeyAuthorization.fromRpc(key.keyAuthorization);
|
|
62
|
-
return Boolean(expiry) && expiry * 1000 <= Date.now();
|
|
63
|
-
}
|
|
64
|
-
/** Forgets the saved tokens. */
|
|
65
|
-
export async function remove(env) {
|
|
66
|
-
await Fs.rm(env.TEMPO_AUTH_CREDENTIALS, { force: true });
|
|
67
|
-
}
|
|
68
|
-
/** Saves a token response and the access key, readable only by the current user. */
|
|
69
|
-
export async function save(env, options) {
|
|
70
|
-
const credentials = {
|
|
71
|
-
accessToken: options.tokens.access_token,
|
|
72
|
-
expiresAt: Date.now() + options.tokens.expires_in * 1000,
|
|
73
|
-
key: options.key,
|
|
74
|
-
refreshToken: options.tokens.refresh_token,
|
|
75
|
-
};
|
|
76
|
-
await Fs.mkdir(Path.dirname(env.TEMPO_AUTH_CREDENTIALS), { recursive: true });
|
|
77
|
-
await Fs.writeFile(env.TEMPO_AUTH_CREDENTIALS, JSON.stringify(credentials), {
|
|
78
|
-
mode: 0o600,
|
|
79
|
-
});
|
|
80
|
-
return credentials;
|
|
81
|
-
}
|
package/dist/TempoAccount.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The app's access key on a person's Tempo Account. The app holds the key, asks Tempo Auth for a
|
|
3
|
-
* key authorization for it, and sends with it on Tempo testnet. The first transaction carries the
|
|
4
|
-
* authorization, which registers the key.
|
|
5
|
-
* @module
|
|
6
|
-
*/
|
|
7
|
-
import { Hex } from 'ox';
|
|
8
|
-
import { KeyAuthorization } from 'ox/tempo';
|
|
9
|
-
import { Account, Addresses, KeyAuthorizationManager } from 'viem/tempo';
|
|
10
|
-
import { client } from './viem.config.js';
|
|
11
|
-
/**
|
|
12
|
-
* The key authorization request for `accessKey`, in the Accounts SDK's `wallet_connect` format: up
|
|
13
|
-
* to 10 pathUSD in transfers, for a day.
|
|
14
|
-
*/
|
|
15
|
-
export function getKeyAuthorizationRequest(accessKey) {
|
|
16
|
-
return {
|
|
17
|
-
address: accessKey.address,
|
|
18
|
-
chainId: Hex.fromNumber(client.chain.id),
|
|
19
|
-
expiry: Math.floor(Date.now() / 1000) + 24 * 60 * 60,
|
|
20
|
-
keyType: 'secp256k1',
|
|
21
|
-
limits: [{ limit: Hex.fromNumber(10000000n), token: Addresses.pathUsd }],
|
|
22
|
-
scopes: [
|
|
23
|
-
{ address: Addresses.pathUsd, selector: 'transfer(address,uint256)' },
|
|
24
|
-
],
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/** The key as a viem account acting for the Tempo Account. viem attaches the authorization until the chain registers the key. */
|
|
28
|
-
export function from(key) {
|
|
29
|
-
const keyAuthorizationManager = KeyAuthorizationManager.memory();
|
|
30
|
-
keyAuthorizationManager.set({
|
|
31
|
-
accessKey: Account.fromSecp256k1(key.privateKey).address,
|
|
32
|
-
address: key.account,
|
|
33
|
-
chainId: client.chain.id,
|
|
34
|
-
}, KeyAuthorization.fromRpc(key.keyAuthorization));
|
|
35
|
-
return Account.fromSecp256k1(key.privateKey, {
|
|
36
|
-
access: key.account,
|
|
37
|
-
keyAuthorizationManager,
|
|
38
|
-
});
|
|
39
|
-
}
|